From 4c5609bb5b090f87d8a1137005ebee32da409874 Mon Sep 17 00:00:00 2001 From: Tyler Dixon Date: Tue, 11 Aug 2026 14:12:42 -0700 Subject: [PATCH] ci: run the test suite against React 19 (#789) CI type-checks React 19 but has never executed a test against it: the react matrix exists only on the type-check job, and the test job runs the lockfile's React 18. This adds react 18/19 as a test-job dimension, so the suite runs on 19 before any src/ change lands for the React 19 foundation. @testing-library/react moves 14 -> 16, with @testing-library/dom as a new direct dependency. This is forced rather than opportunistic: RTL 14 declares React ^18 only, so the 19 leg cannot run without it. The lockfile shrinks because RTL 14's nested @testing-library/dom collapses into the top-level install; no packages are added. The React 18 legs keep their existing job names, so main's required status checks keep matching and no admin change is needed. Only the 19 legs get a suffix, which also means a React 19 failure does not gate a merge while 19 support is still being built. A guard step after the artifact overlay asserts the installed React matches the matrix leg. Without it the dimension is decorative: the only behavioural difference between the legs is one skipped test, so a 19 leg that silently ran 18 would pass. That one test is skipped on 19 only, tracked in #793: in suspense mode, swapping the observable resumes with the previous observable's value on 19.2.8 while passing on 18.2.0, deterministic across three runs each. Emulator jobs go from two to four, which doubles per-PR exposure to the #776 flake. Refs #789 --- .github/workflows/test.yaml | 33 +++++- package-lock.json | 223 +++++------------------------------- package.json | 3 +- test/database.test.tsx | 2 +- test/useObservable.test.tsx | 9 +- 5 files changed, 71 insertions(+), 199 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d4158b35..4cc1c150 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -52,8 +52,14 @@ jobs: strategy: matrix: node: ["22", "24"] + react: ["18", "19"] fail-fast: false - name: Test Node.js ${{ matrix.node }} (Ubuntu) + # The React 18 legs keep the original job names on purpose: `main`'s required + # status checks reference them, so renaming would block every PR there until + # an admin updated the contexts. Only the non-18 legs get a suffix, which + # also means a React 19 failure does not gate a merge while 19 support is + # still being built. Add the contexts when we want it to. + name: Test Node.js ${{ matrix.node }}${{ matrix.react != '18' && format(' / React {0}', matrix.react) || '' }} (Ubuntu) steps: - name: Checkout uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 @@ -67,6 +73,14 @@ jobs: cache: 'npm' - name: Install deps run: npm ci + # Only the 19 leg swaps the runtime; the 18 leg runs the lockfile's React + # as-is. --no-save keeps the lockfile out of the diff. Note this differs + # from the type-check job, which installs on both legs and also installs + # the types, so the 18 legs of the two jobs cover different React 18 + # patch versions. + - name: Install React ${{ matrix.react }} + if: matrix.react == '19' + run: npm install --no-save react@${{ matrix.react }} react-dom@${{ matrix.react }} - name: Setup Java uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0 with: @@ -86,6 +100,23 @@ jobs: run: | chmod +x reactfire-${{ github.run_id }}/unpack.sh ./reactfire-${{ github.run_id }}/unpack.sh + # Without this the matrix is decorative: the only behavioural difference + # between the legs is one skipped test, so a 19 leg silently running 18 + # would pass. Runs after the artifact overlay so it covers everything + # that could disturb the install. + - name: Confirm React ${{ matrix.react }} is what will run + env: + WANT_REACT: ${{ matrix.react }} + run: | + node -e ' + const want = process.env.WANT_REACT; + const got = require("react/package.json").version; + if (!got.startsWith(want + ".")) { + console.error(`expected React ${want}, got ${got}`); + process.exit(1); + } + console.log(`React ${got} confirmed for matrix leg ${want}`); + ' - name: Run tests run: npm run test type-check: diff --git a/package-lock.json b/package-lock.json index 5e7c64cb..44d209c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,9 @@ "devDependencies": { "@rollup/plugin-typescript": "^12.3.0", "@size-limit/preset-small-lib": "^8.2.6", + "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^14.0.0", + "@testing-library/react": "^16.3.2", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@types/use-sync-external-store": "^0.0.3", @@ -3564,47 +3565,23 @@ "license": "MIT" }, "node_modules/@testing-library/dom": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz", - "integrity": "sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", - "chalk": "^4.1.0", + "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", + "picocolors": "1.1.1", "pretty-format": "^27.0.2" }, "engines": { - "node": ">=14" - } - }, - "node_modules/@testing-library/dom/node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "dev": true, - "dependencies": { - "deep-equal": "^2.0.5" - } - }, - "node_modules/@testing-library/dom/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=18" } }, "node_modules/@testing-library/jest-dom": { @@ -3630,21 +3607,31 @@ } }, "node_modules/@testing-library/react": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.0.0.tgz", - "integrity": "sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg==", + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^9.0.0", - "@types/react-dom": "^18.0.0" + "@babel/runtime": "^7.12.5" }, "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, "node_modules/@tootallnate/once": { @@ -6213,35 +6200,6 @@ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", "dev": true }, - "node_modules/deep-equal": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.1.tgz", - "integrity": "sha512-lKdkdV6EOGoVn65XaOsPdH4rMxTZOnmFyuIkMjM1i5HHCbfjC97dawgTAy0deYNfuqUqW+Q5VrVaQYtUpSd6yQ==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.0", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.0", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -6636,26 +6594,6 @@ "node": ">= 0.4" } }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/es-module-lexer": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", @@ -9388,22 +9326,6 @@ "node": ">= 0.10" } }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -9609,15 +9531,6 @@ "node": ">=8" } }, - "node_modules/is-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -9730,15 +9643,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-set": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-shared-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", @@ -9845,15 +9749,6 @@ "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", "dev": true }, - "node_modules/is-weakmap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -9866,19 +9761,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-weakset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", - "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-wsl": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", @@ -9909,12 +9791,6 @@ "node": ">=v0.10.0" } }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -11980,22 +11856,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -14333,18 +14193,6 @@ "dev": true, "license": "MIT" }, - "node_modules/stop-iteration-iterator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", - "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "dev": true, - "dependencies": { - "internal-slot": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/stream-chain": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", @@ -15990,21 +15838,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/which-collection": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", - "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", - "dev": true, - "dependencies": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/which-typed-array": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", diff --git a/package.json b/package.json index 6b6b707f..f10a7f34 100644 --- a/package.json +++ b/package.json @@ -73,8 +73,9 @@ "devDependencies": { "@rollup/plugin-typescript": "^12.3.0", "@size-limit/preset-small-lib": "^8.2.6", + "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^14.0.0", + "@testing-library/react": "^16.3.2", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@types/use-sync-external-store": "^0.0.3", diff --git a/test/database.test.tsx b/test/database.test.tsx index b3d912a1..65f381fa 100644 --- a/test/database.test.tsx +++ b/test/database.test.tsx @@ -12,7 +12,7 @@ describe('Realtime Database (RTDB)', () => { const database = getDatabase(app); connectDatabaseEmulator(database, 'localhost', 9000); - const Provider: React.FunctionComponent<{ children: React.ReactElement }> = ({ children }) => ( + const Provider: React.FunctionComponent<{ children: React.ReactNode }> = ({ children }) => ( {children} diff --git a/test/useObservable.test.tsx b/test/useObservable.test.tsx index fd7250a9..43eeb369 100644 --- a/test/useObservable.test.tsx +++ b/test/useObservable.test.tsx @@ -326,7 +326,14 @@ describe('useObservable', () => { expect(comp2).toHaveTextContent(values[1]); }); - it(`emits the new observable's value if the observable is swapped out`, async () => { + // Skipped on React 19: the swap suspends correctly, then resumes with the + // PREVIOUS observable's value (expects 'James', renders 'Jeff'). Deterministic + // on 19.2.8 and passing on 18.2.0, three runs each. Cause not yet diagnosed. + // Tracked in #793; restore this to a plain `it` once that closes. + // `it.skipIf` is vitest-only and the globals here are typed by @types/jest, + // so the ternary is what type-checks. + const itUnlessReact19 = Number(React.version.split('.')[0]) >= 19 ? it.skip : it; + itUnlessReact19(`emits the new observable's value if the observable is swapped out`, async () => { const obs1$ = new Subject(); const obs2$ = new Subject();