-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #47 from mbland/request-class
Add frontend request module wrapping fetch()
- Loading branch information
Showing
3 changed files
with
136 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
/* | ||
* This Source Code Form is subject to the terms of the Mozilla Public | ||
* License, v. 2.0. If a copy of the MPL was not distributed with this | ||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
*/ | ||
|
||
/** | ||
* Posts the data from a <form> via fetch() and returns the response object | ||
* @see https://simonplend.com/how-to-use-fetch-to-post-form-data-as-json-to-your-api/ | ||
* @param {FormData} form - form containing data to POST | ||
* @returns {Promise<any>} - response from the server | ||
*/ | ||
export async function postForm(form) { | ||
return post(form.action, Object.fromEntries(new FormData(form).entries())) | ||
} | ||
|
||
/** | ||
* Posts an object payload via fetch() and returns the response object | ||
* @param {string} url - address of server request | ||
* @param {object} payload - data to include in the POST request | ||
* @returns {Promise<any>} - response from the server | ||
*/ | ||
export async function post(url, payload) { | ||
const res = await fetch(url, postOptions(payload)) | ||
const body = await res.text() | ||
|
||
if (body.startsWith('{') && body.includes('"error":')) { | ||
throw new Error(JSON.parse(body).error) | ||
} else if (!res.ok) { | ||
const msg = body.length !== 0 ? body : `${res.status}: ${res.statusText}` | ||
throw new Error(msg) | ||
} | ||
return JSON.parse(body) | ||
} | ||
|
||
/** | ||
* Prepares the fetch() options for an application/json POST request | ||
* @param {object} payload - data to include in the POST request options | ||
* @returns {object} - an options object for a fetch() POST request | ||
*/ | ||
export function postOptions(payload) { | ||
return { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json', | ||
Accept: 'application/json' | ||
}, | ||
body: JSON.stringify(payload) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
/* eslint-env browser, node, jest, vitest */ | ||
/* | ||
* This Source Code Form is subject to the terms of the Mozilla Public | ||
* License, v. 2.0. If a copy of the MPL was not distributed with this | ||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
*/ | ||
import { post, postForm, postOptions } from './request' | ||
import { afterEach, describe, expect, test, vi } from 'vitest' | ||
|
||
// @vitest-environment jsdom | ||
describe('Request', () => { | ||
const req = { want: 'foo' } | ||
|
||
const setupFetchStub = (body, options) => { | ||
const fetchStub = vi.fn() | ||
|
||
fetchStub.mockReturnValueOnce(Promise.resolve(new Response(body, options))) | ||
vi.stubGlobal('fetch', fetchStub) | ||
return fetchStub | ||
} | ||
|
||
afterEach(() => { vi.unstubAllGlobals() }) | ||
|
||
describe('post', () => { | ||
test('succeeds', async () => { | ||
const res = { foo: 'bar' } | ||
const fetchStub = setupFetchStub(JSON.stringify(res)) | ||
|
||
await expect(post('/fetch', req)).resolves.toEqual(res) | ||
expect(fetchStub).toHaveBeenCalledWith('/fetch', postOptions(req)) | ||
}) | ||
|
||
test('rejects with an error if the response contains "error"', async () => { | ||
const res = { error: 'OK status, but still an error' } | ||
setupFetchStub(JSON.stringify(res)) | ||
|
||
await expect(post('/fetch', req)).rejects.toThrow(res.error) | ||
}) | ||
|
||
test('rejects with an error if the response status is not OK', async () => { | ||
const res = 'totally our fault' | ||
setupFetchStub(res, { status: 500 }) | ||
|
||
await expect(post('/fetch', req)).rejects.toThrow(res) | ||
}) | ||
|
||
test('rejects with default status text if no response body', async () => { | ||
setupFetchStub('', { status: 500, statusText: 'Internal Server Error' }) | ||
|
||
await expect(post('/fetch', req)) | ||
.rejects.toThrow('500: Internal Server Error') | ||
}) | ||
}) | ||
|
||
describe('postForm', () => { | ||
test('succeeds', async () => { | ||
// We have to be careful creating the <form>, because form.action resolves | ||
// differently depending on how we created it. | ||
// | ||
// Originally I tried creating it using fragment() from '../test/helpers', | ||
// which creates elements using a new <template> containing a | ||
// DocumentFragment. However, the elements in that DocumentFragment are in | ||
// a separate DOM. This caused the <form action="/fetch"> attribute to be: | ||
// | ||
// - '/fetch' in jsdom | ||
// - '' in Chrome | ||
// - `#{document.location.origin}/fetch` in Firefox | ||
// | ||
// Creating a <form> element via document.createElement() as below | ||
// causes form.action to become `#{document.location.origin}/fetch` in | ||
// every environment. | ||
const form = document.createElement('form') | ||
const resolvedAction = `${document.location.origin}/fetch` | ||
const res = { foo: 'bar' } | ||
const fetchStub = setupFetchStub(JSON.stringify(res)) | ||
|
||
form.action = '/fetch' | ||
form.innerHTML = '<input type="text" name="want" id="want" value="foo" />' | ||
|
||
expect(form.action).toBe(resolvedAction) | ||
await expect(postForm(form)).resolves.toEqual(res) | ||
expect(fetchStub).toHaveBeenCalledWith(resolvedAction, postOptions(req)) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters