testaro 2.3.11 → 4.0.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/README.md +101 -72
- package/commands.js +15 -1
- package/index.js +291 -253
- package/package.json +2 -1
- package/samples/scripts/simple.json +20 -0
- package/samples/scripts/tenon.json +28 -0
- package/tests/ibm.js +45 -19
- package/tests/tenon.js +123 -0
- package/validation/executors/{appNoBatch.js → app.js} +15 -17
- package/validation/executors/tenon.js +18 -0
- package/validation/executors/test.js +18 -0
- package/validation/executors/tests.js +12 -14
- package/validation/executors/appBatch.js +0 -87
package/index.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
testaro main script.
|
|
4
4
|
*/
|
|
5
5
|
// ########## IMPORTS
|
|
6
|
+
// Module to keep secrets.
|
|
7
|
+
require('dotenv').config();
|
|
6
8
|
// Requirements for commands.
|
|
7
9
|
const {commands} = require('./commands');
|
|
8
10
|
// ########## CONSTANTS
|
|
@@ -40,6 +42,7 @@ const tests = {
|
|
|
40
42
|
role: 'roles',
|
|
41
43
|
styleDiff: 'style inconsistencies',
|
|
42
44
|
tabNav: 'keyboard navigation between tab elements',
|
|
45
|
+
tenon: 'Tenon',
|
|
43
46
|
wave: 'WAVE',
|
|
44
47
|
zIndex: 'z indexes'
|
|
45
48
|
};
|
|
@@ -55,6 +58,11 @@ const browserTypeNames = {
|
|
|
55
58
|
};
|
|
56
59
|
// Items that may be waited for.
|
|
57
60
|
const waitables = ['url', 'title', 'body'];
|
|
61
|
+
// Tenon data.
|
|
62
|
+
const tenonData = {
|
|
63
|
+
accessToken: '',
|
|
64
|
+
requestIDs: {}
|
|
65
|
+
};
|
|
58
66
|
// ########## VARIABLES
|
|
59
67
|
// Facts about the current session.
|
|
60
68
|
let logCount = 0;
|
|
@@ -194,38 +202,18 @@ const isValidScript = script => {
|
|
|
194
202
|
&& isURL(commands[1].which)
|
|
195
203
|
&& commands.every(command => isValidCommand(command));
|
|
196
204
|
};
|
|
197
|
-
// Validates a batch.
|
|
198
|
-
const isValidBatch = batch => {
|
|
199
|
-
// If the batch exists:
|
|
200
|
-
if (batch) {
|
|
201
|
-
// Get its data.
|
|
202
|
-
const {what, hosts} = batch;
|
|
203
|
-
// Return whether the batch is valid:
|
|
204
|
-
return what
|
|
205
|
-
&& hosts
|
|
206
|
-
&& typeof what === 'string'
|
|
207
|
-
&& Array.isArray(hosts)
|
|
208
|
-
&& hosts.every(host => host.which && host.what && isURL(host.which));
|
|
209
|
-
}
|
|
210
|
-
// Otherwise, i.e. if the batch does not exist:
|
|
211
|
-
else {
|
|
212
|
-
// Return that it is valid, because it is optional.
|
|
213
|
-
return true;
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
205
|
// Validates an initialized reports array.
|
|
217
|
-
const
|
|
206
|
+
const isValidActs = acts => Array.isArray(acts) && ! acts.length;
|
|
218
207
|
// Validates an initialized log array.
|
|
219
208
|
const isValidLog = log => Array.isArray(log) && ! log.length;
|
|
220
|
-
// Validates
|
|
221
|
-
const
|
|
222
|
-
if (
|
|
223
|
-
const {id, script,
|
|
209
|
+
// Validates a report object.
|
|
210
|
+
const isValidReport = async report => {
|
|
211
|
+
if (report) {
|
|
212
|
+
const {id, script, log, acts} = report;
|
|
224
213
|
return id
|
|
225
214
|
&& isValidScript(script)
|
|
226
|
-
&& isValidBatch(batch)
|
|
227
215
|
&& isValidLog(log)
|
|
228
|
-
&&
|
|
216
|
+
&& isValidActs(acts);
|
|
229
217
|
}
|
|
230
218
|
else {
|
|
231
219
|
return false;
|
|
@@ -379,7 +367,7 @@ const textOf = async (page, element) => {
|
|
|
379
367
|
const matchElement = async (page, selector, matchText, index = 0) => {
|
|
380
368
|
// If the page still exists:
|
|
381
369
|
if (page) {
|
|
382
|
-
// Wait
|
|
370
|
+
// Wait 2 seconds until the body contains any text to be matched.
|
|
383
371
|
const slimText = debloat(matchText);
|
|
384
372
|
const bodyText = await page.textContent('body');
|
|
385
373
|
const slimBody = debloat(bodyText);
|
|
@@ -569,7 +557,7 @@ const waitError = (page, act, error, what) => {
|
|
|
569
557
|
act.result.error = `ERROR waiting for ${what}`;
|
|
570
558
|
return false;
|
|
571
559
|
};
|
|
572
|
-
// Recursively performs the
|
|
560
|
+
// Recursively performs the acts in a report.
|
|
573
561
|
const doActs = async (report, actIndex, page) => {
|
|
574
562
|
const {acts} = report;
|
|
575
563
|
// If any more commands are to be performed:
|
|
@@ -726,157 +714,125 @@ const doActs = async (report, actIndex, page) => {
|
|
|
726
714
|
await require('./procs/test/allVis').allVis(page);
|
|
727
715
|
act.result = 'All elements visible.';
|
|
728
716
|
}
|
|
729
|
-
// Otherwise, if
|
|
730
|
-
else if (act.type === '
|
|
731
|
-
const {
|
|
732
|
-
const
|
|
733
|
-
//
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
const currentJSHandle = await page.evaluateHandle(actCount => {
|
|
748
|
-
// Initialize it as the focused element.
|
|
749
|
-
let currentElement = document.activeElement;
|
|
750
|
-
// If it exists in the page:
|
|
751
|
-
if (currentElement && currentElement.tagName !== 'BODY') {
|
|
752
|
-
// Change it, if necessary, to its active descendant.
|
|
753
|
-
if (currentElement.hasAttribute('aria-activedescendant')) {
|
|
754
|
-
currentElement = document.getElementById(
|
|
755
|
-
currentElement.getAttribute('aria-activedescendant')
|
|
756
|
-
);
|
|
757
|
-
}
|
|
758
|
-
// Or change it, if necessary, to its selected option.
|
|
759
|
-
else if (currentElement.tagName === 'SELECT') {
|
|
760
|
-
const currentIndex = Math.max(0, currentElement.selectedIndex);
|
|
761
|
-
const options = currentElement.querySelectorAll('option');
|
|
762
|
-
currentElement = options[currentIndex];
|
|
763
|
-
}
|
|
764
|
-
// Or change it, if necessary, to its active shadow-DOM element.
|
|
765
|
-
else if (currentElement.shadowRoot) {
|
|
766
|
-
currentElement = currentElement.shadowRoot.activeElement;
|
|
767
|
-
}
|
|
768
|
-
// If there is a current element:
|
|
769
|
-
if (currentElement) {
|
|
770
|
-
// If it was already reached within this command performance:
|
|
771
|
-
if (currentElement.dataset.pressesReached === actCount.toString(10)) {
|
|
772
|
-
// Report the error.
|
|
773
|
-
console.log(`ERROR: ${currentElement.tagName} element reached again`);
|
|
774
|
-
status = 'ERROR';
|
|
775
|
-
return 'ERROR: locallyExhausted';
|
|
776
|
-
}
|
|
777
|
-
// Otherwise, i.e. if it is newly reached within this act:
|
|
778
|
-
else {
|
|
779
|
-
// Mark and return it.
|
|
780
|
-
currentElement.dataset.pressesReached = actCount;
|
|
781
|
-
return currentElement;
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
// Otherwise, i.e. if there is no current element:
|
|
785
|
-
else {
|
|
786
|
-
// Report the error.
|
|
787
|
-
status = 'ERROR';
|
|
788
|
-
return 'noActiveElement';
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
// Otherwise, i.e. if there is no focus in the page:
|
|
792
|
-
else {
|
|
793
|
-
// Report the error.
|
|
794
|
-
status = 'ERROR';
|
|
795
|
-
return 'ERROR: globallyExhausted';
|
|
796
|
-
}
|
|
797
|
-
}, actCount);
|
|
798
|
-
// If the current element exists:
|
|
799
|
-
const currentElement = currentJSHandle.asElement();
|
|
800
|
-
if (currentElement) {
|
|
801
|
-
// Update the data.
|
|
802
|
-
const tagNameJSHandle = await currentElement.getProperty('tagName');
|
|
803
|
-
const tagName = await tagNameJSHandle.jsonValue();
|
|
804
|
-
const text = await textOf(page, currentElement);
|
|
805
|
-
// If the text of the current element was found:
|
|
806
|
-
if (text !== null) {
|
|
807
|
-
const textLength = text.length;
|
|
808
|
-
// If it is non-empty and there are texts to match:
|
|
809
|
-
if (matchTexts.length && textLength) {
|
|
810
|
-
// Identify the matching text.
|
|
811
|
-
matchedText = matchTexts.find(matchText => text.includes(matchText));
|
|
812
|
-
}
|
|
813
|
-
// Update the item data if required.
|
|
814
|
-
if (withItems) {
|
|
815
|
-
const itemData = {
|
|
816
|
-
tagName,
|
|
817
|
-
text,
|
|
818
|
-
textLength
|
|
819
|
-
};
|
|
820
|
-
if (matchedText) {
|
|
821
|
-
itemData.matchedText = matchedText;
|
|
717
|
+
// Otherwise, if the act is a tenon request:
|
|
718
|
+
else if (act.type === 'tenonRequest') {
|
|
719
|
+
const {id, withNewContent} = act;
|
|
720
|
+
const https = require('https');
|
|
721
|
+
// If a Tenon access token has not yet been obtained:
|
|
722
|
+
if (! tenonData.accessToken) {
|
|
723
|
+
// Authenticate with the Tenon API.
|
|
724
|
+
const authData = await new Promise(resolve => {
|
|
725
|
+
const request = https.request(
|
|
726
|
+
{
|
|
727
|
+
host: 'tenon.io',
|
|
728
|
+
path: '/api/v2/auth',
|
|
729
|
+
port: 443,
|
|
730
|
+
protocol: 'https:',
|
|
731
|
+
method: 'POST',
|
|
732
|
+
headers: {
|
|
733
|
+
'Content-Type': 'application/json',
|
|
734
|
+
'Cache-Control': 'no-cache'
|
|
822
735
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
const inputText = act.text;
|
|
834
|
-
if (inputText) {
|
|
835
|
-
await page.keyboard.type(inputText);
|
|
836
|
-
presses += inputText.length;
|
|
736
|
+
},
|
|
737
|
+
response => {
|
|
738
|
+
let responseData = '';
|
|
739
|
+
response.on('data', chunk => {
|
|
740
|
+
responseData += chunk;
|
|
741
|
+
});
|
|
742
|
+
response.on('end', () => {
|
|
743
|
+
try {
|
|
744
|
+
const responseJSON = JSON.parse(responseData);
|
|
745
|
+
return resolve(responseJSON);
|
|
837
746
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
747
|
+
catch(error) {
|
|
748
|
+
return resolve({
|
|
749
|
+
error: 'Tenon did not return JSON authentication data.',
|
|
750
|
+
responseData
|
|
751
|
+
});
|
|
842
752
|
}
|
|
843
|
-
}
|
|
753
|
+
});
|
|
844
754
|
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
755
|
+
);
|
|
756
|
+
const tenonUser = process.env.TESTARO_TENON_USER;
|
|
757
|
+
const tenonPassword = process.env.TESTARO_TENON_PASSWORD;
|
|
758
|
+
const postData = JSON.stringify({
|
|
759
|
+
username: tenonUser,
|
|
760
|
+
password: tenonPassword
|
|
761
|
+
});
|
|
762
|
+
request.write(postData);
|
|
763
|
+
request.end();
|
|
764
|
+
});
|
|
765
|
+
// If the authentication succeeded:
|
|
766
|
+
if (authData.access_token) {
|
|
767
|
+
// Record the access token.
|
|
768
|
+
tenonData.accessToken = authData.access_token;
|
|
849
769
|
}
|
|
850
|
-
// Otherwise, i.e. if
|
|
770
|
+
// Otherwise, i.e. if the authentication failed:
|
|
851
771
|
else {
|
|
852
|
-
|
|
853
|
-
status = await currentJSHandle.jsonValue();
|
|
772
|
+
console.log('ERROR: tenon authentication failed');
|
|
854
773
|
}
|
|
855
774
|
}
|
|
856
|
-
//
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
775
|
+
// If a Tenon access token exists:
|
|
776
|
+
if (tenonData.accessToken) {
|
|
777
|
+
// Request a Tenon test of the page and get a response ID.
|
|
778
|
+
const option = {};
|
|
779
|
+
// If Tenon is to be given the URL and not the content of the page:
|
|
780
|
+
if (withNewContent) {
|
|
781
|
+
// Specify this.
|
|
782
|
+
option.url = page.url();
|
|
862
783
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
784
|
+
// Otherwise, i.e. if Tenon is to be given the page content:
|
|
785
|
+
else {
|
|
786
|
+
// Specify this.
|
|
787
|
+
option.src = await page.content();
|
|
788
|
+
}
|
|
789
|
+
// Request a Tenon test and get a response ID.
|
|
790
|
+
const responseID = await new Promise(resolve => {
|
|
791
|
+
const request = https.request(
|
|
792
|
+
{
|
|
793
|
+
host: 'tenon.io',
|
|
794
|
+
path: '/api/v2/',
|
|
795
|
+
port: 443,
|
|
796
|
+
protocol: 'https:',
|
|
797
|
+
method: 'POST',
|
|
798
|
+
headers: {
|
|
799
|
+
'Content-Type': 'application/json',
|
|
800
|
+
'Cache-Control': 'no-cache',
|
|
801
|
+
Authorization: `Bearer ${tenonData.accessToken}`
|
|
802
|
+
}
|
|
803
|
+
},
|
|
804
|
+
response => {
|
|
805
|
+
let resultJSON = '';
|
|
806
|
+
response.on('data', chunk => {
|
|
807
|
+
resultJSON += chunk;
|
|
808
|
+
});
|
|
809
|
+
// When the data arrive, return them as an object.
|
|
810
|
+
response.on('end', () => {
|
|
811
|
+
try {
|
|
812
|
+
const result = JSON.parse(resultJSON);
|
|
813
|
+
resolve(result.data.responseID || '');
|
|
814
|
+
}
|
|
815
|
+
catch (error) {
|
|
816
|
+
console.log('ERROR: Tenon did not return JSON.');
|
|
817
|
+
resolve('');
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
);
|
|
822
|
+
const postData = JSON.stringify(option);
|
|
823
|
+
request.write(postData);
|
|
824
|
+
request.end();
|
|
825
|
+
});
|
|
826
|
+
// Record the response ID.
|
|
827
|
+
tenonData.requestIDs[id] = responseID || '';
|
|
869
828
|
}
|
|
870
|
-
// Add the totals to the report.
|
|
871
|
-
report.presses += presses;
|
|
872
|
-
report.amountRead += amountRead;
|
|
873
829
|
}
|
|
874
830
|
// Otherwise, if the act is a test:
|
|
875
831
|
else if (act.type === 'test') {
|
|
876
832
|
// Add a description of the test to the act.
|
|
877
833
|
act.what = tests[act.which];
|
|
878
834
|
// Initialize the arguments.
|
|
879
|
-
const args = [page];
|
|
835
|
+
const args = [act.which === 'tenon' ? tenonData : page];
|
|
880
836
|
// Identify the additional validator of the test.
|
|
881
837
|
const testValidator = commands.tests[act.which];
|
|
882
838
|
// If it exists:
|
|
@@ -1009,7 +965,7 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1009
965
|
// Otherwise, if it is entering text on the element:
|
|
1010
966
|
else if (act.type === 'text') {
|
|
1011
967
|
// If the text contains a placeholder for an environment variable:
|
|
1012
|
-
let {what} = act;
|
|
968
|
+
let {what} = act;
|
|
1013
969
|
if (/__[A-Z]+__/.test(what)) {
|
|
1014
970
|
// Replace it.
|
|
1015
971
|
const envKey = /__([A-Z]+)__/.exec(what)[1];
|
|
@@ -1045,6 +1001,151 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1045
1001
|
const qualifier = act.again ? `${1 + act.again} times` : 'once';
|
|
1046
1002
|
act.result = `pressed ${qualifier}`;
|
|
1047
1003
|
}
|
|
1004
|
+
// Otherwise, if it is a repetitive keyboard navigation:
|
|
1005
|
+
else if (act.type === 'presses') {
|
|
1006
|
+
const {navKey, what, which, withItems} = act;
|
|
1007
|
+
const matchTexts = which ? which.map(text => debloat(text)) : [];
|
|
1008
|
+
// Initialize the loop variables.
|
|
1009
|
+
let status = 'more';
|
|
1010
|
+
let presses = 0;
|
|
1011
|
+
let amountRead = 0;
|
|
1012
|
+
let items = [];
|
|
1013
|
+
let matchedText;
|
|
1014
|
+
// As long as a matching element has not been reached:
|
|
1015
|
+
while (status === 'more') {
|
|
1016
|
+
// Press the Escape key to dismiss any modal dialog.
|
|
1017
|
+
await page.keyboard.press('Escape');
|
|
1018
|
+
// Press the specified navigation key.
|
|
1019
|
+
await page.keyboard.press(navKey);
|
|
1020
|
+
presses++;
|
|
1021
|
+
// Identify the newly current element or a failure.
|
|
1022
|
+
const currentJSHandle = await page.evaluateHandle(actCount => {
|
|
1023
|
+
// Initialize it as the focused element.
|
|
1024
|
+
let currentElement = document.activeElement;
|
|
1025
|
+
// If it exists in the page:
|
|
1026
|
+
if (currentElement && currentElement.tagName !== 'BODY') {
|
|
1027
|
+
// Change it, if necessary, to its active descendant.
|
|
1028
|
+
if (currentElement.hasAttribute('aria-activedescendant')) {
|
|
1029
|
+
currentElement = document.getElementById(
|
|
1030
|
+
currentElement.getAttribute('aria-activedescendant')
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
// Or change it, if necessary, to its selected option.
|
|
1034
|
+
else if (currentElement.tagName === 'SELECT') {
|
|
1035
|
+
const currentIndex = Math.max(0, currentElement.selectedIndex);
|
|
1036
|
+
const options = currentElement.querySelectorAll('option');
|
|
1037
|
+
currentElement = options[currentIndex];
|
|
1038
|
+
}
|
|
1039
|
+
// Or change it, if necessary, to its active shadow-DOM element.
|
|
1040
|
+
else if (currentElement.shadowRoot) {
|
|
1041
|
+
currentElement = currentElement.shadowRoot.activeElement;
|
|
1042
|
+
}
|
|
1043
|
+
// If there is a current element:
|
|
1044
|
+
if (currentElement) {
|
|
1045
|
+
// If it was already reached within this command performance:
|
|
1046
|
+
if (currentElement.dataset.pressesReached === actCount.toString(10)) {
|
|
1047
|
+
// Report the error.
|
|
1048
|
+
console.log(`ERROR: ${currentElement.tagName} element reached again`);
|
|
1049
|
+
status = 'ERROR';
|
|
1050
|
+
return 'ERROR: locallyExhausted';
|
|
1051
|
+
}
|
|
1052
|
+
// Otherwise, i.e. if it is newly reached within this act:
|
|
1053
|
+
else {
|
|
1054
|
+
// Mark and return it.
|
|
1055
|
+
currentElement.dataset.pressesReached = actCount;
|
|
1056
|
+
return currentElement;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
// Otherwise, i.e. if there is no current element:
|
|
1060
|
+
else {
|
|
1061
|
+
// Report the error.
|
|
1062
|
+
status = 'ERROR';
|
|
1063
|
+
return 'noActiveElement';
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
// Otherwise, i.e. if there is no focus in the page:
|
|
1067
|
+
else {
|
|
1068
|
+
// Report the error.
|
|
1069
|
+
status = 'ERROR';
|
|
1070
|
+
return 'ERROR: globallyExhausted';
|
|
1071
|
+
}
|
|
1072
|
+
}, actCount);
|
|
1073
|
+
// If the current element exists:
|
|
1074
|
+
const currentElement = currentJSHandle.asElement();
|
|
1075
|
+
if (currentElement) {
|
|
1076
|
+
// Update the data.
|
|
1077
|
+
const tagNameJSHandle = await currentElement.getProperty('tagName');
|
|
1078
|
+
const tagName = await tagNameJSHandle.jsonValue();
|
|
1079
|
+
const text = await textOf(page, currentElement);
|
|
1080
|
+
// If the text of the current element was found:
|
|
1081
|
+
if (text !== null) {
|
|
1082
|
+
const textLength = text.length;
|
|
1083
|
+
// If it is non-empty and there are texts to match:
|
|
1084
|
+
if (matchTexts.length && textLength) {
|
|
1085
|
+
// Identify the matching text.
|
|
1086
|
+
matchedText = matchTexts.find(matchText => text.includes(matchText));
|
|
1087
|
+
}
|
|
1088
|
+
// Update the item data if required.
|
|
1089
|
+
if (withItems) {
|
|
1090
|
+
const itemData = {
|
|
1091
|
+
tagName,
|
|
1092
|
+
text,
|
|
1093
|
+
textLength
|
|
1094
|
+
};
|
|
1095
|
+
if (matchedText) {
|
|
1096
|
+
itemData.matchedText = matchedText;
|
|
1097
|
+
}
|
|
1098
|
+
items.push(itemData);
|
|
1099
|
+
}
|
|
1100
|
+
amountRead += textLength;
|
|
1101
|
+
// If there is no text-match failure:
|
|
1102
|
+
if (matchedText || ! matchTexts.length) {
|
|
1103
|
+
// If the element has any specified tag name:
|
|
1104
|
+
if (! what || tagName === what) {
|
|
1105
|
+
// Change the status.
|
|
1106
|
+
status = 'done';
|
|
1107
|
+
// Perform the action.
|
|
1108
|
+
const inputText = act.text;
|
|
1109
|
+
if (inputText) {
|
|
1110
|
+
await page.keyboard.type(inputText);
|
|
1111
|
+
presses += inputText.length;
|
|
1112
|
+
}
|
|
1113
|
+
if (act.action) {
|
|
1114
|
+
presses++;
|
|
1115
|
+
await page.keyboard.press(act.action);
|
|
1116
|
+
await page.waitForLoadState();
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
else {
|
|
1122
|
+
status = 'ERROR';
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
// Otherwise, i.e. if there was a failure:
|
|
1126
|
+
else {
|
|
1127
|
+
// Update the status.
|
|
1128
|
+
status = await currentJSHandle.jsonValue();
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
// Add the result to the act.
|
|
1132
|
+
act.result = {
|
|
1133
|
+
status,
|
|
1134
|
+
totals: {
|
|
1135
|
+
presses,
|
|
1136
|
+
amountRead
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
if (status === 'done' && matchedText) {
|
|
1140
|
+
act.result.matchedText = matchedText;
|
|
1141
|
+
}
|
|
1142
|
+
if (withItems) {
|
|
1143
|
+
act.result.items = items;
|
|
1144
|
+
}
|
|
1145
|
+
// Add the totals to the report.
|
|
1146
|
+
report.presses += presses;
|
|
1147
|
+
report.amountRead += amountRead;
|
|
1148
|
+
}
|
|
1048
1149
|
// Otherwise, i.e. if the act type is unknown:
|
|
1049
1150
|
else {
|
|
1050
1151
|
// Add the error result to the act.
|
|
@@ -1079,7 +1180,7 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1079
1180
|
}
|
|
1080
1181
|
};
|
|
1081
1182
|
// Performs the commands in a script.
|
|
1082
|
-
const doScript = async (
|
|
1183
|
+
const doScript = async (report) => {
|
|
1083
1184
|
// Reinitialize the log statistics.
|
|
1084
1185
|
logCount = logSize = prohibitedCount = visitTimeoutCount = visitRejectionCount= 0;
|
|
1085
1186
|
// Add the start time to the report.
|
|
@@ -1122,88 +1223,35 @@ const doScript = async (options, report) => {
|
|
|
1122
1223
|
const endTime = new Date();
|
|
1123
1224
|
report.endTime = endTime.toISOString().slice(0, 19);
|
|
1124
1225
|
report.elapsedSeconds = Math.floor((endTime - startTime) / 1000);
|
|
1125
|
-
// Add the report to the reports array.
|
|
1126
|
-
options.reports.push(report);
|
|
1127
|
-
};
|
|
1128
|
-
// Recursively performs commands on the hosts of a batch.
|
|
1129
|
-
const doBatch = async (options, reportTemplate, hostIndex = 0) => {
|
|
1130
|
-
const {hosts} = options.batch;
|
|
1131
|
-
const host = hosts[hostIndex];
|
|
1132
|
-
// If the specified host exists:
|
|
1133
|
-
if (host) {
|
|
1134
|
-
// Create a report for it.
|
|
1135
|
-
const hostReport = JSON.parse(JSON.stringify(reportTemplate));
|
|
1136
|
-
// Copy the properties of the specified host to all url acts.
|
|
1137
|
-
hostReport.acts.forEach(act => {
|
|
1138
|
-
if (act.type === 'url') {
|
|
1139
|
-
act.which = host.which;
|
|
1140
|
-
act.what = host.what;
|
|
1141
|
-
}
|
|
1142
|
-
});
|
|
1143
|
-
// Add the host’s ID to the host report.
|
|
1144
|
-
hostReport.hostName = host.id;
|
|
1145
|
-
// Add data from the template to the host report.
|
|
1146
|
-
hostReport.orderName = options.id;
|
|
1147
|
-
hostReport.id = `${options.id}-${host.id}`;
|
|
1148
|
-
hostReport.orderUserName = options.userName;
|
|
1149
|
-
hostReport.orderTime = options.orderTime;
|
|
1150
|
-
hostReport.assignedBy = options.assignedBy;
|
|
1151
|
-
hostReport.assignedTime = options.assignedTime;
|
|
1152
|
-
hostReport.tester = options.tester;
|
|
1153
|
-
hostReport.scriptName = options.scriptName;
|
|
1154
|
-
hostReport.batchName = options.batchName;
|
|
1155
|
-
hostReport.scriptIsValid = options.scriptIsValid;
|
|
1156
|
-
hostReport.batchIsValid = options.batchIsValid;
|
|
1157
|
-
hostReport.host = host;
|
|
1158
|
-
// Perform the commands on the host and add a report to the options object.
|
|
1159
|
-
await doScript(options, hostReport);
|
|
1160
|
-
// Process the remaining hosts.
|
|
1161
|
-
await doBatch(options, reportTemplate, hostIndex + 1);
|
|
1162
|
-
}
|
|
1163
|
-
};
|
|
1164
|
-
// Performs a script, with or without a batch.
|
|
1165
|
-
const doScriptOrBatch = async (options, reportTemplate) => {
|
|
1166
|
-
// If there is a batch:
|
|
1167
|
-
if (options.batch) {
|
|
1168
|
-
// Perform the script on all the hosts in the batch.
|
|
1169
|
-
console.log('Starting batch');
|
|
1170
|
-
await doBatch(options, reportTemplate);
|
|
1171
|
-
}
|
|
1172
|
-
// Otherwise, i.e. if there is no batch:
|
|
1173
|
-
else {
|
|
1174
|
-
// Perform the script.
|
|
1175
|
-
console.log('Starting no-batch script');
|
|
1176
|
-
await doScript(options, reportTemplate);
|
|
1177
|
-
}
|
|
1178
1226
|
// Add an end time to the log.
|
|
1179
|
-
|
|
1227
|
+
report.log.push({
|
|
1180
1228
|
event: 'endTime',
|
|
1181
1229
|
value: ((new Date()).toISOString().slice(0, 19))
|
|
1182
1230
|
});
|
|
1183
1231
|
};
|
|
1184
|
-
// Injects url
|
|
1185
|
-
const
|
|
1232
|
+
// Injects url acts into a report where necessary to undo DOM changes.
|
|
1233
|
+
const injectURLActs = acts => {
|
|
1186
1234
|
let injectMore = true;
|
|
1187
1235
|
while (injectMore) {
|
|
1188
|
-
const injectIndex =
|
|
1189
|
-
index <
|
|
1190
|
-
&&
|
|
1191
|
-
&&
|
|
1192
|
-
&& domChangers.has(
|
|
1236
|
+
const injectIndex = acts.findIndex((act, index) =>
|
|
1237
|
+
index < acts.length - 1
|
|
1238
|
+
&& act.type === 'test'
|
|
1239
|
+
&& acts[index + 1].type === 'test'
|
|
1240
|
+
&& domChangers.has(act.which)
|
|
1193
1241
|
);
|
|
1194
1242
|
if (injectIndex === -1) {
|
|
1195
1243
|
injectMore = false;
|
|
1196
1244
|
}
|
|
1197
1245
|
else {
|
|
1198
|
-
const lastURL =
|
|
1199
|
-
if (
|
|
1200
|
-
return
|
|
1246
|
+
const lastURL = acts.reduce((url, act, index) => {
|
|
1247
|
+
if (act.type === 'url' && index < injectIndex) {
|
|
1248
|
+
return act.which;
|
|
1201
1249
|
}
|
|
1202
1250
|
else {
|
|
1203
1251
|
return url;
|
|
1204
1252
|
}
|
|
1205
1253
|
}, '');
|
|
1206
|
-
|
|
1254
|
+
acts.splice(injectIndex + 1, 0, {
|
|
1207
1255
|
type: 'url',
|
|
1208
1256
|
which: lastURL,
|
|
1209
1257
|
what: 'URL'
|
|
@@ -1212,36 +1260,26 @@ const injectURLCommands = commands => {
|
|
|
1212
1260
|
}
|
|
1213
1261
|
};
|
|
1214
1262
|
// Handles a request.
|
|
1215
|
-
exports.handleRequest = async
|
|
1216
|
-
// If the
|
|
1217
|
-
if(
|
|
1218
|
-
// Add a start time
|
|
1219
|
-
|
|
1263
|
+
exports.handleRequest = async report => {
|
|
1264
|
+
// If the report object is valid:
|
|
1265
|
+
if(isValidReport(report)) {
|
|
1266
|
+
// Add a start time to the log.
|
|
1267
|
+
report.log.push(
|
|
1220
1268
|
{
|
|
1221
1269
|
event: 'startTime',
|
|
1222
1270
|
value: ((new Date()).toISOString().slice(0, 19))
|
|
1223
|
-
},
|
|
1224
|
-
{
|
|
1225
|
-
event: 'timeStamp',
|
|
1226
|
-
value: Math.floor((Date.now() - Date.UTC(2022, 1)) / 500).toString(36)
|
|
1227
1271
|
}
|
|
1228
1272
|
);
|
|
1229
|
-
// Add
|
|
1230
|
-
if (
|
|
1231
|
-
|
|
1232
|
-
event: 'batchSize',
|
|
1233
|
-
value: options.batch.hosts.length
|
|
1234
|
-
});
|
|
1273
|
+
// Add an ID to the report if none exists yet.
|
|
1274
|
+
if (! report.id) {
|
|
1275
|
+
report.id = Math.floor((Date.now() - Date.UTC(2022, 1)) / 2000).toString(36);
|
|
1235
1276
|
}
|
|
1236
|
-
//
|
|
1237
|
-
|
|
1238
|
-
host: '',
|
|
1239
|
-
acts: JSON.parse(JSON.stringify(options.script.commands))
|
|
1240
|
-
};
|
|
1277
|
+
// Add the script commands to the report as its initial acts.
|
|
1278
|
+
report.acts = JSON.parse(JSON.stringify(report.script.commands));
|
|
1241
1279
|
// Inject url acts where necessary to undo DOM changes.
|
|
1242
|
-
|
|
1243
|
-
// Perform the script,
|
|
1244
|
-
await
|
|
1280
|
+
injectURLActs(report.acts);
|
|
1281
|
+
// Perform the script, asynchronously adding to the log and report.
|
|
1282
|
+
await doScript(report);
|
|
1245
1283
|
}
|
|
1246
1284
|
else {
|
|
1247
1285
|
console.log('ERROR: options missing or invalid');
|