@jrpool/kilotest 46.1.2 → 47.0.1
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/.claude/settings.local.json +13 -1
- package/.devin/config.local.json +5 -1
- package/.github/workflows/ci.yml +0 -3
- package/.github/workflows/periodic-smoke-tests.yml +30 -0
- package/.github/workflows/publish-mcp.yml +4 -4
- package/{alerts.test.js → alerts.test.ts} +17 -29
- package/{alerts.js → alerts.ts} +10 -13
- package/api/{getReport.test.js → getReport.test.ts} +9 -10
- package/api/{getReport.js → getReport.ts} +23 -24
- package/api/{listDiagnoses.test.js → listDiagnoses.test.ts} +26 -27
- package/api/{listDiagnoses.js → listDiagnoses.ts} +46 -70
- package/api/{listIssues.test.js → listIssues.test.ts} +21 -14
- package/api/{listIssues.js → listIssues.ts} +75 -74
- package/api/{listReports.test.js → listReports.test.ts} +17 -16
- package/api/listReports.ts +87 -0
- package/api/{listViolators.test.js → listViolators.test.ts} +19 -20
- package/api/{listViolators.js → listViolators.ts} +45 -70
- package/api/requestFeature.test.ts +54 -0
- package/api/{requestFeature.js → requestFeature.ts} +18 -15
- package/api/{requestRetest.test.js → requestRetest.test.ts} +33 -28
- package/api/{requestRetest.js → requestRetest.ts} +32 -25
- package/api/{requestTest.test.js → requestTest.test.ts} +35 -30
- package/api/{requestTest.js → requestTest.ts} +35 -28
- package/api/{routes.test.js → routes.test.ts} +5 -5
- package/api/{routes.js → routes.ts} +6 -6
- package/api/{schemas.js → schemas.ts} +69 -50
- package/api/{util.test.js → util.test.ts} +31 -11
- package/api/{util.js → util.ts} +61 -36
- package/api/version.ts +8 -0
- package/{apiIntegration.test.js → apiIntegration.test.ts} +12 -13
- package/balances.ts +87 -0
- package/{checkinfo.js → checkinfo.ts} +24 -20
- package/docs/AI-TOOLS.md +4 -4
- package/docs/BACKLOG.md +356 -0
- package/docs/SERVICE.md +144 -17
- package/docs/decisions/002-coverage-tool.md +40 -0
- package/docs/decisions/003-adr-threshold.md +34 -0
- package/docs/decisions/004-docs-directory.md +23 -0
- package/docs/decisions/005-observability.md +28 -0
- package/docs/decisions/006-git-workflow.md +28 -0
- package/docs/decisions/007-internal-testing.md +34 -0
- package/docs/decisions/008-dependencies.md +28 -0
- package/docs/decisions/009-qai-integration.md +40 -0
- package/docs/decisions/QAI-HISTORICAL-CONTEXT.md +42 -0
- package/eslint.config.mjs +27 -0
- package/index.html +3 -3
- package/{index.test.js → index.test.ts} +380 -143
- package/{index.js → index.ts} +382 -283
- package/{indexBalances.test.js → indexBalances.test.ts} +21 -17
- package/{indexServe.test.js → indexServe.test.ts} +9 -9
- package/linkValidation.test.ts +190 -0
- package/{mcp.test.js → mcp.test.ts} +53 -26
- package/{mcp.js → mcp.ts} +29 -20
- package/memory/MEMORY.md +7 -0
- package/memory/user_maintainer_context.md +24 -0
- package/openapi.yaml +115 -68
- package/package.json +35 -15
- package/pm2.config.cjs +29 -0
- package/{routes.test.js → routes.test.ts} +8 -7
- package/{routing.test.js → routing.test.ts} +11 -12
- package/scripts/{generate-openapi.js → generate-openapi.ts} +13 -13
- package/serve.ts +15 -0
- package/smokeTest.ts +234 -0
- package/test/dbFixture.ts +21 -0
- package/test/fixtures/{buildFixtures.js → buildFixtures.ts} +14 -13
- package/test/{setup.cjs → setup.ts} +2 -2
- package/tsconfig.json +16 -0
- package/{util.test.js → util.test.ts} +350 -257
- package/util.ts +921 -0
- package/web/ai0BalanceForm/{ai0BalanceForm.test.js → ai0BalanceForm.test.ts} +15 -15
- package/web/ai0BalanceForm/{index.js → index.ts} +10 -18
- package/web/enqueue/index.ts +56 -0
- package/web/enqueueForm/index.html +9 -9
- package/web/enqueueForm/index.ts +43 -0
- package/web/expungeReportsForm/{expungeReportsForm.test.js → expungeReportsForm.test.ts} +18 -20
- package/web/expungeReportsForm/index.ts +18 -0
- package/web/hideReportForm/{hideReportForm.test.js → hideReportForm.test.ts} +14 -14
- package/web/hideReportForm/{index.js → index.ts} +17 -21
- package/web/listDiagnoses/{index.js → index.ts} +48 -58
- package/web/listDiagnoses/{listDiagnoses.test.js → listDiagnoses.test.ts} +39 -34
- package/web/listIssues/{index.js → index.ts} +67 -92
- package/web/listIssues/{listIssues.test.js → listIssues.test.ts} +45 -51
- package/web/listReports/index.html +3 -3
- package/web/listReports/{index.js → index.ts} +48 -53
- package/web/listReports/{listReports.test.js → listReports.test.ts} +41 -43
- package/web/listRules/{index.js → index.ts} +18 -23
- package/web/listRules/{listRules.test.js → listRules.test.ts} +26 -19
- package/web/listTopIssues/{index.js → index.ts} +36 -47
- package/web/listTopIssues/{listTopIssues.test.js → listTopIssues.test.ts} +26 -17
- package/web/listViolators/{index.js → index.ts} +51 -71
- package/web/listViolators/{listViolators.test.js → listViolators.test.ts} +43 -36
- package/web/manage/index.ts +22 -0
- package/web/pruneReportsForm/index.ts +18 -0
- package/web/pruneReportsForm/{pruneReportsForm.test.js → pruneReportsForm.test.ts} +11 -11
- package/web/reannotate/index.ts +70 -0
- package/web/reannotate/{reannotate.test.js → reannotate.test.ts} +19 -22
- package/web/reannotateForm/{index.js → index.ts} +41 -50
- package/web/reannotateForm/{reannotateForm.test.js → reannotateForm.test.ts} +13 -16
- package/web/renewWCAG/{index.js → index.ts} +9 -9
- package/web/renewWCAG/{renewWCAG.test.js → renewWCAG.test.ts} +17 -20
- package/web/renewWCAGForm/index.ts +22 -0
- package/web/{expungeReportsForm/index.js → reportDeletion.ts} +56 -38
- package/web/requestRetest/index.html +7 -7
- package/web/requestRetest/index.ts +31 -0
- package/web/requestRetest/requestRetest.test.ts +116 -0
- package/web/requestRetestForm/{index.js → index.ts} +10 -16
- package/web/requestRetestForm/{requestRetestForm.test.js → requestRetestForm.test.ts} +11 -12
- package/web/requestTest/index.html +7 -7
- package/web/requestTest/{index.js → index.ts} +5 -5
- package/web/requestTest/{requestTest.test.js → requestTest.test.ts} +21 -21
- package/web/requestTestForm/index.html +4 -4
- package/web/requestTestForm/index.ts +21 -0
- package/web/requestTestForm/{requestTestForm.test.js → requestTestForm.test.ts} +9 -9
- package/web/rewindReportsForm/index.ts +18 -0
- package/web/rewindReportsForm/{rewindReportsForm.test.js → rewindReportsForm.test.ts} +11 -11
- package/web/tutorialAI/index.html +184 -0
- package/web/tutorialAI/index.ts +80 -0
- package/web/tutorialAI/tutorialAI.test.ts +192 -0
- package/web/{tutorial → tutorialWeb}/index.html +9 -8
- package/web/tutorialWeb/index.ts +78 -0
- package/web/tutorialWeb/tutorialWeb.test.ts +195 -0
- package/web/unhideReportForm/{index.js → index.ts} +17 -21
- package/web/unhideReportForm/{unhideReportForm.test.js → unhideReportForm.test.ts} +16 -16
- package/aceconfig.js +0 -28
- package/api/listReports.js +0 -89
- package/api/requestFeature.test.js +0 -58
- package/api/version.js +0 -8
- package/indexFallback.test.js +0 -33
- package/indexWorkers.test.js +0 -79
- package/jsconfig.json +0 -8
- package/pm2.config.js +0 -18
- package/smokeTest.js +0 -128
- package/util.js +0 -744
- package/web/enqueue/index.js +0 -67
- package/web/enqueueForm/index.js +0 -49
- package/web/manage/index.js +0 -22
- package/web/pruneReportsForm/index.js +0 -123
- package/web/reannotate/index.js +0 -58
- package/web/renewWCAGForm/index.js +0 -22
- package/web/requestRetest/index.js +0 -31
- package/web/requestRetest/requestRetest.test.js +0 -119
- package/web/requestTestForm/index.js +0 -21
- package/web/rewindReportsForm/index.js +0 -121
- package/web/tutorial/index.js +0 -63
- package/web/tutorial/tutorial.test.js +0 -97
- /package/test/fixtures/db/jobs/{recs.json → testRequests.json} +0 -0
- /package/web/{tutorial → tutorialWeb}/images/newsletter-form.png +0 -0
|
@@ -1,77 +1,67 @@
|
|
|
1
1
|
/*
|
|
2
|
-
listDiagnoses.
|
|
2
|
+
listDiagnoses.ts
|
|
3
3
|
Returns details about one violator of one issue in one report and facts about all its diagnoses.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import {z} from 'zod';
|
|
9
|
+
import {
|
|
9
10
|
getIssueSpec,
|
|
10
11
|
getReportBasics,
|
|
11
12
|
getResponseMetadata,
|
|
12
13
|
getToolsFacts,
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
14
|
+
getThisHost
|
|
15
|
+
} from './util.ts';
|
|
16
|
+
import {getReport, getTestActInstances, isReportError} from '../util.ts';
|
|
17
|
+
import {listDiagnosesResponseSchema} from './schemas.ts';
|
|
18
|
+
|
|
19
|
+
// TYPES
|
|
20
|
+
|
|
21
|
+
// The response content defined by the response schema.
|
|
22
|
+
type ResponseContent = z.infer<typeof listDiagnosesResponseSchema>['response content'];
|
|
23
|
+
// The array variant of the union allowed for the diagnoses property.
|
|
24
|
+
type Diagnoses = Extract<ResponseContent['diagnoses of how the element exhibited the issue'], unknown[]>;
|
|
16
25
|
|
|
17
26
|
// FUNCTIONS
|
|
18
27
|
|
|
19
28
|
// Returns the response body.
|
|
20
|
-
|
|
29
|
+
export const response = async (args: string[]) => {
|
|
21
30
|
const [catalogIndex = '', issueID = '', timeStamp = '', jobID = ''] = args;
|
|
22
|
-
|
|
23
|
-
const responseContent = {
|
|
24
|
-
'basics about the report': null,
|
|
25
|
-
'basics about the issue': null,
|
|
26
|
-
'basics about the element': null,
|
|
27
|
-
'details about the element': null,
|
|
28
|
-
'diagnoses of how the element exhibited the issue': null
|
|
29
|
-
};
|
|
31
|
+
const thisHost = getThisHost();
|
|
30
32
|
// Get the report.
|
|
31
33
|
const report = await getReport(timeStamp, jobID);
|
|
32
|
-
// If this failed:
|
|
33
|
-
if (report.error) {
|
|
34
|
-
// Add this to the response content.
|
|
35
|
-
responseContent['basics about the report'] = report;
|
|
36
|
-
}
|
|
37
|
-
// Otherwise, i.e. if it succeeded:
|
|
38
|
-
else {
|
|
39
|
-
// Get the basics about the report (which may be only an error message).
|
|
40
|
-
const reportBasics = await getReportBasics(timeStamp, jobID);
|
|
41
|
-
// Add them to the response content.
|
|
42
|
-
responseContent['basics about the report'] = reportBasics;
|
|
43
|
-
}
|
|
44
34
|
// Get the specification of the issue.
|
|
45
35
|
const issueSpec = issueID ? getIssueSpec(issueID) : null;
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
36
|
+
// Initialize the response content.
|
|
37
|
+
const responseContent: ResponseContent = {
|
|
38
|
+
'basics about the report': isReportError(report) ? report : await getReportBasics(timeStamp, jobID),
|
|
39
|
+
'basics about the issue': {
|
|
50
40
|
'error': 'No information about the specified issue is available'
|
|
51
|
-
}
|
|
52
|
-
|
|
41
|
+
},
|
|
42
|
+
'basics about the element': null,
|
|
43
|
+
'details about the element': null,
|
|
44
|
+
'diagnoses of how the element exhibited the issue': issueSpec ? null : {
|
|
53
45
|
'error': 'No diagnoses of how the element exhibited the issue is available'
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
//
|
|
57
|
-
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
// If the issue is non-ignorable and fully classified:
|
|
49
|
+
if (issueSpec) {
|
|
58
50
|
const {summary, wcag, weight, why} = issueSpec;
|
|
59
|
-
//
|
|
60
|
-
|
|
51
|
+
// Add the basics about it to the response content.
|
|
52
|
+
responseContent['basics about the issue'] = {
|
|
61
53
|
'identifier': issueID,
|
|
62
54
|
summary,
|
|
63
55
|
'impact on a user': why,
|
|
64
56
|
'related WCAG standard': {
|
|
65
|
-
'layer': (wcag.length > 4 ? 'success criterion' : 'guideline'),
|
|
57
|
+
'layer': (wcag.length > 4 ? 'success criterion' : 'guideline') as 'success criterion' | 'guideline',
|
|
66
58
|
'identifier': wcag
|
|
67
59
|
},
|
|
68
|
-
priority: ['lowest', 'low', 'high', 'highest'][weight - 1]
|
|
60
|
+
priority: ['lowest', 'low', 'high', 'highest'][weight - 1] as 'lowest' | 'low' | 'high' | 'highest'
|
|
69
61
|
};
|
|
70
|
-
// Add them to the response content.
|
|
71
|
-
responseContent['basics about the issue'] = issueBasics;
|
|
72
62
|
}
|
|
73
|
-
// If the report
|
|
74
|
-
if (report
|
|
63
|
+
// If the report was retrieved successfully:
|
|
64
|
+
if (!isReportError(report)) {
|
|
75
65
|
const catalogItem = report.catalog[catalogIndex];
|
|
76
66
|
// If the violator is not in it:
|
|
77
67
|
if (!catalogItem) {
|
|
@@ -101,33 +91,19 @@ exports.response = async args => {
|
|
|
101
91
|
// If the issue is non-ignorable and fully classified:
|
|
102
92
|
if (issueSpec) {
|
|
103
93
|
// Initialize data about the diagnoses of the violation of the issue.
|
|
104
|
-
const diagnoses = [];
|
|
105
|
-
// For each
|
|
106
|
-
report
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
outcome !== 'cantTell'
|
|
117
|
-
&& instance.issueID === issueID
|
|
118
|
-
&& instance.catalogIndex === catalogIndex
|
|
119
|
-
) {
|
|
120
|
-
// Get the diagnosis.
|
|
121
|
-
const diagnosis = {
|
|
122
|
-
'identifier of the violated rule': ruleID !== what ? ruleID : null,
|
|
123
|
-
'description of the violation': what,
|
|
124
|
-
'severity of the violation on a 0-to-3 scale': ordinalSeverity,
|
|
125
|
-
'count of violations of the rule by the element': count ?? 1
|
|
126
|
-
}
|
|
127
|
-
diagnoses.push(diagnosis);
|
|
128
|
-
}
|
|
129
|
-
});
|
|
94
|
+
const diagnoses: Diagnoses = [];
|
|
95
|
+
// For each instance reporting a violation of the issue by the violator:
|
|
96
|
+
getTestActInstances(report, {violationsOnly: true, issueID, catalogIndex})
|
|
97
|
+
.forEach(({instance}) => {
|
|
98
|
+
const {count, ordinalSeverity, ruleID, what} = instance;
|
|
99
|
+
// Get the diagnosis.
|
|
100
|
+
const diagnosis = {
|
|
101
|
+
'identifier of the violated rule': ruleID !== what ? ruleID : null,
|
|
102
|
+
'description of the violation': what,
|
|
103
|
+
'severity of the violation on a 0-to-3 scale': ordinalSeverity,
|
|
104
|
+
'count of violations of the rule by the element': count ?? 1
|
|
130
105
|
}
|
|
106
|
+
diagnoses.push(diagnosis);
|
|
131
107
|
});
|
|
132
108
|
// Add the diagnoses to the response content.
|
|
133
109
|
responseContent['diagnoses of how the element exhibited the issue'] = diagnoses;
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
/*
|
|
2
|
-
listIssues.test.
|
|
3
|
-
Tests for api/listIssues.
|
|
2
|
+
listIssues.test.ts
|
|
3
|
+
Tests for api/listIssues.ts using the fixture corpus, with emphasis on outcome handling.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const {response} = require('./listIssues');
|
|
8
|
+
import {test, before, after} from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import {response} from './listIssues.ts';
|
|
12
11
|
|
|
13
12
|
// SETUP AND TEARDOWN
|
|
14
13
|
|
|
15
14
|
const savedDBDir = process.env.DB_DIR;
|
|
16
15
|
|
|
17
|
-
before(() => {
|
|
18
|
-
process.env.DB_DIR =
|
|
16
|
+
before(async () => {
|
|
17
|
+
process.env.DB_DIR = (await import('../test/dbFixture.ts')).fixtureDBDir;
|
|
19
18
|
});
|
|
20
19
|
|
|
21
20
|
after(() => {
|
|
@@ -32,33 +31,37 @@ after(() => {
|
|
|
32
31
|
test('listIssues returns 2 issues for the mixed-outcomes report, excluding cantTell', async () => {
|
|
33
32
|
const body = await response(['260101T0000', 'mix']);
|
|
34
33
|
const issues = body['response content']['basics about all issues reported in the report'];
|
|
34
|
+
assert.ok(issues);
|
|
35
35
|
assert.equal(issues.length, 2);
|
|
36
|
-
const summaries = issues.map(i => i.summary).sort();
|
|
36
|
+
const summaries = issues.map((i: any) => i.summary).sort();
|
|
37
37
|
assert.deepEqual(summaries, ['all-capital text', 'link not named']);
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
test('listIssues returns 0 issues for the all-cantTell report', async () => {
|
|
41
41
|
const body = await response(['260101T0001', 'ct']);
|
|
42
42
|
const issues = body['response content']['basics about all issues reported in the report'];
|
|
43
|
+
assert.ok(issues);
|
|
43
44
|
assert.equal(issues.length, 0);
|
|
44
45
|
});
|
|
45
46
|
|
|
46
47
|
test('listIssues treats missing outcome as a violation, not cantTell', async () => {
|
|
47
48
|
const body = await response(['260101T0002', 'no']);
|
|
48
49
|
const issues = body['response content']['basics about all issues reported in the report'];
|
|
50
|
+
assert.ok(issues);
|
|
49
51
|
assert.equal(issues.length, 1);
|
|
50
|
-
assert.equal(issues[0]
|
|
52
|
+
assert.equal(issues[0]!.identifier, 'linkNoText');
|
|
51
53
|
});
|
|
52
54
|
|
|
53
55
|
test('listIssues returns 0 issues for the empty report', async () => {
|
|
54
56
|
const body = await response(['260101T0005', 'emp']);
|
|
55
57
|
const issues = body['response content']['basics about all issues reported in the report'];
|
|
58
|
+
assert.ok(issues);
|
|
56
59
|
assert.equal(issues.length, 0);
|
|
57
60
|
});
|
|
58
61
|
|
|
59
62
|
test('listIssues reports prevented rule engines for the prevented report', async () => {
|
|
60
63
|
const body = await response(['260101T0006', 'prv']);
|
|
61
|
-
const details = body['response content']['details about the report'];
|
|
64
|
+
const details = body['response content']['details about the report'] as any;
|
|
62
65
|
const preventions = details['test results']['rule engines that could not test the page'];
|
|
63
66
|
assert.equal(preventions.length, 1);
|
|
64
67
|
assert.equal(preventions[0].name, 'Alfa');
|
|
@@ -68,15 +71,18 @@ test('listIssues reports prevented rule engines for the prevented report', async
|
|
|
68
71
|
test('listIssues includes reporter names for each issue in the mixed report', async () => {
|
|
69
72
|
const body = await response(['260101T0000', 'mix']);
|
|
70
73
|
const issues = body['response content']['basics about all issues reported in the report'];
|
|
74
|
+
assert.ok(issues);
|
|
71
75
|
const linkIssue = issues.find(i => i.identifier === 'linkNoText');
|
|
76
|
+
assert.ok(linkIssue);
|
|
72
77
|
assert.deepEqual(linkIssue['rule engines with any violations belonging to the issue'], ['Alfa', 'Axe']);
|
|
73
78
|
const allCapsIssue = issues.find(i => i.identifier === 'allCaps');
|
|
79
|
+
assert.ok(allCapsIssue);
|
|
74
80
|
assert.deepEqual(allCapsIssue['rule engines with any violations belonging to the issue'], ['Alfa']);
|
|
75
81
|
});
|
|
76
82
|
|
|
77
83
|
test('listIssues counts issues by priority correctly for the mixed report', async () => {
|
|
78
84
|
const body = await response(['260101T0000', 'mix']);
|
|
79
|
-
const details = body['response content']['details about the report'];
|
|
85
|
+
const details = body['response content']['details about the report'] as any;
|
|
80
86
|
const counts = details['test results']['counts of issues by priority'];
|
|
81
87
|
// linkNoText weight 4 (highest), allCaps weight 1 (lowest).
|
|
82
88
|
assert.equal(counts.highest, 1);
|
|
@@ -87,13 +93,13 @@ test('listIssues counts issues by priority correctly for the mixed report', asyn
|
|
|
87
93
|
|
|
88
94
|
test('listIssues reports the superseded status for the mixed report', async () => {
|
|
89
95
|
const body = await response(['260101T0000', 'mix']);
|
|
90
|
-
const basics = body['response content']['basics about the report'];
|
|
96
|
+
const basics = body['response content']['basics about the report'] as any;
|
|
91
97
|
assert.equal(basics['whether a later report about the same page exists'], true);
|
|
92
98
|
});
|
|
93
99
|
|
|
94
100
|
test('listIssues returns an error for a nonexistent report', async () => {
|
|
95
101
|
const body = await response(['999999T9999', 'xyz']);
|
|
96
|
-
const basics = body['response content']['basics about the report'];
|
|
102
|
+
const basics = body['response content']['basics about the report'] as any;
|
|
97
103
|
assert.ok(basics.error);
|
|
98
104
|
});
|
|
99
105
|
|
|
@@ -102,6 +108,7 @@ test('listIssues handles instances with missing issueID and null instances witho
|
|
|
102
108
|
const issues = body['response content']['basics about all issues reported in the report'];
|
|
103
109
|
// The brd fixture has one instance with duplicateID (a valid issue) and one with no issueID (skipped).
|
|
104
110
|
// The alfa act has no instances array, so it should be handled as empty.
|
|
111
|
+
assert.ok(issues);
|
|
105
112
|
assert.ok(issues.length >= 1);
|
|
106
113
|
const ids = issues.map(i => i.identifier);
|
|
107
114
|
assert.ok(ids.includes('duplicateID'));
|
|
@@ -1,29 +1,43 @@
|
|
|
1
1
|
/*
|
|
2
|
-
listIssues.
|
|
2
|
+
listIssues.ts
|
|
3
3
|
Returns details about one report and basics about all the issues in it.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import {z} from 'zod';
|
|
9
|
+
import {
|
|
9
10
|
getIssueSpec,
|
|
10
11
|
getReportBasics,
|
|
11
12
|
getResponseMetadata,
|
|
12
13
|
getRuleEngineFacts,
|
|
13
14
|
getRuleEnginesFacts,
|
|
14
15
|
getToolsFacts,
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
16
|
+
getThisHost
|
|
17
|
+
} from './util.ts';
|
|
18
|
+
import {
|
|
19
|
+
getReport, getReportStats, getTestActInstances, getTestActs, isReportError, objectSort
|
|
20
|
+
} from '../util.ts';
|
|
21
|
+
import {listIssuesResponseSchema} from './schemas.ts';
|
|
22
|
+
|
|
23
|
+
// TYPES
|
|
24
|
+
|
|
25
|
+
// The response content defined by the response schema.
|
|
26
|
+
type ResponseContent = z.infer<typeof listIssuesResponseSchema>['response content'];
|
|
18
27
|
|
|
19
28
|
// FUNCTIONS
|
|
20
29
|
|
|
21
30
|
// Returns the response body.
|
|
22
|
-
|
|
31
|
+
export const response = async (args: string[]) => {
|
|
23
32
|
const [timeStamp = '', jobID = ''] = args;
|
|
33
|
+
const thisHost = getThisHost();
|
|
34
|
+
// Get the report.
|
|
35
|
+
const report = await getReport(timeStamp, jobID);
|
|
36
|
+
// Get the basics about the report (which may be only an error message).
|
|
37
|
+
const reportBasics = isReportError(report) ? report : await getReportBasics(timeStamp, jobID);
|
|
24
38
|
// Initialize the response content.
|
|
25
|
-
const responseContent = {
|
|
26
|
-
'basics about the report':
|
|
39
|
+
const responseContent: ResponseContent = {
|
|
40
|
+
'basics about the report': reportBasics,
|
|
27
41
|
'details about the report': null,
|
|
28
42
|
'how to request that the page be retested': null,
|
|
29
43
|
'how a web user can request that the page be retested': null,
|
|
@@ -31,21 +45,10 @@ exports.response = async args => {
|
|
|
31
45
|
'how a web user can get the full report in JSON': null,
|
|
32
46
|
'basics about all issues reported in the report': null
|
|
33
47
|
};
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// Add this to the response content.
|
|
39
|
-
responseContent['basics about the report'] = report;
|
|
40
|
-
}
|
|
41
|
-
// Otherwise, i.e. if it succeeded:
|
|
42
|
-
else {
|
|
43
|
-
// Get the basics about the report (which may be only an error message).
|
|
44
|
-
const reportBasics = await getReportBasics(timeStamp, jobID);
|
|
45
|
-
// Add them to the response content.
|
|
46
|
-
responseContent['basics about the report'] = reportBasics;
|
|
47
|
-
// If the basics about the report were obtained:
|
|
48
|
-
if (!reportBasics.error) {
|
|
48
|
+
// If the report was retrieved:
|
|
49
|
+
if (!isReportError(report)) {
|
|
50
|
+
// If its basics were computed:
|
|
51
|
+
if (!('error' in reportBasics)) {
|
|
49
52
|
const {
|
|
50
53
|
strict = null,
|
|
51
54
|
standard = null,
|
|
@@ -55,54 +58,52 @@ exports.response = async args => {
|
|
|
55
58
|
// Get details about the job definition.
|
|
56
59
|
const jobDefinitionDetails = {
|
|
57
60
|
'whether the job prohibited redirection': strict,
|
|
58
|
-
'whether the native results of rule engines are reported': ['also', 'no'].includes(standard),
|
|
59
|
-
'whether standardized results are reported': ['also', 'only'].includes(standard),
|
|
61
|
+
'whether the native results of rule engines are reported': ['also', 'no'].includes(standard as string),
|
|
62
|
+
'whether standardized results are reported': ['also', 'only'].includes(standard as string),
|
|
60
63
|
'device emulated by the job': device,
|
|
61
64
|
'browser type used by the job': browserID
|
|
62
65
|
};
|
|
63
66
|
// Initialize data about the test results.
|
|
64
|
-
const ruleEngineIDs = new Set();
|
|
65
|
-
const reporterIDs = new Set();
|
|
66
|
-
const violatorIndexes = new Set();
|
|
67
|
-
const issuesData
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
});
|
|
67
|
+
const ruleEngineIDs: Set<string> = new Set();
|
|
68
|
+
const reporterIDs: Set<string> = new Set();
|
|
69
|
+
const violatorIndexes: Set<string> = new Set();
|
|
70
|
+
const issuesData: Record<string, {
|
|
71
|
+
id: string;
|
|
72
|
+
summary: string;
|
|
73
|
+
weight: number;
|
|
74
|
+
why: string;
|
|
75
|
+
reporterIDs: Set<string>;
|
|
76
|
+
}> = {};
|
|
77
|
+
// For each test act in the report:
|
|
78
|
+
getTestActs(report).forEach(act => {
|
|
79
|
+
// Ensure its rule engine is in the results data.
|
|
80
|
+
ruleEngineIDs.add(act.which!);
|
|
81
|
+
});
|
|
82
|
+
// For each violating standard instance of each test act:
|
|
83
|
+
getTestActInstances(report, {violationsOnly: true}).forEach(({act, instance}) => {
|
|
84
|
+
const {catalogIndex, issueID} = instance;
|
|
85
|
+
// Get the specification of its issue.
|
|
86
|
+
const issueSpec = issueID ? getIssueSpec(issueID) : null;
|
|
87
|
+
// If the instance has a non-ignorable and fully classified issue:
|
|
88
|
+
if (issueSpec) {
|
|
89
|
+
const {summary, weight, why} = issueSpec;
|
|
90
|
+
// Ensure the rule-engine ID is in the reporters data.
|
|
91
|
+
reporterIDs.add(act.which!);
|
|
92
|
+
// If the instance has a catalog index:
|
|
93
|
+
if (catalogIndex) {
|
|
94
|
+
// Ensure the index of the violator is in the results data.
|
|
95
|
+
violatorIndexes.add(String(catalogIndex));
|
|
96
|
+
}
|
|
97
|
+
// Ensure the data about the issue are in the results data.
|
|
98
|
+
issuesData[issueID!] ??= {
|
|
99
|
+
id: issueID!,
|
|
100
|
+
summary,
|
|
101
|
+
weight,
|
|
102
|
+
why,
|
|
103
|
+
reporterIDs: new Set()
|
|
104
|
+
};
|
|
105
|
+
// Ensure the reporter ID is in the data about the issue.
|
|
106
|
+
issuesData[issueID!]!.reporterIDs.add(act.which!);
|
|
106
107
|
}
|
|
107
108
|
});
|
|
108
109
|
const {preventions} = report.jobData;
|
|
@@ -117,7 +118,7 @@ exports.response = async args => {
|
|
|
117
118
|
// For each issue:
|
|
118
119
|
Object.values(issuesData).forEach(issueData => {
|
|
119
120
|
// Increment the count of issues with its weight.
|
|
120
|
-
weightCounts[issueData.weight - 1]
|
|
121
|
+
weightCounts[issueData.weight - 1]!++;
|
|
121
122
|
});
|
|
122
123
|
// Get details about the test results.
|
|
123
124
|
const resultDetails = {
|
|
@@ -126,10 +127,10 @@ exports.response = async args => {
|
|
|
126
127
|
'names of rule engines that reported rule violations': getRuleEnginesFacts(reporterIDs)
|
|
127
128
|
.map(facts => facts.name),
|
|
128
129
|
'counts of issues by priority': {
|
|
129
|
-
'highest': weightCounts[3]
|
|
130
|
-
'high': weightCounts[2]
|
|
131
|
-
'low': weightCounts[1]
|
|
132
|
-
'lowest': weightCounts[0]
|
|
130
|
+
'highest': weightCounts[3]!,
|
|
131
|
+
'high': weightCounts[2]!,
|
|
132
|
+
'low': weightCounts[1]!,
|
|
133
|
+
'lowest': weightCounts[0]!
|
|
133
134
|
},
|
|
134
135
|
'number of elements reported as violators': violatorIndexes.size
|
|
135
136
|
};
|
|
@@ -186,12 +187,12 @@ exports.response = async args => {
|
|
|
186
187
|
return {
|
|
187
188
|
identifier: id,
|
|
188
189
|
summary,
|
|
189
|
-
priority: ['lowest', 'low', 'high', 'highest'][weight - 1],
|
|
190
|
+
priority: ['lowest', 'low', 'high', 'highest'][weight - 1] as 'lowest' | 'low' | 'high' | 'highest',
|
|
190
191
|
'impact on a user': why,
|
|
191
192
|
'rule engines with any violations belonging to the issue': getRuleEnginesFacts(reporterIDs)
|
|
192
193
|
.map(ruleEnginesFact => ruleEnginesFact.name),
|
|
193
194
|
'how to get details about the issue': {
|
|
194
|
-
method: 'GET',
|
|
195
|
+
method: 'GET' as const,
|
|
195
196
|
URL: `${thisHost}/api/listViolators/${id}/${timeStamp}/${jobID}`
|
|
196
197
|
},
|
|
197
198
|
'web users can get details about the issue at': `${thisHost}/listViolators.html/${id}/${timeStamp}/${jobID}`
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/*
|
|
2
|
-
listReports.test.
|
|
3
|
-
Tests for api/listReports.
|
|
2
|
+
listReports.test.ts
|
|
3
|
+
Tests for api/listReports.ts using the fixture corpus.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const {response} = require('./listReports');
|
|
8
|
+
import {test, before, after} from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import {response} from './listReports.ts';
|
|
12
11
|
|
|
13
12
|
// CONSTANTS
|
|
14
13
|
|
|
@@ -28,8 +27,8 @@ const fixtureIds = [
|
|
|
28
27
|
|
|
29
28
|
const savedDBDir = process.env.DB_DIR;
|
|
30
29
|
|
|
31
|
-
before(() => {
|
|
32
|
-
process.env.DB_DIR =
|
|
30
|
+
before(async () => {
|
|
31
|
+
process.env.DB_DIR = (await import('../test/dbFixture.ts')).fixtureDBDir;
|
|
33
32
|
});
|
|
34
33
|
|
|
35
34
|
after(() => {
|
|
@@ -47,7 +46,7 @@ test('listReports returns basics about all 8 non-hidden reports', async () => {
|
|
|
47
46
|
const body = await response();
|
|
48
47
|
const reportsBasics = body['response content']['basics about all available reports'];
|
|
49
48
|
assert.equal(reportsBasics.length, 8);
|
|
50
|
-
const ids = reportsBasics.map(b => b.identifier).sort();
|
|
49
|
+
const ids = reportsBasics.map((b: any) => b.identifier).sort();
|
|
51
50
|
const expectedIds = fixtureIds.map(([ts, jid]) => `${ts}-${jid}`).sort();
|
|
52
51
|
assert.deepEqual(ids, expectedIds);
|
|
53
52
|
});
|
|
@@ -55,14 +54,14 @@ test('listReports returns basics about all 8 non-hidden reports', async () => {
|
|
|
55
54
|
test('listReports does not include the hidden report', async () => {
|
|
56
55
|
const body = await response();
|
|
57
56
|
const reportsBasics = body['response content']['basics about all available reports'];
|
|
58
|
-
const ids = reportsBasics.map(b => b.identifier);
|
|
57
|
+
const ids = reportsBasics.map((b: any) => b.identifier);
|
|
59
58
|
assert.ok(!ids.includes('260101T0007-hid'), 'hidden report must not appear in listReports');
|
|
60
59
|
});
|
|
61
60
|
|
|
62
61
|
test('listReports sorts reports by page description and then by completion time', async () => {
|
|
63
62
|
const body = await response();
|
|
64
63
|
const reportsBasics = body['response content']['basics about all available reports'];
|
|
65
|
-
const descriptions = reportsBasics.map(b => b['tested web page'].description);
|
|
64
|
+
const descriptions = reportsBasics.map((b: any) => b['tested web page'].description);
|
|
66
65
|
// All CantTell Page, Empty Results Page, Mixed Outcomes Page, Mixed Outcomes Page, No Outcomes Page, Prevented Page.
|
|
67
66
|
assert.deepEqual(
|
|
68
67
|
descriptions,
|
|
@@ -78,16 +77,18 @@ test('listReports sorts reports by page description and then by completion time'
|
|
|
78
77
|
]
|
|
79
78
|
);
|
|
80
79
|
// The two Mixed Outcomes Page reports should be sorted by completion time (older first).
|
|
81
|
-
const mixedReports = reportsBasics.filter(b => b['tested web page'].description === 'Mixed Outcomes Page');
|
|
82
|
-
assert.equal(mixedReports[0]
|
|
83
|
-
assert.equal(mixedReports[1]
|
|
80
|
+
const mixedReports = reportsBasics.filter((b: any) => b['tested web page'].description === 'Mixed Outcomes Page');
|
|
81
|
+
assert.equal(mixedReports[0]!.identifier, '260101T0000-mix');
|
|
82
|
+
assert.equal(mixedReports[1]!.identifier, '260202T0000-new');
|
|
84
83
|
});
|
|
85
84
|
|
|
86
85
|
test('listReports marks the superseded report correctly', async () => {
|
|
87
86
|
const body = await response();
|
|
88
87
|
const reportsBasics = body['response content']['basics about all available reports'];
|
|
89
|
-
const mix = reportsBasics.find(b => b.identifier === '260101T0000-mix');
|
|
90
|
-
const newer = reportsBasics.find(b => b.identifier === '260202T0000-new');
|
|
88
|
+
const mix = reportsBasics.find((b: any) => b.identifier === '260101T0000-mix');
|
|
89
|
+
const newer = reportsBasics.find((b: any) => b.identifier === '260202T0000-new');
|
|
90
|
+
assert.ok(mix);
|
|
91
|
+
assert.ok(newer);
|
|
91
92
|
assert.equal(mix['whether a later report about the same page exists'], true);
|
|
92
93
|
assert.equal(newer['whether a later report about the same page exists'], false);
|
|
93
94
|
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
listReports.ts
|
|
3
|
+
Returns basics about all the available reports.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
import {z} from 'zod';
|
|
9
|
+
import {getReportBasics, getResponseMetadata, getThisHost, getToolsFacts} from './util.ts';
|
|
10
|
+
import {getReportExtracts} from '../util.ts';
|
|
11
|
+
import {listReportsResponseSchema} from './schemas.ts';
|
|
12
|
+
|
|
13
|
+
// TYPES
|
|
14
|
+
|
|
15
|
+
// The response content defined by the response schema.
|
|
16
|
+
type ResponseContent = z.infer<typeof listReportsResponseSchema>['response content'];
|
|
17
|
+
|
|
18
|
+
// FUNCTIONS
|
|
19
|
+
|
|
20
|
+
// Returns the response body.
|
|
21
|
+
export const response = async () => {
|
|
22
|
+
const thisHost = getThisHost();
|
|
23
|
+
// Initialize an array of basics about the reports.
|
|
24
|
+
const reportsBasics: ResponseContent['basics about all available reports'] = [];
|
|
25
|
+
// Get extracts of all available reports.
|
|
26
|
+
const reportExtracts = await getReportExtracts();
|
|
27
|
+
// For each report:
|
|
28
|
+
for (const extract of reportExtracts) {
|
|
29
|
+
const {jobID, timeStamp} = extract;
|
|
30
|
+
// Get the basics about it.
|
|
31
|
+
const reportBasics = await getReportBasics(timeStamp, jobID, extract);
|
|
32
|
+
// Add the basics, with instructions for getting details, to the array.
|
|
33
|
+
reportsBasics.push({
|
|
34
|
+
...reportBasics,
|
|
35
|
+
'how to get details about the report': {
|
|
36
|
+
method: 'GET',
|
|
37
|
+
URL: `${thisHost}/api/listIssues/${timeStamp}/${jobID}`
|
|
38
|
+
},
|
|
39
|
+
'web users can get details about the report at': `${thisHost}/listIssues.html/${timeStamp}/${jobID}`
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// Sort the array by page description and secondarily by completion recency.
|
|
43
|
+
reportsBasics.sort((a, b) => {
|
|
44
|
+
if (a['tested web page'].description !== b['tested web page'].description) {
|
|
45
|
+
return a['tested web page']
|
|
46
|
+
.description
|
|
47
|
+
.localeCompare(b['tested web page'].description, 'en', {sensitivity: 'base'});
|
|
48
|
+
}
|
|
49
|
+
return a['completion date and time'].localeCompare(b['completion date and time']);
|
|
50
|
+
});
|
|
51
|
+
// Create the response content.
|
|
52
|
+
const responseContent: ResponseContent = {
|
|
53
|
+
'basics about all available reports': reportsBasics,
|
|
54
|
+
'how to request that a page with no report be tested': {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
URL: `${thisHost}/api/requestTest`,
|
|
57
|
+
'request body': {
|
|
58
|
+
description: '10- to 100-character description of the page conforming to the naming convention used in this list of reports',
|
|
59
|
+
URL: '12- to 300-character URL of the page, including the https:// scheme and any query',
|
|
60
|
+
reason: '20- to 100-character reason why the page should be tested'
|
|
61
|
+
},
|
|
62
|
+
'how to check whether the request has been fulfilled': 'use this listReports tool to determine whether a report about the page has become available (typical wait time: 1 hour to 1 day)'
|
|
63
|
+
},
|
|
64
|
+
'how a web user can request that the page be tested': {
|
|
65
|
+
URL: `${thisHost}/requestTestForm.html`
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
// Create a response body.
|
|
69
|
+
const body = {
|
|
70
|
+
'tool collection': getToolsFacts(),
|
|
71
|
+
'tool name': 'listReports',
|
|
72
|
+
'this request': {
|
|
73
|
+
description: 'Provide basics about all available reports.',
|
|
74
|
+
method: 'GET',
|
|
75
|
+
URL: `${thisHost}/api/listReports`,
|
|
76
|
+
'closest ancestor request': null
|
|
77
|
+
},
|
|
78
|
+
'URLs of similar requests for web users': {
|
|
79
|
+
'this request': `${thisHost}/listReports.html`,
|
|
80
|
+
'closest ancestor request': null
|
|
81
|
+
},
|
|
82
|
+
'response metadata': getResponseMetadata(),
|
|
83
|
+
'response content': responseContent
|
|
84
|
+
};
|
|
85
|
+
// Return it.
|
|
86
|
+
return body;
|
|
87
|
+
};
|