mirror of
https://github.com/SoPat712/videospeed.git
synced 2026-04-21 04:42:35 -04:00
310 lines
7.3 KiB
JavaScript
310 lines
7.3 KiB
JavaScript
const fs = require("fs");
|
|
const path = require("path");
|
|
const { JSDOM } = require("jsdom");
|
|
const { vi } = require("vitest");
|
|
|
|
const ROOT = path.resolve(__dirname, "..", "..");
|
|
|
|
function clone(value) {
|
|
if (value === undefined) return undefined;
|
|
return JSON.parse(JSON.stringify(value));
|
|
}
|
|
|
|
function workspacePath(relPath) {
|
|
return path.join(ROOT, relPath);
|
|
}
|
|
|
|
function readWorkspaceFile(relPath) {
|
|
return fs.readFileSync(workspacePath(relPath), "utf8");
|
|
}
|
|
|
|
function loadHtmlFile(relPath) {
|
|
loadHtmlString(readWorkspaceFile(relPath));
|
|
}
|
|
|
|
function applyJSDOMWindow(win) {
|
|
globalThis.window = win;
|
|
globalThis.document = win.document;
|
|
globalThis.navigator = win.navigator;
|
|
globalThis.customElements = win.customElements;
|
|
globalThis.HTMLElement = win.HTMLElement;
|
|
globalThis.Element = win.Element;
|
|
globalThis.Node = win.Node;
|
|
globalThis.Text = win.Text;
|
|
globalThis.DocumentFragment = win.DocumentFragment;
|
|
globalThis.Event = win.Event;
|
|
globalThis.MouseEvent = win.MouseEvent;
|
|
globalThis.KeyboardEvent = win.KeyboardEvent;
|
|
globalThis.DOMParser = win.DOMParser;
|
|
globalThis.URL = win.URL;
|
|
globalThis.Blob = win.Blob;
|
|
globalThis.FileReader = win.FileReader;
|
|
win.Date = globalThis.Date;
|
|
win.open = vi.fn();
|
|
win.close = vi.fn();
|
|
}
|
|
|
|
function loadHtmlString(html) {
|
|
const dom = new JSDOM(html, {
|
|
url: "https://example.org/",
|
|
pretendToBeVisual: true,
|
|
runScripts: "dangerously"
|
|
});
|
|
applyJSDOMWindow(dom.window);
|
|
}
|
|
|
|
const WINDOW_GLOBAL_SKIP = new Set([
|
|
"alert",
|
|
"atob",
|
|
"blur",
|
|
"btoa",
|
|
"cancelAnimationFrame",
|
|
"captureEvents",
|
|
"clearInterval",
|
|
"clearTimeout",
|
|
"close",
|
|
"confirm",
|
|
"fetch",
|
|
"focus",
|
|
"getComputedStyle",
|
|
"matchMedia",
|
|
"open",
|
|
"prompt",
|
|
"queueMicrotask",
|
|
"releaseEvents",
|
|
"requestAnimationFrame",
|
|
"setInterval",
|
|
"setTimeout",
|
|
"stop"
|
|
]);
|
|
|
|
function mirrorExtensionGlobalsFromWindow(win) {
|
|
if (!win) return;
|
|
if (win.tc) {
|
|
globalThis.tc = win.tc;
|
|
}
|
|
for (const key of Object.keys(win)) {
|
|
if (WINDOW_GLOBAL_SKIP.has(key)) continue;
|
|
if (/^[A-Z]/.test(key)) continue;
|
|
const val = win[key];
|
|
if (typeof val === "function") {
|
|
globalThis[key] = val;
|
|
}
|
|
}
|
|
}
|
|
|
|
function evaluateScript(relPath) {
|
|
const absPath = workspacePath(relPath);
|
|
const source =
|
|
"var chrome = window.chrome || globalThis.chrome;\n" +
|
|
readWorkspaceFile(relPath) +
|
|
`\n//# sourceURL=${absPath.replace(/\\/g, "/")}`;
|
|
const el = document.createElement("script");
|
|
el.textContent = source;
|
|
document.head.appendChild(el);
|
|
mirrorExtensionGlobalsFromWindow(window);
|
|
}
|
|
|
|
function fireDOMContentLoaded() {
|
|
document.dispatchEvent(
|
|
new window.Event("DOMContentLoaded", {
|
|
bubbles: true,
|
|
cancelable: true
|
|
})
|
|
);
|
|
}
|
|
|
|
async function flushAsyncWork(turns) {
|
|
const count = turns || 2;
|
|
for (let i = 0; i < count; i += 1) {
|
|
await Promise.resolve();
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
}
|
|
}
|
|
|
|
function pickStorageValues(data, keys) {
|
|
if (keys == null) return clone(data || {});
|
|
|
|
if (typeof keys === "string") {
|
|
return { [keys]: clone(data ? data[keys] : undefined) };
|
|
}
|
|
|
|
if (Array.isArray(keys)) {
|
|
const result = {};
|
|
keys.forEach((key) => {
|
|
result[key] = clone(data ? data[key] : undefined);
|
|
});
|
|
return result;
|
|
}
|
|
|
|
if (typeof keys === "object") {
|
|
const result = clone(keys) || {};
|
|
Object.keys(keys).forEach((key) => {
|
|
if (data && Object.prototype.hasOwnProperty.call(data, key)) {
|
|
result[key] = clone(data[key]);
|
|
}
|
|
});
|
|
return result;
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
function createChromeEvent() {
|
|
const listeners = [];
|
|
return {
|
|
addListener(listener) {
|
|
listeners.push(listener);
|
|
},
|
|
removeListener(listener) {
|
|
const index = listeners.indexOf(listener);
|
|
if (index >= 0) {
|
|
listeners.splice(index, 1);
|
|
}
|
|
},
|
|
hasListener(listener) {
|
|
return listeners.includes(listener);
|
|
},
|
|
emit(...args) {
|
|
listeners.slice().forEach((listener) => listener(...args));
|
|
},
|
|
listeners
|
|
};
|
|
}
|
|
|
|
function createStorageArea(areaName, initialData, onChangedEvent) {
|
|
let data = clone(initialData) || {};
|
|
|
|
function emitChanges(changes) {
|
|
if (changes && Object.keys(changes).length > 0) {
|
|
onChangedEvent.emit(changes, areaName);
|
|
}
|
|
}
|
|
|
|
return {
|
|
get: vi.fn((keys, callback) => {
|
|
if (callback) callback(pickStorageValues(data, keys));
|
|
}),
|
|
set: vi.fn((items, callback) => {
|
|
const nextItems = items || {};
|
|
const changes = {};
|
|
|
|
Object.keys(nextItems).forEach((key) => {
|
|
const oldValue = clone(data[key]);
|
|
const newValue = clone(nextItems[key]);
|
|
data[key] = newValue;
|
|
changes[key] = { oldValue, newValue };
|
|
});
|
|
|
|
emitChanges(changes);
|
|
if (callback) callback();
|
|
}),
|
|
remove: vi.fn((keys, callback) => {
|
|
const list = Array.isArray(keys) ? keys : [keys];
|
|
const changes = {};
|
|
|
|
list.forEach((key) => {
|
|
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
|
changes[key] = {
|
|
oldValue: clone(data[key]),
|
|
newValue: undefined
|
|
};
|
|
delete data[key];
|
|
}
|
|
});
|
|
|
|
emitChanges(changes);
|
|
if (callback) callback();
|
|
}),
|
|
clear: vi.fn((callback) => {
|
|
const changes = {};
|
|
Object.keys(data).forEach((key) => {
|
|
changes[key] = {
|
|
oldValue: clone(data[key]),
|
|
newValue: undefined
|
|
};
|
|
});
|
|
data = {};
|
|
emitChanges(changes);
|
|
if (callback) callback();
|
|
}),
|
|
_dump() {
|
|
return clone(data);
|
|
}
|
|
};
|
|
}
|
|
|
|
function createChromeMock(options) {
|
|
const config = options || {};
|
|
const storageOnChanged = createChromeEvent();
|
|
const tabsOnActivated = createChromeEvent();
|
|
const tabsOnUpdated = createChromeEvent();
|
|
const runtimeOnMessage = createChromeEvent();
|
|
|
|
const chrome = {
|
|
runtime: {
|
|
lastError: null,
|
|
getManifest: vi.fn(() => clone(config.manifest) || { version: "0.0.0-test" }),
|
|
getURL: vi.fn((relPath) => `moz-extension://${relPath}`),
|
|
onMessage: runtimeOnMessage
|
|
},
|
|
browserAction: {
|
|
setIcon: vi.fn()
|
|
},
|
|
tabs: {
|
|
query: vi.fn((queryInfo, callback) => {
|
|
const tabs = clone(config.tabsQueryResult) || [
|
|
{ id: 1, active: true, url: "https://example.com/" }
|
|
];
|
|
if (callback) callback(tabs);
|
|
}),
|
|
sendMessage: vi.fn((tabId, message, callback) => {
|
|
if (callback) callback(null);
|
|
}),
|
|
executeScript: vi.fn((tabId, details, callback) => {
|
|
if (callback) callback([]);
|
|
}),
|
|
create: vi.fn(),
|
|
onActivated: tabsOnActivated,
|
|
onUpdated: tabsOnUpdated
|
|
},
|
|
storage: {
|
|
onChanged: storageOnChanged,
|
|
sync: null,
|
|
local: null
|
|
}
|
|
};
|
|
|
|
chrome.storage.sync = createStorageArea(
|
|
"sync",
|
|
config.syncData,
|
|
storageOnChanged
|
|
);
|
|
chrome.storage.local = createStorageArea(
|
|
"local",
|
|
config.localData,
|
|
storageOnChanged
|
|
);
|
|
|
|
return chrome;
|
|
}
|
|
|
|
function installCommonWindowMocks() {
|
|
window.open = vi.fn();
|
|
window.close = vi.fn();
|
|
window.requestAnimationFrame = vi.fn((callback) => setTimeout(callback, 0));
|
|
window.cancelAnimationFrame = vi.fn((id) => clearTimeout(id));
|
|
}
|
|
|
|
module.exports = {
|
|
createChromeMock,
|
|
evaluateScript,
|
|
fireDOMContentLoaded,
|
|
flushAsyncWork,
|
|
installCommonWindowMocks,
|
|
loadHtmlFile,
|
|
loadHtmlString,
|
|
readWorkspaceFile,
|
|
workspacePath
|
|
};
|