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 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 2bdeec31..1f2bde5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,12 +37,15 @@ "@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", "@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", @@ -1771,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", @@ -3767,9 +3783,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3787,9 +3800,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3807,9 +3817,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3827,9 +3834,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3847,9 +3851,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3867,9 +3868,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3887,9 +3885,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4348,9 +4343,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4372,9 +4364,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4396,9 +4385,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4420,9 +4406,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4444,9 +4427,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4468,9 +4448,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4555,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", @@ -4691,9 +4684,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4708,9 +4698,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4725,9 +4712,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4742,9 +4726,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4759,9 +4740,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4776,9 +4754,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4793,9 +4768,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4810,9 +4782,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4827,9 +4796,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4844,9 +4810,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4861,9 +4824,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4878,9 +4838,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4895,9 +4852,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5965,6 +5919,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", @@ -10856,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 e3ce9b85..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", @@ -49,12 +51,15 @@ "@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", "@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/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-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..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 6d6195a1..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,6 +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) + ? '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/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..998a2b6a --- /dev/null +++ b/projects/ppwcode/ng-e2e-testing/src/lib/a11y/axe.ts @@ -0,0 +1,73 @@ +import AxeBuilder from '@axe-core/playwright' +import { expect, test } from '@playwright/test' +import type { Locator, 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, 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() +} + +/** + * 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}`) +} + +/** + * 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/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" + } +} 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..ea22c7c5 --- /dev/null +++ b/projects/ppwcode/ng-unit-testing/src/lib/a11y/axe.ts @@ -0,0 +1,87 @@ +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 +// 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 + +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. + */ +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}`) +} + +/** + * 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/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' 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/app.component.spec.ts b/src/app/app.component.spec.ts index a4bb8bd1..aa40a7f1 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -6,6 +6,7 @@ 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 { AppComponent } from './app.component' import LanguageSelectComponent from './language-select/language-select.component' @@ -32,4 +33,6 @@ describe('AppComponent', () => { const app = fixture.componentInstance expect(app).toBeTruthy() }) + + verifyA11y(AppComponent) }) 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..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.spec.ts b/src/app/components-dashboard-demo/components-dashboard-demo.component.spec.ts new file mode 100644 index 00000000..5120d602 --- /dev/null +++ b/src/app/components-dashboard-demo/components-dashboard-demo.component.spec.ts @@ -0,0 +1,22 @@ +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 { verifyA11y } 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() + }) + + verifyA11y(ComponentsDashboardDemoComponent) +}) 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/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/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..432de5ba --- /dev/null +++ b/src/app/components-page-container/components-page-container.component.spec.ts @@ -0,0 +1,19 @@ +import { TestBed } from '@angular/core/testing' +import { RouterTestingModule } from '@angular/router/testing' +import { verifyA11y } 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() + }) + + 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 new file mode 100644 index 00000000..1660f478 --- /dev/null +++ b/src/app/confirmation-dialog-demo/confirmation-dialog-demo.component.spec.ts @@ -0,0 +1,19 @@ +import { TestBed } from '@angular/core/testing' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { verifyA11y } 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() + }) + + verifyA11y(ConfirmationDialogDemoComponent) +}) 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.component.spec.ts b/src/app/dashboard-item-demo/dashboard-item-demo.component.spec.ts index 28730aef..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 { verifyA11y } from '@ppwcode/ng-unit-testing' import { DashboardItemDemoComponent } from './dashboard-item-demo.component' describe('DashboardItemDemoComponent', () => { @@ -21,4 +21,6 @@ describe('DashboardItemDemoComponent', () => { it('should create', () => { expect(component).toBeTruthy() }) + + verifyA11y(DashboardItemDemoComponent) }) 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.component.html b/src/app/editable-table/editable-table.component.html index 4896519f..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.spec.ts b/src/app/editable-table/editable-table.component.spec.ts new file mode 100644 index 00000000..49adf118 --- /dev/null +++ b/src/app/editable-table/editable-table.component.spec.ts @@ -0,0 +1,21 @@ +import { TestBed } from '@angular/core/testing' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { provideTranslateService } from '@ngx-translate/core' +import { verifyA11y } 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() + }) + + verifyA11y(EditableTableComponent) +}) 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/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.component.html b/src/app/expandable-card/expandable-card-demo.component.html index 6da713c5..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 new file mode 100644 index 00000000..697cf4e4 --- /dev/null +++ b/src/app/expandable-card/expandable-card-demo.component.spec.ts @@ -0,0 +1,21 @@ +import { TestBed } from '@angular/core/testing' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { provideTranslateService } from '@ngx-translate/core' +import { verifyA11y } from '@ppwcode/ng-unit-testing' +import ExpandableCardDemoComponent from './expandable-card-demo.component' + +describe('ExpandableCardDemoComponent', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [ExpandableCardDemoComponent, NoopAnimationsModule], + providers: [provideTranslateService({})] + }) + ) + + it('should create', () => { + const fixture = TestBed.createComponent(ExpandableCardDemoComponent) + expect(fixture.componentInstance).toBeTruthy() + }) + + verifyA11y(ExpandableCardDemoComponent) +}) 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/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.component.spec.ts b/src/app/global-error-handler/global-error-handler.component.spec.ts new file mode 100644 index 00000000..ca819844 --- /dev/null +++ b/src/app/global-error-handler/global-error-handler.component.spec.ts @@ -0,0 +1,19 @@ +import { TestBed } from '@angular/core/testing' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { verifyA11y } 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() + }) + + verifyA11y(GlobalErrorHandlerComponent) +}) 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/language-select/language-select.component.html b/src/app/language-select/language-select.component.html index 97780f66..2006ac9f 100644 --- a/src/app/language-select/language-select.component.html +++ b/src/app/language-select/language-select.component.html @@ -1,6 +1,10 @@
- +
{{ selectedLanguage }} 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..9d66e7aa --- /dev/null +++ b/src/app/language-select/language-select.component.spec.ts @@ -0,0 +1,21 @@ +import { TestBed } from '@angular/core/testing' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { provideTranslateService } from '@ngx-translate/core' +import { verifyA11y } 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() + }) + + 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 new file mode 100644 index 00000000..a0fc2638 --- /dev/null +++ b/src/app/logging/in-memory-logging-demo/in-memory-logging-demo.component.spec.ts @@ -0,0 +1,21 @@ +import { TestBed } from '@angular/core/testing' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { provideTranslateService } from '@ngx-translate/core' +import { verifyA11y } 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() + }) + + verifyA11y(InMemoryLoggingDemoComponent) +}) 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.component.spec.ts b/src/app/message-bar/message-bar.component.spec.ts new file mode 100644 index 00000000..bc3f7287 --- /dev/null +++ b/src/app/message-bar/message-bar.component.spec.ts @@ -0,0 +1,19 @@ +import { TestBed } from '@angular/core/testing' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { verifyA11y } 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() + }) + + verifyA11y(MessageBarComponent) +}) 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/empty-async-result.component.spec.ts b/src/app/table/empty-async-result.component.spec.ts new file mode 100644 index 00000000..5a1db9ad --- /dev/null +++ b/src/app/table/empty-async-result.component.spec.ts @@ -0,0 +1,19 @@ +import { TestBed } from '@angular/core/testing' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { verifyA11y } 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() + }) + + verifyA11y(EmptyAsyncResultComponent) +}) 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..d37164a2 --- /dev/null +++ b/src/app/table/empty-page.component.spec.ts @@ -0,0 +1,18 @@ +import { TestBed } from '@angular/core/testing' +import { verifyA11y } 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() + }) + + 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 new file mode 100644 index 00000000..ffd90796 --- /dev/null +++ b/src/app/table/expandable-table-demo/expandable-table-demo.component.spec.ts @@ -0,0 +1,21 @@ +import { TestBed } from '@angular/core/testing' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { provideTranslateService } from '@ngx-translate/core' +import { verifyA11y } 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() + }) + + verifyA11y(ExpandableTableDemoComponent) +}) 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..0df0dba8 --- /dev/null +++ b/src/app/table/table-demo.component.spec.ts @@ -0,0 +1,26 @@ +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 { verifyA11y } 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() + }) + + verifyA11y(TableDemoComponent) +}) 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/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", 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; 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"],