From 3c5812800282d5acaa5d6eeab1346f571a1db94a Mon Sep 17 00:00:00 2001 From: Zell Liew Date: Sun, 16 Apr 2023 02:24:29 +0800 Subject: [PATCH] Yay --- README.md | 258 +++++++++++++++++------------- package-lock.json | 1 + package.json | 1 + src/createRequestOptions.js | 138 ++++++++++------ src/index.js | 61 ++++++- test/actual.spec.js | 27 ++-- test/helpers/integration-tests.js | 86 ++++++++-- test/helpers/server.js | 10 +- todos.md | 12 ++ 9 files changed, 414 insertions(+), 180 deletions(-) diff --git a/README.md b/README.md index 425a4e9..d6f7b2c 100644 --- a/README.md +++ b/README.md @@ -2,22 +2,24 @@ zlFetch is a wrapper around fetch that provides you with a convenient way to make requests. +Note: From `v4.0.0` onwards, zlFetch is a ESM library. It cannot be used with CommonJS anymore. + It's features are as follows: -- [Use the response right away](#quick-start) without using `response.json()`, or `response.text()` -- [Get everything you need](#contains-all-data-about-the-response) about your response — headers, body, statuses, and more. -- [Debug your request](#debugging-the-request) without looking at the Network panel -- Shorthand for GET, POST, PUT, PATCH, and DELETE methods -- [Generates query strings automatically](#automatic-generation-of-query-strings) so you don't have to mess with query parameters. -- Sets [`Content-Type` is set to `application/json` by default](#automatic-content-type-generation-and-body-formatting). You can override the `Content-Type` header anytime you need to -- [Body is converted into JSON automatically](#automatic-content-type-generation-and-body-formatting) when Content Type is `application/json` -- [Body is converted into form data automatically](#automatic-content-type-generation-and-body-formatting) when Content Type is `application/x-www-form-urlencoded`. -- [Authorization headers (both basic and bearer) are generated automatically ](#automatic-authorization-header-generation) with an `auth` property. -- [Promise-like error handling](#error-handling) — all 400 and 500 errors are directed into the `catch` block automatically. -- [Easy error handling when using `await`](#error-handling) — errors can be returned so you don't have to write a `try/catch` block. -- [Instances can be created to hold common options](#creating-instances) so you don't have to repeat yourself. +- Quality of life improvements over the native `fetch` function -Note: From `v4.0.0` onwards, zlFetch is a ESM library. It cannot be used with CommonJS anymore. + - [Use the response right away](#quick-start) without using `response.json()`, `response.text()`, or `response.blob()` + - [Promise-like error handling](#error-handling) — all 400 and 500 errors are directed into the `catch` block automatically. + - [Easy error handling when using `await`](#easy-error-handling-when-using-asyncawait) — errors can be returned so you don't have to write a `try/catch` block. + +- Additional improvements over the native `fetch` function + - `Content-Type` headers are set [automatically](#content-type-generation-based-on-body-content) based on the `body` content. + - [Get everything you need](#the-response-contains-all-the-data-you-may-need) about your response — `headers`, `body`, `status`, and more. + - [Debug your request](#debugging-the-request) without looking at the Network panel + - Shorthand for `GET`, `POST`, `PUT`, `PATCH`, and `DELETE` methods + - [Helper for generating query strings](#query-string-helpers) so you don't have to mess with query parameters. + - [Generates authorization headers](#authorization-header-helpers) with an `auth` property. + - [Create instances to hold url and options](#creating-a-zlfetch-instance) so you don't have to repeat yourself. ## Installing zlFetch @@ -56,7 +58,25 @@ zlFetch('url') .catch(error => console.log(error)) ``` -### Contains all data about the response +### Shorthand methods for GET, POST, PUT, PATCH, and DELETE + +zlFetch contains shorthand methods for these common REST methods so you can use them quickly. + +```js +zlFetch.get(/* some-url */) +zlFetch.post(/* some-url */) +zlFetch.put(/* some-url */) +zlFetch.patch(/* some-url */) +zlFetch.delete(/* some-url */) +``` + +### Supported response types + +zlFetch supports `json`, `text`, and `blob` response types so you don't have to write `response.json()`, `response.text()` or `response.blob()`. + +Other response types are not supported right now. If you need to support other response types, consider using your own [response handler](#custom-response-handler) + +### The response contains all the data you may need zlFetch sends you all the data you need in the `response` object. This includes the following: @@ -66,39 +86,64 @@ zlFetch sends you all the data you need in the `response` object. This includes - `statusText`: response status text - `response`: original response from Fetch +We do this so you don't have to fish out the `headers`, `status`, `statusText` or even the rest of the `response` object by yourself. + ### Debugging the request New in `v4.0.0`: You can debug the request object by adding a `debug` option. This will reveal a `debug` object that contains the request being constructed. -- url -- method -- headers -- body +- `url` +- `method` +- `headers` +- `body` ```js zlFetch('url', { debug: true }) .then({ debug } => console.log(debug)) ``` -Note: The `logRequestOptions` option is replaced by the `debug` object in `v4.0.0`. The `logRequestOptions` option is no longer available. +### Error Handling -### Shorthand methods for GET, POST, PUT, PATCH, and DELETE +zlFetch directs all 400 and 500 errors to the `catch` method. Errors contain the same information as a response. -zlFetch contains shorthand methods for these common REST methods so you can use them quickly. +- `headers`: response headers +- `body`: response body +- `status`: response status +- `statusText`: response status text +- `response`: original response from fetch + +This makes is zlFetch super easy to use with promises. ```js -zlFetch.get(/* some-url */) -zlFetch.post(/* some-url */) -zlFetch.put(/* some-url */) -zlFetch.patch(/* some-url */) -zlFetch.delete(/* some-url */) +zlFetch('some-url').catch(error => { + /* Handle error */ +}) + +// The above request can be written in Fetch like this: +fetch('some-url') + .then(response => { + if (!response.ok) { + Promise.reject(response.json) + } + }) + .catch(error => { + /* Handle error */ + }) +``` + +### Easy error handling when using `async`/`await` + +zlFetch lets you pass all errors into an `errors` object. You can do this by adding a `returnError` option. This is useful when you work a lot with `async/await`. + +```js +const { response, error } = await zlFetch('some-url', { returnError: true }) ``` -## Features that help you write less code +## Helpful Features -### Automatic Generation of Query Strings +### Query string helpers -You can add `query` or `queries` as an option and zlFetch will create a query string for you automatically.: +You can add `query` or `queries` as an option and zlFetch will create a query string for you automatically. Use this with `GET` requests. ```js zlFetch('some-url', { @@ -112,18 +157,22 @@ zlFetch('some-url', { fetch('url?param1=value1¶m2=to%20encode') ``` -### Automatic Content-Type Generation and Body Formatting +### `Content-Type` generation based on `body` content + +zlFetch sets `Content-Type` appropriately depending on your `body` data. It supports three kinds of data: -zlFetch sets `Content-Type` to `application/json` for you automatically if your `method` is `POST`, `PUT`, or `PATCH`. +- Object +- Query Strings +- Form Data -It will also help you `JSON.stringify` your body so you don't have to do it yourself. +If you pass in an `object`, zlFetch will set `Content-Type` to `application/json`. It will also `JSON.stringify` your body so you don't have to do it yourself. ```js zlFetch.post('some-url', { body: { message: 'Good game' }, }) -// The request above can be written in Fetch like this: +// The above request is equivalent to this fetch('some-url', { method: 'post', headers: { 'Content-Type': 'application/json' }, @@ -131,26 +180,56 @@ fetch('some-url', { }) ``` -You can manually set your `Content-Type` to other values and zlFetch will honour the value you set. +zlFetch contains a `toObject` helper that lets you convert Form Data into an object. This makes it super easy to zlFetch with forms. + +```js +import { toObject } from 'zl-fetch' +const data = new FormData(form.elements) + +zlFetch('some-url', { + body: toObject(data), +}) +``` + +If you pass in a string, zlFetch will set `Content-Type` to `application/x-www-form-urlencoded`. -If you set `Content-Type` to `application/x-www-form-urlencoded`, zlFetch will automatically format your body to `x-www-form-urlencoded` for you. +zlFetch also contains a `toQueryString` method that can help you convert objects to query strings so you can use this option easily. ```js +import { toQueryString } from 'zl-fetch' + zlFetch.post('some-url', { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: { message: 'Good game' }, + body: toQueryString({ message: 'Good game' }), }) -// The request above can be written in Fetch like this: +// The above request is equivalent to this fetch('some-url', { method: 'post', - body: 'message=Good+game', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: 'message=Good%20game', }) ``` -### Automatic Authorization Header Generation +If you pass in a Form Data, zlFetch will let the native `fetch` function handle the `Content-Type`. Generally, this will use `multipart/form-data` with the default options. If you use this, make sure your server can receive `multipart/form-data`! + +```js +import { toObject } from 'zl-fetch' +const data = new FormData(form.elements) + +zlFetch('some-url', { body: data }) + +// The above request is equivalent to this +fetch('some-url', { body: data }) + +// Your server should be able to receive multipart/form-data if you do this. If you're using Express, you can a middleware like multer to make this possible: +import multer from 'multer' +const upload = multer() +app.use(upload.array()) +``` + +**Breaking Change in `v5.0.0`**: If you pass in a `Content-Type` header, zlFetch will not set format your body content anymore. We expect you to be able to pass in the correct data type. (We had to do this to support the new API mentioned above). + +### Authorization header helpers If you provide zlFetch with an `auth` property, it will generate an Authorization Header for you. @@ -181,65 +260,53 @@ fetch('some-url', { }); ``` -## Error Handling +### Creating a zlFetch Instance -zlFetch directs all 400 and 500 errors to the `catch` method. Errors contain the same information as a response. +You can create an instance of `zlFetch` with predefined options. This is super helpful if you need to send requests with similar `options` or `url`. -- `headers`: response headers -- `body`: response body -- `status`: response status -- `statusText`: response status text -- `response`: original response from fetch - -This makes is zlFetch super easy to use with promises. +- `url` is required +- `options` is optional ```js -zlFetch('some-url').catch(error => { - /* Handle error */ -}) +import { createZLFetch } from 'zl-fetch' -// The above request can be written in Fetch like this: -fetch('some-url') - .then(response => { - if (!response.ok) { - Promise.reject(response.json) - } - }) - .catch(error => { - /* Handle error */ - }) +// Creating the instance +const api = zlFetch(baseUrl, options) ``` -zlFetch also gives you the option to pass all errors into an `errors` object instead of handling them in `catch`. This option is very much preferred when you don't your errors to be passed into a catch method. (Very useful when used in servers). +All instances have shorthand methods as well. ```js -const { response, error } = await zlFetch('some-url') +// Shorthand methods +const response = api.get(/* ... */) +const response = api.post(/* ... */) +const response = api.put(/* ... */) +const response = api.patch(/* ... */) +const response = api.delete(/* ... */) ``` -`zlFetch` changes the response and error objects. In zlFetch, `response` and `error` objects both include these five properties: +New in `v5.0.0` -1. `headers`: response headers -2. `body`: response body -3. `status`: response status -4. `statusText`: response status text -5. `response`: original response from fetch +You can now use a `zlFetch` instance without passing a URL. This is useful if you have created an instance with the right endpoints. ```js -zlFetch('url') - .then(response => { - const headers = response.headers - const body = response.body - }) - .catch(error => { - const headers = error.headers - const body = error.body - const status = error.status - }) +import { createZLFetch } from 'zl-fetch' + +// Creating the instance +const api = zlFetch(baseUrl, options) ``` -## Handling other Response Types +All instances have shorthand methods as well. + +```js +// Shorthand methods +const response = api.get() // Without URL, without options +const response = api.get('some-url') // With URL, without options +const response = api.post('some-url', { body: 'message=good+game' }) // With URL, with options +const response = api.post({ body: 'message=good+game' }) // Without URL, with options +``` -zlFetch only supports `json`,`blob`, and `text` response types at this point. (PRs welcome if you want to help zlFetch handle more response types!). +### Custom response handler If you want to handle a response not supported by zlFetch, you can pass `customResponseParser: true` into the options. This returns the response from a normal Fetch request without any additional treatments from zlFetch. You can then use `response.json()` or other methods as you deem fit. @@ -249,26 +316,3 @@ const response = await zlFetch('url', { }) const data = await response.arrayBuffer() ``` - -## Creating a zlFetch instance - -This feature is super useful if you are going to send requests with the similar options url or options. - -```js -import { createZLFetch } from 'zl-fetch' - -// Creating the instance -const api = zlFetch(baseUrl, options) - -// Using the created instance -const response = api.post('/resource', { - body: { - message: 'Hello', - }, -}) -``` - -A few notes here: - -- `baseURL` will be prepended to the `url` for all requests made with the instance. -- `options` will be merged with the options passed into request. The request options will override the instance options. diff --git a/package-lock.json b/package-lock.json index 18e381c..c10bef3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@vitest/ui": "^0.23.4", "body-parser": "^1.20.0", "express": "^4.18.1", + "form-data": "^4.0.0", "jsdom": "^20.0.0", "np": "^7.6.2", "portastic": "^1.0.1", diff --git a/package.json b/package.json index e7eec76..79fa79e 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@vitest/ui": "^0.23.4", "body-parser": "^1.20.0", "express": "^4.18.1", + "form-data": "^4.0.0", "jsdom": "^20.0.0", "np": "^7.6.2", "portastic": "^1.0.1", diff --git a/src/createRequestOptions.js b/src/createRequestOptions.js index bf1ede8..124a7a8 100644 --- a/src/createRequestOptions.js +++ b/src/createRequestOptions.js @@ -1,11 +1,18 @@ -export default function createRequestOptions (options = {}) { +export default function createRequestOptions(options = {}) { const opts = Object.assign({}, options) + const fetchHeaders = options.fetch.Headers + + // Note: headers get mutated after setHeaders is called. + // This is why we get the content type here to know what the user originally set. + const headers = new fetchHeaders(options.headers) + const userContentType = headers.get('content-type') opts.url = setUrl(opts) opts.method = setMethod(opts) opts.headers = setHeaders(opts) - opts.body = setBody(opts) + // If Content Type is set explicitly, we expect the user to pass in the appropriate data. So we don't treat the body + opts.body = !userContentType ? setBody(opts) : opts.body return opts } @@ -13,11 +20,11 @@ export default function createRequestOptions (options = {}) { * Appends queries to URL * @param {Object} opts */ -function setUrl (options) { +function setUrl(options) { const { url, queries, query } = options // Merge queries and query for easier use - // So users don't have to remember singluar or plural forms + // So users don't have to remember singular or plural forms const q = Object.assign({}, queries, query) if (isEmptyObject(q)) return options.url @@ -26,7 +33,7 @@ function setUrl (options) { return `${url}?${searchParams.toString()}` } -function isEmptyObject (obj) { +function isEmptyObject(obj) { return ( obj && // 👈 null and undefined check Object.keys(obj).length === 0 && @@ -34,55 +41,87 @@ function isEmptyObject (obj) { ) } -function setMethod (options) { +function setMethod(options) { // Method set to GET by default unless otherwise specified const method = options.method || 'get' return method } -function setHeaders (options) { +// ======================== +// Set Headers +// We set the headers depending on the request body and method +// ======================== +function setHeaders(options) { const fetchHeaders = options.fetch.Headers - const headers = new fetchHeaders(options.headers) + let headers = new fetchHeaders(options.headers) + headers = contentTypeHeader(options, headers) + headers = authHeader(options, headers) + + return headers +} +function contentTypeHeader(options, headers) { // For preflight requests, we don't want to set headers. // This allows requests to remain simple. if (options.method === 'options') return headers - // Set headers to Content-Type: application/json by default - // We set this only for POST, PUT, PATCH, DELETE so GET requests can remain simple. - if (!headers.get('content-type') && options.method !== 'get') { + // For GET requests, we also don't want to set headers. + // This allows requests to remain simple. + if (options.method === 'get') return headers + + // If a content type is aleady set, we return the content type as is. + // This allows users to set their own content type. + if (headers.get('content-type')) return headers + + // If the body is a string, we assume it's a query string. + // So we set headers to Content-Type: application/x-www-form-urlencoded. + if (typeof options.body === 'string') { + headers.set('content-type', 'application/x-www-form-urlencoded') + return headers + } + + // If the body is an object and not FormData, we set it to `application/json`. Checking for FormData is important here because Form Data requires another content type. + if (typeof options.body === 'object' && !isFormData(options.body)) { headers.set('content-type', 'application/json') + return headers } - // Create Authorization Headers if the auth option is present - if (!options.auth) return headers + // What's left here is FormData. + // We don't set the content type here because fetch will set it automatically. + return headers +} +// Sets the auth headers as necessary +function authHeader(options, headers) { + // If no auth options, means we don't have to set Authorization headers const { auth } = options - const btoa = getBtoa() + if (!auth) return headers - // We help to create Basic Authentication when users pass in username and password fields. - if (typeof auth === 'object') { - let { username, password } = auth - if (!username) { - throw new Error( - 'Please fill in your username to create an Authorization Header for Basic Authentication' - ) - } - - // Password field can be empty for implicit grant - if (!password) password = '' - - const encodedValue = btoa(`${username}:${password}`) - headers.set('Authorization', `Basic ${encodedValue}`) - } else { - // We help to create Bearer Authentication when the user passes a token into the `auth` option. + // Set Bearer authentication when user passes in a string into auth + if (typeof auth === 'string') { headers.set('Authorization', `Bearer ${auth}`) + return headers } + // Set Basic Authentication headers when user passes in username and password into auth. + const btoa = getBtoa() + + // Password field can be empty for implicit grant + const { username, password = '' } = auth + + if (!username) { + throw new Error( + 'Username required to create Authorization Header for a Basic Authentication' + ) + } + + const encodedValue = btoa(`${username}:${password}`) + headers.set('Authorization', `Basic ${encodedValue}`) return headers } -export function getBtoa () { +// Gets Btoa for creating basic auth. +export function getBtoa() { if (typeof window !== 'undefined' && window.btoa) { return window.btoa } @@ -92,26 +131,35 @@ export function getBtoa () { } } -function setBody (options) { - // If it is a GET request, we return an empty value because GET requests don't use the body property. +// ======================== +// Set Body +// ======================== +function setBody(options) { + // Return empty body for preflight requests const method = options.method - if (method === 'get') return + if (['get', 'head', 'options'].includes(method)) return - // If the content type is not specified, we ignore the body field so we can return a simple request for preflight checks - const contentType = options.headers.get('content-type') - if (!contentType) return + // If the body is form data, we return it as it is because users will have to set it up appropriately themselves. + if (isFormData(options.body)) return options.body - // If the content type is x-www-form-urlencoded, we format the body with a query string. - if (contentType.includes('x-www-form-urlencoded')) { - const searchParams = new URLSearchParams(options.body) - return searchParams.toString() - } + // If the body is a string, we assume it's a query string. + // We ask users to use toQueryString to convert an object to a query string as they send in the request. + if (typeof options.body === 'string') return options.body - // If the content type is JSON, we stringify the body. - if (contentType.includes('json')) { + // If it's an object, we convert it to JSON + if (typeof options.body === 'object') { return JSON.stringify(options.body) } - // If the above conditions don't trigger, we return the body as is, just in case. + // If the above conditions don't trigger, we return the body as is. + // One example that will reach here is FormData return options.body } + +// Checks if body is FormData +// Only works on browsers. +// Returns false in Node because Node doesn't have FormData. +export function isFormData(body) { + if (typeof window === 'undefined') return false + return body instanceof FormData +} diff --git a/src/index.js b/src/index.js index b3b0878..cf08046 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,6 @@ -import { handleError, handleResponse } from './handleResponse.js' - /* globals fetch */ -import createRequestOptions from './createRequestOptions.js' +import createRequestOptions, { isFormData } from './createRequestOptions.js' +import { handleError, handleResponse } from './handleResponse.js' /** * Main zlFetch Function @@ -30,8 +29,8 @@ for (const method of methods) { // Creates an instance of zlFetch to be used later export function createZlFetch(baseURL, options) { - const fn = function (url, newOptions) { - url = makeURL(baseURL, url) + const fn = function (...args) { + const { url, newOptions } = normalize(args) return fetchInstance({ url, ...options, ...newOptions }) } @@ -39,17 +38,40 @@ export function createZlFetch(baseURL, options) { const methods = ['get', 'post', 'put', 'patch', 'delete'] for (const method of methods) { - fn[method] = function (url, newOptions) { - url = makeURL(baseURL, url) + fn[method] = function (...args) { + const { url, newOptions } = normalize(args) return fetchInstance({ url, method, ...options, ...newOptions }) } } + // Normalize the URL and options + // Allows user to use the created zlFetch item without passing in further URLs. + // Naming can be improved, but can't think of a better name for now + function normalize(args = []) { + const [arg1, arg2] = args + + // This means no options are given. So we simply use the baseURL as the URL + if (!arg1) return { url: baseURL } + + // If the firs argument is an object, it means there are options but the user didn't pass in a URL. + if (typeof arg1 === 'object') return { url: baseURL, newOptions: arg1 } + + // The leftover possibility is that the first argument is a string. + // In this case we need to make a new URL with this argument. + const url = makeURL(baseURL, arg1) + + // Wwe need to check whether the second argument is an object or not. If arg2 is undefined, then we simply return the URL since there are no options + if (!arg2) return { url } + + // The only possibility left is that arg2 is an object, which means there are new options. + return { url, newOptions: arg2 } + } + return fn } // Joins the baseURL and endpoint. -// Uses a simple string concatenation instead of path.join +// Uses a simple string concatenation instead of path.join so it works in the browser. function makeURL(baseURL, url) { if (baseURL.endsWith('/') && url.startsWith('/')) { url = url.slice(1) @@ -111,3 +133,26 @@ function debugHeaders(requestOptions) { clone.headers = headers return clone } + +/** + * Converts Form Data into an object + * @param {FormData} formData + * @returns Object + */ +export function toJSON(formData) { + const obj = {} + for (const data of formData) { + obj[data[0]] = data[1] + } + return obj +} + +/** + * Converts object into a query string + * @param {Object} object + * @returns + */ +export function toQueryString(object) { + const searchParams = new URLSearchParams(object) + return searchParams.toString() +} diff --git a/test/actual.spec.js b/test/actual.spec.js index b4450da..cefce8f 100644 --- a/test/actual.spec.js +++ b/test/actual.spec.js @@ -1,14 +1,21 @@ -import { expect, it } from 'vitest' +import { describe, expect, it } from 'vitest' + import zlFetch from '../src/index.js' -// Sending an actual test to Github to ensure zlFetch works -it('Test sending to Github', async () => { - const response = await zlFetch('https://api.github.com/users/zellwk/repos') - expect(response.status).toBe(200) - expect(response.body.length === 30) -}) +describe('Actual Tests', _ => { + // Sending an actual test to Github to ensure zlFetch works + it('Test sending to Github', async () => { + const response = await zlFetch('https://api.github.com/users/zellwk/repos') + expect(response.status).toBe(200) + expect(response.body.length === 30) + }) + + it('Test with Open Dota API (preflight)', async () => { + const response = await zlFetch('https://api.opendota.com/api/heroStats') + expect(response.status).toBe(200) + }) -it('Test with Open Dota API (preflight)', async () => { - const response = await zlFetch('https://api.opendota.com/api/heroStats') - expect(response.status).toBe(200) + it.todo( + 'Test with FormData — Find a way to test this because we need a browser environment.' + ) }) diff --git a/test/helpers/integration-tests.js b/test/helpers/integration-tests.js index 9fb2f68..a9582c9 100644 --- a/test/helpers/integration-tests.js +++ b/test/helpers/integration-tests.js @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' -import zlFetch, { createZlFetch } from '../../src/index.js' +import zlFetch, { createZlFetch, toQueryString } from '../../src/index.js' +import FormData from 'form-data' import { getBtoa } from '../../src/createRequestOptions.js' export default function tests(environment) { @@ -61,16 +62,55 @@ export default function tests(environment) { expect(queries[1]).toBe('toEncode=http%3A%2F%2Fgoogle.com') }) - it('POST with x-www-form-urlencoded', async ({ endpoint }) => { + it('POST explicit content-type: application/json', async ({ endpoint }) => { + // This one should fail because the body is not an object + const { response, error } = await zlFetch.post(`${endpoint}/body`, { + method: 'post', + headers: { 'Content-Type': 'application/json' }, + body: toQueryString({ message: 'good game' }), + returnError: true, + }) + + expect(response).toBe(null) + expect(error.status).toBe(400) + expect(error.statusText).toBe('Bad Request') + expect(error.body.message).toContain(/SyntaxError: Unexpected token/) + }) + + it('POST with x-www-form-urlencoded data', async ({ endpoint }) => { const response = await zlFetch.post(`${endpoint}/body`, { method: 'post', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: { message: 'good game' }, + body: toQueryString({ message: 'good game' }), }) expect(response.status).toBe(200) expect(response.body.message).toBe('good game') }) + + it('POST explicit content-type: x-www-form-urlencoded', async ({ + endpoint, + }) => { + // This one will send "fail" because the body is not a query string. + // It will not return an error in this test because of how we configured the backend. But the data will be `object Object: ''` instead of `message=good+game` + const { response, error, debug } = await zlFetch.post( + `${endpoint}/body`, + { + method: 'post', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: { message: 'good game' }, + returnError: true, + debug: true, + } + ) + + // This is just how we have configured our backend + expect(response.status).toBe(200) + expect(response.body).toEqual({ 'object Object': '' }) + expect(debug.body).toEqual({ message: 'good game' }) + }) + + it.todo('POST with Form Data') + it.todo(`POST with Content Type set to 'x-www-form-urlencoded`) }) describe(`Receiving Responses (from ${environment})`, context => { @@ -125,8 +165,9 @@ export default function tests(environment) { }) it('Shorthand PUT request', async ({ endpoint }) => { - const response = await zlFetch.put(`${endpoint}/body`, { + const { response } = await zlFetch.put(`${endpoint}/body`, { body: { message: 'good game' }, + returnError: true, }) expect(response.status).toBe(200) @@ -277,16 +318,21 @@ describe('Create zlFetch object', _ => { it('Shorthand POST request', async ({ endpoint }) => { const created = createZlFetch(endpoint) - const response = await created.post('createZlFetch') + const response = await created.post('createZlFetch', { + body: { message: 'good game' }, + }) - expect(response.body).toBe('Hello World') + expect(response.body.message).toBe('good game') expect(response.status).toBe(200) }) it('Shorthand PUT request', async ({ endpoint }) => { const created = createZlFetch(endpoint) - const response = await created.put('createZlFetch') - expect(response.body).toBe('Hello World') + const response = await created.put('createZlFetch', { + body: { message: 'good game' }, + }) + + expect(response.body.message).toBe('good game') expect(response.status).toBe(200) }) @@ -318,3 +364,25 @@ describe('Create zlFetch object', _ => { expect(body.message).toBe('Hello World') }) }) + +describe('Created zlFetch Object', _ => { + it('Should not require a URL', async ({ endpoint }) => { + // Because the URL can already be set in createZlFetch itself + const created = createZlFetch(`${endpoint}/createZlFetch/`) + const response = await created() + // const response2 = await created('something') + // const response3 = await created('something', { body: 'haha' }) + // const response4 = await created({ body: 'haha' }) + expect(response.body).toBe('Hello World') + expect(response.status).toBe(200) + }) + + it('Can have options without URL', async ({ endpoint }) => { + // Because the URL can already be set in createZlFetch itself + const created = createZlFetch(`${endpoint}/createZlFetch/`) + const response = await created.post({ body: { message: 'haha' } }) + + expect(response.body.message).toBe('haha') + expect(response.status).toBe(200) + }) +}) diff --git a/test/helpers/server.js b/test/helpers/server.js index fef23ef..0c69f6f 100644 --- a/test/helpers/server.js +++ b/test/helpers/server.js @@ -18,10 +18,18 @@ app.get('/', (req, res) => { }) // For testing createZlFetch -app.use('/createZlFetch', (req, res) => { +app.get('/createZlFetch', (req, res) => { + res.json('Hello World') +}) + +app.delete('/createZlFetch', (req, res) => { res.json('Hello World') }) +app.use('/createZlFetch', (req, res) => { + res.json(req.body) +}) + // Returns the req.body app.use('/body', (req, res) => { res.json(req.body) diff --git a/todos.md b/todos.md index d522dfa..9eedf0a 100644 --- a/todos.md +++ b/todos.md @@ -1,3 +1,15 @@ # Improvements - Support ability to abort fetch calls with Abort controller +- createZlFetch should be able to create a default URL so we don't have to call that url again + +# Add to readme and changelog + +- Changelog + + - Setting Content-Type headers is no longer required. We will now automatically set the Content-Type header based on the body type. + - If you set Content-Type, you're expected to send in the correct body content yourself. + +- Working with FormData + - You can send it. But make sure your server is able to receive it. Since FormData is multipart/form-data with boundaries. Show example in Express or something. + - Get Form Data and send it => Use a to toObject transformer because zlFetch automatically converts object to JSON.