├── .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 |
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 |
Audio test
11 |
Video test
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 |
Audio test
11 |
Video test
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 |
23 |
40 |
41 |
42 |
43 |
44 |
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 | validate
56 |
57 |
58 |
59 | hi
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | +
79 | -
80 |
81 | Next
82 |
83 | Back
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 | 
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 :
13 |
14 | Header One
15 | Header Two
16 |
19 |
20 |
21 |
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 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 :
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 :
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 :
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 |
--------------------------------------------------------------------------------