diff --git a/.gitattributes b/.gitattributes index c7cdf5a36..53839e8dd 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ * text=auto docs/* linguist-documentation release/* linguist-vendored +playwright/snapshots/e2e/**/*.png filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index 661210046..f9ec40e6d 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -14,6 +14,9 @@ jobs: with: node-version: lts/iron + - name: Pull Repository with LFS + run: git lfs pull + - name: Install uses: bahmutov/npm-install@v1 @@ -41,6 +44,18 @@ jobs: env: CI: true + - name: E2E Prepare + run: | + npm run e2e:prepare --if-present + env: + CI: true + + - name: E2E + run: | + npm run e2e --if-present + env: + CI: true + - name: Build Docs run: | npm run build-docs --if-present diff --git a/.gitignore b/.gitignore index 878002f47..c8bcbc9ac 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,9 @@ coverage/ yarn.lock /.vscode *.tgz +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/results/ +.env diff --git a/.lfsconfig b/.lfsconfig new file mode 100644 index 000000000..57d2776cb --- /dev/null +++ b/.lfsconfig @@ -0,0 +1,3 @@ +[lfs] + url = https://github.com/siemens/ngx-datatable-snapshots.git/info/lfs + pushurl = https://github.com/siemens/ngx-datatable-snapshots.git/info/lfs diff --git a/e2e-local.sh b/e2e-local.sh new file mode 100644 index 000000000..6bd2131bf --- /dev/null +++ b/e2e-local.sh @@ -0,0 +1,105 @@ +#!/bin/sh + +cd $(dirname $0) + +# this is to stop MSys (Git bash on Windows) from messing with the paths +export MSYS_NO_PATHCONV=1 + +# Keep image version in sync with image used in .gitlab-ci.yml +PLAYWRIGHT_IMAGE="mcr.microsoft.com/playwright:v1.46.0-jammy" +OS=$(uname -s) +CWD=$(pwd) + +if [ x"$DOCKER" = "x" ]; then + DOCKER=docker +fi + +case "$DOCKER" in + *podman*) + BRIDGE_ADDRESS=host.containers.internal + ;; + *) + BRIDGE_ADDRESS=host.docker.internal + ;; +esac + +if command -v getenforce &> /dev/null && [ "$(getenforce)" = "Enforcing" ]; then + MOUNT_FLAGS=",Z" +fi + +LOCAL_ADDRESS=$BRIDGE_ADDRESS +NETWORK_MODE=bridge +DISPLAY=$LOCAL_ADDRESS:0 + +case "$OS" in + Linux*) + LOCAL_ADDRESS=localhost + NETWORK_MODE=host + DISPLAY=$DISPLAY + ;; + MINGW*) + CWD=$(cygpath -w $CWD) + ;; +esac + +if [ x$PORT = "x" ]; then + PORT=4200 +fi + +echo "Using '$DOCKER' in '$NETWORK_MODE' mode, connecting to '$LOCAL_ADDRESS:$PORT'" + + +if [ x$1 = "xshell" ]; then + shift + $DOCKER run -it --rm \ + -e DISPLAY=$DISPLAY \ + -e LOCAL_ADDRESS=$LOCAL_ADDRESS \ + -e PORT=$PORT \ + -e PLAYWRIGHT_CONTAINER=true \ + -e PLAYWRIGHT_isvrt=true \ + -e PLAYWRIGHT_staticTest=$PLAYWRIGHT_staticTest \ + -v $CWD:/e2e:rw$MOUNT_FLAGS \ + -w /e2e \ + --net=$NETWORK_MODE \ + --ipc=host \ + --entrypoint bash \ + $PLAYWRIGHT_IMAGE \ + "$@" + +else + if [ x$1 = "xrun" ]; then + shift + fi + if [ x$1 = "xvrt" ]; then + shift + PLAYWRIGHT_isvrt=true + fi + if [ x$1 = "xa11y" ]; then + shift + PLAYWRIGHT_isa11y=true + fi + if [ x$1 = "xupdate" ]; then + shift + # using env var so the user can pass a --env + UPDATE_ARGS="--update-snapshots" + PLAYWRIGHT_isvrt=true + fi + $DOCKER run -it --rm \ + -e LOCAL_ADDRESS=$LOCAL_ADDRESS \ + -e PORT=$PORT \ + -e PLAYWRIGHT_CONTAINER=true \ + -e PLAYWRIGHT_isa11y=$PLAYWRIGHT_isa11y \ + -e PLAYWRIGHT_isvrt=$PLAYWRIGHT_isvrt \ + -e PLAYWRIGHT_staticTest=$PLAYWRIGHT_staticTest \ + -v $CWD:/e2e:rw$MOUNT_FLAGS \ + -w /e2e \ + --net=$NETWORK_MODE \ + --ipc=host \ + $PLAYWRIGHT_IMAGE \ + npx \ + playwright \ + test \ + $UPDATE_ARGS \ + "$@" \ + || yarn playwright show-report playwright/results/preview +fi diff --git a/package-lock.json b/package-lock.json index fef987996..eac8705aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "eslint": "^9.8.0", "eslint-plugin-jsdoc": "48.0.2", "eslint-plugin-prefer-arrow": "^1.2.3", + "http-server": "^13.0.0", "jasmine": "^3.5.0", "jasmine-core": "~3.10.1", "jasmine-spec-reporter": "~7.0.0", @@ -58,6 +59,11 @@ "scss-bundle": "^3.1.1", "ts-node": "^10.9.2", "typescript": "~5.5.4" + }, + "optionalDependencies": { + "@axe-core/playwright": "4.8.2", + "@playwright/test": "1.46.0", + "axe-html-reporter": "2.2.3" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1133,6 +1139,29 @@ "rxjs": "^6.5.3 || ^7.4.0" } }, + "node_modules/@axe-core/playwright": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.8.2.tgz", + "integrity": "sha512-9KOhX2tNuvqn9DzpBNyqoqNKRZBrexeSiN9irQ0sEdq8zH13JnatepCJxobuXn4UopNy6iIpP4342beMiH+MSQ==", + "license": "MPL-2.0", + "optional": true, + "dependencies": { + "axe-core": "~4.8.2" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, + "node_modules/@axe-core/playwright/node_modules/axe-core": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.8.4.tgz", + "integrity": "sha512-CZLSKisu/bhJ2awW4kJndluz2HLZYIHh5Uy1+ZwDRkJi69811xgIXXfdU9HSLX0Th+ILrHj8qfL/5wzamsFtQg==", + "license": "MPL-2.0", + "optional": true, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/code-frame": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", @@ -4748,6 +4777,22 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.0.tgz", + "integrity": "sha512-/QYft5VArOrGRP5pgkrfKksqsKA6CEFyGQ/gjNe6q0y4tZ1aaPfq4gIjudr1s3D+pXyrPRdsy4opKDrjBabE5w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "playwright": "1.46.0" + }, + "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", @@ -6440,10 +6485,11 @@ } }, "node_modules/async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "dev": true, + "license": "MIT", "dependencies": { "lodash": "^4.17.14" } @@ -6537,6 +6583,34 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, + "node_modules/axe-core": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.2.tgz", + "integrity": "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==", + "license": "MPL-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/axe-html-reporter": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/axe-html-reporter/-/axe-html-reporter-2.2.3.tgz", + "integrity": "sha512-io8aCEt4fJvv43W+33n3zEa8rdplH5Ti2v5fOnth3GBKLhLHarNs7jj46xGfpnGnpaNrz23/tXPHC3HbwTzwwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "mustache": "^4.0.1", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=8.9.0" + }, + "peerDependencies": { + "axe-core": ">=3" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -6644,7 +6718,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "devOptional": true }, "node_modules/base": { "version": "0.11.2", @@ -6705,6 +6779,16 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/basic-auth": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.1.0.tgz", + "integrity": "sha512-CtGuTyWf3ig+sgRyC7uP6DM3N+5ur/p8L+FPfsd+BbIfIs74TFfCajZTHnCw6K5dqM0bZEbRIqRy1fAdiUJhTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -6849,7 +6933,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "devOptional": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7635,7 +7719,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "devOptional": true }, "node_modules/connect": { "version": "3.7.0", @@ -7886,6 +7970,16 @@ "node": ">= 0.10" } }, + "node_modules/corser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", + "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/cosmiconfig": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", @@ -8547,10 +8641,11 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -10732,7 +10827,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "devOptional": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -10965,7 +11060,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, + "devOptional": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -11342,6 +11437,16 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/hosted-git-info": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", @@ -11542,6 +11647,122 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/http-server": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/http-server/-/http-server-13.1.0.tgz", + "integrity": "sha512-MLqBMXeY/YN0FYMz4ifeOQCcg8pKj8YdmzX1pr/Vb2VrNnbxHN1s4K9BuZRVSyK/j3DQ8UVrrABb8m6EmFjWog==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-auth": "^1.0.3", + "chalk": "^4.1.2", + "corser": "^2.0.1", + "he": "^1.1.0", + "http-proxy": "^1.18.0", + "mime": "^1.6.0", + "minimist": "^1.2.5", + "opener": "^1.5.1", + "portfinder": "^1.0.25", + "secure-compare": "3.0.1", + "union": "~0.5.0", + "url-join": "^2.0.5" + }, + "bin": { + "http-server": "bin/http-server" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/http-server/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/http-server/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/http-server/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/http-server/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-server/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/http-server/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/http-server/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -11764,7 +11985,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, + "devOptional": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -11774,7 +11995,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "devOptional": true }, "node_modules/ini": { "version": "4.1.3", @@ -14014,7 +14235,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, + "devOptional": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -14278,6 +14499,16 @@ "multicast-dns": "cli.js" } }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "optional": true, + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/mute-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", @@ -15608,7 +15839,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, + "devOptional": true, "dependencies": { "wrappy": "1" } @@ -15647,6 +15878,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -16054,7 +16295,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -16278,6 +16519,104 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/playwright": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.0.tgz", + "integrity": "sha512-XYJ5WvfefWONh1uPAUAi0H2xXV5S3vrtcnXe6uAOgdGi3aSpqOSXX08IAjXW34xitfuOJsvXU5anXZxPSEQiJw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "playwright-core": "1.46.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz", + "integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "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==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright/node_modules/playwright-core": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.0.tgz", + "integrity": "sha512-9Y/d5UIwuJk8t3+lhmMSAJyNP1BUC/DqP3cQJDQQL/oWqAiuPTLgy7Q5dzglmTLwcBRdetzgNM/gni7ckfTr6A==", + "license": "Apache-2.0", + "optional": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/portfinder/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/portfinder/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -17410,7 +17749,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, + "devOptional": true, "dependencies": { "glob": "^7.1.3" }, @@ -17800,6 +18139,13 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/secure-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", + "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==", + "dev": true, + "license": "MIT" + }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -19755,6 +20101,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/union": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", + "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", + "dev": true, + "dependencies": { + "qs": "^6.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -19909,6 +20267,13 @@ "deprecated": "Please see https://github.com/lydell/urix#deprecated", "dev": true }, + "node_modules/url-join": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-2.0.5.tgz", + "integrity": "sha512-c2H1fIgpUdwFRIru9HFno5DT73Ok8hg5oOb5AT3ayIgvCRfxgs2jyt5Slw8kEB7j3QUr6yJmMPDT/odjk7jXow==", + "dev": true, + "license": "MIT" + }, "node_modules/use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", @@ -21216,7 +21581,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "devOptional": true }, "node_modules/ws": { "version": "8.11.0", @@ -21957,6 +22322,23 @@ "tslib": "^2.3.0" } }, + "@axe-core/playwright": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.8.2.tgz", + "integrity": "sha512-9KOhX2tNuvqn9DzpBNyqoqNKRZBrexeSiN9irQ0sEdq8zH13JnatepCJxobuXn4UopNy6iIpP4342beMiH+MSQ==", + "optional": true, + "requires": { + "axe-core": "~4.8.2" + }, + "dependencies": { + "axe-core": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.8.4.tgz", + "integrity": "sha512-CZLSKisu/bhJ2awW4kJndluz2HLZYIHh5Uy1+ZwDRkJi69811xgIXXfdU9HSLX0Th+ILrHj8qfL/5wzamsFtQg==", + "optional": true + } + } + }, "@babel/code-frame": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", @@ -24236,6 +24618,15 @@ "dev": true, "optional": true }, + "@playwright/test": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.0.tgz", + "integrity": "sha512-/QYft5VArOrGRP5pgkrfKksqsKA6CEFyGQ/gjNe6q0y4tZ1aaPfq4gIjudr1s3D+pXyrPRdsy4opKDrjBabE5w==", + "optional": true, + "requires": { + "playwright": "1.46.0" + } + }, "@rollup/plugin-json": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", @@ -25452,9 +25843,9 @@ "dev": true }, "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "dev": true, "requires": { "lodash": "^4.17.14" @@ -25510,6 +25901,23 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, + "axe-core": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.2.tgz", + "integrity": "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==", + "optional": true, + "peer": true + }, + "axe-html-reporter": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/axe-html-reporter/-/axe-html-reporter-2.2.3.tgz", + "integrity": "sha512-io8aCEt4fJvv43W+33n3zEa8rdplH5Ti2v5fOnth3GBKLhLHarNs7jj46xGfpnGnpaNrz23/tXPHC3HbwTzwwA==", + "optional": true, + "requires": { + "mustache": "^4.0.1", + "rimraf": "^3.0.2" + } + }, "axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -25592,7 +26000,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "devOptional": true }, "base": { "version": "0.11.2", @@ -25632,6 +26040,12 @@ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", "dev": true }, + "basic-auth": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.1.0.tgz", + "integrity": "sha512-CtGuTyWf3ig+sgRyC7uP6DM3N+5ur/p8L+FPfsd+BbIfIs74TFfCajZTHnCw6K5dqM0bZEbRIqRy1fAdiUJhTA==", + "dev": true + }, "batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -25755,7 +26169,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "devOptional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -26317,7 +26731,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "devOptional": true }, "connect": { "version": "3.7.0", @@ -26489,6 +26903,12 @@ "vary": "^1" } }, + "corser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", + "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", + "dev": true + }, "cosmiconfig": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", @@ -27008,9 +27428,9 @@ } }, "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -28583,7 +29003,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "devOptional": true }, "fsevents": { "version": "2.3.3", @@ -28746,7 +29166,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, + "devOptional": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -29032,6 +29452,12 @@ "function-bind": "^1.1.2" } }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, "hosted-git-info": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", @@ -29185,6 +29611,83 @@ } } }, + "http-server": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/http-server/-/http-server-13.1.0.tgz", + "integrity": "sha512-MLqBMXeY/YN0FYMz4ifeOQCcg8pKj8YdmzX1pr/Vb2VrNnbxHN1s4K9BuZRVSyK/j3DQ8UVrrABb8m6EmFjWog==", + "dev": true, + "requires": { + "basic-auth": "^1.0.3", + "chalk": "^4.1.2", + "corser": "^2.0.1", + "he": "^1.1.0", + "http-proxy": "^1.18.0", + "mime": "^1.6.0", + "minimist": "^1.2.5", + "opener": "^1.5.1", + "portfinder": "^1.0.25", + "secure-compare": "3.0.1", + "union": "~0.5.0", + "url-join": "^2.0.5" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -29330,7 +29833,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, + "devOptional": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -29340,7 +29843,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "devOptional": true }, "ini": { "version": "4.1.3", @@ -30924,7 +31427,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, + "devOptional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -31117,6 +31620,12 @@ "thunky": "^1.0.2" } }, + "mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "optional": true + }, "mute-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", @@ -31947,7 +32456,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, + "devOptional": true, "requires": { "wrappy": "1" } @@ -31973,6 +32482,12 @@ "is-wsl": "^3.1.0" } }, + "opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true + }, "optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -32271,7 +32786,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "devOptional": true }, "path-is-inside": { "version": "1.0.2", @@ -32421,6 +32936,68 @@ } } }, + "playwright": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.0.tgz", + "integrity": "sha512-XYJ5WvfefWONh1uPAUAi0H2xXV5S3vrtcnXe6uAOgdGi3aSpqOSXX08IAjXW34xitfuOJsvXU5anXZxPSEQiJw==", + "optional": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.46.0" + }, + "dependencies": { + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "optional": true + }, + "playwright-core": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.0.tgz", + "integrity": "sha512-9Y/d5UIwuJk8t3+lhmMSAJyNP1BUC/DqP3cQJDQQL/oWqAiuPTLgy7Q5dzglmTLwcBRdetzgNM/gni7ckfTr6A==", + "optional": true + } + } + }, + "playwright-core": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz", + "integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==", + "optional": true, + "peer": true + }, + "portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "dev": true, + "requires": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "requires": { + "minimist": "^1.2.6" + } + } + } + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -33243,7 +33820,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, + "devOptional": true, "requires": { "glob": "^7.1.3" } @@ -33515,6 +34092,12 @@ } } }, + "secure-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", + "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==", + "dev": true + }, "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -34951,6 +35534,15 @@ "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", "dev": true }, + "union": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", + "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", + "dev": true, + "requires": { + "qs": "^6.4.0" + } + }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -35058,6 +35650,12 @@ "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", "dev": true }, + "url-join": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-2.0.5.tgz", + "integrity": "sha512-c2H1fIgpUdwFRIru9HFno5DT73Ok8hg5oOb5AT3ayIgvCRfxgs2jyt5Slw8kEB7j3QUr6yJmMPDT/odjk7jXow==", + "dev": true + }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", @@ -35829,7 +36427,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "devOptional": true }, "ws": { "version": "8.11.0", diff --git a/package.json b/package.json index 387b40647..e966d41e2 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "scripts": { "ng": "ng", "start": "ng serve", + "start:prod": "http-server dist/ngx-datatable -s -p 4200 -a 127.0.0.1", "build": "ng build", "format": "prettier --write .", "format:check": "prettier --check .", @@ -16,7 +17,9 @@ "test:ci": "ng test ngx-datatable-lib --watch=false --progress=false --browsers=ChromeHeadlessCI", "ci": "run-s lint test:ci", "lint": "ng lint", - "e2e": "ng e2e", + "e2e:prepare": "playwright install", + "e2e": "playwright test", + "e2e:update": "yarn e2e --update-snapshots", "build-docs": "cross-env NODE_ENV=production ng build --configuration production --base-href=\"/ngx-datatable/\"", "predeploy-docs": "npm run build-docs", "deploy-docs": "angular-cli-ghpages --dir ./dist/ngx-datatable", @@ -64,6 +67,7 @@ "eslint": "^9.8.0", "eslint-plugin-jsdoc": "48.0.2", "eslint-plugin-prefer-arrow": "^1.2.3", + "http-server": "^13.0.0", "jasmine": "^3.5.0", "jasmine-core": "~3.10.1", "jasmine-spec-reporter": "~7.0.0", @@ -80,5 +84,10 @@ "scss-bundle": "^3.1.1", "ts-node": "^10.9.2", "typescript": "~5.5.4" + }, + "optionalDependencies": { + "@axe-core/playwright": "4.8.2", + "@playwright/test": "1.46.0", + "axe-html-reporter": "2.2.3" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..71cbda8ce --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,134 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const isContainer = !!process.env.PLAYWRIGHT_CONTAINER; +const port = process.env.PORT ?? '4200'; +const localAddress = process.env.LOCAL_ADDRESS ?? 'localhost'; +const isCI = !!process.env.CI; +const webServerCommand = 'yarn start:prod'; + +let isA11y = + !!process.env.PLAYWRIGHT_isa11y && process.env.PLAYWRIGHT_isa11y.toLocaleLowerCase() !== 'false'; +let isVrt = + !!process.env.PLAYWRIGHT_isvrt && process.env.PLAYWRIGHT_isvrt.toLocaleLowerCase() !== 'false'; +// Per default do both A11y and VRT +if (!isA11y && !isVrt) { + isA11y = true; + isVrt = true; +} + +const baseViewport = { + width: 1000, + height: 660 +}; + +const chromeLaunchOptions = { + args: [ + '--disable-skia-runtime-opts', + '--force-color-profile=srgb', + '--disable-low-res-tiling', + '--disable-oop-rasterization', + '--disable-composited-antialiasing', + '--disable-smooth-scrolling' + ] +}; + +export default defineConfig({ + testDir: './playwright', + snapshotDir: './playwright/snapshots', + /* Maximum time one test can run for. */ + timeout: 30 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000, + toHaveScreenshot: { maxDiffPixels: 0, threshold: 0.075 } + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + workers: 4, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: isCI + ? [ + [ + 'html', + { + open: 'on-failure', + outputFolder: './playwright/results/preview' + } + ], + [ + 'junit', + { + outputFile: `./playwright/results/reports/report-e2e.xml`, + includeProjectInTestName: true + } + ], + [ + './playwright/reporters/playwright-axe-reporter.ts', + { + outputFile: './playwright/results/a11y/accessibility-report.json', + htmlOutputDir: './playwright/results/a11y/tests' + } + ] + ] + : [ + ['line'], + [ + 'html', + { + open: isContainer ? 'never' : 'on-failure', + outputFolder: './playwright/results/preview' + } + ], + [ + 'junit', + { + outputFile: `./playwright/results/reports/report-${ + isA11y && isVrt ? 'e2e' : isA11y ? 'a11y' : 'vrt' + }.xml`, + includeProjectInTestName: true + } + ], + [ + './playwright/reporters/playwright-axe-reporter.ts', + { + outputFile: './playwright/results/a11y/accessibility-report.json', + htmlOutputDir: './playwright/results/a11y/tests', + isA11y + } + ] + ], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + baseURL: `http://${localAddress}:${port}`, + launchOptions: chromeLaunchOptions, + viewport: baseViewport, + actionTimeout: 0, + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry' + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] } + } + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: webServerCommand, + url: `http://${localAddress}:${port}`, + reuseExistingServer: !isCI + } +}); diff --git a/playwright/e2e/filter.spec.ts b/playwright/e2e/filter.spec.ts new file mode 100644 index 000000000..48e5748aa --- /dev/null +++ b/playwright/e2e/filter.spec.ts @@ -0,0 +1,28 @@ +import { expect, test } from '../support/test-helpers'; + +test.describe('table with filter', () => { + const example = 'filter'; + + test(example, async ({ si, page }) => { + await si.visitExample(example); + await expect(page.getByText('Sarah Massey')).toBeVisible(); + await si.runVisualAndA11yTests(); + + const paginator = page.locator('.datatable-pager').getByText('2'); + paginator.click(); + await expect(page.getByText('Sarah Massey')).not.toBeVisible(); + await expect(page.getByText('Robles Boyle')).toBeVisible(); + await si.runVisualAndA11yTests('next-page'); + + const filter = page.getByPlaceholder('Type to filter the name column...'); + filter.clear(); + filter.pressSequentially('Wong Craft'); + await expect(page.getByText('1 total')).toBeVisible(); + await si.runVisualAndA11yTests('filtered'); + + filter.clear(); + filter.pressSequentially('Some random text'); + await expect(page.getByText('No data to display')).toBeVisible(); + await si.runVisualAndA11yTests('empty'); + }); +}); diff --git a/playwright/e2e/row-group.spec.ts b/playwright/e2e/row-group.spec.ts new file mode 100644 index 000000000..b1aba11b1 --- /dev/null +++ b/playwright/e2e/row-group.spec.ts @@ -0,0 +1,70 @@ +import { expect, test } from '../support/test-helpers'; + +test.describe('row grouping', () => { + const example = 'row-grouping'; + + test(example, async ({ si, page }) => { + await si.visitExample(example); + + await expect(page.getByText('Ethel Price')).toBeVisible(); + await si.runVisualAndA11yTests('default', [ + // interactive elements< (, etc) within table cells are failing + { + id: 'aria-required-children', + enabled: false + }, + // disable label required with forms elements + { + id: 'label', + enabled: false + } + ]); + + const groupCheckbox = page.locator('.datatable-group-cell .datatable-checkbox input').first(); + groupCheckbox.check(); + + await expect(page.getByText('4 selected')).toBeVisible(); + + await si.runVisualAndA11yTests('group-selected', [ + { + id: 'aria-required-children', + enabled: false + }, + { + id: 'label', + enabled: false + } + ]); + }); + + test(example + ' expand/collapse', async ({ si, page }) => { + await si.visitExample(example); + + await expect(page.getByText('Ethel Price')).toBeVisible(); + const groupHeader = page.getByTitle('Expand/Collapse Group').first(); + groupHeader.click(); + await expect(page.getByText('Ethel Price')).not.toBeVisible(); + await si.runVisualAndA11yTests('group-collapsed', [ + { + id: 'aria-required-children', + enabled: false + }, + { + id: 'label', + enabled: false + } + ]); + groupHeader.click(); + await expect(page.getByText('Ethel Price')).toBeVisible(); + await si.runVisualAndA11yTests('group-expanded', [ + { + id: 'aria-required-children', + enabled: false + }, + { + id: 'label', + enabled: false + } + ]); + }); +}); diff --git a/playwright/reporters/playwright-axe-reporter.ts b/playwright/reporters/playwright-axe-reporter.ts new file mode 100644 index 000000000..3687ea664 --- /dev/null +++ b/playwright/reporters/playwright-axe-reporter.ts @@ -0,0 +1,198 @@ +/* eslint-disable no-console */ +import { mkdir, writeFile } from 'fs/promises'; + +import { expect, type FullConfig, type TestInfo } from '@playwright/test'; +import { type Reporter, Suite, TestCase } from '@playwright/test/reporter'; +import * as axe from 'axe-core'; +import { createHtmlReport } from 'axe-html-reporter'; + +const A11Y_VIOLATIONS_ID = 'a11yViolations'; + +const A11Y_TEST_NAME_ID = 'a11yTestName'; + +export const expectNoA11yViolations = async ( + testInfo: TestInfo, + violations: axe.Result[], + testName: string +): Promise => { + testInfo.annotations.push({ type: A11Y_TEST_NAME_ID, description: testName }); + await testInfo.attach(A11Y_VIOLATIONS_ID, { body: JSON.stringify(violations, null, 2) }); + const errorMessages = generateErrorMessages(testInfo, violations, testName); + expect(errorMessages).toEqual(''); +}; + +const generateHTMLFileName = (reportFileName: string): string => `${reportFileName}.html`; + +const generateHTMLFilePath = (directory: string, reportTestName: string): string => + `${directory}/${generateHTMLFileName(reportTestName)}`; + +const getReporterConfig = (testInfo: TestInfo): any | undefined => { + const reporters = testInfo.config.reporter; + + if (reporters && typeof reporters !== 'string') { + return reporters.find(reporter => reporter[0].includes('playwright-axe-reporter')); + } + + return undefined; +}; + +const generateErrorMessages = ( + testInfo: TestInfo, + violations: axe.Result[], + testName: string +): string => { + if (violations.length) { + let html: string | undefined; + + const reporterConfig = getReporterConfig(testInfo); + + const htmlOutputDir = reporterConfig?.[1] ? reporterConfig[1].htmlOutputDir : undefined; + + if (htmlOutputDir) { + html = generateHTMLFilePath(htmlOutputDir, testName); + } + + let message = `${violations.length} a11y violation(s) detected\n`; + violations.forEach(v => { + message += ` - ${v.nodes.length} ${v.id} (${v.impact}): ${v.description}\n`; + }); + if (html) { + message += ` => report: ${html}`; + } + return message; + } + return ''; +}; + +class PlaywrightAxeReporter implements Reporter { + private isA11y = true; + private outputFile?: string; + private htmlOutputDir?: string; + + private tests?: TestCase[]; + + constructor( + options: { outputFile?: string; htmlOutputDir?: string; isA11y?: boolean | string } = {} + ) { + this.outputFile = options.outputFile; + this.htmlOutputDir = options.htmlOutputDir; + if (options.isA11y === false) { + this.isA11y = false; + } else if (options.isA11y && options.isA11y !== true) { + const env = process.env[options.isA11y]; + this.isA11y = !!env && env.toLocaleLowerCase() !== 'false'; + } + } + + private getTestName(test: TestCase): string { + return ( + test.annotations.find(result => result.type === A11Y_TEST_NAME_ID)?.description ?? test.title + ); + } + + // Get all attachments of the latest test result (if there are multiple there were retries). + private getViolations(test: TestCase): axe.Result[] { + for (let index = test.results.length - 1; index >= 0; index--) { + const result = test.results[index]; + + let violations: axe.Result[] | undefined; + + result.attachments.forEach(attachment => { + if (attachment.name !== A11Y_VIOLATIONS_ID) { + return; + } + + const attachmentString = attachment.body?.toString('utf8'); + + if (!attachmentString) { + return; + } + + const foundViolations = JSON.parse(attachmentString) as axe.Result[]; + + if (!violations) { + violations = [...foundViolations]; + } else { + violations.push(...foundViolations); + } + }); + + if (violations) { + return violations; + } + } + + return []; + } + + private generateHTMLReport( + testTitle: string, + reportTestName: string, + violations: axe.Result[] + ): void { + if (this.htmlOutputDir) { + // createHtmlReport contains a hardcoded console.info() that generates noise + const origConsoleInfo = console.info; + (console.info as any) = () => {}; + + createHtmlReport({ + results: { violations }, + options: { + projectKey: testTitle, + outputDir: this.htmlOutputDir, + reportFileName: generateHTMLFileName(reportTestName) + } + }); + + console.info = origConsoleInfo; + } + } + + onBegin(_: FullConfig, suite: Suite): void { + if (this.isA11y && this.outputFile) { + this.tests = suite.allTests(); + } + } + + async onEnd(): Promise { + if (this.isA11y && this.outputFile && this.tests) { + const a11yReport: Record = {}; + + this.tests.forEach(test => { + const violations = this.getViolations(test); + + if (violations.length) { + const testName = this.getTestName(test); + + this.generateHTMLReport(test.title, testName, violations); + + a11yReport[testName] = violations; + } + }); + + const jsonReport = { + total: this.tests.length, + passes: this.tests.reduce( + (count, test) => + test.outcome() === 'expected' || test.outcome() === 'flaky' ? count + 1 : count, + 0 + ), + errors: this.tests.reduce( + (count, test) => (test.outcome() === 'unexpected' ? count + 1 : count), + 0 + ), + results: a11yReport + }; + + await mkdir(this.outputFile.substring(0, this.outputFile.lastIndexOf('/')), { + recursive: true + }); + await writeFile(this.outputFile, JSON.stringify(jsonReport, null, 2), 'utf8'); + } + } + + printsToStdio(): boolean { + return false; + } +} +export default PlaywrightAxeReporter; diff --git a/playwright/snapshots/e2e/filter.spec.ts-snapshots/filter--empty-chromium-linux.png b/playwright/snapshots/e2e/filter.spec.ts-snapshots/filter--empty-chromium-linux.png new file mode 100644 index 000000000..2326256f2 --- /dev/null +++ b/playwright/snapshots/e2e/filter.spec.ts-snapshots/filter--empty-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29914c7e2ef15d4cb66e3aa691004961bd76acb71d5fe66fc3c7d3cce1bbef56 +size 54101 diff --git a/playwright/snapshots/e2e/filter.spec.ts-snapshots/filter--filtered-chromium-linux.png b/playwright/snapshots/e2e/filter.spec.ts-snapshots/filter--filtered-chromium-linux.png new file mode 100644 index 000000000..e1e87a029 --- /dev/null +++ b/playwright/snapshots/e2e/filter.spec.ts-snapshots/filter--filtered-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6384868d96f3546307ad4de39d4a45329c0426d70c06b474d514fa86c937669a +size 53757 diff --git a/playwright/snapshots/e2e/filter.spec.ts-snapshots/filter--next-page-chromium-linux.png b/playwright/snapshots/e2e/filter.spec.ts-snapshots/filter--next-page-chromium-linux.png new file mode 100644 index 000000000..2b61a9910 --- /dev/null +++ b/playwright/snapshots/e2e/filter.spec.ts-snapshots/filter--next-page-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4a59a3e5823cde46a42cfe670232728fd45b116851cc6a501eccdd24144afa4 +size 82061 diff --git a/playwright/snapshots/e2e/filter.spec.ts-snapshots/filter-chromium-linux.png b/playwright/snapshots/e2e/filter.spec.ts-snapshots/filter-chromium-linux.png new file mode 100644 index 000000000..418f91aa1 --- /dev/null +++ b/playwright/snapshots/e2e/filter.spec.ts-snapshots/filter-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33d7c7f6f3494720709cf293c56e7906451659433f70b53b6098dba660e60324 +size 84359 diff --git a/playwright/snapshots/e2e/row-group.spec.ts-snapshots/row-grouping--default-chromium-linux.png b/playwright/snapshots/e2e/row-group.spec.ts-snapshots/row-grouping--default-chromium-linux.png new file mode 100644 index 000000000..0d3250991 --- /dev/null +++ b/playwright/snapshots/e2e/row-group.spec.ts-snapshots/row-grouping--default-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1dfd6a166d80945ccf7356b395f3e01b3a985acf0e7ace3d728b936a777e5c0 +size 79891 diff --git a/playwright/snapshots/e2e/row-group.spec.ts-snapshots/row-grouping--group-collapsed-chromium-linux.png b/playwright/snapshots/e2e/row-group.spec.ts-snapshots/row-grouping--group-collapsed-chromium-linux.png new file mode 100644 index 000000000..2ff394d9e --- /dev/null +++ b/playwright/snapshots/e2e/row-group.spec.ts-snapshots/row-grouping--group-collapsed-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e86a365017cc4665c94fc30411bee60549ed87774d3e4173ed58434d048b90a0 +size 67621 diff --git a/playwright/snapshots/e2e/row-group.spec.ts-snapshots/row-grouping--group-expanded-chromium-linux.png b/playwright/snapshots/e2e/row-group.spec.ts-snapshots/row-grouping--group-expanded-chromium-linux.png new file mode 100644 index 000000000..0d3250991 --- /dev/null +++ b/playwright/snapshots/e2e/row-group.spec.ts-snapshots/row-grouping--group-expanded-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1dfd6a166d80945ccf7356b395f3e01b3a985acf0e7ace3d728b936a777e5c0 +size 79891 diff --git a/playwright/snapshots/e2e/row-group.spec.ts-snapshots/row-grouping--group-selected-chromium-linux.png b/playwright/snapshots/e2e/row-group.spec.ts-snapshots/row-grouping--group-selected-chromium-linux.png new file mode 100644 index 000000000..7a30546ff --- /dev/null +++ b/playwright/snapshots/e2e/row-group.spec.ts-snapshots/row-grouping--group-selected-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:295b259b4a1e86b7e6dc0e006691b9f6d559dd3c7cdf94c8da9562837fa8c41b +size 80388 diff --git a/playwright/support/test-helpers.ts b/playwright/support/test-helpers.ts new file mode 100644 index 000000000..07d11a715 --- /dev/null +++ b/playwright/support/test-helpers.ts @@ -0,0 +1,227 @@ +// eslint-disable-next-line @typescript-eslint/naming-convention +import AxeBuilder from '@axe-core/playwright'; +import { + test as baseTest, + type ElementHandle, + expect, + type Page, + type TestInfo +} from '@playwright/test'; +import axe from 'axe-core'; + +import { expectNoA11yViolations } from '../reporters/playwright-axe-reporter'; + +export { expect } from '@playwright/test'; + +const NGX_EXAMPLE_NAME_ID = 'siExampleName'; + +const detectTestTypes = (): { isA11y: boolean; isVrt: boolean } => { + let isA11y = + !!process.env.PLAYWRIGHT_isa11y && + process.env.PLAYWRIGHT_isa11y.toLocaleLowerCase() !== 'false'; + + let isVrt = + !!process.env.PLAYWRIGHT_isvrt && process.env.PLAYWRIGHT_isvrt.toLocaleLowerCase() !== 'false'; + + // Per default do both A11y and VRT + if (!isA11y && !isVrt) { + isA11y = true; + isVrt = true; + } + + return { isA11y, isVrt }; +}; + +const { isA11y, isVrt } = detectTestTypes(); + +export const VIEWPORTS = [ + { name: 'default', width: 0, height: 0 }, // Default, 0 will skip resizing and use the default value + { name: 'tablet-portrait', width: 768, height: 1024 }, + { name: 'tablet-landscape', width: 1024, height: 768 } +]; +export type Viewport = (typeof VIEWPORTS)[number]; + +export type StaticTestOptions = { + delay?: number; + maxDiffPixels?: number; + disabledA11yRules?: string[]; + viewports?: (Viewport | string)[]; + waitCallback?: (page: Page) => Promise; + skipAutoScaleViewport?: boolean; +}; + +class SiTestHelpers { + private disableAnimationsTag: ElementHandle | undefined; + + constructor( + private page: Page, + private testInfo: TestInfo + ) {} + + public async visitExample(name: string, autoScaleViewport = true): Promise { + await test.step( + 'visitExample: ' + name, + async () => { + // Set it initially (or override previous). + this.overrideExampleName(name); + const urlParams = []; + + const urlParamsString = urlParams.length ? '?' + urlParams.join('&') : ''; + const newHash = `/#/${name}${urlParamsString}`; + await this.page.goto(newHash); + + await this.page.evaluate(() => document.fonts.ready); + + if (autoScaleViewport) { + const height = await this.page.evaluate(() => document.body.scrollHeight); + const width = await this.page.evaluate(() => document.body.scrollWidth); + const viewportSize = this.page.viewportSize(); + if (!viewportSize || viewportSize.height < height || viewportSize.width < width) { + await this.page.setViewportSize({ + height: Math.max(height, viewportSize?.height ?? 0), + width: Math.max(width, viewportSize?.width ?? 0) + }); + } + } + }, + { box: true } + ); + } + + public async runVisualAndA11yTests( + step?: string, + axeRulesSet: (string | { id: string; enabled: boolean })[] = [], + maxDiffPixels?: number + ): Promise { + const example = this.getExampleName() ?? this.testInfo.title; + const testName = this.makeTestName(example, step); + await test.step( + 'runVisualAndA11yTests: ' + testName, + async () => { + if (isA11y) { + await this.enableDisableAnimations(this.page, false); + const rules = axeRulesSet + .filter( + item => + typeof item === 'string' || (typeof item === 'object' && item?.enabled === true) + ) + .map(item => (typeof item === 'object' ? item.id : item)); + + if ( + !!process.env.PLAYWRIGHT_a11y_all && + process.env.PLAYWRIGHT_a11y_all.toLocaleLowerCase() !== 'false' + ) { + // Only use global disabled rules. + axeRulesSet = []; + } + + const disabledRules = [ + 'landmark-one-main', + 'page-has-heading-one', + 'region', + ...axeRulesSet + .filter(item => typeof item === 'object' && item?.enabled === false) + .map(item => (typeof item === 'object' ? item.id : item)) + ]; + let axeResults: axe.AxeResults; + try { + if (rules.length) { + axeResults = (await new AxeBuilder({ page: this.page }) + .exclude('.e2e-ignore-a11y') + .withRules(rules) + .disableRules(disabledRules) + .analyze()) as axe.AxeResults; + } else { + axeResults = (await new AxeBuilder({ page: this.page }) + .exclude('.e2e-ignore-a11y') + .disableRules(disabledRules) + .analyze()) as axe.AxeResults; + } + } finally { + await this.enableDisableAnimations(this.page, true); + } + await expectNoA11yViolations(this.testInfo, axeResults.violations, testName); + } + if (isVrt) { + await expect(this.page).toHaveScreenshot(testName + '.png', { + maxDiffPixels, + stylePath: './playwright/support/vrt-styles.css' + }); + } + }, + { box: true } + ); + } + + public async waitForAllAnimationsToComplete(threshold = 0): Promise { + await this.page.waitForFunction( + count => window.document.getAnimations().length <= count, + threshold + ); + } + + /** + * Set the example name to be used by `runVisualAndA11yTests`, automatically set by `visitExample`. + * @param name - Name to be set, if empty or undefined then the previous value is removed. + */ + public overrideExampleName(name?: string | undefined): void { + const previousIndex = this.testInfo.annotations.findIndex( + result => result.type === NGX_EXAMPLE_NAME_ID + ); + if (previousIndex >= 0) { + this.testInfo.annotations.splice(previousIndex, 1); + } + if (name) { + this.testInfo.annotations.push({ type: NGX_EXAMPLE_NAME_ID, description: name }); + } + } + + /** + * Get the example name set by `visitExample`. + */ + public getExampleName(): string | undefined { + return this.testInfo.annotations.find(result => result.type === NGX_EXAMPLE_NAME_ID) + ?.description; + } + + private async enableDisableAnimations(page: Page, show: boolean): Promise { + if (!show) { + await this.disableAnimationsTag?.evaluate(element => + element.parentNode?.removeChild(element) + ); + await this.disableAnimationsTag?.dispose(); + this.disableAnimationsTag = await page.addStyleTag({ + content: `*, *:before, *:after { + transition-property: none !important; + animation: none !important; +}` + }); + } else { + await this.disableAnimationsTag?.evaluate(element => + element.parentNode?.removeChild(element) + ); + await this.disableAnimationsTag?.dispose(); + this.disableAnimationsTag = undefined; + } + } + + private makeTestName(example: string, step?: string): string { + // this is so that we have filenames that make sense + let testName = example.replace(/\//g, '--'); + if (step) { + testName += `--${step}`; + } + return testName; + } +} + +export const test = baseTest.extend<{ + si: SiTestHelpers; +}>({ + si: [ + async ({ page }, use, testInfo) => { + await use(new SiTestHelpers(page, testInfo)); + }, + { box: true } + ] +}); diff --git a/playwright/support/vrt-styles.css b/playwright/support/vrt-styles.css new file mode 100644 index 000000000..837376e79 --- /dev/null +++ b/playwright/support/vrt-styles.css @@ -0,0 +1,3 @@ +.e2e-ignore { + display: none; +} diff --git a/projects/ngx-datatable/src/lib/components/datatable.component.html b/projects/ngx-datatable/src/lib/components/datatable.component.html index d51309697..96eb260e7 100644 --- a/projects/ngx-datatable/src/lib/components/datatable.component.html +++ b/projects/ngx-datatable/src/lib/components/datatable.component.html @@ -78,7 +78,9 @@ -
+
+
+
diff --git a/projects/ngx-datatable/src/lib/components/footer/pager.component.ts b/projects/ngx-datatable/src/lib/components/footer/pager.component.ts index 636652f22..b59916a8f 100644 --- a/projects/ngx-datatable/src/lib/components/footer/pager.component.ts +++ b/projects/ngx-datatable/src/lib/components/footer/pager.component.ts @@ -18,12 +18,11 @@ import { Page } from '../../types/internal.types'; @for (pg of pages; track pg.number) {
  • - + {{ pg.text }}
  • diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 9da938891..e91c39133 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -118,7 +118,11 @@ const routes: Routes = [ ]; @NgModule({ - imports: [RouterModule.forRoot(routes)], + imports: [ + RouterModule.forRoot(routes, { + useHash: true + }) + ], exports: [RouterModule] }) export class AppRoutingModule {} diff --git a/src/app/basic/row-grouping.component.ts b/src/app/basic/row-grouping.component.ts index 50b5fc259..a0e123fa7 100644 --- a/src/app/basic/row-grouping.component.ts +++ b/src/app/basic/row-grouping.component.ts @@ -51,7 +51,7 @@ import { >
    @@ -98,6 +99,7 @@ import { name="{{ rowIndex }}" value="1" class="expectedpayment2" + [attr.aria-label]="'ex pay2' + rowIndex" (change)="checkGroup($event, row, rowIndex, group)" [checked]="row.exppayno === 1" /> @@ -109,6 +111,7 @@ import { name="{{ rowIndex }}" value="2" class="expectedpayment3" + [attr.aria-label]="'ex pay3' + rowIndex" (change)="checkGroup($event, row, rowIndex, group)" [checked]="row.exppaypending === 1" /> @@ -139,6 +142,7 @@ import { (blur)="updateValue($event, 'comment', rowIndex)" type="text" name="comment" + aria-label="comment" [value]="value" /> diff --git a/src/assets/app.css b/src/assets/app.css index cc1a0d2de..c8135d999 100644 --- a/src/assets/app.css +++ b/src/assets/app.css @@ -78,7 +78,7 @@ input:focus { } a { - color: grey; + color: #4c4c68; text-decoration: none; } @@ -88,7 +88,7 @@ a { } h3 { - background: #1f89ff; + background: #206ed9; margin: 0 0 30px 0; color: #fff; text-align: left; @@ -98,7 +98,7 @@ h3 { } h3 a { - color: #ccc; + color: #fff; } h3 small { diff --git a/src/index.html b/src/index.html index 4a7a9edc6..79da4de8d 100644 --- a/src/index.html +++ b/src/index.html @@ -1,5 +1,5 @@ - + ngx-datatable - Angular component for presenting large and complex data