f1181e8510
Reviewed-on: #413 Co-authored-by: miteruzo <miteruzo@naver.com> Co-committed-by: miteruzo <miteruzo@naver.com>
158 行
5.3 KiB
TypeScript
158 行
5.3 KiB
TypeScript
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 }) => <div>{children}</div>,
|
|
main: ({ children }: { children?: ReactNode }) => <main>{children}</main> } }))
|
|
|
|
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 <output aria-label="current-location">{location.pathname}{location.search}</output>
|
|
}
|
|
|
|
|
|
const renderReviewPage = (urls: string[]) => {
|
|
const search = urls.map (url => encodeURIComponent (url)).join ('+')
|
|
return renderWithProviders (
|
|
<>
|
|
<PostImportReviewPage user={buildUser ()}/>
|
|
<LocationProbe/>
|
|
</>,
|
|
{ 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<typeof metadata>) => 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))
|
|
})
|
|
})
|