Reviewed-on: #413 Co-authored-by: miteruzo <miteruzo@naver.com> Co-committed-by: miteruzo <miteruzo@naver.com>
このコミットはPull リクエスト #413 でマージされました.
このコミットが含まれているのは:
@@ -0,0 +1,157 @@
|
||||
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))
|
||||
})
|
||||
})
|
||||
新しい課題から参照
ユーザをブロックする