Add configurable skip delay (0-30s) for Skip Intro and Skip Credits

This commit is contained in:
Justin Oro
2026-09-02 16:50:04 -07:00
parent 0f1f2a309e
commit b2082d0624
9 changed files with 333 additions and 21 deletions
+104
View File
@@ -0,0 +1,104 @@
const test = require("node:test");
const assert = require("node:assert");
const { loadContentScript, wait } = require("./helpers");
test("zero delay still clicks immediately (default behavior preserved)", async () => {
const { window } = loadContentScript({
url: "https://app.plex.tv/web/index.html",
html: `<!DOCTYPE html><html><body><button>Skip Intro</button></body></html>`,
settings: { introDelaySeconds: 0 }
});
await wait(50);
assert.strictEqual(window.__clickedElements.length, 1);
});
test("does not click immediately when a delay is configured, but clicks once it elapses", async () => {
const { window } = loadContentScript({
url: "https://app.plex.tv/web/index.html",
html: `<!DOCTYPE html><html><body><button>Skip Credits</button></body></html>`,
settings: { creditsDelaySeconds: 1 }
});
await wait(50);
assert.strictEqual(window.__clickedElements.length, 0, "should not click before the delay elapses");
await wait(1100);
assert.strictEqual(window.__clickedElements.length, 1, "should click once the delay elapses");
});
test("does not schedule duplicate clicks while a delay is already pending", async () => {
const { window } = loadContentScript({
url: "https://app.plex.tv/web/index.html",
html: `<!DOCTYPE html><html><body><button id="skip">Skip Intro</button></body></html>`,
settings: { introDelaySeconds: 1 }
});
await wait(50);
window.document.body.appendChild(window.document.createElement("div"));
await wait(50);
window.document.body.appendChild(window.document.createElement("div"));
await wait(1200);
assert.strictEqual(window.__clickedElements.length, 1);
});
test("cancels a scheduled click if its toggle is turned off mid-delay", async () => {
const { window, fireStorageChange } = loadContentScript({
url: "https://app.plex.tv/web/index.html",
html: `<!DOCTYPE html><html><body><button>Skip Intro</button></body></html>`,
settings: { introDelaySeconds: 1 }
});
await wait(50);
fireStorageChange({ skipIntro: false });
await wait(1200);
assert.strictEqual(window.__clickedElements.length, 0);
});
test("does not click if the button is removed from the page before the delay elapses", async () => {
const { window } = loadContentScript({
url: "https://app.plex.tv/web/index.html",
html: `<!DOCTYPE html><html><body><button id="skip">Skip Intro</button></body></html>`,
settings: { introDelaySeconds: 1 }
});
await wait(50);
window.document.getElementById("skip").remove();
await wait(1200);
assert.strictEqual(window.__clickedElements.length, 0);
});
test("does not click if the delayed button is hidden before the delay elapses", async () => {
const { window } = loadContentScript({
url: "https://app.plex.tv/web/index.html",
html: `<!DOCTYPE html><html><body><button id="skip">Skip Intro</button></body></html>`,
settings: { introDelaySeconds: 1 }
});
await wait(50);
window.document.getElementById("skip").style.display = "none";
await wait(1200);
assert.strictEqual(window.__clickedElements.length, 0);
});
test("intro and credits delays run independently", async () => {
const { window } = loadContentScript({
url: "https://app.plex.tv/web/index.html",
html: `<!DOCTYPE html><html><body>
<button id="intro">Skip Intro</button>
<button id="credits">Skip Credits</button>
</body></html>`,
settings: { introDelaySeconds: 0, creditsDelaySeconds: 1 }
});
await wait(50);
assert.strictEqual(window.__clickedElements.length, 1, "Skip Intro should click immediately");
assert.strictEqual(window.__clickedElements[0], window.document.getElementById("intro"));
await wait(1100);
assert.strictEqual(window.__clickedElements.length, 2, "Skip Credits should click after its own delay");
});
+26 -3
View File
@@ -10,7 +10,13 @@ function loadContentScript({ url, html, settings }) {
const window = dom.window;
const storedSettings = Object.assign(
{ plexUrl: "https://app.plex.tv", skipIntro: true, skipCredits: true },
{
plexUrl: "https://app.plex.tv",
skipIntro: true,
skipCredits: true,
introDelaySeconds: 0,
creditsDelaySeconds: 0
},
settings || {}
);
const changeListeners = [];
@@ -57,7 +63,13 @@ function loadPopupScript({ initialSettings } = {}) {
const dom = new JSDOM(html, { url: "chrome-extension://fake/popup.html", runScripts: "dangerously" });
const window = dom.window;
const storedSettings = Object.assign(
{ plexUrl: "https://app.plex.tv", skipIntro: true, skipCredits: true },
{
plexUrl: "https://app.plex.tv",
skipIntro: true,
skipCredits: true,
introDelaySeconds: 0,
creditsDelaySeconds: 0
},
initialSettings || {}
);
const savedCalls = [];
@@ -82,8 +94,19 @@ function loadPopupScript({ initialSettings } = {}) {
return { dom, window, savedCalls, storedSettings };
}
function loadUtils() {
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>", {
url: "https://example.com",
runScripts: "dangerously"
});
const window = dom.window;
const utilsCode = fs.readFileSync(path.join(__dirname, "..", "utils.js"), "utf8");
window.eval(utilsCode);
return window;
}
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
module.exports = { loadContentScript, loadPopupScript, wait };
module.exports = { loadContentScript, loadPopupScript, loadUtils, wait };
+42 -1
View File
@@ -2,7 +2,7 @@ const test = require("node:test");
const assert = require("node:assert");
const { loadPopupScript, wait } = require("./helpers");
test("defaults to app.plex.tv preset with both toggles on", async () => {
test("defaults to app.plex.tv preset with both toggles on and delays at 0s", async () => {
const { window } = loadPopupScript();
await wait(20);
@@ -10,6 +10,47 @@ test("defaults to app.plex.tv preset with both toggles on", async () => {
assert.strictEqual(window.document.getElementById("skipIntro").checked, true);
assert.strictEqual(window.document.getElementById("skipCredits").checked, true);
assert.strictEqual(window.document.getElementById("customUrlRow").style.display, "none");
assert.strictEqual(window.document.getElementById("introDelay").value, "0");
assert.strictEqual(window.document.getElementById("creditsDelay").value, "0");
assert.strictEqual(window.document.getElementById("introDelayValue").textContent, "0s");
assert.strictEqual(window.document.getElementById("creditsDelayValue").textContent, "0s");
});
test("loads a previously saved delay value onto the sliders", async () => {
const { window } = loadPopupScript({
initialSettings: { introDelaySeconds: 3, creditsDelaySeconds: 12 }
});
await wait(20);
assert.strictEqual(window.document.getElementById("introDelay").value, "3");
assert.strictEqual(window.document.getElementById("introDelayValue").textContent, "3s");
assert.strictEqual(window.document.getElementById("creditsDelay").value, "12");
assert.strictEqual(window.document.getElementById("creditsDelayValue").textContent, "12s");
});
test("disables a delay slider when its skip toggle is off", async () => {
const { window } = loadPopupScript({
initialSettings: { skipCredits: false }
});
await wait(20);
assert.strictEqual(window.document.getElementById("introDelay").disabled, false);
assert.strictEqual(window.document.getElementById("creditsDelay").disabled, true);
});
test("saving persists the configured delay values", async () => {
const { window, savedCalls } = loadPopupScript();
await wait(20);
window.document.getElementById("introDelay").value = "5";
window.document.getElementById("creditsDelay").value = "20";
window.document.getElementById("saveBtn").click();
await wait(20);
assert.strictEqual(savedCalls.length, 1);
assert.strictEqual(savedCalls[0].introDelaySeconds, 5);
assert.strictEqual(savedCalls[0].creditsDelaySeconds, 20);
});
test("shows custom URL field pre-filled when a custom URL is already saved", async () => {
+30
View File
@@ -0,0 +1,30 @@
const test = require("node:test");
const assert = require("node:assert");
const { loadUtils } = require("./helpers");
test("clampDelaySeconds keeps in-range integer values as-is", () => {
const window = loadUtils();
assert.strictEqual(window.clampDelaySeconds(0), 0);
assert.strictEqual(window.clampDelaySeconds(5), 5);
assert.strictEqual(window.clampDelaySeconds(30), 30);
});
test("clampDelaySeconds clamps below 0 and above 30", () => {
const window = loadUtils();
assert.strictEqual(window.clampDelaySeconds(-5), 0);
assert.strictEqual(window.clampDelaySeconds(45), 30);
});
test("clampDelaySeconds rounds fractional values", () => {
const window = loadUtils();
assert.strictEqual(window.clampDelaySeconds(15.6), 16);
assert.strictEqual(window.clampDelaySeconds(15.4), 15);
});
test("clampDelaySeconds falls back to 0 for non-numeric input", () => {
const window = loadUtils();
assert.strictEqual(window.clampDelaySeconds("abc"), 0);
assert.strictEqual(window.clampDelaySeconds(undefined), 0);
assert.strictEqual(window.clampDelaySeconds(null), 0);
assert.strictEqual(window.clampDelaySeconds(NaN), 0);
});