@@ -222,6 +222,119 @@ export class ShadowDOMSuite extends RenderTest {
222222 ) ;
223223 }
224224
225+ @test
226+ 'two sibling shadow root components: only the updated component re-renders' ( ) {
227+ // Use closures so each component can call assert.step without needing access to the test
228+ const assert = this . assert ;
229+
230+ class ComponentA extends GlimmerishComponent {
231+ @tracked value = 'a-initial' ;
232+
233+ constructor ( owner : Owner , args : Dict ) {
234+ super ( owner , args ) ;
235+ aInstance . capture ( this ) ;
236+ }
237+
238+ get renderedValue ( ) {
239+ assert . step ( 'component-a rendered' ) ;
240+ return this . value ;
241+ }
242+ }
243+
244+ class ComponentB extends GlimmerishComponent {
245+ @tracked value = 'b-initial' ;
246+
247+ constructor ( owner : Owner , args : Dict ) {
248+ super ( owner , args ) ;
249+ bInstance . capture ( this ) ;
250+ }
251+
252+ get renderedValue ( ) {
253+ assert . step ( 'component-b rendered' ) ;
254+ return this . value ;
255+ }
256+ }
257+
258+ // Capture holders declared after the class definitions but before render,
259+ // so TypeScript can resolve the type parameters.
260+ const aInstance = this . capture < ComponentA > ( ) ;
261+ const bInstance = this . capture < ComponentB > ( ) ;
262+
263+ this . registerComponent (
264+ 'Glimmer' ,
265+ 'ComponentA' ,
266+ '<template shadowrootmode="open"><span class="a">{{this.renderedValue}}</span></template>' ,
267+ ComponentA as any
268+ ) ;
269+ this . registerComponent (
270+ 'Glimmer' ,
271+ 'ComponentB' ,
272+ '<template shadowrootmode="open"><span class="b">{{this.renderedValue}}</span></template>' ,
273+ ComponentB as any
274+ ) ;
275+
276+ this . render ( '<div class="host-a"><ComponentA /></div><div class="host-b"><ComponentB /></div>' ) ;
277+
278+ // Both rendered on initial render — consume those steps
279+ this . assert . verifySteps (
280+ [ 'component-a rendered' , 'component-b rendered' ] ,
281+ 'both components render initially'
282+ ) ;
283+
284+ const rootEl = castToBrowser ( this . element , 'HTML' ) ;
285+ const hostA = rootEl . querySelector ( '.host-a' ) as HTMLElement | null ;
286+ const hostB = rootEl . querySelector ( '.host-b' ) as HTMLElement | null ;
287+
288+ this . assert . strictEqual (
289+ hostA ?. shadowRoot ?. querySelector ( '.a' ) ?. textContent ,
290+ 'a-initial' ,
291+ 'ComponentA initial value'
292+ ) ;
293+ this . assert . strictEqual (
294+ hostB ?. shadowRoot ?. querySelector ( '.b' ) ?. textContent ,
295+ 'b-initial' ,
296+ 'ComponentB initial value'
297+ ) ;
298+
299+ // Mutate only ComponentA — ComponentB must NOT re-render
300+ aInstance . captured . value = 'a-updated' ;
301+ this . rerender ( ) ;
302+
303+ this . assert . strictEqual (
304+ hostA ?. shadowRoot ?. querySelector ( '.a' ) ?. textContent ,
305+ 'a-updated' ,
306+ 'ComponentA updated after tracked mutation'
307+ ) ;
308+ this . assert . strictEqual (
309+ hostB ?. shadowRoot ?. querySelector ( '.b' ) ?. textContent ,
310+ 'b-initial' ,
311+ 'ComponentB unchanged'
312+ ) ;
313+ this . assert . verifySteps (
314+ [ 'component-a rendered' ] ,
315+ 'only ComponentA re-renders when its tracked value changes'
316+ ) ;
317+
318+ // Mutate only ComponentB — ComponentA must NOT re-render
319+ bInstance . captured . value = 'b-updated' ;
320+ this . rerender ( ) ;
321+
322+ this . assert . strictEqual (
323+ hostA ?. shadowRoot ?. querySelector ( '.a' ) ?. textContent ,
324+ 'a-updated' ,
325+ 'ComponentA still unchanged'
326+ ) ;
327+ this . assert . strictEqual (
328+ hostB ?. shadowRoot ?. querySelector ( '.b' ) ?. textContent ,
329+ 'b-updated' ,
330+ 'ComponentB updated after tracked mutation'
331+ ) ;
332+ this . assert . verifySteps (
333+ [ 'component-b rendered' ] ,
334+ 'only ComponentB re-renders when its tracked value changes'
335+ ) ;
336+ }
337+
225338 @test
226339 'conditional <template shadowrootmode="open"> inside a host element attaches shadow root when rendered' ( ) {
227340 this . render (
0 commit comments