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