testaro 5.14.2 → 5.14.4

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/README.md CHANGED
@@ -103,7 +103,7 @@ A script is a JSON file with the properties:
103
103
  }
104
104
  ```
105
105
 
106
- The `timeLimit` property is optional. If it is omitted, a default of 300 seconds (5 minutes) is set.
106
+ The `timeLimit` property is optional. If it is omitted, a default of 300 seconds (5 minutes) is set.
107
107
 
108
108
  ### Example
109
109
 
@@ -444,6 +444,8 @@ That would state the expectation that the `result` property of the `acts` item f
444
444
 
445
445
  The first item in each array is an identifier of a property within the `result` property. The item has the format of a string with `.` delimiters. Each `.`-delimited segment its the name of the next property in the hierarchy. If the current object is an array, the next segment must be a non-negative integer, representing the index of an element of the array. For example, `items.1.attributes.0` references the first element of the array that is the `attributes` property of the object that is the second element of the array that is the `items` property of `result`. (In JavaScript, this would be written `items[1].attributes[0]`, but in the `expect` property all property names are `.`-delimited.)
446
446
 
447
+ If there is only 1 item in an array, it states the expectation that the specified property does not exist. Otherwise, there are 3 items in the array.
448
+
447
449
  The second item in each array, if there are 3 items in the array, is an operator, drawn from:
448
450
  - `<`: less than
449
451
  - `=`: equal to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "5.14.2",
3
+ "version": "5.14.4",
4
4
  "description": "Automation of accessibility testing",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/run.js CHANGED
@@ -65,7 +65,17 @@ const tests = {
65
65
  };
66
66
  // Tests that may change the DOM.
67
67
  const domChangers = new Set([
68
- 'axe', 'continuum', 'focAll', 'focInd', 'focOp', 'hover', 'htmlcs', 'ibm', 'menuNav', 'wave'
68
+ 'axe',
69
+ 'continuum',
70
+ 'focAll',
71
+ 'focInd',
72
+ 'focOp',
73
+ 'hover',
74
+ 'htmlcs',
75
+ 'ibm',
76
+ 'menuNav',
77
+ 'textNodes',
78
+ 'wave'
69
79
  ]);
70
80
  // Browser types available in PlayWright.
71
81
  const browserTypeNames = {
@@ -557,23 +567,29 @@ const visit = async (act, page, isStrict) => {
557
567
  };
558
568
  // Returns a property value and whether it satisfies an expectation.
559
569
  const isTrue = (object, specs) => {
560
- let satisfied;
561
570
  const property = specs[0];
562
571
  const propertyTree = property.split('.');
563
- const relation = specs[1];
564
- const criterion = specs[2];
565
572
  let actual = property.length ? object[propertyTree[0]] : object;
566
573
  // Identify the actual value of the specified property.
567
574
  while (propertyTree.length > 1 && actual !== undefined) {
568
575
  propertyTree.shift();
569
576
  actual = actual[propertyTree[0]];
570
577
  }
571
- if (actual === undefined) {
572
- return [null, false];
578
+ // If the expectation is that the property does not exist:
579
+ if (specs.length === 1) {
580
+ // Return whether the expectation is satisfied.
581
+ return [actual, actual === undefined];
573
582
  }
574
- else {
583
+ // Otherwise, i.e. if the expectation is of a property value:
584
+ else if (specs.length === 3) {
575
585
  // Determine whether the expectation was fulfilled.
576
- if (relation === '=') {
586
+ const relation = specs[1];
587
+ const criterion = specs[2];
588
+ let satisfied;
589
+ if (actual === undefined) {
590
+ return [null, false];
591
+ }
592
+ else if (relation === '=') {
577
593
  satisfied = actual === criterion;
578
594
  }
579
595
  else if (relation === '<') {
@@ -591,11 +607,12 @@ const isTrue = (object, specs) => {
591
607
  else if (relation === '!i') {
592
608
  satisfied = ! actual.includes(criterion);
593
609
  }
594
- else if (! relation) {
595
- satisfied = actual === undefined;
596
- }
597
610
  return [actual, satisfied];
598
611
  }
612
+ // Otherwise, i.e. if the specifications are invalid:
613
+ else {
614
+ //
615
+ }
599
616
  };
600
617
  // Adds a wait error result to an act.
601
618
  const waitError = (page, act, error, what) => {
@@ -113,6 +113,12 @@
113
113
  "withItems": true,
114
114
  "what": "keyboard navigation within true-focus menus"
115
115
  },
116
+ {
117
+ "type": "test",
118
+ "which": "nonTable",
119
+ "withItems": true,
120
+ "what": "tables used for layout"
121
+ },
116
122
  {
117
123
  "type": "test",
118
124
  "which": "radioSet",
package/tests/nonTable.js CHANGED
@@ -4,17 +4,21 @@
4
4
  This test reports tables used for layout.
5
5
  */
6
6
  exports.reporter = async (page, withItems) => {
7
- // Identify the visible links without href attributes.
7
+ // Identify the tables used for layout.
8
8
  const badTableTexts = await page.$$eval('table', tables => {
9
+ // Initialize an array of pseudotable texts.
9
10
  const badTableTexts = [];
10
11
  // FUNCTION DEFINITIONS START
11
12
  // Returns a space-minimized copy of a string.
12
13
  const compact = string => string.replace(/[\t\n]/g, '').replace(/\s{2,}/g, ' ').trim();
14
+ // Adds the first 100 characters of the code of a pseudotable to the array of pseudotable texts.
13
15
  const addBad = table => {
14
16
  badTableTexts.push(compact(table.outerHTML).slice(0, 100));
15
17
  };
16
18
  // FUNCTION DEFINITIONS END
19
+ // For each table on the page:
17
20
  tables.forEach(table => {
21
+ // Ignore it if it has a grid or treegrid role, a caption, or a table-like element.
18
22
  const role = table.getAttribute('role');
19
23
  if (
20
24
  table.caption
@@ -23,10 +27,13 @@ exports.reporter = async (page, withItems) => {
23
27
  ) {
24
28
  return;
25
29
  }
30
+ // Otherwise, if the table contains another table:
26
31
  else if (table.querySelector('table')) {
32
+ // Treat it as a pseudotable.
27
33
  addBad(table);
28
34
  return;
29
35
  }
36
+ // Otherwise, if the table has only 1 row or 1 column:
30
37
  else if (
31
38
  table.querySelectorAll('tr').length === 1
32
39
  || Math.max(
@@ -35,18 +42,23 @@ exports.reporter = async (page, withItems) => {
35
42
  .map(row => Array.from(row.querySelectorAll('td')).length)
36
43
  ) === 1
37
44
  ) {
45
+ // Treat it as a pseudotable.
38
46
  addBad(table);
39
47
  return;
40
48
  }
49
+ // Otherwise, if the table contains an object or player:
41
50
  else if (table.querySelector('object, embed, applet, audio, video')) {
51
+ // Treat it as a pseudotable.
42
52
  addBad(table);
43
53
  return;
44
54
  }
45
55
  });
56
+ // Return the array of pseudotable text beginnings.
46
57
  return badTableTexts;
47
58
  });
59
+ // Return the result.
48
60
  const data = {
49
- totals: badTableTexts.length
61
+ total: badTableTexts.length
50
62
  };
51
63
  if (withItems) {
52
64
  data.items = badTableTexts;
@@ -3,9 +3,10 @@
3
3
  This test reports data about specified text nodes.
4
4
  Meanings of detailLevel values:
5
5
  0. Only total node count; no detail.
6
- 1+. Count of ancestry levels to provide data on (1 = text node, 2 = also parent, etc.)
6
+ 1-3. Count of ancestry levels to provide data on (1 = text node, 2 = also parent,
7
+ 3 = also grandparent)
7
8
  */
8
- exports.reporter = async (page, detailLevel, text) => {
9
+ exports.reporter = async (page, detailLevel, text = '') => {
9
10
  let data = {};
10
11
  // Get the data on the text nodes.
11
12
  try {
@@ -13,14 +14,10 @@ exports.reporter = async (page, detailLevel, text) => {
13
14
  const detailLevel = args[0];
14
15
  const text = args[1];
15
16
  const matchNodes = [];
16
- // Normalize the body.
17
+ // Collapse any adjacent text nodes.
17
18
  document.body.normalize();
18
- // Make a copy of the body.
19
- const tempBody = document.body.cloneNode(true);
20
- // Insert it into the document.
21
- document.body.appendChild(tempBody);
22
- // Remove the irrelevant text content from the copy.
23
- const extraElements = Array.from(tempBody.querySelectorAll('style, script, svg'));
19
+ // Remove the irrelevant text content.
20
+ const extraElements = Array.from(document.body.querySelectorAll('style, script, svg'));
24
21
  extraElements.forEach(element => {
25
22
  element.textContent = '';
26
23
  });
@@ -28,8 +25,11 @@ exports.reporter = async (page, detailLevel, text) => {
28
25
  // Compacts a string.
29
26
  const compact = string => string.replace(/\s+/g, ' ').trim();
30
27
  // Compacts and lower-cases a string.
31
- const normalize = string => compact(string).toLowerCase();
32
- // Gets data on an element.
28
+ const standardize = string => compact(string).toLowerCase();
29
+ /*
30
+ Gets data (tagName, text if specified, attributes, refLabels, labels, and children)
31
+ on an element.
32
+ */
33
33
  const getElementData = (element, withText) => {
34
34
  // Initialize the data.
35
35
  const data = {
@@ -80,15 +80,16 @@ exports.reporter = async (page, detailLevel, text) => {
80
80
  return data;
81
81
  };
82
82
  // FUNCTION DEFINITIONS END
83
- const normText = normalize(text);
83
+ const stdText = standardize(text);
84
84
  // Create a collection of the text nodes.
85
- const walker = document.createTreeWalker(tempBody, NodeFilter.SHOW_TEXT);
85
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
86
86
  // Get their count.
87
87
  const data = {nodeCount: 0};
88
88
  let more = true;
89
89
  while(more) {
90
90
  if (walker.nextNode()) {
91
- if (normalize(walker.currentNode.nodeValue).includes(normText)) {
91
+ const stdCurrent = standardize(walker.currentNode.nodeValue);
92
+ if (stdCurrent.includes(stdText)) {
92
93
  data.nodeCount++;
93
94
  matchNodes.push(walker.currentNode);
94
95
  }
@@ -97,27 +98,25 @@ exports.reporter = async (page, detailLevel, text) => {
97
98
  more = false;
98
99
  }
99
100
  }
100
- // If no itemization is required:
101
- if (detailLevel === 0) {
102
- // Return the node count.
103
- return data;
104
- }
105
- // Otherwise, i.e. if itemization is required:
106
- else {
101
+ // If itemization is required:
102
+ if (detailLevel > 0) {
107
103
  // Initialize the item data.
108
104
  data.items = [];
109
- // For each text node matching the specified text:
105
+ // For each text node matching any specified text:
110
106
  matchNodes.forEach(node => {
111
107
  // Initialize the data on it.
112
108
  const itemData = {text: compact(node.nodeValue)};
113
109
  // If ancestral itemization is required:
114
110
  if (detailLevel > 1) {
115
- // Add the ancestral data to the item data.
111
+ // Add the ancestral data, starting with the parent, to the item data.
116
112
  itemData.ancestors = [];
117
113
  let base = node;
118
114
  let currentLevel = 1;
115
+ // For each specified ancestral distance:
119
116
  while(currentLevel++ < detailLevel) {
117
+ // Add data on it to the data on the text node.
120
118
  const newBase = base.parentElement;
119
+ // Omit the text of the text node if the ancestor is its parent.
121
120
  itemData.ancestors.push(getElementData(newBase, currentLevel > 2));
122
121
  base = newBase;
123
122
  }
@@ -126,7 +125,6 @@ exports.reporter = async (page, detailLevel, text) => {
126
125
  data.items.push(itemData);
127
126
  });
128
127
  }
129
- document.body.removeChild(tempBody);
130
128
  return data;
131
129
  }, [detailLevel, text]);
132
130
  }
@@ -6,10 +6,6 @@ const fs = require('fs').promises;
6
6
  const {handleRequest} = require(`${__dirname}/../../run`);
7
7
  const test = process.argv[2];
8
8
  const validateTests = async () => {
9
- const totals = {
10
- attempts: 0,
11
- successes: 0
12
- };
13
9
  const scriptFileNames = await fs.readdir(`${__dirname}/../tests/scripts`);
14
10
  for (const scriptFileName of scriptFileNames.filter(fileName => fileName === `${test}.json`)) {
15
11
  const rawScriptJSON = await fs
@@ -29,37 +25,25 @@ const validateTests = async () => {
29
25
  console.log('Failure: Log empty or invalid');
30
26
  console.log(JSON.stringify(log, null, 2));
31
27
  }
28
+ const testActs = acts.filter(act => act.type && act.type === 'test');
32
29
  if (
33
- acts.length === script.commands.length
34
- && acts.every(
35
- act => act.type && act.type === 'test'
36
- ? act.result && act.result.failureCount !== undefined
37
- : true
38
- )
30
+ testActs.length === script.commands.filter(cmd => cmd.type === 'test').length
31
+ && testActs.every(testAct => testAct.result && testAct.result.failureCount !== undefined)
39
32
  ) {
40
- totals.attempts++;
41
- totals.successes++;
42
33
  console.log('Success: Reports have been correctly populated');
43
- if (acts.every(
44
- act => act.type === 'test' ? act.result.failureCount === 0 : true
45
- )) {
46
- totals.attempts++;
47
- totals.successes++;
34
+ if (testActs.every(testAct => testAct.result.failureCount === 0)) {
48
35
  console.log('Success: No failures');
49
36
  }
50
37
  else {
51
- totals.attempts++;
52
- console.log('Failure: At least one test has at least one failure');
38
+ console.log('Failure: The test has at least one failure');
53
39
  console.log(JSON.stringify(acts, null, 2));
54
40
  }
55
41
  }
56
42
  else {
57
- totals.attempts++;
58
43
  console.log('Failure: Reports empty or invalid');
59
44
  console.log(JSON.stringify(acts, null, 2));
60
45
  }
61
46
  }
62
- console.log(`Grand totals: attempts ${totals.attempts}, successes ${totals.successes}`);
63
47
  return Promise.resolve('');
64
48
  };
65
49
  validateTests()
@@ -0,0 +1,28 @@
1
+ {
2
+ "what": "validation of nonTable test",
3
+ "strict": true,
4
+ "commands": [
5
+ {
6
+ "type": "launch",
7
+ "which": "chromium",
8
+ "what": "usual browser"
9
+ },
10
+ {
11
+ "type": "url",
12
+ "which": "__targets__/nonTable/index.html",
13
+ "what": "page with a real table and 3 pseudotables"
14
+ },
15
+ {
16
+ "type": "test",
17
+ "which": "nonTable",
18
+ "what": "tables used for layout",
19
+ "withItems": true,
20
+ "expect": [
21
+ ["total", "=", 3],
22
+ ["items.0", "i", "Unit"],
23
+ ["items.1", "i", "Sales"],
24
+ ["items.2", "i", "Chime"]
25
+ ]
26
+ }
27
+ ]
28
+ }
@@ -0,0 +1,89 @@
1
+ {
2
+ "what": "validation of elements test",
3
+ "strict": true,
4
+ "commands": [
5
+ {
6
+ "type": "launch",
7
+ "which": "chromium",
8
+ "what": "usual browser"
9
+ },
10
+ {
11
+ "type": "url",
12
+ "which": "__targets__/textNodes/index.html",
13
+ "what": "page with a shadow root"
14
+ },
15
+ {
16
+ "type": "test",
17
+ "which": "textNodes",
18
+ "what": "text nodes",
19
+ "detailLevel": 0,
20
+ "expect": [
21
+ ["nodeCount", "=", 35]
22
+ ]
23
+ },
24
+ {
25
+ "type": "test",
26
+ "which": "textNodes",
27
+ "what": "text nodes",
28
+ "detailLevel": 0,
29
+ "text": "alia",
30
+ "expect": [
31
+ ["nodeCount", "=", 2]
32
+ ]
33
+ },
34
+ {
35
+ "type": "test",
36
+ "which": "textNodes",
37
+ "what": "text nodes",
38
+ "detailLevel": 1,
39
+ "text": "alia",
40
+ "expect": [
41
+ ["nodeCount", "=", 2],
42
+ ["items.0.text", "=", "Io alia"]
43
+ ]
44
+ },
45
+ {
46
+ "type": "test",
47
+ "which": "textNodes",
48
+ "what": "text nodes",
49
+ "detailLevel": 2,
50
+ "text": "This is the",
51
+ "expect": [
52
+ ["nodeCount", "=", 2],
53
+ ["items.0.text", "=", "This is the first button"],
54
+ ["items.1.text", "=", "This is the second button"],
55
+ ["items.0.ancestors.0.tagName", "=", "BUTTON"],
56
+ ["items.1.ancestors.0.tagName", "=", "BUTTON"],
57
+ ["items.0.ancestors.0.attributes.0.name", "=", "id"],
58
+ ["items.0.ancestors.0.attributes.0.value", "=", "button0"],
59
+ ["items.1.ancestors.0.attributes.1.name", "=", "aria-labelledby"],
60
+ ["items.1.ancestors.0.attributes.1.value", "=", "button1Label"],
61
+ ["items.0.ancestors.0.refLabels"],
62
+ ["items.1.ancestors.0.refLabels.0", "=", "Click the second button."],
63
+ ["items.0.ancestors.0.labels.0", "=", "Click the first button."],
64
+ ["items.0.ancestors.0.labels.1", "=", "Did you click it?"],
65
+ ["items.1.ancestors.0.labels"],
66
+ ["items.0.ancestors.0.children"]
67
+ ]
68
+ },
69
+ {
70
+ "type": "test",
71
+ "which": "textNodes",
72
+ "what": "text nodes",
73
+ "detailLevel": 3,
74
+ "text": "tute alia",
75
+ "expect": [
76
+ ["nodeCount", "=", 1],
77
+ ["items.0.text", "=", "Io tute alia"],
78
+ ["items.0.ancestors.0.tagName", "=", "LI"],
79
+ ["items.0.ancestors.1.tagName", "=", "UL"],
80
+ ["items.0.ancestors.0.attributes.0.name", "=", "class"],
81
+ ["items.0.ancestors.0.attributes.0.value", "=", "last"],
82
+ ["items.0.ancestors.1.attributes.0.name", "=", "lang"],
83
+ ["items.0.ancestors.1.attributes.0.value", "=", "eo"],
84
+ ["items.0.ancestors.1.children.1.tagName", "=", "LI"],
85
+ ["items.0.ancestors.1.children.2.attributes.1.name", "=", "hidden"]
86
+ ]
87
+ }
88
+ ]
89
+ }
@@ -2,13 +2,13 @@
2
2
  <html lang="en-US">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
- <title>Page</title>
5
+ <title>Page for elements test validation</title>
6
6
  <meta name="description" content="tester">
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1">
8
8
  </head>
9
9
  <body>
10
10
  <main>
11
- <h1>Page</h1>
11
+ <h1>Page for elements test validation</h1>
12
12
  <h2>Main part</h2>
13
13
  <p class="first">This is a paragraph.</p>
14
14
  <p>This is a list:</p>
@@ -0,0 +1,65 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en-US">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Page with a real table and pseudotables</title>
6
+ <meta name="description" content="tester">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <style>
9
+ table {
10
+ border-collapse: collapse;
11
+ }
12
+ td, th {
13
+ padding: 0.3rem;
14
+ border: 0.2rem black solid;
15
+ }
16
+ </style>
17
+ </head>
18
+ <body>
19
+ <main>
20
+ <h1>Page with a real table and pseudotables</h1>
21
+ <h2>Real table</h2>
22
+ <table>
23
+ <caption>Personnel</caption>
24
+ <thead>
25
+ <tr><th>Name</th><th>Department</th></tr>
26
+ </thead>
27
+ <tbody>
28
+ <tr><th>Amy</th><td>Accessibility</td></tr>
29
+ <tr><th>Abe</th><td>Marketing</td></tr>
30
+ </tbody>
31
+ </table>
32
+ <h2>Pseudotables 0 and 1: personnel</h2>
33
+ <table>
34
+ <tr><td>Given name</td><td>Unit</td></tr>
35
+ <tr><td>Kim</td><td>Usability</td></tr>
36
+ <tr><td>Ken</td><td><table><tr><td>Advertising</td><td>Sales</td></tr></table></td></tr>
37
+ </table>
38
+ <h2>Pseudotable 2: ringtones</h2>
39
+ <table>
40
+ <tr>
41
+ <td>Chime</td>
42
+ <td>
43
+ <object
44
+ title="chime"
45
+ type="audio/mp4"
46
+ data="chime.m4a"
47
+ >
48
+ </object>
49
+ </td>
50
+ </tr>
51
+ <tr>
52
+ <td>Gong</td>
53
+ <td>
54
+ <object
55
+ title="gong"
56
+ type="audio/mp4"
57
+ data="gong.m4a"
58
+ >
59
+ </object>
60
+ </td>
61
+ </tr>
62
+ </table>
63
+ </main>
64
+ </body>
65
+ </html>
@@ -0,0 +1,40 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en-US">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Page for textNodes test validation</title>
6
+ <meta name="description" content="tester">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ </head>
9
+ <body>
10
+ <main>
11
+ <h1>Page for textNodes test validation</h1>
12
+ <h2>Main part</h2>
13
+ <p class="first">This is a paragraph.</p>
14
+ <p>This is a list:</p>
15
+ <ul lang="eo">
16
+ <li class="first">Io</li>
17
+ <li>Io alia</li>
18
+ <li class="last" hidden>Io tute alia</li>
19
+ </ul>
20
+ <h2>Auxiliary part</h2>
21
+ <p>This is a summary.</p>
22
+ <div id="host"></div>
23
+ <div id="guest">
24
+ <h3>Details</h3>
25
+ <p class="sole">Here are some details.</p>
26
+ </div>
27
+ <label for="button0">Click the first button.</label>
28
+ <button id="button0" type="button">This is the first button</button>
29
+ <label for="button0">Did you click it?</label>
30
+ <p id="button1Label">Click the second button.</p>
31
+ <button type="button" aria-labelledby="button1Label">This is the second button</button>
32
+ </main>
33
+ <script>
34
+ document
35
+ .getElementById('host')
36
+ .attachShadow({mode: 'open'})
37
+ .appendChild(document.getElementById('guest'));
38
+ </script>
39
+ </body>
40
+ </html>