chore: 整理 third-party Dark Reader 目录并移除本地说明文件跟踪
This commit is contained in:
+171
@@ -0,0 +1,171 @@
|
||||
// @ts-check
|
||||
import path from 'node:path';
|
||||
|
||||
import {writeFile} from '../../tasks/utils.js';
|
||||
|
||||
/** @typedef {{text: string; covered: boolean}} CodePart */
|
||||
|
||||
/**
|
||||
* @param {string} code
|
||||
* @param {{start: number; end: number}[]} ranges
|
||||
* @returns {CodePart[]}
|
||||
*/
|
||||
function splitCode(code, ranges) {
|
||||
/** @type {CodePart[]} */
|
||||
const parts = [];
|
||||
|
||||
if (ranges.length === 0) {
|
||||
parts.push({text: code, covered: false});
|
||||
return;
|
||||
}
|
||||
|
||||
if (ranges[0].start > 0) {
|
||||
parts.push({text: code.substring(0, ranges[0].start), covered: false});
|
||||
}
|
||||
|
||||
const lastRange = ranges[ranges.length - 1];
|
||||
|
||||
for (let i = 0; i < ranges.length; i++) {
|
||||
const range = ranges[i];
|
||||
parts.push({text: code.substring(range.start, range.end), covered: true});
|
||||
if (range !== lastRange) {
|
||||
const nextRange = ranges[i + 1];
|
||||
parts.push({text: code.substring(range.end, nextRange.start), covered: false});
|
||||
}
|
||||
}
|
||||
|
||||
if (lastRange.end < code.length) {
|
||||
parts.push({text: code.substring(lastRange.end), covered: false});
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
function red(/** @type {string} */text) {
|
||||
return `\x1b[31m${text}\x1b[0m`;
|
||||
}
|
||||
|
||||
function green(/** @type {string} */text) {
|
||||
return `\x1b[32m${text}\x1b[0m`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} code
|
||||
* @param {{start: number; end: number}[]} ranges
|
||||
*/
|
||||
export function logCoverage(code, ranges) {
|
||||
code = code.substring(0, code.indexOf('//# sourceMappingURL='));
|
||||
const parts = splitCode(code, ranges);
|
||||
const message = parts
|
||||
.map(({text, covered}) => (covered ? green : red)(text))
|
||||
.join('');
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
function escapeHTML(/** @type {string} */html) {
|
||||
return html
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/** @typedef {{name: string; length: number; coverage: number; parts: {text: string; covered: boolean}[]}} FileCoverageInfo */
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {string} code
|
||||
* @param {{start: number; end: number}[]} ranges
|
||||
* @returns {FileCoverageInfo}
|
||||
*/
|
||||
function getCoverageInfo(name, code, ranges) {
|
||||
code = code.substring(0, code.indexOf('//# sourceMappingURL='));
|
||||
const parts = splitCode(code, ranges);
|
||||
const length = code.length;
|
||||
const coverage = parts.filter((p) => p.covered).reduce((sum, p) => sum + p.text.length, 0) / length;
|
||||
return {name, length, coverage, parts};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} dir
|
||||
* @param {FileCoverageInfo} info
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function generateHTMLCoverageReport(dir, info) {
|
||||
const {name, coverage, parts} = info;
|
||||
|
||||
/** @type {string[]} */
|
||||
const lines = [];
|
||||
lines.push('<!DOCTYPE html>');
|
||||
lines.push('<html>');
|
||||
lines.push('<head>');
|
||||
const title = `Coverage report: ${name}`;
|
||||
lines.push(` <title>${title}</title>`);
|
||||
lines.push(' <style>');
|
||||
lines.push(' body { background: #111; color: #ccc; }');
|
||||
lines.push(' code { background: #222; display: inline-block; white-space: pre-wrap; }');
|
||||
lines.push(' .uncovered { background: #934; color: white; }');
|
||||
lines.push(' </style>');
|
||||
lines.push('</head>');
|
||||
lines.push('<body>');
|
||||
lines.push(` <h1>${title}</h1>`);
|
||||
lines.push(` <h3>Covered ${(coverage * 100).toFixed(0)}%</h3>`);
|
||||
lines.push(` <code>${parts.map((p) => `<span${p.covered ? '' : ' class="uncovered"'}>${escapeHTML(p.text)}</span>`).join('')}</code>`);
|
||||
lines.push('</body>');
|
||||
lines.push('</html>');
|
||||
|
||||
await writeFile(path.join(dir, `${name}.html`), lines.join('\n'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} dir
|
||||
* @param {FileCoverageInfo[]} info
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function generateIndexHTMLCoveragePage(dir, info) {
|
||||
/** @type {string[]} */
|
||||
const lines = [];
|
||||
lines.push('<!DOCTYPE html>');
|
||||
lines.push('<html>');
|
||||
lines.push('<head>');
|
||||
lines.push(` <title>Code coverage reports</title>`);
|
||||
lines.push(' <style>');
|
||||
lines.push(' body { background: #111; color: #ccc; }');
|
||||
lines.push(' a { color: #7ae; }');
|
||||
lines.push(' </style>');
|
||||
lines.push('</head>');
|
||||
lines.push('<body>');
|
||||
lines.push(` <h1>Code coverage reports</h1>`);
|
||||
const totalCoverage = info.reduce((sum, i) => sum + i.coverage * i.length, 0);
|
||||
const totalLength = info.reduce((sum, i) => sum + i.length, 0);
|
||||
lines.push(` <h3>Total coverage ${(totalCoverage / totalLength * 100).toFixed(0)}%</h3>`);
|
||||
lines.push(' <table>');
|
||||
info.forEach((i) => {
|
||||
lines.push(' <tr>');
|
||||
lines.push(` <td>${(i.coverage * 100).toFixed(0)}%</td>`);
|
||||
lines.push(` <td><a href="${i.name}.html">${i.name}</a></td>`);
|
||||
lines.push(' </tr>');
|
||||
});
|
||||
lines.push(' </table>');
|
||||
lines.push('</body>');
|
||||
lines.push('</html>');
|
||||
|
||||
await writeFile(path.join(dir, 'index.html'), lines.join('\n'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} dir
|
||||
* @param {import('puppeteer-core').CoverageEntry[]} coverage
|
||||
*/
|
||||
export async function generateHTMLCoverageReports(dir, coverage) {
|
||||
const info = coverage
|
||||
.filter(({url}) => url.startsWith('chrome-extension://'))
|
||||
.map(({url, text, ranges}) => {
|
||||
const name = url.replace(/^chrome-extension:\/\/.*?\//, '');
|
||||
return getCoverageInfo(name, text, ranges);
|
||||
});
|
||||
|
||||
await generateIndexHTMLCoveragePage(dir, info);
|
||||
await Promise.all(info.map((i) => generateHTMLCoverageReport(dir, i)));
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import {multiline} from '../../support/test-utils';
|
||||
import type {StyleExpectations} from '../globals';
|
||||
|
||||
async function loadBasicPage() {
|
||||
await loadTestPage({
|
||||
'/': multiline(
|
||||
'<!DOCTYPE html>',
|
||||
'<html>',
|
||||
'<head>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
' <span style="color: red;">Inline style override</span>',
|
||||
'</body>',
|
||||
'</html>',
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
async function expectStyles(styles: StyleExpectations) {
|
||||
await expectPageStyles(expect, styles);
|
||||
}
|
||||
|
||||
describe('Inline style override', () => {
|
||||
it('should override inline style', async () => {
|
||||
await loadBasicPage();
|
||||
|
||||
await expectStyles(['span', 'color', 'rgb(255, 26, 26)']);
|
||||
});
|
||||
|
||||
it('should watch for inline style change', async () => {
|
||||
await loadBasicPage();
|
||||
|
||||
await expectStyles(['span', 'color', 'rgb(255, 26, 26)']);
|
||||
|
||||
await expect(pageUtils.evaluateScript(async () => {
|
||||
const span = document.querySelector('span')!;
|
||||
span.style.color = 'green';
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
return getComputedStyle(span).color;
|
||||
})).resolves.toBe('rgb(114, 255, 114)');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
import {multiline} from '../../support/test-utils';
|
||||
import type {StyleExpectations} from '../globals';
|
||||
|
||||
async function expectStyles(styles: StyleExpectations) {
|
||||
await expectPageStyles(expect, styles);
|
||||
}
|
||||
|
||||
describe('Link override', () => {
|
||||
it('should override link style', async () => {
|
||||
await loadTestPage({
|
||||
'/': multiline(
|
||||
'<!DOCTYPE html>',
|
||||
'<html>',
|
||||
'<head>',
|
||||
' <link rel="stylesheet" href="style.css"/>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
' <h1>Link style <strong>override</strong>!</h1>',
|
||||
'</body>',
|
||||
'</html>',
|
||||
),
|
||||
'/style.css': multiline(
|
||||
'body { background: gray; }',
|
||||
'h1 strong { color: red; }',
|
||||
),
|
||||
});
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['body', 'background-color', 'rgb(96, 104, 108)'],
|
||||
['body', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1 strong', 'color', 'rgb(255, 26, 26)'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should override CORS style', async () => {
|
||||
await loadTestPage({
|
||||
'/': multiline(
|
||||
'<!DOCTYPE html>',
|
||||
'<html>',
|
||||
'<head>',
|
||||
` <link rel="stylesheet" href="${corsURL}/style.css"/>`,
|
||||
'</head>',
|
||||
'<body>',
|
||||
' <h1>CORS style <strong>override</strong>!</h1>',
|
||||
'</body>',
|
||||
'</html>',
|
||||
),
|
||||
cors: {
|
||||
'/style.css': multiline(
|
||||
'body { background: gray; }',
|
||||
'h1 strong { color: red; }',
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['body', 'background-color', 'rgb(96, 104, 108)'],
|
||||
['body', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1 strong', 'color', 'rgb(255, 26, 26)'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should wait till style is loading', async () => {
|
||||
const styleResolvers: Array<() => any> = [];
|
||||
let didStyleResolve = false;
|
||||
|
||||
await loadTestPage({
|
||||
'/': multiline(
|
||||
'<!DOCTYPE html>',
|
||||
'<html>',
|
||||
'<head>',
|
||||
' <link rel="stylesheet" href="style.css"/>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
' <h1>Link loading <strong>delay</strong></h1>',
|
||||
'</body>',
|
||||
'</html>',
|
||||
),
|
||||
'/style.css': async (_, res) => {
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'text/css');
|
||||
|
||||
// For some reason Firefox 151 performs two requests.
|
||||
// Only the second has effect on page's look.
|
||||
if (!didStyleResolve) {
|
||||
await new Promise<void>((resolve) => styleResolvers.push(resolve));
|
||||
}
|
||||
|
||||
res.end(multiline(
|
||||
'body { background: gray; }',
|
||||
'h1 strong { color: red; }',
|
||||
), 'utf8');
|
||||
},
|
||||
}, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['body', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['body', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1 strong', 'color', 'rgb(232, 230, 227)'],
|
||||
]);
|
||||
|
||||
styleResolvers.forEach((resolve) => resolve());
|
||||
didStyleResolve = true;
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['body', 'background-color', 'rgb(96, 104, 108)'],
|
||||
['body', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1 strong', 'color', 'rgb(255, 26, 26)'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,255 @@
|
||||
import {multiline} from '../../support/test-utils';
|
||||
import type {StyleExpectations} from '../globals';
|
||||
|
||||
async function expectStyles(styles: StyleExpectations) {
|
||||
await expectPageStyles(expect, styles);
|
||||
}
|
||||
|
||||
describe('Style override', () => {
|
||||
it('should override user agent style', async () => {
|
||||
await loadTestPage({
|
||||
'/': multiline(
|
||||
'<!DOCTYPE html>',
|
||||
'<html>',
|
||||
'<head>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
' Text',
|
||||
' <a href="#">Link</a>',
|
||||
'</body>',
|
||||
'</html>',
|
||||
),
|
||||
});
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['document', 'color', 'rgb(232, 230, 227)'],
|
||||
['body', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['body', 'color', 'rgb(232, 230, 227)'],
|
||||
['a', 'color', 'rgb(51, 145, 255)'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should override static style', async () => {
|
||||
await loadTestPage({
|
||||
'/': multiline(
|
||||
'<!DOCTYPE html>',
|
||||
'<html>',
|
||||
'<head>',
|
||||
' <style>',
|
||||
' body { background: gray; }',
|
||||
' h1 strong { color: red; }',
|
||||
' </style>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
' <h1>Style <strong>override</strong>!</h1>',
|
||||
'</body>',
|
||||
'</html>',
|
||||
),
|
||||
});
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['body', 'background-color', 'rgb(96, 104, 108)'],
|
||||
['body', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1 strong', 'color', 'rgb(255, 26, 26)'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should restore override', async () => {
|
||||
await loadTestPage({
|
||||
'/': multiline(
|
||||
'<!DOCTYPE html>',
|
||||
'<html>',
|
||||
'<head>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
' <h1>Style <strong>override</strong>!</h1>',
|
||||
'</body>',
|
||||
'</html>',
|
||||
),
|
||||
});
|
||||
|
||||
await pageUtils.evaluateScript(() => {
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.classList.add('testcase-style');
|
||||
document.head.append(styleElement);
|
||||
styleElement.sheet!.insertRule('h1 { color: gray }');
|
||||
styleElement.sheet!.insertRule('strong { color: red }');
|
||||
});
|
||||
|
||||
await expectStyles([
|
||||
['h1', 'color', 'rgb(152, 143, 129)'],
|
||||
['h1 strong', 'color', 'rgb(255, 26, 26)'],
|
||||
]);
|
||||
|
||||
await expect(pageUtils.evaluateScript(async () => {
|
||||
const style = document.querySelector('.testcase-style')!;
|
||||
style.nextSibling!.remove();
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
return (style.nextSibling as HTMLStyleElement).classList.contains('darkreader--sync');
|
||||
})).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('should move override', async () => {
|
||||
await loadTestPage({
|
||||
'/': multiline(
|
||||
'<!DOCTYPE html>',
|
||||
'<html>',
|
||||
'<head>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
' <h1>Some test foor...... <strong>Moving styles</strong>!</h1>',
|
||||
'</body>',
|
||||
'</html>',
|
||||
),
|
||||
});
|
||||
|
||||
await pageUtils.evaluateScript(() => {
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.classList.add('testcase-style');
|
||||
document.head.append(styleElement);
|
||||
styleElement.sheet!.insertRule('h1 { color: gray } ');
|
||||
styleElement.sheet!.insertRule('strong { color: red } ');
|
||||
});
|
||||
|
||||
await expect(pageUtils.evaluateScript(async () => {
|
||||
const style = document.querySelector('.testcase-style')!;
|
||||
document.body.append(style);
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
return (style.nextSibling as HTMLStyleElement).classList.contains('darkreader--sync');
|
||||
})).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('should remove override', async () => {
|
||||
await loadTestPage({
|
||||
'/': multiline(
|
||||
'<!DOCTYPE html>',
|
||||
'<html>',
|
||||
'<head>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
' <h1>Some test foor...... <strong>Oh uhm removing styles :(</strong>!</h1>',
|
||||
'</body>',
|
||||
'</html>',
|
||||
),
|
||||
});
|
||||
|
||||
await pageUtils.evaluateScript(() => {
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.classList.add('testcase-style');
|
||||
document.head.append(styleElement);
|
||||
styleElement.sheet!.insertRule('h1 { color: gray }');
|
||||
styleElement.sheet!.insertRule('strong { color: red }');
|
||||
});
|
||||
|
||||
await expect(pageUtils.evaluateScript(async () => {
|
||||
const style = document.querySelector('.testcase-style')!;
|
||||
const sibling = style.nextSibling!;
|
||||
style.remove();
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
return sibling.isConnected && !((sibling as HTMLStyleElement).classList.contains('darkreader--sync'));
|
||||
})).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('should react to updated style', async () => {
|
||||
if (product === 'firefox') {
|
||||
expect(true);
|
||||
return;
|
||||
}
|
||||
await loadTestPage({
|
||||
'/': multiline(
|
||||
'<!DOCTYPE html>',
|
||||
'<html>',
|
||||
'<head>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
' <h1>Some test foor...... <strong>Oh uhm a pink background</strong></h1>',
|
||||
'</body>',
|
||||
'</html>',
|
||||
),
|
||||
});
|
||||
|
||||
await pageUtils.evaluateScript(() => {
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.classList.add('testcase-style');
|
||||
document.head.append(styleElement);
|
||||
styleElement.sheet!.insertRule('h1 { color: gray }');
|
||||
styleElement.sheet!.insertRule('strong { color: red }');
|
||||
});
|
||||
|
||||
await expect(pageUtils.evaluateScript(async () => {
|
||||
const style = document.querySelector('.testcase-style')!;
|
||||
(style as HTMLStyleElement).sheet!.insertRule('html { background-color: pink }');
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
return (style.nextSibling as HTMLStyleElement).sheet!.cssRules[0].cssText;
|
||||
})).resolves.toBe('html { background-color: var(--darkreader-background-ffc0cb, #590010); }');
|
||||
});
|
||||
|
||||
it('should react to a new style', async () => {
|
||||
await loadTestPage({
|
||||
'/': multiline(
|
||||
'<!DOCTYPE html>',
|
||||
'<html>',
|
||||
'<head>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
' <h1>Some test foor...... <strong>Oh uhm what?</strong>!</h1>',
|
||||
'</body>',
|
||||
'</html>',
|
||||
),
|
||||
});
|
||||
|
||||
await expect(pageUtils.evaluateScript(async () => {
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.classList.add('testcase-style');
|
||||
document.head.append(styleElement);
|
||||
styleElement.sheet!.insertRule('h1 { color: pink }');
|
||||
styleElement.sheet!.insertRule('strong { color: orange }');
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
return (styleElement.nextSibling as HTMLStyleElement).sheet!.cssRules.length === 2 && (styleElement.nextSibling as HTMLStyleElement).classList.contains('darkreader--sync');
|
||||
})).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('should handle defined custom elements', async () => {
|
||||
if (product === 'firefox') {
|
||||
return;
|
||||
}
|
||||
await loadTestPage({
|
||||
'/': multiline(
|
||||
'<!DOCTYPE html>',
|
||||
'<html>',
|
||||
'<head>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
'<custom-element>',
|
||||
'</custom-element>',
|
||||
'</body>',
|
||||
'</html>',
|
||||
),
|
||||
});
|
||||
|
||||
pageUtils.evaluateScript(async () => {
|
||||
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);
|
||||
});
|
||||
|
||||
await expectStyles([
|
||||
[['custom-element', 'p'], 'color', 'rgb(255, 160, 177)'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import {multiline} from '../../support/test-utils';
|
||||
import type {StyleExpectations} from '../globals';
|
||||
|
||||
async function loadBasicPage() {
|
||||
await loadTestPage({
|
||||
'/': multiline(
|
||||
'<!DOCTYPE html>',
|
||||
'<html>',
|
||||
'<head>',
|
||||
' <style>',
|
||||
' h1 { color: red; }',
|
||||
' </style>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
' <h1>E2E test page</h1>',
|
||||
' <p>Text</p>',
|
||||
' <a href="#">Link</a>',
|
||||
'</body>',
|
||||
'</html>',
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
async function expectStyles(styles: StyleExpectations) {
|
||||
await expectPageStyles(expect, styles);
|
||||
}
|
||||
|
||||
describe('Modifying config via Developer tools', () => {
|
||||
it('Modifying config', async () => {
|
||||
await loadBasicPage();
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['document', 'color', 'rgb(232, 230, 227)'],
|
||||
['body', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['body', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1', 'color', 'rgb(255, 26, 26)'],
|
||||
['a', 'color', 'rgb(51, 145, 255)'],
|
||||
]);
|
||||
|
||||
await devtoolsUtils.paste([
|
||||
'*',
|
||||
'',
|
||||
'CSS',
|
||||
'h1 {',
|
||||
' background: black;',
|
||||
' color: white;',
|
||||
'}',
|
||||
'',
|
||||
'============================',
|
||||
'',
|
||||
'nonexistent.com',
|
||||
'',
|
||||
'CSS',
|
||||
'h1 {',
|
||||
' color: red;',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'));
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['document', 'color', 'rgb(232, 230, 227)'],
|
||||
['body', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['body', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1', 'background-color', 'rgb(0, 0, 0)'],
|
||||
['h1', 'color', 'rgb(255, 255, 255)'],
|
||||
['a', 'color', 'rgb(51, 145, 255)'],
|
||||
]);
|
||||
|
||||
await devtoolsUtils.reset();
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['document', 'color', 'rgb(232, 230, 227)'],
|
||||
['body', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['body', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1', 'color', 'rgb(255, 26, 26)'],
|
||||
['a', 'color', 'rgb(51, 145, 255)'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import {multiline} from '../../support/test-utils';
|
||||
|
||||
async function loadBasicPage(header = 'E2E test page') {
|
||||
await loadTestPage({
|
||||
'/': multiline(
|
||||
'<!DOCTYPE html>',
|
||||
'<html>',
|
||||
'<head>',
|
||||
' <style>',
|
||||
' h1 { color: red; }',
|
||||
' </style>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
` <h1>${header}</h1>`,
|
||||
' <p>Text</p>',
|
||||
' <a href="#">Link</a>',
|
||||
'</body>',
|
||||
'</html>',
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
describe('Test environment', () => {
|
||||
it('should turn On/Off', async () => {
|
||||
await loadBasicPage();
|
||||
|
||||
const initialColorScheme = await getColorScheme();
|
||||
const overrideColorScheme = initialColorScheme === 'dark' ? 'light' : 'dark';
|
||||
|
||||
expect(initialColorScheme === 'light' || initialColorScheme === 'dark');
|
||||
if (product === 'firefox') {
|
||||
await expect(backgroundUtils.getColorScheme()).resolves.toBe(initialColorScheme);
|
||||
}
|
||||
|
||||
await emulateColorScheme(overrideColorScheme);
|
||||
await expect(getColorScheme()).resolves.toBe(overrideColorScheme);
|
||||
if (product === 'firefox') {
|
||||
await expect(backgroundUtils.getColorScheme()).resolves.toBe(overrideColorScheme);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import {timeout} from '../../support/test-utils';
|
||||
|
||||
describe('Export settings', () => {
|
||||
it('Should download file', async () => {
|
||||
await timeout(1000);
|
||||
const p = new Promise<{ok: boolean}>((resolve) => backgroundUtils.onDownload(resolve));
|
||||
await popupUtils.saveFile('example', 'content');
|
||||
expect((await p).ok).toBe(true);
|
||||
if (product === 'firefox') {
|
||||
await timeout(1000);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,241 @@
|
||||
import {multiline} from '../../support/test-utils';
|
||||
import type {StyleExpectations} from '../globals';
|
||||
|
||||
async function loadBasicPage() {
|
||||
await loadTestPage({
|
||||
'/': multiline(
|
||||
'<!DOCTYPE html>',
|
||||
'<html>',
|
||||
'<head>',
|
||||
' <style>',
|
||||
' h1 { color: red; }',
|
||||
' </style>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
' <h1>E2E test page</h1>',
|
||||
' <p>Text</p>',
|
||||
'</body>',
|
||||
'</html>',
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
async function expectStyles(styles: StyleExpectations) {
|
||||
await expectPageStyles(expect, styles);
|
||||
}
|
||||
|
||||
describe('Correct fixes are chosen', () => {
|
||||
it('If no matching URL found, returns only default fix', async () => {
|
||||
await loadBasicPage();
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['document', 'color', 'rgb(232, 230, 227)'],
|
||||
['body', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['body', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1', 'color', 'rgb(255, 26, 26)'],
|
||||
]);
|
||||
|
||||
await devtoolsUtils.paste(multiline(
|
||||
'*',
|
||||
'',
|
||||
'CSS',
|
||||
'body {',
|
||||
' background: navy;',
|
||||
' color: white;',
|
||||
'}',
|
||||
'h1 {',
|
||||
' color: orange;',
|
||||
'}',
|
||||
'',
|
||||
'============================',
|
||||
'',
|
||||
'nonexistent.com',
|
||||
'',
|
||||
'CSS',
|
||||
'body {',
|
||||
' bachground: green;',
|
||||
'}',
|
||||
'',
|
||||
'============================',
|
||||
'',
|
||||
'other.net',
|
||||
'',
|
||||
'CSS',
|
||||
'body {',
|
||||
' bachground: blue;',
|
||||
'}',
|
||||
));
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['document', 'color', 'rgb(232, 230, 227)'],
|
||||
['body', 'background-color', 'rgb(0, 0, 128)'],
|
||||
['body', 'color', 'rgb(255, 255, 255)'],
|
||||
['h1', 'color', 'rgb(255, 165, 0)'],
|
||||
]);
|
||||
|
||||
await devtoolsUtils.reset();
|
||||
});
|
||||
|
||||
it('If multiple matching URL patterns found, select the most specific one', async () => {
|
||||
await loadBasicPage();
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['document', 'color', 'rgb(232, 230, 227)'],
|
||||
['body', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['body', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1', 'color', 'rgb(255, 26, 26)'],
|
||||
]);
|
||||
|
||||
await devtoolsUtils.paste(multiline(
|
||||
'*',
|
||||
'',
|
||||
'CSS',
|
||||
'body {',
|
||||
' background: navy;',
|
||||
' color: white;',
|
||||
'}',
|
||||
'h1 {',
|
||||
' color: orange;',
|
||||
'}',
|
||||
'',
|
||||
'============================',
|
||||
'',
|
||||
'localhost:8891/',
|
||||
'',
|
||||
'CSS',
|
||||
'body {',
|
||||
' bachground: blue;',
|
||||
'}',
|
||||
'',
|
||||
'============================',
|
||||
'',
|
||||
'localhost:8891',
|
||||
'',
|
||||
'CSS',
|
||||
'body {',
|
||||
' bachground: green;',
|
||||
'}',
|
||||
));
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['document', 'color', 'rgb(232, 230, 227)'],
|
||||
['body', 'background-color', 'rgb(0, 0, 128)'],
|
||||
['body', 'color', 'rgb(255, 255, 255)'],
|
||||
['h1', 'color', 'rgb(255, 165, 0)'],
|
||||
]);
|
||||
|
||||
await devtoolsUtils.reset();
|
||||
});
|
||||
|
||||
it('BUG COMPATIBILITY: If multiple matching URL patterns found, the most specific fix is determined by the length of first pattern', async () => {
|
||||
await loadBasicPage();
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['document', 'color', 'rgb(232, 230, 227)'],
|
||||
['body', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['body', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1', 'color', 'rgb(255, 26, 26)'],
|
||||
]);
|
||||
|
||||
await devtoolsUtils.paste(multiline(
|
||||
'*',
|
||||
'',
|
||||
'CSS',
|
||||
'body {',
|
||||
' background: navy;',
|
||||
' color: white;',
|
||||
'}',
|
||||
'h1 {',
|
||||
' color: orange;',
|
||||
'}',
|
||||
'',
|
||||
'============================',
|
||||
'',
|
||||
'example.com/loooooooong',
|
||||
'localhost:8891',
|
||||
'',
|
||||
'CSS',
|
||||
'body {',
|
||||
' bachground: blue;',
|
||||
'}',
|
||||
'',
|
||||
'============================',
|
||||
'',
|
||||
'example.com',
|
||||
'localhost:8891/',
|
||||
'',
|
||||
'CSS',
|
||||
'body {',
|
||||
' bachground: green;',
|
||||
'}',
|
||||
));
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['document', 'color', 'rgb(232, 230, 227)'],
|
||||
['body', 'background-color', 'rgb(0, 0, 128)'],
|
||||
['body', 'color', 'rgb(255, 255, 255)'],
|
||||
['h1', 'color', 'rgb(255, 165, 0)'],
|
||||
]);
|
||||
|
||||
await devtoolsUtils.reset();
|
||||
});
|
||||
|
||||
it('BUG COMPATIBILITY: If multiple matching URL patterns of the same length are found, select the first one', async () => {
|
||||
await loadBasicPage();
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['document', 'color', 'rgb(232, 230, 227)'],
|
||||
['body', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['body', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1', 'color', 'rgb(255, 26, 26)'],
|
||||
]);
|
||||
|
||||
await devtoolsUtils.paste(multiline(
|
||||
'*',
|
||||
'',
|
||||
'CSS',
|
||||
'body {',
|
||||
' background: navy;',
|
||||
' color: white;',
|
||||
'}',
|
||||
'h1 {',
|
||||
' color: orange;',
|
||||
'}',
|
||||
'',
|
||||
'============================',
|
||||
'',
|
||||
'localhost:8891',
|
||||
'',
|
||||
'CSS',
|
||||
'body {',
|
||||
' bachground: blue;',
|
||||
'}',
|
||||
'',
|
||||
'============================',
|
||||
'',
|
||||
'localhost:8891',
|
||||
'',
|
||||
'CSS',
|
||||
'body {',
|
||||
' bachground: green;',
|
||||
'}',
|
||||
));
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['document', 'color', 'rgb(232, 230, 227)'],
|
||||
['body', 'background-color', 'rgb(0, 0, 128)'],
|
||||
['body', 'color', 'rgb(255, 255, 255)'],
|
||||
['h1', 'color', 'rgb(255, 165, 0)'],
|
||||
]);
|
||||
|
||||
await devtoolsUtils.reset();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
describe('News', () => {
|
||||
const newsSelector = 'div.news.news--expanded';
|
||||
|
||||
it('should display news', async () => {
|
||||
await backgroundUtils.setNews([{
|
||||
id: 'some',
|
||||
date: '10',
|
||||
url: '/',
|
||||
headline: 'Test news',
|
||||
}]);
|
||||
|
||||
popupUtils.exists(newsSelector);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,180 @@
|
||||
import {multiline} from '../../support/test-utils';
|
||||
import type {StyleExpectations} from '../globals';
|
||||
|
||||
async function expectStyles(styles: StyleExpectations) {
|
||||
await expectPageStyles(expect, styles);
|
||||
}
|
||||
|
||||
async function loadBasicPage() {
|
||||
await loadTestPage({
|
||||
'/': multiline(
|
||||
'<!DOCTYPE html>',
|
||||
'<html>',
|
||||
'<head>',
|
||||
' <style>',
|
||||
' h1 { color: white; }',
|
||||
' </style>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
' <h1>White title - should stay</h1>',
|
||||
' <h2>White subtitle - should change</h2>',
|
||||
' <iframe id="red" src="/red.html"></iframe>',
|
||||
' <iframe id="blue" src="/blue.html"></iframe>',
|
||||
'</body>',
|
||||
'</html>',
|
||||
),
|
||||
'/red.html': multiline(
|
||||
'<!DOCTYPE html>',
|
||||
'<html>',
|
||||
'<head>',
|
||||
' <style>',
|
||||
' h1 { color: red; }',
|
||||
' h2 { color: white; }',
|
||||
' </style>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
' <h1>Red title - should change</h1>',
|
||||
' <h2>White subtitle - should stay</h2>',
|
||||
'</body>',
|
||||
'</html>',
|
||||
),
|
||||
'/blue.html': multiline(
|
||||
'<!DOCTYPE html>',
|
||||
'<html>',
|
||||
'<head>',
|
||||
' <style>',
|
||||
' h1 { color: blue; }',
|
||||
' h2 { color: white; }',
|
||||
' </style>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
' <h1>Blue title - should change</h1>',
|
||||
' <h2>White subtitle - should stay</h2>',
|
||||
'</body>',
|
||||
'</html>',
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
describe('Different paths in URL patterns', () => {
|
||||
it('Different paths upon initial load', async () => {
|
||||
await Promise.all([
|
||||
awaitForEvent('ready-/red.html'),
|
||||
awaitForEvent('ready-/blue.html'),
|
||||
loadBasicPage(),
|
||||
]);
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['document', 'color', 'rgb(232, 230, 227)'],
|
||||
['body', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['body', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1', 'color', 'rgb(232, 230, 227)'],
|
||||
|
||||
[['iframe#red', 'document'], 'background-color', 'rgba(0, 0, 0, 0)'],
|
||||
[['iframe#red', 'document'], 'color', 'rgb(232, 230, 227)'],
|
||||
[['iframe#red', 'body'], 'background-color', 'rgba(0, 0, 0, 0)'],
|
||||
[['iframe#red', 'body'], 'color', 'rgb(232, 230, 227)'],
|
||||
[['iframe#red', 'h1'], 'color', 'rgb(255, 26, 26)'],
|
||||
|
||||
[['iframe#blue', 'document'], 'background-color', 'rgba(0, 0, 0, 0)'],
|
||||
[['iframe#blue', 'document'], 'color', 'rgb(232, 230, 227)'],
|
||||
[['iframe#blue', 'body'], 'background-color', 'rgba(0, 0, 0, 0)'],
|
||||
[['iframe#blue', 'body'], 'color', 'rgb(232, 230, 227)'],
|
||||
[['iframe#blue', 'h1'], 'color', 'rgb(51, 125, 255)'],
|
||||
]);
|
||||
|
||||
await devtoolsUtils.paste([
|
||||
'*',
|
||||
'',
|
||||
'CSS',
|
||||
'body {',
|
||||
' background: black',
|
||||
' color: white',
|
||||
'}',
|
||||
'',
|
||||
'============================',
|
||||
'',
|
||||
'*/red.html',
|
||||
'',
|
||||
'CSS',
|
||||
'body {',
|
||||
' background: black',
|
||||
'}',
|
||||
'h1 {',
|
||||
' color: indigo;',
|
||||
'}',
|
||||
'',
|
||||
'============================',
|
||||
'',
|
||||
'*/blue.html',
|
||||
'',
|
||||
'CSS',
|
||||
'body {',
|
||||
' background: black',
|
||||
'}',
|
||||
'h1 {',
|
||||
' color: navy;',
|
||||
'}',
|
||||
'',
|
||||
'============================',
|
||||
'',
|
||||
'nonexistent.com',
|
||||
'',
|
||||
'CSS',
|
||||
'body {',
|
||||
' background: purple',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'));
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['document', 'color', 'rgb(232, 230, 227)'],
|
||||
['body', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['body', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1', 'color', 'rgb(232, 230, 227)'],
|
||||
['h2', 'color', 'rgb(232, 230, 227)'],
|
||||
|
||||
[['iframe#red', 'document'], 'background-color', 'rgba(0, 0, 0, 0)'],
|
||||
[['iframe#red', 'document'], 'color', 'rgb(232, 230, 227)'],
|
||||
[['iframe#red', 'body'], 'background-color', 'rgb(0, 0, 0)'],
|
||||
[['iframe#red', 'body'], 'color', 'rgb(232, 230, 227)'],
|
||||
[['iframe#red', 'h1'], 'color', 'rgb(75, 0, 130)'],
|
||||
|
||||
[['iframe#blue', 'document'], 'background-color', 'rgba(0, 0, 0, 0)'],
|
||||
[['iframe#blue', 'document'], 'color', 'rgb(232, 230, 227)'],
|
||||
[['iframe#blue', 'body'], 'background-color', 'rgb(0, 0, 0)'],
|
||||
[['iframe#blue', 'body'], 'color', 'rgb(232, 230, 227)'],
|
||||
[['iframe#blue', 'h1'], 'color', 'rgb(0, 0, 128)'],
|
||||
]);
|
||||
|
||||
const redPromise = awaitForEvent('darkreader-dynamic-theme-ready-/red.html');
|
||||
const bluePromise = awaitForEvent('darkreader-dynamic-theme-ready-/blue.html');
|
||||
await Promise.all([
|
||||
redPromise,
|
||||
bluePromise,
|
||||
devtoolsUtils.reset(),
|
||||
]);
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['document', 'color', 'rgb(232, 230, 227)'],
|
||||
['body', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['body', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1', 'color', 'rgb(232, 230, 227)'],
|
||||
|
||||
[['iframe#red', 'document'], 'background-color', 'rgba(0, 0, 0, 0)'],
|
||||
[['iframe#red', 'document'], 'color', 'rgb(232, 230, 227)'],
|
||||
[['iframe#red', 'body'], 'background-color', 'rgba(0, 0, 0, 0)'],
|
||||
[['iframe#red', 'body'], 'color', 'rgb(232, 230, 227)'],
|
||||
[['iframe#red', 'h1'], 'color', 'rgb(255, 26, 26)'],
|
||||
|
||||
[['iframe#blue', 'document'], 'background-color', 'rgba(0, 0, 0, 0)'],
|
||||
[['iframe#blue', 'document'], 'color', 'rgb(232, 230, 227)'],
|
||||
[['iframe#blue', 'body'], 'background-color', 'rgba(0, 0, 0, 0)'],
|
||||
[['iframe#blue', 'body'], 'color', 'rgb(232, 230, 227)'],
|
||||
[['iframe#blue', 'h1'], 'color', 'rgb(51, 125, 255)'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import {multiline} from '../../support/test-utils';
|
||||
import type {StyleExpectations} from '../globals';
|
||||
|
||||
async function expectStyles(styles: StyleExpectations) {
|
||||
await expectPageStyles(expect, styles);
|
||||
}
|
||||
|
||||
describe('Custom HTML elements', () => {
|
||||
it('Asynchronous define', async () => {
|
||||
// Temporarily disable this test on Firefox
|
||||
if (product === 'firefox') {
|
||||
expect(true);
|
||||
return;
|
||||
}
|
||||
|
||||
await loadTestPage({
|
||||
'/': multiline(
|
||||
'<!DOCTYPE html>',
|
||||
'<html>',
|
||||
'<head>',
|
||||
'</head>',
|
||||
'<body></body>',
|
||||
'</html>',
|
||||
),
|
||||
});
|
||||
|
||||
await pageUtils.evaluateScript(async () => {
|
||||
class ElementWitAsync extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
const root = this.attachShadow({mode: 'open'});
|
||||
setTimeout(() => root.innerHTML =
|
||||
'<style>\
|
||||
p { color: red; }\
|
||||
</style>\
|
||||
<p>\
|
||||
Should be red initially and then change to green.\
|
||||
</p>'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('elem-with-async', ElementWitAsync);
|
||||
const elem = document.createElement('elem-with-async');
|
||||
document.body.appendChild(elem);
|
||||
});
|
||||
|
||||
await expectStyles([
|
||||
[['elem-with-async', 'p'], 'color', 'rgb(255, 26, 26)'],
|
||||
]);
|
||||
|
||||
await devtoolsUtils.paste(multiline(
|
||||
'*',
|
||||
'',
|
||||
'CSS',
|
||||
'p {',
|
||||
' color: green !important;',
|
||||
'}',
|
||||
));
|
||||
|
||||
await expectStyles([
|
||||
[['elem-with-async', 'p'], 'color', 'rgb(0, 128, 0)'],
|
||||
]);
|
||||
|
||||
await devtoolsUtils.reset();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,257 @@
|
||||
import {multiline, timeout} from '../../support/test-utils';
|
||||
import type {StyleExpectations} from '../globals';
|
||||
|
||||
async function expectStyles(styles: StyleExpectations) {
|
||||
await expectPageStyles(expect, styles);
|
||||
}
|
||||
|
||||
async function loadBasicPage(header: string) {
|
||||
await loadTestPage({
|
||||
'/': multiline(
|
||||
'<!DOCTYPE html>',
|
||||
'<html>',
|
||||
'<head>',
|
||||
' <style>',
|
||||
' h1 { color: red; }',
|
||||
' </style>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
` <h1>${header}</h1>`,
|
||||
' <p>Text</p>',
|
||||
' <a href="#">Link</a>',
|
||||
'</body>',
|
||||
'</html>',
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
describe('Toggling the extension', () => {
|
||||
const automationMenuSelector = '.header__more-settings-button';
|
||||
const automationSystemSelector = '.header__more-settings__system-dark-mode__checkbox .checkbox__input';
|
||||
|
||||
it('should turn On/Off', async () => {
|
||||
await loadBasicPage('Toggle on/off');
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['document', 'color', 'rgb(232, 230, 227)'],
|
||||
['body', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['body', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1', 'color', 'rgb(255, 26, 26)'],
|
||||
['a', 'color', 'rgb(51, 145, 255)'],
|
||||
]);
|
||||
|
||||
await popupUtils.click('.toggle__off');
|
||||
await timeout(500);
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgba(0, 0, 0, 0)'],
|
||||
['document', 'color', 'rgb(0, 0, 0)'],
|
||||
['body', 'background-color', 'rgba(0, 0, 0, 0)'],
|
||||
['body', 'color', 'rgb(0, 0, 0)'],
|
||||
['h1', 'color', 'rgb(255, 0, 0)'],
|
||||
['a', 'color', 'rgb(0, 0, 238)'],
|
||||
]);
|
||||
|
||||
await popupUtils.click('.toggle__on');
|
||||
await timeout(500);
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['document', 'color', 'rgb(232, 230, 227)'],
|
||||
['body', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['body', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1', 'color', 'rgb(255, 26, 26)'],
|
||||
['a', 'color', 'rgb(51, 145, 255)'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should follow system color scheme', async () => {
|
||||
await loadBasicPage('Automation (color scheme)');
|
||||
|
||||
|
||||
await emulateColorScheme('light');
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['document', 'color', 'rgb(232, 230, 227)'],
|
||||
['body', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['body', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1', 'color', 'rgb(255, 26, 26)'],
|
||||
['a', 'color', 'rgb(51, 145, 255)'],
|
||||
]);
|
||||
|
||||
await popupUtils.click(automationMenuSelector);
|
||||
await popupUtils.click(automationSystemSelector);
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgba(0, 0, 0, 0)'],
|
||||
['document', 'color', 'rgb(0, 0, 0)'],
|
||||
['body', 'background-color', 'rgba(0, 0, 0, 0)'],
|
||||
['body', 'color', 'rgb(0, 0, 0)'],
|
||||
['h1', 'color', 'rgb(255, 0, 0)'],
|
||||
['a', 'color', 'rgb(0, 0, 238)'],
|
||||
]);
|
||||
if ((await backgroundUtils.getManifest()).manifest_version === 3) {
|
||||
expect(await backgroundUtils.getChromeStorage('local', ['system-color-state'])).toEqual({
|
||||
'system-color-state': {wasLastColorSchemeDark: false},
|
||||
});
|
||||
}
|
||||
|
||||
await emulateColorScheme('dark');
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['document', 'color', 'rgb(232, 230, 227)'],
|
||||
['body', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['body', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1', 'color', 'rgb(255, 26, 26)'],
|
||||
['a', 'color', 'rgb(51, 145, 255)'],
|
||||
]);
|
||||
|
||||
await emulateColorScheme('light');
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgba(0, 0, 0, 0)'],
|
||||
['document', 'color', 'rgb(0, 0, 0)'],
|
||||
['body', 'background-color', 'rgba(0, 0, 0, 0)'],
|
||||
['body', 'color', 'rgb(0, 0, 0)'],
|
||||
['h1', 'color', 'rgb(255, 0, 0)'],
|
||||
['a', 'color', 'rgb(0, 0, 238)'],
|
||||
]);
|
||||
|
||||
await popupUtils.click(automationSystemSelector);
|
||||
|
||||
await expectStyles([
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['document', 'color', 'rgb(232, 230, 227)'],
|
||||
['body', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['body', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1', 'color', 'rgb(255, 26, 26)'],
|
||||
['a', 'color', 'rgb(51, 145, 255)'],
|
||||
]);
|
||||
|
||||
await emulateColorScheme('dark');
|
||||
});
|
||||
|
||||
// Note: this test is relevant only to Firefox and Thunderbird
|
||||
it('should ignore color watcher messages from subframes', async () => {
|
||||
const darkPageExpectations: StyleExpectations = [
|
||||
['document', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['document', 'color', 'rgb(232, 230, 227)'],
|
||||
['body', 'background-color', 'rgb(24, 26, 27)'],
|
||||
['body', 'color', 'rgb(232, 230, 227)'],
|
||||
['h1', 'color', 'rgb(255, 26, 26)'],
|
||||
['a', 'color', 'rgb(51, 145, 255)'],
|
||||
];
|
||||
|
||||
const lightPageExpectations: StyleExpectations = [
|
||||
['document', 'background-color', 'rgba(0, 0, 0, 0)'],
|
||||
['document', 'color', 'rgb(0, 0, 0)'],
|
||||
['body', 'background-color', 'rgba(0, 0, 0, 0)'],
|
||||
['body', 'color', 'rgb(0, 0, 0)'],
|
||||
['h1', 'color', 'rgb(255, 0, 0)'],
|
||||
['a', 'color', 'rgb(0, 0, 238)'],
|
||||
];
|
||||
|
||||
const darkSubframePageExpectations: StyleExpectations = [
|
||||
[['iframe', 'h1'], 'color', 'rgb(255, 26, 26)'],
|
||||
[['iframe', 'a'], 'color', 'rgb(51, 145, 255)'],
|
||||
];
|
||||
|
||||
const lightSubframePageExpectations: StyleExpectations = [
|
||||
[['iframe', 'h1'], 'color', 'rgb(255, 0, 0)'],
|
||||
[['iframe', 'a'], 'color', 'rgb(0, 0, 238)'],
|
||||
];
|
||||
|
||||
let loadSubframe: () => void = () => void(0);
|
||||
const loadCompleted = loadTestPage({
|
||||
'/': multiline(
|
||||
'<!DOCTYPE html>',
|
||||
'<html>',
|
||||
'<head>',
|
||||
' <style>',
|
||||
' h1 { color: red; }',
|
||||
' </style>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
` <h1>Color scheme detector</h1>`,
|
||||
' <p>Text</p>',
|
||||
' <a href="#">Link</a>',
|
||||
' <iframe src="/subframe.html" style="color-scheme: light"></iframe>',
|
||||
'</body>',
|
||||
'</html>',
|
||||
),
|
||||
'/subframe.html': async (_, res) => {
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
|
||||
await new Promise<void>((resolve) => loadSubframe = resolve);
|
||||
|
||||
res.end(
|
||||
multiline(
|
||||
'<!DOCTYPE html>',
|
||||
'<html>',
|
||||
'<head>',
|
||||
' <style>',
|
||||
' h1 { color: red; }',
|
||||
' </style>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
' <h1>Header</h1>',
|
||||
' <p>Text</p>',
|
||||
' <a href="#">Link</a>',
|
||||
'</body>',
|
||||
'</html>',
|
||||
),
|
||||
'utf8'
|
||||
);
|
||||
},
|
||||
}, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
|
||||
await awaitForEvent('ready-/');
|
||||
|
||||
await emulateColorScheme('dark');
|
||||
await expectStyles(darkPageExpectations);
|
||||
|
||||
await popupUtils.click(automationMenuSelector);
|
||||
await popupUtils.click(automationSystemSelector);
|
||||
|
||||
await expectStyles(darkPageExpectations);
|
||||
|
||||
// Finalize page load
|
||||
loadSubframe();
|
||||
// Top-level page may finish loading only after the subframe has loaded
|
||||
await loadCompleted;
|
||||
|
||||
// Ensure that the subframe received its styles
|
||||
await expectStyles(darkSubframePageExpectations);
|
||||
// Ensure that the parent frame retained its styles
|
||||
await expectStyles(darkPageExpectations);
|
||||
|
||||
await emulateColorScheme('light');
|
||||
|
||||
await expectStyles(lightPageExpectations);
|
||||
await expectStyles(lightSubframePageExpectations);
|
||||
|
||||
await popupUtils.click(automationSystemSelector);
|
||||
|
||||
await expectStyles(darkPageExpectations);
|
||||
|
||||
await emulateColorScheme('dark');
|
||||
});
|
||||
|
||||
it('should have new design button on desktop', async () => {
|
||||
await devtoolsUtils.click('.settings-tab-panel__button:nth-child(4)');
|
||||
expect(await devtoolsUtils.exists('.preview-design-button'));
|
||||
});
|
||||
|
||||
it('dynamic themes', async () => {
|
||||
await loadBasicPage('Dynamic styles');
|
||||
|
||||
const numStyles = await pageUtils.evaluateScript(() => document.styleSheets.length);
|
||||
expect(numStyles).toBe(1);
|
||||
});
|
||||
});
|
||||
+466
@@ -0,0 +1,466 @@
|
||||
import {TestEnvironment} from 'jest-environment-node';
|
||||
import {launch} from 'puppeteer-core';
|
||||
import {WebSocketServer} from 'ws';
|
||||
|
||||
import {generateHTMLCoverageReports} from './coverage.js';
|
||||
import {getChromePath, getFirefoxPath, chromeMV3ExtensionDebugDir, chromePlusExtensionDebugDir, firefoxExtensionDebugDir, getEdgePath} from './paths.js';
|
||||
import {createTestServer, generateRandomId} from './server.js';
|
||||
|
||||
const TEST_SERVER_PORT = 8891;
|
||||
const CORS_SERVER_PORT = 8892;
|
||||
const FIREFOX_DEVTOOLS_PORT = 8893;
|
||||
const POPUP_TEST_PORT = 8894;
|
||||
|
||||
export default class CustomJestEnvironment extends TestEnvironment {
|
||||
/** @type {() => void} */
|
||||
extensionStartListeners = [];
|
||||
pageEventListeners = new Map();
|
||||
|
||||
/** @type {Browser} */
|
||||
browser;
|
||||
/** @type {WebSocketServer} */
|
||||
messageServer;
|
||||
|
||||
async setup() {
|
||||
await super.setup();
|
||||
|
||||
const promises1 = [
|
||||
this.createMessageServer(),
|
||||
this.launchBrowser(),
|
||||
];
|
||||
const promises2 = [
|
||||
createTestServer(TEST_SERVER_PORT),
|
||||
createTestServer(CORS_SERVER_PORT),
|
||||
];
|
||||
|
||||
const results1 = await Promise.all(promises1);
|
||||
this.messageServer = results1[0];
|
||||
this.browser = results1[1];
|
||||
|
||||
promises2.push(
|
||||
this.createTestPage(),
|
||||
);
|
||||
|
||||
const results2 = await Promise.all(promises2);
|
||||
this.testServer = results2[0];
|
||||
this.corsServer = results2[1];
|
||||
this.page = results2[2];
|
||||
|
||||
// Wait for tabs to load?
|
||||
|
||||
this.assignTestGlobals(this.global, this.testServer, this.corsServer, this.page);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async waitForStartup() {
|
||||
if (!this.extensionOrigin) {
|
||||
return new Promise((ready) => this.extensionStartListeners.push(ready));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<Browser>}
|
||||
*/
|
||||
async launchBrowser() {
|
||||
let browser;
|
||||
if (this.global.product === 'edge') {
|
||||
browser = await this.launchEdge();
|
||||
} else if (this.global.product === 'chrome-mv3') {
|
||||
browser = await this.launchChrome();
|
||||
} else if (this.global.product === 'firefox') {
|
||||
browser = await this.launchFirefox();
|
||||
}
|
||||
// Wait for the extension to start
|
||||
await this.waitForStartup();
|
||||
return browser;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<Browser>}
|
||||
*/
|
||||
async launchChrome() {
|
||||
const extensionDir = chromeMV3ExtensionDebugDir;
|
||||
let executablePath;
|
||||
try {
|
||||
executablePath = await getChromePath();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
// Explanation of these options:
|
||||
// https://pptr.dev/guides/chrome-extensions
|
||||
return await launch({
|
||||
args: [
|
||||
'--show-component-extension-options',
|
||||
],
|
||||
enableExtensions: [extensionDir],
|
||||
executablePath,
|
||||
headless: false,
|
||||
pipe: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<Browser>}
|
||||
*/
|
||||
async launchEdge() {
|
||||
const extensionDir = chromePlusExtensionDebugDir;
|
||||
let executablePath;
|
||||
try {
|
||||
executablePath = await getEdgePath();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return await launch({
|
||||
args: [
|
||||
'--show-component-extension-options',
|
||||
],
|
||||
enableExtensions: [extensionDir],
|
||||
executablePath,
|
||||
headless: false,
|
||||
pipe: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<Browser>}
|
||||
*/
|
||||
async launchFirefox() {
|
||||
// We need to manually launch Firefox via cmd.run() to install extension
|
||||
// because Firefox does not support installing via CLI arguments
|
||||
process.setMaxListeners(process.getMaxListeners() + 1);
|
||||
const firefox = await getFirefoxPath();
|
||||
const browser = await launch({
|
||||
browser: 'firefox',
|
||||
executablePath: firefox,
|
||||
protocol: 'webDriverBiDi',
|
||||
headless: false,
|
||||
args: [`--remote-debugging-port=${FIREFOX_DEVTOOLS_PORT}`],
|
||||
});
|
||||
await browser.installExtension(firefoxExtensionDebugDir);
|
||||
return browser;
|
||||
}
|
||||
|
||||
async createTestPage() {
|
||||
const page = await this.browser.newPage();
|
||||
page.on('pageerror', (err) => process.emit('uncaughtException', err));
|
||||
if (this.global.product !== 'firefox') {
|
||||
await page.coverage.startJSCoverage();
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
async getURL(path) {
|
||||
// By this point browser should be loaded and extension should be started, but
|
||||
// let's wait anuway
|
||||
await this.waitForStartup();
|
||||
const url = new URL(path, this.extensionOrigin);
|
||||
return url.href;
|
||||
}
|
||||
|
||||
async getChromiumMV2BackgroundPage() {
|
||||
const targets = this.browser.targets();
|
||||
const backgroundTarget = targets.find((t) => t.type() === 'background_page');
|
||||
return await backgroundTarget.page();
|
||||
}
|
||||
|
||||
async awaitForEvent(uuid) {
|
||||
return new Promise((resolve) => {
|
||||
if (this.pageEventListeners.has(uuid)) {
|
||||
this.pageEventListeners.get(uuid).push(resolve);
|
||||
} else {
|
||||
this.pageEventListeners.set(uuid, [resolve]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Page} page
|
||||
* @param {string} url
|
||||
* @param {WaitForOptions} gotoOptions
|
||||
* @returns Promise which resolves when page loads
|
||||
*/
|
||||
async pageGoto(url, gotoOptions) {
|
||||
// Normalize URL
|
||||
const pathname = new URL(url).pathname;
|
||||
// Depending on external circumstances, page may connect to server before page.goto() reolves
|
||||
const promise = this.awaitForEvent(`ready-${pathname}`);
|
||||
await this.page.goto(url, gotoOptions);
|
||||
await promise;
|
||||
}
|
||||
|
||||
async openTestPage(url, gotoOptions) {
|
||||
await this.page.bringToFront();
|
||||
await this.pageGoto(url, gotoOptions);
|
||||
}
|
||||
|
||||
onPageEventResponse(eventUUID) {
|
||||
const resolves = this.pageEventListeners.get(eventUUID);
|
||||
this.pageEventListeners.delete(eventUUID);
|
||||
resolves && resolves.forEach((r) => r());
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is evaluated within browser's page context
|
||||
* after being passed to page.evaluate()
|
||||
* It can use methods which will be defined in the page context,
|
||||
* but can not use variables defined in this file besides those passed into it.
|
||||
*/
|
||||
async checkPageStylesInBrowserContext(expectations) {
|
||||
const checkOne = (expectation) => {
|
||||
const [selector, cssAttributeName, expectedValue] = expectation;
|
||||
const selector_ = Array.isArray(selector) ? selector : [selector];
|
||||
let element = document;
|
||||
for (const part of selector_) {
|
||||
if (element instanceof HTMLIFrameElement) {
|
||||
element = element.contentDocument;
|
||||
}
|
||||
if (element.shadowRoot instanceof ShadowRoot) {
|
||||
element = element.shadowRoot;
|
||||
}
|
||||
if (part === 'document') {
|
||||
element = element.documentElement;
|
||||
} else {
|
||||
element = element.querySelector(part);
|
||||
}
|
||||
if (!element) {
|
||||
return `Could not find element ${part}`;
|
||||
}
|
||||
}
|
||||
const style = getComputedStyle(element);
|
||||
if (style[cssAttributeName] !== expectedValue) {
|
||||
return `Expected ${selector_.join(' ')} '${cssAttributeName}' to be '${expectedValue}', but got '${style[cssAttributeName]}'`;
|
||||
}
|
||||
};
|
||||
|
||||
const checkAll = () => {
|
||||
/** @type{Array<[number, string]>} */
|
||||
const errors = [];
|
||||
for (let i = 0; i < expectations.length; i++) {
|
||||
const error = checkOne(expectations[i]);
|
||||
if (error) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
};
|
||||
|
||||
let timeout = 10;
|
||||
let errors = checkAll();
|
||||
for (let i = 0; (errors.length !== 0) && (i < 10); i++) {
|
||||
timeout *= 2;
|
||||
await new Promise((r) => requestIdleCallback(r, {timeout}));
|
||||
errors = checkAll();
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
assignTestGlobals(global, testServer, corsServer, page) {
|
||||
global.getColorScheme = async () => {
|
||||
if (global.product === 'firefox') {
|
||||
return await global.backgroundUtils.getColorScheme();
|
||||
}
|
||||
const isDark = await page.evaluate(() => matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
return isDark ? 'dark' : 'light';
|
||||
};
|
||||
|
||||
global.pageUtils.evaluateScript = async (script) => await page.evaluate(script);
|
||||
|
||||
global.expectPageStyles = async (expect, expectations) => {
|
||||
if (!Array.isArray(expectations[0])) {
|
||||
expectations = [expectations];
|
||||
}
|
||||
const errors = await page.evaluate(this.checkPageStylesInBrowserContext, expectations);
|
||||
expect(errors.join('\n')).toBe('');
|
||||
};
|
||||
|
||||
global.emulateColorScheme = async (colorScheme) => {
|
||||
if (global.product === 'firefox') {
|
||||
await global.pageUtils.emulateColorScheme(colorScheme);
|
||||
await global.backgroundUtils.emulateColorScheme(colorScheme);
|
||||
const newPageColorScheme = await global.backgroundUtils.getColorScheme();
|
||||
const newBGColorScheme = await global.pageUtils.getColorScheme();
|
||||
if (newPageColorScheme !== colorScheme || newBGColorScheme !== colorScheme) {
|
||||
throw new Error('Failed to apply new color scheme');
|
||||
}
|
||||
return;
|
||||
}
|
||||
await page.emulateMediaFeatures([{name: 'prefers-color-scheme', value: colorScheme}]);
|
||||
if (global.product === 'edge') {
|
||||
const page = await this.getChromiumMV2BackgroundPage();
|
||||
await page.emulateMediaFeatures([{name: 'prefers-color-scheme', value: colorScheme}]);
|
||||
}
|
||||
};
|
||||
|
||||
global.loadTestPage = async (paths, gotoOptions) => {
|
||||
const {cors, ...testPaths} = paths;
|
||||
testServer.setPaths(testPaths);
|
||||
cors && corsServer.setPaths(cors);
|
||||
await this.openTestPage(`http://localhost:${TEST_SERVER_PORT}`, gotoOptions);
|
||||
};
|
||||
|
||||
global.corsURL = corsServer.url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a server and returns once extension connects to it
|
||||
* @returns {Promise<WebSocketServer>} server
|
||||
*/
|
||||
async createMessageServer() {
|
||||
const awaitForEvent = this.awaitForEvent.bind(this);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const wsServer = new WebSocketServer({port: POPUP_TEST_PORT});
|
||||
let backgroundSocket = null;
|
||||
let devToolsSocket = null;
|
||||
const popupSockets = new Set();
|
||||
const pageSockets = new Set();
|
||||
const resolvers = new Map();
|
||||
const rejectors = new Map();
|
||||
|
||||
let onDownloadCallback = null;
|
||||
|
||||
wsServer.on('connection', async (ws) => {
|
||||
ws.on('message', (data) => {
|
||||
const message = JSON.parse(data);
|
||||
if (message.id === null && message.data && message.data.type === 'background' && message.data.extensionOrigin) {
|
||||
// This is the initial message which contains extension's URL origin
|
||||
// and signals that extenstion is ready
|
||||
this.extensionOrigin = message.data.extensionOrigin;
|
||||
this.extensionStartListeners.forEach((ready) => ready());
|
||||
ws.on('close', () => backgroundSocket = null);
|
||||
backgroundSocket = ws;
|
||||
resolve(wsServer);
|
||||
} else if (message.id === null && message.data && message.data.type === 'devtools') {
|
||||
ws.on('close', () => devToolsSocket = null);
|
||||
devToolsSocket = ws;
|
||||
this.onPageEventResponse(message.data.uuid);
|
||||
} else if (message.id === null && message.data && message.data.type === 'popup') {
|
||||
ws.on('close', () => popupSockets.delete(ws));
|
||||
popupSockets.add(ws);
|
||||
this.onPageEventResponse(message.data.uuid);
|
||||
} else if (message.id === null && message.data && message.data.type === 'page') {
|
||||
if (message.data.message === 'page-ready' && message.data.uuid === 'ready-/') {
|
||||
ws.on('close', () => pageSockets.delete(ws));
|
||||
pageSockets.add(ws);
|
||||
}
|
||||
this.onPageEventResponse(message.data.uuid);
|
||||
} else if (message.id === null && message.data && message.data.type === 'download') {
|
||||
if (onDownloadCallback) {
|
||||
onDownloadCallback(message.data);
|
||||
}
|
||||
} else if (message.error) {
|
||||
const reject = rejectors.get(message.id);
|
||||
reject(message.error);
|
||||
} else {
|
||||
const resolve = resolvers.get(message.id);
|
||||
resolve(message.data);
|
||||
}
|
||||
resolvers.delete(message.id);
|
||||
rejectors.delete(message.id);
|
||||
});
|
||||
});
|
||||
|
||||
function sendToContext(sockets, type, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = generateRandomId();
|
||||
resolvers.set(id, resolve);
|
||||
rejectors.set(id, reject);
|
||||
const json = JSON.stringify({type, data, id});
|
||||
for (const ws of sockets) {
|
||||
ws.send(json);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function sendToPopup(type, data) {
|
||||
return sendToContext(Array.from(popupSockets), type, data);
|
||||
}
|
||||
|
||||
function sendToDevTools(type, data) {
|
||||
return sendToContext([devToolsSocket], type, data);
|
||||
}
|
||||
|
||||
function sendToBackground(type, data) {
|
||||
return sendToContext([backgroundSocket], type, data);
|
||||
}
|
||||
|
||||
function sendToPage(type, data) {
|
||||
return sendToContext(Array.from(pageSockets), type, data);
|
||||
}
|
||||
|
||||
async function applyDevtoolsConfig(type, fixes) {
|
||||
const promise = awaitForEvent('darkreader-dynamic-theme-ready');
|
||||
await Promise.all([
|
||||
sendToDevTools(type, fixes),
|
||||
promise,
|
||||
]);
|
||||
}
|
||||
|
||||
this.global.popupUtils = {
|
||||
saveFile: async (name, content) => sendToPopup('popup-saveFile', {name, content}),
|
||||
click: async (selector) => await sendToPopup('popup-click', selector),
|
||||
exists: async (selector) => await sendToPopup('popup-exists', selector),
|
||||
};
|
||||
|
||||
this.global.devtoolsUtils = {
|
||||
click: async (selector) => await sendToDevTools('devtools-click', selector),
|
||||
exists: async (selector) => await sendToDevTools('devtools-exists', selector),
|
||||
paste: async (fixes) => await applyDevtoolsConfig('devtools-paste', fixes),
|
||||
reset: async () => await applyDevtoolsConfig('devtools-reset'),
|
||||
};
|
||||
|
||||
this.global.backgroundUtils = {
|
||||
changeSettings: async (settings) => await sendToBackground('changeSettings', settings),
|
||||
collectData: async () => await sendToBackground('collectData'),
|
||||
changeChromeStorage: async (region, data) => await sendToBackground('changeChromeStorage', {region, data}),
|
||||
getChromeStorage: async (region, keys) => await sendToBackground('getChromeStorage', {region, keys}),
|
||||
getManifest: async () => await sendToBackground('getManifest'),
|
||||
getColorScheme: async () => {
|
||||
return await sendToBackground('firefox-getColorScheme');
|
||||
},
|
||||
emulateColorScheme: async (colorScheme) => {
|
||||
await sendToBackground('firefox-emulateColorScheme', colorScheme);
|
||||
},
|
||||
setNews: async (news) => await sendToBackground('setNews', news),
|
||||
onDownload: (callback) => onDownloadCallback = callback,
|
||||
};
|
||||
|
||||
this.global.pageUtils = {
|
||||
emulateColorScheme: async (colorScheme) => await sendToPage('firefox-emulateColorScheme', colorScheme),
|
||||
getColorScheme: async () => {
|
||||
return await sendToPage('firefox-getColorScheme');
|
||||
},
|
||||
};
|
||||
|
||||
this.global.awaitForEvent = awaitForEvent;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async teardown() {
|
||||
await super.teardown();
|
||||
|
||||
const promises = [];
|
||||
if (this.global.product !== 'firefox' && this.page?.coverage) {
|
||||
const coverage = await this.page.coverage.stopJSCoverage();
|
||||
const dir = './tests/browser/coverage/';
|
||||
const promise = generateHTMLCoverageReports(dir, coverage);
|
||||
promise.then(() => console.info('Coverage reports generated in', dir));
|
||||
promises.push(promise);
|
||||
}
|
||||
|
||||
// Note: this.browser.close() will close all tabs, so no need to close them
|
||||
// explicitly
|
||||
promises.push([
|
||||
this.testServer?.close(),
|
||||
this.corsServer?.close(),
|
||||
this.messageServer?.close(),
|
||||
this.browser?.close(),
|
||||
]);
|
||||
await Promise.all(promises);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type {RequestListener} from 'http';
|
||||
|
||||
import type {WaitForOptions} from 'puppeteer-core';
|
||||
|
||||
import type {ColorScheme, ExtensionData, News, UserSettings} from '../../src/definitions';
|
||||
|
||||
type PathsObject = {[path: string]: string | RequestListener | PathsObject};
|
||||
type OneStyleExpectation = [selector: string | string[], cssAttributeName: string, expectedValue: string];
|
||||
type StyleExpectations = OneStyleExpectation[] | OneStyleExpectation;
|
||||
|
||||
declare global {
|
||||
const loadTestPage: (paths: PathsObject & {cors?: PathsObject}, gotoOptions?: WaitForOptions) => Promise<void>;
|
||||
const corsURL: string;
|
||||
const popupUtils: {
|
||||
click: (selector: string) => Promise<void>;
|
||||
exists: (selector: string) => Promise<void>;
|
||||
saveFile: (name: string, content: string) => Promise<void>;
|
||||
};
|
||||
const devtoolsUtils: {
|
||||
click: (selector: string) => Promise<void>;
|
||||
exists: (selector: string) => Promise<void>;
|
||||
paste: (fixes: string) => Promise<void>;
|
||||
reset: () => Promise<void>;
|
||||
};
|
||||
const backgroundUtils: {
|
||||
changeSettings: (settings: Partial<UserSettings>) => Promise<void>;
|
||||
collectData: () => Promise<ExtensionData>;
|
||||
changeChromeStorage: (region: 'local' | 'sync', data: {[key: string]: any}) => Promise<void>;
|
||||
getColorScheme: () => Promise<ColorScheme>;
|
||||
getChromeStorage: (region: 'local' | 'sync', keys: string[]) => Promise<{[key: string]: any}>;
|
||||
getManifest: () => Promise<chrome.runtime.Manifest>;
|
||||
setNews: (news: News[] | null) => Promise<void>;
|
||||
onDownload: (callback: (p: {ok: boolean}) => void) => void;
|
||||
emulateColorScheme: (colorScheme: ColorScheme) => Promise<void>;
|
||||
};
|
||||
const pageUtils: {
|
||||
evaluateScript: (script: () => any) => Promise<any>;
|
||||
};
|
||||
const emulateColorScheme: (value: ColorScheme) => Promise<void>;
|
||||
const awaitForEvent: (uuid: string) => Promise<void>;
|
||||
const expectPageStyles: (expect: jest.Expect, expectations: StyleExpectations) => Promise<void>;
|
||||
const getColorScheme: () => Promise<ColorScheme>;
|
||||
const product: 'firefox';
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import config from './jest.config.mjs';
|
||||
config.globals.product = 'chrome-mv3';
|
||||
config.globals.__CHROMIUM_MV2__ = false;
|
||||
config.globals.__CHROMIUM_MV3__ = true;
|
||||
export default config;
|
||||
@@ -0,0 +1,4 @@
|
||||
import config from './jest.config.mjs';
|
||||
config.globals.product = 'firefox';
|
||||
config.globals.__CHROMIUM_MV2__ = false;
|
||||
export default config;
|
||||
@@ -0,0 +1,31 @@
|
||||
// @ts-check
|
||||
|
||||
import {dirname} from 'node:path';
|
||||
import {createRequire} from 'node:module';
|
||||
const rootDir = dirname(createRequire(import.meta.url).resolve('../../package.json'));
|
||||
|
||||
/** @type {import('@jest/types').Config.InitialOptions} */
|
||||
const config = {
|
||||
rootDir,
|
||||
testMatch: ['<rootDir>/tests/browser/**/*.tests.ts'],
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js'],
|
||||
testEnvironment: '<rootDir>/tests/browser/environment.js',
|
||||
verbose: true,
|
||||
transform: {'^.+\\.ts(x?)$': ['ts-jest', {tsconfig: '<rootDir>/tests/browser/tsconfig.json'}]},
|
||||
globals: {
|
||||
__DEBUG__: false,
|
||||
__CHROMIUM_MV2__: true,
|
||||
__CHROMIUM_MV3__: false,
|
||||
__FIREFOX_MV2__: false,
|
||||
__THUNDERBIRD__: false,
|
||||
__TEST__: true,
|
||||
product: 'edge',
|
||||
},
|
||||
setupFilesAfterEnv: ['jest-extended/all'],
|
||||
collectCoverage: false,
|
||||
coverageDirectory: 'coverage',
|
||||
collectCoverageFrom: ['<rootDir>/src/**/*.{ts,tsx}'],
|
||||
coveragePathIgnorePatterns: ['^.+\\.d\\.ts$'],
|
||||
};
|
||||
|
||||
export default config;
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
// @ts-check
|
||||
import {exec} from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import * as url from 'url';
|
||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
/**
|
||||
* @param {string} relPath
|
||||
* @returns {string}
|
||||
*/
|
||||
function winProgramFiles(relPath) {
|
||||
const x64Path = path.join(process.env.PROGRAMFILES, relPath);
|
||||
if (fs.existsSync(x64Path)) {
|
||||
return x64Path;
|
||||
}
|
||||
return path.join(process.env['ProgramFiles(x86)'], relPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} app
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
function linuxAppPath(app) {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`which ${app}`, (err, result) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(result.trim());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function getChromePath() {
|
||||
if (process.platform === 'darwin') {
|
||||
return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
return winProgramFiles('Google\\Chrome\\Application\\chrome.exe');
|
||||
}
|
||||
const possibleLinuxPaths = ['google-chrome', 'google-chrome-stable', 'chromium'];
|
||||
for (const possiblePath of possibleLinuxPaths) {
|
||||
try {
|
||||
return await linuxAppPath(possiblePath);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
throw new Error('Could not find Chrome');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function getEdgePath() {
|
||||
if (process.platform === 'darwin') {
|
||||
return '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge';
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
return winProgramFiles('Microsoft\\Edge\\Application\\msedge.exe');
|
||||
}
|
||||
const possibleLinuxPaths = ['microsoft-edge', 'microsoft-edge-stable'];
|
||||
for (const possiblePath of possibleLinuxPaths) {
|
||||
try {
|
||||
return await linuxAppPath(possiblePath);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
throw new Error('Could not find Edge');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function getFirefoxPath() {
|
||||
if (process.platform === 'darwin') {
|
||||
return '/Applications/Firefox Nightly.app/Contents/MacOS/firefox';
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
return await winProgramFiles('Firefox Nightly\\firefox.exe');
|
||||
}
|
||||
const possibleLinuxPaths = ['firefox-nightly', 'firefox'];
|
||||
for (const possiblePath of possibleLinuxPaths) {
|
||||
try {
|
||||
// snap profile folders do not get loaded
|
||||
const option = await linuxAppPath(possiblePath);
|
||||
// Firefox snap can not access the regular system-wide temporary directory,
|
||||
// so we create a separate one within build folder
|
||||
// See also: https://github.com/mozilla/web-ext/issues/1696
|
||||
if (!option.includes('/snap/')) {
|
||||
return option;
|
||||
}
|
||||
const firefoxProfile = './build/firefox-profile-for-testing';
|
||||
process.env.TMPDIR = firefoxProfile;
|
||||
try {
|
||||
fs.mkdirSync(firefoxProfile);
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
return option;
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
throw new Error('Could not find firefox-nightly');
|
||||
}
|
||||
|
||||
export const chromeExtensionDebugDir = path.join(__dirname, '../../build/debug/chrome');
|
||||
export const chromePlusExtensionDebugDir = path.join(__dirname, '../../build/debug/chrome-plus');
|
||||
export const chromeMV3ExtensionDebugDir = path.join(__dirname, '../../build/debug/chrome-mv3');
|
||||
export const firefoxExtensionDebugDir = path.join(__dirname, '../../build/debug/firefox');
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
// @ts-check
|
||||
import http from 'node:http';
|
||||
import path from 'node:path';
|
||||
|
||||
const mimeTypes = new Map(
|
||||
Object.entries({
|
||||
'.css': 'text/css',
|
||||
'.html': 'text/html',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.js': 'text/javascript',
|
||||
'.json': 'application/json',
|
||||
'.png': 'image/png',
|
||||
'.svg': 'image/svg+xml',
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* We reuse a single listener for each of exit and SIGINT events to
|
||||
* avoid warnings about possible event listener leaks.
|
||||
* @type {Array<() => Promise<void>>}
|
||||
*/
|
||||
const terminationListeners = [];
|
||||
const terminationListener = () => {
|
||||
terminationListeners.forEach((listener) => listener());
|
||||
};
|
||||
|
||||
export function generateRandomId() {
|
||||
return Math.floor(Math.random() * 2 ** 55).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} port
|
||||
*/
|
||||
export async function createTestServer(port) {
|
||||
/** @type {import('http').Server} */
|
||||
let server;
|
||||
/** @type {{[path: string]: string | import('http').RequestListener}} */
|
||||
const paths = {};
|
||||
/** @type {Set<import('net').Socket>} */
|
||||
const sockets = new Set();
|
||||
|
||||
/** @type {import('http').RequestListener} */
|
||||
function handleRequest(req, res) {
|
||||
const parsedURL = new URL(req.url, 'https://localhost');
|
||||
const pathName = parsedURL.pathname;
|
||||
|
||||
if (!paths.hasOwnProperty(pathName)) {
|
||||
res.statusCode = 404;
|
||||
res.end('Not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const contentOrListener = paths[pathName];
|
||||
|
||||
if (typeof contentOrListener === 'function') {
|
||||
const listener = contentOrListener;
|
||||
return listener(req, res);
|
||||
}
|
||||
|
||||
const content = contentOrListener;
|
||||
const ext = pathName === '/' ? '.html' : path.extname(pathName);
|
||||
const contentType = mimeTypes.get(ext) || 'text/plain';
|
||||
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', contentType);
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.end(content, 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function start() {
|
||||
return new Promise((resolve) => {
|
||||
server = http
|
||||
.createServer(handleRequest)
|
||||
.listen(port, () => resolve());
|
||||
|
||||
server.on('connection', (socket) => {
|
||||
sockets.add(socket);
|
||||
socket.on('close', () => sockets.delete(socket));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{[path: string]: string | import('http').RequestListener}} newPaths
|
||||
*/
|
||||
function setPaths(newPaths) {
|
||||
Object.assign(paths, newPaths);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function close() {
|
||||
if (!server) {
|
||||
return;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
server.close((err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
}
|
||||
server = null;
|
||||
resolve();
|
||||
});
|
||||
sockets.forEach((socket) => {
|
||||
socket.destroy();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (terminationListeners.length === 0) {
|
||||
process.on('exit', terminationListener);
|
||||
process.on('SIGINT', terminationListener);
|
||||
}
|
||||
terminationListeners.push(close);
|
||||
|
||||
await start();
|
||||
|
||||
return {
|
||||
setPaths,
|
||||
close,
|
||||
url: `http://localhost:${port}`,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
describe('Modifying settings', () => {
|
||||
it('Modifying sync settings to contain long list', async () => {
|
||||
const newSettings = {
|
||||
enabledFor: [] as string[],
|
||||
disabledFor: [] as string[],
|
||||
syncSettings: true,
|
||||
};
|
||||
|
||||
// Cumulative length should be over the browser limit on record size
|
||||
for (let i = 0; i < 1000; i ++) {
|
||||
newSettings.disabledFor.push(`example${i}.com`);
|
||||
}
|
||||
|
||||
await backgroundUtils.changeSettings(newSettings);
|
||||
|
||||
const extensionData = await backgroundUtils.collectData();
|
||||
expect(extensionData.settings.syncSettings).toBe(true);
|
||||
expect(extensionData.settings.disabledFor.length).toBe(1000);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2019",
|
||||
"module": "CommonJS",
|
||||
"lib": [
|
||||
"ES2015",
|
||||
"Dom"
|
||||
],
|
||||
"types": [
|
||||
"chrome",
|
||||
"jest",
|
||||
"puppeteer-core"
|
||||
],
|
||||
"allowJs": true,
|
||||
"downlevelIteration": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react",
|
||||
"jsxFactory": "m",
|
||||
"moduleResolution": "Node",
|
||||
"noEmit": true,
|
||||
"noImplicitAny": true,
|
||||
"ignoreDeprecations": "6.0",
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user