@jrpool/kilotest 46.1.2 → 48.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.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.ts +144 -0
- package/api/requestRetest.ts +119 -0
- package/api/requestTest.test.ts +153 -0
- package/api/requestTest.ts +123 -0
- 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} +35 -14
- package/api/{util.js → util.ts} +54 -51
- 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 +360 -0
- package/docs/SERVICE.md +170 -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} +397 -152
- package/{index.js → index.ts} +386 -285
- 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.ts +1129 -0
- package/util.ts +1017 -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} +74 -58
- 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 +71 -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} +57 -39
- package/web/requestRetest/index.html +7 -7
- package/web/requestRetest/index.ts +62 -0
- package/web/requestRetest/requestRetest.test.ts +130 -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.ts +52 -0
- package/web/requestTest/{requestTest.test.js → requestTest.test.ts} +39 -24
- 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} +18 -22
- 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/requestRetest.js +0 -96
- package/api/requestRetest.test.js +0 -80
- package/api/requestTest.js +0 -106
- package/api/requestTest.test.js +0 -92
- 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/util.test.js +0 -968
- 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/requestTest/index.js +0 -24
- 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,18 +1,18 @@
|
|
|
1
1
|
/*
|
|
2
|
-
index.
|
|
2
|
+
index.ts
|
|
3
3
|
Renews the WCAG map.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import {getJSON} from '../../util.ts';
|
|
11
11
|
|
|
12
12
|
// FUNCTIONS
|
|
13
13
|
|
|
14
14
|
// Renews the WCAG map and serves an acknowledgment.
|
|
15
|
-
|
|
15
|
+
export const answer = async (authCode: string) => {
|
|
16
16
|
// If the authorization code is valid:
|
|
17
17
|
if (authCode === process.env.AUTH_CODE) {
|
|
18
18
|
// Get the map source response.
|
|
@@ -29,16 +29,16 @@ exports.answer = async authCode => {
|
|
|
29
29
|
// If there are entries:
|
|
30
30
|
if (mapEntries) {
|
|
31
31
|
// Initialize the WCAG map.
|
|
32
|
-
const wcagMap = {};
|
|
32
|
+
const wcagMap: Record<string, string> = {};
|
|
33
33
|
// For each entry:
|
|
34
34
|
for (const entry of mapEntries) {
|
|
35
35
|
// Add it to the map.
|
|
36
|
-
wcagMap[entry[2]] = entry[1]
|
|
36
|
+
wcagMap[entry[2]!] = entry[1]!;
|
|
37
37
|
}
|
|
38
38
|
// Save the map, replacing any existing one.
|
|
39
|
-
await fs.writeFile(path.join(
|
|
39
|
+
await fs.writeFile(path.join(import.meta.dirname, '..', '..', 'wcagMap.json'), getJSON(wcagMap));
|
|
40
40
|
// Get the acknowledgment page.
|
|
41
|
-
const answerPage = await fs.readFile(path.join(
|
|
41
|
+
const answerPage = await fs.readFile(path.join(import.meta.dirname, 'index.html'), 'utf8');
|
|
42
42
|
// Return it.
|
|
43
43
|
return {
|
|
44
44
|
status: 'ok',
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
/*
|
|
2
|
-
renewWCAG.test.
|
|
3
|
-
Unit tests for web/renewWCAG/index.
|
|
2
|
+
renewWCAG.test.ts
|
|
3
|
+
Unit tests for web/renewWCAG/index.ts, covering auth, fetch status, and success branches.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
import {test, before, after, afterEach} from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import fs from 'node:fs/promises';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import {parse} from 'node-html-parser';
|
|
13
13
|
|
|
14
14
|
// CONSTANTS
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
const wcagMapPath = path.join(
|
|
16
|
+
import {fixtureDBDir} from '../../test/dbFixture.ts';
|
|
17
|
+
const wcagMapPath = path.join(import.meta.dirname, '..', '..', 'wcagMap.json');
|
|
18
18
|
|
|
19
19
|
// SETUP AND TEARDOWN
|
|
20
20
|
|
|
@@ -23,8 +23,8 @@ before(async () => {
|
|
|
23
23
|
process.env.AUTH_CODE = 'test-auth-code';
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
let originalFetch;
|
|
27
|
-
let wcagMapBackup;
|
|
26
|
+
let originalFetch: any;
|
|
27
|
+
let wcagMapBackup: any;
|
|
28
28
|
|
|
29
29
|
afterEach(async () => {
|
|
30
30
|
// Restore the original fetch after each test.
|
|
@@ -50,7 +50,7 @@ after(async () => {
|
|
|
50
50
|
// HELPER
|
|
51
51
|
|
|
52
52
|
// Replaces global.fetch with a mock that returns the given status and body.
|
|
53
|
-
const mockFetch = (status, body = '') => {
|
|
53
|
+
const mockFetch = (status: number, body = '') => {
|
|
54
54
|
originalFetch = global.fetch;
|
|
55
55
|
// @ts-expect-error: Replacing the real function with a mock for testing.
|
|
56
56
|
global.fetch = async () => ({
|
|
@@ -62,7 +62,7 @@ const mockFetch = (status, body = '') => {
|
|
|
62
62
|
// TESTS
|
|
63
63
|
|
|
64
64
|
test('answer returns an error for an invalid authorization code', async () => {
|
|
65
|
-
const {answer} =
|
|
65
|
+
const {answer} = await import('./index.ts');
|
|
66
66
|
const result = await answer('wrong-code');
|
|
67
67
|
assert.equal(result.status, 'error');
|
|
68
68
|
assert.equal(result.message, 'Authorization code invalid');
|
|
@@ -70,8 +70,7 @@ test('answer returns an error for an invalid authorization code', async () => {
|
|
|
70
70
|
|
|
71
71
|
test('answer returns an error when the WCAG map source returns a non-200 status', async () => {
|
|
72
72
|
mockFetch(404);
|
|
73
|
-
|
|
74
|
-
const {answer} = require('./index');
|
|
73
|
+
const {answer} = await import('./index.ts');
|
|
75
74
|
const result = await answer('test-auth-code');
|
|
76
75
|
assert.equal(result.status, 'error');
|
|
77
76
|
assert.equal(result.message, 'WCAG map source not retrieved');
|
|
@@ -87,8 +86,7 @@ test('answer returns ok and rewrites the WCAG map when the source returns valid
|
|
|
87
86
|
'<a href="understanding/target-size-minimum"><span class="secno">2.5.8 </span>'
|
|
88
87
|
].join('\n');
|
|
89
88
|
mockFetch(200, mockHTML);
|
|
90
|
-
|
|
91
|
-
const {answer} = require('./index');
|
|
89
|
+
const {answer} = await import('./index.ts');
|
|
92
90
|
const result = await answer('test-auth-code');
|
|
93
91
|
assert.equal(result.status, 'ok');
|
|
94
92
|
assert.ok(result.answerPage, 'answerPage should be present');
|
|
@@ -107,13 +105,12 @@ test('answer returns ok and rewrites the WCAG map when the source returns valid
|
|
|
107
105
|
test('answer returns an error when matchAll returns no entries', async () => {
|
|
108
106
|
// Mock matchAll to return null, covering the dead else branch on lines 48-55.
|
|
109
107
|
const originalMatchAll = String.prototype.matchAll;
|
|
110
|
-
String.prototype.matchAll = function() {
|
|
108
|
+
String.prototype.matchAll = function(): any {
|
|
111
109
|
return null;
|
|
112
110
|
};
|
|
113
111
|
mockFetch(200, '<html></html>');
|
|
114
112
|
try {
|
|
115
|
-
|
|
116
|
-
const {answer} = require('./index');
|
|
113
|
+
const {answer} = await import('./index.ts');
|
|
117
114
|
const result = await answer('test-auth-code');
|
|
118
115
|
assert.equal(result.status, 'error');
|
|
119
116
|
assert.equal(result.message, 'No entries found in WCAG map source');
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
index.ts
|
|
3
|
+
Serves a form for renewing the WCAG map.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
// FUNCTIONS
|
|
12
|
+
|
|
13
|
+
// Returns a form for renewing the WCAG map.
|
|
14
|
+
export const answer = async (): Promise<{status: string; answerPage: string}> => {
|
|
15
|
+
// Get the map renewal form.
|
|
16
|
+
const answerPage = await fs.readFile(path.join(import.meta.dirname, 'index.html'), 'utf8');
|
|
17
|
+
// Return it.
|
|
18
|
+
return {
|
|
19
|
+
status: 'ok',
|
|
20
|
+
answerPage
|
|
21
|
+
};
|
|
22
|
+
};
|
|
@@ -1,18 +1,44 @@
|
|
|
1
1
|
/*
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
reportDeletion.ts
|
|
3
|
+
Shared implementation of the forms for deleting reports.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
import {
|
|
9
|
+
errorMessage, getReportData, objectSort, populateTemplate, readdirOrCreate, reportsPath
|
|
10
|
+
} from '../util.ts';
|
|
11
|
+
import fs from 'node:fs/promises';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
|
|
14
|
+
// TYPES
|
|
15
|
+
|
|
16
|
+
// A summary of an available report, as shown in a deletion form.
|
|
17
|
+
type ReportSpec = {
|
|
18
|
+
timeStamp: string;
|
|
19
|
+
jobID: string;
|
|
20
|
+
issueCount: number;
|
|
21
|
+
preventedEngineCount: number;
|
|
22
|
+
url: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// The distinctive properties of a report-deletion form.
|
|
26
|
+
export type DeletionSpec = {
|
|
27
|
+
intro: string;
|
|
28
|
+
emptyIntro: string;
|
|
29
|
+
failurePrefix: string;
|
|
30
|
+
isDeletable: (specs: ReportSpec[], index: number) => boolean;
|
|
31
|
+
};
|
|
11
32
|
|
|
12
33
|
// FUNCTIONS
|
|
13
34
|
|
|
14
|
-
// Returns a form for deleting
|
|
15
|
-
|
|
35
|
+
// Returns a form for deleting the reports that the predicate marks as deletable.
|
|
36
|
+
export const reportDeletionForm = async (
|
|
37
|
+
dirName: string,
|
|
38
|
+
search: string,
|
|
39
|
+
deletionSpec: DeletionSpec
|
|
40
|
+
): Promise<{status: string; message?: string; answerPage?: string}> => {
|
|
41
|
+
const {emptyIntro, failurePrefix, intro, isDeletable} = deletionSpec;
|
|
16
42
|
const searchParams = new URLSearchParams(search);
|
|
17
43
|
const authCode = searchParams?.get('authCode');
|
|
18
44
|
const jobNames = searchParams?.getAll('report');
|
|
@@ -28,12 +54,12 @@ exports.answer = async (_, search) => {
|
|
|
28
54
|
}
|
|
29
55
|
}
|
|
30
56
|
// If this failed:
|
|
31
|
-
catch (error) {
|
|
57
|
+
catch (error: unknown) {
|
|
32
58
|
// Return why.
|
|
33
59
|
return {
|
|
34
60
|
status: 'error',
|
|
35
|
-
message:
|
|
36
|
-
}
|
|
61
|
+
message: `${failurePrefix} (${errorMessage(error)})`
|
|
62
|
+
};
|
|
37
63
|
}
|
|
38
64
|
}
|
|
39
65
|
// Otherwise, i.e. if the authorization code is invalid:
|
|
@@ -42,25 +68,27 @@ exports.answer = async (_, search) => {
|
|
|
42
68
|
return {
|
|
43
69
|
status: 'error',
|
|
44
70
|
message: 'Invalid authorization code'
|
|
45
|
-
}
|
|
71
|
+
};
|
|
46
72
|
}
|
|
47
73
|
}
|
|
48
|
-
const reportNames = await
|
|
49
|
-
|
|
74
|
+
const reportNames = await readdirOrCreate(reportsPath(), 'Reports directory');
|
|
75
|
+
// Initialize an array of report summaries.
|
|
76
|
+
const reportSpecs: ReportSpec[] = [];
|
|
50
77
|
// For each report:
|
|
51
78
|
for (const reportName of reportNames) {
|
|
52
|
-
const [timeStamp, jobID] = reportName.slice(0, -5).split('-');
|
|
79
|
+
const [timeStamp, jobID] = reportName.slice(0, -5).split('-') as [string, string];
|
|
53
80
|
// Get a summary of it.
|
|
54
81
|
const reportFacts = await getReportData(timeStamp, jobID);
|
|
55
|
-
const {error, issueCount, preventedEngineCount, url} = reportFacts;
|
|
56
82
|
// If this failed:
|
|
57
|
-
if (error) {
|
|
83
|
+
if (reportFacts.error !== undefined) {
|
|
58
84
|
// Return why.
|
|
59
85
|
return {
|
|
60
86
|
status: 'error',
|
|
61
|
-
message: error
|
|
62
|
-
}
|
|
87
|
+
message: reportFacts.error
|
|
88
|
+
};
|
|
63
89
|
}
|
|
90
|
+
const {issueCount, preventedEngineCount, url} = reportFacts;
|
|
91
|
+
// Otherwise, i.e. if it succeeded, add the summary to the array.
|
|
64
92
|
reportSpecs.push({
|
|
65
93
|
timeStamp,
|
|
66
94
|
jobID,
|
|
@@ -69,49 +97,39 @@ exports.answer = async (_, search) => {
|
|
|
69
97
|
url
|
|
70
98
|
});
|
|
71
99
|
}
|
|
72
|
-
// Sort the summaries primarily URL and secondarily by time stamp.
|
|
100
|
+
// Sort the summaries primarily by URL and secondarily by time stamp.
|
|
73
101
|
objectSort(reportSpecs, 'timeStamp', 'alpha');
|
|
74
102
|
objectSort(reportSpecs, 'url', 'alpha');
|
|
75
|
-
const lines = [];
|
|
103
|
+
const lines: string[] = [];
|
|
76
104
|
const margin = ' '.repeat(12);
|
|
77
105
|
let anyDeletable = false;
|
|
78
106
|
// For each summary:
|
|
79
|
-
reportSpecs.forEach((
|
|
80
|
-
const {timeStamp, jobID, issueCount, preventedEngineCount, url} =
|
|
107
|
+
reportSpecs.forEach((reportSpec, index) => {
|
|
108
|
+
const {timeStamp, jobID, issueCount, preventedEngineCount, url} = reportSpec;
|
|
81
109
|
const jobName = `${timeStamp}-${jobID}`;
|
|
82
110
|
const specString = `<code>${url}</code> (<code>${jobName}</code>): preventions ${preventedEngineCount}, issues ${issueCount}`;
|
|
83
|
-
// If its report is
|
|
84
|
-
if (reportSpecs
|
|
111
|
+
// If its report is deletable:
|
|
112
|
+
if (isDeletable(reportSpecs, index)) {
|
|
85
113
|
// Add a line with a deletion checkbox.
|
|
86
114
|
lines.push(
|
|
87
115
|
`${margin}<p><input type="checkbox" name="report" value="${jobName}"> ${specString}</p>`
|
|
88
116
|
);
|
|
89
117
|
anyDeletable = true;
|
|
90
118
|
}
|
|
91
|
-
// Otherwise, i.e. if its report is not
|
|
119
|
+
// Otherwise, i.e. if its report is not deletable:
|
|
92
120
|
else {
|
|
93
121
|
// Add a line without a deletion checkbox.
|
|
94
122
|
lines.push(`${margin}<p>${specString}</p>`);
|
|
95
123
|
}
|
|
96
124
|
});
|
|
97
|
-
const
|
|
98
|
-
? 'Choose the sole reports to delete.'
|
|
99
|
-
: 'Each target has at least 2 reports, so there are no reports to delete.';
|
|
100
|
-
const disabled = anyDeletable ? '' : ' disabled';
|
|
101
|
-
const query = {
|
|
125
|
+
const query: Record<string, string> = {
|
|
102
126
|
reports: lines.join('\n'),
|
|
103
|
-
intro,
|
|
104
|
-
disabled
|
|
127
|
+
intro: anyDeletable ? intro : emptyIntro,
|
|
128
|
+
disabled: anyDeletable ? '' : ' disabled'
|
|
105
129
|
};
|
|
106
|
-
// Get the order form template.
|
|
107
|
-
let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
|
|
108
|
-
// Replace its placeholders.
|
|
109
|
-
Object.keys(query).forEach(param => {
|
|
110
|
-
answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
|
|
111
|
-
});
|
|
112
130
|
// Return the populated page.
|
|
113
131
|
return {
|
|
114
132
|
status: 'ok',
|
|
115
|
-
answerPage
|
|
133
|
+
answerPage: await populateTemplate(dirName, query)
|
|
116
134
|
};
|
|
117
135
|
};
|
|
@@ -6,18 +6,18 @@
|
|
|
6
6
|
<meta name="publisher" content="Jonathan Robert Pool">
|
|
7
7
|
<meta name="creator" content="Jonathan Robert Pool">
|
|
8
8
|
<meta name="keywords" content="report,accessibility,a11y">
|
|
9
|
-
<title>__target__ retest
|
|
9
|
+
<title>__target__ retest request received | Kilotest</title>
|
|
10
10
|
<link rel="icon" href="/favicon.ico">
|
|
11
11
|
<link rel="stylesheet" href="/style.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
14
14
|
<main>
|
|
15
|
-
<h1><a href="/">Kilotest</a>: Retest
|
|
16
|
-
<p>Your
|
|
17
|
-
<p>The reason for your
|
|
18
|
-
<p>Thank you for your
|
|
19
|
-
<p>If a <a href="/enqueueForm.html">Kilotest manager approves this
|
|
20
|
-
<p>In most cases, if you check Kilotest again in 1 day, you will find that the
|
|
15
|
+
<h1><a href="/">Kilotest</a>: Retest request received for __description__</h1>
|
|
16
|
+
<p>Your request to retest the __description__ page at __url__ has been received.</p>
|
|
17
|
+
<p>The reason for your request has been recorded as <q>__reason__</q>.</p>
|
|
18
|
+
<p>Thank you for your request.</p>
|
|
19
|
+
<p>If a <a href="/enqueueForm.html">Kilotest manager approves this request</a>, a test order will be added to the queue.</p>
|
|
20
|
+
<p>In most cases, if you check Kilotest again in 1 day, you will find that the requested test has been performed.</p>
|
|
21
21
|
</main>
|
|
22
22
|
</body>
|
|
23
23
|
</html>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/*
|
|
2
|
+
index.ts
|
|
3
|
+
Records a retest request.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
import {populateTemplate, processTestRequest} from '../../util.ts';
|
|
9
|
+
|
|
10
|
+
// FUNCTIONS
|
|
11
|
+
|
|
12
|
+
export const answer = async (pageArgs: string, reason: string) => {
|
|
13
|
+
const [timeStamp, jobID] = pageArgs.split('/') as [string, string];
|
|
14
|
+
// Process the request. This resolves the cited report's description and URL itself.
|
|
15
|
+
const processResult = await processTestRequest(reason, {timeStamp, jobID});
|
|
16
|
+
// If the cited report does not exist:
|
|
17
|
+
if (processResult.result === 'nonreport') {
|
|
18
|
+
// Report this.
|
|
19
|
+
return {
|
|
20
|
+
status: 'error',
|
|
21
|
+
message: 'Invalid request'
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const {result, description, url} = processResult;
|
|
25
|
+
// If the request was recorded:
|
|
26
|
+
if (result === 'ok') {
|
|
27
|
+
const query = {
|
|
28
|
+
description,
|
|
29
|
+
url,
|
|
30
|
+
reason
|
|
31
|
+
};
|
|
32
|
+
// Return the populated page.
|
|
33
|
+
return {
|
|
34
|
+
status: 'ok',
|
|
35
|
+
answerPage: await populateTemplate(import.meta.dirname, query)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// Otherwise, if the request is a duplicate:
|
|
39
|
+
else if (result === 'duplicate') {
|
|
40
|
+
// Report this.
|
|
41
|
+
return {
|
|
42
|
+
status: 'error',
|
|
43
|
+
message: 'Test request duplicates an already submitted request'
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Otherwise, if the cited report has been superseded:
|
|
47
|
+
else if (result === 'superseded') {
|
|
48
|
+
// Report this.
|
|
49
|
+
return {
|
|
50
|
+
status: 'error',
|
|
51
|
+
message: 'A later report about the page is already available'
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Otherwise, i.e. if a request to test a page with the same description or URL is approved:
|
|
55
|
+
else {
|
|
56
|
+
// Report this.
|
|
57
|
+
return {
|
|
58
|
+
status: 'error',
|
|
59
|
+
message: 'A request to test a page with the same description or URL is already approved'
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/*
|
|
2
|
+
requestRetest.test.ts
|
|
3
|
+
Unit tests for web/requestRetest/index.ts, covering the success path of answer.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
import {test, before, after, beforeEach} from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import fs from 'node:fs/promises';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import {parse} from 'node-html-parser';
|
|
13
|
+
|
|
14
|
+
// CONSTANTS
|
|
15
|
+
|
|
16
|
+
import {fixtureDBDir} from '../../test/dbFixture.ts';
|
|
17
|
+
const testRequestsPath = path.join(fixtureDBDir, 'jobs', 'testRequests.json');
|
|
18
|
+
|
|
19
|
+
// SETUP AND TEARDOWN
|
|
20
|
+
|
|
21
|
+
before(async () => {
|
|
22
|
+
process.env.DB_DIR = fixtureDBDir;
|
|
23
|
+
process.env.AUTH_CODE = 'test-auth-code';
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
// Reset testRequests.json and clean job directories before each test.
|
|
28
|
+
await fs.writeFile(testRequestsPath, '{}\n');
|
|
29
|
+
for (const sub of ['claimed', 'queue', 'failed']) {
|
|
30
|
+
const dir = path.join(fixtureDBDir, 'jobs', sub);
|
|
31
|
+
await fs.mkdir(dir, {recursive: true});
|
|
32
|
+
const files = await fs.readdir(dir).catch(() => []);
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
try {
|
|
35
|
+
await fs.unlink(path.join(dir, file));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Ignore cleanup errors for files that may not exist.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
after(async () => {
|
|
45
|
+
// Restore testRequests.json and clean job directories after all tests.
|
|
46
|
+
await fs.writeFile(testRequestsPath, '{}\n');
|
|
47
|
+
for (const sub of ['claimed', 'queue', 'failed']) {
|
|
48
|
+
const dir = path.join(fixtureDBDir, 'jobs', sub);
|
|
49
|
+
await fs.mkdir(dir, {recursive: true});
|
|
50
|
+
const files = await fs.readdir(dir).catch(() => []);
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
try {
|
|
53
|
+
await fs.unlink(path.join(dir, file));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Ignore cleanup errors for files that may not exist.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// TESTS
|
|
63
|
+
|
|
64
|
+
test('answer returns ok with a populated answer page for a valid retest request', async () => {
|
|
65
|
+
// Require the module after DB_DIR is set.
|
|
66
|
+
const {answer} = await import('./index.ts');
|
|
67
|
+
// Use the 260101T0001-ct report (All CantTell Page, https://example.com/canttell).
|
|
68
|
+
const result: any = await answer('260101T0001/ct', 'Because changes were made');
|
|
69
|
+
assert.equal(result.status, 'ok');
|
|
70
|
+
assert.ok(result.answerPage, 'answerPage should be present');
|
|
71
|
+
// Verify the description and reason placeholders were replaced with the report target.
|
|
72
|
+
// (Note: the template's <title> uses __target__, a placeholder query never supplies;
|
|
73
|
+
// that pre-existing template mismatch is out of scope here and left unasserted.)
|
|
74
|
+
assert.ok(result.answerPage.includes('All CantTell Page'));
|
|
75
|
+
assert.ok(result.answerPage.includes('Because changes were made'));
|
|
76
|
+
assert.ok(!result.answerPage.includes('__description__'));
|
|
77
|
+
assert.ok(!result.answerPage.includes('__url__'));
|
|
78
|
+
assert.ok(!result.answerPage.includes('__reason__'));
|
|
79
|
+
// Verify the HTML is well-formed.
|
|
80
|
+
const doc = parse(result.answerPage);
|
|
81
|
+
const h1 = doc.querySelector('h1');
|
|
82
|
+
assert.ok(h1);
|
|
83
|
+
assert.ok(h1.textContent.includes('All CantTell Page'));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('answer returns an error when the report does not exist', async () => {
|
|
87
|
+
const {answer} = await import('./index.ts');
|
|
88
|
+
// Use a nonexistent report timestamp and jobID.
|
|
89
|
+
const result: any = await answer('990101T0000/xxx', 'Because changes were made');
|
|
90
|
+
assert.equal(result.status, 'error');
|
|
91
|
+
assert.equal(result.message, 'Invalid request');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('answer returns an error for a duplicate retest request', async () => {
|
|
95
|
+
const {answer} = await import('./index.ts');
|
|
96
|
+
// Submit the same retest request twice; the second is a duplicate.
|
|
97
|
+
await answer('260101T0001/ct', 'Because changes were made');
|
|
98
|
+
const result: any = await answer('260101T0001/ct', 'Because changes were made');
|
|
99
|
+
assert.equal(result.status, 'error');
|
|
100
|
+
assert.equal(result.message, 'Test request duplicates an already submitted request');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('answer returns an error when the cited report has been superseded', async () => {
|
|
104
|
+
const {answer} = await import('./index.ts');
|
|
105
|
+
// 260101T0000-mix (Mixed Outcomes Page) is superseded by 260202T0000-new.
|
|
106
|
+
const result: any = await answer('260101T0000/mix', 'Because changes were made');
|
|
107
|
+
assert.equal(result.status, 'error');
|
|
108
|
+
assert.equal(result.message, 'A later report about the page is already available');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('answer returns an error when a matching job is already claimed or queued', async () => {
|
|
112
|
+
const {answer} = await import('./index.ts');
|
|
113
|
+
// Create a claimed job sharing the description of the cited report
|
|
114
|
+
// (260101T0001-ct, "All CantTell Page").
|
|
115
|
+
const claimedDir = path.join(fixtureDBDir, 'jobs', 'claimed');
|
|
116
|
+
const jobPath = path.join(claimedDir, 'claimedJob.json');
|
|
117
|
+
await fs.writeFile(jobPath, JSON.stringify({
|
|
118
|
+
id: 'claimedJob',
|
|
119
|
+
target: {what: 'All CantTell Page', url: 'https://example.com/unrelated'}
|
|
120
|
+
}));
|
|
121
|
+
try {
|
|
122
|
+
const result: any = await answer('260101T0001/ct', 'Because changes were made');
|
|
123
|
+
assert.equal(result.status, 'error');
|
|
124
|
+
assert.equal(result.message, 'A request to test a page with the same description or URL is already approved');
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
await fs.unlink(jobPath).catch(() => {});
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
@@ -1,38 +1,36 @@
|
|
|
1
1
|
/*
|
|
2
|
-
index.
|
|
2
|
+
index.ts
|
|
3
3
|
Serves a form for requesting a retest.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
const fs = require('fs/promises');
|
|
10
|
-
const path = require('path');
|
|
8
|
+
import {getAgoString, getDateTimeString, getReportExtracts, populateTemplate} from '../../util.ts';
|
|
11
9
|
|
|
12
10
|
// FUNCTIONS
|
|
13
11
|
|
|
14
12
|
// Returns a retest recommendation form.
|
|
15
|
-
|
|
16
|
-
const [timeStamp, jobID] = pageArgs.split('/');
|
|
13
|
+
export const answer = async (pageArgs: string) => {
|
|
14
|
+
const [timeStamp, jobID] = pageArgs.split('/') as [string, string];
|
|
17
15
|
// Get data on the latest available reports.
|
|
18
|
-
const reportExtracts = await
|
|
16
|
+
const reportExtracts = await getReportExtracts(true);
|
|
19
17
|
// Get data on the report whose page is to be retested.
|
|
20
18
|
const reportExtract = reportExtracts.find(
|
|
21
|
-
reportExtract => reportExtract.timeStamp === timeStamp && reportExtract.jobID === jobID
|
|
19
|
+
(reportExtract: any) => reportExtract.timeStamp === timeStamp && reportExtract.jobID === jobID
|
|
22
20
|
);
|
|
23
21
|
// Initialize the page description.
|
|
24
|
-
let target;
|
|
22
|
+
let target: string;
|
|
25
23
|
// If getting the data succeeded:
|
|
26
24
|
if (reportExtract) {
|
|
27
25
|
// Update the page description.
|
|
28
|
-
target = reportExtract.
|
|
26
|
+
target = reportExtract.description;
|
|
29
27
|
}
|
|
30
28
|
// Otherwise, i.e. if it failed:
|
|
31
29
|
else {
|
|
32
30
|
// Make the form report the failure.
|
|
33
31
|
target = 'The specified page is not available for retesting';
|
|
34
32
|
}
|
|
35
|
-
const query = {
|
|
33
|
+
const query: Record<string, string> = {
|
|
36
34
|
target,
|
|
37
35
|
timeStamp,
|
|
38
36
|
jobID,
|
|
@@ -40,11 +38,7 @@ exports.answer = async pageArgs => {
|
|
|
40
38
|
dateTime: getDateTimeString(timeStamp)
|
|
41
39
|
};
|
|
42
40
|
// Get the recommendation form template.
|
|
43
|
-
|
|
44
|
-
// Replace its placeholders.
|
|
45
|
-
Object.keys(query).forEach(param => {
|
|
46
|
-
answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
|
|
47
|
-
});
|
|
41
|
+
const answerPage = await populateTemplate(import.meta.dirname, query);
|
|
48
42
|
// Return the populated page.
|
|
49
43
|
return {
|
|
50
44
|
status: 'ok',
|
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
/*
|
|
2
|
-
requestRetestForm.test.
|
|
3
|
-
Unit tests for web/requestRetestForm/index.
|
|
2
|
+
requestRetestForm.test.ts
|
|
3
|
+
Unit tests for web/requestRetestForm/index.ts.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const {answer} = require('./index');
|
|
8
|
+
import {test, before, after} from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import {parse} from 'node-html-parser';
|
|
11
|
+
import {answer} from './index.ts';
|
|
13
12
|
|
|
14
13
|
// SETUP AND TEARDOWN
|
|
15
14
|
|
|
16
15
|
const savedDBDir = process.env.DB_DIR;
|
|
17
16
|
|
|
18
|
-
before(() => {
|
|
19
|
-
process.env.DB_DIR =
|
|
17
|
+
before(async () => {
|
|
18
|
+
process.env.DB_DIR = (await import('../../test/dbFixture.ts')).fixtureDBDir;
|
|
20
19
|
});
|
|
21
20
|
|
|
22
21
|
after(() => {
|
|
@@ -35,15 +34,15 @@ test('requestRetestForm returns ok with the page name for a valid report', async
|
|
|
35
34
|
assert.equal(result.status, 'ok');
|
|
36
35
|
assert.ok(result.answerPage);
|
|
37
36
|
const html = parse(result.answerPage);
|
|
38
|
-
const title = html.querySelector('title')
|
|
39
|
-
assert.ok(title
|
|
37
|
+
const title = html.querySelector('title')?.text;
|
|
38
|
+
assert.ok(title?.includes('Mixed Outcomes Page'));
|
|
40
39
|
});
|
|
41
40
|
|
|
42
41
|
test('requestRetestForm populates the form action with the timeStamp and jobID', async () => {
|
|
43
42
|
const result = await answer('260202T0000/new');
|
|
44
43
|
const html = parse(result.answerPage);
|
|
45
44
|
const form = html.querySelector('form');
|
|
46
|
-
assert.equal(form
|
|
45
|
+
assert.equal(form?.getAttribute('action'), '/requestRetest.html/260202T0000/new');
|
|
47
46
|
});
|
|
48
47
|
|
|
49
48
|
test('requestRetestForm shows the fallback message for a nonexistent report', async () => {
|