@luna_ui/luna 0.3.3 → 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.
- package/dist/cli.mjs +1264 -27
- package/dist/css/index.d.ts +194 -0
- package/dist/css/index.js +721 -0
- package/dist/css/runtime.d.ts +92 -0
- package/dist/css/runtime.js +179 -0
- package/dist/index.js +1 -1
- package/dist/jsx-dev-runtime.js +1 -1
- package/dist/jsx-runtime.d.ts +5 -0
- package/dist/jsx-runtime.js +1 -1
- package/dist/src-CHiGeWfy.js +1 -0
- package/dist/vite-plugin.d.ts +122 -0
- package/dist/vite-plugin.js +1518 -0
- package/package.json +16 -2
- package/src/css/extract.ts +798 -0
- package/src/css/index.ts +10 -0
- package/src/css/inject.ts +205 -0
- package/src/css/inline.ts +182 -0
- package/src/css/minify.ts +70 -0
- package/src/css/optimizer.ts +6 -0
- package/src/css/runtime.ts +344 -0
- package/src/css-optimizer/README.md +353 -0
- package/src/css-optimizer/cooccurrence.ts +100 -0
- package/src/css-optimizer/core.ts +263 -0
- package/src/css-optimizer/extractors.ts +243 -0
- package/src/css-optimizer/hash.ts +54 -0
- package/src/css-optimizer/index.ts +129 -0
- package/src/css-optimizer/merge.ts +109 -0
- package/src/css-optimizer/moonbit-analyzer.ts +210 -0
- package/src/css-optimizer/parser.ts +120 -0
- package/src/css-optimizer/pattern.ts +171 -0
- package/src/css-optimizer/transformers.ts +301 -0
- package/src/css-optimizer/types.ts +128 -0
- package/src/event-utils.ts +227 -0
- package/src/index.ts +890 -0
- package/src/jsx-dev-runtime.ts +2 -0
- package/src/jsx-runtime.ts +398 -0
- package/src/vite-plugin.ts +718 -0
- package/tests/__screenshots__/context.test.ts/Context-API-context-with-reactive-effects-context-value-accessible-in-effect-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-For-component--SolidJS-style--For-updates-when-signal-changes-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-accepts-children-as-function-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-toggles-visibility-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-attribute-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-style-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-createElementNs--SVG-support--createElementNs-with-dynamic-attribute-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-effect-with-DOM-effect-tracks-signal-changes-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-clear-to-empty-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-empty-array-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-removes-items-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-renders-initial-list-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-updates-when-items-change-1.png +0 -0
- 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
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-handles-reordering-in-SVG-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-updates-SVG-elements-when-signal-changes-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-with-nested-SVG-groups-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-ref-callback--JSX-style--ref-callback-with-nested-elements-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-creates-a-node-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-with-false-condition-creates-placeholder-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-text-nodes-textDyn-creates-reactive-text-node-1.png +0 -0
- 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
- 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
- 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
- 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
- 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
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Context---ForEach-integration-forEach-items-can-access-context-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-renders-initial-list-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-updates-when-signal-changes-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-with-object-items-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-hides-when-condition-is-false-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-renders-when-condition-is-true-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-from-false-to-true-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-reactively-1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--event-listener-pattern--Solid-js-docs-example--1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--multiple-cleanups-in-component-body--LIFO-order--1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--onCleanup-in-component-body-runs-on-unmount-1.png +0 -0
- 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
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--timer-cleanup-pattern--Solid-js-style--1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Effects-effect-cleanup-runs-before-re-run-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-large-list-update-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-nested-batch-operations-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-rapid-sequential-updates-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-Show-component---visible-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-show-hide-element---visible-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-fragment-with-list-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-nested-fragments-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-simple-fragment-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-conditional-toggle-updates-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-addition-updates-match-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-removal-updates-match-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-text-updates-match-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-className-updates-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-style-updates-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-multiple-dynamic-attributes-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-deeply-nested-conditionals-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-empty-to-populated-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-populated-to-empty-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-cleanup-order-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-with-inner-signal-change-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-is-called-when-effect-re-runs-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-with-resource-simulation-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-multiple-children--no-wrapper--1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-no-children-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-fragment-with-list-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-nested-Fragments-work-correctly-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-complex-reordering-with-additions-and-removals-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-insert-in-middle-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-remove-from-middle-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-reverse-list-order-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-shuffle-list-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-Show-component-renders-when-condition-is-true-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-renders-content-when-initially-true-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-toggles-visibility-dynamically-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Memo-Dependency-Chain-conditional-memo-dependencies-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-basic-signal-get-set-produces-same-values-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-batch-updates-produce-same-final-values-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-peek-reads-value-without-tracking-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-selective-tracking-with-untrack-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-untrack-prevents-dependency-tracking-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API--SolidJS-style--reactivity-accessor-is-reactive-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-empty-string-for-non-failure-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-undefined-for-non-failure-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsFailure-and-stateError-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsPending-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsSuccess-and-stateValue-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateValue-returns-undefined-for-non-success-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-reject-transitions-to-failure-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-resolve-transitions-to-success-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-returns-resource--resolve--and-reject-functions-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-starts-in-pending-state-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-async-resolve-works-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-starts-in-pending-state-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-failure-on-reject-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-success-on-resolve-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-can-wrap-fetch-like-async-operations-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-works-with-setTimeout-simulation-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourceGet-tracks-dependencies-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourcePeek-does-not-track-dependencies-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-resourceRefetch-refetch-resets-to-pending-and-re-runs-fetcher-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-children-to-body-by-default-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-to-selector-mount-target-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/SolidJS-API-compatibility-createEffect-tracks-dependencies-automatically-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-accessor-condition-in-Match-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-multiple-Match-components-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-single-Match-and-fallback-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-components-Switch-updates-DOM-when-signal-changes-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-multiple-dependencies-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-single-dependency-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-with-defer-option-skips-initial-run-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Arrays-array-updates-work-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Reactivity-only-triggers-when-accessed-property-changes-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Reactivity-parent-path-change-notifies-child-accessors-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-nested-property-access-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-property-access-in-effects-1.png +0 -0
- package/tests/context.test.ts +118 -0
- package/tests/css-optimizer-extractors.test.ts +264 -0
- package/tests/css-optimizer-integration.test.ts +566 -0
- package/tests/css-optimizer-transformers.test.ts +301 -0
- package/tests/css-optimizer.test.ts +646 -0
- package/tests/css-runtime.bench.ts +442 -0
- package/tests/css-runtime.test.ts +342 -0
- package/tests/dom.test.ts +872 -0
- package/tests/integration.test.ts +405 -0
- package/tests/issue-5-for-infinite-loop.test.ts +516 -0
- package/tests/jsx-runtime.test.tsx +393 -0
- package/tests/lifecycle.test.ts +833 -0
- package/tests/move-before.bench.ts +304 -0
- package/tests/preact-signals-comparison.test.ts +1608 -0
- package/tests/resource.test.ts +160 -0
- package/tests/router.test.ts +117 -0
- package/tests/show-initial-mount-leak.test.tsx +182 -0
- package/tests/solidjs-api.test.ts +659 -0
- package/tests/static-perf.bench.ts +64 -0
- package/tests/store.test.ts +263 -0
- package/tests/tsx-syntax.test.tsx +404 -0
- package/dist/src-DGWY0NYx.js +0 -1
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lifecycle tests for onMount and onCleanup
|
|
3
|
+
* Based on Solid.js semantics
|
|
4
|
+
*
|
|
5
|
+
* Expected behavior:
|
|
6
|
+
* - onMount runs AFTER DOM is created and refs are bound
|
|
7
|
+
* - onCleanup registered inside onMount should run when component unmounts
|
|
8
|
+
* - onCleanup registered inside effects should run when effect re-runs or component unmounts
|
|
9
|
+
*/
|
|
10
|
+
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
11
|
+
import {
|
|
12
|
+
createSignal,
|
|
13
|
+
createEffect,
|
|
14
|
+
createRenderEffect,
|
|
15
|
+
createRoot,
|
|
16
|
+
onMount,
|
|
17
|
+
onCleanup,
|
|
18
|
+
createElement,
|
|
19
|
+
text,
|
|
20
|
+
textDyn,
|
|
21
|
+
render,
|
|
22
|
+
show,
|
|
23
|
+
Show,
|
|
24
|
+
For,
|
|
25
|
+
} from "../src/index";
|
|
26
|
+
|
|
27
|
+
// MoonBit AttrValue constructors
|
|
28
|
+
const AttrValue = {
|
|
29
|
+
Static: (value: string) => ({ $tag: 0, _0: value }),
|
|
30
|
+
Dynamic: (getter: () => string) => ({ $tag: 1, _0: getter }),
|
|
31
|
+
Handler: (handler: (e: unknown) => void) => ({ $tag: 2, _0: handler }),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function attr(name: string, value: unknown) {
|
|
35
|
+
return { _0: name, _1: value };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Helper to wait for microtasks to complete
|
|
39
|
+
const flushMicrotasks = () => new Promise<void>(resolve => queueMicrotask(resolve));
|
|
40
|
+
|
|
41
|
+
describe("onMount Basic Behavior", () => {
|
|
42
|
+
let container: HTMLElement;
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
container = document.createElement("div");
|
|
46
|
+
document.body.appendChild(container);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
container.remove();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("onMount runs after component creation", async () => {
|
|
54
|
+
const log: string[] = [];
|
|
55
|
+
|
|
56
|
+
function Component() {
|
|
57
|
+
log.push("component function");
|
|
58
|
+
onMount(() => {
|
|
59
|
+
log.push("onMount");
|
|
60
|
+
});
|
|
61
|
+
log.push("before return");
|
|
62
|
+
return createElement("div", [], [text("content")]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
createRoot(() => {
|
|
66
|
+
render(container, Component());
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Wait for microtasks (onMount deferred execution)
|
|
70
|
+
await flushMicrotasks();
|
|
71
|
+
|
|
72
|
+
// onMount should run after component function completes
|
|
73
|
+
expect(log).toContain("component function");
|
|
74
|
+
expect(log).toContain("before return");
|
|
75
|
+
expect(log).toContain("onMount");
|
|
76
|
+
|
|
77
|
+
// Order: component function -> before return -> onMount
|
|
78
|
+
const componentIdx = log.indexOf("component function");
|
|
79
|
+
const beforeReturnIdx = log.indexOf("before return");
|
|
80
|
+
const onMountIdx = log.indexOf("onMount");
|
|
81
|
+
|
|
82
|
+
expect(componentIdx).toBeLessThan(beforeReturnIdx);
|
|
83
|
+
expect(beforeReturnIdx).toBeLessThan(onMountIdx);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("onMount has access to DOM refs", async () => {
|
|
87
|
+
let refElement: HTMLElement | null = null;
|
|
88
|
+
let mountElement: HTMLElement | null = null;
|
|
89
|
+
|
|
90
|
+
function Component() {
|
|
91
|
+
onMount(() => {
|
|
92
|
+
mountElement = refElement;
|
|
93
|
+
});
|
|
94
|
+
return createElement(
|
|
95
|
+
"div",
|
|
96
|
+
[attr("__ref", AttrValue.Handler((el: any) => { refElement = el; }))],
|
|
97
|
+
[text("content")]
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
createRoot(() => {
|
|
102
|
+
render(container, Component());
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Wait for microtasks
|
|
106
|
+
await flushMicrotasks();
|
|
107
|
+
|
|
108
|
+
// By the time onMount runs, ref should be available
|
|
109
|
+
expect(refElement).not.toBeNull();
|
|
110
|
+
expect(mountElement).not.toBeNull();
|
|
111
|
+
expect(mountElement).toBe(refElement);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("ref is bound before onMount runs", async () => {
|
|
115
|
+
const log: string[] = [];
|
|
116
|
+
|
|
117
|
+
function Component() {
|
|
118
|
+
onMount(() => {
|
|
119
|
+
log.push("onMount");
|
|
120
|
+
});
|
|
121
|
+
return createElement(
|
|
122
|
+
"p",
|
|
123
|
+
[attr("__ref", AttrValue.Handler(() => { log.push("ref"); }))],
|
|
124
|
+
[text("content")]
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
createRoot(() => {
|
|
129
|
+
render(container, Component());
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Wait for microtasks
|
|
133
|
+
await flushMicrotasks();
|
|
134
|
+
|
|
135
|
+
// ref should be called before onMount
|
|
136
|
+
expect(log).toEqual(["ref", "onMount"]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("multiple onMount calls run in order", async () => {
|
|
140
|
+
const log: string[] = [];
|
|
141
|
+
|
|
142
|
+
function Component() {
|
|
143
|
+
onMount(() => log.push("first"));
|
|
144
|
+
onMount(() => log.push("second"));
|
|
145
|
+
onMount(() => log.push("third"));
|
|
146
|
+
return createElement("div", [], [text("content")]);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
createRoot(() => {
|
|
150
|
+
render(container, Component());
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Wait for microtasks
|
|
154
|
+
await flushMicrotasks();
|
|
155
|
+
|
|
156
|
+
expect(log).toEqual(["first", "second", "third"]);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("onCleanup with onMount", () => {
|
|
161
|
+
let container: HTMLElement;
|
|
162
|
+
|
|
163
|
+
beforeEach(() => {
|
|
164
|
+
container = document.createElement("div");
|
|
165
|
+
document.body.appendChild(container);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
afterEach(() => {
|
|
169
|
+
container.remove();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("onCleanup inside onMount runs on unmount", async () => {
|
|
173
|
+
const log: string[] = [];
|
|
174
|
+
const [visible, setVisible] = createSignal(true);
|
|
175
|
+
|
|
176
|
+
function Child() {
|
|
177
|
+
onMount(() => {
|
|
178
|
+
log.push("mounted");
|
|
179
|
+
onCleanup(() => {
|
|
180
|
+
log.push("cleanup");
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
return createElement("p", [], [text("I'm here")]);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
createRoot(() => {
|
|
187
|
+
render(
|
|
188
|
+
container,
|
|
189
|
+
createElement("div", [], [
|
|
190
|
+
// Use function children so Child is created inside the owner scope
|
|
191
|
+
Show({ when: visible, children: () => Child() }),
|
|
192
|
+
])
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Wait for microtasks
|
|
197
|
+
await flushMicrotasks();
|
|
198
|
+
|
|
199
|
+
expect(log).toEqual(["mounted"]);
|
|
200
|
+
expect(container.querySelector("p")).not.toBeNull();
|
|
201
|
+
|
|
202
|
+
// Unmount the child
|
|
203
|
+
setVisible(false);
|
|
204
|
+
|
|
205
|
+
// Wait for microtasks (in case cleanup is async)
|
|
206
|
+
await flushMicrotasks();
|
|
207
|
+
|
|
208
|
+
expect(log).toEqual(["mounted", "cleanup"]);
|
|
209
|
+
expect(container.querySelector("p")).toBeNull();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("onCleanup works with show() primitive", async () => {
|
|
213
|
+
const log: string[] = [];
|
|
214
|
+
const [visible, setVisible] = createSignal(true);
|
|
215
|
+
|
|
216
|
+
function Child() {
|
|
217
|
+
onMount(() => {
|
|
218
|
+
log.push("mounted");
|
|
219
|
+
onCleanup(() => {
|
|
220
|
+
log.push("cleanup");
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
return createElement("span", [], [text("visible")]);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
createRoot(() => {
|
|
227
|
+
render(
|
|
228
|
+
container,
|
|
229
|
+
createElement("div", [], [
|
|
230
|
+
show(visible, () => Child()),
|
|
231
|
+
])
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Wait for microtasks
|
|
236
|
+
await flushMicrotasks();
|
|
237
|
+
|
|
238
|
+
expect(log).toEqual(["mounted"]);
|
|
239
|
+
|
|
240
|
+
setVisible(false);
|
|
241
|
+
await flushMicrotasks();
|
|
242
|
+
expect(log).toEqual(["mounted", "cleanup"]);
|
|
243
|
+
|
|
244
|
+
// Re-show should mount again
|
|
245
|
+
setVisible(true);
|
|
246
|
+
await flushMicrotasks();
|
|
247
|
+
expect(log).toEqual(["mounted", "cleanup", "mounted"]);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("nested cleanup order (LIFO)", async () => {
|
|
251
|
+
const log: string[] = [];
|
|
252
|
+
const [visible, setVisible] = createSignal(true);
|
|
253
|
+
|
|
254
|
+
function Child() {
|
|
255
|
+
onMount(() => {
|
|
256
|
+
onCleanup(() => log.push("first registered"));
|
|
257
|
+
onCleanup(() => log.push("second registered"));
|
|
258
|
+
onCleanup(() => log.push("third registered"));
|
|
259
|
+
});
|
|
260
|
+
return createElement("div", [], [text("child")]);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
createRoot(() => {
|
|
264
|
+
render(
|
|
265
|
+
container,
|
|
266
|
+
// Use function children so Child is created inside the owner scope
|
|
267
|
+
Show({ when: visible, children: () => Child() })
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Wait for microtasks
|
|
272
|
+
await flushMicrotasks();
|
|
273
|
+
|
|
274
|
+
setVisible(false);
|
|
275
|
+
await flushMicrotasks();
|
|
276
|
+
|
|
277
|
+
// Cleanups run in reverse order (LIFO)
|
|
278
|
+
expect(log).toEqual([
|
|
279
|
+
"third registered",
|
|
280
|
+
"second registered",
|
|
281
|
+
"first registered",
|
|
282
|
+
]);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe("onCleanup in Effects", () => {
|
|
287
|
+
test("render effect cleanup runs before re-run (synchronous)", () => {
|
|
288
|
+
const log: string[] = [];
|
|
289
|
+
const [count, setCount] = createSignal(0);
|
|
290
|
+
|
|
291
|
+
createRoot(() => {
|
|
292
|
+
// Use createRenderEffect for synchronous testing
|
|
293
|
+
createRenderEffect(() => {
|
|
294
|
+
const current = count();
|
|
295
|
+
log.push(`effect run: ${current}`);
|
|
296
|
+
onCleanup(() => {
|
|
297
|
+
log.push(`cleanup: ${current}`);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
expect(log).toEqual(["effect run: 0"]);
|
|
303
|
+
|
|
304
|
+
setCount(1);
|
|
305
|
+
expect(log).toEqual(["effect run: 0", "cleanup: 0", "effect run: 1"]);
|
|
306
|
+
|
|
307
|
+
setCount(2);
|
|
308
|
+
expect(log).toEqual([
|
|
309
|
+
"effect run: 0",
|
|
310
|
+
"cleanup: 0",
|
|
311
|
+
"effect run: 1",
|
|
312
|
+
"cleanup: 1",
|
|
313
|
+
"effect run: 2",
|
|
314
|
+
]);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("deferred effect: initial run is deferred, re-runs are sync", async () => {
|
|
318
|
+
const log: string[] = [];
|
|
319
|
+
const [count, setCount] = createSignal(0);
|
|
320
|
+
|
|
321
|
+
createRoot(() => {
|
|
322
|
+
// createEffect is deferred for initial run
|
|
323
|
+
createEffect(() => {
|
|
324
|
+
const current = count();
|
|
325
|
+
log.push(`effect run: ${current}`);
|
|
326
|
+
onCleanup(() => {
|
|
327
|
+
log.push(`cleanup: ${current}`);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Effect hasn't run yet (initial run is deferred)
|
|
333
|
+
expect(log).toEqual([]);
|
|
334
|
+
|
|
335
|
+
// Wait for microtask - initial run happens
|
|
336
|
+
await flushMicrotasks();
|
|
337
|
+
expect(log).toEqual(["effect run: 0"]);
|
|
338
|
+
|
|
339
|
+
// Subsequent updates trigger synchronous re-runs (after effect is established)
|
|
340
|
+
setCount(1);
|
|
341
|
+
expect(log).toEqual(["effect run: 0", "cleanup: 0", "effect run: 1"]);
|
|
342
|
+
|
|
343
|
+
setCount(2);
|
|
344
|
+
expect(log).toEqual([
|
|
345
|
+
"effect run: 0",
|
|
346
|
+
"cleanup: 0",
|
|
347
|
+
"effect run: 1",
|
|
348
|
+
"cleanup: 1",
|
|
349
|
+
"effect run: 2",
|
|
350
|
+
]);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe("Timer Cleanup Pattern (from docs)", () => {
|
|
355
|
+
let container: HTMLElement;
|
|
356
|
+
|
|
357
|
+
beforeEach(() => {
|
|
358
|
+
container = document.createElement("div");
|
|
359
|
+
document.body.appendChild(container);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
afterEach(() => {
|
|
363
|
+
container.remove();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("timer is properly cleaned up on unmount", async () => {
|
|
367
|
+
const [visible, setVisible] = createSignal(true);
|
|
368
|
+
let intervalCleared = false;
|
|
369
|
+
|
|
370
|
+
function Timer() {
|
|
371
|
+
const [count, setCount] = createSignal(0);
|
|
372
|
+
|
|
373
|
+
onMount(() => {
|
|
374
|
+
// This is the pattern from the docs - onCleanup inside onMount
|
|
375
|
+
onCleanup(() => {
|
|
376
|
+
intervalCleared = true;
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
return createElement("p", [], [textDyn(() => `Count: ${count()}`)]);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
createRoot(() => {
|
|
384
|
+
render(
|
|
385
|
+
container,
|
|
386
|
+
// Use function children so Timer is created inside the owner scope
|
|
387
|
+
Show({ when: visible, children: () => Timer() })
|
|
388
|
+
);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
await flushMicrotasks();
|
|
392
|
+
|
|
393
|
+
expect(intervalCleared).toBe(false);
|
|
394
|
+
|
|
395
|
+
setVisible(false);
|
|
396
|
+
await flushMicrotasks();
|
|
397
|
+
|
|
398
|
+
expect(intervalCleared).toBe(true);
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
describe("Event Listener Cleanup Pattern (from docs)", () => {
|
|
403
|
+
let container: HTMLElement;
|
|
404
|
+
|
|
405
|
+
beforeEach(() => {
|
|
406
|
+
container = document.createElement("div");
|
|
407
|
+
document.body.appendChild(container);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
afterEach(() => {
|
|
411
|
+
container.remove();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("event listener is removed on unmount", async () => {
|
|
415
|
+
const [visible, setVisible] = createSignal(true);
|
|
416
|
+
const addedListeners: string[] = [];
|
|
417
|
+
const removedListeners: string[] = [];
|
|
418
|
+
|
|
419
|
+
// Mock document.addEventListener/removeEventListener
|
|
420
|
+
const originalAdd = document.addEventListener;
|
|
421
|
+
const originalRemove = document.removeEventListener;
|
|
422
|
+
|
|
423
|
+
document.addEventListener = (type: string) => {
|
|
424
|
+
addedListeners.push(type);
|
|
425
|
+
};
|
|
426
|
+
document.removeEventListener = (type: string) => {
|
|
427
|
+
removedListeners.push(type);
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
function KeyboardHandler() {
|
|
431
|
+
onMount(() => {
|
|
432
|
+
const handler = (e: KeyboardEvent) => {};
|
|
433
|
+
document.addEventListener("keydown", handler as any);
|
|
434
|
+
|
|
435
|
+
onCleanup(() => {
|
|
436
|
+
document.removeEventListener("keydown", handler as any);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
return createElement("div", [], [text("Press Escape to close")]);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
createRoot(() => {
|
|
444
|
+
render(
|
|
445
|
+
container,
|
|
446
|
+
// Use function children so KeyboardHandler is created inside the owner scope
|
|
447
|
+
Show({ when: visible, children: () => KeyboardHandler() })
|
|
448
|
+
);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
await flushMicrotasks();
|
|
452
|
+
|
|
453
|
+
expect(addedListeners).toEqual(["keydown"]);
|
|
454
|
+
expect(removedListeners).toEqual([]);
|
|
455
|
+
|
|
456
|
+
setVisible(false);
|
|
457
|
+
await flushMicrotasks();
|
|
458
|
+
|
|
459
|
+
expect(removedListeners).toEqual(["keydown"]);
|
|
460
|
+
|
|
461
|
+
// Restore
|
|
462
|
+
document.addEventListener = originalAdd;
|
|
463
|
+
document.removeEventListener = originalRemove;
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
describe("Third-Party Library Pattern (from docs)", () => {
|
|
468
|
+
let container: HTMLElement;
|
|
469
|
+
|
|
470
|
+
beforeEach(() => {
|
|
471
|
+
container = document.createElement("div");
|
|
472
|
+
document.body.appendChild(container);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
afterEach(() => {
|
|
476
|
+
container.remove();
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("library is initialized after DOM ready and cleaned up on unmount", async () => {
|
|
480
|
+
const log: string[] = [];
|
|
481
|
+
const [visible, setVisible] = createSignal(true);
|
|
482
|
+
|
|
483
|
+
// Mock chart library
|
|
484
|
+
class ChartLibrary {
|
|
485
|
+
constructor(container: HTMLElement, options: any) {
|
|
486
|
+
log.push(`init: ${container.tagName}`);
|
|
487
|
+
}
|
|
488
|
+
destroy() {
|
|
489
|
+
log.push("destroy");
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function Chart() {
|
|
494
|
+
let containerRef: HTMLElement | null = null;
|
|
495
|
+
|
|
496
|
+
onMount(() => {
|
|
497
|
+
if (containerRef) {
|
|
498
|
+
const chart = new ChartLibrary(containerRef, {});
|
|
499
|
+
onCleanup(() => {
|
|
500
|
+
chart.destroy();
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
return createElement(
|
|
506
|
+
"div",
|
|
507
|
+
[
|
|
508
|
+
attr("className", AttrValue.Static("chart-container")),
|
|
509
|
+
attr("__ref", AttrValue.Handler((el: any) => { containerRef = el; })),
|
|
510
|
+
],
|
|
511
|
+
[]
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
createRoot(() => {
|
|
516
|
+
render(
|
|
517
|
+
container,
|
|
518
|
+
// Use function children so Chart is created inside the owner scope
|
|
519
|
+
Show({ when: visible, children: () => Chart() })
|
|
520
|
+
);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
await flushMicrotasks();
|
|
524
|
+
|
|
525
|
+
// Chart should be initialized with the container element
|
|
526
|
+
expect(log).toEqual(["init: DIV"]);
|
|
527
|
+
|
|
528
|
+
setVisible(false);
|
|
529
|
+
await flushMicrotasks();
|
|
530
|
+
|
|
531
|
+
expect(log).toEqual(["init: DIV", "destroy"]);
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
describe("For loop with cleanup", () => {
|
|
536
|
+
let container: HTMLElement;
|
|
537
|
+
|
|
538
|
+
beforeEach(() => {
|
|
539
|
+
container = document.createElement("div");
|
|
540
|
+
document.body.appendChild(container);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
afterEach(() => {
|
|
544
|
+
container.remove();
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("items are cleaned up when removed from list", async () => {
|
|
548
|
+
const cleanups: string[] = [];
|
|
549
|
+
const [items, setItems] = createSignal(["a", "b", "c"]);
|
|
550
|
+
|
|
551
|
+
function Item({ id }: { id: string }) {
|
|
552
|
+
onMount(() => {
|
|
553
|
+
onCleanup(() => {
|
|
554
|
+
cleanups.push(id);
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
return createElement("li", [], [text(id)]);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
createRoot(() => {
|
|
561
|
+
render(
|
|
562
|
+
container,
|
|
563
|
+
createElement("ul", [], [
|
|
564
|
+
For({
|
|
565
|
+
each: items,
|
|
566
|
+
children: (item: string) => Item({ id: item }),
|
|
567
|
+
}),
|
|
568
|
+
])
|
|
569
|
+
);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
await flushMicrotasks();
|
|
573
|
+
|
|
574
|
+
expect(container.querySelectorAll("li").length).toBe(3);
|
|
575
|
+
expect(cleanups).toEqual([]);
|
|
576
|
+
|
|
577
|
+
// Remove one item
|
|
578
|
+
setItems(["a", "c"]);
|
|
579
|
+
await flushMicrotasks();
|
|
580
|
+
|
|
581
|
+
expect(container.querySelectorAll("li").length).toBe(2);
|
|
582
|
+
// "b" should be cleaned up
|
|
583
|
+
expect(cleanups).toContain("b");
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// ============================================================================
|
|
588
|
+
// Solid.js Style: onCleanup in Component Body (not inside onMount)
|
|
589
|
+
// ============================================================================
|
|
590
|
+
describe("onCleanup in Component Body (Solid.js style)", () => {
|
|
591
|
+
let container: HTMLElement;
|
|
592
|
+
|
|
593
|
+
beforeEach(() => {
|
|
594
|
+
container = document.createElement("div");
|
|
595
|
+
document.body.appendChild(container);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
afterEach(() => {
|
|
599
|
+
container.remove();
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
test("onCleanup in component body runs on unmount", async () => {
|
|
603
|
+
const log: string[] = [];
|
|
604
|
+
const [visible, setVisible] = createSignal(true);
|
|
605
|
+
|
|
606
|
+
// Solid.js style: onCleanup directly in component body
|
|
607
|
+
function Child() {
|
|
608
|
+
log.push("component created");
|
|
609
|
+
|
|
610
|
+
// This is the Solid.js pattern - cleanup in component body, not in onMount
|
|
611
|
+
onCleanup(() => {
|
|
612
|
+
log.push("cleanup");
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
return createElement("p", [], [text("I'm here")]);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
createRoot(() => {
|
|
619
|
+
render(
|
|
620
|
+
container,
|
|
621
|
+
Show({ when: visible, children: () => Child() })
|
|
622
|
+
);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
// No microtask wait needed - component body runs synchronously
|
|
626
|
+
expect(log).toEqual(["component created"]);
|
|
627
|
+
expect(container.querySelector("p")).not.toBeNull();
|
|
628
|
+
|
|
629
|
+
// Unmount the child
|
|
630
|
+
setVisible(false);
|
|
631
|
+
|
|
632
|
+
expect(log).toEqual(["component created", "cleanup"]);
|
|
633
|
+
expect(container.querySelector("p")).toBeNull();
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
test("timer cleanup pattern (Solid.js style)", async () => {
|
|
637
|
+
const [visible, setVisible] = createSignal(true);
|
|
638
|
+
let timerCleared = false;
|
|
639
|
+
|
|
640
|
+
// Solid.js docs example pattern
|
|
641
|
+
function Timer() {
|
|
642
|
+
const [count, setCount] = createSignal(0);
|
|
643
|
+
|
|
644
|
+
// Setup side effect directly in component
|
|
645
|
+
const interval = setInterval(() => {
|
|
646
|
+
setCount(c => c + 1);
|
|
647
|
+
}, 1000);
|
|
648
|
+
|
|
649
|
+
// Cleanup registered directly in component body
|
|
650
|
+
onCleanup(() => {
|
|
651
|
+
clearInterval(interval);
|
|
652
|
+
timerCleared = true;
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
return createElement("p", [], [textDyn(() => `Count: ${count()}`)]);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
createRoot(() => {
|
|
659
|
+
render(
|
|
660
|
+
container,
|
|
661
|
+
Show({ when: visible, children: () => Timer() })
|
|
662
|
+
);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
expect(timerCleared).toBe(false);
|
|
666
|
+
|
|
667
|
+
setVisible(false);
|
|
668
|
+
|
|
669
|
+
expect(timerCleared).toBe(true);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test("event listener pattern (Solid.js docs example)", async () => {
|
|
673
|
+
const [visible, setVisible] = createSignal(true);
|
|
674
|
+
const addedListeners: string[] = [];
|
|
675
|
+
const removedListeners: string[] = [];
|
|
676
|
+
|
|
677
|
+
// Mock document.addEventListener/removeEventListener
|
|
678
|
+
const originalAdd = document.addEventListener;
|
|
679
|
+
const originalRemove = document.removeEventListener;
|
|
680
|
+
|
|
681
|
+
document.addEventListener = (type: string) => {
|
|
682
|
+
addedListeners.push(type);
|
|
683
|
+
};
|
|
684
|
+
document.removeEventListener = (type: string) => {
|
|
685
|
+
removedListeners.push(type);
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
// Exact pattern from Solid.js docs
|
|
689
|
+
function ClickCounter() {
|
|
690
|
+
const [count, setCount] = createSignal(0);
|
|
691
|
+
const handleClick = () => setCount(value => value + 1);
|
|
692
|
+
|
|
693
|
+
document.addEventListener("click", handleClick as any);
|
|
694
|
+
|
|
695
|
+
onCleanup(() => {
|
|
696
|
+
document.removeEventListener("click", handleClick as any);
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
return createElement("main", [], [
|
|
700
|
+
textDyn(() => `Document has been clicked ${count()} times`)
|
|
701
|
+
]);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
createRoot(() => {
|
|
705
|
+
render(
|
|
706
|
+
container,
|
|
707
|
+
Show({ when: visible, children: () => ClickCounter() })
|
|
708
|
+
);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
expect(addedListeners).toEqual(["click"]);
|
|
712
|
+
expect(removedListeners).toEqual([]);
|
|
713
|
+
|
|
714
|
+
setVisible(false);
|
|
715
|
+
|
|
716
|
+
expect(removedListeners).toEqual(["click"]);
|
|
717
|
+
|
|
718
|
+
// Restore
|
|
719
|
+
document.addEventListener = originalAdd;
|
|
720
|
+
document.removeEventListener = originalRemove;
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
test("multiple cleanups in component body (LIFO order)", async () => {
|
|
724
|
+
const log: string[] = [];
|
|
725
|
+
const [visible, setVisible] = createSignal(true);
|
|
726
|
+
|
|
727
|
+
function Child() {
|
|
728
|
+
onCleanup(() => log.push("first registered"));
|
|
729
|
+
onCleanup(() => log.push("second registered"));
|
|
730
|
+
onCleanup(() => log.push("third registered"));
|
|
731
|
+
|
|
732
|
+
return createElement("div", [], [text("child")]);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
createRoot(() => {
|
|
736
|
+
render(
|
|
737
|
+
container,
|
|
738
|
+
Show({ when: visible, children: () => Child() })
|
|
739
|
+
);
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
setVisible(false);
|
|
743
|
+
|
|
744
|
+
// Cleanups run in reverse order (LIFO)
|
|
745
|
+
expect(log).toEqual([
|
|
746
|
+
"third registered",
|
|
747
|
+
"second registered",
|
|
748
|
+
"first registered",
|
|
749
|
+
]);
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
test("onCleanup works with For loop items (component body style)", async () => {
|
|
753
|
+
const cleanups: string[] = [];
|
|
754
|
+
const [items, setItems] = createSignal(["a", "b", "c"]);
|
|
755
|
+
|
|
756
|
+
function Item({ id }: { id: string }) {
|
|
757
|
+
// Solid.js style - cleanup in component body
|
|
758
|
+
onCleanup(() => {
|
|
759
|
+
cleanups.push(id);
|
|
760
|
+
});
|
|
761
|
+
return createElement("li", [], [text(id)]);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
createRoot(() => {
|
|
765
|
+
render(
|
|
766
|
+
container,
|
|
767
|
+
createElement("ul", [], [
|
|
768
|
+
For({
|
|
769
|
+
each: items,
|
|
770
|
+
children: (item: string) => Item({ id: item }),
|
|
771
|
+
}),
|
|
772
|
+
])
|
|
773
|
+
);
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
expect(container.querySelectorAll("li").length).toBe(3);
|
|
777
|
+
expect(cleanups).toEqual([]);
|
|
778
|
+
|
|
779
|
+
// Remove one item
|
|
780
|
+
setItems(["a", "c"]);
|
|
781
|
+
|
|
782
|
+
expect(container.querySelectorAll("li").length).toBe(2);
|
|
783
|
+
expect(cleanups).toContain("b");
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
test("combining onMount and onCleanup in component body", async () => {
|
|
787
|
+
const log: string[] = [];
|
|
788
|
+
const [visible, setVisible] = createSignal(true);
|
|
789
|
+
|
|
790
|
+
function Child() {
|
|
791
|
+
log.push("component body start");
|
|
792
|
+
|
|
793
|
+
// Cleanup in component body (runs on unmount)
|
|
794
|
+
onCleanup(() => {
|
|
795
|
+
log.push("body cleanup");
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
// onMount for DOM-dependent setup
|
|
799
|
+
onMount(() => {
|
|
800
|
+
log.push("mounted");
|
|
801
|
+
// Cleanup inside onMount (also runs on unmount)
|
|
802
|
+
onCleanup(() => {
|
|
803
|
+
log.push("mount cleanup");
|
|
804
|
+
});
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
log.push("component body end");
|
|
808
|
+
return createElement("div", [], [text("content")]);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
createRoot(() => {
|
|
812
|
+
render(
|
|
813
|
+
container,
|
|
814
|
+
Show({ when: visible, children: () => Child() })
|
|
815
|
+
);
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
await flushMicrotasks();
|
|
819
|
+
|
|
820
|
+
expect(log).toEqual([
|
|
821
|
+
"component body start",
|
|
822
|
+
"component body end",
|
|
823
|
+
"mounted"
|
|
824
|
+
]);
|
|
825
|
+
|
|
826
|
+
setVisible(false);
|
|
827
|
+
await flushMicrotasks();
|
|
828
|
+
|
|
829
|
+
// Both cleanups should run
|
|
830
|
+
expect(log).toContain("body cleanup");
|
|
831
|
+
expect(log).toContain("mount cleanup");
|
|
832
|
+
});
|
|
833
|
+
});
|