diff --git a/packages/examples/.storybook/main.ts b/packages/examples/.storybook/main.ts index f222173..6b42db4 100644 --- a/packages/examples/.storybook/main.ts +++ b/packages/examples/.storybook/main.ts @@ -8,7 +8,9 @@ const config: StorybookConfig = { ], framework: { name: "@storybook/react-webpack5", - options: {}, + options: { + strictMode: true, + }, }, webpackFinal: (config) => { return { diff --git a/packages/examples/package.json b/packages/examples/package.json index f073d53..7db8da8 100644 --- a/packages/examples/package.json +++ b/packages/examples/package.json @@ -43,7 +43,7 @@ "@storybook/react-webpack5": "^7.6.19", "@storybook/test": "^7.6.19", "@storybook/types": "^7.6.19", - "@testing-library/jest-dom": "^5.14.1", + "@testing-library/jest-dom": "^6.1.3", "@testing-library/react": "^15.0.0", "@types/jest": "^29.2.0", "@types/react": "^18.3.1", diff --git a/packages/examples/src/Feedback/FeedbackContainer.stories.ts b/packages/examples/src/Feedback/FeedbackContainer.stories.ts index 39b0e84..5fb6a42 100644 --- a/packages/examples/src/Feedback/FeedbackContainer.stories.ts +++ b/packages/examples/src/Feedback/FeedbackContainer.stories.ts @@ -77,8 +77,10 @@ export const LikeFailure: Story = { graphql: { mock }, } = getNovaEnvironmentForStory(context); - // wait for next tick for apollo client to update state - await new Promise((resolve) => setTimeout(resolve, 0)); + await waitFor(async () => { + const operation = mock.getMostRecentOperation(); + await expect(operation).toBeDefined(); + }); await mock.resolveMostRecentOperation((operation) => MockPayloadGenerator.generate(operation, { Feedback: () => sampleFeedback, diff --git a/packages/nova-react/src/commanding/nova-centralized-commanding-provider.test.tsx b/packages/nova-react/src/commanding/nova-centralized-commanding-provider.test.tsx index ba7cccd..c50cdc9 100644 --- a/packages/nova-react/src/commanding/nova-centralized-commanding-provider.test.tsx +++ b/packages/nova-react/src/commanding/nova-centralized-commanding-provider.test.tsx @@ -21,21 +21,17 @@ describe(useNovaCentralizedCommanding, () => { expect.assertions(1); const TestUndefinedContextComponent: React.FC = () => { - try { - useNovaCentralizedCommanding(); - } catch (e) { - expect((e as Error).message).toMatch( - "Nova Centralized Commanding provider must be initialized prior to consumption!", - ); - } + useNovaCentralizedCommanding(); return null; }; - render(); + expect(() => render()).toThrow( + "Nova Centralized Commanding provider must be initialized prior to consumption!", + ); }); it("is able to access the commanding instance provided by the provider", () => { - expect.assertions(2); + expect.assertions(1); const commanding = { trigger: jest.fn(), @@ -43,18 +39,23 @@ describe(useNovaCentralizedCommanding, () => { const TestPassedContextComponent: React.FC = () => { const facadeFromContext = useNovaCentralizedCommanding(); - expect(facadeFromContext).toBe(commanding); - facadeFromContext.trigger({ - entity: { - type: EntityType.teams_activity, - action: EntityAction.default, - }, - command: { - stateTransition: EntityStateTransition.new, - visibilityState: EntityVisibilityState.show, - }, - }); - expect(commanding.trigger).toBeCalledTimes(1); + const didTrigger = React.useRef(false); + React.useEffect(() => { + if (didTrigger.current) { + return; + } + facadeFromContext.trigger({ + entity: { + type: EntityType.teams_activity, + action: EntityAction.default, + }, + command: { + stateTransition: EntityStateTransition.new, + visibilityState: EntityVisibilityState.show, + }, + }); + didTrigger.current = true; + }, []); return null; }; @@ -63,5 +64,7 @@ describe(useNovaCentralizedCommanding, () => { , ); + + expect(commanding.trigger).toBeCalledTimes(1); }); }); diff --git a/packages/nova-react/src/eventing/nova-eventing-provider.test.tsx b/packages/nova-react/src/eventing/nova-eventing-provider.test.tsx index a2d34ad..29341eb 100644 --- a/packages/nova-react/src/eventing/nova-eventing-provider.test.tsx +++ b/packages/nova-react/src/eventing/nova-eventing-provider.test.tsx @@ -71,34 +71,26 @@ describe("useNovaEventing", () => { expect.assertions(1); const TestUndefinedContextComponent: React.FC = () => { - try { - useNovaEventing(); - } catch (e) { - expect((e as Error).message).toMatch( - "Nova Eventing provider must be initialized prior to consumption of eventing!", - ); - } + useNovaEventing(); return null; }; - render(); + expect(() => render()).toThrow( + "Nova Eventing provider must be initialized prior to consumption of eventing!", + ); }); it("useNovaUnmountEventing throws without a provider", () => { expect.assertions(1); const TestUndefinedContextComponent: React.FC = () => { - try { - useNovaUnmountEventing(); - } catch (e) { - expect((e as Error).message).toMatch( - "Nova Eventing provider must be initialized prior to consumption of unmountEventing!", - ); - } + useNovaUnmountEventing(); return null; }; - render(); + expect(() => render()).toThrow( + "Nova Eventing provider must be initialized prior to consumption of unmountEventing!", + ); }); test("Takes in children and eventing props, renders children, and updates children as expected.", () => { @@ -113,7 +105,8 @@ describe("useNovaEventing", () => { initialChildren, ); - expect(renderSpy).toHaveBeenCalledTimes(1); + // called twice on each render due to strict mode + expect(renderSpy).toHaveBeenCalledTimes(2); wrapper.rerender( { expect(wrapper.queryAllByTestId("children")[0].innerHTML).toBe( updatedChildren, ); - expect(renderSpy).toHaveBeenCalledTimes(2); + // called twice on each render due to strict mode + expect(renderSpy).toHaveBeenCalledTimes(4); }); test("Takes in children and eventing props, creates a stable wrapped NovaReactEventing instance from eventing across re-renders when children do not change.", () => { @@ -143,7 +137,8 @@ describe("useNovaEventing", () => { expect(wrapper.queryAllByTestId("children")[0].innerHTML).toBe( initialChildren, ); - expect(renderSpy).toHaveBeenCalledTimes(1); + // called twice on each render due to strict mode + expect(renderSpy).toHaveBeenCalledTimes(2); wrapper.rerender( { expect(wrapper.queryAllByTestId("children")[0].innerHTML).toBe( initialChildren, ); - expect(renderSpy).toHaveBeenCalledTimes(1); + expect(renderSpy).toHaveBeenCalledTimes(2); // Update eventing instance to test useRef pathway. This will ensure the wrapped eventing instance // returned from useEventing is stable from one render to the next. @@ -172,7 +167,7 @@ describe("useNovaEventing", () => { expect(wrapper.queryAllByTestId("children")[0].innerHTML).toBe( initialChildren, ); - expect(renderSpy).toHaveBeenCalledTimes(1); + expect(renderSpy).toHaveBeenCalledTimes(2); //Trigger a callback on the test child through eventing eventCallback(); @@ -195,7 +190,7 @@ describe("useNovaEventing", () => { expect(wrapper.queryAllByTestId("children")[0].innerHTML).toBe( initialChildren, ); - expect(renderSpy).toHaveBeenCalledTimes(1); + expect(renderSpy).toHaveBeenCalledTimes(2); //Trigger a callback on the test child through eventing eventCallback(); @@ -230,17 +225,23 @@ describe("NovaReactEventing exposes 'generateEvent'", () => { event, }; } + const didGenerate = React.useRef(false); + React.useEffect(() => { + if (didGenerate.current) { + return; + } + facadeFromContext.generateEvent(eventWrapper); + expect(eventing.bubble).toBeCalledWith({ + event, + source: { + inputType: InputType.programmatic, + timeStamp: expectedTime, + }, + }); + expect(mapper).toBeCalledTimes(0); + didGenerate.current = true; + }, []); - facadeFromContext.generateEvent(eventWrapper); - - expect(eventing.bubble).toBeCalledWith({ - event, - source: { - inputType: InputType.programmatic, - timeStamp: expectedTime, - }, - }); - expect(mapper).toBeCalledTimes(0); return null; }; diff --git a/packages/nova-react/src/graphql/nova-graphql-provider.test.tsx b/packages/nova-react/src/graphql/nova-graphql-provider.test.tsx index 8a2bd16..fc413e6 100644 --- a/packages/nova-react/src/graphql/nova-graphql-provider.test.tsx +++ b/packages/nova-react/src/graphql/nova-graphql-provider.test.tsx @@ -12,21 +12,17 @@ describe(useNovaGraphQL, () => { expect.assertions(1); const TestUndefinedContextComponent: React.FC = () => { - try { - useNovaGraphQL(); - } catch (e) { - expect((e as Error).message).toMatch( - "Nova GraphQL provider must be initialized prior to consumption!", - ); - } + useNovaGraphQL(); return null; }; - render(); + expect(() => render()).toThrow( + "Nova GraphQL provider must be initialized prior to consumption!", + ); }); it("is able to access the GraphQL instance provided by the provider", () => { - expect.assertions(3); + expect.assertions(1); const graphql = { useLazyLoadQuery: jest.fn(), @@ -34,14 +30,11 @@ describe(useNovaGraphQL, () => { const TestPassedContextComponent: React.FC = () => { const graphqlFromContext = useNovaGraphQL(); - expect(graphqlFromContext).toBe(graphql); - expect(graphqlFromContext.useLazyLoadQuery).toBeDefined(); // TODO figure out if this is needed if (!graphqlFromContext.useLazyLoadQuery) { return null; } graphqlFromContext.useLazyLoadQuery("foo", {}); - expect(graphql.useLazyLoadQuery).toBeCalledTimes(1); return null; }; @@ -50,5 +43,8 @@ describe(useNovaGraphQL, () => { , ); + + // twice due to strict mode + expect(graphql.useLazyLoadQuery).toBeCalledTimes(2); }); }); diff --git a/scripts/config/jest.config.js b/scripts/config/jest.config.ts similarity index 59% rename from scripts/config/jest.config.js rename to scripts/config/jest.config.ts index bff3566..5a2baee 100644 --- a/scripts/config/jest.config.js +++ b/scripts/config/jest.config.ts @@ -1,4 +1,6 @@ -module.exports = { +import path from "path"; + +export default { preset: "ts-jest", rootDir: process.cwd(), roots: ["/src"], @@ -7,4 +9,6 @@ module.exports = { transform: { "\\.(gql|graphql)$": "@graphql-tools/jest-transform", }, + setupFiles: [path.join(__dirname, "jest.setup.ts")], + setupFilesAfterEnv: [path.join(__dirname, "jest.setupAfterEnv.ts")], }; diff --git a/scripts/config/jest.setup.ts b/scripts/config/jest.setup.ts new file mode 100644 index 0000000..2e6c1b5 --- /dev/null +++ b/scripts/config/jest.setup.ts @@ -0,0 +1,6 @@ +import { configure } from "@testing-library/react"; + +configure({ + reactStrictMode: true, +}); + diff --git a/scripts/config/jest.setupAfterEnv.ts b/scripts/config/jest.setupAfterEnv.ts new file mode 100644 index 0000000..19c15d0 --- /dev/null +++ b/scripts/config/jest.setupAfterEnv.ts @@ -0,0 +1,6 @@ +import { cleanup } from "@testing-library/react"; + +afterEach(() => { + // For some reason needed with strict mode enabled to cleanup DOM after each test + cleanup(); +}); diff --git a/scripts/just.config.ts b/scripts/just.config.ts index 42400b5..8dfd7b1 100644 --- a/scripts/just.config.ts +++ b/scripts/just.config.ts @@ -67,7 +67,7 @@ export const build = () => { export const test = () => { return jestTask({ - config: path.join(__dirname, "config", "jest.config.js"), + config: path.join(__dirname, "config", "jest.config.ts"), watch: argv().watch, _: argv()._, }); diff --git a/scripts/package.json b/scripts/package.json index 656287a..a466c52 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -9,6 +9,7 @@ }, "devDependencies": { "@graphql-tools/jest-transform": "^1.2.2", + "@testing-library/react": "^15.0.0", "@types/node": "^16.11.6", "@typescript-eslint/eslint-plugin": "^5.50.0", "@typescript-eslint/parser": "^5.50.0", diff --git a/yarn.lock b/yarn.lock index 1b3438b..b99e582 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -"@adobe/css-tools@^4.0.1", "@adobe/css-tools@^4.3.2": +"@adobe/css-tools@^4.3.2": version "4.3.3" resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.3.tgz#90749bde8b89cd41764224f5aac29cd4138f75ff" integrity sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ== @@ -3488,21 +3488,6 @@ lz-string "^1.5.0" pretty-format "^27.0.2" -"@testing-library/jest-dom@^5.14.1": - version "5.16.5" - resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz#3912846af19a29b2dbf32a6ae9c31ef52580074e" - integrity sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA== - dependencies: - "@adobe/css-tools" "^4.0.1" - "@babel/runtime" "^7.9.2" - "@types/testing-library__jest-dom" "^5.9.1" - aria-query "^5.0.0" - chalk "^3.0.0" - css.escape "^1.5.1" - dom-accessibility-api "^0.5.6" - lodash "^4.17.15" - redent "^3.0.0" - "@testing-library/jest-dom@^6.1.3": version "6.4.5" resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.4.5.tgz#badb40296477149136dabef32b572ddd3b56adf1" @@ -3737,7 +3722,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@*", "@types/jest@^29.2.0": +"@types/jest@^29.2.0": version "29.5.0" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.0.tgz#337b90bbcfe42158f39c2fb5619ad044bbb518ac" integrity sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg== @@ -3894,13 +3879,6 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== -"@types/testing-library__jest-dom@^5.9.1": - version "5.14.5" - resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz#d113709c90b3c75fdb127ec338dad7d5f86c974f" - integrity sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ== - dependencies: - "@types/jest" "*" - "@types/tough-cookie@*": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" @@ -5797,7 +5775,7 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9: +dom-accessibility-api@^0.5.9: version "0.5.16" resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==