Skip to content

Commit

Permalink
feat(observable): add ability to spy on observable properties
Browse files Browse the repository at this point in the history
now you can pass a config object to createSpyFromClass to tell it which observable properties to
create spies for | and it will add the spying methods like "nextWith" to it | the last commit also
added type safety to manually configured methods (I just squash merged it so it got hidden from the
changelog)

closes #2
  • Loading branch information
shairez committed Sep 3, 2020
1 parent 980b830 commit b75269a
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 93 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"build": "run-s clean compile",
"test": "karma start",
"test:watch": "karma start --auto-watch --no-single-run",
"test:full": "run-s lint test",
"test:full": "run-s lint:fix test",
"format:fix": "pretty-quick --staged",
"lint": "eslint . --ext .js,.ts",
"lint:fix": "eslint . --ext .js,.ts --fix",
Expand Down Expand Up @@ -52,6 +52,7 @@
"@commitlint/cli": "9.1.2",
"@commitlint/config-conventional": "9.1.2",
"@hirez_io/jasmine-given": "1.0.4",
"@hirez_io/observer-spy": "^1.3.0",
"@types/deep-equal": "1.0.1",
"@types/jasmine": "^3.5.14",
"@types/tapable": "1.0.6",
Expand Down
98 changes: 45 additions & 53 deletions src/auto-spies.types.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,60 @@
/// <reference types="jasmine" />
import { Observable, Subject } from 'rxjs';

export type Spy<T> = { [k in keyof T]: AddSpyTypes<T[k]> };
export type Spy<T> = {
[k in keyof T]: T[k] extends (...args: any[]) => any
? AddSpyTypesToMethods<T[k]>
: T[k] extends Observable<infer OR>
? T[k] & AddObservableSpyMethods<OR>
: T[k];
};

export type AddSpyTypes<T> = T extends (...args: any[]) => any
export type AddSpyTypesToMethods<T> = T extends (...args: any[]) => any
? AddSpyByReturnTypes<T>
: T;

// this will add spy types on every proeprty of the given type.
// not only functions by return value.
// export type AddSpyTypes<T> = T extends (...args: any[]) => any
// ? AddSpyOnReturnTypes<T>
// : T extends Promise<any>
// ? AddSpyOnPromise<T>
// : T extends Observable<any> ? AddSpyOnObservable<T> : T;
// Wrap the return type of the given function type with the appropriate spy methods
export type AddSpyByReturnTypes<TF extends (...args: any[]) => any> = TF &
(TF extends (...args: any[]) => infer TR
? // returns a Promise
TR extends Promise<infer PR>
? AddSpyToPromiseMethod<PR>
: // returns an Observable
TR extends Observable<infer OR>
? AddSpyToObservableMethod<OR>
: // for any other type
AddSpyToMethod<TF>
: never);

export type AddSpyToObservableProps<OP extends Observable<any>> = OP extends Observable<
infer OR
>
? OP & AddObservableSpyMethods<OR>
: OP;

export type AddSpyToPromiseMethod<PromiseReturnType> = {
and: PromiseMethodSpy<PromiseReturnType>;
calledWith(...args: any[]): PromiseMethodSpy<PromiseReturnType>;
mustBeCalledWith(...args: any[]): PromiseMethodSpy<PromiseReturnType>;
} & jasmine.Spy;

export interface PromiseMethodSpy<T> {
resolveWith(value?: T): void;
rejectWith(value?: any): void;
}

export interface ObservableMethodSpy<T> {
export type AddSpyToObservableMethod<ObservableReturnType> = {
and: AddObservableSpyMethods<ObservableReturnType>;
calledWith(...args: any[]): AddObservableSpyMethods<ObservableReturnType>;
mustBeCalledWith(...args: any[]): AddObservableSpyMethods<ObservableReturnType>;
} & jasmine.Spy;

export interface AddObservableSpyMethods<T> {
nextWith(value?: T): void;
nextOneTimeWith(value?: T): void; // emit one value and completes
throwWith(value: any): void;
complete(): void;
returnSubject<R = any>(): Subject<R>;
returnSubject(): Subject<T>;
}

export interface MethodSpy {
Expand All @@ -41,51 +70,10 @@ export interface MethodSpy {
};
}

export type AddSpyOnFunction<T extends (...args: any[]) => any> = T &
export type AddSpyToMethod<T extends (...args: any[]) => any> = T &
MethodSpy &
jasmine.Spy;

export type AddSpyOnPromise<T extends Promise<any>> = {
and: PromiseMethodSpy<Unpacked<T>>;
calledWith(...args: any[]): PromiseMethodSpy<Unpacked<T>>;
mustBeCalledWith(...args: any[]): PromiseMethodSpy<Unpacked<T>>;
} & jasmine.Spy;

export type AddSpyOnObservable<T extends Observable<any>> = {
and: ObservableMethodSpy<Unpacked<T>>;
calledWith(...args: any[]): ObservableMethodSpy<Unpacked<T>>;
mustBeCalledWith(...args: any[]): ObservableMethodSpy<Unpacked<T>>;
} & jasmine.Spy;

// Wrap the return type of the given function type with the appropriate spy methods
export type AddSpyByReturnTypes<TF extends (...args: any[]) => any> = TF &
(TF extends (...args: any[]) => infer TR // returns a function
? TR extends (...args: any[]) => infer R2
? AddSpyOnFunction<TR> // returns a Promise
: TR extends Promise<any>
? AddSpyOnPromise<TR> // returns an Observable
: TR extends Observable<any>
? AddSpyOnObservable<TR>
: AddSpyOnFunction<TF>
: never);

// export type AddSpyOnFunctionReturnType<
// TF extends (...args: any[]) => any
// > = TF extends (...args: any[]) => any
// ? TF & { and: AddSpyOnReturnTypes<TF> }
// : never;

// https://github.com/Microsoft/TypeScript/issues/21705#issue-294964744
export type Unpacked<T> = T extends Array<infer U1>
? U1
: T extends (...args: any[]) => infer U2
? U2
: T extends Promise<infer U3>
? U3
: T extends Observable<infer U4>
? U4
: T;

type KeysForPropertyType<ObjectType, PropType> = Extract<
{
[Key in keyof ObjectType]: ObjectType[Key] extends PropType ? Key : never;
Expand All @@ -94,3 +82,7 @@ type KeysForPropertyType<ObjectType, PropType> = Extract<
>;

export type OnlyMethodKeysOf<T> = KeysForPropertyType<T, { (...args: any[]): any }>;

export type OnlyObservablePropsOf<T> = KeysForPropertyType<T, Observable<any>>;

export type GetObservableReturnType<OT> = OT extends Observable<infer OR> ? OR : never;
4 changes: 2 additions & 2 deletions src/create-function-spy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AddSpyTypes } from '.';
import { AddSpyTypesToMethods } from '.';
import {
CalledWithObject,
FunctionSpyReturnValueContainer,
Expand All @@ -14,7 +14,7 @@ import {
import deepEqual from 'deep-equal';
import { throwArgumentsError } from './errors/error-handling';

export function createFunctionSpy<MT>(name: string): AddSpyTypes<MT> {
export function createFunctionSpy<MT>(name: string): AddSpyTypesToMethods<MT> {
const functionSpy: any = jasmine.createSpy(name);

let calledWithObject: CalledWithObject = {
Expand Down
17 changes: 17 additions & 0 deletions src/create-spy-from-class.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,23 @@ describe('createSpyFromClass', () => {
});
});

describe('GIVEN a synchronous method is being manually configured using the config object', () => {
Given(() => {
fakeClassSpy = createSpyFromClass(FakeClass, {
providedMethodNames: ['arrowMethod'],
});
fakeClassSpy.arrowMethod.and.returnValue(fakeValue);
});

When(() => {
actualResult = fakeClassSpy.arrowMethod();
});

Then(() => {
expect(actualResult).toBe(fakeValue);
});
});

describe('GIVEN a synchronous method is being configured with specific parameters', () => {
Given(() => {
fakeArgs = [1, { a: 2 }];
Expand Down
29 changes: 26 additions & 3 deletions src/create-spy-from-class.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,42 @@
import { Spy, OnlyMethodKeysOf } from './auto-spies.types';
import { Spy, OnlyMethodKeysOf, OnlyObservablePropsOf } from './auto-spies.types';
import { createFunctionSpy } from './create-function-spy';
import { createObservablePropSpy } from './observables/observable-spy-utils';

export interface ClassSpyConfiguration<T> {
providedMethodNames?: OnlyMethodKeysOf<T>[];
observablePropsToSpyOn?: OnlyObservablePropsOf<T>[];
}

export function createSpyFromClass<T>(
ObjectClass: { new (...args: any[]): T; [key: string]: any },
providedMethodNames?: OnlyMethodKeysOf<T>[]
providedMethodNamesOrConfig?: OnlyMethodKeysOf<T>[] | ClassSpyConfiguration<T>
): Spy<T> {
const proto = ObjectClass.prototype;
const methodNames = getAllMethodNames(proto);

if (providedMethodNames && providedMethodNames.length > 0) {
let providedMethodNames: OnlyMethodKeysOf<T>[] = [];
let observablePropsToSpyOn: OnlyObservablePropsOf<T>[] = [];

if (providedMethodNamesOrConfig) {
if (Array.isArray(providedMethodNamesOrConfig)) {
providedMethodNames = providedMethodNamesOrConfig;
} else {
observablePropsToSpyOn = providedMethodNamesOrConfig.observablePropsToSpyOn || [];
providedMethodNames = providedMethodNamesOrConfig.providedMethodNames || [];
}
}
if (providedMethodNames.length > 0) {
methodNames.push(...providedMethodNames);
}

const autoSpy: any = {};

if (observablePropsToSpyOn.length > 0) {
observablePropsToSpyOn.forEach((observablePropName) => {
autoSpy[observablePropName] = createObservablePropSpy();
});
}

methodNames.forEach((methodName) => {
autoSpy[methodName] = createFunctionSpy(methodName);
});
Expand Down
Loading

0 comments on commit b75269a

Please sign in to comment.