Skip to content

Commit

Permalink
Allow fragment refs to be null or undefined (#94)
Browse files Browse the repository at this point in the history
* Make fragment ref nullable

* Update test name

* Change files

* Clarify type names

* Fixes from PR comments
  • Loading branch information
Markionium authored Mar 20, 2024
1 parent b47d7a5 commit 78f7c8c
Show file tree
Hide file tree
Showing 6 changed files with 467 additions and 17 deletions.
7 changes: 7 additions & 0 deletions change/@nova-react-c10c9e36-4cda-4e41-bc32-85c8d2031bdb.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Make fragment ref nullable",
"packageName": "@nova/react",
"email": "[email protected]",
"dependentChangeType": "patch"
}
1 change: 1 addition & 0 deletions packages/nova-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"devDependencies": {
"@graphitation/graphql-js-tag": "^0.9.0",
"@testing-library/react": "^12.0.0",
"@testing-library/react-hooks": "^8.0.1",
"@types/invariant": "^2.2.35",
"@types/jest": "^29.2.0",
"@types/react": "^17.0.2",
Expand Down
332 changes: 331 additions & 1 deletion packages/nova-react/src/graphql/hooks.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import React from "react";
import { render, screen } from "@testing-library/react";
import { renderHook } from "@testing-library/react-hooks";

import { NovaGraphQLProvider } from "./nova-graphql-provider";
import type { NovaGraphQL } from "@nova/types";
Expand All @@ -18,6 +19,9 @@ import {
import type { GraphQLTaggedNode } from "./taggedNode";
import type { FragmentRefs } from "./types";

type IsNotNull<T> = null extends T ? false : true;
type IsNotUndefined<T> = undefined extends T ? false : true;

describe(useLazyLoadQuery, () => {
it("ensures an implementation is supplied", () => {
const graphql: NovaGraphQL = {};
Expand Down Expand Up @@ -67,7 +71,7 @@ describe(useLazyLoadQuery, () => {
expect(graphql.useLazyLoadQuery).toHaveBeenCalledWith(
query,
{},
{ context: { callerInfo: "subject-with-query" } }
{ context: { callerInfo: "subject-with-query" } },
);
expect(screen.getByText("some-data")).toBeDefined();
});
Expand Down Expand Up @@ -152,9 +156,100 @@ describe(useFragment, () => {
useFragment(fragment, opaqueFragmentRef);
void _;
});

it("allows null or undefined to be passed as the fragment ref", () => {
const fragment = {} as unknown as GraphQLTaggedNode;

const { result, rerender } = renderHook<
{ fragmentRef: null | undefined },
null | undefined
>(({ fragmentRef }) => useFragment(fragment, fragmentRef), {
wrapper: ({ children }) => (
<NovaGraphQLProvider graphql={{}}>{children}</NovaGraphQLProvider>
),
initialProps: {
fragmentRef: null,
},
});

expect(result.current).toBeNull();

rerender({ fragmentRef: undefined });

expect(result.current).toBeUndefined();
});

it("returns a value that can be `null`, `undefined` or data", () => {
type SomeFragment$data = { someKey: string };
type SomeFragment$key = {
readonly " $data"?: SomeFragment$data;
readonly " $fragmentRefs": FragmentRefs<"SomeFragment">;
};

const fragment = {} as unknown as GraphQLTaggedNode;
const opaqueFragmentRef = {} as unknown as SomeFragment$key | null;

const { result } = renderHook(
() => useFragment(fragment, opaqueFragmentRef),
{
wrapper: ({ children }) => (
<NovaGraphQLProvider graphql={{}}>{children}</NovaGraphQLProvider>
),
},
);

type ExpectedReturnType = typeof result.current;

const _: ExpectedReturnType = null;
const __: ExpectedReturnType = undefined;
const ___: ExpectedReturnType = { someKey: "some-data" };

// Workaround for TS complaining about unused variables
void _, __, ___;
});

it("returns a value that can not be `null` or `undefined` when the fragment ref is not `null` or `undefined`", () => {
type SomeFragment$data = { someKey: string };
type SomeFragment$key = {
readonly " $data"?: SomeFragment$data;
readonly " $fragmentRefs": FragmentRefs<"SomeFragment">;
};

const fragment = {} as unknown as GraphQLTaggedNode;
const opaqueFragmentRef = {} as unknown as SomeFragment$key;

const { result } = renderHook(
() => useFragment(fragment, opaqueFragmentRef),
{
wrapper: ({ children }) => (
<NovaGraphQLProvider graphql={{}}>{children}</NovaGraphQLProvider>
),
initialProps: {
fragmentRef: opaqueFragmentRef,
},
},
);

type ExpectedReturnType = typeof result.current;

const _: IsNotNull<ExpectedReturnType> = true;
const __: IsNotUndefined<ExpectedReturnType> = true;
const ___: ExpectedReturnType = { someKey: "some-data" };

// Workaround for TS complaining about unused variables
void _, __, ___;
});
});

describe(useRefetchableFragment, () => {
beforeEach(() => {
jest.restoreAllMocks();

jest.spyOn(console, "error").mockImplementation(() => {
/* noop */
});
});

it("uses the host's hook, if provided", () => {
const graphql: NovaGraphQL = {
useRefetchableFragment: jest.fn(() => [
Expand Down Expand Up @@ -207,9 +302,146 @@ describe(useRefetchableFragment, () => {
`"Expected host to provide a useRefetchableFragment hook"`,
);
});

it("supports passing null as reference to the fragment", () => {
const { result } = renderHook(
() => {
const fragment = {} as unknown as GraphQLTaggedNode;

return useRefetchableFragment(fragment, null);
},
{
wrapper: ({ children }) => (
<NovaGraphQLProvider
graphql={{
useRefetchableFragment: (_, ref) => [ref, jest.fn()],
}}
>
{children}
</NovaGraphQLProvider>
),
},
);

const [data] = result.current;

const _: null | undefined = data;
void _;

expect(result.current[0]).toBeNull();
});

it("supports passing null as reference to the fragment", () => {
const { result } = renderHook(
() => {
const fragment = {} as unknown as GraphQLTaggedNode;
const fragmentRef = null;

return useRefetchableFragment(fragment, fragmentRef);
},
{
wrapper: ({ children }) => (
<NovaGraphQLProvider
graphql={{
useRefetchableFragment: (_, ref) => [ref, jest.fn()],
}}
>
{children}
</NovaGraphQLProvider>
),
},
);

expect(result.current[0]).toBeNull();
});

it("return type does not include null or undefined when the ref is not null", () => {
const { result } = renderHook(
() => {
type SomeFragment$data = { someKey: string };
type SomeFragment$key = {
readonly " $data"?: SomeFragment$data;
readonly " $fragmentRefs": FragmentRefs<"SomeFragment">;
};

const fragment = {} as unknown as GraphQLTaggedNode;
const opaqueFragmentRef = {} as unknown as SomeFragment$key;

return useRefetchableFragment(fragment, opaqueFragmentRef);
},
{
wrapper: ({ children }) => (
<NovaGraphQLProvider
graphql={{
useRefetchableFragment: (_, ref) => [ref, jest.fn()],
}}
>
{children}
</NovaGraphQLProvider>
),
},
);

type ExpectedReturnType = (typeof result.current)[0];

const _: IsNotNull<ExpectedReturnType> = true;
const __: IsNotUndefined<ExpectedReturnType> = true;
const ___: ExpectedReturnType = { someKey: "some-data " };

// Workaround for TS complaining about unused variables
void _, __, ___;
});

it("supports null and undefined as result types when the key can be null or undefined", () => {
const { result } = renderHook(
() => {
type SomeFragment$data = { someKey: string };
type SomeFragment$key = {
readonly " $data"?: SomeFragment$data;
readonly " $fragmentRefs": FragmentRefs<"SomeFragment">;
};

const fragment = {} as unknown as GraphQLTaggedNode;
const opaqueFragmentRef = {} as unknown as
| SomeFragment$key
| null
| undefined;

return useRefetchableFragment(fragment, opaqueFragmentRef);
},
{
wrapper: ({ children }) => (
<NovaGraphQLProvider
graphql={{
useRefetchableFragment: (_, ref) => [ref, jest.fn()],
}}
>
{children}
</NovaGraphQLProvider>
),
},
);

type ExpectedReturnType = (typeof result.current)[0];

const _: IsNotNull<ExpectedReturnType> = false;
const __: IsNotUndefined<ExpectedReturnType> = false;
const ___: ExpectedReturnType = { someKey: "some-data " };

// Workaround for TS complaining about unused variables
void _, __, ___;
});
});

describe(usePaginationFragment, () => {
beforeEach(() => {
jest.restoreAllMocks();

jest.spyOn(console, "error").mockImplementation(() => {
/* noop */
});
});

it("uses the host's hook, if provided", () => {
const mockedResponse = {
data: {
Expand Down Expand Up @@ -272,6 +504,104 @@ describe(usePaginationFragment, () => {
`"Expected host to provide a usePaginationFragment hook"`,
);
});

it("does not include null or undefined in the return type when the key is defined.", () => {
const graphql: NovaGraphQL = {
usePaginationFragment: jest.fn().mockImplementation(() => ({
data: {},
})),
};

type SomeFragment$data = { someKey: string };

const { result } = renderHook(
() => {
type SomeFragment$key = {
readonly " $data"?: SomeFragment$data;
readonly " $fragmentRefs": FragmentRefs<"SomeFragment">;
};

const fragment = {} as unknown as GraphQLTaggedNode;
const opaqueFragmentRef = {} as unknown as SomeFragment$key;

return usePaginationFragment(fragment, opaqueFragmentRef);
},
{
wrapper: ({ children }) => (
<NovaGraphQLProvider graphql={graphql}>
{children}
</NovaGraphQLProvider>
),
},
);

const _: SomeFragment$data = result.current.data;
void _;
});

it("allows fragment ref, null, or undefined to be passed as a fragment ref and returns data, null, or undefined", () => {
const graphql: NovaGraphQL = {
usePaginationFragment: jest.fn().mockImplementation(() => ({
data: {},
})),
};

type SomeFragment$data = { someKey: string };

const { result } = renderHook(
() => {
type SomeFragment$key = {
readonly " $data"?: SomeFragment$data;
readonly " $fragmentRefs": FragmentRefs<"SomeFragment">;
};

const fragment = {} as unknown as GraphQLTaggedNode;
const opaqueFragmentRef = {} as unknown as
| SomeFragment$key
| null
| undefined;

return usePaginationFragment(fragment, opaqueFragmentRef);
},
{
wrapper: ({ children }) => (
<NovaGraphQLProvider graphql={graphql}>
{children}
</NovaGraphQLProvider>
),
},
);

const _: SomeFragment$data | null | undefined = result.current.data;
void _;
});

it("allows null or undefined to be passed as a fragment ref and returns null or undefined", () => {
const graphql: NovaGraphQL = {
usePaginationFragment: jest.fn().mockImplementation(() => ({
data: {},
})),
};

const { result } = renderHook(
() => {
const fragment = {} as unknown as GraphQLTaggedNode;
const fragmentRef = null;

return usePaginationFragment(fragment, fragmentRef);
},
{
wrapper: ({ children }) => (
<NovaGraphQLProvider graphql={graphql}>
{children}
</NovaGraphQLProvider>
),
},
);

const _: null | undefined = result.current.data;
void _;
});
});

describe(useSubscription, () => {
Expand Down
Loading

0 comments on commit 78f7c8c

Please sign in to comment.