Skip to content

Commit 306a58a

Browse files
committed
test(query-core): add unit tests for thenable
1 parent 75b2d10 commit 306a58a

1 file changed

Lines changed: 112 additions & 0 deletions

File tree

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
import { pendingThenable, tryResolveSync } from '../thenable'
3+
import type { FulfilledThenable, RejectedThenable } from '../thenable'
4+
5+
describe('pendingThenable', () => {
6+
it('should start out pending with resolve and reject attached', () => {
7+
const thenable = pendingThenable<string>()
8+
9+
expect(thenable.status).toBe('pending')
10+
expect(typeof thenable.resolve).toBe('function')
11+
expect(typeof thenable.reject).toBe('function')
12+
})
13+
14+
it('should expose the value and drop the pending props once resolved', async () => {
15+
const thenable = pendingThenable<string>()
16+
17+
thenable.resolve('data')
18+
19+
expect(thenable.status).toBe('fulfilled')
20+
expect((thenable as unknown as FulfilledThenable<string>).value).toBe('data')
21+
expect(thenable.resolve).toBeUndefined()
22+
expect(thenable.reject).toBeUndefined()
23+
await expect(thenable).resolves.toBe('data')
24+
})
25+
26+
it('should expose the reason and drop the pending props once rejected', async () => {
27+
const thenable = pendingThenable<string>()
28+
const reason = new Error('error')
29+
30+
thenable.reject(reason)
31+
32+
expect(thenable.status).toBe('rejected')
33+
expect((thenable as unknown as RejectedThenable<string>).reason).toBe(reason)
34+
expect(thenable.resolve).toBeUndefined()
35+
expect(thenable.reject).toBeUndefined()
36+
await expect(thenable).rejects.toBe(reason)
37+
})
38+
39+
it('should not reject a second time after being resolved', async () => {
40+
const thenable = pendingThenable<string>()
41+
42+
thenable.resolve('data')
43+
// `reject` is deleted on finalize, so a late caller cannot settle it twice
44+
expect(thenable.reject).toBeUndefined()
45+
46+
await expect(thenable).resolves.toBe('data')
47+
})
48+
49+
it('should not report an unhandled rejection when nobody awaits it', async () => {
50+
const onUnhandledRejection = vi.fn()
51+
process.on('unhandledRejection', onUnhandledRejection)
52+
53+
pendingThenable<string>().reject(new Error('error'))
54+
// unhandled rejections are reported after the microtask queue drains
55+
await new Promise((resolve) => setTimeout(resolve, 0))
56+
57+
process.off('unhandledRejection', onUnhandledRejection)
58+
expect(onUnhandledRejection).not.toHaveBeenCalled()
59+
})
60+
})
61+
62+
describe('tryResolveSync', () => {
63+
it('should return undefined for an already resolved native promise', () => {
64+
// `then` on a native promise always defers to a microtask, so the data is
65+
// never synchronously available
66+
expect(tryResolveSync(Promise.resolve('data'))).toBeUndefined()
67+
})
68+
69+
it('should return the data of a thenable that resolves synchronously', () => {
70+
const thenable = {
71+
then: (onFulfilled: (value: string) => unknown) => {
72+
onFulfilled('data')
73+
return Promise.resolve('data')
74+
},
75+
} as unknown as Promise<string>
76+
77+
expect(tryResolveSync(thenable)).toEqual({ data: 'data' })
78+
})
79+
80+
it('should support a synchronous thenable whose then() has no catch', () => {
81+
// a React thenable is not always a full promise, so `then` may return
82+
// something without a `catch` method
83+
const thenable = {
84+
then: (onFulfilled: (value: string) => unknown) => {
85+
onFulfilled('data')
86+
},
87+
} as unknown as Promise<string>
88+
89+
expect(tryResolveSync(thenable)).toEqual({ data: 'data' })
90+
})
91+
92+
it('should return undefined when a synchronous thenable resolves with undefined', () => {
93+
const thenable = {
94+
then: (onFulfilled: (value: undefined) => unknown) => {
95+
onFulfilled(undefined)
96+
},
97+
} as unknown as Promise<undefined>
98+
99+
expect(tryResolveSync(thenable)).toBeUndefined()
100+
})
101+
102+
it('should return undefined for a rejected promise without leaving it unhandled', async () => {
103+
const onUnhandledRejection = vi.fn()
104+
process.on('unhandledRejection', onUnhandledRejection)
105+
106+
expect(tryResolveSync(Promise.reject(new Error('error')))).toBeUndefined()
107+
await new Promise((resolve) => setTimeout(resolve, 0))
108+
109+
process.off('unhandledRejection', onUnhandledRejection)
110+
expect(onUnhandledRejection).not.toHaveBeenCalled()
111+
})
112+
})

0 commit comments

Comments
 (0)