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

732 lines
25 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, 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(
<QueryClientProvider client={new QueryClient()}>
<Refine dataProvider={dataProvider}>
<MemoryRouter>{element}</MemoryRouter>
</Refine>
</QueryClientProvider>,
);
}
function renderAccountPage(dataProvider) {
return render(
<QueryClientProvider client={new QueryClient()}>
<Refine dataProvider={dataProvider}>
<MemoryRouter initialEntries={["/accounts/account-a/edit"]}>
<Routes>
<Route path="/accounts/:id/edit" element={<AccountEditPage />} />
</Routes>
</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 },
),
),
});
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(<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("opens share-link monitoring and shows the works table", async () => {
const dataProvider = provider();
renderPage(<CreatorCompetitorsPage />, 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(<AccountList />, 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(<AccountList />, 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(<AccountList />, 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(<CreatorCompetitorsPage />, 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(<CreatorCompetitorsPage />, 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(<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),
}),
),
);
});
});