Skip to content

Commit 9ffa00a

Browse files
authored
Merge pull request #4 from snowcoders/support-class-components
Added class component support
2 parents f9b2cd2 + a4cd99b commit 9ffa00a

File tree

24 files changed

+772
-164
lines changed

24 files changed

+772
-164
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.0.0] - 2022-12-31
11+
1012
- Initial release

README.md

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,20 @@ Simple utiltiies to help with mocking React scenarios for testing.
44

55
## Usage
66

7-
The best option is to check out our [unit tests](./src/functional-component/index.test.tsx) to see different uses however here's a quick code snippet.
7+
The best option is to check out our [integration tests](./src/tests-functional-component/child-with-props/index.test.tsx) to see more real world scenarios.
88

99
```typescript
10-
// Step 1: if using typescript, import the Props for the child component
10+
import { render } from "@testing-library/react";
11+
import React from "react";
12+
import { it, jest } from "@jest/globals";
13+
import { createMockComponent, getMockComponentPropCalls } from "../../index.js";
14+
15+
// Step 1: if using typescript, import the Prop types for the child component
1116
import type { ChildProps } from "./test-asset.child.js";
1217

1318
// Step 2: Now mock the child component
1419
jest.unstable_mockModule("./test-asset.child.js", () => ({
15-
Child: createMockFunctionComponent<ChildProps>("button"),
20+
Child: createMockComponent<ChildProps>("button"),
1621
}));
1722

1823
// Step 3: Import the parent and child, mocking the child
@@ -24,9 +29,27 @@ afterEach(() => {
2429
});
2530

2631
// Step 4: Write your test
27-
it("Mock child callback causes click count to increase", async () => {
28-
// Act
32+
it("Child callback causes click count to increase", async () => {
33+
// Arrange
34+
const result = render(<Parent />);
35+
36+
// Act - Fires the onComplicatedCallback for the last render cycle
37+
await act(() =>
38+
getMockComponentPropCalls(Child)
39+
?.at(-1)
40+
?.onClick?.({} as any)
41+
);
42+
43+
// Assert
44+
const countElement = result.getByTestId("click-count");
45+
expect(countElement.innerHTML).toBe("1");
46+
});
47+
48+
it("Clicking child causes click count to increase", async () => {
49+
// Arrange
2950
const result = render(<Parent />);
51+
52+
// Act
3053
await userEvent.click(result.getByRole("button"));
3154

3255
// Assert
@@ -46,9 +69,7 @@ There's two halves to this problem:
4669
- Does your runtime environment support this? Node started [support](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#browser_compatibility) in 14.8.0.
4770
- If using typescript, is your `module` set to [ES2022](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-5.html#module-es2022) or later?
4871

49-
## Goals
50-
51-
### Why mock?
72+
## Purpose
5273

5374
Unfortunately there exist scenarios where you may not want to render a child component; for example when that child component is delay loaded, complex, unstable, server driven, or not owned by you directly and is already covered by integration or end to end testing scenarios.
5475

@@ -62,6 +83,8 @@ To create a full integration test for this scenario would be extremely complex,
6283

6384
Instead of constantly being on the backfoot and your CI breaking because another company updated their systems, mocking those dependencies provides a level of stability at the sacrifice of real world resemblance.
6485

86+
## Goals
87+
6588
### Dependencies
6689

6790
This project's goal is to have only two dependencies: React and Jest. This way it can be utilized by any React testing system (e.g. @testing-library/react, react-test-renderer, or other) and not tie you down to a specific testing system.

src/functional-component/child-with-children/index.test.tsx

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

src/functional-component/index.ts

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

src/index.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// This file contains some basic tests, for the integration tests see `tests-class-component` and `tests-function-component`
2+
3+
import { describe, it, jest } from "@jest/globals";
4+
import { render } from "@testing-library/react";
5+
import { createMockComponent, getMockComponentPropCalls } from "./index.js";
6+
import React from "react";
7+
import "@testing-library/jest-dom";
8+
9+
describe("createMockComponent", () => {
10+
it("returns without any parameters", () => {
11+
// Arrange
12+
const result = createMockComponent();
13+
14+
// Assert
15+
expect(result).toBeDefined();
16+
});
17+
18+
it("returns with empty options", () => {
19+
// Arrange
20+
const result = createMockComponent({});
21+
22+
// Assert
23+
expect(result).toBeDefined();
24+
});
25+
26+
it("returns with custom elementType", () => {
27+
// Arrange
28+
const result = createMockComponent({ elementType: "custom" });
29+
30+
// Assert
31+
expect(result).toBeDefined();
32+
});
33+
});
34+
35+
describe("getMockComponentPropCalls", () => {
36+
describe("Success scenarios", () => {
37+
it("has 0 calls if not rendered", () => {
38+
// Arrange
39+
const result = createMockComponent();
40+
41+
// Assert
42+
const calls = getMockComponentPropCalls(result);
43+
expect(calls).toHaveLength(0);
44+
});
45+
46+
it("has 1 call if rendered", () => {
47+
// Arrange
48+
const result = createMockComponent();
49+
50+
// Act
51+
render(React.createElement(result));
52+
53+
// Assert
54+
const calls = getMockComponentPropCalls(result);
55+
expect(calls).toHaveLength(1);
56+
});
57+
});
58+
59+
describe("Negative tests", () => {
60+
it("throws an error if passed nothing", () => {
61+
expect(() => {
62+
// @ts-expect-error I'm testing a negative scenario here so I need to break typescript a bit
63+
getMockComponentPropCalls();
64+
}).toThrowError("Did you forget to call createMockComponent");
65+
});
66+
67+
it("throws an error if passed a real component", () => {
68+
expect(() => {
69+
getMockComponentPropCalls(
70+
// @ts-expect-error I'm testing a negative scenario here so I need to break typescript a bit
71+
() => React.createElement("span")
72+
);
73+
}).toThrowError("Did you forget to call createMockComponent");
74+
});
75+
});
76+
});

src/index.ts

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,59 @@
1-
import { createMockFunctionComponent, getMockFunctionComponentPropCalls } from "./functional-component/index.js";
1+
import React from "react";
2+
import { jest } from "@jest/globals";
3+
4+
const defaultOptions: Required<Options> = {
5+
elementType: "div",
6+
};
7+
8+
export type Options = {
9+
/**
10+
* The HTMLElement type to render. Default is div.
11+
*/
12+
elementType?: string;
13+
};
214

315
/**
416
* Generates a new mock component with the prop signature provided.
517
*
618
* @param {string} elementType the HTMLElement type to render. Default is div.
719
* @returns A mock component to be used in conjunction with other utilities in this library
820
*/
9-
export function createMockComponent<TProps>(
10-
mockComponent: Parameters<typeof createMockFunctionComponent<TProps>>[0]
11-
): ReturnType<typeof createMockFunctionComponent<TProps>> {
12-
return createMockFunctionComponent(mockComponent);
21+
export function createMockComponent<TProps>(options?: Options) {
22+
const { elementType } = {
23+
...defaultOptions,
24+
...options,
25+
};
26+
27+
// Our implementation renders all components (class and function) as function components
28+
// so forcing the type to function here
29+
return jest.fn((props: TProps) => {
30+
if (props == null) {
31+
return React.createElement(elementType);
32+
} else if (typeof props === "object" && "children" in props) {
33+
const { children, ...rest } = props as React.PropsWithChildren<TProps>;
34+
return React.createElement(elementType, rest, children);
35+
} else {
36+
return React.createElement(elementType, props);
37+
}
38+
});
1339
}
1440

1541
export function getMockComponentPropCalls<TProps>(
16-
mockComponent: Parameters<typeof getMockFunctionComponentPropCalls<TProps>>[0]
17-
): ReturnType<typeof getMockFunctionComponentPropCalls<TProps>> {
18-
return getMockFunctionComponentPropCalls(mockComponent);
42+
mockComponent:
43+
| jest.MockedFunction<React.FC<TProps>> // Function components
44+
| typeof React.Component<TProps> // Class components
45+
): Readonly<TProps>[] {
46+
// Our implementation renders all components (class and function) as function components
47+
// so forcing the type to function here
48+
const castedMockComponent = mockComponent as jest.MockedFunction<React.FC<TProps>>;
49+
const calls = castedMockComponent?.mock?.calls;
50+
if (calls == null) {
51+
throw new Error(
52+
"Parameter to getMockComponentPropCalls must be a MockComponent. Did you forget to call createMockComponent?"
53+
);
54+
}
55+
const propCalls = calls.map((value) => {
56+
return value[0];
57+
});
58+
return propCalls;
1959
}

0 commit comments

Comments
 (0)