@excom/include-content 0.1.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/.rush/temp/chunked-rush-logs/include-content.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/include-content.build_docs.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/include-content.build_package-metas.chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/all.log +1 -0
- package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/state.json +3 -0
- package/.rush/temp/operation/build_docs/all.log +1 -0
- package/.rush/temp/operation/build_docs/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_docs/state.json +3 -0
- package/.rush/temp/operation/build_package-metas/all.log +1 -0
- package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_package-metas/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +3 -0
- package/config/rig.json +5 -0
- package/include-content.ts +176 -0
- package/index.css +5 -0
- package/index.ts +17 -0
- package/package.json +45 -0
- package/rush-logs/include-content.apply-exports.cache.log +1 -0
- package/rush-logs/include-content.apply-exports.log +1 -0
- package/rush-logs/include-content.build_docs.cache.log +1 -0
- package/rush-logs/include-content.build_docs.log +1 -0
- package/rush-logs/include-content.build_package-metas.cache.log +1 -0
- package/rush-logs/include-content.build_package-metas.log +1 -0
- package/src/include-content.css +66 -0
- package/support/custom-elements.json +277 -0
- package/support/demos/lazy.html +19 -0
- package/support/demos/persist-content.html +23 -0
- package/support/demos/portal.html +8 -0
- package/support/demos/simple.html +16 -0
- package/support/demos/template-include.html +7 -0
- package/support/demos/template-ref.html +7 -0
- package/support/dist-docs/include-content.md +265 -0
- package/support/docs/README.md +64 -0
- package/support/package-meta.json +360 -0
- package/support/tests/include-content.test.ts +764 -0
- package/support/tests/lazy.view.test.ts +22 -0
- package/support/tests/persist-content.view.test.ts +33 -0
- package/support/tests/portal.view.test.ts +21 -0
- package/support/tests/simple.view.test.ts +23 -0
- package/support/tests/template-include.view.test.ts +35 -0
- package/support/tests/template-ref.view.test.ts +21 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
import { invokeCommand } from "@excom/neutron";
|
|
2
|
+
import "../../index";
|
|
3
|
+
import {
|
|
4
|
+
afterEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
fixture,
|
|
8
|
+
it,
|
|
9
|
+
spyFetch,
|
|
10
|
+
vi,
|
|
11
|
+
wait,
|
|
12
|
+
waitForEvent,
|
|
13
|
+
} from "@excom/heft-rig/profiles/default/config/test-utils";
|
|
14
|
+
|
|
15
|
+
describe("include-content", () => {
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
document.body.innerHTML = "";
|
|
18
|
+
vi.restoreAllMocks();
|
|
19
|
+
vi.unstubAllGlobals();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("renders inline template when activated", async () => {
|
|
23
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
24
|
+
`<include-content><template><p>hello</p></template></include-content>`,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
expect(el.isActive).toBeFalsy();
|
|
28
|
+
|
|
29
|
+
await waitForEvent(el, "include-content-did-render", () => {
|
|
30
|
+
el.isActive = true;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(el).dom.to.equalTag(
|
|
34
|
+
`<include-content is-active did-load></include-content>`,
|
|
35
|
+
);
|
|
36
|
+
expect(el.querySelector("p")?.textContent).toBe("hello");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("unrenders content when deactivated", async () => {
|
|
40
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
41
|
+
`<include-content><template><p>hello</p></template></include-content>`,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
await waitForEvent(el, "include-content-did-render", () => {
|
|
45
|
+
el.isActive = true;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(el.querySelector("p")).not.toBeNull();
|
|
49
|
+
|
|
50
|
+
el.isActive = false;
|
|
51
|
+
await wait(0);
|
|
52
|
+
|
|
53
|
+
// `did-load` stays set: template remains cached for a fast re-render
|
|
54
|
+
expect(el).dom.to.equalTag(`<include-content did-load></include-content>`);
|
|
55
|
+
expect(el.querySelector("p")).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("activates after idle when idleLoad is set", async () => {
|
|
59
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
60
|
+
`<include-content idle-load><template><p>idle</p></template></include-content>`,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(el.isActive).toBeFalsy();
|
|
64
|
+
await wait(25);
|
|
65
|
+
expect(el.isActive).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("does not activate via idleLoad if already active", () => {
|
|
69
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
70
|
+
`<include-content is-active idle-load><template><p>idle</p></template></include-content>`,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(el.isActive).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("does not activate if idleLoad is removed before idle fires", async () => {
|
|
77
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
78
|
+
`<include-content idle-load><template><p>idle</p></template></include-content>`,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
el.idleLoad = false;
|
|
82
|
+
await wait(25);
|
|
83
|
+
expect(el.isActive).toBeFalsy();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("creates an observer when lazyLoad is set", () => {
|
|
87
|
+
const observeSpy = vi.fn();
|
|
88
|
+
vi.stubGlobal(
|
|
89
|
+
"IntersectionObserver",
|
|
90
|
+
class {
|
|
91
|
+
observe = observeSpy;
|
|
92
|
+
disconnect = vi.fn();
|
|
93
|
+
unobserve = vi.fn();
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
98
|
+
`<include-content lazy-load><template><p>lazy</p></template></include-content>`,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect(el.observer).not.toBeNull();
|
|
102
|
+
expect(observeSpy).toHaveBeenCalledWith(el);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("creates an observer when lazyUnload is set", () => {
|
|
106
|
+
const observeSpy = vi.fn();
|
|
107
|
+
vi.stubGlobal(
|
|
108
|
+
"IntersectionObserver",
|
|
109
|
+
class {
|
|
110
|
+
observe = observeSpy;
|
|
111
|
+
disconnect = vi.fn();
|
|
112
|
+
unobserve = vi.fn();
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
117
|
+
`<include-content lazy-unload><template><p>lazy</p></template></include-content>`,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(el.observer).not.toBeNull();
|
|
121
|
+
expect(observeSpy).toHaveBeenCalledWith(el);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("destroys observer when lazyLoad is removed", () => {
|
|
125
|
+
const disconnectSpy = vi.fn();
|
|
126
|
+
vi.stubGlobal(
|
|
127
|
+
"IntersectionObserver",
|
|
128
|
+
class {
|
|
129
|
+
observe = vi.fn();
|
|
130
|
+
disconnect = disconnectSpy;
|
|
131
|
+
unobserve = vi.fn();
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
136
|
+
`<include-content lazy-load><template><p>lazy</p></template></include-content>`,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
expect(el.observer).not.toBeNull();
|
|
140
|
+
|
|
141
|
+
el.lazyLoad = false;
|
|
142
|
+
|
|
143
|
+
expect(disconnectSpy).toHaveBeenCalled();
|
|
144
|
+
expect(el.observer).toBeNull();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("destroys observer on disconnect", async () => {
|
|
148
|
+
const disconnectSpy = vi.fn();
|
|
149
|
+
vi.stubGlobal(
|
|
150
|
+
"IntersectionObserver",
|
|
151
|
+
class {
|
|
152
|
+
observe = vi.fn();
|
|
153
|
+
disconnect = disconnectSpy;
|
|
154
|
+
unobserve = vi.fn();
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
159
|
+
`<include-content lazy-load><template><p>lazy</p></template></include-content>`,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
expect(el.observer).not.toBeNull();
|
|
163
|
+
|
|
164
|
+
el.remove();
|
|
165
|
+
await wait(0);
|
|
166
|
+
|
|
167
|
+
expect(disconnectSpy).toHaveBeenCalled();
|
|
168
|
+
expect(el.observer).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("activates via observer when intersecting with lazyLoad", () => {
|
|
172
|
+
let observerCallback: (entries: Partial<IntersectionObserverEntry>[]) => void;
|
|
173
|
+
vi.stubGlobal(
|
|
174
|
+
"IntersectionObserver",
|
|
175
|
+
class {
|
|
176
|
+
constructor(cb: any) {
|
|
177
|
+
observerCallback = cb;
|
|
178
|
+
}
|
|
179
|
+
observe = vi.fn();
|
|
180
|
+
disconnect = vi.fn();
|
|
181
|
+
unobserve = vi.fn();
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
186
|
+
`<include-content lazy-load><template><p>lazy</p></template></include-content>`,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
expect(el.isActive).toBeFalsy();
|
|
190
|
+
|
|
191
|
+
observerCallback!([{ isIntersecting: true }]);
|
|
192
|
+
|
|
193
|
+
expect(el.isActive).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("deactivates via observer when not intersecting with lazyUnload", () => {
|
|
197
|
+
let observerCallback: (entries: Partial<IntersectionObserverEntry>[]) => void;
|
|
198
|
+
vi.stubGlobal(
|
|
199
|
+
"IntersectionObserver",
|
|
200
|
+
class {
|
|
201
|
+
constructor(cb: any) {
|
|
202
|
+
observerCallback = cb;
|
|
203
|
+
}
|
|
204
|
+
observe = vi.fn();
|
|
205
|
+
disconnect = vi.fn();
|
|
206
|
+
unobserve = vi.fn();
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
211
|
+
`<include-content lazy-load lazy-unload><template><p>lazy</p></template></include-content>`,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
observerCallback!([{ isIntersecting: true }]);
|
|
215
|
+
expect(el.isActive).toBe(true);
|
|
216
|
+
|
|
217
|
+
observerCallback!([{ isIntersecting: false }]);
|
|
218
|
+
expect(el.isActive).toBe(false);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("rebuilds observer on reconnect when lazy options are set", async () => {
|
|
222
|
+
const observeSpy = vi.fn();
|
|
223
|
+
const disconnectSpy = vi.fn();
|
|
224
|
+
vi.stubGlobal(
|
|
225
|
+
"IntersectionObserver",
|
|
226
|
+
class {
|
|
227
|
+
observe = observeSpy;
|
|
228
|
+
disconnect = disconnectSpy;
|
|
229
|
+
unobserve = vi.fn();
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
234
|
+
`<include-content lazy-load><template><p>lazy</p></template></include-content>`,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
expect(observeSpy).toHaveBeenCalledTimes(1);
|
|
238
|
+
|
|
239
|
+
el.remove();
|
|
240
|
+
await wait(0);
|
|
241
|
+
expect(disconnectSpy).toHaveBeenCalled();
|
|
242
|
+
|
|
243
|
+
document.body.appendChild(el);
|
|
244
|
+
await wait(0);
|
|
245
|
+
expect(observeSpy).toHaveBeenCalledTimes(2);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("destroys observer when activated without lazyUnload", () => {
|
|
249
|
+
const disconnectSpy = vi.fn();
|
|
250
|
+
vi.stubGlobal(
|
|
251
|
+
"IntersectionObserver",
|
|
252
|
+
class {
|
|
253
|
+
observe = vi.fn();
|
|
254
|
+
disconnect = disconnectSpy;
|
|
255
|
+
unobserve = vi.fn();
|
|
256
|
+
},
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
260
|
+
`<include-content lazy-load><template><p>lazy</p></template></include-content>`,
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
expect(el.observer).not.toBeNull();
|
|
264
|
+
|
|
265
|
+
el.isActive = true;
|
|
266
|
+
|
|
267
|
+
expect(disconnectSpy).toHaveBeenCalled();
|
|
268
|
+
expect(el.observer).toBeNull();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("passes observer options from element attributes", () => {
|
|
272
|
+
let receivedOptions: any;
|
|
273
|
+
vi.stubGlobal(
|
|
274
|
+
"IntersectionObserver",
|
|
275
|
+
class {
|
|
276
|
+
constructor(_cb: any, opts: any) {
|
|
277
|
+
receivedOptions = opts;
|
|
278
|
+
}
|
|
279
|
+
observe = vi.fn();
|
|
280
|
+
disconnect = vi.fn();
|
|
281
|
+
unobserve = vi.fn();
|
|
282
|
+
},
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
fixture<HTMLIncludeContentElement>(
|
|
286
|
+
`<include-content lazy-load observer-root-margin="10px" observer-threshold="0.5" observer-delay="100"></include-content>`,
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
expect(receivedOptions.rootMargin).toBe("10px");
|
|
290
|
+
expect(receivedOptions.threshold).toBe(0.5);
|
|
291
|
+
expect(receivedOptions.delay).toBe(100);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("uses default observer root margin when not specified", () => {
|
|
295
|
+
let receivedOptions: any;
|
|
296
|
+
vi.stubGlobal(
|
|
297
|
+
"IntersectionObserver",
|
|
298
|
+
class {
|
|
299
|
+
constructor(_cb: any, opts: any) {
|
|
300
|
+
receivedOptions = opts;
|
|
301
|
+
}
|
|
302
|
+
observe = vi.fn();
|
|
303
|
+
disconnect = vi.fn();
|
|
304
|
+
unobserve = vi.fn();
|
|
305
|
+
},
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
fixture<HTMLIncludeContentElement>(
|
|
309
|
+
`<include-content lazy-load></include-content>`,
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
expect(receivedOptions.rootMargin).toBe("-1px -1px -1px -1px");
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe("include-content host targeting (host-ref)", () => {
|
|
317
|
+
afterEach(() => {
|
|
318
|
+
document.body.innerHTML = "";
|
|
319
|
+
vi.restoreAllMocks();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("attaches a shadow root and renders into it when host-ref='shadow'", async () => {
|
|
323
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
324
|
+
`<include-content host-ref="shadow"><template><p>shadow</p></template></include-content>`,
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
expect(el.shadowRoot).not.toBeNull();
|
|
328
|
+
expect(el.renderHost).toBe(el.shadowRoot);
|
|
329
|
+
|
|
330
|
+
await waitForEvent(el, "include-content-did-render", () => {
|
|
331
|
+
el.isActive = true;
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
expect(el.shadowRoot!.querySelector("p")?.textContent).toBe("shadow");
|
|
335
|
+
// light DOM has no rendered <p>, only the original template
|
|
336
|
+
expect(el.querySelector("p")).toBeNull();
|
|
337
|
+
expect(el.querySelector("template")).not.toBeNull();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("renders into an author-provided iframe when host-ref='iframe'", async () => {
|
|
341
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
342
|
+
`<include-content host-ref="iframe">
|
|
343
|
+
<template><p>iframed</p></template>
|
|
344
|
+
<iframe data-render-host srcdoc="<!doctype html><html><body></body></html>"></iframe>
|
|
345
|
+
</include-content>`,
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
const iframe = el.querySelector(
|
|
349
|
+
"iframe[data-render-host]",
|
|
350
|
+
) as HTMLIFrameElement;
|
|
351
|
+
expect(iframe).not.toBeNull();
|
|
352
|
+
|
|
353
|
+
await waitForEvent(el, "include-content-did-render", () => {
|
|
354
|
+
el.isActive = true;
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
expect(el.renderHost).toBe(iframe.contentDocument!.body);
|
|
358
|
+
expect(
|
|
359
|
+
iframe.contentDocument!.body.querySelector("p")?.textContent,
|
|
360
|
+
).toBe("iframed");
|
|
361
|
+
// iframe and template stay in light DOM; no rendered <p> outside the iframe
|
|
362
|
+
expect(el.querySelector("iframe[data-render-host]")).toBe(iframe);
|
|
363
|
+
expect(el.querySelector("template")).not.toBeNull();
|
|
364
|
+
expect(Array.from(el.children).some((c) => c.tagName === "P")).toBe(false);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("does not create an iframe when host-ref='iframe' and none is provided", async () => {
|
|
368
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
369
|
+
`<include-content host-ref="iframe"><template><p>x</p></template></include-content>`,
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
expect(el.querySelector("iframe")).toBeNull();
|
|
373
|
+
expect(el.renderHost).toBeNull();
|
|
374
|
+
|
|
375
|
+
el.isActive = true;
|
|
376
|
+
await wait(50);
|
|
377
|
+
|
|
378
|
+
expect(el.querySelector("iframe")).toBeNull();
|
|
379
|
+
expect(el.querySelector("p")).toBeNull();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("renders into a host found by CSS selector when host-ref is a selector", async () => {
|
|
383
|
+
document.body.innerHTML = `
|
|
384
|
+
<div id="render-target"></div>
|
|
385
|
+
<include-content host-ref="#render-target">
|
|
386
|
+
<template><p>elsewhere</p></template>
|
|
387
|
+
</include-content>
|
|
388
|
+
`;
|
|
389
|
+
const el = document.querySelector(
|
|
390
|
+
"include-content",
|
|
391
|
+
) as HTMLIncludeContentElement;
|
|
392
|
+
const target = document.getElementById("render-target")!;
|
|
393
|
+
|
|
394
|
+
expect(el.renderHost).toBe(target);
|
|
395
|
+
|
|
396
|
+
await waitForEvent(el, "include-content-did-render", () => {
|
|
397
|
+
el.isActive = true;
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
expect(target.querySelector("p")?.textContent).toBe("elsewhere");
|
|
401
|
+
expect(el.querySelector("p")).toBeNull();
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("keeps the author iframe when host-ref switches away from 'iframe'", async () => {
|
|
405
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
406
|
+
`<include-content host-ref="iframe">
|
|
407
|
+
<template><p>x</p></template>
|
|
408
|
+
<iframe data-render-host srcdoc="<!doctype html><html><body></body></html>"></iframe>
|
|
409
|
+
</include-content>`,
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
await waitForEvent(el, "include-content-did-render", () => {
|
|
413
|
+
el.isActive = true;
|
|
414
|
+
});
|
|
415
|
+
const iframe = el.querySelector(
|
|
416
|
+
"iframe[data-render-host]",
|
|
417
|
+
) as HTMLIFrameElement;
|
|
418
|
+
expect(iframe.contentDocument!.body.querySelector("p")).not.toBeNull();
|
|
419
|
+
|
|
420
|
+
await waitForEvent(el, "include-content-did-render", () => {
|
|
421
|
+
el.hostRef = "shadow";
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// Author-owned iframe stays; its body is cleared and content moves to shadow
|
|
425
|
+
expect(el.querySelector("iframe[data-render-host]")).toBe(iframe);
|
|
426
|
+
expect(iframe.contentDocument!.body.querySelector("p")).toBeNull();
|
|
427
|
+
expect(el.shadowRoot!.querySelector("p")?.textContent).toBe("x");
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("re-targets rendered content when host-ref changes from light DOM to shadow", async () => {
|
|
431
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
432
|
+
`<include-content><template><p>moves</p></template></include-content>`,
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
await waitForEvent(el, "include-content-did-render", () => {
|
|
436
|
+
el.isActive = true;
|
|
437
|
+
});
|
|
438
|
+
// initial render goes into light DOM
|
|
439
|
+
expect(el.querySelector("p")?.textContent).toBe("moves");
|
|
440
|
+
expect(el.shadowRoot).toBeNull();
|
|
441
|
+
|
|
442
|
+
await waitForEvent(el, "include-content-did-render", () => {
|
|
443
|
+
el.hostRef = "shadow";
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
expect(el.shadowRoot).not.toBeNull();
|
|
447
|
+
expect(el.shadowRoot!.querySelector("p")?.textContent).toBe("moves");
|
|
448
|
+
expect(el.querySelector("p")).toBeNull();
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
describe("include-content reload", () => {
|
|
453
|
+
afterEach(() => {
|
|
454
|
+
document.body.innerHTML = "";
|
|
455
|
+
vi.restoreAllMocks();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("re-fetches and re-renders when --reload is invoked while active", async () => {
|
|
459
|
+
let nthCall = 0;
|
|
460
|
+
spyFetch(() => ({
|
|
461
|
+
body: `<p>render-${++nthCall}</p>`,
|
|
462
|
+
headers: new Headers({ "content-type": "text/html" }),
|
|
463
|
+
}));
|
|
464
|
+
|
|
465
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
466
|
+
`<include-content template-ref="/reload-refetch.html"></include-content>`,
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
await waitForEvent(el, "include-content-did-render", () => {
|
|
470
|
+
el.isActive = true;
|
|
471
|
+
});
|
|
472
|
+
expect(el.querySelector("p")?.textContent).toBe("render-1");
|
|
473
|
+
|
|
474
|
+
await waitForEvent(el, "include-content-did-render", () => {
|
|
475
|
+
invokeCommand(el, "--reload");
|
|
476
|
+
});
|
|
477
|
+
expect(el.querySelector("p")?.textContent).toBe("render-2");
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("does nothing when --reload is invoked while inactive", async () => {
|
|
481
|
+
const fetchSpy = spyFetch({
|
|
482
|
+
body: "<p>x</p>",
|
|
483
|
+
headers: new Headers({ "content-type": "text/html" }),
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
487
|
+
`<include-content template-ref="/reload-inactive.html"></include-content>`,
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
// not active -> no initial fetch
|
|
491
|
+
await wait(10);
|
|
492
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
493
|
+
|
|
494
|
+
invokeCommand(el, "--reload");
|
|
495
|
+
await wait(10);
|
|
496
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
497
|
+
expect(el.isLoading).toBeFalsy();
|
|
498
|
+
expect(el.didLoad).toBeFalsy();
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it("clears the previous didLoad/_persistedTree before reloading", async () => {
|
|
502
|
+
let nthCall = 0;
|
|
503
|
+
spyFetch(() => ({
|
|
504
|
+
body: `<p>v${++nthCall}</p>`,
|
|
505
|
+
headers: new Headers({ "content-type": "text/html" }),
|
|
506
|
+
}));
|
|
507
|
+
|
|
508
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
509
|
+
`<include-content template-ref="/reload-clears.html"></include-content>`,
|
|
510
|
+
);
|
|
511
|
+
await waitForEvent(el, "include-content-did-render", () => {
|
|
512
|
+
el.isActive = true;
|
|
513
|
+
});
|
|
514
|
+
expect(el.didLoad).toBe(true);
|
|
515
|
+
|
|
516
|
+
// Reload handler should reset; wait for the next `did-render`.
|
|
517
|
+
await waitForEvent(el, "include-content-did-render", () => {
|
|
518
|
+
invokeCommand(el, "--reload");
|
|
519
|
+
});
|
|
520
|
+
expect(el.didLoad).toBe(true);
|
|
521
|
+
expect(el.querySelector("p")?.textContent).toBe("v2");
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
describe("include-content idle / lazy edge cases", () => {
|
|
526
|
+
afterEach(() => {
|
|
527
|
+
document.body.innerHTML = "";
|
|
528
|
+
vi.restoreAllMocks();
|
|
529
|
+
vi.unstubAllGlobals();
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it("idle-load schedules activation through requestIdleCallback when available", () => {
|
|
533
|
+
const idleSpy = vi.fn((cb: () => void) => {
|
|
534
|
+
cb();
|
|
535
|
+
return 1;
|
|
536
|
+
});
|
|
537
|
+
vi.stubGlobal("requestIdleCallback", idleSpy);
|
|
538
|
+
|
|
539
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
540
|
+
`<include-content idle-load><template><p>idle</p></template></include-content>`,
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
expect(idleSpy).toHaveBeenCalledWith(expect.any(Function), {
|
|
544
|
+
timeout: 17,
|
|
545
|
+
});
|
|
546
|
+
expect(el.isActive).toBe(true);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("resolves observer-root as a selector for the IntersectionObserver root", () => {
|
|
550
|
+
let receivedOptions: any;
|
|
551
|
+
vi.stubGlobal(
|
|
552
|
+
"IntersectionObserver",
|
|
553
|
+
class {
|
|
554
|
+
constructor(_cb: any, opts: any) {
|
|
555
|
+
receivedOptions = opts;
|
|
556
|
+
}
|
|
557
|
+
observe = vi.fn();
|
|
558
|
+
disconnect = vi.fn();
|
|
559
|
+
unobserve = vi.fn();
|
|
560
|
+
},
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
document.body.innerHTML = `
|
|
564
|
+
<section id="ic-scroll-root">
|
|
565
|
+
<include-content lazy-load observer-root="#ic-scroll-root"></include-content>
|
|
566
|
+
</section>
|
|
567
|
+
`;
|
|
568
|
+
|
|
569
|
+
expect(receivedOptions.root).toBe(
|
|
570
|
+
document.getElementById("ic-scroll-root"),
|
|
571
|
+
);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it("keeps the existing observer when a second lazy option is set", () => {
|
|
575
|
+
const observeSpy = vi.fn();
|
|
576
|
+
let constructed = 0;
|
|
577
|
+
vi.stubGlobal(
|
|
578
|
+
"IntersectionObserver",
|
|
579
|
+
class {
|
|
580
|
+
constructor() {
|
|
581
|
+
constructed++;
|
|
582
|
+
}
|
|
583
|
+
observe = observeSpy;
|
|
584
|
+
disconnect = vi.fn();
|
|
585
|
+
unobserve = vi.fn();
|
|
586
|
+
},
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
590
|
+
`<include-content lazy-load></include-content>`,
|
|
591
|
+
);
|
|
592
|
+
const observer = el.observer;
|
|
593
|
+
expect(constructed).toBe(1);
|
|
594
|
+
|
|
595
|
+
el.lazyUnload = true;
|
|
596
|
+
expect(constructed).toBe(1);
|
|
597
|
+
expect(el.observer).toBe(observer);
|
|
598
|
+
expect(observeSpy).toHaveBeenCalledTimes(1);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it("recreates the observer when lazy options are cleared and re-set", () => {
|
|
602
|
+
let constructed = 0;
|
|
603
|
+
const disconnectSpy = vi.fn();
|
|
604
|
+
vi.stubGlobal(
|
|
605
|
+
"IntersectionObserver",
|
|
606
|
+
class {
|
|
607
|
+
constructor() {
|
|
608
|
+
constructed++;
|
|
609
|
+
}
|
|
610
|
+
observe = vi.fn();
|
|
611
|
+
disconnect = disconnectSpy;
|
|
612
|
+
unobserve = vi.fn();
|
|
613
|
+
},
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
617
|
+
`<include-content lazy-load lazy-unload></include-content>`,
|
|
618
|
+
);
|
|
619
|
+
expect(constructed).toBe(1);
|
|
620
|
+
|
|
621
|
+
el.lazyLoad = false;
|
|
622
|
+
// lazy-unload still set: observer survives
|
|
623
|
+
expect(disconnectSpy).not.toHaveBeenCalled();
|
|
624
|
+
expect(el.observer).not.toBeNull();
|
|
625
|
+
|
|
626
|
+
el.lazyUnload = false;
|
|
627
|
+
expect(disconnectSpy).toHaveBeenCalledTimes(1);
|
|
628
|
+
expect(el.observer).toBeNull();
|
|
629
|
+
|
|
630
|
+
el.lazyLoad = true;
|
|
631
|
+
expect(constructed).toBe(2);
|
|
632
|
+
expect(el.observer).not.toBeNull();
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it("rebuilds the observer on reconnect when only lazy-unload is set", async () => {
|
|
636
|
+
const observeSpy = vi.fn();
|
|
637
|
+
const disconnectSpy = vi.fn();
|
|
638
|
+
vi.stubGlobal(
|
|
639
|
+
"IntersectionObserver",
|
|
640
|
+
class {
|
|
641
|
+
observe = observeSpy;
|
|
642
|
+
disconnect = disconnectSpy;
|
|
643
|
+
unobserve = vi.fn();
|
|
644
|
+
},
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
648
|
+
`<include-content lazy-unload><template><p>lazy</p></template></include-content>`,
|
|
649
|
+
);
|
|
650
|
+
expect(observeSpy).toHaveBeenCalledTimes(1);
|
|
651
|
+
|
|
652
|
+
el.remove();
|
|
653
|
+
await wait(0);
|
|
654
|
+
expect(disconnectSpy).toHaveBeenCalledTimes(1);
|
|
655
|
+
expect(el.observer).toBeNull();
|
|
656
|
+
|
|
657
|
+
document.body.appendChild(el);
|
|
658
|
+
await wait(0);
|
|
659
|
+
expect(observeSpy).toHaveBeenCalledTimes(2);
|
|
660
|
+
expect(el.observer).not.toBeNull();
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it("ignores an intersection when already active", () => {
|
|
664
|
+
let observerCallback: (entries: Partial<IntersectionObserverEntry>[]) => void;
|
|
665
|
+
vi.stubGlobal(
|
|
666
|
+
"IntersectionObserver",
|
|
667
|
+
class {
|
|
668
|
+
constructor(cb: any) {
|
|
669
|
+
observerCallback = cb;
|
|
670
|
+
}
|
|
671
|
+
observe = vi.fn();
|
|
672
|
+
disconnect = vi.fn();
|
|
673
|
+
unobserve = vi.fn();
|
|
674
|
+
},
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
678
|
+
`<include-content lazy-load lazy-unload is-active><template><p>on</p></template></include-content>`,
|
|
679
|
+
);
|
|
680
|
+
expect(el.isActive).toBe(true);
|
|
681
|
+
expect(el.observer).not.toBeNull();
|
|
682
|
+
|
|
683
|
+
observerCallback!([{ isIntersecting: true }]);
|
|
684
|
+
expect(el.isActive).toBe(true);
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it("stays active when leaving the viewport without lazy-unload", () => {
|
|
688
|
+
let observerCallback: (entries: Partial<IntersectionObserverEntry>[]) => void;
|
|
689
|
+
vi.stubGlobal(
|
|
690
|
+
"IntersectionObserver",
|
|
691
|
+
class {
|
|
692
|
+
constructor(cb: any) {
|
|
693
|
+
observerCallback = cb;
|
|
694
|
+
}
|
|
695
|
+
observe = vi.fn();
|
|
696
|
+
disconnect = vi.fn();
|
|
697
|
+
unobserve = vi.fn();
|
|
698
|
+
},
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
702
|
+
`<include-content lazy-load><template><p>on</p></template></include-content>`,
|
|
703
|
+
);
|
|
704
|
+
observerCallback!([{ isIntersecting: true }]);
|
|
705
|
+
expect(el.isActive).toBe(true);
|
|
706
|
+
|
|
707
|
+
observerCallback!([{ isIntersecting: false }]);
|
|
708
|
+
expect(el.isActive).toBe(true);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it("treats a hidden element as off screen", () => {
|
|
712
|
+
let observerCallback: (entries: Partial<IntersectionObserverEntry>[]) => void;
|
|
713
|
+
vi.stubGlobal(
|
|
714
|
+
"IntersectionObserver",
|
|
715
|
+
class {
|
|
716
|
+
constructor(cb: any) {
|
|
717
|
+
observerCallback = cb;
|
|
718
|
+
}
|
|
719
|
+
observe = vi.fn();
|
|
720
|
+
disconnect = vi.fn();
|
|
721
|
+
unobserve = vi.fn();
|
|
722
|
+
},
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
726
|
+
`<include-content lazy-load lazy-unload><template><p>hidden</p></template></include-content>`,
|
|
727
|
+
);
|
|
728
|
+
el.checkVisibility = () => false;
|
|
729
|
+
|
|
730
|
+
observerCallback!([{ isIntersecting: true }]);
|
|
731
|
+
expect(el.isActive).toBeFalsy();
|
|
732
|
+
|
|
733
|
+
el.checkVisibility = () => true;
|
|
734
|
+
observerCallback!([{ isIntersecting: true }]);
|
|
735
|
+
expect(el.isActive).toBe(true);
|
|
736
|
+
|
|
737
|
+
el.checkVisibility = () => false;
|
|
738
|
+
observerCallback!([{ isIntersecting: true }]);
|
|
739
|
+
expect(el.isActive).toBe(false);
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it("treats a missing checkVisibility as visible", () => {
|
|
743
|
+
let observerCallback: (entries: Partial<IntersectionObserverEntry>[]) => void;
|
|
744
|
+
vi.stubGlobal(
|
|
745
|
+
"IntersectionObserver",
|
|
746
|
+
class {
|
|
747
|
+
constructor(cb: any) {
|
|
748
|
+
observerCallback = cb;
|
|
749
|
+
}
|
|
750
|
+
observe = vi.fn();
|
|
751
|
+
disconnect = vi.fn();
|
|
752
|
+
unobserve = vi.fn();
|
|
753
|
+
},
|
|
754
|
+
);
|
|
755
|
+
|
|
756
|
+
const el = fixture<HTMLIncludeContentElement>(
|
|
757
|
+
`<include-content lazy-load><template><p>legacy</p></template></include-content>`,
|
|
758
|
+
);
|
|
759
|
+
(el as any).checkVisibility = undefined;
|
|
760
|
+
|
|
761
|
+
observerCallback!([{ isIntersecting: true }]);
|
|
762
|
+
expect(el.isActive).toBe(true);
|
|
763
|
+
});
|
|
764
|
+
});
|