Skip to content

Commit

Permalink
test: add tests for useList
Browse files Browse the repository at this point in the history
  • Loading branch information
netchampfaris committed Dec 18, 2024
1 parent fe2512e commit 8c0aa26
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 17 deletions.
5 changes: 4 additions & 1 deletion src/data-fetching/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Reactive, Ref } from 'vue'

export type Field = string

export type ChildTableField = {
Expand All @@ -8,7 +10,7 @@ export type FilterValue =
| string
| number
| boolean
| [string, string | number | boolean]
| [string, string | number | boolean | Ref<string | number | boolean>]

export interface ListFilters {
[key: Field]: FilterValue
Expand All @@ -34,6 +36,7 @@ export interface ListOptions<T> {
initialData?: T[]
immediate?: boolean
refetch?: boolean
baseUrl?: string
transform?: (data: T[]) => T[]
onSuccess?: (data: T[]) => void
onError?: (error: Error) => void
Expand Down
16 changes: 4 additions & 12 deletions src/data-fetching/useCall.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
* @vitest-environment node
*/

import { nextTick, ref } from 'vue'
import { ref } from 'vue'
import { useCall } from './index'
import { url } from '../mocks/utils'
import { url, waitUntilValueChanges } from '../mocks/utils'

describe('msw works', () => {
it('ping responds with pong', async () => {
Expand Down Expand Up @@ -38,14 +38,8 @@ describe('useCall', () => {
ping.execute()
expect(ping.loading).toBe(true)

// Wait for response
await ping.promise

// TODO: 3 nextTicks are required to ensure loading is updated
// find a better way to handle this
await nextTick()
await nextTick()
await nextTick()
await waitUntilValueChanges(() => ping.loading)

// Verify final state
expect(ping.data).toBe('pong')
Expand All @@ -65,9 +59,7 @@ describe('useCall', () => {
errorCall.fetch()
await errorCall.promise.catch(() => {})

await nextTick()
await nextTick()
await nextTick()
await waitUntilValueChanges(() => errorCall.loading)

expect(errorCall.loading).toBe(false)
expect(errorCall.error).toBeInstanceOf(Error)
Expand Down
173 changes: 173 additions & 0 deletions src/data-fetching/useList.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/**
* @vitest-environment node
*/

import { ref } from 'vue'
import { baseUrl, waitUntilValueChanges } from '../mocks/utils'
import { useList } from './index'

describe('useList', () => {
it('it returns expected object', async () => {
interface User {
name: string
email: string
}

let users = useList<User>({
baseUrl,
doctype: 'User',
fields: ['name', 'email'],
groupBy: 'name',
orderBy: 'email asc',
start: 0,
limit: 2,
immediate: false,
})

// Verify initial state
expect(users.data).toBe(null)
expect(users.error).toBe(null)
expect(users.hasNextPage).toBe(false)
expect(typeof users.fetch).toBe('function')

// fetch
await users.fetch()

// Verify final state
expect(users.data).toStrictEqual([
{ name: 'User1', email: '[email protected]' },
{ name: 'User2', email: '[email protected]' },
])
expect(users.error).toBe(null)
expect(users.isFinished).toBe(true)
expect(users.loading).toBe(false)
})

it('handles pagination correctly', async () => {
const users = useList({
baseUrl,
doctype: 'User',
fields: ['name', 'email'],
limit: 2,
immediate: false,
})

await users.fetch()

expect(users.hasNextPage).toBe(true)
expect(users.hasPreviousPage).toBe(false)
expect(users.start).toBe(0)

users.next()
await waitUntilValueChanges(() => users.data)

expect(users.start).toBe(2)
expect(users.hasPreviousPage).toBe(true)
expect(users.data).toStrictEqual([
{ name: 'User3', email: '[email protected]' },
{ name: 'User4', email: '[email protected]' },
])

await users.previous()
expect(users.start).toBe(0)
expect(users.hasPreviousPage).toBe(false)
})

it('dynamic filters should refetch the list', async () => {
const query = ref('user1')
const users = useList({
baseUrl,
doctype: 'User',
fields: ['name', 'email'],
filters: {
email: ['like', query],
},
limit: 3,
})

await waitUntilValueChanges(() => users.data)
expect(users.data).toStrictEqual([
{ name: 'User1', email: '[email protected]' },
])

query.value = 'user2'
await waitUntilValueChanges(() => users.data)
expect(users.data).toStrictEqual([
{ name: 'User2', email: '[email protected]' },
])
})

it('params are parsed and sent to server correctly', async () => {
const query = ref('user1')
const users = useList({
baseUrl,
doctype: 'User',
fields: ['name', 'email'],
filters: {
name: 'User1',
email: ['like', query],
},
limit: 2,
immediate: false,
})

// intercept fetch and check request params
const fetchSpy = vi.spyOn(global, 'fetch')

await users.fetch()

let searchParams = new URLSearchParams({
fields: JSON.stringify(['name', 'email']),
filters: JSON.stringify({
name: 'User1',
email: ['like', '%user1%'],
}),
start: '0',
limit: '2',
})

expect(fetchSpy).toHaveBeenCalledWith(
expect.stringContaining(
`${baseUrl}/api/v2/document/User?${searchParams.toString()}`,
),
expect.any(Object),
)

fetchSpy.mockRestore()
})

it('transforms data using transform function', async () => {
const users = useList({
baseUrl,
doctype: 'User',
fields: ['name', 'email'],
transform: (data) => {
return data.map((user) => ({
...user,
displayName: user.name.toUpperCase(),
}))
},
immediate: false,
})

await users.fetch()
// @ts-ignore
expect(users.data[0].displayName).toBe('USER1')
})

it('handles errors correctly', async () => {
let errorCaught = null
const users = useList({
baseUrl,
doctype: 'InvalidDoctype',
onError: (error) => {
errorCaught = error
},
immediate: false,
})

await users.fetch()
expect(users.error).toBeTruthy()
expect(errorCaught).toBeTruthy()
})
})
6 changes: 4 additions & 2 deletions src/data-fetching/useList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function useList<T>(options: ListOptions<T>) {
initialData,
immediate = true,
refetch = true,
baseUrl = '',
} = options

const _start = ref(start || 0)
Expand All @@ -36,7 +37,7 @@ export function useList<T>(options: ListOptions<T>) {
parent: parent,
debug: debug,
})
return `/api/v2/document/${doctype}?${params}`
return `${baseUrl}/api/v2/document/${doctype}?${params}`
})

const fetchOptions: UseFetchOptions = {
Expand Down Expand Up @@ -98,9 +99,10 @@ export function useList<T>(options: ListOptions<T>) {
aborted,
url,
abort,
execute,
next,
previous,
execute,
fetch: execute,
reload: execute,
insert,
})
Expand Down
46 changes: 46 additions & 0 deletions src/mocks/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,50 @@ export const handlers = [
data: { success: true },
})
}),

http.get(url('/api/v2/document/User'), async ({ request }) => {
const url = new URL(request.url)

let listParams = parseListParams(url.searchParams)
let result = getUsers(listParams)

return HttpResponse.json({
data: {
result,
has_next_page: true,
},
})
}),
]

function getUsers(listParams) {
let { start = 0, limit = 20, filters = {} } = listParams

return Array.from({ length: limit }, (_, i) => {
let n = i + start + 1
return {
name: `User${n}`,
email: `user${n}@example.com`,
}
}).filter((user) => {
if (filters.email?.[0] === 'like') {
let query = filters.email[1].replace(/%/g, '')
return user.email.includes(query)
}
return true
})
}

function parseListParams(searchParams) {
let out = {}
for (let [key, value] of searchParams) {
if (key === 'fields' || key === 'filters') {
out[key] = JSON.parse(value)
} else if (key === 'start' || key === 'limit') {
out[key] = parseInt(value)
} else if (['group_by', 'order_by', 'parent'].includes(key)) {
out[key] = value
}
}
return out
}
23 changes: 21 additions & 2 deletions src/mocks/utils.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,21 @@
export let url = (path: string) =>
new URL(path, 'http://example.com').toString()
import { watch } from 'vue'

export let baseUrl = 'http://example.com'

export let url = (path: string) => new URL(path, baseUrl).toString()

export function waitUntilValueChanges(
getter: () => any,
timeout = 1000,
): Promise<void> {
return new Promise((resolve) => {
let stop = watch(getter, () => {
stop()
resolve()
})
setTimeout(() => {
stop()
resolve()
}, timeout)
})
}

0 comments on commit 8c0aa26

Please sign in to comment.