chore: 整理 third-party Dark Reader 目录并移除本地说明文件跟踪

This commit is contained in:
2026-08-08 17:38:08 +08:00
parent 202ab537d1
commit 0bb552b026
1004 changed files with 115066 additions and 20081 deletions
@@ -0,0 +1,125 @@
import '../support/polyfills';
import {DEFAULT_THEME} from '../../../src/defaults';
import {createOrUpdateDynamicTheme, removeDynamicTheme} from '../../../src/inject/dynamic-theme';
import {multiline} from '../support/test-utils';
const theme = {
...DEFAULT_THEME,
};
let container: HTMLElement;
beforeEach(() => {
container = document.body;
container.innerHTML = '';
});
afterEach(() => {
removeDynamicTheme();
container.innerHTML = '';
});
describe('COLOR PARSING', () => {
it('should modify RGBA', async () => {
container.innerHTML = multiline(
'<style>',
' h1 { background: rgb(245, 185, 124); }',
' h1 strong { color: rgb(34, 52, 34); }',
' body { background-color: rgba(51, 170, 51, .4); }',
'</style>',
'<h1>RGB <strong>Power</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(126, 68, 10)');
expect(getComputedStyle(container.querySelector('h1 strong')!).color).toBe('rgb(205, 200, 194)');
expect(getComputedStyle(container).backgroundColor).toBe('rgba(41, 136, 41, 0.4)');
});
it('should modify HSL', async () => {
container.innerHTML = multiline(
'<style>',
' h1 { background: hsl(270,60%,70%); }',
' h1 strong { color: hsl(.75turn, 60%, 70%); }',
' body { background-color: hsl(4.71239rad, 60%, 70%); }',
'</style>',
'<h1>HSL <strong>Power</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(72, 29, 114)');
expect(getComputedStyle(container.querySelector('h1 strong')!).color).toBe('rgb(176, 129, 223)');
expect(getComputedStyle(container).backgroundColor).toBe('rgb(72, 29, 114)');
});
it('should modify HSLA', async () => {
container.innerHTML = multiline(
'<style>',
' h1 { background: hsla(240, 100%, 50%, .7); }',
' h1 strong { color: hsla(240, 100%, 50%, 1); }',
' body { background-color: hsla(240, 100%, 50%, .05); }',
'</style>',
'<h1>HSLA <strong>Power</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgba(0, 0, 204, 0.7)');
expect(getComputedStyle(container.querySelector('h1 strong')!).color).toBe('rgb(51, 125, 255)');
expect(getComputedStyle(container).backgroundColor).toBe('rgba(0, 0, 204, 0.05)');
});
it('should modify knownColors', async () => {
container.innerHTML = multiline(
'<style>',
' h1 { background: rebeccapurple; }',
' h1 strong { color: InfoBackground; }',
' body { background-color: transparent; }',
'</style>',
'<h1>Weird color <strong>Power</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(82, 41, 122)');
expect(getComputedStyle(container.querySelector('h1 strong')!).color).toBe('rgb(249, 250, 166)');
expect(getComputedStyle(container).backgroundColor).toBe('rgba(0, 0, 0, 0)');
});
it('should handle calc(...) cases', () => {
container.innerHTML = multiline(
'<style>',
' h1 { background-color: hsl(0, 0%, calc(95% - 3%)) }',
'</style>',
'<h1>Weird color <strong>Power</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(35, 38, 40)');
});
it('should handle gradient\'s cases with rgb(...) xx%', async () => {
container.innerHTML = multiline(
'<style>',
' h1 { background-image: -webkit-linear-gradient(bottom, rgb(255, 255, 255) 15%, rgb(246, 246, 245) 85%); }',
'</style>',
'<h1>Weird color <strong>Power</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).backgroundImage).toBe('-webkit-linear-gradient(bottom, rgb(24, 26, 27) 15%, rgb(29, 32, 33) 85%)');
});
it('should handle complex calc(...) cases', () => {
container.innerHTML = multiline(
'<style>',
' h1 { background-color: rgb(calc(216.75 + 153 * .15), calc(216.75 + 205 * .15), calc(216.75 + 255 * .15)) }',
'</style>',
'<h1>Weird color <strong>Power</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(28, 31, 32)');
});
it('should handle gradients with calc(...) cases', () => {
container.innerHTML = multiline(
'<style>',
' h1 { background-image: linear-gradient(rgb(249, 249, 251) calc(100% - 3px), transparent), linear-gradient(-90deg, rgb(255, 145, 0), rgb(241, 3, 102) 50%, rgb(97, 115, 255)) }',
'</style>',
'<h1>Weird color <strong>Power</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).backgroundImage).toBe('linear-gradient(rgb(27, 29, 30), calc(100% - 3px), rgba(0, 0, 0, 0)), linear-gradient(-90deg, rgb(204, 116, 0), rgb(193, 2, 82) 50%, rgb(0, 17, 146))');
});
});
@@ -0,0 +1,118 @@
import '../support/polyfills';
import {DEFAULT_THEME} from '../../../src/defaults';
import type {DynamicThemeFix} from '../../../src/definitions';
import {FilterMode} from '../../../src/generators/css-filter';
import {createOrUpdateDynamicTheme, removeDynamicTheme} from '../../../src/inject/dynamic-theme';
import {removeNode} from '../../../src/inject/utils/dom';
import {multiline, timeout} from '../support/test-utils';
let container: HTMLElement;
beforeEach(() => {
container = document.body;
container.innerHTML = '';
});
afterEach(() => {
removeDynamicTheme();
container.innerHTML = '';
removeNode(document.head.querySelector('meta[name="darkreader-lock"]'));
});
describe('FIXES', () => {
it('should add custom attributes to root element', () => {
createOrUpdateDynamicTheme(DEFAULT_THEME, null, false);
expect(document.documentElement.getAttribute(`data-darkreader-mode`)).toBe('dynamic');
expect(document.documentElement.getAttribute('data-darkreader-scheme')).toBe('dark');
createOrUpdateDynamicTheme({...DEFAULT_THEME, mode: FilterMode.light}, null, false);
expect(document.documentElement.getAttribute('data-darkreader-scheme')).toBe('dimmed');
});
it('should invert selectors', async () => {
container.innerHTML = multiline(
'<div class="logo">Some logo</div>',
);
const fixes: DynamicThemeFix[] = [{
url: ['*'],
invert: ['.logo'],
css: '',
ignoreInlineStyle: [],
ignoreImageAnalysis: [],
ignoreCSSUrl: [],
disableStyleSheetsProxy: false,
disableCustomElementRegistryProxy: false,
}];
createOrUpdateDynamicTheme(DEFAULT_THEME, fixes, false);
expect(getComputedStyle(container.querySelector('.logo')!).filter).toBe('invert(1) hue-rotate(180deg) contrast(0.9)');
});
it('should insert CSS', async () => {
container.innerHTML = multiline(
'<p class="text">Some text need to be red</p>',
);
const fixes: DynamicThemeFix[] = [{
url: ['*'],
invert: [''],
css: '.text { color: red }',
ignoreInlineStyle: [],
ignoreImageAnalysis: [],
ignoreCSSUrl: [],
disableStyleSheetsProxy: false,
disableCustomElementRegistryProxy: false,
}];
createOrUpdateDynamicTheme(DEFAULT_THEME, fixes, false);
expect(getComputedStyle(container.querySelector('.text')!).color).toBe('rgb(255, 0, 0)');
});
it('should ignore inline style', async () => {
container.innerHTML = multiline(
'<p class="text" style="background-color: purple">Some text need to be red</p>',
);
const fixes: DynamicThemeFix[] = [{
url: ['*'],
invert: [''],
css: '',
ignoreInlineStyle: ['.text'],
ignoreImageAnalysis: [],
ignoreCSSUrl: [],
disableStyleSheetsProxy: false,
disableCustomElementRegistryProxy: false,
}];
createOrUpdateDynamicTheme(DEFAULT_THEME, fixes, false);
expect(getComputedStyle(container.querySelector('.text')!).backgroundColor).toBe('rgb(128, 0, 128)');
});
it('should ignore styling when darkreader-lock detected', async () => {
document.head.innerHTML = '<meta name="darkreader-lock">',
container.innerHTML = multiline(
'<style>',
' body {',
' background-color: pink !important;',
' }',
'</style>',
);
createOrUpdateDynamicTheme(DEFAULT_THEME, null, false);
expect(getComputedStyle(document.body).backgroundColor).toBe('rgb(255, 192, 203)');
});
it('should ignore styling when delayed darkreader-lock detected', async () => {
container.innerHTML = multiline(
'<style>',
' body {',
' background-color: pink !important;',
' }',
'</style>',
);
createOrUpdateDynamicTheme(DEFAULT_THEME, null, false);
expect(getComputedStyle(container).backgroundColor).toBe('rgb(89, 0, 16)');
const metaElement: HTMLMetaElement = document.createElement('meta');
metaElement.name = 'darkreader-lock';
document.head.appendChild(metaElement);
await timeout(100);
expect(getComputedStyle(container).backgroundColor).toBe('rgb(255, 192, 203)');
});
});
@@ -0,0 +1,290 @@
import '../support/polyfills';
import {DEFAULT_THEME} from '../../../src/defaults';
import type {DynamicThemeFix} from '../../../src/definitions';
import {createOrUpdateDynamicTheme, removeDynamicTheme} from '../../../src/inject/dynamic-theme';
import {getImageDetails} from '../../../src/inject/dynamic-theme/image';
import {multiline, timeout, waitForEvent} from '../support/test-utils';
const theme = {
...DEFAULT_THEME,
darkSchemeBackgroundColor: 'black',
darkSchemeTextColor: 'white',
};
let container: HTMLElement;
beforeEach(() => {
container = document.body;
container.innerHTML = '';
});
afterEach(() => {
removeDynamicTheme();
container.innerHTML = '';
});
function svgToDataURL(svg: string) {
return `data:image/svg+xml;base64,${btoa(svg)}`;
}
function getSVGImageCSS(svg: string, width: number, height: number, selector: string) {
return multiline(
`${selector} {`,
` background-image: url("${svgToDataURL(svg)}");`,
' background-position: center;',
' background-repeat: no-repeat;',
' background-size: cover;',
' display: inline-block;',
` height: ${height}px;`,
` width: ${width}px;`,
'}',
);
}
const images = {
darkIcon: multiline(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" width="8" height="8">',
' <rect fill="black" width="100%" height="100%" />',
'</svg>',
),
lightIcon: multiline(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" width="8" height="8">',
' <rect fill="white" width="100%" height="100%" />',
'</svg>',
),
darkTransparentIcon: 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>',
),
lightTransparentIcon: multiline(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" width="8" height="8">',
' <circle fill="white" cx="4" cy="4" r="3" />',
'</svg>',
),
largeDarkImage: multiline(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" width="1024" height="1024">',
' <rect fill="black" width="100%" height="100%" />',
'</svg>',
),
largeLightImage: multiline(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" width="1024" height="1024">',
' <rect fill="white" width="100%" height="100%" />',
'</svg>',
),
};
async function urlToImage(url: string) {
return new Promise<HTMLImageElement>((resolve, reject) => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = () => reject(`Unable to load image ${url}`);
image.src = url;
});
}
async function getBgImageInfo(bgImageValue: string) {
const bgImageURL = bgImageValue.match(/^url\("(.*)"\)$/)![1];
const image = await urlToImage(bgImageURL);
const width = 8;
const height = 8;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const context = canvas.getContext('2d')!;
context.drawImage(image, 0, 0, width, height);
const d = context.getImageData(0, 0, width, height).data;
let lightPixels = 0;
let darkPixels = 0;
let opaquePixels = 0;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = 4 * (x + width * y);
const r = d[i + 0];
const g = d[i + 1];
const b = d[i + 2];
const a = d[i + 3];
if (a > 127) {
opaquePixels++;
const lightness = (r + g + b) / 3 / 255;
if (lightness > 0.7) {
lightPixels++;
} else if (lightness < 0.3) {
darkPixels++;
}
}
}
}
return {
lightness: lightPixels / opaquePixels,
darkness: darkPixels / opaquePixels,
};
}
describe('IMAGE ANALYSIS', () => {
it('should analyze dark icon', async () => {
const details = await getImageDetails(svgToDataURL(images.darkIcon));
expect(details.width).toBe(8);
expect(details.height).toBe(8);
expect(details.isDark).toBe(true);
expect(details.isLight).toBe(false);
expect(details.isTransparent).toBe(false);
expect(details.isLarge).toBe(false);
});
it('should analyze light icon', async () => {
const details = await getImageDetails(svgToDataURL(images.lightIcon));
expect(details.width).toBe(8);
expect(details.height).toBe(8);
expect(details.isDark).toBe(false);
expect(details.isLight).toBe(true);
expect(details.isTransparent).toBe(false);
expect(details.isLarge).toBe(false);
});
it('should analyze dark transparent icon', async () => {
const details = await getImageDetails(svgToDataURL(images.darkTransparentIcon));
expect(details.width).toBe(8);
expect(details.height).toBe(8);
expect(details.isDark).toBe(true);
expect(details.isLight).toBe(false);
expect(details.isTransparent).toBe(true);
expect(details.isLarge).toBe(false);
});
it('should not analyze large image', async () => {
const details = await getImageDetails(svgToDataURL(images.largeDarkImage));
expect(details.width).toBe(1024);
expect(details.height).toBe(1024);
expect(details.isLarge).toBe(true);
});
it('should invert dark icons', async () => {
container.innerHTML = multiline(
'<style>',
getSVGImageCSS(images.darkTransparentIcon, 16, 16, 'i'),
'</style>',
'<h1>Dark icon <i></i></h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
await waitForEvent('__darkreader__test__asyncQueueComplete');
await timeout(500);
const bgImageValue = getComputedStyle(container.querySelector('i')!).backgroundImage;
const info = await getBgImageInfo(bgImageValue);
expect(info.darkness).toBe(0);
expect(info.lightness).toBe(1);
});
it('should not invert light icons', async () => {
container.innerHTML = multiline(
'<style>',
getSVGImageCSS(images.lightTransparentIcon, 16, 16, 'i'),
'</style>',
'<h1>Light icon <i></i></h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
const bgImageValue = getComputedStyle(container.querySelector('i')!).backgroundImage;
const info = await getBgImageInfo(bgImageValue);
expect(info.darkness).toBe(0);
expect(info.lightness).toBe(1);
});
it('should not invert dark backgrounds', async () => {
container.innerHTML = multiline(
'<style>',
getSVGImageCSS(images.largeDarkImage, 320, 320, 'h1'),
'</style>',
'<h1>Dark background</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
const bgImageValue = getComputedStyle(container.querySelector('h1')!).backgroundImage;
const info = await getBgImageInfo(bgImageValue);
expect(info.darkness).toBe(1);
expect(info.lightness).toBe(0);
});
it('should hide light backgrounds', async () => {
container.innerHTML = multiline(
'<style>',
getSVGImageCSS(images.largeLightImage, 320, 320, 'h1'),
'</style>',
'<h1>Light background</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
await waitForEvent('__darkreader__test__asyncQueueComplete');
const bgImageValue = getComputedStyle(container.querySelector('h1')!).backgroundImage;
expect(bgImageValue).toBe('none');
});
it('should ignore image analysis', async () => {
container.innerHTML = multiline(
'<style>',
getSVGImageCSS(images.darkTransparentIcon, 16, 16, 'i'),
'</style>',
'<h1>Dark icon <i></i></h1>',
);
const fixes: DynamicThemeFix[] = [{
url: ['*'],
invert: [''],
css: '',
ignoreInlineStyle: ['.'],
ignoreImageAnalysis: ['*'],
ignoreCSSUrl: [],
disableStyleSheetsProxy: false,
disableCustomElementRegistryProxy: false,
}];
createOrUpdateDynamicTheme(theme, fixes, false);
const backgroundImage = getComputedStyle(container.querySelector('i')!).backgroundImage;
expect(backgroundImage).toContain('data:');
});
it('should handle background-image with URL and gradient', async () => {
container.innerHTML = multiline(
'<style>',
` h1 { background-image: url("${svgToDataURL(images.lightIcon)}"), linear-gradient(red, white);`,
'</style>',
'<h1>Weird color <strong>Power</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
await waitForEvent('__darkreader__test__asyncQueueComplete');
expect(getComputedStyle(container.querySelector('h1')!).backgroundImage).toMatch(/^url\("blob:.*"\), linear-gradient\(rgb\(204, 0, 0\), rgb\(0, 0, 0\)\)$/);
});
it('should handle background-image with non-base64 data URL', async () => {
container.innerHTML = multiline(
'<style>',
` h1 { background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="6" height="3">%3Cpath%20d%3D%22m0%202.5%20l2%20-1.5%20l1%200%20l2%201.5%20l1%200%22%20stroke%3D%22%23d11%22%20fill%3D%22none%22%20stroke-width%3D%22.7%22%2F%3E</svg>');`,
'</style>',
'<h1>Weird color <strong>Power</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
await waitForEvent('__darkreader__test__asyncQueueComplete');
expect(getComputedStyle(container.querySelector('h1')!).backgroundImage).toMatch(/^url\("blob:.*"\)$/);
});
it('should handle background-image with URL and gradient (revered)', async () => {
container.innerHTML = multiline(
'<style>',
` h1 { background-image: linear-gradient(red, white), url("${svgToDataURL(images.lightIcon)}");`,
'</style>',
'<h1>Weird color <strong>Power</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
await waitForEvent('__darkreader__test__asyncQueueComplete');
await timeout(500);
expect(getComputedStyle(container.querySelector('h1')!).backgroundImage).toMatch(/^linear-gradient\(rgb\(204, 0, 0\), rgb\(0, 0, 0\)\), url\("blob:.*"\)$/);
});
it('should handle background-image with empty URLs', async () => {
container.innerHTML = multiline(
'<style>',
` h1 { background-image: url(''), url(''), url("${svgToDataURL(images.lightIcon)}");`,
'</style>',
'<h1>Weird color <strong>Power</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
await waitForEvent('__darkreader__test__asyncQueueComplete');
expect(getComputedStyle(container.querySelector('h1')!).backgroundImage).toMatch(/^url\(""\), url\(""\), url\("blob:.*"\)$/);
});
});
@@ -0,0 +1,58 @@
import {injectStyleAway, removeStyleContainer} from '../../../src/inject/dynamic-theme/injection';
import {timeout} from '../support/test-utils';
describe('STYLE INJECTION', () => {
let originalBody: HTMLElement;
beforeEach(() => {
removeStyleContainer();
originalBody = document.body;
originalBody.remove();
});
afterEach(() => {
removeStyleContainer();
document.body?.remove();
document.documentElement.append(originalBody);
});
it('should inject a queued style when the body becomes available', async () => {
const style = document.createElement('style');
style.textContent = 'body { color: white; }';
injectStyleAway(style);
expect(style.isConnected).toBe(false);
const body = document.createElement('body');
document.documentElement.append(body);
await timeout(0);
const container = body.querySelector('.darkreader-style-container');
expect(container).not.toBeNull();
expect(container!.lastElementChild).toBe(style);
expect(style.sheet!.cssRules.length).toBe(1);
});
it('should clear queued styles and allow observing a new body', async () => {
const discardedStyle = document.createElement('style');
injectStyleAway(discardedStyle);
removeStyleContainer();
const firstBody = document.createElement('body');
document.documentElement.append(firstBody);
await timeout(0);
expect(firstBody.querySelector('.darkreader-style-container')).toBeNull();
expect(discardedStyle.isConnected).toBe(false);
firstBody.remove();
const injectedStyle = document.createElement('style');
injectStyleAway(injectedStyle);
const secondBody = document.createElement('body');
document.documentElement.append(secondBody);
await timeout(0);
expect(secondBody.querySelector('.darkreader-style-container')?.lastElementChild).toBe(injectedStyle);
});
});
@@ -0,0 +1,85 @@
import '../support/polyfills';
import {DEFAULT_THEME} from '../../../src/defaults';
import {createOrUpdateDynamicTheme, removeDynamicTheme} from '../../../src/inject/dynamic-theme';
import {multiline, timeout} from '../support/test-utils';
const theme = {
...DEFAULT_THEME,
darkSchemeBackgroundColor: 'black',
darkSchemeTextColor: 'white',
};
let container: HTMLElement;
beforeEach(() => {
container = document.body;
container.innerHTML = '';
});
afterEach(() => {
removeDynamicTheme();
container.innerHTML = '';
});
describe('INLINE STYLES', () => {
it('should override inline style', () => {
container.innerHTML = '<span style="color: red;">Inline style override</span>';
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(document.querySelector('span')!).color).toBe('rgb(255, 26, 26)');
});
it('should watch for inline style change', async () => {
container.innerHTML = '<span style="color: red;">Watch inline style</span>';
createOrUpdateDynamicTheme(theme, null, false);
const span = document.querySelector('span')!;
expect(getComputedStyle(span).color).toBe('rgb(255, 26, 26)');
span.style.color = 'green';
await timeout(0);
expect(getComputedStyle(span).color).toBe('rgb(140, 255, 140)');
});
it('should override only a single inline style property', async () => {
container.innerHTML = multiline(
'<style>.bg-gray { background: gray; }</style>',
'<span class="bg-gray" style="color: red;">Inline style override</span>',
);
createOrUpdateDynamicTheme(theme, null, false);
const span = container.querySelector('span')!;
expect(getComputedStyle(span).backgroundColor).toBe('rgb(102, 102, 102)');
expect(getComputedStyle(span).color).toBe('rgb(255, 26, 26)');
span.style.color = 'green';
await timeout(0);
expect(getComputedStyle(span).color).toBe('rgb(140, 255, 140)');
expect(getComputedStyle(span).backgroundColor).toBe('rgb(102, 102, 102)');
});
it('should clean up the customProp after originial is gone', async () => {
container.innerHTML = '<span style="color: red;">Watch inline style</span>';
createOrUpdateDynamicTheme(theme, null, false);
const span = document.querySelector('span')!;
expect(span.getAttribute('style')!.startsWith('color: red; --darkreader-inline-color:')).toBeTrue();
expect(span.getAttribute('style')!.includes('--darkreader-inline-color: var(--darkreader-text-ff0000, #ff1a1a);')).toBe(true);
span.style.color = '';
await timeout(0);
expect(span.getAttribute('style')).toBe('');
});
it(`shouldn't touch rel="mask-icon"`, async () => {
container.innerHTML = '<link rel="mask-icon" color="red">';
createOrUpdateDynamicTheme(theme, null, false);
const maskIcon = document.querySelector('link[rel="mask-icon"]')!;
expect(maskIcon.getAttribute('style')).toBe(null);
});
it(`shouldn't touch a "none" value for fill`, async () => {
container.innerHTML = `<svg> <rect width="100" height="100" fill="none" /></svg>`;
container.innerHTML += `<style> rect[width][height] { fill: red }</style>`;
createOrUpdateDynamicTheme(theme, null, false);
const rect = container.querySelector('rect')!;
expect(getComputedStyle(rect).fill).toBe('rgb(255, 26, 26)');
});
});
@@ -0,0 +1,225 @@
import {DEFAULT_THEME} from '../../../src/defaults';
import {createOrUpdateDynamicTheme, removeDynamicTheme} from '../../../src/inject/dynamic-theme';
import {resetChromeRuntimeMessageStub, stubBackgroundFetchResponse, stubChromeRuntimeMessage} from '../support/background-stub';
import {getCSSEchoURL} from '../support/echo-client';
import {multiline, timeout, waitForEvent} from '../support/test-utils';
const theme = {
...DEFAULT_THEME,
darkSchemeBackgroundColor: 'black',
darkSchemeTextColor: 'white',
};
let container: HTMLElement;
const links: HTMLLinkElement[] = [];
function createStyleLink(href: string | null) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.classList.add('testcase--link');
if (href) {
link.href = href;
}
document.head.append(link);
links.push(link);
return link;
}
function selectTestStyleLink() {
return document.querySelector('.testcase--link')! as HTMLLinkElement;
}
function createCorsLink(content: string) {
const url = getCSSEchoURL(content);
stubBackgroundFetchResponse(url, content);
return createStyleLink(url);
}
async function waitForLinkLoading(link: HTMLLinkElement) {
return new Promise((resolve, reject) => {
link.addEventListener('load', resolve, {once: true});
link.addEventListener('error', reject, {once: true});
});
}
beforeEach(() => {
container = document.body;
container.innerHTML = '';
stubChromeRuntimeMessage();
});
afterEach(() => {
removeDynamicTheme();
container.innerHTML = '';
links.forEach((l) => l.remove());
links.splice(0);
resetChromeRuntimeMessageStub();
});
describe('LINK STYLES', () => {
it('should override same-origin link', async () => {
createStyleLink(`data:text/css;utf8,${encodeURIComponent(multiline(
'h1 { background: gray; }',
'h1 strong { color: red; }',
))}`);
container.innerHTML = multiline(
'<h1>Link <strong>override</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
await timeout(50);
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 cross-origin link', async () => {
createCorsLink(multiline(
'h1 { background: gray; }',
'h1 strong { color: red; }',
));
container.innerHTML = multiline(
'<h1><strong>Cross-origin</strong> link override</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
await waitForEvent('__darkreader__test__dynamicUpdateComplete');
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 cross-origin imports in linked CSS', async () => {
const importedCSS = 'h1 { background: gray; }';
const importedURL = getCSSEchoURL(importedCSS);
stubBackgroundFetchResponse(importedURL, importedCSS);
createCorsLink(multiline(
`@import "${importedURL}";`,
'h1 strong { color: red; }',
));
container.innerHTML = multiline(
'<h1><strong>Cross-origin import</strong> link override</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
await waitForEvent('__darkreader__test__dynamicUpdateComplete');
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 cross-origin imports in linked CSS with capital @import', async () => {
const importedCSS = 'h1 { background: gray; }';
const importedURL = getCSSEchoURL(importedCSS);
stubBackgroundFetchResponse(importedURL, importedCSS);
createCorsLink(multiline(
`@IMPORT "${importedURL}";`,
'h1 strong { color: red; }',
));
container.innerHTML = multiline(
'<h1><strong>Cross-origin import</strong> link override</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
await waitForEvent('__darkreader__test__dynamicUpdateComplete');
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 cross-origin link that has already been loaded', async () => {
createCorsLink(multiline(
'h1 { background: gray; }',
'h1 strong { color: red; }',
));
container.innerHTML = multiline(
'<h1>Loaded <strong>cross-origin</strong> link override</h1>',
);
await waitForLinkLoading(selectTestStyleLink());
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(128, 128, 128)');
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(0, 0, 0)');
expect(getComputedStyle(container.querySelector('h1 strong')!).color).toBe('rgb(255, 0, 0)');
createOrUpdateDynamicTheme(theme, null, false);
await waitForEvent('__darkreader__test__dynamicUpdateComplete');
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 remove manager from disabled link', async () => {
const link = createStyleLink(`data:text/css;utf8,${encodeURIComponent(multiline(
'h1 { background: gray; }',
'h1 strong { color: red; }',
))}`);
container.innerHTML = multiline(
'<h1>Link <strong>disabled</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
await timeout(50);
expect(selectTestStyleLink().nextElementSibling!.classList.contains('darkreader--sync')).toBe(true);
link.disabled = true;
await timeout(0);
expect(selectTestStyleLink().nextElementSibling!.classList.contains('darkreader--sync')).toBe(false);
});
it("Shouldn't wait on link that won't be loaded", async () => {
const link = createStyleLink(null);
link.setAttribute('data-href', `data:text/css;utf8,${encodeURIComponent(multiline(
'h1 { background: green !important; }',
'h1 strong { color: orange !important; }',
))}`);
container.innerHTML = multiline(
'<style>',
' h1 { background: gray; }',
'</style>',
'<h1>Link <strong>loading with non-href attribute</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
await timeout(50);
const h1 = document.querySelector('h1')!;
expect(getComputedStyle(h1).backgroundColor).toBe('rgb(102, 102, 102)');
expect(document.querySelector('.darkreader--fallback')!.textContent).toBe('');
});
it('should handle styles with @import "..." screen;', async () => {
const importedCSS = 'h1 { background: gray; }';
const importedURL = getCSSEchoURL(importedCSS);
stubBackgroundFetchResponse(importedURL, importedCSS);
createCorsLink(multiline(
`@import "${importedURL}" screen;`,
'h1 strong { color: red; }',
));
container.innerHTML = multiline(
'<h1><strong>Cross-origin import</strong> link override</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
await waitForEvent('__darkreader__test__dynamicUpdateComplete');
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 handle styles with invalid url(...)', async () => {
const importedCSS = 'h1 { background-image: url("freecookies:3https://example.com"); background-color: gray; }';
const importedURL = getCSSEchoURL(importedCSS);
stubBackgroundFetchResponse(importedURL, importedCSS);
stubBackgroundFetchResponse('freecookies:3https://example.com', '');
createCorsLink(multiline(
`@import "${importedURL}" screen;`,
'h1 strong { color: red; }',
));
container.innerHTML = multiline(
'<h1><strong>Cross-origin import</strong> link override</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
await waitForEvent('__darkreader__test__dynamicUpdateComplete');
await timeout(1000);
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)');
});
});
@@ -0,0 +1,188 @@
import '../support/polyfills';
import {DEFAULT_THEME} from '../../../src/defaults';
import {createOrUpdateDynamicTheme, removeDynamicTheme} from '../../../src/inject/dynamic-theme';
import {multiline, timeout} from '../support/test-utils';
const theme = {
...DEFAULT_THEME,
darkSchemeBackgroundColor: 'black',
darkSchemeTextColor: 'white',
};
let container: HTMLElement;
beforeEach(() => {
container = document.body;
container.innerHTML = '';
});
afterEach(() => {
removeDynamicTheme();
container.innerHTML = '';
});
describe('MEDIA QUERIES', () => {
it('should not style blacklisted media', async () => {
container.innerHTML = multiline(
'<style class="testcase-style">',
' h1 { background: green; }',
' h1 strong { color: orange; }',
'</style>',
'<style class="testcase-style-2" media="print">',
' h1 { background: gray; }',
' h1 strong { color: red; }',
'</style>',
'<h1>Some test foor...... <strong>Oh uhm removing styles :(</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(document.querySelector('h1')!).backgroundColor).toBe('rgb(0, 102, 0)');
expect(getComputedStyle(document.querySelector('h1 strong')!).color).toBe('rgb(255, 174, 26)');
expect(document.querySelector('.testcase-style-2')!.nextElementSibling!.classList.contains('darkreader--sync')).toBe(false);
});
it('should style lazyloaded media', async () => {
container.innerHTML = multiline(
'<style class="testcase-style" media="print">',
' h1 { background: green; }',
' h1 strong { color: orange; }',
'</style>',
'<h1>Some test foor...... <strong>Oh uhm removing styles :(</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
(document.querySelector('.testcase-style') as HTMLStyleElement).media = 'screen';
await timeout(0);
expect((document.querySelector('.testcase-style') as HTMLStyleElement).media).toBe('screen');
expect(getComputedStyle(document.querySelector('h1')!).backgroundColor).toBe('rgb(0, 102, 0)');
expect(getComputedStyle(document.querySelector('h1 strong')!).color).toBe('rgb(255, 174, 26)');
expect(document.querySelector('.testcase-style')!.nextElementSibling!.classList.contains('darkreader--sync')).toBe(true);
});
it('should check for CSS support', async () => {
container.innerHTML = multiline(
'<style class="testcase-style">',
' @supports (background: green) {',
' h1 { background: green; }',
' }',
' @supports (color: orange) {',
' h1 strong { color: orange; }',
' }',
' @supports (some-non-existing-prop: some-value) {',
' body { background: pink; }',
' }',
'</style>',
'<h1>Some test foor...... <strong>Oh uhm removing styles :(</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(document.querySelector('h1')!).backgroundColor).toBe('rgb(0, 102, 0)');
expect(getComputedStyle(document.querySelector('h1 strong')!).color).toBe('rgb(255, 174, 26)');
expect(getComputedStyle(document.body).backgroundColor).toBe('rgb(0, 0, 0)');
});
it('should check for CSS @media', async () => {
container.innerHTML = multiline(
'<style class="testcase-style">',
' @media screen and (min-width: 2px) {',
' h1 { background: green; }',
' }',
' @media screen and (min-width: 200000px) {',
' h1 { background: orange; }',
' }',
'</style>',
'<h1>Some test media query i guess</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(document.querySelector('h1')!).backgroundColor).toBe('rgb(0, 102, 0)');
expect((document.querySelector('.testcase-style')!.nextElementSibling as HTMLStyleElement).sheet!.cssRules.length).toBe(2);
});
it('should check for nested CSS @media', async () => {
container.innerHTML = multiline(
'<style class="testcase-style">',
' @media screen and (min-width: 2px) {',
' @media screen and (min-width: 2px) {',
' h1 { background: green; }',
' }',
' }',
' @media screen and (min-width: 200000px) {',
' h1 { background: orange; }',
' }',
'</style>',
'<h1>Some test media query i guess</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(document.querySelector('h1')!).backgroundColor).toBe('rgb(0, 102, 0)');
expect((document.querySelector('.testcase-style')!.nextElementSibling as HTMLStyleElement).sheet!.cssRules.length).toBe(2);
});
it('should style print/media query', () => {
container.innerHTML = multiline(
'<style class="testcase-style">',
' h1 { background: green; }',
' h1 strong { color: orange; }',
'</style>',
'<style class="testcase-style-2" media="print, screen and (max-width: 9999999px)">',
' h1 { background: gray; }',
' h1 strong { color: red; }',
'</style>',
'<h1>Some test foor...... <strong>Oh uh media query :D</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(document.querySelector('h1')!).backgroundColor).toBe('rgb(102, 102, 102)');
expect(getComputedStyle(document.querySelector('h1 strong')!).color).toBe('rgb(255, 26, 26)');
expect(document.querySelector('.testcase-style-2')!.nextElementSibling!.classList.contains('darkreader--sync')).toBe(true);
});
it('should handle same cssText but different media rule', () => {
container.innerHTML = multiline(
'<style class="testcase-style">',
' @media screen and (min-width: 200000000000000000000px) {',
' h1 { background: green; }',
' }',
' @media screen and (min-width: 4px) {',
' h1 { background: green; }',
' }',
'</style>',
'<h1>Some test foor...... <strong>Oh uhm removing styles :(</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(document.querySelector('h1')!).backgroundColor).toBe('rgb(0, 102, 0)');
});
it('should not style blacklisted media and handle uppercase media', () => {
container.innerHTML = multiline(
'<style class="testcase-style">',
' h1 { background: green; }',
' h1 strong { color: orange; }',
'</style>',
'<style class="testcase-style-2" media="Print">',
' h1 { background: gray; }',
' h1 strong { color: red; }',
'</style>',
'<h1>Some test foor...... <strong>Oh uhm removing styles :(</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(document.querySelector('h1')!).backgroundColor).toBe('rgb(0, 102, 0)');
expect(getComputedStyle(document.querySelector('h1 strong')!).color).toBe('rgb(255, 174, 26)');
expect(document.querySelector('.testcase-style-2')!.nextElementSibling!.classList.contains('darkreader--sync')).toBe(false);
});
it('should handle media query and print', () => {
container.innerHTML = multiline(
'<style class="testcase-style">',
' @media (min-width: 2px), print {',
' h1 { background: green; }',
' }',
'</style>',
'<h1>Some test foor...... <strong>Oh uhm removing styles :(</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(document.querySelector('h1')!).backgroundColor).toBe('rgb(0, 102, 0)');
});
});
@@ -0,0 +1,178 @@
import '../support/polyfills';
import {DEFAULT_THEME} from '../../../src/defaults';
import {createOrUpdateDynamicTheme, removeDynamicTheme} from '../../../src/inject/dynamic-theme';
import {isFirefox, isSafari} from '../../../src/utils/platform';
import {multiline, timeout} from '../support/test-utils';
const theme = {
...DEFAULT_THEME,
darkSchemeBackgroundColor: 'black',
darkSchemeTextColor: 'white',
};
let container: HTMLElement;
beforeEach(() => {
container = document.body;
container.innerHTML = '';
});
afterEach(() => {
removeDynamicTheme();
container.innerHTML = '';
});
describe('SHADOW DOM', () => {
it('should add static overrides', async () => {
container.innerHTML = multiline(
'<div class="shadow-dom-wrapper"></div>',
);
document.querySelector('.shadow-dom-wrapper')!.attachShadow({mode: 'open'});
createOrUpdateDynamicTheme(theme, null, false);
const shadowRoot = document.querySelector('.shadow-dom-wrapper')!.shadowRoot!;
expect(shadowRoot.firstElementChild!.classList.contains('darkreader--inline')).toBe(true);
expect(shadowRoot.firstElementChild!.nextElementSibling!.classList.contains('darkreader--override')).toBe(true);
expect(shadowRoot.firstElementChild!.nextElementSibling!.nextElementSibling!.classList.contains('darkreader--invert')).toBe(true);
});
it('should override styles', async () => {
container.innerHTML = multiline(
'<div class="shadow-dom-wrapper"></div>',
);
const shadow = document.querySelector('.shadow-dom-wrapper')!.attachShadow({mode: 'open'});
const style = document.createElement('style');
style.classList.add('test-case-style');
shadow.appendChild(style);
style.sheet!.insertRule('h1 { color: gray }');
style.sheet!.insertRule('strong { color: red }');
createOrUpdateDynamicTheme(theme, null, false);
const shadowRoot = document.querySelector('.shadow-dom-wrapper')!.shadowRoot!;
const testCase = shadowRoot.querySelector('.test-case-style')!;
expect(testCase.nextElementSibling!.classList.contains('darkreader--sync')).toBe(true);
expect((testCase.nextElementSibling as HTMLStyleElement).sheet!.cssRules.length).toBe(2);
});
it('should react to DOM changes', async () => {
container.innerHTML = multiline(
'<div class="shadow-dom-wrapper"></div>',
);
const shadow = document.querySelector('.shadow-dom-wrapper')!.attachShadow({mode: 'open'});
createOrUpdateDynamicTheme(theme, null, false);
const style = document.createElement('style');
style.classList.add('test-case-style');
shadow.appendChild(style);
style.sheet!.insertRule('h1 { color: gray }');
style.sheet!.insertRule('strong { color: red }');
await timeout(0);
const shadowRoot = document.querySelector('.shadow-dom-wrapper')!.shadowRoot!;
const testCase = shadowRoot.querySelector('.test-case-style')!;
expect(shadowRoot.firstElementChild!.classList.contains('darkreader--inline')).toBe(true);
expect(shadowRoot.firstElementChild!.nextElementSibling!.classList.contains('darkreader--override')).toBe(true);
expect(testCase.nextElementSibling!.classList.contains('darkreader--sync')).toBe(true);
expect((testCase.nextElementSibling as HTMLStyleElement).sheet!.cssRules.length).toBe(2);
});
it('should override inline styles', async () => {
container.innerHTML = multiline(
'<div class="shadow-dom-wrapper"></div>',
);
const shadow = document.querySelector('.shadow-dom-wrapper')!.attachShadow({mode: 'open'});
createOrUpdateDynamicTheme(theme, null, false);
const standardElement = document.createElement('p');
standardElement.style.color = 'red';
shadow.appendChild(standardElement);
await timeout(0);
const shadowRoot = document.querySelector('.shadow-dom-wrapper')!.shadowRoot!;
expect(getComputedStyle(shadowRoot.querySelector('p')!).color).toBe('rgb(255, 26, 26)');
});
it('should handle defined custom elements', async () => {
container.innerHTML = multiline(
'<custom-element>',
'</custom-element>',
);
class CustomElement extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
const style = document.createElement('style');
style.textContent = 'p { color: pink }';
const paragraph = document.createElement('p');
paragraph.textContent = 'Some text content that should be pink.';
shadowRoot.append(style);
shadowRoot.append(paragraph);
}
}
customElements.define('custom-element', CustomElement);
createOrUpdateDynamicTheme(theme, null, false);
const shadowRoot = document.querySelector('custom-element')!.shadowRoot!;
expect(getComputedStyle(shadowRoot.querySelector('p')!).color).toBe('rgb(255, 198, 208)');
});
it('should react to defined custom elements', async () => {
container.innerHTML = multiline(
'<delayed-custom-element>',
'</delayed-custom-element>',
);
class DelayedCustomElement extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
const style = document.createElement('style');
style.textContent = 'p { color: pink }';
const paragraph = document.createElement('p');
paragraph.textContent = 'Some text content that should be pink.';
shadowRoot.append(style);
shadowRoot.append(paragraph);
}
}
createOrUpdateDynamicTheme(theme, null, false);
customElements.define('delayed-custom-element', DelayedCustomElement);
await timeout(0);
const shadowRoot = document.querySelector('delayed-custom-element')!.shadowRoot!;
expect(getComputedStyle(shadowRoot.querySelector('p')!).color).toBe('rgb(255, 198, 208)');
});
it('should override styles', async () => {
// Firefox by default doesn't enable the CSSStyleSheet constructor.
if (isFirefox || isSafari) {
return;
}
container.innerHTML = multiline(
'<div class="shadow-dom-wrapper"></div>',
);
const newRule = new CSSStyleSheet();
newRule.insertRule(':host { --red: red }');
const shadow = document.querySelector('.shadow-dom-wrapper')!.attachShadow({mode: 'open'});
shadow.adoptedStyleSheets = [newRule];
const style = document.createElement('style');
style.classList.add('test-case-style');
shadow.appendChild(style);
style.sheet!.insertRule('h1 { color: var(--red) }');
const h1 = document.createElement('h1');
shadow.appendChild(h1);
createOrUpdateDynamicTheme(theme, null, false);
const shadowRoot = document.querySelector('.shadow-dom-wrapper')!.shadowRoot!;
const testCase = shadowRoot.querySelector('.test-case-style')!;
const darkendH1 = shadowRoot.querySelector('h1')!;
expect(testCase.nextElementSibling!.classList.contains('darkreader--sync')).toBe(true);
expect((testCase.nextElementSibling as HTMLStyleElement).sheet!.cssRules.length).toBe(1);
expect(shadowRoot.adoptedStyleSheets.length).toBe(2);
expect(getComputedStyle(darkendH1).color).toBe('rgb(255, 26, 26)');
});
});
@@ -0,0 +1,248 @@
import '../support/polyfills';
import {DEFAULT_THEME} from '../../../src/defaults';
import {createOrUpdateDynamicTheme, removeDynamicTheme} from '../../../src/inject/dynamic-theme';
import {createStyleSheetModifier} from '../../../src/inject/dynamic-theme/stylesheet-modifier';
import {multiline, timeout} from '../support/test-utils';
const theme = {
...DEFAULT_THEME,
darkSchemeBackgroundColor: 'black',
darkSchemeTextColor: 'white',
};
let container: HTMLElement;
beforeEach(() => {
container = document.body;
container.innerHTML = '';
});
afterEach(() => {
removeDynamicTheme();
container.innerHTML = '';
});
describe('STYLE ELEMENTS', () => {
it('should fill CSSStyleSheet with overridden rules', () => {
const style = document.createElement('style');
style.textContent = 'body { background-color: white; } h1 { color: black; }';
container.append(style);
const modifier = createStyleSheetModifier();
const overrideStyle = document.createElement('style');
container.append(overrideStyle);
const override = overrideStyle.sheet!;
modifier.modifySheet({
theme,
sourceCSSRules: style.sheet!.cssRules,
ignoreImageAnalysis: [],
force: false,
prepareSheet: () => override,
isAsyncCancelled: () => false,
});
expect(override.cssRules.length).toBe(2);
expect((override.cssRules[0] as CSSStyleRule).selectorText).toBe('body');
expect((override.cssRules[0] as CSSStyleRule).style.getPropertyValue('background-color')).toBe('var(--darkreader-background-ffffff, #000000)');
expect((override.cssRules[1] as CSSStyleRule).selectorText).toBe('h1');
expect((override.cssRules[1] as CSSStyleRule).style.getPropertyValue('color')).toBe('var(--darkreader-text-000000, #ffffff)');
});
it('should override User Agent style', async () => {
container.innerHTML = multiline(
'<span>Text</span>',
'<a href="#">Link</a>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container).backgroundColor).toBe('rgb(0, 0, 0)');
expect(getComputedStyle(container).color).toBe('rgb(255, 255, 255)');
expect(getComputedStyle(container.querySelector('span')!).color).toBe('rgb(255, 255, 255)');
expect(getComputedStyle(container.querySelector('a')!).color).toBe('rgb(51, 145, 255)');
});
it('should override static style', async () => {
container.innerHTML = multiline(
'<style>',
' h1 { background: gray; }',
' h1 strong { color: red; }',
'</style>',
'<h1>Style <strong>override</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 @import', async () => {
container.innerHTML = multiline(
'<style>',
` @import "data:text/css;utf8,${encodeURIComponent('h1 { background: gray; }')}";`,
' h1 strong { color: red; }',
'</style>',
'<h1>Style <strong>with @import</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
await timeout(50);
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 @import"..."', async () => {
container.innerHTML = multiline(
'<style>',
` @import"data:text/css;utf8,${encodeURIComponent('h1 { background: gray; }')}";`,
' h1 strong { color: red; }',
'</style>',
'<h1>Style <strong>with @import"..."</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
await timeout(50);
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 restore override', async () => {
container.innerHTML = multiline(
'<style class="testcase-style">',
' h1 { color: gray; }',
' h1 strong { color: red; }',
'</style>',
'<h1>Style <strong>override</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(141, 141, 141)');
expect(getComputedStyle(container.querySelector('h1 strong')!).color).toBe('rgb(255, 26, 26)');
const style = document.querySelector('.testcase-style')!;
style.nextSibling!.remove();
await timeout(0);
expect((style.nextSibling as HTMLStyleElement).classList.contains('darkreader--sync')).toBe(true);
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(141, 141, 141)');
expect(getComputedStyle(container.querySelector('h1 strong')!).color).toBe('rgb(255, 26, 26)');
});
it('should move override', async () => {
container.innerHTML = multiline(
'<style class="testcase-style">',
' h1 { background: gray; }',
' h1 strong { color: red; }',
'</style>',
'<h1>Some test foor...... <strong>Moving styles</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(102, 102, 102)');
expect(getComputedStyle(container.querySelector('h1 strong')!).color).toBe('rgb(255, 26, 26)');
const style = document.querySelector('.testcase-style')!;
container.append(style);
await timeout(0);
expect((style.nextSibling as HTMLStyleElement).classList.contains('darkreader--sync')).toBe(true);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(102, 102, 102)');
expect(getComputedStyle(container.querySelector('h1 strong')!).color).toBe('rgb(255, 26, 26)');
});
it('should remove override', async () => {
container.innerHTML = multiline(
'<style class="testcase-style">',
' h1 { background: gray; }',
' h1 strong { color: red; }',
'</style>',
'<h1>Some test foor...... <strong>Oh uhm removing styles :(</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
const style = document.querySelector('.testcase-style')!;
const sibling = style.nextSibling!;
style.remove();
await timeout(0);
expect(sibling.isConnected).toBe(false);
expect(document.querySelector('.darkreader--sync')).toBe(null);
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 react to updated style', async () => {
container.innerHTML = multiline(
'<style class="testcase-style"></style>',
'<h1>Some test foor...... <strong>Oh uhm a pink background</strong></h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
const style: HTMLStyleElement = document.querySelector('.testcase-style')!;
style.sheet!.insertRule('h1 { color: gray }');
style.sheet!.insertRule('strong { color: red }');
style.sheet!.insertRule('body { background-color: pink }');
await timeout(0);
expect((style.nextSibling as HTMLStyleElement).sheet!.cssRules[0].cssText).toBe('body { background-color: var(--darkreader-background-ffc0cb, #320009); }');
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(141, 141, 141)');
expect(getComputedStyle(container.querySelector('h1 strong')!).color).toBe('rgb(255, 26, 26)');
expect(getComputedStyle(document.body).backgroundColor).toBe('rgb(50, 0, 9)');
});
it('should react on style text change', async () => {
container.innerHTML = multiline(
'<style class="testcase-style">',
' h1 strong { color: red; }',
'</style>',
'<h1>Style <strong>text change</strong></h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(document.querySelector('h1 strong')!).color).toBe('rgb(255, 26, 26)');
document.querySelector('.testcase-style')!.textContent = 'h1 strong { color: green; }';
await timeout(0);
expect(getComputedStyle(document.querySelector('h1 strong')!).color).toBe('rgb(140, 255, 140)');
});
it('should react to a new style', async () => {
container.innerHTML = multiline(
'<h1>Some test foor...... <strong>Oh uhm what?</strong>!</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
const style: HTMLStyleElement = document.createElement('style');
style.classList.add('testcase-style');
container.append(style);
style.sheet!.insertRule('h1 { color: pink }');
style.sheet!.insertRule('strong { color: orange }');
await timeout(0);
const newStyle: HTMLStyleElement = document.querySelector('.testcase-style')!;
const nextSibling = newStyle.nextSibling as HTMLStyleElement;
expect(nextSibling.sheet!.cssRules.length).toBe(2);
expect(nextSibling.classList.contains('darkreader--sync')).toBe(true);
expect(getComputedStyle(document.querySelector('h1')!).color).toBe('rgb(255, 198, 208)');
expect(getComputedStyle(document.querySelector('h1 strong')!).color).toBe('rgb(255, 174, 26)');
});
it('should restore override after container move', async () => {
container.innerHTML = multiline(
'<div class="style-container">',
' <style class="testcase-style">',
' h1 { background: gray; }',
' h1 { color: green; }',
' </style>',
'</div>',
'<h1>Moving container</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(102, 102, 102)');
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(140, 255, 140)');
const styleContainer = document.querySelector('.style-container')!;
const style = styleContainer.querySelector('.testcase-style')!;
const override = style.nextElementSibling as HTMLStyleElement;
container.append(styleContainer);
await timeout(0);
expect(style.nextElementSibling).toBe(override);
expect(override.sheet!.cssRules.length).toBe(2);
expect(getComputedStyle(container.querySelector('h1')!).backgroundColor).toBe('rgb(102, 102, 102)');
expect(getComputedStyle(container.querySelector('h1')!).color).toBe('rgb(140, 255, 140)');
});
});
File diff suppressed because it is too large Load Diff