237 lines
7.0 KiB
TypeScript
237 lines
7.0 KiB
TypeScript
/* @vitest-environment jsdom */
|
|
|
|
import {fireEvent, render, screen, waitFor} from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import {beforeEach, expect, test, vi} from 'vitest';
|
|
|
|
import {
|
|
getAdminCreationEntryConfig,
|
|
upsertAdminCreationEntryBanners,
|
|
upsertAdminCreationEntryConfig,
|
|
} 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(),
|
|
}));
|
|
|
|
const puzzleSpec: UnifiedCreationSpecPayload = {
|
|
playId: 'puzzle',
|
|
title: '想做个什么玩法?',
|
|
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>',
|
|
},
|
|
],
|
|
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);
|
|
});
|
|
|
|
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-panel .admin-panel')).toBeNull();
|
|
expect(container.querySelector('.admin-muted')).toBeNull();
|
|
|
|
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,
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
test('创作入口后台拒绝 playId 不一致的统一创作契约', async () => {
|
|
const user = userEvent.setup();
|
|
render(
|
|
<AdminCreationEntrySwitchPage
|
|
token="admin-token"
|
|
onUnauthorized={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
const textarea = await screen.findByLabelText('契约 JSON');
|
|
fireEvent.change(textarea, {
|
|
target: {
|
|
value: JSON.stringify({
|
|
...puzzleSpec,
|
|
playId: 'match3d',
|
|
}),
|
|
},
|
|
});
|
|
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('创作入口后台把旧结构化公告回显成 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();
|
|
});
|