diff --git a/README.md b/README.md index bd2cf5d..a6afaae 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ There are few packages those help the developers to mock the backend requests wh | `status` | All possible HTTP status codes. | - | `200` | | `response` | JSON format or function.

Response function is a function that contains request object as a parameter. See the **Custom Response** section for example. | Y | - | | `delay` | Emulate delayed response time in milliseconds. | - | `0` | +| `uploadFrameCount` | Emulate response progress, progress will be emitted in delayed time duration. | - | `5` | +| `uploadTimingFunction` | Emulate response progress, the timing function makes customized progress. Supports `linear`, `ease-in`, `ease-out`. | - | `linear` | > You can change the **status**, **response** and **delay** from the storybook panel on the fly! :rocket: diff --git a/src/utils/faker.js b/src/utils/faker.js index f75fe75..aa59544 100644 --- a/src/utils/faker.js +++ b/src/utils/faker.js @@ -121,11 +121,38 @@ export class Faker { return global.realFetch(input, options); }; + /** + * mock xhr request + * @param {import('mock-xmlhttprequest').MockXhr} xhr + */ mockXhrRequest = (xhr) => { const { method, url, body } = xhr; const matched = this.matchMock(url, method); if (matched) { - const { response, status, delay = 0 } = matched; + const { + response, + status, + delay = 0, + uploadTimingFunction = 'linear', + uploadFrameCount = 5, + } = matched; + // split delays + let timeForTransitionEmit = []; + if (delay > 0) { + timeForTransitionEmit = getTimingSlice( + uploadTimingFunction, + uploadFrameCount + ); + } + + timeForTransitionEmit.forEach((progressRatio, timeoutIndex) => { + setTimeout(() => { + xhr.uploadProgress( + progressRatio * xhr.getRequestBodySize() + ); + }, ((timeoutIndex + 1) * +delay) / uploadFrameCount); + }); + setTimeout(() => { if (typeof response === 'function') { const data = response(new Request(url, { method, body })); @@ -172,4 +199,28 @@ export class Faker { }; } +function getTimingSlice(timingFunction, sliceCount) { + const timeSlice = []; + + if (timingFunction === 'ease-in') { + for (let i = 0; i < sliceCount; i++) { + timeSlice.splice(i, 0, Math.pow((i + 1) / sliceCount, 1.675)); + } + } else if (timingFunction === 'ease-out') { + for (let i = 0; i < sliceCount; i++) { + timeSlice.splice( + i, + 0, + 1 - Math.pow(1 - (i + 1) / sliceCount, 1.675) + ); + } + } else { + // linear + for (let i = 0; i < sliceCount; i++) { + timeSlice.splice(i, 0, (i + 1) / sliceCount); + } + } + return timeSlice; +} + export default new Faker(); diff --git a/stories/components/non-get-request/index.js b/stories/components/non-get-request/index.js index 8db4b24..03d0ef9 100644 --- a/stories/components/non-get-request/index.js +++ b/stories/components/non-get-request/index.js @@ -16,6 +16,7 @@ export const NonGetRequest = ({ title, method, callApi }) => { const [todoName, setTodoName] = useState('Item 1'); const [response, setResponse] = useState({}); const [loading, setLoading] = useState(false); + const [uploadProgress, setUploadProgress] = useState(0); const handleSubmit = async (event) => { event.preventDefault(); @@ -27,6 +28,9 @@ export const NonGetRequest = ({ title, method, callApi }) => { url, method, body: { name: todoName }, + onUploadProgress: (pev) => { + setUploadProgress(pev.loaded / pev.total); + }, }); setResponse(apiResponse); setLoading(false); @@ -62,7 +66,11 @@ export const NonGetRequest = ({ title, method, callApi }) => { )} - + ); }; diff --git a/stories/components/response-container/index.js b/stories/components/response-container/index.js index 789852f..e7147c9 100644 --- a/stories/components/response-container/index.js +++ b/stories/components/response-container/index.js @@ -3,10 +3,19 @@ import PropTypes from 'prop-types'; import { errorContainerStyles, responseContainerStyles } from './styles'; -export const ResponseContainer = ({ loading, status, error, data }) => ( +export const ResponseContainer = ({ + loading, + uploadProgress, + status, + error, + data, +}) => ( <> {loading ? ( -
Loading...
+
+ Loading... + {!!uploadProgress && {uploadProgress * 100} %} +
) : (
{status &&
Status: {status}
} @@ -29,6 +38,7 @@ export const ResponseContainer = ({ loading, status, error, data }) => ( ResponseContainer.propTypes = { loading: PropTypes.bool, + uploadProgress: PropTypes.number, status: PropTypes.number, error: PropTypes.object, data: PropTypes.object, diff --git a/stories/components/search-params-request/index.js b/stories/components/search-params-request/index.js index 448143e..bf6efaf 100644 --- a/stories/components/search-params-request/index.js +++ b/stories/components/search-params-request/index.js @@ -15,6 +15,7 @@ export const SearchParamsRequest = ({ title, isFetch = true }) => { const [todoId, setTodoId] = useState('1'); const [response, setResponse] = useState({}); const [loading, setLoading] = useState(false); + const [uploadProgress, setUploadProgress] = useState(0); const handleSubmit = async (event) => { event.preventDefault(); @@ -25,7 +26,12 @@ export const SearchParamsRequest = ({ title, isFetch = true }) => { const fetchResponse = await callFetch({ url }); setResponse(fetchResponse); } else { - const axiosResponse = await callAxios({ url }); + const axiosResponse = await callAxios({ + url, + onUploadProgress: (pev) => { + setUploadProgress(pev.loaded / pev.total); + }, + }); setResponse(axiosResponse); } setLoading(false); @@ -45,7 +51,11 @@ export const SearchParamsRequest = ({ title, isFetch = true }) => {
- + ); }; diff --git a/stories/example.stories.js b/stories/example.stories.js index 3b5372d..dc76110 100644 --- a/stories/example.stories.js +++ b/stories/example.stories.js @@ -52,6 +52,55 @@ const mockData = [ }, ]; +const mockWithDelayData = [ + { + url: 'https://jsonplaceholder.typicode.com/todos/:id', + method: 'GET', + status: 200, + delay: 5000, + response: { + id: '1', + name: 'Item 1', + }, + }, + { + url: 'https://jsonplaceholder.typicode.com/todos', + method: 'POST', + status: 201, + delay: 5000, + uploadFrameCount: 10, + response: { + message: 'New item created', + }, + }, + { + url: 'https://jsonplaceholder.typicode.com/todos/:id', + method: 'PUT', + status: 200, + delay: 5000, + uploadFrameCount: 100, + response: { + id: '1', + name: 'Item 1', + }, + }, + { + url: 'https://jsonplaceholder.typicode.com/todos/:id', + method: 'PATCH', + status: 204, + delay: 5000, + uploadFrameCount: 100, + response: null, + }, + { + url: 'https://jsonplaceholder.typicode.com/todos/:id', + method: 'DELETE', + status: 202, + delay: 5000, + uploadFrameCount: 100, + response: null, + }, +]; const mockCustomFunctionData = [ { url: 'https://jsonplaceholder.typicode.com/todos/:id', @@ -320,6 +369,17 @@ storiesOf('Examples/Custom Function/Axios', module) storiesOf('Examples/Special Cases', module) .addDecorator(withMock) + .add( + 'Axios request with uploadProgress', + () => ( + + ), + { mockData: mockWithDelayData } + ) .add( 'Same API calls multiple times', () => ( diff --git a/stories/utils/index.js b/stories/utils/index.js index fd67143..353ffc4 100644 --- a/stories/utils/index.js +++ b/stories/utils/index.js @@ -46,6 +46,7 @@ export const callAxios = async ({ method = DEFAULT_METHOD, url = DEFAULT_URL, body, + onUploadProgress, }) => { let data = null; let error = null; @@ -56,6 +57,7 @@ export const callAxios = async ({ url, method, data: body ? JSON.stringify(body) : undefined, + onUploadProgress, }); data = response.data; status = response.status;