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( {initialEntries ? ( {node} ) : ( {node} )} ); } 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(); 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(); 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(, ["/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(, ["/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) => void) | undefined; vi.mocked(createTaskEventsSource).mockReturnValue({ addEventListener: vi.fn((_type: string, handler: EventListenerOrEventListenerObject) => { snapshotHandler = handler as (event: MessageEvent) => 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(, ["/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(, ["/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(, ["/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" }) ); }); }); });