Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 26 additions & 14 deletions e2e/popup-matching-regressions.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test, expect } from "./fixtures";
import { test, expect, startMockServer, type MockServer } from "./server-fixtures";
import { installScriptByCode } from "./utils";
import type { Page } from "@playwright/test";

Expand All @@ -7,19 +7,19 @@ function scriptCode(name: string, rule: "match" | "include") {
// @name ${name}
// @namespace issue-1591-e2e
// @version 1.0.0
// @${rule} https://example.com/*
// @${rule} http://sitea.test/*
// @grant none
// ==/UserScript==
console.log("${name}");`;
}

async function getTargetTab(extensionPage: Page) {
return extensionPage.evaluate(async () => {
async function getTargetTab(extensionPage: Page, targetUrl: string) {
return extensionPage.evaluate(async (targetUrl) => {
const tabs = await chrome.tabs.query({});
const tab = tabs.find((item) => item.url?.startsWith("https://example.com/"));
const tab = tabs.find((item) => item.url === targetUrl);
if (!tab?.id || !tab.url) throw new Error("target tab not found");
return { tabId: tab.id, url: tab.url };
});
}, targetUrl);
}

async function verifyExcludeRoundTrip(
Expand All @@ -41,7 +41,7 @@ async function verifyExcludeRoundTrip(

const exclude = await chrome.runtime.sendMessage({
action: "serviceWorker/script/excludeUrl",
data: { uuid: script.uuid, excludePattern: "*://example.com/*", remove: false },
data: { uuid: script.uuid, excludePattern: "*://sitea.test/*", remove: false },
});
if (exclude.code) throw new Error(`exclude failed: ${JSON.stringify(exclude)}`);

Expand All @@ -51,7 +51,7 @@ async function verifyExcludeRoundTrip(

const unexclude = await chrome.runtime.sendMessage({
action: "serviceWorker/script/excludeUrl",
data: { uuid: script.uuid, excludePattern: "*://example.com/*", remove: true },
data: { uuid: script.uuid, excludePattern: "*://sitea.test/*", remove: true },
});
if (unexclude.code) throw new Error(`unexclude failed: ${JSON.stringify(unexclude)}`);

Expand All @@ -66,6 +66,18 @@ async function verifyExcludeRoundTrip(
}

test.describe("Issue 1591: Popup exclusion regression", () => {
let server: MockServer;
let targetUrl: string;

test.beforeEach(async () => {
server = await startMockServer();
targetUrl = server.url("sitea.test", "/page");
});

test.afterEach(async () => {
await server.close();
});

test("@match script remains visible and reversible after excluding the current site", async ({
context,
extensionId,
Expand All @@ -75,9 +87,9 @@ test.describe("Issue 1591: Popup exclusion regression", () => {
const target = await context.newPage();
const extensionPage = await context.newPage();
try {
await target.goto("https://example.com/", { waitUntil: "domcontentloaded" });
await target.goto(targetUrl, { waitUntil: "domcontentloaded" });
await extensionPage.goto(`chrome-extension://${extensionId}/src/options.html`);
const result = await verifyExcludeRoundTrip(extensionPage, await getTargetTab(extensionPage), name);
const result = await verifyExcludeRoundTrip(extensionPage, await getTargetTab(extensionPage, targetUrl), name);
expect(result).toEqual({ excludedIsEffective: false, restoredIsEffective: true });
} finally {
await extensionPage.close();
Expand All @@ -94,9 +106,9 @@ test.describe("Issue 1591: Popup exclusion regression", () => {
const target = await context.newPage();
const extensionPage = await context.newPage();
try {
await target.goto("https://example.com/", { waitUntil: "domcontentloaded" });
await target.goto(targetUrl, { waitUntil: "domcontentloaded" });
await extensionPage.goto(`chrome-extension://${extensionId}/src/options.html`);
const result = await verifyExcludeRoundTrip(extensionPage, await getTargetTab(extensionPage), name);
const result = await verifyExcludeRoundTrip(extensionPage, await getTargetTab(extensionPage, targetUrl), name);
expect(result).toEqual({ excludedIsEffective: false, restoredIsEffective: true });
} finally {
await extensionPage.close();
Expand All @@ -117,9 +129,9 @@ test.describe("Issue 1591: Popup exclusion regression", () => {
const target = await context.newPage();
const extensionPage = await context.newPage();
try {
await target.goto("https://example.com/", { waitUntil: "domcontentloaded" });
await target.goto(targetUrl, { waitUntil: "domcontentloaded" });
await extensionPage.goto(`chrome-extension://${extensionId}/src/options.html`);
const targetTab = await getTargetTab(extensionPage);
const targetTab = await getTargetTab(extensionPage, targetUrl);
const popupData = await extensionPage.evaluate(
({ tabId, url }) =>
chrome.runtime.sendMessage({ action: "serviceWorker/popup/getPopupData", data: { tabId, url } }),
Expand Down
5 changes: 5 additions & 0 deletions packages/chrome-extension-mock/extension.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export default class Extension {
inIncognitoContext = false;

// 默认已授权访问 file://;需要未授权场景的测试自行 spyOn 覆写。
isAllowedFileSchemeAccess(): Promise<boolean> {
return Promise.resolve(true);
}
}
2 changes: 2 additions & 0 deletions packages/chrome-extension-mock/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Permissions from "./permissions";
import Extension from "./extension";
import MockUserScripts from "./user_scripts";
import Action from "./action";
import WebNavigation from "./web_navigation";

const chromeMock = {
tabs: new MockTab(),
Expand All @@ -26,6 +27,7 @@ const chromeMock = {
extension: new Extension(),
userScripts: new MockUserScripts(),
action: new Action(),
webNavigation: new WebNavigation(),
init() {
this.downloads.reset();
this.permissions.reset();
Expand Down
2 changes: 1 addition & 1 deletion packages/chrome-extension-mock/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export default class Runtime {
},
],
backScriptList: [],
isBlacklist: false,
pageStatus: "ok",
};
}

Expand Down
8 changes: 8 additions & 0 deletions packages/chrome-extension-mock/web_navigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default class WebNavigation {
// 默认无框架资料;需要框架的测试自行 spyOn 覆写返回值。
getAllFrames(
_details: chrome.webNavigation.GetAllFrameDetails
): Promise<chrome.webNavigation.GetAllFrameResultDetails[] | null> {
return Promise.resolve([]);
}
}
2 changes: 2 additions & 0 deletions src/app/cache_key.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export const CACHE_KEY_IMPORT_FILE = "importFile:"; // importFile 导入文件
export const CACHE_KEY_TAB_SCRIPT = "tabScript:";
// 记录某 tab 最近一次 content script 报到的 origin,用于判定「本页扩展是否触及得到」
export const CACHE_KEY_TAB_LOADED = "tabLoaded:";
export const CACHE_KEY_SET_VALUE = "setValue:";
export const CACHE_KEY_PERMISSION = "permission:";
export const CACHE_KEY_SKILL_INSTALL = "skillInstall:"; // Skill ZIP 待安装数据缓存
8 changes: 8 additions & 0 deletions src/app/service/content/scripting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,14 @@ export default class ScriptingRuntime {

pageLoad() {
const client = new RuntimeClient(this.senderToExt);
// bfcache 还原不会重新执行 content script,pageLoad 因此只发生一次;
// 但页面里的脚本仍在运行,需要补一次上报,否则 Popup 会误判本页没有脚本在跑。
// 只有顶层 frame 参与判定,子 frame 不必上报。
if (window.top === window) {
window.addEventListener("pageshow", (e) => {
if (e.persisted) client.pageShow();
});
}
// 向service_worker请求脚本列表及环境信息
client.pageLoad().then((o) => {
if (!o.ok) return;
Expand Down
11 changes: 8 additions & 3 deletions src/app/service/service_worker/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { type Resource } from "@App/app/repo/resource";
import { type Subscribe } from "@App/app/repo/subscribe";
import { type Logger } from "@App/app/repo/logger";
import { type Permission } from "@App/app/repo/permission";
import type { InstallSource, ScriptMenu, ScriptMenuItem, TBatchUpdateListAction } from "./types";
import type { InstallSource, ScriptMenu, ScriptMenuItem, TBatchUpdateListAction, TPopupPageStatus } from "./types";
import { Client } from "@Packages/message/client";
import type { MessageSend } from "@Packages/message/types";
import type PermissionVerify from "./permission_verify";
Expand Down Expand Up @@ -234,6 +234,11 @@ export class RuntimeClient extends Client {
return this.doThrow("pageLoad");
}

/** bfcache 还原上报:只告知本页仍在运行,不请求脚本 */
pageShow() {
return this.do("pageShow");
}

scriptLoad(flag: string, uuid: string) {
return this.do("scriptLoad", { flag, uuid });
}
Expand All @@ -245,8 +250,8 @@ export type GetPopupDataReq = {
};

export type GetPopupDataRes = {
// 在黑名单
isBlacklist: boolean;
// 当前页状态:非 ok 时 scriptList 为空,由 Popup 说明原因
pageStatus: TPopupPageStatus;
scriptList: ScriptMenu[];
backScriptList: ScriptMenu[];
};
Expand Down
Loading
Loading