@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,66 @@
1
1
  /*
2
- index.js
2
+ index.ts
3
3
  Lists the diagnoses of a violator of an issue in a report.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {
8
+ import {
9
9
  getPageDataStrings,
10
10
  getReport,
11
+ getTestActInstances,
11
12
  getTextFragmentHref,
12
13
  getWCAGLink,
13
14
  getWeightName,
14
15
  htmlSafe,
15
- isHidden,
16
+ isReportError,
17
+ populateTemplate,
16
18
  ruleEngines
17
- } = require('../../util');
18
- const {issues: issueSpecs} = require('testaro-issues');
19
- const fs = require('fs/promises');
20
- const path = require('path');
19
+ } from '../../util.ts';
20
+ import {issues as issueSpecs} from 'testaro-issues';
21
21
 
22
22
  // FUNCTIONS
23
23
 
24
24
  // Adds parameters to a query for the answer page.
25
- const populateQuery = async (issueID, timeStamp, jobID, catalogIndex, pathID, query) => {
25
+ const populateQuery = async (
26
+ issueID: string,
27
+ timeStamp: string,
28
+ jobID: string,
29
+ catalogIndex: string,
30
+ pathID: string | null,
31
+ query: Record<string, any>
32
+ ) => {
26
33
  // Get descriptions of the page facts.
27
34
  const pageDataStrings = await getPageDataStrings(timeStamp, jobID);
28
35
  // If this failed:
29
- if (pageDataStrings.error) {
36
+ if (pageDataStrings.error !== undefined) {
30
37
  // Populate the query with the reason.
31
38
  query.error = pageDataStrings.error;
32
39
  // Stop populating the query.
33
40
  return;
34
41
  }
35
- const {testInfo, url, urlLink, what} = pageDataStrings;
42
+ const {testInfo, url, urlLink, description} = pageDataStrings;
36
43
  // Otherwise, i.e. if it succeeded, get the report.
37
44
  const report = await getReport(timeStamp, jobID);
38
- const {acts, catalog} = report;
39
45
  // If this failed:
40
- if (report.error) {
46
+ if (isReportError(report)) {
41
47
  // Populate the query with the reason.
42
48
  query.error = report.error;
43
49
  // Stop populating the query.
44
50
  return;
45
51
  }
52
+ const {catalog} = report;
46
53
  // Otherwise, i.e. if it succeeded, get the catalog item of the specified violator.
47
- const catalogItem = catalog[catalogIndex] ?? {};
48
- const {boxID, startTag, tagName, text} = catalogItem;
54
+ const catalogItem = catalog[catalogIndex];
55
+ const boxID = catalogItem?.boxID;
56
+ const startTag = catalogItem?.startTag;
57
+ const tagName = catalogItem?.tagName;
58
+ const text = catalogItem?.text;
49
59
  query.catalogIndex = catalogIndex;
50
- const lines = [];
60
+ const lines: string[] = [];
51
61
  const margin = ' '.repeat(6);
52
- if (catalogIndex && catalogItem.textLinkable) {
53
- const href = getTextFragmentHref(text, url);
62
+ if (catalogIndex && catalogItem?.textLinkable) {
63
+ const href = getTextFragmentHref(text as string, url);
54
64
  const label = `Take me to element ${catalogIndex} on the page (in a new tab)`;
55
65
  const link = `<a href="${href}" target="_blank" aria-label="${label}">Take me there</a>`;
56
66
  query.takeMeThere = `${margin} <p>${link}</p>`;
@@ -59,7 +69,7 @@ const populateQuery = async (issueID, timeStamp, jobID, catalogIndex, pathID, qu
59
69
  query.takeMeThere = '';
60
70
  }
61
71
  // Add facts about the issue to the query.
62
- query.target = what;
72
+ query.target = description;
63
73
  query.urlLink = urlLink;
64
74
  query.testInfo = testInfo;
65
75
  query.issue = issueSpecs[issueID]?.summary;
@@ -71,20 +81,20 @@ const populateQuery = async (issueID, timeStamp, jobID, catalogIndex, pathID, qu
71
81
  return;
72
82
  }
73
83
  // Otherwise, i.e. if it succeeded, get the issue details.
74
- const issue = issueSpecs[issueID];
84
+ const issue = issueSpecs[issueID]!;
75
85
  const {wcag, weight, why} = issue;
76
86
  query.why = why;
77
87
  query.priority = getWeightName(weight);
78
88
  query.wcag = `<a href="${getWCAGLink(wcag)}">${wcag}</a>`;
79
89
  query.tagName = tagName || 'HTML';
80
- if (text && !['HTML', 'BODY', 'HEAD', 'SCRIPT', 'STYLE', 'NOSCRIPT'].includes(tagName)) {
90
+ if (text && !['HTML', 'BODY', 'HEAD', 'SCRIPT', 'STYLE', 'NOSCRIPT'].includes(tagName as string)) {
81
91
  const textString = text.split('\n').join(' … ');
82
92
  query.text = `<q>${htmlSafe(textString)}</q>`;
83
93
  }
84
94
  else {
85
95
  query.text = '[not applicable]';
86
96
  }
87
- query.startTag = htmlSafe(startTag) || '[not obtained]';
97
+ query.startTag = htmlSafe(startTag ?? '') || '[not obtained]';
88
98
  query.pathID = pathID || '[not obtained]';
89
99
  if (boxID) {
90
100
  const dims = boxID.split(':');
@@ -94,23 +104,15 @@ const populateQuery = async (issueID, timeStamp, jobID, catalogIndex, pathID, qu
94
104
  query.box = '[not obtained]';
95
105
  }
96
106
  // Initialize an array of diagnoses.
97
- let diagnoses = [];
98
- const testActs = acts.filter(act => act.type === 'test');
99
- // For each test act:
100
- testActs.forEach(act => {
101
- const {result, which} = act;
102
- const caseInstances = result?.standardResult?.instances?.filter(
103
- instance => instance.issueID === issueID && instance.catalogIndex === catalogIndex
104
- ) ?? [];
105
- // For each standard instance that pertains to this combination of issue and violator:
106
- caseInstances.forEach(instance => {
107
- const {ruleID, what} = instance;
108
- // Add lines for it to the array.
109
- diagnoses.push({
110
- engineID: which,
111
- ruleID,
112
- what
113
- });
107
+ const diagnoses: any[] = [];
108
+ // For each violating standard instance that pertains to this combination of issue and violator:
109
+ getTestActInstances(report, {violationsOnly: true, issueID, catalogIndex}).forEach(({act, instance}) => {
110
+ const {ruleID, what} = instance;
111
+ // Add lines for it to the array.
112
+ diagnoses.push({
113
+ engineID: act.which,
114
+ ruleID,
115
+ what
114
116
  });
115
117
  });
116
118
  // For each diagnosis:
@@ -118,9 +120,9 @@ const populateQuery = async (issueID, timeStamp, jobID, catalogIndex, pathID, qu
118
120
  const {engineID, ruleID, what} = diagnosis;
119
121
  // Add lines.
120
122
  lines.push(`${margin}<li>${htmlSafe(what)}`);
121
- lines.push(
122
- `${margin} <p>Rule engine: ${ruleEngines[engineID][0]} (${ruleEngines[engineID][1]})</p>`
123
- );
123
+ // engineID is act.which, guaranteed by isUsableReport to be a key in ruleEngines.
124
+ const [engineName, engineOrg] = ruleEngines[engineID]!;
125
+ lines.push(`${margin} <p>Rule engine: ${engineName} (${engineOrg})</p>`);
124
126
  if (ruleID !== what) {
125
127
  lines.push(`${margin} <p>Rule: <code>${ruleID}</code></p>`);
126
128
  }
@@ -130,19 +132,11 @@ const populateQuery = async (issueID, timeStamp, jobID, catalogIndex, pathID, qu
130
132
  query.diagnoses = lines.join('\n');
131
133
  };
132
134
  // Returns a page answering the diagnoses question.
133
- exports.answer = async (pageArgs, search) => {
134
- const [issueID, timeStamp, jobID, catalogIndex] = pageArgs.split('/');
135
- const reportIsHidden = await isHidden(timeStamp, jobID);
136
- // If the report is not available:
137
- if (reportIsHidden) {
138
- return {
139
- status: 'error',
140
- message: 'Report not available'
141
- };
142
- }
135
+ export const answer = async (pageArgs: string, search: string) => {
136
+ const [issueID, timeStamp, jobID, catalogIndex] = pageArgs.split('/') as [string, string, string, string];
143
137
  const params = new URLSearchParams(search);
144
138
  const pathID = params.get('pathID');
145
- const query = {};
139
+ const query: Record<string, any> = {};
146
140
  // Create a query to replace the placeholders.
147
141
  await populateQuery(issueID, timeStamp, jobID, catalogIndex, pathID, query);
148
142
  // If this failed:
@@ -155,12 +149,8 @@ exports.answer = async (pageArgs, search) => {
155
149
  }
156
150
  // Otherwise, if it succeeded and the report facts were obtained:
157
151
  if (query.testInfo) {
158
- // Get the template.
159
- let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
160
- // Replace its placeholders.
161
- Object.keys(query).forEach(param => {
162
- answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
163
- });
152
+ // Get the populated template.
153
+ const answerPage = await populateTemplate(import.meta.dirname, query);
164
154
  // Return the populated page.
165
155
  return {
166
156
  status: 'ok',
@@ -1,46 +1,49 @@
1
1
  /*
2
- listDiagnoses.test.js
3
- UI tests for web/listDiagnoses/index.js using the fixture corpus.
2
+ listDiagnoses.test.ts
3
+ UI tests for web/listDiagnoses/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
-
14
- // Monkey-patch util functions before requiring index, so that index.js
15
- // destructures the patched versions.
16
- const util = require('../../util');
17
- const realGetReport = util.getReport;
18
- const realGetPageDataStrings = util.getPageDataStrings;
8
+ import {test, before, after, mock} 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
+
14
+ // Mock the util functions called by index.ts before requiring it, so that
15
+ // it imports the mocked versions. Other exports delegate to the real module.
16
+ import * as realUtil from '../../util.ts';
19
17
  let getReportCallCount = 0;
20
18
  let failGetReportOnCall = -1;
21
- let pageDataStringsOverride = null;
22
- util.getReport = async (...args) => {
23
- getReportCallCount++;
24
- if (getReportCallCount === failGetReportOnCall) {
25
- return {error: 'Report is invalid (test override)'};
26
- }
27
- return realGetReport(...args);
28
- };
29
- util.getPageDataStrings = async (...args) => {
30
- if (pageDataStringsOverride !== null) {
31
- return pageDataStringsOverride;
19
+ let pageDataStringsOverride: any = null;
20
+ mock.module('../../util.ts', {
21
+ exports: {
22
+ ...realUtil,
23
+ getReport: async (...args: any[]) => {
24
+ getReportCallCount++;
25
+ if (getReportCallCount === failGetReportOnCall) {
26
+ return {error: 'Report is invalid (test override)'};
27
+ }
28
+ return (realUtil.getReport as any)(...args);
29
+ },
30
+ getPageDataStrings: async (...args: any[]) => {
31
+ if (pageDataStringsOverride !== null) {
32
+ return pageDataStringsOverride;
33
+ }
34
+ return (realUtil.getPageDataStrings as any)(...args);
35
+ }
32
36
  }
33
- return realGetPageDataStrings(...args);
34
- };
37
+ } as any);
35
38
 
36
- const {answer} = require('./index');
39
+ const {answer} = await import('./index.ts') as any;
37
40
 
38
41
  // SETUP AND TEARDOWN
39
42
 
40
43
  const savedDBDir = process.env.DB_DIR;
41
44
 
42
- before(() => {
43
- process.env.DB_DIR = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
45
+ before(async () => {
46
+ process.env.DB_DIR = (await import('../../test/dbFixture.ts')).fixtureDBDir;
44
47
  });
45
48
 
46
49
  after(() => {
@@ -77,11 +80,13 @@ test('listDiagnoses includes diagnosis descriptions from the rule engines', asyn
77
80
  assert.ok(result.answerPage.includes('The link does not have an accessible name'));
78
81
  });
79
82
 
80
- test('listDiagnoses returns an error status for a hidden report', async () => {
81
- const result = await answer('linkNoText/260101T0007/hid/0');
82
- assert.equal(result.status, 'error');
83
+ test('listDiagnoses excludes cantTell instances from the diagnoses list', async () => {
84
+ const result = await answer('focusIndicationBad/260101T0000/mix/0');
85
+ assert.equal(result.status, 'ok');
86
+ assert.ok(!result.answerPage.includes('Focus Visible'));
83
87
  });
84
88
 
89
+
85
90
  test('listDiagnoses returns an error status for a nonexistent report', async () => {
86
91
  const result = await answer('linkNoText/999999T9999/xyz/0');
87
92
  assert.equal(result.status, 'error');
@@ -111,7 +116,7 @@ test('listDiagnoses returns an error when getReport fails after getPageDataStrin
111
116
  });
112
117
 
113
118
  test('listDiagnoses includes a take-me-there link for a text-linkable catalog item', async () => {
114
- const dbDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
119
+ const dbDir = (await import('../../test/dbFixture.ts')).fixtureDBDir;
115
120
  const reportsDir = path.join(dbDir, 'reports');
116
121
  const reportPath = path.join(reportsDir, '260101T0003-tlk.json');
117
122
  try {
@@ -193,7 +198,7 @@ test('listDiagnoses returns an error when report facts are not obtained', async
193
198
 
194
199
  test('listDiagnoses handles acts with no standardResult instances gracefully', async () => {
195
200
  // Create a temporary report where an act has no standardResult.
196
- const dbDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
201
+ const dbDir = (await import('../../test/dbFixture.ts')).fixtureDBDir;
197
202
  const reportPath = path.join(dbDir, 'reports', '260101T0004-nst.json');
198
203
  const report = {
199
204
  id: '260101T0004-nst',
@@ -1,48 +1,42 @@
1
1
  /*
2
- index.js
2
+ index.ts
3
3
  List the issues in a report.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {
8
+ import {
9
9
  getPageData,
10
10
  getPageDataStrings,
11
11
  getReport,
12
- getEngineList,
12
+ getEngineNamesString,
13
+ getTestActInstances,
13
14
  getWCAGLink,
14
15
  getWeightName,
15
16
  htmlSafe,
16
- isHidden,
17
- isValidReport,
17
+ isUsableReport,
18
18
  objectSort,
19
+ populateTemplate,
19
20
  ruleEngines
20
- } = require('../../util');
21
- const {issues: issueSpecs} = require('testaro-issues');
22
- const fs = require('fs/promises');
23
- const path = require('path');
21
+ } from '../../util.ts';
22
+ import {issues as issueSpecs} from 'testaro-issues';
24
23
 
25
24
  // FUNCTIONS
26
25
 
27
26
  // Returns data on the issues reported by a report.
28
- const getIssuesData = async (timeStamp, jobID) => {
27
+ const getIssuesData = async (timeStamp: string, jobID: string) => {
29
28
  // Get the report.
30
29
  const report = await getReport(timeStamp, jobID);
31
- const reportIsHidden = await isHidden(timeStamp, jobID);
32
- // If it exists and is hidden:
33
- if (reportIsHidden) {
34
- return {error: 'Report is not available'}
35
- }
36
- // Otherwise, if it exists and is valid:
37
- if (isValidReport(report)) {
30
+ // If it exists and is valid:
31
+ if (isUsableReport(report)) {
38
32
  // Initialize the temporary data.
39
33
  const temp = {
40
- issues: {},
41
- reporters: new Set(),
42
- violators: new Set()
34
+ issues: {} as Record<string, any>,
35
+ reporters: new Set<string>(),
36
+ violators: new Set<string>()
43
37
  };
44
38
  // Initialize the final data.
45
- const final = {
39
+ const final: Record<string, any> = {
46
40
  reporters: [],
47
41
  reporterList: '',
48
42
  reporterCount: 0,
@@ -56,60 +50,53 @@ const getIssuesData = async (timeStamp, jobID) => {
56
50
  },
57
51
  issueCount: 0
58
52
  };
59
- // For each act in the report:
60
- report.acts.forEach(act => {
61
- // If it is a test act:
62
- if (act.type === 'test') {
63
- const {result, which} = act;
64
- const instances = result?.standardResult?.instances ?? [];
65
- // For each of its standard instances:
66
- instances.forEach(instance => {
67
- const {catalogIndex, issueID, outcome} = instance;
68
- // If the instance reports a violation and identifies a non-ignorable issue:
69
- if (outcome !== 'cantTell' && issueID && issueID !== 'ignorable') {
70
- const issueClassification = issueSpecs[issueID];
71
- // If the issue has a current weighted classification:
72
- if (issueClassification && [1, 2, 3, 4].includes(issueClassification.weight)) {
73
- const {summary, wcag, weight, why} = issueClassification;
74
- // Initialize the temporary data on the issue if necessary.
75
- temp.issues[issueID] ??= {
76
- issueID,
77
- summary,
78
- wcag,
79
- why,
80
- weight,
81
- reporters: new Set(),
82
- reporterList: '',
83
- violators: new Set()
84
- };
85
- // Ensure the rule engine is in the temporary data.
86
- temp.issues[issueID].reporters.add(which);
87
- temp.reporters.add(which);
88
- // If the instance has a catalog index:
89
- if (catalogIndex) {
90
- // Ensure the violator is in the temporary data.
91
- temp.issues[issueID].violators.add(catalogIndex);
92
- temp.violators.add(catalogIndex);
93
- }
94
- }
53
+ // For each violating standard instance of each test act:
54
+ getTestActInstances(report, {violationsOnly: true}).forEach(({act, instance}) => {
55
+ const {catalogIndex, issueID} = instance;
56
+ const which = act.which!;
57
+ // If it identifies a non-ignorable issue:
58
+ if (issueID && issueID !== 'ignorable') {
59
+ const issueClassification = issueSpecs[issueID];
60
+ // If the issue has a current weighted classification:
61
+ if (issueClassification && [1, 2, 3, 4].includes(issueClassification.weight)) {
62
+ const {summary, wcag, weight, why} = issueClassification;
63
+ // Initialize the temporary data on the issue if necessary.
64
+ temp.issues[issueID] ??= {
65
+ issueID,
66
+ summary,
67
+ wcag,
68
+ why,
69
+ weight,
70
+ reporters: new Set(),
71
+ reporterList: '',
72
+ violators: new Set()
73
+ };
74
+ // Ensure the rule engine is in the temporary data.
75
+ temp.issues[issueID].reporters.add(which);
76
+ temp.reporters.add(which);
77
+ // If the instance has a catalog index:
78
+ if (catalogIndex) {
79
+ // Ensure the violator is in the temporary data.
80
+ temp.issues[issueID].violators.add(catalogIndex);
81
+ temp.violators.add(String(catalogIndex));
95
82
  }
96
- });
83
+ }
97
84
  }
98
85
  });
99
86
  // Finish populating the final data.
100
- final.reporterList = getEngineList(temp.reporters);
87
+ final.reporterList = getEngineNamesString(temp.reporters);
101
88
  final.reporterCount = temp.reporters.size;
102
89
  final.violatorCount = temp.violators.size;
103
90
  Object.values(temp.issues).forEach(issue => {
104
91
  const {issueID, summary, wcag, why, weight} = issue;
105
- const finalIssue = {
92
+ const finalIssue: Record<string, any> = {
106
93
  issueID,
107
94
  summary,
108
95
  wcag,
109
96
  why,
110
97
  weight
111
98
  };
112
- finalIssue.reporterList = getEngineList(issue.reporters);
99
+ finalIssue.reporterList = getEngineNamesString(issue.reporters);
113
100
  finalIssue.reporterCount = issue.reporters.size;
114
101
  finalIssue.violatorCount = issue.violators.size;
115
102
  final.issues[issue.weight].push(finalIssue);
@@ -129,10 +116,10 @@ const getIssuesData = async (timeStamp, jobID) => {
129
116
  return {error: 'Report missing or invalid.'};
130
117
  };
131
118
  // Get page and issues data from a report.
132
- const getData = async (timeStamp, jobID) => {
119
+ const getData = async (timeStamp: string, jobID: string) => {
133
120
  const pageData = await getPageData(timeStamp, jobID);
134
- const issuesData = await getIssuesData(timeStamp, jobID);
135
- const pageError = pageData.error || '';
121
+ const issuesData: any = await getIssuesData(timeStamp, jobID);
122
+ const pageError = ('error' in pageData ? pageData.error : '');
136
123
  const issuesError = issuesData.error || '';
137
124
  const errors = [pageError, issuesError].filter(Boolean).join('; ');
138
125
  // If the data of either type are missing or invalid:
@@ -147,9 +134,9 @@ const getData = async (timeStamp, jobID) => {
147
134
  };
148
135
  };
149
136
  // Adds parameters to a query for the answer page.
150
- const populateQuery = async (timeStamp, jobID, query) => {
137
+ const populateQuery = async (timeStamp: string, jobID: string, query: Record<string, any>) => {
151
138
  // Get data on the target and its issues according to the report.
152
- const data = await getData(timeStamp, jobID);
139
+ const data: any = await getData(timeStamp, jobID);
153
140
  const {pageData, issuesData} = data;
154
141
  // If this failed:
155
142
  if (data.error) {
@@ -160,16 +147,16 @@ const populateQuery = async (timeStamp, jobID, query) => {
160
147
  }
161
148
  // Otherwise, i.e. if it succeeded, get fact descriptions for the target.
162
149
  const pageInfo = await getPageDataStrings(timeStamp, jobID, pageData);
163
- const {testInfo, urlLink, what} = pageInfo;
164
150
  // If this failed:
165
- if (pageInfo.error) {
151
+ if (pageInfo.error !== undefined) {
166
152
  // Populate the query with the reason.
167
153
  query.error = pageInfo.error;
168
154
  // Stop populating the query.
169
155
  return;
170
156
  }
157
+ const {testInfo, urlLink, description} = pageInfo;
171
158
  // Otherwise, i.e. if it succeeded, add target data to the query.
172
- query.target = what;
159
+ query.target = description;
173
160
  query.urlLink = urlLink;
174
161
  query.testInfo = testInfo;
175
162
  const {
@@ -181,11 +168,11 @@ const populateQuery = async (timeStamp, jobID, query) => {
181
168
  issues
182
169
  } = issuesData;
183
170
  // Initialize strings for the prevention notices query property.
184
- const preventionStrings = [];
171
+ const preventionStrings: string[] = [];
185
172
  const margin = ' '.repeat(6);
186
173
  Object.keys(preventions).forEach(preventedEngineID => {
187
- const engineName = ruleEngines[preventedEngineID];
188
- const engineNameString = `${engineName[0]} (${engineName[1]})`;
174
+ const [engineName, engineSponsor] = ruleEngines[preventedEngineID] ?? [preventedEngineID, 'unknown sponsor'];
175
+ const engineNameString = `${engineName} (${engineSponsor})`;
189
176
  const causeString = htmlSafe(preventions[preventedEngineID]);
190
177
  const preventionString = `${margin}<li>Page not testable by ${engineNameString}: ${causeString}</li>`;
191
178
  preventionStrings.push(preventionString);
@@ -213,9 +200,9 @@ const populateQuery = async (timeStamp, jobID, query) => {
213
200
  // If any reported issues have the weight:
214
201
  if (weightIssues.length) {
215
202
  // Initialize lines for the weight details query property.
216
- const detailsLines = [];
203
+ const detailsLines: string[] = [];
217
204
  // For each issue with the weight:
218
- weightIssues.forEach(issueData => {
205
+ weightIssues.forEach((issueData: any) => {
219
206
  const weightIssueCount = weightIssues.length;
220
207
  // Add the issue count to the query.
221
208
  query[`${weightName}Count`] = weightIssueCount;
@@ -251,7 +238,7 @@ const populateQuery = async (timeStamp, jobID, query) => {
251
238
  // Add the start of a link list to the lines.
252
239
  detailsLines.push(`${margin} <ul class="nav">`);
253
240
  const whereQuestionString = 'Where was the issue found?';
254
- const labelString = `Where was the ${summary} issue found on the ${what} page?`;
241
+ const labelString = `Where was the ${summary} issue found on the ${description} page?`;
255
242
  const href = `href="/listViolators.html/${issueID}/${timeStamp}/${jobID}"`;
256
243
  const label = `aria-label="${labelString}"`;
257
244
  const whereLink = `<a ${href} ${label}>${whereQuestionString}</a>`;
@@ -272,17 +259,9 @@ const populateQuery = async (timeStamp, jobID, query) => {
272
259
  });
273
260
  };
274
261
  // Returns a page answering the target-issues question.
275
- exports.answer = async pageArgs => {
276
- const [timeStamp, jobID] = pageArgs.split('/');
277
- const reportIsHidden = await isHidden(timeStamp, jobID);
278
- // If the report is not available:
279
- if (reportIsHidden) {
280
- return {
281
- status: 'error',
282
- message: 'Report not available'
283
- };
284
- }
285
- const query = {};
262
+ export const answer = async (pageArgs: string) => {
263
+ const [timeStamp, jobID] = pageArgs.split('/') as [string, string];
264
+ const query: Record<string, any> = {};
286
265
  // Create a query to replace the placeholders.
287
266
  await populateQuery(timeStamp, jobID, query);
288
267
  // If this failed:
@@ -295,12 +274,8 @@ exports.answer = async pageArgs => {
295
274
  }
296
275
  // Otherwise, if it succeeded and the report facts were obtained:
297
276
  if (query.testInfo) {
298
- // Get the template.
299
- let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
300
- // Replace its placeholders.
301
- Object.keys(query).forEach(param => {
302
- answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
303
- });
277
+ // Get the populated template.
278
+ const answerPage = await populateTemplate(import.meta.dirname, query);
304
279
  // Return the populated page.
305
280
  return {
306
281
  status: 'ok',