Skip to content

Commit

Permalink
refactor: put isContext methods on axe.utils (#4524)
Browse files Browse the repository at this point in the history
Had a couple scenarios where this code was getting copied over to other
repos. For backward compat reasons that's going to need to stay, but at
least if we have these methods on axe.utils we can prevent problems in
the future if we change how context works.

No QA needed on this one.
  • Loading branch information
WilcoFiers committed Jul 10, 2024
1 parent 5b4cb9d commit a6361bb
Show file tree
Hide file tree
Showing 11 changed files with 333 additions and 60 deletions.
23 changes: 18 additions & 5 deletions axe.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,19 @@ declare namespace axe {
| LabelledShadowDomSelector
| LabelledFramesSelector;
type SelectorList = Array<Selector | FramesSelector> | NodeList;
type ContextProp = Selector | SelectorList;
type ContextObject =
| {
include: Selector | SelectorList;
exclude?: Selector | SelectorList;
include: ContextProp;
exclude?: ContextProp;
}
| {
exclude: Selector | SelectorList;
include?: Selector | SelectorList;
exclude: ContextProp;
include?: ContextProp;
};
type ElementContext = Selector | SelectorList | ContextObject;
type ContextSpec = ContextProp | ContextObject;
/** Synonym to ContextSpec */
type ElementContext = ContextSpec;

type SerialSelector =
| BaseSelector
Expand Down Expand Up @@ -406,6 +409,16 @@ declare namespace axe {
shadowSelect: (selector: CrossTreeSelector) => Element | null;
shadowSelectAll: (selector: CrossTreeSelector) => Element[];
getStandards(): Required<Standards>;
isContextSpec: (context: unknown) => context is ContextSpec;
isContextObject: (context: unknown) => context is ContextObject;
isContextProp: (context: unknown) => context is ContextProp;
isLabelledFramesSelector: (
selector: unknown
) => selector is LabelledFramesSelector;
isLabelledShadowDomSelector: (
selector: unknown
) => selector is LabelledShadowDomSelector;

DqElement: new (
elm: Element,
options?: { absolutePaths?: boolean }
Expand Down
62 changes: 9 additions & 53 deletions lib/core/base/context/normalize-context.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { assert as utilsAssert } from '../../utils';
import {
assert as utilsAssert,
objectHasOwn,
isArrayLike,
isContextObject,
isContextProp,
isLabelledFramesSelector,
isLabelledShadowDomSelector
} from '../../utils';

/**
* Normalize the input of "context" so that many different methods of input are accepted
Expand Down Expand Up @@ -29,16 +37,6 @@ export function normalizeContext(contextSpec) {
return { include, exclude };
}

/**
* Determine if some value can be parsed as a context
* @private
* @param {Mixed} contextSpec The configuration object passed to `Context`
* @return {boolea}
*/
export function isContextSpec(contextSpec) {
return isContextObject(contextSpec) || isContextProp(contextSpec);
}

function normalizeContextList(selectorList = []) {
const normalizedList = [];
if (!isArrayLike(selectorList)) {
Expand Down Expand Up @@ -89,30 +87,6 @@ function normalizeFrameSelectors(frameSelectors) {
return normalizedSelectors;
}

function isContextObject(contextSpec) {
return ['include', 'exclude'].some(
prop => objectHasOwn(contextSpec, prop) && isContextProp(contextSpec[prop])
);
}

function isContextProp(contextList) {
return (
typeof contextList === 'string' ||
contextList instanceof window.Node ||
isLabelledFramesSelector(contextList) ||
isLabelledShadowDomSelector(contextList) ||
isArrayLike(contextList)
);
}

function isLabelledFramesSelector(selector) {
return objectHasOwn(selector, 'fromFrames');
}

function isLabelledShadowDomSelector(selector) {
return objectHasOwn(selector, 'fromShadowDom');
}

function assertLabelledFrameSelector(selector) {
assert(
Array.isArray(selector.fromFrames),
Expand Down Expand Up @@ -157,28 +131,10 @@ function isShadowSelector(selector) {
);
}

function isArrayLike(arr) {
return (
// Avoid DOM weirdness
arr &&
typeof arr === 'object' &&
typeof arr.length === 'number' &&
arr instanceof window.Node === false
);
}

// Wrapper to ensure the correct message
function assert(bool, str) {
utilsAssert(
bool,
`Invalid context; ${str}\nSee: https://github.com/dequelabs/axe-core/blob/master/doc/context.md`
);
}

// Wrapper to prevent throwing for non-objects & null
function objectHasOwn(obj, prop) {
if (!obj || typeof obj !== 'object') {
return false;
}
return Object.prototype.hasOwnProperty.call(obj, prop);
}
3 changes: 1 addition & 2 deletions lib/core/public/run/normalize-run-params.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { clone } from '../../utils';
import { isContextSpec } from '../../base/context/normalize-context';
import { clone, isContextSpec } from '../../utils';

/**
* Normalize the optional params of axe.run()
Expand Down
9 changes: 9 additions & 0 deletions lib/core/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ export { default as getStyleSheetFactory } from './get-stylesheet-factory';
export { default as getXpath } from './get-xpath';
export { default as getAncestry } from './get-ancestry';
export { default as injectStyle } from './inject-style';
export { default as isArrayLike } from './is-array-like';
export {
isContextSpec,
isContextObject,
isContextProp,
isLabelledShadowDomSelector,
isLabelledFramesSelector
} from './is-context';
export { default as isHidden } from './is-hidden';
export { default as isHtmlElement } from './is-html-element';
export { default as isNodeInContext } from './is-node-in-context';
Expand All @@ -59,6 +67,7 @@ export { default as mergeResults } from './merge-results';
export { default as nodeSerializer } from './node-serializer';
export { default as nodeSorter } from './node-sorter';
export { default as nodeLookup } from './node-lookup';
export { default as objectHasOwn } from './object-has-own';
export { default as parseCrossOriginStylesheet } from './parse-crossorigin-stylesheet';
export { default as parseSameOriginStylesheet } from './parse-sameorigin-stylesheet';
export { default as parseStylesheet } from './parse-stylesheet';
Expand Down
15 changes: 15 additions & 0 deletions lib/core/utils/is-array-like.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Checks if a value is array-like.
*
* @param {any} arr - The value to check.
* @returns {boolean} - Returns true if the value is array-like, false otherwise.
*/
export default function isArrayLike(arr) {
return (
!!arr &&
typeof arr === 'object' &&
typeof arr.length === 'number' &&
// Avoid DOM weirdness
arr instanceof window.Node === false
);
}
53 changes: 53 additions & 0 deletions lib/core/utils/is-context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import objectHasOwn from './object-has-own';
import isArrayLike from './is-array-like';

/**
* Determine if some value can be parsed as a context
* @private
* @param {Mixed} contextSpec The configuration object passed to `Context`
* @return {boolea}
*/
export function isContextSpec(contextSpec) {
return isContextObject(contextSpec) || isContextProp(contextSpec);
}

/**
* Checks if the given context specification is a valid context object.
*
* @param {Object} contextSpec - The context specification object to check.
* @returns {boolean} - Returns true if the context specification is a valid context object, otherwise returns false.
*/
export function isContextObject(contextSpec) {
return ['include', 'exclude'].some(
prop => objectHasOwn(contextSpec, prop) && isContextProp(contextSpec[prop])
);
}

/**
* Checks if the given contextList is a valid context property.
* @param {string|Node|Array} contextList - The contextList to check.
* @returns {boolean} - Returns true if the contextList is a valid context property, otherwise false.
*/
export function isContextProp(contextList) {
return (
typeof contextList === 'string' ||
contextList instanceof window.Node ||
isLabelledFramesSelector(contextList) ||
isLabelledShadowDomSelector(contextList) ||
isArrayLike(contextList)
);
}

export function isLabelledFramesSelector(selector) {
// This doesn't guarantee the selector is valid.
// Just that this isn't a runOptions object
// Normalization will ignore invalid selectors
return objectHasOwn(selector, 'fromFrames');
}

export function isLabelledShadowDomSelector(selector) {
// This doesn't guarantee the selector is valid.
// Just that this isn't a runOptions object
// Normalization will ignore invalid selectors
return objectHasOwn(selector, 'fromShadowDom');
}
7 changes: 7 additions & 0 deletions lib/core/utils/object-has-own.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Wrapper to prevent throwing for non-objects & null
export default function objectHasOwn(obj, prop) {
if (!obj || typeof obj !== 'object') {
return false;
}
return Object.prototype.hasOwnProperty.call(obj, prop);
}
30 changes: 30 additions & 0 deletions test/core/utils/is-array-like.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
describe('axe.utils.isArrayLike', () => {
const isArrayLike = axe.utils.isArrayLike;

it('is true for an array', () => {
assert.isTrue(isArrayLike([]));
});

it('is true for an array-like object', () => {
assert.isTrue(isArrayLike({ length: 1 }));
});

it('is false for strings (which also have .length)', () => {
assert.isFalse(isArrayLike('string'));
});

it('is false for a Node with .length', () => {
const div = document.createElement('div');
div.length = 123;
assert.isFalse(isArrayLike(div));
});

it('is false for non-array-like objects', () => {
assert.isFalse(isArrayLike({}));
assert.isFalse(isArrayLike(null));
assert.isFalse(isArrayLike(undefined));
assert.isFalse(isArrayLike(1));
assert.isFalse(isArrayLike(true));
assert.isFalse(isArrayLike(false));
});
});
Loading

0 comments on commit a6361bb

Please sign in to comment.