From 543ae3751e109305a8a02061beb46b5b6edac03f Mon Sep 17 00:00:00 2001 From: Jonathan Pool Date: Sat, 3 Oct 2026 21:17:14 -0700 Subject: [PATCH 01/23] continue reorganizing web UI lists --- web/listIssues/index.html | 18 ++++++++++++++++-- web/listIssues/index.ts | 11 +++++++++++ web/listReports/index.ts | 25 ++++++++----------------- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/web/listIssues/index.html b/web/listIssues/index.html index 2d8a16bc..f451ef17 100644 --- a/web/listIssues/index.html +++ b/web/listIssues/index.html @@ -17,13 +17,27 @@

About the __target__ page

Should Kilotest retest the page?

Issues reported

Summary

__reporterCount__ reported issues (__reporters__)

-

__issueCount__ reported (__highestCount__ highest, __highCount__ high, __lowCount__ low, __lowestCount__ lowest priority)

+

Summary of results:

+

__violatorCount__ reported

Download the full technical report

Details

diff --git a/web/listIssues/index.ts b/web/listIssues/index.ts index ccf47a9d..ac142030 100644 --- a/web/listIssues/index.ts +++ b/web/listIssues/index.ts @@ -42,6 +42,7 @@ const getIssuesData = async (timeStamp: string, jobID: string) => { reporters: [], reporterList: '', reporterCount: 0, + violationCount: 0, violatorCount: 0, preventions: report.jobData.preventions, issues: { @@ -71,11 +72,15 @@ const getIssuesData = async (timeStamp: string, jobID: string) => { weight, reporters: new Set(), reporterList: '', + violationCount: 0, violators: new Set() }; // Ensure the rule engine is in the temporary data. temp.issues[issueID].reporters.add(which); temp.reporters.add(which); + // Increment the violation counts. + temp.issues[issueID].violationCount++; + final.violationCount++; // If the instance has a catalog index: if (catalogIndex) { // Ensure the violator is in the temporary data. @@ -100,6 +105,7 @@ const getIssuesData = async (timeStamp: string, jobID: string) => { }; finalIssue.reporterList = getEngineNamesString(issue.reporters); finalIssue.reporterCount = issue.reporters.size; + finalIssue.violationCount = issue.violationCount; finalIssue.violatorCount = issue.violators.size; final.issues[issue.weight].push(finalIssue); }); @@ -164,11 +170,16 @@ const populateQuery = async (timeStamp: string, jobID: string, query: Record) => { const { issueCount, preventedEngineCount, - preventedEngineNames, - reporterNames, reporterCount, violationCount, violatorCount @@ -134,26 +131,20 @@ const populateQuery = async (query: Record) => { lines.tested.push(`${margin}
  • URL: ${urlLink}
  • `); // Add facts about the report to the lines. lines.tested.push(`${margin}
  • ${testInfo}
  • `); - // If the page prevented any rule engine from performing its tests: - if (preventedEngineCount) { - // Add this to the lines. - const engineCountString = getCountString(preventedEngineCount, 'rule engine', 'rule engines'); - lines.tested.push( - `${margin}
  • Page not testable by ${engineCountString} (${preventedEngineNames.join(' + ')})
  • `, - ); - } // Add facts about the test results to the lines. - let reporterString = `${getCountString(reporterCount, 'rule engine', 'rule engines')} reported issues`; - if (reporterCount) { - const reporterNamesString = reporterNames.join(' + '); - reporterString = `${reporterString} (${reporterNamesString})`; - } - lines.tested.push(`${margin}
  • ${reporterString}
  • `); lines.tested.push(`${margin}
  • Summary of results:`); lines.tested.push(`${margin}
      `); lines.tested.push(`${margin}
    • Violations: ${violationCount}
    • `); lines.tested.push(`${margin}
    • Violators: ${violatorCount}
    • `); lines.tested.push(`${margin}
    • Issues: ${issueCount}
    • `); + lines.tested.push(`${margin}
    • Rule engines reporting issues: ${reporterCount}
    • `); + // If the page prevented any rule engine from performing its tests: + if (preventedEngineCount) { + // Add this to the lines. + lines.tested.push( + `${margin}
    • Rule engines prevented from testing: ${preventedEngineCount}
    • `, + ); + } lines.tested.push(`${margin}
    `); lines.tested.push(`${margin}
  • `); lines.tested.push(`${margin} `); From 44d711ad1358c0daf7a14d9cdf1c95f5137507bc Mon Sep 17 00:00:00 2001 From: Jonathan Pool Date: Sun, 4 Oct 2026 07:50:55 -0700 Subject: [PATCH 02/23] revise job ID string --- web/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/util.ts b/web/util.ts index 71d6de84..e57766b4 100644 --- a/web/util.ts +++ b/web/util.ts @@ -256,7 +256,7 @@ export const getPageDataStrings = async ( description, url, urlLink: `${url}`, - testInfo: `Tested ${daysAgo === 1 ? '1 day' : `${daysAgo} days`} ago by job ${jobID} on ${when}` + testInfo: `Tested ${daysAgo === 1 ? '1 day' : `${daysAgo} days`} ago by job ${timeStamp}-${jobID} on ${when}` }; }; // Gets the descriptions of multi-report pages. From 46fa1c83a081e62ed09337d62e252b56ba044e2d Mon Sep 17 00:00:00 2001 From: Jonathan Pool Date: Sun, 4 Oct 2026 07:53:25 -0700 Subject: [PATCH 03/23] revise test info string --- web/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/util.ts b/web/util.ts index e57766b4..3034e6d0 100644 --- a/web/util.ts +++ b/web/util.ts @@ -256,7 +256,7 @@ export const getPageDataStrings = async ( description, url, urlLink: `${url}`, - testInfo: `Tested ${daysAgo === 1 ? '1 day' : `${daysAgo} days`} ago by job ${timeStamp}-${jobID} on ${when}` + testInfo: `Tested ${daysAgo === 1 ? '1 day' : `${daysAgo} days`} ago on ${when} by job ${timeStamp}-${jobID}` }; }; // Gets the descriptions of multi-report pages. From 7ca408a43566949d59ec3b8b223d73c45fed5552 Mon Sep 17 00:00:00 2001 From: Jonathan Pool Date: Sun, 4 Oct 2026 08:09:58 -0700 Subject: [PATCH 04/23] revise report summaries --- web/listReports/index.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/web/listReports/index.ts b/web/listReports/index.ts index 823dd004..54ced5ee 100644 --- a/web/listReports/index.ts +++ b/web/listReports/index.ts @@ -113,6 +113,7 @@ const populateQuery = async (query: Record) => { return; } const { + engineCount, issueCount, preventedEngineCount, reporterCount, @@ -134,21 +135,24 @@ const populateQuery = async (query: Record) => { // Add facts about the test results to the lines. lines.tested.push(`${margin}
  • Summary of results:`); lines.tested.push(`${margin}
      `); - lines.tested.push(`${margin}
    • Violations: ${violationCount}
    • `); - lines.tested.push(`${margin}
    • Violators: ${violatorCount}
    • `); - lines.tested.push(`${margin}
    • Issues: ${issueCount}
    • `); - lines.tested.push(`${margin}
    • Rule engines reporting issues: ${reporterCount}
    • `); + lines.tested.push(`${margin}
    • Rule engines:`); + lines.tested.push(`${margin}
        `); + lines.tested.push(`${margin}
      • Called: ${engineCount}
      • `); + lines.tested.push(`${margin}
      • Reported issues: ${reporterCount}
      • `); // If the page prevented any rule engine from performing its tests: if (preventedEngineCount) { // Add this to the lines. - lines.tested.push( - `${margin}
      • Rule engines prevented from testing: ${preventedEngineCount}
      • `, - ); + lines.tested.push(`${margin}
      • Unable to test: ${preventedEngineCount}
      • `); } + lines.tested.push(`${margin}
      `); + lines.tested.push(`${margin}
    • `); + lines.tested.push(`${margin}
    • Issues: ${issueCount}
    • `); + lines.tested.push(`${margin}
    • Violators: ${violatorCount}
    • `); + lines.tested.push(`${margin}
    • Violations: ${violationCount}
    • `); lines.tested.push(`${margin}
    `); lines.tested.push(`${margin}
  • `); lines.tested.push(`${margin} `); - lines.tested.push(`${margin}`); lines.tested.push(`${margin} `); - lines.tested.push(`${margin}
  • Issues: ${issueCount}
  • `); - lines.tested.push(`${margin}
  • Violators: ${violatorCount}
  • `); lines.tested.push(`${margin}
  • Violations: ${violationCount}
  • `); + lines.tested.push(`${margin}
  • Violators: ${violatorCount}
  • `); + lines.tested.push(`${margin}
  • Issues: ${issueCount}
  • `); lines.tested.push(`${margin} `); lines.tested.push(`${margin} `); lines.tested.push(`${margin} `); From 6b0efc25bf9521c09eaeaed28078b530bc053938 Mon Sep 17 00:00:00 2001 From: Jonathan Pool Date: Sun, 4 Oct 2026 08:30:00 -0700 Subject: [PATCH 08/23] revise report summaries --- web/listIssues/index.html | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/web/listIssues/index.html b/web/listIssues/index.html index f451ef17..81593f2e 100644 --- a/web/listIssues/index.html +++ b/web/listIssues/index.html @@ -13,17 +13,22 @@

    Kilotest: Issues reported for __target__

    -

    About the __target__ page

    +

    About the page

    • URL: __urlLink__
    • __testInfo__
    • Summary of results:
        +
      • Rule engines: +
          +
        • Called: __engineCount__
        • +
        • Were able to test: __testedCount__
        • +
        • Reported any rule violations: __reporterCount__
        • +
        +
      • Violations: __violationCount__
      • Violators: __violatorCount__
      • Issues: __issueCount__
      • -
      • Rule engines reporting issues: __reporterCount__
      • - __preventions__
    From 603fabccd9b13961fb05a0ae3f355f9b2b9972c0 Mon Sep 17 00:00:00 2001 From: Jonathan Pool Date: Sun, 4 Oct 2026 09:16:38 -0700 Subject: [PATCH 09/23] revise report summaries --- web/listIssues/index.html | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/web/listIssues/index.html b/web/listIssues/index.html index 81593f2e..b3b69826 100644 --- a/web/listIssues/index.html +++ b/web/listIssues/index.html @@ -34,18 +34,6 @@

    About the page

    Should Kilotest retest the page?

    Issues reported

    -

    Summary

    -

    __reporterCount__ reported issues (__reporters__)

    -

    Summary of results:

    -
      -
    • Violations: __violationCount__
    • -
    • - (__highestCount__ highest, __highCount__ high, __lowCount__ low, __lowestCount__ lowest priority) -
    • -
    -

    __violatorCount__ reported

    -

    Download the full technical report

    -

    Details

    Highest priority: __highestCount__
      From 8e6904903df3ec13a2a9f70ae3aae3df4a21cd2e Mon Sep 17 00:00:00 2001 From: Jonathan Pool Date: Sun, 4 Oct 2026 09:30:33 -0700 Subject: [PATCH 10/23] revise report summaries --- web/listIssues/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/web/listIssues/index.ts b/web/listIssues/index.ts index ac142030..1031c342 100644 --- a/web/listIssues/index.ts +++ b/web/listIssues/index.ts @@ -34,6 +34,7 @@ const getIssuesData = async (timeStamp: string, jobID: string) => { // Initialize the temporary data. const temp = { issues: {} as Record, + ruleEngines: new Set(), reporters: new Set(), violators: new Set() }; @@ -57,6 +58,8 @@ const getIssuesData = async (timeStamp: string, jobID: string) => { getTestActInstances(report, {violationsOnly: true}).forEach(({act, instance}) => { const {catalogIndex, issueID} = instance; const which = act.which!; + // Ensure the rule engine is in the temporary data. + temp.ruleEngines.add(which); // If it identifies a non-ignorable issue: if (issueID && issueID !== 'ignorable') { const issueClassification = issueSpecs[issueID]; @@ -91,6 +94,7 @@ const getIssuesData = async (timeStamp: string, jobID: string) => { } }); // Finish populating the final data. + final.engineCount = temp.ruleEngines.size; final.reporterList = getEngineNamesString(temp.reporters); final.reporterCount = temp.reporters.size; final.violatorCount = temp.violators.size; @@ -168,6 +172,7 @@ const populateQuery = async (timeStamp: string, jobID: string, query: Record Date: Sun, 4 Oct 2026 09:32:49 -0700 Subject: [PATCH 11/23] debug --- web/listIssues/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/listIssues/index.ts b/web/listIssues/index.ts index 1031c342..57b05a24 100644 --- a/web/listIssues/index.ts +++ b/web/listIssues/index.ts @@ -203,7 +203,7 @@ const populateQuery = async (timeStamp: string, jobID: string, query: Record Date: Sun, 4 Oct 2026 09:34:38 -0700 Subject: [PATCH 12/23] debug --- web/listIssues/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/listIssues/index.ts b/web/listIssues/index.ts index 57b05a24..471e401c 100644 --- a/web/listIssues/index.ts +++ b/web/listIssues/index.ts @@ -206,13 +206,13 @@ const populateQuery = async (timeStamp: string, jobID: string, query: Record { const weightName = getWeightName(weight); From 0aad528580e32ab2200d9b3b56c4994f5a17e0ab Mon Sep 17 00:00:00 2001 From: Jonathan Pool Date: Sun, 4 Oct 2026 09:43:33 -0700 Subject: [PATCH 13/23] debug --- web/listIssues/index.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/web/listIssues/index.ts b/web/listIssues/index.ts index 471e401c..cda18d99 100644 --- a/web/listIssues/index.ts +++ b/web/listIssues/index.ts @@ -231,6 +231,7 @@ const populateQuery = async (timeStamp: string, jobID: string, query: RecordReported by ${reporterCountString} (${reporterList})` ); + const violationCountString = violationCount === 1 + ? '1 violation' + : `${violationCount} violations`; const violatorCountString = violatorCount === 1 - ? '1 violator was' - : `${violatorCount} violators were`; - detailsLines.push(`${margin}
    • ${violatorCountString} reported
    • `); + ? '1 violator' + : `${violatorCount} violators`; + detailsLines.push(`${margin}
    • ${violationCountString} by ${violatorCountString} reported
    • `); // Add the end of the fact list to the lines. detailsLines.push(`${margin}
    `); // Add the start of a link list to the lines. From d0d67397c9cfcddce4b398acae9b2c55d8e0eb49 Mon Sep 17 00:00:00 2001 From: Jonathan Pool Date: Sun, 4 Oct 2026 09:54:22 -0700 Subject: [PATCH 14/23] debug --- web/listViolators/index.html | 3 ++- web/listViolators/index.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/web/listViolators/index.html b/web/listViolators/index.html index 66594954..ab3a891a 100644 --- a/web/listViolators/index.html +++ b/web/listViolators/index.html @@ -25,7 +25,8 @@

    About the __issue__ issue

  • Priority: __priority__
  • Related WCAG standard: __wcag__
  • Reported by __reporterCount__ (__reporters__)
  • -
  • __violatorCount__ reported
  • +
  • Violations: __violationCount__
  • +
  • Violators: __violatorCount__
  • Violators

    Here are the HTML elements reported to violate rules belonging to the __issue__ issue.

    diff --git a/web/listViolators/index.ts b/web/listViolators/index.ts index 75b4a5e8..7c7eb113 100644 --- a/web/listViolators/index.ts +++ b/web/listViolators/index.ts @@ -74,8 +74,10 @@ const populateQuery = async ( return; } const {catalog} = report; + const testActInstances = getTestActInstances(report, {violationsOnly: true, issueID}); + query.violationCount = testActInstances.length; // Otherwise, i.e. if it succeeded, for each violating standard instance of the issue: - getTestActInstances(report, {violationsOnly: true, issueID}).forEach(({act, instance}) => { + testActInstances.forEach(({act, instance}) => { const pathID = instance.pathID || '/html'; const catalogIndex = String(instance.catalogIndex || '0'); const tagName = catalog[catalogIndex]?.tagName From 1d17958221d918e4c96942cbda62b661c28daf99 Mon Sep 17 00:00:00 2001 From: Jonathan Pool Date: Sun, 4 Oct 2026 09:56:30 -0700 Subject: [PATCH 15/23] debug --- web/listViolators/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/listViolators/index.ts b/web/listViolators/index.ts index 7c7eb113..1afc6d8c 100644 --- a/web/listViolators/index.ts +++ b/web/listViolators/index.ts @@ -94,7 +94,7 @@ const populateQuery = async ( }); // Populate the violator count. const violatorCount = Object.keys(violators).length; - query.violatorCount = violatorCount === 1 ? '1 violator was' : `${violatorCount} violators were`; + query.violatorCount = violatorCount; // For each violator: Object.values(violators).forEach((violatorData: any) => { // Convert the set of its reporters to a string. From 51fb7ec370776f269507889429a2e889c251bfb9 Mon Sep 17 00:00:00 2001 From: Jonathan Pool Date: Sun, 4 Oct 2026 10:25:04 -0700 Subject: [PATCH 16/23] debug --- web/listIssues/index.ts | 42 +++++++------------- web/listIssues/listIssues.test.ts | 24 +++++++---- web/listReports/index.ts | 8 +--- web/listReports/listReports.test.ts | 53 +++++++++++++++---------- web/listViolators/listViolators.test.ts | 4 +- web/util.test.ts | 4 +- web/util.ts | 15 +++++-- 7 files changed, 79 insertions(+), 71 deletions(-) diff --git a/web/listIssues/index.ts b/web/listIssues/index.ts index cda18d99..547285c1 100644 --- a/web/listIssues/index.ts +++ b/web/listIssues/index.ts @@ -8,11 +8,10 @@ import { getReport, getTestActInstances, - htmlSafe, + getTestActs, isUsableReport, objectSort, - populateTemplate, - ruleEngines + populateTemplate } from '../../util.ts'; import { getPageData, @@ -34,7 +33,6 @@ const getIssuesData = async (timeStamp: string, jobID: string) => { // Initialize the temporary data. const temp = { issues: {} as Record, - ruleEngines: new Set(), reporters: new Set(), violators: new Set() }; @@ -43,9 +41,10 @@ const getIssuesData = async (timeStamp: string, jobID: string) => { reporters: [], reporterList: '', reporterCount: 0, + engineCount: 0, + testedCount: 0, violationCount: 0, violatorCount: 0, - preventions: report.jobData.preventions, issues: { 4: [], 3: [], @@ -58,8 +57,6 @@ const getIssuesData = async (timeStamp: string, jobID: string) => { getTestActInstances(report, {violationsOnly: true}).forEach(({act, instance}) => { const {catalogIndex, issueID} = instance; const which = act.which!; - // Ensure the rule engine is in the temporary data. - temp.ruleEngines.add(which); // If it identifies a non-ignorable issue: if (issueID && issueID !== 'ignorable') { const issueClassification = issueSpecs[issueID]; @@ -94,7 +91,13 @@ const getIssuesData = async (timeStamp: string, jobID: string) => { } }); // Finish populating the final data. - final.engineCount = temp.ruleEngines.size; + const testedEngineIDs = new Set(getTestActs(report).map(act => act.which!)); + const calledEngineIDs = new Set([ + ...testedEngineIDs, + ...Object.keys(report.jobData?.preventions ?? {}) + ]); + final.engineCount = calledEngineIDs.size; + final.testedCount = testedEngineIDs.size; final.reporterList = getEngineNamesString(temp.reporters); final.reporterCount = temp.reporters.size; final.violatorCount = temp.violators.size; @@ -173,46 +176,29 @@ const populateQuery = async (timeStamp: string, jobID: string, query: Record { - const [engineName, engineSponsor] = ruleEngines[preventedEngineID] ?? [preventedEngineID, 'unknown sponsor']; - const engineNameString = `${engineName} (${engineSponsor})`; - const causeString = htmlSafe(preventions[preventedEngineID]); - const preventionString = `${margin}
  • Page not testable by ${engineNameString}: ${causeString}
  • `; - preventionStrings.push(preventionString); - }); - // Add prevention notices to the query. - query.preventions = preventionStrings.join('\n'); // Add report data to the query. query.timeStamp = timeStamp; query.jobID = jobID; - query.testedCount = engineCount - Object.keys(preventions).length; - // Add reporter information to the query. - query.reporterCount = reporterCount; - query.reporters = reporterList; // Add a summary of the issues to the query. - query.issueCount = issueCount; query.highestCount = issues[4].length; query.highCount = issues[3].length; query.lowCount = issues[2].length; query.lowestCount = issues[1].length; - // Add a violator count to the query. - query.violatorCount = violatorCount; // For each weight: [4, 3, 2, 1].forEach(weight => { const weightName = getWeightName(weight); diff --git a/web/listIssues/listIssues.test.ts b/web/listIssues/listIssues.test.ts index ed6d9b7d..4322b313 100644 --- a/web/listIssues/listIssues.test.ts +++ b/web/listIssues/listIssues.test.ts @@ -93,14 +93,23 @@ test('listIssues for the empty report shows no issue links', async () => { }); -test('listIssues includes prevention notices for the prevented report', async () => { +test('listIssues counts the prevented rule engine as called but not able to test', async () => { const result = await answer('260101T0006/prv'); assert.equal(result.status, 'ok'); - assert.ok(result.answerPage.includes('Page not testable by')); - assert.ok(result.answerPage.includes('page timed out')); + assert.ok(result.answerPage.includes('
  • Called: 2
  • ')); + assert.ok(result.answerPage.includes('
  • Were able to test: 1
  • ')); + assert.ok(result.answerPage.includes('
  • Reported any rule violations: 1
  • ')); }); -test('listIssues falls back to the engine ID for an unknown prevented engine', async () => { +test('listIssues counts a rule engine that reported no violations as called and able to test', async () => { + const result = await answer('260101T0005/emp'); + assert.equal(result.status, 'ok'); + assert.ok(result.answerPage.includes('
  • Called: 1
  • ')); + assert.ok(result.answerPage.includes('
  • Were able to test: 1
  • ')); + assert.ok(result.answerPage.includes('
  • Reported any rule violations: 0
  • ')); +}); + +test('listIssues counts an unknown prevented engine as called but not able to test', async () => { const fs = await import('node:fs/promises'); const prvJSON = await fs.readFile(path.join(realUtil.reportsPath(), '260101T0006-prv.json'), 'utf8'); const report = JSON.parse(prvJSON); @@ -110,7 +119,8 @@ test('listIssues falls back to the engine ID for an unknown prevented engine', a try { const result = await answer('260103T0000/unk'); assert.equal(result.status, 'ok'); - assert.ok(result.answerPage.includes('unknownEngine (unknown sponsor)')); + assert.ok(result.answerPage.includes('
  • Called: 3
  • ')); + assert.ok(result.answerPage.includes('
  • Were able to test: 1
  • ')); } finally { await fs.unlink(reportPath); @@ -146,10 +156,10 @@ test('listIssues returns an error when report facts are not obtained', async () }); test('listIssues shows plural violator count for an issue with multiple violators', async () => { - // The mul report has linkNoText with 3 violators. + // The mul report has linkNoText with 4 violations by 3 violators. const result = await answer('260101T0008/mul'); assert.equal(result.status, 'ok'); - assert.ok(result.answerPage.includes('3 violators were')); + assert.ok(result.answerPage.includes('4 violations by 3 violators reported')); }); test('listIssues handles acts with no standardResult instances', async () => { diff --git a/web/listReports/index.ts b/web/listReports/index.ts index 3555d364..9dd0bfbf 100644 --- a/web/listReports/index.ts +++ b/web/listReports/index.ts @@ -115,8 +115,8 @@ const populateQuery = async (query: Record) => { const { engineCount, issueCount, - preventedEngineCount, reporterCount, + testedEngineCount, violationCount, violatorCount } = reportData; @@ -138,11 +138,7 @@ const populateQuery = async (query: Record) => { lines.tested.push(`${margin}
  • Rule engines:`); lines.tested.push(`${margin}
      `); lines.tested.push(`${margin}
    • Called: ${engineCount}
    • `); - // If the page prevented any rule engine from performing its tests: - if (preventedEngineCount) { - // Add this to the lines. - lines.tested.push(`${margin}
    • Were able to test: ${engineCount - preventedEngineCount}
    • `); - } + lines.tested.push(`${margin}
    • Were able to test: ${testedEngineCount}
    • `); lines.tested.push(`${margin}
    • Reported any rule violations: ${reporterCount}
    • `); lines.tested.push(`${margin}
    `); lines.tested.push(`${margin}
  • `); diff --git a/web/listReports/listReports.test.ts b/web/listReports/listReports.test.ts index 433d4e67..5ac165ae 100644 --- a/web/listReports/listReports.test.ts +++ b/web/listReports/listReports.test.ts @@ -231,7 +231,7 @@ const getReportDetails = (html: any, description: string, hrefFragment?: string) return matches[0]; }; -// Returns the rule-engine line, the prevention line, and the 3 summary amounts of a details element. +// Returns the 3 rule-engine amounts and the 3 summary amounts of a details element. const getResultFacts = (details: any) => { const items = details.querySelectorAll('li'); const itemTexts: string[] = items.map((li: any) => li.text.trim()); @@ -240,13 +240,17 @@ const getResultFacts = (details: any) => { assert.ok(text, `${label} item exists`); return Number(text.slice(label.length + 2)); }; - // The summary item is the parent of the 3 amount items. + // The summary item is the parent of the rule-engine item and the 3 amount items. const summaryItem = items.find((li: any) => li.text.trim().startsWith('Summary of results:')); assert.ok(summaryItem, 'Summary of results item exists'); - const amountLabels = summaryItem.querySelectorAll('ul > li').map((li: any) => li.text.trim().split(':')[0]); + const summaryList = summaryItem.querySelector('ul'); + const amountLabels = summaryList.childNodes + .filter((node: any) => node.tagName === 'LI') + .map((li: any) => li.text.trim().split(':')[0]); return { - reporterLine: itemTexts.find(t => t.endsWith('reported issues') || t.includes('reported issues (')), - preventionLine: itemTexts.find(t => t.startsWith('Page not testable by')), + called: getAmount('Called'), + tested: getAmount('Were able to test'), + reporters: getAmount('Reported any rule violations'), amountLabels, violations: getAmount('Violations'), violators: getAmount('Violators'), @@ -255,37 +259,42 @@ const getResultFacts = (details: any) => { }; // Expected facts, hand-computed from the fixtures. -const expectedFacts: [string, string | undefined, string, number, number, number][] = [ - // Description, issue-link fragment, reporter line, violations, violators, issues. - ['Mixed Outcomes Page', '260101T0000/mix', '2 rule engines reported issues (Alfa + Axe)', 3, 2, 2], - ['Mixed Outcomes Page', '260202T0000/new', '1 rule engine reported issues (Axe)', 1, 1, 1], - ['All CantTell Page', undefined, '0 rule engines reported issues', 0, 0, 0], - ['No Outcomes Page', undefined, '1 rule engine reported issues (Accessibility Checker)', 1, 1, 1], - ['Empty Results Page', undefined, '0 rule engines reported issues', 0, 0, 0], - ['Prevented Page', undefined, '1 rule engine reported issues (Axe)', 1, 1, 1], - ['Multi Violator Page', undefined, '2 rule engines reported issues (Alfa + Axe)', 4, 3, 1], - ['Branch Coverage Page', undefined, '1 rule engine reported issues (Axe)', 4, 4, 1] +const expectedFacts: [string, string | undefined, number, number, number, number, number, number][] = [ + // Description, issue-link fragment, engines called, engines able to test, reporting engines, violations, violators, issues. + ['Mixed Outcomes Page', '260101T0000/mix', 2, 2, 2, 3, 2, 2], + ['Mixed Outcomes Page', '260202T0000/new', 1, 1, 1, 1, 1, 1], + ['All CantTell Page', undefined, 1, 1, 0, 0, 0, 0], + ['No Outcomes Page', undefined, 1, 1, 1, 1, 1, 1], + ['Empty Results Page', undefined, 1, 1, 0, 0, 0, 0], + ['Prevented Page', undefined, 2, 1, 1, 1, 1, 1], + ['Multi Violator Page', undefined, 2, 2, 2, 4, 3, 1], + ['Branch Coverage Page', undefined, 3, 3, 1, 4, 4, 1] ]; -for (const [description, fragment, reporterLine, violations, violators, issues] of expectedFacts) { - test(`listReports gives correct rule-engine count and names and summary amounts for ${description}${fragment ? ` (${fragment})` : ''}`, async () => { +for (const [description, fragment, called, tested, reporters, violations, violators, issues] of expectedFacts) { + test(`listReports gives correct rule-engine and summary amounts for ${description}${fragment ? ` (${fragment})` : ''}`, async () => { const result: any = await answer(); const html = parse(result.answerPage); const facts = getResultFacts(getReportDetails(html, description, fragment)); - assert.equal(facts.reporterLine, reporterLine); - assert.deepEqual(facts.amountLabels, ['Violations', 'Violators', 'Issues']); + assert.equal(facts.called, called); + assert.equal(facts.tested, tested); + assert.equal(facts.reporters, reporters); + assert.deepEqual(facts.amountLabels, ['Rule engines', 'Violations', 'Violators', 'Issues']); assert.equal(facts.violations, violations); assert.equal(facts.violators, violators); assert.equal(facts.issues, issues); }); } -test('listReports reports the prevented rule engine only for the report with a prevention', async () => { +test('listReports counts a prevented rule engine as called but not able to test', async () => { const result: any = await answer(); const html = parse(result.answerPage); const prevented = getResultFacts(getReportDetails(html, 'Prevented Page')); - assert.equal(prevented.preventionLine, 'Page not testable by 1 rule engine (Alfa)'); + assert.equal(prevented.called - prevented.tested, 1); const others = html.querySelectorAll('details').filter((d: any) => !d.text.includes('Prevented Page')); assert.equal(others.length, 7); - assert.ok(others.every((d: any) => !d.text.includes('Page not testable by'))); + assert.ok(others.every((d: any) => { + const facts = getResultFacts(d); + return facts.called === facts.tested; + })); }); diff --git a/web/listViolators/listViolators.test.ts b/web/listViolators/listViolators.test.ts index ed830541..41813438 100644 --- a/web/listViolators/listViolators.test.ts +++ b/web/listViolators/listViolators.test.ts @@ -86,7 +86,7 @@ test('listViolators includes links to listDiagnoses for violators', async () => test('listViolators excludes cantTell instances from the violator count', async () => { const result = await answer('focusIndicationBad/260101T0000/mix'); assert.equal(result.status, 'ok'); - assert.ok(result.answerPage.includes('0 violators were')); + assert.ok(result.answerPage.includes('
  • Violators: 0
  • ')); assert.ok(!result.answerPage.includes('

    Element')); }); @@ -260,7 +260,7 @@ test('listViolators handles instances with missing catalogIndex and acts with no const result = await answer('linkNoText/260101T0004/nvi'); assert.equal(result.status, 'ok'); // Should show plural violator count. - assert.ok(result.answerPage.includes('2 violators were')); + assert.ok(result.answerPage.includes('
  • Violators: 2
  • ')); } finally { await fs.unlink(reportPath).catch(() => {}); diff --git a/web/util.test.ts b/web/util.test.ts index c4c1b5ad..8162ec23 100644 --- a/web/util.test.ts +++ b/web/util.test.ts @@ -132,14 +132,14 @@ test('getPageDataStrings returns HTML strings for a valid report', async () => { assert.equal(strings.description, 'Mixed Outcomes Page'); assert.equal(strings.url, 'https://example.com/mixed'); assert.equal(strings.urlLink, 'https://example.com/mixed'); - assert.ok(strings.testInfo.includes('by job mix')); + assert.ok(strings.testInfo.includes('by job 260101T0000-mix')); assert.ok(strings.testInfo.includes('2026-01-01 at 00:00')); }); test('getPageDataStrings returns different testInfo for a different timeStamp', async () => { const strings = await getPageDataStrings('260101T0001', 'ct') as any; assert.equal(strings.description, 'All CantTell Page'); - assert.ok(strings.testInfo.includes('by job ct')); + assert.ok(strings.testInfo.includes('by job 260101T0001-ct')); assert.ok(strings.testInfo.includes('2026-01-01 at 00:01')); }); diff --git a/web/util.ts b/web/util.ts index 3034e6d0..84bccbbd 100644 --- a/web/util.ts +++ b/web/util.ts @@ -52,6 +52,7 @@ export type ReportData = { issueCount: number; engineNames: string[]; engineCount: number; + testedEngineCount: number; reporterNames: string[]; reporterCount: number; violationCount: number; @@ -159,6 +160,7 @@ export const getReportData = async (timeStamp: string, jobID: string): Promise(); - const engineNameSet = new Set(); + const testedEngineIDSet = new Set(); const reporterIDSet = new Set(); const violatorIndexSet = new Set(); // For each test act of the report: getTestActs(report).forEach(act => { // Ensure that the rule engine is in the temporary data. - engineNameSet.add(ruleEngines[act.which!]![0]); + testedEngineIDSet.add(act.which!); }); + const preventedEngineIDs = Object.keys(report.jobData?.preventions || {}); + // Identify the rule engines called, including those prevented from testing. + const calledEngineIDSet = new Set([...testedEngineIDSet, ...preventedEngineIDs]); // For each violating standard instance of each test act: getTestActInstances(report, {violationsOnly: true}).forEach(({act, instance}) => { const {catalogIndex, issueID} = instance; @@ -196,9 +201,11 @@ export const getReportData = async (timeStamp: string, jobID: string): Promise ruleEngines[id]?.[0] || id) .sort((a, b) => a.localeCompare(b, 'en', {sensitivity: 'base'})); - data.engineCount = engineNameSet.size; + data.engineCount = calledEngineIDSet.size; + data.testedEngineCount = testedEngineIDSet.size; data.reporterNames = Array .from(reporterIDSet) .map(id => ruleEngines[id]![0]) From ccabc94d79b3515493c8049f63af5fa002f2656a Mon Sep 17 00:00:00 2001 From: Jonathan Pool Date: Sun, 4 Oct 2026 10:39:22 -0700 Subject: [PATCH 17/23] debug --- test/fixtures/buildFixtures.ts | 11 ++++- test/fixtures/db/reports/260101T0006-prv.json | 10 +++++ web/listIssues/index.ts | 12 ++--- web/util.test.ts | 43 ++++++++++++++++++ web/util.ts | 44 +++++++++++++------ 5 files changed, 98 insertions(+), 22 deletions(-) diff --git a/test/fixtures/buildFixtures.ts b/test/fixtures/buildFixtures.ts index 9711afd0..22f93e8c 100644 --- a/test/fixtures/buildFixtures.ts +++ b/test/fixtures/buildFixtures.ts @@ -203,11 +203,20 @@ const main = async () => { const preventedCatalog = { '0': catalogItem('IMG', 'An image', '/html/body/img[1]', '0:0:200:100') }; + // As in real reports, the prevented engine has a test act, with no standard result. + const preventedAlfaAct = { + type: 'test', + which: engineAlfa, + startTime: '26-01-01T00:00', + data: {prevented: true, error: 'page timed out'}, + endTime: '26-01-01T00:01' + }; const preventedActs = [ testAct(engineAxe, [ instance('r11', 'The image has no alt text', 'failed', issueLinkNoText, 0, 2, 1) - ]) + ]), + preventedAlfaAct ]; const preventedReport = report('260101T0006-prv', 'Prevented Page', 'https://example.com/prevented', preventedActs, preventedCatalog); diff --git a/test/fixtures/db/reports/260101T0006-prv.json b/test/fixtures/db/reports/260101T0006-prv.json index 5ad49b28..90ba0c43 100644 --- a/test/fixtures/db/reports/260101T0006-prv.json +++ b/test/fixtures/db/reports/260101T0006-prv.json @@ -42,6 +42,16 @@ } } } + }, + { + "type": "test", + "which": "alfa", + "startTime": "26-01-01T00:00", + "data": { + "prevented": true, + "error": "page timed out" + }, + "endTime": "26-01-01T00:01" } ], "jobData": { diff --git a/web/listIssues/index.ts b/web/listIssues/index.ts index 547285c1..9bc01272 100644 --- a/web/listIssues/index.ts +++ b/web/listIssues/index.ts @@ -8,7 +8,6 @@ import { getReport, getTestActInstances, - getTestActs, isUsableReport, objectSort, populateTemplate @@ -16,6 +15,7 @@ import { import { getPageData, getPageDataStrings, + getEngineIDs, getEngineNamesString, getWCAGLink, getWeightName @@ -91,13 +91,9 @@ const getIssuesData = async (timeStamp: string, jobID: string) => { } }); // Finish populating the final data. - const testedEngineIDs = new Set(getTestActs(report).map(act => act.which!)); - const calledEngineIDs = new Set([ - ...testedEngineIDs, - ...Object.keys(report.jobData?.preventions ?? {}) - ]); - final.engineCount = calledEngineIDs.size; - final.testedCount = testedEngineIDs.size; + const {calledIDs, preventedIDs} = getEngineIDs(report); + final.engineCount = calledIDs.length; + final.testedCount = calledIDs.length - preventedIDs.length; final.reporterList = getEngineNamesString(temp.reporters); final.reporterCount = temp.reporters.size; final.violatorCount = temp.violators.size; diff --git a/web/util.test.ts b/web/util.test.ts index 8162ec23..f30817f7 100644 --- a/web/util.test.ts +++ b/web/util.test.ts @@ -14,6 +14,7 @@ import { checkCommentLength, getAgoString, getCountString, + getEngineIDs, getEngineNamesString, getMultiReportWhats, getPageData, @@ -238,3 +239,45 @@ test('getMultiReportWhats returns descriptions that have multiple reports', asyn const whats = await getMultiReportWhats(); assert.ok(whats.includes('Mixed Outcomes Page')); }); + +// Returns a minimal report for testing rule-engine identification. +const makeEngineReport = (whiches: string[], preventions: Record) => ({ + acts: whiches.map(which => ({type: 'test', which})), + jobData: {preventions} +}); + +test('getEngineIDs counts a prevented engine, which has an act, once', () => { + const ids = getEngineIDs(makeEngineReport(['axe', 'wave'], {wave: 'failed'})); + assert.deepEqual(ids.calledIDs.sort(), ['axe', 'wave']); + assert.deepEqual(ids.preventedIDs, ['wave']); +}); + +test('getEngineIDs counts a prevented engine that has no act', () => { + const ids = getEngineIDs(makeEngineReport(['axe'], {alfa: 'failed'})); + assert.deepEqual(ids.calledIDs.sort(), ['alfa', 'axe']); + assert.deepEqual(ids.preventedIDs, ['alfa']); +}); + +test('getEngineIDs counts nuVal and nuVnu as one engine when nuVal succeeds', () => { + const ids = getEngineIDs(makeEngineReport(['nuVal', 'nuVnu'], {})); + assert.deepEqual(ids.calledIDs, ['nuVal']); + assert.deepEqual(ids.preventedIDs, []); +}); + +test('getEngineIDs ignores a nuVal prevention when nuVnu succeeds', () => { + const ids = getEngineIDs(makeEngineReport(['nuVal', 'nuVnu'], {nuVal: 'failed'})); + assert.deepEqual(ids.calledIDs, ['nuVal']); + assert.deepEqual(ids.preventedIDs, []); +}); + +test('getEngineIDs counts one prevention when both nuVal and nuVnu are prevented', () => { + const ids = getEngineIDs(makeEngineReport(['nuVal', 'nuVnu'], {nuVal: 'failed', nuVnu: 'failed'})); + assert.deepEqual(ids.calledIDs, ['nuVal']); + assert.deepEqual(ids.preventedIDs, ['nuVal']); +}); + +test('getEngineIDs counts a nuVal prevention when nuVnu was not called', () => { + const ids = getEngineIDs(makeEngineReport(['nuVal'], {nuVal: 'failed'})); + assert.deepEqual(ids.calledIDs, ['nuVal']); + assert.deepEqual(ids.preventedIDs, ['nuVal']); +}); diff --git a/web/util.ts b/web/util.ts index 84bccbbd..773ccbb3 100644 --- a/web/util.ts +++ b/web/util.ts @@ -141,6 +141,32 @@ export const getPathID = (catalog: Catalog, catalogIndex: string, pathID?: strin } return pathID ?? '/html'; }; +// Identifies the rule engines called and the rule engines prevented from testing, by a report. +// Prevented engines have test acts. The nuVal and nuVnu engines are 2 implementations of one +// engine, so they count as one. A nuVal prevention is ignored if nuVnu then ran successfully. +export const getEngineIDs = (report: any) => { + const preventions = report.jobData?.preventions ?? {}; + const calledIDs = new Set([ + ...getTestActs(report).map((act: any) => act.which as string), + ...Object.keys(preventions) + ]); + const preventedIDs = new Set(Object.keys(preventions)); + // If both implementations were called: + if (calledIDs.has('nuVal') && calledIDs.has('nuVnu')) { + // Count them as one engine. + calledIDs.delete('nuVnu'); + // If nuVnu was not prevented from testing: + if (!preventedIDs.has('nuVnu')) { + // Ignore any nuVal prevention. + preventedIDs.delete('nuVal'); + } + // Otherwise, i.e. if nuVnu was prevented, count both preventions as one. + else { + preventedIDs.delete('nuVnu'); + } + } + return {calledIDs: Array.from(calledIDs), preventedIDs: Array.from(preventedIDs)}; +}; // Returns basics about an available report. export const getReportData = async (timeStamp: string, jobID: string): Promise => { // Get the report. @@ -169,17 +195,9 @@ export const getReportData = async (timeStamp: string, jobID: string): Promise(); - const testedEngineIDSet = new Set(); const reporterIDSet = new Set(); const violatorIndexSet = new Set(); - // For each test act of the report: - getTestActs(report).forEach(act => { - // Ensure that the rule engine is in the temporary data. - testedEngineIDSet.add(act.which!); - }); - const preventedEngineIDs = Object.keys(report.jobData?.preventions || {}); - // Identify the rule engines called, including those prevented from testing. - const calledEngineIDSet = new Set([...testedEngineIDSet, ...preventedEngineIDs]); + const {calledIDs, preventedIDs} = getEngineIDs(report); // For each violating standard instance of each test act: getTestActInstances(report, {violationsOnly: true}).forEach(({act, instance}) => { const {catalogIndex, issueID} = instance; @@ -201,11 +219,11 @@ export const getReportData = async (timeStamp: string, jobID: string): Promise ruleEngines[id]?.[0] || id) .sort((a, b) => a.localeCompare(b, 'en', {sensitivity: 'base'})); - data.engineCount = calledEngineIDSet.size; - data.testedEngineCount = testedEngineIDSet.size; + data.engineCount = calledIDs.length; + data.testedEngineCount = calledIDs.length - preventedIDs.length; data.reporterNames = Array .from(reporterIDSet) .map(id => ruleEngines[id]![0]) @@ -213,7 +231,7 @@ export const getReportData = async (timeStamp: string, jobID: string): Promise ruleEngines[engineID]?.[0] || engineID) .sort((a, b) => a.localeCompare(b, 'en', {sensitivity: 'base'})); data.preventedEngineCount = data.preventedEngineNames.length; From 324d6475421978f948c56b0d542de1cf1f571d3b Mon Sep 17 00:00:00 2001 From: Jonathan Pool Date: Sun, 4 Oct 2026 11:28:33 -0700 Subject: [PATCH 18/23] refactor --- util.ts | 1 + web/listDiagnoses/index.html | 5 +- web/listDiagnoses/index.ts | 22 ++- web/listDiagnoses/listDiagnoses.test.ts | 16 +- web/listIssues/index.html | 19 +- web/listIssues/index.ts | 250 ++++++++++-------------- web/listIssues/listIssues.test.ts | 4 +- web/listReports/index.ts | 57 ++---- web/listViolators/index.html | 5 +- web/listViolators/index.ts | 30 +-- web/listViolators/listViolators.test.ts | 16 +- web/reportDeletion.ts | 12 +- web/util.test.ts | 77 +++++--- web/util.ts | 82 ++++---- 14 files changed, 292 insertions(+), 304 deletions(-) diff --git a/util.ts b/util.ts index 246f3056..15d18b95 100644 --- a/util.ts +++ b/util.ts @@ -122,6 +122,7 @@ export type AnnotatedAct = Act & { // and the else/after-return branch is narrowed to UsableReport. export type UsableReport = Omit & { error?: never; + id: string; target: {what: string; url: string}; catalog: Catalog; jobData: NonNullable & {endTime: string; issuelessRules?: string[]}; diff --git a/web/listDiagnoses/index.html b/web/listDiagnoses/index.html index 0991b2fd..12e54407 100644 --- a/web/listDiagnoses/index.html +++ b/web/listDiagnoses/index.html @@ -15,10 +15,7 @@

    Kilotest: Diagnoses of __issue__ violation by HTML element __catalogIndex__ on __target__ page

    Basics

    About the __target__ page

    -
      -
    • URL: __urlLink__
    • -
    • __testInfo__
    • -
    +__pageFacts__

    About HTML element __catalogIndex__

    • Tag name: __tagName__
    • diff --git a/web/listDiagnoses/index.ts b/web/listDiagnoses/index.ts index 25cf7c75..5d97fcdc 100644 --- a/web/listDiagnoses/index.ts +++ b/web/listDiagnoses/index.ts @@ -15,6 +15,8 @@ import { } from '../../util.ts'; import { getPageDataStrings, + getPageFactsLines, + getReportData, getTextFragmentHref, getWCAGLink, getWeightName @@ -32,25 +34,25 @@ const populateQuery = async ( pathID: string | null, query: Record ) => { - // Get descriptions of the page facts. - const pageDataStrings = await getPageDataStrings(timeStamp, jobID); + // Get the report. + const report = await getReport(timeStamp, jobID); // If this failed: - if (pageDataStrings.error !== undefined) { + if (isReportError(report)) { // Populate the query with the reason. - query.error = pageDataStrings.error; + query.error = report.error; // Stop populating the query. return; } - const {testInfo, url, urlLink, description} = pageDataStrings; - // Otherwise, i.e. if it succeeded, get the report. - const report = await getReport(timeStamp, jobID); + // Otherwise, i.e. if it succeeded, get descriptions of the page facts. + const pageDataStrings = getPageDataStrings(report); // If this failed: - if (isReportError(report)) { + if (pageDataStrings.error !== undefined) { // Populate the query with the reason. - query.error = report.error; + query.error = pageDataStrings.error; // Stop populating the query. return; } + const {testInfo, url, description} = pageDataStrings; const {catalog} = report; // Otherwise, i.e. if it succeeded, get the catalog item of the specified violator. const catalogItem = catalog[catalogIndex]; @@ -72,8 +74,8 @@ const populateQuery = async ( } // Add facts about the issue to the query. query.target = description; - query.urlLink = urlLink; query.testInfo = testInfo; + query.pageFacts = getPageFactsLines(pageDataStrings, getReportData(report), margin).join('\n'); query.issue = issueSpecs[issueID]?.summary; // If adding the issue summary failed: if (!query.issue) { diff --git a/web/listDiagnoses/listDiagnoses.test.ts b/web/listDiagnoses/listDiagnoses.test.ts index f68409a1..7529cfd8 100644 --- a/web/listDiagnoses/listDiagnoses.test.ts +++ b/web/listDiagnoses/listDiagnoses.test.ts @@ -33,7 +33,7 @@ mock.module('../../util.ts', { mock.module('../util.ts', { exports: { ...realWebUtil, - getPageDataStrings: async (...args: any[]) => { + getPageDataStrings: (...args: any[]) => { if (pageDataStringsOverride !== null) { return pageDataStringsOverride; } @@ -108,7 +108,7 @@ test('listDiagnoses returns ok with no diagnoses for a nonexistent catalog index assert.ok(!diagnosisTexts.some(t => t.includes('The link does not have an accessible name'))); }); -test('listDiagnoses returns an error when getReport fails after getPageDataStrings succeeds', async () => { +test('listDiagnoses returns an error when getReport fails', async () => { getReportCallCount = 0; failGetReportOnCall = 1; try { @@ -121,6 +121,18 @@ test('listDiagnoses returns an error when getReport fails after getPageDataStrin } }); +test('listDiagnoses returns an error when getPageDataStrings fails after getReport succeeds', async () => { + pageDataStringsOverride = {error: 'Page data strings error'}; + try { + const result = await answer('linkNoText/260101T0000/mix/0'); + assert.equal(result.status, 'error'); + assert.equal(result.message, 'Page data strings error'); + } + finally { + pageDataStringsOverride = null; + } +}); + test('listDiagnoses includes a take-me-there link for a text-linkable catalog item', async () => { const dbDir = (await import('../../test/dbFixture.ts')).fixtureDBDir; const reportsDir = path.join(dbDir, 'reports'); diff --git a/web/listIssues/index.html b/web/listIssues/index.html index b3b69826..b35efb62 100644 --- a/web/listIssues/index.html +++ b/web/listIssues/index.html @@ -14,24 +14,7 @@

      Kilotest: Issues reported for __target__

      About the page

      -
        -
      • URL: __urlLink__
      • -
      • __testInfo__
      • -
      • Summary of results: -
          -
        • Rule engines: -
            -
          • Called: __engineCount__
          • -
          • Were able to test: __testedCount__
          • -
          • Reported any rule violations: __reporterCount__
          • -
          -
        • -
        • Violations: __violationCount__
        • -
        • Violators: __violatorCount__
        • -
        • Issues: __issueCount__
        • -
        -
      • -
      +__pageFacts__

      Should Kilotest retest the page?

      Issues reported

      diff --git a/web/listIssues/index.ts b/web/listIssues/index.ts index 9bc01272..5b6107f6 100644 --- a/web/listIssues/index.ts +++ b/web/listIssues/index.ts @@ -8,156 +8,132 @@ import { getReport, getTestActInstances, - isUsableReport, + isReportError, objectSort, populateTemplate } from '../../util.ts'; +import type {UsableReport} from '../../util.ts'; import { - getPageData, getPageDataStrings, + getPageFactsLines, getEngineIDs, getEngineNamesString, getWCAGLink, getWeightName } from '../util.ts'; +import type {ResultsSummary} from '../util.ts'; import {issues as issueSpecs} from 'testaro-issues'; // FUNCTIONS // Returns data on the issues reported by a report. -const getIssuesData = async (timeStamp: string, jobID: string) => { - // Get the report. - const report = await getReport(timeStamp, jobID); - // If it exists and is valid: - if (isUsableReport(report)) { - // Initialize the temporary data. - const temp = { - issues: {} as Record, - reporters: new Set(), - violators: new Set() - }; - // Initialize the final data. - const final: Record = { - reporters: [], - reporterList: '', - reporterCount: 0, - engineCount: 0, - testedCount: 0, - violationCount: 0, - violatorCount: 0, - issues: { - 4: [], - 3: [], - 2: [], - 1: [] - }, - issueCount: 0 - }; - // For each violating standard instance of each test act: - getTestActInstances(report, {violationsOnly: true}).forEach(({act, instance}) => { - const {catalogIndex, issueID} = instance; - const which = act.which!; - // If it identifies a non-ignorable issue: - if (issueID && issueID !== 'ignorable') { - const issueClassification = issueSpecs[issueID]; - // If the issue has a current weighted classification: - if (issueClassification && [1, 2, 3, 4].includes(issueClassification.weight)) { - const {summary, wcag, weight, why} = issueClassification; - // Initialize the temporary data on the issue if necessary. - temp.issues[issueID] ??= { - issueID, - summary, - wcag, - why, - weight, - reporters: new Set(), - reporterList: '', - violationCount: 0, - violators: new Set() - }; - // Ensure the rule engine is in the temporary data. - temp.issues[issueID].reporters.add(which); - temp.reporters.add(which); - // Increment the violation counts. - temp.issues[issueID].violationCount++; - final.violationCount++; - // If the instance has a catalog index: - if (catalogIndex) { - // Ensure the violator is in the temporary data. - temp.issues[issueID].violators.add(catalogIndex); - temp.violators.add(String(catalogIndex)); - } +const getIssuesData = (report: UsableReport): ResultsSummary & Record => { + // Initialize the temporary data. + const temp = { + issues: {} as Record, + reporters: new Set(), + violators: new Set() + }; + // Initialize the final data. + const final: ResultsSummary & Record = { + reporters: [], + reporterList: '', + reporterCount: 0, + engineCount: 0, + testedEngineCount: 0, + violationCount: 0, + violatorCount: 0, + issues: { + 4: [], + 3: [], + 2: [], + 1: [] + }, + issueCount: 0 + }; + // For each violating standard instance of each test act: + getTestActInstances(report, {violationsOnly: true}).forEach(({act, instance}) => { + const {catalogIndex, issueID} = instance; + const which = act.which!; + // If it identifies a non-ignorable issue: + if (issueID && issueID !== 'ignorable') { + const issueClassification = issueSpecs[issueID]; + // If the issue has a current weighted classification: + if (issueClassification && [1, 2, 3, 4].includes(issueClassification.weight)) { + const {summary, wcag, weight, why} = issueClassification; + // Initialize the temporary data on the issue if necessary. + temp.issues[issueID] ??= { + issueID, + summary, + wcag, + why, + weight, + reporters: new Set(), + reporterList: '', + violationCount: 0, + violators: new Set() + }; + // Ensure the rule engine is in the temporary data. + temp.issues[issueID].reporters.add(which); + temp.reporters.add(which); + // Increment the violation counts. + temp.issues[issueID].violationCount++; + final.violationCount++; + // If the instance has a catalog index: + if (catalogIndex) { + // Ensure the violator is in the temporary data. + temp.issues[issueID].violators.add(catalogIndex); + temp.violators.add(String(catalogIndex)); } } - }); - // Finish populating the final data. - const {calledIDs, preventedIDs} = getEngineIDs(report); - final.engineCount = calledIDs.length; - final.testedCount = calledIDs.length - preventedIDs.length; - final.reporterList = getEngineNamesString(temp.reporters); - final.reporterCount = temp.reporters.size; - final.violatorCount = temp.violators.size; - Object.values(temp.issues).forEach(issue => { - const {issueID, summary, wcag, why, weight} = issue; - const finalIssue: Record = { - issueID, - summary, - wcag, - why, - weight - }; - finalIssue.reporterList = getEngineNamesString(issue.reporters); - finalIssue.reporterCount = issue.reporters.size; - finalIssue.violationCount = issue.violationCount; - finalIssue.violatorCount = issue.violators.size; - final.issues[issue.weight].push(finalIssue); - }); - final.issueCount = Object.keys(temp.issues).length; - // For each weight: - [4, 3, 2, 1].forEach(weight => { - // Sort its issues in the final data alphabetically by reporter names. - objectSort(final.issues[weight], 'reporterList', 'alpha'); - // Sort the issues again in descending reporter-count order, making this the primary order. - objectSort(final.issues[weight], 'reporterCount', 'numericDown'); - }); - // Return the data. - return final; - } - // Otherwise, i.e. if it is invalid or does not exist, return this. - return {error: 'Report missing or invalid.'}; -}; -// Get page and issues data from a report. -const getData = async (timeStamp: string, jobID: string) => { - const pageData = await getPageData(timeStamp, jobID); - const issuesData: any = await getIssuesData(timeStamp, jobID); - const pageError = ('error' in pageData ? pageData.error : ''); - const issuesError = issuesData.error || ''; - const errors = [pageError, issuesError].filter(Boolean).join('; '); - // If the data of either type are missing or invalid: - if (errors) { - // Return this. - return {error: errors}; - } - // Otherwise, return the data. - return { - pageData, - issuesData - }; + } + }); + // Finish populating the final data. + const {calledIDs, preventedIDs} = getEngineIDs(report); + final.engineCount = calledIDs.length; + final.testedEngineCount = calledIDs.length - preventedIDs.length; + final.reporterList = getEngineNamesString(temp.reporters); + final.reporterCount = temp.reporters.size; + final.violatorCount = temp.violators.size; + Object.values(temp.issues).forEach(issue => { + const {issueID, summary, wcag, why, weight} = issue; + const finalIssue: Record = { + issueID, + summary, + wcag, + why, + weight + }; + finalIssue.reporterList = getEngineNamesString(issue.reporters); + finalIssue.reporterCount = issue.reporters.size; + finalIssue.violationCount = issue.violationCount; + finalIssue.violatorCount = issue.violators.size; + final.issues[issue.weight].push(finalIssue); + }); + final.issueCount = Object.keys(temp.issues).length; + // For each weight: + [4, 3, 2, 1].forEach(weight => { + // Sort its issues in the final data alphabetically by reporter names. + objectSort(final.issues[weight], 'reporterList', 'alpha'); + // Sort the issues again in descending reporter-count order, making this the primary order. + objectSort(final.issues[weight], 'reporterCount', 'numericDown'); + }); + // Return the data. + return final; }; // Adds parameters to a query for the answer page. const populateQuery = async (timeStamp: string, jobID: string, query: Record) => { - // Get data on the target and its issues according to the report. - const data: any = await getData(timeStamp, jobID); - const {pageData, issuesData} = data; + // Get the report. + const report = await getReport(timeStamp, jobID); // If this failed: - if (data.error) { + if (isReportError(report)) { // Populate the query with the reason. - query.error = data.error; + query.error = report.error; // Stop populating the query. return; } // Otherwise, i.e. if it succeeded, get fact descriptions for the target. - const pageInfo = await getPageDataStrings(timeStamp, jobID, pageData); + const pageInfo = getPageDataStrings(report); // If this failed: if (pageInfo.error !== undefined) { // Populate the query with the reason. @@ -165,28 +141,16 @@ const populateQuery = async (timeStamp: string, jobID: string, query: Record { + getPageDataStrings: (...args: any[]) => { if (pageDataStringsOverride !== null) { return pageDataStringsOverride; } @@ -127,7 +127,7 @@ test('listIssues counts an unknown prevented engine as called but not able to te } }); -test('listIssues returns an error when getPageDataStrings fails after getData succeeds', async () => { +test('listIssues returns an error when getPageDataStrings fails after getReport succeeds', async () => { pageDataStringsOverride = {error: 'Page data strings error'}; try { const result = await answer('260101T0000/mix'); diff --git a/web/listReports/index.ts b/web/listReports/index.ts index 9dd0bfbf..63eea9a6 100644 --- a/web/listReports/index.ts +++ b/web/listReports/index.ts @@ -6,11 +6,12 @@ // IMPORTS import { - getAgoDays, getJobNames, getObject, - getTestRequests, + getReport, getReportExtracts, + getTestRequests, + isReportError, jobsPath, objectSort, populateTemplate @@ -18,6 +19,7 @@ import { import { getMultiReportWhats, getPageDataStrings, + getPageFactsLines, getReportData } from '../util.ts'; import path from 'node:path'; @@ -102,52 +104,27 @@ const populateQuery = async (query: Record) => { // For each report: for (const extract of sortedExtracts) { const {jobID, timeStamp, url, description, superseded} = extract; - // Get data about it. - const reportData = await getReportData(timeStamp, jobID); + // Get the report. + const report = await getReport(timeStamp, jobID); // If this failed: - if (reportData.error !== undefined) { - console.error(reportData.error); + if (isReportError(report)) { + console.error(report.error); // Populate the query with the reason. - query.error = reportData.error; + query.error = report.error; // Stop populating the query. return; } - const { - engineCount, - issueCount, - reporterCount, - testedEngineCount, - violationCount, - violatorCount - } = reportData; - // Otherwise, i.e. if it succeeded, add lines about the report. + // Otherwise, i.e. if it succeeded, get data about the report. + const reportData = getReportData(report); + const pageDataStrings = getPageDataStrings(report); + const {issueCount} = reportData; + const {testInfo} = pageDataStrings; + // Add lines about the report. lines.tested.push(`${margin}
      `); - const daysAgo = getAgoDays(timeStamp); - const pageDataStrings = await getPageDataStrings(timeStamp, jobID, {description, url, daysAgo}); - const {urlLink, testInfo}: any = pageDataStrings; const testText = multiReportWhats.includes(description) ? ` (${testInfo.toLowerCase()})` : ''; lines.tested.push(`${margin} ${description}${testText}`); - lines.tested.push(`${margin}
        `); - // Add the URL of the target to the lines. - lines.tested.push(`${margin}
      • URL: ${urlLink}
      • `); - // Add facts about the report to the lines. - lines.tested.push(`${margin}
      • ${testInfo}
      • `); - // Add facts about the test results to the lines. - lines.tested.push(`${margin}
      • Summary of results:`); - lines.tested.push(`${margin}
          `); - lines.tested.push(`${margin}
        • Rule engines:`); - lines.tested.push(`${margin}
            `); - lines.tested.push(`${margin}
          • Called: ${engineCount}
          • `); - lines.tested.push(`${margin}
          • Were able to test: ${testedEngineCount}
          • `); - lines.tested.push(`${margin}
          • Reported any rule violations: ${reporterCount}
          • `); - lines.tested.push(`${margin}
          `); - lines.tested.push(`${margin}
        • `); - lines.tested.push(`${margin}
        • Violations: ${violationCount}
        • `); - lines.tested.push(`${margin}
        • Violators: ${violatorCount}
        • `); - lines.tested.push(`${margin}
        • Issues: ${issueCount}
        • `); - lines.tested.push(`${margin}
        `); - lines.tested.push(`${margin}
      • `); - lines.tested.push(`${margin}
      `); + // Add the page facts to the lines. + lines.tested.push(...getPageFactsLines(pageDataStrings, reportData, `${margin} `)); lines.tested.push(`${margin}