diff --git a/src/scriptlets/inject-css-in-shadow-dom.js b/src/scriptlets/inject-css-in-shadow-dom.js index 656deee5a..c9f71271b 100644 --- a/src/scriptlets/inject-css-in-shadow-dom.js +++ b/src/scriptlets/inject-css-in-shadow-dom.js @@ -1,5 +1,6 @@ import { hit, + logMessage, hijackAttachShadow, } from '../helpers/index'; @@ -11,10 +12,10 @@ import { * * **Syntax** * ``` - * example.org#%#//scriptlet('inject-css-in-shadow-dom', cssRule) + * example.org#%#//scriptlet('inject-css-in-shadow-dom', cssText) * ``` * - * - `cssRule` - required, string of comma-separated css rules + * - `cssText` - required, string of comma-separated css rules * * **Examples** * ``` @@ -22,28 +23,43 @@ import { * example.org#%#//scriptlet('inject-css-in-shadow-dom', '#advertisement { display: none !important; }') * * ! apply multiple css rules - * example.org#%#//scriptlet('inject-css-in-shadow-dom', '#advertisement { display: none !important; }, #content { margin-top: 0 !important; }') + * example.org#%#//scriptlet('inject-css-in-shadow-dom', '#advertisement { display: none !important; }|#content { margin-top: 0 !important; }') * ``` */ /* eslint-enable max-len */ -export function injectCssInShadowDom(source, cssRule) { +export function injectCssInShadowDom(source, cssText) { // do nothing if browser does not support ShadowRoot // https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot if (!Element.prototype.attachShadow) { return; } - const parsedStyleRules = cssRule.split(','); + const parsedStyleRules = cssText.split('|'); const callback = (shadowRoot) => { const stylesheet = new CSSStyleSheet(); // fill stylesheet with rules - parsedStyleRules.forEach((rule) => stylesheet.insertRule(rule)); + parsedStyleRules.forEach((rule) => { + try { + stylesheet.insertRule(rule); + } catch { + logMessage(source, `Failed to parse the rule: ${rule}`); + } + }); // attach stylesheet to shadow root so the whole subtree would be affected - // https://developer.mozilla.org/en-US/docs/Web/API/Document/adoptedStyleSheets - shadowRoot.adoptedStyleSheets = [...shadowRoot.adoptedStyleSheets, stylesheet]; + if (shadowRoot.adoptedStyleSheets) { + // adoptedStyleSheets is not yet supported by Safari + // https://developer.mozilla.org/en-US/docs/Web/API/Document/adoptedStyleSheets + shadowRoot.adoptedStyleSheets = [...shadowRoot.adoptedStyleSheets, stylesheet]; + } else { + const styleTag = document.createElement('style'); + styleTag.innerText = cssText; + shadowRoot.appendChild(styleTag); + } + + hit(source); }; hijackAttachShadow(window, callback); @@ -55,5 +71,6 @@ injectCssInShadowDom.names = [ injectCssInShadowDom.injections = [ hit, + logMessage, hijackAttachShadow, ]; diff --git a/tests/scriptlets/index.test.js b/tests/scriptlets/index.test.js index aa6410e30..e27d0be95 100644 --- a/tests/scriptlets/index.test.js +++ b/tests/scriptlets/index.test.js @@ -50,3 +50,4 @@ import './trusted-set-cookie.test'; import './trusted-replace-fetch-response.test'; import './trusted-set-local-storage-item.test'; import './trusted-set-constant.test'; +import './inject-css-in-shadow-dom.test'; diff --git a/tests/scriptlets/inject-css-in-shadow-dom.test.js b/tests/scriptlets/inject-css-in-shadow-dom.test.js new file mode 100644 index 000000000..31119aa39 --- /dev/null +++ b/tests/scriptlets/inject-css-in-shadow-dom.test.js @@ -0,0 +1,63 @@ +/* eslint-disable no-underscore-dangle */ +import { runScriptlet, clearGlobalProps } from '../helpers'; + +const { test, module } = QUnit; +const name = 'inject-css-in-shadow-dom'; + +const HOST_ID = 'host'; +const TARGET_ID = 'target'; +const TARGET_CSS_PROP = 'color'; +const TARGET_CSS_VALUE = 'rgb(255, 0, 0)'; +const CSS_TEXT = `#target { ${TARGET_CSS_PROP}: ${TARGET_CSS_VALUE} !important}`; +// const CSS_TEXT = '#target { color: red !important}'; + +const appendTarget = (parent) => { + const target = document.createElement('h1'); + target.id = TARGET_ID; + target.innerText = 'Target element'; + return parent.appendChild(target); +}; + +const appendHost = () => { + const host = document.createElement('div'); + host.id = HOST_ID; + return document.body.appendChild(host); +}; + +const removeHost = () => document.getElementById(HOST_ID)?.remove(); + +const beforeEach = () => { + window.__debug = () => { + window.hit = 'FIRED'; + }; +}; + +const afterEach = () => { + clearGlobalProps('hit', '__debug'); + removeHost(); +}; + +module(name, { beforeEach, afterEach }); + +// some browsers do not support ShadowRoot +// for example, Firefox 52 which is used for browserstack tests +// https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot +const isSupported = typeof Element.prototype.attachShadow !== 'undefined'; + +if (!isSupported) { + test('unsupported', (assert) => { + assert.ok(true, 'Browser does not support it'); + }); +} else { + test('styles applied to shadow root subtree with adoptedStyleSheets', (assert) => { + runScriptlet(name, [CSS_TEXT]); + + const host = appendHost(); + const shadowRoot = host.attachShadow({ mode: 'closed' }); + appendTarget(shadowRoot); + + const target = shadowRoot.getElementById(TARGET_ID); + assert.strictEqual(getComputedStyle(target)[TARGET_CSS_PROP], TARGET_CSS_VALUE, 'style was applied on target'); + assert.strictEqual(window.hit, 'FIRED', 'hit function was executed'); + }); +}