From 9fd66f6668c5079f966176022abc83523b200150 Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Mon, 1 Jun 2026 01:24:09 -0400 Subject: [PATCH 1/2] Reorganize extension source layout --- .github/workflows/deploy.yml | 8 +- README.md | 10 ++ build.py | 157 ------------------ {icons => extension/assets/icons}/icon128.png | Bin {icons => extension/assets/icons}/icon16.png | Bin {icons => extension/assets/icons}/icon19.png | Bin .../assets/icons}/icon19_disabled.png | Bin {icons => extension/assets/icons}/icon38.png | Bin .../assets/icons}/icon38_disabled.png | Bin {icons => extension/assets/icons}/icon48.png | Bin .../assets/icons}/icon48_disabled.png | Bin .../assets/images}/kofi_symbol.svg | 0 .../background/background.js | 2 +- .../content/frame-speed-snapshot.js | 0 inject.css => extension/content/inject.css | 0 inject.js => extension/content/inject.js | 5 +- shadow.css => extension/content/shadow.css | 0 manifest.json => extension/manifest.json | 28 ++-- .../options/import-export.js | 0 .../options/lucide-client.js | 0 options.css => extension/options/options.css | 0 .../options/options.html | 14 +- options.js => extension/options/options.js | 0 popup.css => extension/popup/popup.css | 0 popup.html => extension/popup/popup.html | 8 +- popup.js => extension/popup/popup.js | 20 +-- .../shared}/controller-utils.js | 0 {shared => extension/shared}/import-export.js | 2 +- {shared => extension/shared}/key-bindings.js | 0 .../shared}/popup-controls.js | 0 .../shared/settings-core.js | 0 {shared => extension/shared}/site-rules.js | 0 ui-icons.js => extension/shared/ui-icons.js | 4 +- scripts/deploy-amo-stable.sh | 13 +- scripts/deploy-beta.sh | 15 +- tests/helpers/extension-test-utils.js | 8 +- tests/importExport.integration.test.js | 6 +- tests/importExport.spec.js | 53 ++++-- tests/inject.spec.js | 12 +- tests/inject.test.js | 21 ++- tests/lucide-client.spec.js | 13 +- tests/options.integration.test.js | 14 +- tests/options.spec.js | 44 ++--- tests/popup.integration.test.js | 24 +-- tests/popup.spec.js | 42 ++--- tests/shared.test.js | 10 +- vitest.config.js | 2 +- 47 files changed, 209 insertions(+), 326 deletions(-) delete mode 100644 build.py rename {icons => extension/assets/icons}/icon128.png (100%) rename {icons => extension/assets/icons}/icon16.png (100%) rename {icons => extension/assets/icons}/icon19.png (100%) rename {icons => extension/assets/icons}/icon19_disabled.png (100%) rename {icons => extension/assets/icons}/icon38.png (100%) rename {icons => extension/assets/icons}/icon38_disabled.png (100%) rename {icons => extension/assets/icons}/icon48.png (100%) rename {icons => extension/assets/icons}/icon48_disabled.png (100%) rename {images => extension/assets/images}/kofi_symbol.svg (100%) rename background.js => extension/background/background.js (57%) rename frameSpeedSnapshot.js => extension/content/frame-speed-snapshot.js (100%) rename inject.css => extension/content/inject.css (100%) rename inject.js => extension/content/inject.js (99%) rename shadow.css => extension/content/shadow.css (100%) rename manifest.json => extension/manifest.json (73%) rename importExport.js => extension/options/import-export.js (100%) rename lucide-client.js => extension/options/lucide-client.js (100%) rename options.css => extension/options/options.css (100%) rename options.html => extension/options/options.html (99%) rename options.js => extension/options/options.js (100%) rename popup.css => extension/popup/popup.css (100%) rename popup.html => extension/popup/popup.html (96%) rename popup.js => extension/popup/popup.js (95%) rename {shared => extension/shared}/controller-utils.js (100%) rename {shared => extension/shared}/import-export.js (98%) rename {shared => extension/shared}/key-bindings.js (100%) rename {shared => extension/shared}/popup-controls.js (100%) rename settings-core.js => extension/shared/settings-core.js (100%) rename {shared => extension/shared}/site-rules.js (100%) rename ui-icons.js => extension/shared/ui-icons.js (97%) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3636a26..699f867 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -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 diff --git a/README.md b/README.md index 8362267..5c197bb 100644 --- a/README.md +++ b/README.md @@ -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? diff --git a/build.py b/build.py deleted file mode 100644 index cc429b0..0000000 --- a/build.py +++ /dev/null @@ -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() diff --git a/icons/icon128.png b/extension/assets/icons/icon128.png similarity index 100% rename from icons/icon128.png rename to extension/assets/icons/icon128.png diff --git a/icons/icon16.png b/extension/assets/icons/icon16.png similarity index 100% rename from icons/icon16.png rename to extension/assets/icons/icon16.png diff --git a/icons/icon19.png b/extension/assets/icons/icon19.png similarity index 100% rename from icons/icon19.png rename to extension/assets/icons/icon19.png diff --git a/icons/icon19_disabled.png b/extension/assets/icons/icon19_disabled.png similarity index 100% rename from icons/icon19_disabled.png rename to extension/assets/icons/icon19_disabled.png diff --git a/icons/icon38.png b/extension/assets/icons/icon38.png similarity index 100% rename from icons/icon38.png rename to extension/assets/icons/icon38.png diff --git a/icons/icon38_disabled.png b/extension/assets/icons/icon38_disabled.png similarity index 100% rename from icons/icon38_disabled.png rename to extension/assets/icons/icon38_disabled.png diff --git a/icons/icon48.png b/extension/assets/icons/icon48.png similarity index 100% rename from icons/icon48.png rename to extension/assets/icons/icon48.png diff --git a/icons/icon48_disabled.png b/extension/assets/icons/icon48_disabled.png similarity index 100% rename from icons/icon48_disabled.png rename to extension/assets/icons/icon48_disabled.png diff --git a/images/kofi_symbol.svg b/extension/assets/images/kofi_symbol.svg similarity index 100% rename from images/kofi_symbol.svg rename to extension/assets/images/kofi_symbol.svg diff --git a/background.js b/extension/background/background.js similarity index 57% rename from background.js rename to extension/background/background.js index 544eb16..7e64d08 100644 --- a/background.js +++ b/extension/background/background.js @@ -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") }); } }); diff --git a/frameSpeedSnapshot.js b/extension/content/frame-speed-snapshot.js similarity index 100% rename from frameSpeedSnapshot.js rename to extension/content/frame-speed-snapshot.js diff --git a/inject.css b/extension/content/inject.css similarity index 100% rename from inject.css rename to extension/content/inject.css diff --git a/inject.js b/extension/content/inject.js similarity index 99% rename from inject.js rename to extension/content/inject.js index f78c1b4..73c5763 100644 --- a/inject.js +++ b/extension/content/inject.js @@ -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; diff --git a/shadow.css b/extension/content/shadow.css similarity index 100% rename from shadow.css rename to extension/content/shadow.css diff --git a/manifest.json b/extension/manifest.json similarity index 73% rename from manifest.json rename to extension/manifest.json index 65e2ccb..9bd4628 100644 --- a/manifest.json +++ b/extension/manifest.json @@ -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" ] } diff --git a/importExport.js b/extension/options/import-export.js similarity index 100% rename from importExport.js rename to extension/options/import-export.js diff --git a/lucide-client.js b/extension/options/lucide-client.js similarity index 100% rename from lucide-client.js rename to extension/options/lucide-client.js diff --git a/options.css b/extension/options/options.css similarity index 100% rename from options.css rename to extension/options/options.css diff --git a/options.html b/extension/options/options.html similarity index 99% rename from options.html rename to extension/options/options.html index 36367a2..053c157 100644 --- a/options.html +++ b/extension/options/options.html @@ -5,14 +5,14 @@ Speeder Settings - - - - + + + + - - + +
@@ -38,7 +38,7 @@ > Speeder - - - + + + @@ -66,7 +66,7 @@ aria-label="Support on Ko-fi (opens in new tab)" > = 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; } diff --git a/scripts/deploy-amo-stable.sh b/scripts/deploy-amo-stable.sh index eafd495..d70706b 100755 --- a/scripts/deploy-amo-stable.sh +++ b/scripts/deploy-amo-stable.sh @@ -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 diff --git a/scripts/deploy-beta.sh b/scripts/deploy-beta.sh index d780d33..3ccd305 100755 --- a/scripts/deploy-beta.sh +++ b/scripts/deploy-beta.sh @@ -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 diff --git a/tests/helpers/extension-test-utils.js b/tests/helpers/extension-test-utils.js index fba54f9..9bda6ad 100644 --- a/tests/helpers/extension-test-utils.js +++ b/tests/helpers/extension-test-utils.js @@ -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)); + } } } diff --git a/tests/importExport.integration.test.js b/tests/importExport.integration.test.js index d1a423a..a692cb9 100644 --- a/tests/importExport.integration.test.js +++ b/tests/importExport.integration.test.js @@ -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; } diff --git a/tests/importExport.spec.js b/tests/importExport.spec.js index 6adc500..7cf0dec 100644 --- a/tests/importExport.spec.js +++ b/tests/importExport.spec.js @@ -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(); diff --git a/tests/inject.spec.js b/tests/inject.spec.js index 95006f2..bd70618 100644 --- a/tests/inject.spec.js +++ b/tests/inject.spec.js @@ -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, diff --git a/tests/inject.test.js b/tests/inject.test.js index 34a5110..eb82a02 100644 --- a/tests/inject.test.js +++ b/tests/inject.test.js @@ -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(); }); }); - diff --git a/tests/lucide-client.spec.js b/tests/lucide-client.spec.js index 4b2b050..a209141 100644 --- a/tests/lucide-client.spec.js +++ b/tests/lucide-client.spec.js @@ -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(""); - 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(""); - evaluateScript("ui-icons.js"); - evaluateScript("lucide-client.js"); + evaluateScript("extension/shared/ui-icons.js"); + evaluateScript("extension/options/lucide-client.js"); const sanitized = window.sanitizeLucideSvg(` @@ -43,8 +42,8 @@ describe("lucide-client.js", () => { it("searches and ranks icon slugs by query", () => { loadHtmlString(""); - evaluateScript("ui-icons.js"); - evaluateScript("lucide-client.js"); + evaluateScript("extension/shared/ui-icons.js"); + evaluateScript("extension/options/lucide-client.js"); const results = window.searchLucideSlugs( { diff --git a/tests/options.integration.test.js b/tests/options.integration.test.js index b0662e4..e6a6a22 100644 --- a/tests/options.integration.test.js +++ b/tests/options.integration.test.js @@ -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; diff --git a/tests/options.spec.js b/tests/options.spec.js index 766d2a3..d34ec70 100644 --- a/tests/options.spec.js +++ b/tests/options.spec.js @@ -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 () => { diff --git a/tests/popup.integration.test.js b/tests/popup.integration.test.js index d6c80bd..a7fcd20 100644 --- a/tests/popup.integration.test.js +++ b/tests/popup.integration.test.js @@ -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); diff --git a/tests/popup.spec.js b/tests/popup.spec.js index 1d7b0d1..2b461cd 100644 --- a/tests/popup.spec.js +++ b/tests/popup.spec.js @@ -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" } }); diff --git a/tests/shared.test.js b/tests/shared.test.js index c1f145f..4924a3e 100644 --- a/tests/shared.test.js +++ b/tests/shared.test.js @@ -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", () => { diff --git a/vitest.config.js b/vitest.config.js index bedeb66..abb398e 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -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"] } }); From a56742b421bafd19b00c5353ce2b80e5ca5a6548 Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Mon, 1 Jun 2026 01:24:50 -0400 Subject: [PATCH 2/2] Bump version to 5.3.2.0 --- extension/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/manifest.json b/extension/manifest.json index 9bd4628..f233193 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,7 +1,7 @@ { "name": "Speeder", "short_name": "Speeder", - "version": "5.3.1.0", + "version": "5.3.2.0", "manifest_version": 2, "description": "Speed up, slow down, advance and rewind HTML5 audio/video with shortcuts (New and improved version of \"Video Speed Controller\")", "homepage_url": "https://github.com/SoPat712/speeder",