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( '

Link override!

', ); 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( '

Cross-origin link override

', ); 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( '

Cross-origin import link override

', ); 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( '

Cross-origin import link override

', ); 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( '

Loaded cross-origin link override

', ); 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( '

Link disabled!

', ); 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( '', '

Link loading with non-href attribute!

', ); 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( '

Cross-origin import link override

', ); 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( '

Cross-origin import link override

', ); 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)'); }); });