このコミットが含まれているのは:
2026-07-16 23:12:14 +09:00
コミット 3f75994bd4
14個のファイルの変更592行の追加404行の削除
+22 -150
ファイルの表示
@@ -1,172 +1,44 @@
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { screen } from '@testing-library/react'
import { MemoryRouter, Route, Routes } from 'react-router-dom'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { toMinutePrecisionIsoUtc } from '@/components/common/DateTimeField'
import PostNewPage from '@/pages/posts/PostNewPage'
import { buildUser } from '@/test/factories'
import { renderWithProviders } from '@/test/render'
const api = vi.hoisted (() => ({
apiGet: vi.fn (),
apiPost: vi.fn (),
isApiError: vi.fn (),
}))
const toastApi = vi.hoisted (() => ({
toast: vi.fn (),
}))
apiPost: vi.fn (),
isApiError: vi.fn () }))
vi.mock ('@/lib/api', () => api)
vi.mock ('@/components/ui/use-toast', () => toastApi)
describe ('PostNewPage', () => {
beforeEach (() => {
vi.clearAllMocks ()
api.isApiError.mockReturnValue (false)
sessionStorage.clear ()
})
it ('blocks guests', () => {
renderWithProviders (<PostNewPage user={buildUser ({ role: 'guest' })}/>)
it ('shows the source page on /posts/new', () => {
renderWithProviders (
<MemoryRouter initialEntries={['/posts/new']}>
<Routes>
<Route path="/posts/new" element={<PostNewPage user={buildUser ()}/>}/>
</Routes>
</MemoryRouter>)
expect (screen.getByText ('403')).toBeInTheDocument ()
expect (screen.getByRole ('heading', { name: '広場に投稿を追加' })).toBeInTheDocument ()
})
it ('submits a new post with manual title and thumbnail fetch UI', async () => {
api.apiPost.mockResolvedValueOnce ({})
api.apiGet.mockResolvedValue ([])
it ('shows the review page when query state is present', () => {
renderWithProviders (
<MemoryRouter initialEntries={['/posts/new?url=https%3A%2F%2Fexample.com%2Fpost']}>
<Routes>
<Route path="/posts/new" element={<PostNewPage user={buildUser ()}/>}/>
</Routes>
</MemoryRouter>)
renderWithProviders (<PostNewPage user={buildUser ({ role: 'member' })}/>)
fireEvent.change (screen.getByLabelText ('URL'), {
target: { value: 'https://example.com/post' } })
fireEvent.change (screen.getByLabelText ('タイトル'), {
target: { value: '投稿タイトル' } })
fireEvent.change (screen.getByLabelText ('親投稿'), {
target: { value: '1 2' } })
fireEvent.change (screen.getByLabelText ('タグ'), {
target: { value: 'tag1 tag2' } })
fireEvent.click (screen.getByRole ('button', { name: '追加' }))
await waitFor (() => {
expect (api.apiPost).toHaveBeenCalledWith (
'/posts',
expect.any (FormData),
{ headers: { 'Content-Type': 'multipart/form-data' } },
)
})
const formData = api.apiPost.mock.calls[0]?.[1] as FormData
expect (formData.get ('url')).toBe ('https://example.com/post')
expect (formData.get ('title')).toBe ('投稿タイトル')
expect (formData.get ('parent_post_ids')).toBe ('1 2')
expect (formData.get ('tags')).toBe ('tag1 tag2')
expect (toastApi.toast).toHaveBeenCalledWith ({ title: '投稿成功!' })
expect (screen.getByRole ('heading', { name: '追加内容確認' })).toBeInTheDocument ()
expect (screen.queryByText ('広場に投稿を追加')).not.toBeInTheDocument ()
})
it ('shows the shared creation field order without duration or unrequested helper copy', () => {
api.apiGet.mockResolvedValue ([])
const { container } =
renderWithProviders (<PostNewPage user={buildUser ({ role: 'member' })}/>)
const labels = Array.from (container.querySelectorAll ('label'))
.map (node => node.textContent?.trim ())
expect (labels.slice (0, 6)).toEqual ([
'URL',
'サムネール',
'タイトル',
'オリジナルの作成日時',
'タグ',
'親投稿'])
expect (screen.queryByPlaceholderText ('例: 2 / 2.5 / 1:23')).not.toBeInTheDocument ()
expect (screen.queryByText ('必要なタイミングで URL から取得できます.'))
.not.toBeInTheDocument ()
expect (screen.getAllByRole ('button', { name: '取得' })).toHaveLength (2)
expect (screen.queryByRole ('button', { name: '変更をリセット' })).not.toBeInTheDocument ()
})
it ('shows 422 validation errors for post fields', async () => {
api.apiGet.mockResolvedValue ([])
api.isApiError.mockReturnValue (true)
api.apiPost.mockRejectedValueOnce ({
response: {
status: 422,
data: {
type: 'validation_error',
message: '入力内容を確認してください.',
errors: { tags: ['ニコニコ・タグは直接指定できません.'] },
base_errors: ['投稿内容を確認してください.'],
},
},
})
renderWithProviders (<PostNewPage user={buildUser ({ role: 'member' })}/>)
fireEvent.change (screen.getByLabelText ('URL'), {
target: { value: 'https://example.com/post' } })
fireEvent.change (screen.getByLabelText ('タイトル'), {
target: { value: '投稿タイトル' } })
fireEvent.change (screen.getByLabelText ('タグ'), {
target: { value: 'nico:nico_tag' } })
fireEvent.click (screen.getByRole ('button', { name: '追加' }))
expect (await screen.findByText ('投稿内容を確認してください.')).toBeInTheDocument ()
expect (screen.getByText ('ニコニコ・タグは直接指定できません.')).toBeInTheDocument ()
expect (screen.getByLabelText ('タグ')).toHaveAttribute ('aria-invalid', 'true')
})
it ('keeps shared tags and datetime submission values without showing duration', async () => {
api.apiPost.mockResolvedValueOnce ({})
api.apiGet.mockResolvedValue ([])
const { container } = renderWithProviders (
<PostNewPage user={buildUser ({ role: 'member' })}/>)
fireEvent.change (screen.getByLabelText ('URL'), {
target: { value: 'https://example.com/post' } })
fireEvent.change (screen.getByLabelText ('タイトル'), {
target: { value: '投稿タイトル' } })
fireEvent.change (screen.getByLabelText ('タグ'), {
target: { value: '動画 tag1 tag2' } })
const datetimeInputs = container.querySelectorAll ('input[type="datetime-local"]')
fireEvent.change (datetimeInputs[0] as HTMLInputElement, {
target: { value: '2024-01-01T12:34' } })
fireEvent.click (screen.getByRole ('button', { name: '追加' }))
await waitFor (() => expect (api.apiPost).toHaveBeenCalled ())
const formData = api.apiPost.mock.calls[0]?.[1] as FormData
expect (container.querySelector ('input[type="file"]')).not.toBeNull ()
expect (formData.get ('duration')).toBeNull ()
expect (formData.get ('tags')).toBe ('動画 tag1 tag2')
expect (formData.get ('original_created_from')).toBe (
toMinutePrecisionIsoUtc ('2024-01-01T12:34'))
})
it (
'shows deduplicated original-created endpoint errors on the shared datetime field',
async () => {
api.apiGet.mockResolvedValue ([])
api.isApiError.mockReturnValue (true)
api.apiPost.mockRejectedValueOnce ({
response: {
status: 422,
data: {
type: 'validation_error',
errors: {
original_created_at: ['日時を確認してください.'],
original_created_from: ['日時を確認してください.'],
original_created_before: ['終了を確認してください.'] },
},
},
})
renderWithProviders (<PostNewPage user={buildUser ({ role: 'member' })}/>)
fireEvent.click (screen.getByRole ('button', { name: '追加' }))
expect (await screen.findByText ('日時を確認してください.')).toBeInTheDocument ()
expect (screen.getByText ('終了を確認してください.')).toBeInTheDocument ()
expect (screen.getAllByText ('日時を確認してください.')).toHaveLength (1)
})
})