@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,360 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
3
|
-
|
|
4
|
-
import { StatefulComponent } from "@praxisjs/core";
|
|
5
|
-
import { isServerRenderPass, setServerRenderPass, signal } from "@praxisjs/core/internal";
|
|
6
|
-
|
|
7
|
-
import { mountComponent } from "../component";
|
|
8
|
-
import { getCurrentScope } from "../context";
|
|
9
|
-
import { mountElement } from "../element";
|
|
10
|
-
import { reconcile } from "../hydration";
|
|
11
|
-
import { registerReactiveNode } from "../hydration-context";
|
|
12
|
-
import { render, Portal } from "../index";
|
|
13
|
-
import { Scope } from "../scope";
|
|
14
|
-
|
|
15
|
-
const SSG_MARKER = "data-praxis-ssg";
|
|
16
|
-
|
|
17
|
-
/** Renders `factory` into a throwaway container and returns the resulting markup, simulating SSR output. */
|
|
18
|
-
function ssrHTML(factory: () => Node | Node[] | null): string {
|
|
19
|
-
const serverContainer = document.createElement("div");
|
|
20
|
-
const unmount = render(factory, serverContainer);
|
|
21
|
-
const html = serverContainer.innerHTML;
|
|
22
|
-
unmount();
|
|
23
|
-
return html;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function hydratable(factory: () => Node | Node[] | null): HTMLElement {
|
|
27
|
-
const container = document.createElement("div");
|
|
28
|
-
container.innerHTML = ssrHTML(factory);
|
|
29
|
-
container.setAttribute(SSG_MARKER, "1");
|
|
30
|
-
document.body.appendChild(container);
|
|
31
|
-
return container;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
afterEach(() => { setServerRenderPass(false); });
|
|
35
|
-
|
|
36
|
-
describe("render() — hydration (build + reconcile)", () => {
|
|
37
|
-
it("adopts a flat element instead of recreating it", () => {
|
|
38
|
-
const factory = () => mountElement("p", { id: "greet", children: "hello" }, new Scope());
|
|
39
|
-
const container = hydratable(factory);
|
|
40
|
-
const serverEl = container.querySelector("#greet");
|
|
41
|
-
expect(serverEl).not.toBeNull();
|
|
42
|
-
|
|
43
|
-
render(factory, container);
|
|
44
|
-
|
|
45
|
-
expect(container.querySelector("#greet")).toBe(serverEl);
|
|
46
|
-
document.body.removeChild(container);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("adopts nested elements at every level — the case that broke the ambient-cursor attempt", () => {
|
|
50
|
-
const factory = () => mountElement("div", {
|
|
51
|
-
id: "outer",
|
|
52
|
-
children: mountElement("span", { id: "inner", children: "text" }, new Scope()),
|
|
53
|
-
}, new Scope());
|
|
54
|
-
const container = hydratable(factory);
|
|
55
|
-
const outerBefore = container.querySelector("#outer");
|
|
56
|
-
const innerBefore = container.querySelector("#inner");
|
|
57
|
-
|
|
58
|
-
render(factory, container);
|
|
59
|
-
|
|
60
|
-
expect(container.querySelector("#outer")).toBe(outerBefore);
|
|
61
|
-
expect(container.querySelector("#inner")).toBe(innerBefore);
|
|
62
|
-
expect(container.textContent).toBe("text");
|
|
63
|
-
document.body.removeChild(container);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("adopts three levels deep with siblings at each level", () => {
|
|
67
|
-
const factory = () => mountElement("ul", {
|
|
68
|
-
id: "list",
|
|
69
|
-
children: [
|
|
70
|
-
mountElement("li", { id: "a", children: mountElement("b", { children: "1" }, new Scope()) }, new Scope()),
|
|
71
|
-
mountElement("li", { id: "b", children: "2" }, new Scope()),
|
|
72
|
-
],
|
|
73
|
-
}, new Scope());
|
|
74
|
-
const container = hydratable(factory);
|
|
75
|
-
const before = {
|
|
76
|
-
list: container.querySelector("#list"),
|
|
77
|
-
a: container.querySelector("#a"),
|
|
78
|
-
b: container.querySelector("#b"),
|
|
79
|
-
bold: container.querySelector("b"),
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
render(factory, container);
|
|
83
|
-
|
|
84
|
-
expect(container.querySelector("#list")).toBe(before.list);
|
|
85
|
-
expect(container.querySelector("#a")).toBe(before.a);
|
|
86
|
-
expect(container.querySelector("#b")).toBe(before.b);
|
|
87
|
-
expect(container.querySelector("b")).toBe(before.bold);
|
|
88
|
-
document.body.removeChild(container);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("wires event listeners onto the adopted (real) element via replay", () => {
|
|
92
|
-
let clicks = 0;
|
|
93
|
-
const factory = () => mountElement("div", {
|
|
94
|
-
children: mountElement("button", { onClick: () => { clicks++; }, children: "Click" }, new Scope()),
|
|
95
|
-
}, new Scope());
|
|
96
|
-
const container = hydratable(factory);
|
|
97
|
-
const buttonBefore = container.querySelector("button");
|
|
98
|
-
|
|
99
|
-
render(factory, container);
|
|
100
|
-
|
|
101
|
-
expect(container.querySelector("button")).toBe(buttonBefore);
|
|
102
|
-
container.querySelector("button")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
103
|
-
expect(clicks).toBe(1);
|
|
104
|
-
document.body.removeChild(container);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("hydrates reactive text and keeps reacting to signal changes afterwards", () => {
|
|
108
|
-
const text = signal("initial");
|
|
109
|
-
const factory = () => mountElement("span", { children: () => text() }, new Scope());
|
|
110
|
-
const container = hydratable(factory);
|
|
111
|
-
expect(container.textContent).toBe("initial");
|
|
112
|
-
|
|
113
|
-
render(factory, container);
|
|
114
|
-
|
|
115
|
-
text.set("updated");
|
|
116
|
-
expect(container.textContent).toBe("updated");
|
|
117
|
-
text.set("initial");
|
|
118
|
-
document.body.removeChild(container);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("hydrates a reactive list of elements, keeping real nodes, and stays correct after a later update", () => {
|
|
122
|
-
const items = signal(["a", "b", "c"]);
|
|
123
|
-
const factory = () => mountElement("ul", {
|
|
124
|
-
id: "list",
|
|
125
|
-
children: () => items().map((item) => mountElement("li", { children: item }, new Scope())),
|
|
126
|
-
}, new Scope());
|
|
127
|
-
const container = hydratable(factory);
|
|
128
|
-
const list = container.querySelector("#list") as HTMLElement;
|
|
129
|
-
const firstLiBefore = list.querySelector("li");
|
|
130
|
-
expect(container.textContent).toBe("abc");
|
|
131
|
-
|
|
132
|
-
render(factory, container);
|
|
133
|
-
|
|
134
|
-
expect(container.querySelector("#list")?.querySelector("li")).toBe(firstLiBefore);
|
|
135
|
-
expect(container.textContent).toBe("abc");
|
|
136
|
-
|
|
137
|
-
// A subsequent reactive update must diff correctly — this only works if
|
|
138
|
-
// reconcile() rewrote mountReactive's `currentNodes` bookkeeping in place
|
|
139
|
-
// when it kept the real <li> elements instead of the fresh ones.
|
|
140
|
-
items.set(["x", "y"]);
|
|
141
|
-
expect(container.textContent).toBe("xy");
|
|
142
|
-
items.set(["z"]);
|
|
143
|
-
expect(container.textContent).toBe("z");
|
|
144
|
-
document.body.removeChild(container);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("hydrates a component nested inside another component, firing onMount/onUnmount correctly", async () => {
|
|
148
|
-
class Inner extends StatefulComponent {
|
|
149
|
-
static __isComponent = true as const;
|
|
150
|
-
static __isStateless = false;
|
|
151
|
-
onMount() {}
|
|
152
|
-
onUnmount() {}
|
|
153
|
-
render() { return mountElement("em", { children: "inner" }, getCurrentScope()); }
|
|
154
|
-
}
|
|
155
|
-
class Outer extends StatefulComponent {
|
|
156
|
-
static __isComponent = true as const;
|
|
157
|
-
static __isStateless = false;
|
|
158
|
-
onMount() {}
|
|
159
|
-
render() {
|
|
160
|
-
return mountElement("section", {
|
|
161
|
-
children: mountComponent(Inner, {}, getCurrentScope()),
|
|
162
|
-
}, getCurrentScope());
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const factory = () => mountComponent(Outer, {}, getCurrentScope());
|
|
167
|
-
const container = hydratable(factory);
|
|
168
|
-
const emBefore = container.querySelector("em");
|
|
169
|
-
|
|
170
|
-
const outerMount = vi.spyOn(Outer.prototype, "onMount");
|
|
171
|
-
const innerMount = vi.spyOn(Inner.prototype, "onMount");
|
|
172
|
-
const innerUnmount = vi.spyOn(Inner.prototype, "onUnmount");
|
|
173
|
-
const dispose = render(factory, container);
|
|
174
|
-
await Promise.resolve();
|
|
175
|
-
|
|
176
|
-
expect(outerMount).toHaveBeenCalledOnce();
|
|
177
|
-
expect(innerMount).toHaveBeenCalledOnce();
|
|
178
|
-
expect(container.querySelector("em")).toBe(emBefore);
|
|
179
|
-
expect(container.textContent).toBe("inner");
|
|
180
|
-
|
|
181
|
-
dispose();
|
|
182
|
-
expect(innerUnmount).toHaveBeenCalledOnce();
|
|
183
|
-
|
|
184
|
-
outerMount.mockRestore();
|
|
185
|
-
innerMount.mockRestore();
|
|
186
|
-
innerUnmount.mockRestore();
|
|
187
|
-
document.body.removeChild(container);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it("a tag mismatch at one node replaces only that node — siblings stay adopted", () => {
|
|
191
|
-
const factory = () => mountElement("div", {
|
|
192
|
-
id: "wrap",
|
|
193
|
-
children: [
|
|
194
|
-
mountElement("p", { id: "before", children: "before" }, new Scope()),
|
|
195
|
-
mountElement("span", { id: "target", children: "new" }, new Scope()),
|
|
196
|
-
mountElement("p", { id: "after", children: "after" }, new Scope()),
|
|
197
|
-
],
|
|
198
|
-
}, new Scope());
|
|
199
|
-
|
|
200
|
-
const container = document.createElement("div");
|
|
201
|
-
container.innerHTML =
|
|
202
|
-
'<div id="wrap"><p id="before">before</p><strong id="target">old</strong><p id="after">after</p></div>';
|
|
203
|
-
container.setAttribute(SSG_MARKER, "1");
|
|
204
|
-
document.body.appendChild(container);
|
|
205
|
-
|
|
206
|
-
const beforeEl = container.querySelector("#before");
|
|
207
|
-
const afterEl = container.querySelector("#after");
|
|
208
|
-
|
|
209
|
-
render(factory, container);
|
|
210
|
-
|
|
211
|
-
expect(container.querySelector("#before")).toBe(beforeEl);
|
|
212
|
-
expect(container.querySelector("#after")).toBe(afterEl);
|
|
213
|
-
expect(container.querySelector("#target")?.tagName.toLowerCase()).toBe("span");
|
|
214
|
-
expect(container.textContent).toBe("beforenewafter");
|
|
215
|
-
document.body.removeChild(container);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it("appends extra fresh nodes when the real DOM has fewer children than the fresh tree", () => {
|
|
219
|
-
// Client factory produces three items; the server only rendered two —
|
|
220
|
-
// exercises reconcile()'s mismatch branch once realCursor has run out.
|
|
221
|
-
const factory = () => mountElement("ul", {
|
|
222
|
-
id: "list",
|
|
223
|
-
children: [
|
|
224
|
-
mountElement("li", { id: "a", children: "a" }, new Scope()),
|
|
225
|
-
mountElement("li", { id: "b", children: "b" }, new Scope()),
|
|
226
|
-
mountElement("li", { id: "c", children: "c" }, new Scope()),
|
|
227
|
-
],
|
|
228
|
-
}, new Scope());
|
|
229
|
-
|
|
230
|
-
const container = document.createElement("div");
|
|
231
|
-
container.innerHTML = '<ul id="list"><li id="a">a</li><li id="b">b</li></ul>';
|
|
232
|
-
container.setAttribute(SSG_MARKER, "1");
|
|
233
|
-
document.body.appendChild(container);
|
|
234
|
-
|
|
235
|
-
const aBefore = container.querySelector("#a");
|
|
236
|
-
const bBefore = container.querySelector("#b");
|
|
237
|
-
|
|
238
|
-
render(factory, container);
|
|
239
|
-
|
|
240
|
-
expect(container.querySelector("#a")).toBe(aBefore);
|
|
241
|
-
expect(container.querySelector("#b")).toBe(bBefore);
|
|
242
|
-
expect(container.querySelector("#c")?.textContent).toBe("c");
|
|
243
|
-
expect(container.querySelectorAll("#list > li")).toHaveLength(3);
|
|
244
|
-
document.body.removeChild(container);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it("keeps the real element even when its fresh counterpart bypassed mountElement (nothing recorded to replay)", () => {
|
|
248
|
-
class RawComp extends StatefulComponent {
|
|
249
|
-
static __isComponent = true as const;
|
|
250
|
-
static __isStateless = false;
|
|
251
|
-
render() {
|
|
252
|
-
const el = document.createElement("mark");
|
|
253
|
-
el.id = "raw";
|
|
254
|
-
el.textContent = "raw content";
|
|
255
|
-
return el;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const factory = () => mountComponent(RawComp, {}, getCurrentScope());
|
|
260
|
-
const container = hydratable(factory);
|
|
261
|
-
const markBefore = container.querySelector("#raw");
|
|
262
|
-
|
|
263
|
-
render(factory, container);
|
|
264
|
-
|
|
265
|
-
expect(container.querySelector("#raw")).toBe(markBefore);
|
|
266
|
-
expect(container.textContent).toBe("raw content");
|
|
267
|
-
document.body.removeChild(container);
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
it("removes trailing real nodes the fresh tree no longer produces", () => {
|
|
271
|
-
// Server rendered three items; the client factory only produces two —
|
|
272
|
-
// exercises reconcile()'s cleanup loop for leftover real siblings past
|
|
273
|
-
// the end of freshChildren.
|
|
274
|
-
const factory = () => mountElement("ul", {
|
|
275
|
-
id: "list",
|
|
276
|
-
children: [
|
|
277
|
-
mountElement("li", { id: "a", children: "a" }, new Scope()),
|
|
278
|
-
mountElement("li", { id: "b", children: "b" }, new Scope()),
|
|
279
|
-
],
|
|
280
|
-
}, new Scope());
|
|
281
|
-
|
|
282
|
-
const container = document.createElement("div");
|
|
283
|
-
container.innerHTML =
|
|
284
|
-
'<ul id="list"><li id="a">a</li><li id="b">b</li><li id="c">c</li></ul>';
|
|
285
|
-
container.setAttribute(SSG_MARKER, "1");
|
|
286
|
-
document.body.appendChild(container);
|
|
287
|
-
|
|
288
|
-
const aBefore = container.querySelector("#a");
|
|
289
|
-
const bBefore = container.querySelector("#b");
|
|
290
|
-
|
|
291
|
-
render(factory, container);
|
|
292
|
-
|
|
293
|
-
expect(container.querySelector("#a")).toBe(aBefore);
|
|
294
|
-
expect(container.querySelector("#b")).toBe(bBefore);
|
|
295
|
-
expect(container.querySelector("#c")).toBeNull();
|
|
296
|
-
expect(container.querySelectorAll("#list > li")).toHaveLength(2);
|
|
297
|
-
document.body.removeChild(container);
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
it("reconcile() is a safe no-op when a fresh node's registered reactive owner doesn't actually contain it", () => {
|
|
301
|
-
// Defensive guard in notifyReplacement(): registerReactiveNode() always
|
|
302
|
-
// registers a node into the exact array collectNodes() just built it
|
|
303
|
-
// into, so indexOf() finding nothing shouldn't happen through normal
|
|
304
|
-
// mounting — this exercises that branch directly, at the unit level, to
|
|
305
|
-
// confirm the guard degrades safely (no throw, no corruption) rather
|
|
306
|
-
// than silently trusting it can never occur.
|
|
307
|
-
const fresh = document.createElement("span");
|
|
308
|
-
const bogusOwner: Node[] = [];
|
|
309
|
-
registerReactiveNode(fresh, bogusOwner);
|
|
310
|
-
|
|
311
|
-
const container = document.createElement("div");
|
|
312
|
-
const real = document.createElement("span");
|
|
313
|
-
container.appendChild(real);
|
|
314
|
-
|
|
315
|
-
expect(() => { reconcile(container, [fresh]); }).not.toThrow();
|
|
316
|
-
expect(bogusOwner).toEqual([]);
|
|
317
|
-
expect(container.firstChild).toBe(real);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
it("plain client render (no SSG marker) is completely unaffected", () => {
|
|
321
|
-
const factory = () => mountElement("p", { children: "hello" }, new Scope());
|
|
322
|
-
const container = document.createElement("div");
|
|
323
|
-
document.body.appendChild(container);
|
|
324
|
-
|
|
325
|
-
render(factory, container);
|
|
326
|
-
|
|
327
|
-
expect(container.textContent).toBe("hello");
|
|
328
|
-
document.body.removeChild(container);
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
describe("Portal — server render pass", () => {
|
|
333
|
-
it("renders nothing into the target during a server render pass", () => {
|
|
334
|
-
setServerRenderPass(true);
|
|
335
|
-
expect(isServerRenderPass()).toBe(true);
|
|
336
|
-
|
|
337
|
-
const target = document.createElement("div");
|
|
338
|
-
document.body.appendChild(target);
|
|
339
|
-
const scope = new Scope();
|
|
340
|
-
const nodes = mountComponent(Portal, { to: target, children: document.createTextNode("hi") }, scope);
|
|
341
|
-
|
|
342
|
-
expect(target.textContent).toBe("");
|
|
343
|
-
expect(nodes.every((n) => n.nodeType === Node.COMMENT_NODE)).toBe(true);
|
|
344
|
-
scope.dispose();
|
|
345
|
-
document.body.removeChild(target);
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
it("mounts normally on the client once the server pass is over", () => {
|
|
349
|
-
setServerRenderPass(false);
|
|
350
|
-
const target = document.createElement("div");
|
|
351
|
-
document.body.appendChild(target);
|
|
352
|
-
const scope = new Scope();
|
|
353
|
-
|
|
354
|
-
mountComponent(Portal, { to: target, children: document.createTextNode("hi") }, scope);
|
|
355
|
-
|
|
356
|
-
expect(target.textContent).toBe("hi");
|
|
357
|
-
scope.dispose();
|
|
358
|
-
document.body.removeChild(target);
|
|
359
|
-
});
|
|
360
|
-
});
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
3
|
-
import { signal } from "@praxisjs/core/internal";
|
|
4
|
-
|
|
5
|
-
import { mountComponent } from "../component";
|
|
6
|
-
import { Portal, resolvePortalTarget } from "../portal";
|
|
7
|
-
import { Scope } from "../scope";
|
|
8
|
-
|
|
9
|
-
function mount(props: Record<string, unknown> = {}): { nodes: Node[]; scope: Scope } {
|
|
10
|
-
const scope = new Scope();
|
|
11
|
-
const nodes = mountComponent(Portal, props, scope);
|
|
12
|
-
return { nodes, scope };
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
document.body.innerHTML = "";
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
describe("Portal", () => {
|
|
20
|
-
it("renders children into document.body by default", () => {
|
|
21
|
-
const child = document.createTextNode("portal-content");
|
|
22
|
-
const { scope } = mount({ children: child });
|
|
23
|
-
expect(document.body.textContent).toContain("portal-content");
|
|
24
|
-
scope.dispose();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("renders children into a provided target element", () => {
|
|
28
|
-
const target = document.createElement("div");
|
|
29
|
-
document.body.appendChild(target);
|
|
30
|
-
const child = document.createTextNode("into-target");
|
|
31
|
-
const { scope } = mount({ to: target, children: child });
|
|
32
|
-
expect(target.textContent).toContain("into-target");
|
|
33
|
-
scope.dispose();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("renders children into an element found by CSS selector", () => {
|
|
37
|
-
const target = document.createElement("div");
|
|
38
|
-
target.id = "portal-root";
|
|
39
|
-
document.body.appendChild(target);
|
|
40
|
-
const child = document.createTextNode("selector-content");
|
|
41
|
-
const { scope } = mount({ to: "#portal-root", children: child });
|
|
42
|
-
expect(target.textContent).toContain("selector-content");
|
|
43
|
-
scope.dispose();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("returns a comment placeholder at the natural DOM position", () => {
|
|
47
|
-
const { nodes, scope } = mount({ children: document.createTextNode("x") });
|
|
48
|
-
const placeholder = nodes.find((n) => n.nodeType === Node.COMMENT_NODE && (n as Comment).data === "portal");
|
|
49
|
-
expect(placeholder).toBeDefined();
|
|
50
|
-
scope.dispose();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("does not append children into the natural parent", () => {
|
|
54
|
-
const container = document.createElement("div");
|
|
55
|
-
const child = document.createTextNode("no-leak");
|
|
56
|
-
const { nodes, scope } = mount({ children: child });
|
|
57
|
-
nodes.forEach((n) => container.appendChild(n));
|
|
58
|
-
expect(container.textContent).toBe(""); // no visible content in natural parent
|
|
59
|
-
scope.dispose();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("removes portal nodes from target when scope disposes", () => {
|
|
63
|
-
const child = document.createTextNode("to-remove");
|
|
64
|
-
const { scope } = mount({ children: child });
|
|
65
|
-
expect(document.body.textContent).toContain("to-remove");
|
|
66
|
-
scope.dispose();
|
|
67
|
-
expect(document.body.textContent).toBe("");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("removes nodes cleanly when target has pre-existing content", () => {
|
|
71
|
-
const pre = document.createTextNode("pre-existing");
|
|
72
|
-
document.body.appendChild(pre);
|
|
73
|
-
const child = document.createTextNode("portal-node");
|
|
74
|
-
const { scope } = mount({ children: child });
|
|
75
|
-
scope.dispose();
|
|
76
|
-
expect(document.body.textContent).toBe("pre-existing");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("does not throw if portal anchors were removed before dispose", () => {
|
|
80
|
-
const child = document.createTextNode("temporary");
|
|
81
|
-
const { scope } = mount({ children: child });
|
|
82
|
-
document.body.innerHTML = "";
|
|
83
|
-
expect(() => scope.dispose()).not.toThrow();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("multiple portals into the same target clean up independently", () => {
|
|
87
|
-
const child1 = document.createTextNode("first");
|
|
88
|
-
const child2 = document.createTextNode("second");
|
|
89
|
-
const { scope: s1 } = mount({ children: child1 });
|
|
90
|
-
const { scope: s2 } = mount({ children: child2 });
|
|
91
|
-
expect(document.body.textContent).toContain("first");
|
|
92
|
-
expect(document.body.textContent).toContain("second");
|
|
93
|
-
s1.dispose();
|
|
94
|
-
expect(document.body.textContent).not.toContain("first");
|
|
95
|
-
expect(document.body.textContent).toContain("second");
|
|
96
|
-
s2.dispose();
|
|
97
|
-
expect(document.body.textContent).toBe("");
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("renders reactive children into target and updates them", async () => {
|
|
101
|
-
const count = signal(0);
|
|
102
|
-
const { scope } = mount({
|
|
103
|
-
children: () => document.createTextNode(String(count())),
|
|
104
|
-
});
|
|
105
|
-
expect(document.body.textContent).toBe("0");
|
|
106
|
-
count.set(1);
|
|
107
|
-
await Promise.resolve();
|
|
108
|
-
expect(document.body.textContent).toBe("1");
|
|
109
|
-
scope.dispose();
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("returns null and mounts nothing when target is not found", () => {
|
|
113
|
-
const bodyBefore = document.body.innerHTML;
|
|
114
|
-
const { scope } = mount({ to: "#nonexistent", children: document.createTextNode("x") });
|
|
115
|
-
expect(document.body.innerHTML).toBe(bodyBefore);
|
|
116
|
-
scope.dispose();
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// ── resolvePortalTarget (unit) ────────────────────────────────────────────────
|
|
121
|
-
|
|
122
|
-
describe("resolvePortalTarget", () => {
|
|
123
|
-
it("returns document.body when to is null (browser environment)", () => {
|
|
124
|
-
expect(resolvePortalTarget(null)).toBe(document.body);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("returns document.body when to is undefined (browser environment)", () => {
|
|
128
|
-
expect(resolvePortalTarget(undefined)).toBe(document.body);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("returns null when to is null and document is not defined (SSR)", () => {
|
|
132
|
-
vi.stubGlobal("document", undefined);
|
|
133
|
-
try {
|
|
134
|
-
const result = resolvePortalTarget(null);
|
|
135
|
-
expect(result).toBeNull();
|
|
136
|
-
} finally {
|
|
137
|
-
vi.unstubAllGlobals();
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("resolves an Element directly", () => {
|
|
142
|
-
const el = document.createElement("div");
|
|
143
|
-
expect(resolvePortalTarget(el)).toBe(el);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("resolves a CSS selector", () => {
|
|
147
|
-
const el = document.createElement("div");
|
|
148
|
-
el.id = "resolve-test";
|
|
149
|
-
document.body.appendChild(el);
|
|
150
|
-
expect(resolvePortalTarget("#resolve-test")).toBe(el);
|
|
151
|
-
document.body.removeChild(el);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("attaches the whole subtree to the target in one operation", () => {
|
|
155
|
-
const target = document.createElement("div");
|
|
156
|
-
document.body.appendChild(target);
|
|
157
|
-
const spy = vi.spyOn(target, "appendChild");
|
|
158
|
-
const children = [document.createElement("p"), document.createElement("span")];
|
|
159
|
-
const { scope } = mount({ to: target, children });
|
|
160
|
-
expect(spy).toHaveBeenCalledOnce();
|
|
161
|
-
expect(target.querySelector("p")).not.toBeNull();
|
|
162
|
-
expect(target.querySelector("span")).not.toBeNull();
|
|
163
|
-
scope.dispose();
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("reactive children mounted through the portal keep updating after attach", () => {
|
|
167
|
-
const target = document.createElement("div");
|
|
168
|
-
document.body.appendChild(target);
|
|
169
|
-
const s = signal("one");
|
|
170
|
-
const { scope } = mount({ to: target, children: () => s() });
|
|
171
|
-
expect(target.textContent).toContain("one");
|
|
172
|
-
s.set("two");
|
|
173
|
-
expect(target.textContent).toContain("two");
|
|
174
|
-
expect(target.textContent).not.toContain("one");
|
|
175
|
-
scope.dispose();
|
|
176
|
-
});
|
|
177
|
-
});
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { describe, it, expect } from "vitest";
|
|
3
|
-
|
|
4
|
-
import { removeNodes } from "../dom/remove";
|
|
5
|
-
|
|
6
|
-
function siblings(...names: string[]): { parent: HTMLElement; nodes: HTMLElement[] } {
|
|
7
|
-
const parent = document.createElement("div");
|
|
8
|
-
const nodes = names.map((name) => {
|
|
9
|
-
const el = document.createElement(name);
|
|
10
|
-
parent.appendChild(el);
|
|
11
|
-
return el;
|
|
12
|
-
});
|
|
13
|
-
return { parent, nodes };
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
describe("removeNodes", () => {
|
|
17
|
-
it("removes the first-to-last span and leaves the surrounding siblings", () => {
|
|
18
|
-
const { parent, nodes } = siblings("a", "b", "c", "d", "e");
|
|
19
|
-
removeNodes(nodes.slice(1, 4));
|
|
20
|
-
expect([...parent.children].map((c) => c.localName)).toEqual(["a", "e"]);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("also removes nodes inserted between first and last after the list was captured", () => {
|
|
24
|
-
const { parent, nodes } = siblings("a", "b");
|
|
25
|
-
const late = document.createElement("late");
|
|
26
|
-
parent.insertBefore(late, nodes[1]);
|
|
27
|
-
removeNodes(nodes);
|
|
28
|
-
expect(parent.childNodes).toHaveLength(0);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("removes a single node", () => {
|
|
32
|
-
const { parent, nodes } = siblings("a", "b");
|
|
33
|
-
removeNodes([nodes[0]]);
|
|
34
|
-
expect([...parent.children]).toEqual([nodes[1]]);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("removes the attached nodes individually when the first one is detached", () => {
|
|
38
|
-
const { parent, nodes } = siblings("a", "b", "c");
|
|
39
|
-
parent.removeChild(nodes[0]);
|
|
40
|
-
expect(() => { removeNodes(nodes); }).not.toThrow();
|
|
41
|
-
expect(parent.childNodes).toHaveLength(0);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("removes each node from its own parent when first and last have different parents", () => {
|
|
45
|
-
const one = siblings("a", "b");
|
|
46
|
-
const two = siblings("c", "d");
|
|
47
|
-
removeNodes([one.nodes[0], one.nodes[1], two.nodes[0]]);
|
|
48
|
-
expect(one.parent.childNodes).toHaveLength(0);
|
|
49
|
-
expect([...two.parent.children]).toEqual([two.nodes[1]]);
|
|
50
|
-
});
|
|
51
|
-
});
|