Initial commit: Plex Auto Skip Chrome extension

This commit is contained in:
Justin Oro
2026-08-16 22:55:33 -07:00
commit 0f1f2a309e
14 changed files with 1522 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
const test = require("node:test");
const assert = require("node:assert");
const { loadContentScript, wait } = require("./helpers");
test("clicks a Skip Intro button on app.plex.tv", async () => {
const { window } = loadContentScript({
url: "https://app.plex.tv/web/index.html",
html: `<!DOCTYPE html><html><body><button>Skip Intro</button></body></html>`
});
await wait(50);
assert.strictEqual(window.__clickedElements.length, 1);
});
test("clicks a Skip Credits button on app.plex.tv", async () => {
const { window } = loadContentScript({
url: "https://app.plex.tv/web/index.html",
html: `<!DOCTYPE html><html><body><button>Skip Credits</button></body></html>`
});
await wait(50);
assert.strictEqual(window.__clickedElements.length, 1);
});
test("does nothing on a non-matching host", async () => {
const { window } = loadContentScript({
url: "https://example.com/web/index.html",
html: `<!DOCTYPE html><html><body><button>Skip Intro</button></body></html>`
});
await wait(50);
assert.strictEqual(window.__clickedElements.length, 0);
});
test("respects skipIntro:false toggle", async () => {
const { window } = loadContentScript({
url: "https://app.plex.tv/web/index.html",
html: `<!DOCTYPE html><html><body><button>Skip Intro</button></body></html>`,
settings: { skipIntro: false, skipCredits: true }
});
await wait(50);
assert.strictEqual(window.__clickedElements.length, 0);
});
test("matches a custom configured URL by hostname", async () => {
const { window } = loadContentScript({
url: "http://192.168.1.50:32400/web/index.html",
html: `<!DOCTYPE html><html><body><button>Skip Intro</button></body></html>`,
settings: { plexUrl: "http://192.168.1.50:32400/web" }
});
await wait(50);
assert.strictEqual(window.__clickedElements.length, 1);
});
test("clicks a button added dynamically after page load", async () => {
const { window } = loadContentScript({
url: "https://app.plex.tv/web/index.html",
html: `<!DOCTYPE html><html><body></body></html>`
});
await wait(20);
const btn = window.document.createElement("button");
btn.textContent = "Skip Intro";
window.document.body.appendChild(btn);
await wait(50);
assert.strictEqual(window.__clickedElements.length, 1);
});
test("does not click an unrelated button", async () => {
const { window } = loadContentScript({
url: "https://app.plex.tv/web/index.html",
html: `<!DOCTYPE html><html><body><button>Watch Now</button></body></html>`
});
await wait(50);
assert.strictEqual(window.__clickedElements.length, 0);
});
test("real Plex markup: skip button includes a countdown suffix", async () => {
const { window } = loadContentScript({
url: "https://app.plex.tv/web/index.html",
html: `<!DOCTYPE html><html><body><button><span>Skip Intro</span><span> 0:05</span></button></body></html>`
});
await wait(50);
assert.strictEqual(window.__clickedElements.length, 1);
});
test("same DOM node reused for a later segment still gets clicked after cooldown", 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>`
});
await wait(50);
assert.strictEqual(window.__clickedElements.length, 1, "first segment's skip button should be clicked");
const btn = window.document.getElementById("skip");
btn.textContent = "";
await wait(1700);
btn.textContent = "Skip Intro";
await wait(50);
assert.strictEqual(
window.__clickedElements.length,
2,
"the same reused button should be clickable again after the cooldown"
);
});
test("does not click a hidden skip button", async () => {
const { window } = loadContentScript({
url: "https://app.plex.tv/web/index.html",
html: `<!DOCTYPE html><html><body><button style="display:none">Skip Intro</button></body></html>`
});
await wait(50);
assert.strictEqual(window.__clickedElements.length, 0);
});
+89
View File
@@ -0,0 +1,89 @@
const fs = require("fs");
const path = require("path");
const { JSDOM } = require("jsdom");
function loadContentScript({ url, html, settings }) {
const dom = new JSDOM(html || "<!DOCTYPE html><html><body></body></html>", {
url: url,
runScripts: "dangerously"
});
const window = dom.window;
const storedSettings = Object.assign(
{ plexUrl: "https://app.plex.tv", skipIntro: true, skipCredits: true },
settings || {}
);
const changeListeners = [];
window.__clickedElements = [];
const originalClick = window.HTMLElement.prototype.click;
window.HTMLElement.prototype.click = function () {
window.__clickedElements.push(this);
return originalClick.call(this);
};
window.chrome = {
storage: {
sync: {
get: (defaults, cb) => cb(Object.assign({}, defaults, storedSettings)),
set: (obj, cb) => {
Object.assign(storedSettings, obj);
if (cb) cb();
}
},
onChanged: {
addListener: (fn) => changeListeners.push(fn)
}
}
};
const utilsCode = fs.readFileSync(path.join(__dirname, "..", "utils.js"), "utf8");
const contentCode = fs.readFileSync(path.join(__dirname, "..", "content.js"), "utf8");
window.eval(utilsCode + "\n" + contentCode);
function fireStorageChange(changes) {
const wrapped = {};
Object.keys(changes).forEach((k) => {
wrapped[k] = { newValue: changes[k] };
});
changeListeners.forEach((fn) => fn(wrapped, "sync"));
}
return { dom, window, fireStorageChange };
}
function loadPopupScript({ initialSettings } = {}) {
const html = fs.readFileSync(path.join(__dirname, "..", "popup.html"), "utf8");
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 },
initialSettings || {}
);
const savedCalls = [];
window.chrome = {
storage: {
sync: {
get: (defaults, cb) => cb(Object.assign({}, defaults, storedSettings)),
set: (obj, cb) => {
savedCalls.push(obj);
Object.assign(storedSettings, obj);
if (cb) cb();
}
}
}
};
const utilsCode = fs.readFileSync(path.join(__dirname, "..", "utils.js"), "utf8");
const popupCode = fs.readFileSync(path.join(__dirname, "..", "popup.js"), "utf8");
window.eval(utilsCode + "\n" + popupCode);
return { dom, window, savedCalls, storedSettings };
}
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
module.exports = { loadContentScript, loadPopupScript, wait };
+94
View File
@@ -0,0 +1,94 @@
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 () => {
const { window } = loadPopupScript();
await wait(20);
assert.strictEqual(window.document.getElementById("urlPreset").value, "default");
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");
});
test("shows custom URL field pre-filled when a custom URL is already saved", async () => {
const { window } = loadPopupScript({
initialSettings: { plexUrl: "http://192.168.1.50:32400/web" }
});
await wait(20);
assert.strictEqual(window.document.getElementById("urlPreset").value, "custom");
assert.strictEqual(window.document.getElementById("customUrl").value, "192.168.1.50:32400/web");
});
test("saving the default preset persists the default Plex URL", async () => {
const { window, savedCalls } = loadPopupScript();
await wait(20);
window.document.getElementById("saveBtn").click();
await wait(20);
assert.strictEqual(savedCalls.length, 1);
assert.strictEqual(savedCalls[0].plexUrl, "https://app.plex.tv");
});
test("saving a custom URL normalizes and persists it", async () => {
const { window, savedCalls } = loadPopupScript();
await wait(20);
const urlPreset = window.document.getElementById("urlPreset");
urlPreset.value = "custom";
urlPreset.dispatchEvent(new window.Event("change"));
window.document.getElementById("customUrl").value = "192.168.1.50:32400/web";
window.document.getElementById("saveBtn").click();
await wait(20);
assert.strictEqual(savedCalls.length, 1);
assert.strictEqual(savedCalls[0].plexUrl, "http://192.168.1.50:32400/web");
});
test("saving Custom URL with a blank field does not silently fall back to the default", async () => {
const { window, savedCalls } = loadPopupScript();
await wait(20);
const urlPreset = window.document.getElementById("urlPreset");
urlPreset.value = "custom";
urlPreset.dispatchEvent(new window.Event("change"));
window.document.getElementById("customUrl").value = " ";
window.document.getElementById("saveBtn").click();
await wait(20);
assert.strictEqual(savedCalls.length, 0, "should not save when the custom URL field is blank");
assert.match(window.document.getElementById("status").textContent, /enter|url/i);
});
test("saving an unparseable custom URL is rejected with an error, not saved", async () => {
const { window, savedCalls } = loadPopupScript();
await wait(20);
const urlPreset = window.document.getElementById("urlPreset");
urlPreset.value = "custom";
urlPreset.dispatchEvent(new window.Event("change"));
window.document.getElementById("customUrl").value = "http://";
window.document.getElementById("saveBtn").click();
await wait(20);
assert.strictEqual(savedCalls.length, 0, "should not save an invalid URL");
});
test("unchecking a toggle and saving persists it as false", async () => {
const { window, savedCalls } = loadPopupScript();
await wait(20);
window.document.getElementById("skipCredits").checked = false;
window.document.getElementById("saveBtn").click();
await wait(20);
assert.strictEqual(savedCalls.length, 1);
assert.strictEqual(savedCalls[0].skipCredits, false);
assert.strictEqual(savedCalls[0].skipIntro, true);
});