Skip to content

Commit

Permalink
refactor: put isContext methods on axe.utils
Browse files Browse the repository at this point in the history
  • Loading branch information
WilcoFiers committed Jul 3, 2024
1 parent 135898b commit f379804
Show file tree
Hide file tree
Showing 11 changed files with 325 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 f379804

Please sign in to comment.