Reorganize extension source layout
@@ -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
|
||||||
|
|||||||
@@ -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?
|
||||||
|
|||||||
@@ -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") });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -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,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=""
|
||||||
@@ -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",
|
||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,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;
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||