Skip to content

Commit 69f7254

Browse files
refactor: extract shadow root tests to in-element-shadow-root.ts and add new tests
Co-authored-by: NullVoxPopuli <[email protected]>
1 parent ae24ef8 commit 69f7254

4 files changed

Lines changed: 344 additions & 82 deletions

File tree

packages/@glimmer-workspace/integration-tests/lib/suites.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from './suites/entry-point';
77
export * from './suites/has-block';
88
export * from './suites/has-block-params';
99
export * from './suites/in-element';
10+
export * from './suites/in-element-shadow-root';
1011
export * from './suites/initial-render';
1112
export * from './suites/scope';
1213
export * from './suites/shadow-dom';
Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
import type { Owner } from '@glimmer/interfaces';
2+
import type { Dict } from '@glimmer/util';
3+
4+
import { GlimmerishComponent } from '../components/emberish-glimmer';
5+
import { RenderTest } from '../render-test';
6+
import { test } from '../test-decorator';
7+
import { tracked } from '../test-helpers/tracked';
8+
9+
function hasShadowDom() {
10+
return typeof document !== 'undefined' && 'attachShadow' in document.createElement('div');
11+
}
12+
13+
export class InElementShadowRootSuite extends RenderTest {
14+
static suiteName = '#in-element (ShadowRoot)';
15+
16+
@test
17+
'Renders curlies into a ShadowRoot'() {
18+
if (!hasShadowDom()) {
19+
this.assert.ok(true, 'Shadow DOM not supported, skipping');
20+
return;
21+
}
22+
23+
const hostElement = document.createElement('div');
24+
const shadowRoot = hostElement.attachShadow({ mode: 'open' });
25+
26+
this.render('{{#in-element this.shadowRoot}}[{{this.foo}}]{{/in-element}}', {
27+
shadowRoot,
28+
foo: 'Hello Shadow!',
29+
});
30+
31+
this.assert.strictEqual(
32+
shadowRoot.textContent,
33+
'[Hello Shadow!]',
34+
'content rendered in shadow root'
35+
);
36+
this.assertHTML('<!---->');
37+
this.assertStableRerender();
38+
39+
this.rerender({ foo: 'Updated!' });
40+
this.assert.strictEqual(shadowRoot.textContent, '[Updated!]', 'content updated in shadow root');
41+
this.assertHTML('<!---->');
42+
43+
this.rerender({ foo: 'Hello Shadow!' });
44+
this.assert.strictEqual(
45+
shadowRoot.textContent,
46+
'[Hello Shadow!]',
47+
'content reverted in shadow root'
48+
);
49+
this.assertHTML('<!---->');
50+
}
51+
52+
@test
53+
'Renders curlies into a DocumentFragment'() {
54+
if (typeof document === 'undefined') {
55+
this.assert.ok(true, 'DOM not supported, skipping');
56+
return;
57+
}
58+
59+
const templateElement = document.createElement('template');
60+
const fragment = templateElement.content;
61+
62+
this.render('{{#in-element this.fragment}}[{{this.foo}}]{{/in-element}}', {
63+
fragment,
64+
foo: 'Hello Fragment!',
65+
});
66+
67+
this.assert.strictEqual(
68+
fragment.textContent,
69+
'[Hello Fragment!]',
70+
'content rendered in document fragment'
71+
);
72+
this.assertHTML('<!---->');
73+
this.assertStableRerender();
74+
75+
this.rerender({ foo: 'Updated Fragment!' });
76+
this.assert.strictEqual(
77+
fragment.textContent,
78+
'[Updated Fragment!]',
79+
'content updated in document fragment'
80+
);
81+
this.assertHTML('<!---->');
82+
83+
this.rerender({ foo: 'Hello Fragment!' });
84+
this.assert.strictEqual(
85+
fragment.textContent,
86+
'[Hello Fragment!]',
87+
'content reverted in fragment'
88+
);
89+
this.assertHTML('<!---->');
90+
}
91+
92+
@test
93+
'Class-based component with tracked property renders into shadow root without full DOM replacement on update'() {
94+
if (!hasShadowDom()) {
95+
this.assert.ok(true, 'Shadow DOM not supported, skipping');
96+
return;
97+
}
98+
99+
const hostElement = document.createElement('div');
100+
const shadowRoot = hostElement.attachShadow({ mode: 'open' });
101+
102+
class Counter extends GlimmerishComponent {
103+
@tracked count: number;
104+
105+
constructor(owner: Owner, args: Dict) {
106+
super(owner, args);
107+
this.count = (args['initial'] as number) ?? 0;
108+
}
109+
}
110+
111+
this.registerComponent('Glimmer', 'Counter', '<p>Count: {{@count}}</p>', Counter as any);
112+
113+
this.render('{{#in-element this.shadowRoot}}<Counter @count={{this.count}} />{{/in-element}}', {
114+
shadowRoot,
115+
count: 0,
116+
});
117+
118+
const p = shadowRoot.querySelector('p');
119+
this.assert.ok(p !== null, 'p element rendered in shadow root');
120+
this.assert.strictEqual(p?.textContent, 'Count: 0', 'initial count is 0');
121+
this.assertHTML('<!---->');
122+
123+
this.rerender({ count: 1 });
124+
this.assert.strictEqual(
125+
shadowRoot.querySelector('p')?.textContent,
126+
'Count: 1',
127+
'count updated to 1'
128+
);
129+
this.assert.strictEqual(
130+
shadowRoot.querySelector('p'),
131+
p,
132+
'same <p> element reused (no full DOM replacement)'
133+
);
134+
this.assertHTML('<!---->');
135+
136+
this.rerender({ count: 42 });
137+
this.assert.strictEqual(
138+
shadowRoot.querySelector('p')?.textContent,
139+
'Count: 42',
140+
'count updated to 42'
141+
);
142+
this.assert.strictEqual(
143+
shadowRoot.querySelector('p'),
144+
p,
145+
'same <p> element still reused after second update'
146+
);
147+
this.assertHTML('<!---->');
148+
}
149+
150+
@test
151+
'Sibling components rendered into the same shadow root'() {
152+
if (!hasShadowDom()) {
153+
this.assert.ok(true, 'Shadow DOM not supported, skipping');
154+
return;
155+
}
156+
157+
const hostElement = document.createElement('div');
158+
const shadowRoot = hostElement.attachShadow({ mode: 'open' });
159+
160+
this.registerComponent('TemplateOnly', 'Header', '<h1>{{@title}}</h1>');
161+
this.registerComponent('TemplateOnly', 'Body', '<p>{{@content}}</p>');
162+
163+
this.render(
164+
'{{#in-element this.shadowRoot insertBefore=null}}<Header @title={{this.title}} />{{/in-element}}' +
165+
'{{#in-element this.shadowRoot insertBefore=null}}<Body @content={{this.content}} />{{/in-element}}',
166+
{
167+
shadowRoot,
168+
title: 'My Title',
169+
content: 'My Content',
170+
}
171+
);
172+
173+
this.assert.strictEqual(
174+
shadowRoot.querySelector('h1')?.textContent,
175+
'My Title',
176+
'Header component rendered in shadow root'
177+
);
178+
this.assert.strictEqual(
179+
shadowRoot.querySelector('p')?.textContent,
180+
'My Content',
181+
'Body component rendered in shadow root'
182+
);
183+
this.assertHTML('<!----><!---->');
184+
this.assertStableRerender();
185+
186+
this.rerender({ title: 'Updated Title', content: 'Updated Content' });
187+
this.assert.strictEqual(
188+
shadowRoot.querySelector('h1')?.textContent,
189+
'Updated Title',
190+
'Header updated'
191+
);
192+
this.assert.strictEqual(
193+
shadowRoot.querySelector('p')?.textContent,
194+
'Updated Content',
195+
'Body updated'
196+
);
197+
this.assertHTML('<!----><!---->');
198+
}
199+
200+
@test
201+
'Sibling shadow roots each receive their own component'() {
202+
if (!hasShadowDom()) {
203+
this.assert.ok(true, 'Shadow DOM not supported, skipping');
204+
return;
205+
}
206+
207+
const host1 = document.createElement('div');
208+
const shadow1 = host1.attachShadow({ mode: 'open' });
209+
const host2 = document.createElement('div');
210+
const shadow2 = host2.attachShadow({ mode: 'open' });
211+
212+
this.registerComponent('TemplateOnly', 'Widget', '<span>{{@label}}</span>');
213+
214+
this.render(
215+
'{{#in-element this.shadow1}}<Widget @label={{this.label1}} />{{/in-element}}' +
216+
'{{#in-element this.shadow2}}<Widget @label={{this.label2}} />{{/in-element}}',
217+
{
218+
shadow1,
219+
shadow2,
220+
label1: 'Widget A',
221+
label2: 'Widget B',
222+
}
223+
);
224+
225+
this.assert.strictEqual(
226+
shadow1.querySelector('span')?.textContent,
227+
'Widget A',
228+
'Widget A rendered in shadow1'
229+
);
230+
this.assert.strictEqual(
231+
shadow2.querySelector('span')?.textContent,
232+
'Widget B',
233+
'Widget B rendered in shadow2'
234+
);
235+
this.assertHTML('<!----><!---->');
236+
this.assertStableRerender();
237+
238+
this.rerender({ label1: 'Widget Alpha', label2: 'Widget Beta' });
239+
this.assert.strictEqual(
240+
shadow1.querySelector('span')?.textContent,
241+
'Widget Alpha',
242+
'Widget A label updated'
243+
);
244+
this.assert.strictEqual(
245+
shadow2.querySelector('span')?.textContent,
246+
'Widget Beta',
247+
'Widget B label updated'
248+
);
249+
this.assertHTML('<!----><!---->');
250+
}
251+
252+
@test
253+
'Multiple in-element calls to the same shadow root'() {
254+
if (!hasShadowDom()) {
255+
this.assert.ok(true, 'Shadow DOM not supported, skipping');
256+
return;
257+
}
258+
259+
const hostElement = document.createElement('div');
260+
const shadowRoot = hostElement.attachShadow({ mode: 'open' });
261+
262+
// Without insertBefore=null, each in-element block manages its own bounded
263+
// region inside the shadow root using comment markers.
264+
this.render(
265+
'{{#in-element this.shadowRoot}}[{{this.foo}}]{{/in-element}}' +
266+
'{{#in-element this.shadowRoot insertBefore=null}}[{{this.bar}}]{{/in-element}}',
267+
{
268+
shadowRoot,
269+
foo: 'first',
270+
bar: 'second',
271+
}
272+
);
273+
274+
this.assert.ok(shadowRoot.textContent?.includes('[first]'), 'first in-element content present');
275+
this.assert.ok(
276+
shadowRoot.textContent?.includes('[second]'),
277+
'second in-element content present'
278+
);
279+
this.assertHTML('<!----><!---->');
280+
this.assertStableRerender();
281+
282+
this.rerender({ foo: 'updated-first', bar: 'updated-second' });
283+
this.assert.ok(
284+
shadowRoot.textContent?.includes('[updated-first]'),
285+
'first in-element content updated'
286+
);
287+
this.assert.ok(
288+
shadowRoot.textContent?.includes('[updated-second]'),
289+
'second in-element content updated'
290+
);
291+
this.assertHTML('<!----><!---->');
292+
}
293+
294+
@test
295+
'Multiple in-element calls to the same shadow root with insertBefore=null'() {
296+
if (!hasShadowDom()) {
297+
this.assert.ok(true, 'Shadow DOM not supported, skipping');
298+
return;
299+
}
300+
301+
const hostElement = document.createElement('div');
302+
const shadowRoot = hostElement.attachShadow({ mode: 'open' });
303+
304+
// With insertBefore=null on both, both blocks append to the shadow root
305+
this.render(
306+
'{{#in-element this.shadowRoot insertBefore=null}}<p id="a">{{this.foo}}</p>{{/in-element}}' +
307+
'{{#in-element this.shadowRoot insertBefore=null}}<p id="b">{{this.bar}}</p>{{/in-element}}',
308+
{
309+
shadowRoot,
310+
foo: 'first',
311+
bar: 'second',
312+
}
313+
);
314+
315+
this.assert.strictEqual(
316+
shadowRoot.querySelector('#a')?.textContent,
317+
'first',
318+
'first block appended to shadow root'
319+
);
320+
this.assert.strictEqual(
321+
shadowRoot.querySelector('#b')?.textContent,
322+
'second',
323+
'second block appended to shadow root'
324+
);
325+
this.assertHTML('<!----><!---->');
326+
this.assertStableRerender();
327+
328+
this.rerender({ foo: 'updated-first', bar: 'updated-second' });
329+
this.assert.strictEqual(
330+
shadowRoot.querySelector('#a')?.textContent,
331+
'updated-first',
332+
'first block updated'
333+
);
334+
this.assert.strictEqual(
335+
shadowRoot.querySelector('#b')?.textContent,
336+
'updated-second',
337+
'second block updated'
338+
);
339+
this.assertHTML('<!----><!---->');
340+
}
341+
}

0 commit comments

Comments
 (0)