@invarn/cibuild 2.0.1 → 2.0.3
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/dist/cli.cjs +350 -3
- package/dist/src/yaml/steps/index.d.ts.map +1 -1
- package/dist/src/yaml/steps/index.js +14 -5
- package/dist/src/yaml/steps/ui-fidelity-render.d.ts +71 -8
- package/dist/src/yaml/steps/ui-fidelity-render.d.ts.map +1 -1
- package/dist/src/yaml/steps/ui-fidelity-render.js +560 -7
- package/dist/src/yaml/steps/ui-fidelity-render.test.js +611 -7
- package/package.json +1 -1
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { describe, test, expect, afterAll } from '@jest/globals';
|
|
18
18
|
import { spawnSync } from 'node:child_process';
|
|
19
|
+
import { gzipSync } from 'node:zlib';
|
|
19
20
|
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync, } from 'node:fs';
|
|
20
21
|
import { tmpdir } from 'node:os';
|
|
21
22
|
import { dirname, join, resolve } from 'node:path';
|
|
@@ -29,25 +30,45 @@ const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
|
29
30
|
const FIXTURE_PACKAGE = resolve(dirname(fileURLToPath(import.meta.url)), '../../../test/fixtures/ui-fidelity-package');
|
|
30
31
|
/**
|
|
31
32
|
* Fake `swift` CLI. Mirrors the exact invocations the render script makes:
|
|
33
|
+
* swift package --package-path <dir> dump-package
|
|
32
34
|
* swift build --package-path <dir> --target RenderProbe
|
|
33
35
|
* swift build --package-path <dir> --product Render_<Screen>
|
|
34
36
|
* swift run --package-path <dir> --skip-build Render_<Screen> <output.png>
|
|
37
|
+
* dump-package output is driven by FAKE_SWIFT_DUMP_PRODUCTS (comma-separated
|
|
38
|
+
* library product names; unset = "FixtureViews", empty = no products) and
|
|
39
|
+
* FAKE_SWIFT_DUMP_FAIL.
|
|
35
40
|
*/
|
|
36
41
|
const FAKE_SWIFT_LINES = [
|
|
37
42
|
'#!/bin/bash',
|
|
38
43
|
'[ -n "$FAKE_SWIFT_LOG" ] && echo "$*" >> "$FAKE_SWIFT_LOG"',
|
|
39
44
|
'cmd="$1"; shift',
|
|
40
45
|
'case "$cmd" in',
|
|
46
|
+
' package)',
|
|
47
|
+
' if [ -n "$FAKE_SWIFT_DUMP_FAIL" ]; then',
|
|
48
|
+
' echo "error: malformed Package.swift manifest" >&2',
|
|
49
|
+
' exit 1',
|
|
50
|
+
' fi',
|
|
51
|
+
' products="${FAKE_SWIFT_DUMP_PRODUCTS-FixtureViews}"',
|
|
52
|
+
' json=""',
|
|
53
|
+
' IFS=","',
|
|
54
|
+
' for p in $products; do',
|
|
55
|
+
' [ -n "$json" ] && json="$json,"',
|
|
56
|
+
' json="$json{\\"name\\":\\"$p\\",\\"type\\":{\\"library\\":[\\"automatic\\"]}}"',
|
|
57
|
+
' done',
|
|
58
|
+
' printf \'{"name":"shipped-package","products":[%s]}\\n\' "$json"',
|
|
59
|
+
' exit 0 ;;',
|
|
41
60
|
' build)',
|
|
42
|
-
' product=""; target=""; pkg=""',
|
|
61
|
+
' product=""; target=""; pkg=""; showbin=""',
|
|
43
62
|
' while [ $# -gt 0 ]; do',
|
|
44
63
|
' case "$1" in',
|
|
45
64
|
' --product) product="$2"; shift 2 ;;',
|
|
46
65
|
' --target) target="$2"; shift 2 ;;',
|
|
47
66
|
' --package-path) pkg="$2"; shift 2 ;;',
|
|
67
|
+
' --show-bin-path) showbin=1; shift ;;',
|
|
48
68
|
' *) shift ;;',
|
|
49
69
|
' esac',
|
|
50
70
|
' done',
|
|
71
|
+
' if [ -n "$showbin" ]; then echo "$pkg/.build/debug"; exit 0; fi',
|
|
51
72
|
' name="${product:-$target}"',
|
|
52
73
|
' if [ "$name" = "RenderProbe" ]; then',
|
|
53
74
|
' if [ -n "$FAKE_SWIFT_PROBE_FAIL" ]; then',
|
|
@@ -90,6 +111,35 @@ const FAKE_SWIFT_LINES = [
|
|
|
90
111
|
' exit 0 ;;',
|
|
91
112
|
'esac',
|
|
92
113
|
];
|
|
114
|
+
/**
|
|
115
|
+
* PATH-shimmed fake `xcrun` covering `xcrun actool`. Logs its args to
|
|
116
|
+
* $FAKE_XCRUN_LOG, writes a stub Assets.car into the --compile directory,
|
|
117
|
+
* and fails (exit 1) when FAKE_ACTOOL_FAIL is set so the warn-and-continue
|
|
118
|
+
* path is exercisable.
|
|
119
|
+
*/
|
|
120
|
+
const FAKE_XCRUN_LINES = [
|
|
121
|
+
'#!/bin/bash',
|
|
122
|
+
'[ -n "$FAKE_XCRUN_LOG" ] && echo "$*" >> "$FAKE_XCRUN_LOG"',
|
|
123
|
+
'sub="$1"; shift',
|
|
124
|
+
'case "$sub" in',
|
|
125
|
+
' actool)',
|
|
126
|
+
' out=""',
|
|
127
|
+
' while [ $# -gt 0 ]; do',
|
|
128
|
+
' case "$1" in',
|
|
129
|
+
' --compile) out="$2"; shift 2 ;;',
|
|
130
|
+
' *) shift ;;',
|
|
131
|
+
' esac',
|
|
132
|
+
' done',
|
|
133
|
+
' if [ -n "$FAKE_ACTOOL_FAIL" ]; then',
|
|
134
|
+
' echo "error: actool: malformed asset catalog" >&2',
|
|
135
|
+
' exit 1',
|
|
136
|
+
' fi',
|
|
137
|
+
' [ -n "$out" ] && mkdir -p "$out" && printf \'fake-car\' > "$out/Assets.car"',
|
|
138
|
+
' exit 0 ;;',
|
|
139
|
+
' *)',
|
|
140
|
+
' exit 0 ;;',
|
|
141
|
+
'esac',
|
|
142
|
+
];
|
|
93
143
|
const tempDirs = [];
|
|
94
144
|
afterAll(() => {
|
|
95
145
|
for (const dir of tempDirs) {
|
|
@@ -112,6 +162,9 @@ function makeProject(params) {
|
|
|
112
162
|
const swiftPath = join(binDir, 'swift');
|
|
113
163
|
writeFileSync(swiftPath, FAKE_SWIFT_LINES.join('\n') + '\n');
|
|
114
164
|
chmodSync(swiftPath, 0o755);
|
|
165
|
+
const xcrunPath = join(binDir, 'xcrun');
|
|
166
|
+
writeFileSync(xcrunPath, FAKE_XCRUN_LINES.join('\n') + '\n');
|
|
167
|
+
chmodSync(xcrunPath, 0o755);
|
|
115
168
|
if (params !== undefined) {
|
|
116
169
|
writeFileSync(join(dir, '.ci', 'inputs', 'params.json'), typeof params === 'string' ? params : JSON.stringify(params));
|
|
117
170
|
}
|
|
@@ -136,6 +189,66 @@ function runScript(project, script, env = {}) {
|
|
|
136
189
|
},
|
|
137
190
|
});
|
|
138
191
|
}
|
|
192
|
+
function tarHeader(name, size, typeflag) {
|
|
193
|
+
const header = Buffer.alloc(512);
|
|
194
|
+
header.write(name, 0, 100, 'utf-8');
|
|
195
|
+
header.write('0000755\0', 100, 8, 'ascii'); // mode
|
|
196
|
+
header.write('0000000\0', 108, 8, 'ascii'); // uid
|
|
197
|
+
header.write('0000000\0', 116, 8, 'ascii'); // gid
|
|
198
|
+
header.write(size.toString(8).padStart(11, '0') + '\0', 124, 12, 'ascii');
|
|
199
|
+
header.write('00000000000\0', 136, 12, 'ascii'); // mtime
|
|
200
|
+
header.write(' ', 148, 8, 'ascii'); // checksum placeholder
|
|
201
|
+
header.write(typeflag, 156, 1, 'ascii');
|
|
202
|
+
header.write('ustar\0' + '00', 257, 8, 'ascii');
|
|
203
|
+
let checksum = 0;
|
|
204
|
+
for (const byte of header)
|
|
205
|
+
checksum += byte;
|
|
206
|
+
header.write(checksum.toString(8).padStart(6, '0') + '\0 ', 148, 8, 'ascii');
|
|
207
|
+
return header;
|
|
208
|
+
}
|
|
209
|
+
function makeTarGz(entries) {
|
|
210
|
+
const blocks = [];
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
const isDir = entry.name.endsWith('/');
|
|
213
|
+
const content = Buffer.from(entry.content ?? '', 'utf-8');
|
|
214
|
+
blocks.push(tarHeader(entry.name, isDir ? 0 : content.length, isDir ? '5' : '0'));
|
|
215
|
+
if (!isDir && content.length > 0) {
|
|
216
|
+
const padded = Buffer.alloc(Math.ceil(content.length / 512) * 512);
|
|
217
|
+
content.copy(padded);
|
|
218
|
+
blocks.push(padded);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
blocks.push(Buffer.alloc(1024)); // end-of-archive
|
|
222
|
+
return gzipSync(Buffer.concat(blocks));
|
|
223
|
+
}
|
|
224
|
+
const SHIPPED_MANIFEST = [
|
|
225
|
+
'// swift-tools-version:5.9',
|
|
226
|
+
'import PackageDescription',
|
|
227
|
+
'let package = Package(',
|
|
228
|
+
' name: "shipped-package",',
|
|
229
|
+
' platforms: [.macOS(.v13)],',
|
|
230
|
+
' products: [.library(name: "FixtureViews", targets: ["FixtureViews"])],',
|
|
231
|
+
' targets: [.target(name: "FixtureViews")]',
|
|
232
|
+
')',
|
|
233
|
+
].join('\n');
|
|
234
|
+
/** Entries for a well-formed shipped package under the given root dir ('' = flat). */
|
|
235
|
+
function shippedPackageEntries(root = 'pkg') {
|
|
236
|
+
const prefix = root === '' ? '' : root + '/';
|
|
237
|
+
const entries = root === '' ? [] : [{ name: prefix }];
|
|
238
|
+
entries.push({ name: prefix + 'Package.swift', content: SHIPPED_MANIFEST }, {
|
|
239
|
+
name: prefix + 'Sources/FixtureViews/HomeView.swift',
|
|
240
|
+
content: 'import SwiftUI\npublic struct HomeView: View { public init() {} public var body: some View { Text("home") } }\n',
|
|
241
|
+
});
|
|
242
|
+
return entries;
|
|
243
|
+
}
|
|
244
|
+
function stageArchive(project, archive) {
|
|
245
|
+
writeFileSync(join(project.dir, '.ci', 'inputs', 'package.tar.gz'), Buffer.isBuffer(archive) ? archive : makeTarGz(archive));
|
|
246
|
+
}
|
|
247
|
+
async function buildInputsScript(extra = {}) {
|
|
248
|
+
const executor = new UiFidelityRenderStepExecutor();
|
|
249
|
+
const step = await executor.execute({ package_source: 'inputs', ...extra }, {}, testConfig);
|
|
250
|
+
return step.script;
|
|
251
|
+
}
|
|
139
252
|
function readResult(project) {
|
|
140
253
|
const resultPath = join(project.dir, '.ci', 'artifacts', 'protocol-result.json');
|
|
141
254
|
expect(existsSync(resultPath)).toBe(true);
|
|
@@ -151,8 +264,9 @@ function isPng(filePath) {
|
|
|
151
264
|
/**
|
|
152
265
|
* Asserts that no error message in the result document contains an absolute
|
|
153
266
|
* runner-filesystem path: not the project dir (the script's cwd), not the OS
|
|
154
|
-
* temp dir (where harness packages live), in
|
|
155
|
-
* (/var/folders/...) or resolved (/private/var/...) form.
|
|
267
|
+
* temp dir (where harness packages and extracted shipped packages live), in
|
|
268
|
+
* either symlinked (/var/folders/...) or resolved (/private/var/...) form.
|
|
269
|
+
* Covers both per-screen errors and the build-level error object.
|
|
156
270
|
*/
|
|
157
271
|
function expectNoAbsolutePathLeaks(project, result) {
|
|
158
272
|
const forbidden = new Set();
|
|
@@ -165,8 +279,9 @@ function expectNoAbsolutePathLeaks(project, result) {
|
|
|
165
279
|
// nonexistent — literal form only
|
|
166
280
|
}
|
|
167
281
|
}
|
|
168
|
-
|
|
169
|
-
|
|
282
|
+
const messages = result.screens.map((screen) => screen.error?.message ?? '');
|
|
283
|
+
messages.push(result.error?.message ?? '');
|
|
284
|
+
for (const message of messages) {
|
|
170
285
|
for (const dir of forbidden) {
|
|
171
286
|
expect(message).not.toContain(dir);
|
|
172
287
|
}
|
|
@@ -182,18 +297,38 @@ describe('registry integration', () => {
|
|
|
182
297
|
const inputs = metadata?.inputs ?? {};
|
|
183
298
|
expect(Object.keys(inputs).sort()).toEqual([
|
|
184
299
|
'package_path',
|
|
300
|
+
'package_source',
|
|
185
301
|
'render_size',
|
|
186
302
|
'scale',
|
|
187
303
|
'target',
|
|
188
304
|
]);
|
|
189
|
-
|
|
190
|
-
|
|
305
|
+
// package_path/target are enforced by the executor in repo mode (the
|
|
306
|
+
// default); a package_source:inputs config is valid without either, so
|
|
307
|
+
// neither is unconditionally required at the schema level.
|
|
308
|
+
expect(inputs.package_path.required).toBe(false);
|
|
309
|
+
expect(inputs.target.required).toBe(false);
|
|
310
|
+
expect(inputs.package_source.required).toBe(false);
|
|
311
|
+
expect(inputs.package_source.default).toBe('repo');
|
|
191
312
|
expect(inputs.render_size.required).toBe(false);
|
|
192
313
|
expect(inputs.render_size.default).toBe(DEFAULT_RENDER_SIZE);
|
|
193
314
|
expect(inputs.scale.required).toBe(false);
|
|
194
315
|
expect(inputs.scale.default).toBe(DEFAULT_SCALE);
|
|
195
316
|
});
|
|
196
317
|
});
|
|
318
|
+
describe('v1 byte compatibility', () => {
|
|
319
|
+
// Snapshot of the script generated before package_source existed, captured
|
|
320
|
+
// from the executor with package_path './MyAppPackage' and target
|
|
321
|
+
// 'MyAppViews' (defaults for everything else). Omitting package_source must
|
|
322
|
+
// keep the generated script byte-identical to that snapshot, so existing
|
|
323
|
+
// protocols are provably untouched. Update the snapshot only for a
|
|
324
|
+
// deliberate, reviewed change to the v1 runtime.
|
|
325
|
+
const V1_SCRIPT_SNAPSHOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../../test/fixtures/ui-fidelity-render-v1-script.txt');
|
|
326
|
+
test('omitting package_source generates a byte-identical script', async () => {
|
|
327
|
+
const executor = new UiFidelityRenderStepExecutor();
|
|
328
|
+
const step = await executor.execute({ package_path: './MyAppPackage', target: 'MyAppViews' }, {}, testConfig);
|
|
329
|
+
expect(step.script).toBe(readFileSync(V1_SCRIPT_SNAPSHOT, 'utf-8'));
|
|
330
|
+
});
|
|
331
|
+
});
|
|
197
332
|
describe('UiFidelityRenderStepExecutor', () => {
|
|
198
333
|
test('returns a node StepDef', async () => {
|
|
199
334
|
const executor = new UiFidelityRenderStepExecutor();
|
|
@@ -234,6 +369,52 @@ describe('UiFidelityRenderStepExecutor', () => {
|
|
|
234
369
|
await expect(executor.execute({ package_path: './pkg', target: 'MyViews', render_size: 'huge' }, {}, testConfig)).rejects.toThrow(/render_size/);
|
|
235
370
|
});
|
|
236
371
|
});
|
|
372
|
+
describe('package_source input', () => {
|
|
373
|
+
test('rejects an unknown package_source value', async () => {
|
|
374
|
+
const executor = new UiFidelityRenderStepExecutor();
|
|
375
|
+
await expect(executor.execute({ package_path: './pkg', target: 'MyViews', package_source: 'artifact' }, {}, testConfig)).rejects.toThrow(/package_source/);
|
|
376
|
+
});
|
|
377
|
+
test('omitting package_source bakes no packageSource into the script config', async () => {
|
|
378
|
+
const executor = new UiFidelityRenderStepExecutor();
|
|
379
|
+
const step = await executor.execute({ package_path: './pkg', target: 'MyViews' }, {}, testConfig);
|
|
380
|
+
expect(step.script).not.toContain('packageSource');
|
|
381
|
+
});
|
|
382
|
+
test('explicit repo keeps the v1 inputs required and records the source', async () => {
|
|
383
|
+
const executor = new UiFidelityRenderStepExecutor();
|
|
384
|
+
const step = await executor.execute({ package_path: './pkg', target: 'MyViews', package_source: 'repo' }, {}, testConfig);
|
|
385
|
+
expect(step.kind).toBe('node');
|
|
386
|
+
expect(step.script).toContain('"packageSource":"repo"');
|
|
387
|
+
expect(step.script).toContain('"packagePath":"./pkg"');
|
|
388
|
+
await expect(executor.execute({ target: 'MyViews', package_source: 'repo' }, {}, testConfig)).rejects.toThrow("Missing required input 'package_path'");
|
|
389
|
+
await expect(executor.execute({ package_path: './pkg', package_source: 'repo' }, {}, testConfig)).rejects.toThrow("Missing required input 'target'");
|
|
390
|
+
});
|
|
391
|
+
test('inputs mode requires neither package_path nor target', async () => {
|
|
392
|
+
const script = await buildInputsScript();
|
|
393
|
+
expect(script).toContain('"packageSource":"inputs"');
|
|
394
|
+
expect(script).toContain('"packagePath":null');
|
|
395
|
+
expect(script).toContain('"target":null');
|
|
396
|
+
});
|
|
397
|
+
test('inputs mode ignores package_path and honors an explicit target', async () => {
|
|
398
|
+
const script = await buildInputsScript({ package_path: './pkg', target: 'MyViews' });
|
|
399
|
+
expect(script).toContain('"packagePath":null');
|
|
400
|
+
expect(script).toContain('"target":"MyViews"');
|
|
401
|
+
});
|
|
402
|
+
test('inputs mode still rejects a malformed target', async () => {
|
|
403
|
+
const executor = new UiFidelityRenderStepExecutor();
|
|
404
|
+
await expect(executor.execute({ package_source: 'inputs', target: 'My Views' }, {}, testConfig)).rejects.toThrow(/target/i);
|
|
405
|
+
});
|
|
406
|
+
test('validation requirements branch on the package source', () => {
|
|
407
|
+
const executor = new UiFidelityRenderStepExecutor();
|
|
408
|
+
const v1Names = executor
|
|
409
|
+
.getValidationRequirements({ package_path: './pkg', target: 'MyViews' }, {}, testConfig)
|
|
410
|
+
.map((requirement) => requirement.name);
|
|
411
|
+
expect(v1Names).toEqual(['package_path', 'target', 'swift']);
|
|
412
|
+
const inputsNames = executor
|
|
413
|
+
.getValidationRequirements({ package_source: 'inputs' }, {}, testConfig)
|
|
414
|
+
.map((requirement) => requirement.name);
|
|
415
|
+
expect(inputsNames).toEqual(['swift', 'tar']);
|
|
416
|
+
});
|
|
417
|
+
});
|
|
237
418
|
describe('render size and scale parsing', () => {
|
|
238
419
|
test('default render size is 393x852 device points', () => {
|
|
239
420
|
expect(DEFAULT_RENDER_SIZE).toBe('393x852');
|
|
@@ -692,6 +873,429 @@ describe('render script runtime', () => {
|
|
|
692
873
|
expect(readResult(project).screens[0].status).toBe('rendered');
|
|
693
874
|
});
|
|
694
875
|
});
|
|
876
|
+
describe('render script runtime (package_source: repo)', () => {
|
|
877
|
+
test('explicit repo renders like v1 and records the package source', async () => {
|
|
878
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
879
|
+
writeReference(project, 'home.png');
|
|
880
|
+
const script = await buildScript(project, { package_source: 'repo' });
|
|
881
|
+
const run = runScript(project, script);
|
|
882
|
+
expect(run.status).toBe(0);
|
|
883
|
+
expect(isPng(artifact(project, 'ui-fidelity/rendered/HomeView.png'))).toBe(true);
|
|
884
|
+
expect(readResult(project)).toEqual({
|
|
885
|
+
renderer: 'imagerenderer-spm',
|
|
886
|
+
package_source: 'repo',
|
|
887
|
+
error: null,
|
|
888
|
+
screens: [
|
|
889
|
+
{
|
|
890
|
+
screen: 'HomeView',
|
|
891
|
+
status: 'rendered',
|
|
892
|
+
error: null,
|
|
893
|
+
reference_image_path: 'ui-fidelity/references/HomeView.png',
|
|
894
|
+
rendered_image_path: 'ui-fidelity/rendered/HomeView.png',
|
|
895
|
+
},
|
|
896
|
+
],
|
|
897
|
+
});
|
|
898
|
+
});
|
|
899
|
+
test('compiles asset catalogs found in the package before rendering', async () => {
|
|
900
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
901
|
+
writeReference(project, 'home.png');
|
|
902
|
+
// A catalog at the package root: SwiftPM ignores it, so the renderer must
|
|
903
|
+
// compile it with actool for Image("name") to resolve at render time.
|
|
904
|
+
const catalog = join(project.packageDir, 'Assets', 'Media.xcassets');
|
|
905
|
+
mkdirSync(catalog, { recursive: true });
|
|
906
|
+
writeFileSync(join(catalog, 'Contents.json'), '{}');
|
|
907
|
+
const script = await buildScript(project, { package_source: 'repo' });
|
|
908
|
+
const xcrunLog = join(project.dir, 'xcrun-invocations.log');
|
|
909
|
+
const run = runScript(project, script, { FAKE_XCRUN_LOG: xcrunLog });
|
|
910
|
+
expect(run.status).toBe(0);
|
|
911
|
+
expect(isPng(artifact(project, 'ui-fidelity/rendered/HomeView.png'))).toBe(true);
|
|
912
|
+
const xcrunCalls = readFileSync(xcrunLog, 'utf-8');
|
|
913
|
+
expect(xcrunCalls).toContain('actool');
|
|
914
|
+
expect(xcrunCalls).toContain('Media.xcassets');
|
|
915
|
+
expect(xcrunCalls).toContain('--compile');
|
|
916
|
+
expect(xcrunCalls).toContain('--platform macosx');
|
|
917
|
+
});
|
|
918
|
+
test('does not invoke actool when the package has no asset catalogs', async () => {
|
|
919
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
920
|
+
writeReference(project, 'home.png');
|
|
921
|
+
const script = await buildScript(project, { package_source: 'repo' });
|
|
922
|
+
const xcrunLog = join(project.dir, 'xcrun-invocations.log');
|
|
923
|
+
const run = runScript(project, script, { FAKE_XCRUN_LOG: xcrunLog });
|
|
924
|
+
expect(run.status).toBe(0);
|
|
925
|
+
expect(isPng(artifact(project, 'ui-fidelity/rendered/HomeView.png'))).toBe(true);
|
|
926
|
+
expect(existsSync(xcrunLog)).toBe(false);
|
|
927
|
+
});
|
|
928
|
+
test('a failing asset-catalog compile warns but still renders (warn-and-continue)', async () => {
|
|
929
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
930
|
+
writeReference(project, 'home.png');
|
|
931
|
+
const catalog = join(project.packageDir, 'Media.xcassets');
|
|
932
|
+
mkdirSync(catalog, { recursive: true });
|
|
933
|
+
writeFileSync(join(catalog, 'Contents.json'), '{}');
|
|
934
|
+
const script = await buildScript(project, { package_source: 'repo' });
|
|
935
|
+
const run = runScript(project, script, { FAKE_ACTOOL_FAIL: '1' });
|
|
936
|
+
expect(run.status).toBe(0);
|
|
937
|
+
expect(isPng(artifact(project, 'ui-fidelity/rendered/HomeView.png'))).toBe(true);
|
|
938
|
+
expect(run.stderr).toContain('asset-catalog compile failed');
|
|
939
|
+
});
|
|
940
|
+
});
|
|
941
|
+
describe('render script runtime (package_source: inputs)', () => {
|
|
942
|
+
test('renders every screen from a shipped archive with a single package root', async () => {
|
|
943
|
+
const project = makeProject({
|
|
944
|
+
screens: { HomeView: 'home.png', SettingsView: 'settings.png' },
|
|
945
|
+
});
|
|
946
|
+
writeReference(project, 'home.png');
|
|
947
|
+
writeReference(project, 'settings.png');
|
|
948
|
+
stageArchive(project, shippedPackageEntries('pkg'));
|
|
949
|
+
const script = await buildInputsScript({ target: 'FixtureViews' });
|
|
950
|
+
const logPath = join(project.dir, 'swift-invocations.log');
|
|
951
|
+
const run = runScript(project, script, {
|
|
952
|
+
FAKE_SWIFT_LOG: logPath,
|
|
953
|
+
UI_FIDELITY_KEEP_HARNESS: '1',
|
|
954
|
+
});
|
|
955
|
+
expect(run.status).toBe(0);
|
|
956
|
+
expect(isPng(artifact(project, 'ui-fidelity/rendered/HomeView.png'))).toBe(true);
|
|
957
|
+
expect(isPng(artifact(project, 'ui-fidelity/rendered/SettingsView.png'))).toBe(true);
|
|
958
|
+
expect(readResult(project)).toEqual({
|
|
959
|
+
renderer: 'imagerenderer-spm',
|
|
960
|
+
package_source: 'inputs',
|
|
961
|
+
error: null,
|
|
962
|
+
screens: [
|
|
963
|
+
{
|
|
964
|
+
screen: 'HomeView',
|
|
965
|
+
status: 'rendered',
|
|
966
|
+
error: null,
|
|
967
|
+
reference_image_path: 'ui-fidelity/references/HomeView.png',
|
|
968
|
+
rendered_image_path: 'ui-fidelity/rendered/HomeView.png',
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
screen: 'SettingsView',
|
|
972
|
+
status: 'rendered',
|
|
973
|
+
error: null,
|
|
974
|
+
reference_image_path: 'ui-fidelity/references/SettingsView.png',
|
|
975
|
+
rendered_image_path: 'ui-fidelity/rendered/SettingsView.png',
|
|
976
|
+
},
|
|
977
|
+
],
|
|
978
|
+
});
|
|
979
|
+
// The synthesized harness depends on the EXTRACTED package, not on any
|
|
980
|
+
// path in the repo checkout.
|
|
981
|
+
const probeArgs = readFileSync(logPath, 'utf-8').trim().split('\n')[0].split(' ');
|
|
982
|
+
const harnessDir = probeArgs[probeArgs.indexOf('--package-path') + 1];
|
|
983
|
+
tempDirs.push(harnessDir);
|
|
984
|
+
const manifest = readFileSync(join(harnessDir, 'Package.swift'), 'utf-8');
|
|
985
|
+
expect(manifest).toMatch(/\.package\(path: "[^"]*ui-fidelity-package-[^"]*\/pkg"\)/);
|
|
986
|
+
expect(manifest).toContain('.product(name: "FixtureViews", package: "pkg")');
|
|
987
|
+
const extracted = manifest.match(/\.package\(path: "([^"]*)\/pkg"\)/);
|
|
988
|
+
if (extracted)
|
|
989
|
+
tempDirs.push(extracted[1]);
|
|
990
|
+
});
|
|
991
|
+
test('accepts Package.swift at the archive root', async () => {
|
|
992
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
993
|
+
writeReference(project, 'home.png');
|
|
994
|
+
stageArchive(project, shippedPackageEntries(''));
|
|
995
|
+
const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }));
|
|
996
|
+
expect(run.status).toBe(0);
|
|
997
|
+
const result = readResult(project);
|
|
998
|
+
expect(result.error).toBeNull();
|
|
999
|
+
expect(result.screens[0].status).toBe('rendered');
|
|
1000
|
+
});
|
|
1001
|
+
test('ignores macOS junk entries when locating the package root', async () => {
|
|
1002
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
1003
|
+
writeReference(project, 'home.png');
|
|
1004
|
+
stageArchive(project, [
|
|
1005
|
+
{ name: '.DS_Store', content: 'junk' },
|
|
1006
|
+
{ name: '._pkg', content: 'apple-double junk' },
|
|
1007
|
+
...shippedPackageEntries('pkg'),
|
|
1008
|
+
]);
|
|
1009
|
+
const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }));
|
|
1010
|
+
expect(run.status).toBe(0);
|
|
1011
|
+
expect(readResult(project).screens[0].status).toBe('rendered');
|
|
1012
|
+
});
|
|
1013
|
+
test('discovers the target from a single library product', async () => {
|
|
1014
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
1015
|
+
writeReference(project, 'home.png');
|
|
1016
|
+
stageArchive(project, shippedPackageEntries('pkg'));
|
|
1017
|
+
const logPath = join(project.dir, 'swift-invocations.log');
|
|
1018
|
+
const run = runScript(project, await buildInputsScript(), {
|
|
1019
|
+
FAKE_SWIFT_LOG: logPath,
|
|
1020
|
+
UI_FIDELITY_KEEP_HARNESS: '1',
|
|
1021
|
+
});
|
|
1022
|
+
expect(run.status).toBe(0);
|
|
1023
|
+
expect(readResult(project).screens[0].status).toBe('rendered');
|
|
1024
|
+
const invocations = readFileSync(logPath, 'utf-8').trim().split('\n');
|
|
1025
|
+
expect(invocations[0]).toContain('dump-package');
|
|
1026
|
+
const dumpArgs = invocations[0].split(' ');
|
|
1027
|
+
tempDirs.push(dumpArgs[dumpArgs.indexOf('--package-path') + 1]);
|
|
1028
|
+
// The discovered product is what the harness imports.
|
|
1029
|
+
const probeArgs = invocations[1].split(' ');
|
|
1030
|
+
const harnessDir = probeArgs[probeArgs.indexOf('--package-path') + 1];
|
|
1031
|
+
tempDirs.push(harnessDir);
|
|
1032
|
+
expect(readFileSync(join(harnessDir, 'Sources', 'Render_HomeView', 'Render.swift'), 'utf-8')).toContain('import FixtureViews');
|
|
1033
|
+
});
|
|
1034
|
+
test('an explicit target skips manifest discovery', async () => {
|
|
1035
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
1036
|
+
writeReference(project, 'home.png');
|
|
1037
|
+
stageArchive(project, shippedPackageEntries('pkg'));
|
|
1038
|
+
const logPath = join(project.dir, 'swift-invocations.log');
|
|
1039
|
+
const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }), { FAKE_SWIFT_LOG: logPath });
|
|
1040
|
+
expect(run.status).toBe(0);
|
|
1041
|
+
expect(readFileSync(logPath, 'utf-8')).not.toContain('dump-package');
|
|
1042
|
+
});
|
|
1043
|
+
test('cleans up the extraction scratch directory after rendering', async () => {
|
|
1044
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
1045
|
+
writeReference(project, 'home.png');
|
|
1046
|
+
stageArchive(project, shippedPackageEntries('pkg'));
|
|
1047
|
+
const logPath = join(project.dir, 'swift-invocations.log');
|
|
1048
|
+
const run = runScript(project, await buildInputsScript(), { FAKE_SWIFT_LOG: logPath });
|
|
1049
|
+
expect(run.status).toBe(0);
|
|
1050
|
+
// The dump-package invocation names the extracted package root.
|
|
1051
|
+
const dumpArgs = readFileSync(logPath, 'utf-8').trim().split('\n')[0].split(' ');
|
|
1052
|
+
const extractedRoot = dumpArgs[dumpArgs.indexOf('--package-path') + 1];
|
|
1053
|
+
expect(extractedRoot).toContain('ui-fidelity-package-');
|
|
1054
|
+
expect(existsSync(extractedRoot)).toBe(false);
|
|
1055
|
+
});
|
|
1056
|
+
test('missing archive yields PACKAGE_ARCHIVE_MISSING with every screen entry present', async () => {
|
|
1057
|
+
const project = makeProject({
|
|
1058
|
+
screens: { HomeView: 'home.png', SettingsView: 'settings.png' },
|
|
1059
|
+
});
|
|
1060
|
+
writeReference(project, 'home.png');
|
|
1061
|
+
writeReference(project, 'settings.png');
|
|
1062
|
+
const logPath = join(project.dir, 'swift-invocations.log');
|
|
1063
|
+
const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }), {
|
|
1064
|
+
FAKE_SWIFT_LOG: logPath,
|
|
1065
|
+
});
|
|
1066
|
+
expect(run.status).not.toBe(0);
|
|
1067
|
+
const result = readResult(project);
|
|
1068
|
+
expect(result.package_source).toBe('inputs');
|
|
1069
|
+
expect(result.error?.code).toBe('PACKAGE_ARCHIVE_MISSING');
|
|
1070
|
+
expect(result.error?.message).toContain('package.tar.gz');
|
|
1071
|
+
// v1 invariant: every screen entry is present even on build-level
|
|
1072
|
+
// failure, with the build error kept distinct from per-screen errors.
|
|
1073
|
+
expect(result.screens).toHaveLength(2);
|
|
1074
|
+
for (const screen of result.screens) {
|
|
1075
|
+
expect(screen.status).toBe('render_failed');
|
|
1076
|
+
expect(screen.error).toBeNull();
|
|
1077
|
+
expect(screen.reference_image_path).not.toBeNull();
|
|
1078
|
+
expect(screen.rendered_image_path).toBeNull();
|
|
1079
|
+
}
|
|
1080
|
+
// The harness never builds: swift is never invoked.
|
|
1081
|
+
expect(existsSync(logPath)).toBe(false);
|
|
1082
|
+
expectNoAbsolutePathLeaks(project, result);
|
|
1083
|
+
});
|
|
1084
|
+
test('a corrupt archive yields PACKAGE_ARCHIVE_INVALID', async () => {
|
|
1085
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
1086
|
+
writeReference(project, 'home.png');
|
|
1087
|
+
stageArchive(project, Buffer.from('definitely not a gzipped tarball'));
|
|
1088
|
+
const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }));
|
|
1089
|
+
expect(run.status).not.toBe(0);
|
|
1090
|
+
const result = readResult(project);
|
|
1091
|
+
expect(result.error?.code).toBe('PACKAGE_ARCHIVE_INVALID');
|
|
1092
|
+
expect(result.screens).toHaveLength(1);
|
|
1093
|
+
expect(result.screens[0].error).toBeNull();
|
|
1094
|
+
expectNoAbsolutePathLeaks(project, result);
|
|
1095
|
+
});
|
|
1096
|
+
test('an archive with dot-dot or absolute members yields PACKAGE_ARCHIVE_INVALID', async () => {
|
|
1097
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
1098
|
+
writeReference(project, 'home.png');
|
|
1099
|
+
const script = await buildInputsScript({ target: 'FixtureViews' });
|
|
1100
|
+
stageArchive(project, [
|
|
1101
|
+
...shippedPackageEntries('pkg'),
|
|
1102
|
+
{ name: 'pkg/../escape.swift', content: '// escape' },
|
|
1103
|
+
]);
|
|
1104
|
+
let run = runScript(project, script);
|
|
1105
|
+
expect(run.status).not.toBe(0);
|
|
1106
|
+
let result = readResult(project);
|
|
1107
|
+
expect(result.error?.code).toBe('PACKAGE_ARCHIVE_INVALID');
|
|
1108
|
+
expect(result.error?.message).toContain('escape.swift');
|
|
1109
|
+
stageArchive(project, [
|
|
1110
|
+
...shippedPackageEntries('pkg'),
|
|
1111
|
+
{ name: '/abs/escape.swift', content: '// escape' },
|
|
1112
|
+
]);
|
|
1113
|
+
run = runScript(project, script);
|
|
1114
|
+
expect(run.status).not.toBe(0);
|
|
1115
|
+
result = readResult(project);
|
|
1116
|
+
expect(result.error?.code).toBe('PACKAGE_ARCHIVE_INVALID');
|
|
1117
|
+
expectNoAbsolutePathLeaks(project, result);
|
|
1118
|
+
});
|
|
1119
|
+
test('an empty archive yields PACKAGE_ARCHIVE_INVALID', async () => {
|
|
1120
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
1121
|
+
writeReference(project, 'home.png');
|
|
1122
|
+
stageArchive(project, []);
|
|
1123
|
+
const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }));
|
|
1124
|
+
expect(run.status).not.toBe(0);
|
|
1125
|
+
expect(readResult(project).error?.code).toBe('PACKAGE_ARCHIVE_INVALID');
|
|
1126
|
+
});
|
|
1127
|
+
test('multiple top-level roots yield PACKAGE_ARCHIVE_INVALID', async () => {
|
|
1128
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
1129
|
+
writeReference(project, 'home.png');
|
|
1130
|
+
stageArchive(project, [
|
|
1131
|
+
...shippedPackageEntries('pkg-one'),
|
|
1132
|
+
...shippedPackageEntries('pkg-two'),
|
|
1133
|
+
]);
|
|
1134
|
+
const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }));
|
|
1135
|
+
expect(run.status).not.toBe(0);
|
|
1136
|
+
const result = readResult(project);
|
|
1137
|
+
expect(result.error?.code).toBe('PACKAGE_ARCHIVE_INVALID');
|
|
1138
|
+
expect(result.error?.message).toContain('pkg-one');
|
|
1139
|
+
expect(result.error?.message).toContain('pkg-two');
|
|
1140
|
+
expectNoAbsolutePathLeaks(project, result);
|
|
1141
|
+
});
|
|
1142
|
+
test('a single top-level file yields PACKAGE_ARCHIVE_INVALID', async () => {
|
|
1143
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
1144
|
+
writeReference(project, 'home.png');
|
|
1145
|
+
stageArchive(project, [{ name: 'HomeView.swift', content: 'import SwiftUI\n' }]);
|
|
1146
|
+
const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }));
|
|
1147
|
+
expect(run.status).not.toBe(0);
|
|
1148
|
+
expect(readResult(project).error?.code).toBe('PACKAGE_ARCHIVE_INVALID');
|
|
1149
|
+
});
|
|
1150
|
+
test('a package root without Package.swift yields PACKAGE_MANIFEST_MISSING', async () => {
|
|
1151
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
1152
|
+
writeReference(project, 'home.png');
|
|
1153
|
+
// The manifest exists, but only deeper than the single top-level root.
|
|
1154
|
+
stageArchive(project, [
|
|
1155
|
+
{ name: 'pkg/' },
|
|
1156
|
+
{ name: 'pkg/Nested/Package.swift', content: SHIPPED_MANIFEST },
|
|
1157
|
+
{ name: 'pkg/Nested/Sources/FixtureViews/HomeView.swift', content: 'import SwiftUI\n' },
|
|
1158
|
+
]);
|
|
1159
|
+
const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }));
|
|
1160
|
+
expect(run.status).not.toBe(0);
|
|
1161
|
+
const result = readResult(project);
|
|
1162
|
+
expect(result.error?.code).toBe('PACKAGE_MANIFEST_MISSING');
|
|
1163
|
+
expectNoAbsolutePathLeaks(project, result);
|
|
1164
|
+
});
|
|
1165
|
+
test('an extraction larger than the size bound yields PACKAGE_ARCHIVE_TOO_LARGE', async () => {
|
|
1166
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
1167
|
+
writeReference(project, 'home.png');
|
|
1168
|
+
stageArchive(project, shippedPackageEntries('pkg'));
|
|
1169
|
+
const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }), {
|
|
1170
|
+
UI_FIDELITY_MAX_PACKAGE_BYTES: '64',
|
|
1171
|
+
});
|
|
1172
|
+
expect(run.status).not.toBe(0);
|
|
1173
|
+
const result = readResult(project);
|
|
1174
|
+
expect(result.error?.code).toBe('PACKAGE_ARCHIVE_TOO_LARGE');
|
|
1175
|
+
expect(result.error?.message).toContain('64');
|
|
1176
|
+
expectNoAbsolutePathLeaks(project, result);
|
|
1177
|
+
});
|
|
1178
|
+
test('the default extraction bound is 32 MiB', async () => {
|
|
1179
|
+
const script = await buildInputsScript({ target: 'FixtureViews' });
|
|
1180
|
+
expect(script).toContain('32 * 1024 * 1024');
|
|
1181
|
+
});
|
|
1182
|
+
test('archive validation failures fail the step even with zero screens', async () => {
|
|
1183
|
+
const project = makeProject({ screens: {} });
|
|
1184
|
+
const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }));
|
|
1185
|
+
expect(run.status).not.toBe(0);
|
|
1186
|
+
const result = readResult(project);
|
|
1187
|
+
expect(result.error?.code).toBe('PACKAGE_ARCHIVE_MISSING');
|
|
1188
|
+
expect(result.screens).toEqual([]);
|
|
1189
|
+
});
|
|
1190
|
+
test('an empty screens object with a valid archive still exits zero', async () => {
|
|
1191
|
+
const project = makeProject({ screens: {} });
|
|
1192
|
+
stageArchive(project, shippedPackageEntries('pkg'));
|
|
1193
|
+
const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }));
|
|
1194
|
+
expect(run.status).toBe(0);
|
|
1195
|
+
expect(readResult(project)).toEqual({
|
|
1196
|
+
renderer: 'imagerenderer-spm',
|
|
1197
|
+
package_source: 'inputs',
|
|
1198
|
+
error: null,
|
|
1199
|
+
screens: [],
|
|
1200
|
+
});
|
|
1201
|
+
});
|
|
1202
|
+
test('absent params.json still records the package source with empty screens', async () => {
|
|
1203
|
+
const project = makeProject();
|
|
1204
|
+
stageArchive(project, shippedPackageEntries('pkg'));
|
|
1205
|
+
const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }));
|
|
1206
|
+
expect(run.status).not.toBe(0);
|
|
1207
|
+
expect(readResult(project)).toEqual({
|
|
1208
|
+
renderer: 'imagerenderer-spm',
|
|
1209
|
+
package_source: 'inputs',
|
|
1210
|
+
error: null,
|
|
1211
|
+
screens: [],
|
|
1212
|
+
});
|
|
1213
|
+
});
|
|
1214
|
+
test('zero library products yield PACKAGE_TARGET_UNRESOLVED', async () => {
|
|
1215
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
1216
|
+
writeReference(project, 'home.png');
|
|
1217
|
+
stageArchive(project, shippedPackageEntries('pkg'));
|
|
1218
|
+
const run = runScript(project, await buildInputsScript(), {
|
|
1219
|
+
FAKE_SWIFT_DUMP_PRODUCTS: '',
|
|
1220
|
+
});
|
|
1221
|
+
expect(run.status).not.toBe(0);
|
|
1222
|
+
const result = readResult(project);
|
|
1223
|
+
expect(result.error?.code).toBe('PACKAGE_TARGET_UNRESOLVED');
|
|
1224
|
+
expect(result.error?.message).toContain('no library products');
|
|
1225
|
+
});
|
|
1226
|
+
test('multiple library products yield PACKAGE_TARGET_UNRESOLVED naming the candidates', async () => {
|
|
1227
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
1228
|
+
writeReference(project, 'home.png');
|
|
1229
|
+
stageArchive(project, shippedPackageEntries('pkg'));
|
|
1230
|
+
const run = runScript(project, await buildInputsScript(), {
|
|
1231
|
+
FAKE_SWIFT_DUMP_PRODUCTS: 'AlphaViews,BetaViews',
|
|
1232
|
+
});
|
|
1233
|
+
expect(run.status).not.toBe(0);
|
|
1234
|
+
const result = readResult(project);
|
|
1235
|
+
expect(result.error?.code).toBe('PACKAGE_TARGET_UNRESOLVED');
|
|
1236
|
+
expect(result.error?.message).toContain('AlphaViews');
|
|
1237
|
+
expect(result.error?.message).toContain('BetaViews');
|
|
1238
|
+
});
|
|
1239
|
+
test('a failing dump-package yields PACKAGE_TARGET_UNRESOLVED', async () => {
|
|
1240
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
1241
|
+
writeReference(project, 'home.png');
|
|
1242
|
+
stageArchive(project, shippedPackageEntries('pkg'));
|
|
1243
|
+
const run = runScript(project, await buildInputsScript(), {
|
|
1244
|
+
FAKE_SWIFT_DUMP_FAIL: '1',
|
|
1245
|
+
});
|
|
1246
|
+
expect(run.status).not.toBe(0);
|
|
1247
|
+
const result = readResult(project);
|
|
1248
|
+
expect(result.error?.code).toBe('PACKAGE_TARGET_UNRESOLVED');
|
|
1249
|
+
expect(result.error?.message).toContain('malformed Package.swift manifest');
|
|
1250
|
+
expectNoAbsolutePathLeaks(project, result);
|
|
1251
|
+
});
|
|
1252
|
+
test('a discovered product that is not a Swift identifier yields PACKAGE_TARGET_UNRESOLVED', async () => {
|
|
1253
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
1254
|
+
writeReference(project, 'home.png');
|
|
1255
|
+
stageArchive(project, shippedPackageEntries('pkg'));
|
|
1256
|
+
const run = runScript(project, await buildInputsScript(), {
|
|
1257
|
+
FAKE_SWIFT_DUMP_PRODUCTS: 'My-Views',
|
|
1258
|
+
});
|
|
1259
|
+
expect(run.status).not.toBe(0);
|
|
1260
|
+
expect(readResult(project).error?.code).toBe('PACKAGE_TARGET_UNRESOLVED');
|
|
1261
|
+
});
|
|
1262
|
+
test('a broken screen fails alone in inputs mode', async () => {
|
|
1263
|
+
const project = makeProject({
|
|
1264
|
+
screens: { HomeView: 'home.png', MissingView: 'missing-view.png' },
|
|
1265
|
+
});
|
|
1266
|
+
writeReference(project, 'home.png');
|
|
1267
|
+
writeReference(project, 'missing-view.png');
|
|
1268
|
+
stageArchive(project, shippedPackageEntries('pkg'));
|
|
1269
|
+
const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }), {
|
|
1270
|
+
FAKE_SWIFT_COMPILE_FAIL: 'MissingView',
|
|
1271
|
+
});
|
|
1272
|
+
expect(run.status).not.toBe(0);
|
|
1273
|
+
const result = readResult(project);
|
|
1274
|
+
// Per-screen isolation is unchanged: the break is per-screen, the
|
|
1275
|
+
// build-level error stays null.
|
|
1276
|
+
expect(result.error).toBeNull();
|
|
1277
|
+
const [home, missing] = result.screens;
|
|
1278
|
+
expect(home.status).toBe('rendered');
|
|
1279
|
+
expect(isPng(artifact(project, 'ui-fidelity/rendered/HomeView.png'))).toBe(true);
|
|
1280
|
+
expect(missing.status).toBe('render_failed');
|
|
1281
|
+
expect(missing.error?.code).toBe('VIEW_COMPILE_FAILED');
|
|
1282
|
+
expectNoAbsolutePathLeaks(project, result);
|
|
1283
|
+
});
|
|
1284
|
+
test('a shipped package that does not build for macOS is sanitized to <package>', async () => {
|
|
1285
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
1286
|
+
writeReference(project, 'home.png');
|
|
1287
|
+
stageArchive(project, shippedPackageEntries('pkg'));
|
|
1288
|
+
const run = runScript(project, await buildInputsScript({ target: 'FixtureViews' }), {
|
|
1289
|
+
FAKE_SWIFT_PROBE_FAIL: '1',
|
|
1290
|
+
});
|
|
1291
|
+
expect(run.status).not.toBe(0);
|
|
1292
|
+
const result = readResult(project);
|
|
1293
|
+
expect(result.error).toBeNull();
|
|
1294
|
+
expect(result.screens[0].error?.code).toBe('RENDER_UNSUPPORTED');
|
|
1295
|
+
expect(result.screens[0].error?.message).toContain('<package>/pkg');
|
|
1296
|
+
expectNoAbsolutePathLeaks(project, result);
|
|
1297
|
+
});
|
|
1298
|
+
});
|
|
695
1299
|
describe('committed fixture package', () => {
|
|
696
1300
|
test('declares the FixtureViews library with the screens the example pipeline uses', () => {
|
|
697
1301
|
const manifest = readFileSync(join(FIXTURE_PACKAGE, 'Package.swift'), 'utf-8');
|