@jrpool/kilotest 57.2.3 → 58.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.
@@ -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.2.3",
3
+ "version": "58.2.0",
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'});
@@ -1682,6 +1714,20 @@ test('orderJob returns a reportExists error when a report already exists for the
1682
1714
  assert.equal(result, 'reportExists');
1683
1715
  });
1684
1716
 
1717
+ test('orderJob returns a reportExists error when a report exists about a page with the same description', async () => {
1718
+ const {result} = await orderJob(
1719
+ {description: 'Mixed Outcomes Page', url: 'https://example.com/drifted'}, 'because accessibility matters here'
1720
+ );
1721
+ assert.equal(result, 'reportExists');
1722
+ });
1723
+
1724
+ test('orderJob returns a reportExists error when a report exists about a page with the same URL', async () => {
1725
+ const {result} = await orderJob(
1726
+ {description: 'Drifted Page', url: 'https://example.com/mixed'}, 'because accessibility matters here'
1727
+ );
1728
+ assert.equal(result, 'reportExists');
1729
+ });
1730
+
1685
1731
  test('orderJob returns "description" for a page matching a claimed job by description', async () => {
1686
1732
  const claimedPath = path.join(jobsPath(), 'claimed', 'clm.json');
1687
1733
  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[]};
@@ -841,8 +842,9 @@ const getApprovedJobProperty = async (
841
842
  // Return this.
842
843
  return 'description';
843
844
  }
844
- // Otherwise, if a job with the URL is in the category:
845
- if (jobsData.some(job => job.url === url)) {
845
+ // Otherwise, if a job with an equivalent URL is in the category:
846
+ const miniURL = minifyURL(url);
847
+ if (jobsData.some(job => minifyURL(job.url) === miniURL)) {
846
848
  // Return this.
847
849
  return 'url';
848
850
  }
@@ -951,13 +953,11 @@ export const processTestRequest = (
951
953
  // Return this.
952
954
  return {result: 'superseded', description, url};
953
955
  }
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
- }
956
+ // Otherwise, if the request is to test a new page and a report already exists about a
957
+ // page with the same description or URL:
958
+ if (requestType === 'newTest' && await isReportAvailable(description, url)) {
959
+ // Return this.
960
+ return {result: 'reportExists', description, url};
961
961
  }
962
962
  // Otherwise, i.e. if the request is genuinely new, if the queue of requests awaiting
963
963
  // approval is already full (a cap of 0 means no limit, so the queue is never full):
@@ -1070,13 +1070,11 @@ export const orderJob = (
1070
1070
  // Return this.
1071
1071
  return {result: 'superseded', description, url};
1072
1072
  }
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
- }
1073
+ // Otherwise, if the order is to test a new page and a report already exists about a
1074
+ // page with the same description or URL:
1075
+ if (!('timeStamp' in target) && await isReportAvailable(description, url)) {
1076
+ // Return this.
1077
+ return {result: 'reportExists', description, url};
1080
1078
  }
1081
1079
  // Otherwise, if the job queue is already full (a cap of 0 means no limit, so the
1082
1080
  // 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">