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 +110 -0
- package/package.json +1 -1
- package/procs/screenShot.js +32 -0
- package/procs/tellServer.js +1 -1
- package/procs/testaro.js +4 -2
- package/procs/visChange.js +7 -25
- package/run.js +120 -47
- package/testaro/adbID.js +2 -2
- package/testaro/altScheme.js +2 -2
- package/testaro/attVal.js +2 -2
- package/testaro/autocomplete.js +2 -2
- package/testaro/captionLoc.js +2 -2
- package/testaro/datalistRef.js +2 -2
- package/testaro/embAc.js +2 -2
- package/testaro/focInd.js +2 -2
- package/testaro/focOp.js +2 -2
- package/testaro/focVis.js +2 -2
- package/testaro/hover.js +2 -2
- package/testaro/labClash.js +2 -2
- package/testaro/lineHeight.js +2 -2
- package/testaro/linkAmb.js +2 -2
- package/testaro/miniText.js +2 -2
- package/testaro/motion.js +67 -59
- package/testaro/motionSolo.js +94 -0
- package/testaro/opFoc.js +2 -2
- package/testaro/phOnly.js +2 -2
- package/testaro/pseudoP.js +2 -2
- package/testaro/radioSet.js +2 -2
- package/testaro/role.js +2 -2
- package/testaro/secHeading.js +2 -2
- package/testaro/shoot.js +57 -0
- package/testaro/targetSmall.js +2 -2
- package/testaro/targetTiny.js +2 -2
- package/testaro/textSem.js +2 -2
- package/testaro/zIndex.js +2 -2
- package/tests/testaro.js +608 -296
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
|
@@ -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
|
+
};
|
package/procs/tellServer.js
CHANGED
|
@@ -35,7 +35,7 @@ const agent = process.env.AGENT;
|
|
|
35
35
|
|
|
36
36
|
// FUNCTIONS
|
|
37
37
|
|
|
38
|
-
// Sends a
|
|
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
|
|
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
|
|
154
|
+
const result = await getRuleResult(withItems, all, ruleID, whats, ordinalSeverity, summaryTagName);
|
|
153
155
|
// Return the result.
|
|
154
156
|
return result;
|
|
155
157
|
};
|
package/procs/visChange.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
|
-
© 2023–
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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[
|
|
1021
|
-
const selector = typeof moves[
|
|
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 (
|
|
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(
|
|
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 ${
|
|
1143
|
+
console.log(`ERROR waiting for stable ${type} (${error.message})`);
|
|
1140
1144
|
act.result.success = false;
|
|
1141
|
-
act.result.error = `ERROR waiting for stable ${
|
|
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 ${
|
|
1155
|
+
console.log(`ERROR checking ${type} (${error.message})`);
|
|
1152
1156
|
act.result.success = false;
|
|
1153
|
-
act.result.error = `ERROR checking ${
|
|
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 ${
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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(
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
66
|
+
return await getRuleResult(withItems, all, 'adbID', whats, 3);
|
|
67
67
|
};
|
package/testaro/altScheme.js
CHANGED
|
@@ -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,
|
|
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
|
|
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,
|
|
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
|
|
57
|
+
return await getRuleResult(withItems, all, 'attVal', whats, 2);
|
|
58
58
|
};
|
package/testaro/autocomplete.js
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
// ########## IMPORTS
|
|
31
31
|
|
|
32
32
|
// Module to perform common operations.
|
|
33
|
-
const {init,
|
|
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
|
|
84
|
+
return await getRuleResult(withItems, all, 'autocomplete', whats, 2);
|
|
85
85
|
};
|