Skip to content

Commit 381b2c3

Browse files
dbjorgeWilcoFiersstraker
authored
refactor: make element spec processing more cosistent (#4093)
* DqElement.setSerializer initial impl * implemented and passing integration tests * Separated nodeSerializer * Cleanup & tests * prevent html: "Undefined" when noHtml is set * Update test/core/public/run-partial.js * Tweaks * Change to nodeSerializer.update * WIP: Delay DqElement serialization * Delay node serialization * Tweak * Simplify a bit further * Cleanup * Improve performance * Update raw reporter * DqElement in checkHelper * Update lib/core/utils/check-helper.js Co-authored-by: Steven Lambert <[email protected]> * Update lib/core/utils/dq-element.js * Update lib/core/utils/node-serializer.js Co-authored-by: Steven Lambert <[email protected]> --------- Co-authored-by: Wilco Fiers <[email protected]> Co-authored-by: Wilco Fiers <[email protected]> Co-authored-by: Steven Lambert <[email protected]>
1 parent 1494b4c commit 381b2c3

32 files changed

+1008
-262
lines changed

doc/run-partial.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ const axeResults = await axe.finishRun(partialResults, options);
1313

1414
**note**: The code in this page uses native DOM methods. This will only work on frames with the same origin. Scripts do not have access to `contentWindow` of cross-origin frames. Use of `runPartial` and `finishRun` in browser drivers like Selenium and Puppeteer works in the same way.
1515

16+
**note**: Because `axe.runPartial()` is designed to be serialized, it will not return element references even if the `elementRef` option is set.
17+
1618
## axe.runPartial(context, options): Promise<PartialResult>
1719

1820
When using `axe.runPartial()` it is important to keep in mind that the `context` may be different for different frames. For example, `context` can be done in such a way that in frame A, `main` is excluded, and in frame B `footer` is. The `axe.utils.getFrameContexts` method will provide a list of frames that must be tested, and what context to test it with.

lib/core/base/audit.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import standards from '../../standards';
44
import RuleResult from './rule-result';
55
import {
66
clone,
7+
DqElement,
78
queue,
89
preload,
910
findBy,
@@ -270,6 +271,7 @@ export default class Audit {
270271
*/
271272
run(context, options, resolve, reject) {
272273
this.normalizeOptions(options);
274+
DqElement.setRunOptions(options);
273275

274276
// TODO: es-modules_selectCache
275277
axe._selectCache = [];

lib/core/base/check.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import metadataFunctionMap from './metadata-function-map';
22
import CheckResult from './check-result';
3-
import { DqElement, checkHelper, deepMerge } from '../utils';
3+
import { nodeSerializer, checkHelper, deepMerge } from '../utils';
44

55
export function createExecutionContext(spec) {
66
/*eslint no-eval:0 */
@@ -108,7 +108,7 @@ Check.prototype.run = function run(node, options, context, resolve, reject) {
108108
// possible reference error.
109109
if (node && node.actualNode) {
110110
// Save a reference to the node we errored on for futher debugging.
111-
e.errorNode = new DqElement(node).toJSON();
111+
e.errorNode = nodeSerializer.toSpec(node);
112112
}
113113
reject(e);
114114
return;
@@ -162,7 +162,7 @@ Check.prototype.runSync = function runSync(node, options, context) {
162162
// possible reference error.
163163
if (node && node.actualNode) {
164164
// Save a reference to the node we errored on for futher debugging.
165-
e.errorNode = new DqElement(node).toJSON();
165+
e.errorNode = nodeSerializer.toSpec(node);
166166
}
167167
throw e;
168168
}

lib/core/base/rule.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ Rule.prototype.run = function run(context, options = {}, resolve, reject) {
258258
.then(results => {
259259
const result = getResult(results);
260260
if (result) {
261-
result.node = new DqElement(node, options);
261+
result.node = new DqElement(node);
262262
ruleResult.nodes.push(result);
263263

264264
// mark rule as incomplete rather than failure for rules with reviewOnFail
@@ -327,7 +327,7 @@ Rule.prototype.runSync = function runSync(context, options = {}) {
327327

328328
const result = getResult(results);
329329
if (result) {
330-
result.node = node.actualNode ? new DqElement(node, options) : null;
330+
result.node = node.actualNode ? new DqElement(node) : null;
331331
ruleResult.nodes.push(result);
332332

333333
// mark rule as incomplete rather than failure for rules with reviewOnFail

lib/core/public/finish-run.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
mergeResults,
44
publishMetaData,
55
finalizeRuleResult,
6-
DqElement,
6+
nodeSerializer,
77
clone
88
} from '../utils';
99

@@ -47,7 +47,7 @@ function getMergedFrameSpecs({
4747
}
4848
// Include the selector/ancestry/... from the parent frames
4949
return childFrameSpecs.map(childFrameSpec => {
50-
return DqElement.mergeSpecs(childFrameSpec, parentFrameSpec);
50+
return nodeSerializer.mergeSpecs(childFrameSpec, parentFrameSpec);
5151
});
5252
}
5353

lib/core/public/load.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Audit from '../base/audit';
22
import cleanup from './cleanup';
33
import runRules from './run-rules';
44
import respondable from '../utils/respondable';
5+
import nodeSerializer from '../utils/node-serializer';
56

67
/**
78
* Sets up Rules, Messages and default options for Checks, must be invoked before attempting analysis
@@ -33,6 +34,8 @@ function runCommand(data, keepalive, callback) {
3334
context,
3435
options,
3536
(results, cleanupFn) => {
37+
// Serialize all DqElements
38+
results = nodeSerializer.mapRawResults(results);
3639
resolve(results);
3740
// Cleanup AFTER resolve so that selectors can be generated
3841
cleanupFn();

lib/core/public/run-partial.js

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Context from '../base/context';
22
import teardown from './teardown';
33
import {
4-
DqElement,
4+
nodeSerializer,
55
getSelectorData,
66
assert,
77
getEnvironmentData
@@ -22,17 +22,17 @@ export default function runPartial(...args) {
2222
axe._selectorData = getSelectorData(contextObj.flatTree);
2323
axe._running = true;
2424

25+
// Even in the top frame, we don't support this with runPartial
26+
options.elementRef = false;
27+
2528
return (
2629
new Promise((res, rej) => {
2730
axe._audit.run(contextObj, options, res, rej);
2831
})
2932
.then(results => {
30-
results = results.map(({ nodes, ...result }) => ({
31-
nodes: nodes.map(serializeNode),
32-
...result
33-
}));
33+
results = nodeSerializer.mapRawResults(results);
3434
const frames = contextObj.frames.map(({ node }) => {
35-
return new DqElement(node, options).toJSON();
35+
return nodeSerializer.toSpec(node);
3636
});
3737
let environmentData;
3838
if (contextObj.initiator) {
@@ -50,16 +50,3 @@ export default function runPartial(...args) {
5050
})
5151
);
5252
}
53-
54-
function serializeNode({ node, ...nodeResult }) {
55-
nodeResult.node = node.toJSON();
56-
for (const type of ['any', 'all', 'none']) {
57-
nodeResult[type] = nodeResult[type].map(
58-
({ relatedNodes, ...checkResult }) => ({
59-
...checkResult,
60-
relatedNodes: relatedNodes.map(relatedNode => relatedNode.toJSON())
61-
})
62-
);
63-
}
64-
return nodeResult;
65-
}

lib/core/reporters/helpers/process-aggregate.js

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import constants from '../../constants';
2+
import { nodeSerializer } from '../../utils';
23

34
const resultKeys = constants.resultGroups;
45

@@ -43,19 +44,8 @@ export default function processAggregate(results, options) {
4344
if (Array.isArray(ruleResult.nodes) && ruleResult.nodes.length > 0) {
4445
ruleResult.nodes = ruleResult.nodes.map(subResult => {
4546
if (typeof subResult.node === 'object') {
46-
subResult.html = subResult.node.source;
47-
if (options.elementRef && !subResult.node.fromFrame) {
48-
subResult.element = subResult.node.element;
49-
}
50-
if (options.selectors !== false || subResult.node.fromFrame) {
51-
subResult.target = subResult.node.selector;
52-
}
53-
if (options.ancestry) {
54-
subResult.ancestry = subResult.node.ancestry;
55-
}
56-
if (options.xpath) {
57-
subResult.xpath = subResult.node.xpath;
58-
}
47+
const serialElm = trimElementSpec(subResult.node, options);
48+
Object.assign(subResult, serialElm);
5949
}
6050
delete subResult.result;
6151
delete subResult.node;
@@ -86,23 +76,32 @@ function normalizeRelatedNodes(node, options) {
8676
.filter(checkRes => Array.isArray(checkRes.relatedNodes))
8777
.forEach(checkRes => {
8878
checkRes.relatedNodes = checkRes.relatedNodes.map(relatedNode => {
89-
const res = {
90-
html: relatedNode?.source ?? 'Undefined'
91-
};
92-
if (options.elementRef && !relatedNode?.fromFrame) {
93-
res.element = relatedNode?.element ?? null;
94-
}
95-
if (options.selectors !== false || relatedNode?.fromFrame) {
96-
res.target = relatedNode?.selector ?? [':root'];
97-
}
98-
if (options.ancestry) {
99-
res.ancestry = relatedNode?.ancestry ?? [':root'];
100-
}
101-
if (options.xpath) {
102-
res.xpath = relatedNode?.xpath ?? ['/'];
103-
}
104-
return res;
79+
return trimElementSpec(relatedNode, options);
10580
});
10681
});
10782
});
10883
}
84+
85+
function trimElementSpec(elmSpec = {}, runOptions) {
86+
// Pass options to limit which properties are calculated
87+
elmSpec = nodeSerializer.dqElmToSpec(elmSpec, runOptions);
88+
const serialElm = {};
89+
if (axe._audit.noHtml) {
90+
serialElm.html = null;
91+
} else {
92+
serialElm.html = elmSpec.source ?? 'Undefined';
93+
}
94+
if (runOptions.elementRef && !elmSpec.fromFrame) {
95+
serialElm.element = elmSpec.element ?? null;
96+
}
97+
if (runOptions.selectors !== false || elmSpec.fromFrame) {
98+
serialElm.target = elmSpec.selector ?? [':root'];
99+
}
100+
if (runOptions.ancestry) {
101+
serialElm.ancestry = elmSpec.ancestry ?? [':root'];
102+
}
103+
if (runOptions.xpath) {
104+
serialElm.xpath = elmSpec.xpath ?? ['/'];
105+
}
106+
return serialElm;
107+
}

lib/core/reporters/raw.js

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { nodeSerializer } from '../utils';
2+
13
const rawReporter = (results, options, callback) => {
24
if (typeof options === 'function') {
35
callback = options;
@@ -13,16 +15,9 @@ const rawReporter = (results, options, callback) => {
1315
const transformedResult = { ...result };
1416
const types = ['passes', 'violations', 'incomplete', 'inapplicable'];
1517
for (const type of types) {
16-
// Some tests don't include all of the types, so we have to guard against that here.
17-
// TODO: ensure tests always use "proper" results to avoid having these hacks in production code paths.
18-
if (transformedResult[type] && Array.isArray(transformedResult[type])) {
19-
transformedResult[type] = transformedResult[type].map(
20-
({ node, ...typeResult }) => {
21-
node = typeof node?.toJSON === 'function' ? node.toJSON() : node;
22-
return { node, ...typeResult };
23-
}
24-
);
25-
}
18+
transformedResult[type] = nodeSerializer.mapRawNodeResults(
19+
transformedResult[type]
20+
);
2621
}
2722

2823
return transformedResult;

lib/core/utils/check-helper.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ function checkHelper(checkResult, options, resolve, reject) {
4343
node = node.actualNode;
4444
}
4545
if (node instanceof window.Node) {
46-
const dqElm = new DqElement(node, options);
46+
const dqElm = new DqElement(node);
4747
checkResult.relatedNodes.push(dqElm);
4848
}
4949
});

0 commit comments

Comments
 (0)