Spaces:
Running
Running
| import { EditingForm, type EditingFormData } from './editing-form'; | |
| import { I18nProvider } from '@/lib/i18n'; | |
| import { | |
| IMAGE_UPSTREAM_PROFILES, | |
| type ImageUpstreamProfile, | |
| type PartialImagesCount | |
| } from '@/lib/image-upstream-profile'; | |
| import type { ImageStreamingStrategy } from '@/lib/image-upstream-strategy'; | |
| import { renderInClientDom } from '@/test-utils/react-dom'; | |
| import assert from 'node:assert/strict'; | |
| import { describe, it } from 'node:test'; | |
| import * as React from 'react'; | |
| import { renderToStaticMarkup } from 'react-dom/server'; | |
| type RenderOptions = { | |
| backend: EditingFormData['image_backend']; | |
| outputFormat?: EditingFormData['output_format']; | |
| advancedOpen?: boolean; | |
| advancedTab?: 'output' | 'model' | 'stream' | 'route'; | |
| reuseContext?: React.ComponentProps<typeof EditingForm>['reuseContext']; | |
| allowStreamingBatch?: boolean; | |
| enableParallelBatch?: boolean; | |
| editN?: number[]; | |
| streamingStrategy?: EditingFormData['streaming_strategy']; | |
| defaultStreamingStrategy?: ImageStreamingStrategy; | |
| allowResponsesImageBackend?: boolean; | |
| hasDefaultResponsesModel?: boolean; | |
| editResponsesModel?: string; | |
| editPrompt?: string; | |
| editModel?: React.ComponentProps<typeof EditingForm>['editModel']; | |
| editSize?: React.ComponentProps<typeof EditingForm>['editSize']; | |
| canApplyRandomInspiration?: boolean; | |
| imageFiles?: File[]; | |
| upstreamProfile?: ImageUpstreamProfile; | |
| upstreamProfileMixed?: boolean; | |
| isActive?: boolean; | |
| partialImages?: PartialImagesCount; | |
| streamMode?: React.ComponentProps<typeof EditingForm>['streamMode']; | |
| isLoading?: boolean; | |
| showLoadingState?: boolean; | |
| }; | |
| const noop = () => {}; | |
| function readSubmitFooterClass(html: string, submitLabel: string): string { | |
| const footerPattern = /<div(?=[^>]*data-slot="card-footer")(?=[^>]*class="([^"]*)")[^>]*>/g; | |
| const footers = [...html.matchAll(footerPattern)]; | |
| for (const [index, footer] of footers.entries()) { | |
| const start = footer.index; | |
| const end = footers[index + 1]?.index ?? html.length; | |
| const footerHtml = html.slice(start, end); | |
| if (footerHtml.includes(submitLabel)) return footer[1]; | |
| } | |
| assert.fail(`missing submit footer for ${submitLabel}`); | |
| } | |
| function assertSubmitFooterAvailable(html: string, submitLabel: string) { | |
| const classNames = readSubmitFooterClass(html, submitLabel).split(/\s+/); | |
| const hiddenClassName = classNames.find((className) => className === 'hidden' || className.endsWith(':hidden')); | |
| assert.equal(hiddenClassName, undefined); | |
| assert.equal(classNames.includes('flex'), true); | |
| assert.equal(classNames.includes('border-t'), true); | |
| } | |
| function assertButtonDisabled(html: string, label: string) { | |
| const button = html.match(/<button[^>]*>[\s\S]*?<\/button>/g)?.find((candidate) => candidate.includes(label)); | |
| assert.ok(button, `missing button for ${label}`); | |
| assert.match(button, /^<button[^>]*disabled=""/); | |
| } | |
| function createEditingFormProps({ | |
| backend, | |
| outputFormat = 'png', | |
| advancedOpen = true, | |
| advancedTab = 'route', | |
| reuseContext = null, | |
| allowStreamingBatch = false, | |
| enableParallelBatch = false, | |
| editN = [1], | |
| streamingStrategy = 'server-default', | |
| defaultStreamingStrategy = 'auto', | |
| allowResponsesImageBackend = true, | |
| hasDefaultResponsesModel = true, | |
| editResponsesModel = '', | |
| editPrompt = '', | |
| editModel = 'gpt-image-2', | |
| editSize = 'auto', | |
| canApplyRandomInspiration = true, | |
| imageFiles = [], | |
| upstreamProfile = IMAGE_UPSTREAM_PROFILES['openai-compatible'], | |
| upstreamProfileMixed = false, | |
| isActive = true, | |
| partialImages = 1, | |
| streamMode = 'auto', | |
| isLoading = false, | |
| showLoadingState = isLoading | |
| }: RenderOptions): React.ComponentProps<typeof EditingForm> { | |
| return { | |
| onSubmit: noop, | |
| onSaveInspiration: noop, | |
| canApplyRandomInspiration, | |
| onPickRandomInspiration: () => '用户保存的编辑提示词', | |
| isLoading, | |
| showLoadingState, | |
| isActive, | |
| currentMode: 'edit', | |
| onModeChange: noop, | |
| reuseContext, | |
| onClearReuseContext: noop, | |
| isPasswordRequiredByBackend: false, | |
| clientPasswordHash: null, | |
| onOpenPasswordDialog: noop, | |
| editModel, | |
| setEditModel: noop, | |
| imageFiles, | |
| sourceImagePreviewUrls: [], | |
| setImageFiles: noop, | |
| setSourceImagePreviewUrls: noop, | |
| maxImages: upstreamProfile.upload.maxImages, | |
| editPrompt, | |
| setEditPrompt: noop, | |
| editN, | |
| setEditN: noop, | |
| editSize, | |
| setEditSize: noop, | |
| editCustomWidth: 1024, | |
| setEditCustomWidth: noop, | |
| editCustomHeight: 1024, | |
| setEditCustomHeight: noop, | |
| editQuality: 'auto', | |
| setEditQuality: noop, | |
| editOutputFormat: outputFormat, | |
| setEditOutputFormat: noop, | |
| editCompression: [85], | |
| setEditCompression: noop, | |
| upstreamProfile, | |
| upstreamProfileMixed, | |
| editModeration: 'auto', | |
| setEditModeration: noop, | |
| editBrushSize: [20], | |
| setEditBrushSize: noop, | |
| editShowMaskEditor: false, | |
| setEditShowMaskEditor: noop, | |
| editGeneratedMaskFile: null, | |
| setEditGeneratedMaskFile: noop, | |
| editIsMaskSaved: false, | |
| setEditIsMaskSaved: noop, | |
| editOriginalImageSize: null, | |
| setEditOriginalImageSize: noop, | |
| editDrawnPoints: [], | |
| setEditDrawnPoints: noop, | |
| editMaskPreviewUrl: null, | |
| setEditMaskPreviewUrl: noop, | |
| streamMode, | |
| setStreamMode: noop, | |
| allowStreamingBatch, | |
| enableParallelBatch, | |
| setEnableParallelBatch: noop, | |
| partialImages, | |
| setPartialImages: noop, | |
| allowResponsesImageBackend, | |
| hasDefaultResponsesModel, | |
| editImageBackend: backend, | |
| setEditImageBackend: noop, | |
| editStreamingStrategy: streamingStrategy, | |
| defaultStreamingStrategy, | |
| setEditStreamingStrategy: noop, | |
| editResponsesModel, | |
| setEditResponsesModel: noop, | |
| editThinking: 'server-default', | |
| setEditThinking: noop, | |
| editPromptOptimization: 'server-default', | |
| setEditPromptOptimization: noop, | |
| editForceWeb: false, | |
| setEditForceWeb: noop, | |
| requestSummaryLabel: '请求 1 张图片', | |
| initialAdvancedOpen: advancedOpen, | |
| initialAdvancedTab: advancedTab | |
| }; | |
| } | |
| function renderEditingForm(options: RenderOptions): string { | |
| return renderToStaticMarkup( | |
| <I18nProvider> | |
| <EditingForm {...createEditingFormProps(options)} /> | |
| </I18nProvider> | |
| ); | |
| } | |
| describe('EditingForm submit footer', { concurrency: false }, () => { | |
| it('keeps the submit footer available outside desktop breakpoints', () => { | |
| const html = renderEditingForm({ | |
| backend: 'server-default', | |
| editPrompt: '用户真实编辑要求', | |
| imageFiles: [new File(['x'], 'source.png', { type: 'image/png' })] | |
| }); | |
| assertSubmitFooterAvailable(html, '编辑图像'); | |
| assert.match(html, /grid grid-cols-\[minmax\(0,1fr\)_minmax\(0,1fr\)\] gap-1\.5 text-xs/); | |
| assert.match(html, /min-w-0 items-center justify-center rounded-md/); | |
| assert.doesNotMatch(html, /text-center whitespace-nowrap/); | |
| assert.doesNotMatch(html, /min-\[1760px\]:flex/); | |
| assert.match(html, /收藏提示词/); | |
| assert.match(html, /随机套用/); | |
| assert.match(html, /min-h-11 min-w-0 items-center justify-center/); | |
| assert.match(html, /focus-visible:ring-2 focus-visible:outline-none/); | |
| assert.match(html, /flex min-h-0 flex-1 flex-col overflow-hidden/); | |
| assert.match(html, /min-h-0 flex-1 space-y-3 overflow-y-auto p-4 pb-4/); | |
| assert.match(html, /border-border bg-card flex shrink-0 border-t p-4/); | |
| }); | |
| it('keeps submit copy while another busy action disables the form', () => { | |
| const html = renderEditingForm({ | |
| backend: 'server-default', | |
| isLoading: true, | |
| showLoadingState: false | |
| }); | |
| assert.match(html, /编辑图像/); | |
| assert.doesNotMatch(html, /编辑中\.\.\./); | |
| }); | |
| it('disables edit inspiration actions when the prompt or saved inspirations are unavailable', () => { | |
| const html = renderEditingForm({ | |
| backend: 'server-default', | |
| editPrompt: '', | |
| canApplyRandomInspiration: false | |
| }); | |
| assertButtonDisabled(html, '收藏提示词'); | |
| assertButtonDisabled(html, '随机套用'); | |
| assert.doesNotMatch(html, /用户保存的编辑提示词/); | |
| }); | |
| it('runs edit inspiration actions through the rendered footer controls', async () => { | |
| const savedPrompts: string[] = []; | |
| const appliedPrompts: string[] = []; | |
| const props = createEditingFormProps({ | |
| backend: 'server-default', | |
| editPrompt: ' 当前编辑灵感 ' | |
| }); | |
| props.onSaveInspiration = (prompt) => savedPrompts.push(prompt); | |
| props.onPickRandomInspiration = () => ' 随机编辑灵感 '; | |
| props.setEditPrompt = (nextPrompt) => { | |
| appliedPrompts.push(typeof nextPrompt === 'function' ? nextPrompt('') : nextPrompt); | |
| }; | |
| const view = await renderInClientDom( | |
| <I18nProvider> | |
| <EditingForm {...props} /> | |
| </I18nProvider> | |
| ); | |
| try { | |
| const saveButton = [...view.container.querySelectorAll('button')].find((button) => | |
| button.textContent?.includes('收藏提示词') | |
| ); | |
| const randomButton = [...view.container.querySelectorAll('button')].find((button) => | |
| button.textContent?.includes('随机套用') | |
| ); | |
| assert.ok(saveButton, 'missing edit save inspiration button'); | |
| assert.ok(randomButton, 'missing edit random inspiration button'); | |
| await view.click(saveButton); | |
| await view.click(randomButton); | |
| assert.deepEqual(savedPrompts, [' 当前编辑灵感 ']); | |
| assert.deepEqual(appliedPrompts, ['随机编辑灵感']); | |
| } finally { | |
| await view.cleanup(); | |
| } | |
| }); | |
| it('only applies edit profile corrections after the form becomes active', async () => { | |
| const countCorrections: number[][] = []; | |
| const partialImageCorrections: number[] = []; | |
| const streamModeCorrections: string[] = []; | |
| const sizeCorrections: string[] = []; | |
| const props = createEditingFormProps({ | |
| backend: 'server-default', | |
| isActive: false, | |
| editN: [0], | |
| editModel: 'gpt-image-1', | |
| editSize: 'custom', | |
| partialImages: 0, | |
| streamMode: 'auto', | |
| streamingStrategy: 'off' | |
| }); | |
| props.setEditN = (nextCount) => { | |
| countCorrections.push(typeof nextCount === 'function' ? nextCount([]) : nextCount); | |
| }; | |
| props.setPartialImages = (nextPartialImages) => { | |
| partialImageCorrections.push( | |
| typeof nextPartialImages === 'function' ? nextPartialImages(1) : nextPartialImages | |
| ); | |
| }; | |
| props.setStreamMode = (nextStreamMode) => { | |
| streamModeCorrections.push(typeof nextStreamMode === 'function' ? nextStreamMode('auto') : nextStreamMode); | |
| }; | |
| props.setEditSize = (nextSize) => { | |
| sizeCorrections.push(typeof nextSize === 'function' ? nextSize('custom') : nextSize); | |
| }; | |
| const view = await renderInClientDom( | |
| <I18nProvider> | |
| <EditingForm {...props} /> | |
| </I18nProvider> | |
| ); | |
| try { | |
| assert.deepEqual(countCorrections, []); | |
| assert.deepEqual(partialImageCorrections, []); | |
| assert.deepEqual(streamModeCorrections, []); | |
| assert.deepEqual(sizeCorrections, []); | |
| await view.render( | |
| <I18nProvider> | |
| <EditingForm {...props} isActive /> | |
| </I18nProvider> | |
| ); | |
| assert.deepEqual(countCorrections, [[1]]); | |
| assert.deepEqual(partialImageCorrections, [1]); | |
| assert.deepEqual(streamModeCorrections, ['non_stream']); | |
| assert.deepEqual(sizeCorrections, ['auto']); | |
| } finally { | |
| await view.cleanup(); | |
| } | |
| }); | |
| }); | |
| describe('EditingForm advanced upstream controls', () => { | |
| it('keeps translated preset labels inside responsive control cells', () => { | |
| const html = renderEditingForm({ backend: 'server-default' }); | |
| assert.match(html, /radio-group-item-content\]\]:min-w-0/); | |
| assert.match(html, /radio-group-item-content\]\]:overflow-hidden/); | |
| assert.match(html, /max-w-full min-w-0 text-center leading-4 break-words whitespace-normal/); | |
| assert.doesNotMatch(html, /radio-group-item-content\]\]:overflow-visible/); | |
| assert.doesNotMatch(html, /2xl:grid-cols-5/); | |
| assert.doesNotMatch(html, /lg:hidden 2xl:block/); | |
| }); | |
| it('keeps the full professional accordion available on desktop and mobile', () => { | |
| const html = renderEditingForm({ backend: 'server-default', advancedTab: 'route' }); | |
| assert.match( | |
| html, | |
| /<div class="border-border bg-muted\/20 rounded-md border"><button[^>]*aria-controls="editing-advanced-panel"/ | |
| ); | |
| assert.doesNotMatch( | |
| html, | |
| /<div class="[^"]*lg:hidden[^"]*"><button[^>]*aria-controls="editing-advanced-panel"/ | |
| ); | |
| }); | |
| it('keeps model and streaming controls out of the default edit form surface', () => { | |
| const html = renderEditingForm({ backend: 'server-default', advancedOpen: false }); | |
| assert.match(html, /参考图/); | |
| assert.match(html, /修改想法/); | |
| assert.match(html, /专业模式/); | |
| assert.doesNotMatch(html, /block truncate text-xs font-normal/); | |
| assert.doesNotMatch(html, /edit-model-select/); | |
| assert.doesNotMatch(html, /edit-stream-mode-select/); | |
| }); | |
| it('translates the default backend into a user-facing route label near submit', () => { | |
| const html = renderEditingForm({ backend: 'server-default', advancedOpen: false }); | |
| assert.match(html, /默认线路/); | |
| assert.match(html, /请求 1 张图片/); | |
| }); | |
| it('renders edit model controls only in the professional model tab', () => { | |
| const html = renderEditingForm({ backend: 'server-default', advancedTab: 'model' }); | |
| assert.match(html, /edit-model-select/); | |
| assert.match(html, /gpt-image-2 始终以高保真方式处理参考图/); | |
| assert.doesNotMatch(html, /edit-image-backend-select/); | |
| }); | |
| it('renders edit stream controls only in the professional stream tab', () => { | |
| const html = renderEditingForm({ backend: 'server-default', advancedTab: 'stream' }); | |
| assert.match(html, /edit-stream-mode-select/); | |
| assert.match(html, /edit-partial-1/); | |
| assert.doesNotMatch(html, /edit-model-select/); | |
| }); | |
| it('hides Matsca-only edit stream controls for the default OpenAI-compatible profile', () => { | |
| const html = renderEditingForm({ backend: 'server-default', advancedTab: 'stream' }); | |
| assert.doesNotMatch(html, /edit-partial-0/); | |
| assert.match(html, /edit-partial-1/); | |
| assert.match(html, /edit-partial-3/); | |
| assert.doesNotMatch(html, /edit-partial-4/); | |
| }); | |
| it('renders Matsca edit stream controls when the active upstream profile allows them', () => { | |
| const html = renderEditingForm({ | |
| backend: 'server-default', | |
| advancedTab: 'stream', | |
| upstreamProfile: IMAGE_UPSTREAM_PROFILES.matsca | |
| }); | |
| assert.match(html, /edit-partial-0/); | |
| assert.match(html, /edit-partial-4/); | |
| }); | |
| it('intersects Matsca edit partial image options with the Responses backend contract', () => { | |
| const html = renderEditingForm({ | |
| backend: 'responses-image-generation', | |
| advancedTab: 'stream', | |
| upstreamProfile: IMAGE_UPSTREAM_PROFILES.matsca | |
| }); | |
| assert.doesNotMatch(html, /edit-partial-0/); | |
| assert.match(html, /edit-partial-1/); | |
| assert.match(html, /edit-partial-3/); | |
| assert.doesNotMatch(html, /edit-partial-4/); | |
| }); | |
| it('uses Matsca edit upload limits when the active upstream profile requires them', () => { | |
| const imageFiles = Array.from( | |
| { length: 9 }, | |
| (_, index) => new File(['x'], `source-${index}.png`, { type: 'image/png' }) | |
| ); | |
| const html = renderEditingForm({ | |
| backend: 'server-default', | |
| advancedOpen: false, | |
| editPrompt: '用户真实编辑要求', | |
| imageFiles, | |
| upstreamProfile: IMAGE_UPSTREAM_PROFILES.matsca | |
| }); | |
| assert.match(html, /最多 8 张/); | |
| assert.match(html, /9 \/ 8 张/); | |
| assert.match(html, /最多只能选择 8 张图片。/); | |
| assert.match(html, /<button[^>]*disabled=""[^>]*>[\s\S]*编辑图像[\s\S]*<\/button>/); | |
| }); | |
| it('renders profile-aware high resolution edit size presets', () => { | |
| const openAiHtml = renderEditingForm({ backend: 'server-default' }); | |
| const matscaHtml = renderEditingForm({ | |
| backend: 'server-default', | |
| upstreamProfile: IMAGE_UPSTREAM_PROFILES.matsca | |
| }); | |
| assert.match(openAiHtml, /id="edit-size-wide-4k"/); | |
| assert.doesNotMatch(openAiHtml, /id="edit-size-square-4k"/); | |
| assert.match(matscaHtml, /id="edit-size-square-4k"/); | |
| }); | |
| it('renders an explicit parallel batch toggle in edit stream settings', () => { | |
| const html = renderEditingForm({ | |
| backend: 'server-default', | |
| advancedTab: 'stream', | |
| allowStreamingBatch: true, | |
| enableParallelBatch: true, | |
| editN: [2] | |
| }); | |
| assert.match(html, /并发批量/); | |
| assert.match(html, /多张图或多条提示词会按当前渠道容量并发执行/); | |
| assert.match(html, /id="edit-parallel-batch-enabled"/); | |
| assert.match(html, /aria-checked="true"/); | |
| }); | |
| it('keeps edit parallel batch disabled for a single output image', () => { | |
| const html = renderEditingForm({ | |
| backend: 'server-default', | |
| advancedTab: 'stream', | |
| allowStreamingBatch: true, | |
| enableParallelBatch: true | |
| }); | |
| assert.match(html, /选择至少 2 张图片或 2 条提示词后可启用并发/); | |
| assert.match(html, /id="edit-parallel-batch-enabled"/); | |
| assert.match(html, /aria-checked="false"/); | |
| assert.match(html, /disabled=""/); | |
| }); | |
| it('keeps edit parallel batch disabled when streaming strategy is off', () => { | |
| const html = renderEditingForm({ | |
| backend: 'server-default', | |
| advancedTab: 'stream', | |
| allowStreamingBatch: true, | |
| enableParallelBatch: true, | |
| editN: [2], | |
| streamingStrategy: 'off' | |
| }); | |
| assert.match(html, /并发批量需要流式模式;非流式会保持顺序执行。/); | |
| assert.match(html, /id="edit-parallel-batch-enabled"/); | |
| assert.match(html, /aria-checked="false"/); | |
| assert.match(html, /disabled=""/); | |
| }); | |
| it('keeps edit parallel batch disabled when the server default streaming strategy is off', () => { | |
| const html = renderEditingForm({ | |
| backend: 'server-default', | |
| advancedTab: 'stream', | |
| allowStreamingBatch: true, | |
| enableParallelBatch: true, | |
| editN: [2], | |
| defaultStreamingStrategy: 'off' | |
| }); | |
| assert.match(html, /并发批量需要流式模式;非流式会保持顺序执行。/); | |
| assert.match(html, /id="edit-parallel-batch-enabled"/); | |
| assert.match(html, /aria-checked="false"/); | |
| assert.match(html, /disabled=""/); | |
| }); | |
| it('disables the edit stream mode selector when the server default streaming strategy is off', () => { | |
| const html = renderEditingForm({ | |
| backend: 'server-default', | |
| advancedTab: 'stream', | |
| defaultStreamingStrategy: 'off' | |
| }); | |
| assert.match( | |
| html, | |
| /<button[^>]*(?:disabled=""[^>]*id="edit-stream-mode-select"|id="edit-stream-mode-select"[^>]*disabled="")/ | |
| ); | |
| }); | |
| it('renders Responses-specific edit controls when the Responses backend is selected', () => { | |
| const html = renderEditingForm({ backend: 'responses-image-generation', upstreamProfileMixed: true }); | |
| assert.match(html, /图片生成后端/); | |
| assert.match(html, /影响说明/); | |
| assert.match(html, /Responses image_generation 需要实验开关和顶层模型/); | |
| assert.match(html, /当前服务端渠道包含不同上游模式/); | |
| assert.match(html, /自动或服务端默认会优先使用当前推荐的流式策略/); | |
| assert.match(html, /GPT 顶层模型/); | |
| assert.match(html, /思考强度/); | |
| assert.match(html, /提示词优化/); | |
| assert.doesNotMatch(html, /优先 Web 账号/); | |
| }); | |
| it('explains the resolved edit server default streaming strategy', () => { | |
| const offHtml = renderEditingForm({ | |
| backend: 'server-default', | |
| advancedTab: 'route', | |
| streamingStrategy: 'server-default', | |
| defaultStreamingStrategy: 'off' | |
| }); | |
| const forceHtml = renderEditingForm({ | |
| backend: 'server-default', | |
| advancedTab: 'route', | |
| streamingStrategy: 'server-default', | |
| defaultStreamingStrategy: 'force-sse' | |
| }); | |
| assert.match(offHtml, /关闭流式会减少长连接不稳定因素/); | |
| assert.doesNotMatch(offHtml, /自动或服务端默认会优先使用当前推荐的流式策略/); | |
| assert.match(forceHtml, /强制 SSE 会跳过自动判断/); | |
| assert.doesNotMatch(forceHtml, /自动或服务端默认会优先使用当前推荐的流式策略/); | |
| }); | |
| it('disables the experimental Responses backend when runtime capabilities do not allow it', () => { | |
| const html = renderEditingForm({ | |
| backend: 'server-default', | |
| allowResponsesImageBackend: false | |
| }); | |
| assert.match(html, /当前运行时或默认服务器渠道未开放 Responses image_generation/); | |
| assert.doesNotMatch(html, /GPT 顶层模型/); | |
| }); | |
| it('blocks Responses edits until a top-level model is available', () => { | |
| const html = renderEditingForm({ | |
| backend: 'responses-image-generation', | |
| hasDefaultResponsesModel: false, | |
| editResponsesModel: '', | |
| editPrompt: '用户真实编辑要求', | |
| imageFiles: [new File(['x'], 'source.png', { type: 'image/png' })] | |
| }); | |
| assert.match(html, /Responses image_generation 需要填写 GPT 顶层模型/); | |
| assert.match(html, /<button[^>]*disabled=""[^>]*>[\s\S]*编辑图像[\s\S]*<\/button>/); | |
| }); | |
| it('renders Images API edit controls and compression when JPEG output is selected', () => { | |
| const html = renderEditingForm({ backend: 'images-api', outputFormat: 'jpeg', advancedTab: 'output' }); | |
| assert.match(html, /Images API/); | |
| assert.match(html, /输出格式/); | |
| assert.match(html, /压缩:85%/); | |
| assert.match(html, /内容审核级别/); | |
| assert.doesNotMatch(html, /GPT 顶层模型/); | |
| }); | |
| }); | |
| describe('EditingForm reused history context', () => { | |
| it('shows which history values were carried into edit mode', () => { | |
| const html = renderEditingForm({ | |
| backend: 'server-default', | |
| reuseContext: { | |
| sourceLabel: '最近生成:2026/6/2 12:00:00', | |
| restoredFields: ['参考图', '提示词', '模型', '尺寸', '数量'], | |
| promptPreview: '用户真实编辑提示词' | |
| } | |
| }); | |
| assert.match(html, /已带入内容/); | |
| assert.match(html, /最近生成:2026\/6\/2 12:00:00/); | |
| assert.match(html, /参考图/); | |
| assert.match(html, /提示词/); | |
| assert.match(html, /模型/); | |
| assert.match(html, /尺寸/); | |
| assert.match(html, /数量/); | |
| assert.match(html, /用户真实编辑提示词/); | |
| assert.match(html, /这些内容已经写入编辑单,可以修改后再生成。/); | |
| }); | |
| }); | |