testaro 5.7.6 → 5.9.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/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.6",
3
+ "version": "5.9.1",
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',
@@ -55,6 +56,7 @@ const tests = {
55
56
  styleDiff: 'style inconsistencies',
56
57
  tabNav: 'keyboard navigation between tab elements',
57
58
  tenon: 'Tenon',
59
+ title: 'page title',
58
60
  titledEl: 'title attributes on inappropriate elements',
59
61
  wave: 'WAVE',
60
62
  zIndex: 'z indexes'
@@ -81,6 +83,7 @@ const errorWords = [
81
83
  'content security policy',
82
84
  'deprecated',
83
85
  'error',
86
+ 'expected',
84
87
  'failed',
85
88
  'invalid',
86
89
  'missing',
@@ -442,7 +445,7 @@ const textOf = async (page, element) => {
442
445
  return null;
443
446
  }
444
447
  };
445
- // Returns an element of a type case-insensitively matching a text.
448
+ // Returns an element of a type case-insensitively including a text.
446
449
  const matchElement = async (page, selector, matchText, index = 0) => {
447
450
  if (matchText) {
448
451
  // If the page still exists:
@@ -454,14 +457,18 @@ const matchElement = async (page, selector, matchText, index = 0) => {
454
457
  if (selections.length) {
455
458
  // If there are enough to make a match possible:
456
459
  if (index < selections.length) {
457
- // Return the specified one, if any.
460
+ // For each element of the specified type:
458
461
  let matchCount = 0;
459
462
  const selectionTexts = [];
460
463
  for (const selection of selections) {
464
+ // Add its text to the list of texts of such elements.
461
465
  const selectionText = await textOf(page, selection);
462
466
  selectionTexts.push(selectionText);
467
+ // If its text includes the specified text:
463
468
  if (selectionText.includes(slimText)) {
469
+ // If the count of such elements with such texts found so far is the specified count:
464
470
  if (matchCount++ === index) {
471
+ // Return it as the matching element.
465
472
  return {
466
473
  success: true,
467
474
  matchingElement: selection
@@ -661,7 +668,8 @@ const isTrue = (object, specs) => {
661
668
  // Adds a wait error result to an act.
662
669
  const waitError = (page, act, error, what) => {
663
670
  console.log(`ERROR waiting for ${what} (${error.message})`);
664
- act.result = {url: page.url()};
671
+ act.result.found = false;
672
+ act.result.url = page.url();
665
673
  act.result.error = `ERROR waiting for ${what}`;
666
674
  return false;
667
675
  };
@@ -754,49 +762,57 @@ const doActs = async (report, actIndex, page) => {
754
762
  // Otherwise, if the act is a wait for text:
755
763
  else if (act.type === 'wait') {
756
764
  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 => {
765
+ console.log(`>> ${what}`);
766
+ const result = act.result = {};
767
+ // Wait for the specified text, and quit if it does not appear.
768
+ if (what === 'url') {
769
+ try {
770
+ await page.waitForURL(which, {timeout: 15000});
771
+ result.found = true;
772
+ result.url = page.url();
773
+ }
774
+ catch(error) {
762
775
  actIndex = -2;
763
776
  waitError(page, act, error, 'URL');
764
- });
777
+ }
765
778
  }
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 => {
779
+ else if (what === 'title') {
780
+ try {
781
+ await page.waitForFunction(
782
+ text => document
783
+ && document.title
784
+ && document.title.toLowerCase().includes(text.toLowerCase()),
785
+ which,
786
+ {
787
+ polling: 1000,
788
+ timeout: 5000
789
+ }
790
+ );
791
+ result.found = true;
792
+ result.title = await page.title();
793
+ }
794
+ catch(error) {
776
795
  actIndex = -2;
777
796
  waitError(page, act, error, 'title');
778
- });
797
+ }
779
798
  }
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 => {
799
+ else if (what === 'body') {
800
+ try {
801
+ await page.waitForFunction(
802
+ text => document
803
+ && document.body
804
+ && document.body.innerText.toLowerCase().includes(text.toLowerCase()),
805
+ which,
806
+ {
807
+ polling: 2000,
808
+ timeout: 10000
809
+ }
810
+ );
811
+ result.found = true;
812
+ }
813
+ catch(error) {
790
814
  actIndex = -2;
791
815
  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
816
  }
801
817
  }
802
818
  }
@@ -1054,24 +1070,36 @@ const doActs = async (report, actIndex, page) => {
1054
1070
  await matchingElement.focus({timeout: 2000});
1055
1071
  act.result = 'focused';
1056
1072
  }
1057
- // Otherwise, if it is clicking a link, perform it.
1073
+ // Otherwise, if it is clicking a link:
1058
1074
  else if (act.type === 'link') {
1075
+ // Try to click it.
1059
1076
  const href = await matchingElement.getAttribute('href');
1060
1077
  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');
1078
+ await matchingElement.click({timeout: 3000})
1079
+ // If it cannot be clicked within 3 seconds:
1080
+ .catch(async error => {
1081
+ // Try to force-click it without actionability checks.
1082
+ const errorSummary = error.message.replace(/\n.+/, '');
1083
+ console.log(`ERROR: Link to ${href} not clickable (${errorSummary})`);
1064
1084
  await matchingElement.click({
1065
- force: true,
1066
- timeout: 10000
1085
+ force: true
1067
1086
  })
1068
- .catch(() => {
1087
+ // If it cannot be force-clicked:
1088
+ .catch(error => {
1089
+ // Quit and report the failure.
1069
1090
  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';
1091
+ const errorSummary = error.message.replace(/\n.+/, '');
1092
+ console.log(`ERROR: Link to ${href} not force-clickable (${errorSummary})`);
1093
+ act.result = {
1094
+ href: href || 'NONE',
1095
+ target: target || 'NONE',
1096
+ error: 'ERROR: Normal and forced attempts to click link timed out'
1097
+ };
1072
1098
  });
1073
1099
  });
1100
+ // If it was clicked:
1074
1101
  if (actIndex > -2) {
1102
+ // Report the success.
1075
1103
  act.result = {
1076
1104
  href: href || 'NONE',
1077
1105
  target: target || 'NONE',
@@ -1089,7 +1117,9 @@ const doActs = async (report, actIndex, page) => {
1089
1117
  const optionText = await option.textContent();
1090
1118
  optionTexts.push(optionText);
1091
1119
  }
1092
- const matchTexts = optionTexts.map((text, index) => text.includes(act.what) ? index : -1);
1120
+ const matchTexts = optionTexts.map(
1121
+ (text, index) => text.includes(act.what) ? index : -1
1122
+ );
1093
1123
  const index = matchTexts.filter(text => text > -1)[act.index || 0];
1094
1124
  if (index !== undefined) {
1095
1125
  await matchingElement.selectOption({index});
@@ -1185,7 +1215,7 @@ const doActs = async (report, actIndex, page) => {
1185
1215
  }
1186
1216
  // If there is a current element:
1187
1217
  if (currentElement) {
1188
- // If it was already reached within this command performance:
1218
+ // If it was already reached within this act:
1189
1219
  if (currentElement.dataset.pressesReached === actCount.toString(10)) {
1190
1220
  // Report the error.
1191
1221
  console.log(`ERROR: ${currentElement.tagName} element reached again`);
@@ -1319,6 +1349,8 @@ const doActs = async (report, actIndex, page) => {
1319
1349
  const errorMsg = `ERROR: Invalid command of type ${act.type}`;
1320
1350
  act.result = errorMsg;
1321
1351
  console.log(errorMsg);
1352
+ // Quit.
1353
+ actIndex = -2;
1322
1354
  }
1323
1355
  // Perform the remaining acts.
1324
1356
  await doActs(report, actIndex + 1, page);
@@ -0,0 +1,130 @@
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 parent = element.parentElement;
50
+ const datum = {
51
+ tagName: element.tagName,
52
+ parentTagName: parent ? parent.tagName : '',
53
+ code: compact(element.outerHTML),
54
+ attributes: [],
55
+ textContent: compact(element.textContent)
56
+ };
57
+ // For each of its attributes:
58
+ for (const attribute of element.attributes) {
59
+ // Add data on the attribute to the element data.
60
+ const {name, value} = attribute;
61
+ datum.attributes.push({
62
+ name,
63
+ value
64
+ });
65
+ // If the element has reference labels:
66
+ if (name === 'aria-labelledby') {
67
+ // Add their texts to the element data.
68
+ const labelerIDs = value.split(/\s+/);
69
+ const labelers = [];
70
+ labelerIDs.forEach(id => {
71
+ const labeler = document.getElementById(id);
72
+ if (labeler) {
73
+ labelers.push(compact(labeler.textContent));
74
+ }
75
+ });
76
+ if (labelers.length) {
77
+ datum.labelers = labelers;
78
+ }
79
+ }
80
+ }
81
+ // If the element has text content:
82
+ const {labels, textContent} = element;
83
+ const compactContent = compact(textContent);
84
+ if (compactContent) {
85
+ // Add it to the element data.
86
+ datum.textContent = compactContent;
87
+ }
88
+ // If the element has labels:
89
+ if (labels && labels.length) {
90
+ // Add their texts to the element data.
91
+ datum.labels = Array.from(labels).map(label => compact(label.textContent));
92
+ }
93
+ // If sibling itemization is required:
94
+ if (detailLevel === 2) {
95
+ // Add the sibling data to the element data.
96
+ datum.siblings = {
97
+ before: [],
98
+ after: []
99
+ };
100
+ let more = element;
101
+ while (more) {
102
+ more = more.previousSibling;
103
+ if (more) {
104
+ const {nodeType, nodeValue} = more;
105
+ if (! (nodeType === 3 && nodeValue === '')) {
106
+ const sibInfo = getSibInfo(more, nodeType, nodeValue);
107
+ datum.siblings.before.unshift(sibInfo);
108
+ }
109
+ }
110
+ }
111
+ more = element;
112
+ while (more) {
113
+ more = more.nextSibling;
114
+ if (more) {
115
+ const {nodeType, textContent} = more;
116
+ if (! (nodeType === 3 && textContent === '')) {
117
+ const sibInfo = getSibInfo(more, nodeType, compact(textContent));
118
+ datum.siblings.after.push(sibInfo);
119
+ }
120
+ }
121
+ }
122
+ }
123
+ data.items.push(datum);
124
+ });
125
+ return data;
126
+ }
127
+ }, detailLevel);
128
+ // Return the result.
129
+ return {result: data};
130
+ };
package/tests/title.js ADDED
@@ -0,0 +1,8 @@
1
+ /*
2
+ title
3
+ This test reports the page title.
4
+ */
5
+ exports.reporter = async page => {
6
+ const title = await page.title();
7
+ return {result: title};
8
+ };