Merge branch 'codex/feature-1'

# Conflicts:
#	docs/【玩法创作】平台入口与玩法链路-2026-05-15.md
#	src/components/platform-entry/PlatformEntryFlowShellImpl.tsx
#	src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx
#	src/services/miniGameDraftGenerationProgress.ts
This commit is contained in:
2026-06-03 03:56:25 +08:00
51 changed files with 3075 additions and 482 deletions

View File

@@ -236,8 +236,12 @@ async function openCreateTemplateHub(user: ReturnType<typeof userEvent.setup>) {
expect(panel.getAttribute('aria-hidden')).toBe('false');
});
expect(
await within(panel).findByRole('tablist', { name: '玩法模板分类' }),
await within(panel).findByRole('tablist', { name: '创作入口页签' }),
).toBeTruthy();
// 中文注释:真实最近创作存在时会成为默认页签,模板入口用例需显式切回模板分类。
if (!within(panel).queryByRole('button', { name: //u })) {
await user.click(await within(panel).findByRole('tab', { name: '热门推荐' }));
}
expect(
await within(panel).findByRole('button', { name: //u }),
).toBeTruthy();
@@ -367,6 +371,26 @@ const testCreationEntryConfig = {
startsAtText: '2026-05-01',
endsAtText: '2026-05-31',
},
eventBanners: [
{
title: '后台拼图赛',
description: '后台配置的拼图横幅。',
coverImageSrc: '/creation-type-references/puzzle.webp',
prizePoolMudPoints: 1000,
startsAtText: '2026-05-01',
endsAtText: '2026-05-31',
renderMode: 'structured' as const,
},
{
title: '后台抓大鹅赛',
description: '后台配置的抓大鹅横幅。',
coverImageSrc: '/creation-type-references/match3d.webp',
prizePoolMudPoints: 1200,
startsAtText: '2026-06-01',
endsAtText: '2026-06-30',
renderMode: 'structured' as const,
},
],
creationTypes: [
{
id: 'rpg',
@@ -377,9 +401,9 @@ const testCreationEntryConfig = {
visible: true,
open: true,
sortOrder: 10,
categoryId: 'recent',
categoryLabel: '最近创作',
categorySortOrder: 10,
categoryId: 'recommended',
categoryLabel: '热门推荐',
categorySortOrder: 20,
updatedAtMicros: 1,
},
{
@@ -391,9 +415,9 @@ const testCreationEntryConfig = {
visible: true,
open: true,
sortOrder: 30,
categoryId: 'recent',
categoryLabel: '最近创作',
categorySortOrder: 10,
categoryId: 'recommended',
categoryLabel: '热门推荐',
categorySortOrder: 20,
updatedAtMicros: 1,
},
{
@@ -405,9 +429,9 @@ const testCreationEntryConfig = {
visible: true,
open: true,
sortOrder: 40,
categoryId: 'recent',
categoryLabel: '最近创作',
categorySortOrder: 10,
categoryId: 'recommended',
categoryLabel: '热门推荐',
categorySortOrder: 20,
updatedAtMicros: 1,
},
{
@@ -419,9 +443,9 @@ const testCreationEntryConfig = {
visible: true,
open: true,
sortOrder: 45,
categoryId: 'recent',
categoryLabel: '最近创作',
categorySortOrder: 10,
categoryId: 'recommended',
categoryLabel: '热门推荐',
categorySortOrder: 20,
updatedAtMicros: 1,
},
{
@@ -433,9 +457,9 @@ const testCreationEntryConfig = {
visible: false,
open: true,
sortOrder: 50,
categoryId: 'recent',
categoryLabel: '最近创作',
categorySortOrder: 10,
categoryId: 'recommended',
categoryLabel: '热门推荐',
categorySortOrder: 20,
updatedAtMicros: 1,
},
{
@@ -447,9 +471,9 @@ const testCreationEntryConfig = {
visible: false,
open: false,
sortOrder: 60,
categoryId: 'recent',
categoryLabel: '最近创作',
categorySortOrder: 10,
categoryId: 'recommended',
categoryLabel: '热门推荐',
categorySortOrder: 20,
updatedAtMicros: 1,
},
{
@@ -461,9 +485,9 @@ const testCreationEntryConfig = {
visible: true,
open: false,
sortOrder: 70,
categoryId: 'recent',
categoryLabel: '最近创作',
categorySortOrder: 10,
categoryId: 'recommended',
categoryLabel: '热门推荐',
categorySortOrder: 20,
updatedAtMicros: 1,
},
{
@@ -475,9 +499,9 @@ const testCreationEntryConfig = {
visible: false,
open: true,
sortOrder: 80,
categoryId: 'recent',
categoryLabel: '最近创作',
categorySortOrder: 10,
categoryId: 'recommended',
categoryLabel: '热门推荐',
categorySortOrder: 20,
updatedAtMicros: 1,
},
{
@@ -489,9 +513,9 @@ const testCreationEntryConfig = {
visible: true,
open: true,
sortOrder: 90,
categoryId: 'recent',
categoryLabel: '最近创作',
categorySortOrder: 10,
categoryId: 'recommended',
categoryLabel: '热门推荐',
categorySortOrder: 20,
updatedAtMicros: 1,
},
],
@@ -645,6 +669,22 @@ vi.mock('../../services/jump-hop/jumpHopClient', () => ({
},
}));
vi.mock('../../services/wooden-fish/woodenFishClient', () => ({
woodenFishClient: {
checkpointRun: vi.fn(),
createSession: vi.fn(),
executeAction: vi.fn(),
finishRun: vi.fn(),
getGalleryDetail: vi.fn(),
getSession: vi.fn(),
getWorkDetail: vi.fn(),
listGallery: vi.fn(),
listWorks: vi.fn(),
publishWork: vi.fn(),
startRun: vi.fn(),
},
}));
vi.mock('../../services/match3d-creation', () => ({
match3dCreationClient: {
createSession: vi.fn(),
@@ -2707,6 +2747,18 @@ beforeEach(() => {
vi.mocked(jumpHopClient.getWorkDetail).mockRejectedValue(
new Error('未找到跳一跳作品'),
);
vi.mocked(woodenFishClient.listGallery).mockResolvedValue({
items: [],
hasMore: false,
nextCursor: null,
});
vi.mocked(woodenFishClient.listWorks).mockResolvedValue({ items: [] });
vi.mocked(woodenFishClient.getSession).mockRejectedValue(
new Error('未找到敲木鱼会话'),
);
vi.mocked(woodenFishClient.getWorkDetail).mockRejectedValue(
new Error('未找到敲木鱼作品'),
);
vi.mocked(saveBabyObjectMatchDraft).mockImplementation(async (payload) => ({
draft: payload.draft,
}));
@@ -3669,14 +3721,17 @@ test('create tab shows template tabs and embeds puzzle form by default', async (
render(<TestWrapper withAuth />);
expect(screen.queryByText('后台拼图赛')).toBeNull();
await openCreateTemplateHub(user);
expect(screen.getByRole('tablist', { name: '玩法模板分类' })).toBeTruthy();
expect(screen.getByRole('tablist', { name: '创作入口页签' })).toBeTruthy();
expect(await screen.findByText('后台拼图赛')).toBeTruthy();
expect(screen.getByText('后台抓大鹅赛')).toBeTruthy();
expect(
screen.getByRole('tablist', { name: '玩法模板分类' }).className,
screen.getByRole('tablist', { name: '创作入口页签' }).className,
).toContain('scroll-px-2');
expect(
screen.getByRole('tab', { name: '最近创作' }).getAttribute('aria-selected'),
screen.getByRole('tab', { name: '热门推荐' }).getAttribute('aria-selected'),
).toBe('true');
expect(await findCreationTypeButton('拼图')).toBeTruthy();
expect(await findCreationTypeButton('文字冒险')).toBeTruthy();
@@ -3686,7 +3741,7 @@ test('create tab shows template tabs and embeds puzzle form by default', async (
expect(queryCreationTypeButton('智能创作')).toBeNull();
expect(
screen
.getByRole('tab', { name: '最近创作' })
.getByRole('tab', { name: '热门推荐' })
.querySelector('[class*="bg-[#d9793f]"]'),
).toBeTruthy();
expect(screen.queryByRole('button', { name: /智能创作/u })).toBeNull();
@@ -3697,6 +3752,69 @@ test('create tab shows template tabs and embeds puzzle form by default', async (
expect(createPuzzleAgentSession).not.toHaveBeenCalled();
});
test('create tab shows recent tab when backend returns failed drafts', async () => {
const user = userEvent.setup();
mockExistingRpgDraftShelf({
title: '入口可见的失败草稿',
summary: '失败草稿也要进入创作入口最近创作。',
stage: 'failed',
stageLabel: '生成失败待处理',
updatedAt: '2026-06-02T10:00:00.000Z',
});
render(<TestWrapper withAuth />);
await clickFirstButtonByName(user, '创作');
const panel = getPlatformTabPanel('create');
const tablist = await within(panel).findByRole('tablist', {
name: '创作入口页签',
});
expect(tablist).toBeTruthy();
expect(
within(panel)
.getByRole('tab', { name: '最近创作' })
.getAttribute('aria-selected'),
).toBe('true');
expect(await within(panel).findByText('入口可见的失败草稿')).toBeTruthy();
expect(
within(panel).getByText('失败草稿也要进入创作入口最近创作。'),
).toBeTruthy();
expect(within(panel).getByText('生成失败待处理')).toBeTruthy();
});
test('create tab refreshes recent works after opening from an empty draft shelf', async () => {
const user = userEvent.setup();
const failedDraft = buildExistingRpgDraftWork({
title: '点击创作后出现的失败草稿',
summary: '创作入口需要在进入时重新读取真实作品架。',
stage: 'error',
stageLabel: '发生错误',
updatedAt: '2026-06-02T10:30:00.000Z',
});
vi.mocked(listRpgCreationWorks)
.mockResolvedValueOnce([])
.mockResolvedValue([failedDraft]);
render(<TestWrapper withAuth />);
await openDraftHub(user);
expect(within(getPlatformTabPanel('saves')).getByText('还没有作品')).toBeTruthy();
await clickFirstButtonByName(user, '创作');
const panel = getPlatformTabPanel('create');
expect(await within(panel).findByText('点击创作后出现的失败草稿')).toBeTruthy();
expect(
within(panel).getByText('创作入口需要在进入时重新读取真实作品架。'),
).toBeTruthy();
expect(within(panel).getByText('发生错误')).toBeTruthy();
await waitFor(() => {
expect(
vi.mocked(listRpgCreationWorks).mock.calls.length,
).toBeGreaterThanOrEqual(2);
});
});
test('create tab opens match3d entry form from the template card', async () => {
const user = userEvent.setup();
@@ -3845,10 +3963,10 @@ test('bark battle form checks mud points before creating image assets', async ()
within(noticeDialog).getByText('本次需要 3 泥点,当前 2 泥点。'),
).toBeTruthy();
expect(screen.getByText('汪汪声浪配置表单')).toBeTruthy();
expect(screen.queryByRole('tablist', { name: '玩法模板分类' })).toBeNull();
expect(
(screen.getByLabelText('汪汪作品标题') as HTMLInputElement).value,
).toBe('自定义声浪杯');
expect(screen.queryByRole('tablist', { name: '创作入口页签' })).toBeNull();
expect((screen.getByLabelText('汪汪作品标题') as HTMLInputElement).value).toBe(
'自定义声浪杯',
);
expect(createBarkBattleDraft).not.toHaveBeenCalled();
expect(generateAllBarkBattleImageAssets).not.toHaveBeenCalled();
});
@@ -3979,7 +4097,7 @@ test('running match3d form generation can return to draft tab and reopen progres
await user.click(screen.getByRole('button', { name: '返回创作中心' }));
await openDraftHub(user);
expect(await screen.findByText('抓大鹅草稿')).toBeTruthy();
expect((await screen.findAllByText('抓大鹅草稿')).length).toBeGreaterThan(0);
await expectDraftHubGeneratingBadgeCountAtLeast(1);
await user.click(
@@ -4883,7 +5001,7 @@ test('puzzle form checks mud points before creating a draft', async () => {
within(noticeDialog).getByText('本次需要 2 泥点,当前 1 泥点。'),
).toBeTruthy();
expect(screen.getByText('拼图工作区missing-session')).toBeTruthy();
expect(screen.queryByRole('tablist', { name: '玩法模板分类' })).toBeNull();
expect(screen.queryByRole('tablist', { name: '创作入口页签' })).toBeNull();
expect(createPuzzleAgentSession).not.toHaveBeenCalled();
expect(executePuzzleAgentAction).not.toHaveBeenCalled();
});
@@ -4910,7 +5028,7 @@ test('match3d form checks mud points before creating a draft', async () => {
within(noticeDialog).getByText('本次需要 10 泥点,当前 9 泥点。'),
).toBeTruthy();
expect(screen.getByText('抓大鹅工作区missing-session')).toBeTruthy();
expect(screen.queryByRole('tablist', { name: '玩法模板分类' })).toBeNull();
expect(screen.queryByRole('tablist', { name: '创作入口页签' })).toBeNull();
expect(match3dCreationClient.createSession).not.toHaveBeenCalled();
expect(match3dCreationClient.executeAction).not.toHaveBeenCalled();
});
@@ -7727,8 +7845,8 @@ test('embedded puzzle form maps raw bearer token errors to user-facing auth copy
expect(createPuzzleAgentSession).toHaveBeenCalledTimes(1);
expect(createCreativeAgentSession).not.toHaveBeenCalled();
expect(
await screen.findAllByText('当前登录状态已失效,请重新登录后继续。'),
).not.toHaveLength(0);
(await screen.findAllByText('当前登录状态已失效,请重新登录后继续。')).length,
).toBeTruthy();
expect(screen.queryByText('缺少 Authorization Bearer Token')).toBeNull();
});
@@ -9022,11 +9140,11 @@ test('running custom world draft generation can return to creation center with s
await user.click(screen.getByRole('button', { name: '返回创作中心' }));
expect(
await screen.findByRole('tablist', { name: '玩法模板分类' }),
await screen.findByRole('tablist', { name: '创作入口页签' }),
).toBeTruthy();
await openDraftHub(user);
expect(await screen.findByText('潮雾列岛')).toBeTruthy();
expect((await screen.findAllByText('潮雾列岛')).length).toBeGreaterThan(0);
await expectDraftHubGeneratingBadgeCountAtLeast(1);
});
@@ -10420,7 +10538,7 @@ test('manual tab switch is preserved after platform bootstrap requests finish',
await clickFirstButtonByName(user, '创作');
expect(
await screen.findByRole('tablist', { name: '玩法模板分类' }),
await screen.findByRole('tablist', { name: '创作入口页签' }),
).toBeTruthy();
resolveGalleryRequest([]);
@@ -10428,7 +10546,7 @@ test('manual tab switch is preserved after platform bootstrap requests finish',
await waitFor(() => {
expect(
within(getPlatformTabPanel('create')).getByRole('tablist', {
name: '玩法模板分类',
name: '创作入口页签',
}),
).toBeTruthy();
});
@@ -11195,7 +11313,11 @@ test('creation hub published work card reveals delete action after card action r
const deleteButtons = screen.getAllByRole('button', { name: '删除' });
expect(deleteButtons.length).toBeGreaterThan(0);
await user.click(deleteButtons[0]!);
const deleteButton = deleteButtons[0];
if (!deleteButton) {
throw new Error('delete button should exist after swipe');
}
await user.click(deleteButton);
const dialog = await screen.findByRole('dialog', { name: '删除作品' });
expect(dialog.parentElement?.className).toContain('platform-theme--light');