├── .husky ├── .gitignore ├── post-checkout ├── pre-push ├── pre-commit └── commit-msg ├── packages ├── wdio │ ├── .depcheckrc │ ├── src │ │ ├── index.ts │ │ └── wdio.ts │ ├── tsconfig.json │ ├── package.json │ └── README.md ├── common │ ├── testMocks │ │ ├── sa11y-custom-rules.json │ │ ├── testProcessFiles │ │ │ └── testProcessHelper.json │ │ └── packageTestHelper.ts │ ├── .depcheckrc │ ├── tsconfig.json │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── wdio.ts │ │ ├── format.ts │ │ └── axe.ts │ └── README.md ├── test-utils │ ├── __data__ │ │ ├── sa11y-custom-rules.json │ │ ├── descendancyA11yIssues.html │ │ ├── a11yIssues.html │ │ ├── a11yIncompleteIssues.html │ │ ├── noA11yIssues.html │ │ ├── a11yIssuesVisual.html │ │ └── a11yCustomIssues.html │ ├── tsconfig.json │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── utils.ts │ │ ├── wdio.ts │ │ └── test-data.ts │ ├── __tests__ │ │ └── test-utils.test.ts │ └── README.md ├── test-integration │ ├── .depcheckrc │ ├── jest.config.js │ ├── package.json │ ├── README.md │ ├── __tests__ │ │ └── jest.test.js │ └── __wdio__ │ │ └── wdio.test.ts ├── jest │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ ├── matcher.test.ts.snap │ │ │ └── setup.test.ts.snap │ │ ├── setup.test.ts │ │ ├── resultsProcessor.test.ts │ │ ├── matcher.test.ts │ │ └── groupViolationResultsProcessor.test.ts │ ├── src │ │ ├── index.ts │ │ ├── automatic.ts │ │ ├── setup.ts │ │ ├── matcher.ts │ │ └── groupViolationResultsProcessor.ts │ ├── tsconfig.json │ └── package.json ├── preset-rules │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ ├── rules.test.ts.snap │ │ │ └── wcag.test.ts.snap │ │ └── wcag.test.ts │ ├── tsconfig.json │ ├── src │ │ ├── custom-rules │ │ │ ├── changes.ts │ │ │ ├── rules.ts │ │ │ └── checks.ts │ │ ├── full.ts │ │ ├── index.ts │ │ ├── config.ts │ │ ├── docgen.ts │ │ ├── wcag.ts │ │ ├── extended.ts │ │ ├── rules.ts │ │ └── base.ts │ └── package.json ├── format │ ├── tsconfig.json │ ├── src │ │ ├── index.ts │ │ └── filter.ts │ ├── package.json │ ├── README.md │ └── __tests__ │ │ └── format.test.ts ├── assert │ ├── tsconfig.json │ ├── package.json │ ├── README.md │ ├── src │ │ └── assert.ts │ └── __tests__ │ │ └── __snapshots__ │ │ └── assert.test.ts.snap ├── browser-lib │ ├── .depcheckrc │ ├── tsconfig.json │ ├── package.json │ ├── src │ │ └── index.ts │ ├── rollup.config.mjs │ └── __wdio__ │ │ └── browser-lib.test.ts ├── matcher │ ├── tsconfig.json │ ├── src │ │ ├── index.ts │ │ ├── matcher.ts │ │ ├── groupViolationResultsProcessor.ts │ │ └── setup.ts │ ├── package.json │ └── README.md └── vitest │ ├── src │ ├── index.ts │ ├── matcher.ts │ ├── automatic.ts │ ├── setup.ts │ └── groupViolationResultsProcessor.ts │ ├── tsconfig.json │ └── package.json ├── .npmrc ├── .github ├── CODEOWNERS └── workflows │ ├── codeql-analysis.yml │ ├── nodejs.yml │ └── scorecards.yml ├── codecov.yml ├── .gitignore ├── CODEOWNERS ├── .editorConfig ├── license-header.txt ├── jest.setup.js ├── .prettierrc.js ├── .markdown-link-check.json ├── babel.config.js ├── .gitattributes ├── lerna.json ├── commitlint.config.js ├── cSpell.json ├── tsconfig.eslint.json ├── tsconfig.common.json ├── SECURITY.md ├── lint-staged.config.js ├── tsconfig.json ├── .releaserc.yml ├── jest.config.js ├── renovate.json ├── LICENSE.txt ├── sitemap.xml ├── .eslintrc.js └── package.json /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /packages/wdio/.depcheckrc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | save-exact=true 3 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @salesforce/sa11y-reviewers 2 | #ECCN:Open Source -------------------------------------------------------------------------------- /.husky/post-checkout: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn build:ci -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn test:clean -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint:staged 5 | -------------------------------------------------------------------------------- /packages/common/testMocks/sa11y-custom-rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": ["rule1", "rule2"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/common/testMocks/testProcessFiles/testProcessHelper.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "value" 3 | } 4 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint -e $GIT_PARAMS 5 | -------------------------------------------------------------------------------- /packages/test-utils/__data__/sa11y-custom-rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": ["sa11y-Keyboard", "color-contrast"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/common/.depcheckrc: -------------------------------------------------------------------------------- 1 | # Ignore webdriverio - we need the types for `WebdriverIO.Browser` 2 | ignores: ['webdriverio'] 3 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | layout: 'reach, diff, files' 3 | branches: # branch names that can post comment 4 | - 'master' 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.tsbuildinfo 3 | .DS_Store 4 | .vscode/ 5 | .idea/ 6 | .project/ 7 | packages/**/dist/ 8 | node_modules/ 9 | coverage/ 10 | /junit.xml 11 | -------------------------------------------------------------------------------- /packages/test-integration/.depcheckrc: -------------------------------------------------------------------------------- 1 | # Sally/common is a devDependency, but depcheck really wants it to be a "dependency". 2 | ignores: ["@sally/common"] 3 | skip-missing: true -------------------------------------------------------------------------------- /packages/test-utils/__data__/descendancyA11yIssues.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
  • testData
  • 5 |
    6 | 7 | 8 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Comment line immediately above ownership line is reserved for related other information. Please be careful while editing. 2 | #ECCN:Open Source 3 | #GUSINFO:Open Source,Open Source Workflow 4 | -------------------------------------------------------------------------------- /packages/jest/__tests__/__snapshots__/matcher.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`mock timer helper should result in error when mock timer is being used from API 1`] = `""`; 4 | -------------------------------------------------------------------------------- /packages/test-utils/__data__/a11yIssues.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

    Header One

    4 |

    Header Two

    5 |
    6 | 7 |
    8 | 9 | 10 | -------------------------------------------------------------------------------- /.editorConfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | 10 | [*.{json,yml}] 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /packages/preset-rules/__tests__/__snapshots__/rules.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`preset-rules documentation should throw error for non existent rule 1`] = `"Unable to find rule: foo"`; 4 | -------------------------------------------------------------------------------- /license-header.txt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) <%= YEAR %>, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | -------------------------------------------------------------------------------- /packages/test-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.common.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src"], 8 | "references": [{ "path": "../common" }] 9 | } 10 | -------------------------------------------------------------------------------- /packages/format/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.common.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src"], 8 | "references": [{ "path": "../test-utils" }, { "path": "../common" }] 9 | } 10 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | // eslint-disable-next-line no-undef 8 | jest.setTimeout(15000); 9 | -------------------------------------------------------------------------------- /packages/wdio/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | export { assertAccessible, assertAccessibleSync } from './wdio'; 9 | -------------------------------------------------------------------------------- /packages/preset-rules/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.common.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "lib": ["DOM", "DOM.Iterable", "es2020"], // Override @tsconfig/node14 7 | 8 | }, 9 | "include": ["src"], 10 | "references": [{ "path": "../common" }] 11 | } 12 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 4, 3 | printWidth: 120, 4 | singleQuote: true, 5 | trailingComma: 'es5', 6 | quoteProps: 'consistent', 7 | overrides: [ 8 | { 9 | files: '*.md', 10 | tabWidth: 2, // To accommodate doctoc formatting (no override for tab width in doctoc) 11 | }, 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /.markdown-link-check.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePatterns": [ 3 | { 4 | "pattern": "^https://www\\.npmjs\\.com/org/sa11y$" 5 | }, 6 | { 7 | "pattern": "^https://www\\.npmjs\\.com/package/@sa11y/" 8 | } 9 | ], 10 | "aliveStatusCodes": [200, 206], 11 | "retryOn429": true, 12 | "retryCount": 3, 13 | "fallbackRetryDelay": "2s" 14 | } 15 | -------------------------------------------------------------------------------- /packages/common/testMocks/packageTestHelper.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | const getFilesToBeExempted = () => ['file1', 'file2']; 8 | 9 | module.exports = getFilesToBeExempted; 10 | -------------------------------------------------------------------------------- /packages/assert/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.common.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src"], 8 | "references": [ 9 | { "path": "../preset-rules" }, 10 | { "path": "../format" }, 11 | { "path": "../test-utils" }, 12 | { "path": "../common" } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.common.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "types": ["node", "webdriverio/async"] 7 | 8 | }, 9 | "include": ["src"], 10 | // "@sa11y/common" is the base package and should not depend on any 11 | // other sa11y packages to prevent cyclic dependency 12 | } 13 | -------------------------------------------------------------------------------- /packages/browser-lib/.depcheckrc: -------------------------------------------------------------------------------- 1 | 2 | # rollup.config.mjs isn't detected by Depcheck 3 | # Explicitly permit them 4 | ignores: 5 | - "@rollup/plugin-commonjs" 6 | - "@rollup/plugin-node-resolve" 7 | - "@rollup/plugin-replace" 8 | - "rollup" 9 | - "rollup-plugin-polyfill-node" 10 | - "rollup-plugin-progress" 11 | - "rollup-plugin-sizes" 12 | - "@rollup/plugin-terser" 13 | - "rollup-plugin-typescript2" 14 | skip-missing: true 15 | -------------------------------------------------------------------------------- /packages/preset-rules/src/custom-rules/changes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import axe from 'axe-core'; 8 | 9 | const changesData = {}; 10 | 11 | export default changesData as { rules: axe.Rule[] }; 12 | -------------------------------------------------------------------------------- /packages/jest/__tests__/__snapshots__/setup.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`jest setup should throw error when global expect is undefined 1`] = ` 4 | "Unable to find Jest's expect function. 5 | Please check your Jest installation and that you have added @sa11y/jest correctly to your jest configuration. 6 | See https://github.com/salesforce/sa11y/tree/master/packages/jest#readme for help." 7 | `; 8 | -------------------------------------------------------------------------------- /packages/matcher/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.common.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src"], 8 | "references": [ 9 | { "path": "../common" }, 10 | { "path": "../preset-rules" }, 11 | { "path": "../assert" }, 12 | { "path": "../format" }, 13 | { "path": "../test-utils" } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | /** @type {import('@babel/core').ConfigFunction} */ 8 | module.exports = { 9 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], 10 | }; 11 | -------------------------------------------------------------------------------- /packages/jest/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import resultsProcessor from './resultsProcessor'; 8 | 9 | export { toBeAccessible } from './matcher'; 10 | export { registerSa11yMatcher, setup } from './setup'; 11 | export { resultsProcessor }; 12 | -------------------------------------------------------------------------------- /packages/vitest/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | export { toBeAccessible } from './matcher.js'; 9 | export { registerSa11yMatcher, setup } from './setup.js'; 10 | export { vitestResultsProcessor } from './groupViolationResultsProcessor.js'; 11 | -------------------------------------------------------------------------------- /packages/jest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.common.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src"], 8 | "references": [ 9 | { "path": "../common" }, 10 | { "path": "../preset-rules" }, 11 | { "path": "../assert" }, 12 | { "path": "../format" }, 13 | { "path": "../test-utils" }, 14 | { "path": "../matcher" } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/format/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | export { A11yError, Options } from './format'; 9 | export { exceptionListFilter, exceptionListFilterSelectorKeywords } from './filter'; 10 | export { A11yResult, A11yResults, appendWcag } from './result'; 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Mark generated files so that github can better display diffs in PRs 2 | # https://help.github.com/articles/customizing-how-changed-files-appear-on-github/ 3 | # https://github.com/github/linguist#using-gitattributes 4 | # https://www.git-scm.com/docs/gitignore#_pattern_format 5 | # 6 | # Mark generated files 7 | yarn.lock linguist-generated=true 8 | # generated JS files 9 | **/dist/** linguist-generated=true 10 | # jest snapshots 11 | **/__snapshots__/** linguist-generated=true 12 | -------------------------------------------------------------------------------- /packages/test-integration/jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /** @type {import('@jest/types').Config.InitialOptions} */ 9 | module.exports = { 10 | testEnvironment: 'jsdom', 11 | testMatch: ['**/__tests__/**/*.[jt]s?(x)'], 12 | testRunner: 'jest-jasmine2', 13 | }; 14 | -------------------------------------------------------------------------------- /packages/wdio/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.common.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "noEmitOnError": false, 7 | "types": ["node", "webdriverio/async", "@wdio/jasmine-framework"] 8 | }, 9 | "include": ["src"], 10 | "references": [ 11 | { "path": "../preset-rules" }, 12 | { "path": "../format" }, 13 | { "path": "../common" }, 14 | { "path": "../test-utils" } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/preset-rules/__tests__/__snapshots__/wcag.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`WCAG Metadata extractor should format WCAG metadata when required data is present (arg:["wcag2a"]) 1`] = `"SA11Y-best-practice-P3"`; 4 | 5 | exports[`WCAG Metadata extractor should format WCAG metadata when required data is present (arg:["wcag21aaa"]) 1`] = `"SA11Y-best-practice-P3"`; 6 | 7 | exports[`WCAG Metadata extractor should format WCAG metadata when required data is present (arg:["wcag222", "wcag21aa"]) 1`] = `"SA11Y-WCAG-SC2.2.2-P3"`; 8 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "version": "5.1.0", 4 | "exact": true, 5 | "npmClient": "yarn", 6 | "useWorkspaces": true, 7 | "command": { 8 | "publish": { 9 | "registry": "https://registry.npmjs.org/", 10 | "access": "public" 11 | }, 12 | "version": { 13 | "conventionalCommits": true 14 | } 15 | }, 16 | "changelogPreset": { 17 | "name": "conventional-changelog-conventionalcommits" 18 | }, 19 | "conventional-commits": true 20 | } 21 | -------------------------------------------------------------------------------- /packages/vitest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.common.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | // ESM-specific settings 7 | "module": "NodeNext", 8 | "moduleResolution": "NodeNext" 9 | }, 10 | "include": ["src"], 11 | "references": [ 12 | { "path": "../common" }, 13 | { "path": "../preset-rules" }, 14 | { "path": "../assert" }, 15 | { "path": "../format" }, 16 | { "path": "../test-utils" }, 17 | { "path": "../matcher" } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* This is the pattern from semantic-release */ 9 | const automaticCommitPattern = /^chore\(release\):.*\[skip ci\]/; 10 | 11 | /** @type {import('@commitlint/types').UserConfig} */ 12 | module.exports = { 13 | extends: ['@commitlint/config-conventional'], 14 | ignores: [(commitMsg) => automaticCommitPattern.test(commitMsg)], 15 | }; 16 | -------------------------------------------------------------------------------- /cSpell.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePaths": ["node_modules/**", "package.json"], 3 | "ignoreWords": [ 4 | "circleci", 5 | "CNCF", 6 | "descendancy", 7 | "dlitem", 8 | "doctoc", 9 | "goog", 10 | "iife", 11 | "sfdx", 12 | "SPDX", 13 | "tsdoc", 14 | "wcag", 15 | "wdio", 16 | "webdriverio", 17 | "webm", 18 | "Vidyard", 19 | "pdnejk", 20 | "slds", 21 | "labelledby", 22 | "describedby" 23 | ], 24 | "flagWords": ["master-slave", "slave", "blacklist", "whitelist"], 25 | "allowCompoundWords": true 26 | } 27 | -------------------------------------------------------------------------------- /packages/browser-lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../../tsconfig.common.json", 4 | "compilerOptions": { 5 | "rootDir": "src", 6 | "outDir": "dist", 7 | "types": ["webdriverio/async", "jest"], 8 | "noEmitOnError": false, 9 | "declaration": false, 10 | "declarationMap": false, 11 | "composite": false 12 | }, 13 | "include": ["src"], 14 | "references": [ 15 | { "path": "../preset-rules" }, 16 | { "path": "../format" }, 17 | { "path": "../common" }, 18 | { "path": "../test-utils" } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.common.json", 3 | "include": ["**.js", "./packages/**/*.ts", "./packages/**/*.conf*.js", "./packages/test-integration/**/*.js"], 4 | "exclude": ["**/dist"], 5 | "compilerOptions": { 6 | "noEmit": true, 7 | "declaration": false /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 8 | "declarationMap": false /* Create sourcemaps for d.ts files. */, 9 | "sourceMap": false /* Create source map files for emitted JavaScript files. */, 10 | "types": ["jest", "node", "webdriverio/async", "@wdio/framework-jasmine", "jasmine"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.common.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node14/tsconfig.json", 3 | "compilerOptions": { 4 | // Ref: https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping 5 | "lib": ["DOM", "DOM.Iterable", "es2020"], // Override @tsconfig/node14 6 | "sourceMap": true, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "composite": true, 10 | "incremental": true, 11 | "noEmitOnError": true, 12 | "noUnusedLocals": true, 13 | // LCT has these: 14 | "experimentalDecorators": true, 15 | "esModuleInterop": true 16 | }, 17 | "exclude": ["**/dist", "**/__tests__"] 18 | } 19 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security 2 | 3 | 4 | 5 | 6 | 7 | Please report any security issue to [security@salesforce.com](mailto:security@salesforce.com) 8 | as soon as it is discovered. This library limits its runtime dependencies in 9 | order to reduce the total cost of ownership as much as can be, but all consumers 10 | should remain vigilant and have their security stakeholders review all third-party 11 | products (3PP) like this one and their dependencies. 12 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | module.exports = { 9 | '**/*.{js,ts,json,yaml,yml,md}': 'prettier --write', 10 | '**/*.{js,ts,md}': ['eslint --fix', 'cspell -- --no-summary'], 11 | '**/*.md': ['markdown-link-check --quiet --config .markdown-link-check.json', 'doctoc --github'], 12 | 'yarn.lock': 'yarn lint:lockfile', 13 | // eslint-disable-next-line @typescript-eslint/explicit-function-return-type 14 | 'package.json': () => 'yarn install', 15 | }; 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "files": [ 4 | /* only process references */ 5 | ], 6 | "references": [ 7 | { "path": "./packages/preset-rules" }, 8 | { "path": "./packages/format" }, 9 | { "path": "./packages/test-utils" }, 10 | { "path": "./packages/assert" }, 11 | { "path": "./packages/jest" }, 12 | { "path": "./packages/vitest" }, 13 | { "path": "./packages/matcher" }, 14 | { "path": "./packages/wdio" } 15 | ], 16 | "exclude": ["**/node_modules"], 17 | "compilerOptions": { 18 | "allowSyntheticDefaultImports": true 19 | }, 20 | "extends": "@tsconfig/node14/tsconfig.json" 21 | } 22 | -------------------------------------------------------------------------------- /packages/preset-rules/src/full.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { defaultPriority, getA11yConfig } from './rules'; 8 | import { extendedRulesInfo } from './extended'; 9 | 10 | // Rules that have been excluded due to being new or deprecated by axe 11 | export const excludedRules = ['aria-roledescription', 'audio-caption', 'duplicate-id', 'duplicate-id-active']; 12 | 13 | // Add excluded rules to extended to get the full list 14 | excludedRules.forEach((rule) => extendedRulesInfo.set(rule, { priority: defaultPriority, wcagSC: '', wcagLevel: '' })); 15 | 16 | export const full = getA11yConfig(extendedRulesInfo); 17 | -------------------------------------------------------------------------------- /packages/test-integration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sa11y/test-integration", 3 | "version": "5.1.0", 4 | "private": true, 5 | "description": "Private package for integration testing @sa11y packages", 6 | "license": "BSD-3-Clause", 7 | "homepage": "https://github.com/salesforce/sa11y/tree/master/packages/test-integ#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/salesforce/sa11y.git", 11 | "directory": "packages/test-integration" 12 | }, 13 | "devDependencies": { 14 | "@jest/globals": "28.1.3", 15 | "@sa11y/common": "5.1.0", 16 | "@sa11y/jest": "5.1.0", 17 | "@sa11y/test-utils": "5.1.0", 18 | "@sa11y/wdio": "5.1.0" 19 | }, 20 | "engines": { 21 | "node": "^20 || ^22 || ^24" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/test-utils/__data__/a11yIncompleteIssues.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Axe Needs Review Test 7 | 8 | 20 | 21 | 22 | 23 |

    Axe Needs Review Test

    24 | 25 |
    26 | Pass
    27 | Fail 28 |
    29 | 30 | 31 | -------------------------------------------------------------------------------- /packages/matcher/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | export { fakeTimerErrMsg, runA11yCheck, formatOptions } from './matcher'; 9 | export { 10 | defaultAutoCheckOpts, 11 | mutationObserverCallback, 12 | observerOptions, 13 | skipTest, 14 | runAutomaticCheck, 15 | AutoCheckOpts, 16 | RenderedDOMSaveOpts, 17 | defaultRenderedDOMSaveOpts, 18 | } from './automatic'; 19 | export { 20 | registerRemoveChild, 21 | defaultSa11yOpts, 22 | improvedChecksFilter, 23 | updateAutoCheckOpts, 24 | registerCustomSa11yRules, 25 | } from './setup'; 26 | export { processA11yDetailsAndMessages } from './groupViolationResultsProcessor'; 27 | -------------------------------------------------------------------------------- /.releaserc.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/semantic-release.json 2 | --- 3 | branches: 4 | - master 5 | - name: alpha 6 | prerelease: true 7 | channel: alpha 8 | 9 | plugins: 10 | - - '@semantic-release/commit-analyzer' 11 | - preset: conventionalcommits 12 | releaseRules: 13 | - type: docs 14 | scope: README 15 | release: patch 16 | 17 | - '@semantic-release/release-notes-generator' 18 | - - '@semantic-release/changelog' 19 | - changelogTitle: "# Changelog\n\nAll notable changes to this project will be documented in this file." 20 | - - '@semantic-release/exec' 21 | - publishCmd: yarn lerna publish --no-git-tag-version --no-git-reset --no-push --yes --access public --exact ${nextRelease.version} 22 | - '@semantic-release/github' 23 | 24 | preset: conventionalcommits 25 | -------------------------------------------------------------------------------- /packages/test-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sa11y/test-utils", 3 | "version": "5.1.0", 4 | "private": true, 5 | "description": "Private package providing test utilities for @sa11y packages", 6 | "license": "BSD-3-Clause", 7 | "homepage": "https://github.com/salesforce/sa11y/tree/master/packages/test-utils#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/salesforce/sa11y.git", 11 | "directory": "packages/test-utils" 12 | }, 13 | "main": "dist/index.js", 14 | "types": "dist/index.d.ts", 15 | "files": [ 16 | "dist/**/*.js", 17 | "dist/**/*.d.ts*" 18 | ], 19 | "dependencies": { 20 | "@sa11y/common": "5.1.0" 21 | }, 22 | "devDependencies": { 23 | "@jest/globals": "28.1.3" 24 | }, 25 | "engines": { 26 | "node": "^20 || ^22 || ^24" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sa11y/common", 3 | "version": "5.1.0", 4 | "description": "Common utilities, constants, error messages for @sa11y", 5 | "license": "BSD-3-Clause", 6 | "homepage": "https://github.com/salesforce/sa11y/tree/master/packages/common#readme", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/salesforce/sa11y.git", 10 | "directory": "packages/common" 11 | }, 12 | "main": "dist/index.js", 13 | "types": "dist/index.d.ts", 14 | "files": [ 15 | "dist/**/*.js", 16 | "dist/**/*.d.ts*" 17 | ], 18 | "dependencies": { 19 | "axe-core": "4.11.0" 20 | }, 21 | "devDependencies": { 22 | "webdriverio": "7.40.0" 23 | }, 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "engines": { 28 | "node": "^20 || ^22 || ^24" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/test-utils/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | export { 9 | audioURL, 10 | exceptionList, 11 | domWithA11yIssues, 12 | domWithDescendancyA11yIssues, 13 | domWithA11yIssuesBodyID, 14 | domWithNoA11yIssues, 15 | domWithNoA11yIssuesChildCount, 16 | domWithVisualA11yIssues, 17 | htmlFileWithA11yIssues, 18 | htmlFileWithNoA11yIssues, 19 | htmlFileWithVisualA11yIssues, 20 | a11yIssuesCount, 21 | a11yIssuesCountFiltered, 22 | shadowDomID, 23 | videoURL, 24 | customRulesFilePath, 25 | domWithA11yCustomIssues, 26 | domWithA11yIncompleteIssues, 27 | } from './test-data'; 28 | export { beforeEachSetup, cartesianProduct } from './utils'; 29 | -------------------------------------------------------------------------------- /packages/common/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | export { 9 | A11yConfig, 10 | AxeResults, 11 | axeIncompleteResults, 12 | axeRuntimeExceptionMsgPrefix, 13 | axeVersion, 14 | getAxeRules, 15 | getViolations, 16 | getIncomplete, 17 | } from './axe'; 18 | export { WdioAssertFunction, WdioOptions } from './wdio'; 19 | export { errMsgHeader, ExceptionList } from './format'; 20 | export { 21 | log, 22 | useFilesToBeExempted, 23 | useCustomRules, 24 | processFiles, 25 | registerCustomRules, 26 | writeHtmlFileInPath, 27 | type ErrorElement, 28 | type A11yViolation, 29 | createA11yRuleViolation, 30 | createA11yErrorElements, 31 | } from './helpers'; 32 | -------------------------------------------------------------------------------- /packages/preset-rules/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | export { 9 | defaultPriority, 10 | priorities, 11 | wcagLevels, 12 | adaptA11yConfig, 13 | adaptA11yConfigCustomRules, 14 | adaptA11yConfigIncompleteResults, 15 | getA11yConfig, 16 | RuleInfo, 17 | } from './rules'; 18 | export { defaultRuleset, getDefaultRuleset } from './config'; 19 | export { extended } from './extended'; 20 | export { base } from './base'; 21 | export { full, excludedRules } from './full'; 22 | export { WcagMetadata } from './wcag'; 23 | import changesData from './custom-rules/changes'; 24 | import rulesData from './custom-rules/rules'; 25 | import checkData from './custom-rules/checks'; 26 | export { changesData, rulesData, checkData }; 27 | -------------------------------------------------------------------------------- /packages/preset-rules/src/config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { A11yConfig } from '@sa11y/common'; 8 | import { base } from './base'; 9 | import { extended } from './extended'; 10 | import { full } from './full'; 11 | 12 | // TODO (refactor): Can this file be merged into rules.ts ? 13 | export const ruleSets = new Map(Object.entries({ base: base, extended: extended, full: full })); 14 | 15 | /** 16 | * Get ruleset from environment variable `SA11Y_RULESET` if defined. 17 | * Defaults to `base` ruleset. 18 | */ 19 | export function getDefaultRuleset(): A11yConfig { 20 | const envRuleset = process.env.SA11Y_RULESET; 21 | return envRuleset ? ruleSets.get(envRuleset) || base : base; 22 | } 23 | 24 | export const defaultRuleset = getDefaultRuleset(); 25 | -------------------------------------------------------------------------------- /packages/test-utils/__data__/noA11yIssues.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test Page 5 | 6 | 7 |
    8 |

    This is a test

    9 |

    This is a test page with no violations

    10 | 11 | 12 |
    13 | 14 | 15 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /** @type {import('@jest/types').Config.InitialOptions} */ 9 | module.exports = { 10 | coverageThreshold: { 11 | global: { 12 | branches: 80, 13 | functions: 80, 14 | lines: 80, 15 | statements: 80, 16 | }, 17 | }, 18 | testEnvironment: 'jsdom', 19 | testRunner: 'jest-jasmine2', 20 | testMatch: ['**/__tests__/**/*.[jt]s?(x)'], 21 | // Custom results processor for a11y results. Only affects JSON results file output. 22 | // To be used with jest cli options --json --outputFile 23 | // * e.g. jest --json --outputFile jestResults.json 24 | // Ref: https://jestjs.io/docs/configuration#testresultsprocessor-string 25 | testResultsProcessor: '/packages/jest/dist/resultsProcessor.js', 26 | setupFilesAfterEnv: ['./jest.setup.js'], 27 | }; 28 | -------------------------------------------------------------------------------- /packages/preset-rules/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sa11y/preset-rules", 3 | "version": "5.1.0", 4 | "description": "Accessibility preset rule configs for axe", 5 | "license": "BSD-3-Clause", 6 | "homepage": "https://github.com/salesforce/sa11y/tree/master/packages/preset-rules#readme", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/salesforce/sa11y.git", 10 | "directory": "packages/preset-rules" 11 | }, 12 | "keywords": [ 13 | "accessibility", 14 | "automated testing", 15 | "axe" 16 | ], 17 | "main": "dist/index.js", 18 | "types": "dist/index.d.ts", 19 | "files": [ 20 | "dist/**/*.js", 21 | "dist/**/*.d.ts*" 22 | ], 23 | "dependencies": { 24 | "@sa11y/common": "5.1.0" 25 | }, 26 | "devDependencies": { 27 | "@jest/globals": "28.1.3", 28 | "axe-core": "4.11.0", 29 | "markdown-table-ts": "1.0.3" 30 | }, 31 | "publishConfig": { 32 | "access": "public" 33 | }, 34 | "engines": { 35 | "node": "^20 || ^22 || ^24" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/test-utils/__data__/a11yIssuesVisual.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test Page 5 | 6 | 7 |
    8 |

    This is a test

    9 |

    This is a test page with violations that can be detected only with a real browser

    10 | 11 | 12 |
    13 | 14 | 15 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/jest/__tests__/setup.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { registerSa11yMatcher } from '../src/setup'; 9 | import { expect, jest } from '@jest/globals'; 10 | 11 | describe('jest setup', () => { 12 | registerSa11yMatcher(); 13 | it('should define matcher on expect object', () => { 14 | expect(expect['toBeAccessible']).toBeDefined(); 15 | }); 16 | 17 | /* Skipped: Difficult to mock the global "expect" when we are `import {expect} from '@jest/globals'` */ 18 | it.skip('should throw error when global expect is undefined', () => { 19 | const globalExpect = expect as jest.Expect; 20 | expect(globalExpect).toBeDefined(); 21 | expect(registerSa11yMatcher).not.toThrow(); 22 | try { 23 | global.expect = undefined; 24 | globalExpect(registerSa11yMatcher).toThrowErrorMatchingSnapshot(); 25 | } finally { 26 | global.expect = globalExpect; 27 | } 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/assert/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sa11y/assert", 3 | "version": "5.1.0", 4 | "description": "Provides assertAccessible API to check DOM for accessibility issues", 5 | "license": "BSD-3-Clause", 6 | "homepage": "https://github.com/salesforce/sa11y/tree/master/packages/assert#readme", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/salesforce/sa11y.git", 10 | "directory": "packages/assert" 11 | }, 12 | "keywords": [ 13 | "accessibility", 14 | "automated testing", 15 | "axe" 16 | ], 17 | "main": "dist/assert.js", 18 | "types": "dist/assert.d.ts", 19 | "files": [ 20 | "dist/**/*.js", 21 | "dist/**/*.d.ts*" 22 | ], 23 | "dependencies": { 24 | "@sa11y/common": "5.1.0", 25 | "@sa11y/format": "5.1.0", 26 | "@sa11y/preset-rules": "5.1.0", 27 | "axe-core": "4.11.0" 28 | }, 29 | "devDependencies": { 30 | "@jest/globals": "28.1.3", 31 | "@sa11y/test-utils": "5.1.0" 32 | }, 33 | "publishConfig": { 34 | "access": "public" 35 | }, 36 | "engines": { 37 | "node": "^20 || ^22 || ^24" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/format/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sa11y/format", 3 | "version": "5.1.0", 4 | "description": "Accessibility results re-formatter", 5 | "license": "BSD-3-Clause", 6 | "homepage": "https://github.com/salesforce/sa11y/tree/master/packages/format#readme", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/salesforce/sa11y.git", 10 | "directory": "packages/format" 11 | }, 12 | "keywords": [ 13 | "accessibility", 14 | "automated testing", 15 | "axe", 16 | "results", 17 | "format", 18 | "template" 19 | ], 20 | "main": "dist/index.js", 21 | "types": "dist/index.d.ts", 22 | "files": [ 23 | "dist/**/*.js", 24 | "dist/**/*.d.ts*" 25 | ], 26 | "dependencies": { 27 | "@sa11y/common": "5.1.0", 28 | "@sa11y/preset-rules": "5.1.0", 29 | "axe-core": "4.11.0" 30 | }, 31 | "devDependencies": { 32 | "@jest/globals": "28.1.3", 33 | "@sa11y/test-utils": "5.1.0" 34 | }, 35 | "publishConfig": { 36 | "access": "public" 37 | }, 38 | "engines": { 39 | "node": "^20 || ^22 || ^24" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/matcher/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sa11y/matcher", 3 | "version": "5.1.0", 4 | "description": "Accessibility testing matcher defination", 5 | "license": "BSD-3-Clause", 6 | "homepage": "https://github.com/salesforce/sa11y/tree/master/packages/matcher#readme", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/salesforce/sa11y.git", 10 | "directory": "packages/matcher" 11 | }, 12 | "keywords": [ 13 | "accessibility", 14 | "automated testing", 15 | "axe", 16 | "matcher" 17 | ], 18 | "main": "dist/index.js", 19 | "types": "dist/index.d.ts", 20 | "files": [ 21 | "dist/**/*.js", 22 | "dist/**/*.d.ts*" 23 | ], 24 | "dependencies": { 25 | "@sa11y/assert": "5.1.0", 26 | "@sa11y/format": "5.1.0", 27 | "@sa11y/preset-rules": "5.1.0" 28 | }, 29 | "devDependencies": { 30 | "@jest/globals": "28.1.3", 31 | "@sa11y/common": "5.1.0", 32 | "@sa11y/test-utils": "5.1.0" 33 | }, 34 | "publishConfig": { 35 | "access": "public" 36 | }, 37 | "engines": { 38 | "node": "^20 || ^22 || ^24" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/vitest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sa11y/vitest", 3 | "version": "5.1.0", 4 | "description": "Accessibility testing matcher for vitest", 5 | "license": "BSD-3-Clause", 6 | "type": "module", 7 | "homepage": "https://github.com/salesforce/sa11y/tree/master/packages/vitest#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/salesforce/sa11y.git", 11 | "directory": "packages/vitest" 12 | }, 13 | "keywords": [ 14 | "accessibility", 15 | "automated testing", 16 | "axe", 17 | "vitest", 18 | "matcher" 19 | ], 20 | "main": "dist/index.js", 21 | "types": "dist/index.d.ts", 22 | "files": [ 23 | "dist/**/*.js", 24 | "dist/**/*.d.ts*" 25 | ], 26 | "dependencies": { 27 | "@sa11y/assert": "5.1.0", 28 | "@sa11y/preset-rules": "5.1.0", 29 | "@sa11y/format": "5.1.0", 30 | "@sa11y/matcher": "5.1.0", 31 | "vitest": "^3.1.3", 32 | "@vitest/runner": "^3.1.3" 33 | }, 34 | "devDependencies": { 35 | "@sa11y/common": "5.1.0" 36 | }, 37 | "publishConfig": { 38 | "access": "public" 39 | }, 40 | "engines": { 41 | "node": "^20 || ^22 || ^24" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/common/src/wdio.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { A11yConfig } from './axe'; 8 | import { ExceptionList } from './format'; 9 | 10 | /** 11 | * Optional arguments passed to WDIO APIs 12 | * @param driver - WDIO {@link WebdriverIO.Browser} instance navigated to the page to be checked. Created automatically by WDIO test runner. Might need to be passed in explicitly when other test runners are used. 13 | * @param scope - Element to check for accessibility found using [`browser.$(selector)`](https://webdriver.io/docs/selectors), defaults to the entire document. 14 | * @param rules - {@link A11yConfig} to be used for checking accessibility. Defaults to {@link base} 15 | * @param exceptionList - map of rule id to corresponding CSS targets that needs to be filtered from results 16 | */ 17 | export type WdioOptions = { 18 | driver: WebdriverIO.Browser; 19 | scope?: Promise | WebdriverIO.Element; 20 | rules?: A11yConfig; 21 | exceptionList?: ExceptionList; 22 | }; 23 | 24 | export type WdioAssertFunction = (opts: Partial) => void | Promise; 25 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base", 5 | "helpers:disableTypesNodeMajor", 6 | ":automergeLinters", 7 | ":automergeRequireAllStatusChecks", 8 | ":automergePatch", 9 | ":automergeTesters", 10 | ":automergeTypes", 11 | ":enableVulnerabilityAlertsWithLabel(security)", 12 | ":label(dependencies)", 13 | ":maintainLockFilesWeekly", 14 | ":timezone(America/Los_Angeles)" 15 | ], 16 | "major": { 17 | "extends": [":dependencyDashboardApproval"] 18 | }, 19 | "packageRules": [ 20 | { 21 | "groupName": "node", 22 | "matchPackageNames": ["node"], 23 | "extends": [":disableMajorUpdates"] 24 | }, 25 | { 26 | "matchPackageNames": ["axe-core"], 27 | "extends": [":dependencyDashboardApproval"] 28 | }, 29 | { 30 | "matchDepTypes": ["devDependencies"], 31 | "matchPackagePatterns": ["chromedriver", "cspell"], 32 | "automerge": true 33 | }, 34 | { 35 | "matchPackageNames": ["@vitest/runner"], 36 | "matchPackagePatterns": ["^vitest"], 37 | "updateTypes": ["patch"], 38 | "enabled": false 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /packages/common/src/format.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { AxeResults } from './axe'; 8 | 9 | export const errMsgHeader = 'Accessibility issues found'; 10 | 11 | /** 12 | * Filter to post-process a11y results from axe 13 | */ 14 | export interface Filter { 15 | (violations: AxeResults, ...args: never[]): AxeResults; 16 | } 17 | 18 | // TODO (refactor): constrain rule id to known rule ids e.g using string literal, keyof, in etc 19 | // e.g. https://stackoverflow.com/a/54061487 20 | // const ruleIDs = getRules().map((ruleObj) => ruleObj.ruleId); 21 | // type RuleID = keyof ruleIDs; 22 | // type RuleID = typeof ruleIDs[number]; 23 | // Array of length 2 or greater 24 | type MultiArray = [T, T, ...T[]]; 25 | 26 | // Selectors within a frame 27 | type BaseSelector = string; 28 | 29 | type ShadowDomSelector = MultiArray; 30 | type CrossTreeSelector = BaseSelector | ShadowDomSelector; 31 | type RuleID = string; 32 | type CssSelectors = CrossTreeSelector[]; 33 | /** 34 | * Exception list of map of rule to corresponding css targets that needs to be filtered from a11y results. 35 | */ 36 | export type ExceptionList = Record; 37 | -------------------------------------------------------------------------------- /packages/vitest/src/matcher.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { A11yCheckableContext } from '@sa11y/assert'; 8 | import { A11yConfig } from '@sa11y/common'; 9 | import { defaultRuleset } from '@sa11y/preset-rules'; 10 | import { fakeTimerErrMsg, runA11yCheck, formatOptions } from '@sa11y/matcher'; 11 | 12 | export function isTestUsingFakeTimer(): boolean { 13 | if (typeof setTimeout === 'undefined') { 14 | return false; 15 | } 16 | // Check for Vitest/Sinon fake timers 17 | const isVitestFake = 'clock' in setTimeout; 18 | return isVitestFake; 19 | } 20 | 21 | export async function toBeAccessible( 22 | received: A11yCheckableContext = document, 23 | config: A11yConfig = defaultRuleset 24 | ): Promise<{ pass: boolean; message: () => string }> { 25 | if (isTestUsingFakeTimer()) throw new Error(fakeTimerErrMsg); 26 | 27 | const { isAccessible, a11yError, receivedMsg } = await runA11yCheck(received, config); 28 | 29 | return { 30 | pass: isAccessible, 31 | message: () => 32 | `Expected: no accessibility violations\nReceived: ${receivedMsg}\n\n${a11yError.format(formatOptions)}`, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /packages/jest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sa11y/jest", 3 | "version": "5.1.0", 4 | "description": "Accessibility testing matcher for Jest", 5 | "license": "BSD-3-Clause", 6 | "homepage": "https://github.com/salesforce/sa11y/tree/master/packages/jest#readme", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/salesforce/sa11y.git", 10 | "directory": "packages/jest" 11 | }, 12 | "keywords": [ 13 | "accessibility", 14 | "automated testing", 15 | "axe", 16 | "jest", 17 | "matcher" 18 | ], 19 | "main": "dist/index.js", 20 | "types": "dist/index.d.ts", 21 | "files": [ 22 | "dist/**/*.js", 23 | "dist/**/*.d.ts*" 24 | ], 25 | "dependencies": { 26 | "@jest/test-result": "^27", 27 | "@sa11y/assert": "5.1.0", 28 | "@sa11y/format": "5.1.0", 29 | "@sa11y/preset-rules": "5.1.0", 30 | "@sa11y/matcher": "5.1.0", 31 | "jest-matcher-utils": "^27" 32 | }, 33 | "peerDependencies": { 34 | "jest": ">=27.0.0" 35 | }, 36 | "devDependencies": { 37 | "@jest/globals": "28.1.3", 38 | "@sa11y/common": "5.1.0", 39 | "@sa11y/test-utils": "5.1.0" 40 | }, 41 | "publishConfig": { 42 | "access": "public" 43 | }, 44 | "engines": { 45 | "node": "^20 || ^22 || ^24" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/test-utils/src/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /** 9 | * Cartesian product of arrays 10 | * Ref: https://eddmann.com/posts/cartesian-product-in-javascript/ 11 | */ 12 | // TODO(types): Fix types for the cartesianProduct function - from any to generics 13 | /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument */ 14 | export function cartesianProduct(...sets: Array): Array { 15 | const flatten = (arr: Array): Array => [].concat([], ...arr); 16 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access 17 | return sets.reduce((acc, set) => flatten(acc.map((x: any) => set.map((y: any) => [...x, y]))), [[]]); 18 | /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument */ 19 | } 20 | 21 | /** 22 | * Common Jest setup that sets up JSDOM as required for the tests 23 | */ 24 | export function beforeEachSetup(): void { 25 | document.documentElement.lang = 'en'; // required for a11y lang check 26 | document.body.innerHTML = ''; // reset body content 27 | } 28 | -------------------------------------------------------------------------------- /packages/jest/src/automatic.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { expect } from '@jest/globals'; 8 | import { 9 | defaultAutoCheckOpts, 10 | defaultRenderedDOMSaveOpts, 11 | mutationObserverCallback, 12 | observerOptions, 13 | runAutomaticCheck, 14 | } from '@sa11y/matcher'; 15 | import { isTestUsingFakeTimer } from './matcher'; 16 | import type { AutoCheckOpts, RenderedDOMSaveOpts } from '@sa11y/matcher'; 17 | 18 | export function registerSa11yAutomaticChecks( 19 | opts: AutoCheckOpts = defaultAutoCheckOpts, 20 | renderedDOMSaveOpts: RenderedDOMSaveOpts = defaultRenderedDOMSaveOpts 21 | ): void { 22 | if (!opts.runAfterEach) return; 23 | 24 | const observer = new MutationObserver(mutationObserverCallback); 25 | beforeEach(() => { 26 | if (opts.runDOMMutationObserver) { 27 | observer.observe(document.body, observerOptions); 28 | } 29 | }); 30 | 31 | afterEach(async () => { 32 | if (opts.runDOMMutationObserver) observer.disconnect(); 33 | await runAutomaticCheck( 34 | opts, 35 | renderedDOMSaveOpts, 36 | expect.getState().testPath, 37 | expect.getState().currentTestName, 38 | isTestUsingFakeTimer 39 | ); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /packages/test-integration/README.md: -------------------------------------------------------------------------------- 1 | # `@sa11y/test-integration` 2 | 3 | Private package for integration testing `@sa11y` packages across different environments and test runners. 4 | 5 | 6 | 7 | 8 | - [Overview](#overview) 9 | - [Test Coverage](#test-coverage) 10 | - [Usage](#usage) 11 | 12 | 13 | 14 | ## Overview 15 | 16 | This package ensures that all `@sa11y` packages work correctly together and in different testing environments. It contains integration tests that verify cross-package compatibility and end-to-end functionality. 17 | 18 | ## Test Coverage 19 | 20 | - **Package Integration**: Tests interaction between packages like `@sa11y/jest`, `@sa11y/matcher`, and `@sa11y/format` 21 | - **Environment Compatibility**: Verifies functionality across different Node.js environments 22 | - **Test Runner Compatibility**: Ensures packages work correctly with different test runners 23 | - **API Consistency**: Validates that package APIs work as expected in real-world scenarios 24 | 25 | ## Usage 26 | 27 | This is an internal package used by the Sa11y development team and CI/CD pipeline. It's not intended for external consumption. 28 | 29 | ```bash 30 | # Run integration tests 31 | yarn test 32 | 33 | # Run specific test suites 34 | yarn test --suite jest 35 | yarn test --suite wdio 36 | ``` 37 | -------------------------------------------------------------------------------- /packages/wdio/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sa11y/wdio", 3 | "version": "5.1.0", 4 | "description": "Accessibility testing API for WebdriverIO", 5 | "license": "BSD-3-Clause", 6 | "homepage": "https://github.com/salesforce/sa11y/tree/master/packages/wdio#readme", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/salesforce/sa11y.git", 10 | "directory": "packages/wdio" 11 | }, 12 | "keywords": [ 13 | "accessibility", 14 | "automated test", 15 | "integration test", 16 | "Webdriver", 17 | "WebdriverIO", 18 | "axe" 19 | ], 20 | "main": "dist/index.js", 21 | "types": "dist/index.d.ts", 22 | "files": [ 23 | "dist/**/*.js", 24 | "dist/**/*.d.ts*" 25 | ], 26 | "scripts": { 27 | "test": "wdio run ../../wdio.conf.js", 28 | "test:debug": "DEBUG=true yarn test", 29 | "test:watch": "yarn test --watch" 30 | }, 31 | "dependencies": { 32 | "@sa11y/common": "5.1.0", 33 | "@sa11y/format": "5.1.0", 34 | "@sa11y/preset-rules": "5.1.0", 35 | "axe-core": "4.11.0" 36 | }, 37 | "peerDependencies": { 38 | "webdriverio": ">=6.0.0" 39 | }, 40 | "devDependencies": { 41 | "@sa11y/test-utils": "5.1.0", 42 | "webdriverio": "8.46.0" 43 | }, 44 | "publishConfig": { 45 | "access": "public" 46 | }, 47 | "engines": { 48 | "node": "^20 || ^22 || ^24" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/test-utils/__tests__/test-utils.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { beforeEachSetup, cartesianProduct } from '../src'; 8 | import { expect } from '@jest/globals'; 9 | const testDOMCleanupContent = 'foo'; 10 | 11 | beforeAll(() => { 12 | // Populate DOM to test cleanup after each test 13 | document.body.innerHTML = testDOMCleanupContent; 14 | }); 15 | 16 | beforeEach(() => { 17 | beforeEachSetup(); 18 | }); 19 | 20 | describe('test utils jest setup', () => { 21 | it('should setup jsdom as expected', () => { 22 | expect(document.documentElement.lang).toBe('en'); 23 | }); 24 | 25 | it('should cleanup document', () => { 26 | expect(document.body.innerHTML).not.toEqual(testDOMCleanupContent); 27 | expect(document.body.innerHTML).toBe(''); 28 | }); 29 | }); 30 | 31 | describe('test utils cartesian product', () => { 32 | const numArr = [0, 1, 2, 3, 4, 5]; 33 | const alphabetArr = ['a', 'b', 'c', 'd', 'e']; 34 | it('should work as expected', () => { 35 | expect(cartesianProduct(numArr, alphabetArr)).toMatchSnapshot(); 36 | expect(cartesianProduct(alphabetArr, numArr)).toMatchSnapshot(); 37 | expect(cartesianProduct(numArr, numArr)).toMatchSnapshot(); 38 | expect(cartesianProduct(alphabetArr, alphabetArr)).toMatchSnapshot(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/vitest/src/automatic.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { beforeEach, afterEach, expect } from 'vitest'; 8 | import { 9 | defaultAutoCheckOpts, 10 | defaultRenderedDOMSaveOpts, 11 | mutationObserverCallback, 12 | observerOptions, 13 | runAutomaticCheck, 14 | } from '@sa11y/matcher'; 15 | import type { AutoCheckOpts, RenderedDOMSaveOpts } from '@sa11y/matcher'; 16 | import { isTestUsingFakeTimer } from './matcher.js'; 17 | 18 | export function registerSa11yAutomaticChecks( 19 | opts: AutoCheckOpts = defaultAutoCheckOpts, 20 | renderedDOMSaveOpts: RenderedDOMSaveOpts = defaultRenderedDOMSaveOpts 21 | ): void { 22 | if (!opts.runAfterEach) return; 23 | // TODO (fix): Make registration idempotent 24 | const observer = new MutationObserver(mutationObserverCallback); 25 | 26 | beforeEach(() => { 27 | if (opts.runDOMMutationObserver) { 28 | observer.observe(document.body, observerOptions); 29 | } 30 | }); 31 | 32 | afterEach(async () => { 33 | if (opts.runDOMMutationObserver) observer.disconnect(); // stop mutation observer 34 | await runAutomaticCheck( 35 | opts, 36 | renderedDOMSaveOpts, 37 | expect.getState().testPath, 38 | expect.getState().currentTestName, 39 | isTestUsingFakeTimer 40 | ); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Salesforce.com, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, 7 | are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, 10 | this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of Salesforce.com nor the names of its contributors may be used 17 | to endorse or promote products derived from this software without specific prior 18 | written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 22 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 23 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 24 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 25 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 26 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 28 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /packages/test-integration/__tests__/jest.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { beforeEachSetup, domWithA11yIssues, domWithNoA11yIssues, domWithVisualA11yIssues } from '@sa11y/test-utils'; 9 | import { registerSa11yMatcher } from '@sa11y/jest'; 10 | import { expect } from '@jest/globals'; 11 | 12 | beforeAll(registerSa11yMatcher); 13 | 14 | beforeEach(beforeEachSetup); 15 | 16 | describe('integration test @sa11y/jest', () => { 17 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 18 | it('should have a11y matchers working with setup in jest.config.js', async () => { 19 | expect(expect.toBeAccessible).toBeDefined(); 20 | document.body.innerHTML = domWithNoA11yIssues; 21 | await expect(document).toBeAccessible(); 22 | }); 23 | 24 | it('should throw error for inaccessible dom', async () => { 25 | document.body.innerHTML = domWithA11yIssues; 26 | await expect(expect(document).toBeAccessible()).rejects.toThrow(); 27 | }); 28 | 29 | it('will not throw error for audio video color-contrast', async () => { 30 | document.body.innerHTML = domWithVisualA11yIssues; 31 | // Even though the dom has a11y issues w.r.t color contrast and audio/video 32 | // elements etc, Jest/JSDOM will not able to detect them 33 | await expect(document).toBeAccessible(); 34 | }); 35 | /* eslint-enable @typescript-eslint/no-unsafe-call */ 36 | }); 37 | -------------------------------------------------------------------------------- /packages/matcher/src/matcher.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { A11yCheckableContext, assertAccessible } from '@sa11y/assert'; 8 | import { A11yError, Options } from '@sa11y/format'; 9 | import { defaultRuleset, adaptA11yConfig } from '@sa11y/preset-rules'; 10 | import { A11yConfig } from '@sa11y/common'; 11 | 12 | const expectedMsg = `0 issues`; 13 | 14 | export const formatOptions: Options = { 15 | a11yViolationIndicator: '⭕', 16 | helpUrlIndicator: '🔗', 17 | highlighter: (text) => text, 18 | deduplicate: false, 19 | }; 20 | 21 | export const fakeTimerErrMsg = 22 | 'Cannot run accessibility check when fake timer is in use. ' + 23 | 'Switch to real timer before invoking accessibility check.'; 24 | 25 | export async function runA11yCheck( 26 | received: A11yCheckableContext = document, 27 | config: A11yConfig = defaultRuleset 28 | ): Promise<{ isAccessible: boolean; a11yError: A11yError; receivedMsg: string }> { 29 | let isAccessible = true; 30 | let a11yError: A11yError = new A11yError([], []); 31 | let receivedMsg = expectedMsg; 32 | 33 | try { 34 | await assertAccessible(received, adaptA11yConfig(config)); 35 | } catch (e) { 36 | if (e instanceof A11yError) { 37 | a11yError = e; 38 | isAccessible = false; 39 | receivedMsg = `${a11yError.length} issues`; 40 | } else { 41 | throw e; 42 | } 43 | } 44 | 45 | return { isAccessible, a11yError, receivedMsg }; 46 | } 47 | -------------------------------------------------------------------------------- /packages/jest/src/setup.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { expect } from '@jest/globals'; 8 | import { 9 | registerRemoveChild, 10 | defaultSa11yOpts, 11 | improvedChecksFilter, 12 | updateAutoCheckOpts, 13 | registerCustomSa11yRules, 14 | } from '@sa11y/matcher'; 15 | import { registerSa11yAutomaticChecks } from './automatic'; 16 | import { toBeAccessible } from './matcher'; 17 | 18 | export function setup(opts = defaultSa11yOpts): void { 19 | const testPath = expect.getState().testPath ?? ''; 20 | const ignoreImprovedChecks = improvedChecksFilter.some((fileName) => 21 | testPath.toLowerCase().includes(fileName.toLowerCase()) 22 | ); 23 | if (process.env.SA11Y_AUTO && !ignoreImprovedChecks) { 24 | registerRemoveChild(); 25 | } 26 | registerSa11yMatcher(); 27 | registerCustomSa11yRules(); 28 | const autoCheckOpts = opts.autoCheckOpts; 29 | updateAutoCheckOpts(autoCheckOpts); 30 | 31 | const renderedDOMSaveOpts = opts.renderedDOMSaveOpts; 32 | registerSa11yAutomaticChecks(autoCheckOpts, renderedDOMSaveOpts); 33 | } 34 | export function registerSa11yMatcher(): void { 35 | if (expect !== undefined) { 36 | expect.extend({ toBeAccessible }); 37 | } else { 38 | throw new Error( 39 | "Unable to find Jest's expect function." + 40 | '\nPlease check your Jest installation and that you have added @sa11y/jest correctly to your jest configuration.' + 41 | '\nSee https://github.com/salesforce/sa11y/tree/master/packages/jest#readme for help.' 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/test-utils/src/wdio.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | // Ignore jest code coverage for WDIO test-utils 9 | /* istanbul ignore file */ 10 | 11 | import { WdioAssertFunction, WdioOptions, axeRuntimeExceptionMsgPrefix, errMsgHeader } from '@sa11y/common'; 12 | 13 | export async function checkA11yErrorWdio( 14 | assertFunc: WdioAssertFunction, 15 | expectNumA11yIssues = 0, 16 | options: Partial = {} 17 | ): Promise { 18 | // TODO (debug): setting expected number of assertions doesn't seem to be working correctly in mocha 19 | // https://webdriver.io/docs/assertion.html 20 | // Check mocha docs: https://mochajs.org/#assertions 21 | // Checkout Jasmine ? https://webdriver.io/docs/frameworks.html 22 | // expect.assertions(99999); // still passes ??? 23 | 24 | // TODO (debug): Not able to get the expect().toThrow() with async functions to work with wdio test runner 25 | // hence using the longer try.. catch alternative 26 | // expect(async () => await assertAccessible()).toThrow(); 27 | let err: Error = new Error(); 28 | try { 29 | await assertFunc(options); 30 | } catch (e) { 31 | err = e as Error; 32 | } 33 | expect(err).toBeTruthy(); 34 | expect(err.message).not.toContain(axeRuntimeExceptionMsgPrefix); 35 | 36 | if (expectNumA11yIssues > 0) { 37 | expect(err).not.toEqual(new Error()); 38 | expect(err.toString()).toContain(`${expectNumA11yIssues} ${errMsgHeader}`); 39 | } else { 40 | expect(err).toEqual(new Error()); 41 | expect(err.toString()).not.toContain(errMsgHeader); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/vitest/src/setup.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { expect } from 'vitest'; 8 | import { 9 | registerRemoveChild, 10 | defaultSa11yOpts, 11 | improvedChecksFilter, 12 | updateAutoCheckOpts, 13 | registerCustomSa11yRules, 14 | } from '@sa11y/matcher'; 15 | import { registerSa11yAutomaticChecks } from './automatic.js'; 16 | import { toBeAccessible } from './matcher.js'; 17 | 18 | export function setup(opts = defaultSa11yOpts): void { 19 | const testPath = expect.getState().testPath ?? ''; 20 | const ignoreImprovedChecks = improvedChecksFilter.some((fileName) => 21 | testPath.toLowerCase().includes(fileName.toLowerCase()) 22 | ); 23 | 24 | if (process.env.SA11Y_AUTO && !ignoreImprovedChecks) { 25 | registerRemoveChild(); 26 | } 27 | 28 | registerSa11yMatcher(); 29 | registerCustomSa11yRules(); 30 | 31 | const autoCheckOpts = opts.autoCheckOpts; 32 | updateAutoCheckOpts(autoCheckOpts); 33 | 34 | const renderedDOMSaveOpts = opts.renderedDOMSaveOpts; 35 | registerSa11yAutomaticChecks(autoCheckOpts, renderedDOMSaveOpts); 36 | } 37 | 38 | export function registerSa11yMatcher(): void { 39 | if (expect !== undefined) { 40 | expect.extend({ toBeAccessible }); 41 | } else { 42 | throw new Error( 43 | "Unable to find vitest's expect function." + 44 | '\nPlease check your vitest installation and that you have added @sa11y/vitest correctly to your vitest configuration.' + 45 | '\nSee https://github.com/salesforce/sa11y/tree/master/packages/vitest#readme for help.' 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/format/README.md: -------------------------------------------------------------------------------- 1 | # `@sa11y/format` 2 | 3 | Format accessibility results from axe 4 | 5 | 6 | 7 | 8 | - [Usage](#usage) 9 | - [Basic Formatting](#basic-formatting) 10 | - [Exception List Filtering](#exception-list-filtering) 11 | - [Result Processing](#result-processing) 12 | 13 | 14 | 15 | ## Usage 16 | 17 | ### Basic Formatting 18 | 19 | ```javascript 20 | import axe from 'axe-core'; 21 | import { A11yError } from '@sa11y/format'; 22 | 23 | const results = await axe.run(); 24 | console.log(A11yError.checkAndThrow(results.violations)); 25 | ``` 26 | 27 | ### Exception List Filtering 28 | 29 | Filter out specific accessibility violations based on rule ID and CSS selectors: 30 | 31 | ```javascript 32 | import { exceptionListFilter, exceptionListFilterSelectorKeywords } from '@sa11y/format'; 33 | 34 | // Filter by rule ID and specific CSS selectors 35 | const exceptionList = { 36 | 'color-contrast': ['.btn-secondary', '.text-muted'], 37 | 'landmark-one-main': ['body'], 38 | }; 39 | const filteredResults = exceptionListFilter(violations, exceptionList); 40 | 41 | // Filter by selector keywords 42 | const keywords = ['known-issue', 'legacy-component']; 43 | const keywordFilteredResults = exceptionListFilterSelectorKeywords(violations, keywords); 44 | ``` 45 | 46 | ### Result Processing 47 | 48 | Process and enhance accessibility results with WCAG metadata: 49 | 50 | ```javascript 51 | import { A11yResult, appendWcag } from '@sa11y/format'; 52 | 53 | // Process individual result 54 | const processedResult = new A11yResult(violationData); 55 | 56 | // Add WCAG metadata to results 57 | const resultsWithWcag = appendWcag(violations); 58 | ``` 59 | -------------------------------------------------------------------------------- /packages/vitest/src/groupViolationResultsProcessor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | // vitestResultsProcessor.ts 8 | 9 | import type { RunnerTestFile } from 'vitest/node'; 10 | import type { Test } from '@vitest/runner'; 11 | import { A11yError } from '@sa11y/format'; 12 | import { processA11yDetailsAndMessages } from '@sa11y/matcher'; 13 | 14 | export function vitestResultsProcessor(testFiles: RunnerTestFile[]): RunnerTestFile[] { 15 | for (const file of testFiles) { 16 | for (const task of file.tasks) { 17 | const test = task as Test; 18 | 19 | if (test.result?.state !== 'fail') { 20 | continue; 21 | } 22 | 23 | const messages: string[] = []; 24 | const details = test.result.errors ?? []; 25 | 26 | const updatedDetails = details.filter((failure) => { 27 | const error = failure as A11yError; 28 | if (error?.name === A11yError.name) { 29 | processA11yDetailsAndMessages(error, messages); 30 | return true; 31 | } 32 | return false; 33 | }); 34 | 35 | if (updatedDetails.length === 0) { 36 | test.result.state = 'pass'; 37 | test.result.errors = []; 38 | } else { 39 | test.result.errors = updatedDetails.map((err, index) => { 40 | const error = err as A11yError; 41 | if (messages[index]) { 42 | error.message += `\n${messages[index]}`; 43 | } 44 | return error; 45 | }); 46 | } 47 | } 48 | } 49 | 50 | return testFiles; 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: 'CodeQL' 2 | 3 | on: 4 | push: 5 | branches: [master, media] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 22 * * 2' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | permissions: 18 | # required for all workflows 19 | security-events: write 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 24 | 25 | # Initializes the CodeQL tools for scanning. 26 | - name: Initialize CodeQL 27 | uses: github/codeql-action/init@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2.28.1 28 | # Override language selection by uncommenting this and choosing your languages 29 | with: 30 | languages: javascript 31 | 32 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 33 | # If this step fails, then you should remove it and run the build manually (see below) 34 | - name: Autobuild 35 | uses: github/codeql-action/autobuild@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2.28.1 36 | 37 | # ℹ️ Command-line programs to run using the OS shell. 38 | # 📚 https://git.io/JvXDl 39 | 40 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 41 | # and modify them (or add more) to build your code if your project 42 | # uses a compiled language 43 | 44 | #- run: | 45 | # make bootstrap 46 | # make release 47 | 48 | - name: Perform CodeQL Analysis 49 | uses: github/codeql-action/analyze@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2.28.1 50 | -------------------------------------------------------------------------------- /packages/browser-lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sa11y/browser-lib", 3 | "version": "5.1.0", 4 | "description": "Provides a minified version of selected `@sa11y` libraries to be injected into a browser (using webdriver) and executed from integration testing workflows.", 5 | "license": "BSD-3-Clause", 6 | "homepage": "https://github.com/salesforce/sa11y/tree/master/packages/browser-lib#readme", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/salesforce/sa11y.git", 10 | "directory": "packages/browser-lib" 11 | }, 12 | "keywords": [ 13 | "accessibility", 14 | "automated test", 15 | "integration test", 16 | "browser", 17 | "webdriver", 18 | "iife", 19 | "axe" 20 | ], 21 | "browser": "dist/sa11y.min.js", 22 | "files": [ 23 | "dist/**/*.js" 24 | ], 25 | "scripts": { 26 | "build": "rollup -c", 27 | "build:debug": "DEBUG=true yarn build", 28 | "build:watch": "yarn build --watch", 29 | "test": "wdio run ../../wdio.conf.js", 30 | "test:debug": "DEBUG=true yarn test", 31 | "test:watch": "yarn test --watch" 32 | }, 33 | "bugs": { 34 | "url": "https://github.com/salesforce/sa11y/issues" 35 | }, 36 | "dependencies": { 37 | "@sa11y/format": "5.1.0", 38 | "@sa11y/preset-rules": "5.1.0", 39 | "axe-core": "4.11.0" 40 | }, 41 | "devDependencies": { 42 | "@rollup/plugin-commonjs": "23.0.7", 43 | "@rollup/plugin-node-resolve": "15.3.1", 44 | "@rollup/plugin-replace": "5.0.7", 45 | "@sa11y/common": "5.1.0", 46 | "@sa11y/test-utils": "5.1.0", 47 | "rollup": "3.29.5", 48 | "rollup-plugin-polyfill-node": "0.13.0", 49 | "rollup-plugin-progress": "1.1.2", 50 | "rollup-plugin-sizes": "1.1.0", 51 | "@rollup/plugin-terser": "0.4.4", 52 | "rollup-plugin-typescript2": "0.36.0" 53 | }, 54 | "publishConfig": { 55 | "access": "public" 56 | }, 57 | "engines": { 58 | "node": "^20 || ^22 || ^24" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/assert/README.md: -------------------------------------------------------------------------------- 1 | # `@sa11y/assert` 2 | 3 | Provides assertAccessible API to check DOM for accessibility issues 4 | 5 | 6 | 7 | 8 | - [Usage](#usage) 9 | - [Basic Usage](#basic-usage) 10 | - [Getting Results](#getting-results) 11 | - [Environment Variables](#environment-variables) 12 | 13 | 14 | 15 | ## Usage 16 | 17 | ### Basic Usage 18 | 19 | ```javascript 20 | import { assertAccessible } from '@sa11y/assert'; 21 | import { full } from '@sa11y/preset-rules'; 22 | 23 | // Setup DOM in the state to be tested for accessibility 24 | // ... 25 | // Assert that the current dom has no a11y issues 26 | // Defaults to using 27 | // - default document context e.g. JSDOM in Jest 28 | // - base ruleset from @sa11y/preset-rules and 29 | // - A11yError.checkAndThrow from @sa11y/format 30 | await assertAccessible(); 31 | 32 | // Can be used to test accessibility of a specific HTML element 33 | const elem = document.getElementById('foo'); 34 | await assertAccessible(elem); 35 | 36 | // Can be overridden to use custom dom, ruleset or formatter 37 | // - Specifying null for formatter will result in using JSON stringify 38 | await assertAccessible(document, full, null); 39 | ``` 40 | 41 | ### Getting Results 42 | 43 | For advanced use cases where you want to get the accessibility results without throwing an error: 44 | 45 | ```javascript 46 | import { getA11yResultsJSDOM, getViolationsJSDOM, getIncompleteJSDOM } from '@sa11y/assert'; 47 | import { extended } from '@sa11y/preset-rules'; 48 | 49 | // Get both violations and incomplete results 50 | const allResults = await getA11yResultsJSDOM(document, extended, true); 51 | 52 | // Get only violations (default behavior) 53 | const violations = await getViolationsJSDOM(document, extended); 54 | 55 | // Get only incomplete results 56 | const incomplete = await getIncompleteJSDOM(document, extended); 57 | ``` 58 | 59 | ### Environment Variables 60 | 61 | - `SELECTOR_FILTER_KEYWORDS`: Comma-separated list of keywords to filter out violations by CSS selector 62 | -------------------------------------------------------------------------------- /packages/preset-rules/src/custom-rules/rules.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import axe from 'axe-core'; 8 | const rulesData = [ 9 | { 10 | id: 'sa11y-Keyboard', 11 | metadata: { 12 | description: 'Element is not keyboard operable', 13 | help: "The following button element '+ ele.innerText+ ' missing keyboard operability. To fix add tabindex='0' attribute and appropriate keyboard event handler.", 14 | helpUrl: '', 15 | }, 16 | selector: 17 | "[role='button']:not(a[href],button,input,select,area[href],textarea,[contentEditable=true],[disabled],details)", 18 | any: [], 19 | all: ['sa11y-Keyboard-check'], 20 | none: [], 21 | tags: ['wcag22aa', 'wcag211'], 22 | }, 23 | { 24 | id: 'Resize-reflow-textoverflow', 25 | selector: '*', 26 | enabled: true, 27 | any: ['Resize-reflow-textoverflow-check'], 28 | all: [], 29 | metadata: { 30 | description: 'Ensure Ellipses are not present as text is truncated.', 31 | help: 'Text elements do not have ellipsis as text is truncated.', 32 | helpUrl: 'https://example.com/custom-rule-help', 33 | impact: 'moderate', 34 | tags: ['wcag1410', 'custom'], 35 | }, 36 | }, 37 | { 38 | id: 'sa11y-Keyboard-button', 39 | selector: 40 | "[role='button']:not(a[href],button,input,select,area[href],textarea,[contentEditable=true],[disabled],details)", 41 | enabled: true, 42 | any: ['sa11y-Keyboard-button-check'], 43 | all: [], 44 | metadata: { 45 | description: 'Element is not keyboard operable', 46 | help: "Fix any one of the following :\n \ 47 | 1.The button element missing keyboard operability. To fix add tabindex='0' attribute and appropriate keyboard event handler.\n \ 48 | 2.Remove role='button' attribute", 49 | helpUrl: '', 50 | 51 | impact: 'critical', 52 | tags: ['wcag211', 'custom'], 53 | }, 54 | }, 55 | ]; 56 | 57 | export default rulesData as axe.Rule[]; 58 | -------------------------------------------------------------------------------- /packages/preset-rules/src/docgen.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { getAxeRules, log } from '@sa11y/common'; 8 | import { baseRulesInfo } from './base'; 9 | import { extendedRulesInfo } from './extended'; 10 | import * as fs from 'fs'; 11 | import { getMarkdownTable } from 'markdown-table-ts'; 12 | 13 | /** 14 | * Generate markdown table of rules with the rules metadata. 15 | * Called from the rules tests. 16 | * @param rulesInfo - preset-rule to generate docs for. Defaults to 17 | * @param updateReadmePath - append given file with generated rules md doc 18 | */ 19 | export function getRulesDoc(rulesInfo = extendedRulesInfo, updateReadmePath = ''): string { 20 | const rulesDocRows: string[][] = []; 21 | // Markers used to denote if a rule belongs in base, extended ruleset 22 | const no = '✖️'; 23 | const yes = '✅'; 24 | const axeRules = getAxeRules(); 25 | for (const [ruleID, ruleMetadata] of rulesInfo.entries()) { 26 | const axeRule = axeRules.get(ruleID); 27 | if (!axeRule) throw new Error(`Unable to find rule: ${ruleID}`); 28 | 29 | rulesDocRows.push([ 30 | `[${ruleID}](${axeRule.helpUrl.split('?')[0]})`, // remove URL referrer for cleaner/shorter doc 31 | axeRule.description.replace(/ extends jest.CustomMatcher { 20 | toBeAccessible(config?: A11yConfig): Promise; 21 | } 22 | } 23 | } 24 | 25 | /** 26 | * Detect if fake timer is being used in a jest test. 27 | * Fake timers result in axe timeout https://github.com/dequelabs/axe-core/issues/3055 28 | * Workaround until underlying issue can be fixed in axe. 29 | * Ref: https://github.com/facebook/jest/issues/10555 30 | */ 31 | export function isTestUsingFakeTimer(): boolean { 32 | return ( 33 | typeof jest !== 'undefined' && 34 | typeof setTimeout !== 'undefined' && 35 | // eslint-disable-next-line no-prototype-builtins 36 | (setTimeout.hasOwnProperty('_isMockFunction') || setTimeout.hasOwnProperty('clock')) 37 | ); 38 | } 39 | 40 | export async function toBeAccessible( 41 | received: A11yCheckableContext = document, 42 | config: A11yConfig = defaultRuleset 43 | ): Promise { 44 | if (isTestUsingFakeTimer()) throw new Error(fakeTimerErrMsg); 45 | 46 | const { isAccessible, a11yError, receivedMsg } = await runA11yCheck(received, config); 47 | return { 48 | pass: isAccessible, 49 | message: (): string => 50 | matcherHint(`toBeAccessible`) + 51 | `\n\nExpected: no accessibility violations\nReceived: ${receivedMsg}\n\n${a11yError.format({ 52 | ...formatOptions, 53 | highlighter: printReceived, 54 | })}`, 55 | a11yError, 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /packages/common/src/axe.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import * as axe from 'axe-core'; 9 | import { resultGroups, RuleMetadata } from 'axe-core'; 10 | 11 | export const axeRuntimeExceptionMsgPrefix = 'Error running accessibility checks using axe:'; 12 | 13 | export const axeVersion: string | undefined = axe.version; 14 | 15 | export type AxeResults = axe.Result[] | axeIncompleteResults[]; 16 | 17 | /** 18 | * Interface that represents a function that runs axe and returns violations 19 | */ 20 | interface AxeRunner { 21 | (): Promise; 22 | } 23 | export interface axeIncompleteResults extends axe.Result { 24 | message?: string; 25 | } 26 | 27 | /** 28 | * A11yConfig defines options to run accessibility checks using axe specifying list of rules to test 29 | */ 30 | export interface A11yConfig extends axe.RunOptions { 31 | runOnly: { 32 | type: 'rule'; 33 | values: string[]; 34 | }; 35 | resultTypes: resultGroups[]; 36 | } 37 | 38 | /** 39 | * Get results by running axe with given function 40 | * @param axeRunner - function satisfying AxeRunner interface 41 | */ 42 | export async function getA11yResults(axeRunner: AxeRunner): Promise { 43 | let results; 44 | try { 45 | results = await axeRunner(); 46 | } catch (e) { 47 | throw new Error(`${axeRuntimeExceptionMsgPrefix} ${(e as Error).message}`); 48 | } 49 | return results; 50 | } 51 | 52 | /** 53 | * Get incomplete by running axe with given function 54 | * @param axeRunner - function satisfying AxeRunner interface 55 | */ 56 | export async function getIncomplete(axeRunner: AxeRunner): Promise { 57 | return getA11yResults(axeRunner); 58 | } 59 | 60 | /** 61 | * Get violations by running axe with given function 62 | * @param axeRunner - function satisfying AxeRunner interface 63 | */ 64 | export async function getViolations(axeRunner: AxeRunner): Promise { 65 | return getA11yResults(axeRunner); 66 | } 67 | 68 | /** 69 | * Return list of axe rules as a map of rule id to corresponding metadata 70 | */ 71 | export function getAxeRules(): Map { 72 | const axeRules = new Map(); 73 | axe.getRules().forEach((rule) => axeRules.set(rule.ruleId, rule)); 74 | return axeRules; 75 | } 76 | -------------------------------------------------------------------------------- /packages/test-utils/README.md: -------------------------------------------------------------------------------- 1 | # `@sa11y/test-utils` 2 | 3 | Private package providing test utilities, mock data, and common testing patterns for `@sa11y` packages. 4 | 5 | 6 | 7 | 8 | - [Usage](#usage) 9 | - [Basic Setup](#basic-setup) 10 | - [DOM Fixtures](#dom-fixtures) 11 | - [WebdriverIO Utilities](#webdriverio-utilities) 12 | - [Test Data](#test-data) 13 | 14 | 15 | 16 | ## Usage 17 | 18 | ### Basic Setup 19 | 20 | ```javascript 21 | import { beforeEachSetup, domWithA11yIssues, domWithNoA11yIssues } from '@sa11y/test-utils'; 22 | import { registerSa11yMatcher } from '@sa11y/jest'; 23 | 24 | beforeAll(() => { 25 | registerSa11yMatcher(); 26 | }); 27 | 28 | beforeEach(() => { 29 | beforeEachSetup(); 30 | }); 31 | 32 | describe('...', () => { 33 | it('...', async () => { 34 | document.body.innerHTML = domWithNoA11yIssues; 35 | await expect(document).toBeAccessible(); 36 | 37 | document.body.innerHTML = domWithA11yIssues; 38 | await expect(document).not.toBeAccessible(); 39 | }); 40 | }); 41 | ``` 42 | 43 | ### DOM Fixtures 44 | 45 | The package provides pre-built DOM fixtures for testing: 46 | 47 | ```javascript 48 | import { 49 | domWithA11yIssues, 50 | domWithNoA11yIssues, 51 | htmlFileWithA11yIssues, 52 | htmlFileWithNoA11yIssues, 53 | } from '@sa11y/test-utils'; 54 | 55 | // DOM strings with known accessibility issues 56 | document.body.innerHTML = domWithA11yIssues; 57 | 58 | // DOM strings with no accessibility issues 59 | document.body.innerHTML = domWithNoA11yIssues; 60 | 61 | // HTML file paths for testing with browser environments 62 | await browser.url(htmlFileWithA11yIssues); 63 | ``` 64 | 65 | ### WebdriverIO Utilities 66 | 67 | For WebdriverIO integration testing: 68 | 69 | ```javascript 70 | import { beforeEachSetupWdio, cartesianProduct, audioURL, videoURL } from '@sa11y/test-utils'; 71 | 72 | describe('WDIO tests', () => { 73 | beforeEach(() => { 74 | beforeEachSetupWdio(); 75 | }); 76 | 77 | // Use media URLs for testing audio/video accessibility 78 | // Use cartesianProduct for generating test combinations 79 | }); 80 | ``` 81 | 82 | ### Test Data 83 | 84 | The package includes various test data and utilities: 85 | 86 | - **Media URLs**: Audio and video file URLs for testing multimedia accessibility 87 | - **Cartesian Product**: Utility for generating test case combinations 88 | - **Mock Data**: Various mock objects and data structures for testing 89 | -------------------------------------------------------------------------------- /packages/browser-lib/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import * as axe from 'axe-core'; 9 | import { exceptionListFilter, appendWcag } from '@sa11y/format'; 10 | import { registerCustomRules } from '@sa11y/common'; 11 | import { 12 | defaultRuleset, 13 | changesData, 14 | rulesData, 15 | checkData, 16 | adaptA11yConfigIncompleteResults, 17 | } from '@sa11y/preset-rules'; 18 | export { base, extended, full, adaptA11yConfigIncompleteResults } from '@sa11y/preset-rules'; 19 | export const namespace = 'sa11y'; 20 | 21 | /** 22 | * Wrapper function to check accessibility to be invoked after the sa11y minified JS file 23 | * is injected into the browser. 24 | * 25 | * @param scope - Scope of the analysis, defaults to the document. 26 | * @param rules - Preset sa11y rules, defaults to {@link base}. 27 | * @param exceptionList - Mapping of rule to CSS selectors to be filtered out using {@link exceptionListFilter}. 28 | * @param addWcagInfo - Flag to add WCAG information to the results, defaults to false. 29 | * @param enableIncompleteResults - Flag to include incomplete results in the analysis, defaults to false. 30 | */ 31 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types 32 | export async function checkAccessibility( 33 | scope = document, 34 | rules = defaultRuleset, 35 | exceptionList = {}, 36 | addWcagInfo = true, 37 | reportType: 'violations' | 'incomplete' = 'violations' 38 | ) { 39 | // TODO (debug): Adding type annotations to arguments and return type results in error: 40 | // "[!] Error: Unexpected token" in both rollup-plugin-typescript2 and @rollup/plugin-typescript. 41 | // Compiling index.ts with tsc and using the dist/index.js file results in an error when importing 42 | // the "namespace" variable. This would probably be easier to fix, potentially allowing us to 43 | // remove the rollup TypeScript plugins. 44 | 45 | // To register custom rules 46 | registerCustomRules(changesData, rulesData, checkData); 47 | 48 | // Adapt rules if incomplete results are enabled 49 | if (reportType === 'incomplete') { 50 | rules = adaptA11yConfigIncompleteResults(rules); 51 | } 52 | const results = await axe.run(scope || document, rules); 53 | const filteredResults = exceptionListFilter(results[reportType], exceptionList); 54 | 55 | if (addWcagInfo) { 56 | appendWcag(filteredResults); 57 | } 58 | return JSON.stringify(filteredResults); 59 | } 60 | -------------------------------------------------------------------------------- /packages/preset-rules/__tests__/wcag.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { WcagMetadata } from '../src'; 8 | import { Result } from 'axe-core'; 9 | import { defaultWcagVersion } from '../src/rules'; 10 | import { extendedRulesInfo } from '../src/extended'; 11 | import { expect } from '@jest/globals'; 12 | 13 | // input tags, expected version, expected level, expected SC 14 | const noErrorCases = [ 15 | [['wcag2a'], '2.0', 'A', WcagMetadata.defaultSC], 16 | [['wcag21aaa'], '2.1', 'AAA', WcagMetadata.defaultSC], 17 | [['wcag222', 'wcag21aa'], '2.1', 'AA', '2.2.2'], 18 | ]; 19 | 20 | const errorCases = [ 21 | [[], undefined, '', WcagMetadata.defaultSC], 22 | [[WcagMetadata.defaultSC], undefined, '', WcagMetadata.defaultSC], 23 | [['wcag222'], undefined, '', '2.2.2'], 24 | [['foo', 'bar'], undefined, '', WcagMetadata.defaultSC], 25 | [['wcag2foo', 'wcag21bar'], undefined, '', WcagMetadata.defaultSC], 26 | ]; 27 | 28 | describe('WCAG Metadata extractor', () => { 29 | it.each([...noErrorCases, ...errorCases])( 30 | 'should extract WCAG version and level (arg:%p)', 31 | (tags: string[], wcagVersion: string, wcagLevel: string, successCriteria: string) => { 32 | const wcag = new WcagMetadata({ tags: tags } as Result); 33 | expect(wcag.wcagVersion).toBe(wcagVersion); 34 | expect(wcag.wcagLevel).toBe(wcagLevel); 35 | expect(wcag.successCriteria).toBe(successCriteria); 36 | } 37 | ); 38 | 39 | it.each(noErrorCases)( 40 | 'should format WCAG metadata when required data is present (arg:%p)', 41 | (tags: string[], wcagVersion: string, wcagLevel: string, successCriteria: string) => { 42 | const wcag = new WcagMetadata({ tags: tags } as Result).toString(); 43 | expect(wcag).toMatchSnapshot(); 44 | expect(wcag).toContain(successCriteria); 45 | } 46 | ); 47 | 48 | it.each([ 49 | 'area-alt', // base rule 50 | 'table-duplicate-name', // extended rule without WCAG SC 51 | ])('should populate WCAG metadata as expected for rule (arg:%p)', (ruleID) => { 52 | const wcag = new WcagMetadata({ id: ruleID, tags: [] } as Result); 53 | const ruleInfo = extendedRulesInfo.get(ruleID); 54 | expect(wcag.wcagVersion).toBe(defaultWcagVersion); 55 | expect(wcag.wcagLevel).toBe(ruleInfo.wcagLevel); 56 | expect(wcag.successCriteria).toBe(ruleInfo.wcagSC); 57 | expect(wcag.priority).toBe(ruleInfo.priority); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /packages/test-utils/src/test-data.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import fs from 'fs'; 9 | import path from 'path'; 10 | 11 | const dataDir = path.resolve(__dirname, '../__data__/'); 12 | 13 | // DOM with a11y issues 14 | export const domWithA11yIssuesBodyID = 'dom-with-issues'; 15 | const fileWithA11yIssues = path.resolve(dataDir, 'a11yIssues.html'); 16 | export const customRulesFilePath = path.resolve(dataDir, 'sa11y-custom-rules.json'); 17 | export const domWithA11yCustomIssuesPath = path.resolve(dataDir, 'a11yCustomIssues.html'); 18 | export const domWithA11yIncompleteIssuesPath = path.resolve(dataDir, 'a11yIncompleteIssues.html'); 19 | 20 | const fileWithDescendancyA11yIssues = path.resolve(dataDir, 'descendancyA11yIssues.html'); 21 | export const htmlFileWithA11yIssues = 'file:///' + fileWithA11yIssues; 22 | export const domWithA11yIssues = fs.readFileSync(fileWithA11yIssues).toString(); 23 | export const domWithA11yCustomIssues = fs.readFileSync(domWithA11yCustomIssuesPath).toString(); 24 | export const domWithA11yIncompleteIssues = fs.readFileSync(domWithA11yIncompleteIssuesPath).toString(); 25 | 26 | export const domWithDescendancyA11yIssues = fs.readFileSync(fileWithDescendancyA11yIssues).toString(); 27 | export const a11yIssuesCount = 5; 28 | export const exceptionList = { 29 | 'document-title': ['html'], 30 | 'link-name': ['a'], 31 | }; 32 | export const a11yIssuesCountFiltered = a11yIssuesCount - Object.keys(exceptionList).length; 33 | 34 | // DOM containing no a11y issues 35 | export const shadowDomID = 'upside-down'; 36 | const fileWithNoA11yIssues = path.resolve(dataDir, 'noA11yIssues.html'); 37 | export const htmlFileWithNoA11yIssues = 'file:///' + fileWithNoA11yIssues; 38 | export const domWithNoA11yIssues = fs.readFileSync(fileWithNoA11yIssues).toString(); 39 | export const domWithNoA11yIssuesChildCount = 4; 40 | 41 | // DOM with video, color contrast a11y issues that can be detected only in a real browser 42 | const fileWithVisualA11yIssues = path.resolve(dataDir, 'a11yIssuesVisual.html'); 43 | export const htmlFileWithVisualA11yIssues = 'file:///' + fileWithVisualA11yIssues; 44 | export const domWithVisualA11yIssues = fs.readFileSync(fileWithVisualA11yIssues).toString(); 45 | 46 | // Sample media files 47 | // TODO (refactor): Is there a way to reuse these values inside the noA11yIssues.html 48 | export const audioURL = 'https://file-examples-com.github.io/uploads/2017/11/file_example_MP3_700KB.mp3'; 49 | export const videoURL = 'https://file-examples-com.github.io/uploads/2020/03/file_example_WEBM_480_900KB.webm'; 50 | -------------------------------------------------------------------------------- /packages/common/README.md: -------------------------------------------------------------------------------- 1 | # `@sa11y/common` 2 | 3 | Common utilities, constants, error messages, and helper functions for `@sa11y` packages. 4 | 5 | 6 | 7 | 8 | - [Overview](#overview) 9 | - [Utilities](#utilities) 10 | - [Environment Detection](#environment-detection) 11 | - [Custom Rules](#custom-rules) 12 | - [File Processing](#file-processing) 13 | - [Result Processing](#result-processing) 14 | - [Environment Variables](#environment-variables) 15 | 16 | 17 | 18 | ## Overview 19 | 20 | This package provides shared functionality used across all `@sa11y` packages. It includes utilities for environment detection, custom rule management, file processing, and result handling. 21 | 22 | ## Utilities 23 | 24 | ### Environment Detection 25 | 26 | ```javascript 27 | import { log, isFakeTimerUsed } from '@sa11y/common'; 28 | 29 | // Debug logging (only outputs when SA11Y_DEBUG is set) 30 | log('Debug message'); 31 | 32 | // Check if fake timers are being used 33 | if (isFakeTimerUsed()) { 34 | // Handle fake timer scenario 35 | } 36 | ``` 37 | 38 | ### Custom Rules 39 | 40 | ```javascript 41 | import { useCustomRules, registerCustomRules } from '@sa11y/common'; 42 | 43 | // Load custom rules from environment 44 | const customRules = useCustomRules(); 45 | 46 | // Register custom axe rules 47 | registerCustomRules(changesData, rulesData, checkData); 48 | ``` 49 | 50 | ### File Processing 51 | 52 | ```javascript 53 | import { processFiles, writeHtmlFileInPath } from '@sa11y/common'; 54 | 55 | // Process files in a directory 56 | const results = []; 57 | processFiles('/path/to/directory', results, '.json', JSON.parse); 58 | 59 | // Write HTML file for debugging 60 | writeHtmlFileInPath('/output/path', 'test.html', '...'); 61 | ``` 62 | 63 | ### Result Processing 64 | 65 | ```javascript 66 | import { getViolations, getIncomplete } from '@sa11y/common'; 67 | 68 | // Get violations using a custom checker function 69 | const violations = await getViolations(async () => { 70 | const results = await axe.run(); 71 | return results.violations; 72 | }); 73 | 74 | // Get incomplete results 75 | const incomplete = await getIncomplete(async () => { 76 | const results = await axe.run(); 77 | return results.incomplete; 78 | }); 79 | ``` 80 | 81 | ## Environment Variables 82 | 83 | - `SA11Y_DEBUG`: Enable debug logging 84 | - `SA11Y_CUSTOM_RULES`: Path to custom rules JSON file 85 | - `SA11Y_AUTO_FILTER_LIST_PACKAGE_NAME`: Package name for auto-filter list 86 | - `SA11Y_AUTO_FILTER_LIST_PACKAGE_REQUIREMENT`: Package requirement for auto-filter 87 | -------------------------------------------------------------------------------- /packages/preset-rules/src/custom-rules/checks.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import axe from 'axe-core'; 8 | const checkData = [ 9 | { 10 | id: 'sa11y-Keyboard-check', 11 | options: ['sa11y-Keyboard-check'], 12 | evaluate: "function(node, options) { return !!node.hasAttribute('tabindex'); }", 13 | metadata: { 14 | impact: 'critical', 15 | messages: { 16 | pass: 'Button elements are Keyboard operable', 17 | fail: "Button elements are not Keyboard operable,To fix add tabindex='0' attribute and appropriate keyboard event handler.", 18 | }, 19 | }, 20 | }, 21 | { 22 | id: 'Resize-reflow-textoverflow-check', 23 | evaluate: 24 | "function (node) {const style = window.getComputedStyle(node); const tabIndex = node.getAttribute('tabindex'); if (tabIndex === '-1' && node.actualNode && !isVisibleOnScreen(node) && !isVisibleToScreenReaders(node)) { return false; } if (!node.innerText ===\"\") { return false; } if (style.getPropertyValue('text-overflow') === 'ellipsis') { function isTextTruncated(element) {const isTruncated = (element.scrollWidth > element.clientWidth); return isTruncated; } return !isTextTruncated(node); } if (style.getPropertyValue('display') === '-webkit-box' && style.getPropertyValue('-webkit-line-clamp') != 0 && style.getPropertyValue('overflow') === 'hidden' && style.getPropertyValue('-webkit-box-orient') === 'vertical') { function isTextTruncated(element) { const isTruncated = (element.scrollWidth>element.clientWidth); return isTruncated; } return !isTextTruncated(node); } return true; }", 25 | metadata: { 26 | impact: 'moderate', 27 | messages: { 28 | pass: 'Text element does not have ellipses ', 29 | fail: 'Text element have ellipses which make difficulty to read', 30 | }, 31 | }, 32 | }, 33 | { 34 | id: 'sa11y-Keyboard-button-check', 35 | evaluate: 36 | "function (node) { const tabIndex = node.getAttribute('tabindex'); if ( tabIndex === '-1' && node.actualNode && !isVisibleOnScreen(node) && !isVisibleToScreenReaders(node)) { return false; } if(!node.innerText ===\"\"){ return false; } if(!node.hasAttribute('tabindex')){ return false; } return true; }", 37 | messages: { 38 | pass: 'Button element are keyboard operable', 39 | fail: "Button element are not keyboard operable, To fix add tabindex='0' attribute and appropriate keyboard event handler.", 40 | }, 41 | }, 42 | ]; 43 | 44 | export default checkData as axe.Check[]; 45 | -------------------------------------------------------------------------------- /sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | https://opensource.salesforce.com/sa11y/ 6 | weekly 7 | 1.0 8 | 9 | 10 | 11 | 12 | 13 | 14 | https://opensource.salesforce.com/sa11y/packages/jest/ 15 | weekly 16 | 0.9 17 | 18 | 19 | 20 | 21 | https://opensource.salesforce.com/sa11y/packages/vitest/ 22 | weekly 23 | 0.9 24 | 25 | 26 | 27 | 28 | https://opensource.salesforce.com/sa11y/packages/wdio/ 29 | weekly 30 | 0.9 31 | 32 | 33 | 34 | 35 | https://opensource.salesforce.com/sa11y/packages/assert/ 36 | weekly 37 | 0.8 38 | 39 | 40 | 41 | 42 | https://opensource.salesforce.com/sa11y/packages/format/ 43 | weekly 44 | 0.8 45 | 46 | 47 | 48 | 49 | https://opensource.salesforce.com/sa11y/packages/preset-rules/ 50 | weekly 51 | 0.8 52 | 53 | 54 | 55 | 56 | https://opensource.salesforce.com/sa11y/packages/browser-lib/ 57 | weekly 58 | 0.8 59 | 60 | 61 | 62 | 63 | https://opensource.salesforce.com/sa11y/packages/matcher/ 64 | weekly 65 | 0.8 66 | 67 | 68 | 69 | 70 | https://opensource.salesforce.com/sa11y/packages/common/ 71 | weekly 72 | 0.7 73 | 74 | 75 | 76 | 77 | https://opensource.salesforce.com/sa11y/packages/test-utils/ 78 | weekly 79 | 0.6 80 | 81 | 82 | 83 | 84 | https://opensource.salesforce.com/sa11y/packages/test-integration/ 85 | weekly 86 | 0.6 87 | 88 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: [master, alpha, beta] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | lint-build-test: 14 | name: Lint, Build, Test - Node ${{ matrix.node-version }} on ${{ matrix.os }} 15 | 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | node-version: 20 | - 20.x 21 | - 22.x 22 | - 24.x 23 | os: 24 | - ubuntu-latest 25 | 26 | steps: 27 | - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 28 | 29 | - name: Use Node.js ${{ matrix.node-version }} 30 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 31 | with: 32 | node-version: ${{ matrix.node-version }} 33 | cache: 'yarn' 34 | 35 | - run: yarn install --frozen-lockfile 36 | - run: yarn build:ci 37 | - run: yarn test:ci 38 | 39 | - uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0 40 | release: 41 | name: Release 42 | needs: lint-build-test # previous job MUST pass to make a release! 43 | runs-on: ubuntu-latest 44 | 45 | # Skip running release workflow on forks 46 | if: github.repository_owner == 'salesforce' && github.event_name == 'push' 47 | 48 | permissions: 49 | contents: write # to be able to publish a GitHub release 50 | issues: write # to be able to comment on released issues 51 | pull-requests: write # to be able to comment on released pull requests 52 | 53 | steps: 54 | - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 55 | with: 56 | fetch-depth: 0 # Need all git history & tags to determine next release version. 57 | persist-credentials: false 58 | - name: Use Node.js 20.x 59 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 60 | with: 61 | node-version: 20.x 62 | cache: 'yarn' 63 | registry-url: 'https://registry.npmjs.org' 64 | 65 | - run: yarn install --frozen-lockfile 66 | - run: yarn build 67 | - run: yarn run semantic-release 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 71 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # used by setup-node@v3 action 72 | -------------------------------------------------------------------------------- /packages/matcher/src/groupViolationResultsProcessor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | // a11yProcessorCore.ts 8 | import { A11yError } from '@sa11y/format'; 9 | import { createA11yRuleViolation } from '@sa11y/common'; 10 | import type { A11yViolation } from '@sa11y/common'; 11 | 12 | /** 13 | * Create a test processA11yDetailsAndMessages violation error message grouped by rule violation 14 | */ 15 | export function processA11yDetailsAndMessages(error: A11yError, a11yFailureMessages: string[]) { 16 | const a11yRuleViolations: { [key: string]: A11yViolation } = {}; 17 | let a11yRuleViolationsCount = 0; 18 | let a11yErrorElementsCount = 0; 19 | error.a11yResults.forEach((a11yResult) => { 20 | a11yErrorElementsCount++; 21 | if (!(a11yRuleViolations as never)[a11yResult.wcag]) { 22 | a11yRuleViolationsCount++; 23 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 24 | a11yRuleViolations[a11yResult.wcag] = { 25 | id: a11yResult.id, 26 | description: a11yResult.description, 27 | helpUrl: a11yResult.helpUrl, 28 | wcagCriteria: a11yResult.wcag, 29 | summary: a11yResult.summary, 30 | errorElements: [], 31 | }; 32 | } 33 | a11yRuleViolations[a11yResult.wcag].errorElements.push({ 34 | html: a11yResult.html, 35 | selectors: a11yResult.selectors, 36 | hierarchy: a11yResult.ancestry, 37 | any: a11yResult.any, 38 | all: a11yResult.all, 39 | none: a11yResult.none, 40 | relatedNodeAny: a11yResult.relatedNodeAny, 41 | relatedNodeAll: a11yResult.relatedNodeAll, 42 | relatedNodeNone: a11yResult.relatedNodeNone, 43 | message: a11yResult?.message, 44 | }); 45 | }); 46 | 47 | const a11yFailureMessage = ` 48 | ${error.renderedDOMSavedFileName ? `HTML Source: ${error.renderedDOMSavedFileName}\n` : ''} 49 | The test has failed the accessibility check. Accessibility Stacktrace/Issues: 50 | ${a11yErrorElementsCount} HTML elements have accessibility issue(s). ${a11yRuleViolationsCount} rules failed. 51 | 52 | ${Object.values(a11yRuleViolations) 53 | .map((a11yRuleViolation, index) => createA11yRuleViolation(a11yRuleViolation, index + 1)) 54 | .join('\n')} 55 | 56 | 57 | For more info about automated accessibility testing: https://sfdc.co/a11y-test 58 | For tips on fixing accessibility bugs: https://sfdc.co/a11y 59 | For technical questions regarding Salesforce accessibility tools, contact our Sa11y team: http://sfdc.co/sa11y-users 60 | For guidance on accessibility related specifics, contact our A11y team: http://sfdc.co/tmp-a11y 61 | `; 62 | a11yFailureMessages.push(a11yFailureMessage); 63 | } 64 | -------------------------------------------------------------------------------- /packages/test-integration/__wdio__/wdio.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { assertAccessible } from '@sa11y/wdio'; 9 | import { 10 | a11yIssuesCount, 11 | htmlFileWithA11yIssues, 12 | htmlFileWithNoA11yIssues, 13 | htmlFileWithVisualA11yIssues, 14 | } from '@sa11y/test-utils'; 15 | import { axeRuntimeExceptionMsgPrefix, errMsgHeader, WdioAssertFunction, WdioOptions } from '@sa11y/common'; 16 | 17 | async function checkA11yErrorWdio( 18 | assertFunc: WdioAssertFunction, 19 | expectNumA11yIssues = 0, 20 | options: Partial = {} 21 | ): Promise { 22 | // TODO (debug): setting expected number of assertions doesn't seem to be working correctly in mocha 23 | // https://webdriver.io/docs/assertion.html 24 | // Check mocha docs: https://mochajs.org/#assertions 25 | // Checkout Jasmine ? https://webdriver.io/docs/frameworks.html 26 | // expect.assertions(99999); // still passes ??? 27 | 28 | // TODO (debug): Not able to get the expect().toThrow() with async functions to work with wdio test runner 29 | // hence using the longer try.. catch alternative 30 | // expect(async () => await assertAccessible()).toThrow(); 31 | let err: Error = new Error(); 32 | try { 33 | await assertFunc(options); 34 | } catch (e) { 35 | err = e as Error; 36 | } 37 | expect(err).toBeTruthy(); 38 | expect(err.message).not.toContain(axeRuntimeExceptionMsgPrefix); 39 | 40 | if (expectNumA11yIssues > 0) { 41 | expect(err).not.toEqual(new Error()); 42 | expect(err.toString()).toContain(`${expectNumA11yIssues} ${errMsgHeader}`); 43 | } else { 44 | expect(err).toEqual(new Error()); 45 | expect(err.toString()).not.toContain(errMsgHeader); 46 | } 47 | } 48 | 49 | // TODO(refactor): Switch to using sa11y API via browser commands for this test module 50 | describe('integration test @sa11y/wdio in async mode', () => { 51 | /* eslint-disable @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call */ 52 | it('should throw error for html with a11y issues', async () => { 53 | await browser.url(htmlFileWithA11yIssues); 54 | await checkA11yErrorWdio(async () => await assertAccessible(), a11yIssuesCount); 55 | }); 56 | 57 | it('should not throw error for html with no a11y issues', async () => { 58 | await browser.url(htmlFileWithNoA11yIssues); 59 | await checkA11yErrorWdio(async () => await assertAccessible()); 60 | }); 61 | 62 | it('should throw error for html with visual a11y issues', async () => { 63 | await browser.url(htmlFileWithVisualA11yIssues); 64 | await checkA11yErrorWdio(async () => await assertAccessible(), 1); 65 | }); 66 | /* eslint-enable @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call */ 67 | }); 68 | -------------------------------------------------------------------------------- /packages/preset-rules/src/wcag.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { defaultPriority, defaultWcagVersion, Priority, WcagLevel, WcagVersion } from './rules'; 9 | import { extendedRulesInfo } from './extended'; 10 | import { Result } from 'axe-core'; 11 | 12 | /** 13 | * Process given tags from a11y violations and extract WCAG meta-data 14 | * Ref: https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#axe-core-tags 15 | */ 16 | export class WcagMetadata { 17 | static readonly regExp = /^(wcag)(?\d+)(?a*)$/; 18 | // Default SC for axe rules not strictly associated with a WCAG SC 19 | // Could also be experimental rules that are enabled in sa11y preset rules 20 | static readonly defaultSC = 'best-practice'; 21 | public wcagLevel: WcagLevel = ''; 22 | public wcagVersion: WcagVersion; 23 | public successCriteria = WcagMetadata.defaultSC; 24 | public priority: Priority = defaultPriority; 25 | 26 | constructor(readonly violation: Result) { 27 | const ruleInfo = extendedRulesInfo.get(violation.id); 28 | if (ruleInfo) { 29 | // rule has metadata provided in preset-rules 30 | this.wcagVersion = defaultWcagVersion; 31 | this.wcagLevel = ruleInfo.wcagLevel; 32 | this.successCriteria = ruleInfo.wcagSC; 33 | this.priority = ruleInfo.priority; 34 | return; 35 | } 36 | 37 | // TODO (refactor): Is the following required anymore? 38 | // Cleanup taking the new preset-rules structure into account? 39 | // If rule info metadata doesn't exist (e.g. full ruleset) 40 | for (const tag of violation.tags.sort()) { 41 | const match = WcagMetadata.regExp.exec(tag); 42 | if (!match || !match.groups) continue; 43 | const level = match.groups.level; 44 | const versionOrSC = match.groups.version_or_sc.split('').join('.'); 45 | // Tags starting with "wcag" can contain either wcag version and level 46 | // or success criteria e.g. "wcag2aa", "wcag111" 47 | if (level) { 48 | this.wcagLevel = level.toUpperCase(); 49 | if (versionOrSC === '2') { 50 | this.wcagVersion = '2.0'; // Add decimal for consistency 51 | } else { 52 | this.wcagVersion = versionOrSC as WcagVersion; 53 | } 54 | } else { 55 | this.successCriteria = `${versionOrSC}`; 56 | } 57 | } 58 | } 59 | 60 | /** 61 | * Return formatted string containing WCAG SC and Priority 62 | */ 63 | public toString(): string { 64 | const successCriteria = 65 | this.successCriteria === WcagMetadata.defaultSC ? this.successCriteria : `WCAG-SC${this.successCriteria}`; 66 | return `SA11Y-${successCriteria}-${this.priority}`; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/test-utils/__data__/a11yCustomIssues.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Button and links 6 | 7 | 22 | 45 | 46 | 47 | 48 | 49 |

    Label in Name (WCAG 2.5.3) and Keyboard checks

    50 | 51 |
    Button one
    52 | 53 |
    Button two
    54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 67 | 68 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 |

    84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | module.exports = { 9 | root: true, 10 | parser: '@typescript-eslint/parser', 11 | parserOptions: { 12 | // Specify a tsconfig specifically for eslint with simple glob patterns without project references 13 | // as @typescript-eslint does not support project references: 14 | // https://github.com/typescript-eslint/typescript-eslint/issues/2094 15 | // Without this checking individual files with lint-staged fails: 16 | // https://github.com/typescript-eslint/typescript-eslint/issues/967#issuecomment-531817014 17 | tsconfigRootDir: __dirname, 18 | project: './tsconfig.eslint.json', 19 | }, 20 | plugins: [ 21 | '@typescript-eslint', 22 | 'tsdoc', 23 | 'jest', 24 | 'prettier', 25 | 'notice', // checks for and fixes copyright header in each file 26 | 'markdown', 27 | ], 28 | extends: [ 29 | 'eslint:recommended', 30 | 'plugin:@typescript-eslint/eslint-recommended', 31 | 'plugin:@typescript-eslint/recommended', 32 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 33 | // TODO (spike): Evaluate using https://github.com/standard/eslint-config-standard-with-typescript 34 | // 'standard-with-typescript', 35 | 36 | 'plugin:prettier/recommended', 37 | 'plugin:import/typescript', 38 | 'plugin:import/errors', 39 | 'plugin:import/warnings', 40 | 'plugin:eslint-comments/recommended', 41 | 'plugin:markdown/recommended', 42 | ], 43 | settings: { 44 | 'import/resolver': { 45 | // Makes plugin:import work with Typescript interfaces etc 46 | typescript: { 47 | alwaysTryTypes: true, // always try to resolve types under `@types` directory even it doesn't contain any source code 48 | }, 49 | }, 50 | }, 51 | rules: { 52 | 'notice/notice': [ 53 | 'error', 54 | { 55 | templateFile: 'license-header.txt', 56 | }, 57 | ], 58 | '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], 59 | '@typescript-eslint/naming-convention': [ 60 | 'error', 61 | { 62 | selector: ['variable', 'function'], 63 | format: ['camelCase'], 64 | }, 65 | ], 66 | 67 | 'prefer-arrow-callback': 'warn', 68 | }, 69 | overrides: [ 70 | { 71 | // Enable the Markdown processor for all .md files. 72 | files: ['**/*.md'], 73 | processor: 'markdown/markdown', 74 | }, 75 | { 76 | files: ['**/__tests__/*.[j|t]s'], 77 | extends: ['plugin:jest/recommended', 'plugin:jest/style'], 78 | }, 79 | { 80 | files: ['**/*.ts'], 81 | rules: { 82 | 'tsdoc/syntax': 'warn', 83 | }, 84 | }, 85 | ], 86 | env: { 87 | browser: true, 88 | node: true, 89 | }, 90 | ignorePatterns: ['node_modules', 'dist', 'coverage'], 91 | }; 92 | -------------------------------------------------------------------------------- /.github/workflows/scorecards.yml: -------------------------------------------------------------------------------- 1 | name: Scorecards supply-chain security 2 | on: 3 | # For Branch-Protection check. Only the default branch is supported. See 4 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 5 | branch_protection_rule: 6 | # To guarantee Maintained check is occasionally updated. See 7 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 8 | schedule: 9 | - cron: '23 2 * * 5' 10 | push: 11 | branches: ['master'] 12 | 13 | # Declare default permissions as read only. 14 | permissions: read-all 15 | 16 | jobs: 17 | analysis: 18 | name: Scorecards analysis 19 | runs-on: ubuntu-latest 20 | permissions: 21 | # Needed to upload the results to code-scanning dashboard. 22 | security-events: write 23 | # Needed to publish results and get a badge (see publish_results below). 24 | id-token: write 25 | # Uncomment the permissions below if installing in a private repository. 26 | # contents: read 27 | # actions: read 28 | 29 | steps: 30 | - name: 'Checkout code' 31 | uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 32 | with: 33 | persist-credentials: false 34 | 35 | - name: 'Run analysis' 36 | uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 37 | with: 38 | results_file: results.sarif 39 | results_format: sarif 40 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 41 | # - you want to enable the Branch-Protection check on a *public* repository, or 42 | # - you are installing Scorecards on a *private* repository 43 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 44 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 45 | 46 | # Public repositories: 47 | # - Publish results to OpenSSF REST API for easy access by consumers 48 | # - Allows the repository to include the Scorecard badge. 49 | # - See https://github.com/ossf/scorecard-action#publishing-results. 50 | # For private repositories: 51 | # - `publish_results` will always be set to `false`, regardless 52 | # of the value entered here. 53 | publish_results: true 54 | 55 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 56 | # format to the repository Actions tab. 57 | - name: 'Upload artifact' 58 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 59 | with: 60 | name: SARIF file 61 | path: results.sarif 62 | retention-days: 5 63 | 64 | # Upload the results to GitHub's code scanning dashboard. 65 | - name: 'Upload to code-scanning' 66 | uses: github/codeql-action/upload-sarif@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2.28.1 67 | with: 68 | sarif_file: results.sarif 69 | -------------------------------------------------------------------------------- /packages/assert/src/assert.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import * as axe from 'axe-core'; 9 | import { defaultRuleset } from '@sa11y/preset-rules'; 10 | import { A11yError, exceptionListFilterSelectorKeywords } from '@sa11y/format'; 11 | import { A11yConfig, AxeResults, getViolations, getIncomplete } from '@sa11y/common'; 12 | 13 | /** 14 | * Context that can be checked for accessibility: Document, Node or CSS selector. 15 | * Limiting to subset of options supported by axe for ease of use and maintenance. 16 | */ 17 | export type A11yCheckableContext = Document | Node | string; 18 | 19 | export async function getA11yResultsJSDOM( 20 | context: A11yCheckableContext = document, 21 | rules: A11yConfig = defaultRuleset, 22 | enableIncompleteResults = false 23 | ): Promise { 24 | return enableIncompleteResults ? getIncompleteJSDOM(context, rules) : getViolationsJSDOM(context, rules); 25 | } 26 | 27 | /** 28 | * Get list of a11y issues incomplete for given element and ruleset 29 | * @param context - DOM or HTML Node to be tested for accessibility 30 | * @param rules - A11yConfig preset rule to use, defaults to `base` ruleset 31 | * @param reportType - Type of report ('violations' or 'incomplete') 32 | * @returns {@link AxeResults} - list of accessibility issues found 33 | */ 34 | export async function getIncompleteJSDOM( 35 | context: A11yCheckableContext = document, 36 | rules: A11yConfig = defaultRuleset 37 | ): Promise { 38 | return await getIncomplete(async () => { 39 | const results = await axe.run(context as axe.ElementContext, rules as axe.RunOptions); 40 | return results.incomplete; 41 | }); 42 | } 43 | 44 | /** 45 | * Get list of a11y issues violations for given element and ruleset 46 | * @param context - DOM or HTML Node to be tested for accessibility 47 | * @param rules - A11yConfig preset rule to use, defaults to `base` ruleset 48 | * @param reportType - Type of report ('violations' or 'incomplete') 49 | * @returns {@link AxeResults} - list of accessibility issues found 50 | */ 51 | export async function getViolationsJSDOM( 52 | context: A11yCheckableContext = document, 53 | rules: A11yConfig = defaultRuleset 54 | ): Promise { 55 | return await getViolations(async () => { 56 | const results = await axe.run(context as axe.ElementContext, rules as axe.RunOptions); 57 | return results.violations; 58 | }); 59 | } 60 | 61 | /** 62 | * Checks DOM for accessibility issues and throws an error if violations are found. 63 | * @param context - DOM or HTML Node to be tested for accessibility 64 | * @param rules - A11yConfig preset rule to use, defaults to `base` ruleset 65 | * @throws error - with the accessibility issues found, does not return any value 66 | * */ 67 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types 68 | export async function assertAccessible(context: A11yCheckableContext = document, rules: A11yConfig = defaultRuleset) { 69 | let violations = await getViolationsJSDOM(context, rules); 70 | if (process.env.SELECTOR_FILTER_KEYWORDS) { 71 | violations = exceptionListFilterSelectorKeywords(violations, process.env.SELECTOR_FILTER_KEYWORDS.split(',')); 72 | } 73 | A11yError.checkAndThrow(violations); 74 | } 75 | -------------------------------------------------------------------------------- /packages/browser-lib/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import progress from 'rollup-plugin-progress'; 9 | import resolve from '@rollup/plugin-node-resolve'; 10 | import replace from '@rollup/plugin-replace'; 11 | import commonjs from '@rollup/plugin-commonjs'; 12 | import typescript from 'rollup-plugin-typescript2'; 13 | import sizes from 'rollup-plugin-sizes'; 14 | import nodePolyfills from 'rollup-plugin-polyfill-node'; 15 | import terser from '@rollup/plugin-terser'; 16 | import fs from 'fs' 17 | 18 | export const namespace = 'sa11y'; 19 | 20 | const globalName = '__SA11Y__'; 21 | 22 | /** 23 | * Get the `version` from `package.json` 24 | * 25 | * @returns {string} the version from package.json 26 | */ 27 | function getPackageVersion() { 28 | const packageData = fs.readFileSync('./package.json') 29 | const obj = JSON.parse(packageData) 30 | return obj.version; 31 | } 32 | 33 | /* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access */ 34 | function getConfig(minified = false) { 35 | const debug = !!process.env.DEBUG; 36 | const pkgVersion = getPackageVersion(); 37 | return { 38 | input: 'src/index.ts', 39 | output: { 40 | file: minified ? `dist/${namespace}.min.js` : `dist/${namespace}.js`, 41 | format: 'iife', 42 | generatedCode: { 43 | constBindings: true, 44 | }, 45 | name: globalName, 46 | // Note: Following is required for the object to get declared in browser using webdriver 47 | banner: `typeof ${namespace} === "undefined" && (${namespace} = {});`, 48 | footer: `Object.assign(${namespace}, ${globalName}); ${namespace}.version = '${pkgVersion}';`, 49 | }, 50 | plugins: [ 51 | debug ? progress({ clearLine: false }) : {}, 52 | nodePolyfills(), // axe-core uses node's "crypto" module 53 | resolve(), 54 | commonjs(), 55 | typescript({ 56 | tsconfigOverride: { compilerOptions: { module: 'es2015' } }, 57 | verbosity: debug ? 3 : 1, 58 | }), 59 | minified ? terser() : {}, 60 | sizes({ details: debug }), 61 | replace({ 62 | // 'process' is not defined in browser 63 | /* eslint-disable @typescript-eslint/naming-convention */ 64 | 'process.env.SA11Y_AUTO': JSON.stringify(''), 65 | 'process.env.SA11Y_AUTO_FILTER': JSON.stringify(''), 66 | 'process.env.SA11Y_CLEANUP': JSON.stringify(''), 67 | 'process.env.SA11Y_DEBUG': JSON.stringify(''), 68 | 'process.env.SA11Y_RULESET': JSON.stringify('base'), 69 | 'process.env.SA11Y_RULESET_PRIORITY': JSON.stringify(''), 70 | /* eslint-enable @typescript-eslint/naming-convention */ 71 | 'preventAssignment': true, 72 | }), 73 | ], 74 | }; 75 | } 76 | /* eslint-enable @typescript-eslint/no-unsafe-call, @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access */ 77 | 78 | export default [ 79 | // Produce both minified and un-minified files 80 | getConfig(false), 81 | getConfig(true), 82 | ]; 83 | -------------------------------------------------------------------------------- /packages/jest/__tests__/resultsProcessor.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import resultsProcessor from '../src/resultsProcessor'; 8 | import { addResult, createEmptyTestResult, makeEmptyAggregatedTestResult } from '@jest/test-result'; 9 | import { AggregatedResult, AssertionResult, TestResult } from '@jest/test-result/build/types'; 10 | import { A11yError, A11yResult } from '@sa11y/format'; 11 | import { getA11yError } from '@sa11y/format/__tests__/format.test'; 12 | import { domWithVisualA11yIssues } from '@sa11y/test-utils'; 13 | import { expect } from '@jest/globals'; 14 | 15 | const a11yResults: A11yResult[] = []; 16 | const aggregatedResults = makeEmptyAggregatedTestResult(); 17 | const testSuite = createEmptyTestResult(); 18 | let numTestFailures = 0; 19 | const numNonA11yFailures = 3; 20 | 21 | function addTestFailure(suite: TestResult, err: Error) { 22 | const failure = { 23 | // Subset of props used by results processor logic 24 | failureDetails: [err], 25 | fullName: `${err.name}-${numTestFailures}`, // Unique test name to test consolidation 26 | status: 'failed', 27 | ancestorTitles: ['sa11y'], 28 | } as AssertionResult; 29 | suite.testResults.push(failure); 30 | suite.numFailingTests += 1; 31 | numTestFailures++; 32 | } 33 | 34 | beforeAll(async () => { 35 | // Prepare test data 36 | const a11yError = await getA11yError(); 37 | const a11yErrorVisual = await getA11yError(domWithVisualA11yIssues); 38 | const combinedViolations = [...a11yError.violations, ...a11yErrorVisual.violations]; 39 | a11yResults.push(...a11yError.a11yResults, ...a11yErrorVisual.a11yResults); 40 | addTestFailure(testSuite, new A11yError(a11yError.violations, a11yError.a11yResults)); 41 | addTestFailure(testSuite, new A11yError(a11yErrorVisual.violations, a11yErrorVisual.a11yResults)); 42 | // Duplicate test result to test consolidation 43 | addTestFailure(testSuite, new A11yError(a11yError.violations, a11yError.a11yResults)); 44 | addTestFailure(testSuite, new A11yError(a11yErrorVisual.violations, a11yErrorVisual.a11yResults)); 45 | addTestFailure(testSuite, new A11yError(combinedViolations, a11yResults)); 46 | // Add non-a11y test failure 47 | addTestFailure(testSuite, new Error('foo')); 48 | testSuite.testFilePath = '/test/data/sa11y-auto-checks.js'; 49 | addResult(aggregatedResults, testSuite); 50 | }); 51 | 52 | describe('Results Processor', () => { 53 | it('should have valid test data to start with', () => { 54 | expect(aggregatedResults.numFailedTestSuites).toBe(1); 55 | expect(aggregatedResults.numFailedTests).toBe(numTestFailures); 56 | expect(aggregatedResults).toMatchSnapshot(); 57 | }); 58 | 59 | it('should process test results as expected', () => { 60 | const numA11yFailures = numTestFailures - numNonA11yFailures; 61 | // Create a copy as results gets mutated by results processor 62 | const results = JSON.parse(JSON.stringify(aggregatedResults)) as AggregatedResult; 63 | const processedResults = resultsProcessor(results); 64 | expect(processedResults).toMatchSnapshot(); 65 | expect(processedResults).not.toEqual(aggregatedResults); 66 | expect(processedResults.numFailedTestSuites).toEqual(numA11yFailures + 1); 67 | expect(processedResults.numTotalTests).toEqual(aggregatedResults.numTotalTests + numA11yFailures + 2); // After consolidation + non-a11y failure 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /packages/format/__tests__/format.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import * as axe from 'axe-core'; 9 | import { beforeEachSetup, domWithA11yIssues, domWithNoA11yIssues } from '@sa11y/test-utils'; 10 | import { AxeResults } from '@sa11y/common'; 11 | import { A11yError } from '../src'; 12 | import { expect } from '@jest/globals'; 13 | 14 | // TODO (refactor): Move to common test-utils 15 | // - without creating circular dep due to "A11yError" 16 | // eslint-disable-next-line jest/no-export 17 | export async function getA11yError(dom: string = domWithA11yIssues): Promise { 18 | document.body.innerHTML = dom; 19 | const violations = await axe.run(document).then((results) => results.violations); 20 | try { 21 | A11yError.checkAndThrow(violations); 22 | } catch (e) { 23 | return e as A11yError; 24 | } 25 | // If there are no violations and no A11yError thrown 26 | return new A11yError([], []); 27 | } 28 | 29 | // TODO (refactor): Move to common test-utils 30 | // - without creating circular dep due to "A11yError" 31 | // eslint-disable-next-line jest/no-export 32 | export async function getViolations(dom = domWithA11yIssues): Promise { 33 | const a11yError = await getA11yError(dom); 34 | return a11yError.violations; 35 | } 36 | 37 | beforeEach(() => { 38 | beforeEachSetup(); 39 | }); 40 | 41 | describe('a11y Results Formatter', () => { 42 | it.each([domWithA11yIssues, domWithNoA11yIssues])( 43 | 'should format a11y issues as expected with default options for dom %#', 44 | async (dom) => { 45 | const a11yError = await getA11yError(dom); 46 | expect(a11yError.format()).toMatchSnapshot(); 47 | expect(a11yError.length).toMatchSnapshot(); 48 | expect(a11yError.message).toMatchSnapshot(); 49 | } 50 | ); 51 | 52 | it.each([{ formatter: JSON.stringify }, { highlighter: (text: string) => `"${text}"` }, {}, undefined, null])( 53 | 'should format using specified options: %#', 54 | async (formatOptions) => { 55 | expect((await getA11yError(domWithA11yIssues)).format(formatOptions)).toMatchSnapshot(); 56 | } 57 | ); 58 | 59 | it('should not throw error when no violations are present', async () => { 60 | const a11yError = await getA11yError(domWithNoA11yIssues); 61 | expect(() => A11yError.checkAndThrow(a11yError.violations)).not.toThrow(); 62 | }); 63 | 64 | it('should throw error when violations are present', async () => { 65 | const a11yError = await getA11yError(domWithA11yIssues); 66 | expect(() => A11yError.checkAndThrow(a11yError.violations)).toThrowErrorMatchingSnapshot(); 67 | }); 68 | 69 | it('should not throw error for repeated violations with consolidation', async () => { 70 | const a11yError = await getA11yError(domWithA11yIssues); 71 | const violations = a11yError.violations; 72 | // Should throw error for the first time 73 | expect(() => A11yError.checkAndThrow(violations, { deduplicate: true })).toThrowErrorMatchingSnapshot(); 74 | // Should not throw error for repeated violations with consolidation 75 | expect(() => A11yError.checkAndThrow(violations, { deduplicate: true })).not.toThrow(); 76 | // Should throw error for repeated violations without consolidation 77 | expect(() => A11yError.checkAndThrow(violations, { deduplicate: false })).toThrowErrorMatchingSnapshot(); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /packages/format/src/filter.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import * as axe from 'axe-core'; 8 | import { AxeResults, ExceptionList } from '@sa11y/common'; 9 | 10 | /** 11 | * Filter a11y violations from axe based on given {@link ExceptionList} 12 | * @param violations - List of violations found with axe 13 | * @param exceptionList - {@link ExceptionList} of map of rule to corresponding css targets that needs to be filtered from a11y results 14 | */ 15 | export function exceptionListFilter(violations: AxeResults, exceptionList: ExceptionList = {}): AxeResults { 16 | const exceptionRules = Object.keys(exceptionList); 17 | if (exceptionRules.length === 0) return violations; 18 | 19 | const filteredViolations: AxeResults = []; 20 | 21 | for (const violation of violations) { 22 | if (!exceptionRules.includes(violation.id)) { 23 | filteredViolations.push(violation); 24 | } else { 25 | for (const result of violation.nodes) { 26 | const filteredResults = []; 27 | result.target.forEach((cssSelectorItem) => { 28 | if (Array.isArray(cssSelectorItem)) { 29 | cssSelectorItem.forEach((cssSelector) => { 30 | if (!exceptionList[violation.id].includes(cssSelector)) { 31 | filteredResults.push(cssSelector); 32 | } 33 | }); 34 | } else { 35 | if (!exceptionList[violation.id].includes(cssSelectorItem)) { 36 | filteredResults.push(cssSelectorItem); 37 | } 38 | } 39 | }); 40 | if (filteredResults.length > 0) { 41 | filteredViolations.push(violation); 42 | } 43 | } 44 | } 45 | } 46 | 47 | return filteredViolations; 48 | } 49 | 50 | /** 51 | * Filter a11y violations from axe based on given selectors filter keywords 52 | * @param violations - List of violations found with axe 53 | * @param selectorFilterKeywords - List of selector keywords to filter violations for 54 | */ 55 | export function exceptionListFilterSelectorKeywords( 56 | violations: AxeResults, 57 | selectorFilterKeywords: string[] 58 | ): AxeResults { 59 | const filteredViolations: AxeResults = []; 60 | for (const violation of violations) { 61 | const filteredNodes: axe.NodeResult[] = []; 62 | for (const node of violation.nodes) { 63 | const isSelectorFilterKeywordsExists = checkSelectorFilterKeyWordsExists(node, selectorFilterKeywords); 64 | if (!isSelectorFilterKeywordsExists) { 65 | filteredNodes.push(node); 66 | } 67 | } 68 | if (filteredNodes.length > 0) { 69 | violation.nodes = filteredNodes; 70 | filteredViolations.push(violation); 71 | } 72 | } 73 | return filteredViolations; 74 | } 75 | 76 | function checkSelectorFilterKeyWordsExists(node: axe.NodeResult, selectorFilterKeywords: string[]): boolean { 77 | const selectorAncestry = (node.ancestry?.flat(Infinity) ?? []) as string[]; 78 | let isExists = false; 79 | selectorFilterKeywords.some((keyword) => { 80 | isExists = selectorAncestry.some((selector) => { 81 | const lastSelector = selector.split('>').pop() as string; 82 | return lastSelector.includes(keyword); 83 | }); 84 | return isExists; 85 | }); 86 | return isExists; 87 | } 88 | -------------------------------------------------------------------------------- /packages/preset-rules/src/extended.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { getA11yConfig, RuleInfo } from './rules'; 9 | import { baseRulesInfo } from './base'; 10 | 11 | export const extendedRulesInfo: RuleInfo = new Map([ 12 | ...baseRulesInfo, 13 | ...(new Map( 14 | Object.entries({ 15 | 'accesskeys': { priority: 'P3', wcagSC: '2.1.1', wcagLevel: 'A' }, 16 | 'aria-text': { priority: 'P3', wcagSC: '4.1.2', wcagLevel: 'A' }, 17 | 'blink': { priority: 'P1', wcagSC: '2.2.2', wcagLevel: 'A' }, 18 | 'color-contrast-enhanced': { priority: 'P3', wcagSC: '1.4.6', wcagLevel: 'AAA' }, 19 | 'css-orientation-lock': { priority: 'P3', wcagSC: '1.3.4', wcagLevel: 'AA' }, 20 | 'empty-heading': { priority: 'P3', wcagSC: '1.3.1', wcagLevel: 'A' }, 21 | 'empty-table-header': { priority: 'P3', wcagSC: '1.3.1', wcagLevel: 'A' }, 22 | 'focus-order-semantics': { priority: 'P3', wcagSC: '', wcagLevel: '' }, 23 | 'frame-tested': { priority: 'P3', wcagSC: '', wcagLevel: '' }, 24 | 'heading-order': { priority: 'P3', wcagSC: '1.3.1', wcagLevel: 'A' }, 25 | 'hidden-content': { priority: 'P3', wcagSC: '', wcagLevel: '' }, 26 | 'identical-links-same-purpose': { priority: 'P3', wcagSC: '2.4.9', wcagLevel: 'AAA' }, 27 | 'image-redundant-alt': { priority: 'P3', wcagSC: '1.1.1', wcagLevel: 'A' }, 28 | 'label-content-name-mismatch': { priority: 'P2', wcagSC: '2.5.3', wcagLevel: 'A' }, 29 | 'label-title-only': { priority: 'P3', wcagSC: '3.3.2', wcagLevel: 'A' }, 30 | 'landmark-banner-is-top-level': { priority: 'P3', wcagSC: '4.1.1', wcagLevel: 'A' }, 31 | 'landmark-complementary-is-top-level': { priority: 'P3', wcagSC: '4.1.1', wcagLevel: 'A' }, 32 | 'landmark-contentinfo-is-top-level': { priority: 'P3', wcagSC: '4.1.1', wcagLevel: 'A' }, 33 | 'landmark-main-is-top-level': { priority: 'P3', wcagSC: '4.1.1', wcagLevel: 'A' }, 34 | 'landmark-no-duplicate-banner': { priority: 'P3', wcagSC: '4.1.1', wcagLevel: 'A' }, 35 | 'landmark-no-duplicate-contentinfo': { priority: 'P3', wcagSC: '4.1.1', wcagLevel: 'A' }, 36 | 'landmark-no-duplicate-main': { priority: 'P3', wcagSC: '4.1.1', wcagLevel: 'A' }, 37 | 'landmark-one-main': { priority: 'P3', wcagSC: '4.1.1', wcagLevel: 'A' }, 38 | 'landmark-unique': { priority: 'P3', wcagSC: '4.1.1', wcagLevel: 'A' }, 39 | 'meta-refresh-no-exceptions': { priority: 'P3', wcagSC: '', wcagLevel: '' }, 40 | 'meta-viewport': { priority: 'P2', wcagSC: '1.4.4', wcagLevel: 'AA' }, 41 | 'meta-viewport-large': { priority: 'P2', wcagSC: '1.4.4', wcagLevel: 'AA' }, 42 | 'page-has-heading-one': { priority: 'P2', wcagSC: '1.3.1', wcagLevel: 'A' }, 43 | 'p-as-heading': { priority: 'P2', wcagSC: '1.3.1', wcagLevel: 'A' }, 44 | 'region': { priority: 'P3', wcagSC: '1.3.1', wcagLevel: '' }, 45 | 'scope-attr-valid': { priority: 'P3', wcagSC: '1.3.1', wcagLevel: 'A' }, 46 | 'skip-link': { priority: 'P3', wcagSC: '2.4.1', wcagLevel: '' }, 47 | 'tabindex': { priority: 'P3', wcagSC: '2.4.3', wcagLevel: 'A' }, 48 | 'table-duplicate-name': { priority: 'P3', wcagSC: '1.3.1', wcagLevel: '' }, 49 | 'table-fake-caption': { priority: 'P3', wcagSC: '', wcagLevel: '' }, 50 | 'td-has-header': { priority: 'P1', wcagSC: '1.3.1', wcagLevel: 'A' }, 51 | }) 52 | ) as RuleInfo), 53 | ]); 54 | 55 | // Contains best-practice rules without an associated WCAG SC and experimental rules 56 | export const extended = getA11yConfig(extendedRulesInfo); 57 | -------------------------------------------------------------------------------- /packages/browser-lib/__wdio__/browser-lib.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import * as fs from 'fs/promises'; 9 | import * as path from 'path'; 10 | import { namespace } from '../src'; 11 | import { axeVersion } from '@sa11y/common'; 12 | import { full } from '@sa11y/preset-rules'; 13 | import { 14 | a11yIssuesCountFiltered, 15 | exceptionList, 16 | htmlFileWithNoA11yIssues, 17 | htmlFileWithA11yIssues, 18 | a11yIssuesCount, 19 | } from '@sa11y/test-utils'; 20 | 21 | type ObjectWithVersion = { 22 | version: string; 23 | }; 24 | 25 | const sa11yJS = '../dist/sa11y.js'; 26 | const sa11yMinJS = '../dist/sa11y.min.js'; 27 | 28 | // TODO (refactor): reuse common code with @sa11y/wdio src/test code 29 | 30 | /** 31 | * Test util function to check if given object with 'version' property is loaded in browser. 32 | * @returns value of given object's 'version' property if available, false otherwise. 33 | */ 34 | function isLoaded(objName: string): Promise { 35 | return browser.execute((objName) => { 36 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any 37 | const obj: ObjectWithVersion = (window as { [key: string]: any })[objName]; 38 | return typeof obj === 'object' ? obj.version : false; 39 | }, objName); 40 | } 41 | 42 | async function loadMinJS(filePath = sa11yMinJS): Promise { 43 | const sa11yMinJsPath = path.resolve(__dirname, filePath); 44 | const sa11yMinJs = (await fs.readFile(sa11yMinJsPath)).toString(); 45 | if (sa11yMinJs.length <= 0) throw new Error('Unable to load min js ' + filePath); 46 | return browser.execute(sa11yMinJs); 47 | } 48 | 49 | /** 50 | * Test util function to inject given file and verify that sa11y and axe are loaded into browser 51 | */ 52 | async function verifySa11yLoaded(filePath: string): Promise { 53 | await browser.reloadSession(); 54 | await loadMinJS(filePath); 55 | // After injecting sa11y and axe should be defined 56 | const packageJSON = JSON.parse( 57 | (await fs.readFile(path.resolve(__dirname, '../package.json'))).toString() 58 | ) as ObjectWithVersion; 59 | await expectAsync(isLoaded(namespace)).toBeResolvedTo(packageJSON.version); 60 | await expectAsync(isLoaded('axe')).toBeResolvedTo(axeVersion); 61 | } 62 | 63 | async function checkNumViolations( 64 | scope = '', 65 | exceptionList = {}, 66 | expectedNumViolations = a11yIssuesCount, 67 | script = '' 68 | ): Promise { 69 | const getViolationsScript = 70 | script || 71 | `return JSON.parse((await sa11y.checkAccessibility( 72 | '${scope}', 73 | sa11y.base, 74 | ${JSON.stringify(exceptionList)}))).length;`; 75 | await browser.url(htmlFileWithNoA11yIssues); 76 | await loadMinJS(); 77 | await expectAsync(browser.execute(getViolationsScript)).toBeResolvedTo(0); 78 | 79 | await browser.url(htmlFileWithA11yIssues); 80 | await loadMinJS(); 81 | await expectAsync(browser.execute(getViolationsScript)).toBeResolvedTo(expectedNumViolations); 82 | } 83 | 84 | describe('@sa11y/browser-lib', () => { 85 | it('should not have axe or sa11y loaded to start with', async () => { 86 | await expectAsync(isLoaded(namespace)).toBeResolvedTo(false); 87 | await expectAsync(isLoaded('axe')).toBeResolvedTo(false); 88 | }); 89 | 90 | it('should inject minified js', () => verifySa11yLoaded(sa11yMinJS)); 91 | 92 | it('should inject un-minified js', () => verifySa11yLoaded(sa11yJS)); 93 | 94 | it('should invoke functions on axe e.g. getRules', async () => { 95 | await loadMinJS(); 96 | return expectAsync(browser.execute('return axe.getRules().length')).toBeResolvedTo(full.runOnly.values.length); 97 | }); 98 | 99 | it('should run a11y checks using axe', () => { 100 | return checkNumViolations('', {}, a11yIssuesCount, 'return (await axe.run()).violations.length;'); 101 | }); 102 | 103 | it('should run a11y checks using sa11y', () => checkNumViolations()); 104 | 105 | it('should filter a11y violations using sa11y', () => 106 | checkNumViolations('', exceptionList, a11yIssuesCountFiltered)); 107 | 108 | it('should analyze only specified scope using sa11y', () => checkNumViolations('div', {}, 1)); 109 | }); 110 | -------------------------------------------------------------------------------- /packages/matcher/src/setup.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { useFilesToBeExempted, registerCustomRules } from '@sa11y/common'; 8 | import { 9 | AutoCheckOpts, 10 | getOriginalDocumentBodyHtml, 11 | setOriginalDocumentBodyHtml, 12 | RenderedDOMSaveOpts, 13 | } from './automatic'; 14 | import { changesData, rulesData, checkData } from '@sa11y/preset-rules'; 15 | 16 | export const improvedChecksFilter = [ 17 | 'ui-email-stream-components/modules/emailStream/adminHealthInsights/__tests__/adminHealthInsights.test.js', 18 | ]; 19 | 20 | export type Sa11yOpts = { 21 | autoCheckOpts: AutoCheckOpts; 22 | renderedDOMSaveOpts: RenderedDOMSaveOpts; 23 | }; 24 | 25 | export const defaultSa11yOpts: Sa11yOpts = { 26 | autoCheckOpts: { 27 | runAfterEach: false, 28 | cleanupAfterEach: false, 29 | consolidateResults: false, 30 | runDOMMutationObserver: false, 31 | enableIncompleteResults: false, 32 | }, 33 | renderedDOMSaveOpts: { 34 | renderedDOMDumpDirPath: '', 35 | }, 36 | }; 37 | 38 | export function registerRemoveChild(): void { 39 | // eslint-disable-next-line @typescript-eslint/unbound-method 40 | const originalRemoveChild = Element.prototype.removeChild; 41 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any 42 | (Element.prototype as any).removeChild = function (oldChild: Node): Node { 43 | if (oldChild.parentNode === this && !getOriginalDocumentBodyHtml()) { 44 | setOriginalDocumentBodyHtml(document?.body?.innerHTML ?? ''); 45 | } 46 | return originalRemoveChild.call(this, oldChild); 47 | }; 48 | } 49 | 50 | export function updateAutoCheckOpts(autoCheckOpts: AutoCheckOpts): void { 51 | autoCheckOpts.runAfterEach ||= !!process.env.SA11Y_AUTO; 52 | autoCheckOpts.cleanupAfterEach ||= !!process.env.SA11Y_CLEANUP; 53 | autoCheckOpts.consolidateResults ||= autoCheckOpts.runAfterEach; 54 | if (process.env.SA11Y_AUTO_FILTER?.trim().length) { 55 | autoCheckOpts.filesFilter ||= process.env.SA11Y_AUTO_FILTER.split(','); 56 | } 57 | const exemptedFiles = useFilesToBeExempted(); 58 | if (exemptedFiles.length !== 0) { 59 | autoCheckOpts.filesFilter = (autoCheckOpts.filesFilter ?? []).concat(exemptedFiles); 60 | } 61 | 62 | autoCheckOpts.filesFilter = (autoCheckOpts.filesFilter ?? []).concat([ 63 | 'ui-help-components/modules/forceHelp/linkToReleaseNotes/__tests__/linkToReleaseNotes.spec.js', 64 | 'ui-help-components/modules/forceHelp/linkToNonSalesforceResource/__tests__/linkToNonSalesforceResource.spec.js', 65 | 'ui-help-components/modules/forceHelp/linkToAppexchange/__tests__/linkToAppexchange.spec.js', 66 | 'ui-help-components/modules/forceHelp/linkToTrailblazer/__tests__/linkToTrailblazer.spec.js', 67 | 'ui-help-components/modules/forceHelp/linkToVidyard/__tests__/linkToVidyard.spec.js', 68 | 'ui-help-components/modules/forceHelp/linkToSalesforceDevelopers/__tests__/linkToSalesforceDevelopers.spec.js', 69 | 'ui-help-components/modules/forceHelp/linkToWebinar/__tests__/linkToWebinar.spec.js', 70 | 'ui-help-components/modules/forceHelp/linkToTrust/__tests__/linkToTrust.spec.js', 71 | 'ui-help-components/modules/forceHelp/linkToPartnerCommunity/__tests__/linkToPartnerCommunity.spec.js', 72 | 'ui-help-components/modules/forceHelp/linkToDocResource/__tests__/linkToDocResource.spec.js', 73 | 'ui-help-components/modules/forceHelp/searchResultItem/__tests__/searchResultItem.spec.js', 74 | 'ui-help-components/modules/forceHelp/linkToTrailhead/__tests__/linkToTrailhead.spec.js', 75 | 'ui-help-components/modules/forceHelp/linkToSalesforceSuccess/__tests__/linkToSalesforceSuccess.spec.js', 76 | 'ui-help-components/modules/forceHelp/linkToSalesforceHelp/__tests__/linkToSalesforceHelp.spec.js', 77 | 'ui-help-components/modules/forceHelp/link/__tests__/link.spec.js', 78 | 'ui-help-components/modules/forceHelp/searchResults/__tests__/searchResults.spec.js', 79 | 'ui-help-components/modules/forceHelp/linkToKnownIssue/__tests__/linkToKnownIssue.spec.js', 80 | ]); 81 | 82 | autoCheckOpts.runDOMMutationObserver ||= !!process.env.SA11Y_ENABLE_DOM_MUTATION_OBSERVER; 83 | autoCheckOpts.enableIncompleteResults ||= !!process.env.SA11Y_ENABLE_INCOMPLETE_RESULTS; 84 | } 85 | 86 | export function registerCustomSa11yRules(): void { 87 | registerCustomRules(changesData, rulesData, checkData); 88 | } 89 | -------------------------------------------------------------------------------- /packages/wdio/src/wdio.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import * as axe from 'axe-core'; 9 | import { defaultRuleset } from '@sa11y/preset-rules'; 10 | import { A11yError, exceptionListFilter } from '@sa11y/format'; 11 | import { A11yConfig, AxeResults, axeVersion, getViolations, WdioOptions } from '@sa11y/common'; 12 | 13 | /** 14 | * Merge given options with default options 15 | */ 16 | function setDefaultOptions(opts: Partial = {}): WdioOptions { 17 | const defaultOptions: WdioOptions = { 18 | driver: browser, // Need to be defined inside a function as it is populated at runtime 19 | scope: undefined, 20 | rules: defaultRuleset, 21 | exceptionList: {}, 22 | }; 23 | return Object.assign(Object.assign({}, defaultOptions), opts) as WdioOptions; 24 | } 25 | 26 | /** 27 | * Return version of axe injected into browser 28 | */ 29 | export function getAxeVersion(driver: WebdriverIO.Browser): Promise { 30 | return driver.execute(() => { 31 | return typeof axe === 'object' ? axe.version : undefined; 32 | }); 33 | } 34 | 35 | /** 36 | * Load axe source into browser if it is not already loaded and return version of axe. 37 | * Since axe min js is large (400+kb), keep polling until given timeout in milliseconds. 38 | */ 39 | export async function loadAxe(driver: WebdriverIO.Browser, timeout = 10000, pollTime = 100): Promise { 40 | // TODO (perf): Conditionally injecting axe based on axe version doesn't 41 | // work reliably resulting in axe undefined error sometimes 42 | await driver.execute(axe.source); 43 | await driver.waitUntil(async () => (await getAxeVersion(driver)) === axeVersion, { 44 | timeout: timeout, 45 | interval: pollTime, 46 | timeoutMsg: `Unable to load axe after ${timeout} ms`, 47 | }); 48 | } 49 | 50 | /** 51 | * Load and run axe in given WDIO instance and return the accessibility violations found. 52 | */ 53 | export async function runAxe(options: Partial = {}): Promise { 54 | const { driver, scope, rules } = setDefaultOptions(options); 55 | const elemSelector = scope ? (await scope).selector : undefined; 56 | await loadAxe(driver); 57 | 58 | // run axe inside browser and return violations 59 | return driver.executeAsync( 60 | // TODO (chore): Fix lint error 61 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 62 | // @ts-ignore 63 | // TS2345: Argument of type is not assignable to parameter of type 64 | (elemSelector: Selector | undefined, rules?: A11yConfig, done: CallableFunction) => { 65 | axe.run( 66 | (elemSelector || document) as axe.ElementContext, 67 | rules as axe.RunOptions, 68 | (err: Error, results: axe.AxeResults) => { 69 | if (err) throw err; 70 | done(results.violations); 71 | } 72 | ); 73 | }, 74 | elemSelector, 75 | rules 76 | ); 77 | } 78 | 79 | /** 80 | * Verify that the currently loaded page in the browser is accessible. 81 | * Throw an error with the accessibility issues found if it is not accessible. 82 | * Asynchronous version of {@link assertAccessibleSync} 83 | */ 84 | export async function assertAccessible(opts: Partial = {}): Promise { 85 | const options = setDefaultOptions(opts); 86 | // TODO (feat): Add as custom commands to both browser for page level and elem 87 | // https://webdriver.io/docs/customcommands.html 88 | const violations = await getViolations(() => runAxe(options)); 89 | // TODO (refactor): move exception list filtering to getViolations() 90 | // and expose it as an arg to assert and jest api as well ? 91 | const filteredResults = exceptionListFilter(violations, options.exceptionList); 92 | A11yError.checkAndThrow(filteredResults); 93 | } 94 | 95 | /** 96 | * Verify that the currently loaded page in the browser is accessible. 97 | * Throw an error with the accessibility issues found if it is not accessible. 98 | * Synchronous version of {@link assertAccessible} 99 | * @deprecated Please update to using async method. 100 | */ 101 | export function assertAccessibleSync(opts: Partial = {}): void { 102 | const options: WdioOptions = setDefaultOptions(opts); 103 | // Note: https://github.com/webdriverio/webdriverio/tree/master/packages/wdio-sync#switching-between-sync-and-async 104 | void options.driver.call(async () => { 105 | await assertAccessible(options); 106 | }); 107 | } 108 | -------------------------------------------------------------------------------- /packages/matcher/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | - [`@sa11y/matcher`](#sa11ymatcher) 5 | - [Overview](#overview) 6 | - [Install](#install) 7 | - [Usage](#usage) 8 | - [Basic Usage](#basic-usage) 9 | - [Automatic Checks](#automatic-checks) 10 | - [Options](#options) 11 | - [Advanced](#advanced) 12 | - [Caveats](#caveats) 13 | 14 | 15 | 16 | # `@sa11y/matcher` 17 | 18 | Accessibility matcher core utilities for custom and framework-agnostic automated accessibility testing. 19 | 20 | ## Overview 21 | 22 | The `@sa11y/matcher` package provides programmatic APIs to check accessibility of HTML elements or the DOM, using the same underlying logic as the Sa11y Jest matcher. It is intended for use in custom test runners, integration with other frameworks, or advanced scenarios where you want direct control over accessibility checks. 23 | 24 | ## Install 25 | 26 | - Using yarn: `yarn add -D @sa11y/matcher` 27 | - Using npm: `npm install -D @sa11y/matcher` 28 | 29 | ## Usage 30 | 31 | ### Basic Usage 32 | 33 | You can use the `runA11yCheck` function to programmatically check the accessibility of the entire document or a specific element. 34 | 35 | ```javascript 36 | import { runA11yCheck } from '@sa11y/matcher'; 37 | 38 | (async () => { 39 | // Check the entire document for accessibility issues 40 | const result = await runA11yCheck(document); 41 | if (!result.isAccessible) { 42 | console.error('Accessibility issues found:', result.a11yError); 43 | } else { 44 | console.log('No accessibility issues found!'); 45 | } 46 | 47 | // Check a specific element 48 | const elem = document.getElementById('foo'); 49 | const elemResult = await runA11yCheck(elem); 50 | if (!elemResult.isAccessible) { 51 | // Handle accessibility errors 52 | } 53 | })(); 54 | ``` 55 | 56 | You can also pass a custom ruleset from `@sa11y/preset-rules`: 57 | 58 | ```javascript 59 | import { runA11yCheck } from '@sa11y/matcher'; 60 | import { extended } from '@sa11y/preset-rules'; 61 | 62 | await runA11yCheck(document, extended); 63 | ``` 64 | 65 | ### Automatic Checks 66 | 67 | The `runAutomaticCheck` API can be used to automatically check each child element in the DOM body for accessibility issues, similar to the automatic checks in the Jest integration. 68 | 69 | ```javascript 70 | import { runAutomaticCheck, defaultAutoCheckOpts, defaultRenderedDOMSaveOpts } from '@sa11y/matcher'; 71 | 72 | await runAutomaticCheck( 73 | { 74 | cleanupAfterEach: true, // Optionally clean up the DOM after checking 75 | runAfterEach: true, // Run after each test (if used in a test runner) 76 | }, 77 | { 78 | renderedDOMDumpDirPath: './a11y-dumps', 79 | generateRenderedDOMFileSaveLocation: (testFilePath, testName) => ({ 80 | fileName: `${testName}.html`, 81 | fileUrl: `/a11y-dumps/${testName}.html`, 82 | }), 83 | } 84 | ); 85 | ``` 86 | 87 | #### Options 88 | 89 | - `autoCheckOpts` (`AutoCheckOpts`): Options for automatic accessibility checks (see below) 90 | - `renderedDOMSaveOpts` (`RenderedDOMSaveOpts`): Options for saving the rendered DOM during automatic checks. Allows customizing how and where the DOM is saved for debugging or reporting purposes. 91 | 92 | **AutoCheckOpts:** 93 | 94 | - `runAfterEach`: Run after each test (for integration with test runners) 95 | - `cleanupAfterEach`: Clean up the DOM after checking 96 | - `consolidateResults`: Deduplicate results 97 | - `filesFilter`: Array of file path substrings to skip automatic checks for 98 | - `runDOMMutationObserver`: Enable DOM mutation observer mode 99 | - `enableIncompleteResults`: Include incomplete results 100 | 101 | **RenderedDOMSaveOpts:** 102 | 103 | - `renderedDOMDumpDirPath`: Directory path where the rendered DOM HTML files will be saved. 104 | - `generateRenderedDOMFileSaveLocation`: Function to generate the file name and URL for saving the rendered DOM, given the test file path and test name. 105 | 106 | ### Advanced 107 | 108 | You can use other exports for custom integrations, such as `mutationObserverCallback`, `observerOptions`, `RenderedDOMSaveOpts`, `defaultRenderedDOMSaveOpts`, and more. 109 | 110 | Accessibility errors are grouped and reported by rule violation for easier debugging. 111 | 112 | ## Caveats 113 | 114 | - **Async**: All APIs are asynchronous and must be awaited. 115 | - **DOM**: Accessibility checks require a rendered DOM (e.g., JSDOM or a real browser environment). 116 | - **Fake Timers**: If using fake timers (e.g., in Jest), switch to real timers before running accessibility checks. 117 | -------------------------------------------------------------------------------- /packages/jest/src/groupViolationResultsProcessor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { AggregatedResult, AssertionResult, TestResult } from '@jest/test-result'; 8 | import { log } from '@sa11y/common'; 9 | import { A11yError } from '@sa11y/format'; 10 | import { processA11yDetailsAndMessages } from '@sa11y/matcher'; 11 | 12 | type FailureDetail = { 13 | error?: A11yError; 14 | }; 15 | interface FailureMatcherDetail { 16 | error?: { 17 | matcherResult?: { 18 | a11yError?: A11yError; 19 | }; 20 | }; 21 | } 22 | 23 | /** 24 | * Convert any a11y errors from test failures into their own test suite, results 25 | */ 26 | function processA11yErrors(results: AggregatedResult, testSuite: TestResult, testResult: AssertionResult) { 27 | const a11yFailureDetails: FailureDetail[] = []; 28 | const a11yFailureMessages: string[] = []; 29 | let a11yErrorsExist = false; 30 | 31 | testResult.failureDetails.forEach((failure) => { 32 | let error = (failure as FailureDetail).error; 33 | // If using circus test runner https://github.com/facebook/jest/issues/11405#issuecomment-843549606 34 | // TODO (code cov): Add test data covering the case for circus test runner 35 | /* istanbul ignore next */ 36 | if (error === undefined) error = failure as A11yError; 37 | if (error.name === A11yError.name) { 38 | a11yErrorsExist = true; 39 | a11yFailureDetails.push({ ...(failure as FailureDetail) } as FailureDetail); 40 | processA11yDetailsAndMessages(error, a11yFailureMessages); 41 | } 42 | }); 43 | if (!a11yErrorsExist) { 44 | testSuite.numFailingTests -= 1; 45 | results.numFailedTests -= 1; 46 | if (testSuite.numFailingTests === 0) results.numFailedTestSuites -= 1; 47 | } 48 | testResult.failureDetails = [...a11yFailureDetails]; 49 | testResult.failureMessages = [...a11yFailureMessages]; 50 | testResult.status = a11yFailureDetails.length === 0 ? 'passed' : testResult.status; 51 | } 52 | 53 | function processA11yMatcherErrors(results: AggregatedResult, testSuite: TestResult, testResult: AssertionResult) { 54 | const a11yFailureMessages: string[] = []; 55 | 56 | testResult.failureDetails.forEach((failure) => { 57 | const error = (failure as FailureMatcherDetail)?.error?.matcherResult?.a11yError as A11yError; 58 | if (error !== undefined) { 59 | processA11yDetailsAndMessages(error, a11yFailureMessages); 60 | testResult.failureMessages = [...a11yFailureMessages]; 61 | } 62 | }); 63 | } 64 | 65 | /** 66 | * Custom results processor for a11y results grouping the violations. Only affects JSON results file output. 67 | * To be used with jest cli options --json --outputFile 68 | * e.g. jest --json --outputFile jestResults.json --testResultsProcessor `node_modules/@sa11y/jest/dist/groupViolationResultsProcessor.js` 69 | * Ref: https://jestjs.io/docs/configuration#testresultsprocessor-string 70 | * - Mapping of AggregatedResult to JSON format to https://github.com/facebook/jest/blob/master/packages/jest-test-result/src/formatTestResults.ts 71 | */ 72 | export function resultsProcessor(results: AggregatedResult): AggregatedResult { 73 | log(`Processing ${results.numTotalTests} tests ..`); 74 | results.testResults // suite results 75 | .filter((testSuite) => testSuite.numFailingTests > 0) 76 | .forEach((testSuite) => { 77 | testSuite.testResults // individual test results 78 | .filter((testResult) => testResult.status === 'failed') 79 | .forEach((testResult) => { 80 | processA11yErrors(results, testSuite, testResult); 81 | }); 82 | }); 83 | 84 | return results; 85 | } 86 | 87 | export function resultsProcessorManualChecks(results: AggregatedResult): AggregatedResult { 88 | log(`Processing ${results.numTotalTests} tests ..`); 89 | results.testResults // suite results 90 | .filter((testSuite) => testSuite.numFailingTests > 0) 91 | .forEach((testSuite) => { 92 | testSuite.testResults // individual test results 93 | .filter((testResult) => testResult.status === 'failed') 94 | .forEach((testResult) => { 95 | processA11yMatcherErrors(results, testSuite, testResult); 96 | }); 97 | }); 98 | 99 | return results; 100 | } 101 | // The processor must be a node module that exports a function 102 | // Explicitly typing the exports for Node.js compatibility 103 | const exportedFunctions = { 104 | resultsProcessor, 105 | resultsProcessorManualChecks, 106 | }; 107 | 108 | module.exports = exportedFunctions; 109 | -------------------------------------------------------------------------------- /packages/wdio/README.md: -------------------------------------------------------------------------------- 1 | # `@sa11y/wdio` 2 | 3 | Provides `assertAccessible()`, `assertAccessibleSync()` APIs that can be used with [WebdriverIO](https://webdriver.io/) to check accessibility of web pages rendered in browsers. 4 | 5 | 6 | 7 | 8 | - [Caution](#caution) 9 | - [Usage](#usage) 10 | - [Async Mode (Recommended)](#async-mode-recommended) 11 | - [Sync Mode (Deprecated)](#sync-mode-deprecated) 12 | - [API](#api) 13 | - [assertAccessible](#assertaccessible) 14 | - [assertAccessibleSync](#assertaccessiblesync) 15 | - [Reference](#reference) 16 | 17 | 18 | 19 | ## Caution 20 | 21 | - **headless**: Checks such as color contrast do not work in headless mode. In general executing tests in headless mode [might yield different accessibility results](https://github.com/dequelabs/axe-core/issues/2088). Hence, it is recommended to run accessibility checks in windowed mode when possible for accurate results. 22 | 23 | ## Usage 24 | 25 | ### Async Mode (Recommended) 26 | 27 | ```javascript 28 | import { assertAccessible } from '@sa11y/wdio'; 29 | 30 | describe('demonstrate usage of @sa11y/wdio', () => { 31 | it('should demonstrate usage of assertAccessible API', async () => { 32 | // Navigate to page to be tested 33 | await browser.url('pageToBeTested.html'); 34 | // Check for accessibility of the loaded page 35 | await assertAccessible(); 36 | }); 37 | 38 | it('should demonstrate checking a11y of a selected element', async () => { 39 | // Navigate to page to be tested 40 | await browser.url('pageToBeTested.html'); 41 | // Check accessibility of a particular element using https://webdriver.io/docs/selectors 42 | await assertAccessible({ scope: browser.$('selector') }); 43 | }); 44 | 45 | it('should demonstrate exception list', async () => { 46 | // Navigate to page to be tested 47 | await browser.url('pageToBeTested.html'); 48 | // Exception list is a map of rule to corresponding css targets that needs to be filtered from a11y results 49 | const exceptions = { 50 | 'document-title': ['html'], 51 | 'link-name': ['a'], 52 | }; 53 | // Check for accessibility of the loaded page, filtering out results from given exception list 54 | await assertAccessible({ exceptionList: exceptions }); 55 | }); 56 | 57 | it('should demonstrate custom rules', async () => { 58 | await browser.url('pageToBeTested.html'); 59 | // Use extended ruleset for more comprehensive checking 60 | await assertAccessible({ rules: extended }); 61 | }); 62 | }); 63 | ``` 64 | 65 | ### Sync Mode (Deprecated) 66 | 67 | > **Note**: WebdriverIO sync mode is deprecated. Please use async mode for new projects. 68 | 69 | ```javascript 70 | import { assertAccessibleSync } from '@sa11y/wdio'; 71 | 72 | describe('demonstrate usage of @sa11y/wdio sync', () => { 73 | it('should demonstrate usage of assertAccessibleSync API', () => { 74 | return sync(() => { 75 | // Navigate to page to be tested 76 | browser.url('pageToBeTested.html'); 77 | // Check for accessibility of the loaded page 78 | assertAccessibleSync(); 79 | }); 80 | }); 81 | }); 82 | ``` 83 | 84 | ## API 85 | 86 | ### assertAccessible 87 | 88 | Asynchronous API for checking accessibility in WebdriverIO tests. 89 | 90 | **Signature:** 91 | 92 | ```typescript 93 | async function assertAccessible(options?: { 94 | driver?: WebdriverIO.Browser; 95 | scope?: WebdriverIO.Element; 96 | rules?: A11yConfig; 97 | exceptionList?: Record; 98 | }): Promise; 99 | ``` 100 | 101 | **Parameters:** 102 | 103 | - `options` (optional): Configuration object 104 | - `driver`: WDIO BrowserObject instance. Created automatically by WDIO test runner. 105 | - `scope`: Element to check for accessibility. Defaults to the entire document. 106 | - `rules`: Preset rules configuration. Defaults to `base` ruleset. 107 | - `exceptionList`: Map of rule id to corresponding CSS targets to be filtered from results. 108 | 109 | ### assertAccessibleSync 110 | 111 | Synchronous API for checking accessibility (deprecated with WebdriverIO sync mode). 112 | 113 | **Signature:** 114 | 115 | ```typescript 116 | function assertAccessibleSync(options?: { 117 | driver?: WebdriverIO.Browser; 118 | scope?: WebdriverIO.Element; 119 | rules?: A11yConfig; 120 | exceptionList?: Record; 121 | }): void; 122 | ``` 123 | 124 | ![Screenshot showing a11y errors from a test using Sa11y WDIO in a terminal](https://github.com/salesforce/sa11y/blob/media/screenshot/wdio.png?raw=true) 125 | 126 | ## Reference 127 | 128 | - [Sync mode vs async · WebdriverIO](https://webdriver.io/docs/sync-vs-async.html) 129 | -------------------------------------------------------------------------------- /packages/jest/__tests__/matcher.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { registerSa11yMatcher, toBeAccessible } from '../src'; 9 | import { base, extended, getA11yConfig } from '@sa11y/preset-rules'; 10 | import { 11 | beforeEachSetup, 12 | cartesianProduct, 13 | domWithA11yIssues, 14 | domWithA11yIssuesBodyID, 15 | domWithNoA11yIssues, 16 | shadowDomID, 17 | } from '@sa11y/test-utils'; 18 | import { isTestUsingFakeTimer } from '../src/matcher'; 19 | import { runAutomaticCheck } from '@sa11y/matcher'; 20 | import { expect, jest } from '@jest/globals'; 21 | import { axeRuntimeExceptionMsgPrefix } from '@sa11y/common'; 22 | 23 | // Collection of values to be tested passed in as different API parameters 24 | const a11yConfigParams = [extended, base, undefined]; 25 | const domParams = [document, undefined]; 26 | const domConfigParams = cartesianProduct(domParams, a11yConfigParams); 27 | 28 | beforeAll(() => { 29 | registerSa11yMatcher(); 30 | }); 31 | 32 | beforeEach(() => { 33 | beforeEachSetup(); 34 | }); 35 | 36 | describe('a11y matchers', () => { 37 | it('should be extendable with expect', () => { 38 | // Mostly here for code cov as it doesn't register correctly with just registerSa11yMatcher() 39 | expect.extend({ toBeAccessible }); 40 | }); 41 | }); 42 | 43 | describe('toBeAccessible jest a11y matcher', () => { 44 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 45 | it.each(domConfigParams)('should not throw error for dom with no a11y issues (arg: %#)', async (dom, config) => { 46 | document.body.innerHTML = domWithNoA11yIssues; 47 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 48 | await expect(dom).toBeAccessible(config); 49 | }); 50 | 51 | it.each(a11yConfigParams)('should throw error for dom with a11y issues with config: %#', async (config) => { 52 | document.body.innerHTML = domWithA11yIssues; 53 | // using the 'not' matcher just for testing, not expecting this to be used out of the unit testing context 54 | await expect(document).not.toBeAccessible(config); 55 | await expect(document.getElementById(domWithA11yIssuesBodyID)).not.toBeAccessible(); 56 | // using without the 'not' matcher which should be the primary way the API is used (without error catching) 57 | await expect(expect(document).toBeAccessible()).rejects.toThrow(); 58 | }); 59 | 60 | it('should be able to check a11y of a HTML element with no issues', async () => { 61 | document.body.innerHTML = domWithNoA11yIssues; 62 | const elem = document.getElementById(shadowDomID); 63 | expect(elem).toBeTruthy(); 64 | await expect(expect(elem).toBeAccessible()).resolves.toBeUndefined(); 65 | }); 66 | 67 | it('should be able to check a11y of a HTML element with no issues: %s', async () => { 68 | document.body.innerHTML = domWithA11yIssues; 69 | const elem = document.getElementById(domWithA11yIssuesBodyID); 70 | expect(elem).toBeTruthy(); 71 | await expect(expect(elem).toBeAccessible()).rejects.toThrow(); 72 | }); 73 | 74 | it('should throw non A11yError for non a11y issues', async () => { 75 | const errConfig = getA11yConfig(['non-existent-rule']); 76 | await expect(expect(document).toBeAccessible(errConfig)).rejects.toThrow(axeRuntimeExceptionMsgPrefix); 77 | }); 78 | /* eslint-enable @typescript-eslint/no-unsafe-call */ 79 | }); 80 | 81 | describe('mock timer helper', () => { 82 | afterEach(() => jest.useRealTimers()); 83 | 84 | it('should detect when mock timer is being used', () => { 85 | // Baseline checks 86 | expect(isTestUsingFakeTimer()).toBeFalsy(); 87 | jest.useRealTimers(); 88 | expect(isTestUsingFakeTimer()).toBeFalsy(); 89 | // Fake timer check 90 | for (const mode of ['modern', 'legacy']) { 91 | jest.useFakeTimers(mode); 92 | expect(isTestUsingFakeTimer()).toBeTruthy(); 93 | } 94 | // Revert back and check 95 | jest.useRealTimers(); 96 | expect(isTestUsingFakeTimer()).toBeFalsy(); 97 | }); 98 | 99 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 100 | // TODO: revisit with jest28. Not sure if this is a test problem or sa11y problem with fake timers. 101 | it.skip('should result in error when mock timer is being used from API', async () => { 102 | // Baseline check 103 | document.body.innerHTML = domWithNoA11yIssues; 104 | await expect(document).toBeAccessible(); 105 | // Check for error when using fake timer 106 | jest.useFakeTimers(); 107 | await expect(expect(document).toBeAccessible()).rejects.toThrow(); 108 | }); 109 | /* eslint-enable @typescript-eslint/no-unsafe-call */ 110 | 111 | it('should skip automatic check when mock timer is being used', async () => { 112 | document.body.innerHTML = domWithA11yIssues; 113 | 114 | await expect(runAutomaticCheck()).rejects.toThrow(); 115 | 116 | jest.useFakeTimers(); 117 | await expect(runAutomaticCheck()).resolves.toBeUndefined(); 118 | 119 | //jest.useRealTimers(); 120 | //await expect(automaticCheck()).rejects.toThrow(); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sa11y-monorepo", 3 | "private": true, 4 | "description": "Salesforce Accessibility Automated Testing Libraries and Tools (@sa11y packages)", 5 | "license": "BSD-3-Clause", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/salesforce/sa11y.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/salesforce/sa11y/issues" 12 | }, 13 | "homepage": "https://github.com/salesforce/sa11y", 14 | "workspaces": [ 15 | "packages/*" 16 | ], 17 | "scripts": { 18 | "build": "tsc --build && yarn workspace @sa11y/browser-lib build", 19 | "build:ci": "yarn install --frozen-lockfile && yarn build", 20 | "build:clean": "tsc --build --clean && rimraf packages/**/dist && yarn build:ci", 21 | "build:watch": "tsc --build --watch", 22 | "clean": "lerna exec rimraf node_modules/ dist/ tsconfig.tsbuildinfo && rimraf node_modules", 23 | "commit": "! git diff --cached --exit-code && lint-staged && git-cz", 24 | "install:clean": "lerna clean --yes && rimraf yarn.lock && rimraf node_modules && yarn install", 25 | "install:update": "yarn audit; yarn upgrade-interactive --latest && yarn install:clean && yarn test:clean", 26 | "lint": "eslint . --ext ts,js,md", 27 | "lint:all": "tsc --noEmit && yarn lint && yarn lint:lockfile && yarn lint:deps", 28 | "lint:deps": "lerna exec depcheck", 29 | "lint:fix": "yarn lint --fix", 30 | "lint:lockfile": "lockfile-lint --path yarn.lock --allowed-hosts registry.yarnpkg.com --validate-https", 31 | "lint:staged": "lint-staged", 32 | "lint:version": "lerna exec --stream --no-bail --since master vertioner", 33 | "lint:watch": "esw --watch --changed --color --ext .js,.ts", 34 | "pkg:diff": "lerna exec --stream --no-bail 'git diff --relative --stat origin/master'", 35 | "pkg:list": "lerna list --long --all --toposort", 36 | "pkg:deps": "yarn pkg:list --graph", 37 | "release:changelog": "conventional-changelog --infile CHANGELOG.md --same-file --preset angular --output-unreleased", 38 | "release:version": "yarn pkg:diff; lerna version --no-push --no-git-tag-version && yarn build:clean", 39 | "release:version:auto": "yarn release:version --conventional-commits --no-changelog && yarn build:clean", 40 | "release:publish": "yarn test:clean && lerna publish from-package", 41 | "test": "jest --coverage --runInBand", 42 | "test:ci": "yarn lint:all && yarn test --ci --reporters=default --reporters=jest-junit", 43 | "test:clean": "yarn build:clean && yarn test:ci", 44 | "test:debug": "SA11Y_DEBUG=1 node --inspect node_modules/.bin/jest --runInBand --watch", 45 | "test:watch": "jest --watch", 46 | "test:auto": "SA11Y_AUTO=1 yarn test --json --outputFile jestResults.json", 47 | "test:wdio": "wdio run wdio.conf.js --suite wdio --suite integration --suite browserLib < /dev/null", 48 | "prepare": "is-ci || husky install" 49 | }, 50 | "config": { 51 | "commitizen": { 52 | "path": "./node_modules/cz-conventional-changelog" 53 | } 54 | }, 55 | "devDependencies": { 56 | "@babel/core": "7.28.5", 57 | "@babel/preset-env": "7.28.5", 58 | "@babel/preset-typescript": "7.28.5", 59 | "@commitlint/cli": "17.8.1", 60 | "@commitlint/config-conventional": "17.8.1", 61 | "@semantic-release/changelog": "6.0.3", 62 | "@semantic-release/exec": "7.1.0", 63 | "@tsconfig/node24": "24.0.3", 64 | "@types/jest": "28.1.8", 65 | "@types/node": "15.14.9", 66 | "@typescript-eslint/eslint-plugin": "5.62.0", 67 | "@typescript-eslint/parser": "5.62.0", 68 | "@wdio/cli": "7.40.0", 69 | "@wdio/jasmine-framework": "7.40.0", 70 | "@wdio/local-runner": "7.40.0", 71 | "@wdio/spec-reporter": "7.40.0", 72 | "babel-jest": "28.1.3", 73 | "chromedriver": "119.0.1", 74 | "commitizen": "4.3.1", 75 | "conventional-changelog-cli": "2.2.2", 76 | "cpx2": "2.0.0", 77 | "cspell": "6.31.3", 78 | "depcheck": "1.4.3", 79 | "doctoc": "2.2.1", 80 | "eslint": "8.57.1", 81 | "eslint-config-prettier": "8.10.2", 82 | "eslint-import-resolver-typescript": "3.10.1", 83 | "eslint-plugin-eslint-comments": "3.2.0", 84 | "eslint-plugin-import": "2.32.0", 85 | "eslint-plugin-jest": "27.9.0", 86 | "eslint-plugin-markdown": "3.0.1", 87 | "eslint-plugin-notice": "0.9.10", 88 | "eslint-plugin-prettier": "4.2.5", 89 | "eslint-plugin-tsdoc": "0.5.0", 90 | "eslint-watch": "8.0.0", 91 | "husky": "8.0.3", 92 | "is-ci": "3.0.1", 93 | "jest": "28.1.3", 94 | "jest-environment-jsdom": "28.1.3", 95 | "jest-jasmine2": "28.1.3", 96 | "jest-junit": "16.0.0", 97 | "lerna": "6.6.2", 98 | "lint-staged": "13.3.0", 99 | "lockfile-lint": "4.14.1", 100 | "markdown-link-check": "3.14.2", 101 | "prettier": "2.8.8", 102 | "rimraf": "3.0.2", 103 | "semantic-release": "24.2.9", 104 | "ts-node": "10.9.2", 105 | "typescript": "4.9.5", 106 | "vertioner": "1.0.6", 107 | "wdio-chromedriver-service": "7.3.2", 108 | "webdriverio": "7.40.0" 109 | }, 110 | "engines": { 111 | "node": "^20 || ^22 || ^24" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /packages/jest/__tests__/groupViolationResultsProcessor.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { addResult, createEmptyTestResult, makeEmptyAggregatedTestResult } from '@jest/test-result'; 8 | import { AggregatedResult, AssertionResult, TestResult } from '@jest/test-result/build/types'; 9 | import { A11yError, A11yResult } from '@sa11y/format'; 10 | import { getA11yError } from '@sa11y/format/__tests__/format.test'; 11 | import { domWithVisualA11yIssues } from '@sa11y/test-utils'; 12 | import { expect } from '@jest/globals'; 13 | import { resultsProcessor, resultsProcessorManualChecks } from '../src/groupViolationResultsProcessor'; 14 | import { ErrorElement, createA11yErrorElements } from '@sa11y/common'; 15 | 16 | const a11yResults: A11yResult[] = []; 17 | const aggregatedResults = makeEmptyAggregatedTestResult(); 18 | const testSuite = createEmptyTestResult(); 19 | let numTestFailures = 0; 20 | 21 | function addTestFailure(suite: TestResult, err: Error, isMatcher = false) { 22 | if (!err) { 23 | throw new Error('Error object is required'); 24 | } 25 | 26 | let failure: AssertionResult; 27 | if (isMatcher) { 28 | failure = { 29 | failureDetails: [ 30 | { 31 | error: { 32 | matcherResult: { 33 | a11yError: err, 34 | }, 35 | }, 36 | }, 37 | ], 38 | fullName: `${err.name}#${numTestFailures}`, // Unique test name to test consolidation 39 | status: 'failed', 40 | ancestorTitles: ['sa11y'], 41 | }; 42 | } else { 43 | failure = { 44 | failureDetails: [err], 45 | fullName: `${err.name}@${numTestFailures}`, // Unique test name to test consolidation 46 | status: 'failed', 47 | ancestorTitles: ['sa11y'], 48 | }; 49 | } 50 | 51 | suite.testResults.push(failure); 52 | suite.numFailingTests += 1; 53 | numTestFailures++; 54 | } 55 | 56 | beforeAll(async () => { 57 | // Prepare test data 58 | const a11yError = await getA11yError(); 59 | const a11yErrorVisual = await getA11yError(domWithVisualA11yIssues); 60 | const combinedViolations = [...a11yError.violations, ...a11yErrorVisual.violations]; 61 | a11yResults.push(...a11yError.a11yResults, ...a11yErrorVisual.a11yResults); 62 | addTestFailure(testSuite, new A11yError(a11yError.violations, a11yError.a11yResults), true); 63 | addTestFailure(testSuite, new A11yError(a11yErrorVisual.violations, a11yErrorVisual.a11yResults)); 64 | // Duplicate test result to test consolidation 65 | addTestFailure(testSuite, new A11yError(a11yError.violations, a11yError.a11yResults)); 66 | addTestFailure(testSuite, new A11yError(a11yErrorVisual.violations, a11yErrorVisual.a11yResults)); 67 | addTestFailure(testSuite, new A11yError(combinedViolations, a11yResults)); 68 | // Add non-a11y test failure 69 | addTestFailure(testSuite, new Error('foo')); 70 | testSuite.testFilePath = '/test/data/sa11y-auto-checks.js'; 71 | addResult(aggregatedResults, testSuite); 72 | }); 73 | 74 | describe('Group Violation Results Processor', () => { 75 | it('should process test results as expected', () => { 76 | // Create a copy as results gets mutated by results processor 77 | const results = JSON.parse(JSON.stringify(aggregatedResults)) as AggregatedResult; 78 | const resultsForMatcher = results; 79 | const processedResultsManual = resultsProcessorManualChecks(resultsForMatcher); 80 | expect(processedResultsManual).toMatchSnapshot(); 81 | 82 | const processedResults = resultsProcessor(results); 83 | expect(processedResults).toMatchSnapshot(); 84 | expect(processedResults).not.toEqual(aggregatedResults); 85 | expect(processedResults.numFailedTestSuites).toBe(1); 86 | }); 87 | 88 | it('should process error Elements as expected', () => { 89 | const errorElements: ErrorElement[] = [ 90 | { 91 | html: '
    Click me
    ', 92 | selectors: '.button', 93 | hierarchy: 'body > div.button', 94 | any: 'role button is interactive', 95 | all: 'element should be focusable and have a click handler', 96 | none: 'no color contrast issues', 97 | relatedNodeAny: 'none', 98 | relatedNodeAll: 'none', 99 | relatedNodeNone: 'none', 100 | message: 'Ensure the element has a valid interactive role and behavior.', 101 | }, 102 | { 103 | html: '', 104 | selectors: 'img', 105 | hierarchy: 'body > img', 106 | all: 'image elements must have an alt attribute', 107 | none: 'no missing alt attribute allowed', 108 | relatedNodeAny: 'none', 109 | relatedNodeAll: 'none', 110 | relatedNodeNone: 'none', 111 | message: 'Add an appropriate alt attribute describing the image.', 112 | any: '', 113 | }, 114 | ]; 115 | 116 | const createdA11yErrorElements = createA11yErrorElements(errorElements); 117 | expect(createdA11yErrorElements).toMatchSnapshot(); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /packages/preset-rules/src/rules.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { A11yConfig, log } from '@sa11y/common'; 9 | 10 | export type WcagVersion = '2.0' | '2.1' | undefined; 11 | export const wcagLevels = ['A', 'AA', 'AAA', '']; 12 | export type WcagLevel = (typeof wcagLevels)[number]; 13 | export const priorities = ['P1', 'P2', 'P3', ''] as const; 14 | export type Priority = (typeof priorities)[number]; 15 | export const defaultPriority: Priority = 'P3'; 16 | export const defaultWcagVersion: WcagVersion = '2.1'; 17 | 18 | export const disabledRules = [ 19 | // Descendancy checks that would fail at unit/component level, but pass at page level 20 | 'aria-required-children', 21 | 'aria-required-parent', 22 | 'dlitem', 23 | 'definition-list', 24 | 'list', 25 | 'listitem', 26 | 'landmark-one-main', 27 | // color-contrast doesn't work for JSDOM and might affect performance 28 | // https://github.com/dequelabs/axe-core/issues/595 29 | // https://github.com/dequelabs/axe-core/blob/develop/doc/examples/jsdom/test/a11y.js 30 | 'color-contrast', 31 | // audio, video elements are stubbed out in JSDOM 32 | // https://github.com/jsdom/jsdom/issues/2155 33 | 'audio-caption', 34 | 'video-caption', 35 | ]; 36 | 37 | /** 38 | * Metadata about rules such as Priority and WCAG SC (overriding values from axe tags) 39 | */ 40 | export type RuleMetadata = { 41 | priority: Priority; 42 | wcagSC: string; 43 | wcagLevel: WcagLevel; 44 | }; 45 | 46 | /** 47 | * Map of rule IDs to RuleMetadata for convenient lookups of metadata based on rule id 48 | */ 49 | export type RuleInfo = Map< 50 | string, // Rule ID 51 | RuleMetadata 52 | >; 53 | 54 | /** 55 | * Get Priority filter for the default ruleset. 56 | * When set, only the rules matching the given priority from the default ruleset will be 57 | * used for the sa11y API and automatic checks. 58 | */ 59 | export function getPriorityFilter(): Priority { 60 | const priorityEnv = process.env.SA11Y_RULESET_PRIORITY; 61 | const priority = priorityEnv && priorities.includes(priorityEnv as Priority) ? (priorityEnv as Priority) : ''; 62 | if (priority) log(`Setting Sa11y rules priority to ${priority}`); 63 | return priority; 64 | } 65 | 66 | /** 67 | * Filter given rules and return those matching given priority or . 68 | */ 69 | export function filterRulesByPriority(rules: RuleInfo, priority: Priority = ''): string[] { 70 | // TODO (refactor): Could be simplified by filtering and returning a RuleInfo map 71 | // and using the rule keys in the calling function 72 | const ruleIDs: string[] = []; 73 | const priorityOverride = priority || getPriorityFilter(); 74 | if (!priorityOverride) { 75 | // if no override is set, return all rules 76 | ruleIDs.push(...rules.keys()); 77 | } else { 78 | for (const [ruleID, ruleInfo] of rules.entries()) { 79 | if (priorityOverride === ruleInfo.priority) ruleIDs.push(ruleID); 80 | } 81 | } 82 | return ruleIDs.sort(); 83 | } 84 | 85 | /** 86 | * Customize sa11y preset rules specific to JSDOM 87 | */ 88 | export function adaptA11yConfigCustomRules(config: A11yConfig, customRules: string[]): A11yConfig { 89 | const adaptedConfig = JSON.parse(JSON.stringify(config)) as A11yConfig; 90 | adaptedConfig.runOnly.values = customRules; 91 | adaptedConfig.ancestry = true; 92 | return adaptedConfig; 93 | } 94 | 95 | /** 96 | * Adapt a11y config to report only incomplete results 97 | */ 98 | export function adaptA11yConfigIncompleteResults(config: A11yConfig): A11yConfig { 99 | const adaptedConfig = JSON.parse(JSON.stringify(config)) as A11yConfig; 100 | adaptedConfig.resultTypes = ['incomplete']; 101 | return adaptedConfig; 102 | } 103 | 104 | /** 105 | * Adapt a11y config by filtering out disabled rules 106 | */ 107 | export function adaptA11yConfig(config: A11yConfig, filterRules = disabledRules): A11yConfig { 108 | const adaptedConfig = JSON.parse(JSON.stringify(config)) as A11yConfig; 109 | adaptedConfig.runOnly.values = config.runOnly.values.filter((rule) => !filterRules.includes(rule)); 110 | adaptedConfig.ancestry = true; 111 | return adaptedConfig; 112 | } 113 | 114 | /** 115 | * Returns config to be used in axe.run() with given rules 116 | * Ref: https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#options-parameter 117 | * @param rules - List of rules to be used in the config 118 | * @returns A11yConfig with formatted rules 119 | */ 120 | export function getA11yConfig(rules: RuleInfo): A11yConfig { 121 | // Note: Making the returned config immutable using Object.freeze() results in 122 | // "TypeError: Cannot add property reporter, object is not extensible" 123 | // even when no local modifications are made. axe.run() itself seems to be modifying the config object. 124 | 125 | return { 126 | runOnly: { 127 | type: 'rule', 128 | values: filterRulesByPriority(rules), 129 | }, 130 | // Disable preloading assets as it causes timeouts for audio/video elements 131 | // with jest and delays webdriver tests by 2-3x when assets are not found (404) 132 | // Ref: https://github.com/dequelabs/axe-core/issues/2528 133 | preload: false, 134 | // Types not listed will still show a maximum of one node 135 | resultTypes: ['violations'], 136 | reporter: 'v1', // Use the default reporter to include failureSummary 137 | }; 138 | } 139 | -------------------------------------------------------------------------------- /packages/preset-rules/src/base.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { getA11yConfig, RuleInfo } from './rules'; 9 | 10 | export const baseRulesInfo: RuleInfo = new Map( 11 | Object.entries({ 12 | 'area-alt': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 13 | 'aria-allowed-attr': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 14 | 'aria-allowed-role': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 15 | 'aria-braille-equivalent': { priority: 'P2', wcagSC: '4.1.2', wcagLevel: 'A' }, 16 | 'aria-conditional-attr': { priority: 'P2', wcagSC: '4.1.2', wcagLevel: 'A' }, 17 | 'aria-deprecated-role': { priority: 'P3', wcagSC: '4.1.2', wcagLevel: 'A' }, 18 | 'aria-command-name': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 19 | 'aria-dialog-name': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 20 | 'aria-hidden-body': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 21 | 'aria-hidden-focus': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 22 | 'aria-input-field-name': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 23 | 'aria-meter-name': { priority: 'P1', wcagSC: '1.1.1', wcagLevel: 'A' }, 24 | 'aria-progressbar-name': { priority: 'P1', wcagSC: '1.1.1', wcagLevel: 'A' }, 25 | 'aria-prohibited-attr': { priority: 'P2', wcagSC: '4.1.2', wcagLevel: 'A' }, 26 | 'aria-required-attr': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 27 | 'aria-required-children': { priority: 'P1', wcagSC: '1.3.1', wcagLevel: 'A' }, 28 | 'aria-required-parent': { priority: 'P1', wcagSC: '1.3.1', wcagLevel: 'A' }, 29 | 'aria-roles': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 30 | 'aria-toggle-field-name': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 31 | 'aria-tooltip-name': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 32 | 'aria-treeitem-name': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 33 | 'aria-valid-attr': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 34 | 'aria-valid-attr-value': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 35 | 'autocomplete-valid': { priority: 'P2', wcagSC: '1.3.5', wcagLevel: 'AA' }, 36 | 'avoid-inline-spacing': { priority: 'P2', wcagSC: '1.4.12', wcagLevel: 'AA' }, 37 | 'button-name': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 38 | 'bypass': { priority: 'P3', wcagSC: '2.4.1', wcagLevel: 'A' }, 39 | 'color-contrast': { priority: 'P1', wcagSC: '1.4.3', wcagLevel: 'AA' }, 40 | 'definition-list': { priority: 'P2', wcagSC: '1.3.1', wcagLevel: 'A' }, 41 | 'dlitem': { priority: 'P2', wcagSC: '1.3.1', wcagLevel: 'A' }, 42 | 'document-title': { priority: 'P2', wcagSC: '2.4.2', wcagLevel: 'A' }, 43 | 'duplicate-id-aria': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 44 | 'form-field-multiple-labels': { priority: 'P2', wcagSC: '3.3.2', wcagLevel: 'A' }, 45 | 'frame-focusable-content': { priority: 'P1', wcagSC: '2.1.1', wcagLevel: 'A' }, 46 | 'frame-title': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 47 | 'frame-title-unique': { priority: 'P3', wcagSC: '4.1.2', wcagLevel: 'A' }, 48 | 'html-has-lang': { priority: 'P2', wcagSC: '3.1.1', wcagLevel: 'A' }, 49 | 'html-lang-valid': { priority: 'P2', wcagSC: '3.1.1', wcagLevel: 'A' }, 50 | 'html-xml-lang-mismatch': { priority: 'P2', wcagSC: '3.1.1', wcagLevel: 'A' }, 51 | 'image-alt': { priority: 'P1', wcagSC: '1.1.1', wcagLevel: 'A' }, 52 | 'input-button-name': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 53 | 'input-image-alt': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 54 | 'label': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 55 | 'link-in-text-block': { priority: 'P2', wcagSC: '1.4.1', wcagLevel: 'A' }, 56 | 'link-name': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 57 | 'list': { priority: 'P2', wcagSC: '1.3.1', wcagLevel: 'A' }, 58 | 'listitem': { priority: 'P2', wcagSC: '1.3.1', wcagLevel: 'A' }, 59 | 'marquee': { priority: 'P1', wcagSC: '2.2.2', wcagLevel: 'A' }, 60 | 'meta-refresh': { priority: 'P1', wcagSC: '2.2.1', wcagLevel: 'A' }, 61 | 'nested-interactive': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 62 | 'no-autoplay-audio': { priority: 'P1', wcagSC: '1.4.2', wcagLevel: 'A' }, 63 | 'object-alt': { priority: 'P1', wcagSC: '1.1.1', wcagLevel: 'A' }, 64 | 'presentation-role-conflict': { priority: 'P2', wcagSC: '4.1.2', wcagLevel: 'A' }, 65 | 'role-img-alt': { priority: 'P1', wcagSC: '1.1.1', wcagLevel: 'A' }, 66 | 'scrollable-region-focusable': { priority: 'P1', wcagSC: '2.1.1', wcagLevel: 'A' }, 67 | 'select-name': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 68 | 'server-side-image-map': { priority: 'P1', wcagSC: '2.1.1', wcagLevel: 'A' }, 69 | 'summary-name': { priority: 'P1', wcagSC: '4.1.2', wcagLevel: 'A' }, 70 | 'svg-img-alt': { priority: 'P1', wcagSC: '1.1.1', wcagLevel: 'A' }, 71 | 'target-size': { priority: 'P2', wcagSC: '2.5.8', wcagLevel: 'AA' }, 72 | 'td-headers-attr': { priority: 'P1', wcagSC: '1.3.1', wcagLevel: 'A' }, 73 | 'th-has-data-cells': { priority: 'P1', wcagSC: '1.3.1', wcagLevel: 'A' }, 74 | 'valid-lang': { priority: 'P2', wcagSC: '3.1.2', wcagLevel: 'A' }, 75 | 'video-caption': { priority: 'P1', wcagSC: '1.2.2', wcagLevel: 'A' }, 76 | }) 77 | ); 78 | 79 | export const base = getA11yConfig(baseRulesInfo); 80 | -------------------------------------------------------------------------------- /packages/assert/__tests__/__snapshots__/assert.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`assertAccessible API should throw an error with a11y issues found for dom with a11y issues 1`] = ` 4 | "2 Accessibility issues found 5 | 6 | 7 | The test has failed the accessibility check. Accessibility Stacktrace/Issues: 8 | 2 HTML elements have accessibility issue(s). 2 rules failed. 9 | 10 | (1) [document-title] Ensure each HTML document contains a non-empty element 11 | * Error element(s) : 1 12 | (1) - HTML element : <html lang=\\"en\\"><head><style>* { pointer-events: all }</style></head><body> 13 | 14 | <h1>Header One</h1> 15 | <h1>Header Two</h1> 16 | <div id=\\"dom-with-issues\\"> 17 | <a href=\\"#\\"></a> 18 | </div> 19 | 20 | 21 | </body></html> 22 | - CSS selector(s) : html 23 | - HTML Tag Hierarchy : 24 | - More Info: To solve the problem, you need to fix at least (1) of the following: 25 | • Document does not have a non-empty <title> element 26 | * Help: 27 | • Help URL: https://dequeuniversity.com/rules/axe/4.11/document-title 28 | • WCAG Criteria: SA11Y-WCAG-SC2.4.2-P2 29 | (2) [link-name] Ensure links have discernible text 30 | * Error element(s) : 1 31 | (1) - HTML element : <a href=\\"#\\"></a> 32 | - CSS selector(s) : a 33 | - HTML Tag Hierarchy : 34 | - More Info: To solve the problem, you need to fix at least (1) of the following: 35 | • Element does not have text that is visible to screen readers 36 | • aria-label attribute does not exist or is empty 37 | • aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty 38 | • Element has no title attribute 39 | - And fix the following: 40 | • Element is in tab order and does not have accessible text 41 | * Help: 42 | • Help URL: https://dequeuniversity.com/rules/axe/4.11/link-name 43 | • WCAG Criteria: SA11Y-WCAG-SC4.1.2-P1 44 | 45 | 46 | For more info about automated accessibility testing: https://sfdc.co/a11y-test 47 | For tips on fixing accessibility bugs: https://sfdc.co/a11y 48 | For technical questions regarding Salesforce accessibility tools, contact our Sa11y team: http://sfdc.co/sa11y-users 49 | For guidance on accessibility related specifics, contact our A11y team: http://sfdc.co/tmp-a11y 50 | " 51 | `; 52 | 53 | exports[`assertAccessible API should throw error with HTML element with a11y issues 1`] = ` 54 | "1 Accessibility issues found 55 | 56 | 57 | The test has failed the accessibility check. Accessibility Stacktrace/Issues: 58 | 1 HTML elements have accessibility issue(s). 1 rules failed. 59 | 60 | (1) [link-name] Ensure links have discernible text 61 | * Error element(s) : 1 62 | (1) - HTML element : <a href=\\"#\\"></a> 63 | - CSS selector(s) : a 64 | - HTML Tag Hierarchy : 65 | - More Info: To solve the problem, you need to fix at least (1) of the following: 66 | • Element does not have text that is visible to screen readers 67 | • aria-label attribute does not exist or is empty 68 | • aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty 69 | • Element has no title attribute 70 | - And fix the following: 71 | • Element is in tab order and does not have accessible text 72 | * Help: 73 | • Help URL: https://dequeuniversity.com/rules/axe/4.11/link-name 74 | • WCAG Criteria: SA11Y-WCAG-SC4.1.2-P1 75 | 76 | 77 | For more info about automated accessibility testing: https://sfdc.co/a11y-test 78 | For tips on fixing accessibility bugs: https://sfdc.co/a11y 79 | For technical questions regarding Salesforce accessibility tools, contact our Sa11y team: http://sfdc.co/sa11y-users 80 | For guidance on accessibility related specifics, contact our A11y team: http://sfdc.co/tmp-a11y 81 | " 82 | `; 83 | 84 | exports[`assertAccessible API should throw error with HTML element with a11y issues when passed with selector keywords 1`] = ` 85 | "1 Accessibility issues found 86 | 87 | 88 | The test has failed the accessibility check. Accessibility Stacktrace/Issues: 89 | 1 HTML elements have accessibility issue(s). 1 rules failed. 90 | 91 | (1) [link-name] Ensure links have discernible text 92 | * Error element(s) : 1 93 | (1) - HTML element : <a href=\\"#\\"></a> 94 | - CSS selector(s) : a 95 | - HTML Tag Hierarchy : 96 | - More Info: To solve the problem, you need to fix at least (1) of the following: 97 | • Element does not have text that is visible to screen readers 98 | • aria-label attribute does not exist or is empty 99 | • aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty 100 | • Element has no title attribute 101 | - And fix the following: 102 | • Element is in tab order and does not have accessible text 103 | * Help: 104 | • Help URL: https://dequeuniversity.com/rules/axe/4.11/link-name 105 | • WCAG Criteria: SA11Y-WCAG-SC4.1.2-P1 106 | 107 | 108 | For more info about automated accessibility testing: https://sfdc.co/a11y-test 109 | For tips on fixing accessibility bugs: https://sfdc.co/a11y 110 | For technical questions regarding Salesforce accessibility tools, contact our Sa11y team: http://sfdc.co/sa11y-users 111 | For guidance on accessibility related specifics, contact our A11y team: http://sfdc.co/tmp-a11y 112 | " 113 | `; 114 | --------------------------------------------------------------------------------