Files
creator-hub/web/src/CreatorPages.test.jsx
T

479 lines
17 KiB
React

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(
<QueryClientProvider client={new QueryClient()}>
<Refine dataProvider={dataProvider}>
<MemoryRouter>{element}</MemoryRouter>
</Refine>
</QueryClientProvider>,
);
}
afterEach(() => {
cleanup();
vi.restoreAllMocks();
});
describe("creator pages", () => {
it("loads and saves collection settings", async () => {
const dataProvider = provider();
renderPage(<CreatorSettingsPage />, 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(<CreatorAccountsPage />, 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(<CreatorAccountsPage />, 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(<CreatorSettingsPage />, 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(<CreatorSettingsPage />, 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(<CreatorWorkbenchPage />, 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(<CreatorCompetitorsPage />, 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(<CreatorCompetitorsPage />, 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(<CreatorCompetitorsPage />, 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(<CreatorAccountsPage />, 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(<CreatorWorkbenchPage />, 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(<CreatorWorkbenchPage />, 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(<CreatorWorkbenchPage />, 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(<CreatorWorkbenchPage />, 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),
}),
),
);
});
});