From 8a46c56a964ae298d3d6ab69152cecb3e8b53a81 Mon Sep 17 00:00:00 2001 From: jubnl Date: Sat, 25 Jul 2026 16:30:33 +0200 Subject: [PATCH] test(client): dismiss the CTA-style support modal in e2e setup --- client/e2e/auth.setup.ts | 17 ++++++-------- client/e2e/helpers.ts | 49 ++++++++++++++++++++++++++++------------ 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/client/e2e/auth.setup.ts b/client/e2e/auth.setup.ts index 46a342292..bd97a4444 100644 --- a/client/e2e/auth.setup.ts +++ b/client/e2e/auth.setup.ts @@ -1,4 +1,5 @@ import { test as setup, expect } from '@playwright/test' +import { dismissSystemNotices } from './helpers' // Relative to the config dir (client/), matching `storageState` in // playwright.config.ts. Playwright runs from the client workspace root. @@ -27,16 +28,12 @@ setup('authenticate the seeded admin (incl. forced password change)', async ({ p await page.waitForURL('**/dashboard', { timeout: 30_000 }) - // Dismiss the first-run "Welcome to TREK" system-notice modal(s). It renders - // asynchronously (after the notices fetch), so wait for it before clicking. - // Dismissal is recorded server-side against this user, so clearing it here - // keeps it cleared for every authenticated flow in the run (shared test DB). - const ok = page.getByRole('button', { name: 'OK', exact: true }) - await ok.waitFor({ state: 'visible', timeout: 10_000 }).catch(() => {}) - for (let i = 0; i < 8 && (await ok.isVisible().catch(() => false)); i++) { - await ok.click() - await page.waitForTimeout(400) - } + // Dismiss the first-run system-notice modal(s) — currently the thank-you / + // support modal, which has NO "OK" button (only CTAs + the X). The shared + // helper handles both notice shapes; dismissal is recorded server-side + // against this user, so clearing it here keeps it cleared for every + // authenticated flow in the run (shared test DB). + await dismissSystemNotices(page, 10_000) await page.context().storageState({ path: stateFile }) }) diff --git a/client/e2e/helpers.ts b/client/e2e/helpers.ts index c91d48152..fa6d43d3d 100644 --- a/client/e2e/helpers.ts +++ b/client/e2e/helpers.ts @@ -1,22 +1,43 @@ import type { Page } from '@playwright/test' /** - * Dismiss the release-notice modal (SystemNoticeHost), which greets a freshly seeded - * user on first load and covers the dashboard — its backdrop swallows clicks aimed at - * anything underneath, `.add-trip-card` included. + * Dismiss the system-notice modal(s) (SystemNoticeHost), which greet a freshly + * seeded user on first load and cover the dashboard — the backdrop swallows + * clicks aimed at anything underneath, `.add-trip-card` included. * - * The X only appears on the notice's last page, so page through first. Dismissal is - * persisted server-side per user, but each spec gets a fresh DB, so every spec that - * touches the dashboard has to clear it. + * The host renders asynchronously (after the notices fetch), so wait for the + * notice dialog before deciding there is nothing to clear. Every lookup is + * scoped INSIDE the dialog — an unscoped /next/i can match dashboard buttons + * (carousel arrows) and satisfy the wait before the modal even mounts. + * + * A notice closes one of two ways depending on its shape: + * - CTA-bearing notices (e.g. the thank-you/support modal) only offer the + * X button (`aria-label="Dismiss"`), shown on the last page. + * - CTA-less notices show an "OK" button that pages forward and dismisses on + * the last page. + * Multi-page notices are paged through via the pager's Next button first. + * Dismissal is persisted server-side per user, so clearing once keeps it + * cleared for every later spec in the run (shared test DB). */ -export async function dismissSystemNotices(page: Page): Promise { - const next = page.getByRole('button', { name: /next/i }) - for (let i = 0; i < 6 && (await next.isVisible().catch(() => false)); i++) { - if (!(await next.isEnabled())) break - await next.click() +export async function dismissSystemNotices(page: Page, appearTimeoutMs = 3_000): Promise { + const dialog = page.getByRole('dialog').first() + await dialog.waitFor({ state: 'visible', timeout: appearTimeoutMs }).catch(() => {}) + + // Clear up to a handful of queued notices. + for (let notice = 0; notice < 4 && (await dialog.isVisible().catch(() => false)); notice++) { + const next = dialog.getByRole('button', { name: /next/i }) + for (let i = 0; i < 8 && (await next.isVisible().catch(() => false)); i++) { + if (!(await next.isEnabled().catch(() => false))) break + await next.click() + } + const dismiss = dialog.getByRole('button', { name: 'Dismiss', exact: true }) + const ok = dialog.getByRole('button', { name: 'OK', exact: true }) + if (await dismiss.isVisible().catch(() => false)) await dismiss.click() + else if (await ok.isVisible().catch(() => false)) await ok.click() + else break + // Exit animation + the next queued notice mounting. + await page.waitForTimeout(400) } - const dismiss = page.getByRole('button', { name: 'Dismiss' }) - if (await dismiss.isVisible().catch(() => false)) await dismiss.click() - await dismiss.waitFor({ state: 'detached' }).catch(() => {}) + await dialog.waitFor({ state: 'detached', timeout: 5_000 }).catch(() => {}) }