フォームのバリデーションとニコ連携の画面変更 (#090) (#355)

Reviewed-on: #355
Co-authored-by: miteruzo <miteruzo@naver.com>
Co-committed-by: miteruzo <miteruzo@naver.com>
このコミットはPull リクエスト #355 でマージされました.
このコミットが含まれているのは:
2026-06-05 01:59:46 +09:00
committed by みてるぞ
コミット 750aa40e8e
66個のファイルの変更2624行の追加802行の削除
+32 -2
ファイルの表示
@@ -1,12 +1,13 @@
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import SettingPage from '@/pages/users/SettingPage'
import { buildUser } from '@/test/factories'
import { renderWithProviders } from '@/test/render'
const api = vi.hoisted (() => ({
apiPut: vi.fn (),
apiPut: vi.fn (),
isApiError: vi.fn (),
}))
const toastApi = vi.hoisted (() => ({
@@ -23,6 +24,11 @@ vi.mock ('@/components/users/InheritDialogue', () => ({
}))
describe ('SettingPage', () => {
beforeEach (() => {
vi.clearAllMocks ()
api.isApiError.mockReturnValue (false)
})
it ('shows loading when user is absent', () => {
renderWithProviders (<SettingPage user={null} setUser={vi.fn ()}/>)
@@ -51,4 +57,28 @@ describe ('SettingPage', () => {
expect (setUser).toHaveBeenCalled ()
expect (toastApi.toast).toHaveBeenCalledWith ({ title: '設定を更新しました.' })
})
it ('shows validation errors returned for the name field', async () => {
const user = buildUser ({ id: 11, name: 'old' })
api.isApiError.mockReturnValue (true)
api.apiPut.mockRejectedValueOnce ({
response: {
status: 422,
data: {
type: 'validation_error',
message: '入力内容を確認してください.',
errors: { name: ['名前は必須です.'] },
base_errors: [],
},
},
})
renderWithProviders (<SettingPage user={user} setUser={vi.fn ()}/>)
fireEvent.change (screen.getByRole ('textbox'), { target: { value: '' } })
fireEvent.click (screen.getByRole ('button', { name: '更新' }))
expect (await screen.findByText ('名前は必須です.')).toBeInTheDocument ()
expect (screen.getByRole ('textbox')).toHaveAttribute ('aria-invalid', 'true')
})
})
+22 -5
ファイルの表示
@@ -3,7 +3,9 @@ import type { FC } from 'react'
import { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import FieldError from '@/components/common/FieldError'
import Form from '@/components/common/Form'
import FormField from '@/components/common/FormField'
import Label from '@/components/common/Label'
import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
@@ -13,22 +15,30 @@ import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config'
import { apiPut } from '@/lib/api'
import { inputClass } from '@/lib/utils'
import { useValidationErrors } from '@/lib/useValidationErrors'
import type { User } from '@/types'
type Props = { user: User | null
setUser: React.Dispatch<React.SetStateAction<User | null>> }
type UserFormField = 'name'
const SettingPage: FC<Props> = ({ user, setUser }) => {
const [name, setName] = useState ('')
const [userCodeVsbl, setUserCodeVsbl] = useState (false)
const [inheritVsbl, setInheritVsbl] = useState (false)
const { baseErrors, fieldErrors, clearValidationErrors, applyValidationError } =
useValidationErrors<UserFormField> ()
const handleSubmit = async () => {
if (!(user))
return
clearValidationErrors ()
const formData = new FormData
formData.append ('name', name)
@@ -40,8 +50,9 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
setUser (user => ({ ...user, ...data }))
toast ({ title: '設定を更新しました.' })
}
catch
catch (e)
{
applyValidationError (e)
toast ({ title: 'しっぱい……' })
}
}
@@ -65,11 +76,16 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
{user ? (
<>
<FieldError messages={baseErrors}/>
{/* 名前 */}
<div>
<Label></Label>
<FormField label="表示名" messages={fieldErrors.name}>
{({ describedBy, invalid }) => (
<>
<input type="text"
className="w-full border rounded p-2"
aria-describedby={describedBy}
aria-invalid={invalid}
className={inputClass (invalid)}
value={name}
placeholder="名もなきニジラー"
onChange={ev => setName (ev.target.value)}/>
@@ -77,7 +93,8 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
<p className="mt-1 text-sm text-red-500">
30 !!!!
</p>)}
</div>
</>)}
</FormField>
{/* 送信 */}
<Button onClick={handleSubmit}