补齐场景必填字段就近错误提示
将场景内容与自定义画风校验错误关联到对应输入框 补充字段错误样式、无障碍状态与提交交互回归测试
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user