Files
halo-dark-mode-plugin/third-party/darkreader/tests/inject/dynamic/variables.tests.ts
T

1321 lines
53 KiB
TypeScript

import '../support/polyfills';
import {DEFAULT_THEME} from '../../../src/defaults';
import {createOrUpdateDynamicTheme, removeDynamicTheme} from '../../../src/inject/dynamic-theme';
import {injectProxy} from '../../../src/inject/dynamic-theme/stylesheet-proxy';
import {isFirefox} from '../../../src/utils/platform';
import {stubChromeRuntimeGetURL} from '../support/background-stub';
import {getJSEchoURL} from '../support/echo-client';
import {multiline, timeout, waitForEvent} from '../support/test-utils';
const theme = {
...DEFAULT_THEME,
darkSchemeBackgroundColor: 'black',
darkSchemeTextColor: 'white',
};
let container: HTMLElement;
beforeAll(() => {
const loader = multiline(
'(function loader() {',
' document && document.currentScript && document.currentScript.remove();',
' const argString = document && document.currentScript && document.currentScript.dataset.arg;',
' if (argString !== undefined) {',
' const arg = JSON.parse(argString);',
` (${injectProxy.toString()})(arg);`,
' }',
'})()',
);
const url = getJSEchoURL(loader);
stubChromeRuntimeGetURL('inject/proxy.js', url);
});
beforeEach(() => {
container = document.body;
container.innerHTML = '';
});
afterEach(() => {
removeDynamicTheme();
container.innerHTML = '';
document.documentElement.removeAttribute('style');
});
describe('CSS VARIABLES OVERRIDE', () => {
it('should override style with variables', () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --bg: gray;',
' --text: red;',
' }',
' h1 { background: var(--bg); }',
' h1 strong { color: var(--text); }',
'</style>',
'<h1>CSS <strong>variables</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container).backgroundColor).toBe('rgb(0, 0, 0)');
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(102, 102, 102)');
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(255, 255, 255)');
expect(getComputedStyle(container.querySelector('h1 strong')!).color).toBe('rgb(255, 26, 26)');
});
it('should override style with variables(that contain spaces)', () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --bg: gray;',
' --text: red;',
' }',
' h1 { background: var( --bg ); }',
' h1 strong { color: var( --text ); }',
'</style>',
'<h1>CSS <strong>variables</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container).backgroundColor).toBe('rgb(0, 0, 0)');
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(102, 102, 102)');
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(255, 255, 255)');
expect(getComputedStyle(container.querySelector('h1 strong')!).color).toBe('rgb(255, 26, 26)');
});
it('should override style with variables (reverse order)', () => {
container.innerHTML = multiline(
'<style>',
' h1 { background: var(--bg); }',
' h1 strong { color: var(--text); }',
'</style>',
'<style>',
' :root {',
' --bg: gray;',
' --text: green;',
' }',
'</style>',
'<h1>CSS <strong>variables</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container).backgroundColor).toBe('rgb(0, 0, 0)');
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(102, 102, 102)');
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(255, 255, 255)');
expect(getComputedStyle(container.querySelector('h1 strong')!).color).toBe('rgb(140, 255, 140)');
});
it('should skip undefined variables', () => {
container.innerHTML = multiline(
'<style>',
' h1 { background: var(--bg); }',
' h1 strong { color: var(--text); }',
'</style>',
'<h1>CSS <strong>variables</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container).backgroundColor).toBe('rgb(0, 0, 0)');
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgba(0, 0, 0, 0)');
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(255, 255, 255)');
expect(getComputedStyle(container.querySelector('h1 strong')!).color).toBe('rgb(255, 255, 255)');
});
it('should use fallback variable value', () => {
container.innerHTML = multiline(
'<style>',
' h1 { color: var(--text, red); }',
'</style>',
'<h1>CSS <strong>variables</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(255, 26, 26)');
});
it('should not use fallback variable value', () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --text: green;',
' }',
' h1 { color: var(--text, red); }',
'</style>',
'<h1>CSS <strong>variables</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(140, 255, 140)');
});
it('should handle variables referencing other variables', () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --alert: red;',
' --text: var(--alert);',
' }',
' h1 { color: var(--text); }',
'</style>',
'<h1>CSS <strong>variables</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(255, 26, 26)');
});
it('should handle variables referencing other variables (reverse order)', () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --text: var(--alert);',
' --alert: red;',
' }',
' h1 { color: var(--text); }',
'</style>',
'<h1>CSS <strong>variables</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(255, 26, 26)');
});
it('should handle shorthand background with deep color refs', () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --red: red;',
' --bg: var(--red);',
' }',
' h1 {',
' background: var(--bg);',
' }',
'</style>',
'<h1>CSS <strong>variables</strong></h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(204, 0, 0)');
});
it('should handle background with deep color refs (backwards)', () => {
container.innerHTML = multiline(
'<style>',
' h1 {',
' background: var(--bg);',
' }',
' :root {',
' --red: red;',
' --bg: var(--red);',
' }',
'</style>',
'<h1>CSS <strong>variables</strong></h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(204, 0, 0)');
});
it('should handle multi-type vars with deep color refs', () => {
container.innerHTML = multiline(
'<style>',
' h1 {',
' background: var(--bg);',
' color: var(--red);',
' }',
' :root {',
' --red: red;',
' --bg: var(--red);',
' }',
'</style>',
'<h1>CSS <strong>variables</strong></h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(204, 0, 0)');
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(255, 26, 26)');
});
it('should not hang on exponentially expanding variable references', () => {
const depth = 30;
const varLines: string[] = [];
for (let i = 1; i < depth; i++) {
varLines.push(` --x${i}: var(--x${i + 1}) var(--x${i + 1});`);
}
varLines.push(` --x${depth}: #770000;`);
container.innerHTML = multiline(
'<style>',
' :root {',
...varLines,
' }',
' h1 {',
' color: rgb(var(--x1), 0, 0);',
' }',
'</style>',
'<h1>CSS <strong>variables</strong></h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(255, 255, 255)');
});
it('should handle variables having multiple types', () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --dark: red;',
' --light: green;',
' }',
' .dark {',
' background-color: var(--dark);',
' color: var(--light);',
' }',
' .light {',
' background: var(--light);',
' color: var(--dark);',
' }',
'</style>',
'<h1 class="dark">Dark background light text</h1>',
'<h1 class="light">Light background dark text</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('.dark')!).backgroundColor).toBe('rgb(204, 0, 0)');
expect(getComputedStyle(container.querySelector('.dark')!).color).toBe('rgb(140, 255, 140)');
expect(getComputedStyle(container.querySelector('.light')!).backgroundColor).toBe('rgb(0, 102, 0)');
expect(getComputedStyle(container.querySelector('.light')!).color).toBe('rgb(255, 26, 26)');
});
/*
it('should handle variables having multiple types in shorthand properties', () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --color: green;',
' }',
' h1 {',
' border: 1px solid var(--color);',
' }',
' h1 {',
' background: linear-gradient(var(--color), white);',
' }',
'</style>',
'<h1>Variables</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).backgroundImage).toBe('linear-gradient(rgb(0, 102, 0), rgb(0, 0, 0))');
expect(getComputedStyle(container.querySelector('h1')!).borderColor).toBe('rgb(0, 217, 0)');
});
*/
it('should use fallback when nested variables are missing', () => {
container.innerHTML = multiline(
'<style>',
' h1 { color: var(--text, var(--alert, green)); }',
'</style>',
'<h1>CSS <strong>variables</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(140, 255, 140)');
});
it('should not freeze on cyclic references', () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --text: var(--text);',
' }',
' h1 { color: var(--text); }',
'</style>',
'<h1>CSS <strong>variables</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(255, 255, 255)');
});
it('should not freeze on nested cyclic references', () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --alert: var(--text);',
' --text: var(--alert);',
' }',
' h1 { color: var(--text); }',
'</style>',
'<h1>CSS <strong>variables</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(255, 255, 255)');
});
it('should react on variable change', async () => {
container.innerHTML = multiline(
'<style id="variables">',
' :root {',
' --text: red;',
' }',
'</style>',
'<style>',
' h1 { color: var(--text); }',
'</style>',
'<h1>CSS <strong>variables</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(255, 26, 26)');
const styleElement = document.getElementById('variables') as HTMLStyleElement;
styleElement.textContent = ':root { --text: green; }';
await timeout(0);
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(140, 255, 140)');
});
it('should use <html> element variables', async () => {
document.documentElement.setAttribute('style', '--text: red;');
container.innerHTML = multiline(
'<style>',
' h1 { color: var(--text); }',
'</style>',
'<h1>CSS <strong>variables</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(255, 26, 26)');
document.documentElement.setAttribute('style', '--text: green;');
await timeout(0);
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(140, 255, 140)');
});
it('should consider variable selector', () => {
container.innerHTML = multiline(
'<style>',
' .red {',
' --text: red;',
' }',
' .green {',
' --text: green;',
' }',
' h1 { color: var(--text); }',
'</style>',
'<h1 class="red">Red CSS variable</h1>',
'<h1 class="green">Green CSS variable</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('.red')!).color).toBe('rgb(255, 26, 26)');
expect(getComputedStyle(container.querySelector('.green')!).color).toBe('rgb(140, 255, 140)');
});
it('should handle internal conversion of hex to RGB', async () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --bg: #fff;',
' --text: #000;',
' }',
'</style>',
'<style>',
' h1 {',
' background: var(--bg);',
' color: var(--text);',
' }',
'</style>',
'<h1>Dark <strong>Theme</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(255, 255, 255)');
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(0, 0, 0)');
});
it('should handle variables inside color values (constructed colors)', async () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --bg: 255, 255, 255;',
' --text: 0, 0, 0;',
' }',
'</style>',
'<style>',
' h1 {',
' background: rgb(var(--bg));',
' color: rgb(var(--text));',
' }',
'</style>',
'<h1>Colors with variables inside</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(255, 255, 255)');
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(0, 0, 0)');
});
it('should use fallback when variable inside a color not found', async () => {
container.innerHTML = multiline(
'<style>',
' h1 {',
' background: rgb(var(--bg, 255, 0, 0));',
' }',
'</style>',
'<h1>Colors with variables inside</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(204, 0, 0)');
});
it('should handle variables in constructed colors that refer to other vars', async () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --v255: 255;',
' --red: var(--v255), 0, 0;',
' --bg: var(--unknown, var(--red));',
' --text: var(--red);',
' }',
'</style>',
'<style>',
' h1 {',
' background: rgb(var(--bg));',
' color: rgb(var(--text));',
' }',
'</style>',
'<h1>Colors with variables inside</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(255, 26, 26)');
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(204, 0, 0)');
});
it('should handle variables that refer to constructed colors', async () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --red: 255, 0, 0;',
' --blue: 0, 0, 255;',
' --rgb-blue: rgb(var(--blue));',
' --bg: rgb(var(--red));',
' --text: var(--rgb-blue);',
' }',
'</style>',
'<style>',
' h1 {',
' background: var(--bg);',
' color: var(--text);',
' }',
'</style>',
'<h1>Colors with variables inside</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(204, 0, 0)');
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(51, 125, 255)');
});
it('should handle variables that refer to constructed colors asynchronously', async () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --red: 255, 0, 0;',
' --bg: rgb(var(--red));',
' }',
' h1 {',
' color: var(--text);',
' }',
'</style>',
'<h1>Colors with variables inside</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
const anotherStyle = document.createElement('style');
anotherStyle.textContent = multiline(
':root {',
' --blue: 0, 0, 255;',
' --rgb-blue: rgb(var(--blue));',
' --text: var(--rgb-blue);',
'}',
'h1 {',
' background: var(--bg);',
'}',
);
container.append(anotherStyle);
await timeout(0);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(204, 0, 0)');
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(51, 125, 255)');
});
it('should handle variables that are both contructed and usual colors', async () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --red: 255, 0, 0;',
' --bg: green;',
' }',
' h1 {',
' --bg: rgb(var(--red));',
' background: var(--bg)',
' }',
'</style>',
'<h1>Colors with variables inside</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(204, 0, 0)');
});
it('should handle cyclic references in constructed colors', async () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --bg: var(--text);',
' --text: var(--bg);',
' }',
'</style>',
'<style>',
' h1 {',
' background: rgb(var(--bg));',
' color: rgb(var(--text));',
' }',
'</style>',
'<h1>Colors with variables inside</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(255, 255, 255)');
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(0, 0, 0)');
});
it('should handle variables inside border color values', async () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --red: 255, 0, 0;',
' }',
'</style>',
'<style>',
' h1 {',
' border: 1px solid rgb(var(--red));',
' }',
'</style>',
'<h1>Colors with variables inside</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
const elementStyle = getComputedStyle(container.querySelector('h1')!);
if (isFirefox) {
expect(elementStyle.borderTopColor).toBe('rgb(179, 0, 0)');
expect(elementStyle.borderRightColor).toBe('rgb(179, 0, 0)');
expect(elementStyle.borderBottomColor).toBe('rgb(179, 0, 0)');
expect(elementStyle.borderLeftColor).toBe('rgb(179, 0, 0)');
} else {
expect(elementStyle.borderColor).toBe('rgb(179, 0, 0)');
}
});
it('should handle media variables', () => {
container.innerHTML = multiline(
'<style>',
' @media screen and (min-width: 2px) {',
' .red {',
' --text: red;',
' }',
' .orange {',
' --text: orange',
' }',
' }',
' @media screen and (min-width: 2000000px) {',
' .green {',
' --text: green;',
' }',
' }',
' h1 { color: var(--text); }',
'</style>',
'<h1 class="red">Red CSS variable</h1>',
'<h1 class="green">Green CSS variable</h1>',
'<h1 class="orange">Orange CSS variable</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container).backgroundColor).toBe('rgb(0, 0, 0)');
expect(getComputedStyle(container.querySelector('.red')!).color).toBe('rgb(255, 26, 26)');
expect(getComputedStyle(container.querySelector('.green')!).color).toBe('rgb(255, 255, 255)');
expect(getComputedStyle(container.querySelector('.orange')!).color).toBe('rgb(255, 174, 26)');
});
it('should handle nested variables', () => {
container.innerHTML = multiline(
'<style>',
' @media screen and (min-width: 2px) {',
' @media screen and (min-width: 3px) {',
' .red {',
' --text: red;',
' }',
' }',
' }',
' @media screen and (min-width: 2px) {',
' @media screen and (min-width: 2000000px) {',
' .green {',
' --text: green;',
' }',
' }',
' }',
' @media screen and (min-width: 2000000px) {',
' @media screen and (min-width: 2px) {',
' .orange {',
' --text: orange;',
' }',
' }',
' }',
' h1 {',
' color: var(--text);',
' }',
'</style>',
'<h1 class="red">Red CSS variable</h1>',
'<h1 class="green">Green CSS variable</h1>',
'<h1 class="orange">Orange CSS variable</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container).backgroundColor).toBe('rgb(0, 0, 0)');
expect(getComputedStyle(container.querySelector('.red')!).color).toBe('rgb(255, 26, 26)');
expect(getComputedStyle(container.querySelector('.green')!).color).toBe('rgb(255, 255, 255)');
expect(getComputedStyle(container.querySelector('.orange')!).color).toBe('rgb(255, 255, 255)');
});
it('should handle media with the same selectors', () => {
container.innerHTML = multiline(
'<style>',
' @media screen and (min-width: 2px) {',
' h1 {',
' --text: green;',
' }',
' }',
' @media screen and (max-width: 2px) {',
' h1 {',
' --text: red;',
' }',
' }',
' h1 { color: var(--text); }',
'</style>',
'<h1>Media with same selectors</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(140, 255, 140)');
});
it('should preserve media after change', () => {
container.innerHTML = multiline(
'<style>',
' @media screen and (min-width: 2px) {',
' /* This media should be used */',
' h1 {',
' --text: green;',
' }',
' }',
' @media screen and (max-width: 2px) {',
' /* This media should not be used */',
' h1 {',
' --text: red;',
' }',
' }',
' h1 { color: var(--text); }',
'</style>',
'<h1>Media after change</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(140, 255, 140)');
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(140, 255, 140)');
});
it('should handle same selector with different variables', () => {
container.innerHTML = multiline(
'<style>',
' @media screen and (min-width: 2px) {',
' h1 {',
' --text: green;',
' }',
' h1 {',
' --bg: red;',
' }',
' }',
' h1 { color: var(--text); background-color: var(--bg); }',
'</style>',
'<h1>Media with same selectors</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(140, 255, 140)');
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(204, 0, 0)');
});
it('should properly modify variables in light mode', () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --bg: white;',
' --text: black;',
' }',
' h1 {',
' background: var(--bg);',
' color: var(--text);',
' }',
'</style>',
'<h1>Light scheme</h1>',
);
const lightTheme = {...theme, mode: 0, lightSchemeBackgroundColor: '#dddddd', lightSchemeTextColor: '#222222'};
createOrUpdateDynamicTheme(lightTheme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(221, 221, 221)');
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(34, 34, 34)');
});
it('should handle variables inside values', () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --border-color: #ff0000;',
' --text: green;',
' }',
' h1 {',
' border-bottom: 2px solid var(--border-color);',
' color: var(--text);',
' }',
'</style>',
'<h1>Border with variable</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).borderBottomColor).toBe('rgb(179, 0, 0)');
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(140, 255, 140)');
});
it('should handle variables that have variables inside', () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --dark-red: #ff0000;',
' --border: 2px solid var(--dark-red);',
' --text: green;',
' }',
' h1 {',
' border: var(--border);',
' color: var(--text);',
' }',
'</style>',
'<h1>Border with variable</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).borderTopColor).toBe('rgb(179, 0, 0)');
expect(getComputedStyle(container.querySelector('h1')!).borderBottomColor).toBe('rgb(179, 0, 0)');
expect(getComputedStyle(container.querySelector('h1')!).borderLeftColor).toBe('rgb(179, 0, 0)');
expect(getComputedStyle(container.querySelector('h1')!).borderRightColor).toBe('rgb(179, 0, 0)');
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(140, 255, 140)');
});
it('should handle border color variables', () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --border-color: green;',
' }',
' h1 {',
' border: 1px solid;',
' border-color: var(--border-color);',
' }',
'</style>',
'<h1>Border color with variable</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
const elementStyle = getComputedStyle(container.querySelector('h1')!);
if (isFirefox) {
expect(elementStyle.borderTopColor).toBe('rgb(0, 217, 0)');
expect(elementStyle.borderRightColor).toBe('rgb(0, 217, 0)');
expect(elementStyle.borderBottomColor).toBe('rgb(0, 217, 0)');
expect(elementStyle.borderLeftColor).toBe('rgb(0, 217, 0)');
} else {
expect(elementStyle.borderColor).toBe('rgb(0, 217, 0)');
}
});
it('should handle variables with gradients', () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --text: red;',
' --gradient: linear-gradient(red, white);',
' --bg: green;',
' }',
' h1 {',
' color: var(--text);',
' background-color: var(--bg);',
' background-image: var(--gradient);',
' }',
' h2 {',
' background: var(--gradient);',
' }',
'</style>',
'<h1>Weow Gradients</h1>',
'<h2>Gradient 2</h2>'
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(255, 26, 26)');
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(0, 102, 0)');
expect(getComputedStyle(container.querySelector('h1')!).backgroundImage).toBe('linear-gradient(rgb(204, 0, 0), rgb(0, 0, 0))');
expect(getComputedStyle(container.querySelector('h2')!).backgroundImage).toBe('linear-gradient(rgb(204, 0, 0), rgb(0, 0, 0))');
});
it('should handle variables with background images', async () => {
const darkIcon = multiline(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" width="8" height="8">',
' <circle fill="black" cx="4" cy="4" r="3" />',
'</svg>',
);
const redCross = multiline(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" width="8" height="8">',
' <path fill="red" d="M3,1 h2 v2 h2 v2 h-2 v2 h-2 v-2 h-2 v-2 h2 z" />',
'</svg>',
);
container.innerHTML = multiline(
'<style>',
' :root {',
` --icon: url("data:image/svg+xml;base64,${btoa(darkIcon)}");`,
` --red-cross: url("data:image/svg+xml;base64,${btoa(redCross)}");`,
' --bg: green;',
' }',
' .icon1 {',
' background-color: var(--bg);',
' background-image: var(--icon);',
' background-repeat: no-repeat;',
' background-size: 100%;',
' display: inline-block;',
' height: 1rem;',
' width: 1rem;',
' }',
' .icon2 {',
' background: no-repeat center/100% var(--icon);',
' display: inline-block;',
' height: 1rem;',
' width: 1rem;',
' }',
' .icon3 {',
' background: no-repeat center/100% var(--red-cross), no-repeat center/100% var(--icon);',
' display: inline-block;',
' height: 1rem;',
' width: 1rem;',
' }',
'</style>',
'<h1>',
' <i class="icon1"></i>',
' <i class="icon2"></i>',
' <i class="icon3"></i>',
' Icons',
'</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
await waitForEvent('__darkreader__test__asyncQueueComplete');
expect(getComputedStyle(container.querySelector('.icon1')!).backgroundImage).toMatch(/^url\("blob:.*"\)$/);
expect(getComputedStyle(container.querySelector('.icon2')!).backgroundImage).toMatch(/^url\("blob:.*"\)$/);
expect(getComputedStyle(container.querySelector('.icon3')!).backgroundImage).toMatch(/^url\("blob:.*"\), url\("blob:.*"\)$/);
});
it('should handle variables with gradients and images', async () => {
const darkIcon = multiline(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" width="8" height="8">',
' <circle fill="black" cx="4" cy="4" r="3" />',
'</svg>',
);
container.innerHTML = multiline(
'<style>',
' :root {',
` --icon: url("data:image/svg+xml;base64,${btoa(darkIcon)}");`,
' --gradient: linear-gradient(red, white);',
' }',
' .icon {',
' background: no-repeat center/100% var(--icon), var(--gradient);',
' display: inline-block;',
' height: 1rem;',
' width: 1rem;',
' }',
'</style>',
'<h1><i class="icon"></i>Mixed background</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
await waitForEvent('__darkreader__test__asyncQueueComplete');
expect(getComputedStyle(container.querySelector('.icon')!).backgroundImage).toMatch(/^url\("blob:.*"\), linear-gradient\(rgb\(204, 0, 0\), rgb\(0, 0, 0\)\)$/);
});
it('should handle asynchronous variable type resolution', async () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --color1: red;',
' --color2: green;',
' }',
'</style>',
'<h1>Asynchronous variables</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgba(0, 0, 0, 0)');
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(255, 255, 255)');
const anotherStyle = document.createElement('style');
anotherStyle.textContent = multiline(
'h1 {',
' background: var(--color2);',
' color: var(--color1);',
'}',
);
container.append(anotherStyle);
await timeout(0);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(0, 102, 0)');
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(255, 26, 26)');
});
it('should handle async resolution of multiple variable types', async () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --color: green;',
' }',
'</style>',
'<h1>Asynchronous variables with multiple types</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgba(0, 0, 0, 0)');
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(255, 255, 255)');
const anotherStyle = document.createElement('style');
anotherStyle.textContent = multiline(
'h1 {',
' background: var(--color);',
' border: 1px solid var(--color);',
' color: var(--color);',
'}',
);
container.append(anotherStyle);
await timeout(0);
const updatedStyle = getComputedStyle(container.querySelector('h1')!);
expect(updatedStyle.backgroundColor).toBe('rgb(0, 102, 0)');
expect(updatedStyle.color).toBe('rgb(140, 255, 140)');
if (isFirefox) {
expect(updatedStyle.borderTopColor).toBe('rgb(0, 217, 0)');
expect(updatedStyle.borderRightColor).toBe('rgb(0, 217, 0)');
expect(updatedStyle.borderBottomColor).toBe('rgb(0, 217, 0)');
expect(updatedStyle.borderLeftColor).toBe('rgb(0, 217, 0)');
} else {
expect(updatedStyle.borderColor).toBe('rgb(0, 217, 0)');
}
});
it('should handle variable type resolution when style changed', async () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --color1: yellow;',
' --color2: blue;',
' }',
'</style>',
'<h1>Asynchronous variables</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgba(0, 0, 0, 0)');
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(255, 255, 255)');
const styleEl = container.querySelector('style')!;
styleEl.textContent = multiline(
':root {',
' --color1: red;',
' --color2: green;',
' --color3: blue;',
'}',
);
await timeout(0);
const anotherStyle = document.createElement('style');
anotherStyle.textContent = multiline(
'h1 {',
' background: var(--color2);',
' color: var(--color1);',
'}',
);
container.append(anotherStyle);
await timeout(0);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(0, 102, 0)');
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(255, 26, 26)');
});
it('should handle variable type change', async () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --color: green;',
' }',
' h1 {',
' background: var(--color);',
' }',
'</style>',
'<h1>Asynchronous variables</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(0, 102, 0)');
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(255, 255, 255)');
const anotherStyle = document.createElement('style');
anotherStyle.textContent = multiline(
'h1 {',
' color: var(--color);',
'}',
);
container.append(anotherStyle);
await timeout(0);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(0, 102, 0)');
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(140, 255, 140)');
});
it('should rebuild dependant variable rule when type becomes known', async () => {
container.innerHTML = multiline(
'<style>',
' h1 {',
' background: var(--color);',
' }',
'</style>',
'<h1>Asynchronous variables</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgba(0, 0, 0, 0)');
const anotherStyle = document.createElement('style');
anotherStyle.textContent = multiline(
':root {',
' --color: green;',
'}',
);
container.append(anotherStyle);
await timeout(50);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(0, 102, 0)');
});
it('should not affect other declarations when variable type resolved', async () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --color1: red;',
' --color2: green;',
' }',
'</style>',
'<h1 class="color1">Variables with color 1</h1>',
'<h1 class="color2">Variables with color 2</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('.color1')!).backgroundColor).toBe('rgba(0, 0, 0, 0)');
expect(getComputedStyle(container.querySelector('.color1')!).color).toBe('rgb(255, 255, 255)');
expect(getComputedStyle(container.querySelector('.color2')!).backgroundColor).toBe('rgba(0, 0, 0, 0)');
expect(getComputedStyle(container.querySelector('.color2')!).color).toBe('rgb(255, 255, 255)');
const anotherStyle = document.createElement('style');
anotherStyle.textContent = multiline(
'.color1 {',
' background: var(--color1);',
' color: var(--color1);',
'}',
'.color2 {',
' background: var(--color2);',
' color: var(--color2);',
'}',
);
container.append(anotherStyle);
await timeout(0);
expect(getComputedStyle(container.querySelector('.color1')!).backgroundColor).toBe('rgb(204, 0, 0)');
expect(getComputedStyle(container.querySelector('.color1')!).color).toBe('rgb(255, 26, 26)');
expect(getComputedStyle(container.querySelector('.color2')!).backgroundColor).toBe('rgb(0, 102, 0)');
expect(getComputedStyle(container.querySelector('.color2')!).color).toBe('rgb(140, 255, 140)');
});
it('should not affect other declarations when variable type resolved', async () => {
container.innerHTML = multiline(
'<style>',
' h1 {',
' --color-bg: red;',
' border: green;',
' }',
' h1 {',
' --color-text: green;',
' }',
'</style>',
'<h1>Variables along with other declarations</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
const elementStyle = getComputedStyle(container.querySelector('h1')!);
expect(elementStyle.backgroundColor).toBe('rgba(0, 0, 0, 0)');
expect(elementStyle.color).toBe('rgb(255, 255, 255)');
if (isFirefox) {
expect(elementStyle.borderTopColor).toBe('rgb(0, 217, 0)');
expect(elementStyle.borderRightColor).toBe('rgb(0, 217, 0)');
expect(elementStyle.borderBottomColor).toBe('rgb(0, 217, 0)');
expect(elementStyle.borderLeftColor).toBe('rgb(0, 217, 0)');
} else {
expect(elementStyle.borderColor).toBe('rgb(0, 217, 0)');
}
const anotherStyle = document.createElement('style');
anotherStyle.textContent = multiline(
'h1 {',
' background-color: var(--color-bg);',
' color: var(--color-text);',
'}',
);
container.append(anotherStyle);
await timeout(0);
const updatedStyle = getComputedStyle(container.querySelector('h1')!);
expect(updatedStyle.backgroundColor).toBe('rgb(204, 0, 0)');
expect(updatedStyle.color).toBe('rgb(140, 255, 140)');
if (isFirefox) {
expect(updatedStyle.borderTopColor).toBe('rgb(0, 217, 0)');
expect(updatedStyle.borderRightColor).toBe('rgb(0, 217, 0)');
expect(updatedStyle.borderBottomColor).toBe('rgb(0, 217, 0)');
expect(updatedStyle.borderLeftColor).toBe('rgb(0, 217, 0)');
} else {
expect(updatedStyle.borderColor).toBe('rgb(0, 217, 0)');
}
});
it('should not affect other declarations when dependency variable type resolved', async () => {
container.innerHTML = multiline(
'<style>',
' h1 {',
' background: var(--color);',
' border: green;',
' }',
' h1 {',
' color: red;',
' }',
'</style>',
'<h1>Dependency variable</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
const elementStyle = getComputedStyle(container.querySelector('h1')!);
expect(elementStyle.backgroundColor).toBe('rgba(0, 0, 0, 0)');
expect(elementStyle.color).toBe('rgb(255, 26, 26)');
if (isFirefox) {
expect(elementStyle.borderTopColor).toBe('rgb(0, 217, 0)');
expect(elementStyle.borderRightColor).toBe('rgb(0, 217, 0)');
expect(elementStyle.borderBottomColor).toBe('rgb(0, 217, 0)');
expect(elementStyle.borderLeftColor).toBe('rgb(0, 217, 0)');
} else {
expect(elementStyle.borderColor).toBe('rgb(0, 217, 0)');
}
const anotherStyle = document.createElement('style');
anotherStyle.textContent = multiline(
':root {',
' --color: green;',
'}',
);
container.append(anotherStyle);
await timeout(50);
const updatedStyle = getComputedStyle(container.querySelector('h1')!);
expect(updatedStyle.backgroundColor).toBe('rgb(0, 102, 0)');
expect(updatedStyle.color).toBe('rgb(255, 26, 26)');
if (isFirefox) {
expect(updatedStyle.borderTopColor).toBe('rgb(0, 217, 0)');
expect(updatedStyle.borderRightColor).toBe('rgb(0, 217, 0)');
expect(updatedStyle.borderBottomColor).toBe('rgb(0, 217, 0)');
expect(updatedStyle.borderLeftColor).toBe('rgb(0, 217, 0)');
} else {
expect(updatedStyle.borderColor).toBe('rgb(0, 217, 0)');
}
});
it('should add variables to root after the variable type is discovered', async () => {
document.documentElement.setAttribute('style', '--text: red;');
container.innerHTML = multiline(
'<style class="testcase-sheet">',
'</style>',
'<h1>Dependency variable</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
const sheet = (document.querySelector('.testcase-sheet') as HTMLStyleElement).sheet!;
sheet.insertRule('h1 { color: var(--text);');
await timeout(0);
expect(getComputedStyle(document.querySelector('h1')!).color).toBe('rgb(255, 26, 26)');
});
it('should modify the caret-color property', async () => {
container.innerHTML = multiline(
'<style>',
' * {',
' --caret-color: green;',
' }',
' h1 {',
' caret-color: var(--caret-color);',
' }',
'</style>',
'<h1>Caret Color</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
await timeout(0);
const elementStyle = getComputedStyle(container.querySelector('h1')!);
expect(elementStyle.caretColor).toBe('rgb(140, 255, 140)');
});
it('should modify the box-shadow actual color values in a variable', async () => {
container.innerHTML = multiline(
'<style>',
' h1 {',
' --offset: 8px;',
' }',
' h1 {',
' box-shadow: calc(var(--offset)*-1 - 1px) 0 0 0 #fff',
' }',
'</style>',
'<h1>COmplicated shit :(</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
await timeout(0);
const elementStyle = getComputedStyle(container.querySelector('h1')!);
expect(elementStyle.boxShadow).toBe('rgb(0, 0, 0) -9px 0px 0px 0px');
});
it(`shouldn't modify the raw value of box-shadow when their is no color`, async () => {
container.innerHTML = multiline(
'<style>',
' h1 {',
' --color-border-muted: green;',
' }',
' h1 {',
' box-shadow: inset 0 -1px 0 var(--color-border-muted)',
' }',
'</style>',
'<h1>COmplicated shit :(</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
await timeout(0);
const elementStyle = getComputedStyle(container.querySelector('h1')!);
expect(elementStyle.boxShadow).toBe('rgb(0, 102, 0) 0px -1px 0px 0px inset');
});
it('should handle raw values within variable declarations and use proper replacement', async () => {
container.innerHTML = multiline(
'<style>',
' h1 {',
' border: 1px solid rgba(var(--color,240,240,240),1);',
' }',
' :root {',
' --color: 123,123,123 !important;',
' }',
' div {',
' --color: 0,0,0 !important;',
' }',
'</style>',
'<h1>Raw values are spooky</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
await timeout(0);
const elementStyle = getComputedStyle(container.querySelector('h1')!);
if (isFirefox) {
expect(elementStyle.borderTopColor).toBe('rgb(91, 91, 91)');
expect(elementStyle.borderRightColor).toBe('rgb(91, 91, 91)');
expect(elementStyle.borderBottomColor).toBe('rgb(91, 91, 91)');
expect(elementStyle.borderLeftColor).toBe('rgb(91, 91, 91)');
} else {
expect(elementStyle.borderColor).toBe('rgb(91, 91, 91)');
}
});
it('should modify inline variable', () => {
container.innerHTML = multiline(
'<style>',
' h1 {',
' background-color: var(--inline-var);',
' }',
'</style>',
'<h1 style="--inline-var: red">Raw values are spooky</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
const elementStyle = getComputedStyle(container.querySelector('h1')!);
expect(elementStyle.backgroundColor).toBe('rgb(204, 0, 0)');
});
});