このコミットが含まれているのは:
@@ -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 ()
|
||||
})
|
||||
})
|
||||
新しい課題から参照
ユーザをブロックする