325 lines
9.4 KiB
TypeScript
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
|
|
};
|
|
}
|