@symbiote-native/test-utils 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -13
- package/build/host-crossings.d.ts +21 -0
- package/build/host-crossings.js +61 -0
- package/build/index.d.ts +3 -2
- package/build/index.js +14 -5
- package/build/live-tree.d.ts +114 -0
- package/build/live-tree.js +135 -0
- package/build/recording-host.d.ts +144 -0
- package/build/recording-host.js +409 -0
- package/package.json +4 -1
- package/build/fake-fabric.d.ts +0 -51
- package/build/fake-fabric.js +0 -151
- package/build/lowering-equivalence.d.ts +0 -135
- package/build/lowering-equivalence.js +0 -224
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import type { IFakeNode } from './fake-fabric';
|
|
2
|
-
/**
|
|
3
|
-
* A committed node stripped of per-mount identity. `tag`, `instanceHandle` and `parentFamilyTag`
|
|
4
|
-
* differ between two mounts of the same tree by construction, so comparing them would fail every
|
|
5
|
-
* correct adapter.
|
|
6
|
-
*/
|
|
7
|
-
export interface ICommittedShape {
|
|
8
|
-
viewName: string;
|
|
9
|
-
props: Record<string, unknown>;
|
|
10
|
-
children: ICommittedShape[];
|
|
11
|
-
}
|
|
12
|
-
export declare function normalizeCommitted(nodes: readonly IFakeNode[]): ICommittedShape[];
|
|
13
|
-
/**
|
|
14
|
-
* The result of one comparison. Differences are returned rather than thrown so a caller can assert
|
|
15
|
-
* on an EMPTY ARRAY and have its runner print the whole list at once — a thrown error reports the
|
|
16
|
-
* first mismatch and hides the rest, and the interesting failures here are plural (six missing keys
|
|
17
|
-
* across three nodes was the real one).
|
|
18
|
-
*/
|
|
19
|
-
export interface IEquivalenceResult {
|
|
20
|
-
equal: boolean;
|
|
21
|
-
differences: string[];
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* The assertion. Compare two committed trees taken from two separate mounts of the same props.
|
|
25
|
-
*
|
|
26
|
-
* The committed tree is the right subject and the RETAINED tree is not: anchors live only in the
|
|
27
|
-
* retained tree and are exactly what lowering removes, so the two retained trees legitimately
|
|
28
|
-
* differ. A committed tree holds no anchors, so it must match exactly, `RCTRawText` children
|
|
29
|
-
* included.
|
|
30
|
-
*/
|
|
31
|
-
export declare function compareLoweringEquivalence(componentTree: readonly IFakeNode[], loweredTree: readonly IFakeNode[]): IEquivalenceResult;
|
|
32
|
-
/**
|
|
33
|
-
* THE SECOND ASSERTION EVERY CASE OWES — an ABSOLUTE expectation, not a comparison.
|
|
34
|
-
*
|
|
35
|
-
* Find the committed node carrying `testID` and require `expected`'s keys to be present with those
|
|
36
|
-
* values. Extra keys are allowed: this asserts that a fold RAN, and pinning the whole payload would
|
|
37
|
-
* turn every unrelated prop addition into a failure here instead of in the test that owns it.
|
|
38
|
-
*
|
|
39
|
-
* Pass the value a fold PRODUCES, never the one the author wrote — `{ nativeID: 'x' }` for an
|
|
40
|
-
* authored `id="x"`, `{ ellipsizeMode: 'tail' }` for a `<Text>` that set none. An expectation
|
|
41
|
-
* restating the input passes with the fold deleted and is the same false green one level down.
|
|
42
|
-
*/
|
|
43
|
-
export declare function expectCommittedProps(tree: readonly IFakeNode[], testID: string, expected: Record<string, unknown>): IEquivalenceResult;
|
|
44
|
-
/**
|
|
45
|
-
* THE GUARD AGAINST THE MOST LIKELY FALSE GREEN: both arms taking the SAME path.
|
|
46
|
-
*
|
|
47
|
-
* Angular is the live instance — its primitives carry a dual selector (`'symbiote-view, View'`) and
|
|
48
|
-
* directive matching is resolved per TEMPLATE, so writing the intrinsic inside a template that
|
|
49
|
-
* imports the component resolves straight back to the component, silently. The general form is any
|
|
50
|
-
* adapter where the "lowered" snippet is hand-written intrinsic markup the renderer happens to route
|
|
51
|
-
* through the wrapper anyway. Two identical arms agree perfectly and prove nothing.
|
|
52
|
-
*
|
|
53
|
-
* The discriminator here is the RETAINED tree: a component form allocates a wrapper the lowered
|
|
54
|
-
* form does not, so the counts cannot be equal.
|
|
55
|
-
*
|
|
56
|
-
* !! THAT PREMISE IS PER-ADAPTER AND IT IS FALSE ON AT LEAST TWO OF THE FIVE. Corrected 2026-09-01,
|
|
57
|
-
* after the Solid and React arms hit it independently within the hour. A component only allocates a
|
|
58
|
-
* retained node if the FRAMEWORK gives it one:
|
|
59
|
-
*
|
|
60
|
-
* svelte HALF TRUE, and corrected by the svelte arm the same day: it holds only for a
|
|
61
|
-
* CHILDREN-BEARING wrapper, because the anchors are what cost the node. `Switch` and
|
|
62
|
-
* `TextInput` render one childless element with `p={descriptor.props}` and BOTH arms
|
|
63
|
-
* retain exactly one node — so this control fired on two correct arms before it ever
|
|
64
|
-
* reached the two real divergences in that file
|
|
65
|
-
* angular a per-component host element counts differ -> this control holds
|
|
66
|
-
* solid a component is a plain function returning the same host node — MEASURED
|
|
67
|
-
* byte-identical: {nodes:1, anchors:0, renderable:1} on both arms
|
|
68
|
-
* react the wrappers render the intrinsic themselves, so both arms build one node
|
|
69
|
-
* vue `hostComponent()` is a FunctionalComponent, so expected to match — UNMEASURED
|
|
70
|
-
*
|
|
71
|
-
* On an adapter where the counts legitimately agree this control fires on a CORRECT arm, which is
|
|
72
|
-
* worse than not having it: a control that cries wolf gets deleted, and the real guard goes with it.
|
|
73
|
-
*
|
|
74
|
-
* So each adapter owes a discriminator whose premise holds FOR IT, stated where it is used. THREE
|
|
75
|
-
* SHAPES EXIST, one per adapter family, and none is a fallback for another:
|
|
76
|
-
*
|
|
77
|
-
* node count this function. Holds where a component allocates a retained node —
|
|
78
|
-
* Svelte's anchors, Angular's per-component host. On Svelte it holds only
|
|
79
|
-
* for a CHILDREN-BEARING wrapper: `Switch` and `TextInput` render a single
|
|
80
|
-
* childless element and both arms retain one node. On ANGULAR it holds only
|
|
81
|
-
* for a COMPOSED component: `View`/`Text` are `SymbiotePrimitiveHost`s whose
|
|
82
|
-
* component IS the node, so both arms census 1.
|
|
83
|
-
* template text Angular's, and it exists because of the trap below. Both arms are built
|
|
84
|
-
* from template STRINGS, so the arm can assert that only the lowered one
|
|
85
|
-
* spells the intrinsic. Weaker than reading compiled output — it proves the
|
|
86
|
-
* author wrote two different tags, not that Angular resolved them
|
|
87
|
-
* differently — and it is the only thing left when no counter separates them.
|
|
88
|
-
* source spelling only the lowered arm names the BARE intrinsic, since a wrapper renders the
|
|
89
|
-
* `-managed` spelling. The strongest of the three — no runtime coincidence
|
|
90
|
-
* satisfies it — and available only where the arm can read its own source.
|
|
91
|
-
* compiled output `_$createComponent(View, …)` vs `_$createElement("symbiote-view")`. Solid's,
|
|
92
|
-
* because its arms are JSX compiled by the runner: there is no source to read
|
|
93
|
-
* at assertion time, and this is the same claim reachable from where it stands.
|
|
94
|
-
*
|
|
95
|
-
* A DISABLED discriminator can be disabled exactly where its trap lives, and only a break-test
|
|
96
|
-
* says so. Measured on Angular 2026-09-02: the node-count control was switched off for `View` and
|
|
97
|
-
* `Text` because their arms legitimately census equal — and those two are the ONLY primitives
|
|
98
|
-
* carrying the dual selector `'symbiote-view, View'`, i.e. the one construct that makes an
|
|
99
|
-
* intrinsic resolve back to its component. Handing the lowered arm its imports, which should have
|
|
100
|
-
* reproduced the trap, left all nine rows GREEN. The five rows where the control WAS active have
|
|
101
|
-
* single-name selectors and could never have shown it.
|
|
102
|
-
*
|
|
103
|
-
* So: when a control is scoped off for some members, ask whether the hazard it guards is
|
|
104
|
-
* concentrated in the members you excluded. Here it was entirely there.
|
|
105
|
-
*
|
|
106
|
-
* Do not generalise any of them into this file. Which artifact distinguishes the two paths is a fact
|
|
107
|
-
* about a framework, and this repo has a standing rule against absorbing those into shared code —
|
|
108
|
-
* a shared second control would force one answer onto five adapters, which is the mistake this
|
|
109
|
-
* correction exists to undo.
|
|
110
|
-
*
|
|
111
|
-
* THE SHAPE THAT TRANSFERS, offered as a pattern and deliberately NOT as a function here, for the
|
|
112
|
-
* reason the paragraph above gives. The one thing lowering must change on every adapter is the TAG:
|
|
113
|
-
* a wrapper renders the `-managed` spelling or a component boundary, and only the lowered path can
|
|
114
|
-
* name the bare intrinsic the spec declares. So the question each arm can ask in its own idiom is
|
|
115
|
-
* "does ONLY the lowered artifact name `HOST_PRIMITIVES[name].intrinsic`" — Svelte asks it of its
|
|
116
|
-
* two source strings, Solid of its compiled output, and an adapter with no compile step has to find
|
|
117
|
-
* its own answer. Two properties are worth keeping when you translate it: assert on the ARTIFACT
|
|
118
|
-
* rather than on runtime state, so no coincidence at mount can satisfy it, and assert the component
|
|
119
|
-
* arm does NOT name the intrinsic, or the check passes on two lowered arms.
|
|
120
|
-
*
|
|
121
|
-
* Match the tag with a boundary. `symbiote-text` is a prefix of `symbiote-text-input` and
|
|
122
|
-
* `symbiote-switch` of `symbiote-switch-managed`, so a bare substring test reads a wrapper's
|
|
123
|
-
* `-managed` output as the bare intrinsic and certifies two arms that are the same arm.
|
|
124
|
-
*
|
|
125
|
-
* Counts are passed IN rather than computed here: `censusRetainedTree` lives in the engine, and
|
|
126
|
-
* this package must not depend on it — test-utils is imported by the engine's own suite, so the
|
|
127
|
-
* edge would be a cycle. Each caller reads the census from the engine it already imports.
|
|
128
|
-
*/
|
|
129
|
-
export declare function assertArmsAreDistinct(componentRetainedNodes: number, loweredRetainedNodes: number): IEquivalenceResult;
|
|
130
|
-
/**
|
|
131
|
-
* THE SECOND FALSE GREEN: both arms empty. `committed` is `[]` until `completeRoot` runs, and two
|
|
132
|
-
* empty trees compare equal, so a wrong flush count silently compares nothing at all. Every arm must
|
|
133
|
-
* prove it committed something before its diff is believed.
|
|
134
|
-
*/
|
|
135
|
-
export declare function assertCommittedSomething(tree: readonly IFakeNode[], armName: string): IEquivalenceResult;
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
export function normalizeCommitted(nodes) {
|
|
2
|
-
return nodes.map(node => ({
|
|
3
|
-
viewName: node.viewName,
|
|
4
|
-
props: { ...node.props },
|
|
5
|
-
children: normalizeCommitted(node.children),
|
|
6
|
-
}));
|
|
7
|
-
}
|
|
8
|
-
function describePath(path) {
|
|
9
|
-
return path.length === 0 ? '<root>' : path.join(' > ');
|
|
10
|
-
}
|
|
11
|
-
// Props are compared by KEY NAME and value, never by count. Counting is what cost a day: two arms
|
|
12
|
-
// agreeing on a total while disagreeing on which keys make it up is a coincidence, not equivalence.
|
|
13
|
-
//
|
|
14
|
-
// `undefined` needs no special case, and adding one would HIDE a defect. `setProp` deletes a key
|
|
15
|
-
// written as `undefined` and no-ops when it was never there, and `setEventListener` derives its
|
|
16
|
-
// gate flag from `typeof value === 'function'` — so an absent key and an `undefined` key commit
|
|
17
|
-
// identically. An adapter whose diff fails to route a disappeared key is exactly what must fail.
|
|
18
|
-
function diffProps(path, component, lowered, differences) {
|
|
19
|
-
const keys = new Set([...Object.keys(component), ...Object.keys(lowered)]);
|
|
20
|
-
for (const key of [...keys].sort()) {
|
|
21
|
-
const inComponent = Object.hasOwn(component, key);
|
|
22
|
-
const inLowered = Object.hasOwn(lowered, key);
|
|
23
|
-
if (!inLowered) {
|
|
24
|
-
differences.push(`${describePath(path)}: lowered is MISSING "${key}" (component has ${JSON.stringify(component[key])}) — a fold the wrapper applied and the lowered path does not`);
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
if (!inComponent) {
|
|
28
|
-
differences.push(`${describePath(path)}: lowered has EXTRA "${key}" = ${JSON.stringify(lowered[key])}`);
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
const left = JSON.stringify(component[key]);
|
|
32
|
-
const right = JSON.stringify(lowered[key]);
|
|
33
|
-
if (left !== right) {
|
|
34
|
-
differences.push(`${describePath(path)}: "${key}" differs — component ${left}, lowered ${right}`);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
function diffShapes(path, component, lowered, differences) {
|
|
39
|
-
if (component.length !== lowered.length) {
|
|
40
|
-
differences.push(`${describePath(path)}: child count differs — component ${component.length}, lowered ${lowered.length}`);
|
|
41
|
-
}
|
|
42
|
-
const count = Math.max(component.length, lowered.length);
|
|
43
|
-
for (let index = 0; index < count; index += 1) {
|
|
44
|
-
const left = component[index];
|
|
45
|
-
const right = lowered[index];
|
|
46
|
-
if (left === undefined || right === undefined)
|
|
47
|
-
continue;
|
|
48
|
-
const here = [...path, `${left.viewName}[${index}]`];
|
|
49
|
-
if (left.viewName !== right.viewName) {
|
|
50
|
-
differences.push(`${describePath(path)}: child ${index} is ${left.viewName} on the component path and ${right.viewName} on the lowered one`);
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
diffProps(here, left.props, right.props, differences);
|
|
54
|
-
diffShapes(here, left.children, right.children, differences);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* The assertion. Compare two committed trees taken from two separate mounts of the same props.
|
|
59
|
-
*
|
|
60
|
-
* The committed tree is the right subject and the RETAINED tree is not: anchors live only in the
|
|
61
|
-
* retained tree and are exactly what lowering removes, so the two retained trees legitimately
|
|
62
|
-
* differ. A committed tree holds no anchors, so it must match exactly, `RCTRawText` children
|
|
63
|
-
* included.
|
|
64
|
-
*/
|
|
65
|
-
export function compareLoweringEquivalence(componentTree, loweredTree) {
|
|
66
|
-
const differences = [];
|
|
67
|
-
diffShapes([], normalizeCommitted(componentTree), normalizeCommitted(loweredTree), differences);
|
|
68
|
-
return { equal: differences.length === 0, differences };
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* THE SECOND ASSERTION EVERY CASE OWES — an ABSOLUTE expectation, not a comparison.
|
|
72
|
-
*
|
|
73
|
-
* Find the committed node carrying `testID` and require `expected`'s keys to be present with those
|
|
74
|
-
* values. Extra keys are allowed: this asserts that a fold RAN, and pinning the whole payload would
|
|
75
|
-
* turn every unrelated prop addition into a failure here instead of in the test that owns it.
|
|
76
|
-
*
|
|
77
|
-
* Pass the value a fold PRODUCES, never the one the author wrote — `{ nativeID: 'x' }` for an
|
|
78
|
-
* authored `id="x"`, `{ ellipsizeMode: 'tail' }` for a `<Text>` that set none. An expectation
|
|
79
|
-
* restating the input passes with the fold deleted and is the same false green one level down.
|
|
80
|
-
*/
|
|
81
|
-
export function expectCommittedProps(tree, testID, expected) {
|
|
82
|
-
const differences = [];
|
|
83
|
-
const found = findByTestID(tree, testID);
|
|
84
|
-
if (found === undefined) {
|
|
85
|
-
return {
|
|
86
|
-
equal: false,
|
|
87
|
-
differences: [
|
|
88
|
-
`no committed node carries testID "${testID}" — the mount did not flush, or the prop never reached the payload`,
|
|
89
|
-
],
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
for (const key of Object.keys(expected).sort()) {
|
|
93
|
-
const actual = JSON.stringify(found.props[key]);
|
|
94
|
-
const wanted = JSON.stringify(expected[key]);
|
|
95
|
-
if (actual !== wanted) {
|
|
96
|
-
differences.push(`${testID}: "${key}" is ${actual}, expected ${wanted} — the fold that produces it did not run on this path`);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return { equal: differences.length === 0, differences };
|
|
100
|
-
}
|
|
101
|
-
function findByTestID(nodes, testID) {
|
|
102
|
-
for (const node of nodes) {
|
|
103
|
-
if (node.props.testID === testID)
|
|
104
|
-
return node;
|
|
105
|
-
const found = findByTestID(node.children, testID);
|
|
106
|
-
if (found !== undefined)
|
|
107
|
-
return found;
|
|
108
|
-
}
|
|
109
|
-
return undefined;
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* THE GUARD AGAINST THE MOST LIKELY FALSE GREEN: both arms taking the SAME path.
|
|
113
|
-
*
|
|
114
|
-
* Angular is the live instance — its primitives carry a dual selector (`'symbiote-view, View'`) and
|
|
115
|
-
* directive matching is resolved per TEMPLATE, so writing the intrinsic inside a template that
|
|
116
|
-
* imports the component resolves straight back to the component, silently. The general form is any
|
|
117
|
-
* adapter where the "lowered" snippet is hand-written intrinsic markup the renderer happens to route
|
|
118
|
-
* through the wrapper anyway. Two identical arms agree perfectly and prove nothing.
|
|
119
|
-
*
|
|
120
|
-
* The discriminator here is the RETAINED tree: a component form allocates a wrapper the lowered
|
|
121
|
-
* form does not, so the counts cannot be equal.
|
|
122
|
-
*
|
|
123
|
-
* !! THAT PREMISE IS PER-ADAPTER AND IT IS FALSE ON AT LEAST TWO OF THE FIVE. Corrected 2026-09-01,
|
|
124
|
-
* after the Solid and React arms hit it independently within the hour. A component only allocates a
|
|
125
|
-
* retained node if the FRAMEWORK gives it one:
|
|
126
|
-
*
|
|
127
|
-
* svelte HALF TRUE, and corrected by the svelte arm the same day: it holds only for a
|
|
128
|
-
* CHILDREN-BEARING wrapper, because the anchors are what cost the node. `Switch` and
|
|
129
|
-
* `TextInput` render one childless element with `p={descriptor.props}` and BOTH arms
|
|
130
|
-
* retain exactly one node — so this control fired on two correct arms before it ever
|
|
131
|
-
* reached the two real divergences in that file
|
|
132
|
-
* angular a per-component host element counts differ -> this control holds
|
|
133
|
-
* solid a component is a plain function returning the same host node — MEASURED
|
|
134
|
-
* byte-identical: {nodes:1, anchors:0, renderable:1} on both arms
|
|
135
|
-
* react the wrappers render the intrinsic themselves, so both arms build one node
|
|
136
|
-
* vue `hostComponent()` is a FunctionalComponent, so expected to match — UNMEASURED
|
|
137
|
-
*
|
|
138
|
-
* On an adapter where the counts legitimately agree this control fires on a CORRECT arm, which is
|
|
139
|
-
* worse than not having it: a control that cries wolf gets deleted, and the real guard goes with it.
|
|
140
|
-
*
|
|
141
|
-
* So each adapter owes a discriminator whose premise holds FOR IT, stated where it is used. THREE
|
|
142
|
-
* SHAPES EXIST, one per adapter family, and none is a fallback for another:
|
|
143
|
-
*
|
|
144
|
-
* node count this function. Holds where a component allocates a retained node —
|
|
145
|
-
* Svelte's anchors, Angular's per-component host. On Svelte it holds only
|
|
146
|
-
* for a CHILDREN-BEARING wrapper: `Switch` and `TextInput` render a single
|
|
147
|
-
* childless element and both arms retain one node. On ANGULAR it holds only
|
|
148
|
-
* for a COMPOSED component: `View`/`Text` are `SymbiotePrimitiveHost`s whose
|
|
149
|
-
* component IS the node, so both arms census 1.
|
|
150
|
-
* template text Angular's, and it exists because of the trap below. Both arms are built
|
|
151
|
-
* from template STRINGS, so the arm can assert that only the lowered one
|
|
152
|
-
* spells the intrinsic. Weaker than reading compiled output — it proves the
|
|
153
|
-
* author wrote two different tags, not that Angular resolved them
|
|
154
|
-
* differently — and it is the only thing left when no counter separates them.
|
|
155
|
-
* source spelling only the lowered arm names the BARE intrinsic, since a wrapper renders the
|
|
156
|
-
* `-managed` spelling. The strongest of the three — no runtime coincidence
|
|
157
|
-
* satisfies it — and available only where the arm can read its own source.
|
|
158
|
-
* compiled output `_$createComponent(View, …)` vs `_$createElement("symbiote-view")`. Solid's,
|
|
159
|
-
* because its arms are JSX compiled by the runner: there is no source to read
|
|
160
|
-
* at assertion time, and this is the same claim reachable from where it stands.
|
|
161
|
-
*
|
|
162
|
-
* A DISABLED discriminator can be disabled exactly where its trap lives, and only a break-test
|
|
163
|
-
* says so. Measured on Angular 2026-09-02: the node-count control was switched off for `View` and
|
|
164
|
-
* `Text` because their arms legitimately census equal — and those two are the ONLY primitives
|
|
165
|
-
* carrying the dual selector `'symbiote-view, View'`, i.e. the one construct that makes an
|
|
166
|
-
* intrinsic resolve back to its component. Handing the lowered arm its imports, which should have
|
|
167
|
-
* reproduced the trap, left all nine rows GREEN. The five rows where the control WAS active have
|
|
168
|
-
* single-name selectors and could never have shown it.
|
|
169
|
-
*
|
|
170
|
-
* So: when a control is scoped off for some members, ask whether the hazard it guards is
|
|
171
|
-
* concentrated in the members you excluded. Here it was entirely there.
|
|
172
|
-
*
|
|
173
|
-
* Do not generalise any of them into this file. Which artifact distinguishes the two paths is a fact
|
|
174
|
-
* about a framework, and this repo has a standing rule against absorbing those into shared code —
|
|
175
|
-
* a shared second control would force one answer onto five adapters, which is the mistake this
|
|
176
|
-
* correction exists to undo.
|
|
177
|
-
*
|
|
178
|
-
* THE SHAPE THAT TRANSFERS, offered as a pattern and deliberately NOT as a function here, for the
|
|
179
|
-
* reason the paragraph above gives. The one thing lowering must change on every adapter is the TAG:
|
|
180
|
-
* a wrapper renders the `-managed` spelling or a component boundary, and only the lowered path can
|
|
181
|
-
* name the bare intrinsic the spec declares. So the question each arm can ask in its own idiom is
|
|
182
|
-
* "does ONLY the lowered artifact name `HOST_PRIMITIVES[name].intrinsic`" — Svelte asks it of its
|
|
183
|
-
* two source strings, Solid of its compiled output, and an adapter with no compile step has to find
|
|
184
|
-
* its own answer. Two properties are worth keeping when you translate it: assert on the ARTIFACT
|
|
185
|
-
* rather than on runtime state, so no coincidence at mount can satisfy it, and assert the component
|
|
186
|
-
* arm does NOT name the intrinsic, or the check passes on two lowered arms.
|
|
187
|
-
*
|
|
188
|
-
* Match the tag with a boundary. `symbiote-text` is a prefix of `symbiote-text-input` and
|
|
189
|
-
* `symbiote-switch` of `symbiote-switch-managed`, so a bare substring test reads a wrapper's
|
|
190
|
-
* `-managed` output as the bare intrinsic and certifies two arms that are the same arm.
|
|
191
|
-
*
|
|
192
|
-
* Counts are passed IN rather than computed here: `censusRetainedTree` lives in the engine, and
|
|
193
|
-
* this package must not depend on it — test-utils is imported by the engine's own suite, so the
|
|
194
|
-
* edge would be a cycle. Each caller reads the census from the engine it already imports.
|
|
195
|
-
*/
|
|
196
|
-
export function assertArmsAreDistinct(componentRetainedNodes, loweredRetainedNodes) {
|
|
197
|
-
if (componentRetainedNodes === loweredRetainedNodes) {
|
|
198
|
-
return {
|
|
199
|
-
equal: false,
|
|
200
|
-
differences: [
|
|
201
|
-
`both arms retained ${componentRetainedNodes} nodes — the lowered arm did not lower. ` +
|
|
202
|
-
`Check that the intrinsic spelling is not resolving back to the component (a dual ` +
|
|
203
|
-
`selector, or a template that still imports the wrapper).`,
|
|
204
|
-
],
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
return { equal: true, differences: [] };
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* THE SECOND FALSE GREEN: both arms empty. `committed` is `[]` until `completeRoot` runs, and two
|
|
211
|
-
* empty trees compare equal, so a wrong flush count silently compares nothing at all. Every arm must
|
|
212
|
-
* prove it committed something before its diff is believed.
|
|
213
|
-
*/
|
|
214
|
-
export function assertCommittedSomething(tree, armName) {
|
|
215
|
-
const count = normalizeCommitted(tree).length;
|
|
216
|
-
return count > 0
|
|
217
|
-
? { equal: true, differences: [] }
|
|
218
|
-
: {
|
|
219
|
-
equal: false,
|
|
220
|
-
differences: [
|
|
221
|
-
`the ${armName} arm committed nothing — the mount did not flush, so the diff below compares two empty trees`,
|
|
222
|
-
],
|
|
223
|
-
};
|
|
224
|
-
}
|