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.
- package/README.md +142 -73
- package/commands.js +14 -7
- package/job.js +92 -0
- package/package.json +2 -1
- package/{index.js → run.js} +262 -166
- package/samples/batches/weborgs.json +16 -0
- package/samples/scripts/tenon.json +28 -0
- package/tests/tenon.js +123 -0
- package/validation/executors/tenon.js +18 -0
- package/scoring/correlation.js +0 -76
- package/scoring/correlations.json +0 -327
- package/scoring/data.json +0 -26021
- package/scoring/dupCounts.js +0 -39
- package/scoring/dupCounts.json +0 -112
- package/scoring/duplications.json +0 -253
- package/scoring/issues.json +0 -304
- package/scoring/packageData.js +0 -171
- package/scoring/packageIssues.js +0 -34
- package/scoring/rulesetData.json +0 -15
package/{index.js → run.js}
RENAMED
|
@@ -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
|
|
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
|
|
710
|
-
else if (act.type === '
|
|
711
|
-
const {
|
|
712
|
-
const
|
|
713
|
-
//
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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
|
-
|
|
827
|
-
|
|
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
|
|
760
|
+
// Otherwise, i.e. if the authentication failed:
|
|
831
761
|
else {
|
|
832
|
-
|
|
833
|
-
status = await currentJSHandle.jsonValue();
|
|
762
|
+
console.log('ERROR: tenon authentication failed');
|
|
834
763
|
}
|
|
835
764
|
}
|
|
836
|
-
//
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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,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
|
+
};
|