Skip to content

feat(core): expose normalizeRunOptions - #4998

Merged
WilcoFiers merged 3 commits into
developfrom
expose-normalize-run-options
Feb 5, 2026
Merged

feat(core): expose normalizeRunOptions#4998
WilcoFiers merged 3 commits into
developfrom
expose-normalize-run-options

Conversation

@qazwsxedcrfvtgb1111

Copy link
Copy Markdown
Contributor

Move normalizeOptions from axe._audit to axe.utils.normalizeRunOptions

Related to: https://github.com/dequelabs/axe-pro-ml-service/issues/1296

@qazwsxedcrfvtgb1111
qazwsxedcrfvtgb1111 requested a review from a team as a code owner January 27, 2026 23:46
Copilot AI review requested due to automatic review settings January 27, 2026 23:46
@CLAassistant

CLAassistant commented Jan 27, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Moves run options normalization/validation out of Audit#normalizeOptions and exposes it as axe.utils.normalizeRunOptions, updating call sites and typings accordingly.

Changes:

  • Added lib/core/utils/normalize-run-options.js and exported it via lib/core/utils/index.js.
  • Updated Audit#run and finishRun to call axe.utils.normalizeRunOptions.
  • Migrated/added tests for run options normalization and updated axe.d.ts to expose the new util.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
test/core/utils/normalize-run-options.js New unit tests for axe.utils.normalizeRunOptions behavior.
test/core/base/audit.js Updates Audit run tests to assert axe.utils.normalizeRunOptions is invoked; removes old Audit#normalizeOptions tests.
lib/core/utils/normalize-run-options.js New implementation of the normalized run options helper in utils.
lib/core/utils/index.js Exposes normalizeRunOptions on axe.utils.
lib/core/public/finish-run.js Uses axe.utils.normalizeRunOptions instead of axe._audit.normalizeOptions.
lib/core/base/audit.js Uses axe.utils.normalizeRunOptions instead of the removed instance method.
axe.d.ts Adds typings for NormalizedRunOptions and utils.normalizeRunOptions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +71 to +76
if (typeof options.rules === 'object') {
Object.keys(options.rules).forEach(ruleId => {
if (!ruleIds.includes(ruleId)) {
throw new Error('unknown rule `' + ruleId + '` in options.rules');
}
});

Copilot AI Jan 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (typeof options.rules === 'object') will also be true for null, and Object.keys(null) will throw a TypeError. Add an explicit null check (or stricter type check) so invalid rules values fail with a predictable error (or are treated as absent).

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,79 @@
/**
* Ensure all rules that are expected to run exist
* @throws {Error} If any tag or rule specified in options is unknown

Copilot AI Jan 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc says this function throws if any tag is unknown, but unknown tags are only logged (errors are thrown for unknown rules). Update the docstring to reflect the actual behavior.

Suggested change
* @throws {Error} If any tag or rule specified in options is unknown
* @throws {Error} If any rule specified in options is unknown. Unknown tags are logged but do not cause an exception.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +83
it('returns the options object when it is valid', () => {
const opt = {
runOnly: {
type: 'rule',
values: ['positive1', 'positive2']
},
rules: {
negative1: { enabled: false }
}
};
assert(axe.utils.normalizeRunOptions(opt), opt);
});

Copilot AI Jan 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert(axe.utils.normalizeRunOptions(opt), opt); does not assert equality (the second argument is treated as a message). Use an equality assertion (e.g., strict/deep equality) so the test actually validates the returned options object.

Copilot uses AI. Check for mistakes.
Comment on lines +97 to +105
it('allows type: rules as an alternative to type: rule', () => {
const opt = {
runOnly: {
type: 'rules',
values: ['positive1', 'positive2']
}
};
assert(axe.utils.normalizeRunOptions(opt).runOnly.type, 'rule');
});

Copilot AI Jan 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert(axe.utils.normalizeRunOptions(opt).runOnly.type, 'rule'); only asserts that runOnly.type is truthy; it does not check that it equals 'rule'. Switch to an equality assertion so this test would fail if the type is wrong.

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +115
it('allows type: tags as an alternative to type: tag', () => {
const opt = {
runOnly: {
type: 'tags',
values: ['positive']
}
};
assert(axe.utils.normalizeRunOptions(opt).runOnly.type, 'tag');
});

Copilot AI Jan 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert(axe.utils.normalizeRunOptions(opt).runOnly.type, 'tag'); only asserts that runOnly.type is truthy; it does not check that it equals 'tag'. Use an equality assertion here so the test actually validates the normalization.

Copilot uses AI. Check for mistakes.
Comment on lines +233 to +236
runOnly: {
type: 'tags',
values: ['unknwon-tag']
}

Copilot AI Jan 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test data uses the misspelling unknwon-tag. Consider changing this to unknown-tag to keep fixtures readable (it still represents an unknown tag).

Copilot uses AI. Check for mistakes.
Comment thread lib/core/utils/normalize-run-options.js Outdated
Comment on lines +7 to +22
export default function normalizeRunOptions(options) {
const tags = [];
const ruleIds = [];
axe._audit.rules.forEach(rule => {
ruleIds.push(rule.id);
rule.tags.forEach(tag => {
if (!tags.includes(tag)) {
tags.push(tag);
}
});
});
// Validate runOnly
if (['object', 'string'].includes(typeof options.runOnly)) {
if (typeof options.runOnly === 'string') {
options.runOnly = [options.runOnly];
}

Copilot AI Jan 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normalizeRunOptions is exposed on axe.utils (and typed as accepting an optional argument), but the implementation will throw if called with undefined/null (it reads options.runOnly unconditionally). Consider defaulting options to {} (and/or throwing a clear TypeError) so the runtime behavior matches the public API contract.

Copilot uses AI. Check for mistakes.
Comment thread lib/core/base/audit.js
@@ -271,7 +271,7 @@ export default class Audit {
* @param {Function} reject Callback function to fire when audit experiences an error
*/
run(context, options, resolve, reject) {

Copilot AI Jan 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Audit#run now calls axe.utils.normalizeRunOptions(options), but normalizeRunOptions derives valid rule IDs/tags from axe._audit.rules (global), not from this.rules. If Audit is instantiated and used without also setting axe._audit to the same instance, option validation can be performed against the wrong rule set. Consider making normalizeRunOptions validate against the current Audit instance (e.g., by passing this/this.rules in, or otherwise avoiding the global dependency).

Suggested change
run(context, options, resolve, reject) {
run(context, options, resolve, reject) {
// Ensure normalizeRunOptions validates against this Audit instance's rules
axe._audit = this;

Copilot uses AI. Check for mistakes.
Comment thread test/core/utils/normalize-run-options.js Outdated
Comment thread test/core/utils/normalize-run-options.js
Comment thread lib/core/base/audit.js Outdated
Comment thread lib/core/public/finish-run.js Outdated
Comment thread lib/core/utils/normalize-run-options.js Outdated
Comment thread test/core/base/audit.js Outdated
Comment thread test/core/utils/normalize-run-options.js Outdated
straker
straker previously requested changes Feb 2, 2026
Comment thread lib/core/base/audit.js Outdated
@straker

straker commented Feb 3, 2026

Copy link
Copy Markdown
Contributor

@qazwsxedcrfvtgb1111 LGTM. Just need you to sign the CLA before we can merge

@WilcoFiers
WilcoFiers merged commit b8e6a59 into develop Feb 5, 2026
23 checks passed
@WilcoFiers
WilcoFiers deleted the expose-normalize-run-options branch February 5, 2026 16:00
WilcoFiers added a commit that referenced this pull request Jun 1, 2026
##
[4.12.0](v4.11.4...v4.12.0)
(2026-06-01)

### Features

- add gather-internals.js external script
([#5099](#5099))
([c61d58b](c61d58b)),
closes [#5080](#5080)
- **aria-allowed/prohibited-attr, aria-required-parent/children:**
partially support element internals role
([#5080](#5080))
([417b48a](417b48a)),
closes [#5039](#5039)
[#4259](#4259)
- **axe.externalAPIs:** add public api for setting elementInternal data
([#5105](#5105))
([63bab8f](63bab8f))
- **core:** expose normalizeRunOptions
([#4998](#4998))
([b8e6a59](b8e6a59))
- expose axe.resetLocale() to restore the default locale
([#5108](#5108))
([c2b5292](c2b5292)),
closes [#5107](#5107)
- **getRules:** include rule enabled state in returned objects
([#5118](#5118))
([75bf772](75bf772)),
closes [#5116](#5116)
- **list,listitem:** support element internals role
([#5119](#5119))
([7d9d696](7d9d696))
- **new-rule:** check that aria-tab have an accessible name
([#5001](#5001))
([0d4e4e7](0d4e4e7)),
closes [#4842](#4842)
- **rules:** deprecate landmark-complementary-is-top-level rules
([#4992](#4992))
([9e09139](9e09139)),
closes [#4950](#4950)
- **utils:** add `getElementInternals` function
([#5077](#5077))
([1c15f82](1c15f82))

### Bug Fixes

- **aria-allowed-attr:** restrict br and wbr elements to aria-hidden
only ([#4974](#4974))
([c6245e7](c6245e7))
- **aria-conditional-attr:** add support for radio
([#5100](#5100))
([8223c98](8223c98))
- **aria-valid-attr-value:** handle multiple aria-errormessage IDs
([#4973](#4973))
([0489e30](0489e30))
- **aria:** prevent getOwnedVirtual from returning duplicate nodes
([#4987](#4987))
([48ca955](48ca955)),
closes [#4840](#4840)
- **commons/text:** exclude natively hidden elements from
aria-labelledby accessible name
([#5076](#5076))
([ea7202c](ea7202c)),
closes [#4704](#4704)
- **DqElement:** avoid calling constructors with cloneNode
([#5013](#5013))
([0281fa1](0281fa1))
- **existing-rule:** aria-busy now shows an error message for a use with
unallowed children
([#5017](#5017))
([2067b87](2067b87))
- **helpUrl:** ensure axe.configure always updates the help URLs
([#5114](#5114))
([c4f60ff](c4f60ff))
- **label-content-name-mismatch:** match visible text with aria-label
and exclude invisible text
([#5096](#5096))
([3a012a1](3a012a1))
- **locale:** ensure all subtags are correctly set
([#5112](#5112))
([13005ed](13005ed))
- **scrollable-region-focusable:** clarify the issue is in safari
([#4995](#4995))
([4ec5211](4ec5211)),
closes
[WebKit#190870](https://github.com/dequelabs/WebKit/issues/190870)
[WebKit#277290](https://github.com/dequelabs/WebKit/issues/277290)
- **scrollable-region-focusable:** do not fail scroll areas when all
content is visible without scrolling
([#4993](#4993))
([838707a](838707a))
- **target-size:** determine offset using clientRects if target is
display:inline
([#5012](#5012))
([a4b8091](a4b8091))
- **target-size:** ignore position: fixed elements that are offscreen
when page is scrolled
([#5066](#5066))
([1229a6e](1229a6e)),
closes [#5065](#5065)
- **target-size:** ignore widgets that are inline with other inline
elements ([#5000](#5000))
([a8dd81b](a8dd81b))
- **utils/getAncestry:** escape node name
([#5079](#5079))
([d1fabaa](d1fabaa)),
closes [#5078](#5078)
- **utils:** Add null check to parseCrossOriginStylesheet, closes
[#5074](#5074)
([#5075](#5075))
([f12ef32](f12ef32))
- **utils:** update isShadowRoot to use spec-compliant custom element
regex ([#5059](#5059))
([edc6ce2](edc6ce2)),
closes [#5030](#5030)

This PR was opened by a robot 🤖 🎉
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants