testaro 5.11.2 → 5.12.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
@@ -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-insensiteve 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.2",
3
+ "version": "5.12.0",
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',
@@ -963,10 +965,10 @@ const doActs = async (report, actIndex, page) => {
963
965
  let matchCount = 0;
964
966
  const selectionTexts = [];
965
967
  for (selection of selections) {
966
- // Add its text to the list of texts of such elements.
967
- const selectionText = await textOf(page, selection);
968
+ // Add its text or an empty string to the list of texts of such elements.
969
+ const selectionText = slimText ? await textOf(page, selection) : '';
968
970
  selectionTexts.push(selectionText);
969
- // If its text includes the specified text:
971
+ // If its text includes any specified text:
970
972
  if (selectionText.includes(slimText)) {
971
973
  // If the element has the specified index among such elements:
972
974
  if (matchCount++ === (act.index || 0)) {
@@ -979,17 +981,20 @@ const doActs = async (report, actIndex, page) => {
979
981
  }
980
982
  // If no element satisfied the specifications:
981
983
  if (! act.result.found) {
984
+ // Add the failure data to the report.
982
985
  act.result.success = false;
983
986
  act.result.error = 'exhausted';
984
987
  act.result.typeElementCount = selections.length;
985
- act.result.textElementCount = --matchCount;
986
- act.result.message = 'Not enough elements have the specified text';
988
+ if (slimText) {
989
+ act.result.textElementCount = --matchCount;
990
+ }
991
+ act.result.message = 'Not enough specified elements exist';
987
992
  act.result.candidateTexts = selectionTexts;
988
993
  }
989
994
  }
990
995
  // Otherwise, i.e. if there are too few such elements to make a match possible:
991
996
  else {
992
- // Return a failure.
997
+ // Add the failure data to the report.
993
998
  act.result.success = false;
994
999
  act.result.error = 'fewer';
995
1000
  act.result.typeElementCount = selections.length;
@@ -998,16 +1003,16 @@ const doActs = async (report, actIndex, page) => {
998
1003
  }
999
1004
  // Otherwise, i.e. if there are no elements of the specified type:
1000
1005
  else {
1001
- // Return a failure.
1006
+ // Add the failure data to the report.
1002
1007
  act.result.success = false;
1003
1008
  act.result.error = 'none';
1004
1009
  act.result.typeElementCount = 0;
1005
- act.result.message = 'No elements specified type found';
1010
+ act.result.message = 'No elements of specified type found';
1006
1011
  }
1007
1012
  }
1008
1013
  // Otherwise, i.e. if the page no longer exists:
1009
1014
  else {
1010
- // Return a failure.
1015
+ // Add the failure data to the report.
1011
1016
  act.result.success = false;
1012
1017
  act.result.error = 'gone';
1013
1018
  act.result.message = 'Page gone';
@@ -1015,7 +1020,7 @@ const doActs = async (report, actIndex, page) => {
1015
1020
  }
1016
1021
  // Otherwise, i.e. if no text was specified:
1017
1022
  else {
1018
- // Return a failure.
1023
+ // Add the failure data to the report.
1019
1024
  act.result.success = false;
1020
1025
  act.result.error = 'text';
1021
1026
  act.result.message = 'No text specified';
@@ -1026,6 +1031,36 @@ const doActs = async (report, actIndex, page) => {
1026
1031
  }
1027
1032
  // If a match was found:
1028
1033
  if (act.result.found) {
1034
+ // FUNCTION DEFINITION START
1035
+ // Perform a click or Enter keypress and wait for a page load.
1036
+ const doAndWait = async actionIsClick => {
1037
+ try {
1038
+ const [newPage] = await Promise.all([
1039
+ page.context().waitForEvent('page', {timeout: 7000}),
1040
+ actionIsClick ? selection.click({timeout: 4000}) : selection.press('Enter')
1041
+ ]);
1042
+ // Wait for the new page to load.
1043
+ await newPage.waitForLoadState('domcontentloaded', {timeout: 10000});
1044
+ // Make the new page the current page.
1045
+ page = newPage;
1046
+ act.result.success = true;
1047
+ act.result.move = actionIsClick ? 'clicked' : 'Enter pressed';
1048
+ act.result.newURL = page.url();
1049
+ }
1050
+ // If the action, event, or load failed:
1051
+ catch(error) {
1052
+ // Quit and report the failure.
1053
+ const action = actionIsClick ? 'clicking' : 'pressing Enter';
1054
+ console.log(
1055
+ `ERROR ${action} (${error.message.replace(/\n.+/s, '')})`
1056
+ );
1057
+ act.result.success = false;
1058
+ act.result.error = 'moveFailure';
1059
+ act.result.message = 'ERROR: move, navigation, or load timed out';
1060
+ actIndex = -2;
1061
+ }
1062
+ };
1063
+ // FUNCTION DEFINITION END
1029
1064
  // If the move is a button click, perform it.
1030
1065
  if (act.type === 'button') {
1031
1066
  await selection.click({timeout: 3000});
@@ -1080,30 +1115,7 @@ const doActs = async (report, actIndex, page) => {
1080
1115
  // If the destination is a new page:
1081
1116
  if (target && target !== '_self') {
1082
1117
  // Click the link and wait for the resulting page event.
1083
- try {
1084
- const [newPage] = await Promise.all([
1085
- page.context().waitForEvent('page', {timeout: 6000}),
1086
- selection.click({timeout: 5000})
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
- }
1118
+ doAndWait(true);
1107
1119
  }
1108
1120
  // Otherwise, i.e. if the destination is in the current page:
1109
1121
  else {
@@ -1111,7 +1123,7 @@ const doActs = async (report, actIndex, page) => {
1111
1123
  try {
1112
1124
  await selection.click({timeout: 5000});
1113
1125
  // Wait for the new content to load.
1114
- await page.waitForLoadState('domcontentloaded', {timeout: 4000});
1126
+ await page.waitForLoadState('domcontentloaded', {timeout: 6000});
1115
1127
  act.result.success = true;
1116
1128
  act.result.move = 'clicked';
1117
1129
  act.result.newURL = page.url();
@@ -1157,8 +1169,8 @@ const doActs = async (report, actIndex, page) => {
1157
1169
  act.result.move = 'selected';
1158
1170
  act.result.option = optionText;
1159
1171
  }
1160
- // Otherwise, if it is entering text on the element:
1161
- else if (act.type === 'text') {
1172
+ // Otherwise, if it is entering text on a text- or search-input element:
1173
+ else if (['text', 'search'].includes(act.type)) {
1162
1174
  // If the text contains a placeholder for an environment variable:
1163
1175
  let {what} = act;
1164
1176
  if (/__[A-Z]+__/.test(what)) {
@@ -1172,6 +1184,11 @@ const doActs = async (report, actIndex, page) => {
1172
1184
  report.presses += act.what.length;
1173
1185
  act.result.success = true;
1174
1186
  act.result.move = 'entered';
1187
+ // If the input is a search input:
1188
+ if (act.type === 'search') {
1189
+ // Press the Enter key and wait for a new page to load.
1190
+ doAndWait(false);
1191
+ }
1175
1192
  }
1176
1193
  // Otherwise, i.e. if the move is unknown, add the failure to the act.
1177
1194
  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,125 @@
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
+ // FUNCTION DEFINITIONS START
19
+ // Compacts a string.
20
+ const compact = string => string.replace(/\s+/g, ' ').trim();
21
+ // Compacts and lower-cases a string.
22
+ const normalize = string => compact(string).toLowerCase();
23
+ // Gets data on an element.
24
+ const getElementData = (element, withText) => {
25
+ // Initialize the data.
26
+ const data = {
27
+ tagName: element.tagName
28
+ };
29
+ if (withText) {
30
+ data.text = element.textContent;
31
+ }
32
+ // Add data on its attributes, if any, to the data.
33
+ const {attributes} = element;
34
+ if (attributes) {
35
+ data.attributes = [];
36
+ for (const attribute of attributes) {
37
+ const {name, value} = attribute;
38
+ data.attributes.push({
39
+ name,
40
+ value
41
+ });
42
+ // If any attribute is a labeler reference:
43
+ if (name === 'aria-labelledby') {
44
+ // Add the label texts to the data.
45
+ const labelerIDs = value.split(/\s+/);
46
+ data.refLabels = [];
47
+ labelerIDs.forEach(id => {
48
+ const labeler = document.getElementById(id);
49
+ if (labeler) {
50
+ data.refLabels.push(compact(labeler.textContent));
51
+ }
52
+ });
53
+ }
54
+ }
55
+ }
56
+ // Add data on its labels, if any, to the data.
57
+ const {labels} = element;
58
+ if (labels) {
59
+ data.labels = Array.from(labels).map(label => compact(label.textContent));
60
+ }
61
+ return data;
62
+ };
63
+ // FUNCTION DEFINITIONS END
64
+ const normText = normalize(text);
65
+ // Create a collection of the text nodes.
66
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
67
+ // Get their count.
68
+ const data = {nodeCount: 0};
69
+ let more = true;
70
+ while(more) {
71
+ if (walker.nextNode()) {
72
+ if (
73
+ normalize(walker.currentNode.nodeValue).includes(normText)
74
+ && walker.currentNode.parentElement.tagName !== 'SCRIPT'
75
+ ) {
76
+ data.nodeCount++;
77
+ matchNodes.push(walker.currentNode);
78
+ }
79
+ }
80
+ else {
81
+ more = false;
82
+ }
83
+ }
84
+ // If no itemization is required:
85
+ if (detailLevel === 0) {
86
+ // Return the node count.
87
+ return data;
88
+ }
89
+ // Otherwise, i.e. if itemization is required:
90
+ else {
91
+ // Initialize the item data.
92
+ data.items = [];
93
+ // For each text node matching the specified text:
94
+ matchNodes.forEach(node => {
95
+ // Initialize the data on it.
96
+ const itemData = {text: compact(node.nodeValue)};
97
+ // If ancestral itemization is required:
98
+ if (detailLevel > 1) {
99
+ // Add the ancestral data to the item data.
100
+ itemData.ancestors = [];
101
+ let base = node;
102
+ let currentLevel = 1;
103
+ while(currentLevel++ < detailLevel) {
104
+ const newBase = base.parentElement;
105
+ itemData.ancestors.push(getElementData(newBase, currentLevel > 2));
106
+ base = newBase;
107
+ }
108
+ }
109
+ // Add the node data to the itemization.
110
+ data.items.push(itemData);
111
+ });
112
+ }
113
+ return data;
114
+ }, [detailLevel, text]);
115
+ }
116
+ catch(error) {
117
+ console.log(`ERROR performing test (${error.message.replace(/\n.+/s, '')})`);
118
+ data = {
119
+ prevented: true,
120
+ error: 'ERROR performing test'
121
+ };
122
+ }
123
+ // Return the result.
124
+ return {result: data};
125
+ };
package/tests/title.js CHANGED
@@ -4,5 +4,8 @@
4
4
  */
5
5
  exports.reporter = async page => {
6
6
  const title = await page.title();
7
- return {result: title};
7
+ return {result: {
8
+ success: true,
9
+ title
10
+ }};
8
11
  };