Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: put isContext methods on axe.utils #4524

Merged
merged 2 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading