From c5e734c16656e05a06c7ad65a8e4a107a48b13d6 Mon Sep 17 00:00:00 2001 From: Tessa Viergever <112861180+TessaViergever@users.noreply.github.com> Date: Thu, 16 Jan 2025 14:33:42 +0100 Subject: [PATCH 1/4] test: step3+4 mock data formstorestate --- src/store/form_store.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/store/form_store.ts b/src/store/form_store.ts index 67d687ca..d71daf31 100644 --- a/src/store/form_store.ts +++ b/src/store/form_store.ts @@ -22,6 +22,36 @@ const initialFormState: FormStoreState = { last_completed_step: FormStep.STEP_0, } +export const defaultStep3FormState: Partial = { + description: 'lamp', + coordinates: [51.61892134, 5.52874105], + address: { + coordinates: [5.52874105, 51.61892134], + id: 'adr-987acc537500c2f62ab7449a6d1e6f2e', + postcode: '5462GJ', + huisnummer: '3A', + woonplaats: 'Veghel', + openbare_ruimte: 'Lage Landstraat', + weergave_naam: 'Lage Landstraat 3A, 5462GJ Veghel', + }, + last_completed_step: 2, +} + +export const defaultStep4FormState: Partial = { + description: 'lamp', + coordinates: [51.61892134, 5.52874105], + address: { + coordinates: [5.52874105, 51.61892134], + id: 'adr-987acc537500c2f62ab7449a6d1e6f2e', + postcode: '5462GJ', + huisnummer: '3A', + woonplaats: 'Veghel', + openbare_ruimte: 'Lage Landstraat', + weergave_naam: 'Lage Landstraat 3A, 5462GJ Veghel', + }, + last_completed_step: 3, +} + export const createFormState = ( data: Partial ): FormStoreState => merge({}, initialFormState, data) From 887efa012c7bba5ea3ec590469f80d18213504e2 Mon Sep 17 00:00:00 2001 From: Tessa Viergever <112861180+TessaViergever@users.noreply.github.com> Date: Thu, 16 Jan 2025 14:43:13 +0100 Subject: [PATCH 2/4] test: add step1,3,4 and thankyou playwright tests --- test/e2e/step1.spec.ts | 109 ++---- test/e2e/step3.spec.ts | 681 ++++++++++++++++++++++++++++++++++++++ test/e2e/step4.spec.ts | 623 ++++++++++++++++++++++++++++++++++ test/e2e/thankyou.spec.ts | 134 ++++++++ 4 files changed, 1469 insertions(+), 78 deletions(-) create mode 100644 test/e2e/step3.spec.ts create mode 100644 test/e2e/step4.spec.ts create mode 100644 test/e2e/thankyou.spec.ts diff --git a/test/e2e/step1.spec.ts b/test/e2e/step1.spec.ts index 0c7841f5..e0cdbe98 100644 --- a/test/e2e/step1.spec.ts +++ b/test/e2e/step1.spec.ts @@ -1,52 +1,5 @@ -/* eslint-disable react-hooks/rules-of-hooks */ -import { expect, test as base, BrowserContext } from '@playwright/test' -import AxeBuilder from '@axe-core/playwright' -import { describe } from 'vitest' -import { createSessionStorageFixture } from '@/store/form_store' -import { FormStoreState } from '@/types/stores' - -type AxeFixture = { - makeAxeBuilder: () => AxeBuilder -} - -// Extend base test by providing "makeAxeBuilder" -// -// This new "test" can be used in multiple test files, and each of them will get -// a consistently configured AxeBuilder instance. -export const test = base.extend({ - makeAxeBuilder: async ({ page }, use, testInfo) => { - const makeAxeBuilder = () => - new AxeBuilder({ page }) - .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']) - .exclude('#commonly-reused-element-with-known-issue') - - await use(makeAxeBuilder) - }, -}) - -const sessionStorageFixture = ( - context: BrowserContext, - sessionStorage: { [index: string]: string } -) => - context.addInitScript((storage) => { - for (const [key, value] of Object.entries(storage)) { - window.sessionStorage.setItem(key, value) - console.log(key, value) - } - }, sessionStorage) - -const formStateFixture = ( - context: BrowserContext, - formState: Partial -) => sessionStorageFixture(context, createSessionStorageFixture(formState)) - -test.use({ - locale: 'nl-NL', - timezoneId: 'Europe/Amsterdam', - geolocation: { latitude: 51.6045656, longitude: 5.5342026 }, -}) - -const websiteURL = 'http://localhost:3000/' +import { expect } from '@playwright/test' +import { test, formStateFixture, websiteURL } from './util' interface MyTextConfig { name: string @@ -63,7 +16,7 @@ const parameters: MyTextConfig[] = [ // { name: 'Forced colors', forcedColors: true }, ] -parameters.slice(0, 1).forEach(async ({ name, testConfig, forcedColors }) => { +parameters.forEach(async ({ name, testConfig, forcedColors }) => { test.describe(`${name}`, () => { // TODO: Make new context for each test config to avoid interference if (testConfig) { @@ -177,45 +130,45 @@ parameters.slice(0, 1).forEach(async ({ name, testConfig, forcedColors }) => { await expect(button).toBeVisible() }) - test.describe('step 2', () => { - const pageURL = 'http://localhost:3000/nl/incident/vulaan' - test('has title', async ({ page, context }) => { - formStateFixture(context, { description: 'lamp' }) + // test.describe('step 2', () => { + // const pageURL = 'http://localhost:3000/nl/incident/vulaan' + // test('has title', async ({ page, context }) => { + // formStateFixture(context, { description: 'lamp' }) - await page.goto(pageURL) + // await page.goto(pageURL) - // Expect a title "to contain" a substring with the step - await expect(page).toHaveTitle(/Stap 2 van 4/i) + // // Expect a title "to contain" a substring with the step + // await expect(page).toHaveTitle(/Stap 2 van 4/i) - // Expect a title "to contain" a substring. - await expect(page).toHaveTitle(/Purmerend/i) - }) + // // Expect a title "to contain" a substring. + // await expect(page).toHaveTitle(/Purmerend/i) + // }) - test('has heading', async ({ context, page }) => { - formStateFixture(context, { description: 'lamp' }) + // test('has heading', async ({ context, page }) => { + // formStateFixture(context, { description: 'lamp' }) - await page.goto(pageURL) + // await page.goto(pageURL) - const heading = page.getByRole('heading', { - name: 'Locatie en vragen', - level: 1, - }) + // const heading = page.getByRole('heading', { + // name: 'Locatie en vragen', + // level: 1, + // }) - await expect(heading).toBeVisible() - }) + // await expect(heading).toBeVisible() + // }) - test('Focus combobox', async ({ context, page }) => { - formStateFixture(context, { description: 'lamp' }) + // test('Focus combobox', async ({ context, page }) => { + // formStateFixture(context, { description: 'lamp' }) - await page.goto(pageURL) + // await page.goto(pageURL) - // const combobox = page.getByRole('combobox', { name: 'Adres' }) - const combobox = page.getByRole('combobox') + // // const combobox = page.getByRole('combobox', { name: 'Adres' }) + // const combobox = page.getByRole('combobox') - await expect(combobox).toBeVisible() + // await expect(combobox).toBeVisible() - await combobox.focus() - }) - }) + // await combobox.focus() + // }) + // }) }) }) diff --git a/test/e2e/step3.spec.ts b/test/e2e/step3.spec.ts new file mode 100644 index 00000000..686c861d --- /dev/null +++ b/test/e2e/step3.spec.ts @@ -0,0 +1,681 @@ +import { expect } from '@playwright/test' +import { test, formStateFixture, websiteURL } from './util' +import { defaultStep3FormState } from '@/store/form_store' + +test.use({ + locale: 'nl-NL', + timezoneId: 'Europe/Amsterdam', + geolocation: { latitude: 51.6045656, longitude: 5.5342026 }, +}) + +interface MyTextConfig { + name: string + testConfig: { colorScheme?: string } + forcedColors?: boolean +} +const parameters: MyTextConfig[] = [ + { + name: 'Light mode', + testConfig: { colorScheme: 'light' }, + forcedColors: false, + }, +] + +parameters.forEach(async ({ name, testConfig, forcedColors }) => { + test.describe(`${name}`, () => { + const pageURL = `${websiteURL}nl/incident/contact` + if (testConfig) { + test.use(testConfig as any) + } + + test('has title', async ({ page, context }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + // Expect a title "to contain" a substring with the step + await expect(page).toHaveTitle(/Stap 3 van 4/i) + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Purmerend/i) + }) + + test('has heading', async ({ page, context }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const heading = page.getByRole('heading', { + name: 'Contactgegevens', + level: 1, + }) + + await expect(heading).toBeVisible() + }) + + // TEXTBOX TESTS // + + // Textbox - Aanwezig + // 1. telefoonnummer + test('has textbox(phone)', async ({ page, context }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const textbox = page.getByRole('textbox', { + name: 'Wat is uw telefoonnummer? (niet verplicht)', + }) + + await expect(textbox).toBeVisible() + }) + // 2. e-mailadres + test('has textbox(mail)', async ({ page, context }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const textbox = page.getByRole('textbox', { + name: 'Wat is uw e-mailadres? (niet verplicht)', + }) + + await expect(textbox).toBeVisible() + }) + + // Textbox - Focus + // 1. telefoonnummer + test('focus textbox(phone)', async ({ makeAxeBuilder, page, context }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const textbox = page.getByRole('textbox', { + name: 'Wat is uw telefoonnummer? (niet verplicht)', + }) + + await expect(textbox).toBeVisible() + + await textbox.focus() + + await expect(textbox).toBeFocused() + + const accessibilityScanResults = await makeAxeBuilder().analyze() + expect(accessibilityScanResults.violations).toEqual([]) + }) + // 2. e-mailadres + test('focus textbox(mail)', async ({ makeAxeBuilder, page, context }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const textbox = page.getByRole('textbox', { + name: 'Wat is uw e-mailadres? (niet verplicht)', + }) + + await expect(textbox).toBeVisible() + + await textbox.focus() + + await expect(textbox).toBeFocused() + + const accessibilityScanResults = await makeAxeBuilder().analyze() + expect(accessibilityScanResults.violations).toEqual([]) + }) + + // Textbox - Foutmelding + // 1. telefoonnummer (verkeerde invoer + 'Volgende' button) + test('textbox(phone) - error trigger', async ({ page, context }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const textbox = page.getByRole('textbox', { + name: 'Wat is uw telefoonnummer? (niet verplicht)', + }) + + await expect(textbox).toBeVisible() + + await textbox.fill('test@test.nl') + await expect(textbox).toHaveValue('test@test.nl') + + const nextButton = page.getByRole('button', { name: 'Volgende' }) + + await nextButton.click() + + await expect(nextButton).toBeVisible() + }) + // 2. e-mailadres (verkeerde invoer + 'Volgende' button) + test('textbox mail - error trigger', async ({ page, context }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const textbox = page.getByRole('textbox', { + name: 'Wat is uw e-mailadres? (niet verplicht)', + }) + + await expect(textbox).toBeVisible() + + await textbox.fill('0612345678') + await expect(textbox).toHaveValue('0612345678') + + const nextButton = page.getByRole('button', { name: 'Volgende' }) + + await nextButton.click() + await expect(nextButton).toBeVisible() + }) + // Textbox - Foutmelding (Axe) + test('textbox - error trigger axe check', async ({ + makeAxeBuilder, + page, + context, + }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const textbox = page.getByRole('textbox', { + name: 'Wat is uw e-mailadres? (niet verplicht)', + }) + + await expect(textbox).toBeVisible() + + await textbox.fill('0612345678') + + const nextButton = page.getByRole('button', { name: 'Volgende' }) + + await nextButton.click() + + const accessibilityScanResults = await makeAxeBuilder().analyze() + expect(accessibilityScanResults.violations).toEqual([]) + }) + + // Textbox - Tekst (invoer + heeft ingevoerde tekst) + // 1. telefoonnummer + test('text fill textbox phone', async ({ page, context }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const textbox = page.getByRole('textbox', { + name: 'Wat is uw telefoonnummer? (niet verplicht)', + }) + + await expect(textbox).toBeVisible() + + await textbox.focus() + await expect(textbox).toBeFocused() + + await textbox.fill('0612345678') + await expect(textbox).toHaveValue('0612345678') + }) + // 2. e-mailadres + test('text fill textbox mail', async ({ page, context }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const textbox = page.getByRole('textbox', { + name: 'Wat is uw e-mailadres? (niet verplicht)', + }) + + await expect(textbox).toBeVisible() + + await textbox.focus() + + await expect(textbox).toBeVisible() + + await textbox.fill('test@test.nl') + + await expect(textbox).toHaveValue('test@test.nl') + }) + // Textbox - Tekst (Axe) + test('text fill textbox axe', async ({ makeAxeBuilder, page, context }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const textbox = page.getByRole('textbox', { + name: 'Wat is uw e-mailadres? (niet verplicht)', + }) + + await expect(textbox).toBeVisible() + + await textbox.focus() + + await textbox.fill('test@test.nl') + + const accessibilityScanResults = await makeAxeBuilder().analyze() + expect(accessibilityScanResults.violations).toEqual([]) + }) + + // CHECKBOX TESTS // + + // Checkbox - Aanwezig + test('has checkbox', async ({ page, context }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const checkbox = page.getByRole('checkbox', { + name: 'Ja, ik geef de gemeente Purmerend toestemming om mijn melding door te sturen naar andere organisaties als de melding niet voor de gemeente is bestemd.', + }) + + await expect(checkbox).toBeVisible() + }) + // Checkbox - is(not)Checked + Axe + test('check/uncheck checkbox', async ({ + makeAxeBuilder, + page, + context, + }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const checkbox = page.getByRole('checkbox', { + name: 'Ja, ik geef de gemeente Purmerend toestemming om mijn melding door te sturen naar andere organisaties als de melding niet voor de gemeente is bestemd.', + }) + + await expect(checkbox).toBeVisible() + + await checkbox.check() + await expect(checkbox).toBeChecked() + + const accessibilityScanResultsChecked = await makeAxeBuilder().analyze() + expect(accessibilityScanResultsChecked.violations).toEqual([]) + + await checkbox.uncheck() + await expect(checkbox).not.toBeChecked() + + const accessibilityScanResultsUnchecked = await makeAxeBuilder().analyze() + expect(accessibilityScanResultsUnchecked.violations).toEqual([]) + }) + // Checkbox - Focus + Axe + test('focus checkbox', async ({ makeAxeBuilder, page, context }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const checkbox = page.getByRole('checkbox', { + name: 'Ja, ik geef de gemeente Purmerend toestemming om mijn melding door te sturen naar andere organisaties als de melding niet voor de gemeente is bestemd.', + }) + + await expect(checkbox).toBeVisible() + + await checkbox.focus() + await expect(checkbox).toBeFocused() + + const accessibilityScanResults = await makeAxeBuilder().analyze() + expect(accessibilityScanResults.violations).toEqual([]) + }) + + // Checkbox - Hover + Axe + test('hover checkbox', async ({ makeAxeBuilder, page, context }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const checkbox = page.getByRole('checkbox', { + name: 'Ja, ik geef de gemeente Purmerend toestemming om mijn melding door te sturen naar andere organisaties als de melding niet voor de gemeente is bestemd.', + }) + + await expect(checkbox).toBeVisible() + await checkbox.hover() + + const accessibilityScanResults = await makeAxeBuilder().analyze() + expect(accessibilityScanResults.violations).toEqual([]) + }) + + // Checkbox - Click + test('click checkbox', async ({ page, context }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const checkbox = page.getByRole('checkbox', { + name: 'Ja, ik geef de gemeente Purmerend toestemming om mijn melding door te sturen naar andere organisaties als de melding niet voor de gemeente is bestemd.', + }) + + await expect(checkbox).toBeVisible() + await checkbox.click() + await expect(checkbox).toBeChecked() + }) + // Checkbox - Press + Axe + test('press checkbox', async ({ makeAxeBuilder, page, context }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const checkbox = page.getByRole('checkbox', { + name: 'Ja, ik geef de gemeente Purmerend toestemming om mijn melding door te sturen naar andere organisaties als de melding niet voor de gemeente is bestemd.', + }) + + await expect(checkbox).toBeVisible() + await checkbox.focus() + await expect(checkbox).toBeFocused() + + await checkbox.press('Space') + await expect(checkbox).toBeChecked() + + const accessibilityScanResults = await makeAxeBuilder().analyze() + expect(accessibilityScanResults.violations).toEqual([]) + }) + + // PREVIOUS STEP BUTTON TESTS // + + // Previous Step Button - Aanwezig + // 1. top + // 2. bottom + test('has "previous page" buttons (top & bottom)', async ({ + page, + context, + }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const previousButton = page.getByRole('button', { + name: 'Vorige', + }) + const topPreviousButton = previousButton.first() + const bottomPreviousButton = previousButton.last() + + await expect(topPreviousButton).toBeVisible() + await expect(bottomPreviousButton).toBeVisible() + }) + + // Previous Step Button - Click + // 1. top + test('click - "previous page" button(top)', async ({ page, context }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const previousButton = page.getByRole('button', { + name: 'Vorige', + }) + + const topPreviousButton = previousButton.first() + const bottomPreviousButton = previousButton.last() + + await expect(topPreviousButton).toBeVisible() + await expect(bottomPreviousButton).toBeVisible() + await topPreviousButton.click() + + const heading = page.getByRole('heading', { + name: 'Locatie en vragen', + }) + + await expect(heading).toBeVisible() + }) + // 2. bottom + test('click - "previous page" button(bottom)', async ({ + page, + context, + }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const previousButton = page.getByRole('button', { + name: 'Vorige', + }) + + const topPreviousButton = previousButton.first() + const bottomPreviousButton = previousButton.last() + + await expect(topPreviousButton).toBeVisible() + await expect(bottomPreviousButton).toBeVisible() + await bottomPreviousButton.click() + + const heading = page.getByRole('heading', { + name: 'Locatie en vragen', + }) + + await expect(heading).toBeVisible() + }) + + // Previous Step Buttons (top & bottom) - Focus + Axe + test('focus - "previous page" buttons', async ({ + makeAxeBuilder, + page, + context, + }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const previousButton = page.getByRole('button', { + name: 'Vorige', + }) + + const topPreviousButton = previousButton.first() + const bottomPreviousButton = previousButton.last() + + await topPreviousButton.focus() + await expect(topPreviousButton).toBeFocused() + + await bottomPreviousButton.focus() + await expect(bottomPreviousButton).toBeFocused() + + const accessibilityScanResults = await makeAxeBuilder().analyze() + expect(accessibilityScanResults.violations).toEqual([]) + }) + + // Previous Step Buttons (top & bottom) - Hover + Axe + test('hover - "previous page" buttons', async ({ + makeAxeBuilder, + page, + context, + }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const previousButton = page.getByRole('button', { + name: 'Vorige', + }) + + const topPreviousButton = previousButton.first() + const bottomPreviousButton = previousButton.last() + + await topPreviousButton.hover() + await expect(topPreviousButton).toBeVisible() + + await bottomPreviousButton.hover() + await expect(bottomPreviousButton).toBeVisible() + + const accessibilityScanResults = await makeAxeBuilder().analyze() + expect(accessibilityScanResults.violations).toEqual([]) + }) + + // Previous Step Button - Press + // 1. top - enter + test('press(enter) - "previous page" button(top)', async ({ + page, + context, + }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const previousButton = page.getByRole('button', { + name: 'Vorige', + }) + + const topPreviousButton = previousButton.first() + const bottomPreviousButton = previousButton.last() + + await expect(topPreviousButton).toBeVisible() + await expect(bottomPreviousButton).toBeVisible() + await topPreviousButton.click() + + const heading = page.getByRole('heading', { + name: 'Locatie en vragen', + }) + + await expect(heading).toBeVisible() + }) + // 2. top - spacebar + test('press(space) - "previous page" button(top)', async ({ + page, + context, + }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const previousButton = page.getByRole('button', { + name: 'Vorige', + }) + + const topPreviousButton = previousButton.first() + const bottomPreviousButton = previousButton.last() + + await expect(topPreviousButton).toBeVisible() + await expect(bottomPreviousButton).toBeVisible() + await topPreviousButton.press('Space') + + const heading = page.getByRole('heading', { + name: 'Locatie en vragen', + }) + + await expect(heading).toBeVisible() + }) + // 3. bottom - enter + test('press(enter) - "previous page" button(bottom)', async ({ + page, + context, + }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const previousButton = page.getByRole('button', { + name: 'Vorige', + }) + + const topPreviousButton = previousButton.first() + const bottomPreviousButton = previousButton.last() + + await expect(topPreviousButton).toBeVisible() + await expect(bottomPreviousButton).toBeVisible() + await bottomPreviousButton.press('Enter') + + const heading = page.getByRole('heading', { + name: 'Locatie en vragen', + }) + + await expect(heading).toBeVisible() + }) + // 4. bottom - spacebar + test('press(space) - "previous page" button(bottom)', async ({ + page, + context, + }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const previousButton = page.getByRole('button', { + name: 'Vorige', + }) + + const topPreviousButton = previousButton.first() + const bottomPreviousButton = previousButton.last() + + await expect(topPreviousButton).toBeVisible() + await expect(bottomPreviousButton).toBeVisible() + await bottomPreviousButton.press('Space') + + const heading = page.getByRole('heading', { + name: 'Locatie en vragen', + }) + + await expect(heading).toBeVisible() + }) + + // NEXT STEP BUTTON TESTS // + + // Next Step Button - Aanwezig + test('has next button', async ({ page, context }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const nextButton = page.getByRole('button', { name: 'Volgende' }) + + await expect(nextButton).toBeVisible() + }) + + // Next Step Button - Click + test('click next button', async ({ page, context }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const nextButton = page.getByRole('button', { name: 'Volgende' }) + + await expect(nextButton).toBeVisible() + await nextButton.click() + + const heading = page.getByRole('heading', { + name: 'Versturen', + }) + + await expect(heading).toBeVisible() + }) + + // Next Step Button - Focus + Axe + test('focus next button', async ({ makeAxeBuilder, page, context }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const nextButton = page.getByRole('button', { name: 'Volgende' }) + + await expect(nextButton).toBeVisible() + + await nextButton.focus() + await expect(nextButton).toBeFocused() + + const accessibilityScanResults = await makeAxeBuilder().analyze() + expect(accessibilityScanResults.violations).toEqual([]) + }) + // Next Step Button - Hover + Axe + test('hover next button', async ({ makeAxeBuilder, page, context }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const nextButton = page.getByRole('button', { name: 'Volgende' }) + + await expect(nextButton).toBeVisible() + + await nextButton.hover() + await expect(nextButton).toBeVisible() + + const accessibilityScanResults = await makeAxeBuilder().analyze() + expect(accessibilityScanResults.violations).toEqual([]) + }) + + // Next Step Button - Press + test('press(enter) next button', async ({ page, context }) => { + formStateFixture(context, defaultStep3FormState) + + await page.goto(pageURL) + + const nextButton = page.getByRole('button', { name: 'Volgende' }) + + await expect(nextButton).toBeVisible() + + await nextButton.press('Enter') + + const heading = page.getByRole('heading', { + name: 'Versturen', + }) + + await expect(heading).toBeVisible() + }) + }) +}) diff --git a/test/e2e/step4.spec.ts b/test/e2e/step4.spec.ts new file mode 100644 index 00000000..1637a790 --- /dev/null +++ b/test/e2e/step4.spec.ts @@ -0,0 +1,623 @@ +import { expect } from '@playwright/test' +import { test, formStateFixture, websiteURL } from './util' +import { defaultStep4FormState } from '@/store/form_store' + +test.use({ + locale: 'nl-NL', + timezoneId: 'Europe/Amsterdam', + geolocation: { latitude: 51.6045656, longitude: 5.5342026 }, +}) + +interface MyTextConfig { + name: string + testConfig: { colorScheme?: string } + forcedColors?: boolean +} +const parameters: MyTextConfig[] = [ + { + name: 'Light mode', + testConfig: { colorScheme: 'light' }, + forcedColors: false, + }, +] + +parameters.forEach(async ({ name, testConfig, forcedColors }) => { + test.describe(`${name}`, () => { + const pageURL = `${websiteURL}nl/incident/samenvatting` + if (testConfig) { + test.use(testConfig as any) + } + + test('has title', async ({ page, context }) => { + formStateFixture(context, defaultStep4FormState) + await page.goto(pageURL) + + // Expect a title "to contain" a substring with the step + await expect(page).toHaveTitle(/Stap 4 van 4/i) + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Purmerend/i) + }) + + test('has heading', async ({ makeAxeBuilder, page, context }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const heading = page.getByRole('heading', { + name: 'Versturen', + level: 1, + }) + + await expect(heading).toBeVisible() + }) + + // SUBMIT BUTTON TESTS // + + // Submit Button - Aanwezig + Axe page check + test('has submit button', async ({ makeAxeBuilder, page, context }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const submitButton = page.getByRole('button', { name: 'Verstuur' }) + + await expect(submitButton).toBeVisible() + + const accessibilityScanResults = await makeAxeBuilder().analyze() + expect(accessibilityScanResults.violations).toEqual([]) + }) + + // Submit Button - Click + test('click submit button', async ({ page, context }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const submitButton = page.getByRole('button', { name: 'Verstuur' }) + + await expect(submitButton).toBeVisible() + + await submitButton.click() + + await expect(submitButton).toBeVisible() //visueel(functioneel?) disabled wanneer net ingedrukt + }) + + // Submit Button - Focus + test('focus submit button', async ({ makeAxeBuilder, page, context }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const submitButton = page.getByRole('button', { name: 'Verstuur' }) + + await expect(submitButton).toBeVisible() + + await submitButton.focus() + await expect(submitButton).toBeFocused() + + const accessibilityScanResults = await makeAxeBuilder().analyze() + expect(accessibilityScanResults.violations).toEqual([]) + }) + // Submit Button - Hover + Axe + test('hover submit button', async ({ makeAxeBuilder, page, context }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const submitButton = page.getByRole('button', { name: 'Verstuur' }) + + await expect(submitButton).toBeVisible() + + await submitButton.hover() + + const accessibilityScanResults = await makeAxeBuilder().analyze() + expect(accessibilityScanResults.violations).toEqual([]) + }) + + // Submit Button - Press(enter) + test('press submit button', async ({ page, context }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const submitButton = page.getByRole('button', { name: 'Verstuur' }) + + await expect(submitButton).toBeVisible() + + await submitButton.focus() + + await submitButton.press('Enter') + + const heading = page.getByRole('heading', { + name: 'Melding verzonden', + }) + + await expect(heading).toBeVisible() + }) + + // Submit Button - Navigates (to succespage) + test('navigate submit button -> succespagina)', async ({ + page, + context, + }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + const submitButton = page.getByRole('button', { name: 'Verstuur' }) + + const heading = page.getByRole('heading', { + name: 'Melding verzonden', + }) + + await expect(submitButton).toBeVisible() + + await submitButton.click() + + await expect(heading).toBeVisible() + await expect(heading).toHaveText('Melding verzonden') + }) + + // WIJZIG LINK TESTS // + + // Wijzig Link - Aanwezig + // 1. wijzig uw melding + test('has wijzig link - wijzig uw melding', async ({ page, context }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const wijzigLink = page.getByRole('link', { name: 'Wijzig uw melding' }) + + await expect(wijzigLink).toBeVisible() + }) + // 2. wijzig locatie en vragen + test('has wijzig link - wijzig locatie en vragen', async ({ + page, + context, + }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const wijzigLink = page.getByRole('link', { + name: 'Wijzig locatie en vragen', + }) + + await expect(wijzigLink).toBeVisible() + }) + // 3. wijzig contactgegevens + test('has wijzig link - wijzig contactgegevens', async ({ + page, + context, + }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const wijzigLink = page.getByRole('link', { + name: 'Wijzig contactgegevens', + }) + + await expect(wijzigLink).toBeVisible() + }) + + // Wijzig Link - Click + test('click wijzig link', async ({ page, context }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const wijzigLink = page.getByRole('link', { name: 'Wijzig uw melding' }) + + await expect(wijzigLink).toBeVisible() + + await wijzigLink.click() + }) + + // Wijzig Link - Focus + test('focus wijzig link', async ({ makeAxeBuilder, page, context }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const wijzigLink = page.getByRole('link', { name: 'Wijzig uw melding' }) + + await expect(wijzigLink).toBeVisible() + + await wijzigLink.focus() + await expect(wijzigLink).toBeFocused() + + const accessibilityScanResults = await makeAxeBuilder().analyze() + expect(accessibilityScanResults.violations).toEqual([]) + }) + + // Wijzig Link - Hover + test('hover wijzig link', async ({ page, context }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const wijzigLink = page.getByRole('link', { name: 'Wijzig uw melding' }) + + await expect(wijzigLink).toBeVisible() + + await wijzigLink.hover() + }) + + // Wijzig Link - Press(enter) + test('press wijzig link', async ({ page, context }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const wijzigLink = page.getByRole('link', { name: 'Wijzig uw melding' }) + + await expect(wijzigLink).toBeVisible() + + await wijzigLink.press('Enter') + }) + + // Wijzig Link - Navigate + // 1. to step 1 + test('navigate wijzig link (wijzig uw melding -> step 1)', async ({ + page, + context, + }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const wijzigLink = page.getByRole('link', { name: 'Wijzig uw melding' }) + const heading = page.getByRole('heading', { + name: 'Beschrijf uw melding', + }) + + await expect(wijzigLink).toBeVisible() + + await wijzigLink.click() + + await expect(heading).toBeVisible() + await expect(heading).toHaveText('Beschrijf uw melding') + }) + // 2. to step 2 + test('navigate wijzig link (wijzig locatie en vragen -> step 2)', async ({ + page, + context, + }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const wijzigLink = page.getByRole('link', { + name: 'Wijzig locatie en vragen', + }) + const heading = page.getByRole('heading', { + name: 'Locatie en vragen', + }) + + await expect(wijzigLink).toBeVisible() + + await wijzigLink.click() + + await expect(heading).toBeVisible() + await expect(heading).toHaveText('Locatie en vragen') + }) + // 3. to step 3 + test('navigate wijzig link (wijzig contactgegevens -> step 3)', async ({ + page, + context, + }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const wijzigLink = page.getByRole('link', { + name: 'Wijzig contactgegevens', + }) + const heading = page.getByRole('heading', { + name: 'Contactgegevens', + level: 1, + }) + + await expect(wijzigLink).toBeVisible() + + await wijzigLink.click() + + await expect(heading).toBeVisible() + await expect(heading).toHaveText('Contactgegevens') + }) + + // PREVIOUS STEP BUTTON TESTS // + + // Previous Step Buttons - Aanwezig + // 1. top + // 2. bottom + test('has "previous page" buttons', async ({ page, context }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const previousButton = page.getByRole('button', { + name: 'Vorige', + }) + const topPreviousButton = previousButton.first() + const bottomPreviousButton = previousButton.last() + + await expect(topPreviousButton).toBeVisible() + await expect(bottomPreviousButton).toBeVisible() + }) + + // Previous Step Button - Click + // 1. top + test('click - "previous page" button(top)', async ({ page, context }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const previousButton = page.getByRole('button', { + name: 'Vorige', + }) + + const topPreviousButton = previousButton.first() + const bottomPreviousButton = previousButton.last() + + await expect(topPreviousButton).toBeVisible() + await expect(bottomPreviousButton).toBeVisible() + await topPreviousButton.click() + + const heading = page.getByRole('heading', { + name: 'Contactgegevens', + level: 1, + }) + + await expect(heading).toBeVisible() + }) + // 2. bottom + test('click - "previous page" button(bottom)', async ({ + page, + context, + }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const previousButton = page.getByRole('button', { + name: 'Vorige', + }) + + const topPreviousButton = previousButton.first() + const bottomPreviousButton = previousButton.last() + + await expect(topPreviousButton).toBeVisible() + await expect(bottomPreviousButton).toBeVisible() + await bottomPreviousButton.click() + + const heading = page.getByRole('heading', { + name: 'Contactgegevens', + level: 1, + }) + + await expect(heading).toBeVisible() + }) + + // Previous Step Buttons (top & bottom) - Focus + // failed op axe (marker) + test('focus - "previous page" buttons', async ({ + makeAxeBuilder, + page, + context, + }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const previousButton = page.getByRole('button', { + name: 'Vorige', + }) + + const topPreviousButton = previousButton.first() + const bottomPreviousButton = previousButton.last() + + await topPreviousButton.focus() + await expect(topPreviousButton).toBeFocused() + + await bottomPreviousButton.focus() + await expect(bottomPreviousButton).toBeFocused() + + const accessibilityScanResults = await makeAxeBuilder().analyze() + expect(accessibilityScanResults.violations).toEqual([]) + }) + + // Previous Step Buttons (top & bottom) - Hover + test('hover - "previous page" buttons', async ({ page, context }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const previousButton = page.getByRole('button', { + name: 'Vorige', + }) + + const topPreviousButton = previousButton.first() + const bottomPreviousButton = previousButton.last() + + await topPreviousButton.hover() + await expect(topPreviousButton).toBeVisible() + + await bottomPreviousButton.hover() + await expect(bottomPreviousButton).toBeVisible() + }) + + // Previous Step Button - Press + // 1. top - enter + test('press(enter) - "previous page" button(top)', async ({ + page, + context, + }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const previousButton = page.getByRole('button', { + name: 'Vorige', + }) + + const topPreviousButton = previousButton.first() + const bottomPreviousButton = previousButton.last() + + await expect(topPreviousButton).toBeVisible() + await expect(bottomPreviousButton).toBeVisible() + await topPreviousButton.press('Enter') + + const heading = page.getByRole('heading', { + name: 'Contactgegevens', + level: 1, + }) + + await expect(heading).toBeVisible() + }) + // 2. top - spacebar + test('press(space) - "previous page" button(top)', async ({ + page, + context, + }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const previousButton = page.getByRole('button', { + name: 'Vorige', + }) + + const topPreviousButton = previousButton.first() + const bottomPreviousButton = previousButton.last() + + await expect(topPreviousButton).toBeVisible() + await expect(bottomPreviousButton).toBeVisible() + await topPreviousButton.press('Space') + await expect(topPreviousButton).toBeVisible() + }) + // 3. bottom - enter + test('press(enter) - "previous page" button(bottom)', async ({ + page, + context, + }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const previousButton = page.getByRole('button', { + name: 'Vorige', + }) + + const topPreviousButton = previousButton.first() + const bottomPreviousButton = previousButton.last() + + await expect(topPreviousButton).toBeVisible() + await expect(bottomPreviousButton).toBeVisible() + await bottomPreviousButton.press('Enter') + await expect(bottomPreviousButton).toBeVisible() + }) + // 4. bottom - spacebar + test('press(space) - "previous page" button(bottom)', async ({ + page, + context, + }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const previousButton = page.getByRole('button', { + name: 'Vorige', + }) + + const topPreviousButton = previousButton.first() + const bottomPreviousButton = previousButton.last() + + await expect(topPreviousButton).toBeVisible() + await expect(bottomPreviousButton).toBeVisible() + await bottomPreviousButton.press('Space') + await expect(bottomPreviousButton).toBeVisible() + }) + + // Previous Step Button - Navigates (to prev. step) + // 1. top + test('navigate previous step button(top) (step 4 -> step 3)', async ({ + makeAxeBuilder, + page, + context, + }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const previousButton = page.getByRole('button', { + name: 'Vorige', + }) + + const topPreviousButton = previousButton.first() + const bottomPreviousButton = previousButton.last() + + await expect(topPreviousButton).toBeVisible() + await expect(bottomPreviousButton).toBeVisible() + + await topPreviousButton.click() + + const heading = page.getByRole('heading', { + name: 'Contactgegevens', + level: 1, + }) + + await expect(heading).toBeVisible() + await expect(heading).toHaveText('Contactgegevens') + }) + // 2. bottom + test('navigate previous step button(bottom) (step 4 -> step 3)', async ({ + page, + context, + }) => { + formStateFixture(context, defaultStep4FormState) + + await page.goto(pageURL) + + const previousButton = page.getByRole('button', { + name: 'Vorige', + }) + + const topPreviousButton = previousButton.first() + const bottomPreviousButton = previousButton.last() + + await expect(bottomPreviousButton).toBeVisible() + await expect(topPreviousButton).toBeVisible() + + await bottomPreviousButton.click() + + const heading = page.getByRole('heading', { + name: 'Contactgegevens', + level: 1, + }) + + await expect(heading).toBeVisible() + await expect(heading).toHaveText('Contactgegevens') + }) + + // TODO: LOADER TEST // + // deze faalt nu wanneer page te snel laadt + // test('has loader', async ({ makeAxeBuilder, page, context }) => { + // formStateFixture(context, defaultStep4FormState) + + // await page.goto(pageURL) + + // const submitButton = page.getByRole('button', { name: 'Verstuur' }) + // await expect(submitButton).toBeVisible() + // await submitButton.click() + + // const loader = page.getByText('Laden...') + // await expect(loader).toBeVisible() + + // const accessibilityScanResults = await makeAxeBuilder().analyze() + // expect(accessibilityScanResults.violations).toEqual([]) + // }) + }) +}) diff --git a/test/e2e/thankyou.spec.ts b/test/e2e/thankyou.spec.ts new file mode 100644 index 00000000..ea07786d --- /dev/null +++ b/test/e2e/thankyou.spec.ts @@ -0,0 +1,134 @@ +import { expect } from '@playwright/test' +import { test, websiteURL } from './util' + +test.use({ + locale: 'nl-NL', + timezoneId: 'Europe/Amsterdam', + geolocation: { latitude: 51.6045656, longitude: 5.5342026 }, +}) + +interface MyTextConfig { + name: string + testConfig: { colorScheme?: string } + forcedColors?: boolean +} +const parameters: MyTextConfig[] = [ + { + name: 'Light mode', + testConfig: { colorScheme: 'light' }, + forcedColors: false, + }, + // { name: 'Dark mode', testConfig: { colorSchema: 'dark' } }, + // { name: 'Forced colors', forcedColors: true }, +] + +parameters.forEach(async ({ name, testConfig, forcedColors }) => { + test.describe(`${name}`, () => { + const pageURL = `${websiteURL}nl/incident/bedankt` // zorg dat je de juiste URL aanvulling invult per pagina + if (testConfig) { + test.use(testConfig as any) + } + + test('has title', async ({ page }) => { + await page.goto(pageURL) + + // Expect a title "to contain" a substring with the step + await expect(page).toHaveTitle(/Melding verzonden/i) + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Purmerend/i) + }) + + test('has heading', async ({ makeAxeBuilder, page }) => { + await page.goto(pageURL) + + const heading = page.getByRole('heading', { + name: 'Melding verzonden', + level: 1, + }) + + await expect(heading).toBeVisible() + await expect(heading).toHaveText('Melding verzonden') + + const accessibilityScanResults = await makeAxeBuilder().analyze() + expect(accessibilityScanResults.violations).toEqual([]) + }) + + // BUTTON TESTS // + // Button - Aanwezig + test('has button', async ({ page }) => { + await page.goto(pageURL) + + const button = page.getByRole('button', { name: 'Maak nog een melding' }) + + await expect(button).toBeVisible() + }) + // Button - Click + test('click button', async ({ makeAxeBuilder, page }) => { + await page.goto(pageURL) + + const button = page.getByRole('button', { name: 'Maak nog een melding' }) + + await expect(button).toBeVisible() + + await button.click() + + const heading = page.getByRole('heading', { + name: 'Beschrijf uw melding', + }) + + await expect(heading).toBeVisible() + }) + // Button - Focus + test('focus button', async ({ makeAxeBuilder, page }) => { + await page.goto(pageURL) + + const button = page.getByRole('button', { name: 'Maak nog een melding' }) + + await expect(button).toBeVisible() + + await button.focus() + await expect(button).toBeFocused() + + const focusedButton = button.and(page.locator('css=:focus')) + + await expect(focusedButton).toBeVisible() + + const accessibilityScanResults = await makeAxeBuilder().analyze() + expect(accessibilityScanResults.violations).toEqual([]) + }) + // Button - Hover + test('hover button', async ({ makeAxeBuilder, page }) => { + await page.goto(pageURL) + + const button = page.getByRole('button', { name: 'Maak nog een melding' }) + const hoveredButton = button.and(page.locator('css=:hover')) + + await expect(button).toBeVisible() + await button.hover() + + await expect(hoveredButton).toBeVisible() + + const accessibilityScanResults = await makeAxeBuilder().analyze() + expect(accessibilityScanResults.violations).toEqual([]) + }) + // Button - Press + test('press button', async ({ page }) => { + await page.goto(pageURL) + + const button = page.getByRole('button', { name: 'Maak nog een melding' }) + + const heading = page.getByRole('heading', { + name: 'Beschrijf uw melding', + }) + + await expect(button).toBeVisible() + + await button.focus() + await button.press('Space') + + await expect(heading).toBeVisible() + await expect(heading).toHaveText('Beschrijf uw melding') + }) + }) +}) From e45ddda8091878e12fb78efd0b58edb3fb0be610 Mon Sep 17 00:00:00 2001 From: Tessa Viergever <112861180+TessaViergever@users.noreply.github.com> Date: Thu, 16 Jan 2025 14:44:22 +0100 Subject: [PATCH 3/4] test: add playwright utility --- test/e2e/util.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 test/e2e/util.ts diff --git a/test/e2e/util.ts b/test/e2e/util.ts new file mode 100644 index 00000000..faef40c4 --- /dev/null +++ b/test/e2e/util.ts @@ -0,0 +1,42 @@ +/* eslint-disable react-hooks/rules-of-hooks */ +import { test as base, BrowserContext } from '@playwright/test' +import AxeBuilder from '@axe-core/playwright' +import { createSessionStorageFixture } from '@/store/form_store' +import { FormStoreState } from '@/types/stores' + +type AxeFixture = { + makeAxeBuilder: () => AxeBuilder +} + +// Extend base test by providing "makeAxeBuilder" +// +// This new "test" can be used in multiple test files, and each of them will get +// a consistently configured AxeBuilder instance. +export const test = base.extend({ + makeAxeBuilder: async ({ page }, use, testInfo) => { + const makeAxeBuilder = () => + new AxeBuilder({ page }) + .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']) + .exclude('.maplibregl-marker') + + await use(makeAxeBuilder) + }, +}) + +export const sessionStorageFixture = ( + context: BrowserContext, + sessionStorage: { [index: string]: string } +) => + context.addInitScript((storage) => { + for (const [key, value] of Object.entries(storage)) { + window.sessionStorage.setItem(key, value) + console.log(key, value) + } + }, sessionStorage) + +export const formStateFixture = ( + context: BrowserContext, + formState: Partial +) => sessionStorageFixture(context, createSessionStorageFixture(formState)) + +export const websiteURL = 'http://localhost:3000/' From f7cf1226dd9fa5a5bba4a0e3c1341631cd4a299c Mon Sep 17 00:00:00 2001 From: Tessa Viergever <112861180+TessaViergever@users.noreply.github.com> Date: Mon, 20 Jan 2025 11:59:03 +0100 Subject: [PATCH 4/4] test: increase timeout CI --- playwright.config.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/playwright.config.ts b/playwright.config.ts index 2df235ee..9efc5877 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -18,7 +18,8 @@ export default defineConfig({ forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 2 : 0, - timeout: 3000, + /* Shorter test time for dev */ + timeout: process.env.CI ? 30000 : 15000, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */