testaro 5.13.1 → 5.14.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/README.md CHANGED
@@ -442,11 +442,16 @@ For example, a `test` command might have this `expect` property:
442
442
 
443
443
  That would state the expectation that the `result` property of the `acts` item for that test in the report will have a `total.links` property with the value 5, a `total.links.underlined` property with a value less than 6, **no** `total.links.outlined` property, and a `docLang` property with a value different from `es-ES`.
444
444
 
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
+
445
447
  The second item in each array, if there are 3 items in the array, is an operator, drawn from:
446
448
  - `<`: less than
447
449
  - `=`: equal to
448
450
  - `>`: greater than
449
451
  - `!`: unequal to
452
+ - `i`: includes
453
+
454
+ The third item in each array, if there are 3 items in the array, is the criterion with which the value of the first property is compared.
450
455
 
451
456
  A typical use for an `expect` property is checking the correctness of a Testaro test. Thus, the validation scripts in the `validation/tests/scripts` directory all contain `test` commands with `expect` properties. See the “Validation” section below.
452
457
 
@@ -593,9 +598,9 @@ In order to make network watching possible, you must define these environment va
593
598
 
594
599
  As mentioned above, using the high-level method to run Testaro jobs requires `SCRIPTDIR`, `BATCHDIR`, and `REPORTDIR` environment variables.
595
600
 
596
- If a `tenon` test is included in the script, environment variables named `TENON_USER` and `TENON_PASSWORD` must exist, with your Tenon username and password, respectively, as their values.
601
+ If a `tenon` test is included in the script, environment variables named `TENON_USER` and `TENON_PASSWORD` must exist, with your Tenon username and password, respectively, as their values. You can obtain those from [Tenon](https://tenon.io/documentation/overview).
597
602
 
598
- If a `wave` test is included in the script, an environment variable named `WAVE_KEY` must exist, with your WAVE API key as its value.
603
+ If a `wave` test is included in the script, an environment variable named `WAVE_KEY` must exist, with your WAVE API key as its value. You can get it from [WebAIM](https://wave.webaim.org/api/).
599
604
 
600
605
  The `text` command can interpolate the value of an environment variable into text that it enters on a page, as documented in the `commands.js` file.
601
606
 
package/high.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  high.js
3
3
  Invokes Testaro with the high-level method.
4
- Usage example: node high tp12 weborgs
4
+ Usage example: node high tp15 weborgs
5
5
  */
6
6
 
7
7
  const {runJob} = require('./create');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "5.13.1",
3
+ "version": "5.14.0",
4
4
  "description": "Automation of accessibility testing",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/run.js CHANGED
@@ -304,24 +304,30 @@ const launch = async typeName => {
304
304
  browserContext.on('page', async page => {
305
305
  // Make the page current.
306
306
  currentPage = page;
307
- // Make abbreviations of its console messages get reported in the Playwright console.
307
+ // Make its console messages get reported in the Playwright console.
308
308
  page.on('console', msg => {
309
309
  const msgText = msg.text();
310
- const parts = [msgText.slice(0, 75)];
311
- if (msgText.length > 75) {
312
- parts.push(msgText.slice(75, 150));
313
- if (msgText.length > 150) {
314
- const tail = msgText.slice(150).slice(-150);
315
- if (msgText.length > 300) {
316
- parts.push('...');
317
- }
318
- parts.push(tail.slice(0, 75));
319
- if (tail.length > 75) {
320
- parts.push(tail.slice(75));
310
+ let indentedMsg = '';
311
+ if (debug) {
312
+ indentedMsg = ` | ${msg.text()}`;
313
+ }
314
+ else {
315
+ const parts = [msgText.slice(0, 75)];
316
+ if (msgText.length > 75) {
317
+ parts.push(msgText.slice(75, 150));
318
+ if (msgText.length > 150) {
319
+ const tail = msgText.slice(150).slice(-150);
320
+ if (msgText.length > 300) {
321
+ parts.push('...');
322
+ }
323
+ parts.push(tail.slice(0, 75));
324
+ if (tail.length > 75) {
325
+ parts.push(tail.slice(75));
326
+ }
321
327
  }
322
328
  }
329
+ indentedMsg = parts.map(part => ` | ${part}`).join('\n');
323
330
  }
324
- const indentedMsg = parts.map(part => ` | ${part}`).join('\n');
325
331
  console.log(`\n${indentedMsg}`);
326
332
  const msgTextLC = msgText.toLowerCase();
327
333
  const msgLength = msgText.length;
@@ -562,23 +568,34 @@ const isTrue = (object, specs) => {
562
568
  propertyTree.shift();
563
569
  actual = actual[propertyTree[0]];
564
570
  }
565
- // Determine whether the expectation was fulfilled.
566
- if (relation === '=') {
567
- satisfied = actual === criterion;
568
- }
569
- else if (relation === '<') {
570
- satisfied = actual < criterion;
571
- }
572
- else if (relation === '>') {
573
- satisfied = actual > criterion;
574
- }
575
- else if (relation === '!') {
576
- satisfied = actual !== criterion;
571
+ if (actual === undefined) {
572
+ return [null, false];
577
573
  }
578
- else if (! relation) {
579
- satisfied = actual === undefined;
574
+ else {
575
+ // Determine whether the expectation was fulfilled.
576
+ if (relation === '=') {
577
+ satisfied = actual === criterion;
578
+ }
579
+ else if (relation === '<') {
580
+ satisfied = actual < criterion;
581
+ }
582
+ else if (relation === '>') {
583
+ satisfied = actual > criterion;
584
+ }
585
+ else if (relation === '!') {
586
+ satisfied = actual !== criterion;
587
+ }
588
+ else if (relation === 'i') {
589
+ satisfied = actual.includes(criterion);
590
+ }
591
+ else if (relation === '!i') {
592
+ satisfied = ! actual.includes(criterion);
593
+ }
594
+ else if (! relation) {
595
+ satisfied = actual === undefined;
596
+ }
597
+ return [actual, satisfied];
580
598
  }
581
- return [actual, satisfied];
582
599
  };
583
600
  // Adds a wait error result to an act.
584
601
  const waitError = (page, act, error, what) => {
@@ -682,7 +699,7 @@ const doActs = async (report, actIndex, page) => {
682
699
  const result = act.result = {};
683
700
  // If the text is to be the URL:
684
701
  if (what === 'url') {
685
- // Wait for it and quit on failure.
702
+ // Wait for the URL to be the exact text and quit on failure.
686
703
  try {
687
704
  await page.waitForURL(which, {timeout: 15000});
688
705
  result.found = true;
@@ -695,7 +712,7 @@ const doActs = async (report, actIndex, page) => {
695
712
  }
696
713
  // Otherwise, if the text is to be a substring of the page title:
697
714
  else if (what === 'title') {
698
- // Wait for it and quit on failure.
715
+ // Wait for the page title to include the text, case-insensitively, and quit on failure.
699
716
  try {
700
717
  await page.waitForFunction(
701
718
  text => document
@@ -717,7 +734,7 @@ const doActs = async (report, actIndex, page) => {
717
734
  }
718
735
  // Otherwise, if the text is to be a substring of the text of the page body:
719
736
  else if (what === 'body') {
720
- // Wait for it and quit on failure.
737
+ // Wait for the body to include the text, case-insensitively, and quit on failure.
721
738
  try {
722
739
  await page.waitForFunction(
723
740
  text => document
@@ -949,13 +966,12 @@ const doActs = async (report, actIndex, page) => {
949
966
  // Otherwise, if the act is a move:
950
967
  else if (moves[act.type]) {
951
968
  const selector = typeof moves[act.type] === 'string' ? moves[act.type] : act.what;
952
- // Try to identify the element to perform the move on.
969
+ // Try up to 5 times to:
953
970
  act.result = {found: false};
954
971
  let selection = {};
955
972
  let tries = 0;
956
973
  const slimText = act.which ? debloat(act.which) : '';
957
974
  while (tries++ < 5 && ! act.result.found) {
958
- // If the page still exists:
959
975
  if (page) {
960
976
  // Identify the elements of the specified type.
961
977
  const selections = await page.$$(selector);
@@ -967,16 +983,17 @@ const doActs = async (report, actIndex, page) => {
967
983
  let matchCount = 0;
968
984
  const selectionTexts = [];
969
985
  for (selection of selections) {
970
- // Add its text or an empty string to the list of texts of such elements.
986
+ // Add its lower-case text or an empty string to the list of element texts.
971
987
  const selectionText = slimText ? await textOf(page, selection) : '';
972
988
  selectionTexts.push(selectionText);
973
- // If its text includes any specified text:
989
+ // If its text includes any specified text, case-insensitively:
974
990
  if (selectionText.includes(slimText)) {
975
991
  // If the element has the specified index among such elements:
976
992
  if (matchCount++ === (act.index || 0)) {
977
993
  // Report it as the matching element and stop checking.
978
994
  act.result.found = true;
979
- act.result.text = slimText;
995
+ act.result.textSpec = slimText;
996
+ act.result.textContent = selectionText;
980
997
  break;
981
998
  }
982
999
  }
@@ -0,0 +1,48 @@
1
+ /*
2
+ allHidden
3
+ This test reports a page that is entirely or mainly hidden.
4
+ */
5
+ exports.reporter = async page => {
6
+ const data = await page.evaluate(() => {
7
+ const {body} = document;
8
+ const main = body ? body.querySelector('main') : null;
9
+ const styles = {
10
+ doc: window.getComputedStyle(document.documentElement),
11
+ body: body ? window.getComputedStyle(body) : null,
12
+ main: main ? window.getComputedStyle(main) : null
13
+ };
14
+ const data = {
15
+ hidden: {
16
+ document: document.documentElement.hidden,
17
+ body: body && body.hidden,
18
+ main: main && main.hidden
19
+ },
20
+ reallyHidden: {
21
+ document: false,
22
+ body: false,
23
+ main: false
24
+ },
25
+ display: {
26
+ document: styles.doc && styles.doc.display,
27
+ body: styles.body && styles.body.display,
28
+ main: styles.main && styles.main.display
29
+ },
30
+ visHidden: {
31
+ document: styles.doc && styles.doc.visibility === 'hidden',
32
+ body: styles.body && styles.body.visibility === 'hidden',
33
+ main: styles.main && styles.main.visibility === 'hidden'
34
+ },
35
+ ariaHidden: {
36
+ document: document.documentElement.ariaHidden === 'true',
37
+ body: body && body.ariaHidden === 'true',
38
+ main: main && main.ariaHidden === 'true'
39
+ }
40
+ };
41
+ ['document', 'body', 'main'].forEach(element => {
42
+ if (data.hidden[element] && ! ['block', 'flex'].includes(data.display[element])) {
43
+ data.reallyHidden[element] = true;
44
+ }
45
+ });
46
+ });
47
+ return {result: data};
48
+ };
package/tests/elements.js CHANGED
@@ -8,8 +8,8 @@
8
8
  3. Data on each specified element also include data on its sibling nodes.
9
9
  */
10
10
  exports.reporter = async (page, detailLevel, tagName, onlyVisible, attribute) => {
11
- // Determine a selector of the specified elements.
12
- let selector = tagName || '*';
11
+ // Determine a selector of the specified elements, including any descendants of open shadow roots.
12
+ let selector = `body ${tagName ? tagName.toLowerCase() : '*'}`;
13
13
  if (attribute) {
14
14
  selector += `[${attribute}]`;
15
15
  }
@@ -19,7 +19,8 @@ exports.reporter = async (page, detailLevel, tagName, onlyVisible, attribute) =>
19
19
  let data = {};
20
20
  // Get the data on the elements.
21
21
  try {
22
- data = await page.$$eval(selector, (elements, detailLevel) => {
22
+ const locator = page.locator(selector);
23
+ data = await locator.evaluateAll((elements, detailLevel) => {
23
24
  // FUNCTION DEFINITIONS START
24
25
  // Compacts a string.
25
26
  const compact = string => string.replace(/\s+/g, ' ').trim();
@@ -28,10 +29,11 @@ exports.reporter = async (page, detailLevel, tagName, onlyVisible, attribute) =>
28
29
  const sibInfo = {
29
30
  type: nodeType
30
31
  };
32
+ // If the sibling node is an element:
31
33
  if (nodeType === 1) {
32
34
  sibInfo.tagName = node.tagName;
33
35
  sibInfo.attributes = [];
34
- node.attributes.forEach(attribute => {
36
+ Array.from(node.attributes).forEach(attribute => {
35
37
  sibInfo.attributes.push({
36
38
  name: attribute.name,
37
39
  value: attribute.value
@@ -106,12 +108,12 @@ exports.reporter = async (page, detailLevel, tagName, onlyVisible, attribute) =>
106
108
  }
107
109
  // If the parental text content is required:
108
110
  if (detailLevel > 1) {
109
- // Add it to the element data.
110
- datum.parentTextContent = parent ? parent.textContent : '';
111
+ // Add it (excluding any shadow-root descendants) to the element data.
112
+ datum.parentTextContent = parent ? compact(parent.textContent) : '';
111
113
  }
112
114
  // If sibling itemization is required:
113
115
  if (detailLevel === 3) {
114
- // Add the sibling data to the element data.
116
+ // Add data on the siblings, excluding any descendants of shadow roots, to the data.
115
117
  datum.siblings = {
116
118
  before: [],
117
119
  after: []
@@ -1,15 +1,17 @@
1
- // app.js
2
- // Validator for Testaro tests.
1
+ // test.js
2
+ // Validator for one Testaro test.
3
+ // Execution example: node validation/executors/test focOp
3
4
 
4
5
  const fs = require('fs').promises;
5
6
  const {handleRequest} = require(`${__dirname}/../../run`);
7
+ const test = process.argv[2];
6
8
  const validateTests = async () => {
7
9
  const totals = {
8
10
  attempts: 0,
9
11
  successes: 0
10
12
  };
11
13
  const scriptFileNames = await fs.readdir(`${__dirname}/../tests/scripts`);
12
- for (const scriptFileName of scriptFileNames.filter(fileName => fileName === 'styleDiff.json')) {
14
+ for (const scriptFileName of scriptFileNames.filter(fileName => fileName === `${test}.json`)) {
13
15
  const rawScriptJSON = await fs
14
16
  .readFile(`${__dirname}/../tests/scripts/${scriptFileName}`, 'utf8');
15
17
  const scriptJSON = rawScriptJSON
@@ -1,4 +1,4 @@
1
- // app.js
1
+ // tests.js
2
2
  // Validator for Testaro tests.
3
3
 
4
4
  const fs = require('fs').promises;
@@ -1,4 +1,4 @@
1
- // watchDir.js
1
+ // watchNet.js
2
2
  // Validator for network watching.
3
3
 
4
4
  const fs = require('fs/promises');
@@ -0,0 +1,37 @@
1
+ {
2
+ "what": "validation of docType 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__/docType/good.html",
13
+ "what": "page with doctype"
14
+ },
15
+ {
16
+ "type": "test",
17
+ "which": "docType",
18
+ "what": "doctype",
19
+ "expect": [
20
+ ["docHasType", "=", true]
21
+ ]
22
+ },
23
+ {
24
+ "type": "url",
25
+ "which": "__targets__/docType/bad.html",
26
+ "what": "page without doctype"
27
+ },
28
+ {
29
+ "type": "test",
30
+ "which": "docType",
31
+ "what": "doctype",
32
+ "expect": [
33
+ ["docHasType", "=", false]
34
+ ]
35
+ }
36
+ ]
37
+ }
@@ -0,0 +1,131 @@
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__/elements/index.html",
13
+ "what": "page with a shadow root"
14
+ },
15
+ {
16
+ "type": "test",
17
+ "which": "elements",
18
+ "what": "elements",
19
+ "detailLevel": 0,
20
+ "expect": [
21
+ ["total", "=", 21]
22
+ ]
23
+ },
24
+ {
25
+ "type": "test",
26
+ "which": "elements",
27
+ "what": "elements",
28
+ "detailLevel": 0,
29
+ "tagName": "P",
30
+ "expect": [
31
+ ["total", "=", 5]
32
+ ]
33
+ },
34
+ {
35
+ "type": "test",
36
+ "which": "elements",
37
+ "what": "elements",
38
+ "detailLevel": 0,
39
+ "tagName": "LI",
40
+ "onlyVisible": true,
41
+ "expect": [
42
+ ["total", "=", 2]
43
+ ]
44
+ },
45
+ {
46
+ "type": "test",
47
+ "which": "elements",
48
+ "what": "elements",
49
+ "detailLevel": 0,
50
+ "tagName": "P",
51
+ "attribute": "class",
52
+ "expect": [
53
+ ["total", "=", 2]
54
+ ]
55
+ },
56
+ {
57
+ "type": "test",
58
+ "which": "elements",
59
+ "what": "elements",
60
+ "detailLevel": 1,
61
+ "tagName": "LI",
62
+ "attribute": "class=first",
63
+ "expect": [
64
+ ["total", "=", 1],
65
+ ["items.0.tagName", "=", "LI"],
66
+ ["items.0.parentTagName", "=", "UL"],
67
+ ["items.0.code", "i", "first"],
68
+ ["items.0.attributes.0.name", "=", "class"],
69
+ ["items.0.attributes.0.value", "=", "first"],
70
+ ["items.0.textContent", "=", "Io"]
71
+ ]
72
+ },
73
+ {
74
+ "type": "test",
75
+ "which": "elements",
76
+ "what": "elements",
77
+ "detailLevel": 1,
78
+ "tagName": "INPUT",
79
+ "expect": [
80
+ ["total", "=", 2],
81
+ ["items.0.labels.1", "i", "anything"],
82
+ ["items.1.labelers.0", "i", "something else"]
83
+ ]
84
+ },
85
+ {
86
+ "type": "test",
87
+ "which": "elements",
88
+ "what": "elements",
89
+ "detailLevel": 2,
90
+ "tagName": "LI",
91
+ "attribute": "class=first",
92
+ "expect": [
93
+ ["items.0.textContent", "=", "Io"],
94
+ ["items.0.parentTextContent", "i", "tute"]
95
+ ]
96
+ },
97
+ {
98
+ "type": "test",
99
+ "which": "elements",
100
+ "what": "elements",
101
+ "detailLevel": 2,
102
+ "tagName": "LI",
103
+ "onlyVisible": true,
104
+ "attribute": "class",
105
+ "expect": [
106
+ ["total", "=", 1],
107
+ ["items.0.parentTextContent", "i", "alia"],
108
+ ["items.0.parentTextContent", "i", "tute"]
109
+ ]
110
+ },
111
+ {
112
+ "type": "test",
113
+ "which": "elements",
114
+ "what": "elements",
115
+ "detailLevel": 3,
116
+ "tagName": "P",
117
+ "attribute": "class=first",
118
+ "expect": [
119
+ ["items.0.textContent", "=", "This is a paragraph."],
120
+ ["items.0.parentTagName", "=", "MAIN"],
121
+ ["items.0.parentTextContent", "i", "something."],
122
+ ["items.0.siblings.before.0.type", "=", 3],
123
+ ["items.0.siblings.before.1.type", "=", 1],
124
+ ["items.0.siblings.before.1.tagName", "=", "H1"],
125
+ ["items.0.siblings.after.12.type", "=", 1],
126
+ ["items.0.siblings.after.12.tagName", "=", "LABEL"],
127
+ ["items.0.siblings.after.12.attributes.0.name", "=", "for"]
128
+ ]
129
+ }
130
+ ]
131
+ }
@@ -0,0 +1,14 @@
1
+ <html lang="en-US">
2
+ <head>
3
+ <meta charset="utf-8">
4
+ <title>Page without doctype</title>
5
+ <meta name="description" content="tester">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ </head>
8
+ <body>
9
+ <main>
10
+ <h1>Page without doctype</h1>
11
+ <p>This page has no doctype.</p>
12
+ </main>
13
+ </body>
14
+ </html>
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en-US">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Page with doctype</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 with doctype</h1>
12
+ <p>This page has a doctype.</p>
13
+ </main>
14
+ </body>
15
+ </html>
@@ -0,0 +1,40 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en-US">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Page</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</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="input0">Input something.</label>
28
+ <input id="input0">
29
+ <label for="input0">Did you input anything?</label>
30
+ <p id="input1Label">Input something else.</p>
31
+ <input aria-labelledby="input1Label">
32
+ </main>
33
+ <script>
34
+ document
35
+ .getElementById('host')
36
+ .attachShadow({mode: 'open'})
37
+ .appendChild(document.getElementById('guest'));
38
+ </script>
39
+ </body>
40
+ </html>