testaro 5.7.5 → 5.9.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/commands.js CHANGED
@@ -150,6 +150,15 @@ exports.commands = {
150
150
  rules: [true, 'array', 'areStrings', 'rule names, or empty if all']
151
151
  }
152
152
  ],
153
+ elements: [
154
+ 'Perform an elements test',
155
+ {
156
+ detailLevel: [true, 'number', '', '0 = counts, 1 = selves, 2 = also sibling nodes'],
157
+ tagName: [false, 'string', '', 'tag name of elements'],
158
+ onlyVisible: [false, 'boolean', '', 'whether to exclude invisible elements'],
159
+ attribute: [false, 'string', 'hasLength', 'required attribute or attribute=value']
160
+ }
161
+ ],
153
162
  embAc: [
154
163
  'Perform an embAc test',
155
164
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "5.7.5",
3
+ "version": "5.9.0",
4
4
  "description": "Automation of accessibility testing",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/run.js CHANGED
@@ -34,6 +34,7 @@ const tests = {
34
34
  bulk: 'count of visible elements',
35
35
  continuum: 'Level Access Continuum, community edition',
36
36
  docType: 'document without a doctype property',
37
+ elements: 'data on specified elements',
37
38
  embAc: 'active elements embedded in links or buttons',
38
39
  focAll: 'focusable and Tab-focused elements',
39
40
  focInd: 'focus indicators',
@@ -81,6 +82,7 @@ const errorWords = [
81
82
  'content security policy',
82
83
  'deprecated',
83
84
  'error',
85
+ 'expected',
84
86
  'failed',
85
87
  'invalid',
86
88
  'missing',
@@ -442,7 +444,7 @@ const textOf = async (page, element) => {
442
444
  return null;
443
445
  }
444
446
  };
445
- // Returns an element of a type case-insensitively matching a text.
447
+ // Returns an element of a type case-insensitively including a text.
446
448
  const matchElement = async (page, selector, matchText, index = 0) => {
447
449
  if (matchText) {
448
450
  // If the page still exists:
@@ -454,14 +456,18 @@ const matchElement = async (page, selector, matchText, index = 0) => {
454
456
  if (selections.length) {
455
457
  // If there are enough to make a match possible:
456
458
  if (index < selections.length) {
457
- // Return the specified one, if any.
459
+ // For each element of the specified type:
458
460
  let matchCount = 0;
459
461
  const selectionTexts = [];
460
462
  for (const selection of selections) {
463
+ // Add its text to the list of texts of such elements.
461
464
  const selectionText = await textOf(page, selection);
462
465
  selectionTexts.push(selectionText);
466
+ // If its text includes the specified text:
463
467
  if (selectionText.includes(slimText)) {
468
+ // If the count of such elements with such texts found so far is the specified count:
464
469
  if (matchCount++ === index) {
470
+ // Return it as the matching element.
465
471
  return {
466
472
  success: true,
467
473
  matchingElement: selection
@@ -661,7 +667,8 @@ const isTrue = (object, specs) => {
661
667
  // Adds a wait error result to an act.
662
668
  const waitError = (page, act, error, what) => {
663
669
  console.log(`ERROR waiting for ${what} (${error.message})`);
664
- act.result = {url: page.url()};
670
+ act.result.found = false;
671
+ act.result.url = page.url();
665
672
  act.result.error = `ERROR waiting for ${what}`;
666
673
  return false;
667
674
  };
@@ -754,49 +761,57 @@ const doActs = async (report, actIndex, page) => {
754
761
  // Otherwise, if the act is a wait for text:
755
762
  else if (act.type === 'wait') {
756
763
  const {what, which} = act;
757
- console.log(`>> for ${what} to include “${which}”`);
758
- // Wait 5 or 10 seconds for the specified text, and quit if it does not appear.
759
- if (act.what === 'url') {
760
- await page.waitForURL(act.which, {timeout: 15000})
761
- .catch(error => {
764
+ console.log(`>> ${what}`);
765
+ const result = act.result = {};
766
+ // Wait for the specified text, and quit if it does not appear.
767
+ if (what === 'url') {
768
+ try {
769
+ await page.waitForURL(which, {timeout: 15000});
770
+ result.found = true;
771
+ result.url = page.url();
772
+ }
773
+ catch(error) {
762
774
  actIndex = -2;
763
775
  waitError(page, act, error, 'URL');
764
- });
776
+ }
765
777
  }
766
- else if (act.what === 'title') {
767
- await page.waitForFunction(
768
- text => document && document.title && document.title.includes(text),
769
- act.which,
770
- {
771
- polling: 1000,
772
- timeout: 5000
773
- }
774
- )
775
- .catch(error => {
778
+ else if (what === 'title') {
779
+ try {
780
+ await page.waitForFunction(
781
+ text => document
782
+ && document.title
783
+ && document.title.toLowerCase().includes(text.toLowerCase()),
784
+ which,
785
+ {
786
+ polling: 1000,
787
+ timeout: 5000
788
+ }
789
+ );
790
+ result.found = true;
791
+ result.title = await page.title();
792
+ }
793
+ catch(error) {
776
794
  actIndex = -2;
777
795
  waitError(page, act, error, 'title');
778
- });
796
+ }
779
797
  }
780
- else if (act.what === 'body') {
781
- await page.waitForFunction(
782
- text => document && document.body && document.body.innerText.includes(text),
783
- act.which,
784
- {
785
- polling: 2000,
786
- timeout: 10000
787
- }
788
- )
789
- .catch(async error => {
798
+ else if (what === 'body') {
799
+ try {
800
+ await page.waitForFunction(
801
+ text => document
802
+ && document.body
803
+ && document.body.innerText.toLowerCase().includes(text.toLowerCase()),
804
+ which,
805
+ {
806
+ polling: 2000,
807
+ timeout: 10000
808
+ }
809
+ );
810
+ result.found = true;
811
+ }
812
+ catch(error) {
790
813
  actIndex = -2;
791
814
  waitError(page, act, error, 'body');
792
- });
793
- }
794
- // If the text was found:
795
- if (actIndex > -2) {
796
- // Add this to the report.
797
- act.result = {url: page.url()};
798
- if (act.what === 'title') {
799
- act.result.title = await page.title();
800
815
  }
801
816
  }
802
817
  }
@@ -1054,24 +1069,36 @@ const doActs = async (report, actIndex, page) => {
1054
1069
  await matchingElement.focus({timeout: 2000});
1055
1070
  act.result = 'focused';
1056
1071
  }
1057
- // Otherwise, if it is clicking a link, perform it.
1072
+ // Otherwise, if it is clicking a link:
1058
1073
  else if (act.type === 'link') {
1074
+ // Try to click it.
1059
1075
  const href = await matchingElement.getAttribute('href');
1060
1076
  const target = await matchingElement.getAttribute('target');
1061
- await matchingElement.click({timeout: 2000})
1062
- .catch(async () => {
1063
- console.log('ERROR: First attempt to click link timed out');
1077
+ await matchingElement.click({timeout: 3000})
1078
+ // If it cannot be clicked within 3 seconds:
1079
+ .catch(async error => {
1080
+ // Try to force-click it without actionability checks.
1081
+ const errorSummary = error.message.replace(/\n.+/, '');
1082
+ console.log(`ERROR: Link to ${href} not clickable (${errorSummary})`);
1064
1083
  await matchingElement.click({
1065
- force: true,
1066
- timeout: 10000
1084
+ force: true
1067
1085
  })
1068
- .catch(() => {
1086
+ // If it cannot be force-clicked:
1087
+ .catch(error => {
1088
+ // Quit and report the failure.
1069
1089
  actIndex = -2;
1070
- console.log('ERROR: Second (forced) attempt to click link timed out');
1071
- act.result = 'ERROR: Normal and forced click attempts timed out';
1090
+ const errorSummary = error.message.replace(/\n.+/, '');
1091
+ console.log(`ERROR: Link to ${href} not force-clickable (${errorSummary})`);
1092
+ act.result = {
1093
+ href: href || 'NONE',
1094
+ target: target || 'NONE',
1095
+ error: 'ERROR: Normal and forced attempts to click link timed out'
1096
+ };
1072
1097
  });
1073
1098
  });
1099
+ // If it was clicked:
1074
1100
  if (actIndex > -2) {
1101
+ // Report the success.
1075
1102
  act.result = {
1076
1103
  href: href || 'NONE',
1077
1104
  target: target || 'NONE',
@@ -1089,7 +1116,9 @@ const doActs = async (report, actIndex, page) => {
1089
1116
  const optionText = await option.textContent();
1090
1117
  optionTexts.push(optionText);
1091
1118
  }
1092
- const matchTexts = optionTexts.map((text, index) => text.includes(act.what) ? index : -1);
1119
+ const matchTexts = optionTexts.map(
1120
+ (text, index) => text.includes(act.what) ? index : -1
1121
+ );
1093
1122
  const index = matchTexts.filter(text => text > -1)[act.index || 0];
1094
1123
  if (index !== undefined) {
1095
1124
  await matchingElement.selectOption({index});
@@ -1185,7 +1214,7 @@ const doActs = async (report, actIndex, page) => {
1185
1214
  }
1186
1215
  // If there is a current element:
1187
1216
  if (currentElement) {
1188
- // If it was already reached within this command performance:
1217
+ // If it was already reached within this act:
1189
1218
  if (currentElement.dataset.pressesReached === actCount.toString(10)) {
1190
1219
  // Report the error.
1191
1220
  console.log(`ERROR: ${currentElement.tagName} element reached again`);
@@ -1319,6 +1348,8 @@ const doActs = async (report, actIndex, page) => {
1319
1348
  const errorMsg = `ERROR: Invalid command of type ${act.type}`;
1320
1349
  act.result = errorMsg;
1321
1350
  console.log(errorMsg);
1351
+ // Quit.
1352
+ actIndex = -2;
1322
1353
  }
1323
1354
  // Perform the remaining acts.
1324
1355
  await doActs(report, actIndex + 1, page);
@@ -0,0 +1,128 @@
1
+ /*
2
+ elements
3
+ This test reports data about specified elements.
4
+ */
5
+ exports.reporter = async (page, detailLevel, tagName, onlyVisible, attribute) => {
6
+ // Determine a selector of the specified elements.
7
+ let selector = tagName || '*';
8
+ if (attribute) {
9
+ selector += `[${attribute}]`;
10
+ }
11
+ if (onlyVisible) {
12
+ selector += ':visible';
13
+ }
14
+ // Get the data on the elements.
15
+ const data = await page.$$eval(selector, (elements, detailLevel) => {
16
+ // FUNCTION DEFINITIONS START
17
+ // Compacts a string.
18
+ const compact = string => string.replace(/\s+/g, ' ').trim();
19
+ // Gets data on the sibling nodes of an element.
20
+ const getSibInfo = (node, nodeType, text) => {
21
+ const sibInfo = {
22
+ type: nodeType
23
+ };
24
+ if (nodeType === 1) {
25
+ sibInfo.tagName = node.tagName;
26
+ }
27
+ else if (nodeType === 3) {
28
+ sibInfo.text = compact(text);
29
+ }
30
+ return sibInfo;
31
+ };
32
+ // FUNCTION DEFINITIONS END
33
+ // Initialize the data with the count of the specified elements.
34
+ const data = {
35
+ total: elements.length
36
+ };
37
+ // If no itemization is required:
38
+ if (detailLevel === 0) {
39
+ // Return the element count.
40
+ return data;
41
+ }
42
+ // Otherwise, i.e. if itemization is required:
43
+ else {
44
+ // Initialize the item data.
45
+ data.items = [];
46
+ // For each specified element:
47
+ elements.forEach(element => {
48
+ // Initialize data on it.
49
+ const datum = {
50
+ tagName: element.tagName,
51
+ code: compact(element.outerHTML),
52
+ attributes: [],
53
+ textContent: compact(element.textContent)
54
+ };
55
+ // For each of its attributes:
56
+ for (const attribute of element.attributes) {
57
+ // Add data on the attribute to the element data.
58
+ const {name, value} = attribute;
59
+ datum.attributes.push({
60
+ name,
61
+ value
62
+ });
63
+ // If the element has reference labels:
64
+ if (name === 'aria-labelledby') {
65
+ // Add their texts to the element data.
66
+ const labelerIDs = value.split(/\s+/);
67
+ const labelers = [];
68
+ labelerIDs.forEach(id => {
69
+ const labeler = document.getElementById(id);
70
+ if (labeler) {
71
+ labelers.push(compact(labeler.textContent));
72
+ }
73
+ });
74
+ if (labelers.length) {
75
+ datum.labelers = labelers;
76
+ }
77
+ }
78
+ }
79
+ // If the element has text content:
80
+ const {labels, textContent} = element;
81
+ const compactContent = compact(textContent);
82
+ if (compactContent) {
83
+ // Add it to the element data.
84
+ datum.textContent = compactContent;
85
+ }
86
+ // If the element has labels:
87
+ if (labels && labels.length) {
88
+ // Add their texts to the element data.
89
+ datum.labels = Array.from(labels).map(label => compact(label.textContent));
90
+ }
91
+ // If sibling itemization is required:
92
+ if (detailLevel === 2) {
93
+ // Add the sibling data to the element data.
94
+ datum.siblings = {
95
+ before: [],
96
+ after: []
97
+ };
98
+ let more = element;
99
+ while (more) {
100
+ more = more.previousSibling;
101
+ if (more) {
102
+ const {nodeType, nodeValue} = more;
103
+ if (! (nodeType === 3 && nodeValue === '')) {
104
+ const sibInfo = getSibInfo(more, nodeType, nodeValue);
105
+ datum.siblings.before.unshift(sibInfo);
106
+ }
107
+ }
108
+ }
109
+ more = element;
110
+ while (more) {
111
+ more = more.nextSibling;
112
+ if (more) {
113
+ const {nodeType, textContent} = more;
114
+ if (! (nodeType === 3 && textContent === '')) {
115
+ const sibInfo = getSibInfo(more, nodeType, compact(textContent));
116
+ datum.siblings.after.push(sibInfo);
117
+ }
118
+ }
119
+ }
120
+ }
121
+ data.items.push(datum);
122
+ });
123
+ return data;
124
+ }
125
+ }, detailLevel);
126
+ // Return the result.
127
+ return {result: data};
128
+ };
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
  };