From ac71e615cc95ace78532c2b44ac21dd3347ab99c Mon Sep 17 00:00:00 2001 From: Joseph Chamochumbi Date: Thu, 16 Dec 2021 16:17:16 +0100 Subject: [PATCH 1/5] feat: Keep track `updatedAt` as part of the SWR state --- src/types.ts | 2 + src/use-swr.ts | 5 ++ src/utils/state.ts | 3 +- test/use-swr-key.test.tsx | 44 +++++++++++++- test/use-swr-loading.test.tsx | 2 +- test/use-swr-updated-at.test.tsx | 98 ++++++++++++++++++++++++++++++++ 6 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 test/use-swr-updated-at.test.tsx diff --git a/src/types.ts b/src/types.ts index 70ededc14..fef0efd02 100644 --- a/src/types.ts +++ b/src/types.ts @@ -155,6 +155,7 @@ export type State = { data?: Data error?: Error isValidating?: boolean + updatedAt?: number } export type Mutator = ( @@ -196,6 +197,7 @@ export interface SWRResponse { data?: Data error?: Error mutate: KeyedMutator + updatedAt?: number isValidating: boolean } diff --git a/src/use-swr.ts b/src/use-swr.ts index 4603f88a8..11ca5fb72 100644 --- a/src/use-swr.ts +++ b/src/use-swr.ts @@ -203,6 +203,7 @@ export const useSWRHandler = ( // considered here. startAt = CONCURRENT_PROMISES_TS[key] newData = await CONCURRENT_PROMISES[key] + newState.updatedAt = Date.now() if (shouldStartNewRequest) { // If the request isn't interrupted, clean it up after the @@ -515,6 +516,10 @@ export const useSWRHandler = ( get isValidating() { stateDependencies.isValidating = true return isValidating + }, + get updatedAt() { + stateDependencies.updatedAt = true + return stateRef.current.updatedAt } } as SWRResponse } diff --git a/src/utils/state.ts b/src/utils/state.ts index 9e1eed449..79b109574 100644 --- a/src/utils/state.ts +++ b/src/utils/state.ts @@ -23,7 +23,8 @@ export const useStateWithDeps = >( const stateDependenciesRef = useRef({ data: false, error: false, - isValidating: false + isValidating: false, + updatedAt: false }) /** diff --git a/test/use-swr-key.test.tsx b/test/use-swr-key.test.tsx index 208612444..e60b5b69e 100644 --- a/test/use-swr-key.test.tsx +++ b/test/use-swr-key.test.tsx @@ -1,6 +1,6 @@ import { act, fireEvent, screen } from '@testing-library/react' import React, { useState, useEffect } from 'react' -import useSWR from 'swr' +import useSWR, { useSWRConfig } from 'swr' import { createKey, createResponse, renderWithConfig, sleep } from './utils' describe('useSWR - key', () => { @@ -213,4 +213,46 @@ describe('useSWR - key', () => { // Only 1 request since the keys are the same. expect(fetcher).toBeCalledTimes(1) }) + + it.only('gracefully handles mutate on non existing keys', async () => { + const fetcher = jest.fn(() => 'data') + const mutSpy = jest.fn() + + const key = createKey() + const mutKey = createKey() + + function Page() { + const { mutate } = useSWRConfig() + useSWR(key, fetcher) + + return ( +
+ Hello + +
+ ) + } + + renderWithConfig() + await screen.findByText('Hello') + + fireEvent.click(screen.getByRole('button')) + + expect(mutSpy).toHaveBeenCalledTimes(1) + + screen.getByText('Hello') + }) }) diff --git a/test/use-swr-loading.test.tsx b/test/use-swr-loading.test.tsx index 8d47bfa98..aef69712f 100644 --- a/test/use-swr-loading.test.tsx +++ b/test/use-swr-loading.test.tsx @@ -138,7 +138,7 @@ describe('useSWR - loading', () => { } renderWithConfig() - screen.getByText('data,error,isValidating,mutate') + screen.getByText('data,error,isValidating,mutate,updatedAt') }) it('should sync loading states', async () => { diff --git a/test/use-swr-updated-at.test.tsx b/test/use-swr-updated-at.test.tsx new file mode 100644 index 000000000..f483912c5 --- /dev/null +++ b/test/use-swr-updated-at.test.tsx @@ -0,0 +1,98 @@ +import { act, fireEvent, screen } from '@testing-library/react' +import React, { useEffect } from 'react' +import useSWR from 'swr' +import { createKey, renderWithConfig, sleep } from './utils' + +describe('useSWR - updatedAt', () => { + it('should be initially undefined', async () => { + const key = createKey() + + const fetcher = () => { + return 'data' + } + + function Page() { + const { mutate, updatedAt } = useSWR(key, fetcher, { + revalidateOnMount: false + }) + + return + } + + renderWithConfig() + + screen.getByText('data:') + }) + + it('should not trigger re-render if not consumed', async () => { + const key = createKey() + + const fetcher = () => { + return 'data' + } + + const renderSpy = jest.fn() + + function Page() { + const { mutate } = useSWR(key, fetcher, { + revalidateOnMount: false + }) + + renderSpy() + + return + } + + renderWithConfig() + + screen.getByText('data') + + fireEvent.click(screen.getByRole('button')) + + expect(renderSpy).toHaveBeenCalledTimes(1) + }) + + it('should eventually reflect the last time the fetcher was called', async () => { + const key = createKey() + + let fetcherCallTime: number + + const fetcher = () => { + fetcherCallTime = Date.now() + return 'data' + } + + const updateSpy = jest.fn() + + function Page() { + const { mutate, updatedAt } = useSWR(key, fetcher) + + useEffect(() => { + updateSpy(updatedAt) + }, [updatedAt]) + + return + } + + renderWithConfig() + + screen.getByText('data') + + fireEvent.click(screen.getByRole('button')) + + expect(updateSpy).toHaveBeenCalledTimes(1) + + expect(updateSpy.mock.calls[0][0]).toBeUndefined() + + await act(async () => { + await sleep(0) + }) + + expect(updateSpy).toHaveBeenCalledTimes(2) + + const updatedAt = updateSpy.mock.calls[1][0] + expect(updatedAt).toBeDefined() + + expect(updatedAt).toBeGreaterThanOrEqual(fetcherCallTime) + }) +}) From 5f4d499200f58826de01a4b0503247a8270b6dd2 Mon Sep 17 00:00:00 2001 From: Joseph Chamochumbi Date: Tue, 28 Dec 2021 14:15:58 +0100 Subject: [PATCH 2/5] feat: Hooks w/ the same `key`, after re-validating, agree on the `updatedAt` value --- src/types.ts | 6 +- src/use-swr.ts | 35 +++++++-- src/utils/broadcast-state.ts | 5 +- test/use-swr-updated-at.test.tsx | 128 ++++++++++++++++++++++++++++++- 4 files changed, 161 insertions(+), 13 deletions(-) diff --git a/src/types.ts b/src/types.ts index 14a6701e1..9dbab10e4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -154,7 +154,8 @@ export type Broadcaster = ( error?: Error, isValidating?: boolean, revalidate?: boolean, - populateCache?: boolean + populateCache?: boolean, + updatedAt?: number ) => Promise export type State = { @@ -237,7 +238,8 @@ export type RevalidateCallback = ( export type StateUpdateCallback = ( data?: Data, error?: Error, - isValidating?: boolean + isValidating?: boolean, + updatedAt?: number ) => void export interface Cache { diff --git a/src/use-swr.ts b/src/use-swr.ts index da7e00c9d..0ad2b1cde 100644 --- a/src/use-swr.ts +++ b/src/use-swr.ts @@ -115,11 +115,16 @@ export const useSWRHandler = ( } const isValidating = resolveValidating() + const updatedAtRef = useRef() + const getUpdatedAt = () => updatedAtRef.current + const updatedAt = getUpdatedAt() + const [stateRef, stateDependencies, setState] = useStateWithDeps( { data, error, - isValidating + isValidating, + updatedAt }, unmountedRef ) @@ -211,7 +216,6 @@ export const useSWRHandler = ( // considered here. startAt = CONCURRENT_PROMISES_TS[key] newData = await CONCURRENT_PROMISES[key] - newState.updatedAt = Date.now() if (shouldStartNewRequest) { // If the request isn't interrupted, clean it up after the @@ -325,7 +329,17 @@ export const useSWRHandler = ( // Here is the source of the request, need to tell all other hooks to // update their states. if (isCurrentKeyMounted() && shouldStartNewRequest) { - broadcastState(cache, key, newState.data, newState.error, false) + updatedAtRef.current = Date.now() + broadcastState( + cache, + key, + newState.data, + newState.error, + false, + false, + undefined, + getUpdatedAt() + ) } return true @@ -375,13 +389,19 @@ export const useSWRHandler = ( const onStateUpdate: StateUpdateCallback = ( updatedData, updatedError, - updatedIsValidating + updatedIsValidating, + updatedUpdatedAt ) => { + updatedAtRef.current = isUndefined(updatedUpdatedAt) + ? updatedAtRef.current + : updatedUpdatedAt + setState( mergeObjects( { error: updatedError, - isValidating: updatedIsValidating + isValidating: updatedIsValidating, + updatedAt: getUpdatedAt() }, // Since `setState` only shallowly compares states, we do a deep // comparison here. @@ -432,7 +452,8 @@ export const useSWRHandler = ( setState({ data, error, - isValidating + isValidating, + updatedAt: UNDEFINED }) } @@ -528,7 +549,7 @@ export const useSWRHandler = ( }, get updatedAt() { stateDependencies.updatedAt = true - return stateRef.current.updatedAt + return updatedAt } } as SWRResponse } diff --git a/src/utils/broadcast-state.ts b/src/utils/broadcast-state.ts index 00fba6407..a7eb172b6 100644 --- a/src/utils/broadcast-state.ts +++ b/src/utils/broadcast-state.ts @@ -9,7 +9,8 @@ export const broadcastState: Broadcaster = ( error, isValidating, revalidate, - populateCache = true + populateCache = true, + updatedAt ) => { const [ EVENT_REVALIDATORS, @@ -25,7 +26,7 @@ export const broadcastState: Broadcaster = ( // Cache was populated, update states of all hooks. if (populateCache && updaters) { for (let i = 0; i < updaters.length; ++i) { - updaters[i](data, error, isValidating) + updaters[i](data, error, isValidating, updatedAt) } } diff --git a/test/use-swr-updated-at.test.tsx b/test/use-swr-updated-at.test.tsx index f483912c5..0a4579b58 100644 --- a/test/use-swr-updated-at.test.tsx +++ b/test/use-swr-updated-at.test.tsx @@ -1,5 +1,5 @@ import { act, fireEvent, screen } from '@testing-library/react' -import React, { useEffect } from 'react' +import React, { useEffect, useReducer } from 'react' import useSWR from 'swr' import { createKey, renderWithConfig, sleep } from './utils' @@ -49,6 +49,10 @@ describe('useSWR - updatedAt', () => { fireEvent.click(screen.getByRole('button')) + await act(async () => { + await new Promise(resolve => window.queueMicrotask(resolve)) + }) + expect(renderSpy).toHaveBeenCalledTimes(1) }) @@ -85,7 +89,7 @@ describe('useSWR - updatedAt', () => { expect(updateSpy.mock.calls[0][0]).toBeUndefined() await act(async () => { - await sleep(0) + await new Promise(resolve => window.queueMicrotask(resolve)) }) expect(updateSpy).toHaveBeenCalledTimes(2) @@ -95,4 +99,124 @@ describe('useSWR - updatedAt', () => { expect(updatedAt).toBeGreaterThanOrEqual(fetcherCallTime) }) + + it('should be consistent in all hooks using the same key', async () => { + const key = createKey() + + const fetcher = () => 'data' + + const Dashboard = ({ + testId = 'testId', + children = null, + revalidateOnMount = false + }) => { + const { updatedAt } = useSWR(key, fetcher, { revalidateOnMount }) + + return ( +
+ {updatedAt} + +
{children}
+
+ ) + } + + const Mutator = () => { + const { mutate } = useSWR(key, fetcher) + + return + } + + function Page() { + const { mutate, updatedAt } = useSWR(key, fetcher) + const [show, toggle] = useReducer(x => !x, false) + + return ( +
+ + + {updatedAt} + + + +
+ + + +
+ + {show && } +
+ ) + } + + renderWithConfig() + + // assert emptiness because the `updatedAt` prop is undefined + expect(screen.getByTestId('zero')).toBeEmptyDOMElement() + expect(screen.getByTestId('first')).toBeEmptyDOMElement() + + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: 'mutate' })) + await new Promise(resolve => window.queueMicrotask(resolve)) + }) + + expect(screen.getByTestId('zero')).not.toBeEmptyDOMElement() + + // copy the text value + const firstUpdatedAt = screen.getByTestId('zero').textContent + + // Assert that `first` agrees with `zero` + expect(screen.getByTestId('first')).toHaveTextContent(firstUpdatedAt) + + // Need to wait on the deduping interval + await act(async () => { + await sleep(2000) + }) + + fireEvent.click(screen.getByRole('button', { name: 'show' })) + + // Assert that when it mounts, `second` has no knowledge of `updatedAt` + expect(screen.getByTestId('second')).toBeEmptyDOMElement() + + await act(async () => { + await new Promise(resolve => window.requestAnimationFrame(resolve)) + }) + + await act(async () => { + await new Promise(resolve => window.queueMicrotask(resolve)) + }) + + // Assert that `second`, eventually agrees with `zero` + expect(screen.getByTestId('second')).toHaveTextContent( + screen.getByTestId('zero').textContent + ) + + await act(async () => { + // mutate from a nested element + fireEvent.click(screen.getByRole('button', { name: 'deep mutator' })) + await new Promise(resolve => window.queueMicrotask(resolve)) + }) + + // not empty, and not the same as before + expect(screen.getByTestId('zero')).not.toBeEmptyDOMElement() + expect(screen.getByTestId('zero')).not.toEqual(firstUpdatedAt) + + // sanity check, the value showed by `first` and `second` has increased + expect(Number(screen.getByTestId('first').textContent)).toBeGreaterThan( + Number(firstUpdatedAt) + ) + + expect(Number(screen.getByTestId('second').textContent)).toBeGreaterThan( + Number(firstUpdatedAt) + ) + + // transitively check that all hooks continue to agree on the `updatedAt` value for `key` + expect(screen.getByTestId('zero')).toHaveTextContent( + screen.getByTestId('first').textContent + ) + expect(screen.getByTestId('first')).toHaveTextContent( + screen.getByTestId('second').textContent + ) + }) }) From 75ef2a9f23ba7db7ce8a54d2e442caf1277e272c Mon Sep 17 00:00:00 2001 From: Joseph Chamochumbi Date: Tue, 28 Dec 2021 14:19:42 +0100 Subject: [PATCH 3/5] test: Remove `only` from `use-swr-key.test` --- test/use-swr-key.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/use-swr-key.test.tsx b/test/use-swr-key.test.tsx index 913dee36d..a3c888dec 100644 --- a/test/use-swr-key.test.tsx +++ b/test/use-swr-key.test.tsx @@ -231,7 +231,7 @@ describe('useSWR - key', () => { expect(fetcher).toBeCalledTimes(1) }) - it.only('gracefully handles mutate on non existing keys', async () => { + it('gracefully handles mutate on non existing keys', async () => { const fetcher = jest.fn(() => 'data') const mutSpy = jest.fn() From 84be6ead0f49d7ff85b824884431d1540a62aa71 Mon Sep 17 00:00:00 2001 From: Joseph Chamochumbi Date: Wed, 5 Jan 2022 12:13:29 +0100 Subject: [PATCH 4/5] feat: On revalidation, `updatedAt`, propagates to all same key hooks --- src/use-swr.ts | 16 +-- test/use-swr-updated-at.test.tsx | 193 +++++++++++++++++++------------ 2 files changed, 128 insertions(+), 81 deletions(-) diff --git a/src/use-swr.ts b/src/use-swr.ts index 5c0756b2d..3daf6d0c9 100644 --- a/src/use-swr.ts +++ b/src/use-swr.ts @@ -215,6 +215,9 @@ export const useSWRHandler = ( newData = await newData if (shouldStartNewRequest) { + const now = Date.now() + updatedAtRef.current = now + newState.updatedAt = now // If the request isn't interrupted, clean it up after the // deduplication interval. setTimeout(cleanupState, config.dedupingInterval) @@ -333,7 +336,6 @@ export const useSWRHandler = ( // Here is the source of the request, need to tell all other hooks to // update their states. if (isCurrentKeyMounted() && shouldStartNewRequest) { - updatedAtRef.current = Date.now() broadcastState( cache, key, @@ -342,7 +344,7 @@ export const useSWRHandler = ( false, false, undefined, - getUpdatedAt() + newState.updatedAt ) } @@ -392,11 +394,11 @@ export const useSWRHandler = ( updatedData, updatedError, updatedIsValidating, - updatedUpdatedAt + nextUpdatedAt ) => { - updatedAtRef.current = isUndefined(updatedUpdatedAt) - ? updatedAtRef.current - : updatedUpdatedAt + updatedAtRef.current = isUndefined(nextUpdatedAt) + ? getUpdatedAt() + : nextUpdatedAt setState( mergeObjects( @@ -551,7 +553,7 @@ export const useSWRHandler = ( }, get updatedAt() { stateDependencies.updatedAt = true - return updatedAt + return getUpdatedAt() } } as SWRResponse } diff --git a/test/use-swr-updated-at.test.tsx b/test/use-swr-updated-at.test.tsx index 0a4579b58..f8c067daf 100644 --- a/test/use-swr-updated-at.test.tsx +++ b/test/use-swr-updated-at.test.tsx @@ -1,7 +1,15 @@ import { act, fireEvent, screen } from '@testing-library/react' import React, { useEffect, useReducer } from 'react' import useSWR from 'swr' -import { createKey, renderWithConfig, sleep } from './utils' +import { + createKey, + renderWithConfig, + nextTick as waitForNextTick, + sleep, + focusOn +} from './utils' + +const focusWindow = () => focusOn(window) describe('useSWR - updatedAt', () => { it('should be initially undefined', async () => { @@ -12,16 +20,18 @@ describe('useSWR - updatedAt', () => { } function Page() { - const { mutate, updatedAt } = useSWR(key, fetcher, { - revalidateOnMount: false - }) + const { updatedAt } = useSWR(key, fetcher) - return + return {updatedAt} } renderWithConfig() - screen.getByText('data:') + expect(screen.getByTestId('updatedAt')).toBeEmptyDOMElement() + + await waitForNextTick() + + expect(screen.getByTestId('updatedAt')).not.toBeEmptyDOMElement() }) it('should not trigger re-render if not consumed', async () => { @@ -34,29 +44,38 @@ describe('useSWR - updatedAt', () => { const renderSpy = jest.fn() function Page() { - const { mutate } = useSWR(key, fetcher, { - revalidateOnMount: false - }) + const { data, isValidating } = useSWR(key, fetcher) + + // reading is validating just to trigger more renders + useEffect(() => {}, [isValidating]) renderSpy() - return + return data: {data} } renderWithConfig() - screen.getByText('data') + expect(screen.getByText(/data/i)).toHaveTextContent('data:') - fireEvent.click(screen.getByRole('button')) + expect(renderSpy).toHaveBeenCalledTimes(1) - await act(async () => { - await new Promise(resolve => window.queueMicrotask(resolve)) - }) + await waitForNextTick() - expect(renderSpy).toHaveBeenCalledTimes(1) + expect(screen.getByText(/data/i)).toHaveTextContent('data: data') + + expect(renderSpy).toHaveBeenCalledTimes(2) + + await focusWindow() + + await waitForNextTick() + + expect(screen.getByText(/data/i)).toHaveTextContent('data: data') + + expect(renderSpy).toHaveBeenCalledTimes(4) }) - it('should eventually reflect the last time the fetcher was called', async () => { + it('should updated when the fetcher is called', async () => { const key = createKey() let fetcherCallTime: number @@ -68,36 +87,52 @@ describe('useSWR - updatedAt', () => { const updateSpy = jest.fn() + const TIME_INTERVAL = Math.floor(100 + Math.random() * 100) + + const config = { + dedupingInterval: TIME_INTERVAL + } + function Page() { - const { mutate, updatedAt } = useSWR(key, fetcher) + const { data, updatedAt } = useSWR(key, fetcher, config) useEffect(() => { - updateSpy(updatedAt) - }, [updatedAt]) + updateSpy(data, updatedAt) + }, [data, updatedAt]) - return + return ( +
+ data: {data} + updatedAt: {updatedAt} +
+ ) } renderWithConfig() - screen.getByText('data') - - fireEvent.click(screen.getByRole('button')) - expect(updateSpy).toHaveBeenCalledTimes(1) + expect(updateSpy).toHaveBeenLastCalledWith(undefined, undefined) - expect(updateSpy.mock.calls[0][0]).toBeUndefined() - - await act(async () => { - await new Promise(resolve => window.queueMicrotask(resolve)) - }) + await waitForNextTick() expect(updateSpy).toHaveBeenCalledTimes(2) + const [data, updatedAt] = updateSpy.mock.calls[1] - const updatedAt = updateSpy.mock.calls[1][0] + expect(data).toEqual('data') expect(updatedAt).toBeDefined() expect(updatedAt).toBeGreaterThanOrEqual(fetcherCallTime) + + await sleep(config.dedupingInterval) + + await focusWindow() + await waitForNextTick() + + expect(updateSpy).toHaveBeenCalledTimes(3) + const [, lastUpdatedAt] = updateSpy.mock.calls[2] + + expect(lastUpdatedAt).toBeGreaterThanOrEqual(fetcherCallTime) + expect(lastUpdatedAt).toBeGreaterThan(updatedAt) }) it('should be consistent in all hooks using the same key', async () => { @@ -105,12 +140,22 @@ describe('useSWR - updatedAt', () => { const fetcher = () => 'data' + const TIME_INTERVAL = Math.floor(100 + Math.random() * 100) + + const config = { + dedupingInterval: TIME_INTERVAL, + refreshInterval: TIME_INTERVAL * 2 + } + const Dashboard = ({ testId = 'testId', children = null, revalidateOnMount = false }) => { - const { updatedAt } = useSWR(key, fetcher, { revalidateOnMount }) + const { updatedAt } = useSWR(key, fetcher, { + ...config, + revalidateOnMount + }) return (
@@ -121,28 +166,18 @@ describe('useSWR - updatedAt', () => { ) } - const Mutator = () => { - const { mutate } = useSWR(key, fetcher) - - return - } - function Page() { - const { mutate, updatedAt } = useSWR(key, fetcher) + const { updatedAt } = useSWR(key, fetcher, config) const [show, toggle] = useReducer(x => !x, false) return (
- - {updatedAt}
- - - +
{show && } @@ -156,65 +191,75 @@ describe('useSWR - updatedAt', () => { expect(screen.getByTestId('zero')).toBeEmptyDOMElement() expect(screen.getByTestId('first')).toBeEmptyDOMElement() - await act(async () => { - fireEvent.click(screen.getByRole('button', { name: 'mutate' })) - await new Promise(resolve => window.queueMicrotask(resolve)) - }) + await waitForNextTick() expect(screen.getByTestId('zero')).not.toBeEmptyDOMElement() + expect(screen.getByTestId('first')).not.toBeEmptyDOMElement() - // copy the text value - const firstUpdatedAt = screen.getByTestId('zero').textContent + const firstUpdatedAt = Number(screen.getByTestId('zero').textContent) // Assert that `first` agrees with `zero` - expect(screen.getByTestId('first')).toHaveTextContent(firstUpdatedAt) - - // Need to wait on the deduping interval - await act(async () => { - await sleep(2000) - }) + expect(screen.getByTestId('first')).toHaveTextContent( + screen.getByTestId('zero').textContent + ) fireEvent.click(screen.getByRole('button', { name: 'show' })) // Assert that when it mounts, `second` has no knowledge of `updatedAt` expect(screen.getByTestId('second')).toBeEmptyDOMElement() + // wait for the refresh interval await act(async () => { - await new Promise(resolve => window.requestAnimationFrame(resolve)) + await sleep(config.refreshInterval) }) - await act(async () => { - await new Promise(resolve => window.queueMicrotask(resolve)) - }) + expect(Number(screen.getByTestId('zero').textContent)).toBeGreaterThan( + firstUpdatedAt + ) - // Assert that `second`, eventually agrees with `zero` - expect(screen.getByTestId('second')).toHaveTextContent( - screen.getByTestId('zero').textContent + expect(Number(screen.getByTestId('first').textContent)).toBeGreaterThan( + firstUpdatedAt ) - await act(async () => { - // mutate from a nested element - fireEvent.click(screen.getByRole('button', { name: 'deep mutator' })) - await new Promise(resolve => window.queueMicrotask(resolve)) - }) + expect(Number(screen.getByTestId('second').textContent)).toBeGreaterThan( + firstUpdatedAt + ) - // not empty, and not the same as before - expect(screen.getByTestId('zero')).not.toBeEmptyDOMElement() - expect(screen.getByTestId('zero')).not.toEqual(firstUpdatedAt) + // transitively check that all hooks continue to agree on the `updatedAt` value for `key` + expect(screen.getByTestId('zero')).toHaveTextContent( + screen.getByTestId('first').textContent + ) + + expect(screen.getByTestId('first')).toHaveTextContent( + screen.getByTestId('second').textContent + ) + + const secondUpdateAt = Number(screen.getByTestId('zero').textContent) + + // let the deduping interval run + await sleep(config.dedupingInterval) + + // trigger revalidation by focus - all `updatedAt` keys should continue to agree + await focusWindow() + await waitForNextTick() + + expect(Number(screen.getByTestId('zero').textContent)).toBeGreaterThan( + secondUpdateAt + ) - // sanity check, the value showed by `first` and `second` has increased expect(Number(screen.getByTestId('first').textContent)).toBeGreaterThan( - Number(firstUpdatedAt) + secondUpdateAt ) expect(Number(screen.getByTestId('second').textContent)).toBeGreaterThan( - Number(firstUpdatedAt) + secondUpdateAt ) // transitively check that all hooks continue to agree on the `updatedAt` value for `key` expect(screen.getByTestId('zero')).toHaveTextContent( screen.getByTestId('first').textContent ) + expect(screen.getByTestId('first')).toHaveTextContent( screen.getByTestId('second').textContent ) From b66ef0e5ec810e8353f032ba189ce647c34565b7 Mon Sep 17 00:00:00 2001 From: Joseph Chamochumbi Date: Wed, 5 Jan 2022 12:16:17 +0100 Subject: [PATCH 5/5] chore: Remove duplicated test for undefined keys --- test/use-swr-key.test.tsx | 44 +-------------------------------------- 1 file changed, 1 insertion(+), 43 deletions(-) diff --git a/test/use-swr-key.test.tsx b/test/use-swr-key.test.tsx index a3c888dec..19f95d4f6 100644 --- a/test/use-swr-key.test.tsx +++ b/test/use-swr-key.test.tsx @@ -1,6 +1,6 @@ import { act, fireEvent, screen } from '@testing-library/react' import React, { useState, useEffect } from 'react' -import useSWR, { useSWRConfig } from 'swr' +import useSWR from 'swr' import { createKey, createResponse, renderWithConfig, sleep } from './utils' describe('useSWR - key', () => { @@ -230,46 +230,4 @@ describe('useSWR - key', () => { // Only 1 request since the keys are the same. expect(fetcher).toBeCalledTimes(1) }) - - it('gracefully handles mutate on non existing keys', async () => { - const fetcher = jest.fn(() => 'data') - const mutSpy = jest.fn() - - const key = createKey() - const mutKey = createKey() - - function Page() { - const { mutate } = useSWRConfig() - useSWR(key, fetcher) - - return ( -
- Hello - -
- ) - } - - renderWithConfig() - await screen.findByText('Hello') - - fireEvent.click(screen.getByRole('button')) - - expect(mutSpy).toHaveBeenCalledTimes(1) - - screen.getByText('Hello') - }) })