Skip to content

Commit a6361bb

Browse files
authored
refactor: put isContext methods on axe.utils (#4524)
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.
1 parent 5b4cb9d commit a6361bb

File tree

11 files changed

+333
-60
lines changed

11 files changed

+333
-60
lines changed

axe.d.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,16 +70,19 @@ declare namespace axe {
7070
| LabelledShadowDomSelector
7171
| LabelledFramesSelector;
7272
type SelectorList = Array<Selector | FramesSelector> | NodeList;
73+
type ContextProp = Selector | SelectorList;
7374
type ContextObject =
7475
| {
75-
include: Selector | SelectorList;
76-
exclude?: Selector | SelectorList;
76+
include: ContextProp;
77+
exclude?: ContextProp;
7778
}
7879
| {
79-
exclude: Selector | SelectorList;
80-
include?: Selector | SelectorList;
80+
exclude: ContextProp;
81+
include?: ContextProp;
8182
};
82-
type ElementContext = Selector | SelectorList | ContextObject;
83+
type ContextSpec = ContextProp | ContextObject;
84+
/** Synonym to ContextSpec */
85+
type ElementContext = ContextSpec;
8386

8487
type SerialSelector =
8588
| BaseSelector
@@ -406,6 +409,16 @@ declare namespace axe {
406409
shadowSelect: (selector: CrossTreeSelector) => Element | null;
407410
shadowSelectAll: (selector: CrossTreeSelector) => Element[];
408411
getStandards(): Required<Standards>;
412+
isContextSpec: (context: unknown) => context is ContextSpec;
413+
isContextObject: (context: unknown) => context is ContextObject;
414+
isContextProp: (context: unknown) => context is ContextProp;
415+
isLabelledFramesSelector: (
416+
selector: unknown
417+
) => selector is LabelledFramesSelector;
418+
isLabelledShadowDomSelector: (
419+
selector: unknown
420+
) => selector is LabelledShadowDomSelector;
421+
409422
DqElement: new (
410423
elm: Element,
411424
options?: { absolutePaths?: boolean }

lib/core/base/context/normalize-context.js

Lines changed: 9 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { assert as utilsAssert } from '../../utils';
1+
import {
2+
assert as utilsAssert,
3+
objectHasOwn,
4+
isArrayLike,
5+
isContextObject,
6+
isContextProp,
7+
isLabelledFramesSelector,
8+
isLabelledShadowDomSelector
9+
} from '../../utils';
210

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

32-
/**
33-
* Determine if some value can be parsed as a context
34-
* @private
35-
* @param {Mixed} contextSpec The configuration object passed to `Context`
36-
* @return {boolea}
37-
*/
38-
export function isContextSpec(contextSpec) {
39-
return isContextObject(contextSpec) || isContextProp(contextSpec);
40-
}
41-
4240
function normalizeContextList(selectorList = []) {
4341
const normalizedList = [];
4442
if (!isArrayLike(selectorList)) {
@@ -89,30 +87,6 @@ function normalizeFrameSelectors(frameSelectors) {
8987
return normalizedSelectors;
9088
}
9189

92-
function isContextObject(contextSpec) {
93-
return ['include', 'exclude'].some(
94-
prop => objectHasOwn(contextSpec, prop) && isContextProp(contextSpec[prop])
95-
);
96-
}
97-
98-
function isContextProp(contextList) {
99-
return (
100-
typeof contextList === 'string' ||
101-
contextList instanceof window.Node ||
102-
isLabelledFramesSelector(contextList) ||
103-
isLabelledShadowDomSelector(contextList) ||
104-
isArrayLike(contextList)
105-
);
106-
}
107-
108-
function isLabelledFramesSelector(selector) {
109-
return objectHasOwn(selector, 'fromFrames');
110-
}
111-
112-
function isLabelledShadowDomSelector(selector) {
113-
return objectHasOwn(selector, 'fromShadowDom');
114-
}
115-
11690
function assertLabelledFrameSelector(selector) {
11791
assert(
11892
Array.isArray(selector.fromFrames),
@@ -157,28 +131,10 @@ function isShadowSelector(selector) {
157131
);
158132
}
159133

160-
function isArrayLike(arr) {
161-
return (
162-
// Avoid DOM weirdness
163-
arr &&
164-
typeof arr === 'object' &&
165-
typeof arr.length === 'number' &&
166-
arr instanceof window.Node === false
167-
);
168-
}
169-
170134
// Wrapper to ensure the correct message
171135
function assert(bool, str) {
172136
utilsAssert(
173137
bool,
174138
`Invalid context; ${str}\nSee: https://github.com/dequelabs/axe-core/blob/master/doc/context.md`
175139
);
176140
}
177-
178-
// Wrapper to prevent throwing for non-objects & null
179-
function objectHasOwn(obj, prop) {
180-
if (!obj || typeof obj !== 'object') {
181-
return false;
182-
}
183-
return Object.prototype.hasOwnProperty.call(obj, prop);
184-
}

lib/core/public/run/normalize-run-params.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { clone } from '../../utils';
2-
import { isContextSpec } from '../../base/context/normalize-context';
1+
import { clone, isContextSpec } from '../../utils';
32

43
/**
54
* Normalize the optional params of axe.run()

lib/core/utils/index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ export { default as getStyleSheetFactory } from './get-stylesheet-factory';
4343
export { default as getXpath } from './get-xpath';
4444
export { default as getAncestry } from './get-ancestry';
4545
export { default as injectStyle } from './inject-style';
46+
export { default as isArrayLike } from './is-array-like';
47+
export {
48+
isContextSpec,
49+
isContextObject,
50+
isContextProp,
51+
isLabelledShadowDomSelector,
52+
isLabelledFramesSelector
53+
} from './is-context';
4654
export { default as isHidden } from './is-hidden';
4755
export { default as isHtmlElement } from './is-html-element';
4856
export { default as isNodeInContext } from './is-node-in-context';
@@ -59,6 +67,7 @@ export { default as mergeResults } from './merge-results';
5967
export { default as nodeSerializer } from './node-serializer';
6068
export { default as nodeSorter } from './node-sorter';
6169
export { default as nodeLookup } from './node-lookup';
70+
export { default as objectHasOwn } from './object-has-own';
6271
export { default as parseCrossOriginStylesheet } from './parse-crossorigin-stylesheet';
6372
export { default as parseSameOriginStylesheet } from './parse-sameorigin-stylesheet';
6473
export { default as parseStylesheet } from './parse-stylesheet';

lib/core/utils/is-array-like.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Checks if a value is array-like.
3+
*
4+
* @param {any} arr - The value to check.
5+
* @returns {boolean} - Returns true if the value is array-like, false otherwise.
6+
*/
7+
export default function isArrayLike(arr) {
8+
return (
9+
!!arr &&
10+
typeof arr === 'object' &&
11+
typeof arr.length === 'number' &&
12+
// Avoid DOM weirdness
13+
arr instanceof window.Node === false
14+
);
15+
}

lib/core/utils/is-context.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import objectHasOwn from './object-has-own';
2+
import isArrayLike from './is-array-like';
3+
4+
/**
5+
* Determine if some value can be parsed as a context
6+
* @private
7+
* @param {Mixed} contextSpec The configuration object passed to `Context`
8+
* @return {boolea}
9+
*/
10+
export function isContextSpec(contextSpec) {
11+
return isContextObject(contextSpec) || isContextProp(contextSpec);
12+
}
13+
14+
/**
15+
* Checks if the given context specification is a valid context object.
16+
*
17+
* @param {Object} contextSpec - The context specification object to check.
18+
* @returns {boolean} - Returns true if the context specification is a valid context object, otherwise returns false.
19+
*/
20+
export function isContextObject(contextSpec) {
21+
return ['include', 'exclude'].some(
22+
prop => objectHasOwn(contextSpec, prop) && isContextProp(contextSpec[prop])
23+
);
24+
}
25+
26+
/**
27+
* Checks if the given contextList is a valid context property.
28+
* @param {string|Node|Array} contextList - The contextList to check.
29+
* @returns {boolean} - Returns true if the contextList is a valid context property, otherwise false.
30+
*/
31+
export function isContextProp(contextList) {
32+
return (
33+
typeof contextList === 'string' ||
34+
contextList instanceof window.Node ||
35+
isLabelledFramesSelector(contextList) ||
36+
isLabelledShadowDomSelector(contextList) ||
37+
isArrayLike(contextList)
38+
);
39+
}
40+
41+
export function isLabelledFramesSelector(selector) {
42+
// This doesn't guarantee the selector is valid.
43+
// Just that this isn't a runOptions object
44+
// Normalization will ignore invalid selectors
45+
return objectHasOwn(selector, 'fromFrames');
46+
}
47+
48+
export function isLabelledShadowDomSelector(selector) {
49+
// This doesn't guarantee the selector is valid.
50+
// Just that this isn't a runOptions object
51+
// Normalization will ignore invalid selectors
52+
return objectHasOwn(selector, 'fromShadowDom');
53+
}

lib/core/utils/object-has-own.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Wrapper to prevent throwing for non-objects & null
2+
export default function objectHasOwn(obj, prop) {
3+
if (!obj || typeof obj !== 'object') {
4+
return false;
5+
}
6+
return Object.prototype.hasOwnProperty.call(obj, prop);
7+
}

test/core/utils/is-array-like.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
describe('axe.utils.isArrayLike', () => {
2+
const isArrayLike = axe.utils.isArrayLike;
3+
4+
it('is true for an array', () => {
5+
assert.isTrue(isArrayLike([]));
6+
});
7+
8+
it('is true for an array-like object', () => {
9+
assert.isTrue(isArrayLike({ length: 1 }));
10+
});
11+
12+
it('is false for strings (which also have .length)', () => {
13+
assert.isFalse(isArrayLike('string'));
14+
});
15+
16+
it('is false for a Node with .length', () => {
17+
const div = document.createElement('div');
18+
div.length = 123;
19+
assert.isFalse(isArrayLike(div));
20+
});
21+
22+
it('is false for non-array-like objects', () => {
23+
assert.isFalse(isArrayLike({}));
24+
assert.isFalse(isArrayLike(null));
25+
assert.isFalse(isArrayLike(undefined));
26+
assert.isFalse(isArrayLike(1));
27+
assert.isFalse(isArrayLike(true));
28+
assert.isFalse(isArrayLike(false));
29+
});
30+
});

0 commit comments

Comments
 (0)