fix(extension): request Firefox host permission during popup click

Preload popup settings before enabling the clip button so the permission request remains in the user action handler. Add a regression test for the request timing.
This commit is contained in:
tianma-if committed 2026-10-03 20:46:51 +08:00
1 parent 4242dedd24
commit dce5e2bc40
4 files changed
+77 -4

No files matched your search

+1 -1
View File
@@ -9,7 +9,7 @@
"dev": "vite --config vite.config.ts",
"lint:firefox": "web-ext lint --source-dir dist-firefox --output text",
"package:firefox": "bun run build:firefox && bun run lint:firefox && web-ext build --source-dir dist-firefox --artifacts-dir web-ext-artifacts --overwrite-dest",
"test": "bun test manifest.test.mjs context-menus.test.mjs image-clip.test.mjs tweet-clip.test.mjs selection-clip.test.mjs github-repo.test.mjs xhs-clip.test.mjs zhihu-clip.test.mjs reddit-clip.test.mjs"
"test": "bun test manifest.test.mjs context-menus.test.mjs popup.test.mjs image-clip.test.mjs tweet-clip.test.mjs selection-clip.test.mjs github-repo.test.mjs xhs-clip.test.mjs zhihu-clip.test.mjs reddit-clip.test.mjs"
},
"dependencies": {
"@mixmark-io/domino": "2.2.0",
+1 -1
View File
@@ -23,7 +23,7 @@
<span class="platform-tag">GitHub</span>
</div>
</div>
<button id="save" class="save-button" type="button">
<button id="save" class="save-button" type="button" disabled>
<svg aria-hidden="true" viewBox="0 0 24 24" width="18" height="18">
<path d="M5 4.75A1.75 1.75 0 0 1 6.75 3h8.836c.464 0 .91.184 1.238.513l1.663 1.663c.329.328.513.774.513 1.238v12.836A1.75 1.75 0 0 1 17.25 21H6.75A1.75 1.75 0 0 1 5 19.25V4.75Z" />
<path d="M8 3v5h7V3M8 21v-7h8v7" />
+64
View File
@@ -0,0 +1,64 @@
import { expect, test } from "bun:test";
test("requests instance permission during the popup click user action", async () => {
const previousChrome = globalThis.chrome;
const previousDocument = globalThis.document;
const listeners = new Map();
const saveButton = {
disabled: false,
addEventListener: (event, listener) => listeners.set(`save:${event}`, listener),
};
const settingsButton = { addEventListener: () => {} };
const status = { textContent: "", dataset: {} };
let finishLoadingSettings;
let insideClick = false;
let permissionRequests = 0;
let captures = 0;
globalThis.document = {
documentElement: { lang: "", dir: "" },
querySelectorAll: () => [],
querySelector: (selector) => ({ "#save": saveButton, "#settings": settingsButton, "#status": status })[selector],
};
globalThis.chrome = {
i18n: { getMessage: (key) => key, getUILanguage: () => "en" },
storage: { local: { get: () => new Promise((resolve) => { finishLoadingSettings = resolve; }) } },
permissions: {
request: ({ origins }) => {
expect(insideClick).toBe(true);
expect(origins).toEqual(["https://example.com/*"]);
permissionRequests += 1;
return Promise.resolve(true);
},
},
runtime: {
sendMessage: async (message) => {
expect(message).toEqual({ type: "captureCurrentPage" });
captures += 1;
return { ok: true };
},
},
};
try {
await import("./src/popup.ts");
expect(saveButton.disabled).toBe(true);
finishLoadingSettings({ instanceUrl: "https://example.com", token: "token", notebookId: "" });
await new Promise((resolve) => setTimeout(resolve, 0));
expect(saveButton.disabled).toBe(false);
insideClick = true;
const click = listeners.get("save:click")();
insideClick = false;
await click;
expect(permissionRequests).toBe(1);
expect(captures).toBe(1);
expect(status.textContent).toBe("savedToEdgeEver");
expect(saveButton.disabled).toBe(false);
} finally {
globalThis.chrome = previousChrome;
globalThis.document = previousDocument;
}
});
+11 -2
View File
@@ -1,5 +1,5 @@
import "./styles.css";
import { getSettings, requestInstancePermission } from "./extension";
import { getSettings, requestInstancePermission, type ExtensionSettings } from "./extension";
import { localizeDocument, t } from "./i18n";
localizeDocument();
@@ -7,6 +7,7 @@ localizeDocument();
const saveButton = document.querySelector<HTMLButtonElement>("#save");
const settingsButton = document.querySelector<HTMLButtonElement>("#settings");
const status = document.querySelector<HTMLParagraphElement>("#status");
let settings: ExtensionSettings | null = null;
const setStatus = (message: string, kind: "normal" | "error" | "success" = "normal") => {
if (status) {
@@ -15,12 +16,20 @@ const setStatus = (message: string, kind: "normal" | "error" | "success" = "norm
}
};
if (saveButton) saveButton.disabled = true;
void getSettings().then((loadedSettings) => {
settings = loadedSettings;
if (saveButton) saveButton.disabled = false;
}).catch((error: unknown) => {
setStatus(error instanceof Error ? error.message : t("saveFailed"), "error");
});
saveButton?.addEventListener("click", async () => {
if (!settings) return;
saveButton.disabled = true;
setStatus(t("readingAndSaving"));
try {
const settings = await getSettings();
if (!settings.instanceUrl || !settings.token) {
throw new Error(t("completePluginConfiguration"));
}