import { buildImageActionTarget, formatLogTime, ImageOutput } from './image-output'; import { I18nProvider } from '@/lib/i18n'; import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { renderToStaticMarkup } from 'react-dom/server'; const noop = () => {}; const buttonContentPattern = '[\\s\\S]*?'; function renderImageOutput(viewMode: 'grid' | number): string { return renderToStaticMarkup( ); } describe('ImageOutput result actions', () => { it('does not show fixed preview timing or dimensions before real image metadata is known', () => { const html = renderImageOutput(0); assert.match(html, /第 1 张 \/ 共 2 张/); assert.doesNotMatch(html, /预计 8-12 秒/); assert.doesNotMatch(html, /1024 x 768/); }); it('uses the localized generated-image alt text when a caller does not override it', () => { const html = renderImageOutput(0); assert.match(html, /alt="生成图片输出"/); assert.doesNotMatch(html, /Generated image output/); }); it('formats activity timestamps with the selected app locale and preserves invalid diagnostic values', () => { const timestamp = '2026-05-31T12:15:30.000Z'; assert.equal(formatLogTime(timestamp, 'zh-CN'), new Date(timestamp).toLocaleTimeString('zh-CN')); assert.equal(formatLogTime(timestamp, 'en-US'), new Date(timestamp).toLocaleTimeString('en-US')); assert.equal(formatLogTime('not-a-timestamp', 'zh-CN'), 'not-a-timestamp'); }); it('uses the no-image state in the preview header before generation', () => { const html = renderToStaticMarkup( ); assert.match(html, /还没有生成图像/); assert.match(html, /写下灵感后点击生成/); assert.match(html, /workbench-panel[^"]*h-fit xl:h-full[\s\S]*preview-gallery-board/); assert.match(html, /min-h-\[220px\] shrink-0 sm:min-h-\[248px\] lg:min-h-\[272px\] xl:min-h-0 xl:flex-1/); assert.doesNotMatch(html, /min-h-\[20rem\]/); assert.match(html, /max-w-\[30rem\]/); assert.match(html, /items-center justify-center/); assert.match(html, /text-center/); assert.doesNotMatch(html, /2xl:max-w-\[34rem\]/); assert.doesNotMatch(html, /photo-paper relative flex aspect-\[8\/5\]/); assert.doesNotMatch(html, /max-w-\[780px\] flex-col justify-between/); assert.doesNotMatch(html, /]*disabled=""[^>]*>${buttonContentPattern}${action}${buttonContentPattern}` ) ); } }); it('keeps the full preview stage height after images exist', () => { const html = renderImageOutput(0); assert.match(html, /workbench-panel[^"]*h-full[\s\S]*preview-gallery-board/); assert.match(html, /min-h-\[300px\] flex-1 sm:min-h-\[420px\] lg:min-h-\[520px\]/); assert.doesNotMatch(html, /min-h-\[220px\] shrink-0 sm:min-h-\[248px\] lg:min-h-\[272px\]/); }); it('uses an edit reference placeholder instead of the sample image in edit mode', () => { const html = renderToStaticMarkup( ); assert.match(html, /先放入参考图/); assert.match(html, /等待参考图/); assert.doesNotMatch(html, /min-h-\[18rem\]/); assert.match(html, /max-w-\[30rem\]/); assert.doesNotMatch(html, /photo-paper relative flex aspect-\[4\/3\]/); assert.doesNotMatch(html, /灵感样张/); }); it('shows the edit reference image before an edit result exists', () => { const html = renderToStaticMarkup( ); assert.match(html, /参考图已就绪/); assert.match(html, /图生图参考图预览/); assert.match(html, /参考图/); assert.doesNotMatch(html, /灵感样张/); }); it('requires an explicit selection before single-image actions in the multi-image grid view', () => { const html = renderImageOutput('grid'); assert.match(html, /aria-label="选择第 1 张图片"/); assert.match(html, /aria-label="选择第 2 张图片"/); assert.match(html, /请选择一张图片/); assert.doesNotMatch(html, /第 1 张 \/ 共 2 张/); for (const action of ['继续编辑', '对比', '下载', '分享']) { assert.match( html, new RegExp( `]*disabled=""[^>]*>${buttonContentPattern}${action}${buttonContentPattern}` ) ); } }); it('keeps single-image actions available after selecting a multi-image result', () => { const html = renderImageOutput(1); assert.match(html, /第 2 张 \/ 共 2 张/); for (const action of ['继续编辑', '对比', '下载', '分享']) { assert.match( html, new RegExp(`]*>${buttonContentPattern}${action}${buttonContentPattern}`) ); assert.doesNotMatch( html, new RegExp( `]*disabled=""[^>]*>${buttonContentPattern}${action}${buttonContentPattern}` ) ); } }); it('builds selected image action targets with storage mode', () => { assert.deepEqual( buildImageActionTarget({ path: 'data:image/png;base64,selected', filename: 'selected.png', storageMode: 'indexeddb' }), { filename: 'selected.png', storageMode: 'indexeddb' } ); assert.deepEqual( buildImageActionTarget({ path: '/api/image/fs.png', filename: 'fs.png' }), { filename: 'fs.png' } ); assert.equal(buildImageActionTarget(null), null); }); it('keeps compare disabled for single-image results', () => { const html = renderToStaticMarkup( ); assert.match( html, new RegExp(`]*disabled=""[^>]*>${buttonContentPattern}对比${buttonContentPattern}`) ); }); it('enables compare for single-image results with a previous history image', () => { const html = renderToStaticMarkup( ); assert.match( html, new RegExp(`]*disabled="")[^>]*>${buttonContentPattern}对比${buttonContentPattern}`) ); }); it('uses user-facing generation activity copy for the activity entry', () => { const html = renderToStaticMarkup( ); assert.match(html, /查看动态/); assert.doesNotMatch(html, /查看日志/); }); it('shows an explicit failure state with retry near the canvas', () => { const html = renderToStaticMarkup( ); assert.match(html, /生成失败/); assert.match(html, /上游或 API 中转站异常。请稍后重试。/); assert.match(html, new RegExp(`]*>${buttonContentPattern}重试生成${buttonContentPattern}`)); assert.doesNotMatch(html, /灵感样张/); }); });