@invarn/cibuild 2.2.7 → 2.2.9
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 +237 -14
- package/dist/src/yaml/fidelity-input-digest.d.ts +37 -0
- package/dist/src/yaml/fidelity-input-digest.d.ts.map +1 -0
- package/dist/src/yaml/fidelity-input-digest.js +208 -0
- package/dist/src/yaml/fidelity-input-digest.test.d.ts +2 -0
- package/dist/src/yaml/fidelity-input-digest.test.d.ts.map +1 -0
- package/dist/src/yaml/fidelity-input-digest.test.js +167 -0
- package/dist/src/yaml/steps/structural-facts.d.ts +68 -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 +133 -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 +42 -1
- package/dist/src/yaml/steps/ui-fidelity-render-android.test.js +28 -0
- package/dist/src/yaml/steps/ui-fidelity-render.d.ts.map +1 -1
- package/dist/src/yaml/steps/ui-fidelity-render.js +23 -0
- package/dist/src/yaml/steps/ui-fidelity-render.test.js +16 -0
- package/package.json +1 -1
|
@@ -1156,6 +1156,139 @@ describe('buildAppearanceReferenceFromParams — union of geometry/text/color id
|
|
|
1156
1156
|
]);
|
|
1157
1157
|
});
|
|
1158
1158
|
});
|
|
1159
|
+
describe('buildStructuralBlock — design-frame anchor (composition runs)', () => {
|
|
1160
|
+
// 686x384px at density 2 → a 343×192dp captured root.
|
|
1161
|
+
const FRAME_DUMP = {
|
|
1162
|
+
canvas: { left: 0, top: 0, right: 686, bottom: 384 },
|
|
1163
|
+
nodes: [
|
|
1164
|
+
{ id: 'voucher', bounds: { left: 32, top: 48, right: 194, bottom: 210 }, parentId: null, clipsChildren: false },
|
|
1165
|
+
],
|
|
1166
|
+
};
|
|
1167
|
+
const GEOM = { voucher: { w: 81, h: 81, x: 16, y: 24 } };
|
|
1168
|
+
test('root ≈ declared frame → no mismatch fact, placement is not withheld', () => {
|
|
1169
|
+
const block = buildStructuralBlock(FRAME_DUMP, GEOM, 2, undefined, undefined, { w: 343, h: 192 });
|
|
1170
|
+
expect(block.facts.filter((f) => f.kind === 'frame_mismatch')).toEqual([]);
|
|
1171
|
+
expect(block.placement).toEqual({ withheld: false, facts: [] });
|
|
1172
|
+
});
|
|
1173
|
+
test('sub-tolerance drift (≤1dp) does not read as a mismatch', () => {
|
|
1174
|
+
const block = buildStructuralBlock(FRAME_DUMP, GEOM, 2, undefined, undefined, { w: 342.5, h: 192.5 });
|
|
1175
|
+
expect(block.facts.filter((f) => f.kind === 'frame_mismatch')).toEqual([]);
|
|
1176
|
+
expect(block.placement).toEqual({ withheld: false, facts: [] });
|
|
1177
|
+
});
|
|
1178
|
+
test('a mis-sized root → frame_mismatch fact with declared/captured dims, placement withheld', () => {
|
|
1179
|
+
const block = buildStructuralBlock(FRAME_DUMP, GEOM, 2, undefined, undefined, { w: 343, h: 100 });
|
|
1180
|
+
const mismatches = block.facts.filter((f) => f.kind === 'frame_mismatch');
|
|
1181
|
+
expect(mismatches).toEqual([
|
|
1182
|
+
{
|
|
1183
|
+
kind: 'frame_mismatch',
|
|
1184
|
+
declared: { w: 343, h: 100 },
|
|
1185
|
+
captured: { w: 343, h: 192 },
|
|
1186
|
+
deltas: [{ dimension: 'h', delta: 92 }],
|
|
1187
|
+
},
|
|
1188
|
+
]);
|
|
1189
|
+
expect(block.placement).toEqual({ withheld: true, reason: 'frame_mismatch' });
|
|
1190
|
+
// A mis-sized wrapper root is not a clean render.
|
|
1191
|
+
expect(block.summary.clean).toBe(false);
|
|
1192
|
+
});
|
|
1193
|
+
test('the mismatch review row reads as a mis-sized wrapper root, never a screen defect', () => {
|
|
1194
|
+
const block = buildStructuralBlock(FRAME_DUMP, GEOM, 2, undefined, undefined, { w: 343, h: 100 });
|
|
1195
|
+
const scaffold = buildReviewScaffold(block);
|
|
1196
|
+
const row = scaffold.rows.find((r) => r.fact.includes('wrapper root'));
|
|
1197
|
+
expect(row).toBeDefined();
|
|
1198
|
+
expect(row.fact).toContain('343×192');
|
|
1199
|
+
expect(row.fact).toContain('343×100');
|
|
1200
|
+
expect(row.fact.toLowerCase()).toContain('size the wrapper root');
|
|
1201
|
+
});
|
|
1202
|
+
test('no declared frame → no placement container, block unchanged from today', () => {
|
|
1203
|
+
const withoutFrame = buildStructuralBlock(FRAME_DUMP, GEOM, 2);
|
|
1204
|
+
expect(withoutFrame.placement).toBeUndefined();
|
|
1205
|
+
expect(buildStructuralBlock(FRAME_DUMP, GEOM, 2, undefined, undefined, null)).toEqual(withoutFrame);
|
|
1206
|
+
});
|
|
1207
|
+
test('embedded stage matches the module for match and mismatch alike (parity)', () => {
|
|
1208
|
+
const embedded = getGeometryStageInternals();
|
|
1209
|
+
for (const frame of [{ w: 343, h: 192 }, { w: 343, h: 100 }, null]) {
|
|
1210
|
+
expect(embedded.buildStructuralBlock(FRAME_DUMP, GEOM, 2, undefined, undefined, frame)).toEqual(buildStructuralBlock(FRAME_DUMP, GEOM, 2, undefined, undefined, frame));
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
});
|
|
1214
|
+
describe('buildStructuralBlock — placement facts (composition runs)', () => {
|
|
1215
|
+
// 686x384px at density 2 → a 343×192dp captured root; frame matches.
|
|
1216
|
+
const FRAME = { w: 343, h: 192 };
|
|
1217
|
+
// voucher: design places it at (16, 24); rendered root-space (unclipped)
|
|
1218
|
+
// bounds put it at (16, 24) → in place, or shifted for the drift cases.
|
|
1219
|
+
function dumpWithVoucherAt(leftPx, topPx) {
|
|
1220
|
+
return {
|
|
1221
|
+
canvas: { left: 0, top: 0, right: 686, bottom: 384 },
|
|
1222
|
+
nodes: [
|
|
1223
|
+
{
|
|
1224
|
+
id: 'voucher',
|
|
1225
|
+
bounds: { left: leftPx, top: topPx, right: leftPx + 162, bottom: topPx + 162 },
|
|
1226
|
+
unclippedBounds: { left: leftPx, top: topPx, right: leftPx + 162, bottom: topPx + 162 },
|
|
1227
|
+
parentId: null,
|
|
1228
|
+
clipsChildren: false,
|
|
1229
|
+
},
|
|
1230
|
+
],
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
const GEOM = { voucher: { w: 81, h: 81, x: 16, y: 24 } };
|
|
1234
|
+
test('a member root shifted beyond tolerance → a placement fact with expected/actual/delta', () => {
|
|
1235
|
+
// Rendered at (20, 100)dp = (40, 200)px — design says (16, 24)dp.
|
|
1236
|
+
const block = buildStructuralBlock(dumpWithVoucherAt(40, 200), GEOM, 2, undefined, undefined, FRAME);
|
|
1237
|
+
expect(block.placement).toEqual({
|
|
1238
|
+
withheld: false,
|
|
1239
|
+
facts: [
|
|
1240
|
+
{
|
|
1241
|
+
elementId: 'voucher',
|
|
1242
|
+
expected: { x: 16, y: 24 },
|
|
1243
|
+
actual: { x: 20, y: 100 },
|
|
1244
|
+
deltas: [
|
|
1245
|
+
{ dimension: 'x', delta: 4 },
|
|
1246
|
+
{ dimension: 'y', delta: 76 },
|
|
1247
|
+
],
|
|
1248
|
+
},
|
|
1249
|
+
],
|
|
1250
|
+
});
|
|
1251
|
+
// Placement drift is load-bearing on a composition State — not clean.
|
|
1252
|
+
expect(block.summary.clean).toBe(false);
|
|
1253
|
+
});
|
|
1254
|
+
test('within tolerance (≤2dp) → measured but no fact, still clean', () => {
|
|
1255
|
+
// Rendered at (17, 25)dp = (34, 50)px — 1dp off design (16, 24).
|
|
1256
|
+
const block = buildStructuralBlock(dumpWithVoucherAt(34, 50), GEOM, 2, undefined, undefined, FRAME);
|
|
1257
|
+
expect(block.placement).toEqual({ withheld: false, facts: [] });
|
|
1258
|
+
expect(block.summary.clean).toBe(true);
|
|
1259
|
+
});
|
|
1260
|
+
test('an element without design x/y contributes no placement fact', () => {
|
|
1261
|
+
const geomNoXY = { voucher: { w: 81, h: 81 } };
|
|
1262
|
+
const block = buildStructuralBlock(dumpWithVoucherAt(40, 200), geomNoXY, 2, undefined, undefined, FRAME);
|
|
1263
|
+
expect(block.placement).toEqual({ withheld: false, facts: [] });
|
|
1264
|
+
});
|
|
1265
|
+
test('frame mismatch → placement facts withheld with the reason, none computed', () => {
|
|
1266
|
+
const block = buildStructuralBlock(dumpWithVoucherAt(40, 200), GEOM, 2, undefined, undefined, { w: 343, h: 100 });
|
|
1267
|
+
expect(block.placement).toEqual({ withheld: true, reason: 'frame_mismatch' });
|
|
1268
|
+
expect(block.placement && 'facts' in block.placement).toBe(false);
|
|
1269
|
+
});
|
|
1270
|
+
test('no declared frame (component runs) → no placement facts, identical deltas stay advisory', () => {
|
|
1271
|
+
const block = buildStructuralBlock(dumpWithVoucherAt(40, 200), GEOM, 2);
|
|
1272
|
+
expect(block.placement).toBeUndefined();
|
|
1273
|
+
// x/y deltas still reported (advisory) on the geometry element, unflagged.
|
|
1274
|
+
const el = block.geometry.elements.find((e) => e.id === 'voucher');
|
|
1275
|
+
expect(el.deltas.some((d) => d.dimension === 'x' || d.dimension === 'y')).toBe(true);
|
|
1276
|
+
expect(el.sizeFlag).toBe(false);
|
|
1277
|
+
expect(block.summary.clean).toBe(true);
|
|
1278
|
+
});
|
|
1279
|
+
test('embedded stage matches the module for drift, clean, and withheld (parity)', () => {
|
|
1280
|
+
const embedded = getGeometryStageInternals();
|
|
1281
|
+
const cases = [
|
|
1282
|
+
[dumpWithVoucherAt(40, 200), FRAME],
|
|
1283
|
+
[dumpWithVoucherAt(34, 50), FRAME],
|
|
1284
|
+
[dumpWithVoucherAt(40, 200), { w: 343, h: 100 }],
|
|
1285
|
+
[dumpWithVoucherAt(40, 200), null],
|
|
1286
|
+
];
|
|
1287
|
+
for (const [dump, frame] of cases) {
|
|
1288
|
+
expect(embedded.buildStructuralBlock(dump, GEOM, 2, undefined, undefined, frame)).toEqual(buildStructuralBlock(dump, GEOM, 2, undefined, undefined, frame));
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
});
|
|
1159
1292
|
describe('embedded geometry stage parity', () => {
|
|
1160
1293
|
// The render ships a JS-source port of the differ + assembler. It must produce
|
|
1161
1294
|
// byte-identical blocks to the canonical typed module, or run-side structural
|
|
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;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-android.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ui-fidelity-render-android.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;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,6BAA8B,CAAC;AAC3D,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7D;;;;GAIG;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;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC;AA47BD;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,yBAAyB,GAAG,MAAM,CAqBrF;AAED;;;GAGG;AACH,qBAAa,mCAAoC,SAAQ,gBAAgB;IACvE,yBAAyB,CACvB,OAAO,EAAE,6BAA6B,EACtC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,qBAAqB,EAAE;IAepB,OAAO,CACX,MAAM,EAAE,6BAA6B,EACrC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,OAAO,CAAC;CA0EpB"}
|
|
@@ -51,6 +51,7 @@ import { BaseStepExecutor } from './base.js';
|
|
|
51
51
|
import { parseScale, DEFAULT_SCALE } from './ui-fidelity-render.js';
|
|
52
52
|
import { RENDER_SCRIPT_TRIM_STAGE } from './render-post-processor.js';
|
|
53
53
|
import { RENDER_SCRIPT_FACTS_STAGE, RENDER_SCRIPT_GEOMETRY_STAGE } from './structural-facts.js';
|
|
54
|
+
import { RENDER_SCRIPT_SOURCE_DIGEST_STAGE } from '../fidelity-input-digest.js';
|
|
54
55
|
/**
|
|
55
56
|
* Default Roborazzi record task. Run from the project root so Gradle resolves
|
|
56
57
|
* it in whichever subproject declares it.
|
|
@@ -122,6 +123,14 @@ var referenceColor = null;
|
|
|
122
123
|
// Marks whether the appearance reference is the design (divergences) or the
|
|
123
124
|
// previous catalog render (regressions/drift, the no-Figma path).
|
|
124
125
|
var referenceBaseline = null;
|
|
126
|
+
// params.reference_frame (composition runs: the design frame's { w, h } per
|
|
127
|
+
// screen, design px treated 1:1 with dp), set in main() and read by the
|
|
128
|
+
// structural assembler, which asserts the captured root against it. Null when
|
|
129
|
+
// none declared → no frame assertion, no placement gate.
|
|
130
|
+
var referenceFrame = null;
|
|
131
|
+
// params.proof_kind ('component' default | 'composition'), set in main(). The
|
|
132
|
+
// frame assertion and the placement gate only apply to composition runs.
|
|
133
|
+
var proofKind = 'component';
|
|
125
134
|
|
|
126
135
|
function log(message) {
|
|
127
136
|
console.log('[ui-fidelity-render-android] ' + message);
|
|
@@ -547,6 +556,14 @@ function findRoborazziDirs(root) {
|
|
|
547
556
|
// screen — the analog of the iOS probe gate). A screen whose PNG is absent
|
|
548
557
|
// after a successful build is RENDER_FAILED on its own.
|
|
549
558
|
function renderScreens(renderable, projectRoot) {
|
|
559
|
+
// Snapshot the package's input files BEFORE the build runs, so the digest is
|
|
560
|
+
// over the pristine committed source and never perturbed by build outputs.
|
|
561
|
+
var packageFiles = __fidCollectPackageFiles(projectRoot);
|
|
562
|
+
// A package is safely cacheable only when it is hermetic — no Gradle script
|
|
563
|
+
// reaches outside its own root. Then the package IS its whole dependency
|
|
564
|
+
// closure, so the content key cannot miss an external change. A non-hermetic
|
|
565
|
+
// package emits no digest, so a later dispatch never skips it.
|
|
566
|
+
var packageHermetic = __fidIsHermetic(packageFiles);
|
|
550
567
|
var gradle = runGradle(projectRoot, CONFIG.gradleTask);
|
|
551
568
|
if (gradle.status !== 0) {
|
|
552
569
|
var detail = outputTail(gradle);
|
|
@@ -606,6 +623,20 @@ function renderScreens(renderable, projectRoot) {
|
|
|
606
623
|
entry.rendered_image_path = relative;
|
|
607
624
|
entry.status = 'rendered';
|
|
608
625
|
entry.error = null;
|
|
626
|
+
// Content key over the package source + this screen's reference: lets a
|
|
627
|
+
// consumer skip a re-render whose inputs are unchanged. Emitted only for a
|
|
628
|
+
// hermetic package; otherwise omitted so no stale verdict can be replayed.
|
|
629
|
+
if (packageHermetic) {
|
|
630
|
+
var referenceBytes = null;
|
|
631
|
+
try {
|
|
632
|
+
if (entry.reference_image_path) {
|
|
633
|
+
referenceBytes = fs.readFileSync(path.resolve(artifactsDir, entry.reference_image_path));
|
|
634
|
+
}
|
|
635
|
+
} catch (digestReferenceError) {
|
|
636
|
+
referenceBytes = null;
|
|
637
|
+
}
|
|
638
|
+
entry.source_digest = __fidSourceDigest(packageFiles, referenceBytes);
|
|
639
|
+
}
|
|
609
640
|
log('rendered ' + entry.screen + ' -> ' + relative);
|
|
610
641
|
trimRenderedImage(entry, outputPath, workDir);
|
|
611
642
|
// Signal A: the render emits a per-node layout dump into a roborazzi
|
|
@@ -634,7 +665,7 @@ function renderScreens(renderable, projectRoot) {
|
|
|
634
665
|
// geometry deltas vs reference_geometry) onto the screen entry, so the
|
|
635
666
|
// judge reads the signals from protocol-result.json without fetching
|
|
636
667
|
// artifacts. CONFIG.scale is the render density (dp = px / scale).
|
|
637
|
-
assembleStructural(entry, layoutSource, referenceGeometry, CONFIG.scale, referenceText, referenceColor, referenceBaseline);
|
|
668
|
+
assembleStructural(entry, layoutSource, referenceGeometry, CONFIG.scale, referenceText, referenceColor, referenceBaseline, referenceFrame, proofKind);
|
|
638
669
|
}
|
|
639
670
|
});
|
|
640
671
|
} finally {
|
|
@@ -720,6 +751,15 @@ function main() {
|
|
|
720
751
|
// sourced from the previous catalog render (no-Figma drift path), else 'figma'.
|
|
721
752
|
referenceBaseline =
|
|
722
753
|
params.reference_baseline === 'previous_render' ? 'previous_render' : 'figma';
|
|
754
|
+
// Composition-run inputs: the declared kind gates the frame assertion; the
|
|
755
|
+
// per-screen design frame is what the captured root is asserted against.
|
|
756
|
+
proofKind = params.proof_kind === 'composition' ? 'composition' : 'component';
|
|
757
|
+
referenceFrame =
|
|
758
|
+
params.reference_frame &&
|
|
759
|
+
typeof params.reference_frame === 'object' &&
|
|
760
|
+
!Array.isArray(params.reference_frame)
|
|
761
|
+
? params.reference_frame
|
|
762
|
+
: null;
|
|
723
763
|
|
|
724
764
|
var screens = Object.keys(params.screens);
|
|
725
765
|
currentEntries = screens.map(function (screen) {
|
|
@@ -1003,6 +1043,7 @@ export function generateAndroidRenderScript(config) {
|
|
|
1003
1043
|
RENDER_SCRIPT_TRIM_STAGE,
|
|
1004
1044
|
RENDER_SCRIPT_FACTS_STAGE,
|
|
1005
1045
|
RENDER_SCRIPT_GEOMETRY_STAGE,
|
|
1046
|
+
RENDER_SCRIPT_SOURCE_DIGEST_STAGE,
|
|
1006
1047
|
main,
|
|
1007
1048
|
].join('\n');
|
|
1008
1049
|
}
|
|
@@ -360,6 +360,34 @@ describe('android render runtime — render + trim (happy path)', () => {
|
|
|
360
360
|
],
|
|
361
361
|
});
|
|
362
362
|
});
|
|
363
|
+
test('emits a source_digest content key on a rendered screen entry', async () => {
|
|
364
|
+
const project = makeProject({ screens: { XProof: 'x.png' } });
|
|
365
|
+
writeReference(project, 'x.png');
|
|
366
|
+
stageArchive(project, gradleProjectEntries());
|
|
367
|
+
const run = runScript(project, await buildScript(), {
|
|
368
|
+
FAKE_ROBORAZZI_SCREENS: 'XProof',
|
|
369
|
+
});
|
|
370
|
+
expect(run.status).toBe(0);
|
|
371
|
+
const screen = readResult(project).screens.find((s) => s.screen === 'XProof');
|
|
372
|
+
expect(screen?.status).toBe('rendered');
|
|
373
|
+
expect(screen?.source_digest).toMatch(/^[0-9a-f]{64}$/);
|
|
374
|
+
});
|
|
375
|
+
test('omits source_digest for a non-hermetic package (Gradle script escapes the root)', async () => {
|
|
376
|
+
const project = makeProject({ screens: { XProof: 'x.png' } });
|
|
377
|
+
writeReference(project, 'x.png');
|
|
378
|
+
// A settings script that pulls in a sibling build via a parent path — the
|
|
379
|
+
// package is no longer its own dependency closure, so it must not be
|
|
380
|
+
// cacheable: it still renders, but emits no content key.
|
|
381
|
+
const entries = gradleProjectEntries().map((e) => e.name.endsWith('settings.gradle.kts')
|
|
382
|
+
? { ...e, content: 'rootProject.name = "p"\ninclude(":proof")\nincludeBuild("../design-system")\n' }
|
|
383
|
+
: e);
|
|
384
|
+
stageArchive(project, entries);
|
|
385
|
+
const run = runScript(project, await buildScript(), { FAKE_ROBORAZZI_SCREENS: 'XProof' });
|
|
386
|
+
expect(run.status).toBe(0);
|
|
387
|
+
const screen = readResult(project).screens.find((s) => s.screen === 'XProof');
|
|
388
|
+
expect(screen?.status).toBe('rendered');
|
|
389
|
+
expect(screen?.source_digest == null).toBe(true);
|
|
390
|
+
});
|
|
363
391
|
test('assembles the inline structural block (facts) onto the screen entry', async () => {
|
|
364
392
|
const project = makeProject({ screens: { XProof: 'x.png' } });
|
|
365
393
|
writeReference(project, 'x.png');
|
|
@@ -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":"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"}
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
*/
|
|
51
51
|
import { BaseStepExecutor } from './base.js';
|
|
52
52
|
import { RENDER_SCRIPT_TRIM_STAGE } from './render-post-processor.js';
|
|
53
|
+
import { RENDER_SCRIPT_SOURCE_DIGEST_STAGE } from '../fidelity-input-digest.js';
|
|
53
54
|
/**
|
|
54
55
|
* Default render size in device points. 393x852 is the iPhone-class portrait
|
|
55
56
|
* canvas shared by the iPhone 14 Pro / 15 / 16 — the most common modern
|
|
@@ -339,6 +340,13 @@ function runSwift(args) {
|
|
|
339
340
|
}
|
|
340
341
|
|
|
341
342
|
function renderWithHarness(renderable, packagePath) {
|
|
343
|
+
// Snapshot the user package's input files BEFORE any Swift build, so build
|
|
344
|
+
// products (.build, DerivedData) never perturb the content key. A package is
|
|
345
|
+
// safely cacheable only when it is hermetic — its own files are its whole
|
|
346
|
+
// dependency closure; a non-hermetic one emits no digest and so is never
|
|
347
|
+
// skipped by a consumer.
|
|
348
|
+
var packageFiles = __fidCollectPackageFiles(packagePath);
|
|
349
|
+
var packageHermetic = __fidIsHermetic(packageFiles);
|
|
342
350
|
var harnessDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ui-fidelity-harness-'));
|
|
343
351
|
registerPathReplacement(harnessDir, '<harness>');
|
|
344
352
|
log('harness package: ' + harnessDir);
|
|
@@ -420,6 +428,20 @@ function renderWithHarness(renderable, packagePath) {
|
|
|
420
428
|
entry.rendered_image_path = relative;
|
|
421
429
|
entry.status = 'rendered';
|
|
422
430
|
entry.error = null;
|
|
431
|
+
// Content key over the package source + this screen's reference: lets a
|
|
432
|
+
// consumer skip a re-render whose inputs are unchanged. Emitted only for a
|
|
433
|
+
// hermetic package; otherwise omitted so no stale verdict can be replayed.
|
|
434
|
+
if (packageHermetic) {
|
|
435
|
+
var referenceBytes = null;
|
|
436
|
+
try {
|
|
437
|
+
if (entry.reference_image_path) {
|
|
438
|
+
referenceBytes = fs.readFileSync(path.resolve(artifactsDir, entry.reference_image_path));
|
|
439
|
+
}
|
|
440
|
+
} catch (digestReferenceError) {
|
|
441
|
+
referenceBytes = null;
|
|
442
|
+
}
|
|
443
|
+
entry.source_digest = __fidSourceDigest(packageFiles, referenceBytes);
|
|
444
|
+
}
|
|
423
445
|
log('rendered ' + entry.screen + ' -> ' + relative);
|
|
424
446
|
});
|
|
425
447
|
} finally {
|
|
@@ -1185,6 +1207,7 @@ export function generateRenderScript(config) {
|
|
|
1185
1207
|
'// ui-fidelity-render runtime script (generated by cibuild at YAML conversion time)',
|
|
1186
1208
|
'var CONFIG = ' + JSON.stringify(config) + ';',
|
|
1187
1209
|
RENDER_SCRIPT_GENERATORS,
|
|
1210
|
+
RENDER_SCRIPT_SOURCE_DIGEST_STAGE,
|
|
1188
1211
|
main,
|
|
1189
1212
|
].join('\n');
|
|
1190
1213
|
}
|
|
@@ -531,6 +531,7 @@ describe('render script runtime', () => {
|
|
|
531
531
|
error: null,
|
|
532
532
|
reference_image_path: 'ui-fidelity/references/HomeView.png',
|
|
533
533
|
rendered_image_path: 'ui-fidelity/rendered/HomeView.png',
|
|
534
|
+
source_digest: expect.stringMatching(/^[0-9a-f]{64}$/),
|
|
534
535
|
},
|
|
535
536
|
{
|
|
536
537
|
screen: 'SettingsView',
|
|
@@ -538,6 +539,7 @@ describe('render script runtime', () => {
|
|
|
538
539
|
error: null,
|
|
539
540
|
reference_image_path: 'ui-fidelity/references/SettingsView.png',
|
|
540
541
|
rendered_image_path: 'ui-fidelity/rendered/SettingsView.png',
|
|
542
|
+
source_digest: expect.stringMatching(/^[0-9a-f]{64}$/),
|
|
541
543
|
},
|
|
542
544
|
],
|
|
543
545
|
});
|
|
@@ -647,6 +649,17 @@ describe('render script runtime', () => {
|
|
|
647
649
|
expect(settings.error).toBeNull();
|
|
648
650
|
expect(isPng(artifact(project, 'ui-fidelity/rendered/SettingsView.png'))).toBe(true);
|
|
649
651
|
});
|
|
652
|
+
test('emits a source_digest content key on a rendered screen entry', async () => {
|
|
653
|
+
const project = makeProject({ screens: { HomeView: 'home.png' } });
|
|
654
|
+
writeReference(project, 'home.png');
|
|
655
|
+
// A committed package file so the digest is over real package source.
|
|
656
|
+
writeFileSync(join(project.packageDir, 'Package.swift'), '// swift-tools-version:5.9\nimport PackageDescription\n');
|
|
657
|
+
const run = runScript(project, await buildScript(project));
|
|
658
|
+
expect(run.status).toBe(0);
|
|
659
|
+
const home = readResult(project).screens[0];
|
|
660
|
+
expect(home.status).toBe('rendered');
|
|
661
|
+
expect(home.source_digest).toMatch(/^[0-9a-f]{64}$/);
|
|
662
|
+
});
|
|
650
663
|
test('a reference that is a directory fails only that screen', async () => {
|
|
651
664
|
const project = makeProject({
|
|
652
665
|
screens: { HomeView: 'home-dir.png', SettingsView: 'settings.png' },
|
|
@@ -916,6 +929,7 @@ describe('render script runtime (package_source: repo)', () => {
|
|
|
916
929
|
error: null,
|
|
917
930
|
reference_image_path: 'ui-fidelity/references/HomeView.png',
|
|
918
931
|
rendered_image_path: 'ui-fidelity/rendered/HomeView.png',
|
|
932
|
+
source_digest: expect.stringMatching(/^[0-9a-f]{64}$/),
|
|
919
933
|
aligned_image_path: 'ui-fidelity/aligned/HomeView.png',
|
|
920
934
|
dimensions: {
|
|
921
935
|
rendered_px: [320, 640],
|
|
@@ -1095,6 +1109,7 @@ describe('render script runtime (package_source: inputs)', () => {
|
|
|
1095
1109
|
error: null,
|
|
1096
1110
|
reference_image_path: 'ui-fidelity/references/HomeView.png',
|
|
1097
1111
|
rendered_image_path: 'ui-fidelity/rendered/HomeView.png',
|
|
1112
|
+
source_digest: expect.stringMatching(/^[0-9a-f]{64}$/),
|
|
1098
1113
|
aligned_image_path: 'ui-fidelity/aligned/HomeView.png',
|
|
1099
1114
|
dimensions: {
|
|
1100
1115
|
rendered_px: [320, 640],
|
|
@@ -1108,6 +1123,7 @@ describe('render script runtime (package_source: inputs)', () => {
|
|
|
1108
1123
|
error: null,
|
|
1109
1124
|
reference_image_path: 'ui-fidelity/references/SettingsView.png',
|
|
1110
1125
|
rendered_image_path: 'ui-fidelity/rendered/SettingsView.png',
|
|
1126
|
+
source_digest: expect.stringMatching(/^[0-9a-f]{64}$/),
|
|
1111
1127
|
aligned_image_path: 'ui-fidelity/aligned/SettingsView.png',
|
|
1112
1128
|
dimensions: {
|
|
1113
1129
|
rendered_px: [320, 640],
|