ファイル
btrc-hub/frontend/src/pages/materials/MaterialNewPage.test.tsx
T
2026-10-20 20:44:14 +09:00

128 行
4.3 KiB
TypeScript

import { fireEvent, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { Route, Routes, useLocation } from 'react-router-dom'
import { textContentOf } from '@/test/text'
import MaterialNewPage from '@/pages/materials/MaterialNewPage'
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 (),
}))
vi.mock ('@/lib/api', () => api)
vi.mock ('@/components/ui/use-toast', () => toastApi)
const LocationView = () => {
const location = useLocation ()
return <div>{location.pathname + location.search}</div>
}
describe ('MaterialNewPage', () => {
beforeEach (() => {
vi.clearAllMocks ()
api.apiGet.mockResolvedValue ([])
api.isApiError.mockReturnValue (false)
})
it ('initializes tag from query and submits form data', async () => {
api.apiPost.mockResolvedValueOnce ({})
renderWithProviders (<MaterialNewPage/>, { route: '/ja/materials/new?tag=%E8%99%B9%E5%A4%8F' })
expect (screen.getAllByRole ('textbox')[0]).toHaveValue ('虹夏')
fireEvent.change (screen.getAllByRole ('textbox')[1], {
target: { value: 'https://example.com/ref' },
})
fireEvent.click (screen.getByRole ('button', { name: '追加' }))
await waitFor (() => {
expect (api.apiPost).toHaveBeenCalledWith (
'/materials', expect.any (FormData), { locale: 'ja' })
})
const formData = api.apiPost.mock.calls[0]?.[1] as FormData
expect (formData.get ('tag')).toBe ('虹夏')
expect (formData.get ('url')).toBe ('https://example.com/ref')
expect (toastApi.toast).toHaveBeenCalledTimes (1)
expect (textContentOf (toastApi.toast.mock.calls[0][0].title)).toBe ('送信成功!')
})
it ('shows validation errors for file and url fields', async () => {
api.isApiError.mockReturnValue (true)
api.apiPost.mockRejectedValueOnce ({
response: {
status: 422,
data: {
type: 'validation_error',
message: '入力内容を確認してください.',
errors: {
file: ['ファイルまたは URL は必須です.'],
url: ['ファイルまたは URL は必須です.'],
},
base_errors: [],
},
},
})
renderWithProviders (<MaterialNewPage/>)
fireEvent.change (screen.getAllByRole ('textbox')[0], { target: { value: '虹夏' } })
fireEvent.click (screen.getByRole ('button', { name: '追加' }))
expect (await screen.findAllByText ('ファイルまたは URL は必須です.')).toHaveLength (2)
expect (screen.getAllByRole ('textbox')[1]).toHaveAttribute ('aria-invalid', 'true')
})
it ('returns only to safe material URLs after successful creation', async () => {
api.apiPost.mockResolvedValueOnce ({})
renderWithProviders (
<Routes>
<Route path="/ja/materials/new" element={<MaterialNewPage/>}/>
<Route path="/ja/materials" element={<LocationView/>}/>
</Routes>,
{
route:
'/ja/materials/new?tag=%E8%99%B9%E5%A4%8F'
+ '&return_to=%2Fmaterials%3Ftag_id%3D20',
},
)
fireEvent.change (screen.getAllByRole ('textbox')[1], {
target: { value: 'https://example.com/ref' },
})
fireEvent.click (screen.getByRole ('button', { name: '追加' }))
expect (await screen.findByText ('/ja/materials?tag_id=20')).toBeInTheDocument ()
})
it ('ignores unsafe return_to values after successful creation', async () => {
api.apiPost.mockResolvedValueOnce ({})
renderWithProviders (
<Routes>
<Route path="/ja/materials/new" element={<MaterialNewPage/>}/>
<Route path="/ja/materials" element={<LocationView/>}/>
</Routes>,
{
route:
'/ja/materials/new?tag=%E8%99%B9%E5%A4%8F'
+ '&return_to=https%3A%2F%2Fexample.com%2Fevil',
},
)
fireEvent.change (screen.getAllByRole ('textbox')[1], {
target: { value: 'https://example.com/ref' },
})
fireEvent.click (screen.getByRole ('button', { name: '追加' }))
expect (await screen.findByText ('/ja/materials')).toBeInTheDocument ()
})
})