star-chart-search-enhancer/tests/page-hook.test.ts

325 lines
9.4 KiB
TypeScript

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
};
}