128 行
4.3 KiB
TypeScript
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 ()
|
|
})
|
|
})
|