testaro 5.7.4 → 5.8.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/package.json +1 -1
- package/run.js +131 -51
- package/tests/hover.js +181 -141
- package/tests/nuVal.js +15 -4
package/package.json
CHANGED
package/run.js
CHANGED
|
@@ -70,7 +70,7 @@ const browserTypeNames = {
|
|
|
70
70
|
'firefox': 'Firefox'
|
|
71
71
|
};
|
|
72
72
|
// Items that may be waited for.
|
|
73
|
-
const waitables = ['url', 'title', 'body'];
|
|
73
|
+
const waitables = ['url', 'title', 'body', 'mailLink'];
|
|
74
74
|
// Tenon data.
|
|
75
75
|
const tenonData = {
|
|
76
76
|
accessToken: '',
|
|
@@ -106,6 +106,7 @@ let visitRejectionCount = 0;
|
|
|
106
106
|
let visitLatency = 0;
|
|
107
107
|
let actCount = 0;
|
|
108
108
|
// Facts about the current browser.
|
|
109
|
+
let browser;
|
|
109
110
|
let browserContext;
|
|
110
111
|
let browserTypeName;
|
|
111
112
|
let requestedURL = '';
|
|
@@ -257,12 +258,24 @@ const isValidReport = async report => {
|
|
|
257
258
|
|
|
258
259
|
// ########## OTHER FUNCTIONS
|
|
259
260
|
|
|
261
|
+
// Closes the current browser.
|
|
262
|
+
const browserClose = async () => {
|
|
263
|
+
if (browser) {
|
|
264
|
+
const contexts = browser.contexts();
|
|
265
|
+
for (const context of contexts) {
|
|
266
|
+
await context.close();
|
|
267
|
+
}
|
|
268
|
+
await browser.close();
|
|
269
|
+
}
|
|
270
|
+
};
|
|
260
271
|
// Launches a browser.
|
|
261
272
|
const launch = async typeName => {
|
|
262
273
|
const browserType = require('playwright')[typeName];
|
|
263
274
|
// If the specified browser type exists:
|
|
264
275
|
if (browserType) {
|
|
265
|
-
//
|
|
276
|
+
// Close the current browser, if any.
|
|
277
|
+
await browserClose();
|
|
278
|
+
// Launch a browser of that type.
|
|
266
279
|
const browserOptions = {};
|
|
267
280
|
if (debug) {
|
|
268
281
|
browserOptions.headless = false;
|
|
@@ -271,7 +284,7 @@ const launch = async typeName => {
|
|
|
271
284
|
browserOptions.slowMo = waits;
|
|
272
285
|
}
|
|
273
286
|
let healthy = true;
|
|
274
|
-
|
|
287
|
+
browser = await browserType.launch(browserOptions)
|
|
275
288
|
.catch(error => {
|
|
276
289
|
healthy = false;
|
|
277
290
|
console.log(`ERROR launching browser: ${error.message.replace(/\n.+/s, '')}`);
|
|
@@ -429,7 +442,7 @@ const textOf = async (page, element) => {
|
|
|
429
442
|
return null;
|
|
430
443
|
}
|
|
431
444
|
};
|
|
432
|
-
// Returns an element of a type case-insensitively
|
|
445
|
+
// Returns an element of a type case-insensitively including a text.
|
|
433
446
|
const matchElement = async (page, selector, matchText, index = 0) => {
|
|
434
447
|
if (matchText) {
|
|
435
448
|
// If the page still exists:
|
|
@@ -441,14 +454,18 @@ const matchElement = async (page, selector, matchText, index = 0) => {
|
|
|
441
454
|
if (selections.length) {
|
|
442
455
|
// If there are enough to make a match possible:
|
|
443
456
|
if (index < selections.length) {
|
|
444
|
-
//
|
|
457
|
+
// For each element of the specified type:
|
|
445
458
|
let matchCount = 0;
|
|
446
459
|
const selectionTexts = [];
|
|
447
460
|
for (const selection of selections) {
|
|
461
|
+
// Add its text to the list of texts of such elements.
|
|
448
462
|
const selectionText = await textOf(page, selection);
|
|
449
463
|
selectionTexts.push(selectionText);
|
|
464
|
+
// If its text includes the specified text:
|
|
450
465
|
if (selectionText.includes(slimText)) {
|
|
466
|
+
// If the count of such elements with such texts found so far is the specified count:
|
|
451
467
|
if (matchCount++ === index) {
|
|
468
|
+
// Return it as the matching element.
|
|
452
469
|
return {
|
|
453
470
|
success: true,
|
|
454
471
|
matchingElement: selection
|
|
@@ -648,7 +665,8 @@ const isTrue = (object, specs) => {
|
|
|
648
665
|
// Adds a wait error result to an act.
|
|
649
666
|
const waitError = (page, act, error, what) => {
|
|
650
667
|
console.log(`ERROR waiting for ${what} (${error.message})`);
|
|
651
|
-
act.result =
|
|
668
|
+
act.result.found = false;
|
|
669
|
+
act.result.url = page.url();
|
|
652
670
|
act.result.error = `ERROR waiting for ${what}`;
|
|
653
671
|
return false;
|
|
654
672
|
};
|
|
@@ -741,49 +759,95 @@ const doActs = async (report, actIndex, page) => {
|
|
|
741
759
|
// Otherwise, if the act is a wait for text:
|
|
742
760
|
else if (act.type === 'wait') {
|
|
743
761
|
const {what, which} = act;
|
|
744
|
-
console.log(`>>
|
|
745
|
-
|
|
746
|
-
if
|
|
747
|
-
|
|
748
|
-
|
|
762
|
+
console.log(`>> ${what}`);
|
|
763
|
+
const result = act.result = {};
|
|
764
|
+
// Wait for the specified text, and quit if it does not appear.
|
|
765
|
+
if (what === 'url') {
|
|
766
|
+
try {
|
|
767
|
+
await page.waitForURL(which, {timeout: 15000});
|
|
768
|
+
result.found = true;
|
|
769
|
+
result.url = page.url();
|
|
770
|
+
}
|
|
771
|
+
catch(error) {
|
|
749
772
|
actIndex = -2;
|
|
750
773
|
waitError(page, act, error, 'URL');
|
|
751
|
-
}
|
|
774
|
+
}
|
|
752
775
|
}
|
|
753
|
-
else if (
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
776
|
+
else if (what === 'title') {
|
|
777
|
+
try {
|
|
778
|
+
await page.waitForFunction(
|
|
779
|
+
text => document
|
|
780
|
+
&& document.title
|
|
781
|
+
&& document.title.toLowerCase().includes(text.toLowerCase()),
|
|
782
|
+
which,
|
|
783
|
+
{
|
|
784
|
+
polling: 1000,
|
|
785
|
+
timeout: 5000
|
|
786
|
+
}
|
|
787
|
+
);
|
|
788
|
+
result.found = true;
|
|
789
|
+
result.title = await page.title();
|
|
790
|
+
}
|
|
791
|
+
catch(error) {
|
|
763
792
|
actIndex = -2;
|
|
764
793
|
waitError(page, act, error, 'title');
|
|
765
|
-
}
|
|
794
|
+
}
|
|
766
795
|
}
|
|
767
|
-
else if (
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
796
|
+
else if (what === 'body') {
|
|
797
|
+
try {
|
|
798
|
+
await page.waitForFunction(
|
|
799
|
+
text => document
|
|
800
|
+
&& document.body
|
|
801
|
+
&& document.body.innerText.toLowerCase().includes(text.toLowerCase()),
|
|
802
|
+
which,
|
|
803
|
+
{
|
|
804
|
+
polling: 2000,
|
|
805
|
+
timeout: 10000
|
|
806
|
+
}
|
|
807
|
+
);
|
|
808
|
+
result.found = true;
|
|
809
|
+
}
|
|
810
|
+
catch(error) {
|
|
777
811
|
actIndex = -2;
|
|
778
812
|
waitError(page, act, error, 'body');
|
|
779
|
-
}
|
|
813
|
+
}
|
|
780
814
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
815
|
+
else if (what === 'mailLink') {
|
|
816
|
+
try {
|
|
817
|
+
const addressJSHandle = await page.waitForFunction(
|
|
818
|
+
text => {
|
|
819
|
+
const mailLinks = document
|
|
820
|
+
&& document.body
|
|
821
|
+
&& document.body.querySelectorAll('a[href^="mailto:"]');
|
|
822
|
+
if (mailLinks && mailLinks.length) {
|
|
823
|
+
const textLC = text.toLowerCase();
|
|
824
|
+
const a11yLink = Array
|
|
825
|
+
.from(mailLinks)
|
|
826
|
+
.find(link => link.textContent.toLowerCase().includes(textLC));
|
|
827
|
+
if (a11yLink) {
|
|
828
|
+
return a11yLink.href.replace(/^mailto:/, '');
|
|
829
|
+
}
|
|
830
|
+
else {
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
},
|
|
838
|
+
which,
|
|
839
|
+
{
|
|
840
|
+
polling: 1000,
|
|
841
|
+
timeout: 5000
|
|
842
|
+
}
|
|
843
|
+
);
|
|
844
|
+
const address = await addressJSHandle.jsonValue();
|
|
845
|
+
result.found = true;
|
|
846
|
+
result.address = address;
|
|
847
|
+
}
|
|
848
|
+
catch(error) {
|
|
849
|
+
actIndex = -2;
|
|
850
|
+
waitError(page, act, error, 'mailLink');
|
|
787
851
|
}
|
|
788
852
|
}
|
|
789
853
|
}
|
|
@@ -1041,24 +1105,36 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1041
1105
|
await matchingElement.focus({timeout: 2000});
|
|
1042
1106
|
act.result = 'focused';
|
|
1043
1107
|
}
|
|
1044
|
-
// Otherwise, if it is clicking a link
|
|
1108
|
+
// Otherwise, if it is clicking a link:
|
|
1045
1109
|
else if (act.type === 'link') {
|
|
1110
|
+
// Try to click it.
|
|
1046
1111
|
const href = await matchingElement.getAttribute('href');
|
|
1047
1112
|
const target = await matchingElement.getAttribute('target');
|
|
1048
|
-
await matchingElement.click({timeout:
|
|
1049
|
-
|
|
1050
|
-
|
|
1113
|
+
await matchingElement.click({timeout: 3000})
|
|
1114
|
+
// If it cannot be clicked within 3 seconds:
|
|
1115
|
+
.catch(async error => {
|
|
1116
|
+
// Try to force-click it without actionability checks.
|
|
1117
|
+
const errorSummary = error.message.replace(/\n.+/, '');
|
|
1118
|
+
console.log(`ERROR: Link to ${href} not clickable (${errorSummary})`);
|
|
1051
1119
|
await matchingElement.click({
|
|
1052
|
-
force: true
|
|
1053
|
-
timeout: 10000
|
|
1120
|
+
force: true
|
|
1054
1121
|
})
|
|
1055
|
-
|
|
1122
|
+
// If it cannot be force-clicked:
|
|
1123
|
+
.catch(error => {
|
|
1124
|
+
// Quit and report the failure.
|
|
1056
1125
|
actIndex = -2;
|
|
1057
|
-
|
|
1058
|
-
|
|
1126
|
+
const errorSummary = error.message.replace(/\n.+/, '');
|
|
1127
|
+
console.log(`ERROR: Link to ${href} not force-clickable (${errorSummary})`);
|
|
1128
|
+
act.result = {
|
|
1129
|
+
href: href || 'NONE',
|
|
1130
|
+
target: target || 'NONE',
|
|
1131
|
+
error: 'ERROR: Normal and forced attempts to click link timed out'
|
|
1132
|
+
};
|
|
1059
1133
|
});
|
|
1060
1134
|
});
|
|
1135
|
+
// If it was clicked:
|
|
1061
1136
|
if (actIndex > -2) {
|
|
1137
|
+
// Report the success.
|
|
1062
1138
|
act.result = {
|
|
1063
1139
|
href: href || 'NONE',
|
|
1064
1140
|
target: target || 'NONE',
|
|
@@ -1076,7 +1152,9 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1076
1152
|
const optionText = await option.textContent();
|
|
1077
1153
|
optionTexts.push(optionText);
|
|
1078
1154
|
}
|
|
1079
|
-
const matchTexts = optionTexts.map(
|
|
1155
|
+
const matchTexts = optionTexts.map(
|
|
1156
|
+
(text, index) => text.includes(act.what) ? index : -1
|
|
1157
|
+
);
|
|
1080
1158
|
const index = matchTexts.filter(text => text > -1)[act.index || 0];
|
|
1081
1159
|
if (index !== undefined) {
|
|
1082
1160
|
await matchingElement.selectOption({index});
|
|
@@ -1172,7 +1250,7 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1172
1250
|
}
|
|
1173
1251
|
// If there is a current element:
|
|
1174
1252
|
if (currentElement) {
|
|
1175
|
-
// If it was already reached within this
|
|
1253
|
+
// If it was already reached within this act:
|
|
1176
1254
|
if (currentElement.dataset.pressesReached === actCount.toString(10)) {
|
|
1177
1255
|
// Report the error.
|
|
1178
1256
|
console.log(`ERROR: ${currentElement.tagName} element reached again`);
|
|
@@ -1306,6 +1384,8 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1306
1384
|
const errorMsg = `ERROR: Invalid command of type ${act.type}`;
|
|
1307
1385
|
act.result = errorMsg;
|
|
1308
1386
|
console.log(errorMsg);
|
|
1387
|
+
// Quit.
|
|
1388
|
+
actIndex = -2;
|
|
1309
1389
|
}
|
|
1310
1390
|
// Perform the remaining acts.
|
|
1311
1391
|
await doActs(report, actIndex + 1, page);
|
package/tests/hover.js
CHANGED
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
value in the same location being the target.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
+
// VARIABLES
|
|
32
|
+
|
|
33
|
+
let hasTimedOut = false;
|
|
34
|
+
|
|
31
35
|
// FUNCTIONS
|
|
32
36
|
|
|
33
37
|
// Samples a population and returns the sample.
|
|
@@ -59,153 +63,172 @@ const textOf = async (element, limit) => {
|
|
|
59
63
|
};
|
|
60
64
|
// Recursively reports impacts of hovering over triggers.
|
|
61
65
|
const find = async (data, withItems, page, region, sample, popRatio) => {
|
|
62
|
-
// If any potential triggers remain:
|
|
63
|
-
if (sample.length) {
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
},
|
|
82
|
-
firstTrigger
|
|
83
|
-
);
|
|
84
|
-
root = rootJSHandle.asElement();
|
|
85
|
-
}
|
|
86
|
-
// Identify all the descendants of the root.
|
|
87
|
-
const preDescendants = await root.$$(':visible');
|
|
88
|
-
// Identify their opacities.
|
|
89
|
-
const preOpacities = await page.evaluate(elements => elements.map(
|
|
90
|
-
element => window.getComputedStyle(element).opacity
|
|
91
|
-
), preDescendants);
|
|
92
|
-
try {
|
|
93
|
-
// Hover over the trigger.
|
|
94
|
-
await firstTrigger.hover({
|
|
95
|
-
timeout: 500,
|
|
96
|
-
noWaitAfter: true
|
|
97
|
-
});
|
|
98
|
-
// Repeatedly seeks impacts.
|
|
99
|
-
const getImpacts = async (interval, triesLeft) => {
|
|
100
|
-
// If the allowed trial count has not yet been exhausted:
|
|
101
|
-
if (triesLeft--) {
|
|
102
|
-
// Get the collection of descendants of the root.
|
|
103
|
-
const postDescendants = await root.$$(':visible');
|
|
104
|
-
// Identify the prior descandants of the root still in existence.
|
|
105
|
-
const remainerIndexes = await page.evaluate(args => {
|
|
106
|
-
const preDescendants = args[0];
|
|
107
|
-
const postDescendants = args[1];
|
|
108
|
-
const remainerIndexes = preDescendants
|
|
109
|
-
.map((element, index) => postDescendants.includes(element) ? index : -1)
|
|
110
|
-
.filter(index => index > -1);
|
|
111
|
-
return remainerIndexes;
|
|
112
|
-
}, [preDescendants, postDescendants]);
|
|
113
|
-
// Get the count of elements added by the hover event.
|
|
114
|
-
const additionCount = postDescendants.length - remainerIndexes.length;
|
|
115
|
-
const removalCount = preDescendants.length - remainerIndexes.length;
|
|
116
|
-
const remainers = [];
|
|
117
|
-
for (const index of remainerIndexes) {
|
|
118
|
-
remainers.push({
|
|
119
|
-
element: preDescendants[index],
|
|
120
|
-
preOpacity: preOpacities[index],
|
|
121
|
-
postOpacity: await page.evaluate(
|
|
122
|
-
element => window.getComputedStyle(element).opacity, preDescendants[index]
|
|
123
|
-
)
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
const opacityChangers = remainers
|
|
127
|
-
.filter(remainer => remainer.postOpacity !== remainer.preOpacity);
|
|
128
|
-
const opacityImpact = opacityChangers ? await page.evaluate(changers => changers.reduce(
|
|
129
|
-
(total, current) => total + current.element.querySelectorAll('*').length, 0
|
|
130
|
-
), opacityChangers) : 0;
|
|
131
|
-
if (additionCount || removalCount || opacityChangers.length) {
|
|
132
|
-
return {
|
|
133
|
-
additionCount,
|
|
134
|
-
removalCount,
|
|
135
|
-
opacityChangers,
|
|
136
|
-
opacityImpact
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
return await new Promise(resolve => {
|
|
141
|
-
setTimeout(() => {
|
|
142
|
-
resolve(getImpacts(interval, triesLeft));
|
|
143
|
-
}, interval);
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
// Repeatedly seek impacts of the hover at intervals.
|
|
152
|
-
const impacts = await getImpacts(300, 4);
|
|
153
|
-
// If there were any:
|
|
154
|
-
if (impacts) {
|
|
155
|
-
// Hover over the upper-left corner of the page, to undo any impacts.
|
|
156
|
-
await page.hover('body', {
|
|
157
|
-
position: {
|
|
158
|
-
x: 0,
|
|
159
|
-
y: 0
|
|
66
|
+
// If any potential triggers remain and the test has not timed out:
|
|
67
|
+
if (sample.length && ! hasTimedOut) {
|
|
68
|
+
// Get and report the impacts until and unless the test times out.
|
|
69
|
+
try {
|
|
70
|
+
// Identify the first of them.
|
|
71
|
+
const firstTrigger = sample[0];
|
|
72
|
+
const tagNameJSHandle = await firstTrigger.getProperty('tagName')
|
|
73
|
+
.catch(error => '');
|
|
74
|
+
if (tagNameJSHandle) {
|
|
75
|
+
const tagName = await tagNameJSHandle.jsonValue();
|
|
76
|
+
// Identify the root of a subtree likely to contain impacted elements.
|
|
77
|
+
let root = firstTrigger;
|
|
78
|
+
if (['A', 'BUTTON', 'LI'].includes(tagName)) {
|
|
79
|
+
const rootJSHandle = await page.evaluateHandle(
|
|
80
|
+
firstTrigger => {
|
|
81
|
+
const parent = firstTrigger.parentElement || firstTrigger;
|
|
82
|
+
const grandparent = parent.parentElement || parent;
|
|
83
|
+
const greatGrandparent = grandparent.parentElement || parent;
|
|
84
|
+
return firstTrigger.tagName === 'LI' ? grandparent : greatGrandparent;
|
|
160
85
|
},
|
|
86
|
+
firstTrigger
|
|
87
|
+
);
|
|
88
|
+
root = rootJSHandle.asElement();
|
|
89
|
+
}
|
|
90
|
+
// Identify all the visible descendants of the root.
|
|
91
|
+
const preDescendants = await root.$$(':visible');
|
|
92
|
+
// Identify their opacities.
|
|
93
|
+
const preOpacities = await page.evaluate(elements => elements.map(
|
|
94
|
+
element => window.getComputedStyle(element).opacity
|
|
95
|
+
), preDescendants);
|
|
96
|
+
try {
|
|
97
|
+
// Hover over the trigger.
|
|
98
|
+
await firstTrigger.hover({
|
|
161
99
|
timeout: 500,
|
|
162
|
-
force: true,
|
|
163
100
|
noWaitAfter: true
|
|
164
101
|
});
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
102
|
+
// FUNCTION DEFINITION START
|
|
103
|
+
// Repeatedly seeks impacts.
|
|
104
|
+
const getImpacts = async (interval, triesLeft) => {
|
|
105
|
+
// If the allowed trial count has not yet been exhausted:
|
|
106
|
+
if (triesLeft-- && ! hasTimedOut) {
|
|
107
|
+
// Get the collection of descendants of the root.
|
|
108
|
+
const postDescendants = await root.$$(':visible');
|
|
109
|
+
// Identify the prior descandants of the root still in existence.
|
|
110
|
+
const remainerIndexes = await page.evaluate(args => {
|
|
111
|
+
const preDescendants = args[0];
|
|
112
|
+
const postDescendants = args[1];
|
|
113
|
+
const remainerIndexes = preDescendants
|
|
114
|
+
.map((element, index) => postDescendants.includes(element) ? index : -1)
|
|
115
|
+
.filter(index => index > -1);
|
|
116
|
+
return remainerIndexes;
|
|
117
|
+
}, [preDescendants, postDescendants]);
|
|
118
|
+
// Get the count of elements added by the hover event.
|
|
119
|
+
const additionCount = postDescendants.length - remainerIndexes.length;
|
|
120
|
+
const removalCount = preDescendants.length - remainerIndexes.length;
|
|
121
|
+
const remainers = [];
|
|
122
|
+
for (const index of remainerIndexes) {
|
|
123
|
+
remainers.push({
|
|
124
|
+
element: preDescendants[index],
|
|
125
|
+
preOpacity: preOpacities[index],
|
|
126
|
+
postOpacity: await page.evaluate(
|
|
127
|
+
element => window.getComputedStyle(element).opacity, preDescendants[index]
|
|
128
|
+
)
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const opacityChangers = remainers
|
|
132
|
+
.filter(remainer => remainer.postOpacity !== remainer.preOpacity);
|
|
133
|
+
const opacityImpact = opacityChangers ? await page.evaluate(changers => changers.reduce(
|
|
134
|
+
(total, current) => total + current.element.querySelectorAll('*').length, 0
|
|
135
|
+
), opacityChangers) : 0;
|
|
136
|
+
if (additionCount || removalCount || opacityChangers.length) {
|
|
137
|
+
return {
|
|
138
|
+
additionCount,
|
|
139
|
+
removalCount,
|
|
140
|
+
opacityChangers,
|
|
141
|
+
opacityImpact
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
return await new Promise(resolve => {
|
|
146
|
+
setTimeout(() => {
|
|
147
|
+
resolve(getImpacts(interval, triesLeft));
|
|
148
|
+
}, interval);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
// FUNCTION DEFINITION END
|
|
157
|
+
// Repeatedly seek impacts of the hover at intervals.
|
|
158
|
+
const impacts = await getImpacts(300, 4);
|
|
159
|
+
// If there were any:
|
|
160
|
+
if (impacts) {
|
|
161
|
+
// Hover over the upper-left corner of the page, to undo any impacts.
|
|
162
|
+
await page.hover('body', {
|
|
163
|
+
position: {
|
|
164
|
+
x: 0,
|
|
165
|
+
y: 0
|
|
166
|
+
},
|
|
167
|
+
timeout: 500,
|
|
168
|
+
force: true,
|
|
169
|
+
noWaitAfter: true
|
|
185
170
|
});
|
|
171
|
+
// Wait for any delayed and/or slowed hover reaction.
|
|
172
|
+
await page.waitForTimeout(200);
|
|
173
|
+
await root.waitForElementState('stable');
|
|
174
|
+
// Increment the counts of triggers and impacts.
|
|
175
|
+
const {additionCount, removalCount, opacityChangers, opacityImpact} = impacts;
|
|
176
|
+
if (hasTimedOut) {
|
|
177
|
+
return Promise.resolve('');
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
data.totals.impactTriggers += popRatio;
|
|
181
|
+
data.totals.additions += popRatio * additionCount;
|
|
182
|
+
data.totals.removals += popRatio * removalCount;
|
|
183
|
+
data.totals.opacityChanges += popRatio * opacityChangers.length;
|
|
184
|
+
data.totals.opacityImpact += popRatio * opacityImpact;
|
|
185
|
+
// If details are to be reported:
|
|
186
|
+
if (withItems) {
|
|
187
|
+
// Report them.
|
|
188
|
+
data.items[region].impactTriggers.push({
|
|
189
|
+
tagName,
|
|
190
|
+
text: await textOf(firstTrigger, 50),
|
|
191
|
+
additions: additionCount,
|
|
192
|
+
removals: removalCount,
|
|
193
|
+
opacityChanges: opacityChangers.length,
|
|
194
|
+
opacityImpact
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
186
198
|
}
|
|
187
199
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (withItems) {
|
|
193
|
-
try {
|
|
194
|
-
const id = await firstTrigger.getAttribute('id');
|
|
195
|
-
data.items[region].unhoverables.push({
|
|
196
|
-
tagName,
|
|
197
|
-
id: id || '',
|
|
198
|
-
text: await textOf(firstTrigger, 50)
|
|
199
|
-
});
|
|
200
|
+
catch (error) {
|
|
201
|
+
console.log(`ERROR hovering (${error.message.replace(/\n.+/s, '')})`);
|
|
202
|
+
if (hasTimedOut) {
|
|
203
|
+
return Promise.resolve('');
|
|
200
204
|
}
|
|
201
|
-
|
|
202
|
-
|
|
205
|
+
else {
|
|
206
|
+
data.totals.unhoverables++;
|
|
207
|
+
if (withItems) {
|
|
208
|
+
try {
|
|
209
|
+
const id = await firstTrigger.getAttribute('id');
|
|
210
|
+
data.items[region].unhoverables.push({
|
|
211
|
+
tagName,
|
|
212
|
+
id: id || '',
|
|
213
|
+
text: await textOf(firstTrigger, 50)
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
catch(error) {
|
|
217
|
+
console.log('ERROR itemizing unhoverable element');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
203
220
|
}
|
|
204
221
|
}
|
|
205
222
|
}
|
|
223
|
+
// Process the remaining potential triggers.
|
|
224
|
+
await find(data, withItems, page, region, sample.slice(1), popRatio);
|
|
206
225
|
}
|
|
207
|
-
|
|
208
|
-
|
|
226
|
+
catch(error) {
|
|
227
|
+
console.log(`ERROR: Test quit when remaining sample size was ${sample.length}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
return Promise.resolve('');
|
|
209
232
|
}
|
|
210
233
|
};
|
|
211
234
|
// Performs the hover test and reports results.
|
|
@@ -213,7 +236,7 @@ exports.reporter = async (
|
|
|
213
236
|
page, headSize = 0, headSampleSize = -1, tailSampleSize = -1, withItems
|
|
214
237
|
) => {
|
|
215
238
|
// Initialize the result.
|
|
216
|
-
|
|
239
|
+
let data = {
|
|
217
240
|
totals: {
|
|
218
241
|
triggers: 0,
|
|
219
242
|
headTriggers: 0,
|
|
@@ -259,17 +282,34 @@ exports.reporter = async (
|
|
|
259
282
|
// Get the head and tail samples.
|
|
260
283
|
const headSample = getSample(headTriggers, headSampleSize);
|
|
261
284
|
const tailSample = tailSampleSize === -1 ? tailTriggers : getSample(tailTriggers, tailSampleSize);
|
|
285
|
+
// Set a time limit to handle pages that slow the operations of this test.
|
|
286
|
+
const timeLimit = Math.round(1.3 * (headSample.length + tailSample.length));
|
|
287
|
+
const timeout = setTimeout(async () => {
|
|
288
|
+
await page.close();
|
|
289
|
+
console.log(
|
|
290
|
+
`ERROR: hover test timed out at ${timeLimit} seconds; page closed`
|
|
291
|
+
);
|
|
292
|
+
hasTimedOut = true;
|
|
293
|
+
data = {
|
|
294
|
+
prevented: true,
|
|
295
|
+
error: 'ERROR: hover test timed out'
|
|
296
|
+
};
|
|
297
|
+
clearTimeout(timeout);
|
|
298
|
+
}, 1000 * timeLimit);
|
|
262
299
|
// Find and document the impacts.
|
|
263
|
-
if (headSample.length) {
|
|
300
|
+
if (headSample.length && ! hasTimedOut) {
|
|
264
301
|
await find(data, withItems, page, 'head', headSample, headTriggerCount / headSample.length);
|
|
265
302
|
}
|
|
266
|
-
if (tailSample.length) {
|
|
303
|
+
if (tailSample.length && ! hasTimedOut) {
|
|
267
304
|
await find(data, withItems, page, 'tail', tailSample, tailTriggerCount / tailSample.length);
|
|
268
305
|
}
|
|
306
|
+
clearTimeout(timeout);
|
|
269
307
|
// Round the reported totals.
|
|
270
|
-
|
|
271
|
-
data.totals
|
|
272
|
-
|
|
308
|
+
if (! hasTimedOut) {
|
|
309
|
+
Object.keys(data.totals).forEach(key => {
|
|
310
|
+
data.totals[key] = Math.round(data.totals[key]);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
273
313
|
// Return the result.
|
|
274
314
|
return {result: data};
|
|
275
315
|
};
|
package/tests/nuVal.js
CHANGED
|
@@ -11,7 +11,7 @@ const https = require('https');
|
|
|
11
11
|
exports.reporter = async page => {
|
|
12
12
|
const pageContent = await page.content();
|
|
13
13
|
// Get the data from a Nu validation.
|
|
14
|
-
const
|
|
14
|
+
const dataPromise = new Promise(resolve => {
|
|
15
15
|
try {
|
|
16
16
|
const request = https.request(
|
|
17
17
|
{
|
|
@@ -40,7 +40,7 @@ exports.reporter = async page => {
|
|
|
40
40
|
return resolve(result);
|
|
41
41
|
}
|
|
42
42
|
catch (error) {
|
|
43
|
-
console.log(`Validation failed (${error.message})`);
|
|
43
|
+
console.log(`ERROR: Validation failed (${error.message})`);
|
|
44
44
|
return resolve({
|
|
45
45
|
prevented: true,
|
|
46
46
|
error: error.message,
|
|
@@ -54,7 +54,7 @@ exports.reporter = async page => {
|
|
|
54
54
|
request.end();
|
|
55
55
|
request.on('error', error => {
|
|
56
56
|
console.log(error.message);
|
|
57
|
-
return
|
|
57
|
+
return resolve({
|
|
58
58
|
prevented: true,
|
|
59
59
|
error: error.message
|
|
60
60
|
});
|
|
@@ -62,11 +62,22 @@ exports.reporter = async page => {
|
|
|
62
62
|
}
|
|
63
63
|
catch(error) {
|
|
64
64
|
console.log(error.message);
|
|
65
|
-
return
|
|
65
|
+
return resolve({
|
|
66
66
|
prevented: true,
|
|
67
67
|
error: error.message
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
70
|
});
|
|
71
|
+
const timeoutPromise = new Promise(resolve => {
|
|
72
|
+
const timeLimit = 12;
|
|
73
|
+
const timeoutID = setTimeout(() => {
|
|
74
|
+
resolve({
|
|
75
|
+
prevented: true,
|
|
76
|
+
error: `ERROR: Validation timed out at ${timeLimit} seconds`
|
|
77
|
+
});
|
|
78
|
+
clearTimeout(timeoutID);
|
|
79
|
+
}, 1000 * timeLimit);
|
|
80
|
+
});
|
|
81
|
+
const data = await Promise.race([dataPromise, timeoutPromise]);
|
|
71
82
|
return {result: data};
|
|
72
83
|
};
|