@jrpool/kilotest 60.1.11 → 60.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lhm.plugin.json CHANGED
@@ -4423,5 +4423,5 @@
4423
4423
  }
4424
4424
  }
4425
4425
  ],
4426
- "version": "60.1.11"
4426
+ "version": "60.2.0"
4427
4427
  }
package/openapi-3.0.yaml CHANGED
@@ -2,7 +2,7 @@ openapi: 3.0.3
2
2
  info:
3
3
  title: Kilotest API
4
4
  description: 'Kilotest is an ensemble testing and reporting service for front-end web quality. It uses an ensemble of 12 rule engines to test web pages for front-end quality (accessibility, usability, and web standards, including HTML, CSS, ARIA, and WCAG). Kilotest acts as a web server for human users, an MCP server for AI platforms, and an API for programmatic access. With this API you can request that a page be tested or retested, discover available reports, incrementally retrieve facts from a report: from what reports are available, to what issues were reported in one report, to what elements violated one issue, to what diagnoses rule engines gave for one violation.'
5
- version: 60.1.11
5
+ version: 60.2.0
6
6
  license:
7
7
  name: MIT
8
8
  url: https://github.com/jrpool/kilotest/blob/main/LICENSE
package/openapi.yaml CHANGED
@@ -2,7 +2,7 @@ openapi: 3.1.0
2
2
  info:
3
3
  title: Kilotest API
4
4
  description: 'Kilotest is an ensemble testing and reporting service for front-end web quality. It uses an ensemble of 12 rule engines to test web pages for front-end quality (accessibility, usability, and web standards, including HTML, CSS, ARIA, and WCAG). Kilotest acts as a web server for human users, an MCP server for AI platforms, and an API for programmatic access. With this API you can request that a page be tested or retested, discover available reports, incrementally retrieve facts from a report: from what reports are available, to what issues were reported in one report, to what elements violated one issue, to what diagnoses rule engines gave for one violation.'
5
- version: 60.1.11
5
+ version: 60.2.0
6
6
  license:
7
7
  name: MIT
8
8
  identifier: MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jrpool/kilotest",
3
- "version": "60.1.11",
3
+ "version": "60.2.0",
4
4
  "description": "Ensemble testing for front-end web quality",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/server.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "https://github.com/jrpool/kilotest",
7
7
  "source": "github"
8
8
  },
9
- "version": "60.1.11",
9
+ "version": "60.2.0",
10
10
  "remotes": [
11
11
  {
12
12
  "type": "streamable-http",
@@ -9,10 +9,9 @@
9
9
  __header__
10
10
  <main class="page">
11
11
  <h1>Diagnoses of the <q>__issue__</q> violation by element __catalogIndex__ on the __target__ page</h1>
12
- <h2>Basics</h2>
13
- <h3>About the page</h3>
12
+ <h2>About the page</h2>
14
13
  __pageFacts__
15
- <h3>About the issue</h3>
14
+ <h2>About the issue</h2>
16
15
  __issueFacts__
17
16
  <h3>About the violator</h3>
18
17
  __violatorFacts__
@@ -30,7 +30,6 @@ const populateQuery = async (
30
30
  issueID: string,
31
31
  reportID: string,
32
32
  catalogIndex: string,
33
- pathID: string | null,
34
33
  query: Record<string, any>
35
34
  ) => {
36
35
  // Get the report.
@@ -81,7 +80,7 @@ const populateQuery = async (
81
80
  }
82
81
  // Otherwise, i.e. if it succeeded, add the issue-facts and violator-facts lists to the query.
83
82
  query.issueFacts = getIssueFactsLines(report, issueID, margin).join('\n');
84
- query.violatorFacts = getViolatorFactsLines(report, issueID, catalogIndex, pathID, margin).join('\n');
83
+ query.violatorFacts = getViolatorFactsLines(report, issueID, catalogIndex, margin).join('\n');
85
84
  // Initialize an array of diagnoses.
86
85
  const diagnoses: any[] = [];
87
86
  // For each violating standard instance that pertains to this combination of issue and violator:
@@ -111,13 +110,11 @@ const populateQuery = async (
111
110
  query.diagnoses = lines.join('\n');
112
111
  };
113
112
  // Returns a page answering the diagnoses question.
114
- export const answer = async (pageArgs: string, search: string) => {
113
+ export const answer = async (pageArgs: string) => {
115
114
  const [issueID, reportID, catalogIndex] = pageArgs.split('/') as [string, string, string];
116
- const params = new URLSearchParams(search);
117
- const pathID = params.get('pathID');
118
115
  const query: Record<string, any> = {};
119
116
  // Create a query to replace the placeholders.
120
- await populateQuery(issueID, reportID, catalogIndex, pathID, query);
117
+ await populateQuery(issueID, reportID, catalogIndex, query);
121
118
  // If this failed:
122
119
  if (query.error) {
123
120
  // Return why.
@@ -10,7 +10,7 @@
10
10
  <main class="page">
11
11
  <h1>Issues reported for __target__</h1>
12
12
  __oldWarn__
13
- <h2>About the tests</h2>
13
+ <h2>About the page</h2>
14
14
  __pageFacts__
15
15
  <h2>History</h2>
16
16
  __history__
@@ -108,17 +108,13 @@ test('listIssues for the empty report shows no issue links', async () => {
108
108
  test('listIssues counts the prevented rule engine as called but not able to test', async () => {
109
109
  const result = await answer('260101T0006-prv');
110
110
  assert.equal(result.status, 'ok');
111
- assert.ok(result.answerPage.includes('<li>Called: 2</li>'));
112
- assert.ok(result.answerPage.includes('<li>Were able to test: 1</li>'));
113
- assert.ok(result.answerPage.includes('<li>Reported any rule violations: 1</li>'));
111
+ assert.ok(result.answerPage.includes('<li>Rule engines: 2 called, 1 able to test, 1 reported any violations</li>'));
114
112
  });
115
113
 
116
114
  test('listIssues counts a rule engine that reported no violations as called and able to test', async () => {
117
115
  const result = await answer('260101T0005-emp');
118
116
  assert.equal(result.status, 'ok');
119
- assert.ok(result.answerPage.includes('<li>Called: 1</li>'));
120
- assert.ok(result.answerPage.includes('<li>Were able to test: 1</li>'));
121
- assert.ok(result.answerPage.includes('<li>Reported any rule violations: 0</li>'));
117
+ assert.ok(result.answerPage.includes('<li>Rule engines: 1 called, 1 able to test, 0 reported any violations</li>'));
122
118
  });
123
119
 
124
120
  test('listIssues counts an unknown prevented engine as called but not able to test', async () => {
@@ -131,8 +127,7 @@ test('listIssues counts an unknown prevented engine as called but not able to te
131
127
  try {
132
128
  const result = await answer('260103T0000-unk');
133
129
  assert.equal(result.status, 'ok');
134
- assert.ok(result.answerPage.includes('<li>Called: 3</li>'));
135
- assert.ok(result.answerPage.includes('<li>Were able to test: 1</li>'));
130
+ assert.ok(result.answerPage.includes('<li>Rule engines: 3 called, 1 able to test, 1 reported any violations</li>'));
136
131
  }
137
132
  finally {
138
133
  await fs.unlink(reportPath);
@@ -171,8 +166,7 @@ test('listIssues shows violation and violator counts for an issue with multiple
171
166
  // The mul report has linkNoText with 4 violations by 3 violators.
172
167
  const result = await answer('260101T0008-mul');
173
168
  assert.equal(result.status, 'ok');
174
- assert.ok(result.answerPage.includes('<li>Violations: 4</li>'));
175
- assert.ok(result.answerPage.includes('<li>Violators: 3</li>'));
169
+ assert.ok(result.answerPage.includes('<li>Results: 4 violations, 3 violators</li>'));
176
170
  });
177
171
 
178
172
  test('listIssues handles acts with no standardResult instances', async () => {
@@ -9,10 +9,9 @@
9
9
  __header__
10
10
  <main class="page">
11
11
  <h1>Violators of the <q>__issue__</q> issue for the __target__ page</h1>
12
- <h2>Basics</h2>
13
- <h3>About the page</h3>
12
+ <h2>About the page</h2>
14
13
  __pageFacts__
15
- <h3>About the issue</h3>
14
+ <h2>About the issue</h2>
16
15
  __issueFacts__
17
16
  <h2>Violators</h2>
18
17
  <p>Here are the HTML elements reported to violate rules belonging to the <q>__issue__</q> issue.</p>
@@ -92,11 +92,11 @@ const populateQuery = async (
92
92
  let takeMeAdviceNeeded = false;
93
93
  // For each violator:
94
94
  violators.forEach((violator: any, index: number) => {
95
- const {catalogIndex, pathID} = violator;
95
+ const {catalogIndex} = violator;
96
96
  // Add a heading to the lines.
97
97
  lines.push(`${margin}<li><h3>Element ${catalogIndex}</h3>`);
98
98
  // Add the violator facts to the lines.
99
- lines.push(...getViolatorFactsLines(report, issueID, catalogIndex, pathID, `${margin} `, 'pseudoTopLevel'));
99
+ lines.push(...getViolatorFactsLines(report, issueID, catalogIndex, `${margin} `, 'pseudoTopLevel'));
100
100
  if (catalogIndex) {
101
101
  const catalogItem = catalog[catalogIndex];
102
102
  if (catalogItem?.textLinkable) {
@@ -107,8 +107,7 @@ const populateQuery = async (
107
107
  lines.push(`${margin} <p class="links">${takeMeLink}</p>`);
108
108
  }
109
109
  }
110
- const href
111
- = `/listDiagnoses.html/${issueID}/${reportID}/${catalogIndex}?pathID=${encodeURIComponent(pathID)}`;
110
+ const href = `/listDiagnoses.html/${issueID}/${reportID}/${catalogIndex}`;
112
111
  const questionString = 'What diagnoses were reported';
113
112
  const labelString = `${questionString} for violator ${index + 1}?`;
114
113
  lines.push(
@@ -86,7 +86,7 @@ test('listViolators includes links to listDiagnoses for violators', async () =>
86
86
  test('listViolators excludes cantTell instances from the violator count', async () => {
87
87
  const result = await answer('focusIndicationBad/260101T0000-mix');
88
88
  assert.equal(result.status, 'ok');
89
- assert.ok(result.answerPage.includes('<li>Violators: 0</li>'));
89
+ assert.ok(result.answerPage.includes('<li>Results: 0 violations, 0 violators</li>'));
90
90
  assert.ok(!result.answerPage.includes('<h3>Element'));
91
91
  });
92
92
 
@@ -272,7 +272,7 @@ test('listViolators handles instances with missing catalogIndex and acts with no
272
272
  const result = await answer('linkNoText/260101T0004-nvi');
273
273
  assert.equal(result.status, 'ok');
274
274
  // Should show plural violator count.
275
- assert.ok(result.answerPage.includes('<li>Violators: 2</li>'));
275
+ assert.ok(result.answerPage.includes('<li>Results: 2 violations, 2 violators</li>'));
276
276
  }
277
277
  finally {
278
278
  await fs.unlink(reportPath).catch(() => {});
package/web/util.test.ts CHANGED
@@ -215,10 +215,8 @@ test('getPageFactsLines returns the about-page list lines for a report', async (
215
215
  const lines = getPageFactsLines(getPageDataStrings(report), getReportData(report), ' ');
216
216
  const html = lines.join('\n');
217
217
  assert.ok(html.includes('<li>URL: <a href="https://example.com/mixed">'));
218
- assert.ok(html.includes('<li>Summary of results:'));
219
- assert.ok(html.includes('<li>Called: 2</li>'));
220
- assert.ok(html.includes('<li>Violations: 3</li>'));
221
- assert.ok(html.includes('<li>Issues: 2</li>'));
218
+ assert.ok(html.includes('<li>Rule engines: 2 called, 2 able to test, 2 reported any violations</li>'));
219
+ assert.ok(html.includes('<li>Results: 3 violations, 2 violators, 2 issues</li>'));
222
220
  });
223
221
 
224
222
  test('getPageFactsLines ends with a link to download the full report', async () => {
@@ -227,7 +225,7 @@ test('getPageFactsLines ends with a link to download the full report', async ()
227
225
  const lines = getPageFactsLines(getPageDataStrings(report), getReportData(report), '');
228
226
  const topItems = parse(lines.join('\n')).querySelectorAll('ul:not(ul ul) > li');
229
227
  const lastItem = topItems[topItems.length - 1]!;
230
- assert.ok(topItems[topItems.length - 2]!.text.trim().startsWith('Summary of results:'));
228
+ assert.ok(topItems[topItems.length - 2]!.text.trim().startsWith('Results:'));
231
229
  assert.equal(lastItem.querySelector('a')?.getAttribute('href'), '/fullReport.json/260101T0000-mix');
232
230
  assert.equal(lastItem.text.trim(), 'Download the full technical report (JSON)');
233
231
  });
@@ -237,28 +235,28 @@ const getResultFacts = async (reportID: string) => {
237
235
  const {getReportData} = await import('./util.ts');
238
236
  const report = await getFixtureReport(reportID);
239
237
  const lines = getPageFactsLines(getPageDataStrings(report), getReportData(report), '');
240
- const items = parse(lines.join('\n')).querySelectorAll('li');
241
- const itemTexts: string[] = items.map(li => li.text.trim());
242
- const getAmount = (label: string) => {
243
- const text = itemTexts.find(t => t.startsWith(`${label}: `));
244
- assert.ok(text, `${label} item exists`);
245
- return Number(text.slice(label.length + 2));
246
- };
247
- // The summary item is the parent of the rule-engine item and the 3 amount items.
248
- const summaryItem = items.find(li => li.text.trim().startsWith('Summary of results:'));
249
- assert.ok(summaryItem, 'Summary of results item exists');
250
- const summaryList = summaryItem.querySelector('ul')!;
251
- const amountLabels = summaryList.childNodes
252
- .filter((node: any) => node.tagName === 'LI')
253
- .map(li => li.text.trim().split(':')[0]);
238
+ const itemTexts: string[] = parse(lines.join('\n'))
239
+ .querySelectorAll('li')
240
+ .map(li => li.text.trim());
241
+ const enginesItem = itemTexts.find(t => t.startsWith('Rule engines: '));
242
+ assert.ok(enginesItem, 'Rule engines item exists');
243
+ const engineAmounts = enginesItem.match(
244
+ /^Rule engines: (\d+) called, (\d+) able to test, (\d+) reported any violations$/
245
+ );
246
+ assert.ok(engineAmounts, 'Rule engines item has the expected format');
247
+ const resultsItem = itemTexts.find(t => t.startsWith('Results: '));
248
+ assert.ok(resultsItem, 'Results item exists');
249
+ const resultAmounts = resultsItem.match(
250
+ /^Results: (\d+) violations?, (\d+) violators?, (\d+) issues?$/
251
+ );
252
+ assert.ok(resultAmounts, 'Results item has the expected format');
254
253
  return {
255
- called: getAmount('Called'),
256
- tested: getAmount('Were able to test'),
257
- reporters: getAmount('Reported any rule violations'),
258
- amountLabels,
259
- violations: getAmount('Violations'),
260
- violators: getAmount('Violators'),
261
- issues: getAmount('Issues')
254
+ called: Number(engineAmounts[1]),
255
+ tested: Number(engineAmounts[2]),
256
+ reporters: Number(engineAmounts[3]),
257
+ violations: Number(resultAmounts[1]),
258
+ violators: Number(resultAmounts[2]),
259
+ issues: Number(resultAmounts[3])
262
260
  };
263
261
  };
264
262
 
@@ -281,7 +279,6 @@ for (const [reportID, called, tested, reporters, violations, violators, issues]
281
279
  assert.equal(facts.called, called);
282
280
  assert.equal(facts.tested, tested);
283
281
  assert.equal(facts.reporters, reporters);
284
- assert.deepEqual(facts.amountLabels, ['Rule engines', 'Violations', 'Violators', 'Issues']);
285
282
  assert.equal(facts.violations, violations);
286
283
  assert.equal(facts.violators, violators);
287
284
  assert.equal(facts.issues, issues);
@@ -297,14 +294,13 @@ test('getIssueFactsLines returns the about-issue list lines for a report', async
297
294
  assert.ok(html.includes('<li>Priority:'));
298
295
  assert.ok(html.includes('<li>Related WCAG standard: <a href='));
299
296
  assert.ok(html.includes('<li>Reported by'));
300
- assert.ok(html.includes('<li>Violations:'));
301
- assert.ok(html.includes('<li>Violators:'));
297
+ assert.ok(/<li>Results: \d+ violations?, \d+ violators?<\/li>/.test(html));
302
298
  });
303
299
 
304
300
  test('getViolatorFactsLines returns the about-violator list lines for a report', async () => {
305
301
  const {getViolatorFactsLines} = await import('./util.ts');
306
302
  const report = await getFixtureReport('260101T0000-mix');
307
- const lines = getViolatorFactsLines(report, 'linkNoText', '0', null, ' ');
303
+ const lines = getViolatorFactsLines(report, 'linkNoText', '0', ' ');
308
304
  const html = lines.join('\n');
309
305
  assert.ok(html.includes('<li>Tag name: <code>'));
310
306
  assert.ok(html.includes('<li>Text:'));
package/web/util.ts CHANGED
@@ -252,7 +252,7 @@ export const getReportData = (report: UsableReport): ReportData => {
252
252
  export const getPageData = (report: UsableReport): PageData => {
253
253
  const {what: description, url} = report.target;
254
254
  // Get the elapsed time in days since the report was completed.
255
- const daysAgo = getAgoDays(getReportEndTime(report));
255
+ const daysAgo = getAgoDays(getReportEndTime(report))!;
256
256
  // Return the data.
257
257
  return {
258
258
  description,
@@ -270,7 +270,7 @@ export const getPageDataStrings = (report: UsableReport): PageDataStrings => {
270
270
  description,
271
271
  url,
272
272
  urlLink: `<a href="${url}">${url}</a>`,
273
- testInfo: `Tested ${daysAgo === 1 ? '1 day' : `${daysAgo} days`} ago on ${when} by job <code>${report.id}</code>`,
273
+ testInfo: `Tested ${getCountString(daysAgo!, 'day', 'days')} ago on ${when} by job <code>${report.id}</code>`,
274
274
  fullReportLink: `<a href="/fullReport.json/${report.id}">Download the full technical report</a> (JSON)`
275
275
  };
276
276
  };
@@ -279,27 +279,20 @@ export const getPageFactsLines = (
279
279
  strings: PageDataStrings,
280
280
  summary: ResultsSummary,
281
281
  margin: string
282
- ): string[] => [
283
- `${margin}<ul>`,
284
- `${margin} <li>URL: ${strings.urlLink}</li>`,
285
- `${margin} <li>${strings.testInfo}</li>`,
286
- `${margin} <li>Summary of results:`,
287
- `${margin} <ul>`,
288
- `${margin} <li>Rule engines:`,
289
- `${margin} <ul>`,
290
- `${margin} <li>Called: ${summary.engineCount}</li>`,
291
- `${margin} <li>Were able to test: ${summary.testedEngineCount}</li>`,
292
- `${margin} <li>Reported any rule violations: ${summary.reporterCount}</li>`,
293
- `${margin} </ul>`,
294
- `${margin} </li>`,
295
- `${margin} <li>Violations: ${summary.violationCount}</li>`,
296
- `${margin} <li>Violators: ${summary.violatorCount}</li>`,
297
- `${margin} <li>Issues: ${summary.issueCount}</li>`,
298
- `${margin} </ul>`,
299
- `${margin} </li>`,
300
- `${margin} <li>${strings.fullReportLink}</li>`,
301
- `${margin}</ul>`
302
- ];
282
+ ): string[] => {
283
+ const {
284
+ engineCount, issueCount, reporterCount, testedEngineCount, violationCount, violatorCount
285
+ } = summary;
286
+ return [
287
+ `${margin}<ul>`,
288
+ `${margin} <li>URL: ${strings.urlLink}</li>`,
289
+ `${margin} <li>${strings.testInfo}</li>`,
290
+ `${margin} <li>Rule engines: ${engineCount} called, ${testedEngineCount} able to test, ${reporterCount} reported any violations</li>`,
291
+ `${margin} <li>Results: ${getCountString(violationCount, 'violation', 'violations')}, ${getCountString(violatorCount, 'violator', 'violators')}, ${getCountString(issueCount, 'issue', 'issues')}</li>`,
292
+ `${margin} <li>${strings.fullReportLink}</li>`,
293
+ `${margin}</ul>`
294
+ ];
295
+ };
303
296
  // Returns the lines of a list of facts about an issue in an available report.
304
297
  export const getIssueFactsLines = (
305
298
  report: UsableReport,
@@ -328,8 +321,7 @@ export const getIssueFactsLines = (
328
321
  `${margin} <li>Priority: ${getWeightName(weight)}</li>`,
329
322
  `${margin} <li>Related WCAG standard: <a href="${getWCAGLink(wcag)}">${wcag}</a></li>`,
330
323
  `${margin} <li>Reported by ${reporterString} (${getEngineNamesString(reporters)})</li>`,
331
- `${margin} <li>Violations: ${violationCount}</li>`,
332
- `${margin} <li>Violators: ${violatorIndexes.size}</li>`,
324
+ `${margin} <li>Results: ${getCountString(violationCount, 'violation', 'violations')}, ${getCountString(violatorIndexes.size, 'violator', 'violators')}</li>`,
333
325
  `${margin}</ul>`
334
326
  ];
335
327
  };
@@ -338,7 +330,6 @@ export const getViolatorFactsLines = (
338
330
  report: UsableReport,
339
331
  issueID: string,
340
332
  catalogIndex: string,
341
- pathID: string | null,
342
333
  margin: string,
343
334
  listClass?: string
344
335
  ): string[] => {
@@ -350,9 +341,12 @@ export const getViolatorFactsLines = (
350
341
  const text = catalogItem?.text;
351
342
  // Identify the rule engines that reported the violator for the issue.
352
343
  const reporters = new Set<string>();
353
- getTestActInstances(report, {violationsOnly: true, issueID, catalogIndex}).forEach(({act}) => {
344
+ let instancePathID: string | undefined;
345
+ getTestActInstances(report, {violationsOnly: true, issueID, catalogIndex}).forEach(({act, instance}) => {
354
346
  reporters.add(act.which!);
347
+ instancePathID ??= instance.pathID;
355
348
  });
349
+ const pathID = catalogItem?.pathID || instancePathID;
356
350
  const lines: string[] = [
357
351
  `${margin}<ul${listClass ? ` class="${listClass}"` : ''}>`,
358
352
  `${margin} <li>Tag name: <code>${tagName || 'HTML'}</code></li>`