@luna_ui/luna 0.11.0 → 0.20.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/dist/api-DAWeanTX.js +1 -0
- package/dist/api-qXll116-.d.ts +80 -0
- package/dist/cli.mjs +27 -22
- package/dist/css/index.js +1 -0
- package/dist/event-utils.d.ts +1 -1
- package/dist/event-utils.js +1 -1
- package/dist/{index-BZoM-af5.d.ts → index-VY8G32hr.d.ts} +16 -76
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1 -1
- package/dist/jsx-dev-runtime.js +1 -1
- package/dist/jsx-runtime.d.ts +1 -1
- package/dist/jsx-runtime.js +1 -1
- package/dist/raw.d.ts +2 -0
- package/dist/raw.js +1 -0
- package/dist/resource.d.ts +41 -0
- package/dist/resource.js +1 -0
- package/dist/router-lite.d.ts +44 -0
- package/dist/router-lite.js +1 -0
- package/dist/signals-shared.d.ts +12 -0
- package/dist/signals-shared.js +1 -0
- package/dist/signals.d.ts +2 -3
- package/dist/signals.js +1 -1
- package/dist/vite-plugin.d.ts +7708 -2
- package/dist/vite-plugin.js +7 -6
- package/package.json +30 -11
- package/dist/event-utils-9cHYnvun.js +0 -1
- package/dist/src-BFWjzzPo.js +0 -1
- package/src/css/extract.ts +0 -798
- package/src/css/index.ts +0 -10
- package/src/css/inject.ts +0 -205
- package/src/css/inline.ts +0 -182
- package/src/css/minify.ts +0 -70
- package/src/css/optimizer.ts +0 -6
- package/src/css/runtime.ts +0 -344
- package/src/css-optimizer/README.md +0 -353
- package/src/css-optimizer/cooccurrence.ts +0 -100
- package/src/css-optimizer/core.ts +0 -263
- package/src/css-optimizer/extractors.ts +0 -243
- package/src/css-optimizer/hash.ts +0 -54
- package/src/css-optimizer/index.ts +0 -129
- package/src/css-optimizer/merge.ts +0 -109
- package/src/css-optimizer/moonbit-analyzer.ts +0 -210
- package/src/css-optimizer/parser.ts +0 -120
- package/src/css-optimizer/pattern.ts +0 -171
- package/src/css-optimizer/transformers.ts +0 -301
- package/src/css-optimizer/types.ts +0 -128
- package/src/event-utils.ts +0 -227
- package/src/hydration/createHydrator.ts +0 -62
- package/src/hydration/delegate.ts +0 -62
- package/src/hydration/drag.ts +0 -214
- package/src/hydration/index.ts +0 -12
- package/src/hydration/keyboard.ts +0 -64
- package/src/hydration/toggle.ts +0 -101
- package/src/index.ts +0 -908
- package/src/jsx-dev-runtime.ts +0 -2
- package/src/jsx-runtime.ts +0 -398
- package/src/signals.ts +0 -113
- package/src/vite-plugin.ts +0 -718
- package/tests/__screenshots__/apg.test.ts/APG-Components---Accessibility-Tests-Button-Pattern-disabled-button-has-aria-disabled-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API--SolidJS-style--createResource-error-is-undefined-when-pending-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API--SolidJS-style--createResource-transitions-to-success-on-resolve-1.png +0 -0
- package/tests/apg.test.ts +0 -466
- package/tests/context.test.ts +0 -118
- package/tests/css-optimizer-extractors.test.ts +0 -264
- package/tests/css-optimizer-integration.test.ts +0 -566
- package/tests/css-optimizer-transformers.test.ts +0 -301
- package/tests/css-optimizer.test.ts +0 -646
- package/tests/css-runtime.bench.ts +0 -442
- package/tests/css-runtime.test.ts +0 -342
- package/tests/debounced.test.ts +0 -165
- package/tests/dom.test.ts +0 -873
- package/tests/integration.test.ts +0 -405
- package/tests/issue-11-show-null-to-truthy.test.ts +0 -176
- package/tests/issue-5-for-infinite-loop.test.ts +0 -516
- package/tests/jsx-runtime.test.tsx +0 -393
- package/tests/lifecycle.test.ts +0 -833
- package/tests/move-before.bench.ts +0 -304
- package/tests/preact-signals-comparison.test.ts +0 -1608
- package/tests/resource.test.ts +0 -170
- package/tests/router.test.ts +0 -117
- package/tests/show-initial-mount-leak.test.tsx +0 -182
- package/tests/solidjs-api.test.ts +0 -660
- package/tests/static-perf.bench.ts +0 -64
- package/tests/store.test.ts +0 -263
- package/tests/tsx-syntax.test.tsx +0 -404
- /package/dist/{event-utils-BkTM7rk5.d.ts → event-utils-BvAf0NwN.d.ts} +0 -0
|
@@ -1,1608 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Comparison tests between Luna and Preact Signals
|
|
3
|
-
*
|
|
4
|
-
* Verifies that Luna produces equivalent DOM output to Preact (excluding internal markers).
|
|
5
|
-
* Tests focus on:
|
|
6
|
-
* - Basic signal behavior
|
|
7
|
-
* - Deep nested lists
|
|
8
|
-
* - Complex fragments
|
|
9
|
-
* - Conditional rendering
|
|
10
|
-
*
|
|
11
|
-
* Known differences:
|
|
12
|
-
* - Luna's Fragment wraps multiple children in a <span> element
|
|
13
|
-
* - Luna's show/Show uses placeholder comments and effects for conditional rendering
|
|
14
|
-
*/
|
|
15
|
-
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
16
|
-
|
|
17
|
-
// Luna imports
|
|
18
|
-
import {
|
|
19
|
-
createElement,
|
|
20
|
-
render as lunaRender,
|
|
21
|
-
text,
|
|
22
|
-
textDyn,
|
|
23
|
-
createSignal,
|
|
24
|
-
createRenderEffect,
|
|
25
|
-
createMemo,
|
|
26
|
-
batch,
|
|
27
|
-
For,
|
|
28
|
-
Show,
|
|
29
|
-
Fragment,
|
|
30
|
-
show,
|
|
31
|
-
onCleanup,
|
|
32
|
-
untrack,
|
|
33
|
-
peek,
|
|
34
|
-
} from "../src/index";
|
|
35
|
-
|
|
36
|
-
// Preact imports
|
|
37
|
-
import { h, render as preactRender, Fragment as PreactFragment, type VNode } from "preact";
|
|
38
|
-
import { signal, computed, effect, batch as preactBatch } from "@preact/signals-core";
|
|
39
|
-
|
|
40
|
-
// =============================================================================
|
|
41
|
-
// Utilities
|
|
42
|
-
// =============================================================================
|
|
43
|
-
|
|
44
|
-
// MoonBit AttrValue constructors
|
|
45
|
-
const AttrValue = {
|
|
46
|
-
Static: (value: string) => ({ $tag: 0, _0: value }),
|
|
47
|
-
Dynamic: (getter: () => string) => ({ $tag: 1, _0: getter }),
|
|
48
|
-
Handler: (handler: (e: unknown) => void) => ({ $tag: 2, _0: handler }),
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
function attr(name: string, value: unknown) {
|
|
52
|
-
return { _0: name, _1: value };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Normalize HTML by removing internal markers (comment nodes, empty text nodes)
|
|
57
|
-
* and normalizing whitespace
|
|
58
|
-
*/
|
|
59
|
-
function normalizeHtml(html: string): string {
|
|
60
|
-
return html
|
|
61
|
-
// Remove HTML comments (Luna uses these as markers)
|
|
62
|
-
.replace(/<!--[\s\S]*?-->/g, "")
|
|
63
|
-
// Remove empty text nodes and normalize whitespace
|
|
64
|
-
.replace(/>\s+</g, "><")
|
|
65
|
-
.trim();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Get normalized innerHTML from a container
|
|
70
|
-
*/
|
|
71
|
-
function getNormalizedContent(container: HTMLElement): string {
|
|
72
|
-
return normalizeHtml(container.innerHTML);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Compare DOM structure (tag names, attributes, text content)
|
|
77
|
-
* ignoring comment nodes and whitespace
|
|
78
|
-
*/
|
|
79
|
-
function getVisibleNodes(element: Element): string[] {
|
|
80
|
-
const result: string[] = [];
|
|
81
|
-
|
|
82
|
-
function walk(node: Node) {
|
|
83
|
-
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
84
|
-
const el = node as Element;
|
|
85
|
-
result.push(`<${el.tagName.toLowerCase()}>`);
|
|
86
|
-
for (const child of el.childNodes) {
|
|
87
|
-
walk(child);
|
|
88
|
-
}
|
|
89
|
-
result.push(`</${el.tagName.toLowerCase()}>`);
|
|
90
|
-
} else if (node.nodeType === Node.TEXT_NODE) {
|
|
91
|
-
const text = node.textContent?.trim();
|
|
92
|
-
if (text) {
|
|
93
|
-
result.push(text);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
// Skip comment nodes
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
for (const child of element.childNodes) {
|
|
100
|
-
walk(child);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return result;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// =============================================================================
|
|
107
|
-
// Tests
|
|
108
|
-
// =============================================================================
|
|
109
|
-
|
|
110
|
-
describe("Signal Behavior Comparison", () => {
|
|
111
|
-
test("basic signal get/set produces same values", () => {
|
|
112
|
-
// Luna
|
|
113
|
-
const [lunaCount, setLunaCount] = createSignal(0);
|
|
114
|
-
const lunaValues: number[] = [];
|
|
115
|
-
createRenderEffect(() => {
|
|
116
|
-
lunaValues.push(lunaCount());
|
|
117
|
-
});
|
|
118
|
-
setLunaCount(1);
|
|
119
|
-
setLunaCount(2);
|
|
120
|
-
|
|
121
|
-
// Preact Signals
|
|
122
|
-
const preactCount = signal(0);
|
|
123
|
-
const preactValues: number[] = [];
|
|
124
|
-
const dispose = effect(() => {
|
|
125
|
-
preactValues.push(preactCount.value);
|
|
126
|
-
});
|
|
127
|
-
preactCount.value = 1;
|
|
128
|
-
preactCount.value = 2;
|
|
129
|
-
dispose();
|
|
130
|
-
|
|
131
|
-
expect(lunaValues).toEqual(preactValues);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test("computed/memo produces same values", () => {
|
|
135
|
-
// Luna
|
|
136
|
-
const [lunaA, setLunaA] = createSignal(2);
|
|
137
|
-
const [lunaB] = createSignal(3);
|
|
138
|
-
const lunaSum = createMemo(() => lunaA() + lunaB());
|
|
139
|
-
|
|
140
|
-
expect(lunaSum()).toBe(5);
|
|
141
|
-
setLunaA(10);
|
|
142
|
-
expect(lunaSum()).toBe(13);
|
|
143
|
-
|
|
144
|
-
// Preact Signals
|
|
145
|
-
const preactA = signal(2);
|
|
146
|
-
const preactB = signal(3);
|
|
147
|
-
const preactSum = computed(() => preactA.value + preactB.value);
|
|
148
|
-
|
|
149
|
-
expect(preactSum.value).toBe(5);
|
|
150
|
-
preactA.value = 10;
|
|
151
|
-
expect(preactSum.value).toBe(13);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("batch updates produce same final values", () => {
|
|
155
|
-
// Luna
|
|
156
|
-
const [lunaA, setLunaA] = createSignal(0);
|
|
157
|
-
const [lunaB, setLunaB] = createSignal(0);
|
|
158
|
-
const lunaUpdates: number[] = [];
|
|
159
|
-
|
|
160
|
-
createRenderEffect(() => {
|
|
161
|
-
lunaUpdates.push(lunaA() + lunaB());
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
batch(() => {
|
|
165
|
-
setLunaA(1);
|
|
166
|
-
setLunaB(2);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// Preact Signals
|
|
170
|
-
const preactA = signal(0);
|
|
171
|
-
const preactB = signal(0);
|
|
172
|
-
const preactUpdates: number[] = [];
|
|
173
|
-
|
|
174
|
-
const dispose = effect(() => {
|
|
175
|
-
preactUpdates.push(preactA.value + preactB.value);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
preactBatch(() => {
|
|
179
|
-
preactA.value = 1;
|
|
180
|
-
preactB.value = 2;
|
|
181
|
-
});
|
|
182
|
-
dispose();
|
|
183
|
-
|
|
184
|
-
// Both should have initial 0 and final 3 (batch should combine updates)
|
|
185
|
-
expect(lunaUpdates[0]).toBe(0);
|
|
186
|
-
expect(lunaUpdates[lunaUpdates.length - 1]).toBe(3);
|
|
187
|
-
expect(preactUpdates[0]).toBe(0);
|
|
188
|
-
expect(preactUpdates[preactUpdates.length - 1]).toBe(3);
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
describe("DOM Rendering Comparison - Simple Elements", () => {
|
|
193
|
-
let lunaContainer: HTMLDivElement;
|
|
194
|
-
let preactContainer: HTMLDivElement;
|
|
195
|
-
|
|
196
|
-
beforeEach(() => {
|
|
197
|
-
lunaContainer = document.createElement("div");
|
|
198
|
-
preactContainer = document.createElement("div");
|
|
199
|
-
document.body.appendChild(lunaContainer);
|
|
200
|
-
document.body.appendChild(preactContainer);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
afterEach(() => {
|
|
204
|
-
lunaContainer.remove();
|
|
205
|
-
preactContainer.remove();
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
test("simple div with text", () => {
|
|
209
|
-
// Luna
|
|
210
|
-
const lunaNode = createElement("div", [], [text("Hello")]);
|
|
211
|
-
lunaRender(lunaContainer, lunaNode);
|
|
212
|
-
|
|
213
|
-
// Preact
|
|
214
|
-
preactRender(h("div", null, "Hello"), preactContainer);
|
|
215
|
-
|
|
216
|
-
expect(getNormalizedContent(lunaContainer)).toBe(
|
|
217
|
-
getNormalizedContent(preactContainer)
|
|
218
|
-
);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
test("nested elements", () => {
|
|
222
|
-
// Luna
|
|
223
|
-
const lunaNode = createElement("div", [], [
|
|
224
|
-
createElement("span", [], [text("A")]),
|
|
225
|
-
createElement("span", [], [text("B")]),
|
|
226
|
-
]);
|
|
227
|
-
lunaRender(lunaContainer, lunaNode);
|
|
228
|
-
|
|
229
|
-
// Preact
|
|
230
|
-
preactRender(
|
|
231
|
-
h("div", null, h("span", null, "A"), h("span", null, "B")),
|
|
232
|
-
preactContainer
|
|
233
|
-
);
|
|
234
|
-
|
|
235
|
-
expect(getNormalizedContent(lunaContainer)).toBe(
|
|
236
|
-
getNormalizedContent(preactContainer)
|
|
237
|
-
);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
test("element with attributes", () => {
|
|
241
|
-
// Luna
|
|
242
|
-
const lunaNode = createElement(
|
|
243
|
-
"div",
|
|
244
|
-
[
|
|
245
|
-
attr("id", AttrValue.Static("test-id")),
|
|
246
|
-
attr("className", AttrValue.Static("test-class")),
|
|
247
|
-
],
|
|
248
|
-
[text("Content")]
|
|
249
|
-
);
|
|
250
|
-
lunaRender(lunaContainer, lunaNode);
|
|
251
|
-
|
|
252
|
-
// Preact
|
|
253
|
-
preactRender(
|
|
254
|
-
h("div", { id: "test-id", className: "test-class" }, "Content"),
|
|
255
|
-
preactContainer
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
expect(getNormalizedContent(lunaContainer)).toBe(
|
|
259
|
-
getNormalizedContent(preactContainer)
|
|
260
|
-
);
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
describe("DOM Rendering Comparison - Lists", () => {
|
|
265
|
-
let lunaContainer: HTMLDivElement;
|
|
266
|
-
let preactContainer: HTMLDivElement;
|
|
267
|
-
|
|
268
|
-
beforeEach(() => {
|
|
269
|
-
lunaContainer = document.createElement("div");
|
|
270
|
-
preactContainer = document.createElement("div");
|
|
271
|
-
document.body.appendChild(lunaContainer);
|
|
272
|
-
document.body.appendChild(preactContainer);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
afterEach(() => {
|
|
276
|
-
lunaContainer.remove();
|
|
277
|
-
preactContainer.remove();
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
test("simple list", () => {
|
|
281
|
-
const items = ["a", "b", "c"];
|
|
282
|
-
|
|
283
|
-
// Luna
|
|
284
|
-
const [lunaItems] = createSignal(items);
|
|
285
|
-
const lunaNode = createElement("ul", [], [
|
|
286
|
-
For({
|
|
287
|
-
each: lunaItems,
|
|
288
|
-
children: (item: string) => createElement("li", [], [text(item)]),
|
|
289
|
-
}),
|
|
290
|
-
]);
|
|
291
|
-
lunaRender(lunaContainer, lunaNode);
|
|
292
|
-
|
|
293
|
-
// Preact
|
|
294
|
-
preactRender(
|
|
295
|
-
h(
|
|
296
|
-
"ul",
|
|
297
|
-
null,
|
|
298
|
-
items.map((item, i) => h("li", { key: i }, item))
|
|
299
|
-
),
|
|
300
|
-
preactContainer
|
|
301
|
-
);
|
|
302
|
-
|
|
303
|
-
expect(getVisibleNodes(lunaContainer)).toEqual(
|
|
304
|
-
getVisibleNodes(preactContainer)
|
|
305
|
-
);
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
test("nested list (2 levels)", () => {
|
|
309
|
-
const data = [
|
|
310
|
-
{ name: "Group A", items: ["a1", "a2"] },
|
|
311
|
-
{ name: "Group B", items: ["b1", "b2", "b3"] },
|
|
312
|
-
];
|
|
313
|
-
|
|
314
|
-
// Luna
|
|
315
|
-
const [lunaData] = createSignal(data);
|
|
316
|
-
const lunaNode = createElement("div", [], [
|
|
317
|
-
For({
|
|
318
|
-
each: lunaData,
|
|
319
|
-
children: (group: (typeof data)[0]) =>
|
|
320
|
-
createElement("div", [attr("className", AttrValue.Static("group"))], [
|
|
321
|
-
createElement("h3", [], [text(group.name)]),
|
|
322
|
-
createElement("ul", [], [
|
|
323
|
-
For({
|
|
324
|
-
each: () => group.items,
|
|
325
|
-
children: (item: string) =>
|
|
326
|
-
createElement("li", [], [text(item)]),
|
|
327
|
-
}),
|
|
328
|
-
]),
|
|
329
|
-
]),
|
|
330
|
-
}),
|
|
331
|
-
]);
|
|
332
|
-
lunaRender(lunaContainer, lunaNode);
|
|
333
|
-
|
|
334
|
-
// Preact
|
|
335
|
-
preactRender(
|
|
336
|
-
h(
|
|
337
|
-
"div",
|
|
338
|
-
null,
|
|
339
|
-
data.map((group, gi) =>
|
|
340
|
-
h(
|
|
341
|
-
"div",
|
|
342
|
-
{ key: gi, className: "group" },
|
|
343
|
-
h("h3", null, group.name),
|
|
344
|
-
h(
|
|
345
|
-
"ul",
|
|
346
|
-
null,
|
|
347
|
-
group.items.map((item, ii) => h("li", { key: ii }, item))
|
|
348
|
-
)
|
|
349
|
-
)
|
|
350
|
-
)
|
|
351
|
-
),
|
|
352
|
-
preactContainer
|
|
353
|
-
);
|
|
354
|
-
|
|
355
|
-
expect(getVisibleNodes(lunaContainer)).toEqual(
|
|
356
|
-
getVisibleNodes(preactContainer)
|
|
357
|
-
);
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
test("deeply nested list (3 levels)", () => {
|
|
361
|
-
const data = [
|
|
362
|
-
{
|
|
363
|
-
name: "Level 1-A",
|
|
364
|
-
children: [
|
|
365
|
-
{ name: "Level 2-A", items: ["item1", "item2"] },
|
|
366
|
-
{ name: "Level 2-B", items: ["item3"] },
|
|
367
|
-
],
|
|
368
|
-
},
|
|
369
|
-
{
|
|
370
|
-
name: "Level 1-B",
|
|
371
|
-
children: [{ name: "Level 2-C", items: ["item4", "item5", "item6"] }],
|
|
372
|
-
},
|
|
373
|
-
];
|
|
374
|
-
|
|
375
|
-
// Luna
|
|
376
|
-
const [lunaData] = createSignal(data);
|
|
377
|
-
const lunaNode = createElement("div", [], [
|
|
378
|
-
For({
|
|
379
|
-
each: lunaData,
|
|
380
|
-
children: (level1: (typeof data)[0]) =>
|
|
381
|
-
createElement("section", [], [
|
|
382
|
-
createElement("h2", [], [text(level1.name)]),
|
|
383
|
-
For({
|
|
384
|
-
each: () => level1.children,
|
|
385
|
-
children: (level2: (typeof data)[0]["children"][0]) =>
|
|
386
|
-
createElement("div", [], [
|
|
387
|
-
createElement("h3", [], [text(level2.name)]),
|
|
388
|
-
createElement("ul", [], [
|
|
389
|
-
For({
|
|
390
|
-
each: () => level2.items,
|
|
391
|
-
children: (item: string) =>
|
|
392
|
-
createElement("li", [], [text(item)]),
|
|
393
|
-
}),
|
|
394
|
-
]),
|
|
395
|
-
]),
|
|
396
|
-
}),
|
|
397
|
-
]),
|
|
398
|
-
}),
|
|
399
|
-
]);
|
|
400
|
-
lunaRender(lunaContainer, lunaNode);
|
|
401
|
-
|
|
402
|
-
// Preact
|
|
403
|
-
preactRender(
|
|
404
|
-
h(
|
|
405
|
-
"div",
|
|
406
|
-
null,
|
|
407
|
-
data.map((level1, i) =>
|
|
408
|
-
h(
|
|
409
|
-
"section",
|
|
410
|
-
{ key: i },
|
|
411
|
-
h("h2", null, level1.name),
|
|
412
|
-
level1.children.map((level2, j) =>
|
|
413
|
-
h(
|
|
414
|
-
"div",
|
|
415
|
-
{ key: j },
|
|
416
|
-
h("h3", null, level2.name),
|
|
417
|
-
h(
|
|
418
|
-
"ul",
|
|
419
|
-
null,
|
|
420
|
-
level2.items.map((item, k) => h("li", { key: k }, item))
|
|
421
|
-
)
|
|
422
|
-
)
|
|
423
|
-
)
|
|
424
|
-
)
|
|
425
|
-
)
|
|
426
|
-
),
|
|
427
|
-
preactContainer
|
|
428
|
-
);
|
|
429
|
-
|
|
430
|
-
expect(getVisibleNodes(lunaContainer)).toEqual(
|
|
431
|
-
getVisibleNodes(preactContainer)
|
|
432
|
-
);
|
|
433
|
-
});
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
describe("Fragment Comparison with Preact", () => {
|
|
437
|
-
let lunaContainer: HTMLDivElement;
|
|
438
|
-
let preactContainer: HTMLDivElement;
|
|
439
|
-
|
|
440
|
-
beforeEach(() => {
|
|
441
|
-
lunaContainer = document.createElement("div");
|
|
442
|
-
preactContainer = document.createElement("div");
|
|
443
|
-
document.body.appendChild(lunaContainer);
|
|
444
|
-
document.body.appendChild(preactContainer);
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
afterEach(() => {
|
|
448
|
-
lunaContainer.remove();
|
|
449
|
-
preactContainer.remove();
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
test("Fragment with single child returns the child directly", () => {
|
|
453
|
-
// Luna
|
|
454
|
-
const lunaNode = Fragment([createElement("span", [], [text("Single")])]);
|
|
455
|
-
lunaRender(lunaContainer, lunaNode);
|
|
456
|
-
|
|
457
|
-
// Preact
|
|
458
|
-
preactRender(h(PreactFragment, null, h("span", null, "Single")), preactContainer);
|
|
459
|
-
|
|
460
|
-
expect(getVisibleNodes(lunaContainer)).toEqual(getVisibleNodes(preactContainer));
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
test("Fragment with multiple children (no wrapper)", () => {
|
|
464
|
-
// Luna
|
|
465
|
-
const lunaNode = Fragment([
|
|
466
|
-
createElement("span", [], [text("A")]),
|
|
467
|
-
createElement("span", [], [text("B")]),
|
|
468
|
-
createElement("span", [], [text("C")]),
|
|
469
|
-
]);
|
|
470
|
-
lunaRender(lunaContainer, lunaNode);
|
|
471
|
-
|
|
472
|
-
// Preact
|
|
473
|
-
preactRender(
|
|
474
|
-
h(
|
|
475
|
-
PreactFragment,
|
|
476
|
-
null,
|
|
477
|
-
h("span", null, "A"),
|
|
478
|
-
h("span", null, "B"),
|
|
479
|
-
h("span", null, "C")
|
|
480
|
-
),
|
|
481
|
-
preactContainer
|
|
482
|
-
);
|
|
483
|
-
|
|
484
|
-
expect(getVisibleNodes(lunaContainer)).toEqual(getVisibleNodes(preactContainer));
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
test("Fragment with no children", () => {
|
|
488
|
-
// Luna
|
|
489
|
-
const lunaNode = Fragment([]);
|
|
490
|
-
lunaRender(lunaContainer, lunaNode);
|
|
491
|
-
|
|
492
|
-
// Preact
|
|
493
|
-
preactRender(h(PreactFragment, null), preactContainer);
|
|
494
|
-
|
|
495
|
-
// Both should be empty (ignoring comment markers)
|
|
496
|
-
expect(getVisibleNodes(lunaContainer)).toEqual(getVisibleNodes(preactContainer));
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
test("nested Fragments work correctly", () => {
|
|
500
|
-
// Luna
|
|
501
|
-
const lunaNode = Fragment([
|
|
502
|
-
Fragment([createElement("div", [], [text("A1")])]),
|
|
503
|
-
Fragment([
|
|
504
|
-
createElement("div", [], [text("B1")]),
|
|
505
|
-
createElement("div", [], [text("B2")]),
|
|
506
|
-
]),
|
|
507
|
-
]);
|
|
508
|
-
lunaRender(lunaContainer, lunaNode);
|
|
509
|
-
|
|
510
|
-
// Preact
|
|
511
|
-
preactRender(
|
|
512
|
-
h(
|
|
513
|
-
PreactFragment,
|
|
514
|
-
null,
|
|
515
|
-
h(PreactFragment, null, h("div", null, "A1")),
|
|
516
|
-
h(
|
|
517
|
-
PreactFragment,
|
|
518
|
-
null,
|
|
519
|
-
h("div", null, "B1"),
|
|
520
|
-
h("div", null, "B2")
|
|
521
|
-
)
|
|
522
|
-
),
|
|
523
|
-
preactContainer
|
|
524
|
-
);
|
|
525
|
-
|
|
526
|
-
expect(getVisibleNodes(lunaContainer)).toEqual(getVisibleNodes(preactContainer));
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
test("fragment with list", () => {
|
|
530
|
-
const items = ["x", "y", "z"];
|
|
531
|
-
|
|
532
|
-
// Luna
|
|
533
|
-
const [lunaItems] = createSignal(items);
|
|
534
|
-
const lunaNode = Fragment([
|
|
535
|
-
createElement("header", [], [text("Header")]),
|
|
536
|
-
For({
|
|
537
|
-
each: lunaItems,
|
|
538
|
-
children: (item: string) => createElement("p", [], [text(item)]),
|
|
539
|
-
}),
|
|
540
|
-
createElement("footer", [], [text("Footer")]),
|
|
541
|
-
]);
|
|
542
|
-
lunaRender(lunaContainer, lunaNode);
|
|
543
|
-
|
|
544
|
-
// Preact
|
|
545
|
-
preactRender(
|
|
546
|
-
h(
|
|
547
|
-
PreactFragment,
|
|
548
|
-
null,
|
|
549
|
-
h("header", null, "Header"),
|
|
550
|
-
items.map((item, i) => h("p", { key: i }, item)),
|
|
551
|
-
h("footer", null, "Footer")
|
|
552
|
-
),
|
|
553
|
-
preactContainer
|
|
554
|
-
);
|
|
555
|
-
|
|
556
|
-
expect(getVisibleNodes(lunaContainer)).toEqual(getVisibleNodes(preactContainer));
|
|
557
|
-
});
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
describe("Luna Conditional Rendering", () => {
|
|
561
|
-
let container: HTMLDivElement;
|
|
562
|
-
|
|
563
|
-
beforeEach(() => {
|
|
564
|
-
container = document.createElement("div");
|
|
565
|
-
document.body.appendChild(container);
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
afterEach(() => {
|
|
569
|
-
container.remove();
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
test("show renders content when initially true", () => {
|
|
573
|
-
const [visible] = createSignal(true);
|
|
574
|
-
const node = createElement("div", [], [
|
|
575
|
-
show(visible, () => createElement("span", [], [text("Visible")])),
|
|
576
|
-
]);
|
|
577
|
-
lunaRender(container, node);
|
|
578
|
-
|
|
579
|
-
// Luna uses placeholder comments and effects
|
|
580
|
-
// Content should be rendered
|
|
581
|
-
expect(container.querySelector("span")).not.toBeNull();
|
|
582
|
-
expect(container.textContent).toContain("Visible");
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
test("show hides content when initially false", () => {
|
|
586
|
-
const [visible] = createSignal(false);
|
|
587
|
-
const node = createElement("div", [], [
|
|
588
|
-
show(visible, () => createElement("span", [], [text("Hidden")])),
|
|
589
|
-
]);
|
|
590
|
-
lunaRender(container, node);
|
|
591
|
-
|
|
592
|
-
expect(container.querySelector("span")).toBeNull();
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
test("show toggles visibility dynamically", () => {
|
|
596
|
-
const [visible, setVisible] = createSignal(false);
|
|
597
|
-
const node = createElement("div", [], [
|
|
598
|
-
show(visible, () => createElement("span", [], [text("Content")])),
|
|
599
|
-
]);
|
|
600
|
-
lunaRender(container, node);
|
|
601
|
-
|
|
602
|
-
expect(container.querySelector("span")).toBeNull();
|
|
603
|
-
|
|
604
|
-
setVisible(true);
|
|
605
|
-
expect(container.querySelector("span")).not.toBeNull();
|
|
606
|
-
expect(container.textContent).toContain("Content");
|
|
607
|
-
|
|
608
|
-
setVisible(false);
|
|
609
|
-
expect(container.querySelector("span")).toBeNull();
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
test("Show component renders when condition is true", () => {
|
|
613
|
-
const [visible] = createSignal(true);
|
|
614
|
-
const node = createElement("div", [], [
|
|
615
|
-
Show({
|
|
616
|
-
when: visible,
|
|
617
|
-
children: createElement("span", [], [text("Shown")]),
|
|
618
|
-
}),
|
|
619
|
-
]);
|
|
620
|
-
lunaRender(container, node);
|
|
621
|
-
|
|
622
|
-
expect(container.querySelector("span")).not.toBeNull();
|
|
623
|
-
expect(container.textContent).toContain("Shown");
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
test("Show component hides when condition is false", () => {
|
|
627
|
-
const [visible] = createSignal(false);
|
|
628
|
-
const node = createElement("div", [], [
|
|
629
|
-
Show({
|
|
630
|
-
when: visible,
|
|
631
|
-
children: createElement("span", [], [text("Hidden")]),
|
|
632
|
-
}),
|
|
633
|
-
]);
|
|
634
|
-
lunaRender(container, node);
|
|
635
|
-
|
|
636
|
-
expect(container.querySelector("span")).toBeNull();
|
|
637
|
-
});
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
describe("DOM Rendering Comparison - Reactive Updates", () => {
|
|
641
|
-
let lunaContainer: HTMLDivElement;
|
|
642
|
-
let preactContainer: HTMLDivElement;
|
|
643
|
-
|
|
644
|
-
beforeEach(() => {
|
|
645
|
-
lunaContainer = document.createElement("div");
|
|
646
|
-
preactContainer = document.createElement("div");
|
|
647
|
-
document.body.appendChild(lunaContainer);
|
|
648
|
-
document.body.appendChild(preactContainer);
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
afterEach(() => {
|
|
652
|
-
lunaContainer.remove();
|
|
653
|
-
preactContainer.remove();
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
test("text updates match", () => {
|
|
657
|
-
// Luna
|
|
658
|
-
const [lunaText, setLunaText] = createSignal("initial");
|
|
659
|
-
const lunaNode = createElement("div", [], [textDyn(lunaText)]);
|
|
660
|
-
lunaRender(lunaContainer, lunaNode);
|
|
661
|
-
|
|
662
|
-
expect(getNormalizedContent(lunaContainer)).toBe("<div>initial</div>");
|
|
663
|
-
|
|
664
|
-
setLunaText("updated");
|
|
665
|
-
expect(getNormalizedContent(lunaContainer)).toBe("<div>updated</div>");
|
|
666
|
-
});
|
|
667
|
-
|
|
668
|
-
test("list addition updates match", () => {
|
|
669
|
-
const initialItems = ["a", "b"];
|
|
670
|
-
|
|
671
|
-
// Luna
|
|
672
|
-
const [lunaItems, setLunaItems] = createSignal(initialItems);
|
|
673
|
-
const lunaNode = createElement("ul", [], [
|
|
674
|
-
For({
|
|
675
|
-
each: lunaItems,
|
|
676
|
-
children: (item: string) => createElement("li", [], [text(item)]),
|
|
677
|
-
}),
|
|
678
|
-
]);
|
|
679
|
-
lunaRender(lunaContainer, lunaNode);
|
|
680
|
-
|
|
681
|
-
// Preact (static initial)
|
|
682
|
-
preactRender(
|
|
683
|
-
h(
|
|
684
|
-
"ul",
|
|
685
|
-
null,
|
|
686
|
-
initialItems.map((item, i) => h("li", { key: i }, item))
|
|
687
|
-
),
|
|
688
|
-
preactContainer
|
|
689
|
-
);
|
|
690
|
-
|
|
691
|
-
expect(getVisibleNodes(lunaContainer)).toEqual(
|
|
692
|
-
getVisibleNodes(preactContainer)
|
|
693
|
-
);
|
|
694
|
-
|
|
695
|
-
// Update Luna
|
|
696
|
-
setLunaItems(["a", "b", "c"]);
|
|
697
|
-
|
|
698
|
-
// Update Preact
|
|
699
|
-
preactRender(
|
|
700
|
-
h(
|
|
701
|
-
"ul",
|
|
702
|
-
null,
|
|
703
|
-
["a", "b", "c"].map((item, i) => h("li", { key: i }, item))
|
|
704
|
-
),
|
|
705
|
-
preactContainer
|
|
706
|
-
);
|
|
707
|
-
|
|
708
|
-
expect(getVisibleNodes(lunaContainer)).toEqual(
|
|
709
|
-
getVisibleNodes(preactContainer)
|
|
710
|
-
);
|
|
711
|
-
});
|
|
712
|
-
|
|
713
|
-
test("list removal updates match", () => {
|
|
714
|
-
// Luna
|
|
715
|
-
const [lunaItems, setLunaItems] = createSignal(["x", "y", "z"]);
|
|
716
|
-
const lunaNode = createElement("ul", [], [
|
|
717
|
-
For({
|
|
718
|
-
each: lunaItems,
|
|
719
|
-
children: (item: string) => createElement("li", [], [text(item)]),
|
|
720
|
-
}),
|
|
721
|
-
]);
|
|
722
|
-
lunaRender(lunaContainer, lunaNode);
|
|
723
|
-
|
|
724
|
-
// Update to remove items
|
|
725
|
-
setLunaItems(["x"]);
|
|
726
|
-
|
|
727
|
-
// Preact with same final state
|
|
728
|
-
preactRender(h("ul", null, h("li", { key: 0 }, "x")), preactContainer);
|
|
729
|
-
|
|
730
|
-
expect(getVisibleNodes(lunaContainer)).toEqual(
|
|
731
|
-
getVisibleNodes(preactContainer)
|
|
732
|
-
);
|
|
733
|
-
});
|
|
734
|
-
|
|
735
|
-
test("conditional toggle updates", () => {
|
|
736
|
-
// Luna
|
|
737
|
-
const [lunaShow, setLunaShow] = createSignal(false);
|
|
738
|
-
const lunaNode = createElement("div", [], [
|
|
739
|
-
show(lunaShow, () => createElement("span", [], [text("Now visible")])),
|
|
740
|
-
]);
|
|
741
|
-
lunaRender(lunaContainer, lunaNode);
|
|
742
|
-
|
|
743
|
-
// Initially hidden
|
|
744
|
-
expect(lunaContainer.querySelector("span")).toBeNull();
|
|
745
|
-
|
|
746
|
-
// Show it
|
|
747
|
-
setLunaShow(true);
|
|
748
|
-
expect(lunaContainer.querySelector("span")).not.toBeNull();
|
|
749
|
-
expect(lunaContainer.textContent).toContain("Now visible");
|
|
750
|
-
|
|
751
|
-
// Hide it again
|
|
752
|
-
setLunaShow(false);
|
|
753
|
-
expect(lunaContainer.querySelector("span")).toBeNull();
|
|
754
|
-
});
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
describe("Complex Scenarios", () => {
|
|
758
|
-
let lunaContainer: HTMLDivElement;
|
|
759
|
-
let preactContainer: HTMLDivElement;
|
|
760
|
-
|
|
761
|
-
beforeEach(() => {
|
|
762
|
-
lunaContainer = document.createElement("div");
|
|
763
|
-
preactContainer = document.createElement("div");
|
|
764
|
-
document.body.appendChild(lunaContainer);
|
|
765
|
-
document.body.appendChild(preactContainer);
|
|
766
|
-
});
|
|
767
|
-
|
|
768
|
-
afterEach(() => {
|
|
769
|
-
lunaContainer.remove();
|
|
770
|
-
preactContainer.remove();
|
|
771
|
-
});
|
|
772
|
-
|
|
773
|
-
test("todo list structure", () => {
|
|
774
|
-
const todos = [
|
|
775
|
-
{ id: 1, text: "Learn Luna", done: true },
|
|
776
|
-
{ id: 2, text: "Build app", done: false },
|
|
777
|
-
{ id: 3, text: "Deploy", done: false },
|
|
778
|
-
];
|
|
779
|
-
|
|
780
|
-
// Luna
|
|
781
|
-
const [lunaTodos] = createSignal(todos);
|
|
782
|
-
const lunaNode = createElement(
|
|
783
|
-
"div",
|
|
784
|
-
[attr("className", AttrValue.Static("todo-app"))],
|
|
785
|
-
[
|
|
786
|
-
createElement("h1", [], [text("Todos")]),
|
|
787
|
-
createElement("ul", [attr("className", AttrValue.Static("todo-list"))], [
|
|
788
|
-
For({
|
|
789
|
-
each: lunaTodos,
|
|
790
|
-
children: (todo: (typeof todos)[0]) =>
|
|
791
|
-
createElement(
|
|
792
|
-
"li",
|
|
793
|
-
[
|
|
794
|
-
attr(
|
|
795
|
-
"className",
|
|
796
|
-
AttrValue.Static(todo.done ? "done" : "pending")
|
|
797
|
-
),
|
|
798
|
-
],
|
|
799
|
-
[
|
|
800
|
-
createElement(
|
|
801
|
-
"input",
|
|
802
|
-
[
|
|
803
|
-
attr("type", AttrValue.Static("checkbox")),
|
|
804
|
-
attr("checked", AttrValue.Static(todo.done ? "true" : "false")),
|
|
805
|
-
],
|
|
806
|
-
[]
|
|
807
|
-
),
|
|
808
|
-
createElement("span", [], [text(todo.text)]),
|
|
809
|
-
]
|
|
810
|
-
),
|
|
811
|
-
}),
|
|
812
|
-
]),
|
|
813
|
-
createElement(
|
|
814
|
-
"footer",
|
|
815
|
-
[],
|
|
816
|
-
[text(`${todos.filter((t) => !t.done).length} items left`)]
|
|
817
|
-
),
|
|
818
|
-
]
|
|
819
|
-
);
|
|
820
|
-
lunaRender(lunaContainer, lunaNode);
|
|
821
|
-
|
|
822
|
-
// Preact
|
|
823
|
-
preactRender(
|
|
824
|
-
h(
|
|
825
|
-
"div",
|
|
826
|
-
{ className: "todo-app" },
|
|
827
|
-
h("h1", null, "Todos"),
|
|
828
|
-
h(
|
|
829
|
-
"ul",
|
|
830
|
-
{ className: "todo-list" },
|
|
831
|
-
todos.map((todo) =>
|
|
832
|
-
h(
|
|
833
|
-
"li",
|
|
834
|
-
{ key: todo.id, className: todo.done ? "done" : "pending" },
|
|
835
|
-
h("input", { type: "checkbox", checked: todo.done }),
|
|
836
|
-
h("span", null, todo.text)
|
|
837
|
-
)
|
|
838
|
-
)
|
|
839
|
-
),
|
|
840
|
-
h("footer", null, `${todos.filter((t) => !t.done).length} items left`)
|
|
841
|
-
),
|
|
842
|
-
preactContainer
|
|
843
|
-
);
|
|
844
|
-
|
|
845
|
-
expect(getVisibleNodes(lunaContainer)).toEqual(
|
|
846
|
-
getVisibleNodes(preactContainer)
|
|
847
|
-
);
|
|
848
|
-
});
|
|
849
|
-
|
|
850
|
-
test("tree structure with variable depth", () => {
|
|
851
|
-
interface TreeNode {
|
|
852
|
-
name: string;
|
|
853
|
-
children?: TreeNode[];
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
const tree: TreeNode = {
|
|
857
|
-
name: "root",
|
|
858
|
-
children: [
|
|
859
|
-
{
|
|
860
|
-
name: "branch1",
|
|
861
|
-
children: [
|
|
862
|
-
{ name: "leaf1a" },
|
|
863
|
-
{ name: "leaf1b" },
|
|
864
|
-
],
|
|
865
|
-
},
|
|
866
|
-
{
|
|
867
|
-
name: "branch2",
|
|
868
|
-
children: [
|
|
869
|
-
{
|
|
870
|
-
name: "branch2a",
|
|
871
|
-
children: [
|
|
872
|
-
{ name: "leaf2a1" },
|
|
873
|
-
],
|
|
874
|
-
},
|
|
875
|
-
],
|
|
876
|
-
},
|
|
877
|
-
{ name: "leaf3" },
|
|
878
|
-
],
|
|
879
|
-
};
|
|
880
|
-
|
|
881
|
-
// Helper to render tree node in Luna
|
|
882
|
-
function renderLunaTree(node: TreeNode): ReturnType<typeof createElement> {
|
|
883
|
-
if (!node.children || node.children.length === 0) {
|
|
884
|
-
return createElement("span", [attr("className", AttrValue.Static("leaf"))], [
|
|
885
|
-
text(node.name),
|
|
886
|
-
]);
|
|
887
|
-
}
|
|
888
|
-
return createElement("div", [attr("className", AttrValue.Static("branch"))], [
|
|
889
|
-
createElement("strong", [], [text(node.name)]),
|
|
890
|
-
createElement("div", [attr("className", AttrValue.Static("children"))],
|
|
891
|
-
node.children.map(child => renderLunaTree(child))
|
|
892
|
-
),
|
|
893
|
-
]);
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
// Helper to render tree node in Preact
|
|
897
|
-
function renderPreactTree(node: TreeNode): VNode<any> {
|
|
898
|
-
if (!node.children || node.children.length === 0) {
|
|
899
|
-
return h("span", { className: "leaf" }, node.name);
|
|
900
|
-
}
|
|
901
|
-
return h(
|
|
902
|
-
"div",
|
|
903
|
-
{ className: "branch" },
|
|
904
|
-
h("strong", null, node.name),
|
|
905
|
-
h(
|
|
906
|
-
"div",
|
|
907
|
-
{ className: "children" },
|
|
908
|
-
node.children.map((child, i) =>
|
|
909
|
-
// Add a fragment wrapper with key for Preact
|
|
910
|
-
h(PreactFragment, { key: i }, renderPreactTree(child))
|
|
911
|
-
)
|
|
912
|
-
)
|
|
913
|
-
);
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
// Luna
|
|
917
|
-
lunaRender(lunaContainer, renderLunaTree(tree));
|
|
918
|
-
|
|
919
|
-
// Preact
|
|
920
|
-
preactRender(renderPreactTree(tree), preactContainer);
|
|
921
|
-
|
|
922
|
-
expect(getVisibleNodes(lunaContainer)).toEqual(
|
|
923
|
-
getVisibleNodes(preactContainer)
|
|
924
|
-
);
|
|
925
|
-
});
|
|
926
|
-
|
|
927
|
-
test("mixed content with text and elements", () => {
|
|
928
|
-
// Luna
|
|
929
|
-
const lunaNode = createElement("article", [], [
|
|
930
|
-
createElement("p", [], [
|
|
931
|
-
text("This is "),
|
|
932
|
-
createElement("strong", [], [text("bold")]),
|
|
933
|
-
text(" and "),
|
|
934
|
-
createElement("em", [], [text("italic")]),
|
|
935
|
-
text(" text."),
|
|
936
|
-
]),
|
|
937
|
-
createElement("p", [], [
|
|
938
|
-
text("Numbers: "),
|
|
939
|
-
createElement("code", [], [text("1")]),
|
|
940
|
-
text(", "),
|
|
941
|
-
createElement("code", [], [text("2")]),
|
|
942
|
-
text(", "),
|
|
943
|
-
createElement("code", [], [text("3")]),
|
|
944
|
-
]),
|
|
945
|
-
]);
|
|
946
|
-
lunaRender(lunaContainer, lunaNode);
|
|
947
|
-
|
|
948
|
-
// Preact
|
|
949
|
-
preactRender(
|
|
950
|
-
h(
|
|
951
|
-
"article",
|
|
952
|
-
null,
|
|
953
|
-
h(
|
|
954
|
-
"p",
|
|
955
|
-
null,
|
|
956
|
-
"This is ",
|
|
957
|
-
h("strong", null, "bold"),
|
|
958
|
-
" and ",
|
|
959
|
-
h("em", null, "italic"),
|
|
960
|
-
" text."
|
|
961
|
-
),
|
|
962
|
-
h(
|
|
963
|
-
"p",
|
|
964
|
-
null,
|
|
965
|
-
"Numbers: ",
|
|
966
|
-
h("code", null, "1"),
|
|
967
|
-
", ",
|
|
968
|
-
h("code", null, "2"),
|
|
969
|
-
", ",
|
|
970
|
-
h("code", null, "3")
|
|
971
|
-
)
|
|
972
|
-
),
|
|
973
|
-
preactContainer
|
|
974
|
-
);
|
|
975
|
-
|
|
976
|
-
expect(getVisibleNodes(lunaContainer)).toEqual(
|
|
977
|
-
getVisibleNodes(preactContainer)
|
|
978
|
-
);
|
|
979
|
-
});
|
|
980
|
-
});
|
|
981
|
-
|
|
982
|
-
describe("Edge Cases", () => {
|
|
983
|
-
let container: HTMLDivElement;
|
|
984
|
-
|
|
985
|
-
beforeEach(() => {
|
|
986
|
-
container = document.createElement("div");
|
|
987
|
-
document.body.appendChild(container);
|
|
988
|
-
});
|
|
989
|
-
|
|
990
|
-
afterEach(() => {
|
|
991
|
-
container.remove();
|
|
992
|
-
});
|
|
993
|
-
|
|
994
|
-
test("empty list", () => {
|
|
995
|
-
const [items] = createSignal<string[]>([]);
|
|
996
|
-
const node = createElement("ul", [], [
|
|
997
|
-
For({
|
|
998
|
-
each: items,
|
|
999
|
-
children: (item: string) => createElement("li", [], [text(item)]),
|
|
1000
|
-
}),
|
|
1001
|
-
]);
|
|
1002
|
-
lunaRender(container, node);
|
|
1003
|
-
|
|
1004
|
-
expect(container.querySelectorAll("li").length).toBe(0);
|
|
1005
|
-
});
|
|
1006
|
-
|
|
1007
|
-
test("list transitions from empty to populated", () => {
|
|
1008
|
-
const [items, setItems] = createSignal<string[]>([]);
|
|
1009
|
-
const node = createElement("ul", [], [
|
|
1010
|
-
For({
|
|
1011
|
-
each: items,
|
|
1012
|
-
children: (item: string) => createElement("li", [], [text(item)]),
|
|
1013
|
-
}),
|
|
1014
|
-
]);
|
|
1015
|
-
lunaRender(container, node);
|
|
1016
|
-
|
|
1017
|
-
expect(container.querySelectorAll("li").length).toBe(0);
|
|
1018
|
-
|
|
1019
|
-
setItems(["a", "b"]);
|
|
1020
|
-
expect(container.querySelectorAll("li").length).toBe(2);
|
|
1021
|
-
});
|
|
1022
|
-
|
|
1023
|
-
test("list transitions from populated to empty", () => {
|
|
1024
|
-
const [items, setItems] = createSignal(["a", "b", "c"]);
|
|
1025
|
-
const node = createElement("ul", [], [
|
|
1026
|
-
For({
|
|
1027
|
-
each: items,
|
|
1028
|
-
children: (item: string) => createElement("li", [], [text(item)]),
|
|
1029
|
-
}),
|
|
1030
|
-
]);
|
|
1031
|
-
lunaRender(container, node);
|
|
1032
|
-
|
|
1033
|
-
expect(container.querySelectorAll("li").length).toBe(3);
|
|
1034
|
-
|
|
1035
|
-
setItems([]);
|
|
1036
|
-
expect(container.querySelectorAll("li").length).toBe(0);
|
|
1037
|
-
});
|
|
1038
|
-
|
|
1039
|
-
test("deeply nested conditionals", () => {
|
|
1040
|
-
const [a, setA] = createSignal(true);
|
|
1041
|
-
const [b, setB] = createSignal(true);
|
|
1042
|
-
const [c, setC] = createSignal(true);
|
|
1043
|
-
|
|
1044
|
-
const node = createElement("div", [], [
|
|
1045
|
-
show(a, () =>
|
|
1046
|
-
createElement("div", [attr("id", AttrValue.Static("a"))], [
|
|
1047
|
-
show(b, () =>
|
|
1048
|
-
createElement("div", [attr("id", AttrValue.Static("b"))], [
|
|
1049
|
-
show(c, () =>
|
|
1050
|
-
createElement("span", [attr("id", AttrValue.Static("c"))], [
|
|
1051
|
-
text("Deepest"),
|
|
1052
|
-
])
|
|
1053
|
-
),
|
|
1054
|
-
])
|
|
1055
|
-
),
|
|
1056
|
-
])
|
|
1057
|
-
),
|
|
1058
|
-
]);
|
|
1059
|
-
lunaRender(container, node);
|
|
1060
|
-
|
|
1061
|
-
// All visible
|
|
1062
|
-
expect(container.querySelector("#a")).not.toBeNull();
|
|
1063
|
-
expect(container.querySelector("#b")).not.toBeNull();
|
|
1064
|
-
expect(container.querySelector("#c")).not.toBeNull();
|
|
1065
|
-
|
|
1066
|
-
// Hide middle layer
|
|
1067
|
-
setB(false);
|
|
1068
|
-
expect(container.querySelector("#a")).not.toBeNull();
|
|
1069
|
-
expect(container.querySelector("#b")).toBeNull();
|
|
1070
|
-
expect(container.querySelector("#c")).toBeNull();
|
|
1071
|
-
|
|
1072
|
-
// Show middle layer again
|
|
1073
|
-
setB(true);
|
|
1074
|
-
expect(container.querySelector("#b")).not.toBeNull();
|
|
1075
|
-
expect(container.querySelector("#c")).not.toBeNull();
|
|
1076
|
-
|
|
1077
|
-
// Hide outer layer
|
|
1078
|
-
setA(false);
|
|
1079
|
-
expect(container.querySelector("#a")).toBeNull();
|
|
1080
|
-
expect(container.querySelector("#b")).toBeNull();
|
|
1081
|
-
expect(container.querySelector("#c")).toBeNull();
|
|
1082
|
-
});
|
|
1083
|
-
});
|
|
1084
|
-
|
|
1085
|
-
// =============================================================================
|
|
1086
|
-
// Dynamic Attributes Tests
|
|
1087
|
-
// =============================================================================
|
|
1088
|
-
|
|
1089
|
-
describe("Dynamic Attributes Comparison", () => {
|
|
1090
|
-
let lunaContainer: HTMLDivElement;
|
|
1091
|
-
let preactContainer: HTMLDivElement;
|
|
1092
|
-
|
|
1093
|
-
beforeEach(() => {
|
|
1094
|
-
lunaContainer = document.createElement("div");
|
|
1095
|
-
preactContainer = document.createElement("div");
|
|
1096
|
-
document.body.appendChild(lunaContainer);
|
|
1097
|
-
document.body.appendChild(preactContainer);
|
|
1098
|
-
});
|
|
1099
|
-
|
|
1100
|
-
afterEach(() => {
|
|
1101
|
-
lunaContainer.remove();
|
|
1102
|
-
preactContainer.remove();
|
|
1103
|
-
});
|
|
1104
|
-
|
|
1105
|
-
test("dynamic className updates", () => {
|
|
1106
|
-
const [lunaActive, setLunaActive] = createSignal(false);
|
|
1107
|
-
const lunaNode = createElement(
|
|
1108
|
-
"div",
|
|
1109
|
-
[attr("className", AttrValue.Dynamic(() => lunaActive() ? "active" : "inactive"))],
|
|
1110
|
-
[text("Toggle")]
|
|
1111
|
-
);
|
|
1112
|
-
lunaRender(lunaContainer, lunaNode);
|
|
1113
|
-
|
|
1114
|
-
expect(lunaContainer.querySelector("div")?.className).toBe("inactive");
|
|
1115
|
-
|
|
1116
|
-
setLunaActive(true);
|
|
1117
|
-
expect(lunaContainer.querySelector("div")?.className).toBe("active");
|
|
1118
|
-
|
|
1119
|
-
setLunaActive(false);
|
|
1120
|
-
expect(lunaContainer.querySelector("div")?.className).toBe("inactive");
|
|
1121
|
-
});
|
|
1122
|
-
|
|
1123
|
-
test("dynamic style updates", () => {
|
|
1124
|
-
const [color, setColor] = createSignal("red");
|
|
1125
|
-
const lunaNode = createElement(
|
|
1126
|
-
"div",
|
|
1127
|
-
[attr("style", AttrValue.Dynamic(() => `color: ${color()}`))],
|
|
1128
|
-
[text("Colored")]
|
|
1129
|
-
);
|
|
1130
|
-
lunaRender(lunaContainer, lunaNode);
|
|
1131
|
-
|
|
1132
|
-
expect(lunaContainer.querySelector("div")?.getAttribute("style")).toContain("red");
|
|
1133
|
-
|
|
1134
|
-
setColor("blue");
|
|
1135
|
-
expect(lunaContainer.querySelector("div")?.getAttribute("style")).toContain("blue");
|
|
1136
|
-
});
|
|
1137
|
-
|
|
1138
|
-
test("multiple dynamic attributes", () => {
|
|
1139
|
-
const [count, setCount] = createSignal(0);
|
|
1140
|
-
const lunaNode = createElement(
|
|
1141
|
-
"div",
|
|
1142
|
-
[
|
|
1143
|
-
attr("id", AttrValue.Dynamic(() => `item-${count()}`)),
|
|
1144
|
-
attr("data-count", AttrValue.Dynamic(() => String(count()))),
|
|
1145
|
-
attr("className", AttrValue.Dynamic(() => count() > 5 ? "high" : "low")),
|
|
1146
|
-
],
|
|
1147
|
-
[textDyn(() => `Count: ${count()}`)]
|
|
1148
|
-
);
|
|
1149
|
-
lunaRender(lunaContainer, lunaNode);
|
|
1150
|
-
|
|
1151
|
-
const div = lunaContainer.querySelector("div")!;
|
|
1152
|
-
expect(div.id).toBe("item-0");
|
|
1153
|
-
expect(div.getAttribute("data-count")).toBe("0");
|
|
1154
|
-
expect(div.className).toBe("low");
|
|
1155
|
-
|
|
1156
|
-
setCount(10);
|
|
1157
|
-
expect(div.id).toBe("item-10");
|
|
1158
|
-
expect(div.getAttribute("data-count")).toBe("10");
|
|
1159
|
-
expect(div.className).toBe("high");
|
|
1160
|
-
});
|
|
1161
|
-
});
|
|
1162
|
-
|
|
1163
|
-
// =============================================================================
|
|
1164
|
-
// Effect Cleanup Tests
|
|
1165
|
-
// =============================================================================
|
|
1166
|
-
|
|
1167
|
-
describe("Effect Cleanup Comparison", () => {
|
|
1168
|
-
test("onCleanup is called when effect re-runs", () => {
|
|
1169
|
-
const cleanupCalls: number[] = [];
|
|
1170
|
-
const [count, setCount] = createSignal(0);
|
|
1171
|
-
|
|
1172
|
-
createRenderEffect(() => {
|
|
1173
|
-
const currentCount = count();
|
|
1174
|
-
onCleanup(() => {
|
|
1175
|
-
cleanupCalls.push(currentCount);
|
|
1176
|
-
});
|
|
1177
|
-
});
|
|
1178
|
-
|
|
1179
|
-
expect(cleanupCalls).toEqual([]);
|
|
1180
|
-
|
|
1181
|
-
setCount(1);
|
|
1182
|
-
expect(cleanupCalls).toEqual([0]);
|
|
1183
|
-
|
|
1184
|
-
setCount(2);
|
|
1185
|
-
expect(cleanupCalls).toEqual([0, 1]);
|
|
1186
|
-
|
|
1187
|
-
setCount(3);
|
|
1188
|
-
expect(cleanupCalls).toEqual([0, 1, 2]);
|
|
1189
|
-
});
|
|
1190
|
-
|
|
1191
|
-
test("onCleanup with resource simulation", () => {
|
|
1192
|
-
const resources: string[] = [];
|
|
1193
|
-
const [resourceId, setResourceId] = createSignal("A");
|
|
1194
|
-
|
|
1195
|
-
createRenderEffect(() => {
|
|
1196
|
-
const id = resourceId();
|
|
1197
|
-
resources.push(`open:${id}`);
|
|
1198
|
-
|
|
1199
|
-
onCleanup(() => {
|
|
1200
|
-
resources.push(`close:${id}`);
|
|
1201
|
-
});
|
|
1202
|
-
});
|
|
1203
|
-
|
|
1204
|
-
expect(resources).toEqual(["open:A"]);
|
|
1205
|
-
|
|
1206
|
-
setResourceId("B");
|
|
1207
|
-
expect(resources).toEqual(["open:A", "close:A", "open:B"]);
|
|
1208
|
-
|
|
1209
|
-
setResourceId("C");
|
|
1210
|
-
expect(resources).toEqual(["open:A", "close:A", "open:B", "close:B", "open:C"]);
|
|
1211
|
-
});
|
|
1212
|
-
|
|
1213
|
-
test("nested effects with inner signal change", () => {
|
|
1214
|
-
const log: string[] = [];
|
|
1215
|
-
const [inner, setInner] = createSignal(0);
|
|
1216
|
-
|
|
1217
|
-
createRenderEffect(() => {
|
|
1218
|
-
// Capture value at effect run time for cleanup
|
|
1219
|
-
const currentValue = inner();
|
|
1220
|
-
log.push(`run:${currentValue}`);
|
|
1221
|
-
onCleanup(() => log.push(`cleanup:${currentValue}`));
|
|
1222
|
-
});
|
|
1223
|
-
|
|
1224
|
-
expect(log).toEqual(["run:0"]);
|
|
1225
|
-
|
|
1226
|
-
setInner(1);
|
|
1227
|
-
expect(log).toContain("cleanup:0");
|
|
1228
|
-
expect(log).toContain("run:1");
|
|
1229
|
-
|
|
1230
|
-
setInner(2);
|
|
1231
|
-
expect(log).toContain("cleanup:1");
|
|
1232
|
-
expect(log).toContain("run:2");
|
|
1233
|
-
});
|
|
1234
|
-
});
|
|
1235
|
-
|
|
1236
|
-
// =============================================================================
|
|
1237
|
-
// Untrack and Peek Tests
|
|
1238
|
-
// =============================================================================
|
|
1239
|
-
|
|
1240
|
-
describe("Untrack and Peek Behavior", () => {
|
|
1241
|
-
test("untrack prevents dependency tracking", () => {
|
|
1242
|
-
const [a, setA] = createSignal(1);
|
|
1243
|
-
const [b, setB] = createSignal(10);
|
|
1244
|
-
const effectRuns: number[] = [];
|
|
1245
|
-
|
|
1246
|
-
createRenderEffect(() => {
|
|
1247
|
-
const aVal = a();
|
|
1248
|
-
const bVal = untrack(() => b());
|
|
1249
|
-
effectRuns.push(aVal + bVal);
|
|
1250
|
-
});
|
|
1251
|
-
|
|
1252
|
-
expect(effectRuns).toEqual([11]);
|
|
1253
|
-
|
|
1254
|
-
// Changing 'a' should trigger effect
|
|
1255
|
-
setA(2);
|
|
1256
|
-
expect(effectRuns).toEqual([11, 12]);
|
|
1257
|
-
|
|
1258
|
-
// Changing 'b' should NOT trigger effect (untracked)
|
|
1259
|
-
setB(20);
|
|
1260
|
-
expect(effectRuns).toEqual([11, 12]);
|
|
1261
|
-
|
|
1262
|
-
// Changing 'a' again will see new 'b' value
|
|
1263
|
-
setA(3);
|
|
1264
|
-
expect(effectRuns).toEqual([11, 12, 23]);
|
|
1265
|
-
});
|
|
1266
|
-
|
|
1267
|
-
test("peek reads value without tracking", () => {
|
|
1268
|
-
const [count, setCount] = createSignal(0);
|
|
1269
|
-
const effectRuns: number[] = [];
|
|
1270
|
-
|
|
1271
|
-
const countSignal = createSignal(0)[0];
|
|
1272
|
-
// Note: Luna's peek works on the raw signal, not the getter
|
|
1273
|
-
// For this test, we use untrack as the equivalent
|
|
1274
|
-
|
|
1275
|
-
createRenderEffect(() => {
|
|
1276
|
-
// Track nothing, just peek
|
|
1277
|
-
const val = untrack(() => count());
|
|
1278
|
-
effectRuns.push(val);
|
|
1279
|
-
});
|
|
1280
|
-
|
|
1281
|
-
// Effect runs once on creation
|
|
1282
|
-
expect(effectRuns.length).toBe(1);
|
|
1283
|
-
|
|
1284
|
-
// Updates should not trigger effect
|
|
1285
|
-
setCount(1);
|
|
1286
|
-
setCount(2);
|
|
1287
|
-
setCount(3);
|
|
1288
|
-
expect(effectRuns.length).toBe(1);
|
|
1289
|
-
});
|
|
1290
|
-
|
|
1291
|
-
test("selective tracking with untrack", () => {
|
|
1292
|
-
const [tracked, setTracked] = createSignal("A");
|
|
1293
|
-
const [untracked1, setUntracked1] = createSignal("X");
|
|
1294
|
-
const [untracked2, setUntracked2] = createSignal("Y");
|
|
1295
|
-
const results: string[] = [];
|
|
1296
|
-
|
|
1297
|
-
createRenderEffect(() => {
|
|
1298
|
-
const t = tracked();
|
|
1299
|
-
const u1 = untrack(() => untracked1());
|
|
1300
|
-
const u2 = untrack(() => untracked2());
|
|
1301
|
-
results.push(`${t}-${u1}-${u2}`);
|
|
1302
|
-
});
|
|
1303
|
-
|
|
1304
|
-
expect(results).toEqual(["A-X-Y"]);
|
|
1305
|
-
|
|
1306
|
-
setUntracked1("X2");
|
|
1307
|
-
setUntracked2("Y2");
|
|
1308
|
-
expect(results).toEqual(["A-X-Y"]); // No change
|
|
1309
|
-
|
|
1310
|
-
setTracked("B");
|
|
1311
|
-
expect(results).toEqual(["A-X-Y", "B-X2-Y2"]); // Sees updated untracked values
|
|
1312
|
-
});
|
|
1313
|
-
});
|
|
1314
|
-
|
|
1315
|
-
// =============================================================================
|
|
1316
|
-
// Memo Dependency Chain Tests
|
|
1317
|
-
// =============================================================================
|
|
1318
|
-
|
|
1319
|
-
describe("Memo Dependency Chain", () => {
|
|
1320
|
-
test("chained memos update correctly", () => {
|
|
1321
|
-
const [base, setBase] = createSignal(1);
|
|
1322
|
-
const doubled = createMemo(() => base() * 2);
|
|
1323
|
-
const quadrupled = createMemo(() => doubled() * 2);
|
|
1324
|
-
const octupled = createMemo(() => quadrupled() * 2);
|
|
1325
|
-
|
|
1326
|
-
expect(base()).toBe(1);
|
|
1327
|
-
expect(doubled()).toBe(2);
|
|
1328
|
-
expect(quadrupled()).toBe(4);
|
|
1329
|
-
expect(octupled()).toBe(8);
|
|
1330
|
-
|
|
1331
|
-
setBase(5);
|
|
1332
|
-
expect(doubled()).toBe(10);
|
|
1333
|
-
expect(quadrupled()).toBe(20);
|
|
1334
|
-
expect(octupled()).toBe(40);
|
|
1335
|
-
});
|
|
1336
|
-
|
|
1337
|
-
test("diamond dependency pattern", () => {
|
|
1338
|
-
// a
|
|
1339
|
-
// / \
|
|
1340
|
-
// b c
|
|
1341
|
-
// \ /
|
|
1342
|
-
// d
|
|
1343
|
-
const [a, setA] = createSignal(1);
|
|
1344
|
-
const b = createMemo(() => a() * 2);
|
|
1345
|
-
const c = createMemo(() => a() * 3);
|
|
1346
|
-
const d = createMemo(() => b() + c());
|
|
1347
|
-
|
|
1348
|
-
expect(d()).toBe(5); // 2 + 3
|
|
1349
|
-
|
|
1350
|
-
setA(10);
|
|
1351
|
-
expect(d()).toBe(50); // 20 + 30
|
|
1352
|
-
});
|
|
1353
|
-
|
|
1354
|
-
test("memo with multiple dependencies", () => {
|
|
1355
|
-
const [x, setX] = createSignal(1);
|
|
1356
|
-
const [y, setY] = createSignal(2);
|
|
1357
|
-
const [z, setZ] = createSignal(3);
|
|
1358
|
-
const sum = createMemo(() => x() + y() + z());
|
|
1359
|
-
|
|
1360
|
-
expect(sum()).toBe(6);
|
|
1361
|
-
|
|
1362
|
-
setX(10);
|
|
1363
|
-
expect(sum()).toBe(15);
|
|
1364
|
-
|
|
1365
|
-
setY(20);
|
|
1366
|
-
expect(sum()).toBe(33);
|
|
1367
|
-
|
|
1368
|
-
setZ(30);
|
|
1369
|
-
expect(sum()).toBe(60);
|
|
1370
|
-
});
|
|
1371
|
-
|
|
1372
|
-
test("conditional memo dependencies", () => {
|
|
1373
|
-
const [condition, setCondition] = createSignal(true);
|
|
1374
|
-
const [a, setA] = createSignal(1);
|
|
1375
|
-
const [b, setB] = createSignal(100);
|
|
1376
|
-
|
|
1377
|
-
const result = createMemo(() => condition() ? a() : b());
|
|
1378
|
-
const effectRuns: number[] = [];
|
|
1379
|
-
|
|
1380
|
-
createRenderEffect(() => {
|
|
1381
|
-
effectRuns.push(result());
|
|
1382
|
-
});
|
|
1383
|
-
|
|
1384
|
-
expect(result()).toBe(1);
|
|
1385
|
-
expect(effectRuns).toEqual([1]);
|
|
1386
|
-
|
|
1387
|
-
// When condition is true, only 'a' changes trigger updates
|
|
1388
|
-
setA(2);
|
|
1389
|
-
expect(result()).toBe(2);
|
|
1390
|
-
|
|
1391
|
-
// 'b' changes don't trigger when condition is true
|
|
1392
|
-
setB(200);
|
|
1393
|
-
expect(result()).toBe(2);
|
|
1394
|
-
|
|
1395
|
-
// Switch condition
|
|
1396
|
-
setCondition(false);
|
|
1397
|
-
expect(result()).toBe(200);
|
|
1398
|
-
|
|
1399
|
-
// Now 'b' changes trigger
|
|
1400
|
-
setB(300);
|
|
1401
|
-
expect(result()).toBe(300);
|
|
1402
|
-
|
|
1403
|
-
// 'a' changes don't trigger when condition is false
|
|
1404
|
-
setA(999);
|
|
1405
|
-
expect(result()).toBe(300);
|
|
1406
|
-
});
|
|
1407
|
-
});
|
|
1408
|
-
|
|
1409
|
-
// =============================================================================
|
|
1410
|
-
// List Reordering Tests
|
|
1411
|
-
// =============================================================================
|
|
1412
|
-
|
|
1413
|
-
describe("List Reordering", () => {
|
|
1414
|
-
let container: HTMLDivElement;
|
|
1415
|
-
|
|
1416
|
-
beforeEach(() => {
|
|
1417
|
-
container = document.createElement("div");
|
|
1418
|
-
document.body.appendChild(container);
|
|
1419
|
-
});
|
|
1420
|
-
|
|
1421
|
-
afterEach(() => {
|
|
1422
|
-
container.remove();
|
|
1423
|
-
});
|
|
1424
|
-
|
|
1425
|
-
test("reverse list order", () => {
|
|
1426
|
-
const [items, setItems] = createSignal(["A", "B", "C", "D"]);
|
|
1427
|
-
const node = createElement("ul", [], [
|
|
1428
|
-
For({
|
|
1429
|
-
each: items,
|
|
1430
|
-
children: (item: string) => createElement("li", [], [text(item)]),
|
|
1431
|
-
}),
|
|
1432
|
-
]);
|
|
1433
|
-
lunaRender(container, node);
|
|
1434
|
-
|
|
1435
|
-
expect(Array.from(container.querySelectorAll("li")).map(el => el.textContent))
|
|
1436
|
-
.toEqual(["A", "B", "C", "D"]);
|
|
1437
|
-
|
|
1438
|
-
setItems(["D", "C", "B", "A"]);
|
|
1439
|
-
expect(Array.from(container.querySelectorAll("li")).map(el => el.textContent))
|
|
1440
|
-
.toEqual(["D", "C", "B", "A"]);
|
|
1441
|
-
});
|
|
1442
|
-
|
|
1443
|
-
test("shuffle list", () => {
|
|
1444
|
-
const [items, setItems] = createSignal([1, 2, 3, 4, 5]);
|
|
1445
|
-
const node = createElement("ul", [], [
|
|
1446
|
-
For({
|
|
1447
|
-
each: items,
|
|
1448
|
-
children: (item: number) => createElement("li", [], [text(String(item))]),
|
|
1449
|
-
}),
|
|
1450
|
-
]);
|
|
1451
|
-
lunaRender(container, node);
|
|
1452
|
-
|
|
1453
|
-
setItems([3, 1, 4, 5, 2]);
|
|
1454
|
-
expect(Array.from(container.querySelectorAll("li")).map(el => el.textContent))
|
|
1455
|
-
.toEqual(["3", "1", "4", "5", "2"]);
|
|
1456
|
-
|
|
1457
|
-
setItems([5, 4, 3, 2, 1]);
|
|
1458
|
-
expect(Array.from(container.querySelectorAll("li")).map(el => el.textContent))
|
|
1459
|
-
.toEqual(["5", "4", "3", "2", "1"]);
|
|
1460
|
-
});
|
|
1461
|
-
|
|
1462
|
-
test("insert in middle", () => {
|
|
1463
|
-
const [items, setItems] = createSignal(["A", "C"]);
|
|
1464
|
-
const node = createElement("ul", [], [
|
|
1465
|
-
For({
|
|
1466
|
-
each: items,
|
|
1467
|
-
children: (item: string) => createElement("li", [], [text(item)]),
|
|
1468
|
-
}),
|
|
1469
|
-
]);
|
|
1470
|
-
lunaRender(container, node);
|
|
1471
|
-
|
|
1472
|
-
expect(container.querySelectorAll("li").length).toBe(2);
|
|
1473
|
-
|
|
1474
|
-
setItems(["A", "B", "C"]);
|
|
1475
|
-
expect(Array.from(container.querySelectorAll("li")).map(el => el.textContent))
|
|
1476
|
-
.toEqual(["A", "B", "C"]);
|
|
1477
|
-
});
|
|
1478
|
-
|
|
1479
|
-
test("remove from middle", () => {
|
|
1480
|
-
const [items, setItems] = createSignal(["A", "B", "C", "D", "E"]);
|
|
1481
|
-
const node = createElement("ul", [], [
|
|
1482
|
-
For({
|
|
1483
|
-
each: items,
|
|
1484
|
-
children: (item: string) => createElement("li", [], [text(item)]),
|
|
1485
|
-
}),
|
|
1486
|
-
]);
|
|
1487
|
-
lunaRender(container, node);
|
|
1488
|
-
|
|
1489
|
-
setItems(["A", "C", "E"]);
|
|
1490
|
-
expect(Array.from(container.querySelectorAll("li")).map(el => el.textContent))
|
|
1491
|
-
.toEqual(["A", "C", "E"]);
|
|
1492
|
-
});
|
|
1493
|
-
|
|
1494
|
-
test("complex reordering with additions and removals", () => {
|
|
1495
|
-
const [items, setItems] = createSignal(["A", "B", "C"]);
|
|
1496
|
-
const node = createElement("ul", [], [
|
|
1497
|
-
For({
|
|
1498
|
-
each: items,
|
|
1499
|
-
children: (item: string) => createElement("li", [], [text(item)]),
|
|
1500
|
-
}),
|
|
1501
|
-
]);
|
|
1502
|
-
lunaRender(container, node);
|
|
1503
|
-
|
|
1504
|
-
// Add, remove, and reorder simultaneously
|
|
1505
|
-
setItems(["D", "B", "E", "A"]);
|
|
1506
|
-
expect(Array.from(container.querySelectorAll("li")).map(el => el.textContent))
|
|
1507
|
-
.toEqual(["D", "B", "E", "A"]);
|
|
1508
|
-
});
|
|
1509
|
-
});
|
|
1510
|
-
|
|
1511
|
-
// =============================================================================
|
|
1512
|
-
// Bulk Updates Performance Tests
|
|
1513
|
-
// =============================================================================
|
|
1514
|
-
|
|
1515
|
-
describe("Bulk Updates", () => {
|
|
1516
|
-
test("many signal updates in batch", () => {
|
|
1517
|
-
const signals = Array.from({ length: 100 }, (_, i) => createSignal(i));
|
|
1518
|
-
let computeCount = 0;
|
|
1519
|
-
|
|
1520
|
-
const sum = createMemo(() => {
|
|
1521
|
-
computeCount++;
|
|
1522
|
-
return signals.reduce((acc, [get]) => acc + get(), 0);
|
|
1523
|
-
});
|
|
1524
|
-
|
|
1525
|
-
// Initial computation
|
|
1526
|
-
expect(sum()).toBe(4950); // Sum of 0..99
|
|
1527
|
-
expect(computeCount).toBe(1);
|
|
1528
|
-
|
|
1529
|
-
// Update all signals in batch
|
|
1530
|
-
batch(() => {
|
|
1531
|
-
signals.forEach(([_, set], i) => set(i * 2));
|
|
1532
|
-
});
|
|
1533
|
-
|
|
1534
|
-
// Should only recompute once after batch
|
|
1535
|
-
expect(sum()).toBe(9900);
|
|
1536
|
-
// Batch should minimize recomputations
|
|
1537
|
-
expect(computeCount).toBeLessThanOrEqual(3);
|
|
1538
|
-
});
|
|
1539
|
-
|
|
1540
|
-
test("rapid sequential updates", () => {
|
|
1541
|
-
const [count, setCount] = createSignal(0);
|
|
1542
|
-
const updates: number[] = [];
|
|
1543
|
-
|
|
1544
|
-
createRenderEffect(() => {
|
|
1545
|
-
updates.push(count());
|
|
1546
|
-
});
|
|
1547
|
-
|
|
1548
|
-
// Rapid updates
|
|
1549
|
-
for (let i = 1; i <= 100; i++) {
|
|
1550
|
-
setCount(i);
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
expect(count()).toBe(100);
|
|
1554
|
-
// All updates should be tracked
|
|
1555
|
-
expect(updates[updates.length - 1]).toBe(100);
|
|
1556
|
-
});
|
|
1557
|
-
|
|
1558
|
-
test("nested batch operations", () => {
|
|
1559
|
-
const [a, setA] = createSignal(0);
|
|
1560
|
-
const [b, setB] = createSignal(0);
|
|
1561
|
-
const [c, setC] = createSignal(0);
|
|
1562
|
-
const effectRuns: string[] = [];
|
|
1563
|
-
|
|
1564
|
-
createRenderEffect(() => {
|
|
1565
|
-
effectRuns.push(`${a()}-${b()}-${c()}`);
|
|
1566
|
-
});
|
|
1567
|
-
|
|
1568
|
-
expect(effectRuns).toEqual(["0-0-0"]);
|
|
1569
|
-
|
|
1570
|
-
batch(() => {
|
|
1571
|
-
setA(1);
|
|
1572
|
-
batch(() => {
|
|
1573
|
-
setB(2);
|
|
1574
|
-
setC(3);
|
|
1575
|
-
});
|
|
1576
|
-
});
|
|
1577
|
-
|
|
1578
|
-
// Nested batches should still result in single update
|
|
1579
|
-
expect(effectRuns[effectRuns.length - 1]).toBe("1-2-3");
|
|
1580
|
-
});
|
|
1581
|
-
|
|
1582
|
-
test("large list update", () => {
|
|
1583
|
-
const initialItems = Array.from({ length: 1000 }, (_, i) => `item-${i}`);
|
|
1584
|
-
const [items, setItems] = createSignal(initialItems);
|
|
1585
|
-
|
|
1586
|
-
const container = document.createElement("div");
|
|
1587
|
-
document.body.appendChild(container);
|
|
1588
|
-
|
|
1589
|
-
const node = createElement("ul", [], [
|
|
1590
|
-
For({
|
|
1591
|
-
each: items,
|
|
1592
|
-
children: (item: string) => createElement("li", [], [text(item)]),
|
|
1593
|
-
}),
|
|
1594
|
-
]);
|
|
1595
|
-
lunaRender(container, node);
|
|
1596
|
-
|
|
1597
|
-
expect(container.querySelectorAll("li").length).toBe(1000);
|
|
1598
|
-
|
|
1599
|
-
// Update to different set
|
|
1600
|
-
const newItems = Array.from({ length: 500 }, (_, i) => `new-${i}`);
|
|
1601
|
-
setItems(newItems);
|
|
1602
|
-
|
|
1603
|
-
expect(container.querySelectorAll("li").length).toBe(500);
|
|
1604
|
-
expect(container.querySelector("li")?.textContent).toBe("new-0");
|
|
1605
|
-
|
|
1606
|
-
container.remove();
|
|
1607
|
-
});
|
|
1608
|
-
});
|