Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b539205
feat: add React 19 / Next 16 support (INTER-2317)
JuroUhlar Jul 17, 2026
1ff896f
fix: declare Node engine requirement in Next example apps
JuroUhlar Jul 17, 2026
f999113
chore: update CI workflow name for clarity
JuroUhlar Jul 17, 2026
ffc711f
fix: configure Next ESLint rootDir and drop stale Next 14 reference
JuroUhlar Jul 17, 2026
8717c3e
test(e2e): add Playwright e2e matrix for examples on React 18 & 19
claude Jul 17, 2026
25fa8a6
ci(e2e): source the region from the FPJS_REGION repo variable
claude Jul 19, 2026
3e2020a
ci(e2e): require FPJS_REGION with no default
claude Jul 19, 2026
9759b06
ci(e2e): accept the public API key as a secret or a variable
claude Jul 19, 2026
7902459
ci(e2e): serve the preact example from a build instead of watch
claude Jul 19, 2026
943e78e
ci(e2e): harden workflow validation
JuroUhlar Jul 19, 2026
1dd3d8d
test(e2e): harden visitor-id assertion and React 19 switch
JuroUhlar Jul 19, 2026
eda0ea9
chore: region param optional
JuroUhlar Jul 20, 2026
7dd5fe7
chore: clean up
JuroUhlar Jul 20, 2026
b4cfbc1
chore: tighten EXAMPLES key typing with satisfies
JuroUhlar Jul 20, 2026
bca2da5
test(e2e): validate production builds
JuroUhlar Jul 20, 2026
f83b75f
fix(examples): guard optional region env access
JuroUhlar Jul 20, 2026
c9b927f
chore: fold preact typecheck into example build
JuroUhlar Jul 20, 2026
c1cabef
fix(examples): drop deprecated TypeScript 6 compiler options
JuroUhlar Jul 20, 2026
8e2846b
fix: address Copilot review on example name check and env reads
JuroUhlar Jul 20, 2026
0bf9954
fix: avoid setState-in-effect and stale responses in immediate mount …
JuroUhlar Jul 20, 2026
8aca20e
refactor: share query state helpers in useVisitorData
JuroUhlar Jul 20, 2026
df6eefc
test: clarify stale immediate response race coverage
JuroUhlar Jul 20, 2026
ad87c55
Merge origin/main into inter-2322/use-visitor-data-mount-fetch
Copilot Jul 21, 2026
1e21294
fix: handle immediate query state transitions
JuroUhlar Jul 21, 2026
5bf49cb
test: close coverage gaps and drop dead wait-until helper
JuroUhlar Jul 21, 2026
f1671b0
chore: changeset
JuroUhlar Jul 21, 2026
61c2213
fix: clarify automatic fetch error handling
JuroUhlar Jul 21, 2026
29f44f9
fix: docs phrasing
JuroUhlar Jul 22, 2026
acb5058
fix: dont mix promises and async-await
JuroUhlar Jul 22, 2026
c11d5b1
fix: use currentImmediate
JuroUhlar Jul 22, 2026
046727d
test: replace actWait with waitFor and act
JuroUhlar Jul 22, 2026
837d3e5
fix: clarify changeset
JuroUhlar Jul 22, 2026
43b4d29
chore: polish changeset
JuroUhlar Jul 22, 2026
e02506d
chore: re-trigger CI after dx-team-toolkit v1.5.8
JuroUhlar Jul 22, 2026
00a5227
Merge branch 'inter-2322/use-visitor-data-mount-fetch' into test/cove…
Copilot Jul 22, 2026
6ea1e4a
Merge branch 'main' into test/coverage-gaps-and-cleanup
Copilot Jul 22, 2026
499278d
test: keep coverage cleanup API-neutral
JuroUhlar Jul 23, 2026
d38ad11
test: avoid provider runtime changes
JuroUhlar Jul 23, 2026
08103c9
chore: add comment
JuroUhlar Jul 23, 2026
bcd57ca
test: remove private utility coverage
JuroUhlar Jul 23, 2026
c8f011a
test: strengthen visitor data behavior coverage
JuroUhlar Jul 23, 2026
0ce763b
test: restore mocks before each visitor data test
JuroUhlar Jul 23, 2026
06e6595
test: restore the original window descriptor
JuroUhlar Jul 23, 2026
fb82343
chore: rename to getDefaultStartOptions
JuroUhlar Jul 23, 2026
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
92 changes: 91 additions & 1 deletion __tests__/detect-env.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { detectEnvironment } from '../src/detect-env'
import { Env } from '../src/env.types'
import { describe, it, expect } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'

describe('Detect user env', () => {
afterEach(() => {
document.getElementById('__NEXT_DATA__')?.remove()
Reflect.deleteProperty(window, 'next')
vi.restoreAllMocks()
})

describe('Preact', () => {
it('should detect preact if class components receive any arguments in render', () => {
const env = detectEnvironment({
Expand Down Expand Up @@ -67,5 +73,89 @@ describe('Detect user env', () => {
version,
})
})

it('should skip Next detection when window is unavailable', () => {
const originalWindowDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'window')
Object.defineProperty(globalThis, 'window', {
value: undefined,
configurable: true,
})

try {
const env = detectEnvironment({
context: { classRenderReceivesAnyArguments: false },
})

expect(env).toEqual({
name: Env.React,
})
} finally {
if (originalWindowDescriptor) {
Object.defineProperty(globalThis, 'window', originalWindowDescriptor)
} else {
Reflect.deleteProperty(globalThis, 'window')
}
}
})
})
})

describe('getEnvironment', () => {
afterEach(() => {
vi.resetModules()
vi.doUnmock('../src/env')
})

it('returns parsed env details when the build-time env placeholder is valid JSON', async () => {
vi.resetModules()
vi.doMock('../src/env', () => ({
env: JSON.stringify({ name: 'react', version: '18.0.0' }),
}))

const { getEnvironment: getEnvironmentFresh } = await import('../src/get-env')

expect(
getEnvironmentFresh({
context: { classRenderReceivesAnyArguments: false },
})
).toEqual({
name: 'react',
version: '18.0.0',
})
})

it('falls back to detection when the build-time env JSON is not env details', async () => {
vi.resetModules()
vi.doMock('../src/env', () => ({
env: JSON.stringify({ foo: 'bar' }),
}))

const { getEnvironment: getEnvironmentFresh } = await import('../src/get-env')

expect(
getEnvironmentFresh({
// absence of classRenderReceivesAnyArguments is React signal
context: { classRenderReceivesAnyArguments: false },
})
).toEqual({
name: Env.React,
})
})

it('falls back to detection when the build-time env is invalid', async () => {
vi.resetModules()
vi.doMock('../src/env', () => ({
env: '%DETECTED_ENV%',
}))

const { getEnvironment: getEnvironmentFresh } = await import('../src/get-env')

expect(
getEnvironmentFresh({
context: { classRenderReceivesAnyArguments: false },
})
).toEqual({
name: Env.React,
})
})
})
168 changes: 162 additions & 6 deletions __tests__/fpjs-provider.test.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,60 @@
import { useContext } from 'react'
import { renderHook } from '@testing-library/react'
import { FingerprintContext } from '../src'
import { createWrapper, getDefaultLoadOptions } from './helpers'
import { PropsWithChildren, useContext } from 'react'
import { act, render, renderHook } from '@testing-library/react'
import { FingerprintContext, FingerprintProvider, FingerprintProviderOptions, useVisitorData } from '../src'
import { createWrapper, getDefaultStartOptions } from './helpers'
import { version } from '../package.json'
import { describe, it, expect, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import * as agent from '@fingerprint/agent'
import type { GetOptions } from '@fingerprint/agent'
import * as ssr from '../src/ssr'

vi.mock('@fingerprint/agent', { spy: true })

const mockGet = vi.fn()
const mockAgent = {
get: mockGet,
collect: vi.fn(),
}

const mockStart = vi.mocked(agent.start)

type InternalFingerprintProviderOptions = FingerprintProviderOptions & {
customAgent?: Pick<typeof agent, 'start'>
getOptions?: GetOptions
}

const InternalFingerprintProvider = (props: PropsWithChildren<InternalFingerprintProviderOptions>) => (
<FingerprintProvider {...props} />
)

const renderProvider = (props: Partial<FingerprintProviderOptions> = {}) =>
render(
<FingerprintProvider {...getDefaultStartOptions()} {...props}>
<div />
</FingerprintProvider>
)

describe('FingerprintProvider', () => {
beforeEach(() => {
vi.resetAllMocks()
mockStart.mockReturnValue(mockAgent)
mockGet.mockResolvedValue({
visitor_id: 'visitor',
event_id: 'event',
sealed_result: null,
cache_hit: false,
suspect_score: 0,
})
})

afterEach(() => {
document.getElementById('__NEXT_DATA__')?.remove()
Reflect.deleteProperty(window, 'next')
vi.restoreAllMocks()
})

it('should configure an instance of the Fp Agent', () => {
const loadOptions = getDefaultLoadOptions()
const loadOptions = getDefaultStartOptions()
const wrapper = createWrapper({
cache: {
cachePrefix: 'cache',
Expand All @@ -33,4 +75,118 @@ describe('FingerprintProvider', () => {
},
})
})

it('should include next version in integrationInfo when Next.js is detected', () => {
Object.assign(window, { next: { version: '14.2.0' } })

renderProvider()

expect(mockStart).toHaveBeenCalledWith(
expect.objectContaining({
integrationInfo: [`react-sdk/${version}/next/14.2.0`],
})
)
})

it('should rebuild the agent when forceRebuild is enabled and options change', () => {
const { rerender } = renderProvider({ apiKey: 'key-a', forceRebuild: true })

expect(mockStart).toHaveBeenCalledTimes(1)

rerender(
<FingerprintProvider apiKey='key-b' forceRebuild>
<div />
</FingerprintProvider>
)

expect(mockStart).toHaveBeenCalledTimes(2)
expect(mockStart).toHaveBeenLastCalledWith(
expect.objectContaining({
apiKey: 'key-b',
})
)
})

it('should not rebuild the agent when options change without forceRebuild', () => {
const { rerender } = renderProvider({ apiKey: 'key-a' })

expect(mockStart).toHaveBeenCalledTimes(1)

rerender(
<FingerprintProvider apiKey='key-b'>
<div />
</FingerprintProvider>
)

expect(mockStart).toHaveBeenCalledTimes(1)
})

it('should use customAgent.start when a valid custom agent loader is provided', async () => {
const customStart = vi.fn().mockReturnValue(mockAgent)
const wrapper = ({ children }: PropsWithChildren) => (
<InternalFingerprintProvider {...getDefaultStartOptions()} customAgent={{ start: customStart }}>
{children}
</InternalFingerprintProvider>
)

const { result } = renderHook(() => useVisitorData({ immediate: false }), { wrapper })

await act(async () => {
await result.current.getData()
})

expect(customStart).toHaveBeenCalled()
expect(mockStart).not.toHaveBeenCalled()
})

it('should fall back to the default agent when customAgent is invalid', async () => {
const wrapper = ({ children }: PropsWithChildren) => (
<InternalFingerprintProvider
{...getDefaultStartOptions()}
// @ts-expect-error intentionally invalid runtime shape
customAgent={{ start: 'not-a-function' }}
>
{children}
</InternalFingerprintProvider>
)

const { result } = renderHook(() => useVisitorData({ immediate: false }), { wrapper })

await act(async () => {
await result.current.getData()
})

expect(mockStart).toHaveBeenCalled()
})

it('should merge provider getOptions into visitor data requests', async () => {
const wrapper = ({ children }: PropsWithChildren) => (
<InternalFingerprintProvider
{...getDefaultStartOptions()}
getOptions={{ linkedId: 'from-provider', tag: { source: 'provider' } }}
>
{children}
</InternalFingerprintProvider>
)

const { result } = renderHook(() => useVisitorData({ immediate: false }), { wrapper })

await act(async () => {
await result.current.getData({ tag: { source: 'getData' } })
})

expect(mockGet).toHaveBeenCalledWith({
linkedId: 'from-provider',
tag: { source: 'getData' },
})
})

it('should throw when the client is used during SSR', async () => {
vi.spyOn(ssr, 'isSSR').mockReturnValue(true)

const wrapper = createWrapper()
const { result } = renderHook(() => useVisitorData({ immediate: false }), { wrapper })

await expect(result.current.getData()).rejects.toThrow('FingerprintProvider client cannot be used in SSR')
})
})
4 changes: 2 additions & 2 deletions __tests__/helpers.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { PropsWithChildren } from 'react'
import { FingerprintProvider, FingerprintProviderOptions } from '../src'

export const getDefaultLoadOptions = () => ({
export const getDefaultStartOptions = () => ({
apiKey: 'test_api_key',
})

export const createWrapper =
(providerProps: Partial<FingerprintProviderOptions> = {}) =>
({ children }: PropsWithChildren<object>) => (
<FingerprintProvider {...getDefaultLoadOptions()} {...providerProps}>
<FingerprintProvider {...getDefaultStartOptions()} {...providerProps}>
{children}
</FingerprintProvider>
)
Expand Down
Loading
Loading