ファイル
btrc-hub/frontend/src/pages/wiki/WikiDetailPage.test.tsx
T
2026-10-19 20:56:07 +09:00

197 行
9.2 KiB
TypeScript

import { act, fireEvent, screen, waitFor, within } from '@testing-library/react'
import { Route, Routes, useLocation } from 'react-router-dom'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { apiPost } from '@/lib/api'
import { getHelpTitleIndex } from '@/lib/help'
import { tagsKeys, wikiKeys } from '@/lib/queryKeys'
import WikiDetailPage from '@/pages/wiki/WikiDetailPage'
import WikiNewPage from '@/pages/wiki/WikiNewPage'
import { buildTag, buildUser, buildWikiPage } from '@/test/factories'
import { renderWithProviders } from '@/test/render'
const wikiApi = vi.hoisted (() => ({
fetchWikiPage: vi.fn (),
fetchWikiPageByTitle: vi.fn (),
fetchWikiPages: vi.fn (),
}))
const tagsApi = vi.hoisted (() => ({
fetchTagByName: vi.fn (),
}))
const postsApi = vi.hoisted (() => ({
fetchPosts: vi.fn (),
}))
vi.mock ('@/lib/wiki', () => ({
...wikiApi,
fetchWikiAutolinkCandidates: async () => [],
fetchWikiPageByReference: (reference: string, ...args: unknown[]) =>
/^\d+$/.test (reference)
? wikiApi.fetchWikiPage (reference, ...args)
: wikiApi.fetchWikiPageByTitle (reference, ...args) }))
vi.mock ('@/lib/tags', () => tagsApi)
vi.mock ('@/lib/posts', () => postsApi)
vi.mock ('@/lib/help', () => ({ getHelpTitleIndex: vi.fn () }))
vi.mock ('@/lib/api', async importOriginal => ({
...await importOriginal<typeof import ('@/lib/api')> (), apiPost: vi.fn () }))
vi.mock ('@/components/ui/use-toast', () => ({ toast: vi.fn () }))
vi.mock ('react-markdown-editor-lite', () => ({
default: ({ value, onChange }: {
value: string
onChange: (event: { text: string }) => void }) => (
<textarea aria-label="本文エディタ" value={value}
onChange={event => onChange ({ text: event.target.value })}/>) }))
const Location = () => {
const location = useLocation ()
return <output data-testid="location">{location.pathname}{location.search}</output>
}
describe ('WikiDetailPage', () => {
beforeEach (() => {
wikiApi.fetchWikiPageByTitle.mockReset ().mockResolvedValue (null)
wikiApi.fetchWikiPages.mockReset ().mockResolvedValue ([])
tagsApi.fetchTagByName.mockReset ().mockResolvedValue (null)
postsApi.fetchPosts.mockReset ().mockResolvedValue ({ posts: [], count: 0 })
vi.mocked (getHelpTitleIndex).mockReset ().mockResolvedValue ({ })
vi.mocked (apiPost).mockReset ()
})
it ('offers creation for an existing tag after the Help lookup completes', async () => {
let finish!: (index: Record<string, string[]>) => void
const pending = new Promise<Record<string, string[]>> (resolve => {
finish = resolve
})
vi.mocked (getHelpTitleIndex).mockReturnValueOnce (pending)
tagsApi.fetchTagByName.mockResolvedValue (buildTag ({ name: '既存タグ', hasWiki: false }))
renderWithProviders (
<Routes><Route path="/ja/wiki/:title" element={<WikiDetailPage/>}/></Routes>,
{ route: `/ja/wiki/${ encodeURIComponent ('既存タグ') }` })
await waitFor (() => expect (getHelpTitleIndex).toHaveBeenCalled ())
expect (screen.queryByText ('404')).not.toBeInTheDocument ()
expect (screen.queryByRole ('link', { name: '新規作成してください' }))
.not.toBeInTheDocument ()
await act (async () => finish ({ }))
expect (await screen.findByRole ('link', { name: '新規作成してください' }))
.toHaveAttribute ('href', `/ja/wiki/new?title=${ encodeURIComponent ('既存タグ') }`)
})
it ('creates a tag description through the existing form and displays its content', async () => {
const page = buildWikiPage ({ title: '既存タグ', body: '説明文',
resourceType: 'tag_description' })
tagsApi.fetchTagByName.mockResolvedValue (buildTag ({ name: page.title, hasWiki: false }))
vi.mocked (apiPost).mockResolvedValueOnce (page)
const path = `/ja/wiki/${ encodeURIComponent (page.title) }`
const { container, queryClient } = renderWithProviders (
<>
<Location/>
<Routes>
<Route path="/ja/wiki/new" element={<WikiNewPage user={buildUser ({ role: 'member' })}/>}/>
<Route path="/ja/wiki/:title" element={<WikiDetailPage/>}/>
</Routes>
</>, { route: path })
fireEvent.click (await screen.findByRole ('link', { name: '新規作成してください' }))
await screen.findByRole ('heading', { name: '新規 Wiki ページ' })
expect (queryClient.getQueryData (wikiKeys.show (page.title, { }))).toBeNull ()
queryClient.setQueryData (wikiKeys.index ({ }), [])
wikiApi.fetchWikiPageByTitle.mockResolvedValue (page)
wikiApi.fetchWikiPages.mockResolvedValue ([page])
tagsApi.fetchTagByName.mockResolvedValue (buildTag ({ name: page.title, hasWiki: true }))
expect (screen.getAllByRole ('textbox')[0]).toHaveValue (page.title)
fireEvent.change (screen.getByRole ('textbox', { name: '本文エディタ' }), {
target: { value: page.body } })
fireEvent.click (screen.getByRole ('button', { name: '追加' }))
await screen.findByRole ('heading', { name: page.title })
expect (screen.getByTestId ('location')).toHaveTextContent (path)
expect (screen.queryByRole ('textbox', { name: '本文エディタ' })).not.toBeInTheDocument ()
expect (within (container.querySelector ('article')!).getByText (page.body))
.toBeInTheDocument ()
const form = vi.mocked (apiPost).mock.calls[0][1] as FormData
expect (form.get ('title')).toBe (page.title)
expect (form.get ('body')).toBe (page.body)
expect (apiPost).toHaveBeenCalledWith ('/wiki', form,
{ locale: 'ja', headers: { 'Content-Type': 'multipart/form-data' } })
expect (screen.queryByText ('404')).not.toBeInTheDocument ()
expect (queryClient.getQueryData (wikiKeys.show (page.title, { }))).toEqual (page)
expect (queryClient.getQueryState (wikiKeys.index ({ }))?.isInvalidated).toBe (true)
await waitFor (() => expect (queryClient.getQueryData (tagsKeys.show (page.title)))
.toMatchObject ({ hasWiki: true }))
expect (getHelpTitleIndex).toHaveBeenCalledTimes (1)
})
it ('keeps failed saves on the form without changing cached Wiki or tag state', async () => {
const title = '既存タグ'
const tag = buildTag ({ name: title, hasWiki: false })
const path = `/ja/wiki/new?title=${ encodeURIComponent (title) }`
vi.mocked (apiPost).mockRejectedValueOnce (new Error ('save failed'))
const { queryClient } = renderWithProviders (
<><Location/><WikiNewPage user={buildUser ({ role: 'member' })}/></>, { route: path })
queryClient.setQueryData (wikiKeys.show (title, { }), null)
queryClient.setQueryData (wikiKeys.index ({ }), [])
queryClient.setQueryData (tagsKeys.show (title), tag)
fireEvent.change (screen.getByRole ('textbox', { name: '本文エディタ' }), {
target: { value: '保存しない本文' } })
await act (async () => fireEvent.click (screen.getByRole ('button', { name: '追加' })))
expect (apiPost).toHaveBeenCalledTimes (1)
expect (screen.getByTestId ('location')).toHaveTextContent (path)
expect (screen.getByRole ('heading', { name: '新規 Wiki ページ' })).toBeInTheDocument ()
expect (queryClient.getQueryData (wikiKeys.show (title, { }))).toBeNull ()
expect (queryClient.getQueryData (tagsKeys.show (title))).toEqual (tag)
expect (queryClient.getQueryState (wikiKeys.index ({ }))?.isInvalidated).toBe (false)
})
it ('shows NotFound when neither a tag nor a Help title exists', async () => {
renderWithProviders (
<Routes><Route path="/ja/wiki/:title" element={<WikiDetailPage/>}/></Routes>,
{ route: '/ja/wiki/missing' })
expect (await screen.findByText ('404')).toBeInTheDocument ()
expect (screen.queryByRole ('link', { name: '新規作成してください' }))
.not.toBeInTheDocument ()
})
it.each (['403', '500', 'offline']) ('keeps Wiki error %s distinct from missing content',
async message => {
wikiApi.fetchWikiPageByTitle.mockRejectedValue (new Error (message))
tagsApi.fetchTagByName.mockResolvedValue (buildTag ({ name: '既存タグ' }))
renderWithProviders (
<Routes><Route path="/ja/wiki/:title" element={<WikiDetailPage/>}/></Routes>,
{ route: `/ja/wiki/${ encodeURIComponent ('既存タグ') }` })
expect (await screen.findByRole ('alert')).toHaveTextContent (message)
expect (getHelpTitleIndex).not.toHaveBeenCalled ()
expect (screen.queryByRole ('link', { name: '新規作成してください' }))
.not.toBeInTheDocument ()
})
it ('renders deprecated state outside the wiki title link', async () => {
wikiApi.fetchWikiPageByTitle.mockResolvedValueOnce (buildWikiPage ({
title: '旧タグ',
deprecatedAt: '2026-06-01T00:00:00.000Z',
}))
tagsApi.fetchTagByName.mockResolvedValueOnce (buildTag ({
name: '旧タグ',
deprecatedAt: '2026-06-01T00:00:00.000Z',
}))
postsApi.fetchPosts.mockResolvedValueOnce ({ posts: [], count: 0 })
renderWithProviders (
<Routes>
<Route path="/ja/wiki/:title" element={<WikiDetailPage/>}/>
</Routes>,
{ route: '/ja/wiki/%E6%97%A7%E3%82%BF%E3%82%B0' },
)
const marker = await screen.findByText ('(廃止)')
const heading = marker.closest ('h1')
const link = screen.getByRole ('link', { name: '旧タグ' })
expect (heading).not.toBeNull ()
expect (heading!).toHaveTextContent ('旧タグ(廃止)')
expect (link).toBeInTheDocument ()
expect (marker.closest ('a')).toBeNull ()
})
})