Skip to content

Commit

Permalink
chore(tests): initial tests for hooks (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
achou11 authored Jan 21, 2025
1 parent f0147c6 commit 8404cad
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 34 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,7 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run test
- name: Run type checks
run: npm run types
- name: Run tests
run: npm run test
2 changes: 1 addition & 1 deletion docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ npm install

## Running tests

Unit tests can be run using `npm test` (or `npm t`).
Tests can be run using `npm test` (or `npm t`).
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@
"lint:format": "prettier --cache --check .",
"lint": "npm-run-all --parallel --continue-on-error --print-label --aggregate-output lint:*",
"types": "tsc",
"test:unit": "vitest run",
"test": "npm-run-all --parallel --continue-on-error --print-label --aggregate-output types test:*",
"test": "vitest run",
"docs:generate": "tsdoc --src=src/contexts/*,src/hooks/* --dest=docs/API.md --noemoji --types"
},
"peerDependencies": {
Expand Down
10 changes: 8 additions & 2 deletions test/helpers/ipc.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createRequire } from 'node:module'
import path from 'node:path'
import { MessageChannel } from 'node:worker_threads'
import { MapeoManager } from '@comapeo/core'
import { FastifyController, MapeoManager } from '@comapeo/core'
import {
closeMapeoClient,
createMapeoClient,
Expand All @@ -28,13 +28,15 @@ const clientMigrationsFolder = path.join(
export function setupCoreIpc() {
const { port1, port2 } = new MessageChannel()

const fastify = Fastify()

const manager = new MapeoManager({
rootKey: KeyManager.generateRootKey(),
dbFolder: ':memory:',
coreStorage: () => new RAM(),
projectMigrationsFolder,
clientMigrationsFolder,
fastify: Fastify(),
fastify,
})

const server = createMapeoServer(manager, port1)
Expand All @@ -43,13 +45,17 @@ export function setupCoreIpc() {
port1.start()
port2.start()

const fastifyController = new FastifyController({ fastify })

return {
port1,
port2,
server,
client,
fastifyController,
cleanup: async () => {
server.close()
fastifyController.stop()
await closeMapeoClient(client)
port1.close()
port2.close()
Expand Down
6 changes: 4 additions & 2 deletions test/helpers/react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import { ClientApiProvider } from '../../src/index.js'

export function createClientApiWrapper({
clientApi,
queryClient,
}: {
clientApi: MapeoClientApi
queryClient?: QueryClient
}) {
const queryClient = new QueryClient()
const qc = queryClient || new QueryClient()

return ({ children }: PropsWithChildren<unknown>) => {
return (
<QueryClientProvider client={queryClient}>
<QueryClientProvider client={qc}>
<ClientApiProvider clientApi={clientApi}>{children}</ClientApiProvider>
</QueryClientProvider>
)
Expand Down
191 changes: 165 additions & 26 deletions test/hooks/client.test.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,179 @@
import { MapeoClientApi } from '@comapeo/ipc'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { renderHook } from '@testing-library/react'
import { assert, assertType, test } from 'vitest'
import { act, renderHook, waitFor } from '@testing-library/react'
import { assert, describe, test } from 'vitest'

import { useClientApi } from '../../src/hooks/client.js'
import {
useClientApi,
useIsArchiveDevice,
useOwnDeviceInfo,
useSetIsArchiveDevice,
useSetOwnDeviceInfo,
} from '../../src/index.js'
import { setupCoreIpc } from '../helpers/ipc.js'
import { createClientApiWrapper } from '../helpers/react.js'

test('useClientApi() throws when ClientApiProvider is not set up', () => {
const queryClient = new QueryClient()

assert.throws(() => {
renderHook(() => useClientApi(), {
wrapper: ({ children }) => {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
},
describe('useClientApi()', () => {
test('throws when ClientApiProvider is not set up', () => {
const queryClient = new QueryClient()

assert.throws(() => {
renderHook(() => useClientApi(), {
wrapper: ({ children }) => {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
},
})
}, 'No client API set')
})

test('returns client api instance when ClientApiProvider is set up properly', (t) => {
const { client, cleanup } = setupCoreIpc()

t.onTestFinished(() => {
return cleanup()
})
}, 'No client API set')
})

test('useClientApi() returns client api instance when ClientApiProvider is set up properly', (t) => {
const { client, cleanup } = setupCoreIpc()
const { result } = renderHook(() => useClientApi(), {
wrapper: createClientApiWrapper({ clientApi: client }),
})

t.onTestFinished(() => {
return cleanup()
assert.isDefined(result.current, 'client is set up properly')
})
})

describe('device info', () => {
test('basic read and write', async (t) => {
const { client, cleanup } = setupCoreIpc()

t.onTestFinished(() => {
return cleanup()
})

const { result } = renderHook(() => useClientApi(), {
wrapper: createClientApiWrapper({ clientApi: client }),
const queryClient = new QueryClient()

const wrapper = createClientApiWrapper({ clientApi: client, queryClient })

const readHook = renderHook(() => useOwnDeviceInfo(), { wrapper })
const writeHook = renderHook(() => useSetOwnDeviceInfo(), { wrapper })

// Since the read hook is Suspense-based, we need to simulate waiting for the data to initially resolve
await waitFor(() => {
assert.isNotNull(readHook.result.current.data)
})

const expectedDeviceId = await client.deviceId()

// 1. Initial state
assert.deepStrictEqual(
readHook.result.current,
{
data: {
deviceId: expectedDeviceId,
deviceType: 'device_type_unspecified',
},
isRefetching: false,
error: null,
},
'read hook has expected initial state',
)
assert.deepStrictEqual(
writeHook.result.current.status,
'idle',
'write hook has expected initial status',
)

// 2. Simulate a user interaction
act(() => {
writeHook.result.current.mutate({
name: 'my device',
deviceType: 'tablet',
})
})

// 3. Write hook lifecycle
// TODO: Ideally check for status === 'pending' before this
await waitFor(() => {
assert.strictEqual(writeHook.result.current.status, 'success')
})

// 4. Read hook lifecycle
// TODO: Ideally check for isRefetching === true before this
await waitFor(() => {
assert.strictEqual(readHook.result.current.isRefetching, false)
})

assert.deepStrictEqual(
readHook.result.current,
{
isRefetching: false,
error: null,
data: {
deviceId: expectedDeviceId,
name: 'my device',
deviceType: 'tablet',
},
},
'read hook has expected updated state',
)
})
})

assertType<MapeoClientApi>(result.current)
describe('is archive device', () => {
test('basic read and write', async (t) => {
const { client, cleanup } = setupCoreIpc()

assert.isDefined(result.current, 'client is set up properly')
t.onTestFinished(() => {
return cleanup()
})

const queryClient = new QueryClient()

const wrapper = createClientApiWrapper({ clientApi: client, queryClient })

const readHook = renderHook(() => useIsArchiveDevice(), { wrapper })
const writeHook = renderHook(() => useSetIsArchiveDevice(), { wrapper })

// Since the hook is Suspense-based, we need to simulate waiting for the data to initially resolve
await waitFor(() => {
assert.isNotNull(readHook.result.current.data)
})

// 1. Initial state
assert.deepStrictEqual(
readHook.result.current,
{
data: true,
error: null,
isRefetching: false,
},
'read hook has expected initial state',
)
assert.deepStrictEqual(
writeHook.result.current.status,
'idle',
'write hook has expected initial status',
)

// 2. Simulate a user interaction
act(() => {
writeHook.result.current.mutate({ isArchiveDevice: false })
})

// 3. Write hook lifecycle
// TODO: Ideally check for status === 'pending' before this
await waitFor(() => {
assert.strictEqual(writeHook.result.current.status, 'success')
})

// 4. Read hook lifecycle
// TODO: Ideally check for isRefetching === true before this
await waitFor(() => {
assert.strictEqual(readHook.result.current.isRefetching, false)
})

assert.strictEqual(readHook.result.current.data, false, 'data has updated')
})
})
57 changes: 57 additions & 0 deletions test/hooks/maps.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { QueryClient } from '@tanstack/react-query'
import { renderHook, waitFor } from '@testing-library/react'
import { assert, test } from 'vitest'

import { useMapStyleUrl } from '../../src/index.js'
import { setupCoreIpc } from '../helpers/ipc.js'
import { createClientApiWrapper } from '../helpers/react.js'

test('basic read works', async (t) => {
const { client, cleanup, fastifyController } = setupCoreIpc()

fastifyController.start()

t.onTestFinished(() => {
return cleanup()
})

const queryClient = new QueryClient()

const wrapper = createClientApiWrapper({ clientApi: client, queryClient })

const mapStyleUrlHook = renderHook<
ReturnType<typeof useMapStyleUrl>,
Parameters<typeof useMapStyleUrl>[0]
>(({ refreshToken } = {}) => useMapStyleUrl({ refreshToken }), {
wrapper,
})

await waitFor(() => {
assert(mapStyleUrlHook.result.current.data !== null)
})

const url1 = new URL(mapStyleUrlHook.result.current.data)

assert(url1, 'map style url hook returns valid URL')

mapStyleUrlHook.rerender({ refreshToken: 'abc_123' })

// TODO: Ideally check for isRefetching === true before this
await waitFor(() => {
assert(mapStyleUrlHook.result.current.isRefetching === false)
})

const url2 = new URL(mapStyleUrlHook.result.current.data)

assert.notStrictEqual(
url2.href,
url1.href,
'map style url hook updates after changing refresh token option',
)

assert.strictEqual(
url2.searchParams.get('refresh_token'),
'abc_123',
'map style url has search param containing refresh token',
)
})
13 changes: 13 additions & 0 deletions test/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { notifyManager } from '@tanstack/react-query'
import { act, cleanup } from '@testing-library/react'
import { afterEach } from 'vitest'

afterEach(() => {
// https://testing-library.com/docs/react-testing-library/api#cleanup
cleanup()
})

// Wrap notifications with act to make sure React knows about React Query updates
notifyManager.setNotifyFunction((fn) => {
act(fn)
})
1 change: 1 addition & 0 deletions vitest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environment: 'happy-dom',
setupFiles: ['./test/setup.ts'],
},
})

0 comments on commit 8404cad

Please sign in to comment.