このコミットが含まれているのは:
2026-07-16 00:44:11 +09:00
コミット 2e1b4449ba
18個のファイルの変更1522行の追加0行の削除
+82
ファイルの表示
@@ -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: ['タイトルを確認してください.'] } })
})
})
+78
ファイルの表示
@@ -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' })
})
})
})
+92
ファイルの表示
@@ -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)
})
})