このコミットが含まれているのは:
@@ -0,0 +1,82 @@
|
||||
import { fireEvent, screen } from '@testing-library/react'
|
||||
import { Route, Routes, useLocation } from 'react-router-dom'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { savePostImportSession } from '@/lib/postImportSession'
|
||||
import PostImportResultPage from '@/pages/posts/PostImportResultPage'
|
||||
import { buildUser } from '@/test/factories'
|
||||
import { buildPostImportRow } from '@/test/postImportFactories'
|
||||
import { renderWithProviders } from '@/test/render'
|
||||
|
||||
const api = vi.hoisted (() => ({ apiPost: vi.fn () }))
|
||||
const toastApi = vi.hoisted (() => ({ toast: vi.fn () }))
|
||||
|
||||
vi.mock ('@/lib/api', () => api)
|
||||
vi.mock ('@/components/ui/use-toast', () => toastApi)
|
||||
|
||||
const ReviewLocation = () => {
|
||||
const location = useLocation ()
|
||||
return <div>{`review route ${ location.search }`}</div>
|
||||
}
|
||||
|
||||
const renderPage = () => renderWithProviders (
|
||||
<Routes>
|
||||
<Route
|
||||
path="/posts/import/:sessionId/result"
|
||||
element={<PostImportResultPage user={buildUser ()}/>}/>
|
||||
<Route path="/posts/import/:sessionId/review" element={<ReviewLocation/>}/>
|
||||
</Routes>,
|
||||
{ route: '/posts/import/session/result' })
|
||||
|
||||
describe ('PostImportResultPage', () => {
|
||||
beforeEach (() => {
|
||||
sessionStorage.clear ()
|
||||
vi.clearAllMocks ()
|
||||
})
|
||||
|
||||
it ('shows exclusive result counts, post links, and failed-row actions', async () => {
|
||||
const rows = [
|
||||
buildPostImportRow ({ sourceRow: 1, importStatus: 'created', createdPostId: 11 }),
|
||||
buildPostImportRow ({ sourceRow: 2, importStatus: 'skipped',
|
||||
skipReason: 'existing', existingPostId: 22 }),
|
||||
buildPostImportRow ({ sourceRow: 3, importStatus: 'failed',
|
||||
importErrors: { base: ['登録中に失敗しました.'] } })]
|
||||
savePostImportSession ('session', { source: '', rows, repairMode: 'all' })
|
||||
|
||||
renderPage ()
|
||||
|
||||
expect (await screen.findByText (
|
||||
/登録成功\s*1件.*スキップ\s*1件.*失敗\s*1件/)).toBeInTheDocument ()
|
||||
expect (screen.getAllByRole ('link', { name: '投稿を開く' })
|
||||
.map (_1 => _1.getAttribute ('href'))).toEqual (['/posts/11', '/posts/22'])
|
||||
expect (screen.getByText ('登録中に失敗しました.')).toBeInTheDocument ()
|
||||
expect (screen.getByRole ('button', { name: '編輯' })).toBeInTheDocument ()
|
||||
expect (screen.getByRole ('button', { name: '再試行' })).toBeInTheDocument ()
|
||||
})
|
||||
|
||||
it ('returns a retry validation error to the review dialogue route', async () => {
|
||||
const failed = buildPostImportRow ({
|
||||
importStatus: 'failed',
|
||||
importErrors: { base: ['old error'] } })
|
||||
const invalid = buildPostImportRow ({
|
||||
importStatus: 'pending',
|
||||
status: 'error',
|
||||
validationErrors: { title: ['タイトルを確認してください.'] } })
|
||||
savePostImportSession ('session', {
|
||||
source: failed.url,
|
||||
rows: [failed],
|
||||
repairMode: 'all' })
|
||||
api.apiPost.mockResolvedValue ({ rows: [invalid] })
|
||||
|
||||
renderPage ()
|
||||
fireEvent.click (await screen.findByRole ('button', { name: '再試行' }))
|
||||
|
||||
expect (await screen.findByText ('review route ?edit=1')).toBeInTheDocument ()
|
||||
expect (api.apiPost).toHaveBeenCalledTimes (1)
|
||||
const saved = JSON.parse (
|
||||
sessionStorage.getItem ('post-import-session:session') ?? '{}')
|
||||
expect (saved.rows[0]).toMatchObject ({
|
||||
importStatus: 'pending',
|
||||
validationErrors: { title: ['タイトルを確認してください.'] } })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,78 @@
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { Route, Routes } from 'react-router-dom'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { savePostImportSession } from '@/lib/postImportSession'
|
||||
import PostImportReviewPage from '@/pages/posts/PostImportReviewPage'
|
||||
import { buildUser } from '@/test/factories'
|
||||
import { buildPostImportRow } from '@/test/postImportFactories'
|
||||
import { renderWithProviders } from '@/test/render'
|
||||
|
||||
const api = vi.hoisted (() => ({ apiPost: vi.fn () }))
|
||||
const toastApi = vi.hoisted (() => ({ toast: vi.fn () }))
|
||||
|
||||
vi.mock ('@/lib/api', () => api)
|
||||
vi.mock ('@/components/ui/use-toast', () => toastApi)
|
||||
|
||||
const renderPage = () => renderWithProviders (
|
||||
<Routes>
|
||||
<Route
|
||||
path="/posts/import/:sessionId/review"
|
||||
element={<PostImportReviewPage user={buildUser ()}/>}/>
|
||||
<Route path="/posts/import/:sessionId/result" element={<div>result route</div>}/>
|
||||
</Routes>,
|
||||
{ route: '/posts/import/session/review' })
|
||||
|
||||
describe ('PostImportReviewPage', () => {
|
||||
beforeEach (() => {
|
||||
sessionStorage.clear ()
|
||||
vi.clearAllMocks ()
|
||||
})
|
||||
|
||||
it ('keeps the review route and opens the first invalid row dialogue', async () => {
|
||||
const row = buildPostImportRow ({ attributes: { title: 'title' } })
|
||||
savePostImportSession ('session', { source: row.url, rows: [row], repairMode: 'all' })
|
||||
api.apiPost.mockResolvedValue ({ rows: [buildPostImportRow ({
|
||||
attributes: { title: 'title' },
|
||||
status: 'error',
|
||||
validationErrors: { title: ['タイトルを確認してください.'] } })] })
|
||||
|
||||
renderPage ()
|
||||
fireEvent.click (await screen.findByRole ('button', { name: '取込実行' }))
|
||||
|
||||
expect (await screen.findByText ('投稿を編輯')).toBeInTheDocument ()
|
||||
expect (screen.getByText ('タイトルを確認してください.')).toBeInTheDocument ()
|
||||
expect (api.apiPost).toHaveBeenCalledTimes (1)
|
||||
expect (screen.queryByText ('result route')).not.toBeInTheDocument ()
|
||||
})
|
||||
|
||||
it ('submits existing rows for a formal skipped result and opens result route', async () => {
|
||||
const row = buildPostImportRow ({
|
||||
skipReason: 'existing',
|
||||
existingPostId: 10 })
|
||||
savePostImportSession ('session', { source: row.url, rows: [row], repairMode: 'all' })
|
||||
api.apiPost
|
||||
.mockResolvedValueOnce ({ rows: [row] })
|
||||
.mockResolvedValueOnce ({
|
||||
created: 0,
|
||||
skipped: 1,
|
||||
failed: 0,
|
||||
rows: [{ sourceRow: 1, status: 'skipped', existingPostId: 10 }] })
|
||||
|
||||
renderPage ()
|
||||
fireEvent.click (await screen.findByRole ('button', { name: '取込実行' }))
|
||||
|
||||
expect (await screen.findByText ('result route')).toBeInTheDocument ()
|
||||
expect (api.apiPost).toHaveBeenCalledTimes (2)
|
||||
expect (api.apiPost.mock.calls[1]?.[1]).toMatchObject ({
|
||||
rows: [expect.objectContaining ({ sourceRow: 1, url: row.url })] })
|
||||
await waitFor (() => {
|
||||
const saved = JSON.parse (
|
||||
sessionStorage.getItem ('post-import-session:session') ?? '{}')
|
||||
expect (saved.rows[0]).toMatchObject ({
|
||||
importStatus: 'skipped',
|
||||
existingPostId: 10,
|
||||
skipReason: 'existing' })
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,92 @@
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import PostImportSourcePage from '@/pages/posts/PostImportSourcePage'
|
||||
import { buildUser } from '@/test/factories'
|
||||
import { buildPostImportRow } from '@/test/postImportFactories'
|
||||
import { renderWithProviders } from '@/test/render'
|
||||
|
||||
const api = vi.hoisted (() => ({
|
||||
apiPost: vi.fn (),
|
||||
isApiError: vi.fn () }))
|
||||
|
||||
const router = vi.hoisted (() => ({ navigate: vi.fn () }))
|
||||
|
||||
const toastApi = vi.hoisted (() => ({ toast: vi.fn () }))
|
||||
|
||||
vi.mock ('@/lib/api', () => api)
|
||||
vi.mock ('@/components/ui/use-toast', () => toastApi)
|
||||
vi.mock ('react-router-dom', async importOriginal => ({
|
||||
...await importOriginal<typeof import('react-router-dom')> (),
|
||||
useNavigate: () => router.navigate }))
|
||||
|
||||
describe ('PostImportSourcePage', () => {
|
||||
beforeEach (() => {
|
||||
sessionStorage.clear ()
|
||||
vi.clearAllMocks ()
|
||||
api.isApiError.mockReturnValue (false)
|
||||
})
|
||||
|
||||
it ('shows no empty error initially and validates only after Next is pressed', () => {
|
||||
renderWithProviders (<PostImportSourcePage user={buildUser ()}/>)
|
||||
|
||||
expect (screen.queryByText ('URL を入力してください.')).not.toBeInTheDocument ()
|
||||
fireEvent.click (screen.getByRole ('button', { name: '次へ' }))
|
||||
expect (screen.getByText ('URL を入力してください.')).toBeInTheDocument ()
|
||||
expect (api.apiPost).not.toHaveBeenCalled ()
|
||||
})
|
||||
|
||||
it ('shows frontend URL issues with line numbers without requesting preview', () => {
|
||||
renderWithProviders (<PostImportSourcePage user={buildUser ()}/>)
|
||||
const input = screen.getByRole ('textbox', { name: '' })
|
||||
|
||||
fireEvent.change (input, {
|
||||
target: { value: '\nftp://example.com/file\nhttps://example.com/valid' } })
|
||||
expect (screen.queryByText (/2 行目/)).not.toBeInTheDocument ()
|
||||
fireEvent.click (screen.getByRole ('button', { name: '次へ' }))
|
||||
|
||||
expect (screen.getByText (/2 行目: HTTP または HTTPS/)).toBeInTheDocument ()
|
||||
expect (screen.getByText ('ftp://example.com/file')).toBeInTheDocument ()
|
||||
expect (input).toHaveAttribute ('aria-invalid', 'true')
|
||||
expect (input.getAttribute ('aria-describedby')).toContain ('post-import-source-issues')
|
||||
expect (api.apiPost).not.toHaveBeenCalled ()
|
||||
})
|
||||
|
||||
it ('shows backend URL errors against original input without a session', async () => {
|
||||
api.apiPost.mockResolvedValue ({
|
||||
rows: [buildPostImportRow ({
|
||||
sourceRow: 2,
|
||||
url: 'https://example.com/canonical',
|
||||
status: 'error',
|
||||
validationErrors: { url: ['URL が重複しています.'] } })] })
|
||||
renderWithProviders (<PostImportSourcePage user={buildUser ()}/>)
|
||||
|
||||
fireEvent.change (screen.getByRole ('textbox', { name: '' }), {
|
||||
target: { value: '\nhttps://example.com/original' } })
|
||||
fireEvent.click (screen.getByRole ('button', { name: '次へ' }))
|
||||
|
||||
expect (await screen.findByText ('2 行目: URL が重複しています.')).toBeInTheDocument ()
|
||||
expect (screen.getAllByText ('https://example.com/original')).toHaveLength (2)
|
||||
expect (router.navigate).not.toHaveBeenCalled ()
|
||||
expect (Array.from ({ length: sessionStorage.length }, (_, index) =>
|
||||
sessionStorage.key (index))).not.toContainEqual(
|
||||
expect.stringMatching (/^post-import-session:/))
|
||||
})
|
||||
|
||||
it ('stores a successful preview before navigating to the review route', async () => {
|
||||
api.apiPost.mockResolvedValue ({ rows: [buildPostImportRow ()] })
|
||||
renderWithProviders (<PostImportSourcePage user={buildUser ()}/>)
|
||||
|
||||
fireEvent.change (screen.getByRole ('textbox', { name: '' }), {
|
||||
target: { value: 'https://example.com/post' } })
|
||||
fireEvent.click (screen.getByRole ('button', { name: '次へ' }))
|
||||
|
||||
await waitFor (() => {
|
||||
expect (router.navigate).toHaveBeenCalledWith (
|
||||
expect.stringMatching (/^\/posts\/import\/[^/]+\/review$/))
|
||||
})
|
||||
const sessionKeys = Array.from ({ length: sessionStorage.length }, (_, index) =>
|
||||
sessionStorage.key (index)).filter (_1 => _1?.startsWith ('post-import-session:'))
|
||||
expect (sessionKeys).toHaveLength (1)
|
||||
})
|
||||
})
|
||||
新しい課題から参照
ユーザをブロックする