1
This commit is contained in:
@@ -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: '删除' }));
|
||||
|
||||
Reference in New Issue
Block a user