Increase VectorEngine timeouts and add image UI
Add VectorEngine image generation config and raise request timeouts (env + scripts) from 180000 to 1000000ms. Introduce a reusable CreativeImageInputPanel component with tests and wire up mobile keyboard-focus helpers; update generation views and related tests (CustomWorldGenerationView, BarkBattle editor, Match3D, Puzzle flows). Improve API error handling / VectorEngine request guidance (packages/shared http.ts and docs), and apply multiple backend/frontend fixes for puzzle/match3d/prompt handling. Also include extensive docs and decision-log updates describing UI/UX decisions and verification steps.
This commit is contained in:
@@ -7,6 +7,10 @@ import { afterEach, 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 {
|
||||
BabyObjectMatchDraft,
|
||||
CreateBabyObjectMatchDraftRequest,
|
||||
} from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
import type {
|
||||
CustomWorldAgentSessionSnapshot,
|
||||
CustomWorldWorkSummary,
|
||||
@@ -37,6 +41,14 @@ import {
|
||||
} from '../../routing/appPageRoutes';
|
||||
import { ApiClientError } from '../../services/apiClient';
|
||||
import type { AuthUser } from '../../services/authService';
|
||||
import {
|
||||
createBabyObjectMatchDraft,
|
||||
deleteLocalBabyObjectMatchDraft,
|
||||
listLocalBabyObjectMatchDrafts,
|
||||
publishBabyObjectMatchWork,
|
||||
regenerateBabyObjectMatchDraftAssets,
|
||||
saveBabyObjectMatchDraft,
|
||||
} from '../../services/edutainment-baby-object';
|
||||
import {
|
||||
createBigFishCreationSession,
|
||||
getBigFishCreationSession,
|
||||
@@ -48,6 +60,10 @@ import {
|
||||
submitBigFishInput,
|
||||
} from '../../services/big-fish-runtime';
|
||||
import { listBigFishWorks } from '../../services/big-fish-works';
|
||||
import {
|
||||
createBarkBattleDraft,
|
||||
publishBarkBattleWork,
|
||||
} from '../../services/bark-battle-creation';
|
||||
import {
|
||||
type CreationEntryConfig,
|
||||
fetchCreationEntryConfig,
|
||||
@@ -193,9 +209,9 @@ async function openDraftHub(user: ReturnType<typeof userEvent.setup>) {
|
||||
async function expectDraftHubGeneratingBadgeCountAtLeast(count: number) {
|
||||
const panel = getPlatformTabPanel('saves');
|
||||
await waitFor(() => {
|
||||
expect(within(panel).getAllByText('生成中').length).toBeGreaterThanOrEqual(
|
||||
count,
|
||||
);
|
||||
expect(
|
||||
within(panel).getAllByLabelText('生成中').length,
|
||||
).toBeGreaterThanOrEqual(count);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -277,6 +293,17 @@ const testCreationEntryConfig = {
|
||||
sortOrder: 40,
|
||||
updatedAtMicros: 1,
|
||||
},
|
||||
{
|
||||
id: 'bark-battle',
|
||||
title: '汪汪声浪',
|
||||
subtitle: '声控狗狗对战',
|
||||
badge: '可创建',
|
||||
imageSrc: '/creation-type-references/bark-battle.webp',
|
||||
visible: true,
|
||||
open: true,
|
||||
sortOrder: 45,
|
||||
updatedAtMicros: 1,
|
||||
},
|
||||
{
|
||||
id: 'square-hole',
|
||||
title: '方洞挑战',
|
||||
@@ -446,6 +473,21 @@ vi.mock('../../services/big-fish-runtime', () => ({
|
||||
submitBigFishInput: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/bark-battle-creation', () => ({
|
||||
createBarkBattleDraft: vi.fn(),
|
||||
publishBarkBattleWork: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/edutainment-baby-object', () => ({
|
||||
createBabyObjectMatchDraft: vi.fn(),
|
||||
deleteLocalBabyObjectMatchDraft: vi.fn(),
|
||||
hasBabyObjectMatchPlaceholderAssets: vi.fn(() => false),
|
||||
listLocalBabyObjectMatchDrafts: vi.fn(),
|
||||
publishBabyObjectMatchWork: vi.fn(),
|
||||
regenerateBabyObjectMatchDraftAssets: vi.fn(),
|
||||
saveBabyObjectMatchDraft: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/match3d-creation', () => ({
|
||||
match3dCreationClient: {
|
||||
createSession: vi.fn(),
|
||||
@@ -806,10 +848,12 @@ vi.mock('../match3d-creation/Match3DAgentWorkspace', () => ({
|
||||
Match3DAgentWorkspace: ({
|
||||
session,
|
||||
isBusy,
|
||||
error,
|
||||
onCreateFromForm,
|
||||
}: {
|
||||
session: { sessionId: string; messages: Array<{ text: string }> } | null;
|
||||
isBusy?: boolean;
|
||||
error?: string | null;
|
||||
onCreateFromForm?: (payload: {
|
||||
seedText: string;
|
||||
themeText: string;
|
||||
@@ -827,6 +871,7 @@ vi.mock('../match3d-creation/Match3DAgentWorkspace', () => ({
|
||||
<div data-testid="match3d-workspace-busy-state">
|
||||
{isBusy ? 'busy' : 'idle'}
|
||||
</div>
|
||||
{error ? <div>{error}</div> : null}
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
@@ -938,6 +983,125 @@ vi.mock('../match3d-runtime/Match3DRuntimeShell', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../bark-battle-creation/BarkBattleConfigEditor', () => ({
|
||||
BarkBattleConfigEditor: ({
|
||||
error,
|
||||
isBusy,
|
||||
showBackButton,
|
||||
title,
|
||||
onPublish,
|
||||
}: {
|
||||
error?: string | null;
|
||||
isBusy?: boolean;
|
||||
showBackButton?: boolean;
|
||||
title?: string | null;
|
||||
onPublish: (payload: {
|
||||
title: string;
|
||||
description: string;
|
||||
themePreset: string;
|
||||
playerDogSkinPreset: string;
|
||||
opponentDogSkinPreset: string;
|
||||
difficultyPreset: 'normal';
|
||||
leaderboardEnabled: boolean;
|
||||
}) => void;
|
||||
}) => (
|
||||
<div className="bark-battle-config-editor-mock">
|
||||
<div>汪汪声浪配置表单</div>
|
||||
<div data-testid="bark-battle-editor-back-state">
|
||||
{showBackButton ? 'back-visible' : 'back-hidden'}
|
||||
</div>
|
||||
<div data-testid="bark-battle-editor-title-state">
|
||||
{title === null ? 'title-hidden' : title}
|
||||
</div>
|
||||
<div data-testid="bark-battle-editor-busy-state">
|
||||
{isBusy ? 'busy' : 'idle'}
|
||||
</div>
|
||||
{error ? <div>{error}</div> : null}
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => {
|
||||
onPublish({
|
||||
title: '汪汪测试杯',
|
||||
description: '',
|
||||
themePreset: 'sunny-yard',
|
||||
playerDogSkinPreset: 'corgi',
|
||||
opponentDogSkinPreset: 'husky',
|
||||
difficultyPreset: 'normal',
|
||||
leaderboardEnabled: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
发布并试玩
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../edutainment-result/BabyObjectMatchResultView', () => ({
|
||||
BabyObjectMatchResultView: ({
|
||||
draft,
|
||||
onBack,
|
||||
onStartTestRun,
|
||||
}: {
|
||||
draft: BabyObjectMatchDraft;
|
||||
onBack: () => void;
|
||||
onStartTestRun?: (draft: BabyObjectMatchDraft) => void;
|
||||
}) => (
|
||||
<div className="baby-object-match-result-view-mock">
|
||||
<div>宝贝识物结果页</div>
|
||||
<div>{draft.workTitle}</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onStartTestRun?.(draft);
|
||||
}}
|
||||
>
|
||||
试玩
|
||||
</button>
|
||||
<button type="button" onClick={onBack}>
|
||||
返回
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../edutainment-runtime/BabyObjectMatchRuntimeShell', () => ({
|
||||
BabyObjectMatchRuntimeShell: ({
|
||||
draft,
|
||||
onBack,
|
||||
}: {
|
||||
draft: BabyObjectMatchDraft;
|
||||
onBack?: () => void;
|
||||
}) => (
|
||||
<div className="baby-object-match-runtime-shell-mock">
|
||||
<div>宝贝识物运行态:{draft.profileId}</div>
|
||||
<button type="button" onClick={onBack}>
|
||||
返回
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../../games/bark-battle/ui/BarkBattleRuntimeShell', () => ({
|
||||
BarkBattleRuntimeShell: ({
|
||||
title,
|
||||
workId,
|
||||
onExit,
|
||||
}: {
|
||||
title?: string;
|
||||
workId?: string;
|
||||
onExit?: () => void;
|
||||
}) => (
|
||||
<div className="bark-battle-runtime-shell-mock">
|
||||
<div>汪汪声浪运行态:{title ?? '未命名'} / {workId ?? 'missing-work'}</div>
|
||||
<button type="button" onClick={onExit}>
|
||||
返回配置
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../custom-world-agent/CustomWorldAgentWorkspace', () => ({
|
||||
CustomWorldAgentWorkspace: ({
|
||||
session,
|
||||
@@ -1070,6 +1234,48 @@ function buildMockCreativeAgentSession(
|
||||
};
|
||||
}
|
||||
|
||||
function buildMockBabyObjectMatchDraft(
|
||||
overrides: Partial<BabyObjectMatchDraft> = {},
|
||||
): BabyObjectMatchDraft {
|
||||
const itemNames = overrides.itemNames ?? ['苹果', '香蕉'];
|
||||
const now = '2026-05-14T10:00:00.000Z';
|
||||
|
||||
return {
|
||||
draftId: 'baby-object-draft-red-dot',
|
||||
profileId: 'baby-object-profile-red-dot',
|
||||
templateId: 'baby-object-match',
|
||||
templateName: '宝贝识物',
|
||||
workTitle: '宝贝识物红点草稿',
|
||||
workDescription: `${itemNames[0]}和${itemNames[1]}识物分类`,
|
||||
itemNames,
|
||||
itemAssets: [
|
||||
{
|
||||
itemId: 'baby-object-item-a',
|
||||
itemName: itemNames[0],
|
||||
imageSrc: '/baby-object/apple.png',
|
||||
assetObjectId: null,
|
||||
generationProvider: 'vector-engine-gpt-image-2',
|
||||
prompt: itemNames[0],
|
||||
},
|
||||
{
|
||||
itemId: 'baby-object-item-b',
|
||||
itemName: itemNames[1],
|
||||
imageSrc: '/baby-object/banana.png',
|
||||
assetObjectId: null,
|
||||
generationProvider: 'vector-engine-gpt-image-2',
|
||||
prompt: itemNames[1],
|
||||
},
|
||||
],
|
||||
visualPackage: null,
|
||||
themeTags: ['寓教于乐', '宝贝识物'],
|
||||
publicationStatus: 'draft',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
publishedAt: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function buildMockSquareHoleAgentSession(
|
||||
overrides: Partial<
|
||||
Parameters<typeof buildMockSquareHoleAgentSessionImpl>[0]
|
||||
@@ -1908,7 +2114,7 @@ beforeEach(() => {
|
||||
testCreationEntryConfig,
|
||||
);
|
||||
vi.mocked(getProfileDashboard).mockResolvedValue({
|
||||
walletBalance: 0,
|
||||
walletBalance: 20,
|
||||
totalPlayTimeMs: 0,
|
||||
playedWorldCount: 0,
|
||||
updatedAt: '2026-04-16T12:00:00.000Z',
|
||||
@@ -2006,6 +2212,22 @@ beforeEach(() => {
|
||||
vi.mocked(deleteRpgEntryWorldProfile).mockResolvedValue([]);
|
||||
vi.mocked(listVisualNovelGallery).mockResolvedValue({ works: [] });
|
||||
vi.mocked(listVisualNovelWorks).mockResolvedValue({ works: [] });
|
||||
vi.mocked(listLocalBabyObjectMatchDrafts).mockResolvedValue([]);
|
||||
vi.mocked(deleteLocalBabyObjectMatchDraft).mockResolvedValue([]);
|
||||
vi.mocked(saveBabyObjectMatchDraft).mockImplementation(async (payload) => ({
|
||||
draft: payload.draft,
|
||||
}));
|
||||
vi.mocked(regenerateBabyObjectMatchDraftAssets).mockImplementation(
|
||||
async (draft) => ({ draft }),
|
||||
);
|
||||
vi.mocked(publishBabyObjectMatchWork).mockImplementation(async (payload) => ({
|
||||
draft: {
|
||||
...payload.draft,
|
||||
publicationStatus: 'published',
|
||||
publishedAt: '2026-05-14T10:10:00.000Z',
|
||||
},
|
||||
publicWorkCode: `BO-${payload.draft.profileId}`,
|
||||
}));
|
||||
vi.mocked(recordBigFishPlay).mockResolvedValue({ items: [] });
|
||||
vi.mocked(recordRpgEntryWorldGalleryPlay).mockImplementation(
|
||||
async (ownerUserId, profileId) => ({
|
||||
@@ -2502,6 +2724,36 @@ beforeEach(() => {
|
||||
},
|
||||
}));
|
||||
vi.mocked(recordBigFishPlay).mockResolvedValue({ items: [] });
|
||||
vi.mocked(createBarkBattleDraft).mockResolvedValue({
|
||||
draftId: 'bark-battle-draft-1',
|
||||
workId: 'bark-battle-work-1',
|
||||
title: '汪汪测试杯',
|
||||
description: '',
|
||||
themePreset: 'sunny-yard',
|
||||
playerDogSkinPreset: 'corgi',
|
||||
opponentDogSkinPreset: 'husky',
|
||||
difficultyPreset: 'normal',
|
||||
leaderboardEnabled: true,
|
||||
configVersion: 1,
|
||||
rulesetVersion: 'bark-battle-ruleset-v1',
|
||||
updatedAt: '2026-05-14T10:00:00.000Z',
|
||||
});
|
||||
vi.mocked(publishBarkBattleWork).mockResolvedValue({
|
||||
workId: 'bark-battle-work-1',
|
||||
draftId: 'bark-battle-draft-1',
|
||||
configVersion: 1,
|
||||
rulesetVersion: 'bark-battle-ruleset-v1',
|
||||
playTypeId: 'bark-battle',
|
||||
title: '汪汪测试杯',
|
||||
description: '',
|
||||
themePreset: 'sunny-yard',
|
||||
playerDogSkinPreset: 'corgi',
|
||||
opponentDogSkinPreset: 'husky',
|
||||
difficultyPreset: 'normal',
|
||||
leaderboardEnabled: true,
|
||||
updatedAt: '2026-05-14T10:00:00.000Z',
|
||||
publishedAt: '2026-05-14T10:00:00.000Z',
|
||||
});
|
||||
vi.mocked(match3dCreationClient.createSession).mockResolvedValue({
|
||||
session: buildMockMatch3DAgentSession(),
|
||||
});
|
||||
@@ -2885,6 +3137,9 @@ test('create tab shows template tabs and embeds puzzle form by default', async (
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '抓大鹅' }).querySelector('img')?.src,
|
||||
).toContain('/creation-type-references/match3d.webp');
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '汪汪声浪' }).querySelector('img')?.src,
|
||||
).toContain('/creation-type-references/bark-battle.webp');
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '宝贝识物' }).querySelector('img')?.src,
|
||||
).toContain('/child-motion-demo/picture-book-grass-stage.png');
|
||||
@@ -2900,6 +3155,7 @@ test('create tab shows template tabs and embeds puzzle form by default', async (
|
||||
expect(screen.queryByRole('tab', { name: /方洞挑战/u })).toBeNull();
|
||||
expect(screen.queryByRole('tab', { name: '视觉小说' })).toBeNull();
|
||||
expect(screen.getByRole('tab', { name: /抓大鹅/u })).toBeTruthy();
|
||||
expect(screen.getByRole('tab', { name: /汪汪声浪/u })).toBeTruthy();
|
||||
expect(screen.getByRole('tab', { name: /宝贝识物/u })).toBeTruthy();
|
||||
expect(createRpgCreationSession).not.toHaveBeenCalled();
|
||||
expect(match3dCreationClient.createSession).not.toHaveBeenCalled();
|
||||
@@ -2922,6 +3178,57 @@ test('create tab switches match3d into the embedded entry form', async () => {
|
||||
expect(match3dCreationClient.createSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('create tab switches bark battle into the embedded config form', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '汪汪声浪' }));
|
||||
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '汪汪声浪' }).getAttribute('aria-selected'),
|
||||
).toBe('true');
|
||||
expect(await screen.findByText('汪汪声浪配置表单')).toBeTruthy();
|
||||
expect(screen.getByTestId('bark-battle-editor-back-state').textContent).toBe(
|
||||
'back-hidden',
|
||||
);
|
||||
expect(screen.getByTestId('bark-battle-editor-title-state').textContent).toBe(
|
||||
'title-hidden',
|
||||
);
|
||||
expect(screen.queryByText('汪汪声浪运行态')).toBeNull();
|
||||
expect(createBarkBattleDraft).not.toHaveBeenCalled();
|
||||
expect(publishBarkBattleWork).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('bark battle publish preview returns to the embedded config form', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '汪汪声浪' }));
|
||||
await user.click(await screen.findByRole('button', { name: '发布并试玩' }));
|
||||
|
||||
expect(createBarkBattleDraft).toHaveBeenCalledWith({
|
||||
title: '汪汪测试杯',
|
||||
description: '',
|
||||
themePreset: 'sunny-yard',
|
||||
playerDogSkinPreset: 'corgi',
|
||||
opponentDogSkinPreset: 'husky',
|
||||
difficultyPreset: 'normal',
|
||||
leaderboardEnabled: true,
|
||||
});
|
||||
expect(await screen.findByText(/汪汪声浪运行态:汪汪测试杯/u)).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '返回配置' }));
|
||||
|
||||
expect(await screen.findByText('汪汪声浪配置表单')).toBeTruthy();
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '汪汪声浪' }).getAttribute('aria-selected'),
|
||||
).toBe('true');
|
||||
});
|
||||
|
||||
test('running match3d form generation can return to draft tab and reopen progress', async () => {
|
||||
const user = userEvent.setup();
|
||||
const runningSession = buildMockMatch3DAgentSession({
|
||||
@@ -3394,6 +3701,116 @@ test('running puzzle form generation creates a new puzzle draft on same template
|
||||
});
|
||||
});
|
||||
|
||||
test('running puzzle draft opens generation progress from draft tab', async () => {
|
||||
const user = userEvent.setup();
|
||||
const runningSession = buildMockPuzzleAgentSession({
|
||||
sessionId: 'puzzle-running-session',
|
||||
draft: null,
|
||||
stage: 'collecting_anchors',
|
||||
progressPercent: 20,
|
||||
});
|
||||
let resolveCompile!: (value: {
|
||||
operation: {
|
||||
operationId: string;
|
||||
type: 'compile_puzzle_draft';
|
||||
status: 'completed';
|
||||
phaseLabel: string;
|
||||
phaseDetail: string;
|
||||
progress: number;
|
||||
};
|
||||
session: PuzzleAgentSessionSnapshot;
|
||||
}) => void;
|
||||
vi.mocked(createPuzzleAgentSession).mockResolvedValueOnce({
|
||||
session: runningSession,
|
||||
});
|
||||
vi.mocked(executePuzzleAgentAction).mockReturnValueOnce(
|
||||
new Promise((resolve) => {
|
||||
resolveCompile = resolve;
|
||||
}),
|
||||
);
|
||||
vi.mocked(getPuzzleAgentSession).mockResolvedValue({
|
||||
session: runningSession,
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(await screen.findByRole('button', { name: '生成草稿' }));
|
||||
expect(await screen.findByText('拼图草稿生成进度')).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '返回创作中心' }));
|
||||
await openDraftHub(user);
|
||||
await expectDraftHubGeneratingBadgeCountAtLeast(1);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /继续创作《拼图草稿》/u }),
|
||||
);
|
||||
|
||||
expect(await screen.findByText('拼图草稿生成进度')).toBeTruthy();
|
||||
expect(screen.queryByText('拼图结果页')).toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
resolveCompile({
|
||||
operation: {
|
||||
operationId: 'compile-puzzle-running',
|
||||
type: 'compile_puzzle_draft',
|
||||
status: 'completed',
|
||||
phaseLabel: '已完成',
|
||||
phaseDetail: '草稿已生成',
|
||||
progress: 1,
|
||||
},
|
||||
session: buildMockPuzzleAgentSession({
|
||||
sessionId: 'puzzle-running-session',
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('puzzle form checks mud points before creating a draft', async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(getProfileDashboard).mockResolvedValue({
|
||||
walletBalance: 1,
|
||||
totalPlayTimeMs: 0,
|
||||
playedWorldCount: 0,
|
||||
updatedAt: '2026-05-14T10:00:00.000Z',
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(await screen.findByRole('button', { name: '生成草稿' }));
|
||||
|
||||
expect(
|
||||
await screen.findByText('泥点不足,本次需要 2 泥点,当前 1 泥点。'),
|
||||
).toBeTruthy();
|
||||
expect(createPuzzleAgentSession).not.toHaveBeenCalled();
|
||||
expect(executePuzzleAgentAction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('match3d form checks mud points before creating a draft', async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(getProfileDashboard).mockResolvedValue({
|
||||
walletBalance: 9,
|
||||
totalPlayTimeMs: 0,
|
||||
playedWorldCount: 0,
|
||||
updatedAt: '2026-05-14T10:00:00.000Z',
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '抓大鹅' }));
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: '生成抓大鹅草稿' }),
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findByText('泥点不足,本次需要 10 泥点,当前 9 泥点。'),
|
||||
).toBeTruthy();
|
||||
expect(match3dCreationClient.createSession).not.toHaveBeenCalled();
|
||||
expect(match3dCreationClient.executeAction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('match3d result trial passes generated models into first runtime mount', async () => {
|
||||
const user = userEvent.setup();
|
||||
const generatedItemAssets: Match3DWorkSummary['generatedItemAssets'] = [
|
||||
@@ -3857,6 +4274,113 @@ test('completed match3d draft notice first opens trial then reopens result', asy
|
||||
expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('completed baby object match draft viewed immediately does not keep unread marker', async () => {
|
||||
const user = userEvent.setup();
|
||||
const generatedDraft = buildMockBabyObjectMatchDraft();
|
||||
vi.mocked(createBabyObjectMatchDraft).mockImplementation(
|
||||
async (payload: CreateBabyObjectMatchDraftRequest) => ({
|
||||
draft: buildMockBabyObjectMatchDraft({
|
||||
itemNames: [payload.itemAName, payload.itemBName],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
vi.mocked(listLocalBabyObjectMatchDrafts).mockResolvedValue([generatedDraft]);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '宝贝识物' }));
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '宝贝识物' }).getAttribute(
|
||||
'aria-selected',
|
||||
),
|
||||
).toBe('true');
|
||||
});
|
||||
await user.type(await screen.findByLabelText('物品 A'), '苹果');
|
||||
await user.type(await screen.findByLabelText('物品 B'), '香蕉');
|
||||
await user.click(screen.getByRole('button', { name: '生成宝贝识物草稿' }));
|
||||
|
||||
expect(await screen.findByText('宝贝识物结果页')).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '返回' }));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('宝贝识物结果页')).toBeNull();
|
||||
});
|
||||
expect(await screen.findByLabelText('物品 A')).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '返回' }));
|
||||
|
||||
await openDraftHub(user);
|
||||
expect(
|
||||
await screen.findByRole('button', {
|
||||
name: /继续创作《宝贝识物红点草稿》/u,
|
||||
}),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByLabelText('新生成完成')).toBeNull();
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', {
|
||||
name: /继续创作《宝贝识物红点草稿》/u,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(await screen.findByText('宝贝识物结果页')).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '返回' }));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('宝贝识物结果页')).toBeNull();
|
||||
});
|
||||
await user.click(await screen.findByRole('button', { name: '返回' }));
|
||||
await openDraftHub(user);
|
||||
expect(screen.queryByLabelText('新生成完成')).toBeNull();
|
||||
});
|
||||
|
||||
test('completed baby object match draft shows unread marker after leaving generation page', async () => {
|
||||
const user = userEvent.setup();
|
||||
const generatedDraft = buildMockBabyObjectMatchDraft();
|
||||
let resolveCreateDraft!: (value: { draft: BabyObjectMatchDraft }) => void;
|
||||
vi.mocked(createBabyObjectMatchDraft).mockReturnValue(
|
||||
new Promise((resolve) => {
|
||||
resolveCreateDraft = resolve;
|
||||
}),
|
||||
);
|
||||
vi.mocked(listLocalBabyObjectMatchDrafts).mockResolvedValue([generatedDraft]);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '宝贝识物' }));
|
||||
await user.type(await screen.findByLabelText('物品 A'), '苹果');
|
||||
await user.type(await screen.findByLabelText('物品 B'), '香蕉');
|
||||
await user.click(screen.getByRole('button', { name: '生成宝贝识物草稿' }));
|
||||
|
||||
expect(await screen.findByText('宝贝识物草稿生成进度')).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '返回创作中心' }));
|
||||
await openDraftHub(user);
|
||||
|
||||
await act(async () => {
|
||||
resolveCreateDraft({ draft: generatedDraft });
|
||||
});
|
||||
|
||||
expect(await screen.findByLabelText('新生成完成')).toBeTruthy();
|
||||
expect(
|
||||
await screen.findByRole('button', { name: '草稿,有新草稿' }),
|
||||
).toBeTruthy();
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('button', {
|
||||
name: /继续创作《宝贝识物红点草稿》/u,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(await screen.findByText('宝贝识物结果页')).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '返回' }));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('宝贝识物结果页')).toBeNull();
|
||||
});
|
||||
await user.click(await screen.findByRole('button', { name: '返回' }));
|
||||
await openDraftHub(user);
|
||||
expect(screen.queryByLabelText('新生成完成')).toBeNull();
|
||||
});
|
||||
|
||||
test('puzzle draft generation auto starts trial and runtime back opens draft result', async () => {
|
||||
const user = userEvent.setup();
|
||||
const generatedDraft: PuzzleResultDraft = {
|
||||
@@ -7943,7 +8467,7 @@ test('creation hub published work experience button enters world directly', asyn
|
||||
expect(handleCustomWorldSelect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('creation hub published work card keeps delete action guarded by detail flow', async () => {
|
||||
test('creation hub published work card reveals delete action after card action reveal', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const publishedWork = {
|
||||
@@ -8014,7 +8538,13 @@ test('creation hub published work card keeps delete action guarded by detail flo
|
||||
|
||||
await openDraftHub(user);
|
||||
|
||||
expect(await screen.findByRole('button', { name: /查看详情/u })).toBeTruthy();
|
||||
const publishedCard = await screen.findByRole('button', {
|
||||
name: /查看详情《潮雾列岛》/u,
|
||||
});
|
||||
publishedCard.focus();
|
||||
await user.keyboard('{ArrowLeft}');
|
||||
|
||||
expect(screen.getByRole('button', { name: '删除' })).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '删除' }));
|
||||
|
||||
const dialog = await screen.findByRole('dialog', { name: '删除作品' });
|
||||
|
||||
@@ -949,10 +949,7 @@ test('profile recharge modal buys points through mock channel outside mini progr
|
||||
const onRechargeSuccess = vi.fn();
|
||||
|
||||
renderProfileView(onRechargeSuccess);
|
||||
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
|
||||
await user.click(
|
||||
within(shortcutRegion).getByRole('button', { name: /充值/u }),
|
||||
);
|
||||
await user.click(screen.getByRole('button', { name: /充值\s*泥点\/会员/u }));
|
||||
|
||||
expect(await screen.findByText('账户充值')).toBeTruthy();
|
||||
expect(mockGetRpgProfileRechargeCenter).toHaveBeenCalledTimes(1);
|
||||
@@ -1022,10 +1019,7 @@ test('profile recharge modal posts requestPayment params in mini program web-vie
|
||||
});
|
||||
|
||||
renderProfileView();
|
||||
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
|
||||
await user.click(
|
||||
within(shortcutRegion).getByRole('button', { name: /充值/u }),
|
||||
);
|
||||
await user.click(screen.getByRole('button', { name: /充值\s*泥点\/会员/u }));
|
||||
await user.click(await screen.findByRole('button', { name: /60泥点/u }));
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -1302,6 +1296,9 @@ test('profile page shows legal entries and ICP record link', async () => {
|
||||
expect(
|
||||
within(shortcutRegion).getByRole('button', { name: /每日任务/u }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(shortcutRegion).queryByRole('button', { name: /充值/u }),
|
||||
).toBeNull();
|
||||
expect(
|
||||
within(shortcutRegion).getByRole('button', { name: /邀请好友/u }),
|
||||
).toBeTruthy();
|
||||
|
||||
@@ -5196,12 +5196,6 @@ export function RpgEntryHomeView({
|
||||
icon={Star}
|
||||
onClick={openTaskCenterPanel}
|
||||
/>
|
||||
<ProfileShortcutButton
|
||||
label="充值"
|
||||
subLabel="泥点/会员"
|
||||
icon={Coins}
|
||||
onClick={openRechargeModal}
|
||||
/>
|
||||
<ProfileShortcutButton
|
||||
label="兑换码"
|
||||
subLabel="福利奖励"
|
||||
|
||||
Reference in New Issue
Block a user