feat(core): expose normalizeRunOptions - #4998
Conversation
There was a problem hiding this comment.
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.jsand exported it vialib/core/utils/index.js. - Updated
Audit#runandfinishRunto callaxe.utils.normalizeRunOptions. - Migrated/added tests for run options normalization and updated
axe.d.tsto 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.
| if (typeof options.rules === 'object') { | ||
| Object.keys(options.rules).forEach(ruleId => { | ||
| if (!ruleIds.includes(ruleId)) { | ||
| throw new Error('unknown rule `' + ruleId + '` in options.rules'); | ||
| } | ||
| }); |
There was a problem hiding this comment.
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).
| @@ -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 | |||
There was a problem hiding this comment.
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.
| * @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. |
| 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); | ||
| }); |
There was a problem hiding this comment.
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.
| 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'); | ||
| }); |
There was a problem hiding this comment.
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.
| 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'); | ||
| }); |
There was a problem hiding this comment.
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.
| runOnly: { | ||
| type: 'tags', | ||
| values: ['unknwon-tag'] | ||
| } |
There was a problem hiding this comment.
The test data uses the misspelling unknwon-tag. Consider changing this to unknown-tag to keep fixtures readable (it still represents an unknown tag).
| 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]; | ||
| } |
There was a problem hiding this comment.
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.
| @@ -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) { | |||
There was a problem hiding this comment.
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).
| run(context, options, resolve, reject) { | |
| run(context, options, resolve, reject) { | |
| // Ensure normalizeRunOptions validates against this Audit instance's rules | |
| axe._audit = this; |
|
@qazwsxedcrfvtgb1111 LGTM. Just need you to sign the CLA before we can merge |
## [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 🤖 🎉
Move normalizeOptions from axe._audit to axe.utils.normalizeRunOptions
Related to: https://github.com/dequelabs/axe-pro-ml-service/issues/1296