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
+198 -315

No files matched your search

+3 -5
View File
@@ -10,8 +10,6 @@ on:
jobs:
build:
runs-on: ubuntu-latest
env:
WEB_EXT_IGNORE_FILES: scripts/**
steps:
- uses: actions/checkout@v4
@@ -20,7 +18,7 @@ jobs:
run: npm install -g web-ext
- name: Lint
run: web-ext lint
run: web-ext lint --source-dir extension
# Beta tag (v*-beta) → Sign as unlisted on AMO, attach signed XPI to GitHub Prerelease
# Firefox blocks all unsigned XPIs, even for self-hosted installs — unlisted signing is required
@@ -30,7 +28,7 @@ jobs:
web-ext sign \
--api-key ${{ secrets.FIREFOX_API_KEY }} \
--api-secret ${{ secrets.FIREFOX_API_SECRET }} \
--source-dir . \
--source-dir extension \
--artifacts-dir web-ext-artifacts \
--channel unlisted
@@ -70,6 +68,6 @@ jobs:
web-ext sign \
--api-key ${{ secrets.FIREFOX_API_KEY }} \
--api-secret ${{ secrets.FIREFOX_API_SECRET }} \
--source-dir . \
--source-dir extension \
--artifacts-dir web-ext-artifacts \
--channel listed
+10
View File
@@ -67,6 +67,16 @@ listens both for lower and upper case values (i.e. you can use
key. This is not a perfect solution, as some sites may listen to both, but works
most of the time.
## Development
The unpacked extension root is `extension/`. Load that directory in
`about:debugging`, and run extension tooling against it, for example:
```sh
npm test
npx --yes web-ext lint --source-dir extension
```
## FAQ
### The video controls are not showing up?
-157
View File
@@ -1,157 +0,0 @@
import glob
import fnmatch
import os
import re
import shutil
import tempfile
import zipfile
SCRIPT_NAME = os.path.basename(__file__)
TARGET_FILE = "manifest.json"
DEFAULT_EXCLUDE_FILES = {".DS_Store"}
DEFAULT_EXCLUDE_DIRS = {"__pycache__", "temp"}
DEFAULT_EXCLUDE_PATTERNS = {"._*", "*.pyc"}
def should_exclude(rel_path, exclude_files, exclude_dirs):
rel_path = os.path.normpath(rel_path)
path_parts = rel_path.split(os.sep)
file_name = path_parts[-1]
if file_name in DEFAULT_EXCLUDE_FILES or rel_path in DEFAULT_EXCLUDE_FILES:
return True
if any(part in DEFAULT_EXCLUDE_DIRS for part in path_parts):
return True
if file_name in exclude_files or rel_path in exclude_files:
return True
if any(part in exclude_dirs for part in path_parts):
return True
if any(fnmatch.fnmatch(file_name, pattern) for pattern in DEFAULT_EXCLUDE_PATTERNS):
return True
return False
def zip_folder(output_name, folder, exclude_files, exclude_dirs):
with zipfile.ZipFile(output_name, "w", zipfile.ZIP_DEFLATED) as zipf:
for root, dirs, files in os.walk(folder):
dirs[:] = [
d
for d in dirs
if not should_exclude(
os.path.relpath(os.path.join(root, d), folder),
exclude_files,
exclude_dirs,
)
]
for file in files:
rel_path = os.path.relpath(os.path.join(root, file), folder)
if should_exclude(rel_path, exclude_files, exclude_dirs):
continue
zipf.write(os.path.join(root, file), arcname=rel_path)
def update_version_line(file_path, new_version):
with open(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
updated = False
for i, line in enumerate(lines):
match = re.match(r'\s*"version":\s*"([^"]+)"', line)
if match:
old_version = match.group(1)
lines[i] = re.sub(
r'"version":\s*".+?"', f'"version": "{new_version}"', line
)
updated = True
print(
f"🛠️ Changed version in {file_path} from {old_version} ➜ {new_version}"
)
break
if updated:
with open(file_path, "w", encoding="utf-8") as f:
f.writelines(lines)
else:
print(f"⚠️ No version line found in {file_path}.")
def main():
# Step 0: Remove all existing .xpi files upfront
xpi_files = glob.glob("*.xpi")
for f in xpi_files:
try:
os.remove(f)
print(f"🗑️ Removed existing archive: {f}")
except Exception as e:
print(f"⚠️ Failed to remove {f}: {e}")
# Read current version from manifest.json
current_dir = os.getcwd()
manifest_path = os.path.join(current_dir, TARGET_FILE)
current_version = "unknown"
if os.path.exists(manifest_path):
with open(manifest_path, "r", encoding="utf-8") as f:
for line in f:
match = re.match(r'\s*"version":\s*"([^"]+)"', line)
if match:
current_version = match.group(1)
break
print(f"📦 Current version: {current_version}")
base_version = input("Enter the new base version (e.g., 2.0.1): ").strip()
if not base_version:
print("❌ No version entered. Exiting.")
return
github_version = f"{base_version}.0"
# Step 1: Update manifest.json on disk to base_version (for Firefox)
if os.path.exists(manifest_path):
update_version_line(manifest_path, base_version)
else:
print(f"❌ {TARGET_FILE} not found. Aborting.")
return
# Step 2: Create videospeed-firefox.xpi (exclude script, .git, AND videospeed-firefox.xpi itself)
exclude_files = [SCRIPT_NAME, "videospeed-firefox.xpi"]
exclude_dirs = [".git"]
zip_folder("videospeed-firefox.xpi", current_dir, exclude_files, exclude_dirs)
print("✅ Created videospeed-firefox.xpi")
# Step 3: Re-scan for .xpi files after Firefox archive creation, exclude them for GitHub zip
current_xpi_files = set(glob.glob("*.xpi"))
exclude_temp_files = current_xpi_files.union({SCRIPT_NAME})
exclude_temp_dirs = set(exclude_dirs)
# Step 4: Create videospeed-github.xpi from temp folder with version bumped to .0
with tempfile.TemporaryDirectory() as temp_dir:
for item in os.listdir(current_dir):
if should_exclude(item, exclude_temp_files, exclude_temp_dirs):
continue
src = os.path.join(current_dir, item)
dst = os.path.join(temp_dir, item)
if os.path.isdir(src):
shutil.copytree(src, dst)
else:
shutil.copy2(src, dst)
temp_manifest = os.path.join(temp_dir, TARGET_FILE)
if os.path.exists(temp_manifest):
update_version_line(temp_manifest, github_version)
else:
print(f"⚠️ {TARGET_FILE} not found in temp folder.")
zip_folder(
"videospeed-github.xpi", temp_dir, exclude_files=[], exclude_dirs=[]
)
print("✅ Created videospeed-github.xpi")
if __name__ == "__main__":
main()
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,5 +1,5 @@
chrome.runtime.onMessage.addListener(function (request) {
if (request.action === "openOptions") {
chrome.tabs.create({ url: chrome.runtime.getURL("options.html") });
chrome.tabs.create({ url: chrome.runtime.getURL("options/options.html") });
}
});
File renamed without changes.
File renamed without changes.
+2 -3
View File
@@ -60,6 +60,7 @@ var YT_NATIVE_MAX = 2.0;
var YT_NATIVE_STEP = 0.05;
var vscObservedRoots = new WeakSet();
var vscConnectedScannedRoots = new WeakSet();
var vscInitializedDocuments = new Set();
var requestIdle =
typeof window.requestIdleCallback === "function"
? window.requestIdleCallback.bind(window)
@@ -1882,7 +1883,7 @@ function defineVideoController() {
var shadow = wrapper.attachShadow({ mode: "open" });
var shadowStylesheet = doc.createElement("link");
shadowStylesheet.rel = "stylesheet";
shadowStylesheet.href = chrome.runtime.getURL("shadow.css");
shadowStylesheet.href = chrome.runtime.getURL("content/shadow.css");
shadow.appendChild(shadowStylesheet);
var controller = doc.createElement("div");
@@ -2253,8 +2254,6 @@ function setupListener(root) {
root.vscRateListenerAttached = true;
}
var vscInitializedDocuments = new Set();
function clearPendingInitialization(doc) {
if (!doc || !doc.vscPendingInitializeHandler) return;
File renamed without changes.
+14 -14
View File
@@ -16,13 +16,13 @@
}
},
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
"16": "assets/icons/icon16.png",
"48": "assets/icons/icon48.png",
"128": "assets/icons/icon128.png"
},
"background": {
"scripts": [
"background.js"
"background/background.js"
]
},
"permissions": [
@@ -30,16 +30,16 @@
"https://cdn.jsdelivr.net/*"
],
"options_ui": {
"page": "options.html",
"page": "options/options.html",
"open_in_tab": true
},
"browser_action": {
"default_icon": {
"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"
},
"default_popup": "popup.html"
"default_popup": "popup/popup.html"
},
"content_scripts": [
{
@@ -56,19 +56,19 @@
"https://meet.google.com/*"
],
"css": [
"inject.css"
"content/inject.css"
],
"js": [
"shared/controller-utils.js",
"shared/key-bindings.js",
"shared/site-rules.js",
"ui-icons.js",
"inject.js"
"shared/ui-icons.js",
"content/inject.js"
]
}
],
"web_accessible_resources": [
"inject.css",
"shadow.css"
"content/inject.css",
"content/shadow.css"
]
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -5,14 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Speeder Settings</title>
<link rel="stylesheet" href="options.css" />
<script src="shared/controller-utils.js"></script>
<script src="shared/key-bindings.js"></script>
<script src="shared/popup-controls.js"></script>
<script src="ui-icons.js"></script>
<script src="../shared/controller-utils.js"></script>
<script src="../shared/key-bindings.js"></script>
<script src="../shared/popup-controls.js"></script>
<script src="../shared/ui-icons.js"></script>
<script src="lucide-client.js"></script>
<script src="options.js"></script>
<script src="shared/import-export.js"></script>
<script src="importExport.js"></script>
<script src="../shared/import-export.js"></script>
<script src="import-export.js"></script>
</head>
<body>
<div class="page-shell">
@@ -38,7 +38,7 @@
>
<img
class="support-cta-kofi-img"
src="images/kofi_symbol.svg"
src="../assets/images/kofi_symbol.svg"
width="241"
height="194"
alt=""
File renamed without changes.
File renamed without changes.
+4 -4
View File
@@ -4,9 +4,9 @@
<meta charset="utf-8" />
<title>Speeder</title>
<link rel="stylesheet" href="popup.css" />
<script src="shared/site-rules.js"></script>
<script src="shared/popup-controls.js"></script>
<script src="ui-icons.js"></script>
<script src="../shared/site-rules.js"></script>
<script src="../shared/popup-controls.js"></script>
<script src="../shared/ui-icons.js"></script>
<script src="popup.js"></script>
</head>
<body>
@@ -66,7 +66,7 @@
aria-label="Support on Ko-fi (opens in new tab)"
>
<img
src="images/kofi_symbol.svg"
src="../assets/images/kofi_symbol.svg"
width="28"
height="22"
alt=""
+10 -10
View File
@@ -4,7 +4,7 @@ document.addEventListener("DOMContentLoaded", function () {
var siteRuleUtils = speederShared.siteRules || {};
var popupControlUtils = speederShared.popupControls || {};
/* `label` is only used if ui-icons.js has no path for this action (fallback). */
/* `label` is only used if shared/ui-icons.js has no path for this action (fallback). */
var controllerButtonDefs = {
rewind: { label: "", className: "rw" },
slower: { label: "", className: "" },
@@ -128,7 +128,7 @@ document.addEventListener("DOMContentLoaded", function () {
var tabId = tabs[0].id;
chrome.tabs.executeScript(
tabId,
{ allFrames: true, file: "frameSpeedSnapshot.js" },
{ allFrames: true, file: "content/frame-speed-snapshot.js" },
function (results) {
if (chrome.runtime.lastError) {
sendToActiveTab({ action: "get_speed" }, function (response) {
@@ -192,7 +192,7 @@ document.addEventListener("DOMContentLoaded", function () {
btn.addEventListener("click", function () {
if (btnId === "settings") {
window.open(chrome.runtime.getURL("options.html"));
window.open(chrome.runtime.getURL("options/options.html"));
return;
}
sendToActiveTab(
@@ -210,11 +210,11 @@ document.addEventListener("DOMContentLoaded", function () {
var manifest = chrome.runtime.getManifest();
var versionElement = document.querySelector("#app-version");
if (versionElement) {
versionElement.innerText = manifest.version;
versionElement.textContent = manifest.version;
}
document.querySelector("#config").addEventListener("click", function () {
window.open(chrome.runtime.getURL("options.html"));
window.open(chrome.runtime.getURL("options/options.html"));
});
document.querySelector("#about").addEventListener("click", function () {
@@ -350,9 +350,9 @@ document.addEventListener("DOMContentLoaded", function () {
const suffix = `${enabled ? "" : "_disabled"}.png`;
chrome.browserAction.setIcon({
path: {
19: "icons/icon19" + suffix,
38: "icons/icon38" + suffix,
48: "icons/icon48" + suffix
19: "assets/icons/icon19" + suffix,
38: "assets/icons/icon38" + suffix,
48: "assets/icons/icon48" + suffix
}
});
}
@@ -366,12 +366,12 @@ document.addEventListener("DOMContentLoaded", function () {
function setStatusMessage(str) {
const status_element = document.querySelector("#status");
status_element.classList.toggle("hide", false);
status_element.innerText = str;
status_element.textContent = str;
}
function clearStatusMessage() {
const status_element = document.querySelector("#status");
status_element.classList.toggle("hide", true);
status_element.innerText = "";
status_element.textContent = "";
}
});
File renamed without changes.
@@ -50,7 +50,7 @@
/**
* Local-only keys excluded from backup JSON. These are disposable caches
* (e.g. Lucide tags.json) that bloat exports and are refetched when needed.
* Keep in sync with lucide-client.js (LUCIDE_TAGS_CACHE_KEY + "At").
* Keep in sync with options/lucide-client.js (LUCIDE_TAGS_CACHE_KEY + "At").
*/
var localSettingsKeysOmittedFromExport = [
"lucideTagsCacheV1",
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2 -2
View File
@@ -76,7 +76,7 @@ function vscSanitizeSvgTree(svg) {
n.remove();
});
svg.querySelectorAll("*").forEach(function (el) {
[svg].concat(Array.from(svg.querySelectorAll("*"))).forEach(function (el) {
for (var i = el.attributes.length - 1; i >= 0; i--) {
var attr = el.attributes[i];
var name = attr.name.toLowerCase();
@@ -94,7 +94,7 @@ function vscSanitizeSvgTree(svg) {
}
});
svg.setAttribute("xmlns", VSC_SVG_NS);
svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns", VSC_SVG_NS);
svg.setAttribute("aria-hidden", "true");
return svg;
}
+7 -6
View File
@@ -7,19 +7,20 @@ set -euo pipefail
ROOT="$(git rev-parse --show-toplevel)"
cd "$ROOT"
MANIFEST_PATH="extension/manifest.json"
manifest_version() {
python3 -c 'import json; print(json.load(open("manifest.json"))["version"])'
MANIFEST_PATH="$MANIFEST_PATH" python3 -c 'import json, os; print(json.load(open(os.environ["MANIFEST_PATH"]))["version"])'
}
bump_manifest() {
local ver="$1"
VER="$ver" python3 <<'PY'
VER="$ver" MANIFEST_PATH="$MANIFEST_PATH" python3 <<'PY'
import json
import os
ver = os.environ["VER"]
path = "manifest.json"
path = os.environ["MANIFEST_PATH"]
with open(path, encoding="utf-8") as f:
data = json.load(f)
data["version"] = ver
@@ -58,8 +59,8 @@ fi
git checkout beta
git pull origin beta
echo "Current version on beta (manifest.json): $(manifest_version)"
read -r -p "Release version for manifest.json + tag (e.g. 5.0.4): " SEMVER_IN
echo "Current version on beta ($MANIFEST_PATH): $(manifest_version)"
read -r -p "Release version for $MANIFEST_PATH + tag (e.g. 5.0.4): " SEMVER_IN
SEMVER="$(normalize_semver "$SEMVER_IN")"
validate_semver "$SEMVER"
@@ -73,7 +74,7 @@ fi
echo
echo "This will:"
echo " 1. checkout main, merge --squash origin/beta (single release commit on main)"
echo " 2. set manifest.json to $SEMVER in that commit (if anything else changed, it is included too)"
echo " 2. set $MANIFEST_PATH to $SEMVER in that commit (if anything else changed, it is included too)"
echo " 3. push origin main, create tag $TAG, push tag (triggers listed AMO submit)"
echo " 4. checkout dev (merge main→dev yourself if you want them aligned)"
read -r -p "Continue? [y/N] " confirm
+8 -7
View File
@@ -6,19 +6,20 @@ set -euo pipefail
ROOT="$(git rev-parse --show-toplevel)"
cd "$ROOT"
MANIFEST_PATH="extension/manifest.json"
manifest_version() {
python3 -c 'import json; print(json.load(open("manifest.json"))["version"])'
MANIFEST_PATH="$MANIFEST_PATH" python3 -c 'import json, os; print(json.load(open(os.environ["MANIFEST_PATH"]))["version"])'
}
bump_manifest() {
local ver="$1"
VER="$ver" python3 <<'PY'
VER="$ver" MANIFEST_PATH="$MANIFEST_PATH" python3 <<'PY'
import json
import os
ver = os.environ["VER"]
path = "manifest.json"
path = os.environ["MANIFEST_PATH"]
with open(path, encoding="utf-8") as f:
data = json.load(f)
data["version"] = ver
@@ -57,8 +58,8 @@ fi
git checkout dev
git pull origin dev
echo "Current version in manifest.json: $(manifest_version)"
read -r -p "New version for manifest.json (e.g. 5.0.4): " SEMVER_IN
echo "Current version in $MANIFEST_PATH: $(manifest_version)"
read -r -p "New version for $MANIFEST_PATH (e.g. 5.0.4): " SEMVER_IN
SEMVER="$(normalize_semver "$SEMVER_IN")"
validate_semver "$SEMVER"
@@ -76,7 +77,7 @@ fi
echo
echo "This will:"
echo " 1. set manifest.json version to $SEMVER, commit on dev, push origin dev"
echo " 1. set $MANIFEST_PATH version to $SEMVER, commit on dev, push origin dev"
echo " 2. checkout beta, merge dev (no-ff), push origin beta"
echo " 3. create tag $TAG and push it (triggers beta AMO + prerelease)"
echo " 4. checkout dev (main is not modified)"
@@ -86,7 +87,7 @@ read -r -p "Continue? [y/N] " confirm
echo "🚀 Releasing beta $TAG"
bump_manifest "$SEMVER"
git add manifest.json
git add "$MANIFEST_PATH"
git commit -m "Bump version to $SEMVER"
git push origin dev
+5 -1
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,8 +118,12 @@ async function flushAsyncWork(turns) {
const count = turns || 2;
for (let i = 0; i < count; i += 1) {
await Promise.resolve();
if (vi && typeof vi.isFakeTimers === "function" && vi.isFakeTimers()) {
await vi.advanceTimersByTimeAsync(0);
} else {
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
}
function pickStorageValues(data, keys) {
+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;
+16 -10
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,8 +170,10 @@ describe("options.js", () => {
expect(savedSettings.controllerMarginTop).toBe(200);
expect(savedSettings.controllerMarginBottom).toBe(0);
expect(savedSettings.popupControllerButtons).toEqual(["advance", "rewind"]);
expect(savedSettings.siteRules).toEqual([
{
expect(savedSettings.siteRules).toHaveLength(3);
expect(savedSettings.siteRules).toEqual(
expect.arrayContaining([
expect.objectContaining({
pattern: "youtube.com",
enabled: true,
controllerLocation: "top-right",
@@ -178,8 +183,9 @@ describe("options.js", () => {
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);
+20 -20
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,23 +91,24 @@ 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
}
});
chrome.tabs.executeScript
.mockImplementationOnce((tabId, details, callback) => {
callback([
},
executeScriptImpl: (tabId, details, callback) => {
speedQueryCount += 1;
callback(
speedQueryCount <= 2
? [
{ speed: 1.25, preferred: false },
{ speed: 1.5, preferred: true }
]);
})
.mockImplementationOnce((tabId, details, callback) => {
callback([{ speed: 1.75, preferred: true }]);
]
: [{ speed: 1.75, preferred: true }]
);
}
});
chrome.tabs.sendMessage.mockImplementation((tabId, message, callback) => {
@@ -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", () => {
+1 -1
View File
@@ -6,7 +6,7 @@ module.exports = defineConfig({
clearMocks: true,
globals: true,
restoreMocks: true,
include: ["tests/**/*.test.js"],
include: ["tests/**/*.test.js", "tests/**/*.spec.js"],
setupFiles: ["./tests/setup.js"]
}
});