Skip to content

Commit 77c80be

Browse files
authored
fix(types): update generics (#443)
* fix(types): augument global jest * chore: move refactors * chore: remove unused deps * chore: allow mocking with undefined in types * test: set value as undefined
1 parent cf3cb72 commit 77c80be

File tree

10 files changed

+131
-758
lines changed

10 files changed

+131
-758
lines changed

package-lock.json

Lines changed: 32 additions & 671 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@
3434
".",
3535
"./node_modules"
3636
],
37+
"moduleFileExtensions": [
38+
"js",
39+
"d.ts",
40+
"ts",
41+
"tsx",
42+
"jsx",
43+
"json",
44+
"node"
45+
],
3746
"setupFilesAfterEnv": [
3847
"./config/setupTests.ts"
3948
],
@@ -101,22 +110,18 @@
101110
"@commitlint/cli": "^11.0.0",
102111
"@commitlint/config-conventional": "^11.0.0",
103112
"@commitlint/travis-cli": "^11.0.0",
104-
"@types/copy-webpack-plugin": "^6.0.0",
105113
"@types/jest": "^26.0.15",
106114
"@types/node": "^14.14.2",
107115
"@types/source-map": "^0.5.2",
108-
"@types/webpack": "^4.41.23",
109116
"@typescript-eslint/eslint-plugin": "^2.34.0",
110117
"@typescript-eslint/parser": "^2.34.0",
111118
"acorn": "^8.0.4",
112119
"babel-eslint": "^10.1.0",
113120
"babel-loader": "^8.1.0",
114121
"commitizen": "^4.2.2",
115-
"copy-webpack-plugin": "^6.2.1",
116122
"coveralls": "^3.1.0",
117123
"cz-conventional-changelog": "^3.3.0",
118124
"eslint": "^7.11.0",
119-
"eslint-config-airbnb": "^18.2.0",
120125
"eslint-config-prettier": "^6.14.0",
121126
"eslint-plugin-import": "^2.22.1",
122127
"eslint-plugin-prettier": "^3.1.4",

src/index.ts

Lines changed: 28 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,17 @@
1-
import { IsMockProp, Obj, Spyable, SpyOnProp, ValueOf } from "./types";
2-
3-
export const messages = {
4-
error: {
5-
invalidSpy: (o: object): string => {
6-
const helpfulValue = `${o ? typeof o : ""}'${o}'`;
7-
return `Cannot spyOn on a primitive value; ${helpfulValue} given.`;
8-
},
9-
noMethodSpy: (p: string): string =>
10-
`Cannot spy on the property '${p}' because it is a function. Please use \`jest.spyOn\`.`,
11-
noUnconfigurableSpy: (p: string): string =>
12-
`Cannot spy on the property '${p}' because it is not configurable`,
13-
},
14-
warn: {
15-
noUndefinedSpy: (p: string): string =>
16-
`Spying on an undefined property '${p}'.`,
17-
},
18-
};
19-
20-
export const log = (...args: unknown[]): void => log.default(...args);
21-
// eslint-disable-next-line no-console
22-
log.default = log.warn = (...args: unknown[]): void => console.warn(...args);
23-
24-
const spiedOn: Map<
1+
import {
2+
ExtendJest,
3+
IsMockProp,
4+
MockProp,
255
Spyable,
26-
Map<string, MockProp<ValueOf<Spyable>>>
27-
> = new Map();
6+
SpyMap,
7+
SpyOnProp,
8+
} from "../typings/globals";
9+
import log from "./utils/logging";
10+
import messages from "./utils/messages";
11+
12+
const spiedOn: SpyMap<Spyable> = new Map();
2813
const getAllSpies = () => {
29-
const spies: Set<MockProp<ValueOf<Spyable>>> = new Set();
14+
const spies: Set<MockProp> = new Set();
3015
for (const spiedProps of spiedOn.values()) {
3116
for (const spy of spiedProps.values()) {
3217
spies.add(spy);
@@ -35,15 +20,15 @@ const getAllSpies = () => {
3520
return spies;
3621
};
3722

38-
export class MockProp<T> {
23+
class MockPropInstance<T, K extends keyof T> implements MockProp<T, K> {
3924
private initialPropDescriptor: PropertyDescriptor;
40-
private initialPropValue: T;
41-
private object: Obj<T>;
42-
private propName: string;
43-
private propValue: T;
44-
private propValues: T[] = [];
25+
private initialPropValue: T[K];
26+
private object: T;
27+
private propName: K;
28+
private propValue: T[K];
29+
private propValues: T[K][] = [];
4530

46-
constructor({ object, propName }: { object: Obj<T>; propName: string }) {
31+
constructor({ object, propName }: { object: T; propName: K }) {
4732
this.initialPropDescriptor = this.validate({ object, propName });
4833
this.object = object;
4934
this.propName = propName;
@@ -81,7 +66,7 @@ export class MockProp<T> {
8166
/**
8267
* Set the value of the mocked property
8368
*/
84-
public mockValue = (value: T): MockProp<T> => {
69+
public mockValue = (value: T[K]): MockProp<T, K> => {
8570
this.propValues = [];
8671
this.propValue = value;
8772
return this;
@@ -90,7 +75,7 @@ export class MockProp<T> {
9075
/**
9176
* Next value returned when the property is accessed
9277
*/
93-
public mockValueOnce = (value: T): MockProp<T> => {
78+
public mockValueOnce = (value: T[K]): MockProp<T, K> => {
9479
this.propValues.push(value);
9580
return this;
9681
};
@@ -102,8 +87,8 @@ export class MockProp<T> {
10287
object,
10388
propName,
10489
}: {
105-
object: Obj<T>;
106-
propName: string;
90+
object: T;
91+
propName: K;
10792
}): PropertyDescriptor => {
10893
const acceptedTypes: Set<string> = new Set(["function", "object"]);
10994
if (object === null || !acceptedTypes.has(typeof object)) {
@@ -158,7 +143,7 @@ export class MockProp<T> {
158143
/**
159144
* Shift and return the first next, defaulting to the mocked value
160145
*/
161-
private nextValue = (): T => this.propValues.shift() || this.propValue;
146+
private nextValue = (): T[K] => this.propValues.shift() || this.propValue;
162147
}
163148

164149
export const isMockProp: IsMockProp = (object, propName) => {
@@ -179,10 +164,10 @@ export const spyOnProp: SpyOnProp = (object, propName) => {
179164
if (isMockProp(object, propName)) {
180165
return spiedOn.get(object).get(propName);
181166
}
182-
return new MockProp({ object, propName });
167+
return new MockPropInstance({ object, propName });
183168
};
184169

185-
export const extend = (jestInstance: typeof jest): void => {
170+
export const extend: ExtendJest = (jestInstance: typeof jest): void => {
186171
const jestClearAll = jestInstance.clearAllMocks;
187172
const jestResetAll = jestInstance.resetAllMocks;
188173
const jestRestoreAll = jestInstance.restoreAllMocks;
@@ -194,3 +179,5 @@ export const extend = (jestInstance: typeof jest): void => {
194179
spyOnProp,
195180
});
196181
};
182+
183+
export * from "../typings/globals";

src/types.d.ts

Lines changed: 0 additions & 26 deletions
This file was deleted.

src/utils/logging.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const log = (...args: unknown[]): void => log.default(...args);
2+
// eslint-disable-next-line no-console
3+
log.default = log.warn = (...args: unknown[]): void => console.warn(...args);
4+
5+
export default log;

src/utils/messages.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export default {
2+
error: {
3+
invalidSpy: <T>(o: T): string => {
4+
const helpfulValue = `${o ? typeof o : ""}'${o}'`;
5+
return `Cannot spyOn on a primitive value; ${helpfulValue} given.`;
6+
},
7+
noMethodSpy: <K>(p: K): string =>
8+
`Cannot spy on the property '${p}' because it is a function. Please use \`jest.spyOn\`.`,
9+
noUnconfigurableSpy: <K>(p: K): string =>
10+
`Cannot spy on the property '${p}' because it is not configurable`,
11+
},
12+
warn: {
13+
noUndefinedSpy: <K>(p: K): string =>
14+
`Spying on an undefined property '${p}'.`,
15+
},
16+
};

tests/index.test.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { Obj } from "src/types";
1+
import messages from "src/utils/messages";
22
import * as mockProps from "src/index";
3+
import { Spyable } from "typings/globals";
34

45
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5-
const mockObject: Obj<any> = {
6+
const mockObject: Spyable = {
67
fn1: (): string => "fnReturnValue",
78
prop1: "1",
89
prop2: 2,
@@ -22,9 +23,11 @@ it("mock object undefined property", () => {
2223
// @ts-ignore
2324
const spy = jest.spyOnProp(process.env, "undefinedProp").mockValue(1);
2425
expect(spyConsoleWarn).toHaveBeenCalledWith(
25-
mockProps.messages.warn.noUndefinedSpy("undefinedProp"),
26+
messages.warn.noUndefinedSpy("undefinedProp"),
2627
);
2728
expect(process.env.undefinedProp).toEqual(1);
29+
spy.mockValue(undefined);
30+
expect(process.env.undefinedProp).toBeUndefined();
2831
process.env.undefinedProp = "5";
2932
expect(process.env.undefinedProp).toEqual("5");
3033
expect(jest.isMockProp(process.env, "undefinedProp")).toBe(true);
@@ -34,7 +37,7 @@ it("mock object undefined property", () => {
3437
});
3538

3639
it("mocks object property value undefined", () => {
37-
const testObject: Obj<number> = { propUndefined: undefined };
40+
const testObject: Record<string, number> = { propUndefined: undefined };
3841
const spy = jest.spyOnProp(testObject, "propUndefined").mockValue(1);
3942
expect(testObject.propUndefined).toEqual(1);
4043
testObject.propUndefined = 5;
@@ -46,7 +49,7 @@ it("mocks object property value undefined", () => {
4649
});
4750

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

200203
it("does not mock object non-configurable property", () => {
201-
const testObject = {};
204+
const testObject: Spyable = {};
202205
Object.defineProperty(testObject, "propUnconfigurable", { value: 2 });
203206
expect(() =>
204207
jest.spyOnProp(testObject, "propUnconfigurable"),

tsconfig.prod.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
"baseUrl": "./src",
44
},
55
"extends": "./tsconfig.json",
6-
"include": ["./src"]
6+
"include": ["./src", "./typings"],
77
}

typings/globals.d.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2+
export type Spyable = any;
3+
4+
export interface MockProp<T = Spyable, K extends keyof T = keyof T> {
5+
mockClear(): void;
6+
mockReset(): void;
7+
mockRestore(): void;
8+
mockValue(v?: T[K]): MockProp<T, K>;
9+
mockValueOnce(v?: T[K]): MockProp<T, K>;
10+
}
11+
12+
export type SpyMap<T> = Map<T, Map<keyof T, MockProp<T, keyof T>>>;
13+
14+
export type SpyOnProp = <T>(object: T, propName: keyof T) => MockProp<T>;
15+
16+
export type IsMockProp = <T, K extends keyof T>(
17+
object: T,
18+
propName: K,
19+
) => boolean;
20+
21+
declare global {
22+
namespace jest {
23+
const isMockProp: IsMockProp;
24+
const spyOnProp: SpyOnProp;
25+
}
26+
}
27+
28+
export type ExtendJest = (jestInstance: typeof jest) => void;

webpack.config.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { execSync } from "child_process";
22
import * as path from "path";
33
import { Configuration } from "webpack";
4-
import * as CopyWebpackPlugin from "copy-webpack-plugin";
54
import { WebpackCompilerPlugin } from "webpack-compiler-plugin";
65

6+
const entryPath = path.resolve(__dirname, "src");
77
const outputPath = path.resolve(__dirname, "lib");
88
const configuration: Configuration = {
99
devtool: "source-map",
10-
entry: "./src",
10+
entry: entryPath,
1111
mode: "production",
1212
module: {
1313
rules: [
@@ -42,15 +42,9 @@ const configuration: Configuration = {
4242
},
4343
stageMessages: null,
4444
}),
45-
new CopyWebpackPlugin({
46-
patterns: ["types"].map((t) => ({
47-
from: `./src/${t}.d.ts`,
48-
to: outputPath,
49-
})),
50-
}),
5145
],
5246
resolve: {
53-
extensions: [".js", ".ts"],
47+
extensions: [".js", ".ts", ".d.ts"],
5448
modules: [path.resolve("./src"), path.resolve("./node_modules")],
5549
},
5650
};

0 commit comments

Comments
 (0)