このコミットが含まれているのは:
@@ -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 ()
|
||||
})
|
||||
})
|
||||
@@ -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: '更新成功!' })
|
||||
})
|
||||
})
|
||||
@@ -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',
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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: '送信成功!' })
|
||||
})
|
||||
})
|
||||
@@ -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 ()
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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: '投稿成功!' })
|
||||
})
|
||||
})
|
||||
@@ -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 ()
|
||||
})
|
||||
})
|
||||
@@ -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 ()
|
||||
})
|
||||
})
|
||||
@@ -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 ()
|
||||
})
|
||||
})
|
||||
@@ -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: '設定を更新しました.' })
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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: '投稿成功!' })
|
||||
})
|
||||
})
|
||||
@@ -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: '投稿成功!' })
|
||||
})
|
||||
})
|
||||
@@ -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 ()
|
||||
})
|
||||
})
|
||||
新しい課題から参照
ユーザをブロックする