diff --git a/e2e/keep-alive.spec.ts b/e2e/keep-alive.spec.ts index 079d49e12..3e20cb040 100644 --- a/e2e/keep-alive.spec.ts +++ b/e2e/keep-alive.spec.ts @@ -7,14 +7,19 @@ const HEARTBEAT_VALIDATION_WINDOW_MS = 31_000; const openRuntimeSettings = async (context: Parameters[0], extensionId: string) => { const page = await openOptionsPage(context, extensionId); - await page - .getByTestId("view-toggle") - .or(page.getByTestId("mobile-search")) - .first() - .waitFor({ state: "visible", timeout: 30_000 }); - await page.goto(`chrome-extension://${extensionId}/src/options.html#/settings`); - await expect(page.getByTestId("setting-page")).toBeVisible({ timeout: 20_000 }); - return page; + try { + await page + .getByTestId("view-toggle") + .or(page.getByTestId("mobile-search")) + .first() + .waitFor({ state: "visible", timeout: 30_000 }); + await page.goto(`chrome-extension://${extensionId}/src/options.html#/settings`); + await expect(page.getByTestId("setting-page")).toBeVisible({ timeout: 20_000 }); + return page; + } catch (error) { + if (!page.isClosed()) await page.close(); + throw error; + } }; type CdpTargetMessage = { diff --git a/src/pages/components/use-is-mobile.test.ts b/src/pages/components/use-is-mobile.test.ts index 4a8d8f69f..b87a75b43 100644 --- a/src/pages/components/use-is-mobile.test.ts +++ b/src/pages/components/use-is-mobile.test.ts @@ -32,6 +32,12 @@ function stubMatchMedia(initialMatches: boolean, matchesOnSubscribe?: boolean) { } describe("useIsMobile 视口断点", () => { + it("视口 < 768px 时返回 true", () => { + stubMatchMedia(true); + const { result } = renderHook(() => useIsMobile()); + expect(result.current).toBe(true); + }); + it("监听 change 事件,视口变化时更新返回值", () => { const mql = stubMatchMedia(false); const { result } = renderHook(() => useIsMobile()); diff --git a/src/pages/install/useInstallData.test.ts b/src/pages/install/useInstallData.test.ts index 502aac12d..aae5da100 100644 --- a/src/pages/install/useInstallData.test.ts +++ b/src/pages/install/useInstallData.test.ts @@ -185,6 +185,7 @@ describe("assembleInstallView 组装安装视图", () => { describe("useInstallData 数据流编排", () => { afterEach(() => { + vi.restoreAllMocks(); vi.clearAllMocks(); window.history.replaceState({}, "", "/install.html"); }); @@ -249,7 +250,7 @@ describe("useInstallData 数据流编排", () => { expect(state.view.oldCode).toBe("// old code"); }); - describe("安装成功后离开安装页:独立新标签应关闭,网页链接接管的原标签应返回上一页", () => { + describe("安装成功后离开安装页:独立新标签应关闭,同标签内被重定向而来应返回上一页", () => { const setupReady = async (paramOptions: Record = {}) => { window.history.replaceState({}, "", "/install.html?uuid=u1"); const metadata = { name: ["示例脚本"], version: ["1.0.0"], match: ["https://e.com/*"] }; @@ -294,13 +295,32 @@ describe("useInstallData 数据流编排", () => { await act(async () => { await result.current.install(); - // leaveInstallPage 延后到 install() 里 300ms 的 setTimeout 再叠一帧 rAF 才真正执行,多等一点确保已触发 await new Promise((r) => setTimeout(r, 320)); }); expect(backSpy).toHaveBeenCalledOnce(); expect(closeSpy).not.toHaveBeenCalled(); }); + + it("byWebRequest 的直接 URL 入口应把来源标记传给脚本匹配", async () => { + window.history.replaceState({}, "", "/install.html?byWebRequest=1&url=https://e.com/x.user.js"); + const metadata = { name: ["示例脚本"], version: ["1.0.0"], match: ["https://e.com/*"] }; + (fetchScriptBody as Mock).mockResolvedValue("// code"); + (parseMetadata as Mock).mockReturnValue(metadata); + (prepareScriptByCode as Mock).mockResolvedValue({ script: makeAction(metadata) }); + + const { result } = renderHook(() => useInstallData()); + await waitFor(() => expect(result.current.state.status).toBe("ready")); + + expect(prepareScriptByCode).toHaveBeenCalledWith( + "// code", + "https://e.com/x.user.js", + undefined, + false, + undefined, + { byWebRequest: true } + ); + }); }); it("?skill= 时读取技能数据进入 skill 状态", async () => { diff --git a/src/pages/install/useInstallData.ts b/src/pages/install/useInstallData.ts index f578a48dc..44f1894c1 100644 --- a/src/pages/install/useInstallData.ts +++ b/src/pages/install/useInstallData.ts @@ -304,7 +304,9 @@ export function useInstallData(): UseInstallData { }); const metadata = parseMetadata(code); if (!metadata) throw new Error(t("install:script_info_load_failed")); - await loadFromInfo(buildScriptInfo(uuidv4(), code, parsed.href, metadata), false, {}); + await loadFromInfo(buildScriptInfo(uuidv4(), code, parsed.href, metadata), false, { + byWebRequest: byWebRequestRef.current, + }); } else if (fid) { const handle = await loadHandle(fid); if (!handle) throw new Error(t("install:script_info_load_failed")); diff --git a/src/pkg/utils/skill-zip.test.ts b/src/pkg/utils/skill-zip.test.ts index 6273397be..3c1e1f13b 100644 --- a/src/pkg/utils/skill-zip.test.ts +++ b/src/pkg/utils/skill-zip.test.ts @@ -190,4 +190,27 @@ description: 淘宝购物助手 expect(toolMeta!.params[0].name).toBe("pageType"); expect(toolMeta!.params[1].name).toBe("tabId"); }); + + it("嵌套目录 ZIP 的完整流程:解析 → 验证 SKILL.md → 验证 SkillScript", async () => { + const zipData = await createTestZip({ + "taobao-skill/SKILL.md": `---\nname: nested-skill\ndescription: 嵌套目录测试\n---\n嵌套 Skill 提示词。`, + "taobao-skill/scripts/extract.js": VALID_SKILLSCRIPT_CODE, + "taobao-skill/references/guide.txt": "使用指南内容", + }); + + const zipResult = await parseSkillZip(zipData); + + const parsed = parseSkillMd(zipResult.skillMd); + expect(parsed).not.toBeNull(); + expect(parsed!.metadata.name).toBe("nested-skill"); + + expect(zipResult.scripts).toHaveLength(1); + const toolMeta = parseSkillScriptMetadata(zipResult.scripts[0].code); + expect(toolMeta).not.toBeNull(); + expect(toolMeta!.name).toBe("taobao_extract"); + + expect(zipResult.references).toHaveLength(1); + expect(zipResult.references[0].name).toBe("guide.txt"); + expect(zipResult.references[0].content).toBe("使用指南内容"); + }); });