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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "64.4.0",
3
+ "version": "64.5.0",
4
4
  "description": "Run 1000 web accessibility tests from 11 tools and get a standardized report",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/procs/identify.js CHANGED
@@ -209,7 +209,7 @@ exports.identify = async (instance, page) => {
209
209
  };
210
210
  }
211
211
  }
212
- // Return the result (not yet getting IDs from Nu Html Checker lines and columns).
212
+ // Return the result.
213
213
  return elementID;
214
214
  }
215
215
  };
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
- // Add it to the data.
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;
@@ -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 > 600) {
22
- return `${string.slice(0, 300)} … ${string.slice(-300)}`;
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 tagNameData = startTagData[1].match(/^[A-Za-z0-9]+/);
43
- const tagName = tagNameData ? tagNameData[0].toUpperCase() : '';
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 {extract, firstColumn, lastColumn, lastLine, message, subType, type} = item;
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 a summary of the message on the console.
381
- const parts = [msgText.slice(0, 75)];
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
- // If the launch is for a testaro test act:
433
- if (isTestaroTest) {
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
- // Get the screenshot PNG buffers made by the shoot0 and shoot1 tests.
39
- let shoot0PNGBuffer = await fs.readFile(`${tmpDir}/testaro-shoot-0.png`);
40
- let shoot1PNGBuffer = await fs.readFile(`${tmpDir}/testaro-shoot-1.png`);
41
- // Delete the buffer files.
42
- await fs.unlink(`${tmpDir}/testaro-shoot-0.png`);
43
- await fs.unlink(`${tmpDir}/testaro-shoot-1.png`);
44
- // If both buffers exist:
45
- if (shoot0PNGBuffer && shoot1PNGBuffer) {
46
- // Parse them into PNG objects.
47
- let shoot0PNG = PNG.sync.read(shoot0PNGBuffer);
48
- let shoot1PNG = PNG.sync.read(shoot1PNGBuffer);
49
- // If their dimensions differ:
50
- if (shoot1PNG.width !== shoot0PNG.width || shoot1PNG.height !== shoot0PNG.height) {
51
- // Report this.
52
- data.prevented = true;
53
- data.error = 'Screenshot dimensions differ';
54
- data.dimensions = {
55
- shoot0: {
56
- width: shoot0PNG.width,
57
- height: shoot0PNG.height
58
- },
59
- shoot1: {
60
- width: shoot1PNG.width,
61
- height: shoot1PNG.height
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 their dimensions are identical:
104
+ // Otherwise, i.e. if they do not both exist:
66
105
  else {
67
- const {width, height} = shoot0PNG;
68
- // Get the count of differing pixels between the shots.
69
- const pixelChanges = pixelmatch(shoot0PNG.data, shoot1PNG.data, null, width, height);
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
- // Otherwise, i.e. if they do not both exist:
104
- else {
111
+ // If getting or deleting either buffer file failed:
112
+ catch(error) {
105
113
  // Report this.
106
114
  data.prevented = true;
107
- data.error = 'At least 1 screenshot missing';
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 file path or a failure result.
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
- // Make and save the second screenshot.
19
- const pngPath = await shoot(page, 1);
20
- // Return the file path or a failure result.
21
- return {
22
- data: {
23
- prevented: ! pngPath
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(line => line.length > 300 ? `${line.slice(0, 300)}...` : line)
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 {