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(
'',
),
lightIcon: multiline(
'',
),
darkTransparentIcon: multiline(
'',
),
lightTransparentIcon: multiline(
'',
),
largeDarkImage: multiline(
'',
),
largeLightImage: multiline(
'',
),
};
async function urlToImage(url: string) {
return new Promise((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(
'',
'Dark icon
',
);
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(
'',
'Light icon
',
);
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(
'',
'Dark background
',
);
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(
'',
'Light background
',
);
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(
'',
'Dark icon
',
);
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(
'',
'Weird color Power!
',
);
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(
'',
'Weird color Power!
',
);
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(
'',
'Weird color Power!
',
);
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(
'',
'Weird color Power!
',
);
createOrUpdateDynamicTheme(theme, null, false);
await waitForEvent('__darkreader__test__asyncQueueComplete');
expect(getComputedStyle(container.querySelector('h1')!).backgroundImage).toMatch(/^url\(""\), url\(""\), url\("blob:.*"\)$/);
});
});