このコミットが含まれているのは:
2026-10-31 20:44:28 +09:00
コミット 14853a8c5f
4個のファイルの変更、91行の追加、15行の削除
+3
ファイルの表示
@@ -141,9 +141,11 @@ export const render = async (
helmetContext = { }
requestContext.status = status
html = await renderDocument (page ())
// Client-only queries must neither run here nor enter the bootstrap below.
const pending = client.getQueryCache ().getAll ().filter (query => {
const options = query.options as QueryObserverOptions
return query.state.status === 'pending'
&& query.meta?.clientOnly !== true
&& options.queryFn != null && options.queryFn !== skipToken
&& options.enabled !== false
&& (typeof options.enabled !== 'function' || options.enabled (query))
@@ -173,6 +175,7 @@ export const render = async (
.map (entry => entry.toString ()).join ('')
const dehydrated = dehydrate (client, { shouldDehydrateQuery: query =>
query.state.status !== 'pending'
&& query.meta?.clientOnly !== true
&& !(query.queryKey[0] === 'help' && query.queryKey[1] === 'edition') })
const state = { ...dehydrated, queries: dehydrated.queries.map (query => ({
...query, state: { ...query.state, fetchFailureReason: null,
+65 -1
ファイルの表示
@@ -1,8 +1,16 @@
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
import { useLocation } from 'react-router-dom'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { hydrateRoot } from 'react-dom/client'
import { renderToString } from 'react-dom/server'
import { HelmetProvider } from 'react-helmet-async'
import { MemoryRouter, useLocation } from 'react-router-dom'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import CountsResourcePage from '@/pages/CountsResourcePage'
import DialogueProvider from '@/components/dialogues/DialogueProvider'
import { countsKeys } from '@/lib/queryKeys'
import { KeyboardShortcutsProvider } from '@/lib/useKeyboardShortcuts'
import { UnsavedChangesGuardProvider } from '@/lib/useUnsavedChangesGuard'
import { detailResult, executionAt, postDefinition } from '@/test/fixtures/counts'
import { renderWithProviders } from '@/test/render'
@@ -32,6 +40,50 @@ beforeEach (() => {
describe ('Counts execution and URL state', () => {
it ('hydrates the server shell before fetching results with the URL conditions', async () => {
const client = new QueryClient ({
defaultOptions: { queries: { retry: false } } })
client.setQueryData (countsKeys.definition ('posts', 'ja'), postDefinition)
const page = <QueryClientProvider client={client}>
<HelmetProvider><MemoryRouter initialEntries={[
'/ja/counts/posts?mode=details&timezone=UTC&resource_id=7']}>
<DialogueProvider><UnsavedChangesGuardProvider>
<KeyboardShortcutsProvider>
<CountsResourcePage resource="posts"/>
</KeyboardShortcutsProvider>
</UnsavedChangesGuardProvider></DialogueProvider>
</MemoryRouter></HelmetProvider>
</QueryClientProvider>
const html = renderToString (page)
expect (fetchCounts).not.toHaveBeenCalled ()
expect (html).toContain ('counts-targets')
expect (html).not.toContain ('Recorded title')
expect (html).not.toContain ('counts-grid')
const resultKey = client.getQueryCache ().getAll ().find (query =>
query.queryKey[0] === 'counts' && query.queryKey[1] === 'posts')!.queryKey
client.setQueryData (resultKey, detailResult)
expect (renderToString (page)).not.toContain ('Recorded title')
client.removeQueries ({ queryKey: resultKey, exact: true })
const container = document.createElement ('div')
container.innerHTML = html
document.body.appendChild (container)
const recoverable = vi.fn ()
const root = hydrateRoot (container, page, { onRecoverableError: recoverable })
try
{
await screen.findByText ('Recorded title')
expect (fetchCounts).toHaveBeenCalledTimes (1)
expect (fetchCounts.mock.calls[0][1].get ('resource_id')).toBe ('7')
expect (recoverable).not.toHaveBeenCalled ()
}
finally
{
await act (async () => root.unmount ())
container.remove ()
client.clear ()
}
})
it ('enriches the initial URL without a second query or removing the page shell', async () => {
renderPage ()
const heading = screen.getByRole ('heading', { name: '広場集計' })
@@ -44,6 +96,18 @@ describe ('Counts execution and URL state', () => {
expect (currentParams ().get ('limit')).toBe ('100')
})
it ('retries an initial CSR failure with the URL filter intact', async () => {
fetchCounts.mockRejectedValueOnce (new Error ('offline'))
renderPage ('&resource_id=7')
await screen.findByRole ('alert')
expect (screen.queryByText ('Recorded title')).not.toBeInTheDocument ()
expect (currentParams ().get ('resource_id')).toBe ('7')
execute ()
await screen.findByText ('Recorded title')
expect (fetchCounts).toHaveBeenCalledTimes (2)
expect (fetchCounts.mock.calls[1][1].get ('resource_id')).toBe ('7')
})
it ('keeps edits local until execution then commits the server timestamp and filters together',
async () => {
renderPage ()
+8 -3
ファイルの表示
@@ -56,6 +56,8 @@ const CountsResourceContent = (
resource: CountsResource; definition?: CountsDefinition; definitionError: boolean },
) => {
const queryClient = useQueryClient ()
const [mounted, setMounted] = useState (false)
useEffect (() => { setMounted (true) }, [])
const [source, setParams] = useSearchParams ()
const [timezone, setTimezone] = useState<string | null> (null)
useEffect (() => {
@@ -103,10 +105,11 @@ const CountsResourceContent = (
const [exporting, setExporting] = useState (false)
const exportInFlight = useRef (false)
const [exportError, setExportError] = useState (false)
const { data, error: queryError, dataUpdatedAt, isFetching: fetching,
const { data: cachedData, error: queryError, dataUpdatedAt, isFetching: fetching,
isPending: pending, isError: errorFlg,
isPlaceholderData: placeholder } = useQuery ({
enabled: definition != null && params.has ('timezone') && !running,
enabled: mounted && definition != null && params.has ('timezone') && !running,
meta: { clientOnly: true },
staleTime: params.has ('execution_at') ? 5 * 60 * 1000 : 0,
queryKey: countsKeys.index (resource, cacheParams.toString (), locale),
queryFn: ({ signal }) => fetchCounts (resource, params, locale, signal), retry: false,
@@ -114,6 +117,8 @@ const CountsResourceContent = (
(query?.queryKey[1] === resource && query?.queryKey[2] === locale
&& queryClient.getQueryData (query.queryKey) === previous)
? previous : undefined })
// Match SSR during hydration even when a client cache already contains results.
const data = mounted ? cachedData : undefined
useEffect (() => {
applied.current = params
@@ -379,7 +384,7 @@ const CountsResourceContent = (
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden"
aria-busy={waiting}>
<div inert={waiting} className="flex min-h-0 flex-1 flex-col">
{definition != null && data != null && <>
{mounted && definition != null && data != null && <>
<p className="my-1 flex flex-wrap items-baseline gap-x-2 text-sm">
<span>{label (resultMode === 'summary' ? 'outputRows' : 'outputTotal', { value: data.outputCount ?? '—' })}</span>
{resultMode === 'details' && data.excludedCountScope === 'current_query'