@luna_ui/luna 0.3.4 → 0.3.5

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.
Files changed (172) hide show
  1. package/dist/cli.mjs +1264 -27
  2. package/dist/css/index.d.ts +194 -0
  3. package/dist/css/index.js +721 -0
  4. package/dist/css/runtime.d.ts +92 -0
  5. package/dist/css/runtime.js +179 -0
  6. package/dist/index.js +1 -1
  7. package/dist/jsx-dev-runtime.js +1 -1
  8. package/dist/jsx-runtime.js +1 -1
  9. package/dist/{src-BDdxGwvq.js → src-CHiGeWfy.js} +1 -1
  10. package/dist/vite-plugin.d.ts +122 -0
  11. package/dist/vite-plugin.js +1518 -0
  12. package/package.json +16 -2
  13. package/src/css/extract.ts +798 -0
  14. package/src/css/index.ts +10 -0
  15. package/src/css/inject.ts +205 -0
  16. package/src/css/inline.ts +182 -0
  17. package/src/css/minify.ts +70 -0
  18. package/src/css/optimizer.ts +6 -0
  19. package/src/css/runtime.ts +344 -0
  20. package/src/css-optimizer/README.md +353 -0
  21. package/src/css-optimizer/cooccurrence.ts +100 -0
  22. package/src/css-optimizer/core.ts +263 -0
  23. package/src/css-optimizer/extractors.ts +243 -0
  24. package/src/css-optimizer/hash.ts +54 -0
  25. package/src/css-optimizer/index.ts +129 -0
  26. package/src/css-optimizer/merge.ts +109 -0
  27. package/src/css-optimizer/moonbit-analyzer.ts +210 -0
  28. package/src/css-optimizer/parser.ts +120 -0
  29. package/src/css-optimizer/pattern.ts +171 -0
  30. package/src/css-optimizer/transformers.ts +301 -0
  31. package/src/css-optimizer/types.ts +128 -0
  32. package/src/event-utils.ts +227 -0
  33. package/src/index.ts +890 -0
  34. package/src/jsx-dev-runtime.ts +2 -0
  35. package/src/jsx-runtime.ts +398 -0
  36. package/src/vite-plugin.ts +718 -0
  37. package/tests/__screenshots__/context.test.ts/Context-API-context-with-reactive-effects-context-value-accessible-in-effect-1.png +0 -0
  38. package/tests/__screenshots__/dom.test.ts/DOM-API-For-component--SolidJS-style--For-updates-when-signal-changes-1.png +0 -0
  39. package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-accepts-children-as-function-1.png +0 -0
  40. package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-toggles-visibility-1.png +0 -0
  41. package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-attribute-1.png +0 -0
  42. package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-style-1.png +0 -0
  43. package/tests/__screenshots__/dom.test.ts/DOM-API-createElementNs--SVG-support--createElementNs-with-dynamic-attribute-1.png +0 -0
  44. package/tests/__screenshots__/dom.test.ts/DOM-API-effect-with-DOM-effect-tracks-signal-changes-1.png +0 -0
  45. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-clear-to-empty-1.png +0 -0
  46. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-empty-array-1.png +0 -0
  47. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-removes-items-1.png +0 -0
  48. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-renders-initial-list-1.png +0 -0
  49. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-updates-when-items-change-1.png +0 -0
  50. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-handles-empty-to-non-empty-transition-in-SVG-1.png +0 -0
  51. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-handles-reordering-in-SVG-1.png +0 -0
  52. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-updates-SVG-elements-when-signal-changes-1.png +0 -0
  53. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-with-nested-SVG-groups-1.png +0 -0
  54. package/tests/__screenshots__/dom.test.ts/DOM-API-ref-callback--JSX-style--ref-callback-with-nested-elements-1.png +0 -0
  55. package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-creates-a-node-1.png +0 -0
  56. package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-with-false-condition-creates-placeholder-1.png +0 -0
  57. package/tests/__screenshots__/dom.test.ts/DOM-API-text-nodes-textDyn-creates-reactive-text-node-1.png +0 -0
  58. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-forEach-renders-correctly-without-show--initial-items--1.png +0 -0
  59. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-forEach-with-context-renders-correctly-without-show-1.png +0 -0
  60. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-nested-components-with-context--forEach--and-show-1.png +0 -0
  61. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-show-and-forEach-inherit-context-from-Owner--fixed--1.png +0 -0
  62. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-show-and-forEach-work-together--context-uses-default--1.png +0 -0
  63. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Context---ForEach-integration-forEach-items-can-access-context-1.png +0 -0
  64. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-renders-initial-list-1.png +0 -0
  65. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-updates-when-signal-changes-1.png +0 -0
  66. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-with-object-items-1.png +0 -0
  67. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-hides-when-condition-is-false-1.png +0 -0
  68. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-renders-when-condition-is-true-1.png +0 -0
  69. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-from-false-to-true-1.png +0 -0
  70. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-reactively-1.png +0 -0
  71. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--event-listener-pattern--Solid-js-docs-example--1.png +0 -0
  72. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--multiple-cleanups-in-component-body--LIFO-order--1.png +0 -0
  73. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--onCleanup-in-component-body-runs-on-unmount-1.png +0 -0
  74. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--onCleanup-works-with-For-loop-items--component-body-style--1.png +0 -0
  75. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--timer-cleanup-pattern--Solid-js-style--1.png +0 -0
  76. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Effects-effect-cleanup-runs-before-re-run-1.png +0 -0
  77. package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-large-list-update-1.png +0 -0
  78. package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-nested-batch-operations-1.png +0 -0
  79. package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-rapid-sequential-updates-1.png +0 -0
  80. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-Show-component---visible-1.png +0 -0
  81. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-show-hide-element---visible-1.png +0 -0
  82. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-fragment-with-list-1.png +0 -0
  83. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-nested-fragments-1.png +0 -0
  84. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-simple-fragment-1.png +0 -0
  85. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-conditional-toggle-updates-1.png +0 -0
  86. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-addition-updates-match-1.png +0 -0
  87. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-removal-updates-match-1.png +0 -0
  88. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-text-updates-match-1.png +0 -0
  89. package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-className-updates-1.png +0 -0
  90. package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-style-updates-1.png +0 -0
  91. package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-multiple-dynamic-attributes-1.png +0 -0
  92. package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-deeply-nested-conditionals-1.png +0 -0
  93. package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-empty-to-populated-1.png +0 -0
  94. package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-populated-to-empty-1.png +0 -0
  95. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-cleanup-order-1.png +0 -0
  96. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-with-inner-signal-change-1.png +0 -0
  97. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-is-called-when-effect-re-runs-1.png +0 -0
  98. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-with-resource-simulation-1.png +0 -0
  99. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-multiple-children--no-wrapper--1.png +0 -0
  100. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-no-children-1.png +0 -0
  101. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-fragment-with-list-1.png +0 -0
  102. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-nested-Fragments-work-correctly-1.png +0 -0
  103. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-complex-reordering-with-additions-and-removals-1.png +0 -0
  104. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-insert-in-middle-1.png +0 -0
  105. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-remove-from-middle-1.png +0 -0
  106. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-reverse-list-order-1.png +0 -0
  107. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-shuffle-list-1.png +0 -0
  108. package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-Show-component-renders-when-condition-is-true-1.png +0 -0
  109. package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-renders-content-when-initially-true-1.png +0 -0
  110. package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-toggles-visibility-dynamically-1.png +0 -0
  111. package/tests/__screenshots__/preact-signals-comparison.test.ts/Memo-Dependency-Chain-conditional-memo-dependencies-1.png +0 -0
  112. package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-basic-signal-get-set-produces-same-values-1.png +0 -0
  113. package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-batch-updates-produce-same-final-values-1.png +0 -0
  114. package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-peek-reads-value-without-tracking-1.png +0 -0
  115. package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-selective-tracking-with-untrack-1.png +0 -0
  116. package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-untrack-prevents-dependency-tracking-1.png +0 -0
  117. package/tests/__screenshots__/resource.test.ts/Resource-API--SolidJS-style--reactivity-accessor-is-reactive-1.png +0 -0
  118. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-empty-string-for-non-failure-1.png +0 -0
  119. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-undefined-for-non-failure-1.png +0 -0
  120. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsFailure-and-stateError-1.png +0 -0
  121. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsPending-1.png +0 -0
  122. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsSuccess-and-stateValue-1.png +0 -0
  123. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateValue-returns-undefined-for-non-success-1.png +0 -0
  124. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-reject-transitions-to-failure-1.png +0 -0
  125. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-resolve-transitions-to-success-1.png +0 -0
  126. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-returns-resource--resolve--and-reject-functions-1.png +0 -0
  127. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-starts-in-pending-state-1.png +0 -0
  128. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-async-resolve-works-1.png +0 -0
  129. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-starts-in-pending-state-1.png +0 -0
  130. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-failure-on-reject-1.png +0 -0
  131. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-success-on-resolve-1.png +0 -0
  132. package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-can-wrap-fetch-like-async-operations-1.png +0 -0
  133. package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-works-with-setTimeout-simulation-1.png +0 -0
  134. package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourceGet-tracks-dependencies-1.png +0 -0
  135. package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourcePeek-does-not-track-dependencies-1.png +0 -0
  136. package/tests/__screenshots__/resource.test.ts/Resource-API-resourceRefetch-refetch-resets-to-pending-and-re-runs-fetcher-1.png +0 -0
  137. package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-children-to-body-by-default-1.png +0 -0
  138. package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-to-selector-mount-target-1.png +0 -0
  139. package/tests/__screenshots__/solidjs-api.test.ts/SolidJS-API-compatibility-createEffect-tracks-dependencies-automatically-1.png +0 -0
  140. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-accessor-condition-in-Match-1.png +0 -0
  141. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-multiple-Match-components-1.png +0 -0
  142. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-single-Match-and-fallback-1.png +0 -0
  143. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-components-Switch-updates-DOM-when-signal-changes-1.png +0 -0
  144. package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-multiple-dependencies-1.png +0 -0
  145. package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-single-dependency-1.png +0 -0
  146. package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-with-defer-option-skips-initial-run-1.png +0 -0
  147. package/tests/__screenshots__/store.test.ts/createStore-Arrays-array-updates-work-1.png +0 -0
  148. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-only-triggers-when-accessed-property-changes-1.png +0 -0
  149. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-parent-path-change-notifies-child-accessors-1.png +0 -0
  150. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-nested-property-access-1.png +0 -0
  151. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-property-access-in-effects-1.png +0 -0
  152. package/tests/context.test.ts +118 -0
  153. package/tests/css-optimizer-extractors.test.ts +264 -0
  154. package/tests/css-optimizer-integration.test.ts +566 -0
  155. package/tests/css-optimizer-transformers.test.ts +301 -0
  156. package/tests/css-optimizer.test.ts +646 -0
  157. package/tests/css-runtime.bench.ts +442 -0
  158. package/tests/css-runtime.test.ts +342 -0
  159. package/tests/dom.test.ts +872 -0
  160. package/tests/integration.test.ts +405 -0
  161. package/tests/issue-5-for-infinite-loop.test.ts +516 -0
  162. package/tests/jsx-runtime.test.tsx +393 -0
  163. package/tests/lifecycle.test.ts +833 -0
  164. package/tests/move-before.bench.ts +304 -0
  165. package/tests/preact-signals-comparison.test.ts +1608 -0
  166. package/tests/resource.test.ts +160 -0
  167. package/tests/router.test.ts +117 -0
  168. package/tests/show-initial-mount-leak.test.tsx +182 -0
  169. package/tests/solidjs-api.test.ts +659 -0
  170. package/tests/static-perf.bench.ts +64 -0
  171. package/tests/store.test.ts +263 -0
  172. package/tests/tsx-syntax.test.tsx +404 -0
@@ -0,0 +1,1608 @@
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
+ });