Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow fragment refs to be null or undefined #94

Merged
merged 5 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading