From dfed9b0bc7f3565ece68b416c3455c4ba259b306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=B3=E5=BB=B7=E5=AE=89?= <73953029+nrps9909@users.noreply.github.com> Date: Fri, 28 Aug 2026 20:19:33 +0800 Subject: [PATCH 1/2] fix: handle missing style container --- src/Dom/dynamicCSS.ts | 24 ++++++++++++++++++++++-- tests/dynamicCSS.test.tsx | 24 ++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/Dom/dynamicCSS.ts b/src/Dom/dynamicCSS.ts index 1abf1b7f..5a566d3d 100644 --- a/src/Dom/dynamicCSS.ts +++ b/src/Dom/dynamicCSS.ts @@ -51,7 +51,11 @@ function getOrder(prepend?: Prepend): AppendType { /** * Find style which inject by rc-util */ -function findStyles(container: ContainerType) { +function findStyles(container: ContainerType | null) { + if (!container) { + return []; + } + return Array.from( (containerCache.get(container) || container).children, ).filter(node => node.tagName === 'STYLE') as HTMLStyleElement[]; @@ -79,6 +83,10 @@ export function injectCSS(css: string, option: Options = {}) { styleNode.innerHTML = css; const container = getContainer(option); + if (!container) { + return null; + } + const { firstChild } = container; if (prepend) { @@ -132,7 +140,7 @@ export function removeCSS(key: string, option: Options = {}) { const existNode = findExistNode(key, option); if (existNode) { const container = getContainer(option); - container.removeChild(existNode); + container?.removeChild(existNode); } } @@ -145,6 +153,10 @@ function syncRealContainer(container: ContainerType, option: Options) { // Find real container when not cached or cached container removed if (!cachedRealContainer || !contains(document, cachedRealContainer)) { const placeholderStyle = injectCSS('', option); + if (!placeholderStyle) { + return; + } + const { parentNode } = placeholderStyle; containerCache.set(container, parentNode); container.removeChild(placeholderStyle); @@ -164,6 +176,10 @@ export function updateCSS( originOption: Options = {}, ) { const container = getContainer(originOption); + if (!container) { + return null; + } + const styles = findStyles(container); const option = { ...originOption, styles }; @@ -185,6 +201,10 @@ export function updateCSS( } const newNode = injectCSS(css, option); + if (!newNode) { + return null; + } + newNode.setAttribute(getMark(option), key); return newNode; } diff --git a/tests/dynamicCSS.test.tsx b/tests/dynamicCSS.test.tsx index 0b3b5db2..9d616731 100644 --- a/tests/dynamicCSS.test.tsx +++ b/tests/dynamicCSS.test.tsx @@ -34,6 +34,18 @@ describe('dynamicCSS', () => { expect(document.querySelector('style').nonce).toEqual('light'); }); + it('does not throw when the document has no style container', () => { + const { head, body } = document; + head.remove(); + body.remove(); + + try { + expect(injectCSS(TEST_STYLE)).toBeNull(); + } finally { + document.documentElement.append(head, body); + } + }); + describe('prepend', () => { function testPrepend() { const head = document.querySelector('head'); @@ -138,6 +150,18 @@ describe('dynamicCSS', () => { expect(document.querySelector('style').innerHTML).toEqual(REPLACE_STYLE); }); + it('does not throw when the document has no style container', () => { + const { head, body } = document; + head.remove(); + body.remove(); + + try { + expect(updateCSS(TEST_STYLE, 'missing-container')).toBeNull(); + } finally { + document.documentElement.append(head, body); + } + }); + it('replace with CSP', () => { const REPLACE_STYLE = '.bamboo { context: "little" }'; updateCSS(REPLACE_STYLE, 'unique', { csp: { nonce: 'only' } }); From 0a7b162f207c9f67ba3f90172af861497437f28e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=B3=E5=BB=B7=E5=AE=89?= <73953029+nrps9909@users.noreply.github.com> Date: Sun, 20 Sep 2026 14:52:41 +0800 Subject: [PATCH 2/2] test: cover transient style container removal --- tests/dynamicCSS.test.tsx | 41 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/dynamicCSS.test.tsx b/tests/dynamicCSS.test.tsx index 9d616731..1cc97d1d 100644 --- a/tests/dynamicCSS.test.tsx +++ b/tests/dynamicCSS.test.tsx @@ -130,6 +130,47 @@ describe('dynamicCSS', () => { expect(document.querySelector('style')).toBeFalsy(); }); + it('can remove styles while the default container is unavailable', () => { + const { head, body } = document; + const style = updateCSS(TEST_STYLE, 'detached'); + head.remove(); + body.remove(); + try { + expect(() => removeCSS('detached')).not.toThrow(); + expect(() => removeCSS('detached', { styles: [style] })).not.toThrow(); + expect(style.parentNode).toBe(head); + } finally { + document.documentElement.append(head, body); + removeCSS('detached'); + clearContainerCache(); + } + }); + + it('returns null when the document containers disappear during style creation', () => { + clearContainerCache(); + const { head, body } = document; + const createElement = document.createElement.bind(document); + const spy = jest + .spyOn(document, 'createElement') + .mockImplementationOnce(tagName => { + head.remove(); + body.remove(); + return createElement(tagName); + }); + try { + expect(updateCSS(TEST_STYLE, 'transient')).toBeNull(); + expect(head.querySelector('style')).toBeNull(); + } finally { + spy.mockRestore(); + document.documentElement.append(head, body); + clearContainerCache(); + } + const restored = updateCSS(TEST_STYLE, 'transient'); + expect(restored.parentNode).toBe(head); + removeCSS('transient'); + clearContainerCache(); + }); + describe('updateCSS', () => { beforeEach(() => { updateCSS(TEST_STYLE, 'unique');