Bläddra i källkod

test(web): api/index.jsx

Paul Armstrong 4 år sedan
förälder
incheckning
6d133ef724

+ 8 - 0
web/config/setupTests.js

@@ -12,3 +12,11 @@ Object.defineProperty(window, 'matchMedia', {
     dispatchEvent: jest.fn(),
   }),
 });
+
+window.fetch = () => Promise.resolve();
+
+beforeEach(() => {
+  jest.spyOn(window, 'fetch').mockImplementation(async (url, opts = {}) => {
+    throw new Error(`Unexpected fetch to ${url}, ${JSON.stringify(opts)}`);
+  });
+});

+ 1 - 0
web/src/api/__mocks__/baseUrl.js

@@ -0,0 +1 @@
+export const baseUrl = 'http://base-url.local:5000';

+ 116 - 0
web/src/api/__tests__/index.test.jsx

@@ -0,0 +1,116 @@
+import { h } from 'preact';
+import { ApiProvider, useFetch, useApiHost } from '..';
+import { render, screen } from '@testing-library/preact';
+
+jest.mock('../baseUrl');
+
+describe('useApiHost', () => {
+  test('is set from the baseUrl', async () => {
+    function Test() {
+      const apiHost = useApiHost();
+      return <div>{apiHost}</div>;
+    }
+    render(
+      <ApiProvider>
+        <Test />
+      </ApiProvider>
+    );
+    expect(screen.queryByText('http://base-url.local:5000')).toBeInTheDocument();
+  });
+});
+
+describe('useFetch', () => {
+  function Test() {
+    const { data, status } = useFetch('/api/tacos');
+    return (
+      <div>
+        <span>{data ? data.returnData : ''}</span>
+        <span>{status}</span>
+      </div>
+    );
+  }
+  test('loads data', async () => {
+    const fetchSpy = jest.spyOn(window, 'fetch').mockImplementation(
+      (url) =>
+        new Promise((resolve) => {
+          setTimeout(() => {
+            resolve({ ok: true, json: () => Promise.resolve({ returnData: 'yep' }) });
+          }, 1);
+        })
+    );
+
+    render(
+      <ApiProvider>
+        <Test />
+      </ApiProvider>
+    );
+
+    expect(screen.queryByText('loading')).toBeInTheDocument();
+    expect(screen.queryByText('yep')).not.toBeInTheDocument();
+
+    jest.runAllTimers();
+    await screen.findByText('loaded');
+    expect(fetchSpy).toHaveBeenCalledWith('http://base-url.local:5000/api/tacos');
+
+    expect(screen.queryByText('loaded')).toBeInTheDocument();
+    expect(screen.queryByText('yep')).toBeInTheDocument();
+  });
+
+  test('sets error if response is not okay', async () => {
+    jest.spyOn(window, 'fetch').mockImplementation(
+      (url) =>
+        new Promise((resolve) => {
+          setTimeout(() => {
+            resolve({ ok: false });
+          }, 1);
+        })
+    );
+
+    render(
+      <ApiProvider>
+        <Test />
+      </ApiProvider>
+    );
+
+    expect(screen.queryByText('loading')).toBeInTheDocument();
+    jest.runAllTimers();
+    await screen.findByText('error');
+  });
+
+  test('does not re-fetch if the query has already been made', async () => {
+    const fetchSpy = jest.spyOn(window, 'fetch').mockImplementation(
+      (url) =>
+        new Promise((resolve) => {
+          setTimeout(() => {
+            resolve({ ok: true, json: () => Promise.resolve({ returnData: 'yep' }) });
+          }, 1);
+        })
+    );
+
+    const { rerender } = render(
+      <ApiProvider>
+        <Test key={0} />
+      </ApiProvider>
+    );
+
+    expect(screen.queryByText('loading')).toBeInTheDocument();
+    expect(screen.queryByText('yep')).not.toBeInTheDocument();
+
+    jest.runAllTimers();
+    await screen.findByText('loaded');
+    expect(fetchSpy).toHaveBeenCalledWith('http://base-url.local:5000/api/tacos');
+
+    rerender(
+      <ApiProvider>
+        <Test key={1} />
+      </ApiProvider>
+    );
+
+    expect(screen.queryByText('loaded')).toBeInTheDocument();
+    expect(screen.queryByText('yep')).toBeInTheDocument();
+
+    jest.runAllTimers();
+
+    expect(fetchSpy).toHaveBeenCalledTimes(1);
+  });
+});

+ 1 - 0
web/src/api/baseUrl.js

@@ -0,0 +1 @@
+export const baseUrl = import.meta.env.SNOWPACK_PUBLIC_API_HOST || window.baseUrl || '';

+ 10 - 7
web/src/api/index.jsx

@@ -1,9 +1,8 @@
+import { baseUrl } from './baseUrl';
 import { h, createContext } from 'preact';
 import produce from 'immer';
 import { useContext, useEffect, useReducer } from 'preact/hooks';
 
-export const ApiHost = createContext(import.meta.env.SNOWPACK_PUBLIC_API_HOST || window.baseUrl || '');
-
 export const FetchStatus = {
   NONE: 'none',
   LOADING: 'loading',
@@ -12,11 +11,11 @@ export const FetchStatus = {
 };
 
 const initialState = Object.freeze({
-  host: import.meta.env.SNOWPACK_PUBLIC_API_HOST || window.baseUrl || '',
+  host: baseUrl,
   queries: {},
 });
-export const Api = createContext(initialState);
-export default Api;
+
+const Api = createContext(initialState);
 
 function reducer(state, { type, payload, meta }) {
   switch (type) {
@@ -65,8 +64,12 @@ export function useFetch(url, fetchId) {
     async function fetchData() {
       await dispatch({ type: 'REQUEST', payload: { url, fetchId } });
       const response = await fetch(`${state.host}${url}`);
-      const data = await response.json();
-      await dispatch({ type: 'RESPONSE', payload: { url, ok: response.ok, data, fetchId } });
+      try {
+        const data = await response.json();
+        await dispatch({ type: 'RESPONSE', payload: { url, ok: response.ok, data, fetchId } });
+      } catch (e) {
+        await dispatch({ type: 'RESPONSE', payload: { url, ok: false, data: null, fetchId } });
+      }
     }
 
     fetchData();