Skip to content

Commit

Permalink
fix: full rewrite of ScopeProvider to address known issues
Browse files Browse the repository at this point in the history
- fixes: #25, #36
  • Loading branch information
dmaskasky committed Sep 25, 2024
1 parent 3198ee1 commit 43a657c
Show file tree
Hide file tree
Showing 54 changed files with 11,225 additions and 324 deletions.
4 changes: 3 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/no-non-null-asserted-optional-chain": "off",
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
"react/button-has-type": "off",
"react/jsx-filename-extension": ["error", { "extensions": [".js", ".tsx"] }],
"react/prop-types": "off",
Expand All @@ -41,6 +42,7 @@
"symbol-description": "off",
"prefer-object-spread": "off",
"no-return-assign": "off",
"no-sparse-arrays": "off",
"no-use-before-define": "off",
"no-unused-vars": "off",
"no-redeclare": "off",
Expand Down
2 changes: 1 addition & 1 deletion __tests__/ScopeProvider/01_basic_spec.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ describe('Counter', () => {
})

/*
base, derivedA(base), derivemB(base)
base, derivedA(base), derivedB(base)
S0[derivedA, derivedB]: derivedA0(base0), derivedB0(base0)
S1[derivedA, derivedB]: derivedA1(base1), derivedB1(base1)
*/
Expand Down
213 changes: 213 additions & 0 deletions __tests__/derive/baseTests/react/abortable.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { StrictMode, Suspense, useState } from 'react'
import { render, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useAtomValue, useSetAtom } from 'jotai/react'
import { atom } from 'jotai/vanilla'

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)
})
if (signal.aborted) {
++abortedCount
}
return count
})

function Component() {
const count = useAtomValue(derivedAtom)
return <div>count: {count}</div>
}

function Controls() {
const setCount = useSetAtom(countAtom)
return <button onClick={() => setCount((c) => c + 1)}>button</button>
}

const { findByText, getByText } = render(
<StrictMode>
<Suspense fallback="loading">
<Component />
<Controls />
</Suspense>
</StrictMode>,
)

await findByText('loading')

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

await userEvent.click(getByText('button'))
await userEvent.click(getByText('button'))
resolve.splice(0).forEach((fn) => fn())
await findByText('count: 2')

expect(abortedCount).toBe(1)

await userEvent.click(getByText('button'))
resolve.splice(0).forEach((fn) => fn())
await findByText('count: 3')
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))
signal.removeEventListener('abort', callback)
return count
})

function Component() {
const count = useAtomValue(derivedAtom)
return <div>count: {count}</div>
}

function Controls() {
const setCount = useSetAtom(countAtom)
return <button onClick={() => setCount((c) => c + 1)}>button</button>
}

const { findByText, getByText } = render(
<StrictMode>
<Suspense fallback="loading">
<Component />
<Controls />
</Suspense>
</StrictMode>,
)

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

expect(abortedCount).toBe(0)

await userEvent.click(getByText('button'))
await userEvent.click(getByText('button'))
resolve.splice(0).forEach((fn) => fn())
await findByText('count: 2')

expect(abortedCount).toBe(1)

await userEvent.click(getByText('button'))
resolve.splice(0).forEach((fn) => fn())
await findByText('count: 3')

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))
if (signal.aborted) {
++abortedCount
}
return count
})

function Component() {
const count = useAtomValue(derivedAtom)
return <div>count: {count}</div>
}

function Parent() {
const setCount = useSetAtom(countAtom)
const [show, setShow] = useState(true)
return (
<>
{show ? <Component /> : 'hidden'}
<button onClick={() => setCount((c) => c + 1)}>button</button>
<button onClick={() => setShow((x) => !x)}>toggle</button>
</>
)
}

const { findByText, getByText } = render(
<StrictMode>
<Suspense fallback="loading">
<Parent />
</Suspense>
</StrictMode>,
)

await findByText('loading')

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

await userEvent.click(getByText('button'))
await userEvent.click(getByText('toggle'))

await findByText('hidden')

resolve.splice(0).forEach((fn) => fn())
await waitFor(() => 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))
if (signal.aborted) {
throw new Error('aborted')
}
return count
})

function Component() {
const count = useAtomValue(derivedAtom)
return <div>count: {count}</div>
}

function Controls() {
const setCount = useSetAtom(countAtom)
return <button onClick={() => setCount((c) => c + 1)}>button</button>
}

const { findByText, getByText } = render(
<StrictMode>
<Suspense fallback="loading">
<Component />
<Controls />
</Suspense>
</StrictMode>,
)

await findByText('loading')

resolve.splice(0).forEach((fn) => fn())
await findByText('count: 0')

await userEvent.click(getByText('button'))
await userEvent.click(getByText('button'))
resolve.splice(0).forEach((fn) => fn())
await findByText('count: 2')

await userEvent.click(getByText('button'))
resolve.splice(0).forEach((fn) => fn())
await findByText('count: 3')
})
})
Loading

0 comments on commit 43a657c

Please sign in to comment.