diff --git a/CHANGELOG.md b/CHANGELOG.md index 935eeea..cc4579b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +### v2.0.4 (2018/4/20) + +**Bug Fixes** + +* Fixes a bug where there could be a cache mismatch when re-rendering the same component + that has a fetch policy configured. + ### v2.0.3 (2018/3/2) **Bug Fixes** diff --git a/README.md b/README.md index 3ef7359..697ecb8 100644 --- a/README.md +++ b/README.md @@ -225,8 +225,9 @@ There are three common use cases for the `doFetch` prop: is passed as `true`. `doFetch` accepts one argument: `options`. Any of the `fetch()` options, such as `url`, `method`, and -`body` are valid `options`. This allows you to customize the request from within the component based -on the component's state. +`body` are valid `options`. You may also specify a new `requestKey` if you are manually generating your +own keys. This method allows you to customize the request from within the component based on the +component's state. ##### `lazy` diff --git a/package.json b/package.json index fbddcc2..bf99492 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-request", - "version": "2.0.3", + "version": "2.0.4", "description": "Declarative HTTP requests with React.", "main": "lib/index.js", "module": "es/index.js", diff --git a/src/fetch.js b/src/fetch.js index b7eb7e3..1e53c6c 100644 --- a/src/fetch.js +++ b/src/fetch.js @@ -7,14 +7,22 @@ import { getRequestKey, fetchDedupe, isRequestInFlight } from 'fetch-dedupe'; // The value of each key is a Response instance let responseCache = {}; +// The docs state that this is not safe to use in an +// application. That's just because I am not writing tests, +// nor designing the API, around folks clearing the cache. +// This was only added to help out with testing your app. +// Use your judgment if you decide to use this in your +// app directly. export function clearResponseCache() { responseCache = {}; } export class Fetch extends React.Component { render() { - const { children, requestName, url } = this.props; - const { fetching, response, data, error, requestKey } = this.state; + // Anything pulled from `this.props` here is not eligible to be + // specified when calling `doFetch`. + const { children, requestName } = this.props; + const { fetching, response, data, error, requestKey, url } = this.state; if (!children) { return null; @@ -49,7 +57,8 @@ export class Fetch extends React.Component { fetching: false, response: null, data: null, - error: null + error: null, + url: props.url }; } @@ -95,22 +104,27 @@ export class Fetch extends React.Component { } } - componentWillReceiveProps(nextProps) { + // Because we use `componentDidUpdate` to determine if we should fetch + // again, there will be at least one render when you receive your new + // fetch options, such as a new URL, but the fetch has not begun yet. + componentDidUpdate(prevProps) { const currentRequestKey = this.props.requestKey || getRequestKey({ ...this.props, method: this.props.method.toUpperCase() }); - const nextRequestKey = - nextProps.requestKey || + const prevRequestKey = + prevProps.requestKey || getRequestKey({ - ...nextProps, - method: this.props.method.toUpperCase() + ...prevProps, + method: prevProps.method.toUpperCase() }); - if (currentRequestKey !== nextRequestKey && !this.isLazy(nextProps)) { - this.fetchData(nextProps); + if (currentRequestKey !== prevRequestKey && !this.isLazy(prevProps)) { + this.fetchData({ + requestKey: currentRequestKey + }); } } @@ -124,6 +138,8 @@ export class Fetch extends React.Component { cancelExistingRequest = reason => { if (this.state.fetching && !this.hasHandledNetworkResponse) { const abortError = new Error(reason); + // This is an effort to mimic the error that is created when a + // fetch is actually aborted using the AbortController API. abortError.name = 'AbortError'; this.onResponseReceived({ ...this.responseReceivedInfo, @@ -145,11 +161,57 @@ export class Fetch extends React.Component { }); }; + // When a subsequent request is made, it is important that the correct + // request key is used. This method computes the right key based on the + // options and props. + getRequestKey = options => { + // A request key in the options gets top priority + if (options && options.requestKey) { + return options.requestKey; + } + + // Otherwise, if we have no request key, but we do have options, then we + // recompute the request key based on these options. + // Note that if the URL, body, or method have not changed, then the request + // key should match the previous request key if it was computed. + // If you passed in a custom request key as a prop, then you will also + // need to pass in a custom key when you call `doFetch()`! + else if (options) { + const { url, method, body } = Object.assign({}, this.props, options); + return getRequestKey({ + url, + body, + method: method.toUpperCase() + }); + } + + // Next in line is the the request key from props. + else if (this.props.requestKey) { + return this.props.requestKey; + } + + // Lastly, we compute the request key from the props. + else { + const { url, method, body } = this.props; + + return getRequestKey({ + url, + body, + method: method.toUpperCase() + }); + } + }; + fetchData = (options, ignoreCache) => { + // These are the things that we do not allow a user to configure in + // `options` when calling `doFetch()`. Perhaps we should, however. const { requestName, dedupe, beforeFetch } = this.props; this.cancelExistingRequest('New fetch initiated'); + const requestKey = this.getRequestKey(options); + const requestOptions = Object.assign({}, this.props, options); + const { url, body, @@ -165,16 +227,7 @@ export class Fetch extends React.Component { integrity, keepalive, signal - } = Object.assign({}, this.props, options); - - // We need to compute a new key, just in case a new value was passed in `doFetch`. - const requestKey = - this.props.requestKey || - getRequestKey({ - url, - method: method.toUpperCase(), - body - }); + } = requestOptions; const uppercaseMethod = method.toUpperCase(); const shouldCacheResponse = this.shouldCacheResponse(); @@ -205,6 +258,7 @@ export class Fetch extends React.Component { // If the request config changes, we need to be able to accurately // cancel the in-flight request. this.responseReceivedInfo = responseReceivedInfo; + this.hasHandledNetworkResponse = false; const fetchPolicy = this.getFetchPolicy(); @@ -238,8 +292,11 @@ export class Fetch extends React.Component { } this.setState({ - fetching: true, - requestKey + requestKey, + url, + error: null, + failed: false, + fetching: true }); const hittingNetwork = !isRequestInFlight(requestKey) || !dedupe; @@ -331,10 +388,12 @@ export class Fetch extends React.Component { this.setState( { + url, data, error, response, - fetching: stillFetching + fetching: stillFetching, + requestKey }, () => this.props.onResponse(error, response) ); diff --git a/test/.eslintrc b/test/.eslintrc index 077076f..83e8b92 100644 --- a/test/.eslintrc +++ b/test/.eslintrc @@ -7,5 +7,8 @@ "ecmaFeatures": { "jsx": true } + }, + "globals": { + "hangingPromise": true } } diff --git a/test/do-fetch.test.js b/test/do-fetch.test.js new file mode 100644 index 0000000..2146656 --- /dev/null +++ b/test/do-fetch.test.js @@ -0,0 +1,412 @@ +import React from 'react'; +import fetchMock from 'fetch-mock'; +import { mount } from 'enzyme'; +import { Fetch, clearRequestCache, clearResponseCache } from '../src'; + +// Some time for the mock fetches to resolve +const networkTimeout = 10; + +beforeEach(() => { + clearRequestCache(); + clearResponseCache(); +}); + +describe('same-component doFetch() with caching (gh-151)', () => { + test('doFetch() with URL and another HTTP method', done => { + // expect.assertions(8); + const onResponseMock = jest.fn(); + const beforeFetchMock = jest.fn(); + const afterFetchMock = jest.fn(); + + let run = 1; + let renderCount = 0; + + mount( + + {options => { + renderCount++; + + // Wait for things to be placed in the cache. + // This occurs on the third render: + // 1st. component mounts + // 2nd. fetch begins + // 3rd. fetch ends + if (run === 1 && renderCount === 3) { + expect(options).toEqual(expect.objectContaining({ + fetching: false, + data: { + books: [1, 42, 150] + }, + error: null, + failed: false, + requestName: 'anonymousRequest', + url: '/test/succeeds/json-one' + })); + + // We need a timeout here to prevent a race condition + // with the assertions after the component mounts. + setTimeout(() => { + run++; + renderCount = 0; + options.doFetch({ + method: 'patch', + url: '/test/succeeds/patch' + }); + + // Now we need another timeout to allow for the fetch + // to occur. + setTimeout(() => { + done(); + }, networkTimeout); + }, networkTimeout * 2); + } + + if (run === 2) { + if (renderCount === 1) { + expect(options).toEqual(expect.objectContaining({ + fetching: true, + data: { + books: [1, 42, 150] + }, + error: null, + failed: false, + requestName: 'anonymousRequest', + url: '/test/succeeds/patch' + })); + } + if (renderCount === 2) { + expect(fetchMock.calls('/test/succeeds/patch').length).toBe(1); + expect(options).toEqual(expect.objectContaining({ + fetching: false, + data: { + movies: [1] + }, + error: null, + failed: false, + requestName: 'anonymousRequest', + url: '/test/succeeds/patch' + })); + } + if (renderCount === 3) { + done.fail(); + } + } + }} + + ); + + setTimeout(() => { + // NOTE: this is for adding stuff to the cache. + // This DOES NOT test the cache-only behavior! + expect(fetchMock.calls('/test/succeeds/json-one').length).toBe(1); + expect(beforeFetchMock).toHaveBeenCalledTimes(1); + expect(afterFetchMock).toHaveBeenCalledTimes(1); + expect(afterFetchMock).toBeCalledWith( + expect.objectContaining({ + url: '/test/succeeds/json-one', + error: null, + failed: false, + didUnmount: false, + data: { + books: [1, 42, 150] + } + }) + ); + expect(onResponseMock).toHaveBeenCalledTimes(1); + expect(onResponseMock).toBeCalledWith( + null, + expect.objectContaining({ + ok: true, + status: 200, + statusText: 'OK', + data: { + books: [1, 42, 150] + } + }) + ); + }, networkTimeout); + }); + + test('doFetch() with request key and URL', done => { + // expect.assertions(8); + const onResponseMock = jest.fn(); + const beforeFetchMock = jest.fn(); + const afterFetchMock = jest.fn(); + + let run = 1; + let renderCount = 0; + + mount( + + {options => { + renderCount++; + + // Wait for things to be placed in the cache. + // This occurs on the third render: + // 1st. component mounts + // 2nd. fetch begins + // 3rd. fetch ends + if (run === 1 && renderCount === 3) { + expect(options).toEqual(expect.objectContaining({ + fetching: false, + data: { + books: [1, 42, 150] + }, + error: null, + failed: false, + requestName: 'anonymousRequest', + url: '/test/succeeds/json-one' + })); + + // We need a timeout here to prevent a race condition + // with the assertions after the component mounts. + setTimeout(() => { + run++; + renderCount = 0; + + options.doFetch({ + requestKey: 'sandwiches', + url: '/test/succeeds/json-two' + }); + + // Now we need another timeout to allow for the fetch + // to occur. + setTimeout(() => { + done(); + }, networkTimeout); + }, networkTimeout * 2); + } + + if (run === 2) { + if (renderCount === 1) { + expect(options).toEqual(expect.objectContaining({ + fetching: true, + data: { + books: [1, 42, 150] + }, + error: null, + failed: false, + requestKey: 'sandwiches', + requestName: 'anonymousRequest', + url: '/test/succeeds/json-two' + })); + } + if (renderCount === 2) { + expect(options).toEqual(expect.objectContaining({ + fetching: false, + data: { + authors: [22, 13] + }, + error: null, + failed: false, + requestKey: 'sandwiches', + requestName: 'anonymousRequest', + url: '/test/succeeds/json-two' + })); + } + if (renderCount === 3) { + done.fail(); + } + } + }} + + ); + + setTimeout(() => { + // NOTE: this is for adding stuff to the cache. + // This DOES NOT test the cache-only behavior! + expect(fetchMock.calls('/test/succeeds/json-one').length).toBe(1); + expect(beforeFetchMock).toHaveBeenCalledTimes(1); + expect(afterFetchMock).toHaveBeenCalledTimes(1); + expect(afterFetchMock).toBeCalledWith( + expect.objectContaining({ + url: '/test/succeeds/json-one', + error: null, + failed: false, + didUnmount: false, + data: { + books: [1, 42, 150] + } + }) + ); + expect(onResponseMock).toHaveBeenCalledTimes(1); + expect(onResponseMock).toBeCalledWith( + null, + expect.objectContaining({ + ok: true, + status: 200, + statusText: 'OK', + data: { + books: [1, 42, 150] + } + }) + ); + }, networkTimeout); + }); + + // Note: this does not test dedupe due to the fact that the requests + // resolve too quickly. + test('doFetch(); testing cancelation', done => { + // expect.assertions(8); + const onResponseMock = jest.fn(); + const beforeFetchMock = jest.fn(); + const afterFetchMock = jest.fn(); + + let run = 1; + let renderCount = 0; + + mount( + + {options => { + renderCount++; + + // Wait for things to be placed in the cache. + // This occurs on the third render: + // 1st. component mounts + // 2nd. fetch begins + // 3rd. fetch ends + if (run === 1 && renderCount === 3) { + expect(options).toEqual(expect.objectContaining({ + fetching: false, + data: { + books: [1, 42, 150] + }, + error: null, + failed: false, + requestName: 'anonymousRequest', + url: '/test/succeeds/json-one' + })); + + // We need a timeout here to prevent a race condition + // with the assertions after the component mounts. + setTimeout(() => { + run++; + renderCount = 0; + + options.doFetch({ + requestKey: 'sandwiches', + url: '/test/succeeds/json-two' + }); + + options.doFetch({ + requestKey: 'sandwiches', + url: '/test/succeeds/json-two' + }); + + // Now we need another timeout to allow for the fetch + // to occur. + setTimeout(() => { + done(); + }, networkTimeout); + }, networkTimeout * 2); + } + + if (run === 2) { + if (renderCount === 1) { + expect(options).toEqual(expect.objectContaining({ + fetching: true, + data: { + books: [1, 42, 150] + }, + error: null, + failed: false, + requestKey: 'sandwiches', + requestName: 'anonymousRequest', + url: '/test/succeeds/json-two' + })); + } + if (renderCount === 2) { + expect(options).toEqual(expect.objectContaining({ + fetching: false, + // I am not sure if I like this behavior! + // See gh-154 for more + data: null, + failed: true, + requestKey: 'sandwiches', + requestName: 'anonymousRequest', + url: '/test/succeeds/json-two' + })); + } + + // This is the 2nd doFetch(). It is difficult to update + // the `run` for that fetch, so we just use the renderCounts. + else if (renderCount === 3) { + expect(options).toEqual(expect.objectContaining({ + fetching: true, + data: null, + error: null, + failed: false, + requestKey: 'sandwiches', + requestName: 'anonymousRequest', + url: '/test/succeeds/json-two' + })); + } + + else if (renderCount === 4) { + expect(options).toEqual(expect.objectContaining({ + fetching: false, + data: { + authors: [22, 13] + }, + error: null, + failed: false, + requestKey: 'sandwiches', + requestName: 'anonymousRequest', + url: '/test/succeeds/json-two' + })); + } + if (renderCount > 4) { + done.fail(); + } + } + }} + + ); + + setTimeout(() => { + // NOTE: this is for adding stuff to the cache. + // This DOES NOT test the cache-only behavior! + expect(fetchMock.calls('/test/succeeds/json-one').length).toBe(1); + expect(beforeFetchMock).toHaveBeenCalledTimes(1); + expect(afterFetchMock).toHaveBeenCalledTimes(1); + expect(afterFetchMock).toBeCalledWith( + expect.objectContaining({ + url: '/test/succeeds/json-one', + error: null, + failed: false, + didUnmount: false, + data: { + books: [1, 42, 150] + } + }) + ); + expect(onResponseMock).toHaveBeenCalledTimes(1); + expect(onResponseMock).toBeCalledWith( + null, + expect.objectContaining({ + ok: true, + status: 200, + statusText: 'OK', + data: { + books: [1, 42, 150] + } + }) + ); + }, networkTimeout); + }); +}); \ No newline at end of file diff --git a/test/index.test.js b/test/index.test.js index f3954a8..a617e86 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -8,27 +8,6 @@ import { clearResponseCache } from '../src'; import { successfulResponse, jsonResponse } from './responses'; -import { setTimeout } from 'timers'; - -function hangingPromise() { - return new Promise(() => {}); -} - -fetchMock.get('/test/hangs', hangingPromise()); -fetchMock.get('/test/hangs/1', hangingPromise()); -fetchMock.get('/test/hangs/2', hangingPromise()); -fetchMock.post('/test/hangs', hangingPromise()); -fetchMock.put('/test/hangs', hangingPromise()); -fetchMock.patch('/test/hangs', hangingPromise()); -fetchMock.head('/test/hangs', hangingPromise()); -fetchMock.delete('/test/hangs', hangingPromise()); - -// This could be improved by adding the URL to the JSON response -fetchMock.get('/test/succeeds', () => { - return new Promise(resolve => { - resolve(jsonResponse()); - }); -}); let success = true; // This could be improved by adding the URL to the JSON response @@ -46,30 +25,6 @@ fetchMock.get('/test/variable', () => { } }); -fetchMock.get( - '/test/succeeds/cache-only-empty', - () => - new Promise(resolve => { - resolve(successfulResponse()); - }) -); - -fetchMock.get( - '/test/succeeds/cache-only-full', - () => - new Promise(resolve => { - resolve(jsonResponse()); - }) -); - -fetchMock.post( - '/test/succeeds/cache-only-full', - () => - new Promise(resolve => { - resolve(jsonResponse()); - }) -); - // Some time for the mock fetches to resolve const networkTimeout = 10; diff --git a/test/responses.js b/test/responses.js index fe3b840..ffe0ae1 100644 --- a/test/responses.js +++ b/test/responses.js @@ -11,3 +11,17 @@ export function jsonResponse() { statusText: 'OK' }); } + +export function jsonResponse2() { + return new Response('{"authors": [22, 13]}', { + status: 200, + statusText: 'OK' + }); +} + +export function jsonResponse3() { + return new Response('{"movies": [1]}', { + status: 200, + statusText: 'OK' + }); +} \ No newline at end of file diff --git a/test/same-component.test.js b/test/same-component.test.js new file mode 100644 index 0000000..0d6f2b7 --- /dev/null +++ b/test/same-component.test.js @@ -0,0 +1,308 @@ +import React from 'react'; +import fetchMock from 'fetch-mock'; +import { mount } from 'enzyme'; +import { Fetch, clearRequestCache, clearResponseCache } from '../src'; + +// Some time for the mock fetches to resolve +const networkTimeout = 10; + +beforeEach(() => { + clearRequestCache(); + clearResponseCache(); +}); + +// Issue 151 describes the 3 situations when requests can be made. This tests +// situation 2. +describe('same-component subsequent requests with caching (gh-151)', () => { + test('it uses a directly-updated request key on subsequent renders', done => { + // expect.assertions(8); + const onResponseMock = jest.fn(); + const beforeFetchMock = jest.fn(); + const afterFetchMock = jest.fn(); + + let run = 1; + let renderCount = 0; + + const wrapper = mount( + + {options => { + if (run === 2) { + // Increment our render count. This allows us to + // test for each of the individual renders involved + // with changing the prop. + renderCount++; + + // This first render is interesting: we basically only have a + // new URL set, but the request has not yet begun. The reason + // for this is because we do the fetch in `componentDidUpdate`. + if (renderCount === 1) { + expect(options).toEqual( + expect.objectContaining({ + requestKey: '1', + fetching: false, + data: { + books: [1, 42, 150] + }, + error: null, + failed: false, + url: '/test/succeeds/json-one' + }) + ); + } else if (renderCount === 2) { + expect(options).toEqual( + expect.objectContaining({ + requestKey: '2', + fetching: true, + data: { + books: [1, 42, 150] + }, + error: null, + failed: false, + url: '/test/succeeds/json-two' + }) + ); + } else if (renderCount === 3) { + expect(options).toEqual( + expect.objectContaining({ + requestKey: '2', + fetching: false, + data: { + authors: [22, 13] + }, + error: null, + failed: false, + url: '/test/succeeds/json-two' + }) + ); + } else if (renderCount > 3) { + done.fail(); + } + } + }} + + ); + + setTimeout(() => { + // NOTE: this is for adding stuff to the cache. + // This DOES NOT test the cache-only behavior! + expect(fetchMock.calls('/test/succeeds/json-one').length).toBe(1); + expect(beforeFetchMock).toHaveBeenCalledTimes(1); + expect(afterFetchMock).toHaveBeenCalledTimes(1); + expect(afterFetchMock).toBeCalledWith( + expect.objectContaining({ + url: '/test/succeeds/json-one', + error: null, + failed: false, + didUnmount: false, + data: { + books: [1, 42, 150] + } + }) + ); + expect(onResponseMock).toHaveBeenCalledTimes(1); + expect(onResponseMock).toBeCalledWith( + null, + expect.objectContaining({ + ok: true, + status: 200, + statusText: 'OK', + data: { + books: [1, 42, 150] + } + }) + ); + + run = 2; + wrapper.setProps({ + url: '/test/succeeds/json-two', + requestKey: '2' + }); + + // We do a network timeout here to ensure that the `expect` within + // render is called a second time. + setTimeout(() => { + done(); + }, networkTimeout); + }, networkTimeout); + }); + + test('it uses an indirectly-updated request key on subsequent renders', done => { + // expect.assertions(10); + const onResponseMock = jest.fn(); + const beforeFetchMock = jest.fn(); + const afterFetchMock = jest.fn(); + + let run = 1; + let renderCount = 0; + + const wrapper = mount( + + {(options) => { + renderCount++; + if (run === 1) { + if (renderCount === 1) { + expect(options).toEqual(expect.objectContaining({ + fetching: false, + data: null, + error: null, + failed: false, + response: null, + requestName: 'anonymousRequest', + url: '/test/succeeds/json-one' + })); + } else if (renderCount === 2) { + expect(options).toEqual(expect.objectContaining({ + fetching: true, + data: null, + error: null, + failed: false, + response: null, + requestName: 'anonymousRequest', + url: '/test/succeeds/json-one' + })); + } else if (renderCount === 3) { + expect(options).toEqual(expect.objectContaining({ + fetching: false, + data: { + books: [1, 42, 150] + }, + error: null, + failed: false, + requestName: 'anonymousRequest', + url: '/test/succeeds/json-one' + })); + } else if (renderCount > 3) { + done.fail(); + } + } + + else if (run === 2) { + if (renderCount === 1) { + expect(options).toEqual(expect.objectContaining({ + fetching: false, + data: { + books: [1, 42, 150] + }, + error: null, + failed: false, + requestName: 'anonymousRequest', + url: '/test/succeeds/json-one' + })); + } else if (renderCount === 2) { + expect(options).toEqual(expect.objectContaining({ + fetching: true, + data: { + books: [1, 42, 150] + }, + error: null, + failed: false, + requestName: 'anonymousRequest', + url: '/test/succeeds/json-two' + })); + } else if (renderCount === 3) { + expect(options).toEqual(expect.objectContaining({ + fetching: false, + data: { + authors: [22, 13] + }, + error: null, + failed: false, + requestName: 'anonymousRequest', + url: '/test/succeeds/json-two' + })); + } else if (renderCount > 3) { + done.fail(); + } + } + + else if (run === 3) { + if (renderCount === 1) { + expect(options).toEqual(expect.objectContaining({ + fetching: false, + data: { + authors: [22, 13] + }, + error: null, + failed: false, + requestName: 'anonymousRequest', + url: '/test/succeeds/json-two' + })); + } + else if (renderCount === 2) { + expect(options).toEqual(expect.objectContaining({ + fetching: false, + data: { + books: [1, 42, 150] + }, + error: null, + failed: false, + requestName: 'anonymousRequest', + url: '/test/succeeds/json-one' + })); + } else if (renderCount > 2) { + done.fail(); + } + } + }} + + ); + + setTimeout(() => { + // NOTE: this is for adding stuff to the cache. + // This DOES NOT test the cache-only behavior! + expect(fetchMock.calls('/test/succeeds/json-one').length).toBe(1); + expect(beforeFetchMock).toHaveBeenCalledTimes(1); + expect(afterFetchMock).toHaveBeenCalledTimes(1); + expect(afterFetchMock).toBeCalledWith( + expect.objectContaining({ + url: '/test/succeeds/json-one', + error: null, + failed: false, + didUnmount: false, + data: { + books: [1, 42, 150] + } + }) + ); + expect(onResponseMock).toHaveBeenCalledTimes(1); + expect(onResponseMock).toBeCalledWith( + null, + expect.objectContaining({ + ok: true, + status: 200, + statusText: 'OK', + data: { + books: [1, 42, 150] + } + }) + ); + + run = 2; + renderCount = 0; + wrapper.setProps({ + url: '/test/succeeds/json-two' + }); + + setTimeout(() => { + run = 3; + renderCount = 0; + wrapper.setProps({ + url: '/test/succeeds/json-one' + }); + + setTimeout(() => { + done(); + }, 500); + }, 500); + }, networkTimeout); + }); +}); diff --git a/test/setup.js b/test/setup.js index ef58578..46e3654 100644 --- a/test/setup.js +++ b/test/setup.js @@ -2,6 +2,9 @@ import 'isomorphic-fetch'; import fetchMock from 'fetch-mock'; import Enzyme from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; +import { + successfulResponse, jsonResponse, jsonResponse2, jsonResponse3 +} from './responses'; Enzyme.configure({ adapter: new Adapter() }); @@ -9,6 +12,74 @@ Enzyme.configure({ adapter: new Adapter() }); // an error. global.AbortSignal = function() {}; +var hangingPromise = global.hangingPromise = function() { + return new Promise(() => {}); +} + +fetchMock.get('/test/hangs', hangingPromise()); +fetchMock.get('/test/hangs/1', hangingPromise()); +fetchMock.get('/test/hangs/2', hangingPromise()); +fetchMock.post('/test/hangs', hangingPromise()); +fetchMock.put('/test/hangs', hangingPromise()); +fetchMock.patch('/test/hangs', hangingPromise()); +fetchMock.head('/test/hangs', hangingPromise()); +fetchMock.delete('/test/hangs', hangingPromise()); + +// This could be improved by adding the URL to the JSON response +fetchMock.get('/test/succeeds', () => { + return new Promise(resolve => { + resolve(jsonResponse()); + }); +}); + +fetchMock.get( + '/test/succeeds/cache-only-empty', + () => + new Promise(resolve => { + resolve(successfulResponse()); + }) +); + +fetchMock.get( + '/test/succeeds/cache-only-full', + () => + new Promise(resolve => { + resolve(jsonResponse()); + }) +); + +fetchMock.post( + '/test/succeeds/cache-only-full', + () => + new Promise(resolve => { + resolve(jsonResponse()); + }) +); + +fetchMock.get( + '/test/succeeds/json-one', + () => + new Promise(resolve => { + resolve(jsonResponse()); + }) +); + +fetchMock.get( + '/test/succeeds/json-two', + () => + new Promise(resolve => { + resolve(jsonResponse2()); + }) +); + +fetchMock.patch( + '/test/succeeds/patch', + () => + new Promise(resolve => { + resolve(jsonResponse3()); + }) +); + // We do this at the start of each test, just in case a test // replaces the global fetch and does not reset it beforeEach(() => {