Reorganize extension source layout

This commit is contained in:
2026-06-01 01:24:09 -04:00
parent 80a54eed06
commit 9fd66f6668
47 changed files with 209 additions and 326 deletions
+3 -5
View File
@@ -10,8 +10,6 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
WEB_EXT_IGNORE_FILES: scripts/**
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -20,7 +18,7 @@ jobs:
run: npm install -g web-ext run: npm install -g web-ext
- name: Lint - 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 # 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 # Firefox blocks all unsigned XPIs, even for self-hosted installs — unlisted signing is required
@@ -30,7 +28,7 @@ jobs:
web-ext sign \ web-ext sign \
--api-key ${{ secrets.FIREFOX_API_KEY }} \ --api-key ${{ secrets.FIREFOX_API_KEY }} \
--api-secret ${{ secrets.FIREFOX_API_SECRET }} \ --api-secret ${{ secrets.FIREFOX_API_SECRET }} \
--source-dir . \ --source-dir extension \
--artifacts-dir web-ext-artifacts \ --artifacts-dir web-ext-artifacts \
--channel unlisted --channel unlisted
@@ -70,6 +68,6 @@ jobs:
web-ext sign \ web-ext sign \
--api-key ${{ secrets.FIREFOX_API_KEY }} \ --api-key ${{ secrets.FIREFOX_API_KEY }} \
--api-secret ${{ secrets.FIREFOX_API_SECRET }} \ --api-secret ${{ secrets.FIREFOX_API_SECRET }} \
--source-dir . \ --source-dir extension \
--artifacts-dir web-ext-artifacts \ --artifacts-dir web-ext-artifacts \
--channel listed --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 key. This is not a perfect solution, as some sites may listen to both, but works
most of the time. 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 ## FAQ
### The video controls are not showing up? ### 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()

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

@@ -1,5 +1,5 @@
chrome.runtime.onMessage.addListener(function (request) { chrome.runtime.onMessage.addListener(function (request) {
if (request.action === "openOptions") { if (request.action === "openOptions") {
chrome.tabs.create({ url: chrome.runtime.getURL("options.html") }); chrome.tabs.create({ url: chrome.runtime.getURL("options/options.html") });
} }
}); });
+2 -3
View File
@@ -60,6 +60,7 @@ var YT_NATIVE_MAX = 2.0;
var YT_NATIVE_STEP = 0.05; var YT_NATIVE_STEP = 0.05;
var vscObservedRoots = new WeakSet(); var vscObservedRoots = new WeakSet();
var vscConnectedScannedRoots = new WeakSet(); var vscConnectedScannedRoots = new WeakSet();
var vscInitializedDocuments = new Set();
var requestIdle = var requestIdle =
typeof window.requestIdleCallback === "function" typeof window.requestIdleCallback === "function"
? window.requestIdleCallback.bind(window) ? window.requestIdleCallback.bind(window)
@@ -1882,7 +1883,7 @@ function defineVideoController() {
var shadow = wrapper.attachShadow({ mode: "open" }); var shadow = wrapper.attachShadow({ mode: "open" });
var shadowStylesheet = doc.createElement("link"); var shadowStylesheet = doc.createElement("link");
shadowStylesheet.rel = "stylesheet"; shadowStylesheet.rel = "stylesheet";
shadowStylesheet.href = chrome.runtime.getURL("shadow.css"); shadowStylesheet.href = chrome.runtime.getURL("content/shadow.css");
shadow.appendChild(shadowStylesheet); shadow.appendChild(shadowStylesheet);
var controller = doc.createElement("div"); var controller = doc.createElement("div");
@@ -2253,8 +2254,6 @@ function setupListener(root) {
root.vscRateListenerAttached = true; root.vscRateListenerAttached = true;
} }
var vscInitializedDocuments = new Set();
function clearPendingInitialization(doc) { function clearPendingInitialization(doc) {
if (!doc || !doc.vscPendingInitializeHandler) return; if (!doc || !doc.vscPendingInitializeHandler) return;
+14 -14
View File
@@ -16,13 +16,13 @@
} }
}, },
"icons": { "icons": {
"16": "icons/icon16.png", "16": "assets/icons/icon16.png",
"48": "icons/icon48.png", "48": "assets/icons/icon48.png",
"128": "icons/icon128.png" "128": "assets/icons/icon128.png"
}, },
"background": { "background": {
"scripts": [ "scripts": [
"background.js" "background/background.js"
] ]
}, },
"permissions": [ "permissions": [
@@ -30,16 +30,16 @@
"https://cdn.jsdelivr.net/*" "https://cdn.jsdelivr.net/*"
], ],
"options_ui": { "options_ui": {
"page": "options.html", "page": "options/options.html",
"open_in_tab": true "open_in_tab": true
}, },
"browser_action": { "browser_action": {
"default_icon": { "default_icon": {
"19": "icons/icon19.png", "19": "assets/icons/icon19.png",
"38": "icons/icon38.png", "38": "assets/icons/icon38.png",
"48": "icons/icon48.png" "48": "assets/icons/icon48.png"
}, },
"default_popup": "popup.html" "default_popup": "popup/popup.html"
}, },
"content_scripts": [ "content_scripts": [
{ {
@@ -56,19 +56,19 @@
"https://meet.google.com/*" "https://meet.google.com/*"
], ],
"css": [ "css": [
"inject.css" "content/inject.css"
], ],
"js": [ "js": [
"shared/controller-utils.js", "shared/controller-utils.js",
"shared/key-bindings.js", "shared/key-bindings.js",
"shared/site-rules.js", "shared/site-rules.js",
"ui-icons.js", "shared/ui-icons.js",
"inject.js" "content/inject.js"
] ]
} }
], ],
"web_accessible_resources": [ "web_accessible_resources": [
"inject.css", "content/inject.css",
"shadow.css" "content/shadow.css"
] ]
} }
@@ -5,14 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Speeder Settings</title> <title>Speeder Settings</title>
<link rel="stylesheet" href="options.css" /> <link rel="stylesheet" href="options.css" />
<script src="shared/controller-utils.js"></script> <script src="../shared/controller-utils.js"></script>
<script src="shared/key-bindings.js"></script> <script src="../shared/key-bindings.js"></script>
<script src="shared/popup-controls.js"></script> <script src="../shared/popup-controls.js"></script>
<script src="ui-icons.js"></script> <script src="../shared/ui-icons.js"></script>
<script src="lucide-client.js"></script> <script src="lucide-client.js"></script>
<script src="options.js"></script> <script src="options.js"></script>
<script src="shared/import-export.js"></script> <script src="../shared/import-export.js"></script>
<script src="importExport.js"></script> <script src="import-export.js"></script>
</head> </head>
<body> <body>
<div class="page-shell"> <div class="page-shell">
@@ -38,7 +38,7 @@
> >
<img <img
class="support-cta-kofi-img" class="support-cta-kofi-img"
src="images/kofi_symbol.svg" src="../assets/images/kofi_symbol.svg"
width="241" width="241"
height="194" height="194"
alt="" alt=""
+4 -4
View File
@@ -4,9 +4,9 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Speeder</title> <title>Speeder</title>
<link rel="stylesheet" href="popup.css" /> <link rel="stylesheet" href="popup.css" />
<script src="shared/site-rules.js"></script> <script src="../shared/site-rules.js"></script>
<script src="shared/popup-controls.js"></script> <script src="../shared/popup-controls.js"></script>
<script src="ui-icons.js"></script> <script src="../shared/ui-icons.js"></script>
<script src="popup.js"></script> <script src="popup.js"></script>
</head> </head>
<body> <body>
@@ -66,7 +66,7 @@
aria-label="Support on Ko-fi (opens in new tab)" aria-label="Support on Ko-fi (opens in new tab)"
> >
<img <img
src="images/kofi_symbol.svg" src="../assets/images/kofi_symbol.svg"
width="28" width="28"
height="22" height="22"
alt="" alt=""
+10 -10
View File
@@ -4,7 +4,7 @@ document.addEventListener("DOMContentLoaded", function () {
var siteRuleUtils = speederShared.siteRules || {}; var siteRuleUtils = speederShared.siteRules || {};
var popupControlUtils = speederShared.popupControls || {}; 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 = { var controllerButtonDefs = {
rewind: { label: "", className: "rw" }, rewind: { label: "", className: "rw" },
slower: { label: "", className: "" }, slower: { label: "", className: "" },
@@ -128,7 +128,7 @@ document.addEventListener("DOMContentLoaded", function () {
var tabId = tabs[0].id; var tabId = tabs[0].id;
chrome.tabs.executeScript( chrome.tabs.executeScript(
tabId, tabId,
{ allFrames: true, file: "frameSpeedSnapshot.js" }, { allFrames: true, file: "content/frame-speed-snapshot.js" },
function (results) { function (results) {
if (chrome.runtime.lastError) { if (chrome.runtime.lastError) {
sendToActiveTab({ action: "get_speed" }, function (response) { sendToActiveTab({ action: "get_speed" }, function (response) {
@@ -192,7 +192,7 @@ document.addEventListener("DOMContentLoaded", function () {
btn.addEventListener("click", function () { btn.addEventListener("click", function () {
if (btnId === "settings") { if (btnId === "settings") {
window.open(chrome.runtime.getURL("options.html")); window.open(chrome.runtime.getURL("options/options.html"));
return; return;
} }
sendToActiveTab( sendToActiveTab(
@@ -210,11 +210,11 @@ document.addEventListener("DOMContentLoaded", function () {
var manifest = chrome.runtime.getManifest(); var manifest = chrome.runtime.getManifest();
var versionElement = document.querySelector("#app-version"); var versionElement = document.querySelector("#app-version");
if (versionElement) { if (versionElement) {
versionElement.innerText = manifest.version; versionElement.textContent = manifest.version;
} }
document.querySelector("#config").addEventListener("click", function () { 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 () { document.querySelector("#about").addEventListener("click", function () {
@@ -350,9 +350,9 @@ document.addEventListener("DOMContentLoaded", function () {
const suffix = `${enabled ? "" : "_disabled"}.png`; const suffix = `${enabled ? "" : "_disabled"}.png`;
chrome.browserAction.setIcon({ chrome.browserAction.setIcon({
path: { path: {
19: "icons/icon19" + suffix, 19: "assets/icons/icon19" + suffix,
38: "icons/icon38" + suffix, 38: "assets/icons/icon38" + suffix,
48: "icons/icon48" + suffix 48: "assets/icons/icon48" + suffix
} }
}); });
} }
@@ -366,12 +366,12 @@ document.addEventListener("DOMContentLoaded", function () {
function setStatusMessage(str) { function setStatusMessage(str) {
const status_element = document.querySelector("#status"); const status_element = document.querySelector("#status");
status_element.classList.toggle("hide", false); status_element.classList.toggle("hide", false);
status_element.innerText = str; status_element.textContent = str;
} }
function clearStatusMessage() { function clearStatusMessage() {
const status_element = document.querySelector("#status"); const status_element = document.querySelector("#status");
status_element.classList.toggle("hide", true); status_element.classList.toggle("hide", true);
status_element.innerText = ""; status_element.textContent = "";
} }
}); });
@@ -50,7 +50,7 @@
/** /**
* Local-only keys excluded from backup JSON. These are disposable caches * Local-only keys excluded from backup JSON. These are disposable caches
* (e.g. Lucide tags.json) that bloat exports and are refetched when needed. * (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 = [ var localSettingsKeysOmittedFromExport = [
"lucideTagsCacheV1", "lucideTagsCacheV1",
+2 -2
View File
@@ -76,7 +76,7 @@ function vscSanitizeSvgTree(svg) {
n.remove(); 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--) { for (var i = el.attributes.length - 1; i >= 0; i--) {
var attr = el.attributes[i]; var attr = el.attributes[i];
var name = attr.name.toLowerCase(); 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"); svg.setAttribute("aria-hidden", "true");
return svg; return svg;
} }
+7 -6
View File
@@ -7,19 +7,20 @@ set -euo pipefail
ROOT="$(git rev-parse --show-toplevel)" ROOT="$(git rev-parse --show-toplevel)"
cd "$ROOT" cd "$ROOT"
MANIFEST_PATH="extension/manifest.json"
manifest_version() { 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() { bump_manifest() {
local ver="$1" local ver="$1"
VER="$ver" python3 <<'PY' VER="$ver" MANIFEST_PATH="$MANIFEST_PATH" python3 <<'PY'
import json import json
import os import os
ver = os.environ["VER"] ver = os.environ["VER"]
path = "manifest.json" path = os.environ["MANIFEST_PATH"]
with open(path, encoding="utf-8") as f: with open(path, encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
data["version"] = ver data["version"] = ver
@@ -58,8 +59,8 @@ fi
git checkout beta git checkout beta
git pull origin beta git pull origin beta
echo "Current version on beta (manifest.json): $(manifest_version)" echo "Current version on beta ($MANIFEST_PATH): $(manifest_version)"
read -r -p "Release version for manifest.json + tag (e.g. 5.0.4): " SEMVER_IN read -r -p "Release version for $MANIFEST_PATH + tag (e.g. 5.0.4): " SEMVER_IN
SEMVER="$(normalize_semver "$SEMVER_IN")" SEMVER="$(normalize_semver "$SEMVER_IN")"
validate_semver "$SEMVER" validate_semver "$SEMVER"
@@ -73,7 +74,7 @@ fi
echo echo
echo "This will:" echo "This will:"
echo " 1. checkout main, merge --squash origin/beta (single release commit on main)" 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 " 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)" echo " 4. checkout dev (merge main→dev yourself if you want them aligned)"
read -r -p "Continue? [y/N] " confirm read -r -p "Continue? [y/N] " confirm
+8 -7
View File
@@ -6,19 +6,20 @@ set -euo pipefail
ROOT="$(git rev-parse --show-toplevel)" ROOT="$(git rev-parse --show-toplevel)"
cd "$ROOT" cd "$ROOT"
MANIFEST_PATH="extension/manifest.json"
manifest_version() { 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() { bump_manifest() {
local ver="$1" local ver="$1"
VER="$ver" python3 <<'PY' VER="$ver" MANIFEST_PATH="$MANIFEST_PATH" python3 <<'PY'
import json import json
import os import os
ver = os.environ["VER"] ver = os.environ["VER"]
path = "manifest.json" path = os.environ["MANIFEST_PATH"]
with open(path, encoding="utf-8") as f: with open(path, encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
data["version"] = ver data["version"] = ver
@@ -57,8 +58,8 @@ fi
git checkout dev git checkout dev
git pull origin dev git pull origin dev
echo "Current version in manifest.json: $(manifest_version)" echo "Current version in $MANIFEST_PATH: $(manifest_version)"
read -r -p "New version for manifest.json (e.g. 5.0.4): " SEMVER_IN read -r -p "New version for $MANIFEST_PATH (e.g. 5.0.4): " SEMVER_IN
SEMVER="$(normalize_semver "$SEMVER_IN")" SEMVER="$(normalize_semver "$SEMVER_IN")"
validate_semver "$SEMVER" validate_semver "$SEMVER"
@@ -76,7 +77,7 @@ fi
echo echo
echo "This will:" 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 " 2. checkout beta, merge dev (no-ff), push origin beta"
echo " 3. create tag $TAG and push it (triggers beta AMO + prerelease)" echo " 3. create tag $TAG and push it (triggers beta AMO + prerelease)"
echo " 4. checkout dev (main is not modified)" echo " 4. checkout dev (main is not modified)"
@@ -86,7 +87,7 @@ read -r -p "Continue? [y/N] " confirm
echo "🚀 Releasing beta $TAG" echo "🚀 Releasing beta $TAG"
bump_manifest "$SEMVER" bump_manifest "$SEMVER"
git add manifest.json git add "$MANIFEST_PATH"
git commit -m "Bump version to $SEMVER" git commit -m "Bump version to $SEMVER"
git push origin dev git push origin dev
+6 -2
View File
@@ -1,7 +1,7 @@
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const { JSDOM } = require("jsdom"); const { JSDOM } = require("jsdom");
const { vi } = require("vitest"); const vi = globalThis.vi;
const ROOT = path.resolve(__dirname, "..", ".."); const ROOT = path.resolve(__dirname, "..", "..");
@@ -118,7 +118,11 @@ async function flushAsyncWork(turns) {
const count = turns || 2; const count = turns || 2;
for (let i = 0; i < count; i += 1) { for (let i = 0; i < count; i += 1) {
await Promise.resolve(); 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));
}
} }
} }
+3 -3
View File
@@ -6,14 +6,14 @@ import {
} from "./helpers/browser.js"; } from "./helpers/browser.js";
async function setupImportExport(overrides = {}) { async function setupImportExport(overrides = {}) {
loadHtml("options.html"); loadHtml("extension/options/options.html");
globalThis.chrome = createChromeMock(overrides); globalThis.chrome = createChromeMock(overrides);
window.chrome = globalThis.chrome; window.chrome = globalThis.chrome;
const restoreSpy = vi.fn(); const restoreSpy = vi.fn();
globalThis.restore_options = restoreSpy; globalThis.restore_options = restoreSpy;
window.restore_options = restoreSpy; window.restore_options = restoreSpy;
loadScript("shared/import-export.js"); loadScript("extension/shared/import-export.js");
loadScript("importExport.js"); loadScript("extension/options/import-export.js");
await flushAsyncWork(); await flushAsyncWork();
return globalThis.chrome; return globalThis.chrome;
} }
+37 -16
View File
@@ -1,8 +1,8 @@
const { afterEach, beforeEach, describe, expect, it, vi } = require("vitest");
const { const {
createChromeMock, createChromeMock,
evaluateScript, evaluateScript,
flushAsyncWork, flushAsyncWork,
fireDOMContentLoaded,
installCommonWindowMocks, installCommonWindowMocks,
loadHtmlString loadHtmlString
} = require("./helpers/extension-test-utils"); } = require("./helpers/extension-test-utils");
@@ -26,18 +26,38 @@ function bootImportExport(options) {
global.chrome = chrome; global.chrome = chrome;
window.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 createObjectURL = vi.fn(() => "blob:test");
const revokeObjectURL = vi.fn(); const revokeObjectURL = vi.fn();
vi.stubGlobal("URL", { Object.defineProperty(window.URL, "createObjectURL", {
createObjectURL, configurable: true,
revokeObjectURL 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 }; return { chrome, createObjectURL, revokeObjectURL };
} }
describe("importExport.js", () => { describe("options/import-export.js", () => {
beforeEach(() => { beforeEach(() => {
vi.useFakeTimers(); vi.useFakeTimers();
}); });
@@ -53,14 +73,15 @@ describe("importExport.js", () => {
bootImportExport(); bootImportExport();
expect(window.generateBackupFilename()).toBe( 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 () => { it("exports sync and local settings into a downloadable backup", async () => {
const clickSpy = vi Object.defineProperty(window.HTMLAnchorElement.prototype, "click", {
.spyOn(window.HTMLAnchorElement.prototype, "click") configurable: true,
.mockImplementation(() => {}); value: vi.fn()
});
const { createObjectURL, revokeObjectURL } = bootImportExport({ const { createObjectURL, revokeObjectURL } = bootImportExport({
syncData: { syncData: {
rememberSpeed: true, rememberSpeed: true,
@@ -74,7 +95,6 @@ describe("importExport.js", () => {
}); });
document.querySelector("#exportSettings").click(); document.querySelector("#exportSettings").click();
await flushAsyncWork();
expect(createObjectURL).toHaveBeenCalledTimes(1); expect(createObjectURL).toHaveBeenCalledTimes(1);
const blob = createObjectURL.mock.calls[0][0]; const blob = createObjectURL.mock.calls[0][0];
@@ -82,15 +102,15 @@ describe("importExport.js", () => {
expect(backup.settings.rememberSpeed).toBe(true); expect(backup.settings.rememberSpeed).toBe(true);
expect(backup.localSettings.customButtonIcons.faster.slug).toBe("rocket"); expect(backup.localSettings.customButtonIcons.faster.slug).toBe("rocket");
expect(clickSpy).toHaveBeenCalledTimes(1);
expect(revokeObjectURL).toHaveBeenCalledWith("blob:test"); expect(revokeObjectURL).toHaveBeenCalledWith("blob:test");
expect(document.querySelector("#status").textContent).toContain("exported"); expect(document.querySelector("#status").textContent).toContain("exported");
}); });
it("omits Lucide tags cache from exported localSettings", async () => { 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({ const { createObjectURL } = bootImportExport({
syncData: { rememberSpeed: true }, syncData: { rememberSpeed: true },
localData: { localData: {
@@ -103,7 +123,6 @@ describe("importExport.js", () => {
}); });
document.querySelector("#exportSettings").click(); document.querySelector("#exportSettings").click();
await flushAsyncWork();
const blob = createObjectURL.mock.calls[0][0]; const blob = createObjectURL.mock.calls[0][0];
const backup = JSON.parse(await blob.text()); const backup = JSON.parse(await blob.text());
@@ -159,6 +178,7 @@ describe("importExport.js", () => {
} }
vi.stubGlobal("FileReader", FakeFileReader); vi.stubGlobal("FileReader", FakeFileReader);
window.FileReader = FakeFileReader;
document.querySelector("#importSettings").click(); document.querySelector("#importSettings").click();
await flushAsyncWork(); await flushAsyncWork();
@@ -208,6 +228,7 @@ describe("importExport.js", () => {
} }
vi.stubGlobal("FileReader", FakeFileReader); vi.stubGlobal("FileReader", FakeFileReader);
window.FileReader = FakeFileReader;
document.querySelector("#importSettings").click(); document.querySelector("#importSettings").click();
await flushAsyncWork(); await flushAsyncWork();
+7 -5
View File
@@ -1,4 +1,3 @@
const { afterEach, describe, expect, it, vi } = require("vitest");
const { const {
createChromeMock, createChromeMock,
evaluateScript, evaluateScript,
@@ -31,8 +30,11 @@ function bootInject(options) {
); );
window.cancelIdleCallback = (id) => clearTimeout(id); window.cancelIdleCallback = (id) => clearTimeout(id);
evaluateScript("ui-icons.js"); evaluateScript("extension/shared/controller-utils.js");
evaluateScript("inject.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; return chrome;
} }
@@ -116,14 +118,14 @@ describe("inject.js helper logic", () => {
bootInject(); bootInject();
await flushAsyncWork(3); await flushAsyncWork(3);
window.tc.settings.siteRules = [{ pattern: "localhost", enabled: false }]; window.tc.settings.siteRules = [{ pattern: "example.org", enabled: false }];
window.captureSiteRuleBase(); window.captureSiteRuleBase();
expect(window.applySiteRuleOverrides()).toBe(true); expect(window.applySiteRuleOverrides()).toBe(true);
window.resetSettingsFromSiteRuleBase(); window.resetSettingsFromSiteRuleBase();
window.tc.settings.siteRules = [ window.tc.settings.siteRules = [
{ {
pattern: "localhost", pattern: "example.org",
controllerLocation: "bottom-left", controllerLocation: "bottom-left",
controllerMarginTop: 300, controllerMarginTop: 300,
controllerMarginBottom: -10, controllerMarginBottom: -10,
+10 -11
View File
@@ -38,11 +38,11 @@ async function bootInject({ sync = {}, local = {} } = {}) {
); );
globalThis.cancelIdleCallback = (id) => clearTimeout(id); globalThis.cancelIdleCallback = (id) => clearTimeout(id);
loadScript("shared/controller-utils.js"); loadScript("extension/shared/controller-utils.js");
loadScript("shared/key-bindings.js"); loadScript("extension/shared/key-bindings.js");
loadScript("shared/site-rules.js"); loadScript("extension/shared/site-rules.js");
loadScript("ui-icons.js"); loadScript("extension/shared/ui-icons.js");
loadScript("inject.js"); loadScript("extension/content/inject.js");
for (let i = 0; i < 3; i += 1) { for (let i = 0; i < 3; i += 1) {
await flushAsyncWork(); await flushAsyncWork();
@@ -215,11 +215,11 @@ describe("inject runtime", () => {
Promise.resolve().then(() => originalLocalGet(keys, callback)); Promise.resolve().then(() => originalLocalGet(keys, callback));
}); });
loadScript("shared/controller-utils.js"); loadScript("extension/shared/controller-utils.js");
loadScript("shared/key-bindings.js"); loadScript("extension/shared/key-bindings.js");
loadScript("shared/site-rules.js"); loadScript("extension/shared/site-rules.js");
loadScript("ui-icons.js"); loadScript("extension/shared/ui-icons.js");
loadScript("inject.js"); loadScript("extension/content/inject.js");
// Fast-forward 3000ms for delayed rescan to trigger // Fast-forward 3000ms for delayed rescan to trigger
vi.advanceTimersByTime(3000); vi.advanceTimersByTime(3000);
@@ -235,4 +235,3 @@ describe("inject runtime", () => {
vi.useRealTimers(); vi.useRealTimers();
}); });
}); });
+6 -7
View File
@@ -1,4 +1,3 @@
const { afterEach, describe, expect, it } = require("vitest");
const { const {
evaluateScript, evaluateScript,
loadHtmlString loadHtmlString
@@ -11,8 +10,8 @@ describe("lucide-client.js", () => {
it("builds icon URLs and rejects invalid slugs", () => { it("builds icon URLs and rejects invalid slugs", () => {
loadHtmlString("<!doctype html><html><body></body></html>"); loadHtmlString("<!doctype html><html><body></body></html>");
evaluateScript("ui-icons.js"); evaluateScript("extension/shared/ui-icons.js");
evaluateScript("lucide-client.js"); evaluateScript("extension/options/lucide-client.js");
expect(window.lucideIconSvgUrl("alarm-clock")).toContain( expect(window.lucideIconSvgUrl("alarm-clock")).toContain(
"/icons/alarm-clock.svg" "/icons/alarm-clock.svg"
@@ -23,8 +22,8 @@ describe("lucide-client.js", () => {
it("sanitizes SVG before persisting a Lucide icon", () => { it("sanitizes SVG before persisting a Lucide icon", () => {
loadHtmlString("<!doctype html><html><body></body></html>"); loadHtmlString("<!doctype html><html><body></body></html>");
evaluateScript("ui-icons.js"); evaluateScript("extension/shared/ui-icons.js");
evaluateScript("lucide-client.js"); evaluateScript("extension/options/lucide-client.js");
const sanitized = window.sanitizeLucideSvg(` const sanitized = window.sanitizeLucideSvg(`
<svg width="10" height="10" onclick="evil()"> <svg width="10" height="10" onclick="evil()">
@@ -43,8 +42,8 @@ describe("lucide-client.js", () => {
it("searches and ranks icon slugs by query", () => { it("searches and ranks icon slugs by query", () => {
loadHtmlString("<!doctype html><html><body></body></html>"); loadHtmlString("<!doctype html><html><body></body></html>");
evaluateScript("ui-icons.js"); evaluateScript("extension/shared/ui-icons.js");
evaluateScript("lucide-client.js"); evaluateScript("extension/options/lucide-client.js");
const results = window.searchLucideSlugs( const results = window.searchLucideSlugs(
{ {
+7 -7
View File
@@ -7,16 +7,16 @@ import {
} from "./helpers/browser.js"; } from "./helpers/browser.js";
async function setupOptions(overrides = {}) { async function setupOptions(overrides = {}) {
loadHtml("options.html"); loadHtml("extension/options/options.html");
globalThis.chrome = createChromeMock(overrides); globalThis.chrome = createChromeMock(overrides);
window.chrome = globalThis.chrome; window.chrome = globalThis.chrome;
globalThis.fetch = vi.fn(); globalThis.fetch = vi.fn();
loadScript("shared/controller-utils.js"); loadScript("extension/shared/controller-utils.js");
loadScript("shared/key-bindings.js"); loadScript("extension/shared/key-bindings.js");
loadScript("shared/popup-controls.js"); loadScript("extension/shared/popup-controls.js");
loadScript("ui-icons.js"); loadScript("extension/shared/ui-icons.js");
loadScript("lucide-client.js"); loadScript("extension/options/lucide-client.js");
loadScript("options.js"); loadScript("extension/options/options.js");
triggerDomContentLoaded(); triggerDomContentLoaded();
await flushAsyncWork(); await flushAsyncWork();
return globalThis.chrome; return globalThis.chrome;
+25 -19
View File
@@ -1,4 +1,3 @@
const { afterEach, beforeEach, describe, expect, it, vi } = require("vitest");
const { const {
createChromeMock, createChromeMock,
evaluateScript, evaluateScript,
@@ -11,7 +10,7 @@ const {
function bootOptions(options) { function bootOptions(options) {
const config = options || {}; const config = options || {};
loadHtmlFile("options.html"); loadHtmlFile("extension/options/options.html");
installCommonWindowMocks(); installCommonWindowMocks();
const chrome = createChromeMock({ const chrome = createChromeMock({
@@ -31,9 +30,12 @@ function bootOptions(options) {
); );
window.fetch = global.fetch; window.fetch = global.fetch;
evaluateScript("ui-icons.js"); evaluateScript("extension/shared/controller-utils.js");
evaluateScript("lucide-client.js"); evaluateScript("extension/shared/key-bindings.js");
evaluateScript("options.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(); fireDOMContentLoaded();
return chrome; return chrome;
@@ -119,7 +121,8 @@ describe("options.js", () => {
window.populatePopupControlBarEditor(["advance", "settings", "rewind"]); window.populatePopupControlBarEditor(["advance", "settings", "rewind"]);
window.createSiteRule({ pattern: "youtube.com" }); 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(".override-placement").checked = true;
ruleEl.querySelector(".site-controllerLocation").value = "top-right"; ruleEl.querySelector(".site-controllerLocation").value = "top-right";
ruleEl.querySelector(".site-controllerMarginTop").value = "300"; ruleEl.querySelector(".site-controllerMarginTop").value = "300";
@@ -167,19 +170,22 @@ describe("options.js", () => {
expect(savedSettings.controllerMarginTop).toBe(200); expect(savedSettings.controllerMarginTop).toBe(200);
expect(savedSettings.controllerMarginBottom).toBe(0); expect(savedSettings.controllerMarginBottom).toBe(0);
expect(savedSettings.popupControllerButtons).toEqual(["advance", "rewind"]); expect(savedSettings.popupControllerButtons).toEqual(["advance", "rewind"]);
expect(savedSettings.siteRules).toEqual([ expect(savedSettings.siteRules).toHaveLength(3);
{ expect(savedSettings.siteRules).toEqual(
pattern: "youtube.com", expect.arrayContaining([
enabled: true, expect.objectContaining({
controllerLocation: "top-right", pattern: "youtube.com",
controllerMarginTop: 200, enabled: true,
controllerMarginBottom: 0, controllerLocation: "top-right",
hideWithControls: true, controllerMarginTop: 200,
hideWithControlsTimer: 0.1, controllerMarginBottom: 0,
showPopupControlBar: false, hideWithControls: true,
popupControllerButtons: ["advance", "rewind"] hideWithControlsTimer: 0.1,
} showPopupControlBar: false,
]); popupControllerButtons: ["advance", "rewind"]
})
])
);
}); });
it("blocks save when a site rule regex is invalid", async () => { it("blocks save when a site rule regex is invalid", async () => {
+12 -12
View File
@@ -7,13 +7,13 @@ import {
} from "./helpers/browser.js"; } from "./helpers/browser.js";
async function setupPopup(overrides = {}) { async function setupPopup(overrides = {}) {
loadHtml("popup.html"); loadHtml("extension/popup/popup.html");
globalThis.chrome = createChromeMock(overrides); globalThis.chrome = createChromeMock(overrides);
window.chrome = globalThis.chrome; window.chrome = globalThis.chrome;
loadScript("shared/site-rules.js"); loadScript("extension/shared/site-rules.js");
loadScript("shared/popup-controls.js"); loadScript("extension/shared/popup-controls.js");
loadScript("ui-icons.js"); loadScript("extension/shared/ui-icons.js");
loadScript("popup.js"); loadScript("extension/popup/popup.js");
triggerDomContentLoaded(); triggerDomContentLoaded();
await flushAsyncWork(); await flushAsyncWork();
return globalThis.chrome; 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.getElementById("popupSpeed").textContent).toBe("1.75");
expect( expect(
document.querySelectorAll("#popupControlBar button").length 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." "Speeder is disabled for this site."
); );
expect(document.getElementById("popupSpeed").textContent).toBe("1.00"); expect(document.getElementById("popupSpeed").textContent).toBe("1.00");
@@ -85,9 +85,9 @@ describe("popup UI", () => {
); );
expect(chrome.browserAction.setIcon).toHaveBeenCalledWith({ expect(chrome.browserAction.setIcon).toHaveBeenCalledWith({
path: { path: {
19: "icons/icon19_disabled.png", 19: "assets/icons/icon19_disabled.png",
38: "icons/icon38_disabled.png", 38: "assets/icons/icon38_disabled.png",
48: "icons/icon48_disabled.png" 48: "assets/icons/icon48_disabled.png"
} }
}); });
}); });
@@ -105,13 +105,13 @@ describe("popup UI", () => {
}); });
document.getElementById("refresh").click(); document.getElementById("refresh").click();
expect(document.getElementById("status").innerText).toBe( expect(document.getElementById("status").textContent).toBe(
"Cannot run on this page." "Cannot run on this page."
); );
response = { status: "complete" }; response = { status: "complete" };
document.getElementById("refresh").click(); document.getElementById("refresh").click();
expect(document.getElementById("status").innerText).toBe( expect(document.getElementById("status").textContent).toBe(
"Scan complete. Closing..." "Scan complete. Closing..."
); );
vi.advanceTimersByTime(500); vi.advanceTimersByTime(500);
+21 -21
View File
@@ -1,4 +1,3 @@
const { afterEach, beforeEach, describe, expect, it, vi } = require("vitest");
const { const {
createChromeMock, createChromeMock,
evaluateScript, evaluateScript,
@@ -11,7 +10,7 @@ const {
function bootPopup(options) { function bootPopup(options) {
const config = options || {}; const config = options || {};
loadHtmlFile("popup.html"); loadHtmlFile("extension/popup/popup.html");
installCommonWindowMocks(); installCommonWindowMocks();
const chrome = createChromeMock({ const chrome = createChromeMock({
@@ -47,8 +46,10 @@ function bootPopup(options) {
global.chrome = chrome; global.chrome = chrome;
window.chrome = chrome; window.chrome = chrome;
evaluateScript("ui-icons.js"); evaluateScript("extension/shared/site-rules.js");
evaluateScript("popup.js"); evaluateScript("extension/shared/popup-controls.js");
evaluateScript("extension/shared/ui-icons.js");
evaluateScript("extension/popup/popup.js");
fireDOMContentLoaded(); fireDOMContentLoaded();
return chrome; return chrome;
@@ -90,25 +91,26 @@ describe("popup.js", () => {
}); });
it("builds sanitized popup buttons and refreshes speed after an action", async () => { it("builds sanitized popup buttons and refreshes speed after an action", async () => {
let speedQueryCount = 0;
const chrome = bootPopup({ const chrome = bootPopup({
syncData: { syncData: {
enabled: true, enabled: true,
controllerButtons: ["faster", "settings", "rewind", "faster"], controllerButtons: ["faster", "settings", "rewind", "faster"],
popupMatchHoverControls: true 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) => { chrome.tabs.sendMessage.mockImplementation((tabId, message, callback) => {
if (message.action === "run_action") { if (message.action === "run_action") {
callback({ speed: 1.75 }); callback({ speed: 1.75 });
@@ -117,7 +119,6 @@ describe("popup.js", () => {
callback({ speed: 1.0 }); callback({ speed: 1.0 });
}); });
document.dispatchEvent(new window.Event("DOMContentLoaded"));
await flushAsyncWork(); await flushAsyncWork();
const buttons = Array.from( const buttons = Array.from(
@@ -138,8 +139,6 @@ describe("popup.js", () => {
}); });
it("toggles enablement and closes after a successful refresh", async () => { it("toggles enablement and closes after a successful refresh", async () => {
vi.useFakeTimers();
const chrome = bootPopup({ const chrome = bootPopup({
syncData: { syncData: {
enabled: false enabled: false
@@ -147,6 +146,7 @@ describe("popup.js", () => {
}); });
await flushAsyncWork(); await flushAsyncWork();
vi.useFakeTimers();
expect(document.querySelector("#enable").classList.contains("hide")).toBe(false); expect(document.querySelector("#enable").classList.contains("hide")).toBe(false);
expect(document.querySelector("#disable").classList.contains("hide")).toBe(true); expect(document.querySelector("#disable").classList.contains("hide")).toBe(true);
@@ -158,9 +158,9 @@ describe("popup.js", () => {
); );
expect(chrome.browserAction.setIcon).toHaveBeenCalledWith({ expect(chrome.browserAction.setIcon).toHaveBeenCalledWith({
path: { path: {
19: "icons/icon19.png", 19: "assets/icons/icon19.png",
38: "icons/icon38.png", 38: "assets/icons/icon38.png",
48: "icons/icon48.png" 48: "assets/icons/icon48.png"
} }
}); });
+5 -5
View File
@@ -1,8 +1,8 @@
import controllerUtils from "../shared/controller-utils.js"; import controllerUtils from "../extension/shared/controller-utils.js";
import importExportUtils from "../shared/import-export.js"; import importExportUtils from "../extension/shared/import-export.js";
import keyBindingUtils from "../shared/key-bindings.js"; import keyBindingUtils from "../extension/shared/key-bindings.js";
import popupControls from "../shared/popup-controls.js"; import popupControls from "../extension/shared/popup-controls.js";
import siteRules from "../shared/site-rules.js"; import siteRules from "../extension/shared/site-rules.js";
describe("shared helpers", () => { describe("shared helpers", () => {
it("matches site rules and skips invalid regex patterns", () => { it("matches site rules and skips invalid regex patterns", () => {
+1 -1
View File
@@ -6,7 +6,7 @@ module.exports = defineConfig({
clearMocks: true, clearMocks: true,
globals: true, globals: true,
restoreMocks: true, restoreMocks: true,
include: ["tests/**/*.test.js"], include: ["tests/**/*.test.js", "tests/**/*.spec.js"],
setupFiles: ["./tests/setup.js"] setupFiles: ["./tests/setup.js"]
} }
}); });