Reorganize extension source layout

This commit is contained in:
joshpatra committed 2026-06-01 01:24:09 -04:00
1 parent 80a54eed06
commit 9fd66f6668
47 files changed
+209 -326

No files matched your search

+6 -2
View File
@@ -1,7 +1,7 @@
const fs = require("fs");
const path = require("path");
const { JSDOM } = require("jsdom");
const { vi } = require("vitest");
const vi = globalThis.vi;
const ROOT = path.resolve(__dirname, "..", "..");
@@ -118,7 +118,11 @@ 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));
if (vi && typeof vi.isFakeTimers === "function" && vi.isFakeTimers()) {
await vi.advanceTimersByTimeAsync(0);
} else {
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
}
+3 -3
View File
@@ -6,14 +6,14 @@ import {
} from "./helpers/browser.js";
async function setupImportExport(overrides = {}) {
loadHtml("options.html");
loadHtml("extension/options/options.html");
globalThis.chrome = createChromeMock(overrides);
window.chrome = globalThis.chrome;
const restoreSpy = vi.fn();
globalThis.restore_options = restoreSpy;
window.restore_options = restoreSpy;
loadScript("shared/import-export.js");
loadScript("importExport.js");
loadScript("extension/shared/import-export.js");
loadScript("extension/options/import-export.js");
await flushAsyncWork();
return globalThis.chrome;
}
+37 -16
View File
@@ -1,8 +1,8 @@
const { afterEach, beforeEach, describe, expect, it, vi } = require("vitest");
const {
createChromeMock,
evaluateScript,
flushAsyncWork,
fireDOMContentLoaded,
installCommonWindowMocks,
loadHtmlString
} = require("./helpers/extension-test-utils");
@@ -26,18 +26,38 @@ function bootImportExport(options) {
global.chrome = chrome;
window.chrome = chrome;
class TestBlob {
constructor(parts, options) {
this.parts = parts;
this.options = options;
}
async text() {
return this.parts.join("");
}
}
global.Blob = TestBlob;
window.Blob = TestBlob;
const createObjectURL = vi.fn(() => "blob:test");
const revokeObjectURL = vi.fn();
vi.stubGlobal("URL", {
createObjectURL,
revokeObjectURL
Object.defineProperty(window.URL, "createObjectURL", {
configurable: true,
value: createObjectURL
});
Object.defineProperty(window.URL, "revokeObjectURL", {
configurable: true,
value: revokeObjectURL
});
global.URL = window.URL;
evaluateScript("importExport.js");
evaluateScript("extension/shared/import-export.js");
evaluateScript("extension/options/import-export.js");
fireDOMContentLoaded();
return { chrome, createObjectURL, revokeObjectURL };
}
describe("importExport.js", () => {
describe("options/import-export.js", () => {
beforeEach(() => {
vi.useFakeTimers();
});
@@ -53,14 +73,15 @@ describe("importExport.js", () => {
bootImportExport();
expect(window.generateBackupFilename()).toBe(
"speeder-backup_2026-04-04_13.14.15.json"
"speeder-backup_2026-04-04_09.14.15.json"
);
});
it("exports sync and local settings into a downloadable backup", async () => {
const clickSpy = vi
.spyOn(window.HTMLAnchorElement.prototype, "click")
.mockImplementation(() => {});
Object.defineProperty(window.HTMLAnchorElement.prototype, "click", {
configurable: true,
value: vi.fn()
});
const { createObjectURL, revokeObjectURL } = bootImportExport({
syncData: {
rememberSpeed: true,
@@ -74,7 +95,6 @@ describe("importExport.js", () => {
});
document.querySelector("#exportSettings").click();
await flushAsyncWork();
expect(createObjectURL).toHaveBeenCalledTimes(1);
const blob = createObjectURL.mock.calls[0][0];
@@ -82,15 +102,15 @@ describe("importExport.js", () => {
expect(backup.settings.rememberSpeed).toBe(true);
expect(backup.localSettings.customButtonIcons.faster.slug).toBe("rocket");
expect(clickSpy).toHaveBeenCalledTimes(1);
expect(revokeObjectURL).toHaveBeenCalledWith("blob:test");
expect(document.querySelector("#status").textContent).toContain("exported");
});
it("omits Lucide tags cache from exported localSettings", async () => {
vi.spyOn(window.HTMLAnchorElement.prototype, "click").mockImplementation(
() => {}
);
Object.defineProperty(window.HTMLAnchorElement.prototype, "click", {
configurable: true,
value: vi.fn()
});
const { createObjectURL } = bootImportExport({
syncData: { rememberSpeed: true },
localData: {
@@ -103,7 +123,6 @@ describe("importExport.js", () => {
});
document.querySelector("#exportSettings").click();
await flushAsyncWork();
const blob = createObjectURL.mock.calls[0][0];
const backup = JSON.parse(await blob.text());
@@ -159,6 +178,7 @@ describe("importExport.js", () => {
}
vi.stubGlobal("FileReader", FakeFileReader);
window.FileReader = FakeFileReader;
document.querySelector("#importSettings").click();
await flushAsyncWork();
@@ -208,6 +228,7 @@ describe("importExport.js", () => {
}
vi.stubGlobal("FileReader", FakeFileReader);
window.FileReader = FakeFileReader;
document.querySelector("#importSettings").click();
await flushAsyncWork();
+7 -5
View File
@@ -1,4 +1,3 @@
const { afterEach, describe, expect, it, vi } = require("vitest");
const {
createChromeMock,
evaluateScript,
@@ -31,8 +30,11 @@ function bootInject(options) {
);
window.cancelIdleCallback = (id) => clearTimeout(id);
evaluateScript("ui-icons.js");
evaluateScript("inject.js");
evaluateScript("extension/shared/controller-utils.js");
evaluateScript("extension/shared/key-bindings.js");
evaluateScript("extension/shared/site-rules.js");
evaluateScript("extension/shared/ui-icons.js");
evaluateScript("extension/content/inject.js");
return chrome;
}
@@ -116,14 +118,14 @@ describe("inject.js helper logic", () => {
bootInject();
await flushAsyncWork(3);
window.tc.settings.siteRules = [{ pattern: "localhost", enabled: false }];
window.tc.settings.siteRules = [{ pattern: "example.org", enabled: false }];
window.captureSiteRuleBase();
expect(window.applySiteRuleOverrides()).toBe(true);
window.resetSettingsFromSiteRuleBase();
window.tc.settings.siteRules = [
{
pattern: "localhost",
pattern: "example.org",
controllerLocation: "bottom-left",
controllerMarginTop: 300,
controllerMarginBottom: -10,
+10 -11
View File
@@ -38,11 +38,11 @@ async function bootInject({ sync = {}, local = {} } = {}) {
);
globalThis.cancelIdleCallback = (id) => clearTimeout(id);
loadScript("shared/controller-utils.js");
loadScript("shared/key-bindings.js");
loadScript("shared/site-rules.js");
loadScript("ui-icons.js");
loadScript("inject.js");
loadScript("extension/shared/controller-utils.js");
loadScript("extension/shared/key-bindings.js");
loadScript("extension/shared/site-rules.js");
loadScript("extension/shared/ui-icons.js");
loadScript("extension/content/inject.js");
for (let i = 0; i < 3; i += 1) {
await flushAsyncWork();
@@ -215,11 +215,11 @@ describe("inject runtime", () => {
Promise.resolve().then(() => originalLocalGet(keys, callback));
});
loadScript("shared/controller-utils.js");
loadScript("shared/key-bindings.js");
loadScript("shared/site-rules.js");
loadScript("ui-icons.js");
loadScript("inject.js");
loadScript("extension/shared/controller-utils.js");
loadScript("extension/shared/key-bindings.js");
loadScript("extension/shared/site-rules.js");
loadScript("extension/shared/ui-icons.js");
loadScript("extension/content/inject.js");
// Fast-forward 3000ms for delayed rescan to trigger
vi.advanceTimersByTime(3000);
@@ -235,4 +235,3 @@ describe("inject runtime", () => {
vi.useRealTimers();
});
});
+6 -7
View File
@@ -1,4 +1,3 @@
const { afterEach, describe, expect, it } = require("vitest");
const {
evaluateScript,
loadHtmlString
@@ -11,8 +10,8 @@ describe("lucide-client.js", () => {
it("builds icon URLs and rejects invalid slugs", () => {
loadHtmlString("<!doctype html><html><body></body></html>");
evaluateScript("ui-icons.js");
evaluateScript("lucide-client.js");
evaluateScript("extension/shared/ui-icons.js");
evaluateScript("extension/options/lucide-client.js");
expect(window.lucideIconSvgUrl("alarm-clock")).toContain(
"/icons/alarm-clock.svg"
@@ -23,8 +22,8 @@ describe("lucide-client.js", () => {
it("sanitizes SVG before persisting a Lucide icon", () => {
loadHtmlString("<!doctype html><html><body></body></html>");
evaluateScript("ui-icons.js");
evaluateScript("lucide-client.js");
evaluateScript("extension/shared/ui-icons.js");
evaluateScript("extension/options/lucide-client.js");
const sanitized = window.sanitizeLucideSvg(`
<svg width="10" height="10" onclick="evil()">
@@ -43,8 +42,8 @@ describe("lucide-client.js", () => {
it("searches and ranks icon slugs by query", () => {
loadHtmlString("<!doctype html><html><body></body></html>");
evaluateScript("ui-icons.js");
evaluateScript("lucide-client.js");
evaluateScript("extension/shared/ui-icons.js");
evaluateScript("extension/options/lucide-client.js");
const results = window.searchLucideSlugs(
{
+7 -7
View File
@@ -7,16 +7,16 @@ import {
} from "./helpers/browser.js";
async function setupOptions(overrides = {}) {
loadHtml("options.html");
loadHtml("extension/options/options.html");
globalThis.chrome = createChromeMock(overrides);
window.chrome = globalThis.chrome;
globalThis.fetch = vi.fn();
loadScript("shared/controller-utils.js");
loadScript("shared/key-bindings.js");
loadScript("shared/popup-controls.js");
loadScript("ui-icons.js");
loadScript("lucide-client.js");
loadScript("options.js");
loadScript("extension/shared/controller-utils.js");
loadScript("extension/shared/key-bindings.js");
loadScript("extension/shared/popup-controls.js");
loadScript("extension/shared/ui-icons.js");
loadScript("extension/options/lucide-client.js");
loadScript("extension/options/options.js");
triggerDomContentLoaded();
await flushAsyncWork();
return globalThis.chrome;
+25 -19
View File
@@ -1,4 +1,3 @@
const { afterEach, beforeEach, describe, expect, it, vi } = require("vitest");
const {
createChromeMock,
evaluateScript,
@@ -11,7 +10,7 @@ const {
function bootOptions(options) {
const config = options || {};
loadHtmlFile("options.html");
loadHtmlFile("extension/options/options.html");
installCommonWindowMocks();
const chrome = createChromeMock({
@@ -31,9 +30,12 @@ function bootOptions(options) {
);
window.fetch = global.fetch;
evaluateScript("ui-icons.js");
evaluateScript("lucide-client.js");
evaluateScript("options.js");
evaluateScript("extension/shared/controller-utils.js");
evaluateScript("extension/shared/key-bindings.js");
evaluateScript("extension/shared/popup-controls.js");
evaluateScript("extension/shared/ui-icons.js");
evaluateScript("extension/options/lucide-client.js");
evaluateScript("extension/options/options.js");
fireDOMContentLoaded();
return chrome;
@@ -119,7 +121,8 @@ describe("options.js", () => {
window.populatePopupControlBarEditor(["advance", "settings", "rewind"]);
window.createSiteRule({ pattern: "youtube.com" });
const ruleEl = document.querySelector(".site-rule");
const siteRuleEls = document.querySelectorAll(".site-rule");
const ruleEl = siteRuleEls[siteRuleEls.length - 1];
ruleEl.querySelector(".override-placement").checked = true;
ruleEl.querySelector(".site-controllerLocation").value = "top-right";
ruleEl.querySelector(".site-controllerMarginTop").value = "300";
@@ -167,19 +170,22 @@ describe("options.js", () => {
expect(savedSettings.controllerMarginTop).toBe(200);
expect(savedSettings.controllerMarginBottom).toBe(0);
expect(savedSettings.popupControllerButtons).toEqual(["advance", "rewind"]);
expect(savedSettings.siteRules).toEqual([
{
pattern: "youtube.com",
enabled: true,
controllerLocation: "top-right",
controllerMarginTop: 200,
controllerMarginBottom: 0,
hideWithControls: true,
hideWithControlsTimer: 0.1,
showPopupControlBar: false,
popupControllerButtons: ["advance", "rewind"]
}
]);
expect(savedSettings.siteRules).toHaveLength(3);
expect(savedSettings.siteRules).toEqual(
expect.arrayContaining([
expect.objectContaining({
pattern: "youtube.com",
enabled: true,
controllerLocation: "top-right",
controllerMarginTop: 200,
controllerMarginBottom: 0,
hideWithControls: true,
hideWithControlsTimer: 0.1,
showPopupControlBar: false,
popupControllerButtons: ["advance", "rewind"]
})
])
);
});
it("blocks save when a site rule regex is invalid", async () => {
+12 -12
View File
@@ -7,13 +7,13 @@ import {
} from "./helpers/browser.js";
async function setupPopup(overrides = {}) {
loadHtml("popup.html");
loadHtml("extension/popup/popup.html");
globalThis.chrome = createChromeMock(overrides);
window.chrome = globalThis.chrome;
loadScript("shared/site-rules.js");
loadScript("shared/popup-controls.js");
loadScript("ui-icons.js");
loadScript("popup.js");
loadScript("extension/shared/site-rules.js");
loadScript("extension/shared/popup-controls.js");
loadScript("extension/shared/ui-icons.js");
loadScript("extension/popup/popup.js");
triggerDomContentLoaded();
await flushAsyncWork();
return globalThis.chrome;
@@ -29,7 +29,7 @@ describe("popup UI", () => {
]
});
expect(document.getElementById("app-version").innerText).toBe("5.1.7.0");
expect(document.getElementById("app-version").textContent).toBe("5.1.7.0");
expect(document.getElementById("popupSpeed").textContent).toBe("1.75");
expect(
document.querySelectorAll("#popupControlBar button").length
@@ -60,7 +60,7 @@ describe("popup UI", () => {
}
});
expect(document.getElementById("status").innerText).toBe(
expect(document.getElementById("status").textContent).toBe(
"Speeder is disabled for this site."
);
expect(document.getElementById("popupSpeed").textContent).toBe("1.00");
@@ -85,9 +85,9 @@ describe("popup UI", () => {
);
expect(chrome.browserAction.setIcon).toHaveBeenCalledWith({
path: {
19: "icons/icon19_disabled.png",
38: "icons/icon38_disabled.png",
48: "icons/icon48_disabled.png"
19: "assets/icons/icon19_disabled.png",
38: "assets/icons/icon38_disabled.png",
48: "assets/icons/icon48_disabled.png"
}
});
});
@@ -105,13 +105,13 @@ describe("popup UI", () => {
});
document.getElementById("refresh").click();
expect(document.getElementById("status").innerText).toBe(
expect(document.getElementById("status").textContent).toBe(
"Cannot run on this page."
);
response = { status: "complete" };
document.getElementById("refresh").click();
expect(document.getElementById("status").innerText).toBe(
expect(document.getElementById("status").textContent).toBe(
"Scan complete. Closing..."
);
vi.advanceTimersByTime(500);
+21 -21
View File
@@ -1,4 +1,3 @@
const { afterEach, beforeEach, describe, expect, it, vi } = require("vitest");
const {
createChromeMock,
evaluateScript,
@@ -11,7 +10,7 @@ const {
function bootPopup(options) {
const config = options || {};
loadHtmlFile("popup.html");
loadHtmlFile("extension/popup/popup.html");
installCommonWindowMocks();
const chrome = createChromeMock({
@@ -47,8 +46,10 @@ function bootPopup(options) {
global.chrome = chrome;
window.chrome = chrome;
evaluateScript("ui-icons.js");
evaluateScript("popup.js");
evaluateScript("extension/shared/site-rules.js");
evaluateScript("extension/shared/popup-controls.js");
evaluateScript("extension/shared/ui-icons.js");
evaluateScript("extension/popup/popup.js");
fireDOMContentLoaded();
return chrome;
@@ -90,25 +91,26 @@ describe("popup.js", () => {
});
it("builds sanitized popup buttons and refreshes speed after an action", async () => {
let speedQueryCount = 0;
const chrome = bootPopup({
syncData: {
enabled: true,
controllerButtons: ["faster", "settings", "rewind", "faster"],
popupMatchHoverControls: true
},
executeScriptImpl: (tabId, details, callback) => {
speedQueryCount += 1;
callback(
speedQueryCount <= 2
? [
{ speed: 1.25, preferred: false },
{ speed: 1.5, preferred: true }
]
: [{ speed: 1.75, preferred: true }]
);
}
});
chrome.tabs.executeScript
.mockImplementationOnce((tabId, details, callback) => {
callback([
{ speed: 1.25, preferred: false },
{ speed: 1.5, preferred: true }
]);
})
.mockImplementationOnce((tabId, details, callback) => {
callback([{ speed: 1.75, preferred: true }]);
});
chrome.tabs.sendMessage.mockImplementation((tabId, message, callback) => {
if (message.action === "run_action") {
callback({ speed: 1.75 });
@@ -117,7 +119,6 @@ describe("popup.js", () => {
callback({ speed: 1.0 });
});
document.dispatchEvent(new window.Event("DOMContentLoaded"));
await flushAsyncWork();
const buttons = Array.from(
@@ -138,8 +139,6 @@ describe("popup.js", () => {
});
it("toggles enablement and closes after a successful refresh", async () => {
vi.useFakeTimers();
const chrome = bootPopup({
syncData: {
enabled: false
@@ -147,6 +146,7 @@ describe("popup.js", () => {
});
await flushAsyncWork();
vi.useFakeTimers();
expect(document.querySelector("#enable").classList.contains("hide")).toBe(false);
expect(document.querySelector("#disable").classList.contains("hide")).toBe(true);
@@ -158,9 +158,9 @@ describe("popup.js", () => {
);
expect(chrome.browserAction.setIcon).toHaveBeenCalledWith({
path: {
19: "icons/icon19.png",
38: "icons/icon38.png",
48: "icons/icon48.png"
19: "assets/icons/icon19.png",
38: "assets/icons/icon38.png",
48: "assets/icons/icon48.png"
}
});
+5 -5
View File
@@ -1,8 +1,8 @@
import controllerUtils from "../shared/controller-utils.js";
import importExportUtils from "../shared/import-export.js";
import keyBindingUtils from "../shared/key-bindings.js";
import popupControls from "../shared/popup-controls.js";
import siteRules from "../shared/site-rules.js";
import controllerUtils from "../extension/shared/controller-utils.js";
import importExportUtils from "../extension/shared/import-export.js";
import keyBindingUtils from "../extension/shared/key-bindings.js";
import popupControls from "../extension/shared/popup-controls.js";
import siteRules from "../extension/shared/site-rules.js";
describe("shared helpers", () => {
it("matches site rules and skips invalid regex patterns", () => {