diff --git a/src/pages/options/routes/ScriptEditor/tabs/SettingsPane.test.tsx b/src/pages/options/routes/ScriptEditor/tabs/SettingsPane.test.tsx index 63f2b6f06..51ab7e726 100644 --- a/src/pages/options/routes/ScriptEditor/tabs/SettingsPane.test.tsx +++ b/src/pages/options/routes/ScriptEditor/tabs/SettingsPane.test.tsx @@ -259,6 +259,47 @@ describe("SettingsPane 网站匹配/排除", () => { fireEvent.click(screen.getByText(t("confirm"), { selector: "button" })); expect(resetMatch).toHaveBeenCalledWith("u1", undefined); }); + + it("重置匹配后应恢复脚本自带规则而非清空列表", async () => { + render(); + await screen.findByText("*://user.com/*"); + fireEvent.click(screen.getAllByText(t("reset"), { selector: "button" })[0]); + fireEvent.click(screen.getByText(t("confirm"), { selector: "button" })); + // 用户添加的 user.com 被清除,脚本自带的 script.com 应保留并标记为「脚本」 + expect(screen.getByText("*://script.com/*")).toBeInTheDocument(); + expect(screen.queryByText("*://user.com/*")).toBeNull(); + expect(screen.getByText(t("editor:from_script"))).toBeInTheDocument(); + }); + + it("重置排除后应恢复脚本自带规则而非清空列表", async () => { + fetchScript.mockResolvedValue({ + ...sampleScript(), + metadata: { ...sampleScript().metadata, exclude: ["*://ads.script.com/*"] }, + selfMetadata: { ...sampleScript().selfMetadata, exclude: ["*://ads.script.com/*", "*://exclude.com/*"] }, + }); + render(); + await screen.findByText("*://exclude.com/*"); + fireEvent.click(screen.getAllByText(t("reset"), { selector: "button" })[1]); + fireEvent.click(screen.getByText(t("confirm"), { selector: "button" })); + // 用户添加的 exclude.com 被清除,脚本自带的 ads.script.com 应保留 + expect(screen.getByText("*://ads.script.com/*")).toBeInTheDocument(); + expect(screen.queryByText("*://exclude.com/*")).toBeNull(); + }); + + it("删除最后一项匹配后应回落脚本自带规则而非清空列表", async () => { + fetchScript.mockResolvedValue({ + ...sampleScript(), + selfMetadata: { exclude: ["*://exclude.com/*"] }, + }); + render(); + await screen.findByText("*://script.com/*"); + fireEvent.click(screen.getByLabelText(`${t("delete")} *://script.com/*`)); + fireEvent.click(screen.getByText(t("confirm"), { selector: "button" })); + // 后端 selfMetadataUpdate 收到空集即删除覆盖,生效列表回落 metadata.match,脚本自带规则应保留 + expect(resetMatch).toHaveBeenCalledWith("u1", []); + expect(screen.getByText("*://script.com/*")).toBeInTheDocument(); + expect(screen.getByText(t("editor:from_script"))).toBeInTheDocument(); + }); }); describe("SettingsPane 授权管理(CORS)", () => { diff --git a/src/pages/options/routes/ScriptEditor/tabs/SettingsPane.tsx b/src/pages/options/routes/ScriptEditor/tabs/SettingsPane.tsx index 2b3ea1f7b..d2200b79e 100644 --- a/src/pages/options/routes/ScriptEditor/tabs/SettingsPane.tsx +++ b/src/pages/options/routes/ScriptEditor/tabs/SettingsPane.tsx @@ -234,14 +234,27 @@ function SettingsPaneContent({ uuid, data }: SettingsPaneProps & { data: Setting // ===== 匹配 / 排除 ===== const setMatchList = (kind: "match" | "exclude", next: string[] | undefined) => { + // next 为 undefined(重置)或空数组(删除最后一项)时后端 selfMetadataUpdate 均删除用户覆盖, + // 生效列表回落到脚本自带的 metadata,前端乐观更新保持一致 + const override = next?.length ? next : undefined; + const metaList = kind === "match" ? metaMatch : metaExclude; if (kind === "match") { void scriptClient.resetMatch(uuid, next); - setMatches(next ?? []); + setMatches(override ?? metaList); } else { void scriptClient.resetExclude(uuid, next); - setExcludes(next ?? []); + setExcludes(override ?? metaList); } - patchSelf({ [kind]: next ?? [] }); + setScript((prev) => { + if (!prev) return prev; + const selfMetadata = { ...prev.selfMetadata }; + if (override === undefined) { + delete selfMetadata[kind]; + } else { + selfMetadata[kind] = override; + } + return { ...prev, selfMetadata }; + }); }; const openBulkMatch = (kind: "match" | "exclude") => { setBulkMatchValue("");