Skip to content

Commit

Permalink
feat: add usePrismicPreviewResolver
Browse files Browse the repository at this point in the history
  • Loading branch information
angeloashmore committed Nov 20, 2021
1 parent d9cc445 commit 4e64de2
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 2 deletions.
3 changes: 3 additions & 0 deletions src/index.ts
Expand Up @@ -30,6 +30,9 @@ export type {
export { PrismicToolbar } from "./PrismicToolbar";
export type { PrismicToolbarProps } from "./PrismicToolbar";

export { usePrismicPreviewResolver } from "./usePrismicPreviewResolver";
export type { UsePrismicPreviewResolverArgs } from "./usePrismicPreviewResolver";

export {
useAllPrismicDocumentsDangerously,
useAllPrismicDocumentsByEveryTag,
Expand Down
85 changes: 85 additions & 0 deletions src/usePrismicPreviewResolver.ts
@@ -0,0 +1,85 @@
import type * as prismic from "@prismicio/client";

import * as React from "react";

import {
ClientHookReturnType,
useStatefulPrismicClientMethod,
} from "./useStatefulPrismicClientMethod";

export type UsePrismicPreviewResolverArgs = {
/**
* An optional `@prismicio/client` instance to override the Client provided to
* `<PrismicProvider>`
*/
client?: prismic.Client;

/**
* A function that maps a Prismic document to a URL within your app.
*/
linkResolver?: Parameters<
prismic.Client["resolvePreviewURL"]
>[0]["linkResolver"];

/**
* A fallback URL if the Link Resolver does not return a value.
*/
defaultURL?: Parameters<prismic.Client["resolvePreviewURL"]>[0]["defaultURL"];

/**
* The preview token (also known as a ref) that will be used to query preview
* content from the Prismic repository.
*/
previewToken?: Parameters<
prismic.Client["resolvePreviewURL"]
>[0]["previewToken"];

/**
* The previewed document that will be used to determine the destination URL.
*/
documentID?: Parameters<prismic.Client["resolvePreviewURL"]>[0]["documentID"];

/**
* A function to automatically navigate to the resolved URL. If a function is
* not provded, `usePreviewResolver` will not navigate to the URL.
*
* @param url - The resolved preview URL.
*/
navigate?: (url: string) => unknown;
};

/**
* Resolve a preview session's URL. The resolved URL can be used to redirect to
* the previewed document.
*
* If a `navigate` function is provided, the hook will automatically navigate to
* the previewed document's URL.
*
* @param args - Arguments to configure how a URL is resolved.
*
* @returns A tuple containing the resolved URL and the hook's state.
*/
export const usePrismicPreviewResolver = (
args: UsePrismicPreviewResolverArgs = {},
): ClientHookReturnType<string> => {
const result = useStatefulPrismicClientMethod(
"resolvePreviewURL",
[
{
linkResolver: args.linkResolver,
defaultURL: args.defaultURL || "/",
previewToken: args.previewToken,
documentID: args.documentID,
},
],
args.client,
);

React.useEffect(() => {
if (result[0] && args.navigate) {
args.navigate(result[0]);
}
}, [result[0], args.navigate]);

return result;
};
5 changes: 3 additions & 2 deletions src/useStatefulPrismicClientMethod.ts
Expand Up @@ -138,16 +138,17 @@ export const useStatefulPrismicClientMethod = <
>(
methodName: TMethodName,
args: TArgs,
explicitClient?: prismic.Client,
): ClientHookReturnType<TData> => {
const lastArg = args[args.length - 1];
const {
client: explicitClient,
client: lastArgExplicitClient,
skip,
...params
} = isParams(lastArg) ? lastArg : ({} as HookOnlyParameters);
const argsWithoutParams = isParams(lastArg) ? args.slice(0, -1) : args;

const client = usePrismicClient(explicitClient);
const client = usePrismicClient(explicitClient || lastArgExplicitClient);

const [state, dispatch] = React.useReducer<
React.Reducer<StateMachineState<TData>, StateMachineAction<TData>>
Expand Down
159 changes: 159 additions & 0 deletions test/usePrismicPreviewResolver.test.tsx
@@ -0,0 +1,159 @@
/* eslint-disable react/display-name */
/* eslint-disable react/prop-types */

import test from "ava";
import * as React from "react";
import * as msw from "msw";
import * as mswNode from "msw/node";
import * as sinon from "sinon";
import * as prismic from "@prismicio/client";
import { renderHook, cleanup } from "@testing-library/react-hooks";

import { createClient } from "./__testutils__/createClient";
import { createDocument } from "./__testutils__/createDocument";
import { createMockQueryHandler } from "./__testutils__/createMockQueryHandler";
import { createMockRepositoryHandler } from "./__testutils__/createMockRepositoryHandler";
import { createQueryResponse } from "./__testutils__/createQueryResponse";
import { createRef } from "./__testutils__/createRef";
import { createRepositoryResponse } from "./__testutils__/createRepositoryResponse";
import { md5 } from "./__testutils__/md5";

import { PrismicProvider, usePrismicPreviewResolver } from "../src";

const server = mswNode.setupServer();
test.before(() => server.listen({ onUnhandledRequest: "error" }));
test.after(() => server.close());

// We must clean up after each test. We also must run each test serially to
// ensure the clean up process only occurs between tests.
test.afterEach(() => {
cleanup();
});

const createWrapper = (client: prismic.Client): React.ComponentType => {
return (props) => <PrismicProvider client={client} {...props} />;
};

test.serial("returns a resolved preview URL", async (t) => {
const client = createClient(t);
const wrapper = createWrapper(client);

const repositoryResponse = createRepositoryResponse();
const document = createDocument({ url: "preview-url" });
const queryResponsePages = [createQueryResponse([document])];
const previewToken = createRef(false).ref;

server.use(
createMockRepositoryHandler(t, repositoryResponse),
createMockQueryHandler(t, queryResponsePages, {
ref: previewToken,
q: `[${prismic.predicate.at("document.id", document.id)}]`,
}),
);

const { result, waitForValueToChange } = renderHook(
() =>
usePrismicPreviewResolver({
documentID: document.id,
previewToken,
}),
{ wrapper },
);

await waitForValueToChange(() => result.current[1].state === "loaded");

t.deepEqual(result.current[0], document.url);
});

test.serial("navigates if a navigate function is provided", async (t) => {
const client = createClient(t);
const wrapper = createWrapper(client);

const repositoryResponse = createRepositoryResponse();
const document = createDocument({ url: "preview-url" });
const queryResponsePages = [createQueryResponse([document])];
const previewToken = createRef(false).ref;

const navigate = sinon.stub();

server.use(
createMockRepositoryHandler(t, repositoryResponse),
createMockQueryHandler(t, queryResponsePages, {
ref: previewToken,
q: `[${prismic.predicate.at("document.id", document.id)}]`,
}),
);

const { result, waitForValueToChange } = renderHook(
() =>
usePrismicPreviewResolver({
documentID: document.id,
previewToken,
navigate,
}),
{ wrapper },
);

await waitForValueToChange(() => result.current[1].state === "loaded");

t.true(navigate.calledWith(document.url));
});

test.serial("supports explicit client", async (t) => {
const client = createClient(t);

const repositoryResponse = createRepositoryResponse();
const document = createDocument({ url: "preview-url" });
const queryResponsePages = [createQueryResponse([document])];
const previewToken = createRef(false).ref;

server.use(
createMockRepositoryHandler(t, repositoryResponse),
createMockQueryHandler(t, queryResponsePages, {
ref: previewToken,
q: `[${prismic.predicate.at("document.id", document.id)}]`,
}),
);

const { result, waitForValueToChange } = renderHook(() =>
usePrismicPreviewResolver({
client,
documentID: document.id,
previewToken,
}),
);

await waitForValueToChange(() => result.current[1].state === "loaded");

t.deepEqual(result.current[0], document.url);
});

test.serial("returns failed state on error", async (t) => {
const client = createClient(t);
const wrapper = createWrapper(client);
const repositoryResponse = {
message: "invalid access token",
oauth_initiate: "oauth_initiate",
oauth_token: "oauth_token",
};

server.use(
msw.rest.get(prismic.getEndpoint(md5(t.title)), (_req, res, ctx) => {
return res(ctx.status(403), ctx.json(repositoryResponse));
}),
);

const { result, waitForValueToChange } = renderHook(
() =>
usePrismicPreviewResolver({
documentID: "documentID",
previewToken: "previewToken",
}),
{ wrapper },
);

await waitForValueToChange(() => result.current[1].state === "failed");

t.true(result.current[1].error instanceof prismic.ForbiddenError);
t.is(result.current[0], undefined);
});

0 comments on commit 4e64de2

Please sign in to comment.