;
+}
+
+export interface RquiredMoonContextValue {
+ client: MoonClient;
+ store: QueryCache;
+}
+
+interface IMoonProviderProps {
+ // The links ( HTTP clients config)
+ links: ILink[];
+ // The global Moon client factory (like the moon-axios Axios client for moon https://github.com/dktunited/moon-axios)
+ clientFactory: ClientFactory;
+ // eslint-disable-next-line no-undef
+ children: JSX.Element;
+ // The react-query cache object
+ store?: QueryCache;
+ // The react-query cache config (please see https://react-query.tanstack.com/docs/api/#reactqueryconfigprovider for more details)
+ config?: ReactQueryConfig;
+ // The react-query initial cache state (please see https://react-query.tanstack.com/docs/api#hydrationdehydrate for more details)
+ hydrate?: HydrateProps;
+}
+
+export type PropsWithoutMoon = Omit
;
+
+export type PropsWithMoon
= P & { client: MoonClient };
+
+export const MoonContext: React.Context = React.createContext({
+ client: null,
+ store: null
+});
+
+class MoonProvider extends React.Component {
+ readonly client: MoonClient;
+
+ constructor(props: IMoonProviderProps) {
+ super(props);
+ const { links, clientFactory } = this.props;
+ this.client = new MoonClient(links, clientFactory);
+ }
+
+ render() {
+ const { children, hydrate, store, config } = this.props;
+ const queryCache = getMoonStore(store);
+ return (
+
+
+
+ {children}
+
+
+
+ );
+ }
+}
+
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+export function withMoon = any>(
+ WrappedComponent: React.ComponentClass | React.FunctionComponent
+) {
+ type WrappedComponentInstance = typeof WrappedComponent extends React.ComponentClass
+ ? InstanceType>
+ : ReturnType>;
+ type WrappedComponentPropsWithoutMoon = PropsWithoutMoon;
+
+ const WithMoonComponent: React.FunctionComponent,
+ WrappedComponentInstance
+ >> = ({ forwardedRef, ...rest }) => {
+ return (
+
+ {({ client, store }) => {
+ const componentProps = { client, store, ...rest } as Props;
+ return ;
+ }}
+
+ );
+ };
+
+ return React.forwardRef((props, ref) => {
+ // @ts-ignore I don't know how to implement this without breaking out of the types.
+ return ;
+ });
+}
+
+export default MoonProvider;
diff --git a/packages/moon/src/mutation-hook.tsx b/packages/moon/src/mutation-hook.tsx
new file mode 100644
index 0000000..7c26049
--- /dev/null
+++ b/packages/moon/src/mutation-hook.tsx
@@ -0,0 +1,50 @@
+import { useMutation as useReactMutation, MutationResultPair, MutationConfig } from "react-query";
+
+import { MutateType } from "./moon-client";
+import { useMoon } from "./hooks";
+
+export interface IMutationProps<
+ MutationVariables = any,
+ MutationResponse = any,
+ MutationError = any,
+ MutationClientConfig = any
+> {
+ /** The link id of the http client */
+ source?: string;
+ /** The REST end point */
+ endPoint?: string;
+ /** The variables of your mutation */
+ variables?: MutationVariables;
+ /** The mutation method. Default value: MutateType.Post */
+ type?: MutateType;
+ /** The http client options of your mutation. */
+ options?: MutationClientConfig;
+ /** The react-query config. Please see the react-query MutationConfig for more details. */
+ mutationConfig?: MutationConfig;
+}
+
+export default function useMutation<
+ MutationVariables = any,
+ MutationResponse = any,
+ MutationError = any,
+ MutationClientConfig = any
+>({
+ source,
+ endPoint,
+ type,
+ variables,
+ options,
+ mutationConfig
+}: IMutationProps): MutationResultPair<
+ MutationResponse | undefined,
+ MutationError,
+ MutationVariables,
+ unknown
+> {
+ const { client } = useMoon();
+ function mutation() {
+ return client.mutate(source, endPoint, type, variables, options);
+ }
+
+ return useReactMutation(mutation, mutationConfig);
+}
diff --git a/packages/moon/src/mutation.tsx b/packages/moon/src/mutation.tsx
new file mode 100644
index 0000000..bda0760
--- /dev/null
+++ b/packages/moon/src/mutation.tsx
@@ -0,0 +1,35 @@
+import { MutateFunction, MutationResult } from "react-query";
+
+import { MutateType } from "./moon-client";
+import { Nullable } from "./typing";
+import useMutation, { IMutationProps } from "./mutation-hook";
+
+export interface IMutationChildrenProps
+ extends MutationResult {
+ actions: { mutate: MutateFunction };
+}
+
+export type MutationChildren = (
+ props: IMutationChildrenProps
+ // eslint-disable-next-line no-undef
+) => Nullable;
+
+export interface IMutationComponentProps
+ extends IMutationProps {
+ children?: MutationChildren;
+}
+
+function Mutation(
+ props: IMutationComponentProps
+ // eslint-disable-next-line no-undef
+): Nullable {
+ const { children, ...mutationProps } = props;
+ const [mutate, state] = useMutation(mutationProps);
+ return children ? children({ ...state, actions: { mutate } }) : null;
+}
+
+Mutation.defaultProps = {
+ type: MutateType.Post
+};
+
+export default Mutation;
diff --git a/packages/moon/src/query-hook.tsx b/packages/moon/src/query-hook.tsx
new file mode 100644
index 0000000..71dab27
--- /dev/null
+++ b/packages/moon/src/query-hook.tsx
@@ -0,0 +1,118 @@
+import * as React from "react";
+import { useQuery as useReactQuery, QueryResult, QueryConfig as ReactQueryConfig } from "react-query";
+
+import { useMoon, usePrevValue } from "./hooks";
+import { ClientConfig, getId } from "./utils";
+
+export enum FetchPolicy {
+ // always try reading data from your cache first
+ CacheFirst = "cache-first",
+ // first trying to read data from your cache
+ CacheAndNetwork = "cache-and-network",
+ // never return you initial data from the cache
+ NetworkOnly = "network-only"
+}
+
+export type IQueryResultProps = [
+ Pick, "clear" | "fetchMore" | "refetch" | "remove"> & { cancel: () => void },
+ Omit, "clear" | "fetchMore" | "refetch" | "remove">
+];
+
+export interface IQueryProps {
+ id?: string;
+ /** The Link id of the http client. */
+ source?: string;
+ /** The REST end point. */
+ endPoint?: string;
+ /** The variables of your query. */
+ variables?: QueryVariables;
+ /**
+ * The fetch policy is an option which allows you to
+ * specify how you want your component to interact with
+ * the Moon data cache. Default value: FetchPolicy.CacheAndNetwork */
+ fetchPolicy?: FetchPolicy;
+ /** The http client options of your query. */
+ options?: QueryConfig;
+ /** The react-query config. Please see the react-query QueryConfig for more details. */
+ queryConfig?: ReactQueryConfig;
+}
+
+export const getQueryId = (queryProps: Pick): string => {
+ return getId(queryProps);
+};
+
+export default function useQuery<
+ QueryVariables = any,
+ QueryResponse = any,
+ QueryError = any,
+ QueryConfig extends ClientConfig = any
+>({
+ id,
+ source,
+ endPoint,
+ variables,
+ options,
+ fetchPolicy = FetchPolicy.CacheAndNetwork,
+ queryConfig
+}: IQueryProps): IQueryResultProps {
+ const { client, store } = useMoon();
+ const isInitialMount = React.useRef(true);
+ const clientProps = { source, endPoint, variables, options };
+ const queryId = getQueryId({ id, ...clientProps });
+ const { value, prevValue } = usePrevValue({ queryId, clientProps });
+ const resolvedQueryConfig = React.useMemo(() => store.getResolvedQueryConfig(queryId, queryConfig), [
+ store,
+ queryId,
+ queryConfig
+ ]);
+
+ const cacheOnly = fetchPolicy === FetchPolicy.CacheFirst;
+ const networkOnly = fetchPolicy === FetchPolicy.NetworkOnly;
+
+ if (isInitialMount.current && networkOnly) {
+ // remove cache if networkOnly
+ store.setQueryData(queryId, queryConfig?.initialData);
+ }
+
+ function cancel() {
+ store.cancelQueries(queryId, { exact: true });
+ }
+
+ function fetch() {
+ const cachedResult = store.getQueryData(queryId);
+ return cacheOnly && cachedResult
+ ? cachedResult
+ : client.query(source, endPoint, variables, options);
+ }
+
+ const queryResult = useReactQuery(queryId, fetch, {
+ ...queryConfig,
+ cacheTime: networkOnly ? 0 : queryConfig?.cacheTime,
+ // default values to false
+ refetchOnReconnect: queryConfig?.refetchOnReconnect || false,
+ refetchOnWindowFocus: queryConfig?.refetchOnWindowFocus || false
+ });
+
+ const { clear, fetchMore, refetch, remove, ...others } = queryResult;
+
+ React.useEffect(() => {
+ if (prevValue.queryId === value.queryId && !isInitialMount.current && resolvedQueryConfig?.enabled) {
+ // refetch on update and when only client options have been changed
+ refetch();
+ }
+ }, [value.clientProps]);
+
+ React.useEffect(() => {
+ isInitialMount.current = false;
+ }, []);
+
+ React.useEffect(() => {
+ return () => {
+ if (networkOnly) {
+ store.setQueryData(queryId, queryConfig?.initialData);
+ }
+ };
+ }, [store, queryId, queryConfig]);
+
+ return [{ clear, fetchMore, refetch, remove, cancel }, others];
+}
diff --git a/packages/moon/src/query.tsx b/packages/moon/src/query.tsx
new file mode 100644
index 0000000..004af95
--- /dev/null
+++ b/packages/moon/src/query.tsx
@@ -0,0 +1,99 @@
+import * as React from "react";
+import { QueryResult } from "react-query";
+
+import { PropsWithForwardRef, Nullable } from "./typing";
+import useQuery, { FetchPolicy, IQueryProps } from "./query-hook";
+import { useQueriesResults, ResultProps, useQueryResult, QueriesResults } from "./hooks";
+
+export interface IQueryChildrenProps
+ extends Omit, "clear" | "fetchMore" | "refetch" | "remove"> {
+ actions: Pick, "clear" | "fetchMore" | "refetch" | "remove">;
+}
+
+export type QueryChildren = (
+ props: IQueryChildrenProps
+ // eslint-disable-next-line no-undef
+) => Nullable;
+
+export interface IQueryComponentProps
+ extends IQueryProps {
+ children?: QueryChildren;
+}
+
+function Query(
+ props: IQueryComponentProps
+ // eslint-disable-next-line no-undef
+): Nullable {
+ const { children, ...queryProps } = props;
+ const [actions, state] = useQuery(queryProps);
+ return children ? children({ ...state, actions }) : null;
+}
+
+Query.defaultProps = {
+ fetchPolicy: FetchPolicy.CacheAndNetwork
+};
+
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+export function withQueryResult(
+ queryId: string,
+ resutToProps?: (result?: Data) => QueryResultProps
+) {
+ type QueryProps = QueryResultProps | { queryResult: Data | undefined };
+ type WrappedComponentPropsWithoutQuery = Pick>;
+ return (WrappedComponent: React.ComponentClass | React.FunctionComponent) => {
+ type WrappedComponentInstance = typeof WrappedComponent extends React.ComponentClass
+ ? InstanceType>
+ : ReturnType>;
+ const WithQueryComponent: React.FunctionComponent,
+ WrappedComponentInstance
+ >> = props => {
+ const { forwardedRef, ...rest } = props;
+ const queryResult = useQueryResult(queryId, resutToProps);
+ const queryProps: QueryProps = resutToProps
+ ? (queryResult as QueryResultProps)
+ : { queryResult: queryResult as Data | undefined };
+ const componentProps = ({ ...queryProps, ...((rest as unknown) as WrappedComponentPropsWithoutQuery) } as unknown) as Props;
+ return ;
+ };
+
+ return React.forwardRef((props, ref) => {
+ // @ts-ignore I don't know how to implement this without breaking out of the types.
+ return ;
+ });
+ };
+}
+
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+export function withQueriesResults(
+ queriesIds: string[],
+ resultsToProps?: (results: QueriesResults) => QueryResultProps
+) {
+ type QueryProps = QueryResultProps | { queriesResults: QueriesResults };
+ type WrappedComponentPropsWithoutQuery = Pick>;
+
+ return (WrappedComponent: React.ComponentClass | React.FunctionComponent) => {
+ type WrappedComponentInstance = typeof WrappedComponent extends React.ComponentClass
+ ? InstanceType>
+ : ReturnType>;
+ const WithQueryComponent: React.FunctionComponent> = props => {
+ const { forwardedRef, ...rest } = props;
+ const queriesResults = useQueriesResults(queriesIds, resultsToProps);
+ const componentProps = ({
+ queriesResults,
+ ...((rest as unknown) as WrappedComponentPropsWithoutQuery)
+ } as unknown) as Props;
+ return ;
+ };
+
+ return React.forwardRef((props, ref) => {
+ // @ts-ignore I don't know how to implement this without breaking out of the types.
+ return ;
+ });
+ };
+}
+
+export default Query;
diff --git a/src/typing.ts b/packages/moon/src/typing.ts
similarity index 78%
rename from src/typing.ts
rename to packages/moon/src/typing.ts
index 0c5e9c8..a350c9c 100644
--- a/src/typing.ts
+++ b/packages/moon/src/typing.ts
@@ -1,3 +1,5 @@
+import * as React from "react";
+
export type Nullable = P | null;
export type PropsWithForwardRef
= P & { forwardedRef?: React.RefObject };
diff --git a/packages/moon/src/utils/client.ts b/packages/moon/src/utils/client.ts
new file mode 100644
index 0000000..bba9e60
--- /dev/null
+++ b/packages/moon/src/utils/client.ts
@@ -0,0 +1,77 @@
+/* eslint-disable import/prefer-default-export */
+
+import { QueryCache } from "react-query";
+
+interface InterceptorManagerUseParams {
+ onFulfilled?: (value: V) => V | Promise;
+ onRejected?: (error: any) => any;
+}
+
+export interface IInterceptors {
+ request?: InterceptorManagerUseParams[];
+ response?: InterceptorManagerUseParams[];
+}
+
+export interface IClients {
+ [id: string]: I;
+}
+
+export type ClientFactory = (
+ config?: C,
+ interceptors?: IInterceptors
+) => I;
+
+export interface ILink {
+ id: string;
+ config?: C;
+ interceptors?: IInterceptors;
+ clientFactory?: ClientFactory;
+}
+
+export type DataTransformer = (data: any) => any;
+
+export interface ClientConfig {
+ baseURL?: string;
+ params?: any;
+}
+
+export interface ClientInterceptorManager {
+ use(onFulfilled?: (value: V) => V | Promise, onRejected?: (error: any) => any): number;
+ eject(id: number): void;
+}
+
+export interface ClientInstance {
+ interceptors?: {
+ request?: ClientInterceptorManager;
+ response?: ClientInterceptorManager;
+ };
+ get(url: string, config?: ClientConfig): Promise;
+ delete(url: string, config?: ClientConfig): Promise;
+ post(url: string, data?: any, config?: ClientConfig): Promise;
+ put(url: string, data?: any, config?: ClientConfig): Promise;
+}
+
+let queryCache: QueryCache;
+
+export function getClients(links: ILink[], clientFactory?: ClientFactory): IClients {
+ return links.reduce((clients, link) => {
+ const linkClientFactory = link.clientFactory || clientFactory;
+ if (!linkClientFactory) {
+ throw new Error("A link client factory must be defined!");
+ }
+ clients[link.id] = linkClientFactory(link.config, link.interceptors);
+ return clients;
+ }, {});
+}
+
+export function getMoonStore(store?: QueryCache): QueryCache {
+ if (store) {
+ queryCache = store;
+ return store;
+ }
+ if (queryCache) {
+ return queryCache;
+ }
+ queryCache = new QueryCache();
+ return queryCache;
+}
diff --git a/packages/moon/src/utils/common.ts b/packages/moon/src/utils/common.ts
new file mode 100644
index 0000000..5c5b6c4
--- /dev/null
+++ b/packages/moon/src/utils/common.ts
@@ -0,0 +1,93 @@
+/* eslint-disable import/prefer-default-export */
+
+const hasOwn = Object.prototype.hasOwnProperty;
+
+export const getId = (params: Record): string => {
+ return params.id !== undefined ? params.id : stableStringify(params);
+};
+
+export function stableStringify(value: any): string {
+ return JSON.stringify(value, stableStringifyReplacer).replace(/(\r\n|\n|\r| )/gm, "");
+}
+
+export function equal(objA: any, objB: any, deep = false): boolean {
+ if (is(objA, objB)) return true;
+
+ if (typeof objA !== "object" || objA === null || typeof objB !== "object" || objB === null) {
+ return false;
+ }
+
+ const keysA = Object.keys(objA);
+ const keysB = Object.keys(objB);
+
+ if (keysA.length !== keysB.length) return false;
+ for (let i = 0; i < keysA.length; i++) {
+ if (!hasOwn.call(objB, keysA[i]) || !compare(objA[keysA[i]], objB[keysA[i]], deep)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function compare(objA: any, objB: any, deep: boolean) {
+ return deep ? equal(objA, objB, deep) : is(objA, objB);
+}
+
+function is(x: any, y: any) {
+ if (x === y) {
+ return x !== 0 || y !== 0 || 1 / x === 1 / y;
+ }
+ return x !== x && y !== y;
+}
+
+// Copied from: https://github.com/jonschlinkert/is-plain-object
+// eslint-disable-next-line @typescript-eslint/ban-types
+function isPlainObject(o: any): o is Object {
+ if (!hasObjectPrototype(o)) {
+ return false;
+ }
+
+ // If has modified constructor
+ const ctor = o.constructor;
+ if (typeof ctor === "undefined") {
+ return true;
+ }
+
+ // If has modified prototype
+ const prot = ctor.prototype;
+ if (!hasObjectPrototype(prot)) {
+ return false;
+ }
+
+ // If constructor does not have an Object-specific method
+ // eslint-disable-next-line no-prototype-builtins
+ if (!prot.hasOwnProperty("isPrototypeOf")) {
+ return false;
+ }
+
+ // Most likely a plain Object
+ return true;
+}
+
+function hasObjectPrototype(o: any): boolean {
+ return Object.prototype.toString.call(o) === "[object Object]";
+}
+
+//Adapted from: https://github.com/tannerlinsley/react-query/blob/master/src/core/utils.ts
+function stableStringifyReplacer(_key: string, value: any): unknown {
+ if (typeof value === "function") {
+ return "function";
+ }
+
+ if (isPlainObject(value)) {
+ return Object.keys(value)
+ .sort()
+ .reduce((result, key) => {
+ result[key] = JSON.stringify(value[key], stableStringifyReplacer);
+ return result;
+ }, {} as any);
+ }
+
+ return value;
+}
diff --git a/src/utils/index.ts b/packages/moon/src/utils/index.ts
similarity index 50%
rename from src/utils/index.ts
rename to packages/moon/src/utils/index.ts
index ba0d0c5..b5e92a8 100644
--- a/src/utils/index.ts
+++ b/packages/moon/src/utils/index.ts
@@ -1,2 +1,2 @@
-export * from "./axios";
export * from "./common";
+export * from "./client";
diff --git a/test/integration/hooks.test.tsx b/packages/moon/test/integration/hooks.test.tsx
similarity index 51%
rename from test/integration/hooks.test.tsx
rename to packages/moon/test/integration/hooks.test.tsx
index 73e8ce2..7f26206 100644
--- a/test/integration/hooks.test.tsx
+++ b/packages/moon/test/integration/hooks.test.tsx
@@ -1,16 +1,15 @@
-/* eslint-disable prefer-destructuring */
///
import * as React from "react";
import { renderHook } from "@testing-library/react-hooks";
import { render } from "@testing-library/react";
+import { QueryState } from "react-query/types/core/query";
import { usePrevValue, useQueryResult, useQueriesResults, useQueryState, useQueriesStates } from "../../src/hooks";
-import { AxiosClient, mockAxiosClientConstructor } from "../testUtils";
-import MoonProvider from "../../src/moon-provider";
import useQuery from "../../src/query-hook";
+import MoonProvider from "../../src/moon-provider";
import { links } from "../moon-client.test";
-import { QueryState } from "../../src/store";
import { withQueryResult, withQueriesResults } from "../../src/query";
+import { getMockedClientFactory, MockedClientConfig } from "../testUtils";
interface QueryData {
users: { id: number; name: string }[];
@@ -25,77 +24,96 @@ const response = {
};
interface Props {
- queryId: QueryState;
+ prop: string;
+ queryResult: QueryState;
}
-const MyComponent: React.FunctionComponent = ({ queryId }) => {
- return {!queryId ? "Loading" : "Success"};
+const MyComponent: React.FunctionComponent = ({ queryResult }) => {
+ return {!queryResult ? "Loading" : "Success"};
};
-const WithQueryResultComponent = withQueryResult, QueryData>("queryId")(MyComponent);
+const WithQueryResultComponent = withQueryResult("queryId")(MyComponent);
-const WithQueriesResultsComponent = withQueriesResults, QueryData>(["queryId"])(MyComponent);
+interface PropsResults {
+ prop: string;
+ queriesResults: { queryId: QueryState };
+}
+
+const MyResultsComponent: React.FunctionComponent = ({ queriesResults }) => {
+ return {!queriesResults.queryId ? "Loading" : "Success"};
+};
+
+const WithQueriesResultsComponent = withQueriesResults(["queryId"])(MyResultsComponent);
describe("Hooks", () => {
test("should render the query result with withQueryResult HOC", async () => {
const get = jest.fn().mockImplementation(() => Promise.resolve(response));
- class CustomAxiosClient extends AxiosClient {
- constructor(baseUrl: string) {
- super(baseUrl);
- this.get = get;
- }
- }
-
- mockAxiosClientConstructor(CustomAxiosClient);
+ const clientFactory = getMockedClientFactory({ get });
- const wrapper = ({ children }: { children?: any }) => {children};
+ const wrapper = ({ children }: { children?: any }) => (
+
+ {children}
+
+ );
const variables = { foo: "bar" };
const { waitForNextUpdate } = renderHook(
- () => useQuery({ id: "queryId", source: "FOO", endPoint: "/users", variables }),
+ () =>
+ useQuery({
+ id: "queryId",
+ source: "FOO",
+ endPoint: "/users",
+ variables
+ }),
{ wrapper }
);
await waitForNextUpdate();
- const { getByText } = render(, { wrapper });
+ const { getByText } = render(, { wrapper });
expect(getByText(/Success/)).toBeTruthy();
});
test("should render the query result with withQueriesResults HOC", async () => {
const get = jest.fn().mockImplementation(() => Promise.resolve(response));
- class CustomAxiosClient extends AxiosClient {
- constructor(baseUrl: string) {
- super(baseUrl);
- this.get = get;
- }
- }
-
- mockAxiosClientConstructor(CustomAxiosClient);
+ const clientFactory = getMockedClientFactory({ get });
- const wrapper = ({ children }: { children?: any }) => {children};
+ const wrapper = ({ children }: { children?: any }) => (
+
+ {children}
+
+ );
const variables = { foo: "bar" };
const { waitForNextUpdate } = renderHook(
- () => useQuery({ id: "queryId", source: "FOO", endPoint: "/users", variables }),
+ () =>
+ useQuery({
+ id: "queryId",
+ source: "FOO",
+ endPoint: "/users",
+ variables
+ }),
{ wrapper }
);
await waitForNextUpdate();
- const { getByText } = render(, { wrapper });
+ const { getByText } = render(, { wrapper });
expect(getByText(/Success/)).toBeTruthy();
});
test("should render the query result with useQueryResult", async () => {
const get = jest.fn().mockImplementation(() => Promise.resolve(response));
- class CustomAxiosClient extends AxiosClient {
- constructor(baseUrl: string) {
- super(baseUrl);
- this.get = get;
- }
- }
-
- mockAxiosClientConstructor(CustomAxiosClient);
+ const clientFactory = getMockedClientFactory({ get });
- const wrapper = ({ children }: { children?: any }) => {children};
+ const wrapper = ({ children }: { children?: any }) => (
+
+ {children}
+
+ );
const variables = { foo: "bar" };
const { waitForNextUpdate } = renderHook(
- () => useQuery({ id: "myQuery", source: "FOO", endPoint: "/users", variables }),
+ () =>
+ useQuery({
+ id: "myQuery",
+ source: "FOO",
+ endPoint: "/users",
+ variables
+ }),
{ wrapper }
);
await waitForNextUpdate();
@@ -108,24 +126,27 @@ describe("Hooks", () => {
test("should render the query result with useQueriesResults ", async () => {
const get = jest.fn().mockImplementation(() => Promise.resolve(response));
- class CustomAxiosClient extends AxiosClient {
- constructor(baseUrl: string) {
- super(baseUrl);
- this.get = get;
- }
- }
+ const clientFactory = getMockedClientFactory({ get });
- mockAxiosClientConstructor(CustomAxiosClient);
-
- const wrapper = ({ children }: { children?: any }) => {children};
+ const wrapper = ({ children }: { children?: any }) => (
+
+ {children}
+
+ );
const variables = { foo: "bar" };
const { waitForNextUpdate } = renderHook(
- () => useQuery({ id: "myQuery1", source: "FOO", endPoint: "/users", variables }),
+ () =>
+ useQuery({
+ id: "myQuery1",
+ source: "FOO",
+ endPoint: "/users",
+ variables
+ }),
{ wrapper }
);
await waitForNextUpdate();
const { result } = renderHook(
- () => useQueriesResults(["myQuery1"]),
+ () => useQueriesResults }>(["myQuery1"]),
{ wrapper }
);
expect(result.current.myQuery1).toEqual(response);
@@ -133,19 +154,22 @@ describe("Hooks", () => {
test("should render the query result with useQueryState", async () => {
const get = jest.fn().mockImplementation(() => Promise.resolve(response));
- class CustomAxiosClient extends AxiosClient {
- constructor(baseUrl: string) {
- super(baseUrl);
- this.get = get;
- }
- }
+ const clientFactory = getMockedClientFactory({ get });
- mockAxiosClientConstructor(CustomAxiosClient);
-
- const wrapper = ({ children }: { children?: any }) => {children};
+ const wrapper = ({ children }: { children?: any }) => (
+
+ {children}
+
+ );
const variables = { foo: "bar" };
const { waitForNextUpdate } = renderHook(
- () => useQuery({ id: "myQuery2", source: "FOO", endPoint: "/users", variables }),
+ () =>
+ useQuery({
+ id: "myQuery2",
+ source: "FOO",
+ endPoint: "/users",
+ variables
+ }),
{ wrapper }
);
await waitForNextUpdate();
@@ -158,24 +182,27 @@ describe("Hooks", () => {
test("should render the query result with useQueriesStates ", async () => {
const get = jest.fn().mockImplementation(() => Promise.resolve(response));
- class CustomAxiosClient extends AxiosClient {
- constructor(baseUrl: string) {
- super(baseUrl);
- this.get = get;
- }
- }
+ const clientFactory = getMockedClientFactory({ get });
- mockAxiosClientConstructor(CustomAxiosClient);
-
- const wrapper = ({ children }: { children?: any }) => {children};
+ const wrapper = ({ children }: { children?: any }) => (
+
+ {children}
+
+ );
const variables = { foo: "bar" };
const { waitForNextUpdate } = renderHook(
- () => useQuery({ id: "myQuery3", source: "FOO", endPoint: "/users", variables }),
+ () =>
+ useQuery({
+ id: "myQuery3",
+ source: "FOO",
+ endPoint: "/users",
+ variables
+ }),
{ wrapper }
);
await waitForNextUpdate();
const { result } = renderHook(
- () => useQueriesStates(["myQuery3"]),
+ () => useQueriesStates(["myQuery3"]),
{ wrapper }
);
//@ts-ignore myQuery3 can't be undefined
diff --git a/test/integration/moon-provider.test.tsx b/packages/moon/test/integration/moon-provider.test.tsx
similarity index 59%
rename from test/integration/moon-provider.test.tsx
rename to packages/moon/test/integration/moon-provider.test.tsx
index 26c6e15..0a822a1 100644
--- a/test/integration/moon-provider.test.tsx
+++ b/packages/moon/test/integration/moon-provider.test.tsx
@@ -1,12 +1,15 @@
import * as React from "react";
-import { render, wait } from "@testing-library/react";
+import { render, waitFor } from "@testing-library/react";
import MoonProvider, { withMoon, IMoonContextValue } from "../../src/moon-provider";
-
import { links } from "../moon-client.test";
-import { mockAxiosClientConstructor, AxiosClient } from "../testUtils";
+import { getMockedClientFactory } from "../testUtils";
+
+interface IProps extends IMoonContextValue {
+ prop: string;
+}
-const MyComponent: React.FunctionComponent = ({ client }) => {
+const MyComponent: React.FunctionComponent = ({ client }) => {
const [response, setResponse] = React.useState(null);
React.useEffect(() => {
//@ts-ignore can't be null
@@ -17,26 +20,20 @@ const MyComponent: React.FunctionComponent = ({ client }) =>
return {!response ? "Loading" : "Success"};
};
-const WithMoonComponent = withMoon(MyComponent);
+const WithMoonComponent = withMoon(MyComponent);
describe("Custom component withMoon HOC", () => {
test("should render the query response", async () => {
const get = jest.fn().mockImplementation(() => Promise.resolve({ status: true }));
- class CustomAxiosClient extends AxiosClient {
- constructor(baseUrl: string) {
- super(baseUrl);
- this.get = get;
- }
- }
- mockAxiosClientConstructor(CustomAxiosClient);
+ const clientFactory = getMockedClientFactory({ get });
const { getByText } = render(
-
-
+
+
);
expect(getByText(/Loading/)).toBeTruthy();
- await wait(() => getByText(/Success/));
+ await waitFor(() => getByText(/Success/));
expect(get).toHaveBeenCalledTimes(1);
});
});
diff --git a/packages/moon/test/integration/mutation-hook.test.tsx b/packages/moon/test/integration/mutation-hook.test.tsx
new file mode 100644
index 0000000..1f089f2
--- /dev/null
+++ b/packages/moon/test/integration/mutation-hook.test.tsx
@@ -0,0 +1,100 @@
+/* eslint-disable max-classes-per-file */
+/* eslint-disable prefer-destructuring */
+///
+import * as React from "react";
+import { renderHook, act } from "@testing-library/react-hooks";
+
+import MoonProvider from "../../src/moon-provider";
+import useMutation from "../../src/mutation-hook";
+import { links } from "../moon-client.test";
+import { getMockedClientFactory, MockedClientConfig } from "../testUtils";
+
+interface MutationVariables {
+ foo: string;
+}
+
+describe("Mutation hook with MoonProvider", () => {
+ test("should call the mutate action", async () => {
+ const data = {
+ data: { status: true }
+ };
+ const post = jest.fn().mockImplementation(() => Promise.resolve(data));
+ const clientFactory = getMockedClientFactory({ post });
+
+ const onResponse = jest.fn();
+ const wrapper = ({ children }: { children?: any }) => (
+
+ {children}
+
+ );
+ const { result, waitForNextUpdate } = renderHook(
+ () =>
+ useMutation({
+ source: "FOO",
+ endPoint: "/users",
+ variables: { foo: "bar" },
+ mutationConfig: { onSuccess: onResponse }
+ }),
+ { wrapper }
+ );
+ act(() => {
+ const [mutate, { data, isLoading, error }] = result.current;
+ expect(data).toBeUndefined();
+ expect(isLoading).toBeFalsy();
+ expect(error).toBeNull();
+ mutate();
+ });
+ let state = result.current[1];
+ expect(state.data).toBeUndefined();
+ expect(state.isLoading).toBeTruthy();
+ expect(state.error).toBeNull();
+ await waitForNextUpdate();
+ state = result.current[1];
+ expect(state.data).toBe(data);
+ expect(state.isLoading).toBeFalsy();
+ expect(state.error).toBeNull();
+ expect(onResponse).toBeCalledTimes(1);
+ expect(onResponse).toBeCalledWith(data, undefined);
+ });
+
+ test("should render an error", async () => {
+ const error = "Bimm!";
+ const post = jest.fn().mockImplementation(() => Promise.reject(error));
+ const clientFactory = getMockedClientFactory({ post });
+
+ const onError = jest.fn();
+ const wrapper = ({ children }: { children?: any }) => (
+
+ {children}
+
+ );
+ const { result, waitForNextUpdate } = renderHook(
+ () =>
+ useMutation({
+ source: "FOO",
+ endPoint: "/users",
+ variables: { foo: "bar" },
+ mutationConfig: { onError }
+ }),
+ { wrapper }
+ );
+ act(() => {
+ const [mutate, { data, isLoading, error }] = result.current;
+ expect(data).toBeUndefined();
+ expect(isLoading).toBeFalsy();
+ expect(error).toBeNull();
+ mutate();
+ });
+ let state = result.current[1];
+ expect(state.data).toBeUndefined();
+ expect(state.isLoading).toBeTruthy();
+ expect(state.error).toBeNull();
+ await waitForNextUpdate();
+ state = result.current[1];
+ expect(state.data).toBeUndefined();
+ expect(state.isLoading).toBeFalsy();
+ expect(state.error).toBe(error);
+ expect(onError).toBeCalledTimes(1);
+ expect(onError).toBeCalledWith(error, undefined, undefined);
+ });
+});
diff --git a/test/integration/mutation.test.tsx b/packages/moon/test/integration/mutation.test.tsx
similarity index 58%
rename from test/integration/mutation.test.tsx
rename to packages/moon/test/integration/mutation.test.tsx
index 736ff1a..80ef6df 100644
--- a/test/integration/mutation.test.tsx
+++ b/packages/moon/test/integration/mutation.test.tsx
@@ -1,15 +1,12 @@
+/* eslint-disable max-classes-per-file */
///
import * as React from "react";
-import { cleanup, render, wait, fireEvent } from "@testing-library/react";
+import { cleanup, render, fireEvent, waitFor } from "@testing-library/react";
import MoonProvider from "../../src/moon-provider";
import Mutation from "../../src/mutation";
import { links } from "../moon-client.test";
-import { mockAxiosClientConstructor, AxiosClient } from "../testUtils";
-
-interface MutationResponse {
- status: boolean;
-}
+import { getMockedClientFactory, MockedClientConfig } from "../testUtils";
interface MutationVariables {
foo: string;
@@ -21,22 +18,19 @@ describe("Mutation component with MoonProvider", () => {
status: true
};
const post = jest.fn().mockImplementation(() => Promise.resolve(response));
- class CustomAxiosClient extends AxiosClient {
- constructor(baseUrl: string) {
- super(baseUrl);
- this.post = post;
- }
- }
-
- mockAxiosClientConstructor(CustomAxiosClient);
+ const clientFactory = getMockedClientFactory({ post });
const { container, getByText } = render(
-
- source="FOO" endPoint="/users" variables={{ foo: "bar" }}>
- {({ actions: { mutate }, response }) => {
- return response && response.status ? (
+
+
+ source="FOO"
+ endPoint="/users"
+ variables={{ foo: "bar" }}
+ >
+ {({ actions: { mutate }, data }) => {
+ return data && data.status ? (
Success
) : (
-