Skip to content

Commit 4534675

Browse files
authored
tests(shared): add missing tests (#210)
* tests(shared): add missing tests * fix README * sync tests * typo
1 parent d5af4f5 commit 4534675

File tree

9 files changed

+329
-71
lines changed

9 files changed

+329
-71
lines changed

packages/server/src/procedure-client.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ describe.each(procedureCases)('createProcedureClient - case %s', async (_, proce
444444
it('transform non-error to error', () => {
445445
handler.mockRejectedValueOnce('non-error')
446446

447-
expect(client({ val: '123' })).rejects.toSatisfy(error => error instanceof Error && error.message === 'non-error')
447+
expect(client({ val: '123' })).rejects.toThrow('Unknown error')
448448
})
449449

450450
it('throw non-ORPC Error right away', () => {

packages/shared/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
> [!WARNING]
2+
>
3+
> `@orpc/shared` is an internal dependency of oRPC packages. It does not follow semver and may change at any time without notice.
4+
> Please do not use it in your project.
5+
16
<div align="center">
27
<image align="center" src="https://orpc.unnoq.com/logo.webp" width=280 alt="oRPC logo" />
38
</div>

packages/shared/src/error.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { toError } from './error'
2+
3+
it('toError', () => {
4+
const error = new Error('hi')
5+
6+
expect(toError(error)).toBe(error)
7+
8+
const e2 = { message: 'hi' }
9+
expect(toError(e2)).toBeInstanceOf(Error)
10+
expect(toError(e2).message).toEqual('Unknown error')
11+
expect(toError(e2).cause).toEqual(e2)
12+
})

packages/shared/src/error.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,7 @@
1-
import { isObject } from './object'
2-
31
export function toError(error: unknown): Error {
42
if (error instanceof Error) {
53
return error
64
}
75

8-
if (typeof error === 'string') {
9-
return new Error(error, { cause: error })
10-
}
11-
12-
if (isObject(error)) {
13-
if ('message' in error && typeof error.message === 'string') {
14-
return new Error(error.message, { cause: error })
15-
}
16-
17-
if ('name' in error && typeof error.name === 'string') {
18-
return new Error(error.name, { cause: error })
19-
}
20-
}
21-
226
return new Error('Unknown error', { cause: error })
237
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { Interceptor } from './interceptor'
2+
import { onError, onFinish, onStart, onSuccess } from './interceptor'
3+
4+
it('onStart', () => {
5+
const interceptor: Interceptor<{ foo: string }, 'success', 'error'> = onStart((options) => {
6+
expectTypeOf(options.foo).toEqualTypeOf<string>()
7+
expectTypeOf(options.next).toBeCallableWith<[options?: { foo: string }]>()
8+
expectTypeOf(options.next()).toEqualTypeOf<Promise<'success'>>()
9+
})
10+
})
11+
12+
it('onSuccess', () => {
13+
const interceptor: Interceptor<{ foo: string }, 'success', 'error'> = onSuccess((result, options) => {
14+
expectTypeOf(result).toEqualTypeOf<'success'>()
15+
16+
expectTypeOf(options.foo).toEqualTypeOf<string>()
17+
expectTypeOf(options.next).toBeCallableWith<[options?: { foo: string }]>()
18+
expectTypeOf(options.next()).toEqualTypeOf<Promise<'success'>>()
19+
})
20+
})
21+
22+
it('onError', () => {
23+
const interceptor: Interceptor<{ foo: string }, 'success', 'error'> = onError((error, options) => {
24+
expectTypeOf(error).toEqualTypeOf<'error'>()
25+
26+
expectTypeOf(options.foo).toEqualTypeOf<string>()
27+
expectTypeOf(options.next).toBeCallableWith<[options?: { foo: string }]>()
28+
expectTypeOf(options.next()).toEqualTypeOf<Promise<'success'>>()
29+
})
30+
})
31+
32+
it('onFinish', () => {
33+
const interceptor: Interceptor<{ foo: string }, 'success', 'error'> = onFinish((state, options) => {
34+
expectTypeOf(state).toEqualTypeOf<['success', null, 'success'] | [undefined, 'error', 'error']>()
35+
36+
expectTypeOf(options.foo).toEqualTypeOf<string>()
37+
expectTypeOf(options.next).toBeCallableWith<[options?: { foo: string }]>()
38+
expectTypeOf(options.next()).toEqualTypeOf<Promise<'success'>>()
39+
})
40+
})
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { intercept, onError, onFinish, onStart, onSuccess } from './interceptor'
2+
3+
beforeEach(() => {
4+
vi.clearAllMocks()
5+
})
6+
7+
describe('intercept', () => {
8+
const interceptor1 = vi.fn(({ next }) => next())
9+
const interceptor2 = vi.fn(({ next }) => next())
10+
const main = vi.fn(() => Promise.resolve('__main__'))
11+
12+
it('can intercept', async () => {
13+
interceptor2.mockReturnValueOnce(Promise.resolve('__interceptor2__'))
14+
15+
const result = await intercept(
16+
[
17+
interceptor1,
18+
interceptor2,
19+
],
20+
{
21+
foo: 'bar',
22+
},
23+
main,
24+
)
25+
26+
expect(result).toEqual('__interceptor2__')
27+
expect(interceptor1).toHaveBeenCalledTimes(1)
28+
expect(interceptor1).toHaveBeenCalledWith({
29+
foo: 'bar',
30+
next: expect.any(Function),
31+
})
32+
33+
expect(interceptor2).toHaveBeenCalledTimes(1)
34+
expect(interceptor2).toHaveBeenCalledWith({
35+
foo: 'bar',
36+
next: expect.any(Function),
37+
})
38+
39+
expect(main).toHaveBeenCalledTimes(0)
40+
41+
expect(await interceptor2.mock.calls[0]![0].next()).toEqual('__main__')
42+
expect(main).toHaveBeenCalledTimes(1)
43+
expect(main).toHaveBeenCalledWith({
44+
foo: 'bar',
45+
})
46+
})
47+
48+
it('can override options', async () => {
49+
interceptor1.mockImplementationOnce(({ next }) => next({ bar: 'foo' }))
50+
51+
const result = await intercept(
52+
[
53+
interceptor1,
54+
interceptor2,
55+
],
56+
{
57+
foo: 'bar',
58+
},
59+
main,
60+
)
61+
62+
expect(result).toEqual('__main__')
63+
64+
expect(interceptor1).toHaveBeenCalledTimes(1)
65+
expect(interceptor1).toHaveBeenCalledWith({
66+
foo: 'bar',
67+
next: expect.any(Function),
68+
})
69+
70+
expect(interceptor2).toHaveBeenCalledTimes(1)
71+
expect(interceptor2).toHaveBeenCalledWith({
72+
bar: 'foo',
73+
next: expect.any(Function),
74+
})
75+
76+
expect(main).toHaveBeenCalledTimes(1)
77+
expect(main).toHaveBeenCalledWith({
78+
bar: 'foo',
79+
})
80+
})
81+
82+
it('ignores conflict in the `next` options', async () => {
83+
/** Ensure even conflict still can override the `next` options */
84+
interceptor2.mockImplementationOnce(({ next }) => next({ bar: 'foo', next: 'hello2' }))
85+
86+
const result = await intercept(
87+
[
88+
interceptor1,
89+
interceptor2,
90+
],
91+
{
92+
foo: 'bar',
93+
next: 'hello',
94+
},
95+
main,
96+
)
97+
98+
expect(result).toEqual('__main__')
99+
100+
expect(interceptor1).toHaveBeenCalledTimes(1)
101+
expect(interceptor1).toHaveBeenCalledWith({
102+
foo: 'bar',
103+
next: expect.any(Function),
104+
})
105+
106+
expect(interceptor2).toHaveBeenCalledTimes(1)
107+
expect(interceptor2).toHaveBeenCalledWith({
108+
foo: 'bar',
109+
next: expect.any(Function),
110+
})
111+
112+
expect(main).toHaveBeenCalledTimes(1)
113+
expect(main).toHaveBeenCalledWith({
114+
bar: 'foo',
115+
next: 'hello2',
116+
})
117+
})
118+
})
119+
120+
describe('onStart / onSuccess / onError / onFinish', () => {
121+
const onStartFn = vi.fn()
122+
const onSuccessFn = vi.fn()
123+
const onErrorFn = vi.fn()
124+
const onFinishFn = vi.fn()
125+
126+
it('on success', async () => {
127+
const result = await intercept(
128+
[
129+
onStart(onStartFn),
130+
onSuccess(onSuccessFn),
131+
onError(onErrorFn),
132+
onFinish(onFinishFn),
133+
],
134+
{
135+
foo: 'bar',
136+
},
137+
() => Promise.resolve('__main__'),
138+
)
139+
140+
expect(result).toEqual('__main__')
141+
142+
expect(onStartFn).toHaveBeenCalledTimes(1)
143+
expect(onStartFn).toHaveBeenCalledWith({
144+
foo: 'bar',
145+
next: expect.any(Function),
146+
})
147+
148+
expect(onSuccessFn).toHaveBeenCalledTimes(1)
149+
expect(onSuccessFn).toHaveBeenCalledWith(
150+
'__main__',
151+
{
152+
foo: 'bar',
153+
next: expect.any(Function),
154+
},
155+
)
156+
157+
expect(onFinishFn).toHaveBeenCalledTimes(1)
158+
expect(onFinishFn).toHaveBeenCalledWith(
159+
['__main__', null, 'success'],
160+
{
161+
foo: 'bar',
162+
next: expect.any(Function),
163+
},
164+
)
165+
166+
expect(onErrorFn).toHaveBeenCalledTimes(0)
167+
})
168+
169+
it('on error', async () => {
170+
await expect(intercept(
171+
[
172+
onStart(onStartFn),
173+
onSuccess(onSuccessFn),
174+
onError(onErrorFn),
175+
onFinish(onFinishFn),
176+
],
177+
{
178+
foo: 'bar',
179+
},
180+
() => Promise.reject(new Error('__error__')),
181+
)).rejects.toThrowError('__error__')
182+
183+
expect(onStartFn).toHaveBeenCalledTimes(1)
184+
expect(onStartFn).toHaveBeenCalledWith({
185+
foo: 'bar',
186+
next: expect.any(Function),
187+
})
188+
189+
expect(onErrorFn).toHaveBeenCalledTimes(1)
190+
expect(onErrorFn).toHaveBeenCalledWith(
191+
new Error('__error__'),
192+
{
193+
foo: 'bar',
194+
next: expect.any(Function),
195+
},
196+
)
197+
198+
expect(onFinishFn).toHaveBeenCalledTimes(1)
199+
expect(onFinishFn).toHaveBeenCalledWith(
200+
[undefined, new Error('__error__'), 'error'],
201+
{
202+
foo: 'bar',
203+
next: expect.any(Function),
204+
},
205+
)
206+
207+
expect(onSuccessFn).toHaveBeenCalledTimes(0)
208+
})
209+
})

packages/shared/src/interceptor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export function onError<TError, TOptions extends { next(): any }, TRest extends
5757
}
5858
}
5959

60-
export type OnFinishState<TResult, TError> = [TResult, undefined, 'success'] | [undefined, TError, 'error']
60+
export type OnFinishState<TResult, TError> = [TResult, null, 'success'] | [undefined, TError, 'error']
6161

6262
/**
6363
* Can used for interceptors or middlewares
@@ -69,7 +69,7 @@ export function onFinish<TError, TOptions extends { next(): any }, TRest extends
6969
return async (options, ...rest) => {
7070
try {
7171
const result = await options.next()
72-
state = [result, undefined, 'success']
72+
state = [result, null, 'success']
7373
return result
7474
}
7575
catch (error) {

packages/shared/src/object.test.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,51 @@
1-
import { isTypescriptObject } from './object'
1+
import { clone, findDeepMatches, isObject, isTypescriptObject } from './object'
2+
3+
it('findDeepMatches', () => {
4+
const { maps, values } = findDeepMatches(v => typeof v === 'string', {
5+
array: ['v1', 'v2'],
6+
nested: {
7+
nested: [
8+
{
9+
nested: {
10+
v: 'v3',
11+
},
12+
},
13+
'v4',
14+
],
15+
},
16+
})
17+
18+
expect(maps).toEqual([
19+
['array', 0],
20+
['array', 1],
21+
['nested', 'nested', 0, 'nested', 'v'],
22+
['nested', 'nested', 1],
23+
])
24+
25+
expect(values).toEqual([
26+
'v1',
27+
'v2',
28+
'v3',
29+
'v4',
30+
])
31+
})
32+
33+
it('isObject', () => {
34+
expect(new Error('hi')).not.toSatisfy(isObject)
35+
expect(new Map()).not.toSatisfy(isObject)
36+
expect(new Set()).not.toSatisfy(isObject)
37+
expect(new Date()).not.toSatisfy(isObject)
38+
expect(false).not.toSatisfy(isObject)
39+
expect([]).not.toSatisfy(isObject)
40+
41+
expect({}).toSatisfy(isObject)
42+
expect(Object.create(null)).toSatisfy(isObject)
43+
expect((() => {
44+
const obj = {}
45+
Object.setPrototypeOf(obj, null)
46+
return obj
47+
})()).toSatisfy(isObject)
48+
})
249

350
it('isTypescriptObject', () => {
451
expect(new Error('hi')).toSatisfy(isTypescriptObject)
@@ -11,3 +58,15 @@ it('isTypescriptObject', () => {
1158
expect(undefined).not.toSatisfy(isTypescriptObject)
1259
expect(true).not.toSatisfy(isTypescriptObject)
1360
})
61+
62+
it('clone', () => {
63+
expect(clone(null)).toBeNull()
64+
65+
const obj = { a: 1, arr: [2, 3], nested: { arr: [{ b: 4 }] } }
66+
const cloned = clone(obj)
67+
68+
expect(cloned).toEqual(obj)
69+
expect(cloned).not.toBe(obj)
70+
expect(cloned.arr).not.toBe(obj.arr)
71+
expect(cloned.nested.arr).not.toBe(obj.nested.arr)
72+
})

0 commit comments

Comments
 (0)