@ccheever/exact-renderer 0.1.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/package.json +118 -0
- package/src/__tests__/adapter-window-state.test.tsx +190 -0
- package/src/__tests__/attrs.test.ts +157 -0
- package/src/__tests__/classname.test.ts +332 -0
- package/src/__tests__/color.test.ts +169 -0
- package/src/__tests__/dom-mirror.test.ts +682 -0
- package/src/__tests__/dom-shim.test.ts +274 -0
- package/src/__tests__/fixtures/SvelteCounter.svelte +7 -0
- package/src/__tests__/fixtures/SvelteInput.svelte +8 -0
- package/src/__tests__/host-config.test.ts +51 -0
- package/src/__tests__/host-ops.test.ts +2234 -0
- package/src/__tests__/image-source.test.ts +135 -0
- package/src/__tests__/liquid-glass.test.ts +72 -0
- package/src/__tests__/multi-root.test.ts +118 -0
- package/src/__tests__/native-view-events.test.ts +102 -0
- package/src/__tests__/nodes.test.ts +399 -0
- package/src/__tests__/normalize.test.ts +576 -0
- package/src/__tests__/paragraph-lowering.test.tsx +144 -0
- package/src/__tests__/props.test.ts +518 -0
- package/src/__tests__/protocol-encoder.test.ts +732 -0
- package/src/__tests__/protocol-fixture-bytes.test.ts +41 -0
- package/src/__tests__/reconciler.test.tsx +241 -0
- package/src/__tests__/svelte-adapter.test.ts +166 -0
- package/src/__tests__/svg-source.test.ts +71 -0
- package/src/__tests__/tags.test.ts +354 -0
- package/src/__tests__/toggle.test.ts +441 -0
- package/src/__tests__/transitions.test.ts +106 -0
- package/src/__tests__/web-primitives.test.tsx +454 -0
- package/src/__tests__/window-hooks.test.tsx +447 -0
- package/src/adapter-contract.ts +68 -0
- package/src/attrs.ts +596 -0
- package/src/classname-contract.ts +87 -0
- package/src/classname-resolve.ts +553 -0
- package/src/classname-runtime.ts +29 -0
- package/src/components.ts +214 -0
- package/src/css-variable-context.ts +83 -0
- package/src/dom-hydration.ts +160 -0
- package/src/dom-mirror.ts +1459 -0
- package/src/dom-shim.ts +1736 -0
- package/src/group-context.ts +69 -0
- package/src/host-config.ts +431 -0
- package/src/host-ops.ts +3167 -0
- package/src/image-source.native.ts +703 -0
- package/src/image-source.ts +554 -0
- package/src/index.ts +278 -0
- package/src/inspector-runtime.ts +244 -0
- package/src/inspector.ts +3570 -0
- package/src/jsx-augmentations.ts +54 -0
- package/src/keyboard-avoidance.ts +217 -0
- package/src/native-primitives.ts +43 -0
- package/src/native-view-events.ts +322 -0
- package/src/native-view.ts +60 -0
- package/src/nodes/index.ts +41 -0
- package/src/nodes/node.ts +531 -0
- package/src/peer-context.ts +100 -0
- package/src/primitives.native.ts +8 -0
- package/src/primitives.ts +8 -0
- package/src/props/index.ts +14 -0
- package/src/props/normalize.ts +816 -0
- package/src/protocol/encoder.ts +940 -0
- package/src/protocol/index.ts +33 -0
- package/src/reconciler.ts +581 -0
- package/src/runtime.ts +11 -0
- package/src/safe-area.ts +543 -0
- package/src/solid.ts +490 -0
- package/src/style/color.js +1 -0
- package/src/style/color.ts +15 -0
- package/src/style/index.js +1 -0
- package/src/style/index.ts +22 -0
- package/src/style/normalize.js +1 -0
- package/src/style/normalize.ts +1426 -0
- package/src/svelte.ts +349 -0
- package/src/svg-source.ts +222 -0
- package/src/tags/index.ts +21 -0
- package/src/tags/tag-map.ts +289 -0
- package/src/text/paragraph-lowering.ts +310 -0
- package/src/types.ts +1175 -0
- package/src/vue.ts +535 -0
- package/src/web-host.ts +19 -0
- package/src/web-primitives.ts +1654 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// @tactical @ref LLP 0150 idea 1 — TS encoder vs kernel conformance-fixture byte parity.
|
|
2
|
+
//
|
|
3
|
+
// The kernel's CommandBufferBuilder generates the committed binary fixture
|
|
4
|
+
// tests/protocol/fixtures/multi-root-batch-v3.bin (regenerated with
|
|
5
|
+
// `EXACT_REGEN_PROTOCOL_FIXTURES=1 cargo test -p exact-kernel -- protocol_fixture`).
|
|
6
|
+
// This test encodes the same logical ops through the TS BufferWriter — the
|
|
7
|
+
// byte-emitting core under packages/exact-renderer/src/protocol/encoder.ts —
|
|
8
|
+
// and asserts the output is byte-for-byte identical. If this fails, either
|
|
9
|
+
// the TS encoder or the Rust builder changed the wire encoding: treat it as
|
|
10
|
+
// an ABI change and reconcile both sides before regenerating the fixture.
|
|
11
|
+
import { describe, expect, it } from 'vitest';
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
import { BufferWriter } from '../../../exact-core/src/protocol/buffer-writer.js';
|
|
17
|
+
import { NodeType, PropId } from '../../../exact-core/src/protocol/opcodes.js';
|
|
18
|
+
|
|
19
|
+
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..');
|
|
20
|
+
const fixturePath = join(repoRoot, 'tests', 'protocol', 'fixtures', 'multi-root-batch-v3.bin');
|
|
21
|
+
|
|
22
|
+
describe('protocol fixture byte parity (LLP 0150 idea 1)', () => {
|
|
23
|
+
it('BufferWriter reproduces multi-root-batch-v3.bin byte-for-byte', () => {
|
|
24
|
+
const expected = new Uint8Array(readFileSync(fixturePath));
|
|
25
|
+
|
|
26
|
+
// Same logical ops as build_multi_root_batch_v3_fixture() in
|
|
27
|
+
// kernel/src/protocol/mod.rs: sequence 1, root 42, v3 header.
|
|
28
|
+
const writer = new BufferWriter(1024, 1, 42);
|
|
29
|
+
writer.createView(1, NodeType.View);
|
|
30
|
+
writer.createView(2, NodeType.Text);
|
|
31
|
+
writer.createView(3, NodeType.View);
|
|
32
|
+
writer.setPropString(2, PropId.TextContent, 'hello exact');
|
|
33
|
+
writer.setStyleSize(1, 320, 200);
|
|
34
|
+
writer.setChildren(1, [2, 3]);
|
|
35
|
+
writer.computeLayout(1, 320, 200);
|
|
36
|
+
const actual = new Uint8Array(writer.build());
|
|
37
|
+
|
|
38
|
+
expect(actual.length).toBe(expected.length);
|
|
39
|
+
expect(Array.from(actual)).toEqual(Array.from(expected));
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import React, { act } from 'react';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { Pressable, Text } from '../components.js';
|
|
5
|
+
import { _clearHandlers } from '../host-config.js';
|
|
6
|
+
import { EventType } from '../host-ops.js';
|
|
7
|
+
import { NodeKind, _resetNodeIdCounter } from '../nodes/index.js';
|
|
8
|
+
import {
|
|
9
|
+
_getRootContainer,
|
|
10
|
+
prepareForBundleReplacement,
|
|
11
|
+
render,
|
|
12
|
+
reset,
|
|
13
|
+
} from '../reconciler.js';
|
|
14
|
+
|
|
15
|
+
interface ExactRuntimeMock {
|
|
16
|
+
dispatch: ReturnType<typeof vi.fn>;
|
|
17
|
+
screenWidth: number;
|
|
18
|
+
screenHeight: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ExactGlobals {
|
|
22
|
+
exact?: ExactRuntimeMock;
|
|
23
|
+
__exactDispatchEvent?: (handlerId: number, payload: unknown) => void;
|
|
24
|
+
__exactViewportChanged?: (width?: number, height?: number) => void;
|
|
25
|
+
IS_REACT_ACT_ENVIRONMENT?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getTextElement(): {
|
|
29
|
+
kind: NodeKind.Element;
|
|
30
|
+
props: Record<string, unknown>;
|
|
31
|
+
} {
|
|
32
|
+
const root = _getRootContainer();
|
|
33
|
+
expect(root).not.toBeNull();
|
|
34
|
+
|
|
35
|
+
const child = root!.children[0];
|
|
36
|
+
expect(child).toBeDefined();
|
|
37
|
+
expect(child?.kind).toBe(NodeKind.Element);
|
|
38
|
+
|
|
39
|
+
if (!child || child.kind !== NodeKind.Element) {
|
|
40
|
+
throw new Error('Expected root child to be an element.');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return child;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getPressableBindingHandlerId(): number {
|
|
47
|
+
const root = _getRootContainer();
|
|
48
|
+
expect(root).not.toBeNull();
|
|
49
|
+
|
|
50
|
+
const child = root!.children[0];
|
|
51
|
+
expect(child).toBeDefined();
|
|
52
|
+
expect(child?.kind).toBe(NodeKind.Element);
|
|
53
|
+
|
|
54
|
+
if (!child || child.kind !== NodeKind.Element) {
|
|
55
|
+
throw new Error('Expected root child to be a Pressable element.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const binding = child.events.get(EventType.Press);
|
|
59
|
+
expect(binding).toBeDefined();
|
|
60
|
+
|
|
61
|
+
return binding!.handlerId;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getNestedTextContent(): string | undefined {
|
|
65
|
+
const root = _getRootContainer();
|
|
66
|
+
expect(root).not.toBeNull();
|
|
67
|
+
|
|
68
|
+
const pressable = root!.children[0];
|
|
69
|
+
expect(pressable).toBeDefined();
|
|
70
|
+
expect(pressable?.kind).toBe(NodeKind.Element);
|
|
71
|
+
|
|
72
|
+
if (!pressable || pressable.kind !== NodeKind.Element) {
|
|
73
|
+
throw new Error('Expected root child to be a Pressable element.');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const text = pressable.children[0];
|
|
77
|
+
expect(text).toBeDefined();
|
|
78
|
+
expect(text?.kind).toBe(NodeKind.Element);
|
|
79
|
+
|
|
80
|
+
if (!text || text.kind !== NodeKind.Element) {
|
|
81
|
+
throw new Error('Expected Pressable child to be a Text element.');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return text.props.textContent as string | undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe('reconciler commit flushing', () => {
|
|
88
|
+
let dispatch: ReturnType<typeof vi.fn>;
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
vi.useFakeTimers();
|
|
92
|
+
dispatch = vi.fn();
|
|
93
|
+
|
|
94
|
+
const globals = globalThis as typeof globalThis & ExactGlobals;
|
|
95
|
+
globals.IS_REACT_ACT_ENVIRONMENT = true;
|
|
96
|
+
globals.exact = {
|
|
97
|
+
dispatch,
|
|
98
|
+
screenWidth: 393,
|
|
99
|
+
screenHeight: 852,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
_resetNodeIdCounter();
|
|
103
|
+
_clearHandlers();
|
|
104
|
+
reset();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
afterEach(() => {
|
|
108
|
+
reset();
|
|
109
|
+
_clearHandlers();
|
|
110
|
+
_resetNodeIdCounter();
|
|
111
|
+
vi.useRealTimers();
|
|
112
|
+
|
|
113
|
+
const globals = globalThis as typeof globalThis & ExactGlobals;
|
|
114
|
+
delete globals.exact;
|
|
115
|
+
delete globals.IS_REACT_ACT_ENVIRONMENT;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('dispatches a fresh native commit for timer-driven state updates', async () => {
|
|
119
|
+
function TimerCounter(): React.ReactElement {
|
|
120
|
+
const [label, setLabel] = React.useState('initial');
|
|
121
|
+
|
|
122
|
+
React.useEffect(() => {
|
|
123
|
+
const timeoutId = setTimeout(() => {
|
|
124
|
+
setLabel('updated');
|
|
125
|
+
}, 10);
|
|
126
|
+
|
|
127
|
+
return () => clearTimeout(timeoutId);
|
|
128
|
+
}, []);
|
|
129
|
+
|
|
130
|
+
return <Text>{label}</Text>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await act(async () => {
|
|
134
|
+
render(<TimerCounter />);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(dispatch).toHaveBeenCalledTimes(1);
|
|
138
|
+
expect(getTextElement().props.textContent).toBe('initial');
|
|
139
|
+
|
|
140
|
+
await act(async () => {
|
|
141
|
+
vi.advanceTimersByTime(10);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(dispatch).toHaveBeenCalledTimes(2);
|
|
145
|
+
expect(getTextElement().props.textContent).toBe('updated');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('dispatches a fresh native commit for state updates from native press events', async () => {
|
|
149
|
+
function PressCounter(): React.ReactElement {
|
|
150
|
+
const [count, setCount] = React.useState(0);
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<Pressable onPress={() => setCount((value) => value + 1)}>
|
|
154
|
+
<Text>{`Count: ${count}`}</Text>
|
|
155
|
+
</Pressable>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
await act(async () => {
|
|
160
|
+
render(<PressCounter />);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(dispatch).toHaveBeenCalledTimes(1);
|
|
164
|
+
expect(getNestedTextContent()).toBe('Count: 0');
|
|
165
|
+
|
|
166
|
+
const globals = globalThis as typeof globalThis & ExactGlobals;
|
|
167
|
+
const dispatchEvent = globals.__exactDispatchEvent;
|
|
168
|
+
expect(dispatchEvent).toBeTypeOf('function');
|
|
169
|
+
|
|
170
|
+
await act(async () => {
|
|
171
|
+
dispatchEvent!(getPressableBindingHandlerId(), {
|
|
172
|
+
nativeEvent: {
|
|
173
|
+
timestamp: Date.now(),
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(dispatch).toHaveBeenCalledTimes(2);
|
|
179
|
+
expect(getNestedTextContent()).toBe('Count: 1');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('batches multiple React updates from one native event into one native commit', async () => {
|
|
183
|
+
function PressCounter(): React.ReactElement {
|
|
184
|
+
const [count, setCount] = React.useState(0);
|
|
185
|
+
const [armed, setArmed] = React.useState(false);
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<Pressable
|
|
189
|
+
onPress={() => {
|
|
190
|
+
setCount((value) => value + 1);
|
|
191
|
+
setArmed((value) => !value);
|
|
192
|
+
}}
|
|
193
|
+
>
|
|
194
|
+
<Text>{`Count: ${count} ${armed ? 'armed' : 'idle'}`}</Text>
|
|
195
|
+
</Pressable>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await act(async () => {
|
|
200
|
+
render(<PressCounter />);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(dispatch).toHaveBeenCalledTimes(1);
|
|
204
|
+
|
|
205
|
+
const globals = globalThis as typeof globalThis & ExactGlobals;
|
|
206
|
+
const dispatchEvent = globals.__exactDispatchEvent;
|
|
207
|
+
expect(dispatchEvent).toBeTypeOf('function');
|
|
208
|
+
|
|
209
|
+
await act(async () => {
|
|
210
|
+
dispatchEvent!(getPressableBindingHandlerId(), {
|
|
211
|
+
nativeEvent: {
|
|
212
|
+
timestamp: Date.now(),
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
expect(dispatch).toHaveBeenCalledTimes(2);
|
|
218
|
+
expect(getNestedTextContent()).toBe('Count: 1 armed');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('clears renderer state for bundle replacement without sending a teardown batch', async () => {
|
|
222
|
+
await act(async () => {
|
|
223
|
+
render(<Text>first</Text>);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(dispatch).toHaveBeenCalledTimes(1);
|
|
227
|
+
expect(getTextElement().props.textContent).toBe('first');
|
|
228
|
+
|
|
229
|
+
prepareForBundleReplacement();
|
|
230
|
+
|
|
231
|
+
expect(dispatch).toHaveBeenCalledTimes(1);
|
|
232
|
+
expect(_getRootContainer()).toBeNull();
|
|
233
|
+
|
|
234
|
+
await act(async () => {
|
|
235
|
+
render(<Text>second</Text>);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
expect(dispatch).toHaveBeenCalledTimes(2);
|
|
239
|
+
expect(getTextElement().props.textContent).toBe('second');
|
|
240
|
+
});
|
|
241
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { getRootNode } from '../inspector.js';
|
|
4
|
+
import { _resetNodeIdCounter, NodeKind, type ElementNode } from '../nodes/index.js';
|
|
5
|
+
import { EventType, dispatchSyntheticEvent } from '../host-ops.js';
|
|
6
|
+
import { _clearHandlers } from '../host-config.js';
|
|
7
|
+
import { _resetHostOpsState } from '../host-ops.js';
|
|
8
|
+
import { _resetInspectorState } from '../inspector.js';
|
|
9
|
+
import { flushSync, mount, reset } from '../svelte.js';
|
|
10
|
+
|
|
11
|
+
import Counter from './fixtures/SvelteCounter.svelte';
|
|
12
|
+
import Input from './fixtures/SvelteInput.svelte';
|
|
13
|
+
|
|
14
|
+
interface ExactRuntimeMock {
|
|
15
|
+
dispatch: ReturnType<typeof vi.fn>;
|
|
16
|
+
screenWidth: number;
|
|
17
|
+
screenHeight: number;
|
|
18
|
+
screenScale?: number;
|
|
19
|
+
safeAreaInsets?: {
|
|
20
|
+
top?: number;
|
|
21
|
+
right?: number;
|
|
22
|
+
bottom?: number;
|
|
23
|
+
left?: number;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ExactGlobals {
|
|
28
|
+
exact?: ExactRuntimeMock;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function flushMicrotasks(): Promise<void> {
|
|
32
|
+
return Promise.resolve().then(() => Promise.resolve());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getElementByTag(rootId: number, tagType: string): ElementNode {
|
|
36
|
+
const root = getRootNode(rootId);
|
|
37
|
+
expect(root).not.toBeNull();
|
|
38
|
+
|
|
39
|
+
const find = (nodes: ElementNode['children']): ElementNode | null => {
|
|
40
|
+
for (const child of nodes) {
|
|
41
|
+
if (child.kind !== NodeKind.Element) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (child.originalTag === tagType) {
|
|
45
|
+
return child;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const nested = find(child.children);
|
|
49
|
+
if (nested) {
|
|
50
|
+
return nested;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return null;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const match = find(root?.children ?? []);
|
|
58
|
+
if (!match) {
|
|
59
|
+
throw new Error(`Expected root ${rootId} to contain <${tagType}>.`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return match;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getTextContent(rootId: number = 0): string {
|
|
66
|
+
const root = getRootNode(rootId);
|
|
67
|
+
expect(root).not.toBeNull();
|
|
68
|
+
|
|
69
|
+
const collect = (node: ElementNode): string =>
|
|
70
|
+
node.children
|
|
71
|
+
.map((child) => {
|
|
72
|
+
if (child.kind === NodeKind.Text) {
|
|
73
|
+
return child.text;
|
|
74
|
+
}
|
|
75
|
+
if (child.kind === NodeKind.Element) {
|
|
76
|
+
return collect(child);
|
|
77
|
+
}
|
|
78
|
+
return '';
|
|
79
|
+
})
|
|
80
|
+
.join('');
|
|
81
|
+
|
|
82
|
+
const firstElement = root?.children.find(
|
|
83
|
+
(child): child is ElementNode => child.kind === NodeKind.Element,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (!firstElement) {
|
|
87
|
+
return '';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return collect(firstElement);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
const globals = globalThis as typeof globalThis & ExactGlobals;
|
|
95
|
+
globals.exact = {
|
|
96
|
+
dispatch: vi.fn(),
|
|
97
|
+
screenWidth: 393,
|
|
98
|
+
screenHeight: 852,
|
|
99
|
+
screenScale: 2,
|
|
100
|
+
safeAreaInsets: {
|
|
101
|
+
top: 0,
|
|
102
|
+
right: 0,
|
|
103
|
+
bottom: 0,
|
|
104
|
+
left: 0,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
_resetNodeIdCounter();
|
|
109
|
+
_clearHandlers();
|
|
110
|
+
_resetHostOpsState();
|
|
111
|
+
_resetInspectorState();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
afterEach(async () => {
|
|
115
|
+
await reset();
|
|
116
|
+
|
|
117
|
+
const globals = globalThis as typeof globalThis & ExactGlobals;
|
|
118
|
+
delete globals.exact;
|
|
119
|
+
|
|
120
|
+
_clearHandlers();
|
|
121
|
+
_resetNodeIdCounter();
|
|
122
|
+
_resetHostOpsState();
|
|
123
|
+
_resetInspectorState();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('Exact Svelte adapter', () => {
|
|
127
|
+
it('mounts a real Svelte component and handles direct click listeners', async () => {
|
|
128
|
+
mount(Counter);
|
|
129
|
+
await flushMicrotasks();
|
|
130
|
+
|
|
131
|
+
expect(getTextContent()).toContain('Count 0');
|
|
132
|
+
|
|
133
|
+
const button = getElementByTag(0, 'button');
|
|
134
|
+
dispatchSyntheticEvent(button, EventType.Press, {
|
|
135
|
+
nativeEvent: {
|
|
136
|
+
locationX: 0,
|
|
137
|
+
locationY: 0,
|
|
138
|
+
timestamp: Date.now(),
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
flushSync();
|
|
143
|
+
await flushMicrotasks();
|
|
144
|
+
|
|
145
|
+
expect(getTextContent()).toContain('Count 1');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('supports bind:value through the DOM shim input event translation', async () => {
|
|
149
|
+
mount(Input);
|
|
150
|
+
await flushMicrotasks();
|
|
151
|
+
|
|
152
|
+
expect(getTextContent()).toContain('hi');
|
|
153
|
+
|
|
154
|
+
const input = getElementByTag(0, 'input');
|
|
155
|
+
dispatchSyntheticEvent(input, EventType.Change, {
|
|
156
|
+
nativeEvent: {
|
|
157
|
+
text: 'updated',
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
flushSync();
|
|
162
|
+
await flushMicrotasks();
|
|
163
|
+
|
|
164
|
+
expect(getTextContent()).toContain('updated');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { asset } from '../../../exact-core/src/assets-fonts-state.js';
|
|
4
|
+
import {
|
|
5
|
+
coerceImageSourceToSvgSource,
|
|
6
|
+
filterSvgColors,
|
|
7
|
+
isInlineSvgSource,
|
|
8
|
+
resolveSvgSourceForFetch,
|
|
9
|
+
resolveSvgSourceForNative,
|
|
10
|
+
} from '../svg-source.js';
|
|
11
|
+
|
|
12
|
+
describe('svg source helpers', () => {
|
|
13
|
+
it('detects inline SVG markup', () => {
|
|
14
|
+
expect(isInlineSvgSource('<svg viewBox="0 0 24 24"></svg>')).toBe(true);
|
|
15
|
+
expect(isInlineSvgSource('<?xml version="1.0"?><svg></svg>')).toBe(true);
|
|
16
|
+
expect(isInlineSvgSource('https://example.com/icon.svg')).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('decodes data URL SVG markup for fetch-backed rendering', () => {
|
|
20
|
+
const resolved = resolveSvgSourceForFetch('data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%2F%3E');
|
|
21
|
+
expect(resolved).toEqual({
|
|
22
|
+
markup: '<svg viewBox="0 0 24 24"/>',
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('wraps inline SVG markup as a data URI for the native image pipeline', () => {
|
|
27
|
+
const native = resolveSvgSourceForNative('<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/></svg>');
|
|
28
|
+
expect(native).toBe(
|
|
29
|
+
'data:image/svg+xml,' +
|
|
30
|
+
encodeURIComponent('<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/></svg>'),
|
|
31
|
+
);
|
|
32
|
+
// Round-trips back to the original markup through the fetch resolver.
|
|
33
|
+
expect(resolveSvgSourceForFetch(native!)).toEqual({
|
|
34
|
+
markup: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/></svg>',
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('passes URL / asset / data SVG sources through to native unchanged', () => {
|
|
39
|
+
expect(resolveSvgSourceForNative('https://cdn.example.com/icon.svg')).toBe(
|
|
40
|
+
'https://cdn.example.com/icon.svg',
|
|
41
|
+
);
|
|
42
|
+
expect(resolveSvgSourceForNative({ uri: 'https://cdn.example.com/icon.svg' })).toBe(
|
|
43
|
+
'https://cdn.example.com/icon.svg',
|
|
44
|
+
);
|
|
45
|
+
expect(resolveSvgSourceForNative('data:image/svg+xml,%3Csvg%2F%3E')).toBe(
|
|
46
|
+
'data:image/svg+xml,%3Csvg%2F%3E',
|
|
47
|
+
);
|
|
48
|
+
expect(resolveSvgSourceForNative(undefined)).toBeUndefined();
|
|
49
|
+
expect(resolveSvgSourceForNative('')).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('filters color overrides to CSS custom properties', () => {
|
|
53
|
+
expect(filterSvgColors({
|
|
54
|
+
'--primary': '#007aff',
|
|
55
|
+
fill: '#111111',
|
|
56
|
+
})).toEqual({
|
|
57
|
+
'--primary': '#007aff',
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('coerces SVG-like image sources for auto-delegation', () => {
|
|
62
|
+
expect(coerceImageSourceToSvgSource('https://cdn.example.com/icon.svg')).toBe('https://cdn.example.com/icon.svg');
|
|
63
|
+
expect(coerceImageSourceToSvgSource(asset('./icons/logo.svg'))).toEqual(asset('./icons/logo.svg'));
|
|
64
|
+
expect(coerceImageSourceToSvgSource([
|
|
65
|
+
{ uri: 'https://cdn.example.com/hero.svg', type: 'image/svg+xml' },
|
|
66
|
+
{ uri: 'https://cdn.example.com/hero@2x.svg', type: 'image/svg+xml' },
|
|
67
|
+
])).toEqual({ uri: 'https://cdn.example.com/hero.svg', headers: undefined, cacheKey: undefined });
|
|
68
|
+
expect(coerceImageSourceToSvgSource({ thumbhash: 'abc123' })).toBeUndefined();
|
|
69
|
+
expect(coerceImageSourceToSvgSource('https://cdn.example.com/photo.png')).toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
});
|