@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.
- package/dist/cli.cjs +688 -69
- package/dist/src/lib.d.ts +2 -0
- package/dist/src/lib.d.ts.map +1 -1
- package/dist/src/lib.js +1 -0
- package/dist/src/yaml/steps/index.d.ts.map +1 -1
- package/dist/src/yaml/steps/index.js +22 -0
- package/dist/src/yaml/steps/render-post-processor.d.ts +73 -0
- package/dist/src/yaml/steps/render-post-processor.d.ts.map +1 -0
- package/dist/src/yaml/steps/render-post-processor.js +303 -0
- package/dist/src/yaml/steps/render-post-processor.test.d.ts +16 -0
- package/dist/src/yaml/steps/render-post-processor.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/render-post-processor.test.js +238 -0
- package/dist/src/yaml/steps/ui-fidelity-preview-android.d.ts +66 -0
- package/dist/src/yaml/steps/ui-fidelity-preview-android.d.ts.map +1 -0
- package/dist/src/yaml/steps/ui-fidelity-preview-android.js +249 -0
- package/dist/src/yaml/steps/ui-fidelity-preview-android.test.d.ts +13 -0
- package/dist/src/yaml/steps/ui-fidelity-preview-android.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/ui-fidelity-preview-android.test.js +299 -0
- package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts +92 -0
- package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts.map +1 -0
- package/dist/src/yaml/steps/ui-fidelity-render-android.js +726 -0
- package/dist/src/yaml/steps/ui-fidelity-render-android.test.d.ts +2 -0
- package/dist/src/yaml/steps/ui-fidelity-render-android.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/ui-fidelity-render-android.test.js +377 -0
- package/dist/src/yaml/steps/ui-fidelity-render.d.ts +0 -28
- package/dist/src/yaml/steps/ui-fidelity-render.d.ts.map +1 -1
- package/dist/src/yaml/steps/ui-fidelity-render.js +1 -259
- package/dist/src/yaml/steps/ui-fidelity-render.test.js +1 -57
- package/package.json +1 -1
|
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"ui-fidelity-render.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ui-fidelity-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAGpE;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IACnC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mFAAmF;IACnF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAE7C,mFAAmF;AACnF,eAAO,MAAM,aAAa,IAAI,CAAC;AAE/B,iDAAiD;AACjD,eAAO,MAAM,eAAe,6BAA8B,CAAC;AAC3D,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7D,sEAAsE;AACtE,eAAO,MAAM,sBAAsB,EAAE,aAAsB,CAAC;AAE5D,+EAA+E;AAC/E,eAAO,MAAM,wBAAwB,mBAAmB,CAAC;AAEzD;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,QAAmB,CAAC;AAE5D,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAWzD;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAQzD;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,kBAAkB;IACjC,2EAA2E;IAC3E,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,yEAAyE;IACzE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED,4DAA4D;AAC5D,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED,gFAAgF;AAChF,MAAM,WAAW,qBAAqB;IACpC,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1C,2BAA2B,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM,CAAC;IACzF,kBAAkB,CAAC,OAAO,EAAE,uBAAuB,GAAG,MAAM,CAAC;IAC7D,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM,CAAC;CAC/E;AAsjCD;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,qBAAqB,CAShE;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAUvE;AAED;;;;GAIG;AACH,qBAAa,4BAA6B,SAAQ,gBAAgB;IAChE,yBAAyB,CACvB,MAAM,EAAE,sBAAsB,EAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,qBAAqB,EAAE;IAoCpB,OAAO,CACX,MAAM,EAAE,sBAAsB,EAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,OAAO,CAAC;CA0DpB"}
|