aboutsummaryrefslogtreecommitdiffstats
path: root/web/src/js
diff options
context:
space:
mode:
Diffstat (limited to 'web/src/js')
-rw-r--r--web/src/js/__tests__/components/ContentView/ContentLoaderSpec.js74
-rw-r--r--web/src/js/__tests__/components/ContentView/ContentViewSpec.js85
-rw-r--r--web/src/js/__tests__/components/ContentView/DownloadContentButtonSpec.js15
-rw-r--r--web/src/js/__tests__/components/ContentView/MetaViewsSpec.js37
-rw-r--r--web/src/js/__tests__/components/ContentView/ShowFullContentButtonSpec.js39
-rw-r--r--web/src/js/__tests__/components/ContentView/UploadContentButtonSpec.js12
-rw-r--r--web/src/js/__tests__/components/ContentView/ViewSelectorSpec.js38
-rw-r--r--web/src/js/__tests__/components/ContentView/__snapshots__/ContentLoaderSpec.js.snap11
-rw-r--r--web/src/js/__tests__/components/ContentView/__snapshots__/ContentViewOptionsSpec.js.snap2
-rw-r--r--web/src/js/__tests__/components/ContentView/__snapshots__/ContentViewSpec.js.snap52
-rw-r--r--web/src/js/__tests__/components/ContentView/__snapshots__/DownloadContentButtonSpec.js.snap13
-rw-r--r--web/src/js/__tests__/components/ContentView/__snapshots__/MetaViewsSpec.js.snap66
-rw-r--r--web/src/js/__tests__/components/ContentView/__snapshots__/ShowFullContentButtonSpec.js.snap25
-rw-r--r--web/src/js/__tests__/components/ContentView/__snapshots__/UploadContentButtonSpec.js.snap19
-rw-r--r--web/src/js/__tests__/components/ContentView/__snapshots__/ViewSelectorSpec.js.snap123
-rw-r--r--web/src/js/__tests__/components/ContentViewSpec.js62
-rw-r--r--web/src/js/__tests__/components/EventLogSpec.js57
-rw-r--r--web/src/js/__tests__/components/FlowTableSpec.js50
-rw-r--r--web/src/js/__tests__/components/FlowView/ToggleEditSpec.js3
-rw-r--r--web/src/js/__tests__/components/FlowView/__snapshots__/DetailsSpec.js.snap4
-rw-r--r--web/src/js/__tests__/components/FlowView/__snapshots__/MessagesSpec.js.snap4
-rw-r--r--web/src/js/__tests__/components/Header/FileMenuSpec.js9
-rw-r--r--web/src/js/__tests__/components/Header/FlowMenuSpec.js1
-rw-r--r--web/src/js/__tests__/components/Header/OptionMenuSpec.js1
-rw-r--r--web/src/js/__tests__/components/Header/__snapshots__/FileMenuSpec.js.snap4
-rw-r--r--web/src/js/__tests__/components/Header/__snapshots__/FlowMenuSpec.js.snap6
-rw-r--r--web/src/js/__tests__/components/Header/__snapshots__/OptionMenuSpec.js.snap82
-rw-r--r--web/src/js/__tests__/components/Modal/ModalSpec.js30
-rw-r--r--web/src/js/__tests__/components/Modal/OptionModalSpec.js54
-rw-r--r--web/src/js/__tests__/components/Modal/OptionSpec.js99
-rw-r--r--web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap255
-rw-r--r--web/src/js/__tests__/components/Modal/__snapshots__/OptionModalSpec.js.snap61
-rw-r--r--web/src/js/__tests__/components/Modal/__snapshots__/OptionSpec.js.snap64
-rw-r--r--web/src/js/__tests__/components/__snapshots__/ContentViewSpec.js.snap80
-rw-r--r--web/src/js/__tests__/components/__snapshots__/EventLogSpec.js.snap76
-rw-r--r--web/src/js/__tests__/components/__snapshots__/FlowTableSpec.js.snap79
-rw-r--r--web/src/js/__tests__/components/common/__snapshots__/ButtonSpec.js.snap2
-rw-r--r--web/src/js/__tests__/ducks/_tflow.js2
-rw-r--r--web/src/js/__tests__/ducks/flowsSpec.js4
-rw-r--r--web/src/js/__tests__/ducks/optionsSpec.js66
-rw-r--r--web/src/js/__tests__/ducks/tutils.js42
-rw-r--r--web/src/js/__tests__/ducks/ui/flowSpec.js12
-rw-r--r--web/src/js/__tests__/ducks/ui/keyboardSpec.js7
-rw-r--r--web/src/js/__tests__/ducks/ui/modalSpec.js25
-rw-r--r--web/src/js/__tests__/ducks/ui/optionEditorSpec.js32
-rw-r--r--web/src/js/__tests__/flow/utilsSpec.js6
-rw-r--r--web/src/js/__tests__/utilsSpec.js5
-rw-r--r--web/src/js/app.jsx11
-rw-r--r--web/src/js/backends/static.js32
-rw-r--r--web/src/js/backends/websocket.js3
-rw-r--r--web/src/js/components/ContentView.jsx13
-rw-r--r--web/src/js/components/ContentView/ContentLoader.jsx156
-rw-r--r--web/src/js/components/ContentView/ContentViews.jsx10
-rw-r--r--web/src/js/components/ContentView/DownloadContentButton.jsx1
-rw-r--r--web/src/js/components/ContentView/ShowFullContentButton.jsx2
-rw-r--r--web/src/js/components/ContentView/UploadContentButton.jsx1
-rw-r--r--web/src/js/components/ContentView/ViewSelector.jsx2
-rw-r--r--web/src/js/components/EventLog.jsx4
-rw-r--r--web/src/js/components/FlowTable.jsx20
-rw-r--r--web/src/js/components/FlowView.jsx101
-rw-r--r--web/src/js/components/FlowView/Messages.jsx51
-rw-r--r--web/src/js/components/Footer.jsx10
-rw-r--r--web/src/js/components/Header.jsx5
-rw-r--r--web/src/js/components/Header/FileMenu.jsx7
-rw-r--r--web/src/js/components/Header/FlowMenu.jsx7
-rw-r--r--web/src/js/components/Header/OptionMenu.jsx86
-rw-r--r--web/src/js/components/MainView.jsx55
-rw-r--r--web/src/js/components/Modal/Modal.jsx24
-rw-r--r--web/src/js/components/Modal/ModalLayout.jsx16
-rw-r--r--web/src/js/components/Modal/ModalList.jsx13
-rw-r--r--web/src/js/components/Modal/Option.jsx141
-rw-r--r--web/src/js/components/Modal/OptionModal.jsx110
-rwxr-xr-xweb/src/js/components/Prompt.jsx67
-rw-r--r--web/src/js/components/ProxyApp.jsx4
-rw-r--r--web/src/js/components/common/Button.jsx2
-rw-r--r--web/src/js/components/common/HideInStatic.jsx5
-rw-r--r--web/src/js/ducks/connection.js2
-rw-r--r--web/src/js/ducks/index.js2
-rw-r--r--web/src/js/ducks/options.js50
-rw-r--r--web/src/js/ducks/settings.js1
-rw-r--r--web/src/js/ducks/ui/flow.js5
-rw-r--r--web/src/js/ducks/ui/index.js4
-rw-r--r--web/src/js/ducks/ui/keyboard.js9
-rw-r--r--web/src/js/ducks/ui/modal.js33
-rw-r--r--web/src/js/ducks/ui/optionsEditor.js73
-rw-r--r--web/src/js/filt/filt.js8
-rw-r--r--web/src/js/filt/filt.peg8
-rw-r--r--web/src/js/flow/utils.js2
-rw-r--r--web/src/js/urlState.js6
-rw-r--r--web/src/js/utils.js12
90 files changed, 2577 insertions, 492 deletions
diff --git a/web/src/js/__tests__/components/ContentView/ContentLoaderSpec.js b/web/src/js/__tests__/components/ContentView/ContentLoaderSpec.js
new file mode 100644
index 00000000..80b40c72
--- /dev/null
+++ b/web/src/js/__tests__/components/ContentView/ContentLoaderSpec.js
@@ -0,0 +1,74 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import withContentLoader from '../../../components/ContentView/ContentLoader'
+import { TFlow } from '../../ducks/tutils'
+import TestUtils from 'react-dom/test-utils'
+import mockXMLHttpRequest from 'mock-xmlhttprequest'
+
+global.XMLHttpRequest = mockXMLHttpRequest
+class tComponent extends React.Component {
+ constructor(props, context){
+ super(props, context)
+ }
+ render() {
+ return (<p>foo</p>)
+ }
+}
+
+let tflow = new TFlow(),
+ ContentLoader = withContentLoader(tComponent)
+
+describe('ContentLoader Component', () => {
+ it('should render correctly', () => {
+ let contentLoader = renderer.create(<ContentLoader flow={tflow} message={tflow.response}/>),
+ tree = contentLoader.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ let contentLoader = TestUtils.renderIntoDocument(<ContentLoader flow={tflow} message={tflow.response}/>)
+
+ it('should handle updateContent', () => {
+ tflow.response.content = 'foo'
+ contentLoader.updateContent({flow: tflow, message: tflow.response})
+ expect(contentLoader.state.request).toEqual(undefined)
+ expect(contentLoader.state.content).toEqual('foo')
+ // when content length is 0 or null
+ tflow.response.contentLength = 0
+ tflow.response.content = undefined
+ contentLoader.updateContent({flow: tflow, message: tflow.response})
+ expect(contentLoader.state.request).toEqual(undefined)
+ expect(contentLoader.state.content).toEqual('')
+ })
+
+ it('should handle componentWillReceiveProps', () => {
+ contentLoader.updateContent = jest.fn()
+ contentLoader.componentWillReceiveProps({flow: tflow, message: tflow.request})
+ expect(contentLoader.updateContent).toBeCalled()
+ })
+
+ it('should handle requestComplete', () => {
+ expect(contentLoader.requestComplete(tflow.request, {})).toEqual(undefined)
+ // request == this.state.request
+ contentLoader.state.request = tflow.request
+ contentLoader.requestComplete(tflow.request, {})
+ expect(contentLoader.state.content).toEqual(tflow.request.responseText)
+ expect(contentLoader.state.request).toEqual(undefined)
+ })
+
+ it('should handle requestFailed', () => {
+ console.error = jest.fn()
+ expect(contentLoader.requestFailed(tflow.request, {})).toEqual(undefined)
+ //request == this.state.request
+ contentLoader.state.request = tflow.request
+ contentLoader.requestFailed(tflow.request, 'foo error')
+ expect(contentLoader.state.content).toEqual('Error getting content.')
+ expect(contentLoader.state.request).toEqual(undefined)
+ expect(console.error).toBeCalledWith('foo error')
+ })
+
+ it('should handle componentWillUnmount', () => {
+ contentLoader.state.request = { abort : jest.fn() }
+ contentLoader.componentWillUnmount()
+ expect(contentLoader.state.request.abort).toBeCalled()
+ })
+})
diff --git a/web/src/js/__tests__/components/ContentView/ContentViewSpec.js b/web/src/js/__tests__/components/ContentView/ContentViewSpec.js
new file mode 100644
index 00000000..f519185d
--- /dev/null
+++ b/web/src/js/__tests__/components/ContentView/ContentViewSpec.js
@@ -0,0 +1,85 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import TestUtils from 'react-dom/test-utils'
+import { Provider } from 'react-redux'
+import { ViewServer, ViewImage, PureViewServer, Edit } from '../../../components/ContentView/ContentViews'
+import { TFlow, TStore } from '../../ducks/tutils'
+import mockXMLHttpRequest from 'mock-xmlhttprequest'
+
+global.XMLHttpRequest = mockXMLHttpRequest
+let tflow = new TFlow()
+
+describe('ViewImage Component', () => {
+ let viewImage = renderer.create(<ViewImage flow={tflow} message={tflow.response}/>),
+ tree = viewImage.toJSON()
+
+ it('should render correctly', () => {
+ expect(tree).toMatchSnapshot()
+ })
+})
+
+describe('ViewServer Component', () => {
+ let store = TStore(),
+ setContentViewDescFn = jest.fn(),
+ setContentFn = jest.fn()
+
+ it('should render correctly and connect to state', () => {
+ let provider = renderer.create(
+ <Provider store={store}>
+ <ViewServer
+ setContentViewDescription={setContentViewDescFn}
+ setContent={setContentFn}
+ flow={tflow}
+ message={tflow.response}
+ />
+ </Provider>),
+ tree = provider.toJSON()
+ expect(tree).toMatchSnapshot()
+
+ let viewServer = renderer.create(
+ <PureViewServer
+ showFullContent={true}
+ maxLines={10}
+ setContentViewDescription={setContentViewDescFn}
+ setContent={setContentViewDescFn}
+ flow={tflow}
+ message={tflow.response}
+ content={JSON.stringify({lines: [['k1', 'v1']]})}
+ />
+ )
+ tree = viewServer.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle componentWillReceiveProps', () => {
+ // case of fail to parse content
+ let viewSever = TestUtils.renderIntoDocument(
+ <PureViewServer
+ showFullContent={true}
+ maxLines={10}
+ setContentViewDescription={setContentViewDescFn}
+ setContent={setContentViewDescFn}
+ flow={tflow}
+ message={tflow.response}
+ content={JSON.stringify({lines: [['k1', 'v1']]})}
+ />
+ )
+ viewSever.componentWillReceiveProps({...viewSever.props, content: '{foo' })
+ let e = ''
+ try {JSON.parse('{foo') } catch(err){ e = err.message}
+ expect(viewSever.data).toEqual({ description: e, lines: [] })
+ })
+})
+
+describe('Edit Component', () => {
+ it('should render correctly', () => {
+ let edit = renderer.create(<Edit
+ content="foo"
+ onChange={jest.fn}
+ flow={tflow}
+ message={tflow.response}
+ />),
+ tree = edit.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+})
diff --git a/web/src/js/__tests__/components/ContentView/DownloadContentButtonSpec.js b/web/src/js/__tests__/components/ContentView/DownloadContentButtonSpec.js
new file mode 100644
index 00000000..fd00627d
--- /dev/null
+++ b/web/src/js/__tests__/components/ContentView/DownloadContentButtonSpec.js
@@ -0,0 +1,15 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import DownloadContentButton from '../../../components/ContentView/DownloadContentButton'
+import { TFlow } from '../../ducks/tutils'
+
+let tflow = new TFlow()
+describe('DownloadContentButton Component', () => {
+ it('should render correctly', () => {
+ let downloadContentButton = renderer.create(
+ <DownloadContentButton flow={tflow} message={tflow.response}/>
+ ),
+ tree = downloadContentButton.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+})
diff --git a/web/src/js/__tests__/components/ContentView/MetaViewsSpec.js b/web/src/js/__tests__/components/ContentView/MetaViewsSpec.js
new file mode 100644
index 00000000..26b2a27c
--- /dev/null
+++ b/web/src/js/__tests__/components/ContentView/MetaViewsSpec.js
@@ -0,0 +1,37 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import { ContentEmpty, ContentMissing, ContentTooLarge } from '../../../components/ContentView/MetaViews'
+import { TFlow } from '../../ducks/tutils'
+
+let tflow = new TFlow()
+
+describe('ContentEmpty Components', () => {
+ it('should render correctly', () => {
+ let contentEmpty = renderer.create(<ContentEmpty flow={tflow} message={tflow.response}/>),
+ tree = contentEmpty.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+})
+
+describe('ContentMissing Components', () => {
+ it('should render correctly', () => {
+ let contentMissing = renderer.create(<ContentMissing flow={tflow} message={tflow.response}/>),
+ tree = contentMissing.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+})
+
+describe('ContentTooLarge Components', () => {
+ it('should render correctly', () => {
+ let clickFn = jest.fn(),
+ uploadContentFn = jest.fn(),
+ contentTooLarge = renderer.create(<ContentTooLarge
+ flow={tflow}
+ message={tflow.response}
+ onClick={clickFn}
+ uploadContent={uploadContentFn}
+ />),
+ tree = contentTooLarge.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+})
diff --git a/web/src/js/__tests__/components/ContentView/ShowFullContentButtonSpec.js b/web/src/js/__tests__/components/ContentView/ShowFullContentButtonSpec.js
new file mode 100644
index 00000000..14871f13
--- /dev/null
+++ b/web/src/js/__tests__/components/ContentView/ShowFullContentButtonSpec.js
@@ -0,0 +1,39 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import { Provider } from 'react-redux'
+import ConnectedComponent, { ShowFullContentButton } from '../../../components/ContentView/ShowFullContentButton'
+import { TStore } from '../../ducks/tutils'
+
+
+describe('ShowFullContentButton Component', () => {
+ let setShowFullContentFn = jest.fn(),
+ showFullContentButton = renderer.create(
+ <ShowFullContentButton
+ setShowFullContent={setShowFullContentFn}
+ showFullContent={false}
+ visibleLines={10}
+ contentLines={20}
+ />
+ ),
+ tree = showFullContentButton.toJSON()
+
+ it('should render correctly', () => {
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle click', () => {
+ tree.children[0].props.onClick()
+ expect(setShowFullContentFn).toBeCalled()
+ })
+
+ it('should connect to state', () => {
+ let store = TStore(),
+ provider = renderer.create(
+ <Provider store={store}>
+ <ConnectedComponent/>
+ </Provider>
+ ),
+ tree = provider.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+})
diff --git a/web/src/js/__tests__/components/ContentView/UploadContentButtonSpec.js b/web/src/js/__tests__/components/ContentView/UploadContentButtonSpec.js
new file mode 100644
index 00000000..3695be72
--- /dev/null
+++ b/web/src/js/__tests__/components/ContentView/UploadContentButtonSpec.js
@@ -0,0 +1,12 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import UploadContentButton from '../../../components/ContentView/UploadContentButton'
+
+describe('UpdateContentButton Component', () => {
+ it('should render correctly', () => {
+ let uploadContentFn = jest.fn(),
+ uploadContentButton = renderer.create(<UploadContentButton uploadContent={uploadContentFn}/>),
+ tree = uploadContentButton.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+})
diff --git a/web/src/js/__tests__/components/ContentView/ViewSelectorSpec.js b/web/src/js/__tests__/components/ContentView/ViewSelectorSpec.js
new file mode 100644
index 00000000..9e87e2f0
--- /dev/null
+++ b/web/src/js/__tests__/components/ContentView/ViewSelectorSpec.js
@@ -0,0 +1,38 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import ConnectedComponent, { ViewSelector } from '../../../components/ContentView/ViewSelector'
+import { Provider } from 'react-redux'
+import { TStore } from '../../ducks/tutils'
+
+
+describe('ViewSelector Component', () => {
+ let contentViews = ['Auto', 'Raw', 'Text'],
+ activeView = 'Auto',
+ setContentViewFn = jest.fn(),
+ viewSelector = renderer.create(
+ <ViewSelector contentViews={contentViews} activeView={activeView} setContentView={setContentViewFn}/>
+ ),
+ tree = viewSelector.toJSON()
+
+ it('should render correctly', () => {
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle click', () => {
+ let mockEvent = { preventDefault: jest.fn() },
+ tab = tree.children[1].children[0].children[1]
+ tab.props.onClick(mockEvent)
+ expect(mockEvent.preventDefault).toBeCalled()
+ })
+
+ it('should connect to state', () => {
+ let store = TStore(),
+ provider = renderer.create(
+ <Provider store={store}>
+ <ConnectedComponent/>
+ </Provider>
+ ),
+ tree = provider.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+})
diff --git a/web/src/js/__tests__/components/ContentView/__snapshots__/ContentLoaderSpec.js.snap b/web/src/js/__tests__/components/ContentView/__snapshots__/ContentLoaderSpec.js.snap
new file mode 100644
index 00000000..88d4a380
--- /dev/null
+++ b/web/src/js/__tests__/components/ContentView/__snapshots__/ContentLoaderSpec.js.snap
@@ -0,0 +1,11 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ContentLoader Component should render correctly 1`] = `
+<div
+ className="text-center"
+>
+ <i
+ className="fa fa-spinner fa-spin"
+ />
+</div>
+`;
diff --git a/web/src/js/__tests__/components/ContentView/__snapshots__/ContentViewOptionsSpec.js.snap b/web/src/js/__tests__/components/ContentView/__snapshots__/ContentViewOptionsSpec.js.snap
index e3561ec1..57368de2 100644
--- a/web/src/js/__tests__/components/ContentView/__snapshots__/ContentViewOptionsSpec.js.snap
+++ b/web/src/js/__tests__/components/ContentView/__snapshots__/ContentViewOptionsSpec.js.snap
@@ -13,7 +13,7 @@ exports[`ContentViewOptions Component should render correctly 1`] = `
 
<a
className="btn btn-default btn-xs"
- href="/flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/response/content"
+ href="./flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/response/content.data"
title="Download the content of the flow."
>
<i
diff --git a/web/src/js/__tests__/components/ContentView/__snapshots__/ContentViewSpec.js.snap b/web/src/js/__tests__/components/ContentView/__snapshots__/ContentViewSpec.js.snap
new file mode 100644
index 00000000..ff651081
--- /dev/null
+++ b/web/src/js/__tests__/components/ContentView/__snapshots__/ContentViewSpec.js.snap
@@ -0,0 +1,52 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Edit Component should render correctly 1`] = `
+<div
+ className="text-center"
+>
+ <i
+ className="fa fa-spinner fa-spin"
+ />
+</div>
+`;
+
+exports[`ViewImage Component should render correctly 1`] = `
+<div
+ className="flowview-image"
+>
+ <img
+ alt="preview"
+ className="img-thumbnail"
+ src="./flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/response/content.data"
+ />
+</div>
+`;
+
+exports[`ViewServer Component should render correctly and connect to state 1`] = `
+<div
+ className="text-center"
+>
+ <i
+ className="fa fa-spinner fa-spin"
+ />
+</div>
+`;
+
+exports[`ViewServer Component should render correctly and connect to state 2`] = `
+<div>
+ <pre>
+ <div>
+ <span
+ className="k"
+ >
+ 1
+ </span>
+ <span
+ className="v"
+ >
+ 1
+ </span>
+ </div>
+ </pre>
+</div>
+`;
diff --git a/web/src/js/__tests__/components/ContentView/__snapshots__/DownloadContentButtonSpec.js.snap b/web/src/js/__tests__/components/ContentView/__snapshots__/DownloadContentButtonSpec.js.snap
new file mode 100644
index 00000000..76f21ce1
--- /dev/null
+++ b/web/src/js/__tests__/components/ContentView/__snapshots__/DownloadContentButtonSpec.js.snap
@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DownloadContentButton Component should render correctly 1`] = `
+<a
+ className="btn btn-default btn-xs"
+ href="./flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/response/content.data"
+ title="Download the content of the flow."
+>
+ <i
+ className="fa fa-download"
+ />
+</a>
+`;
diff --git a/web/src/js/__tests__/components/ContentView/__snapshots__/MetaViewsSpec.js.snap b/web/src/js/__tests__/components/ContentView/__snapshots__/MetaViewsSpec.js.snap
new file mode 100644
index 00000000..ad722abf
--- /dev/null
+++ b/web/src/js/__tests__/components/ContentView/__snapshots__/MetaViewsSpec.js.snap
@@ -0,0 +1,66 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ContentEmpty Components should render correctly 1`] = `
+<div
+ className="alert alert-info"
+>
+ No
+ response
+ content.
+</div>
+`;
+
+exports[`ContentMissing Components should render correctly 1`] = `
+<div
+ className="alert alert-info"
+>
+ Response
+ content missing.
+</div>
+`;
+
+exports[`ContentTooLarge Components should render correctly 1`] = `
+<div>
+ <div
+ className="alert alert-warning"
+ >
+ <button
+ className="btn btn-xs btn-warning pull-right"
+ onClick={[Function]}
+ >
+ Display anyway
+ </button>
+ 7b
+ content size.
+ </div>
+ <div
+ className="view-options text-center"
+ >
+ <a
+ className="btn btn-default btn-xs"
+ href="#"
+ onClick={[Function]}
+ title="Upload a file to replace the content."
+ >
+ <i
+ className="fa fa-fw fa-upload"
+ />
+ <input
+ className="hidden"
+ onChange={[Function]}
+ type="file"
+ />
+ </a>
+  
+ <a
+ className="btn btn-default btn-xs"
+ href="./flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/response/content.data"
+ title="Download the content of the flow."
+ >
+ <i
+ className="fa fa-download"
+ />
+ </a>
+ </div>
+</div>
+`;
diff --git a/web/src/js/__tests__/components/ContentView/__snapshots__/ShowFullContentButtonSpec.js.snap b/web/src/js/__tests__/components/ContentView/__snapshots__/ShowFullContentButtonSpec.js.snap
new file mode 100644
index 00000000..e0532154
--- /dev/null
+++ b/web/src/js/__tests__/components/ContentView/__snapshots__/ShowFullContentButtonSpec.js.snap
@@ -0,0 +1,25 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ShowFullContentButton Component should connect to state 1`] = `null`;
+
+exports[`ShowFullContentButton Component should render correctly 1`] = `
+<div>
+ <div
+ className="view-all-content-btn btn-xs btn btn-default"
+ disabled={undefined}
+ onClick={[Function]}
+ title={undefined}
+ >
+ Show full content
+ </div>
+ <span
+ className="pull-right"
+ >
+
+ 10
+ /
+ 20
+ are visible  
+ </span>
+</div>
+`;
diff --git a/web/src/js/__tests__/components/ContentView/__snapshots__/UploadContentButtonSpec.js.snap b/web/src/js/__tests__/components/ContentView/__snapshots__/UploadContentButtonSpec.js.snap
new file mode 100644
index 00000000..f642d731
--- /dev/null
+++ b/web/src/js/__tests__/components/ContentView/__snapshots__/UploadContentButtonSpec.js.snap
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`UpdateContentButton Component should render correctly 1`] = `
+<a
+ className="btn btn-default btn-xs"
+ href="#"
+ onClick={[Function]}
+ title="Upload a file to replace the content."
+>
+ <i
+ className="fa fa-fw fa-upload"
+ />
+ <input
+ className="hidden"
+ onChange={[Function]}
+ type="file"
+ />
+</a>
+`;
diff --git a/web/src/js/__tests__/components/ContentView/__snapshots__/ViewSelectorSpec.js.snap b/web/src/js/__tests__/components/ContentView/__snapshots__/ViewSelectorSpec.js.snap
new file mode 100644
index 00000000..481bd56a
--- /dev/null
+++ b/web/src/js/__tests__/components/ContentView/__snapshots__/ViewSelectorSpec.js.snap
@@ -0,0 +1,123 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ViewSelector Component should connect to state 1`] = `
+<div
+ className="dropup pull-left"
+>
+ <a
+ className="btn btn-default btn-xs"
+ href="#"
+ onClick={[Function]}
+ >
+ <span>
+
+ <b>
+ View:
+ </b>
+
+ auto
+
+ <span
+ className="caret"
+ />
+
+ </span>
+ </a>
+ <ul
+ className="dropdown-menu"
+ role="menu"
+ >
+ <li>
+
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ auto
+ </a>
+
+ </li>
+ <li>
+
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ raw
+ </a>
+
+ </li>
+ <li>
+
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ text
+ </a>
+
+ </li>
+ </ul>
+</div>
+`;
+
+exports[`ViewSelector Component should render correctly 1`] = `
+<div
+ className="dropup pull-left"
+>
+ <a
+ className="btn btn-default btn-xs"
+ href="#"
+ onClick={[Function]}
+ >
+ <span>
+
+ <b>
+ View:
+ </b>
+
+ auto
+
+ <span
+ className="caret"
+ />
+
+ </span>
+ </a>
+ <ul
+ className="dropdown-menu"
+ role="menu"
+ >
+ <li>
+
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ auto
+ </a>
+
+ </li>
+ <li>
+
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ raw
+ </a>
+
+ </li>
+ <li>
+
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ text
+ </a>
+
+ </li>
+ </ul>
+</div>
+`;
diff --git a/web/src/js/__tests__/components/ContentViewSpec.js b/web/src/js/__tests__/components/ContentViewSpec.js
new file mode 100644
index 00000000..a654870e
--- /dev/null
+++ b/web/src/js/__tests__/components/ContentViewSpec.js
@@ -0,0 +1,62 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import ContentView from '../../components/ContentView'
+import { TStore, TFlow } from '../ducks/tutils'
+import { Provider } from 'react-redux'
+import mockXMLHttpRequest from 'mock-xmlhttprequest'
+
+global.XMLHttpRequest = mockXMLHttpRequest
+
+describe('ContentView Component', () => {
+ let store = TStore()
+
+ it('should render correctly', () => {
+ store.getState().ui.flow.contentView = 'Edit'
+ let tflow = TFlow(),
+ provider = renderer.create(
+ <Provider store={store}>
+ <ContentView flow={tflow} message={tflow.request}/>
+ </Provider>),
+ tree = provider.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should render correctly with empty content', () => {
+ let tflow = TFlow()
+ tflow.response.contentLength = 0
+ let provider = renderer.create(
+ <Provider store={store}>
+ <ContentView flow={tflow} message={tflow.response} readonly={true}/>
+ </Provider>),
+ tree = provider.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should render correctly with missing content', () => {
+ let tflow = TFlow()
+ tflow.response.contentLength = null
+ let provider = renderer.create(
+ <Provider store={store}>
+ <ContentView flow={tflow} message={tflow.response} readonly={true}/>
+ </Provider>),
+ tree = provider.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should render correctly with content too large', () => {
+ let tflow = TFlow()
+ tflow.response.contentLength = 1024 * 1024 * 100
+ let provider = renderer.create(
+ <Provider store={store}>
+ <ContentView
+ flow={tflow}
+ message={tflow.response}
+ readonly={true}
+ uploadContent={jest.fn()}
+ onOpenFile={jest.fn()}
+ />
+ </Provider>),
+ tree = provider.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+})
diff --git a/web/src/js/__tests__/components/EventLogSpec.js b/web/src/js/__tests__/components/EventLogSpec.js
new file mode 100644
index 00000000..8510de55
--- /dev/null
+++ b/web/src/js/__tests__/components/EventLogSpec.js
@@ -0,0 +1,57 @@
+jest.mock('../../components/EventLog/EventList')
+
+import React from 'react'
+import renderer from 'react-test-renderer'
+import TestUtils from 'react-dom/test-utils'
+import EventLog, { PureEventLog } from '../../components/EventLog'
+import { Provider } from 'react-redux'
+import { TStore } from '../ducks/tutils'
+
+window.addEventListener = jest.fn()
+window.removeEventListener = jest.fn()
+
+describe('EventLog Component', () => {
+ let store = TStore(),
+ provider = renderer.create(
+ <Provider store={store}>
+ <EventLog/>
+ </Provider>),
+ tree = provider.toJSON()
+
+ it('should connect to state and render correctly', () => {
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle toggleFilter', () => {
+ let debugToggleButton = tree.children[0].children[1].children[0]
+ debugToggleButton.props.onClick()
+ })
+
+ provider = TestUtils.renderIntoDocument(
+ <Provider store={store}><EventLog/></Provider>)
+ let eventLog = TestUtils.findRenderedComponentWithType(provider, PureEventLog),
+ mockEvent = { preventDefault: jest.fn() }
+
+ it('should handle DragStart', () => {
+ eventLog.onDragStart(mockEvent)
+ expect(mockEvent.preventDefault).toBeCalled()
+ expect(window.addEventListener).toBeCalledWith('mousemove', eventLog.onDragMove)
+ expect(window.addEventListener).toBeCalledWith('mouseup', eventLog.onDragStop)
+ expect(window.addEventListener).toBeCalledWith('dragend', eventLog.onDragStop)
+ mockEvent.preventDefault.mockClear()
+ })
+
+ it('should handle DragMove', () => {
+ eventLog.onDragMove(mockEvent)
+ expect(mockEvent.preventDefault).toBeCalled()
+ mockEvent.preventDefault.mockClear()
+ })
+
+ console.error = jest.fn() // silent the error.
+ it('should handle DragStop', () => {
+ eventLog.onDragStop(mockEvent)
+ expect(mockEvent.preventDefault).toBeCalled()
+ expect(window.removeEventListener).toBeCalledWith('mousemove', eventLog.onDragMove)
+ })
+
+})
diff --git a/web/src/js/__tests__/components/FlowTableSpec.js b/web/src/js/__tests__/components/FlowTableSpec.js
new file mode 100644
index 00000000..f63b28fc
--- /dev/null
+++ b/web/src/js/__tests__/components/FlowTableSpec.js
@@ -0,0 +1,50 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import {PureFlowTable as FlowTable} from '../../components/FlowTable'
+import TestUtils from 'react-dom/test-utils'
+import { TFlow, TStore } from '../ducks/tutils'
+import { Provider } from 'react-redux'
+
+window.addEventListener = jest.fn()
+
+describe('FlowTable Component', () => {
+ let selectFn = jest.fn(),
+ tflow = TFlow(),
+ store = TStore()
+
+ it('should render correctly', () => {
+ let provider = renderer.create(
+ <Provider store={store}>
+ <FlowTable onSelect={selectFn} flows={[tflow]}/>
+ </Provider>),
+ tree = provider.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ let provider = TestUtils.renderIntoDocument(
+ <Provider store={store} >
+ <FlowTable onSelect={selectFn} flows={[tflow]}/>
+ </Provider>),
+ flowTable = TestUtils.findRenderedComponentWithType(provider, FlowTable)
+
+ it('should handle componentWillUnmount', () => {
+ flowTable.componentWillUnmount()
+ expect(window.addEventListener).toBeCalledWith('resize', flowTable.onViewportUpdate)
+ })
+
+ it('should handle componentDidUpdate', () => {
+ // flowTable.shouldScrollIntoView == false
+ expect(flowTable.componentDidUpdate()).toEqual(undefined)
+ // rowTop - headHeight < viewportTop
+ flowTable.shouldScrollIntoView = true
+ flowTable.componentDidUpdate()
+ // rowBottom > viewportTop + viewportHeight
+ flowTable.shouldScrollIntoView = true
+ flowTable.componentDidUpdate()
+ })
+
+ it('should handle componentWillReceiveProps', () => {
+ flowTable.componentWillReceiveProps({selected: tflow})
+ expect(flowTable.shouldScrollIntoView).toBeTruthy()
+ })
+})
diff --git a/web/src/js/__tests__/components/FlowView/ToggleEditSpec.js b/web/src/js/__tests__/components/FlowView/ToggleEditSpec.js
index 4578fdc8..ec3a8462 100644
--- a/web/src/js/__tests__/components/FlowView/ToggleEditSpec.js
+++ b/web/src/js/__tests__/components/FlowView/ToggleEditSpec.js
@@ -6,6 +6,7 @@ import { Provider } from 'react-redux'
import { startEdit, stopEdit } from '../../../ducks/ui/flow'
import { TFlow, TStore } from '../../ducks/tutils'
+global.fetch = jest.fn()
let tflow = new TFlow()
describe('ToggleEdit Component', () => {
@@ -24,7 +25,7 @@ describe('ToggleEdit Component', () => {
it('should handle click on stopEdit', () => {
tree.children[0].props.onClick()
- expect(store.getActions()).toEqual([stopEdit(tflow, true)])
+ expect(fetch).toBeCalled()
})
it('should handle click on startEdit', () => {
diff --git a/web/src/js/__tests__/components/FlowView/__snapshots__/DetailsSpec.js.snap b/web/src/js/__tests__/components/FlowView/__snapshots__/DetailsSpec.js.snap
index dcee1895..6e01ae85 100644
--- a/web/src/js/__tests__/components/FlowView/__snapshots__/DetailsSpec.js.snap
+++ b/web/src/js/__tests__/components/FlowView/__snapshots__/DetailsSpec.js.snap
@@ -12,7 +12,7 @@ exports[`ConnectionInfo Component should render correctly 1`] = `
Address:
</td>
<td>
- address:22
+ 127.0.0.1:22
</td>
</tr>
<tr>
@@ -47,7 +47,7 @@ exports[`Details Component should render correctly 1`] = `
Address:
</td>
<td>
- address:22
+ 127.0.0.1:22
</td>
</tr>
<tr>
diff --git a/web/src/js/__tests__/components/FlowView/__snapshots__/MessagesSpec.js.snap b/web/src/js/__tests__/components/FlowView/__snapshots__/MessagesSpec.js.snap
index c8290e45..cc021f13 100644
--- a/web/src/js/__tests__/components/FlowView/__snapshots__/MessagesSpec.js.snap
+++ b/web/src/js/__tests__/components/FlowView/__snapshots__/MessagesSpec.js.snap
@@ -265,7 +265,7 @@ exports[`Request Component should render correctly 1`] = `
 
<a
className="btn btn-default btn-xs"
- href="/flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/request/content"
+ href="./flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/request/content.data"
title="Download the content of the flow."
>
<i
@@ -528,7 +528,7 @@ exports[`Response Component should render correctly 1`] = `
 
<a
className="btn btn-default btn-xs"
- href="/flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/response/content"
+ href="./flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/response/content.data"
title="Download the content of the flow."
>
<i
diff --git a/web/src/js/__tests__/components/Header/FileMenuSpec.js b/web/src/js/__tests__/components/Header/FileMenuSpec.js
index 65b4647a..2f4e746d 100644
--- a/web/src/js/__tests__/components/Header/FileMenuSpec.js
+++ b/web/src/js/__tests__/components/Header/FileMenuSpec.js
@@ -8,13 +8,20 @@ describe('FileMenu Component', () => {
let clearFn = jest.fn(),
loadFn = jest.fn(),
saveFn = jest.fn(),
+ openModalFn = jest.fn(),
mockEvent = {
preventDefault: jest.fn(),
target: { files: ["foo", "bar "] }
},
createNodeMock = () => { return { click: jest.fn() }},
fileMenu = renderer.create(
- <FileMenu clearFlows={clearFn} loadFlows={loadFn} saveFlows={saveFn}/>, { createNodeMock }),
+ <FileMenu
+ clearFlows={clearFn}
+ loadFlows={loadFn}
+ saveFlows={saveFn}
+ openModal={openModalFn}
+ />,
+ { createNodeMock }),
tree = fileMenu.toJSON()
it('should render correctly', () => {
diff --git a/web/src/js/__tests__/components/Header/FlowMenuSpec.js b/web/src/js/__tests__/components/Header/FlowMenuSpec.js
index 1278d8ee..65fde213 100644
--- a/web/src/js/__tests__/components/Header/FlowMenuSpec.js
+++ b/web/src/js/__tests__/components/Header/FlowMenuSpec.js
@@ -7,7 +7,6 @@ import { TFlow, TStore }from '../../ducks/tutils'
import { MessageUtils } from "../../../flow/utils"
import { Provider } from 'react-redux'
-
describe('FlowMenu Component', () => {
let actions = {
resumeFlow: jest.fn(),
diff --git a/web/src/js/__tests__/components/Header/OptionMenuSpec.js b/web/src/js/__tests__/components/Header/OptionMenuSpec.js
index b84fce6e..980285ef 100644
--- a/web/src/js/__tests__/components/Header/OptionMenuSpec.js
+++ b/web/src/js/__tests__/components/Header/OptionMenuSpec.js
@@ -4,7 +4,6 @@ import { Provider } from 'react-redux'
import OptionMenu from '../../../components/Header/OptionMenu'
import { TStore } from '../../ducks/tutils'
-
describe('OptionMenu Component', () => {
it('should render correctly', () => {
let store = TStore(),
diff --git a/web/src/js/__tests__/components/Header/__snapshots__/FileMenuSpec.js.snap b/web/src/js/__tests__/components/Header/__snapshots__/FileMenuSpec.js.snap
index 13ecf3f5..f4018fcd 100644
--- a/web/src/js/__tests__/components/Header/__snapshots__/FileMenuSpec.js.snap
+++ b/web/src/js/__tests__/components/Header/__snapshots__/FileMenuSpec.js.snap
@@ -66,10 +66,6 @@ exports[`FileMenu Component should render correctly 1`] = `
<hr
className="divider"
/>
-
- </li>
- <li>
-
<a
href="http://mitm.it/"
target="_blank"
diff --git a/web/src/js/__tests__/components/Header/__snapshots__/FlowMenuSpec.js.snap b/web/src/js/__tests__/components/Header/__snapshots__/FlowMenuSpec.js.snap
index b0b28f1b..2ed3600e 100644
--- a/web/src/js/__tests__/components/Header/__snapshots__/FlowMenuSpec.js.snap
+++ b/web/src/js/__tests__/components/Header/__snapshots__/FlowMenuSpec.js.snap
@@ -33,7 +33,7 @@ exports[`FlowMenu Component should connect to state 1`] = `
<div
className="btn btn-default"
disabled={true}
- onClick={false}
+ onClick={undefined}
title="revert changes to flow [V]"
>
<i
@@ -92,7 +92,7 @@ exports[`FlowMenu Component should connect to state 1`] = `
<div
className="btn btn-default"
disabled={true}
- onClick={false}
+ onClick={undefined}
title="[a]ccept intercepted flow"
>
<i
@@ -103,7 +103,7 @@ exports[`FlowMenu Component should connect to state 1`] = `
<div
className="btn btn-default"
disabled={true}
- onClick={false}
+ onClick={undefined}
title="kill intercepted flow [x]"
>
<i
diff --git a/web/src/js/__tests__/components/Header/__snapshots__/OptionMenuSpec.js.snap b/web/src/js/__tests__/components/Header/__snapshots__/OptionMenuSpec.js.snap
index 9299e69f..83d9355c 100644
--- a/web/src/js/__tests__/components/Header/__snapshots__/OptionMenuSpec.js.snap
+++ b/web/src/js/__tests__/components/Header/__snapshots__/OptionMenuSpec.js.snap
@@ -9,46 +9,24 @@ exports[`OptionMenu Component should render correctly 1`] = `
className="menu-content"
>
<div
- className="menu-entry"
- >
- <label>
- <input
- checked={false}
- onChange={[Function]}
- type="checkbox"
- />
- HTTP/2.0
- </label>
- </div>
- <div
- className="menu-entry"
- >
- <label>
- <input
- checked={false}
- onChange={[Function]}
- type="checkbox"
- />
- WebSockets
- </label>
- </div>
- <div
- className="menu-entry"
+ className="btn btn-default"
+ disabled={undefined}
+ onClick={[Function]}
+ title="Open Options"
>
- <label>
- <input
- checked={false}
- onChange={[Function]}
- type="checkbox"
- />
- Raw TCP
- </label>
+ <i
+ className="fa fa-fw fa-cogs text-primary"
+ />
+ Edit Options
+ <sup>
+ alpha
+ </sup>
</div>
</div>
<div
className="menu-legend"
>
- Protocol Support
+ Options Editor
</div>
</div>
<div
@@ -66,7 +44,7 @@ exports[`OptionMenu Component should render correctly 1`] = `
onChange={[Function]}
type="checkbox"
/>
- Disable Caching
+ Strip cache headers
<a
href="http://docs.mitmproxy.org/en/stable/features/anticache.html"
target="_blank"
@@ -86,18 +64,26 @@ exports[`OptionMenu Component should render correctly 1`] = `
onChange={[Function]}
type="checkbox"
/>
- Disable Compression
- <i
- className="fa fa-question-circle"
- title="Do not forward Accept-Encoding headers to the server to force an uncompressed response."
+ Use host header for display
+ </label>
+ </div>
+ <div
+ className="menu-entry"
+ >
+ <label>
+ <input
+ checked={false}
+ onChange={[Function]}
+ type="checkbox"
/>
+ Verify server certificates
</label>
</div>
</div>
<div
className="menu-legend"
>
- HTTP Options
+ Quick Options
</div>
</div>
<div
@@ -111,22 +97,6 @@ exports[`OptionMenu Component should render correctly 1`] = `
>
<label>
<input
- checked={false}
- onChange={[Function]}
- type="checkbox"
- />
- Use Host Header
- <i
- className="fa fa-question-circle"
- title="Use the Host header to construct URLs for display."
- />
- </label>
- </div>
- <div
- className="menu-entry"
- >
- <label>
- <input
checked={true}
onChange={[Function]}
type="checkbox"
diff --git a/web/src/js/__tests__/components/Modal/ModalSpec.js b/web/src/js/__tests__/components/Modal/ModalSpec.js
new file mode 100644
index 00000000..e4880d08
--- /dev/null
+++ b/web/src/js/__tests__/components/Modal/ModalSpec.js
@@ -0,0 +1,30 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import Modal from '../../../components/Modal/Modal'
+import { Provider } from 'react-redux'
+import { TStore } from '../../ducks/tutils'
+
+describe('Modal Component', () => {
+ let store = TStore()
+
+ it('should render correctly', () => {
+ // hide modal by default
+ let provider = renderer.create(
+ <Provider store={store}>
+ <Modal/>
+ </Provider>
+ ),
+ tree = provider.toJSON()
+ expect(tree).toMatchSnapshot()
+
+ // option modal show up
+ store.getState().ui.modal.activeModal = 'OptionModal'
+ provider = renderer.create(
+ <Provider store={store}>
+ <Modal/>
+ </Provider>
+ )
+ tree = provider.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+})
diff --git a/web/src/js/__tests__/components/Modal/OptionModalSpec.js b/web/src/js/__tests__/components/Modal/OptionModalSpec.js
new file mode 100644
index 00000000..dd4e70a2
--- /dev/null
+++ b/web/src/js/__tests__/components/Modal/OptionModalSpec.js
@@ -0,0 +1,54 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import { PureOptionDefault } from '../../../components/Modal/OptionModal'
+
+describe('PureOptionDefault Component', () => {
+
+ it('should return null when the value is default', () => {
+ let pureOptionDefault = renderer.create(
+ <PureOptionDefault value="foo" defaultVal="foo"/>
+ ),
+ tree = pureOptionDefault.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle boolean type', () => {
+ let pureOptionDefault = renderer.create(
+ <PureOptionDefault value={true} defaultVal={false}/>
+ ),
+ tree = pureOptionDefault.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle array', () => {
+ let a = [""], b = [], c = ['c'],
+ pureOptionDefault = renderer.create(
+ <PureOptionDefault value={a} defaultVal={b}/>
+ ),
+ tree = pureOptionDefault.toJSON()
+ expect(tree).toMatchSnapshot()
+
+ pureOptionDefault = renderer.create(
+ <PureOptionDefault value={a} defaultVal={c}/>
+ )
+ tree = pureOptionDefault.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle string', () => {
+ let pureOptionDefault = renderer.create(
+ <PureOptionDefault value="foo" defaultVal=""/>
+ ),
+ tree = pureOptionDefault.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle null value', () => {
+ let pureOptionDefault = renderer.create(
+ <PureOptionDefault value="foo" defaultVal={null}/>
+ ),
+ tree = pureOptionDefault.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+})
diff --git a/web/src/js/__tests__/components/Modal/OptionSpec.js b/web/src/js/__tests__/components/Modal/OptionSpec.js
new file mode 100644
index 00000000..a275aee6
--- /dev/null
+++ b/web/src/js/__tests__/components/Modal/OptionSpec.js
@@ -0,0 +1,99 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import { Options, ChoicesOption } from '../../../components/Modal/Option'
+
+describe('BooleanOption Component', () => {
+ let BooleanOption = Options['bool'],
+ onChangeFn = jest.fn(),
+ booleanOption = renderer.create(
+ <BooleanOption value={true} onChange={onChangeFn}/>
+ ),
+ tree = booleanOption.toJSON()
+
+ it('should render correctly', () => {
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle onChange', () => {
+ let input = tree.children[0].children[0],
+ mockEvent = { target: { checked: true }}
+ input.props.onChange(mockEvent)
+ expect(onChangeFn).toBeCalledWith(mockEvent.target.checked)
+ })
+})
+
+describe('StringOption Component', () => {
+ let StringOption = Options['str'],
+ onChangeFn = jest.fn(),
+ stringOption = renderer.create(
+ <StringOption value="foo" onChange={onChangeFn}/>
+ ),
+ tree = stringOption.toJSON()
+
+ it('should render correctly', () => {
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle onChange', () => {
+ let mockEvent = { target: { value: 'bar' }}
+ tree.props.onChange(mockEvent)
+ expect(onChangeFn).toBeCalledWith(mockEvent.target.value)
+ })
+
+})
+
+describe('NumberOption Component', () => {
+ let NumberOption = Options['int'],
+ onChangeFn = jest.fn(),
+ numberOption = renderer.create(
+ <NumberOption value={1} onChange={onChangeFn}/>
+ ),
+ tree = numberOption.toJSON()
+
+ it('should render correctly', () => {
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle onChange', () => {
+ let mockEvent = {target: { value: '2'}}
+ tree.props.onChange(mockEvent)
+ expect(onChangeFn).toBeCalledWith(2)
+ })
+})
+
+describe('ChoiceOption Component', () => {
+ let onChangeFn = jest.fn(),
+ choiceOption = renderer.create(
+ <ChoicesOption value='a' choices={['a', 'b', 'c']} onChange={onChangeFn}/>
+ ),
+ tree = choiceOption.toJSON()
+
+ it('should render correctly', () => {
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle onChange', () => {
+ let mockEvent = { target: {value: 'b'} }
+ tree.props.onChange(mockEvent)
+ expect(onChangeFn).toBeCalledWith(mockEvent.target.value)
+ })
+})
+
+describe('StringOption Component', () => {
+ let onChangeFn = jest.fn(),
+ StringSequenceOption = Options['sequence of str'],
+ stringSequenceOption = renderer.create(
+ <StringSequenceOption value={['a', 'b']} onChange={onChangeFn}/>
+ ),
+ tree = stringSequenceOption.toJSON()
+
+ it('should render correctly', () => {
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle onChange', () => {
+ let mockEvent = { target: {value: 'a\nb\nc\n'}}
+ tree.props.onChange(mockEvent)
+ expect(onChangeFn).toBeCalledWith(['a', 'b', 'c', ''])
+ })
+})
diff --git a/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap b/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap
new file mode 100644
index 00000000..8d9271f1
--- /dev/null
+++ b/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap
@@ -0,0 +1,255 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Modal Component should render correctly 1`] = `<div />`;
+
+exports[`Modal Component should render correctly 2`] = `
+<div>
+ <div
+ className="modal-backdrop fade in"
+ />
+ <div
+ aria-labelledby="options"
+ className="modal modal-visible"
+ id="optionsModal"
+ role="dialog"
+ tabIndex="-1"
+ >
+ <div
+ className="modal-dialog modal-lg"
+ role="document"
+ >
+ <div
+ className="modal-content"
+ >
+ <div>
+ <div
+ className="modal-header"
+ >
+ <button
+ className="close"
+ data-dismiss="modal"
+ onClick={[Function]}
+ type="button"
+ >
+ <i
+ className="fa fa-fw fa-times"
+ />
+ </button>
+ <div
+ className="modal-title"
+ >
+ <h4>
+ Options
+ </h4>
+ </div>
+ </div>
+ <div
+ className="modal-body"
+ >
+ <div
+ className="form-horizontal"
+ >
+ <div
+ className="form-group"
+ >
+ <div
+ className="col-xs-6"
+ >
+ <label
+ htmlFor="booleanOption"
+ >
+ booleanOption
+ </label>
+ <div
+ className="help-block small"
+ >
+ foo
+ </div>
+ </div>
+ <div
+ className="col-xs-6"
+ >
+ <div
+ className=""
+ >
+ <div
+ className="checkbox"
+ >
+ <label>
+ <input
+ checked={false}
+ name="booleanOption"
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ type="checkbox"
+ />
+ Enable
+ </label>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div
+ className="form-group"
+ >
+ <div
+ className="col-xs-6"
+ >
+ <label
+ htmlFor="choiceOption"
+ >
+ choiceOption
+ </label>
+ <div
+ className="help-block small"
+ >
+ foo
+ </div>
+ </div>
+ <div
+ className="col-xs-6"
+ >
+ <div
+ className=""
+ >
+ <select
+ className="form-control"
+ name="choiceOption"
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ value="b"
+ >
+ <option
+ value="a"
+ >
+ a
+ </option>
+ <option
+ value="b"
+ >
+ b
+ </option>
+ <option
+ value="c"
+ >
+ c
+ </option>
+ </select>
+ </div>
+ <div
+ className="small"
+ >
+ Default:
+ <strong>
+
+ a
+
+ </strong>
+
+ </div>
+ </div>
+ </div>
+ <div
+ className="form-group"
+ >
+ <div
+ className="col-xs-6"
+ >
+ <label
+ htmlFor="intOption"
+ >
+ intOption
+ </label>
+ <div
+ className="help-block small"
+ >
+ foo
+ </div>
+ </div>
+ <div
+ className="col-xs-6"
+ >
+ <div
+ className=""
+ >
+ <input
+ className="form-control"
+ name="intOption"
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ type="number"
+ value={1}
+ />
+ </div>
+ <div
+ className="small"
+ >
+ Default:
+ <strong>
+
+ 0
+
+ </strong>
+
+ </div>
+ </div>
+ </div>
+ <div
+ className="form-group"
+ >
+ <div
+ className="col-xs-6"
+ >
+ <label
+ htmlFor="strOption"
+ >
+ strOption
+ </label>
+ <div
+ className="help-block small"
+ >
+ foo
+ </div>
+ </div>
+ <div
+ className="col-xs-6"
+ >
+ <div
+ className="has-error"
+ >
+ <input
+ className="form-control"
+ name="strOption"
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ type="text"
+ value="str content"
+ />
+ </div>
+ <div
+ className="small text-danger"
+ />
+ <div
+ className="small"
+ >
+ Default:
+ <strong>
+
+ null
+
+ </strong>
+
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div
+ className="modal-footer"
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+`;
diff --git a/web/src/js/__tests__/components/Modal/__snapshots__/OptionModalSpec.js.snap b/web/src/js/__tests__/components/Modal/__snapshots__/OptionModalSpec.js.snap
new file mode 100644
index 00000000..68f1c9fc
--- /dev/null
+++ b/web/src/js/__tests__/components/Modal/__snapshots__/OptionModalSpec.js.snap
@@ -0,0 +1,61 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`PureOptionDefault Component should handle array 1`] = `null`;
+
+exports[`PureOptionDefault Component should handle array 2`] = `
+<div
+ className="small"
+>
+ Default:
+ <strong>
+
+ [ ]
+
+ </strong>
+
+</div>
+`;
+
+exports[`PureOptionDefault Component should handle boolean type 1`] = `
+<div
+ className="small"
+>
+ Default:
+ <strong>
+
+ false
+
+ </strong>
+
+</div>
+`;
+
+exports[`PureOptionDefault Component should handle null value 1`] = `
+<div
+ className="small"
+>
+ Default:
+ <strong>
+
+ null
+
+ </strong>
+
+</div>
+`;
+
+exports[`PureOptionDefault Component should handle string 1`] = `
+<div
+ className="small"
+>
+ Default:
+ <strong>
+
+ ""
+
+ </strong>
+
+</div>
+`;
+
+exports[`PureOptionDefault Component should return null when the value is default 1`] = `null`;
diff --git a/web/src/js/__tests__/components/Modal/__snapshots__/OptionSpec.js.snap b/web/src/js/__tests__/components/Modal/__snapshots__/OptionSpec.js.snap
new file mode 100644
index 00000000..257bddce
--- /dev/null
+++ b/web/src/js/__tests__/components/Modal/__snapshots__/OptionSpec.js.snap
@@ -0,0 +1,64 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`BooleanOption Component should render correctly 1`] = `
+<div
+ className="checkbox"
+>
+ <label>
+ <input
+ checked={true}
+ onChange={[Function]}
+ type="checkbox"
+ />
+ Enable
+ </label>
+</div>
+`;
+
+exports[`ChoiceOption Component should render correctly 1`] = `
+<select
+ onChange={[Function]}
+ value="a"
+>
+ <option
+ value="a"
+ >
+ a
+ </option>
+ <option
+ value="b"
+ >
+ b
+ </option>
+ <option
+ value="c"
+ >
+ c
+ </option>
+</select>
+`;
+
+exports[`NumberOption Component should render correctly 1`] = `
+<input
+ onChange={[Function]}
+ type="number"
+ value={1}
+/>
+`;
+
+exports[`StringOption Component should render correctly 1`] = `
+<input
+ onChange={[Function]}
+ type="text"
+ value="foo"
+/>
+`;
+
+exports[`StringOption Component should render correctly 2`] = `
+<textarea
+ onChange={[Function]}
+ rows={2}
+ value="a
+b"
+/>
+`;
diff --git a/web/src/js/__tests__/components/__snapshots__/ContentViewSpec.js.snap b/web/src/js/__tests__/components/__snapshots__/ContentViewSpec.js.snap
new file mode 100644
index 00000000..1ee71d52
--- /dev/null
+++ b/web/src/js/__tests__/components/__snapshots__/ContentViewSpec.js.snap
@@ -0,0 +1,80 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ContentView Component should render correctly 1`] = `
+<div
+ className="contentview"
+>
+ <div
+ className="text-center"
+ >
+ <i
+ className="fa fa-spinner fa-spin"
+ />
+ </div>
+</div>
+`;
+
+exports[`ContentView Component should render correctly with content too large 1`] = `
+<div>
+ <div
+ className="alert alert-warning"
+ >
+ <button
+ className="btn btn-xs btn-warning pull-right"
+ onClick={[Function]}
+ >
+ Display anyway
+ </button>
+ 100mb
+ content size.
+ </div>
+ <div
+ className="view-options text-center"
+ >
+ <a
+ className="btn btn-default btn-xs"
+ href="#"
+ onClick={[Function]}
+ title="Upload a file to replace the content."
+ >
+ <i
+ className="fa fa-fw fa-upload"
+ />
+ <input
+ className="hidden"
+ onChange={[Function]}
+ type="file"
+ />
+ </a>
+  
+ <a
+ className="btn btn-default btn-xs"
+ href="./flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/response/content.data"
+ title="Download the content of the flow."
+ >
+ <i
+ className="fa fa-download"
+ />
+ </a>
+ </div>
+</div>
+`;
+
+exports[`ContentView Component should render correctly with empty content 1`] = `
+<div
+ className="alert alert-info"
+>
+ No
+ response
+ content.
+</div>
+`;
+
+exports[`ContentView Component should render correctly with missing content 1`] = `
+<div
+ className="alert alert-info"
+>
+ Response
+ content missing.
+</div>
+`;
diff --git a/web/src/js/__tests__/components/__snapshots__/EventLogSpec.js.snap b/web/src/js/__tests__/components/__snapshots__/EventLogSpec.js.snap
new file mode 100644
index 00000000..11c3a29e
--- /dev/null
+++ b/web/src/js/__tests__/components/__snapshots__/EventLogSpec.js.snap
@@ -0,0 +1,76 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EventLog Component should connect to state and render correctly 1`] = `
+<div
+ className="eventlog"
+ style={
+ Object {
+ "height": 200,
+ }
+ }
+>
+ <div
+ onMouseDown={[Function]}
+ >
+ Eventlog
+ <div
+ className="pull-right"
+ >
+ <div
+ className="btn btn-toggle btn-primary"
+ onClick={[Function]}
+ >
+ <i
+ className="fa fa-fw fa-check-square-o"
+ />
+  
+ debug
+ </div>
+ <div
+ className="btn btn-toggle btn-primary"
+ onClick={[Function]}
+ >
+ <i
+ className="fa fa-fw fa-check-square-o"
+ />
+  
+ info
+ </div>
+ <div
+ className="btn btn-toggle btn-default"
+ onClick={[Function]}
+ >
+ <i
+ className="fa fa-fw fa-square-o"
+ />
+  
+ web
+ </div>
+ <div
+ className="btn btn-toggle btn-primary"
+ onClick={[Function]}
+ >
+ <i
+ className="fa fa-fw fa-check-square-o"
+ />
+  
+ warn
+ </div>
+ <div
+ className="btn btn-toggle btn-primary"
+ onClick={[Function]}
+ >
+ <i
+ className="fa fa-fw fa-check-square-o"
+ />
+  
+ error
+ </div>
+ <i
+ className="fa fa-close"
+ onClick={[Function]}
+ />
+ </div>
+ </div>
+</div>
+`;
diff --git a/web/src/js/__tests__/components/__snapshots__/FlowTableSpec.js.snap b/web/src/js/__tests__/components/__snapshots__/FlowTableSpec.js.snap
new file mode 100644
index 00000000..7149903c
--- /dev/null
+++ b/web/src/js/__tests__/components/__snapshots__/FlowTableSpec.js.snap
@@ -0,0 +1,79 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FlowTable Component should render correctly 1`] = `
+<div
+ className="flow-table"
+ onScroll={[Function]}
+>
+ <table>
+ <thead
+ style={
+ Object {
+ "transform": "translateY(undefinedpx)",
+ }
+ }
+ >
+ <tr>
+ <th
+ className="col-tls"
+ onClick={[Function]}
+ >
+
+ </th>
+ <th
+ className="col-icon"
+ onClick={[Function]}
+ >
+
+ </th>
+ <th
+ className="col-path sort-desc"
+ onClick={[Function]}
+ >
+ Path
+ </th>
+ <th
+ className="col-method"
+ onClick={[Function]}
+ >
+ Method
+ </th>
+ <th
+ className="col-status"
+ onClick={[Function]}
+ >
+ Status
+ </th>
+ <th
+ className="col-size"
+ onClick={[Function]}
+ >
+ Size
+ </th>
+ <th
+ className="col-time"
+ onClick={[Function]}
+ >
+ Time
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr
+ style={
+ Object {
+ "height": 0,
+ }
+ }
+ />
+ <tr
+ style={
+ Object {
+ "height": 0,
+ }
+ }
+ />
+ </tbody>
+ </table>
+</div>
+`;
diff --git a/web/src/js/__tests__/components/common/__snapshots__/ButtonSpec.js.snap b/web/src/js/__tests__/components/common/__snapshots__/ButtonSpec.js.snap
index 1d403b2d..8b1c9d6d 100644
--- a/web/src/js/__tests__/components/common/__snapshots__/ButtonSpec.js.snap
+++ b/web/src/js/__tests__/components/common/__snapshots__/ButtonSpec.js.snap
@@ -4,7 +4,7 @@ exports[`Button Component should be able to be disabled 1`] = `
<div
className="classname btn btn-default"
disabled="true"
- onClick={false}
+ onClick={undefined}
title={undefined}
>
<a>
diff --git a/web/src/js/__tests__/ducks/_tflow.js b/web/src/js/__tests__/ducks/_tflow.js
index f6a382bd..44b32342 100644
--- a/web/src/js/__tests__/ducks/_tflow.js
+++ b/web/src/js/__tests__/ducks/_tflow.js
@@ -2,7 +2,7 @@ export default function(){
return {
"client_conn": {
"address": [
- "address",
+ "127.0.0.1",
22
],
"alpn_proto_negotiated": "http/1.1",
diff --git a/web/src/js/__tests__/ducks/flowsSpec.js b/web/src/js/__tests__/ducks/flowsSpec.js
index 5bd866f2..d749d9e1 100644
--- a/web/src/js/__tests__/ducks/flowsSpec.js
+++ b/web/src/js/__tests__/ducks/flowsSpec.js
@@ -157,7 +157,9 @@ describe('flows actions', () => {
file = new window.Blob(['foo'], { type: 'plain/text' })
body.append('file', file)
store.dispatch(flowActions.uploadContent(tflow, 'foo', 'foo'))
- expect(fetchApi).toBeCalledWith('/flows/1/foo/content', { method: 'POST', body})
+ // window.Blob's lastModified is always the current time,
+ // which causes flaky tests on comparison.
+ expect(fetchApi).toBeCalledWith('/flows/1/foo/content', { method: 'POST', body: expect.anything()})
})
it('should handle clear action', () => {
diff --git a/web/src/js/__tests__/ducks/optionsSpec.js b/web/src/js/__tests__/ducks/optionsSpec.js
new file mode 100644
index 00000000..d3f9b8e5
--- /dev/null
+++ b/web/src/js/__tests__/ducks/optionsSpec.js
@@ -0,0 +1,66 @@
+import reduceOptions, * as OptionsActions from '../../ducks/options'
+import configureStore from 'redux-mock-store'
+import thunk from 'redux-thunk'
+import * as OptionsEditorActions from '../../ducks/ui/optionsEditor'
+
+const mockStore = configureStore([ thunk ])
+
+describe('option reducer', () => {
+ it('should return initial state', () => {
+ expect(reduceOptions(undefined, {})).toEqual({})
+ })
+
+ it('should handle receive action', () => {
+ let action = { type: OptionsActions.RECEIVE, data: 'foo' }
+ expect(reduceOptions(undefined, action)).toEqual('foo')
+ })
+
+ it('should handle update action', () => {
+ let action = {type: OptionsActions.UPDATE, data: {id: 1} }
+ expect(reduceOptions(undefined, action)).toEqual({id: 1})
+ })
+})
+
+let store = mockStore()
+
+describe('option actions', () => {
+
+ it('should be possible to update option', () => {
+ let mockResponse = { status: 200 },
+ promise = Promise.resolve(mockResponse)
+ global.fetch = r => { return promise }
+ store.dispatch(OptionsActions.update('foo', 'bar'))
+ expect(store.getActions()).toEqual([
+ { type: OptionsEditorActions.OPTION_UPDATE_START, option: 'foo', value: 'bar'}
+ ])
+ store.clearActions()
+ })
+})
+
+describe('sendUpdate', () => {
+
+ it('should handle error', () => {
+ let mockResponse = { status: 400, text: p => Promise.resolve('error') },
+ promise = Promise.resolve(mockResponse)
+ global.fetch = r => { return promise }
+ OptionsActions.pureSendUpdate('bar', 'error')
+ expect(store.getActions()).toEqual([
+ { type: OptionsEditorActions.OPTION_UPDATE_SUCCESS, option: 'foo'}
+ ])
+ })
+})
+
+describe('save', () => {
+
+ it('should dump options', () => {
+ global.fetch = jest.fn()
+ store.dispatch(OptionsActions.save())
+ expect(fetch).toBeCalledWith(
+ './options/save?_xsrf=undefined',
+ {
+ credentials: "same-origin",
+ method: "POST"
+ }
+ )
+ })
+})
diff --git a/web/src/js/__tests__/ducks/tutils.js b/web/src/js/__tests__/ducks/tutils.js
index 2a79ede0..22240448 100644
--- a/web/src/js/__tests__/ducks/tutils.js
+++ b/web/src/js/__tests__/ducks/tutils.js
@@ -32,6 +32,15 @@ export function TStore(){
},
header: {
tab: 'Start'
+ },
+ modal: {
+ activeModal: undefined
+ },
+ optionsEditor: {
+ booleanOption: { isUpdating: true, error: false },
+ strOption: { error: true },
+ intOption: {},
+ choiceOption: {},
}
},
settings: {
@@ -39,6 +48,36 @@ export function TStore(){
anticache: true,
anticomp: false
},
+ options: {
+ booleanOption: {
+ choices: null,
+ default: false,
+ help: "foo",
+ type: "bool",
+ value: false
+ },
+ strOption: {
+ choices: null,
+ default: null,
+ help: "foo",
+ type: "str",
+ value: "str content"
+ },
+ intOption: {
+ choices: null,
+ default: 0,
+ help: "foo",
+ type: "int",
+ value: 1
+ },
+ choiceOption: {
+ choices: ['a', 'b', 'c'],
+ default: 'a',
+ help: "foo",
+ type: "str",
+ value: "b"
+ },
+ },
flows: {
selected: ["d91165be-ca1f-4612-88a9-c0f8696f3e29"],
byId: {"d91165be-ca1f-4612-88a9-c0f8696f3e29": tflow},
@@ -47,7 +86,8 @@ export function TStore(){
sort: {
desc: true,
column: 'PathColumn'
- }
+ },
+ view: [ tflow ]
},
connection: {
state: ConnectionState.ESTABLISHED
diff --git a/web/src/js/__tests__/ducks/ui/flowSpec.js b/web/src/js/__tests__/ducks/ui/flowSpec.js
index cd6ffa2f..11ca021e 100644
--- a/web/src/js/__tests__/ducks/ui/flowSpec.js
+++ b/web/src/js/__tests__/ducks/ui/flowSpec.js
@@ -11,7 +11,7 @@ import reducer, {
displayLarge
} from '../../../ducks/ui/flow'
-import { select, updateFlow } from '../../../ducks/flows'
+import * as flowActions from '../../../ducks/flows'
describe('flow reducer', () => {
it('should return initial state', () => {
@@ -61,11 +61,11 @@ describe('flow reducer', () => {
})
it('should not change the contentview mode', () => {
- expect(reducer({contentView: 'foo'}, select(1)).contentView).toEqual('foo')
+ expect(reducer({contentView: 'foo'}, flowActions.select(1)).contentView).toEqual('foo')
})
it('should change the contentview mode to auto after editing when a new flow will be selected', () => {
- expect(reducer({contentView: 'foo', modifiedFlow : 'test_flow'}, select(1)).contentView).toEqual('Auto')
+ expect(reducer({contentView: 'foo', modifiedFlow : 'test_flow'}, flowActions.select(1)).contentView).toEqual('Auto')
})
it('should set update and merge the modifiedflow with the update values', () => {
@@ -84,7 +84,11 @@ describe('flow reducer', () => {
it('should stop editing when the selected flow is updated', () => {
let modifiedFlow = {id: 1}
let updatedFlow = {id: 1}
- expect(reducer({modifiedFlow}, stopEdit(updatedFlow, modifiedFlow)).modifiedFlow).toBeFalsy()
+ expect(reducer(
+ { modifiedFlow },
+ {type: flowActions.UPDATE, data: modifiedFlow}
+ ).modifiedFlow
+ ).toBeFalsy()
})
it('should set content view', () => {
diff --git a/web/src/js/__tests__/ducks/ui/keyboardSpec.js b/web/src/js/__tests__/ducks/ui/keyboardSpec.js
index 500733cb..cf17943f 100644
--- a/web/src/js/__tests__/ducks/ui/keyboardSpec.js
+++ b/web/src/js/__tests__/ducks/ui/keyboardSpec.js
@@ -6,6 +6,7 @@ import reduceFlows from '../../../ducks/flows'
import reduceUI from '../../../ducks/ui/index'
import * as flowsActions from '../../../ducks/flows'
import * as UIActions from '../../../ducks/ui/flow'
+import * as modalActions from '../../../ducks/ui/modal'
import configureStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import { fetchApi } from '../../../utils'
@@ -154,4 +155,10 @@ describe('onKeyDown', () => {
expect(fetchApi).not.toBeCalled()
})
+ it('should close modal', () => {
+ store.getState().ui.modal.activeModal = true
+ store.dispatch(createKeyEvent(Key.ESC))
+ expect(store.getActions()).toEqual([ {type: modalActions.HIDE_MODAL} ])
+ })
+
})
diff --git a/web/src/js/__tests__/ducks/ui/modalSpec.js b/web/src/js/__tests__/ducks/ui/modalSpec.js
new file mode 100644
index 00000000..30c39760
--- /dev/null
+++ b/web/src/js/__tests__/ducks/ui/modalSpec.js
@@ -0,0 +1,25 @@
+import reduceModal, * as ModalActions from '../../../ducks/ui/modal'
+
+describe('modal reducer', () => {
+ let state = undefined
+
+ it('should return the initial state', () => {
+ expect(reduceModal(undefined, {})).toEqual(
+ { activeModal: undefined }
+ )
+ })
+
+ it('should handle setActiveModal action', () => {
+ state = reduceModal(undefined, ModalActions.setActiveModal('foo'))
+ expect(state).toEqual(
+ { activeModal: 'foo' }
+ )
+ })
+
+ it('should handle hideModal action', () => {
+ state = reduceModal(state, ModalActions.hideModal())
+ expect(state).toEqual(
+ { activeModal: undefined }
+ )
+ })
+})
diff --git a/web/src/js/__tests__/ducks/ui/optionEditorSpec.js b/web/src/js/__tests__/ducks/ui/optionEditorSpec.js
new file mode 100644
index 00000000..df9161a4
--- /dev/null
+++ b/web/src/js/__tests__/ducks/ui/optionEditorSpec.js
@@ -0,0 +1,32 @@
+import reduceOptionsEditor, * as optionsEditorActions from '../../../ducks/ui/optionsEditor'
+import { HIDE_MODAL } from '../../../ducks/ui/modal'
+
+describe('optionsEditor reducer', () => {
+
+ it('should return initial state', () => {
+ expect(reduceOptionsEditor(undefined, {})).toEqual({})
+ })
+
+ let state = undefined
+ it('should handle option update start', () => {
+ state = reduceOptionsEditor(undefined, optionsEditorActions.startUpdate('foo', 'bar'))
+ expect(state).toEqual({ foo: {error: false, isUpdating: true, value: 'bar'}})
+ })
+
+ it('should handle option update success', () => {
+ expect(reduceOptionsEditor(state, optionsEditorActions.updateSuccess('foo'))).toEqual({foo: undefined})
+ })
+
+ it('should handle option update error', () => {
+ state = reduceOptionsEditor(state, optionsEditorActions.updateError('foo', 'errorMsg'))
+ expect(state).toEqual({ foo: {error: 'errorMsg', isUpdating: false, value: 'bar'}})
+ // boolean type
+ state = reduceOptionsEditor(undefined, optionsEditorActions.startUpdate('foo', true))
+ state = reduceOptionsEditor(state, optionsEditorActions.updateError('foo', 'errorMsg'))
+ expect(state).toEqual({ foo: {error: 'errorMsg', isUpdating: false, value: false}})
+ })
+
+ it('should handle hide modal', () => {
+ expect(reduceOptionsEditor(undefined, {type: HIDE_MODAL})).toEqual({})
+ })
+})
diff --git a/web/src/js/__tests__/flow/utilsSpec.js b/web/src/js/__tests__/flow/utilsSpec.js
index 2d8f0456..dc84001e 100644
--- a/web/src/js/__tests__/flow/utilsSpec.js
+++ b/web/src/js/__tests__/flow/utilsSpec.js
@@ -25,15 +25,15 @@ describe('MessageUtils', () => {
let msg = "foo", view = "bar",
flow = { request: msg, id: 1}
expect(utils.MessageUtils.getContentURL(flow, msg, view)).toEqual(
- "/flows/1/request/content/bar"
+ "./flows/1/request/content/bar.json"
)
expect(utils.MessageUtils.getContentURL(flow, msg, '')).toEqual(
- "/flows/1/request/content"
+ "./flows/1/request/content.data"
)
// response
flow = {response: msg, id: 2}
expect(utils.MessageUtils.getContentURL(flow, msg, view)).toEqual(
- "/flows/2/response/content/bar"
+ "./flows/2/response/content/bar.json"
)
})
})
diff --git a/web/src/js/__tests__/utilsSpec.js b/web/src/js/__tests__/utilsSpec.js
index 9a1a0750..d0beca14 100644
--- a/web/src/js/__tests__/utilsSpec.js
+++ b/web/src/js/__tests__/utilsSpec.js
@@ -83,11 +83,6 @@ describe('pure', () => {
expect(utils.pure(tFunc).displayName).toEqual('tFunc')
})
- it('should suggest when should component update', () => {
- expect(f.shouldComponentUpdate('foo')).toBeTruthy()
- expect(f.shouldComponentUpdate('bar')).toBeFalsy()
- })
-
it('should render properties', () => {
expect(f.render()).toEqual(tFunc('bar'))
})
diff --git a/web/src/js/app.jsx b/web/src/js/app.jsx
index a94d2ef6..ee660fd6 100644
--- a/web/src/js/app.jsx
+++ b/web/src/js/app.jsx
@@ -9,13 +9,14 @@ import rootReducer from './ducks/index'
import { add as addLog } from './ducks/eventLog'
import useUrlState from './urlState'
import WebSocketBackend from './backends/websocket'
+import StaticBackend from './backends/static'
+import { logger } from 'redux-logger'
const middlewares = [thunk];
if (process.env.NODE_ENV !== 'production') {
- const createLogger = require('redux-logger');
- middlewares.push(createLogger());
+ middlewares.push(logger);
}
// logger must be last
@@ -25,7 +26,11 @@ const store = createStore(
)
useUrlState(store)
-window.backend = new WebSocketBackend(store)
+if (MITMWEB_STATIC) {
+ window.backend = new StaticBackend(store)
+} else {
+ window.backend = new WebSocketBackend(store)
+}
window.addEventListener('error', msg => {
store.dispatch(addLog(msg))
diff --git a/web/src/js/backends/static.js b/web/src/js/backends/static.js
new file mode 100644
index 00000000..1da222ce
--- /dev/null
+++ b/web/src/js/backends/static.js
@@ -0,0 +1,32 @@
+/*
+ * This backend uses the REST API only to host static instances,
+ * without any Websocket connection.
+ */
+import { fetchApi } from "../utils"
+
+export default class StaticBackend {
+ constructor(store) {
+ this.store = store
+ this.onOpen()
+ }
+
+ onOpen() {
+ this.fetchData("flows")
+ this.fetchData("settings")
+ // this.fetchData("events") # TODO: Add events log to static viewer.
+ }
+
+ fetchData(resource) {
+ fetchApi(`./${resource}`)
+ .then(res => res.json())
+ .then(json => {
+ this.receive(resource, json)
+ })
+ }
+
+ receive(resource, data) {
+ let type = `${resource}_RECEIVE`.toUpperCase()
+ this.store.dispatch({ type, cmd: "receive", resource, data })
+ }
+
+}
diff --git a/web/src/js/backends/websocket.js b/web/src/js/backends/websocket.js
index 01094ac4..08136617 100644
--- a/web/src/js/backends/websocket.js
+++ b/web/src/js/backends/websocket.js
@@ -27,13 +27,14 @@ export default class WebsocketBackend {
this.fetchData("settings")
this.fetchData("flows")
this.fetchData("events")
+ this.fetchData("options")
this.store.dispatch(connectionActions.startFetching())
}
fetchData(resource) {
let queue = []
this.activeFetches[resource] = queue
- fetchApi(`/${resource}`)
+ fetchApi(`./${resource}`)
.then(res => res.json())
.then(json => {
// Make sure that we are not superseded yet by the server sending a RESET.
diff --git a/web/src/js/components/ContentView.jsx b/web/src/js/components/ContentView.jsx
index a79bf9e5..cb4749c5 100644
--- a/web/src/js/components/ContentView.jsx
+++ b/web/src/js/components/ContentView.jsx
@@ -1,7 +1,7 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
-import * as ContentViews from './ContentView/ContentViews'
+import { Edit, ViewServer, ViewImage } from './ContentView/ContentViews'
import * as MetaViews from './ContentView/MetaViews'
import ShowFullContentButton from './ContentView/ShowFullContentButton'
@@ -16,7 +16,7 @@ ContentView.propTypes = {
message: PropTypes.object.isRequired,
}
-ContentView.isContentTooLarge = msg => msg.contentLength > 1024 * 1024 * (ContentViews.ViewImage.matches(msg) ? 10 : 0.2)
+ContentView.isContentTooLarge = msg => msg.contentLength > 1024 * 1024 * (ViewImage.matches(msg) ? 10 : 0.2)
function ContentView(props) {
const { flow, message, contentView, isDisplayLarge, displayLarge, onContentChange, readonly } = props
@@ -33,10 +33,15 @@ function ContentView(props) {
return <MetaViews.ContentTooLarge {...props} onClick={displayLarge}/>
}
- const View = ContentViews[contentView] || ContentViews['ViewServer']
+ let view;
+ if(contentView === "Edit") {
+ view = <Edit flow={flow} message={message} onChange={onContentChange}/>
+ } else {
+ view = <ViewServer flow={flow} message={message} contentView={contentView}/>
+ }
return (
<div className="contentview">
- <View flow={flow} message={message} contentView={contentView} readonly={readonly} onChange={onContentChange}/>
+ {view}
<ShowFullContentButton/>
</div>
)
diff --git a/web/src/js/components/ContentView/ContentLoader.jsx b/web/src/js/components/ContentView/ContentLoader.jsx
index 4cafde28..44716e12 100644
--- a/web/src/js/components/ContentView/ContentLoader.jsx
+++ b/web/src/js/components/ContentView/ContentLoader.jsx
@@ -2,98 +2,100 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { MessageUtils } from '../../flow/utils.js'
-export default View => class extends React.Component {
+export default function withContentLoader(View) {
+
+ return class extends React.Component {
+ static displayName = View.displayName || View.name
+ static matches = View.matches
- static displayName = View.displayName || View.name
- static matches = View.matches
-
- static propTypes = {
- ...View.propTypes,
- content: PropTypes.string, // mark as non-required
- flow: PropTypes.object.isRequired,
- message: PropTypes.object.isRequired,
- }
-
- constructor(props) {
- super(props)
- this.state = {
- content: undefined,
- request: undefined,
+ static propTypes = {
+ ...View.propTypes,
+ content: PropTypes.string, // mark as non-required
+ flow: PropTypes.object.isRequired,
+ message: PropTypes.object.isRequired,
}
- }
- componentWillMount() {
- this.updateContent(this.props)
- }
-
- componentWillReceiveProps(nextProps) {
- if (
- nextProps.message.content !== this.props.message.content ||
- nextProps.message.contentHash !== this.props.message.contentHash ||
- nextProps.contentView !== this.props.contentView
- ) {
- this.updateContent(nextProps)
+ constructor(props) {
+ super(props)
+ this.state = {
+ content: undefined,
+ request: undefined,
+ }
}
- }
- componentWillUnmount() {
- if (this.state.request) {
- this.state.request.abort()
+ componentWillMount() {
+ this.updateContent(this.props)
}
- }
- updateContent(props) {
- if (this.state.request) {
- this.state.request.abort()
+ componentWillReceiveProps(nextProps) {
+ if (
+ nextProps.message.content !== this.props.message.content ||
+ nextProps.message.contentHash !== this.props.message.contentHash ||
+ nextProps.contentView !== this.props.contentView
+ ) {
+ this.updateContent(nextProps)
+ }
}
- // We have a few special cases where we do not need to make an HTTP request.
- if(props.message.content !== undefined) {
- return this.setState({request: undefined, content: props.message.content})
- }
- if(props.message.contentLength === 0 || props.message.contentLength === null){
- return this.setState({request: undefined, content: ""})
+
+ componentWillUnmount() {
+ if (this.state.request) {
+ this.state.request.abort()
+ }
}
- let requestUrl = MessageUtils.getContentURL(props.flow, props.message, (View.name == 'ViewServer' ? props.contentView : undefined))
+ updateContent(props) {
+ if (this.state.request) {
+ this.state.request.abort()
+ }
+ // We have a few special cases where we do not need to make an HTTP request.
+ if (props.message.content !== undefined) {
+ return this.setState({request: undefined, content: props.message.content})
+ }
+ if (props.message.contentLength === 0 || props.message.contentLength === null) {
+ return this.setState({request: undefined, content: ""})
+ }
- // We use XMLHttpRequest instead of fetch() because fetch() is not (yet) abortable.
- let request = new XMLHttpRequest();
- request.addEventListener("load", this.requestComplete.bind(this, request));
- request.addEventListener("error", this.requestFailed.bind(this, request));
- request.open("GET", requestUrl);
- request.send();
- this.setState({ request, content: undefined })
- }
+ let requestUrl = MessageUtils.getContentURL(props.flow, props.message, props.contentView)
- requestComplete(request, e) {
- if (request !== this.state.request) {
- return // Stale request
+ // We use XMLHttpRequest instead of fetch() because fetch() is not (yet) abortable.
+ let request = new XMLHttpRequest();
+ request.addEventListener("load", this.requestComplete.bind(this, request));
+ request.addEventListener("error", this.requestFailed.bind(this, request));
+ request.open("GET", requestUrl);
+ request.send();
+ this.setState({request, content: undefined})
}
- this.setState({
- content: request.responseText,
- request: undefined
- })
- }
- requestFailed(request, e) {
- if (request !== this.state.request) {
- return // Stale request
+ requestComplete(request, e) {
+ if (request !== this.state.request) {
+ return // Stale request
+ }
+ this.setState({
+ content: request.responseText,
+ request: undefined
+ })
+ }
+
+ requestFailed(request, e) {
+ if (request !== this.state.request) {
+ return // Stale request
+ }
+ console.error(e)
+ // FIXME: Better error handling
+ this.setState({
+ content: "Error getting content.",
+ request: undefined
+ })
}
- console.error(e)
- // FIXME: Better error handling
- this.setState({
- content: "Error getting content.",
- request: undefined
- })
- }
- render() {
- return this.state.content !== undefined ? (
- <View content={this.state.content} {...this.props}/>
- ) : (
- <div className="text-center">
- <i className="fa fa-spinner fa-spin"></i>
- </div>
- )
+ render() {
+ return this.state.content !== undefined ? (
+ <View content={this.state.content} {...this.props}/>
+ ) : (
+ <div className="text-center">
+ <i className="fa fa-spinner fa-spin"></i>
+ </div>
+ )
+ }
}
};
diff --git a/web/src/js/components/ContentView/ContentViews.jsx b/web/src/js/components/ContentView/ContentViews.jsx
index 136188d4..387c940a 100644
--- a/web/src/js/components/ContentView/ContentViews.jsx
+++ b/web/src/js/components/ContentView/ContentViews.jsx
@@ -2,7 +2,7 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { setContentViewDescription, setContent } from '../../ducks/ui/flow'
-import ContentLoader from './ContentLoader'
+import withContentLoader from './ContentLoader'
import { MessageUtils } from '../../flow/utils'
import CodeEditor from './CodeEditor'
@@ -28,9 +28,9 @@ Edit.propTypes = {
function Edit({ content, onChange }) {
return <CodeEditor content={content} onChange={onChange}/>
}
-Edit = ContentLoader(Edit)
+Edit = withContentLoader(Edit)
-class ViewServer extends Component {
+export class PureViewServer extends Component {
static propTypes = {
showFullContent: PropTypes.bool.isRequired,
maxLines: PropTypes.number.isRequired,
@@ -85,7 +85,7 @@ class ViewServer extends Component {
}
-ViewServer = connect(
+const ViewServer = connect(
state => ({
showFullContent: state.ui.flow.showFullContent,
maxLines: state.ui.flow.maxContentLines
@@ -94,6 +94,6 @@ ViewServer = connect(
setContentViewDescription,
setContent
}
-)(ContentLoader(ViewServer))
+)(withContentLoader(PureViewServer))
export { Edit, ViewServer, ViewImage }
diff --git a/web/src/js/components/ContentView/DownloadContentButton.jsx b/web/src/js/components/ContentView/DownloadContentButton.jsx
index 447db211..f32a19ca 100644
--- a/web/src/js/components/ContentView/DownloadContentButton.jsx
+++ b/web/src/js/components/ContentView/DownloadContentButton.jsx
@@ -1,3 +1,4 @@
+import React from 'react'
import { MessageUtils } from "../../flow/utils"
import PropTypes from 'prop-types'
diff --git a/web/src/js/components/ContentView/ShowFullContentButton.jsx b/web/src/js/components/ContentView/ShowFullContentButton.jsx
index fd627ad9..c6d8c2f2 100644
--- a/web/src/js/components/ContentView/ShowFullContentButton.jsx
+++ b/web/src/js/components/ContentView/ShowFullContentButton.jsx
@@ -12,7 +12,7 @@ ShowFullContentButton.propTypes = {
showFullContent: PropTypes.bool.isRequired
}
-function ShowFullContentButton ( {setShowFullContent, showFullContent, visibleLines, contentLines} ){
+export function ShowFullContentButton ( {setShowFullContent, showFullContent, visibleLines, contentLines} ){
return (
!showFullContent &&
diff --git a/web/src/js/components/ContentView/UploadContentButton.jsx b/web/src/js/components/ContentView/UploadContentButton.jsx
index 0021593f..847d4eb0 100644
--- a/web/src/js/components/ContentView/UploadContentButton.jsx
+++ b/web/src/js/components/ContentView/UploadContentButton.jsx
@@ -1,3 +1,4 @@
+import React from 'react'
import PropTypes from 'prop-types'
import FileChooser from '../common/FileChooser'
diff --git a/web/src/js/components/ContentView/ViewSelector.jsx b/web/src/js/components/ContentView/ViewSelector.jsx
index 4c99d5ed..812d87e5 100644
--- a/web/src/js/components/ContentView/ViewSelector.jsx
+++ b/web/src/js/components/ContentView/ViewSelector.jsx
@@ -11,7 +11,7 @@ ViewSelector.propTypes = {
setContentView: PropTypes.func.isRequired
}
-function ViewSelector ({contentViews, activeView, setContentView}){
+export function ViewSelector ({contentViews, activeView, setContentView}){
let inner = <span> <b>View:</b> {activeView.toLowerCase()} <span className="caret"></span> </span>
return (
diff --git a/web/src/js/components/EventLog.jsx b/web/src/js/components/EventLog.jsx
index a83cdb28..40fe900e 100644
--- a/web/src/js/components/EventLog.jsx
+++ b/web/src/js/components/EventLog.jsx
@@ -5,7 +5,7 @@ import { toggleFilter, toggleVisibility } from '../ducks/eventLog'
import ToggleButton from './common/ToggleButton'
import EventList from './EventLog/EventList'
-class EventLog extends Component {
+export class PureEventLog extends Component {
static propTypes = {
filters: PropTypes.object.isRequired,
@@ -77,4 +77,4 @@ export default connect(
close: toggleVisibility,
toggleFilter: toggleFilter,
}
-)(EventLog)
+)(PureEventLog)
diff --git a/web/src/js/components/FlowTable.jsx b/web/src/js/components/FlowTable.jsx
index 24c1f3a1..a6381d0d 100644
--- a/web/src/js/components/FlowTable.jsx
+++ b/web/src/js/components/FlowTable.jsx
@@ -1,17 +1,20 @@
import React from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
+import { connect } from 'react-redux'
import shallowEqual from 'shallowequal'
import AutoScroll from './helpers/AutoScroll'
import { calcVScroll } from './helpers/VirtualScroll'
import FlowTableHead from './FlowTable/FlowTableHead'
import FlowRow from './FlowTable/FlowRow'
import Filt from "../filt/filt"
+import * as flowsActions from '../ducks/flows'
+
class FlowTable extends React.Component {
static propTypes = {
- onSelect: PropTypes.func.isRequired,
+ selectFlow: PropTypes.func.isRequired,
flows: PropTypes.array.isRequired,
rowHeight: PropTypes.number,
highlight: PropTypes.string,
@@ -107,7 +110,7 @@ class FlowTable extends React.Component {
flow={flow}
selected={flow === selected}
highlighted={isHighlighted(flow)}
- onSelect={this.props.onSelect}
+ onSelect={this.props.selectFlow}
/>
))}
<tr style={{ height: vScroll.paddingBottom }}></tr>
@@ -118,4 +121,15 @@ class FlowTable extends React.Component {
}
}
-export default AutoScroll(FlowTable)
+export const PureFlowTable = AutoScroll(FlowTable)
+
+export default connect(
+ state => ({
+ flows: state.flows.view,
+ highlight: state.flows.highlight,
+ selected: state.flows.byId[state.flows.selected[0]],
+ }),
+ {
+ selectFlow: flowsActions.select,
+ }
+)(PureFlowTable)
diff --git a/web/src/js/components/FlowView.jsx b/web/src/js/components/FlowView.jsx
index d03d681a..25e7bb9f 100644
--- a/web/src/js/components/FlowView.jsx
+++ b/web/src/js/components/FlowView.jsx
@@ -3,94 +3,47 @@ import { connect } from 'react-redux'
import _ from 'lodash'
import Nav from './FlowView/Nav'
-import { Request, Response, ErrorView as Error } from './FlowView/Messages'
+import { ErrorView as Error, Request, Response } from './FlowView/Messages'
import Details from './FlowView/Details'
-import Prompt from './Prompt'
import { selectTab } from '../ducks/ui/flow'
-class FlowView extends Component {
+export const allTabs = { Request, Response, Error, Details }
- static allTabs = { Request, Response, Error, Details }
+function FlowView({ flow, tabName, selectTab }) {
- constructor(props, context) {
- super(props, context)
- this.onPromptFinish = this.onPromptFinish.bind(this)
- }
+ // only display available tab names
+ const tabs = ['request', 'response', 'error'].filter(k => flow[k])
+ tabs.push("details")
- onPromptFinish(edit) {
- this.props.setPrompt(false)
- if (edit && this.tabComponent) {
- this.tabComponent.edit(edit)
+ if (tabs.indexOf(tabName) < 0) {
+ if (tabName === 'response' && flow.error) {
+ tabName = 'error'
+ } else if (tabName === 'error' && flow.response) {
+ tabName = 'response'
+ } else {
+ tabName = tabs[0]
}
}
- getPromptOptions() {
- switch (this.props.tab) {
-
- case 'request':
- return [
- 'method',
- 'url',
- { text: 'http version', key: 'v' },
- 'header'
- ]
- break
-
- case 'response':
- return [
- { text: 'http version', key: 'v' },
- 'code',
- 'message',
- 'header'
- ]
- break
-
- case 'details':
- return
-
- default:
- throw 'Unknown tab for edit: ' + this.props.tab
- }
- }
-
- render() {
- let { flow, tab: active, updateFlow } = this.props
- const tabs = ['request', 'response', 'error'].filter(k => flow[k]).concat(['details'])
-
- if (tabs.indexOf(active) < 0) {
- if (active === 'response' && flow.error) {
- active = 'error'
- } else if (active === 'error' && flow.response) {
- active = 'response'
- } else {
- active = tabs[0]
- }
- }
-
- const Tab = FlowView.allTabs[_.capitalize(active)]
-
- return (
- <div className="flow-detail">
- <Nav
- flow={flow}
- tabs={tabs}
- active={active}
- onSelectTab={this.props.selectTab}
- />
- <Tab ref={ tab => this.tabComponent = tab } flow={flow} updateFlow={updateFlow} />
- {this.props.promptOpen && (
- <Prompt options={this.getPromptOptions()} done={this.onPromptFinish} />
- )}
- </div>
- )
- }
+ const Tab = allTabs[_.capitalize(tabName)]
+
+ return (
+ <div className="flow-detail">
+ <Nav
+ tabs={tabs}
+ active={tabName}
+ onSelectTab={selectTab}
+ />
+ <Tab flow={flow}/>
+ </div>
+ )
}
export default connect(
state => ({
- promptOpen: state.ui.promptOpen,
- tab: state.ui.flow.tab
+ flow: state.flows.byId[state.flows.selected[0]],
+ tabName: state.ui.flow.tab,
}),
{
selectTab,
diff --git a/web/src/js/components/FlowView/Messages.jsx b/web/src/js/components/FlowView/Messages.jsx
index 4a31faf4..c1af36c5 100644
--- a/web/src/js/components/FlowView/Messages.jsx
+++ b/web/src/js/components/FlowView/Messages.jsx
@@ -9,6 +9,7 @@ import ContentView from '../ContentView'
import ContentViewOptions from '../ContentView/ContentViewOptions'
import ValidateEditor from '../ValueEditor/ValidateEditor'
import ValueEditor from '../ValueEditor/ValueEditor'
+import HideInStatic from '../common/HideInStatic'
import Headers from './Headers'
import { startEdit, updateEdit } from '../../ducks/ui/flow'
@@ -105,6 +106,7 @@ export class Request extends Component {
onContentChange={content => updateFlow({ request: {content}})}
message={flow.request}/>
</article>
+ <HideInStatic>
{!noContent &&
<footer>
<ContentViewOptions
@@ -114,33 +116,10 @@ export class Request extends Component {
uploadContent={content => uploadContent(flow, content, "request")}/>
</footer>
}
+ </HideInStatic>
</section>
)
}
-
-
- edit(k) {
- throw "unimplemented"
- /*
- switch (k) {
- case 'm':
- this.refs.requestLine.refs.method.focus()
- break
- case 'u':
- this.refs.requestLine.refs.url.focus()
- break
- case 'v':
- this.refs.requestLine.refs.httpVersion.focus()
- break
- case 'h':
- this.refs.headers.edit()
- break
- default:
- throw new Error(`Unimplemented: ${k}`)
- }
- */
- }
-
}
Request = Message(Request)
@@ -172,6 +151,7 @@ export class Response extends Component {
message={flow.response}
/>
</article>
+ <HideInStatic>
{!noContent &&
<footer >
<ContentViewOptions
@@ -181,31 +161,10 @@ export class Response extends Component {
readonly={!isEdit}/>
</footer>
}
+ </HideInStatic>
</section>
)
}
-
- edit(k) {
- throw "unimplemented"
- /*
- switch (k) {
- case 'c':
- this.refs.responseLine.refs.status_code.focus()
- break
- case 'm':
- this.refs.responseLine.refs.msg.focus()
- break
- case 'v':
- this.refs.responseLine.refs.httpVersion.focus()
- break
- case 'h':
- this.refs.headers.edit()
- break
- default:
- throw new Error(`'Unimplemented: ${k}`)
- }
- */
- }
}
Response = Message(Response)
diff --git a/web/src/js/components/Footer.jsx b/web/src/js/components/Footer.jsx
index 08d15496..db9afe6f 100644
--- a/web/src/js/components/Footer.jsx
+++ b/web/src/js/components/Footer.jsx
@@ -2,6 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { formatSize } from '../utils.js'
+import HideInStatic from '../components/common/HideInStatic'
Footer.propTypes = {
settings: PropTypes.object.isRequired,
@@ -49,11 +50,14 @@ function Footer({ settings }) {
<span className="label label-success">stream: {formatSize(stream_large_bodies)}</span>
)}
<div className="pull-right">
- {server && (
+ <HideInStatic>
+ {
+ server && (
<span className="label label-primary" title="HTTP Proxy Server Address">
{listen_host||"*"}:{listen_port}
- </span>
- )}
+ </span>)
+ }
+ </HideInStatic>
<span className="label label-info" title="Mitmproxy Version">
v{version}
</span>
diff --git a/web/src/js/components/Header.jsx b/web/src/js/components/Header.jsx
index ebe7453c..9b7354eb 100644
--- a/web/src/js/components/Header.jsx
+++ b/web/src/js/components/Header.jsx
@@ -8,6 +8,7 @@ import FileMenu from './Header/FileMenu'
import FlowMenu from './Header/FlowMenu'
import {setActiveMenu} from '../ducks/ui/header'
import ConnectionIndicator from "./Header/ConnectionIndicator"
+import HideInStatic from './common/HideInStatic'
class Header extends Component {
static entries = [MainMenu, OptionMenu]
@@ -40,7 +41,9 @@ class Header extends Component {
{Entry.title}
</a>
))}
- <ConnectionIndicator/>
+ <HideInStatic>
+ <ConnectionIndicator/>
+ </HideInStatic>
</nav>
<div>
<Active/>
diff --git a/web/src/js/components/Header/FileMenu.jsx b/web/src/js/components/Header/FileMenu.jsx
index 70fbb2c3..c88efcd8 100644
--- a/web/src/js/components/Header/FileMenu.jsx
+++ b/web/src/js/components/Header/FileMenu.jsx
@@ -4,11 +4,13 @@ import { connect } from 'react-redux'
import FileChooser from '../common/FileChooser'
import Dropdown, {Divider} from '../common/Dropdown'
import * as flowsActions from '../../ducks/flows'
+import * as modalActions from '../../ducks/ui/modal'
+import HideInStatic from "../common/HideInStatic";
FileMenu.propTypes = {
clearFlows: PropTypes.func.isRequired,
loadFlows: PropTypes.func.isRequired,
- saveFlows: PropTypes.func.isRequired
+ saveFlows: PropTypes.func.isRequired,
}
FileMenu.onNewClick = (e, clearFlows) => {
@@ -34,12 +36,13 @@ export function FileMenu ({clearFlows, loadFlows, saveFlows}) {
&nbsp;Save...
</a>
+ <HideInStatic>
<Divider/>
-
<a href="http://mitm.it/" target="_blank">
<i className="fa fa-fw fa-external-link"></i>
&nbsp;Install Certificates...
</a>
+ </HideInStatic>
</Dropdown>
)
}
diff --git a/web/src/js/components/Header/FlowMenu.jsx b/web/src/js/components/Header/FlowMenu.jsx
index 8f104213..70c8bfcf 100644
--- a/web/src/js/components/Header/FlowMenu.jsx
+++ b/web/src/js/components/Header/FlowMenu.jsx
@@ -4,6 +4,7 @@ import { connect } from "react-redux"
import Button from "../common/Button"
import { MessageUtils } from "../../flow/utils.js"
import * as flowsActions from "../../ducks/flows"
+import HideInStatic from "../common/HideInStatic";
FlowMenu.title = 'Flow'
@@ -22,6 +23,7 @@ export function FlowMenu({ flow, resumeFlow, killFlow, replayFlow, duplicateFlow
return <div/>
return (
<div>
+ <HideInStatic>
<div className="menu-group">
<div className="menu-content">
<Button title="[r]eplay flow" icon="fa-repeat text-primary"
@@ -43,6 +45,8 @@ export function FlowMenu({ flow, resumeFlow, killFlow, replayFlow, duplicateFlow
</div>
<div className="menu-legend">Flow Modification</div>
</div>
+ </HideInStatic>
+
<div className="menu-group">
<div className="menu-content">
<Button title="download" icon="fa-download"
@@ -52,6 +56,8 @@ export function FlowMenu({ flow, resumeFlow, killFlow, replayFlow, duplicateFlow
</div>
<div className="menu-legend">Export</div>
</div>
+
+ <HideInStatic>
<div className="menu-group">
<div className="menu-content">
<Button disabled={!flow || !flow.intercepted} title="[a]ccept intercepted flow"
@@ -65,6 +71,7 @@ export function FlowMenu({ flow, resumeFlow, killFlow, replayFlow, duplicateFlow
</div>
<div className="menu-legend">Interception</div>
</div>
+ </HideInStatic>
</div>
diff --git a/web/src/js/components/Header/OptionMenu.jsx b/web/src/js/components/Header/OptionMenu.jsx
index b33d578d..765129ed 100644
--- a/web/src/js/components/Header/OptionMenu.jsx
+++ b/web/src/js/components/Header/OptionMenu.jsx
@@ -1,66 +1,56 @@
-import React from "react"
-import PropTypes from 'prop-types'
+import React from "react"
import { connect } from "react-redux"
-import { SettingsToggle, EventlogToggle } from "./MenuToggle"
+import { EventlogToggle, SettingsToggle } from "./MenuToggle"
+import Button from "../common/Button"
import DocsLink from "../common/DocsLink"
+import HideInStatic from "../common/HideInStatic";
+import * as modalActions from "../../ducks/ui/modal"
OptionMenu.title = 'Options'
-export default function OptionMenu() {
+function OptionMenu({ openOptions }) {
return (
<div>
- <div className="menu-group">
- <div className="menu-content">
- <SettingsToggle setting="http2">HTTP/2.0</SettingsToggle>
- <SettingsToggle setting="websocket">WebSockets</SettingsToggle>
- <SettingsToggle setting="rawtcp">Raw TCP</SettingsToggle>
+ <HideInStatic>
+ <div className="menu-group">
+ <div className="menu-content">
+ <Button title="Open Options" icon="fa-cogs text-primary"
+ onClick={openOptions}>
+ Edit Options <sup>alpha</sup>
+ </Button>
+ </div>
+ <div className="menu-legend">Options Editor</div>
</div>
- <div className="menu-legend">Protocol Support</div>
- </div>
- <div className="menu-group">
- <div className="menu-content">
- <SettingsToggle setting="anticache">
- Disable Caching <DocsLink resource="features/anticache.html"/>
- </SettingsToggle>
- <SettingsToggle setting="anticomp">
- Disable Compression <i className="fa fa-question-circle"
- title="Do not forward Accept-Encoding headers to the server to force an uncompressed response."></i>
- </SettingsToggle>
+
+ <div className="menu-group">
+ <div className="menu-content">
+ <SettingsToggle setting="anticache">
+ Strip cache headers <DocsLink resource="features/anticache.html"/>
+ </SettingsToggle>
+ <SettingsToggle setting="showhost">
+ Use host header for display
+ </SettingsToggle>
+ <SettingsToggle setting="ssl_insecure">
+ Verify server certificates
+ </SettingsToggle>
+ </div>
+ <div className="menu-legend">Quick Options</div>
</div>
- <div className="menu-legend">HTTP Options</div>
- </div>
+ </HideInStatic>
+
<div className="menu-group">
<div className="menu-content">
- <SettingsToggle setting="showhost">
- Use Host Header <i className="fa fa-question-circle"
- title="Use the Host header to construct URLs for display."></i>
- </SettingsToggle>
<EventlogToggle/>
</div>
<div className="menu-legend">View Options</div>
</div>
- { /*
- <ToggleButton text="no_upstream_cert"
- checked={settings.no_upstream_cert}
- onToggle={() => updateSettings({ no_upstream_cert: !settings.no_upstream_cert })}
- />
- <ToggleInputButton name="stickyauth" placeholder="Sticky auth filter"
- checked={!!settings.stickyauth}
- txt={settings.stickyauth}
- onToggleChanged={txt => updateSettings({ stickyauth: !settings.stickyauth ? txt : null })}
- />
- <ToggleInputButton name="stickycookie" placeholder="Sticky cookie filter"
- checked={!!settings.stickycookie}
- txt={settings.stickycookie}
- onToggleChanged={txt => updateSettings({ stickycookie: !settings.stickycookie ? txt : null })}
- />
- <ToggleInputButton name="stream_large_bodies" placeholder="stream..."
- checked={!!settings.stream_large_bodies}
- txt={settings.stream_large_bodies}
- inputType="number"
- onToggleChanged={txt => updateSettings({ stream_large_bodies: !settings.stream_large_bodies ? txt : null })}
- />
- */}
</div>
)
}
+
+export default connect(
+ null,
+ {
+ openOptions: () => modalActions.setActiveModal('OptionModal')
+ }
+)(OptionMenu)
diff --git a/web/src/js/components/MainView.jsx b/web/src/js/components/MainView.jsx
index e2bedc88..03bfce7f 100644
--- a/web/src/js/components/MainView.jsx
+++ b/web/src/js/components/MainView.jsx
@@ -1,54 +1,27 @@
-import React, { Component } from 'react'
+import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import Splitter from './common/Splitter'
import FlowTable from './FlowTable'
import FlowView from './FlowView'
-import * as flowsActions from '../ducks/flows'
-class MainView extends Component {
-
- static propTypes = {
- highlight: PropTypes.string,
- sort: PropTypes.object,
- }
+MainView.propTypes = {
+ hasSelection: PropTypes.bool.isRequired,
+}
- render() {
- const { flows, selectedFlow, highlight } = this.props
- return (
- <div className="main-view">
- <FlowTable
- ref="flowTable"
- flows={flows}
- selected={selectedFlow}
- highlight={highlight}
- onSelect={this.props.selectFlow}
- />
- {selectedFlow && [
- <Splitter key="splitter"/>,
- <FlowView
- key="flowDetails"
- ref="flowDetails"
- tab={this.props.tab}
- updateFlow={data => this.props.updateFlow(selectedFlow, data)}
- flow={selectedFlow}
- />
- ]}
- </div>
- )
- }
+function MainView({ hasSelection }) {
+ return (
+ <div className="main-view">
+ <FlowTable/>
+ {hasSelection && <Splitter key="splitter"/>}
+ {hasSelection && <FlowView key="flowDetails"/>}
+ </div>
+ )
}
export default connect(
state => ({
- flows: state.flows.view,
- filter: state.flows.filter,
- highlight: state.flows.highlight,
- selectedFlow: state.flows.byId[state.flows.selected[0]],
- tab: state.ui.flow.tab,
+ hasSelection: !!state.flows.byId[state.flows.selected[0]]
}),
- {
- selectFlow: flowsActions.select,
- updateFlow: flowsActions.update,
- }
+ {}
)(MainView)
diff --git a/web/src/js/components/Modal/Modal.jsx b/web/src/js/components/Modal/Modal.jsx
new file mode 100644
index 00000000..88e81156
--- /dev/null
+++ b/web/src/js/components/Modal/Modal.jsx
@@ -0,0 +1,24 @@
+import React, { Component } from 'react'
+import { connect } from 'react-redux'
+import ModalList from './ModalList'
+
+class PureModal extends Component {
+
+ constructor(props, context) {
+ super(props, context)
+ }
+
+ render() {
+ const { activeModal } = this.props
+ const ActiveModal = ModalList.find(m => m.name === activeModal )
+ return(
+ activeModal ? <ActiveModal/> : <div/>
+ )
+ }
+}
+
+export default connect(
+ state => ({
+ activeModal: state.ui.modal.activeModal
+ })
+)(PureModal)
diff --git a/web/src/js/components/Modal/ModalLayout.jsx b/web/src/js/components/Modal/ModalLayout.jsx
new file mode 100644
index 00000000..cf357b2b
--- /dev/null
+++ b/web/src/js/components/Modal/ModalLayout.jsx
@@ -0,0 +1,16 @@
+import React from 'react'
+
+export default function ModalLayout ({ children }) {
+ return (
+ <div>
+ <div className="modal-backdrop fade in"></div>
+ <div className="modal modal-visible" id="optionsModal" tabIndex="-1" role="dialog" aria-labelledby="options">
+ <div className="modal-dialog modal-lg" role="document">
+ <div className="modal-content">
+ {children}
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+}
diff --git a/web/src/js/components/Modal/ModalList.jsx b/web/src/js/components/Modal/ModalList.jsx
new file mode 100644
index 00000000..1175d5ea
--- /dev/null
+++ b/web/src/js/components/Modal/ModalList.jsx
@@ -0,0 +1,13 @@
+import React from 'react'
+import ModalLayout from './ModalLayout'
+import OptionContent from './OptionModal'
+
+function OptionModal() {
+ return (
+ <ModalLayout>
+ <OptionContent/>
+ </ModalLayout>
+ )
+}
+
+export default [ OptionModal ]
diff --git a/web/src/js/components/Modal/Option.jsx b/web/src/js/components/Modal/Option.jsx
new file mode 100644
index 00000000..38e2f239
--- /dev/null
+++ b/web/src/js/components/Modal/Option.jsx
@@ -0,0 +1,141 @@
+import React, { Component } from "react"
+import PropTypes from "prop-types"
+import { connect } from "react-redux"
+import { update as updateOptions } from "../../ducks/options"
+import { Key } from "../../utils"
+import classnames from 'classnames'
+
+const stopPropagation = e => {
+ if (e.keyCode !== Key.ESC) {
+ e.stopPropagation()
+ }
+}
+
+BooleanOption.PropTypes = {
+ value: PropTypes.bool.isRequired,
+ onChange: PropTypes.func.isRequired,
+}
+function BooleanOption({ value, onChange, ...props }) {
+ return (
+ <div className="checkbox">
+ <label>
+ <input type="checkbox"
+ checked={value}
+ onChange={e => onChange(e.target.checked)}
+ {...props}
+ />
+ Enable
+ </label>
+ </div>
+ )
+}
+
+StringOption.PropTypes = {
+ value: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+}
+function StringOption({ value, onChange, ...props }) {
+ return (
+ <input type="text"
+ value={value || ""}
+ onChange={e => onChange(e.target.value)}
+ {...props}
+ />
+ )
+}
+function Optional(Component) {
+ return function ({ onChange, ...props }) {
+ return <Component
+ onChange={x => onChange(x ? x : null)}
+ {...props}
+ />
+ }
+}
+
+NumberOption.PropTypes = {
+ value: PropTypes.number.isRequired,
+ onChange: PropTypes.func.isRequired,
+}
+function NumberOption({ value, onChange, ...props }) {
+ return (
+ <input type="number"
+ value={value}
+ onChange={(e) => onChange(parseInt(e.target.value))}
+ {...props}
+ />
+ )
+}
+
+ChoicesOption.PropTypes = {
+ value: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+}
+export function ChoicesOption({ value, onChange, choices, ...props }) {
+ return (
+ <select
+ onChange={(e) => onChange(e.target.value)}
+ value={value}
+ {...props}
+ >
+ { choices.map(
+ choice => (
+ <option key={choice} value={choice}>{choice}</option>
+ )
+ )}
+ </select>
+ )
+}
+
+StringSequenceOption.PropTypes = {
+ value: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+}
+function StringSequenceOption({ value, onChange, ...props }) {
+ const height = Math.max(value.length, 1)
+ return <textarea
+ rows={height}
+ value={value.join('\n')}
+ onChange={e => onChange(e.target.value.split("\n"))}
+ {...props}
+ />
+}
+
+export const Options = {
+ "bool": BooleanOption,
+ "str": StringOption,
+ "int": NumberOption,
+ "optional str": Optional(StringOption),
+ "sequence of str": StringSequenceOption,
+}
+
+function PureOption({ choices, type, value, onChange, name, error }) {
+ let Opt, props = {}
+ if (choices) {
+ Opt = ChoicesOption;
+ props.choices = choices
+ } else {
+ Opt = Options[type]
+ }
+ if (Opt !== BooleanOption) {
+ props.className = "form-control"
+ }
+
+ return <div className={classnames({'has-error':error})}>
+ <Opt
+ name={name}
+ value={value}
+ onChange={onChange}
+ onKeyDown={stopPropagation}
+ {...props}
+ />
+ </div>
+}
+export default connect(
+ (state, { name }) => ({
+ ...state.options[name],
+ ...state.ui.optionsEditor[name]
+ }),
+ (dispatch, { name }) => ({
+ onChange: value => dispatch(updateOptions(name, value))
+ })
+)(PureOption)
diff --git a/web/src/js/components/Modal/OptionModal.jsx b/web/src/js/components/Modal/OptionModal.jsx
new file mode 100644
index 00000000..fed0048d
--- /dev/null
+++ b/web/src/js/components/Modal/OptionModal.jsx
@@ -0,0 +1,110 @@
+import React, { Component } from "react"
+import { connect } from "react-redux"
+import * as modalAction from "../../ducks/ui/modal"
+import * as optionAction from "../../ducks/options"
+import Option from "./Option"
+import _ from "lodash"
+
+function PureOptionHelp({help}){
+ return <div className="help-block small">{help}</div>;
+}
+const OptionHelp = connect((state, {name}) => ({
+ help: state.options[name].help,
+}))(PureOptionHelp);
+
+function PureOptionError({error}){
+ if(!error) return null;
+ return <div className="small text-danger">{error}</div>;
+}
+const OptionError = connect((state, {name}) => ({
+ error: state.ui.optionsEditor[name] && state.ui.optionsEditor[name].error
+}))(PureOptionError);
+
+export function PureOptionDefault({value, defaultVal}){
+ if( value === defaultVal ) {
+ return null
+ } else {
+ if (typeof(defaultVal) === 'boolean') {
+ defaultVal = defaultVal ? 'true' : 'false'
+ } else if (Array.isArray(defaultVal)){
+ if (_.isEmpty(_.compact(value)) && // filter the empty string in array
+ _.isEmpty(defaultVal)){
+ return null
+ }
+ defaultVal = '[ ]'
+ } else if (defaultVal === ''){
+ defaultVal = '\"\"'
+ } else if (defaultVal === null){
+ defaultVal = 'null'
+ }
+ return <div className="small">Default: <strong> {defaultVal} </strong> </div>
+ }
+}
+const OptionDefault = connect((state, {name}) => ({
+ value: state.options[name].value,
+ defaultVal: state.options[name].default
+}))(PureOptionDefault)
+
+class PureOptionModal extends Component {
+
+ constructor(props, context) {
+ super(props, context)
+ this.state = { title: 'Options' }
+ }
+
+ componentWillUnmount(){
+ // this.props.save()
+ }
+
+ render() {
+ const { hideModal, options } = this.props
+ const { title } = this.state
+ return (
+ <div>
+ <div className="modal-header">
+ <button type="button" className="close" data-dismiss="modal" onClick={() => {
+ hideModal()
+ }}>
+ <i className="fa fa-fw fa-times"></i>
+ </button>
+ <div className="modal-title">
+ <h4>{ title }</h4>
+ </div>
+ </div>
+
+ <div className="modal-body">
+ <div className="form-horizontal">
+ {
+ options.map(name =>
+ <div key={name} className="form-group">
+ <div className="col-xs-6">
+ <label htmlFor={name}>{name}</label>
+ <OptionHelp name={name}/>
+ </div>
+ <div className="col-xs-6">
+ <Option name={name}/>
+ <OptionError name={name}/>
+ <OptionDefault name={name}/>
+ </div>
+ </div>
+ )
+ }
+ </div>
+ </div>
+
+ <div className="modal-footer">
+ </div>
+ </div>
+ )
+ }
+}
+
+export default connect(
+ state => ({
+ options: Object.keys(state.options).sort()
+ }),
+ {
+ hideModal: modalAction.hideModal,
+ save: optionAction.save,
+ }
+)(PureOptionModal)
diff --git a/web/src/js/components/Prompt.jsx b/web/src/js/components/Prompt.jsx
deleted file mode 100755
index 77b07027..00000000
--- a/web/src/js/components/Prompt.jsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import React from 'react'
-import PropTypes from 'prop-types'
-import ReactDOM from 'react-dom'
-import _ from 'lodash'
-
-import {Key} from '../utils.js'
-
-Prompt.propTypes = {
- options: PropTypes.array.isRequired,
- done: PropTypes.func.isRequired,
- prompt: PropTypes.string,
-}
-
-export default function Prompt({ prompt, done, options }) {
- const opts = []
-
- for (let i = 0; i < options.length; i++) {
- let opt = options[i]
- if (_.isString(opt)) {
- let str = opt
- while (str.length > 0 && keyTaken(str[0])) {
- str = str.substr(1)
- }
- opt = { text: opt, key: str[0] }
- }
- if (!opt.text || !opt.key || keyTaken(opt.key)) {
- throw 'invalid options'
- }
- opts.push(opt)
- }
-
- function keyTaken(k) {
- return _.map(opts, 'key').includes(k)
- }
-
- function onKeyDown(event) {
- event.stopPropagation()
- event.preventDefault()
- const key = opts.find(opt => Key[opt.key.toUpperCase()] === event.keyCode)
- if (!key && event.keyCode !== Key.ESC && event.keyCode !== Key.ENTER) {
- return
- }
- done(key.key || false)
- }
-
- return (
- <div tabIndex="0" onKeyDown={onKeyDown} className="prompt-dialog">
- <div className="prompt-content">
- {prompt || <strong>Select: </strong> }
- {opts.map(opt => {
- const idx = opt.text.indexOf(opt.key)
- function onClick(event) {
- done(opt.key)
- event.stopPropagation()
- }
- return (
- <span key={opt.key} className="option" onClick={onClick}>
- {idx !== -1 ? opt.text.substring(0, idx) : opt.text + '('}
- <strong className="text-primary">{opt.key}</strong>
- {idx !== -1 ? opt.text.substring(idx + 1) : ')'}
- </span>
- )
- })}
- </div>
- </div>
- )
-}
diff --git a/web/src/js/components/ProxyApp.jsx b/web/src/js/components/ProxyApp.jsx
index af5b3caa..15384e02 100644
--- a/web/src/js/components/ProxyApp.jsx
+++ b/web/src/js/components/ProxyApp.jsx
@@ -7,6 +7,7 @@ import MainView from './MainView'
import Header from './Header'
import EventLog from './EventLog'
import Footer from './Footer'
+import Modal from './Modal/Modal'
class ProxyAppMain extends Component {
@@ -19,7 +20,7 @@ class ProxyAppMain extends Component {
}
render() {
- const { showEventLog, location, filter, highlight } = this.props
+ const { showEventLog } = this.props
return (
<div id="container" tabIndex="0">
<Header/>
@@ -28,6 +29,7 @@ class ProxyAppMain extends Component {
<EventLog key="eventlog"/>
)}
<Footer />
+ <Modal/>
</div>
)
}
diff --git a/web/src/js/components/common/Button.jsx b/web/src/js/components/common/Button.jsx
index e02ae010..02dab305 100644
--- a/web/src/js/components/common/Button.jsx
+++ b/web/src/js/components/common/Button.jsx
@@ -12,7 +12,7 @@ Button.propTypes = {
export default function Button({ onClick, children, icon, disabled, className, title }) {
return (
<div className={classnames(className, 'btn btn-default')}
- onClick={!disabled && onClick}
+ onClick={disabled ? undefined : onClick}
disabled={disabled}
title={title}>
{icon && (<i className={"fa fa-fw " + icon}/> )}
diff --git a/web/src/js/components/common/HideInStatic.jsx b/web/src/js/components/common/HideInStatic.jsx
new file mode 100644
index 00000000..c5f3bf47
--- /dev/null
+++ b/web/src/js/components/common/HideInStatic.jsx
@@ -0,0 +1,5 @@
+import React from 'react'
+
+export default function HideInStatic({ children }) {
+ return global.MITMWEB_STATIC ? null : [children]
+}
diff --git a/web/src/js/ducks/connection.js b/web/src/js/ducks/connection.js
index ffa2c309..151277fb 100644
--- a/web/src/js/ducks/connection.js
+++ b/web/src/js/ducks/connection.js
@@ -1,6 +1,6 @@
export const ConnectionState = {
INIT: Symbol("init"),
- FETCHING: Symbol("fetching"), // WebSocket is established, but still startFetching resources.
+ FETCHING: Symbol("fetching"), // WebSocket is established, but still fetching resources.
ESTABLISHED: Symbol("established"),
ERROR: Symbol("error"),
OFFLINE: Symbol("offline"), // indicates that there is no live (websocket) backend.
diff --git a/web/src/js/ducks/index.js b/web/src/js/ducks/index.js
index 0f2426ec..be2f2885 100644
--- a/web/src/js/ducks/index.js
+++ b/web/src/js/ducks/index.js
@@ -4,6 +4,7 @@ import flows from "./flows"
import settings from "./settings"
import ui from "./ui/index"
import connection from "./connection"
+import options from './options'
export default combineReducers({
eventLog,
@@ -11,4 +12,5 @@ export default combineReducers({
settings,
connection,
ui,
+ options,
})
diff --git a/web/src/js/ducks/options.js b/web/src/js/ducks/options.js
new file mode 100644
index 00000000..0da0fb8c
--- /dev/null
+++ b/web/src/js/ducks/options.js
@@ -0,0 +1,50 @@
+import { fetchApi } from "../utils"
+import * as optionsEditorActions from "./ui/optionsEditor"
+import _ from "lodash"
+
+export const RECEIVE = 'OPTIONS_RECEIVE'
+export const UPDATE = 'OPTIONS_UPDATE'
+export const REQUEST_UPDATE = 'REQUEST_UPDATE'
+
+const defaultState = {}
+
+export default function reducer(state = defaultState, action) {
+ switch (action.type) {
+
+ case RECEIVE:
+ return action.data
+
+ case UPDATE:
+ return {
+ ...state,
+ ...action.data,
+ }
+
+ default:
+ return state
+ }
+}
+
+export function pureSendUpdate (option, value, dispatch) {
+ fetchApi.put('/options', { [option]: value }).then(response => {
+ if (response.status === 200) {
+ dispatch(optionsEditorActions.updateSuccess(option))
+ } else {
+ response.text().then(error => {
+ dispatch(optionsEditorActions.updateError(option, error))
+ })
+ }
+ })
+}
+let sendUpdate = _.throttle(pureSendUpdate, 700, { leading: true, trailing: true })
+
+export function update(option, value) {
+ return dispatch => {
+ dispatch(optionsEditorActions.startUpdate(option, value))
+ sendUpdate(option, value, dispatch);
+ }
+}
+
+export function save() {
+ return dispatch => fetchApi('/options/save', { method: 'POST' })
+}
diff --git a/web/src/js/ducks/settings.js b/web/src/js/ducks/settings.js
index a2e360de..38c36842 100644
--- a/web/src/js/ducks/settings.js
+++ b/web/src/js/ducks/settings.js
@@ -3,7 +3,6 @@ import { fetchApi } from '../utils'
export const RECEIVE = 'SETTINGS_RECEIVE'
export const UPDATE = 'SETTINGS_UPDATE'
export const REQUEST_UPDATE = 'REQUEST_UPDATE'
-export const UNKNOWN_CMD = 'SETTINGS_UNKNOWN_CMD'
const defaultState = {
diff --git a/web/src/js/ducks/ui/flow.js b/web/src/js/ducks/ui/flow.js
index 51ad4184..ea31db19 100644
--- a/web/src/js/ducks/ui/flow.js
+++ b/web/src/js/ducks/ui/flow.js
@@ -148,7 +148,6 @@ export function setContent(content){
return { type: SET_CONTENT, content }
}
-export function stopEdit(data, modifiedFlow) {
- let diff = getDiff(data, modifiedFlow)
- return {type: flowsActions.UPDATE, data, diff }
+export function stopEdit(flow, modifiedFlow) {
+ return flowsActions.update(flow, getDiff(flow, modifiedFlow))
}
diff --git a/web/src/js/ducks/ui/index.js b/web/src/js/ducks/ui/index.js
index 1d989eb1..f5e6851f 100644
--- a/web/src/js/ducks/ui/index.js
+++ b/web/src/js/ducks/ui/index.js
@@ -1,9 +1,13 @@
import { combineReducers } from 'redux'
import flow from './flow'
import header from './header'
+import modal from './modal'
+import optionsEditor from './optionsEditor'
// TODO: Just move ducks/ui/* into ducks/?
export default combineReducers({
flow,
header,
+ modal,
+ optionsEditor,
})
diff --git a/web/src/js/ducks/ui/keyboard.js b/web/src/js/ducks/ui/keyboard.js
index 0e3491fa..ed4dbba5 100644
--- a/web/src/js/ducks/ui/keyboard.js
+++ b/web/src/js/ducks/ui/keyboard.js
@@ -1,10 +1,11 @@
import { Key } from "../../utils"
import { selectTab } from "./flow"
import * as flowsActions from "../flows"
+import * as modalActions from "./modal"
export function onKeyDown(e) {
- console.debug("onKeyDown", e)
+ //console.debug("onKeyDown", e)
if (e.ctrlKey) {
return () => {
}
@@ -46,7 +47,11 @@ export function onKeyDown(e) {
break
case Key.ESC:
- dispatch(flowsActions.select(null))
+ if(getState().ui.modal.activeModal){
+ dispatch(modalActions.hideModal())
+ } else {
+ dispatch(flowsActions.select(null))
+ }
break
case Key.LEFT: {
diff --git a/web/src/js/ducks/ui/modal.js b/web/src/js/ducks/ui/modal.js
new file mode 100644
index 00000000..aafddaf7
--- /dev/null
+++ b/web/src/js/ducks/ui/modal.js
@@ -0,0 +1,33 @@
+export const HIDE_MODAL = 'UI_HIDE_MODAL'
+export const SET_ACTIVE_MODAL = 'UI_SET_ACTIVE_MODAL'
+
+const defaultState = {
+ activeModal: undefined,
+}
+
+export default function reducer(state = defaultState, action){
+ switch (action.type){
+
+ case SET_ACTIVE_MODAL:
+ return {
+ ...state,
+ activeModal: action.activeModal,
+ }
+
+ case HIDE_MODAL:
+ return {
+ ...state,
+ activeModal: undefined
+ }
+ default:
+ return state
+ }
+}
+
+export function setActiveModal(activeModal) {
+ return { type: SET_ACTIVE_MODAL, activeModal }
+}
+
+export function hideModal(){
+ return { type: HIDE_MODAL }
+}
diff --git a/web/src/js/ducks/ui/optionsEditor.js b/web/src/js/ducks/ui/optionsEditor.js
new file mode 100644
index 00000000..a8a8f69e
--- /dev/null
+++ b/web/src/js/ducks/ui/optionsEditor.js
@@ -0,0 +1,73 @@
+import { HIDE_MODAL } from "./modal"
+
+export const OPTION_UPDATE_START = 'UI_OPTION_UPDATE_START'
+export const OPTION_UPDATE_SUCCESS = 'UI_OPTION_UPDATE_SUCCESS'
+export const OPTION_UPDATE_ERROR = 'UI_OPTION_UPDATE_ERROR'
+
+const defaultState = {
+ /* optionName -> {isUpdating, value (client-side), error} */
+}
+
+export default function reducer(state = defaultState, action) {
+ switch (action.type) {
+ case OPTION_UPDATE_START:
+ return {
+ ...state,
+ [action.option]: {
+ isUpdating: true,
+ value: action.value,
+ error: false,
+ }
+ }
+
+ case OPTION_UPDATE_SUCCESS:
+ return {
+ ...state,
+ [action.option]: undefined
+ }
+
+ case OPTION_UPDATE_ERROR:
+ let val = state[action.option].value;
+ if (typeof(val) === "boolean") {
+ // If a boolean option errs, reset it to its previous state to be less confusing.
+ // Example: Start mitmweb, check "add_upstream_certs_to_client_chain".
+ val = !val;
+ }
+ return {
+ ...state,
+ [action.option]: {
+ value: val,
+ isUpdating: false,
+ error: action.error
+ }
+ }
+
+ case HIDE_MODAL:
+ return {}
+
+ default:
+ return state
+ }
+}
+
+export function startUpdate(option, value) {
+ return {
+ type: OPTION_UPDATE_START,
+ option,
+ value,
+ }
+}
+export function updateSuccess(option) {
+ return {
+ type: OPTION_UPDATE_SUCCESS,
+ option,
+ }
+}
+
+export function updateError(option, error) {
+ return {
+ type: OPTION_UPDATE_ERROR,
+ option,
+ error,
+ }
+}
diff --git a/web/src/js/filt/filt.js b/web/src/js/filt/filt.js
index 26058649..19a41af2 100644
--- a/web/src/js/filt/filt.js
+++ b/web/src/js/filt/filt.js
@@ -1929,7 +1929,7 @@ module.exports = (function() {
function body(regex){
regex = new RegExp(regex, "i");
function bodyFilter(flow){
- return True;
+ return true;
}
bodyFilter.desc = "body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10";
return bodyFilter;
@@ -1937,7 +1937,7 @@ module.exports = (function() {
function requestBody(regex){
regex = new RegExp(regex, "i");
function requestBodyFilter(flow){
- return True;
+ return true;
}
requestBodyFilter.desc = "body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10";
return requestBodyFilter;
@@ -1945,7 +1945,7 @@ module.exports = (function() {
function responseBody(regex){
regex = new RegExp(regex, "i");
function responseBodyFilter(flow){
- return True;
+ return true;
}
responseBodyFilter.desc = "body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10";
return responseBodyFilter;
@@ -2104,4 +2104,4 @@ module.exports = (function() {
SyntaxError: peg$SyntaxError,
parse: peg$parse
};
-})(); \ No newline at end of file
+})();
diff --git a/web/src/js/filt/filt.peg b/web/src/js/filt/filt.peg
index 12959474..e4b151ad 100644
--- a/web/src/js/filt/filt.peg
+++ b/web/src/js/filt/filt.peg
@@ -1,4 +1,4 @@
-// PEG.js filter rules - see http://pegjs.majda.cz/online
+// PEG.js filter rules - see https://pegjs.org/
{
var flowutils = require("../flow/utils.js");
@@ -72,7 +72,7 @@ function responseCode(code){
function body(regex){
regex = new RegExp(regex, "i");
function bodyFilter(flow){
- return True;
+ return true;
}
bodyFilter.desc = "body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10";
return bodyFilter;
@@ -80,7 +80,7 @@ function body(regex){
function requestBody(regex){
regex = new RegExp(regex, "i");
function requestBodyFilter(flow){
- return True;
+ return true;
}
requestBodyFilter.desc = "body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10";
return requestBodyFilter;
@@ -88,7 +88,7 @@ function requestBody(regex){
function responseBody(regex){
regex = new RegExp(regex, "i");
function responseBodyFilter(flow){
- return True;
+ return true;
}
responseBodyFilter.desc = "body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10";
return responseBodyFilter;
diff --git a/web/src/js/flow/utils.js b/web/src/js/flow/utils.js
index 3c38058e..d38f4a86 100644
--- a/web/src/js/flow/utils.js
+++ b/web/src/js/flow/utils.js
@@ -49,7 +49,7 @@ export var MessageUtils = {
} else if (message === flow.response) {
message = "response";
}
- return `/flows/${flow.id}/${message}/content` + (view ? `/${view}` : '');
+ return `./flows/${flow.id}/${message}/` + (view ? `content/${view}.json` : 'content.data');
}
};
diff --git a/web/src/js/urlState.js b/web/src/js/urlState.js
index 7802bdb8..d9c50c03 100644
--- a/web/src/js/urlState.js
+++ b/web/src/js/urlState.js
@@ -72,8 +72,12 @@ export function updateUrlFromStore(store) {
if (queryStr) {
url += "?" + queryStr
}
+ let pathname = window.location.pathname
+ if(pathname === "blank") {
+ pathname = "/" // this happens in tests...
+ }
if (window.location.hash.substr(1) !== url) {
- history.replaceState(undefined, "", `/#${url}`)
+ history.replaceState(undefined, "", `${pathname}#${url}`)
}
}
diff --git a/web/src/js/utils.js b/web/src/js/utils.js
index e8470cec..80f0c42a 100644
--- a/web/src/js/utils.js
+++ b/web/src/js/utils.js
@@ -1,6 +1,5 @@
import _ from 'lodash'
import React from 'react'
-import shallowEqual from 'shallowequal'
window._ = _;
window.React = React;
@@ -88,6 +87,11 @@ export function fetchApi(url, options={}) {
} else {
url += "&" + xsrf;
}
+ } else {
+ url += '.json'
+ }
+ if (url.startsWith("/")) {
+ url = "." + url;
}
return fetch(url, {
@@ -121,13 +125,9 @@ export function getDiff(obj1, obj2) {
return result
}
-export const pure = renderFn => class extends React.Component {
+export const pure = renderFn => class extends React.PureComponent {
static displayName = renderFn.name
- shouldComponentUpdate(nextProps) {
- return !shallowEqual(this.props, nextProps)
- }
-
render() {
return renderFn(this.props)
}