@praxisjs/runtime 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/dist/children.d.ts.map +1 -1
- package/dist/children.js +2 -1
- package/dist/children.js.map +1 -1
- package/dist/component.d.ts +2 -3
- package/dist/component.d.ts.map +1 -1
- package/dist/component.js +133 -60
- package/dist/component.js.map +1 -1
- package/dist/context.d.ts +13 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +24 -2
- package/dist/context.js.map +1 -1
- package/dist/dom/constants.d.ts +1 -1
- package/dist/dom/constants.d.ts.map +1 -1
- package/dist/dom/constants.js +24 -83
- package/dist/dom/constants.js.map +1 -1
- package/dist/dom/create.d.ts +1 -1
- package/dist/dom/create.d.ts.map +1 -1
- package/dist/dom/create.js +3 -3
- package/dist/dom/create.js.map +1 -1
- package/dist/dom/events.d.ts +28 -0
- package/dist/dom/events.d.ts.map +1 -1
- package/dist/dom/events.js +163 -17
- package/dist/dom/events.js.map +1 -1
- package/dist/dom/props.d.ts +4 -0
- package/dist/dom/props.d.ts.map +1 -1
- package/dist/dom/props.js +55 -7
- package/dist/dom/props.js.map +1 -1
- package/dist/dom/range.d.ts +4 -0
- package/dist/dom/range.d.ts.map +1 -0
- package/dist/dom/range.js +43 -0
- package/dist/dom/range.js.map +1 -0
- package/dist/dom/remove.d.ts.map +1 -1
- package/dist/dom/remove.js +2 -8
- package/dist/dom/remove.js.map +1 -1
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +7 -5
- package/dist/element.js.map +1 -1
- package/dist/hmr.d.ts +35 -0
- package/dist/hmr.d.ts.map +1 -0
- package/dist/hmr.js +85 -0
- package/dist/hmr.js.map +1 -0
- package/dist/hydration-context.d.ts +9 -1
- package/dist/hydration-context.d.ts.map +1 -1
- package/dist/hydration-context.js +34 -13
- package/dist/hydration-context.js.map +1 -1
- package/dist/hydration.d.ts.map +1 -1
- package/dist/hydration.js +4 -11
- package/dist/hydration.js.map +1 -1
- package/dist/index.d.ts +16 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -7
- package/dist/index.js.map +1 -1
- package/dist/list.d.ts +72 -0
- package/dist/list.d.ts.map +1 -0
- package/dist/list.js +786 -0
- package/dist/list.js.map +1 -0
- package/dist/portal.d.ts.map +1 -1
- package/dist/portal.js +3 -0
- package/dist/portal.js.map +1 -1
- package/dist/reactive.d.ts +2 -2
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +109 -34
- package/dist/reactive.js.map +1 -1
- package/dist/scope.d.ts +14 -4
- package/dist/scope.d.ts.map +1 -1
- package/dist/scope.js +55 -15
- package/dist/scope.js.map +1 -1
- package/dist/template.d.ts +48 -0
- package/dist/template.d.ts.map +1 -0
- package/dist/template.js +270 -0
- package/dist/template.js.map +1 -0
- package/package.json +15 -6
- package/src/children.ts +2 -1
- package/src/component.ts +135 -63
- package/src/context.ts +35 -4
- package/src/dom/constants.ts +25 -82
- package/src/dom/create.ts +3 -3
- package/src/dom/events.ts +184 -16
- package/src/dom/props.ts +65 -8
- package/src/dom/range.ts +45 -0
- package/src/dom/remove.ts +3 -7
- package/src/element.ts +8 -6
- package/src/hmr.ts +110 -0
- package/src/hydration-context.ts +34 -13
- package/src/hydration.ts +4 -9
- package/src/index.ts +54 -7
- package/src/list.ts +886 -0
- package/src/portal.ts +3 -0
- package/src/reactive.ts +147 -40
- package/src/scope.ts +63 -16
- package/src/template.ts +271 -0
- package/dist/__tests__/children.test.d.ts +0 -2
- package/dist/__tests__/children.test.d.ts.map +0 -1
- package/dist/__tests__/children.test.js +0 -241
- package/dist/__tests__/children.test.js.map +0 -1
- package/dist/__tests__/component.test.d.ts +0 -2
- package/dist/__tests__/component.test.d.ts.map +0 -1
- package/dist/__tests__/component.test.js +0 -347
- package/dist/__tests__/component.test.js.map +0 -1
- package/dist/__tests__/context.test.d.ts +0 -2
- package/dist/__tests__/context.test.d.ts.map +0 -1
- package/dist/__tests__/context.test.js +0 -111
- package/dist/__tests__/context.test.js.map +0 -1
- package/dist/__tests__/dom.test.d.ts +0 -2
- package/dist/__tests__/dom.test.d.ts.map +0 -1
- package/dist/__tests__/dom.test.js +0 -429
- package/dist/__tests__/dom.test.js.map +0 -1
- package/dist/__tests__/hydration.test.d.ts +0 -2
- package/dist/__tests__/hydration.test.d.ts.map +0 -1
- package/dist/__tests__/hydration.test.js +0 -299
- package/dist/__tests__/hydration.test.js.map +0 -1
- package/dist/__tests__/portal.test.d.ts +0 -2
- package/dist/__tests__/portal.test.d.ts.map +0 -1
- package/dist/__tests__/portal.test.js +0 -157
- package/dist/__tests__/portal.test.js.map +0 -1
- package/dist/__tests__/remove.test.d.ts +0 -2
- package/dist/__tests__/remove.test.d.ts.map +0 -1
- package/dist/__tests__/remove.test.js +0 -45
- package/dist/__tests__/remove.test.js.map +0 -1
- package/dist/__tests__/render.test.d.ts +0 -2
- package/dist/__tests__/render.test.d.ts.map +0 -1
- package/dist/__tests__/render.test.js +0 -97
- package/dist/__tests__/render.test.js.map +0 -1
- package/dist/__tests__/scope.test.d.ts +0 -2
- package/dist/__tests__/scope.test.d.ts.map +0 -1
- package/dist/__tests__/scope.test.js +0 -110
- package/dist/__tests__/scope.test.js.map +0 -1
- package/src/__tests__/children.test.ts +0 -276
- package/src/__tests__/component.test.ts +0 -381
- package/src/__tests__/context.test.ts +0 -128
- package/src/__tests__/dom.test.ts +0 -483
- package/src/__tests__/hydration.test.ts +0 -360
- package/src/__tests__/portal.test.ts +0 -177
- package/src/__tests__/remove.test.ts +0 -51
- package/src/__tests__/render.test.ts +0 -119
- package/src/__tests__/scope.test.ts +0 -120
- package/tsconfig.json +0 -8
|
@@ -1,483 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { describe, it, expect, vi } from "vitest";
|
|
3
|
-
|
|
4
|
-
import { signal } from "@praxisjs/core/internal";
|
|
5
|
-
|
|
6
|
-
import { createElement } from "../dom/create";
|
|
7
|
-
import { addEvent } from "../dom/events";
|
|
8
|
-
import { applyProp } from "../dom/props";
|
|
9
|
-
import { Scope } from "../scope";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// ── createElement ────────────────────────────────────────────────────────────
|
|
13
|
-
|
|
14
|
-
describe("createElement", () => {
|
|
15
|
-
it("creates an HTMLElement for regular tags", () => {
|
|
16
|
-
const el = createElement("div");
|
|
17
|
-
expect(el).toBeInstanceOf(HTMLDivElement);
|
|
18
|
-
expect(el.tagName.toLowerCase()).toBe("div");
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("creates an SVGElement for svg tags", () => {
|
|
22
|
-
const el = createElement("svg");
|
|
23
|
-
expect(el).toBeInstanceOf(SVGElement);
|
|
24
|
-
expect(el.tagName.toLowerCase()).toBe("svg");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("creates nested svg elements with the SVG namespace", () => {
|
|
28
|
-
const path = createElement("path");
|
|
29
|
-
expect(path.namespaceURI).toBe("http://www.w3.org/2000/svg");
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// ── applyProp ────────────────────────────────────────────────────────────────
|
|
34
|
-
|
|
35
|
-
describe("applyProp", () => {
|
|
36
|
-
it("sets class attribute", () => {
|
|
37
|
-
const el = document.createElement("div");
|
|
38
|
-
const scope = new Scope();
|
|
39
|
-
applyProp(el, "class", "foo bar", scope);
|
|
40
|
-
expect(el.getAttribute("class")).toBe("foo bar");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("sets className as class attribute", () => {
|
|
44
|
-
const el = document.createElement("div");
|
|
45
|
-
const scope = new Scope();
|
|
46
|
-
applyProp(el, "className", "my-class", scope);
|
|
47
|
-
expect(el.getAttribute("class")).toBe("my-class");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("removes class when value is null", () => {
|
|
51
|
-
const el = document.createElement("div");
|
|
52
|
-
el.setAttribute("class", "old");
|
|
53
|
-
const scope = new Scope();
|
|
54
|
-
applyProp(el, "class", null, scope);
|
|
55
|
-
expect(el.hasAttribute("class")).toBe(false);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("sets style as string", () => {
|
|
59
|
-
const el = document.createElement("div");
|
|
60
|
-
const scope = new Scope();
|
|
61
|
-
applyProp(el, "style", "color: red;", scope);
|
|
62
|
-
expect(el.getAttribute("style")).toBe("color: red;");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("sets style as object", () => {
|
|
66
|
-
const el = document.createElement("div") as HTMLElement;
|
|
67
|
-
const scope = new Scope();
|
|
68
|
-
applyProp(el, "style", { color: "blue" }, scope);
|
|
69
|
-
expect(el.style.color).toBe("blue");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("sets boolean true as empty attribute", () => {
|
|
73
|
-
const el = document.createElement("input");
|
|
74
|
-
const scope = new Scope();
|
|
75
|
-
applyProp(el, "disabled", true, scope);
|
|
76
|
-
expect(el.hasAttribute("disabled")).toBe(true);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("removes attribute when value is false", () => {
|
|
80
|
-
const el = document.createElement("input");
|
|
81
|
-
el.setAttribute("disabled", "");
|
|
82
|
-
const scope = new Scope();
|
|
83
|
-
applyProp(el, "disabled", false, scope);
|
|
84
|
-
expect(el.hasAttribute("disabled")).toBe(false);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("sets value prop directly on element", () => {
|
|
88
|
-
const el = document.createElement("input");
|
|
89
|
-
const scope = new Scope();
|
|
90
|
-
applyProp(el, "value", "hello", scope);
|
|
91
|
-
expect(el.value).toBe("hello");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("calls ref callback with the element", () => {
|
|
95
|
-
const el = document.createElement("span");
|
|
96
|
-
const scope = new Scope();
|
|
97
|
-
const ref = vi.fn();
|
|
98
|
-
applyProp(el, "ref", ref, scope);
|
|
99
|
-
expect(ref).toHaveBeenCalledWith(el);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("skips 'children' and 'key' props", () => {
|
|
103
|
-
const el = document.createElement("div");
|
|
104
|
-
const scope = new Scope();
|
|
105
|
-
applyProp(el, "children", "should be ignored", scope);
|
|
106
|
-
applyProp(el, "key", "k1", scope);
|
|
107
|
-
expect(el.childNodes.length).toBe(0);
|
|
108
|
-
expect(el.hasAttribute("key")).toBe(false);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("attaches event listener for onClick", () => {
|
|
112
|
-
const el = document.createElement("button");
|
|
113
|
-
const scope = new Scope();
|
|
114
|
-
const handler = vi.fn();
|
|
115
|
-
applyProp(el, "onClick", handler, scope);
|
|
116
|
-
el.dispatchEvent(new MouseEvent("click"));
|
|
117
|
-
expect(handler).toHaveBeenCalledOnce();
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("removes event listener on scope dispose", () => {
|
|
121
|
-
const el = document.createElement("button");
|
|
122
|
-
const scope = new Scope();
|
|
123
|
-
const handler = vi.fn();
|
|
124
|
-
applyProp(el, "onClick", handler, scope);
|
|
125
|
-
scope.dispose();
|
|
126
|
-
el.dispatchEvent(new MouseEvent("click"));
|
|
127
|
-
expect(handler).not.toHaveBeenCalled();
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it("tracks reactive prop — updates when signal changes", () => {
|
|
131
|
-
const el = document.createElement("div");
|
|
132
|
-
const scope = new Scope();
|
|
133
|
-
const cls = signal("initial");
|
|
134
|
-
applyProp(el, "class", () => cls(), scope);
|
|
135
|
-
expect(el.getAttribute("class")).toBe("initial");
|
|
136
|
-
cls.set("updated");
|
|
137
|
-
expect(el.getAttribute("class")).toBe("updated");
|
|
138
|
-
scope.dispose();
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("normalizes htmlFor to for attribute", () => {
|
|
142
|
-
const label = document.createElement("label");
|
|
143
|
-
const scope = new Scope();
|
|
144
|
-
applyProp(label, "htmlFor", "my-input", scope);
|
|
145
|
-
expect(label.getAttribute("for")).toBe("my-input");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("removes style attribute when value is null", () => {
|
|
149
|
-
const el = document.createElement("div");
|
|
150
|
-
el.setAttribute("style", "color: red;");
|
|
151
|
-
const scope = new Scope();
|
|
152
|
-
applyProp(el, "style", null, scope);
|
|
153
|
-
expect(el.hasAttribute("style")).toBe(false);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("sets boolean true as empty string for non-VALUE_PROPS attributes (applyAttr path)", () => {
|
|
157
|
-
const el = document.createElement("div");
|
|
158
|
-
const scope = new Scope();
|
|
159
|
-
applyProp(el, "aria-expanded", true, scope);
|
|
160
|
-
expect(el.getAttribute("aria-expanded")).toBe("");
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("removes style attribute when value is undefined", () => {
|
|
164
|
-
const el = document.createElement("div");
|
|
165
|
-
el.setAttribute("style", "color: red;");
|
|
166
|
-
const scope = new Scope();
|
|
167
|
-
applyProp(el, "style", undefined, scope);
|
|
168
|
-
expect(el.hasAttribute("style")).toBe(false);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("removes attribute when value is null for generic attr", () => {
|
|
172
|
-
const el = document.createElement("div");
|
|
173
|
-
el.setAttribute("data-x", "1");
|
|
174
|
-
const scope = new Scope();
|
|
175
|
-
applyProp(el, "data-x", null, scope);
|
|
176
|
-
expect(el.hasAttribute("data-x")).toBe(false);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it("removes attribute when value is undefined for generic attr", () => {
|
|
180
|
-
const el = document.createElement("div");
|
|
181
|
-
el.setAttribute("data-y", "1");
|
|
182
|
-
const scope = new Scope();
|
|
183
|
-
applyProp(el, "data-y", undefined, scope);
|
|
184
|
-
expect(el.hasAttribute("data-y")).toBe(false);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("data-x attribute applied via setAttribute not as DOM property", () => {
|
|
188
|
-
const el = document.createElement("div");
|
|
189
|
-
const scope = new Scope();
|
|
190
|
-
applyProp(el, "data-x", "foo", scope);
|
|
191
|
-
expect(el.getAttribute("data-x")).toBe("foo");
|
|
192
|
-
// should not be set as a direct property
|
|
193
|
-
expect((el as unknown as Record<string, unknown>)["data-x"]).toBeUndefined();
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("aria-label applied via setAttribute", () => {
|
|
197
|
-
const el = document.createElement("button");
|
|
198
|
-
const scope = new Scope();
|
|
199
|
-
applyProp(el, "aria-label", "close", scope);
|
|
200
|
-
expect(el.getAttribute("aria-label")).toBe("close");
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it("style object with CSS variable { \"--color\": \"red\" } — verify it's applied", () => {
|
|
204
|
-
const el = document.createElement("div") as HTMLElement;
|
|
205
|
-
const scope = new Scope();
|
|
206
|
-
applyProp(el, "style", { "--color": "red" }, scope);
|
|
207
|
-
expect(el.style.getPropertyValue("--color")).toBe("red");
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("reactive prop going from a value to null — attribute is removed", () => {
|
|
211
|
-
const el = document.createElement("div");
|
|
212
|
-
const scope = new Scope();
|
|
213
|
-
const cls = signal<string | null>("visible");
|
|
214
|
-
applyProp(el, "class", () => cls(), scope);
|
|
215
|
-
expect(el.getAttribute("class")).toBe("visible");
|
|
216
|
-
cls.set(null);
|
|
217
|
-
expect(el.hasAttribute("class")).toBe(false);
|
|
218
|
-
scope.dispose();
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it("boolean false — attribute is removed (not set to \"false\")", () => {
|
|
222
|
-
const el = document.createElement("div");
|
|
223
|
-
el.setAttribute("data-active", "true");
|
|
224
|
-
const scope = new Scope();
|
|
225
|
-
applyProp(el, "data-active", false, scope);
|
|
226
|
-
expect(el.hasAttribute("data-active")).toBe(false);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it("sets muted prop directly on a media element (no hardcoded list needed)", () => {
|
|
230
|
-
const el = document.createElement("video");
|
|
231
|
-
const scope = new Scope();
|
|
232
|
-
applyProp(el, "muted", true, scope);
|
|
233
|
-
expect(el.muted).toBe(true);
|
|
234
|
-
// the muted *attribute* only sets defaultMuted — it should stay untouched
|
|
235
|
-
expect(el.hasAttribute("muted")).toBe(false);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it("tracks reactive muted — keeps working after user interaction dirties the element", () => {
|
|
239
|
-
const el = document.createElement("video");
|
|
240
|
-
const scope = new Scope();
|
|
241
|
-
const muted = signal(false);
|
|
242
|
-
applyProp(el, "muted", () => muted(), scope);
|
|
243
|
-
el.muted = true; // simulate the user/browser changing live state directly
|
|
244
|
-
muted.set(true);
|
|
245
|
-
expect(el.muted).toBe(true);
|
|
246
|
-
muted.set(false);
|
|
247
|
-
expect(el.muted).toBe(false);
|
|
248
|
-
scope.dispose();
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it("does not write SVG geometry props as DOM properties (read-only accessors would throw)", () => {
|
|
252
|
-
const el = createElement("rect") as SVGRectElement;
|
|
253
|
-
const scope = new Scope();
|
|
254
|
-
expect(() => { applyProp(el, "width", "50", scope); }).not.toThrow();
|
|
255
|
-
expect(el.getAttribute("width")).toBe("50");
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it("falls back to setAttribute for read-only reference properties like 'list'", () => {
|
|
259
|
-
const el = document.createElement("input");
|
|
260
|
-
const scope = new Scope();
|
|
261
|
-
expect(() => { applyProp(el, "list", "my-datalist", scope); }).not.toThrow();
|
|
262
|
-
expect(el.getAttribute("list")).toBe("my-datalist");
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it("removes attribute for a two-way-reflected prop when value is undefined", () => {
|
|
266
|
-
const el = document.createElement("div");
|
|
267
|
-
el.id = "old-id";
|
|
268
|
-
const scope = new Scope();
|
|
269
|
-
applyProp(el, "id", undefined, scope);
|
|
270
|
-
expect(el.hasAttribute("id")).toBe(false);
|
|
271
|
-
});
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
// ── addEvent ─────────────────────────────────────────────────────────────────
|
|
275
|
-
|
|
276
|
-
describe("addEvent", () => {
|
|
277
|
-
it("adds event listener that fires on dispatch", () => {
|
|
278
|
-
const el = document.createElement("button");
|
|
279
|
-
const scope = new Scope();
|
|
280
|
-
const fn = vi.fn();
|
|
281
|
-
addEvent(el, "click", fn, scope);
|
|
282
|
-
el.dispatchEvent(new MouseEvent("click"));
|
|
283
|
-
expect(fn).toHaveBeenCalledOnce();
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
it("removes listener when scope is disposed", () => {
|
|
287
|
-
const el = document.createElement("button");
|
|
288
|
-
const scope = new Scope();
|
|
289
|
-
const fn = vi.fn();
|
|
290
|
-
addEvent(el, "click", fn, scope);
|
|
291
|
-
scope.dispose();
|
|
292
|
-
el.dispatchEvent(new MouseEvent("click"));
|
|
293
|
-
expect(fn).not.toHaveBeenCalled();
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
it("event handler runs and scope cleanup still works afterwards", () => {
|
|
297
|
-
// Verify that after a handler fires, the scope can still be disposed cleanly.
|
|
298
|
-
// (Throwing inside an event handler becomes an uncaught global exception in jsdom,
|
|
299
|
-
// so we verify the invariant without triggering that path.)
|
|
300
|
-
const el = document.createElement("button");
|
|
301
|
-
const scope = new Scope();
|
|
302
|
-
const handler = vi.fn();
|
|
303
|
-
const cleanup = vi.fn();
|
|
304
|
-
addEvent(el, "click", handler, scope);
|
|
305
|
-
scope.add(cleanup);
|
|
306
|
-
el.dispatchEvent(new MouseEvent("click"));
|
|
307
|
-
expect(handler).toHaveBeenCalledOnce();
|
|
308
|
-
expect(() => scope.dispose()).not.toThrow();
|
|
309
|
-
expect(cleanup).toHaveBeenCalled();
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it("same handler registered twice on same element/event — both fire (browser behavior)", () => {
|
|
313
|
-
// Note: addEventListener deduplicates identical handler+options combos;
|
|
314
|
-
// addEvent is a thin wrapper so each call registers independently.
|
|
315
|
-
// Two separate scopes / two separate registrations = two calls.
|
|
316
|
-
const el = document.createElement("button");
|
|
317
|
-
const scope = new Scope();
|
|
318
|
-
const fn = vi.fn();
|
|
319
|
-
addEvent(el, "click", fn, scope);
|
|
320
|
-
addEvent(el, "click", fn, scope);
|
|
321
|
-
el.dispatchEvent(new MouseEvent("click"));
|
|
322
|
-
// addEventListener with the same listener reference is a no-op per spec
|
|
323
|
-
expect(fn).toHaveBeenCalledOnce();
|
|
324
|
-
scope.dispose();
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it("batches signal writes made by a handler so effects run once", () => {
|
|
328
|
-
const el = document.createElement("button");
|
|
329
|
-
const scope = new Scope();
|
|
330
|
-
const a = signal(0);
|
|
331
|
-
const b = signal(0);
|
|
332
|
-
const runs = vi.fn();
|
|
333
|
-
scope.effect(() => {
|
|
334
|
-
void a();
|
|
335
|
-
void b();
|
|
336
|
-
runs();
|
|
337
|
-
});
|
|
338
|
-
runs.mockClear();
|
|
339
|
-
addEvent(el, "click", () => {
|
|
340
|
-
a.set(1);
|
|
341
|
-
b.set(1);
|
|
342
|
-
}, scope);
|
|
343
|
-
el.dispatchEvent(new MouseEvent("click"));
|
|
344
|
-
expect(runs).toHaveBeenCalledOnce();
|
|
345
|
-
scope.dispose();
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
it("calls the handler with the element as this and forwards the event", () => {
|
|
349
|
-
const el = document.createElement("button");
|
|
350
|
-
const scope = new Scope();
|
|
351
|
-
let seenThis: unknown;
|
|
352
|
-
let seenEvent: Event | undefined;
|
|
353
|
-
addEvent(el, "click", function (this: unknown, e: Event) {
|
|
354
|
-
seenThis = this;
|
|
355
|
-
seenEvent = e;
|
|
356
|
-
}, scope);
|
|
357
|
-
const event = new MouseEvent("click");
|
|
358
|
-
el.dispatchEvent(event);
|
|
359
|
-
expect(seenThis).toBe(el);
|
|
360
|
-
expect(seenEvent).toBe(event);
|
|
361
|
-
scope.dispose();
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
it("registers the same handler on different elements independently", () => {
|
|
365
|
-
const a = document.createElement("button");
|
|
366
|
-
const b = document.createElement("button");
|
|
367
|
-
const scope = new Scope();
|
|
368
|
-
const fn = vi.fn();
|
|
369
|
-
addEvent(a, "click", fn, scope);
|
|
370
|
-
addEvent(b, "click", fn, scope);
|
|
371
|
-
a.dispatchEvent(new MouseEvent("click"));
|
|
372
|
-
b.dispatchEvent(new MouseEvent("click"));
|
|
373
|
-
expect(fn).toHaveBeenCalledTimes(2);
|
|
374
|
-
scope.dispose();
|
|
375
|
-
a.dispatchEvent(new MouseEvent("click"));
|
|
376
|
-
expect(fn).toHaveBeenCalledTimes(2);
|
|
377
|
-
});
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
describe("applyProp writable-property cache", () => {
|
|
381
|
-
it("assigns prototype properties and keeps doing so for later elements of the same tag", () => {
|
|
382
|
-
const scope = new Scope();
|
|
383
|
-
const first = document.createElement("input");
|
|
384
|
-
const second = document.createElement("input");
|
|
385
|
-
applyProp(first, "value", "a", scope);
|
|
386
|
-
applyProp(second, "value", "b", scope);
|
|
387
|
-
expect(first.value).toBe("a");
|
|
388
|
-
expect(second.value).toBe("b");
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
it("falls back to attributes for unknown and read-only properties, consistently across elements", () => {
|
|
392
|
-
const scope = new Scope();
|
|
393
|
-
for (let i = 0; i < 2; i++) {
|
|
394
|
-
const input = document.createElement("input");
|
|
395
|
-
applyProp(input, "data-x", "1", scope);
|
|
396
|
-
applyProp(input, "list", "dl", scope);
|
|
397
|
-
expect(input.getAttribute("data-x")).toBe("1");
|
|
398
|
-
expect(input.getAttribute("list")).toBe("dl");
|
|
399
|
-
}
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
it("honours own properties: writable ones are assigned, getter-only ones become attributes", () => {
|
|
403
|
-
const scope = new Scope();
|
|
404
|
-
const el = document.createElement("div") as unknown as HTMLElement & Record<string, unknown>;
|
|
405
|
-
el.expando = "old";
|
|
406
|
-
Object.defineProperty(el, "readonlyThing", { get: () => "fixed", configurable: true });
|
|
407
|
-
applyProp(el, "expando", "new", scope);
|
|
408
|
-
applyProp(el, "readonlyThing", "attr", scope);
|
|
409
|
-
expect(el.expando).toBe("new");
|
|
410
|
-
expect(el.readonlyThing).toBe("fixed");
|
|
411
|
-
expect(el.getAttribute("readonlyThing")).toBe("attr");
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
it("an own property with a setter is assigned", () => {
|
|
415
|
-
const scope = new Scope();
|
|
416
|
-
const el = document.createElement("div") as unknown as HTMLElement & Record<string, unknown>;
|
|
417
|
-
let stored = "";
|
|
418
|
-
Object.defineProperty(el, "custom", { get: () => stored, set: (v: string) => { stored = v; }, configurable: true });
|
|
419
|
-
applyProp(el, "custom", "set", scope);
|
|
420
|
-
expect(stored).toBe("set");
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
it("runs ref callbacks untracked so they do not subscribe the enclosing effect", () => {
|
|
424
|
-
const scope = new Scope();
|
|
425
|
-
const s = signal(0);
|
|
426
|
-
const runs = vi.fn();
|
|
427
|
-
scope.effect(() => {
|
|
428
|
-
runs();
|
|
429
|
-
applyProp(document.createElement("div"), "ref", () => { void s(); }, scope);
|
|
430
|
-
});
|
|
431
|
-
s.set(1);
|
|
432
|
-
expect(runs).toHaveBeenCalledOnce();
|
|
433
|
-
});
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
describe("event props without a usable handler", () => {
|
|
437
|
-
it.each([undefined, null])("addEvent ignores a %s handler", (handler) => {
|
|
438
|
-
const el = document.createElement("button");
|
|
439
|
-
const scope = new Scope();
|
|
440
|
-
const add = vi.spyOn(el, "addEventListener");
|
|
441
|
-
expect(() => { addEvent(el, "click", handler, scope); }).not.toThrow();
|
|
442
|
-
expect(add).not.toHaveBeenCalled();
|
|
443
|
-
expect(() => { scope.dispose(); }).not.toThrow();
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
it.each([undefined, null])("applyProp skips an onClick prop whose value is %s", (value) => {
|
|
447
|
-
const el = document.createElement("button");
|
|
448
|
-
const scope = new Scope();
|
|
449
|
-
expect(() => { applyProp(el, "onClick", value, scope); }).not.toThrow();
|
|
450
|
-
expect(() => { el.dispatchEvent(new MouseEvent("click")); }).not.toThrow();
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
it("still registers a handler passed later in the same props object", () => {
|
|
454
|
-
const el = document.createElement("form");
|
|
455
|
-
const scope = new Scope();
|
|
456
|
-
const onInput = vi.fn();
|
|
457
|
-
applyProp(el, "onSubmit", undefined, scope);
|
|
458
|
-
applyProp(el, "onInput", onInput, scope);
|
|
459
|
-
el.dispatchEvent(new Event("input"));
|
|
460
|
-
expect(onInput).toHaveBeenCalledOnce();
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
it("supports an EventListenerObject, calling handleEvent as the DOM does", () => {
|
|
464
|
-
const el = document.createElement("button");
|
|
465
|
-
const scope = new Scope();
|
|
466
|
-
const handleEvent = vi.fn();
|
|
467
|
-
const listener = { handleEvent };
|
|
468
|
-
addEvent(el, "click", listener, scope);
|
|
469
|
-
const event = new MouseEvent("click");
|
|
470
|
-
el.dispatchEvent(event);
|
|
471
|
-
expect(handleEvent).toHaveBeenCalledWith(event);
|
|
472
|
-
scope.dispose();
|
|
473
|
-
el.dispatchEvent(new MouseEvent("click"));
|
|
474
|
-
expect(handleEvent).toHaveBeenCalledOnce();
|
|
475
|
-
});
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
describe("ref prop without a callback", () => {
|
|
479
|
-
it.each([undefined, null])("applyProp ignores a ref whose value is %s", (value) => {
|
|
480
|
-
const el = document.createElement("div");
|
|
481
|
-
expect(() => { applyProp(el, "ref", value, new Scope()); }).not.toThrow();
|
|
482
|
-
});
|
|
483
|
-
});
|