このコミットが含まれているのは:
@@ -2,20 +2,18 @@ require 'rails_helper'
|
|||||||
|
|
||||||
RSpec.describe 'error responses', type: :request do
|
RSpec.describe 'error responses', type: :request do
|
||||||
describe 'manual input errors' do
|
describe 'manual input errors' do
|
||||||
it 'returns a stable errors array for bad requests' do
|
it 'returns a stable payload for bad requests' do
|
||||||
user = create(:user)
|
get '/tags/name/%20/deerjikists'
|
||||||
sign_in_as(user)
|
|
||||||
|
|
||||||
put "/users/#{ user.id }", params: { name: ' ' }
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:bad_request)
|
expect(response).to have_http_status(:bad_request)
|
||||||
expect(json.fetch('errors')).to contain_exactly(
|
expect(json).to include(
|
||||||
include('code' => 'bad_request',
|
'type' => 'bad_request',
|
||||||
'field' => 'name',
|
'message' => be_present,
|
||||||
'message' => be_present))
|
'errors' => {},
|
||||||
|
'base_errors' => [be_present])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns a stable errors array for unprocessable requests' do
|
it 'returns a stable field-error payload for unprocessable requests' do
|
||||||
member = create(:user, :member)
|
member = create(:user, :member)
|
||||||
tag = create(:tag, :general, name: 'error_response_tag')
|
tag = create(:tag, :general, name: 'error_response_tag')
|
||||||
sign_in_as(member)
|
sign_in_as(member)
|
||||||
@@ -23,25 +21,27 @@ RSpec.describe 'error responses', type: :request do
|
|||||||
patch "/tags/#{ tag.id }", params: { category: 'nico' }
|
patch "/tags/#{ tag.id }", params: { category: 'nico' }
|
||||||
|
|
||||||
expect(response).to have_http_status(:unprocessable_entity)
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
expect(json.fetch('errors')).to contain_exactly(
|
expect(json).to include(
|
||||||
include('code' => 'invalid',
|
'type' => 'validation_error',
|
||||||
'field' => 'category',
|
'message' => '入力内容を確認してください.',
|
||||||
'message' => be_present))
|
'base_errors' => [])
|
||||||
|
expect(json.fetch('errors')).to include(
|
||||||
|
'category' => ['ニコタグは変更できません.'])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'model validation errors' do
|
describe 'model validation errors' do
|
||||||
it 'returns field, code, and message for model errors' do
|
it 'returns field messages for model errors' do
|
||||||
user = create(:user)
|
user = create(:user)
|
||||||
sign_in_as(user)
|
sign_in_as(user)
|
||||||
|
|
||||||
put "/users/#{ user.id }", params: { name: 'a' * 256 }
|
put "/users/#{ user.id }", params: { name: 'a' * 256 }
|
||||||
|
|
||||||
expect(response).to have_http_status(:unprocessable_entity)
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
expect(json.fetch('errors')).to include(
|
expect(json).to include(
|
||||||
include('code' => 'too_long',
|
'type' => 'validation_error',
|
||||||
'field' => 'name',
|
'message' => '入力内容を確認してください.')
|
||||||
'message' => be_present))
|
expect(json.fetch('errors').fetch('name')).to include(be_present)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -141,16 +141,21 @@ RSpec.describe 'Materials API', type: :request do
|
|||||||
context 'when logged in' do
|
context 'when logged in' do
|
||||||
before { sign_in_as(guest_user) }
|
before { sign_in_as(guest_user) }
|
||||||
|
|
||||||
it 'returns 400 when tag is blank' do
|
it 'returns 422 when tag is blank' do
|
||||||
post '/materials', params: { tag: ' ', file: dummy_upload }
|
post '/materials', params: { tag: ' ', file: dummy_upload }
|
||||||
|
|
||||||
expect(response).to have_http_status(:bad_request)
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(json.fetch('errors')).to include(
|
||||||
|
'tag' => ['タグは必須です.'])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns 400 when both file and url are blank' do
|
it 'returns 422 when both file and url are blank' do
|
||||||
post '/materials', params: { tag: 'material_create_blank' }
|
post '/materials', params: { tag: 'material_create_blank' }
|
||||||
|
|
||||||
expect(response).to have_http_status(:bad_request)
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(json.fetch('errors')).to include(
|
||||||
|
'file' => ['ファイルまたは URL は必須です.'],
|
||||||
|
'url' => ['ファイルまたは URL は必須です.'])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a material with an attached file' do
|
it 'creates a material with an attached file' do
|
||||||
@@ -261,21 +266,26 @@ RSpec.describe 'Materials API', type: :request do
|
|||||||
expect(response).to have_http_status(:not_found)
|
expect(response).to have_http_status(:not_found)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns 400 when tag is blank' do
|
it 'returns 422 when tag is blank' do
|
||||||
put "/materials/#{ material.id }", params: {
|
put "/materials/#{ material.id }", params: {
|
||||||
tag: ' ',
|
tag: ' ',
|
||||||
file: dummy_upload
|
file: dummy_upload
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(response).to have_http_status(:bad_request)
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(json.fetch('errors')).to include(
|
||||||
|
'tag' => ['タグは必須です.'])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns 400 when both file and url are blank' do
|
it 'returns 422 when both file and url are blank' do
|
||||||
put "/materials/#{ material.id }", params: {
|
put "/materials/#{ material.id }", params: {
|
||||||
tag: 'material_update_no_payload'
|
tag: 'material_update_no_payload'
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(response).to have_http_status(:bad_request)
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(json.fetch('errors')).to include(
|
||||||
|
'file' => ['ファイルまたは URL は必須です.'],
|
||||||
|
'url' => ['ファイルまたは URL は必須です.'])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'updates tag, url, file, and updated_by_user' do
|
it 'updates tag, url, file, and updated_by_user' do
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ RSpec.describe 'NicoTags', type: :request do
|
|||||||
expect(versions.last.created_by_user_id).to eq(admin.id)
|
expect(versions.last.created_by_user_id).to eq(admin.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it '400 when linked tag normalises to nico tag' do
|
it 'returns 422 when linked tag normalises to nico tag' do
|
||||||
sign_in_as(member)
|
sign_in_as(member)
|
||||||
|
|
||||||
other_nico = create(:tag, :nico, name: 'nico:linked_ng')
|
other_nico = create(:tag, :nico, name: 'nico:linked_ng')
|
||||||
@@ -87,7 +87,9 @@ RSpec.describe 'NicoTags', type: :request do
|
|||||||
patch "/tags/nico/#{nico_tag.id}", params: { tags: 'linked_ng_alias' }
|
patch "/tags/nico/#{nico_tag.id}", params: { tags: 'linked_ng_alias' }
|
||||||
}.not_to change(NicoTagVersion, :count)
|
}.not_to change(NicoTagVersion, :count)
|
||||||
|
|
||||||
expect(response).to have_http_status(:bad_request)
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(json.fetch('errors')).to include(
|
||||||
|
'tags' => ['ニコニコ・タグ同士は連携できません.'])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -704,7 +704,7 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
category: :nico)
|
category: :nico)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'return 400' do
|
it 'returns 422 with tag field errors' do
|
||||||
sign_in_as(member)
|
sign_in_as(member)
|
||||||
|
|
||||||
post '/posts', params: post_write_params(
|
post '/posts', params: post_write_params(
|
||||||
@@ -714,7 +714,13 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
thumbnail: dummy_upload
|
thumbnail: dummy_upload
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(response).to have_http_status(:bad_request), response.body
|
expect(response).to have_http_status(:unprocessable_entity), response.body
|
||||||
|
expect(json).to include(
|
||||||
|
'type' => 'validation_error',
|
||||||
|
'message' => '入力内容を確認してください.',
|
||||||
|
'base_errors' => [])
|
||||||
|
expect(json.fetch('errors')).to include(
|
||||||
|
'tags' => ['ニコニコ・タグは直接指定できません.'])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -931,7 +937,7 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
category: :nico)
|
category: :nico)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'return 400' do
|
it 'returns 422 with tag field errors' do
|
||||||
sign_in_as(member)
|
sign_in_as(member)
|
||||||
|
|
||||||
put "/posts/#{post_record.id}", params: post_update_params(
|
put "/posts/#{post_record.id}", params: post_update_params(
|
||||||
@@ -939,7 +945,13 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
title: 'updated title',
|
title: 'updated title',
|
||||||
tags: 'nico:nico_tag')
|
tags: 'nico:nico_tag')
|
||||||
|
|
||||||
expect(response).to have_http_status(:bad_request), response.body
|
expect(response).to have_http_status(:unprocessable_entity), response.body
|
||||||
|
expect(json).to include(
|
||||||
|
'type' => 'validation_error',
|
||||||
|
'message' => '入力内容を確認してください.',
|
||||||
|
'base_errors' => [])
|
||||||
|
expect(json.fetch('errors')).to include(
|
||||||
|
'tags' => ['ニコニコ・タグは直接指定できません.'])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -275,6 +275,30 @@ RSpec.describe 'Tags deerjikists API', type: :request do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when a row is invalid' do
|
||||||
|
let(:payload) do
|
||||||
|
[
|
||||||
|
{ platform: '', code: code1 },
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 422 with indexed field errors and does not replace existing deerjikists' do
|
||||||
|
Deerjikist.create!(platform: platform1, code: code1, tag: tag)
|
||||||
|
|
||||||
|
expect {
|
||||||
|
do_request
|
||||||
|
}.not_to change { Deerjikist.where(tag: tag).map { |d| [d.platform, d.code] } }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(json).to include(
|
||||||
|
'type' => 'validation_error',
|
||||||
|
'message' => '入力内容を確認してください.',
|
||||||
|
'base_errors' => [])
|
||||||
|
expect(json.fetch('errors')).to include(
|
||||||
|
'deerjikists.0.platform' => [be_present])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when youtube code is handle' do
|
context 'when youtube code is handle' do
|
||||||
let(:channel_id) { 'UCabcdefghijklmnopqrstuv' }
|
let(:channel_id) { 'UCabcdefghijklmnopqrstuv' }
|
||||||
let(:payload) do
|
let(:payload) do
|
||||||
|
|||||||
@@ -90,12 +90,14 @@ RSpec.describe 'Users', type: :request do
|
|||||||
expect(response).to have_http_status(:unauthorized)
|
expect(response).to have_http_status(:unauthorized)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns 400 when name is blank' do
|
it 'returns 422 when name is blank' do
|
||||||
put "/users/#{user.id}",
|
put "/users/#{user.id}",
|
||||||
params: { name: ' ' },
|
params: { name: ' ' },
|
||||||
headers: auth_headers(user)
|
headers: auth_headers(user)
|
||||||
|
|
||||||
expect(response).to have_http_status(:bad_request)
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(json.fetch('errors')).to include(
|
||||||
|
'name' => ['名前は必須です.'])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'updates name and returns user slice' do
|
it 'updates name and returns user slice' do
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
const api = vi.hoisted (() => ({
|
||||||
|
isApiError: vi.fn (),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock ('@/lib/api', () => api)
|
||||||
|
|
||||||
|
describe ('extractValidationError', () => {
|
||||||
|
it ('extracts field and base errors from 422 validation responses', async () => {
|
||||||
|
api.isApiError.mockReturnValueOnce (true)
|
||||||
|
|
||||||
|
const { extractValidationError } = await import ('@/lib/apiErrors')
|
||||||
|
const validationError = extractValidationError<'name'> ({
|
||||||
|
response: {
|
||||||
|
status: 422,
|
||||||
|
data: {
|
||||||
|
type: 'validation_error',
|
||||||
|
message: '入力内容を確認してください.',
|
||||||
|
errors: { name: ['名前は必須です.'] },
|
||||||
|
base_errors: ['全体エラー'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect (validationError).toEqual ({
|
||||||
|
message: '入力内容を確認してください.',
|
||||||
|
fieldErrors: { name: ['名前は必須です.'] },
|
||||||
|
baseErrors: ['全体エラー'],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it ('preserves dotted field keys for indexed form rows', async () => {
|
||||||
|
api.isApiError.mockReturnValueOnce (true)
|
||||||
|
|
||||||
|
const { extractValidationError } = await import ('@/lib/apiErrors')
|
||||||
|
const validationError = extractValidationError<'deerjikists.0.platform'> ({
|
||||||
|
response: {
|
||||||
|
status: 422,
|
||||||
|
data: {
|
||||||
|
type: 'validation_error',
|
||||||
|
errors: { 'deerjikists.0.platform': ['プラットフォームを入力してください.'] },
|
||||||
|
base_errors: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect (validationError?.fieldErrors).toEqual ({
|
||||||
|
'deerjikists.0.platform': ['プラットフォームを入力してください.'],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it ('does not treat 400 bad requests as form validation errors', async () => {
|
||||||
|
api.isApiError.mockReturnValueOnce (true)
|
||||||
|
|
||||||
|
const { extractValidationError } = await import ('@/lib/apiErrors')
|
||||||
|
const validationError = extractValidationError ({
|
||||||
|
response: {
|
||||||
|
status: 400,
|
||||||
|
data: {
|
||||||
|
type: 'bad_request',
|
||||||
|
message: 'リクエストが不正です.',
|
||||||
|
errors: {},
|
||||||
|
base_errors: ['リクエストが不正です.'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect (validationError).toBeNull ()
|
||||||
|
})
|
||||||
|
|
||||||
|
it ('ignores non-api errors', async () => {
|
||||||
|
api.isApiError.mockReturnValueOnce (false)
|
||||||
|
|
||||||
|
const { extractValidationError } = await import ('@/lib/apiErrors')
|
||||||
|
|
||||||
|
expect (extractValidationError (new Error ('network'))).toBeNull ()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import toCamel from 'camelcase-keys'
|
|
||||||
|
|
||||||
import { isApiError } from '@/lib/api'
|
import { isApiError } from '@/lib/api'
|
||||||
|
|
||||||
export type FieldErrors<T extends string = string> = Partial<Record<T, string[]>>
|
export type FieldErrors<T extends string = string> = Partial<Record<T, string[]>>
|
||||||
@@ -19,8 +17,13 @@ export const extractValidationError = <T extends string = string> (err: unknown)
|
|||||||
if (!(isApiError (err)) || err.response?.status !== 422)
|
if (!(isApiError (err)) || err.response?.status !== 422)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
const data = toCamel ((err.response.data ?? { }) as Record<string, unknown>,
|
const rawData = (err.response.data ?? { }) as Record<string, unknown>
|
||||||
{ deep: true }) as RawValidationError
|
const data: RawValidationError = {
|
||||||
|
type: rawData.type as string | undefined,
|
||||||
|
message: rawData.message as string | undefined,
|
||||||
|
errors: rawData.errors as Record<string, string[]> | undefined,
|
||||||
|
baseErrors: rawData.base_errors as string[] | undefined,
|
||||||
|
}
|
||||||
|
|
||||||
if (data.type !== 'validation_error' && !(data.errors))
|
if (data.type !== 'validation_error' && !(data.errors))
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
|
import { Route, Routes } from 'react-router-dom'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
import DeerjikistDetailPage from '@/pages/deerjikists/DeerjikistDetailPage'
|
||||||
|
import { buildTag } from '@/test/factories'
|
||||||
|
import { renderWithProviders } from '@/test/render'
|
||||||
|
|
||||||
|
const tagsApi = vi.hoisted (() => ({
|
||||||
|
fetchDeerjikistsByTag: vi.fn (),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const api = vi.hoisted (() => ({
|
||||||
|
apiPut: vi.fn (),
|
||||||
|
isApiError: vi.fn (),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const toastApi = vi.hoisted (() => ({
|
||||||
|
toast: vi.fn (),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock ('@/lib/tags', () => tagsApi)
|
||||||
|
vi.mock ('@/lib/api', () => api)
|
||||||
|
vi.mock ('@/components/ui/use-toast', () => toastApi)
|
||||||
|
|
||||||
|
const renderPage = () =>
|
||||||
|
renderWithProviders (
|
||||||
|
<Routes>
|
||||||
|
<Route path="/tags/:id/deerjikists" element={<DeerjikistDetailPage/>}/>
|
||||||
|
</Routes>,
|
||||||
|
{ route: '/tags/7/deerjikists' },
|
||||||
|
)
|
||||||
|
|
||||||
|
describe ('DeerjikistDetailPage', () => {
|
||||||
|
beforeEach (() => {
|
||||||
|
vi.clearAllMocks ()
|
||||||
|
api.isApiError.mockReturnValue (false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it ('shows indexed validation errors returned for deerjikist rows', async () => {
|
||||||
|
tagsApi.fetchDeerjikistsByTag.mockResolvedValueOnce ({
|
||||||
|
tag: buildTag ({ id: 7, name: 'deerjika', category: 'deerjikist' }),
|
||||||
|
deerjikists: [{ platform: null, code: 'abc' }],
|
||||||
|
})
|
||||||
|
api.isApiError.mockReturnValue (true)
|
||||||
|
api.apiPut.mockRejectedValueOnce ({
|
||||||
|
response: {
|
||||||
|
status: 422,
|
||||||
|
data: {
|
||||||
|
type: 'validation_error',
|
||||||
|
message: '入力内容を確認してください.',
|
||||||
|
errors: { 'deerjikists.0.platform': ['プラットフォームを入力してください.'] },
|
||||||
|
base_errors: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
renderPage ()
|
||||||
|
|
||||||
|
await screen.findByDisplayValue ('abc')
|
||||||
|
fireEvent.submit (screen.getByRole ('button', { name: '更新' }).closest ('form')!)
|
||||||
|
|
||||||
|
await waitFor (() => {
|
||||||
|
expect (api.apiPut).toHaveBeenCalledWith (
|
||||||
|
'/tags/7/deerjikists',
|
||||||
|
[{ platform: null, code: 'abc' }],
|
||||||
|
)
|
||||||
|
})
|
||||||
|
expect (await screen.findByText ('プラットフォームを入力してください.')).toBeInTheDocument ()
|
||||||
|
expect (screen.getByRole ('combobox')).toHaveAttribute ('aria-invalid', 'true')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
import MaterialNewPage from '@/pages/materials/MaterialNewPage'
|
import MaterialNewPage from '@/pages/materials/MaterialNewPage'
|
||||||
import { renderWithProviders } from '@/test/render'
|
import { renderWithProviders } from '@/test/render'
|
||||||
|
|
||||||
const api = vi.hoisted (() => ({
|
const api = vi.hoisted (() => ({
|
||||||
|
apiGet: vi.fn (),
|
||||||
apiPost: vi.fn (),
|
apiPost: vi.fn (),
|
||||||
|
isApiError: vi.fn (),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const toastApi = vi.hoisted (() => ({
|
const toastApi = vi.hoisted (() => ({
|
||||||
@@ -16,6 +18,12 @@ vi.mock ('@/lib/api', () => api)
|
|||||||
vi.mock ('@/components/ui/use-toast', () => toastApi)
|
vi.mock ('@/components/ui/use-toast', () => toastApi)
|
||||||
|
|
||||||
describe ('MaterialNewPage', () => {
|
describe ('MaterialNewPage', () => {
|
||||||
|
beforeEach (() => {
|
||||||
|
vi.clearAllMocks ()
|
||||||
|
api.apiGet.mockResolvedValue ([])
|
||||||
|
api.isApiError.mockReturnValue (false)
|
||||||
|
})
|
||||||
|
|
||||||
it ('initializes tag from query and submits form data', async () => {
|
it ('initializes tag from query and submits form data', async () => {
|
||||||
api.apiPost.mockResolvedValueOnce ({})
|
api.apiPost.mockResolvedValueOnce ({})
|
||||||
|
|
||||||
@@ -35,4 +43,30 @@ describe ('MaterialNewPage', () => {
|
|||||||
expect (formData.get ('url')).toBe ('https://example.com/ref')
|
expect (formData.get ('url')).toBe ('https://example.com/ref')
|
||||||
expect (toastApi.toast).toHaveBeenCalledWith ({ title: '送信成功!' })
|
expect (toastApi.toast).toHaveBeenCalledWith ({ title: '送信成功!' })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
import PostNewPage from '@/pages/posts/PostNewPage'
|
import PostNewPage from '@/pages/posts/PostNewPage'
|
||||||
import { buildUser } from '@/test/factories'
|
import { buildUser } from '@/test/factories'
|
||||||
@@ -8,6 +8,7 @@ import { renderWithProviders } from '@/test/render'
|
|||||||
const api = vi.hoisted (() => ({
|
const api = vi.hoisted (() => ({
|
||||||
apiGet: vi.fn (),
|
apiGet: vi.fn (),
|
||||||
apiPost: vi.fn (),
|
apiPost: vi.fn (),
|
||||||
|
isApiError: vi.fn (),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const toastApi = vi.hoisted (() => ({
|
const toastApi = vi.hoisted (() => ({
|
||||||
@@ -18,6 +19,11 @@ vi.mock ('@/lib/api', () => api)
|
|||||||
vi.mock ('@/components/ui/use-toast', () => toastApi)
|
vi.mock ('@/components/ui/use-toast', () => toastApi)
|
||||||
|
|
||||||
describe ('PostNewPage', () => {
|
describe ('PostNewPage', () => {
|
||||||
|
beforeEach (() => {
|
||||||
|
vi.clearAllMocks ()
|
||||||
|
api.isApiError.mockReturnValue (false)
|
||||||
|
})
|
||||||
|
|
||||||
it ('blocks guests', () => {
|
it ('blocks guests', () => {
|
||||||
renderWithProviders (<PostNewPage user={buildUser ({ role: 'guest' })}/>)
|
renderWithProviders (<PostNewPage user={buildUser ({ role: 'guest' })}/>)
|
||||||
|
|
||||||
@@ -55,4 +61,36 @@ describe ('PostNewPage', () => {
|
|||||||
expect (formData.get ('tags')).toBe ('tag1 tag2')
|
expect (formData.get ('tags')).toBe ('tag1 tag2')
|
||||||
expect (toastApi.toast).toHaveBeenCalledWith ({ title: '投稿成功!' })
|
expect (toastApi.toast).toHaveBeenCalledWith ({ title: '投稿成功!' })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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' })}/>)
|
||||||
|
|
||||||
|
const checkboxes = screen.getAllByRole ('checkbox', { name: '自動' })
|
||||||
|
fireEvent.click (checkboxes[0])
|
||||||
|
fireEvent.click (checkboxes[1])
|
||||||
|
|
||||||
|
const textboxes = screen.getAllByRole ('textbox')
|
||||||
|
fireEvent.change (textboxes[0], { target: { value: 'https://example.com/post' } })
|
||||||
|
fireEvent.change (textboxes[1], { target: { value: '投稿タイトル' } })
|
||||||
|
fireEvent.change (textboxes[3], { target: { value: 'nico:nico_tag' } })
|
||||||
|
fireEvent.click (screen.getByRole ('button', { name: '追加' }))
|
||||||
|
|
||||||
|
expect (await screen.findByText ('投稿内容を確認してください.')).toBeInTheDocument ()
|
||||||
|
expect (screen.getByText ('ニコニコ・タグは直接指定できません.')).toBeInTheDocument ()
|
||||||
|
expect (screen.getAllByRole ('textbox')[3]).toHaveAttribute ('aria-invalid', 'true')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||||
import { Route, Routes } from 'react-router-dom'
|
import { Route, Routes } from 'react-router-dom'
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
import TagDetailPage from '@/pages/tags/TagDetailPage'
|
import TagDetailPage from '@/pages/tags/TagDetailPage'
|
||||||
import { buildTag } from '@/test/factories'
|
import { buildTag } from '@/test/factories'
|
||||||
@@ -12,6 +12,7 @@ const tagsApi = vi.hoisted (() => ({
|
|||||||
|
|
||||||
const api = vi.hoisted (() => ({
|
const api = vi.hoisted (() => ({
|
||||||
apiPut: vi.fn (),
|
apiPut: vi.fn (),
|
||||||
|
isApiError: vi.fn (),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const toastApi = vi.hoisted (() => ({
|
const toastApi = vi.hoisted (() => ({
|
||||||
@@ -31,6 +32,11 @@ const renderPage = () =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
describe ('TagDetailPage', () => {
|
describe ('TagDetailPage', () => {
|
||||||
|
beforeEach (() => {
|
||||||
|
vi.clearAllMocks ()
|
||||||
|
api.isApiError.mockReturnValue (false)
|
||||||
|
})
|
||||||
|
|
||||||
it ('loads and displays an editable tag', async () => {
|
it ('loads and displays an editable tag', async () => {
|
||||||
tagsApi.fetchTag.mockResolvedValueOnce (
|
tagsApi.fetchTag.mockResolvedValueOnce (
|
||||||
buildTag ({ id: 7, name: '虹夏', category: 'character', aliases: ['drums'] }),
|
buildTag ({ id: 7, name: '虹夏', category: 'character', aliases: ['drums'] }),
|
||||||
@@ -68,4 +74,28 @@ describe ('TagDetailPage', () => {
|
|||||||
|
|
||||||
expect (await screen.findByRole ('button', { name: '更新' })).toBeDisabled ()
|
expect (await screen.findByRole ('button', { name: '更新' })).toBeDisabled ()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it ('shows validation errors returned for tag fields', async () => {
|
||||||
|
tagsApi.fetchTag.mockResolvedValueOnce (buildTag ({ id: 7, name: 'old' }))
|
||||||
|
api.isApiError.mockReturnValue (true)
|
||||||
|
api.apiPut.mockRejectedValueOnce ({
|
||||||
|
response: {
|
||||||
|
status: 422,
|
||||||
|
data: {
|
||||||
|
type: 'validation_error',
|
||||||
|
message: '入力内容を確認してください.',
|
||||||
|
errors: { category: ['ニコタグは変更できません.'] },
|
||||||
|
base_errors: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
renderPage ()
|
||||||
|
|
||||||
|
await screen.findByDisplayValue ('old')
|
||||||
|
fireEvent.submit (screen.getByRole ('button', { name: '更新' }).closest ('form')!)
|
||||||
|
|
||||||
|
expect (await screen.findByText ('ニコタグは変更できません.')).toBeInTheDocument ()
|
||||||
|
expect (screen.getByRole ('combobox')).toHaveAttribute ('aria-invalid', 'true')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
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 SettingPage from '@/pages/users/SettingPage'
|
||||||
import { buildUser } from '@/test/factories'
|
import { buildUser } from '@/test/factories'
|
||||||
@@ -7,6 +7,7 @@ import { renderWithProviders } from '@/test/render'
|
|||||||
|
|
||||||
const api = vi.hoisted (() => ({
|
const api = vi.hoisted (() => ({
|
||||||
apiPut: vi.fn (),
|
apiPut: vi.fn (),
|
||||||
|
isApiError: vi.fn (),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const toastApi = vi.hoisted (() => ({
|
const toastApi = vi.hoisted (() => ({
|
||||||
@@ -23,6 +24,11 @@ vi.mock ('@/components/users/InheritDialogue', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
describe ('SettingPage', () => {
|
describe ('SettingPage', () => {
|
||||||
|
beforeEach (() => {
|
||||||
|
vi.clearAllMocks ()
|
||||||
|
api.isApiError.mockReturnValue (false)
|
||||||
|
})
|
||||||
|
|
||||||
it ('shows loading when user is absent', () => {
|
it ('shows loading when user is absent', () => {
|
||||||
renderWithProviders (<SettingPage user={null} setUser={vi.fn ()}/>)
|
renderWithProviders (<SettingPage user={null} setUser={vi.fn ()}/>)
|
||||||
|
|
||||||
@@ -51,4 +57,28 @@ describe ('SettingPage', () => {
|
|||||||
expect (setUser).toHaveBeenCalled ()
|
expect (setUser).toHaveBeenCalled ()
|
||||||
expect (toastApi.toast).toHaveBeenCalledWith ({ title: '設定を更新しました.' })
|
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')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする