testaro 3.0.1 → 4.1.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.
@@ -3,8 +3,11 @@
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');
10
+ const { handleRequest } = require('./job');
8
11
  // ########## CONSTANTS
9
12
  // Set DEBUG environment variable to 'true' to add debugging features.
10
13
  const debug = process.env.TESTARO_DEBUG === 'true';
@@ -40,6 +43,7 @@ const tests = {
40
43
  role: 'roles',
41
44
  styleDiff: 'style inconsistencies',
42
45
  tabNav: 'keyboard navigation between tab elements',
46
+ tenon: 'Tenon',
43
47
  wave: 'WAVE',
44
48
  zIndex: 'z indexes'
45
49
  };
@@ -55,6 +59,11 @@ const browserTypeNames = {
55
59
  };
56
60
  // Items that may be waited for.
57
61
  const waitables = ['url', 'title', 'body'];
62
+ // Tenon data.
63
+ const tenonData = {
64
+ accessToken: '',
65
+ requestIDs: {}
66
+ };
58
67
  // ########## VARIABLES
59
68
  // Facts about the current session.
60
69
  let logCount = 0;
@@ -359,7 +368,7 @@ const textOf = async (page, element) => {
359
368
  const matchElement = async (page, selector, matchText, index = 0) => {
360
369
  // If the page still exists:
361
370
  if (page) {
362
- // Wait 3 seconds until the body contains any text to be matched.
371
+ // Wait 2 seconds until the body contains any text to be matched.
363
372
  const slimText = debloat(matchText);
364
373
  const bodyText = await page.textContent('body');
365
374
  const slimBody = debloat(bodyText);
@@ -605,17 +614,6 @@ const doActs = async (report, actIndex, page) => {
605
614
  // Identify its only page as current.
606
615
  page = browserContext.pages()[0];
607
616
  }
608
- // Otherwise, if it is a score:
609
- else if (act.type === 'score') {
610
- // Compute and report the score.
611
- try {
612
- const {scorer} = require(`./procs/score/${act.which}`);
613
- act.result = scorer(report.acts);
614
- }
615
- catch (error) {
616
- act.error = `ERROR: ${error.message}\n${error.stack}`;
617
- }
618
- }
619
617
  // Otherwise, if a current page exists:
620
618
  else if (page) {
621
619
  // If the command is a url:
@@ -706,157 +704,125 @@ const doActs = async (report, actIndex, page) => {
706
704
  await require('./procs/test/allVis').allVis(page);
707
705
  act.result = 'All elements visible.';
708
706
  }
709
- // Otherwise, if it is a repetitive keyboard navigation:
710
- else if (act.type === 'presses') {
711
- const {navKey, what, which, withItems} = act;
712
- const matchTexts = which ? which.map(text => debloat(text)) : [];
713
- // Initialize the loop variables.
714
- let status = 'more';
715
- let presses = 0;
716
- let amountRead = 0;
717
- let items = [];
718
- let matchedText;
719
- // As long as a matching element has not been reached:
720
- while (status === 'more') {
721
- // Press the Escape key to dismiss any modal dialog.
722
- await page.keyboard.press('Escape');
723
- // Press the specified navigation key.
724
- await page.keyboard.press(navKey);
725
- presses++;
726
- // Identify the newly current element or a failure.
727
- const currentJSHandle = await page.evaluateHandle(actCount => {
728
- // Initialize it as the focused element.
729
- let currentElement = document.activeElement;
730
- // If it exists in the page:
731
- if (currentElement && currentElement.tagName !== 'BODY') {
732
- // Change it, if necessary, to its active descendant.
733
- if (currentElement.hasAttribute('aria-activedescendant')) {
734
- currentElement = document.getElementById(
735
- currentElement.getAttribute('aria-activedescendant')
736
- );
737
- }
738
- // Or change it, if necessary, to its selected option.
739
- else if (currentElement.tagName === 'SELECT') {
740
- const currentIndex = Math.max(0, currentElement.selectedIndex);
741
- const options = currentElement.querySelectorAll('option');
742
- currentElement = options[currentIndex];
743
- }
744
- // Or change it, if necessary, to its active shadow-DOM element.
745
- else if (currentElement.shadowRoot) {
746
- currentElement = currentElement.shadowRoot.activeElement;
747
- }
748
- // If there is a current element:
749
- if (currentElement) {
750
- // If it was already reached within this command performance:
751
- if (currentElement.dataset.pressesReached === actCount.toString(10)) {
752
- // Report the error.
753
- console.log(`ERROR: ${currentElement.tagName} element reached again`);
754
- status = 'ERROR';
755
- return 'ERROR: locallyExhausted';
756
- }
757
- // Otherwise, i.e. if it is newly reached within this act:
758
- else {
759
- // Mark and return it.
760
- currentElement.dataset.pressesReached = actCount;
761
- return currentElement;
707
+ // Otherwise, if the act is a tenon request:
708
+ else if (act.type === 'tenonRequest') {
709
+ const {id, withNewContent} = act;
710
+ const https = require('https');
711
+ // If a Tenon access token has not yet been obtained:
712
+ if (! tenonData.accessToken) {
713
+ // Authenticate with the Tenon API.
714
+ const authData = await new Promise(resolve => {
715
+ const request = https.request(
716
+ {
717
+ host: 'tenon.io',
718
+ path: '/api/v2/auth',
719
+ port: 443,
720
+ protocol: 'https:',
721
+ method: 'POST',
722
+ headers: {
723
+ 'Content-Type': 'application/json',
724
+ 'Cache-Control': 'no-cache'
762
725
  }
763
- }
764
- // Otherwise, i.e. if there is no current element:
765
- else {
766
- // Report the error.
767
- status = 'ERROR';
768
- return 'noActiveElement';
769
- }
770
- }
771
- // Otherwise, i.e. if there is no focus in the page:
772
- else {
773
- // Report the error.
774
- status = 'ERROR';
775
- return 'ERROR: globallyExhausted';
776
- }
777
- }, actCount);
778
- // If the current element exists:
779
- const currentElement = currentJSHandle.asElement();
780
- if (currentElement) {
781
- // Update the data.
782
- const tagNameJSHandle = await currentElement.getProperty('tagName');
783
- const tagName = await tagNameJSHandle.jsonValue();
784
- const text = await textOf(page, currentElement);
785
- // If the text of the current element was found:
786
- if (text !== null) {
787
- const textLength = text.length;
788
- // If it is non-empty and there are texts to match:
789
- if (matchTexts.length && textLength) {
790
- // Identify the matching text.
791
- matchedText = matchTexts.find(matchText => text.includes(matchText));
792
- }
793
- // Update the item data if required.
794
- if (withItems) {
795
- const itemData = {
796
- tagName,
797
- text,
798
- textLength
799
- };
800
- if (matchedText) {
801
- itemData.matchedText = matchedText;
802
- }
803
- items.push(itemData);
804
- }
805
- amountRead += textLength;
806
- // If there is no text-match failure:
807
- if (matchedText || ! matchTexts.length) {
808
- // If the element has any specified tag name:
809
- if (! what || tagName === what) {
810
- // Change the status.
811
- status = 'done';
812
- // Perform the action.
813
- const inputText = act.text;
814
- if (inputText) {
815
- await page.keyboard.type(inputText);
816
- presses += inputText.length;
726
+ },
727
+ response => {
728
+ let responseData = '';
729
+ response.on('data', chunk => {
730
+ responseData += chunk;
731
+ });
732
+ response.on('end', () => {
733
+ try {
734
+ const responseJSON = JSON.parse(responseData);
735
+ return resolve(responseJSON);
817
736
  }
818
- if (act.action) {
819
- presses++;
820
- await page.keyboard.press(act.action);
821
- await page.waitForLoadState();
737
+ catch(error) {
738
+ return resolve({
739
+ error: 'Tenon did not return JSON authentication data.',
740
+ responseData
741
+ });
822
742
  }
823
- }
743
+ });
824
744
  }
825
- }
826
- else {
827
- status = 'ERROR';
828
- }
745
+ );
746
+ const tenonUser = process.env.TESTARO_TENON_USER;
747
+ const tenonPassword = process.env.TESTARO_TENON_PASSWORD;
748
+ const postData = JSON.stringify({
749
+ username: tenonUser,
750
+ password: tenonPassword
751
+ });
752
+ request.write(postData);
753
+ request.end();
754
+ });
755
+ // If the authentication succeeded:
756
+ if (authData.access_token) {
757
+ // Record the access token.
758
+ tenonData.accessToken = authData.access_token;
829
759
  }
830
- // Otherwise, i.e. if there was a failure:
760
+ // Otherwise, i.e. if the authentication failed:
831
761
  else {
832
- // Update the status.
833
- status = await currentJSHandle.jsonValue();
762
+ console.log('ERROR: tenon authentication failed');
834
763
  }
835
764
  }
836
- // Add the result to the act.
837
- act.result = {
838
- status,
839
- totals: {
840
- presses,
841
- amountRead
765
+ // If a Tenon access token exists:
766
+ if (tenonData.accessToken) {
767
+ // Request a Tenon test of the page and get a response ID.
768
+ const option = {};
769
+ // If Tenon is to be given the URL and not the content of the page:
770
+ if (withNewContent) {
771
+ // Specify this.
772
+ option.url = page.url();
842
773
  }
843
- };
844
- if (status === 'done' && matchedText) {
845
- act.result.matchedText = matchedText;
846
- }
847
- if (withItems) {
848
- act.result.items = items;
774
+ // Otherwise, i.e. if Tenon is to be given the page content:
775
+ else {
776
+ // Specify this.
777
+ option.src = await page.content();
778
+ }
779
+ // Request a Tenon test and get a response ID.
780
+ const responseID = await new Promise(resolve => {
781
+ const request = https.request(
782
+ {
783
+ host: 'tenon.io',
784
+ path: '/api/v2/',
785
+ port: 443,
786
+ protocol: 'https:',
787
+ method: 'POST',
788
+ headers: {
789
+ 'Content-Type': 'application/json',
790
+ 'Cache-Control': 'no-cache',
791
+ Authorization: `Bearer ${tenonData.accessToken}`
792
+ }
793
+ },
794
+ response => {
795
+ let resultJSON = '';
796
+ response.on('data', chunk => {
797
+ resultJSON += chunk;
798
+ });
799
+ // When the data arrive, return them as an object.
800
+ response.on('end', () => {
801
+ try {
802
+ const result = JSON.parse(resultJSON);
803
+ resolve(result.data.responseID || '');
804
+ }
805
+ catch (error) {
806
+ console.log('ERROR: Tenon did not return JSON.');
807
+ resolve('');
808
+ }
809
+ });
810
+ }
811
+ );
812
+ const postData = JSON.stringify(option);
813
+ request.write(postData);
814
+ request.end();
815
+ });
816
+ // Record the response ID.
817
+ tenonData.requestIDs[id] = responseID || '';
849
818
  }
850
- // Add the totals to the report.
851
- report.presses += presses;
852
- report.amountRead += amountRead;
853
819
  }
854
820
  // Otherwise, if the act is a test:
855
821
  else if (act.type === 'test') {
856
822
  // Add a description of the test to the act.
857
823
  act.what = tests[act.which];
858
824
  // Initialize the arguments.
859
- const args = [page];
825
+ const args = [act.which === 'tenon' ? tenonData : page];
860
826
  // Identify the additional validator of the test.
861
827
  const testValidator = commands.tests[act.which];
862
828
  // If it exists:
@@ -1025,6 +991,151 @@ const doActs = async (report, actIndex, page) => {
1025
991
  const qualifier = act.again ? `${1 + act.again} times` : 'once';
1026
992
  act.result = `pressed ${qualifier}`;
1027
993
  }
994
+ // Otherwise, if it is a repetitive keyboard navigation:
995
+ else if (act.type === 'presses') {
996
+ const {navKey, what, which, withItems} = act;
997
+ const matchTexts = which ? which.map(text => debloat(text)) : [];
998
+ // Initialize the loop variables.
999
+ let status = 'more';
1000
+ let presses = 0;
1001
+ let amountRead = 0;
1002
+ let items = [];
1003
+ let matchedText;
1004
+ // As long as a matching element has not been reached:
1005
+ while (status === 'more') {
1006
+ // Press the Escape key to dismiss any modal dialog.
1007
+ await page.keyboard.press('Escape');
1008
+ // Press the specified navigation key.
1009
+ await page.keyboard.press(navKey);
1010
+ presses++;
1011
+ // Identify the newly current element or a failure.
1012
+ const currentJSHandle = await page.evaluateHandle(actCount => {
1013
+ // Initialize it as the focused element.
1014
+ let currentElement = document.activeElement;
1015
+ // If it exists in the page:
1016
+ if (currentElement && currentElement.tagName !== 'BODY') {
1017
+ // Change it, if necessary, to its active descendant.
1018
+ if (currentElement.hasAttribute('aria-activedescendant')) {
1019
+ currentElement = document.getElementById(
1020
+ currentElement.getAttribute('aria-activedescendant')
1021
+ );
1022
+ }
1023
+ // Or change it, if necessary, to its selected option.
1024
+ else if (currentElement.tagName === 'SELECT') {
1025
+ const currentIndex = Math.max(0, currentElement.selectedIndex);
1026
+ const options = currentElement.querySelectorAll('option');
1027
+ currentElement = options[currentIndex];
1028
+ }
1029
+ // Or change it, if necessary, to its active shadow-DOM element.
1030
+ else if (currentElement.shadowRoot) {
1031
+ currentElement = currentElement.shadowRoot.activeElement;
1032
+ }
1033
+ // If there is a current element:
1034
+ if (currentElement) {
1035
+ // If it was already reached within this command performance:
1036
+ if (currentElement.dataset.pressesReached === actCount.toString(10)) {
1037
+ // Report the error.
1038
+ console.log(`ERROR: ${currentElement.tagName} element reached again`);
1039
+ status = 'ERROR';
1040
+ return 'ERROR: locallyExhausted';
1041
+ }
1042
+ // Otherwise, i.e. if it is newly reached within this act:
1043
+ else {
1044
+ // Mark and return it.
1045
+ currentElement.dataset.pressesReached = actCount;
1046
+ return currentElement;
1047
+ }
1048
+ }
1049
+ // Otherwise, i.e. if there is no current element:
1050
+ else {
1051
+ // Report the error.
1052
+ status = 'ERROR';
1053
+ return 'noActiveElement';
1054
+ }
1055
+ }
1056
+ // Otherwise, i.e. if there is no focus in the page:
1057
+ else {
1058
+ // Report the error.
1059
+ status = 'ERROR';
1060
+ return 'ERROR: globallyExhausted';
1061
+ }
1062
+ }, actCount);
1063
+ // If the current element exists:
1064
+ const currentElement = currentJSHandle.asElement();
1065
+ if (currentElement) {
1066
+ // Update the data.
1067
+ const tagNameJSHandle = await currentElement.getProperty('tagName');
1068
+ const tagName = await tagNameJSHandle.jsonValue();
1069
+ const text = await textOf(page, currentElement);
1070
+ // If the text of the current element was found:
1071
+ if (text !== null) {
1072
+ const textLength = text.length;
1073
+ // If it is non-empty and there are texts to match:
1074
+ if (matchTexts.length && textLength) {
1075
+ // Identify the matching text.
1076
+ matchedText = matchTexts.find(matchText => text.includes(matchText));
1077
+ }
1078
+ // Update the item data if required.
1079
+ if (withItems) {
1080
+ const itemData = {
1081
+ tagName,
1082
+ text,
1083
+ textLength
1084
+ };
1085
+ if (matchedText) {
1086
+ itemData.matchedText = matchedText;
1087
+ }
1088
+ items.push(itemData);
1089
+ }
1090
+ amountRead += textLength;
1091
+ // If there is no text-match failure:
1092
+ if (matchedText || ! matchTexts.length) {
1093
+ // If the element has any specified tag name:
1094
+ if (! what || tagName === what) {
1095
+ // Change the status.
1096
+ status = 'done';
1097
+ // Perform the action.
1098
+ const inputText = act.text;
1099
+ if (inputText) {
1100
+ await page.keyboard.type(inputText);
1101
+ presses += inputText.length;
1102
+ }
1103
+ if (act.action) {
1104
+ presses++;
1105
+ await page.keyboard.press(act.action);
1106
+ await page.waitForLoadState();
1107
+ }
1108
+ }
1109
+ }
1110
+ }
1111
+ else {
1112
+ status = 'ERROR';
1113
+ }
1114
+ }
1115
+ // Otherwise, i.e. if there was a failure:
1116
+ else {
1117
+ // Update the status.
1118
+ status = await currentJSHandle.jsonValue();
1119
+ }
1120
+ }
1121
+ // Add the result to the act.
1122
+ act.result = {
1123
+ status,
1124
+ totals: {
1125
+ presses,
1126
+ amountRead
1127
+ }
1128
+ };
1129
+ if (status === 'done' && matchedText) {
1130
+ act.result.matchedText = matchedText;
1131
+ }
1132
+ if (withItems) {
1133
+ act.result.items = items;
1134
+ }
1135
+ // Add the totals to the report.
1136
+ report.presses += presses;
1137
+ report.amountRead += amountRead;
1138
+ }
1028
1139
  // Otherwise, i.e. if the act type is unknown:
1029
1140
  else {
1030
1141
  // Add the error result to the act.
@@ -1079,25 +1190,6 @@ const doScript = async (report) => {
1079
1190
  report.prohibitedCount = prohibitedCount;
1080
1191
  report.visitTimeoutCount = visitTimeoutCount;
1081
1192
  report.visitRejectionCount = visitRejectionCount;
1082
- // If logs are to be scored, do so.
1083
- const scoreTables = report.acts.filter(act => act.type === 'score');
1084
- if (scoreTables.length) {
1085
- const scoreTable = scoreTables[0];
1086
- const {result} = scoreTable;
1087
- if (result) {
1088
- const {logWeights, scores} = result;
1089
- if (logWeights && scores) {
1090
- scores.log = Math.floor(
1091
- logWeights.count * logCount
1092
- + logWeights.size * logSize
1093
- + logWeights.prohibited * prohibitedCount
1094
- + logWeights.visitTimeout * visitTimeoutCount
1095
- + logWeights.visitRejection * visitRejectionCount
1096
- );
1097
- scores.total += scores.log;
1098
- }
1099
- }
1100
- }
1101
1193
  // Add the end time and duration to the report.
1102
1194
  const endTime = new Date();
1103
1195
  report.endTime = endTime.toISOString().slice(0, 19);
@@ -1164,3 +1256,7 @@ exports.handleRequest = async report => {
1164
1256
  console.log('ERROR: options missing or invalid');
1165
1257
  }
1166
1258
  };
1259
+
1260
+ // ########## OPERATION
1261
+
1262
+ handleRequest(process.argv[2]);
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "weborgs",
3
+ "what": "Web organizations",
4
+ "hosts": [
5
+ {
6
+ "id": "mozilla",
7
+ "which": "https://www.mozilla.org/en-US/",
8
+ "what": "Mozilla"
9
+ },
10
+ {
11
+ "id": "w3c",
12
+ "which": "https://www.w3.org/",
13
+ "what": "W3C"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "what": "Test Wikipedia with tenon",
3
+ "strict": true,
4
+ "commands": [
5
+ {
6
+ "type": "launch",
7
+ "which": "chromium",
8
+ "what": "Chromium browser"
9
+ },
10
+ {
11
+ "type": "url",
12
+ "which": "https://en.wikipedia.org/wiki/Main_Page",
13
+ "what": "Wikipedia English home page"
14
+ },
15
+ {
16
+ "type": "tenonRequest",
17
+ "withNewContent": true,
18
+ "id": "a",
19
+ "what": "tenon request"
20
+ },
21
+ {
22
+ "type": "test",
23
+ "which": "tenon",
24
+ "id": "a",
25
+ "what": "result of prior tenon request"
26
+ }
27
+ ]
28
+ }
package/tests/tenon.js ADDED
@@ -0,0 +1,123 @@
1
+ /*
2
+ tenon
3
+ This test processes a previously requested test by the Tenon API.
4
+ */
5
+ const https = require('https');
6
+ // Wait until a time limit in seconds expires.
7
+ const wait = timeLimit => new Promise(resolve => setTimeout(resolve, 1000 * timeLimit));
8
+ exports.reporter = async (tenonData, id) => {
9
+ if (tenonData && tenonData.accessToken && tenonData.requestIDs && tenonData.requestIDs[id]) {
10
+ // Shared request options.
11
+ const requestOptions = {
12
+ host: 'tenon.io',
13
+ path: `/api/v2/${tenonData.requestIDs[id]}`,
14
+ port: 443,
15
+ protocol: 'https:',
16
+ headers: {
17
+ Authorization: `Bearer ${tenonData.accessToken}`,
18
+ 'Cache-Control': 'no-cache'
19
+ }
20
+ };
21
+ // Gets the test status.
22
+ const getStatus = async () => {
23
+ const testStatus = await new Promise((resolve, reject) => {
24
+ requestOptions.method = 'HEAD';
25
+ const statusRequest = https.request(requestOptions, statusResponse => {
26
+ const {statusCode} = statusResponse;
27
+ resolve(statusCode);
28
+ });
29
+ statusRequest.on('error', error => {
30
+ console.log(`ERROR getting Tenon test status (${error.message})`);
31
+ reject(`ERROR getting Tenon test status (${error.message})`);
32
+ });
33
+ statusRequest.end();
34
+ });
35
+ return testStatus;
36
+ };
37
+ // Gets the test result.
38
+ const getResult = async () => {
39
+ const testResult = await new Promise(resolve => {
40
+ requestOptions.method = 'GET';
41
+ const resultRequest = https.request(requestOptions, resultResponse => {
42
+ let resultJSON = '';
43
+ resultResponse.on('data', chunk => {
44
+ resultJSON += chunk;
45
+ });
46
+ resultResponse.on('end', () => {
47
+ try {
48
+ const result = JSON.parse(resultJSON);
49
+ resolve(result);
50
+ }
51
+ catch(error) {
52
+ console.log(`ERROR getting Tenon test result (${resultJSON.slice(0, 80)} …)`);
53
+ resolve({
54
+ error: 'ERROR getting Tenon test result',
55
+ resultStart: resultJSON.slice(0, 80)
56
+ });
57
+ }
58
+ });
59
+ });
60
+ resultRequest.end();
61
+ });
62
+ return testResult;
63
+ };
64
+ // Get the test status (not reliable: may say 200 instead of 202).
65
+ let testStatus = await getStatus();
66
+ // If the test is still in the Tenon queue:
67
+ if (testStatus === 202) {
68
+ // Wait 5 seconds.
69
+ await wait(5);
70
+ // Get the test status again.
71
+ testStatus = await getStatus();
72
+ }
73
+ // If the test has allegedly been completed:
74
+ if (testStatus === 200) {
75
+ // Get the test result.
76
+ let testResult = await getResult();
77
+ // If the test is still in the Tenon queue:
78
+ let {status} = testResult;
79
+ if (status === 202) {
80
+ // Wait 5 seconds.
81
+ await wait(5);
82
+ // Get the test result again.
83
+ testResult = await getResult();
84
+ // If the test is still in the Tenon queue:
85
+ status = testResult.status;
86
+ if (status === 202) {
87
+ // Wait 15 more seconds.
88
+ await wait(15);
89
+ // Get the test result again.
90
+ testResult = await getResult();
91
+ status = testResult.status;
92
+ }
93
+ }
94
+ // If the test has really been completed:
95
+ if (status === 200) {
96
+ // Return its result.
97
+ return {result: testResult};
98
+ }
99
+ // Otherwise, i.e. if the test is still running or failed:
100
+ else {
101
+ return {result: {
102
+ error: 'ERROR: Tenon result not retrieved',
103
+ status
104
+ }};
105
+ }
106
+ }
107
+ // Otherwise, if the test is still running after a wait for its status:
108
+ else {
109
+ // Report the test status.
110
+ return {result: {
111
+ error: 'ERROR: test status not completed',
112
+ testStatus
113
+ }};
114
+ }
115
+ }
116
+ else {
117
+ return {
118
+ result: {
119
+ error: 'ERROR: tenon authorization and test data incomplete'
120
+ }
121
+ };
122
+ }
123
+ };