补齐场景必填字段就近错误提示
Project CI / Repository checks (pull_request) Successful in 53s
Project CI / Backend tests (pull_request) Successful in 3m46s
Project CI / Frontend tests (pull_request) Successful in 2m26s
Project CI / Native shell tests (pull_request) Successful in 12m4s

将场景内容与自定义画风校验错误关联到对应输入框
补充字段错误样式、无障碍状态与提交交互回归测试
This commit is contained in:
2026-08-05 18:49:54 +08:00
parent 68fab3b6ce
commit d8838429d0
4 changed files with 200 additions and 3 deletions
@@ -21,9 +21,16 @@ import type {
} from './ImageCanvasEditorTypes';
import { ImageCanvasGenerationImageOptionsView } from './ImageCanvasGenerationImageOptionsView';
import { calculateEditorImageGenerationPrice } from './ImageCanvasGenerationModel';
import {
EDITOR_SCENE_CONTENT_REQUIRED_ERROR,
EDITOR_SCENE_CUSTOM_STYLE_REQUIRED_ERROR,
} from './ImageCanvasGenerationSubmissionModel';
import { ImageCanvasReferenceSlot } from './ImageCanvasReferenceSlot';
import { useImageCanvasFloatingOptionDismiss } from './useImageCanvasFloatingOptionDismiss';
const EDITOR_SCENE_CONTENT_ERROR_ID = 'editor-scene-content-error';
const EDITOR_SCENE_CUSTOM_STYLE_ERROR_ID = 'editor-scene-custom-style-error';
type ReferenceLabelFormatter = (
reference: CharacterReferenceImage,
index: number,
@@ -173,6 +180,22 @@ export function ImageCanvasBasicGenerationComposerView({
const finalFooterClassName =
footerClassName ?? 'image-canvas-editor__generation-composer-footer';
const sceneContentError =
dialog.mode === 'scene' &&
dialog.status === 'failed' &&
dialog.errorMessage === EDITOR_SCENE_CONTENT_REQUIRED_ERROR
? dialog.errorMessage
: null;
const sceneCustomStyleError =
dialog.mode === 'scene' &&
dialog.status === 'failed' &&
dialog.errorMessage === EDITOR_SCENE_CUSTOM_STYLE_REQUIRED_ERROR
? dialog.errorMessage
: null;
const hasSceneFieldError = Boolean(
sceneContentError || sceneCustomStyleError,
);
useImageCanvasFloatingOptionDismiss({
isOpen: isGenerationReferenceMenuOpen,
boundaryRefs: [generationReferenceButtonRef],
@@ -252,6 +275,10 @@ export function ImageCanvasBasicGenerationComposerView({
) : null}
<AutoGrowTextArea
aria-label={resolvedPromptLabel}
aria-invalid={sceneContentError ? true : undefined}
aria-describedby={
sceneContentError ? EDITOR_SCENE_CONTENT_ERROR_ID : undefined
}
value={dialog.prompt}
disabled={dialog.status === 'generating'}
placeholder={resolvedPromptPlaceholder}
@@ -267,10 +294,25 @@ export function ImageCanvasBasicGenerationComposerView({
)
}
/>
{sceneContentError ? (
<p
id={EDITOR_SCENE_CONTENT_ERROR_ID}
className="image-canvas-editor__scene-field-error"
role="alert"
>
{sceneContentError}
</p>
) : null}
{dialog.mode === 'scene' &&
dialog.sceneStylePreset === CUSTOM_EDITOR_SCENE_STYLE_PRESET ? (
<AutoGrowTextArea
aria-label="自定义画风"
aria-invalid={sceneCustomStyleError ? true : undefined}
aria-describedby={
sceneCustomStyleError
? EDITOR_SCENE_CUSTOM_STYLE_ERROR_ID
: undefined
}
value={dialog.sceneCustomStyle ?? ''}
disabled={dialog.status === 'generating'}
placeholder="填写自定义画风,例如:90 年代复古像素风"
@@ -287,6 +329,15 @@ export function ImageCanvasBasicGenerationComposerView({
}
/>
) : null}
{sceneCustomStyleError ? (
<p
id={EDITOR_SCENE_CUSTOM_STYLE_ERROR_ID}
className="image-canvas-editor__scene-field-error"
role="alert"
>
{sceneCustomStyleError}
</p>
) : null}
<div className={finalFooterClassName}>
<ImageCanvasGenerationImageOptionsView
dialog={dialog}
@@ -320,7 +371,7 @@ export function ImageCanvasBasicGenerationComposerView({
{resolvedSubmittingStatusLabel}
</PlatformStatusMessage>
) : null}
{dialog.status === 'failed' ? (
{dialog.status === 'failed' && !hasSceneFieldError ? (
<PlatformStatusMessage
tone="error"
surface="platform"
@@ -12,6 +12,8 @@ import { describe, expect, it, vi } from 'vitest';
import type { GenerateDialogState } from './ImageCanvasEditorTypes';
import { ImageCanvasGenerationComposerView } from './ImageCanvasGenerationComposerView';
import { resolveImageGenerationErrorMessage } from './ImageCanvasGenerationModel';
import { buildImageGenerationSubmissionPlan } from './ImageCanvasGenerationSubmissionModel';
function mockStateSetter<T>() {
return vi.fn() as unknown as Dispatch<SetStateAction<T>>;
@@ -105,6 +107,41 @@ function createComposerProps(
};
}
function SceneValidationHarness({
initialDialog,
}: {
initialDialog: GenerateDialogState;
}) {
const [dialog, setDialog] = useState<GenerateDialogState | null>(
initialDialog,
);
if (!dialog) {
return null;
}
const props = createComposerProps(dialog, {
setGenerateDialog: setDialog,
onSubmitImageGeneration: (submittedDialog) => {
try {
buildImageGenerationSubmissionPlan({
dialog: submittedDialog,
layers: [],
nextGeneratedIndex: 1,
});
} catch (error) {
setDialog({
...submittedDialog,
status: 'failed',
composerOpen: true,
errorMessage: resolveImageGenerationErrorMessage(error),
});
}
},
});
return <ImageCanvasGenerationComposerView {...props} />;
}
function renderComposer(
generateDialog: GenerateDialogState,
overrides: Partial<
@@ -216,6 +253,92 @@ describe('ImageCanvasGenerationComposerView', () => {
expect(customStyle.className).not.toContain('platform-text-field');
});
it('提交空场景内容时在对应字段附近呈现关联错误', () => {
render(
<SceneValidationHarness
initialDialog={{
mode: 'scene',
prompt: '',
status: 'idle',
composerOpen: true,
generationReferences: [],
sceneStylePreset: 'anime',
sceneCustomStyle: '',
imageModel: 'gemini-3.1-flash-image-preview',
aspectRatio: '16:9',
imageSize: '1K',
}}
/>,
);
const panel = screen.getByRole('dialog', { name: '游戏场景生成器' });
const sceneContent = within(panel).getByRole('textbox', {
name: '画面内容',
});
fireEvent.click(within(panel).getByRole('button', { name: '生成' }));
const error = within(panel).getByRole('alert');
expect(error.textContent).toBe('请填写画面内容');
expect(error.className).toContain(
'image-canvas-editor__scene-field-error',
);
expect(sceneContent.getAttribute('aria-invalid')).toBe('true');
expect(sceneContent.getAttribute('aria-describedby')).toBe(error.id);
expect(
panel.querySelector('.image-canvas-editor__generate-status'),
).toBeNull();
fireEvent.change(sceneContent, { target: { value: '雨夜中的海边车站' } });
expect(within(panel).queryByRole('alert')).toBeNull();
expect(sceneContent.getAttribute('aria-invalid')).toBeNull();
expect(sceneContent.getAttribute('aria-describedby')).toBeNull();
});
it('提交空自定义画风时在自定义字段附近呈现关联错误', () => {
render(
<SceneValidationHarness
initialDialog={{
mode: 'scene',
prompt: '雨夜中的海边车站',
status: 'idle',
composerOpen: true,
generationReferences: [],
sceneStylePreset: 'custom',
sceneCustomStyle: '',
imageModel: 'gemini-3.1-flash-image-preview',
aspectRatio: '16:9',
imageSize: '1K',
}}
/>,
);
const panel = screen.getByRole('dialog', { name: '游戏场景生成器' });
const customStyle = within(panel).getByRole('textbox', {
name: '自定义画风',
});
fireEvent.click(within(panel).getByRole('button', { name: '生成' }));
const error = within(panel).getByRole('alert');
expect(error.textContent).toBe('请填写自定义画风');
expect(error.className).toContain(
'image-canvas-editor__scene-field-error',
);
expect(customStyle.getAttribute('aria-invalid')).toBe('true');
expect(customStyle.getAttribute('aria-describedby')).toBe(error.id);
expect(
panel.querySelector('.image-canvas-editor__generate-status'),
).toBeNull();
fireEvent.change(customStyle, { target: { value: '90 年代复古像素风' } });
expect(within(panel).queryByRole('alert')).toBeNull();
expect(customStyle.getAttribute('aria-invalid')).toBeNull();
expect(customStyle.getAttribute('aria-describedby')).toBeNull();
});
it('让生成UI设计图面板复用普通图片生成面板的纵向结构', () => {
const setGenerateDialog = vi.fn();
renderComposer({
@@ -63,6 +63,9 @@ type ImageGenerationSubmissionOptions = {
nextGeneratedIndex: number;
};
export const EDITOR_SCENE_CONTENT_REQUIRED_ERROR = '请填写画面内容';
export const EDITOR_SCENE_CUSTOM_STYLE_REQUIRED_ERROR = '请填写自定义画风';
export const EDITOR_GENERATED_ASSET_LABEL_MAX_CHARS = 80;
export function resolveGenerationAssetLabel(
@@ -323,13 +326,13 @@ export function buildImageGenerationSubmissionPlan({
if (dialog.mode === 'scene') {
const sceneContent = dialog.prompt.trim();
if (!sceneContent) {
throw new Error('请填写画面内容');
throw new Error(EDITOR_SCENE_CONTENT_REQUIRED_ERROR);
}
const stylePreset =
dialog.sceneStylePreset ?? DEFAULT_EDITOR_SCENE_STYLE_PRESET;
const customStyle = dialog.sceneCustomStyle?.trim() ?? '';
if (stylePreset === CUSTOM_EDITOR_SCENE_STYLE_PRESET && !customStyle) {
throw new Error('请填写自定义画风');
throw new Error(EDITOR_SCENE_CUSTOM_STYLE_REQUIRED_ERROR);
}
const references = dialog.generationReferences ?? [];
const imageModel = normalizeEditorImageModel(dialog.imageModel);
+20
View File
@@ -6771,6 +6771,26 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
color: #94a3b8;
}
.image-canvas-editor__generation-prompt[aria-invalid='true'] {
border-color: #e11d48;
background: #fff7f8;
}
.image-canvas-editor__generation-prompt[aria-invalid='true']:focus {
border-color: #e11d48;
box-shadow: 0 0 0 3px rgba(225, 29, 72, 0.14);
}
.image-canvas-editor__scene-field-error {
grid-column: 1 / -1;
margin: 0;
padding: 0.02rem 0.82rem 0.12rem;
color: #be123c;
font-size: 0.74rem;
font-weight: 760;
line-height: 1.35;
}
.image-canvas-editor__video-prompt {
grid-column: 1 / -1;
min-height: 4.1rem;