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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "5.7.4",
3
+ "version": "5.8.0",
4
4
  "description": "Automation of accessibility testing",
5
5
  "main": "index.js",
6
6
  "scripts": {
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
- // Launch a browser of the specified type.
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
- const browser = await browserType.launch(browserOptions)
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 matching a text.
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
- // Return the specified one, if any.
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 = {url: page.url()};
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(`>> for ${what} to include “${which}”`);
745
- // Wait 5 or 10 seconds for the specified text, and quit if it does not appear.
746
- if (act.what === 'url') {
747
- await page.waitForURL(act.which, {timeout: 15000})
748
- .catch(error => {
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 (act.what === 'title') {
754
- await page.waitForFunction(
755
- text => document && document.title && document.title.includes(text),
756
- act.which,
757
- {
758
- polling: 1000,
759
- timeout: 5000
760
- }
761
- )
762
- .catch(error => {
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 (act.what === 'body') {
768
- await page.waitForFunction(
769
- text => document && document.body && document.body.innerText.includes(text),
770
- act.which,
771
- {
772
- polling: 2000,
773
- timeout: 10000
774
- }
775
- )
776
- .catch(async error => {
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
- // If the text was found:
782
- if (actIndex > -2) {
783
- // Add this to the report.
784
- act.result = {url: page.url()};
785
- if (act.what === 'title') {
786
- act.result.title = await page.title();
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, perform it.
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: 2000})
1049
- .catch(async () => {
1050
- console.log('ERROR: First attempt to click link timed out');
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
- .catch(() => {
1122
+ // If it cannot be force-clicked:
1123
+ .catch(error => {
1124
+ // Quit and report the failure.
1056
1125
  actIndex = -2;
1057
- console.log('ERROR: Second (forced) attempt to click link timed out');
1058
- act.result = 'ERROR: Normal and forced click attempts timed out';
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((text, index) => text.includes(act.what) ? index : -1);
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 command performance:
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
- // Identify the first of them.
65
- const firstTrigger = sample[0];
66
- const tagNameJSHandle = await firstTrigger.getProperty('tagName')
67
- .catch(error => {
68
- return '';
69
- });
70
- if (tagNameJSHandle) {
71
- const tagName = await tagNameJSHandle.jsonValue();
72
- // Identify the root of a subtree likely to contain impacted elements.
73
- let root = firstTrigger;
74
- if (['A', 'BUTTON', 'LI'].includes(tagName)) {
75
- const rootJSHandle = await page.evaluateHandle(
76
- firstTrigger => {
77
- const parent = firstTrigger.parentElement || firstTrigger;
78
- const grandparent = parent.parentElement || parent;
79
- const greatGrandparent = grandparent.parentElement || parent;
80
- return firstTrigger.tagName === 'LI' ? grandparent : greatGrandparent;
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
- // Wait for any delayed and/or slowed hover reaction.
166
- await page.waitForTimeout(200);
167
- await root.waitForElementState('stable');
168
- // Increment the counts of triggers and impacts.
169
- const {additionCount, removalCount, opacityChangers, opacityImpact} = impacts;
170
- data.totals.impactTriggers += popRatio;
171
- data.totals.additions += popRatio * additionCount;
172
- data.totals.removals += popRatio * removalCount;
173
- data.totals.opacityChanges += popRatio * opacityChangers.length;
174
- data.totals.opacityImpact += popRatio * opacityImpact;
175
- // If details are to be reported:
176
- if (withItems) {
177
- // Report them.
178
- data.items[region].impactTriggers.push({
179
- tagName,
180
- text: await textOf(firstTrigger, 50),
181
- additions: additionCount,
182
- removals: removalCount,
183
- opacityChanges: opacityChangers.length,
184
- opacityImpact
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
- catch (error) {
190
- console.log(`ERROR hovering (${error.message.replace(/\n.+/s, '')})`);
191
- data.totals.unhoverables++;
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
- catch(error) {
202
- console.log('ERROR itemizing unhoverable element');
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
- // Process the remaining potential triggers.
208
- await find(data, withItems, page, region, sample.slice(1), popRatio);
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
- const data = {
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
- Object.keys(data.totals).forEach(key => {
271
- data.totals[key] = Math.round(data.totals[key]);
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 data = await new Promise((resolve, reject) => {
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 reject({
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 reject({
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
  };