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:
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user