Skip to content

Commit 40f251b

Browse files
apognuChibiBlasphem
authored andcommitted
Add DB fixtures for data model, and simple E2E test for the AST builder.
1 parent 5ed6ffa commit 40f251b

8 files changed

Lines changed: 364 additions & 15 deletions

File tree

.github/workflows/playground.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ jobs:
99
runs-on: ubuntu-latest
1010
steps:
1111
- uses: actions/checkout@v4
12-
- uses: actions/setup-node@v4
1312
- uses: pnpm/action-setup@v4
13+
- uses: actions/setup-node@v4
14+
with:
15+
cache: pnpm
1416
- run: pnpm install --frozen-lockfile
15-
- run: pnpm --filter tests exec playwright install
17+
- run: pnpm --filter tests exec playwright install chromium-headless-shell
1618
- run: pnpm --filter tests run test
1719
- uses: actions/upload-artifact@v4
1820
if: ${{ !cancelled() }}

packages/tests/e2e/custom-lists.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { waitForThen } from 'tests/common/utils';
44

55
test('Create new list', async ({ page }) => {
66
await page.goto('/lists');
7-
7+
88
const listName = crypto.randomUUID();
99
const values = Array.from({ length: 5 }, () => crypto.randomUUID());
1010

@@ -21,9 +21,10 @@ test('Create new list', async ({ page }) => {
2121
await page.waitForURL('/lists/**');
2222
await page.waitForLoadState();
2323

24+
2425
for (const value of values) {
2526
await waitForThen(page, page.getByRole('button', { name: 'New value' }), async (button) =>
26-
button.click(),
27+
await button.click(),
2728
);
2829

2930
await waitForThen(

packages/tests/e2e/fixtures.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import knex from 'knex';
2+
import crypto from 'crypto';
3+
4+
interface Org { id: string; }
5+
6+
interface Table {
7+
table: string;
8+
fields: { [key: string]: string};
9+
}
10+
11+
const TABLES: Table[] = [
12+
{
13+
table: 'transactions',
14+
fields: {
15+
'beneficiary': 'String',
16+
'amount': 'Float',
17+
}
18+
}
19+
]
20+
21+
export const setupFixtures = async (dsn: string, apiUrl: string) => {
22+
const sql = knex({ client: 'pg', connection: dsn });
23+
const org = await sql<Org>('organizations').select('id').where('name', 'Zorg').first();
24+
const apiKey = crypto.randomUUID();
25+
26+
const hash = crypto.createHash('sha256').update(apiKey).digest();
27+
28+
await sql('api_keys').insert({
29+
'org_id': org!.id,
30+
'role': 4,
31+
'prefix': apiKey.substring(0, 3),
32+
'key_hash': hash,
33+
});
34+
35+
apiUrl = `http://localhost:${apiUrl}`;
36+
37+
for (const spec of TABLES) {
38+
createTable(apiUrl, apiKey, spec);
39+
}
40+
};
41+
42+
const createTable = async (apiUrl: string, apiKey: string, table: Table) => {
43+
const tableResponse = await fetch(`${apiUrl}/data-model/tables`, {
44+
method: 'POST',
45+
headers: {
46+
'x-api-key': apiKey,
47+
},
48+
body: JSON.stringify({ 'name': table.table, 'description': 'Lorem ipsum.' })
49+
});
50+
51+
if (tableResponse.status != 200) throw new Error('failed to create data model table');
52+
53+
const tableId = (await tableResponse.json() as { id: string; }).id;
54+
55+
for (const [fieldName, fieldType] of Object.entries(table.fields)) {
56+
const fieldResponse = await fetch(`${apiUrl}/data-model/tables/${tableId}/fields`, {
57+
method: 'POST',
58+
headers: {
59+
'x-api-key': apiKey,
60+
},
61+
body: JSON.stringify({
62+
'name': fieldName,
63+
'type': fieldType,
64+
}),
65+
});
66+
67+
if (fieldResponse.status != 200) throw new Error('failed to create data model table field');
68+
}
69+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { test, expect } from '@playwright/test';
2+
import crypto from 'crypto';
3+
import { waitForThen } from 'tests/common/utils';
4+
5+
test('Create a simple scenario', async ({ page }) => {
6+
await page.goto('/scenarios');
7+
8+
const scenarioName = crypto.randomUUID();
9+
10+
await page.getByRole('button', { name: 'New Scenario' }).click();
11+
12+
await waitForThen(
13+
page,
14+
page.getByRole('textbox', { name: 'Name' }),
15+
async (field) => await field.fill(scenarioName),
16+
);
17+
18+
await page.getByRole('textbox', { name: 'Description' }).fill('DESC');
19+
20+
await waitForThen(
21+
page,
22+
page.getByRole('combobox'),
23+
async (box) => await box.click(),
24+
);
25+
26+
await page.waitForTimeout(200);
27+
28+
await waitForThen(
29+
page,
30+
page.getByRole('option', { name: 'transactions' }),
31+
async (option) => await option.click(),
32+
);
33+
34+
await page.getByRole('button', { name: 'Save' }).click();
35+
36+
await waitForThen(
37+
page,
38+
page.getByRole('button', { name: 'Add trigger condition' }),
39+
async (button) => await button.click(),
40+
);
41+
42+
await page.getByRole('button', { name: 'Condition', exact: true }).click();
43+
await page.getByRole('button', { name: 'Select an operand...' }).first().click();
44+
await page.getByRole('option', { name: 'transactions' }).hover();
45+
await page.getByText('amount').click();
46+
await page.getByRole('combobox').click();
47+
await page.getByRole('option', { name: '>' }).click();
48+
await page.getByRole('button', { name: 'Select an operand...' }).click();
49+
await page.getByRole('combobox', { name: 'Select or create an operand' }).fill('100');
50+
await page.getByRole('combobox', { name: 'Select or create an operand' }).press('Enter');
51+
await page.getByRole('button', { name: 'Save' }).click();
52+
await page.getByRole('link', { name: 'Rules' }).click();
53+
await page.getByRole('button', { name: 'Add' }).click();
54+
await page.getByRole('button', { name: 'Add a Rule Add a rule to the' }).click();
55+
await page.getByRole('button', { name: 'Group', exact: true }).click();
56+
await page.getByRole('button', { name: 'Select an operand...' }).first().click();
57+
await page.getByRole('option', { name: 'transactions' }).hover();
58+
await page.getByText('amount').click();
59+
await page.getByRole('combobox').click();
60+
await page.getByRole('option', { name: '>' }).click();
61+
await page.getByRole('button', { name: 'Select an operand...' }).click();
62+
await page.getByRole('combobox', { name: 'Select or create an operand' }).fill('9000');
63+
await page.getByRole('combobox', { name: 'Select or create an operand' }).press('Enter');
64+
await page.getByRole('button', { name: 'Save' }).click();
65+
66+
await page.getByRole('listitem').filter({ hasText: 'Scenarios' }).getByRole('link').click();
67+
68+
await expect(page.getByRole('link', { name: `${scenarioName} DESC`})).toBeVisible();
69+
70+
await page.getByRole('link', { name: `${scenarioName} DESC` }).click();
71+
await page.getByRole('link', { name: 'draft' }).click();
72+
73+
await expect(page.getByRole('button', { name: 'edit_operand.operator_type.' })).toHaveText('amount');
74+
await expect(page.getByRole('combobox')).toHaveText('>');
75+
await expect(page.getByRole('button', { name: 'Number 100' })).toBeVisible();
76+
});

packages/tests/e2e/setup.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { GenericContainer, Network, Wait } from 'testcontainers';
22

3+
import { setupFixtures } from './fixtures';
4+
35
async function globalSetup() {
46
if (process.env['PW_SETUP_DONE']) return;
57

@@ -17,6 +19,9 @@ async function globalSetup() {
1719
.withDefaultLogDriver()
1820
.start();
1921

22+
const dsn = 'postgres://postgres:marble@db/marble?sslmode=disable';
23+
const externalDsn = `postgres://postgres:marble@${db.getIpAddress(net.getName())}/marble?sslmode=disable`;
24+
2025
const firebase = await new GenericContainer(
2126
'europe-west1-docker.pkg.dev/marble-infra/marble/firebase-emulator:latest',
2227
)
@@ -31,45 +36,46 @@ async function globalSetup() {
3136
.withPlatform('linux/x86_64')
3237
.withNetwork(net)
3338
.withEnvironment({
34-
PG_CONNECTION_STRING: `postgres://postgres:marble@db/marble?sslmode=disable`,
39+
PG_CONNECTION_STRING: dsn,
3540
})
3641
.withCommand(['-migrations'])
3742
.withWaitStrategy(Wait.forLogMessage('successfully migrated'))
3843
.withDefaultLogDriver()
3944
.start();
4045

46+
4147
const api = await new GenericContainer(
4248
'europe-west1-docker.pkg.dev/marble-infra/marble/marble-backend',
4349
)
4450
.withPlatform('linux/x86_64')
4551
.withNetwork(net)
4652
.withNetworkAliases('api')
4753
.withEnvironment({
48-
PG_CONNECTION_STRING: `postgres://postgres:marble@db/marble?sslmode=disable`,
54+
PG_CONNECTION_STRING: dsn,
4955
MARBLE_APP_URL: 'http://localhost:3000',
5056
FIREBASE_AUTH_EMULATOR_HOST: `firebase:9099`,
5157
GOOGLE_CLOUD_PROJECT: 'test-project',
5258
CREATE_GLOBAL_ADMIN_EMAIL: 'admin@checkmarble.com',
5359
CREATE_ORG_NAME: 'Zorg',
5460
CREATE_ORG_ADMIN_EMAIL: 'jbe@zorg.com',
55-
AUTHENTICATION_JWT_SIGNING_KEY:
56-
'-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAs+6r50m7qqLHHy7CvfmJPnAi+t/tubi7DPSM2jvA1etT1jEX\nrwbFbmooOu9LTgmjmxOq01p+XwkW1f7iPZViKrf7dEDEuqmpqYG9jPX4G/7xFcci\nGn1iSOiNx9awIKSYZa1wodlMCRM081DGqFNDMf1PScWIyM40nIwaGqLZht4HcOAq\nLbKDa15bxubBqZ9o/YnE1KmyBfq1tTnk0KzAb12Axt0xN4qB2zktsV/LLds+szMk\n/gRHjann1+fCZvxw1JzzRPtgeHLLYzn4ks3mwzy67RO3q/663KPZCsuYhlNCsMqp\n/HAbrF5PaihqzCZqLTDoIXXciCFFgwtLwm951wIDAQABAoIBAQCpb60tJX+1VYeQ\n06XK43rb8xjdiZUA+PYbYwZoUzBpwSq3Xo9g4E12hjzQEpqlJ+qKk+CfGm457AM3\nDMfbGhrRA2Oku4EGDdKYrnXikZVMN6yqx1RUAZJV+bfZYU+Fzbk8tjCEGG3DdfS8\n02nfBFkYb+MEIyGFhriAWmYSgxu4JTN0XRTyPqBytoSLqVCFbv0/yV2oJQDaXW08\nWAA8JtWhzqxACbFnPYe0hYUnrCA71t0v1P/N5uB4kKxI0tulGtW84noSyWA2LSdn\nJlKQW5WsyeMulGBMnIpj/OQJtQErupoITsh1TNi+6ffGgmuMCT1za70DHXVq9Ihu\nkpKBe0wRAoGBAOSarLfNvsS2lTH/8zPyhWBddCS5CfQAeUFLD5xWhQ7/6SenYzYY\n+oiiH2uL7d8grkobX5QLVvJ5ZXziYWoKgJe3SlrvRuNJZCAxvuynUCahhCT+chwW\nGz7ihXh3bGD0gtO6iogGBfrAkvRQnorkdSmVEZd1PsJV/lXp8LKgxJ91AoGBAMl+\ny/6NbzVHt9oQsrVrG/sCAOlqlfTt5KW6pI1WC4LoKBaGe+hy4emZ0G/M2feAJEPR\n92QrPRkVF5bVCjalJj42/7gQIl6r+DQ4+08gLB1MSpWua2M3UtEi/2gsMcQff/wg\n6kmNZObW5Jcnqpp6u72zQTQwF4H29XucV/Yw93abAoGADGvfIKmcSQIGv03CADuY\nRbEuQ2SOhuSTshmLApqs5jC/kXkF6gWXb18nx+c1iJ80+S/dlKS9F7XC7vM6CdIC\nRLwf3SsNNgJh32H0ltVMhJzYGk59EsuctWEHkZEjoW0HwstrBZMWNhbKpV3QD4n0\nV8sSxqEHRPX5ON/aRUp5BJUCgYEAlsymr2P6js2V80X7+Xqn/juJoyd6A0znioEd\nFgoHo3lMR09u/JC+Mq5DKOkPWAQ3H+rMU9NobpUyilf2xN7kuDtBNugcUO4zXCIp\nMxbI7URjrZJUHHUTLiIbNEOfG0DX8EJSFaoUkg7SFa5CKEsipt65Ne2oKkRBhLmF\nu2L6UXECgYBH1bpi0R6j7lIADtZtIJII/TezQbp+VK2R9qoNgkTnHoDjkRVR7v3m\n75wReMvTy1h0Qx/ROtStZz8d5uQuhdeJvbQPQR8KGFUFZDmVWxU+y15WI2H39FMA\nMireKxzCfGGtTsZnhDqYl9NuRPcAGYt5jvoERXlz7b69rkqQUrfy+Q==\n-----END RSA PRIVATE KEY-----',
57-
SEGMENT_WRITE_KEY: 'UgkImFmHmBZAWh5fxIKBY3QtvlcBrhqQ',
5861
})
5962
.withExposedPorts(8080)
6063
.withCommand(['-server'])
6164
.withWaitStrategy(Wait.forHttp('/liveness', 8080, { abortOnContainerExit: true }))
6265
.withDefaultLogDriver()
6366
.start();
6467

68+
6569
process.env['API_PORT'] = api.getFirstMappedPort().toString();
6670
process.env['FIREBASE_PORT'] = firebase.getFirstMappedPort().toString();
6771

68-
process.on('exit', () => {
69-
api.stop();
70-
firebase.stop();
71-
db.stop();
72-
net.stop();
72+
await setupFixtures(externalDsn, process.env['API_PORT']);
73+
74+
process.on('exit', async () => {
75+
await api.stop({ timeout: 1 });
76+
await firebase.stop({ timeout: 1 });
77+
await db.stop({ timeout: 1 });
78+
await net.stop();
7379
});
7480

7581
process.env['PW_SETUP_DONE'] = 'true';

packages/tests/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
"devDependencies": {
1616
"@playwright/test": "^1.51.0",
1717
"@types/node": "22.7.7",
18+
"knex": "^3.1.0",
19+
"pg": "^8.13.3",
1820
"playwright": "^1.51.0",
1921
"testcontainers": "^10.18.0"
2022
}

packages/tests/playwright.config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@ await setup();
66

77
export default defineConfig({
88
testDir: './e2e',
9-
fullyParallel: true,
109
reporter: 'html',
10+
fullyParallel: true,
11+
workers: 4,
1112
use: {
1213
headless: !process.env['PW_DEBUG'],
1314
baseURL: 'http://localhost:3000',
1415
trace: 'retain-on-failure',
1516
screenshot: 'only-on-failure',
17+
launchOptions: {
18+
slowMo: 250,
19+
}
1620
},
1721
projects: [
1822
{

0 commit comments

Comments
 (0)