chore: 整理 third-party Dark Reader 目录并移除本地说明文件跟踪
This commit is contained in:
@@ -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
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* This file is necessary because Karma does not support configutation
|
||||
* via ES modules. This file is a CommonJS module which wraps a regular
|
||||
* ES module.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const realConfigureKarma = import('./karma.conf.js');
|
||||
|
||||
async function configureKarma(config, env) {
|
||||
return ((await realConfigureKarma).configureKarma)(config, env);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {LocalConfig} config
|
||||
* @returns {void}
|
||||
*/
|
||||
module.exports = async (config) =>
|
||||
config.set(await configureKarma(config, process.env));
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
module.exports.configureKarma = async (config, env) => configureKarma(config, env);
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
/** @typedef {import('karma').Config & {headless: boolean, debug: boolean, ci: boolean, coverage: boolean}} LocalConfig */
|
||||
/** @typedef {import('karma').ConfigOptions} ConfigOptions */
|
||||
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
|
||||
import rollupPluginReplace from '@rollup/plugin-replace';
|
||||
import rollupPluginTypescript from '@rollup/plugin-typescript';
|
||||
import rollupPluginIstanbul from 'rollup-plugin-istanbul';
|
||||
import typescript from 'typescript';
|
||||
|
||||
import {absolutePath} from '../../tasks/paths.js';
|
||||
|
||||
import {createEchoServer} from './support/echo-server.js';
|
||||
|
||||
/**
|
||||
* @param {Partial<LocalConfig>} config
|
||||
* @param {Record<string, string>} env
|
||||
* @returns {ConfigOptions}
|
||||
*/
|
||||
export function configureKarma(config, env) {
|
||||
const headless = config.headless || Boolean(env.KARMA_HEADLESS) || false;
|
||||
|
||||
/** @type {ConfigOptions} */
|
||||
const options = {
|
||||
failOnFailingTestSuite: true,
|
||||
failOnEmptyTestSuite: true,
|
||||
basePath: '../..',
|
||||
frameworks: ['jasmine'],
|
||||
files: [
|
||||
'tests/inject/support/customize.ts',
|
||||
'tests/inject/support/polyfills.ts',
|
||||
{pattern: 'tests/inject/**/*.tests.ts', watched: false},
|
||||
],
|
||||
plugins: [
|
||||
'karma-chrome-launcher',
|
||||
'karma-firefox-launcher',
|
||||
process.platform === 'darwin' ? 'karma-safari-launcher' : null,
|
||||
'karma-rollup-preprocessor',
|
||||
'karma-jasmine',
|
||||
'karma-spec-reporter',
|
||||
].filter(Boolean),
|
||||
preprocessors: {
|
||||
'**/*.+(ts|tsx)': ['rollup'],
|
||||
},
|
||||
rollupPreprocessor: {
|
||||
plugins: [
|
||||
rollupPluginTypescript({
|
||||
rootDir: absolutePath('.'),
|
||||
typescript,
|
||||
outDir: 'build/tests',
|
||||
tsconfig: absolutePath('tests/inject/tsconfig.json'),
|
||||
cacheDir: `${fs.realpathSync(os.tmpdir())}/darkreader_typescript_test_cache`,
|
||||
}),
|
||||
rollupPluginReplace({
|
||||
preventAssignment: true,
|
||||
__DEBUG__: false,
|
||||
__FIREFOX_MV2__: false,
|
||||
__CHROMIUM_MV2__: false,
|
||||
__CHROMIUM_MV3__: false,
|
||||
__THUNDERBIRD__: false,
|
||||
__PLUS__: false,
|
||||
__PORT__: '-1',
|
||||
__TEST__: true,
|
||||
__WATCH__: false,
|
||||
}),
|
||||
],
|
||||
output: {
|
||||
dir: 'build/tests',
|
||||
strict: true,
|
||||
format: 'iife',
|
||||
sourcemap: 'inline',
|
||||
},
|
||||
},
|
||||
reporters: ['spec'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: headless
|
||||
? ['ChromeHeadless', 'FirefoxHeadless']
|
||||
: ['Chrome', 'Firefox', process.platform === 'darwin' ? 'Safari' : null].filter(Boolean),
|
||||
singleRun: true,
|
||||
concurrency: 1,
|
||||
};
|
||||
|
||||
if (config.debug) {
|
||||
options.browsers = ['Chrome'];
|
||||
options.singleRun = false;
|
||||
options.concurrency = Infinity;
|
||||
options.logLevel = config.LOG_DEBUG;
|
||||
}
|
||||
|
||||
if (config.ci) {
|
||||
options.customLaunchers = {};
|
||||
options.browsers = [];
|
||||
|
||||
// CHROME_TEST and FIREFOX_TEST are used in CI
|
||||
const chrome = env.CHROME_TEST;
|
||||
const firefox = env.FIREFOX_TEST;
|
||||
const all = !chrome && !firefox;
|
||||
// Chrome
|
||||
if (chrome || all) {
|
||||
options.customLaunchers['CIChromeHeadless'] = {
|
||||
base: 'ChromeHeadless',
|
||||
flags: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
};
|
||||
options.browsers.push('CIChromeHeadless');
|
||||
}
|
||||
|
||||
// Firefox
|
||||
if (firefox || all) {
|
||||
options.customLaunchers['CIFirefoxHeadless'] = {
|
||||
base: 'FirefoxHeadless',
|
||||
};
|
||||
options.browsers.push('CIFirefoxHeadless');
|
||||
}
|
||||
|
||||
options.autoWatch = false;
|
||||
options.singleRun = true;
|
||||
options.concurrency = 1;
|
||||
options.logLevel = config.LOG_DEBUG;
|
||||
}
|
||||
|
||||
if (config.coverage) {
|
||||
options.plugins.push('karma-coverage');
|
||||
const plugin = rollupPluginIstanbul({
|
||||
exclude: ['tests/**/*.*', 'src/inject/dynamic-theme/stylesheet-proxy.ts'],
|
||||
});
|
||||
options.rollupPreprocessor.plugins.push(plugin);
|
||||
options.reporters.push('coverage');
|
||||
options.coverageReporter = {
|
||||
type: 'html',
|
||||
dir: 'tests/inject/coverage/',
|
||||
};
|
||||
}
|
||||
|
||||
// HACK: Create CORS server here
|
||||
// Previously a separate Karma runner file was used
|
||||
const corsServerPort = 9966;
|
||||
createEchoServer(corsServerPort).then(() => console.log(`CORS echo server running on port ${corsServerPort}`));
|
||||
|
||||
return options;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import type {MessageBGtoCS, MessageCStoBG} from '../../../src/definitions';
|
||||
import {MessageTypeBGtoCS, MessageTypeCStoBG} from '../../../src/utils/message';
|
||||
|
||||
let nativeSendMessage: typeof chrome.runtime.sendMessage;
|
||||
const bgResponses = new Map<string, string>();
|
||||
|
||||
export function stubChromeRuntimeMessage(): void {
|
||||
nativeSendMessage = chrome.runtime.sendMessage;
|
||||
const listeners: Array<(message: MessageBGtoCS) => void> = (chrome.runtime.onMessage as any)['__listeners__'];
|
||||
|
||||
(chrome.runtime as any).sendMessage = (message: MessageCStoBG) => {
|
||||
if (message.type === MessageTypeCStoBG.FETCH) {
|
||||
const {id, data: {url}} = message;
|
||||
setTimeout(() => {
|
||||
listeners.forEach((listener) => {
|
||||
if (!bgResponses.has(url)) {
|
||||
throw new Error('Response is missing, use `stubBackgroundFetchResponse()`');
|
||||
}
|
||||
const data = bgResponses.get(url);
|
||||
listener({type: MessageTypeBGtoCS.FETCH_RESPONSE, data, error: null, id});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function resetChromeRuntimeMessageStub(): void {
|
||||
chrome.runtime.sendMessage = nativeSendMessage;
|
||||
bgResponses.clear();
|
||||
}
|
||||
|
||||
export function stubBackgroundFetchResponse(url: string, content: string): void {
|
||||
bgResponses.set(url, content);
|
||||
}
|
||||
|
||||
const urlResponses = new Map<string, string>();
|
||||
export function stubChromeRuntimeGetURL(path: string, url: string): void {
|
||||
urlResponses.set(path, url);
|
||||
(chrome.runtime as any).getURL = (path: string) => {
|
||||
return urlResponses.get(path);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
(() => {
|
||||
if (window.top === window.self) {
|
||||
return;
|
||||
}
|
||||
const topDoc = window.top!.document;
|
||||
const style = topDoc.createElement('style');
|
||||
style.textContent = [
|
||||
'body { background-color: #222222; color: #dddddd; }',
|
||||
'#banner { background-color: #226644; }',
|
||||
'.executing { background-color: #662233; }',
|
||||
].join('\n');
|
||||
topDoc.head.append(style);
|
||||
})();
|
||||
@@ -0,0 +1,11 @@
|
||||
export function getEchoURL(content: string, type = 'text/plain'): string {
|
||||
return `http://localhost:9966/echo?${new URLSearchParams({type, content})}`;
|
||||
}
|
||||
|
||||
export function getCSSEchoURL(content: string): string {
|
||||
return getEchoURL(content, 'text/css');
|
||||
}
|
||||
|
||||
export function getJSEchoURL(script: string): string {
|
||||
return getEchoURL(script, 'application/javascript');
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// @ts-check
|
||||
import http from 'http';
|
||||
|
||||
export async function createEchoServer(/** @type {number} */port) {
|
||||
/** @type {import('http').Server | null} */
|
||||
let server;
|
||||
|
||||
/** @type {import('http').RequestListener} */
|
||||
function handleRequest(req, res) {
|
||||
const parsedURL = new URL(req.url ?? '', `http://${req.headers.host}`);
|
||||
const pathName = parsedURL.pathname;
|
||||
|
||||
if (pathName !== '/echo') {
|
||||
res.statusCode = 500;
|
||||
res.end('The URL path must be /echo');
|
||||
return;
|
||||
}
|
||||
|
||||
const {searchParams} = parsedURL;
|
||||
|
||||
const content = searchParams.get('content');
|
||||
if (!content) {
|
||||
res.statusCode = 500;
|
||||
res.end('Send content like /echo?type=text%2Fplain&content=XYZ');
|
||||
return;
|
||||
}
|
||||
|
||||
const contentType = searchParams.get('type') || 'text/plain';
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', contentType);
|
||||
res.end(content, 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function start() {
|
||||
return new Promise((resolve) => {
|
||||
server = http
|
||||
.createServer(handleRequest)
|
||||
.listen(port, () => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<void> | undefined}
|
||||
*/
|
||||
function close() {
|
||||
if (!server) {
|
||||
return;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
server?.close((err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
}
|
||||
server = null;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
process.on('exit', close);
|
||||
process.on('SIGINT', close);
|
||||
|
||||
await start();
|
||||
|
||||
return {
|
||||
close,
|
||||
url: `http://localhost:${port}`,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
if (!window.hasOwnProperty('chrome')) {
|
||||
window.chrome = {} as any;
|
||||
}
|
||||
if (!chrome.hasOwnProperty('runtime')) {
|
||||
chrome.runtime = {} as any;
|
||||
}
|
||||
if (!chrome.runtime.hasOwnProperty('onMessage')) {
|
||||
type AnyFunction = () => void;
|
||||
const listeners = new Set<AnyFunction>();
|
||||
(chrome.runtime as any).onMessage = {
|
||||
addListener: (listener: AnyFunction) => {
|
||||
listeners.add(listener);
|
||||
},
|
||||
removeListener: (listener: AnyFunction) => {
|
||||
listeners.delete(listener);
|
||||
},
|
||||
} as any;
|
||||
(chrome.runtime.onMessage as any)['__listeners__'] = listeners;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Loaded with HTML/DOM only
|
||||
|
||||
export function multiline(...lines: string[]): string {
|
||||
if (lines.length < 1) {
|
||||
return '\n';
|
||||
}
|
||||
if (lines[lines.length - 1] !== '') {
|
||||
lines.push('');
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function timeout(delay: number): Promise<void> {
|
||||
return new Promise<void>((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
|
||||
export function waitForEvent(eventName: string): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
document.addEventListener(eventName, () => resolve(), {once: true});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "ES2019",
|
||||
"module": "ES2015",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"types": [
|
||||
"chrome",
|
||||
"jasmine"
|
||||
],
|
||||
"allowJs": true,
|
||||
"downlevelIteration": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react",
|
||||
"jsxFactory": "m",
|
||||
"moduleResolution": "Node",
|
||||
"noEmit": true,
|
||||
"noImplicitAny": true,
|
||||
"removeComments": false,
|
||||
"sourceMap": true,
|
||||
"inlineSources": true,
|
||||
"noEmitOnError": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"paths": {
|
||||
"@plus/*": ["../../src/stubs/*"],
|
||||
"definitions": ["../../src/definitions"],
|
||||
"utils/*": ["../../src/utils/*"]
|
||||
},
|
||||
"ignoreDeprecations": "6.0",
|
||||
},
|
||||
"exclude": [
|
||||
"./coverage"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
import type {UserSettings} from '../../../src/definitions';
|
||||
import {isURLEnabled, isURLMatched, isPDF, getURLHostOrProtocol, getAbsoluteURL} from '../../../src/utils/url';
|
||||
|
||||
it('URL is enabled', () => {
|
||||
function fillUserSettings(settings: Partial<UserSettings>): UserSettings {
|
||||
return {
|
||||
schemeVersion: 2,
|
||||
enabled: false,
|
||||
fetchNews: false,
|
||||
theme: null,
|
||||
presets: [],
|
||||
customThemes: [],
|
||||
disabledFor: [],
|
||||
enabledFor: [],
|
||||
enabledByDefault: true,
|
||||
changeBrowserTheme: false,
|
||||
syncSettings: false,
|
||||
syncSitesFixes: false,
|
||||
automation: null,
|
||||
time: null,
|
||||
location: null,
|
||||
previewNewDesign: false,
|
||||
enableForPDF: false,
|
||||
enableForProtectedPages: false,
|
||||
enableContextMenus: false,
|
||||
detectDarkTheme: false,
|
||||
...settings,
|
||||
} as UserSettings;
|
||||
}
|
||||
|
||||
// Not invert listed
|
||||
expect(isURLEnabled(
|
||||
'https://www.google.com/',
|
||||
fillUserSettings({disabledFor: [], enabledFor: [], enabledByDefault: true}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toBe(true);
|
||||
expect(isURLEnabled(
|
||||
'https://www.google.com/',
|
||||
fillUserSettings({disabledFor: ['google.com'], enabledFor: [], enabledByDefault: true}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toBe(false);
|
||||
expect(isURLEnabled(
|
||||
'https://mail.google.com/mail/u/0/',
|
||||
fillUserSettings({disabledFor: ['google.com'], enabledFor: [], enabledByDefault: true}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toBe(true);
|
||||
expect(isURLEnabled(
|
||||
'https://mail.google.com/mail/u/0/',
|
||||
fillUserSettings({disabledFor: ['mail.google.com'], enabledFor: [], enabledByDefault: true}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toBe(false);
|
||||
expect(isURLEnabled(
|
||||
'https://mail.google.com/mail/u/0/',
|
||||
fillUserSettings({disabledFor: ['mail.google.*'], enabledFor: [], enabledByDefault: true}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toBe(false);
|
||||
expect(isURLEnabled(
|
||||
'https://mail.google.com/mail/u/0/',
|
||||
fillUserSettings({disabledFor: ['mail.google.*/mail'], enabledFor: [], enabledByDefault: true}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toBe(false);
|
||||
expect(isURLEnabled(
|
||||
'https://mail.google.com/mail/u/0/',
|
||||
fillUserSettings({disabledFor: [], enabledFor: [], enabledByDefault: true}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toBe(true);
|
||||
expect(isURLEnabled(
|
||||
'https://mail.google.com/mail/u/0/',
|
||||
fillUserSettings({disabledFor: ['google.com/maps'], enabledFor: [], enabledByDefault: true}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toBe(true);
|
||||
|
||||
// Invert listed only
|
||||
expect(isURLEnabled(
|
||||
'https://mail.google.com/mail/u/0/',
|
||||
fillUserSettings({disabledFor: [], enabledFor: ['google.com'], enabledByDefault: false}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toBe(false);
|
||||
expect(isURLEnabled(
|
||||
'https://mail.google.com/mail/u/0/',
|
||||
fillUserSettings({disabledFor: [], enabledFor: ['mail.google.*/mail'], enabledByDefault: false}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toBe(true);
|
||||
expect(isURLEnabled(
|
||||
'https://mail.google.com/mail/u/0/',
|
||||
fillUserSettings({disabledFor: [], enabledFor: [], enabledByDefault: false}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toBe(false);
|
||||
expect(isURLEnabled(
|
||||
'https://mail.google.com/mail/u/0/',
|
||||
fillUserSettings({disabledFor: [], enabledFor: ['*.google.com/maps'], enabledByDefault: false}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toBe(false);
|
||||
|
||||
// Special URLs
|
||||
expect(isURLEnabled(
|
||||
'https://chrome.google.com/webstore',
|
||||
fillUserSettings({disabledFor: ['chrome.google.com'], enabledFor: [], enabledByDefault: true, enableForProtectedPages: true}),
|
||||
{isProtected: true, isInDarkList: false},
|
||||
)).toBe(false);
|
||||
expect(isURLEnabled(
|
||||
'https://chrome.google.com/webstore',
|
||||
fillUserSettings({disabledFor: [], enabledFor: ['chrome.google.com'], enabledByDefault: false, enableForProtectedPages: true}),
|
||||
{isProtected: true, isInDarkList: false},
|
||||
)).toBe(true);
|
||||
expect(isURLEnabled(
|
||||
'https://chrome.google.com/webstore',
|
||||
fillUserSettings({disabledFor: [], enabledFor: [], enabledByDefault: true, enableForProtectedPages: false}),
|
||||
{isProtected: true, isInDarkList: false},
|
||||
)).toBe(false);
|
||||
expect(isURLEnabled(
|
||||
'https://chrome.google.com/webstore',
|
||||
fillUserSettings({disabledFor: ['chrome.google.com'], enabledFor: [], enabledByDefault: true, enableForProtectedPages: true}),
|
||||
{isProtected: true, isInDarkList: false},
|
||||
)).toBe(false);
|
||||
expect(isURLEnabled(
|
||||
'https://microsoftedge.microsoft.com/addons',
|
||||
fillUserSettings({disabledFor: ['microsoftedge.microsoft.com'], enabledFor: [], enabledByDefault: true, enableForProtectedPages: true}),
|
||||
{isProtected: true, isInDarkList: false},
|
||||
)).toBe(false);
|
||||
expect(isURLEnabled(
|
||||
'https://microsoftedge.microsoft.com/addons',
|
||||
fillUserSettings({disabledFor: [], enabledFor: ['microsoftedge.microsoft.com'], enabledByDefault: false, enableForProtectedPages: true}),
|
||||
{isProtected: true, isInDarkList: false},
|
||||
)).toBe(true);
|
||||
expect(isURLEnabled(
|
||||
'https://duckduckgo.com',
|
||||
fillUserSettings({disabledFor: [], enabledFor: [], enabledByDefault: true, enableForProtectedPages: true}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toBe(true);
|
||||
expect(isURLEnabled(
|
||||
'https://darkreader.org/',
|
||||
fillUserSettings({disabledFor: [], enabledFor: [], enabledByDefault: true}),
|
||||
{isProtected: false, isInDarkList: true},
|
||||
)).toBe(false);
|
||||
expect(isURLEnabled(
|
||||
'https://darkreader.org/',
|
||||
fillUserSettings({disabledFor: [], enabledFor: ['darkreader.org'], enabledByDefault: false}),
|
||||
{isProtected: false, isInDarkList: true},
|
||||
)).toBe(true);
|
||||
expect(isURLEnabled(
|
||||
'https://www.google.com/file.pdf',
|
||||
fillUserSettings({enableForPDF: true, disabledFor: [], enabledFor: [], enabledByDefault: true}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toBe(true);
|
||||
expect(isURLEnabled(
|
||||
'https://www.google.com/file.pdf',
|
||||
fillUserSettings({enableForPDF: true, disabledFor: [], enabledFor: [], enabledByDefault: false}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toBe(true);
|
||||
expect(isURLEnabled(
|
||||
'https://www.google.com/file.pdf',
|
||||
fillUserSettings({enableForPDF: false, disabledFor: [], enabledFor: [], enabledByDefault: true}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toBe(false);
|
||||
expect(isURLEnabled(
|
||||
'https://www.google.com/file.pdf/resource',
|
||||
fillUserSettings({enableForPDF: true, disabledFor: [], enabledFor: [], enabledByDefault: false}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toBe(false);
|
||||
expect(isURLEnabled(
|
||||
'https://www.google.com/file.pdf/resource',
|
||||
fillUserSettings({enableForPDF: true, disabledFor: [], enabledFor: [], enabledByDefault: true}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toBe(true);
|
||||
expect(isURLEnabled(
|
||||
'https://www.google.com/very/good/hidden/folder/pdf#file.pdf',
|
||||
fillUserSettings({enableForPDF: true, disabledFor: ['https://www.google.com/very/good/hidden/folder/pdf'], enabledFor: [], enabledByDefault: true}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toBe(false);
|
||||
expect(isURLEnabled(
|
||||
'https://leetcode.com/problems/two-sum/',
|
||||
fillUserSettings({enableForPDF: false, disabledFor: ['leetcode.com/problems/'], enabledFor: [], enabledByDefault: true}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toBe(false);
|
||||
expect(isURLEnabled(
|
||||
'https://leetcode.com/problemset/all/',
|
||||
fillUserSettings({enableForPDF: false, disabledFor: ['leetcode.com/problems/'], enabledFor: [], enabledByDefault: true}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toBe(true);
|
||||
|
||||
// Dark theme detection
|
||||
expect(isURLEnabled(
|
||||
'https://github.com/',
|
||||
fillUserSettings({disabledFor: [], enabledFor: [], enabledByDefault: true, detectDarkTheme: true}),
|
||||
{isProtected: false, isInDarkList: false, isDarkThemeDetected: true},
|
||||
)).toBe(false);
|
||||
expect(isURLEnabled(
|
||||
'https://github.com/',
|
||||
fillUserSettings({disabledFor: [], enabledFor: [], enabledByDefault: true, detectDarkTheme: false}),
|
||||
{isProtected: false, isInDarkList: false, isDarkThemeDetected: true},
|
||||
)).toBe(true);
|
||||
expect(isURLEnabled(
|
||||
'https://github.com/',
|
||||
fillUserSettings({disabledFor: [], enabledFor: [], enabledByDefault: true, detectDarkTheme: true}),
|
||||
{isProtected: false, isInDarkList: false, isDarkThemeDetected: false},
|
||||
)).toBe(true);
|
||||
expect(isURLEnabled(
|
||||
'https://github.com/',
|
||||
fillUserSettings({disabledFor: [], enabledFor: ['github.com'], enabledByDefault: true, detectDarkTheme: true}),
|
||||
{isProtected: false, isInDarkList: false, isDarkThemeDetected: true},
|
||||
)).toBe(true);
|
||||
|
||||
// Test for PDF enabling
|
||||
expect(isPDF(
|
||||
'https://www.google.com/file.pdf'
|
||||
)).toBe(true);
|
||||
expect(isPDF(
|
||||
'https://www.google.com/file.pdf?id=2'
|
||||
)).toBe(true);
|
||||
expect(isPDF(
|
||||
'https://www.google.com/file.pdf/resource'
|
||||
)).toBe(false);
|
||||
expect(isPDF(
|
||||
'https://www.google.com/resource?file=file.pdf'
|
||||
)).toBe(false);
|
||||
expect(isPDF(
|
||||
'https://www.google.com/very/good/hidden/folder/pdf#file.pdf'
|
||||
)).toBe(false);
|
||||
expect(isPDF(
|
||||
'https://fi.wikipedia.org/wiki/Tiedosto:ExtIPA_chart_(2015).pdf?uselang=en'
|
||||
)).toBe(false);
|
||||
expect(isPDF(
|
||||
'https://commons.wikimedia.org/wiki/File:ExtIPA_chart_(2015).pdf'
|
||||
)).toBe(false);
|
||||
expect(isPDF(
|
||||
'https://upload.wikimedia.org/wikipedia/commons/5/56/ExtIPA_chart_(2015).pdf'
|
||||
)).toBe(true);
|
||||
|
||||
// IPV6 Testing
|
||||
expect(isURLEnabled(
|
||||
'https://[::1]:1337',
|
||||
fillUserSettings({disabledFor: ['google.com'], enabledFor: [], enabledByDefault: true}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toBe(true);
|
||||
expect(isURLEnabled(
|
||||
'https://[::1]:8080',
|
||||
fillUserSettings({disabledFor: ['[::1]:8080'], enabledFor: [], enabledByDefault: true}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toEqual(false);
|
||||
expect(isURLEnabled(
|
||||
'https://[::1]:8080',
|
||||
fillUserSettings({disabledFor: ['[::1]:8081'], enabledFor: [], enabledByDefault: false}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toEqual(false);
|
||||
expect(isURLEnabled(
|
||||
'https://[::1]:8080',
|
||||
fillUserSettings({disabledFor: ['[::1]:8081'], enabledFor: [], enabledByDefault: true}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toEqual(true);
|
||||
expect(isURLEnabled(
|
||||
'https://[::1]:17',
|
||||
fillUserSettings({disabledFor: ['[::1]'], enabledFor: [], enabledByDefault: false}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toEqual(false);
|
||||
expect(isURLEnabled(
|
||||
'https://[2001:4860:4860::8888]',
|
||||
fillUserSettings({disabledFor: [], enabledFor: ['[2001:4860:4860::8888]'], enabledByDefault: false}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toEqual(true);
|
||||
expect(isURLEnabled(
|
||||
'https://[2001:4860:4860::8844]',
|
||||
fillUserSettings({disabledFor: [], enabledFor: ['[2001:4860:4860::8844]'], enabledByDefault: false}),
|
||||
{isProtected: false, isInDarkList: true},
|
||||
)).toEqual(true);
|
||||
expect(isURLEnabled(
|
||||
'https://[2001:4860:4860::8844]',
|
||||
fillUserSettings({disabledFor: [], enabledFor: [], enabledByDefault: false}),
|
||||
{isProtected: false, isInDarkList: true},
|
||||
)).toEqual(false);
|
||||
|
||||
// Default URL matches everything
|
||||
expect(isURLMatched('http://example.com', '*')).toEqual(true);
|
||||
expect(isURLMatched('https://example.com', '*')).toEqual(true);
|
||||
expect(isURLMatched('file:///c/some_file.pdf', '*')).toEqual(true);
|
||||
expect(isURLMatched('chrome://settings', '*')).toEqual(true);
|
||||
expect(isURLMatched('chrome-extension://settings', '*')).toEqual(true);
|
||||
expect(isURLMatched('edge://settings', '*')).toEqual(true);
|
||||
expect(isURLMatched('brave://settings', '*')).toEqual(true);
|
||||
expect(isURLMatched('kiwi://settings', '*')).toEqual(true);
|
||||
expect(isURLMatched('about:blank', '*')).toEqual(true);
|
||||
expect(isURLMatched('about:preferences', '*')).toEqual(true);
|
||||
expect(isURLMatched('http://[::1]', '*')).toEqual(true);
|
||||
expect(isURLMatched('http://[::1]:80', '*')).toEqual(true);
|
||||
expect(isURLMatched('http://127.0.0.1', '*')).toEqual(true);
|
||||
expect(isURLMatched('http://127.0.0.1:80', '*')).toEqual(true);
|
||||
expect(isURLMatched('http://localhost', '*')).toEqual(true);
|
||||
|
||||
// No wildcard
|
||||
expect(isURLMatched('https://example.com/abc', 'example.com')).toEqual(true);
|
||||
expect(isURLMatched('https://www.example.com/abc', 'example.com')).toEqual(true);
|
||||
expect(isURLMatched('https://a.example.com/abc', 'example.com')).toEqual(false);
|
||||
expect(isURLMatched('https://example.com/abc', 'a.example.com')).toEqual(false);
|
||||
expect(isURLMatched('https://a.example.com/abc', 'b.example.com')).toEqual(false);
|
||||
|
||||
// Single wildcard with unbound non-extended left math
|
||||
expect(isURLMatched('https://example.com/abc', 'example.com/abc/*')).toEqual(false);
|
||||
expect(isURLMatched('https://example.com/abcd', 'example.com/abc/*')).toEqual(false);
|
||||
expect(isURLMatched('https://example.com/abc/def', 'example.com/*/def')).toEqual(true);
|
||||
expect(isURLMatched('https://example.com/abcd/ef', 'example.com/*/def')).toEqual(false);
|
||||
expect(isURLMatched('https://example.com/abc', 'example.com/*')).toEqual(true);
|
||||
expect(isURLMatched('https://example.com/abc', 'example.*/abc')).toEqual(true);
|
||||
expect(isURLMatched('https://example.com/abc', 'example.*')).toEqual(true);
|
||||
expect(isURLMatched('https://example.com/abc', 'example.*abc')).toEqual(false);
|
||||
|
||||
// Single wildcard with unbound extended left math
|
||||
expect(isURLMatched('https://a.example.com/abc', '*.example.com')).toEqual(true);
|
||||
expect(isURLMatched('https://a.example.com/abc', '*.example.com/abc/*')).toEqual(false);
|
||||
|
||||
// Multiple wildcards with unbound extended left math
|
||||
expect(isURLMatched('https://a.example.com/abc/def/ghi', 'a.example.com/*/def/*')).toEqual(true);
|
||||
expect(isURLMatched('https://a.example.com/abc/def/ghi', 'a.example.com/*/abc/*')).toEqual(false);
|
||||
|
||||
// Escapes
|
||||
expect(isURLMatched('https://example.com/*', '/example\\.com\\/\\*/')).toEqual(true);
|
||||
expect(isURLMatched('https://example.com/?q=*', '/example\\.com\\/\\?q\\=\\*/')).toEqual(true);
|
||||
expect(isURLMatched('https://example.com/abc?q=*', '/example\\.com\\/abc\\?q\\=\\*/')).toEqual(true);
|
||||
|
||||
// Some URLs can have unescaped [] in query
|
||||
expect(isURLMatched(
|
||||
'https://google.co.uk/order.php?bar=[foo]',
|
||||
'google.co.uk',
|
||||
)).toEqual(true);
|
||||
expect(isURLMatched(
|
||||
'https://[2001:4860:4860::8844]/order.php?bar=foo',
|
||||
'[2001:4860:4860::8844]',
|
||||
)).toEqual(true);
|
||||
expect(isURLMatched(
|
||||
'https://[2001:4860:4860::8844]/order.php?bar=[foo]',
|
||||
'[2001:4860:4860::8844]',
|
||||
)).toEqual(true);
|
||||
expect(isURLMatched(
|
||||
'https://google.co.uk/order.php?bar=[foo]',
|
||||
'[2001:4860:4860::8844]',
|
||||
)).toEqual(false);
|
||||
|
||||
// Temporary Dark Sites list fix
|
||||
expect(isURLEnabled(
|
||||
'https://darkreader.org/',
|
||||
fillUserSettings({disabledFor: [], enabledFor: ['darkreader.org'], enabledByDefault: true}),
|
||||
{isProtected: false, isInDarkList: true},
|
||||
)).toBe(true);
|
||||
expect(isURLEnabled(
|
||||
'https://darkreader.org/',
|
||||
fillUserSettings({disabledFor: [], enabledFor: [], enabledByDefault: true}),
|
||||
{isProtected: false, isInDarkList: true},
|
||||
)).toBe(false);
|
||||
expect(isURLEnabled(
|
||||
'https://google.com/',
|
||||
fillUserSettings({disabledFor: [], enabledFor: ['darkreader.org'], enabledByDefault: true}),
|
||||
{isProtected: false, isInDarkList: false},
|
||||
)).toBe(true);
|
||||
expect(isURLEnabled(
|
||||
'https://netflix.com',
|
||||
fillUserSettings({enableForPDF: true, disabledFor: [''], enabledFor: ['netflix.com'], enabledByDefault: false}),
|
||||
{isProtected: false, isInDarkList: true},
|
||||
)).toBe(true);
|
||||
expect(isURLEnabled(
|
||||
'https://netflix.com',
|
||||
fillUserSettings({enableForPDF: true, disabledFor: [''], enabledFor: ['netflix.com'], enabledByDefault: true}),
|
||||
{isProtected: false, isInDarkList: true},
|
||||
)).toBe(true);
|
||||
});
|
||||
|
||||
it('Get URL host or protocol', () => {
|
||||
expect(getURLHostOrProtocol('https://www.google.com')).toBe('www.google.com');
|
||||
expect(getURLHostOrProtocol('https://www.google.com/maps')).toBe('www.google.com');
|
||||
expect(getURLHostOrProtocol('http://localhost:8080')).toBe('localhost:8080');
|
||||
expect(getURLHostOrProtocol('about:blank')).toBe('about:');
|
||||
expect(getURLHostOrProtocol('http://user:pass@www.example.org')).toBe('www.example.org');
|
||||
expect(getURLHostOrProtocol('data:text/html,<html>Hello</html>')).toBe('data:');
|
||||
expect(getURLHostOrProtocol('file:///Users/index.html')).toBe('/Users/index.html');
|
||||
});
|
||||
|
||||
it('Absolute URL', () => {
|
||||
expect(getAbsoluteURL('https://www.google.com', 'image.jpg')).toBe('https://www.google.com/image.jpg');
|
||||
expect(getAbsoluteURL('https://www.google.com', '/image.jpg')).toBe('https://www.google.com/image.jpg');
|
||||
expect(getAbsoluteURL('https://www.google.com/path', '/image.jpg')).toBe('https://www.google.com/image.jpg');
|
||||
expect(getAbsoluteURL('//www.google.com', '/image.jpg')).toBe(`${location.protocol}//www.google.com/image.jpg`);
|
||||
expect(getAbsoluteURL('https://www.google.com', 'image.jpg?size=128')).toBe('https://www.google.com/image.jpg?size=128');
|
||||
expect(getAbsoluteURL('https://www.google.com/path', 'image.jpg')).toBe('https://www.google.com/image.jpg');
|
||||
expect(getAbsoluteURL('https://www.google.com/path/', 'image.jpg')).toBe('https://www.google.com/path/image.jpg');
|
||||
expect(getAbsoluteURL('https://www.google.com/long/path', '../image.jpg')).toBe('https://www.google.com/image.jpg');
|
||||
expect(getAbsoluteURL('https://www.google.com/long/path/', '../image.jpg')).toBe('https://www.google.com/long/image.jpg');
|
||||
expect(getAbsoluteURL('https://www.google.com/long/path/', '../another/image.jpg')).toBe('https://www.google.com/long/another/image.jpg');
|
||||
expect(getAbsoluteURL('https://www.google.com/path/page.html', 'image.jpg')).toBe('https://www.google.com/path/image.jpg');
|
||||
expect(getAbsoluteURL('https://www.google.com/path/page.html', '/image.jpg')).toBe('https://www.google.com/image.jpg');
|
||||
expect(getAbsoluteURL('https://www.google.com', '//www.google.com/path/image.jpg')).toBe(`${location.protocol}//www.google.com/path/image.jpg`);
|
||||
expect(getAbsoluteURL('https://www.google.com', 'https://www.google.com/path/image.jpg')).toBe('https://www.google.com/path/image.jpg');
|
||||
expect(getAbsoluteURL('https://www.google.com', 'https://www.google.com/path/../another/image.jpg')).toBe('https://www.google.com/another/image.jpg');
|
||||
expect(getAbsoluteURL('https://www.google.com/path/page.html', 'image.jpg')).toBe('https://www.google.com/path/image.jpg');
|
||||
expect(getAbsoluteURL('https://www.google.com/path/page.html', '../image.jpg')).toBe('https://www.google.com/image.jpg');
|
||||
expect(getAbsoluteURL('path/index.html', 'image.jpg')).toBe(`${location.origin}/path/image.jpg`);
|
||||
expect(getAbsoluteURL('path/index.html', '/image.jpg?size=128')).toBe(`${location.origin}/image.jpg?size=128`);
|
||||
});
|
||||
Reference in New Issue
Block a user