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:
2026-05-15 02:40:59 +08:00
parent 4642855fd0
commit 74fd9a33ac
87 changed files with 5508 additions and 1261 deletions

View File

@@ -1,6 +1,12 @@
// @vitest-environment jsdom
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import {
fireEvent,
render,
screen,
waitFor,
within,
} from '@testing-library/react';
import { afterEach, describe, expect, test, vi } from 'vitest';
import type { Match3DWorkProfile } from '../../../packages/shared/src/contracts/match3dWorks';
@@ -147,6 +153,7 @@ describe('Match3DResultView', () => {
expect(screen.getByText('作品标签')).toBeTruthy();
expect(screen.getByText('水果')).toBeTruthy();
expect(screen.getByText('抓大鹅')).toBeTruthy();
expect(screen.queryByRole('button', { name: '封面图' })).toBeNull();
fireEvent.click(screen.getByRole('button', { name: '试玩' }));
@@ -188,7 +195,7 @@ describe('Match3DResultView', () => {
});
});
test('封面图独立面板支持引用物品素材作为多参考图生成', async () => {
test('发布面板支持引用物品素材作为多参考图生成封面', async () => {
const profile = createProfile({
generatedItemAssets: [
{
@@ -232,13 +239,19 @@ describe('Match3DResultView', () => {
/>,
);
fireEvent.click(screen.getByRole('button', { name: '封面图' }));
expect(screen.getByRole('dialog', { name: '封面图' })).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '引用草莓' }));
fireEvent.change(screen.getByLabelText('封面描述'), {
fireEvent.click(screen.getByRole('button', { name: '发布' }));
const publishDialog = screen.getByRole('dialog', {
name: '发布抓大鹅作品',
});
fireEvent.click(
within(publishDialog).getByRole('button', { name: '引用草莓' }),
);
fireEvent.change(within(publishDialog).getByLabelText('封面描述'), {
target: { value: '草莓抓大鹅封面图' },
});
fireEvent.click(screen.getByRole('button', { name: '生成封面图' }));
fireEvent.click(
within(publishDialog).getByRole('button', { name: '生成封面图' }),
);
await waitFor(() => {
expect(
@@ -251,7 +264,74 @@ describe('Match3DResultView', () => {
uploadedImageSrc: null,
});
expect(onSaved).toHaveBeenCalledWith(nextProfile);
expect(screen.queryByRole('dialog', { name: '封面图' })).toBeNull();
expect(
screen.getByRole('dialog', { name: '发布抓大鹅作品' }),
).toBeTruthy();
});
});
test('生成封面图只更新封面字段,不用旧回包覆盖当前物品素材和配置', async () => {
const generatedItemAssets = [createReadyGeneratedItemAsset(1)];
const profile = createProfile({
clearCount: 12,
difficulty: 4,
generatedItemAssets,
});
const staleResponseProfile = createProfile({
...profile,
coverImageSrc:
'/generated-match3d-assets/session/profile/cover/task/cover.png',
clearCount: 8,
difficulty: 2,
generatedItemAssets: [],
});
const onSaved = vi.fn();
vi.mocked(match3dWorksService.generateMatch3DCoverImage).mockResolvedValue({
item: staleResponseProfile,
coverImageSrc:
'/generated-match3d-assets/session/profile/cover/task/cover.png',
coverImageObjectKey:
'generated-match3d-assets/session/profile/cover/task/cover.png',
prompt: '水果封面图',
});
render(
<Match3DResultView
profile={profile}
onBack={() => {}}
onSaved={onSaved}
onStartTestRun={() => {}}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '发布' }));
const publishDialog = screen.getByRole('dialog', {
name: '发布抓大鹅作品',
});
fireEvent.change(within(publishDialog).getByLabelText('封面描述'), {
target: { value: '水果封面图' },
});
fireEvent.click(
within(publishDialog).getByRole('button', { name: '生成封面图' }),
);
await waitFor(() => {
expect(onSaved).toHaveBeenCalledWith(
expect.objectContaining({
coverImageSrc:
'/generated-match3d-assets/session/profile/cover/task/cover.png',
clearCount: 12,
difficulty: 4,
generatedItemAssets: expect.arrayContaining([
expect.objectContaining({
itemId: 'match3d-item-1',
imageViews: expect.arrayContaining([
expect.objectContaining({ viewId: 'view-01' }),
]),
}),
]),
}),
);
});
});
@@ -280,9 +360,14 @@ describe('Match3DResultView', () => {
/>,
);
fireEvent.click(screen.getByRole('button', { name: '封面图' }));
fireEvent.click(screen.getByRole('button', { name: '发布' }));
const publishDialog = screen.getByRole('dialog', {
name: '发布抓大鹅作品',
});
fireEvent.change(
screen.getByLabelText('上传封面图', { selector: 'input' }),
within(publishDialog).getByLabelText('上传封面图', {
selector: 'input',
}),
{
target: {
files: [new File(['x'], 'cover.png', { type: 'image/png' })],
@@ -291,16 +376,22 @@ describe('Match3DResultView', () => {
);
await waitFor(() => {
expect(screen.getByRole('switch', { name: 'AI重绘' })).toBeTruthy();
expect(screen.getByRole('button', { name: '移除封面图' })).toBeTruthy();
expect(screen.getByLabelText('AI重绘要求')).toBeTruthy();
expect(
within(publishDialog).getByRole('switch', { name: 'AI重绘' }),
).toBeTruthy();
expect(
within(publishDialog).getByRole('button', { name: '移除封面图' }),
).toBeTruthy();
expect(within(publishDialog).getByLabelText('AI重绘要求')).toBeTruthy();
});
expect(screen.queryByText('参考图')).toBeNull();
expect(within(publishDialog).queryByText('参考图')).toBeNull();
fireEvent.change(screen.getByLabelText('AI重绘要求'), {
fireEvent.change(within(publishDialog).getByLabelText('AI重绘要求'), {
target: { value: '保留构图,改成节日果园' },
});
fireEvent.click(screen.getByRole('button', { name: '生成封面图' }));
fireEvent.click(
within(publishDialog).getByRole('button', { name: '生成封面图' }),
);
await waitFor(() => {
expect(
@@ -418,9 +509,23 @@ describe('Match3DResultView', () => {
);
const publishButton = screen.getByRole('button', { name: '发布' });
expect(publishButton).toHaveProperty('disabled', true);
expect(publishButton).toHaveProperty('disabled', false);
fireEvent.click(publishButton);
const publishDialog = screen.getByRole('dialog', {
name: '发布抓大鹅作品',
});
expect(within(publishDialog).getByText('封面图不能为空。')).toBeTruthy();
expect(
within(publishDialog).getByText('标签数量需要在 3 到 6 个之间。'),
).toBeTruthy();
expect(
within(publishDialog).getByRole('button', { name: '发布到广场' }),
).toHaveProperty('disabled', true);
fireEvent.click(
within(publishDialog).getByRole('button', { name: '发布到广场' }),
);
expect(within(publishDialog).getByText('封面图不能为空。')).toBeTruthy();
expect(match3dWorksService.publishMatch3DWork).not.toHaveBeenCalled();
});
@@ -449,9 +554,21 @@ describe('Match3DResultView', () => {
);
const publishButton = screen.getByRole('button', { name: '发布' });
expect(publishButton).toHaveProperty('disabled', true);
expect(publishButton).toHaveProperty('disabled', false);
fireEvent.click(publishButton);
const publishDialog = screen.getByRole('dialog', {
name: '发布抓大鹅作品',
});
expect(
within(publishDialog).getByText(
'当前难度需要 3 种物品,已生成 2 种,请先在素材配置中补齐。',
),
).toBeTruthy();
expect(
within(publishDialog).getByRole('button', { name: '发布到广场' }),
).toHaveProperty('disabled', true);
expect(match3dWorksService.publishMatch3DWork).not.toHaveBeenCalled();
fireEvent.click(within(publishDialog).getByRole('button', { name: '取消' }));
fireEvent.click(screen.getByRole('button', { name: '难度配置' }));
expect(screen.getByText('已生成物品种类')).toBeTruthy();
expect(screen.getAllByText('2 种').length).toBeGreaterThan(0);
@@ -505,6 +622,11 @@ describe('Match3DResultView', () => {
);
fireEvent.click(screen.getByRole('button', { name: '发布' }));
fireEvent.click(
within(
screen.getByRole('dialog', { name: '发布抓大鹅作品' }),
).getByRole('button', { name: '发布到广场' }),
);
await waitFor(() => {
expect(
@@ -1062,7 +1184,7 @@ describe('Match3DResultView', () => {
).toBe(true);
});
test('物品详情五视角预览使用上方焦点区和底部缩略图栏', () => {
test('物品详情五视角预览使用上方大图和底部缩略图栏', () => {
render(
<Match3DResultView
profile={createProfile({
@@ -1079,17 +1201,29 @@ describe('Match3DResultView', () => {
const preview = screen.getByLabelText('物品1五视角预览');
const stage = screen.getByTestId('match3d-item-preview-stage');
const focusFrame = screen.getByTestId('match3d-item-preview-focus-frame');
const focusImage = screen.getByTestId('match3d-item-preview-focus-image');
const thumbnails = screen.getByTestId('match3d-item-preview-thumbnails');
expect(stage.className).toContain('aspect-square');
expect(focusFrame.className).toContain('inset-[7%]');
expect(stage.className).toContain('max-w-[22rem]');
expect(focusImage.className).toContain('place-items-center');
expect(focusImage.querySelector('img')?.className).toContain('p-3');
expect(thumbnails.style.gridAutoColumns).toBe('calc((100% - 1.5rem) / 4)');
expect(preview.querySelectorAll('img')).toHaveLength(10);
expect(preview.querySelectorAll('img')).toHaveLength(6);
expect(
screen
.getByRole('button', { name: '切换物品1视角3' })
.getAttribute('aria-pressed'),
).toBe('true');
expect(
screen.queryByTestId('match3d-item-preview-focus-frame'),
).toBeNull();
fireEvent.click(screen.getByRole('button', { name: '切换物品1视角5' }));
expect(
screen
.getByTestId('match3d-item-preview-focus-image')
.getAttribute('data-preview-src'),
).toContain('views/view-05.png');
});
test('草稿阶段仅有切割图片时展示 2D 素材', () => {
@@ -1216,6 +1350,11 @@ describe('Match3DResultView', () => {
fireEvent.click(screen.getByRole('button', { name: '预览UI页面' }));
expect(screen.getByRole('dialog', { name: 'UI预览' })).toBeTruthy();
expect(screen.getByText('1:30')).toBeTruthy();
expect(
document.querySelector(
'img[src="/match3d-background-references/pot-fused-reference.png"]',
),
).toBeTruthy();
});
test('素材配置 UI 子 Tab 修改提示词后调用背景图生成接口并刷新素材', async () => {