testaro 5.11.4 → 5.12.2

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
@@ -3,7 +3,7 @@ exports.commands = {
3
3
  button: [
4
4
  'Click a button or submit input',
5
5
  {
6
- which: [true, 'string', 'hasLength', 'substring of button text'],
6
+ which: [false, 'string', 'hasLength', 'substring of button text'],
7
7
  index: [false, 'number', '', 'index among matches if not 0'],
8
8
  what: [false, 'string', 'hasLength', 'comment']
9
9
  }
@@ -21,7 +21,7 @@ exports.commands = {
21
21
  {
22
22
  what: [true, 'string', 'isFocusable', 'selector of element to be focused'],
23
23
  index: [false, 'number', '', 'index among matches if not 0'],
24
- which: [true, 'string', 'hasLength', 'substring of element text']
24
+ which: [false, 'string', 'hasLength', 'substring of element text']
25
25
  }
26
26
  ],
27
27
  launch: [
@@ -34,7 +34,7 @@ exports.commands = {
34
34
  link: [
35
35
  'Click a link and wait for the page to be idle or loaded',
36
36
  {
37
- which: [true, 'string', 'hasLength', 'substring of link text'],
37
+ which: [false, 'string', 'hasLength', 'substring of link text'],
38
38
  index: [false, 'number', '', 'index among matches if not 0'],
39
39
  what: [false, 'string', 'hasLength', 'comment']
40
40
  }
@@ -89,10 +89,18 @@ exports.commands = {
89
89
  what: [false, 'string', 'hasLength', 'comment']
90
90
  }
91
91
  ],
92
+ search: [
93
+ 'Enter text into a search input, optionally with 1 placeholder for an all-caps literal environment variable',
94
+ {
95
+ which: [false, 'string', 'hasLength', 'substring of input text'],
96
+ index: [false, 'number', '', 'index among matches if not 0'],
97
+ what: [true, 'string', 'hasLength', 'text to enter, with optional __PLACEHOLDER__']
98
+ }
99
+ ],
92
100
  select: [
93
101
  'Select a select option',
94
102
  {
95
- which: [true, 'string', 'hasLength', 'substring of select-list text'],
103
+ which: [false, 'string', 'hasLength', 'substring of select-list text'],
96
104
  index: [false, 'number', '', 'index among matches if not 0'],
97
105
  what: [true, 'string', 'hasLength', 'substring of option text content']
98
106
  }
@@ -122,7 +130,7 @@ exports.commands = {
122
130
  text: [
123
131
  'Enter text into a text input, optionally with 1 placeholder for an all-caps literal environment variable',
124
132
  {
125
- which: [true, 'string', 'hasLength', 'substring of input text'],
133
+ which: [false, 'string', 'hasLength', 'substring of input text'],
126
134
  index: [false, 'number', '', 'index among matches if not 0'],
127
135
  what: [true, 'string', 'hasLength', 'text to enter, with optional __PLACEHOLDER__']
128
136
  }
@@ -154,9 +162,9 @@ exports.commands = {
154
162
  'Perform an elements test',
155
163
  {
156
164
  detailLevel: [true, 'number', '', '0 to 3, to specify the level of detail'],
157
- tagName: [false, 'string', '', 'tag name of elements'],
165
+ tagName: [false, 'string', 'hasLength', 'tag name of elements'],
158
166
  onlyVisible: [false, 'boolean', '', 'whether to exclude invisible elements'],
159
- attribute: [false, 'string', 'hasLength', 'required attribute or attribute=value']
167
+ attribute: [false, 'string', 'hasLength', 'required attribute selector']
160
168
  }
161
169
  ],
162
170
  embAc: [
@@ -273,6 +281,13 @@ exports.commands = {
273
281
  id: [true, 'string', 'hasLength', 'ID of the requested test instance']
274
282
  }
275
283
  ],
284
+ textNodes: [
285
+ 'Perform a textNodes test',
286
+ {
287
+ detailLevel: [true, 'number', '', '0 to 3, to specify the level of detail'],
288
+ text: [false, 'string', 'hasLength', 'case-insensitive substring of the text node']
289
+ }
290
+ ],
276
291
  titledEl: [
277
292
  'Perform a titledEl test',
278
293
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "5.11.4",
3
+ "version": "5.12.2",
4
4
  "description": "Automation of accessibility testing",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/run.js CHANGED
@@ -24,6 +24,7 @@ const moves = {
24
24
  focus: true,
25
25
  link: 'a, [role=link]',
26
26
  radio: 'input[type=radio]',
27
+ search: 'input[type=search]',
27
28
  select: 'select',
28
29
  text: 'input[type=text]'
29
30
  };
@@ -56,6 +57,7 @@ const tests = {
56
57
  styleDiff: 'style inconsistencies',
57
58
  tabNav: 'keyboard navigation between tab elements',
58
59
  tenon: 'Tenon',
60
+ textNodes: 'data on specified text nodes',
59
61
  title: 'page title',
60
62
  titledEl: 'title attributes on inappropriate elements',
61
63
  wave: 'WAVE',
@@ -685,7 +687,7 @@ const doActs = async (report, actIndex, page) => {
685
687
  }
686
688
  catch(error) {
687
689
  actIndex = -2;
688
- waitError(page, act, error, 'URL');
690
+ waitError(page, act, error, 'text in the URL');
689
691
  }
690
692
  }
691
693
  // Otherwise, if the text is to be a substring of the page title:
@@ -707,7 +709,7 @@ const doActs = async (report, actIndex, page) => {
707
709
  }
708
710
  catch(error) {
709
711
  actIndex = -2;
710
- waitError(page, act, error, 'title');
712
+ waitError(page, act, error, 'text in the title');
711
713
  }
712
714
  }
713
715
  // Otherwise, if the text is to be a substring of the text of the page body:
@@ -721,14 +723,14 @@ const doActs = async (report, actIndex, page) => {
721
723
  which,
722
724
  {
723
725
  polling: 2000,
724
- timeout: 10000
726
+ timeout: 15000
725
727
  }
726
728
  );
727
729
  result.found = true;
728
730
  }
729
731
  catch(error) {
730
732
  actIndex = -2;
731
- waitError(page, act, error, 'body');
733
+ waitError(page, act, error, 'text in the body');
732
734
  }
733
735
  }
734
736
  }
@@ -948,77 +950,71 @@ const doActs = async (report, actIndex, page) => {
948
950
  act.result = {found: false};
949
951
  let selection = {};
950
952
  let tries = 0;
951
- const slimText = debloat(act.which);
953
+ const slimText = act.which ? debloat(act.which) : '';
952
954
  while (tries++ < 5 && ! act.result.found) {
953
- if (act.which) {
954
- // If the page still exists:
955
- if (page) {
956
- // Identify the elements of the specified type.
957
- const selections = await page.$$(selector);
958
- // If there are any:
959
- if (selections.length) {
960
- // If there are enough to make a match possible:
961
- if ((act.index || 0) < selections.length) {
962
- // For each element of the specified type:
963
- let matchCount = 0;
964
- const selectionTexts = [];
965
- for (selection of selections) {
966
- // Add its text to the list of texts of such elements.
967
- const selectionText = await textOf(page, selection);
968
- selectionTexts.push(selectionText);
969
- // If its text includes the specified text:
970
- if (selectionText.includes(slimText)) {
971
- // If the element has the specified index among such elements:
972
- if (matchCount++ === (act.index || 0)) {
973
- // Report it as the matching element and stop checking.
974
- act.result.found = true;
975
- act.result.text = slimText;
976
- break;
977
- }
955
+ // If the page still exists:
956
+ if (page) {
957
+ // Identify the elements of the specified type.
958
+ const selections = await page.$$(selector);
959
+ // If there are any:
960
+ if (selections.length) {
961
+ // If there are enough to make a match possible:
962
+ if ((act.index || 0) < selections.length) {
963
+ // For each element of the specified type:
964
+ let matchCount = 0;
965
+ const selectionTexts = [];
966
+ for (selection of selections) {
967
+ // Add its text or an empty string to the list of texts of such elements.
968
+ const selectionText = slimText ? await textOf(page, selection) : '';
969
+ selectionTexts.push(selectionText);
970
+ // If its text includes any specified text:
971
+ if (selectionText.includes(slimText)) {
972
+ // If the element has the specified index among such elements:
973
+ if (matchCount++ === (act.index || 0)) {
974
+ // Report it as the matching element and stop checking.
975
+ act.result.found = true;
976
+ act.result.text = slimText;
977
+ break;
978
978
  }
979
979
  }
980
- // If no element satisfied the specifications:
981
- if (! act.result.found) {
982
- act.result.success = false;
983
- act.result.error = 'exhausted';
984
- act.result.typeElementCount = selections.length;
985
- act.result.textElementCount = --matchCount;
986
- act.result.message = 'Not enough elements have the specified text';
987
- act.result.candidateTexts = selectionTexts;
988
- }
989
980
  }
990
- // Otherwise, i.e. if there are too few such elements to make a match possible:
991
- else {
992
- // Return a failure.
981
+ // If no element satisfied the specifications:
982
+ if (! act.result.found) {
983
+ // Add the failure data to the report.
993
984
  act.result.success = false;
994
- act.result.error = 'fewer';
985
+ act.result.error = 'exhausted';
995
986
  act.result.typeElementCount = selections.length;
996
- act.result.message = 'Elements of specified type too few';
987
+ if (slimText) {
988
+ act.result.textElementCount = --matchCount;
989
+ }
990
+ act.result.message = 'Not enough specified elements exist';
991
+ act.result.candidateTexts = selectionTexts;
997
992
  }
998
993
  }
999
- // Otherwise, i.e. if there are no elements of the specified type:
994
+ // Otherwise, i.e. if there are too few such elements to make a match possible:
1000
995
  else {
1001
- // Return a failure.
996
+ // Add the failure data to the report.
1002
997
  act.result.success = false;
1003
- act.result.error = 'none';
1004
- act.result.typeElementCount = 0;
1005
- act.result.message = 'No elements specified type found';
998
+ act.result.error = 'fewer';
999
+ act.result.typeElementCount = selections.length;
1000
+ act.result.message = 'Elements of specified type too few';
1006
1001
  }
1007
1002
  }
1008
- // Otherwise, i.e. if the page no longer exists:
1003
+ // Otherwise, i.e. if there are no elements of the specified type:
1009
1004
  else {
1010
- // Return a failure.
1005
+ // Add the failure data to the report.
1011
1006
  act.result.success = false;
1012
- act.result.error = 'gone';
1013
- act.result.message = 'Page gone';
1007
+ act.result.error = 'none';
1008
+ act.result.typeElementCount = 0;
1009
+ act.result.message = 'No elements of specified type found';
1014
1010
  }
1015
1011
  }
1016
- // Otherwise, i.e. if no text was specified:
1012
+ // Otherwise, i.e. if the page no longer exists:
1017
1013
  else {
1018
- // Return a failure.
1014
+ // Add the failure data to the report.
1019
1015
  act.result.success = false;
1020
- act.result.error = 'text';
1021
- act.result.message = 'No text specified';
1016
+ act.result.error = 'gone';
1017
+ act.result.message = 'Page gone';
1022
1018
  }
1023
1019
  if (! act.result.found) {
1024
1020
  await wait(2000);
@@ -1026,6 +1022,36 @@ const doActs = async (report, actIndex, page) => {
1026
1022
  }
1027
1023
  // If a match was found:
1028
1024
  if (act.result.found) {
1025
+ // FUNCTION DEFINITION START
1026
+ // Perform a click or Enter keypress and wait for a page load.
1027
+ const doAndWait = async actionIsClick => {
1028
+ try {
1029
+ const [newPage] = await Promise.all([
1030
+ page.context().waitForEvent('page', {timeout: 15000}),
1031
+ actionIsClick ? selection.click({timeout: 4000}) : selection.press('Enter')
1032
+ ]);
1033
+ // Wait for the new page to load.
1034
+ await newPage.waitForLoadState('domcontentloaded', {timeout: 15000});
1035
+ // Make the new page the current page.
1036
+ page = newPage;
1037
+ act.result.success = true;
1038
+ act.result.move = actionIsClick ? 'clicked' : 'Enter pressed';
1039
+ act.result.newURL = page.url();
1040
+ }
1041
+ // If the action, event, or load failed:
1042
+ catch(error) {
1043
+ // Quit and report the failure.
1044
+ const action = actionIsClick ? 'clicking' : 'pressing Enter';
1045
+ console.log(
1046
+ `ERROR ${action} (${error.message.replace(/\n.+/s, '')})`
1047
+ );
1048
+ act.result.success = false;
1049
+ act.result.error = 'moveFailure';
1050
+ act.result.message = 'ERROR: move, navigation, or load timed out';
1051
+ actIndex = -2;
1052
+ }
1053
+ };
1054
+ // FUNCTION DEFINITION END
1029
1055
  // If the move is a button click, perform it.
1030
1056
  if (act.type === 'button') {
1031
1057
  await selection.click({timeout: 3000});
@@ -1080,30 +1106,7 @@ const doActs = async (report, actIndex, page) => {
1080
1106
  // If the destination is a new page:
1081
1107
  if (target && target !== '_self') {
1082
1108
  // Click the link and wait for the resulting page event.
1083
- try {
1084
- const [newPage] = await Promise.all([
1085
- page.context().waitForEvent('page', {timeout: 7000}),
1086
- selection.click({timeout: 4000})
1087
- ]);
1088
- // Wait for the new page to load.
1089
- await newPage.waitForLoadState('domcontentloaded', {timeout: 10000});
1090
- // Make the new page the current page.
1091
- page = newPage;
1092
- act.result.success = true;
1093
- act.result.move = 'clicked';
1094
- act.result.newURL = page.url();
1095
- }
1096
- // If the click, event, or load failed:
1097
- catch(error) {
1098
- // Quit and report the failure.
1099
- console.log(
1100
- `ERROR clicking new-page link (${error.message.replace(/\n.+/s, '')})`
1101
- );
1102
- act.result.success = false;
1103
- act.result.error = 'unclickable';
1104
- act.result.message = 'ERROR: click, navigation, or load timed out';
1105
- actIndex = -2;
1106
- }
1109
+ doAndWait(true);
1107
1110
  }
1108
1111
  // Otherwise, i.e. if the destination is in the current page:
1109
1112
  else {
@@ -1157,8 +1160,8 @@ const doActs = async (report, actIndex, page) => {
1157
1160
  act.result.move = 'selected';
1158
1161
  act.result.option = optionText;
1159
1162
  }
1160
- // Otherwise, if it is entering text on the element:
1161
- else if (act.type === 'text') {
1163
+ // Otherwise, if it is entering text on a text- or search-input element:
1164
+ else if (['text', 'search'].includes(act.type)) {
1162
1165
  // If the text contains a placeholder for an environment variable:
1163
1166
  let {what} = act;
1164
1167
  if (/__[A-Z]+__/.test(what)) {
@@ -1172,6 +1175,11 @@ const doActs = async (report, actIndex, page) => {
1172
1175
  report.presses += act.what.length;
1173
1176
  act.result.success = true;
1174
1177
  act.result.move = 'entered';
1178
+ // If the input is a search input:
1179
+ if (act.type === 'search') {
1180
+ // Press the Enter key and wait for a new page to load.
1181
+ doAndWait(false);
1182
+ }
1175
1183
  }
1176
1184
  // Otherwise, i.e. if the move is unknown, add the failure to the act.
1177
1185
  else {
package/tests/elements.js CHANGED
@@ -23,13 +23,20 @@ exports.reporter = async (page, detailLevel, tagName, onlyVisible, attribute) =>
23
23
  // FUNCTION DEFINITIONS START
24
24
  // Compacts a string.
25
25
  const compact = string => string.replace(/\s+/g, ' ').trim();
26
- // Gets data on the sibling nodes of an element.
26
+ // Gets data on a sibling node of an element.
27
27
  const getSibInfo = (node, nodeType, text) => {
28
28
  const sibInfo = {
29
29
  type: nodeType
30
30
  };
31
31
  if (nodeType === 1) {
32
32
  sibInfo.tagName = node.tagName;
33
+ sibInfo.attributes = [];
34
+ node.attributes.forEach(attribute => {
35
+ sibInfo.attributes.push({
36
+ name: attribute.name,
37
+ value: attribute.value
38
+ });
39
+ });
33
40
  }
34
41
  else if (nodeType === 3) {
35
42
  sibInfo.text = compact(text);
@@ -0,0 +1,142 @@
1
+ /*
2
+ textNodes
3
+ This test reports data about specified text nodes.
4
+ Meanings of detailLevel values:
5
+ 0. Only total node count; no detail.
6
+ 1+. Count of ancestry levels to provide data on (1 = text node, 2 = also parent, etc.)
7
+ */
8
+ exports.reporter = async (page, detailLevel, text) => {
9
+ let data = {};
10
+ // Get the data on the text nodes.
11
+ try {
12
+ data = await page.evaluate(args => {
13
+ const detailLevel = args[0];
14
+ const text = args[1];
15
+ const matchNodes = [];
16
+ // Normalize the body.
17
+ document.body.normalize();
18
+ // Make a copy of the body.
19
+ const tempBody = document.body.cloneNode(true);
20
+ // Insert it into the document.
21
+ document.body.appendChild(tempBody);
22
+ // Remove the irrelevant text content from the copy.
23
+ const extraElements = Array.from(tempBody.querySelectorAll('style, script, svg'));
24
+ extraElements.forEach(element => {
25
+ element.textContent = '';
26
+ });
27
+ // FUNCTION DEFINITIONS START
28
+ // Compacts a string.
29
+ const compact = string => string.replace(/\s+/g, ' ').trim();
30
+ // Compacts and lower-cases a string.
31
+ const normalize = string => compact(string).toLowerCase();
32
+ // Gets data on an element.
33
+ const getElementData = (element, withText) => {
34
+ // Initialize the data.
35
+ const data = {
36
+ tagName: element.tagName
37
+ };
38
+ if (! ['STYLE', 'SCRIPT', 'SVG', 'svg'].includes(element.tagName)) {
39
+ if (withText) {
40
+ data.text = compact(element.textContent);
41
+ }
42
+ // Add data on its attributes, if any, to the data.
43
+ const {attributes} = element;
44
+ if (attributes) {
45
+ data.attributes = [];
46
+ for (const attribute of attributes) {
47
+ const {name, value} = attribute;
48
+ data.attributes.push({
49
+ name,
50
+ value
51
+ });
52
+ // If any attribute is a labeler reference:
53
+ if (name === 'aria-labelledby') {
54
+ // Add the label texts to the data.
55
+ const labelerIDs = value.split(/\s+/);
56
+ data.refLabels = [];
57
+ labelerIDs.forEach(id => {
58
+ const labeler = document.getElementById(id);
59
+ if (labeler) {
60
+ data.refLabels.push(compact(labeler.textContent));
61
+ }
62
+ });
63
+ }
64
+ }
65
+ }
66
+ // Add data on its labels, if any, to the data.
67
+ const {labels} = element;
68
+ if (labels && labels.length) {
69
+ data.labels = Array.from(labels).map(label => compact(label.textContent));
70
+ }
71
+ // Add data on its child elements, if any, to the data.
72
+ if (element.childElementCount) {
73
+ const children = Array.from(element.children);
74
+ data.children = [];
75
+ children.forEach(child => {
76
+ data.children.push(getElementData(child, true));
77
+ });
78
+ }
79
+ }
80
+ return data;
81
+ };
82
+ // FUNCTION DEFINITIONS END
83
+ const normText = normalize(text);
84
+ // Create a collection of the text nodes.
85
+ const walker = document.createTreeWalker(tempBody, NodeFilter.SHOW_TEXT);
86
+ // Get their count.
87
+ const data = {nodeCount: 0};
88
+ let more = true;
89
+ while(more) {
90
+ if (walker.nextNode()) {
91
+ if (normalize(walker.currentNode.nodeValue).includes(normText)) {
92
+ data.nodeCount++;
93
+ matchNodes.push(walker.currentNode);
94
+ }
95
+ }
96
+ else {
97
+ more = false;
98
+ }
99
+ }
100
+ // If no itemization is required:
101
+ if (detailLevel === 0) {
102
+ // Return the node count.
103
+ return data;
104
+ }
105
+ // Otherwise, i.e. if itemization is required:
106
+ else {
107
+ // Initialize the item data.
108
+ data.items = [];
109
+ // For each text node matching the specified text:
110
+ matchNodes.forEach(node => {
111
+ // Initialize the data on it.
112
+ const itemData = {text: compact(node.nodeValue)};
113
+ // If ancestral itemization is required:
114
+ if (detailLevel > 1) {
115
+ // Add the ancestral data to the item data.
116
+ itemData.ancestors = [];
117
+ let base = node;
118
+ let currentLevel = 1;
119
+ while(currentLevel++ < detailLevel) {
120
+ const newBase = base.parentElement;
121
+ itemData.ancestors.push(getElementData(newBase, currentLevel > 2));
122
+ base = newBase;
123
+ }
124
+ }
125
+ // Add the node data to the itemization.
126
+ data.items.push(itemData);
127
+ });
128
+ }
129
+ document.body.removeChild(tempBody);
130
+ return data;
131
+ }, [detailLevel, text]);
132
+ }
133
+ catch(error) {
134
+ console.log(`ERROR performing test (${error.message.replace(/\n.+/s, '')})`);
135
+ data = {
136
+ prevented: true,
137
+ error: 'ERROR performing test'
138
+ };
139
+ }
140
+ // Return the result.
141
+ return {result: data};
142
+ };