import { JSDOM } from "jsdom"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { installPageHook } from "../src/page/hook"; import { CANDIDATE_ANALYSIS_MESSAGE_TYPE, CANDIDATE_REQUEST_MESSAGE_TYPE, EXTENSION_MESSAGE_SOURCE, HOOK_READY_MESSAGE_TYPE, RESULT_MESSAGE_TYPE } from "../src/shared/message-types"; import type { ExtractAfterSearchRatesResult } from "../src/shared/result-types"; describe("page hook", () => { let dom: JSDOM; beforeEach(() => { vi.useFakeTimers(); dom = new JSDOM("", { url: "https://xingtu.cn/ad/creator/author-homepage/douyin-video/6629661559960371207" }); }); afterEach(() => { dom.window.close(); vi.useRealTimers(); }); test("reads fetch bodies through response.clone() and leaves the original response intact", async () => { const payload = JSON.stringify({ metrics: { personal_video_after_search_rate: "0.5% - 1%", single_video_after_search_rate: "1% - 2%" } }); const response = new Response(payload, { headers: { "content-type": "application/json" }, status: 200 }); const cloneSpy = vi.spyOn(response, "clone"); const originalFetch = vi.fn().mockResolvedValue(response); const postMessage = vi.fn(); installPageHook({ extractAfterSearchRates: () => successExtraction(), postMessage, timeoutMs: 5_000, window: createHookWindow(dom.window, originalFetch) }); const returnedResponse = await dom.window.fetch("https://api.xingtu.cn/creator/metrics"); await vi.runAllTimersAsync(); expect(cloneSpy).toHaveBeenCalledTimes(1); expect(await returnedResponse.text()).toBe(payload); }); test("keeps requests alive if extraction throws", async () => { const response = new Response(JSON.stringify({ ok: true }), { headers: { "content-type": "application/json" }, status: 200 }); const originalFetch = vi.fn().mockResolvedValue(response); installPageHook({ extractAfterSearchRates: () => { throw new Error("boom"); }, postMessage: vi.fn(), timeoutMs: 5_000, window: createHookWindow(dom.window, originalFetch) }); const returnedResponse = await dom.window.fetch("https://api.xingtu.cn/creator/metrics"); expect(returnedResponse.status).toBe(200); expect(await returnedResponse.json()).toEqual({ ok: true }); }); test("posts a structured success result when extraction succeeds", async () => { const response = new Response(JSON.stringify({ any: "payload" }), { headers: { "content-type": "application/json" }, status: 200 }); const postMessage = vi.fn(); installPageHook({ extractAfterSearchRates: () => successExtraction(), postMessage, timeoutMs: 5_000, window: createHookWindow(dom.window, vi.fn().mockResolvedValue(response)) }); await dom.window.fetch("https://api.xingtu.cn/creator/metrics"); await vi.runAllTimersAsync(); expect(postMessage).toHaveBeenCalledWith( expect.objectContaining({ payload: expect.objectContaining({ matchedRequestUrl: "https://api.xingtu.cn/creator/metrics", pageStarId: "6629661559960371207", routeKey: "6629661559960371207::/ad/creator/author-homepage/douyin-video/6629661559960371207::1", stage: "captured", success: true }), source: EXTENSION_MESSAGE_SOURCE, type: RESULT_MESSAGE_TYPE }), "*" ); }); test("posts one timeout result if no successful capture appears", async () => { const postMessage = vi.fn(); installPageHook({ extractAfterSearchRates: () => unmatchedExtraction(), postMessage, timeoutMs: 100, window: createHookWindow(dom.window, vi.fn()) }); await vi.advanceTimersByTimeAsync(100); expect(postMessage).toHaveBeenNthCalledWith( 2, expect.objectContaining({ payload: expect.objectContaining({ reason: "Timed out waiting for after-search-rate capture", stage: "timeout", success: false }), source: EXTENSION_MESSAGE_SOURCE, type: RESULT_MESSAGE_TYPE }), "*" ); }); test("announces hook-ready immediately after installation", () => { const postMessage = vi.fn(); installPageHook({ extractAfterSearchRates: () => unmatchedExtraction(), postMessage, timeoutMs: 5_000, window: createHookWindow(dom.window, vi.fn()) }); expect(postMessage).toHaveBeenCalledWith( expect.objectContaining({ payload: expect.objectContaining({ pageStarId: "6629661559960371207", routeKey: "6629661559960371207::/ad/creator/author-homepage/douyin-video/6629661559960371207::1" }), source: EXTENSION_MESSAGE_SOURCE, type: HOOK_READY_MESSAGE_TYPE }), "*" ); }); test("emits a candidate-request diagnostic before extraction", async () => { const response = new Response(JSON.stringify({ any: "payload" }), { headers: { "content-type": "application/json" }, status: 200 }); const postMessage = vi.fn(); installPageHook({ extractAfterSearchRates: () => unmatchedExtraction(), postMessage, timeoutMs: 5_000, window: createHookWindow(dom.window, vi.fn().mockResolvedValue(response)) }); await dom.window.fetch("https://api.xingtu.cn/creator/value"); await vi.runAllTimersAsync(); expect(postMessage).toHaveBeenCalledWith( expect.objectContaining({ payload: expect.objectContaining({ requestMethod: "GET", requestUrl: "https://api.xingtu.cn/creator/value", routeKey: "6629661559960371207::/ad/creator/author-homepage/douyin-video/6629661559960371207::1", status: 200 }), source: EXTENSION_MESSAGE_SOURCE, type: CANDIDATE_REQUEST_MESSAGE_TYPE }), "*" ); }); test("emits a candidate-analysis diagnostic after evaluating a candidate response", async () => { const response = new Response( JSON.stringify({ data: { avg_search_after_view_rate: 0.0015, avg_search_after_view_rate_rank_percent: 0.9 }, status_code: 0 }), { headers: { "content-type": "application/json" }, status: 200 } ); const postMessage = vi.fn(); installPageHook({ extractAfterSearchRates: () => unmatchedExtraction(), postMessage, timeoutMs: 5_000, window: createHookWindow(dom.window, vi.fn().mockResolvedValue(response)) }); await dom.window.fetch("https://api.xingtu.cn/creator/value"); await vi.runAllTimersAsync(); expect(postMessage).toHaveBeenCalledWith( expect.objectContaining({ payload: expect.objectContaining({ extractorLevel: "none", matched: false, requestUrl: "https://api.xingtu.cn/creator/value", routeKey: "6629661559960371207::/ad/creator/author-homepage/douyin-video/6629661559960371207::1", signalEntries: [ "$.data.avg_search_after_view_rate=0.0015", "$.data.avg_search_after_view_rate_rank_percent=0.9" ], success: false, topLevelKeys: ["data", "status_code"] }), source: EXTENSION_MESSAGE_SOURCE, type: CANDIDATE_ANALYSIS_MESSAGE_TYPE }), "*" ); }); test("prevents duplicate patching", async () => { const response = new Response(JSON.stringify({ any: "payload" }), { headers: { "content-type": "application/json" }, status: 200 }); const fetchSpy = vi.fn().mockResolvedValue(response); const postMessage = vi.fn(); const hookWindow = createHookWindow(dom.window, fetchSpy); const firstInstall = installPageHook({ extractAfterSearchRates: () => successExtraction(), postMessage, timeoutMs: 5_000, window: hookWindow }); const secondInstall = installPageHook({ extractAfterSearchRates: () => successExtraction(), postMessage, timeoutMs: 5_000, window: hookWindow }); await dom.window.fetch("https://api.xingtu.cn/creator/metrics"); await vi.runAllTimersAsync(); expect(firstInstall.alreadyInstalled).toBe(false); expect(secondInstall.alreadyInstalled).toBe(true); expect(postMessage).toHaveBeenCalledTimes(4); }); }); function createHookWindow(window: Window, fetchImpl: typeof fetch) { Object.defineProperty(window, "fetch", { configurable: true, value: fetchImpl, writable: true }); class FakeXMLHttpRequest { addEventListener() {} open() {} send() {} } Object.defineProperty(window, "XMLHttpRequest", { configurable: true, value: FakeXMLHttpRequest, writable: true }); return window as Window & typeof globalThis & { XMLHttpRequest: typeof FakeXMLHttpRequest; fetch: typeof fetch; }; } function successExtraction(): ExtractAfterSearchRatesResult { return { extractorLevel: "label-value", matched: true, rawPathHints: ["$.metrics[0]"], rates: { personalVideoAfterSearchRate: "0.5% - 1%", singleVideoAfterSearchRate: "1% - 2%" }, success: true }; } function unmatchedExtraction(): ExtractAfterSearchRatesResult { return { extractorLevel: "none", matched: false, rawPathHints: [], reason: "No signals found", success: false }; }