Skip to content

Commit ea871bb

Browse files
sjwilczynskiStanislaw Wilczynski
and
Stanislaw Wilczynski
authored
feat: bump react to version 18 and run all tests/stories in strict mode (#101)
* bump react * Change files * use strict mode in stories and tests * bump node version * make strict mode hack a little clearer --------- Co-authored-by: Stanislaw Wilczynski <[email protected]>
1 parent a3380cd commit ea871bb

23 files changed

+566
-530
lines changed

.github/workflows/pr.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010

1111
strategy:
1212
matrix:
13-
node-version: [16.x]
13+
node-version: [18.x]
1414

1515
steps:
1616
- uses: actions/checkout@v2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "bump react",
4+
"packageName": "@nova/examples",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "bump react",
4+
"packageName": "@nova/react",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "bump react",
4+
"packageName": "@nova/react-test-utils",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

packages/examples/.storybook/main.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ const config: StorybookConfig = {
88
],
99
framework: {
1010
name: "@storybook/react-webpack5",
11-
options: {},
11+
options: {
12+
strictMode: true,
13+
},
1214
},
1315
webpackFinal: (config) => {
1416
return {

packages/examples/package.json

+7-6
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
"@nova/react-test-utils": "4.3.1",
2121
"@nova/types": "1.4.0",
2222
"graphql": "^15.5.0",
23-
"react": "^17.0.2",
24-
"react-dom": "^17.0.2"
23+
"react": "^18.3.1",
24+
"react-dom": "^18.3.1"
2525
},
2626
"devDependencies": {
2727
"@babel/core": "^7.20.2",
@@ -42,11 +42,12 @@
4242
"@storybook/react": "^7.6.19",
4343
"@storybook/react-webpack5": "^7.6.19",
4444
"@storybook/test": "^7.6.19",
45-
"@testing-library/jest-dom": "^5.14.1",
46-
"@testing-library/react": "^12.0.0",
45+
"@storybook/types": "^7.6.19",
46+
"@testing-library/jest-dom": "^6.1.3",
47+
"@testing-library/react": "^15.0.0",
4748
"@types/jest": "^29.2.0",
48-
"@types/react": "^17.0.2",
49-
"@types/react-dom": "^17.0.11",
49+
"@types/react": "^18.3.1",
50+
"@types/react-dom": "^18.3.0",
5051
"esbuild-loader": "^3.0.1",
5152
"monorepo-scripts": "*",
5253
"prop-types": "15.8.1",

packages/examples/src/Feedback/FeedbackContainer.stories.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,10 @@ export const LikeFailure: Story = {
7777
graphql: { mock },
7878
} = getNovaEnvironmentForStory(context);
7979

80-
// wait for next tick for apollo client to update state
81-
await new Promise((resolve) => setTimeout(resolve, 0));
80+
await waitFor(async () => {
81+
const operation = mock.getMostRecentOperation();
82+
await expect(operation).toBeDefined();
83+
});
8284
await mock.resolveMostRecentOperation((operation) =>
8385
MockPayloadGenerator.generate(operation, {
8486
Feedback: () => sampleFeedback,

packages/examples/src/Feedback/FeedbackContainer.test.tsx

+15-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { composeStories } from "@storybook/react";
22
import * as stories from "./FeedbackContainer.stories";
3-
import { act, render, screen } from "@testing-library/react";
3+
import { render, screen, waitFor } from "@testing-library/react";
44
import * as React from "react";
55
import "@testing-library/jest-dom";
66
import { prepareStoryContextForTest } from "@nova/react-test-utils";
7+
import { executePlayFunction } from "../testing-utils/executePlayFunction";
78

89
const { AutoGeneratedDataOnly, Primary, Liked, Like, LikeFailure, Loading } =
910
composeStories(stories);
@@ -29,17 +30,16 @@ describe("FeedbackContainer", () => {
2930

3031
it("should show unlike button after clicking like button", async () => {
3132
const { container } = render(<Like />);
32-
await act(async () => Like.play({ canvasElement: container }));
33+
await executePlayFunction(Like, { canvasElement: container });
3334
const button = await screen.findByRole("button", { name: "Unlike" });
3435
expect(button).toBeInTheDocument();
3536
});
3637

3738
it("should show an error if the like button fails", async () => {
3839
const { container } = render(<LikeFailure />);
39-
// This needs to be wrapped in act as play function for this story
40-
// relies on mock client to resolve queries which updates component state
41-
await act(async () =>
42-
LikeFailure.play(prepareStoryContextForTest(LikeFailure, container)),
40+
await executePlayFunction(
41+
LikeFailure,
42+
prepareStoryContextForTest(LikeFailure, container),
4343
);
4444
const error = await screen.findByText("Something went wrong");
4545
expect(error).toBeInTheDocument();
@@ -55,8 +55,14 @@ describe("FeedbackContainer", () => {
5555
render(<Primary />);
5656
render(<Liked />);
5757
render(<Primary />);
58-
const texts = await screen.findAllByText("Feedback: Feedback title");
59-
expect(texts).toHaveLength(3);
60-
expect(texts[2]).toBeInTheDocument();
58+
const text = "Feedback: Feedback title";
59+
await waitFor(
60+
() => {
61+
const texts = screen.getAllByText(text);
62+
expect(texts).toHaveLength(3);
63+
},
64+
{ timeout: 3000 },
65+
);
66+
expect(screen.getAllByText(text)[2]).toBeInTheDocument();
6167
});
6268
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { ReactRenderer } from "@storybook/react";
2+
import type {
3+
ComposedStoryPlayContext,
4+
ComposedStoryFn,
5+
} from "@storybook/types";
6+
import { act, waitFor } from "@testing-library/react";
7+
8+
type PlayFunctionThatReturnsPromise = (
9+
options: ComposedStoryPlayContext<ReactRenderer>,
10+
) => Promise<void>;
11+
12+
// This function is a workaround for the issues with React 18 that if someone
13+
// tries to call `play` function from act like:
14+
// await act(async () => {
15+
// await Story.play({ canvasElement: container });
16+
// });
17+
// it will fail by never rerendering the component causing any assertions in play
18+
// function to fail. To mitigate that we mimic awaiting by adding a flag that will
19+
// be set to true once the play function is completed and we wait for that flag to
20+
// be true.
21+
export const executePlayFunction = async (
22+
Story: ComposedStoryFn<ReactRenderer>,
23+
options: ComposedStoryPlayContext<ReactRenderer>,
24+
) => {
25+
let playFunctionCompleted = false;
26+
const playFunction = Story.play as PlayFunctionThatReturnsPromise;
27+
act(() => {
28+
playFunction(options)
29+
.then(() => {
30+
playFunctionCompleted = true;
31+
})
32+
.catch((error) => {
33+
throw error;
34+
});
35+
});
36+
await waitFor(() => {
37+
expect(playFunctionCompleted).toBe(true);
38+
});
39+
};

packages/nova-react-test-utils/package.json

+6-6
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
},
1313
"peerDependencies": {
1414
"@storybook/react": "^7.6",
15-
"react": "^17.0.2"
15+
"react": "^17.0.2 || ^18"
1616
},
1717
"dependencies": {
1818
"@apollo/client": "^3.4.15",
@@ -34,13 +34,13 @@
3434
"@graphitation/apollo-react-relay-duct-tape-compiler": "^1.4.0",
3535
"@storybook/react": "^7.6.19",
3636
"@storybook/types": "^7.6.19",
37-
"@testing-library/react": "^12.0.0",
37+
"@testing-library/react": "^15.0.0",
3838
"@types/jest": "^29.2.0",
39-
"@types/react": "^17.0.2",
40-
"@types/react-test-renderer": "^17.0.1",
39+
"@types/react": "^18.3.1",
40+
"@types/react-test-renderer": "^18.3.0",
4141
"monorepo-scripts": "*",
42-
"react": "^17.0.2",
43-
"react-test-renderer": "^17.0.2"
42+
"react": "^18.3.1",
43+
"react-test-renderer": "^18.3.1"
4444
},
4545
"repository": {
4646
"type": "git",

packages/nova-react-test-utils/src/nova-mock-environment.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from "react";
2-
import type { ComponentType } from "react";
2+
import type { ComponentType, PropsWithChildren } from "react";
33
import type {
44
NovaCentralizedCommanding,
55
NovaGraphQL,
@@ -33,7 +33,7 @@ export interface NovaMockEnvironment<T extends Environment = "test"> {
3333
* A React component that will be used to wrap the NovaFacadeProvider children. This is used by the test-utils to
3434
* inject a ApolloProvider.
3535
*/
36-
providerWrapper: ComponentType;
36+
providerWrapper: ComponentType<PropsWithChildren>;
3737
}
3838

3939
interface NovaMockEnvironmentProviderProps<T extends Environment> {

packages/nova-react-test-utils/src/storybook-nova-decorator.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export const getNovaEnvironmentDecorator: (
109109
context.parameters[NAME_OF_ASSIGNED_PARAMETER_IN_DECORATOR] = environment;
110110
return (
111111
<NovaMockEnvironmentProvider environment={environment}>
112-
{getStory(context)}
112+
{getStory(context) as React.ReactNode}
113113
</NovaMockEnvironmentProvider>
114114
);
115115
},

packages/nova-react/package.json

+5-6
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,14 @@
2222
},
2323
"devDependencies": {
2424
"@graphitation/graphql-js-tag": "^0.9.0",
25-
"@testing-library/react": "^12.0.0",
26-
"@testing-library/react-hooks": "^8.0.1",
25+
"@testing-library/react": "^15.0.0",
2726
"@types/invariant": "^2.2.35",
2827
"@types/jest": "^29.2.0",
29-
"@types/react": "^17.0.2",
30-
"@types/react-dom": "^17.0.11",
28+
"@types/react": "^18.3.1",
29+
"@types/react-dom": "^18.3.0",
3130
"monorepo-scripts": "*",
32-
"react": "^17.0.2",
33-
"react-dom": "^17.0.2"
31+
"react": "^18.3.1",
32+
"react-dom": "^18.3.1"
3433
},
3534
"repository": {
3635
"type": "git",

packages/nova-react/src/commanding/nova-centralized-commanding-provider.test.tsx

+24-21
Original file line numberDiff line numberDiff line change
@@ -21,40 +21,41 @@ describe(useNovaCentralizedCommanding, () => {
2121
expect.assertions(1);
2222

2323
const TestUndefinedContextComponent: React.FC = () => {
24-
try {
25-
useNovaCentralizedCommanding();
26-
} catch (e) {
27-
expect((e as Error).message).toMatch(
28-
"Nova Centralized Commanding provider must be initialized prior to consumption!",
29-
);
30-
}
24+
useNovaCentralizedCommanding();
3125
return null;
3226
};
3327

34-
render(<TestUndefinedContextComponent />);
28+
expect(() => render(<TestUndefinedContextComponent />)).toThrow(
29+
"Nova Centralized Commanding provider must be initialized prior to consumption!",
30+
);
3531
});
3632

3733
it("is able to access the commanding instance provided by the provider", () => {
38-
expect.assertions(2);
34+
expect.assertions(1);
3935

4036
const commanding = {
4137
trigger: jest.fn(),
4238
} as unknown as NovaCentralizedCommanding;
4339

4440
const TestPassedContextComponent: React.FC = () => {
4541
const facadeFromContext = useNovaCentralizedCommanding();
46-
expect(facadeFromContext).toBe(commanding);
47-
facadeFromContext.trigger({
48-
entity: {
49-
type: EntityType.teams_activity,
50-
action: EntityAction.default,
51-
},
52-
command: {
53-
stateTransition: EntityStateTransition.new,
54-
visibilityState: EntityVisibilityState.show,
55-
},
56-
});
57-
expect(commanding.trigger).toBeCalledTimes(1);
42+
const didTrigger = React.useRef(false);
43+
React.useEffect(() => {
44+
if (didTrigger.current) {
45+
return;
46+
}
47+
facadeFromContext.trigger({
48+
entity: {
49+
type: EntityType.teams_activity,
50+
action: EntityAction.default,
51+
},
52+
command: {
53+
stateTransition: EntityStateTransition.new,
54+
visibilityState: EntityVisibilityState.show,
55+
},
56+
});
57+
didTrigger.current = true;
58+
}, []);
5859
return null;
5960
};
6061

@@ -63,5 +64,7 @@ describe(useNovaCentralizedCommanding, () => {
6364
<TestPassedContextComponent />
6465
</NovaCentralizedCommandingProvider>,
6566
);
67+
68+
expect(commanding.trigger).toBeCalledTimes(1);
6669
});
6770
});

0 commit comments

Comments
 (0)