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: 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()
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) { 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") });
} }
}); });
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 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;
File renamed without changes.
+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"
] ]
} }
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" /> <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=""
File renamed without changes.
File renamed without changes.
+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 = "";
} }
}); });
File renamed without changes.
@@ -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",
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(); 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
+5 -1
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,8 +118,12 @@ 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();
if (vi && typeof vi.isFakeTimers === "function" && vi.isFakeTimers()) {
await vi.advanceTimersByTimeAsync(0);
} else {
await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0));
} }
}
} }
function pickStorageValues(data, keys) { function pickStorageValues(data, keys) {
+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;
+16 -10
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,8 +170,10 @@ 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(
expect.arrayContaining([
expect.objectContaining({
pattern: "youtube.com", pattern: "youtube.com",
enabled: true, enabled: true,
controllerLocation: "top-right", controllerLocation: "top-right",
@@ -178,8 +183,9 @@ describe("options.js", () => {
hideWithControlsTimer: 0.1, hideWithControlsTimer: 0.1,
showPopupControlBar: false, showPopupControlBar: false,
popupControllerButtons: ["advance", "rewind"] 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);
+20 -20
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,23 +91,24 @@ 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;
chrome.tabs.executeScript callback(
.mockImplementationOnce((tabId, details, callback) => { speedQueryCount <= 2
callback([ ? [
{ speed: 1.25, preferred: false }, { speed: 1.25, preferred: false },
{ speed: 1.5, preferred: true } { speed: 1.5, preferred: true }
]); ]
}) : [{ speed: 1.75, 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) => {
@@ -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"]
} }
}); });