Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 0 additions & 31 deletions .eslintrc.js

This file was deleted.

2 changes: 1 addition & 1 deletion __tests__/fpjs-provider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ vi.mock('@fingerprint/agent', { spy: true })
const mockStart = vi.mocked(agent.start)

describe('FingerprintProvider', () => {
it('should configure an instance of the Fp Agent', async () => {
it('should configure an instance of the Fp Agent', () => {
const loadOptions = getDefaultLoadOptions()
const wrapper = createWrapper({
cache: {
Expand Down
7 changes: 5 additions & 2 deletions __tests__/helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ export const getDefaultLoadOptions = () => ({

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

export const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
export const wait = (ms: number) =>
new Promise<void>((resolve) => {
setTimeout(resolve, ms)
})

export const actWait = async (ms: number) => {
await act(async () => {
Expand Down
71 changes: 49 additions & 22 deletions __tests__/use-visitor-data.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useVisitorData, UseVisitorDataReturn } from '../src'
import { act, render, renderHook } from '@testing-library/react'
import { act, render, renderHook, screen } from '@testing-library/react'
import { actWait, createWrapper, wait } from './helpers'
import { useEffect, useState } from 'react'
import userEvent from '@testing-library/user-event'
Expand Down Expand Up @@ -32,7 +32,7 @@ describe('useVisitorData', () => {
mockStart.mockReturnValue(mockAgent)
})

it('should provide the Fp context', async () => {
it('should provide the Fp context', () => {
const wrapper = createWrapper()
const {
result: { current },
Expand Down Expand Up @@ -97,7 +97,25 @@ describe('useVisitorData', () => {
)
})

it("shouldn't call getData on mount if 'immediate' option is set to false", async () => {
it('should not deduplicate requests with distinct empty tag values', async () => {
mockGet.mockImplementation(async () => {
await wait(250)
return mockGetResult
})

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

await Promise.all([
result.current.getData({ tag: undefined }),
result.current.getData({ tag: null }),
result.current.getData({ tag: '' }),
])

expect(mockGet).toHaveBeenCalledTimes(3)
})

it("shouldn't call getData on mount if 'immediate' option is set to false", () => {
mockGet.mockImplementation(() => mockGetResult)

const wrapper = createWrapper()
Expand Down Expand Up @@ -141,25 +159,30 @@ describe('useVisitorData', () => {

return (
<>
<button onClick={() => setTag((prev) => prev + 1)}>Change options</button>
<button
onClick={() => {
setTag((prev) => prev + 1)
}}
>
Change options
</button>
<pre>{JSON.stringify(data)}</pre>
</>
)
}

const Wrapper = createWrapper()
const user = userEvent.setup()

const { container } = render(
render(
<Wrapper>
<Component />
</Wrapper>
)

await actWait(1000)

act(() => {
userEvent.click(container.querySelector('button')!)
})
await user.click(screen.getByRole('button', { name: 'Change options' }))

await actWait(1000)

Expand Down Expand Up @@ -278,33 +301,37 @@ describe('useVisitorData', () => {
getDataValues.push(getData)
return (
<>
<button onClick={() => setCount((count) => count + 1)}>Increment count</button>
<button
onClick={() => {
setCount((count) => count + 1)
}}
>
Increment count
</button>
<pre>{JSON.stringify(data)}</pre>
</>
)
}

const Wrapper = createWrapper()
const user = userEvent.setup()

const { container } = render(
render(
<Wrapper>
<Component />
</Wrapper>
)

await act(async () => {
await userEvent.click(container.querySelector('button')!)
})
const incrementButton = screen.getByRole('button', { name: 'Increment count' })
await user.click(incrementButton)

await act(async () => {
// This second click needs to be in a separate act otherwise
// React will coalesce the state updates to the count into a
// a single update. Meaning that count will jump from 0 to 2
// in between renders if this click were in the previous act block.
// This ensures that the case is covered where the options
// object does not semantically change.
await userEvent.click(container.querySelector('button')!)
})
// This second click needs to be a separate awaited interaction otherwise
// React will coalesce the state updates to the count into a
// a single update. Meaning that count will jump from 0 to 2
// in between renders if this click were in the previous act block.
// This ensures that the case is covered where the options
// object does not semantically change.
await user.click(incrementButton)

expect(getDataValues).toHaveLength(4)
expect(getDataValues[0]).toBe(getDataValues[1])
Expand Down
6 changes: 3 additions & 3 deletions __tests__/with-environment.preact.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ describe('WithEnvironment', () => {
})
it('should detect env as "preact"', async () => {
const { WithEnvironment } = await import('../src/components/with-environment')
const PrintEnv = (props: any) => h('div', null, props?.env?.name)
const PrintEnv = (props: { env: { name: string } }) => h('div', null, props.env.name)

// @ts-ignore
const { container } = preactRender(h(WithEnvironment, null, h(PrintEnv, null)))
// @ts-expect-error -- preact's render signature does not match React's @testing-library/react types
const { container } = preactRender(h(WithEnvironment, { children: (env) => h(PrintEnv, { env }) }))

expect(container.innerHTML).toContain('preact')
})
Expand Down
35 changes: 9 additions & 26 deletions __tests__/with-environment.test.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,28 @@
import { FunctionComponent } from 'react'
import { act, render } from '@testing-library/react'
import { render, screen } from '@testing-library/react'
import { WithEnvironment } from '../src/components/with-environment'
import { Link, MemoryRouter, Route, Routes } from 'react-router-dom'
import userEvent from '@testing-library/user-event'
import { actWait } from './helpers'
import { describe, it, expect, vi } from 'vitest'

describe('WithEnvironment', () => {
it('enhances provided element with `env` prop', () => {
const Mock = vi.fn(() => <div>foo</div>) as FunctionComponent
const renderChild = vi.fn(() => <div>foo</div>)

render(
<WithEnvironment>
<Mock />
</WithEnvironment>
)
render(<WithEnvironment>{renderChild}</WithEnvironment>)

expect(Mock).toHaveBeenCalledWith(expect.objectContaining({ env: expect.any(Object) }), expect.anything())
expect(renderChild).toHaveBeenCalledWith(expect.objectContaining({ name: 'react' }))
})

it('keeps the original props of the element', () => {
const Echo = ({ message }: { message: string }) => <span>{message}</span>

const { container } = render(
<WithEnvironment>
<Echo message='hello' />
</WithEnvironment>
)
const { container } = render(<WithEnvironment>{() => <Echo message='hello' />}</WithEnvironment>)

expect(container.innerHTML).toContain('hello')
})

it('should not break navigation', async () => {
const user = userEvent.setup()
const Home = () => (
<Link to='/test' id='test'>
Go to test
Expand All @@ -49,18 +40,10 @@ describe('WithEnvironment', () => {
)
}

const { container } = render(
<WithEnvironment>
<App />
</WithEnvironment>
)

act(() => {
userEvent.click(container.querySelector('#test')!)
})
render(<WithEnvironment>{() => <App />}</WithEnvironment>)

await actWait(250)
await user.click(screen.getByRole('link', { name: 'Go to test' }))

expect(container.innerHTML).toContain('Test page')
expect(screen.getByText('Test page')).toBeDefined()
})
})
47 changes: 47 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { includeIgnoreFile } from 'eslint/config'
import path from 'path'
import { fileURLToPath } from 'url'
import cfg from '@fingerprintjs/eslint-config-dx-team/type-checked'
import react from '@eslint-react/eslint-plugin'
import tseslint from 'typescript-eslint'
import reactHooks from 'eslint-plugin-react-hooks'

const __dirname = fileURLToPath(new URL('.', import.meta.url))

const config = [
includeIgnoreFile(path.resolve(__dirname, '.gitignore')),
{
ignores: ['examples/**/build/**', 'examples/**/dist/**', 'examples/**/.next/**', 'examples/**/node_modules/**'],
},
...cfg,
{
files: ['**/*.{ts,tsx}'],
...react.configs['recommended-type-checked'],
},
reactHooks.configs.flat['recommended-latest'],
{
files: ['**/*.{ts,tsx,mts,cts}'],
languageOptions: {
parserOptions: {
project: './tsconfig.eslint.json',
tsconfigRootDir: __dirname,
},
},
},
{
files: ['examples/preact/**/*.{ts,tsx}', 'examples/**/vite.config.ts'],
...tseslint.configs.disableTypeChecked,
},
{
files: ['**/*.{js,mjs,cjs,jsx}'],
...tseslint.configs.disableTypeChecked,
},
{
files: ['__tests__/**/*.{ts,tsx}'],
rules: {
'@typescript-eslint/no-unsafe-assignment': 'off',
},
},
]

export default config
3 changes: 2 additions & 1 deletion examples/create-react-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"scripts": {
"dev": "PORT=3001 DISABLE_ESLINT_PLUGIN=true react-scripts start",
"start": "pnpm dev",
"build": "DISABLE_ESLINT_PLUGIN=true react-scripts build"
"build": "DISABLE_ESLINT_PLUGIN=true react-scripts build",
"lint": "eslint . --max-warnings 0"
},
"browserslist": {
"production": [
Expand Down
7 changes: 6 additions & 1 deletion examples/create-react-app/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import ReactDOM from 'react-dom/client'
import './index.css'
import App from './App'

const root = ReactDOM.createRoot(document.getElementById('root')!)
const rootElement = document.getElementById('root')
if (rootElement === null) {
throw new Error('Root element not found')
}

const root = ReactDOM.createRoot(rootElement)

root.render(
<React.StrictMode>
Expand Down
9 changes: 7 additions & 2 deletions examples/create-react-app/src/shared/components/Toggler.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { PropsWithChildren, useState } from 'react'

function Toggler({ children }: PropsWithChildren<{}>) {
function Toggler({ children }: PropsWithChildren<object>) {
const [showChildren, setShowChildren] = useState(true)

return (
Expand All @@ -10,7 +10,12 @@ function Toggler({ children }: PropsWithChildren<{}>) {
<b>caching algorithm</b> as you unmount the component with the fingerprint and mount it again thus calling the{' '}
<code>useVisitorData</code> hook.
</p>
<button className='toggle-button' onClick={() => setShowChildren((show) => !show)}>
<button
className='toggle-button'
onClick={() => {
setShowChildren((show) => !show)
}}
>
{showChildren ? 'Hide' : 'Show'} visitor data
</button>
{showChildren && children}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ function VisitorDataPresenter({
return (
<div className='visitor-data'>
<p>
<b>Visitor ID:</b> {isLoading ? 'Loading...' : data ? data.visitor_id : 'not established yet'}
<b>Visitor ID:</b>{' '}
{isLoading === true ? 'Loading...' : data !== undefined ? data.visitor_id : 'not established yet'}
</p>
{data && (
<>
Expand Down
7 changes: 6 additions & 1 deletion examples/create-react-app/src/shared/pages/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ function HomePage() {
<Toggler>
<VisitorDataComponent />
</Toggler>
<button className='clear-cache-button' onClick={() => clearCache()}>
<button
className='clear-cache-button'
onClick={() => {
clearCache()
}}
>
Clear cache
</button>
</section>
Expand Down
Loading
Loading