@jrpool/kilotest 46.0.4 → 46.0.6

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.
@@ -0,0 +1,62 @@
1
+ /*
2
+ getReport.test.js
3
+ Tests for api/getReport.js using the fixture corpus.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ const {test, before, after} = require('node:test');
9
+ const assert = require('node:assert/strict');
10
+ const path = require('node:path');
11
+ const {response} = require('./getReport');
12
+
13
+ // SETUP AND TEARDOWN
14
+
15
+ const savedDBDir = process.env.DB_DIR;
16
+
17
+ before(() => {
18
+ process.env.DB_DIR = path.join(__dirname, '..', 'test', 'fixtures', 'db');
19
+ });
20
+
21
+ after(() => {
22
+ if (savedDBDir !== undefined) {
23
+ process.env.DB_DIR = savedDBDir;
24
+ }
25
+ else {
26
+ delete process.env.DB_DIR;
27
+ }
28
+ });
29
+
30
+ // TESTS
31
+
32
+ test('getReport returns the full report for a valid report', async () => {
33
+ const body = await response(['260101T0000', 'mix']);
34
+ const content = body['response content'];
35
+ assert.equal(typeof content['size of the report in bytes'], 'number');
36
+ assert.ok(content['size of the report in bytes'] > 0);
37
+ const report = content['full report'];
38
+ assert.equal(report.id, '260101T0000-mix');
39
+ assert.equal(report.target.what, 'Mixed Outcomes Page');
40
+ assert.equal(report.target.url, 'https://example.com/mixed');
41
+ assert.equal(report.acts.length, 2);
42
+ });
43
+
44
+ test('getReport returns an error for a nonexistent report', async () => {
45
+ const body = await response(['999999T9999', 'xyz']);
46
+ const content = body['response content'];
47
+ assert.equal(content['full report'], null);
48
+ assert.equal(typeof content['size of the report in bytes'], 'string');
49
+ });
50
+
51
+ test('getReport returns an error for the hidden report', async () => {
52
+ const body = await response(['260101T0007', 'hid']);
53
+ const content = body['response content'];
54
+ assert.equal(content['full report'], null);
55
+ assert.equal(typeof content['size of the report in bytes'], 'string');
56
+ });
57
+
58
+ test('getReport includes tool name and metadata', async () => {
59
+ const body = await response(['260101T0000', 'mix']);
60
+ assert.equal(body['tool name'], 'getReport');
61
+ assert.ok(body['response metadata'].identifier);
62
+ });
@@ -0,0 +1,88 @@
1
+ /*
2
+ listDiagnoses.test.js
3
+ Tests for api/listDiagnoses.js using the fixture corpus, with emphasis on outcome handling.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ const {test, before, after} = require('node:test');
9
+ const assert = require('node:assert/strict');
10
+ const path = require('node:path');
11
+ const {response} = require('./listDiagnoses');
12
+
13
+ // SETUP AND TEARDOWN
14
+
15
+ const savedDBDir = process.env.DB_DIR;
16
+
17
+ before(() => {
18
+ process.env.DB_DIR = path.join(__dirname, '..', 'test', 'fixtures', 'db');
19
+ });
20
+
21
+ after(() => {
22
+ if (savedDBDir !== undefined) {
23
+ process.env.DB_DIR = savedDBDir;
24
+ }
25
+ else {
26
+ delete process.env.DB_DIR;
27
+ }
28
+ });
29
+
30
+ // TESTS
31
+
32
+ test('listDiagnoses returns 2 diagnoses for catalogIndex 0, linkNoText, in the mixed report (axe and alfa, excluding cantTell)', async () => {
33
+ const body = await response(['0', 'linkNoText', '260101T0000', 'mix']);
34
+ const diagnoses = body['response content']['diagnoses of how the element exhibited the issue'];
35
+ assert.equal(diagnoses.length, 2);
36
+ const rules = diagnoses.map(d => d['identifier of the violated rule']).sort();
37
+ assert.deepEqual(rules, ['r11', 'r11']);
38
+ const descriptions = diagnoses.map(d => d['description of the violation']);
39
+ assert.ok(descriptions.every(d => d === 'The link does not have an accessible name'));
40
+ });
41
+
42
+ test('listDiagnoses returns 0 diagnoses when all instances are cantTell', async () => {
43
+ const body = await response(['0', 'focusIndicationBad', '260101T0001', 'ct']);
44
+ const diagnoses = body['response content']['diagnoses of how the element exhibited the issue'];
45
+ assert.equal(diagnoses.length, 0);
46
+ });
47
+
48
+ test('listDiagnoses treats missing outcome as a violation', async () => {
49
+ const body = await response(['0', 'linkNoText', '260101T0002', 'no']);
50
+ const diagnoses = body['response content']['diagnoses of how the element exhibited the issue'];
51
+ assert.equal(diagnoses.length, 1);
52
+ assert.equal(diagnoses[0]['description of the violation'], 'Button has no accessible name');
53
+ assert.equal(diagnoses[0]['severity of the violation on a 0-to-3 scale'], 3);
54
+ });
55
+
56
+ test('listDiagnoses returns element basics from the catalog', async () => {
57
+ const body = await response(['0', 'linkNoText', '260101T0000', 'mix']);
58
+ const elementBasics = body['response content']['basics about the element'];
59
+ assert.equal(elementBasics.identifier, '0');
60
+ assert.equal(elementBasics['tag name'], 'A');
61
+ assert.equal(elementBasics['inner text'], 'About Us');
62
+ });
63
+
64
+ test('listDiagnoses returns element details from the catalog', async () => {
65
+ const body = await response(['0', 'linkNoText', '260101T0000', 'mix']);
66
+ const elementDetails = body['response content']['details about the element'];
67
+ assert.equal(elementDetails['start tag'], '<a>');
68
+ assert.equal(elementDetails['XPath'], '/html/body/a[1]');
69
+ assert.equal(elementDetails['x, y, width, and height of bounding box'], '10:20:80:30');
70
+ });
71
+
72
+ test('listDiagnoses returns an error for an unknown issue ID', async () => {
73
+ const body = await response(['0', 'nonexistentIssue', '260101T0000', 'mix']);
74
+ const basics = body['response content']['basics about the issue'];
75
+ assert.ok(basics.error);
76
+ });
77
+
78
+ test('listDiagnoses returns an error for a nonexistent catalog index', async () => {
79
+ const body = await response(['999', 'linkNoText', '260101T0000', 'mix']);
80
+ const basics = body['response content']['basics about the element'];
81
+ assert.ok(basics.error);
82
+ });
83
+
84
+ test('listDiagnoses returns an error for a nonexistent report', async () => {
85
+ const body = await response(['0', 'linkNoText', '999999T9999', 'xyz']);
86
+ const basics = body['response content']['basics about the report'];
87
+ assert.ok(basics.error);
88
+ });
@@ -0,0 +1,98 @@
1
+ /*
2
+ listIssues.test.js
3
+ Tests for api/listIssues.js using the fixture corpus, with emphasis on outcome handling.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ const {test, before, after} = require('node:test');
9
+ const assert = require('node:assert/strict');
10
+ const path = require('node:path');
11
+ const {response} = require('./listIssues');
12
+
13
+ // SETUP AND TEARDOWN
14
+
15
+ const savedDBDir = process.env.DB_DIR;
16
+
17
+ before(() => {
18
+ process.env.DB_DIR = path.join(__dirname, '..', 'test', 'fixtures', 'db');
19
+ });
20
+
21
+ after(() => {
22
+ if (savedDBDir !== undefined) {
23
+ process.env.DB_DIR = savedDBDir;
24
+ }
25
+ else {
26
+ delete process.env.DB_DIR;
27
+ }
28
+ });
29
+
30
+ // TESTS
31
+
32
+ test('listIssues returns 2 issues for the mixed-outcomes report, excluding cantTell', async () => {
33
+ const body = await response(['260101T0000', 'mix']);
34
+ const issues = body['response content']['basics about all issues reported in the report'];
35
+ assert.equal(issues.length, 2);
36
+ const summaries = issues.map(i => i.summary).sort();
37
+ assert.deepEqual(summaries, ['all-capital text', 'link not named']);
38
+ });
39
+
40
+ test('listIssues returns 0 issues for the all-cantTell report', async () => {
41
+ const body = await response(['260101T0001', 'ct']);
42
+ const issues = body['response content']['basics about all issues reported in the report'];
43
+ assert.equal(issues.length, 0);
44
+ });
45
+
46
+ test('listIssues treats missing outcome as a violation, not cantTell', async () => {
47
+ const body = await response(['260101T0002', 'no']);
48
+ const issues = body['response content']['basics about all issues reported in the report'];
49
+ assert.equal(issues.length, 1);
50
+ assert.equal(issues[0].identifier, 'linkNoText');
51
+ });
52
+
53
+ test('listIssues returns 0 issues for the empty report', async () => {
54
+ const body = await response(['260101T0005', 'emp']);
55
+ const issues = body['response content']['basics about all issues reported in the report'];
56
+ assert.equal(issues.length, 0);
57
+ });
58
+
59
+ test('listIssues reports prevented rule engines for the prevented report', async () => {
60
+ const body = await response(['260101T0006', 'prv']);
61
+ const details = body['response content']['details about the report'];
62
+ const preventions = details['test results']['rule engines that could not test the page'];
63
+ assert.equal(preventions.length, 1);
64
+ assert.equal(preventions[0].name, 'Alfa');
65
+ assert.equal(preventions[0]['reason for failure'], 'page timed out');
66
+ });
67
+
68
+ test('listIssues includes reporter names for each issue in the mixed report', async () => {
69
+ const body = await response(['260101T0000', 'mix']);
70
+ const issues = body['response content']['basics about all issues reported in the report'];
71
+ const linkIssue = issues.find(i => i.identifier === 'linkNoText');
72
+ assert.deepEqual(linkIssue['rule engines with any violations belonging to the issue'], ['Alfa', 'Axe']);
73
+ const allCapsIssue = issues.find(i => i.identifier === 'allCaps');
74
+ assert.deepEqual(allCapsIssue['rule engines with any violations belonging to the issue'], ['Alfa']);
75
+ });
76
+
77
+ test('listIssues counts issues by priority correctly for the mixed report', async () => {
78
+ const body = await response(['260101T0000', 'mix']);
79
+ const details = body['response content']['details about the report'];
80
+ const counts = details['test results']['counts of issues by priority'];
81
+ // linkNoText weight 4 (highest), allCaps weight 1 (lowest).
82
+ assert.equal(counts.highest, 1);
83
+ assert.equal(counts.high, 0);
84
+ assert.equal(counts.low, 0);
85
+ assert.equal(counts.lowest, 1);
86
+ });
87
+
88
+ test('listIssues reports the superseded status for the mixed report', async () => {
89
+ const body = await response(['260101T0000', 'mix']);
90
+ const basics = body['response content']['basics about the report'];
91
+ assert.equal(basics['whether a later report about the same page exists'], true);
92
+ });
93
+
94
+ test('listIssues returns an error for a nonexistent report', async () => {
95
+ const body = await response(['999999T9999', 'xyz']);
96
+ const basics = body['response content']['basics about the report'];
97
+ assert.ok(basics.error);
98
+ });
@@ -0,0 +1,104 @@
1
+ /*
2
+ listReports.test.js
3
+ Tests for api/listReports.js using the fixture corpus.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ const {test, before, after} = require('node:test');
9
+ const assert = require('node:assert/strict');
10
+ const path = require('node:path');
11
+ const {response} = require('./listReports');
12
+
13
+ // CONSTANTS
14
+
15
+ // Fixture report identifiers in the reports/ directory (excluding the hidden one).
16
+ const fixtureIds = [
17
+ ['260101T0000', 'mix'],
18
+ ['260101T0001', 'ct'],
19
+ ['260101T0002', 'no'],
20
+ ['260101T0005', 'emp'],
21
+ ['260101T0006', 'prv'],
22
+ ['260202T0000', 'new']
23
+ ];
24
+
25
+ // SETUP AND TEARDOWN
26
+
27
+ const savedDBDir = process.env.DB_DIR;
28
+
29
+ before(() => {
30
+ process.env.DB_DIR = path.join(__dirname, '..', 'test', 'fixtures', 'db');
31
+ });
32
+
33
+ after(() => {
34
+ if (savedDBDir !== undefined) {
35
+ process.env.DB_DIR = savedDBDir;
36
+ }
37
+ else {
38
+ delete process.env.DB_DIR;
39
+ }
40
+ });
41
+
42
+ // TESTS
43
+
44
+ test('listReports returns basics about all 6 non-hidden reports', async () => {
45
+ const body = await response();
46
+ const reportsBasics = body['response content']['basics about all available reports'];
47
+ assert.equal(reportsBasics.length, 6);
48
+ const ids = reportsBasics.map(b => b.identifier).sort();
49
+ const expectedIds = fixtureIds.map(([ts, jid]) => `${ts}-${jid}`).sort();
50
+ assert.deepEqual(ids, expectedIds);
51
+ });
52
+
53
+ test('listReports does not include the hidden report', async () => {
54
+ const body = await response();
55
+ const reportsBasics = body['response content']['basics about all available reports'];
56
+ const ids = reportsBasics.map(b => b.identifier);
57
+ assert.ok(!ids.includes('260101T0007-hid'), 'hidden report must not appear in listReports');
58
+ });
59
+
60
+ test('listReports sorts reports by page description and then by completion time', async () => {
61
+ const body = await response();
62
+ const reportsBasics = body['response content']['basics about all available reports'];
63
+ const descriptions = reportsBasics.map(b => b['tested web page'].description);
64
+ // All CantTell Page, Empty Results Page, Mixed Outcomes Page, Mixed Outcomes Page, No Outcomes Page, Prevented Page.
65
+ assert.deepEqual(
66
+ descriptions,
67
+ [
68
+ 'All CantTell Page',
69
+ 'Empty Results Page',
70
+ 'Mixed Outcomes Page',
71
+ 'Mixed Outcomes Page',
72
+ 'No Outcomes Page',
73
+ 'Prevented Page'
74
+ ]
75
+ );
76
+ // The two Mixed Outcomes Page reports should be sorted by completion time (older first).
77
+ const mixedReports = reportsBasics.filter(b => b['tested web page'].description === 'Mixed Outcomes Page');
78
+ assert.equal(mixedReports[0].identifier, '260101T0000-mix');
79
+ assert.equal(mixedReports[1].identifier, '260202T0000-new');
80
+ });
81
+
82
+ test('listReports marks the superseded report correctly', async () => {
83
+ const body = await response();
84
+ const reportsBasics = body['response content']['basics about all available reports'];
85
+ const mix = reportsBasics.find(b => b.identifier === '260101T0000-mix');
86
+ const newer = reportsBasics.find(b => b.identifier === '260202T0000-new');
87
+ assert.equal(mix['whether a later report about the same page exists'], true);
88
+ assert.equal(newer['whether a later report about the same page exists'], false);
89
+ });
90
+
91
+ test('listReports includes request-test instructions', async () => {
92
+ const body = await response();
93
+ const content = body['response content'];
94
+ assert.ok(content['how to request that a page with no report be tested']);
95
+ assert.ok(content['how a web user can request that the page be tested']);
96
+ assert.equal(content['how to request that a page with no report be tested'].method, 'POST');
97
+ });
98
+
99
+ test('listReports includes tool name and metadata', async () => {
100
+ const body = await response();
101
+ assert.equal(body['tool name'], 'listReports');
102
+ assert.ok(body['response metadata'].identifier);
103
+ assert.ok(body['response metadata']['date and time']);
104
+ });
@@ -0,0 +1,73 @@
1
+ /*
2
+ listViolators.test.js
3
+ Tests for api/listViolators.js using the fixture corpus, with emphasis on outcome handling.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ const {test, before, after} = require('node:test');
9
+ const assert = require('node:assert/strict');
10
+ const path = require('node:path');
11
+ const {response} = require('./listViolators');
12
+
13
+ // SETUP AND TEARDOWN
14
+
15
+ const savedDBDir = process.env.DB_DIR;
16
+
17
+ before(() => {
18
+ process.env.DB_DIR = path.join(__dirname, '..', 'test', 'fixtures', 'db');
19
+ });
20
+
21
+ after(() => {
22
+ if (savedDBDir !== undefined) {
23
+ process.env.DB_DIR = savedDBDir;
24
+ }
25
+ else {
26
+ delete process.env.DB_DIR;
27
+ }
28
+ });
29
+
30
+ // TESTS
31
+
32
+ test('listViolators returns 1 violator for linkNoText in the mixed report, excluding cantTell', async () => {
33
+ const body = await response(['linkNoText', '260101T0000', 'mix']);
34
+ const violators = body['response content']['basics about all elements exhibiting the issue'];
35
+ assert.equal(violators.length, 1);
36
+ assert.equal(violators[0].identifier, '0');
37
+ assert.equal(violators[0]['tag name'], 'A');
38
+ assert.equal(violators[0]['inner text'], 'About Us');
39
+ assert.equal(violators[0]['count of rule engines reporting that the element exhibited the issue'], 2);
40
+ });
41
+
42
+ test('listViolators returns 0 violators for an issue when all instances are cantTell', async () => {
43
+ const body = await response(['focusIndicationBad', '260101T0001', 'ct']);
44
+ const violators = body['response content']['basics about all elements exhibiting the issue'];
45
+ assert.equal(violators.length, 0);
46
+ });
47
+
48
+ test('listViolators treats missing outcome as a violation', async () => {
49
+ const body = await response(['linkNoText', '260101T0002', 'no']);
50
+ const violators = body['response content']['basics about all elements exhibiting the issue'];
51
+ assert.equal(violators.length, 1);
52
+ assert.equal(violators[0].identifier, '0');
53
+ });
54
+
55
+ test('listViolators returns an error for an unknown issue ID', async () => {
56
+ const body = await response(['nonexistentIssue', '260101T0000', 'mix']);
57
+ const basics = body['response content']['basics about the issue'];
58
+ assert.ok(basics.error);
59
+ });
60
+
61
+ test('listViolators returns an error for a nonexistent report', async () => {
62
+ const body = await response(['linkNoText', '999999T9999', 'xyz']);
63
+ const basics = body['response content']['basics about the report'];
64
+ assert.ok(basics.error);
65
+ });
66
+
67
+ test('listViolators includes reporter facts for the issue', async () => {
68
+ const body = await response(['linkNoText', '260101T0000', 'mix']);
69
+ const details = body['response content']['details about the issue'];
70
+ const reporters = details['rule engines reporting violations belonging to the issue'];
71
+ const names = reporters.map(r => r.name).sort();
72
+ assert.deepEqual(names, ['Alfa', 'Axe']);
73
+ });
@@ -0,0 +1,54 @@
1
+ /*
2
+ requestFeature.test.js
3
+ Tests for api/requestFeature.js with mocked side effects.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ const {test, before, after} = require('node:test');
9
+ const assert = require('node:assert/strict');
10
+ const alerts = require('../alerts');
11
+
12
+ // SETUP AND TEARDOWN
13
+
14
+ let sendAlertCalls = [];
15
+
16
+ before(() => {
17
+ sendAlertCalls = [];
18
+ // @ts-expect-error: Replacing the real function with a mock for testing.
19
+ alerts.sendAlert = async (subject, body) => {
20
+ sendAlertCalls.push({subject, body});
21
+ };
22
+ });
23
+
24
+ // Require requestFeature after the mock is in place, so it captures the mocked sendAlert.
25
+ const {response} = require('./requestFeature');
26
+
27
+ after(() => {
28
+ });
29
+
30
+ // TESTS
31
+
32
+ test('requestFeature rejects an empty feature request', async () => {
33
+ const body = await response(['']);
34
+ const details = body['response content']['details about your request'];
35
+ assert.ok(details.error);
36
+ assert.equal(sendAlertCalls.length, 0);
37
+ });
38
+
39
+ test('requestFeature accepts a non-empty feature request and notifies the manager', async () => {
40
+ const body = await response(['Add a dark mode toggle']);
41
+ const details = body['response content']['details about your request'];
42
+ assert.equal(details.error, undefined);
43
+ assert.ok(details['date and time received']);
44
+ assert.equal(details.disposition, 'received and logged; manager notified');
45
+ assert.equal(sendAlertCalls.length, 1);
46
+ assert.equal(sendAlertCalls[0].subject, 'MCP feature request received');
47
+ assert.equal(sendAlertCalls[0].body, 'Add a dark mode toggle');
48
+ });
49
+
50
+ test('requestFeature includes tool name and metadata', async () => {
51
+ const body = await response(['Some feature']);
52
+ assert.equal(body['tool name'], 'requestFeature');
53
+ assert.ok(body['response metadata'].identifier);
54
+ });
@@ -6,7 +6,7 @@
6
6
  // IMPORTS
7
7
 
8
8
  const {getResponseMetadata, getToolsFacts, processTestRequest, thisHost} = require('./util');
9
- const {getLatestReportExtracts, getReportsExtract} = require('../util');
9
+ const {getLatestReportExtracts, getReportExtract} = require('../util');
10
10
 
11
11
  // FUNCTIONS
12
12
 
@@ -19,15 +19,11 @@ exports.response = async args => {
19
19
  'disposition of your request': null
20
20
  };
21
21
  const reasonLength = reason.length;
22
- // Get data on the available reports.
23
- const reportsExtract = await getReportsExtract();
24
22
  // Get data on the report.
25
- const reportExtract = Object
26
- .values(reportsExtract)
27
- .find(extract => extract.timeStamp === timeStamp && extract.jobID === jobID);
28
- const {what, url} = reportExtract || {};
23
+ const reportExtract = await getReportExtract(timeStamp, jobID);
24
+ const {what, url} = reportExtract.error ? {} : reportExtract;
29
25
  // If this failed:
30
- if (!reportExtract) {
26
+ if (reportExtract.error) {
31
27
  // Add this to the response content.
32
28
  responseContent['details about your request'] = {
33
29
  error: 'request invalid: the specified existing report is not an available report'
@@ -0,0 +1,76 @@
1
+ /*
2
+ requestRetest.test.js
3
+ Tests for api/requestRetest.js using the fixture corpus, with mocked side effects.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ const {test, before, after} = require('node:test');
9
+ const assert = require('node:assert/strict');
10
+ const path = require('node:path');
11
+ const apiUtil = require('./util');
12
+
13
+ // SETUP AND TEARDOWN
14
+
15
+ const savedDBDir = process.env.DB_DIR;
16
+ let processTestRequestCalls = [];
17
+
18
+ before(() => {
19
+ process.env.DB_DIR = path.join(__dirname, '..', 'test', 'fixtures', 'db');
20
+ processTestRequestCalls = [];
21
+ // @ts-expect-error: Replacing the real function with a mock for testing.
22
+ apiUtil.processTestRequest = async (testType, what, url, reason) => {
23
+ processTestRequestCalls.push({testType, what, url, reason});
24
+ };
25
+ });
26
+
27
+ // Require requestRetest after the mock is in place, so it captures the mocked processTestRequest.
28
+ const {response} = require('./requestRetest');
29
+
30
+ after(() => {
31
+ if (savedDBDir !== undefined) {
32
+ process.env.DB_DIR = savedDBDir;
33
+ }
34
+ else {
35
+ delete process.env.DB_DIR;
36
+ }
37
+ });
38
+
39
+ // TESTS
40
+
41
+ test('requestRetest rejects a nonexistent report', async () => {
42
+ const body = await response(['999999T9999', 'xyz', 'A reason that is long enough.']);
43
+ assert.ok(body['response content']['details about your request'].error);
44
+ assert.equal(processTestRequestCalls.length, 0);
45
+ });
46
+
47
+ test('requestRetest rejects a superseded report', async () => {
48
+ const body = await response(['260101T0000', 'mix', 'A reason that is long enough.']);
49
+ const details = body['response content']['details about your request'];
50
+ assert.ok(details.error.includes('later report'));
51
+ assert.equal(processTestRequestCalls.length, 0);
52
+ });
53
+
54
+ test('requestRetest rejects a reason shorter than 20 characters', async () => {
55
+ const body = await response(['260101T0001', 'ct', 'short']);
56
+ const details = body['response content']['details about your request'];
57
+ assert.ok(details.error.includes('reason'));
58
+ assert.equal(processTestRequestCalls.length, 0);
59
+ });
60
+
61
+ test('requestRetest rejects a reason longer than 100 characters', async () => {
62
+ const longReason = 'x'.repeat(101);
63
+ const body = await response(['260101T0001', 'ct', longReason]);
64
+ const details = body['response content']['details about your request'];
65
+ assert.ok(details.error.includes('reason'));
66
+ assert.equal(processTestRequestCalls.length, 0);
67
+ });
68
+
69
+ test('requestRetest accepts a valid retest request for the latest report of a page', async () => {
70
+ const body = await response(['260202T0000', 'new', 'A reason that is long enough.']);
71
+ const details = body['response content']['details about your request'];
72
+ assert.equal(details.error, undefined);
73
+ assert.equal(details['page to be retested'].description, 'Mixed Outcomes Page');
74
+ assert.equal(processTestRequestCalls.length, 1);
75
+ assert.equal(processTestRequestCalls[0].testType, 'retest');
76
+ });
@@ -0,0 +1,81 @@
1
+ /*
2
+ requestTest.test.js
3
+ Tests for api/requestTest.js using the fixture corpus, with mocked side effects.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ const {test, before, after} = require('node:test');
9
+ const assert = require('node:assert/strict');
10
+ const path = require('node:path');
11
+ const apiUtil = require('./util');
12
+
13
+ // SETUP AND TEARDOWN
14
+
15
+ const savedDBDir = process.env.DB_DIR;
16
+ let processTestRequestCalls = [];
17
+
18
+ before(() => {
19
+ process.env.DB_DIR = path.join(__dirname, '..', 'test', 'fixtures', 'db');
20
+ processTestRequestCalls = [];
21
+ // @ts-expect-error: Replacing the real function with a mock for testing.
22
+ apiUtil.processTestRequest = async (testType, what, url, reason) => {
23
+ processTestRequestCalls.push({testType, what, url, reason});
24
+ };
25
+ });
26
+
27
+ // Require requestTest after the mock is in place, so it captures the mocked processTestRequest.
28
+ const {response} = require('./requestTest');
29
+
30
+ after(() => {
31
+ if (savedDBDir !== undefined) {
32
+ process.env.DB_DIR = savedDBDir;
33
+ }
34
+ else {
35
+ delete process.env.DB_DIR;
36
+ }
37
+ });
38
+
39
+ // TESTS
40
+
41
+ test('requestTest rejects an empty description', async () => {
42
+ const body = await response(['', 'https://example.com/test', 'A reason that is long enough.']);
43
+ assert.ok(body['response content']['details about your request'].error);
44
+ assert.equal(processTestRequestCalls.length, 0);
45
+ });
46
+
47
+ test('requestTest rejects a description longer than 100 characters', async () => {
48
+ const longWhat = 'x'.repeat(101);
49
+ const body = await response([longWhat, 'https://example.com/test', 'A reason that is long enough.']);
50
+ assert.ok(body['response content']['details about your request'].error);
51
+ assert.equal(processTestRequestCalls.length, 0);
52
+ });
53
+
54
+ test('requestTest rejects a URL shorter than 12 characters', async () => {
55
+ const body = await response(['Test Page', 'short', 'A reason that is long enough.']);
56
+ assert.ok(body['response content']['details about your request'].error);
57
+ assert.equal(processTestRequestCalls.length, 0);
58
+ });
59
+
60
+ test('requestTest rejects an already-tested page', async () => {
61
+ const body = await response(['Mixed Outcomes Page', 'https://example.com/mixed', 'A reason that is long enough.']);
62
+ const details = body['response content']['details about your request'];
63
+ assert.ok(details.error.includes('already been tested'));
64
+ assert.equal(processTestRequestCalls.length, 0);
65
+ });
66
+
67
+ test('requestTest accepts a valid new page request', async () => {
68
+ const body = await response(['Brand New Page', 'https://example.com/brandnew', 'A reason that is long enough.']);
69
+ const details = body['response content']['details about your request'];
70
+ assert.equal(details.error, undefined);
71
+ assert.equal(details['page to be tested'].description, 'Brand New Page');
72
+ assert.equal(processTestRequestCalls.length, 1);
73
+ assert.equal(processTestRequestCalls[0].testType, 'test');
74
+ });
75
+
76
+ test('requestTest includes disposition information for a valid request', async () => {
77
+ const body = await response(['Brand New Page', 'https://example.com/brandnew', 'A reason that is long enough.']);
78
+ const disposition = body['response content']['disposition of your request'];
79
+ assert.ok(disposition);
80
+ assert.ok(disposition['what happens next']);
81
+ });
package/index.js CHANGED
@@ -24,7 +24,6 @@ const {
24
24
  isTimeStamp,
25
25
  isJobID,
26
26
  jobsPath,
27
- makeReportsExtract,
28
27
  recsLock,
29
28
  reportsPath
30
29
  } = require('./util');
@@ -747,8 +746,6 @@ const requestHandler = async (request, response) => {
747
746
  report.sources = {...report.sources, worker: workerName};
748
747
  // Save the report.
749
748
  await fs.writeFile(getReportPath(timeStamp, jobID), getJSON(report));
750
- // Update the data on the available reports.
751
- await makeReportsExtract();
752
749
  // Annotate the report.
753
750
  await annotateReport(timeStamp, jobID);
754
751
  console.log(`Testaro report ${id} was annotated, saved, and indexed`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jrpool/kilotest",
3
- "version": "46.0.4",
3
+ "version": "46.0.6",
4
4
  "description": "Ensemble testing for front-end web quality",
5
5
  "main": "index.js",
6
6
  "engines": {
@@ -216,7 +216,7 @@ const main = async () => {
216
216
  preventedReport
217
217
  );
218
218
 
219
- // Fixture 7: hidden. A report placed in hiddenReports/ instead of reports/, so it must not appear in listReports, listIssues, listViolators, listDiagnoses, or getReport API responses. The reportsExtract must not include it.
219
+ // Fixture 7: hidden. A report placed in hiddenReports/ instead of reports/, so it must not appear in listReports, listIssues, listViolators, listDiagnoses, or getReport API responses.
220
220
  const hiddenCatalog = {
221
221
  '0': catalogItem('A', 'Secret link', '/html/body/a[1]', '5:10:60:20')
222
222
  };
package/util.js CHANGED
@@ -24,8 +24,6 @@ const recsPath = exports.recsPath = () => path.join(jobsPath(), 'recs.json');
24
24
  const reportsPath = exports.reportsPath = () => path.join(dbPath(), 'reports');
25
25
  // Path of the hidden-reports directory.
26
26
  const hiddenReportsPath = exports.hiddenReportsPath = () => path.join(dbPath(), 'hiddenReports');
27
- // Path of the reports data file.
28
- const reportsExtractPath = exports.reportsExtractPath = () => path.join(dbPath(), 'reportsExtract.json');
29
27
  // IDs, names, and sponsors of Testaro rule engines.
30
28
  const ruleEngines = exports.ruleEngines = {
31
29
  alfa: ['Alfa', 'Siteimprove'],
@@ -643,11 +641,11 @@ exports.getPageDataStrings = async (timeStamp, jobID, pageData) => {
643
641
  };
644
642
  // Returns the creation time and size of a report.
645
643
  const getReportStats = exports.getReportStats = async (timeStamp, jobID) => {
646
- const reportStat = await fs.stat(
647
- path.join(reportsPath(), `${timeStamp}-${jobID}.json`),
648
- {throwIfNoEntry: false}
649
- );
650
- if (!reportStat) {
644
+ let reportStat;
645
+ try {
646
+ reportStat = await fs.stat(path.join(reportsPath(), `${timeStamp}-${jobID}.json`));
647
+ }
648
+ catch {
651
649
  return null;
652
650
  }
653
651
  const reportTime = reportStat.birthtime;
@@ -662,68 +660,43 @@ exports.isHidden = async (timeStamp, jobID) => {
662
660
  // Return whether the report is among them.
663
661
  return hiddenReportFileNames.includes(`${timeStamp}-${jobID}.json`);
664
662
  };
665
- // Creates, saves, and returns an extract of all available reports.
666
- const makeReportsExtract = exports.makeReportsExtract = async () => {
667
- // Initialize the reports data.
668
- const reportsExtract = {};
669
- // Get the names of all report files.
670
- const reportFileNames = await fs.readdir(reportsPath());
671
- // For each of them:
672
- for (const reportFileName of reportFileNames) {
673
- // Get its content.
674
- const reportJSON = await fs.readFile(path.join(reportsPath(), reportFileName), 'utf8');
675
- // Get the object that it represents.
663
+ // Returns an extract of an available report, or an error object if it cannot be read or parsed.
664
+ const getReportExtract = exports.getReportExtract = async (timeStamp, jobID) => {
665
+ try {
666
+ const reportJSON = await fs.readFile(
667
+ path.join(reportsPath(), `${timeStamp}-${jobID}.json`), 'utf8'
668
+ );
676
669
  const report = JSON.parse(reportJSON);
677
670
  const {target, jobData} = report;
678
- const jobName = reportFileName.slice(0, -5);
679
- const [timeStamp, jobID] = jobName.split('-');
680
671
  const {what, url} = target;
681
- // Get an extract from it.
682
- const reportExtract = {
672
+ return {
683
673
  timeStamp,
684
674
  jobID,
685
675
  what,
686
676
  url,
687
677
  reportTime: new Date(`20${jobData.endTime}Z`).toISOString()
688
678
  };
689
- // Add the report extract to the reports extract.
690
- reportsExtract[jobName] = reportExtract;
691
- }
692
- // Ensure the data directory exists.
693
- await fs.mkdir(dbPath(), {recursive: true});
694
- // Save the extract of the reports.
695
- await fs.writeFile(reportsExtractPath(), getJSON(reportsExtract));
696
- // Return it.
697
- return reportsExtract;
698
- };
699
- // Gets an extract of all available reports.
700
- const getReportsExtract = exports.getReportsExtract = async () => {
701
- let reportsExtract;
702
- try {
703
- // Get the reports extract file.
704
- const reportsExtractJSON = await fs.readFile(reportsExtractPath(), 'utf8');
705
- // Get its data.
706
- reportsExtract = JSON.parse(reportsExtractJSON);
707
- // If it is empty:
708
- if (!Object.keys(reportsExtract).length) {
709
- // Throw an error.
710
- throw new Error('Reports extract file is empty');
711
- }
712
679
  }
713
- // If this fails:
714
680
  catch {
715
- // Create or recreate and get the reports extract.
716
- reportsExtract = await makeReportsExtract();
681
+ return {
682
+ error: `No report ${timeStamp}-${jobID} is available`
683
+ };
717
684
  }
718
- // Return the extract.
719
- return reportsExtract;
720
685
  };
721
686
  // Returns extracts of all available reports.
722
687
  const getReportExtracts = exports.getReportExtracts = async () => {
723
- const reportsExtract = await getReportsExtract();
724
- return Object.values(reportsExtract);
688
+ const reportFileNames = await fs.readdir(reportsPath());
689
+ const extracts = [];
690
+ for (const reportFileName of reportFileNames) {
691
+ const [timeStamp, jobID] = reportFileName.slice(0, -5).split('-');
692
+ const extract = await getReportExtract(timeStamp, jobID);
693
+ if (!extract.error) {
694
+ extracts.push(extract);
695
+ }
696
+ }
697
+ return extracts;
725
698
  };
726
- // Returns whether a report is available on a page with a description or URL.
699
+ // Returns whether a report with a description or URL is available.
727
700
  exports.isReportAvailable = async (what, url) => {
728
701
  const reportExtracts = await getReportExtracts();
729
702
  const whats = reportExtracts.map(reportExtract => reportExtract.what);
@@ -732,43 +705,19 @@ exports.isReportAvailable = async (what, url) => {
732
705
  };
733
706
  // Gets extracts of the latest available reports for all page descriptions.
734
707
  exports.getLatestReportExtracts = async () => {
735
- // Get extracts of all available reports.
736
708
  const reportExtracts = await getReportExtracts();
737
- // Sort them primarily by page description and secondarily by completion time.
738
709
  objectSort(reportExtracts, 'reportTime', 'alpha');
739
710
  objectSort(reportExtracts, 'what', 'alpha');
740
- // Get the latest ones for all page descriptions.
741
711
  const latestReportExtracts = reportExtracts
742
712
  .filter((extract, index) => extract.what !== reportExtracts[index + 1]?.what);
743
- // Return them.
744
713
  return latestReportExtracts;
745
714
  };
746
- // Returns an extract of an available report.
747
- exports.getReportExtract = async (timeStamp, jobID) => {
748
- // Get the extract of all available reports.
749
- const reportsExtract = await getReportsExtract();
750
- // Get the extract of the report from it.
751
- const reportExtract = reportsExtract[`${timeStamp}-${jobID}`];
752
- // If it exists:
753
- if (reportExtract) {
754
- // Return it.
755
- return reportExtract;
756
- }
757
- // Otherwise, i.e. if it does not exist, return this.
758
- return {
759
- error: `No report ${timeStamp}-${jobID} is available`
760
- };
761
- };
762
715
  // Gets the descriptions of multi-report pages.
763
716
  exports.getMultiReportWhats = async () => {
764
- // Get extracts of all available reports.
765
717
  const reportExtracts = await getReportExtracts();
766
- // Get their sorted page descriptions.
767
718
  const sortedWhats = reportExtracts.map(extract => extract.what).sort();
768
- // Get those that at least 2 reports have.
769
719
  const multiReportWhats = sortedWhats.filter(
770
720
  (what, index) => what !== sortedWhats[index - 1] && what === sortedWhats[index + 1]
771
721
  );
772
- // Return an array of descriptions of multi-report pages.
773
722
  return multiReportWhats;
774
723
  };
package/util.test.js CHANGED
@@ -13,8 +13,7 @@ const {
13
13
  jobsPath,
14
14
  recsPath,
15
15
  reportsPath,
16
- hiddenReportsPath,
17
- reportsExtractPath
16
+ hiddenReportsPath
18
17
  } = require('./util');
19
18
 
20
19
  // TESTS
@@ -48,7 +47,7 @@ test('dbPath honors DB_DIR when it is set', () => {
48
47
  }
49
48
  });
50
49
 
51
- test('jobsPath, recsPath, reportsPath, hiddenReportsPath, and reportsExtractPath derive from DB_DIR', () => {
50
+ test('jobsPath, recsPath, reportsPath, and hiddenReportsPath derive from DB_DIR', () => {
52
51
  const saved = process.env.DB_DIR;
53
52
  process.env.DB_DIR = '/tmp/kilotest-fixtures';
54
53
  try {
@@ -56,7 +55,6 @@ test('jobsPath, recsPath, reportsPath, hiddenReportsPath, and reportsExtractPath
56
55
  assert.equal(recsPath(), path.join('/tmp/kilotest-fixtures', 'jobs', 'recs.json'));
57
56
  assert.equal(reportsPath(), path.join('/tmp/kilotest-fixtures', 'reports'));
58
57
  assert.equal(hiddenReportsPath(), path.join('/tmp/kilotest-fixtures', 'hiddenReports'));
59
- assert.equal(reportsExtractPath(), path.join('/tmp/kilotest-fixtures', 'reportsExtract.json'));
60
58
  }
61
59
  finally {
62
60
  if (saved !== undefined) {
@@ -5,7 +5,7 @@
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {getReportData, makeReportsExtract, objectSort, reportsPath} = require('../../util');
8
+ const {getReportData, objectSort, reportsPath} = require('../../util');
9
9
  const fs = require('fs/promises');
10
10
  const path = require('path');
11
11
 
@@ -26,8 +26,6 @@ exports.answer = async (_, search) => {
26
26
  // Delete it.
27
27
  await fs.unlink(path.join(reportsPath(), `${jobName}.json`));
28
28
  }
29
- // Update the extract of the available reports.
30
- await makeReportsExtract();
31
29
  }
32
30
  // If this failed:
33
31
  catch (error) {
@@ -5,7 +5,7 @@
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {getReportExtracts, hiddenReportsPath, makeReportsExtract, reportsPath} = require('../../util');
8
+ const {getReportExtracts, hiddenReportsPath, reportsPath} = require('../../util');
9
9
  const fs = require('fs/promises');
10
10
  const path = require('path');
11
11
 
@@ -24,8 +24,6 @@ exports.answer = async (_, search) => {
24
24
  try {
25
25
  // Move the specified report to the directory of hidden reports.
26
26
  await fs.rename(path.join(reportsPath(), fileName), path.join(hiddenReportsPath(), fileName));
27
- // Update the data on the available reports.
28
- await makeReportsExtract();
29
27
  }
30
28
  // If this failed:
31
29
  catch (error) {
@@ -5,7 +5,7 @@
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {getReportData, makeReportsExtract, reportsPath} = require('../../util');
8
+ const {getReportData, reportsPath} = require('../../util');
9
9
  const fs = require('fs/promises');
10
10
  const path = require('path');
11
11
 
@@ -26,8 +26,6 @@ exports.answer = async (_, search) => {
26
26
  // Delete it.
27
27
  await fs.unlink(path.join(reportsPath(), `${jobName}.json`));
28
28
  }
29
- // Update the extract of the available reports.
30
- await makeReportsExtract();
31
29
  }
32
30
  // If this failed:
33
31
  catch (error) {
@@ -5,7 +5,7 @@
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {hiddenReportsPath, makeReportsExtract, reportsPath} = require('../../util');
8
+ const {hiddenReportsPath, reportsPath} = require('../../util');
9
9
  const fs = require('fs/promises');
10
10
  const path = require('path');
11
11
 
@@ -24,8 +24,6 @@ exports.answer = async (_, search) => {
24
24
  try {
25
25
  // Move the report to the reports directory.
26
26
  await fs.rename(path.join(hiddenReportsPath(), fileName), path.join(reportsPath(), fileName));
27
- // Update the data on the available reports.
28
- await makeReportsExtract();
29
27
  }
30
28
  catch (error) {
31
29
  // Return why.