diff --git a/web/config/setupTests.js b/web/config/setupTests.js
index 546046389..22f663dab 100644
--- a/web/config/setupTests.js
+++ b/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)}`);
+ });
+});
diff --git a/web/src/api/__mocks__/baseUrl.js b/web/src/api/__mocks__/baseUrl.js
new file mode 100644
index 000000000..8d3c67582
--- /dev/null
+++ b/web/src/api/__mocks__/baseUrl.js
@@ -0,0 +1 @@
+export const baseUrl = 'http://base-url.local:5000';
diff --git a/web/src/api/__tests__/index.test.jsx b/web/src/api/__tests__/index.test.jsx
new file mode 100644
index 000000000..393bb382b
--- /dev/null
+++ b/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
{apiHost}
;
+ }
+ render(
+
+
+
+ );
+ expect(screen.queryByText('http://base-url.local:5000')).toBeInTheDocument();
+ });
+});
+
+describe('useFetch', () => {
+ function Test() {
+ const { data, status } = useFetch('/api/tacos');
+ return (
+
+ {data ? data.returnData : ''}
+ {status}
+
+ );
+ }
+ 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(
+
+
+
+ );
+
+ 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(
+
+
+
+ );
+
+ 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(
+
+
+
+ );
+
+ 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(
+
+
+
+ );
+
+ expect(screen.queryByText('loaded')).toBeInTheDocument();
+ expect(screen.queryByText('yep')).toBeInTheDocument();
+
+ jest.runAllTimers();
+
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/web/src/api/baseUrl.js b/web/src/api/baseUrl.js
new file mode 100644
index 000000000..b6c7ea2e9
--- /dev/null
+++ b/web/src/api/baseUrl.js
@@ -0,0 +1 @@
+export const baseUrl = import.meta.env.SNOWPACK_PUBLIC_API_HOST || window.baseUrl || '';
diff --git a/web/src/api/index.jsx b/web/src/api/index.jsx
index eb94ff89f..7df4c5785 100644
--- a/web/src/api/index.jsx
+++ b/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();