Files
speeder/tests/options.spec.js
T

486 lines
17 KiB
JavaScript

const {
createChromeMock,
evaluateScript,
fireDOMContentLoaded,
flushAsyncWork,
installCommonWindowMocks,
loadHtmlFile
} = require("./helpers/extension-test-utils");
function bootOptions(options) {
const config = options || {};
loadHtmlFile("extension/options/options.html");
installCommonWindowMocks();
const chrome = createChromeMock({
manifest: { version: "5.1.7.0" },
syncData: config.syncData,
localData: config.localData
});
global.chrome = chrome;
window.chrome = chrome;
global.fetch = vi.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({}),
text: () => Promise.resolve("<svg></svg>")
})
);
window.fetch = global.fetch;
evaluateScript("extension/shared/controller-utils.js");
evaluateScript("extension/shared/key-bindings.js");
evaluateScript("extension/shared/settings-core.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;
}
describe("options.js", () => {
afterEach(() => {
vi.useRealTimers();
vi.unstubAllGlobals();
delete global.chrome;
delete global.fetch;
});
it("rejects invalid zero-valued speed steps instead of saving broken shortcuts", async () => {
const chrome = bootOptions();
await flushAsyncWork(3);
const fasterRow = Array.from(
document.querySelectorAll("#customs .shortcut-row")
).find((row) => row.dataset.action === "faster");
fasterRow.querySelector(".customValue").value = "0";
chrome.storage.sync.set.mockClear();
window.save_options();
expect(document.getElementById("status").textContent).toContain(
"must be greater than 0"
);
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
});
it("automatically saves changed settings", async () => {
const chrome = bootOptions({ syncData: { rememberSpeed: false } });
await flushAsyncWork(3);
vi.useFakeTimers();
chrome.storage.sync.set.mockClear();
const rememberSpeed = document.getElementById("rememberSpeed");
rememberSpeed.checked = true;
rememberSpeed.dispatchEvent(new Event("change", { bubbles: true }));
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(300);
expect(chrome.storage.sync._dump().rememberSpeed).toBe(true);
expect(document.getElementById("status").textContent).toBe("Auto-saved");
});
it("copies privacy-safe diagnostics from settings", async () => {
bootOptions({
syncData: {
enabled: false,
rememberSpeed: true,
siteRules: [
{
title: "Private account",
pattern: "secret.example/private-token",
enabled: true
}
]
}
});
const writeText = vi.fn(() => Promise.resolve());
Object.defineProperty(navigator, "clipboard", {
configurable: true,
value: { writeText }
});
await flushAsyncWork(3);
document.getElementById("copyDiagnostics").click();
await flushAsyncWork(3);
const reportText = writeText.mock.calls[0][0];
const report = JSON.parse(reportText);
expect(report.globalSettings.enabled).toBe(false);
expect(report.globalSettings.rememberSpeed).toBe(true);
expect(report.page).toEqual({ protocol: null, hostname: null });
expect(report.matchedSiteRule.matched).toBe(false);
expect(report.frame).toBeNull();
expect(reportText).not.toContain("Private account");
expect(reportText).not.toContain("private-token");
expect(document.getElementById("status").textContent).toContain("copied");
});
it("does not partially save options when a required site shortcut is invalid", async () => {
const chrome = bootOptions({ syncData: { rememberSpeed: false } });
await flushAsyncWork(3);
window.createSiteRule({
pattern: "example.org",
shortcuts: [
{
action: "faster",
code: "KeyD",
value: 0.1,
disabled: false
}
]
});
const rule = document.getElementById("siteRulesContainer").lastElementChild;
rule.querySelector('.shortcut-row[data-action="faster"] .customKey').vscBinding =
null;
document.getElementById("rememberSpeed").checked = true;
chrome.storage.sync.set.mockClear();
chrome.storage.sync.remove.mockClear();
window.save_options();
expect(document.getElementById("status").textContent).toContain(
"cannot be empty"
);
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
expect(chrome.storage.sync.remove).not.toHaveBeenCalled();
expect(chrome.storage.sync._dump().rememberSpeed).toBe(false);
});
it("preserves the supported unexposed subtitle nudge amount when saving", async () => {
const chrome = bootOptions({ syncData: { subtitleNudgeAmount: 0.004 } });
await flushAsyncWork(3);
window.save_options();
expect(
window.vscExpandStoredSettings(chrome.storage.sync._dump())
.subtitleNudgeAmount
).toBe(0.004);
});
it("does not turn a blank per-site preferred speed into the 1.8 fast default", async () => {
const chrome = bootOptions();
await flushAsyncWork(3);
const shortsRule = Array.from(document.querySelectorAll(".site-rule")).find(
(rule) => rule.querySelector(".site-title").value === "YouTube Shorts"
);
expect(shortsRule).toBeTruthy();
expect(shortsRule.querySelector(".site-preferredSpeed").value).toBe("");
window.save_options();
const expanded = window.vscExpandStoredSettings(chrome.storage.sync._dump());
const savedShortsRule = expanded.siteRules.find(
(rule) => rule.title === "YouTube Shorts"
);
expect(savedShortsRule.preferredSpeed).toBeUndefined();
});
it("keeps the loaded UI and blocks saves when a settings reload fails", async () => {
const chrome = bootOptions({
syncData: {
rememberSpeed: true,
siteRules: [{ pattern: "example.org", enabled: true }]
}
});
await flushAsyncWork(3);
const originalRuleCount = document.querySelectorAll(".site-rule").length;
chrome.storage.sync.get.mockImplementationOnce(function (_keys, callback) {
chrome.runtime.lastError = { message: "temporary read failure" };
callback({});
chrome.runtime.lastError = null;
});
chrome.storage.sync.set.mockClear();
chrome.storage.sync.remove.mockClear();
window.restore_options();
expect(document.getElementById("rememberSpeed").checked).toBe(true);
expect(document.querySelectorAll(".site-rule")).toHaveLength(
originalRuleCount
);
expect(document.getElementById("save").disabled).toBe(true);
expect(document.getElementById("status").textContent).toContain(
"temporary read failure"
);
document.getElementById("rememberSpeed").checked = false;
window.save_options();
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
expect(chrome.storage.sync.remove).not.toHaveBeenCalled();
expect(chrome.storage.sync._dump().rememberSpeed).toBe(true);
expect(document.getElementById("status").textContent).toContain(
"Settings have not loaded"
);
});
it("does not clear local data or live rows when the sync reset fails", async () => {
const chrome = bootOptions({
syncData: {
keyBindings: [
{ action: "display", code: "KeyV", value: 0, predefined: true },
{ action: "pause", code: "KeyQ", value: 0, predefined: false }
]
},
localData: {
customButtonIcons: { faster: { slug: "rocket" } },
rememberedSpeeds: { "https://example.org/video.mp4": 1.5 }
}
});
await flushAsyncWork(3);
expect(
document.querySelector('.shortcut-row.customs[data-action="pause"]')
).not.toBeNull();
chrome.storage.local.remove.mockClear();
chrome.storage.sync.remove.mockImplementationOnce(function (_keys, callback) {
chrome.runtime.lastError = { message: "sync reset failed" };
callback();
chrome.runtime.lastError = null;
});
window.restore_defaults();
await flushAsyncWork(3);
expect(chrome.storage.local.remove).not.toHaveBeenCalled();
expect(chrome.storage.local._dump().customButtonIcons).toBeDefined();
expect(chrome.storage.local._dump().rememberedSpeeds).toBeDefined();
expect(
document.querySelector('.shortcut-row.customs[data-action="pause"]')
).not.toBeNull();
expect(document.getElementById("save").disabled).toBe(false);
expect(document.getElementById("status").textContent).toContain(
"sync reset failed"
);
});
it("reports a local reset failure after resetting sync defaults", async () => {
const chrome = bootOptions({
syncData: { rememberSpeed: true, lastSpeed: 1.75 },
localData: { customButtonIcons: { faster: { slug: "rocket" } } }
});
await flushAsyncWork(3);
chrome.storage.local.remove.mockImplementationOnce(function (_keys, callback) {
chrome.runtime.lastError = { message: "local reset failed" };
callback();
chrome.runtime.lastError = null;
});
window.restore_defaults();
await flushAsyncWork(3);
expect(
window.vscExpandStoredSettings(chrome.storage.sync._dump()).rememberSpeed
).toBe(false);
expect(chrome.storage.sync._dump().lastSpeed).toBe(1);
expect(chrome.storage.local._dump().customButtonIcons).toBeDefined();
expect(document.getElementById("save").disabled).toBe(false);
expect(document.getElementById("status").textContent).toContain(
"local reset failed"
);
});
it("does not remove local data when the speed reset marker cannot be written", async () => {
const chrome = bootOptions({
localData: {
customButtonIcons: { faster: { slug: "rocket" } },
rememberedSpeeds: { "https://example.org/video.mp4": 1.5 }
}
});
await flushAsyncWork(3);
chrome.storage.local.remove.mockClear();
chrome.storage.local.set.mockImplementationOnce(function (_items, callback) {
chrome.runtime.lastError = { message: "reset marker failed" };
callback();
chrome.runtime.lastError = null;
});
window.restore_defaults();
await flushAsyncWork(3);
expect(chrome.storage.local.remove).not.toHaveBeenCalled();
expect(chrome.storage.local._dump().customButtonIcons).toBeDefined();
expect(chrome.storage.local._dump().rememberedSpeeds).toBeDefined();
expect(document.getElementById("status").textContent).toContain(
"reset marker failed"
);
});
it("restores saved settings, bindings, site rules, and popup bar order", async () => {
bootOptions({
syncData: {
rememberSpeed: true,
forceLastSavedSpeed: true,
controllerLocation: "middle-right",
controllerOpacity: 0.75,
controllerMarginTop: 22,
controllerMarginBottom: 14,
popupMatchHoverControls: false,
controllerButtons: ["rewind", "fast", "display"],
popupControllerButtons: ["advance", "settings", "rewind", "advance"],
keyBindings: [
{ action: "display", code: "KeyV", value: 0, predefined: true },
{ action: "pause", code: "KeyQ", value: 0, predefined: false }
],
siteRules: [
{
pattern: "youtube.com",
enabled: false,
controllerMarginTop: 12,
popupControllerButtons: ["advance", "settings", "rewind"]
}
]
}
});
await flushAsyncWork(3);
expect(document.getElementById("app-version").textContent).toBe("5.1.7.0");
expect(document.getElementById("rememberSpeed").checked).toBe(true);
expect(document.getElementById("forceLastSavedSpeed").checked).toBe(true);
expect(document.getElementById("controllerLocation").value).toBe(
"middle-right"
);
expect(document.getElementById("controllerOpacity").value).toBe("0.75");
expect(document.getElementById("controllerMarginTop").value).toBe("22");
expect(document.getElementById("popupMatchHoverControls").checked).toBe(false);
expect(
document.getElementById("popupCbEditorWrap").classList.contains(
"cb-editor-disabled"
)
).toBe(false);
const popupButtons = Array.from(
document.querySelectorAll("#popupControlBarActive .cb-block")
).map((block) => block.dataset.buttonId);
expect(popupButtons).toEqual(["advance", "rewind"]);
expect(
document.querySelector('.shortcut-row.customs[data-action="pause"]')
).not.toBeNull();
expect(document.querySelectorAll(".site-rule")).toHaveLength(1);
expect(document.querySelector(".site-rule .site-enabled").checked).toBe(false);
});
it("saves normalized settings and site rule overrides", async () => {
const chrome = bootOptions();
await flushAsyncWork(3);
document.getElementById("rememberSpeed").checked = true;
document.getElementById("hideWithControlsTimer").value = "99";
document.getElementById("controllerLocation").value = "bottom-left";
document.getElementById("controllerOpacity").value = "0.65";
document.getElementById("controllerMarginTop").value = "250";
document.getElementById("controllerMarginBottom").value = "-5";
document.getElementById("popupMatchHoverControls").checked = false;
document.getElementById("showPopupControlBar").checked = true;
window.populatePopupControlBarEditor(["advance", "settings", "rewind"]);
window.createSiteRule({ pattern: "youtube.com" });
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";
ruleEl.querySelector(".site-controllerMarginBottom").value = "-10";
ruleEl.querySelector(".override-autohide").checked = true;
ruleEl.querySelector(".site-hideWithControls").checked = true;
ruleEl.querySelector(".site-hideWithControlsTimer").value = "0";
ruleEl.querySelector(".override-popup-controlbar").checked = true;
ruleEl.querySelector(".site-showPopupControlBar").checked = false;
window.populateControlBarZones(
ruleEl.querySelector(".site-popup-cb-active"),
ruleEl.querySelector(".site-popup-cb-available"),
["advance", "settings", "rewind"],
function (id) {
return id !== "settings";
}
);
window.save_options();
const savedSettings = window.vscExpandStoredSettings(
chrome.storage.sync._dump()
);
expect(savedSettings.rememberSpeed).toBe(true);
expect(savedSettings.hideWithControlsTimer).toBe(15);
expect(savedSettings.controllerLocation).toBe("bottom-left");
expect(savedSettings.controllerMarginTop).toBe(200);
expect(savedSettings.controllerMarginBottom).toBe(0);
expect(savedSettings.popupControllerButtons).toEqual(["advance", "rewind"]);
expect(savedSettings.siteRules).toHaveLength(4);
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 () => {
const chrome = bootOptions();
await flushAsyncWork(3);
window.createSiteRule({ pattern: "/[abc/" });
window.save_options();
expect(document.getElementById("status").textContent).toContain(
"Invalid site rule regex"
);
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
});
it("adds shortcuts from the selector and records key input states", async () => {
bootOptions();
await flushAsyncWork(3);
const selector = document.getElementById("addShortcutSelector");
selector.value = "pause";
selector.dispatchEvent(new window.Event("change", { bubbles: true }));
const row = document.querySelector('.shortcut-row.customs[data-action="pause"]');
expect(row).not.toBeNull();
const keyInput = row.querySelector(".customKey");
keyInput.dispatchEvent(
new window.KeyboardEvent("keydown", {
key: "q",
code: "KeyQ",
bubbles: true
})
);
expect(keyInput.vscBinding.code).toBe("KeyQ");
expect(keyInput.value).toBe("Q");
keyInput.dispatchEvent(
new window.KeyboardEvent("keydown", {
key: "Escape",
code: "Escape",
bubbles: true
})
);
expect(keyInput.vscBinding.disabled).toBe(true);
expect(selector.disabled).toBe(false);
});
});