@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,56 +1,55 @@
|
|
|
1
1
|
/*
|
|
2
|
-
index.
|
|
2
|
+
index.ts
|
|
3
3
|
List the rules belonging to an issue.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import {
|
|
9
9
|
getWeightName,
|
|
10
10
|
htmlSafe,
|
|
11
|
+
populateTemplate,
|
|
11
12
|
ruleEngines
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const fs = require('fs/promises');
|
|
15
|
-
const path = require('path');
|
|
13
|
+
} from '../../util.ts';
|
|
14
|
+
import {issues as issueSpecs, issueRules, rules as ruleSpecs} from 'testaro-issues';
|
|
16
15
|
|
|
17
16
|
// FUNCTIONS
|
|
18
17
|
|
|
19
18
|
// Adds parameters to a query for the answer page.
|
|
20
|
-
const populateQuery = async (issueID, query) => {
|
|
19
|
+
const populateQuery = async (issueID: string, query: Record<string, any>) => {
|
|
21
20
|
// Add facts about the issue to the query.
|
|
22
21
|
query.issue = issueSpecs[issueID]?.summary;
|
|
23
22
|
if (!query.issue) {
|
|
24
23
|
query.error = 'Issue not found';
|
|
25
24
|
return;
|
|
26
25
|
}
|
|
27
|
-
const issue = issueSpecs[issueID]
|
|
26
|
+
const issue = issueSpecs[issueID]!;
|
|
28
27
|
const {wcag, weight, why} = issue;
|
|
29
28
|
query.why = why;
|
|
30
29
|
query.priority = getWeightName(weight);
|
|
31
30
|
query.wcag = wcag;
|
|
32
31
|
// Initialize the lines.
|
|
33
|
-
const lines = [];
|
|
32
|
+
const lines: string[] = [];
|
|
34
33
|
const margin = ' '.repeat(6);
|
|
35
34
|
// For each rule engine with any rules belonging to the issue:
|
|
36
35
|
Object.keys(issueRules[issueID] ?? {}).forEach(engineID => {
|
|
37
36
|
// Add a heading for the rules of the rule engine.
|
|
38
|
-
lines.push(`${margin}<h3>${ruleEngines[engineID][0]} rules</h3>`);
|
|
39
|
-
const {invariant, variable} = issueRules[issueID][engineID]
|
|
40
|
-
const rulesByType = {
|
|
37
|
+
lines.push(`${margin}<h3>${ruleEngines[engineID]![0]} rules</h3>`);
|
|
38
|
+
const {invariant, variable} = issueRules[issueID]![engineID]!;
|
|
39
|
+
const rulesByType: Record<string, any[]> = {
|
|
41
40
|
invariant,
|
|
42
41
|
variable
|
|
43
42
|
};
|
|
44
43
|
// For each rule variability:
|
|
45
44
|
Object.keys(rulesByType).forEach(typeName => {
|
|
46
45
|
// If any rules of the rule engine belonging to the issue have it:
|
|
47
|
-
if (rulesByType[typeName]
|
|
46
|
+
if (rulesByType[typeName]!.length) {
|
|
48
47
|
// Add a heading for them.
|
|
49
48
|
lines.push(`${margin}<h4>${typeName} rules</h4>`);
|
|
50
49
|
// Add a list of facts about them.
|
|
51
50
|
lines.push(`${margin}<ul>`);
|
|
52
|
-
rulesByType[typeName]
|
|
53
|
-
const {what} = ruleSpecs[engineID][typeName][ruleID];
|
|
51
|
+
rulesByType[typeName]!.forEach(ruleID => {
|
|
52
|
+
const {what} = (ruleSpecs as any)[engineID][typeName][ruleID];
|
|
54
53
|
if (what === ruleID) {
|
|
55
54
|
lines.push(`${margin} <li><code>${htmlSafe(ruleID)}</code></li>`);
|
|
56
55
|
}
|
|
@@ -66,18 +65,14 @@ const populateQuery = async (issueID, query) => {
|
|
|
66
65
|
query.rules = lines.join('\n');
|
|
67
66
|
};
|
|
68
67
|
// Returns a page answering the issue-rules question.
|
|
69
|
-
|
|
70
|
-
const query = {};
|
|
68
|
+
export const answer = async (issueID: string) => {
|
|
69
|
+
const query: Record<string, any> = {};
|
|
71
70
|
// Create a query to replace the placeholders.
|
|
72
71
|
await populateQuery(issueID, query);
|
|
73
72
|
// If this succeeded:
|
|
74
73
|
if (query.issue) {
|
|
75
|
-
// Get the template.
|
|
76
|
-
|
|
77
|
-
// Replace its placeholders.
|
|
78
|
-
Object.keys(query).forEach(param => {
|
|
79
|
-
answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
|
|
80
|
-
});
|
|
74
|
+
// Get the populated template.
|
|
75
|
+
const answerPage = await populateTemplate(import.meta.dirname, query);
|
|
81
76
|
// Return the populated page.
|
|
82
77
|
return {
|
|
83
78
|
status: 'ok',
|
|
@@ -1,49 +1,56 @@
|
|
|
1
1
|
/*
|
|
2
|
-
listRules.test.
|
|
3
|
-
Unit tests for web/listRules/index.
|
|
2
|
+
listRules.test.ts
|
|
3
|
+
Unit tests for web/listRules/index.ts.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
import {test, mock} from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import {parse} from 'node-html-parser';
|
|
11
11
|
|
|
12
|
-
//
|
|
13
|
-
// covering the dead branch on line 55 of listRules/index.
|
|
14
|
-
|
|
12
|
+
// Mock testaro-issues with an added rule having what === ruleID,
|
|
13
|
+
// covering the dead branch on line 55 of listRules/index.ts.
|
|
14
|
+
import * as testaroIssues from 'testaro-issues';
|
|
15
15
|
const originalRules = testaroIssues.rules;
|
|
16
|
-
const patchEngine = Object.keys(originalRules)[0]
|
|
17
|
-
const patchType = Object.keys(originalRules[patchEngine])[0]
|
|
16
|
+
const patchEngine = Object.keys(originalRules)[0]!;
|
|
17
|
+
const patchType = Object.keys((originalRules as any)[patchEngine])[0]!;
|
|
18
18
|
const patchRuleID = '__testWhatEqualsID__';
|
|
19
|
-
|
|
19
|
+
const patchedRules = {
|
|
20
20
|
...originalRules,
|
|
21
21
|
[patchEngine]: {
|
|
22
|
-
...originalRules[patchEngine],
|
|
22
|
+
...(originalRules as any)[patchEngine],
|
|
23
23
|
[patchType]: {
|
|
24
|
-
...originalRules[patchEngine][patchType],
|
|
24
|
+
...(originalRules as any)[patchEngine][patchType],
|
|
25
25
|
[patchRuleID]: {what: patchRuleID}
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
};
|
|
29
29
|
// Add the patched rule to an existing issue's rule list.
|
|
30
|
-
const patchIssueID = Object.keys(testaroIssues.issueRules)[0];
|
|
31
30
|
const originalIssueRules = testaroIssues.issueRules;
|
|
32
|
-
|
|
31
|
+
const patchIssueID = Object.keys(originalIssueRules)[0]!;
|
|
32
|
+
const patchedIssueRules = {
|
|
33
33
|
...originalIssueRules,
|
|
34
34
|
[patchIssueID]: {
|
|
35
|
-
...originalIssueRules[patchIssueID],
|
|
35
|
+
...(originalIssueRules as any)[patchIssueID],
|
|
36
36
|
[patchEngine]: {
|
|
37
|
-
...originalIssueRules[patchIssueID][patchEngine],
|
|
37
|
+
...(originalIssueRules as any)[patchIssueID][patchEngine],
|
|
38
38
|
[patchType]: [
|
|
39
|
-
...originalIssueRules[patchIssueID][patchEngine][patchType],
|
|
39
|
+
...(originalIssueRules as any)[patchIssueID][patchEngine][patchType],
|
|
40
40
|
patchRuleID
|
|
41
41
|
]
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
|
+
mock.module('testaro-issues', {
|
|
46
|
+
exports: {
|
|
47
|
+
...testaroIssues,
|
|
48
|
+
rules: patchedRules,
|
|
49
|
+
issueRules: patchedIssueRules
|
|
50
|
+
}
|
|
51
|
+
} as any);
|
|
45
52
|
|
|
46
|
-
const {answer} =
|
|
53
|
+
const {answer} = await import('./index.ts') as any;
|
|
47
54
|
|
|
48
55
|
// TESTS
|
|
49
56
|
|
|
@@ -1,67 +1,60 @@
|
|
|
1
1
|
/*
|
|
2
|
-
index.
|
|
2
|
+
index.ts
|
|
3
3
|
Lists the most commonly reported issues in all available latest reports.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import {sendAlert} from '../../alerts.ts';
|
|
9
|
+
import {
|
|
10
10
|
getEngineNamesString,
|
|
11
|
-
|
|
11
|
+
getReportExtracts,
|
|
12
12
|
getReport,
|
|
13
|
+
getTestActInstances,
|
|
13
14
|
getWCAGLink,
|
|
14
15
|
getWeightName,
|
|
16
|
+
isReportError,
|
|
15
17
|
objectSort,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const path = require('path');
|
|
18
|
+
populateTemplate,
|
|
19
|
+
} from '../../util.ts';
|
|
20
|
+
import {issues as issueSpecs} from 'testaro-issues';
|
|
20
21
|
|
|
21
22
|
// FUNCTIONS
|
|
22
23
|
|
|
23
24
|
// Gets summary data on the issues reported in a set of reports.
|
|
24
25
|
const getIssuesSummary = async () => {
|
|
25
26
|
// Initialize the summary.
|
|
26
|
-
const summary = {
|
|
27
|
+
const summary: Record<string, any> = {
|
|
27
28
|
totalCount: 0,
|
|
28
29
|
issues: []
|
|
29
30
|
};
|
|
30
31
|
// Initialize data for a summary.
|
|
31
|
-
const issuesData = {};
|
|
32
|
+
const issuesData: Record<string, any> = {};
|
|
32
33
|
// Get extracts of the latest available report on each page.
|
|
33
|
-
const latestReportExtracts = await
|
|
34
|
+
const latestReportExtracts = await getReportExtracts(true);
|
|
34
35
|
// For each of them:
|
|
35
36
|
for (const reportExtract of latestReportExtracts) {
|
|
36
37
|
const {timeStamp, jobID} = reportExtract;
|
|
37
38
|
// Get the corresponding report.
|
|
38
39
|
const report = await getReport(timeStamp, jobID);
|
|
39
|
-
const {acts = [], error} = report;
|
|
40
40
|
// If this failed:
|
|
41
|
-
if (
|
|
41
|
+
if (isReportError(report)) {
|
|
42
42
|
// Populate the summary with the reason.
|
|
43
|
-
summary.error = error;
|
|
43
|
+
summary.error = report.error;
|
|
44
|
+
continue;
|
|
44
45
|
}
|
|
45
|
-
// For each
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
count: 0,
|
|
58
|
-
reporters: new Set()
|
|
59
|
-
};
|
|
60
|
-
// Increment the data with the count and reporter of the instance.
|
|
61
|
-
issuesData[issueID].count += count ?? 1;
|
|
62
|
-
issuesData[issueID].reporters.add(which);
|
|
63
|
-
}
|
|
64
|
-
});
|
|
46
|
+
// For each violating standard instance of each test act in it:
|
|
47
|
+
getTestActInstances(report, {violationsOnly: true}).forEach(({act, instance}) => {
|
|
48
|
+
const {count, issueID} = instance;
|
|
49
|
+
// If the instance has a non-ignorable issue ID:
|
|
50
|
+
if (issueID && issueID !== 'ignorable') {
|
|
51
|
+
issuesData[issueID] ??= {
|
|
52
|
+
count: 0,
|
|
53
|
+
reporters: new Set()
|
|
54
|
+
};
|
|
55
|
+
// Increment the data with the count and reporter of the instance.
|
|
56
|
+
issuesData[issueID].count += count ?? 1;
|
|
57
|
+
issuesData[issueID].reporters.add(act.which);
|
|
65
58
|
}
|
|
66
59
|
});
|
|
67
60
|
}
|
|
@@ -90,7 +83,7 @@ const getIssuesSummary = async () => {
|
|
|
90
83
|
}
|
|
91
84
|
});
|
|
92
85
|
// For each summarized issue:
|
|
93
|
-
summary.issues.forEach(issue => {
|
|
86
|
+
summary.issues.forEach((issue: any) => {
|
|
94
87
|
// Add its percentage to its entry.
|
|
95
88
|
issue.percentage = Math.round(100 * (issue.count / summary.totalCount));
|
|
96
89
|
});
|
|
@@ -102,7 +95,7 @@ const getIssuesSummary = async () => {
|
|
|
102
95
|
return summary;
|
|
103
96
|
};
|
|
104
97
|
// Adds parameters to a query for the answer page.
|
|
105
|
-
const populateQuery = async query => {
|
|
98
|
+
const populateQuery = async (query: Record<string, any>) => {
|
|
106
99
|
// Get summary data on the issues.
|
|
107
100
|
const issuesSummary = await getIssuesSummary();
|
|
108
101
|
// If this failed:
|
|
@@ -113,7 +106,7 @@ const populateQuery = async query => {
|
|
|
113
106
|
return;
|
|
114
107
|
}
|
|
115
108
|
// Otherwise, i.e. if it succeeded, initialize the lines.
|
|
116
|
-
const lines = [];
|
|
109
|
+
const lines: string[] = [];
|
|
117
110
|
const margin = ' '.repeat(6);
|
|
118
111
|
// For each weight:
|
|
119
112
|
[4, 3, 2, 1].forEach(weight => {
|
|
@@ -123,13 +116,13 @@ const populateQuery = async query => {
|
|
|
123
116
|
lines.push(`${margin}<ul>`);
|
|
124
117
|
let existsIssue = false;
|
|
125
118
|
// For each reported issue:
|
|
126
|
-
reportedIssues.forEach(reportedIssue => {
|
|
119
|
+
reportedIssues.forEach((reportedIssue: any) => {
|
|
127
120
|
const {issueID, percentage, reporters} = reportedIssue;
|
|
128
121
|
// If it has the weight and its percentage is at least 2:
|
|
129
122
|
if (reportedIssue.weight === weight && percentage >= 2) {
|
|
130
123
|
existsIssue = true;
|
|
131
124
|
// Get the data on it from the issue classification.
|
|
132
|
-
const issue = issueSpecs[issueID]
|
|
125
|
+
const issue = issueSpecs[issueID]!;
|
|
133
126
|
const {summary, wcag, why} = issue;
|
|
134
127
|
const wcagLink = `<a href="${getWCAGLink(wcag)}">${wcag}</a>`;
|
|
135
128
|
// Add a description of it to the lines.
|
|
@@ -160,8 +153,8 @@ const populateQuery = async query => {
|
|
|
160
153
|
query.issues = lines.join('\n');
|
|
161
154
|
};
|
|
162
155
|
// Returns a page answering the issues question.
|
|
163
|
-
|
|
164
|
-
const query = {};
|
|
156
|
+
export const answer = async () => {
|
|
157
|
+
const query: Record<string, any> = {};
|
|
165
158
|
// Create a query to replace placeholders.
|
|
166
159
|
await populateQuery(query);
|
|
167
160
|
// If the query reports an error:
|
|
@@ -172,12 +165,8 @@ exports.answer = async () => {
|
|
|
172
165
|
message: query.error
|
|
173
166
|
};
|
|
174
167
|
}
|
|
175
|
-
// Otherwise, i.e. if the query does not report an error, get the template.
|
|
176
|
-
|
|
177
|
-
// Replace its placeholders.
|
|
178
|
-
Object.keys(query).forEach(param => {
|
|
179
|
-
answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
|
|
180
|
-
});
|
|
168
|
+
// Otherwise, i.e. if the query does not report an error, get the populated template.
|
|
169
|
+
const answerPage = await populateTemplate(import.meta.dirname, query);
|
|
181
170
|
// Return the populated page.
|
|
182
171
|
return {
|
|
183
172
|
status: 'ok',
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/*
|
|
2
|
-
listTopIssues.test.
|
|
3
|
-
UI tests for web/listTopIssues/index.
|
|
2
|
+
listTopIssues.test.ts
|
|
3
|
+
UI tests for web/listTopIssues/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
|
-
before(() => {
|
|
20
|
-
process.env.DB_DIR =
|
|
19
|
+
before(async () => {
|
|
20
|
+
process.env.DB_DIR = (await import('../../test/dbFixture.ts')).fixtureDBDir;
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
after(() => {
|
|
@@ -32,7 +32,7 @@ after(() => {
|
|
|
32
32
|
// TESTS
|
|
33
33
|
|
|
34
34
|
test('listTopIssues returns an ok status with valid HTML', async () => {
|
|
35
|
-
const result = await answer();
|
|
35
|
+
const result: any = await answer();
|
|
36
36
|
assert.equal(result.status, 'ok');
|
|
37
37
|
assert.ok(result.answerPage);
|
|
38
38
|
const html = parse(result.answerPage);
|
|
@@ -40,23 +40,32 @@ test('listTopIssues returns an ok status with valid HTML', async () => {
|
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
test('listTopIssues includes priority headings in the page', async () => {
|
|
43
|
-
const result = await answer();
|
|
43
|
+
const result: any = await answer();
|
|
44
44
|
assert.equal(result.status, 'ok');
|
|
45
45
|
assert.ok(result.answerPage.includes('priority'));
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
test('listTopIssues excludes cantTell instances from issue counts', async () => {
|
|
49
|
+
// In the fixture corpus, focusIndicationBad is reported only with a cantTell outcome
|
|
50
|
+
// (in the mixed and all-cantTell reports), never as a violation, so it should not
|
|
51
|
+
// appear in the summary.
|
|
52
|
+
const result: any = await answer();
|
|
53
|
+
assert.equal(result.status, 'ok');
|
|
54
|
+
assert.ok(!result.answerPage.includes('focus indication poor'));
|
|
55
|
+
});
|
|
56
|
+
|
|
48
57
|
test('listTopIssues returns an error when a report file is invalid', async () => {
|
|
49
|
-
const dbDir =
|
|
58
|
+
const dbDir = (await import('../../test/dbFixture.ts')).fixtureDBDir;
|
|
50
59
|
const reportsDir = path.join(dbDir, 'reports');
|
|
51
60
|
const invalidReportPath = path.join(reportsDir, '260101T9999-bad.json');
|
|
52
61
|
try {
|
|
53
|
-
// Valid for getReportExtract (has target and jobData.endTime) but
|
|
62
|
+
// Valid for getReportExtract (has target and jobData.endTime) but not usable by isUsableReport (no acts or catalog).
|
|
54
63
|
const invalidReport = {
|
|
55
64
|
target: {what: 'Invalid TopIssues Report', url: 'https://example.com/invalidtop'},
|
|
56
65
|
jobData: {endTime: '26-01-01T00:10'}
|
|
57
66
|
};
|
|
58
67
|
await fs.writeFile(invalidReportPath, JSON.stringify(invalidReport, null, 2));
|
|
59
|
-
const result = await answer();
|
|
68
|
+
const result: any = await answer();
|
|
60
69
|
assert.equal(result.status, 'error');
|
|
61
70
|
assert.ok(result.message);
|
|
62
71
|
}
|
|
@@ -66,7 +75,7 @@ test('listTopIssues returns an error when a report file is invalid', async () =>
|
|
|
66
75
|
});
|
|
67
76
|
|
|
68
77
|
test('listTopIssues handles reports with unclassified issue IDs', async () => {
|
|
69
|
-
const dbDir =
|
|
78
|
+
const dbDir = (await import('../../test/dbFixture.ts')).fixtureDBDir;
|
|
70
79
|
const reportsDir = path.join(dbDir, 'reports');
|
|
71
80
|
const reportPath = path.join(reportsDir, '260101T0004-unc.json');
|
|
72
81
|
try {
|
|
@@ -115,7 +124,7 @@ test('listTopIssues handles reports with unclassified issue IDs', async () => {
|
|
|
115
124
|
checkpoints: []
|
|
116
125
|
};
|
|
117
126
|
await fs.writeFile(reportPath, JSON.stringify(report, null, 2));
|
|
118
|
-
const result = await answer();
|
|
127
|
+
const result: any = await answer();
|
|
119
128
|
// The unclassified issue is skipped, but the page should still render successfully.
|
|
120
129
|
assert.equal(result.status, 'ok');
|
|
121
130
|
assert.ok(result.answerPage);
|
|
@@ -1,34 +1,39 @@
|
|
|
1
1
|
/*
|
|
2
|
-
index.
|
|
2
|
+
index.ts
|
|
3
3
|
Lists the violators of an issue in a report.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import {
|
|
9
9
|
getPageDataStrings,
|
|
10
10
|
getPathID,
|
|
11
11
|
getReport,
|
|
12
12
|
getEngineNamesString,
|
|
13
|
+
getTestActInstances,
|
|
13
14
|
getTextFragmentHref,
|
|
14
15
|
getWCAGLink,
|
|
15
16
|
getWeightName,
|
|
16
17
|
htmlSafe,
|
|
17
|
-
|
|
18
|
+
isReportError,
|
|
18
19
|
makeBreakable,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const path = require('path');
|
|
20
|
+
populateTemplate,
|
|
21
|
+
} from '../../util.ts';
|
|
22
|
+
import {issues as issueSpecs} from 'testaro-issues';
|
|
23
23
|
|
|
24
24
|
// FUNCTIONS
|
|
25
25
|
|
|
26
26
|
// Adds parameters to a query for the answer page.
|
|
27
|
-
const populateQuery = async (
|
|
27
|
+
const populateQuery = async (
|
|
28
|
+
issueID: string,
|
|
29
|
+
timeStamp: string,
|
|
30
|
+
jobID: string,
|
|
31
|
+
query: Record<string, any>
|
|
32
|
+
) => {
|
|
28
33
|
// Get descriptions of the page facts.
|
|
29
34
|
const pageDataStrings = await getPageDataStrings(timeStamp, jobID);
|
|
30
35
|
// If this failed:
|
|
31
|
-
if (pageDataStrings.error) {
|
|
36
|
+
if (pageDataStrings.error !== undefined) {
|
|
32
37
|
// Populate the query with the reason.
|
|
33
38
|
query.error = pageDataStrings.error;
|
|
34
39
|
// Stop populating the query.
|
|
@@ -43,12 +48,12 @@ const populateQuery = async (issueID, timeStamp, jobID, query) => {
|
|
|
43
48
|
// Stop populating the query.
|
|
44
49
|
return;
|
|
45
50
|
}
|
|
46
|
-
const {testInfo, url, urlLink,
|
|
51
|
+
const {testInfo, url, urlLink, description} = pageDataStrings;
|
|
47
52
|
// Add page facts to the query.
|
|
48
|
-
query.target =
|
|
53
|
+
query.target = description;
|
|
49
54
|
query.urlLink = urlLink;
|
|
50
55
|
query.testInfo = testInfo;
|
|
51
|
-
const issue = issueSpecs[issueID]
|
|
56
|
+
const issue = issueSpecs[issueID]!;
|
|
52
57
|
const {wcag, weight, why} = issue;
|
|
53
58
|
query.why = why;
|
|
54
59
|
query.priority = getWeightName(weight);
|
|
@@ -56,51 +61,38 @@ const populateQuery = async (issueID, timeStamp, jobID, query) => {
|
|
|
56
61
|
// Initialize those whose values depend on instance inspection.
|
|
57
62
|
query.count = 0;
|
|
58
63
|
query.reporters = new Set();
|
|
59
|
-
let violators = {};
|
|
64
|
+
let violators: any = {};
|
|
60
65
|
// Get the report.
|
|
61
66
|
const report = await getReport(timeStamp, jobID);
|
|
62
|
-
const {acts, catalog} = report;
|
|
63
67
|
// If this failed:
|
|
64
|
-
if (report
|
|
68
|
+
if (isReportError(report)) {
|
|
65
69
|
// Populate the query with the reason.
|
|
66
70
|
query.error = report.error;
|
|
67
71
|
// Stop populating the query.
|
|
68
72
|
return;
|
|
69
73
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const tagName = catalog[catalogIndex]?.tagName
|
|
87
|
-
?? pathID.split('/').pop().replace(/\[.+$/, '').toUpperCase();
|
|
88
|
-
violators[catalogIndex] ??= {
|
|
89
|
-
pathID: getPathID(catalog, catalogIndex, pathID),
|
|
90
|
-
tagName,
|
|
91
|
-
text: catalog[catalogIndex]?.text ?? '',
|
|
92
|
-
reporters: new Set()
|
|
93
|
-
};
|
|
94
|
-
// Ensure that the rule engine is in the sets of reporters of the violator and the issue.
|
|
95
|
-
violators[catalogIndex].reporters.add(which);
|
|
96
|
-
query.reporters.add(which);
|
|
97
|
-
});
|
|
98
|
-
// Populate the violator count.
|
|
99
|
-
const violatorCount = Object.keys(violators).length;
|
|
100
|
-
query.violatorCount = violatorCount === 1 ? '1 violator was' : `${violatorCount} violators were`;
|
|
74
|
+
const {catalog} = report;
|
|
75
|
+
// Otherwise, i.e. if it succeeded, for each violating standard instance of the issue:
|
|
76
|
+
getTestActInstances(report, {violationsOnly: true, issueID}).forEach(({act, instance}) => {
|
|
77
|
+
const pathID = instance.pathID || '/html';
|
|
78
|
+
const catalogIndex = String(instance.catalogIndex || '0');
|
|
79
|
+
const tagName = catalog[catalogIndex]?.tagName
|
|
80
|
+
?? pathID.split('/').pop()!.replace(/\[.+$/, '').toUpperCase();
|
|
81
|
+
violators[catalogIndex] ??= {
|
|
82
|
+
pathID: getPathID(catalog, catalogIndex, pathID),
|
|
83
|
+
tagName,
|
|
84
|
+
text: catalog[catalogIndex]?.text ?? '',
|
|
85
|
+
reporters: new Set()
|
|
86
|
+
};
|
|
87
|
+
// Ensure that the rule engine is in the sets of reporters of the violator and the issue.
|
|
88
|
+
violators[catalogIndex].reporters.add(act.which);
|
|
89
|
+
query.reporters.add(act.which);
|
|
101
90
|
});
|
|
91
|
+
// Populate the violator count.
|
|
92
|
+
const violatorCount = Object.keys(violators).length;
|
|
93
|
+
query.violatorCount = violatorCount === 1 ? '1 violator was' : `${violatorCount} violators were`;
|
|
102
94
|
// For each violator:
|
|
103
|
-
Object.values(violators).forEach(violatorData => {
|
|
95
|
+
Object.values(violators).forEach((violatorData: any) => {
|
|
104
96
|
// Convert the set of its reporters to a string.
|
|
105
97
|
violatorData.reporters = getEngineNamesString(violatorData.reporters);
|
|
106
98
|
});
|
|
@@ -109,18 +101,18 @@ const populateQuery = async (issueID, timeStamp, jobID, query) => {
|
|
|
109
101
|
// Convert the set of issue reporters to a string.
|
|
110
102
|
query.reporters = getEngineNamesString(query.reporters);
|
|
111
103
|
// Convert the violator data to an array.
|
|
112
|
-
violators = Object.entries(violators).map(entry => ({
|
|
104
|
+
violators = Object.entries(violators).map((entry: [string, any]) => ({
|
|
113
105
|
catalogIndex: entry[0],
|
|
114
106
|
...entry[1]
|
|
115
107
|
}));
|
|
116
108
|
// Sort the violators in XPath order.
|
|
117
|
-
violators.sort((a, b) => a.pathID.localeCompare(b.pathID));
|
|
109
|
+
violators.sort((a: any, b: any) => a.pathID.localeCompare(b.pathID));
|
|
118
110
|
// Initialize the lines.
|
|
119
|
-
const lines = [];
|
|
111
|
+
const lines: string[] = [];
|
|
120
112
|
const margin = ' '.repeat(6);
|
|
121
113
|
let takeMeAdviceNeeded = false;
|
|
122
114
|
// For each violator:
|
|
123
|
-
violators.forEach((violator, index) => {
|
|
115
|
+
violators.forEach((violator: any, index: number) => {
|
|
124
116
|
const {catalogIndex, pathID, reporters, tagName, text} = violator;
|
|
125
117
|
// Add a heading to the lines.
|
|
126
118
|
lines.push(`${margin}<li><h3>Element ${catalogIndex}</h3>`);
|
|
@@ -140,10 +132,10 @@ const populateQuery = async (issueID, timeStamp, jobID, query) => {
|
|
|
140
132
|
lines.push(`${margin} </ul>`);
|
|
141
133
|
lines.push(`${margin} <ul class="nav">`);
|
|
142
134
|
if (catalogIndex) {
|
|
143
|
-
const catalogItem = catalog[catalogIndex]
|
|
144
|
-
if (catalogItem
|
|
135
|
+
const catalogItem = catalog[catalogIndex];
|
|
136
|
+
if (catalogItem?.textLinkable) {
|
|
145
137
|
takeMeAdviceNeeded = true;
|
|
146
|
-
const href = getTextFragmentHref(catalogItem.text, url);
|
|
138
|
+
const href = getTextFragmentHref(catalogItem.text as string, url);
|
|
147
139
|
const label = `Take me to element ${catalogIndex} on the page (in a new tab)`;
|
|
148
140
|
const takeMeLink = `<a href="${href}" target="_blank" aria-label="${label}">Take me there</a>`;
|
|
149
141
|
lines.push(`${margin} <li>${takeMeLink}</li>`);
|
|
@@ -169,17 +161,9 @@ const populateQuery = async (issueID, timeStamp, jobID, query) => {
|
|
|
169
161
|
}
|
|
170
162
|
};
|
|
171
163
|
// Returns a page answering the violators question.
|
|
172
|
-
|
|
173
|
-
const [issueID, timeStamp, jobID] = pageArgs.split('/');
|
|
174
|
-
const
|
|
175
|
-
// If the report is not available:
|
|
176
|
-
if (reportIsHidden) {
|
|
177
|
-
return {
|
|
178
|
-
status: 'error',
|
|
179
|
-
message: 'Report not available'
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
const query = {};
|
|
164
|
+
export const answer = async (pageArgs: string) => {
|
|
165
|
+
const [issueID, timeStamp, jobID] = pageArgs.split('/') as [string, string, string];
|
|
166
|
+
const query: Record<string, any> = {};
|
|
183
167
|
// Create a query to replace the placeholders.
|
|
184
168
|
await populateQuery(issueID, timeStamp, jobID, query);
|
|
185
169
|
// If this failed:
|
|
@@ -192,12 +176,8 @@ exports.answer = async pageArgs => {
|
|
|
192
176
|
}
|
|
193
177
|
// Otherwise, if it succeeded and the report facts were obtained:
|
|
194
178
|
if (query.testInfo) {
|
|
195
|
-
// Get the template.
|
|
196
|
-
|
|
197
|
-
// Replace its placeholders.
|
|
198
|
-
Object.keys(query).forEach(param => {
|
|
199
|
-
answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
|
|
200
|
-
});
|
|
179
|
+
// Get the populated template.
|
|
180
|
+
const answerPage = await populateTemplate(import.meta.dirname, query);
|
|
201
181
|
// Return the populated page.
|
|
202
182
|
return {
|
|
203
183
|
status: 'ok',
|