import { afterEach, describe, expect, it, vi } from "vitest"; import { cleanup, fireEvent, render, screen, waitFor, } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { Refine } from "@refinedev/core"; import { MemoryRouter } from "react-router"; import { CreatorAccountsPage } from "./CreatorAccountsPage"; import { CreatorCompetitorsPage } from "./CreatorCompetitorsPage"; import { CreatorSettingsPage } from "./CreatorSettingsPage"; import { CreatorWorkbenchPage } from "./CreatorWorkbenchPage"; const profile = { id: "account-a", name: "主账号", platform: "douyin", platform_account_key: "sec-a", authorization_status: "authorized", runtime_status: "running", business_status: "normal", login_status: "logged_in", real_name_status: "recorded", big_account: false, cooldown_seconds: 86400, updated_at: "2026-01-01T00:00:00Z", }; const settings = { lookback_days: 30, new_work_interval_seconds: 1800, metric_initial_interval_seconds: 3600, metric_multiplier: 2, metric_max_interval_seconds: 86400, metric_age_seconds: 2592000, ai_provider: "", ai_model: "", ai_configured: false, transcription_provider: "", transcription_model: "", transcription_configured: false, }; function provider(overrides = {}) { return { getList: vi.fn(({ resource }) => Promise.resolve({ data: resource === "creator-accounts" ? [profile] : resource === "creator-competitors" ? [] : resource === "creator-works" ? [] : [], total: resource === "creator-accounts" ? 1 : 0, }), ), creatorGet: vi.fn((path) => Promise.resolve(path === "/creator/settings" ? settings : []), ), creatorUpdate: vi.fn().mockResolvedValue(settings), creatorAction: vi.fn().mockResolvedValue({ ...profile, big_account: true }), creatorCreate: vi.fn().mockResolvedValue({ id: "created", ...profile }), create: vi.fn().mockResolvedValue({ data: { id: "competitor-a", account_id: "account-a", platform: "douyin", platform_account_key: "sec-b", homepage_url: "https://www.douyin.com/user/sec-b", enabled: true, }, }), getOne: vi.fn(), update: vi.fn(), delete: vi.fn(), getMany: vi.fn(), getManyReference: vi.fn(), updateMany: vi.fn(), deleteMany: vi.fn(), ...overrides, }; } function renderPage(element, dataProvider) { return render( {element} , ); } afterEach(() => { cleanup(); vi.restoreAllMocks(); }); describe("creator pages", () => { it("loads and saves collection settings", async () => { const dataProvider = provider(); renderPage(, dataProvider); expect(await screen.findByText("采集设置")).toBeTruthy(); fireEvent.change(screen.getByLabelText("竞品回溯天数"), { target: { value: "14" }, }); fireEvent.click(screen.getByRole("button", { name: "保存设置" })); await waitFor(() => expect(dataProvider.creatorUpdate).toHaveBeenCalledWith( "/creator/settings", expect.objectContaining({ lookback_days: 14 }), ), ); expect(dataProvider.creatorUpdate).not.toHaveBeenCalledWith( "/creator/settings", expect.objectContaining({ updated_at: expect.anything() }), ); }); it("verifies account identity and toggles big-account mode", async () => { const dataProvider = provider({ creatorAction: vi.fn((path) => Promise.resolve( path.endsWith("/verify") ? { ...profile, login_status: "logged_in" } : { ...profile, big_account: true }, ), ), }); renderPage(, dataProvider); expect( await screen.findByRole("button", { name: "开启大号模式" }), ).toBeTruthy(); fireEvent.click(screen.getByRole("button", { name: "核验浏览器身份" })); await waitFor(() => expect(dataProvider.creatorAction).toHaveBeenCalledWith( "/creator/accounts/account-a/verify", ), ); fireEvent.click(screen.getByRole("button", { name: "开启大号模式" })); await waitFor(() => expect(dataProvider.creatorAction).toHaveBeenCalledWith( "/creator/accounts/account-a/big-account", { enabled: true }, ), ); expect(await screen.findByText("已开启大号模式。")).toBeTruthy(); }); it("shows retryable account and strategy loading failures", async () => { const dataProvider = provider({ getList: vi.fn().mockRejectedValue(new Error("accounts offline")), creatorGet: vi.fn().mockRejectedValue(new Error("creator offline")), }); renderPage(, dataProvider); expect(await screen.findByText("accounts offline")).toBeTruthy(); fireEvent.click(screen.getByRole("button", { name: "重试" })); await waitFor(() => expect(dataProvider.getList).toHaveBeenCalledTimes(2)); }); it("shows settings save conflicts", async () => { const dataProvider = provider({ creatorUpdate: vi.fn().mockRejectedValue({ status: 409, body: { reason: "version changed" }, }), }); renderPage(, dataProvider); expect(await screen.findByText("采集设置")).toBeTruthy(); fireEvent.click(screen.getByRole("button", { name: "保存设置" })); expect(await screen.findByText("冲突(409):设置保存失败")).toBeTruthy(); }); it("saves settings successfully", async () => { const dataProvider = provider({ creatorUpdate: vi.fn().mockResolvedValue({}), }); renderPage(, dataProvider); expect(await screen.findByText("采集设置")).toBeTruthy(); fireEvent.click(screen.getByLabelText("已完成 AI 配置审批")); fireEvent.click(screen.getByRole("button", { name: "保存设置" })); expect(await screen.findByText("采集与 AI 配置已保存。")).toBeTruthy(); }); it("does not render stale data using another tab's shape", async () => { const dataProvider = provider({ getList: vi.fn(({ resource }) => Promise.resolve({ data: resource === "creator-comments" ? [ { id: "comment-a", author_name: "评论用户", author_uid: "peer-a", content: "你好", platform: "douyin", work_id: "work-a", }, ] : resource === "creator-rules" ? [ { id: "rule-a", name: "规则", include_keywords: ["你好"], enabled: true, topic: "主题", }, ] : resource === "creator-leads" ? [ { id: "lead-a", comment: { id: "comment-a", author_name: "线索用户", author_uid: "peer-a", content: "线索", }, rule_ids: ["rule-a"], }, ] : [], }), ), }); renderPage(, dataProvider); expect(await screen.findByText("评论用户")).toBeTruthy(); fireEvent.click(screen.getByRole("tab", { name: "线索" })); expect(await screen.findByText("线索用户", { exact: false })).toBeTruthy(); }); it("requires explicit stable competitor identity and shows empty works state", async () => { const dataProvider = provider(); renderPage(, dataProvider); expect( await screen.findByRole("heading", { name: "加入监测" }), ).toBeTruthy(); fireEvent.click(screen.getByRole("button", { name: "加入监测" })); expect(dataProvider.create).not.toHaveBeenCalled(); expect(await screen.findByText("暂无符合条件的作品。")).toBeTruthy(); }); it("requires preview confirmation before importing a competitor", async () => { const dataProvider = provider(); renderPage(, dataProvider); await waitFor(() => expect(dataProvider.getList).toHaveBeenCalledWith( expect.objectContaining({ resource: "creator-accounts" }), ), ); fireEvent.change(screen.getByLabelText("主页 URL", { exact: false }), { target: { value: "https://www.douyin.com/user/sec-b" }, }); fireEvent.click(screen.getByRole("button", { name: "解析链接预览" })); expect(await screen.findByText("导入预览")).toBeTruthy(); await waitFor(() => expect(dataProvider.create).toHaveBeenCalledWith( expect.objectContaining({ resource: "creator-competitor-preview", variables: expect.objectContaining({ account_id: "account-a", platform: "douyin", platform_account_key: "sec-b", }), }), ), ); fireEvent.click(screen.getByRole("button", { name: "确认预览内容" })); fireEvent.click(screen.getByRole("button", { name: "加入监测" })); await waitFor(() => expect(dataProvider.create).toHaveBeenCalledTimes(2)); expect(dataProvider.create).toHaveBeenLastCalledWith( expect.objectContaining({ resource: "creator-competitors", variables: expect.objectContaining({ platform: "douyin", platform_account_key: "sec-b", account_id: "account-a", }), }), ); }); it("parses and confirms a Xiaohongshu profile URL", async () => { const dataProvider = provider(); renderPage(, dataProvider); fireEvent.click(screen.getAllByRole("combobox")[0]); fireEvent.click(screen.getByRole("option", { name: "小红书" })); fireEvent.change(screen.getByLabelText("主页 URL", { exact: false }), { target: { value: "https://www.xiaohongshu.com/user/profile/xhs-b", }, }); fireEvent.click(screen.getByRole("button", { name: "解析链接预览" })); await waitFor(() => expect( screen.getAllByText("xhs-b", { exact: false }).length, ).toBeGreaterThan(0), ); fireEvent.click(screen.getByRole("button", { name: "确认预览内容" })); fireEvent.click(screen.getByRole("button", { name: "加入监测" })); await waitFor(() => expect(dataProvider.create).toHaveBeenCalled()); expect(dataProvider.create).toHaveBeenCalledWith( expect.objectContaining({ resource: "creator-competitors", variables: expect.objectContaining({ platform: "xiaohongshu", platform_account_key: "xhs-b", }), }), ); }); it("keeps account password out of the returned profile and exposes big-account action", async () => { const dataProvider = provider(); renderPage(, dataProvider); expect(await screen.findByRole("heading", { name: "主账号" })).toBeTruthy(); fireEvent.click(screen.getByRole("button", { name: "开启大号模式" })); await waitFor(() => expect(dataProvider.creatorAction).toHaveBeenCalledWith( "/creator/accounts/account-a/big-account", { enabled: true }, ), ); expect(screen.getByLabelText("更新密码")).toBeTruthy(); }); it("allows switching private-message accounts across platforms", async () => { const xhsProfile = { ...profile, id: "account-xhs", name: "小红书账号", platform: "xiaohongshu", platform_account_key: "xhs-a", }; const conversation = { id: "conversation-a", account_id: profile.id, platform: profile.platform, peer_uid: "peer-a", peer_name: "客户", }; const dataProvider = provider({ getList: vi.fn(({ resource }) => Promise.resolve({ data: resource === "creator-accounts" ? [profile, xhsProfile] : resource === "creator-conversations" ? [conversation] : [], total: 1, }), ), creatorGet: vi.fn(() => Promise.resolve([])), }); renderPage(, dataProvider); fireEvent.click(await screen.findByRole("tab", { name: "私信" })); expect(await screen.findByText("客户")).toBeTruthy(); fireEvent.click(screen.getByRole("combobox", { name: "发送账号" })); expect(screen.getByRole("option", { name: "小红书账号" })).toBeTruthy(); }); it("shows the private-message business update connection state", async () => { const creatorSubscribe = vi.fn(() => new Promise(() => {})); const conversation = { id: "conversation-a", account_id: profile.id, platform: profile.platform, peer_uid: "peer-a", peer_name: "客户", }; const dataProvider = provider({ creatorSubscribe, getList: vi.fn(({ resource }) => Promise.resolve({ data: resource === "creator-accounts" ? [profile] : resource === "creator-conversations" ? [conversation] : [], total: resource === "creator-conversations" ? 1 : 0, }), ), }); renderPage(, dataProvider); fireEvent.click(await screen.findByRole("tab", { name: "私信" })); expect(await screen.findByText("后台更新连接中")).toBeTruthy(); expect(creatorSubscribe).toHaveBeenCalledWith( "/creator/updates", expect.any(Function), expect.any(AbortSignal), expect.any(Function), ); }); it("subscribes to realtime updates on non-DM operational tabs", async () => { const creatorSubscribe = vi.fn(() => new Promise(() => {})); const dataProvider = provider({ creatorSubscribe }); renderPage(, dataProvider); await waitFor(() => expect(creatorSubscribe).toHaveBeenCalledTimes(1)); fireEvent.click(await screen.findByRole("tab", { name: "事件监听" })); await waitFor(() => expect(creatorSubscribe).toHaveBeenCalledTimes(2)); fireEvent.click(screen.getByRole("tab", { name: "操作记录" })); await waitFor(() => expect(creatorSubscribe).toHaveBeenCalledTimes(3)); }); it("isolates private messages by account and confirms one durable send", async () => { const conversation = { id: "conversation-a", account_id: profile.id, platform: profile.platform, peer_uid: "peer-a", peer_name: "客户", }; const dataProvider = provider({ getList: vi.fn(({ resource }) => Promise.resolve({ data: resource === "creator-accounts" ? [profile] : resource === "creator-conversations" ? [conversation] : resource === "creator-rules" ? [] : [], total: resource === "creator-conversations" || resource === "creator-accounts" ? 1 : 0, }), ), creatorGet: vi.fn((path) => Promise.resolve( path.includes("/messages") ? [] : path === "/creator/settings" ? settings : [], ), ), creatorAction: vi .fn() .mockResolvedValue({ state: "succeeded", id: "operation-a" }), }); renderPage(, dataProvider); fireEvent.click(await screen.findByRole("tab", { name: "私信" })); expect(await screen.findByText("客户")).toBeTruthy(); fireEvent.click(screen.getByRole("combobox", { name: "发送账号" })); fireEvent.click(await screen.findByRole("option", { name: "主账号" })); fireEvent.change(screen.getByRole("textbox", { name: "人工私信文案" }), { target: { value: "你好" }, }); fireEvent.click(screen.getByRole("button", { name: "确认并发送" })); fireEvent.click( await screen.findByRole("button", { name: "确认并记录操作" }), ); await waitFor(() => expect(dataProvider.creatorCreate).toHaveBeenCalledWith( "/creator/operations", expect.objectContaining({ action: "dm", account_id: profile.id, target_uid: conversation.peer_uid, text: "你好", idempotency_key: expect.any(String), }), ), ); }); });