Skip to content
Open
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
93723d9
test(react/useSetAtom): replace 'userEvent' with 'fireEvent'
sukvvon Sep 13, 2025
9c44611
test(react/useAtomValue): replace 'userEvent' with 'fireEvent', 'find…
sukvvon Sep 13, 2025
c93671a
test(react/provider): remove 'waitFor'
sukvvon Sep 13, 2025
c871edf
test(react/optimization): replace 'userEvent' with 'fireEvent', 'find…
sukvvon Sep 13, 2025
1c953e2
test(react/onmount): replace 'userEvent' with 'fireEvent', 'findByTex…
sukvvon Sep 13, 2025
a06801a
test(react/items): replace 'userEvent' with 'fireEvent', 'findByText'…
sukvvon Sep 13, 2025
f6140d4
test(react/dependency): replace 'userEvent' with 'fireEvent', 'findBy…
sukvvon Sep 14, 2025
1e1686e
test(react/vanilla-utils/atomFamily): replace 'userEvent' with 'fireE…
sukvvon Sep 14, 2025
5a0345a
test(react/vanilla-utils/atomWithDefault): replace 'userEvent' with '…
sukvvon Sep 14, 2025
f9fad21
test(react/vanilla-utils/atomWithReducer): replace 'userEvent' with '…
sukvvon Sep 14, 2025
f94ed77
test(react/vanilla-utils/atomWithRefresh): replace 'userEvent' with '…
sukvvon Sep 14, 2025
56c178a
Merge branch 'main' into test/migrate-to-vitest-fake-timer
sukvvon Sep 22, 2025
70ee812
test(react/vanilla-utils/atomWithStorage): replace 'userEvent' with '…
sukvvon Sep 25, 2025
d046470
Merge branch 'main' into test/migrate-to-vitest-fake-timer
sukvvon Sep 25, 2025
f5c7ea2
Merge branch 'main' into test/migrate-to-vitest-fake-timer
sukvvon Oct 5, 2025
ecabe83
test(react/abortable): replace Promise resolver pattern with setTimeo…
sukvvon Oct 12, 2025
aeee4dd
test(react/async): replace Promise resolver pattern with setTimeout d…
sukvvon Oct 18, 2025
00dd86f
test(react/async2): replace Promise resolver pattern with setTimeout …
sukvvon Oct 19, 2025
d6fb6f0
test(react/basic): migrate to Vitest fake timers, replace Promise res…
sukvvon Oct 19, 2025
54a94c4
test(react/vanilla-utils/freezeAtom): replace 'userEvent' with 'fireE…
sukvvon Oct 19, 2025
6dad0b2
test(react/vanilla-utils/selectAtom): replace 'userEvent' with 'fireE…
sukvvon Oct 19, 2025
61be143
test(react/vanilla-utils/splitAtom): replace 'userEvent' with 'fireEv…
sukvvon Oct 19, 2025
54e3df2
test(react/utils/useAtomCallback): replace 'userEvent' with 'fireEven…
sukvvon Oct 19, 2025
5a3a628
test(react/utils/useHydrateAtoms): replace 'userEvent' with 'fireEven…
sukvvon Oct 19, 2025
1c76bce
test(react/utils/useReducerAtom): replace 'userEvent' with 'fireEvent…
sukvvon Oct 19, 2025
801f462
test(react/utils/useResetAtom): replace 'userEvent' with 'fireEvent',…
sukvvon Oct 19, 2025
a84617a
Merge branch 'main' into test/migrate-to-vitest-fake-timer
sukvvon Oct 20, 2025
501c99c
test(vanilla/utils/atomWithLazy): remove unnecessary 'async' keywords
sukvvon Oct 25, 2025
69f6dbd
test(vanilla/utils/loadable): replace Promise.resolve with setTimeout…
sukvvon Oct 25, 2025
118a594
test(vanilla/utils/unwrap): replace Promise resolver pattern with set…
sukvvon Oct 25, 2025
0a7f38d
test(vanilla/dependency): replace Promise resolver pattern with setTi…
sukvvon Oct 25, 2025
899d135
test(vanilla/effect): replace 'Promise.resolve' with 'vi.advanceTimer…
sukvvon Oct 25, 2025
96bfcc6
test(vanilla/store): migrate to Vitest fake timers, remove unnecessar…
sukvvon Oct 25, 2025
117443d
test(react/transition): replace 'userEvent' with 'fireEvent', replace…
sukvvon Oct 25, 2025
44e2422
test(react/error): replace 'userEvent' with 'fireEvent', add loading …
sukvvon Oct 25, 2025
a3fed86
test(react/vanilla-utils/loadable): replace Promise resolver pattern …
sukvvon Oct 25, 2025
d693163
test(react/async2): remove unnecessary 'act' wrapping from 'render', …
sukvvon Oct 25, 2025
521c20f
test(react/dependency): add 'async' keyword and 'await' to 'act' call…
sukvvon Oct 25, 2025
a537d89
test(react/abortable, async, dependency, error, onmount, transition):…
sukvvon Oct 26, 2025
a9a4abf
test(react/vanilla-utils/atomWithObservable): replace 'userEvent' wit…
sukvvon Oct 26, 2025
105be28
test(react/vanilla-utils/atomFamily, atomWithStorage): remove unneces…
sukvvon Oct 26, 2025
db93174
test(react): replace string Suspense fallback with JSX element
sukvvon Oct 26, 2025
66b310c
test(react/abortable): wrap fireEvent.click with await act to resolve…
sukvvon Oct 26, 2025
90b902e
test(react/async): wrap fireEvent.click with await act to resolve act…
sukvvon Oct 26, 2025
01ac25b
test(react/vanilla-utils/atomWithStorage): wrap render and fireEvent.…
sukvvon Oct 26, 2025
528d028
test(react/vanilla-utils/loadable): wrap render with await act to res…
sukvvon Oct 26, 2025
1696533
test(react/async2): wrap fireEvent.click and timer advance with await…
sukvvon Oct 26, 2025
03add44
chore(*): remove '@testing-library/user-event'
sukvvon Oct 26, 2025
ed61b2f
test(react): wrap render calls with StrictMode, use Fragment for timi…
sukvvon Oct 29, 2025
16e6546
test(react/async2): reduce to single assertion in setSelf timing test
sukvvon Oct 29, 2025
9c85c41
test(react/async): fix setCountAtom to use Promise with setTimeout
sukvvon Oct 29, 2025
d697f23
test(react/async2): restore infinite pending test with Promise<never>
sukvvon Oct 29, 2025
a3bf362
Update tests/react/async.test.tsx
sukvvon Oct 29, 2025
99c839e
test(react/async2): remove unnecessary act wrapper from fireEvent calls
sukvvon Oct 30, 2025
54d053f
test(react/async): fix multiple async atoms test with precise timer c…
sukvvon Oct 30, 2025
0124c86
Merge branch 'main' into test/migrate-to-vitest-fake-timer
sukvvon Nov 2, 2025
cae2ff2
test(vanilla/utils/unwrap): replace 'setTimeout' with fake timer in e…
sukvvon Nov 2, 2025
7647031
test(react/error): revert read function to sync in async write functi…
sukvvon Nov 2, 2025
eb4b6b4
test(react/dependency): restore intentional async keyword in activate…
sukvvon Nov 2, 2025
278abe2
test(react/dependency): remove setTimeout from asyncAtom to restore m…
sukvvon Nov 2, 2025
aaef034
test(react/dependency): add initial loading assertion for async depen…
sukvvon Nov 2, 2025
b8c465c
test(react/basic): replace FIXME with NOTE explaining no loading state
sukvvon Nov 2, 2025
db138c3
Revert "test(react/dependency): remove setTimeout from asyncAtom to r…
sukvvon Nov 2, 2025
f8cf85e
test(react/dependency): add loading assertion and remove unnecessary …
sukvvon Nov 2, 2025
f49290e
test(react/async2): restore intermediate assertions with 'expect.asse…
sukvvon Nov 2, 2025
3f49240
test(react): simplify Suspense fallback from '<div>loading</div>' to …
sukvvon Nov 5, 2025
ffda141
test(react/abortable): remove redundant 'abortedCount' assertion befo…
sukvvon Nov 5, 2025
1442388
test(react/async): remove 'setTimeout' from immediately-resolving asy…
sukvvon Nov 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/babel__core": "^7.20.5",
"@types/babel__template": "^7.4.4",
"@types/node": "^24.5.2",
Expand Down
21 changes: 4 additions & 17 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

149 changes: 80 additions & 69 deletions tests/react/abortable.test.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import { StrictMode, Suspense, useState } from 'react'
import { act, render, screen, waitFor } from '@testing-library/react'
import userEventOrig from '@testing-library/user-event'
import { describe, expect, it } from 'vitest'
import { act, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useAtomValue, useSetAtom } from 'jotai/react'
import { atom } from 'jotai/vanilla'

const userEvent = {
click: (element: Element) => act(() => userEventOrig.click(element)),
}
beforeEach(() => {
vi.useFakeTimers()
})

afterEach(() => {
vi.useRealTimers()
})

describe('abortable atom test', () => {
it('can abort with signal.aborted', async () => {
const countAtom = atom(0)
let abortedCount = 0
const resolve: (() => void)[] = []
const derivedAtom = atom(async (get, { signal }) => {
const count = get(countAtom)
await new Promise<void>((r) => resolve.push(r))
await new Promise((resolve) => setTimeout(resolve, 100))
if (signal.aborted) {
++abortedCount
}
Expand All @@ -37,47 +39,51 @@ describe('abortable atom test', () => {
)
}

await act(async () => {
await act(() =>
render(
<StrictMode>
<Suspense fallback="loading">
<Suspense fallback={<div>loading</div>}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change important? If so, can you create another PR? We would like to reduce diffs so that it reduces the human brain power for the review.
If you use AI coding assistant, asking them to reduce git diff -w output might be helpful.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dai-shi Changed all Suspense fallback from <div>loading</div> to "loading" for simplicity. Also changed error.test.tsx from fallback={null} to fallback="loading" since null fallback doesn't provide meaningful loading state in 3f49240.

<Component />
<Controls />
</Suspense>
</StrictMode>,
)
})
),
)

expect(await screen.findByText('loading')).toBeInTheDocument()
expect(screen.getByText('loading')).toBeInTheDocument()

await act(() => vi.advanceTimersByTimeAsync(100))
expect(screen.getByText('count: 0')).toBeInTheDocument()

resolve.splice(0).forEach((fn) => fn())
expect(await screen.findByText('count: 0')).toBeInTheDocument()
expect(abortedCount).toBe(0)

await userEvent.click(screen.getByText('button'))
await userEvent.click(screen.getByText('button'))
resolve.splice(0).forEach((fn) => fn())
expect(await screen.findByText('count: 2')).toBeInTheDocument()
await act(() => fireEvent.click(screen.getByText('button')))
expect(screen.getByText('loading')).toBeInTheDocument()
await act(() => fireEvent.click(screen.getByText('button')))
expect(screen.getByText('loading')).toBeInTheDocument()
await act(() => vi.advanceTimersByTimeAsync(100))
expect(screen.getByText('count: 2')).toBeInTheDocument()

expect(abortedCount).toBe(1)

await userEvent.click(screen.getByText('button'))
resolve.splice(0).forEach((fn) => fn())
expect(await screen.findByText('count: 3')).toBeInTheDocument()
await act(() => fireEvent.click(screen.getByText('button')))
expect(screen.getByText('loading')).toBeInTheDocument()
await act(() => vi.advanceTimersByTimeAsync(100))
expect(screen.getByText('count: 3')).toBeInTheDocument()

expect(abortedCount).toBe(1)
})

it('can abort with event listener', async () => {
const countAtom = atom(0)
let abortedCount = 0
const resolve: (() => void)[] = []
const derivedAtom = atom(async (get, { signal }) => {
const count = get(countAtom)
const callback = () => {
++abortedCount
}
signal.addEventListener('abort', callback)
await new Promise<void>((r) => resolve.push(r))
await new Promise((resolve) => setTimeout(resolve, 100))
signal.removeEventListener('abort', callback)
return count
})
Expand All @@ -96,44 +102,47 @@ describe('abortable atom test', () => {
)
}

await act(async () => {
await act(() =>
render(
<StrictMode>
<Suspense fallback="loading">
<Suspense fallback={<div>loading</div>}>
<Component />
<Controls />
</Suspense>
</StrictMode>,
)
})
),
)

expect(await screen.findByText('loading')).toBeInTheDocument()
resolve.splice(0).forEach((fn) => fn())
expect(await screen.findByText('count: 0')).toBeInTheDocument()
expect(screen.getByText('loading')).toBeInTheDocument()

await act(() => vi.advanceTimersByTimeAsync(100))
expect(screen.getByText('count: 0')).toBeInTheDocument()

expect(abortedCount).toBe(0)

await userEvent.click(screen.getByText('button'))
await userEvent.click(screen.getByText('button'))
resolve.splice(0).forEach((fn) => fn())
expect(await screen.findByText('count: 2')).toBeInTheDocument()
await act(() => fireEvent.click(screen.getByText('button')))
expect(screen.getByText('loading')).toBeInTheDocument()
await act(() => fireEvent.click(screen.getByText('button')))
expect(screen.getByText('loading')).toBeInTheDocument()
await act(() => vi.advanceTimersByTimeAsync(100))
expect(screen.getByText('count: 2')).toBeInTheDocument()

expect(abortedCount).toBe(1)

await userEvent.click(screen.getByText('button'))
resolve.splice(0).forEach((fn) => fn())
expect(await screen.findByText('count: 3')).toBeInTheDocument()
await act(() => fireEvent.click(screen.getByText('button')))
expect(screen.getByText('loading')).toBeInTheDocument()
await act(() => vi.advanceTimersByTimeAsync(100))
expect(screen.getByText('count: 3')).toBeInTheDocument()

expect(abortedCount).toBe(1)
})

it('does not abort on unmount', async () => {
const countAtom = atom(0)
let abortedCount = 0
const resolve: (() => void)[] = []
const derivedAtom = atom(async (get, { signal }) => {
const count = get(countAtom)
await new Promise<void>((r) => resolve.push(r))
await new Promise((resolve) => setTimeout(resolve, 100))
if (signal.aborted) {
++abortedCount
}
Expand All @@ -157,37 +166,36 @@ describe('abortable atom test', () => {
)
}

await act(async () => {
await act(() =>
render(
<StrictMode>
<Suspense fallback="loading">
<Suspense fallback={<div>loading</div>}>
<Parent />
</Suspense>
</StrictMode>,
)
})
),
)

expect(await screen.findByText('loading')).toBeInTheDocument()
expect(screen.getByText('loading')).toBeInTheDocument()

resolve.splice(0).forEach((fn) => fn())
expect(await screen.findByText('count: 0')).toBeInTheDocument()
expect(abortedCount).toBe(0)
await act(() => vi.advanceTimersByTimeAsync(100))
expect(screen.getByText('count: 0')).toBeInTheDocument()

await userEvent.click(screen.getByText('button'))
await userEvent.click(screen.getByText('toggle'))
expect(abortedCount).toBe(0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this line? You can leave it if it's helpful.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dai-shi Removed redundant abortedCount check before unmount in ffda141. The meaningful assertion is after unmount to verify abort didn't happen.


expect(await screen.findByText('hidden')).toBeInTheDocument()
await act(() => fireEvent.click(screen.getByText('button')))
expect(screen.getByText('loading')).toBeInTheDocument()
await act(() => fireEvent.click(screen.getByText('toggle')))
expect(screen.getByText('hidden')).toBeInTheDocument()

resolve.splice(0).forEach((fn) => fn())
await waitFor(() => expect(abortedCount).toBe(0))
expect(abortedCount).toBe(0)
})

it('throws aborted error (like fetch)', async () => {
const countAtom = atom(0)
const resolve: (() => void)[] = []
const derivedAtom = atom(async (get, { signal }) => {
const count = get(countAtom)
await new Promise<void>((r) => resolve.push(r))
await new Promise((resolve) => setTimeout(resolve, 100))
if (signal.aborted) {
throw new Error('aborted')
}
Expand All @@ -208,29 +216,32 @@ describe('abortable atom test', () => {
)
}

await act(async () => {
await act(() =>
render(
<StrictMode>
<Suspense fallback="loading">
<Suspense fallback={<div>loading</div>}>
<Component />
<Controls />
</Suspense>
</StrictMode>,
)
})
),
)

expect(await screen.findByText('loading')).toBeInTheDocument()
expect(screen.getByText('loading')).toBeInTheDocument()

resolve.splice(0).forEach((fn) => fn())
expect(await screen.findByText('count: 0')).toBeInTheDocument()
await act(() => vi.advanceTimersByTimeAsync(100))
expect(screen.getByText('count: 0')).toBeInTheDocument()

await userEvent.click(screen.getByText('button'))
await userEvent.click(screen.getByText('button'))
resolve.splice(0).forEach((fn) => fn())
expect(await screen.findByText('count: 2')).toBeInTheDocument()
await act(() => fireEvent.click(screen.getByText('button')))
expect(screen.getByText('loading')).toBeInTheDocument()
await act(() => fireEvent.click(screen.getByText('button')))
expect(screen.getByText('loading')).toBeInTheDocument()
await act(() => vi.advanceTimersByTimeAsync(100))
expect(screen.getByText('count: 2')).toBeInTheDocument()

await userEvent.click(screen.getByText('button'))
resolve.splice(0).forEach((fn) => fn())
expect(await screen.findByText('count: 3')).toBeInTheDocument()
await act(() => fireEvent.click(screen.getByText('button')))
expect(screen.getByText('loading')).toBeInTheDocument()
await act(() => vi.advanceTimersByTimeAsync(100))
expect(screen.getByText('count: 3')).toBeInTheDocument()
})
})
Loading
Loading