testaro 60.7.0 → 60.7.2

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": "60.7.0",
3
+ "version": "60.7.2",
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/testaro.js CHANGED
@@ -90,19 +90,19 @@ const getRuleResult = exports.getRuleResult = async (
90
90
  // If itemization is required:
91
91
  if (withItems) {
92
92
  // Get the bounding box of the element.
93
- const {location} = elData;
93
+ const {tagName,id, location, excerpt} = elData;
94
94
  const box = location.type === 'box' ? location.spec : await boxOf(loc);
95
95
  // Add a standard instance to the result.
96
96
  standardInstances.push({
97
97
  ruleID,
98
98
  what: whatParam ? whats[0].replace('__param__', whatParam) : whats[0],
99
99
  ordinalSeverity,
100
- tagName: elData.tagName,
101
- id: elData.id,
102
- location: elData.location,
103
- excerpt: elData.excerpt,
100
+ tagName,
101
+ id,
102
+ location,
103
+ excerpt,
104
104
  boxID: boxToString(box),
105
- pathID: await xPath(loc)
105
+ pathID: tagName === 'HTML' ? '/html' : await xPath(loc)
106
106
  });
107
107
  }
108
108
  }
@@ -151,7 +151,9 @@ exports.simplify = async (page, withItems, ruleData) => {
151
151
  complaints.instance,
152
152
  complaints.summary
153
153
  ];
154
- const result = await getRuleResult(withItems, all, ruleID, whats, ordinalSeverity, summaryTagName);
154
+ const result = await getRuleResult(
155
+ withItems, all, ruleID, whats, ordinalSeverity, summaryTagName
156
+ );
155
157
  // Return the result.
156
158
  return result;
157
159
  };
@@ -0,0 +1,51 @@
1
+ /*
2
+ reproInitScripts.js
3
+ Minimal example to demonstrate an apparent Playwright bug reported as issue 38442
4
+ (https://github.com/microsoft/playwright/issues/38442).
5
+ */
6
+
7
+ // save as e.g. reproInitScripts.js
8
+ // run with: node reproInitScripts.js
9
+
10
+ const {chromium} = require('playwright');
11
+
12
+ (async () => {
13
+
14
+ const browser = await chromium.launch({headless: true});
15
+ const context = await browser.newContext();
16
+
17
+ context.on('page', async page => {
18
+ console.log('context.on("page") fired');
19
+
20
+ // First init script
21
+ await page.addInitScript(() => {
22
+ window.helperOne = () => 'one';
23
+ });
24
+
25
+ // Second init script
26
+ await page.addInitScript(() => {
27
+ window.helperTwo = () => 'two';
28
+ });
29
+
30
+ // Third init script
31
+ await page.addInitScript(() => {
32
+ window.helperThree = () => 'three';
33
+ });
34
+
35
+ });
36
+
37
+ const page = await context.newPage();
38
+
39
+ await page.goto('https://example.com', {waitUntil: 'domcontentloaded'});
40
+
41
+ const result = await page.evaluate(() => ({
42
+ helperOne: typeof window.helperOne,
43
+ helperTwo: typeof window.helperTwo,
44
+ helperThree: typeof window.helperThree
45
+ }));
46
+
47
+ console.log('Helper types:', result);
48
+
49
+ await browser.close();
50
+
51
+ })();
package/run.js CHANGED
@@ -372,8 +372,9 @@ const launch = exports.launch = async (
372
372
  browserContext.setDefaultTimeout(0);
373
373
  // When a page (i.e. tab) is added to the browser context (i.e. browser window):
374
374
  browserContext.on('page', async page => {
375
- // Mask automation detection
376
- await page.addInitScript(() => {
375
+ const isTestaroTest = act.type === 'test' && act.which === 'testaro';
376
+ // Mask automation detection.
377
+ await page.addInitScript(isTestaroTest => {
377
378
  Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
378
379
  window.chrome = {runtime: {}};
379
380
  Object.defineProperty(navigator, 'plugins', {
@@ -382,11 +383,63 @@ const launch = exports.launch = async (
382
383
  Object.defineProperty(navigator, 'languages', {
383
384
  get: () => ['en-US', 'en']
384
385
  });
385
- });
386
- // If the act is a testaro test act:
387
- if (act.type === 'test' && act.which === 'testaro') {
388
- // Add a script that defines a window method to get the XPath of an element.
389
- await page.addInitScript(() => {
386
+ // If the act is a testaro test act:
387
+ if (isTestaroTest) {
388
+ // Add a window method to return an instance.
389
+ window.getInstance = (
390
+ element, ruleID, what, count = 1, ordinalSeverity, summaryTagName = ''
391
+ ) => {
392
+ // If the element exists:
393
+ if (element) {
394
+ // Get its properties.
395
+ const boxData = element.getBoundingClientRect();
396
+ ['x', 'y', 'width', 'height'].forEach(dimension => {
397
+ boxData[dimension] = Math.round(boxData[dimension]);
398
+ });
399
+ const {x, y, width, height} = boxData;
400
+ const {tagName, id = ''} = element;
401
+ // Return an itemized instance.
402
+ return {
403
+ ruleID,
404
+ what,
405
+ count,
406
+ ordinalSeverity,
407
+ tagName,
408
+ id,
409
+ location: {
410
+ doc: 'dom',
411
+ type: 'box',
412
+ spec: {
413
+ x,
414
+ y,
415
+ width,
416
+ height
417
+ }
418
+ },
419
+ excerpt: element.textContent.trim().replace(/\s+/g, ' ').slice(0, 100),
420
+ boxID: [x, y, width, height].join(':'),
421
+ pathID: window.getXPath(element)
422
+ };
423
+ }
424
+ // Otherwise, i.e. if no element exists, return a summary instance.
425
+ return {
426
+ ruleID,
427
+ what,
428
+ count,
429
+ ordinalSeverity,
430
+ tagName: summaryTagName,
431
+ id: '',
432
+ location: {
433
+ doc: '',
434
+ type: '',
435
+ spec: ''
436
+ },
437
+ excerpt: '',
438
+ boxID: '',
439
+ pathID: ''
440
+ };
441
+ };
442
+ // Add a window method to get the XPath of an element.
390
443
  window.getXPath = element => {
391
444
  if (!element || element.nodeType !== Node.ELEMENT_NODE) {
392
445
  return '';
@@ -432,8 +485,8 @@ const launch = exports.launch = async (
432
485
  // Return the XPath.
433
486
  return `/${segments.join('/')}`;
434
487
  };
435
- });
436
- }
488
+ }
489
+ }, isTestaroTest);
437
490
  // Ensure the report has a jobData property.
438
491
  report.jobData ??= {};
439
492
  const {jobData} = report;
package/testaro/adbID.js CHANGED
@@ -25,43 +25,93 @@
25
25
 
26
26
  /*
27
27
  adbID
28
- Clean-room rule: report elements that reference aria-describedby targets that are missing or ambiguous (duplicate ids).
28
+ This test reports elements referencing aria-describedby targets that are missing
29
+ or, because of duplicate IDs, ambiguous. An earlier version of this test was
30
+ originally developed under a clean-room procedure to ensure its independence from
31
+ the implementation of a test for a similar rule in the Tenon tool.
29
32
  */
30
33
 
31
- const {init, getRuleResult} = require('../procs/testaro');
34
+ // FUNCTIONS
32
35
 
36
+ // Runs the test and returns the result.
33
37
  exports.reporter = async (page, withItems) => {
34
- // elements that reference aria-describedby
35
- const all = await init(200, page, '[aria-describedby]');
36
- for (const loc of all.allLocs) {
37
- const isBad = await loc.evaluate(el => {
38
- const raw = el.getAttribute('aria-describedby') || '';
39
- const ids = raw.trim().split(/\s+/).filter(Boolean);
40
- if (ids.length === 0) return false;
41
- // for each referenced id, check how many elements have that id
42
- for (const id of ids) {
43
- try {
44
- // exact match (case-sensitive)
45
- const exact = document.querySelectorAll('#' + CSS.escape(id));
46
- if (exact.length === 1) continue;
47
- // if not exactly one, try case-insensitive match by normalizing
48
- const allIds = Array.from(document.querySelectorAll('[id]')).map(e => e.getAttribute('id'));
49
- const ci = allIds.filter(i => i && i.toLowerCase() === id.toLowerCase()).length;
50
- if (ci === 1) continue;
51
- // otherwise it's missing or ambiguous
52
- return true;
53
- } catch (e) {
54
- return true;
38
+ // Get data on violations of the rule.
39
+ const violationData = await page.evaluate(withItems => {
40
+ // Get all candidates, i.e. elements with aria-describedby attributes.
41
+ const candidates = document.body.querySelectorAll('[aria-describedby]');
42
+ let violationCount = 0;
43
+ const instances = [];
44
+ // For each candidate:
45
+ candidates.forEach(element => {
46
+ // Get the IDs in its aria-describedby attribute.
47
+ const IDs = element.getAttribute('aria-describedby').trim().split(/\s+/).filter(Boolean);
48
+ // If there are none:
49
+ if (! IDs.length) {
50
+ // Increment the violation count.
51
+ violationCount++;
52
+ // If itemization is required:
53
+ if (withItems) {
54
+ const what = 'Element has an aria-describedby attribute with no value';
55
+ // Add an instance to the instances.
56
+ instances.push(window.getInstance(element, 'adbID', what, 1, 3));
57
+ }
58
+ }
59
+ // Otherwise, i.e. if there is at least 1 ID:
60
+ else {
61
+ // For each ID:
62
+ for (const id of IDs) {
63
+ // Get the element with that ID.
64
+ const describer = document.getElementById(id);
65
+ // If it doesn't exist:
66
+ if (! describer) {
67
+ // Increment the violation count.
68
+ violationCount++;
69
+ // If itemization is required:
70
+ if (withItems) {
71
+ const what = `No element has the aria-describedby ID ${id}`;
72
+ // Add an instance to the instances.
73
+ instances.push(window.getInstance(element, 'adbID', what, 1, 3));
74
+ }
75
+ // Stop checking the element.
76
+ break;
77
+ }
78
+ // Otherwise, i.e. if it exists:
79
+ else {
80
+ // Get the elements with that ID.
81
+ const sameIDElements = document.querySelectorAll(`#${id}`);
82
+ // If there is more than one:
83
+ if (sameIDElements.length > 1) {
84
+ // Increment the violation count.
85
+ violationCount++;
86
+ // If itemization is required:
87
+ if (withItems) {
88
+ const what = `Multiple elements share the aria-describedby ID ${id}`;
89
+ // Add an instance to the instances.
90
+ instances.push(window.getInstance(element, 'adbID', what, 1, 2));
91
+ }
92
+ // Stop checking the element.
93
+ break;
94
+ }
95
+ }
55
96
  }
56
97
  }
57
- return false;
58
98
  });
59
- if (isBad) all.locs.push(loc);
60
- }
61
-
62
- const whats = [
63
- 'Referenced description of the element is ambiguous or missing',
64
- 'Referenced descriptions of elements are ambiguous or missing'
65
- ];
66
- return await getRuleResult(withItems, all, 'adbID', whats, 3);
99
+ // If there were any violations and itemization is not required:
100
+ if (violationCount && ! withItems) {
101
+ const what = 'Elements have aria-describedby attributes with missing or invalid id values';
102
+ // Add a summary instance to the instances.
103
+ instances.push(window.getInstance(null, 'lineHeight', what, violationCount, 3));
104
+ }
105
+ return {
106
+ violationCount,
107
+ instances
108
+ };
109
+ }, withItems);
110
+ const {violationCount, instances} = violationData;
111
+ // Return the result.
112
+ return {
113
+ data: {},
114
+ totals: [0, violationCount, 0, 0],
115
+ standardInstances: instances
116
+ };
67
117
  };
@@ -36,67 +36,56 @@
36
36
 
37
37
  // Runs the test and returns the result.
38
38
  exports.reporter = async (page, withItems) => {
39
- const violators = await page.evaluate(() => {
40
- const elements = document.body.querySelectorAll('*');
41
- const elementsWithText = Array.from(elements).filter(el =>
39
+ // Get data on violations of the rule.
40
+ const violationData = await page.evaluate(withItems => {
41
+ // Get all elements.
42
+ const allElements = document.body.querySelectorAll('*');
43
+ // Get all violation candidates, i.e. elements that have non-empty child text nodes.
44
+ const candidates = Array.from(allElements).filter(el =>
42
45
  Array.from(el.childNodes).some(child =>
43
46
  child.nodeType === Node.TEXT_NODE &&
44
47
  child.textContent.trim().length
45
48
  )
46
49
  );
47
- const violatorData = [];
48
- elementsWithText.forEach(el => {
49
- const styleDec = window.getComputedStyle(el);
50
+ let violationCount = 0;
51
+ const instances = [];
52
+ // For each candidate:
53
+ candidates.forEach(element => {
54
+ // Get its relevant style properties.
55
+ const styleDec = window.getComputedStyle(element);
50
56
  const {fontSize, lineHeight} = styleDec;
51
57
  const fontSizeNum = Number.parseFloat(fontSize);
52
58
  const lineHeightNum = Number.parseFloat(lineHeight);
53
- const fontSizeTrunc = fontSizeNum.toFixed(1);
54
- const lineHeightTrunc = lineHeightNum.toFixed(1);
59
+ // If it violates the rule:
55
60
  if (lineHeightNum < 1.495 * fontSizeNum) {
56
- const boxData = el.getBoundingClientRect();
57
- ['x', 'y', 'width', 'height'].forEach(dimension => {
58
- boxData[dimension] = Math.round(boxData[dimension]);
59
- });
60
- const {x, y, width, height} = boxData;
61
- violatorData.push({
62
- tagName: el.tagName,
63
- id: el.id,
64
- location: {
65
- doc: 'dom',
66
- type: 'box',
67
- spec: {
68
- x,
69
- y,
70
- width,
71
- height
72
- }
73
- },
74
- excerpt: el.textContent.trim(),
75
- boxID: [x, y, width, height].join(':'),
76
- pathID: window.getXPath(el),
77
- fontSize: fontSizeTrunc,
78
- lineHeight: lineHeightTrunc
79
- });
61
+ // Increment the violation count.
62
+ violationCount++;
63
+ // If itemization is required:
64
+ if (withItems) {
65
+ const fontSizeRounded = fontSizeNum.toFixed(1);
66
+ const lineHeightRounded = lineHeightNum.toFixed(1);
67
+ const what = `Element line height (${lineHeightRounded}px) is less than 1.5 times its font size (${fontSizeRounded}px)`;
68
+ // Add an instance to the instances.
69
+ instances.push(window.getInstance(element, 'lineHeight', what, 1, 1));
70
+ }
80
71
  }
81
72
  });
82
- return violatorData;
83
- });
73
+ // If there were any violations and itemization is not required:
74
+ if (violationCount && ! withItems) {
75
+ const what = 'Element line heights are less than 1.5 times their font sizes';
76
+ // Add a summary instance to the instances.
77
+ instances.push(window.getInstance(null, 'lineHeight', what, violationCount, 1));
78
+ }
79
+ return {
80
+ violationCount,
81
+ instances
82
+ };
83
+ }, withItems);
84
+ const {violationCount, instances} = violationData;
85
+ // Return the result.
84
86
  return {
85
87
  data: {},
86
- totals: [0, violators.length, 0, 0],
87
- standardInstances: violators.map(violator => {
88
- const {tagName, id, location, excerpt, boxID, pathID, fontSize, lineHeight} = violator;
89
- return {
90
- ruleID: 'lineHeight',
91
- what: `Element line height (${lineHeight}px) is less than 1.5 times its font size (${fontSize}px)`,
92
- ordinalSeverity: 1,
93
- tagName,
94
- id,
95
- location,
96
- excerpt,
97
- boxID,
98
- pathID
99
- };
100
- })
88
+ totals: [0, violationCount, 0, 0],
89
+ standardInstances: instances
101
90
  };
102
91
  };
@@ -1,70 +0,0 @@
1
- /*
2
- © 2023–2024 CVS Health and/or one of its affiliates. All rights reserved.
3
- © 2025 Jonathan Robert Pool. All rights reserved.
4
-
5
- MIT License
6
-
7
- Permission is hereby granted, free of charge, to any person obtaining a copy
8
- of this software and associated documentation files (the "Software"), to deal
9
- in the Software without restriction, including without limitation the rights
10
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
- copies of the Software, and to permit persons to whom the Software is
12
- furnished to do so, subject to the following conditions:
13
-
14
- The above copyright notice and this permission notice shall be included in all
15
- copies or substantial portions of the Software.
16
-
17
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
- SOFTWARE.
24
- */
25
-
26
- /*
27
- lineHeight
28
- Related to Tenon rule 144.
29
- This test reports elements whose line heights are less than 1.5 times their font sizes. Even
30
- such elements with no text create accessibility risk, because any text node added to one of
31
- them would have a substandard line height. Nonetheless, elements with no non-spacing text in
32
- their subtrees are excluded.
33
- */
34
-
35
- // IMPORTS
36
-
37
- // Module to perform common operations.
38
- const {init, getRuleResult} = require('../procs/testaro');
39
-
40
- // FUNCTIONS
41
-
42
- // Runs the test and returns the result.
43
- exports.reporter = async (page, withItems) => {
44
- // Initialize the locators and result.
45
- const all = await init(100, page, 'body *', {hasText: /[^\s]/});
46
- // For each locator:
47
- for (const loc of all.allLocs) {
48
- // Get whether its element violates the rule.
49
- const data = await loc.evaluate(el => {
50
- const styleDec = window.getComputedStyle(el);
51
- const {fontSize, lineHeight} = styleDec;
52
- return {
53
- fontSize: Number.parseFloat(fontSize),
54
- lineHeight: Number.parseFloat(lineHeight)
55
- };
56
- });
57
- // If it does, after a grace margin for rounding:
58
- const isBad = data.lineHeight < 1.49 * data.fontSize;
59
- if (isBad) {
60
- // Add the locator to the array of violators.
61
- all.locs.push([loc, `font size ${data.fontSize} px, line height ${data.lineHeight} px`]);
62
- }
63
- }
64
- // Populate and return the result.
65
- const whats = [
66
- 'Element line height is less than 1.5 times its font size (__param__)',
67
- 'Elements have line heights less than 1.5 times their font sizes'
68
- ];
69
- return await getRuleResult(withItems, all, 'lineHeight', whats, 1);
70
- };