diff --git a/packages/webui-framework/src/element/styles.test.ts b/packages/webui-framework/src/element/styles.test.ts deleted file mode 100644 index c90411f1..00000000 --- a/packages/webui-framework/src/element/styles.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { strict as assert } from 'node:assert'; -import { describe, test, beforeEach, afterEach } from 'node:test'; - -/** Minimal CSSStyleSheet shim for Node.js tests. */ -class FakeCSSStyleSheet { - cssText = ''; - replaceSync(text: string) { - this.cssText = text; - } -} - -function makeStyleDef(specifier: string, css: string) { - return { - type: 'module', - specifier, - textContent: css, - getAttribute(name: string) { - if (name === 'specifier') return this.specifier; - if (name === 'type') return this.type; - return null; - }, - }; -} - -describe('injectModuleStyle', () => { - let prevDocument: unknown; - let prevCSSStyleSheet: unknown; - let headAppendedStyles: Array<{ textContent: string }>; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let styleDefs: any[]; - - beforeEach(() => { - prevDocument = (globalThis as any).document; - prevCSSStyleSheet = (globalThis as any).CSSStyleSheet; - headAppendedStyles = []; - // Each test uses unique specifiers to avoid cross-test cache pollution - styleDefs = []; - - (globalThis as any).CSSStyleSheet = FakeCSSStyleSheet; - (globalThis as any).document = { - querySelectorAll(selector: string) { - if (selector === 'style[type="module"][specifier]') { - return Object.assign([...styleDefs], { length: styleDefs.length }); - } - return Object.assign([], { length: 0 }); - }, - createElement(tag: string) { - return { tagName: tag, textContent: '' }; - }, - head: { - appendChild(el: { textContent: string }) { - headAppendedStyles.push(el); - return el; - }, - }, - }; - }); - - afterEach(() => { - (globalThis as any).document = prevDocument; - (globalThis as any).CSSStyleSheet = prevCSSStyleSheet; - }); - - test('creates CSSStyleSheet and adopts onto shadow root', async () => { - styleDefs.push(makeStyleDef('adopt-test-1', '.a{color:red}')); - const { injectModuleStyle } = await import('./styles.js'); - - const sr = { adoptedStyleSheets: [] as FakeCSSStyleSheet[] }; - injectModuleStyle('adopt-test-1', sr as unknown as ShadowRoot); - - assert.equal(sr.adoptedStyleSheets.length, 1); - assert.equal(sr.adoptedStyleSheets[0].cssText, '.a{color:red}'); - }); - - test('reuses cached sheet for second shadow root', async () => { - styleDefs.push(makeStyleDef('cache-test-1', '.b{color:blue}')); - const { injectModuleStyle } = await import('./styles.js'); - - const sr1 = { adoptedStyleSheets: [] as FakeCSSStyleSheet[] }; - const sr2 = { adoptedStyleSheets: [] as FakeCSSStyleSheet[] }; - - injectModuleStyle('cache-test-1', sr1 as unknown as ShadowRoot); - injectModuleStyle('cache-test-1', sr2 as unknown as ShadowRoot); - - assert.equal(sr1.adoptedStyleSheets.length, 1); - assert.equal(sr2.adoptedStyleSheets.length, 1); - assert.equal( - sr1.adoptedStyleSheets[0], - sr2.adoptedStyleSheets[0], - 'both shadow roots should share the same CSSStyleSheet instance', - ); - }); - - test('does not duplicate adoption on same shadow root', async () => { - styleDefs.push(makeStyleDef('dedup-test-1', '.c{color:green}')); - const { injectModuleStyle } = await import('./styles.js'); - - const sr = { adoptedStyleSheets: [] as FakeCSSStyleSheet[] }; - injectModuleStyle('dedup-test-1', sr as unknown as ShadowRoot); - injectModuleStyle('dedup-test-1', sr as unknown as ShadowRoot); - - assert.equal(sr.adoptedStyleSheets.length, 1); - }); - - test('appends style to head for light DOM (null shadow root)', async () => { - styleDefs.push(makeStyleDef('light-test-1', '.d{color:pink}')); - const { injectModuleStyle } = await import('./styles.js'); - - injectModuleStyle('light-test-1', null); - - assert.equal(headAppendedStyles.length, 1); - assert.equal(headAppendedStyles[0].textContent, '.d{color:pink}'); - }); - - test('skips silently when specifier not found', async () => { - // styleDefs is empty — no definitions - const { injectModuleStyle } = await import('./styles.js'); - - const sr = { adoptedStyleSheets: [] as FakeCSSStyleSheet[] }; - injectModuleStyle('nonexistent-spec', sr as unknown as ShadowRoot); - - assert.equal(sr.adoptedStyleSheets.length, 0); - assert.equal(headAppendedStyles.length, 0); - }); -}); diff --git a/packages/webui-framework/tests/fixtures/css-module/css-module.spec.ts b/packages/webui-framework/tests/fixtures/css-module/css-module.spec.ts index de60fb9d..e3ade890 100644 --- a/packages/webui-framework/tests/fixtures/css-module/css-module.spec.ts +++ b/packages/webui-framework/tests/fixtures/css-module/css-module.spec.ts @@ -12,34 +12,26 @@ test.describe(`css module fixture [${mode} DOM]`, () => { }); test('client-created components adopt module styles from registered specifiers', async ({ page }) => { - const before = await page.locator('test-module-host').evaluate((host) => { - const label = (host.shadowRoot ?? host).querySelector('.host-label'); - // In shadow DOM mode, stylesheets are on shadowRoot. - // In light DOM mode, styles are injected into document head. - const sr = host.shadowRoot; - const sheetCount = sr - ? sr.adoptedStyleSheets?.length ?? 0 - : document.querySelectorAll('style').length > 0 ? 1 : 0; - return { - hasStyles: sheetCount > 0, - hostColor: label instanceof HTMLElement ? getComputedStyle(label).color : null, - }; - }); - - expect(before.hasStyles).toBe(true); - expect(before.hostColor).toBe('rgb(0, 102, 204)'); + // Wait for async CSS module injection (import().then() in injectModuleStyle) + await expect(async () => { + const hostColor = await page.locator('test-module-host').evaluate((host) => { + const label = (host.shadowRoot ?? host).querySelector('.host-label'); + return label instanceof HTMLElement ? getComputedStyle(label).color : null; + }); + expect(hostColor).toBe('rgb(0, 102, 204)'); + }).toPass({ timeout: 5_000 }); await page.locator('test-module-host .spawn').click(); - const after = await page.locator('test-module-host').evaluate((host) => { - const child = (host.shadowRoot ?? host).querySelector('test-module-child'); - const label = (child?.shadowRoot ?? child)?.querySelector('.child-label'); - return { - childColor: label instanceof HTMLElement ? getComputedStyle(label).color : null, - }; - }); - - expect(after.childColor).toBe('rgb(178, 34, 34)'); + // Wait for async CSS module adoption on the dynamically-created child + await expect(async () => { + const childColor = await page.locator('test-module-host').evaluate((host) => { + const child = (host.shadowRoot ?? host).querySelector('test-module-child'); + const label = (child?.shadowRoot ?? child)?.querySelector('.child-label'); + return label instanceof HTMLElement ? getComputedStyle(label).color : null; + }); + expect(childColor).toBe('rgb(178, 34, 34)'); + }).toPass({ timeout: 5_000 }); }); }); } diff --git a/packages/webui-framework/tests/fixtures/css-module/fixture-shadow.html b/packages/webui-framework/tests/fixtures/css-module/fixture-shadow.html index 814721f2..615e16bd 100644 --- a/packages/webui-framework/tests/fixtures/css-module/fixture-shadow.html +++ b/packages/webui-framework/tests/fixtures/css-module/fixture-shadow.html @@ -1,13 +1,26 @@ + CSS Module Fixture - - + + + - - + + + + + - + + \ No newline at end of file diff --git a/packages/webui-framework/tests/fixtures/css-module/fixture.html b/packages/webui-framework/tests/fixtures/css-module/fixture.html index af75c5fe..e4f281e9 100644 --- a/packages/webui-framework/tests/fixtures/css-module/fixture.html +++ b/packages/webui-framework/tests/fixtures/css-module/fixture.html @@ -1,13 +1,24 @@ + CSS Module Fixture - - + + + - + + +

Host

+
+
- + + \ No newline at end of file diff --git a/packages/webui-test-support/src/fixture-build.ts b/packages/webui-test-support/src/fixture-build.ts index 0413095b..159a57cc 100644 --- a/packages/webui-test-support/src/fixture-build.ts +++ b/packages/webui-test-support/src/fixture-build.ts @@ -48,6 +48,7 @@ export async function buildFixtureEntries({ outbase: fixturesRoot, platform: 'browser', target: 'es2022', + supported: { 'import-attributes': true }, tsconfig, logLevel: 'info', });