electrobun 1.18.4-beta.18 → 1.18.4-beta.21
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 +9 -0
- package/bin/electrobun.cjs +165 -0
- package/dist/api/browser/ui/__tests__/dom.test.ts +473 -0
- package/dist/api/browser/ui/__tests__/domStub.ts +218 -0
- package/dist/api/browser/ui/dom.ts +490 -0
- package/dist/api/browser/ui/index.ts +44 -0
- package/dist/api/browser/ui/jsx-dev-runtime.ts +16 -0
- package/dist/api/browser/ui/jsx-runtime.ts +56 -0
- package/dist/api/config/ElectrobunConfig.ts +33 -0
- package/dist/api/preload/.generated/compiled.ts +1 -1
- package/dist/api/preload/index.ts +2 -0
- package/dist/api/preload/uiTag.ts +45 -0
- package/dist/api/sdks/main/__tests__/utils-quit-exit-code.test.ts +44 -0
- package/dist/api/sdks/main/core/GpuWindow.ts +19 -0
- package/dist/api/sdks/main/core/Utils.ts +52 -4
- package/dist/api/sdks/main/core/WGPUView.ts +9 -0
- package/dist/api/sdks/main/entries/ui.ts +1 -0
- package/dist/api/sdks/main/proc/native.ts +187 -0
- package/dist/api/sdks/main/ui/__tests__/font.test.ts +49 -0
- package/dist/api/sdks/main/ui/__tests__/hit.test.ts +64 -0
- package/dist/api/sdks/main/ui/__tests__/jsx.test.ts +252 -0
- package/dist/api/sdks/main/ui/__tests__/layout.test.ts +159 -0
- package/dist/api/sdks/main/ui/__tests__/paint.test.ts +115 -0
- package/dist/api/sdks/main/ui/__tests__/reactive.test.ts +456 -0
- package/dist/api/sdks/main/ui/__tests__/scroll-focus-input.test.ts +298 -0
- package/dist/api/sdks/main/ui/__tests__/tree.test.ts +96 -0
- package/dist/api/sdks/main/ui/__tests__/ui.test.ts +170 -0
- package/dist/api/sdks/main/ui/elements.ts +135 -0
- package/dist/api/sdks/main/ui/font.ts +168 -0
- package/dist/api/sdks/main/ui/hit.ts +46 -0
- package/dist/api/sdks/main/ui/index.ts +71 -0
- package/dist/api/sdks/main/ui/input.ts +268 -0
- package/dist/api/sdks/main/ui/jsx-dev-runtime.ts +16 -0
- package/dist/api/sdks/main/ui/jsx-runtime.ts +136 -0
- package/dist/api/sdks/main/ui/keymap.ts +147 -0
- package/dist/api/sdks/main/ui/layout.ts +178 -0
- package/dist/api/sdks/main/ui/paint.ts +196 -0
- package/dist/api/sdks/main/ui/reactive.ts +4 -0
- package/dist/api/sdks/main/ui/renderer.ts +278 -0
- package/dist/api/sdks/main/ui/text.ts +175 -0
- package/dist/api/sdks/main/ui/textInput.ts +121 -0
- package/dist/api/sdks/main/ui/tree.ts +276 -0
- package/dist/api/sdks/main/ui/ui.ts +457 -0
- package/dist/api/sdks/main/ui/uiTagHost.ts +56 -0
- package/dist/api/sdks/main/ui/uiwindow.ts +330 -0
- package/dist/api/shared/build-dependencies.test.ts +1 -1
- package/dist/api/shared/build-dependencies.ts +4 -4
- package/dist/api/shared/linux-webkit-automation.test.ts +1 -1
- package/dist/api/shared/warren/jsx.ts +279 -0
- package/dist/api/shared/warren/reactive.ts +638 -0
- package/dist/api/shared/windows-unicode-ui.test.ts +4 -4
- package/dist/preload-full.js +35 -0
- package/dist/zig-sdk/electrobun.zig +197 -162
- package/{dash.config.ts → hutch.config.ts} +4 -3
- package/package.json +14 -2
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
// Warren reactivity — the spec's test checklist.
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
|
+
import {
|
|
5
|
+
batch,
|
|
6
|
+
cleanup,
|
|
7
|
+
commit,
|
|
8
|
+
createRoot,
|
|
9
|
+
inert,
|
|
10
|
+
isLive,
|
|
11
|
+
live,
|
|
12
|
+
memo,
|
|
13
|
+
setDevMode,
|
|
14
|
+
signal,
|
|
15
|
+
store,
|
|
16
|
+
type LiveBinding,
|
|
17
|
+
} from "../reactive";
|
|
18
|
+
|
|
19
|
+
function effect(fn: () => void): void {
|
|
20
|
+
// An effect is a live whose return value nobody uses.
|
|
21
|
+
live(fn);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function withWarnings<T>(fn: () => T): { result: T; warnings: string[] } {
|
|
25
|
+
const warnings: string[] = [];
|
|
26
|
+
const original = console.warn;
|
|
27
|
+
console.warn = (msg: unknown) => warnings.push(String(msg));
|
|
28
|
+
try {
|
|
29
|
+
return { result: fn(), warnings };
|
|
30
|
+
} finally {
|
|
31
|
+
console.warn = original;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("tracking tiers", () => {
|
|
36
|
+
test("signal call inside live subscribes; outside any scope it does not", () => {
|
|
37
|
+
const [count, setCount] = signal(0);
|
|
38
|
+
const seen: number[] = [];
|
|
39
|
+
createRoot(() => {
|
|
40
|
+
effect(() => seen.push(count()));
|
|
41
|
+
});
|
|
42
|
+
expect(seen).toEqual([0]);
|
|
43
|
+
setCount(1);
|
|
44
|
+
expect(seen).toEqual([0, 1]);
|
|
45
|
+
|
|
46
|
+
const snapshot = count();
|
|
47
|
+
expect(snapshot).toBe(1);
|
|
48
|
+
setCount(2);
|
|
49
|
+
expect(seen).toEqual([0, 1, 2]); // only the live re-ran; no phantom subs
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("property read inside live subscribes; component-body read does not", () => {
|
|
53
|
+
const [state, setState] = store({ user: { name: "ada" } });
|
|
54
|
+
const seen: string[] = [];
|
|
55
|
+
let bodySnapshot = "";
|
|
56
|
+
createRoot(() => {
|
|
57
|
+
bodySnapshot = state.user.name; // inert: body is not a scope
|
|
58
|
+
effect(() => seen.push(state.user.name)); // scope: tracks
|
|
59
|
+
});
|
|
60
|
+
expect(bodySnapshot).toBe("ada");
|
|
61
|
+
expect(seen).toEqual(["ada"]);
|
|
62
|
+
setState((s) => (s.user.name = "grace"));
|
|
63
|
+
expect(seen).toEqual(["ada", "grace"]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("dynamic extent: helper property reads track inside live", () => {
|
|
67
|
+
const [state, setState] = store({ user: { name: "ada", plan: "pro" } });
|
|
68
|
+
const label = (u: { name: string; plan: string }) => `${u.name} · ${u.plan}`;
|
|
69
|
+
const seen: string[] = [];
|
|
70
|
+
createRoot(() => {
|
|
71
|
+
effect(() => seen.push(label(state.user)));
|
|
72
|
+
});
|
|
73
|
+
expect(seen).toEqual(["ada · pro"]);
|
|
74
|
+
setState((s) => (s.user.plan = "free"));
|
|
75
|
+
expect(seen).toEqual(["ada · pro", "ada · free"]);
|
|
76
|
+
setState((s) => (s.user.name = "grace"));
|
|
77
|
+
expect(seen).toEqual(["ada · pro", "ada · free", "grace · free"]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("inert inside a scope suppresses that read only", () => {
|
|
81
|
+
const [state, setState] = store({ user: { name: "ada", plan: "pro" } });
|
|
82
|
+
const seen: string[] = [];
|
|
83
|
+
createRoot(() => {
|
|
84
|
+
effect(() => {
|
|
85
|
+
const plan = inert(() => state.user.plan);
|
|
86
|
+
seen.push(`${state.user.name} · ${plan}`);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
expect(seen).toEqual(["ada · pro"]);
|
|
90
|
+
setState((s) => (s.user.plan = "free")); // not subscribed
|
|
91
|
+
expect(seen).toEqual(["ada · pro"]);
|
|
92
|
+
setState((s) => (s.user.name = "grace")); // subscribed
|
|
93
|
+
expect(seen).toEqual(["ada · pro", "grace · free"]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("inert outside a scope is a no-op", () => {
|
|
97
|
+
const [count] = signal(5);
|
|
98
|
+
expect(inert(count)).toBe(5);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("scopes", () => {
|
|
103
|
+
test("live is deferred: runs after commit, then re-runs on change", () => {
|
|
104
|
+
const [count, setCount] = signal(0);
|
|
105
|
+
const runs: number[] = [];
|
|
106
|
+
createRoot(() => {
|
|
107
|
+
effect(() => runs.push(count()));
|
|
108
|
+
expect(runs).toEqual([]); // not yet — commit hasn't exited
|
|
109
|
+
});
|
|
110
|
+
expect(runs).toEqual([0]);
|
|
111
|
+
setCount(1);
|
|
112
|
+
expect(runs).toEqual([0, 1]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("memo caches and recomputes on dependency change", () => {
|
|
116
|
+
const [count, setCount] = signal(2);
|
|
117
|
+
let computes = 0;
|
|
118
|
+
let doubled: () => number = () => 0;
|
|
119
|
+
createRoot(() => {
|
|
120
|
+
doubled = memo(() => {
|
|
121
|
+
computes++;
|
|
122
|
+
return count() * 2;
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
expect(doubled()).toBe(4);
|
|
126
|
+
expect(doubled()).toBe(4);
|
|
127
|
+
expect(computes).toBe(1);
|
|
128
|
+
setCount(3);
|
|
129
|
+
expect(doubled()).toBe(6);
|
|
130
|
+
expect(computes).toBe(2);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("a live reading a memo never observes a stale value (no glitch)", () => {
|
|
134
|
+
const [count, setCount] = signal(1);
|
|
135
|
+
const observed: Array<[number, number]> = [];
|
|
136
|
+
createRoot(() => {
|
|
137
|
+
const doubled = memo(() => count() * 2);
|
|
138
|
+
effect(() => observed.push([count(), doubled()]));
|
|
139
|
+
});
|
|
140
|
+
setCount(2);
|
|
141
|
+
setCount(3);
|
|
142
|
+
for (const [c, d] of observed) {
|
|
143
|
+
expect(d).toBe(c * 2); // consistent at every observation
|
|
144
|
+
}
|
|
145
|
+
expect(observed.length).toBe(3);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("memo equals cut stops propagation: dependents skip equal values", () => {
|
|
149
|
+
const [items, setItems] = signal<string[]>(["a", "b"]);
|
|
150
|
+
let runs = 0;
|
|
151
|
+
createRoot(() => {
|
|
152
|
+
const empty = memo(() => items().length === 0);
|
|
153
|
+
effect(() => {
|
|
154
|
+
empty();
|
|
155
|
+
runs++;
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
expect(runs).toBe(1);
|
|
159
|
+
setItems(["b", "a"]); // memo recomputes, value unchanged -> no re-run
|
|
160
|
+
expect(runs).toBe(1);
|
|
161
|
+
setItems([]); // value flips -> dependent re-runs
|
|
162
|
+
expect(runs).toBe(2);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("cleanup runs before every re-run, not only at disposal", () => {
|
|
166
|
+
const [count, setCount] = signal(0);
|
|
167
|
+
const events: string[] = [];
|
|
168
|
+
createRoot(() => {
|
|
169
|
+
effect(() => {
|
|
170
|
+
const value = count();
|
|
171
|
+
cleanup(() => events.push(`cleanup ${value}`));
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
expect(events).toEqual([]);
|
|
175
|
+
setCount(1);
|
|
176
|
+
expect(events).toEqual(["cleanup 0"]);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("nested scopes dispose with their parent", () => {
|
|
180
|
+
const [outer, setOuter] = signal(0);
|
|
181
|
+
const [inner, setInner] = signal(0);
|
|
182
|
+
let innerRuns = 0;
|
|
183
|
+
createRoot(() => {
|
|
184
|
+
effect(() => {
|
|
185
|
+
outer();
|
|
186
|
+
effect(() => {
|
|
187
|
+
inner();
|
|
188
|
+
innerRuns++;
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
expect(innerRuns).toBe(1);
|
|
193
|
+
setOuter(1); // outer re-runs; previous inner scope disposed
|
|
194
|
+
expect(innerRuns).toBe(2);
|
|
195
|
+
setInner(1); // only the current inner scope re-runs
|
|
196
|
+
expect(innerRuns).toBe(3);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("body-level lives dispose with the root", () => {
|
|
200
|
+
const [count, setCount] = signal(0);
|
|
201
|
+
let runs = 0;
|
|
202
|
+
createRoot((dispose) => {
|
|
203
|
+
effect(() => {
|
|
204
|
+
count();
|
|
205
|
+
runs++;
|
|
206
|
+
});
|
|
207
|
+
dispose();
|
|
208
|
+
});
|
|
209
|
+
setCount(1);
|
|
210
|
+
expect(runs).toBe(0); // disposed before its first (deferred) run
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("state", () => {
|
|
215
|
+
test("store proxy throws on write (in production too)", () => {
|
|
216
|
+
const [state] = store({ value: 1 });
|
|
217
|
+
expect(() => {
|
|
218
|
+
(state as any).value = 2;
|
|
219
|
+
}).toThrow(/read-only/);
|
|
220
|
+
expect(() => {
|
|
221
|
+
delete (state as any).value;
|
|
222
|
+
}).toThrow(/read-only/);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("the setter's draft is writable and mutates directly", () => {
|
|
226
|
+
const [state, setState] = store({
|
|
227
|
+
user: { name: "ada" },
|
|
228
|
+
tags: [] as string[],
|
|
229
|
+
});
|
|
230
|
+
setState((s) => {
|
|
231
|
+
s.user.name = "grace";
|
|
232
|
+
s.tags.push("admin");
|
|
233
|
+
});
|
|
234
|
+
expect(state.user.name).toBe("grace");
|
|
235
|
+
expect(state.tags.length).toBe(1);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("a setter call is one propagation for its store", () => {
|
|
239
|
+
const [state, setState] = store({ a: 1, b: 2 });
|
|
240
|
+
let runs = 0;
|
|
241
|
+
createRoot(() => {
|
|
242
|
+
effect(() => {
|
|
243
|
+
state.a;
|
|
244
|
+
state.b;
|
|
245
|
+
runs++;
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
expect(runs).toBe(1);
|
|
249
|
+
setState((s) => {
|
|
250
|
+
s.a = 10;
|
|
251
|
+
s.b = 20;
|
|
252
|
+
});
|
|
253
|
+
expect(runs).toBe(2); // one propagation, not two
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("signal tuple get/set with functional updates", () => {
|
|
257
|
+
const [count, setCount] = signal(10);
|
|
258
|
+
setCount((c) => c + 5);
|
|
259
|
+
expect(count()).toBe(15);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe("batch", () => {
|
|
264
|
+
test("nests and collapses to a single flush", () => {
|
|
265
|
+
const [a, setA] = signal(0);
|
|
266
|
+
const [b, setB] = signal(0);
|
|
267
|
+
let runs = 0;
|
|
268
|
+
createRoot(() => {
|
|
269
|
+
effect(() => {
|
|
270
|
+
a();
|
|
271
|
+
b();
|
|
272
|
+
runs++;
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
expect(runs).toBe(1);
|
|
276
|
+
batch(() => {
|
|
277
|
+
setA(1);
|
|
278
|
+
batch(() => {
|
|
279
|
+
setB(1);
|
|
280
|
+
});
|
|
281
|
+
setA(2);
|
|
282
|
+
});
|
|
283
|
+
expect(runs).toBe(2);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("reads inside a batch see pending writes", () => {
|
|
287
|
+
const [total, setTotal] = signal(0);
|
|
288
|
+
batch(() => {
|
|
289
|
+
for (const price of [1, 2, 3]) {
|
|
290
|
+
setTotal((t) => t + price);
|
|
291
|
+
}
|
|
292
|
+
expect(total()).toBe(6);
|
|
293
|
+
});
|
|
294
|
+
expect(total()).toBe(6);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("throw inside a batch: exception propagates, depth resets, queue flushes", () => {
|
|
298
|
+
const [count, setCount] = signal(0);
|
|
299
|
+
let runs = 0;
|
|
300
|
+
createRoot(() => {
|
|
301
|
+
effect(() => {
|
|
302
|
+
count();
|
|
303
|
+
runs++;
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
expect(runs).toBe(1);
|
|
307
|
+
expect(() =>
|
|
308
|
+
batch(() => {
|
|
309
|
+
setCount(1);
|
|
310
|
+
throw new Error("boom");
|
|
311
|
+
}),
|
|
312
|
+
).toThrow("boom");
|
|
313
|
+
// The mutation landed; the queued notification flushed on the way out.
|
|
314
|
+
expect(runs).toBe(2);
|
|
315
|
+
// Depth reset: later writes propagate normally.
|
|
316
|
+
setCount(2);
|
|
317
|
+
expect(runs).toBe(3);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("throw inside live/inert restores tracking (no phantom subscriptions)", () => {
|
|
321
|
+
const [count, setCount] = signal(0);
|
|
322
|
+
const [other, setOther] = signal(0);
|
|
323
|
+
let runs = 0;
|
|
324
|
+
createRoot(() => {
|
|
325
|
+
effect(() => {
|
|
326
|
+
count();
|
|
327
|
+
runs++;
|
|
328
|
+
if (count() === 1) throw new Error("live boom");
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
expect(runs).toBe(1);
|
|
332
|
+
expect(() => setCount(1)).toThrow("live boom");
|
|
333
|
+
// Tracking stack restored: reads out here must not subscribe.
|
|
334
|
+
other();
|
|
335
|
+
setOther(1);
|
|
336
|
+
expect(runs).toBe(2);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
describe("hard errors", () => {
|
|
341
|
+
test("cleanup() with no argument throws", () => {
|
|
342
|
+
const seen: string[] = [];
|
|
343
|
+
createRoot(() => {
|
|
344
|
+
effect(() => {
|
|
345
|
+
try {
|
|
346
|
+
(cleanup as any)();
|
|
347
|
+
} catch (e) {
|
|
348
|
+
seen.push(String(e));
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
expect(seen.length).toBe(1);
|
|
353
|
+
expect(seen[0]).toContain("requires a function");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("cleanup() outside a scope throws", () => {
|
|
357
|
+
expect(() => cleanup(() => {})).toThrow(/outside a scope/);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("cleanup() inside a memo throws", () => {
|
|
361
|
+
createRoot(() => {
|
|
362
|
+
const bad = memo(() => {
|
|
363
|
+
cleanup(() => {});
|
|
364
|
+
return 1;
|
|
365
|
+
});
|
|
366
|
+
expect(() => bad()).toThrow(/inside a memo/);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("live() outside JSX and outside any scope throws", () => {
|
|
371
|
+
expect(() => live(() => 1)).toThrow(/outside/);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
describe("dev warnings", () => {
|
|
376
|
+
test("signal read during render with no scope warns", () => {
|
|
377
|
+
setDevMode(true);
|
|
378
|
+
const [count] = signal(0);
|
|
379
|
+
const { warnings } = withWarnings(() => {
|
|
380
|
+
createRoot(() => {
|
|
381
|
+
count(); // unwrapped read during a commit
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
expect(warnings.some((w) => w.includes("no scope"))).toBe(true);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("live() with zero dependencies warns", () => {
|
|
388
|
+
setDevMode(true);
|
|
389
|
+
const { warnings } = withWarnings(() => {
|
|
390
|
+
createRoot(() => {
|
|
391
|
+
live(() => 42); // static expression, defensive over-wrap
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
expect(warnings.some((w) => w.includes("zero dependencies"))).toBe(true);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("nested live() warns and stays transparent", () => {
|
|
398
|
+
setDevMode(true);
|
|
399
|
+
const [state, setState] = store({ user: { tier: "gold" } });
|
|
400
|
+
const seen: string[] = [];
|
|
401
|
+
const { warnings } = withWarnings(() => {
|
|
402
|
+
createRoot(() => {
|
|
403
|
+
live(() => {
|
|
404
|
+
const tier = live(() => state.user.tier);
|
|
405
|
+
seen.push(String(tier));
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
setState((s) => (s.user.tier = "silver"));
|
|
409
|
+
});
|
|
410
|
+
expect(warnings.some((w) => w.includes("nested live"))).toBe(true);
|
|
411
|
+
expect(seen).toEqual(["gold", "silver"]);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("warnings are silent when dev mode is off", () => {
|
|
415
|
+
setDevMode(false);
|
|
416
|
+
const [count] = signal(0);
|
|
417
|
+
const { warnings } = withWarnings(() => {
|
|
418
|
+
createRoot(() => {
|
|
419
|
+
count();
|
|
420
|
+
live(() => 42);
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
expect(warnings.length).toBe(0);
|
|
424
|
+
setDevMode(true);
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe("live bindings", () => {
|
|
429
|
+
test("statement live is an effect; value live is a claimable binding", () => {
|
|
430
|
+
const [count] = signal(1);
|
|
431
|
+
const applied: number[] = [];
|
|
432
|
+
createRoot(() => {
|
|
433
|
+
const binding = live(() => count() * 10);
|
|
434
|
+
expect(isLive(binding)).toBe(true);
|
|
435
|
+
(binding as LiveBinding<number>).claimed = true;
|
|
436
|
+
applied.push((binding as LiveBinding<number>).fn());
|
|
437
|
+
});
|
|
438
|
+
expect(applied).toEqual([10]);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test("commit boundaries defer lives created inside", () => {
|
|
442
|
+
const runs: number[] = [];
|
|
443
|
+
const [count] = signal(7);
|
|
444
|
+
createRoot(() => {
|
|
445
|
+
commit(() => {
|
|
446
|
+
live(() => {
|
|
447
|
+
runs.push(count());
|
|
448
|
+
});
|
|
449
|
+
expect(runs).toEqual([]);
|
|
450
|
+
});
|
|
451
|
+
// Inner commit exited but the root's commit is still open.
|
|
452
|
+
expect(runs).toEqual([]);
|
|
453
|
+
});
|
|
454
|
+
expect(runs).toEqual([7]);
|
|
455
|
+
});
|
|
456
|
+
});
|