@invarn/cibuild 2.4.0 → 2.4.2
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 +29 -15
- package/dist/src/yaml/steps/render-post-processor.d.ts +6 -0
- package/dist/src/yaml/steps/render-post-processor.d.ts.map +1 -1
- package/dist/src/yaml/steps/render-post-processor.js +19 -5
- package/dist/src/yaml/steps/render-post-processor.test.js +50 -0
- package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts +16 -5
- package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts.map +1 -1
- package/dist/src/yaml/steps/ui-fidelity-render-android.js +183 -10
- package/dist/src/yaml/steps/ui-fidelity-render-android.test.js +156 -1
- package/dist/src/yaml/steps/ui-fidelity-render.d.ts +20 -4
- package/dist/src/yaml/steps/ui-fidelity-render.d.ts.map +1 -1
- package/dist/src/yaml/steps/ui-fidelity-render.js +185 -9
- package/dist/src/yaml/steps/ui-fidelity-render.test.js +155 -2
- package/package.json +1 -1
|
@@ -52,6 +52,12 @@ export interface TrimDims {
|
|
|
52
52
|
export interface TrimHelperInvocation {
|
|
53
53
|
cmd: string;
|
|
54
54
|
args: string[];
|
|
55
|
+
/** Child environment override. Set only for the pkg self-exec, where the
|
|
56
|
+
* node-invoke sentinel (PKG_EXECPATH) must be stripped so the re-executed ci
|
|
57
|
+
* binary runs its baked cli entrypoint (dispatching `fidelity-trim`) instead
|
|
58
|
+
* of re-entering Node-emulation and treating it as a script path. Absent for
|
|
59
|
+
* the other homes, which spawn with the inherited environment unchanged. */
|
|
60
|
+
env?: Record<string, string>;
|
|
55
61
|
}
|
|
56
62
|
/**
|
|
57
63
|
* The platform-neutral helpers embedded in the trim-stage script.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-post-processor.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/render-post-processor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAMH;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,wBAAwB,EAAE,
|
|
1
|
+
{"version":3,"file":"render-post-processor.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/render-post-processor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAMH;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,wBAAwB,EAAE,MA6HtC,CAAC;AAEF,iFAAiF;AACjF,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE5C,iFAAiF;AACjF,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,YAAY,CAAC;IACvB,SAAS,EAAE,YAAY,CAAC;IACxB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,yDAAyD;AACzD,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,EAAE,CAAC;IACf;;;;iFAI6E;IAC7E,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,sBAAsB;IACrC,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAAC;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,oBAAoB,CAAC;IAC3D,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG;QAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1F;AAED;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,SAAS,CAAC,EAAE;IACpD,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,GAAG,sBAAsB,CASzB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAqBhD"}
|
|
@@ -88,7 +88,22 @@ function trimHelperInvocation(args) {
|
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
if (process.pkg) {
|
|
91
|
-
|
|
91
|
+
// The runner spawns this script as 'node <script>', which the pkg binary
|
|
92
|
+
// intercepts and runs in Node-emulation mode -- so process.env carries
|
|
93
|
+
// pkg's node-invoke sentinel (PKG_EXECPATH). Re-execing process.execPath
|
|
94
|
+
// with it still set makes the child ci binary ALSO enter Node-emulation and
|
|
95
|
+
// treat 'fidelity-trim' as a main-module script path (Cannot find module
|
|
96
|
+
// '<cwd>/fidelity-trim'), never reaching the subcommand dispatch -- so the
|
|
97
|
+
// trim silently fails and untrimmed renders ship. Strip PKG_EXECPATH from
|
|
98
|
+
// the child env so the re-exec runs the baked cli entrypoint, which routes
|
|
99
|
+
// 'fidelity-trim' to the in-binary helper (with bundled pngjs).
|
|
100
|
+
var invocation = { cmd: process.execPath, args: ['fidelity-trim'].concat(args) };
|
|
101
|
+
if (process.env.PKG_EXECPATH !== undefined) {
|
|
102
|
+
var appEnv = Object.assign({}, process.env);
|
|
103
|
+
delete appEnv.PKG_EXECPATH;
|
|
104
|
+
invocation.env = appEnv;
|
|
105
|
+
}
|
|
106
|
+
return invocation;
|
|
92
107
|
}
|
|
93
108
|
return { cmd: 'ci', args: ['fidelity-trim'].concat(args) };
|
|
94
109
|
}
|
|
@@ -97,10 +112,9 @@ function trimHelperInvocation(args) {
|
|
|
97
112
|
// stdout line, and exit codes are the retired CoreGraphics helper's contract).
|
|
98
113
|
function runTrimHelper(args) {
|
|
99
114
|
var invocation = trimHelperInvocation(args);
|
|
100
|
-
var
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
});
|
|
115
|
+
var options = { encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024 };
|
|
116
|
+
if (invocation.env !== undefined) options.env = invocation.env;
|
|
117
|
+
var result = cp.spawnSync(invocation.cmd, invocation.args, options);
|
|
104
118
|
if (result.stdout) process.stdout.write(result.stdout);
|
|
105
119
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
106
120
|
if (result.error) {
|
|
@@ -97,6 +97,56 @@ describe('trim stage — helper invocation resolution', () => {
|
|
|
97
97
|
args: ['fidelity-trim', 'in.png', 'out.png'],
|
|
98
98
|
});
|
|
99
99
|
});
|
|
100
|
+
// The production runner spawns the render step as `node <script>`, which the
|
|
101
|
+
// pkg-built ci binary intercepts and runs in Node-emulation mode — so the
|
|
102
|
+
// running script's process.env carries pkg's node-invoke sentinel
|
|
103
|
+
// (PKG_EXECPATH). If the trim re-exec inherits it, the child ci binary ALSO
|
|
104
|
+
// enters Node-emulation and treats 'fidelity-trim' as a main-module script
|
|
105
|
+
// path ("Cannot find module '<cwd>/fidelity-trim'") instead of dispatching
|
|
106
|
+
// the subcommand — the exact failure observed in production, which silently
|
|
107
|
+
// ships untrimmed renders. The re-exec must clear PKG_EXECPATH so the binary
|
|
108
|
+
// runs its baked cli entrypoint.
|
|
109
|
+
test('under pkg node-emulation the re-exec clears PKG_EXECPATH so the binary runs its app entrypoint', () => {
|
|
110
|
+
const { trimHelperInvocation } = internalsWith({
|
|
111
|
+
env: { PKG_EXECPATH: 'PKG_INVOKE_NODEJS', PATH: '/usr/bin:/bin' },
|
|
112
|
+
pkg: {},
|
|
113
|
+
execPath: '/opt/homebrew/bin/ci',
|
|
114
|
+
});
|
|
115
|
+
const invocation = trimHelperInvocation(['in.png', 'out.png']);
|
|
116
|
+
expect(invocation.cmd).toBe('/opt/homebrew/bin/ci');
|
|
117
|
+
expect(invocation.args).toEqual(['fidelity-trim', 'in.png', 'out.png']);
|
|
118
|
+
// A child env is supplied with the node-invoke sentinel removed, but the
|
|
119
|
+
// rest of the environment (PATH, etc.) preserved.
|
|
120
|
+
expect(invocation.env).toBeDefined();
|
|
121
|
+
expect(invocation.env?.PKG_EXECPATH).toBeUndefined();
|
|
122
|
+
expect(invocation.env?.PATH).toBe('/usr/bin:/bin');
|
|
123
|
+
});
|
|
124
|
+
test('runTrimHelper spawns the pkg re-exec with a PKG_EXECPATH-cleared env', () => {
|
|
125
|
+
const calls = [];
|
|
126
|
+
const fakeCp = {
|
|
127
|
+
spawnSync: (...args) => {
|
|
128
|
+
calls.push(args);
|
|
129
|
+
return { status: 0, stdout: 'TRIMDIMS 10 10 0 0 10 10\n', stderr: '' };
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
const { runTrimHelper } = getPostProcessorInternals({
|
|
133
|
+
cp: fakeCp,
|
|
134
|
+
processRef: {
|
|
135
|
+
env: { PKG_EXECPATH: 'PKG_INVOKE_NODEJS', FOO: 'bar' },
|
|
136
|
+
pkg: {},
|
|
137
|
+
execPath: '/opt/homebrew/bin/ci',
|
|
138
|
+
stdout: { write: () => true },
|
|
139
|
+
stderr: { write: () => true },
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
runTrimHelper(['in.png', 'out.png']);
|
|
143
|
+
expect(calls).toHaveLength(1);
|
|
144
|
+
const [cmd, , options] = calls[0];
|
|
145
|
+
expect(cmd).toBe('/opt/homebrew/bin/ci');
|
|
146
|
+
expect(options.env).toBeDefined();
|
|
147
|
+
expect(options.env?.PKG_EXECPATH).toBeUndefined();
|
|
148
|
+
expect(options.env?.FOO).toBe('bar');
|
|
149
|
+
});
|
|
100
150
|
test('runTrimHelper reports a spawn error as status 127 with the message appended', () => {
|
|
101
151
|
const calls = [];
|
|
102
152
|
const fakeCp = {
|
|
@@ -76,12 +76,16 @@ export interface UiFidelityRenderAndroidInputs {
|
|
|
76
76
|
*/
|
|
77
77
|
export declare const DEFAULT_GRADLE_TASK = "recordRoborazziDebug";
|
|
78
78
|
/** Valid values for the package_source input. */
|
|
79
|
-
export declare const PACKAGE_SOURCES: readonly ["repo", "inputs"];
|
|
79
|
+
export declare const PACKAGE_SOURCES: readonly ["repo", "inputs", "url"];
|
|
80
80
|
export type PackageSource = (typeof PACKAGE_SOURCES)[number];
|
|
81
81
|
/**
|
|
82
82
|
* Default package source: the Gradle project ships as package.tar.gz in the
|
|
83
83
|
* run-inputs directory (the agent-upload path). "repo" instead renders the
|
|
84
84
|
* committed project at package_path, for a triggered (agent-less) run.
|
|
85
|
+
* "url" (fidelity-linux 03) downloads the same package.tar.gz from a signed
|
|
86
|
+
* package_url into the run-inputs directory and then proceeds byte-for-byte
|
|
87
|
+
* like inputs mode — the clone-free delivery for triggered runs, on macOS
|
|
88
|
+
* and Linux workers alike.
|
|
85
89
|
*/
|
|
86
90
|
export declare const DEFAULT_PACKAGE_SOURCE: PackageSource;
|
|
87
91
|
/** Valid values for the reference_source input. */
|
|
@@ -107,16 +111,23 @@ export interface AndroidRenderScriptConfig {
|
|
|
107
111
|
scale: number;
|
|
108
112
|
gradleTask: string;
|
|
109
113
|
/**
|
|
110
|
-
* Only present
|
|
111
|
-
*
|
|
114
|
+
* Only present when package_source was explicitly set to a non-default mode
|
|
115
|
+
* ("repo" or "url"). When absent, the project ships as package.tar.gz and is
|
|
112
116
|
* extracted/validated exactly as the default, byte-identical script.
|
|
113
117
|
*/
|
|
114
118
|
packageSource?: PackageSource;
|
|
115
119
|
/**
|
|
116
120
|
* Repo-relative path to the committed Gradle project. Present (and required)
|
|
117
|
-
*
|
|
121
|
+
* with packageSource "repo" (the checkout dir to render) and "url" (the
|
|
122
|
+
* prefix committed reference paths are resolved under, inside the extracted
|
|
123
|
+
* package); ignored otherwise.
|
|
118
124
|
*/
|
|
119
125
|
packagePath?: string;
|
|
126
|
+
/**
|
|
127
|
+
* Signed download URL for package.tar.gz. Present (and required) only with
|
|
128
|
+
* packageSource "url"; ignored otherwise.
|
|
129
|
+
*/
|
|
130
|
+
packageUrl?: string;
|
|
120
131
|
/**
|
|
121
132
|
* Only present (and only ever "repo") when reference_source was explicitly set
|
|
122
133
|
* to "repo". When absent, references are read from `.ci/inputs/<basename>` and
|
|
@@ -138,7 +149,7 @@ export declare function generateAndroidRenderScript(config: AndroidRenderScriptC
|
|
|
138
149
|
* script performs the render at runtime on the macOS runner.
|
|
139
150
|
*/
|
|
140
151
|
export declare class UiFidelityRenderAndroidStepExecutor extends BaseStepExecutor {
|
|
141
|
-
getValidationRequirements(
|
|
152
|
+
getValidationRequirements(inputs: UiFidelityRenderAndroidInputs, _env: Record<string, string>, _config: CIConfig): ValidationRequirement[];
|
|
142
153
|
execute(inputs: UiFidelityRenderAndroidInputs, _env: Record<string, string>, _config: CIConfig): Promise<StepDef>;
|
|
143
154
|
}
|
|
144
155
|
//# sourceMappingURL=ui-fidelity-render-android.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui-fidelity-render-android.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ui-fidelity-render-android.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;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;AAMpE,sDAAsD;AACtD,MAAM,WAAW,6BAA6B;IAC5C;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAE1D,iDAAiD;AACjD,eAAO,MAAM,eAAe,
|
|
1
|
+
{"version":3,"file":"ui-fidelity-render-android.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ui-fidelity-render-android.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;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;AAMpE,sDAAsD;AACtD,MAAM,WAAW,6BAA6B;IAC5C;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAE1D,iDAAiD;AACjD,eAAO,MAAM,eAAe,oCAAqC,CAAC;AAClE,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7D;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,EAAE,aAAwB,CAAC;AAE9D,mDAAmD;AACnD,eAAO,MAAM,iBAAiB,6BAA8B,CAAC;AAC7D,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjE;;;GAGG;AACH,eAAO,MAAM,wBAAwB,EAAE,eAA0B,CAAC;AAElE,gFAAgF;AAChF,eAAO,MAAM,wBAAwB,mBAAmB,CAAC;AAEzD,4DAA4D;AAC5D,eAAO,MAAM,gBAAgB,0BAA0B,CAAC;AAExD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED,8EAA8E;AAC9E,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC;AAurCD;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,yBAAyB,GAAG,MAAM,CA0BrF;AAED;;;GAGG;AACH,qBAAa,mCAAoC,SAAQ,gBAAgB;IACvE,yBAAyB,CACvB,MAAM,EAAE,6BAA6B,EACrC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,qBAAqB,EAAE;IAyBpB,OAAO,CACX,MAAM,EAAE,6BAA6B,EACrC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,OAAO,CAAC;CAiGpB"}
|
|
@@ -63,11 +63,15 @@ import { RENDER_SCRIPT_SOURCE_DIGEST_STAGE } from '../fidelity-input-digest.js';
|
|
|
63
63
|
*/
|
|
64
64
|
export const DEFAULT_GRADLE_TASK = 'recordRoborazziDebug';
|
|
65
65
|
/** Valid values for the package_source input. */
|
|
66
|
-
export const PACKAGE_SOURCES = ['repo', 'inputs'];
|
|
66
|
+
export const PACKAGE_SOURCES = ['repo', 'inputs', 'url'];
|
|
67
67
|
/**
|
|
68
68
|
* Default package source: the Gradle project ships as package.tar.gz in the
|
|
69
69
|
* run-inputs directory (the agent-upload path). "repo" instead renders the
|
|
70
70
|
* committed project at package_path, for a triggered (agent-less) run.
|
|
71
|
+
* "url" (fidelity-linux 03) downloads the same package.tar.gz from a signed
|
|
72
|
+
* package_url into the run-inputs directory and then proceeds byte-for-byte
|
|
73
|
+
* like inputs mode — the clone-free delivery for triggered runs, on macOS
|
|
74
|
+
* and Linux workers alike.
|
|
71
75
|
*/
|
|
72
76
|
export const DEFAULT_PACKAGE_SOURCE = 'inputs';
|
|
73
77
|
/** Valid values for the reference_source input. */
|
|
@@ -1114,6 +1118,145 @@ function buildAndroidReferenceRepoMain(base) {
|
|
|
1114
1118
|
' }\n' +
|
|
1115
1119
|
' var source = path.resolve(refPath);');
|
|
1116
1120
|
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Builds the runtime main for package_source "url" (fidelity-linux 03) by
|
|
1123
|
+
* patching the default (inputs) runtime text. Three departures from the
|
|
1124
|
+
* default, all forced by the same fact — a clone-free worker has no checkout:
|
|
1125
|
+
*
|
|
1126
|
+
* 1. The package is DOWNLOADED (curl, already a runner requirement in this
|
|
1127
|
+
* mode) to `.ci/inputs/package.tar.gz` and extracted BEFORE the per-screen
|
|
1128
|
+
* reference stage, because committed references travel inside it. A failed
|
|
1129
|
+
* download is one structured per-build error, never per-screen noise.
|
|
1130
|
+
* 2. Each screen's reference is the committed repo-relative path resolved
|
|
1131
|
+
* INSIDE the extracted package: the path must sit under packagePath, and
|
|
1132
|
+
* the remainder resolves against the extracted project root.
|
|
1133
|
+
* 3. The render stage reuses the already-extracted root (no second extract);
|
|
1134
|
+
* cleanup mirrors the inputs path. The result document records
|
|
1135
|
+
* package_source "url".
|
|
1136
|
+
*
|
|
1137
|
+
* ANDROID_RENDER_SCRIPT_MAIN itself is never modified, so omitting
|
|
1138
|
+
* package_source keeps the generated script byte-identical to the default.
|
|
1139
|
+
*/
|
|
1140
|
+
function buildAndroidPackageUrlMain(base) {
|
|
1141
|
+
let main = base;
|
|
1142
|
+
// The result document advertises the url package source.
|
|
1143
|
+
main = replaceOnce(main, "package_source: 'inputs',", "package_source: 'url',");
|
|
1144
|
+
// Download + extract FIRST — the reference stage below reads from inside
|
|
1145
|
+
// the extracted package.
|
|
1146
|
+
main = replaceOnce(main, ' // 2. Per-screen validation + reference copies. Each screen gets its OWN copy\n' +
|
|
1147
|
+
' // named by screen, even when two screens share a reference basename.\n' +
|
|
1148
|
+
' currentEntries.forEach(function (entry) {', ' // 2a. Download + extract the shipped package FIRST (URL mode): committed\n' +
|
|
1149
|
+
' // references live inside it, so the per-screen reference copies below\n' +
|
|
1150
|
+
' // need the extracted project root. A failed download or an invalid\n' +
|
|
1151
|
+
' // archive is ONE structured per-build error; per-screen reference\n' +
|
|
1152
|
+
' // work then stops cleanly (no bogus REFERENCE_MISSING noise).\n' +
|
|
1153
|
+
' var urlProjectRoot = null;\n' +
|
|
1154
|
+
' (function () {\n' +
|
|
1155
|
+
' try {\n' +
|
|
1156
|
+
' fs.mkdirSync(inputsDir, { recursive: true });\n' +
|
|
1157
|
+
' } catch (mkdirError) {\n' +
|
|
1158
|
+
' // statSync below reports the real failure\n' +
|
|
1159
|
+
' }\n' +
|
|
1160
|
+
' var dest = path.join(inputsDir, PACKAGE_ARCHIVE_NAME);\n' +
|
|
1161
|
+
" log('downloading render package');\n" +
|
|
1162
|
+
' var download = cp.spawnSync(\n' +
|
|
1163
|
+
" 'curl',\n" +
|
|
1164
|
+
" ['-fsSL', '--retry', '2', '--max-time', '300', '-o', dest, CONFIG.packageUrl],\n" +
|
|
1165
|
+
" { encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024 }\n" +
|
|
1166
|
+
' );\n' +
|
|
1167
|
+
' if (download.error) {\n' +
|
|
1168
|
+
' download.status = download.status == null ? 127 : download.status;\n' +
|
|
1169
|
+
" download.stderr = (download.stderr || '') + String(download.error.message || download.error);\n" +
|
|
1170
|
+
' }\n' +
|
|
1171
|
+
' if (download.status !== 0) {\n' +
|
|
1172
|
+
' setBuildError(\n' +
|
|
1173
|
+
" 'ANDROID_PACKAGE_DOWNLOAD_FAILED',\n" +
|
|
1174
|
+
" 'render package download failed (curl exit ' + download.status + '):\\n' + outputTail(download)\n" +
|
|
1175
|
+
' );\n' +
|
|
1176
|
+
' return;\n' +
|
|
1177
|
+
' }\n' +
|
|
1178
|
+
' urlProjectRoot = prepareShippedProject();\n' +
|
|
1179
|
+
' })();\n' +
|
|
1180
|
+
'\n' +
|
|
1181
|
+
' // 2. Per-screen validation + reference copies. Each screen gets its OWN copy\n' +
|
|
1182
|
+
' // named by screen, even when two screens share a reference basename.\n' +
|
|
1183
|
+
' currentEntries.forEach(function (entry) {');
|
|
1184
|
+
// References resolve INSIDE the extracted package: repo-relative path minus
|
|
1185
|
+
// the packagePath prefix, against the extracted project root.
|
|
1186
|
+
main = replaceOnce(main, 'var basename = params.screens[entry.screen];\n' +
|
|
1187
|
+
' if (\n' +
|
|
1188
|
+
" typeof basename !== 'string' || basename === '' ||\n" +
|
|
1189
|
+
" basename === '.' || basename === '..' ||\n" +
|
|
1190
|
+
' basename !== path.basename(basename)\n' +
|
|
1191
|
+
' ) {\n' +
|
|
1192
|
+
' setError(\n' +
|
|
1193
|
+
' entry,\n' +
|
|
1194
|
+
" 'REFERENCE_MISSING',\n" +
|
|
1195
|
+
" 'reference for ' + entry.screen + ' must be a plain file basename inside ' +\n" +
|
|
1196
|
+
" inputsDir + ', got: ' + JSON.stringify(basename)\n" +
|
|
1197
|
+
' );\n' +
|
|
1198
|
+
' return;\n' +
|
|
1199
|
+
' }\n' +
|
|
1200
|
+
' var source = path.join(inputsDir, basename);', 'var refPath = params.screens[entry.screen];\n' +
|
|
1201
|
+
" if (typeof refPath !== 'string' || refPath === '') {\n" +
|
|
1202
|
+
' setError(\n' +
|
|
1203
|
+
' entry,\n' +
|
|
1204
|
+
" 'REFERENCE_MISSING',\n" +
|
|
1205
|
+
" 'reference for ' + entry.screen + ' must be a non-empty repo-relative path, got: ' +\n" +
|
|
1206
|
+
' JSON.stringify(refPath)\n' +
|
|
1207
|
+
' );\n' +
|
|
1208
|
+
' return;\n' +
|
|
1209
|
+
' }\n' +
|
|
1210
|
+
' if (urlProjectRoot === null) {\n' +
|
|
1211
|
+
' // Package unavailable: the per-build error above carries the cause.\n' +
|
|
1212
|
+
' return;\n' +
|
|
1213
|
+
' }\n' +
|
|
1214
|
+
" var packagePrefix = CONFIG.packagePath.replace(/^\\.\\//, '').replace(/\\/+$/, '');\n" +
|
|
1215
|
+
" var normalizedRef = refPath.replace(/^\\.\\//, '');\n" +
|
|
1216
|
+
" if (normalizedRef.indexOf(packagePrefix + '/') !== 0) {\n" +
|
|
1217
|
+
' setError(\n' +
|
|
1218
|
+
' entry,\n' +
|
|
1219
|
+
" 'REFERENCE_MISSING',\n" +
|
|
1220
|
+
" 'reference for ' + entry.screen + ' must live inside the shipped package (' +\n" +
|
|
1221
|
+
" packagePrefix + '), got: ' + JSON.stringify(refPath)\n" +
|
|
1222
|
+
' );\n' +
|
|
1223
|
+
' return;\n' +
|
|
1224
|
+
' }\n' +
|
|
1225
|
+
' var source = path.join(urlProjectRoot, normalizedRef.slice(packagePrefix.length + 1));');
|
|
1226
|
+
// Render from the package extracted in stage 2a — no second extract.
|
|
1227
|
+
main = replaceOnce(main, ' // 3. Extract + validate the shipped Gradle project, then render the\n' +
|
|
1228
|
+
' // renderable screens. The project stage runs even when no screen is\n' +
|
|
1229
|
+
' // renderable, so packaging mistakes are always reported.\n' +
|
|
1230
|
+
' var renderable = currentEntries.filter(function (entry) { return entry.error === null; });\n' +
|
|
1231
|
+
' try {\n' +
|
|
1232
|
+
' var projectRoot = prepareShippedProject();\n' +
|
|
1233
|
+
' if (projectRoot !== null) {\n' +
|
|
1234
|
+
' if (renderable.length > 0) {\n' +
|
|
1235
|
+
' renderScreens(renderable, projectRoot);\n' +
|
|
1236
|
+
' } else if (currentEntries.length > 0) {\n' +
|
|
1237
|
+
" log('no renderable screens, skipping render');\n" +
|
|
1238
|
+
' }\n' +
|
|
1239
|
+
' }\n' +
|
|
1240
|
+
' } finally {\n' +
|
|
1241
|
+
' cleanupShippedProject();\n' +
|
|
1242
|
+
' }', ' // 3. Render from the package downloaded + extracted in stage 2a. The\n' +
|
|
1243
|
+
' // extraction already reported packaging mistakes as the per-build\n' +
|
|
1244
|
+
' // error, so a null root here just skips the render.\n' +
|
|
1245
|
+
' var renderable = currentEntries.filter(function (entry) { return entry.error === null; });\n' +
|
|
1246
|
+
' try {\n' +
|
|
1247
|
+
' if (urlProjectRoot !== null) {\n' +
|
|
1248
|
+
' if (renderable.length > 0) {\n' +
|
|
1249
|
+
' renderScreens(renderable, urlProjectRoot);\n' +
|
|
1250
|
+
' } else if (currentEntries.length > 0) {\n' +
|
|
1251
|
+
" log('no renderable screens, skipping render');\n" +
|
|
1252
|
+
' }\n' +
|
|
1253
|
+
' }\n' +
|
|
1254
|
+
' } finally {\n' +
|
|
1255
|
+
' cleanupShippedProject();\n' +
|
|
1256
|
+
' }');
|
|
1257
|
+
return main;
|
|
1258
|
+
}
|
|
1259
|
+
const ANDROID_RENDER_SCRIPT_MAIN_URL = buildAndroidPackageUrlMain(ANDROID_RENDER_SCRIPT_MAIN);
|
|
1117
1260
|
/**
|
|
1118
1261
|
* Generates the self-contained runtime node script for the step. Pure function
|
|
1119
1262
|
* of its config — exported so tests can execute the script directly.
|
|
@@ -1123,10 +1266,15 @@ function buildAndroidReferenceRepoMain(base) {
|
|
|
1123
1266
|
* runtime is used).
|
|
1124
1267
|
*/
|
|
1125
1268
|
export function generateAndroidRenderScript(config) {
|
|
1126
|
-
let main = config.packageSource === 'repo'
|
|
1269
|
+
let main = config.packageSource === 'repo'
|
|
1270
|
+
? ANDROID_RENDER_SCRIPT_MAIN_REPO
|
|
1271
|
+
: config.packageSource === 'url'
|
|
1272
|
+
? ANDROID_RENDER_SCRIPT_MAIN_URL
|
|
1273
|
+
: ANDROID_RENDER_SCRIPT_MAIN;
|
|
1127
1274
|
// Reference-from-repo is an independent dimension layered on top of whichever
|
|
1128
1275
|
// package base applies. Absent → byte-identical reference block (default
|
|
1129
|
-
// basename behaviour).
|
|
1276
|
+
// basename behaviour). URL mode embeds its own reference sourcing and the
|
|
1277
|
+
// executor rejects the combination before this point.
|
|
1130
1278
|
if (config.referenceSource === 'repo') {
|
|
1131
1279
|
main = buildAndroidReferenceRepoMain(main);
|
|
1132
1280
|
}
|
|
@@ -1148,13 +1296,19 @@ export function generateAndroidRenderScript(config) {
|
|
|
1148
1296
|
* script performs the render at runtime on the macOS runner.
|
|
1149
1297
|
*/
|
|
1150
1298
|
export class UiFidelityRenderAndroidStepExecutor extends BaseStepExecutor {
|
|
1151
|
-
getValidationRequirements(
|
|
1299
|
+
getValidationRequirements(inputs, _env, _config) {
|
|
1152
1300
|
// No platform-toolchain requirement: trim/align is pure JS inside the ci
|
|
1153
1301
|
// binary (fidelity-linux 06), so the step validates on macOS and Linux
|
|
1154
1302
|
// runners alike.
|
|
1155
|
-
|
|
1303
|
+
const requirements = [
|
|
1156
1304
|
this.requireCommand('tar', 'Archive tool used to extract the shipped Gradle project', 'Install tar on the runner'),
|
|
1157
1305
|
];
|
|
1306
|
+
// URL mode (fidelity-linux 03) downloads the package at runtime; existing
|
|
1307
|
+
// inputs/repo runs gain no new requirement.
|
|
1308
|
+
if (this.getInput(inputs, 'package_source', undefined) === 'url') {
|
|
1309
|
+
requirements.push(this.requireCommand('curl', 'Downloader used to fetch the render package by signed URL', 'Install curl on the runner'));
|
|
1310
|
+
}
|
|
1311
|
+
return requirements;
|
|
1158
1312
|
}
|
|
1159
1313
|
async execute(inputs, _env, _config) {
|
|
1160
1314
|
const stepName = 'ui-fidelity-render-android';
|
|
@@ -1165,9 +1319,11 @@ export class UiFidelityRenderAndroidStepExecutor extends BaseStepExecutor {
|
|
|
1165
1319
|
'expected a Gradle task path (letters, digits, and : - _)');
|
|
1166
1320
|
}
|
|
1167
1321
|
// package_source: validate when provided, but only record (and patch the
|
|
1168
|
-
// script for) the non-default
|
|
1322
|
+
// script for) the non-default modes so omitting it — or setting the default
|
|
1169
1323
|
// "inputs" — keeps the default archive-extract script byte-identical. In
|
|
1170
|
-
// repo mode package_path is required (the committed project to render)
|
|
1324
|
+
// repo mode package_path is required (the committed project to render); in
|
|
1325
|
+
// url mode (fidelity-linux 03) package_url and package_path are required
|
|
1326
|
+
// (the signed download + the prefix committed reference paths sit under).
|
|
1171
1327
|
const rawPackageSource = this.getInput(inputs, 'package_source', undefined);
|
|
1172
1328
|
let packageSource;
|
|
1173
1329
|
if (rawPackageSource !== undefined) {
|
|
@@ -1176,14 +1332,21 @@ export class UiFidelityRenderAndroidStepExecutor extends BaseStepExecutor {
|
|
|
1176
1332
|
throw new Error(`Invalid package_source '${value}' for step '${stepName}': ` +
|
|
1177
1333
|
`expected one of: ${PACKAGE_SOURCES.join(', ')}`);
|
|
1178
1334
|
}
|
|
1179
|
-
if (value === 'repo') {
|
|
1180
|
-
packageSource =
|
|
1335
|
+
if (value === 'repo' || value === 'url') {
|
|
1336
|
+
packageSource = value;
|
|
1181
1337
|
}
|
|
1182
1338
|
}
|
|
1183
1339
|
let packagePath;
|
|
1184
|
-
if (packageSource === 'repo') {
|
|
1340
|
+
if (packageSource === 'repo' || packageSource === 'url') {
|
|
1185
1341
|
packagePath = String(this.getRequiredInput(inputs, 'package_path', stepName));
|
|
1186
1342
|
}
|
|
1343
|
+
let packageUrl;
|
|
1344
|
+
if (packageSource === 'url') {
|
|
1345
|
+
packageUrl = String(this.getRequiredInput(inputs, 'package_url', stepName));
|
|
1346
|
+
if (!/^https?:\/\//.test(packageUrl)) {
|
|
1347
|
+
throw new Error(`Invalid package_url for step '${stepName}': expected an http(s) URL`);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1187
1350
|
// reference_source: validate when provided, but only record (and patch the
|
|
1188
1351
|
// script for) the non-default "repo" so omitting it — or setting the default
|
|
1189
1352
|
// "inputs" — keeps the default basename reference block intact.
|
|
@@ -1195,6 +1358,13 @@ export class UiFidelityRenderAndroidStepExecutor extends BaseStepExecutor {
|
|
|
1195
1358
|
throw new Error(`Invalid reference_source '${value}' for step '${stepName}': ` +
|
|
1196
1359
|
`expected one of: ${REFERENCE_SOURCES.join(', ')}`);
|
|
1197
1360
|
}
|
|
1361
|
+
// URL mode owns reference sourcing (inside the extracted package) —
|
|
1362
|
+
// an explicit reference_source alongside it is a contradiction.
|
|
1363
|
+
if (packageSource === 'url') {
|
|
1364
|
+
throw new Error(`Invalid reference_source for step '${stepName}': ` +
|
|
1365
|
+
"package_source 'url' resolves references inside the downloaded " +
|
|
1366
|
+
'package; omit reference_source');
|
|
1367
|
+
}
|
|
1198
1368
|
if (value === 'repo') {
|
|
1199
1369
|
referenceSource = 'repo';
|
|
1200
1370
|
}
|
|
@@ -1206,6 +1376,9 @@ export class UiFidelityRenderAndroidStepExecutor extends BaseStepExecutor {
|
|
|
1206
1376
|
if (packagePath !== undefined) {
|
|
1207
1377
|
config.packagePath = packagePath;
|
|
1208
1378
|
}
|
|
1379
|
+
if (packageUrl !== undefined) {
|
|
1380
|
+
config.packageUrl = packageUrl;
|
|
1381
|
+
}
|
|
1209
1382
|
if (referenceSource !== undefined) {
|
|
1210
1383
|
config.referenceSource = referenceSource;
|
|
1211
1384
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
2
|
import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
|
|
3
3
|
import { gzipSync } from 'node:zlib';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
@@ -826,6 +826,161 @@ describe('package_source: repo', () => {
|
|
|
826
826
|
expect(screen?.reference_image_path).toBe('ui-fidelity/references/XProof.png');
|
|
827
827
|
});
|
|
828
828
|
});
|
|
829
|
+
// Clone-free package delivery (fidelity-linux 03): the dispatched YAML carries
|
|
830
|
+
// a signed package_url; the runtime downloads it to .ci/inputs/package.tar.gz
|
|
831
|
+
// and proceeds byte-for-byte like inputs mode, resolving each screen's
|
|
832
|
+
// committed reference INSIDE the extracted package (repo-relative path minus
|
|
833
|
+
// the package_path prefix) — there is no checkout on a clone-free worker.
|
|
834
|
+
describe('package_source: url', () => {
|
|
835
|
+
/**
|
|
836
|
+
* Serve one tar.gz (or a 404) from a loopback HTTP server IN A CHILD
|
|
837
|
+
* PROCESS. runScript executes the generated script via spawnSync, which
|
|
838
|
+
* blocks this process's event loop — an in-process server would deadlock
|
|
839
|
+
* against the script's curl. The child prints its port once listening.
|
|
840
|
+
*/
|
|
841
|
+
const SERVE_PACKAGE_SRC = [
|
|
842
|
+
"var http = require('http');",
|
|
843
|
+
"var fs = require('fs');",
|
|
844
|
+
'var filePath = process.argv[1];',
|
|
845
|
+
'var server = http.createServer(function (req, res) {',
|
|
846
|
+
" if (filePath === '-') { res.statusCode = 404; res.end('gone'); return; }",
|
|
847
|
+
" res.setHeader('content-type', 'application/gzip');",
|
|
848
|
+
' res.end(fs.readFileSync(filePath));',
|
|
849
|
+
'});',
|
|
850
|
+
"server.listen(0, '127.0.0.1', function () {",
|
|
851
|
+
" console.log('PORT ' + server.address().port);",
|
|
852
|
+
'});',
|
|
853
|
+
].join('\n');
|
|
854
|
+
async function servePackage(archive) {
|
|
855
|
+
let filePath = '-';
|
|
856
|
+
if (archive !== null) {
|
|
857
|
+
const dir = mkdtempSync(join(tmpdir(), 'ui-fidelity-url-serve-'));
|
|
858
|
+
tempDirs.push(dir);
|
|
859
|
+
filePath = join(dir, 'package.tar.gz');
|
|
860
|
+
writeFileSync(filePath, archive);
|
|
861
|
+
}
|
|
862
|
+
const child = spawn('node', ['-e', SERVE_PACKAGE_SRC, filePath], {
|
|
863
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
864
|
+
});
|
|
865
|
+
const port = await new Promise((resolvePort, rejectPort) => {
|
|
866
|
+
let buffered = '';
|
|
867
|
+
const timer = setTimeout(() => rejectPort(new Error('package server did not start')), 10_000);
|
|
868
|
+
child.stdout.on('data', (chunk) => {
|
|
869
|
+
buffered += chunk.toString();
|
|
870
|
+
const match = /PORT (\d+)/.exec(buffered);
|
|
871
|
+
if (match) {
|
|
872
|
+
clearTimeout(timer);
|
|
873
|
+
resolvePort(Number(match[1]));
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
child.on('exit', () => rejectPort(new Error('package server exited early')));
|
|
877
|
+
});
|
|
878
|
+
return {
|
|
879
|
+
url: `http://127.0.0.1:${port}/api/fidelity/packages/blob1?exp=1&sig=ab`,
|
|
880
|
+
close: () => new Promise((resolveClose) => {
|
|
881
|
+
child.once('exit', () => resolveClose(undefined));
|
|
882
|
+
child.kill('SIGKILL');
|
|
883
|
+
}),
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
/** Archive shaped like the dashboard's assembly: package-dir contents at the
|
|
887
|
+
* archive root, committed references under references/. */
|
|
888
|
+
function packagedProject() {
|
|
889
|
+
return makeTarGz([
|
|
890
|
+
...gradleProjectEntries(''),
|
|
891
|
+
{ name: 'references/x.png', content: Buffer.concat([PNG_MAGIC, Buffer.from('ref:url-x')]) },
|
|
892
|
+
]);
|
|
893
|
+
}
|
|
894
|
+
test('package_source url requires package_url and package_path', async () => {
|
|
895
|
+
const executor = new UiFidelityRenderAndroidStepExecutor();
|
|
896
|
+
await expect(executor.execute({ package_source: 'url', package_path: 'android-app' }, {}, testConfig)).rejects.toThrow(/package_url/);
|
|
897
|
+
await expect(executor.execute({ package_source: 'url', package_url: 'https://x.test/p' }, {}, testConfig)).rejects.toThrow(/package_path/);
|
|
898
|
+
});
|
|
899
|
+
test('rejects reference_source alongside package_source url (URL mode owns references)', async () => {
|
|
900
|
+
const executor = new UiFidelityRenderAndroidStepExecutor();
|
|
901
|
+
await expect(executor.execute({
|
|
902
|
+
package_source: 'url',
|
|
903
|
+
package_url: 'https://x.test/p',
|
|
904
|
+
package_path: 'android-app',
|
|
905
|
+
reference_source: 'repo',
|
|
906
|
+
}, {}, testConfig)).rejects.toThrow(/reference_source/);
|
|
907
|
+
});
|
|
908
|
+
test('url mode requires curl on the runner (validation requirement)', () => {
|
|
909
|
+
const executor = new UiFidelityRenderAndroidStepExecutor();
|
|
910
|
+
const urlReqs = executor.getValidationRequirements({ package_source: 'url' }, {}, testConfig);
|
|
911
|
+
expect(urlReqs.some((r) => r.name === 'curl')).toBe(true);
|
|
912
|
+
// Default inputs mode keeps tar only — no new requirement for existing runs.
|
|
913
|
+
const defaultReqs = executor.getValidationRequirements({}, {}, testConfig);
|
|
914
|
+
expect(defaultReqs.some((r) => r.name === 'curl')).toBe(false);
|
|
915
|
+
});
|
|
916
|
+
test('downloads the package, renders, and resolves references inside it', async () => {
|
|
917
|
+
const project = makeProject({ screens: { XProof: 'android-app/references/x.png' } });
|
|
918
|
+
const served = await servePackage(packagedProject());
|
|
919
|
+
try {
|
|
920
|
+
const run = runScript(project, await buildScript({
|
|
921
|
+
package_source: 'url',
|
|
922
|
+
package_url: served.url,
|
|
923
|
+
package_path: 'android-app',
|
|
924
|
+
}), { FAKE_ROBORAZZI_SCREENS: 'XProof' });
|
|
925
|
+
expect(run.status).toBe(0);
|
|
926
|
+
const doc = readResult(project);
|
|
927
|
+
expect(doc.package_source).toBe('url');
|
|
928
|
+
const screen = doc.screens.find((s) => s.screen === 'XProof');
|
|
929
|
+
expect(screen?.status).toBe('rendered');
|
|
930
|
+
expect(screen?.reference_image_path).toBe('ui-fidelity/references/XProof.png');
|
|
931
|
+
expect(isPng(artifact(project, 'ui-fidelity/references/XProof.png'))).toBe(true);
|
|
932
|
+
}
|
|
933
|
+
finally {
|
|
934
|
+
await served.close();
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
test('a failed download is one structured build error, not per-screen noise', async () => {
|
|
938
|
+
const project = makeProject({ screens: { XProof: 'android-app/references/x.png' } });
|
|
939
|
+
const served = await servePackage(null); // 404
|
|
940
|
+
try {
|
|
941
|
+
const run = runScript(project, await buildScript({
|
|
942
|
+
package_source: 'url',
|
|
943
|
+
package_url: served.url,
|
|
944
|
+
package_path: 'android-app',
|
|
945
|
+
}));
|
|
946
|
+
expect(run.status).not.toBe(0);
|
|
947
|
+
const doc = readResult(project);
|
|
948
|
+
expect(doc.error?.code).toBe('ANDROID_PACKAGE_DOWNLOAD_FAILED');
|
|
949
|
+
const screen = doc.screens.find((s) => s.screen === 'XProof');
|
|
950
|
+
// The failure is the build's, not the screen's — no bogus
|
|
951
|
+
// REFERENCE_MISSING masking the real cause.
|
|
952
|
+
expect(screen?.error).toBeNull();
|
|
953
|
+
}
|
|
954
|
+
finally {
|
|
955
|
+
await served.close();
|
|
956
|
+
}
|
|
957
|
+
});
|
|
958
|
+
test('a reference outside the package directory fails that screen only', async () => {
|
|
959
|
+
const project = makeProject({
|
|
960
|
+
screens: {
|
|
961
|
+
XProof: 'android-app/references/x.png',
|
|
962
|
+
Stray: 'design/elsewhere/stray.png',
|
|
963
|
+
},
|
|
964
|
+
});
|
|
965
|
+
const served = await servePackage(packagedProject());
|
|
966
|
+
try {
|
|
967
|
+
const run = runScript(project, await buildScript({
|
|
968
|
+
package_source: 'url',
|
|
969
|
+
package_url: served.url,
|
|
970
|
+
package_path: 'android-app',
|
|
971
|
+
}), { FAKE_ROBORAZZI_SCREENS: 'XProof' });
|
|
972
|
+
expect(run.status).not.toBe(0);
|
|
973
|
+
const doc = readResult(project);
|
|
974
|
+
expect(doc.screens.find((s) => s.screen === 'XProof')?.status).toBe('rendered');
|
|
975
|
+
const stray = doc.screens.find((s) => s.screen === 'Stray');
|
|
976
|
+
expect(stray?.status).toBe('render_failed');
|
|
977
|
+
expect(stray?.error?.code).toBe('REFERENCE_MISSING');
|
|
978
|
+
}
|
|
979
|
+
finally {
|
|
980
|
+
await served.close();
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
});
|
|
829
984
|
describe('reference_source: repo', () => {
|
|
830
985
|
test('rejects an unknown reference_source value', async () => {
|
|
831
986
|
const executor = new UiFidelityRenderAndroidStepExecutor();
|