@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,45 +1,39 @@
|
|
|
1
1
|
/*
|
|
2
|
-
listIssues.test.
|
|
3
|
-
UI tests for web/listIssues/index.
|
|
2
|
+
listIssues.test.ts
|
|
3
|
+
UI tests for web/listIssues/index.ts using the fixture corpus.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
8
|
+
import {test, before, after, mock} from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import {parse} from 'node-html-parser';
|
|
12
|
+
|
|
13
|
+
// Mock the util functions called by index.ts before requiring it, so that
|
|
14
|
+
// it imports the mocked versions. Other exports delegate to the real module.
|
|
15
|
+
import * as realUtil from '../../util.ts';
|
|
16
|
+
let pageDataStringsOverride: any = null;
|
|
17
|
+
mock.module('../../util.ts', {
|
|
18
|
+
exports: {
|
|
19
|
+
...realUtil,
|
|
20
|
+
getPageDataStrings: async (...args: any[]) => {
|
|
21
|
+
if (pageDataStringsOverride !== null) {
|
|
22
|
+
return pageDataStringsOverride;
|
|
23
|
+
}
|
|
24
|
+
return (realUtil.getPageDataStrings as any)(...args);
|
|
25
|
+
}
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
-
};
|
|
28
|
-
util.getPageDataStrings = async (...args) => {
|
|
29
|
-
if (pageDataStringsOverride !== null) {
|
|
30
|
-
return pageDataStringsOverride;
|
|
31
|
-
}
|
|
32
|
-
return realGetPageDataStrings(...args);
|
|
33
|
-
};
|
|
27
|
+
} as any);
|
|
34
28
|
|
|
35
|
-
const {answer} =
|
|
29
|
+
const {answer} = await import('./index.ts') as any;
|
|
36
30
|
|
|
37
31
|
// SETUP AND TEARDOWN
|
|
38
32
|
|
|
39
33
|
const savedDBDir = process.env.DB_DIR;
|
|
40
34
|
|
|
41
|
-
before(() => {
|
|
42
|
-
process.env.DB_DIR =
|
|
35
|
+
before(async () => {
|
|
36
|
+
process.env.DB_DIR = (await import('../../test/dbFixture.ts')).fixtureDBDir;
|
|
43
37
|
});
|
|
44
38
|
|
|
45
39
|
after(() => {
|
|
@@ -72,13 +66,8 @@ test('listIssues includes links to listViolators for issues in the mixed report'
|
|
|
72
66
|
const violatorLinks = html.querySelectorAll('a[href*="listViolators.html"]');
|
|
73
67
|
assert.ok(violatorLinks.length >= 2);
|
|
74
68
|
const hrefs = violatorLinks.map(a => a.getAttribute('href'));
|
|
75
|
-
assert.ok(hrefs.some(href => href
|
|
76
|
-
assert.ok(hrefs.some(href => href
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test('listIssues returns an error status for a hidden report', async () => {
|
|
80
|
-
const result = await answer('260101T0007/hid');
|
|
81
|
-
assert.equal(result.status, 'error');
|
|
69
|
+
assert.ok(hrefs.some(href => href?.includes('linkNoText')));
|
|
70
|
+
assert.ok(hrefs.some(href => href?.includes('allCaps')));
|
|
82
71
|
});
|
|
83
72
|
|
|
84
73
|
test('listIssues returns an error status for a nonexistent report', async () => {
|
|
@@ -102,18 +91,6 @@ test('listIssues for the empty report shows no issue links', async () => {
|
|
|
102
91
|
assert.equal(violatorLinks.length, 0);
|
|
103
92
|
});
|
|
104
93
|
|
|
105
|
-
test('listIssues returns an error when a report becomes hidden during processing', async () => {
|
|
106
|
-
isHiddenCallCount = 0;
|
|
107
|
-
forceHiddenOnCall = 2;
|
|
108
|
-
try {
|
|
109
|
-
const result = await answer('260101T0000/mix');
|
|
110
|
-
assert.equal(result.status, 'error');
|
|
111
|
-
assert.equal(result.message, 'Report is not available');
|
|
112
|
-
}
|
|
113
|
-
finally {
|
|
114
|
-
forceHiddenOnCall = -1;
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
94
|
|
|
118
95
|
test('listIssues includes prevention notices for the prevented report', async () => {
|
|
119
96
|
const result = await answer('260101T0006/prv');
|
|
@@ -122,6 +99,23 @@ test('listIssues includes prevention notices for the prevented report', async ()
|
|
|
122
99
|
assert.ok(result.answerPage.includes('page timed out'));
|
|
123
100
|
});
|
|
124
101
|
|
|
102
|
+
test('listIssues falls back to the engine ID for an unknown prevented engine', async () => {
|
|
103
|
+
const fs = await import('node:fs/promises');
|
|
104
|
+
const prvJSON = await fs.readFile(path.join(realUtil.reportsPath(), '260101T0006-prv.json'), 'utf8');
|
|
105
|
+
const report = JSON.parse(prvJSON);
|
|
106
|
+
report.jobData.preventions.unknownEngine = 'mystery failure';
|
|
107
|
+
const reportPath = path.join(realUtil.reportsPath(), '260103T0000-unk.json');
|
|
108
|
+
await fs.writeFile(reportPath, JSON.stringify(report));
|
|
109
|
+
try {
|
|
110
|
+
const result = await answer('260103T0000/unk');
|
|
111
|
+
assert.equal(result.status, 'ok');
|
|
112
|
+
assert.ok(result.answerPage.includes('unknownEngine (unknown sponsor)'));
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
await fs.unlink(reportPath);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
125
119
|
test('listIssues returns an error when getPageDataStrings fails after getData succeeds', async () => {
|
|
126
120
|
pageDataStringsOverride = {error: 'Page data strings error'};
|
|
127
121
|
try {
|
|
@@ -159,8 +153,8 @@ test('listIssues shows plural violator count for an issue with multiple violator
|
|
|
159
153
|
|
|
160
154
|
test('listIssues handles acts with no standardResult instances', async () => {
|
|
161
155
|
// Create a temporary report with an act that has no standardResult.
|
|
162
|
-
const fs =
|
|
163
|
-
const dbDir =
|
|
156
|
+
const fs = await import('node:fs/promises');
|
|
157
|
+
const dbDir = (await import('../../test/dbFixture.ts')).fixtureDBDir;
|
|
164
158
|
const reportPath = path.join(dbDir, 'reports', '260101T0004-nsi.json');
|
|
165
159
|
const report = {
|
|
166
160
|
id: '260101T0004-nsi',
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
<body>
|
|
14
14
|
<main>
|
|
15
15
|
<h1>Pages tested by <a href="/">Kilotest</a></h1>
|
|
16
|
-
<h2>
|
|
16
|
+
<h2>Requested for testing</h2>
|
|
17
17
|
<ol>
|
|
18
|
-
|
|
18
|
+
__requests__
|
|
19
19
|
</ol>
|
|
20
|
-
<p>
|
|
20
|
+
<p>__noRequests__</p>
|
|
21
21
|
<p>Should Kilotest <a href="requestTestForm.html">test a page</a> that is not listed below?</p>
|
|
22
22
|
<h2>Queued for testing</h2>
|
|
23
23
|
<ol>
|
|
@@ -1,66 +1,82 @@
|
|
|
1
1
|
/*
|
|
2
|
-
index.
|
|
2
|
+
index.ts
|
|
3
3
|
Lists all available reports.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import {
|
|
9
9
|
getAgoDays,
|
|
10
10
|
getCountString,
|
|
11
11
|
getJobNames,
|
|
12
12
|
getMultiReportWhats,
|
|
13
13
|
getObject,
|
|
14
14
|
getPageDataStrings,
|
|
15
|
-
|
|
15
|
+
getTestRequests,
|
|
16
16
|
getReportData,
|
|
17
17
|
getReportExtracts,
|
|
18
|
-
isRecommendable,
|
|
19
18
|
jobsPath,
|
|
20
|
-
objectSort
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
objectSort,
|
|
20
|
+
populateTemplate
|
|
21
|
+
} from '../../util.ts';
|
|
22
|
+
import path from 'node:path';
|
|
24
23
|
|
|
25
24
|
// FUNCTIONS
|
|
26
25
|
|
|
27
26
|
// Adds parameters to a query for the answer page.
|
|
28
|
-
const populateQuery = async query => {
|
|
27
|
+
const populateQuery = async (query: Record<string, any>) => {
|
|
29
28
|
const margin = ' '.repeat(8);
|
|
30
29
|
// Initialize the classes of lines.
|
|
31
|
-
const lines = {
|
|
32
|
-
|
|
30
|
+
const lines: {requests: string[], queue: string[], claimed: string[], tested: string[]} = {
|
|
31
|
+
requests: [],
|
|
33
32
|
queue: [],
|
|
34
33
|
claimed: [],
|
|
35
34
|
tested: []
|
|
36
35
|
};
|
|
37
|
-
// Get the
|
|
38
|
-
const
|
|
39
|
-
// For each
|
|
40
|
-
Object.keys(
|
|
41
|
-
// For each of its
|
|
42
|
-
|
|
43
|
-
const {
|
|
36
|
+
// Get the test requests.
|
|
37
|
+
const testRequests = await getTestRequests();
|
|
38
|
+
// For each requested URL:
|
|
39
|
+
Object.keys(testRequests).forEach(url => {
|
|
40
|
+
// For each of its requests:
|
|
41
|
+
testRequests[url]!.forEach((req: any) => {
|
|
42
|
+
const {description, reason} = req;
|
|
44
43
|
// Add a line.
|
|
45
|
-
lines.
|
|
44
|
+
lines.requests.push(`${margin}<li><code>${url}</code> (${description}): ${reason}</li>`);
|
|
46
45
|
});
|
|
47
46
|
});
|
|
48
47
|
// Sort the lines in alphabetical order by URL and secondarily by proposed name.
|
|
49
|
-
lines.
|
|
48
|
+
lines.requests.sort();
|
|
50
49
|
// Add the lines to the query.
|
|
51
|
-
query.
|
|
52
|
-
// Add a no-
|
|
53
|
-
query.
|
|
54
|
-
? 'Kilotest managers can <a href="enqueueForm.html">approve or reject a
|
|
55
|
-
: 'No
|
|
50
|
+
query.requests = lines.requests.join('\n');
|
|
51
|
+
// Add a no-requests message, if applicable, to the query.
|
|
52
|
+
query.noRequests = lines.requests.length
|
|
53
|
+
? 'Kilotest managers can <a href="enqueueForm.html">approve or reject a request</a>.'
|
|
54
|
+
: 'No requests await approval now.';
|
|
56
55
|
// Get the file names of all queued and claimed jobs.
|
|
57
56
|
const jobFileNames = await getJobNames();
|
|
57
|
+
// Initialize sets of the page descriptions and URLs of jobs in both categories.
|
|
58
|
+
const jobsData = {
|
|
59
|
+
queue: {
|
|
60
|
+
descriptions: new Set(),
|
|
61
|
+
urls: new Set()
|
|
62
|
+
},
|
|
63
|
+
claimed: {
|
|
64
|
+
descriptions: new Set(),
|
|
65
|
+
urls: new Set()
|
|
66
|
+
}
|
|
67
|
+
};
|
|
58
68
|
// For each job category:
|
|
59
|
-
for (const category of ['queue', 'claimed']) {
|
|
69
|
+
for (const category of ['queue', 'claimed'] as const) {
|
|
60
70
|
// For each job in the category:
|
|
61
71
|
for (const fileName of jobFileNames[category]) {
|
|
62
72
|
// Get the job.
|
|
63
|
-
const job = await getObject(path.join(jobsPath(), category, fileName));
|
|
73
|
+
const job = await getObject(path.join(jobsPath(), category, fileName)) as {target: {url: string, what: string}};
|
|
74
|
+
// Get the description and URL of its page.
|
|
75
|
+
const {target} = job;
|
|
76
|
+
const {what, url} = target;
|
|
77
|
+
// Ensure they are in the sets of properties of the category.
|
|
78
|
+
jobsData[category].descriptions.add(what);
|
|
79
|
+
jobsData[category].urls.add(url);
|
|
64
80
|
// Add a line.
|
|
65
81
|
lines[category].push(`${margin}<li><code>${job.target.url}</code> (${job.target.what})</li>`);
|
|
66
82
|
}
|
|
@@ -81,14 +97,21 @@ const populateQuery = async query => {
|
|
|
81
97
|
const multiReportWhats = await getMultiReportWhats();
|
|
82
98
|
// Sort them primarily by page description and secondarily by completion time.
|
|
83
99
|
let sortedExtracts = objectSort(reportExtracts, 'reportTime', 'alpha');
|
|
84
|
-
sortedExtracts = objectSort(sortedExtracts, '
|
|
100
|
+
sortedExtracts = objectSort(sortedExtracts, 'description', 'alpha');
|
|
85
101
|
// For each report:
|
|
86
102
|
for (const extract of sortedExtracts) {
|
|
87
|
-
const {jobID, timeStamp, url,
|
|
103
|
+
const {jobID, timeStamp, url, description, superseded} = extract;
|
|
88
104
|
// Get data about it.
|
|
89
105
|
const reportData = await getReportData(timeStamp, jobID);
|
|
106
|
+
// If this failed:
|
|
107
|
+
if (reportData.error !== undefined) {
|
|
108
|
+
console.error(reportData.error);
|
|
109
|
+
// Populate the query with the reason.
|
|
110
|
+
query.error = reportData.error;
|
|
111
|
+
// Stop populating the query.
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
90
114
|
const {
|
|
91
|
-
error,
|
|
92
115
|
issueCount,
|
|
93
116
|
preventedEngineCount,
|
|
94
117
|
preventedEngineNames,
|
|
@@ -96,21 +119,13 @@ const populateQuery = async query => {
|
|
|
96
119
|
reporterCount,
|
|
97
120
|
violatorCount
|
|
98
121
|
} = reportData;
|
|
99
|
-
// If this failed:
|
|
100
|
-
if (error) {
|
|
101
|
-
console.error(error);
|
|
102
|
-
// Populate the query with the reason.
|
|
103
|
-
query.error = error;
|
|
104
|
-
// Stop populating the query.
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
122
|
// Otherwise, i.e. if it succeeded, add lines about the report.
|
|
108
123
|
lines.tested.push(`${margin}<details>`);
|
|
109
124
|
const daysAgo = getAgoDays(timeStamp);
|
|
110
|
-
const pageDataStrings = await getPageDataStrings(timeStamp, jobID, {
|
|
111
|
-
const {urlLink, testInfo} = pageDataStrings;
|
|
112
|
-
const testText = multiReportWhats.includes(
|
|
113
|
-
lines.tested.push(`${margin} <summary>${
|
|
125
|
+
const pageDataStrings = await getPageDataStrings(timeStamp, jobID, {description, url, daysAgo});
|
|
126
|
+
const {urlLink, testInfo}: any = pageDataStrings;
|
|
127
|
+
const testText = multiReportWhats.includes(description) ? ` (${testInfo.toLowerCase()})` : '';
|
|
128
|
+
lines.tested.push(`${margin} <summary>${description}${testText}</summary>`);
|
|
114
129
|
lines.tested.push(`${margin} <ul>`);
|
|
115
130
|
// Add the URL of the target to the lines.
|
|
116
131
|
lines.tested.push(`${margin} <li>URL: ${urlLink}</li>`);
|
|
@@ -125,7 +140,7 @@ const populateQuery = async query => {
|
|
|
125
140
|
);
|
|
126
141
|
}
|
|
127
142
|
// Add facts about the test results to the lines.
|
|
128
|
-
|
|
143
|
+
let reporterString = `${getCountString(reporterCount, 'rule engine', 'rule engines')} reported issues`;
|
|
129
144
|
if (reporterCount) {
|
|
130
145
|
const reporterNamesString = reporterNames.join(' + ');
|
|
131
146
|
reporterString = `${reporterString} (${reporterNamesString})`;
|
|
@@ -141,34 +156,39 @@ const populateQuery = async query => {
|
|
|
141
156
|
if (issueCount) {
|
|
142
157
|
// Add a question link about the reported issues to the lines.
|
|
143
158
|
const href = `href="listIssues.html/${timeStamp}/${jobID}"`;
|
|
144
|
-
const label = `aria-label="What ${issueCountString} reported for the ${
|
|
159
|
+
const label = `aria-label="What ${issueCountString} reported for the ${description} page?"`;
|
|
145
160
|
const questionString = issueCount === 1 ? 'was the issue' : 'were the issues';
|
|
146
161
|
const link = `<a ${href} ${label}>What ${questionString}?</a>`;
|
|
147
162
|
lines.tested.push(`${margin} <li>${link}</li>`);
|
|
148
163
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
164
|
+
let retestString: string = '';
|
|
165
|
+
// If a page with the same description or URL is being tested:
|
|
166
|
+
if (jobsData.claimed.descriptions.has(description) || jobsData.claimed.urls.has(url)) {
|
|
167
|
+
// Report this.
|
|
153
168
|
retestString = 'Currently being retested';
|
|
154
169
|
}
|
|
155
|
-
|
|
170
|
+
// Otherwise, if such a page is queued:
|
|
171
|
+
else if (jobsData.queue.descriptions.has(description) || jobsData.queue.urls.has(url)) {
|
|
156
172
|
retestString = 'Currently in the queue for retesting';
|
|
157
173
|
}
|
|
158
|
-
|
|
174
|
+
// Otherwise, if no such page is being tested or queued and the report is not superseded:
|
|
175
|
+
else if (!superseded) {
|
|
176
|
+
// Make the page available for retesting.
|
|
159
177
|
const href = `/requestRetestForm.html/${timeStamp}/${jobID}`;
|
|
160
178
|
const retestContent = 'Should Kilotest retest the page?';
|
|
161
179
|
retestString = `<a href="${href}">${retestContent}</a>`;
|
|
162
180
|
}
|
|
163
|
-
|
|
181
|
+
if (retestString) {
|
|
182
|
+
lines.tested.push(`${margin} <li>${retestString}</li>`);
|
|
183
|
+
}
|
|
164
184
|
lines.tested.push(`${margin} </ul>`);
|
|
165
185
|
lines.tested.push(`${margin}</details>`);
|
|
166
186
|
}
|
|
167
187
|
query.testedPages = lines.tested.join('\n');
|
|
168
188
|
};
|
|
169
189
|
// Returns a page answering the targets question.
|
|
170
|
-
|
|
171
|
-
const query = {};
|
|
190
|
+
export const answer = async () => {
|
|
191
|
+
const query: Record<string, any> = {};
|
|
172
192
|
// Create a query to replace placeholders.
|
|
173
193
|
await populateQuery(query);
|
|
174
194
|
// If the query reports an error:
|
|
@@ -180,11 +200,7 @@ exports.answer = async () => {
|
|
|
180
200
|
};
|
|
181
201
|
}
|
|
182
202
|
// Otherwise, i.e. if it does not report an error, get the template.
|
|
183
|
-
|
|
184
|
-
// Replace its placeholders.
|
|
185
|
-
Object.keys(query).forEach(param => {
|
|
186
|
-
answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
|
|
187
|
-
});
|
|
203
|
+
const answerPage = await populateTemplate(import.meta.dirname, query);
|
|
188
204
|
// Return the populated page.
|
|
189
205
|
return {
|
|
190
206
|
status: 'ok',
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/*
|
|
2
|
-
listReports.test.
|
|
3
|
-
UI tests for web/listReports/index.
|
|
2
|
+
listReports.test.ts
|
|
3
|
+
UI tests for web/listReports/index.ts using the fixture corpus.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
import {test, before, after} from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import fs from 'node:fs/promises';
|
|
12
|
+
import {parse} from 'node-html-parser';
|
|
13
|
+
import {answer} from './index.ts';
|
|
14
14
|
|
|
15
15
|
// SETUP AND TEARDOWN
|
|
16
16
|
|
|
17
17
|
const savedDBDir = process.env.DB_DIR;
|
|
18
18
|
|
|
19
19
|
before(async () => {
|
|
20
|
-
process.env.DB_DIR =
|
|
20
|
+
process.env.DB_DIR = (await import('../../test/dbFixture.ts')).fixtureDBDir;
|
|
21
21
|
// Ensure job subdirectories exist (they are empty and not tracked by Git).
|
|
22
22
|
for (const sub of ['queue', 'claimed', 'failed']) {
|
|
23
23
|
await fs.mkdir(path.join(process.env.DB_DIR, 'jobs', sub), {recursive: true});
|
|
@@ -36,22 +36,22 @@ after(() => {
|
|
|
36
36
|
// TESTS
|
|
37
37
|
|
|
38
38
|
test('listReports returns an ok status with valid HTML', async () => {
|
|
39
|
-
const result = await answer();
|
|
39
|
+
const result: any = await answer();
|
|
40
40
|
assert.equal(result.status, 'ok');
|
|
41
41
|
assert.ok(result.answerPage);
|
|
42
42
|
const html = parse(result.answerPage);
|
|
43
|
-
assert.equal(html.querySelector('title')
|
|
43
|
+
assert.equal(html.querySelector('title')?.text, 'Pages tested | Kilotest');
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
test('listReports includes the 8 non-hidden fixture reports as details elements', async () => {
|
|
47
|
-
const result = await answer();
|
|
47
|
+
const result: any = await answer();
|
|
48
48
|
const html = parse(result.answerPage);
|
|
49
49
|
const details = html.querySelectorAll('details');
|
|
50
50
|
assert.equal(details.length, 8);
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
test('listReports includes the page descriptions in summary elements', async () => {
|
|
54
|
-
const result = await answer();
|
|
54
|
+
const result: any = await answer();
|
|
55
55
|
const html = parse(result.answerPage);
|
|
56
56
|
const summaries = html.querySelectorAll('details > summary').map(s => s.text);
|
|
57
57
|
assert.ok(summaries.some(s => s.startsWith('Mixed Outcomes Page')));
|
|
@@ -62,55 +62,55 @@ test('listReports includes the page descriptions in summary elements', async ()
|
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
test('listReports does not include the hidden report', async () => {
|
|
65
|
-
const result = await answer();
|
|
65
|
+
const result: any = await answer();
|
|
66
66
|
assert.ok(!result.answerPage.includes('Hidden Page'));
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
test('listReports includes links to listIssues for reports with issues', async () => {
|
|
70
|
-
const result = await answer();
|
|
70
|
+
const result: any = await answer();
|
|
71
71
|
const html = parse(result.answerPage);
|
|
72
72
|
const issueLinks = html.querySelectorAll('a[href*="listIssues.html"]');
|
|
73
73
|
assert.ok(issueLinks.length > 0);
|
|
74
74
|
const hrefs = issueLinks.map(a => a.getAttribute('href'));
|
|
75
|
-
assert.ok(hrefs.some(href => href
|
|
75
|
+
assert.ok(hrefs.some(href => href?.includes('260101T0000/mix')));
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
test('listReports includes the page URLs in the report details', async () => {
|
|
79
|
-
const result = await answer();
|
|
79
|
+
const result: any = await answer();
|
|
80
80
|
assert.ok(result.answerPage.includes('https://example.com/mixed'));
|
|
81
81
|
assert.ok(result.answerPage.includes('https://example.com/canttell'));
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
test('listReports includes a link to request testing a new page', async () => {
|
|
85
|
-
const result = await answer();
|
|
85
|
+
const result: any = await answer();
|
|
86
86
|
const html = parse(result.answerPage);
|
|
87
87
|
const testLink = html.querySelector('a[href="requestTestForm.html"]');
|
|
88
88
|
assert.ok(testLink);
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
-
test('listReports shows
|
|
92
|
-
const dbDir =
|
|
93
|
-
const
|
|
94
|
-
const
|
|
91
|
+
test('listReports shows requests when testRequests.json has entries', {timeout: 500}, async () => {
|
|
92
|
+
const dbDir = (await import('../../test/dbFixture.ts')).fixtureDBDir;
|
|
93
|
+
const testRequestsPath = path.join(dbDir, 'jobs', 'testRequests.json');
|
|
94
|
+
const originalTestRequests = await fs.readFile(testRequestsPath, 'utf8');
|
|
95
95
|
try {
|
|
96
|
-
const
|
|
96
|
+
const testRequests = {
|
|
97
97
|
'https://example.com/mixed': [
|
|
98
|
-
{
|
|
98
|
+
{description: 'Mixed Outcomes Page', reason: 'Needs retesting for accessibility'}
|
|
99
99
|
]
|
|
100
100
|
};
|
|
101
|
-
await fs.writeFile(
|
|
102
|
-
const result = await answer();
|
|
101
|
+
await fs.writeFile(testRequestsPath, JSON.stringify(testRequests, null, 2));
|
|
102
|
+
const result: any = await answer();
|
|
103
103
|
assert.equal(result.status, 'ok');
|
|
104
104
|
assert.ok(result.answerPage.includes('https://example.com/mixed'));
|
|
105
105
|
assert.ok(result.answerPage.includes('Needs retesting for accessibility'));
|
|
106
106
|
}
|
|
107
107
|
finally {
|
|
108
|
-
await fs.writeFile(
|
|
108
|
+
await fs.writeFile(testRequestsPath, originalTestRequests);
|
|
109
109
|
}
|
|
110
110
|
});
|
|
111
111
|
|
|
112
112
|
test('listReports shows queued and claimed jobs when they exist', async () => {
|
|
113
|
-
const dbDir =
|
|
113
|
+
const dbDir = (await import('../../test/dbFixture.ts')).fixtureDBDir;
|
|
114
114
|
const queueDir = path.join(dbDir, 'jobs', 'queue');
|
|
115
115
|
const claimedDir = path.join(dbDir, 'jobs', 'claimed');
|
|
116
116
|
const queueFile = path.join(queueDir, 'queuedJob.json');
|
|
@@ -124,7 +124,7 @@ test('listReports shows queued and claimed jobs when they exist', async () => {
|
|
|
124
124
|
};
|
|
125
125
|
await fs.writeFile(queueFile, JSON.stringify(queuedJob, null, 2));
|
|
126
126
|
await fs.writeFile(claimedFile, JSON.stringify(claimedJob, null, 2));
|
|
127
|
-
const result = await answer();
|
|
127
|
+
const result: any = await answer();
|
|
128
128
|
assert.equal(result.status, 'ok');
|
|
129
129
|
assert.ok(result.answerPage.includes('https://example.com/queued'));
|
|
130
130
|
assert.ok(result.answerPage.includes('Queued Page'));
|
|
@@ -138,7 +138,7 @@ test('listReports shows queued and claimed jobs when they exist', async () => {
|
|
|
138
138
|
});
|
|
139
139
|
|
|
140
140
|
test('listReports shows claimed retest status for a report with a matching claimed job', async () => {
|
|
141
|
-
const dbDir =
|
|
141
|
+
const dbDir = (await import('../../test/dbFixture.ts')).fixtureDBDir;
|
|
142
142
|
const claimedDir = path.join(dbDir, 'jobs', 'claimed');
|
|
143
143
|
const claimedFile = path.join(claimedDir, 'claimedRetest.json');
|
|
144
144
|
try {
|
|
@@ -146,7 +146,7 @@ test('listReports shows claimed retest status for a report with a matching claim
|
|
|
146
146
|
target: {url: 'https://example.com/mixed', what: 'Mixed Outcomes Page'}
|
|
147
147
|
};
|
|
148
148
|
await fs.writeFile(claimedFile, JSON.stringify(claimedJob, null, 2));
|
|
149
|
-
const result = await answer();
|
|
149
|
+
const result: any = await answer();
|
|
150
150
|
assert.equal(result.status, 'ok');
|
|
151
151
|
assert.ok(result.answerPage.includes('Currently being retested'));
|
|
152
152
|
}
|
|
@@ -156,7 +156,7 @@ test('listReports shows claimed retest status for a report with a matching claim
|
|
|
156
156
|
});
|
|
157
157
|
|
|
158
158
|
test('listReports shows queued retest status for a report with a matching queued job', async () => {
|
|
159
|
-
const dbDir =
|
|
159
|
+
const dbDir = (await import('../../test/dbFixture.ts')).fixtureDBDir;
|
|
160
160
|
const queueDir = path.join(dbDir, 'jobs', 'queue');
|
|
161
161
|
const queueFile = path.join(queueDir, 'queuedRetest.json');
|
|
162
162
|
try {
|
|
@@ -164,7 +164,7 @@ test('listReports shows queued retest status for a report with a matching queued
|
|
|
164
164
|
target: {url: 'https://example.com/mixed', what: 'Mixed Outcomes Page'}
|
|
165
165
|
};
|
|
166
166
|
await fs.writeFile(queueFile, JSON.stringify(queuedJob, null, 2));
|
|
167
|
-
const result = await answer();
|
|
167
|
+
const result: any = await answer();
|
|
168
168
|
assert.equal(result.status, 'ok');
|
|
169
169
|
assert.ok(result.answerPage.includes('Currently in the queue for retesting'));
|
|
170
170
|
}
|
|
@@ -173,18 +173,18 @@ test('listReports shows queued retest status for a report with a matching queued
|
|
|
173
173
|
}
|
|
174
174
|
});
|
|
175
175
|
|
|
176
|
-
test('listReports returns an error when a report file is invalid', async () => {
|
|
177
|
-
const dbDir =
|
|
176
|
+
test('listReports returns an error when a report file is invalid', {timeout: 500}, async () => {
|
|
177
|
+
const dbDir = (await import('../../test/dbFixture.ts')).fixtureDBDir;
|
|
178
178
|
const reportsDir = path.join(dbDir, 'reports');
|
|
179
179
|
const invalidReportPath = path.join(reportsDir, '260101T9999-bad.json');
|
|
180
180
|
try {
|
|
181
|
-
// Valid for getReportExtract (has target and jobData.endTime) but
|
|
181
|
+
// Valid for getReportExtract (has target and jobData.endTime) but not usable by isUsableReport (no acts or catalog).
|
|
182
182
|
const invalidReport = {
|
|
183
183
|
target: {what: 'Invalid Report', url: 'https://example.com/invalid'},
|
|
184
184
|
jobData: {endTime: '26-01-01T00:10'}
|
|
185
185
|
};
|
|
186
186
|
await fs.writeFile(invalidReportPath, JSON.stringify(invalidReport, null, 2));
|
|
187
|
-
const result = await answer();
|
|
187
|
+
const result: any = await answer();
|
|
188
188
|
assert.equal(result.status, 'error');
|
|
189
189
|
assert.ok(result.message);
|
|
190
190
|
}
|
|
@@ -195,26 +195,24 @@ test('listReports returns an error when a report file is invalid', async () => {
|
|
|
195
195
|
|
|
196
196
|
test('listReports shows no-reports message when the database is empty', async () => {
|
|
197
197
|
// Create a temp DB with empty reports and jobs directories.
|
|
198
|
-
const os =
|
|
198
|
+
const os = await import('node:os');
|
|
199
199
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'listreports-empty-'));
|
|
200
200
|
await fs.mkdir(path.join(tmpDir, 'reports'), {recursive: true});
|
|
201
201
|
await fs.mkdir(path.join(tmpDir, 'jobs', 'queue'), {recursive: true});
|
|
202
202
|
await fs.mkdir(path.join(tmpDir, 'jobs', 'claimed'), {recursive: true});
|
|
203
203
|
await fs.mkdir(path.join(tmpDir, 'jobs', 'failed'), {recursive: true});
|
|
204
|
-
await fs.writeFile(path.join(tmpDir, 'jobs', '
|
|
204
|
+
await fs.writeFile(path.join(tmpDir, 'jobs', 'testRequests.json'), '{}\n');
|
|
205
205
|
const savedDBDir = process.env.DB_DIR;
|
|
206
206
|
process.env.DB_DIR = tmpDir;
|
|
207
207
|
try {
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
const result = await answer();
|
|
208
|
+
const {answer} = await import('./index.ts');
|
|
209
|
+
const result: any = await answer();
|
|
211
210
|
assert.equal(result.status, 'ok');
|
|
212
211
|
assert.ok(result.answerPage.includes('no'));
|
|
213
212
|
assert.ok(result.answerPage.includes(' a '));
|
|
214
213
|
}
|
|
215
214
|
finally {
|
|
216
215
|
process.env.DB_DIR = savedDBDir;
|
|
217
|
-
delete require.cache[require.resolve('./index')];
|
|
218
216
|
await fs.rm(tmpDir, {recursive: true}).catch(() => {});
|
|
219
217
|
}
|
|
220
218
|
});
|