744 lines
24 KiB
TypeScript

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { act, cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import type { ReactNode } from "react";
import { MemoryRouter } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("./lib/api", () => ({
cancelJdQrLogin: vi.fn(),
cancelTmallQrLogin: vi.fn(),
clearJdManagedSession: vi.fn(),
clearJdSessionManagerConfig: vi.fn(),
clearPlatformSession: vi.fn(),
clearTmallManagedSession: vi.fn(),
clearTmallSessionManagerConfig: vi.fn(),
confirmTask: vi.fn(),
createTask: vi.fn(),
createTaskEventsSource: vi.fn(() => ({
addEventListener: vi.fn(),
close: vi.fn(),
removeEventListener: vi.fn()
})),
deleteTask: vi.fn(),
getJdKeywordPreview: vi.fn(),
getJdLiveSession: vi.fn(),
getJdQrLoginState: vi.fn(),
getJdSessionManager: vi.fn(),
getHistoryTasks: vi.fn(),
getPlatformReadiness: vi.fn(),
getPlatformSession: vi.fn(),
getTask: vi.fn(),
getTaskCandidates: vi.fn(),
getTaskReport: vi.fn(),
getTmallLiveSession: vi.fn(),
getTmallQrLoginState: vi.fn(),
getTmallSessionManager: vi.fn(),
importJdManagedSession: vi.fn(),
importTmallManagedSession: vi.fn(),
preparePlatform: vi.fn(),
resumeJdQrLoginManualRecovery: vi.fn(),
runJdSessionManagerHealthCheck: vi.fn(),
runJdSessionManagerRecovery: vi.fn(),
runTmallSessionManagerHealthCheck: vi.fn(),
retryTaskPlatform: vi.fn(),
startJdQrLogin: vi.fn(),
startTmallQrLogin: vi.fn(),
updateJdSessionManagerConfig: vi.fn(),
updateTmallSessionManagerConfig: vi.fn()
}));
import { App, NewTaskPage } from "./App";
import {
cancelJdQrLogin,
cancelTmallQrLogin,
clearJdManagedSession,
clearPlatformSession,
clearTmallManagedSession,
getJdKeywordPreview,
getJdLiveSession,
getJdQrLoginState,
getJdSessionManager,
getHistoryTasks,
getPlatformReadiness,
getPlatformSession,
getTask,
getTaskCandidates,
getTmallLiveSession,
getTmallQrLoginState,
getTmallSessionManager,
importJdManagedSession,
importTmallManagedSession,
preparePlatform,
createTaskEventsSource,
runJdSessionManagerHealthCheck,
runJdSessionManagerRecovery,
runTmallSessionManagerHealthCheck,
startJdQrLogin,
startTmallQrLogin,
updateJdSessionManagerConfig
} from "./lib/api";
function renderWithProviders(node: ReactNode, initialEntries?: string[]) {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false
}
}
});
return render(
<QueryClientProvider client={queryClient}>
{initialEntries ? (
<MemoryRouter initialEntries={initialEntries}>{node}</MemoryRouter>
) : (
<MemoryRouter>{node}</MemoryRouter>
)}
</QueryClientProvider>
);
}
describe("task composer and session console", () => {
beforeEach(() => {
const jdManagerState = {
status: "healthy",
enabled: true,
autoLoginMode: "command",
commandConfigured: true,
accountConfigured: true,
passwordConfigured: false,
accountLabel: "jd***86",
browserProfilePath: "D:\\ops\\jd-profile",
heartbeatQuery: "iPhone 15",
checkIntervalMs: 600000,
runnerTimeoutMs: 300000,
pendingManualAction: false,
note: "京东会话当前可用。",
publicNote: "京东会话由运维后台维护,当前可用。",
configuredAt: "2026-04-02T08:00:00.000Z",
lastCheckAt: "2026-04-02T10:00:00.000Z",
lastRecoveredAt: "2026-04-02T09:00:00.000Z",
session: {
configured: true,
importedAt: "2026-04-02T10:00:00.000Z",
hasCookie: true,
userAgent: "stub-user-agent",
searchApiTemplate: {
available: true
},
detailTemplate: {
available: true,
skuId: "100068388533"
},
reviewsTemplate: {
available: true,
skuId: "100068388533"
}
}
};
const tmallManagerState = {
status: "healthy",
enabled: true,
heartbeatItemId: "934454505228",
checkIntervalMs: 600000,
pendingManualAction: false,
note: "天猫会话当前可用。",
publicNote: "天猫会话由运维后台维护,当前可用。",
configuredAt: "2026-04-02T08:00:00.000Z",
lastCheckAt: "2026-04-02T10:00:00.000Z",
lastHealthyAt: "2026-04-02T10:00:00.000Z",
session: {
configured: true,
importedAt: "2026-04-02T10:00:00.000Z",
hasCookie: true,
userAgent: "stub-user-agent",
detailTemplate: {
available: true,
itemId: "934454505228"
},
reviewsTemplate: {
available: true,
itemId: "934454505228"
}
}
};
vi.mocked(getPlatformReadiness).mockResolvedValue({
platforms: [
{
platform: "tmall",
ready: true,
status: "ready",
searchRequirement: "recommended",
reason: "当前工作区存在可复用会话,创建任务时会再次校验。",
lastPreparedAt: "2026-04-02T12:00:00.000Z",
expiresAt: "2026-04-03T12:00:00.000Z"
},
{
platform: "jd",
ready: false,
status: "missing",
searchRequirement: "required",
reason: "需要先完成会话准备,否则系统会标记为 SearchBlocked。"
}
]
} as any);
vi.mocked(getHistoryTasks).mockResolvedValue({
tasks: [
{
taskId: "task-1",
query: "Nintendo Switch 2",
taskStatus: "Completed",
updatedAt: "2026-04-02T12:00:00.000Z",
hasReport: true,
defaultReportVersion: 2,
failedPlatforms: [],
blockedPlatforms: []
},
{
taskId: "task-2",
query: "DJI Pocket 3",
taskStatus: "AwaitingConfirmation",
updatedAt: "2026-04-02T11:30:00.000Z",
hasReport: false,
failedPlatforms: [],
blockedPlatforms: ["jd"]
}
]
} as any);
vi.mocked(getPlatformSession).mockResolvedValue({
session: {
platform: "tmall",
ready: true,
status: "ready",
searchRequirement: "recommended",
scope: "workspace",
ttlHours: 24,
lastPreparedAt: "2026-04-02T10:00:00.000Z",
expiresAt: "2026-04-03T10:00:00.000Z",
encryptedSnapshotAvailable: true,
cipherLabel: "mock-aes-gcm-v1"
}
} as any);
vi.mocked(getJdLiveSession).mockResolvedValue({
session: {
configured: true,
importedAt: "2026-04-02T10:00:00.000Z",
hasCookie: true,
userAgent: "stub-user-agent",
searchApiTemplate: {
available: false
},
detailTemplate: {
available: true,
skuId: "100068388533"
},
reviewsTemplate: {
available: true,
skuId: "100068388533"
}
}
} as any);
vi.mocked(getJdQrLoginState).mockResolvedValue({
qrLogin: {
platform: "jd",
status: "idle",
note: "尚未启动扫码登录。",
sessionImported: false
}
} as any);
vi.mocked(getJdKeywordPreview).mockResolvedValue({
preview: {
query: "小米手环10",
search: {
source: "html",
candidateCount: 2,
selected: {
skuId: "222222222222",
score: 168,
summary: "标题完整命中关键词;位于搜索结果前列;已解析出可回放 SKU",
matchedTokens: ["小米手环10"],
candidate: {
candidateId: "jd-222222222222",
platform: "jd",
title: "小米手环10 标准版 智能手环",
price: 269,
priceLabel: "¥269",
storeName: "小米京东自营旗舰店",
productUrl: "https://item.jd.com/222222222222.html",
imageUrl: "https://img14.360buyimg.com/n2/jfs/t1/example-10.jpg",
salesHint: "已售50万+",
specLabel: "标准版",
highlights: ["14天续航"]
}
},
alternatives: [
{
skuId: "111111111111",
score: 118,
summary: "标题/卖点命中 1 个关键词片段;已解析出可回放 SKU",
matchedTokens: ["小米"],
candidate: {
candidateId: "jd-111111111111",
platform: "jd",
title: "小米手环9 NFC版",
price: 249,
priceLabel: "¥249",
storeName: "小米京东自营旗舰店",
productUrl: "https://item.jd.com/111111111111.html",
imageUrl: "https://img14.360buyimg.com/n2/jfs/t1/example-9.jpg",
salesHint: "已售20万+",
specLabel: "NFC版",
highlights: ["健康监测"]
}
}
]
},
product: {
skuId: "222222222222",
source: "api",
detail: {
skuId: "222222222222",
title: "小米手环10 标准版 智能手环",
price: "269.00",
originalPrice: "299.00",
estimatedPrice: "269.00",
shopName: "小米京东自营旗舰店",
vendorId: null,
categoryPath: ["智能设备", "智能手环"],
stockState: "现货",
mainImage: "https://img14.360buyimg.com/n2/jfs/t1/example-10.jpg",
averageScore: "4.9"
},
pagination: {
requestedPage: 1,
requestedCommentCount: 12,
maxPages: 2,
pagesFetched: 2,
pageKey: "page"
},
reviews: {
skuId: "222222222222",
total: "10000+",
goodRate: "97%",
pictureCount: "800",
tags: [
{
tagId: "tag-1",
name: "续航很久",
count: "5300"
}
],
comments: [
{
id: "comment-1",
content: "表带舒适,睡眠监测比较准。",
score: "5",
creationTime: "2026-04-03 09:00:00",
userLevelName: "PLUS会员"
}
]
}
}
}
} as any);
vi.mocked(getJdSessionManager).mockResolvedValue({
manager: jdManagerState
} as any);
vi.mocked(getTmallSessionManager).mockResolvedValue({
manager: tmallManagerState
} as any);
vi.mocked(getTmallLiveSession).mockResolvedValue({
session: tmallManagerState.session
} as any);
vi.mocked(getTmallQrLoginState).mockResolvedValue({
qrLogin: {
platform: "tmall",
status: "idle",
note: "尚未启动扫码登录。",
sessionImported: false
}
} as any);
vi.mocked(clearPlatformSession).mockResolvedValue(undefined);
vi.mocked(importJdManagedSession).mockResolvedValue({
manager: {
...jdManagerState,
note: "京东会话已更新。",
lastRecoveredAt: "2026-04-02T10:05:00.000Z",
session: {
...jdManagerState.session,
importedAt: "2026-04-02T10:05:00.000Z",
}
}
} as any);
vi.mocked(importTmallManagedSession).mockResolvedValue({
manager: {
...tmallManagerState,
note: "天猫会话已更新。",
lastHealthyAt: "2026-04-02T10:05:00.000Z",
session: {
...tmallManagerState.session,
importedAt: "2026-04-02T10:05:00.000Z"
}
}
} as any);
vi.mocked(clearJdManagedSession).mockResolvedValue({
manager: {
status: "idle",
enabled: true,
autoLoginMode: "command",
commandConfigured: true,
accountConfigured: true,
passwordConfigured: false,
accountLabel: "jd***86",
heartbeatQuery: "iPhone 15",
checkIntervalMs: 600000,
runnerTimeoutMs: 300000,
pendingManualAction: false,
note: "京东会话已清理。",
publicNote: "京东会话由运维后台维护,当前尚未就绪。",
session: {
configured: false,
hasCookie: false,
searchApiTemplate: {
available: false
},
detailTemplate: {
available: false
},
reviewsTemplate: {
available: false
}
}
}
} as any);
vi.mocked(clearTmallManagedSession).mockResolvedValue({
manager: {
status: "idle",
enabled: true,
heartbeatItemId: "934454505228",
checkIntervalMs: 600000,
pendingManualAction: false,
note: "天猫会话已清理。",
publicNote: "天猫会话由运维后台维护,当前尚未就绪。",
session: {
configured: false,
hasCookie: false,
detailTemplate: {
available: false
},
reviewsTemplate: {
available: false
}
}
}
} as any);
vi.mocked(updateJdSessionManagerConfig).mockResolvedValue({
manager: jdManagerState
} as any);
vi.mocked(runJdSessionManagerHealthCheck).mockResolvedValue({
state: jdManagerState,
recovered: false
} as any);
vi.mocked(runJdSessionManagerRecovery).mockResolvedValue({
state: jdManagerState,
recovered: true
} as any);
vi.mocked(startJdQrLogin).mockResolvedValue({
qrLogin: {
platform: "jd",
status: "waiting_for_scan",
note: "二维码已生成,请扫码。",
targetId: "100068388533",
qrImageDataUrl: "data:image/png;base64,stub",
sessionImported: false
}
} as any);
vi.mocked(cancelJdQrLogin).mockResolvedValue({
qrLogin: {
platform: "jd",
status: "cancelled",
note: "扫码已取消。",
sessionImported: false
}
} as any);
vi.mocked(runTmallSessionManagerHealthCheck).mockResolvedValue({
state: tmallManagerState,
recovered: false
} as any);
vi.mocked(startTmallQrLogin).mockResolvedValue({
qrLogin: {
platform: "tmall",
status: "waiting_for_scan",
note: "二维码已生成,请扫码。",
targetId: "934454505228",
qrImageDataUrl: "data:image/png;base64,stub",
sessionImported: false
}
} as any);
vi.mocked(cancelTmallQrLogin).mockResolvedValue({
qrLogin: {
platform: "tmall",
status: "cancelled",
note: "扫码已取消。",
sessionImported: false
}
} as any);
vi.mocked(preparePlatform).mockResolvedValue({
platform: "jd",
session_ready: true,
status: "ready",
last_prepared_at: "2026-04-02T10:00:00.000Z",
expires_at: "2026-04-03T10:00:00.000Z",
encrypted_snapshot_available: true
} as any);
});
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
it("shows readiness details and recent task shortcuts on the new task page", async () => {
renderWithProviders(<NewTaskPage />);
expect(await screen.findByText("最近任务捷径")).toBeInTheDocument();
expect(await screen.findByText("Nintendo Switch 2")).toBeInTheDocument();
expect(await screen.findByText("DJI Pocket 3")).toBeInTheDocument();
expect(await screen.findByText("搜索要求:建议预热")).toBeInTheDocument();
expect(
await screen.findByText(/当前工作区已有可复用会话,有效至/)
).toBeInTheDocument();
});
it("runs the jd keyword-only preview loop from the new task page", async () => {
const user = userEvent.setup();
renderWithProviders(<NewTaskPage />);
await user.clear(await screen.findByLabelText("商品关键词 / 描述"));
await user.type(screen.getByLabelText("商品关键词 / 描述"), "小米手环10");
await user.click(screen.getByRole("button", { name: "只用关键词抓京东" }));
await waitFor(() => {
expect(getJdKeywordPreview).toHaveBeenCalledWith("小米手环10", {
commentCount: 12,
maxPages: 2
});
});
expect(await screen.findByText("小米手环10 标准版 智能手环")).toBeInTheDocument();
expect(await screen.findByText(/SKU 222222222222/)).toBeInTheDocument();
expect(await screen.findByText("好评率 97%")).toBeInTheDocument();
expect(await screen.findByText("表带舒适,睡眠监测比较准。")).toBeInTheDocument();
});
it("redirects the tmall prepare route to the unified ops page and clears the managed session", async () => {
const user = userEvent.setup();
renderWithProviders(<App />, ["/ops/platforms/tmall/prepare?from=/tasks/new"]);
await user.click(screen.getByRole("button", { name: "清理当前会话" }));
await waitFor(() => {
expect(clearTmallManagedSession).toHaveBeenCalled();
});
});
it("redirects the legacy jd prepare route to the ops session manager page", async () => {
renderWithProviders(<App />, ["/sessions/jd/prepare?from=/tasks/new"]);
expect(await screen.findByText("京东运维会话管理")).toBeInTheDocument();
expect(screen.getByText(/返回业务页面:\/tasks\/new/)).toBeInTheDocument();
});
it("updates the confirm page when a task snapshot restores JD candidates", async () => {
let snapshotHandler: ((event: MessageEvent<string>) => void) | undefined;
vi.mocked(createTaskEventsSource).mockReturnValue({
addEventListener: vi.fn((_type: string, handler: EventListenerOrEventListenerObject) => {
snapshotHandler = handler as (event: MessageEvent<string>) => void;
}),
close: vi.fn(),
removeEventListener: vi.fn()
} as unknown as EventSource);
vi.mocked(getTask).mockResolvedValue({
task: {
taskId: "task-confirm-live",
query: "iPhone 15",
createdAt: "2026-04-07T09:00:00.000Z",
updatedAt: "2026-04-07T09:00:00.000Z",
perLinkLimit: 100,
taskTotalLimit: 500,
taskStatus: "AwaitingConfirmation",
taskStage: "confirmation",
platformRuns: [
{
platform: "tmall",
searchRequirement: "recommended",
status: "AwaitingSelection",
candidateCount: 1,
selectedCandidateIds: [],
lastUpdatedAt: "2026-04-07T09:00:00.000Z"
},
{
platform: "jd",
searchRequirement: "required",
status: "SearchBlocked",
candidateCount: 0,
selectedCandidateIds: [],
reason: "waiting for ops recovery",
lastUpdatedAt: "2026-04-07T09:00:00.000Z"
}
],
platformCandidates: {
tmall: [],
jd: []
},
events: [],
reportVersions: []
}
} as any);
vi.mocked(getTaskCandidates)
.mockResolvedValueOnce({
candidates: {
tmall: [],
jd: []
}
} as any)
.mockResolvedValue({
candidates: {
tmall: [],
jd: [
{
candidateId: "jd-100068388533",
platform: "jd",
title: "Apple iPhone 15",
price: 3898,
priceLabel: "CNY 3898",
storeName: "JD Self Operated",
productUrl: "https://item.jd.com/100068388533.html",
imageUrl: "https://img14.360buyimg.com/example.jpg",
salesHint: "sold 500+",
specLabel: "128GB",
highlights: ["A16"]
}
]
}
} as any);
renderWithProviders(<App />, ["/tasks/task-confirm-live/confirm"]);
expect(await screen.findByText("waiting for ops recovery")).toBeInTheDocument();
await act(async () => {
snapshotHandler?.(
new MessageEvent("task.snapshot", {
data: JSON.stringify({
task: {
taskId: "task-confirm-live",
query: "iPhone 15",
createdAt: "2026-04-07T09:00:00.000Z",
updatedAt: "2026-04-07T09:01:00.000Z",
perLinkLimit: 100,
taskTotalLimit: 500,
taskStatus: "AwaitingConfirmation",
taskStage: "confirmation",
platformRuns: [
{
platform: "tmall",
searchRequirement: "recommended",
status: "AwaitingSelection",
candidateCount: 1,
selectedCandidateIds: [],
lastUpdatedAt: "2026-04-07T09:00:00.000Z"
},
{
platform: "jd",
searchRequirement: "required",
status: "AwaitingSelection",
candidateCount: 1,
selectedCandidateIds: [],
lastUpdatedAt: "2026-04-07T09:01:00.000Z"
}
],
platformCandidates: {
tmall: [],
jd: []
},
events: [],
reportVersions: []
}
})
})
);
});
expect(await screen.findByText("Apple iPhone 15")).toBeInTheDocument();
await waitFor(() => {
expect(getTaskCandidates).toHaveBeenCalledTimes(2);
});
});
it("imports jd managed session payload from the ops page", async () => {
const user = userEvent.setup();
renderWithProviders(<App />, ["/ops/session-manager?platform=jd&from=/tasks/new"]);
await user.clear(await screen.findByLabelText("Cookie Header"));
await user.type(screen.getByLabelText("Cookie Header"), "thor=masked; pin=masked;");
await user.type(
screen.getByLabelText("Detail Template URL"),
"https://api.m.jd.com/?functionId=pc_detailpage_wareBusiness"
);
await user.type(
screen.getByLabelText("Reviews Template URL"),
"https://api.m.jd.com/?functionId=getLegoWareDetailComment"
);
await user.click(screen.getByRole("button", { name: "注入京东会话" }));
await waitFor(() => {
expect(importJdManagedSession).toHaveBeenCalledWith(
expect.objectContaining({
cookieHeader: "thor=masked; pin=masked;",
detailTemplateUrl: "https://api.m.jd.com/?functionId=pc_detailpage_wareBusiness",
reviewsTemplateUrl: "https://api.m.jd.com/?functionId=getLegoWareDetailComment"
})
);
});
});
it("imports tmall managed session payload from the unified ops page", async () => {
const user = userEvent.setup();
renderWithProviders(<App />, ["/ops/session-manager?platform=tmall&from=/tasks/new"]);
await user.clear(await screen.findByLabelText("Cookie Header"));
await user.type(
screen.getByLabelText("Cookie Header"),
"_m_h5_tk=masked_token_123; cookie2=masked;"
);
await user.type(
screen.getByLabelText("Detail Template URL"),
"https://detail.tmall.com/item.htm?id=934454505228"
);
await user.type(
screen.getByLabelText("Reviews Template URL"),
"https://h5api.m.tmall.com/h5/mtop.taobao.rate.detaillist.get/6.0/?data=%7B%22auctionNumId%22%3A%22934454505228%22%7D"
);
await user.click(screen.getByRole("button", { name: "注入天猫会话" }));
await waitFor(() => {
expect(importTmallManagedSession).toHaveBeenCalledWith(
expect.objectContaining({
cookieHeader: "_m_h5_tk=masked_token_123; cookie2=masked;",
detailTemplateUrl: "https://detail.tmall.com/item.htm?id=934454505228",
reviewsTemplateUrl:
"https://h5api.m.tmall.com/h5/mtop.taobao.rate.detaillist.get/6.0/?data=%7B%22auctionNumId%22%3A%22934454505228%22%7D"
})
);
});
});
});