From dd138547fb2bca34fc61f7d11736d4fedf8599af Mon Sep 17 00:00:00 2001 From: Matt Ciaccio Date: Tue, 28 Apr 2026 00:19:51 +0200 Subject: [PATCH] test(e2e): fix admin-nav locator + add residential interest API coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 21-role-based-ui: tighten the Settings link locator. The previous `getByRole('link', { name: /settings/i }).first().or(getByText(/.../) .first())` chain hit a strict-mode violation once the sidebar Admin section became default-expanded — both the section header text node and the Settings link matched. Match the link directly with exact: true. - 26-residential: extend smoke with two API-driven specs covering the residential interest pipeline — create+list and detail-page render — using preferences-string stamp + heading match for assertions. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/e2e/smoke/21-role-based-ui.spec.ts | 28 ++------- tests/e2e/smoke/26-residential.spec.ts | 78 ++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 24 deletions(-) diff --git a/tests/e2e/smoke/21-role-based-ui.spec.ts b/tests/e2e/smoke/21-role-based-ui.spec.ts index c79bc33..cd40ea3 100644 --- a/tests/e2e/smoke/21-role-based-ui.spec.ts +++ b/tests/e2e/smoke/21-role-based-ui.spec.ts @@ -8,30 +8,10 @@ test.describe('Role-Based UI', () => { // Give React Query time to resolve permissions await page.waitForTimeout(3_000); - // Admin section (Settings / Administration) should appear in the sidebar - const adminNav = page - .getByText(/admin/i) - .first() - .or(page.getByRole('link', { name: /settings/i }).first()) - .or(page.getByRole('link', { name: /administration/i }).first()); - - const adminNavVisible = await adminNav.isVisible({ timeout: 10_000 }).catch(() => false); - - if (!adminNavVisible) { - // Some layouts collapse the admin section behind a toggle — try expanding - const adminToggle = page.locator('[data-testid*="admin"], [class*="admin"]').first(); - if (await adminToggle.isVisible({ timeout: 3_000 }).catch(() => false)) { - await adminToggle.click(); - await page.waitForTimeout(1_000); - } - } - - // Re-check for admin-related navigation after any expansion attempt - const settingsLink = page - .getByRole('link', { name: /settings/i }) - .first() - .or(page.getByText(/settings|administration|admin/i).first()); - + // Sidebar exposes a Settings link inside the Admin section (visible by + // default for super_admin). Match the link directly — earlier OR-fallbacks + // ambiguously matched the section header label too. + const settingsLink = page.getByRole('link', { name: 'Settings', exact: true }).first(); await expect(settingsLink).toBeVisible({ timeout: 10_000 }); // "+ New" button (or equivalent CTA) should be visible diff --git a/tests/e2e/smoke/26-residential.spec.ts b/tests/e2e/smoke/26-residential.spec.ts index c6540b6..193d478 100644 --- a/tests/e2e/smoke/26-residential.spec.ts +++ b/tests/e2e/smoke/26-residential.spec.ts @@ -67,6 +67,84 @@ test.describe('Residential', () => { }); }); + test('create a residential interest via API and see it on the interests list', async ({ + page, + }) => { + const portId = await getPortId(page); + const stamp = Date.now(); + + // 1) Create a residential client via the v1 API. + const clientRes = await page.request.post('/api/v1/residential/clients', { + data: { + fullName: `E2E Res ${stamp}`, + email: `e2e-res-${stamp}@test.example`, + phone: '+1-555-0199', + }, + headers: { 'Content-Type': 'application/json', 'x-port-id': portId }, + }); + expect(clientRes.status()).toBe(201); + const clientBody = await clientRes.json(); + const residentialClientId = clientBody.data?.id ?? clientBody.id; + expect(residentialClientId).toBeTruthy(); + + // 2) Create a residential interest tied to that client. + const interestRes = await page.request.post('/api/v1/residential/interests', { + data: { + residentialClientId, + preferences: `2BR oceanview, budget €${stamp}`, + notes: `Smoke interest ${stamp}`, + source: 'manual', + }, + headers: { 'Content-Type': 'application/json', 'x-port-id': portId }, + }); + expect(interestRes.status()).toBe(201); + const interestBody = await interestRes.json(); + const residentialInterestId = interestBody.data?.id ?? interestBody.id; + expect(residentialInterestId).toBeTruthy(); + + // 3) Confirm it shows up on the residential interests list page. + // The list renders preferences/notes (not the client name), so match + // against the unique stamp embedded in the preferences string. + await navigateTo(page, '/residential/interests'); + await expect(page.getByText(new RegExp(String(stamp))).first()).toBeVisible({ + timeout: 15_000, + }); + }); + + test('residential interest detail page renders for a freshly-created interest', async ({ + page, + }) => { + const portId = await getPortId(page); + const stamp = Date.now(); + + const clientRes = await page.request.post('/api/v1/residential/clients', { + data: { + fullName: `E2E ResDetail ${stamp}`, + email: `e2e-res-detail-${stamp}@test.example`, + }, + headers: { 'Content-Type': 'application/json', 'x-port-id': portId }, + }); + expect(clientRes.status()).toBe(201); + const clientId = (await clientRes.json()).data?.id; + + const interestRes = await page.request.post('/api/v1/residential/interests', { + data: { residentialClientId: clientId, preferences: 'penthouse, sea view', source: 'manual' }, + headers: { 'Content-Type': 'application/json', 'x-port-id': portId }, + }); + expect(interestRes.status()).toBe(201); + const interestId = (await interestRes.json()).data?.id; + + // Navigate to the detail route — the page should render the standard + // residential-interest detail layout (eyebrow + client name h1 + sections). + await navigateTo(page, `/residential/interests/${interestId}`); + await expect(page.getByText('Residential interest', { exact: true }).first()).toBeVisible({ + timeout: 15_000, + }); + await expect(page.getByRole('heading', { name: /E2E ResDetail/ }).first()).toBeVisible({ + timeout: 5_000, + }); + }); + test('public residential inquiry endpoint accepts a submission', async ({ page }) => { // Direct API call to the public endpoint — confirms wiring without UI. // Public endpoint takes portId via query string (no auth, but the helper