@invarn/cibuild 2.2.5 → 2.2.7
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 +16 -10
- package/dist/src/yaml/steps/index.d.ts.map +1 -1
- package/dist/src/yaml/steps/index.js +29 -2
- package/dist/src/yaml/steps/structural-facts.d.ts.map +1 -1
- package/dist/src/yaml/steps/structural-facts.js +0 -0
- package/dist/src/yaml/steps/structural-facts.test.js +35 -9
- package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts +38 -0
- package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts.map +1 -1
- package/dist/src/yaml/steps/ui-fidelity-render-android.js +171 -1
- package/dist/src/yaml/steps/ui-fidelity-render-android.test.js +179 -1
- package/dist/src/yaml/steps/ui-fidelity-render.d.ts +21 -0
- package/dist/src/yaml/steps/ui-fidelity-render.d.ts.map +1 -1
- package/dist/src/yaml/steps/ui-fidelity-render.js +66 -3
- package/dist/src/yaml/steps/ui-fidelity-render.test.js +55 -0
- package/package.json +1 -1
|
@@ -4,7 +4,10 @@ import { gzipSync } from 'node:zlib';
|
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import { ANDROID_RENDERER, UiFidelityRenderAndroidStepExecutor, } from './ui-fidelity-render-android.js';
|
|
7
|
+
import { ANDROID_RENDERER, DEFAULT_GRADLE_TASK, UiFidelityRenderAndroidStepExecutor, } from './ui-fidelity-render-android.js';
|
|
8
|
+
import { DEFAULT_SCALE } from './ui-fidelity-render.js';
|
|
9
|
+
import { clearRegistry, getStepMetadata, hasStep } from './registry.js';
|
|
10
|
+
import { initializeStepRegistry } from './index.js';
|
|
8
11
|
import { testConfig } from './test-config.js';
|
|
9
12
|
const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
10
13
|
const tempDirs = [];
|
|
@@ -166,6 +169,48 @@ function runScript(project, script, env = {}) {
|
|
|
166
169
|
function readResult(project) {
|
|
167
170
|
return JSON.parse(readFileSync(join(project.dir, '.ci', 'artifacts', 'protocol-result.json'), 'utf-8'));
|
|
168
171
|
}
|
|
172
|
+
describe('registry integration', () => {
|
|
173
|
+
test('ui-fidelity-render-android is registered with its input schema', () => {
|
|
174
|
+
clearRegistry();
|
|
175
|
+
initializeStepRegistry();
|
|
176
|
+
expect(hasStep('ui-fidelity-render-android')).toBe(true);
|
|
177
|
+
const metadata = getStepMetadata('ui-fidelity-render-android');
|
|
178
|
+
expect(metadata?.platform).toBe('android');
|
|
179
|
+
const inputs = metadata?.inputs ?? {};
|
|
180
|
+
expect(Object.keys(inputs).sort()).toEqual([
|
|
181
|
+
'gradle_task',
|
|
182
|
+
'package_path',
|
|
183
|
+
'package_source',
|
|
184
|
+
'reference_source',
|
|
185
|
+
'scale',
|
|
186
|
+
]);
|
|
187
|
+
expect(inputs.package_source.required).toBe(false);
|
|
188
|
+
expect(inputs.package_source.default).toBe('inputs');
|
|
189
|
+
expect(inputs.reference_source.required).toBe(false);
|
|
190
|
+
expect(inputs.reference_source.default).toBe('inputs');
|
|
191
|
+
// package_path is enforced by the executor only in repo mode; the default
|
|
192
|
+
// (inputs) config is valid without it, so it is not unconditionally required.
|
|
193
|
+
expect(inputs.package_path.required).toBe(false);
|
|
194
|
+
expect(inputs.scale.required).toBe(false);
|
|
195
|
+
expect(inputs.scale.default).toBe(DEFAULT_SCALE);
|
|
196
|
+
expect(inputs.gradle_task.required).toBe(false);
|
|
197
|
+
expect(inputs.gradle_task.default).toBe(DEFAULT_GRADLE_TASK);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
describe('default byte compatibility', () => {
|
|
201
|
+
// Snapshot of the script generated for the default inputs (no package_source,
|
|
202
|
+
// no reference_source), captured from the executor with everything else at its
|
|
203
|
+
// default. Omitting both *_source inputs must keep the generated script
|
|
204
|
+
// byte-identical to this snapshot, so existing protocols are provably
|
|
205
|
+
// untouched. Update the snapshot only for a deliberate, reviewed change to the
|
|
206
|
+
// default runtime.
|
|
207
|
+
const DEFAULT_SCRIPT_SNAPSHOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../../test/fixtures/ui-fidelity-render-android-v1-script.txt');
|
|
208
|
+
test('omitting package_source and reference_source generates a byte-identical script', async () => {
|
|
209
|
+
const executor = new UiFidelityRenderAndroidStepExecutor();
|
|
210
|
+
const step = await executor.execute({}, {}, testConfig);
|
|
211
|
+
expect(step.script).toBe(readFileSync(DEFAULT_SCRIPT_SNAPSHOT, 'utf-8'));
|
|
212
|
+
});
|
|
213
|
+
});
|
|
169
214
|
describe('UiFidelityRenderAndroidStepExecutor', () => {
|
|
170
215
|
test('returns a node StepDef named ui-fidelity-render-android', async () => {
|
|
171
216
|
const executor = new UiFidelityRenderAndroidStepExecutor();
|
|
@@ -541,6 +586,139 @@ describe('android render runtime — error isolation', () => {
|
|
|
541
586
|
expect(y?.error?.code).toBe('RENDER_FAILED');
|
|
542
587
|
});
|
|
543
588
|
});
|
|
589
|
+
/** Materialize a well-formed Gradle render project (fake gradlew) on disk. */
|
|
590
|
+
function writeGradleProjectDir(dir) {
|
|
591
|
+
mkdirSync(join(dir, 'proof'), { recursive: true });
|
|
592
|
+
writeFileSync(join(dir, 'settings.gradle.kts'), 'rootProject.name = "p"\ninclude(":proof")\n');
|
|
593
|
+
const gradlew = join(dir, 'gradlew');
|
|
594
|
+
writeFileSync(gradlew, FAKE_GRADLEW_LINES.join('\n') + '\n');
|
|
595
|
+
chmodSync(gradlew, 0o755);
|
|
596
|
+
writeFileSync(join(dir, 'proof', 'build.gradle.kts'), '// module\n');
|
|
597
|
+
}
|
|
598
|
+
describe('package_source: repo', () => {
|
|
599
|
+
test('rejects an unknown package_source value', async () => {
|
|
600
|
+
const executor = new UiFidelityRenderAndroidStepExecutor();
|
|
601
|
+
await expect(executor.execute({ package_source: 'cloud' }, {}, testConfig)).rejects.toThrow(/package_source/);
|
|
602
|
+
});
|
|
603
|
+
test('package_source repo requires package_path', async () => {
|
|
604
|
+
const executor = new UiFidelityRenderAndroidStepExecutor();
|
|
605
|
+
await expect(executor.execute({ package_source: 'repo' }, {}, testConfig)).rejects.toThrow(/package_path/);
|
|
606
|
+
});
|
|
607
|
+
test('omitting package_source bakes no packageSource/packagePath into the config', async () => {
|
|
608
|
+
const script = await buildScript();
|
|
609
|
+
const configLine = script.split('\n').find((l) => l.startsWith('var CONFIG = '));
|
|
610
|
+
expect(configLine).toBeDefined();
|
|
611
|
+
expect(configLine).not.toContain('packageSource');
|
|
612
|
+
expect(configLine).not.toContain('packagePath');
|
|
613
|
+
});
|
|
614
|
+
test('renders the checked-out Gradle project at package_path (no archive)', async () => {
|
|
615
|
+
const project = makeProject({ screens: { XProof: 'x.png' } });
|
|
616
|
+
writeReference(project, 'x.png');
|
|
617
|
+
// The committed project lives at a repo-relative path; no package.tar.gz.
|
|
618
|
+
writeGradleProjectDir(join(project.dir, 'android-app'));
|
|
619
|
+
const run = runScript(project, await buildScript({ package_source: 'repo', package_path: 'android-app' }), { FAKE_ROBORAZZI_SCREENS: 'XProof' });
|
|
620
|
+
expect(run.status).toBe(0);
|
|
621
|
+
const doc = readResult(project);
|
|
622
|
+
expect(doc.package_source).toBe('repo');
|
|
623
|
+
expect(doc.error).toBeNull();
|
|
624
|
+
const screen = doc.screens.find((s) => s.screen === 'XProof');
|
|
625
|
+
expect(screen?.status).toBe('rendered');
|
|
626
|
+
expect(screen?.rendered_image_path).toBe('ui-fidelity/rendered/XProof.png');
|
|
627
|
+
// The render came from the checkout, not an extracted tarball.
|
|
628
|
+
expect(existsSync(join(project.dir, '.ci', 'inputs', 'package.tar.gz'))).toBe(false);
|
|
629
|
+
});
|
|
630
|
+
test('a missing package_path directory fails cleanly with a per-build error', async () => {
|
|
631
|
+
const project = makeProject({ screens: { XProof: 'x.png' } });
|
|
632
|
+
writeReference(project, 'x.png');
|
|
633
|
+
// No project at the path.
|
|
634
|
+
const run = runScript(project, await buildScript({ package_source: 'repo', package_path: 'nope' }), { FAKE_ROBORAZZI_SCREENS: 'XProof' });
|
|
635
|
+
expect(run.status).toBe(1);
|
|
636
|
+
const doc = readResult(project);
|
|
637
|
+
expect(doc.package_source).toBe('repo');
|
|
638
|
+
expect(doc.error?.code).toBe('ANDROID_PROJECT_MISSING');
|
|
639
|
+
});
|
|
640
|
+
test('a package_path without a settings script fails cleanly', async () => {
|
|
641
|
+
const project = makeProject({ screens: { XProof: 'x.png' } });
|
|
642
|
+
writeReference(project, 'x.png');
|
|
643
|
+
// A directory that is not a Gradle project root.
|
|
644
|
+
mkdirSync(join(project.dir, 'not-a-project'), { recursive: true });
|
|
645
|
+
writeFileSync(join(project.dir, 'not-a-project', 'README.md'), '# nope\n');
|
|
646
|
+
const run = runScript(project, await buildScript({ package_source: 'repo', package_path: 'not-a-project' }), { FAKE_ROBORAZZI_SCREENS: 'XProof' });
|
|
647
|
+
expect(run.status).toBe(1);
|
|
648
|
+
expect(readResult(project).error?.code).toBe('ANDROID_PROJECT_ROOT_MISSING');
|
|
649
|
+
});
|
|
650
|
+
test('composes with reference_source repo (project + reference both from the checkout)', async () => {
|
|
651
|
+
const project = makeProject({ screens: { XProof: 'refs/x.png' } });
|
|
652
|
+
mkdirSync(join(project.dir, 'refs'), { recursive: true });
|
|
653
|
+
writeFileSync(join(project.dir, 'refs', 'x.png'), Buffer.concat([PNG_MAGIC, Buffer.from('ref:x')]));
|
|
654
|
+
writeGradleProjectDir(join(project.dir, 'android-app'));
|
|
655
|
+
const run = runScript(project, await buildScript({
|
|
656
|
+
package_source: 'repo',
|
|
657
|
+
package_path: 'android-app',
|
|
658
|
+
reference_source: 'repo',
|
|
659
|
+
}), { FAKE_ROBORAZZI_SCREENS: 'XProof' });
|
|
660
|
+
expect(run.status).toBe(0);
|
|
661
|
+
const doc = readResult(project);
|
|
662
|
+
expect(doc.package_source).toBe('repo');
|
|
663
|
+
const screen = doc.screens.find((s) => s.screen === 'XProof');
|
|
664
|
+
expect(screen?.status).toBe('rendered');
|
|
665
|
+
expect(screen?.reference_image_path).toBe('ui-fidelity/references/XProof.png');
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
describe('reference_source: repo', () => {
|
|
669
|
+
test('rejects an unknown reference_source value', async () => {
|
|
670
|
+
const executor = new UiFidelityRenderAndroidStepExecutor();
|
|
671
|
+
await expect(executor.execute({ reference_source: 'artifact' }, {}, testConfig)).rejects.toThrow(/reference_source/);
|
|
672
|
+
});
|
|
673
|
+
test('omitting reference_source bakes no referenceSource into the script config', async () => {
|
|
674
|
+
const script = await buildScript();
|
|
675
|
+
const configLine = script.split('\n').find((l) => l.startsWith('var CONFIG = '));
|
|
676
|
+
expect(configLine).toBeDefined();
|
|
677
|
+
expect(configLine).not.toContain('referenceSource');
|
|
678
|
+
});
|
|
679
|
+
test('reads each screen reference from its repo-relative path in the checkout', async () => {
|
|
680
|
+
const project = makeProject({
|
|
681
|
+
screens: { XProof: 'refs/x.png', YProof: 'design/y.png' },
|
|
682
|
+
});
|
|
683
|
+
// Committed references live at repo-relative paths (resolved against cwd),
|
|
684
|
+
// NOT in .ci/inputs.
|
|
685
|
+
mkdirSync(join(project.dir, 'refs'), { recursive: true });
|
|
686
|
+
mkdirSync(join(project.dir, 'design'), { recursive: true });
|
|
687
|
+
writeFileSync(join(project.dir, 'refs', 'x.png'), Buffer.concat([PNG_MAGIC, Buffer.from('ref:x')]));
|
|
688
|
+
writeFileSync(join(project.dir, 'design', 'y.png'), Buffer.concat([PNG_MAGIC, Buffer.from('ref:y')]));
|
|
689
|
+
stageArchive(project, gradleProjectEntries());
|
|
690
|
+
const run = runScript(project, await buildScript({ reference_source: 'repo' }), {
|
|
691
|
+
FAKE_ROBORAZZI_SCREENS: 'XProof,YProof',
|
|
692
|
+
});
|
|
693
|
+
expect(run.status).toBe(0);
|
|
694
|
+
const doc = readResult(project);
|
|
695
|
+
expect(doc.screens.map((s) => s.status)).toEqual(['rendered', 'rendered']);
|
|
696
|
+
expect(doc.screens.map((s) => s.reference_image_path)).toEqual([
|
|
697
|
+
'ui-fidelity/references/XProof.png',
|
|
698
|
+
'ui-fidelity/references/YProof.png',
|
|
699
|
+
]);
|
|
700
|
+
expect(isPng(artifact(project, 'ui-fidelity/references/XProof.png'))).toBe(true);
|
|
701
|
+
expect(isPng(artifact(project, 'ui-fidelity/references/YProof.png'))).toBe(true);
|
|
702
|
+
});
|
|
703
|
+
test('fails one screen when its committed reference is missing, renders the rest', async () => {
|
|
704
|
+
const project = makeProject({
|
|
705
|
+
screens: { XProof: 'refs/x.png', GhostProof: 'refs/ghost.png' },
|
|
706
|
+
});
|
|
707
|
+
mkdirSync(join(project.dir, 'refs'), { recursive: true });
|
|
708
|
+
writeFileSync(join(project.dir, 'refs', 'x.png'), Buffer.concat([PNG_MAGIC, Buffer.from('ref:x')]));
|
|
709
|
+
// refs/ghost.png intentionally absent.
|
|
710
|
+
stageArchive(project, gradleProjectEntries());
|
|
711
|
+
const run = runScript(project, await buildScript({ reference_source: 'repo' }), {
|
|
712
|
+
FAKE_ROBORAZZI_SCREENS: 'XProof',
|
|
713
|
+
});
|
|
714
|
+
expect(run.status).not.toBe(0);
|
|
715
|
+
const doc = readResult(project);
|
|
716
|
+
expect(doc.screens.find((s) => s.screen === 'XProof')?.status).toBe('rendered');
|
|
717
|
+
const ghost = doc.screens.find((s) => s.screen === 'GhostProof');
|
|
718
|
+
expect(ghost?.status).toBe('render_failed');
|
|
719
|
+
expect(ghost?.error?.code).toBe('REFERENCE_MISSING');
|
|
720
|
+
});
|
|
721
|
+
});
|
|
544
722
|
// The fake gradlew/swift cannot exercise the real Roborazzi/Robolectric render
|
|
545
723
|
// or the CoreGraphics trim. This end-to-end guard ships the committed Roborazzi
|
|
546
724
|
// fixture as the render package and runs the whole step with the real
|
|
@@ -75,6 +75,16 @@ export interface UiFidelityRenderInputs {
|
|
|
75
75
|
* product.
|
|
76
76
|
*/
|
|
77
77
|
target?: string;
|
|
78
|
+
/**
|
|
79
|
+
* Where each screen's reference image comes from (default "inputs"):
|
|
80
|
+
* - "inputs" (default / v1): `params.screens[<Screen>]` is a plain file
|
|
81
|
+
* basename read from `.ci/inputs/<basename>` (an agent-uploaded run input).
|
|
82
|
+
* - "repo": `params.screens[<Screen>]` is a repo-relative path to a COMMITTED
|
|
83
|
+
* reference image, resolved against the checkout. Lets a triggered
|
|
84
|
+
* (agent-less) run point at a reference that lives in the cloned repo.
|
|
85
|
+
* Omitting reference_source generates a byte-identical v1 script.
|
|
86
|
+
*/
|
|
87
|
+
reference_source?: 'repo' | 'inputs';
|
|
78
88
|
/** Render size in device points, formatted "<width>x<height>" (default 393x852) */
|
|
79
89
|
render_size?: string;
|
|
80
90
|
/** Display scale factor applied to the render (default 2) */
|
|
@@ -94,6 +104,11 @@ export declare const PACKAGE_SOURCES: readonly ["repo", "inputs"];
|
|
|
94
104
|
export type PackageSource = (typeof PACKAGE_SOURCES)[number];
|
|
95
105
|
/** Default package source: the package lives in the repo checkout. */
|
|
96
106
|
export declare const DEFAULT_PACKAGE_SOURCE: PackageSource;
|
|
107
|
+
/** Valid values for the reference_source input. */
|
|
108
|
+
export declare const REFERENCE_SOURCES: readonly ["repo", "inputs"];
|
|
109
|
+
export type ReferenceSource = (typeof REFERENCE_SOURCES)[number];
|
|
110
|
+
/** Default reference source: references arrive as run inputs (v1 behaviour). */
|
|
111
|
+
export declare const DEFAULT_REFERENCE_SOURCE: ReferenceSource;
|
|
97
112
|
/** Basename of the shipped package archive inside the run-inputs directory. */
|
|
98
113
|
export declare const PACKAGE_ARCHIVE_BASENAME = "package.tar.gz";
|
|
99
114
|
/**
|
|
@@ -140,6 +155,12 @@ export interface RenderScriptConfig {
|
|
|
140
155
|
height: number;
|
|
141
156
|
scale: number;
|
|
142
157
|
packageSource?: PackageSource;
|
|
158
|
+
/**
|
|
159
|
+
* Only present (and only ever "repo") when reference_source was explicitly set
|
|
160
|
+
* to "repo". When absent, references are read from `.ci/inputs/<basename>` and
|
|
161
|
+
* the generated reference-sourcing code is byte-identical to v1.
|
|
162
|
+
*/
|
|
163
|
+
referenceSource?: ReferenceSource;
|
|
143
164
|
}
|
|
144
165
|
/** Options consumed by the pure Swift-source generators. */
|
|
145
166
|
export interface HarnessGeneratorOptions {
|
|
@@ -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;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;
|
|
1
|
+
{"version":3,"file":"ui-fidelity-render.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ui-fidelity-render.ts"],"names":[],"mappings":"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"}
|
|
@@ -63,6 +63,10 @@ export const DEFAULT_SCALE = 2;
|
|
|
63
63
|
export const PACKAGE_SOURCES = ['repo', 'inputs'];
|
|
64
64
|
/** Default package source: the package lives in the repo checkout. */
|
|
65
65
|
export const DEFAULT_PACKAGE_SOURCE = 'repo';
|
|
66
|
+
/** Valid values for the reference_source input. */
|
|
67
|
+
export const REFERENCE_SOURCES = ['repo', 'inputs'];
|
|
68
|
+
/** Default reference source: references arrive as run inputs (v1 behaviour). */
|
|
69
|
+
export const DEFAULT_REFERENCE_SOURCE = 'inputs';
|
|
66
70
|
/** Basename of the shipped package archive inside the run-inputs directory. */
|
|
67
71
|
export const PACKAGE_ARCHIVE_BASENAME = 'package.tar.gz';
|
|
68
72
|
/**
|
|
@@ -1112,6 +1116,41 @@ function buildPackageSourceMain() {
|
|
|
1112
1116
|
return main;
|
|
1113
1117
|
}
|
|
1114
1118
|
const RENDER_SCRIPT_MAIN_WITH_PACKAGE_SOURCE = buildPackageSourceMain();
|
|
1119
|
+
/**
|
|
1120
|
+
* Patches the reference-sourcing block to read each screen's reference from a
|
|
1121
|
+
* repo-relative path (resolved against the checkout) instead of a plain
|
|
1122
|
+
* basename under `.ci/inputs/`. Applied on top of EITHER base main (v1 or the
|
|
1123
|
+
* package_source variant) — the package patches never touch this block, so the
|
|
1124
|
+
* anchor is present and unique in both. The downstream stat/copy logic is
|
|
1125
|
+
* unchanged; only how `source` is computed differs.
|
|
1126
|
+
*/
|
|
1127
|
+
function buildReferenceRepoMain(base) {
|
|
1128
|
+
return replaceOnce(base, 'var basename = params.screens[entry.screen];\n' +
|
|
1129
|
+
' if (\n' +
|
|
1130
|
+
" typeof basename !== 'string' || basename === '' ||\n" +
|
|
1131
|
+
" basename === '.' || basename === '..' ||\n" +
|
|
1132
|
+
' basename !== path.basename(basename)\n' +
|
|
1133
|
+
' ) {\n' +
|
|
1134
|
+
' setError(\n' +
|
|
1135
|
+
' entry,\n' +
|
|
1136
|
+
" 'REFERENCE_MISSING',\n" +
|
|
1137
|
+
" 'reference for ' + entry.screen + ' must be a plain file basename inside ' +\n" +
|
|
1138
|
+
" inputsDir + ', got: ' + JSON.stringify(basename)\n" +
|
|
1139
|
+
' );\n' +
|
|
1140
|
+
' return;\n' +
|
|
1141
|
+
' }\n' +
|
|
1142
|
+
' var source = path.join(inputsDir, basename);', 'var refPath = params.screens[entry.screen];\n' +
|
|
1143
|
+
" if (typeof refPath !== 'string' || refPath === '') {\n" +
|
|
1144
|
+
' setError(\n' +
|
|
1145
|
+
' entry,\n' +
|
|
1146
|
+
" 'REFERENCE_MISSING',\n" +
|
|
1147
|
+
" 'reference for ' + entry.screen + ' must be a non-empty repo-relative path, got: ' +\n" +
|
|
1148
|
+
' JSON.stringify(refPath)\n' +
|
|
1149
|
+
' );\n' +
|
|
1150
|
+
' return;\n' +
|
|
1151
|
+
' }\n' +
|
|
1152
|
+
' var source = path.resolve(refPath);');
|
|
1153
|
+
}
|
|
1115
1154
|
/**
|
|
1116
1155
|
* Evaluates the embedded generator source and returns the real functions, so
|
|
1117
1156
|
* unit tests exercise exactly the code that ships inside the runtime script.
|
|
@@ -1133,14 +1172,20 @@ export function getRenderScriptInternals() {
|
|
|
1133
1172
|
* unpatched v1 runtime is used).
|
|
1134
1173
|
*/
|
|
1135
1174
|
export function generateRenderScript(config) {
|
|
1175
|
+
let main = config.packageSource === undefined
|
|
1176
|
+
? RENDER_SCRIPT_MAIN
|
|
1177
|
+
: RENDER_SCRIPT_MAIN_WITH_PACKAGE_SOURCE;
|
|
1178
|
+
// Reference-from-repo is an independent dimension layered on top of whichever
|
|
1179
|
+
// package base applies. Absent → byte-identical reference block (v1).
|
|
1180
|
+
if (config.referenceSource === 'repo') {
|
|
1181
|
+
main = buildReferenceRepoMain(main);
|
|
1182
|
+
}
|
|
1136
1183
|
return [
|
|
1137
1184
|
"'use strict';",
|
|
1138
1185
|
'// ui-fidelity-render runtime script (generated by cibuild at YAML conversion time)',
|
|
1139
1186
|
'var CONFIG = ' + JSON.stringify(config) + ';',
|
|
1140
1187
|
RENDER_SCRIPT_GENERATORS,
|
|
1141
|
-
|
|
1142
|
-
? RENDER_SCRIPT_MAIN
|
|
1143
|
-
: RENDER_SCRIPT_MAIN_WITH_PACKAGE_SOURCE,
|
|
1188
|
+
main,
|
|
1144
1189
|
].join('\n');
|
|
1145
1190
|
}
|
|
1146
1191
|
/**
|
|
@@ -1196,12 +1241,30 @@ export class UiFidelityRenderStepExecutor extends BaseStepExecutor {
|
|
|
1196
1241
|
throw new Error(`Invalid target '${target}' for step '${stepName}': ` +
|
|
1197
1242
|
'must be a Swift module identifier (letters, digits, underscores)');
|
|
1198
1243
|
}
|
|
1244
|
+
// reference_source mirrors package_source: validate when provided, but only
|
|
1245
|
+
// record (and patch the script for) the non-default "repo" so omitting it —
|
|
1246
|
+
// or setting the default "inputs" — keeps the v1 reference block intact.
|
|
1247
|
+
const rawReferenceSource = this.getInput(inputs, 'reference_source', undefined);
|
|
1248
|
+
let referenceSource;
|
|
1249
|
+
if (rawReferenceSource !== undefined) {
|
|
1250
|
+
const value = String(rawReferenceSource);
|
|
1251
|
+
if (!REFERENCE_SOURCES.includes(value)) {
|
|
1252
|
+
throw new Error(`Invalid reference_source '${value}' for step '${stepName}': ` +
|
|
1253
|
+
`expected one of: ${REFERENCE_SOURCES.join(', ')}`);
|
|
1254
|
+
}
|
|
1255
|
+
if (value === 'repo') {
|
|
1256
|
+
referenceSource = 'repo';
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1199
1259
|
const { width, height } = parseRenderSize(String(this.getInput(inputs, 'render_size', DEFAULT_RENDER_SIZE)));
|
|
1200
1260
|
const scale = parseScale(this.getInput(inputs, 'scale', DEFAULT_SCALE));
|
|
1201
1261
|
const config = { packagePath, target, width, height, scale };
|
|
1202
1262
|
if (packageSource !== undefined) {
|
|
1203
1263
|
config.packageSource = packageSource;
|
|
1204
1264
|
}
|
|
1265
|
+
if (referenceSource !== undefined) {
|
|
1266
|
+
config.referenceSource = referenceSource;
|
|
1267
|
+
}
|
|
1205
1268
|
const script = generateRenderScript(config);
|
|
1206
1269
|
return this.createNodeStep(script, stepName);
|
|
1207
1270
|
}
|
|
@@ -319,10 +319,13 @@ describe('registry integration', () => {
|
|
|
319
319
|
expect(Object.keys(inputs).sort()).toEqual([
|
|
320
320
|
'package_path',
|
|
321
321
|
'package_source',
|
|
322
|
+
'reference_source',
|
|
322
323
|
'render_size',
|
|
323
324
|
'scale',
|
|
324
325
|
'target',
|
|
325
326
|
]);
|
|
327
|
+
expect(inputs.reference_source.required).toBe(false);
|
|
328
|
+
expect(inputs.reference_source.default).toBe('inputs');
|
|
326
329
|
// package_path/target are enforced by the executor in repo mode (the
|
|
327
330
|
// default); a package_source:inputs config is valid without either, so
|
|
328
331
|
// neither is unconditionally required at the schema level.
|
|
@@ -1509,4 +1512,56 @@ describe('committed fixture package', () => {
|
|
|
1509
1512
|
expectNoAbsolutePathLeaks(project, result);
|
|
1510
1513
|
}, 600_000);
|
|
1511
1514
|
});
|
|
1515
|
+
describe('reference_source: repo', () => {
|
|
1516
|
+
test('rejects an unknown reference_source value', async () => {
|
|
1517
|
+
const executor = new UiFidelityRenderStepExecutor();
|
|
1518
|
+
await expect(executor.execute({ package_path: './pkg', target: 'MyViews', reference_source: 'artifact' }, {}, testConfig)).rejects.toThrow(/reference_source/);
|
|
1519
|
+
});
|
|
1520
|
+
test('omitting reference_source bakes no referenceSource into the script config', async () => {
|
|
1521
|
+
const script = await buildScript(makeProject());
|
|
1522
|
+
const configLine = script
|
|
1523
|
+
.split('\n')
|
|
1524
|
+
.find((l) => l.startsWith('var CONFIG = '));
|
|
1525
|
+
expect(configLine).toBeDefined();
|
|
1526
|
+
expect(configLine).not.toContain('referenceSource');
|
|
1527
|
+
});
|
|
1528
|
+
test('reads each screen reference from its repo-relative path in the checkout', async () => {
|
|
1529
|
+
const project = makeProject({
|
|
1530
|
+
screens: { HomeView: 'refs/home.png', SettingsView: 'design/settings.png' },
|
|
1531
|
+
});
|
|
1532
|
+
// Committed references live at repo-relative paths (resolved against cwd),
|
|
1533
|
+
// NOT in .ci/inputs.
|
|
1534
|
+
mkdirSync(join(project.dir, 'refs'), { recursive: true });
|
|
1535
|
+
mkdirSync(join(project.dir, 'design'), { recursive: true });
|
|
1536
|
+
writeFileSync(join(project.dir, 'refs', 'home.png'), Buffer.concat([PNG_MAGIC, Buffer.from('ref:home')]));
|
|
1537
|
+
writeFileSync(join(project.dir, 'design', 'settings.png'), Buffer.concat([PNG_MAGIC, Buffer.from('ref:settings')]));
|
|
1538
|
+
const script = await buildScript(project, { reference_source: 'repo' });
|
|
1539
|
+
const run = runScript(project, script);
|
|
1540
|
+
expect(run.status).toBe(0);
|
|
1541
|
+
expect(isPng(artifact(project, 'ui-fidelity/references/HomeView.png'))).toBe(true);
|
|
1542
|
+
expect(isPng(artifact(project, 'ui-fidelity/references/SettingsView.png'))).toBe(true);
|
|
1543
|
+
const result = readResult(project);
|
|
1544
|
+
expect(result.screens.map((s) => s.status)).toEqual(['rendered', 'rendered']);
|
|
1545
|
+
expect(result.screens.map((s) => s.reference_image_path)).toEqual([
|
|
1546
|
+
'ui-fidelity/references/HomeView.png',
|
|
1547
|
+
'ui-fidelity/references/SettingsView.png',
|
|
1548
|
+
]);
|
|
1549
|
+
});
|
|
1550
|
+
test('fails one screen when its committed reference is missing, renders the rest', async () => {
|
|
1551
|
+
const project = makeProject({
|
|
1552
|
+
screens: { HomeView: 'refs/home.png', GhostView: 'refs/ghost.png' },
|
|
1553
|
+
});
|
|
1554
|
+
mkdirSync(join(project.dir, 'refs'), { recursive: true });
|
|
1555
|
+
writeFileSync(join(project.dir, 'refs', 'home.png'), Buffer.concat([PNG_MAGIC, Buffer.from('ref:home')]));
|
|
1556
|
+
// refs/ghost.png intentionally absent.
|
|
1557
|
+
const run = runScript(project, await buildScript(project, { reference_source: 'repo' }));
|
|
1558
|
+
expect(run.status).not.toBe(0);
|
|
1559
|
+
const result = readResult(project);
|
|
1560
|
+
const home = result.screens.find((s) => s.screen === 'HomeView');
|
|
1561
|
+
const ghost = result.screens.find((s) => s.screen === 'GhostView');
|
|
1562
|
+
expect(home?.status).toBe('rendered');
|
|
1563
|
+
expect(ghost?.status).toBe('render_failed');
|
|
1564
|
+
expect(ghost?.error?.code).toBe('REFERENCE_MISSING');
|
|
1565
|
+
});
|
|
1566
|
+
});
|
|
1512
1567
|
//# sourceMappingURL=ui-fidelity-render.test.js.map
|