import { toChapterViewModel } from '@/features/memoir/mappers'; import type { Chapter, ImageAsset } from '@/features/memoir/types'; function makeImage(overrides: Partial = {}): ImageAsset { return { placeholder: 'img_1', description: 'A scene', index: 0, status: 'completed', prompt: null, url: 'https://example.com/img.jpg', provider: null, style: null, size: null, error: null, retryable: null, created_at: null, updated_at: null, ...overrides, }; } function makeChapter(overrides: Partial = {}): Chapter { return { id: 'ch-1', title: '童年', order_index: 0, status: 'ready', category: 'childhood', images: [], cover_asset: null, canonical_markdown: '段落1', updated_at: '2026-01-01T00:00:00Z', is_new: false, source_segments: [], ...overrides, }; } describe('toChapterViewModel', () => { test('maps basic chapter fields', () => { const vm = toChapterViewModel(makeChapter()); expect(vm.id).toBe('ch-1'); expect(vm.title).toBe('童年'); expect(vm.category).toBe('childhood'); expect(vm.orderIndex).toBe(0); expect(vm.isEmpty).toBe(false); expect(vm.isNew).toBe(false); expect(vm.wordCount).toBe('段落1'.length); }); test('uses word_count from API when canonical markdown exists', () => { const vm = toChapterViewModel( makeChapter({ word_count: 1200, canonical_markdown: 'x'.repeat(1200), }), ); expect(vm.wordCount).toBe(1200); }); test('uses cover_asset from list payload', () => { const cover = makeImage({ url: 'https://example.com/from-asset.jpg' }); const vm = toChapterViewModel( makeChapter({ cover_asset: cover, images: [] }), ); expect(vm.coverImageUrl).toBe('https://example.com/from-asset.jpg'); expect(vm.hasImages).toBe(true); }); test('detects empty chapters', () => { const vm = toChapterViewModel(makeChapter({ canonical_markdown: '' })); expect(vm.isEmpty).toBe(true); }); test('uses canonical_markdown for isEmpty when present', () => { const vm = toChapterViewModel( makeChapter({ status: 'ready', canonical_markdown: '# 童年\n\n一段回忆。', }), ); expect(vm.isEmpty).toBe(false); }); test('derives image status counts correctly', () => { const images = [ makeImage({ status: 'completed' }), makeImage({ status: 'pending' }), makeImage({ status: 'processing' }), makeImage({ status: 'failed' }), ]; const vm = toChapterViewModel(makeChapter({ images })); expect(vm.hasImages).toBe(true); expect(vm.allImagesReady).toBe(false); expect(vm.pendingImageCount).toBe(2); expect(vm.failedImageCount).toBe(1); }); test('allImagesReady is true when all completed', () => { const images = [ makeImage({ status: 'completed' }), makeImage({ status: 'completed' }), ]; const vm = toChapterViewModel(makeChapter({ images })); expect(vm.allImagesReady).toBe(true); expect(vm.pendingImageCount).toBe(0); expect(vm.failedImageCount).toBe(0); }); test('extracts cover image URL', () => { const cover = makeImage({ url: 'https://example.com/cover.jpg' }); const vm = toChapterViewModel(makeChapter({ cover_asset: cover })); expect(vm.coverImageUrl).toBe('https://example.com/cover.jpg'); }); test('cover image URL is null when no cover', () => { const vm = toChapterViewModel(makeChapter({ cover_asset: null })); expect(vm.coverImageUrl).toBeNull(); }); });