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/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 isValidReports = reports => Array.isArray(reports) && ! reports.length;
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 an options object.
221
- const isValidOptions = async options => {
222
- if (options) {
223
- const {id, script, batch, log, reports} = options;
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
- && isValidReports(reports);
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 3 seconds until the body contains any text to be matched.
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 commands in a report.
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 it is a repetitive keyboard navigation:
730
- else if (act.type === 'presses') {
731
- const {navKey, what, which, withItems} = act;
732
- const matchTexts = which ? which.map(text => debloat(text)) : [];
733
- // Initialize the loop variables.
734
- let status = 'more';
735
- let presses = 0;
736
- let amountRead = 0;
737
- let items = [];
738
- let matchedText;
739
- // As long as a matching element has not been reached:
740
- while (status === 'more') {
741
- // Press the Escape key to dismiss any modal dialog.
742
- await page.keyboard.press('Escape');
743
- // Press the specified navigation key.
744
- await page.keyboard.press(navKey);
745
- presses++;
746
- // Identify the newly current element or a failure.
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
- items.push(itemData);
824
- }
825
- amountRead += textLength;
826
- // If there is no text-match failure:
827
- if (matchedText || ! matchTexts.length) {
828
- // If the element has any specified tag name:
829
- if (! what || tagName === what) {
830
- // Change the status.
831
- status = 'done';
832
- // Perform the action.
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
- if (act.action) {
839
- presses++;
840
- await page.keyboard.press(act.action);
841
- await page.waitForLoadState();
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
- else {
847
- status = 'ERROR';
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 there was a failure:
770
+ // Otherwise, i.e. if the authentication failed:
851
771
  else {
852
- // Update the status.
853
- status = await currentJSHandle.jsonValue();
772
+ console.log('ERROR: tenon authentication failed');
854
773
  }
855
774
  }
856
- // Add the result to the act.
857
- act.result = {
858
- status,
859
- totals: {
860
- presses,
861
- amountRead
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
- if (status === 'done' && matchedText) {
865
- act.result.matchedText = matchedText;
866
- }
867
- if (withItems) {
868
- act.result.items = items;
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 (options, report) => {
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
- options.log.push({
1227
+ report.log.push({
1180
1228
  event: 'endTime',
1181
1229
  value: ((new Date()).toISOString().slice(0, 19))
1182
1230
  });
1183
1231
  };
1184
- // Injects url commands into a report where necessary to undo DOM changes.
1185
- const injectURLCommands = commands => {
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 = commands.findIndex((command, index) =>
1189
- index < commands.length - 1
1190
- && command.type === 'test'
1191
- && commands[index + 1].type === 'test'
1192
- && domChangers.has(command.which)
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 = commands.reduce((url, command, index) => {
1199
- if (command.type === 'url' && index < injectIndex) {
1200
- return command.which;
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
- commands.splice(injectIndex + 1, 0, {
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 options => {
1216
- // If the options object is valid:
1217
- if(isValidOptions(options)) {
1218
- // Add a start time and a timeStamp to the log.
1219
- options.log.push(
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 the batch size to the log if there is a batch.
1230
- if (options.batch) {
1231
- options.log.push({
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
- // Create a report template, containing a copy of the commands as its acts.
1237
- const reportTemplate = {
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
- injectURLCommands(reportTemplate.acts);
1243
- // Perform the script, with or without a batch, asynchronously adding to the log and reports.
1244
- await doScriptOrBatch(options, reportTemplate);
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');