This commit is contained in:
2026-05-08 11:44:42 +08:00
parent b08127031c
commit abf1f1ebea
249 changed files with 39411 additions and 887 deletions

View File

@@ -6,6 +6,7 @@ import { useState } from 'react';
import { beforeEach, expect, test, vi } from 'vitest';
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { CreativeAgentSessionSnapshot } from '../../../packages/shared/src/contracts/creativeAgent';
import type {
CustomWorldAgentSessionSnapshot,
CustomWorldWorkSummary,
@@ -43,6 +44,13 @@ import {
submitBigFishInput,
} from '../../services/big-fish-runtime';
import { listBigFishWorks } from '../../services/big-fish-works';
import {
cancelCreativeAgentSession,
confirmCreativePuzzleTemplate,
createCreativeAgentSession,
streamCreativeAgentMessage,
streamCreativeDraftEdit,
} from '../../services/creative-agent';
import { match3dCreationClient } from '../../services/match3d-creation';
import {
clickMatch3DItem,
@@ -109,6 +117,20 @@ import {
recordRpgEntryWorldGalleryPlay,
remixRpgEntryWorldGallery,
} from '../../services/rpg-entry/rpgEntryLibraryClient';
import { squareHoleCreationClient } from '../../services/square-hole-creation';
import {
dropSquareHoleShape,
finishSquareHoleTimeUp,
restartSquareHoleRun,
startSquareHoleRun,
stopSquareHoleRun,
} from '../../services/square-hole-runtime';
import {
deleteSquareHoleWork,
getSquareHoleWorkDetail,
listSquareHoleGallery,
listSquareHoleWorks,
} from '../../services/square-hole-works';
import { type CustomWorldProfile, WorldType } from '../../types';
import {
AuthUiContext,
@@ -136,18 +158,51 @@ async function clickFirstAsyncButtonByName(
await user.click(buttons[0]!);
}
async function openCreationHub(user: ReturnType<typeof userEvent.setup>) {
async function openCreateTemplateHub(user: ReturnType<typeof userEvent.setup>) {
await clickFirstButtonByName(user, '创作');
expect(
await screen.findByRole('button', { name: /.*/u }),
await screen.findByText('10分钟创作一个精品互动玩法'),
).toBeTruthy();
expect(screen.getByRole('tab', { name: '拼图' })).toBeTruthy();
expect(screen.getByText('拼图工作区missing-session')).toBeTruthy();
}
async function openDraftHub(user: ReturnType<typeof userEvent.setup>) {
await clickFirstButtonByName(user, '草稿');
const panel = getPlatformTabPanel('saves');
await waitFor(() => {
expect(panel.getAttribute('aria-hidden')).toBe('false');
});
expect(
await within(panel).findByRole('button', { name: //u }),
).toBeTruthy();
}
async function openDiscoverHub(user: ReturnType<typeof userEvent.setup>) {
await clickFirstButtonByName(user, '发现');
const panel = getPlatformTabPanel('category');
await waitFor(() => {
expect(panel.getAttribute('aria-hidden')).toBe('false');
});
expect(
await within(panel).findByPlaceholderText(
'搜索作品号、名称、作者、描述',
),
).toBeTruthy();
return panel;
}
async function openProfilePlayedWorks(user: ReturnType<typeof userEvent.setup>) {
await clickFirstButtonByName(user, '我的');
await user.click(await screen.findByRole('button', { name: //u }));
expect(await screen.findByText('可继续')).toBeTruthy();
}
async function openExistingRpgDraft(
user: ReturnType<typeof userEvent.setup>,
actionName: string | RegExp = /(?:|)/u,
) {
await openCreationHub(user);
await openDraftHub(user);
await user.click(await screen.findByRole('button', { name: actionName }));
}
@@ -289,6 +344,40 @@ vi.mock('../../services/match3d-runtime', () => ({
stopMatch3DRun: vi.fn(),
}));
vi.mock('../../services/square-hole-creation', () => ({
squareHoleCreationClient: {
createSession: vi.fn(),
executeAction: vi.fn(),
getSession: vi.fn(),
sendMessage: vi.fn(),
streamMessage: vi.fn(),
},
}));
vi.mock('../../services/square-hole-runtime', () => ({
dropSquareHoleShape: vi.fn(),
finishSquareHoleTimeUp: vi.fn(),
getSquareHoleRun: vi.fn(),
restartSquareHoleRun: vi.fn(),
startSquareHoleRun: vi.fn(),
stopSquareHoleRun: vi.fn(),
}));
vi.mock('../../services/square-hole-works', () => ({
deleteSquareHoleWork: vi.fn(),
getSquareHoleWorkDetail: vi.fn(),
listSquareHoleGallery: vi.fn(),
listSquareHoleWorks: vi.fn(),
}));
vi.mock('../../services/creative-agent', () => ({
cancelCreativeAgentSession: vi.fn(),
confirmCreativePuzzleTemplate: vi.fn(),
createCreativeAgentSession: vi.fn(),
streamCreativeAgentMessage: vi.fn(),
streamCreativeDraftEdit: vi.fn(),
}));
vi.mock('../../services/puzzle-runtime/puzzleLocalRuntime', async () => {
const actual = await vi.importActual<
typeof import('../../services/puzzle-runtime/puzzleLocalRuntime')
@@ -573,6 +662,168 @@ const mockAuthUser: AuthUser = {
createdAt: new Date().toISOString(),
};
function buildMockCreativeAgentSession(
overrides: Partial<CreativeAgentSessionSnapshot> = {},
): CreativeAgentSessionSnapshot {
const sessionId = overrides.sessionId ?? 'creative-agent-session-1';
return {
sessionId,
stage: 'waiting_user',
inputSummary: {
text: null,
entryContext: 'creation_home',
images: [],
materialSummary: null,
unsupportedCapabilities: [],
},
messages: [
{
id: 'creative-agent-message-1',
role: 'assistant',
kind: 'chat',
text: '说一个灵感,我来帮你做成互动内容。',
createdAt: '2026-05-05T10:00:00.000Z',
},
],
puzzleTemplateCatalog: [],
puzzleTemplateSelection: null,
puzzleImageGenerationPlan: null,
targetBinding: null,
updatedAt: '2026-05-05T10:00:00.000Z',
...overrides,
};
}
function buildMockSquareHoleAgentSession(
overrides: Partial<Parameters<typeof buildMockSquareHoleAgentSessionImpl>[0]> = {},
) {
return buildMockSquareHoleAgentSessionImpl(overrides);
}
function buildMockSquareHoleAgentSessionImpl(
overrides: Partial<{
sessionId: string;
stage: string;
messages: Array<{ id: string; role: string; kind: string; text: string; createdAt: string }>;
updatedAt: string;
}> = {},
) {
const sessionId = overrides.sessionId ?? 'square-hole-session-1';
return {
sessionId,
currentTurn: 0,
progressPercent: 20,
stage: 'collecting_config',
anchorPack: {
theme: {
key: 'theme',
label: '题材主题',
value: '霓虹形状',
status: 'confirmed',
},
twistRule: {
key: 'twistRule',
label: '反直觉规则',
value: '颜色会误导洞口',
status: 'confirmed',
},
shapeCount: {
key: 'shapeCount',
label: '形状数量',
value: '12',
status: 'confirmed',
},
difficulty: {
key: 'difficulty',
label: '难度',
value: '5',
status: 'confirmed',
},
},
config: {
themeText: '霓虹形状',
twistRule: '颜色会误导洞口',
shapeCount: 12,
difficulty: 5,
shapeOptions: [
{
optionId: 'shape-square',
shapeKind: 'square',
label: '方块',
targetHoleId: 'hole-square',
imagePrompt: '霓虹方块',
imageSrc: null,
},
],
holeOptions: [
{
holeId: 'hole-square',
holeKind: 'square',
label: '方洞',
imagePrompt: '发光方洞',
imageSrc: null,
},
],
backgroundPrompt: '霓虹街机背景',
coverImageSrc: null,
backgroundImageSrc: null,
},
draft: null,
messages: [
{
id: 'square-hole-message-1',
role: 'assistant',
kind: 'chat',
text: '先确定方洞挑战的题材和反直觉规则。',
createdAt: '2026-05-01T10:00:00.000Z',
},
],
lastAssistantReply: '先确定方洞挑战的题材和反直觉规则。',
publishedProfileId: null,
updatedAt: '2026-05-01T10:00:00.000Z',
...overrides,
};
}
function buildMockSquareHoleRun(profileId: string) {
return {
runId: `square-hole-run-${profileId}`,
profileId,
ownerUserId: 'user-2',
status: 'running',
snapshotVersion: 1,
startedAtMs: 1_000,
durationLimitMs: 600_000,
remainingMs: 600_000,
totalShapeCount: 12,
completedShapeCount: 0,
combo: 0,
bestCombo: 0,
score: 0,
ruleLabel: '颜色会误导洞口',
currentShape: {
shapeId: 'shape-1',
shapeKind: 'square',
label: '方块',
targetHoleId: 'hole-square',
color: '#ff5f7e',
imageSrc: null,
},
holes: [
{
holeId: 'hole-square',
holeKind: 'square',
label: '方洞',
x: 0.2,
y: 0.5,
},
],
lastFeedback: null,
};
}
function buildMockPuzzleRun(
profileId: string,
levelName: string,
@@ -1779,6 +2030,50 @@ beforeEach(() => {
vi.mocked(stopMatch3DRun).mockResolvedValue({
run: buildMockMatch3DRun('match3d-profile-stopped'),
});
vi.mocked(squareHoleCreationClient.createSession).mockResolvedValue({
session: buildMockSquareHoleAgentSession(),
});
vi.mocked(squareHoleCreationClient.getSession).mockResolvedValue({
session: buildMockSquareHoleAgentSession(),
});
vi.mocked(squareHoleCreationClient.streamMessage).mockResolvedValue(
buildMockSquareHoleAgentSession(),
);
vi.mocked(squareHoleCreationClient.executeAction).mockResolvedValue({
session: buildMockSquareHoleAgentSession(),
});
vi.mocked(listSquareHoleWorks).mockResolvedValue({
items: [],
});
vi.mocked(listSquareHoleGallery).mockResolvedValue({
items: [],
});
vi.mocked(getSquareHoleWorkDetail).mockRejectedValue(
new Error('未找到方洞挑战作品'),
);
vi.mocked(deleteSquareHoleWork).mockResolvedValue({
items: [],
});
vi.mocked(startSquareHoleRun).mockResolvedValue({
run: buildMockSquareHoleRun('square-hole-profile-1'),
});
vi.mocked(dropSquareHoleShape).mockResolvedValue({
feedback: {
accepted: true,
rejectReason: null,
message: '投入成功',
},
run: buildMockSquareHoleRun('square-hole-profile-1'),
});
vi.mocked(restartSquareHoleRun).mockResolvedValue({
run: buildMockSquareHoleRun('square-hole-profile-1'),
});
vi.mocked(finishSquareHoleTimeUp).mockResolvedValue({
run: buildMockSquareHoleRun('square-hole-profile-1'),
});
vi.mocked(stopSquareHoleRun).mockResolvedValue({
run: buildMockSquareHoleRun('square-hole-profile-1'),
});
vi.mocked(listPuzzleWorks).mockResolvedValue({
items: [],
});
@@ -1871,44 +2166,119 @@ beforeEach(() => {
});
vi.mocked(getRpgCreationSession).mockResolvedValue(mockSession);
vi.mocked(streamRpgCreationMessage).mockResolvedValue(mockSession);
vi.mocked(createCreativeAgentSession).mockResolvedValue({
session: buildMockCreativeAgentSession(),
});
vi.mocked(streamCreativeAgentMessage).mockImplementation(
async (sessionId, payload) =>
buildMockCreativeAgentSession({
sessionId,
stage: 'collaborating',
messages: [
{
id: 'creative-agent-message-1',
role: 'assistant',
kind: 'chat',
text: '说一个灵感,我来帮你做成互动内容。',
createdAt: '2026-05-05T10:00:00.000Z',
},
{
id: payload.clientMessageId,
role: 'user',
kind: 'chat',
text: payload.content
.map((part) =>
part.type === 'input_text' ? part.text.trim() : '参考图',
)
.filter(Boolean)
.join(' / '),
createdAt: '2026-05-05T10:01:00.000Z',
},
{
id: 'creative-agent-message-2',
role: 'assistant',
kind: 'chat',
text: '收到,我先帮你整理成可创作方案。',
createdAt: '2026-05-05T10:01:01.000Z',
},
],
}),
);
vi.mocked(cancelCreativeAgentSession).mockResolvedValue({
session: buildMockCreativeAgentSession({ stage: 'failed' }),
});
vi.mocked(confirmCreativePuzzleTemplate).mockResolvedValue({
session: buildMockCreativeAgentSession(),
});
vi.mocked(streamCreativeDraftEdit).mockResolvedValue(
buildMockCreativeAgentSession(),
);
});
test('create hub hides RPG while keeping Match3D open and future templates locked', async () => {
test('create tab shows template tabs and embeds puzzle form by default', async () => {
const user = userEvent.setup();
render(<TestWrapper withAuth />);
await openCreationHub(user);
await openCreateTemplateHub(user);
const match3dButton = screen.getByRole('button', {
name: /.*/u,
});
const airpButton = screen.getByRole('button', { name: /AIRP/u });
const visualNovelButton = screen.getByRole('button', {
name: //u,
});
expect((match3dButton as HTMLButtonElement).disabled).toBe(false);
expect((airpButton as HTMLButtonElement).disabled).toBe(true);
expect((visualNovelButton as HTMLButtonElement).disabled).toBe(true);
expect(screen.getByRole('tablist', { name: '选择模板' })).toBeTruthy();
expect(
screen.getByRole('tab', { name: '拼图' }).getAttribute('aria-selected'),
).toBe('true');
expect(
screen.getByRole('tab', { name: '拼图' }).querySelector('img')?.src,
).toContain('/creation-type-references/puzzle.webp');
expect(
screen.getByRole('tab', { name: '方洞挑战' }).querySelector('img')?.src,
).toContain('/creation-type-references/square-hole.webp');
expect(
screen.getByRole('tab', { name: '视觉小说' }).querySelector('img')?.src,
).toContain('/creation-type-references/visual-novel.webp');
expect(
screen.getByRole('tab', { name: 'AIRP' }).querySelector('img')?.src,
).toContain('/creation-type-references/airp.webp');
expect(
screen.getByRole('tab', { name: '拼图' }).querySelector('.text-white'),
).toBeTruthy();
expect(
screen.getByRole('tab', { name: '拼图' }).querySelector('.text-inherit'),
).toBeNull();
expect(screen.queryByRole('button', { name: //u })).toBeNull();
expect(screen.queryByPlaceholderText('问一问百梦')).toBeNull();
expect(screen.queryByRole('button', { name: //u })).toBeNull();
expect(screen.queryByRole('tab', { name: //u })).toBeNull();
expect(createRpgCreationSession).not.toHaveBeenCalled();
expect(match3dCreationClient.createSession).not.toHaveBeenCalled();
expect(createPuzzleAgentSession).not.toHaveBeenCalled();
});
test('platform create hub does not prefetch hidden big fish platform data', async () => {
test('embedded puzzle form routes through requireAuth while logged out', async () => {
const user = userEvent.setup();
const requireAuth = vi.fn();
render(<TestWrapper withAuth />);
render(
<TestWrapper
authValue={createAuthValue({
user: null,
openLoginModal: () => {},
requireAuth,
})}
/>,
);
await openCreationHub(user);
await openCreateTemplateHub(user);
const generateButton = await screen.findByRole('button', {
name: /稿/u,
});
expect(
await screen.findByRole('button', { name: /.*/u }),
).toBeTruthy();
expect(screen.queryByRole('button', { name: //u })).toBeNull();
expect(listBigFishWorks).not.toHaveBeenCalled();
expect(listBigFishGallery).not.toHaveBeenCalled();
await user.click(generateButton);
expect(requireAuth).toHaveBeenCalledTimes(1);
expect(createCreativeAgentSession).not.toHaveBeenCalled();
expect(streamCreativeAgentMessage).not.toHaveBeenCalled();
expect(createRpgCreationSession).not.toHaveBeenCalled();
expect(createPuzzleAgentSession).not.toHaveBeenCalled();
expect(match3dCreationClient.createSession).not.toHaveBeenCalled();
});
test('opening RPG agent workspace does not refetch session snapshot in a render loop', async () => {
@@ -2002,7 +2372,7 @@ test('create tab opens compiled agent draft in result refinement page', async ()
render(<TestWrapper withAuth />);
await openCreationHub(user);
await openDraftHub(user);
expect(await screen.findByRole('button', { name: //u })).toBeTruthy();
@@ -2056,7 +2426,7 @@ test('create tab resumes agent workspace when draft has no compiled result yet',
render(<TestWrapper withAuth />);
await openCreationHub(user);
await openDraftHub(user);
expect(await screen.findByRole('button', { name: //u })).toBeTruthy();
@@ -2112,7 +2482,7 @@ test('create tab resumes agent workspace when session has no draft profile even
render(<TestWrapper withAuth />);
await openCreationHub(user);
await openDraftHub(user);
expect(await screen.findByRole('button', { name: //u })).toBeTruthy();
await user.click(await screen.findByRole('button', { name: //u }));
@@ -2122,7 +2492,7 @@ test('create tab resumes agent workspace when session has no draft profile even
expect(screen.queryByText('世界档案')).toBeNull();
});
test('opening a compiled draft with a missing agent session falls back to create hub', async () => {
test('opening a compiled draft with a missing agent session falls back to draft hub', async () => {
const user = userEvent.setup();
vi.mocked(listRpgCreationWorks)
@@ -2166,20 +2536,22 @@ test('opening a compiled draft with a missing agent session falls back to create
render(<TestWrapper withAuth />);
await openCreationHub(user);
await openDraftHub(user);
await user.click(await screen.findByRole('button', { name: //u }));
const fallbackDraftPanel = getPlatformTabPanel('saves');
await waitFor(() => {
expect(fallbackDraftPanel.getAttribute('aria-hidden')).toBe('false');
expect(
within(getPlatformTabPanel('create')).getByText(
'这份共创草稿已失效,已为你返回创作中心,请重新开始创作。',
within(fallbackDraftPanel).getByText(
'这份共创草稿已失效,已为你返回草稿列表,请重新开始创作。',
),
).toBeTruthy();
});
expect(window.location.search).toBe('');
expect(listRpgCreationWorks).toHaveBeenCalledTimes(2);
expect(screen.getByText('还没有作品')).toBeTruthy();
expect(within(fallbackDraftPanel).getByText('还没有作品')).toBeTruthy();
expect(
screen.queryByText('Agent工作区custom-world-agent-session-missing'),
).toBeNull();
@@ -2374,6 +2746,7 @@ test('logged out public detail gates big fish start before local runtime', async
/>,
);
await openDiscoverHub(user);
const searchInput = await screen.findByPlaceholderText(
'搜索作品号、名称、作者、描述',
);
@@ -2469,23 +2842,22 @@ test('creation hub clears all private work shelves immediately after logout stat
const { rerender } = render(<TestWrapper authValue={loggedInAuth} />);
await openCreationHub(user);
const createPanel = getPlatformTabPanel('create');
await openDraftHub(user);
const draftPanel = getPlatformTabPanel('saves');
await waitFor(() => {
expect(listRpgCreationWorks).toHaveBeenCalled();
});
expect(await within(createPanel).findByText('拼图退出缓存作品')).toBeTruthy();
expect(within(createPanel).queryByText('RPG 退出缓存作品')).toBeNull();
expect(within(createPanel).queryByText('大鱼退出缓存作品')).toBeNull();
expect(await within(draftPanel).findByText('拼图退出缓存作品')).toBeTruthy();
expect(within(draftPanel).queryByText('RPG 退出缓存作品')).toBeNull();
expect(within(draftPanel).queryByText('大鱼退出缓存作品')).toBeNull();
rerender(<TestWrapper authValue={loggedOutAuth} />);
await waitFor(() => {
expect(within(createPanel).queryByText('RPG 退出缓存作品')).toBeNull();
expect(within(createPanel).queryByText('拼图退出缓存作品')).toBeNull();
expect(screen.queryByText('RPG 退出缓存作品')).toBeNull();
expect(screen.queryByText('拼图退出缓存作品')).toBeNull();
});
expect(within(createPanel).getByText('还没有作品')).toBeTruthy();
});
test('published puzzle works appear on home and mobile game category channel', async () => {
@@ -2522,12 +2894,15 @@ test('published puzzle works appear on home and mobile game category channel', a
expect(screen.getAllByText('星桥机关').length).toBeGreaterThan(0);
});
await user.click(screen.getByRole('button', { name: '游戏分类' }));
await clickFirstButtonByName(user, '发现');
await user.click(screen.getByRole('button', { name: '分类' }));
const homePanel = getPlatformTabPanel('home');
expect(within(homePanel).getAllByText('星桥机关').length).toBeGreaterThan(0);
const discoverPanel = getPlatformTabPanel('category');
expect(
within(homePanel).getAllByRole('button', { name: //u }).length,
within(discoverPanel).getAllByText('星桥机关').length,
).toBeGreaterThan(0);
expect(
within(discoverPanel).getAllByRole('button', { name: //u }).length,
).toBeGreaterThan(0);
expect(screen.queryByRole('button', { name: 'PC游戏' })).toBeNull();
expect(screen.queryByRole('button', { name: '即点即玩' })).toBeNull();
@@ -2564,12 +2939,13 @@ test('published big fish works stay hidden from platform home and game category
});
expect(screen.queryByText('机械深海 大鱼吃小鱼')).toBeNull();
await user.click(screen.getByRole('button', { name: '游戏分类' }));
await clickFirstButtonByName(user, '发现');
await user.click(screen.getByRole('button', { name: '分类' }));
const homePanel = getPlatformTabPanel('home');
expect(within(homePanel).queryByText('机械深海 大鱼吃小鱼')).toBeNull();
const discoverPanel = getPlatformTabPanel('category');
expect(within(discoverPanel).queryByText('机械深海 大鱼吃小鱼')).toBeNull();
expect(
within(homePanel).queryAllByRole('button', { name: //u }).length,
within(discoverPanel).queryAllByRole('button', { name: //u }).length,
).toBe(0);
});
@@ -2603,6 +2979,7 @@ test('published puzzle detail returns to the ranking platform tab', async () =>
render(<TestWrapper withAuth />);
await clickFirstButtonByName(user, '发现');
await user.click(await screen.findByRole('button', { name: '排行' }));
await waitFor(() => {
expect(document.getElementById('platform-tab-panel-category')).toBeTruthy();
@@ -2647,32 +3024,6 @@ test('published puzzle detail returns to the ranking platform tab', async () =>
});
});
test('selecting puzzle creation while logged out routes through requireAuth', async () => {
const user = userEvent.setup();
const requireAuth = vi.fn();
render(
<TestWrapper
authValue={createAuthValue({
user: null,
openLoginModal: () => {},
requireAuth,
})}
/>,
);
await openCreationHub(user);
const puzzleButton = await screen.findByRole('button', {
name: /.*/u,
});
expect((puzzleButton as HTMLButtonElement).disabled).toBe(false);
await user.click(puzzleButton);
expect(requireAuth).toHaveBeenCalledTimes(1);
expect(createRpgCreationSession).not.toHaveBeenCalled();
expect(createPuzzleAgentSession).not.toHaveBeenCalled();
});
test('restoring an agent workspace while logged out opens login modal before loading the protected session', async () => {
const openLoginModal = vi.fn();
@@ -2779,7 +3130,7 @@ test('refreshing RPG agent path restores stored agent workspace pointer', async
).toBeTruthy();
});
test('new puzzle creation entry maps raw bearer token errors to user-facing auth copy', async () => {
test('embedded puzzle form maps raw bearer token errors to user-facing auth copy', async () => {
const user = userEvent.setup();
vi.mocked(createPuzzleAgentSession).mockRejectedValueOnce(
@@ -2792,36 +3143,35 @@ test('new puzzle creation entry maps raw bearer token errors to user-facing auth
render(<TestWrapper withAuth />);
await openCreationHub(user);
const puzzleButton = screen.getByRole('button', {
name: /.*/u,
});
await openCreateTemplateHub(user);
const generateButton = screen.getByRole('button', { name: /稿/u });
expect((puzzleButton as HTMLButtonElement).disabled).toBe(false);
await user.click(puzzleButton);
expect((generateButton as HTMLButtonElement).disabled).toBe(false);
await user.click(generateButton);
expect(listPuzzleWorks).toHaveBeenCalled();
expect(createPuzzleAgentSession).toHaveBeenCalledTimes(1);
expect(createCreativeAgentSession).not.toHaveBeenCalled();
expect(
await within(getPlatformTabPanel('create')).findByText(
await screen.findByText(
'当前登录状态已失效,请重新登录后继续。',
),
).toBeTruthy();
expect(screen.queryByText('缺少 Authorization Bearer Token')).toBeNull();
});
test('hidden big fish creation entry does not render in platform create hub', async () => {
test('create tab does not render legacy gameplay creation entries', async () => {
const user = userEvent.setup();
render(<TestWrapper withAuth />);
await openCreationHub(user);
await openCreateTemplateHub(user);
expect(screen.queryByText('选择创作类型')).toBeNull();
expect(screen.queryByRole('button', { name: //u })).toBeNull();
expect(createBigFishCreationSession).not.toHaveBeenCalled();
});
test('puzzle creation timeout exits busy state and shows a readable error', async () => {
test('embedded puzzle form timeout exits busy state and shows a readable error', async () => {
const user = userEvent.setup();
vi.mocked(createPuzzleAgentSession).mockRejectedValueOnce(
@@ -2832,9 +3182,9 @@ test('puzzle creation timeout exits busy state and shows a readable error', asyn
render(<TestWrapper withAuth />);
await openCreationHub(user);
await openCreateTemplateHub(user);
const button = screen.getByRole('button', { name: /.*/u });
const button = screen.getByRole('button', { name: /稿/u });
await user.click(button);
await waitFor(() => {
@@ -2845,12 +3195,12 @@ test('puzzle creation timeout exits busy state and shows a readable error', asyn
).toBeGreaterThan(0);
});
expect(button as HTMLButtonElement).toHaveProperty('disabled', false);
expect(screen.queryByText(//u)).toBeNull();
expect(createCreativeAgentSession).not.toHaveBeenCalled();
expect(streamCreativeAgentMessage).not.toHaveBeenCalled();
});
test('visible match3d creation card opens workspace even when public galleries fail', async () => {
test('hidden match3d creation card stays closed even when public galleries fail', async () => {
const user = userEvent.setup();
const match3dSession = buildMockMatch3DAgentSession();
vi.mocked(listRpgEntryWorldGallery).mockRejectedValueOnce(
new Error('读取作品广场失败'),
@@ -2858,23 +3208,16 @@ test('visible match3d creation card opens workspace even when public galleries f
vi.mocked(listMatch3DGallery).mockRejectedValueOnce(
new Error('读取抓大鹅广场失败'),
);
vi.mocked(match3dCreationClient.createSession).mockResolvedValueOnce({
session: match3dSession,
});
render(<TestWrapper withAuth />);
await openCreationHub(user);
await openCreateTemplateHub(user);
expect(screen.queryByText('读取作品广场失败')).toBeNull();
expect(screen.queryByText('读取抓大鹅广场失败')).toBeNull();
await user.click(
screen.getByRole('button', { name: /.*/u }),
);
await waitFor(() => {
expect(match3dCreationClient.createSession).toHaveBeenCalledTimes(1);
});
expect(await screen.findByText('抓大鹅工作区match3d-agent-session-1')).toBeTruthy();
expect(
screen.queryByRole('tab', { name: /.*/u }),
).toBeNull();
expect(match3dCreationClient.createSession).not.toHaveBeenCalled();
});
test('puzzle draft result back button returns to creation hub', async () => {
@@ -2905,7 +3248,7 @@ test('puzzle draft result back button returns to creation hub', async () => {
render(<TestWrapper withAuth />);
await openCreationHub(user);
await openDraftHub(user);
expect(await screen.findByRole('button', { name: //u })).toBeTruthy();
await user.click(await screen.findByRole('button', { name: //u }));
@@ -2919,11 +3262,9 @@ test('puzzle draft result back button returns to creation hub', async () => {
await user.click(screen.getByRole('button', { name: '返回' }));
expect(
await screen.findByRole('button', { name: /.*/u }),
await screen.findByText('10分钟创作一个精品互动玩法'),
).toBeTruthy();
expect(
screen.queryByText('雨夜里有一只会发光的猫站在遗迹台阶上。'),
).toBeNull();
expect(screen.getByText('雨夜里有一只会发光的猫站在遗迹台阶上。')).toBeTruthy();
expect(screen.queryByText('拼图结果页')).toBeNull();
});
@@ -2955,7 +3296,7 @@ test('published puzzle work card restores its source session for editing', async
render(<TestWrapper withAuth />);
await openCreationHub(user);
await openDraftHub(user);
expect(await screen.findByRole('button', { name: //u })).toBeTruthy();
await user.click(await screen.findByRole('button', { name: //u }));
@@ -3089,6 +3430,7 @@ test('formal puzzle runtime uses frontend move merge logic and backend leaderboa
item: puzzleWork,
});
render(<TestWrapper withAuth />);
await openDiscoverHub(user);
const searchInput = await screen.findByPlaceholderText(
'搜索作品号、名称、作者、描述',
@@ -3274,6 +3616,7 @@ test('formal puzzle similar work keeps current run level progression', async ()
item: profileId === similarWork.profileId ? similarWork : entryWork,
}));
render(<TestWrapper withAuth />);
await openDiscoverHub(user);
const searchInput = await screen.findByPlaceholderText(
'搜索作品号、名称、作者、描述',
@@ -3375,6 +3718,7 @@ test('first puzzle runtime back click can open remix result page', async () => {
});
render(<TestWrapper withAuth />);
await openDiscoverHub(user);
const searchInput = await screen.findByPlaceholderText(
'搜索作品号、名称、作者、描述',
@@ -3428,6 +3772,7 @@ test('public code search opens a published puzzle by PZ code', async () => {
});
render(<TestWrapper withAuth />);
await openDiscoverHub(user);
const searchInput = await screen.findByPlaceholderText(
'搜索作品号、名称、作者、描述',
@@ -3471,6 +3816,7 @@ test('public code search opens a published big fish work by BF code', async () =
});
render(<TestWrapper withAuth />);
await openDiscoverHub(user);
const searchInput = await screen.findByPlaceholderText(
'搜索作品号、名称、作者、描述',
@@ -3523,6 +3869,7 @@ test('public code search opens a published Match3D work by M3 code and starts ru
});
render(<TestWrapper withAuth />);
await openDiscoverHub(user);
const searchInput = await screen.findByPlaceholderText(
'搜索作品号、名称、作者、描述',
@@ -3672,7 +4019,7 @@ test('failed draft work continues on generation progress view instead of agent w
render(<TestWrapper withAuth />);
await openCreationHub(user);
await openDraftHub(user);
expect(await screen.findByText('失败中的潮雾列岛')).toBeTruthy();
await user.click(await screen.findByRole('button', { name: //u }));
@@ -4193,7 +4540,7 @@ test('agent result view does not keep legacy publish blockers when preview uses
render(<TestWrapper withAuth />);
await openCreationHub(user);
await openDraftHub(user);
expect(await screen.findByRole('button', { name: //u })).toBeTruthy();
await user.click(await screen.findByRole('button', { name: //u }));
@@ -4370,9 +4717,7 @@ test('agent draft result back button returns to creation hub without syncing res
await user.click(screen.getByRole('button', { name: //u }));
await waitFor(() => {
expect(
screen.getByRole('button', { name: /.*/u }),
).toBeTruthy();
expect(screen.getByText('10分钟创作一个精品互动玩法')).toBeTruthy();
});
expect(
@@ -4625,7 +4970,9 @@ test('agent draft result can open from server result preview without embedded le
);
});
test('authenticated users with save archives default into the saves tab', async () => {
test('authenticated users can open save archives from the profile played panel', async () => {
const user = userEvent.setup();
vi.mocked(listProfileSaveArchives).mockResolvedValue([
{
worldKey: 'custom:world-1',
@@ -4642,15 +4989,18 @@ test('authenticated users with save archives default into the saves tab', async
render(<TestWrapper withAuth />);
expect((await screen.findAllByText('全部存档')).length).toBeGreaterThan(0);
await openProfilePlayedWorks(user);
expect((await screen.findAllByText('潮雾列岛')).length).toBeGreaterThan(0);
expect(screen.getAllByText('最近更新时间排序').length).toBeGreaterThan(0);
expect(screen.getAllByText('旧灯塔与失控航路').length).toBeGreaterThan(0);
expect(screen.queryByText('SAVE ARCHIVE')).toBeNull();
expect(screen.queryByText('ARCHIVE')).toBeNull();
expect(screen.queryByText('最近存档')).toBeNull();
});
test('puzzle save archive highlights work title and level subtitle', async () => {
const user = userEvent.setup();
vi.mocked(listProfileSaveArchives).mockResolvedValue([
{
worldKey: 'puzzle:puzzle-profile-1',
@@ -4667,6 +5017,8 @@ test('puzzle save archive highlights work title and level subtitle', async () =>
render(<TestWrapper withAuth />);
await openProfilePlayedWorks(user);
expect((await screen.findAllByText('雨夜猫塔')).length).toBeGreaterThan(0);
expect(screen.getAllByText('第 2 关 · 星桥机关').length).toBeGreaterThan(0);
expect(screen.queryByText('ARCHIVE')).toBeNull();
@@ -4689,16 +5041,16 @@ test('manual tab switch is preserved after platform bootstrap requests finish',
await clickFirstButtonByName(user, '创作');
expect(
await screen.findByRole('button', { name: /.*/u }),
await screen.findByText('10分钟创作一个精品互动玩法'),
).toBeTruthy();
resolveGalleryRequest([]);
await waitFor(() => {
expect(
within(getPlatformTabPanel('create')).getByRole('button', {
name: /.*/u,
}),
within(getPlatformTabPanel('create')).getByText(
'10分钟创作一个精品互动玩法',
),
).toBeTruthy();
});
@@ -4750,6 +5102,8 @@ test('save tab can resume a selected archive directly into the game', async () =
render(<TestWrapper withAuth onContinueGame={handleContinueGame} />);
await openProfilePlayedWorks(user);
await clickFirstAsyncButtonByName(user, //u);
await waitFor(() => {
@@ -4827,7 +5181,7 @@ test('creation hub published work can open detail view before deleting from deta
render(<TestWrapper withAuth />);
await openCreationHub(user);
await openDraftHub(user);
await user.click(await screen.findByRole('button', { name: //u }));
expect(await screen.findByText('详情')).toBeTruthy();
@@ -4902,7 +5256,7 @@ test('creation hub published work enters existing detail view', async () => {
render(<TestWrapper withAuth />);
await openCreationHub(user);
await openDraftHub(user);
await user.click(await screen.findByRole('button', { name: //u }));
expect(await screen.findByText('详情')).toBeTruthy();
@@ -4976,7 +5330,7 @@ test('creation hub published work experience button enters world directly', asyn
render(<TestWrapper withAuth onSelectWorld={handleCustomWorldSelect} />);
await openCreationHub(user);
await openDraftHub(user);
await user.click(await screen.findByRole('button', { name: //u }));
await user.click(await screen.findByRole('button', { name: '启动' }));
@@ -5060,7 +5414,7 @@ test('creation hub published work card keeps delete action guarded by detail flo
render(<TestWrapper withAuth />);
await clickFirstButtonByName(user, '创作');
await openDraftHub(user);
expect(await screen.findByRole('button', { name: //u })).toBeTruthy();
await user.click(screen.getByRole('button', { name: '删除' }));