@flighthq/tool-capture 0.3.0 → 0.3.1-next.1041.35b950c
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/README.md +1 -1
- package/dist/baselineStore.d.ts +12 -1
- package/dist/baselineStore.d.ts.map +1 -1
- package/dist/baselineStore.js +83 -6
- package/dist/baselineStore.js.map +1 -1
- package/dist/bin.js +80 -6
- package/dist/bin.js.map +1 -1
- package/dist/captureBaselineCoverageManifest.d.ts +80 -0
- package/dist/captureBaselineCoverageManifest.d.ts.map +1 -0
- package/dist/captureBaselineCoverageManifest.js +178 -0
- package/dist/captureBaselineCoverageManifest.js.map +1 -0
- package/dist/captureBaselineSanity.d.ts +9 -0
- package/dist/captureBaselineSanity.d.ts.map +1 -0
- package/dist/captureBaselineSanity.js +32 -0
- package/dist/captureBaselineSanity.js.map +1 -0
- package/dist/captureBenchmark.d.ts.map +1 -1
- package/dist/captureBenchmark.js +5 -1
- package/dist/captureBenchmark.js.map +1 -1
- package/dist/captureDomReadback.d.ts +4 -0
- package/dist/captureDomReadback.d.ts.map +1 -0
- package/dist/captureDomReadback.js +51 -0
- package/dist/captureDomReadback.js.map +1 -0
- package/dist/captureEntry.d.ts +12 -3
- package/dist/captureEntry.d.ts.map +1 -1
- package/dist/captureEntry.js +87 -36
- package/dist/captureEntry.js.map +1 -1
- package/dist/captureEntryFilter.d.ts +5 -0
- package/dist/captureEntryFilter.d.ts.map +1 -0
- package/dist/captureEntryFilter.js +23 -0
- package/dist/captureEntryFilter.js.map +1 -0
- package/dist/captureFlightPreset.js +10 -4
- package/dist/captureFlightPreset.js.map +1 -1
- package/dist/captureProtocol.d.ts +3 -0
- package/dist/captureProtocol.d.ts.map +1 -1
- package/dist/captureProtocol.js.map +1 -1
- package/dist/captureResourceFailure.d.ts +4 -0
- package/dist/captureResourceFailure.d.ts.map +1 -0
- package/dist/captureResourceFailure.js +31 -0
- package/dist/captureResourceFailure.js.map +1 -0
- package/dist/captureScreenshotHash.d.ts +30 -0
- package/dist/captureScreenshotHash.d.ts.map +1 -0
- package/dist/captureScreenshotHash.js +108 -0
- package/dist/captureScreenshotHash.js.map +1 -0
- package/dist/captureServer.d.ts +8 -0
- package/dist/captureServer.d.ts.map +1 -1
- package/dist/captureServer.js +126 -22
- package/dist/captureServer.js.map +1 -1
- package/dist/captureSuite.d.ts +11 -0
- package/dist/captureSuite.d.ts.map +1 -1
- package/dist/captureSuite.js +6 -4
- package/dist/captureSuite.js.map +1 -1
- package/dist/captureValidation.d.ts +20 -3
- package/dist/captureValidation.d.ts.map +1 -1
- package/dist/captureValidation.js +222 -70
- package/dist/captureValidation.js.map +1 -1
- package/dist/captureWorkflow.d.ts.map +1 -1
- package/dist/captureWorkflow.js +3 -0
- package/dist/captureWorkflow.js.map +1 -1
- package/dist/functionalScene3Ds.d.ts.map +1 -1
- package/dist/functionalScene3Ds.js +5 -3
- package/dist/functionalScene3Ds.js.map +1 -1
- package/dist/functionalVerify.d.ts.map +1 -1
- package/dist/functionalVerify.js +57 -16
- package/dist/functionalVerify.js.map +1 -1
- package/package.json +5 -5
- package/src/baselineStore.test.ts +210 -7
- package/src/bin.test.ts +32 -0
- package/src/captureBaselineCoverageManifest.test.ts +368 -0
- package/src/captureBaselineSanity.test.ts +38 -0
- package/src/captureBenchmark.e2e.test.ts +29 -1
- package/src/captureDomReadback.test.ts +35 -0
- package/src/captureEntry.e2e.test.ts +108 -0
- package/src/captureEntryFilter.test.ts +45 -0
- package/src/captureEyes.e2e.test.ts +43 -0
- package/src/captureFlightPreset.test.ts +10 -0
- package/src/capturePage.test.ts +23 -3
- package/src/captureResourceFailure.test.ts +60 -0
- package/src/captureScreenshotHash.test.ts +142 -0
- package/src/captureServer.test.ts +103 -3
- package/src/captureValidation.test.ts +217 -36
- package/src/functionalScene3Ds.test.ts +7 -0
- package/src/functionalVerify.test.ts +33 -3
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
baselinePath,
|
|
9
|
+
getBaselineField,
|
|
10
|
+
getBaselineProvenance,
|
|
11
|
+
setBaselineCaptureEvidence,
|
|
12
|
+
setBaselineField,
|
|
13
|
+
setBaselineProvenance,
|
|
14
|
+
} from './baselineStore';
|
|
8
15
|
|
|
9
16
|
let root: string;
|
|
10
17
|
|
|
@@ -38,17 +45,184 @@ describe('getBaselineField', () => {
|
|
|
38
45
|
});
|
|
39
46
|
});
|
|
40
47
|
|
|
48
|
+
describe('getBaselineProvenance', () => {
|
|
49
|
+
it('reads back what was recorded, and reads a legacy column as UNKNOWN', () => {
|
|
50
|
+
setBaselineField(root, 'functional', 'foo', 'canvas', 'sha256', 'abc123');
|
|
51
|
+
// Written before provenance existed: null, not a claim that it matches anything.
|
|
52
|
+
expect(getBaselineProvenance(root, 'functional', 'foo', 'canvas', 'sha256')).toBeNull();
|
|
53
|
+
|
|
54
|
+
setBaselineProvenance(root, 'functional', 'foo', 'canvas', 'sha256', PROVENANCE);
|
|
55
|
+
expect(getBaselineProvenance(root, 'functional', 'foo', 'canvas', 'sha256')).toEqual(PROVENANCE);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('setBaselineCaptureEvidence', () => {
|
|
60
|
+
it('writes fingerprint and sha256 atomically while preserving other columns', () => {
|
|
61
|
+
setBaselineField(root, 'functional', 'foo', 'webgl', 'sha256', 'webgl-hash');
|
|
62
|
+
|
|
63
|
+
setBaselineCaptureEvidence(root, 'functional', 'foo', 'canvas', {
|
|
64
|
+
fingerprint: '2:000000ffffff',
|
|
65
|
+
sourceHash: 'source',
|
|
66
|
+
sha256: 'hash',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(getBaselineField(root, 'functional', 'foo', 'canvas', 'fingerprint')).toBe('2:000000ffffff');
|
|
70
|
+
expect(getBaselineField(root, 'functional', 'foo', 'canvas', 'sourceHash')).toBe('source');
|
|
71
|
+
expect(getBaselineField(root, 'functional', 'foo', 'canvas', 'sha256')).toBe('hash');
|
|
72
|
+
expect(getBaselineField(root, 'functional', 'foo', 'webgl', 'sha256')).toBe('webgl-hash');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('replaces the full evidence column instead of retaining stale optional fields', () => {
|
|
76
|
+
setBaselineCaptureEvidence(root, 'functional', 'foo', 'canvas', {
|
|
77
|
+
fingerprint: '2:000000ffffff',
|
|
78
|
+
sourceHash: 'old-source',
|
|
79
|
+
sha256: 'old-hash',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
setBaselineCaptureEvidence(root, 'functional', 'foo', 'canvas', {
|
|
83
|
+
fingerprint: '2:111111eeeeee',
|
|
84
|
+
sha256: 'new-hash',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(getBaselineField(root, 'functional', 'foo', 'canvas', 'fingerprint')).toBe('2:111111eeeeee');
|
|
88
|
+
expect(getBaselineField(root, 'functional', 'foo', 'canvas', 'sourceHash')).toBeNull();
|
|
89
|
+
expect(getBaselineField(root, 'functional', 'foo', 'canvas', 'sha256')).toBe('new-hash');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('refuses runtime callers that omit either half of the evidence pair', () => {
|
|
93
|
+
expect(() =>
|
|
94
|
+
setBaselineCaptureEvidence(root, 'functional', 'foo', 'canvas', {
|
|
95
|
+
fingerprint: 'fp',
|
|
96
|
+
} as never),
|
|
97
|
+
).toThrow(/fingerprint and sha256 must be written together/);
|
|
98
|
+
expect(() =>
|
|
99
|
+
setBaselineCaptureEvidence(root, 'functional', 'foo', 'canvas', {
|
|
100
|
+
sha256: 'hash',
|
|
101
|
+
} as never),
|
|
102
|
+
).toThrow(/fingerprint and sha256 must be written together/);
|
|
103
|
+
expect(existsSync(baselinePath(root, 'functional', 'foo'))).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('inherits the uniform-fingerprint refusal instead of blessing a stable blank shape', () => {
|
|
107
|
+
expect(() =>
|
|
108
|
+
setBaselineCaptureEvidence(root, 'functional', 'foo', 'canvas', {
|
|
109
|
+
fingerprint: '2:eeeeeeeeeeee',
|
|
110
|
+
sha256: 'hash',
|
|
111
|
+
}),
|
|
112
|
+
).toThrow(/fingerprint is uniform/);
|
|
113
|
+
expect(existsSync(baselinePath(root, 'functional', 'foo'))).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('inherits the known-blank screenshot refusal instead of blessing a deterministic bad hash', () => {
|
|
117
|
+
expect(() =>
|
|
118
|
+
setBaselineCaptureEvidence(root, 'functional', 'foo', 'canvas', {
|
|
119
|
+
fingerprint: '2:000000ffffff',
|
|
120
|
+
sha256: 'a4f2105ecdefec94c5fe749c1dc5f2fb9dd74b9832cba0afcd3434f38c0380d0',
|
|
121
|
+
}),
|
|
122
|
+
).toThrow(/sha256 is a known blank frame/);
|
|
123
|
+
expect(existsSync(baselinePath(root, 'functional', 'foo'))).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('refuses a pair whose two provenance records disagree', () => {
|
|
127
|
+
expect(() =>
|
|
128
|
+
setBaselineCaptureEvidence(root, 'functional', 'foo', 'canvas', {
|
|
129
|
+
fingerprint: '2:000000ffffff',
|
|
130
|
+
fingerprintProvenance: PROVENANCE,
|
|
131
|
+
sha256: 'hash',
|
|
132
|
+
sha256Provenance: OTHER_PROVENANCE,
|
|
133
|
+
}),
|
|
134
|
+
).toThrow(/refusing split baseline provenance/);
|
|
135
|
+
expect(existsSync(baselinePath(root, 'functional', 'foo'))).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
41
139
|
describe('setBaselineField', () => {
|
|
42
|
-
it('preserves
|
|
43
|
-
setBaselineField(root, 'functional', 'foo', 'canvas', 'fingerprint', '
|
|
44
|
-
setBaselineField(root, 'functional', 'foo', 'canvas', '
|
|
140
|
+
it('preserves unrelated fields and columns on a read-merge-write', () => {
|
|
141
|
+
setBaselineField(root, 'functional', 'foo', 'canvas', 'fingerprint', '2:000000ffffff');
|
|
142
|
+
setBaselineField(root, 'functional', 'foo', 'canvas', 'sourceHash', 'source');
|
|
45
143
|
setBaselineField(root, 'functional', 'foo', 'webgl', 'sha256', 'hash2');
|
|
46
144
|
|
|
47
|
-
expect(getBaselineField(root, 'functional', 'foo', 'canvas', 'fingerprint')).toBe('
|
|
48
|
-
expect(getBaselineField(root, 'functional', 'foo', 'canvas', '
|
|
145
|
+
expect(getBaselineField(root, 'functional', 'foo', 'canvas', 'fingerprint')).toBe('2:000000ffffff');
|
|
146
|
+
expect(getBaselineField(root, 'functional', 'foo', 'canvas', 'sourceHash')).toBe('source');
|
|
49
147
|
expect(getBaselineField(root, 'functional', 'foo', 'webgl', 'sha256')).toBe('hash2');
|
|
50
148
|
});
|
|
51
149
|
|
|
150
|
+
it('allows the normal sha256-only stage and a legacy join with unknown provenance', () => {
|
|
151
|
+
setBaselineField(root, 'functional', 'foo', 'canvas', 'sha256', 'hash');
|
|
152
|
+
setBaselineField(root, 'functional', 'foo', 'canvas', 'fingerprint', '2:000000ffffff');
|
|
153
|
+
|
|
154
|
+
expect(getBaselineField(root, 'functional', 'foo', 'canvas', 'sha256')).toBe('hash');
|
|
155
|
+
expect(getBaselineField(root, 'functional', 'foo', 'canvas', 'fingerprint')).toBe('2:000000ffffff');
|
|
156
|
+
expect(getBaselineProvenance(root, 'functional', 'foo', 'canvas', 'sha256')).toBeNull();
|
|
157
|
+
expect(getBaselineProvenance(root, 'functional', 'foo', 'canvas', 'fingerprint')).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('allows a join when both provenance records agree', () => {
|
|
161
|
+
setBaselineField(root, 'functional', 'foo', 'canvas', 'sha256', 'hash', PROVENANCE);
|
|
162
|
+
setBaselineField(root, 'functional', 'foo', 'canvas', 'fingerprint', '2:000000ffffff', PROVENANCE);
|
|
163
|
+
|
|
164
|
+
expect(getBaselineField(root, 'functional', 'foo', 'canvas', 'sha256')).toBe('hash');
|
|
165
|
+
expect(getBaselineField(root, 'functional', 'foo', 'canvas', 'fingerprint')).toBe('2:000000ffffff');
|
|
166
|
+
expect(getBaselineProvenance(root, 'functional', 'foo', 'canvas', 'sha256')).toEqual(PROVENANCE);
|
|
167
|
+
expect(getBaselineProvenance(root, 'functional', 'foo', 'canvas', 'fingerprint')).toEqual(PROVENANCE);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('allows a join when either field has unknown provenance', () => {
|
|
171
|
+
setBaselineField(root, 'functional', 'foo', 'canvas', 'sha256', 'hash', PROVENANCE);
|
|
172
|
+
setBaselineField(root, 'functional', 'foo', 'canvas', 'fingerprint', '2:000000ffffff');
|
|
173
|
+
|
|
174
|
+
expect(getBaselineField(root, 'functional', 'foo', 'canvas', 'fingerprint')).toBe('2:000000ffffff');
|
|
175
|
+
expect(getBaselineProvenance(root, 'functional', 'foo', 'canvas', 'fingerprint')).toBeNull();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('refuses a fingerprint join whose provenance differs and leaves the record byte-identical', () => {
|
|
179
|
+
setBaselineField(root, 'functional', 'foo', 'canvas', 'sha256', 'hash', PROVENANCE);
|
|
180
|
+
const path = baselinePath(root, 'functional', 'foo');
|
|
181
|
+
const before = readFileSync(path, 'utf8');
|
|
182
|
+
|
|
183
|
+
expect(() =>
|
|
184
|
+
setBaselineField(root, 'functional', 'foo', 'canvas', 'fingerprint', '2:000000ffffff', OTHER_PROVENANCE),
|
|
185
|
+
).toThrow(/refusing split baseline provenance/);
|
|
186
|
+
expect(readFileSync(path, 'utf8')).toBe(before);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('refuses a sha256 join whose provenance differs and leaves the record byte-identical', () => {
|
|
190
|
+
setBaselineField(root, 'functional', 'foo', 'canvas', 'fingerprint', '2:000000ffffff', PROVENANCE);
|
|
191
|
+
const path = baselinePath(root, 'functional', 'foo');
|
|
192
|
+
const before = readFileSync(path, 'utf8');
|
|
193
|
+
|
|
194
|
+
expect(() => setBaselineField(root, 'functional', 'foo', 'canvas', 'sha256', 'hash', OTHER_PROVENANCE)).toThrow(
|
|
195
|
+
/refusing split baseline provenance/,
|
|
196
|
+
);
|
|
197
|
+
expect(readFileSync(path, 'utf8')).toBe(before);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('clears stale same-field provenance when a caller writes without provenance', () => {
|
|
201
|
+
setBaselineField(root, 'functional', 'foo', 'canvas', 'sha256', 'old-hash', PROVENANCE);
|
|
202
|
+
setBaselineField(root, 'functional', 'foo', 'canvas', 'sha256', 'new-hash');
|
|
203
|
+
|
|
204
|
+
expect(getBaselineField(root, 'functional', 'foo', 'canvas', 'sha256')).toBe('new-hash');
|
|
205
|
+
expect(getBaselineProvenance(root, 'functional', 'foo', 'canvas', 'sha256')).toBeNull();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('inherits the baseline sanity refusals on provenance-aware field writes', () => {
|
|
209
|
+
expect(() =>
|
|
210
|
+
setBaselineField(root, 'functional', 'foo', 'canvas', 'fingerprint', '2:eeeeeeeeeeee', PROVENANCE),
|
|
211
|
+
).toThrow(/fingerprint is uniform/);
|
|
212
|
+
expect(() =>
|
|
213
|
+
setBaselineField(
|
|
214
|
+
root,
|
|
215
|
+
'functional',
|
|
216
|
+
'foo',
|
|
217
|
+
'canvas',
|
|
218
|
+
'sha256',
|
|
219
|
+
'a4f2105ecdefec94c5fe749c1dc5f2fb9dd74b9832cba0afcd3434f38c0380d0',
|
|
220
|
+
PROVENANCE,
|
|
221
|
+
),
|
|
222
|
+
).toThrow(/sha256 is a known blank frame/);
|
|
223
|
+
expect(existsSync(baselinePath(root, 'functional', 'foo'))).toBe(false);
|
|
224
|
+
});
|
|
225
|
+
|
|
52
226
|
it('writes sorted, prettier-compatible JSON with a trailing newline', () => {
|
|
53
227
|
setBaselineField(root, 'functional', 'foo', 'webgl', 'sha256', 'h2');
|
|
54
228
|
setBaselineField(root, 'functional', 'foo', 'canvas', 'sha256', 'h1');
|
|
@@ -57,3 +231,32 @@ describe('setBaselineField', () => {
|
|
|
57
231
|
expect(text.indexOf('canvas')).toBeLessThan(text.indexOf('webgl'));
|
|
58
232
|
});
|
|
59
233
|
});
|
|
234
|
+
|
|
235
|
+
describe('setBaselineProvenance', () => {
|
|
236
|
+
it('preserves the other fields and the other columns', () => {
|
|
237
|
+
setBaselineField(root, 'functional', 'foo', 'canvas', 'sha256', 'abc123');
|
|
238
|
+
setBaselineField(root, 'functional', 'foo', 'webgl', 'sha256', 'def456');
|
|
239
|
+
|
|
240
|
+
setBaselineProvenance(root, 'functional', 'foo', 'canvas', 'sha256', PROVENANCE);
|
|
241
|
+
|
|
242
|
+
// Read-merge-write: recording provenance on one column rewrites nothing else on disk.
|
|
243
|
+
expect(getBaselineField(root, 'functional', 'foo', 'canvas', 'sha256')).toBe('abc123');
|
|
244
|
+
expect(getBaselineField(root, 'functional', 'foo', 'webgl', 'sha256')).toBe('def456');
|
|
245
|
+
expect(getBaselineProvenance(root, 'functional', 'foo', 'webgl', 'sha256')).toBeNull();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('refuses to attach provenance that disagrees with the counterpart', () => {
|
|
249
|
+
setBaselineField(root, 'functional', 'foo', 'canvas', 'fingerprint', '2:000000ffffff', PROVENANCE);
|
|
250
|
+
setBaselineField(root, 'functional', 'foo', 'canvas', 'sha256', 'hash');
|
|
251
|
+
const path = baselinePath(root, 'functional', 'foo');
|
|
252
|
+
const before = readFileSync(path, 'utf8');
|
|
253
|
+
|
|
254
|
+
expect(() => setBaselineProvenance(root, 'functional', 'foo', 'canvas', 'sha256', OTHER_PROVENANCE)).toThrow(
|
|
255
|
+
/refusing split baseline provenance/,
|
|
256
|
+
);
|
|
257
|
+
expect(readFileSync(path, 'utf8')).toBe(before);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const PROVENANCE = { frames: 0, sourceHash: null, targetKind: null, verifyPublished: true, warmupFrames: 0 };
|
|
262
|
+
const OTHER_PROVENANCE = { ...PROVENANCE, sourceHash: 'different-source' };
|
package/src/bin.test.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const ROOT = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
describe('tool-capture CLI', () => {
|
|
10
|
+
it('rejects a misspelled gate option before performing command work', () => {
|
|
11
|
+
const directory = mkdtempSync(join(tmpdir(), 'tool-capture-cli-'));
|
|
12
|
+
try {
|
|
13
|
+
const result = spawnSync(
|
|
14
|
+
process.execPath,
|
|
15
|
+
[
|
|
16
|
+
require.resolve('tsx/cli'),
|
|
17
|
+
join(ROOT, 'bin.ts'),
|
|
18
|
+
'batch',
|
|
19
|
+
`--config=${join(directory, 'missing.json')}`,
|
|
20
|
+
'--fail-on-chagned',
|
|
21
|
+
],
|
|
22
|
+
{ encoding: 'utf8', timeout: 10_000 },
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
expect(result.status).toBe(1);
|
|
26
|
+
expect(result.stderr).toContain('unknown option --fail-on-chagned');
|
|
27
|
+
expect(result.stderr).not.toContain('ENOENT');
|
|
28
|
+
} finally {
|
|
29
|
+
rmSync(directory, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
}, 15_000);
|
|
32
|
+
});
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
CAPTURE_BASELINE_COVERAGE_MANIFEST_VERSION,
|
|
7
|
+
CAPTURE_BASELINE_EVIDENCE_KINDS,
|
|
8
|
+
formatCaptureBaselineEvidenceIdentity,
|
|
9
|
+
listCaptureBaselineEvidence,
|
|
10
|
+
captureBaselineCoverageManifestPath,
|
|
11
|
+
createCaptureBaselineCoverageManifest,
|
|
12
|
+
diffCaptureBaselineCoverage,
|
|
13
|
+
formatCaptureBaselineCoverageIdentity,
|
|
14
|
+
isCaptureBaselineCoverageFailure,
|
|
15
|
+
readCaptureBaselineCoverageManifest,
|
|
16
|
+
writeCaptureBaselineCoverageManifest,
|
|
17
|
+
} from './captureBaselineCoverageManifest';
|
|
18
|
+
|
|
19
|
+
const ALL = ['fingerprint', 'oracle', 'screenshot'] as const;
|
|
20
|
+
const manifest = createCaptureBaselineCoverageManifest({
|
|
21
|
+
functional: {
|
|
22
|
+
'svg-image/webgl': ALL,
|
|
23
|
+
'svg-image/canvas': ALL,
|
|
24
|
+
'node-alpha/canvas': ALL,
|
|
25
|
+
},
|
|
26
|
+
examples: { 'text/canvas': ALL },
|
|
27
|
+
});
|
|
28
|
+
const everything = (
|
|
29
|
+
identities: readonly string[],
|
|
30
|
+
): Record<string, readonly ('fingerprint' | 'oracle' | 'screenshot')[]> =>
|
|
31
|
+
Object.fromEntries(identities.map((identity) => [identity, ALL]));
|
|
32
|
+
|
|
33
|
+
describe('CAPTURE_BASELINE_COVERAGE_MANIFEST_VERSION', () => {
|
|
34
|
+
it('is the schema the manifest is written with', () => {
|
|
35
|
+
expect(createCaptureBaselineCoverageManifest({}).schemaVersion).toBe(CAPTURE_BASELINE_COVERAGE_MANIFEST_VERSION);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('CAPTURE_BASELINE_EVIDENCE_KINDS', () => {
|
|
40
|
+
it('is the three evidence kinds, sorted', () => {
|
|
41
|
+
expect(CAPTURE_BASELINE_EVIDENCE_KINDS).toEqual(['fingerprint', 'oracle', 'screenshot']);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('captureBaselineCoverageManifestPath', () => {
|
|
46
|
+
it('pins the manifest beside the reachability baseline', () => {
|
|
47
|
+
expect(captureBaselineCoverageManifestPath('/repo')).toBe('/repo/scripts/capture-baseline-coverage-manifest.json');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('createCaptureBaselineCoverageManifest', () => {
|
|
52
|
+
it('sorts identities so the committed file has a stable diff', () => {
|
|
53
|
+
expect(Object.keys(manifest.subjects.functional)).toEqual([
|
|
54
|
+
'node-alpha/canvas',
|
|
55
|
+
'svg-image/canvas',
|
|
56
|
+
'svg-image/webgl',
|
|
57
|
+
]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('keeps subjects separate', () => {
|
|
61
|
+
expect(manifest.subjects.examples).toEqual({ 'text/canvas': ['fingerprint', 'oracle', 'screenshot'] });
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('diffCaptureBaselineCoverage', () => {
|
|
66
|
+
const all = ['svg-image/webgl', 'svg-image/canvas', 'node-alpha/canvas'];
|
|
67
|
+
const WHOLE = { entryFiltered: false, activeRenderers: null };
|
|
68
|
+
const FILTERED = { entryFiltered: true, activeRenderers: null };
|
|
69
|
+
|
|
70
|
+
it('reports nothing when observed coverage matches the pin', () => {
|
|
71
|
+
const diff = diffCaptureBaselineCoverage(manifest, 'functional', everything(all), all, WHOLE);
|
|
72
|
+
expect(diff).toEqual({ gained: [], lost: [], absent: [] });
|
|
73
|
+
expect(isCaptureBaselineCoverageFailure(diff)).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// The defect this mechanism closes: one target loses its baseline while others still compare, so the
|
|
77
|
+
// zero-floor is satisfied and the run stays green. Here that loss is named.
|
|
78
|
+
it('NAMES a target that ran but is no longer covered', () => {
|
|
79
|
+
const covered = ['svg-image/canvas', 'node-alpha/canvas'];
|
|
80
|
+
const diff = diffCaptureBaselineCoverage(manifest, 'functional', everything(covered), all, WHOLE);
|
|
81
|
+
expect(diff.lost).toEqual(['svg-image/webgl#fingerprint', 'svg-image/webgl#oracle', 'svg-image/webgl#screenshot']);
|
|
82
|
+
expect(isCaptureBaselineCoverageFailure(diff)).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// The manifest is an exact set: an unaccepted gain ages the pin, so it must be accepted, not ignored.
|
|
86
|
+
it('names new coverage as a gain AND fails on it', () => {
|
|
87
|
+
const covered = [...all, 'svg-text/webgl'];
|
|
88
|
+
const diff = diffCaptureBaselineCoverage(manifest, 'functional', everything(covered), covered, WHOLE);
|
|
89
|
+
expect(diff.gained).toEqual(['svg-text/webgl#fingerprint', 'svg-text/webgl#oracle', 'svg-text/webgl#screenshot']);
|
|
90
|
+
expect(diff.lost).toEqual([]);
|
|
91
|
+
expect(isCaptureBaselineCoverageFailure(diff)).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ★ The case that justifies identities over a count: the total is unchanged, so every count-shaped
|
|
95
|
+
// check reconciles and reports clean. Only an exact set names the half that left and the half that came.
|
|
96
|
+
it('catches a SAME-COUNT swap, naming both halves', () => {
|
|
97
|
+
const covered = ['svg-image/canvas', 'node-alpha/canvas', 'svg-text/webgl'];
|
|
98
|
+
const diff = diffCaptureBaselineCoverage(
|
|
99
|
+
manifest,
|
|
100
|
+
'functional',
|
|
101
|
+
everything(covered),
|
|
102
|
+
[...all, 'svg-text/webgl'],
|
|
103
|
+
WHOLE,
|
|
104
|
+
);
|
|
105
|
+
expect(covered.length).toBe(Object.keys(manifest.subjects.functional).length);
|
|
106
|
+
expect(diff.lost).toEqual(['svg-image/webgl#fingerprint', 'svg-image/webgl#oracle', 'svg-image/webgl#screenshot']);
|
|
107
|
+
expect(diff.gained).toEqual(['svg-text/webgl#fingerprint', 'svg-text/webgl#oracle', 'svg-text/webgl#screenshot']);
|
|
108
|
+
expect(isCaptureBaselineCoverageFailure(diff)).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// A whole-repo run can tell "this target vanished from discovery" from "this target lost its
|
|
112
|
+
// baseline"; a scoped run cannot, because not-visited is indistinguishable from filtered-out.
|
|
113
|
+
it('reports a vanished target as absent ONLY on a whole-repo run', () => {
|
|
114
|
+
const covered = ['svg-image/canvas', 'svg-image/webgl'];
|
|
115
|
+
const visited = covered;
|
|
116
|
+
expect(diffCaptureBaselineCoverage(manifest, 'functional', everything(covered), visited, WHOLE).absent).toEqual([
|
|
117
|
+
'node-alpha/canvas#fingerprint',
|
|
118
|
+
'node-alpha/canvas#oracle',
|
|
119
|
+
'node-alpha/canvas#screenshot',
|
|
120
|
+
]);
|
|
121
|
+
expect(diffCaptureBaselineCoverage(manifest, 'functional', everything(covered), visited, FILTERED).absent).toEqual(
|
|
122
|
+
[],
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// A scoped run must still be able to fail: it visited the target and found no baseline.
|
|
127
|
+
it('still reports a loss on a scoped run for a target it actually visited', () => {
|
|
128
|
+
const diff = diffCaptureBaselineCoverage(manifest, 'functional', {}, ['svg-image/webgl'], FILTERED);
|
|
129
|
+
expect(diff.lost).toEqual(['svg-image/webgl#fingerprint', 'svg-image/webgl#oracle', 'svg-image/webgl#screenshot']);
|
|
130
|
+
expect(isCaptureBaselineCoverageFailure(diff)).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// --renderer=webgl must not report every canvas pin as vanished.
|
|
134
|
+
it('narrows the absence check to the renderers the run actually ran', () => {
|
|
135
|
+
const diff = diffCaptureBaselineCoverage(
|
|
136
|
+
manifest,
|
|
137
|
+
'functional',
|
|
138
|
+
everything(['svg-image/webgl']),
|
|
139
|
+
['svg-image/webgl'],
|
|
140
|
+
{
|
|
141
|
+
entryFiltered: false,
|
|
142
|
+
activeRenderers: ['webgl'],
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
expect(diff.absent).toEqual([]);
|
|
146
|
+
expect(diff.lost).toEqual([]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// A page that never loaded settles nothing. Reporting it as vanished would blame the manifest for a
|
|
150
|
+
// load failure the run already reported on its own.
|
|
151
|
+
it('does not report an UNDETERMINED identity as absent', () => {
|
|
152
|
+
const diff = diffCaptureBaselineCoverage(
|
|
153
|
+
manifest,
|
|
154
|
+
'functional',
|
|
155
|
+
everything(['svg-image/canvas', 'node-alpha/canvas']),
|
|
156
|
+
['svg-image/canvas', 'node-alpha/canvas'],
|
|
157
|
+
{
|
|
158
|
+
entryFiltered: false,
|
|
159
|
+
activeRenderers: null,
|
|
160
|
+
undetermined: ['svg-image/webgl'],
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
expect(diff.absent).toEqual([]);
|
|
164
|
+
expect(diff.lost).toEqual([]);
|
|
165
|
+
expect(isCaptureBaselineCoverageFailure(diff)).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('treats an unknown subject as an empty pin rather than throwing', () => {
|
|
169
|
+
const diff = diffCaptureBaselineCoverage(manifest, 'reference', everything(['a/canvas']), ['a/canvas'], WHOLE);
|
|
170
|
+
expect(diff).toEqual({
|
|
171
|
+
gained: ['a/canvas#fingerprint', 'a/canvas#oracle', 'a/canvas#screenshot'],
|
|
172
|
+
lost: [],
|
|
173
|
+
absent: [],
|
|
174
|
+
});
|
|
175
|
+
expect(isCaptureBaselineCoverageFailure(diff)).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('evidence-kind scope', () => {
|
|
180
|
+
const all = ['svg-image/webgl', 'svg-image/canvas', 'node-alpha/canvas'];
|
|
181
|
+
|
|
182
|
+
// ★ The rule that makes ONE manifest over three tiers safe. A validate run observes fingerprints and
|
|
183
|
+
// nothing else; without this it would report every screenshot and oracle pin as lost on every run —
|
|
184
|
+
// absence of OBSERVATION reported as absence of EVIDENCE, which is the defect being fixed.
|
|
185
|
+
it('does not report a kind the run never observed', () => {
|
|
186
|
+
const diff = diffCaptureBaselineCoverage(
|
|
187
|
+
manifest,
|
|
188
|
+
'functional',
|
|
189
|
+
Object.fromEntries(all.map((identity) => [identity, ['fingerprint'] as const])),
|
|
190
|
+
all,
|
|
191
|
+
{ entryFiltered: false, activeRenderers: null, kinds: ['fingerprint'] },
|
|
192
|
+
);
|
|
193
|
+
expect(diff).toEqual({ gained: [], lost: [], absent: [] });
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Same run, same data, WITHOUT declaring the observed kind: the two unobserved columns read as lost.
|
|
197
|
+
// This is the control — it shows the guard above is load-bearing rather than decorative.
|
|
198
|
+
it('DOES report them when the run fails to declare what it observed', () => {
|
|
199
|
+
const diff = diffCaptureBaselineCoverage(
|
|
200
|
+
manifest,
|
|
201
|
+
'functional',
|
|
202
|
+
Object.fromEntries(all.map((identity) => [identity, ['fingerprint'] as const])),
|
|
203
|
+
all,
|
|
204
|
+
{ entryFiltered: false, activeRenderers: null },
|
|
205
|
+
);
|
|
206
|
+
expect(diff.lost).toContain('svg-image/webgl#oracle');
|
|
207
|
+
expect(diff.lost).toContain('svg-image/webgl#screenshot');
|
|
208
|
+
expect(diff.lost).not.toContain('svg-image/webgl#fingerprint');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// One target losing ONE of its three columns is the erosion this consolidation must still catch.
|
|
212
|
+
it('names the single lost column, not the whole target', () => {
|
|
213
|
+
const covered: Record<string, readonly ('fingerprint' | 'oracle' | 'screenshot')[]> = {
|
|
214
|
+
'svg-image/webgl': ['fingerprint', 'screenshot'],
|
|
215
|
+
'svg-image/canvas': ALL,
|
|
216
|
+
'node-alpha/canvas': ALL,
|
|
217
|
+
};
|
|
218
|
+
const diff = diffCaptureBaselineCoverage(manifest, 'functional', covered, all, {
|
|
219
|
+
entryFiltered: false,
|
|
220
|
+
activeRenderers: null,
|
|
221
|
+
});
|
|
222
|
+
expect(diff.lost).toEqual(['svg-image/webgl#oracle']);
|
|
223
|
+
expect(isCaptureBaselineCoverageFailure(diff)).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe('formatCaptureBaselineCoverageIdentity', () => {
|
|
228
|
+
it('joins entry and renderer into the pinned identity', () => {
|
|
229
|
+
expect(formatCaptureBaselineCoverageIdentity('svg-image', 'webgl')).toBe('svg-image/webgl');
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('formatCaptureBaselineEvidenceIdentity', () => {
|
|
234
|
+
// `#`, not `:` — a renderer id may itself contain a colon (`flight:webgl`), so a colon separator
|
|
235
|
+
// would make the target and the kind ambiguous to split.
|
|
236
|
+
it('joins target and kind with a separator no renderer id contains', () => {
|
|
237
|
+
expect(formatCaptureBaselineEvidenceIdentity('svg-image/flight:webgl', 'oracle')).toBe(
|
|
238
|
+
'svg-image/flight:webgl#oracle',
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('isCaptureBaselineCoverageFailure', () => {
|
|
244
|
+
it('is true in every direction and false only for an exact match', () => {
|
|
245
|
+
expect(isCaptureBaselineCoverageFailure({ gained: [], lost: [], absent: [] })).toBe(false);
|
|
246
|
+
expect(isCaptureBaselineCoverageFailure({ gained: ['x/canvas#oracle'], lost: [], absent: [] })).toBe(true);
|
|
247
|
+
expect(isCaptureBaselineCoverageFailure({ gained: [], lost: ['x/canvas#fingerprint'], absent: [] })).toBe(true);
|
|
248
|
+
expect(isCaptureBaselineCoverageFailure({ gained: [], lost: [], absent: ['x/canvas#screenshot'] })).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe('listCaptureBaselineEvidence', () => {
|
|
253
|
+
it('flattens every pinned target into its individual evidence units', () => {
|
|
254
|
+
expect(listCaptureBaselineEvidence(manifest, 'examples')).toEqual([
|
|
255
|
+
'text/canvas#fingerprint',
|
|
256
|
+
'text/canvas#oracle',
|
|
257
|
+
'text/canvas#screenshot',
|
|
258
|
+
]);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('is empty for a subject the manifest does not pin', () => {
|
|
262
|
+
expect(listCaptureBaselineEvidence(manifest, 'reference')).toEqual([]);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe('readCaptureBaselineCoverageManifest', () => {
|
|
267
|
+
it('reads a missing manifest as empty rather than throwing', () => {
|
|
268
|
+
const root = mkdtempSync(join(tmpdir(), 'flight-coverage-'));
|
|
269
|
+
expect(readCaptureBaselineCoverageManifest(root).subjects).toEqual({});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('round-trips what was written', () => {
|
|
273
|
+
const root = mkdtempSync(join(tmpdir(), 'flight-coverage-'));
|
|
274
|
+
mkdirSync(join(root, 'scripts'));
|
|
275
|
+
writeCaptureBaselineCoverageManifest(root, 'functional', everything(['b/canvas', 'a/webgl']));
|
|
276
|
+
expect(Object.keys(readCaptureBaselineCoverageManifest(root).subjects.functional)).toEqual(['a/webgl', 'b/canvas']);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('schema 1 migration', () => {
|
|
281
|
+
// Schema 1 knew only fingerprints, so a bare identity list means fingerprint-only. Reading it as all
|
|
282
|
+
// three would manufacture screenshot and oracle pins nobody ever accepted, and the next run would
|
|
283
|
+
// report them as losses.
|
|
284
|
+
it('reads a v1 identity list as fingerprint evidence only', () => {
|
|
285
|
+
const root = mkdtempSync(join(tmpdir(), 'flight-coverage-v1-'));
|
|
286
|
+
mkdirSync(join(root, 'scripts'));
|
|
287
|
+
writeFileSync(
|
|
288
|
+
join(root, 'scripts', 'capture-baseline-coverage-manifest.json'),
|
|
289
|
+
JSON.stringify({ schemaVersion: 1, subjects: { functional: ['a/webgl', 'b/canvas'] } }),
|
|
290
|
+
);
|
|
291
|
+
const read = readCaptureBaselineCoverageManifest(root);
|
|
292
|
+
expect(read.schemaVersion).toBe(2);
|
|
293
|
+
expect(read.subjects.functional).toEqual({ 'a/webgl': ['fingerprint'], 'b/canvas': ['fingerprint'] });
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe('writeCaptureBaselineCoverageManifest', () => {
|
|
298
|
+
// A functional update run knows nothing about the examples subject and must not speak for it.
|
|
299
|
+
it('rewrites ONE subject and preserves the others', () => {
|
|
300
|
+
const root = mkdtempSync(join(tmpdir(), 'flight-coverage-'));
|
|
301
|
+
mkdirSync(join(root, 'scripts'));
|
|
302
|
+
writeCaptureBaselineCoverageManifest(root, 'examples', everything(['text/canvas']));
|
|
303
|
+
const manifest = writeCaptureBaselineCoverageManifest(root, 'functional', everything(['a/webgl']));
|
|
304
|
+
expect(Object.keys(manifest.subjects.examples)).toEqual(['text/canvas']);
|
|
305
|
+
expect(Object.keys(manifest.subjects.functional)).toEqual(['a/webgl']);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// The regression tier runs --renderer=canvas,webgl,webgpu. Such a run must not retire dom pins.
|
|
309
|
+
it('carries pins for renderers the update run did not cover', () => {
|
|
310
|
+
const root = mkdtempSync(join(tmpdir(), 'flight-coverage-'));
|
|
311
|
+
mkdirSync(join(root, 'scripts'));
|
|
312
|
+
writeCaptureBaselineCoverageManifest(root, 'functional', everything(['a/dom', 'a/canvas']));
|
|
313
|
+
const manifest = writeCaptureBaselineCoverageManifest(root, 'functional', everything(['a/canvas']), [
|
|
314
|
+
'canvas',
|
|
315
|
+
'webgl',
|
|
316
|
+
]);
|
|
317
|
+
expect(Object.keys(manifest.subjects.functional)).toEqual(['a/canvas', 'a/dom']);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// One flaky target must not retire its own pin: the run never determined it, so the pin stands.
|
|
321
|
+
it('carries forward a pin the run did not determine', () => {
|
|
322
|
+
const root = mkdtempSync(join(tmpdir(), 'flight-coverage-'));
|
|
323
|
+
mkdirSync(join(root, 'scripts'));
|
|
324
|
+
writeCaptureBaselineCoverageManifest(root, 'examples', everything(['collision/webgpu', 'collision/canvas']));
|
|
325
|
+
const manifestAfter = writeCaptureBaselineCoverageManifest(
|
|
326
|
+
root,
|
|
327
|
+
'examples',
|
|
328
|
+
everything(['collision/canvas']),
|
|
329
|
+
null,
|
|
330
|
+
['collision/canvas'],
|
|
331
|
+
);
|
|
332
|
+
expect(Object.keys(manifestAfter.subjects.examples)).toEqual(['collision/canvas', 'collision/webgpu']);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Determined AND uncovered is a real retirement, and must still go through.
|
|
336
|
+
it('retires a pin the run determined to be uncovered', () => {
|
|
337
|
+
const root = mkdtempSync(join(tmpdir(), 'flight-coverage-'));
|
|
338
|
+
mkdirSync(join(root, 'scripts'));
|
|
339
|
+
writeCaptureBaselineCoverageManifest(root, 'examples', everything(['collision/webgpu', 'collision/canvas']));
|
|
340
|
+
const manifestAfter = writeCaptureBaselineCoverageManifest(
|
|
341
|
+
root,
|
|
342
|
+
'examples',
|
|
343
|
+
everything(['collision/canvas']),
|
|
344
|
+
null,
|
|
345
|
+
['collision/canvas', 'collision/webgpu'],
|
|
346
|
+
);
|
|
347
|
+
expect(Object.keys(manifestAfter.subjects.examples)).toEqual(['collision/canvas']);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// With no renderer scope the run speaks for the whole subject, so a dropped target really is retired.
|
|
351
|
+
it('retires a target on an unscoped update', () => {
|
|
352
|
+
const root = mkdtempSync(join(tmpdir(), 'flight-coverage-'));
|
|
353
|
+
mkdirSync(join(root, 'scripts'));
|
|
354
|
+
writeCaptureBaselineCoverageManifest(root, 'functional', everything(['a/dom', 'a/canvas']));
|
|
355
|
+
const manifest = writeCaptureBaselineCoverageManifest(root, 'functional', everything(['a/canvas']), null, [
|
|
356
|
+
'a/canvas',
|
|
357
|
+
'a/dom',
|
|
358
|
+
]);
|
|
359
|
+
expect(Object.keys(manifest.subjects.functional)).toEqual(['a/canvas']);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('writes a trailing newline so the format gate never churns it', () => {
|
|
363
|
+
const root = mkdtempSync(join(tmpdir(), 'flight-coverage-'));
|
|
364
|
+
mkdirSync(join(root, 'scripts'));
|
|
365
|
+
writeCaptureBaselineCoverageManifest(root, 'functional', everything(['a/webgl']));
|
|
366
|
+
expect(readFileSync(captureBaselineCoverageManifestPath(root), 'utf8').endsWith('}\n')).toBe(true);
|
|
367
|
+
});
|
|
368
|
+
});
|