Skip to content

Commit

Permalink
fix(types): update generics (#443)
Browse files Browse the repository at this point in the history
* fix(types): augument global jest

* chore: move refactors

* chore: remove unused deps

* chore: allow mocking with undefined in types

* test: set value as undefined
  • Loading branch information
iamogbz authored Oct 24, 2020
1 parent cf3cb72 commit 77c80be
Show file tree
Hide file tree
Showing 10 changed files with 131 additions and 758 deletions.
703 changes: 32 additions & 671 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 9 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@
".",
"./node_modules"
],
"moduleFileExtensions": [
"js",
"d.ts",
"ts",
"tsx",
"jsx",
"json",
"node"
],
"setupFilesAfterEnv": [
"./config/setupTests.ts"
],
Expand Down Expand Up @@ -101,22 +110,18 @@
"@commitlint/cli": "^11.0.0",
"@commitlint/config-conventional": "^11.0.0",
"@commitlint/travis-cli": "^11.0.0",
"@types/copy-webpack-plugin": "^6.0.0",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.2",
"@types/source-map": "^0.5.2",
"@types/webpack": "^4.41.23",
"@typescript-eslint/eslint-plugin": "^2.34.0",
"@typescript-eslint/parser": "^2.34.0",
"acorn": "^8.0.4",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"commitizen": "^4.2.2",
"copy-webpack-plugin": "^6.2.1",
"coveralls": "^3.1.0",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^7.11.0",
"eslint-config-airbnb": "^18.2.0",
"eslint-config-prettier": "^6.14.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-prettier": "^3.1.4",
Expand Down
69 changes: 28 additions & 41 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,17 @@
import { IsMockProp, Obj, Spyable, SpyOnProp, ValueOf } from "./types";

export const messages = {
error: {
invalidSpy: (o: object): string => {
const helpfulValue = `${o ? typeof o : ""}'${o}'`;
return `Cannot spyOn on a primitive value; ${helpfulValue} given.`;
},
noMethodSpy: (p: string): string =>
`Cannot spy on the property '${p}' because it is a function. Please use \`jest.spyOn\`.`,
noUnconfigurableSpy: (p: string): string =>
`Cannot spy on the property '${p}' because it is not configurable`,
},
warn: {
noUndefinedSpy: (p: string): string =>
`Spying on an undefined property '${p}'.`,
},
};

export const log = (...args: unknown[]): void => log.default(...args);
// eslint-disable-next-line no-console
log.default = log.warn = (...args: unknown[]): void => console.warn(...args);

const spiedOn: Map<
import {
ExtendJest,
IsMockProp,
MockProp,
Spyable,
Map<string, MockProp<ValueOf<Spyable>>>
> = new Map();
SpyMap,
SpyOnProp,
} from "../typings/globals";
import log from "./utils/logging";
import messages from "./utils/messages";

const spiedOn: SpyMap<Spyable> = new Map();
const getAllSpies = () => {
const spies: Set<MockProp<ValueOf<Spyable>>> = new Set();
const spies: Set<MockProp> = new Set();
for (const spiedProps of spiedOn.values()) {
for (const spy of spiedProps.values()) {
spies.add(spy);
Expand All @@ -35,15 +20,15 @@ const getAllSpies = () => {
return spies;
};

export class MockProp<T> {
class MockPropInstance<T, K extends keyof T> implements MockProp<T, K> {
private initialPropDescriptor: PropertyDescriptor;
private initialPropValue: T;
private object: Obj<T>;
private propName: string;
private propValue: T;
private propValues: T[] = [];
private initialPropValue: T[K];
private object: T;
private propName: K;
private propValue: T[K];
private propValues: T[K][] = [];

constructor({ object, propName }: { object: Obj<T>; propName: string }) {
constructor({ object, propName }: { object: T; propName: K }) {
this.initialPropDescriptor = this.validate({ object, propName });
this.object = object;
this.propName = propName;
Expand Down Expand Up @@ -81,7 +66,7 @@ export class MockProp<T> {
/**
* Set the value of the mocked property
*/
public mockValue = (value: T): MockProp<T> => {
public mockValue = (value: T[K]): MockProp<T, K> => {
this.propValues = [];
this.propValue = value;
return this;
Expand All @@ -90,7 +75,7 @@ export class MockProp<T> {
/**
* Next value returned when the property is accessed
*/
public mockValueOnce = (value: T): MockProp<T> => {
public mockValueOnce = (value: T[K]): MockProp<T, K> => {
this.propValues.push(value);
return this;
};
Expand All @@ -102,8 +87,8 @@ export class MockProp<T> {
object,
propName,
}: {
object: Obj<T>;
propName: string;
object: T;
propName: K;
}): PropertyDescriptor => {
const acceptedTypes: Set<string> = new Set(["function", "object"]);
if (object === null || !acceptedTypes.has(typeof object)) {
Expand Down Expand Up @@ -158,7 +143,7 @@ export class MockProp<T> {
/**
* Shift and return the first next, defaulting to the mocked value
*/
private nextValue = (): T => this.propValues.shift() || this.propValue;
private nextValue = (): T[K] => this.propValues.shift() || this.propValue;
}

export const isMockProp: IsMockProp = (object, propName) => {
Expand All @@ -179,10 +164,10 @@ export const spyOnProp: SpyOnProp = (object, propName) => {
if (isMockProp(object, propName)) {
return spiedOn.get(object).get(propName);
}
return new MockProp({ object, propName });
return new MockPropInstance({ object, propName });
};

export const extend = (jestInstance: typeof jest): void => {
export const extend: ExtendJest = (jestInstance: typeof jest): void => {
const jestClearAll = jestInstance.clearAllMocks;
const jestResetAll = jestInstance.resetAllMocks;
const jestRestoreAll = jestInstance.restoreAllMocks;
Expand All @@ -194,3 +179,5 @@ export const extend = (jestInstance: typeof jest): void => {
spyOnProp,
});
};

export * from "../typings/globals";
26 changes: 0 additions & 26 deletions src/types.d.ts

This file was deleted.

5 changes: 5 additions & 0 deletions src/utils/logging.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const log = (...args: unknown[]): void => log.default(...args);
// eslint-disable-next-line no-console
log.default = log.warn = (...args: unknown[]): void => console.warn(...args);

export default log;
16 changes: 16 additions & 0 deletions src/utils/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export default {
error: {
invalidSpy: <T>(o: T): string => {
const helpfulValue = `${o ? typeof o : ""}'${o}'`;
return `Cannot spyOn on a primitive value; ${helpfulValue} given.`;
},
noMethodSpy: <K>(p: K): string =>
`Cannot spy on the property '${p}' because it is a function. Please use \`jest.spyOn\`.`,
noUnconfigurableSpy: <K>(p: K): string =>
`Cannot spy on the property '${p}' because it is not configurable`,
},
warn: {
noUndefinedSpy: <K>(p: K): string =>
`Spying on an undefined property '${p}'.`,
},
};
15 changes: 9 additions & 6 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Obj } from "src/types";
import messages from "src/utils/messages";
import * as mockProps from "src/index";
import { Spyable } from "typings/globals";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockObject: Obj<any> = {
const mockObject: Spyable = {
fn1: (): string => "fnReturnValue",
prop1: "1",
prop2: 2,
Expand All @@ -22,9 +23,11 @@ it("mock object undefined property", () => {
// @ts-ignore
const spy = jest.spyOnProp(process.env, "undefinedProp").mockValue(1);
expect(spyConsoleWarn).toHaveBeenCalledWith(
mockProps.messages.warn.noUndefinedSpy("undefinedProp"),
messages.warn.noUndefinedSpy("undefinedProp"),
);
expect(process.env.undefinedProp).toEqual(1);
spy.mockValue(undefined);
expect(process.env.undefinedProp).toBeUndefined();
process.env.undefinedProp = "5";
expect(process.env.undefinedProp).toEqual("5");
expect(jest.isMockProp(process.env, "undefinedProp")).toBe(true);
Expand All @@ -34,7 +37,7 @@ it("mock object undefined property", () => {
});

it("mocks object property value undefined", () => {
const testObject: Obj<number> = { propUndefined: undefined };
const testObject: Record<string, number> = { propUndefined: undefined };
const spy = jest.spyOnProp(testObject, "propUndefined").mockValue(1);
expect(testObject.propUndefined).toEqual(1);
testObject.propUndefined = 5;
Expand All @@ -46,7 +49,7 @@ it("mocks object property value undefined", () => {
});

it("mocks object property value null", () => {
const testObject: Obj<number> = { propNull: null };
const testObject: Record<string, number> = { propNull: null };
const spy = jest.spyOnProp(testObject, "propNull").mockValue(2);
expect(testObject.propNull).toEqual(2);
testObject.propNull = 10;
Expand Down Expand Up @@ -198,7 +201,7 @@ it.each([undefined, null, 99, "value", true].map((v) => [v && typeof v, v]))(
);

it("does not mock object non-configurable property", () => {
const testObject = {};
const testObject: Spyable = {};
Object.defineProperty(testObject, "propUnconfigurable", { value: 2 });
expect(() =>
jest.spyOnProp(testObject, "propUnconfigurable"),
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.prod.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
"baseUrl": "./src",
},
"extends": "./tsconfig.json",
"include": ["./src"]
"include": ["./src", "./typings"],
}
28 changes: 28 additions & 0 deletions typings/globals.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Spyable = any;

export interface MockProp<T = Spyable, K extends keyof T = keyof T> {
mockClear(): void;
mockReset(): void;
mockRestore(): void;
mockValue(v?: T[K]): MockProp<T, K>;
mockValueOnce(v?: T[K]): MockProp<T, K>;
}

export type SpyMap<T> = Map<T, Map<keyof T, MockProp<T, keyof T>>>;

export type SpyOnProp = <T>(object: T, propName: keyof T) => MockProp<T>;

export type IsMockProp = <T, K extends keyof T>(
object: T,
propName: K,
) => boolean;

declare global {
namespace jest {
const isMockProp: IsMockProp;
const spyOnProp: SpyOnProp;
}
}

export type ExtendJest = (jestInstance: typeof jest) => void;
12 changes: 3 additions & 9 deletions webpack.config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { execSync } from "child_process";
import * as path from "path";
import { Configuration } from "webpack";
import * as CopyWebpackPlugin from "copy-webpack-plugin";
import { WebpackCompilerPlugin } from "webpack-compiler-plugin";

const entryPath = path.resolve(__dirname, "src");
const outputPath = path.resolve(__dirname, "lib");
const configuration: Configuration = {
devtool: "source-map",
entry: "./src",
entry: entryPath,
mode: "production",
module: {
rules: [
Expand Down Expand Up @@ -42,15 +42,9 @@ const configuration: Configuration = {
},
stageMessages: null,
}),
new CopyWebpackPlugin({
patterns: ["types"].map((t) => ({
from: `./src/${t}.d.ts`,
to: outputPath,
})),
}),
],
resolve: {
extensions: [".js", ".ts"],
extensions: [".js", ".ts", ".d.ts"],
modules: [path.resolve("./src"), path.resolve("./node_modules")],
},
};
Expand Down

0 comments on commit 77c80be

Please sign in to comment.