Files
Genarrative/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.test.tsx
T

329 lines
9.7 KiB
TypeScript

/* @vitest-environment jsdom */
import {
fireEvent,
render,
screen,
waitFor,
within,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { beforeEach, expect, test, vi } from 'vitest';
import {
getAdminCreationEntryConfig,
upsertAdminCreationEntryBanners,
upsertAdminCreationEntryConfig,
upsertAdminPublicWorkInteractions,
} from '../api/adminApiClient';
import type {
AdminCreationEntryConfigResponse,
UnifiedCreationSpecPayload,
} from '../api/adminApiTypes';
import { AdminCreationEntrySwitchPage } from './AdminCreationEntrySwitchPage';
vi.mock('../api/adminApiClient', () => ({
formatAdminApiError: vi.fn((error: unknown) =>
error instanceof Error ? error.message : '请求失败',
),
getAdminCreationEntryConfig: vi.fn(),
isAdminApiError: vi.fn(() => false),
upsertAdminCreationEntryBanners: vi.fn(),
upsertAdminCreationEntryConfig: vi.fn(),
upsertAdminPublicWorkInteractions: vi.fn(),
}));
const puzzleSpec: UnifiedCreationSpecPayload = {
playId: 'puzzle',
title: '拼图',
mudPointCost: 10,
workspaceStage: 'puzzle-agent-workspace',
generationStage: 'puzzle-generating',
resultStage: 'puzzle-result',
fields: [
{
id: 'pictureDescription',
kind: 'text',
label: '画面描述',
required: true,
},
],
};
const configResponse: AdminCreationEntryConfigResponse = {
eventBanners: [
{
title: '创作公告',
description: '',
coverImageSrc: '',
prizePoolMudPoints: 0,
startsAtText: '',
endsAtText: '',
renderMode: 'html',
htmlCode: '<section>后台公告</section>',
},
],
publicWorkInteractions: [
{
sourceType: 'puzzle',
likeEnabled: true,
remixEnabled: true,
likeDisabledMessage: '拼图点赞暂不可用。',
remixDisabledMessage: '拼图作品改造暂不可用。',
},
],
entries: [
{
id: 'puzzle',
title: '拼图',
subtitle: '拼图关卡创作',
badge: '可创建',
imageSrc: '/creation-type-references/puzzle.webp',
visible: true,
open: true,
sortOrder: 30,
categoryId: 'recommended',
categoryLabel: '热门推荐',
categorySortOrder: 20,
updatedAtMicros: 1,
unifiedCreationSpec: puzzleSpec,
},
],
};
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(getAdminCreationEntryConfig).mockResolvedValue(configResponse);
vi.mocked(upsertAdminCreationEntryBanners).mockResolvedValue(configResponse);
vi.mocked(upsertAdminCreationEntryConfig).mockResolvedValue(configResponse);
vi.mocked(upsertAdminPublicWorkInteractions).mockResolvedValue(
configResponse,
);
});
test('创作入口后台展示并保存统一创作契约', async () => {
const user = userEvent.setup();
const { container } = render(
<AdminCreationEntrySwitchPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
await screen.findByText('pictureDescription');
expect(
container.querySelector('.admin-subsection .admin-info-list'),
).not.toBeNull();
expect(
container.querySelector('.admin-subsection .admin-info-list')?.textContent,
).toContain('拼图');
expect(container.querySelector('.admin-panel .admin-panel')).toBeNull();
expect(container.querySelector('.admin-muted')).toBeNull();
expect(screen.queryByLabelText('契约 JSON')).toBeNull();
expect(screen.queryByText('puzzle-generating')).toBeNull();
await user.click(screen.getByRole('button', { name: '修改契约' }));
const dialog = screen.getByRole('dialog', { name: '统一创作契约' });
expect(within(dialog).queryByLabelText('玩法 ID')).toBeNull();
expect(within(dialog).queryByLabelText('工作台阶段')).toBeNull();
expect(within(dialog).queryByLabelText('生成阶段')).toBeNull();
expect(within(dialog).queryByLabelText('结果阶段')).toBeNull();
fireEvent.change(within(dialog).getByLabelText('泥点消耗'), {
target: { value: '12' },
});
await user.click(within(dialog).getByRole('button', { name: '应用修改' }));
expect(screen.queryByRole('dialog', { name: '统一创作契约' })).toBeNull();
expect(screen.getByText('12泥点数')).toBeTruthy();
await user.click(screen.getByRole('button', { name: '保存入库' }));
await user.click(screen.getByRole('button', { name: '确认' }));
await waitFor(() => {
expect(upsertAdminCreationEntryConfig).toHaveBeenCalledWith(
'admin-token',
expect.objectContaining({
id: 'puzzle',
unifiedCreationSpec: {
...puzzleSpec,
mudPointCost: 12,
},
}),
);
});
});
test('创作入口后台拒绝 playId 不一致的统一创作契约', async () => {
const user = userEvent.setup();
vi.mocked(getAdminCreationEntryConfig).mockResolvedValueOnce({
...configResponse,
entries: [
{
...configResponse.entries[0]!,
unifiedCreationSpec: {
...puzzleSpec,
playId: 'match3d',
},
},
],
});
render(
<AdminCreationEntrySwitchPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
await screen.findByText('pictureDescription');
await user.click(screen.getByRole('button', { name: '保存入库' }));
expect(
await screen.findByText('统一创作契约 playId 必须与入口 ID 一致'),
).toBeTruthy();
expect(upsertAdminCreationEntryConfig).not.toHaveBeenCalled();
});
test('创作入口后台用表单保存公告配置', async () => {
const user = userEvent.setup();
render(
<AdminCreationEntrySwitchPage
mode="announcements"
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
expect(
await screen.findAllByRole('heading', { name: '创作入口公告' }),
).toHaveLength(2);
expect(screen.queryByLabelText('公告代码 JSON')).toBeNull();
fireEvent.change(await screen.findByLabelText('公告 1 标题'), {
target: { value: '周末创作赛' },
});
fireEvent.change(screen.getByLabelText('公告 1 HTML'), {
target: { value: '<section>新的入口公告</section>' },
});
await user.click(screen.getByRole('button', { name: '新增公告' }));
fireEvent.change(screen.getByLabelText('公告 2 标题'), {
target: { value: '第二条公告' },
});
fireEvent.change(screen.getByLabelText('公告 2 HTML'), {
target: { value: '<section>轮播第二条</section>' },
});
await user.click(screen.getByRole('button', { name: '保存公告' }));
await user.click(screen.getByRole('button', { name: '确认' }));
await waitFor(() => {
expect(upsertAdminCreationEntryBanners).toHaveBeenCalled();
});
const [, payload] = vi.mocked(upsertAdminCreationEntryBanners).mock.calls[0]!;
expect(JSON.parse(payload.eventBannersJson)).toEqual([
{
title: '周末创作赛',
htmlCode: '<section>新的入口公告</section>',
},
{
title: '第二条公告',
htmlCode: '<section>轮播第二条</section>',
},
]);
expect(JSON.parse(payload.eventBannersJson)[0]).not.toHaveProperty(
'description',
);
expect(JSON.parse(payload.eventBannersJson)[0]).not.toHaveProperty(
'coverImageSrc',
);
});
test('创作入口后台用表单保存作品互动配置', async () => {
const user = userEvent.setup();
render(
<AdminCreationEntrySwitchPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
await screen.findByText('作品互动');
const likeToggle = screen.getAllByRole('checkbox')[0]!;
await user.click(likeToggle);
fireEvent.change(screen.getByLabelText('拼图 / puzzle 点赞关闭提示'), {
target: { value: '拼图点赞维护中。' },
});
await user.click(screen.getByRole('button', { name: '保存作品互动' }));
await user.click(screen.getByRole('button', { name: '确认' }));
await waitFor(() => {
expect(upsertAdminPublicWorkInteractions).toHaveBeenCalledWith(
'admin-token',
{
publicWorkInteractions: [
{
sourceType: 'puzzle',
likeEnabled: false,
remixEnabled: true,
likeDisabledMessage: '拼图点赞维护中。',
remixDisabledMessage: '拼图作品改造暂不可用。',
},
],
},
);
});
});
test('创作入口后台把旧结构化公告回显成 HTML 表单', async () => {
vi.mocked(getAdminCreationEntryConfig).mockResolvedValueOnce({
...configResponse,
eventBanners: [
{
title: '旧公告 <标题>',
description: '旧描述 & 需要转义',
coverImageSrc: '/legacy.png',
prizePoolMudPoints: 120,
startsAtText: '2026-06-01',
endsAtText: '2026-06-30',
renderMode: 'structured',
},
],
});
render(
<AdminCreationEntrySwitchPage
mode="announcements"
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
expect(await screen.findByLabelText('公告 1 标题')).toHaveProperty(
'value',
'旧公告 <标题>',
);
expect(screen.getByLabelText('公告 1 HTML')).toHaveProperty(
'value',
'<section><h1>旧公告 &lt;标题&gt;</h1><p>旧描述 &amp; 需要转义</p></section>',
);
});
test('创作入口后台拒绝空公告表单', async () => {
const user = userEvent.setup();
render(
<AdminCreationEntrySwitchPage
mode="announcements"
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
fireEvent.change(await screen.findByLabelText('公告 1 标题'), {
target: { value: '' },
});
fireEvent.change(screen.getByLabelText('公告 1 HTML'), {
target: { value: '' },
});
await user.click(screen.getByRole('button', { name: '保存公告' }));
expect(await screen.findByText('公告 1 标题和 HTML 都不能为空')).toBeTruthy();
expect(upsertAdminCreationEntryBanners).not.toHaveBeenCalled();
});