import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { ImagePromptMuseum } from '@/pages/ImagePromptMuseum'; import { useImagePromptMuseumStore } from '@/stores/image-prompt-museum'; import type { PromptMuseumEntry, PromptMuseumPage } from '../../shared/image-prompt-museum'; const fetchPromptMuseumPageMock = vi.hoisted(() => vi.fn()); const fetchPromptMuseumEntryMock = vi.hoisted(() => vi.fn()); const fetchPromptMuseumMediaMock = vi.hoisted(() => vi.fn()); vi.mock('@/lib/image-prompt-museum', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, fetchPromptMuseumPage: (...args: unknown[]) => fetchPromptMuseumPageMock(...args), fetchPromptMuseumEntry: (...args: unknown[]) => fetchPromptMuseumEntryMock(...args), fetchPromptMuseumMedia: (...args: unknown[]) => fetchPromptMuseumMediaMock(...args), }; }); const pageFixture: PromptMuseumPage = { items: [{ id: 'prompt-one', slug: 'editorial-poster', title: '编辑感产品海报', summary: '一张留白充足的编辑版式海报。', thumbnail: { url: 'https://cdn.example.com/prompt-one/thumb.webp', width: 1200, height: 900, alt: '编辑感产品海报示例', }, categories: [ { id: 'poster', name: '海报', group: 'use_case' }, { id: 'editorial', name: '编辑感', group: 'style' }, ], model: { id: 'gpt-image-2', name: 'GPT Image 2' }, language: 'zh-CN', attribution: { author: { name: '示例作者' }, source: { name: '示例来源', url: 'https://example.com/source' }, license: { name: 'CC BY 4.0', attributionText: '示例作者 / 示例来源 / CC BY 4.0' }, }, publishedAt: '2026-08-01T00:00:00Z', updatedAt: '2026-08-01T00:00:00Z', }], facets: { useCases: [{ id: 'poster', name: '海报', count: 1 }], styles: [{ id: 'editorial', name: '编辑感', count: 1 }], subjects: [], }, nextCursor: null, total: 1, }; const entryFixture: PromptMuseumEntry = { ...pageFixture.items[0], prompt: 'Create a clean editorial product poster with generous negative space.', variables: [], images: [pageFixture.items[0].thumbnail], requiresReferenceImages: false, }; describe('Prompt Museum page', () => { beforeEach(() => { vi.clearAllMocks(); useImagePromptMuseumStore.getState().clearPendingPrompt(); fetchPromptMuseumPageMock.mockResolvedValue(pageFixture); fetchPromptMuseumEntryMock.mockResolvedValue(entryFixture); fetchPromptMuseumMediaMock.mockResolvedValue('data:image/webp;base64,AQID'); }); it('keeps the inspiration filters compact and below the native title bar', async () => { render( } /> , ); expect(screen.queryByText('提示词博物馆')).not.toBeInTheDocument(); expect(screen.getByTestId('image-prompt-museum-filters')).toHaveClass('sticky', 'top-0'); }); it('renders server cards, opens attribution details, and sends the exact prompt back to Canvas', async () => { render( } /> Canvas} /> , ); fireEvent.click(await screen.findByRole('button', { name: '查看 编辑感产品海报' })); expect(await screen.findByText('示例作者')).toBeInTheDocument(); expect(screen.getByText('示例来源')).toBeInTheDocument(); expect(screen.getByText(entryFixture.prompt)).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '使用此 Prompt' })); await waitFor(() => expect(screen.getByTestId('canvas-target')).toBeInTheDocument()); expect(useImagePromptMuseumStore.getState().pendingPrompt).toEqual({ promptId: entryFixture.id, title: entryFixture.title, prompt: entryFixture.prompt, }); }); it('loads the next server cursor without bundling another page locally', async () => { const secondItem = { ...pageFixture.items[0], id: 'prompt-two', title: '第二条灵感' }; fetchPromptMuseumPageMock .mockResolvedValueOnce({ ...pageFixture, nextCursor: 'cursor-two' }) .mockResolvedValueOnce({ ...pageFixture, items: [secondItem], nextCursor: null }); render( } /> , ); fireEvent.click(await screen.findByRole('button', { name: '加载更多' })); expect(await screen.findByRole('button', { name: '查看 第二条灵感' })).toBeInTheDocument(); expect(fetchPromptMuseumPageMock).toHaveBeenNthCalledWith( 2, expect.objectContaining({ cursor: 'cursor-two' }), ); }); it('renders source attribution as text when the server omits its optional URL', async () => { fetchPromptMuseumEntryMock.mockResolvedValueOnce({ ...entryFixture, attribution: { ...entryFixture.attribution, source: { name: 'PromptHero' }, }, }); render( } /> , ); fireEvent.click(await screen.findByRole('button', { name: '查看 编辑感产品海报' })); expect(await screen.findByText('PromptHero')).toBeInTheDocument(); expect(screen.queryByRole('link', { name: 'PromptHero' })).not.toBeInTheDocument(); }); it('loads relative media through Main and renders its data URL', async () => { fetchPromptMuseumPageMock.mockResolvedValueOnce({ ...pageFixture, items: [{ ...pageFixture.items[0], thumbnail: { ...pageFixture.items[0].thumbnail, url: '/api/image-prompt-museum/prompt-one/media/thumbnail', }, }], }); render( } /> , ); const image = await screen.findByRole('img', { name: '编辑感产品海报示例' }); expect(image).toHaveAttribute('src', 'data:image/webp;base64,AQID'); expect(fetchPromptMuseumMediaMock).toHaveBeenCalledWith( '/api/image-prompt-museum/prompt-one/media/thumbnail', ); }); it('keeps a card usable when its relative image fails', async () => { fetchPromptMuseumMediaMock.mockRejectedValueOnce(new Error('image unavailable')); fetchPromptMuseumPageMock.mockResolvedValueOnce({ ...pageFixture, items: [{ ...pageFixture.items[0], thumbnail: { ...pageFixture.items[0].thumbnail, url: '/api/image-prompt-museum/prompt-one/media/thumbnail', }, }], }); render( } /> , ); expect(await screen.findByLabelText('编辑感产品海报示例加载失败')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '查看 编辑感产品海报' })).toBeEnabled(); }); });