@jrpool/kilotest 58.2.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.
- package/package.json +1 -1
- package/util.test.ts +51 -1
- package/util.ts +45 -7
- package/web/listReports/index.ts +1 -1
- package/web/util.test.ts +10 -0
- package/web/util.ts +6 -4
package/package.json
CHANGED
package/util.test.ts
CHANGED
|
@@ -1276,7 +1276,7 @@ test('getIssue returns null for a known engine with an unknown variable rule', (
|
|
|
1276
1276
|
});
|
|
1277
1277
|
|
|
1278
1278
|
test('getIssue returns an issue ID for a variable rule pattern match', () => {
|
|
1279
|
-
const result = getIssue('nuVal', 'Duplicate attribute foo');
|
|
1279
|
+
const result = getIssue('nuVal', 'Duplicate attribute foo.');
|
|
1280
1280
|
assert.ok(typeof result === 'string');
|
|
1281
1281
|
assert.equal(result, 'duplicateAttribute');
|
|
1282
1282
|
});
|
|
@@ -1451,6 +1451,56 @@ test('getReportExtracts with onlyLatest returns only the latest report for each
|
|
|
1451
1451
|
assert.equal(mixReports[0]!.timeStamp, '260202T0000');
|
|
1452
1452
|
});
|
|
1453
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
|
+
|
|
1454
1504
|
test('getExclusionCookieValue is deterministic for a given AUTH_CODE', async () => {
|
|
1455
1505
|
const savedAuthCode = process.env.AUTH_CODE;
|
|
1456
1506
|
process.env.AUTH_CODE = 'test-auth-code';
|
package/util.ts
CHANGED
|
@@ -793,20 +793,58 @@ export const getReportExtract = async (timeStamp: string, jobID: string): Promis
|
|
|
793
793
|
};
|
|
794
794
|
}
|
|
795
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}>();
|
|
796
802
|
// Returns extracts of all available reports.
|
|
797
803
|
export const getReportExtracts = async (onlyLatest: boolean = false): Promise<ReportExtract[]> => {
|
|
798
|
-
|
|
799
|
-
|
|
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
|
+
);
|
|
800
821
|
// Initialize an array of extracts.
|
|
801
822
|
const extracts: ReportExtract[] = [];
|
|
802
|
-
// For each
|
|
803
|
-
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.
|
|
804
836
|
const [timeStamp, jobID] = reportFileName.slice(0, -5).split('-') as [string, string];
|
|
805
|
-
// Get an extract of it.
|
|
806
837
|
const extract = await getReportExtract(timeStamp, jobID);
|
|
838
|
+
// If this succeeded:
|
|
807
839
|
if (!('error' in extract)) {
|
|
808
|
-
//
|
|
809
|
-
|
|
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);
|
|
810
848
|
}
|
|
811
849
|
}
|
|
812
850
|
// Sort the extracts by page description and, secondarily, completion time.
|
package/web/listReports/index.ts
CHANGED
|
@@ -97,7 +97,7 @@ const populateQuery = async (query: Record<string, any>) => {
|
|
|
97
97
|
query.some = (reportCount || jobFileNames.queue.length || jobFileNames.claimed.length)
|
|
98
98
|
? 'another'
|
|
99
99
|
: 'a';
|
|
100
|
-
const multiReportWhats = await getMultiReportWhats();
|
|
100
|
+
const multiReportWhats = await getMultiReportWhats(reportExtracts);
|
|
101
101
|
// Sort them primarily by page description and secondarily by completion time.
|
|
102
102
|
let sortedExtracts = objectSort(reportExtracts, 'reportTime', 'alpha');
|
|
103
103
|
sortedExtracts = objectSort(sortedExtracts, 'description', 'alpha');
|
package/web/util.test.ts
CHANGED
|
@@ -293,6 +293,16 @@ test('getMultiReportWhats returns descriptions that have multiple reports', asyn
|
|
|
293
293
|
assert.ok(whats.includes('Mixed Outcomes Page'));
|
|
294
294
|
});
|
|
295
295
|
|
|
296
|
+
test('getMultiReportWhats uses extracts provided by the caller', async () => {
|
|
297
|
+
const extract = {timeStamp: 't', jobID: 'j', url: 'https://example.com/', reportTime: ''};
|
|
298
|
+
const whats = await getMultiReportWhats([
|
|
299
|
+
{...extract, description: 'B'},
|
|
300
|
+
{...extract, description: 'A'},
|
|
301
|
+
{...extract, description: 'B'}
|
|
302
|
+
]);
|
|
303
|
+
assert.deepEqual(whats, ['B']);
|
|
304
|
+
});
|
|
305
|
+
|
|
296
306
|
// Returns a minimal report for testing rule-engine identification.
|
|
297
307
|
const makeEngineReport = (whiches: string[], preventions: Record<string, string>) => ({
|
|
298
308
|
acts: whiches.map(which => ({type: 'test', which})),
|
package/web/util.ts
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
} from '../util.ts';
|
|
22
22
|
import {issues as issueSpecs} from 'testaro-issues';
|
|
23
23
|
import type {Catalog} from 'testaro';
|
|
24
|
-
import type {UsableReport} from '../util.ts';
|
|
24
|
+
import type {ReportExtract, UsableReport} from '../util.ts';
|
|
25
25
|
/* c8 ignore stop */
|
|
26
26
|
import wcagMap from '../wcagMap.json' with {type: 'json'};
|
|
27
27
|
|
|
@@ -372,9 +372,11 @@ export const getViolatorFactsLines = (
|
|
|
372
372
|
lines.push(`${margin}</ul>`);
|
|
373
373
|
return lines;
|
|
374
374
|
};
|
|
375
|
-
// Gets the descriptions of multi-report pages.
|
|
376
|
-
export const getMultiReportWhats = async (
|
|
377
|
-
|
|
375
|
+
// Gets the descriptions of multi-report pages, from extracts already obtained, if provided.
|
|
376
|
+
export const getMultiReportWhats = async (
|
|
377
|
+
reportExtracts?: ReportExtract[]
|
|
378
|
+
): Promise<string[]> => {
|
|
379
|
+
reportExtracts ??= await getReportExtracts();
|
|
378
380
|
const sortedDescriptions = reportExtracts.map(extract => extract.description).sort();
|
|
379
381
|
const multiReportDescriptions = sortedDescriptions.filter(
|
|
380
382
|
(description, index) => description !== sortedDescriptions[index - 1] && description === sortedDescriptions[index + 1]
|