このコミットが含まれているのは:
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { createHelpFixture } from '@/test/helpFixtures'
|
||||
|
||||
|
||||
describe ('Help file catalogue', () => {
|
||||
it ('uses current_edition rather than the newest file, without YAML titles', async () => {
|
||||
const { api } = createHelpFixture ()
|
||||
expect (api.getHelpArticle ('guide')?.languages.ja.currentEdition).toBe ('2026-10-02')
|
||||
expect (await api.getHelpSearchEntries ('ja')).toEqual ([
|
||||
{ identifier: 'guide', edition: '2026-10-02',
|
||||
title: 'ホーム', body: '\nタグを検索する' },
|
||||
{ identifier: '設定 手順', edition: '2026-10-03',
|
||||
title: '設定', body: '\nタグを設定する' }])
|
||||
})
|
||||
|
||||
it ('orders same-day editions numerically and separates languages', () => {
|
||||
const { api } = createHelpFixture ()
|
||||
expect (api.getHelpEditions ('guide', 'ja')).toEqual ([
|
||||
'2026-10-01', '2026-10-02', '2026-10-02.2', '2026-10-02.3', '2026-10-02.10'])
|
||||
expect (api.getHelpEditions ('guide', 'en')).toEqual (['2026-10-02'])
|
||||
})
|
||||
|
||||
it ('derives each edition title from its first H1', async () => {
|
||||
const { api, raw } = createHelpFixture ()
|
||||
expect (await api.getHelpEditionText ('guide', 'ja', '2026-10-01'))
|
||||
.toEqual ({ title: '旧ホーム', body: '\n過去限定本文' })
|
||||
raw['../../help/guide/editions/ja/2026-10-02.mdx']
|
||||
.mockResolvedValue ('\uFEFF# 改題\r\n\r\n本文\r\n# 別見出し')
|
||||
expect (await api.getHelpEditionText ('guide', 'ja', '2026-10-02'))
|
||||
.toEqual ({ title: '改題', body: '\r\n本文\r\n# 別見出し' })
|
||||
})
|
||||
|
||||
it ('returns missing resources without substituting another edition', async () => {
|
||||
const { api } = createHelpFixture ()
|
||||
expect (api.getHelpArticle ('absent')).toBeNull ()
|
||||
expect (api.getHelpEditions ('absent', 'ja')).toEqual ([])
|
||||
expect (await api.getHelpEdition ('guide', 'ja', 'missing')).toBeNull ()
|
||||
expect (await api.getHelpEditionText ('guide', 'fr', '2026-10-02')).toBeNull ()
|
||||
})
|
||||
|
||||
it ('rejects a missing current file instead of silently hiding an article', async () => {
|
||||
const { api, raw } = createHelpFixture ()
|
||||
delete raw['../../help/guide/editions/ja/2026-10-02.mdx']
|
||||
await expect (api.getHelpSearchEntries ('ja')).rejects.toThrow ()
|
||||
})
|
||||
|
||||
it ('reports raw loading failures and missing leading H1', async () => {
|
||||
const { api, raw } = createHelpFixture ()
|
||||
const loader = raw['../../help/guide/editions/ja/2026-10-02.mdx']
|
||||
loader.mockRejectedValueOnce (new Error ('fixture failure'))
|
||||
await expect (api.getHelpSearchEntries ('ja')).rejects.toThrow ('fixture failure')
|
||||
loader.mockResolvedValueOnce ('本文\n# 遅い見出し')
|
||||
await expect (api.getHelpEditionText ('guide', 'ja', '2026-10-02')).rejects.toThrow ()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,94 @@
|
||||
import { QueryClient } from '@tanstack/react-query'
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { Route, Routes, useLocation } from 'react-router-dom'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
import * as prefetchers from '@/lib/prefetchers'
|
||||
import HelpDetailPage from '@/pages/help/HelpDetailPage'
|
||||
import { helpFixture } from '@/test/helpFixtures'
|
||||
import { renderWithProviders } from '@/test/render'
|
||||
|
||||
const settings = vi.hoisted (() => ({ linkPreload: 'intent' }))
|
||||
vi.mock ('@/lib/useClientBehaviourSettings', () => ({
|
||||
useClientBehaviourSettings: () => settings }))
|
||||
vi.mock ('@/lib/help', async () => (await import ('@/test/helpFixtures')).helpFixture.api)
|
||||
|
||||
|
||||
const Location = () => {
|
||||
const location = useLocation ()
|
||||
return <output data-testid="location">{location.pathname}</output>
|
||||
}
|
||||
|
||||
|
||||
describe ('Help prefetch integration', () => {
|
||||
beforeEach (() => {
|
||||
settings.linkPreload = 'intent'
|
||||
vi.clearAllMocks ()
|
||||
})
|
||||
|
||||
it.each (['/help', '/help/guide', '/help/changes',
|
||||
'/help/%E8%A8%AD%E5%AE%9A%20%E6%89%8B%E9%A0%86?edition=2026-10-03']) (
|
||||
'registers a unique prefetcher for %s', path => {
|
||||
const url = new URL (path, window.location.origin)
|
||||
expect (prefetchers.routePrefetchers.filter (entry => entry.test (url))).toHaveLength (1)
|
||||
})
|
||||
|
||||
it ('loads and reuses the same MDX module for repeated prefetch and display', async () => {
|
||||
const client = new QueryClient ()
|
||||
const loader = helpFixture.modules['../../help/guide/editions/ja/2026-10-02.mdx']
|
||||
await Promise.all ([
|
||||
prefetchers.prefetchForURL (client, '/help/guide'),
|
||||
prefetchers.prefetchForURL (client, '/help/guide')])
|
||||
expect (loader).toHaveBeenCalledTimes (1)
|
||||
renderWithProviders (
|
||||
<Routes><Route path="/help/:identifier" element={<HelpDetailPage/>}/></Routes>,
|
||||
{ route: '/help/guide' })
|
||||
await screen.findByRole ('heading', { name: 'ホーム' })
|
||||
expect (loader).toHaveBeenCalledTimes (1)
|
||||
})
|
||||
|
||||
it ('prefetches the requested identifier and edition independently', async () => {
|
||||
const client = new QueryClient ()
|
||||
await prefetchers.prefetchForURL (client, '/help/guide?edition=2026-10-02.3')
|
||||
await prefetchers.prefetchForURL (client, `/help/${encodeURIComponent ('設定 手順')}`)
|
||||
expect (helpFixture.modules['../../help/guide/editions/ja/2026-10-02.3.mdx'])
|
||||
.toHaveBeenCalledTimes (1)
|
||||
expect (helpFixture.modules['../../help/設定 手順/editions/ja/2026-10-03.mdx'])
|
||||
.toHaveBeenCalledTimes (1)
|
||||
expect (helpFixture.modules['../../help/guide/editions/en/2026-10-02.mdx'])
|
||||
.not.toHaveBeenCalled ()
|
||||
})
|
||||
|
||||
it ('loads history editions without treating changes as an article', async () => {
|
||||
const article = vi.spyOn (helpFixture.api, 'getHelpArticle')
|
||||
await prefetchers.prefetchForURL (new QueryClient (), '/help/changes')
|
||||
expect (article).not.toHaveBeenCalledWith ('changes')
|
||||
expect (helpFixture.raw['../../help/guide/editions/ja/2026-10-01.mdx'])
|
||||
.toHaveBeenCalled ()
|
||||
})
|
||||
|
||||
it.each (['intent', 'click', 'off']) ('respects %s intent settings and still prefetches clicks',
|
||||
async mode => {
|
||||
settings.linkPreload = mode
|
||||
const run = vi.spyOn (prefetchers, 'prefetchForURL').mockResolvedValue ()
|
||||
renderWithProviders (<><Location/><PrefetchLink to="/help/guide">Help</PrefetchLink></>)
|
||||
const link = screen.getByRole ('link', { name: 'Help' })
|
||||
fireEvent.mouseEnter (link)
|
||||
fireEvent.touchStart (link)
|
||||
expect (run).toHaveBeenCalledTimes (mode === 'intent' ? 2 : 0)
|
||||
fireEvent.click (link)
|
||||
await waitFor (() => expect (screen.getByTestId ('location'))
|
||||
.toHaveTextContent ('/help/guide'))
|
||||
expect (run).toHaveBeenCalledTimes (mode === 'intent' ? 3 : 1)
|
||||
})
|
||||
|
||||
it ('continues navigation after prefetch failure', async () => {
|
||||
vi.spyOn (console, 'error').mockImplementation (() => {})
|
||||
vi.spyOn (prefetchers, 'prefetchForURL').mockRejectedValue (new Error ('offline'))
|
||||
renderWithProviders (<><Location/><PrefetchLink to="/help/guide">Help</PrefetchLink></>)
|
||||
fireEvent.click (screen.getByRole ('link', { name: 'Help' }))
|
||||
await waitFor (() => expect (screen.getByTestId ('location'))
|
||||
.toHaveTextContent ('/help/guide'))
|
||||
})
|
||||
})
|
||||
+24
-17
@@ -2,9 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { fetchWikiPage, fetchWikiPageByTitle, fetchWikiPages } from '@/lib/wiki'
|
||||
|
||||
const api = vi.hoisted (() => ({
|
||||
apiGet: vi.fn (),
|
||||
}))
|
||||
const api = vi.hoisted (() => ({ apiGet: vi.fn () }))
|
||||
|
||||
vi.mock ('@/lib/api', () => api)
|
||||
|
||||
@@ -21,28 +19,37 @@ describe ('wiki API functions', () => {
|
||||
await fetchWikiPage ('1', { version: '3' })
|
||||
|
||||
expect (api.apiGet).toHaveBeenNthCalledWith (
|
||||
1,
|
||||
'/wiki',
|
||||
{ params: { title: '虹' } },
|
||||
)
|
||||
1,
|
||||
'/wiki',
|
||||
{ params: { title: '虹' } })
|
||||
expect (api.apiGet).toHaveBeenNthCalledWith (
|
||||
2,
|
||||
'/wiki/1',
|
||||
{ params: { version: '3' } },
|
||||
)
|
||||
2,
|
||||
'/wiki/1',
|
||||
{ params: { version: '3' } })
|
||||
})
|
||||
|
||||
it ('encodes title path segments and returns null on misses', async () => {
|
||||
api.apiGet.mockResolvedValueOnce ({ id: 2 })
|
||||
api.apiGet.mockRejectedValueOnce (new Error ('missing'))
|
||||
api.apiGet.mockRejectedValueOnce ({ isAxiosError: true, response: { status: 404 } })
|
||||
|
||||
await fetchWikiPageByTitle ('a/b c', { version: undefined })
|
||||
await expect (fetchWikiPageByTitle ('missing', {})).resolves.toBeNull ()
|
||||
await expect (fetchWikiPageByTitle ('missing', { })).resolves.toBeNull ()
|
||||
|
||||
expect (api.apiGet).toHaveBeenNthCalledWith (
|
||||
1,
|
||||
'/wiki/title/a%2Fb%20c',
|
||||
{ params: { version: undefined } },
|
||||
)
|
||||
1,
|
||||
'/wiki/title/a%2Fb%20c',
|
||||
{ params: { version: undefined } })
|
||||
})
|
||||
|
||||
it.each ([403, 500]) ('preserves HTTP %s errors', async status => {
|
||||
const error = { isAxiosError: true, response: { status } }
|
||||
api.apiGet.mockRejectedValueOnce (error)
|
||||
await expect (fetchWikiPageByTitle ('ホーム', { })).rejects.toBe (error)
|
||||
})
|
||||
|
||||
it ('preserves network failures for callers', async () => {
|
||||
const error = new Error ('network failure')
|
||||
api.apiGet.mockRejectedValueOnce (error)
|
||||
await expect (fetchWikiPageByTitle ('ホーム', { })).rejects.toBe (error)
|
||||
})
|
||||
})
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする