testaro 21.0.0 → 23.0.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
@@ -144,14 +144,9 @@ Here is an example of a job:
144
144
  {
145
145
  type: 'launch',
146
146
  which: 'chromium',
147
+ url: 'https://www.w3c.org',
147
148
  what: 'Chromium browser'
148
149
  },
149
- {
150
- type: 'url',
151
- which: 'https://www.w3c.org',
152
- what: 'World Wide Web Consortium',
153
- id: 'w3c'
154
- },
155
150
  {
156
151
  type: 'test',
157
152
  which: 'alfa',
@@ -174,9 +169,8 @@ Here is an example of a job:
174
169
  }
175
170
  ```
176
171
 
177
- This job contains three _acts_, telling Testaro to:
178
- 1. open a page in the Chromium browser
179
- 1. navigate to a specified URL
172
+ This job contains two _acts_, telling Testaro to:
173
+ 1. open a page in the Chromium browser and navigate to a specified URL
180
174
  1. perform two of the tests of the `alfa` tool (the tests for rules `r25` and `r71`) on that URL
181
175
 
182
176
  Job properties:
@@ -306,7 +300,7 @@ Each act object has a `type` property and optionally has a `name` property (used
306
300
 
307
301
  #### Act sequence
308
302
 
309
- The first two acts in any job have the types `launch` and `url`, respectively, as shown in the example above. They launch a browser and then use it to visit a URL.
303
+ The first act in any job has the type `launch`, as shown in the example above. It launches a browser and then uses it to visit a URL.
310
304
 
311
305
  #### Act types
312
306
 
@@ -338,11 +332,9 @@ When the texts of multiple elements of the same type will contain the same `whic
338
332
 
339
333
  ##### Navigations
340
334
 
341
- An example of a **navigation** is the act of type `url` above.
335
+ An example of a **navigation** is the act of type `launch` above.
342
336
 
343
- Once you have included a `url` act in a job, you do not need to add more `url` acts unless you want the browser to visit a different URL or revisit the same URL.
344
-
345
- If any act alters the page, you can restore the page to its original state for the next act by inserting new `launch` and `url` acts (and, if necessary, additional page-specific acts) between them.
337
+ If any act alters the page, you can restore the page to its original state for the next act by inserting a new `launch` act (and, if necessary, additional page-specific acts) between them.
346
338
 
347
339
  Another navigation example is:
348
340
 
@@ -585,7 +577,11 @@ The `rules` property for `testaro` is an array whose first item is either `'y'`
585
577
 
586
578
  The `testaro` tool (like the `ibm` tool) has a `withItems` property. If you set it to `false`, the `standardResult` object of `testaro` will contain an `instances` property with summaries that identify issues and instance counts. If you set it to `true`, some of the instances will be itemized.
587
579
 
588
- Unlike any other tool, the `testaro` tools requires a `stopOnFail` property, which specifies whether a failure to conform to any rule (i.e. any value of `totals` other than `[0, 0, 0, 0]`) should terminate the execution of tests for the remaining rules.
580
+ Unlike any other tool, the `testaro` tool requires a `stopOnFail` property, which specifies whether a failure to conform to any rule (i.e. any value of `totals` other than `[0, 0, 0, 0]`) should terminate the execution of tests for the remaining rules.
581
+
582
+ Warnings in the `testaro/hover.js`, `testaro/motion.js`, and `procs/visChange.js` files advise you to avoid launching particular browser types for the performance of particular Testaro tests.
583
+
584
+ Several Testaro tests make use of the `init()` function in the `procs/testaro` module. That function samples elements if the population of elements to be tested is larger than 100. The purpose is to achieve reasonable performance. The sampling overweights elements near the beginning of a page, because of the tendency of that location to have important and atypical elements.
589
585
 
590
586
  You can add custom rules to the rules of any tool. Testaro provides a template, `data/template.js`, for the definition of a rule to be added. Once you have created a copy of the template with revisions, you can move the copy into the `testaro` directory and add an entry for your custom rule to the `evalRules` object in the `tests/testaro.js` file. Then your custom rule will act as a Testaro rule.
591
587
 
@@ -618,6 +614,26 @@ This act checks the result of the previous act to determine whether its `result.
618
614
 
619
615
  A `next` act can use a `next` property instead of a `jump` property. The value of the `next` property is an act name. It tells Testaro to continue performing acts starting with the act having that value as the value of its `name` property.
620
616
 
617
+ #### Browser types
618
+
619
+ After any act in a job, you can change the browser type by inserting a `launch` act. One reason for specifying a particular browser type is that particular tests have different results with different browser types. Another is that you may wish to perform tests with more than a single browser type.
620
+
621
+ The warning comments in the `testaro/hover.js` and `testaro/motion.js` files state that those tests operate correctly only with the `webkit` browser type.
622
+
623
+ When you want to run some tests of a tool with one browser type and other tests of the same tool with another browser type, you can do so by splitting the rules into two test acts. For example, one test act can specify the rules as
624
+
625
+ ```javascript
626
+ ['y', 'r15', 'r54']
627
+ ```
628
+
629
+ and the other test act can specify the rules as
630
+
631
+ ```javascript
632
+ ['n', 'r15', 'r54']
633
+ ```
634
+
635
+ Before each test act, you can ensure that the latest `launch` act has specified the browser type to be used in that test act.
636
+
621
637
  #### `actSpecs` file
622
638
 
623
639
  ##### Introduction
package/actSpecs.js CHANGED
@@ -27,9 +27,10 @@ exports.actSpecs = {
27
27
  launch: [
28
28
  'Launch a Playwright browser',
29
29
  {
30
- which: [true, 'string', 'isBrowserType', 'chromium”, firefox”, or webkit'],
31
- what: [false, 'string', 'hasLength', 'comment'],
32
- lowMotion: [false, 'boolean', '', 'set reduced-motion option if true']
30
+ which: [true, 'string', 'isBrowserType', 'chromium, firefox, or webkit'],
31
+ url: [true, 'string', 'isURL', 'initial URL to navigate to'],
32
+ lowMotion: [false, 'boolean', '', 'set reduced-motion option if true'],
33
+ what: [false, 'string', 'hasLength', 'comment']
33
34
  }
34
35
  ],
35
36
  link: [
package/data/template.js CHANGED
@@ -13,12 +13,12 @@ const {init, report} = require('../procs/testaro');
13
13
  // Runs the test and returns the result.
14
14
  exports.reporter = async (page, withItems) => {
15
15
  // Initialize the locators and result.
16
- const all = await init(page, 'body *');
16
+ const all = await init(page, 'body a');
17
17
  // For each locator:
18
18
  for (const loc of all.allLocs) {
19
19
  // Get whether its element violates the rule.
20
20
  const isBad = await loc.evaluate(el => {
21
- const isViolator = el.tagName.length > 300;
21
+ const isViolator = ! el.href;
22
22
  return isViolator;
23
23
  });
24
24
  // If it does:
package/dirWatch.js CHANGED
@@ -34,11 +34,11 @@ const reWatch = () => {
34
34
  }
35
35
  if (code === 0) {
36
36
  console.log('Watcher exited successfully');
37
+ reWatch();
37
38
  }
38
39
  else {
39
40
  console.log(`Watcher exited with error code ${code}`);
40
41
  }
41
- reWatch();
42
42
  });
43
43
  };
44
44
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "21.0.0",
3
+ "version": "23.0.0",
4
4
  "description": "Run 920 web accessibility tests from 9 tools and get a standardized report",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/procs/nav.js ADDED
@@ -0,0 +1,220 @@
1
+ // nav
2
+
3
+ // ######## IMPORTS
4
+
5
+ // Playwright package.
6
+ const playwright = require('playwright');
7
+
8
+ // ######## CONSTANTS
9
+
10
+ // Strings in log messages indicating errors.
11
+ const errorWords = [
12
+ 'but not used',
13
+ 'content security policy',
14
+ 'deprecated',
15
+ 'error',
16
+ 'exception',
17
+ 'expected',
18
+ 'failed',
19
+ 'invalid',
20
+ 'missing',
21
+ 'non-standard',
22
+ 'not supported',
23
+ 'refused',
24
+ 'requires',
25
+ 'sorry',
26
+ 'suspicious',
27
+ 'unrecognized',
28
+ 'violates',
29
+ 'warning'
30
+ ];
31
+
32
+ // ######## VARIABLES
33
+
34
+ let browser;
35
+
36
+ // ######## FUNCTIONS
37
+
38
+ // Returns a string with any final slash removed.
39
+ const deSlash = string => string.endsWith('/') ? string.slice(0, -1) : string;
40
+ // Visits a URL and returns the response of the server.
41
+ const goTo = async (report, page, url, timeout, waitUntil) => {
42
+ if (url.startsWith('file://')) {
43
+ url = url.replace('file://', `file://${__dirname}/`);
44
+ }
45
+ // Visit the URL.
46
+ const startTime = Date.now();
47
+ try {
48
+ const response = await page.goto(url, {
49
+ timeout,
50
+ waitUntil
51
+ });
52
+ report.jobData.visitLatency += Math.round((Date.now() - startTime) / 1000);
53
+ const httpStatus = response.status();
54
+ // If the response status was normal:
55
+ if ([200, 304].includes(httpStatus) || url.startsWith('file:')) {
56
+ // If the browser was redirected in violation of a strictness requirement:
57
+ const actualURL = page.url();
58
+ if (report.strict && deSlash(actualURL) !== deSlash(url)) {
59
+ // Return an error.
60
+ console.log(`ERROR: Visit to ${url} redirected to ${actualURL}`);
61
+ return {
62
+ exception: 'badRedirection'
63
+ };
64
+ }
65
+ // Otherwise, i.e. if no prohibited redirection occurred:
66
+ else {
67
+ // Press the Escape key to dismiss any modal dialog.
68
+ await page.keyboard.press('Escape');
69
+ // Return the response.
70
+ return response;
71
+ }
72
+ }
73
+ // Otherwise, i.e. if the response status was abnormal:
74
+ else {
75
+ // Return an error.
76
+ console.log(`ERROR: Visit to ${url} got status ${httpStatus}`);
77
+ report.jobData.visitRejectionCount++;
78
+ return {
79
+ error: 'badStatus'
80
+ };
81
+ }
82
+ }
83
+ catch(error) {
84
+ console.log(`ERROR visiting ${url} (${error.message.slice(0, 200)})`);
85
+ return {
86
+ error: 'noVisit'
87
+ };
88
+ }
89
+ };
90
+ // Closes the current browser.
91
+ const browserClose = async () => {
92
+ if (browser) {
93
+ const browserType = browser.browserType().name();
94
+ let contexts = browser.contexts();
95
+ for (const context of contexts) {
96
+ await context.close();
97
+ contexts = browser.contexts();
98
+ }
99
+ await browser.close();
100
+ browser = null;
101
+ console.log(`${browserType} browser closed`);
102
+ }
103
+ };
104
+ // Launches a browser, navigates to a URL, and returns the status.
105
+ const launch = async (report, typeName, url, debug, waits, isLowMotion = false) => {
106
+ // If the specified browser type exists:
107
+ const browserType = playwright[typeName];
108
+ if (browserType) {
109
+ // Close the current browser, if any.
110
+ await browserClose();
111
+ // Launch a browser of the specified type.
112
+ const browserOptions = {
113
+ logger: {
114
+ isEnabled: () => false,
115
+ log: (name, severity, message) => console.log(message.slice(0, 100))
116
+ }
117
+ };
118
+ if (debug) {
119
+ browserOptions.headless = false;
120
+ }
121
+ if (waits) {
122
+ browserOptions.slowMo = waits;
123
+ }
124
+ browser = await browserType.launch(browserOptions)
125
+ // If the launch failed:
126
+ .catch(async error => {
127
+ healthy = false;
128
+ console.log(`ERROR launching browser (${errorStart(error)})`);
129
+ // Return this.
130
+ return false;
131
+ });
132
+ // Open a context (i.e. browser tab), with reduced motion if specified.
133
+ const options = {reduceMotion: isLowMotion ? 'reduce' : 'no-preference'};
134
+ browserContext = await browser.newContext(options);
135
+ // When a page (i.e. browser tab) is added to the browser context (i.e. browser window):
136
+ browserContext.on('page', async page => {
137
+ // Make the page current.
138
+ currentPage = page;
139
+ // If it emits a message:
140
+ page.on('console', msg => {
141
+ const msgText = msg.text();
142
+ let indentedMsg = '';
143
+ // If debugging is on:
144
+ if (debug) {
145
+ // Log a summary of the message on the console.
146
+ const parts = [msgText.slice(0, 75)];
147
+ if (msgText.length > 75) {
148
+ parts.push(msgText.slice(75, 150));
149
+ if (msgText.length > 150) {
150
+ const tail = msgText.slice(150).slice(-150);
151
+ if (msgText.length > 300) {
152
+ parts.push('...');
153
+ }
154
+ parts.push(tail.slice(0, 75));
155
+ if (tail.length > 75) {
156
+ parts.push(tail.slice(75));
157
+ }
158
+ }
159
+ }
160
+ indentedMsg = parts.map(part => ` | ${part}`).join('\n');
161
+ console.log(`\n${indentedMsg}`);
162
+ }
163
+ // Add statistics on the message to the report.
164
+ const msgTextLC = msgText.toLowerCase();
165
+ const msgLength = msgText.length;
166
+ report.jobData.logCount++;
167
+ report.jobData.logSize += msgLength;
168
+ if (errorWords.some(word => msgTextLC.includes(word))) {
169
+ report.jobData.errorLogCount++;
170
+ report.jobData.errorLogSize += msgLength;
171
+ }
172
+ const msgLC = msgText.toLowerCase();
173
+ if (
174
+ msgText.includes('403') && (msgLC.includes('status')
175
+ || msgLC.includes('prohibited'))
176
+ ) {
177
+ report.jobData.prohibitedCount++;
178
+ }
179
+ });
180
+ });
181
+ // Open the first page of the context.
182
+ currentPage = await browserContext.newPage();
183
+ try {
184
+ // Wait until it is stable.
185
+ await currentPage.waitForLoadState('domcontentloaded', {timeout: 5000});
186
+ // Navigate to the specified URL.
187
+ const navResult = await goTo(report, currentPage, url, 15000, 'domcontentloaded');
188
+ // If the navigation failed:
189
+ if (navResult.error) {
190
+ // Return this.
191
+ return false;
192
+ }
193
+ // Otherwise, i.e. if it succeeded:
194
+ else if (! navResult.error) {
195
+ // Update the name of the current browser type and store it in the page.
196
+ currentPage.browserTypeName = typeName;
197
+ // Return the browser context and the page.
198
+ return {
199
+ browserContext,
200
+ currentPage
201
+ };
202
+ }
203
+ }
204
+ // If it fails to become stable by the deadline:
205
+ catch(error) {
206
+ // Return this.
207
+ console.log(`ERROR: Blank page load in new tab timed out (${error.message})`);
208
+ return false;
209
+ }
210
+ }
211
+ // Otherwise, i.e. if it does not exist:
212
+ else {
213
+ // Return this.
214
+ console.log(`ERROR: Browser of type ${typeName} could not be launched`);
215
+ return false;
216
+ }
217
+ };
218
+ exports.browserClose = browserClose;
219
+ exports.goTo = goTo;
220
+ exports.launch = launch;
package/procs/sample.js CHANGED
@@ -10,7 +10,7 @@ exports.getSample = (population, sampleSize) => {
10
10
  const popSize = population.length;
11
11
  // If the sample is smaller than the population:
12
12
  if (sampleSize < popSize) {
13
- // Assign to each trigger a priority randomly decreasing with its index.
13
+ // Assign to each individual a priority randomly decreasing with its index.
14
14
  const WeightedPopulation = population.map((item, index) => {
15
15
  const weight = 1 + Math.sin(Math.PI * index / popSize + Math.PI / 2);
16
16
  const priority = weight * Math.random();
@@ -20,7 +20,7 @@ exports.getSample = (population, sampleSize) => {
20
20
  const sortedPopulation = WeightedPopulation.sort((a, b) => b[1] - a[1]);
21
21
  const sample = sortedPopulation.slice(0, sampleSize);
22
22
  const domOrderSample = sample.sort((a, b) => a[0] - b[0]);
23
- return domOrderSample.map(trigger => trigger[0]);
23
+ return domOrderSample.map(individual => individual[0]);
24
24
  }
25
25
  // Otherwise, i.e. if the sample is at least as large as the population:
26
26
  else {
@@ -400,6 +400,20 @@ const convert = (toolName, result, standardResult) => {
400
400
  console.log(`ERROR: Testaro rule ${rule} result has no standardInstances property`);
401
401
  }
402
402
  });
403
+ const preventionCount = result.preventions && result.preventions.length;
404
+ if (preventionCount) {
405
+ standardResult.totals[3] += preventionCount;
406
+ standardResult.instances.push({
407
+ ruleID: 'testPrevention',
408
+ what: 'Page prevented tests from being performed',
409
+ ordinalSeverity: 3,
410
+ count: preventionCount,
411
+ tagName: '',
412
+ id: '',
413
+ location: '',
414
+ excerpt: ''
415
+ });
416
+ }
403
417
  standardResult.totals = standardResult.totals.map(total => Math.round(total));
404
418
  }
405
419
  // wave
package/procs/testaro.js CHANGED
@@ -2,18 +2,32 @@
2
2
 
3
3
  // ########## IMPORTS
4
4
 
5
+ // Module to sample a population.
6
+ const {getSample} = require('../procs/sample');
5
7
  // Module to get locator data.
6
8
  const {getLocatorData} = require('../procs/getLocatorData');
7
9
 
10
+ // ########## CONSTANTS
11
+
12
+ const sampleMax = 100;
13
+
8
14
  // ########## FUNCTIONS
9
15
 
10
16
  // Initializes locators and a result.
11
17
  exports.init = async (page, locAllSelector, options = {}) => {
12
18
  // Get locators for the specified elements.
13
- const locAll = page.locator(locAllSelector, options);
14
- const allLocs = await locAll.all();
19
+ const locPop = page.locator(locAllSelector, options);
20
+ const locPops = await locPop.all();
21
+ const populationSize = locPops.length;
22
+ const sampleSize = Math.min(sampleMax, populationSize);
23
+ const locIndexes = getSample(locPops, sampleSize);
24
+ const allLocs = locIndexes.map(index => locPops[index]);
15
25
  const result = {
16
- data: {},
26
+ data: {
27
+ populationSize,
28
+ sampleSize,
29
+ populationRatio: sampleSize ? populationSize / sampleSize : null
30
+ },
17
31
  totals: [0, 0, 0, 0],
18
32
  standardInstances: []
19
33
  };
@@ -28,7 +42,7 @@ exports.init = async (page, locAllSelector, options = {}) => {
28
42
  // Populates a result.
29
43
  exports.report = async (withItems, all, ruleID, whats, ordinalSeverity, tagName = '') => {
30
44
  const {locs, result} = all;
31
- const {totals, standardInstances} = result;
45
+ const {data, totals, standardInstances} = result;
32
46
  // For each instance locator:
33
47
  for (const locItem of locs) {
34
48
  // Get data on its element.
@@ -42,7 +56,7 @@ exports.report = async (withItems, all, ruleID, whats, ordinalSeverity, tagName
42
56
  }
43
57
  const elData = await getLocatorData(loc);
44
58
  // Add to the totals.
45
- totals[ordinalSeverity]++;
59
+ totals[ordinalSeverity] += data.populationRatio;
46
60
  // If itemization is required:
47
61
  if (withItems) {
48
62
  // Add a standard instance to the result.
@@ -64,7 +78,7 @@ exports.report = async (withItems, all, ruleID, whats, ordinalSeverity, tagName
64
78
  ruleID,
65
79
  what: whats[1],
66
80
  ordinalSeverity,
67
- count: totals[ordinalSeverity],
81
+ count: Math.round(totals[ordinalSeverity]),
68
82
  tagName,
69
83
  id: '',
70
84
  location: {
@@ -2,6 +2,7 @@
2
2
  visChange
3
3
  This procedure reports a change in the visible content of a page between two times, optionally
4
4
  hovering over a locator-defined element immediately after the first time.
5
+
5
6
  WARNING: This test uses the Playwright page.screenshot method, which produces incorrect results
6
7
  when the browser type is chromium and is not implemented for the firefox browser type. The only
7
8
  browser type usable with this test is webkit.
@@ -23,7 +24,6 @@ const shoot = async (page, exclusion = null) => {
23
24
  timeout: 2000
24
25
  };
25
26
  if (exclusion) {
26
- const exclusionText = await exclusion.textContent();
27
27
  options.mask = [exclusion];
28
28
  }
29
29
  return await page.screenshot(options)