testaro 64.4.0 → 64.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/procs/identify.js +1 -1
- package/procs/nu.js +53 -2
- package/procs/standardize.js +23 -9
- package/run.js +8 -19
- package/testaro/motion.js +70 -62
- package/testaro/shoot0.js +1 -1
- package/testaro/shoot1.js +20 -9
- package/tests/alfa.js +3 -2
- package/tests/nuVal.js +1 -1
- package/tests/nuVnu.js +1 -1
package/package.json
CHANGED
package/procs/identify.js
CHANGED
package/procs/nu.js
CHANGED
|
@@ -43,14 +43,21 @@ exports.getContent = async (page, withSource) => {
|
|
|
43
43
|
}
|
|
44
44
|
// Otherwise, i.e. if the specified content type was the Playwright page content:
|
|
45
45
|
else {
|
|
46
|
-
//
|
|
46
|
+
// Annotate all elements in the page with unique identifiers.
|
|
47
|
+
await page.evaluate(() => {
|
|
48
|
+
let serialID = 0;
|
|
49
|
+
for (const element of Array.from(document.querySelectorAll('*'))) {
|
|
50
|
+
element.setAttribute('data-testaro-id', `${serialID++}#`);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
// Add the annotated page content to the data.
|
|
47
54
|
data.testTarget = await page.content();
|
|
48
55
|
}
|
|
49
56
|
// Return the data.
|
|
50
57
|
return data;
|
|
51
58
|
};
|
|
52
59
|
// Postprocesses a result from nuVal or nuVnu tests.
|
|
53
|
-
exports.curate = (data, nuData, rules) => {
|
|
60
|
+
exports.curate = async (page, data, nuData, rules) => {
|
|
54
61
|
// Delete most of the test target from the data.
|
|
55
62
|
data.testTarget = `${data.testTarget.slice(0, 200)}…`;
|
|
56
63
|
let result;
|
|
@@ -76,11 +83,55 @@ exports.curate = (data, nuData, rules) => {
|
|
|
76
83
|
return false;
|
|
77
84
|
}
|
|
78
85
|
}));
|
|
86
|
+
}
|
|
87
|
+
// If there is a result:
|
|
88
|
+
if (result) {
|
|
79
89
|
// Remove messages reporting duplicate blank IDs.
|
|
80
90
|
const badMessages = new Set(['Duplicate ID .', 'The first occurrence of ID was here.']);
|
|
81
91
|
result.messages = result.messages.filter(
|
|
82
92
|
message => ! badMessages.has(message.message)
|
|
83
93
|
);
|
|
94
|
+
// For each message:
|
|
95
|
+
for (const message of result.messages) {
|
|
96
|
+
const {extract} = message;
|
|
97
|
+
const testaroIDArray = extract.match(/data-testaro-id="(\d+)#"/);
|
|
98
|
+
// If its extract contains a Testaro identifier:
|
|
99
|
+
if (testaroIDArray) {
|
|
100
|
+
const testaroID = message.testaroID = testaroIDArray[1];
|
|
101
|
+
// Add location data for the element to the message.
|
|
102
|
+
message.elementLocation = await page.evaluate(testaroID => {
|
|
103
|
+
const element = document.querySelector(`[data-testaro-id="${testaroID}#"]`);
|
|
104
|
+
// If any element has that identifier:
|
|
105
|
+
if (element) {
|
|
106
|
+
// Get a box specification and an XPath for the element.
|
|
107
|
+
const box = {};
|
|
108
|
+
const boundingBox = element.getBoundingClientRect() || {};
|
|
109
|
+
if (boundingBox.x) {
|
|
110
|
+
['x', 'y', 'width', 'height'].forEach(coordinate => {
|
|
111
|
+
box[coordinate] = Math.round(boundingBox[coordinate]);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const xPath = window.getXPath(element) || '';
|
|
115
|
+
// Treat them as the element location.
|
|
116
|
+
return {
|
|
117
|
+
box,
|
|
118
|
+
xPath
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
// Otherwise, i.e. if no element has it, make the location data empty.
|
|
122
|
+
return {};
|
|
123
|
+
}, testaroID);
|
|
124
|
+
}
|
|
125
|
+
// Otherwise, i.e. if its extract contains no Testaro identifier:
|
|
126
|
+
else {
|
|
127
|
+
// Add a non-DOM location to the message.
|
|
128
|
+
message.elementLocation = {
|
|
129
|
+
notInDOM: true,
|
|
130
|
+
box: {},
|
|
131
|
+
xPath: ''
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
84
135
|
}
|
|
85
136
|
// Return the result.
|
|
86
137
|
return result;
|
package/procs/standardize.js
CHANGED
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
// Limits the length of and unilinearizes a string.
|
|
19
19
|
const cap = rawString => {
|
|
20
20
|
const string = (rawString || '').replace(/[\s\u2028\u2029]+/g, ' ');
|
|
21
|
-
if (string && string.length >
|
|
22
|
-
return `${string.slice(0,
|
|
21
|
+
if (string && string.length > 1000) {
|
|
22
|
+
return `${string.slice(0, 500)} … ${string.slice(-500)}`;
|
|
23
23
|
}
|
|
24
24
|
else if (string) {
|
|
25
25
|
return string;
|
|
@@ -35,12 +35,12 @@ const getIdentifiers = code => {
|
|
|
35
35
|
// Normalize the code.
|
|
36
36
|
code = code.replace(/\s+/g, ' ').replace(/\\"/g, '"');
|
|
37
37
|
// Get the first start tag of an element, if any.
|
|
38
|
-
const startTagData = code.match(/^.*?<
|
|
38
|
+
const startTagData = code.match(/^.*?<([a-zA-][^>]*)/);
|
|
39
39
|
// If there is any:
|
|
40
40
|
if (startTagData) {
|
|
41
41
|
// Get the tag name.
|
|
42
|
-
const
|
|
43
|
-
const tagName =
|
|
42
|
+
const tagNameArray = startTagData[1].match(/^[A-Za-z0-9]+/);
|
|
43
|
+
const tagName = tagNameArray ? tagNameArray[0].toUpperCase() : '';
|
|
44
44
|
// Get the value of the id attribute, if any.
|
|
45
45
|
const idData = startTagData[1].match(/ id="([^"]+)"/);
|
|
46
46
|
const id = idData ? idData[1] : '';
|
|
@@ -209,12 +209,23 @@ const doHTMLCS = (result, standardResult, severity) => {
|
|
|
209
209
|
});
|
|
210
210
|
}
|
|
211
211
|
};
|
|
212
|
-
// Converts issue instances from a nuVal or nuVnu result
|
|
212
|
+
// Converts issue instances from a nuVal or nuVnu result.
|
|
213
213
|
const doNu = (withSource, result, standardResult) => {
|
|
214
214
|
const items = result && result.messages;
|
|
215
|
+
// If there are any messages:
|
|
215
216
|
if (items && items.length) {
|
|
217
|
+
// For each one:
|
|
216
218
|
items.forEach(item => {
|
|
217
|
-
const {
|
|
219
|
+
const {
|
|
220
|
+
extract,
|
|
221
|
+
firstColumn,
|
|
222
|
+
lastColumn,
|
|
223
|
+
lastLine,
|
|
224
|
+
message,
|
|
225
|
+
subType,
|
|
226
|
+
type,
|
|
227
|
+
elementLocation
|
|
228
|
+
} = item;
|
|
218
229
|
const identifiers = getIdentifiers(extract);
|
|
219
230
|
if (! identifiers[0] && message) {
|
|
220
231
|
const tagNameLCArray = message.match(
|
|
@@ -229,6 +240,7 @@ const doNu = (withSource, result, standardResult) => {
|
|
|
229
240
|
if (locationSegments.every(segment => typeof segment === 'number')) {
|
|
230
241
|
spec = locationSegments.join(':');
|
|
231
242
|
}
|
|
243
|
+
const {notInDOM} = elementLocation;
|
|
232
244
|
const instance = {
|
|
233
245
|
ruleID: message,
|
|
234
246
|
what: message,
|
|
@@ -236,11 +248,13 @@ const doNu = (withSource, result, standardResult) => {
|
|
|
236
248
|
tagName: identifiers[0],
|
|
237
249
|
id: identifiers[1],
|
|
238
250
|
location: {
|
|
239
|
-
doc: withSource ? 'source' : 'dom',
|
|
251
|
+
doc: withSource ? 'source' : (notInDOM ? 'notInDOM' : 'dom'),
|
|
240
252
|
type: 'code',
|
|
241
253
|
spec
|
|
242
254
|
},
|
|
243
|
-
excerpt: cap(extract)
|
|
255
|
+
excerpt: cap(extract),
|
|
256
|
+
boxID: elementLocation?.box || {},
|
|
257
|
+
pathID: elementLocation?.xPath || ''
|
|
244
258
|
};
|
|
245
259
|
if (type === 'info' && subType === 'warning') {
|
|
246
260
|
instance.ordinalSeverity = 0;
|
package/run.js
CHANGED
|
@@ -377,23 +377,8 @@ const launch = exports.launch = async (
|
|
|
377
377
|
let indentedMsg = '';
|
|
378
378
|
// If debugging is on:
|
|
379
379
|
if (debug) {
|
|
380
|
-
// Log
|
|
381
|
-
|
|
382
|
-
if (msgText.length > 75) {
|
|
383
|
-
parts.push(msgText.slice(75, 150));
|
|
384
|
-
if (msgText.length > 150) {
|
|
385
|
-
const tail = msgText.slice(150).slice(-150);
|
|
386
|
-
if (msgText.length > 300) {
|
|
387
|
-
parts.push('...');
|
|
388
|
-
}
|
|
389
|
-
parts.push(tail.slice(0, 75));
|
|
390
|
-
if (tail.length > 75) {
|
|
391
|
-
parts.push(tail.slice(75));
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
indentedMsg = parts.map(part => ` | ${part}`).join('\n');
|
|
396
|
-
console.log(`\n${indentedMsg}`);
|
|
380
|
+
// Log the start of the message on the console.
|
|
381
|
+
console.log(`\n${msgText.slice(0, 300)}`);
|
|
397
382
|
}
|
|
398
383
|
// Add statistics on the message to the report.
|
|
399
384
|
const msgTextLC = msgText.toLowerCase();
|
|
@@ -429,8 +414,9 @@ const launch = exports.launch = async (
|
|
|
429
414
|
});
|
|
430
415
|
});
|
|
431
416
|
const isTestaroTest = act.type === 'test' && act.which === 'testaro';
|
|
432
|
-
|
|
433
|
-
|
|
417
|
+
const isNuTest = act.type === 'test' && (['nuVal', 'nuVnu'].some(id => act.which === id));
|
|
418
|
+
// If the launch is for a testaro or Nu test act:
|
|
419
|
+
if (isTestaroTest || isNuTest) {
|
|
434
420
|
// Add a script to the page to add a window method to get the XPath of an element.
|
|
435
421
|
await page.addInitScript(() => {
|
|
436
422
|
window.getXPath = element => {
|
|
@@ -477,6 +463,9 @@ const launch = exports.launch = async (
|
|
|
477
463
|
return `/${segments.join('/')}`;
|
|
478
464
|
};
|
|
479
465
|
});
|
|
466
|
+
}
|
|
467
|
+
// If the launch is for a testaro test act:
|
|
468
|
+
if (isTestaroTest) {
|
|
480
469
|
// Add a script to the page to compute the accessible name of an element.
|
|
481
470
|
await page.addInitScript({path: require.resolve('./dist/nameComputation.js')});
|
|
482
471
|
// Add a script to the page to:
|
package/testaro/motion.js
CHANGED
|
@@ -35,76 +35,84 @@ exports.reporter = async page => {
|
|
|
35
35
|
const data = {};
|
|
36
36
|
const totals = [0, 0, 0, 0];
|
|
37
37
|
const standardInstances = [];
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
38
|
+
try {
|
|
39
|
+
// Get the screenshot PNG buffers made by the shoot0 and shoot1 tests.
|
|
40
|
+
let shoot0PNGBuffer = await fs.readFile(`${tmpDir}/testaro-shoot-0.png`);
|
|
41
|
+
let shoot1PNGBuffer = await fs.readFile(`${tmpDir}/testaro-shoot-1.png`);
|
|
42
|
+
// Delete the buffer files.
|
|
43
|
+
await fs.unlink(`${tmpDir}/testaro-shoot-0.png`);
|
|
44
|
+
await fs.unlink(`${tmpDir}/testaro-shoot-1.png`);
|
|
45
|
+
// If both buffers exist:
|
|
46
|
+
if (shoot0PNGBuffer && shoot1PNGBuffer) {
|
|
47
|
+
// Parse them into PNG objects.
|
|
48
|
+
let shoot0PNG = PNG.sync.read(shoot0PNGBuffer);
|
|
49
|
+
let shoot1PNG = PNG.sync.read(shoot1PNGBuffer);
|
|
50
|
+
// If their dimensions differ:
|
|
51
|
+
if (shoot1PNG.width !== shoot0PNG.width || shoot1PNG.height !== shoot0PNG.height) {
|
|
52
|
+
// Report this.
|
|
53
|
+
data.prevented = true;
|
|
54
|
+
data.error = 'Screenshot dimensions differ';
|
|
55
|
+
data.dimensions = {
|
|
56
|
+
shoot0: {
|
|
57
|
+
width: shoot0PNG.width,
|
|
58
|
+
height: shoot0PNG.height
|
|
59
|
+
},
|
|
60
|
+
shoot1: {
|
|
61
|
+
width: shoot1PNG.width,
|
|
62
|
+
height: shoot1PNG.height
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Otherwise, i.e. if their dimensions are identical:
|
|
67
|
+
else {
|
|
68
|
+
const {width, height} = shoot0PNG;
|
|
69
|
+
// Get the count of differing pixels between the shots.
|
|
70
|
+
const pixelChanges = pixelmatch(shoot0PNG.data, shoot1PNG.data, null, width, height);
|
|
71
|
+
// Get the ratio of differing to all pixels as a percentage.
|
|
72
|
+
const changePercent = Math.round(100 * pixelChanges / (width * height));
|
|
73
|
+
// Free the memory used by screenshots.
|
|
74
|
+
shoot0PNG = shoot1PNG = shoot0PNGBuffer = shoot1PNGBuffer = null;
|
|
75
|
+
// If any pixels were changed:
|
|
76
|
+
if (pixelChanges) {
|
|
77
|
+
// Get the ordinal severity from the fractional pixel change.
|
|
78
|
+
const ordinalSeverity = Math.floor(Math.min(3, 0.4 * Math.sqrt(changePercent)));
|
|
79
|
+
// Add to the totals.
|
|
80
|
+
totals[ordinalSeverity] = 1;
|
|
81
|
+
// Get a summary standard instance.
|
|
82
|
+
standardInstances.push({
|
|
83
|
+
ruleID: 'motion',
|
|
84
|
+
what: `Content moves or changes spontaneously (${changePercent}% of pixels changed)`,
|
|
85
|
+
count: 1,
|
|
86
|
+
ordinalSeverity,
|
|
87
|
+
tagName: 'HTML',
|
|
88
|
+
id: '',
|
|
89
|
+
location: {
|
|
90
|
+
doc: 'dom',
|
|
91
|
+
type: 'box',
|
|
92
|
+
spec: {
|
|
93
|
+
x: 0,
|
|
94
|
+
y: 0,
|
|
95
|
+
width,
|
|
96
|
+
height
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
excerpt: '<html>…</html>'
|
|
100
|
+
});
|
|
62
101
|
}
|
|
63
102
|
}
|
|
64
103
|
}
|
|
65
|
-
// Otherwise, i.e. if
|
|
104
|
+
// Otherwise, i.e. if they do not both exist:
|
|
66
105
|
else {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// Get the ratio of differing to all pixels as a percentage.
|
|
71
|
-
const changePercent = Math.round(100 * pixelChanges / (width * height));
|
|
72
|
-
// Free the memory used by screenshots.
|
|
73
|
-
shoot0PNG = shoot1PNG = shoot0PNGBuffer = shoot1PNGBuffer = null;
|
|
74
|
-
// If any pixels were changed:
|
|
75
|
-
if (pixelChanges) {
|
|
76
|
-
// Get the ordinal severity from the fractional pixel change.
|
|
77
|
-
const ordinalSeverity = Math.floor(Math.min(3, 0.4 * Math.sqrt(changePercent)));
|
|
78
|
-
// Add to the totals.
|
|
79
|
-
totals[ordinalSeverity] = 1;
|
|
80
|
-
// Get a summary standard instance.
|
|
81
|
-
standardInstances.push({
|
|
82
|
-
ruleID: 'motion',
|
|
83
|
-
what: `Content moves or changes spontaneously (${changePercent}% of pixels changed)`,
|
|
84
|
-
count: 1,
|
|
85
|
-
ordinalSeverity,
|
|
86
|
-
tagName: 'HTML',
|
|
87
|
-
id: '',
|
|
88
|
-
location: {
|
|
89
|
-
doc: 'dom',
|
|
90
|
-
type: 'box',
|
|
91
|
-
spec: {
|
|
92
|
-
x: 0,
|
|
93
|
-
y: 0,
|
|
94
|
-
width,
|
|
95
|
-
height
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
excerpt: '<html>…</html>'
|
|
99
|
-
});
|
|
100
|
-
}
|
|
106
|
+
// Report this.
|
|
107
|
+
data.prevented = true;
|
|
108
|
+
data.error = 'At least 1 screenshot missing';
|
|
101
109
|
}
|
|
102
110
|
}
|
|
103
|
-
//
|
|
104
|
-
|
|
111
|
+
// If getting or deleting either buffer file failed:
|
|
112
|
+
catch(error) {
|
|
105
113
|
// Report this.
|
|
106
114
|
data.prevented = true;
|
|
107
|
-
data.error =
|
|
115
|
+
data.error = `Screenshot file error (${error.message})`;
|
|
108
116
|
}
|
|
109
117
|
// Return the result.
|
|
110
118
|
return {
|
package/testaro/shoot0.js
CHANGED
|
@@ -17,7 +17,7 @@ const {shoot} = require('../procs/shoot');
|
|
|
17
17
|
exports.reporter = async page => {
|
|
18
18
|
// Make and save the first screenshot.
|
|
19
19
|
const pngPath = await shoot(page, 0);
|
|
20
|
-
// Return the
|
|
20
|
+
// Return whether the screenshot was prevented.
|
|
21
21
|
return {
|
|
22
22
|
data: {
|
|
23
23
|
prevented: ! pngPath
|
package/testaro/shoot1.js
CHANGED
|
@@ -5,22 +5,33 @@
|
|
|
5
5
|
|
|
6
6
|
/*
|
|
7
7
|
shoot1
|
|
8
|
-
This test makes and saves the second of two screenshots.
|
|
8
|
+
This test makes and saves the second of two screenshots. It aborts if the first screenshot was prevented.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
// IMPORTS
|
|
12
12
|
|
|
13
|
+
const fs = require('fs/promises');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const path = require('path');
|
|
13
16
|
const {shoot} = require('../procs/shoot');
|
|
14
17
|
|
|
18
|
+
// CONSTANTS
|
|
19
|
+
|
|
20
|
+
const tmpDir = os.tmpdir();
|
|
21
|
+
|
|
15
22
|
// FUNCTIONS
|
|
16
23
|
|
|
17
24
|
exports.reporter = async page => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
const tempFileNames = await fs.readdir(tmpDir);
|
|
26
|
+
// If there is a shoot0 file:
|
|
27
|
+
if (tempFileNames.includes('testaro-shoot-0.png')) {
|
|
28
|
+
// Make and save the second screenshot.
|
|
29
|
+
const pngPath = await shoot(page, 1);
|
|
30
|
+
// Return whether the screenshot was prevented.
|
|
31
|
+
return {
|
|
32
|
+
data: {
|
|
33
|
+
prevented: ! pngPath
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
26
37
|
};
|
package/tests/alfa.js
CHANGED
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
|
|
18
18
|
let alfaRules = require('@siteimprove/alfa-rules').default;
|
|
19
19
|
const {Audit} = require('@siteimprove/alfa-act');
|
|
20
|
-
const path = require('path');
|
|
21
20
|
const {Playwright} = require('@siteimprove/alfa-playwright');
|
|
22
21
|
|
|
23
22
|
// FUNCTIONS
|
|
@@ -88,7 +87,9 @@ exports.reporter = async (page, report, actIndex) => {
|
|
|
88
87
|
type: targetJ.type,
|
|
89
88
|
tagName: targetJ.name || '',
|
|
90
89
|
path: target.path(),
|
|
91
|
-
codeLines: codeLines.map(
|
|
90
|
+
codeLines: codeLines.map(
|
|
91
|
+
line => line.length > 300 ? `${line.slice(0, 300)}...` : line
|
|
92
|
+
)
|
|
92
93
|
}
|
|
93
94
|
};
|
|
94
95
|
// If the rule summary is missing:
|
package/tests/nuVal.js
CHANGED
|
@@ -65,7 +65,7 @@ exports.reporter = async (page, report, actIndex) => {
|
|
|
65
65
|
data.error = message;
|
|
66
66
|
};
|
|
67
67
|
// Postprocess the response data.
|
|
68
|
-
result = curate(data, nuData, rules);
|
|
68
|
+
result = await curate(page, data, nuData, rules);
|
|
69
69
|
}
|
|
70
70
|
// Otherwise, i.e. if the page content was not obtained:
|
|
71
71
|
else {
|
package/tests/nuVnu.js
CHANGED
|
@@ -69,7 +69,7 @@ exports.reporter = async (page, report, actIndex) => {
|
|
|
69
69
|
// Delete the temporary file.
|
|
70
70
|
await fs.unlink(pagePath);
|
|
71
71
|
// Postprocess the result.
|
|
72
|
-
result = curate(data, nuData, rules);
|
|
72
|
+
result = await curate(page, data, nuData, rules);
|
|
73
73
|
}
|
|
74
74
|
// Otherwise, i.e. if the content was not obtained:
|
|
75
75
|
else {
|