feat: integrate accessibility checker for automated a11y scans

- Added `accessibility-checker` package to the project dependencies.
- Enhanced test fixtures to include accessibility scanning capabilities.
- Implemented `runA11yScan` method in the `LangflowPage` type for running accessibility checks.
- Created utility functions for building accessibility scan labels, formatting failure messages, and generating summary attachments.
- Updated test setup to manage accessibility sessions and assert compliance based on scan results.
This commit is contained in:
Viktor Avelino
2026-06-09 16:19:05 -04:00
parent 2ffd2d1b5d
commit fa7d54100b
7 changed files with 881 additions and 6 deletions

View File

@ -25,9 +25,10 @@ Work top-to-bottom. Early phases fix shared primitives first, then page surfaces
### 0.1 — Set up IBM Equal Access Checker in Playwright
- [ ] **Fix:** Add `accessibility-checker` and wire `runA11yScan` into Playwright fixtures. Ensure reports write to `coverage/accessibility-reports/`.
- [ ] **Fix:** Add `accessibility-checker` and wire `runA11yScan` into Playwright fixtures. Start in report mode with `RUN_A11Y=true`; gate assertion separately with `RUN_A11Y_ASSERT=true`. Ensure reports write to `coverage/accessibility-reports/`.
- [ ] **Auto:** `RUN_A11Y=true npx playwright test` produces IBM reports without crashing.
- [ ] **Manual:** Open generated HTML report and confirm IBM rules + code snippets are present.
- [ ] **Note:** IBM checker scans `IBM_Accessibility` policy, not a direct `Level 1` policy. Use report output as evidence source, then map findings back to current Level 1 scope.
### 0.2 — Restore visible focus indicators globally

View File

@ -0,0 +1,14 @@
ruleArchive: 19May2026
policies:
- IBM_Accessibility
reportLevels:
- violation
# - potentialviolation
# - recommendation
# - potentialrecommendation
# - manual
outputFormat:
- html
outputFilenameTimestamp: false
baselineFolder: tests/baselines
outputFolder: coverage/accessibility-reports

View File

@ -125,6 +125,7 @@
"@types/sortablejs": "^1.15.8",
"@types/uuid": "^9.0.8",
"@vitejs/plugin-react-swc": "^4.2.2",
"accessibility-checker": "^4.0.26",
"autoprefixer": "^10.4.19",
"jest": "^30.0.3",
"jest-environment-jsdom": "^30.0.2",
@ -2089,6 +2090,16 @@
"react-hook-form": "^7.0.0"
}
},
"node_modules/@ibm/telemetry-js": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/@ibm/telemetry-js/-/telemetry-js-1.11.0.tgz",
"integrity": "sha512-RO/9j+URJnSfseWg9ZkEX9p+a3Ousd33DBU7rOafoZB08RqdzxFVYJ2/iM50dkBuD0o7WX7GYt1sLbNgCoE+pA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"ibmtelemetry": "dist/collect.js"
}
},
"node_modules/@internationalized/date": {
"version": "3.12.1",
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.1.tgz",
@ -3623,6 +3634,31 @@
"node": ">=18"
}
},
"node_modules/@puppeteer/browsers": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.0.4.tgz",
"integrity": "sha512-HGM8iAmGTf+Y7t0373szVbTmt3d7vPkYL/1bpOkOFO0YUYLgSeuYBCzESklogNPvOBnZ/MRD5f07OkpqH1trtA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"modern-tar": "^0.7.6",
"yargs": "^17.7.2"
},
"bin": {
"browsers": "lib/main-cli.js"
},
"engines": {
"node": ">=22.12.0"
},
"peerDependencies": {
"proxy-agent": ">=8.0.1"
},
"peerDependenciesMeta": {
"proxy-agent": {
"optional": true
}
}
},
"node_modules/@radix-ui/number": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
@ -6406,6 +6442,13 @@
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@testim/chrome-version": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.1.4.tgz",
"integrity": "sha512-kIhULpw9TrGYnHp/8VfdcneIcxKnLixmADtukQRtJUmsVlMg0niMkwV0xZmi8hqa57xqilIHjWFA0GKvEjVU5g==",
"dev": true,
"license": "MIT"
},
"node_modules/@testing-library/dom": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
@ -7769,6 +7812,62 @@
"node": ">= 0.6"
}
},
"node_modules/accessibility-checker": {
"version": "4.0.26",
"resolved": "https://registry.npmjs.org/accessibility-checker/-/accessibility-checker-4.0.26.tgz",
"integrity": "sha512-opI8Wp90lb953wKr3TuVpeTW9y23uzlr71rWzmvej4nHenzQJWxezYJkgI+XB7q4qdVd/psDr19gD8xbc+eh1Q==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@ibm/telemetry-js": "^1.9.1",
"chromedriver": "*",
"js-yaml": "^4.2.0",
"microdiff": "^1.5.0",
"puppeteer": "^25.1.0"
},
"bin": {
"achecker": "bin/achecker.js"
},
"peerDependencies": {
"exceljs": "^4.4.0"
},
"peerDependenciesMeta": {
"exceljs": {
"optional": true
}
}
},
"node_modules/accessibility-checker/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/accessibility-checker/node_modules/js-yaml": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
"integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/nodeca"
}
],
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/ace-builds": {
"version": "1.43.6",
"resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.43.6.tgz",
@ -7797,6 +7896,16 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/adm-zip": {
"version": "0.5.17",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz",
"integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0"
}
},
"node_modules/ag-charts-types": {
"version": "10.3.9",
"resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-10.3.9.tgz",
@ -8430,6 +8539,16 @@
"node": ">=6.0.0"
}
},
"node_modules/basic-ftp": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz",
"integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/bin-version": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/bin-version/-/bin-version-6.0.0.tgz",
@ -8992,6 +9111,46 @@
"license": "ISC",
"optional": true
},
"node_modules/chromedriver": {
"version": "149.0.2",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-149.0.2.tgz",
"integrity": "sha512-/ArEVtowNDQWy0KTKYD+1E3h1HeZoosU6X3PAvUFcJQr5wMGfG7h+L3CdiDF2BvOL2M9sAtozqw+pbzrD2MmzA==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@testim/chrome-version": "^1.1.4",
"adm-zip": "^0.5.17",
"axios": "^1.16.0",
"compare-versions": "^6.1.0",
"proxy-agent": "^8.0.1",
"proxy-from-env": "^2.0.0",
"tcp-port-used": "^1.0.2"
},
"bin": {
"chromedriver": "bin/chromedriver"
},
"engines": {
"node": ">=22"
}
},
"node_modules/chromium-bidi": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-16.0.1.tgz",
"integrity": "sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"mitt": "^3.0.1",
"zod": "^3.24.1"
},
"engines": {
"node": ">=20.19.0 <22.0.0 || >=22.12.0"
},
"peerDependencies": {
"devtools-protocol": "*"
}
},
"node_modules/ci-info": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz",
@ -9182,6 +9341,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/compare-versions": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz",
"integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==",
"dev": true,
"license": "MIT"
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@ -9454,6 +9620,16 @@
"node": ">=12"
}
},
"node_modules/data-uri-to-buffer": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-8.0.0.tgz",
"integrity": "sha512-6UHfyCux51b8PTGDgveqtz1tvphBku5DrMKKJbFAZAJOI2zsjDpDoYE1+QGj7FOMS4BdTFNJsJiR3zEB0xH0yQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 20"
}
},
"node_modules/data-urls": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
@ -9579,6 +9755,13 @@
"node": ">=4.0.0"
}
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true,
"license": "MIT"
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
@ -9671,6 +9854,37 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/degenerator": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/degenerator/-/degenerator-7.0.1.tgz",
"integrity": "sha512-ABErK0IefDSyHjlPH7WUEenIAX2rPPnrDcDM+TS3z3+zu9TfyKKi07BQM+8rmxpdE2y1v5fjjdoAS/x4D2U60w==",
"dev": true,
"license": "MIT",
"dependencies": {
"ast-types": "^0.13.4",
"escodegen": "^2.1.0",
"esprima": "^4.0.1"
},
"engines": {
"node": ">= 20"
},
"peerDependencies": {
"quickjs-wasi": "^2.2.0"
}
},
"node_modules/degenerator/node_modules/ast-types": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
"integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
"dev": true,
"license": "MIT",
"dependencies": {
"tslib": "^2.0.1"
},
"engines": {
"node": ">=4"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -9744,6 +9958,13 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/devtools-protocol": {
"version": "0.0.1624250",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1624250.tgz",
"integrity": "sha512-YFAat/lOiIk0ARmBweG+ygrEcbZrq5B9urRyUoeQKp53MlidHXE2TmTbxKcaXoQj7u/aX+jebDO4BW55rs0WwA==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@ -10046,6 +10267,39 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/escodegen": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
"integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"esprima": "^4.0.1",
"estraverse": "^5.2.0",
"esutils": "^2.0.2"
},
"bin": {
"escodegen": "bin/escodegen.js",
"esgenerate": "bin/esgenerate.js"
},
"engines": {
"node": ">=6.0"
},
"optionalDependencies": {
"source-map": "~0.6.1"
}
},
"node_modules/escodegen/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/eslint-visitor-keys": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
@ -10123,6 +10377,16 @@
}
}
},
"node_modules/estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
},
"node_modules/estree-util-is-identifier-name": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
@ -10980,6 +11244,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-uri": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-8.0.1.tgz",
"integrity": "sha512-/5N/P4Lrh0p/mDwlDRi7Y1+P2o/OyzZI3l6Iz1Ov6XXwwm1y3RlZLuo3gVgML99djrEDtV980bBxSuOeHLk8ww==",
"dev": true,
"license": "MIT",
"dependencies": {
"basic-ftp": "^5.3.1",
"data-uri-to-buffer": "8.0.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
@ -11703,6 +11982,16 @@
"node": ">= 12"
}
},
"node_modules/ip-regex": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz",
"integrity": "sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -11928,6 +12217,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/is-url": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
"dev": true,
"license": "MIT"
},
"node_modules/is-windows": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
@ -11954,6 +12250,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is2": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is2/-/is2-2.0.1.tgz",
"integrity": "sha512-+WaJvnaA7aJySz2q/8sLjMb2Mw14KTplHmSwcSpZ/fWJPkUmqw3YTzSWbPJ7OAwRvdYTWF2Wg+yYJ1AdP5Z8CA==",
"dev": true,
"license": "MIT",
"dependencies": {
"deep-is": "^0.1.3",
"ip-regex": "^2.1.0",
"is-url": "^1.2.2"
},
"engines": {
"node": ">=v0.10.0"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
@ -14579,6 +14890,13 @@
"integrity": "sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==",
"license": "Apache-2.0"
},
"node_modules/microdiff": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/microdiff/-/microdiff-1.5.0.tgz",
"integrity": "sha512-Drq+/THMvDdzRYrK0oxJmOKiC24ayUV8ahrt8l3oRK51PWt6gdtrIGrlIH3pT/lFh1z93FbAcidtsHcWbnRz8Q==",
"dev": true,
"license": "MIT"
},
"node_modules/micromark": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
@ -15279,6 +15597,13 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"dev": true,
"license": "MIT"
},
"node_modules/mj-context-menu": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz",
@ -15305,6 +15630,16 @@
"license": "MIT",
"optional": true
},
"node_modules/modern-tar": {
"version": "0.7.6",
"resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.6.tgz",
"integrity": "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
@ -15438,6 +15773,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/netmask": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz",
"integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/no-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
@ -15897,6 +16242,83 @@
"node": ">=6"
}
},
"node_modules/pac-proxy-agent": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-9.1.0.tgz",
"integrity": "sha512-1aU+1mpj3DrQPfo3gh+3Gap3G5x+axnMx1P/y0ZF2ch7kb2meyOCAH8K2k9d27ROsTE7TnAerzxqF9aon2jqnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "9.0.0",
"debug": "^4.3.4",
"get-uri": "8.0.1",
"http-proxy-agent": "9.1.0",
"https-proxy-agent": "9.1.0",
"pac-resolver": "9.0.1",
"quickjs-wasi": "^2.2.0",
"socks-proxy-agent": "10.1.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/pac-proxy-agent/node_modules/agent-base": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz",
"integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 20"
}
},
"node_modules/pac-proxy-agent/node_modules/http-proxy-agent": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-9.1.0.tgz",
"integrity": "sha512-2NxoveTT58mjYT4n3RPTEfCZGLMbidoO8XEieXfpSYxu+PQJ1qpx4ypwH6N+uF9twBPIvRRgvkvW5HUTYWENig==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "9.0.0",
"debug": "^4.3.4",
"proxy-agent-negotiate": "1.1.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/pac-proxy-agent/node_modules/https-proxy-agent": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.1.0.tgz",
"integrity": "sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "9.0.0",
"debug": "^4.3.4",
"proxy-agent-negotiate": "1.1.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/pac-resolver": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-9.0.1.tgz",
"integrity": "sha512-lJbS008tmkj08VhoM8Hzuv/VE5tK9MS0OIQ/7+s0lIF+BYhiQWFYzkSpML7lXs9iBu2jfmzBTLzhe9n6BX+dYw==",
"dev": true,
"license": "MIT",
"dependencies": {
"degenerator": "7.0.1",
"netmask": "^2.0.2"
},
"engines": {
"node": ">= 20"
},
"peerDependencies": {
"quickjs-wasi": "^2.2.0"
}
},
"node_modules/package-hash": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz",
@ -16544,6 +16966,94 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-agent": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-8.0.2.tgz",
"integrity": "sha512-idLLRewuemWd7GH/BDJzGiB0dWGfT2SQs3jy6NtZtGWU9uPTTSdeC1/cdbqLwgzhfv027daGFuXX426e2Eg20A==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "9.0.0",
"debug": "^4.3.4",
"http-proxy-agent": "9.1.0",
"https-proxy-agent": "9.1.0",
"lru-cache": "^7.14.1",
"pac-proxy-agent": "9.1.0",
"proxy-from-env": "^2.0.0",
"socks-proxy-agent": "10.1.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/proxy-agent-negotiate": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-agent-negotiate/-/proxy-agent-negotiate-1.1.0.tgz",
"integrity": "sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 20"
},
"peerDependencies": {
"kerberos": "^2.0.0"
},
"peerDependenciesMeta": {
"kerberos": {
"optional": true
}
}
},
"node_modules/proxy-agent/node_modules/agent-base": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz",
"integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 20"
}
},
"node_modules/proxy-agent/node_modules/http-proxy-agent": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-9.1.0.tgz",
"integrity": "sha512-2NxoveTT58mjYT4n3RPTEfCZGLMbidoO8XEieXfpSYxu+PQJ1qpx4ypwH6N+uF9twBPIvRRgvkvW5HUTYWENig==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "9.0.0",
"debug": "^4.3.4",
"proxy-agent-negotiate": "1.1.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/proxy-agent/node_modules/https-proxy-agent": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.1.0.tgz",
"integrity": "sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "9.0.0",
"debug": "^4.3.4",
"proxy-agent-negotiate": "1.1.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/proxy-agent/node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/proxy-from-env": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
@ -16574,6 +17084,46 @@
"node": ">=6"
}
},
"node_modules/puppeteer": {
"version": "25.1.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.1.0.tgz",
"integrity": "sha512-7L6/0JM7XStK99lIL4xQySyNEXNfII6pk0BxkI5kKBTOhR7AsoQiv067YTsE/rIXxQiq9ajlO4WcqBjS/FWK1A==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "3.0.4",
"chromium-bidi": "16.0.1",
"devtools-protocol": "0.0.1624250",
"lilconfig": "^3.1.3",
"puppeteer-core": "25.1.0",
"typed-query-selector": "^2.12.2"
},
"bin": {
"puppeteer": "lib/puppeteer/node/cli.js"
},
"engines": {
"node": ">=22.12.0"
}
},
"node_modules/puppeteer-core": {
"version": "25.1.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.1.0.tgz",
"integrity": "sha512-jKzy5y4WG6uNuFbTWgW1D7mqoT9o0nllc/6a1DGF775T1mPmgw3scdFEtEq67yVFikavQmbYq6NLfbTfxHSlqQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "3.0.4",
"chromium-bidi": "16.0.1",
"devtools-protocol": "0.0.1624250",
"typed-query-selector": "^2.12.2",
"webdriver-bidi-protocol": "0.4.2",
"ws": "^8.21.0"
},
"engines": {
"node": ">=22.12.0"
}
},
"node_modules/pure-rand": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz",
@ -16640,6 +17190,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/quickjs-wasi": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/quickjs-wasi/-/quickjs-wasi-2.2.0.tgz",
"integrity": "sha512-zQxXmQMrEoD3S+jQdYsloq4qAuaxKFHZj6hHqOYGwB2iQZH+q9e/lf5zQPXCKOk0WJuAjzRFbO4KwHIp2D05Iw==",
"dev": true,
"license": "MIT"
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@ -17990,6 +18547,17 @@
"node": ">=8"
}
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/snake-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz",
@ -18001,6 +18569,46 @@
"tslib": "^2.0.3"
}
},
"node_modules/socks": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz",
"integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==",
"dev": true,
"license": "MIT",
"dependencies": {
"ip-address": "^10.1.1",
"smart-buffer": "^4.2.0"
},
"engines": {
"node": ">= 10.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks-proxy-agent": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-10.1.0.tgz",
"integrity": "sha512-WlMj/67cEJ6MDI1OcsnjuYKDNDoyPCCYZ249kuuXPiMDw9F8PXkVaQ7YWu3siTydfQ/4BEZcvGzu+aYvz7dDCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "9.0.0",
"debug": "^4.3.4",
"socks": "^2.8.3"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/socks-proxy-agent/node_modules/agent-base": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz",
"integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 20"
}
},
"node_modules/sort-keys": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
@ -18760,6 +19368,42 @@
"streamx": "^2.15.0"
}
},
"node_modules/tcp-port-used": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.3.tgz",
"integrity": "sha512-4CEQ3qRJYo+mtEbJ+OoQu3dF4TDkwaO3RDVC4UzP5cpAOIUWwuwPjD7sdxDFFqsMUjsXVVYBMlg/boAaloThMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "4.3.1",
"is2": "2.0.1"
}
},
"node_modules/tcp-port-used/node_modules/debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/tcp-port-used/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true,
"license": "MIT"
},
"node_modules/teex": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
@ -19164,6 +19808,13 @@
"node": ">= 0.6"
}
},
"node_modules/typed-query-selector": {
"version": "2.12.2",
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz",
"integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==",
"dev": true,
"license": "MIT"
},
"node_modules/typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
@ -20369,6 +21020,13 @@
"integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==",
"license": "Apache-2.0"
},
"node_modules/webdriver-bidi-protocol": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz",
"integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",

View File

@ -177,6 +177,7 @@
"@types/sortablejs": "^1.15.8",
"@types/uuid": "^9.0.8",
"@vitejs/plugin-react-swc": "^4.2.2",
"accessibility-checker": "^4.0.26",
"autoprefixer": "^10.4.19",
"jest": "^30.0.3",
"jest-environment-jsdom": "^30.0.2",

View File

@ -1,9 +1,26 @@
// tests/fixtures.ts
import { test as base, expect, Page } from "@playwright/test";
import type { ICheckerResult } from "accessibility-checker";
import * as aChecker from "accessibility-checker";
import "./playwrightCoverage";
import {
buildA11yScanLabel,
buildA11ySummaryAttachment,
formatA11yFailure,
isCheckerReport,
} from "./utils/accessibility-checker";
import type { LangflowPage } from "./utils/types";
export type { LangflowPage } from "./utils/types";
const RUN_A11Y = process.env.RUN_A11Y === "true";
const RUN_A11Y_ASSERT = process.env.RUN_A11Y_ASSERT === "true";
type A11yFixtures = {
_a11ySession: void;
};
// Optional CPU throttling for reproducing race conditions seen on slower
// runners (Windows CI). Enable with LF_CPU_THROTTLE=<rate>, e.g. 4.
const CPU_THROTTLE_RATE = (() => {
@ -14,8 +31,18 @@ const CPU_THROTTLE_RATE = (() => {
})();
// Extend test to log backend errors
export const test = base.extend({
page: async ({ page }, use) => {
export const test = base.extend<{ page: LangflowPage }, A11yFixtures>({
_a11ySession: [
async ({}, use) => {
await use();
if (RUN_A11Y) {
await aChecker.close();
}
},
{ scope: "worker", auto: true },
],
page: async ({ page }, use, testInfo) => {
if (CPU_THROTTLE_RATE > 0) {
try {
const client = await page.context().newCDPSession(page);
@ -43,6 +70,45 @@ export const test = base.extend({
allowFlowErrors = true;
};
let a11yScanIndex = 0;
(
page as Page & {
runA11yScan?: (label: string) => Promise<ICheckerResult | null>;
}
).runA11yScan = async (label: string) => {
if (!RUN_A11Y) {
return null;
}
const scanIndex = a11yScanIndex++;
const scanLabel = buildA11yScanLabel(
testInfo.project.name,
label,
scanIndex,
);
const result = await aChecker.getCompliance(page, scanLabel);
if (!isCheckerReport(result.report)) {
throw new Error(
`IBM accessibility scan failed for ${scanLabel}: checker returned an error payload.`,
);
}
testInfo.attachments.push(
buildA11ySummaryAttachment(scanIndex, scanLabel, result.report),
);
if (RUN_A11Y_ASSERT) {
const returnCode = aChecker.assertCompliance(result.report);
const failureMessage = formatA11yFailure(scanLabel, result.report);
expect(returnCode, failureMessage).toBe(0);
}
return result;
};
// Monitor API responses for errors
page.on("response", async (response) => {
const url = response.url();
@ -126,6 +192,10 @@ export const test = base.extend({
return;
}
if (typeof bodyResult !== "string") {
return;
}
responseBody = bodyResult;
} catch (bodyReadErr) {
if (timeoutId) {
@ -220,8 +290,6 @@ export const test = base.extend({
`Error: ${errorPreview}\n\n` +
`If this error is expected, call page.allowFlowErrors() at the start of your test.`;
// Use page.close() to fail the test immediately
page.emit("pageerror", new Error(errorMessage));
throw new Error(errorMessage);
}
}
@ -238,7 +306,7 @@ export const test = base.extend({
}
});
await use(page);
await use(page as LangflowPage);
// Check for errors and fail test if not allowed
if (errors.length > 0) {

View File

@ -0,0 +1,131 @@
import type { ICheckerReport, ICheckerResult } from "accessibility-checker";
import path from "path";
function compactWhitespace(value: string): string {
return value.replace(/\s+/g, " ").trim();
}
function truncate(value: string, max = 140): string {
return value.length > max ? `${value.slice(0, max - 1)}` : value;
}
export function sanitizeA11yLabelPart(value: string): string {
const sanitized = value.replace(/[^\w-]+/g, "_").replace(/^_+|_+$/g, "");
return sanitized || "scan";
}
export function buildA11yScanLabel(
projectName: string,
label: string,
scanIndex: number,
): string {
const scanLabelParts = [
sanitizeA11yLabelPart(projectName),
sanitizeA11yLabelPart(label),
];
if (scanIndex > 0) {
scanLabelParts.push(`${scanIndex}`);
}
return scanLabelParts.join("__");
}
export function isCheckerReport(
report: ICheckerResult["report"],
): report is ICheckerReport {
return "summary" in report && "results" in report && "label" in report;
}
export function buildA11ySummaryAttachment(
scanIndex: number,
scanLabel: string,
report: ICheckerReport,
) {
return {
name: `a11y-summary-${scanIndex}`,
contentType: "application/json",
body: Buffer.from(
JSON.stringify(
{
label: scanLabel,
url: report.summary.URL,
policies: report.summary.policies,
ruleArchive: report.summary.ruleArchive,
counts: report.summary.counts,
},
null,
2,
),
),
};
}
export function formatA11yFailure(
scanLabel: string,
report: ICheckerReport,
): string {
const counts = report.summary.counts;
const reportPath = path.posix.join(
"coverage",
"accessibility-reports",
`${scanLabel}.html`,
);
const failingIssues = report.results.filter(
(issue) =>
issue.level === "violation" || issue.level === "potentialviolation",
);
const groupedIssues = new Map<
string,
{
count: number;
message: string;
level: string;
ruleId: string;
snippet: string;
xpath: string;
}
>();
for (const issue of failingIssues) {
const key = `${issue.level}::${issue.ruleId}::${issue.message}`;
const existing = groupedIssues.get(key);
if (existing) {
existing.count += 1;
continue;
}
groupedIssues.set(key, {
count: 1,
message: issue.message,
level: issue.level ?? "unknown",
ruleId: issue.ruleId,
snippet: truncate(compactWhitespace(issue.snippet)),
xpath: Object.values(issue.path ?? {})[0] ?? "n/a",
});
}
const topIssues = Array.from(groupedIssues.values())
.sort((left, right) => right.count - left.count)
.slice(0, 8);
const lines = [
`IBM accessibility scan failed: ${scanLabel}`,
`Report: ${reportPath}`,
`Counts: violation=${counts.violation}, potential=${counts.potentialviolation}, recommendation=${counts.recommendation}, manual=${counts.manual}`,
`Top issues (${topIssues.length}/${groupedIssues.size} groups shown):`,
...topIssues.map(
(issue) =>
`- [${issue.level}] ${issue.ruleId} x${issue.count}: ${issue.message}\n xpath: ${issue.xpath}\n snippet: ${issue.snippet}`,
),
];
if (groupedIssues.size > topIssues.length) {
lines.push("More issues in HTML report.");
}
return lines.join("\n");
}

View File

@ -1,4 +1,5 @@
import type { Page } from "@playwright/test";
import type { ICheckerResult } from "accessibility-checker";
/**
* Page augmented with the `allowFlowErrors()` helper attached by
@ -8,4 +9,5 @@ import type { Page } from "@playwright/test";
*/
export type LangflowPage = Page & {
allowFlowErrors: () => void;
runA11yScan: (label: string) => Promise<ICheckerResult | null>;
};