@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.
Files changed (147) hide show
  1. package/.claude/settings.local.json +13 -1
  2. package/.devin/config.local.json +5 -1
  3. package/.github/workflows/ci.yml +0 -3
  4. package/.github/workflows/periodic-smoke-tests.yml +30 -0
  5. package/.github/workflows/publish-mcp.yml +4 -4
  6. package/{alerts.test.js → alerts.test.ts} +17 -29
  7. package/{alerts.js → alerts.ts} +10 -13
  8. package/api/{getReport.test.js → getReport.test.ts} +9 -10
  9. package/api/{getReport.js → getReport.ts} +23 -24
  10. package/api/{listDiagnoses.test.js → listDiagnoses.test.ts} +26 -27
  11. package/api/{listDiagnoses.js → listDiagnoses.ts} +46 -70
  12. package/api/{listIssues.test.js → listIssues.test.ts} +21 -14
  13. package/api/{listIssues.js → listIssues.ts} +75 -74
  14. package/api/{listReports.test.js → listReports.test.ts} +17 -16
  15. package/api/listReports.ts +87 -0
  16. package/api/{listViolators.test.js → listViolators.test.ts} +19 -20
  17. package/api/{listViolators.js → listViolators.ts} +45 -70
  18. package/api/requestFeature.test.ts +54 -0
  19. package/api/{requestFeature.js → requestFeature.ts} +18 -15
  20. package/api/{requestRetest.test.js → requestRetest.test.ts} +33 -28
  21. package/api/{requestRetest.js → requestRetest.ts} +32 -25
  22. package/api/{requestTest.test.js → requestTest.test.ts} +35 -30
  23. package/api/{requestTest.js → requestTest.ts} +35 -28
  24. package/api/{routes.test.js → routes.test.ts} +5 -5
  25. package/api/{routes.js → routes.ts} +6 -6
  26. package/api/{schemas.js → schemas.ts} +69 -50
  27. package/api/{util.test.js → util.test.ts} +31 -11
  28. package/api/{util.js → util.ts} +61 -36
  29. package/api/version.ts +8 -0
  30. package/{apiIntegration.test.js → apiIntegration.test.ts} +12 -13
  31. package/balances.ts +87 -0
  32. package/{checkinfo.js → checkinfo.ts} +24 -20
  33. package/docs/AI-TOOLS.md +4 -4
  34. package/docs/BACKLOG.md +356 -0
  35. package/docs/SERVICE.md +144 -17
  36. package/docs/decisions/002-coverage-tool.md +40 -0
  37. package/docs/decisions/003-adr-threshold.md +34 -0
  38. package/docs/decisions/004-docs-directory.md +23 -0
  39. package/docs/decisions/005-observability.md +28 -0
  40. package/docs/decisions/006-git-workflow.md +28 -0
  41. package/docs/decisions/007-internal-testing.md +34 -0
  42. package/docs/decisions/008-dependencies.md +28 -0
  43. package/docs/decisions/009-qai-integration.md +40 -0
  44. package/docs/decisions/QAI-HISTORICAL-CONTEXT.md +42 -0
  45. package/eslint.config.mjs +27 -0
  46. package/index.html +3 -3
  47. package/{index.test.js → index.test.ts} +380 -143
  48. package/{index.js → index.ts} +382 -283
  49. package/{indexBalances.test.js → indexBalances.test.ts} +21 -17
  50. package/{indexServe.test.js → indexServe.test.ts} +9 -9
  51. package/linkValidation.test.ts +190 -0
  52. package/{mcp.test.js → mcp.test.ts} +53 -26
  53. package/{mcp.js → mcp.ts} +29 -20
  54. package/memory/MEMORY.md +7 -0
  55. package/memory/user_maintainer_context.md +24 -0
  56. package/openapi.yaml +115 -68
  57. package/package.json +35 -15
  58. package/pm2.config.cjs +29 -0
  59. package/{routes.test.js → routes.test.ts} +8 -7
  60. package/{routing.test.js → routing.test.ts} +11 -12
  61. package/scripts/{generate-openapi.js → generate-openapi.ts} +13 -13
  62. package/serve.ts +15 -0
  63. package/smokeTest.ts +234 -0
  64. package/test/dbFixture.ts +21 -0
  65. package/test/fixtures/{buildFixtures.js → buildFixtures.ts} +14 -13
  66. package/test/{setup.cjs → setup.ts} +2 -2
  67. package/tsconfig.json +16 -0
  68. package/{util.test.js → util.test.ts} +350 -257
  69. package/util.ts +921 -0
  70. package/web/ai0BalanceForm/{ai0BalanceForm.test.js → ai0BalanceForm.test.ts} +15 -15
  71. package/web/ai0BalanceForm/{index.js → index.ts} +10 -18
  72. package/web/enqueue/index.ts +56 -0
  73. package/web/enqueueForm/index.html +9 -9
  74. package/web/enqueueForm/index.ts +43 -0
  75. package/web/expungeReportsForm/{expungeReportsForm.test.js → expungeReportsForm.test.ts} +18 -20
  76. package/web/expungeReportsForm/index.ts +18 -0
  77. package/web/hideReportForm/{hideReportForm.test.js → hideReportForm.test.ts} +14 -14
  78. package/web/hideReportForm/{index.js → index.ts} +17 -21
  79. package/web/listDiagnoses/{index.js → index.ts} +48 -58
  80. package/web/listDiagnoses/{listDiagnoses.test.js → listDiagnoses.test.ts} +39 -34
  81. package/web/listIssues/{index.js → index.ts} +67 -92
  82. package/web/listIssues/{listIssues.test.js → listIssues.test.ts} +45 -51
  83. package/web/listReports/index.html +3 -3
  84. package/web/listReports/{index.js → index.ts} +48 -53
  85. package/web/listReports/{listReports.test.js → listReports.test.ts} +41 -43
  86. package/web/listRules/{index.js → index.ts} +18 -23
  87. package/web/listRules/{listRules.test.js → listRules.test.ts} +26 -19
  88. package/web/listTopIssues/{index.js → index.ts} +36 -47
  89. package/web/listTopIssues/{listTopIssues.test.js → listTopIssues.test.ts} +26 -17
  90. package/web/listViolators/{index.js → index.ts} +51 -71
  91. package/web/listViolators/{listViolators.test.js → listViolators.test.ts} +43 -36
  92. package/web/manage/index.ts +22 -0
  93. package/web/pruneReportsForm/index.ts +18 -0
  94. package/web/pruneReportsForm/{pruneReportsForm.test.js → pruneReportsForm.test.ts} +11 -11
  95. package/web/reannotate/index.ts +70 -0
  96. package/web/reannotate/{reannotate.test.js → reannotate.test.ts} +19 -22
  97. package/web/reannotateForm/{index.js → index.ts} +41 -50
  98. package/web/reannotateForm/{reannotateForm.test.js → reannotateForm.test.ts} +13 -16
  99. package/web/renewWCAG/{index.js → index.ts} +9 -9
  100. package/web/renewWCAG/{renewWCAG.test.js → renewWCAG.test.ts} +17 -20
  101. package/web/renewWCAGForm/index.ts +22 -0
  102. package/web/{expungeReportsForm/index.js → reportDeletion.ts} +56 -38
  103. package/web/requestRetest/index.html +7 -7
  104. package/web/requestRetest/index.ts +31 -0
  105. package/web/requestRetest/requestRetest.test.ts +116 -0
  106. package/web/requestRetestForm/{index.js → index.ts} +10 -16
  107. package/web/requestRetestForm/{requestRetestForm.test.js → requestRetestForm.test.ts} +11 -12
  108. package/web/requestTest/index.html +7 -7
  109. package/web/requestTest/{index.js → index.ts} +5 -5
  110. package/web/requestTest/{requestTest.test.js → requestTest.test.ts} +21 -21
  111. package/web/requestTestForm/index.html +4 -4
  112. package/web/requestTestForm/index.ts +21 -0
  113. package/web/requestTestForm/{requestTestForm.test.js → requestTestForm.test.ts} +9 -9
  114. package/web/rewindReportsForm/index.ts +18 -0
  115. package/web/rewindReportsForm/{rewindReportsForm.test.js → rewindReportsForm.test.ts} +11 -11
  116. package/web/tutorialAI/index.html +184 -0
  117. package/web/tutorialAI/index.ts +80 -0
  118. package/web/tutorialAI/tutorialAI.test.ts +192 -0
  119. package/web/{tutorial → tutorialWeb}/index.html +9 -8
  120. package/web/tutorialWeb/index.ts +78 -0
  121. package/web/tutorialWeb/tutorialWeb.test.ts +195 -0
  122. package/web/unhideReportForm/{index.js → index.ts} +17 -21
  123. package/web/unhideReportForm/{unhideReportForm.test.js → unhideReportForm.test.ts} +16 -16
  124. package/aceconfig.js +0 -28
  125. package/api/listReports.js +0 -89
  126. package/api/requestFeature.test.js +0 -58
  127. package/api/version.js +0 -8
  128. package/indexFallback.test.js +0 -33
  129. package/indexWorkers.test.js +0 -79
  130. package/jsconfig.json +0 -8
  131. package/pm2.config.js +0 -18
  132. package/smokeTest.js +0 -128
  133. package/util.js +0 -744
  134. package/web/enqueue/index.js +0 -67
  135. package/web/enqueueForm/index.js +0 -49
  136. package/web/manage/index.js +0 -22
  137. package/web/pruneReportsForm/index.js +0 -123
  138. package/web/reannotate/index.js +0 -58
  139. package/web/renewWCAGForm/index.js +0 -22
  140. package/web/requestRetest/index.js +0 -31
  141. package/web/requestRetest/requestRetest.test.js +0 -119
  142. package/web/requestTestForm/index.js +0 -21
  143. package/web/rewindReportsForm/index.js +0 -121
  144. package/web/tutorial/index.js +0 -63
  145. package/web/tutorial/tutorial.test.js +0 -97
  146. /package/test/fixtures/db/jobs/{recs.json → testRequests.json} +0 -0
  147. /package/web/{tutorial → tutorialWeb}/images/newsletter-form.png +0 -0
@@ -1,56 +1,55 @@
1
1
  /*
2
- index.js
2
+ index.ts
3
3
  List the rules belonging to an issue.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {
8
+ import {
9
9
  getWeightName,
10
10
  htmlSafe,
11
+ populateTemplate,
11
12
  ruleEngines
12
- } = require('../../util');
13
- const {issues: issueSpecs, issueRules, rules: ruleSpecs} = require('testaro-issues');
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].length) {
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].forEach(ruleID => {
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
- exports.answer = async issueID => {
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
- let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
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.js
3
- Unit tests for web/listRules/index.js.
2
+ listRules.test.ts
3
+ Unit tests for web/listRules/index.ts.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {test} = require('node:test');
9
- const assert = require('node:assert/strict');
10
- const {parse} = require('node-html-parser');
8
+ import {test, mock} from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import {parse} from 'node-html-parser';
11
11
 
12
- // Monkey-patch testaro-issues to add a rule with what === ruleID,
13
- // covering the dead branch on line 55 of listRules/index.js.
14
- const testaroIssues = require('testaro-issues');
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
- testaroIssues.rules = {
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
- testaroIssues.issueRules = {
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} = require('./index');
53
+ const {answer} = await import('./index.ts') as any;
47
54
 
48
55
  // TESTS
49
56
 
@@ -1,67 +1,60 @@
1
1
  /*
2
- index.js
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
- const {sendAlert} = require('../../alerts');
9
- const {
8
+ import {sendAlert} from '../../alerts.ts';
9
+ import {
10
10
  getEngineNamesString,
11
- getLatestReportExtracts,
11
+ getReportExtracts,
12
12
  getReport,
13
+ getTestActInstances,
13
14
  getWCAGLink,
14
15
  getWeightName,
16
+ isReportError,
15
17
  objectSort,
16
- } = require('../../util');
17
- const {issues: issueSpecs} = require('testaro-issues');
18
- const fs = require('fs/promises');
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 getLatestReportExtracts();
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 (error) {
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 act in it (none if the report retrieval failed):
46
- acts.forEach(act => {
47
- // If it is a test act:
48
- if (act.type === 'test') {
49
- const {result, which} = act;
50
- const instances = result?.standardResult?.instances ?? [];
51
- // For each of its standard instances:
52
- instances.forEach(instance => {
53
- const {count, issueID} = instance;
54
- // If the instance has a non-ignorable issue ID:
55
- if (issueID && issueID !== 'ignorable') {
56
- issuesData[issueID] ??= {
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
- exports.answer = async () => {
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
- let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
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.js
3
- UI tests for web/listTopIssues/index.js using the fixture corpus.
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
- const {test, before, after} = require('node:test');
9
- const assert = require('node:assert/strict');
10
- const path = require('node:path');
11
- const fs = require('node:fs/promises');
12
- const {parse} = require('node-html-parser');
13
- const {answer} = require('./index');
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 = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
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 = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
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 invalid for isValidReport (no acts or catalog).
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 = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
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.js
2
+ index.ts
3
3
  Lists the violators of an issue in a report.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {
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
- isHidden,
18
+ isReportError,
18
19
  makeBreakable,
19
- } = require('../../util');
20
- const {issues: issueSpecs} = require('testaro-issues');
21
- const fs = require('fs/promises');
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 (issueID, timeStamp, jobID, query) => {
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, what} = pageDataStrings;
51
+ const {testInfo, url, urlLink, description} = pageDataStrings;
47
52
  // Add page facts to the query.
48
- query.target = what;
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.error) {
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
- // Otherwise, i.e. if it succeeded, get the test acts of the report.
71
- const testActs = acts.filter(act => act.type === 'test');
72
- // For each test act:
73
- testActs.forEach(act => {
74
- const {result, which} = act;
75
- const issueInstances = result?.standardResult?.instances?.filter(
76
- instance => instance.issueID === issueID
77
- ) ?? [];
78
- // If the rule of any of its standard instances belongs to the issue:
79
- if (issueInstances.length) {
80
- query.reporters.add(which);
81
- }
82
- // For each standard instance whose rule belongs to the issue:
83
- issueInstances.forEach(instance => {
84
- const pathID = instance.pathID || '/html';
85
- const catalogIndex = instance.catalogIndex || '0';
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.textLinkable) {
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
- exports.answer = async pageArgs => {
173
- const [issueID, timeStamp, jobID] = pageArgs.split('/');
174
- const reportIsHidden = await isHidden(timeStamp, jobID);
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
- let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
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',