From 99e028b040a2bfbf92f7287a6ecaa01c1970f604 Mon Sep 17 00:00:00 2001 From: Daniel Castro <2968031+odanieldcs@users.noreply.github.com> Date: Mon, 12 Oct 2026 13:27:35 -0300 Subject: [PATCH 1/2] test(e2e): catch accessibility, overflow, and console regressions (ENG-160) Axe, mobile overflow, and console guards fail when contrast, min-width, or hydration bugs return. WebKit only runs the smoke tag. --- README.md | 12 ++-- e2e/axe.spec.ts | 58 +++++++++++++++++++ e2e/console.spec.ts | 34 ++++++++++++ e2e/helpers/axe.ts | 18 ++++++ e2e/helpers/console.ts | 21 +++++-- e2e/helpers/guard-routes.ts | 18 ++++++ e2e/helpers/overflow.ts | 27 +++++++++ e2e/helpers/posts.ts | 4 ++ e2e/mobile-menu.spec.ts | 108 +++++++++++++++++++----------------- e2e/overflow.spec.ts | 18 ++++++ e2e/skip-link.spec.ts | 4 +- e2e/smoke.spec.ts | 62 +++++++++++++++++++++ package.json | 1 + playwright.config.ts | 11 ++++ pnpm-lock.yaml | 19 +++++++ 15 files changed, 352 insertions(+), 63 deletions(-) create mode 100644 e2e/axe.spec.ts create mode 100644 e2e/console.spec.ts create mode 100644 e2e/helpers/axe.ts create mode 100644 e2e/helpers/guard-routes.ts create mode 100644 e2e/helpers/overflow.ts create mode 100644 e2e/overflow.spec.ts create mode 100644 e2e/smoke.spec.ts diff --git a/README.md b/README.md index 1707efd..eaf7a16 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Personal website of Daniel Castro. Static-first Next.js app, hosted on Vercel. - Biome (lint and format) - MDX loaded from `content/` in `lib/content` (gray-matter, Zod, `next-mdx-remote`) - Vitest and Testing Library -- Playwright (Chromium) +- Playwright (`chromium`, `mobile`, and `webkit`) - Vercel ## Getting started @@ -31,16 +31,18 @@ Environment variables are optional; the site runs locally without them. `NEXT_PU - `pnpm build` — production build - `pnpm start` — serve the production build - `pnpm test` — unit tests (Vitest) -- `pnpm test:e2e` — E2E smoke tests (Playwright, Chromium) +- `pnpm test:e2e` — Playwright. Untagged tests run in `chromium` (Desktop Chrome). `@mobile` runs in `mobile` (Pixel 7). `@smoke` runs in `webkit` (Desktop Safari). - `pnpm lint` — Biome check - `pnpm format` — Biome check and write - `pnpm typecheck` — `tsc --noEmit` -Install Playwright's Chromium browser once before the first E2E run: +Install Chromium and WebKit once before the first local E2E run. CI uses the Playwright image, which already includes the browsers, and does not install them: ```sh -pnpm exec playwright install +pnpm exec playwright install chromium webkit pnpm test:e2e +pnpm test:e2e -- --project=mobile +pnpm test:e2e -- --project=webkit ``` `pnpm test:e2e` builds and starts the app (`pnpm build && pnpm start`) via Playwright's `webServer`. @@ -52,7 +54,7 @@ pnpm test:e2e - `public/` — static files, including `media/` for posts, personal photos, icons, illustrations, and documents - `lib/` — shared TypeScript modules - `components/` — UI by responsibility: `views/` (pages), `layout/` (shell, header, footer, container), `mdx/` (MDX blocks), `ui/` (controls and icons). Providers and route infra stay at the root (`theme-provider`, `interface-language-provider`, `json-ld`, `route-error-boundary`). -- `e2e/` — Playwright smoke tests +- `e2e/` — Playwright tests (`chromium`, `@mobile`, `@smoke`) `@/foo` resolves to `/foo` via `compilerOptions.paths` in `tsconfig.json`. Example: `@/lib/site` → `lib/site.ts`. There is no `~/*` alias. diff --git a/e2e/axe.spec.ts b/e2e/axe.spec.ts new file mode 100644 index 0000000..96ecbfd --- /dev/null +++ b/e2e/axe.spec.ts @@ -0,0 +1,58 @@ +import { expect, type Page, test } from '@playwright/test' +import { collectAxeViolations } from './helpers/axe' +import { guardRoutes } from './helpers/guard-routes' + +const themes = ['dark', 'light'] as const + +type AxeViolation = Awaited>[number] + +function formatViolations( + route: string, + theme: (typeof themes)[number], + violations: AxeViolation[], +): string { + return violations + .flatMap((violation) => + violation.nodes.map((node) => { + const selector = node.target + .map((part) => (Array.isArray(part) ? part.join(' ') : part)) + .join(' ') + return `${violation.id} on ${route} (${theme}): ${selector}` + }), + ) + .join('\n') +} + +async function openThemedRoute( + page: Page, + route: string, + theme: (typeof themes)[number], +) { + await page.addInitScript((value) => { + localStorage.setItem('theme', value) + }, theme) + await page.goto(route) + + const html = page.locator('html') + if (theme === 'dark') { + await expect(html).toHaveClass(/\bdark\b/, { timeout: 5000 }) + } else { + await expect(html).not.toHaveClass(/\bdark\b/) + } + + await expect(page.getByRole('main')).toBeVisible() + await page.evaluate(async () => { + await document.fonts.ready + }) +} + +for (const route of guardRoutes) { + for (const theme of themes) { + test(`${route} ${theme} has no axe violations`, async ({ page }) => { + await openThemedRoute(page, route, theme) + + const violations = await collectAxeViolations(page) + expect(formatViolations(route, theme, violations)).toBe('') + }) + } +} diff --git a/e2e/console.spec.ts b/e2e/console.spec.ts new file mode 100644 index 0000000..b163d0c --- /dev/null +++ b/e2e/console.spec.ts @@ -0,0 +1,34 @@ +import { expect, test } from '@playwright/test' +import { type ConsoleIssue, collectConsoleErrors } from './helpers/console' +import { guardRoutes } from './helpers/guard-routes' + +/** + * Chromium logs the 404 document as + * "Failed to load resource: the server responded with a status of 404". + * That message is ignored only on `/qualquer`, and only when `location().url` + * is that document. Any other 404, on any route, fails the test. + */ +function isAllowedDocument404(route: string, issue: ConsoleIssue): boolean { + if (route !== '/qualquer' || !issue.locationUrl) { + return false + } + + const pathname = URL.parse(issue.locationUrl)?.pathname + return ( + pathname === '/qualquer' && + issue.text.includes('Failed to load resource') && + issue.text.includes('404') + ) +} + +for (const route of guardRoutes) { + test(`${route} has no console errors`, async ({ page }) => { + const issues = collectConsoleErrors(page) + await page.goto(route) + await expect(page.getByRole('main')).toBeVisible() + + expect( + issues.filter((issue) => !isAllowedDocument404(route, issue)), + ).toEqual([]) + }) +} diff --git a/e2e/helpers/axe.ts b/e2e/helpers/axe.ts new file mode 100644 index 0000000..ce42762 --- /dev/null +++ b/e2e/helpers/axe.ts @@ -0,0 +1,18 @@ +import AxeBuilder from '@axe-core/playwright' +import type { Page } from '@playwright/test' + +const wcagTags = [ + 'wcag2a', + 'wcag2aa', + 'wcag21a', + 'wcag21aa', + 'wcag22aa', +] as const + +/** WCAG A/AA violations. `incomplete` results are left out of the assertion. */ +export function collectAxeViolations(page: Page) { + return new AxeBuilder({ page }) + .withTags([...wcagTags]) + .analyze() + .then((results) => results.violations) +} diff --git a/e2e/helpers/console.ts b/e2e/helpers/console.ts index 4aab3d9..13ec120 100644 --- a/e2e/helpers/console.ts +++ b/e2e/helpers/console.ts @@ -1,16 +1,27 @@ import type { Page } from '@playwright/test' -export function collectConsoleErrors(page: Page): string[] { - const consoleErrors: string[] = [] +const hydrationWarning = /hydration|did not match|server-rendered html/i + +export type ConsoleIssue = { + text: string + locationUrl?: string +} + +export function collectConsoleErrors(page: Page): ConsoleIssue[] { + const consoleErrors: ConsoleIssue[] = [] page.on('console', (msg) => { - if (msg.type() === 'error') { - consoleErrors.push(msg.text()) + const text = msg.text() + if ( + msg.type() === 'error' || + (msg.type() === 'warning' && hydrationWarning.test(text)) + ) { + consoleErrors.push({ text, locationUrl: msg.location().url }) } }) page.on('pageerror', (error) => { - consoleErrors.push(error.message) + consoleErrors.push({ text: error.message }) }) return consoleErrors diff --git a/e2e/helpers/guard-routes.ts b/e2e/helpers/guard-routes.ts new file mode 100644 index 0000000..302833c --- /dev/null +++ b/e2e/helpers/guard-routes.ts @@ -0,0 +1,18 @@ +/** Shared by the axe and console guards. Overflow unions this list with every post. */ +export const guardRoutes = [ + '/', + '/en', + '/blog', + '/blog?view=grid', + '/en/blog', + '/about', + '/en/about', + '/community', + '/en/community', + '/privacy', + '/alunos', + '/qualquer', + '/blog/postgresql-e-pgadmin-com-docker-compose', + '/blog/16-livros-para-desenvolvedores-ler-em-2022', + '/en/blog/postgresql-e-pgadmin-com-docker-compose', +] as const diff --git a/e2e/helpers/overflow.ts b/e2e/helpers/overflow.ts new file mode 100644 index 0000000..a27c7e7 --- /dev/null +++ b/e2e/helpers/overflow.ts @@ -0,0 +1,27 @@ +import { expect, type Page } from '@playwright/test' +import { guardRoutes } from './guard-routes' +import { getAllBlogPosts } from './posts' + +export function overflowRoutes(): string[] { + const postRoutes = getAllBlogPosts('overflow').map( + (post) => `/blog/${post.slug}`, + ) + + return [...new Set([...guardRoutes, ...postRoutes])] +} + +export async function expectNoHorizontalOverflow( + page: Page, + route: string, + width: number, +) { + const { scrollWidth, clientWidth } = await page.evaluate(() => ({ + scrollWidth: document.documentElement.scrollWidth, + clientWidth: document.documentElement.clientWidth, + })) + + expect( + scrollWidth, + `${route} at ${width}px overflows: scrollWidth ${scrollWidth}, clientWidth ${clientWidth}`, + ).toBe(clientWidth) +} diff --git a/e2e/helpers/posts.ts b/e2e/helpers/posts.ts index 578a57b..77c74fc 100644 --- a/e2e/helpers/posts.ts +++ b/e2e/helpers/posts.ts @@ -14,6 +14,10 @@ function loadPosts(context: string): PostSummary[] { return cachedPosts } +export function getAllBlogPosts(context: string): PostSummary[] { + return loadPosts(context) +} + export function getNewestBlogPost(context: string): PostSummary { const posts = loadPosts(context) const newest = posts[0] diff --git a/e2e/mobile-menu.spec.ts b/e2e/mobile-menu.spec.ts index be07efb..baa06bd 100644 --- a/e2e/mobile-menu.spec.ts +++ b/e2e/mobile-menu.spec.ts @@ -1,7 +1,5 @@ import { expect, type Page, test } from '@playwright/test' -test.use({ viewport: { width: 390, height: 844 } }) - function focusedRegion(page: Page) { return page.evaluate(() => { const current = document.activeElement @@ -28,68 +26,74 @@ function focusedRegion(page: Page) { }) } -test.beforeEach(async ({ page }) => { - await page.goto('/') -}) +test.describe('mobile menu', { tag: '@mobile' }, () => { + test.use({ viewport: { width: 390, height: 844 } }) -test('Enter and Space open the menu on the Blog link', async ({ page }) => { - const trigger = page.getByRole('button', { name: 'Menu' }) - await trigger.focus() - await page.keyboard.press('Enter') + test.beforeEach(async ({ page }) => { + await page.goto('/') + }) - const blog = page - .getByRole('navigation', { name: 'Navegação principal' }) - .getByRole('link', { name: 'Blog' }) - await expect(blog).toBeFocused() + test('Enter and Space open the menu on the Blog link', async ({ page }) => { + const trigger = page.getByRole('button', { name: 'Menu' }) + await trigger.focus() + await page.keyboard.press('Enter') - await page.keyboard.press('Escape') - await expect(trigger).toBeFocused() + const blog = page + .getByRole('navigation', { name: 'Navegação principal' }) + .getByRole('link', { name: 'Blog' }) + await expect(blog).toBeFocused() - await page.keyboard.press('Space') - await expect(blog).toBeFocused() -}) + await page.keyboard.press('Escape') + await expect(trigger).toBeFocused() -test('Tab and Shift+Tab stay in the header and the menu', async ({ page }) => { - const trigger = page.getByRole('button', { name: 'Menu' }) - await trigger.focus() - await page.keyboard.press('Enter') + await page.keyboard.press('Space') + await expect(blog).toBeFocused() + }) - const blog = page - .getByRole('navigation', { name: 'Navegação principal' }) - .getByRole('link', { name: 'Blog' }) - await expect(blog).toBeFocused() + test('Tab and Shift+Tab stay in the header and the menu', async ({ + page, + }) => { + const trigger = page.getByRole('button', { name: 'Menu' }) + await trigger.focus() + await page.keyboard.press('Enter') - await page.keyboard.press('Shift+Tab') - expect(await focusedRegion(page)).toBe('header') + const blog = page + .getByRole('navigation', { name: 'Navegação principal' }) + .getByRole('link', { name: 'Blog' }) + await expect(blog).toBeFocused() - for (let step = 0; step < 10; step += 1) { await page.keyboard.press('Shift+Tab') - const region = await focusedRegion(page) - expect(['header', 'menu', 'body'], region).toContain(region) - } + expect(await focusedRegion(page)).toBe('header') - await blog.focus() - await expect(blog).toBeFocused() - await page.keyboard.press('Tab') - expect(await focusedRegion(page)).toBe('menu') + for (let step = 0; step < 10; step += 1) { + await page.keyboard.press('Shift+Tab') + const region = await focusedRegion(page) + expect(['header', 'menu', 'body'], region).toContain(region) + } - for (let step = 0; step < 10; step += 1) { + await blog.focus() + await expect(blog).toBeFocused() await page.keyboard.press('Tab') - const region = await focusedRegion(page) - expect(['header', 'menu', 'body'], region).toContain(region) - } -}) + expect(await focusedRegion(page)).toBe('menu') -test('Escape returns focus to the trigger and clears inert', async ({ - page, -}) => { - const trigger = page.getByRole('button', { name: 'Menu' }) - await trigger.focus() - await page.keyboard.press('Enter') - await expect(page.locator('[inert]').first()).toBeAttached() + for (let step = 0; step < 10; step += 1) { + await page.keyboard.press('Tab') + const region = await focusedRegion(page) + expect(['header', 'menu', 'body'], region).toContain(region) + } + }) - await page.keyboard.press('Escape') - await expect(trigger).toBeFocused() - await expect(trigger).toHaveAttribute('aria-expanded', 'false') - await expect(page.locator('[inert]')).toHaveCount(0) + test('Escape returns focus to the trigger and clears inert', async ({ + page, + }) => { + const trigger = page.getByRole('button', { name: 'Menu' }) + await trigger.focus() + await page.keyboard.press('Enter') + await expect(page.locator('[inert]').first()).toBeAttached() + + await page.keyboard.press('Escape') + await expect(trigger).toBeFocused() + await expect(trigger).toHaveAttribute('aria-expanded', 'false') + await expect(page.locator('[inert]')).toHaveCount(0) + }) }) diff --git a/e2e/overflow.spec.ts b/e2e/overflow.spec.ts new file mode 100644 index 0000000..1ccf79e --- /dev/null +++ b/e2e/overflow.spec.ts @@ -0,0 +1,18 @@ +import { test } from '@playwright/test' +import { expectNoHorizontalOverflow, overflowRoutes } from './helpers/overflow' + +const widths = [320, 390] as const +const routes = overflowRoutes() + +// Pixel 7 only selects this project. +test.describe('horizontal overflow', { tag: '@mobile' }, () => { + for (const route of routes) { + for (const width of widths) { + test(`${route} does not overflow at ${width}px`, async ({ page }) => { + await page.setViewportSize({ width, height: 844 }) + await page.goto(route) + await expectNoHorizontalOverflow(page, route, width) + }) + } + } +}) diff --git a/e2e/skip-link.spec.ts b/e2e/skip-link.spec.ts index 7924bff..09013a0 100644 --- a/e2e/skip-link.spec.ts +++ b/e2e/skip-link.spec.ts @@ -27,7 +27,9 @@ test('first Tab skips to content on a post', async ({ page }) => { await skipToContent(page, '/blog/meu-blog-profissional') }) -test('open mobile menu makes the skip link inert', async ({ page }) => { +test('open mobile menu makes the skip link inert', { tag: '@mobile' }, async ({ + page, +}) => { await page.setViewportSize({ width: 390, height: 844 }) await page.goto('/') await page.getByRole('button', { name: 'Menu' }).click() diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts new file mode 100644 index 0000000..fbd4135 --- /dev/null +++ b/e2e/smoke.spec.ts @@ -0,0 +1,62 @@ +import { expect, test } from '@playwright/test' + +const lightBackground = 'rgb(242, 235, 235)' +const darkBackground = 'rgb(16, 13, 13)' + +test.describe('webkit smoke', { tag: '@smoke' }, () => { + test('home shows the Portuguese H1', async ({ page }) => { + await page.goto('/') + await expect( + page.getByRole('heading', { + level: 1, + name: 'Criando e compartilhando, de Dev para Dev.', + }), + ).toBeVisible() + }) + + test('blog shows the blog H1', async ({ page }) => { + await page.goto('/blog') + await expect( + page.getByRole('heading', { + level: 1, + name: 'Aprendizados e ideias.', + }), + ).toBeVisible() + }) + + test('postgresql article shows its H1', async ({ page }) => { + await page.goto('/blog/postgresql-e-pgadmin-com-docker-compose') + await expect( + page.getByRole('heading', { + level: 1, + name: 'PostgreSQL e pgAdmin com Docker Compose', + }), + ).toBeVisible() + }) + + test('theme toggle switches the background', async ({ page }) => { + await page.goto('/') + + await page.getByRole('button', { name: 'Modo claro' }).click() + await expect(page.locator('html')).not.toHaveClass(/\bdark\b/) + await expect(page.locator('body')).toHaveCSS( + 'background-color', + lightBackground, + ) + + await page.getByRole('button', { name: 'Modo escuro' }).click() + await expect(page.locator('body')).toHaveCSS( + 'background-color', + darkBackground, + ) + }) + + test('language switch goes to English and back', async ({ page }) => { + await page.goto('/') + await page.getByRole('link', { name: 'Switch to English' }).click() + await expect(page).toHaveURL('/en') + + await page.getByRole('link', { name: 'Mudar para português' }).click() + await expect(page).toHaveURL('/') + }) +}) diff --git a/package.json b/package.json index c0a8d21..5c36bb8 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "zod": "^4.6.5" }, "devDependencies": { + "@axe-core/playwright": "4.13.0", "@biomejs/biome": "^2.5.14", "@playwright/test": "^1.63.0", "@testing-library/dom": "^10.4.2", diff --git a/playwright.config.ts b/playwright.config.ts index 011215a..8d671f4 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -16,6 +16,17 @@ export default defineConfig({ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, + grepInvert: /@mobile|@smoke/, + }, + { + name: 'mobile', + use: { ...devices['Pixel 7'] }, + grep: /@mobile/, + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + grep: /@smoke/, }, ], webServer: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a2e793..37bb7d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,6 +51,9 @@ importers: specifier: ^4.6.5 version: 4.6.5 devDependencies: + '@axe-core/playwright': + specifier: 4.13.0 + version: 4.13.0(playwright-core@1.63.0) '@biomejs/biome': specifier: ^2.5.14 version: 2.5.14 @@ -99,6 +102,11 @@ packages: resolution: {integrity: sha512-NT4s3yZLjovPpliRpTvdsdzyPjqRqiCZj9MxnarBihbaY5MbAG7DWAJcrLlhmC7xKTNCXsTELcqB8YsqpLnUFQ==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + '@axe-core/playwright@4.13.0': + resolution: {integrity: sha512-6YLx+kxXu5GJceG4ozFg+33a2EMTdjYwWGloJ3sb9Kta5pp+ZNS53uxGVog5JetIY8s++P5UrtX+cri+u0VAVg==} + peerDependencies: + playwright-core: '>= 1.0.0' + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -846,6 +854,10 @@ packages: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true + axe-core@4.13.0: + resolution: {integrity: sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==} + engines: {node: '>=4'} + bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -1920,6 +1932,11 @@ snapshots: is-potential-custom-element-name: 1.0.1 lru-cache: 11.5.3 + '@axe-core/playwright@4.13.0(playwright-core@1.63.0)': + dependencies: + axe-core: 4.13.0 + playwright-core: 1.63.0 + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -2490,6 +2507,8 @@ snapshots: astring@1.9.0: {} + axe-core@4.13.0: {} + bail@2.0.2: {} baseline-browser-mapping@2.11.25: {} From 0d47647a7c276f78a0d7933adf886013bf7ae767 Mon Sep 17 00:00:00 2001 From: Daniel Castro <2968031+odanieldcs@users.noreply.github.com> Date: Mon, 12 Oct 2026 13:42:15 -0300 Subject: [PATCH 2/2] test(e2e): measure overflow once layout is ready (ENG-160) Waiting for the load event stalls on the PM2 post in CI, so the guard never reaches scrollWidth. --- e2e/helpers/overflow.ts | 10 ++++++++++ e2e/overflow.spec.ts | 8 ++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/e2e/helpers/overflow.ts b/e2e/helpers/overflow.ts index a27c7e7..ecd7028 100644 --- a/e2e/helpers/overflow.ts +++ b/e2e/helpers/overflow.ts @@ -10,6 +10,16 @@ export function overflowRoutes(): string[] { return [...new Set([...guardRoutes, ...postRoutes])] } +export async function openForOverflowCheck(page: Page, route: string) { + // `load` also waits for images. One post never reaches it in CI, which + // hides the scrollWidth check. Layout is ready with the document and fonts. + await page.goto(route, { waitUntil: 'domcontentloaded' }) + await expect(page.getByRole('main')).toBeVisible() + await page.evaluate(async () => { + await document.fonts.ready + }) +} + export async function expectNoHorizontalOverflow( page: Page, route: string, diff --git a/e2e/overflow.spec.ts b/e2e/overflow.spec.ts index 1ccf79e..0b4ddb4 100644 --- a/e2e/overflow.spec.ts +++ b/e2e/overflow.spec.ts @@ -1,5 +1,9 @@ import { test } from '@playwright/test' -import { expectNoHorizontalOverflow, overflowRoutes } from './helpers/overflow' +import { + expectNoHorizontalOverflow, + openForOverflowCheck, + overflowRoutes, +} from './helpers/overflow' const widths = [320, 390] as const const routes = overflowRoutes() @@ -10,7 +14,7 @@ test.describe('horizontal overflow', { tag: '@mobile' }, () => { for (const width of widths) { test(`${route} does not overflow at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 844 }) - await page.goto(route) + await openForOverflowCheck(page, route) await expectNoHorizontalOverflow(page, route, width) }) }