Skip to content

Conversation

@sukvvon
Copy link
Contributor

@sukvvon sukvvon commented Sep 13, 2025

Related Bug Reports or Discussions

Fixes #

Summary

This PR migrates all tests from real timers to Vitest fake timers to improve test determinism and execution speed. The migration includes comprehensive updates across 33 test files, eliminates most React act warnings, and removes the @testing-library/user-event dependency in favor of fireEvent.

Key Changes

  1. Migrate to Vitest fake timers - Add beforeEach(vi.useFakeTimers) and afterEach(vi.useRealTimers)
  2. Replace userEvent with fireEvent - Use synchronous event simulation (userEvent uses real timers internally)
  3. Replace Promise resolver pattern with setTimeout delays - Convert manual promise resolution to timer-controlled delays
  4. Replace Testing Library async utilities - Switch from findByText/waitFor to getByText with explicit timer control
  5. Add loading state assertions - Verify Suspense loading states during async transitions
  6. Fix React act warnings - Wrap operations properly to eliminate console warnings

Why Replace userEvent with fireEvent?

  • userEvent uses real timers internally, conflicting with fake timers
  • fireEvent is synchronous and fully compatible with vi.useFakeTimers()
  • For state management testing, we need logic correctness, not interaction realism
  • Simpler, faster, and one less dependency

fireEvent Pattern (Simple Rule)

Use await act(() => fireEvent.click(...)) when immediately checking Suspense loading state.

Otherwise use plain fireEvent.click(...).

Examples

With Suspense (need act):

await act(() => fireEvent.click(screen.getByText('button')))
expect(screen.getByText('loading')).toBeInTheDocument() // ← checking loading right after
await act(() => vi.advanceTimersByTimeAsync(100))
expect(screen.getByText('result')).toBeInTheDocument()

Without Suspense (plain fireEvent):

fireEvent.click(screen.getByText('button')) // ← no loading check needed
expect(screen.getByText('count: 1')).toBeInTheDocument()

Pro tip: When in doubt, try plain fireEvent first. If the test fails or shows act warnings, wrap with await act().

Statistics

  • Files changed: 33 test files + package.json
  • Lines changed: +2,249 / -2,229 (net +20)
  • Dependencies removed: @testing-library/user-event

Check List

  • pnpm run fix for formatting and linting code and docs

@vercel
Copy link

vercel bot commented Sep 13, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
jotai Ready Ready Preview Comment Nov 5, 2025 5:25pm

@codesandbox-ci
Copy link

codesandbox-ci bot commented Sep 13, 2025

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Sep 13, 2025

More templates

npm i https://pkg.pr.new/jotai@3147

commit: 1442388

@github-actions
Copy link

github-actions bot commented Sep 13, 2025

LiveCodes Preview in LiveCodes

Latest commit: 1442388
Last updated: Nov 5, 2025 5:24pm (UTC)

Playground Link
React demo https://livecodes.io?x=id/BFT2XY2NG

See documentations for usage instructions.

…ByText' with 'getByText', and add fake timers
…ByText' with 'getByText', and remove 'waitFor'
…t' with 'getByText', add fake timer, and remove 'waitFor'
@dai-shi
Copy link
Member

dai-shi commented Nov 2, 2025

I feel like I'm talking to AI. My review is not comprehensive, and I need to count on you @sukvvon human to review all tests follow what I suggested.

@sukvvon
Copy link
Contributor Author

sukvvon commented Nov 2, 2025

@dai-shi

I feel like I'm talking to AI. My review is not comprehensive, and I need to count on you @sukvvon human to review all tests follow what I suggested.

I understand. Thank you for sharing your thoughts.

Copy link
Member

@dai-shi dai-shi left a comment

Choose a reason for hiding this comment

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

As I felt like I might overlook something, I reviewed abortable.test.tsx and async.test.tsx again.

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.


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.

const asyncCountAtom = atom(
async (get) => get(countAtom),
async (get) => {
await new Promise<void>((resolve) => setTimeout(resolve, 100))
Copy link
Member

Choose a reason for hiding this comment

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

Actually "it" says "without setTimeout", so maybe we want to remove this and remove "loading" check if necessary.

https://github.com/pmndrs/jotai/blob/b6ada607bd928e54214ebb00c5ef0838f53dfe2e/tests/async.test.tsx is the original commit. We used setTimeout for the following test.

What do you think?

Copy link
Contributor Author

@sukvvon sukvvon Nov 5, 2025

Choose a reason for hiding this comment

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

@dai-shi I agree with you. I removed in 1442388. Also removed loading assertions since there's no async delay.

it('async get and useEffect on parent', async () => {
const countAtom = atom(0)
const asyncAtom = atom(async (get) => {
await new Promise((resolve) => setTimeout(resolve, 100))
Copy link
Member

Choose a reason for hiding this comment

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

Maybe this too.

Comment on lines 459 to 460
expect(screen.getByText('loading')).toBeInTheDocument()
await act(() => vi.advanceTimersByTimeAsync(100))
Copy link
Member

Choose a reason for hiding this comment

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

Can remove this if we remove that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants