@praxisjs/decorators 0.4.1 → 0.4.3

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 (63) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/__tests__/component.test.d.ts +2 -0
  3. package/dist/__tests__/component.test.d.ts.map +1 -0
  4. package/dist/__tests__/component.test.js +137 -0
  5. package/dist/__tests__/component.test.js.map +1 -0
  6. package/dist/__tests__/events.test.d.ts +2 -0
  7. package/dist/__tests__/events.test.d.ts.map +1 -0
  8. package/dist/__tests__/events.test.js +190 -0
  9. package/dist/__tests__/events.test.js.map +1 -0
  10. package/dist/__tests__/functions.test.d.ts +2 -0
  11. package/dist/__tests__/functions.test.d.ts.map +1 -0
  12. package/dist/__tests__/functions.test.js +427 -0
  13. package/dist/__tests__/functions.test.js.map +1 -0
  14. package/dist/__tests__/lazy.test.d.ts +2 -0
  15. package/dist/__tests__/lazy.test.d.ts.map +1 -0
  16. package/dist/__tests__/lazy.test.js +165 -0
  17. package/dist/__tests__/lazy.test.js.map +1 -0
  18. package/dist/__tests__/memo.test.d.ts +2 -0
  19. package/dist/__tests__/memo.test.d.ts.map +1 -0
  20. package/dist/__tests__/memo.test.js +79 -0
  21. package/dist/__tests__/memo.test.js.map +1 -0
  22. package/dist/__tests__/properties-browser.test.d.ts +2 -0
  23. package/dist/__tests__/properties-browser.test.d.ts.map +1 -0
  24. package/dist/__tests__/properties-browser.test.js +178 -0
  25. package/dist/__tests__/properties-browser.test.js.map +1 -0
  26. package/dist/__tests__/properties.test.d.ts +2 -0
  27. package/dist/__tests__/properties.test.d.ts.map +1 -0
  28. package/dist/__tests__/properties.test.js +571 -0
  29. package/dist/__tests__/properties.test.js.map +1 -0
  30. package/dist/__tests__/slot.test.d.ts +2 -0
  31. package/dist/__tests__/slot.test.d.ts.map +1 -0
  32. package/dist/__tests__/slot.test.js +115 -0
  33. package/dist/__tests__/slot.test.js.map +1 -0
  34. package/dist/__tests__/virtual.test.d.ts +2 -0
  35. package/dist/__tests__/virtual.test.d.ts.map +1 -0
  36. package/dist/__tests__/virtual.test.js +201 -0
  37. package/dist/__tests__/virtual.test.js.map +1 -0
  38. package/dist/component/lazy.d.ts +1 -1
  39. package/dist/component/lazy.d.ts.map +1 -1
  40. package/dist/component/lazy.js +4 -1
  41. package/dist/component/lazy.js.map +1 -1
  42. package/dist/component/virtual.d.ts +1 -1
  43. package/dist/component/virtual.d.ts.map +1 -1
  44. package/dist/functions/watch.d.ts.map +1 -1
  45. package/dist/functions/watch.js +10 -2
  46. package/dist/functions/watch.js.map +1 -1
  47. package/dist/properties/history.d.ts.map +1 -1
  48. package/dist/properties/history.js +4 -8
  49. package/dist/properties/history.js.map +1 -1
  50. package/package.json +2 -2
  51. package/src/__tests__/component.test.ts +156 -0
  52. package/src/__tests__/events.test.ts +236 -0
  53. package/src/__tests__/functions.test.ts +549 -0
  54. package/src/__tests__/lazy.test.ts +220 -0
  55. package/src/__tests__/memo.test.ts +90 -0
  56. package/src/__tests__/properties-browser.test.ts +234 -0
  57. package/src/__tests__/properties.test.ts +725 -0
  58. package/src/__tests__/slot.test.ts +141 -0
  59. package/src/__tests__/virtual.test.ts +241 -0
  60. package/src/component/lazy.ts +4 -2
  61. package/src/component/virtual.tsx +1 -1
  62. package/src/functions/watch.ts +11 -2
  63. package/src/properties/history.ts +4 -8
@@ -0,0 +1,220 @@
1
+ // @vitest-environment jsdom
2
+ import { beforeEach, afterEach, describe, it, expect, vi } from "vitest";
3
+
4
+ import { StatefulComponent } from "@praxisjs/core";
5
+ import { Lazy } from "../component/lazy";
6
+
7
+ type AnyConstructor = new (...args: unknown[]) => StatefulComponent;
8
+
9
+ function applyLazy<T extends AnyConstructor>(cls: T, placeholder?: number): T {
10
+ return placeholder !== undefined
11
+ ? Lazy(placeholder)(cls, {} as ClassDecoratorContext)
12
+ : Lazy()(cls, {} as ClassDecoratorContext);
13
+ }
14
+
15
+ class BaseComp extends StatefulComponent {
16
+ render() {
17
+ return document.createTextNode("content");
18
+ }
19
+ }
20
+
21
+ describe("Lazy decorator", () => {
22
+ let intersectionCallback: IntersectionObserverCallback = () => {};
23
+ let mockObserve: ReturnType<typeof vi.fn>;
24
+ let mockDisconnect: ReturnType<typeof vi.fn>;
25
+
26
+ beforeEach(() => {
27
+ intersectionCallback = () => {};
28
+ mockObserve = vi.fn();
29
+ mockDisconnect = vi.fn();
30
+
31
+ globalThis.IntersectionObserver = class {
32
+ constructor(cb: IntersectionObserverCallback) {
33
+ intersectionCallback = cb;
34
+ }
35
+ observe = mockObserve;
36
+ disconnect = mockDisconnect;
37
+ } as unknown as typeof IntersectionObserver;
38
+ });
39
+
40
+ afterEach(() => {
41
+ // @ts-expect-error – removing test stub
42
+ delete globalThis.IntersectionObserver;
43
+ });
44
+
45
+ it("returns null from render before visibility", () => {
46
+ const Wrapped = applyLazy(BaseComp as AnyConstructor);
47
+ const instance = new Wrapped();
48
+ expect(instance.render()).toBeNull();
49
+ });
50
+
51
+ it("sets _lazyVisible to true when IntersectionObserver is not supported", () => {
52
+ // Remove IntersectionObserver to simulate unsupported environment
53
+ // @ts-expect-error – removing test stub
54
+ delete globalThis.IntersectionObserver;
55
+
56
+ const Wrapped = applyLazy(BaseComp as AnyConstructor);
57
+ const instance = new Wrapped();
58
+
59
+ // Simulate anchor with parent element
60
+ const parent = document.createElement("div");
61
+ document.body.appendChild(parent);
62
+ const anchor = document.createComment("end");
63
+ parent.appendChild(anchor);
64
+ (instance as unknown as { _anchor: Comment })._anchor = anchor;
65
+
66
+ instance.onMount?.();
67
+ // Without IntersectionObserver, _lazyVisible should be set to true
68
+ const lazyVisible = (instance as unknown as { _lazyVisible: () => boolean })._lazyVisible;
69
+ expect(lazyVisible()).toBe(true);
70
+ document.body.removeChild(parent);
71
+ });
72
+
73
+ it("sets minHeight placeholder on mount", () => {
74
+ const Wrapped = applyLazy(BaseComp as AnyConstructor, 300);
75
+ const instance = new Wrapped();
76
+
77
+ const parent = document.createElement("div");
78
+ document.body.appendChild(parent);
79
+ const anchor = document.createComment("end");
80
+ parent.appendChild(anchor);
81
+ (instance as unknown as { _anchor: Comment })._anchor = anchor;
82
+
83
+ instance.onMount?.();
84
+ expect(parent.style.minHeight).toBe("300px");
85
+ document.body.removeChild(parent);
86
+ });
87
+
88
+ it("sets _lazyVisible and clears minHeight when intersecting", () => {
89
+ const Wrapped = applyLazy(BaseComp as AnyConstructor, 200);
90
+ const instance = new Wrapped();
91
+
92
+ const parent = document.createElement("div");
93
+ document.body.appendChild(parent);
94
+ const anchor = document.createComment("end");
95
+ parent.appendChild(anchor);
96
+ (instance as unknown as { _anchor: Comment })._anchor = anchor;
97
+
98
+ instance.onMount?.();
99
+ expect(instance.render()).toBeNull();
100
+
101
+ intersectionCallback(
102
+ [{ isIntersecting: true } as IntersectionObserverEntry],
103
+ null as unknown as IntersectionObserver,
104
+ );
105
+
106
+ expect(parent.style.minHeight).toBe("");
107
+ const lazyVisible = (instance as unknown as { _lazyVisible: () => boolean })._lazyVisible;
108
+ expect(lazyVisible()).toBe(true);
109
+ expect(mockDisconnect).toHaveBeenCalled();
110
+ document.body.removeChild(parent);
111
+ });
112
+
113
+ it("does nothing when _anchor has no parent", () => {
114
+ const Wrapped = applyLazy(BaseComp as AnyConstructor);
115
+ const instance = new Wrapped();
116
+
117
+ const anchor = document.createComment("end");
118
+ (instance as unknown as { _anchor: Comment })._anchor = anchor;
119
+
120
+ // Should not throw even though anchor has no parent
121
+ expect(() => instance.onMount?.()).not.toThrow();
122
+ });
123
+
124
+ it("disconnects observer on unmount", () => {
125
+ const Wrapped = applyLazy(BaseComp as AnyConstructor);
126
+ const instance = new Wrapped();
127
+
128
+ const parent = document.createElement("div");
129
+ document.body.appendChild(parent);
130
+ const anchor = document.createComment("end");
131
+ parent.appendChild(anchor);
132
+ (instance as unknown as { _anchor: Comment })._anchor = anchor;
133
+
134
+ instance.onMount?.();
135
+ instance.onUnmount?.();
136
+ expect(mockDisconnect).toHaveBeenCalled();
137
+ document.body.removeChild(parent);
138
+ });
139
+
140
+ it("onMount does nothing when _anchor is undefined", () => {
141
+ const Wrapped = applyLazy(BaseComp as AnyConstructor);
142
+ const instance = new Wrapped();
143
+ // _anchor is undefined by default
144
+ expect(() => instance.onMount?.()).not.toThrow();
145
+ });
146
+
147
+ it("render() returns original content after becoming visible", () => {
148
+ const Wrapped = applyLazy(BaseComp as AnyConstructor, 200);
149
+ const instance = new Wrapped();
150
+
151
+ const parent = document.createElement("div");
152
+ document.body.appendChild(parent);
153
+ const anchor = document.createComment("end");
154
+ parent.appendChild(anchor);
155
+ (instance as unknown as { _anchor: Comment })._anchor = anchor;
156
+
157
+ instance.onMount?.();
158
+ expect(instance.render()).toBeNull(); // not yet visible
159
+
160
+ intersectionCallback(
161
+ [{ isIntersecting: true } as IntersectionObserverEntry],
162
+ null as unknown as IntersectionObserver,
163
+ );
164
+
165
+ // Now visible: render() must delegate to the original render()
166
+ const result = instance.render();
167
+ expect(result).not.toBeNull();
168
+ document.body.removeChild(parent);
169
+ });
170
+
171
+ it("observer stays active and minHeight is preserved when entry is not intersecting", () => {
172
+ const Wrapped = applyLazy(BaseComp as AnyConstructor, 200);
173
+ const instance = new Wrapped();
174
+
175
+ const parent = document.createElement("div");
176
+ document.body.appendChild(parent);
177
+ const anchor = document.createComment("end");
178
+ parent.appendChild(anchor);
179
+ (instance as unknown as { _anchor: Comment })._anchor = anchor;
180
+
181
+ instance.onMount?.();
182
+
183
+ // Fire callback with isIntersecting=false — should be a no-op
184
+ intersectionCallback(
185
+ [{ isIntersecting: false } as IntersectionObserverEntry],
186
+ null as unknown as IntersectionObserver,
187
+ );
188
+
189
+ expect(instance.render()).toBeNull();
190
+ expect(mockDisconnect).not.toHaveBeenCalled();
191
+ expect(parent.style.minHeight).toBe("200px");
192
+ document.body.removeChild(parent);
193
+ });
194
+
195
+ it("does not re-apply minHeight on remount when already visible", () => {
196
+ const Wrapped = applyLazy(BaseComp as AnyConstructor, 300);
197
+ const instance = new Wrapped();
198
+
199
+ const parent = document.createElement("div");
200
+ document.body.appendChild(parent);
201
+ const anchor = document.createComment("end");
202
+ parent.appendChild(anchor);
203
+ (instance as unknown as { _anchor: Comment })._anchor = anchor;
204
+
205
+ // First mount — become visible
206
+ instance.onMount?.();
207
+ intersectionCallback(
208
+ [{ isIntersecting: true } as IntersectionObserverEntry],
209
+ null as unknown as IntersectionObserver,
210
+ );
211
+ instance.onUnmount?.();
212
+
213
+ // Second mount — _lazyVisible is already true
214
+ instance.onMount?.();
215
+
216
+ // minHeight must NOT be re-applied because _lazyVisible() is true
217
+ expect(parent.style.minHeight).toBe("");
218
+ document.body.removeChild(parent);
219
+ });
220
+ });
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+
3
+ import { signal } from "@praxisjs/core/internal";
4
+ import { Memo } from "../functions/memo";
5
+
6
+ function mockCtx(name: string) {
7
+ return { name, kind: "method" as const } as ClassMethodDecoratorContext;
8
+ }
9
+
10
+ describe("Memo", () => {
11
+ it("returns the computed value", () => {
12
+ const fn = vi.fn((x: unknown) => (x as number) * 2);
13
+ const wrapped = Memo()(fn, mockCtx("double"));
14
+ const obj = {};
15
+ expect(wrapped.call(obj, 5)).toBe(10);
16
+ });
17
+
18
+ it("caches result for same args — calls original only once", () => {
19
+ const fn = vi.fn((x: unknown) => (x as number) + 1);
20
+ const wrapped = Memo()(fn, mockCtx("inc"));
21
+ const obj = {};
22
+ wrapped.call(obj, 3);
23
+ wrapped.call(obj, 3);
24
+ wrapped.call(obj, 3);
25
+ expect(fn).toHaveBeenCalledTimes(1);
26
+ });
27
+
28
+ it("computes separately for different args", () => {
29
+ const fn = vi.fn((x: unknown) => (x as number) * 10);
30
+ const wrapped = Memo()(fn, mockCtx("mul"));
31
+ const obj = {};
32
+ expect(wrapped.call(obj, 2)).toBe(20);
33
+ expect(wrapped.call(obj, 3)).toBe(30);
34
+ expect(fn).toHaveBeenCalledTimes(2);
35
+ });
36
+
37
+ it("is per-instance — different instances have separate caches", () => {
38
+ const fn = vi.fn(() => Math.random());
39
+ const wrapped = Memo()(fn, mockCtx("rand"));
40
+ const a = {};
41
+ const b = {};
42
+ const v1 = wrapped.call(a);
43
+ const v2 = wrapped.call(b);
44
+ expect(fn).toHaveBeenCalledTimes(2);
45
+ expect(v1).not.toBe(v2); // different random values per instance
46
+ });
47
+
48
+ it("serializes no-args calls with __no_args__ key", () => {
49
+ const fn = vi.fn(() => 42);
50
+ const wrapped = Memo()(fn, mockCtx("noop"));
51
+ const obj = {};
52
+ wrapped.call(obj);
53
+ wrapped.call(obj);
54
+ expect(fn).toHaveBeenCalledTimes(1);
55
+ });
56
+
57
+ it("serializes object args via JSON.stringify", () => {
58
+ const fn = vi.fn((o: unknown) => JSON.stringify(o));
59
+ const wrapped = Memo()(fn, mockCtx("obj"));
60
+ const obj = {};
61
+ wrapped.call(obj, { a: 1 });
62
+ wrapped.call(obj, { a: 1 }); // same key
63
+ expect(fn).toHaveBeenCalledTimes(1);
64
+ });
65
+
66
+ it("re-evaluates when a reactive signal read inside the method changes", () => {
67
+ const s = signal(1);
68
+ const fn = vi.fn(() => s() * 10);
69
+ const wrapped = Memo()(fn, mockCtx("reactive"));
70
+ const obj = {};
71
+
72
+ expect(wrapped.call(obj)).toBe(10);
73
+ expect(fn).toHaveBeenCalledTimes(1);
74
+
75
+ s.set(5);
76
+ // The computed tracks s() — next read triggers recomputation
77
+ expect(wrapped.call(obj)).toBe(50);
78
+ expect(fn).toHaveBeenCalledTimes(2);
79
+ });
80
+
81
+ it("serializes symbol args via toString()", () => {
82
+ const fn = vi.fn((s: unknown) => String(s));
83
+ const wrapped = Memo()(fn, mockCtx("sym"));
84
+ const obj = {};
85
+ const sym = Symbol("test");
86
+ wrapped.call(obj, sym);
87
+ wrapped.call(obj, sym);
88
+ expect(fn).toHaveBeenCalledTimes(1);
89
+ });
90
+ });
@@ -0,0 +1,234 @@
1
+ // @vitest-environment jsdom
2
+ import { describe, it, expect, vi, beforeEach } from "vitest";
3
+
4
+ import { StatefulComponent } from "@praxisjs/core";
5
+
6
+ import { Persisted } from "../properties/persisted";
7
+ import { Slot, initSlots, getSlot } from "../properties/slot";
8
+
9
+ beforeEach(() => {
10
+ localStorage.clear();
11
+ });
12
+
13
+ // Helper: creates a ClassFieldDecoratorContext mock
14
+ function fieldCtx(name: string) {
15
+ const initializers: Array<(this: unknown) => void> = [];
16
+ return {
17
+ ctx: {
18
+ name,
19
+ kind: "field" as const,
20
+ addInitializer(fn: (this: unknown) => void) {
21
+ initializers.push(fn);
22
+ },
23
+ } as ClassFieldDecoratorContext,
24
+ run(instance: unknown) {
25
+ initializers.forEach((fn) => { fn.call(instance); });
26
+ },
27
+ };
28
+ }
29
+
30
+ // Minimal concrete component
31
+ class TestComponent extends StatefulComponent {
32
+ render() { return null; }
33
+ }
34
+
35
+ // ── Persisted ─────────────────────────────────────────────────────────────────
36
+
37
+ describe("Persisted decorator", () => {
38
+ it("reads back the initial value when nothing is stored", () => {
39
+ const { ctx, run } = fieldCtx("theme");
40
+ Persisted<string>("theme")(undefined, ctx);
41
+
42
+ const instance = new TestComponent();
43
+ (instance as unknown as Record<string, unknown>).theme = "light";
44
+ run(instance);
45
+
46
+ expect((instance as unknown as Record<string, unknown>).theme).toBe("light");
47
+ });
48
+
49
+ it("persists set value to localStorage", () => {
50
+ const { ctx, run } = fieldCtx("lang");
51
+ Persisted<string>("lang")(undefined, ctx);
52
+
53
+ const instance = new TestComponent();
54
+ (instance as unknown as Record<string, unknown>).lang = "en";
55
+ run(instance);
56
+
57
+ (instance as unknown as Record<string, unknown>).lang = "pt";
58
+ expect(localStorage.getItem("lang")).toBe('"pt"');
59
+ expect((instance as unknown as Record<string, unknown>).lang).toBe("pt");
60
+ });
61
+
62
+ it("reads existing localStorage value over the initialValue", () => {
63
+ localStorage.setItem("color", '"red"');
64
+ const { ctx, run } = fieldCtx("color");
65
+ Persisted<string>("color")(undefined, ctx);
66
+
67
+ const instance = new TestComponent();
68
+ (instance as unknown as Record<string, unknown>).color = "blue";
69
+ run(instance);
70
+
71
+ expect((instance as unknown as Record<string, unknown>).color).toBe("red");
72
+ });
73
+
74
+ it("uses the property name as storage key when no explicit key given", () => {
75
+ const { ctx, run } = fieldCtx("count");
76
+ Persisted<number>()(undefined, ctx);
77
+
78
+ const instance = new TestComponent();
79
+ (instance as unknown as Record<string, unknown>).count = 0;
80
+ run(instance);
81
+
82
+ (instance as unknown as Record<string, unknown>).count = 7;
83
+ expect(localStorage.getItem("count")).toBe("7");
84
+ });
85
+
86
+ it("returns separate values per instance", () => {
87
+ const { ctx: ctxA, run: runA } = fieldCtx("val");
88
+ Persisted<number>("valA")(undefined, ctxA);
89
+
90
+ const { ctx: ctxB, run: runB } = fieldCtx("val");
91
+ Persisted<number>("valB")(undefined, ctxB);
92
+
93
+ const a = new TestComponent();
94
+ (a as unknown as Record<string, unknown>).val = 1;
95
+ runA(a);
96
+
97
+ const b = new TestComponent();
98
+ (b as unknown as Record<string, unknown>).val = 2;
99
+ runB(b);
100
+
101
+ (a as unknown as Record<string, unknown>).val = 10;
102
+ (b as unknown as Record<string, unknown>).val = 20;
103
+
104
+ expect((a as unknown as Record<string, unknown>).val).toBe(10);
105
+ expect((b as unknown as Record<string, unknown>).val).toBe(20);
106
+ });
107
+ });
108
+
109
+ // ── Slot ──────────────────────────────────────────────────────────────────────
110
+
111
+ describe("Slot decorator", () => {
112
+ it("returns an empty array before initSlots is called", () => {
113
+ const { ctx, run } = fieldCtx("default");
114
+ Slot()(undefined, ctx);
115
+
116
+ const instance = new TestComponent();
117
+ run(instance);
118
+
119
+ expect((instance as unknown as Record<string, unknown>).default).toEqual([]);
120
+ });
121
+
122
+ it("returns default slot children after initSlots", () => {
123
+ const { ctx, run } = fieldCtx("default");
124
+ Slot()(undefined, ctx);
125
+
126
+ const instance = new TestComponent();
127
+ run(instance);
128
+
129
+ const el = document.createElement("span");
130
+ initSlots(instance, [el]);
131
+
132
+ expect((instance as unknown as Record<string, unknown>).default).toContain(el);
133
+ });
134
+
135
+ it("resolves named slot children", () => {
136
+ const { ctx, run } = fieldCtx("header");
137
+ Slot("header")(undefined, ctx);
138
+
139
+ const instance = new TestComponent();
140
+ run(instance);
141
+
142
+ const el = document.createElement("div");
143
+ el.setAttribute("slot", "header");
144
+ initSlots(instance, [el]);
145
+
146
+ const result = (instance as unknown as Record<string, unknown>).header as unknown[];
147
+ expect(result).toHaveLength(1);
148
+ expect(result[0]).toBe(el);
149
+ });
150
+
151
+ it("named slot element has slot attribute removed", () => {
152
+ const { ctx, run } = fieldCtx("footer");
153
+ Slot("footer")(undefined, ctx);
154
+
155
+ const instance = new TestComponent();
156
+ run(instance);
157
+
158
+ const el = document.createElement("div");
159
+ el.setAttribute("slot", "footer");
160
+ initSlots(instance, [el]);
161
+
162
+ expect(el.getAttribute("slot")).toBeNull();
163
+ });
164
+
165
+ it("warns when attempting to set a slot directly (dev mode)", () => {
166
+ const originalEnv = process.env.NODE_ENV;
167
+ process.env.NODE_ENV = "development";
168
+
169
+ const { ctx, run } = fieldCtx("content");
170
+ Slot("content")(undefined, ctx);
171
+
172
+ const instance = new TestComponent();
173
+ run(instance);
174
+
175
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
176
+ (instance as unknown as Record<string, unknown>).content = ["something"];
177
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining("[Slot]"));
178
+ warn.mockRestore();
179
+
180
+ process.env.NODE_ENV = originalEnv;
181
+ });
182
+
183
+ it("uses property name as slot name when no name given and prop is not 'default'", () => {
184
+ const { ctx, run } = fieldCtx("sidebar");
185
+ Slot()(undefined, ctx);
186
+
187
+ const instance = new TestComponent();
188
+ run(instance);
189
+
190
+ const el = document.createElement("aside");
191
+ el.setAttribute("slot", "sidebar");
192
+ initSlots(instance, [el]);
193
+
194
+ const result = (instance as unknown as Record<string, unknown>).sidebar as unknown[];
195
+ expect(result).toHaveLength(1);
196
+ });
197
+ });
198
+
199
+ // ── initSlots / getSlot helpers ───────────────────────────────────────────────
200
+
201
+ describe("initSlots / getSlot", () => {
202
+ it("splits children into named and default slots", () => {
203
+ const named = document.createElement("div");
204
+ named.setAttribute("slot", "header");
205
+
206
+ const plain = document.createElement("p");
207
+
208
+ const instance = {};
209
+ initSlots(instance, [named, plain]);
210
+
211
+ expect(getSlot(instance, "header")).toContain(named);
212
+ expect(getSlot(instance, "default")).toContain(plain);
213
+ });
214
+
215
+ it("handles null children gracefully", () => {
216
+ const instance = {};
217
+ initSlots(instance, null);
218
+ expect(getSlot(instance, "default")).toEqual([]);
219
+ });
220
+
221
+ it("flattens nested arrays", () => {
222
+ const a = document.createElement("span");
223
+ const b = document.createElement("span");
224
+ const instance = {};
225
+ initSlots(instance, [[a, b]]);
226
+ expect(getSlot(instance, "default")).toHaveLength(2);
227
+ });
228
+
229
+ it("returns empty array for unknown slot", () => {
230
+ const instance = {};
231
+ initSlots(instance, []);
232
+ expect(getSlot(instance, "nonexistent")).toEqual([]);
233
+ });
234
+ });