Skip to content
61 changes: 58 additions & 3 deletions __tests__/use-visitor-data.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useVisitorData, UseVisitorDataReturn } from '../src'
import { act, render, renderHook, screen } from '@testing-library/react'
import { FingerprintContext, useVisitorData, UseVisitorDataReturn } from '../src'
import { act, render, renderHook, screen, waitFor } from '@testing-library/react'
import { actWait, createWrapper, wait } from './helpers'
import { useEffect, useState } from 'react'
import { type PropsWithChildren, useEffect, useState } from 'react'
import userEvent from '@testing-library/user-event'
import * as agent from '@fingerprint/agent'
import { GetResult } from '@fingerprint/agent'
Expand All @@ -25,6 +25,24 @@ vi.mock('@fingerprint/agent', { spy: true })

const mockStart = vi.mocked(agent.start)

function createDeferred<T>() {
let resolvePromise: ((value: T) => void) | undefined
const promise = new Promise<T>((resolve) => {
resolvePromise = resolve
})

return {
promise,
resolve(value: T) {
if (resolvePromise === undefined) {
throw new Error('Promise resolver was not initialized')
}

resolvePromise(value)
},
}
}

describe('useVisitorData', () => {
beforeEach(() => {
vi.resetAllMocks()
Expand Down Expand Up @@ -191,6 +209,43 @@ describe('useVisitorData', () => {
expect(mockGet).toHaveBeenNthCalledWith(2, { tag: 2 })
})

it('should enter the loading state when the automatic request source changes', async () => {
const secondResult = { ...mockGetResult, visitor_id: 'second-visitor' }
const secondRequest = createDeferred<GetResult>()
let getVisitorData: ReturnType<typeof vi.fn<() => Promise<GetResult>>> = vi.fn(() => Promise.resolve(mockGetResult))
const wrapper = ({ children }: PropsWithChildren<object>) => (
<FingerprintContext.Provider value={{ getVisitorData }}>{children}</FingerprintContext.Provider>
)
const hook = renderHook(() => useVisitorData(), { wrapper })

await waitFor(() => {
expect(hook.result.current.data).toEqual(mockGetResult)
})

getVisitorData = vi.fn(() => secondRequest.promise)
hook.rerender()

expect(hook.result.current).toMatchObject({
isLoading: true,
isFetched: false,
data: undefined,
})
await waitFor(() => {
expect(getVisitorData).toHaveBeenCalledTimes(1)
})

await act(async () => {
secondRequest.resolve(secondResult)
await secondRequest.promise
})

expect(hook.result.current).toMatchObject({
isLoading: false,
isFetched: true,
data: secondResult,
})
})

it('should correctly pass errors from agent', async () => {
const ERROR_CLIENT_TIMEOUT = 'timeout'
mockGet.mockRejectedValue(new Error(ERROR_CLIENT_TIMEOUT))
Expand Down
61 changes: 48 additions & 13 deletions src/use-visitor-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,17 @@
export function useVisitorData(
{ immediate, ...getOptions }: UseVisitorDataOptions = { immediate: true }
): UseVisitorDataReturn {
assertIsDefined(getOptions, 'getOptions')

const { getVisitorData } = useContext<FingerprintContextInterface>(FingerprintContext)

const [currentGetOptions, setCurrentGetOptions] = useState(getOptions)
const [currentConfig, setCurrentConfig] = useState({ immediate, getOptions, getVisitorData })
const [queryState, setQueryState] = useState<VisitorQueryResult>({
isLoading: immediate,
data: undefined,
isFetched: false,
error: undefined,
})
const currentGetOptions = currentConfig.getOptions
const currentGetVisitorData = currentConfig.getVisitorData

const getData = useCallback<UseVisitorDataReturn['getData']>(
async (params = {}) => {
Expand All @@ -69,7 +69,7 @@
...params,
}

const result = await getVisitorData(getDataOptions)
const result = await currentGetVisitorData(getDataOptions)
setQueryState({
isLoading: false,
isFetched: true,
Expand All @@ -91,21 +91,56 @@
throw error
}
},
[currentGetOptions, getVisitorData]
[currentGetOptions, currentGetVisitorData, setQueryState]
)

useEffect(() => {
if (immediate) {
// TODO: refactor mount fetch to avoid synchronous setState inside the effect
// eslint-disable-next-line react-hooks/set-state-in-effect -- preserve existing getData() mount behavior
getData().catch((unknownError: unknown) => {
console.error(`Failed to fetch visitor data on mount: ${String(unknownError)}`)
})
void Promise.resolve()
.then(() => currentGetVisitorData(currentGetOptions))
.then(
(result) => {
setQueryState({
isLoading: false,
isFetched: true,
data: result,
error: undefined,
})
},
(unknownError: unknown) => {

Check warning on line 110 in src/use-visitor-data.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
const error = toError(unknownError)

Check warning on line 111 in src/use-visitor-data.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

setQueryState({
isLoading: false,
isFetched: false,
data: undefined,
error,
})

Check warning on line 118 in src/use-visitor-data.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
console.error(`Failed to fetch visitor data on mount: ${String(error)}`)

Check warning on line 119 in src/use-visitor-data.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}
)
}
}, [immediate, getData])
}, [immediate, currentGetOptions, currentGetVisitorData, setQueryState])

const getOptionsChanged = !Object.is(currentGetOptions, getOptions) && !deepEquals(currentGetOptions, getOptions)

const requestSourceChanged = currentGetVisitorData !== getVisitorData

if (!Object.is(currentGetOptions, getOptions) && !deepEquals(currentGetOptions, getOptions)) {
setCurrentGetOptions(getOptions)
if (currentConfig.immediate !== immediate || getOptionsChanged || requestSourceChanged) {
setCurrentConfig({
immediate,
getOptions: getOptionsChanged ? getOptions : currentGetOptions,
getVisitorData,
})

if (immediate) {
setQueryState({
isLoading: true,
isFetched: false,
data: undefined,
error: undefined,
})
}
}

return useMemo(
Expand Down
Loading