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, Route, Routes } from "react-router"; import { AccountEditPage } from "./AccountEditPage"; import { AccountList } from "./AccountsPage"; 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_kind: "owned", 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} , ); } function renderAccountPage(dataProvider) { return render( } /> , ); } 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 }, ), ), }); renderAccountPage(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("only offers owned accounts as execution small accounts", async () => { const ownedProfile = { ...profile, big_account: true }; const dataProvider = provider({ getList: vi.fn(({ resource }) => Promise.resolve({ data: resource === "creator-accounts" ? [ ownedProfile, { ...profile, id: "authorized-a", name: "监测账号", authorization_kind: "authorized", }, ] : [], total: resource === "creator-accounts" ? 2 : 0, }), ), }); renderAccountPage(dataProvider); const executionAccount = await screen.findByRole("combobox", { name: "执行小号", }); fireEvent.click(executionAccount); expect(screen.queryByRole("option", { name: "监测账号" })).toBeNull(); expect( screen.getByText("这里只显示自有账号;监测账号不能作为小号。"), ).toBeTruthy(); }); it("displays the CreatorHub login screen returned by the gateway", async () => { const dataProvider = provider({ creatorAction: vi.fn((path) => path.endsWith("/login-qr") ? Promise.resolve({ status: "manual_login", content_type: "image/png", image_base64: "cG5n", qr_detected: true, expires_at: "2026-01-01T00:02:00Z", }) : Promise.resolve({ ...profile, login_status: "logged_in" }), ), }); renderAccountPage(dataProvider); fireEvent.click( await screen.findByRole("button", { name: "显示登录二维码" }), ); await waitFor(() => expect(dataProvider.creatorAction).toHaveBeenCalledWith( "/creator/accounts/account-a/login-qr", ), ); expect( screen .getByRole("img", { name: "抖音登录二维码或人工验证画面" }) .getAttribute("src"), ).toBe("data:image/png;base64,cG5n"); expect( await screen.findByText("请使用抖音 App 扫描下方二维码。"), ).toBeTruthy(); }); it("shows the environment reason when login QR startup is unavailable", async () => { const dataProvider = provider({ creatorAction: vi.fn().mockRejectedValue({ status: 503, message: "creator capability is unavailable", body: { error: "creator capability is unavailable", reason: "creator capability is unavailable: start account environment: gateway unavailable", }, }), }); renderAccountPage(dataProvider); fireEvent.click( await screen.findByRole("button", { name: "显示登录二维码" }), ); expect( await screen.findByText( "环境不可用(503):creator capability is unavailable: start account environment: gateway unavailable", ), ).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")), }); renderAccountPage(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("opens share-link monitoring and shows the works table", async () => { const dataProvider = provider(); renderPage(, dataProvider); expect(await screen.findByRole("button", { name: "添加监听" })).toBeTruthy(); fireEvent.click(screen.getByRole("button", { name: "添加监听" })); expect(await screen.findByLabelText("分享内容或链接", { exact: false })).toBeTruthy(); expect(await screen.findByText("暂无符合条件的作品。")).toBeTruthy(); expect(screen.queryByRole("heading", { name: "已监测账号" })).toBeNull(); expect(dataProvider.create).not.toHaveBeenCalled(); }); it("merges owned and monitored accounts and filters by source", async () => { const owned = { id: "account-a", name: "自有账号", platform: "douyin", platform_account_key: "owned-a", tags: ["主账号"], authorization_status: "authorized", runtime_status: "active", }; const competitor = { id: "competitor-a", nickname: "监测作者", platform: "douyin", platform_account_key: "monitor-a", tags: ["重点"], sync_status: "succeeded", enabled: true, }; const dataProvider = provider({ getList: vi.fn(({ resource }) => Promise.resolve({ data: resource === "accounts" ? [owned] : resource === "creator-competitors" ? [competitor] : resource === "creator-accounts" ? [profile] : [], total: resource === "accounts" || resource === "creator-competitors" ? 1 : 0, }), ), }); renderPage(, dataProvider); expect(await screen.findByRole("heading", { name: "账号管理" })).toBeTruthy(); expect(await screen.findByText("自有账号")).toBeTruthy(); expect(screen.getByText("监测作者")).toBeTruthy(); fireEvent.click(screen.getByRole("combobox", { name: "账号类型" })); fireEvent.click(await screen.findByRole("option", { name: "监测" })); expect(screen.queryByText("自有账号")).toBeNull(); expect(screen.getByText("监测作者")).toBeTruthy(); }); it("saves tags for a monitored account", async () => { const competitor = { id: "competitor-a", nickname: "监测作者", platform: "douyin", platform_account_key: "monitor-a", tags: [], enabled: true, }; const dataProvider = provider({ getList: vi.fn(({ resource }) => Promise.resolve({ data: resource === "creator-competitors" ? [competitor] : [], total: resource === "creator-competitors" ? 1 : 0, }), ), creatorUpdate: vi.fn().mockResolvedValue({ id: "competitor-a", tags: ["重点监测"], }), }); renderPage(, dataProvider); fireEvent.click(await screen.findByRole("button", { name: "编辑标签" })); const input = screen.getByRole("textbox", { name: "标签" }); fireEvent.change(input, { target: { value: "重点监测" } }); fireEvent.keyDown(input, { key: "Enter", code: "Enter" }); fireEvent.click(screen.getByRole("button", { name: "保存标签" })); await waitFor(() => expect(dataProvider.creatorUpdate).toHaveBeenCalledWith( "/creator/competitors/competitor-a", { tags: ["重点监测"] }, ), ); }); it("explains when a monitored account has no owned collection account", async () => { const competitor = { id: "competitor-a", nickname: "监测作者", platform: "douyin", platform_account_key: "monitor-a", enabled: true, }; const dataProvider = provider({ getList: vi.fn(({ resource }) => Promise.resolve({ data: resource === "creator-competitors" ? [competitor] : resource === "accounts" ? [] : resource === "creator-accounts" ? [] : [], total: resource === "creator-competitors" ? 1 : 0, }), ), }); renderPage(, dataProvider); fireEvent.click(await screen.findByRole("button", { name: "同步" })); const warning = await screen.findByRole("alert"); expect(warning.textContent).toContain("没有可用于同步抖音监测账号的已登录自有账号"); }); it("extracts a share URL and requires author confirmation before queuing", async () => { const dataProvider = provider({ create: vi.fn(({ resource, variables }) => Promise.resolve({ data: resource === "creator-competitor-preview" ? { platform: variables.platform, platform_account_key: "sec-b", nickname: "竞品作者", homepage_url: "https://www.douyin.com/user/sec-b", } : { 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, }, }), ), }); renderPage(, dataProvider); fireEvent.click(await screen.findByRole("button", { name: "添加监听" })); fireEvent.change(screen.getByLabelText("分享内容或链接", { exact: false }), { target: { value: "复制打开抖音 https://v.douyin.com/share-a/。" }, }); const tagInput = screen.getByLabelText("TAGS"); fireEvent.change(tagInput, { target: { value: "重点监测" } }); fireEvent.keyDown(tagInput, { key: "Enter", code: "Enter" }); expect(await screen.findByText(/已提取链接:https:\/\/v\.douyin\.com\/share-a/)).toBeTruthy(); fireEvent.click(screen.getByRole("button", { name: "解析并预览作者" })); expect(await screen.findByText("作者预览")).toBeTruthy(); expect(dataProvider.create).toHaveBeenCalledWith( expect.objectContaining({ resource: "creator-competitor-preview", variables: { account_id: "account-a", platform: "douyin", share_url: "https://v.douyin.com/share-a/", }, }), ); 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: { account_id: "account-a", platform: "douyin", share_url: "https://v.douyin.com/share-a/", tags: ["重点监测"], }, }), ); }); it("detects Xiaohongshu from a pasted share link", async () => { const dataProvider = provider({ getList: vi.fn(({ resource }) => Promise.resolve({ data: resource === "creator-accounts" ? [{ ...profile, platform: "xiaohongshu" }] : [], total: resource === "creator-accounts" ? 1 : 0, }), ), create: vi.fn(({ resource, variables }) => Promise.resolve({ data: resource === "creator-competitor-preview" ? { platform: variables.platform, platform_account_key: "xhs-b", nickname: "小红书作者", homepage_url: "https://www.xiaohongshu.com/user/profile/xhs-b", } : { id: "competitor-xhs", ...variables, enabled: true }, }), ), }); renderPage(, dataProvider); fireEvent.click(await screen.findByRole("button", { name: "添加监听" })); fireEvent.change(screen.getByLabelText("分享内容或链接", { exact: false }), { target: { value: "小红书分享 https://www.xiaohongshu.com/explore/note-a?xsec_token=t", }, }); expect( await screen.findByText(/已提取链接:https:\/\/www\.xiaohongshu\.com\/explore\/note-a/), ).toBeTruthy(); fireEvent.click(screen.getByRole("button", { name: "解析并预览作者" })); await waitFor(() => expect(dataProvider.create).toHaveBeenCalledWith( expect.objectContaining({ resource: "creator-competitor-preview", variables: expect.objectContaining({ platform: "xiaohongshu", share_url: "https://www.xiaohongshu.com/explore/note-a?xsec_token=t", }), }), ), ); }); it("keeps account password out of the returned profile and exposes big-account action", async () => { const dataProvider = provider(); renderAccountPage(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), }), ), ); }); });