Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
test: verify that Angular supports bootstrapping under shadow roots
This tests bootstrapping Angular underneath a shadow root and that styles are applied and removed at the correct locations.
  • Loading branch information
dgp1130 committed Mar 28, 2026
commit 5b5a4f0406f345c04936d22c980878ca4a1c0f57
208 changes: 207 additions & 1 deletion packages/platform-browser/test/dom/shadow_dom_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import {Component, NgModule, ViewEncapsulation} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {BrowserModule} from '../../index';
import {BrowserModule, createApplication} from '../../index';
import {expect} from '@angular/private/testing/matchers';
import {isNode} from '@angular/private/testing';

Expand All @@ -23,6 +23,10 @@ describe('ShadowDOM Support', () => {
TestBed.configureTestingModule({imports: [TestModule]});
});

beforeEach(() => {
for (const node of Array.from(document.body.childNodes)) node.remove();
});

it('should attach and use a shadowRoot when ViewEncapsulation.ShadowDom is set', () => {
const compEl = TestBed.createComponent(ShadowComponent).nativeElement;
expect(compEl.shadowRoot!.textContent).toEqual('Hello World');
Expand Down Expand Up @@ -81,6 +85,208 @@ describe('ShadowDOM Support', () => {
expect(articleContent.assignedSlot).toBe(articleSlot);
expect(articleSubcontent.assignedSlot).toBe(articleSlot);
});

it('should support bootstrapping under a shadow root', async () => {
@Component({
selector: 'app-root',
template: '<div>Hello, World!</div>',
styles: `
div {
color: red;
}
`,
})
class Root {}

const container = document.createElement('div');
container.attachShadow({mode: 'open'});
const root = document.createElement('app-root');
container.shadowRoot!.append(root);
document.body.append(container);

const appRef = await createApplication();
appRef.bootstrap(Root, root);

expect(getComputedStyle(root.querySelector('div')!).color).toBe('rgb(255, 0, 0)');

expect(document.head.innerHTML).not.toContain('<style>');

appRef.destroy();

expect(container.shadowRoot!.innerHTML).not.toContain('<style>');
});

it('should support bootstrapping multiple root components under different shadow roots', async () => {
const appRef = await createApplication();

{
@Component({
selector: 'app-root',
template: '<div>Hello, World!</div>',
styles: `
div {
color: red;
}
`,
})
class Root {}

const container = document.createElement('div');
container.attachShadow({mode: 'open'});
const root = document.createElement('app-root');
container.shadowRoot!.append(root);
document.body.append(container);

appRef.bootstrap(Root, root);
expect(getComputedStyle(root.querySelector('div')!).color).toBe('rgb(255, 0, 0)');
}

{
@Component({
selector: 'app-root-2',
template: '<div>Hello, World!</div>',
styles: `
div {
color: lime;
}
`,
})
class Root {}

const container = document.createElement('div');
container.attachShadow({mode: 'open'});
const root = document.createElement('app-root-2');
container.shadowRoot!.append(root);
document.body.append(container);

appRef.bootstrap(Root, root);
expect(getComputedStyle(root.querySelector('div')!).color).toBe('rgb(0, 255, 0)');
}

expect(document.head.innerHTML).not.toContain('<style>');

appRef.destroy();

const containers = Array.from(document.querySelectorAll('div'));
const [shadowRoot1, shadowRoot2] = containers.map((container) => container.shadowRoot!);
expect(shadowRoot1.innerHTML).not.toContain('<style>');
expect(shadowRoot2.innerHTML).not.toContain('<style>');
});

it('should support bootstrapping multiple root components under the same shadow root', async () => {
const container = document.createElement('div');
container.attachShadow({mode: 'open'});
document.body.append(container);

const appRef = await createApplication();

@Component({
selector: 'app-root',
template: '<div>Hello, World!</div>',
styles: `
div {
color: red;
}
`,
})
class Root {}

const rootEl = document.createElement('app-root');
container.shadowRoot!.append(rootEl);

const root = appRef.bootstrap(Root, rootEl);
expect(container.shadowRoot!.innerHTML).toContain('color: red;');
expect(getComputedStyle(rootEl.querySelector('div')!).color).toBe('rgb(255, 0, 0)');

@Component({
selector: 'app-root-2',
template: '<div>Hello, World!</div>',
styles: `
div {
color: lime;
}
`,
})
class Root2 {}

const root2El = document.createElement('app-root-2');
container.shadowRoot!.append(root2El);

const root2 = appRef.bootstrap(Root2, root2El);
expect(container.shadowRoot!.innerHTML).toContain('color: lime;');
expect(getComputedStyle(root2El.querySelector('div')!).color).toBe('rgb(0, 255, 0)');

expect(document.head.innerHTML).not.toContain('<style>');

root.destroy();
expect(container.shadowRoot!.innerHTML).not.toContain('color: red;');
expect(getComputedStyle(root2El.querySelector('div')!).color).toBe('rgb(0, 255, 0)');

root2.destroy();
expect(container.shadowRoot!.innerHTML).not.toContain('color: lime;');

appRef.destroy();
});

it('should not leak styles into previously used shadow roots', async () => {
const container1 = document.createElement('div');
container1.attachShadow({mode: 'open'});
document.body.append(container1);

const container2 = document.createElement('div');
container2.attachShadow({mode: 'open'});
document.body.append(container2);

const appRef = await createApplication();

{
@Component({
selector: 'app-root',
template: '<div>Hello, World!</div>',
styles: `
div {
color: red;
}
`,
})
class Root {}

const rootEl = document.createElement('app-root');
container1.shadowRoot!.append(rootEl);

const root = appRef.bootstrap(Root, rootEl);
expect(getComputedStyle(rootEl.querySelector('div')!).color).toBe('rgb(255, 0, 0)');
root.destroy();

expect(container1.shadowRoot!.innerHTML).not.toContain('<style>');
}

{
@Component({
selector: 'app-root-2',
template: '<div>Hello, World!</div>',
styles: `
div {
color: lime;
}
`,
})
class Root2 {}

const root2El = document.createElement('app-root-2');
container2.shadowRoot!.append(root2El);

const root = appRef.bootstrap(Root2, root2El);
expect(getComputedStyle(root2El.querySelector('div')!).color).toBe('rgb(0, 255, 0)');

// Should not leak into `container1`.
expect(container1.shadowRoot!.innerHTML).not.toContain('<style>');

root.destroy();
}

appRef.destroy();
});
});

@Component({
Expand Down