197 行
9.2 KiB
TypeScript
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 ()
|
|
})
|
|
})
|