From 37f8596ac9d5d7882b8dac1832958389b02624a1 Mon Sep 17 00:00:00 2001 From: bhansell1 Date: Thu, 30 Oct 2025 17:09:56 +0000 Subject: [PATCH 1/8] CCM-12666: WIP - playwright accessibility tests --- package-lock.json | 32 +++++++ .../accessibility/accessibility.config.ts | 58 ++++++++++++ tests/test-team/config/playwright.config.ts | 2 +- tests/test-team/fixtures/axe-test.ts | 34 +++++++ tests/test-team/package.json | 3 + tests/test-team/pages/routing/index.ts | 4 + .../message-plans-page.ts} | 2 +- .../routing/routing.accessibility.spec.ts | 92 +++++++++++++++++++ ...emplate-protected-routes.component.spec.ts | 2 +- .../message-plans.routing-component.spec.ts | 2 +- 10 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 tests/test-team/config/accessibility/accessibility.config.ts create mode 100644 tests/test-team/fixtures/axe-test.ts create mode 100644 tests/test-team/pages/routing/index.ts rename tests/test-team/pages/{routing-message-plans-page.ts => routing/message-plans-page.ts} (90%) create mode 100644 tests/test-team/template-mgmt-accessibility/routing/routing.accessibility.spec.ts diff --git a/package-lock.json b/package-lock.json index ef90e849d..61c18c71f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15443,6 +15443,27 @@ "node": ">=18.0.0" } }, + "node_modules/@axe-core/playwright": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.0.tgz", + "integrity": "sha512-70vBT/Ylqpm65RQz2iCG2o0JJCEG/WCNyefTr2xcOcr1CoSee60gNQYUMZZ7YukoKkFLv26I/jjlsvwwp532oQ==", + "license": "MPL-2.0", + "dependencies": { + "axe-core": "~4.11.0" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, + "node_modules/@axe-core/playwright/node_modules/axe-core": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", + "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -36491,11 +36512,13 @@ "@aws-sdk/client-sqs": "^3.864.0", "@aws-sdk/client-ssm": "^3.864.0", "@aws-sdk/lib-dynamodb": "^3.864.0", + "@axe-core/playwright": "^4.11.0", "@faker-js/faker": "^9.9.0", "@nhsdigital/nhs-notify-event-schemas-template-management": "*", "@playwright/test": "^1.51.1", "async-mutex": "^0.5.0", "aws-amplify": "^6.13.6", + "axe-core": "^4.11.0", "date-fns": "^4.1.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-base": "^15.0.0", @@ -37513,6 +37536,15 @@ "npm": ">=9.0.0" } }, + "tests/test-team/node_modules/axe-core": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", + "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, "tests/test-team/node_modules/eslint-plugin-playwright": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-2.2.2.tgz", diff --git a/tests/test-team/config/accessibility/accessibility.config.ts b/tests/test-team/config/accessibility/accessibility.config.ts new file mode 100644 index 000000000..b5bc033e2 --- /dev/null +++ b/tests/test-team/config/accessibility/accessibility.config.ts @@ -0,0 +1,58 @@ +import path from 'node:path'; +import { defineConfig, devices } from '@playwright/test'; +import baseConfig from '../playwright.config'; + +const buildCommand = [ + 'INCLUDE_AUTH_PAGES=true', + 'npm run build && npm run start', +].join(' '); + +export default defineConfig({ + ...baseConfig, + fullyParallel: true, + timeout: 60_000, // 30 seconds in the playwright default + expect: { + timeout: 10_000, // default is 5 seconds. After creating and previewing sometimes the load is slow on a cold start + }, + projects: [ + { + name: 'accessibility:setup', + testMatch: 'ui.setup.ts', + use: { + baseURL: 'http://localhost:3000', + ...devices['Desktop Chrome'], + headless: true, + screenshot: 'only-on-failure', + }, + }, + { + name: 'accessibility', + testMatch: '*.accessibility.spec.ts', + use: { + screenshot: 'only-on-failure', + baseURL: 'http://localhost:3000', + ...devices['Desktop Chrome'], + headless: false, + launchOptions: { + slowMo: 200, + }, + storageState: path.resolve(__dirname, '../.auth/user.json'), + }, + dependencies: ['accessibility:setup'], + teardown: 'accessibility:teardown', + }, + { + name: 'accessibility:teardown', + testMatch: 'ui.teardown.ts', + }, + ], + /* Run your local dev server before starting the tests */ + webServer: { + timeout: 2 * 60 * 1000, // 2 minutes + command: buildCommand, + cwd: path.resolve(__dirname, '../../../..'), + url: 'http://localhost:3000/templates/create-and-submit-templates', + reuseExistingServer: !process.env.CI, + stderr: 'pipe', + }, +}); diff --git a/tests/test-team/config/playwright.config.ts b/tests/test-team/config/playwright.config.ts index 53faf0ce2..6dc2a5ef8 100644 --- a/tests/test-team/config/playwright.config.ts +++ b/tests/test-team/config/playwright.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ forbidOnly: !!process.env.CI, retries: 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 4 : undefined, + workers: process.env.CI ? 4 : 1, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: [ ['line'], diff --git a/tests/test-team/fixtures/axe-test.ts b/tests/test-team/fixtures/axe-test.ts new file mode 100644 index 000000000..cfec5b4ab --- /dev/null +++ b/tests/test-team/fixtures/axe-test.ts @@ -0,0 +1,34 @@ +import { test as base } from '@playwright/test'; +import AxeBuilder from '@axe-core/playwright'; + +export type AxeFixture = { + makeAxeBuilder: () => AxeBuilder; +}; + +/* + * https://dequeuniversity.com/rules/axe/html/4.11 - search for wcag131 + */ +const PRINCIPLE_1_GUIDELINE_1_1_3_1_AAA = [ + 'aria-hidden-body', + 'aria-required-children', + 'aria-required-parent', + 'definition-list', + 'dlitem', + 'list', + 'listitem', + 'p-as-heading', + 'table-fake-caption', + 'td-has-header', + 'td-headers-attr', + 'th-has-data-cells', +]; + +export const test = base.extend({ + makeAxeBuilder: async ({ page }, use) => { + const makeAxeBuilder = () => + new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']); // This list is inclusive + + await use(makeAxeBuilder); + }, +}); +export { expect } from '@playwright/test'; diff --git a/tests/test-team/package.json b/tests/test-team/package.json index 9f23482e3..0f448b204 100644 --- a/tests/test-team/package.json +++ b/tests/test-team/package.json @@ -11,6 +11,8 @@ "@faker-js/faker": "^9.9.0", "@nhsdigital/nhs-notify-event-schemas-template-management": "*", "@playwright/test": "^1.51.1", + "@axe-core/playwright": "^4.11.0", + "axe-core": "^4.11.0", "async-mutex": "^0.5.0", "aws-amplify": "^6.13.6", "date-fns": "^4.1.0", @@ -30,6 +32,7 @@ "scripts": { "lint": "eslint .", "lint:fix": "npm run lint -- --fix", + "test:accessibility": "playwright test --project accessibility -c config/accessibility/accessibility.config.ts", "test:api": "playwright test --project api -c config/api/api.config.ts", "test:e2e": "playwright test --project e2e -c config/e2e/e2e.config.ts", "test:event": "playwright test -c config/event/event.config.ts", diff --git a/tests/test-team/pages/routing/index.ts b/tests/test-team/pages/routing/index.ts new file mode 100644 index 000000000..52f353ce3 --- /dev/null +++ b/tests/test-team/pages/routing/index.ts @@ -0,0 +1,4 @@ +export * from './campaign-id-required-page'; +export * from './choose-message-order-page'; +export * from './create-message-plan-page'; +export * from './message-plans-page'; diff --git a/tests/test-team/pages/routing-message-plans-page.ts b/tests/test-team/pages/routing/message-plans-page.ts similarity index 90% rename from tests/test-team/pages/routing-message-plans-page.ts rename to tests/test-team/pages/routing/message-plans-page.ts index 35c82864e..c71417ce4 100644 --- a/tests/test-team/pages/routing-message-plans-page.ts +++ b/tests/test-team/pages/routing/message-plans-page.ts @@ -1,5 +1,5 @@ import { Locator, type Page } from '@playwright/test'; -import { TemplateMgmtBasePageNonDynamic } from './template-mgmt-base-page-non-dynamic'; +import { TemplateMgmtBasePageNonDynamic } from '../template-mgmt-base-page-non-dynamic'; export class RoutingMessagePlansPage extends TemplateMgmtBasePageNonDynamic { static readonly pageUrlSegment = 'message-plans'; diff --git a/tests/test-team/template-mgmt-accessibility/routing/routing.accessibility.spec.ts b/tests/test-team/template-mgmt-accessibility/routing/routing.accessibility.spec.ts new file mode 100644 index 000000000..a8caf7a50 --- /dev/null +++ b/tests/test-team/template-mgmt-accessibility/routing/routing.accessibility.spec.ts @@ -0,0 +1,92 @@ +import { + createAuthHelper, + TestUser, + testUsers, +} from 'helpers/auth/cognito-auth-helper'; +import { RoutingConfigFactory } from 'helpers/factories/routing-config-factory'; +import { RoutingConfigStorageHelper } from 'helpers/db/routing-config-storage-helper'; +import { test, expect } from 'fixtures/axe-test'; +import { + RoutingChooseMessageOrderPage, + RoutingCreateMessagePlanPage, + RoutingMessagePlanCampaignIdRequiredPage, + RoutingMessagePlansPage, +} from 'pages/routing'; +import { loginAsUser } from 'helpers/auth/login-as-user'; +import { TemplateMgmtBasePage } from 'pages/template-mgmt-base-page'; +import AxeBuilder from '@axe-core/playwright'; + +let user: TestUser; +let userWithMultipleCampaigns: TestUser; + +const routingStorageHelper = new RoutingConfigStorageHelper(); + +async function run( + page: T, + makeAxeBuilder: () => AxeBuilder, + fn?: (page: T) => Promise +) { + await page.loadPage(); + if (fn) { + await fn(page); + } + const results = await makeAxeBuilder().analyze(); + expect(results.violations).toEqual([]); +} + +test.describe('Routing - Accessibility', () => { + test.beforeAll(async () => { + const authHelper = createAuthHelper(); + + user = await authHelper.getTestUser(testUsers.User1.userId); + userWithMultipleCampaigns = await authHelper.getTestUser( + testUsers.UserWithMultipleCampaigns.userId + ); + await routingStorageHelper.seed([ + RoutingConfigFactory.create(user).dbEntry, + ]); + }); + + test.afterAll(async () => { + await routingStorageHelper.deleteSeeded(); + }); + + test('Message plans', async ({ page, makeAxeBuilder }) => + run(new RoutingMessagePlansPage(page), makeAxeBuilder)); + + test('Campaign required', async ({ page, makeAxeBuilder }) => + run(new RoutingMessagePlanCampaignIdRequiredPage(page), makeAxeBuilder)); + + test('Choose message order', async ({ page, makeAxeBuilder }) => + run(new RoutingChooseMessageOrderPage(page), makeAxeBuilder)); + + test('Choose message order - error', async ({ page, makeAxeBuilder }) => + run(new RoutingChooseMessageOrderPage(page), makeAxeBuilder, (p) => + p.clickContinueButton() + )); + + test.describe('client has multiple campaigns', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + test.beforeEach(async ({ page }) => { + await loginAsUser(userWithMultipleCampaigns, page); + }); + + test('Create message plan', async ({ page, makeAxeBuilder }) => + run( + new RoutingCreateMessagePlanPage(page, { + messageOrder: 'NHSAPP,EMAIL,SMS,LETTER', + }), + makeAxeBuilder + )); + + test('Create message plan - error', async ({ page, makeAxeBuilder }) => + run( + new RoutingCreateMessagePlanPage(page, { + messageOrder: 'NHSAPP,EMAIL,SMS,LETTER', + }), + makeAxeBuilder, + (p) => p.clickSubmit() + )); + }); +}); diff --git a/tests/test-team/template-mgmt-component-tests/template-protected-routes.component.spec.ts b/tests/test-team/template-mgmt-component-tests/template-protected-routes.component.spec.ts index 77daa0299..af25167fc 100644 --- a/tests/test-team/template-mgmt-component-tests/template-protected-routes.component.spec.ts +++ b/tests/test-team/template-mgmt-component-tests/template-protected-routes.component.spec.ts @@ -36,7 +36,7 @@ import { TemplateMgmtUploadLetterMissingCampaignClientIdPage } from '../pages/le import { RoutingChooseMessageOrderPage } from '../pages/routing/choose-message-order-page'; import { RoutingCreateMessagePlanPage } from '../pages/routing/create-message-plan-page'; import { RoutingMessagePlanCampaignIdRequiredPage } from '../pages/routing/campaign-id-required-page'; -import { RoutingMessagePlansPage } from '../pages/routing-message-plans-page'; +import { RoutingMessagePlansPage } from '../pages/routing/message-plans-page'; // Reset storage state for this file to avoid being authenticated test.use({ storageState: { cookies: [], origins: [] } }); diff --git a/tests/test-team/template-mgmt-routing-component-tests/message-plans.routing-component.spec.ts b/tests/test-team/template-mgmt-routing-component-tests/message-plans.routing-component.spec.ts index 290889621..668e1cd82 100644 --- a/tests/test-team/template-mgmt-routing-component-tests/message-plans.routing-component.spec.ts +++ b/tests/test-team/template-mgmt-routing-component-tests/message-plans.routing-component.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test'; -import { RoutingMessagePlansPage } from '../pages/routing-message-plans-page'; +import { RoutingMessagePlansPage } from '../pages/routing/message-plans-page'; import { assertFooterLinks, assertSignOutLink, From 18d7ffa001fab754c6e2442151b7e18a390bbc97 Mon Sep 17 00:00:00 2001 From: bhansell1 Date: Mon, 3 Nov 2025 12:59:07 +0000 Subject: [PATCH 2/8] CCM-12666: make tests run in the pipeline --- .github/actions/test-types.json | 1 + scripts/tests/test.mk | 5 ++ scripts/tests/ui-accessibility.sh | 15 +++++ .../fixtures/accessibility-analyze.ts | 39 ++++++++++++ tests/test-team/fixtures/axe-test.ts | 34 ----------- .../routing.accessibility.spec.ts | 59 ++++++++----------- 6 files changed, 83 insertions(+), 70 deletions(-) create mode 100755 scripts/tests/ui-accessibility.sh create mode 100644 tests/test-team/fixtures/accessibility-analyze.ts delete mode 100644 tests/test-team/fixtures/axe-test.ts rename tests/test-team/template-mgmt-accessibility/{routing => }/routing.accessibility.spec.ts (51%) diff --git a/.github/actions/test-types.json b/.github/actions/test-types.json index a71cbe2b9..bc8439978 100644 --- a/.github/actions/test-types.json +++ b/.github/actions/test-types.json @@ -2,6 +2,7 @@ "accessibility", "api", "event", + "ui-accessibility", "ui-component", "ui-routing-component", "ui-e2e", diff --git a/scripts/tests/test.mk b/scripts/tests/test.mk index 1c6a541dc..202b3c044 100644 --- a/scripts/tests/test.mk +++ b/scripts/tests/test.mk @@ -23,6 +23,9 @@ test-coverage: # Evaluate code coverage from scripts/test/coverage @Testing test-accessibility: # Run tests from scripts/tests/accessibility.sh @Testing make _test name="accessibility" +test-ui-accessibility: # Run tests from scripts/tests/ui-accessibility.sh @Testing + make _test name="ui-accessibility" + test-ui-routing-component: # Run tests from scripts/tests/ui-routing-component.sh @Testing make _test name="ui-routing-component" @@ -50,6 +53,7 @@ test: # Run all the test tasks @Testing test-lint \ test-typecheck \ test-coverage \ + test-ui-accessibility \ test-ui-component \ test-ui-e2e \ test-api \ @@ -73,6 +77,7 @@ ${VERBOSE}.SILENT: \ test-coverage \ test-lint \ test-typecheck \ + test-ui-accessibility \ test-ui-component \ test-api \ test-ui-e2e \ diff --git a/scripts/tests/ui-accessibility.sh b/scripts/tests/ui-accessibility.sh new file mode 100755 index 000000000..d139b5d5f --- /dev/null +++ b/scripts/tests/ui-accessibility.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -euo pipefail +cd "$(git rev-parse --show-toplevel)" +npx playwright install --with-deps > /dev/null +cd tests/test-team +TEST_EXIT_CODE=0 +npm run test:accessibility || TEST_EXIT_CODE=$? +echo "TEST_EXIT_CODE=$TEST_EXIT_CODE" + +mkdir -p ../acceptance-test-report +cp -r ./playwright-report ../acceptance-test-report +[[ -e test-results ]] && cp -r ./test-results ../acceptance-test-report + +exit $TEST_EXIT_CODE diff --git a/tests/test-team/fixtures/accessibility-analyze.ts b/tests/test-team/fixtures/accessibility-analyze.ts new file mode 100644 index 000000000..6936c001d --- /dev/null +++ b/tests/test-team/fixtures/accessibility-analyze.ts @@ -0,0 +1,39 @@ +import { test as base, Page } from '@playwright/test'; +import AxeBuilder from '@axe-core/playwright'; +import { TemplateMgmtBasePage } from 'pages/template-mgmt-base-page'; +import { expect } from '@playwright/test'; + +type Analyze = ( + page: T, + opts?: { + id?: string; + beforeAnalyze?: (page: T) => Promise; + } +) => Promise; + +type AccessibilityFixture = { + analyze: Analyze; +}; + +const makeAxeBuilder = (page: Page) => + new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']); + +export const test = base.extend({ + analyze: async ({ page }, use) => { + const analyze: Analyze = async (pageUnderTest, opts) => { + const { id, beforeAnalyze } = opts ?? {}; + + await pageUnderTest.loadPage(id); + + if (beforeAnalyze) { + await beforeAnalyze(pageUnderTest); + } + + const results = await makeAxeBuilder(page).analyze(); + + expect(results.violations).toEqual([]); + }; + + await use(analyze); + }, +}); diff --git a/tests/test-team/fixtures/axe-test.ts b/tests/test-team/fixtures/axe-test.ts deleted file mode 100644 index cfec5b4ab..000000000 --- a/tests/test-team/fixtures/axe-test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { test as base } from '@playwright/test'; -import AxeBuilder from '@axe-core/playwright'; - -export type AxeFixture = { - makeAxeBuilder: () => AxeBuilder; -}; - -/* - * https://dequeuniversity.com/rules/axe/html/4.11 - search for wcag131 - */ -const PRINCIPLE_1_GUIDELINE_1_1_3_1_AAA = [ - 'aria-hidden-body', - 'aria-required-children', - 'aria-required-parent', - 'definition-list', - 'dlitem', - 'list', - 'listitem', - 'p-as-heading', - 'table-fake-caption', - 'td-has-header', - 'td-headers-attr', - 'th-has-data-cells', -]; - -export const test = base.extend({ - makeAxeBuilder: async ({ page }, use) => { - const makeAxeBuilder = () => - new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']); // This list is inclusive - - await use(makeAxeBuilder); - }, -}); -export { expect } from '@playwright/test'; diff --git a/tests/test-team/template-mgmt-accessibility/routing/routing.accessibility.spec.ts b/tests/test-team/template-mgmt-accessibility/routing.accessibility.spec.ts similarity index 51% rename from tests/test-team/template-mgmt-accessibility/routing/routing.accessibility.spec.ts rename to tests/test-team/template-mgmt-accessibility/routing.accessibility.spec.ts index a8caf7a50..d4de909d3 100644 --- a/tests/test-team/template-mgmt-accessibility/routing/routing.accessibility.spec.ts +++ b/tests/test-team/template-mgmt-accessibility/routing.accessibility.spec.ts @@ -5,7 +5,7 @@ import { } from 'helpers/auth/cognito-auth-helper'; import { RoutingConfigFactory } from 'helpers/factories/routing-config-factory'; import { RoutingConfigStorageHelper } from 'helpers/db/routing-config-storage-helper'; -import { test, expect } from 'fixtures/axe-test'; +import { test } from 'fixtures/accessibility-analyze'; import { RoutingChooseMessageOrderPage, RoutingCreateMessagePlanPage, @@ -13,35 +13,22 @@ import { RoutingMessagePlansPage, } from 'pages/routing'; import { loginAsUser } from 'helpers/auth/login-as-user'; -import { TemplateMgmtBasePage } from 'pages/template-mgmt-base-page'; -import AxeBuilder from '@axe-core/playwright'; let user: TestUser; let userWithMultipleCampaigns: TestUser; const routingStorageHelper = new RoutingConfigStorageHelper(); -async function run( - page: T, - makeAxeBuilder: () => AxeBuilder, - fn?: (page: T) => Promise -) { - await page.loadPage(); - if (fn) { - await fn(page); - } - const results = await makeAxeBuilder().analyze(); - expect(results.violations).toEqual([]); -} - test.describe('Routing - Accessibility', () => { test.beforeAll(async () => { const authHelper = createAuthHelper(); user = await authHelper.getTestUser(testUsers.User1.userId); + userWithMultipleCampaigns = await authHelper.getTestUser( testUsers.UserWithMultipleCampaigns.userId ); + await routingStorageHelper.seed([ RoutingConfigFactory.create(user).dbEntry, ]); @@ -51,42 +38,42 @@ test.describe('Routing - Accessibility', () => { await routingStorageHelper.deleteSeeded(); }); - test('Message plans', async ({ page, makeAxeBuilder }) => - run(new RoutingMessagePlansPage(page), makeAxeBuilder)); + test('Message plans', async ({ page, analyze }) => + analyze(new RoutingMessagePlansPage(page))); - test('Campaign required', async ({ page, makeAxeBuilder }) => - run(new RoutingMessagePlanCampaignIdRequiredPage(page), makeAxeBuilder)); + test('Campaign required', async ({ page, analyze }) => + analyze(new RoutingMessagePlanCampaignIdRequiredPage(page))); - test('Choose message order', async ({ page, makeAxeBuilder }) => - run(new RoutingChooseMessageOrderPage(page), makeAxeBuilder)); + test('Choose message order', async ({ page, analyze }) => + analyze(new RoutingChooseMessageOrderPage(page))); - test('Choose message order - error', async ({ page, makeAxeBuilder }) => - run(new RoutingChooseMessageOrderPage(page), makeAxeBuilder, (p) => - p.clickContinueButton() - )); + test('Choose message order - error', async ({ page, analyze }) => + analyze(new RoutingChooseMessageOrderPage(page), { + beforeAnalyze: (p) => p.clickContinueButton(), + })); test.describe('client has multiple campaigns', () => { + const messageOrder = 'NHSAPP,EMAIL,SMS,LETTER'; + test.use({ storageState: { cookies: [], origins: [] } }); test.beforeEach(async ({ page }) => { await loginAsUser(userWithMultipleCampaigns, page); }); - test('Create message plan', async ({ page, makeAxeBuilder }) => - run( + test('Create message plan', async ({ page, analyze }) => + analyze( new RoutingCreateMessagePlanPage(page, { - messageOrder: 'NHSAPP,EMAIL,SMS,LETTER', - }), - makeAxeBuilder + messageOrder, + }) )); - test('Create message plan - error', async ({ page, makeAxeBuilder }) => - run( + test('Create message plan - error', async ({ page, analyze }) => + analyze( new RoutingCreateMessagePlanPage(page, { - messageOrder: 'NHSAPP,EMAIL,SMS,LETTER', + messageOrder, }), - makeAxeBuilder, - (p) => p.clickSubmit() + { beforeAnalyze: (p) => p.clickSubmit() } )); }); }); From 3444f2553c1e2bc1e07a32f831c6f2b9d15b8500 Mon Sep 17 00:00:00 2001 From: bhansell1 Date: Mon, 3 Nov 2025 14:15:54 +0000 Subject: [PATCH 3/8] CCM-12666: config --- .../config/accessibility/accessibility.config.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/test-team/config/accessibility/accessibility.config.ts b/tests/test-team/config/accessibility/accessibility.config.ts index b5bc033e2..6165c07ca 100644 --- a/tests/test-team/config/accessibility/accessibility.config.ts +++ b/tests/test-team/config/accessibility/accessibility.config.ts @@ -32,10 +32,7 @@ export default defineConfig({ screenshot: 'only-on-failure', baseURL: 'http://localhost:3000', ...devices['Desktop Chrome'], - headless: false, - launchOptions: { - slowMo: 200, - }, + headless: true, storageState: path.resolve(__dirname, '../.auth/user.json'), }, dependencies: ['accessibility:setup'], @@ -48,11 +45,12 @@ export default defineConfig({ ], /* Run your local dev server before starting the tests */ webServer: { - timeout: 2 * 60 * 1000, // 2 minutes + timeout: 4 * 60 * 1000, // 2 minutes command: buildCommand, cwd: path.resolve(__dirname, '../../../..'), url: 'http://localhost:3000/templates/create-and-submit-templates', reuseExistingServer: !process.env.CI, stderr: 'pipe', + stdout: 'pipe', }, }); From 8c1c53e9cd54d8da203fe798e9687ec222bb866f Mon Sep 17 00:00:00 2001 From: bhansell1 Date: Mon, 3 Nov 2025 14:54:18 +0000 Subject: [PATCH 4/8] CCM-12666: add specific accessibility issue to see tests fail in pipeline --- .../components/forms/ChooseMessageOrder/ChooseMessageOrder.tsx | 3 +++ tests/test-team/config/playwright.config.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/forms/ChooseMessageOrder/ChooseMessageOrder.tsx b/frontend/src/components/forms/ChooseMessageOrder/ChooseMessageOrder.tsx index eee6de6ba..f7d0f8258 100644 --- a/frontend/src/components/forms/ChooseMessageOrder/ChooseMessageOrder.tsx +++ b/frontend/src/components/forms/ChooseMessageOrder/ChooseMessageOrder.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/alt-text */ +/* eslint-disable @next/next/no-img-element */ 'use client'; import { useActionState, useState } from 'react'; @@ -43,6 +45,7 @@ export const ChooseMessageOrder = () => { return ( + Date: Mon, 3 Nov 2025 15:24:42 +0000 Subject: [PATCH 5/8] CCM-12666: add specific accessibility issue to see tests fail in pipeline --- .../__snapshots__/page.test.tsx.snap | 3 +++ .../__snapshots__/ChooseMessageOrder.test.tsx.snap | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/frontend/src/__tests__/app/message-plans/choose-message-order/__snapshots__/page.test.tsx.snap b/frontend/src/__tests__/app/message-plans/choose-message-order/__snapshots__/page.test.tsx.snap index 74721dac9..69e70a532 100644 --- a/frontend/src/__tests__/app/message-plans/choose-message-order/__snapshots__/page.test.tsx.snap +++ b/frontend/src/__tests__/app/message-plans/choose-message-order/__snapshots__/page.test.tsx.snap @@ -7,6 +7,9 @@ exports[`ChooseMessageOrderPage 1`] = ` id="maincontent" role="main" > +
diff --git a/frontend/src/__tests__/components/forms/ChooseMessageOrder/__snapshots__/ChooseMessageOrder.test.tsx.snap b/frontend/src/__tests__/components/forms/ChooseMessageOrder/__snapshots__/ChooseMessageOrder.test.tsx.snap index 5a2dfa37a..f29210f2b 100644 --- a/frontend/src/__tests__/components/forms/ChooseMessageOrder/__snapshots__/ChooseMessageOrder.test.tsx.snap +++ b/frontend/src/__tests__/components/forms/ChooseMessageOrder/__snapshots__/ChooseMessageOrder.test.tsx.snap @@ -32,6 +32,9 @@ exports[`Choose message order page Client-side validation triggers 1`] = ` + @@ -296,6 +299,9 @@ exports[`Choose message order page renders error component 1`] = ` + @@ -524,6 +530,9 @@ exports[`Choose message order page renders form 1`] = ` id="maincontent" role="main" > + From 06a62900edf91602cb8051b34e77e68df8d1d7ea Mon Sep 17 00:00:00 2001 From: bhansell1 Date: Mon, 3 Nov 2025 15:44:52 +0000 Subject: [PATCH 6/8] CCM-12666: revert change --- .../__snapshots__/page.test.tsx.snap | 3 --- .../__snapshots__/ChooseMessageOrder.test.tsx.snap | 9 --------- .../forms/ChooseMessageOrder/ChooseMessageOrder.tsx | 1 - 3 files changed, 13 deletions(-) diff --git a/frontend/src/__tests__/app/message-plans/choose-message-order/__snapshots__/page.test.tsx.snap b/frontend/src/__tests__/app/message-plans/choose-message-order/__snapshots__/page.test.tsx.snap index 69e70a532..74721dac9 100644 --- a/frontend/src/__tests__/app/message-plans/choose-message-order/__snapshots__/page.test.tsx.snap +++ b/frontend/src/__tests__/app/message-plans/choose-message-order/__snapshots__/page.test.tsx.snap @@ -7,9 +7,6 @@ exports[`ChooseMessageOrderPage 1`] = ` id="maincontent" role="main" > - diff --git a/frontend/src/__tests__/components/forms/ChooseMessageOrder/__snapshots__/ChooseMessageOrder.test.tsx.snap b/frontend/src/__tests__/components/forms/ChooseMessageOrder/__snapshots__/ChooseMessageOrder.test.tsx.snap index f29210f2b..5a2dfa37a 100644 --- a/frontend/src/__tests__/components/forms/ChooseMessageOrder/__snapshots__/ChooseMessageOrder.test.tsx.snap +++ b/frontend/src/__tests__/components/forms/ChooseMessageOrder/__snapshots__/ChooseMessageOrder.test.tsx.snap @@ -32,9 +32,6 @@ exports[`Choose message order page Client-side validation triggers 1`] = ` - @@ -299,9 +296,6 @@ exports[`Choose message order page renders error component 1`] = ` - @@ -530,9 +524,6 @@ exports[`Choose message order page renders form 1`] = ` id="maincontent" role="main" > - diff --git a/frontend/src/components/forms/ChooseMessageOrder/ChooseMessageOrder.tsx b/frontend/src/components/forms/ChooseMessageOrder/ChooseMessageOrder.tsx index f7d0f8258..8020af0a7 100644 --- a/frontend/src/components/forms/ChooseMessageOrder/ChooseMessageOrder.tsx +++ b/frontend/src/components/forms/ChooseMessageOrder/ChooseMessageOrder.tsx @@ -45,7 +45,6 @@ export const ChooseMessageOrder = () => { return ( - Date: Mon, 3 Nov 2025 16:57:07 +0000 Subject: [PATCH 7/8] CCM-12666: add new tests for new pages --- .../fixtures/accessibility-analyze.ts | 10 ++- tests/test-team/pages/routing/index.ts | 2 + .../routing.accessibility.spec.ts | 64 ++++++++++++++++--- 3 files changed, 67 insertions(+), 9 deletions(-) diff --git a/tests/test-team/fixtures/accessibility-analyze.ts b/tests/test-team/fixtures/accessibility-analyze.ts index 6936c001d..552e83737 100644 --- a/tests/test-team/fixtures/accessibility-analyze.ts +++ b/tests/test-team/fixtures/accessibility-analyze.ts @@ -19,7 +19,7 @@ const makeAxeBuilder = (page: Page) => new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']); export const test = base.extend({ - analyze: async ({ page }, use) => { + analyze: async ({ baseURL, page }, use) => { const analyze: Analyze = async (pageUnderTest, opts) => { const { id, beforeAnalyze } = opts ?? {}; @@ -29,6 +29,14 @@ export const test = base.extend({ await beforeAnalyze(pageUnderTest); } + const pageUrlSegment = ( + pageUnderTest.constructor as typeof TemplateMgmtBasePage + ).pageUrlSegment; + + await expect(page).toHaveURL( + new RegExp(`${baseURL}/templates/${pageUrlSegment}(.*)`) // eslint-disable-line security/detect-non-literal-regexp + ); + const results = await makeAxeBuilder(page).analyze(); expect(results.violations).toEqual([]); diff --git a/tests/test-team/pages/routing/index.ts b/tests/test-team/pages/routing/index.ts index 52f353ce3..8052b1bfb 100644 --- a/tests/test-team/pages/routing/index.ts +++ b/tests/test-team/pages/routing/index.ts @@ -1,4 +1,6 @@ export * from './campaign-id-required-page'; export * from './choose-message-order-page'; +export * from './choose-templates-page'; export * from './create-message-plan-page'; export * from './message-plans-page'; +export * from './invalid-message-plan-page'; diff --git a/tests/test-team/template-mgmt-accessibility/routing.accessibility.spec.ts b/tests/test-team/template-mgmt-accessibility/routing.accessibility.spec.ts index d4de909d3..82283e05f 100644 --- a/tests/test-team/template-mgmt-accessibility/routing.accessibility.spec.ts +++ b/tests/test-team/template-mgmt-accessibility/routing.accessibility.spec.ts @@ -9,33 +9,75 @@ import { test } from 'fixtures/accessibility-analyze'; import { RoutingChooseMessageOrderPage, RoutingCreateMessagePlanPage, + RoutingChooseTemplatesPage, + RoutingInvalidMessagePlanPage, RoutingMessagePlanCampaignIdRequiredPage, RoutingMessagePlansPage, } from 'pages/routing'; import { loginAsUser } from 'helpers/auth/login-as-user'; +import { randomUUID } from 'node:crypto'; +import { TemplateFactory } from 'helpers/factories/template-factory'; +import { TemplateStorageHelper } from 'helpers/db/template-storage-helper'; -let user: TestUser; let userWithMultipleCampaigns: TestUser; - const routingStorageHelper = new RoutingConfigStorageHelper(); +const templateStorageHelper = new TemplateStorageHelper(); +const validRoutingConfigId = randomUUID(); +const messageOrder = 'NHSAPP,EMAIL,SMS,LETTER'; test.describe('Routing - Accessibility', () => { test.beforeAll(async () => { const authHelper = createAuthHelper(); - user = await authHelper.getTestUser(testUsers.User1.userId); + const user = await authHelper.getTestUser(testUsers.User1.userId); userWithMultipleCampaigns = await authHelper.getTestUser( testUsers.UserWithMultipleCampaigns.userId ); - await routingStorageHelper.seed([ - RoutingConfigFactory.create(user).dbEntry, - ]); + const templateIds = { + NHSAPP: randomUUID(), + SMS: randomUUID(), + LETTER: randomUUID(), + }; + + const routingConfig = RoutingConfigFactory.createForMessageOrder( + user, + messageOrder, + { + id: validRoutingConfigId, + name: 'Test plan with some templates', + } + ) + .addTemplate('NHSAPP', templateIds.NHSAPP) + .addTemplate('SMS', templateIds.SMS) + .addTemplate('LETTER', templateIds.LETTER).dbEntry; + + const templates = [ + TemplateFactory.createNhsAppTemplate( + templateIds.NHSAPP, + user, + 'Test NHS App template' + ), + TemplateFactory.createSmsTemplate( + templateIds.SMS, + user, + 'Test SMS template' + ), + TemplateFactory.uploadLetterTemplate( + templateIds.LETTER, + user, + 'Test Letter template' + ), + ]; + + await routingStorageHelper.seed([routingConfig]); + await templateStorageHelper.seedTemplateData(templates); }); test.afterAll(async () => { await routingStorageHelper.deleteSeeded(); + await templateStorageHelper.deleteSeededTemplates(); }); test('Message plans', async ({ page, analyze }) => @@ -44,17 +86,23 @@ test.describe('Routing - Accessibility', () => { test('Campaign required', async ({ page, analyze }) => analyze(new RoutingMessagePlanCampaignIdRequiredPage(page))); + test('Invalid message plans', async ({ page, analyze }) => + analyze(new RoutingInvalidMessagePlanPage(page))); + test('Choose message order', async ({ page, analyze }) => analyze(new RoutingChooseMessageOrderPage(page))); + test('Choose template', async ({ page, analyze }) => + analyze(new RoutingChooseTemplatesPage(page), { + id: validRoutingConfigId, + })); + test('Choose message order - error', async ({ page, analyze }) => analyze(new RoutingChooseMessageOrderPage(page), { beforeAnalyze: (p) => p.clickContinueButton(), })); test.describe('client has multiple campaigns', () => { - const messageOrder = 'NHSAPP,EMAIL,SMS,LETTER'; - test.use({ storageState: { cookies: [], origins: [] } }); test.beforeEach(async ({ page }) => { From cf7f92c7cfcb500df0c4d9e7a6d5e18fb7497c45 Mon Sep 17 00:00:00 2001 From: bhansell1 Date: Tue, 11 Nov 2025 15:51:00 +0000 Subject: [PATCH 8/8] CCM-12666: add wcag2aaa standard which includes the heading order violation rule. --- .../config/accessibility/accessibility.config.ts | 2 +- tests/test-team/fixtures/accessibility-analyze.ts | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/test-team/config/accessibility/accessibility.config.ts b/tests/test-team/config/accessibility/accessibility.config.ts index 6165c07ca..3f3509536 100644 --- a/tests/test-team/config/accessibility/accessibility.config.ts +++ b/tests/test-team/config/accessibility/accessibility.config.ts @@ -45,7 +45,7 @@ export default defineConfig({ ], /* Run your local dev server before starting the tests */ webServer: { - timeout: 4 * 60 * 1000, // 2 minutes + timeout: 4 * 60 * 1000, // 4 minutes command: buildCommand, cwd: path.resolve(__dirname, '../../../..'), url: 'http://localhost:3000/templates/create-and-submit-templates', diff --git a/tests/test-team/fixtures/accessibility-analyze.ts b/tests/test-team/fixtures/accessibility-analyze.ts index 552e83737..cde5ae3f8 100644 --- a/tests/test-team/fixtures/accessibility-analyze.ts +++ b/tests/test-team/fixtures/accessibility-analyze.ts @@ -15,8 +15,17 @@ type AccessibilityFixture = { analyze: Analyze; }; +const DISABLED_RULES = [ + /* We don't have control over NHS colours. + * Axe decides the page is 5.75 ratio and wcag2aaa expects 7:1 + */ + 'color-contrast-enhanced', +]; + const makeAxeBuilder = (page: Page) => - new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']); + new AxeBuilder({ page }) + .withTags(['wcag2a', 'wcag2aa', 'wcag2aaa']) + .disableRules(DISABLED_RULES); export const test = base.extend({ analyze: async ({ baseURL, page }, use) => {