Files
makelore/tests/unit/image-prompt-museum-page.test.tsx

207 lines
7.6 KiB
TypeScript

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<typeof import('@/lib/image-prompt-museum')>();
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(
<MemoryRouter initialEntries={['/image-prompts']}>
<Routes>
<Route path="/image-prompts" element={<ImagePromptMuseum />} />
</Routes>
</MemoryRouter>,
);
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(
<MemoryRouter initialEntries={['/image-prompts']}>
<Routes>
<Route path="/image-prompts" element={<ImagePromptMuseum />} />
<Route path="/image-canvas" element={<div data-testid="canvas-target">Canvas</div>} />
</Routes>
</MemoryRouter>,
);
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(
<MemoryRouter initialEntries={['/image-prompts']}>
<Routes>
<Route path="/image-prompts" element={<ImagePromptMuseum />} />
</Routes>
</MemoryRouter>,
);
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(
<MemoryRouter initialEntries={['/image-prompts']}>
<Routes>
<Route path="/image-prompts" element={<ImagePromptMuseum />} />
</Routes>
</MemoryRouter>,
);
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(
<MemoryRouter initialEntries={['/image-prompts']}>
<Routes>
<Route path="/image-prompts" element={<ImagePromptMuseum />} />
</Routes>
</MemoryRouter>,
);
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(
<MemoryRouter initialEntries={['/image-prompts']}>
<Routes>
<Route path="/image-prompts" element={<ImagePromptMuseum />} />
</Routes>
</MemoryRouter>,
);
expect(await screen.findByLabelText('编辑感产品海报示例加载失败')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '查看 编辑感产品海报' })).toBeEnabled();
});
});