@invarn/cibuild 2.0.8 → 2.1.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.
Files changed (29) hide show
  1. package/dist/cli.cjs +688 -69
  2. package/dist/src/lib.d.ts +2 -0
  3. package/dist/src/lib.d.ts.map +1 -1
  4. package/dist/src/lib.js +1 -0
  5. package/dist/src/yaml/steps/index.d.ts.map +1 -1
  6. package/dist/src/yaml/steps/index.js +22 -0
  7. package/dist/src/yaml/steps/render-post-processor.d.ts +73 -0
  8. package/dist/src/yaml/steps/render-post-processor.d.ts.map +1 -0
  9. package/dist/src/yaml/steps/render-post-processor.js +303 -0
  10. package/dist/src/yaml/steps/render-post-processor.test.d.ts +16 -0
  11. package/dist/src/yaml/steps/render-post-processor.test.d.ts.map +1 -0
  12. package/dist/src/yaml/steps/render-post-processor.test.js +238 -0
  13. package/dist/src/yaml/steps/ui-fidelity-preview-android.d.ts +66 -0
  14. package/dist/src/yaml/steps/ui-fidelity-preview-android.d.ts.map +1 -0
  15. package/dist/src/yaml/steps/ui-fidelity-preview-android.js +249 -0
  16. package/dist/src/yaml/steps/ui-fidelity-preview-android.test.d.ts +13 -0
  17. package/dist/src/yaml/steps/ui-fidelity-preview-android.test.d.ts.map +1 -0
  18. package/dist/src/yaml/steps/ui-fidelity-preview-android.test.js +299 -0
  19. package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts +92 -0
  20. package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts.map +1 -0
  21. package/dist/src/yaml/steps/ui-fidelity-render-android.js +726 -0
  22. package/dist/src/yaml/steps/ui-fidelity-render-android.test.d.ts +2 -0
  23. package/dist/src/yaml/steps/ui-fidelity-render-android.test.d.ts.map +1 -0
  24. package/dist/src/yaml/steps/ui-fidelity-render-android.test.js +377 -0
  25. package/dist/src/yaml/steps/ui-fidelity-render.d.ts +0 -28
  26. package/dist/src/yaml/steps/ui-fidelity-render.d.ts.map +1 -1
  27. package/dist/src/yaml/steps/ui-fidelity-render.js +1 -259
  28. package/dist/src/yaml/steps/ui-fidelity-render.test.js +1 -57
  29. package/package.json +1 -1
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ui-fidelity-render-android.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui-fidelity-render-android.test.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ui-fidelity-render-android.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,377 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { chmodSync, cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
3
+ import { gzipSync } from 'node:zlib';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { ANDROID_RENDERER, UiFidelityRenderAndroidStepExecutor, } from './ui-fidelity-render-android.js';
8
+ import { testConfig } from './test-config.js';
9
+ const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
10
+ const tempDirs = [];
11
+ afterAll(() => {
12
+ for (const dir of tempDirs) {
13
+ try {
14
+ rmSync(dir, { recursive: true, force: true });
15
+ }
16
+ catch {
17
+ // best effort
18
+ }
19
+ }
20
+ });
21
+ // ---- fake toolchain ----------------------------------------------------------
22
+ // Fake `gradlew`, shipped inside the archive at the project root. With
23
+ // FAKE_GRADLE_FAIL set it prints a compiler-style diagnostic and exits 1
24
+ // (the project-does-not-build path). Otherwise it writes one PNG per screen
25
+ // named in FAKE_ROBORAZZI_SCREENS (comma-separated) to
26
+ // proof/build/outputs/roborazzi/<Screen>.png, mimicking a Roborazzi record.
27
+ const FAKE_GRADLEW_LINES = [
28
+ '#!/bin/bash',
29
+ '[ -n "$FAKE_GRADLE_LOG" ] && echo "$*" >> "$FAKE_GRADLE_LOG"',
30
+ 'if [ -n "$FAKE_GRADLE_FAIL" ]; then',
31
+ ' echo "> Task :proof:compileDebugKotlin FAILED" >&2',
32
+ ' echo "e: XProof.kt:7:20 unresolved reference: Missing" >&2',
33
+ ' exit 1',
34
+ 'fi',
35
+ 'out="proof/build/outputs/roborazzi"',
36
+ 'mkdir -p "$out"',
37
+ 'IFS=","',
38
+ 'for s in $FAKE_ROBORAZZI_SCREENS; do',
39
+ ' [ -z "$s" ] && continue',
40
+ " printf '\\x89PNG\\r\\n\\x1a\\n' > \"$out/$s.png\"",
41
+ ' printf \'roborazzi:%s\' "$s" >> "$out/$s.png"',
42
+ 'done',
43
+ 'exit 0',
44
+ ];
45
+ // Fake `swift`, providing only the script-interpreter branch the shared trim
46
+ // stage uses: `swift <Trim.swift> <in> <out> <aligned> <ref>`. Overwrites
47
+ // <out> with a "trimmed:<in basename>" PNG, writes an "aligned:<in basename>"
48
+ // PNG, and emits a fixed TRIMDIMS line. FAKE_TRIM_FAIL exercises warn-and-continue.
49
+ const FAKE_SWIFT_LINES = [
50
+ '#!/bin/bash',
51
+ 'case "$1" in',
52
+ ' *.swift)',
53
+ ' if [ -n "$FAKE_TRIM_FAIL" ]; then echo "trim: simulated failure" >&2; exit 1; fi',
54
+ ' in_png="$2"; out_png="$3"; aligned_png="$4"',
55
+ " printf '\\x89PNG\\r\\n\\x1a\\n' > \"$out_png\"",
56
+ ' printf \'trimmed:%s\' "$(basename "$in_png")" >> "$out_png"',
57
+ ' if [ -n "$aligned_png" ]; then',
58
+ " printf '\\x89PNG\\r\\n\\x1a\\n' > \"$aligned_png\"",
59
+ ' printf \'aligned:%s\' "$(basename "$in_png")" >> "$aligned_png"',
60
+ ' fi',
61
+ ' echo "TRIMDIMS 320 640 160 320 400 800"',
62
+ ' exit 0 ;;',
63
+ ' *) exit 0 ;;',
64
+ 'esac',
65
+ ];
66
+ function makeProject(params) {
67
+ const dir = mkdtempSync(join(tmpdir(), 'ui-fidelity-android-'));
68
+ tempDirs.push(dir);
69
+ mkdirSync(join(dir, '.ci', 'inputs'), { recursive: true });
70
+ const binDir = join(dir, 'bin');
71
+ mkdirSync(binDir, { recursive: true });
72
+ const swiftPath = join(binDir, 'swift');
73
+ writeFileSync(swiftPath, FAKE_SWIFT_LINES.join('\n') + '\n');
74
+ chmodSync(swiftPath, 0o755);
75
+ if (params !== undefined) {
76
+ writeFileSync(join(dir, '.ci', 'inputs', 'params.json'), typeof params === 'string' ? params : JSON.stringify(params));
77
+ }
78
+ return { dir, binDir };
79
+ }
80
+ function writeReference(project, basename, label = basename) {
81
+ writeFileSync(join(project.dir, '.ci', 'inputs', basename), Buffer.concat([PNG_MAGIC, Buffer.from('ref:' + label)]));
82
+ }
83
+ function isPng(filePath) {
84
+ try {
85
+ return readFileSync(filePath).subarray(0, 8).equals(PNG_MAGIC);
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ }
91
+ function artifact(project, relative) {
92
+ return join(project.dir, '.ci', 'artifacts', relative);
93
+ }
94
+ function tarHeader(name, size, typeflag, mode) {
95
+ const header = Buffer.alloc(512);
96
+ header.write(name, 0, 100, 'utf-8');
97
+ header.write(mode.toString(8).padStart(7, '0') + '\0', 100, 8, 'ascii');
98
+ header.write('0000000\0', 108, 8, 'ascii');
99
+ header.write('0000000\0', 116, 8, 'ascii');
100
+ header.write(size.toString(8).padStart(11, '0') + '\0', 124, 12, 'ascii');
101
+ header.write('00000000000\0', 136, 12, 'ascii');
102
+ header.write(' ', 148, 8, 'ascii');
103
+ header.write(typeflag, 156, 1, 'ascii');
104
+ header.write('ustar\0' + '00', 257, 8, 'ascii');
105
+ let checksum = 0;
106
+ for (const byte of header)
107
+ checksum += byte;
108
+ header.write(checksum.toString(8).padStart(6, '0') + '\0 ', 148, 8, 'ascii');
109
+ return header;
110
+ }
111
+ function makeTarGz(entries) {
112
+ const blocks = [];
113
+ for (const entry of entries) {
114
+ const isDir = entry.name.endsWith('/');
115
+ const content = Buffer.isBuffer(entry.content)
116
+ ? entry.content
117
+ : Buffer.from(entry.content ?? '', 'utf-8');
118
+ const mode = entry.mode ?? (isDir ? 0o755 : 0o644);
119
+ blocks.push(tarHeader(entry.name, isDir ? 0 : content.length, isDir ? '5' : '0', mode));
120
+ if (!isDir && content.length > 0) {
121
+ const padded = Buffer.alloc(Math.ceil(content.length / 512) * 512);
122
+ content.copy(padded);
123
+ blocks.push(padded);
124
+ }
125
+ }
126
+ blocks.push(Buffer.alloc(1024));
127
+ return gzipSync(Buffer.concat(blocks));
128
+ }
129
+ /** Entries for a well-formed shipped Gradle project under the given root. */
130
+ function gradleProjectEntries(root = 'project') {
131
+ const prefix = root === '' ? '' : root + '/';
132
+ const entries = root === '' ? [] : [{ name: prefix }];
133
+ entries.push({ name: prefix + 'settings.gradle.kts', content: 'rootProject.name = "p"\ninclude(":proof")\n' }, { name: prefix + 'gradlew', content: FAKE_GRADLEW_LINES.join('\n') + '\n', mode: 0o755 }, { name: prefix + 'proof/build.gradle.kts', content: '// module\n' });
134
+ return entries;
135
+ }
136
+ function stageArchive(project, archive) {
137
+ writeFileSync(join(project.dir, '.ci', 'inputs', 'package.tar.gz'), Buffer.isBuffer(archive) ? archive : makeTarGz(archive));
138
+ }
139
+ // ---- script build + run ------------------------------------------------------
140
+ async function buildScript(inputs = {}) {
141
+ const executor = new UiFidelityRenderAndroidStepExecutor();
142
+ const step = await executor.execute(inputs, {}, testConfig);
143
+ return step.script;
144
+ }
145
+ function runScript(project, script, env = {}) {
146
+ return spawnSync('node', ['-e', script], {
147
+ cwd: project.dir,
148
+ encoding: 'utf-8',
149
+ env: {
150
+ ...process.env,
151
+ PATH: project.binDir + ':' + (process.env.PATH ?? ''),
152
+ ...env,
153
+ },
154
+ });
155
+ }
156
+ function readResult(project) {
157
+ return JSON.parse(readFileSync(join(project.dir, '.ci', 'artifacts', 'protocol-result.json'), 'utf-8'));
158
+ }
159
+ describe('UiFidelityRenderAndroidStepExecutor', () => {
160
+ test('returns a node StepDef named ui-fidelity-render-android', async () => {
161
+ const executor = new UiFidelityRenderAndroidStepExecutor();
162
+ const step = await executor.execute({}, {}, testConfig);
163
+ expect(step.kind).toBe('node');
164
+ expect(step.name).toBe('ui-fidelity-render-android');
165
+ expect(step.script.length).toBeGreaterThan(0);
166
+ });
167
+ });
168
+ describe('android render runtime — params validation', () => {
169
+ test('writes a contract-shaped, empty-screens result and fails when params.json is absent', async () => {
170
+ const project = makeProject();
171
+ const script = await buildScript();
172
+ const run = runScript(project, script);
173
+ expect(run.status).toBe(1);
174
+ const doc = readResult(project);
175
+ expect(doc.renderer).toBe(ANDROID_RENDERER);
176
+ expect(doc.error).toBeNull();
177
+ expect(doc.screens).toEqual([]);
178
+ });
179
+ test('fails when params.json is malformed', async () => {
180
+ const project = makeProject('{ not json');
181
+ const run = runScript(project, await buildScript());
182
+ expect(run.status).toBe(1);
183
+ expect(readResult(project).screens).toEqual([]);
184
+ });
185
+ });
186
+ describe('android render runtime — reference setup', () => {
187
+ test('records REFERENCE_MISSING for a screen whose reference is absent', async () => {
188
+ const project = makeProject({ screens: { XProof: 'x.png' } });
189
+ const run = runScript(project, await buildScript());
190
+ expect(run.status).toBe(1);
191
+ const screen = readResult(project).screens.find((s) => s.screen === 'XProof');
192
+ expect(screen?.error?.code).toBe('REFERENCE_MISSING');
193
+ expect(screen?.reference_image_path).toBeNull();
194
+ });
195
+ test('copies a present reference into the artifacts and records its path', async () => {
196
+ const project = makeProject({ screens: { XProof: 'x.png' } });
197
+ writeReference(project, 'x.png');
198
+ const run = runScript(project, await buildScript());
199
+ const screen = readResult(project).screens.find((s) => s.screen === 'XProof');
200
+ expect(screen?.reference_image_path).toBe('ui-fidelity/references/XProof.png');
201
+ expect(isPng(artifact(project, 'ui-fidelity/references/XProof.png'))).toBe(true);
202
+ });
203
+ });
204
+ describe('android render runtime — archive validation', () => {
205
+ function projectWithRef() {
206
+ const project = makeProject({ screens: { XProof: 'x.png' } });
207
+ writeReference(project, 'x.png');
208
+ return project;
209
+ }
210
+ test('missing archive yields ANDROID_ARCHIVE_MISSING with every screen entry present', async () => {
211
+ const project = projectWithRef();
212
+ const run = runScript(project, await buildScript());
213
+ expect(run.status).toBe(1);
214
+ const doc = readResult(project);
215
+ expect(doc.error?.code).toBe('ANDROID_ARCHIVE_MISSING');
216
+ expect(doc.screens.map((s) => s.screen)).toEqual(['XProof']);
217
+ });
218
+ test('a corrupt archive yields ANDROID_ARCHIVE_INVALID', async () => {
219
+ const project = projectWithRef();
220
+ stageArchive(project, Buffer.from('not a gzip'));
221
+ const run = runScript(project, await buildScript());
222
+ expect(readResult(project).error?.code).toBe('ANDROID_ARCHIVE_INVALID');
223
+ expect(run.status).toBe(1);
224
+ });
225
+ test('an archive with dot-dot members yields ANDROID_ARCHIVE_INVALID', async () => {
226
+ const project = projectWithRef();
227
+ stageArchive(project, [{ name: '../escape.txt', content: 'x' }]);
228
+ runScript(project, await buildScript());
229
+ expect(readResult(project).error?.code).toBe('ANDROID_ARCHIVE_INVALID');
230
+ });
231
+ test('an extraction larger than the size bound yields ANDROID_ARCHIVE_TOO_LARGE', async () => {
232
+ const project = projectWithRef();
233
+ stageArchive(project, gradleProjectEntries());
234
+ runScript(project, await buildScript(), { UI_FIDELITY_MAX_PACKAGE_BYTES: '10' });
235
+ expect(readResult(project).error?.code).toBe('ANDROID_ARCHIVE_TOO_LARGE');
236
+ });
237
+ test('an archive without a Gradle settings file yields ANDROID_PROJECT_ROOT_MISSING', async () => {
238
+ const project = projectWithRef();
239
+ stageArchive(project, [
240
+ { name: 'project/' },
241
+ { name: 'project/build.gradle.kts', content: '// no settings\n' },
242
+ ]);
243
+ runScript(project, await buildScript());
244
+ expect(readResult(project).error?.code).toBe('ANDROID_PROJECT_ROOT_MISSING');
245
+ });
246
+ test('archive validation failures fail the step even with zero screens', async () => {
247
+ const project = makeProject({ screens: {} });
248
+ const run = runScript(project, await buildScript());
249
+ expect(run.status).toBe(1);
250
+ expect(readResult(project).error?.code).toBe('ANDROID_ARCHIVE_MISSING');
251
+ });
252
+ });
253
+ describe('android render runtime — render + trim (happy path)', () => {
254
+ test('renders each screen, trims via the shared post-processor, and records the contract shape', async () => {
255
+ const project = makeProject({ screens: { XProof: 'x.png' } });
256
+ writeReference(project, 'x.png');
257
+ stageArchive(project, gradleProjectEntries());
258
+ const run = runScript(project, await buildScript(), { FAKE_ROBORAZZI_SCREENS: 'XProof' });
259
+ expect(run.status).toBe(0);
260
+ const doc = readResult(project);
261
+ expect(doc.error).toBeNull();
262
+ const screen = doc.screens.find((s) => s.screen === 'XProof');
263
+ expect(screen?.status).toBe('rendered');
264
+ expect(screen?.error).toBeNull();
265
+ expect(screen?.rendered_image_path).toBe('ui-fidelity/rendered/XProof.png');
266
+ expect(screen?.aligned_image_path).toBe('ui-fidelity/aligned/XProof.png');
267
+ // dimensions from the (fake) trim helper's TRIMDIMS, logical_pt = px / scale 2.
268
+ expect(screen?.dimensions).toEqual({
269
+ rendered_px: [320, 640],
270
+ logical_pt: [160, 320],
271
+ reference_px: [160, 320],
272
+ });
273
+ expect(isPng(artifact(project, 'ui-fidelity/rendered/XProof.png'))).toBe(true);
274
+ expect(isPng(artifact(project, 'ui-fidelity/aligned/XProof.png'))).toBe(true);
275
+ // The rendered file holds the trim helper's output, not the raw render.
276
+ expect(readFileSync(artifact(project, 'ui-fidelity/rendered/XProof.png'), 'utf-8')).toContain('trimmed:XProof.png');
277
+ });
278
+ test('renders multiple screens independently in one Gradle run', async () => {
279
+ const project = makeProject({ screens: { XProof: 'x.png', YProof: 'y.png' } });
280
+ writeReference(project, 'x.png');
281
+ writeReference(project, 'y.png');
282
+ stageArchive(project, gradleProjectEntries());
283
+ const run = runScript(project, await buildScript(), { FAKE_ROBORAZZI_SCREENS: 'XProof,YProof' });
284
+ expect(run.status).toBe(0);
285
+ const doc = readResult(project);
286
+ expect(doc.screens.map((s) => s.status)).toEqual(['rendered', 'rendered']);
287
+ });
288
+ });
289
+ describe('android render runtime — error isolation', () => {
290
+ function twoScreenProject() {
291
+ const project = makeProject({ screens: { XProof: 'x.png', YProof: 'y.png' } });
292
+ writeReference(project, 'x.png');
293
+ writeReference(project, 'y.png');
294
+ stageArchive(project, gradleProjectEntries());
295
+ return project;
296
+ }
297
+ test('a Gradle build failure marks every screen RENDER_UNSUPPORTED', async () => {
298
+ const project = twoScreenProject();
299
+ const run = runScript(project, await buildScript(), { FAKE_GRADLE_FAIL: '1' });
300
+ expect(run.status).toBe(1);
301
+ const doc = readResult(project);
302
+ // Build-level failure is reported per screen (not an archive-level error).
303
+ expect(doc.error).toBeNull();
304
+ for (const screen of doc.screens) {
305
+ expect(screen.status).toBe('render_failed');
306
+ expect(screen.error?.code).toBe('RENDER_UNSUPPORTED');
307
+ expect(screen.error?.message).toContain('did not build');
308
+ }
309
+ });
310
+ test('a screen whose PNG is not produced is RENDER_FAILED while others render', async () => {
311
+ const project = twoScreenProject();
312
+ // Only XProof records a PNG; YProof's is absent.
313
+ const run = runScript(project, await buildScript(), { FAKE_ROBORAZZI_SCREENS: 'XProof' });
314
+ expect(run.status).toBe(1);
315
+ const doc = readResult(project);
316
+ const x = doc.screens.find((s) => s.screen === 'XProof');
317
+ const y = doc.screens.find((s) => s.screen === 'YProof');
318
+ expect(x?.status).toBe('rendered');
319
+ expect(y?.status).toBe('render_failed');
320
+ expect(y?.error?.code).toBe('RENDER_FAILED');
321
+ });
322
+ });
323
+ // The fake gradlew/swift cannot exercise the real Roborazzi/Robolectric render
324
+ // or the CoreGraphics trim. This end-to-end guard ships the committed Roborazzi
325
+ // fixture as the render package and runs the whole step with the real
326
+ // toolchain, proving the tracer bullet: ship a lean Gradle project, get a
327
+ // paired result back. Gated behind CIBUILD_UI_FIDELITY_REAL_ANDROID=1 (the
328
+ // Android analog of the iOS real-Swift gate); needs the Android toolchain +
329
+ // warm Roborazzi caches on the dev machine.
330
+ describe('android render runtime — real toolchain', () => {
331
+ const realAndroidTest = process.env.CIBUILD_UI_FIDELITY_REAL_ANDROID === '1' ? test : test.skip;
332
+ // A real 1x1 PNG so the trim helper's reference load/align step succeeds.
333
+ const ONE_BY_ONE_PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', 'base64');
334
+ const FIXTURE_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '../../../../cibuild-runner/scripts/roborazzi-fixture');
335
+ realAndroidTest('renders the Roborazzi fixture at 720x1280 and returns a paired result', async () => {
336
+ const dir = mkdtempSync(join(tmpdir(), 'ui-fidelity-android-real-'));
337
+ tempDirs.push(dir);
338
+ const project = { dir, binDir: '' };
339
+ mkdirSync(join(dir, '.ci', 'inputs'), { recursive: true });
340
+ writeFileSync(join(dir, '.ci', 'inputs', 'params.json'), JSON.stringify({ screens: { XProof: 'ref.png' } }));
341
+ writeFileSync(join(dir, '.ci', 'inputs', 'ref.png'), ONE_BY_ONE_PNG);
342
+ // Archive the committed fixture (minus build/.gradle noise) as the
343
+ // shipped render package, with the real system tar.
344
+ const stage = mkdtempSync(join(tmpdir(), 'ui-fidelity-android-fixture-'));
345
+ tempDirs.push(stage);
346
+ cpSync(FIXTURE_DIR, join(stage, 'project'), { recursive: true });
347
+ const archive = join(dir, '.ci', 'inputs', 'package.tar.gz');
348
+ const tar = spawnSync('tar', ['-czf', archive, '-C', stage, '--exclude', '*/build', '--exclude', '*/.gradle', 'project'], { encoding: 'utf-8' });
349
+ expect(tar.stderr).toBe('');
350
+ expect(tar.status).toBe(0);
351
+ const script = await buildScript();
352
+ const run = spawnSync('node', ['-e', script], {
353
+ cwd: dir,
354
+ encoding: 'utf-8',
355
+ env: { ...process.env },
356
+ maxBuffer: 64 * 1024 * 1024,
357
+ });
358
+ if (run.status !== 0) {
359
+ // eslint-disable-next-line no-console
360
+ console.error('STDOUT\n' + run.stdout + '\nSTDERR\n' + run.stderr);
361
+ }
362
+ expect(run.status).toBe(0);
363
+ const doc = readResult(project);
364
+ expect(doc.error).toBeNull();
365
+ const screen = doc.screens.find((s) => s.screen === 'XProof');
366
+ expect(screen?.status).toBe('rendered');
367
+ expect(screen?.error).toBeNull();
368
+ // The raw render before trimming is the fixture's full 720x1280 canvas.
369
+ expect(run.stdout).toContain('XProof: 720x1280');
370
+ // Paired, contract-shaped artifacts are produced.
371
+ expect(isPng(artifact(project, 'ui-fidelity/rendered/XProof.png'))).toBe(true);
372
+ expect(isPng(artifact(project, 'ui-fidelity/aligned/XProof.png'))).toBe(true);
373
+ expect(screen?.dimensions?.rendered_px[0]).toBeGreaterThan(0);
374
+ expect(screen?.dimensions?.rendered_px[1]).toBeGreaterThan(0);
375
+ }, 600_000);
376
+ });
377
+ //# sourceMappingURL=ui-fidelity-render-android.test.js.map
@@ -157,34 +157,6 @@ export interface RenderScriptInternals {
157
157
  generateProbeSwift(options: HarnessGeneratorOptions): string;
158
158
  generateScreenSwift(screen: string, options: HarnessGeneratorOptions): string;
159
159
  }
160
- /**
161
- * Runtime support for trimming the rendered output (package_source variants).
162
- *
163
- * The render entry forces the view into a fixed device-sized frame, so a
164
- * component smaller than the canvas renders with transparent margins. After a
165
- * screen renders, crop the PNG in place to the bounding box of its
166
- * non-transparent pixels, so the rendered artifact is the component itself,
167
- * tight -- no canvas, no background ambiguity for the human comparing screens.
168
- *
169
- * The crop is done by a small CoreGraphics helper run in Swift's
170
- * script-interpreter mode (so it needs no extra harness target and reuses the
171
- * one `swift` the runner already has). Best-effort: a trim failure is logged
172
- * (warn-and-continue) and the untrimmed render is kept -- a cosmetic post-step
173
- * never fails an otherwise good render. This is prep, not scoring: it produces
174
- * a tighter image for the human's judgment and computes nothing about
175
- * fidelity.
176
- */
177
- /**
178
- * The CoreGraphics trim/align helper source, run via
179
- * `swift <Trim.swift> <in> <out> <aligned> <ref>` (script-interpreter mode).
180
- * Crops a PNG to its content bounding box (transparent or uniform-background
181
- * margins), writes a reference-sized aligned copy, and prints a TRIMDIMS line.
182
- *
183
- * Exported so a real-toolchain test can exercise the pixel logic directly --
184
- * the fake `swift` in the unit tests cannot, which is why a background-detection
185
- * bug shipped twice undetected before a real-pixel guard existed.
186
- */
187
- export declare const TRIM_HELPER_SWIFT_SOURCE: string;
188
160
  /**
189
161
  * Evaluates the embedded generator source and returns the real functions, so
190
162
  * unit tests exercise exactly the code that ships inside the runtime script.
@@ -1 +1 @@
1
- {"version":3,"file":"ui-fidelity-render.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ui-fidelity-render.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ui-fidelity-render.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ui-fidelity-render.ts"],"names":[],"mappings":"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"}