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();