@invarn/cibuild 2.1.0 → 2.1.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 +246 -8
- package/dist/src/lib.d.ts +2 -0
- package/dist/src/lib.d.ts.map +1 -1
- package/dist/src/lib.js +2 -0
- package/dist/src/yaml/fidelity-manifest.d.ts +126 -0
- package/dist/src/yaml/fidelity-manifest.d.ts.map +1 -0
- package/dist/src/yaml/fidelity-manifest.js +134 -0
- package/dist/src/yaml/fidelity-manifest.test.d.ts +12 -0
- package/dist/src/yaml/fidelity-manifest.test.d.ts.map +1 -0
- package/dist/src/yaml/fidelity-manifest.test.js +251 -0
- package/dist/src/yaml/steps/fidelity-aspect-guard.d.ts +53 -0
- package/dist/src/yaml/steps/fidelity-aspect-guard.d.ts.map +1 -0
- package/dist/src/yaml/steps/fidelity-aspect-guard.js +72 -0
- package/dist/src/yaml/steps/fidelity-aspect-guard.test.d.ts +11 -0
- package/dist/src/yaml/steps/fidelity-aspect-guard.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/fidelity-aspect-guard.test.js +50 -0
- package/dist/src/yaml/steps/fidelity-reference-provenance.d.ts +45 -0
- package/dist/src/yaml/steps/fidelity-reference-provenance.d.ts.map +1 -0
- package/dist/src/yaml/steps/fidelity-reference-provenance.js +69 -0
- package/dist/src/yaml/steps/fidelity-reference-provenance.test.d.ts +14 -0
- package/dist/src/yaml/steps/fidelity-reference-provenance.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/fidelity-reference-provenance.test.js +54 -0
- package/dist/src/yaml/steps/structural-facts.d.ts +101 -0
- package/dist/src/yaml/steps/structural-facts.d.ts.map +1 -0
- package/dist/src/yaml/steps/structural-facts.js +0 -0
- package/dist/src/yaml/steps/structural-facts.test.d.ts +12 -0
- package/dist/src/yaml/steps/structural-facts.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/structural-facts.test.js +174 -0
- package/dist/src/yaml/steps/ui-fidelity-preview-android.d.ts.map +1 -1
- package/dist/src/yaml/steps/ui-fidelity-preview-android.js +2 -9
- package/dist/src/yaml/steps/ui-fidelity-preview.d.ts +26 -0
- package/dist/src/yaml/steps/ui-fidelity-preview.d.ts.map +1 -1
- package/dist/src/yaml/steps/ui-fidelity-preview.js +21 -9
- package/dist/src/yaml/steps/ui-fidelity-preview.test.js +31 -1
- package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts +9 -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 +131 -1
- package/dist/src/yaml/steps/ui-fidelity-render-android.test.js +170 -1
- package/package.json +1 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fidelity aspect-ratio guard.
|
|
3
|
+
*
|
|
4
|
+
* One named composable per state is the authoring norm; the failure this
|
|
5
|
+
* catches is the opposite — several states stacked into one composable, which
|
|
6
|
+
* renders far taller (or wider) than the single-state reference it is paired
|
|
7
|
+
* against. That shows up as a sharp divergence between the rendered and
|
|
8
|
+
* reference aspect ratios, well before a human reads the diff.
|
|
9
|
+
*
|
|
10
|
+
* The guard is a pure comparison over the dimensions the render already
|
|
11
|
+
* reports (`rendered_px`, `reference_px`). It emits a WARNING when the
|
|
12
|
+
* divergence exceeds a documented fold factor — it never fails the render, and
|
|
13
|
+
* it stays silent when there is no reference to compare against.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Fold factor beyond which a rendered/reference aspect-ratio divergence is
|
|
17
|
+
* worth a warning. A faithful single-state wrapper sits near 1.0; trim and
|
|
18
|
+
* reference alignment nudge it a few percent; a re-stacked 2-up wrapper roughly
|
|
19
|
+
* doubles one dimension and lands near 2.0. 1.5 catches the stack while
|
|
20
|
+
* tolerating the wobble.
|
|
21
|
+
*/
|
|
22
|
+
export const ASPECT_RATIO_WARN_FACTOR = 1.5;
|
|
23
|
+
function isPositiveSize(size) {
|
|
24
|
+
return (Array.isArray(size) &&
|
|
25
|
+
size.length === 2 &&
|
|
26
|
+
Number.isFinite(size[0]) &&
|
|
27
|
+
Number.isFinite(size[1]) &&
|
|
28
|
+
size[0] > 0 &&
|
|
29
|
+
size[1] > 0);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Compares a rendered size against its reference size. Returns a
|
|
33
|
+
* non-warning, non-comparable result when either size is missing or
|
|
34
|
+
* non-positive — the guard only speaks when it has two real frames to compare.
|
|
35
|
+
*/
|
|
36
|
+
export function evaluateAspectRatio(renderedPx, referencePx, factor = ASPECT_RATIO_WARN_FACTOR) {
|
|
37
|
+
if (!isPositiveSize(renderedPx) || !isPositiveSize(referencePx)) {
|
|
38
|
+
return {
|
|
39
|
+
comparable: false,
|
|
40
|
+
renderedAspect: null,
|
|
41
|
+
referenceAspect: null,
|
|
42
|
+
divergence: null,
|
|
43
|
+
warn: false,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const renderedAspect = renderedPx[0] / renderedPx[1];
|
|
47
|
+
const referenceAspect = referencePx[0] / referencePx[1];
|
|
48
|
+
const divergence = Math.max(renderedAspect / referenceAspect, referenceAspect / renderedAspect);
|
|
49
|
+
return {
|
|
50
|
+
comparable: true,
|
|
51
|
+
renderedAspect,
|
|
52
|
+
referenceAspect,
|
|
53
|
+
divergence,
|
|
54
|
+
warn: divergence > factor,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Human/agent-readable warning line for a divergent screen, or null when the
|
|
59
|
+
* screen does not warrant one. The caller decides where to surface it (preview
|
|
60
|
+
* result, preflight output) — it is advisory, never a failure.
|
|
61
|
+
*/
|
|
62
|
+
export function formatAspectRatioWarning(screen, check) {
|
|
63
|
+
if (!check.warn || check.divergence === null)
|
|
64
|
+
return null;
|
|
65
|
+
const r = check.renderedAspect?.toFixed(2);
|
|
66
|
+
const ref = check.referenceAspect?.toFixed(2);
|
|
67
|
+
return (`Screen "${screen}" renders at aspect ratio ${r}:1 but its reference is ` +
|
|
68
|
+
`${ref}:1 (${check.divergence.toFixed(1)}× divergence). This often means ` +
|
|
69
|
+
`multiple states were stacked into one composable — author one named ` +
|
|
70
|
+
`composable per state instead.`);
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=fidelity-aspect-guard.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the fidelity aspect-ratio guard.
|
|
3
|
+
*
|
|
4
|
+
* A wrapper that stacks several states into one composable renders far taller
|
|
5
|
+
* (or wider) than its single-state reference, so its aspect ratio diverges
|
|
6
|
+
* sharply from the reference's. The guard is the early warning for that
|
|
7
|
+
* re-stacking: it compares the rendered vs reference aspect ratios and flags a
|
|
8
|
+
* divergence beyond a documented fold factor — a WARNING, never a failure.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=fidelity-aspect-guard.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fidelity-aspect-guard.test.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/fidelity-aspect-guard.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the fidelity aspect-ratio guard.
|
|
3
|
+
*
|
|
4
|
+
* A wrapper that stacks several states into one composable renders far taller
|
|
5
|
+
* (or wider) than its single-state reference, so its aspect ratio diverges
|
|
6
|
+
* sharply from the reference's. The guard is the early warning for that
|
|
7
|
+
* re-stacking: it compares the rendered vs reference aspect ratios and flags a
|
|
8
|
+
* divergence beyond a documented fold factor — a WARNING, never a failure.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, test, expect } from '@jest/globals';
|
|
11
|
+
import { evaluateAspectRatio, ASPECT_RATIO_WARN_FACTOR, } from './fidelity-aspect-guard.js';
|
|
12
|
+
describe('evaluateAspectRatio', () => {
|
|
13
|
+
test('does not warn when rendered and reference aspect ratios match', () => {
|
|
14
|
+
// A faithful single-state banner: same proportions as its reference.
|
|
15
|
+
const check = evaluateAspectRatio([720, 1280], [360, 640]);
|
|
16
|
+
expect(check.comparable).toBe(true);
|
|
17
|
+
expect(check.warn).toBe(false);
|
|
18
|
+
expect(check.divergence).toBeCloseTo(1, 5);
|
|
19
|
+
});
|
|
20
|
+
test('warns when a stacked render is far off the reference aspect ratio', () => {
|
|
21
|
+
// The canonical miss: two banner states stacked in one Column render at
|
|
22
|
+
// ~2.2:1, judged against a single-state ~4.76:1 reference.
|
|
23
|
+
const rendered = [880, 400]; // 2.2:1
|
|
24
|
+
const reference = [952, 200]; // 4.76:1
|
|
25
|
+
const check = evaluateAspectRatio(rendered, reference);
|
|
26
|
+
expect(check.comparable).toBe(true);
|
|
27
|
+
expect(check.warn).toBe(true);
|
|
28
|
+
expect(check.divergence).toBeCloseTo(4.76 / 2.2, 1);
|
|
29
|
+
});
|
|
30
|
+
test('tolerates a small alignment/trim wobble within the fold factor', () => {
|
|
31
|
+
// Trim + reference alignment can nudge dimensions a few percent; that must
|
|
32
|
+
// not trip the warning.
|
|
33
|
+
const check = evaluateAspectRatio([726, 1280], [360, 640]);
|
|
34
|
+
expect(check.warn).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
test('is not comparable when there is no reference', () => {
|
|
37
|
+
const check = evaluateAspectRatio([720, 1280], null);
|
|
38
|
+
expect(check.comparable).toBe(false);
|
|
39
|
+
expect(check.warn).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
test('is not comparable when a dimension is zero or negative', () => {
|
|
42
|
+
expect(evaluateAspectRatio([720, 0], [360, 640]).comparable).toBe(false);
|
|
43
|
+
expect(evaluateAspectRatio([720, 1280], [0, 640]).comparable).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
test('exposes a sane default fold factor', () => {
|
|
46
|
+
expect(ASPECT_RATIO_WARN_FACTOR).toBeGreaterThan(1);
|
|
47
|
+
expect(ASPECT_RATIO_WARN_FACTOR).toBeLessThan(2);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
//# sourceMappingURL=fidelity-aspect-guard.test.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference-provenance gate.
|
|
3
|
+
*
|
|
4
|
+
* A UI-fidelity reference is a PNG the gate cannot read, so reference validity
|
|
5
|
+
* is asserted at export time as provenance metadata (`figmaNode`, `locale`,
|
|
6
|
+
* `resolved`, `exportedAt`) and the gate checks that METADATA — never the
|
|
7
|
+
* pixels, never via OCR. A reference that is absent, unprovenanced,
|
|
8
|
+
* placeholder-bearing (`resolved == false`), or in a locale other than the
|
|
9
|
+
* render's is UNVERIFIABLE: a hard, mechanical verdict that fails loud instead
|
|
10
|
+
* of producing a confident-but-false PASS, and that kills the ad-hoc-Figma-fetch
|
|
11
|
+
* root cause (an ad-hoc fetch has no provenance, so it is Unverifiable).
|
|
12
|
+
*
|
|
13
|
+
* Content differences (copy wording, geometry) are deliberately NOT evaluated
|
|
14
|
+
* here — those are soft, judged findings that legitimately drift between
|
|
15
|
+
* reference-export and gate-run.
|
|
16
|
+
*
|
|
17
|
+
* This module is the authoritative spec for the check. The Android render
|
|
18
|
+
* runtime inlines an equivalent JS implementation (it ships as a self-contained
|
|
19
|
+
* script string and cannot import this module); the render-step integration
|
|
20
|
+
* tests exercise that inlined copy end-to-end against the same fixtures, so the
|
|
21
|
+
* two cannot silently drift.
|
|
22
|
+
*/
|
|
23
|
+
/** Export-time provenance as it travels in the run-input sidecar. */
|
|
24
|
+
export interface ReferenceProvenanceMeta {
|
|
25
|
+
figmaNode?: string;
|
|
26
|
+
locale?: string;
|
|
27
|
+
resolved?: boolean;
|
|
28
|
+
exportedAt?: string;
|
|
29
|
+
}
|
|
30
|
+
/** A reason a reference cannot be honestly judged. */
|
|
31
|
+
export interface ProvenanceFault {
|
|
32
|
+
/** Stable machine code (NO_PROVENANCE / UNRESOLVED_REFERENCE / …). */
|
|
33
|
+
code: string;
|
|
34
|
+
/** Human/agent-readable reason, naming the screen. */
|
|
35
|
+
message: string;
|
|
36
|
+
}
|
|
37
|
+
/** The per-state status string written to protocol-result.json. */
|
|
38
|
+
export declare const UNVERIFIABLE_STATUS = "unverifiable";
|
|
39
|
+
/**
|
|
40
|
+
* Returns the reason a reference is UNVERIFIABLE, or null when it is verifiable.
|
|
41
|
+
* Checks metadata then existence; order is irrelevant for single-fault inputs
|
|
42
|
+
* but deterministic when several faults coincide.
|
|
43
|
+
*/
|
|
44
|
+
export declare function provenanceFault(screen: string, provenance: ReferenceProvenanceMeta | null | undefined, renderLocale: string, referenceExists: boolean): ProvenanceFault | null;
|
|
45
|
+
//# sourceMappingURL=fidelity-reference-provenance.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fidelity-reference-provenance.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/fidelity-reference-provenance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,qEAAqE;AACrE,MAAM,WAAW,uBAAuB;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,sDAAsD;AACtD,MAAM,WAAW,eAAe;IAC9B,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,mEAAmE;AACnE,eAAO,MAAM,mBAAmB,iBAAiB,CAAC;AAgBlD;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,uBAAuB,GAAG,IAAI,GAAG,SAAS,EACtD,YAAY,EAAE,MAAM,EACpB,eAAe,EAAE,OAAO,GACvB,eAAe,GAAG,IAAI,CAgCxB"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference-provenance gate.
|
|
3
|
+
*
|
|
4
|
+
* A UI-fidelity reference is a PNG the gate cannot read, so reference validity
|
|
5
|
+
* is asserted at export time as provenance metadata (`figmaNode`, `locale`,
|
|
6
|
+
* `resolved`, `exportedAt`) and the gate checks that METADATA — never the
|
|
7
|
+
* pixels, never via OCR. A reference that is absent, unprovenanced,
|
|
8
|
+
* placeholder-bearing (`resolved == false`), or in a locale other than the
|
|
9
|
+
* render's is UNVERIFIABLE: a hard, mechanical verdict that fails loud instead
|
|
10
|
+
* of producing a confident-but-false PASS, and that kills the ad-hoc-Figma-fetch
|
|
11
|
+
* root cause (an ad-hoc fetch has no provenance, so it is Unverifiable).
|
|
12
|
+
*
|
|
13
|
+
* Content differences (copy wording, geometry) are deliberately NOT evaluated
|
|
14
|
+
* here — those are soft, judged findings that legitimately drift between
|
|
15
|
+
* reference-export and gate-run.
|
|
16
|
+
*
|
|
17
|
+
* This module is the authoritative spec for the check. The Android render
|
|
18
|
+
* runtime inlines an equivalent JS implementation (it ships as a self-contained
|
|
19
|
+
* script string and cannot import this module); the render-step integration
|
|
20
|
+
* tests exercise that inlined copy end-to-end against the same fixtures, so the
|
|
21
|
+
* two cannot silently drift.
|
|
22
|
+
*/
|
|
23
|
+
/** The per-state status string written to protocol-result.json. */
|
|
24
|
+
export const UNVERIFIABLE_STATUS = 'unverifiable';
|
|
25
|
+
function hasProvenance(p) {
|
|
26
|
+
// Provenance is only "present" when the fields the gate reasons over are
|
|
27
|
+
// there. A partial object (e.g. a hand-dropped `{ figmaNode }`) is treated as
|
|
28
|
+
// no provenance — verifiable is earned only through the full export path.
|
|
29
|
+
return (p != null &&
|
|
30
|
+
typeof p === 'object' &&
|
|
31
|
+
typeof p.locale === 'string' &&
|
|
32
|
+
typeof p.resolved === 'boolean');
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Returns the reason a reference is UNVERIFIABLE, or null when it is verifiable.
|
|
36
|
+
* Checks metadata then existence; order is irrelevant for single-fault inputs
|
|
37
|
+
* but deterministic when several faults coincide.
|
|
38
|
+
*/
|
|
39
|
+
export function provenanceFault(screen, provenance, renderLocale, referenceExists) {
|
|
40
|
+
if (!hasProvenance(provenance)) {
|
|
41
|
+
return {
|
|
42
|
+
code: 'NO_PROVENANCE',
|
|
43
|
+
message: `reference for ${screen} has no export provenance; a reference is ` +
|
|
44
|
+
`verifiable only when exported through the deterministic path`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (provenance.resolved === false) {
|
|
48
|
+
return {
|
|
49
|
+
code: 'UNRESOLVED_REFERENCE',
|
|
50
|
+
message: `reference for ${screen} still has unresolved copy placeholders ` +
|
|
51
|
+
`(resolved=false) — export it with the copy filled in`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (provenance.locale !== renderLocale) {
|
|
55
|
+
return {
|
|
56
|
+
code: 'LOCALE_MISMATCH',
|
|
57
|
+
message: `reference for ${screen} is locale "${provenance.locale}" but the ` +
|
|
58
|
+
`render locale is "${renderLocale}"`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (!referenceExists) {
|
|
62
|
+
return {
|
|
63
|
+
code: 'REFERENCE_ABSENT',
|
|
64
|
+
message: `reference image for ${screen} is absent`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=fidelity-reference-provenance.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the reference-provenance gate.
|
|
3
|
+
*
|
|
4
|
+
* A UI-fidelity reference is a PNG; the gate cannot read its text. So validity
|
|
5
|
+
* is asserted at export time as provenance metadata, and the gate checks the
|
|
6
|
+
* METADATA, never the pixels (no OCR). A reference that is absent, unprovenanced,
|
|
7
|
+
* placeholder-bearing (`resolved == false`), or in the wrong locale is
|
|
8
|
+
* UNVERIFIABLE — a loud, mechanical verdict, not a confident-but-false PASS.
|
|
9
|
+
*
|
|
10
|
+
* Content differences (copy wording, geometry) are NOT checked here — those are
|
|
11
|
+
* soft, judged findings that legitimately drift between export and run.
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=fidelity-reference-provenance.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fidelity-reference-provenance.test.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/fidelity-reference-provenance.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the reference-provenance gate.
|
|
3
|
+
*
|
|
4
|
+
* A UI-fidelity reference is a PNG; the gate cannot read its text. So validity
|
|
5
|
+
* is asserted at export time as provenance metadata, and the gate checks the
|
|
6
|
+
* METADATA, never the pixels (no OCR). A reference that is absent, unprovenanced,
|
|
7
|
+
* placeholder-bearing (`resolved == false`), or in the wrong locale is
|
|
8
|
+
* UNVERIFIABLE — a loud, mechanical verdict, not a confident-but-false PASS.
|
|
9
|
+
*
|
|
10
|
+
* Content differences (copy wording, geometry) are NOT checked here — those are
|
|
11
|
+
* soft, judged findings that legitimately drift between export and run.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, test, expect } from '@jest/globals';
|
|
14
|
+
import { provenanceFault, UNVERIFIABLE_STATUS, } from './fidelity-reference-provenance.js';
|
|
15
|
+
const GOOD = {
|
|
16
|
+
figmaNode: '12:345',
|
|
17
|
+
locale: 'en',
|
|
18
|
+
resolved: true,
|
|
19
|
+
exportedAt: '2026-06-18T10:00:00Z',
|
|
20
|
+
};
|
|
21
|
+
describe('provenanceFault', () => {
|
|
22
|
+
test('a present, provenanced, resolved, locale-matching reference is verifiable', () => {
|
|
23
|
+
expect(provenanceFault('Guest', GOOD, 'en', true)).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
test('an absent reference is UNVERIFIABLE (REFERENCE_ABSENT)', () => {
|
|
26
|
+
const fault = provenanceFault('Guest', GOOD, 'en', false);
|
|
27
|
+
expect(fault?.code).toBe('REFERENCE_ABSENT');
|
|
28
|
+
});
|
|
29
|
+
test('a reference with no provenance is UNVERIFIABLE (NO_PROVENANCE)', () => {
|
|
30
|
+
expect(provenanceFault('Guest', null, 'en', true)?.code).toBe('NO_PROVENANCE');
|
|
31
|
+
expect(provenanceFault('Guest', undefined, 'en', true)?.code).toBe('NO_PROVENANCE');
|
|
32
|
+
});
|
|
33
|
+
test('incomplete provenance (missing locale/resolved) is UNVERIFIABLE (NO_PROVENANCE)', () => {
|
|
34
|
+
expect(provenanceFault('Guest', { figmaNode: 'x', exportedAt: 'y' }, 'en', true)?.code).toBe('NO_PROVENANCE');
|
|
35
|
+
});
|
|
36
|
+
test('a placeholder reference (resolved=false) is UNVERIFIABLE (UNRESOLVED_REFERENCE)', () => {
|
|
37
|
+
const fault = provenanceFault('Guest', { ...GOOD, resolved: false }, 'en', true);
|
|
38
|
+
expect(fault?.code).toBe('UNRESOLVED_REFERENCE');
|
|
39
|
+
});
|
|
40
|
+
test('a wrong-locale reference is UNVERIFIABLE (LOCALE_MISMATCH)', () => {
|
|
41
|
+
// Italian reference vs an English render.
|
|
42
|
+
const fault = provenanceFault('Guest', { ...GOOD, locale: 'it' }, 'en', true);
|
|
43
|
+
expect(fault?.code).toBe('LOCALE_MISMATCH');
|
|
44
|
+
});
|
|
45
|
+
test('every fault carries the screen name and a human reason', () => {
|
|
46
|
+
const fault = provenanceFault('LoggedIn', { ...GOOD, resolved: false }, 'en', true);
|
|
47
|
+
expect(fault?.message).toContain('LoggedIn');
|
|
48
|
+
expect(fault?.message.length).toBeGreaterThan(10);
|
|
49
|
+
});
|
|
50
|
+
test('exposes the unverifiable status string the result document uses', () => {
|
|
51
|
+
expect(UNVERIFIABLE_STATUS).toBe('unverifiable');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
//# sourceMappingURL=fidelity-reference-provenance.test.js.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural-facts deriver — Signal A of the UI-fidelity structural pass.
|
|
3
|
+
*
|
|
4
|
+
* Pure and platform-agnostic: it takes a per-node layout dump (bounds in px,
|
|
5
|
+
* parent, clip flag) emitted alongside the render and derives intent-agnostic
|
|
6
|
+
* geometric facts the fidelity judge should check instead of discovering them
|
|
7
|
+
* by eye:
|
|
8
|
+
*
|
|
9
|
+
* - `overflow` — a node extends past its parent's box (parent does
|
|
10
|
+
* NOT clip, so the excess is visibly sticking out).
|
|
11
|
+
* - `clipped_by_parent` — a node extends past a CLIPPING parent's box, so the
|
|
12
|
+
* excess is cut away (the motivating euro-coin case).
|
|
13
|
+
* - `off_screen` — a node extends past the root canvas.
|
|
14
|
+
* - `overlap` — two siblings overlap by a significant area.
|
|
15
|
+
*
|
|
16
|
+
* The deriver does NOT judge or score; it reports facts. The verdict stays the
|
|
17
|
+
* agent's. This is the brain of Signal A; the render-side emission + facts
|
|
18
|
+
* artifact wiring live in the render step (a separate slice).
|
|
19
|
+
*/
|
|
20
|
+
export interface LayoutBounds {
|
|
21
|
+
left: number;
|
|
22
|
+
top: number;
|
|
23
|
+
right: number;
|
|
24
|
+
bottom: number;
|
|
25
|
+
}
|
|
26
|
+
export interface LayoutNode {
|
|
27
|
+
/** Stable id/label for the node within one dump. */
|
|
28
|
+
id: string;
|
|
29
|
+
/** Root-space bounds in px. */
|
|
30
|
+
bounds: LayoutBounds;
|
|
31
|
+
/** Parent node id, or null for a root node. */
|
|
32
|
+
parentId: string | null;
|
|
33
|
+
/** Whether this node clips its children to its bounds. */
|
|
34
|
+
clipsChildren: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface LayoutDump {
|
|
37
|
+
/** The captured canvas / root bounds in px. */
|
|
38
|
+
canvas: LayoutBounds;
|
|
39
|
+
nodes: LayoutNode[];
|
|
40
|
+
}
|
|
41
|
+
export type Edge = 'left' | 'top' | 'right' | 'bottom';
|
|
42
|
+
export type StructuralFact = {
|
|
43
|
+
kind: 'overflow';
|
|
44
|
+
nodeId: string;
|
|
45
|
+
parentId: string;
|
|
46
|
+
edges: Edge[];
|
|
47
|
+
overflowPx: Partial<Record<Edge, number>>;
|
|
48
|
+
} | {
|
|
49
|
+
kind: 'clipped_by_parent';
|
|
50
|
+
nodeId: string;
|
|
51
|
+
clipperId: string;
|
|
52
|
+
edges: Edge[];
|
|
53
|
+
clippedPx: Partial<Record<Edge, number>>;
|
|
54
|
+
} | {
|
|
55
|
+
kind: 'off_screen';
|
|
56
|
+
nodeId: string;
|
|
57
|
+
edges: Edge[];
|
|
58
|
+
offscreenPx: Partial<Record<Edge, number>>;
|
|
59
|
+
} | {
|
|
60
|
+
kind: 'overlap';
|
|
61
|
+
nodeIds: [string, string];
|
|
62
|
+
overlapPx: {
|
|
63
|
+
width: number;
|
|
64
|
+
height: number;
|
|
65
|
+
};
|
|
66
|
+
ratio: number;
|
|
67
|
+
};
|
|
68
|
+
export interface DeriveStructuralFactsOptions {
|
|
69
|
+
/** Edge excesses / overlaps at or below this many px are ignored, so that
|
|
70
|
+
* exact-touching layout edges don't read as overflow or overlap. */
|
|
71
|
+
tolerancePx?: number;
|
|
72
|
+
/** Minimum (intersection area / smaller node area) for an overlap to be
|
|
73
|
+
* reported as significant. */
|
|
74
|
+
overlapMinRatio?: number;
|
|
75
|
+
}
|
|
76
|
+
export declare function deriveStructuralFacts(dump: LayoutDump, options?: DeriveStructuralFactsOptions): StructuralFact[];
|
|
77
|
+
/**
|
|
78
|
+
* Render-side embedded form of the deriver.
|
|
79
|
+
*
|
|
80
|
+
* The Android render runs as a single self-contained generated script (see
|
|
81
|
+
* `generateAndroidRenderScript`), so render-side pure logic is embedded as a
|
|
82
|
+
* JS-source string and concatenated in — exactly like `RENDER_SCRIPT_TRIM_STAGE`.
|
|
83
|
+
* The typed `deriveStructuralFacts` above stays the canonical, tested spec; the
|
|
84
|
+
* `parity` test asserts this embedded port produces identical facts on shared
|
|
85
|
+
* fixtures so the two cannot drift.
|
|
86
|
+
*
|
|
87
|
+
* `deriveStructuralFacts` here is pure; `deriveAndWriteFacts` references runtime
|
|
88
|
+
* globals (`fs`, `path`, `artifactsDir`, `log`) defined in the main script and
|
|
89
|
+
* is only invoked from there — harmless to define in the eval sandbox.
|
|
90
|
+
*/
|
|
91
|
+
export declare const RENDER_SCRIPT_FACTS_STAGE: string;
|
|
92
|
+
/**
|
|
93
|
+
* Evaluates the embedded facts stage and returns its pure `deriveStructuralFacts`,
|
|
94
|
+
* so the parity test exercises exactly the code that ships in the runtime script
|
|
95
|
+
* (mirrors `getPostProcessorInternals`). `deriveAndWriteFacts` references runtime
|
|
96
|
+
* globals only when called, so defining it in the sandbox is harmless.
|
|
97
|
+
*/
|
|
98
|
+
export declare function getStructuralFactsStageInternals(): {
|
|
99
|
+
deriveStructuralFacts: (dump: LayoutDump, options?: DeriveStructuralFactsOptions) => StructuralFact[];
|
|
100
|
+
};
|
|
101
|
+
//# sourceMappingURL=structural-facts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"structural-facts.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/structural-facts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,oDAAoD;IACpD,EAAE,EAAE,MAAM,CAAC;IACX,+BAA+B;IAC/B,MAAM,EAAE,YAAY,CAAC;IACrB,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,0DAA0D;IAC1D,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACzB,+CAA+C;IAC/C,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AAED,MAAM,MAAM,IAAI,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEvD,MAAM,MAAM,cAAc,GACtB;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;CAC3C,GACD;IACE,IAAI,EAAE,mBAAmB,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;CAC1C,GACD;IACE,IAAI,EAAE,YAAY,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,WAAW,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;CAC5C,GACD;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1B,SAAS,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEN,MAAM,WAAW,4BAA4B;IAC3C;yEACqE;IACrE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;mCAC+B;IAC/B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAoCD,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,UAAU,EAChB,OAAO,GAAE,4BAAiC,GACzC,cAAc,EAAE,CA2ElB;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yBAAyB,EAAE,MAuHvC,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,gCAAgC,IAAI;IAClD,qBAAqB,EAAE,CACrB,IAAI,EAAE,UAAU,EAChB,OAAO,CAAC,EAAE,4BAA4B,KACnC,cAAc,EAAE,CAAC;CACvB,CAWA"}
|
|
Binary file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure unit tests for the structural-facts deriver (UI-fidelity structural
|
|
3
|
+
* pass, Signal A). Fixture-driven, no toolchain — the deriver turns a
|
|
4
|
+
* per-node layout dump into intent-agnostic geometric facts the fidelity
|
|
5
|
+
* judge checks instead of eyeballing.
|
|
6
|
+
*
|
|
7
|
+
* The canonical regression fixture is the motivating miss: a card clips an
|
|
8
|
+
* overflowing voucher tag; the deriver must surface the bottom clip as a
|
|
9
|
+
* fact so it is never again skipped by eye.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=structural-facts.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"structural-facts.test.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/structural-facts.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure unit tests for the structural-facts deriver (UI-fidelity structural
|
|
3
|
+
* pass, Signal A). Fixture-driven, no toolchain — the deriver turns a
|
|
4
|
+
* per-node layout dump into intent-agnostic geometric facts the fidelity
|
|
5
|
+
* judge checks instead of eyeballing.
|
|
6
|
+
*
|
|
7
|
+
* The canonical regression fixture is the motivating miss: a card clips an
|
|
8
|
+
* overflowing voucher tag; the deriver must surface the bottom clip as a
|
|
9
|
+
* fact so it is never again skipped by eye.
|
|
10
|
+
*/
|
|
11
|
+
import { deriveStructuralFacts, getStructuralFactsStageInternals, } from './structural-facts.js';
|
|
12
|
+
// 200x400 canvas in px for all fixtures unless noted.
|
|
13
|
+
const CANVAS = { left: 0, top: 0, right: 200, bottom: 400 };
|
|
14
|
+
describe('deriveStructuralFacts — containment', () => {
|
|
15
|
+
test('a fully-contained child produces no clip/overflow fact', () => {
|
|
16
|
+
const dump = {
|
|
17
|
+
canvas: CANVAS,
|
|
18
|
+
nodes: [
|
|
19
|
+
{ id: 'card', bounds: { left: 20, top: 20, right: 180, bottom: 200 }, parentId: null, clipsChildren: true },
|
|
20
|
+
{ id: 'label', bounds: { left: 30, top: 30, right: 170, bottom: 60 }, parentId: 'card', clipsChildren: false },
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
expect(deriveStructuralFacts(dump)).toEqual([]);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
describe('deriveStructuralFacts — overflow vs clip', () => {
|
|
27
|
+
test('child overflowing a NON-clipping parent is reported as overflow, not clipped', () => {
|
|
28
|
+
const dump = {
|
|
29
|
+
canvas: CANVAS,
|
|
30
|
+
nodes: [
|
|
31
|
+
{ id: 'row', bounds: { left: 20, top: 20, right: 180, bottom: 100 }, parentId: null, clipsChildren: false },
|
|
32
|
+
// sticks out 15px past the row's right edge
|
|
33
|
+
{ id: 'chip', bounds: { left: 150, top: 30, right: 195, bottom: 80 }, parentId: 'row', clipsChildren: false },
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
const facts = deriveStructuralFacts(dump);
|
|
37
|
+
expect(facts).toHaveLength(1);
|
|
38
|
+
expect(facts[0]).toMatchObject({
|
|
39
|
+
kind: 'overflow',
|
|
40
|
+
nodeId: 'chip',
|
|
41
|
+
parentId: 'row',
|
|
42
|
+
edges: ['right'],
|
|
43
|
+
overflowPx: { right: 15 },
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
test('child overflowing a CLIPPING parent is reported as clipped-by-parent with the clipped extent', () => {
|
|
47
|
+
const dump = {
|
|
48
|
+
canvas: CANVAS,
|
|
49
|
+
nodes: [
|
|
50
|
+
{ id: 'card', bounds: { left: 20, top: 20, right: 180, bottom: 200 }, parentId: null, clipsChildren: true },
|
|
51
|
+
// overflows the card's bottom by 24px → that extent is clipped away
|
|
52
|
+
{ id: 'tag', bounds: { left: 60, top: 150, right: 140, bottom: 224 }, parentId: 'card', clipsChildren: false },
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
const facts = deriveStructuralFacts(dump);
|
|
56
|
+
expect(facts).toHaveLength(1);
|
|
57
|
+
expect(facts[0]).toMatchObject({
|
|
58
|
+
kind: 'clipped_by_parent',
|
|
59
|
+
nodeId: 'tag',
|
|
60
|
+
clipperId: 'card',
|
|
61
|
+
edges: ['bottom'],
|
|
62
|
+
clippedPx: { bottom: 24 },
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe('deriveStructuralFacts — off-screen', () => {
|
|
67
|
+
test('a node extending past the canvas is reported off-screen on those edges', () => {
|
|
68
|
+
const dump = {
|
|
69
|
+
canvas: CANVAS,
|
|
70
|
+
nodes: [
|
|
71
|
+
{ id: 'root', bounds: { left: 0, top: 0, right: 200, bottom: 400 }, parentId: null, clipsChildren: false },
|
|
72
|
+
// bottom edge 30px below the canvas
|
|
73
|
+
{ id: 'sheet', bounds: { left: 0, top: 360, right: 200, bottom: 430 }, parentId: 'root', clipsChildren: false },
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
const facts = deriveStructuralFacts(dump);
|
|
77
|
+
const offscreen = facts.filter((f) => f.kind === 'off_screen');
|
|
78
|
+
expect(offscreen).toHaveLength(1);
|
|
79
|
+
expect(offscreen[0]).toMatchObject({
|
|
80
|
+
kind: 'off_screen',
|
|
81
|
+
nodeId: 'sheet',
|
|
82
|
+
edges: ['bottom'],
|
|
83
|
+
offscreenPx: { bottom: 30 },
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe('deriveStructuralFacts — overlap', () => {
|
|
88
|
+
test('two significantly overlapping siblings are reported as an overlap', () => {
|
|
89
|
+
const dump = {
|
|
90
|
+
canvas: CANVAS,
|
|
91
|
+
nodes: [
|
|
92
|
+
{ id: 'root', bounds: { left: 0, top: 0, right: 200, bottom: 400 }, parentId: null, clipsChildren: false },
|
|
93
|
+
{ id: 'a', bounds: { left: 20, top: 20, right: 120, bottom: 120 }, parentId: 'root', clipsChildren: false },
|
|
94
|
+
{ id: 'b', bounds: { left: 80, top: 60, right: 180, bottom: 160 }, parentId: 'root', clipsChildren: false },
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
const overlap = deriveStructuralFacts(dump).filter((f) => f.kind === 'overlap');
|
|
98
|
+
expect(overlap).toHaveLength(1);
|
|
99
|
+
expect(overlap[0]).toMatchObject({ kind: 'overlap', nodeIds: ['a', 'b'] });
|
|
100
|
+
});
|
|
101
|
+
test('barely-touching siblings within tolerance are not an overlap', () => {
|
|
102
|
+
const dump = {
|
|
103
|
+
canvas: CANVAS,
|
|
104
|
+
nodes: [
|
|
105
|
+
{ id: 'root', bounds: { left: 0, top: 0, right: 200, bottom: 400 }, parentId: null, clipsChildren: false },
|
|
106
|
+
{ id: 'a', bounds: { left: 20, top: 20, right: 100, bottom: 100 }, parentId: 'root', clipsChildren: false },
|
|
107
|
+
{ id: 'b', bounds: { left: 100, top: 20, right: 180, bottom: 100 }, parentId: 'root', clipsChildren: false },
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
expect(deriveStructuralFacts(dump).filter((f) => f.kind === 'overlap')).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe('embedded render-side stage parity', () => {
|
|
114
|
+
// The render script ships a JS-source port of the deriver. It must produce
|
|
115
|
+
// byte-identical facts to the canonical typed module, or render-side facts
|
|
116
|
+
// would silently diverge from what the tests guarantee.
|
|
117
|
+
const { deriveStructuralFacts: embedded } = getStructuralFactsStageInternals();
|
|
118
|
+
const fixtures = {
|
|
119
|
+
clip: {
|
|
120
|
+
canvas: CANVAS,
|
|
121
|
+
nodes: [
|
|
122
|
+
{ id: 'card', bounds: { left: 20, top: 20, right: 180, bottom: 200 }, parentId: null, clipsChildren: true },
|
|
123
|
+
{ id: 'tag', bounds: { left: 60, top: 150, right: 140, bottom: 224 }, parentId: 'card', clipsChildren: false },
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
overflow: {
|
|
127
|
+
canvas: CANVAS,
|
|
128
|
+
nodes: [
|
|
129
|
+
{ id: 'row', bounds: { left: 20, top: 20, right: 180, bottom: 100 }, parentId: null, clipsChildren: false },
|
|
130
|
+
{ id: 'chip', bounds: { left: 150, top: 30, right: 195, bottom: 80 }, parentId: 'row', clipsChildren: false },
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
overlap: {
|
|
134
|
+
canvas: CANVAS,
|
|
135
|
+
nodes: [
|
|
136
|
+
{ id: 'root', bounds: { left: 0, top: 0, right: 200, bottom: 400 }, parentId: null, clipsChildren: false },
|
|
137
|
+
{ id: 'a', bounds: { left: 20, top: 20, right: 120, bottom: 120 }, parentId: 'root', clipsChildren: false },
|
|
138
|
+
{ id: 'b', bounds: { left: 80, top: 60, right: 180, bottom: 160 }, parentId: 'root', clipsChildren: false },
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
for (const [name, dump] of Object.entries(fixtures)) {
|
|
143
|
+
test(`embedded port matches the module for the ${name} fixture`, () => {
|
|
144
|
+
expect(embedded(dump)).toEqual(deriveStructuralFacts(dump));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
describe('deriveStructuralFacts — motivating regression (voucher tag bottom-clip)', () => {
|
|
149
|
+
// The render-side layout dump for the HubPromoBanner-style case: a card
|
|
150
|
+
// clips children, and the voucher tag (with its euro coin) overflows the
|
|
151
|
+
// card's bottom. The deriver MUST surface the bottom clip so the verdict
|
|
152
|
+
// can compare its extent to the design and never miss it by eye again.
|
|
153
|
+
test('reports the voucher tag as clipped by the card at the bottom, with the clipped px', () => {
|
|
154
|
+
const dump = {
|
|
155
|
+
canvas: { left: 0, top: 0, right: 360, bottom: 720 },
|
|
156
|
+
nodes: [
|
|
157
|
+
{ id: 'HubPromoBanner', bounds: { left: 16, top: 100, right: 344, bottom: 262 }, parentId: null, clipsChildren: true },
|
|
158
|
+
{ id: 'promoText', bounds: { left: 32, top: 116, right: 240, bottom: 180 }, parentId: 'HubPromoBanner', clipsChildren: false },
|
|
159
|
+
// voucher tag overflows the card bottom (262) down to 280 → 18px clipped
|
|
160
|
+
{ id: 'voucherTag', bounds: { left: 250, top: 210, right: 330, bottom: 280 }, parentId: 'HubPromoBanner', clipsChildren: false },
|
|
161
|
+
],
|
|
162
|
+
};
|
|
163
|
+
const facts = deriveStructuralFacts(dump);
|
|
164
|
+
const clip = facts.find((f) => f.kind === 'clipped_by_parent' && f.nodeId === 'voucherTag');
|
|
165
|
+
expect(clip).toMatchObject({
|
|
166
|
+
kind: 'clipped_by_parent',
|
|
167
|
+
nodeId: 'voucherTag',
|
|
168
|
+
clipperId: 'HubPromoBanner',
|
|
169
|
+
edges: ['bottom'],
|
|
170
|
+
clippedPx: { bottom: 18 },
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
//# sourceMappingURL=structural-facts.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui-fidelity-preview-android.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ui-fidelity-preview-android.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;
|
|
1
|
+
{"version":3,"file":"ui-fidelity-preview-android.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ui-fidelity-preview-android.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAuBH,OAAO,KAAK,EAEV,uBAAuB,EACvB,uBAAuB,EAGxB,MAAM,0BAA0B,CAAC;AAElC,MAAM,WAAW,+BAA+B;IAC9C;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gEAAgE;IAChE,OAAO,EAAE,uBAAuB,EAAE,CAAC;IACnC;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAQD;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,8BAA8B,CAAC;AAqErE;;;;;;GAMG;AACH,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,+BAA+B,GACvC,uBAAuB,CAuKzB"}
|