testaro 77.2.0 → 78.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
@@ -142,6 +142,7 @@ Here is a sample job, showing properties that you can set:
142
142
  strict: true, // Whether to reject redirections from the target URL
143
143
  standard: 'only', // Report native (no), standard (only), or both (also) results
144
144
  imageColor: 0, // Color type (0, 2, 4, 6) of the page image, if one is to be created along with a catalog
145
+ imageScale: 2, // Optional: also capture the page image at this device pixel density (see the images section)
145
146
  device: { // Device to emulate
146
147
  id: 'iPhone 8',
147
148
  windowOptions: {
@@ -375,7 +376,9 @@ In some cases no catalog entry can be found. The reasons may include:
375
376
 
376
377
  #### `images`
377
378
 
378
- Testaro inserts an `images` array property if necessary to store page images in the report. If the job has an `imageColor` property with `0`, `2`, `4`, or `6` as its value and Testaro will insert a `catalog` property, then Testaro also creates a page image with that color type and makes its base64-encoded PNG the first item in the `images` array.
379
+ Testaro inserts an `images` array property if necessary to store page images in the report. If the job has an `imageColor` property with `0`, `2`, `4`, or `6` as its value and Testaro will insert a `catalog` property, then Testaro also creates a page image with that color type and makes its base64-encoded PNG the first item in the `images` array. The first item is always captured at CSS-pixel scale (one image pixel per CSS pixel), so the `motion` test of the `testaro` tool can compare it with its own CSS-pixel screenshot.
380
+
381
+ If the job also has an `imageScale` property with a number greater than 1 as its value, then the catalog page is rendered at that device scale factor, and Testaro captures a second page image at device-pixel scale and makes it the second item in the `images` array. That image has `imageScale` times the pixels of the CSS layout in each dimension, for crisp display on high-resolution screens. The `boxID` properties of the catalog remain in CSS pixels; consumers can map them onto the second image by multiplying the coordinates by `imageScale`. Fractional values (such as a device's native `2.625`) are valid. A natural choice is the emulated device's own `deviceScaleFactor`, which also makes the catalog page select the same `srcset`/`image-set` resources as the test pages. If `imageScale` is omitted, `1`, or invalid, the behavior is identical to that before this property existed.
379
382
 
380
383
  There is a `shoot` act type that can be used to make additional page images during a job.
381
384
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "77.2.0",
3
+ "version": "78.0.0",
4
4
  "description": "Run 1300 web accessibility tests from 10 tools and get a standardized report",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/procs/catalog.js CHANGED
@@ -33,6 +33,16 @@ const {shoot} = require('./shoot');
33
33
  exports.getCatalog = async report => {
34
34
  const {browserID} = report;
35
35
  const targetURL = report.target?.url;
36
+ // Image scale factor (report.imageScale). When greater than 1, the catalog context
37
+ // runs at that deviceScaleFactor and a supplemental page image is captured at device
38
+ // scale, i.e. with imageScale times the pixels of the CSS layout. Box IDs are
39
+ // CSS-pixel regardless, because getBoundingClientRect reports CSS pixels by
40
+ // definition; consumers map them onto the supplemental image by multiplying by
41
+ // imageScale. Omitted, 1, or invalid values keep the behavior identical to before
42
+ // this option existed.
43
+ const imageScale = Number.isFinite(report.imageScale) && report.imageScale > 1
44
+ ? report.imageScale
45
+ : 1;
36
46
  // If the report specifies a global browser ID and a global target URL:
37
47
  if (browserID && targetURL) {
38
48
  // Launch a browser and visit the target, or abort the job on failure.
@@ -40,7 +50,8 @@ exports.getCatalog = async report => {
40
50
  report,
41
51
  actIndex: null,
42
52
  tempBrowserID: browserID,
43
- tempURL: targetURL
53
+ tempURL: targetURL,
54
+ contextOverrides: imageScale > 1 ? {deviceScaleFactor: imageScale} : {}
44
55
  });
45
56
  // If the launch and navigation succeeded:
46
57
  if (page) {
@@ -58,13 +69,27 @@ exports.getCatalog = async report => {
58
69
  });
59
70
  // If a page image is required:
60
71
  if ([0, 2, 4, 6].includes(report.imageColor)) {
61
- // Create one and add it to the report.
72
+ // Create one at CSS-pixel scale and add it to the report as images[0]. This
73
+ // scale is invariant to imageScale and to the context's deviceScaleFactor, so
74
+ // the testaro motion rule, which compares its own CSS-scale screenshot with
75
+ // images[0], is unaffected by the imageScale option.
62
76
  console.log('Creating page image');
63
77
  await shoot(page, report, {
64
78
  exclusionSelector: '',
65
79
  colorType: report.imageColor,
66
80
  action: 'report'
67
81
  });
82
+ // If a supersampled page image is also required:
83
+ if (imageScale > 1) {
84
+ // Create one at device-pixel scale and add it to the report as images[1].
85
+ console.log(`Creating page image at ${imageScale}x device scale`);
86
+ await shoot(page, report, {
87
+ exclusionSelector: '',
88
+ colorType: report.imageColor,
89
+ action: 'report',
90
+ scale: 'device'
91
+ });
92
+ }
68
93
  }
69
94
  // Get a catalog of the elements in the page and a map of path IDs to catalog indexes.
70
95
  console.log('Creating catalog');
@@ -114,7 +139,14 @@ exports.getCatalog = async report => {
114
139
  texts[text] ??= [];
115
140
  texts[text].push(index);
116
141
  }
117
- const domRect = element.getBoundingClientRect();
142
+ // Get its bounding box, but only if the element is painted. Chromium reports
143
+ // plausible nonzero boxes for laid-out but unpainted content (visibility: hidden
144
+ // and content-visibility: hidden subtrees), and such a box disagrees with the
145
+ // page image, overlapping unrelated visible elements.
146
+ const isVisible = typeof element.checkVisibility === 'function'
147
+ ? element.checkVisibility({checkVisibilityCSS: true, visibilityProperty: true})
148
+ : true;
149
+ const domRect = isVisible ? element.getBoundingClientRect() : null;
118
150
  // Get its box ID.
119
151
  const boxID = domRect
120
152
  ? ['x', 'y', 'width', 'height'].map(key => Math.round(domRect[key])).join(':')
package/procs/launch.js CHANGED
@@ -281,7 +281,10 @@ const launchOnce = async opts => {
281
281
  tempURL = '',
282
282
  headEmulation = 'high',// low, high
283
283
  xPathNeed = 'script',// own, script, attribute, none
284
- needsAccessibleName = false
284
+ needsAccessibleName = false,
285
+ // Extra Playwright context options (e.g. deviceScaleFactor for device-pixel page
286
+ // images), spread last so they win over the defaults.
287
+ contextOverrides = {}
285
288
  } = opts;
286
289
  const act = report.acts[actIndex] ?? {};
287
290
  const {device} = report;
@@ -373,7 +376,9 @@ const launchOnce = async opts => {
373
376
  'Accept-Encoding': 'gzip, deflate, br',
374
377
  'DNT': '1',
375
378
  'Upgrade-Insecure-Requests': '1'
376
- }
379
+ },
380
+ // Caller-specified context options (see contextOverrides above).
381
+ ...contextOverrides
377
382
  };
378
383
  browserContext = await browser.newContext(contextOptions);
379
384
  // Prevent default timeouts.
@@ -599,7 +604,9 @@ exports.launch = async (opts = {}) => {
599
604
  headEmulation = 'high',
600
605
  xPathNeed = 'script',
601
606
  needsAccessibleName = false,
602
- retries = 2
607
+ retries = 2,
608
+ // Extra Playwright context options, passed through to launchOnce.
609
+ contextOverrides = {}
603
610
  } = opts;
604
611
  // If the report is valid:
605
612
  const jobValidation = isValidJob(report);
@@ -615,7 +622,8 @@ exports.launch = async (opts = {}) => {
615
622
  tempURL,
616
623
  headEmulation,
617
624
  xPathNeed,
618
- needsAccessibleName
625
+ needsAccessibleName,
626
+ contextOverrides
619
627
  }
620
628
  );
621
629
  // If the launch and navigation succeeded:
@@ -657,7 +665,8 @@ exports.launch = async (opts = {}) => {
657
665
  tempURL,
658
666
  headEmulation,
659
667
  xPathNeed,
660
- needsAccessibleName
668
+ needsAccessibleName,
669
+ contextOverrides
661
670
  }
662
671
  );
663
672
  // If the launch and navigation succeeded:
package/procs/shoot.js CHANGED
@@ -26,10 +26,13 @@ const randomFileName = (suffixLength = 3) => {
26
26
  return fileName;
27
27
  };
28
28
  // Creates and returns a screenshot.
29
- const screenShot = async (page, exclusionLocator = null) => {
29
+ const screenShot = async (page, exclusionLocator = null, scale = 'css') => {
30
30
  const options = {
31
31
  fullPage: true,
32
- scale: 'css',
32
+ // 'css' renders 1 image pixel per CSS pixel; 'device' renders at the context's
33
+ // deviceScaleFactor, i.e. an image whose pixel coordinates are the CSS
34
+ // coordinates multiplied by that factor.
35
+ scale,
33
36
  timeout: applyMultiplier(4000)
34
37
  };
35
38
  if (exclusionLocator) {
@@ -49,10 +52,12 @@ exports.shoot = async (page, report, {
49
52
  // Color fidelity: 0 (grayscale), 2 (RGB), 4 (grayscale alpha), 6 (RGBA).
50
53
  colorType = 0,
51
54
  // Disposition: return, report, file.
52
- action = 'return'
55
+ action = 'return',
56
+ // Screenshot scale ('css' or 'device'); see screenShot above.
57
+ scale = 'css'
53
58
  } = {}) => {
54
59
  // Make and get a screenshot as a buffer.
55
- let shot = await screenShot(page, exclusionSelector ? page.locator(exclusionSelector) : null);
60
+ let shot = await screenShot(page, exclusionSelector ? page.locator(exclusionSelector) : null, scale);
56
61
  // If it succeeded:
57
62
  if (shot.length) {
58
63
  // Get the screenshot as an object representation of a PNG image.