@jrpool/kilotest 57.3.0 → 58.2.4

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.
@@ -121,7 +121,7 @@ test('orderNewTest rejects an already-tested page', async () => {
121
121
  const disposition = body['response content']['disposition of your order'] as any;
122
122
  assert.ok(
123
123
  disposition['what happens next']
124
- .includes('a report about a page with the same description and URL is available.')
124
+ .includes('a report about a page with the same description or URL is available.')
125
125
  );
126
126
  assert.equal(disposition['report identifier'], null);
127
127
  });
@@ -100,7 +100,7 @@ export const response = async (args: string[]) => {
100
100
  requestDisposition = {
101
101
  'what happens next': `Your order will not be processed, because ${getRequestFailureReason(
102
102
  orderResult.result,
103
- {result: 'reportExists', reason: 'a report about a page with the same description and URL is available.'}
103
+ {result: 'reportExists', reason: 'a report about a page with the same description or URL is available.'}
104
104
  )}`,
105
105
  'report identifier': null,
106
106
  'how you can check for completion': 'Not applicable.',
@@ -118,7 +118,7 @@ test('requestNewTest rejects an already-tested page', async () => {
118
118
  const disposition = body['response content']['disposition of your request'] as any;
119
119
  assert.ok(
120
120
  disposition['what happens next']
121
- .includes('a report about a page with the same description and URL is available.')
121
+ .includes('a report about a page with the same description or URL is available.')
122
122
  );
123
123
  assert.ok(!logged.some(line => line.includes('request for a new test awaits approval')));
124
124
  });
@@ -84,7 +84,7 @@ export const response = async (args: string[]) => {
84
84
  howToCheck: 'You can call the listReports tool to learn whether the page has been tested and a report is available.',
85
85
  howWebUserChecks: `A web user can visit ${thisHost}/listReports.html to learn whether the page has been tested and a report is available.`
86
86
  },
87
- {result: 'reportExists', reason: 'a report about a page with the same description and URL is available.'}
87
+ {result: 'reportExists', reason: 'a report about a page with the same description or URL is available.'}
88
88
  );
89
89
  }
90
90
  // Create the response content.
package/index.test.ts CHANGED
@@ -476,7 +476,7 @@ test('POST /requestNewTest.html with an already-tested URL returns an error', as
476
476
  why: 'Because accessibility matters'
477
477
  });
478
478
  assert.equal(res.statusCode, 400);
479
- assert.ok(res.body.includes('already been tested'));
479
+ assert.ok(res.body.includes('same description or URL is already available'));
480
480
  });
481
481
 
482
482
  test('POST /requestNewTest.html with invalid data returns an error', async () => {
package/index.ts CHANGED
@@ -23,7 +23,6 @@ import {
23
23
  isReportError,
24
24
  getReportPath,
25
25
  hiddenReportsPath,
26
- isReportAvailable,
27
26
  isTimeStamp,
28
27
  isJobID,
29
28
  isURL,
@@ -949,27 +948,19 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse)
949
948
  const {description, url, why} = postData as {description?: string; url: string; why?: string};
950
949
  // If the request is valid:
951
950
  if (description && isURL(url) && why && await isAllowedTarget(url)) {
952
- // If a report on the page is already available:
953
- if (await isReportAvailable(description, url)) {
954
- // Report the error.
955
- await serveError({message: 'ERROR: Page has already been tested'}, response, true);
951
+ // Serve headers for a response.
952
+ setHeaders('text/html', pathname, 'ultra');
953
+ // Get the answer data.
954
+ const answerData = await answer.requestNewTest(description, url, why);
955
+ // If they are valid:
956
+ if (answerData.status === 'ok') {
957
+ // Serve the answer page.
958
+ response.end(answerData.answerPage);
956
959
  }
957
- // Otherwise, i.e. if no report on the page is available:
960
+ // Otherwise, i.e. if they are invalid:
958
961
  else {
959
- // Serve headers for a response.
960
- setHeaders('text/html', pathname, 'ultra');
961
- // Get the answer data.
962
- const answerData = await answer.requestNewTest(description, url, why);
963
- // If they are valid:
964
- if (answerData.status === 'ok') {
965
- // Serve the answer page.
966
- response.end(answerData.answerPage);
967
- }
968
- // Otherwise, i.e. if they are invalid:
969
- else {
970
- // Report the error.
971
- await serveError({message: answerData.message}, response, true);
972
- }
962
+ // Report the error.
963
+ await serveError({message: answerData.message}, response, true);
973
964
  }
974
965
  }
975
966
  // Otherwise, i.e. if the request is invalid:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jrpool/kilotest",
3
- "version": "57.3.0",
3
+ "version": "58.2.4",
4
4
  "description": "Ensemble testing for front-end web quality",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -203,11 +203,20 @@ const main = async () => {
203
203
  const preventedCatalog = {
204
204
  '0': catalogItem('IMG', 'An image', '/html/body/img[1]', '0:0:200:100')
205
205
  };
206
+ // As in real reports, the prevented engine has a test act, with no standard result.
207
+ const preventedAlfaAct = {
208
+ type: 'test',
209
+ which: engineAlfa,
210
+ startTime: '26-01-01T00:00',
211
+ data: {prevented: true, error: 'page timed out'},
212
+ endTime: '26-01-01T00:01'
213
+ };
206
214
  const preventedActs = [
207
215
  testAct(engineAxe, [
208
216
  instance('r11', 'The image has no alt text', 'failed',
209
217
  issueLinkNoText, 0, 2, 1)
210
- ])
218
+ ]),
219
+ preventedAlfaAct
211
220
  ];
212
221
  const preventedReport = report('260101T0006-prv', 'Prevented Page',
213
222
  'https://example.com/prevented', preventedActs, preventedCatalog);
@@ -42,6 +42,16 @@
42
42
  }
43
43
  }
44
44
  }
45
+ },
46
+ {
47
+ "type": "test",
48
+ "which": "alfa",
49
+ "startTime": "26-01-01T00:00",
50
+ "data": {
51
+ "prevented": true,
52
+ "error": "page timed out"
53
+ },
54
+ "endTime": "26-01-01T00:01"
45
55
  }
46
56
  ],
47
57
  "jobData": {
package/util.test.ts CHANGED
@@ -773,6 +773,21 @@ test('processTestRequest returns a reportExists error when a report already exis
773
773
  assert.equal(result, 'reportExists');
774
774
  });
775
775
 
776
+ test('processTestRequest returns a reportExists error when a report exists about a page with the same description', async () => {
777
+ const {result} = await processTestRequest(
778
+ 'because accessibility', {description: 'Mixed Outcomes Page', url: 'https://example.com/drifted'}
779
+ );
780
+ assert.equal(result, 'reportExists');
781
+ });
782
+
783
+ test('processTestRequest returns a reportExists error when a report exists about a page with an equivalent URL', async () => {
784
+ // The URL differs from that of the report only by a www prefix, a trailing slash, and case.
785
+ const {result} = await processTestRequest(
786
+ 'because accessibility', {description: 'Drifted Page', url: 'https://www.Example.com/mixed/'}
787
+ );
788
+ assert.equal(result, 'reportExists');
789
+ });
790
+
776
791
  test('processTestRequest returns a queueFull error once the pending-request queue is full', {timeout: 500}, async () => {
777
792
  // Fill testRequests.json with 20 pending requests, all for the same URL, so the total
778
793
  // pending count (summed across all URLs' request arrays) reaches the cap directly,
@@ -972,6 +987,23 @@ test('processTestRequest returns "url" for a page matching a queued job by URL',
972
987
  }
973
988
  });
974
989
 
990
+ test('processTestRequest returns "url" for a page matching a queued job by an equivalent URL', async () => {
991
+ const queuedPath = path.join(jobsPath(), 'queue', 'que.json');
992
+ await fs.writeFile(queuedPath, getJSON({
993
+ target: {what: 'Some Queued Page', url: 'https://example.com/queued-url'}
994
+ }));
995
+ try {
996
+ // The URL differs from that of the job only by a www prefix, a trailing slash, and case.
997
+ const {result} = await processTestRequest(
998
+ 'because', {description: 'A Different Page', url: 'https://www.Example.com/queued-url/'}
999
+ );
1000
+ assert.equal(result, 'url');
1001
+ }
1002
+ finally {
1003
+ await fs.unlink(queuedPath);
1004
+ }
1005
+ });
1006
+
975
1007
  test('processTestRequest returns "nonreport" when the cited report does not exist', async () => {
976
1008
  // Cite a timeStamp/jobID that does not correspond to any available report.
977
1009
  const {result} = await processTestRequest('because', {timeStamp: '999999T9999', jobID: 'xxx'});
@@ -1244,7 +1276,7 @@ test('getIssue returns null for a known engine with an unknown variable rule', (
1244
1276
  });
1245
1277
 
1246
1278
  test('getIssue returns an issue ID for a variable rule pattern match', () => {
1247
- const result = getIssue('nuVal', 'Duplicate attribute foo');
1279
+ const result = getIssue('nuVal', 'Duplicate attribute foo.');
1248
1280
  assert.ok(typeof result === 'string');
1249
1281
  assert.equal(result, 'duplicateAttribute');
1250
1282
  });
@@ -1419,6 +1451,56 @@ test('getReportExtracts with onlyLatest returns only the latest report for each
1419
1451
  assert.equal(mixReports[0]!.timeStamp, '260202T0000');
1420
1452
  });
1421
1453
 
1454
+ test('getReportExtracts reflects reports added, modified, and removed after a previous call', async () => {
1455
+ const os = await import('node:os');
1456
+ const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'kilotest-extracts-'));
1457
+ const tmpReportsDir = path.join(tmpRoot, 'db', 'reports');
1458
+ const savedDbDir = process.env.DB_DIR;
1459
+ const sourcePath = path.join(reportsPath(), '260101T0000-mix.json');
1460
+ process.env.DB_DIR = path.join(tmpRoot, 'db');
1461
+ try {
1462
+ await fs.mkdir(tmpReportsDir, {recursive: true});
1463
+ const report = JSON.parse(await fs.readFile(sourcePath, 'utf8'));
1464
+ const firstPath = path.join(tmpReportsDir, '260101T0000-aaa.json');
1465
+ await fs.writeFile(firstPath, JSON.stringify(report));
1466
+ let extracts = await getReportExtracts();
1467
+ assert.deepEqual(extracts.map(e => e.description), ['Mixed Outcomes Page']);
1468
+ // Modify the report and give it a later modification time, as reannotation would.
1469
+ report.target.what = 'Revised Page';
1470
+ await fs.writeFile(firstPath, JSON.stringify(report));
1471
+ const later = new Date(Date.now() + 60000);
1472
+ await fs.utimes(firstPath, later, later);
1473
+ extracts = await getReportExtracts();
1474
+ assert.deepEqual(extracts.map(e => e.description), ['Revised Page']);
1475
+ // Add a report.
1476
+ report.target.what = 'Added Page';
1477
+ const secondPath = path.join(tmpReportsDir, '260101T0001-bbb.json');
1478
+ await fs.writeFile(secondPath, JSON.stringify(report));
1479
+ extracts = await getReportExtracts();
1480
+ assert.deepEqual(extracts.map(e => e.description), ['Added Page', 'Revised Page']);
1481
+ // Mutating a returned extract does not affect later results.
1482
+ extracts[0]!.superseded = true;
1483
+ extracts = await getReportExtracts();
1484
+ assert.equal(extracts[0]!.superseded, undefined);
1485
+ // Make a cached report unreadable.
1486
+ await fs.writeFile(secondPath, 'not JSON');
1487
+ extracts = await getReportExtracts();
1488
+ assert.deepEqual(extracts.map(e => e.description), ['Revised Page']);
1489
+ // Restore it.
1490
+ await fs.writeFile(secondPath, JSON.stringify(report));
1491
+ extracts = await getReportExtracts();
1492
+ assert.deepEqual(extracts.map(e => e.description), ['Added Page', 'Revised Page']);
1493
+ // Remove a report, as hiding would.
1494
+ await fs.rm(firstPath);
1495
+ extracts = await getReportExtracts();
1496
+ assert.deepEqual(extracts.map(e => e.description), ['Added Page']);
1497
+ }
1498
+ finally {
1499
+ process.env.DB_DIR = savedDbDir;
1500
+ await fs.rm(tmpRoot, {recursive: true}).catch(() => {});
1501
+ }
1502
+ });
1503
+
1422
1504
  test('getExclusionCookieValue is deterministic for a given AUTH_CODE', async () => {
1423
1505
  const savedAuthCode = process.env.AUTH_CODE;
1424
1506
  process.env.AUTH_CODE = 'test-auth-code';
@@ -1682,6 +1764,20 @@ test('orderJob returns a reportExists error when a report already exists for the
1682
1764
  assert.equal(result, 'reportExists');
1683
1765
  });
1684
1766
 
1767
+ test('orderJob returns a reportExists error when a report exists about a page with the same description', async () => {
1768
+ const {result} = await orderJob(
1769
+ {description: 'Mixed Outcomes Page', url: 'https://example.com/drifted'}, 'because accessibility matters here'
1770
+ );
1771
+ assert.equal(result, 'reportExists');
1772
+ });
1773
+
1774
+ test('orderJob returns a reportExists error when a report exists about a page with the same URL', async () => {
1775
+ const {result} = await orderJob(
1776
+ {description: 'Drifted Page', url: 'https://example.com/mixed'}, 'because accessibility matters here'
1777
+ );
1778
+ assert.equal(result, 'reportExists');
1779
+ });
1780
+
1685
1781
  test('orderJob returns "description" for a page matching a claimed job by description', async () => {
1686
1782
  const claimedPath = path.join(jobsPath(), 'claimed', 'clm.json');
1687
1783
  await fs.writeFile(claimedPath, getJSON({
package/util.ts CHANGED
@@ -77,8 +77,8 @@ export type TestRequest = {
77
77
  // Test requests by URL.
78
78
  export type TestRequests = Record<string, TestRequest[]>;
79
79
 
80
- // Test request addition result. 'reportExists' means a report already exists for a
81
- // page a newTest request or order named (an outcome), distinct from the 'retest'
80
+ // Test request addition result. 'reportExists' means a report already exists about a
81
+ // page with the description or URL that a newTest request or order named (an outcome), distinct from the 'retest'
82
82
  // requestType discriminant elsewhere in this file, which means the caller asked to
83
83
  // retest a page that already has a report (a request kind); the two must not be
84
84
  // confused with one another.
@@ -122,6 +122,7 @@ export type AnnotatedAct = Act & {
122
122
  // and the else/after-return branch is narrowed to UsableReport.
123
123
  export type UsableReport = Omit<Report, 'target' | 'catalog' | 'jobData' | 'acts'> & {
124
124
  error?: never;
125
+ id: string;
125
126
  target: {what: string; url: string};
126
127
  catalog: Catalog;
127
128
  jobData: NonNullable<Report['jobData']> & {endTime: string; issuelessRules?: string[]};
@@ -792,20 +793,58 @@ export const getReportExtract = async (timeStamp: string, jobID: string): Promis
792
793
  };
793
794
  }
794
795
  };
796
+ // Cache of report extracts, keyed by report file path. Each entry records the modification
797
+ // time and size of the file when it was extracted, so an entry is reused only while the file
798
+ // is unchanged. Reports are mutable (e.g., reannotation rewrites all of them) and may be
799
+ // written, hidden, or deleted by any process, so the cache is validated against the files
800
+ // before every use rather than invalidated by the code that changes them.
801
+ const reportExtractCache = new Map<string, {mtimeMs: number, size: number, extract: ReportExtract}>();
795
802
  // Returns extracts of all available reports.
796
803
  export const getReportExtracts = async (onlyLatest: boolean = false): Promise<ReportExtract[]> => {
797
- // Get the names of the available report files.
798
- const reportFileNames = await readdirOrCreate(reportsPath(), 'Reports directory');
804
+ const reportsDir = reportsPath();
805
+ // Get the names and paths of the available report files.
806
+ const reportFileNames = await readdirOrCreate(reportsDir, 'Reports directory');
807
+ const reportFilePaths = reportFileNames.map(fileName => path.join(reportsDir, fileName));
808
+ const reportFilePathSet = new Set(reportFilePaths);
809
+ // Remove from the cache any report that is no longer available (e.g., hidden or deleted).
810
+ for (const cachedPath of reportExtractCache.keys()) {
811
+ if (!reportFilePathSet.has(cachedPath)) {
812
+ reportExtractCache.delete(cachedPath);
813
+ }
814
+ }
815
+ // Get the current modification times and sizes of the report files. A file that disappeared
816
+ // after the directory was read gets values that match no cache entry, so it is reread, which
817
+ // fails and removes it from the cache.
818
+ const reportStats = await Promise.all(
819
+ reportFilePaths.map(filePath => fs.stat(filePath).catch(() => ({mtimeMs: NaN, size: NaN})))
820
+ );
799
821
  // Initialize an array of extracts.
800
822
  const extracts: ReportExtract[] = [];
801
- // For each one:
802
- for (const reportFileName of reportFileNames) {
823
+ // For each report file:
824
+ for (const [index, reportFileName] of reportFileNames.entries()) {
825
+ const filePath = reportFilePaths[index]!;
826
+ const stat = reportStats[index]!;
827
+ const cached = reportExtractCache.get(filePath);
828
+ // If its cached extract is current:
829
+ if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
830
+ // Add a copy of the cached extract to the array.
831
+ extracts.push({...cached.extract});
832
+ continue;
833
+ }
834
+ // Otherwise, get a new extract of it. The stat precedes the read, so a change between
835
+ // them leaves a stale modification time in the cache and causes a reread next time.
803
836
  const [timeStamp, jobID] = reportFileName.slice(0, -5).split('-') as [string, string];
804
- // Get an extract of it.
805
837
  const extract = await getReportExtract(timeStamp, jobID);
838
+ // If this succeeded:
806
839
  if (!('error' in extract)) {
807
- // Add the extract to the array.
808
- extracts.push(extract);
840
+ // Cache it and add a copy of it to the array.
841
+ reportExtractCache.set(filePath, {mtimeMs: stat.mtimeMs, size: stat.size, extract});
842
+ extracts.push({...extract});
843
+ }
844
+ // Otherwise, i.e. if it failed:
845
+ else {
846
+ // Forget any outdated extract of it.
847
+ reportExtractCache.delete(filePath);
809
848
  }
810
849
  }
811
850
  // Sort the extracts by page description and, secondarily, completion time.
@@ -841,8 +880,9 @@ const getApprovedJobProperty = async (
841
880
  // Return this.
842
881
  return 'description';
843
882
  }
844
- // Otherwise, if a job with the URL is in the category:
845
- if (jobsData.some(job => job.url === url)) {
883
+ // Otherwise, if a job with an equivalent URL is in the category:
884
+ const miniURL = minifyURL(url);
885
+ if (jobsData.some(job => minifyURL(job.url) === miniURL)) {
846
886
  // Return this.
847
887
  return 'url';
848
888
  }
@@ -951,13 +991,11 @@ export const processTestRequest = (
951
991
  // Return this.
952
992
  return {result: 'superseded', description, url};
953
993
  }
954
- // Otherwise, if the request is to test a new page for which a report already exists:
955
- if (requestType === 'newTest') {
956
- const reportExtracts = await getReportExtracts();
957
- if (reportExtracts.some(report => report.description === description && report.url === url)) {
958
- // Return this.
959
- return {result: 'reportExists', description, url};
960
- }
994
+ // Otherwise, if the request is to test a new page and a report already exists about a
995
+ // page with the same description or URL:
996
+ if (requestType === 'newTest' && await isReportAvailable(description, url)) {
997
+ // Return this.
998
+ return {result: 'reportExists', description, url};
961
999
  }
962
1000
  // Otherwise, i.e. if the request is genuinely new, if the queue of requests awaiting
963
1001
  // approval is already full (a cap of 0 means no limit, so the queue is never full):
@@ -1070,13 +1108,11 @@ export const orderJob = (
1070
1108
  // Return this.
1071
1109
  return {result: 'superseded', description, url};
1072
1110
  }
1073
- // Otherwise, if the order is to test a new page for which a report already exists:
1074
- if (!('timeStamp' in target)) {
1075
- const reportExtracts = await getReportExtracts();
1076
- if (reportExtracts.some(report => report.description === description && report.url === url)) {
1077
- // Return this.
1078
- return {result: 'reportExists', description, url};
1079
- }
1111
+ // Otherwise, if the order is to test a new page and a report already exists about a
1112
+ // page with the same description or URL:
1113
+ if (!('timeStamp' in target) && await isReportAvailable(description, url)) {
1114
+ // Return this.
1115
+ return {result: 'reportExists', description, url};
1080
1116
  }
1081
1117
  // Otherwise, if the job queue is already full (a cap of 0 means no limit, so the
1082
1118
  // queue is never full):
@@ -14,28 +14,15 @@
14
14
  <main>
15
15
  <h1><a href="/">Kilotest</a>: Diagnoses of <q>__issue__</q> violation by HTML element __catalogIndex__ on __target__ page</h1>
16
16
  <h2>Basics</h2>
17
- <h3>About the __target__ page</h3>
18
- <ul>
19
- <li>URL: __urlLink__</li>
20
- <li>__testInfo__</li>
21
- </ul>
22
- <h3>About HTML element __catalogIndex__</h3>
23
- <ul>
24
- <li>Tag name: <code>__tagName__</code></li>
25
- <li>Text: __text__</li>
26
- <li>Start tag: <code>__startTag__</code></li>
27
- <li>XPath: <code>__pathID__</code></li>
28
- <li>Bounding box: __box__</li>
29
- </ul>
17
+ <h3>About the page</h3>
18
+ __pageFacts__
19
+ <h3>About the issue</h3>
20
+ __issueFacts__
21
+ <h3>About the violator</h3>
22
+ __violatorFacts__
30
23
  <ul class="nav">
31
24
  __takeMeThere__
32
25
  </ul>
33
- <h3>About the <q>__issue__</q> issue</h3>
34
- <ul>
35
- <li>Why it matters: __why__</li>
36
- <li>Priority: __priority__</li>
37
- <li>Related WCAG standard: __wcag__</li>
38
- </ul>
39
26
  <h2>Diagnoses</h2>
40
27
  <p>Here is how rule engines diagnose the <q>__issue__</q> issue for HTML element __catalogIndex__ of the __target__ page.</p>
41
28
  <ol>
@@ -14,10 +14,12 @@ import {
14
14
  ruleEngines
15
15
  } from '../../util.ts';
16
16
  import {
17
+ getIssueFactsLines,
17
18
  getPageDataStrings,
19
+ getPageFactsLines,
20
+ getReportData,
18
21
  getTextFragmentHref,
19
- getWCAGLink,
20
- getWeightName
22
+ getViolatorFactsLines
21
23
  } from '../util.ts';
22
24
  import {issues as issueSpecs} from 'testaro-issues';
23
25
 
@@ -32,37 +34,33 @@ const populateQuery = async (
32
34
  pathID: string | null,
33
35
  query: Record<string, any>
34
36
  ) => {
35
- // Get descriptions of the page facts.
36
- const pageDataStrings = await getPageDataStrings(timeStamp, jobID);
37
+ // Get the report.
38
+ const report = await getReport(timeStamp, jobID);
37
39
  // If this failed:
38
- if (pageDataStrings.error !== undefined) {
40
+ if (isReportError(report)) {
39
41
  // Populate the query with the reason.
40
- query.error = pageDataStrings.error;
42
+ query.error = report.error;
41
43
  // Stop populating the query.
42
44
  return;
43
45
  }
44
- const {testInfo, url, urlLink, description} = pageDataStrings;
45
- // Otherwise, i.e. if it succeeded, get the report.
46
- const report = await getReport(timeStamp, jobID);
46
+ // Otherwise, i.e. if it succeeded, get descriptions of the page facts.
47
+ const pageDataStrings = getPageDataStrings(report);
47
48
  // If this failed:
48
- if (isReportError(report)) {
49
+ if (pageDataStrings.error !== undefined) {
49
50
  // Populate the query with the reason.
50
- query.error = report.error;
51
+ query.error = pageDataStrings.error;
51
52
  // Stop populating the query.
52
53
  return;
53
54
  }
55
+ const {testInfo, url, description} = pageDataStrings;
54
56
  const {catalog} = report;
55
- // Otherwise, i.e. if it succeeded, get the catalog item of the specified violator.
56
- const catalogItem = catalog[catalogIndex];
57
- const boxID = catalogItem?.boxID;
58
- const startTag = catalogItem?.startTag;
59
- const tagName = catalogItem?.tagName;
60
- const text = catalogItem?.text;
61
57
  query.catalogIndex = catalogIndex;
62
58
  const lines: string[] = [];
63
59
  const margin = ' '.repeat(6);
60
+ const catalogItem = catalog[catalogIndex];
61
+ // If the violator has a linkable text item, add a take-me-there link.
64
62
  if (catalogIndex && catalogItem?.textLinkable) {
65
- const href = getTextFragmentHref(text as string, url);
63
+ const href = getTextFragmentHref(catalogItem.text as string, url);
66
64
  const label = `Take me to element ${catalogIndex} on the page (in a new tab)`;
67
65
  const link = `<a href="${href}" target="_blank" aria-label="${label}">Take me there</a>`;
68
66
  query.takeMeThere = `${margin} <p>${link}</p>`;
@@ -70,10 +68,10 @@ const populateQuery = async (
70
68
  else {
71
69
  query.takeMeThere = '';
72
70
  }
73
- // Add facts about the issue to the query.
71
+ // Add facts about the page to the query.
74
72
  query.target = description;
75
- query.urlLink = urlLink;
76
73
  query.testInfo = testInfo;
74
+ query.pageFacts = getPageFactsLines(pageDataStrings, getReportData(report), margin).join('\n');
77
75
  query.issue = issueSpecs[issueID]?.summary;
78
76
  // If adding the issue summary failed:
79
77
  if (!query.issue) {
@@ -82,29 +80,9 @@ const populateQuery = async (
82
80
  // Stop populating the query.
83
81
  return;
84
82
  }
85
- // Otherwise, i.e. if it succeeded, get the issue details.
86
- const issue = issueSpecs[issueID]!;
87
- const {wcag, weight, why} = issue;
88
- query.why = why;
89
- query.priority = getWeightName(weight);
90
- query.wcag = `<a href="${getWCAGLink(wcag)}">${wcag}</a>`;
91
- query.tagName = tagName || 'HTML';
92
- if (text && !['HTML', 'BODY', 'HEAD', 'SCRIPT', 'STYLE', 'NOSCRIPT'].includes(tagName as string)) {
93
- const textString = text.split('\n').join(' … ');
94
- query.text = `<q>${htmlSafe(textString)}</q>`;
95
- }
96
- else {
97
- query.text = '[not applicable]';
98
- }
99
- query.startTag = htmlSafe(startTag ?? '') || '[not obtained]';
100
- query.pathID = pathID || '[not obtained]';
101
- if (boxID) {
102
- const dims = boxID.split(':');
103
- query.box = `x = ${dims[0]}, y = ${dims[1]}, width = ${dims[2]}, height = ${dims[3]}`;
104
- }
105
- else {
106
- query.box = '[not obtained]';
107
- }
83
+ // Otherwise, i.e. if it succeeded, add the issue-facts and violator-facts lists to the query.
84
+ query.issueFacts = getIssueFactsLines(report, issueID, margin).join('\n');
85
+ query.violatorFacts = getViolatorFactsLines(report, issueID, catalogIndex, pathID, margin).join('\n');
108
86
  // Initialize an array of diagnoses.
109
87
  const diagnoses: any[] = [];
110
88
  // For each violating standard instance that pertains to this combination of issue and violator:
@@ -33,7 +33,7 @@ mock.module('../../util.ts', {
33
33
  mock.module('../util.ts', {
34
34
  exports: {
35
35
  ...realWebUtil,
36
- getPageDataStrings: async (...args: any[]) => {
36
+ getPageDataStrings: (...args: any[]) => {
37
37
  if (pageDataStringsOverride !== null) {
38
38
  return pageDataStringsOverride;
39
39
  }
@@ -108,7 +108,7 @@ test('listDiagnoses returns ok with no diagnoses for a nonexistent catalog index
108
108
  assert.ok(!diagnosisTexts.some(t => t.includes('The link does not have an accessible name')));
109
109
  });
110
110
 
111
- test('listDiagnoses returns an error when getReport fails after getPageDataStrings succeeds', async () => {
111
+ test('listDiagnoses returns an error when getReport fails', async () => {
112
112
  getReportCallCount = 0;
113
113
  failGetReportOnCall = 1;
114
114
  try {
@@ -121,6 +121,18 @@ test('listDiagnoses returns an error when getReport fails after getPageDataStrin
121
121
  }
122
122
  });
123
123
 
124
+ test('listDiagnoses returns an error when getPageDataStrings fails after getReport succeeds', async () => {
125
+ pageDataStringsOverride = {error: 'Page data strings error'};
126
+ try {
127
+ const result = await answer('linkNoText/260101T0000/mix/0');
128
+ assert.equal(result.status, 'error');
129
+ assert.equal(result.message, 'Page data strings error');
130
+ }
131
+ finally {
132
+ pageDataStringsOverride = null;
133
+ }
134
+ });
135
+
124
136
  test('listDiagnoses includes a take-me-there link for a text-linkable catalog item', async () => {
125
137
  const dbDir = (await import('../../test/dbFixture.ts')).fixtureDBDir;
126
138
  const reportsDir = path.join(dbDir, 'reports');
@@ -13,20 +13,20 @@
13
13
  <body>
14
14
  <main>
15
15
  <h1><a href="/">Kilotest</a>: Issues reported for __target__</h1>
16
- <h2>About the __target__ page</h2>
17
- <ul>
18
- <li>URL: __urlLink__</li>
19
- <li>__testInfo__</li>
20
- __preventions__
21
- </ul>
16
+
17
+ <h2>About the tests</h2>
18
+ <details>
19
+ <summary>How to deal with the issues</summary>
20
+ <h3>Do it yourself</h3>
21
+ <p>Explore the issues below in as much detail as you want. Decide which ones are actual defects. Plan and implement corrections.</p>
22
+ <h3>Or use AI</h3>
23
+ <p>Make Kilotest available to your AI assistant. There are <a href="/tutorialAI.html">step-by-step instructions</a> for this.</p>
24
+ <p>After that, you can ask:</p>
25
+ <blockquote>Kilotest reports defects on the __target__ web page. Summarize them and propose a plan to resolve the genuine ones.</blockquote>
26
+ </details>
27
+ __pageFacts__
22
28
  <p><a href="/requestRetestForm.html/__timeStamp__/__jobID__">Should Kilotest retest the page?</a></p>
23
29
  <h2>Issues reported</h2>
24
- <h3>Summary</h3>
25
- <p>__reporterCount__ reported issues (__reporters__)</p>
26
- <p>__issueCount__ reported (__highestCount__ highest, __highCount__ high, __lowCount__ low, __lowestCount__ lowest priority)</p>
27
- <p>__violatorCount__ reported</p>
28
- <p>Download the <a href="/fullReport.json/__timeStamp__/__jobID__">full technical report</a></p>
29
- <h3>Details</h3>
30
30
  <details>
31
31
  <summary>Highest priority: __highestCount__</summary>
32
32
  <ul class="headed">