329 lines
9.7 KiB
TypeScript
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>旧公告 <标题></h1><p>旧描述 & 需要转义</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();
|
|
});
|