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 +22 -7
- package/package.json +1 -1
- package/run.js +91 -83
- package/tests/elements.js +8 -1
- package/tests/textNodes.js +142 -0
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: [
|
|
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: [
|
|
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: [
|
|
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: [
|
|
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: [
|
|
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
|
|
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
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:
|
|
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
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
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
|
-
//
|
|
991
|
-
|
|
992
|
-
//
|
|
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 = '
|
|
985
|
+
act.result.error = 'exhausted';
|
|
995
986
|
act.result.typeElementCount = selections.length;
|
|
996
|
-
|
|
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
|
|
994
|
+
// Otherwise, i.e. if there are too few such elements to make a match possible:
|
|
1000
995
|
else {
|
|
1001
|
-
//
|
|
996
|
+
// Add the failure data to the report.
|
|
1002
997
|
act.result.success = false;
|
|
1003
|
-
act.result.error = '
|
|
1004
|
-
act.result.typeElementCount =
|
|
1005
|
-
act.result.message = '
|
|
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
|
|
1003
|
+
// Otherwise, i.e. if there are no elements of the specified type:
|
|
1009
1004
|
else {
|
|
1010
|
-
//
|
|
1005
|
+
// Add the failure data to the report.
|
|
1011
1006
|
act.result.success = false;
|
|
1012
|
-
act.result.error = '
|
|
1013
|
-
act.result.
|
|
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
|
|
1012
|
+
// Otherwise, i.e. if the page no longer exists:
|
|
1017
1013
|
else {
|
|
1018
|
-
//
|
|
1014
|
+
// Add the failure data to the report.
|
|
1019
1015
|
act.result.success = false;
|
|
1020
|
-
act.result.error = '
|
|
1021
|
-
act.result.message = '
|
|
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
|
-
|
|
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
|
|
1161
|
-
else if (act.type
|
|
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
|
|
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
|
+
};
|