このコミットが含まれているのは:
2026-05-13 20:42:25 +09:00
コミット 0a13c00f37
48個のファイルの変更2378行の追加7行の削除
+26
ファイルの表示
@@ -0,0 +1,26 @@
import { screen } from '@testing-library/react'
import { Route, Routes } from 'react-router-dom'
import { describe, expect, it, vi } from 'vitest'
import MaterialBasePage from '@/pages/materials/MaterialBasePage'
import { renderWithProviders } from '@/test/render'
vi.mock ('@/components/MaterialSidebar', () => ({
default: () => <aside>Material sidebar</aside>,
}))
describe ('MaterialBasePage', () => {
it ('renders the material sidebar and nested route outlet', () => {
renderWithProviders (
<Routes>
<Route path="/materials" element={<MaterialBasePage/>}>
<Route index element={<div>Outlet content</div>}/>
</Route>
</Routes>,
{ route: '/materials' },
)
expect (screen.getByText ('Material sidebar')).toBeInTheDocument ()
expect (screen.getByText ('Outlet content')).toBeInTheDocument ()
})
})
+86
ファイルの表示
@@ -0,0 +1,86 @@
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { Route, Routes } from 'react-router-dom'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import MaterialDetailPage from '@/pages/materials/MaterialDetailPage'
import { buildMaterial, buildTag } from '@/test/factories'
import { renderWithProviders } from '@/test/render'
const api = vi.hoisted (() => ({
apiGet: vi.fn (),
apiPut: vi.fn (),
}))
const wikiApi = vi.hoisted (() => ({
fetchWikiPages: vi.fn (),
}))
const toastApi = vi.hoisted (() => ({
toast: vi.fn (),
}))
vi.mock ('@/lib/api', () => api)
vi.mock ('@/lib/wiki', () => wikiApi)
vi.mock ('@/components/ui/use-toast', () => toastApi)
const renderPage = () =>
renderWithProviders (
<Routes>
<Route path="/materials/:id" element={<MaterialDetailPage/>}/>
</Routes>,
{ route: '/materials/8' },
)
describe ('MaterialDetailPage', () => {
beforeEach (() => {
vi.clearAllMocks ()
api.apiGet.mockResolvedValue ([])
wikiApi.fetchWikiPages.mockResolvedValue ([])
vi.stubGlobal ('fetch', vi.fn (async () => ({
blob: async () => new Blob (['image'], { type: 'image/png' }),
})))
})
it ('loads and displays material detail', async () => {
api.apiGet.mockResolvedValueOnce (
buildMaterial ({
id: 8,
tag: buildTag ({ name: '素材タグ' }),
file: 'image.png',
contentType: 'image/png',
}),
)
renderPage ()
await waitFor (() => {
expect (api.apiGet).toHaveBeenCalledWith ('/materials/8')
})
expect (await screen.findByAltText ('素材タグ')).toHaveAttribute ('src', 'image.png')
})
it ('submits edited material fields', async () => {
api.apiGet.mockResolvedValueOnce (
buildMaterial ({ id: 8, tag: buildTag ({ name: 'old' }), url: '' }),
)
api.apiPut.mockResolvedValueOnce (
buildMaterial ({ id: 8, tag: buildTag ({ name: 'new' }) }),
)
renderPage ()
fireEvent.click (await screen.findByText ('編輯'))
const textboxes = screen.getAllByRole ('textbox')
fireEvent.change (textboxes[0], { target: { value: 'new' } })
fireEvent.change (textboxes[1], { target: { value: 'https://example.com/ref' } })
fireEvent.click (screen.getByRole ('button', { name: '更新' }))
await waitFor (() => {
expect (api.apiPut).toHaveBeenCalledWith ('/materials/8', expect.any (FormData))
})
const formData = api.apiPut.mock.calls[0]?.[1] as FormData
expect (formData.get ('tag')).toBe ('new')
expect (formData.get ('url')).toBe ('https://example.com/ref')
expect (toastApi.toast).toHaveBeenCalledWith ({ title: '更新成功!' })
})
})
+62
ファイルの表示
@@ -0,0 +1,62 @@
import { screen, waitFor } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import MaterialListPage from '@/pages/materials/MaterialListPage'
import { buildMaterial, buildTag } from '@/test/factories'
import { renderWithProviders } from '@/test/render'
const api = vi.hoisted (() => ({
apiGet: vi.fn (),
}))
vi.mock ('@/lib/api', () => api)
describe ('MaterialListPage', () => {
it ('shows the empty selection guide without a tag query', () => {
renderWithProviders (<MaterialListPage/>, { route: '/materials' })
expect (screen.getByText ('左のリストから照会したいタグを選択してください。')).toBeInTheDocument ()
expect (screen.getByRole ('link', { name: '素材を新規追加する' })).toHaveAttribute (
'href',
'/materials/new',
)
})
it ('loads materials for a tag query', async () => {
const tag = {
...buildTag ({
id: 4,
name: '素材タグ',
category: 'material',
}),
material: buildMaterial ({ id: 8, contentType: 'image/png', file: 'image.png' }),
children: [],
}
api.apiGet.mockResolvedValueOnce (tag)
renderWithProviders (<MaterialListPage/>, { route: '/materials?tag=%E7%B4%A0%E6%9D%90' })
await waitFor (() => {
expect (api.apiGet).toHaveBeenCalledWith (
'/tags/name/%E7%B4%A0%E6%9D%90/materials',
)
})
expect (await screen.findByRole ('link', { name: '素材タグ' })).toBeInTheDocument ()
expect (screen.getByRole ('link', { name: '' })).toHaveAttribute ('href', '/materials/8')
})
it ('offers adding a missing non-meme material', async () => {
api.apiGet.mockResolvedValueOnce ({
...buildTag ({ name: '未登録', category: 'material' }),
material: null,
children: [],
})
renderWithProviders (<MaterialListPage/>, { route: '/materials?tag=x' })
expect (await screen.findByRole ('link', { name: '追加' })).toHaveAttribute (
'href',
'/materials/new?tag=%E6%9C%AA%E7%99%BB%E9%8C%B2',
)
})
})
+38
ファイルの表示
@@ -0,0 +1,38 @@
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import MaterialNewPage from '@/pages/materials/MaterialNewPage'
import { renderWithProviders } from '@/test/render'
const api = vi.hoisted (() => ({
apiPost: vi.fn (),
}))
const toastApi = vi.hoisted (() => ({
toast: vi.fn (),
}))
vi.mock ('@/lib/api', () => api)
vi.mock ('@/components/ui/use-toast', () => toastApi)
describe ('MaterialNewPage', () => {
it ('initializes tag from query and submits form data', async () => {
api.apiPost.mockResolvedValueOnce ({})
renderWithProviders (<MaterialNewPage/>, { route: '/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))
})
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).toHaveBeenCalledWith ({ title: '送信成功!' })
})
})
+109
ファイルの表示
@@ -0,0 +1,109 @@
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { Route, Routes } from 'react-router-dom'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import PostDetailPage from '@/pages/posts/PostDetailPage'
import { buildPost, buildUser } from '@/test/factories'
import { renderWithProviders } from '@/test/render'
import type { ReactNode } from 'react'
const postsApi = vi.hoisted (() => ({
fetchPost: vi.fn (),
toggleViewedFlg: vi.fn (),
}))
const api = vi.hoisted (() => ({
isApiError: vi.fn (() => false),
}))
vi.mock ('@/lib/posts', () => postsApi)
vi.mock ('@/lib/api', () => api)
vi.mock ('@/components/PostEmbed', () => ({
default: ({ post }: { post: { url: string } }) => <div>Embed:{post.url}</div>,
}))
vi.mock ('@/components/TagDetailSidebar', () => ({
default: () => <aside>Tag sidebar</aside>,
}))
vi.mock ('@/components/PostEditForm', () => ({
default: () => <div>Post edit form</div>,
}))
vi.mock ('framer-motion', () => ({
motion: {
div: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
main: ({ children }: { children?: ReactNode }) => <main>{children}</main>,
},
}))
const renderPage = (user = buildUser ({ role: 'member' })) =>
renderWithProviders (
<Routes>
<Route path="/posts/:id" element={<PostDetailPage user={user}/>}/>
</Routes>,
{ route: '/posts/9' },
)
describe ('PostDetailPage', () => {
beforeEach (() => {
vi.clearAllMocks ()
postsApi.toggleViewedFlg.mockResolvedValue (undefined)
})
it ('loads and displays a post detail', async () => {
postsApi.fetchPost.mockResolvedValue (
buildPost ({
id: 9,
url: 'https://example.com/9',
related: [],
thumbnail: null,
thumbnailBase: null,
}),
)
renderPage ()
await waitFor (() => {
expect (postsApi.fetchPost).toHaveBeenCalledWith ('9')
})
expect (await screen.findByText ('Embed:https://example.com/9')).toBeInTheDocument ()
expect (screen.getByRole ('button', { name: '未閲覧' })).toBeInTheDocument ()
expect (screen.getByText ('まだないよ(笑)')).toBeInTheDocument ()
})
it ('toggles viewed state through the mutation', async () => {
postsApi.fetchPost.mockResolvedValue (
buildPost ({ id: 9, viewed: false, thumbnail: null, thumbnailBase: null }),
)
renderPage ()
fireEvent.click (await screen.findByRole ('button', { name: '未閲覧' }))
await waitFor (() => {
expect (postsApi.toggleViewedFlg).toHaveBeenCalledWith ('9', true)
})
})
it ('shows the edit tab for members', async () => {
postsApi.fetchPost.mockResolvedValue (
buildPost ({ id: 9, thumbnail: null, thumbnailBase: null }),
)
renderPage (buildUser ({ role: 'member' }))
fireEvent.click (await screen.findByText ('編輯'))
expect (screen.getByText ('Post edit form')).toBeInTheDocument ()
})
it ('hides the edit tab for guests', async () => {
postsApi.fetchPost.mockResolvedValue (
buildPost ({ id: 9, thumbnail: null, thumbnailBase: null }),
)
renderPage (buildUser ({ role: 'guest' }))
expect (await screen.findByText ('関聯')).toBeInTheDocument ()
expect (screen.queryByText ('編輯')).not.toBeInTheDocument ()
})
})
+5 -7
ファイルの表示
@@ -44,17 +44,15 @@ const PostDetailPage: FC<Props> = ({ user }) => {
const [status, setStatus] = useState (200)
const changeViewedFlg = useMutation ({
mutationFn: async () => {
const cur = qc.getQueryData<Post> (postKey)
const next = !(cur?.viewed)
mutationFn: async (next: boolean) => {
await toggleViewedFlg (postId, next)
return next
},
onMutate: async () => {
onMutate: async (next: boolean) => {
await qc.cancelQueries ({ queryKey: postKey })
const prev = qc.getQueryData<Post> (postKey)
qc.setQueryData (postKey,
(cur: Post | undefined) => cur ? { ...cur, viewed: !(cur.viewed) } : cur)
(cur: Post | undefined) => cur ? { ...cur, viewed: next } : cur)
return { prev }
},
onError: (...[, , ctx]) => {
@@ -155,7 +153,7 @@ const PostDetailPage: FC<Props> = ({ user }) => {
ref={embedRef}
post={post}
onLoadComplete={() => embedRef.current?.play ()}/>
<Button onClick={() => changeViewedFlg.mutate ()}
<Button onClick={() => changeViewedFlg.mutate (!(post.viewed))}
disabled={changeViewedFlg.isPending}
className={cn ('text-white', viewedClass)}>
{post.viewed ? '閲覧済' : '未閲覧'}
@@ -188,4 +186,4 @@ const PostDetailPage: FC<Props> = ({ user }) => {
</div>)
}
export default PostDetailPage
export default PostDetailPage
+74
ファイルの表示
@@ -0,0 +1,74 @@
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import PostListPage from '@/pages/posts/PostListPage'
import { buildPost, buildTag, buildWikiPage } from '@/test/factories'
import { renderWithProviders } from '@/test/render'
const postsApi = vi.hoisted (() => ({
fetchPosts: vi.fn (),
}))
const wikiApi = vi.hoisted (() => ({
fetchWikiPageByTitle: vi.fn (),
fetchWikiPages: vi.fn (),
}))
vi.mock ('@/lib/posts', () => postsApi)
vi.mock ('@/lib/wiki', () => wikiApi)
describe ('PostListPage', () => {
it ('loads posts from the current query and renders the plaza tab', async () => {
const tag = buildTag ({ name: '虹夏' })
postsApi.fetchPosts.mockResolvedValueOnce ({
posts: [buildPost ({ id: 5, title: '投稿5', tags: [tag] })],
count: 1,
})
wikiApi.fetchWikiPageByTitle.mockResolvedValueOnce (buildWikiPage ({ title: '虹夏' }))
wikiApi.fetchWikiPages.mockResolvedValueOnce ([buildWikiPage ({ title: '虹夏' })])
renderWithProviders (<PostListPage/>, { route: '/posts?tags=%E8%99%B9%E5%A4%8F&page=2' })
await waitFor (() => {
expect (postsApi.fetchPosts).toHaveBeenCalledWith (
expect.objectContaining ({
tags: '虹夏',
match: 'all',
page: 2,
limit: 20,
}),
)
})
expect (await screen.findByRole ('link', { name: '投稿5' })).toHaveAttribute (
'href',
'/posts/5',
)
expect (screen.getByText ('広場')).toBeInTheDocument ()
})
it ('shows the empty state when loading finishes without posts', async () => {
postsApi.fetchPosts.mockResolvedValueOnce ({ posts: [], count: 0 })
renderWithProviders (<PostListPage/>, { route: '/posts' })
expect (await screen.findByText ('広場には何もありませんよ.')).toBeInTheDocument ()
})
it ('renders the wiki tab for single-tag pages', async () => {
postsApi.fetchPosts.mockResolvedValueOnce ({ posts: [], count: 0 })
wikiApi.fetchWikiPageByTitle.mockResolvedValueOnce (
buildWikiPage ({ title: '虹夏', body: 'Wiki body' }),
)
wikiApi.fetchWikiPages.mockResolvedValueOnce ([buildWikiPage ({ title: '虹夏' })])
renderWithProviders (<PostListPage/>, { route: '/posts?tags=%E8%99%B9%E5%A4%8F' })
fireEvent.click (await screen.findByText ('Wiki'))
expect (await screen.findByText ('Wiki body')).toBeInTheDocument ()
expect (screen.getByRole ('link', { name: 'Wiki を見る' })).toHaveAttribute (
'href',
'/wiki/%E8%99%B9%E5%A4%8F',
)
})
})
+58
ファイルの表示
@@ -0,0 +1,58 @@
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
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 (),
}))
const toastApi = vi.hoisted (() => ({
toast: vi.fn (),
}))
vi.mock ('@/lib/api', () => api)
vi.mock ('@/components/ui/use-toast', () => toastApi)
describe ('PostNewPage', () => {
it ('blocks guests', () => {
renderWithProviders (<PostNewPage user={buildUser ({ role: 'guest' })}/>)
expect (screen.getByText ('403')).toBeInTheDocument ()
})
it ('submits a new post with manual title and thumbnail settings', async () => {
api.apiPost.mockResolvedValueOnce ({})
api.apiGet.mockResolvedValue ([])
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[2], { target: { value: '1 2' } })
fireEvent.change (textboxes[3], { 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: '投稿成功!' })
})
})
+84
ファイルの表示
@@ -0,0 +1,84 @@
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import PostSearchPage from '@/pages/posts/PostSearchPage'
import { buildPost, buildTag } from '@/test/factories'
import { renderWithProviders } from '@/test/render'
const postsApi = vi.hoisted (() => ({
fetchPosts: vi.fn (),
}))
const api = vi.hoisted (() => ({
apiGet: vi.fn (),
}))
vi.mock ('@/lib/posts', () => postsApi)
vi.mock ('@/lib/api', () => api)
describe ('PostSearchPage', () => {
beforeEach (() => {
vi.clearAllMocks ()
api.apiGet.mockResolvedValue ([])
})
it ('loads posts from URL search filters', async () => {
postsApi.fetchPosts.mockResolvedValueOnce ({
posts: [buildPost ({ id: 4, title: '検索対象', tags: [buildTag ({ name: '虹夏' })] })],
count: 1,
})
renderWithProviders (
<PostSearchPage/>,
{ route: '/posts/search?title=%E6%A4%9C%E7%B4%A2&tags=x&match=any&page=2' },
)
await waitFor (() => {
expect (postsApi.fetchPosts).toHaveBeenCalledWith (
expect.objectContaining ({
title: '検索',
tags: 'x',
match: 'any',
page: 2,
}),
)
})
expect ((await screen.findAllByRole ('link', { name: '検索対象' }))[0]).toHaveAttribute (
'href',
'/posts/4',
)
})
it ('submits form state into a new search', async () => {
postsApi.fetchPosts.mockResolvedValue ({ posts: [], count: 0 })
renderWithProviders (<PostSearchPage/>, { route: '/posts/search' })
const textboxes = screen.getAllByRole ('textbox')
fireEvent.change (textboxes[0], { target: { value: 'title' } })
fireEvent.change (textboxes[1], { target: { value: 'https://example.com' } })
fireEvent.change (textboxes[2], { target: { value: 'tag' } })
fireEvent.click (screen.getByRole ('radio', { name: 'OR' }))
fireEvent.click (screen.getByRole ('button', { name: '検索' }))
await waitFor (() => {
expect (postsApi.fetchPosts).toHaveBeenLastCalledWith (
expect.objectContaining ({
title: 'title',
url: 'https://example.com',
tags: 'tag',
match: 'any',
page: 1,
}),
)
})
})
it ('shows the no-result message', async () => {
postsApi.fetchPosts.mockResolvedValueOnce ({ posts: [], count: 0 })
renderWithProviders (<PostSearchPage/>, { route: '/posts/search' })
expect (await screen.findByText ('結果ないよ(笑)')).toBeInTheDocument ()
})
})
+71
ファイルの表示
@@ -0,0 +1,71 @@
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { Route, Routes } from 'react-router-dom'
import { describe, expect, it, vi } from 'vitest'
import TagDetailPage from '@/pages/tags/TagDetailPage'
import { buildTag } from '@/test/factories'
import { renderWithProviders } from '@/test/render'
const tagsApi = vi.hoisted (() => ({
fetchTag: vi.fn (),
}))
const api = vi.hoisted (() => ({
apiPut: 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" element={<TagDetailPage/>}/>
</Routes>,
{ route: '/tags/7' },
)
describe ('TagDetailPage', () => {
it ('loads and displays an editable tag', async () => {
tagsApi.fetchTag.mockResolvedValueOnce (
buildTag ({ id: 7, name: '虹夏', category: 'character', aliases: ['drums'] }),
)
renderPage ()
expect (await screen.findByDisplayValue ('虹夏')).toBeInTheDocument ()
expect (screen.getByRole ('combobox')).toHaveValue ('character')
expect (screen.getByDisplayValue ('drums')).toBeInTheDocument ()
})
it ('submits edited tag fields', async () => {
tagsApi.fetchTag.mockResolvedValueOnce (buildTag ({ id: 7, name: 'old' }))
api.apiPut.mockResolvedValueOnce (buildTag ({ id: 7, name: 'new', aliases: ['alias'] }))
renderPage ()
const name = await screen.findByDisplayValue ('old')
fireEvent.change (name, { target: { value: 'new' } })
fireEvent.submit (screen.getByRole ('button', { name: '更新' }).closest ('form')!)
await waitFor (() => {
expect (api.apiPut).toHaveBeenCalledWith ('/tags/7', expect.any (FormData))
})
const formData = api.apiPut.mock.calls[0]?.[1] as FormData
expect (formData.get ('name')).toBe ('new')
expect (toastApi.toast).toHaveBeenCalledWith ({ description: '更新しました.' })
})
it ('keeps nico tags disabled', async () => {
tagsApi.fetchTag.mockResolvedValueOnce (buildTag ({ category: 'nico' }))
renderPage ()
expect (await screen.findByRole ('button', { name: '更新' })).toBeDisabled ()
})
})
+71
ファイルの表示
@@ -0,0 +1,71 @@
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import TagListPage from '@/pages/tags/TagListPage'
import { buildTag } from '@/test/factories'
import { renderWithProviders } from '@/test/render'
const tagsApi = vi.hoisted (() => ({
fetchTags: vi.fn (),
}))
vi.mock ('@/lib/tags', () => tagsApi)
describe ('TagListPage', () => {
it ('loads tags from URL filters and renders the results table', async () => {
tagsApi.fetchTags.mockResolvedValueOnce ({
tags: [buildTag ({ id: 7, name: '虹夏', category: 'character', postCount: 99 })],
count: 1,
})
renderWithProviders (
<TagListPage/>,
{ route: '/tags?name=%E8%99%B9&category=character&page=3&post_count_gte=5' },
)
await waitFor (() => {
expect (tagsApi.fetchTags).toHaveBeenCalledWith (
expect.objectContaining ({
name: '虹',
category: 'character',
page: 3,
postCountGTE: 5,
}),
)
})
expect (await screen.findByRole ('link', { name: '虹夏' })).toHaveAttribute (
'href',
'/tags/7',
)
expect (screen.getAllByText ('キャラクター').length).toBeGreaterThan (0)
})
it ('navigates to a normalized search URL on submit', async () => {
tagsApi.fetchTags.mockResolvedValue ({ tags: [], count: 0 })
renderWithProviders (<TagListPage/>, { route: '/tags' })
fireEvent.change (screen.getByRole ('textbox'), { target: { value: '虹夏' } })
fireEvent.change (screen.getByRole ('combobox'), { target: { value: 'character' } })
fireEvent.submit (screen.getByRole ('button', { name: '検索' }).closest ('form')!)
await waitFor (() => {
expect (tagsApi.fetchTags).toHaveBeenLastCalledWith (
expect.objectContaining ({
name: '虹夏',
category: 'character',
page: 1,
}),
)
})
expect (screen.getByRole ('textbox')).toHaveValue ('虹夏')
})
it ('shows the no-result message', async () => {
tagsApi.fetchTags.mockResolvedValueOnce ({ tags: [], count: 0 })
renderWithProviders (<TagListPage/>, { route: '/tags' })
expect (await screen.findByText ('結果ないよ(笑)')).toBeInTheDocument ()
})
})
+54
ファイルの表示
@@ -0,0 +1,54 @@
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { 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 (),
}))
const toastApi = vi.hoisted (() => ({
toast: vi.fn (),
}))
vi.mock ('@/lib/api', () => api)
vi.mock ('@/components/ui/use-toast', () => toastApi)
vi.mock ('@/components/users/UserCodeDialogue', () => ({
default: () => null,
}))
vi.mock ('@/components/users/InheritDialogue', () => ({
default: () => null,
}))
describe ('SettingPage', () => {
it ('shows loading when user is absent', () => {
renderWithProviders (<SettingPage user={null} setUser={vi.fn ()}/>)
expect (screen.getByText ('Loading...')).toBeInTheDocument ()
})
it ('updates the current user name', async () => {
const user = buildUser ({ id: 11, name: 'old' })
const setUser = vi.fn ()
api.apiPut.mockResolvedValueOnce ({ ...user, name: 'new' })
renderWithProviders (<SettingPage user={user} setUser={setUser}/>)
fireEvent.change (screen.getByRole ('textbox'), { target: { value: 'new' } })
fireEvent.click (screen.getByRole ('button', { name: '更新' }))
await waitFor (() => {
expect (api.apiPut).toHaveBeenCalledWith (
'/users/11',
expect.any (FormData),
{ headers: { 'Content-Type': 'multipart/form-data' } },
)
})
const formData = api.apiPut.mock.calls[0]?.[1] as FormData
expect (formData.get ('name')).toBe ('new')
expect (setUser).toHaveBeenCalled ()
expect (toastApi.toast).toHaveBeenCalledWith ({ title: '設定を更新しました.' })
})
})
+45
ファイルの表示
@@ -0,0 +1,45 @@
import { screen, waitFor } from '@testing-library/react'
import { Route, Routes } from 'react-router-dom'
import { describe, expect, it, vi } from 'vitest'
import WikiDiffPage from '@/pages/wiki/WikiDiffPage'
import { renderWithProviders } from '@/test/render'
const api = vi.hoisted (() => ({
apiGet: vi.fn (),
}))
vi.mock ('@/lib/api', () => api)
describe ('WikiDiffPage', () => {
it ('fetches and renders wiki diff lines', async () => {
api.apiGet.mockResolvedValueOnce ({
wikiPageId: 3,
title: '差分対象',
olderRevisionId: 1,
newerRevisionId: 2,
diff: [
{ type: 'context', content: 'same' },
{ type: 'added', content: 'added line' },
{ type: 'removed', content: 'removed line' },
],
})
renderWithProviders (
<Routes>
<Route path="/wiki/:id/diff" element={<WikiDiffPage/>}/>
</Routes>,
{ route: '/wiki/3/diff?from=1&to=2' },
)
await waitFor (() => {
expect (api.apiGet).toHaveBeenCalledWith (
'/wiki/3/diff',
{ params: { from: '1', to: '2' } },
)
})
expect (screen.getByText ('差分対象')).toBeInTheDocument ()
expect (screen.getByText ('added line')).toHaveClass ('bg-green-200')
expect (screen.getByText ('removed line')).toHaveClass ('bg-red-200')
})
})
+82
ファイルの表示
@@ -0,0 +1,82 @@
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { Route, Routes } from 'react-router-dom'
import { describe, expect, it, vi } from 'vitest'
import WikiEditPage from '@/pages/wiki/WikiEditPage'
import { buildUser, buildWikiPage } from '@/test/factories'
import { renderWithProviders } from '@/test/render'
const api = vi.hoisted (() => ({
apiGet: vi.fn (),
apiPut: vi.fn (),
}))
const toastApi = vi.hoisted (() => ({
toast: vi.fn (),
}))
vi.mock ('@/lib/api', () => api)
vi.mock ('@/components/ui/use-toast', () => toastApi)
vi.mock ('react-markdown-editor-lite', () => ({
default: ({ value, onChange }: {
value: string
onChange: (event: { text: string }) => void
}) => (
<textarea
aria-label="本文エディタ"
value={value}
onChange={ev => onChange ({ text: ev.target.value })}/>
),
}))
const renderPage = (user = buildUser ({ role: 'member' })) =>
renderWithProviders (
<Routes>
<Route path="/wiki/:id/edit" element={<WikiEditPage user={user}/>}/>
</Routes>,
{ route: '/wiki/9/edit' },
)
describe ('WikiEditPage', () => {
it ('blocks guests', () => {
renderPage (buildUser ({ role: 'guest' }))
expect (screen.getByText ('403')).toBeInTheDocument ()
})
it ('loads the target page for editing', async () => {
api.apiGet.mockResolvedValueOnce (buildWikiPage ({ title: '既存', body: '本文' }))
renderPage ()
await waitFor (() => {
expect (api.apiGet).toHaveBeenCalledWith ('/wiki/9')
})
expect (screen.getAllByRole ('textbox')[0]).toHaveValue ('既存')
expect (screen.getByLabelText ('本文エディタ')).toHaveValue ('本文')
})
it ('submits edited title and body', async () => {
api.apiGet.mockResolvedValueOnce (buildWikiPage ({ title: '既存', body: '本文' }))
api.apiPut.mockResolvedValueOnce ({})
renderPage ()
const title = await screen.findByDisplayValue ('既存')
fireEvent.change (title, { target: { value: '更新済み' } })
fireEvent.change (screen.getByLabelText ('本文エディタ'), { target: { value: '更新本文' } })
fireEvent.click (screen.getByRole ('button', { name: '編輯' }))
await waitFor (() => {
expect (api.apiPut).toHaveBeenCalledWith (
'/wiki/9',
expect.any (FormData),
{ headers: { 'Content-Type': 'multipart/form-data' } },
)
})
const formData = api.apiPut.mock.calls[0]?.[1] as FormData
expect (formData.get ('title')).toBe ('更新済み')
expect (formData.get ('body')).toBe ('更新本文')
expect (toastApi.toast).toHaveBeenCalledWith ({ title: '投稿成功!' })
})
})
+61
ファイルの表示
@@ -0,0 +1,61 @@
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import WikiNewPage from '@/pages/wiki/WikiNewPage'
import { buildUser, buildWikiPage } from '@/test/factories'
import { renderWithProviders } from '@/test/render'
const api = vi.hoisted (() => ({
apiPost: vi.fn (),
}))
const toastApi = vi.hoisted (() => ({
toast: vi.fn (),
}))
vi.mock ('@/lib/api', () => api)
vi.mock ('@/components/ui/use-toast', () => toastApi)
vi.mock ('react-markdown-editor-lite', () => ({
default: ({ value, onChange }: {
value: string
onChange: (event: { text: string }) => void
}) => (
<textarea
aria-label="本文エディタ"
value={value}
onChange={ev => onChange ({ text: ev.target.value })}/>
),
}))
describe ('WikiNewPage', () => {
it ('blocks guests', () => {
renderWithProviders (<WikiNewPage user={buildUser ({ role: 'guest' })}/>)
expect (screen.getByText ('403')).toBeInTheDocument ()
})
it ('creates a wiki page from title query and body', async () => {
api.apiPost.mockResolvedValueOnce (buildWikiPage ({ title: '作成済み' }))
renderWithProviders (
<WikiNewPage user={buildUser ({ role: 'member' })}/>,
{ route: '/wiki/new?title=%E4%B8%8B%E6%9B%B8%E3%81%8D' },
)
expect (screen.getAllByRole ('textbox')[0]).toHaveValue ('下書き')
fireEvent.change (screen.getByLabelText ('本文エディタ'), { target: { value: '本文' } })
fireEvent.click (screen.getByRole ('button', { name: '追加' }))
await waitFor (() => {
expect (api.apiPost).toHaveBeenCalledWith (
'/wiki',
expect.any (FormData),
{ headers: { 'Content-Type': 'multipart/form-data' } },
)
})
const formData = api.apiPost.mock.calls[0]?.[1] as FormData
expect (formData.get ('title')).toBe ('下書き')
expect (formData.get ('body')).toBe ('本文')
expect (toastApi.toast).toHaveBeenCalledWith ({ title: '投稿成功!' })
})
})
+45
ファイルの表示
@@ -0,0 +1,45 @@
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import WikiSearchPage from '@/pages/wiki/WikiSearchPage'
import { buildWikiPage } from '@/test/factories'
import { renderWithProviders } from '@/test/render'
const api = vi.hoisted (() => ({
apiGet: vi.fn (),
}))
vi.mock ('@/lib/api', () => api)
describe ('WikiSearchPage', () => {
it ('loads initial wiki pages and renders links', async () => {
api.apiGet.mockResolvedValueOnce ([buildWikiPage ({ title: '虹夏' })])
renderWithProviders (<WikiSearchPage/>)
expect (await screen.findByRole ('link', { name: '虹夏' })).toHaveAttribute (
'href',
'/wiki/%E8%99%B9%E5%A4%8F',
)
expect (api.apiGet).toHaveBeenCalledWith ('/wiki', { params: { title: '' } })
})
it ('searches by title on submit', async () => {
api.apiGet
.mockResolvedValueOnce ([])
.mockResolvedValueOnce ([buildWikiPage ({ title: '検索結果' })])
renderWithProviders (<WikiSearchPage/>)
fireEvent.change (screen.getAllByRole ('textbox')[0], { target: { value: '検索' } })
fireEvent.click (screen.getByRole ('button', { name: '検索' }))
await waitFor (() => {
expect (api.apiGet).toHaveBeenLastCalledWith (
'/wiki',
{ params: { title: '検索' } },
)
})
expect (await screen.findByRole ('link', { name: '検索結果' })).toBeInTheDocument ()
})
})