Skip to content

Commit

Permalink
Add Mock.Of<F> type (#141)
Browse files Browse the repository at this point in the history
  • Loading branch information
hasparus authored Nov 22, 2021
1 parent a0f7b98 commit 00cae63
Show file tree
Hide file tree
Showing 7 changed files with 52 additions and 27 deletions.
5 changes: 5 additions & 0 deletions .changeset/happy-apples-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'earljs': patch
---

Function mocks can now be typed as Mock.Of<TFunctionType>
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"cross-spawn-with-kill": "^1.0.0",
"conditional-type-checks": "^1.0.5",
"eslint": "^7.29.0",
"eslint-config-typestrict": "^1.0.2",
"eslint-plugin-import": "^2.23.4",
Expand Down
2 changes: 2 additions & 0 deletions packages/docs/guides/mocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ To create a mock do:
```typescript
import { mockFn } from 'earljs'

// mock: Mock<[number, number], number>
const mock = mockFn<[number, number], number>()
```

Expand All @@ -25,6 +26,7 @@ that's why you need to pass type arguments between angle brackets `<`, `>`.
Alternatively, you can pass function type as a type argument:

```typescript
// mock: Mock.Of<(a: number, b: number) => number>
const mock = mockFn<(a: number, b: number) => number>()
```

Expand Down
4 changes: 1 addition & 3 deletions packages/earljs/src/mocks/mockFn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@ interface Override {
spec: Spec
}

export function mockFn<FUNCTION_SIG extends (...args: any) => any>(
defaultImpl?: FUNCTION_SIG,
): Mock<Parameters<FUNCTION_SIG>, ReturnType<FUNCTION_SIG>>
export function mockFn<FUNCTION_SIG extends (...args: any) => any>(defaultImpl?: FUNCTION_SIG): Mock.Of<FUNCTION_SIG>
export function mockFn<ARGS extends any[], RETURN = any>(defaultImpl?: (...args: ARGS) => RETURN): Mock<ARGS, RETURN>
export function mockFn<ARGS extends any[], RETURN = any>(defaultImpl?: (...args: ARGS) => RETURN): Mock<ARGS, RETURN> {
let spec: Spec = {
Expand Down
50 changes: 27 additions & 23 deletions packages/earljs/src/mocks/types.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
export interface MockCall<ARGS, RETURN> {
args: ARGS
result: { type: 'return'; value: RETURN } | { type: 'throw'; error: any }
export interface MockCall<TArgs, TReturn> {
args: TArgs
result: { type: 'return'; value: TReturn } | { type: 'throw'; error: any }
}

export type Awaited<T> = T extends PromiseLike<infer PT> ? PT : never

export interface Mock<ARGS extends any[], RETURN> {
export interface Mock<TArgs extends any[], TReturn> {
/**
* Calls the mock function.
*/
(...args: ARGS): RETURN
(...args: TArgs): TReturn

/**
* An array containing all the performed calls.
*/
calls: MockCall<ARGS, RETURN>[]
calls: MockCall<TArgs, TReturn>[]

/**
* Checks if all the expected calls to the mock have been performed.
Expand All @@ -26,106 +26,110 @@ export interface Mock<ARGS extends any[], RETURN> {
* Overrides any previous configuration.
* @param value value to be returned.
*/
returns(value: RETURN): Mock<ARGS, RETURN>
returns(value: TReturn): Mock<TArgs, TReturn>
/**
* Schedules the mock to return a value the next time it's called.
* If anything is already scheduled it will be used first.
* @param value value to be returned.
*/
returnsOnce(value: RETURN): Mock<ARGS, RETURN>
returnsOnce(value: TReturn): Mock<TArgs, TReturn>

/**
* Sets the error thrown by calls to the Mock.
* Overrides any previous configuration.
* @param error error to be thrown.
*/
throws(error: any): Mock<ARGS, RETURN>
throws(error: any): Mock<TArgs, TReturn>
/**
* Schedules the mock to throw an error the next time it's called.
* If anything is already scheduled it will be used first.
* @param error error to be thrown.
*/
throwsOnce(error: any): Mock<ARGS, RETURN>
throwsOnce(error: any): Mock<TArgs, TReturn>

/**
* Sets the underlying implementation of the Mock.
* Overrides any previous configuration.
* @param implementation function to execute.
*/
executes(implementation: (...args: ARGS[]) => RETURN): Mock<ARGS, RETURN>
executes(implementation: (...args: TArgs[]) => TReturn): Mock<TArgs, TReturn>
/**
* Schedules the mock to use the provided implementation the next time it's called.
* If anything is already scheduled it will be used first.
* @param implementation function to execute.
*/
executesOnce(implementation: (...args: ARGS[]) => RETURN): Mock<ARGS, RETURN>
executesOnce(implementation: (...args: TArgs[]) => TReturn): Mock<TArgs, TReturn>

/**
* Sets the return value wrapped in Promise.resolve of calls to the Mock.
* @param value value to be returned.
*/
resolvesTo(value: Awaited<RETURN>): Mock<ARGS, RETURN>
resolvesTo(value: Awaited<TReturn>): Mock<TArgs, TReturn>
/**
* Schedules the mock to return value wrapped in Promise.resolve the next time it's called.
* If anything is already scheduled it will be used first.
* @param value value to be returned.
*/
resolvesToOnce(value: Awaited<RETURN>): Mock<ARGS, RETURN>
resolvesToOnce(value: Awaited<TReturn>): Mock<TArgs, TReturn>

/**
* Sets the error rejected by calls to the Mock.
* @param error error to be thrown.
*/
rejectsWith(error: any): Mock<ARGS, RETURN>
rejectsWith(error: any): Mock<TArgs, TReturn>
/**
* Schedules the mock to reject with value the next time it's called.
* If anything is already scheduled it will be used first.
* @param error error to be thrown.
*/
rejectsWithOnce(error: any): Mock<ARGS, any>
rejectsWithOnce(error: any): Mock<TArgs, any>

/**
* Specifies a different behavior when other arguments are given
* @param args arguments to match
*/
given<B extends ARGS>(
given<B extends TArgs>(
...args: B
): {
/**
* Schedules the mock to return a value the next time it's called.
* If anything is already scheduled it will be used first.
* @param value value to be returned.
*/
returnsOnce(value: RETURN): Mock<ARGS, RETURN>
returnsOnce(value: TReturn): Mock<TArgs, TReturn>

/**
* Schedules the mock to throw an error the next time it's called.
* If anything is already scheduled it will be used first.
* @param error error to be thrown.
*/
throwsOnce(error: any): Mock<ARGS, RETURN>
throwsOnce(error: any): Mock<TArgs, TReturn>

/**
* Schedules the mock use the provided implementation the next time it's called.
* If anything is already scheduled it will be used first.
* @param implementation function to execute.
*/
executesOnce(implementation: (...args: B) => RETURN): Mock<ARGS, RETURN>
executesOnce(implementation: (...args: B) => TReturn): Mock<TArgs, TReturn>

/**
* Schedules the mock to return value wrapped in Promise.resolve the next time it's called.
* If anything is already scheduled it will be used first.
* @param value value to be returned.
*/
resolvesToOnce(value: Awaited<RETURN>): Mock<ARGS, RETURN>
resolvesToOnce(value: Awaited<TReturn>): Mock<TArgs, TReturn>

/**
* Schedules the mock to reject with value the next time it's called.
* If anything is already scheduled it will be used first.
* @param error error to be thrown.
*/
rejectsWithOnce(error: any): Mock<ARGS, RETURN>
rejectsWithOnce(error: any): Mock<TArgs, TReturn>
}
}

export type MockArgs<T> = T extends Mock<infer ARGS, any> ? ARGS : never
export type MockArgs<T> = T extends Mock<infer Args, any> ? Args : never

export declare namespace Mock {
export type Of<T extends (...args: any[]) => any> = Mock<Parameters<T>, ReturnType<T>>
}
12 changes: 11 additions & 1 deletion packages/earljs/test/mocks/mockFn.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { expect } from 'chai'
import { AssertTrue, IsExact } from 'conditional-type-checks'

import { expect as earl } from '../../src'
import { mockFn, MockNotConfiguredError } from '../../src/mocks'
import { Mock, mockFn, MockNotConfiguredError } from '../../src/mocks'
import { noop } from '../common'

const sum = (a: number, b: number) => a + b
Expand Down Expand Up @@ -135,6 +136,15 @@ describe('Mock', () => {

expect(fn(2, 2)).to.eq(4)
})

it('infers types correctly with functional generic parameter', () => {
type Operation = (a: number, b: number) => number
const fn = mockFn<Operation>((a, b) => a + b)

expect(fn(2, 2)).to.eq(4)

type _ = AssertTrue<IsExact<Mock<[number, number], number>, Mock.Of<Operation>>>
})
})

describe('.executesOnce', () => {
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4579,6 +4579,11 @@ concordance@^4.0.0:
semver "^5.5.1"
well-known-symbols "^2.0.0"

conditional-type-checks@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/conditional-type-checks/-/conditional-type-checks-1.0.5.tgz#310ecd13c46c3963fbe9a2f8f93511d88cdcc973"
integrity sha512-DkfkvmjXVe4ye4llJ1JADtO3dNvqqcQM08cA9BhNt9Oe8pyRW8X1CZyBg9Qst05bDV9BJM01KLmnFh78NcJgNg==

config-chain@^1.1.11:
version "1.1.12"
resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa"
Expand Down

0 comments on commit 00cae63

Please sign in to comment.