Skip to content

Commit 76c02d8

Browse files
easelclaude
andcommitted
fix(e2e): use relative URLs and same port for both E2E configs
All 54 hardcoded `http://localhost:4170` occurrences replaced with relative paths ('/ui/...', '/collections/...') so Playwright's baseURL from whichever config is active determines host and port. Both configs now bind to port 4170. - playwright.e2e.postgres.config.ts: remove the unnecessary port 4171; uses the same default port as the memory config - health.spec.ts: relax backend-type assertion to be config-agnostic (was asserting 'memory' literally; now checks that p.muted is non-empty) - schemas.spec.ts: replace raw fetch() in beforeAll with Playwright request fixture so the APIRequestContext respects baseURL - audit.spec.ts: fix filter race condition — wait for the /audit/query response before asserting row counts instead of networkidle, which fires before Svelte's in-page fetch resolves All 50 E2E tests pass. Verification: bunx playwright test --config playwright.e2e.config.ts (50 passed) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent dc0efdb commit 76c02d8

8 files changed

Lines changed: 158 additions & 268 deletions

ui/playwright.e2e.postgres.config.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default defineConfig({
2020
reporter: 'html',
2121

2222
use: {
23-
baseURL: 'http://localhost:4171',
23+
baseURL: 'http://localhost:4170',
2424
trace: 'on-first-retry',
2525
screenshot: 'only-on-failure',
2626
},
@@ -37,13 +37,10 @@ export default defineConfig({
3737
* AXON_POSTGRES_DSN must be set to a valid PostgreSQL connection string. */
3838
webServer: {
3939
command:
40-
'cargo run -p axon-cli -- serve --no-auth --storage postgres --http-port 4171 --postgres-dsn $AXON_POSTGRES_DSN --ui-dir ui/build',
41-
url: 'http://localhost:4171/health',
40+
'cargo run -p axon-cli -- serve --no-auth --storage postgres --postgres-dsn $AXON_POSTGRES_DSN --ui-dir ui/build',
41+
url: 'http://localhost:4170/health',
4242
reuseExistingServer: !process.env.CI,
4343
timeout: 120000,
4444
cwd: '..',
45-
env: {
46-
AXON_POSTGRES_DSN: process.env.AXON_POSTGRES_DSN ?? '',
47-
},
4845
},
4946
});

ui/tests/e2e/audit.spec.ts

Lines changed: 45 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { expect, test } from '@playwright/test';
55
*
66
* Uses collection "e2e-audit" and creates an entity via the API in beforeAll so
77
* at least one audit entry exists when the log page is loaded.
8+
*
9+
* Uses relative URLs so the baseURL from the active Playwright config applies.
810
*/
911

1012
const COLLECTION_NAME = 'e2e-audit';
@@ -13,41 +15,35 @@ const ENTITY_ID = 'audit-entity-001';
1315
test.describe('Audit Log', () => {
1416
test.beforeAll(async ({ request }) => {
1517
// Create collection.
16-
const collResp = await request.post(
17-
`http://localhost:4170/collections/${COLLECTION_NAME}`,
18-
{
19-
data: {
20-
schema: {
21-
description: null,
22-
version: 1,
23-
entity_schema: { type: 'object', properties: {} },
24-
link_types: {},
25-
},
26-
actor: 'e2e-test',
18+
const collResp = await request.post(`/collections/${COLLECTION_NAME}`, {
19+
data: {
20+
schema: {
21+
description: null,
22+
version: 1,
23+
entity_schema: { type: 'object', properties: {} },
24+
link_types: {},
2725
},
26+
actor: 'e2e-test',
2827
},
29-
);
28+
});
3029
expect([201, 409]).toContain(collResp.status());
3130

3231
// Create an entity so there is an audit entry to show.
33-
const entityResp = await request.post(
34-
`http://localhost:4170/entities/${COLLECTION_NAME}/${ENTITY_ID}`,
35-
{
36-
data: { data: { note: 'audit test' }, actor: 'e2e-test' },
37-
},
38-
);
32+
const entityResp = await request.post(`/entities/${COLLECTION_NAME}/${ENTITY_ID}`, {
33+
data: { data: { note: 'audit test' }, actor: 'e2e-test' },
34+
});
3935
expect([201, 409]).toContain(entityResp.status());
4036
});
4137

4238
test('Audit Log heading is visible', async ({ page }) => {
43-
await page.goto('http://localhost:4170/ui/audit');
39+
await page.goto('/ui/audit');
4440
await page.waitForLoadState('networkidle');
4541

4642
await expect(page.getByRole('heading', { name: 'Audit Log' })).toBeVisible({ timeout: 15000 });
4743
});
4844

4945
test('audit log page has filter controls', async ({ page }) => {
50-
await page.goto('http://localhost:4170/ui/audit');
46+
await page.goto('/ui/audit');
5147
await page.waitForLoadState('networkidle');
5248

5349
await expect(page.getByPlaceholder('All collections')).toBeVisible({ timeout: 15000 });
@@ -56,7 +52,7 @@ test.describe('Audit Log', () => {
5652
});
5753

5854
test('audit log contains entries after entity creation', async ({ page }) => {
59-
await page.goto('http://localhost:4170/ui/audit');
55+
await page.goto('/ui/audit');
6056
await page.waitForLoadState('networkidle');
6157

6258
// Wait for the "Recent Entries" panel to load (loading spinner disappears).
@@ -75,7 +71,7 @@ test.describe('Audit Log', () => {
7571
});
7672

7773
test('clicking an audit row shows entry detail', async ({ page }) => {
78-
await page.goto('http://localhost:4170/ui/audit');
74+
await page.goto('/ui/audit');
7975
await page.waitForLoadState('networkidle');
8076

8177
const recentPanel = page.locator('section.panel').filter({ hasText: 'Recent Entries' });
@@ -90,7 +86,6 @@ test.describe('Audit Log', () => {
9086
await firstRow.click();
9187

9288
// The detail panel is the second panel in the two-column grid.
93-
// The audit page auto-selects the first entry, so the heading shows "Entry #N".
9489
const detailPanel = page.locator('.two-column section.panel').nth(1);
9590
await expect(detailPanel.getByRole('heading', { name: /Entry #\d+/ })).toBeVisible({
9691
timeout: 15000,
@@ -103,37 +98,28 @@ test.describe('Audit Log', () => {
10398
});
10499

105100
test.describe('Audit Log filtering', () => {
106-
// The beforeAll from "Audit Log" already runs when tests execute in order,
107-
// but we need our own to guarantee the collection and entity exist regardless
108-
// of test ordering.
109101
test.beforeAll(async ({ request }) => {
110-
const collResp = await request.post(
111-
`http://localhost:4170/collections/${COLLECTION_NAME}`,
112-
{
113-
data: {
114-
schema: {
115-
description: null,
116-
version: 1,
117-
entity_schema: { type: 'object', properties: {} },
118-
link_types: {},
119-
},
120-
actor: 'e2e-test',
102+
const collResp = await request.post(`/collections/${COLLECTION_NAME}`, {
103+
data: {
104+
schema: {
105+
description: null,
106+
version: 1,
107+
entity_schema: { type: 'object', properties: {} },
108+
link_types: {},
121109
},
110+
actor: 'e2e-test',
122111
},
123-
);
112+
});
124113
expect([201, 409]).toContain(collResp.status());
125114

126-
const entityResp = await request.post(
127-
`http://localhost:4170/entities/${COLLECTION_NAME}/${ENTITY_ID}`,
128-
{
129-
data: { data: { note: 'audit filter test' }, actor: 'e2e-test' },
130-
},
131-
);
115+
const entityResp = await request.post(`/entities/${COLLECTION_NAME}/${ENTITY_ID}`, {
116+
data: { data: { note: 'audit filter test' }, actor: 'e2e-test' },
117+
});
132118
expect([201, 409]).toContain(entityResp.status());
133119
});
134120

135121
test('audit table shows all expected columns', async ({ page }) => {
136-
await page.goto('http://localhost:4170/ui/audit');
122+
await page.goto('/ui/audit');
137123
await page.waitForLoadState('networkidle');
138124

139125
const recentPanel = page.locator('section.panel').filter({ hasText: 'Recent Entries' });
@@ -152,7 +138,7 @@ test.describe('Audit Log filtering', () => {
152138
});
153139

154140
test('filter by collection narrows results', async ({ page }) => {
155-
await page.goto('http://localhost:4170/ui/audit');
141+
await page.goto('/ui/audit');
156142
await page.waitForLoadState('networkidle');
157143

158144
const recentPanel = page.locator('section.panel').filter({ hasText: 'Recent Entries' });
@@ -162,11 +148,16 @@ test.describe('Audit Log filtering', () => {
162148

163149
// Apply a collection filter for "e2e-audit".
164150
await page.getByPlaceholder('All collections').fill(COLLECTION_NAME);
151+
// Wait for the network response from the filter request, then check results.
152+
const filterResponsePromise = page.waitForResponse(
153+
(resp) => resp.url().includes('/audit/query') && resp.status() === 200,
154+
);
165155
await page.getByRole('button', { name: 'Apply Filters' }).click();
166-
await page.waitForLoadState('networkidle');
156+
await filterResponsePromise;
167157

168158
// All visible Collection cells (3rd column) should contain "e2e-audit".
169159
const collectionCells = auditTable.locator('tbody tr td:nth-child(3)');
160+
await expect(collectionCells.first()).toBeVisible({ timeout: 15000 });
170161
const count = await collectionCells.count();
171162
expect(count).toBeGreaterThan(0);
172163
for (let i = 0; i < count; i++) {
@@ -175,23 +166,25 @@ test.describe('Audit Log filtering', () => {
175166

176167
// Negative test: filter by a non-existent collection name.
177168
await page.getByPlaceholder('All collections').fill('zzznonexistent');
169+
const emptyResponsePromise = page.waitForResponse(
170+
(resp) => resp.url().includes('/audit/query') && resp.status() === 200,
171+
);
178172
await page.getByRole('button', { name: 'Apply Filters' }).click();
179-
await page.waitForLoadState('networkidle');
173+
await emptyResponsePromise;
180174

181175
await expect(
182176
page.getByText('No audit entries matched the current filters.'),
183177
).toBeVisible({ timeout: 15000 });
184178
});
185179

186180
test('filter by actor shows entries for that actor', async ({ page }) => {
187-
await page.goto('http://localhost:4170/ui/audit');
181+
await page.goto('/ui/audit');
188182
await page.waitForLoadState('networkidle');
189183

190184
const recentPanel = page.locator('section.panel').filter({ hasText: 'Recent Entries' });
191185
await expect(recentPanel).toBeVisible({ timeout: 15000 });
192186

193-
// In --no-auth mode all entries are recorded with actor="anonymous" regardless of
194-
// the actor field sent in the request body.
187+
// In --no-auth mode all entries are recorded with actor="anonymous".
195188
await page.getByPlaceholder('All actors').fill('anonymous');
196189
await page.getByRole('button', { name: 'Apply Filters' }).click();
197190
await page.waitForLoadState('networkidle');
@@ -209,7 +202,7 @@ test.describe('Audit Log filtering', () => {
209202
});
210203

211204
test('clear filters restores all entries', async ({ page }) => {
212-
await page.goto('http://localhost:4170/ui/audit');
205+
await page.goto('/ui/audit');
213206
await page.waitForLoadState('networkidle');
214207

215208
const recentPanel = page.locator('section.panel').filter({ hasText: 'Recent Entries' });

ui/tests/e2e/collections.spec.ts

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,7 @@ import { expect, test } from '@playwright/test';
44
* E2E tests for collection creation and browsing against a real axon-server.
55
*
66
* Uses collection name "e2e-tasks" to avoid interference with other test files.
7-
* Memory storage resets between server restarts but NOT between tests in a run,
8-
* so the collection created in beforeAll is visible in later tests.
9-
*
10-
* Note: test.describe.configure({ mode: 'serial' }) is set because the workflow
11-
* tests are ordered by design and share server-side state.
7+
* Uses relative URLs so the baseURL from the active Playwright config applies.
128
*/
139

1410
const COLLECTION_NAME = 'e2e-tasks';
@@ -18,22 +14,17 @@ test.describe('Collections workflow', () => {
1814

1915
test.beforeAll(async ({ request }) => {
2016
// Ensure the collection exists before the "check presence" tests run.
21-
// Using the API directly avoids a race condition with the UI creation test
22-
// when tests are distributed across workers.
23-
const response = await request.post(
24-
`http://localhost:4170/collections/${COLLECTION_NAME}`,
25-
{
26-
data: {
27-
schema: {
28-
description: null,
29-
version: 1,
30-
entity_schema: { type: 'object', properties: {} },
31-
link_types: {},
32-
},
33-
actor: 'e2e-test',
17+
const response = await request.post(`/collections/${COLLECTION_NAME}`, {
18+
data: {
19+
schema: {
20+
description: null,
21+
version: 1,
22+
entity_schema: { type: 'object', properties: {} },
23+
link_types: {},
3424
},
25+
actor: 'e2e-test',
3526
},
36-
);
27+
});
3728
// 201 Created or 409 Conflict (already exists) are both acceptable.
3829
expect([201, 409]).toContain(response.status());
3930
});
@@ -42,7 +33,7 @@ test.describe('Collections workflow', () => {
4233
// Use a unique name so this test always creates a brand-new collection,
4334
// regardless of prior runs (COLLECTION_NAME may already exist from beforeAll).
4435
const uniqueName = `e2e-create-${Date.now()}`;
45-
await page.goto('http://localhost:4170/ui/schemas');
36+
await page.goto('/ui/schemas');
4637
await page.waitForLoadState('networkidle');
4738

4839
// Fill in the collection name in the Create Collection form.
@@ -63,7 +54,7 @@ test.describe('Collections workflow', () => {
6354
});
6455

6556
test('new collection appears in schemas collection list', async ({ page }) => {
66-
await page.goto('http://localhost:4170/ui/schemas');
57+
await page.goto('/ui/schemas');
6758
await page.waitForLoadState('networkidle');
6859

6960
// The left-hand panel lists registered collections.
@@ -76,7 +67,7 @@ test.describe('Collections workflow', () => {
7667
});
7768

7869
test('new collection appears in collections table', async ({ page }) => {
79-
await page.goto('http://localhost:4170/ui/collections');
70+
await page.goto('/ui/collections');
8071
await page.waitForLoadState('networkidle');
8172

8273
// The collections table should show the collection we created.
@@ -88,7 +79,7 @@ test.describe('Collections workflow', () => {
8879
});
8980

9081
test('collection detail page shows 0 entities', async ({ page }) => {
91-
await page.goto(`http://localhost:4170/ui/collections/${COLLECTION_NAME}`);
82+
await page.goto(`/ui/collections/${COLLECTION_NAME}`);
9283
await page.waitForLoadState('networkidle');
9384

9485
// Page heading matches the collection name.

ui/tests/e2e/databases.spec.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,20 @@ test.describe('Databases page', () => {
66
test.describe.configure({ mode: 'serial' });
77

88
test('sidebar has Databases link', async ({ page }) => {
9-
await page.goto('http://localhost:4170/ui/collections');
9+
await page.goto('/ui/collections');
1010
await page.waitForLoadState('networkidle');
1111
const nav = page.locator('nav');
1212
await expect(nav.getByRole('link', { name: 'Databases' })).toBeVisible();
1313
});
1414

1515
test('databases page loads with heading', async ({ page }) => {
16-
await page.goto('http://localhost:4170/ui/databases');
16+
await page.goto('/ui/databases');
1717
await page.waitForLoadState('networkidle');
1818
await expect(page.getByRole('heading', { name: 'Databases' })).toBeVisible({ timeout: 15000 });
1919
});
2020

2121
test('create tenant form is visible', async ({ page }) => {
22-
await page.goto('http://localhost:4170/ui/databases');
22+
await page.goto('/ui/databases');
2323
await page.waitForLoadState('networkidle');
2424
await expect(page.getByRole('heading', { name: 'Create Tenant' })).toBeVisible();
2525
await expect(page.getByPlaceholder('my-org')).toBeVisible();
@@ -28,14 +28,14 @@ test.describe('Databases page', () => {
2828
});
2929

3030
test('create tenant button enables when name entered', async ({ page }) => {
31-
await page.goto('http://localhost:4170/ui/databases');
31+
await page.goto('/ui/databases');
3232
await page.waitForLoadState('networkidle');
3333
await page.getByPlaceholder('my-org').fill('e2e-tenant-test');
3434
await expect(page.getByRole('button', { name: 'Create Tenant' })).toBeEnabled();
3535
});
3636

3737
test('can create a tenant and see it in the list', async ({ page }) => {
38-
await page.goto('http://localhost:4170/ui/databases');
38+
await page.goto('/ui/databases');
3939
await page.waitForLoadState('networkidle');
4040
await page.getByPlaceholder('my-org').fill('e2e-org');
4141
await page.getByRole('button', { name: 'Create Tenant' }).click();
@@ -44,7 +44,7 @@ test.describe('Databases page', () => {
4444
});
4545

4646
test('can assign a database to a tenant', async ({ page }) => {
47-
await page.goto('http://localhost:4170/ui/databases');
47+
await page.goto('/ui/databases');
4848
await page.waitForLoadState('networkidle');
4949
// The e2e-org tenant should exist from the previous test (same server instance)
5050
const tenantPanel = page.locator('section.panel').filter({ hasText: 'e2e-org' });
@@ -57,7 +57,7 @@ test.describe('Databases page', () => {
5757
});
5858

5959
test('can remove a database from a tenant', async ({ page }) => {
60-
await page.goto('http://localhost:4170/ui/databases');
60+
await page.goto('/ui/databases');
6161
await page.waitForLoadState('networkidle');
6262
const tenantPanel = page.locator('section.panel').filter({ hasText: 'e2e-org' });
6363
await expect(tenantPanel).toBeVisible({ timeout: 15000 });

0 commit comments

Comments
 (0)