Skip to content

Commit fd10c62

Browse files
authored
test: Add Playwright structural conformance tests for aligned pages (#1625)
* test: Add Playwright structural conformance tests for aligned pages Verify that all feature pages follow the canonical layout pattern: - PageShell wrapper (space-y-6) present on all pages - PageHeader h1 with text-3xl font-bold tracking-tight styling - DataTable wrapped inside Card (data-slot="card") on list pages - Breadcrumbs navigation on detail pages Covers accounts, parties, payments, positions, reconciliation, starlark-config, ledger, internal-accounts, audit-log, market-data, gateway-mappings, reference-data sub-pages, and admin user pages. * fix: Separate Nodes tree view from DataTable assertion The Nodes page uses a tree view inside a Card rather than a DataTable. Split the Card presence check (all pages) from the table element check (only DataTable pages) to avoid false failures. * fix: Address review feedback — assert heading text and strengthen locators - Assert expected heading text on all list pages and Reference Data hub to catch wrong-page rendering (CodeRabbit, claude-review feedback) - Strengthen detail page PageShell locator to filter by Breadcrumb presence instead of using .first() (claude-review feedback) --------- Co-authored-by: Ben Coombs <bjcoombs@users.noreply.github.com>
1 parent 9755887 commit fd10c62

1 file changed

Lines changed: 199 additions & 0 deletions

File tree

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import { test, expect, navigateTo } from './fixtures'
2+
3+
/**
4+
* Structural conformance tests for the canonical page layout.
5+
*
6+
* Every feature page should follow the shared layout pattern:
7+
* - PageShell: div.space-y-6 wrapper
8+
* - PageHeader: h1 with text-3xl font-bold tracking-tight
9+
* - List pages: DataTable wrapped in a Card (data-slot="card")
10+
* - Detail pages: Breadcrumbs nav + h1 + tab navigation
11+
*
12+
* These tests verify structural conformance only — no backend required.
13+
* Auth tokens are memory-only; all navigation uses navigateTo() to preserve state.
14+
*/
15+
16+
// ─── List pages ────────────────────────────────────────────────────────────────
17+
18+
const LIST_PAGES = [
19+
{ path: '/accounts', heading: 'Accounts' },
20+
{ path: '/parties', heading: 'Parties' },
21+
{ path: '/payments', heading: 'Payments' },
22+
{ path: '/positions', heading: 'Positions' },
23+
{ path: '/reconciliation', heading: 'Reconciliation' },
24+
{ path: '/starlark-config', heading: 'Starlark Configuration' },
25+
{ path: '/ledger', heading: 'Ledger' },
26+
{ path: '/internal-accounts', heading: 'Internal Accounts' },
27+
{ path: '/audit-log', heading: 'Audit Log' },
28+
{ path: '/market-data', heading: 'Market Data' },
29+
{ path: '/gateway-mappings', heading: 'Gateway Mappings' },
30+
] as const
31+
32+
const REFERENCE_DATA_TABLE_PAGES = [
33+
{ path: '/reference-data/instruments', heading: 'Instruments' },
34+
{ path: '/reference-data/account-types', heading: 'Account Types' },
35+
] as const
36+
37+
// Nodes uses a tree view inside a Card, not a DataTable
38+
const REFERENCE_DATA_CARD_PAGES = [
39+
{ path: '/reference-data/nodes', heading: 'Nodes' },
40+
] as const
41+
42+
const ADMIN_LIST_PAGES = [
43+
{ path: '/users', heading: 'Users' },
44+
] as const
45+
46+
const ALL_TENANT_LIST_PAGES = [...LIST_PAGES, ...REFERENCE_DATA_TABLE_PAGES, ...REFERENCE_DATA_CARD_PAGES]
47+
48+
test.describe('Structural conformance — list pages (tenant-user)', () => {
49+
for (const page of ALL_TENANT_LIST_PAGES) {
50+
test.describe(page.path, () => {
51+
test('has PageShell wrapper (space-y-6)', async ({ authenticatedPage }) => {
52+
await navigateTo(authenticatedPage, page.path)
53+
// Wait for the heading to confirm page rendered
54+
await expect(
55+
authenticatedPage.getByRole('heading', { level: 1 }),
56+
).toBeVisible({ timeout: 10_000 })
57+
// PageShell renders a div.space-y-6 containing the h1
58+
const shell = authenticatedPage.locator('.space-y-6').filter({
59+
has: authenticatedPage.getByRole('heading', { level: 1 }),
60+
})
61+
await expect(shell).toBeVisible()
62+
})
63+
64+
test('has PageHeader h1 with correct text and styling', async ({ authenticatedPage }) => {
65+
await navigateTo(authenticatedPage, page.path)
66+
const h1 = authenticatedPage.getByRole('heading', { level: 1 })
67+
await expect(h1).toBeVisible({ timeout: 10_000 })
68+
await expect(h1).toHaveText(page.heading)
69+
await expect(h1).toHaveClass(/text-3xl/)
70+
await expect(h1).toHaveClass(/font-bold/)
71+
await expect(h1).toHaveClass(/tracking-tight/)
72+
})
73+
74+
test('has content inside a Card', async ({ authenticatedPage }) => {
75+
await navigateTo(authenticatedPage, page.path)
76+
await expect(
77+
authenticatedPage.getByRole('heading', { level: 1 }),
78+
).toBeVisible({ timeout: 10_000 })
79+
// Card uses data-slot="card"
80+
const card = authenticatedPage.locator('[data-slot="card"]').first()
81+
await expect(card).toBeVisible({ timeout: 10_000 })
82+
})
83+
})
84+
}
85+
86+
// DataTable-specific: verify <table> element exists inside the Card
87+
for (const page of [...LIST_PAGES, ...REFERENCE_DATA_TABLE_PAGES]) {
88+
test(`${page.path} has DataTable (table element) inside Card`, async ({ authenticatedPage }) => {
89+
await navigateTo(authenticatedPage, page.path)
90+
await expect(
91+
authenticatedPage.getByRole('heading', { level: 1 }),
92+
).toBeVisible({ timeout: 10_000 })
93+
const card = authenticatedPage.locator('[data-slot="card"]').first()
94+
await expect(card).toBeVisible({ timeout: 10_000 })
95+
const table = card.locator('table')
96+
await expect(table).toBeVisible()
97+
})
98+
}
99+
})
100+
101+
test.describe('Structural conformance — list pages (platform-admin)', () => {
102+
for (const page of ADMIN_LIST_PAGES) {
103+
test.describe(page.path, () => {
104+
test('has PageShell wrapper (space-y-6)', async ({ platformAdminPage }) => {
105+
await navigateTo(platformAdminPage, page.path)
106+
await expect(
107+
platformAdminPage.getByRole('heading', { level: 1 }),
108+
).toBeVisible({ timeout: 10_000 })
109+
const shell = platformAdminPage.locator('.space-y-6').filter({
110+
has: platformAdminPage.getByRole('heading', { level: 1 }),
111+
})
112+
await expect(shell).toBeVisible()
113+
})
114+
115+
test('has PageHeader h1 with correct text and styling', async ({ platformAdminPage }) => {
116+
await navigateTo(platformAdminPage, page.path)
117+
const h1 = platformAdminPage.getByRole('heading', { level: 1 })
118+
await expect(h1).toBeVisible({ timeout: 10_000 })
119+
await expect(h1).toHaveText(page.heading)
120+
await expect(h1).toHaveClass(/text-3xl/)
121+
await expect(h1).toHaveClass(/font-bold/)
122+
await expect(h1).toHaveClass(/tracking-tight/)
123+
})
124+
125+
test('has DataTable inside a Card', async ({ platformAdminPage }) => {
126+
await navigateTo(platformAdminPage, page.path)
127+
await expect(
128+
platformAdminPage.getByRole('heading', { level: 1 }),
129+
).toBeVisible({ timeout: 10_000 })
130+
const card = platformAdminPage.locator('[data-slot="card"]').first()
131+
await expect(card).toBeVisible({ timeout: 10_000 })
132+
const table = card.locator('table')
133+
await expect(table).toBeVisible()
134+
})
135+
})
136+
}
137+
})
138+
139+
// ─── Detail pages ──────────────────────────────────────────────────────────────
140+
141+
/**
142+
* Detail pages are tested by navigating to a known-invalid ID.
143+
* Even for "not found" states, the canonical layout should render:
144+
* - Breadcrumbs (nav[aria-label="Breadcrumb"])
145+
* - PageShell wrapper (space-y-6)
146+
*
147+
* Detail pages that render a full detail view (with tabs) require a live backend
148+
* and seeded data. Those structural checks are skipped unless a backend is available.
149+
*/
150+
151+
const DETAIL_PAGES = [
152+
{ path: '/accounts/test-id-000', listPath: '/accounts', label: 'Accounts' },
153+
{ path: '/parties/test-id-000', listPath: '/parties', label: 'Parties' },
154+
{ path: '/payments/test-id-000', listPath: '/payments', label: 'Payments' },
155+
{ path: '/positions/test-id-000', listPath: '/positions', label: 'Positions' },
156+
{ path: '/starlark-config/test-id-000', listPath: '/starlark-config', label: 'Starlark' },
157+
{ path: '/gateway-mappings/test-id-000', listPath: '/gateway-mappings', label: 'Mappings' },
158+
] as const
159+
160+
test.describe('Structural conformance — detail pages', () => {
161+
for (const page of DETAIL_PAGES) {
162+
test.describe(page.path, () => {
163+
test('has Breadcrumbs navigation', async ({ authenticatedPage }) => {
164+
await navigateTo(authenticatedPage, page.path)
165+
// Breadcrumbs component renders nav[aria-label="Breadcrumb"]
166+
const breadcrumbs = authenticatedPage.getByLabel('Breadcrumb')
167+
await expect(breadcrumbs).toBeVisible({ timeout: 10_000 })
168+
})
169+
170+
test('has PageShell wrapper (space-y-6)', async ({ authenticatedPage }) => {
171+
await navigateTo(authenticatedPage, page.path)
172+
// Wait for breadcrumbs to confirm page rendered, then check PageShell
173+
await expect(authenticatedPage.getByLabel('Breadcrumb')).toBeVisible({ timeout: 10_000 })
174+
const shell = authenticatedPage.locator('.space-y-6').filter({
175+
has: authenticatedPage.getByLabel('Breadcrumb'),
176+
})
177+
await expect(shell).toBeVisible()
178+
})
179+
})
180+
}
181+
})
182+
183+
// ─── Reference Data hub page ───────────────────────────────────────────────────
184+
185+
test.describe('Structural conformance — Reference Data hub', () => {
186+
test('has PageShell and PageHeader', async ({ authenticatedPage }) => {
187+
await navigateTo(authenticatedPage, '/reference-data')
188+
const h1 = authenticatedPage.getByRole('heading', { level: 1 })
189+
await expect(h1).toBeVisible({ timeout: 10_000 })
190+
await expect(h1).toHaveText('Reference Data')
191+
await expect(h1).toHaveClass(/text-3xl/)
192+
await expect(h1).toHaveClass(/font-bold/)
193+
await expect(h1).toHaveClass(/tracking-tight/)
194+
const shell = authenticatedPage.locator('.space-y-6').filter({
195+
has: authenticatedPage.getByRole('heading', { level: 1 }),
196+
})
197+
await expect(shell).toBeVisible()
198+
})
199+
})

0 commit comments

Comments
 (0)