testaro 60.3.0 → 60.4.1

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/UPGRADES.md CHANGED
@@ -2802,3 +2802,113 @@ This gives you:
2802
2802
  ✅ No git conflicts
2803
2803
 
2804
2804
  Claude Sonnet 4.5 • 1x
2805
+
2806
+ ## Cleanup synchronization
2807
+
2808
+ Claude Sonnet 4.5 recommendation to handle already-closed error messages on server (but not the local host) after end of a job:
2809
+
2810
+ The issue is that forceKillBrowsers() is killing the browser processes immediately, but the async browserClose() calls in the signal handlers are still trying to close already-dead browsers, causing repeated errors.
2811
+
2812
+ Solution: Coordinate Async and Sync Cleanup
2813
+ Replace the cleanup handlers in run.js:
2814
+
2815
+ ```
2816
+ // CLEANUP HANDLERS
2817
+
2818
+ // Track cleanup state.
2819
+ let cleanupInProgress = false;
2820
+ let forceKillScheduled = false;
2821
+
2822
+ // Force kills all Playwright browser processes synchronously.
2823
+ const forceKillBrowsers = () => {
2824
+ if (cleanupInProgress) {
2825
+ return;
2826
+ }
2827
+ cleanupInProgress = true;
2828
+
2829
+ const {execSync} = require('child_process');
2830
+ try {
2831
+ // Kill Chromium headless shell processes.
2832
+ execSync('pkill -9 -f "chromium_headless_shell.*headless_shell"', {stdio: 'ignore'});
2833
+ }
2834
+ catch(error) {
2835
+ // Ignore errors - processes may not exist.
2836
+ }
2837
+ };
2838
+
2839
+ // Graceful cleanup with timeout fallback.
2840
+ const gracefulCleanup = async (timeoutMs = 2000) => {
2841
+ if (forceKillScheduled) {
2842
+ return;
2843
+ }
2844
+ forceKillScheduled = true;
2845
+
2846
+ // Try graceful close with timeout.
2847
+ const closePromise = browserClose();
2848
+ const timeoutPromise = new Promise(resolve => setTimeout(resolve, timeoutMs));
2849
+
2850
+ await Promise.race([closePromise, timeoutPromise]);
2851
+
2852
+ // Force kill any remaining processes.
2853
+ forceKillBrowsers();
2854
+ };
2855
+
2856
+ // Synchronous cleanup on process exit (last resort).
2857
+ process.on('exit', () => {
2858
+ if (!cleanupInProgress) {
2859
+ forceKillBrowsers();
2860
+ }
2861
+ });
2862
+
2863
+ // Graceful cleanup on SIGINT (Ctrl+C).
2864
+ process.on('SIGINT', async () => {
2865
+ console.log('\nReceived SIGINT, cleaning up...');
2866
+ await gracefulCleanup();
2867
+ process.exit(130);
2868
+ });
2869
+
2870
+ // Graceful cleanup on SIGTERM.
2871
+ process.on('SIGTERM', async () => {
2872
+ console.log('Received SIGTERM, cleaning up...');
2873
+ await gracefulCleanup();
2874
+ process.exit(143);
2875
+ });
2876
+
2877
+ // Cleanup on uncaught exceptions.
2878
+ process.on('uncaughtException', async error => {
2879
+ console.error('Uncaught exception:', error);
2880
+ await gracefulCleanup(1000);
2881
+ process.exit(1);
2882
+ });
2883
+ ```
2884
+
2885
+ And in `doActs`, replace the cleanup:
2886
+
2887
+ ```
2888
+ console.log('Acts completed');
2889
+ // If standardization is required:
2890
+ if (['also', 'only'].includes(standard)) {
2891
+ // ... existing standardization code ...
2892
+ }
2893
+ // Close the browser gracefully.
2894
+ await gracefulCleanup();
2895
+ // Delete the temporary report file.
2896
+ await fs.rm(reportPath, {force: true});
2897
+ return report;
2898
+ ```
2899
+
2900
+ Key changes:
2901
+
2902
+ gracefulCleanup(): Attempts async browserClose() but races it against a timeout, then force-kills
2903
+ Guard flags: Prevent multiple simultaneous cleanup attempts that cause the error messages
2904
+ Timeout: Gives browserClose 2 seconds to complete before forcing kill
2905
+ No redundant calls: Only gracefulCleanup() calls both browserClose() and forceKillBrowsers()
2906
+ process.on('exit') as fallback: Only runs if other cleanup didn't complete
2907
+ This approach:
2908
+
2909
+ Tries graceful closure first (avoiding the errors)
2910
+ Falls back to force-kill after timeout
2911
+ Prevents the async cleanup from running after force-kill
2912
+ Works consistently on both macOS and Ubuntu
2913
+
2914
+ The above recommendation seems complex. Meanwhile the error message has been suppressed on the basis that context closure is not necessarily an error.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "60.3.0",
3
+ "version": "60.4.1",
4
4
  "description": "Run 1000 web accessibility tests from 11 tools and get a standardized report",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,32 @@
1
+ /*
2
+ © 2023–2025 CVS Health and/or one of its affiliates. All rights reserved.
3
+ © 2025 Jonathan Robert Pool. All rights reserved.
4
+ Licensed under the MIT License. See LICENSE file for details.
5
+ */
6
+
7
+ /*
8
+ screenShot
9
+ This procedure creates and returns a full-page screenshot, optionally with an exclused element.
10
+ This procedure uses the Playwright page.screenshot method, which is not implemented for the
11
+ firefox browser type.
12
+ */
13
+
14
+ // FUNCTIONS
15
+
16
+ // Creates and returns a screenshot.
17
+ exports.screenShot = async (page, exclusion = null) => {
18
+ const options = {
19
+ fullPage: true,
20
+ omitBackground: true,
21
+ timeout: 2000
22
+ };
23
+ if (exclusion) {
24
+ options.mask = [exclusion];
25
+ }
26
+ // Make and return a screenshot as a buffer.
27
+ return await page.screenshot(options)
28
+ .catch(error => {
29
+ console.log(`ERROR: Screenshot failed (${error.message})`);
30
+ return '';
31
+ });
32
+ };
@@ -35,7 +35,7 @@ const agent = process.env.AGENT;
35
35
 
36
36
  // FUNCTIONS
37
37
 
38
- // Sends a notification to an observer.
38
+ // Sends a notice to an observer.
39
39
  exports.tellServer = (report, messageParams, logMessage) => {
40
40
  const {serverID} = report.sources;
41
41
  const observerURL = typeof serverID === 'number' ? process.env[`NETWATCH_URL_${serverID}_OBSERVE`] : '';
package/procs/testaro.js CHANGED
@@ -68,7 +68,9 @@ const init = exports.init = async (sampleMax, page, locAllSelector, options = {}
68
68
  };
69
69
 
70
70
  // Populates and returns a result.
71
- const report = exports.report = async (withItems, all, ruleID, whats, ordinalSeverity, tagName = '') => {
71
+ const getRuleResult = exports.getRuleResult = async (
72
+ withItems, all, ruleID, whats, ordinalSeverity, tagName = ''
73
+ ) => {
72
74
  const {locs, result} = all;
73
75
  const {data, totals, standardInstances} = result;
74
76
  // For each violation locator:
@@ -149,7 +151,7 @@ exports.simplify = async (page, withItems, ruleData) => {
149
151
  complaints.instance,
150
152
  complaints.summary
151
153
  ];
152
- const result = await report(withItems, all, ruleID, whats, ordinalSeverity, summaryTagName);
154
+ const result = await getRuleResult(withItems, all, ruleID, whats, ordinalSeverity, summaryTagName);
153
155
  // Return the result.
154
156
  return result;
155
157
  };
@@ -1,5 +1,6 @@
1
1
  /*
2
- © 2023–2024 CVS Health and/or one of its affiliates. All rights reserved.
2
+ © 2023–2025 CVS Health and/or one of its affiliates. All rights reserved.
3
+ © 2025 Jonathan Robert Pool. All rights reserved.
3
4
 
4
5
  MIT License
5
6
 
@@ -25,37 +26,18 @@
25
26
  /*
26
27
  visChange
27
28
  This procedure reports a change in the visible content of a page between two times, optionally
28
- hovering over a locator-defined element immediately after the first time.
29
-
30
- WARNING: This test uses the Playwright page.screenshot method, which produces incorrect results
31
- when the browser type is chromium and is not implemented for the firefox browser type. The only
32
- browser type usable with this test is webkit.
29
+ hovering over a locator-defined element immediately after the first time. This test uses the
30
+ Playwright page.screenshot method, which is not implemented for the firefox browser type.
33
31
  */
34
32
 
35
33
  // IMPORTS
36
34
 
37
35
  const pixelmatch = require('pixelmatch').default;
38
36
  const {PNG} = require('pngjs');
37
+ const {screenShot} = require('./screenShot');
39
38
 
40
39
  // FUNCTIONS
41
40
 
42
- // Creates and returns a screenshot.
43
- const shoot = async (page, exclusion = null) => {
44
- // Make a screenshot as a buffer.
45
- const options = {
46
- fullPage: true,
47
- omitBackground: true,
48
- timeout: 2000
49
- };
50
- if (exclusion) {
51
- options.mask = [exclusion];
52
- }
53
- return await page.screenshot(options)
54
- .catch(error => {
55
- console.log(`ERROR: Screenshot failed (${error.message})`);
56
- return '';
57
- });
58
- };
59
41
  exports.visChange = async (page, options = {}) => {
60
42
  const {delayBefore, delayBetween, exclusion} = options;
61
43
  // Wait, if required.
@@ -74,7 +56,7 @@ exports.visChange = async (page, options = {}) => {
74
56
  });
75
57
  }
76
58
  // Make and get a screenshot, excluding an element if specified.
77
- const shot0 = await shoot(page, exclusion);
59
+ const shot0 = await screenShot(page, exclusion);
78
60
  // If it succeeded:
79
61
  if (shot0.length) {
80
62
  // If an exclusion was specified:
@@ -96,7 +78,7 @@ exports.visChange = async (page, options = {}) => {
96
78
  // Wait as specified, or 3 seconds.
97
79
  await page.waitForTimeout(delayBetween || 3000);
98
80
  // Make and get another screenshot.
99
- const shot1 = await shoot(page, exclusion);
81
+ const shot1 = await screenShot(page, exclusion);
100
82
  // If it succeeded:
101
83
  if (shot1.length) {
102
84
  // Get the shots as PNG images.
package/run.js CHANGED
@@ -34,6 +34,8 @@
34
34
  const fs = require('fs/promises');
35
35
  // Module to keep secrets.
36
36
  require('dotenv').config({quiet: true});
37
+ // Module to execute shell commands.
38
+ const {execSync} = require('child_process');
37
39
  // Module to validate jobs.
38
40
  const {isBrowserID, isDeviceID, isURL, isValidJob, tools} = require('./procs/job');
39
41
  // Module to evade automation detection.
@@ -109,12 +111,14 @@ const tmpDir = os.tmpdir();
109
111
 
110
112
  // Facts about the current session.
111
113
  let actCount = 0;
112
- let browserCloseIntentional = false;
113
114
  // Facts about the current act.
114
115
  let actIndex = 0;
115
116
  let browser;
117
+ let cleanupInProgress = false;
118
+ let browserCloseIntentional = false;
116
119
  let browserContext;
117
120
  let page;
121
+ let report;
118
122
  let requestedURL = '';
119
123
 
120
124
  // FUNCTIONS
@@ -241,25 +245,6 @@ const goTo = async (report, page, url, timeout, waitUntil) => {
241
245
  };
242
246
  }
243
247
  };
244
- // Closes the current browser.
245
- const browserClose = async () => {
246
- if (browser) {
247
- browserCloseIntentional = true;
248
- for (const context of browser.contexts()) {
249
- try {
250
- await context.close();
251
- }
252
- catch(error) {
253
- console.log(
254
- `ERROR trying to close context: ${error.message.slice(0, 200).replace(/\n.+/s, '')}`
255
- );
256
- }
257
- }
258
- await browser.close();
259
- browserCloseIntentional = false;
260
- browser = null;
261
- }
262
- };
263
248
  // Adds an error result to an act.
264
249
  const addError = (alsoLog, alsoAbort, report, actIndex, message) => {
265
250
  // If the error is to be logged:
@@ -286,6 +271,24 @@ const addError = (alsoLog, alsoAbort, report, actIndex, message) => {
286
271
  abortActs(report, actIndex);
287
272
  }
288
273
  };
274
+ // Closes any current browser.
275
+ const browserClose = async () => {
276
+ // If a browser exists:
277
+ if (browser) {
278
+ browserCloseIntentional = true;
279
+ // Try to close all its contexts and ignore any messages that they are already closed.
280
+ for (const context of browser.contexts()) {
281
+ try {
282
+ await context.close();
283
+ }
284
+ catch(error) {}
285
+ }
286
+ // Close the browser.
287
+ await browser.close();
288
+ browserCloseIntentional = false;
289
+ browser = null;
290
+ }
291
+ };
289
292
  // Launches a browser and navigates to a URL.
290
293
  const launch = exports.launch = async (
291
294
  report, debug, waits, tempBrowserID, tempURL, retries = 2
@@ -301,7 +304,7 @@ const launch = exports.launch = async (
301
304
  report.target.url = url;
302
305
  // Create a browser of the specified or default type.
303
306
  const browserType = playwrightBrowsers[browserID];
304
- // Close the current browser, if any.
307
+ // Close any current browser.
305
308
  await browserClose();
306
309
  // Define browser options.
307
310
  const browserOptions = {
@@ -676,6 +679,7 @@ const launchSpecs = (act, report) => [
676
679
  // Performs the acts in a report and adds the results to the report.
677
680
  const doActs = async (report, opts = {}) => {
678
681
  const {acts} = report;
682
+ // Get the granular observation options, if any.
679
683
  const {onProgress = null, signal = null} = opts;
680
684
  // Get the standardization specification.
681
685
  const standard = report.standard || 'only';
@@ -694,7 +698,7 @@ const doActs = async (report, opts = {}) => {
694
698
  if (report.observe) {
695
699
  const whichParam = which ? `&which=${which}` : '';
696
700
  const messageParams = `act=${type}${whichParam}`;
697
- // If a progress callback has been provided:
701
+ // If a progress callback has been provided by a caller on this host:
698
702
  if (onProgress) {
699
703
  // Notify the observer of the act.
700
704
  try {
@@ -710,7 +714,7 @@ const doActs = async (report, opts = {}) => {
710
714
  }
711
715
  // Otherwise, i.e. if no progress callback has been provided:
712
716
  else {
713
- // Notify the observer of the act and log it.
717
+ // Notify the remote observer of the act and log it.
714
718
  tellServer(report, messageParams, message);
715
719
  }
716
720
  }
@@ -854,7 +858,7 @@ const doActs = async (report, opts = {}) => {
854
858
  // Otherwise, if a current page exists:
855
859
  else if (page) {
856
860
  // If the act is navigation to a url:
857
- if (act.type === 'url') {
861
+ if (type === 'url') {
858
862
  // Identify the URL.
859
863
  const resolved = act.which.replace('__dirname', __dirname);
860
864
  requestedURL = resolved;
@@ -888,7 +892,7 @@ const doActs = async (report, opts = {}) => {
888
892
  }
889
893
  }
890
894
  // Otherwise, if the act is a wait for text:
891
- else if (act.type === 'wait') {
895
+ else if (type === 'wait') {
892
896
  const {what, which} = act;
893
897
  console.log(`>> ${what}`);
894
898
  const result = act.result = {};
@@ -956,7 +960,7 @@ const doActs = async (report, opts = {}) => {
956
960
  }
957
961
  }
958
962
  // Otherwise, if the act is a wait for a state:
959
- else if (act.type === 'state') {
963
+ else if (type === 'state') {
960
964
  // Wait for it.
961
965
  const stateIndex = ['loaded', 'idle'].indexOf(act.which);
962
966
  await page.waitForLoadState(
@@ -978,7 +982,7 @@ const doActs = async (report, opts = {}) => {
978
982
  }
979
983
  }
980
984
  // Otherwise, if the act is a page switch:
981
- else if (act.type === 'page') {
985
+ else if (type === 'page') {
982
986
  // Wait for a page to be created and identify it as current.
983
987
  page = await browserContext.waitForEvent('page');
984
988
  // Wait until it is idle.
@@ -995,7 +999,7 @@ const doActs = async (report, opts = {}) => {
995
999
  // Add the URL to the act.
996
1000
  act.actualURL = url;
997
1001
  // If the act is a revelation:
998
- if (act.type === 'reveal') {
1002
+ if (type === 'reveal') {
999
1003
  act.result = {
1000
1004
  success: true
1001
1005
  };
@@ -1017,8 +1021,8 @@ const doActs = async (report, opts = {}) => {
1017
1021
  });
1018
1022
  }
1019
1023
  // Otherwise, if the act is a move:
1020
- else if (moves[act.type]) {
1021
- const selector = typeof moves[act.type] === 'string' ? moves[act.type] : act.what;
1024
+ else if (moves[type]) {
1025
+ const selector = typeof moves[type] === 'string' ? moves[type] : act.what;
1022
1026
  // Try up to 5 times to:
1023
1027
  act.result = {found: false};
1024
1028
  let selection = {};
@@ -1127,18 +1131,18 @@ const doActs = async (report, opts = {}) => {
1127
1131
  };
1128
1132
  // FUNCTION DEFINITION END
1129
1133
  // If the move is a button click, perform it.
1130
- if (act.type === 'button') {
1134
+ if (type === 'button') {
1131
1135
  await selection.click({timeout: 3000});
1132
1136
  act.result.success = true;
1133
1137
  act.result.move = 'clicked';
1134
1138
  }
1135
1139
  // Otherwise, if it is checking a radio button or checkbox, perform it.
1136
- else if (['checkbox', 'radio'].includes(act.type)) {
1140
+ else if (['checkbox', 'radio'].includes(type)) {
1137
1141
  await selection.waitForElementState('stable', {timeout: 2000})
1138
1142
  .catch(error => {
1139
- console.log(`ERROR waiting for stable ${act.type} (${error.message})`);
1143
+ console.log(`ERROR waiting for stable ${type} (${error.message})`);
1140
1144
  act.result.success = false;
1141
- act.result.error = `ERROR waiting for stable ${act.type}`;
1145
+ act.result.error = `ERROR waiting for stable ${type}`;
1142
1146
  });
1143
1147
  if (! act.result.error) {
1144
1148
  const isEnabled = await selection.isEnabled();
@@ -1148,9 +1152,9 @@ const doActs = async (report, opts = {}) => {
1148
1152
  timeout: 2000
1149
1153
  })
1150
1154
  .catch(error => {
1151
- console.log(`ERROR checking ${act.type} (${error.message})`);
1155
+ console.log(`ERROR checking ${type} (${error.message})`);
1152
1156
  act.result.success = false;
1153
- act.result.error = `ERROR checking ${act.type}`;
1157
+ act.result.error = `ERROR checking ${type}`;
1154
1158
  });
1155
1159
  if (! act.result.error) {
1156
1160
  act.result.success = true;
@@ -1158,20 +1162,20 @@ const doActs = async (report, opts = {}) => {
1158
1162
  }
1159
1163
  }
1160
1164
  else {
1161
- const report = `ERROR: could not check ${act.type} because disabled`;
1165
+ const report = `ERROR: could not check ${type} because disabled`;
1162
1166
  act.result.success = false;
1163
1167
  act.result.error = report;
1164
1168
  }
1165
1169
  }
1166
1170
  }
1167
1171
  // Otherwise, if it is focusing the element, perform it.
1168
- else if (act.type === 'focus') {
1172
+ else if (type === 'focus') {
1169
1173
  await selection.focus({timeout: 2000});
1170
1174
  act.result.success = true;
1171
1175
  act.result.move = 'focused';
1172
1176
  }
1173
1177
  // Otherwise, if it is clicking a link:
1174
- else if (act.type === 'link') {
1178
+ else if (type === 'link') {
1175
1179
  const href = await selection.getAttribute('href');
1176
1180
  const target = await selection.getAttribute('target');
1177
1181
  act.result.href = href || 'NONE';
@@ -1210,7 +1214,7 @@ const doActs = async (report, opts = {}) => {
1210
1214
  }
1211
1215
  }
1212
1216
  // Otherwise, if it is selecting an option in a select list, perform it.
1213
- else if (act.type === 'select') {
1217
+ else if (type === 'select') {
1214
1218
  const options = await selection.$$('option');
1215
1219
  let optionText = '';
1216
1220
  if (options && Array.isArray(options) && options.length) {
@@ -1233,7 +1237,7 @@ const doActs = async (report, opts = {}) => {
1233
1237
  act.result.option = optionText;
1234
1238
  }
1235
1239
  // Otherwise, if it is entering text in an input element:
1236
- else if (['text', 'search'].includes(act.type)) {
1240
+ else if (['text', 'search'].includes(type)) {
1237
1241
  act.result.attributes = {};
1238
1242
  const {attributes} = act.result;
1239
1243
  const type = await selection.getAttribute('type');
@@ -1256,7 +1260,7 @@ const doActs = async (report, opts = {}) => {
1256
1260
  act.result.success = true;
1257
1261
  act.result.move = 'entered';
1258
1262
  // If the input is a search input:
1259
- if (act.type === 'search') {
1263
+ if (type === 'search') {
1260
1264
  // Press the Enter key and wait for a network to be idle.
1261
1265
  doAndWait(false);
1262
1266
  }
@@ -1280,7 +1284,7 @@ const doActs = async (report, opts = {}) => {
1280
1284
  }
1281
1285
  }
1282
1286
  // Otherwise, if the act is a keypress:
1283
- else if (act.type === 'press') {
1287
+ else if (type === 'press') {
1284
1288
  // Identify the number of times to press the key.
1285
1289
  let times = 1 + (act.again || 0);
1286
1290
  report.jobData.presses += times;
@@ -1296,7 +1300,7 @@ const doActs = async (report, opts = {}) => {
1296
1300
  };
1297
1301
  }
1298
1302
  // Otherwise, if it is a repetitive keyboard navigation:
1299
- else if (act.type === 'presses') {
1303
+ else if (type === 'presses') {
1300
1304
  const {navKey, what, which, withItems} = act;
1301
1305
  const matchTexts = which ? which.map(text => debloat(text)) : [];
1302
1306
  // Initialize the loop variables.
@@ -1465,7 +1469,31 @@ const doActs = async (report, opts = {}) => {
1465
1469
  console.log('Acts completed');
1466
1470
  // If standardization is required:
1467
1471
  if (['also', 'only'].includes(standard)) {
1468
- console.log('>>>> Standardizing results of test acts');
1472
+ // If granular reporting has been specified:
1473
+ if (report.observe) {
1474
+ // If a progress callback has been provided:
1475
+ if (onProgress) {
1476
+ // Notify the observer of the start of standardization.
1477
+ try {
1478
+ onProgress({
1479
+ type: 'standardization',
1480
+ which: 'start'
1481
+ });
1482
+ console.log(`${'Standardization started'} (observer notified)`);
1483
+ }
1484
+ catch (error) {
1485
+ console.log(`${message} (observer notification failed: ${errorStart(error)})`);
1486
+ }
1487
+ }
1488
+ // Otherwise, i.e. if no progress callback has been provided:
1489
+ else {
1490
+ // Notify the observer of the act and log it.
1491
+ tellServer(report, messageParams, message);
1492
+ }
1493
+
1494
+ // Notify the observer and log the start of standardization.
1495
+ tellServer(report, '', 'Starting result standardization');
1496
+ }
1469
1497
  const launchSpecActs = {};
1470
1498
  // For each act:
1471
1499
  report.acts.forEach((act, index) => {
@@ -1534,7 +1562,7 @@ const doActs = async (report, opts = {}) => {
1534
1562
  // Runs a job and returns a report.
1535
1563
  exports.doJob = async (job, opts = {}) => {
1536
1564
  // Make a report as a copy of the job.
1537
- let report = JSON.parse(JSON.stringify(job));
1565
+ report = JSON.parse(JSON.stringify(job));
1538
1566
  const jobData = report.jobData = {};
1539
1567
  // Get whether the job is valid and, if not, why.
1540
1568
  const jobInvalidity = isValidJob(job);
@@ -1572,7 +1600,7 @@ exports.doJob = async (job, opts = {}) => {
1572
1600
  process.exit();
1573
1601
  }
1574
1602
  });
1575
- // Perform the acts and get a report.
1603
+ // Perform the acts with any specified same-host observation options and get a report.
1576
1604
  report = await doActs(report, opts);
1577
1605
  // Add the end time and duration to the report.
1578
1606
  const endTime = new Date();
@@ -1594,3 +1622,48 @@ exports.doJob = async (job, opts = {}) => {
1594
1622
  // Return the report.
1595
1623
  return report;
1596
1624
  };
1625
+
1626
+ // CLEANUP HANDLERS
1627
+
1628
+ // Force-kills any Playwright browser processes synchronously.
1629
+ const forceKillBrowsers = () => {
1630
+ if (cleanupInProgress) {
1631
+ return;
1632
+ }
1633
+ cleanupInProgress = true;
1634
+ try {
1635
+ // Kill any Chromium headless shell processes.
1636
+ execSync('pkill -9 -f "chromium_headless_shell.*headless_shell"', {stdio: 'ignore'});
1637
+ }
1638
+ catch(error) {}
1639
+ };
1640
+ // Force-kills any headless shell processes synchronously on process exit.
1641
+ process.on('exit', () => {
1642
+ forceKillBrowsers();
1643
+ });
1644
+ // Force-kills any headless shell processes synchronously on beforeExit.
1645
+ process.on('beforeExit', async () => {
1646
+ if (!browserCloseIntentional) {
1647
+ await browserClose();
1648
+ }
1649
+ forceKillBrowsers();
1650
+ });
1651
+ // Force-kills any headless shell processes synchronously on uncaught exceptions.
1652
+ process.on('uncaughtException', async error => {
1653
+ console.error('Uncaught exception:', error);
1654
+ await browserClose();
1655
+ forceKillBrowsers();
1656
+ process.exit(1);
1657
+ });
1658
+ // Force-kills any headless shell processes synchronously on SIGINT.
1659
+ process.on('SIGINT', async () => {
1660
+ await browserClose();
1661
+ forceKillBrowsers();
1662
+ process.exit(0);
1663
+ });
1664
+ // Force-kills any headless shell processes synchronously on SIGTERM.
1665
+ process.on('SIGTERM', async () => {
1666
+ await browserClose();
1667
+ forceKillBrowsers();
1668
+ process.exit(0);
1669
+ });
package/testaro/adbID.js CHANGED
@@ -28,7 +28,7 @@
28
28
  Clean-room rule: report elements that reference aria-describedby targets that are missing or ambiguous (duplicate ids).
29
29
  */
30
30
 
31
- const {init, report} = require('../procs/testaro');
31
+ const {init, getRuleResult} = require('../procs/testaro');
32
32
 
33
33
  exports.reporter = async (page, withItems) => {
34
34
  // elements that reference aria-describedby
@@ -63,5 +63,5 @@ exports.reporter = async (page, withItems) => {
63
63
  'Referenced description of the element is ambiguous or missing',
64
64
  'Referenced descriptions of elements are ambiguous or missing'
65
65
  ];
66
- return await report(withItems, all, 'adbID', whats, 3);
66
+ return await getRuleResult(withItems, all, 'adbID', whats, 3);
67
67
  };
@@ -28,7 +28,7 @@
28
28
  Identify img elements whose alt attribute is an entire URL or clearly a file name (favicon).
29
29
  */
30
30
 
31
- const {init, report} = require('../procs/testaro');
31
+ const {init, getRuleResult} = require('../procs/testaro');
32
32
 
33
33
  exports.reporter = async (page, withItems) => {
34
34
  // Candidate images: any img with an alt attribute (including empty)
@@ -54,5 +54,5 @@ exports.reporter = async (page, withItems) => {
54
54
  'Element has an alt attribute with a URL as its entire value',
55
55
  'img elements have alt attributes with URLs as their entire values'
56
56
  ];
57
- return await report(withItems, all, 'altScheme', whats, 2);
57
+ return await getRuleResult(withItems, all, 'altScheme', whats, 2);
58
58
  };
package/testaro/attVal.js CHANGED
@@ -30,7 +30,7 @@
30
30
  // ########## IMPORTS
31
31
 
32
32
  // Module to perform common operations.
33
- const {init, report} = require('../procs/testaro');
33
+ const {init, getRuleResult} = require('../procs/testaro');
34
34
 
35
35
  // ########## FUNCTIONS
36
36
 
@@ -54,5 +54,5 @@ exports.reporter = async (page, withItems, attributeName, areLicit, values) => {
54
54
  `Element has attribute ${attributeName} with illicit value “__param__”`,
55
55
  `Elements have attribute ${attributeName} with illicit values`
56
56
  ];
57
- return await report(withItems, all, 'attVal', whats, 2);
57
+ return await getRuleResult(withItems, all, 'attVal', whats, 2);
58
58
  };
@@ -30,7 +30,7 @@
30
30
  // ########## IMPORTS
31
31
 
32
32
  // Module to perform common operations.
33
- const {init, report} = require('../procs/testaro');
33
+ const {init, getRuleResult} = require('../procs/testaro');
34
34
  // Module to get locator data.
35
35
  const {getLocatorData} = require('../procs/getLocatorData');
36
36
 
@@ -81,5 +81,5 @@ exports.reporter = async (
81
81
  'Input is missing an autocomplete attribute with value __param__',
82
82
  'Inputs are missing applicable autocomplete attributes'
83
83
  ];
84
- return await report(withItems, all, 'autocomplete', whats, 2);
84
+ return await getRuleResult(withItems, all, 'autocomplete', whats, 2);
85
85
  };