import { fireEvent, screen, waitFor } from '@testing-library/react' import { useLocation } from 'react-router-dom' import { beforeEach, describe, expect, it, vi } from 'vitest' import PostImportReviewPage from '@/pages/posts/PostImportReviewPage' import { buildUser } from '@/test/factories' import { renderWithProviders } from '@/test/render' import type { ReactNode } from 'react' const api = vi.hoisted (() => ({ apiGet: vi.fn (), apiPost: vi.fn (), isApiError: vi.fn (() => false) })) vi.mock ('@/lib/api', () => api) vi.mock ('framer-motion', () => ({ AnimatePresence: ({ children }: { children?: ReactNode }) => <>{children}, motion: { div: ({ children }: { children?: ReactNode }) =>
{children}
, main: ({ children }: { children?: ReactNode }) =>
{children}
} })) const metadata = (url: string, title: string) => ({ url, title, thumbnailBase: 'https://example.com/thumbnail.jpg', originalCreatedFrom: '', originalCreatedBefore: '', duration: '', tags: '', parentPostIds: [], fieldWarnings: { }, baseWarnings: [], displayTags: [] }) const LocationProbe = () => { const location = useLocation () return {location.pathname}{location.search} } const renderReviewPage = (urls: string[]) => { const search = urls.map (url => encodeURIComponent (url)).join ('+') return renderWithProviders ( <> , { route: `/posts/new?urls=${ search }` }) } describe ('PostImportReviewPage', () => { beforeEach (() => { vi.clearAllMocks () api.isApiError.mockReturnValue (false) }) it ('fetches metadata with at most four concurrent requests', async () => { const resolvers: Array<(value: ReturnType) => void> = [] api.apiGet.mockImplementation ((_path, options) => new Promise (resolve => { resolvers.push (resolve) const url = String (options.params.url) void url })) const urls = Array.from ( { length: 6 }, (_, index) => `https://example.com/${ index + 1 }`) renderReviewPage (urls) await waitFor (() => expect (api.apiGet).toHaveBeenCalledTimes (4)) resolvers[0]?.(metadata (urls[0]!, 'first')) expect (await screen.findAllByText ('first')).toHaveLength (2) await waitFor (() => expect (api.apiGet).toHaveBeenCalledTimes (5)) expect (screen.getByRole ('button', { name: '追加' })).toBeDisabled () for (let index = 1; index < resolvers.length; ++index) resolvers[index]?.(metadata (urls[index]!, `post ${ index + 1 }`)) }) it ('keeps successful rows when another metadata request fails', async () => { api.apiGet .mockResolvedValueOnce (metadata ('https://example.com/one', 'first')) .mockRejectedValueOnce (new TypeError ('network')) renderReviewPage (['https://example.com/one', 'https://example.com/two']) expect (await screen.findAllByText ('first')).toHaveLength (2) await waitFor (() => { expect (screen.getAllByText ('登録不可')).toHaveLength (2) }) expect (screen.getAllByRole ('button', { name: '編輯' })[0]).toBeEnabled () }) it ('shows existing posts with Active Storage thumbnail precedence', async () => { api.apiGet.mockResolvedValue ({ ...metadata ('https://example.com/post', ''), existingPostId: 24, existingPost: { id: 24, title: 'existing post', url: 'https://example.com/post', thumbnail: 'https://example.com/storage.jpg', thumbnailBase: 'https://example.com/base.jpg' } }) renderReviewPage (['https://example.com/post']) const disclosure = await screen.findByRole ('button', { name: '既存投稿による自動スキップ 1件' }) expect (screen.getByRole ('button', { name: '追加' })).toBeDisabled () fireEvent.click (disclosure) expect ((await screen.findAllByRole ('img', { name: 'サムネール' }))[0]) .toHaveAttribute ('src', 'https://example.com/storage.jpg') }) it ('keeps a recoverable bulk failure on the review screen', async () => { api.apiGet.mockResolvedValue ( metadata ('https://example.com/post', 'new post')) api.apiPost.mockResolvedValue ({ results: [{ status: 'failed', recoverable: true, errors: { title: ['invalid title'] } }] }) renderReviewPage (['https://example.com/post']) fireEvent.click (await screen.findByRole ('button', { name: '追加' })) expect (await screen.findAllByText ('登録失敗')).toHaveLength (2) expect (screen.getAllByText ('invalid title')).toHaveLength (2) expect (screen.getAllByRole ('button', { name: '編輯' })[0]).toBeEnabled () expect (screen.getAllByRole ('button', { name: '再試行' })[0]).toBeEnabled () expect (screen.getByLabelText ('current-location')).toHaveTextContent ('/posts/new?') }) it ('navigates to /posts after every submitted row is created', async () => { api.apiGet.mockResolvedValue ( metadata ('https://example.com/post', 'new post')) api.apiPost.mockResolvedValue ({ results: [{ status: 'created', post: { id: 42 } }] }) renderReviewPage (['https://example.com/post']) fireEvent.click (await screen.findByRole ('button', { name: '追加' })) await waitFor (() => { expect (screen.getByLabelText ('current-location')).toHaveTextContent ('/posts') }) expect (api.apiPost).toHaveBeenCalledWith ('/posts/bulk', expect.any (FormData)) }) })