From 6a2d4fdc261ebbb2a85d947c5807209cc04d4509 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:03:28 +0000 Subject: [PATCH] test: add tests for usePending React hook Co-authored-by: johnstrand <11484777+johnstrand@users.noreply.github.com> --- package-lock.json | 46 +++++++++ package.json | 2 + src/__tests__/Squawk.usePending.test.ts | 120 ++++++++++++++++++++++++ 3 files changed, 168 insertions(+) create mode 100644 src/__tests__/Squawk.usePending.test.ts diff --git a/package-lock.json b/package-lock.json index ba1489d..2c7d434 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@types/jest": "^26.0.24", "@types/node": "^14.14.31", "@types/react": "^16.8.10", + "@types/react-test-renderer": "^16.8.3", "@typescript-eslint/eslint-plugin": "3.6.1", "@typescript-eslint/parser": "3.6.1", "eslint": "7.32.0", @@ -29,6 +30,7 @@ "jest": "^26.6.3", "prettier": "2.0.5", "react": "^16.8.6", + "react-test-renderer": "^16.8.6", "ts-jest": "^26.5.6", "typescript": "^3.9.2" }, @@ -1344,6 +1346,16 @@ "csstype": "^3.2.2" } }, + "node_modules/@types/react-test-renderer": { + "version": "16.8.3", + "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-16.8.3.tgz", + "integrity": "sha512-vEEYh6gFk3jkPHqRe7N5CxWA+yb92hCZxNyhq/1Wj3mClqVWLJYakJDMp3iFmntCgEq96m68N9Oad3wOHm+pdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/scheduler": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", @@ -7211,6 +7223,29 @@ "dev": true, "license": "MIT" }, + "node_modules/react-test-renderer": { + "version": "16.8.6", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.8.6.tgz", + "integrity": "sha512-H2srzU5IWYT6cZXof6AhUcx/wEyJddQ8l7cLM/F7gDXYyPr4oq+vCIxJYXVGhId1J706sqziAjuOEjyNkfgoEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "react-is": "^16.8.6", + "scheduler": "^0.13.6" + }, + "peerDependencies": { + "react": "^16.0.0" + } + }, + "node_modules/react-test-renderer/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/read-pkg": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", @@ -7956,6 +7991,17 @@ "node": ">=10" } }, + "node_modules/scheduler": { + "version": "0.13.6", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz", + "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", diff --git a/package.json b/package.json index fffca5d..3627470 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@types/jest": "^26.0.24", "@types/node": "^14.14.31", "@types/react": "^16.8.10", + "@types/react-test-renderer": "^16.8.3", "@typescript-eslint/eslint-plugin": "3.6.1", "@typescript-eslint/parser": "3.6.1", "eslint": "7.32.0", @@ -48,6 +49,7 @@ "jest": "^26.6.3", "prettier": "2.0.5", "react": "^16.8.6", + "react-test-renderer": "^16.8.6", "ts-jest": "^26.5.6", "typescript": "^3.9.2" } diff --git a/src/__tests__/Squawk.usePending.test.ts b/src/__tests__/Squawk.usePending.test.ts new file mode 100644 index 0000000..fd9b96e --- /dev/null +++ b/src/__tests__/Squawk.usePending.test.ts @@ -0,0 +1,120 @@ +import * as React from "react"; +import * as renderer from "react-test-renderer"; + +import createStore from "../Squawk"; + +const { act } = renderer; + +// React 16.8 react-test-renderer act doesn't support async, so we use a standard trick. +const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0)); + +describe("usePending", () => { + it("tracks pending state correctly", async () => { + const store = createStore({ foo: "bar", baz: 123 }); + + // eslint-disable-next-line immutable/no-let, @typescript-eslint/no-explicit-any + let currentPending: any; + + const TestComponent = () => { + currentPending = store.usePending(); + // Access foo to track it + return React.createElement("div", null, currentPending.foo ? "loading" : "done"); + }; + + act(() => { + renderer.create(React.createElement(TestComponent)); + }); + + // Initially not pending + expect(currentPending.foo).toBe(false); + expect(currentPending.baz).toBe(false); + + // Make an action to trigger pending state + // We do not await inside the action as we control the resolution + // eslint-disable-next-line immutable/no-let, @typescript-eslint/no-explicit-any + let resolveAction: any; + const action = store.action(async () => { + return new Promise((resolve) => { + resolveAction = resolve; + }); + }, ["foo"]); + + // Start action + // eslint-disable-next-line immutable/no-let, @typescript-eslint/no-explicit-any + let promise: Promise; + act(() => { + promise = action(); + }); + + // After action start, should be pending + expect(currentPending.foo).toBe(true); + + act(() => { + resolveAction(); + }); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await promise!; + await flushPromises(); + + // The promise resolves outside of act, so state updates in Squawk fire outside of act. + // In test-renderer, we need to artificially flush these to not get a warning and to see the updated state. + act(() => { + // We do not return anything to avoid the act warning. + // The act itself triggers the react update batching. + }); + + // After action completes, should no longer be pending + expect(currentPending.foo).toBe(false); + }); + + it("includes explicit contexts in the tracked list", async () => { + const store = createStore({ foo: "bar", baz: 123 }); + + // eslint-disable-next-line immutable/no-let, @typescript-eslint/no-explicit-any + let currentPending: any; + + const TestComponent = () => { + currentPending = store.usePending("baz"); + return React.createElement("div", null, currentPending.baz ? "loading baz" : "done baz"); + }; + + act(() => { + renderer.create(React.createElement(TestComponent)); + }); + + expect(currentPending.baz).toBe(false); + + // eslint-disable-next-line immutable/no-let, @typescript-eslint/no-explicit-any + let resolveAction: any; + const action = store.action(async () => { + return new Promise((resolve) => { + resolveAction = resolve; + }); + }, ["baz"]); + + // eslint-disable-next-line immutable/no-let, @typescript-eslint/no-explicit-any + let promise: Promise; + act(() => { + promise = action(); + }); + + expect(currentPending.baz).toBe(true); + // foo was not requested or accessed + expect(currentPending.foo).toBe(false); + + act(() => { + resolveAction(); + }); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await promise!; + await flushPromises(); + + act(() => { + // Flush effects + }); + + expect(currentPending.baz).toBe(false); + }); +});