diff --git a/build/tasks/esbuild.js b/build/tasks/esbuild.js index ad074a52..546c9317 100644 --- a/build/tasks/esbuild.js +++ b/build/tasks/esbuild.js @@ -1,5 +1,36 @@ const { build } = require('esbuild'); const path = require('path'); +const fs = require('fs'); + +// [a11y-core]: resolve ip-protection imports to real file in monorepo, +// or fall back to a stub when building axe-core standalone (CI). +// Set A11Y_FINGERPRINT_PATH env var to override the fingerprint module location. +const fingerprintFallback = { + name: 'a11y-fingerprint-fallback', + setup(pluginBuild) { + pluginBuild.onResolve( + { filter: /ip-protection\/utils\/fingerprint/ }, + args => { + if (process.env.A11Y_FINGERPRINT_PATH) { + const envPath = path.resolve(process.env.A11Y_FINGERPRINT_PATH); + if (fs.existsSync(envPath)) { + return { path: envPath }; + } + } + const realPath = path.resolve(args.resolveDir, args.path + '.js'); + if (fs.existsSync(realPath)) { + return { path: realPath }; + } + return { + path: path.resolve( + __dirname, + '../../lib/core/utils/fingerprint-stub.js' + ) + }; + } + ); + } +}; module.exports = function (grunt) { grunt.registerMultiTask( @@ -24,7 +55,8 @@ module.exports = function (grunt) { outfile: path.join(dest, name), minify: false, format: 'esm', - bundle: true + bundle: true, + plugins: [fingerprintFallback] }) .then(done) .catch(e => { diff --git a/build/tasks/validate.js b/build/tasks/validate.js index 9b53a17f..79218aa7 100644 --- a/build/tasks/validate.js +++ b/build/tasks/validate.js @@ -227,6 +227,15 @@ function createSchemas() { description: { required: true, type: 'string' + }, + violationConfidence: { + type: 'number' + }, + needsReviewConfidence: { + type: 'number' + }, + defaultCategory: { + type: 'string' } } } diff --git a/lib/checks/aria/aria-prohibited-attr-evaluate.js b/lib/checks/aria/aria-prohibited-attr-evaluate.js index bee8b99a..f2225809 100644 --- a/lib/checks/aria/aria-prohibited-attr-evaluate.js +++ b/lib/checks/aria/aria-prohibited-attr-evaluate.js @@ -59,7 +59,15 @@ export default function ariaProhibitedAttrEvaluate( let messageKey = role !== null ? 'hasRole' : 'noRole'; messageKey += prohibited.length > 1 ? 'Plural' : 'Singular'; - this.data({ role, nodeName, messageKey, prohibited }); + // a11y-rule-aria-prohibited-attr: reviewPayload for bulk NR + const accessibleName = axe.commons.text.accessibleText(node); + this.data({ + role, + nodeName, + messageKey, + prohibited, + reviewPayload: { visualHelperData: { accessibleName } } + }); // `subtreeDescendant` to override namedFromContents const textContent = subtreeText(virtualNode, { subtreeDescendant: true }); diff --git a/lib/checks/color/color-contrast-evaluate.js b/lib/checks/color/color-contrast-evaluate.js index e12d2f62..1161dd0b 100644 --- a/lib/checks/color/color-contrast-evaluate.js +++ b/lib/checks/color/color-contrast-evaluate.js @@ -148,7 +148,20 @@ export default function colorContrastEvaluate(node, options, virtualNode) { fontWeight: bold ? 'bold' : 'normal', messageKey: missing, expectedContrastRatio: expected + ':1', - shadowColor: shadowColor ? shadowColor.toHexString() : undefined + shadowColor: shadowColor ? shadowColor.toHexString() : undefined, + // a11y-rule-color-contrast: reviewPayload for bulk NR + reviewPayload: { + visualHelperData: { + fgColor: fgColor ? fgColor.toHexString() : null, + bgColor: bgColor ? bgColor.toHexString() : null, + isLargeText: !isSmallFont, + textContent: visibleText + ? visibleText.length <= 100 + ? visibleText + : visibleText.slice(0, visibleText.lastIndexOf(' ', 100) || 100) + : null + } + } }); // We don't know, so we'll put it into Can't Tell diff --git a/lib/checks/label/label-content-name-mismatch-evaluate.js b/lib/checks/label/label-content-name-mismatch-evaluate.js index f5b72c88..56d41319 100644 --- a/lib/checks/label/label-content-name-mismatch-evaluate.js +++ b/lib/checks/label/label-content-name-mismatch-evaluate.js @@ -140,10 +140,29 @@ function labelContentNameMismatchEvaluate(node, options, virtualNode) { isHumanInterpretable(accText) < 1 || isHumanInterpretable(visibleText) < 1 ) { + // a11y-rule-label-content-name-mismatch: reviewPayload for bulk NR + this.data({ + reviewPayload: { + visualHelperData: { + accessibleName: accText || null + } + } + }); return undefined; } - return isStringContained(visibleText, accText); + const result = isStringContained(visibleText, accText); + if (!result) { + // a11y-rule-label-content-name-mismatch: reviewPayload for bulk NR + this.data({ + reviewPayload: { + visualHelperData: { + accessibleName: accText || null + } + } + }); + } + return result; } export default labelContentNameMismatchEvaluate; diff --git a/lib/core/reporters/helpers/process-aggregate.js b/lib/core/reporters/helpers/process-aggregate.js index dfd14361..3d80208c 100644 --- a/lib/core/reporters/helpers/process-aggregate.js +++ b/lib/core/reporters/helpers/process-aggregate.js @@ -103,5 +103,9 @@ function trimElementSpec(elmSpec = {}, runOptions) { if (runOptions.xpath) { serialElm.xpath = elmSpec.xpath ?? ['/']; } + // [a11y-core]: propagate htmlHash to final output + if (elmSpec.htmlHash) { + serialElm.htmlHash = elmSpec.htmlHash; + } return serialElm; } diff --git a/lib/core/utils/dq-element.js b/lib/core/utils/dq-element.js index b66e48f2..cdaedd29 100644 --- a/lib/core/utils/dq-element.js +++ b/lib/core/utils/dq-element.js @@ -7,6 +7,7 @@ import cache from '../base/cache'; import memoize from './memoize'; import getNodeAttributes from './get-node-attributes'; import VirtualNode from '../../core/base/virtual-node/virtual-node'; +import { computeFingerprintHash } from '../../../../ip-protection/utils/fingerprint'; const CACHE_KEY = 'DqElm.RunOptions'; @@ -341,6 +342,9 @@ const DqElement = memoize(function DqElement(elm, options, spec) { this.nodeIndexes = [this._virtualNode.nodeIndex]; } + // [a11y-core]: compute htmlHash before truncation + this._htmlHash = computeFingerprintHash(this._element?.outerHTML); + /** * The generated HTML source code of the element * @type {String|null} @@ -411,7 +415,8 @@ DqElement.prototype = { xpath: this.xpath, ancestry: this.ancestry, nodeIndexes: this.nodeIndexes, - fromFrame: this.fromFrame + fromFrame: this.fromFrame, + htmlHash: this._htmlHash }; if (this._includeElementInJson) { spec.element = this._element; diff --git a/lib/core/utils/fingerprint-stub.js b/lib/core/utils/fingerprint-stub.js new file mode 100644 index 00000000..3404a8c3 --- /dev/null +++ b/lib/core/utils/fingerprint-stub.js @@ -0,0 +1,7 @@ +// Fallback stub used when axe-core builds outside the a11y-engine monorepo. +// The real fingerprint logic lives in ip-protection/utils/fingerprint.js +// and is resolved by the esbuild plugin during monorepo builds. +// eslint-disable-next-line no-unused-vars +export function computeFingerprintHash(_outerHTML) { + return null; +} diff --git a/lib/rules/accesskeys.json b/lib/rules/accesskeys.json index 4b3cdfbc..91b07007 100644 --- a/lib/rules/accesskeys.json +++ b/lib/rules/accesskeys.json @@ -6,7 +6,8 @@ "tags": ["cat.keyboard", "best-practice"], "metadata": { "description": "Ensure every accesskey attribute value is unique", - "help": "accesskey attribute value should be unique" + "help": "accesskey attribute value should be unique", + "violationConfidence": 99 }, "all": [], "any": [], diff --git a/lib/rules/aria-allowed-attr.json b/lib/rules/aria-allowed-attr.json index d9075969..3533d68a 100644 --- a/lib/rules/aria-allowed-attr.json +++ b/lib/rules/aria-allowed-attr.json @@ -14,7 +14,8 @@ "actIds": ["5c01ea"], "metadata": { "description": "Ensure an element's role supports its ARIA attributes", - "help": "Elements must only use supported ARIA attributes" + "help": "Elements must only use supported ARIA attributes", + "needsReviewConfidence": 50 }, "all": ["aria-allowed-attr"], "any": [], diff --git a/lib/rules/aria-allowed-role.json b/lib/rules/aria-allowed-role.json index 62c14398..998c0c67 100644 --- a/lib/rules/aria-allowed-role.json +++ b/lib/rules/aria-allowed-role.json @@ -7,7 +7,8 @@ "tags": ["cat.aria", "best-practice"], "metadata": { "description": "Ensure role attribute has an appropriate value for the element", - "help": "ARIA role should be appropriate for the element" + "help": "ARIA role should be appropriate for the element", + "needsReviewConfidence": 50 }, "all": [], "any": ["aria-allowed-role"], diff --git a/lib/rules/aria-hidden-focus.json b/lib/rules/aria-hidden-focus.json index 84f36292..af661357 100755 --- a/lib/rules/aria-hidden-focus.json +++ b/lib/rules/aria-hidden-focus.json @@ -18,7 +18,8 @@ "actIds": ["6cfa84"], "metadata": { "description": "Ensure aria-hidden elements are not focusable nor contain focusable elements", - "help": "ARIA hidden element must not be focusable or contain focusable elements" + "help": "ARIA hidden element must not be focusable or contain focusable elements", + "needsReviewConfidence": 50 }, "all": [ "focusable-modal-open", diff --git a/lib/rules/aria-input-field-name.json b/lib/rules/aria-input-field-name.json index d43ec944..4e8b97ef 100644 --- a/lib/rules/aria-input-field-name.json +++ b/lib/rules/aria-input-field-name.json @@ -18,7 +18,8 @@ "actIds": ["e086e5"], "metadata": { "description": "Ensure every ARIA input field has an accessible name", - "help": "ARIA input fields must have an accessible name" + "help": "ARIA input fields must have an accessible name", + "needsReviewConfidence": 50 }, "all": [], "any": ["aria-label", "aria-labelledby", "non-empty-title"], diff --git a/lib/rules/aria-meter-name.json b/lib/rules/aria-meter-name.json index 8c9a253a..c1c32abb 100644 --- a/lib/rules/aria-meter-name.json +++ b/lib/rules/aria-meter-name.json @@ -14,7 +14,8 @@ ], "metadata": { "description": "Ensure every ARIA meter node has an accessible name", - "help": "ARIA meter nodes must have an accessible name" + "help": "ARIA meter nodes must have an accessible name", + "violationConfidence": 99 }, "all": [], "any": ["aria-label", "aria-labelledby", "non-empty-title"], diff --git a/lib/rules/aria-prohibited-attr.json b/lib/rules/aria-prohibited-attr.json index 3cfe5949..c3b4ef9c 100644 --- a/lib/rules/aria-prohibited-attr.json +++ b/lib/rules/aria-prohibited-attr.json @@ -14,7 +14,10 @@ "actIds": ["5c01ea"], "metadata": { "description": "Ensure ARIA attributes are not prohibited for an element's role", - "help": "Elements must only use permitted ARIA attributes" + "help": "Elements must only use permitted ARIA attributes", + "violationConfidence": 90, + "needsReviewConfidence": 50, + "defaultCategory": "names-and-labels" }, "all": [], "any": [], diff --git a/lib/rules/aria-required-children.json b/lib/rules/aria-required-children.json index 3ff50d1a..03b3529f 100644 --- a/lib/rules/aria-required-children.json +++ b/lib/rules/aria-required-children.json @@ -15,7 +15,8 @@ "actIds": ["bc4a75", "ff89c9"], "metadata": { "description": "Ensure elements with an ARIA role that require child roles contain them", - "help": "Certain ARIA roles must contain particular children" + "help": "Certain ARIA roles must contain particular children", + "needsReviewConfidence": 50 }, "all": [], "any": ["aria-required-children"], diff --git a/lib/rules/aria-toggle-field-name.json b/lib/rules/aria-toggle-field-name.json index 7f277bcf..a29d62e2 100644 --- a/lib/rules/aria-toggle-field-name.json +++ b/lib/rules/aria-toggle-field-name.json @@ -18,7 +18,8 @@ "actIds": ["e086e5"], "metadata": { "description": "Ensure every ARIA toggle field has an accessible name", - "help": "ARIA toggle fields must have an accessible name" + "help": "ARIA toggle fields must have an accessible name", + "needsReviewConfidence": 50 }, "all": [], "any": [ diff --git a/lib/rules/aria-treeitem-name.json b/lib/rules/aria-treeitem-name.json index 15e6b80d..f94ed71a 100644 --- a/lib/rules/aria-treeitem-name.json +++ b/lib/rules/aria-treeitem-name.json @@ -6,7 +6,8 @@ "tags": ["cat.aria", "best-practice"], "metadata": { "description": "Ensure every ARIA treeitem node has an accessible name", - "help": "ARIA treeitem nodes should have an accessible name" + "help": "ARIA treeitem nodes should have an accessible name", + "violationConfidence": 99 }, "all": [], "any": [ diff --git a/lib/rules/aria-valid-attr-value.json b/lib/rules/aria-valid-attr-value.json index 038a8dc5..afe72fab 100644 --- a/lib/rules/aria-valid-attr-value.json +++ b/lib/rules/aria-valid-attr-value.json @@ -14,7 +14,8 @@ "actIds": ["6a7281"], "metadata": { "description": "Ensure all ARIA attributes have valid values", - "help": "ARIA attributes must conform to valid values" + "help": "ARIA attributes must conform to valid values", + "needsReviewConfidence": 50 }, "all": ["aria-valid-attr-value", "aria-errormessage", "aria-level"], "any": [], diff --git a/lib/rules/blink.json b/lib/rules/blink.json index 6d3205f8..e76a4758 100644 --- a/lib/rules/blink.json +++ b/lib/rules/blink.json @@ -18,7 +18,8 @@ ], "metadata": { "description": "Ensure elements are not used", - "help": " elements are deprecated and must not be used" + "help": " elements are deprecated and must not be used", + "violationConfidence": 99 }, "all": [], "any": [], diff --git a/lib/rules/button-name.json b/lib/rules/button-name.json index ec421553..4037ad55 100644 --- a/lib/rules/button-name.json +++ b/lib/rules/button-name.json @@ -20,7 +20,8 @@ "actIds": ["97a4e1", "m6b1q3"], "metadata": { "description": "Ensure buttons have discernible text", - "help": "Buttons must have discernible text" + "help": "Buttons must have discernible text", + "needsReviewConfidence": 50 }, "all": [], "any": [ diff --git a/lib/rules/bypass.json b/lib/rules/bypass.json index 66eeed20..570e1df4 100644 --- a/lib/rules/bypass.json +++ b/lib/rules/bypass.json @@ -21,7 +21,8 @@ "actIds": ["cf77f2", "047fe0", "b40fd1", "3e12e1", "ye5d6e"], "metadata": { "description": "Ensure each page has at least one mechanism for a user to bypass navigation and jump straight to the content", - "help": "Page must have means to bypass repeated blocks" + "help": "Page must have means to bypass repeated blocks", + "needsReviewConfidence": 50 }, "all": [], "any": ["internal-link-present", "header-present", "landmark"], diff --git a/lib/rules/color-contrast-enhanced.json b/lib/rules/color-contrast-enhanced.json index bad39ce4..5af07dfb 100644 --- a/lib/rules/color-contrast-enhanced.json +++ b/lib/rules/color-contrast-enhanced.json @@ -8,7 +8,9 @@ "actIds": ["09o5cg"], "metadata": { "description": "Ensure the contrast between foreground and background colors meets WCAG 2 AAA enhanced contrast ratio thresholds", - "help": "Elements must meet enhanced color contrast ratio thresholds" + "help": "Elements must meet enhanced color contrast ratio thresholds", + "needsReviewConfidence": 50, + "defaultCategory": "text-contrast-enhanced" }, "all": [], "any": ["color-contrast-enhanced"], diff --git a/lib/rules/color-contrast.json b/lib/rules/color-contrast.json index 8c3261f1..61382756 100644 --- a/lib/rules/color-contrast.json +++ b/lib/rules/color-contrast.json @@ -18,7 +18,9 @@ "actIds": ["afw4f7", "09o5cg"], "metadata": { "description": "Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds", - "help": "Elements must meet minimum color contrast ratio thresholds" + "help": "Elements must meet minimum color contrast ratio thresholds", + "needsReviewConfidence": 50, + "defaultCategory": "text-contrast" }, "all": [], "any": ["color-contrast"], diff --git a/lib/rules/css-orientation-lock.json b/lib/rules/css-orientation-lock.json index e987805b..43e2b371 100644 --- a/lib/rules/css-orientation-lock.json +++ b/lib/rules/css-orientation-lock.json @@ -17,7 +17,9 @@ "actIds": ["b33eff"], "metadata": { "description": "Ensure content is not locked to any specific display orientation, and the content is operable in all display orientations", - "help": "CSS Media queries must not lock display orientation" + "help": "CSS Media queries must not lock display orientation", + "violationConfidence": 90, + "needsReviewConfidence": 50 }, "all": ["css-orientation-lock"], "any": [], diff --git a/lib/rules/duplicate-id-aria.json b/lib/rules/duplicate-id-aria.json index 9a52cac8..5fb840df 100644 --- a/lib/rules/duplicate-id-aria.json +++ b/lib/rules/duplicate-id-aria.json @@ -17,7 +17,8 @@ "actIds": ["3ea0c8"], "metadata": { "description": "Ensure every id attribute value used in ARIA and in labels is unique", - "help": "IDs used in ARIA and labels must be unique" + "help": "IDs used in ARIA and labels must be unique", + "needsReviewConfidence": 50 }, "all": [], "any": ["duplicate-id-aria"], diff --git a/lib/rules/focus-order-semantics.json b/lib/rules/focus-order-semantics.json index eead6e85..e50f11d8 100644 --- a/lib/rules/focus-order-semantics.json +++ b/lib/rules/focus-order-semantics.json @@ -14,7 +14,8 @@ ], "metadata": { "description": "Ensure elements in the focus order have a role appropriate for interactive content", - "help": "Elements in the focus order should have an appropriate role" + "help": "Elements in the focus order should have an appropriate role", + "violationConfidence": 90 }, "all": [], "any": ["has-widget-role", "valid-scrollable-semantics"], diff --git a/lib/rules/form-field-multiple-labels.json b/lib/rules/form-field-multiple-labels.json index aa0d766d..a5dca1cd 100644 --- a/lib/rules/form-field-multiple-labels.json +++ b/lib/rules/form-field-multiple-labels.json @@ -16,7 +16,8 @@ ], "metadata": { "description": "Ensure form field does not have multiple label elements", - "help": "Form field must not have multiple label elements" + "help": "Form field must not have multiple label elements", + "needsReviewConfidence": 50 }, "all": [], "any": [], diff --git a/lib/rules/frame-title-unique.json b/lib/rules/frame-title-unique.json index 2c9054be..4e9ce6be 100644 --- a/lib/rules/frame-title-unique.json +++ b/lib/rules/frame-title-unique.json @@ -17,7 +17,8 @@ "actIds": ["4b1c6c"], "metadata": { "description": "Ensure