このコミットが含まれているのは:
@@ -4,6 +4,7 @@ import { useLocation } from 'react-router-dom'
|
|||||||
|
|
||||||
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
|
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
|
||||||
import { useUITranslation } from '@/lib/useUITranslation'
|
import { useUITranslation } from '@/lib/useUITranslation'
|
||||||
|
import PrefetchLink from '@/components/PrefetchLink'
|
||||||
import TagLink from '@/components/TagLink'
|
import TagLink from '@/components/TagLink'
|
||||||
import TagSearch from '@/components/TagSearch'
|
import TagSearch from '@/components/TagSearch'
|
||||||
import SectionTitle from '@/components/common/SectionTitle'
|
import SectionTitle from '@/components/common/SectionTitle'
|
||||||
@@ -93,25 +94,37 @@ const TagSidebar: FC<Props> = ({ posts, onClick }) => {
|
|||||||
</li>))) : [])}
|
</li>))) : [])}
|
||||||
</ul>
|
</ul>
|
||||||
<SectionTitle>{label ('labels.relations')}</SectionTitle>
|
<SectionTitle>{label ('labels.relations')}</SectionTitle>
|
||||||
<a href="#"
|
<ul>
|
||||||
onClick={ev => {
|
<li>
|
||||||
ev.preventDefault ()
|
<a
|
||||||
void ((async () => {
|
href="#"
|
||||||
try
|
onClick={ev => {
|
||||||
{
|
ev.preventDefault ()
|
||||||
const data = await apiGet<Post> ('/posts/random',
|
void ((async () => {
|
||||||
{ locale, params: { tags: tagsQuery.split (' ').filter (e => e !== '').join (' '),
|
try
|
||||||
match: (anyFlg ? 'any' : 'all') } })
|
{
|
||||||
navigate (`/posts/${ data.id }`)
|
const data = await apiGet<Post> (
|
||||||
}
|
'/posts/random',
|
||||||
catch
|
{ locale,
|
||||||
{
|
params: { tags: tagsQuery.split (' ').filter (e => e !== '').join (' '),
|
||||||
;
|
match: (anyFlg ? 'any' : 'all') } })
|
||||||
}
|
navigate (`/posts/${ data.id }`)
|
||||||
}) ())
|
}
|
||||||
}}>
|
catch
|
||||||
{label ('actions.random')}
|
{
|
||||||
</a>
|
;
|
||||||
|
}
|
||||||
|
}) ())
|
||||||
|
}}>
|
||||||
|
{label ('actions.random')}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<PrefetchLink to={`/counts/posts?tags=${ encodeURIComponent (tagsQuery) }`}>
|
||||||
|
{label ('actions.count')}
|
||||||
|
</PrefetchLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</>)
|
</>)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { DndContext, DragOverlay, KeyboardSensor, MouseSensor, TouchSensor,
|
|||||||
closestCenter, useSensor, useSensors } from '@dnd-kit/core'
|
closestCenter, useSensor, useSensors } from '@dnd-kit/core'
|
||||||
import { SortableContext, arrayMove, horizontalListSortingStrategy,
|
import { SortableContext, arrayMove, horizontalListSortingStrategy,
|
||||||
sortableKeyboardCoordinates } from '@dnd-kit/sortable'
|
sortableKeyboardCoordinates } from '@dnd-kit/sortable'
|
||||||
import { useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
import { CSS } from '@dnd-kit/utilities'
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
|
|
||||||
@@ -15,6 +15,8 @@ const SortableColumns = ({ ids, fixed = [], name, onChange, children }: {
|
|||||||
ids: string[]; fixed?: string[]; name: (id: string) => { text: string; language: string }
|
ids: string[]; fixed?: string[]; name: (id: string) => { text: string; language: string }
|
||||||
onChange: (ids: string[]) => void; children: (ids: string[]) => ReactNode },
|
onChange: (ids: string[]) => void; children: (ids: string[]) => ReactNode },
|
||||||
) => {
|
) => {
|
||||||
|
const [mounted, setMounted] = useState (false)
|
||||||
|
useEffect (() => setMounted (true), [])
|
||||||
const [active, setActive] = useState<string | null> (null)
|
const [active, setActive] = useState<string | null> (null)
|
||||||
const drag = useRef<{ id: string; ids: string[] } | null> (null)
|
const drag = useRef<{ id: string; ids: string[] } | null> (null)
|
||||||
const motion = useClientMotion ()
|
const motion = useClientMotion ()
|
||||||
@@ -56,7 +58,7 @@ const SortableColumns = ({ ids, fixed = [], name, onChange, children }: {
|
|||||||
<SortableContext items={ids} strategy={horizontalListSortingStrategy}>
|
<SortableContext items={ids} strategy={horizontalListSortingStrategy}>
|
||||||
{children (ids)}
|
{children (ids)}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
{typeof document !== 'undefined' && createPortal (<DragOverlay adjustScale={false}
|
{mounted && typeof document !== 'undefined' && createPortal (<DragOverlay adjustScale={false}
|
||||||
style={{ width: 'max-content', height: 'auto', maxWidth: 'calc(100vw - 2rem)' }}
|
style={{ width: 'max-content', height: 'auto', maxWidth: 'calc(100vw - 2rem)' }}
|
||||||
dropAnimation={motion.reduced ? null : {
|
dropAnimation={motion.reduced ? null : {
|
||||||
duration: motion.duration ('drop'), easing: motion.easing,
|
duration: motion.duration ('drop'), easing: motion.easing,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { DndContext, DragOverlay, KeyboardSensor, MouseSensor, TouchSensor,
|
|||||||
from '@dnd-kit/core'
|
from '@dnd-kit/core'
|
||||||
import { SortableContext, arrayMove, rectSortingStrategy,
|
import { SortableContext, arrayMove, rectSortingStrategy,
|
||||||
sortableKeyboardCoordinates } from '@dnd-kit/sortable'
|
sortableKeyboardCoordinates } from '@dnd-kit/sortable'
|
||||||
import { useId, useRef, useState } from 'react'
|
import { useEffect, useId, useRef, useState } from 'react'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
import { CSS } from '@dnd-kit/utilities'
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
|
|
||||||
@@ -38,6 +38,8 @@ const SortableList = (
|
|||||||
renderGroup: (id: string, content: ReactNode, over: boolean) => ReactNode
|
renderGroup: (id: string, content: ReactNode, over: boolean) => ReactNode
|
||||||
onChange: (groups: Groups) => void; fixed?: string[] },
|
onChange: (groups: Groups) => void; fixed?: string[] },
|
||||||
) => {
|
) => {
|
||||||
|
const [mounted, setMounted] = useState (false)
|
||||||
|
useEffect (() => setMounted (true), [])
|
||||||
const drag = useRef<{ id: string; groups: Groups } | null> (null)
|
const drag = useRef<{ id: string; groups: Groups } | null> (null)
|
||||||
const scope = useId ()
|
const scope = useId ()
|
||||||
const [active, setActive] = useState<string | null> (null)
|
const [active, setActive] = useState<string | null> (null)
|
||||||
@@ -139,7 +141,7 @@ const SortableList = (
|
|||||||
</div>
|
</div>
|
||||||
</SortableContext>, overGroup === group)}
|
</SortableContext>, overGroup === group)}
|
||||||
</Group>)}
|
</Group>)}
|
||||||
{typeof document !== 'undefined' && createPortal (
|
{mounted && typeof document !== 'undefined' && createPortal (
|
||||||
<DragOverlay adjustScale={false}
|
<DragOverlay adjustScale={false}
|
||||||
style={{ width: 'max-content', height: 'auto', maxWidth: 'calc(100vw - 2rem)' }}
|
style={{ width: 'max-content', height: 'auto', maxWidth: 'calc(100vw - 2rem)' }}
|
||||||
dropAnimation={motion.reduced ? null : {
|
dropAnimation={motion.reduced ? null : {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
{
|
{ "head": {
|
||||||
"head": {
|
"description": "ぼざクリ ぼざろクリーチャーシリーズ 伊地知ニジカ リンク集 Wiki ニコニコ" },
|
||||||
"description": "ぼざクリ ぼざろクリーチャーシリーズ 伊地知ニジカ リンク集 Wiki ニコニコ"
|
|
||||||
},
|
|
||||||
"dialogue": { "confirm": "確定", "close": "閉ぢる" },
|
"dialogue": { "confirm": "確定", "close": "閉ぢる" },
|
||||||
"sidebarWidth": "サイドバー幅",
|
"sidebarWidth": "サイドバー幅",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
@@ -9,30 +7,27 @@
|
|||||||
"results": "検索結果",
|
"results": "検索結果",
|
||||||
"updateFailedMessage": "更新に失敗しました.",
|
"updateFailedMessage": "更新に失敗しました.",
|
||||||
"labels": {
|
"labels": {
|
||||||
"title": "タイトル", "content": "内容", "body": "本文",
|
"title": "タイトル", "content": "内容", "body": "本文",
|
||||||
"lastUpdated": "最終更新", "thumbnail": "サムネール",
|
"lastUpdated": "最終更新", "thumbnail": "サムネール",
|
||||||
"noThumbnail": "サムネイルなし", "thumbnailMissing": "サムネールなし",
|
"noThumbnail": "サムネイルなし", "thumbnailMissing": "サムネールなし",
|
||||||
"parentPosts": "親投稿", "duration": "動画時間", "platform": "プラットフォーム",
|
"parentPosts": "親投稿", "duration": "動画時間", "platform": "プラットフォーム",
|
||||||
"code": "コード", "information": "情報", "relations": "関聯",
|
"code": "コード", "information": "情報", "relations": "関聯",
|
||||||
"uploadedBy": "耕作者: ", "uploadedAt": "耕作日時: ", "link": "リンク: ",
|
"uploadedBy": "耕作者: ", "uploadedAt": "耕作日時: ", "link": "リンク: ",
|
||||||
"originalPostedAt": "オリジナルの投稿日時: ", "originalCreatedAt": "オリジナルの作成日時",
|
"originalPostedAt": "オリジナルの投稿日時: ", "originalCreatedAt": "オリジナルの作成日時",
|
||||||
"brand": "ぼざクリ タグ広場", "since": "以降", "before": "より前",
|
"brand": "ぼざクリ タグ広場", "since": "以降", "before": "より前",
|
||||||
"tagSearch": "タグ検索...", "hideTags": "▲▲▲ タグ一覧を閉じる ▲▲▲",
|
"tagSearch": "タグ検索...", "hideTags": "▲▲▲ タグ一覧を閉じる ▲▲▲",
|
||||||
"showTags": "▼▼▼ タグ一覧を表示 ▼▼▼",
|
"showTags": "▼▼▼ タグ一覧を表示 ▼▼▼",
|
||||||
"resultsCount": "{{count}} 件", "menu": "メニュー", "anonymous": "名もなきニジラー",
|
"resultsCount": "{{count}} 件", "menu": "メニュー", "anonymous": "名もなきニジラー",
|
||||||
"bot": "bot操作", "deprecatedSuffix": "(廃止)", "development": "開発モード"
|
"bot": "bot操作", "deprecatedSuffix": "(廃止)", "development": "開発モード" },
|
||||||
},
|
|
||||||
"actions": {
|
"actions": {
|
||||||
"update": "更新", "edit": "編輯", "add": "追加", "reset": "リセット",
|
"update": "更新", "edit": "編輯", "add": "追加", "reset": "リセット",
|
||||||
"cancel": "取消", "retry": "再試行", "skip": "スキップ", "random": "ランダム",
|
"cancel": "取消", "retry": "再試行", "skip": "スキップ", "random": "ランダム",
|
||||||
"old": "< 古", "new": "新 >", "oldest": "(最古)", "latest": "(最新)",
|
"count": "件数",
|
||||||
"diff": "差分", "restore": "復元", "show": "表示"
|
"old": "< 古", "new": "新 >", "oldest": "(最古)", "latest": "(最新)",
|
||||||
},
|
"diff": "差分", "restore": "復元", "show": "表示" },
|
||||||
"messages": {
|
"messages": {
|
||||||
"loading": "読み込み中...", "loadingEllipsis": "読込中……",
|
"loading": "読込中……", "loadingEllipsis": "読込中……",
|
||||||
"updated": "更新しました.", "updateFailed": "更新失敗", "failed": "失敗……",
|
"updated": "更新しました.", "updateFailed": "更新失敗", "failed": "失敗……",
|
||||||
"checkInput": "入力を確認してください.", "checkInputModern": "入力を確認してください。",
|
"checkInput": "入力を確認してください.", "checkInputModern": "入力を確認してください。",
|
||||||
"checkInputContent": "入力内容を確認してください.", "reviewInput": "入力を見直してください.",
|
"checkInputContent": "入力内容を確認してください.", "reviewInput": "入力を見直してください.",
|
||||||
"postSuccess": "投稿成功!", "postFailed": "投稿失敗", "resetChanges": "変更をリセットしますか?"
|
"postSuccess": "投稿成功!", "postFailed": "投稿失敗", "resetChanges": "変更をリセットしますか?" } }
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|||||||
import { hydrateRoot } from 'react-dom/client'
|
import { hydrateRoot } from 'react-dom/client'
|
||||||
import { renderToString } from 'react-dom/server'
|
import { renderToString } from 'react-dom/server'
|
||||||
import { HelmetProvider } from 'react-helmet-async'
|
import { HelmetProvider } from 'react-helmet-async'
|
||||||
import { MemoryRouter, useLocation } from 'react-router-dom'
|
import { createMemoryRouter, RouterProvider, useLocation } from 'react-router-dom'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
import CountsResourcePage from '@/pages/CountsResourcePage'
|
import CountsResourcePage from '@/pages/CountsResourcePage'
|
||||||
import DialogueProvider from '@/components/dialogues/DialogueProvider'
|
import DialogueProvider from '@/components/dialogues/DialogueProvider'
|
||||||
import { countsKeys } from '@/lib/queryKeys'
|
import { countsKeys } from '@/lib/queryKeys'
|
||||||
|
import SortableColumns from '@/components/common/SortableColumns'
|
||||||
|
import SortableList from '@/components/common/SortableList'
|
||||||
import { KeyboardShortcutsProvider } from '@/lib/useKeyboardShortcuts'
|
import { KeyboardShortcutsProvider } from '@/lib/useKeyboardShortcuts'
|
||||||
import { UnsavedChangesGuardProvider } from '@/lib/useUnsavedChangesGuard'
|
import { UnsavedChangesGuardProvider } from '@/lib/useUnsavedChangesGuard'
|
||||||
import { detailResult, executionAt, postDefinition } from '@/test/fixtures/counts'
|
import { detailResult, executionAt, postDefinition } from '@/test/fixtures/counts'
|
||||||
@@ -19,6 +21,10 @@ import type { CountsResult } from '@/lib/counts'
|
|||||||
const { fetchCounts, fetchCountsDefinition } = vi.hoisted (() => ({
|
const { fetchCounts, fetchCountsDefinition } = vi.hoisted (() => ({
|
||||||
fetchCounts: vi.fn (), fetchCountsDefinition: vi.fn () }))
|
fetchCounts: vi.fn (), fetchCountsDefinition: vi.fn () }))
|
||||||
|
|
||||||
|
// Layout animation is outside this state/URL contract and adds asynchronous frames.
|
||||||
|
vi.mock ('@/lib/useClientBehaviourSettings', () => ({
|
||||||
|
useClientBehaviourSettings: () => ({ animation: 'off', linkPreload: 'off' }) }))
|
||||||
|
|
||||||
vi.mock ('@/lib/counts', async original => ({
|
vi.mock ('@/lib/counts', async original => ({
|
||||||
...await original<typeof import ('@/lib/counts')> (), fetchCounts }))
|
...await original<typeof import ('@/lib/counts')> (), fetchCounts }))
|
||||||
vi.mock ('@/lib/countsDefinitions', async original => ({
|
vi.mock ('@/lib/countsDefinitions', async original => ({
|
||||||
@@ -27,12 +33,27 @@ vi.mock ('@/lib/countsDefinitions', async original => ({
|
|||||||
const Location = () => <output data-testid="location">{useLocation ().search}</output>
|
const Location = () => <output data-testid="location">{useLocation ().search}</output>
|
||||||
const currentParams = () => new URLSearchParams (screen.getByTestId ('location').textContent ?? '')
|
const currentParams = () => new URLSearchParams (screen.getByTestId ('location').textContent ?? '')
|
||||||
const execute = () => fireEvent.submit (
|
const execute = () => fireEvent.submit (
|
||||||
screen.getByRole ('button', { name: '集計実行' }).closest ('form')!)
|
screen.getByText ('集計実行').closest ('form')!)
|
||||||
const openConditions = () => fireEvent.click (screen.getByRole ('button', { name: '集計条件' }))
|
const openConditions = () => fireEvent.click (
|
||||||
|
screen.getByText ('集計条件').closest ('button')!)
|
||||||
const renderPage = (extra = '') => renderWithProviders (<>
|
const renderPage = (extra = '') => renderWithProviders (<>
|
||||||
<CountsResourcePage resource="posts"/><Location/>
|
<CountsResourcePage resource="posts"/><Location/>
|
||||||
</>, { route: `/ja/counts/posts?mode=details&timezone=UTC${extra}` })
|
</>, { route: `/ja/counts/posts?mode=details&timezone=UTC${extra}` })
|
||||||
|
|
||||||
|
const waitForResult = async (client: QueryClient) => {
|
||||||
|
await waitFor (() => {
|
||||||
|
expect (fetchCounts).toHaveBeenCalled ()
|
||||||
|
const queries = client.getQueryCache ().getAll ().filter (query =>
|
||||||
|
query.queryKey[0] === 'counts' && query.queryKey[1] === 'posts')
|
||||||
|
expect (queries.map (query => ({
|
||||||
|
key: query.queryKey, status: query.state.status, data: query.state.data,
|
||||||
|
error: query.state.error }))).toEqual (expect.arrayContaining ([
|
||||||
|
expect.objectContaining ({ status: 'success', data: detailResult })]))
|
||||||
|
})
|
||||||
|
await waitFor (() => expect (currentParams ().get ('mode')).toBe ('details'))
|
||||||
|
await screen.findByText ('Recorded title')
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach (() => {
|
beforeEach (() => {
|
||||||
fetchCounts.mockReset ().mockResolvedValue (detailResult)
|
fetchCounts.mockReset ().mockResolvedValue (detailResult)
|
||||||
fetchCountsDefinition.mockReset ().mockResolvedValue (postDefinition)
|
fetchCountsDefinition.mockReset ().mockResolvedValue (postDefinition)
|
||||||
@@ -40,19 +61,30 @@ beforeEach (() => {
|
|||||||
|
|
||||||
|
|
||||||
describe ('Counts execution and URL state', () => {
|
describe ('Counts execution and URL state', () => {
|
||||||
|
it ('renders sortable shells on the server even when JSDOM provides document', () => {
|
||||||
|
const name = () => ({ text: 'Title', language: 'en' })
|
||||||
|
expect (() => renderToString (<SortableColumns ids={['title']}
|
||||||
|
name={name} onChange={() => {}}>
|
||||||
|
{() => <div>Title</div>}
|
||||||
|
</SortableColumns>)).not.toThrow ()
|
||||||
|
expect (() => renderToString (<SortableList groups={{ fields: ['title'] }}
|
||||||
|
name={name} onChange={() => {}} renderItem={() => <div>Title</div>}
|
||||||
|
renderGroup={(_, content) => <section>{content}</section>}/>)).not.toThrow ()
|
||||||
|
})
|
||||||
|
|
||||||
it ('hydrates the server shell before fetching results with the URL conditions', async () => {
|
it ('hydrates the server shell before fetching results with the URL conditions', async () => {
|
||||||
const client = new QueryClient ({
|
const client = new QueryClient ({
|
||||||
defaultOptions: { queries: { retry: false } } })
|
defaultOptions: { queries: { retry: false } } })
|
||||||
client.setQueryData (countsKeys.definition ('posts', 'ja'), postDefinition)
|
client.setQueryData (countsKeys.definition ('posts', 'ja'), postDefinition)
|
||||||
const page = <QueryClientProvider client={client}>
|
const router = createMemoryRouter ([{ path: '*', element: (
|
||||||
<HelmetProvider><MemoryRouter initialEntries={[
|
|
||||||
'/ja/counts/posts?mode=details&timezone=UTC&resource_id=7']}>
|
|
||||||
<DialogueProvider><UnsavedChangesGuardProvider>
|
<DialogueProvider><UnsavedChangesGuardProvider>
|
||||||
<KeyboardShortcutsProvider>
|
<KeyboardShortcutsProvider>
|
||||||
<CountsResourcePage resource="posts"/>
|
<CountsResourcePage resource="posts"/><Location/>
|
||||||
</KeyboardShortcutsProvider>
|
</KeyboardShortcutsProvider>
|
||||||
</UnsavedChangesGuardProvider></DialogueProvider>
|
</UnsavedChangesGuardProvider></DialogueProvider>) }], {
|
||||||
</MemoryRouter></HelmetProvider>
|
initialEntries: ['/ja/counts/posts?mode=details&timezone=UTC&resource_id=7'] })
|
||||||
|
const page = <QueryClientProvider client={client}>
|
||||||
|
<HelmetProvider><RouterProvider router={router}/></HelmetProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
const html = renderToString (page)
|
const html = renderToString (page)
|
||||||
expect (fetchCounts).not.toHaveBeenCalled ()
|
expect (fetchCounts).not.toHaveBeenCalled ()
|
||||||
@@ -71,23 +103,26 @@ describe ('Counts execution and URL state', () => {
|
|||||||
const root = hydrateRoot (container, page, { onRecoverableError: recoverable })
|
const root = hydrateRoot (container, page, { onRecoverableError: recoverable })
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await screen.findByText ('Recorded title')
|
await waitForResult (client)
|
||||||
expect (fetchCounts).toHaveBeenCalledTimes (1)
|
expect (fetchCounts).toHaveBeenCalledTimes (1)
|
||||||
expect (fetchCounts.mock.calls[0][1].get ('resource_id')).toBe ('7')
|
expect (fetchCounts.mock.calls[0][1].get ('resource_id')).toBe ('7')
|
||||||
|
await waitFor (() => expect (currentParams ().get ('execution_at')).toBe (executionAt))
|
||||||
|
expect (currentParams ().get ('resource_id')).toBe ('7')
|
||||||
expect (recoverable).not.toHaveBeenCalled ()
|
expect (recoverable).not.toHaveBeenCalled ()
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
await act (async () => root.unmount ())
|
await act (async () => root.unmount ())
|
||||||
container.remove ()
|
container.remove ()
|
||||||
|
router.dispose ()
|
||||||
client.clear ()
|
client.clear ()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it ('enriches the initial URL without a second query or removing the page shell', async () => {
|
it ('enriches the initial URL without a second query or removing the page shell', async () => {
|
||||||
renderPage ()
|
const { queryClient } = renderPage ()
|
||||||
const heading = screen.getByRole ('heading', { name: '広場集計' })
|
const heading = screen.getByRole ('heading', { name: '広場集計' })
|
||||||
await screen.findByText ('Recorded title')
|
await waitForResult (queryClient)
|
||||||
await waitFor (() => expect (currentParams ().get ('execution_at')).toBe (executionAt))
|
await waitFor (() => expect (currentParams ().get ('execution_at')).toBe (executionAt))
|
||||||
expect (fetchCounts).toHaveBeenCalledTimes (1)
|
expect (fetchCounts).toHaveBeenCalledTimes (1)
|
||||||
expect (screen.getByRole ('heading', { name: '広場集計' })).toBe (heading)
|
expect (screen.getByRole ('heading', { name: '広場集計' })).toBe (heading)
|
||||||
@@ -98,24 +133,25 @@ describe ('Counts execution and URL state', () => {
|
|||||||
|
|
||||||
it ('retries an initial CSR failure with the URL filter intact', async () => {
|
it ('retries an initial CSR failure with the URL filter intact', async () => {
|
||||||
fetchCounts.mockRejectedValueOnce (new Error ('offline'))
|
fetchCounts.mockRejectedValueOnce (new Error ('offline'))
|
||||||
renderPage ('&resource_id=7')
|
const { queryClient } = renderPage ('&resource_id=7')
|
||||||
await screen.findByRole ('alert')
|
await screen.findByRole ('alert')
|
||||||
expect (screen.queryByText ('Recorded title')).not.toBeInTheDocument ()
|
expect (screen.queryByText ('Recorded title')).not.toBeInTheDocument ()
|
||||||
expect (currentParams ().get ('resource_id')).toBe ('7')
|
expect (currentParams ().get ('resource_id')).toBe ('7')
|
||||||
execute ()
|
execute ()
|
||||||
await screen.findByText ('Recorded title')
|
await waitForResult (queryClient)
|
||||||
expect (fetchCounts).toHaveBeenCalledTimes (2)
|
expect (fetchCounts).toHaveBeenCalledTimes (2)
|
||||||
expect (fetchCounts.mock.calls[1][1].get ('resource_id')).toBe ('7')
|
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',
|
it ('keeps edits local until execution then commits the server timestamp and filters together',
|
||||||
async () => {
|
async () => {
|
||||||
renderPage ()
|
const { queryClient } = renderPage ()
|
||||||
|
await waitForResult (queryClient)
|
||||||
await waitFor (() => expect (currentParams ().get ('execution_at')).toBe (executionAt))
|
await waitFor (() => expect (currentParams ().get ('execution_at')).toBe (executionAt))
|
||||||
openConditions ()
|
openConditions ()
|
||||||
const input = screen.getByRole ('textbox', { name: 'Id.' })
|
const input = screen.getByRole ('textbox', { name: 'Id.' })
|
||||||
fireEvent.change (input, { target: { value: '7' } })
|
fireEvent.change (input, { target: { value: '7' } })
|
||||||
fireEvent.change (screen.getByRole ('combobox', { name: '表示件数' }), {
|
fireEvent.change (screen.getByLabelText ('表示件数', { selector: 'select' }), {
|
||||||
target: { value: '20' } })
|
target: { value: '20' } })
|
||||||
openConditions ()
|
openConditions ()
|
||||||
openConditions ()
|
openConditions ()
|
||||||
@@ -129,6 +165,7 @@ describe ('Counts execution and URL state', () => {
|
|||||||
const oldURL = currentParams ().toString ()
|
const oldURL = currentParams ().toString ()
|
||||||
execute ()
|
execute ()
|
||||||
await waitFor (() => expect (fetchCounts).toHaveBeenCalledTimes (2))
|
await waitFor (() => expect (fetchCounts).toHaveBeenCalledTimes (2))
|
||||||
|
expect (fetchCounts.mock.calls[1][1].get ('resource_id')).toBe ('7')
|
||||||
expect (currentParams ().toString ()).toBe (oldURL)
|
expect (currentParams ().toString ()).toBe (oldURL)
|
||||||
expect (screen.getByRole ('textbox', { name: 'Id.' })).toBe (input)
|
expect (screen.getByRole ('textbox', { name: 'Id.' })).toBe (input)
|
||||||
await act (async () => complete ({ ...detailResult,
|
await act (async () => complete ({ ...detailResult,
|
||||||
@@ -141,7 +178,8 @@ describe ('Counts execution and URL state', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it ('keeps the filter and identifier after a failed execution', async () => {
|
it ('keeps the filter and identifier after a failed execution', async () => {
|
||||||
renderPage ()
|
const { queryClient } = renderPage ()
|
||||||
|
await waitForResult (queryClient)
|
||||||
await waitFor (() => expect (currentParams ().get ('execution_at')).toBe (executionAt))
|
await waitFor (() => expect (currentParams ().get ('execution_at')).toBe (executionAt))
|
||||||
openConditions ()
|
openConditions ()
|
||||||
fireEvent.change (screen.getByRole ('textbox', { name: 'Id.' }), {
|
fireEvent.change (screen.getByRole ('textbox', { name: 'Id.' }), {
|
||||||
@@ -149,6 +187,8 @@ describe ('Counts execution and URL state', () => {
|
|||||||
fetchCounts.mockRejectedValueOnce (new Error ('offline'))
|
fetchCounts.mockRejectedValueOnce (new Error ('offline'))
|
||||||
execute ()
|
execute ()
|
||||||
await screen.findByRole ('alert')
|
await screen.findByRole ('alert')
|
||||||
|
expect (fetchCounts).toHaveBeenCalledTimes (2)
|
||||||
|
expect (fetchCounts.mock.calls[1][1].get ('resource_id')).toBe ('99')
|
||||||
expect (screen.getByRole ('textbox', { name: 'Id.' })).toHaveValue ('99')
|
expect (screen.getByRole ('textbox', { name: 'Id.' })).toHaveValue ('99')
|
||||||
expect (currentParams ().has ('resource_id')).toBe (false)
|
expect (currentParams ().has ('resource_id')).toBe (false)
|
||||||
})
|
})
|
||||||
@@ -170,17 +210,17 @@ describe ('Counts execution and URL state', () => {
|
|||||||
it ('can execute after an initial column-limit failure with no execution timestamp', async () => {
|
it ('can execute after an initial column-limit failure with no execution timestamp', async () => {
|
||||||
fetchCounts.mockRejectedValueOnce ({ isAxiosError: true,
|
fetchCounts.mockRejectedValueOnce ({ isAxiosError: true,
|
||||||
response: { status: 422, data: { error: 'too_many_columns', max_columns: 73 } } })
|
response: { status: 422, data: { error: 'too_many_columns', max_columns: 73 } } })
|
||||||
renderPage ()
|
const { queryClient } = renderPage ()
|
||||||
await screen.findByRole ('alert')
|
await screen.findByRole ('alert')
|
||||||
execute ()
|
execute ()
|
||||||
await screen.findByText ('Recorded title')
|
await waitForResult (queryClient)
|
||||||
await waitFor (() => expect (currentParams ().get ('execution_at')).toBe (executionAt))
|
await waitFor (() => expect (currentParams ().get ('execution_at')).toBe (executionAt))
|
||||||
expect (fetchCounts).toHaveBeenCalledTimes (2)
|
expect (fetchCounts).toHaveBeenCalledTimes (2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it ('keeps the page heading and common site title consistent', async () => {
|
it ('keeps the page heading and common site title consistent', async () => {
|
||||||
renderPage ()
|
const { queryClient } = renderPage ()
|
||||||
await screen.findByText ('Recorded title')
|
await waitForResult (queryClient)
|
||||||
await waitFor (() => expect (document.title).toContain ('広場集計 | '))
|
await waitFor (() => expect (document.title).toContain ('広場集計 | '))
|
||||||
expect (screen.getByRole ('heading', { name: '広場集計' })).toBeInTheDocument ()
|
expect (screen.getByRole ('heading', { name: '広場集計' })).toBeInTheDocument ()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -53,7 +53,9 @@ const tooManyColumns = (failure: unknown): number | null => {
|
|||||||
|
|
||||||
const CountsResourceContent = (
|
const CountsResourceContent = (
|
||||||
{ resource, definition, definitionError }: {
|
{ resource, definition, definitionError }: {
|
||||||
resource: CountsResource; definition?: CountsDefinition; definitionError: boolean },
|
resource: CountsResource
|
||||||
|
definition?: CountsDefinition
|
||||||
|
definitionError: boolean },
|
||||||
) => {
|
) => {
|
||||||
const queryClient = useQueryClient ()
|
const queryClient = useQueryClient ()
|
||||||
const [mounted, setMounted] = useState (false)
|
const [mounted, setMounted] = useState (false)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
import { render, screen } from '@testing-library/react'
|
import { act, render, screen, within } from '@testing-library/react'
|
||||||
import { HelmetProvider } from 'react-helmet-async'
|
import { HelmetProvider } from 'react-helmet-async'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
import { apiGet } from '@/lib/api'
|
import { apiGet } from '@/lib/api'
|
||||||
import App from '@/App'
|
import App, { pageRoutes } from '@/App'
|
||||||
import { helpFixture } from '@/test/helpFixtures'
|
import { helpFixture } from '@/test/helpFixtures'
|
||||||
|
|
||||||
vi.hoisted (() => {
|
vi.hoisted (() => {
|
||||||
@@ -28,12 +28,17 @@ describe ('Help global edition history', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it ('routes /help/changes to published editions in date and numeric order', async () => {
|
it ('routes /help/changes to published editions in date and numeric order', async () => {
|
||||||
|
// Resolve the real lazy route before waiting for its asynchronously loaded data.
|
||||||
|
const route = pageRoutes (null, () => {}).find (route => route.path === 'help/changes')!
|
||||||
|
expect (route.handle?.preload).toBeTypeOf ('function')
|
||||||
|
await act (async () => { await route.handle!.preload! ('ja') })
|
||||||
const client = new QueryClient ({ defaultOptions: { queries: { retry: false } } })
|
const client = new QueryClient ({ defaultOptions: { queries: { retry: false } } })
|
||||||
const { container } = render (
|
const { container } = render (
|
||||||
<QueryClientProvider client={client}>
|
<QueryClientProvider client={client}>
|
||||||
<HelmetProvider><App/></HelmetProvider>
|
<HelmetProvider><App/></HelmetProvider>
|
||||||
</QueryClientProvider>)
|
</QueryClientProvider>)
|
||||||
const links = await screen.findAllByRole ('link')
|
const table = await screen.findByRole ('table')
|
||||||
|
const links = within (table).getAllByRole ('link')
|
||||||
const editions = links.map (link => link.getAttribute ('href'))
|
const editions = links.map (link => link.getAttribute ('href'))
|
||||||
.filter (href => href?.startsWith ('/ja/help/') && href.includes ('edition='))
|
.filter (href => href?.startsWith ('/ja/help/') && href.includes ('edition='))
|
||||||
expect (editions).toEqual ([
|
expect (editions).toEqual ([
|
||||||
|
|||||||
@@ -106,8 +106,7 @@ const PostListPage: FC = () => {
|
|||||||
updatePage (Math.min (page + 1, totalPages))
|
updatePage (Math.min (page + 1, totalPages))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: { }),
|
: { }) }), [page, totalPages, location.search]))
|
||||||
}), [page, totalPages, location.search]))
|
|
||||||
|
|
||||||
useLayoutEffect (() => {
|
useLayoutEffect (() => {
|
||||||
scroll (0, 0)
|
scroll (0, 0)
|
||||||
|
|||||||
新しいイシューから参照
ユーザーをブロックする