diff --git a/test/a11y/e2e.spec.ts b/test/a11y/e2e.spec.ts index f44070b58c4..361c9f399fb 100644 --- a/test/a11y/e2e.spec.ts +++ b/test/a11y/e2e.spec.ts @@ -1,4 +1,4 @@ -import type { Page, TestInfo } from '@playwright/test' +import type { Page } from '@playwright/test' import { expect, test } from '@playwright/test' import { openNav } from 'helpers/e2e/toggleNav.js' @@ -7,8 +7,11 @@ import { fileURLToPath } from 'url' import { ensureCompilationIsDone, initPageConsoleErrorCatch } from '../helpers.js' import { AdminUrlUtil } from '../helpers/adminUrlUtil.js' -import { checkFocusIndicators } from '../helpers/e2e/checkFocusIndicators.js' -import { checkHorizontalOverflow } from '../helpers/e2e/checkHorizontalOverflow.js' +import { assertAllElementsHaveFocusIndicators } from '../helpers/e2e/checkFocusIndicators.js' +import { + assertNoHorizontalOverflow, + checkHorizontalOverflow, +} from '../helpers/e2e/checkHorizontalOverflow.js' import { runAxeScan } from '../helpers/e2e/runAxeScan.js' import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js' import { TEST_TIMEOUT_LONG } from '../playwright.config.js' @@ -46,7 +49,7 @@ test.describe('A11y', () => { test('Dashboard', async ({}, testInfo) => { await page.goto(postsUrl.admin) - await page.locator('.dashboard').waitFor() + await expect(page.locator('.dashboard')).toBeVisible() const accessibilityScanResults = await runAxeScan({ page, testInfo }) @@ -68,7 +71,7 @@ test.describe('A11y', () => { test.fixme('Account page', async ({}, testInfo) => { await page.goto(postsUrl.account) - await page.locator('.auth-fields').waitFor() + await expect(page.locator('.auth-fields')).toBeVisible() const accessibilityScanResults = await runAxeScan({ page, @@ -83,7 +86,7 @@ test.describe('A11y', () => { test.fixme('list view', async ({}, testInfo) => { await page.goto(postsUrl.list) - await page.locator('.list-controls').waitFor() + await expect(page.locator('.list-controls')).toBeVisible() const accessibilityScanResults = await runAxeScan({ page, testInfo }) @@ -93,7 +96,7 @@ test.describe('A11y', () => { test.fixme('create view', async ({}, testInfo) => { await page.goto(postsUrl.create) - await page.locator('#field-title').waitFor() + await expect(page.locator('#field-title')).toBeVisible() const accessibilityScanResults = await runAxeScan({ page, testInfo }) @@ -104,7 +107,7 @@ test.describe('A11y', () => { await page.goto(postsUrl.list) await page.locator('.table a').first().click() - await page.locator('#field-title').waitFor() + await expect(page.locator('#field-title')).toBeVisible() const accessibilityScanResults = await runAxeScan({ page, testInfo }) @@ -116,7 +119,7 @@ test.describe('A11y', () => { test('list view', async ({}, testInfo) => { await page.goto(mediaUrl.list) - await page.locator('.list-controls').waitFor() + await expect(page.locator('.list-controls')).toBeVisible() const accessibilityScanResults = await runAxeScan({ page, testInfo }) @@ -126,7 +129,7 @@ test.describe('A11y', () => { test.fixme('create view', async ({}, testInfo) => { await page.goto(mediaUrl.create) - await page.locator('.file-field').first().waitFor() + await expect(page.locator('.file-field').first()).toBeVisible() const accessibilityScanResults = await runAxeScan({ page, testInfo }) @@ -138,85 +141,64 @@ test.describe('A11y', () => { test('Dashboard - should have visible focus indicators', async ({}, testInfo) => { await page.goto(postsUrl.admin) - await page.locator('.dashboard').waitFor() + await expect(page.locator('.dashboard')).toBeVisible() - const result = await checkFocusIndicators({ + await assertAllElementsHaveFocusIndicators({ page, testInfo, verbose: false, selector: '.dashboard', }) - - expect.soft(result.totalFocusableElements).toBeGreaterThan(0) - expect.soft(result.elementsWithoutIndicators).toBe(0) }) test('Posts create view - fields should have visible focus indicators', async ({}, testInfo) => { await page.goto(postsUrl.create) - await page.locator('#field-title').waitFor() + await expect(page.locator('#field-title')).toBeVisible() - const result = await checkFocusIndicators({ + await assertAllElementsHaveFocusIndicators({ page, selector: 'main.collection-edit', testInfo, }) - - expect.soft(result.totalFocusableElements).toBeGreaterThan(0) - expect.soft(result.elementsWithoutIndicators).toBe(0) }) - test.fixme( - 'Posts create view - breadcrumbs should have visible focus indicators', - async ({}, testInfo) => { - await page.goto(postsUrl.create) - - await page.locator('#field-title').waitFor() - - const result = await checkFocusIndicators({ - page, - selector: '.app-header__controls-wrapper', - testInfo, - }) + test.fixme('Posts create view - breadcrumbs should have visible focus indicators', async ({}, testInfo) => { + await page.goto(postsUrl.create) - expect.soft(result.totalFocusableElements).toBeGreaterThan(0) - expect.soft(result.elementsWithoutIndicators).toBe(0) - }, - ) + await expect(page.locator('#field-title')).toBeVisible() - test.fixme( - 'Navigation sidebar - should have visible focus indicators', - async ({}, testInfo) => { - await page.goto(postsUrl.admin) + await assertAllElementsHaveFocusIndicators({ + page, + selector: '.app-header__controls-wrapper', + testInfo, + }) + }) - await page.locator('.nav').waitFor() + test.fixme('Navigation sidebar - should have visible focus indicators', async ({}, testInfo) => { + await page.goto(postsUrl.admin) - await openNav(page) + await expect(page.locator('.nav')).toBeVisible() - const result = await checkFocusIndicators({ - page, - selector: '.nav', - testInfo, - }) + await openNav(page) - expect.soft(result.totalFocusableElements).toBeGreaterThan(0) - expect.soft(result.elementsWithoutIndicators).toBe(0) - }, - ) + await assertAllElementsHaveFocusIndicators({ + page, + selector: '.nav', + testInfo, + }) + }) test.fixme('Account page - should have visible focus indicators', async ({}, testInfo) => { await page.goto(postsUrl.account) - await page.locator('.auth-fields').waitFor() + await expect(page.locator('.auth-fields')).toBeVisible() - const result = await checkFocusIndicators({ + await assertAllElementsHaveFocusIndicators({ page, testInfo, verbose: false, }) - - expect.soft(result.totalFocusableElements).toBeGreaterThan(0) - expect.soft(result.elementsWithoutIndicators).toBe(0) }) }) @@ -224,90 +206,66 @@ test.describe('A11y', () => { test('Dashboard - should not have horizontal overflow at 320px', async ({}, testInfo) => { await page.setViewportSize({ width: 320, height: 568 }) await page.goto(postsUrl.admin) - await page.locator('.dashboard').waitFor() - - const result = await checkHorizontalOverflow(page, testInfo) + await expect(page.locator('.dashboard')).toBeVisible() - expect(result.hasHorizontalOverflow).toBe(false) - expect(result.overflowingElements.length).toBe(0) + await assertNoHorizontalOverflow(page, testInfo) }) test('Account page - should not have horizontal overflow at 320px', async ({}, testInfo) => { await page.setViewportSize({ width: 320, height: 568 }) await page.goto(postsUrl.account) - await page.locator('.auth-fields').waitFor() - - const result = await checkHorizontalOverflow(page, testInfo) + await expect(page.locator('.auth-fields')).toBeVisible() - expect(result.hasHorizontalOverflow).toBe(false) - expect(result.overflowingElements.length).toBe(0) + await assertNoHorizontalOverflow(page, testInfo) }) test('Posts list view - should not have horizontal overflow at 320px', async ({}, testInfo) => { await page.setViewportSize({ width: 320, height: 568 }) await page.goto(postsUrl.list) - await page.locator('.collection-list').waitFor() + await expect(page.locator('.collection-list')).toBeVisible() - const result = await checkHorizontalOverflow(page, testInfo) - - expect(result.hasHorizontalOverflow).toBe(false) - expect(result.overflowingElements.length).toBe(0) + await assertNoHorizontalOverflow(page, testInfo) }) test('Posts create view - should not have horizontal overflow at 320px', async ({}, testInfo) => { await page.setViewportSize({ width: 320, height: 568 }) await page.goto(postsUrl.create) - await page.locator('#field-title').waitFor() - - const result = await checkHorizontalOverflow(page, testInfo) + await expect(page.locator('#field-title')).toBeVisible() - expect(result.hasHorizontalOverflow).toBe(false) - expect(result.overflowingElements.length).toBe(0) + await assertNoHorizontalOverflow(page, testInfo) }) test('Posts edit view - should not have horizontal overflow at 320px', async ({}, testInfo) => { await page.setViewportSize({ width: 320, height: 568 }) await page.goto(postsUrl.list) await page.locator('.table a').first().click() - await page.locator('#field-title').waitFor() - - const result = await checkHorizontalOverflow(page, testInfo) + await expect(page.locator('#field-title')).toBeVisible() - expect(result.hasHorizontalOverflow).toBe(false) - expect(result.overflowingElements.length).toBe(0) + await assertNoHorizontalOverflow(page, testInfo) }) test('Media list view - should not have horizontal overflow at 320px', async ({}, testInfo) => { await page.setViewportSize({ width: 320, height: 568 }) await page.goto(mediaUrl.list) - await page.locator('.list-controls').waitFor() + await expect(page.locator('.list-controls')).toBeVisible() - const result = await checkHorizontalOverflow(page, testInfo) - - expect(result.hasHorizontalOverflow).toBe(false) - expect(result.overflowingElements.length).toBe(0) + await assertNoHorizontalOverflow(page, testInfo) }) test('Media create view - should not have horizontal overflow at 320px', async ({}, testInfo) => { await page.setViewportSize({ width: 320, height: 568 }) await page.goto(mediaUrl.create) - await page.locator('.file-field').first().waitFor() - - const result = await checkHorizontalOverflow(page, testInfo) + await expect(page.locator('.file-field').first()).toBeVisible() - expect(result.hasHorizontalOverflow).toBe(false) - expect(result.overflowingElements.length).toBe(0) + await assertNoHorizontalOverflow(page, testInfo) }) test('Navigation sidebar - should not have horizontal overflow at 320px', async ({}, testInfo) => { await page.setViewportSize({ width: 320, height: 568 }) await page.goto(postsUrl.admin) - await page.locator('.nav').waitFor() - - const result = await checkHorizontalOverflow(page, testInfo) + await expect(page.locator('.nav')).toBeVisible() - expect(result.hasHorizontalOverflow).toBe(false) - expect(result.overflowingElements.length).toBe(0) + await assertNoHorizontalOverflow(page, testInfo) }) }) @@ -323,7 +281,7 @@ test.describe('A11y', () => { for (const { level, scale } of zoomLevels) { test(`should be usable at ${level}% zoom`, async ({}, testInfo) => { await page.goto(postsUrl.admin) - await page.locator('.dashboard').waitFor() + await expect(page.locator('.dashboard')).toBeVisible() // Simulate zoom by setting device scale factor await page.evaluate((zoomScale) => { @@ -335,6 +293,7 @@ test.describe('A11y', () => { // At high zoom levels, some horizontal overflow might be acceptable // but we should at least verify the page is still functional + // eslint-disable-next-line playwright/no-conditional-in-test if (level <= 200) { // At 200% or less, should not have overflow expect(overflowResult.hasHorizontalOverflow).toBe(false) @@ -351,7 +310,7 @@ test.describe('A11y', () => { for (const { level, scale } of zoomLevels) { test(`should be usable at ${level}% zoom`, async ({}, testInfo) => { await page.goto(postsUrl.create) - await page.locator('#field-title').waitFor() + await expect(page.locator('#field-title')).toBeVisible() await page.evaluate((zoomScale) => { document.body.style.zoom = String(zoomScale) @@ -378,7 +337,7 @@ test.describe('A11y', () => { for (const { level, scale } of zoomLevels) { test(`should be usable at ${level}% zoom`, async ({}, testInfo) => { await page.goto(postsUrl.list) - await page.locator('.list-controls').waitFor() + await expect(page.locator('.list-controls')).toBeVisible() await page.evaluate((zoomScale) => { document.body.style.zoom = String(zoomScale) @@ -405,7 +364,7 @@ test.describe('A11y', () => { for (const { level, scale } of zoomLevels) { test.fixme(`should be usable at ${level}% zoom`, async ({}, testInfo) => { await page.goto(postsUrl.account) - await page.locator('.auth-fields').waitFor() + await expect(page.locator('.auth-fields')).toBeVisible() await page.evaluate((zoomScale) => { document.body.style.zoom = String(zoomScale) @@ -427,7 +386,7 @@ test.describe('A11y', () => { for (const { level, scale } of zoomLevels) { test(`Media list view should be usable at ${level}% zoom`, async ({}, testInfo) => { await page.goto(mediaUrl.list) - await page.locator('.collection-list').waitFor() + await expect(page.locator('.collection-list')).toBeVisible() await page.evaluate((zoomScale) => { document.body.style.zoom = String(zoomScale) @@ -449,7 +408,7 @@ test.describe('A11y', () => { for (const { level, scale } of zoomLevels) { test(`should be usable at ${level}% zoom`, async ({}, testInfo) => { await page.goto(postsUrl.admin) - await page.locator('.nav').waitFor() + await expect(page.locator('.nav')).toBeVisible() await page.evaluate((zoomScale) => { document.body.style.zoom = String(zoomScale) diff --git a/test/a11y/payload-types.ts b/test/a11y/payload-types.ts index fc10c82ab7f..a08a5f49158 100644 --- a/test/a11y/payload-types.ts +++ b/test/a11y/payload-types.ts @@ -88,6 +88,7 @@ export interface Config { db: { defaultIDType: string; }; + fallbackLocale: null; globals: { menu: Menu; }; @@ -235,10 +236,6 @@ export interface PayloadLockedDocument { relationTo: 'media'; value: string | Media; } | null) - | ({ - relationTo: 'payload-kv'; - value: string | PayloadKv; - } | null) | ({ relationTo: 'users'; value: string | User; diff --git a/test/eslint.config.js b/test/eslint.config.js index 68ef706d17d..71f60aab739 100644 --- a/test/eslint.config.js +++ b/test/eslint.config.js @@ -77,6 +77,8 @@ export const testEslintConfig = [ 'assertURLParams', 'uploadImage', 'getRowByCellValueAndAssert', + 'assertAllElementsHaveFocusIndicators', + 'assertNoHorizontalOverflow', ], }, ], diff --git a/test/helpers/e2e/checkFocusIndicators.ts b/test/helpers/e2e/checkFocusIndicators.ts index da7285cf1ea..0c2903a639c 100644 --- a/test/helpers/e2e/checkFocusIndicators.ts +++ b/test/helpers/e2e/checkFocusIndicators.ts @@ -342,13 +342,13 @@ export async function checkFocusIndicators( const hasVisibleOutline = (style: string, width: string, color: string) => Boolean( style && - style !== 'none' && - width && - width !== '0px' && - color && - color !== 'transparent' && - color !== 'rgba(0, 0, 0, 0)' && - !color.includes('rgba(0, 0, 0, 0)'), + style !== 'none' && + width && + width !== '0px' && + color && + color !== 'transparent' && + color !== 'rgba(0, 0, 0, 0)' && + !color.includes('rgba(0, 0, 0, 0)'), ) // Helper to check if a style has a visible box-shadow @@ -383,13 +383,13 @@ export async function checkFocusIndicators( const hasVisibleBorderCheck = (bdr: string, width: string, color: string) => Boolean( bdr && - bdr !== 'none' && - width && - width !== '0px' && - color && - color !== 'transparent' && - color !== 'rgba(0, 0, 0, 0)' && - !color.includes('rgba(0, 0, 0, 0)'), + bdr !== 'none' && + width && + width !== '0px' && + color && + color !== 'transparent' && + color !== 'rgba(0, 0, 0, 0)' && + !color.includes('rgba(0, 0, 0, 0)'), ) // Check if element has a visible focus indicator on the element itself @@ -727,13 +727,14 @@ export async function checkFocusIndicators( export async function assertAllElementsHaveFocusIndicators( options: CheckFocusIndicatorsOptions, ): Promise { - const result = await checkFocusIndicators(options) + await expect(async () => { + const result = await checkFocusIndicators(options) - if (result.elementsWithoutIndicators > 0) { - console.error('Elements without focus indicators:', result.elementsWithoutIndicatorDetails) - } + if (result.elementsWithoutIndicators > 0) { + console.error('Elements without focus indicators:', result.elementsWithoutIndicatorDetails) + } - await expect(() => { + expect(result.totalFocusableElements).toBeGreaterThan(0) expect(result.elementsWithoutIndicators).toBe(0) }).toPass() } diff --git a/test/helpers/e2e/checkHorizontalOverflow.ts b/test/helpers/e2e/checkHorizontalOverflow.ts index 9084fc1a119..46e9d2c90a6 100644 --- a/test/helpers/e2e/checkHorizontalOverflow.ts +++ b/test/helpers/e2e/checkHorizontalOverflow.ts @@ -1,5 +1,7 @@ import type { Page, TestInfo } from '@playwright/test' +import { expect } from '@playwright/test' + export interface HorizontalOverflowResult { /** Whether horizontal overflow/scrolling was detected */ hasHorizontalOverflow: boolean @@ -165,3 +167,24 @@ export async function checkHorizontalOverflow( return result } + +/** + * Assertion helper to verify no horizontal overflow with built-in polling. + * Retries the check until the result stabilizes and meets expectations. + * + * @param page - Playwright page object + * @param testInfo - Optional TestInfo for attaching results + * + * @example + * ```typescript + * await page.setViewportSize({ width: 320, height: 568 }) + * await assertNoHorizontalOverflow(page, testInfo) + * ``` + */ +export async function assertNoHorizontalOverflow(page: Page, testInfo?: TestInfo): Promise { + await expect(async () => { + const result = await checkHorizontalOverflow(page, testInfo) + expect(result.hasHorizontalOverflow).toBe(false) + expect(result.overflowingElements.length).toBe(0) + }).toPass() +}