Skip to content

Commit

Permalink
New Render-to-Render testing utilities based on React.Profiler (#11116
Browse files Browse the repository at this point in the history
)

Co-authored-by: Jerel Miller <[email protected]>
  • Loading branch information
phryneas and jerelmiller authored Sep 6, 2023
1 parent 9e59b25 commit 7c90f9a
Show file tree
Hide file tree
Showing 16 changed files with 1,370 additions and 541 deletions.
114 changes: 54 additions & 60 deletions src/react/components/__tests__/client/Query.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { InMemoryCache } from "../../../../cache";
import { ApolloProvider } from "../../../context";
import { itAsync, MockedProvider, mockSingleLink } from "../../../../testing";
import { Query } from "../../Query";
import { QueryResult } from "../../../types/types";
import { profile } from "../../../../testing/internal";

const allPeopleQuery: DocumentNode = gql`
query people {
Expand Down Expand Up @@ -1481,84 +1483,76 @@ describe("Query component", () => {
cache: new InMemoryCache({ addTypename: false }),
});

let count = 0;
let testFailures: any[] = [];
const noop = () => null;

const AllPeopleQuery2 = Query;

function Container() {
return (
<AllPeopleQuery2 query={query} notifyOnNetworkStatusChange={true}>
{(result: any) => {
try {
switch (count++) {
case 0:
// Waiting for the first result to load
expect(result.loading).toBe(true);
break;
case 1:
// First result is loaded, run a refetch to get the second result
// which is an error.
expect(result.data.allPeople).toEqual(data.allPeople);
setTimeout(() => {
result.refetch().then(() => {
fail("Expected error value on first refetch.");
}, noop);
}, 0);
break;
case 2:
// Waiting for the second result to load
expect(result.loading).toBe(true);
break;
case 3:
setTimeout(() => {
result.refetch().catch(() => {
fail("Expected good data on second refetch.");
});
}, 0);
// fallthrough
// The error arrived, run a refetch to get the third result
// which should now contain valid data.
expect(result.loading).toBe(false);
expect(result.error).toBeTruthy();
break;
case 4:
expect(result.loading).toBe(true);
expect(result.error).toBeFalsy();
break;
case 5:
expect(result.loading).toBe(false);
expect(result.error).toBeFalsy();
expect(result.data.allPeople).toEqual(dataTwo.allPeople);
break;
default:
throw new Error("Unexpected fall through");
}
} catch (e) {
// if we throw the error inside the component,
// we will get more rerenders in the test, but the `expect` error
// might not propagate anyways
testFailures.push(e);
}
{(r: any) => {
ProfiledContainer.updateSnapshot(r);
return null;
}}
</AllPeopleQuery2>
);
}

const ProfiledContainer = profile<QueryResult>({
Component: Container,
});

render(
<ApolloProvider client={client}>
<Container />
<ProfiledContainer />
</ApolloProvider>
);

await waitFor(() => {
if (testFailures.length > 0) {
throw testFailures[0];
}
expect(count).toBe(6);
});
{
const { snapshot } = await ProfiledContainer.takeRender();
expect(snapshot.loading).toBe(true);
}

{
const { snapshot } = await ProfiledContainer.takeRender();
expect(snapshot.loading).toBe(false);
expect(snapshot.data.allPeople).toEqual(data.allPeople);
// First result is loaded, run a refetch to get the second result
// which is an error.
snapshot.refetch().then(() => {
fail("Expected error value on first refetch.");
}, noop);
}

{
const { snapshot } = await ProfiledContainer.takeRender();
// Waiting for the second result to load
expect(snapshot.loading).toBe(true);
}

{
const { snapshot } = await ProfiledContainer.takeRender();
expect(snapshot.loading).toBe(false);
expect(snapshot.error).toBeTruthy();
// The error arrived, run a refetch to get the third result
// which should now contain valid data.
snapshot.refetch().catch(() => {
fail("Expected good data on second refetch.");
});
}

{
const { snapshot } = await ProfiledContainer.takeRender();
expect(snapshot.loading).toBe(true);
expect(snapshot.error).toBeFalsy();
}

{
const { snapshot } = await ProfiledContainer.takeRender();
expect(snapshot.loading).toBe(false);
expect(snapshot.error).toBeFalsy();
expect(snapshot.data.allPeople).toEqual(dataTwo.allPeople);
}
});

itAsync(
Expand Down
82 changes: 36 additions & 46 deletions src/react/hoc/__tests__/queries/lifecycle.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { InMemoryCache as Cache } from "../../../../cache";
import { mockSingleLink } from "../../../../testing";
import { Query as QueryComponent } from "../../../components";
import { graphql } from "../../graphql";
import { ChildProps } from "../../types";
import { ChildProps, DataValue } from "../../types";
import { profile } from "../../../../testing/internal";

describe("[queries] lifecycle", () => {
// lifecycle
Expand Down Expand Up @@ -50,62 +51,51 @@ describe("[queries] lifecycle", () => {
}),
})(
class extends React.Component<ChildProps<Vars, Data, Vars>> {
componentDidUpdate(prevProps: ChildProps<Vars, Data, Vars>) {
try {
const { data } = this.props;
switch (++count) {
case 1:
expect(prevProps.data!.loading).toBe(true);
expect(prevProps.data!.allPeople).toBe(undefined);
expect(data!.loading).toBe(false);
expect(data!.variables).toEqual(variables1);
expect(data!.allPeople).toEqual(data1.allPeople);
break;
case 2:
expect(data!.loading).toBe(true);
expect(data!.variables).toEqual(variables2);
expect(data!.allPeople).toBe(undefined);
break;
case 3:
expect(data!.loading).toBe(false);
expect(data!.variables).toEqual(variables2);
expect(data!.allPeople).toEqual(data2.allPeople);
break;
default:
fail(`Too many renders (${count})`);
}
} catch (err) {
fail(err);
}
}

render() {
ProfiledApp.updateSnapshot(this.props.data!);
return null;
}
}
);

class ChangingProps extends React.Component<{}, { first: number }> {
state = { first: 1 };
const ProfiledApp = profile<DataValue<Data, Vars>, Vars>({
Component: Container,
});

componentDidMount() {
setTimeout(() => {
this.setState({ first: 2 });
}, 50);
}
const { rerender } = render(<ProfiledApp first={1} />, {
wrapper: ({ children }) => (
<ApolloProvider client={client}>{children}</ApolloProvider>
),
});

render() {
return <Container first={this.state.first} />;
}
{
const { snapshot } = await ProfiledApp.takeRender();
expect(snapshot!.loading).toBe(true);
expect(snapshot!.allPeople).toBe(undefined);
}

render(
<ApolloProvider client={client}>
<ChangingProps />
</ApolloProvider>
);
{
const { snapshot } = await ProfiledApp.takeRender();
expect(snapshot!.loading).toBe(false);
expect(snapshot!.variables).toEqual(variables1);
expect(snapshot!.allPeople).toEqual(data1.allPeople);
}

await waitFor(() => expect(count).toBe(3));
rerender(<ProfiledApp first={2} />);

{
const { snapshot } = await ProfiledApp.takeRender();
expect(snapshot!.loading).toBe(true);
expect(snapshot!.variables).toEqual(variables2);
expect(snapshot!.allPeople).toBe(undefined);
}

{
const { snapshot } = await ProfiledApp.takeRender();
expect(snapshot!.loading).toBe(false);
expect(snapshot!.variables).toEqual(variables2);
expect(snapshot!.allPeople).toEqual(data2.allPeople);
}
});

it("rebuilds the queries on prop change when using `options`", async () => {
Expand Down
93 changes: 50 additions & 43 deletions src/react/hoc/__tests__/queries/loading.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import { ApolloProvider } from "../../../context";
import { InMemoryCache as Cache } from "../../../../cache";
import { itAsync, mockSingleLink } from "../../../../testing";
import { graphql } from "../../graphql";
import { ChildProps } from "../../types";
import { ChildProps, DataValue } from "../../types";
import { profile } from "../../../../testing/internal";

describe("[queries] loading", () => {
// networkStatus / loading
Expand Down Expand Up @@ -388,8 +389,6 @@ describe("[queries] loading", () => {
queryDeduplication: false,
});

let count = 0;

const usedFetchPolicies: WatchQueryFetchPolicy[] = [];
const Container = graphql<{}, Data>(query, {
options: {
Expand All @@ -408,57 +407,65 @@ describe("[queries] loading", () => {
})(
class extends React.Component<ChildProps<{}, Data>> {
render() {
++count;
if (count === 1) {
expect(this.props.data!.loading).toBe(true);
expect(this.props.data!.allPeople).toBeUndefined();
} else if (count === 2) {
expect(this.props.data!.loading).toBe(false);
expect(this.props.data!.allPeople!.people[0].name).toMatch(
/Darth Skywalker - /
);
// Has data
setTimeout(() => render(App));
} else if (count === 3) {
// Loading after remount
expect(this.props.data!.loading).toBe(true);
expect(this.props.data!.allPeople).toBeUndefined();
} else if (count >= 4) {
// Fetched data loading after remount
expect(this.props.data!.loading).toBe(false);
expect(this.props.data!.allPeople!.people[0].name).toMatch(
/Darth Skywalker - /
);
}
ProfiledContainer.updateSnapshot(this.props.data!);
return null;
}
}
);

const App: React.ReactElement<any> = (
const ProfiledContainer = profile<
DataValue<{
allPeople: {
people: {
name: string;
}[];
};
}>
>({
Component: Container,
});

const App = (
<ApolloProvider client={client}>
<Container />
<ProfiledContainer />
</ApolloProvider>
);

render(App);

await waitFor(
() => {
expect(usedFetchPolicies).toEqual([
"network-only",
"network-only",
"cache-first",
]);
},
{ interval: 1 }
);
await waitFor(
() => {
expect(count).toBe(5);
},
{ interval: 1 }
);
{
const { snapshot } = await ProfiledContainer.takeRender();
expect(snapshot.loading).toBe(true);
expect(snapshot.allPeople).toBeUndefined();
}
{
const { snapshot } = await ProfiledContainer.takeRender();
expect(snapshot.loading).toBe(false);
expect(snapshot.allPeople?.people[0].name).toMatch(/Darth Skywalker - /);
}
render(App);
// Loading after remount
{
const { snapshot } = await ProfiledContainer.takeRender();
expect(snapshot.loading).toBe(true);
expect(snapshot.allPeople).toBeUndefined();
}
{
const { snapshot } = await ProfiledContainer.takeRender();
// Fetched data loading after remount
expect(snapshot.loading).toBe(false);
expect(snapshot.allPeople!.people[0].name).toMatch(/Darth Skywalker - /);
}

await expect(ProfiledContainer).toRenderExactlyTimes(5, {
timeout: 100,
});

expect(usedFetchPolicies).toEqual([
"network-only",
"network-only",
"cache-first",
]);
});

itAsync(
Expand Down
Loading

0 comments on commit 7c90f9a

Please sign in to comment.