ファイル
btrc-hub/frontend/src/pages/posts/PostSearchPage.test.tsx
T
みてるぞ f5b632ed89 設定画面 (#34) (#397)
Reviewed-on: #397
Co-authored-by: miteruzo <miteruzo@naver.com>
Co-committed-by: miteruzo <miteruzo@naver.com>
2026-07-07 00:48:41 +09:00

125 行
4.0 KiB
TypeScript

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'
import { CLIENT_SETTINGS_STORAGE_KEY } from '@/lib/settings'
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 (() => {
localStorage.clear ()
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,
limit: 20,
}),
)
})
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 ()
})
it ('uses URL limit before localStorage and stores the active list settings', async () => {
localStorage.setItem (
CLIENT_SETTINGS_STORAGE_KEY,
JSON.stringify ({ lists: { postSearch: { limit: 100, order: 'title:asc' } } }))
postsApi.fetchPosts.mockResolvedValueOnce ({ posts: [], count: 0 })
renderWithProviders (
<PostSearchPage/>,
{ route: '/posts/search?limit=50&order=created_at%3Adesc' },
)
await waitFor (() => {
expect (postsApi.fetchPosts).toHaveBeenCalledWith (
expect.objectContaining ({ limit: 50, order: 'created_at:desc' }),
)
})
expect (JSON.parse (localStorage.getItem (CLIENT_SETTINGS_STORAGE_KEY) ?? '{}'))
.toMatchObject ({ lists: { postSearch: { limit: 50, order: 'created_at:desc' } } })
})
it ('moves the compact limit control outside the search form and resets page on change', async () => {
postsApi.fetchPosts.mockResolvedValue ({ posts: [], count: 0 })
renderWithProviders (<PostSearchPage/>, { route: '/posts/search?page=4' })
const limitSelect = await screen.findByLabelText ('件数')
expect (limitSelect.closest ('form')).toBeNull ()
fireEvent.change (limitSelect, { target: { value: '100' } })
await waitFor (() => {
expect (postsApi.fetchPosts).toHaveBeenLastCalledWith (
expect.objectContaining ({ page: 1, limit: 100 }),
)
})
})
})