Skip to content
This repository has been archived by the owner on Nov 11, 2023. It is now read-only.

Commit

Permalink
Merge pull request #18 from contiamo/unit-tests
Browse files Browse the repository at this point in the history
Unit tests - Get component
  • Loading branch information
Tejas Kumar authored Aug 6, 2018
2 parents 1342b8a + 5af9348 commit 53dc5f7
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 53 deletions.
128 changes: 128 additions & 0 deletions src/Get.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,30 @@ describe("Get", () => {
});
});

describe("with error", () => {
it("should set the `error` object properly", async () => {
nock("https://my-awesome-api.fake")
.get("/")
.reply(401, { message: "You shall not pass!" });

const children = jest.fn();
children.mockReturnValue(<div />);

render(
<RestfulProvider base="https://my-awesome-api.fake">
<Get path="">{children}</Get>
</RestfulProvider>,
);

await wait(() => expect(children.mock.calls.length).toBe(2));
expect(children.mock.calls[1][0]).toEqual(null);
expect(children.mock.calls[1][1].error).toEqual({
data: { message: "You shall not pass!" },
message: "Failed to fetch: 401 Unauthorized",
});
});
});

describe("with custom resolver", () => {
it("should transform data", async () => {
nock("https://my-awesome-api.fake")
Expand Down Expand Up @@ -142,4 +166,108 @@ describe("Get", () => {
expect(children.mock.calls[0][0]).toBe(null);
});
});

describe("with base", () => {
it("should override the base url", async () => {
nock("https://my-awesome-api.fake")
.get("/")
.reply(200, { id: 1 });

const children = jest.fn();
children.mockReturnValue(<div />);

render(
<RestfulProvider base="https://not-here.fake">
<Get path="" base="https://my-awesome-api.fake">
{children}
</Get>
</RestfulProvider>,
);

await wait(() => expect(children.mock.calls.length).toBe(2));
expect(children.mock.calls[1][1].loading).toEqual(false);
expect(children.mock.calls[1][0]).toEqual({ id: 1 });
});

it("should override the base url and compose with the path", async () => {
nock("https://my-awesome-api.fake")
.get("/plop")
.reply(200, { id: 1 });

const children = jest.fn();
children.mockReturnValue(<div />);

render(
<RestfulProvider base="https://not-here.fake">
<Get path="/plop" base="https://my-awesome-api.fake">
{children}
</Get>
</RestfulProvider>,
);

await wait(() => expect(children.mock.calls.length).toBe(2));
expect(children.mock.calls[1][1].loading).toEqual(false);
expect(children.mock.calls[1][0]).toEqual({ id: 1 });
});
});

describe("with custom request options", () => {
it("should add a custom header", async () => {
nock("https://my-awesome-api.fake", { reqheaders: { foo: "bar" } })
.get("/")
.reply(200, { id: 1 });

const children = jest.fn();
children.mockReturnValue(<div />);

render(
<RestfulProvider base="https://my-awesome-api.fake">
<Get path="" requestOptions={{ headers: { foo: "bar" } }}>
{children}
</Get>
</RestfulProvider>,
);

await wait(() => expect(children.mock.calls.length).toBe(2));
expect(children.mock.calls[1][1].loading).toEqual(false);
expect(children.mock.calls[1][0]).toEqual({ id: 1 });
});
});

describe("actions", () => {
it("should refetch", async () => {
nock("https://my-awesome-api.fake")
.get("/")
.reply(200, { id: 1 });

const children = jest.fn();
children.mockReturnValue(<div />);

// initial fetch
render(
<RestfulProvider base="https://my-awesome-api.fake">
<Get path="">{children}</Get>
</RestfulProvider>,
);

await wait(() => expect(children.mock.calls.length).toBe(2));
expect(children.mock.calls[1][1].loading).toEqual(false);
expect(children.mock.calls[1][0]).toEqual({ id: 1 });

// refetch
nock("https://my-awesome-api.fake")
.get("/")
.reply(200, { id: 2 });
children.mock.calls[1][2].refetch();
await wait(() => expect(children.mock.calls.length).toBe(4));

// transition state
expect(children.mock.calls[2][1].loading).toEqual(true);
expect(children.mock.calls[2][0]).toEqual({ id: 1 });

// after refetch state
expect(children.mock.calls[3][1].loading).toEqual(false);
expect(children.mock.calls[3][0]).toEqual({ id: 2 });
});
});
});
37 changes: 20 additions & 17 deletions src/Get.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,29 @@ import RestfulReactProvider, { RestfulReactConsumer, RestfulReactProviderProps }
*/
export type ResolveFunction<T> = (data: any) => T;

export interface GetDataError<S> {
export interface GetDataError<TError> {
message: string;
data: S;
data: TError;
}

/**
* An enumeration of states that a fetchable
* view could possibly have.
*/
export interface States<S> {
export interface States<TData, TError> {
/** Is our view currently loading? */
loading: boolean;
/** Do we have an error in the view? */
error?: GetComponentState<S>["error"];
error?: GetComponentState<TData, TError>["error"];
}

/**
* An interface of actions that can be performed
* within Get
*/
export interface Actions<T> {
export interface Actions<TData> {
/** Refetches the same path */
refetch: () => Promise<T>;
refetch: () => Promise<TData | null>;
}

/**
Expand All @@ -46,7 +46,7 @@ export interface Meta {
/**
* Props for the <Get /> component.
*/
export interface GetComponentProps<T = {}, S = {}> {
export interface GetComponentProps<TData, TError> {
/**
* The path at which to request data,
* typically composed by parent Gets or the RestfulProvider.
Expand All @@ -59,14 +59,14 @@ export interface GetComponentProps<T = {}, S = {}> {
* @param data - data returned from the request.
* @param actions - a key/value map of HTTP verbs, aliasing destroy to DELETE.
*/
children: (data: T | null, states: States<S>, actions: Actions<T>, meta: Meta) => React.ReactNode;
children: (data: TData | null, states: States<TData, TError>, actions: Actions<TData>, meta: Meta) => React.ReactNode;
/** Options passed into the fetch call. */
requestOptions?: RestfulReactProviderProps["requestOptions"];
/**
* A function to resolve data return from the backend, most typically
* used when the backend response needs to be adapted in some way.
*/
resolve?: ResolveFunction<T>;
resolve?: ResolveFunction<TData>;
/**
* Should we wait until we have data before rendering?
* This is useful in cases where data is available too quickly
Expand All @@ -90,7 +90,7 @@ export interface GetComponentProps<T = {}, S = {}> {
* are implementation details and should be
* hidden from any consumers.
*/
export interface GetComponentState<T, S = {}> {
export interface GetComponentState<T, S> {
data: T | null;
response: Response | null;
error: GetDataError<S> | null;
Expand All @@ -102,15 +102,18 @@ export interface GetComponentState<T, S = {}> {
* is a named class because it is useful in
* debugging.
*/
class ContextlessGet<T> extends React.Component<GetComponentProps<T>, Readonly<GetComponentState<T>>> {
public readonly state: Readonly<GetComponentState<T>> = {
class ContextlessGet<TData, TError> extends React.Component<
GetComponentProps<TData, TError>,
Readonly<GetComponentState<TData, TError>>
> {
public readonly state: Readonly<GetComponentState<TData, TError>> = {
data: null, // Means we don't _yet_ have data.
response: null,
loading: !this.props.lazy,
error: null,
};

public static defaultProps: Partial<GetComponentProps<{}>> = {
public static defaultProps = {
resolve: (unresolvedData: any) => unresolvedData,
};

Expand All @@ -120,7 +123,7 @@ class ContextlessGet<T> extends React.Component<GetComponentProps<T>, Readonly<G
}
}

public componentDidUpdate(prevProps: GetComponentProps<T>) {
public componentDidUpdate(prevProps: GetComponentProps<TData, TError>) {
// If the path or base prop changes, refetch!
const { path, base } = this.props;
if (prevProps.path !== path || prevProps.base !== base) {
Expand Down Expand Up @@ -168,15 +171,15 @@ class ContextlessGet<T> extends React.Component<GetComponentProps<T>, Readonly<G
const request = new Request(`${base}${requestPath || path || ""}`, this.getRequestOptions(thisRequestOptions));
const response = await fetch(request);

const data: T =
const data =
response.headers.get("content-type") === "application/json" ? await response.json() : await response.text();

if (!response.ok) {
this.setState({
loading: false,
error: { message: `Failed to fetch: ${response.status} ${response.statusText}`, data },
});
throw response;
return null;
}

this.setState({ loading: false, data: resolve!(data) });
Expand Down Expand Up @@ -205,7 +208,7 @@ class ContextlessGet<T> extends React.Component<GetComponentProps<T>, Readonly<G
* in order to provide new `base` props that contain
* a segment of the path, creating composable URLs.
*/
function Get<T>(props: GetComponentProps<T>) {
function Get<TData = {}, TError = {}>(props: GetComponentProps<TData, TError>) {
return (
<RestfulReactConsumer>
{contextProps => (
Expand Down
39 changes: 26 additions & 13 deletions src/Mutate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import { GetComponentState } from "./Get";
* An enumeration of states that a fetchable
* view could possibly have.
*/
export interface States<T = {}> {
export interface States<TData, TError> {
/** Is our view currently loading? */
loading: boolean;
/** Do we have an error in the view? */
error?: GetComponentState<T>["error"];
error?: GetComponentState<TData, TError>["error"];
}

/**
Expand Down Expand Up @@ -47,38 +47,48 @@ export interface MutateComponentCommonProps {
requestOptions?: RestfulReactProviderProps["requestOptions"];
}

export interface MutateComponentWithDelete extends MutateComponentCommonProps {
export interface MutateComponentWithDelete<TData, TError> extends MutateComponentCommonProps {
verb: "DELETE";
/**
* A function that recieves a mutation function, along with
* some metadata.
*
* @param actions - a key/value map of HTTP verbs, aliasing destroy to DELETE.
*/
children: (mutate: (resourceId?: string | {}) => Promise<Response>, states: States, meta: Meta) => React.ReactNode;
children: (
mutate: (resourceId?: string | {}) => Promise<Response>,
states: States<TData, TError>,
meta: Meta,
) => React.ReactNode;
}

export interface MutateComponentWithOtherVerb extends MutateComponentCommonProps {
export interface MutateComponentWithOtherVerb<TData, TError> extends MutateComponentCommonProps {
verb: "POST" | "PUT" | "PATCH";
/**
* A function that recieves a mutation function, along with
* some metadata.
*
* @param actions - a key/value map of HTTP verbs, aliasing destroy to DELETE.
*/
children: (mutate: (body?: string | {}) => Promise<Response>, states: States, meta: Meta) => React.ReactNode;
children: (
mutate: (body?: string | {}) => Promise<Response>,
states: States<TData, TError>,
meta: Meta,
) => React.ReactNode;
}

export type MutateComponentProps = MutateComponentWithDelete | MutateComponentWithOtherVerb;
export type MutateComponentProps<TData, TError> =
| MutateComponentWithDelete<TData, TError>
| MutateComponentWithOtherVerb<TData, TError>;

/**
* State for the <Mutate /> component. These
* are implementation details and should be
* hidden from any consumers.
*/
export interface MutateComponentState<S = {}> {
export interface MutateComponentState<TData, TError> {
response: Response | null;
error: GetComponentState<S>["error"];
error: GetComponentState<TData, TError>["error"];
loading: boolean;
}

Expand All @@ -87,8 +97,11 @@ export interface MutateComponentState<S = {}> {
* is a named class because it is useful in
* debugging.
*/
class ContextlessMutate extends React.Component<MutateComponentProps, MutateComponentState> {
public readonly state: Readonly<MutateComponentState> = {
class ContextlessMutate<TData, TError> extends React.Component<
MutateComponentProps<TData, TError>,
MutateComponentState<TData, TError>
> {
public readonly state: Readonly<MutateComponentState<TData, TError>> = {
response: null,
loading: false,
error: null,
Expand Down Expand Up @@ -146,12 +159,12 @@ class ContextlessMutate extends React.Component<MutateComponentProps, MutateComp
* in order to provide new `base` props that contain
* a segment of the path, creating composable URLs.
*/
function Mutate(props: MutateComponentProps) {
function Mutate<TError = {}, TData = {}>(props: MutateComponentProps<TData, TError>) {
return (
<RestfulReactConsumer>
{contextProps => (
<RestfulReactProvider {...contextProps} base={`${contextProps.base}${props.path}`}>
<ContextlessMutate {...contextProps} {...props} />
<ContextlessMutate<TData, TError> {...contextProps} {...props} />
</RestfulReactProvider>
)}
</RestfulReactConsumer>
Expand Down
Loading

0 comments on commit 53dc5f7

Please sign in to comment.