From 41872108e9036f4bd80effcfba54ffc07ed0828a Mon Sep 17 00:00:00 2001 From: David De Kok Date: Fri, 24 Apr 2026 16:15:12 +0200 Subject: [PATCH 1/9] Integrate `axe-core` for accessibility testing and export reusable utilities for a11y checks --- package-lock.json | 89 +++---------------- package.json | 1 + projects/ppwcode/ng-unit-testing/package.json | 1 + .../ng-unit-testing/src/lib/a11y/axe.ts | 52 +++++++++++ .../ppwcode/ng-unit-testing/src/public-api.ts | 1 + 5 files changed, 66 insertions(+), 78 deletions(-) create mode 100644 projects/ppwcode/ng-unit-testing/src/lib/a11y/axe.ts diff --git a/package-lock.json b/package-lock.json index 2bdeec31..33696767 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "@typescript-eslint/eslint-plugin": "8.58.2", "@typescript-eslint/parser": "8.58.2", "angular-eslint": "22.0.0", + "axe-core": "4.12.1", "concurrently": "8.2.1", "copyfiles": "2.4.1", "cross-env": "7.0.3", @@ -3767,9 +3768,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3787,9 +3785,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3807,9 +3802,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3827,9 +3819,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3847,9 +3836,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3867,9 +3853,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3887,9 +3870,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4348,9 +4328,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4372,9 +4349,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4396,9 +4370,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4420,9 +4391,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4444,9 +4412,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4468,9 +4433,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4691,9 +4653,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4708,9 +4667,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4725,9 +4681,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4742,9 +4695,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4759,9 +4709,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4776,9 +4723,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4793,9 +4737,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4810,9 +4751,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4827,9 +4765,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4844,9 +4779,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4861,9 +4793,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4878,9 +4807,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4895,9 +4821,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5965,6 +5888,16 @@ "meriyah": "^6.0.3" } }, + "node_modules/axe-core": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz", + "integrity": "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", diff --git a/package.json b/package.json index e3ce9b85..3d55f05a 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@typescript-eslint/eslint-plugin": "8.58.2", "@typescript-eslint/parser": "8.58.2", "angular-eslint": "22.0.0", + "axe-core": "4.12.1", "concurrently": "8.2.1", "copyfiles": "2.4.1", "cross-env": "7.0.3", diff --git a/projects/ppwcode/ng-unit-testing/package.json b/projects/ppwcode/ng-unit-testing/package.json index 1494e965..1d6188b8 100644 --- a/projects/ppwcode/ng-unit-testing/package.json +++ b/projects/ppwcode/ng-unit-testing/package.json @@ -10,6 +10,7 @@ "@angular/core": "^22.0.0", "@ppwcode/ng-async": "^22.0.0", "@ppwcode/ng-utils": "^22.0.0", + "axe-core": "^4.0.0", "jasmine-core": "^3.9.0 || ^4.0.0 || ^5.0.0" }, "dependencies": { diff --git a/projects/ppwcode/ng-unit-testing/src/lib/a11y/axe.ts b/projects/ppwcode/ng-unit-testing/src/lib/a11y/axe.ts new file mode 100644 index 00000000..d6cf23cf --- /dev/null +++ b/projects/ppwcode/ng-unit-testing/src/lib/a11y/axe.ts @@ -0,0 +1,52 @@ +import axe from 'axe-core' + +// `axe-core` ships a single namespace export (`export = axe`), so both the +// runtime function and the result/option types must be accessed via the +// namespace rather than named imports (which resolve to `{}` under some +// TypeScript module-resolution modes and break callers with TS2349). +export type AxeResults = axe.AxeResults +export type ElementContext = axe.ElementContext +export type RunOptions = axe.RunOptions + +/** + * The WCAG 2.0 A/AA, WCAG 2.1 A/AA and WCAG 2.2 A/AA rule tags — the accessibility compliance + * target for this application. + */ +export const WCAG_2_2_AA_TAGS: readonly string[] = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa'] + +/** + * Runs `axe-core` against the given DOM context with WCAG 2.2 AA rules. + * + * Usage in specs: + * const results = await runA11yChecks(fixture.nativeElement) + * expectNoA11yViolations(results) + */ +export function runA11yChecks(context: ElementContext, options: RunOptions = {}): Promise { + return axe.run(context, { + runOnly: { type: 'tag', values: [...WCAG_2_2_AA_TAGS] }, + ...options + }) +} + +/** + * Fails the current test with a formatted list of accessibility violations + * when `results.violations` is non-empty. + */ +export function expectNoA11yViolations(results: AxeResults): void { + if (results.violations.length === 0) { + return + } + + const details = results.violations + .map((violation) => { + const nodeList = violation.nodes + .map((node) => ` - ${node.target.join(', ')}\n ${node.failureSummary ?? ''}`) + .join('\n') + return ` • [${violation.id}] ${violation.help} (${violation.impact ?? 'n/a'})\n ${ + violation.helpUrl + }\n${nodeList}` + }) + .join('\n\n') + + throw new Error(`Expected no accessibility violations but found ${results.violations.length}:\n\n${details}`) +} diff --git a/projects/ppwcode/ng-unit-testing/src/public-api.ts b/projects/ppwcode/ng-unit-testing/src/public-api.ts index 425e807e..ea132f55 100644 --- a/projects/ppwcode/ng-unit-testing/src/public-api.ts +++ b/projects/ppwcode/ng-unit-testing/src/public-api.ts @@ -2,6 +2,7 @@ * Public API Surface of ng-unit-testing */ +export * from './lib/a11y/axe' export * from './lib/constants' export * from './lib/http/http-call-tester' export * from './lib/http/http-client-testing-controller' From e292d83a832bc263ca61e32d99d7a38f136686f8 Mon Sep 17 00:00:00 2001 From: David De Kok Date: Mon, 6 Jul 2026 09:55:48 +0200 Subject: [PATCH 2/9] Add unit tests with accessibility checks for the demo components --- src/app/app.component.spec.ts | 8 +++++ ...omponents-dashboard-demo.component.spec.ts | 27 ++++++++++++++++ ...omponents-page-container.component.spec.ts | 24 ++++++++++++++ ...confirmation-dialog-demo.component.spec.ts | 24 ++++++++++++++ .../dashboard-item-demo.component.spec.ts | 7 ++++- .../editable-table.component.spec.ts | 26 ++++++++++++++++ .../expandable-card-demo.component.spec.ts | 24 ++++++++++++++ .../global-error-handler.component.spec.ts | 24 ++++++++++++++ .../language-select.component.spec.ts | 26 ++++++++++++++++ .../in-memory-logging-demo.component.spec.ts | 26 ++++++++++++++++ .../message-bar/message-bar.component.spec.ts | 24 ++++++++++++++ .../empty-async-result.component.spec.ts | 24 ++++++++++++++ src/app/table/empty-page.component.spec.ts | 23 ++++++++++++++ .../expandable-table-demo.component.spec.ts | 26 ++++++++++++++++ src/app/table/table-demo.component.spec.ts | 31 +++++++++++++++++++ 15 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 src/app/components-dashboard-demo/components-dashboard-demo.component.spec.ts create mode 100644 src/app/components-page-container/components-page-container.component.spec.ts create mode 100644 src/app/confirmation-dialog-demo/confirmation-dialog-demo.component.spec.ts create mode 100644 src/app/editable-table/editable-table.component.spec.ts create mode 100644 src/app/expandable-card/expandable-card-demo.component.spec.ts create mode 100644 src/app/global-error-handler/global-error-handler.component.spec.ts create mode 100644 src/app/language-select/language-select.component.spec.ts create mode 100644 src/app/logging/in-memory-logging-demo/in-memory-logging-demo.component.spec.ts create mode 100644 src/app/message-bar/message-bar.component.spec.ts create mode 100644 src/app/table/empty-async-result.component.spec.ts create mode 100644 src/app/table/empty-page.component.spec.ts create mode 100644 src/app/table/expandable-table-demo/expandable-table-demo.component.spec.ts create mode 100644 src/app/table/table-demo.component.spec.ts diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index a4bb8bd1..c5c90caf 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -7,6 +7,7 @@ import { RouterTestingModule } from '@angular/router/testing' import { provideTranslateService } from '@ngx-translate/core' import { provideBreadcrumbOptions } from '@ppwcode/ng-router' import { WireframeComponent } from '@ppwcode/ng-wireframe' +import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' import { AppComponent } from './app.component' import LanguageSelectComponent from './language-select/language-select.component' @@ -32,4 +33,11 @@ describe('AppComponent', () => { const app = fixture.componentInstance expect(app).toBeTruthy() }) + + it('should have no accessibility violations in its default state', async () => { + const fixture = TestBed.createComponent(AppComponent) + fixture.detectChanges() + const results = await runA11yChecks(fixture.nativeElement) + expectNoA11yViolations(results) + }) }) diff --git a/src/app/components-dashboard-demo/components-dashboard-demo.component.spec.ts b/src/app/components-dashboard-demo/components-dashboard-demo.component.spec.ts new file mode 100644 index 00000000..3c67c7bf --- /dev/null +++ b/src/app/components-dashboard-demo/components-dashboard-demo.component.spec.ts @@ -0,0 +1,27 @@ +import { TestBed } from '@angular/core/testing' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { RouterTestingModule } from '@angular/router/testing' +import { provideTranslateService } from '@ngx-translate/core' +import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import ComponentsDashboardDemoComponent from './components-dashboard-demo.component' + +describe('ComponentsDashboardDemoComponent', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [ComponentsDashboardDemoComponent, NoopAnimationsModule, RouterTestingModule], + providers: [provideTranslateService({})] + }) + ) + + it('should create', () => { + const fixture = TestBed.createComponent(ComponentsDashboardDemoComponent) + expect(fixture.componentInstance).toBeTruthy() + }) + + it('should have no accessibility violations in its default state', async () => { + const fixture = TestBed.createComponent(ComponentsDashboardDemoComponent) + fixture.detectChanges() + const results = await runA11yChecks(fixture.nativeElement) + expectNoA11yViolations(results) + }) +}) diff --git a/src/app/components-page-container/components-page-container.component.spec.ts b/src/app/components-page-container/components-page-container.component.spec.ts new file mode 100644 index 00000000..b0cc6101 --- /dev/null +++ b/src/app/components-page-container/components-page-container.component.spec.ts @@ -0,0 +1,24 @@ +import { TestBed } from '@angular/core/testing' +import { RouterTestingModule } from '@angular/router/testing' +import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import ComponentsPageContainerComponent from './components-page-container.component' + +describe('ComponentsPageContainerComponent', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [ComponentsPageContainerComponent, RouterTestingModule] + }) + ) + + it('should create', () => { + const fixture = TestBed.createComponent(ComponentsPageContainerComponent) + expect(fixture.componentInstance).toBeTruthy() + }) + + it('should have no accessibility violations in its default state', async () => { + const fixture = TestBed.createComponent(ComponentsPageContainerComponent) + fixture.detectChanges() + const results = await runA11yChecks(fixture.nativeElement) + expectNoA11yViolations(results) + }) +}) diff --git a/src/app/confirmation-dialog-demo/confirmation-dialog-demo.component.spec.ts b/src/app/confirmation-dialog-demo/confirmation-dialog-demo.component.spec.ts new file mode 100644 index 00000000..5aca6169 --- /dev/null +++ b/src/app/confirmation-dialog-demo/confirmation-dialog-demo.component.spec.ts @@ -0,0 +1,24 @@ +import { TestBed } from '@angular/core/testing' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import ConfirmationDialogDemoComponent from './confirmation-dialog-demo.component' + +describe('ConfirmationDialogDemoComponent', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [ConfirmationDialogDemoComponent, NoopAnimationsModule] + }) + ) + + it('should create', () => { + const fixture = TestBed.createComponent(ConfirmationDialogDemoComponent) + expect(fixture.componentInstance).toBeTruthy() + }) + + it('should have no accessibility violations in its default state', async () => { + const fixture = TestBed.createComponent(ConfirmationDialogDemoComponent) + fixture.detectChanges() + const results = await runA11yChecks(fixture.nativeElement) + expectNoA11yViolations(results) + }) +}) diff --git a/src/app/dashboard-item-demo/dashboard-item-demo.component.spec.ts b/src/app/dashboard-item-demo/dashboard-item-demo.component.spec.ts index 28730aef..f382ac1f 100644 --- a/src/app/dashboard-item-demo/dashboard-item-demo.component.spec.ts +++ b/src/app/dashboard-item-demo/dashboard-item-demo.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { provideTranslateService } from '@ngx-translate/core' - +import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' import { DashboardItemDemoComponent } from './dashboard-item-demo.component' describe('DashboardItemDemoComponent', () => { @@ -21,4 +21,9 @@ describe('DashboardItemDemoComponent', () => { it('should create', () => { expect(component).toBeTruthy() }) + + it('should have no accessibility violations in its default state', async () => { + const results = await runA11yChecks(fixture.nativeElement) + expectNoA11yViolations(results) + }) }) diff --git a/src/app/editable-table/editable-table.component.spec.ts b/src/app/editable-table/editable-table.component.spec.ts new file mode 100644 index 00000000..925abfff --- /dev/null +++ b/src/app/editable-table/editable-table.component.spec.ts @@ -0,0 +1,26 @@ +import { TestBed } from '@angular/core/testing' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { provideTranslateService } from '@ngx-translate/core' +import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import EditableTableComponent from './editable-table.component' + +describe('EditableTableComponent', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [EditableTableComponent, NoopAnimationsModule], + providers: [provideTranslateService({})] + }) + ) + + it('should create', () => { + const fixture = TestBed.createComponent(EditableTableComponent) + expect(fixture.componentInstance).toBeTruthy() + }) + + it('should have no accessibility violations in its default state', async () => { + const fixture = TestBed.createComponent(EditableTableComponent) + fixture.detectChanges() + const results = await runA11yChecks(fixture.nativeElement) + expectNoA11yViolations(results) + }) +}) diff --git a/src/app/expandable-card/expandable-card-demo.component.spec.ts b/src/app/expandable-card/expandable-card-demo.component.spec.ts new file mode 100644 index 00000000..0b580d0f --- /dev/null +++ b/src/app/expandable-card/expandable-card-demo.component.spec.ts @@ -0,0 +1,24 @@ +import { TestBed } from '@angular/core/testing' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import ExpandableCardDemoComponent from './expandable-card-demo.component' + +describe('ExpandableCardDemoComponent', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [ExpandableCardDemoComponent, NoopAnimationsModule] + }) + ) + + it('should create', () => { + const fixture = TestBed.createComponent(ExpandableCardDemoComponent) + expect(fixture.componentInstance).toBeTruthy() + }) + + it('should have no accessibility violations in its default state', async () => { + const fixture = TestBed.createComponent(ExpandableCardDemoComponent) + fixture.detectChanges() + const results = await runA11yChecks(fixture.nativeElement) + expectNoA11yViolations(results) + }) +}) diff --git a/src/app/global-error-handler/global-error-handler.component.spec.ts b/src/app/global-error-handler/global-error-handler.component.spec.ts new file mode 100644 index 00000000..0695bd50 --- /dev/null +++ b/src/app/global-error-handler/global-error-handler.component.spec.ts @@ -0,0 +1,24 @@ +import { TestBed } from '@angular/core/testing' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import { GlobalErrorHandlerComponent } from './global-error-handler.component' + +describe('GlobalErrorHandlerComponent', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [GlobalErrorHandlerComponent, NoopAnimationsModule] + }) + ) + + it('should create', () => { + const fixture = TestBed.createComponent(GlobalErrorHandlerComponent) + expect(fixture.componentInstance).toBeTruthy() + }) + + it('should have no accessibility violations in its default state', async () => { + const fixture = TestBed.createComponent(GlobalErrorHandlerComponent) + fixture.detectChanges() + const results = await runA11yChecks(fixture.nativeElement) + expectNoA11yViolations(results) + }) +}) diff --git a/src/app/language-select/language-select.component.spec.ts b/src/app/language-select/language-select.component.spec.ts new file mode 100644 index 00000000..4e787212 --- /dev/null +++ b/src/app/language-select/language-select.component.spec.ts @@ -0,0 +1,26 @@ +import { TestBed } from '@angular/core/testing' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { provideTranslateService } from '@ngx-translate/core' +import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import LanguageSelectComponent from './language-select.component' + +describe('LanguageSelectComponent', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [LanguageSelectComponent, NoopAnimationsModule], + providers: [provideTranslateService({})] + }) + ) + + it('should create', () => { + const fixture = TestBed.createComponent(LanguageSelectComponent) + expect(fixture.componentInstance).toBeTruthy() + }) + + it('should have no accessibility violations in its default state', async () => { + const fixture = TestBed.createComponent(LanguageSelectComponent) + fixture.detectChanges() + const results = await runA11yChecks(fixture.nativeElement) + expectNoA11yViolations(results) + }) +}) diff --git a/src/app/logging/in-memory-logging-demo/in-memory-logging-demo.component.spec.ts b/src/app/logging/in-memory-logging-demo/in-memory-logging-demo.component.spec.ts new file mode 100644 index 00000000..47dcfe34 --- /dev/null +++ b/src/app/logging/in-memory-logging-demo/in-memory-logging-demo.component.spec.ts @@ -0,0 +1,26 @@ +import { TestBed } from '@angular/core/testing' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { provideTranslateService } from '@ngx-translate/core' +import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import { InMemoryLoggingDemoComponent } from './in-memory-logging-demo.component' + +describe('InMemoryLoggingDemoComponent', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [InMemoryLoggingDemoComponent, NoopAnimationsModule], + providers: [provideTranslateService({})] + }) + ) + + it('should create', () => { + const fixture = TestBed.createComponent(InMemoryLoggingDemoComponent) + expect(fixture.componentInstance).toBeTruthy() + }) + + it('should have no accessibility violations in its default state', async () => { + const fixture = TestBed.createComponent(InMemoryLoggingDemoComponent) + fixture.detectChanges() + const results = await runA11yChecks(fixture.nativeElement) + expectNoA11yViolations(results) + }) +}) diff --git a/src/app/message-bar/message-bar.component.spec.ts b/src/app/message-bar/message-bar.component.spec.ts new file mode 100644 index 00000000..01e5556a --- /dev/null +++ b/src/app/message-bar/message-bar.component.spec.ts @@ -0,0 +1,24 @@ +import { TestBed } from '@angular/core/testing' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import MessageBarComponent from './message-bar.component' + +describe('MessageBarDemoComponent', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [MessageBarComponent, NoopAnimationsModule] + }) + ) + + it('should create', () => { + const fixture = TestBed.createComponent(MessageBarComponent) + expect(fixture.componentInstance).toBeTruthy() + }) + + it('should have no accessibility violations in its default state', async () => { + const fixture = TestBed.createComponent(MessageBarComponent) + fixture.detectChanges() + const results = await runA11yChecks(fixture.nativeElement) + expectNoA11yViolations(results) + }) +}) diff --git a/src/app/table/empty-async-result.component.spec.ts b/src/app/table/empty-async-result.component.spec.ts new file mode 100644 index 00000000..834f349c --- /dev/null +++ b/src/app/table/empty-async-result.component.spec.ts @@ -0,0 +1,24 @@ +import { TestBed } from '@angular/core/testing' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import { EmptyAsyncResultComponent } from './empty-async-result.component' + +describe('EmptyAsyncResultComponent', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [EmptyAsyncResultComponent, NoopAnimationsModule] + }) + ) + + it('should create', () => { + const fixture = TestBed.createComponent(EmptyAsyncResultComponent) + expect(fixture.componentInstance).toBeTruthy() + }) + + it('should have no accessibility violations in its default state', async () => { + const fixture = TestBed.createComponent(EmptyAsyncResultComponent) + fixture.detectChanges() + const results = await runA11yChecks(fixture.nativeElement) + expectNoA11yViolations(results) + }) +}) diff --git a/src/app/table/empty-page.component.spec.ts b/src/app/table/empty-page.component.spec.ts new file mode 100644 index 00000000..8a59e234 --- /dev/null +++ b/src/app/table/empty-page.component.spec.ts @@ -0,0 +1,23 @@ +import { TestBed } from '@angular/core/testing' +import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import { EmptyTablePageComponent } from './empty-page.component' + +describe('EmptyTablePageComponent', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [EmptyTablePageComponent] + }) + ) + + it('should create', () => { + const fixture = TestBed.createComponent(EmptyTablePageComponent) + expect(fixture.componentInstance).toBeTruthy() + }) + + it('should have no accessibility violations in its default state', async () => { + const fixture = TestBed.createComponent(EmptyTablePageComponent) + fixture.detectChanges() + const results = await runA11yChecks(fixture.nativeElement) + expectNoA11yViolations(results) + }) +}) diff --git a/src/app/table/expandable-table-demo/expandable-table-demo.component.spec.ts b/src/app/table/expandable-table-demo/expandable-table-demo.component.spec.ts new file mode 100644 index 00000000..9f94d683 --- /dev/null +++ b/src/app/table/expandable-table-demo/expandable-table-demo.component.spec.ts @@ -0,0 +1,26 @@ +import { TestBed } from '@angular/core/testing' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { provideTranslateService } from '@ngx-translate/core' +import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import { ExpandableTableDemoComponent } from './expandable-table-demo.component' + +describe('ExpandableTableDemoComponent', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [ExpandableTableDemoComponent, NoopAnimationsModule], + providers: [provideTranslateService({})] + }) + ) + + it('should create', () => { + const fixture = TestBed.createComponent(ExpandableTableDemoComponent) + expect(fixture.componentInstance).toBeTruthy() + }) + + it('should have no accessibility violations in its default state', async () => { + const fixture = TestBed.createComponent(ExpandableTableDemoComponent) + fixture.detectChanges() + const results = await runA11yChecks(fixture.nativeElement) + expectNoA11yViolations(results) + }) +}) diff --git a/src/app/table/table-demo.component.spec.ts b/src/app/table/table-demo.component.spec.ts new file mode 100644 index 00000000..34e5d6d0 --- /dev/null +++ b/src/app/table/table-demo.component.spec.ts @@ -0,0 +1,31 @@ +import { TestBed } from '@angular/core/testing' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { RouterTestingModule } from '@angular/router/testing' +import { provideTranslateService } from '@ngx-translate/core' +import { PPW_TABLE_DEFAULT_OPTIONS } from '@ppwcode/ng-common-components' +import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import TableDemoComponent from './table-demo.component' + +describe('TableDemoComponent', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [TableDemoComponent, NoopAnimationsModule, RouterTestingModule], + providers: [ + provideTranslateService({}), + { provide: PPW_TABLE_DEFAULT_OPTIONS, useValue: { dateColumnFormatter: (v: unknown) => String(v) } } + ] + }) + ) + + it('should create', () => { + const fixture = TestBed.createComponent(TableDemoComponent) + expect(fixture.componentInstance).toBeTruthy() + }) + + it('should have no accessibility violations in its default state', async () => { + const fixture = TestBed.createComponent(TableDemoComponent) + fixture.detectChanges() + const results = await runA11yChecks(fixture.nativeElement) + expectNoA11yViolations(results) + }) +}) From 2509912a04fb7ef922e36be3cb044b2c527e5947 Mon Sep 17 00:00:00 2001 From: David De Kok Date: Mon, 6 Jul 2026 09:57:16 +0200 Subject: [PATCH 3/9] Fix a11y issues by adding aria-labels in the demo components --- .../src/lib/expandable-card/expandable-card.component.html | 2 +- .../src/lib/expandable-card/expandable-card.component.ts | 1 + .../src/lib/loader/loader.component.html | 2 +- .../ng-common-components/src/lib/table/table.component.html | 1 + .../components-dashboard-demo.component.html | 2 +- src/app/editable-table/editable-table.component.html | 4 ++-- src/app/expandable-card/expandable-card-demo.component.html | 2 +- src/app/language-select/language-select.component.html | 6 +++++- 8 files changed, 13 insertions(+), 7 deletions(-) diff --git a/projects/ppwcode/ng-common-components/src/lib/expandable-card/expandable-card.component.html b/projects/ppwcode/ng-common-components/src/lib/expandable-card/expandable-card.component.html index 2771ba22..eac3eca8 100644 --- a/projects/ppwcode/ng-common-components/src/lib/expandable-card/expandable-card.component.html +++ b/projects/ppwcode/ng-common-components/src/lib/expandable-card/expandable-card.component.html @@ -6,7 +6,7 @@ (opened)="panelOpenState = true" (closed)="panelOpenState = false" > - + @if (cardTitle()) { {{ cardTitle() }} diff --git a/projects/ppwcode/ng-common-components/src/lib/expandable-card/expandable-card.component.ts b/projects/ppwcode/ng-common-components/src/lib/expandable-card/expandable-card.component.ts index 62e0e37f..c32ae26e 100644 --- a/projects/ppwcode/ng-common-components/src/lib/expandable-card/expandable-card.component.ts +++ b/projects/ppwcode/ng-common-components/src/lib/expandable-card/expandable-card.component.ts @@ -14,6 +14,7 @@ export class ExpandableCardComponent implements OnInit { // Inputs cardTitle: InputSignal = input() cardDescription: InputSignal = input() + headerAriaLabel: InputSignal = input() canBeCollapsed: InputSignal = input(true) openAsExpanded: InputSignal = input(true) diff --git a/projects/ppwcode/ng-common-components/src/lib/loader/loader.component.html b/projects/ppwcode/ng-common-components/src/lib/loader/loader.component.html index 5aeb3d76..91bc9624 100644 --- a/projects/ppwcode/ng-common-components/src/lib/loader/loader.component.html +++ b/projects/ppwcode/ng-common-components/src/lib/loader/loader.component.html @@ -1,6 +1,6 @@
@if (loading()) { - + } @else {
 
} diff --git a/projects/ppwcode/ng-common-components/src/lib/table/table.component.html b/projects/ppwcode/ng-common-components/src/lib/table/table.component.html index 6d6195a1..35a466b0 100644 --- a/projects/ppwcode/ng-common-components/src/lib/table/table.component.html +++ b/projects/ppwcode/ng-common-components/src/lib/table/table.component.html @@ -128,6 +128,7 @@ (click)="toggleExpand(record.initialRecord); $event.stopPropagation()" class="ppw-table-expand-toggle-button" [class.ppw-table-expand-toggle-button--expanded]="isExpanded(record.initialRecord)" + [aria-label]="isExpanded(record.initialRecord) ? 'Collapse row' : 'Expand row'" > keyboard_arrow_down diff --git a/src/app/components-dashboard-demo/components-dashboard-demo.component.html b/src/app/components-dashboard-demo/components-dashboard-demo.component.html index ec581101..45cfa563 100644 --- a/src/app/components-dashboard-demo/components-dashboard-demo.component.html +++ b/src/app/components-dashboard-demo/components-dashboard-demo.component.html @@ -16,7 +16,7 @@
- +
diff --git a/src/app/editable-table/editable-table.component.html b/src/app/editable-table/editable-table.component.html index 4896519f..27bdab87 100644 --- a/src/app/editable-table/editable-table.component.html +++ b/src/app/editable-table/editable-table.component.html @@ -12,12 +12,12 @@ - - diff --git a/src/app/expandable-card/expandable-card-demo.component.html b/src/app/expandable-card/expandable-card-demo.component.html index 6da713c5..e71849c7 100644 --- a/src/app/expandable-card/expandable-card-demo.component.html +++ b/src/app/expandable-card/expandable-card-demo.component.html @@ -17,7 +17,7 @@
card contents
- +
this is a card without a title or description in the header
diff --git a/src/app/language-select/language-select.component.html b/src/app/language-select/language-select.component.html index 97780f66..9085fabe 100644 --- a/src/app/language-select/language-select.component.html +++ b/src/app/language-select/language-select.component.html @@ -1,6 +1,10 @@
- +
{{ selectedLanguage }} From 432c4dc1863a83296321d500d7d8e1f859b2136d Mon Sep 17 00:00:00 2001 From: David De Kok Date: Mon, 6 Jul 2026 10:30:02 +0200 Subject: [PATCH 4/9] Translate all aria-labels across components --- .../src/lib/loader/loader.component.html | 6 +++++- .../src/lib/loader/loader.component.spec.ts | 4 +++- .../src/lib/loader/loader.component.ts | 3 ++- .../src/lib/table/table.component.html | 7 ++++++- .../src/lib/table/table.component.spec.ts | 4 +++- .../src/lib/table/table.module.ts | 4 +++- .../components-dashboard-demo.component.html | 2 +- .../components-dashboard-demo.component.ts | 3 ++- .../editable-table.component.html | 4 ++-- .../editable-table.component.ts | 4 +++- .../expandable-card-demo.component.html | 2 +- .../expandable-card-demo.component.spec.ts | 4 +++- .../expandable-card-demo.component.ts | 3 ++- .../language-select.component.html | 2 +- src/assets/i18n/en.json | 20 ++++++++++++++++++- src/assets/i18n/nl.json | 20 ++++++++++++++++++- 16 files changed, 75 insertions(+), 17 deletions(-) diff --git a/projects/ppwcode/ng-common-components/src/lib/loader/loader.component.html b/projects/ppwcode/ng-common-components/src/lib/loader/loader.component.html index 91bc9624..b3b38284 100644 --- a/projects/ppwcode/ng-common-components/src/lib/loader/loader.component.html +++ b/projects/ppwcode/ng-common-components/src/lib/loader/loader.component.html @@ -1,6 +1,10 @@
@if (loading()) { - + } @else {
 
} diff --git a/projects/ppwcode/ng-common-components/src/lib/loader/loader.component.spec.ts b/projects/ppwcode/ng-common-components/src/lib/loader/loader.component.spec.ts index 999e739a..57f44a10 100644 --- a/projects/ppwcode/ng-common-components/src/lib/loader/loader.component.spec.ts +++ b/projects/ppwcode/ng-common-components/src/lib/loader/loader.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' +import { provideTranslateService } from '@ngx-translate/core' import { LoaderComponent } from './loader.component' @@ -8,7 +9,8 @@ describe('LoaderComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [LoaderComponent] + imports: [LoaderComponent], + providers: [provideTranslateService({})] }).compileComponents() fixture = TestBed.createComponent(LoaderComponent) diff --git a/projects/ppwcode/ng-common-components/src/lib/loader/loader.component.ts b/projects/ppwcode/ng-common-components/src/lib/loader/loader.component.ts index eecd67e3..31536e4e 100644 --- a/projects/ppwcode/ng-common-components/src/lib/loader/loader.component.ts +++ b/projects/ppwcode/ng-common-components/src/lib/loader/loader.component.ts @@ -1,9 +1,10 @@ import { Component, input, InputSignal, ChangeDetectionStrategy } from '@angular/core' import { MatProgressBar } from '@angular/material/progress-bar' +import { TranslatePipe } from '@ngx-translate/core' @Component({ selector: 'ppw-loader', - imports: [MatProgressBar], + imports: [MatProgressBar, TranslatePipe], templateUrl: './loader.component.html', changeDetection: ChangeDetectionStrategy.Eager, styleUrl: './loader.component.scss' diff --git a/projects/ppwcode/ng-common-components/src/lib/table/table.component.html b/projects/ppwcode/ng-common-components/src/lib/table/table.component.html index 35a466b0..5e8e198c 100644 --- a/projects/ppwcode/ng-common-components/src/lib/table/table.component.html +++ b/projects/ppwcode/ng-common-components/src/lib/table/table.component.html @@ -128,7 +128,12 @@ (click)="toggleExpand(record.initialRecord); $event.stopPropagation()" class="ppw-table-expand-toggle-button" [class.ppw-table-expand-toggle-button--expanded]="isExpanded(record.initialRecord)" - [aria-label]="isExpanded(record.initialRecord) ? 'Collapse row' : 'Expand row'" + [aria-label]=" + (isExpanded(record.initialRecord) + ? 'common-components.table.collapse-row' + : 'common-components.table.expand-row' + ) | translate + " > keyboard_arrow_down diff --git a/projects/ppwcode/ng-common-components/src/lib/table/table.component.spec.ts b/projects/ppwcode/ng-common-components/src/lib/table/table.component.spec.ts index 740fd19b..ba326b32 100644 --- a/projects/ppwcode/ng-common-components/src/lib/table/table.component.spec.ts +++ b/projects/ppwcode/ng-common-components/src/lib/table/table.component.spec.ts @@ -1,5 +1,6 @@ import { ChangeDetectionStrategy, Component, signal, ViewChild, WritableSignal } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' +import { provideTranslateService } from '@ngx-translate/core' import { ColumnType } from './columns/column' import { TableComponent } from './table.component' @@ -147,7 +148,8 @@ describe('TableComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [TestTableComponent] + imports: [TestTableComponent], + providers: [provideTranslateService({})] }) fixture = TestBed.createComponent(TestTableComponent) diff --git a/projects/ppwcode/ng-common-components/src/lib/table/table.module.ts b/projects/ppwcode/ng-common-components/src/lib/table/table.module.ts index f3f130dc..14352ebd 100644 --- a/projects/ppwcode/ng-common-components/src/lib/table/table.module.ts +++ b/projects/ppwcode/ng-common-components/src/lib/table/table.module.ts @@ -14,6 +14,7 @@ import { TableComponent } from './table.component' import { FormTableComponent } from './form-table.component' import { MatButtonModule } from '@angular/material/button' import { MatSortModule } from '@angular/material/sort' +import { TranslatePipe } from '@ngx-translate/core' @NgModule({ declarations: [ @@ -40,7 +41,8 @@ import { MatSortModule } from '@angular/material/sort' NgTemplateOutlet, CdkDragPlaceholder, NgComponentOutlet, - MatSortModule + MatSortModule, + TranslatePipe ], exports: [ TableComponent, diff --git a/src/app/components-dashboard-demo/components-dashboard-demo.component.html b/src/app/components-dashboard-demo/components-dashboard-demo.component.html index 45cfa563..4c334942 100644 --- a/src/app/components-dashboard-demo/components-dashboard-demo.component.html +++ b/src/app/components-dashboard-demo/components-dashboard-demo.component.html @@ -16,7 +16,7 @@
- +
diff --git a/src/app/components-dashboard-demo/components-dashboard-demo.component.ts b/src/app/components-dashboard-demo/components-dashboard-demo.component.ts index f3df7788..a66bdab5 100644 --- a/src/app/components-dashboard-demo/components-dashboard-demo.component.ts +++ b/src/app/components-dashboard-demo/components-dashboard-demo.component.ts @@ -2,13 +2,14 @@ import { ChangeDetectionStrategy, Component, computed, inject, Signal, TemplateR import { MatIcon } from '@angular/material/icon' import { MatProgressSpinner } from '@angular/material/progress-spinner' import { Router } from '@angular/router' +import { TranslatePipe } from '@ngx-translate/core' import { DashboardItem, DashboardItemAction, DashboardItemsTableComponent } from '@ppwcode/ng-common-components' import { getFullRoutePath } from '@ppwcode/ng-router' import { ROUTE_MAP } from '../app.routes' @Component({ selector: 'ppw-components-dashboard-demo', - imports: [DashboardItemsTableComponent, MatIcon, MatProgressSpinner], + imports: [DashboardItemsTableComponent, MatIcon, MatProgressSpinner, TranslatePipe], templateUrl: './components-dashboard-demo.component.html', styleUrl: './components-dashboard-demo.component.scss', changeDetection: ChangeDetectionStrategy.OnPush diff --git a/src/app/editable-table/editable-table.component.html b/src/app/editable-table/editable-table.component.html index 27bdab87..6c3fcb88 100644 --- a/src/app/editable-table/editable-table.component.html +++ b/src/app/editable-table/editable-table.component.html @@ -12,12 +12,12 @@ - - diff --git a/src/app/editable-table/editable-table.component.ts b/src/app/editable-table/editable-table.component.ts index ef18964e..88cf0178 100644 --- a/src/app/editable-table/editable-table.component.ts +++ b/src/app/editable-table/editable-table.component.ts @@ -6,6 +6,7 @@ import { MatCard, MatCardContent, MatCardHeader, MatCardTitle } from '@angular/m import { MatFormField, MatLabel } from '@angular/material/form-field' import { MatIcon } from '@angular/material/icon' import { MatInput } from '@angular/material/input' +import { TranslatePipe } from '@ngx-translate/core' import { AsyncResultModule } from '@ppwcode/ng-async' import { FormTableComponent, PpwTableModule, PpwTableOptions } from '@ppwcode/ng-common-components' @@ -32,7 +33,8 @@ type TodosForm = { MatIconButton, MatCardHeader, MatCardTitle, - JsonPipe + JsonPipe, + TranslatePipe ], templateUrl: './editable-table.component.html', styleUrl: './editable-table.component.scss', diff --git a/src/app/expandable-card/expandable-card-demo.component.html b/src/app/expandable-card/expandable-card-demo.component.html index e71849c7..d27011e9 100644 --- a/src/app/expandable-card/expandable-card-demo.component.html +++ b/src/app/expandable-card/expandable-card-demo.component.html @@ -17,7 +17,7 @@
card contents
- +
this is a card without a title or description in the header
diff --git a/src/app/expandable-card/expandable-card-demo.component.spec.ts b/src/app/expandable-card/expandable-card-demo.component.spec.ts index 0b580d0f..d07f92fa 100644 --- a/src/app/expandable-card/expandable-card-demo.component.spec.ts +++ b/src/app/expandable-card/expandable-card-demo.component.spec.ts @@ -1,12 +1,14 @@ import { TestBed } from '@angular/core/testing' import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { provideTranslateService } from '@ngx-translate/core' import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' import ExpandableCardDemoComponent from './expandable-card-demo.component' describe('ExpandableCardDemoComponent', () => { beforeEach(() => TestBed.configureTestingModule({ - imports: [ExpandableCardDemoComponent, NoopAnimationsModule] + imports: [ExpandableCardDemoComponent, NoopAnimationsModule], + providers: [provideTranslateService({})] }) ) diff --git a/src/app/expandable-card/expandable-card-demo.component.ts b/src/app/expandable-card/expandable-card-demo.component.ts index ffcdc061..f5a7502e 100644 --- a/src/app/expandable-card/expandable-card-demo.component.ts +++ b/src/app/expandable-card/expandable-card-demo.component.ts @@ -1,11 +1,12 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' +import { TranslatePipe } from '@ngx-translate/core' import { ExpandableCardComponent } from '@ppwcode/ng-common-components' @Component({ selector: 'ppw-expandable-card-demo', templateUrl: './expandable-card-demo.component.html', styleUrls: ['./expandable-card-demo.component.scss'], - imports: [ExpandableCardComponent], + imports: [ExpandableCardComponent, TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush }) export default class ExpandableCardDemoComponent {} diff --git a/src/app/language-select/language-select.component.html b/src/app/language-select/language-select.component.html index 9085fabe..2006ac9f 100644 --- a/src/app/language-select/language-select.component.html +++ b/src/app/language-select/language-select.component.html @@ -1,7 +1,7 @@
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 216c46f4..60944ddf 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1,18 +1,33 @@ { "APP_TITLE": "PPW Code", "button": { + "add": "Add", + "delete": "Delete", "open": "Open" }, "confirmation-dialog": { "body": "Are you sure you want to execute this action?\n\nThis action cannot be undone.", "title": "Confirm execution?" }, + "common-components": { + "loader": { + "loading": "Loading" + }, + "table": { + "collapse-row": "Collapse row", + "expand-row": "Expand row" + } + }, "dashboard": { "components-description": "Opens the dashboard page for the PPW components", "confirmation-dialog-description": "A demo page showcasing the confirm dialog component", "expandable-card-description": "A demo page that shows all possibilities with the expandable card component", "global-error-handler-description": "A demo page that shows all possibilities with the global error handler", - "in-memory-logging-description": "A demo page that shows all possibilities with the in-memory logging provider" + "in-memory-logging-description": "A demo page that shows all possibilities with the in-memory logging provider", + "loading-spinner": "Loading spinner" + }, + "language-select": { + "aria-label": "Language selector" }, "global-error-dialog": { "copy-all-errors": "Copy All Errors", @@ -24,6 +39,9 @@ "single-error-details": "Error Details", "title": "Unexpected error" }, + "expandable-card": { + "card-without-visible-title": "Card without visible title" + }, "navigation": { "components": "Components", "confirmation_dialog": "Confirmation Dialog", diff --git a/src/assets/i18n/nl.json b/src/assets/i18n/nl.json index cad54242..55f6c3ff 100644 --- a/src/assets/i18n/nl.json +++ b/src/assets/i18n/nl.json @@ -1,18 +1,33 @@ { "APP_TITLE": "PPW Code", "button": { + "add": "Toevoegen", + "delete": "Verwijderen", "open": "Openen" }, "confirmation-dialog": { "body": "Bent u zeker dat u deze actie wil uitvoeren?\n\nDeze actie kan niet ongedaan gemaakt worden.", "title": "Bevestig uitvoering?" }, + "common-components": { + "loader": { + "loading": "Laden" + }, + "table": { + "collapse-row": "Rij dichtklappen", + "expand-row": "Rij openklappen" + } + }, "dashboard": { "components-description": "Opent de dashboard pagina van de PPW components", "confirmation-dialog-description": "Een demo pagina die de confirmation dialog component laat zien", "expandable-card-description": "Een demo pagina die alle mogelijkheden met de expandable card component laat zien", "global-error-handler-description": "Een demo pagina die alle mogelijkheden van de global error handler laat zien", - "in-memory-logging-description": "Een demo pagina die alle mogelijkheden van de in-memory logging provider laat zien" + "in-memory-logging-description": "Een demo pagina die alle mogelijkheden van de in-memory logging provider laat zien", + "loading-spinner": "Laadindicator" + }, + "language-select": { + "aria-label": "Taalselector" }, "global-error-dialog": { "copy-all-errors": "Kopieer alle fouten", @@ -24,6 +39,9 @@ "single-error-details": "Fout details", "title": "Onverwachte fout" }, + "expandable-card": { + "card-without-visible-title": "Kaart zonder zichtbare titel" + }, "navigation": { "components": "Componenten", "confirmation_dialog": "Confirmation Dialog", From a7fa7edbd488e49a4c54222aa1dc4439e8d76350 Mon Sep 17 00:00:00 2001 From: David De Kok Date: Mon, 6 Jul 2026 10:51:18 +0200 Subject: [PATCH 5/9] Refactor spec files to use `verifyA11y` utility for streamlined accessibility testing --- .../ng-unit-testing/src/lib/a11y/axe.ts | 35 +++++++++++++++++++ src/app/app.component.spec.ts | 9 ++--- ...omponents-dashboard-demo.component.spec.ts | 9 ++--- ...omponents-page-container.component.spec.ts | 9 ++--- ...confirmation-dialog-demo.component.spec.ts | 9 ++--- .../dashboard-item-demo.component.spec.ts | 7 ++-- .../editable-table.component.spec.ts | 9 ++--- .../expandable-card-demo.component.spec.ts | 9 ++--- .../global-error-handler.component.spec.ts | 9 ++--- .../language-select.component.spec.ts | 9 ++--- .../in-memory-logging-demo.component.spec.ts | 9 ++--- .../message-bar/message-bar.component.spec.ts | 9 ++--- .../empty-async-result.component.spec.ts | 9 ++--- src/app/table/empty-page.component.spec.ts | 9 ++--- .../expandable-table-demo.component.spec.ts | 9 ++--- src/app/table/table-demo.component.spec.ts | 9 ++--- 16 files changed, 65 insertions(+), 103 deletions(-) diff --git a/projects/ppwcode/ng-unit-testing/src/lib/a11y/axe.ts b/projects/ppwcode/ng-unit-testing/src/lib/a11y/axe.ts index d6cf23cf..ea22c7c5 100644 --- a/projects/ppwcode/ng-unit-testing/src/lib/a11y/axe.ts +++ b/projects/ppwcode/ng-unit-testing/src/lib/a11y/axe.ts @@ -1,3 +1,5 @@ +import type { Type } from '@angular/core' +import { type ComponentFixture, TestBed } from '@angular/core/testing' import axe from 'axe-core' // `axe-core` ships a single namespace export (`export = axe`), so both the @@ -8,6 +10,24 @@ export type AxeResults = axe.AxeResults export type ElementContext = axe.ElementContext export type RunOptions = axe.RunOptions +export interface VerifyA11yOptions { + /** + * The generated test name. + */ + readonly description?: string + + /** + * Optional hook for setting inputs or interacting with the fixture before + * running axe. + */ + readonly prepare?: (fixture: ComponentFixture) => void | Promise + + /** + * Additional axe-core options. + */ + readonly runOptions?: RunOptions +} + /** * The WCAG 2.0 A/AA, WCAG 2.1 A/AA and WCAG 2.2 A/AA rule tags — the accessibility compliance * target for this application. @@ -50,3 +70,18 @@ export function expectNoA11yViolations(results: AxeResults): void { throw new Error(`Expected no accessibility violations but found ${results.violations.length}:\n\n${details}`) } + +/** + * Registers a default accessibility spec for the given component. + */ +export function verifyA11y(component: Type, options: VerifyA11yOptions = {}): void { + it(options.description ?? 'should have no accessibility violations in its default state', async () => { + const fixture = TestBed.createComponent(component) + fixture.detectChanges() + await options.prepare?.(fixture) + fixture.detectChanges() + + const results = await runA11yChecks(fixture.nativeElement, options.runOptions) + expectNoA11yViolations(results) + }) +} diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index c5c90caf..aa40a7f1 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -6,8 +6,8 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations' import { RouterTestingModule } from '@angular/router/testing' import { provideTranslateService } from '@ngx-translate/core' import { provideBreadcrumbOptions } from '@ppwcode/ng-router' +import { verifyA11y } from '@ppwcode/ng-unit-testing' import { WireframeComponent } from '@ppwcode/ng-wireframe' -import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' import { AppComponent } from './app.component' import LanguageSelectComponent from './language-select/language-select.component' @@ -34,10 +34,5 @@ describe('AppComponent', () => { expect(app).toBeTruthy() }) - it('should have no accessibility violations in its default state', async () => { - const fixture = TestBed.createComponent(AppComponent) - fixture.detectChanges() - const results = await runA11yChecks(fixture.nativeElement) - expectNoA11yViolations(results) - }) + verifyA11y(AppComponent) }) diff --git a/src/app/components-dashboard-demo/components-dashboard-demo.component.spec.ts b/src/app/components-dashboard-demo/components-dashboard-demo.component.spec.ts index 3c67c7bf..5120d602 100644 --- a/src/app/components-dashboard-demo/components-dashboard-demo.component.spec.ts +++ b/src/app/components-dashboard-demo/components-dashboard-demo.component.spec.ts @@ -2,7 +2,7 @@ import { TestBed } from '@angular/core/testing' import { NoopAnimationsModule } from '@angular/platform-browser/animations' import { RouterTestingModule } from '@angular/router/testing' import { provideTranslateService } from '@ngx-translate/core' -import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import { verifyA11y } from '@ppwcode/ng-unit-testing' import ComponentsDashboardDemoComponent from './components-dashboard-demo.component' describe('ComponentsDashboardDemoComponent', () => { @@ -18,10 +18,5 @@ describe('ComponentsDashboardDemoComponent', () => { expect(fixture.componentInstance).toBeTruthy() }) - it('should have no accessibility violations in its default state', async () => { - const fixture = TestBed.createComponent(ComponentsDashboardDemoComponent) - fixture.detectChanges() - const results = await runA11yChecks(fixture.nativeElement) - expectNoA11yViolations(results) - }) + verifyA11y(ComponentsDashboardDemoComponent) }) diff --git a/src/app/components-page-container/components-page-container.component.spec.ts b/src/app/components-page-container/components-page-container.component.spec.ts index b0cc6101..432de5ba 100644 --- a/src/app/components-page-container/components-page-container.component.spec.ts +++ b/src/app/components-page-container/components-page-container.component.spec.ts @@ -1,6 +1,6 @@ import { TestBed } from '@angular/core/testing' import { RouterTestingModule } from '@angular/router/testing' -import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import { verifyA11y } from '@ppwcode/ng-unit-testing' import ComponentsPageContainerComponent from './components-page-container.component' describe('ComponentsPageContainerComponent', () => { @@ -15,10 +15,5 @@ describe('ComponentsPageContainerComponent', () => { expect(fixture.componentInstance).toBeTruthy() }) - it('should have no accessibility violations in its default state', async () => { - const fixture = TestBed.createComponent(ComponentsPageContainerComponent) - fixture.detectChanges() - const results = await runA11yChecks(fixture.nativeElement) - expectNoA11yViolations(results) - }) + verifyA11y(ComponentsPageContainerComponent) }) diff --git a/src/app/confirmation-dialog-demo/confirmation-dialog-demo.component.spec.ts b/src/app/confirmation-dialog-demo/confirmation-dialog-demo.component.spec.ts index 5aca6169..1660f478 100644 --- a/src/app/confirmation-dialog-demo/confirmation-dialog-demo.component.spec.ts +++ b/src/app/confirmation-dialog-demo/confirmation-dialog-demo.component.spec.ts @@ -1,6 +1,6 @@ import { TestBed } from '@angular/core/testing' import { NoopAnimationsModule } from '@angular/platform-browser/animations' -import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import { verifyA11y } from '@ppwcode/ng-unit-testing' import ConfirmationDialogDemoComponent from './confirmation-dialog-demo.component' describe('ConfirmationDialogDemoComponent', () => { @@ -15,10 +15,5 @@ describe('ConfirmationDialogDemoComponent', () => { expect(fixture.componentInstance).toBeTruthy() }) - it('should have no accessibility violations in its default state', async () => { - const fixture = TestBed.createComponent(ConfirmationDialogDemoComponent) - fixture.detectChanges() - const results = await runA11yChecks(fixture.nativeElement) - expectNoA11yViolations(results) - }) + verifyA11y(ConfirmationDialogDemoComponent) }) diff --git a/src/app/dashboard-item-demo/dashboard-item-demo.component.spec.ts b/src/app/dashboard-item-demo/dashboard-item-demo.component.spec.ts index f382ac1f..043b7f11 100644 --- a/src/app/dashboard-item-demo/dashboard-item-demo.component.spec.ts +++ b/src/app/dashboard-item-demo/dashboard-item-demo.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { provideTranslateService } from '@ngx-translate/core' -import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import { verifyA11y } from '@ppwcode/ng-unit-testing' import { DashboardItemDemoComponent } from './dashboard-item-demo.component' describe('DashboardItemDemoComponent', () => { @@ -22,8 +22,5 @@ describe('DashboardItemDemoComponent', () => { expect(component).toBeTruthy() }) - it('should have no accessibility violations in its default state', async () => { - const results = await runA11yChecks(fixture.nativeElement) - expectNoA11yViolations(results) - }) + verifyA11y(DashboardItemDemoComponent) }) diff --git a/src/app/editable-table/editable-table.component.spec.ts b/src/app/editable-table/editable-table.component.spec.ts index 925abfff..49adf118 100644 --- a/src/app/editable-table/editable-table.component.spec.ts +++ b/src/app/editable-table/editable-table.component.spec.ts @@ -1,7 +1,7 @@ import { TestBed } from '@angular/core/testing' import { NoopAnimationsModule } from '@angular/platform-browser/animations' import { provideTranslateService } from '@ngx-translate/core' -import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import { verifyA11y } from '@ppwcode/ng-unit-testing' import EditableTableComponent from './editable-table.component' describe('EditableTableComponent', () => { @@ -17,10 +17,5 @@ describe('EditableTableComponent', () => { expect(fixture.componentInstance).toBeTruthy() }) - it('should have no accessibility violations in its default state', async () => { - const fixture = TestBed.createComponent(EditableTableComponent) - fixture.detectChanges() - const results = await runA11yChecks(fixture.nativeElement) - expectNoA11yViolations(results) - }) + verifyA11y(EditableTableComponent) }) diff --git a/src/app/expandable-card/expandable-card-demo.component.spec.ts b/src/app/expandable-card/expandable-card-demo.component.spec.ts index d07f92fa..697cf4e4 100644 --- a/src/app/expandable-card/expandable-card-demo.component.spec.ts +++ b/src/app/expandable-card/expandable-card-demo.component.spec.ts @@ -1,7 +1,7 @@ import { TestBed } from '@angular/core/testing' import { NoopAnimationsModule } from '@angular/platform-browser/animations' import { provideTranslateService } from '@ngx-translate/core' -import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import { verifyA11y } from '@ppwcode/ng-unit-testing' import ExpandableCardDemoComponent from './expandable-card-demo.component' describe('ExpandableCardDemoComponent', () => { @@ -17,10 +17,5 @@ describe('ExpandableCardDemoComponent', () => { expect(fixture.componentInstance).toBeTruthy() }) - it('should have no accessibility violations in its default state', async () => { - const fixture = TestBed.createComponent(ExpandableCardDemoComponent) - fixture.detectChanges() - const results = await runA11yChecks(fixture.nativeElement) - expectNoA11yViolations(results) - }) + verifyA11y(ExpandableCardDemoComponent) }) diff --git a/src/app/global-error-handler/global-error-handler.component.spec.ts b/src/app/global-error-handler/global-error-handler.component.spec.ts index 0695bd50..ca819844 100644 --- a/src/app/global-error-handler/global-error-handler.component.spec.ts +++ b/src/app/global-error-handler/global-error-handler.component.spec.ts @@ -1,6 +1,6 @@ import { TestBed } from '@angular/core/testing' import { NoopAnimationsModule } from '@angular/platform-browser/animations' -import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import { verifyA11y } from '@ppwcode/ng-unit-testing' import { GlobalErrorHandlerComponent } from './global-error-handler.component' describe('GlobalErrorHandlerComponent', () => { @@ -15,10 +15,5 @@ describe('GlobalErrorHandlerComponent', () => { expect(fixture.componentInstance).toBeTruthy() }) - it('should have no accessibility violations in its default state', async () => { - const fixture = TestBed.createComponent(GlobalErrorHandlerComponent) - fixture.detectChanges() - const results = await runA11yChecks(fixture.nativeElement) - expectNoA11yViolations(results) - }) + verifyA11y(GlobalErrorHandlerComponent) }) diff --git a/src/app/language-select/language-select.component.spec.ts b/src/app/language-select/language-select.component.spec.ts index 4e787212..9d66e7aa 100644 --- a/src/app/language-select/language-select.component.spec.ts +++ b/src/app/language-select/language-select.component.spec.ts @@ -1,7 +1,7 @@ import { TestBed } from '@angular/core/testing' import { NoopAnimationsModule } from '@angular/platform-browser/animations' import { provideTranslateService } from '@ngx-translate/core' -import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import { verifyA11y } from '@ppwcode/ng-unit-testing' import LanguageSelectComponent from './language-select.component' describe('LanguageSelectComponent', () => { @@ -17,10 +17,5 @@ describe('LanguageSelectComponent', () => { expect(fixture.componentInstance).toBeTruthy() }) - it('should have no accessibility violations in its default state', async () => { - const fixture = TestBed.createComponent(LanguageSelectComponent) - fixture.detectChanges() - const results = await runA11yChecks(fixture.nativeElement) - expectNoA11yViolations(results) - }) + verifyA11y(LanguageSelectComponent) }) diff --git a/src/app/logging/in-memory-logging-demo/in-memory-logging-demo.component.spec.ts b/src/app/logging/in-memory-logging-demo/in-memory-logging-demo.component.spec.ts index 47dcfe34..a0fc2638 100644 --- a/src/app/logging/in-memory-logging-demo/in-memory-logging-demo.component.spec.ts +++ b/src/app/logging/in-memory-logging-demo/in-memory-logging-demo.component.spec.ts @@ -1,7 +1,7 @@ import { TestBed } from '@angular/core/testing' import { NoopAnimationsModule } from '@angular/platform-browser/animations' import { provideTranslateService } from '@ngx-translate/core' -import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import { verifyA11y } from '@ppwcode/ng-unit-testing' import { InMemoryLoggingDemoComponent } from './in-memory-logging-demo.component' describe('InMemoryLoggingDemoComponent', () => { @@ -17,10 +17,5 @@ describe('InMemoryLoggingDemoComponent', () => { expect(fixture.componentInstance).toBeTruthy() }) - it('should have no accessibility violations in its default state', async () => { - const fixture = TestBed.createComponent(InMemoryLoggingDemoComponent) - fixture.detectChanges() - const results = await runA11yChecks(fixture.nativeElement) - expectNoA11yViolations(results) - }) + verifyA11y(InMemoryLoggingDemoComponent) }) diff --git a/src/app/message-bar/message-bar.component.spec.ts b/src/app/message-bar/message-bar.component.spec.ts index 01e5556a..bc3f7287 100644 --- a/src/app/message-bar/message-bar.component.spec.ts +++ b/src/app/message-bar/message-bar.component.spec.ts @@ -1,6 +1,6 @@ import { TestBed } from '@angular/core/testing' import { NoopAnimationsModule } from '@angular/platform-browser/animations' -import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import { verifyA11y } from '@ppwcode/ng-unit-testing' import MessageBarComponent from './message-bar.component' describe('MessageBarDemoComponent', () => { @@ -15,10 +15,5 @@ describe('MessageBarDemoComponent', () => { expect(fixture.componentInstance).toBeTruthy() }) - it('should have no accessibility violations in its default state', async () => { - const fixture = TestBed.createComponent(MessageBarComponent) - fixture.detectChanges() - const results = await runA11yChecks(fixture.nativeElement) - expectNoA11yViolations(results) - }) + verifyA11y(MessageBarComponent) }) diff --git a/src/app/table/empty-async-result.component.spec.ts b/src/app/table/empty-async-result.component.spec.ts index 834f349c..5a1db9ad 100644 --- a/src/app/table/empty-async-result.component.spec.ts +++ b/src/app/table/empty-async-result.component.spec.ts @@ -1,6 +1,6 @@ import { TestBed } from '@angular/core/testing' import { NoopAnimationsModule } from '@angular/platform-browser/animations' -import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import { verifyA11y } from '@ppwcode/ng-unit-testing' import { EmptyAsyncResultComponent } from './empty-async-result.component' describe('EmptyAsyncResultComponent', () => { @@ -15,10 +15,5 @@ describe('EmptyAsyncResultComponent', () => { expect(fixture.componentInstance).toBeTruthy() }) - it('should have no accessibility violations in its default state', async () => { - const fixture = TestBed.createComponent(EmptyAsyncResultComponent) - fixture.detectChanges() - const results = await runA11yChecks(fixture.nativeElement) - expectNoA11yViolations(results) - }) + verifyA11y(EmptyAsyncResultComponent) }) diff --git a/src/app/table/empty-page.component.spec.ts b/src/app/table/empty-page.component.spec.ts index 8a59e234..d37164a2 100644 --- a/src/app/table/empty-page.component.spec.ts +++ b/src/app/table/empty-page.component.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from '@angular/core/testing' -import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import { verifyA11y } from '@ppwcode/ng-unit-testing' import { EmptyTablePageComponent } from './empty-page.component' describe('EmptyTablePageComponent', () => { @@ -14,10 +14,5 @@ describe('EmptyTablePageComponent', () => { expect(fixture.componentInstance).toBeTruthy() }) - it('should have no accessibility violations in its default state', async () => { - const fixture = TestBed.createComponent(EmptyTablePageComponent) - fixture.detectChanges() - const results = await runA11yChecks(fixture.nativeElement) - expectNoA11yViolations(results) - }) + verifyA11y(EmptyTablePageComponent) }) diff --git a/src/app/table/expandable-table-demo/expandable-table-demo.component.spec.ts b/src/app/table/expandable-table-demo/expandable-table-demo.component.spec.ts index 9f94d683..ffd90796 100644 --- a/src/app/table/expandable-table-demo/expandable-table-demo.component.spec.ts +++ b/src/app/table/expandable-table-demo/expandable-table-demo.component.spec.ts @@ -1,7 +1,7 @@ import { TestBed } from '@angular/core/testing' import { NoopAnimationsModule } from '@angular/platform-browser/animations' import { provideTranslateService } from '@ngx-translate/core' -import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import { verifyA11y } from '@ppwcode/ng-unit-testing' import { ExpandableTableDemoComponent } from './expandable-table-demo.component' describe('ExpandableTableDemoComponent', () => { @@ -17,10 +17,5 @@ describe('ExpandableTableDemoComponent', () => { expect(fixture.componentInstance).toBeTruthy() }) - it('should have no accessibility violations in its default state', async () => { - const fixture = TestBed.createComponent(ExpandableTableDemoComponent) - fixture.detectChanges() - const results = await runA11yChecks(fixture.nativeElement) - expectNoA11yViolations(results) - }) + verifyA11y(ExpandableTableDemoComponent) }) diff --git a/src/app/table/table-demo.component.spec.ts b/src/app/table/table-demo.component.spec.ts index 34e5d6d0..0df0dba8 100644 --- a/src/app/table/table-demo.component.spec.ts +++ b/src/app/table/table-demo.component.spec.ts @@ -3,7 +3,7 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations' import { RouterTestingModule } from '@angular/router/testing' import { provideTranslateService } from '@ngx-translate/core' import { PPW_TABLE_DEFAULT_OPTIONS } from '@ppwcode/ng-common-components' -import { expectNoA11yViolations, runA11yChecks } from '@ppwcode/ng-unit-testing' +import { verifyA11y } from '@ppwcode/ng-unit-testing' import TableDemoComponent from './table-demo.component' describe('TableDemoComponent', () => { @@ -22,10 +22,5 @@ describe('TableDemoComponent', () => { expect(fixture.componentInstance).toBeTruthy() }) - it('should have no accessibility violations in its default state', async () => { - const fixture = TestBed.createComponent(TableDemoComponent) - fixture.detectChanges() - const results = await runA11yChecks(fixture.nativeElement) - expectNoA11yViolations(results) - }) + verifyA11y(TableDemoComponent) }) From 665442b1e70fd225572e9f25ab79cdf2cb510dc7 Mon Sep 17 00:00:00 2001 From: David De Kok Date: Fri, 24 Apr 2026 17:21:04 +0200 Subject: [PATCH 6/9] Set up `ng-e2e-testing` library with accessibility testing utilities using `axe-core` and Playwright --- angular.json | 239 ++++++++++-------- package-lock.json | 78 ++++++ package.json | 2 + .../ppwcode/ng-e2e-testing/.eslintrc.json | 44 ++++ .../ppwcode/ng-e2e-testing/ng-package.json | 7 + projects/ppwcode/ng-e2e-testing/package.json | 16 ++ .../ng-e2e-testing/src/lib/a11y/axe.ts | 45 ++++ .../ppwcode/ng-e2e-testing/src/public-api.ts | 5 + .../ppwcode/ng-e2e-testing/tsconfig.lib.json | 12 + .../ng-e2e-testing/tsconfig.lib.prod.json | 10 + 10 files changed, 350 insertions(+), 108 deletions(-) create mode 100644 projects/ppwcode/ng-e2e-testing/.eslintrc.json create mode 100644 projects/ppwcode/ng-e2e-testing/ng-package.json create mode 100644 projects/ppwcode/ng-e2e-testing/package.json create mode 100644 projects/ppwcode/ng-e2e-testing/src/lib/a11y/axe.ts create mode 100644 projects/ppwcode/ng-e2e-testing/src/public-api.ts create mode 100644 projects/ppwcode/ng-e2e-testing/tsconfig.lib.json create mode 100644 projects/ppwcode/ng-e2e-testing/tsconfig.lib.prod.json diff --git a/angular.json b/angular.json index c0f6e4e7..69776fd5 100644 --- a/angular.json +++ b/angular.json @@ -28,10 +28,10 @@ "src/assets" ], "styles": [ - "@angular/material/prebuilt-themes/purple-green.css", - "node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css", - "node_modules/@fortawesome/fontawesome-free/css/all.min.css", - "src/styles.scss" + "@angular/material/prebuilt-themes/purple-green.css", + "node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css", + "node_modules/@fortawesome/fontawesome-free/css/all.min.css", + "src/styles.scss" ], "scripts": [] }, @@ -64,10 +64,10 @@ "builder": "@angular/build:dev-server", "configurations": { "production": { - "buildTarget": "ppwcode:build:production" + "buildTarget": "ppwcode:build:production" }, "development": { - "buildTarget": "ppwcode:build:development" + "buildTarget": "ppwcode:build:development" } }, "defaultConfiguration": "development" @@ -75,7 +75,7 @@ "extract-i18n": { "builder": "@angular/build:extract-i18n", "options": { - "buildTarget": "ppwcode:build" + "buildTarget": "ppwcode:build" } }, "test": { @@ -409,124 +409,147 @@ } }, "@ppwcode/ng-common-components": { - "projectType": "library", - "root": "projects/ppwcode/ng-common-components", - "sourceRoot": "projects/ppwcode/ng-common-components/src", - "prefix": "lib", - "architect": { - "build": { - "builder": "@angular/build:ng-packagr", - "options": { - "project": "projects/ppwcode/ng-common-components/ng-package.json" - }, - "configurations": { - "production": { - "tsConfig": "projects/ppwcode/ng-common-components/tsconfig.lib.prod.json" + "projectType": "library", + "root": "projects/ppwcode/ng-common-components", + "sourceRoot": "projects/ppwcode/ng-common-components/src", + "prefix": "lib", + "architect": { + "build": { + "builder": "@angular/build:ng-packagr", + "options": { + "project": "projects/ppwcode/ng-common-components/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "projects/ppwcode/ng-common-components/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "projects/ppwcode/ng-common-components/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" }, - "development": { - "tsConfig": "projects/ppwcode/ng-common-components/tsconfig.lib.json" + "test": { + "builder": "@angular/build:unit-test", + "options": { + "runnerConfig": "vitest.config.ts", + "tsConfig": "projects/ppwcode/ng-common-components/tsconfig.spec.json" + } } - }, - "defaultConfiguration": "production" - }, - "test": { - "builder": "@angular/build:unit-test", - "options": { - "runnerConfig": "vitest.config.ts", - "tsConfig": "projects/ppwcode/ng-common-components/tsconfig.spec.json" - } } - } }, "@ppwcode/ng-unit-testing": { - "projectType": "library", - "root": "projects/ppwcode/ng-unit-testing", - "sourceRoot": "projects/ppwcode/ng-unit-testing/src", - "prefix": "lib", - "architect": { - "build": { - "builder": "@angular/build:ng-packagr", - "options": { - "project": "projects/ppwcode/ng-unit-testing/ng-package.json" - }, - "configurations": { - "production": { - "tsConfig": "projects/ppwcode/ng-unit-testing/tsconfig.lib.prod.json" + "projectType": "library", + "root": "projects/ppwcode/ng-unit-testing", + "sourceRoot": "projects/ppwcode/ng-unit-testing/src", + "prefix": "lib", + "architect": { + "build": { + "builder": "@angular/build:ng-packagr", + "options": { + "project": "projects/ppwcode/ng-unit-testing/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "projects/ppwcode/ng-unit-testing/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "projects/ppwcode/ng-unit-testing/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" }, - "development": { - "tsConfig": "projects/ppwcode/ng-unit-testing/tsconfig.lib.json" + "test": { + "builder": "@angular/build:unit-test", + "options": { + "runnerConfig": "vitest.config.ts", + "tsConfig": "projects/ppwcode/ng-unit-testing/tsconfig.spec.json" + } } - }, - "defaultConfiguration": "production" - }, - "test": { - "builder": "@angular/build:unit-test", - "options": { - "runnerConfig": "vitest.config.ts", - "tsConfig": "projects/ppwcode/ng-unit-testing/tsconfig.spec.json" - } } - } }, "@ppwcode/ng-sdk": { - "projectType": "library", - "root": "projects/ppwcode/ng-sdk", - "sourceRoot": "projects/ppwcode/ng-sdk/src", - "prefix": "lib", - "architect": { - "build": { - "builder": "@angular/build:ng-packagr", - "configurations": { - "production": { - "tsConfig": "projects/ppwcode/ng-sdk/tsconfig.lib.prod.json" + "projectType": "library", + "root": "projects/ppwcode/ng-sdk", + "sourceRoot": "projects/ppwcode/ng-sdk/src", + "prefix": "lib", + "architect": { + "build": { + "builder": "@angular/build:ng-packagr", + "configurations": { + "production": { + "tsConfig": "projects/ppwcode/ng-sdk/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "projects/ppwcode/ng-sdk/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" }, - "development": { - "tsConfig": "projects/ppwcode/ng-sdk/tsconfig.lib.json" + "test": { + "builder": "@angular/build:unit-test", + "options": { + "tsConfig": "projects/ppwcode/ng-sdk/tsconfig.spec.json" + } + } + } + }, + "@ppwcode/ng-e2e-testing": { + "projectType": "library", + "root": "projects/ppwcode/ng-e2e-testing", + "sourceRoot": "projects/ppwcode/ng-e2e-testing/src", + "prefix": "lib", + "architect": { + "build": { + "builder": "@angular/build:ng-packagr", + "options": { + "project": "projects/ppwcode/ng-e2e-testing/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "projects/ppwcode/ng-e2e-testing/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "projects/ppwcode/ng-e2e-testing/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" } - }, - "defaultConfiguration": "production" - }, - "test": { - "builder": "@angular/build:unit-test", - "options": { - "tsConfig": "projects/ppwcode/ng-sdk/tsconfig.spec.json" - } } - } } }, "cli": { - "analytics": false + "analytics": false }, "schematics": { - "@schematics/angular:component": { - "type": "component" - }, - "@schematics/angular:directive": { - "type": "directive" - }, - "@schematics/angular:service": { - "type": "service" - }, - "@schematics/angular:guard": { - "typeSeparator": "." - }, - "@schematics/angular:interceptor": { - "typeSeparator": "." - }, - "@schematics/angular:module": { - "typeSeparator": "." - }, - "@schematics/angular:pipe": { - "typeSeparator": "." - }, - "@schematics/angular:resolver": { - "typeSeparator": "." - }, - "@schematics/angular": { - "component": { - "changeDetection": "OnPush" + "@schematics/angular:component": { + "type": "component" + }, + "@schematics/angular:directive": { + "type": "directive" + }, + "@schematics/angular:service": { + "type": "service" + }, + "@schematics/angular:guard": { + "typeSeparator": "." + }, + "@schematics/angular:interceptor": { + "typeSeparator": "." + }, + "@schematics/angular:module": { + "typeSeparator": "." + }, + "@schematics/angular:pipe": { + "typeSeparator": "." + }, + "@schematics/angular:resolver": { + "typeSeparator": "." + }, + "@schematics/angular": { + "component": { + "changeDetection": "OnPush" + } } - } } -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 33696767..1f2bde5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,8 @@ "@angular/build": "22.0.1", "@angular/cli": "22.0.1", "@angular/compiler-cli": "22.0.1", + "@axe-core/playwright": "4.12.1", + "@playwright/test": "1.61.1", "@schematics/angular": "21.2.3", "@types/file-saver-es": "2.0.1", "@types/node": "22.13.14", @@ -1772,6 +1774,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@axe-core/playwright": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.12.1.tgz", + "integrity": "sha512-rMd7xriptqKpP+w5265i4Hdkv2X5kbu6uiBi/B2I7uf3hieRBM3qDCfaKPtxfiYb2mKXfF+yLODJwIx+Jv1GDw==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "axe-core": "~4.12.1" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -4517,6 +4532,22 @@ "license": "MIT", "optional": true }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rollup/plugin-json": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", @@ -10789,6 +10820,53 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", diff --git a/package.json b/package.json index 3d55f05a..dc515af4 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,8 @@ "@angular/build": "22.0.1", "@angular/cli": "22.0.1", "@angular/compiler-cli": "22.0.1", + "@axe-core/playwright": "4.12.1", + "@playwright/test": "1.61.1", "@schematics/angular": "21.2.3", "@types/file-saver-es": "2.0.1", "@types/node": "22.13.14", diff --git a/projects/ppwcode/ng-e2e-testing/.eslintrc.json b/projects/ppwcode/ng-e2e-testing/.eslintrc.json new file mode 100644 index 00000000..00c0e584 --- /dev/null +++ b/projects/ppwcode/ng-e2e-testing/.eslintrc.json @@ -0,0 +1,44 @@ +{ + "extends": "../../../.eslintrc.json", + "ignorePatterns": ["!**/*"], + "rules": { + "no-restricted-imports": [ + "error", + { + "paths": [ + { + "name": "@ppwcode/ng-e2e-testing", + "message": "Use a relative import instead. Importing from @ppwcode/ng-e2e-testing is not allowed inside this lib directory." + } + ] + } + ] + }, + "overrides": [ + { + "files": ["*.ts"], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "ppw", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "ppw", + "style": "kebab-case" + } + ] + } + }, + { + "files": ["*.html"], + "rules": {} + } + ] +} diff --git a/projects/ppwcode/ng-e2e-testing/ng-package.json b/projects/ppwcode/ng-e2e-testing/ng-package.json new file mode 100644 index 00000000..cb06d75c --- /dev/null +++ b/projects/ppwcode/ng-e2e-testing/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/ppwcode/ng-e2e-testing", + "lib": { + "entryFile": "src/public-api.ts" + } +} diff --git a/projects/ppwcode/ng-e2e-testing/package.json b/projects/ppwcode/ng-e2e-testing/package.json new file mode 100644 index 00000000..d38a4f64 --- /dev/null +++ b/projects/ppwcode/ng-e2e-testing/package.json @@ -0,0 +1,16 @@ +{ + "name": "@ppwcode/ng-e2e-testing", + "version": "0.0.1", + "repository": { + "url": "https://github.com/peopleware/angular-sdk" + }, + "peerDependencies": { + "@axe-core/playwright": "^4.0.0", + "@playwright/test": "^1.0.0", + "axe-core": "^4.0.0" + }, + "dependencies": { + "tslib": "^2.3.0" + }, + "sideEffects": false +} diff --git a/projects/ppwcode/ng-e2e-testing/src/lib/a11y/axe.ts b/projects/ppwcode/ng-e2e-testing/src/lib/a11y/axe.ts new file mode 100644 index 00000000..0ae6b5b9 --- /dev/null +++ b/projects/ppwcode/ng-e2e-testing/src/lib/a11y/axe.ts @@ -0,0 +1,45 @@ +import AxeBuilder from '@axe-core/playwright' +import type { Page } from '@playwright/test' +import type { AxeResults } from 'axe-core' + +/** + * The WCAG 2.0 A/AA, WCAG 2.1 A/AA and WCAG 2.2 A/AA rule tags — the accessibility compliance + * target for this application. + */ +export const WCAG_2_2_AA_TAGS: readonly string[] = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa'] + +/** + * Runs axe-core against the given Playwright page with WCAG 2.2 AA rules. + * Mirrors the unit-test helper so both layers stay aligned on what "accessible" + * means for this project. + * + * Usage in e2e specs: + * const results = await analyzePage(page) + * expectNoViolations(results) + */ +export function analyzePage(page: Page): Promise { + return new AxeBuilder({ page }).withTags([...WCAG_2_2_AA_TAGS]).analyze() +} + +/** + * Fails the current test with a formatted list of accessibility violations + * when `results.violations` is non-empty. + */ +export function expectNoViolations(results: AxeResults): void { + if (results.violations.length === 0) { + return + } + + const details = results.violations + .map((violation) => { + const nodeList = violation.nodes + .map((node) => ` - ${node.target.join(', ')}\n ${node.failureSummary ?? ''}`) + .join('\n') + return ` • [${violation.id}] ${violation.help} (${violation.impact ?? 'n/a'})\n ${ + violation.helpUrl + }\n${nodeList}` + }) + .join('\n\n') + + throw new Error(`Expected no accessibility violations but found ${results.violations.length}:\n\n${details}`) +} diff --git a/projects/ppwcode/ng-e2e-testing/src/public-api.ts b/projects/ppwcode/ng-e2e-testing/src/public-api.ts new file mode 100644 index 00000000..d7f8aafe --- /dev/null +++ b/projects/ppwcode/ng-e2e-testing/src/public-api.ts @@ -0,0 +1,5 @@ +/* + * Public API Surface of ng-e2e-testing + */ + +export * from './lib/a11y/axe' diff --git a/projects/ppwcode/ng-e2e-testing/tsconfig.lib.json b/projects/ppwcode/ng-e2e-testing/tsconfig.lib.json new file mode 100644 index 00000000..bf1e26bd --- /dev/null +++ b/projects/ppwcode/ng-e2e-testing/tsconfig.lib.json @@ -0,0 +1,12 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../../tsconfig.angular.json", + "compilerOptions": { + "outDir": "../../../out-tsc/lib", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": ["node"] + }, + "exclude": ["**/*.spec.ts"] +} diff --git a/projects/ppwcode/ng-e2e-testing/tsconfig.lib.prod.json b/projects/ppwcode/ng-e2e-testing/tsconfig.lib.prod.json new file mode 100644 index 00000000..560c2391 --- /dev/null +++ b/projects/ppwcode/ng-e2e-testing/tsconfig.lib.prod.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "partial" + } +} From 5df2d45bcb7870ad20cb5f9cb39707d9241e64ae Mon Sep 17 00:00:00 2001 From: David De Kok Date: Mon, 6 Jul 2026 11:55:27 +0200 Subject: [PATCH 7/9] Add end-to-end testing setup with Playwright and accessibility checks --- package.json | 2 + playwright.config.ts | 23 ++++ .../ng-e2e-testing/src/lib/a11y/axe.ts | 34 ++++- scripts/ci/test-e2e.js | 118 ++++++++++++++++++ .../components-dashboard-demo.e2e-spec.ts | 8 ++ .../confirmation-dialog-demo.e2e-spec.ts | 8 ++ .../dashboard-item-demo.e2e-spec.ts | 8 ++ .../editable-table/editable-table.e2e-spec.ts | 8 ++ .../expandable-card-demo.e2e-spec.ts | 8 ++ .../global-error-handler.e2e-spec.ts | 8 ++ .../in-memory-logging-demo.e2e-spec.ts | 8 ++ src/app/message-bar/message-bar.e2e-spec.ts | 8 ++ src/app/table/table-demo.e2e-spec.ts | 8 ++ tsconfig.e2e.json | 9 ++ tsconfig.json | 1 + 15 files changed, 256 insertions(+), 3 deletions(-) create mode 100644 playwright.config.ts create mode 100644 scripts/ci/test-e2e.js create mode 100644 src/app/components-dashboard-demo/components-dashboard-demo.e2e-spec.ts create mode 100644 src/app/confirmation-dialog-demo/confirmation-dialog-demo.e2e-spec.ts create mode 100644 src/app/dashboard-item-demo/dashboard-item-demo.e2e-spec.ts create mode 100644 src/app/editable-table/editable-table.e2e-spec.ts create mode 100644 src/app/expandable-card/expandable-card-demo.e2e-spec.ts create mode 100644 src/app/global-error-handler/global-error-handler.e2e-spec.ts create mode 100644 src/app/logging/in-memory-logging-demo/in-memory-logging-demo.e2e-spec.ts create mode 100644 src/app/message-bar/message-bar.e2e-spec.ts create mode 100644 src/app/table/table-demo.e2e-spec.ts create mode 100644 tsconfig.e2e.json diff --git a/package.json b/package.json index dc515af4..da83720a 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "lint:lint": "ng lint", "format:lint": "ng lint --fix", "ci:test-libs": "node ./scripts/ci/test-libs.js", + "ci:test-e2e": "node ./scripts/ci/test-e2e.js", + "e2e:ui": "node ./scripts/ci/test-e2e.js --ui", "ci:lint": "npm run lint:prettier && npm run lint:lint", "ci:build": "ng build ppwcode --base-href /angular-sdk/ --deploy-url /angular-sdk/", "ci:build-libs": "./scripts/ci/build-libs.sh", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..11d2fbb0 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,23 @@ +/// + +import { defineConfig, devices } from '@playwright/test' + +export default defineConfig({ + testDir: './src/app', + testMatch: '**/*.e2e-spec.ts', + fullyParallel: true, + forbidOnly: !!process.env['CI'], + retries: process.env['CI'] ? 2 : 0, + workers: process.env['CI'] ? 1 : undefined, + reporter: process.env['CI'] ? 'github' : 'list', + use: { + baseURL: 'http://127.0.0.1:4200/angular-sdk/', + trace: 'retain-on-failure' + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] } + } + ] +}) diff --git a/projects/ppwcode/ng-e2e-testing/src/lib/a11y/axe.ts b/projects/ppwcode/ng-e2e-testing/src/lib/a11y/axe.ts index 0ae6b5b9..998a2b6a 100644 --- a/projects/ppwcode/ng-e2e-testing/src/lib/a11y/axe.ts +++ b/projects/ppwcode/ng-e2e-testing/src/lib/a11y/axe.ts @@ -1,5 +1,6 @@ import AxeBuilder from '@axe-core/playwright' -import type { Page } from '@playwright/test' +import { expect, test } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' import type { AxeResults } from 'axe-core' /** @@ -17,8 +18,14 @@ export const WCAG_2_2_AA_TAGS: readonly string[] = ['wcag2a', 'wcag2aa', 'wcag21 * const results = await analyzePage(page) * expectNoViolations(results) */ -export function analyzePage(page: Page): Promise { - return new AxeBuilder({ page }).withTags([...WCAG_2_2_AA_TAGS]).analyze() +export function analyzePage(page: Page, options: { includeSelector?: string } = {}): Promise { + let builder = new AxeBuilder({ page }).withTags([...WCAG_2_2_AA_TAGS]) + + if (options.includeSelector) { + builder = builder.include(options.includeSelector) + } + + return builder.analyze() } /** @@ -43,3 +50,24 @@ export function expectNoViolations(results: AxeResults): void { throw new Error(`Expected no accessibility violations but found ${results.violations.length}:\n\n${details}`) } + +/** + * Defines a Playwright test that opens a route, waits until the page is ready, + * and fails when axe reports WCAG 2.2 AA accessibility violations. + */ +export function verifyE2eA11y(options: { + name?: string + path: string + includeSelector: string + readyLocator: (page: Page) => Locator +}): void { + test(options.name ?? `${options.path} has no WCAG 2.2 AA violations`, async ({ page }) => { + await page.goto(options.path) + + await expect(options.readyLocator(page)).toBeVisible() + + const results = await analyzePage(page, { includeSelector: options.includeSelector }) + + expectNoViolations(results) + }) +} diff --git a/scripts/ci/test-e2e.js b/scripts/ci/test-e2e.js new file mode 100644 index 00000000..1d7d06b0 --- /dev/null +++ b/scripts/ci/test-e2e.js @@ -0,0 +1,118 @@ +const { spawn } = require('child_process') +const http = require('http') +const path = require('path') + +const isWindows = process.platform === 'win32' +const serverUrl = 'http://127.0.0.1:4200/angular-sdk/' +const commandExtension = isWindows ? '.cmd' : '' +const nodeBinPath = path.join(__dirname, '..', '..', 'node_modules', '.bin') + +const ngCommand = path.join(nodeBinPath, `ng${commandExtension}`) +const playwrightCommand = path.join(nodeBinPath, `playwright${commandExtension}`) +const playwrightArgs = ['test', ...process.argv.slice(2)] + +const wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)) +const quoteCommandPart = (commandPart) => `"${commandPart.replaceAll('"', '\\"')}"` + +const getSpawnOptions = (command, args) => { + if (!isWindows) { + return { command, args, shell: false } + } + + return { + command: [command, ...args].map(quoteCommandPart).join(' '), + args: [], + shell: true + } +} + +const isServerReady = () => + new Promise((resolve) => { + const request = http.get(serverUrl, (response) => { + response.resume() + resolve(response.statusCode >= 200 && response.statusCode < 500) + }) + + request.on('error', () => resolve(false)) + request.setTimeout(1000, () => { + request.destroy() + resolve(false) + }) + }) + +const waitForServer = async () => { + const deadline = Date.now() + 120_000 + + while (Date.now() < deadline) { + if (await isServerReady()) { + return + } + + await wait(1000) + } + + throw new Error(`Timed out waiting for ${serverUrl}`) +} + +const runCommand = (command, args, options = {}) => + new Promise((resolve) => { + const spawnOptions = getSpawnOptions(command, args) + const childProcess = spawn(spawnOptions.command, spawnOptions.args, { + stdio: 'inherit', + shell: spawnOptions.shell, + ...options + }) + + childProcess.on('exit', (code) => resolve(code ?? 1)) + childProcess.on('error', (error) => { + console.error(error) + resolve(1) + }) + }) + +const killProcessTree = (processId) => + new Promise((resolve) => { + if (isWindows) { + const taskkill = spawn('taskkill', ['/pid', processId.toString(), '/T', '/F'], { + stdio: 'ignore' + }) + taskkill.on('exit', resolve) + taskkill.on('error', resolve) + return + } + + try { + process.kill(-processId, 'SIGTERM') + } catch { + try { + process.kill(processId, 'SIGTERM') + } catch { + // The process has already exited. + } + } + + resolve() + }) + +const run = async () => { + const serverSpawnOptions = getSpawnOptions(ngCommand, ['serve', '--host', '127.0.0.1', '--port', '4200']) + const serverProcess = spawn(serverSpawnOptions.command, serverSpawnOptions.args, { + detached: !isWindows, + shell: serverSpawnOptions.shell, + stdio: 'inherit' + }) + + try { + await waitForServer() + process.exitCode = await runCommand(playwrightCommand, playwrightArgs) + } finally { + await killProcessTree(serverProcess.pid) + } +} + +run() + .then(() => process.exit(process.exitCode ?? 0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/src/app/components-dashboard-demo/components-dashboard-demo.e2e-spec.ts b/src/app/components-dashboard-demo/components-dashboard-demo.e2e-spec.ts new file mode 100644 index 00000000..591f192f --- /dev/null +++ b/src/app/components-dashboard-demo/components-dashboard-demo.e2e-spec.ts @@ -0,0 +1,8 @@ +import { verifyE2eA11y } from '@ppwcode/ng-e2e-testing' + +verifyE2eA11y({ + name: 'components dashboard demo has no WCAG 2.2 AA violations', + path: 'components', + includeSelector: 'ppw-components-dashboard-demo', + readyLocator: (page) => page.getByText('Confirmation Dialog') +}) diff --git a/src/app/confirmation-dialog-demo/confirmation-dialog-demo.e2e-spec.ts b/src/app/confirmation-dialog-demo/confirmation-dialog-demo.e2e-spec.ts new file mode 100644 index 00000000..fea1d3ea --- /dev/null +++ b/src/app/confirmation-dialog-demo/confirmation-dialog-demo.e2e-spec.ts @@ -0,0 +1,8 @@ +import { verifyE2eA11y } from '@ppwcode/ng-e2e-testing' + +verifyE2eA11y({ + name: 'confirmation dialog demo has no WCAG 2.2 AA violations', + path: 'components/confirmation-dialog', + includeSelector: 'ppw-confirmation-dialog-demo', + readyLocator: (page) => page.getByRole('button', { name: 'Request confirmation' }) +}) diff --git a/src/app/dashboard-item-demo/dashboard-item-demo.e2e-spec.ts b/src/app/dashboard-item-demo/dashboard-item-demo.e2e-spec.ts new file mode 100644 index 00000000..01c1c262 --- /dev/null +++ b/src/app/dashboard-item-demo/dashboard-item-demo.e2e-spec.ts @@ -0,0 +1,8 @@ +import { verifyE2eA11y } from '@ppwcode/ng-e2e-testing' + +verifyE2eA11y({ + name: 'dashboard item demo has no WCAG 2.2 AA violations', + path: 'dashboard-item', + includeSelector: 'ppw-dashboard-item-demo', + readyLocator: (page) => page.locator('mat-card-title', { hasText: 'Components' }) +}) diff --git a/src/app/editable-table/editable-table.e2e-spec.ts b/src/app/editable-table/editable-table.e2e-spec.ts new file mode 100644 index 00000000..4487a0cc --- /dev/null +++ b/src/app/editable-table/editable-table.e2e-spec.ts @@ -0,0 +1,8 @@ +import { verifyE2eA11y } from '@ppwcode/ng-e2e-testing' + +verifyE2eA11y({ + name: 'form table demo has no WCAG 2.2 AA violations', + path: 'components/form-table', + includeSelector: 'ppw-editable-table', + readyLocator: (page) => page.getByText('Current form array value:') +}) diff --git a/src/app/expandable-card/expandable-card-demo.e2e-spec.ts b/src/app/expandable-card/expandable-card-demo.e2e-spec.ts new file mode 100644 index 00000000..5ab912a8 --- /dev/null +++ b/src/app/expandable-card/expandable-card-demo.e2e-spec.ts @@ -0,0 +1,8 @@ +import { verifyE2eA11y } from '@ppwcode/ng-e2e-testing' + +verifyE2eA11y({ + name: 'expandable card demo has no WCAG 2.2 AA violations', + path: 'components/expandable-card', + includeSelector: 'ppw-expandable-card-demo', + readyLocator: (page) => page.getByText('Demo card which cannot be collapsed') +}) diff --git a/src/app/global-error-handler/global-error-handler.e2e-spec.ts b/src/app/global-error-handler/global-error-handler.e2e-spec.ts new file mode 100644 index 00000000..328f5b9f --- /dev/null +++ b/src/app/global-error-handler/global-error-handler.e2e-spec.ts @@ -0,0 +1,8 @@ +import { verifyE2eA11y } from '@ppwcode/ng-e2e-testing' + +verifyE2eA11y({ + name: 'global error handler demo has no WCAG 2.2 AA violations', + path: 'global-error-handler', + includeSelector: 'ppw-global-error-handler', + readyLocator: (page) => page.getByRole('button', { name: 'Invoke error' }) +}) diff --git a/src/app/logging/in-memory-logging-demo/in-memory-logging-demo.e2e-spec.ts b/src/app/logging/in-memory-logging-demo/in-memory-logging-demo.e2e-spec.ts new file mode 100644 index 00000000..872df0a8 --- /dev/null +++ b/src/app/logging/in-memory-logging-demo/in-memory-logging-demo.e2e-spec.ts @@ -0,0 +1,8 @@ +import { verifyE2eA11y } from '@ppwcode/ng-e2e-testing' + +verifyE2eA11y({ + name: 'in-memory logging demo has no WCAG 2.2 AA violations', + path: 'in-memory-logging', + includeSelector: 'ppw-in-memory-logging-demo', + readyLocator: (page) => page.getByRole('button', { name: 'Add debug line' }) +}) diff --git a/src/app/message-bar/message-bar.e2e-spec.ts b/src/app/message-bar/message-bar.e2e-spec.ts new file mode 100644 index 00000000..d3ab71c3 --- /dev/null +++ b/src/app/message-bar/message-bar.e2e-spec.ts @@ -0,0 +1,8 @@ +import { verifyE2eA11y } from '@ppwcode/ng-e2e-testing' + +verifyE2eA11y({ + name: 'message bar demo has no WCAG 2.2 AA violations', + path: 'components/message-bar', + includeSelector: 'ppw-message-bar-demo', + readyLocator: (page) => page.getByRole('heading', { name: 'Message bar with input parameter message' }) +}) diff --git a/src/app/table/table-demo.e2e-spec.ts b/src/app/table/table-demo.e2e-spec.ts new file mode 100644 index 00000000..23dcbde4 --- /dev/null +++ b/src/app/table/table-demo.e2e-spec.ts @@ -0,0 +1,8 @@ +import { verifyE2eA11y } from '@ppwcode/ng-e2e-testing' + +verifyE2eA11y({ + name: 'table demo has no WCAG 2.2 AA violations', + path: 'components/table', + includeSelector: 'ppw-table-demo', + readyLocator: (page) => page.getByRole('heading', { name: 'Filter table' }) +}) diff --git a/tsconfig.e2e.json b/tsconfig.e2e.json new file mode 100644 index 00000000..9956a753 --- /dev/null +++ b/tsconfig.e2e.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "./", + "types": ["node"] + }, + "files": [], + "include": ["src/app/**/*.e2e-spec.ts", "playwright.config.ts"] +} diff --git a/tsconfig.json b/tsconfig.json index c1cbdafa..8fc0f29b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "@ppwcode/ng-common": ["./projects/ppwcode/ng-common/src/public-api"], "@ppwcode/ng-common-components": ["./projects/ppwcode/ng-common-components/src/public-api"], "@ppwcode/ng-dialogs": ["./projects/ppwcode/ng-dialogs/src/public-api"], + "@ppwcode/ng-e2e-testing": ["./projects/ppwcode/ng-e2e-testing/src/public-api"], "@ppwcode/ng-forms": ["./projects/ppwcode/ng-forms/src/public-api"], "@ppwcode/ng-router": ["./projects/ppwcode/ng-router/src/public-api"], "@ppwcode/ng-state-management": ["./projects/ppwcode/ng-state-management/src/public-api"], From ce06ed81c874f3f53434fe649b8fa65c99e96d54 Mon Sep 17 00:00:00 2001 From: David De Kok Date: Mon, 6 Jul 2026 11:57:41 +0200 Subject: [PATCH 8/9] Update CI workflow to include Playwright installation and end-to-end tests --- .github/workflows/ci.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index eafa448e..58e5cc77 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,3 +22,5 @@ jobs: - run: npm run ci:lint - run: npm run ci:build-libs - run: npm run ci:test-libs + - run: npx playwright install --with-deps chromium + - run: npm run ci:test-e2e From 67c301148a917defbcc666081326589666f21e60 Mon Sep 17 00:00:00 2001 From: David De Kok Date: Mon, 6 Jul 2026 13:43:32 +0200 Subject: [PATCH 9/9] Fix a11y issues - update primary color shades and adjust associated CSS variables --- src/styles.scss | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/styles.scss b/src/styles.scss index a679d286..254bea66 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -13,7 +13,7 @@ $ppw-primary-palette: ( 200: #80cd9f, 300: #4db978, 400: #26aa5b, - 500: #009b3e, + 500: #007f28, 600: #009338, 700: #008930, 800: #007f28, @@ -86,12 +86,13 @@ body { --ppw-confirmation-dialog-header-text-color: #{mat.m2-get-contrast-color-from-palette($ppw-primary-palette, 500)}; --ppw-expandable-card-header-text-color: white; - --ppw-expandable-card-header-background-color: #6eb343; + --ppw-expandable-card-header-background-color: #007f28; --ppw-expandable-card-header-indicator-color: white; --ppw-loader-content-padding: 16px; --ppw-message-bar-warning-text-color: black; + --ppw-message-bar-success-background-color: #007f28; --ppw-sidenav-background-color: #18428c; --ppw-sidenav-navigation-item-icon-color: #6eb343; @@ -107,7 +108,7 @@ body { --ppw-dashboard-items-table-width: 750px; --ppw-dashboard-items-table-primary-color: #18428c; - --ppw-dashboard-items-table-background-color: #6eb343; + --ppw-dashboard-items-table-background-color: #d9ead3; --ppw-dashboard-wrapper-container-vertical-margin: 32px; --ppw-dashboard-items-card-margin: 8px;