このコミットが含まれているのは:
@@ -7,77 +7,151 @@ import { renderWithProviders } from '@/test/render'
|
||||
|
||||
import type { CountsResult } from '@/lib/counts'
|
||||
|
||||
|
||||
const { fetchCounts } = vi.hoisted (() => ({ fetchCounts: vi.fn () }))
|
||||
const { fetchCounts, fetchTagByName } = vi.hoisted (() => ({
|
||||
fetchCounts: vi.fn (), fetchTagByName: vi.fn () }))
|
||||
|
||||
vi.mock ('@/lib/counts', async importOriginal => ({
|
||||
...await importOriginal<typeof import ('@/lib/counts')> (), fetchCounts }))
|
||||
vi.mock ('@/lib/tags', async importOriginal => ({
|
||||
...await importOriginal<typeof import ('@/lib/tags')> (), fetchTagByName }))
|
||||
|
||||
|
||||
beforeEach (() => {
|
||||
fetchCounts.mockReset ()
|
||||
fetchTagByName.mockReset ()
|
||||
fetchCounts.mockResolvedValue ({ count: 0, groupCount: 0, groups: [], page: 1, limit: 20 })
|
||||
})
|
||||
|
||||
|
||||
describe ('counts pages', () => {
|
||||
it ('links to the two locale-aware resource screens', () => {
|
||||
const conditions = () => screen.getByRole ('button', { name: '集計条件' })
|
||||
|
||||
|
||||
const execute = () => fireEvent.submit (
|
||||
screen.getByRole ('button', { name: '集計実行' }).closest ('form')!)
|
||||
|
||||
|
||||
describe ('counts workspace', () => {
|
||||
it ('opens the posts workspace at the root with a resource sidebar', async () => {
|
||||
renderWithProviders (<CountsPage/>, { route: '/ja/counts' })
|
||||
fireEvent.click (screen.getByRole ('button', { name: '集計対象' }))
|
||||
expect (screen.getByRole ('link', { name: '投稿' }))
|
||||
.toHaveAttribute ('href', '/ja/counts/posts')
|
||||
expect (screen.getByRole ('link', { name: '投稿' }))
|
||||
.toHaveAttribute ('aria-current', 'page')
|
||||
expect (screen.getByRole ('link', { name: 'タグ' }))
|
||||
.toHaveAttribute ('href', '/ja/counts/tags')
|
||||
expect (conditions ()).toHaveAttribute ('aria-expanded', 'false')
|
||||
await waitFor (() => expect (fetchCounts.mock.lastCall?.[1].get ('axis')).toBe ('month'))
|
||||
})
|
||||
|
||||
it ('restores filters and selected columns and preserves them in page links', async () => {
|
||||
fetchCounts.mockResolvedValue ({
|
||||
count: 21, page: 1, limit: 20,
|
||||
versions: [{ postId: 7, versionNo: 2,
|
||||
url: { current: 'https://example.com' },
|
||||
title: { current: 'Snapshot' } }] as CountsResult['versions'] })
|
||||
renderWithProviders (<CountsResourcePage resource="posts"/>, {
|
||||
route: '/ja/counts/posts?mode=details&columns=title&user_id=3' })
|
||||
expect (await screen.findByRole ('cell', { name: 'Snapshot' })).toBeInTheDocument ()
|
||||
expect (screen.getByRole ('checkbox', { name: '投稿タイトル' })).toBeChecked ()
|
||||
expect (screen.getByRole ('checkbox', { name: '投稿 URL' })).not.toBeChecked ()
|
||||
expect (screen.getByLabelText ('次のページ')).toHaveAttribute (
|
||||
'href', '/ja/counts/posts?mode=details&columns=title&user_id=3&page=2')
|
||||
expect (fetchCounts.mock.calls[0][1].get ('user_id')).toBe ('3')
|
||||
fireEvent.click (screen.getByRole ('checkbox', { name: '投稿 URL' }))
|
||||
await waitFor (() => {
|
||||
expect (fetchCounts.mock.lastCall?.[1].get ('columns')).toBe ('title,url')
|
||||
})
|
||||
})
|
||||
|
||||
it ('executes combined filters and resets the page', async () => {
|
||||
it ('preserves draft filters on collapse without querying until execution', async () => {
|
||||
renderWithProviders (<CountsResourcePage resource="posts"/>, {
|
||||
route: '/ja/counts/posts?page=3&mode=details' })
|
||||
await screen.findByText ('履歴レコード件数:0')
|
||||
const calls = fetchCounts.mock.calls.length
|
||||
fireEvent.click (conditions ())
|
||||
const output = document.querySelector ('[data-counts-output]')!
|
||||
const filters = document.querySelector ('[data-counts-filters]')!
|
||||
expect (output.compareDocumentPosition (filters) & Node.DOCUMENT_POSITION_FOLLOWING)
|
||||
.toBeTruthy ()
|
||||
fireEvent.change (screen.getByRole ('textbox', { name: '投稿 ID' }), {
|
||||
target: { value: '7' } })
|
||||
fireEvent.change (screen.getByRole ('textbox', { name: '操作者 ID' }), {
|
||||
target: { value: '9' } })
|
||||
fireEvent.submit (screen.getByRole ('button', { name: '実行' }).closest ('form')!)
|
||||
fireEvent.click (conditions ())
|
||||
expect (screen.getByRole ('table')).toBeInTheDocument ()
|
||||
fireEvent.click (conditions ())
|
||||
expect (screen.getByRole ('textbox', { name: '投稿 ID' })).toHaveValue ('7')
|
||||
expect (fetchCounts).toHaveBeenCalledTimes (calls)
|
||||
execute ()
|
||||
await waitFor (() => {
|
||||
const params = fetchCounts.mock.lastCall?.[1] as URLSearchParams
|
||||
expect (params.get ('resource_id')).toBe ('7')
|
||||
expect (params.get ('user_id')).toBe ('9')
|
||||
expect (params.has ('page')).toBe (false)
|
||||
})
|
||||
const calls = fetchCounts.mock.calls.length
|
||||
fireEvent.submit (screen.getByRole ('button', { name: '実行' }).closest ('form')!)
|
||||
await waitFor (() => expect (fetchCounts.mock.calls.length).toBeGreaterThan (calls))
|
||||
const refreshed = fetchCounts.mock.calls.length
|
||||
execute ()
|
||||
await waitFor (() => expect (fetchCounts.mock.calls.length).toBeGreaterThan (refreshed))
|
||||
})
|
||||
|
||||
it ('keeps a partial-month date range when drilling down into that month', async () => {
|
||||
it ('applies selected columns only on execution and retains pagination state', async () => {
|
||||
fetchCounts.mockResolvedValue ({ count: 21, page: 1, limit: 20,
|
||||
versions: [{ postId: 7, versionNo: 2, url: { current: 'https://example.com' },
|
||||
title: { current: 'Snapshot' } }] as CountsResult['versions'] })
|
||||
renderWithProviders (<CountsResourcePage resource="posts"/>, {
|
||||
route: '/ja/counts/posts?axis=month&mode=details&columns=title&user_id=3' })
|
||||
expect (await screen.findByRole ('cell', { name: 'Snapshot' })).toBeInTheDocument ()
|
||||
expect (screen.getByLabelText ('次のページ')).toHaveAttribute (
|
||||
'href', '/ja/counts/posts?axis=month&mode=details&columns=title&user_id=3&page=2')
|
||||
fireEvent.click (conditions ())
|
||||
expect (screen.getByRole ('checkbox', { name: '投稿タイトル' })).toBeChecked ()
|
||||
fireEvent.click (screen.getByRole ('checkbox', { name: '投稿 URL' }))
|
||||
expect (fetchCounts.mock.lastCall?.[1].get ('columns')).toBe ('title')
|
||||
execute ()
|
||||
await waitFor (() => {
|
||||
expect (fetchCounts.mock.lastCall?.[1].get ('columns')).toBe ('title,url')
|
||||
})
|
||||
})
|
||||
|
||||
it ('removes legacy event fields and axes from public query state', async () => {
|
||||
renderWithProviders (<CountsResourcePage resource="tags"/>, {
|
||||
route: '/ja/counts/tags?axis=event_type&event_type=update&columns=event_type,name' })
|
||||
await waitFor (() => {
|
||||
const params = fetchCounts.mock.lastCall?.[1] as URLSearchParams
|
||||
expect (params.get ('axis')).toBe ('month')
|
||||
expect (params.has ('event_type')).toBe (false)
|
||||
expect (params.get ('columns')).toBe ('name')
|
||||
})
|
||||
fireEvent.click (conditions ())
|
||||
expect (screen.queryByText ('操作種別')).not.toBeInTheDocument ()
|
||||
expect (screen.getByRole ('combobox', { name: '集計軸' })).toHaveValue ('month')
|
||||
})
|
||||
|
||||
it ('resolves a tag name with the existing lookup before running the query', async () => {
|
||||
fetchTagByName.mockResolvedValue ({ id: 12 })
|
||||
renderWithProviders (<CountsResourcePage resource="posts"/>, {
|
||||
route: '/ja/counts/posts' })
|
||||
fireEvent.click (conditions ())
|
||||
fireEvent.change (screen.getByRole ('textbox', { name: 'タグ名または ID' }), {
|
||||
target: { value: '日本語タグ' } })
|
||||
execute ()
|
||||
await waitFor (() => expect (fetchCounts.mock.lastCall?.[1].get ('tag')).toBe ('12'))
|
||||
expect (fetchTagByName).toHaveBeenCalledWith ('日本語タグ', 'ja')
|
||||
})
|
||||
|
||||
it ('uses Japanese fallback metadata in the Ainu workspace without exposing keys', async () => {
|
||||
const { container } = renderWithProviders (<CountsResourcePage resource="tags"/>, {
|
||||
route: '/ain-Kana/counts/tags' })
|
||||
const title = screen.getByRole ('heading', { name: 'タグの集計・明細' })
|
||||
expect (title.querySelector ('[lang="ja"]')).toHaveTextContent ('タグの集計・明細')
|
||||
expect (conditions ().querySelector ('[lang="ja"]')).toBeInTheDocument ()
|
||||
fireEvent.click (conditions ())
|
||||
await screen.findByRole ('table')
|
||||
expect (container.textContent).not.toMatch (/counts:|columns\.|fields\.|axes\./)
|
||||
})
|
||||
|
||||
it ('drills through an actor group while preserving the existing filters', async () => {
|
||||
fetchCounts.mockResolvedValue ({ count: 3, groupCount: 1, page: 1, limit: 20,
|
||||
groups: [{ key: 9, name: 'Actor name', count: 3 }] })
|
||||
renderWithProviders (<CountsResourcePage resource="posts"/>, {
|
||||
route: '/ja/counts/posts?axis=user&resource_id=7' })
|
||||
const link = await screen.findByRole ('link', { name: 'Actor name' })
|
||||
const url = new URL (link.getAttribute ('href')!, 'http://localhost')
|
||||
expect (url.searchParams.get ('resource_id')).toBe ('7')
|
||||
expect (url.searchParams.get ('user_id')).toBe ('9')
|
||||
expect (url.searchParams.get ('mode')).toBe ('details')
|
||||
})
|
||||
|
||||
it ('keeps partial-month boundaries when drilling through to details', async () => {
|
||||
fetchCounts.mockResolvedValue ({ count: 4, groupCount: 1, page: 1, limit: 20,
|
||||
groups: [{ key: '2024-01', name: null, count: 4 }] })
|
||||
renderWithProviders (<CountsResourcePage resource="tags"/>, {
|
||||
route: '/ja/counts/tags?axis=month&from=2024-01-15T00:00Z&event_type=update' })
|
||||
route: '/ja/counts/tags?axis=month&from=2024-01-15T00:00Z' })
|
||||
const link = await screen.findByRole ('link', { name: '2024-01' })
|
||||
const url = new URL (link.getAttribute ('href')!, 'http://localhost')
|
||||
expect (url.searchParams.get ('from')).toBe ('2024-01-15T00:00Z')
|
||||
expect (url.searchParams.get ('before')).toBe ('2024-02-01T00:00Z')
|
||||
expect (url.searchParams.get ('event_type')).toBe ('update')
|
||||
expect (url.searchParams.get ('mode')).toBe ('details')
|
||||
})
|
||||
})
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする