Skip to content

Commit 9859311

Browse files
committed
test: migrate suspense test to e2e for react 19/canary
1 parent 48212e0 commit 9859311

File tree

39 files changed

+1669
-669
lines changed

39 files changed

+1669
-669
lines changed

.github/workflows/test-canary.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ jobs:
1919
run: corepack pnpm upgrade react@canary react-dom@canary use-sync-external-store@canary
2020

2121
- name: Lint and test
22+
env:
23+
TEST_REACT_CANARY: 1
2224
run: |
2325
pnpm clean
2426
pnpm build

Suspense-tests-todo.md

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
# Suspense Tests To-Do
22

3-
- [x] test/use-swr-infinite.test.tsx — should update the getKey reference with the suspense mode (migrated to e2e/test/suspense-infinite-get-key.test.ts)
4-
- [x] test/use-swr-suspense.test.tsx — should render fallback (migrated to e2e/test/suspense-render-fallback.test.ts)
5-
- [x] test/use-swr-suspense.test.tsx — should render multiple SWR fallbacks (migrated to e2e/test/render-suspense-multiple-fallbacks.test.ts)
6-
- [x] test/use-swr-suspense.test.tsx — should work for non-promises (migrated to e2e/test/render-suspense-non-promise.test.ts)
7-
- [x] test/use-swr-suspense.test.tsx — should throw errors (migrated to e2e/test/render-suspense-error.test.ts)
8-
- [ ] test/use-swr-suspense.test.tsx — should render cached data with error
9-
- [ ] test/use-swr-suspense.test.tsx — should not fetch when cached data is present and `revalidateIfStale` is false
10-
- [ ] test/use-swr-suspense.test.tsx — should pause when key changes
11-
- [ ] test/use-swr-suspense.test.tsx — should render correctly when key changes (but with same response data)
12-
- [ ] test/use-swr-suspense.test.tsx — should render correctly when key changes (from null to valid key)
13-
- [ ] test/use-swr-suspense.test.tsx — should render initial data if set
14-
- [ ] test/use-swr-suspense.test.tsx — should avoid unnecessary re-renders
15-
- [ ] test/use-swr-suspense.test.tsx — should return `undefined` data for falsy key
16-
- [ ] test/use-swr-suspense.test.tsx — should only render fallback once when `keepPreviousData` is set to true
17-
- [ ] test/use-swr-streaming-ssr.test.tsx — should match the ssr result when streaming and partially hydrating (failing)
18-
- [ ] test/use-swr-fetcher.test.tsx — should use the latest fetcher reference with the suspense mode when the key has been changed
19-
- [ ] test/use-swr-infinite-preload.test.tsx — preload the fetcher function with the suspense mode
20-
- [ ] test/use-swr-infinite-preload.test.tsx — avoid suspense waterfall by prefetching the resources (skipped)
21-
- [ ] test/use-swr-server.test.tsx — should enable the IS_SERVER flag - suspense on server without fallback
22-
- [ ] test/use-swr-promise.test.tsx — should suspend when resolving the fallback promise
23-
- [ ] test/use-swr-promise.test.tsx — should handle errors with fallback promises
24-
- [ ] test/use-swr-promise.test.tsx — should handle same fallback promise that is already pending
25-
- [ ] test/use-swr-preload.test.tsx — preload the fetcher function with the suspense mode
26-
- [ ] test/use-swr-preload.test.tsx — avoid suspense waterfall by prefetching the resources
3+
- [x] test/use-swr-infinite.test.tsx — should update the getKey reference with the suspense mode (covered in e2e/test/suspense-scenarios.test.ts)
4+
- [x] test/use-swr-suspense.test.tsx — should render fallback (covered in e2e/test/suspense-scenarios.test.ts)
5+
- [x] test/use-swr-suspense.test.tsx — should render multiple SWR fallbacks (covered in e2e/test/suspense-scenarios.test.ts)
6+
- [x] test/use-swr-suspense.test.tsx — should work for non-promises (covered in e2e/test/suspense-scenarios.test.ts)
7+
- [x] test/use-swr-suspense.test.tsx — should throw errors (covered in e2e/test/suspense-scenarios.test.ts)
8+
- [x] test/use-swr-suspense.test.tsx — should render cached data with error (covered in e2e/test/suspense-scenarios.test.ts)
9+
- [x] test/use-swr-suspense.test.tsx — should not fetch when cached data is present and `revalidateIfStale` is false (covered in e2e/test/suspense-scenarios.test.ts)
10+
- [x] test/use-swr-suspense.test.tsx — should pause when key changes (covered in e2e/test/suspense-scenarios.test.ts)
11+
- [x] test/use-swr-suspense.test.tsx — should render correctly when key changes (but with same response data) (covered in e2e/test/suspense-scenarios.test.ts)
12+
- [x] test/use-swr-suspense.test.tsx — should render correctly when key changes (from null to valid key) (covered in e2e/test/suspense-scenarios.test.ts)
13+
- [x] test/use-swr-suspense.test.tsx — should render initial data if set (covered in e2e/test/suspense-scenarios.test.ts)
14+
- [x] test/use-swr-suspense.test.tsx — should avoid unnecessary re-renders (covered in e2e/test/suspense-scenarios.test.ts)
15+
- [x] test/use-swr-suspense.test.tsx — should return `undefined` data for falsy key (covered in e2e/test/suspense-scenarios.test.ts)
16+
- [x] test/use-swr-suspense.test.tsx — should only render fallback once when `keepPreviousData` is set to true (covered in e2e/test/suspense-scenarios.test.ts)
17+
- [x] test/use-swr-streaming-ssr.test.tsx — should match the ssr result when streaming and partially hydrating (covered in e2e/test/streaming-partial-hydration.test.ts)
18+
- [x] test/use-swr-fetcher.test.tsx — should use the latest fetcher reference with the suspense mode when the key has been changed (covered in e2e/test/suspense-scenarios.test.ts)
19+
- [x] test/use-swr-infinite-preload.test.tsx — preload the fetcher function with the suspense mode (covered in e2e/test/suspense-scenarios.test.ts)
20+
- [x] test/use-swr-infinite-preload.test.tsx — avoid suspense waterfall by prefetching the resources (covered in e2e/test/preload-scenarios.test.ts)
21+
- [x] test/use-swr-server.test.tsx — should enable the IS_SERVER flag - suspense on server without fallback (covered in e2e/test/server-suspense.test.ts)
22+
- [x] test/use-swr-promise.test.tsx — should suspend when resolving the fallback promise (covered in e2e/test/promise-scenarios.test.ts)
23+
- [x] test/use-swr-promise.test.tsx — should handle errors with fallback promises (covered in e2e/test/promise-scenarios.test.ts)
24+
- [x] test/use-swr-promise.test.tsx — should handle same fallback promise that is already pending (covered in e2e/test/promise-scenarios.test.ts)
25+
- [x] test/use-swr-preload.test.tsx — preload the fetcher function (covered in e2e/test/preload-scenarios.test.ts) with the suspense mode
26+
- [x] test/use-swr-preload.test.tsx — avoid suspense waterfall by prefetching the resources (covered in e2e/test/preload-scenarios.test.ts)
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
'use client'
2+
3+
import { Suspense, useEffect, useState } from 'react'
4+
import useSWR, { preload } from 'swr'
5+
import { OnlyRenderInClient } from '~/component/only-render-in-client'
6+
import { sleep } from '~/lib/sleep'
7+
8+
const keyA = 'render-preload-avoid-waterfall:a'
9+
const keyB = 'render-preload-avoid-waterfall:b'
10+
const delay = 200
11+
12+
async function fetcherA() {
13+
await sleep(delay)
14+
return 'foo'
15+
}
16+
17+
async function fetcherB() {
18+
await sleep(delay)
19+
return 'bar'
20+
}
21+
22+
function Preload({ children }: { children?: React.ReactNode }) {
23+
const [ready, setReady] = useState(false)
24+
25+
useEffect(() => {
26+
preload(keyA, fetcherA)
27+
preload(keyB, fetcherB)
28+
setReady(true)
29+
}, [])
30+
31+
return ready ? <>{children}</> : null
32+
}
33+
34+
function Content() {
35+
const { data: first } = useSWR(keyA, fetcherA, { suspense: true })
36+
const { data: second } = useSWR(keyB, fetcherB, { suspense: true })
37+
38+
useEffect(() => {
39+
if (!first || !second) {
40+
return
41+
}
42+
}, [first, second])
43+
44+
return (
45+
<div style={{ display: 'grid', gap: '0.5rem' }}>
46+
<div data-testid="data">
47+
data:{first}:{second}
48+
</div>
49+
</div>
50+
)
51+
}
52+
53+
export default function Page() {
54+
return (
55+
<OnlyRenderInClient>
56+
<Preload>
57+
<Suspense fallback={<div data-testid="fallback">Loading...</div>}>
58+
<Content />
59+
</Suspense>
60+
</Preload>
61+
</OnlyRenderInClient>
62+
)
63+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use client'
2+
3+
import { Suspense, useEffect, useState } from 'react'
4+
import useSWR, { preload } from 'swr'
5+
import { OnlyRenderInClient } from '~/component/only-render-in-client'
6+
import { sleep } from '~/lib/sleep'
7+
8+
const key = 'render-preload-basic'
9+
let fetchCount = 0
10+
11+
async function fetcher() {
12+
await sleep(100)
13+
fetchCount += 1
14+
return 'foo'
15+
}
16+
17+
function Preload({ children }: { children?: React.ReactNode }) {
18+
const [isPreloaded, setIsPreloaded] = useState(false)
19+
20+
useEffect(() => {
21+
preload(key, fetcher)
22+
setIsPreloaded(true)
23+
}, [])
24+
return <>{isPreloaded ? children : null}</>
25+
}
26+
27+
export default function Page() {
28+
const { data } = useSWR(key, fetcher)
29+
const [count, setCount] = useState(fetchCount)
30+
31+
useEffect(() => {
32+
setCount(fetchCount)
33+
}, [data])
34+
35+
return (
36+
<OnlyRenderInClient>
37+
<Preload>
38+
<Suspense fallback={<div>Loading...</div>}>
39+
<div style={{ display: 'grid', gap: '0.5rem' }}>
40+
<div data-testid="data">data:{data ?? ''}</div>
41+
<div data-testid="fetch-count">fetches: {count}</div>
42+
</div>
43+
</Suspense>
44+
</Preload>
45+
</OnlyRenderInClient>
46+
)
47+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
'use client'
2+
3+
import { Suspense, useMemo, useState } from 'react'
4+
import type { ReactNode } from 'react'
5+
import { ErrorBoundary } from 'react-error-boundary'
6+
import useSWR, { SWRConfig } from 'swr'
7+
import { OnlyRenderInClient } from '~/component/only-render-in-client'
8+
import { sleep } from '~/lib/sleep'
9+
10+
const key = 'render-promise-suspense-error'
11+
const fallbackDelay = 150
12+
13+
function PromiseConfig({ children }: { children: ReactNode }) {
14+
const [fallback] = useState(() =>
15+
sleep(fallbackDelay).then(() => {
16+
throw new Error('error')
17+
})
18+
)
19+
20+
const value = useMemo(() => ({ fallback: { [key]: fallback } }), [fallback])
21+
22+
return <SWRConfig value={value}>{children}</SWRConfig>
23+
}
24+
25+
function Content() {
26+
const { data } = useSWR<string>(key)
27+
return <div data-testid="data">data:{data ?? 'undefined'}</div>
28+
}
29+
30+
export default function Page() {
31+
return (
32+
<OnlyRenderInClient>
33+
<PromiseConfig>
34+
<ErrorBoundary
35+
fallbackRender={({ error }) => (
36+
<div data-testid="error">{error.message}</div>
37+
)}
38+
>
39+
<Suspense fallback={<div data-testid="fallback">loading</div>}>
40+
<Content />
41+
</Suspense>
42+
</ErrorBoundary>
43+
</PromiseConfig>
44+
</OnlyRenderInClient>
45+
)
46+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'use client'
2+
3+
import { Suspense, useMemo, useState } from 'react'
4+
import type { ReactNode } from 'react'
5+
import useSWR, { SWRConfig } from 'swr'
6+
import { OnlyRenderInClient } from '~/component/only-render-in-client'
7+
import { sleep } from '~/lib/sleep'
8+
import { useDebugHistory } from '~/lib/use-debug-history'
9+
10+
const key = 'render-promise-suspense-resolve'
11+
const fallbackDelay = 150
12+
const fetchDelay = 200
13+
14+
async function fetcher() {
15+
await sleep(fetchDelay)
16+
return 'new data'
17+
}
18+
19+
function PromiseConfig({ children }: { children: ReactNode }) {
20+
const [fallback] = useState(() =>
21+
sleep(fallbackDelay).then(() => 'initial data')
22+
)
23+
24+
const value = useMemo(() => ({ fallback: { [key]: fallback } }), [fallback])
25+
26+
return <SWRConfig value={value}>{children}</SWRConfig>
27+
}
28+
29+
function Content() {
30+
const { data } = useSWR(key, fetcher)
31+
const historyRef = useDebugHistory(data, 'history:')
32+
33+
return (
34+
<div style={{ display: 'grid', gap: '0.5rem' }}>
35+
<div data-testid="data">data:{data ?? 'undefined'}</div>
36+
<div data-testid="history" ref={historyRef}></div>
37+
</div>
38+
)
39+
}
40+
41+
export default function Page() {
42+
return (
43+
<OnlyRenderInClient>
44+
<PromiseConfig>
45+
<Suspense fallback={<div data-testid="fallback">loading</div>}>
46+
<Content />
47+
</Suspense>
48+
</PromiseConfig>
49+
</OnlyRenderInClient>
50+
)
51+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use client'
2+
3+
import { Suspense, useMemo, useState } from 'react'
4+
import type { ReactNode } from 'react'
5+
import useSWR, { SWRConfig } from 'swr'
6+
import { OnlyRenderInClient } from '~/component/only-render-in-client'
7+
import { sleep } from '~/lib/sleep'
8+
9+
const key = 'render-promise-suspense-shared'
10+
const fallbackDelay = 150
11+
12+
function PromiseConfig({ children }: { children: ReactNode }) {
13+
const [fallback] = useState(() => sleep(fallbackDelay).then(() => 'value'))
14+
const value = useMemo(() => ({ fallback: { [key]: fallback } }), [fallback])
15+
return <SWRConfig value={value}>{children}</SWRConfig>
16+
}
17+
18+
function Item({ id }: { id: string }) {
19+
const { data } = useSWR<string>(key)
20+
return <div data-testid={`data-${id}`}>data:{data ?? 'undefined'}</div>
21+
}
22+
23+
export default function Page() {
24+
return (
25+
<OnlyRenderInClient>
26+
<PromiseConfig>
27+
<Suspense fallback={<div data-testid="fallback">loading</div>}>
28+
<div style={{ display: 'grid', gap: '0.5rem' }}>
29+
<Item id="first" />
30+
<Item id="second" />
31+
</div>
32+
</Suspense>
33+
</PromiseConfig>
34+
</OnlyRenderInClient>
35+
)
36+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use client'
2+
3+
import { Suspense, useRef } from 'react'
4+
import useSWR from 'swr'
5+
import { OnlyRenderInClient } from '~/component/only-render-in-client'
6+
import { sleep } from '~/lib/sleep'
7+
8+
async function fetchValue() {
9+
await sleep(150)
10+
return 'SWR'
11+
}
12+
13+
function Section() {
14+
const startCountRef = useRef(0)
15+
const dataCountRef = useRef(0)
16+
const prevDataRef = useRef<any>(Symbol('initial'))
17+
18+
startCountRef.current += 1
19+
20+
const { data } = useSWR('render-suspense-avoid-rerender', fetchValue, {
21+
suspense: true
22+
})
23+
24+
if (data !== prevDataRef.current) {
25+
if (data !== undefined) {
26+
dataCountRef.current += 1
27+
}
28+
prevDataRef.current = data
29+
}
30+
31+
return (
32+
<div style={{ display: 'grid', gap: '0.5rem' }}>
33+
<div data-testid="start-count">
34+
start renders: {startCountRef.current}
35+
</div>
36+
<div data-testid="data-count">data renders: {dataCountRef.current}</div>
37+
<div data-testid="data">{data}</div>
38+
</div>
39+
)
40+
}
41+
42+
export default function Page() {
43+
return (
44+
<OnlyRenderInClient>
45+
<Suspense fallback={<div data-testid="fallback">fallback</div>}>
46+
<Section />
47+
</Suspense>
48+
</OnlyRenderInClient>
49+
)
50+
}

0 commit comments

Comments
 (0)