Skip to content

Commit

Permalink
start testing with React 19 (#11883)
Browse files Browse the repository at this point in the history
* start testing with React 19

---------

Co-authored-by: phryneas <phryneas@users.noreply.github.com>
  • Loading branch information
phryneas and phryneas committed Jun 11, 2024
1 parent b0c2d42 commit 6ca5ef4
Show file tree
Hide file tree
Showing 13 changed files with 137 additions and 19 deletions.
16 changes: 12 additions & 4 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,20 @@ jobs:
Tests:
docker:
- image: cimg/node:22.2.0
parameters:
project:
type: string
steps:
- checkout
- run: npm run ci:precheck
- run: npm version
- run: npm ci
- run: if test "<< parameters.project >>" = "Core Tests"; then npm run test:memory; fi
- run:
name: Jest suite with coverage
command: npm run test:ci
command: npm run test:ci -- --selectProjects "<< parameters.project >>"
environment:
JEST_JUNIT_OUTPUT_FILE: "reports/junit/js-test-results.xml"
JEST_JUNIT_OUTPUT_FILE: "reports/junit/js-test-results-<< parameters.project >>.xml"
- store_test_results:
path: reports/junit
- store_artifacts:
Expand Down Expand Up @@ -124,7 +128,11 @@ workflows:
Build and Test:
jobs:
# - Filesize
- Tests
- Tests:
matrix:
parameters:
project:
["Core Tests", "ReactDOM 17", "ReactDOM 18", "ReactDOM 19"]
- Formatting
- Lint
- BuildTarball
Expand Down Expand Up @@ -165,7 +173,7 @@ workflows:
- "@types/react@16.8 @types/react-dom@16.8"
- "@types/react@17 @types/react-dom@17"
- "@types/react@18 @types/react-dom@18"
- "@types/react@npm:types-react@19.0.0-alpha.3 @types/react-dom@npm:types-react-dom@19.0.0-alpha.3"
- "@types/react@npm:types-react@19.0.0-rc.0 @types/react-dom@npm:types-react-dom@19.0.0-rc.0"
- "typescript@next"
security-scans:
jobs:
Expand Down
28 changes: 27 additions & 1 deletion config/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ const defaults = {
const ignoreTSFiles = ".ts$";
const ignoreTSXFiles = ".tsx$";

const react19TestFileIgnoreList = [
ignoreTSFiles,
// The HOCs and Render Prop Components have been deprecated since March 2020,
// and to test them we would need to rewrite a lot of our test suites.
// We will not support them any more for React 19.
// They will probably work, but we make no more guarantees.
"src/react/hoc/.*",
"src/react/components/.*",
];

const react17TestFileIgnoreList = [
ignoreTSFiles,
// We only support Suspense with React 18, so don't test suspense hooks with
Expand All @@ -49,6 +59,17 @@ const tsStandardConfig = {

// For both React (Jest) "projects", ignore core tests (.ts files) as they
// do not import React, to avoid running them twice.
const standardReact19Config = {
...defaults,
displayName: "ReactDOM 19",
testPathIgnorePatterns: react19TestFileIgnoreList,
moduleNameMapper: {
"^react$": "react-19",
"^react-dom$": "react-dom-19",
"^react-dom/(.*)$": "react-dom-19/$1",
},
};

const standardReact18Config = {
...defaults,
displayName: "ReactDOM 18",
Expand All @@ -69,5 +90,10 @@ const standardReact17Config = {
};

module.exports = {
projects: [tsStandardConfig, standardReact17Config, standardReact18Config],
projects: [
tsStandardConfig,
standardReact17Config,
standardReact18Config,
standardReact19Config,
],
};
31 changes: 31 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"inline-inherit-doc": "ts-node-script config/inlineInheritDoc.ts",
"test": "node --expose-gc ./node_modules/jest/bin/jest.js --config ./config/jest.config.js",
"test:debug": "node --inspect-brk node_modules/.bin/jest --config ./config/jest.config.js --runInBand --testTimeout 99999 --logHeapUsage",
"test:ci": "TEST_ENV=ci npm run test:coverage -- --logHeapUsage && npm run test:memory",
"test:ci": "TEST_ENV=ci npm run test:coverage -- --logHeapUsage",
"test:watch": "jest --config ./config/jest.config.js --watch",
"test:memory": "npm i && npm run build && cd scripts/memory && npm i && npm test",
"test:coverage": "npm run coverage -- --ci --runInBand --reporters=default --reporters=jest-junit",
Expand Down Expand Up @@ -162,8 +162,10 @@
"prettier": "3.1.1",
"react": "18.3.1",
"react-17": "npm:react@^17",
"react-19": "npm:react@19.0.0-rc-cc1ec60d0d-20240607",
"react-dom": "18.3.1",
"react-dom-17": "npm:react-dom@^17",
"react-dom-19": "npm:react-dom@19.0.0-rc-cc1ec60d0d-20240607",
"react-error-boundary": "4.0.13",
"recast": "0.23.6",
"resolve": "1.22.8",
Expand Down
2 changes: 1 addition & 1 deletion src/react/hooks/__tests__/useFragment.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
within,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { act } from "react-dom/test-utils";
import { act } from "@testing-library/react";

import { UseFragmentOptions, useFragment } from "../useFragment";
import { MockedProvider } from "../../../testing";
Expand Down
6 changes: 6 additions & 0 deletions src/react/hooks/__tests__/useLoadableQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ import {
useTrackRenders,
} from "../../../testing/internal";

const IS_REACT_19 = React.version.startsWith("19");

afterEach(() => {
jest.useRealTimers();
});
Expand Down Expand Up @@ -4594,6 +4596,8 @@ it('does not suspend deferred queries with partial data in the cache and using a
});

it("throws when calling loadQuery on first render", async () => {
// We don't provide this functionality with React 19 anymore since it requires internals access
if (IS_REACT_19) return;
using _consoleSpy = spyOnConsole("error");
const { query, mocks } = useSimpleQueryCase();

Expand All @@ -4613,6 +4617,8 @@ it("throws when calling loadQuery on first render", async () => {
});

it("throws when calling loadQuery on subsequent render", async () => {
// We don't provide this functionality with React 19 anymore since it requires internals access
if (React.version.startsWith("19")) return;
using _consoleSpy = spyOnConsole("error");
const { query, mocks } = useSimpleQueryCase();

Expand Down
2 changes: 1 addition & 1 deletion src/react/hooks/__tests__/useMutation.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect } from "react";
import { GraphQLError } from "graphql";
import gql from "graphql-tag";
import { act } from "react-dom/test-utils";
import { act } from "@testing-library/react";
import { render, waitFor, screen, renderHook } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import fetchMock from "fetch-mock";
Expand Down
34 changes: 31 additions & 3 deletions src/react/hooks/__tests__/useQuery.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { Fragment, ReactNode, useEffect, useRef, useState } from "react";
import { DocumentNode, GraphQLError } from "graphql";
import gql from "graphql-tag";
import { act } from "react-dom/test-utils";
import { act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { render, screen, waitFor, renderHook } from "@testing-library/react";
import {
Expand Down Expand Up @@ -35,6 +35,8 @@ import {
import { useApolloClient } from "../useApolloClient";
import { useLazyQuery } from "../useLazyQuery";

const IS_REACT_19 = React.version.startsWith("19");

describe("useQuery Hook", () => {
describe("General use", () => {
it("should handle a simple query", async () => {
Expand Down Expand Up @@ -1557,7 +1559,33 @@ describe("useQuery Hook", () => {

function checkObservableQueries(expectedLinkCount: number) {
const obsQueries = client.getObservableQueries("all");
expect(obsQueries.size).toBe(2);
/*
This is due to a timing change in React 19
In React 18, you observe this pattern:
1. render
2. useState initializer
3. component continues to render with first state
4. strictMode: render again
5. strictMode: call useState initializer again
6. component continues to render with second state
now, in React 19 it looks like this:
1. render
2. useState initializer
3. strictMode: call useState initializer again
4. component continues to render with one of these two states
5. strictMode: render again
6. component continues to render with the same state as during the first render
Since useQuery breaks the rules of React and mutably creates an ObservableQuery on the state during render if none is present, React 18 did create two, while React 19 only creates one.
This is pure coincidence though, and the useQuery rewrite that doesn't break the rules of hooks as much and creates the ObservableQuery as part of the state initializer will end up with behaviour closer to the old React 18 behaviour again.
*/
expect(obsQueries.size).toBe(IS_REACT_19 ? 1 : 2);

const activeSet = new Set<typeof result.current.observable>();
const inactiveSet = new Set<typeof result.current.observable>();
Expand All @@ -1578,7 +1606,7 @@ describe("useQuery Hook", () => {
}
});
expect(activeSet.size).toBe(1);
expect(inactiveSet.size).toBe(1);
expect(inactiveSet.size).toBe(obsQueries.size - activeSet.size);
}

checkObservableQueries(1);
Expand Down
3 changes: 2 additions & 1 deletion src/react/hooks/__tests__/useReactiveVar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { makeVar } from "../../../core";
import { useReactiveVar } from "../useReactiveVar";

const IS_REACT_18 = React.version.startsWith("18");
const IS_REACT_19 = React.version.startsWith("19");

describe("useReactiveVar Hook", () => {
it("works with one component", async () => {
Expand Down Expand Up @@ -277,7 +278,7 @@ describe("useReactiveVar Hook", () => {
);

await waitFor(() => {
if (IS_REACT_18) {
if (IS_REACT_18 || IS_REACT_19) {
expect(mock).toHaveBeenCalledTimes(3);
expect(mock).toHaveBeenNthCalledWith(1, 0);
expect(mock).toHaveBeenNthCalledWith(2, 0);
Expand Down
13 changes: 9 additions & 4 deletions src/react/hooks/__tests__/useSuspenseQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9591,9 +9591,14 @@ describe("useSuspenseQuery", () => {

await act(() => user.type(input, "ab"));

await waitFor(() => {
expect(screen.getByTestId("result")).toHaveTextContent("ab");
});
await waitFor(
() => {
expect(screen.getByTestId("result")).toHaveTextContent("ab");
},
{
timeout: 10000,
}
);

await act(() => user.type(input, "c"));

Expand All @@ -9612,7 +9617,7 @@ describe("useSuspenseQuery", () => {
await waitFor(() => {
expect(screen.getByTestId("result")).toHaveTextContent("abc");
});
});
}, 10000);

it("works with startTransition to change variables", async () => {
type Variables = {
Expand Down
3 changes: 3 additions & 0 deletions src/react/hooks/internal/__tests__/useRenderGuard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import { render, waitFor } from "@testing-library/react";
import { withCleanup } from "../../../../testing/internal";

const UNDEF = {};
const IS_REACT_19 = React.version.startsWith("19");

it("returns a function that returns `true` if called during render", () => {
// We don't provide this functionality with React 19 anymore since it requires internals access
if (IS_REACT_19) return;
let result: boolean | typeof UNDEF = UNDEF;
function TestComponent() {
const calledDuringRender = useRenderGuard();
Expand Down
11 changes: 9 additions & 2 deletions src/testing/internal/profile/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -434,13 +434,20 @@ export function profileHook<ReturnValue extends ValidSnapshot, Props>(
);
}

function resolveHookOwner(): React.ComponentType | undefined {
function resolveR18HookOwner(): React.ComponentType | undefined {
return (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
?.ReactCurrentOwner?.current?.elementType;
}

function resolveR19HookOwner(): React.ComponentType | undefined {
return (
React as any
).__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE?.A?.getOwner()
.elementType;
}

export function useTrackRenders({ name }: { name?: string } = {}) {
const component = name || resolveHookOwner();
const component = name || resolveR18HookOwner() || resolveR19HookOwner();

if (!component) {
throw new Error(
Expand Down
3 changes: 2 additions & 1 deletion src/testing/react/__tests__/mockSubscriptionLink.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ApolloProvider } from "../../../react/context";
import { useSubscription } from "../../../react/hooks";

const IS_REACT_18 = React.version.startsWith("18");
const IS_REACT_19 = React.version.startsWith("19");

describe("mockSubscriptionLink", () => {
it("should work with multiple subscribers to the same mock websocket", async () => {
Expand Down Expand Up @@ -64,7 +65,7 @@ describe("mockSubscriptionLink", () => {
</ApolloProvider>
);

const numRenders = IS_REACT_18 ? 2 : results.length + 1;
const numRenders = IS_REACT_18 || IS_REACT_19 ? 2 : results.length + 1;

// automatic batching in React 18 means we only see 2 renders vs. 5 in v17
await waitFor(
Expand Down

0 comments on commit 6ca5ef4

Please sign in to comment.