diff --git a/test/jest.conf.js b/jest.config.js similarity index 91% rename from test/jest.conf.js rename to jest.config.js index 4a07292..490d318 100644 --- a/test/jest.conf.js +++ b/jest.config.js @@ -1,6 +1,5 @@ -{ +module.exports = { "testRegex": "/test/.*\\.test\\.js$", - "rootDir": "..", "moduleDirectories": ["node_modules", "src/"], "testPathIgnorePatterns": ["/node_modules/", "/dist/"], "setupTestFrameworkScriptFile": "./test/setupTestFramework.js", diff --git a/package.json b/package.json index 9f2e4b7..ae2cbb2 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,10 @@ "multiprocess": true }, "scripts": { - "test": "jest --coverage --config test/jest.conf.js", - "test:watch": "jest --watchman --watchAll --config test/jest.conf.js", - "test:snapshot": "jest --update-snapshot --config test/jest.conf.js", - "test:debugger": "node --debug-brk --inspect ./node_modules/.bin/jest -i --config test/jest.conf.js", - "load": "web-ext run --no-reload", + "test": "jest --coverage", + "test:watch": "jest --watchman --watchAll", + "test:snapshot": "jest --update-snapshot", + "test:debugger": "node --debug-brk --inspect ./node_modules/.bin/jest -i", "eslint": "eslint src test", "eslint:fix": "eslint --fix src test", "start": "npm run dev", @@ -42,7 +41,7 @@ "react-immutable-proptypes": "^2.1.0", "react-redux": "^5.0.3", "redux": "^3.6.0", - "redux-form": "^6.6.0", + "redux-form": "^7.0.1", "redux-saga": "^0.14.3", "reselect": "^3.0.0", "styled-components": "^1.4.4", @@ -72,15 +71,14 @@ "eslint-plugin-jsx-a11y": "^2.2.3", "eslint-plugin-react": "^6.6.0", "husky": "^0.13.3", - "jest": "^19.0.2", + "jest": "^20.0.4", "react-addons-test-utils": "^15.4.2", "react-dnd-test-backend": "^2.3.0", "react-test-renderer": "^15.4.2", "redux-devtools": "^3.3.2", "redux-devtools-dock-monitor": "^1.1.1", "redux-devtools-log-monitor": "^1.2.0", - "web-ext": "^1.9.1", - "webpack": "^2.3.2", + "webpack": "^3.4.0", "whatwg-fetch": "^2.0.3" } } diff --git a/src/components/Request/BodyField.js b/src/components/Request/BodyField.js index e97168b..9b7c5d6 100644 --- a/src/components/Request/BodyField.js +++ b/src/components/Request/BodyField.js @@ -173,8 +173,12 @@ export function BodyField({ bodyType, changeBodyType }) { ); } +BodyField.defaultProps = { + bodyType: 'json', +}; + BodyField.propTypes = { - bodyType: PropTypes.oneOf(['json', 'multipart', 'urlencoded', 'custom']).isRequired, + bodyType: PropTypes.oneOf(['json', 'multipart', 'urlencoded', 'custom']), changeBodyType: PropTypes.func.isRequired, }; diff --git a/src/components/Request/Titlebar.js b/src/components/Request/Titlebar.js index cc97b06..3d271aa 100644 --- a/src/components/Request/Titlebar.js +++ b/src/components/Request/Titlebar.js @@ -107,7 +107,7 @@ Titlebar.propTypes = { removeModal: PropTypes.func.isRequired, formPristine: PropTypes.bool.isRequired, formInvalid: PropTypes.bool.isRequired, - collectionsMinimized: PropTypes.bool.isRequired, + collectionsMinimized: PropTypes.bool, isEditing: PropTypes.bool.isRequired, editingRequest: PropTypes.shape({ name: PropTypes.string, diff --git a/src/store/config/reducer.js b/src/store/config/reducer.js index af4d6f1..75e6537 100644 --- a/src/store/config/reducer.js +++ b/src/store/config/reducer.js @@ -4,7 +4,11 @@ import { TOGGLE_EDIT, } from './types'; -const initialState = {}; +const initialState = { + requestBody: { + expanded: true, + }, +}; export default function (state = initialState, action) { switch (action.type) { diff --git a/src/store/options/sagas.js b/src/store/options/sagas.js index 6cd33fa..b844e89 100644 --- a/src/store/options/sagas.js +++ b/src/store/options/sagas.js @@ -1,10 +1,13 @@ import Immutable from 'immutable'; import localforage from 'localforage'; import { select, put, call, takeEvery } from 'redux-saga/effects'; +import { change } from 'redux-form'; + +import { requestForm } from 'components/Request'; import { FETCH_REQUESTED, UPDATE_REQUESTED, UPDATE_OPTION } from './types'; import { startFetch, receiveOptions } from './actions'; -import { getOptions } from './selectors'; +import { getOptions, getBodyType } from './selectors'; function* updateLocalStorage() { const options = (yield select(getOptions)).toJS(); @@ -22,6 +25,11 @@ function* fetchOptionsSaga() { options = Immutable.fromJS(options) || Immutable.Map(); yield put(receiveOptions(options)); + + // The selected bodyType is persisted across reloads and is put into the form + // when we are done loading the initial options + const bodyType = yield select(getBodyType); + yield put(change(requestForm, 'bodyType', bodyType)); } function* updateOptionSaga({ option, value }) { diff --git a/src/store/options/selectors.js b/src/store/options/selectors.js index 3dada80..7fe07cd 100644 --- a/src/store/options/selectors.js +++ b/src/store/options/selectors.js @@ -33,6 +33,11 @@ export const getHistorySize = createSelector( options => options && options.getIn(['options', 'historySize'], 10), ); +export const getBodyType = createSelector( + [getOptions], + options => options && options.getIn(['options', 'bodyType'], 'json'), +); + export const isDisabledHighlighting = createSelector( [getOptions], options => options && options.getIn(['options', 'disableHighlighting'], false), diff --git a/src/store/request/reducer.js b/src/store/request/reducer.js index e5c0c07..a1acd0b 100644 --- a/src/store/request/reducer.js +++ b/src/store/request/reducer.js @@ -5,7 +5,6 @@ import { PUSH_REDIRECT_CHAIN, RECEIVE_RESPONSE, CLEAR_RESPONSE, - CHANGE_BODY_TYPE, REQUEST_FAILED, } from './types'; @@ -19,7 +18,6 @@ const initialState = { redirectChain: [], lastRequestTime: null, loading: false, - useFormData: true, }; export default function (state = initialState, action) { @@ -60,13 +58,6 @@ export default function (state = initialState, action) { error: undefined, }); - case CHANGE_BODY_TYPE: - // Set Content-Type header to application/x-www-form-urlencoded - // Unset Content-Type when set to application/x-www-form-urlencoded - return Object.assign({}, state, { - bodyType: action.bodyType, - }); - case REQUEST_FAILED: return Object.assign({}, state, { error: action.error, diff --git a/src/store/request/sagas.js b/src/store/request/sagas.js index aaf05a4..b6fea58 100644 --- a/src/store/request/sagas.js +++ b/src/store/request/sagas.js @@ -9,8 +9,9 @@ import { prependHttp, mapParameters } from 'utils/request'; import { pushHistory } from 'store/history/actions'; import { getUrlVariables } from 'store/urlVariables/selectors'; import { requestForm } from 'components/Request'; +import { updateOption } from 'store/options/actions'; -import { getPlaceholderUrl, getUseFormData, getHeaders } from './selectors'; +import { getPlaceholderUrl, getHeaders } from './selectors'; import { executeRequest, receiveResponse } from './actions'; import { SEND_REQUEST, REQUEST_FAILED, SELECT_REQUESTED, CHANGE_BODY_TYPE } from './types'; @@ -42,7 +43,7 @@ export function* createResource(request) { return yield call(prependHttp, resource); } -export function* buildHeaders({ headers, basicAuth, bodyType }) { +export function* buildHeaders({ headers, basicAuth }) { const parameters = yield call(getParameters); const requestHeaders = new Headers(reMapHeaders(headers, parameters)); if (basicAuth && basicAuth.username) { @@ -64,7 +65,6 @@ function buildRequestData({ bodyType, formData }) { formData.forEach(f => { body.append(f.name, f.value); }); - console.log('body', body); return body; } @@ -96,6 +96,7 @@ function buildRequestData({ bodyType, formData }) { default: return null; } + return null; } // Needed for unit tests to be consistent @@ -133,7 +134,7 @@ function createUUID() { export function* fetchData({ request }) { try { yield put(executeRequest()); - const useFormData = yield select(getUseFormData); + const bodyType = request.bodyType; const resource = yield call(createResource, request); const headers = yield call(buildHeaders, request); @@ -141,7 +142,7 @@ export function* fetchData({ request }) { // Build body for requests that support it let body; if (!['GET', 'HEAD'].includes(request.method)) { - body = useFormData + body = bodyType !== 'custom' ? buildRequestData(request) : request.data; } @@ -189,47 +190,55 @@ function* selectRequest({ request }) { function setContentType(array, value) { const index = array.findIndex(item => item.name === 'Content-Type'); + // Replace any existing Content-Type headers if (index > -1) { return [ ...array.slice(0, index), { name: 'Content-Type', value }, - ...array.slice(index + 1) + ...array.slice(index + 1), ]; - } else { + } + + // When the last row is empty, overwrite it instead of pushing + const lastItem = array.length >= 1 + ? array[array.length - 1] + : null; + if (lastItem && !lastItem.name) { return [ - ...array, + ...array.slice(0, array.length - 1), { name: 'Content-Type', value }, ]; } + + return [ + ...array, + { name: 'Content-Type', value }, + ]; } -// TODO breaks when switching after request is sent -function* setTypeHeaderSaga({ bodyType }) { - try { - let headers = yield select(getHeaders) - console.log('bodyType', bodyType); - console.log('headers', headers); - switch (bodyType) { - case 'multipart': - headers = setContentType(headers, 'multipart/form-data'); - break; - case 'urlencoded': - headers = setContentType(headers, 'application/x-www-urlencoded'); - break; - case 'json': - headers = setContentType(headers, 'application/json'); - break; - } - console.log('headers', headers); - yield put(change(requestForm, 'headers', headers)); - } catch (e) { - console.log('e', e); +function* changeBodyTypeSaga({ bodyType }) { + let headers = yield select(getHeaders); + switch (bodyType) { + case 'multipart': + headers = setContentType(headers, 'multipart/form-data'); + break; + case 'urlencoded': + headers = setContentType(headers, 'application/x-www-urlencoded'); + break; + case 'json': + headers = setContentType(headers, 'application/json'); + break; + default: + throw new Error(`Body type ${bodyType} is not supported`); } + yield put(change(requestForm, 'headers', headers)); + // For persistence on load + yield put(updateOption('bodyType', bodyType)); } export default function* rootSaga() { yield takeLatest(SEND_REQUEST, fetchData); yield takeEvery(SELECT_REQUESTED, selectRequest); - yield takeEvery(CHANGE_BODY_TYPE, setTypeHeaderSaga); + yield takeEvery(CHANGE_BODY_TYPE, changeBodyTypeSaga); } diff --git a/src/store/request/selectors.js b/src/store/request/selectors.js index a870774..b441184 100644 --- a/src/store/request/selectors.js +++ b/src/store/request/selectors.js @@ -5,7 +5,6 @@ export const getResponse = state => state.request.response; export const getInterceptedResponse = state => state.request.interceptedResponse; export const getRedirectChain = state => state.request.redirectChain; export const getLoading = state => state.request.loading; -export const getUseFormData = state => state.request.useFormData; const getValues = getFormValues('request'); export const getBodyType = state => getValues(state).bodyType; diff --git a/test/components/request/BodyField.test.js b/test/components/request/BodyField.test.js index e904234..629bccf 100644 --- a/test/components/request/BodyField.test.js +++ b/test/components/request/BodyField.test.js @@ -16,8 +16,8 @@ describe('BodyField', () => { form: 'testForm', })(() => ( {}} + bodyType="custom" + changeBodyType={() => {}} /> )); @@ -30,13 +30,51 @@ describe('BodyField', () => { expect(tree).toMatchSnapshot(); }); - it('should match the previous snapshot when !!useFormData', () => { + it('should match the previous snapshot when bodyType is json', () => { const Decorated = reduxForm({ form: 'testForm', })(() => ( {}} + bodyType="json" + changeBodyType={() => {}} + /> + )); + + const tree = renderer.create( + + + , + ).toJSON(); + + expect(tree).toMatchSnapshot(); + }); + + it('should match the previous snapshot when bodyType is urlencoded', () => { + const Decorated = reduxForm({ + form: 'testForm', + })(() => ( + {}} + /> + )); + + const tree = renderer.create( + + + , + ).toJSON(); + + expect(tree).toMatchSnapshot(); + }); + + it('should match the previous snapshot when bodyType is multipart', () => { + const Decorated = reduxForm({ + form: 'testForm', + })(() => ( + {}} /> )); @@ -66,8 +104,8 @@ describe('BodyField', () => { form: 'testForm', })(() => ( {}} + bodyType="json" + changeBodyType={() => {}} /> )); @@ -109,8 +147,8 @@ describe('BodyField', () => { form: 'testForm', })(() => ( {}} + bodyType="custom" + changeBodyType={() => {}} /> )); @@ -129,15 +167,15 @@ describe('BodyField', () => { expect(input.length).toBe(1); }); - it('should call setUseFormData when !!useFormData checkbox is clicked', () => { - const setUseFormData = jest.fn(); + it('should handle chaning the selected body type', () => { + const changeBodyType = jest.fn(); const Decorated = reduxForm({ form: 'testForm', })(() => ( )); @@ -147,38 +185,15 @@ describe('BodyField', () => { , ); - expect(setUseFormData).not.toHaveBeenCalled(); - - const checkbox = tree.find('input').first(); - checkbox.simulate('change'); - - expect(setUseFormData).toHaveBeenCalledWith(false); - }); - - it('should call setUseFormData when !useFormData checkbox is clicked', () => { - const setUseFormData = jest.fn(); - - const Decorated = reduxForm({ - form: 'testForm', - })(() => ( - - )); - - const tree = mount( - - - , - ); + expect(changeBodyType).not.toHaveBeenCalled(); - expect(setUseFormData).not.toHaveBeenCalled(); + const select = tree.find('select').first(); + select.simulate('change', { target: { value: 'multipart' } }); - const checkbox = tree.find('input').first(); - checkbox.simulate('change'); + expect(changeBodyType).toHaveBeenCalledWith('multipart'); - expect(setUseFormData).toHaveBeenCalledWith(true); + select.simulate('change', { target: { value: 'custom' } }); + expect(changeBodyType).toHaveBeenCalledWith('custom'); }); }); diff --git a/test/components/request/HeadersField.test.js b/test/components/request/HeadersField.test.js index b5829a1..8f4a313 100644 --- a/test/components/request/HeadersField.test.js +++ b/test/components/request/HeadersField.test.js @@ -33,7 +33,7 @@ describe('HeadersField', () => { beforeEach(() => { props = { fields: { - map: Array.map.bind(this, headers), + map: headers.map.bind(headers), remove: jest.fn(), push: jest.fn(), }, diff --git a/test/components/request/__snapshots__/BodyField.test.js.snap b/test/components/request/__snapshots__/BodyField.test.js.snap index 643ab71..49396ed 100644 --- a/test/components/request/__snapshots__/BodyField.test.js.snap +++ b/test/components/request/__snapshots__/BodyField.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`BodyField should match the previous snapshot when !!useFormData 1`] = ` +exports[`BodyField should match the previous snapshot when !useFormData 1`] = `
@@ -29,13 +29,13 @@ exports[`BodyField should match the previous snapshot when !!useFormData 1`] = ` className="form-group" >
-