aboutsummaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/package.json14
-rw-r--r--web/src/css/header.less30
-rw-r--r--web/src/js/__tests__/components/FlowTable/FlowColumnsSpec.js108
-rw-r--r--web/src/js/__tests__/components/FlowTable/__snapshots__/FlowColumnsSpec.js.snap160
-rw-r--r--web/src/js/__tests__/components/ValueEditor/ValidateEditorSpec.js47
-rw-r--r--web/src/js/__tests__/components/ValueEditor/ValueEditorSpec.js155
-rw-r--r--web/src/js/__tests__/components/ValueEditor/__snapshots__/ValidateEditorSpec.js.snap21
-rw-r--r--web/src/js/__tests__/components/ValueEditor/__snapshots__/ValueEditorSpec.js.snap21
-rw-r--r--web/src/js/__tests__/components/common/ButtonSpec.js26
-rw-r--r--web/src/js/__tests__/components/common/DocsLinkSpec.js17
-rw-r--r--web/src/js/__tests__/components/common/DropdownSpec.js38
-rw-r--r--web/src/js/__tests__/components/common/FileChooserSpec.js38
-rw-r--r--web/src/js/__tests__/components/common/SplitterSpec.js84
-rw-r--r--web/src/js/__tests__/components/common/ToggleButtonSpec.js26
-rw-r--r--web/src/js/__tests__/components/common/ToggleInputButtonSpec.js43
-rw-r--r--web/src/js/__tests__/components/common/__snapshots__/ButtonSpec.js.snap30
-rw-r--r--web/src/js/__tests__/components/common/__snapshots__/DocsLinkSpec.js.snap21
-rw-r--r--web/src/js/__tests__/components/common/__snapshots__/DropdownSpec.js.snap162
-rw-r--r--web/src/js/__tests__/components/common/__snapshots__/FileChooserSpec.js.snap19
-rw-r--r--web/src/js/__tests__/components/common/__snapshots__/SplitterSpec.js.snap12
-rw-r--r--web/src/js/__tests__/components/common/__snapshots__/ToggleButtonSpec.js.snap14
-rw-r--r--web/src/js/__tests__/components/common/__snapshots__/ToggleInputButtonSpec.js.snap31
-rw-r--r--web/src/js/__tests__/components/helpers/AutoScrollSpec.js41
-rw-r--r--web/src/js/__tests__/components/helpers/VirtualScrollSpec.js21
-rw-r--r--web/src/js/__tests__/ducks/_tflow.js97
-rw-r--r--web/src/js/__tests__/ducks/connectionSpec.js41
-rw-r--r--web/src/js/__tests__/ducks/eventLogSpec.js38
-rw-r--r--web/src/js/__tests__/ducks/flowsSpec.js241
-rw-r--r--web/src/js/__tests__/ducks/indexSpec.js12
-rw-r--r--web/src/js/__tests__/ducks/settingsSpec.js25
-rw-r--r--web/src/js/__tests__/ducks/tutils.js5
-rw-r--r--web/src/js/__tests__/ducks/ui/flowSpec.js4
-rw-r--r--web/src/js/__tests__/ducks/ui/headerSpec.js3
-rw-r--r--web/src/js/__tests__/ducks/ui/indexSpec.js9
-rw-r--r--web/src/js/__tests__/ducks/ui/keyboardSpec.js157
-rw-r--r--web/src/js/__tests__/ducks/utils/storeSpec.js2
-rw-r--r--web/src/js/__tests__/flow/utilsSpec.js69
-rw-r--r--web/src/js/__tests__/urlStateSpec.js100
-rw-r--r--web/src/js/__tests__/utilsSpec.js95
-rw-r--r--web/src/js/backends/websocket.js19
-rw-r--r--web/src/js/components/ContentView.jsx7
-rw-r--r--web/src/js/components/ContentView/CodeEditor.jsx3
-rw-r--r--web/src/js/components/ContentView/ContentLoader.jsx3
-rw-r--r--web/src/js/components/ContentView/ContentViewOptions.jsx7
-rw-r--r--web/src/js/components/ContentView/ContentViews.jsx5
-rw-r--r--web/src/js/components/ContentView/DownloadContentButton.jsx2
-rw-r--r--web/src/js/components/ContentView/ShowFullContentButton.jsx3
-rw-r--r--web/src/js/components/ContentView/UploadContentButton.jsx4
-rw-r--r--web/src/js/components/ContentView/ViewSelector.jsx3
-rw-r--r--web/src/js/components/EventLog.jsx5
-rw-r--r--web/src/js/components/EventLog/EventList.jsx10
-rw-r--r--web/src/js/components/FlowTable.jsx3
-rw-r--r--web/src/js/components/FlowTable/FlowRow.jsx3
-rw-r--r--web/src/js/components/FlowTable/FlowTableHead.jsx7
-rw-r--r--web/src/js/components/FlowView/Headers.jsx3
-rw-r--r--web/src/js/components/FlowView/Messages.jsx3
-rw-r--r--web/src/js/components/FlowView/Nav.jsx3
-rw-r--r--web/src/js/components/FlowView/ToggleEdit.jsx3
-rw-r--r--web/src/js/components/Footer.jsx13
-rw-r--r--web/src/js/components/Header.jsx9
-rw-r--r--web/src/js/components/Header/ConnectionIndicator.jsx30
-rw-r--r--web/src/js/components/Header/FileMenu.jsx3
-rw-r--r--web/src/js/components/Header/FilterInput.jsx3
-rw-r--r--web/src/js/components/Header/FlowMenu.jsx3
-rw-r--r--web/src/js/components/Header/MainMenu.jsx3
-rw-r--r--web/src/js/components/Header/MenuToggle.jsx2
-rw-r--r--web/src/js/components/Header/OptionMenu.jsx3
-rw-r--r--web/src/js/components/MainView.jsx3
-rwxr-xr-xweb/src/js/components/Prompt.jsx3
-rw-r--r--web/src/js/components/ProxyApp.jsx3
-rwxr-xr-xweb/src/js/components/ValueEditor/ValidateEditor.jsx3
-rw-r--r--web/src/js/components/ValueEditor/ValueEditor.jsx3
-rw-r--r--web/src/js/components/common/Button.jsx3
-rw-r--r--web/src/js/components/common/DocsLink.jsx3
-rw-r--r--web/src/js/components/common/Dropdown.jsx3
-rw-r--r--web/src/js/components/common/FileChooser.jsx5
-rw-r--r--web/src/js/components/common/ToggleButton.jsx3
-rw-r--r--web/src/js/components/common/ToggleInputButton.jsx3
-rw-r--r--web/src/js/ducks/connection.js44
-rw-r--r--web/src/js/ducks/eventLog.js2
-rw-r--r--web/src/js/ducks/flows.js37
-rw-r--r--web/src/js/ducks/index.js12
-rw-r--r--web/src/js/ducks/ui/flow.js6
-rw-r--r--web/src/js/ducks/ui/header.js2
-rw-r--r--web/src/js/ducks/ui/keyboard.js19
-rw-r--r--web/src/js/filt/filt.js2
-rw-r--r--web/src/js/urlState.js4
-rw-r--r--web/yarn.lock42
88 files changed, 2291 insertions, 156 deletions
diff --git a/web/package.json b/web/package.json
index 601d7077..94b0ee60 100644
--- a/web/package.json
+++ b/web/package.json
@@ -2,29 +2,39 @@
"name": "mitmproxy",
"private": true,
"scripts": {
- "test": "jest",
+ "test": "jest --coverage",
"build": "gulp prod",
"start": "gulp"
},
"jest": {
"testRegex": "__tests__/.*Spec.js$",
- "testPathDirs": [
+ "roots": [
"<rootDir>/src/js"
],
"unmockedModulePathPatterns": [
"react"
+ ],
+ "coverageDirectory": "./coverage",
+ "coveragePathIgnorePatterns": [
+ "<rootDir>/src/js/filt/filt.js"
+ ],
+ "collectCoverageFrom": [
+ "src/js/**/*.{js,jsx}"
]
},
"dependencies": {
"bootstrap": "^3.3.7",
"classnames": "^2.2.5",
"lodash": "^4.17.4",
+ "prop-types": "^15.5.0",
"react": "^15.4.2",
"react-codemirror": "^0.3.0",
"react-dom": "^15.4.2",
"react-redux": "^5.0.2",
+ "react-test-renderer": "^15.5.4",
"redux": "^3.6.0",
"redux-logger": "^2.8.1",
+ "redux-mock-store": "^1.2.3",
"redux-thunk": "^2.2.0",
"shallowequal": "^0.2.2"
},
diff --git a/web/src/css/header.less b/web/src/css/header.less
index aa9abc76..55fc59d0 100644
--- a/web/src/css/header.less
+++ b/web/src/css/header.less
@@ -1,5 +1,7 @@
@import (reference) '../../node_modules/bootstrap/less/variables.less';
@import (reference) '../../node_modules/bootstrap/less/mixins/grid.less';
+@import (reference) "../../node_modules/bootstrap/less/mixins/labels.less";
+@import (reference) "../../node_modules/bootstrap/less/labels.less";
@menu-height: 85px;
@@ -7,7 +9,7 @@ header {
padding-top: 6px;
background-color: white;
@separator-color: lighten(grey, 15%);
- menu {
+ > div {
display: block;
margin: 0;
padding: 0;
@@ -45,7 +47,6 @@ header {
}
}
-
.menu-entry {
text-align: left;
height: (@menu-height - @menu-legend-height)/3;
@@ -63,7 +64,6 @@ header {
}
}
-
.menu-legend {
height: @menu-legend-height;
text-align: center;
@@ -130,3 +130,27 @@ header {
}
}
}
+
+.connection-indicator {
+ .label();
+ float: right;
+ margin: 5px;
+ opacity: 1;
+ transition: all 1s linear;
+
+ &.init, &.fetching {
+ background-color: @label-info-bg;
+ }
+ &.established {
+ background-color: @label-success-bg;
+ opacity: 0;
+ }
+ &.error {
+ background-color: @label-danger-bg;
+ transition: all 0.2s linear;
+ }
+ &.offline {
+ background-color: @label-warning-bg;
+ opacity: 1;
+ }
+}
diff --git a/web/src/js/__tests__/components/FlowTable/FlowColumnsSpec.js b/web/src/js/__tests__/components/FlowTable/FlowColumnsSpec.js
new file mode 100644
index 00000000..f3373c02
--- /dev/null
+++ b/web/src/js/__tests__/components/FlowTable/FlowColumnsSpec.js
@@ -0,0 +1,108 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import * as Columns from '../../../components/FlowTable/FlowColumns'
+import { TFlow } from '../../ducks/tutils'
+
+describe('FlowColumns Components', () => {
+
+ let tflow = TFlow()
+ it('should render TLSColumn', () => {
+ let tlsColumn = renderer.create(<Columns.TLSColumn flow={tflow}/>),
+ tree = tlsColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should render IconColumn', () => {
+ let iconColumn = renderer.create(<Columns.IconColumn flow={tflow}/>),
+ tree = iconColumn.toJSON()
+ // plain
+ expect(tree).toMatchSnapshot()
+ // not modified
+ tflow.response.status_code = 304
+ iconColumn = renderer.create(<Columns.IconColumn flow={tflow}/>)
+ tree = iconColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ // redirect
+ tflow.response.status_code = 302
+ iconColumn = renderer.create(<Columns.IconColumn flow={tflow}/>)
+ tree = iconColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ // image
+ let imageFlow = TFlow()
+ imageFlow.response.headers = [['Content-Type', 'image/jpeg']]
+ iconColumn = renderer.create(<Columns.IconColumn flow={imageFlow}/>)
+ tree = iconColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ // javascript
+ let jsFlow = TFlow()
+ jsFlow.response.headers = [['Content-Type', 'application/x-javascript']]
+ iconColumn = renderer.create(<Columns.IconColumn flow={jsFlow}/>)
+ tree = iconColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ // css
+ let cssFlow = TFlow()
+ cssFlow.response.headers = [['Content-Type', 'text/css']]
+ iconColumn = renderer.create(<Columns.IconColumn flow={cssFlow}/>)
+ tree = iconColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ // html
+ let htmlFlow = TFlow()
+ htmlFlow.response.headers = [['Content-Type', 'text/html']]
+ iconColumn = renderer.create(<Columns.IconColumn flow={htmlFlow}/>)
+ tree = iconColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ // default
+ let fooFlow = TFlow()
+ fooFlow.response.headers = [['Content-Type', 'foo']]
+ iconColumn = renderer.create(<Columns.IconColumn flow={fooFlow}/>)
+ tree = iconColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ // no response
+ tflow.response = null
+ iconColumn = renderer.create(<Columns.IconColumn flow={tflow}/>)
+ tree = iconColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should render pathColumn', () => {
+ let pathColumn = renderer.create(<Columns.PathColumn flow={tflow}/>),
+ tree = pathColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+
+ tflow.error.msg = 'Connection killed'
+ tflow.intercepted = true
+ pathColumn = renderer.create(<Columns.PathColumn flow={tflow}/>)
+ tree = pathColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should render MethodColumn', () => {
+ let methodColumn =renderer.create(<Columns.MethodColumn flow={tflow}/>),
+ tree = methodColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should render StatusColumn', () => {
+ let statusColumn = renderer.create(<Columns.StatusColumn flow={tflow}/>),
+ tree = statusColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should render SizeColumn', () => {
+ tflow = TFlow()
+ let sizeColumn = renderer.create(<Columns.SizeColumn flow={tflow}/>),
+ tree = sizeColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should render TimeColumn', () => {
+ let timeColumn = renderer.create(<Columns.TimeColumn flow={tflow}/>),
+ tree = timeColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+
+ tflow.response = null
+ timeColumn = renderer.create(<Columns.TimeColumn flow={tflow}/>),
+ tree = timeColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+})
diff --git a/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowColumnsSpec.js.snap b/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowColumnsSpec.js.snap
new file mode 100644
index 00000000..d6946507
--- /dev/null
+++ b/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowColumnsSpec.js.snap
@@ -0,0 +1,160 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FlowColumns Components should render IconColumn 1`] = `
+<td
+ className="col-icon"
+>
+ <div
+ className="resource-icon resource-icon-plain"
+ />
+</td>
+`;
+
+exports[`FlowColumns Components should render IconColumn 2`] = `
+<td
+ className="col-icon"
+>
+ <div
+ className="resource-icon resource-icon-not-modified"
+ />
+</td>
+`;
+
+exports[`FlowColumns Components should render IconColumn 3`] = `
+<td
+ className="col-icon"
+>
+ <div
+ className="resource-icon resource-icon-redirect"
+ />
+</td>
+`;
+
+exports[`FlowColumns Components should render IconColumn 4`] = `
+<td
+ className="col-icon"
+>
+ <div
+ className="resource-icon resource-icon-image"
+ />
+</td>
+`;
+
+exports[`FlowColumns Components should render IconColumn 5`] = `
+<td
+ className="col-icon"
+>
+ <div
+ className="resource-icon resource-icon-js"
+ />
+</td>
+`;
+
+exports[`FlowColumns Components should render IconColumn 6`] = `
+<td
+ className="col-icon"
+>
+ <div
+ className="resource-icon resource-icon-css"
+ />
+</td>
+`;
+
+exports[`FlowColumns Components should render IconColumn 7`] = `
+<td
+ className="col-icon"
+>
+ <div
+ className="resource-icon resource-icon-document"
+ />
+</td>
+`;
+
+exports[`FlowColumns Components should render IconColumn 8`] = `
+<td
+ className="col-icon"
+>
+ <div
+ className="resource-icon resource-icon-plain"
+ />
+</td>
+`;
+
+exports[`FlowColumns Components should render IconColumn 9`] = `
+<td
+ className="col-icon"
+>
+ <div
+ className="resource-icon resource-icon-plain"
+ />
+</td>
+`;
+
+exports[`FlowColumns Components should render MethodColumn 1`] = `
+<td
+ className="col-method"
+>
+ GET
+</td>
+`;
+
+exports[`FlowColumns Components should render SizeColumn 1`] = `
+<td
+ className="col-size"
+>
+ 14b
+</td>
+`;
+
+exports[`FlowColumns Components should render StatusColumn 1`] = `
+<td
+ className="col-status"
+/>
+`;
+
+exports[`FlowColumns Components should render TLSColumn 1`] = `
+<td
+ className="col-tls col-tls-http"
+/>
+`;
+
+exports[`FlowColumns Components should render TimeColumn 1`] = `
+<td
+ className="col-time"
+>
+ 415381h
+</td>
+`;
+
+exports[`FlowColumns Components should render TimeColumn 2`] = `
+<td
+ className="col-time"
+>
+ ...
+</td>
+`;
+
+exports[`FlowColumns Components should render pathColumn 1`] = `
+<td
+ className="col-path"
+>
+ <i
+ className="fa fa-fw fa-exclamation pull-right"
+ />
+ http://address:22/path
+</td>
+`;
+
+exports[`FlowColumns Components should render pathColumn 2`] = `
+<td
+ className="col-path"
+>
+ <i
+ className="fa fa-fw fa-pause pull-right"
+ />
+ <i
+ className="fa fa-fw fa-times pull-right"
+ />
+ http://address:22/path
+</td>
+`;
diff --git a/web/src/js/__tests__/components/ValueEditor/ValidateEditorSpec.js b/web/src/js/__tests__/components/ValueEditor/ValidateEditorSpec.js
new file mode 100644
index 00000000..32dabe59
--- /dev/null
+++ b/web/src/js/__tests__/components/ValueEditor/ValidateEditorSpec.js
@@ -0,0 +1,47 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import TestUtils from 'react-dom/test-utils'
+import ValidateEditor from '../../../components/ValueEditor/ValidateEditor'
+
+describe('ValidateEditor Component', () => {
+ let validateFn = jest.fn( content => content.length == 3),
+ doneFn = jest.fn()
+
+ it('should render correctly', () => {
+ let validateEditor = renderer.create(
+ <ValidateEditor content="foo" onDone={doneFn} isValid={validateFn}/>
+ ),
+ tree = validateEditor.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ let validateEditor = TestUtils.renderIntoDocument(
+ <ValidateEditor content="foo" onDone={doneFn} isValid={validateFn}/>
+ )
+ it('should handle componentWillReceiveProps', () => {
+ let mockProps = {
+ isValid: s => s.length == 3,
+ content: "bar"
+ }
+ validateEditor.componentWillReceiveProps(mockProps)
+ expect(validateEditor.state.valid).toBeTruthy()
+ validateEditor.componentWillReceiveProps({...mockProps, content: "bars"})
+ expect(validateEditor.state.valid).toBeFalsy()
+
+ })
+
+ it('should handle input', () => {
+ validateEditor.onInput("foo bar")
+ expect(validateFn).toBeCalledWith("foo bar")
+ })
+
+ it('should handle done', () => {
+ // invalid
+ validateEditor.editor.reset = jest.fn()
+ validateEditor.onDone("foo bar")
+ expect(validateEditor.editor.reset).toBeCalled()
+ // valid
+ validateEditor.onDone("bar")
+ expect(doneFn).toBeCalledWith("bar")
+ })
+})
diff --git a/web/src/js/__tests__/components/ValueEditor/ValueEditorSpec.js b/web/src/js/__tests__/components/ValueEditor/ValueEditorSpec.js
new file mode 100644
index 00000000..f94a6acc
--- /dev/null
+++ b/web/src/js/__tests__/components/ValueEditor/ValueEditorSpec.js
@@ -0,0 +1,155 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import TestUtils from 'react-dom/test-utils'
+import ValueEditor from '../../../components/ValueEditor/ValueEditor'
+import { Key } from '../../../utils'
+
+describe('ValueEditor Component', () => {
+
+ let mockFn = jest.fn()
+ it ('should render correctly', () => {
+ let valueEditor = renderer.create(
+ <ValueEditor content="foo" onDone={mockFn}/>
+ ),
+ tree = valueEditor.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ let valueEditor = TestUtils.renderIntoDocument(
+ <ValueEditor content="<script>foo</script>" onDone={mockFn}/>
+ )
+ it('should handle this.blur', () => {
+ valueEditor.input.blur = jest.fn()
+ valueEditor.blur()
+ expect(valueEditor.input.blur).toHaveBeenCalled()
+ })
+
+ it('should handle reset', () => {
+ valueEditor.reset()
+ expect(valueEditor.input.innerHTML).toEqual(
+ "&lt;script&gt;foo&lt;/script&gt;"
+ )
+ })
+
+ it('should handle paste', () => {
+ let mockEvent = {
+ preventDefault: jest.fn(),
+ clipboardData: { getData: (t) => "foo content"}
+ }
+ document.execCommand = jest.fn()
+ valueEditor.onPaste(mockEvent)
+ expect(document.execCommand).toBeCalledWith('insertHTML', false, "foo content")
+ })
+
+ it('should handle mouseDown', () => {
+ window.addEventListener = jest.fn()
+ valueEditor.onMouseDown({})
+ expect(valueEditor._mouseDown).toBeTruthy()
+ expect(window.addEventListener).toBeCalledWith('mouseup', valueEditor.onMouseUp)
+ })
+
+ it('should handle mouseUp', () => {
+ window.removeEventListener = jest.fn()
+ valueEditor.onMouseUp()
+ expect(window.removeEventListener).toBeCalledWith('mouseup', valueEditor.onMouseUp)
+ })
+
+ it('should handle focus', () => {
+ let mockEvent = { clientX: 1, clientY: 2 },
+ mockSelection = {
+ rangeCount: 1,
+ getRangeAt: jest.fn( (index) => {return { selectNodeContents: jest.fn() }}),
+ removeAllRanges: jest.fn(),
+ addRange: jest.fn()
+ },
+ clearState = (v) => {
+ v._mouseDown = false
+ v._ignore_events = false
+ v.state.editable = false
+ }
+ window.getSelection = () => mockSelection
+
+ // return undefined when mouse down
+ valueEditor.onMouseDown()
+ expect(valueEditor.onFocus(mockEvent)).toEqual(undefined)
+ valueEditor.onMouseUp()
+
+ // sel.rangeCount > 0
+ valueEditor.onFocus(mockEvent)
+ expect(mockSelection.getRangeAt).toBeCalledWith(0)
+ expect(valueEditor.state.editable).toBeTruthy()
+ expect(mockSelection.removeAllRanges).toBeCalled()
+ expect(mockSelection.addRange).toBeCalled()
+ clearState(valueEditor)
+
+ // document.caretPositionFromPoint
+ mockSelection.rangeCount = 0
+ let mockRange = { setStart: jest.fn(), selectNodeContents: jest.fn() }
+
+ document.caretPositionFromPoint = jest.fn((x, y) => {
+ return { offsetNode: 0, offset: x + y}
+ })
+ document.createRange = jest.fn(() => mockRange)
+ valueEditor.onFocus(mockEvent)
+ expect(mockRange.setStart).toBeCalledWith(0, 3)
+ clearState(valueEditor)
+ document.caretPositionFromPoint = null
+
+ //document.caretRangeFromPoint
+ document.caretRangeFromPoint = jest.fn(() => mockRange)
+ valueEditor.onFocus(mockEvent)
+ expect(document.caretRangeFromPoint).toBeCalledWith(1, 2)
+ clearState(valueEditor)
+ document.caretRangeFromPoint = null
+
+ //else
+ valueEditor.onFocus(mockEvent)
+ expect(mockRange.selectNodeContents).toBeCalledWith(valueEditor.input)
+ clearState(valueEditor)
+ })
+
+ it('should handle click', () => {
+ valueEditor.onMouseUp = jest.fn()
+ valueEditor.onFocus = jest.fn()
+ valueEditor.onClick('foo')
+ expect(valueEditor.onMouseUp).toBeCalled()
+ expect(valueEditor.onFocus).toBeCalledWith('foo')
+ })
+
+ it('should handle blur', () => {
+ // return undefined
+ valueEditor._ignore_events = true
+ expect(valueEditor.onBlur({})).toEqual(undefined)
+ // else
+ valueEditor._ignore_events = false
+ valueEditor.onBlur({})
+ expect(valueEditor.state.editable).toBeFalsy()
+ expect(valueEditor.props.onDone).toBeCalledWith(valueEditor.input.textContent)
+ })
+
+ it('should handle key down', () => {
+ let mockKeyEvent = (keyCode, shiftKey=false) => {
+ return {
+ keyCode: keyCode,
+ shiftKey: shiftKey,
+ stopPropagation: jest.fn(),
+ preventDefault: jest.fn()
+ }
+ }
+ valueEditor.reset = jest.fn()
+ valueEditor.blur = jest.fn()
+ valueEditor.onKeyDown(mockKeyEvent(Key.ESC))
+ expect(valueEditor.reset).toBeCalled()
+ expect(valueEditor.blur).toBeCalled()
+ valueEditor.blur.mockReset()
+
+ valueEditor.onKeyDown(mockKeyEvent(Key.ENTER))
+ expect(valueEditor.blur).toBeCalled()
+
+ valueEditor.onKeyDown(mockKeyEvent(Key.SPACE))
+ })
+
+ it('should handle input', () => {
+ valueEditor.onInput()
+ })
+})
diff --git a/web/src/js/__tests__/components/ValueEditor/__snapshots__/ValidateEditorSpec.js.snap b/web/src/js/__tests__/components/ValueEditor/__snapshots__/ValidateEditorSpec.js.snap
new file mode 100644
index 00000000..96b9ce19
--- /dev/null
+++ b/web/src/js/__tests__/components/ValueEditor/__snapshots__/ValidateEditorSpec.js.snap
@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ValidateEditor Component should render correctly 1`] = `
+<div
+ className="inline-input editable has-success"
+ contentEditable={undefined}
+ dangerouslySetInnerHTML={
+ Object {
+ "__html": "foo",
+ }
+ }
+ onBlur={[Function]}
+ onClick={[Function]}
+ onFocus={[Function]}
+ onInput={[Function]}
+ onKeyDown={[Function]}
+ onMouseDown={[Function]}
+ onPaste={[Function]}
+ tabIndex={0}
+/>
+`;
diff --git a/web/src/js/__tests__/components/ValueEditor/__snapshots__/ValueEditorSpec.js.snap b/web/src/js/__tests__/components/ValueEditor/__snapshots__/ValueEditorSpec.js.snap
new file mode 100644
index 00000000..91e8ee84
--- /dev/null
+++ b/web/src/js/__tests__/components/ValueEditor/__snapshots__/ValueEditorSpec.js.snap
@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ValueEditor Component should render correctly 1`] = `
+<div
+ className="inline-input editable"
+ contentEditable={undefined}
+ dangerouslySetInnerHTML={
+ Object {
+ "__html": "foo",
+ }
+ }
+ onBlur={[Function]}
+ onClick={[Function]}
+ onFocus={[Function]}
+ onInput={[Function]}
+ onKeyDown={[Function]}
+ onMouseDown={[Function]}
+ onPaste={[Function]}
+ tabIndex={0}
+/>
+`;
diff --git a/web/src/js/__tests__/components/common/ButtonSpec.js b/web/src/js/__tests__/components/common/ButtonSpec.js
new file mode 100644
index 00000000..ea05ee6e
--- /dev/null
+++ b/web/src/js/__tests__/components/common/ButtonSpec.js
@@ -0,0 +1,26 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import Button from '../../../components/common/Button'
+
+describe('Button Component', () => {
+
+ it('should render correctly', () => {
+ let button = renderer.create(
+ <Button className="classname" onClick={() => "onclick"} title="title" icon="icon">
+ <a>foo</a>
+ </Button>
+ ),
+ tree = button.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should be able to be disabled', () => {
+ let button = renderer.create(
+ <Button className="classname" onClick={() => "onclick"} disabled="true" children="children">
+ <a>foo</a>
+ </Button>
+ ),
+ tree = button.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+})
diff --git a/web/src/js/__tests__/components/common/DocsLinkSpec.js b/web/src/js/__tests__/components/common/DocsLinkSpec.js
new file mode 100644
index 00000000..effed1b7
--- /dev/null
+++ b/web/src/js/__tests__/components/common/DocsLinkSpec.js
@@ -0,0 +1,17 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import DocsLink from '../../../components/common/DocsLink'
+
+describe('DocsLink Component', () => {
+ it('should be able to be rendered with children nodes', () => {
+ let docsLink = renderer.create(<DocsLink children="foo" resource="bar"></DocsLink>),
+ tree = docsLink.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should be able to be rendered without children nodes', () => {
+ let docsLink = renderer.create(<DocsLink resource="bar"></DocsLink>),
+ tree = docsLink.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+})
diff --git a/web/src/js/__tests__/components/common/DropdownSpec.js b/web/src/js/__tests__/components/common/DropdownSpec.js
new file mode 100644
index 00000000..c8c57ea6
--- /dev/null
+++ b/web/src/js/__tests__/components/common/DropdownSpec.js
@@ -0,0 +1,38 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import Dropdown, { Divider } from '../../../components/common/Dropdown'
+
+describe('Dropdown Component', () => {
+ let dropup = renderer.create(<Dropdown dropup btnClass="foo">
+ <a href="#">1</a>
+ <Divider/>
+ <a href="#">2</a>
+ </Dropdown>),
+ dropdown = renderer.create(<Dropdown btnClass="foo">
+ <a href="#">1</a>
+ <a href="#">2</a>
+ </Dropdown>)
+
+ it('should render correctly', () => {
+ let tree = dropup.toJSON()
+ expect(tree).toMatchSnapshot()
+
+ tree = dropdown.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle open/close action', () => {
+ document.body.addEventListener('click', ()=>{})
+ let tree = dropup.toJSON(),
+ e = { preventDefault: jest.fn() }
+ tree.children[0].props.onClick(e)
+ expect(tree).toMatchSnapshot()
+
+ // click action when the state is open
+ tree.children[0].props.onClick(e)
+
+ // close
+ document.body.click()
+ expect(tree).toMatchSnapshot()
+ })
+})
diff --git a/web/src/js/__tests__/components/common/FileChooserSpec.js b/web/src/js/__tests__/components/common/FileChooserSpec.js
new file mode 100644
index 00000000..7d031a38
--- /dev/null
+++ b/web/src/js/__tests__/components/common/FileChooserSpec.js
@@ -0,0 +1,38 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import FileChooser from '../../../components/common/FileChooser'
+
+describe('FileChooser Component', () => {
+ let openFileFunc = jest.fn(),
+ createNodeMock = () => { return { click: jest.fn() } },
+ fileChooser = renderer.create(
+ <FileChooser className="foo" title="bar" onOpenFile={ openFileFunc }/>
+ , { createNodeMock })
+ //[test refs with react-test-renderer](https://github.com/facebook/react/issues/7371)
+
+ it('should render correctly', () => {
+ let tree = fileChooser.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle click action', () => {
+ let tree = fileChooser.toJSON(),
+ mockEvent = {
+ preventDefault: jest.fn(),
+ target: {
+ files: [ "foo", "bar" ]
+ }
+ }
+ tree.children[1].props.onChange(mockEvent)
+ expect(openFileFunc).toBeCalledWith("foo")
+ tree.props.onClick()
+ // without files
+ mockEvent = {
+ ...mockEvent,
+ target: { files: [ ]}
+ }
+ openFileFunc.mockClear()
+ tree.children[1].props.onChange(mockEvent)
+ expect(openFileFunc).not.toBeCalled()
+ })
+})
diff --git a/web/src/js/__tests__/components/common/SplitterSpec.js b/web/src/js/__tests__/components/common/SplitterSpec.js
new file mode 100644
index 00000000..9ec48350
--- /dev/null
+++ b/web/src/js/__tests__/components/common/SplitterSpec.js
@@ -0,0 +1,84 @@
+import React from 'react'
+import ReactDOM from 'react-dom'
+import renderer from 'react-test-renderer'
+import Splitter from '../../../components/common/Splitter'
+import TestUtils from 'react-dom/test-utils';
+
+describe('Splitter Component', () => {
+
+ it('should render correctly', () => {
+ let splitter = renderer.create(<Splitter></Splitter>),
+ tree = splitter.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ let splitter = TestUtils.renderIntoDocument(<Splitter></Splitter>),
+ dom = ReactDOM.findDOMNode(splitter),
+ previousElementSibling = {
+ offsetHeight: 0,
+ offsetWidth: 0,
+ style: {flex: ''}
+ },
+ nextElementSibling = {
+ style: {flex: ''}
+ }
+
+ it('should handle mouseDown ', () => {
+ window.addEventListener = jest.fn()
+ splitter.onMouseDown({ pageX: 1, pageY: 2})
+ expect(splitter.state.startX).toEqual(1)
+ expect(splitter.state.startY).toEqual(2)
+ expect(window.addEventListener).toBeCalledWith('mousemove', splitter.onMouseMove)
+ expect(window.addEventListener).toBeCalledWith('mouseup', splitter.onMouseUp)
+ expect(window.addEventListener).toBeCalledWith('dragend', splitter.onDragEnd)
+ })
+
+ it('should handle dragEnd', () => {
+ window.removeEventListener = jest.fn()
+ splitter.onDragEnd()
+ expect(dom.style.transform).toEqual('')
+ expect(window.removeEventListener).toBeCalledWith('dragend', splitter.onDragEnd)
+ expect(window.removeEventListener).toBeCalledWith('mouseup', splitter.onMouseUp)
+ expect(window.removeEventListener).toBeCalledWith('mousemove', splitter.onMouseMove)
+ })
+
+ it('should handle mouseUp', () => {
+
+ Object.defineProperty(dom, 'previousElementSibling', { value: previousElementSibling })
+ Object.defineProperty(dom, 'nextElementSibling', { value: nextElementSibling })
+ splitter.onMouseUp({ pageX: 3, pageY: 4 })
+ expect(splitter.state.applied).toBeTruthy()
+ expect(nextElementSibling.style.flex).toEqual('1 1 auto')
+ expect(previousElementSibling.style.flex).toEqual('0 0 2px')
+ })
+
+ it('should handle mouseMove', () => {
+ splitter.onMouseMove({pageX: 10, pageY: 10})
+ expect(dom.style.transform).toEqual("translate(9px, 0px)")
+
+ let splitterY = TestUtils.renderIntoDocument(<Splitter axis="y"></Splitter>)
+ splitterY.onMouseMove({pageX: 10, pageY: 10})
+ expect(ReactDOM.findDOMNode(splitterY).style.transform).toEqual("translate(0px, 10px)")
+ })
+
+ it('should handle resize', () => {
+ window.setTimeout = jest.fn((event, time) => event())
+ splitter.onResize()
+ expect(window.setTimeout).toHaveBeenCalled()
+ })
+
+ it('should handle componentWillUnmount', () => {
+ splitter.componentWillUnmount()
+ expect(previousElementSibling.style.flex).toEqual('')
+ expect(nextElementSibling.style.flex).toEqual('')
+ expect(splitter.state.applied).toBeTruthy()
+ })
+
+ it('should handle reset', () => {
+ splitter.reset(false)
+ expect(splitter.state.applied).toBeFalsy()
+
+ expect(splitter.reset(true)).toEqual(undefined)
+ })
+
+})
diff --git a/web/src/js/__tests__/components/common/ToggleButtonSpec.js b/web/src/js/__tests__/components/common/ToggleButtonSpec.js
new file mode 100644
index 00000000..2188da82
--- /dev/null
+++ b/web/src/js/__tests__/components/common/ToggleButtonSpec.js
@@ -0,0 +1,26 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import ToggleButton from '../../../components/common/ToggleButton'
+
+describe('ToggleButton Component', () => {
+ let mockFunc = jest.fn()
+
+ it('should render correctly', () => {
+ let checkedButton = renderer.create(
+ <ToggleButton checked={true} onToggle={mockFunc} text="foo">
+ text
+ </ToggleButton>),
+ tree = checkedButton.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle click action', () => {
+ let uncheckButton = renderer.create(
+ <ToggleButton checked={false} onToggle={mockFunc} text="foo">
+ text
+ </ToggleButton>),
+ tree = uncheckButton.toJSON()
+ tree.props.onClick()
+ expect(mockFunc).toBeCalled()
+ })
+})
diff --git a/web/src/js/__tests__/components/common/ToggleInputButtonSpec.js b/web/src/js/__tests__/components/common/ToggleInputButtonSpec.js
new file mode 100644
index 00000000..39e555cd
--- /dev/null
+++ b/web/src/js/__tests__/components/common/ToggleInputButtonSpec.js
@@ -0,0 +1,43 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import ToggleInputButton from '../../../components/common/ToggleInputButton'
+import { Key } from '../../../utils'
+
+describe('ToggleInputButton Component', () => {
+ let mockFunc = jest.fn(),
+ toggleInputButton = undefined,
+ tree = undefined
+
+ it('should render correctly', () => {
+ toggleInputButton = renderer.create(
+ <ToggleInputButton checked={true} name="foo" onToggleChanged={mockFunc}
+ placeholder="bar">text</ToggleInputButton>)
+ tree = toggleInputButton.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle keydown and click action', () => {
+ toggleInputButton = renderer.create(
+ <ToggleInputButton checked={false} name="foo" onToggleChanged={mockFunc}
+ placeholder="bar" txt="txt">text</ToggleInputButton>)
+ tree = toggleInputButton.toJSON()
+ let mockEvent = {
+ keyCode: Key.ENTER,
+ stopPropagation: jest.fn()
+ }
+
+ tree.children[1].props.onKeyDown(mockEvent)
+ expect(mockFunc).toBeCalledWith("txt")
+
+ tree.children[0].props.onClick()
+ expect(mockFunc).toBeCalledWith("txt")
+ })
+
+ it('should update state onChange', () => {
+ // trigger onChange
+ tree.children[1].props.onChange({ target: { value: "foo" }})
+ // update the tree
+ tree = toggleInputButton.toJSON()
+ expect(tree.children[1].props.value).toEqual("foo")
+ })
+})
diff --git a/web/src/js/__tests__/components/common/__snapshots__/ButtonSpec.js.snap b/web/src/js/__tests__/components/common/__snapshots__/ButtonSpec.js.snap
new file mode 100644
index 00000000..1d403b2d
--- /dev/null
+++ b/web/src/js/__tests__/components/common/__snapshots__/ButtonSpec.js.snap
@@ -0,0 +1,30 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Button Component should be able to be disabled 1`] = `
+<div
+ className="classname btn btn-default"
+ disabled="true"
+ onClick={false}
+ title={undefined}
+>
+ <a>
+ foo
+ </a>
+</div>
+`;
+
+exports[`Button Component should render correctly 1`] = `
+<div
+ className="classname btn btn-default"
+ disabled={undefined}
+ onClick={[Function]}
+ title="title"
+>
+ <i
+ className="fa fa-fw icon"
+ />
+ <a>
+ foo
+ </a>
+</div>
+`;
diff --git a/web/src/js/__tests__/components/common/__snapshots__/DocsLinkSpec.js.snap b/web/src/js/__tests__/components/common/__snapshots__/DocsLinkSpec.js.snap
new file mode 100644
index 00000000..d91b77f7
--- /dev/null
+++ b/web/src/js/__tests__/components/common/__snapshots__/DocsLinkSpec.js.snap
@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DocsLink Component should be able to be rendered with children nodes 1`] = `
+<a
+ href="http://docs.mitmproxy.org/en/stable/bar"
+ target="_blank"
+>
+ foo
+</a>
+`;
+
+exports[`DocsLink Component should be able to be rendered without children nodes 1`] = `
+<a
+ href="http://docs.mitmproxy.org/en/stable/bar"
+ target="_blank"
+>
+ <i
+ className="fa fa-question-circle"
+ />
+</a>
+`;
diff --git a/web/src/js/__tests__/components/common/__snapshots__/DropdownSpec.js.snap b/web/src/js/__tests__/components/common/__snapshots__/DropdownSpec.js.snap
new file mode 100644
index 00000000..57d4968d
--- /dev/null
+++ b/web/src/js/__tests__/components/common/__snapshots__/DropdownSpec.js.snap
@@ -0,0 +1,162 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Dropdown Component should handle open/close action 1`] = `
+<div
+ className="dropup"
+>
+ <a
+ className="foo"
+ href="#"
+ onClick={[Function]}
+ />
+ <ul
+ className="dropdown-menu"
+ role="menu"
+ >
+ <li>
+
+ <a
+ href="#"
+ >
+ 1
+ </a>
+
+ </li>
+ <li>
+
+ <hr
+ className="divider"
+ />
+
+ </li>
+ <li>
+
+ <a
+ href="#"
+ >
+ 2
+ </a>
+
+ </li>
+ </ul>
+</div>
+`;
+
+exports[`Dropdown Component should handle open/close action 2`] = `
+<div
+ className="dropup"
+>
+ <a
+ className="foo"
+ href="#"
+ onClick={[Function]}
+ />
+ <ul
+ className="dropdown-menu"
+ role="menu"
+ >
+ <li>
+
+ <a
+ href="#"
+ >
+ 1
+ </a>
+
+ </li>
+ <li>
+
+ <hr
+ className="divider"
+ />
+
+ </li>
+ <li>
+
+ <a
+ href="#"
+ >
+ 2
+ </a>
+
+ </li>
+ </ul>
+</div>
+`;
+
+exports[`Dropdown Component should render correctly 1`] = `
+<div
+ className="dropup"
+>
+ <a
+ className="foo"
+ href="#"
+ onClick={[Function]}
+ />
+ <ul
+ className="dropdown-menu"
+ role="menu"
+ >
+ <li>
+
+ <a
+ href="#"
+ >
+ 1
+ </a>
+
+ </li>
+ <li>
+
+ <hr
+ className="divider"
+ />
+
+ </li>
+ <li>
+
+ <a
+ href="#"
+ >
+ 2
+ </a>
+
+ </li>
+ </ul>
+</div>
+`;
+
+exports[`Dropdown Component should render correctly 2`] = `
+<div
+ className="dropdown"
+>
+ <a
+ className="foo"
+ href="#"
+ onClick={[Function]}
+ />
+ <ul
+ className="dropdown-menu"
+ role="menu"
+ >
+ <li>
+
+ <a
+ href="#"
+ >
+ 1
+ </a>
+
+ </li>
+ <li>
+
+ <a
+ href="#"
+ >
+ 2
+ </a>
+
+ </li>
+ </ul>
+</div>
+`;
diff --git a/web/src/js/__tests__/components/common/__snapshots__/FileChooserSpec.js.snap b/web/src/js/__tests__/components/common/__snapshots__/FileChooserSpec.js.snap
new file mode 100644
index 00000000..5f0b3cf3
--- /dev/null
+++ b/web/src/js/__tests__/components/common/__snapshots__/FileChooserSpec.js.snap
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FileChooser Component should render correctly 1`] = `
+<a
+ className="foo"
+ href="#"
+ onClick={[Function]}
+ title="bar"
+>
+ <i
+ className="fa fa-fw undefined"
+ />
+ <input
+ className="hidden"
+ onChange={[Function]}
+ type="file"
+ />
+</a>
+`;
diff --git a/web/src/js/__tests__/components/common/__snapshots__/SplitterSpec.js.snap b/web/src/js/__tests__/components/common/__snapshots__/SplitterSpec.js.snap
new file mode 100644
index 00000000..dd70ed7a
--- /dev/null
+++ b/web/src/js/__tests__/components/common/__snapshots__/SplitterSpec.js.snap
@@ -0,0 +1,12 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Splitter Component should render correctly 1`] = `
+<div
+ className="splitter splitter-x"
+>
+ <div
+ draggable="true"
+ onMouseDown={[Function]}
+ />
+</div>
+`;
diff --git a/web/src/js/__tests__/components/common/__snapshots__/ToggleButtonSpec.js.snap b/web/src/js/__tests__/components/common/__snapshots__/ToggleButtonSpec.js.snap
new file mode 100644
index 00000000..f468d39f
--- /dev/null
+++ b/web/src/js/__tests__/components/common/__snapshots__/ToggleButtonSpec.js.snap
@@ -0,0 +1,14 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ToggleButton Component should render correctly 1`] = `
+<div
+ className="btn btn-toggle btn-primary"
+ onClick={[Function]}
+>
+ <i
+ className="fa fa-fw fa-check-square-o"
+ />
+  
+ foo
+</div>
+`;
diff --git a/web/src/js/__tests__/components/common/__snapshots__/ToggleInputButtonSpec.js.snap b/web/src/js/__tests__/components/common/__snapshots__/ToggleInputButtonSpec.js.snap
new file mode 100644
index 00000000..b8d80177
--- /dev/null
+++ b/web/src/js/__tests__/components/common/__snapshots__/ToggleInputButtonSpec.js.snap
@@ -0,0 +1,31 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ToggleInputButton Component should render correctly 1`] = `
+<div
+ className="input-group toggle-input-btn"
+>
+ <span
+ className="input-group-btn"
+ onClick={[Function]}
+ >
+ <div
+ className="btn btn-primary"
+ >
+ <span
+ className="fa fa-check-square-o"
+ />
+  
+ foo
+ </div>
+ </span>
+ <input
+ className="form-control"
+ disabled={true}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="bar"
+ type="text"
+ value=""
+ />
+</div>
+`;
diff --git a/web/src/js/__tests__/components/helpers/AutoScrollSpec.js b/web/src/js/__tests__/components/helpers/AutoScrollSpec.js
new file mode 100644
index 00000000..18a3d669
--- /dev/null
+++ b/web/src/js/__tests__/components/helpers/AutoScrollSpec.js
@@ -0,0 +1,41 @@
+import React from "react"
+import ReactDOM from "react-dom"
+import AutoScroll from '../../../components/helpers/AutoScroll'
+import { calcVScroll } from '../../../components/helpers/VirtualScroll'
+import TestUtils from 'react-dom/test-utils'
+
+describe('Autoscroll', () => {
+ let mockFn = jest.fn()
+ class tComponent extends React.Component {
+ constructor(props, context){
+ super(props, context)
+ this.state = { vScroll: calcVScroll() }
+ }
+
+ componentWillUpdate() {
+ mockFn("foo")
+ }
+
+ componentDidUpdate() {
+ mockFn("bar")
+ }
+
+ render() {
+ return (<p>foo</p>)
+ }
+ }
+
+ it('should update component', () => {
+ let Foo = AutoScroll(tComponent),
+ autoScroll = TestUtils.renderIntoDocument(<Foo></Foo>),
+ viewport = ReactDOM.findDOMNode(autoScroll)
+ viewport.scrollTop = 10
+ Object.defineProperty(viewport, "scrollHeight", { value: 10, writable: true })
+ autoScroll.componentWillUpdate()
+ expect(mockFn).toBeCalledWith("foo")
+
+ Object.defineProperty(viewport, "scrollHeight", { value: 0, writable: true })
+ autoScroll.componentDidUpdate()
+ expect(mockFn).toBeCalledWith("bar")
+ })
+})
diff --git a/web/src/js/__tests__/components/helpers/VirtualScrollSpec.js b/web/src/js/__tests__/components/helpers/VirtualScrollSpec.js
new file mode 100644
index 00000000..8081e90d
--- /dev/null
+++ b/web/src/js/__tests__/components/helpers/VirtualScrollSpec.js
@@ -0,0 +1,21 @@
+import { calcVScroll } from '../../../components/helpers/VirtualScroll'
+
+describe('VirtualScroll', () => {
+
+ it('should return default state without options', () => {
+ expect(calcVScroll()).toEqual({start: 0, end: 0, paddingTop: 0, paddingBottom: 0})
+ })
+
+ it('should calculate position without itemHeights', () => {
+ expect(calcVScroll({itemCount: 0, rowHeight: 32, viewportHeight: 400, viewportTop: 0})).toEqual({
+ start: 0, end: 0, paddingTop: 0, paddingBottom: 0
+ })
+ })
+
+ it('should calculate position with itemHeights', () => {
+ expect(calcVScroll({itemCount: 5, itemHeights: [100, 100, 100, 100, 100],
+ viewportHeight: 300, viewportTop: 0})).toEqual({
+ start: 0, end: 4, paddingTop: 0, paddingBottom: 100
+ })
+ })
+})
diff --git a/web/src/js/__tests__/ducks/_tflow.js b/web/src/js/__tests__/ducks/_tflow.js
new file mode 100644
index 00000000..f6a382bd
--- /dev/null
+++ b/web/src/js/__tests__/ducks/_tflow.js
@@ -0,0 +1,97 @@
+export default function(){
+ return {
+ "client_conn": {
+ "address": [
+ "address",
+ 22
+ ],
+ "alpn_proto_negotiated": "http/1.1",
+ "cipher_name": "cipher",
+ "clientcert": null,
+ "id": "4a18d1a0-50a1-48dd-9aa6-d45d74282939",
+ "sni": "address",
+ "ssl_established": false,
+ "timestamp_end": 3.0,
+ "timestamp_ssl_setup": 2.0,
+ "timestamp_start": 1.0,
+ "tls_version": "TLSv1.2"
+ },
+ "error": {
+ "msg": "error",
+ "timestamp": 1495370312.4814785
+ },
+ "id": "d91165be-ca1f-4612-88a9-c0f8696f3e29",
+ "intercepted": false,
+ "marked": false,
+ "modified": false,
+ "request": {
+ "contentHash": "ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73",
+ "contentLength": 7,
+ "headers": [
+ [
+ "header",
+ "qvalue"
+ ],
+ [
+ "content-length",
+ "7"
+ ]
+ ],
+ "host": "address",
+ "http_version": "HTTP/1.1",
+ "is_replay": false,
+ "method": "GET",
+ "path": "/path",
+ "port": 22,
+ "pretty_host": "address",
+ "scheme": "http",
+ "timestamp_end": null,
+ "timestamp_start": null
+ },
+ "response": {
+ "contentHash": "ab530a13e45914982b79f9b7e3fba994cfd1f3fb22f71cea1afbf02b460c6d1d",
+ "contentLength": 7,
+ "headers": [
+ [
+ "header-response",
+ "svalue"
+ ],
+ [
+ "content-length",
+ "7"
+ ]
+ ],
+ "http_version": "HTTP/1.1",
+ "is_replay": false,
+ "reason": "OK",
+ "status_code": 200,
+ "timestamp_end": 1495370312.4814625,
+ "timestamp_start": 1495370312.481462
+ },
+ "server_conn": {
+ "address": [
+ "address",
+ 22
+ ],
+ "alpn_proto_negotiated": null,
+ "id": "f087e7b2-6d0a-41a8-a8f0-e1a4761395f8",
+ "ip_address": [
+ "192.168.0.1",
+ 22
+ ],
+ "sni": "address",
+ "source_address": [
+ "address",
+ 22
+ ],
+ "ssl_established": false,
+ "timestamp_end": 4.0,
+ "timestamp_ssl_setup": 3.0,
+ "timestamp_start": 1.0,
+ "timestamp_tcp_setup": 2.0,
+ "tls_version": "TLSv1.2",
+ "via": null
+ },
+ "type": "http"
+}
+} \ No newline at end of file
diff --git a/web/src/js/__tests__/ducks/connectionSpec.js b/web/src/js/__tests__/ducks/connectionSpec.js
new file mode 100644
index 00000000..d087e867
--- /dev/null
+++ b/web/src/js/__tests__/ducks/connectionSpec.js
@@ -0,0 +1,41 @@
+import reduceConnection from "../../ducks/connection"
+import * as ConnectionActions from "../../ducks/connection"
+import { ConnectionState } from "../../ducks/connection"
+
+describe('connection reducer', () => {
+ it('should return initial state', () => {
+ expect(reduceConnection(undefined, {})).toEqual({
+ state: ConnectionState.INIT,
+ message: null,
+ })
+ })
+
+ it('should handle start fetch', () => {
+ expect(reduceConnection(undefined, ConnectionActions.startFetching())).toEqual({
+ state: ConnectionState.FETCHING,
+ message: undefined,
+ })
+ })
+
+ it('should handle connection established', () => {
+ expect(reduceConnection(undefined, ConnectionActions.connectionEstablished())).toEqual({
+ state: ConnectionState.ESTABLISHED,
+ message: undefined,
+ })
+ })
+
+ it('should handle connection error', () => {
+ expect(reduceConnection(undefined, ConnectionActions.connectionError("no internet"))).toEqual({
+ state: ConnectionState.ERROR,
+ message: "no internet",
+ })
+ })
+
+ it('should handle offline mode', () => {
+ expect(reduceConnection(undefined, ConnectionActions.setOffline())).toEqual({
+ state: ConnectionState.OFFLINE,
+ message: undefined,
+ })
+ })
+
+})
diff --git a/web/src/js/__tests__/ducks/eventLogSpec.js b/web/src/js/__tests__/ducks/eventLogSpec.js
new file mode 100644
index 00000000..1c993734
--- /dev/null
+++ b/web/src/js/__tests__/ducks/eventLogSpec.js
@@ -0,0 +1,38 @@
+import reduceEventLog, * as eventLogActions from '../../ducks/eventLog'
+import reduceStore from '../../ducks/utils/store'
+
+describe('event log reducer', () => {
+ it('should return initial state', () => {
+ expect(reduceEventLog(undefined, {})).toEqual({
+ visible: false,
+ filters: { debug: false, info: true, web: true, warn: true, error: true },
+ ...reduceStore(undefined, {}),
+ })
+ })
+
+ it('should be possible to toggle filter', () => {
+ let state = reduceEventLog(undefined, eventLogActions.add('foo'))
+ expect(reduceEventLog(state, eventLogActions.toggleFilter('info'))).toEqual({
+ visible: false,
+ filters: { ...state.filters, info: false},
+ ...reduceStore(state, {})
+ })
+ })
+
+ it('should be possible to toggle visibility', () => {
+ let state = reduceEventLog(undefined, {})
+ expect(reduceEventLog(state, eventLogActions.toggleVisibility())).toEqual({
+ visible: true,
+ filters: {...state.filters},
+ ...reduceStore(undefined, {})
+ })
+ })
+
+ it('should be possible to add message', () => {
+ let state = reduceEventLog(undefined, eventLogActions.add('foo'))
+ expect(state.visible).toBeFalsy()
+ expect(state.filters).toEqual({
+ debug: false, info: true, web: true, warn: true, error: true
+ })
+ })
+})
diff --git a/web/src/js/__tests__/ducks/flowsSpec.js b/web/src/js/__tests__/ducks/flowsSpec.js
index acfa3083..5bd866f2 100644
--- a/web/src/js/__tests__/ducks/flowsSpec.js
+++ b/web/src/js/__tests__/ducks/flowsSpec.js
@@ -1,31 +1,224 @@
-jest.unmock('../../ducks/flows');
+jest.mock('../../utils')
-import reduceFlows, * as flowActions from '../../ducks/flows'
-import * as storeActions from '../../ducks/utils/store'
+import reduceFlows from "../../ducks/flows"
+import * as flowActions from "../../ducks/flows"
+import reduceStore from "../../ducks/utils/store"
+import { fetchApi } from "../../utils"
+import { createStore } from "./tutils"
-
-describe('select flow', () => {
-
- let state = reduceFlows(undefined, {})
+describe('flow reducer', () => {
+ let state = undefined
for (let i of [1, 2, 3, 4]) {
- state = reduceFlows(state, storeActions.add({ id: i }))
+ state = reduceFlows(state, { type: flowActions.ADD, data: { id: i }, cmd: 'add' })
}
- it('should be possible to select a single flow', () => {
- expect(reduceFlows(state, flowActions.select(2))).toEqual(
- {
- ...state,
- selected: [2],
- }
- )
- })
-
- it('should be possible to deselect a flow', () => {
- expect(reduceFlows({ ...state, selected: [1] }, flowActions.select())).toEqual(
- {
- ...state,
- selected: [],
- }
- )
+ it('should return initial state', () => {
+ expect(reduceFlows(undefined, {})).toEqual({
+ highlight: null,
+ filter: null,
+ sort: { column: null, desc: false },
+ selected: [],
+ ...reduceStore(undefined, {})
+ })
+ })
+
+ describe('selections', () => {
+ it('should be possible to select a single flow', () => {
+ expect(reduceFlows(state, flowActions.select(2))).toEqual(
+ {
+ ...state,
+ selected: [2],
+ }
+ )
+ })
+
+ it('should be possible to deselect a flow', () => {
+ expect(reduceFlows({ ...state, selected: [1] }, flowActions.select())).toEqual(
+ {
+ ...state,
+ selected: [],
+ }
+ )
+ })
+
+ it('should be possible to select relative', () => {
+ // haven't selected any flow
+ expect(
+ flowActions.selectRelative(state, 1)
+ ).toEqual(
+ flowActions.select(4)
+ )
+
+ // already selected some flows
+ expect(
+ flowActions.selectRelative({ ...state, selected: [2] }, 1)
+ ).toEqual(
+ flowActions.select(3)
+ )
+ })
+
+ it('should update state.selected on remove', () => {
+ let next
+ next = reduceFlows({ ...state, selected: [2] }, {
+ type: flowActions.REMOVE,
+ data: 2,
+ cmd: 'remove'
+ })
+ expect(next.selected).toEqual([3])
+
+ //last row
+ next = reduceFlows({ ...state, selected: [4] }, {
+ type: flowActions.REMOVE,
+ data: 4,
+ cmd: 'remove'
+ })
+ expect(next.selected).toEqual([3])
+
+ //multiple selection
+ next = reduceFlows({ ...state, selected: [2, 3, 4] }, {
+ type: flowActions.REMOVE,
+ data: 3,
+ cmd: 'remove'
+ })
+ expect(next.selected).toEqual([2, 4])
+ })
+ })
+
+ it('should be possible to set filter', () => {
+ let filt = "~u 123"
+ expect(reduceFlows(undefined, flowActions.setFilter(filt)).filter).toEqual(filt)
+ })
+
+ it('should be possible to set highlight', () => {
+ let key = "foo"
+ expect(reduceFlows(undefined, flowActions.setHighlight(key)).highlight).toEqual(key)
+ })
+
+ it('should be possible to set sort', () => {
+ let sort = { column: "TLSColumn", desc: 1 }
+ expect(reduceFlows(undefined, flowActions.setSort(sort.column, sort.desc)).sort).toEqual(sort)
+ })
+
+})
+
+describe('flows actions', () => {
+
+ let store = createStore({ reduceFlows })
+
+ let tflow = { id: 1 }
+ it('should handle resume action', () => {
+ store.dispatch(flowActions.resume(tflow))
+ expect(fetchApi).toBeCalledWith('/flows/1/resume', { method: 'POST' })
+ })
+
+ it('should handle resumeAll action', () => {
+ store.dispatch(flowActions.resumeAll())
+ expect(fetchApi).toBeCalledWith('/flows/resume', { method: 'POST' })
+ })
+
+ it('should handle kill action', () => {
+ store.dispatch(flowActions.kill(tflow))
+ expect(fetchApi).toBeCalledWith('/flows/1/kill', { method: 'POST' })
+
+ })
+
+ it('should handle killAll action', () => {
+ store.dispatch(flowActions.killAll())
+ expect(fetchApi).toBeCalledWith('/flows/kill', { method: 'POST' })
+ })
+
+ it('should handle remove action', () => {
+ store.dispatch(flowActions.remove(tflow))
+ expect(fetchApi).toBeCalledWith('/flows/1', { method: 'DELETE' })
+ })
+
+ it('should handle duplicate action', () => {
+ store.dispatch(flowActions.duplicate(tflow))
+ expect(fetchApi).toBeCalledWith('/flows/1/duplicate', { method: 'POST' })
+ })
+
+ it('should handle replay action', () => {
+ store.dispatch(flowActions.replay(tflow))
+ expect(fetchApi).toBeCalledWith('/flows/1/replay', { method: 'POST' })
+ })
+
+ it('should handle revert action', () => {
+ store.dispatch(flowActions.revert(tflow))
+ expect(fetchApi).toBeCalledWith('/flows/1/revert', { method: 'POST' })
+ })
+
+ it('should handle update action', () => {
+ store.dispatch(flowActions.update(tflow, 'foo'))
+ expect(fetchApi.put).toBeCalledWith('/flows/1', 'foo')
+ })
+
+ it('should handle uploadContent action', () => {
+ let body = new FormData(),
+ 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})
+ })
+
+ it('should handle clear action', () => {
+ store.dispatch(flowActions.clear())
+ expect(fetchApi).toBeCalledWith('/clear', { method: 'POST'} )
+ })
+
+ it('should handle download action', () => {
+ let state = reduceFlows(undefined, {})
+ expect(reduceFlows(state, flowActions.download())).toEqual(state)
+ })
+
+ it('should handle upload action', () => {
+ let body = new FormData()
+ body.append('file', 'foo')
+ store.dispatch(flowActions.upload('foo'))
+ expect(fetchApi).toBeCalledWith('/flows/dump', { method: 'POST', body })
+ })
+})
+
+describe('makeSort', () => {
+ it('should be possible to sort by TLSColumn', () => {
+ let sort = flowActions.makeSort({ column: 'TLSColumn', desc: true }),
+ a = { request: { scheme: 'http' } },
+ b = { request: { scheme: 'https' } }
+ expect(sort(a, b)).toEqual(1)
+ })
+
+ it('should be possible to sort by PathColumn', () => {
+ let sort = flowActions.makeSort({ column: 'PathColumn', desc: true }),
+ a = { request: {} },
+ b = { request: {} }
+ expect(sort(a, b)).toEqual(0)
+
+ })
+
+ it('should be possible to sort by MethodColumn', () => {
+ let sort = flowActions.makeSort({ column: 'MethodColumn', desc: true }),
+ a = { request: { method: 'GET' } },
+ b = { request: { method: 'POST' } }
+ expect(sort(b, a)).toEqual(-1)
+ })
+
+ it('should be possible to sort by StatusColumn', () => {
+ let sort = flowActions.makeSort({ column: 'StatusColumn', desc: false }),
+ a = { response: { status_code: 200 } },
+ b = { response: { status_code: 404 } }
+ expect(sort(a, b)).toEqual(-1)
+ })
+
+ it('should be possible to sort by TimeColumn', () => {
+ let sort = flowActions.makeSort({ column: 'TimeColumn', desc: false }),
+ a = { response: { timestamp_end: 9 }, request: { timestamp_start: 8 } },
+ b = { response: { timestamp_end: 10 }, request: { timestamp_start: 8 } }
+ expect(sort(b, a)).toEqual(1)
+ })
+
+ it('should be possible to sort by SizeColumn', () => {
+ let sort = flowActions.makeSort({ column: 'SizeColumn', desc: true }),
+ a = { request: { contentLength: 1 }, response: { contentLength: 1 } },
+ b = { request: { contentLength: 1 } }
+ expect(sort(a, b)).toEqual(-1)
})
})
diff --git a/web/src/js/__tests__/ducks/indexSpec.js b/web/src/js/__tests__/ducks/indexSpec.js
new file mode 100644
index 00000000..c5c4d525
--- /dev/null
+++ b/web/src/js/__tests__/ducks/indexSpec.js
@@ -0,0 +1,12 @@
+import reduceState from '../../ducks/index'
+
+describe('reduceState in js/ducks/index.js', () => {
+ it('should combine flow and header', () => {
+ let state = reduceState(undefined, {})
+ expect(state.hasOwnProperty('eventLog')).toBeTruthy()
+ expect(state.hasOwnProperty('flows')).toBeTruthy()
+ expect(state.hasOwnProperty('settings')).toBeTruthy()
+ expect(state.hasOwnProperty('connection')).toBeTruthy()
+ expect(state.hasOwnProperty('ui')).toBeTruthy()
+ })
+})
diff --git a/web/src/js/__tests__/ducks/settingsSpec.js b/web/src/js/__tests__/ducks/settingsSpec.js
new file mode 100644
index 00000000..46d56ec7
--- /dev/null
+++ b/web/src/js/__tests__/ducks/settingsSpec.js
@@ -0,0 +1,25 @@
+jest.mock('../../utils')
+
+import reduceSettings, * as SettingsActions from '../../ducks/settings'
+
+describe('setting reducer', () => {
+ it('should return initial state', () => {
+ expect(reduceSettings(undefined, {})).toEqual({})
+ })
+
+ it('should handle receive action', () => {
+ let action = { type: SettingsActions.RECEIVE, data: 'foo' }
+ expect(reduceSettings(undefined, action)).toEqual('foo')
+ })
+
+ it('should handle update action', () => {
+ let action = {type: SettingsActions.UPDATE, data: {id: 1} }
+ expect(reduceSettings(undefined, action)).toEqual({id: 1})
+ })
+})
+
+describe('setting actions', () => {
+ it('should be possible to update setting', () => {
+ expect(reduceSettings(undefined, SettingsActions.update())).toEqual({})
+ })
+})
diff --git a/web/src/js/__tests__/ducks/tutils.js b/web/src/js/__tests__/ducks/tutils.js
index 90a21b78..211b61e3 100644
--- a/web/src/js/__tests__/ducks/tutils.js
+++ b/web/src/js/__tests__/ducks/tutils.js
@@ -1,6 +1,3 @@
-jest.unmock('redux')
-jest.unmock('redux-thunk')
-
import { combineReducers, applyMiddleware, createStore as createReduxStore } from 'redux'
import thunk from 'redux-thunk'
@@ -10,3 +7,5 @@ export function createStore(parts) {
applyMiddleware(...[thunk])
)
}
+
+export { default as TFlow } from './_tflow'
diff --git a/web/src/js/__tests__/ducks/ui/flowSpec.js b/web/src/js/__tests__/ducks/ui/flowSpec.js
index e994624d..cd6ffa2f 100644
--- a/web/src/js/__tests__/ducks/ui/flowSpec.js
+++ b/web/src/js/__tests__/ducks/ui/flowSpec.js
@@ -1,7 +1,3 @@
-jest.unmock('../../../ducks/ui/flow')
-jest.unmock('../../../ducks/flows')
-jest.unmock('lodash')
-
import _ from 'lodash'
import reducer, {
startEdit,
diff --git a/web/src/js/__tests__/ducks/ui/headerSpec.js b/web/src/js/__tests__/ducks/ui/headerSpec.js
index 8968e636..98822fd8 100644
--- a/web/src/js/__tests__/ducks/ui/headerSpec.js
+++ b/web/src/js/__tests__/ducks/ui/headerSpec.js
@@ -1,6 +1,3 @@
-jest.unmock('../../../ducks/ui/header')
-jest.unmock('../../../ducks/flows')
-
import reducer, { setActiveMenu } from '../../../ducks/ui/header'
import * as flowActions from '../../../ducks/flows'
diff --git a/web/src/js/__tests__/ducks/ui/indexSpec.js b/web/src/js/__tests__/ducks/ui/indexSpec.js
new file mode 100644
index 00000000..3c136bff
--- /dev/null
+++ b/web/src/js/__tests__/ducks/ui/indexSpec.js
@@ -0,0 +1,9 @@
+import reduceUI from '../../../ducks/ui/index'
+
+describe('reduceUI in js/ducks/ui/index.js', () => {
+ it('should combine flow and header', () => {
+ let state = reduceUI(undefined, {})
+ expect(state.hasOwnProperty('flow')).toBeTruthy()
+ expect(state.hasOwnProperty('header')).toBeTruthy()
+ })
+})
diff --git a/web/src/js/__tests__/ducks/ui/keyboardSpec.js b/web/src/js/__tests__/ducks/ui/keyboardSpec.js
new file mode 100644
index 00000000..500733cb
--- /dev/null
+++ b/web/src/js/__tests__/ducks/ui/keyboardSpec.js
@@ -0,0 +1,157 @@
+jest.mock('../../../utils')
+
+import { Key } from '../../../utils'
+import { onKeyDown } from '../../../ducks/ui/keyboard'
+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 configureStore from 'redux-mock-store'
+import thunk from 'redux-thunk'
+import { fetchApi } from '../../../utils'
+
+const mockStore = configureStore([ thunk ])
+console.debug = jest.fn()
+
+describe('onKeyDown', () => {
+ let flows = undefined
+ for( let i=1; i <= 12; i++ ) {
+ flows = reduceFlows(flows, {type: flowsActions.ADD, data: {id: i, request: true, response: true}, cmd: 'add'})
+ }
+ let store = mockStore({ flows, ui: reduceUI(undefined, {}) })
+ let createKeyEvent = (keyCode, shiftKey = undefined, ctrlKey = undefined) => {
+ return onKeyDown({ keyCode, shiftKey, ctrlKey, preventDefault: jest.fn() })
+ }
+
+ afterEach(() => {
+ store.clearActions()
+ fetchApi.mockClear()
+ });
+
+ it('should handle cursor up', () => {
+ store.getState().flows = reduceFlows(flows, flowsActions.select(2))
+ store.dispatch(createKeyEvent(Key.K))
+ expect(store.getActions()).toEqual([{ flowIds: [1], type: flowsActions.SELECT }])
+
+ store.clearActions()
+ store.dispatch(createKeyEvent(Key.UP))
+ expect(store.getActions()).toEqual([{ flowIds: [1], type: flowsActions.SELECT }])
+ })
+
+ it('should handle cursor down', () => {
+ store.dispatch(createKeyEvent(Key.J))
+ expect(store.getActions()).toEqual([{ flowIds: [3], type: flowsActions.SELECT }])
+
+ store.clearActions()
+ store.dispatch(createKeyEvent(Key.DOWN))
+ expect(store.getActions()).toEqual([{ flowIds: [3], type: flowsActions.SELECT }])
+ })
+
+ it('should handle page down', () => {
+ store.dispatch(createKeyEvent(Key.SPACE))
+ expect(store.getActions()).toEqual([{ flowIds: [12], type: flowsActions.SELECT }])
+
+ store.getState().flows = reduceFlows(flows, flowsActions.select(1))
+ store.clearActions()
+ store.dispatch(createKeyEvent(Key.PAGE_DOWN))
+ expect(store.getActions()).toEqual([{ flowIds: [11], type: flowsActions.SELECT }])
+ })
+
+ it('should handle page up', () => {
+ store.getState().flows = reduceFlows(flows, flowsActions.select(11))
+ store.dispatch(createKeyEvent(Key.PAGE_UP))
+ expect(store.getActions()).toEqual([{ flowIds: [1], type: flowsActions.SELECT }])
+ })
+
+ it('should handle select first', () => {
+ store.dispatch(createKeyEvent(Key.HOME))
+ expect(store.getActions()).toEqual([{ flowIds: [1], type: flowsActions.SELECT }])
+ })
+
+ it('should handle select last', () => {
+ store.getState().flows = reduceFlows(flows, flowsActions.select(1))
+ store.dispatch(createKeyEvent(Key.END))
+ expect(store.getActions()).toEqual([{ flowIds: [12], type: flowsActions.SELECT }])
+ })
+
+ it('should handle deselect', () => {
+ store.dispatch(createKeyEvent(Key.ESC))
+ expect(store.getActions()).toEqual([{ flowIds: [], type: flowsActions.SELECT }])
+ })
+
+ it('should handle switch to left tab', () => {
+ store.dispatch(createKeyEvent(Key.LEFT))
+ expect(store.getActions()).toEqual([{ tab: 'details', type: UIActions.SET_TAB }])
+ })
+
+ it('should handle switch to right tab', () => {
+ store.dispatch(createKeyEvent(Key.TAB))
+ expect(store.getActions()).toEqual([{ tab: 'response', type: UIActions.SET_TAB }])
+
+ store.clearActions()
+ store.dispatch(createKeyEvent(Key.RIGHT))
+ expect(store.getActions()).toEqual([{ tab: 'response', type: UIActions.SET_TAB }])
+ })
+
+ it('should handle delete action', () => {
+ store.dispatch(createKeyEvent(Key.D))
+ expect(fetchApi).toBeCalledWith('/flows/1', { method: 'DELETE' })
+
+ })
+
+ it('should handle duplicate action', () => {
+ store.dispatch(createKeyEvent(Key.D, true))
+ expect(fetchApi).toBeCalledWith('/flows/1/duplicate', { method: 'POST' })
+ })
+
+ it('should handle resume action', () => {
+ // resume all
+ store.dispatch(createKeyEvent(Key.A, true))
+ expect(fetchApi).toBeCalledWith('/flows/resume', { method: 'POST' })
+ // resume
+ store.getState().flows.byId[store.getState().flows.selected[0]].intercepted = true
+ store.dispatch(createKeyEvent(Key.A))
+ expect(fetchApi).toBeCalledWith('/flows/1/resume', { method: 'POST' })
+ })
+
+ it('should handle replay action', () => {
+ store.dispatch(createKeyEvent(Key.R))
+ expect(fetchApi).toBeCalledWith('/flows/1/replay', { method: 'POST' })
+ })
+
+ it('should handle revert action', () => {
+ store.getState().flows.byId[store.getState().flows.selected[0]].modified = true
+ store.dispatch(createKeyEvent(Key.V))
+ expect(fetchApi).toBeCalledWith('/flows/1/revert', { method: 'POST' })
+ })
+
+ it('should handle kill action', () => {
+ // kill all
+ store.dispatch(createKeyEvent(Key.X, true))
+ expect(fetchApi).toBeCalledWith('/flows/kill', { method: 'POST' })
+ // kill
+ store.dispatch(createKeyEvent(Key.X))
+ expect(fetchApi).toBeCalledWith('/flows/1/kill', { method: 'POST' })
+ })
+
+ it('should handle clear action', () => {
+ store.dispatch(createKeyEvent(Key.Z))
+ expect(fetchApi).toBeCalledWith('/clear', { method: 'POST' })
+ })
+
+ it('should stop on some action with no flow is selected', () => {
+ store.getState().flows = reduceFlows(undefined, {})
+ store.dispatch(createKeyEvent(Key.LEFT))
+ store.dispatch(createKeyEvent(Key.TAB))
+ store.dispatch(createKeyEvent(Key.RIGHT))
+ store.dispatch(createKeyEvent(Key.D))
+ expect(fetchApi).not.toBeCalled()
+ })
+
+ it('should do nothing when Ctrl and undefined key is pressed ', () => {
+ store.dispatch(createKeyEvent(Key.BACKSPACE, false, true))
+ store.dispatch(createKeyEvent(0))
+ expect(fetchApi).not.toBeCalled()
+ })
+
+})
diff --git a/web/src/js/__tests__/ducks/utils/storeSpec.js b/web/src/js/__tests__/ducks/utils/storeSpec.js
index e4742490..11a8fe23 100644
--- a/web/src/js/__tests__/ducks/utils/storeSpec.js
+++ b/web/src/js/__tests__/ducks/utils/storeSpec.js
@@ -1,5 +1,3 @@
-jest.unmock('../../../ducks/utils/store')
-
import reduceStore, * as storeActions from '../../../ducks/utils/store'
describe('store reducer', () => {
diff --git a/web/src/js/__tests__/flow/utilsSpec.js b/web/src/js/__tests__/flow/utilsSpec.js
new file mode 100644
index 00000000..2d8f0456
--- /dev/null
+++ b/web/src/js/__tests__/flow/utilsSpec.js
@@ -0,0 +1,69 @@
+import * as utils from '../../flow/utils'
+
+describe('MessageUtils', () => {
+ it('should be possible to get first header', () => {
+ let msg = { headers: [["foo", "bar"]]}
+ expect(utils.MessageUtils.get_first_header(msg, "foo")).toEqual("bar")
+ expect(utils.MessageUtils.get_first_header(msg, "123")).toEqual(undefined)
+ })
+
+ it('should be possible to get Content-Type', () => {
+ let type = "text/html",
+ msg = { headers: [["Content-Type", type]]}
+ expect(utils.MessageUtils.getContentType(msg)).toEqual(type)
+ })
+
+ it('should be possible to match header', () => {
+ let h1 = ["foo", "bar"],
+ msg = {headers : [h1]}
+ expect(utils.MessageUtils.match_header(msg, /foo/i)).toEqual(h1)
+ expect(utils.MessageUtils.match_header(msg, /123/i)).toBeFalsy()
+ })
+
+ it('should be possible to get content URL', () => {
+ // request
+ let msg = "foo", view = "bar",
+ flow = { request: msg, id: 1}
+ expect(utils.MessageUtils.getContentURL(flow, msg, view)).toEqual(
+ "/flows/1/request/content/bar"
+ )
+ expect(utils.MessageUtils.getContentURL(flow, msg, '')).toEqual(
+ "/flows/1/request/content"
+ )
+ // response
+ flow = {response: msg, id: 2}
+ expect(utils.MessageUtils.getContentURL(flow, msg, view)).toEqual(
+ "/flows/2/response/content/bar"
+ )
+ })
+})
+
+describe('RequestUtils', () => {
+ it('should be possible prettify url', () => {
+ let request = {port: 4444, scheme: "http", pretty_host: "foo", path: "/bar"}
+ expect(utils.RequestUtils.pretty_url(request)).toEqual(
+ "http://foo:4444/bar"
+ )
+ })
+})
+
+describe('parseUrl', () => {
+ it('should be possible to parse url', () => {
+ let url = "http://foo:4444/bar"
+ expect(utils.parseUrl(url)).toEqual({
+ port: 4444,
+ scheme: 'http',
+ host: 'foo',
+ path: '/bar'
+ })
+
+ expect(utils.parseUrl("foo:foo")).toBeFalsy()
+ })
+})
+
+describe('isValidHttpVersion', () => {
+ it('should be possible to validate http version', () => {
+ expect(utils.isValidHttpVersion("HTTP/1.1")).toBeTruthy()
+ expect(utils.isValidHttpVersion("HTTP//1")).toBeFalsy()
+ })
+})
diff --git a/web/src/js/__tests__/urlStateSpec.js b/web/src/js/__tests__/urlStateSpec.js
new file mode 100644
index 00000000..c57c0a00
--- /dev/null
+++ b/web/src/js/__tests__/urlStateSpec.js
@@ -0,0 +1,100 @@
+import initialize from '../urlState'
+import { updateStoreFromUrl, updateUrlFromStore } from '../urlState'
+
+import reduceFlows from '../ducks/flows'
+import reduceUI from '../ducks/ui/index'
+import reduceEventLog from '../ducks/eventLog'
+import * as flowsActions from '../ducks/flows'
+
+import configureStore from 'redux-mock-store'
+
+const mockStore = configureStore()
+history.replaceState = jest.fn()
+
+describe('updateStoreFromUrl', () => {
+
+ it('should handle search query', () => {
+ window.location.hash = "#/flows?s=foo"
+ let store = mockStore()
+ updateStoreFromUrl(store)
+ expect(store.getActions()).toEqual([{ filter: "foo", type: "FLOWS_SET_FILTER" }])
+ })
+
+ it('should handle highlight query', () => {
+ window.location.hash = "#/flows?h=foo"
+ let store = mockStore()
+ updateStoreFromUrl(store)
+ expect(store.getActions()).toEqual([{ highlight: "foo", type: "FLOWS_SET_HIGHLIGHT" }])
+ })
+
+ it('should handle show event log', () => {
+ window.location.hash = "#/flows?e=true"
+ let initialState = { eventLog: reduceEventLog(undefined, {}) },
+ store = mockStore(initialState)
+ updateStoreFromUrl(store)
+ expect(store.getActions()).toEqual([{ type: "EVENTS_TOGGLE_VISIBILITY" }])
+ })
+
+ it('should handle unimplemented query argument', () => {
+ window.location.hash = "#/flows?foo=bar"
+ console.error = jest.fn()
+ let store = mockStore()
+ updateStoreFromUrl(store)
+ expect(console.error).toBeCalledWith("unimplemented query arg: foo=bar")
+ })
+
+ it('should select flow and tab', () => {
+ window.location.hash = "#/flows/123/request"
+ let store = mockStore()
+ updateStoreFromUrl(store)
+ expect(store.getActions()).toEqual([
+ {
+ flowIds: ["123"],
+ type: "FLOWS_SELECT"
+ },
+ {
+ tab: "request",
+ type: "UI_FLOWVIEW_SET_TAB"
+ }
+ ])
+ })
+})
+
+describe('updateUrlFromStore', () => {
+ let initialState = {
+ flows: reduceFlows(undefined, {}),
+ ui: reduceUI(undefined, {}),
+ eventLog: reduceEventLog(undefined, {})
+ }
+
+ it('should update initial url', () => {
+ let store = mockStore(initialState)
+ updateUrlFromStore(store)
+ expect(history.replaceState).toBeCalledWith(undefined, '', '/#/flows')
+ })
+
+ it('should update url', () => {
+ let flows = reduceFlows(undefined, flowsActions.select(123)),
+ state = {
+ ...initialState,
+ flows: reduceFlows(flows, flowsActions.setFilter('~u foo'))
+ },
+ store = mockStore(state)
+ updateUrlFromStore(store)
+ expect(history.replaceState).toBeCalledWith(undefined, '', '/#/flows/123/request?s=~u foo')
+ })
+})
+
+describe('initialize', () => {
+ let initialState = {
+ flows: reduceFlows(undefined, {}),
+ ui: reduceUI(undefined, {}),
+ eventLog: reduceEventLog(undefined, {})
+ }
+
+ it('should handle initial state', () => {
+ let store = mockStore(initialState)
+ initialize(store)
+ store.dispatch({ type: "foo" })
+ })
+})
diff --git a/web/src/js/__tests__/utilsSpec.js b/web/src/js/__tests__/utilsSpec.js
new file mode 100644
index 00000000..9a1a0750
--- /dev/null
+++ b/web/src/js/__tests__/utilsSpec.js
@@ -0,0 +1,95 @@
+import * as utils from '../utils'
+
+global.fetch = jest.fn()
+
+describe('formatSize', () => {
+ it('should return 0 when 0 byte', () => {
+ expect(utils.formatSize(0)).toEqual('0')
+ })
+
+ it('should return formatted size', () => {
+ expect(utils.formatSize(27104011)).toEqual("25.8mb")
+ expect(utils.formatSize(1023)).toEqual("1023b")
+ })
+})
+
+describe('formatTimeDelta', () => {
+ it('should return formatted time', () => {
+ expect(utils.formatTimeDelta(3600100)).toEqual("1h")
+ })
+})
+
+describe('formatTimeSTamp', () => {
+ it('should return formatted time', () => {
+ expect(utils.formatTimeStamp(1483228800)).toEqual("2017-01-01 00:00:00.000")
+ })
+})
+
+describe('reverseString', () => {
+ it('should return reversed string', () => {
+ let str1 = "abc", str2="xyz"
+ expect(utils.reverseString(str1) > utils.reverseString(str2)).toBeTruthy()
+ })
+})
+
+describe('fetchApi', () => {
+ it('should handle fetch operation', () => {
+ utils.fetchApi('http://foo/bar', {method: "POST"})
+ expect(fetch.mock.calls[0][0]).toEqual(
+ "http://foo/bar?_xsrf=undefined"
+ )
+ fetch.mockClear()
+
+ utils.fetchApi('http://foo?bar=1', {method: "POST"})
+ expect(fetch.mock.calls[0][0]).toEqual(
+ "http://foo?bar=1&_xsrf=undefined"
+ )
+
+ })
+
+ it('should be possible to do put request', () => {
+ fetch.mockClear()
+ utils.fetchApi.put("http://foo", [1, 2, 3], {})
+ expect(fetch.mock.calls[0]).toEqual(
+ [
+ "http://foo?_xsrf=undefined",
+ {
+ body: "[1,2,3]",
+ credentials: "same-origin",
+ headers: { "Content-Type": "application/json" },
+ method: "PUT"
+ },
+ ]
+ )
+ })
+})
+
+describe('getDiff', () => {
+ it('should return json object including only the changed keys value pairs', () => {
+ let obj1 = {a: 1, b:{ foo: 1} , c: [3]},
+ obj2 = {a: 1, b:{ foo: 2} , c: [4]}
+ expect(utils.getDiff(obj1, obj2)).toEqual({ b: {foo: 2}, c:[4]})
+ })
+})
+
+describe('pure', () => {
+ let tFunc = function({ className }) {
+ return (<p className={ className }>foo</p>)
+ },
+ puredFunc = utils.pure(tFunc),
+ f = new puredFunc('bar')
+
+ it('should display function name', () => {
+ 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/backends/websocket.js b/web/src/js/backends/websocket.js
index 44b260c9..01094ac4 100644
--- a/web/src/js/backends/websocket.js
+++ b/web/src/js/backends/websocket.js
@@ -4,6 +4,7 @@
* An alternative backend may use the REST API only to host static instances.
*/
import { fetchApi } from "../utils"
+import * as connectionActions from "../ducks/connection"
const CMD_RESET = 'reset'
@@ -17,7 +18,7 @@ export default class WebsocketBackend {
connect() {
this.socket = new WebSocket(location.origin.replace('http', 'ws') + '/updates')
this.socket.addEventListener('open', () => this.onOpen())
- this.socket.addEventListener('close', () => this.onClose())
+ this.socket.addEventListener('close', event => this.onClose(event))
this.socket.addEventListener('message', msg => this.onMessage(JSON.parse(msg.data)))
this.socket.addEventListener('error', error => this.onError(error))
}
@@ -26,6 +27,7 @@ export default class WebsocketBackend {
this.fetchData("settings")
this.fetchData("flows")
this.fetchData("events")
+ this.store.dispatch(connectionActions.startFetching())
}
fetchData(resource) {
@@ -59,15 +61,22 @@ export default class WebsocketBackend {
let queue = this.activeFetches[resource]
delete this.activeFetches[resource]
queue.forEach(msg => this.onMessage(msg))
+
+ if(Object.keys(this.activeFetches).length === 0) {
+ // We have fetched the last resource
+ this.store.dispatch(connectionActions.connectionEstablished())
+ }
}
- onClose() {
- // FIXME
- console.error("onClose", arguments)
+ onClose(closeEvent) {
+ this.store.dispatch(connectionActions.connectionError(
+ `Connection closed at ${new Date().toUTCString()} with error code ${closeEvent.code}.`
+ ))
+ console.error("websocket connection closed", closeEvent)
}
onError() {
// FIXME
- console.error("onError", arguments)
+ console.error("websocket connection errored", arguments)
}
}
diff --git a/web/src/js/components/ContentView.jsx b/web/src/js/components/ContentView.jsx
index 398438ab..a79bf9e5 100644
--- a/web/src/js/components/ContentView.jsx
+++ b/web/src/js/components/ContentView.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import * as ContentViews from './ContentView/ContentViews'
import * as MetaViews from './ContentView/MetaViews'
@@ -11,8 +12,8 @@ ContentView.propTypes = {
// It may seem a bit weird at the first glance:
// Every view takes the flow and the message as props, e.g.
// <Auto flow={flow} message={flow.request}/>
- flow: React.PropTypes.object.isRequired,
- message: React.PropTypes.object.isRequired,
+ flow: PropTypes.object.isRequired,
+ message: PropTypes.object.isRequired,
}
ContentView.isContentTooLarge = msg => msg.contentLength > 1024 * 1024 * (ContentViews.ViewImage.matches(msg) ? 10 : 0.2)
diff --git a/web/src/js/components/ContentView/CodeEditor.jsx b/web/src/js/components/ContentView/CodeEditor.jsx
index 8afc128f..f5961447 100644
--- a/web/src/js/components/ContentView/CodeEditor.jsx
+++ b/web/src/js/components/ContentView/CodeEditor.jsx
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react'
+import React from 'react'
+import PropTypes from 'prop-types'
import Codemirror from 'react-codemirror';
diff --git a/web/src/js/components/ContentView/ContentLoader.jsx b/web/src/js/components/ContentView/ContentLoader.jsx
index e7a6f379..4cafde28 100644
--- a/web/src/js/components/ContentView/ContentLoader.jsx
+++ b/web/src/js/components/ContentView/ContentLoader.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import { MessageUtils } from '../../flow/utils.js'
export default View => class extends React.Component {
diff --git a/web/src/js/components/ContentView/ContentViewOptions.jsx b/web/src/js/components/ContentView/ContentViewOptions.jsx
index 1ec9013e..e3cc39cd 100644
--- a/web/src/js/components/ContentView/ContentViewOptions.jsx
+++ b/web/src/js/components/ContentView/ContentViewOptions.jsx
@@ -1,12 +1,13 @@
-import React, { PropTypes } from 'react'
+import React from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import ViewSelector from './ViewSelector'
import UploadContentButton from './UploadContentButton'
import DownloadContentButton from './DownloadContentButton'
ContentViewOptions.propTypes = {
- flow: React.PropTypes.object.isRequired,
- message: React.PropTypes.object.isRequired,
+ flow: PropTypes.object.isRequired,
+ message: PropTypes.object.isRequired,
}
function ContentViewOptions({ flow, message, uploadContent, readonly, contentViewDescription }) {
diff --git a/web/src/js/components/ContentView/ContentViews.jsx b/web/src/js/components/ContentView/ContentViews.jsx
index db239195..136188d4 100644
--- a/web/src/js/components/ContentView/ContentViews.jsx
+++ b/web/src/js/components/ContentView/ContentViews.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes, Component } from 'react'
+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'
@@ -21,7 +22,7 @@ function ViewImage({ flow, message }) {
}
Edit.propTypes = {
- content: React.PropTypes.string.isRequired,
+ content: PropTypes.string.isRequired,
}
function Edit({ content, onChange }) {
diff --git a/web/src/js/components/ContentView/DownloadContentButton.jsx b/web/src/js/components/ContentView/DownloadContentButton.jsx
index 3f11f909..447db211 100644
--- a/web/src/js/components/ContentView/DownloadContentButton.jsx
+++ b/web/src/js/components/ContentView/DownloadContentButton.jsx
@@ -1,5 +1,5 @@
import { MessageUtils } from "../../flow/utils"
-import { PropTypes } from 'react'
+import PropTypes from 'prop-types'
DownloadContentButton.propTypes = {
flow: PropTypes.object.isRequired,
diff --git a/web/src/js/components/ContentView/ShowFullContentButton.jsx b/web/src/js/components/ContentView/ShowFullContentButton.jsx
index fd68991e..fd627ad9 100644
--- a/web/src/js/components/ContentView/ShowFullContentButton.jsx
+++ b/web/src/js/components/ContentView/ShowFullContentButton.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { render } from 'react-dom';
import Button from '../common/Button';
diff --git a/web/src/js/components/ContentView/UploadContentButton.jsx b/web/src/js/components/ContentView/UploadContentButton.jsx
index de349af4..0021593f 100644
--- a/web/src/js/components/ContentView/UploadContentButton.jsx
+++ b/web/src/js/components/ContentView/UploadContentButton.jsx
@@ -1,4 +1,4 @@
-import { PropTypes } from 'react'
+import PropTypes from 'prop-types'
import FileChooser from '../common/FileChooser'
UploadContentButton.propTypes = {
@@ -6,7 +6,7 @@ UploadContentButton.propTypes = {
}
export default function UploadContentButton({ uploadContent }) {
-
+
return (
<FileChooser
icon="fa-upload"
diff --git a/web/src/js/components/ContentView/ViewSelector.jsx b/web/src/js/components/ContentView/ViewSelector.jsx
index 43a53995..4c99d5ed 100644
--- a/web/src/js/components/ContentView/ViewSelector.jsx
+++ b/web/src/js/components/ContentView/ViewSelector.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes, Component } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { setContentView } from '../../ducks/ui/flow';
import Dropdown from '../common/Dropdown'
diff --git a/web/src/js/components/EventLog.jsx b/web/src/js/components/EventLog.jsx
index 1a449511..a83cdb28 100644
--- a/web/src/js/components/EventLog.jsx
+++ b/web/src/js/components/EventLog.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { toggleFilter, toggleVisibility } from '../ducks/eventLog'
import ToggleButton from './common/ToggleButton'
@@ -55,7 +56,7 @@ class EventLog extends Component {
<div onMouseDown={this.onDragStart}>
Eventlog
<div className="pull-right">
- {['debug', 'info', 'web'].map(type => (
+ {['debug', 'info', 'web', 'warn', 'error'].map(type => (
<ToggleButton key={type} text={type} checked={filters[type]} onToggle={() => toggleFilter(type)}/>
))}
<i onClick={close} className="fa fa-close"></i>
diff --git a/web/src/js/components/EventLog/EventList.jsx b/web/src/js/components/EventLog/EventList.jsx
index d0b036e7..a77b7e36 100644
--- a/web/src/js/components/EventLog/EventList.jsx
+++ b/web/src/js/components/EventLog/EventList.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import shallowEqual from 'shallowequal'
import AutoScroll from '../helpers/AutoScroll'
@@ -83,7 +84,12 @@ class EventLogList extends Component {
}
function LogIcon({ event }) {
- const icon = { web: 'html5', debug: 'bug' }[event.level] || 'info'
+ const icon = {
+ web: 'html5',
+ debug: 'bug',
+ warn: 'exclamation-triangle',
+ error: 'ban'
+ }[event.level] || 'info'
return <i className={`fa fa-fw fa-${icon}`}></i>
}
diff --git a/web/src/js/components/FlowTable.jsx b/web/src/js/components/FlowTable.jsx
index eddeed62..24c1f3a1 100644
--- a/web/src/js/components/FlowTable.jsx
+++ b/web/src/js/components/FlowTable.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes } from 'react'
+import React from 'react'
+import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import shallowEqual from 'shallowequal'
import AutoScroll from './helpers/AutoScroll'
diff --git a/web/src/js/components/FlowTable/FlowRow.jsx b/web/src/js/components/FlowTable/FlowRow.jsx
index 7961d502..71a30e39 100644
--- a/web/src/js/components/FlowTable/FlowRow.jsx
+++ b/web/src/js/components/FlowTable/FlowRow.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes } from 'react'
+import React from 'react'
+import PropTypes from 'prop-types'
import classnames from 'classnames'
import columns from './FlowColumns'
import { pure } from '../../utils'
diff --git a/web/src/js/components/FlowTable/FlowTableHead.jsx b/web/src/js/components/FlowTable/FlowTableHead.jsx
index b201285f..59ad73e2 100644
--- a/web/src/js/components/FlowTable/FlowTableHead.jsx
+++ b/web/src/js/components/FlowTable/FlowTableHead.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes } from 'react'
+import React from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import classnames from 'classnames'
import columns from './FlowColumns'
@@ -7,8 +8,8 @@ import { setSort } from '../../ducks/flows'
FlowTableHead.propTypes = {
setSort: PropTypes.func.isRequired,
- sortDesc: React.PropTypes.bool.isRequired,
- sortColumn: React.PropTypes.string,
+ sortDesc: PropTypes.bool.isRequired,
+ sortColumn: PropTypes.string,
}
function FlowTableHead({ sortColumn, sortDesc, setSort }) {
diff --git a/web/src/js/components/FlowView/Headers.jsx b/web/src/js/components/FlowView/Headers.jsx
index 2e181383..92e11465 100644
--- a/web/src/js/components/FlowView/Headers.jsx
+++ b/web/src/js/components/FlowView/Headers.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import ValueEditor from '../ValueEditor/ValueEditor'
import { Key } from '../../utils'
diff --git a/web/src/js/components/FlowView/Messages.jsx b/web/src/js/components/FlowView/Messages.jsx
index 93c52660..4a31faf4 100644
--- a/web/src/js/components/FlowView/Messages.jsx
+++ b/web/src/js/components/FlowView/Messages.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
diff --git a/web/src/js/components/FlowView/Nav.jsx b/web/src/js/components/FlowView/Nav.jsx
index 37c073ce..af5a879e 100644
--- a/web/src/js/components/FlowView/Nav.jsx
+++ b/web/src/js/components/FlowView/Nav.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes } from 'react'
+import React from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import classnames from 'classnames'
diff --git a/web/src/js/components/FlowView/ToggleEdit.jsx b/web/src/js/components/FlowView/ToggleEdit.jsx
index 6a691a3d..b47b45db 100644
--- a/web/src/js/components/FlowView/ToggleEdit.jsx
+++ b/web/src/js/components/FlowView/ToggleEdit.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes, Component } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { startEdit, stopEdit } from '../../ducks/ui/flow'
diff --git a/web/src/js/components/Footer.jsx b/web/src/js/components/Footer.jsx
index 58dd0dcb..08d15496 100644
--- a/web/src/js/components/Footer.jsx
+++ b/web/src/js/components/Footer.jsx
@@ -1,14 +1,15 @@
import React from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { formatSize } from '../utils.js'
Footer.propTypes = {
- settings: React.PropTypes.object.isRequired,
+ settings: PropTypes.object.isRequired,
}
function Footer({ settings }) {
let {mode, intercept, showhost, no_upstream_cert, rawtcp, http2, websocket, anticache, anticomp,
- stickyauth, stickycookie, stream_large_bodies, listen_host, listen_port, version} = settings;
+ stickyauth, stickycookie, stream_large_bodies, listen_host, listen_port, version, server} = settings;
return (
<footer>
{mode && mode != "regular" && (
@@ -48,9 +49,11 @@ function Footer({ settings }) {
<span className="label label-success">stream: {formatSize(stream_large_bodies)}</span>
)}
<div className="pull-right">
- <span className="label label-primary" title="HTTP Proxy Server Address">
- {listen_host || "*"}:{listen_port}
- </span>
+ {server && (
+ <span className="label label-primary" title="HTTP Proxy Server Address">
+ {listen_host||"*"}:{listen_port}
+ </span>
+ )}
<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 c15c951f..ebe7453c 100644
--- a/web/src/js/components/Header.jsx
+++ b/web/src/js/components/Header.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import classnames from 'classnames'
import MainMenu from './Header/MainMenu'
@@ -6,6 +7,7 @@ import OptionMenu from './Header/OptionMenu'
import FileMenu from './Header/FileMenu'
import FlowMenu from './Header/FlowMenu'
import {setActiveMenu} from '../ducks/ui/header'
+import ConnectionIndicator from "./Header/ConnectionIndicator"
class Header extends Component {
static entries = [MainMenu, OptionMenu]
@@ -38,10 +40,11 @@ class Header extends Component {
{Entry.title}
</a>
))}
+ <ConnectionIndicator/>
</nav>
- <menu>
+ <div>
<Active/>
- </menu>
+ </div>
</header>
)
}
diff --git a/web/src/js/components/Header/ConnectionIndicator.jsx b/web/src/js/components/Header/ConnectionIndicator.jsx
new file mode 100644
index 00000000..1ee42e25
--- /dev/null
+++ b/web/src/js/components/Header/ConnectionIndicator.jsx
@@ -0,0 +1,30 @@
+import React from "react"
+import PropTypes from "prop-types"
+import { connect } from "react-redux"
+import { ConnectionState } from "../../ducks/connection"
+
+
+ConnectionIndicator.propTypes = {
+ state: PropTypes.symbol.isRequired,
+ message: PropTypes.string,
+
+}
+function ConnectionIndicator({ state, message }) {
+ switch (state) {
+ case ConnectionState.INIT:
+ return <span className="connection-indicator init">connecting…</span>;
+ case ConnectionState.FETCHING:
+ return <span className="connection-indicator fetching">fetching data…</span>;
+ case ConnectionState.ESTABLISHED:
+ return <span className="connection-indicator established">connected</span>;
+ case ConnectionState.ERROR:
+ return <span className="connection-indicator error"
+ title={message}>connection lost</span>;
+ case ConnectionState.OFFLINE:
+ return <span className="connection-indicator offline">offline</span>;
+ }
+}
+
+export default connect(
+ state => state.connection,
+)(ConnectionIndicator)
diff --git a/web/src/js/components/Header/FileMenu.jsx b/web/src/js/components/Header/FileMenu.jsx
index ec32c857..1975d1cb 100644
--- a/web/src/js/components/Header/FileMenu.jsx
+++ b/web/src/js/components/Header/FileMenu.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import FileChooser from '../common/FileChooser'
import Dropdown, {Divider} from '../common/Dropdown'
diff --git a/web/src/js/components/Header/FilterInput.jsx b/web/src/js/components/Header/FilterInput.jsx
index 12479c10..44496d5b 100644
--- a/web/src/js/components/Header/FilterInput.jsx
+++ b/web/src/js/components/Header/FilterInput.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes, Component } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import classnames from 'classnames'
import { Key } from '../../utils.js'
diff --git a/web/src/js/components/Header/FlowMenu.jsx b/web/src/js/components/Header/FlowMenu.jsx
index a404fdb7..fb61baf1 100644
--- a/web/src/js/components/Header/FlowMenu.jsx
+++ b/web/src/js/components/Header/FlowMenu.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes } from "react"
+import React from "react"
+import PropTypes from 'prop-types'
import { connect } from "react-redux"
import Button from "../common/Button"
import { MessageUtils } from "../../flow/utils.js"
diff --git a/web/src/js/components/Header/MainMenu.jsx b/web/src/js/components/Header/MainMenu.jsx
index 6a4e12bf..465649d7 100644
--- a/web/src/js/components/Header/MainMenu.jsx
+++ b/web/src/js/components/Header/MainMenu.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react"
+import React, { Component } from "react"
+import PropTypes from 'prop-types'
import { connect } from "react-redux"
import FilterInput from "./FilterInput"
import { update as updateSettings } from "../../ducks/settings"
diff --git a/web/src/js/components/Header/MenuToggle.jsx b/web/src/js/components/Header/MenuToggle.jsx
index 91f093c6..220a2b79 100644
--- a/web/src/js/components/Header/MenuToggle.jsx
+++ b/web/src/js/components/Header/MenuToggle.jsx
@@ -1,4 +1,4 @@
-import { PropTypes } from "react"
+import PropTypes from 'prop-types'
import { connect } from "react-redux"
import { update as updateSettings } from "../../ducks/settings"
import { toggleVisibility } from "../../ducks/eventLog"
diff --git a/web/src/js/components/Header/OptionMenu.jsx b/web/src/js/components/Header/OptionMenu.jsx
index d6a8dfc2..b33d578d 100644
--- a/web/src/js/components/Header/OptionMenu.jsx
+++ b/web/src/js/components/Header/OptionMenu.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes } from "react"
+import React from "react"
+import PropTypes from 'prop-types'
import { connect } from "react-redux"
import { SettingsToggle, EventlogToggle } from "./MenuToggle"
import DocsLink from "../common/DocsLink"
diff --git a/web/src/js/components/MainView.jsx b/web/src/js/components/MainView.jsx
index 5c9a2d30..e2bedc88 100644
--- a/web/src/js/components/MainView.jsx
+++ b/web/src/js/components/MainView.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import Splitter from './common/Splitter'
import FlowTable from './FlowTable'
diff --git a/web/src/js/components/Prompt.jsx b/web/src/js/components/Prompt.jsx
index 1c20b1a9..77b07027 100755
--- a/web/src/js/components/Prompt.jsx
+++ b/web/src/js/components/Prompt.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes } from 'react'
+import React from 'react'
+import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import _ from 'lodash'
diff --git a/web/src/js/components/ProxyApp.jsx b/web/src/js/components/ProxyApp.jsx
index 18976de0..af5b3caa 100644
--- a/web/src/js/components/ProxyApp.jsx
+++ b/web/src/js/components/ProxyApp.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { onKeyDown } from '../ducks/ui/keyboard'
diff --git a/web/src/js/components/ValueEditor/ValidateEditor.jsx b/web/src/js/components/ValueEditor/ValidateEditor.jsx
index 7415c1b8..27b8ca48 100755
--- a/web/src/js/components/ValueEditor/ValidateEditor.jsx
+++ b/web/src/js/components/ValueEditor/ValidateEditor.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import ValueEditor from './ValueEditor'
import classnames from 'classnames'
diff --git a/web/src/js/components/ValueEditor/ValueEditor.jsx b/web/src/js/components/ValueEditor/ValueEditor.jsx
index 852f82c4..9301c181 100644
--- a/web/src/js/components/ValueEditor/ValueEditor.jsx
+++ b/web/src/js/components/ValueEditor/ValueEditor.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import _ from "lodash"
import classnames from 'classnames'
diff --git a/web/src/js/components/common/Button.jsx b/web/src/js/components/common/Button.jsx
index f05a68d0..e02ae010 100644
--- a/web/src/js/components/common/Button.jsx
+++ b/web/src/js/components/common/Button.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes } from "react"
+import React from "react"
+import PropTypes from 'prop-types'
import classnames from "classnames"
Button.propTypes = {
diff --git a/web/src/js/components/common/DocsLink.jsx b/web/src/js/components/common/DocsLink.jsx
index 182811a3..70974133 100644
--- a/web/src/js/components/common/DocsLink.jsx
+++ b/web/src/js/components/common/DocsLink.jsx
@@ -1,4 +1,5 @@
-import { PropTypes } from 'react'
+import React from "react"
+import PropTypes from "prop-types"
DocsLink.propTypes = {
resource: PropTypes.string.isRequired,
diff --git a/web/src/js/components/common/Dropdown.jsx b/web/src/js/components/common/Dropdown.jsx
index cc95a6dc..991e127e 100644
--- a/web/src/js/components/common/Dropdown.jsx
+++ b/web/src/js/components/common/Dropdown.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import classnames from 'classnames'
export const Divider = () => <hr className="divider"/>
diff --git a/web/src/js/components/common/FileChooser.jsx b/web/src/js/components/common/FileChooser.jsx
index d59d2d6d..0b14a87e 100644
--- a/web/src/js/components/common/FileChooser.jsx
+++ b/web/src/js/components/common/FileChooser.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes } from 'react'
+import React from 'react'
+import PropTypes from 'prop-types'
FileChooser.propTypes = {
icon: PropTypes.string,
@@ -20,7 +21,7 @@ export default function FileChooser({ icon, text, className, title, onOpenFile }
ref={ref => fileInput = ref}
className="hidden"
type="file"
- onChange={e => { e.preventDefault(); if(e.target.files.length > 0) onOpenFile(e.target.files[0]); fileInput = "";}}
+ onChange={e => { e.preventDefault(); if(e.target.files.length > 0) onOpenFile(e.target.files[0]); fileInput.value="";}}
/>
</a>
)
diff --git a/web/src/js/components/common/ToggleButton.jsx b/web/src/js/components/common/ToggleButton.jsx
index 6027728b..925d3c39 100644
--- a/web/src/js/components/common/ToggleButton.jsx
+++ b/web/src/js/components/common/ToggleButton.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes } from 'react'
+import React from 'react'
+import PropTypes from 'prop-types'
ToggleButton.propTypes = {
checked: PropTypes.bool.isRequired,
diff --git a/web/src/js/components/common/ToggleInputButton.jsx b/web/src/js/components/common/ToggleInputButton.jsx
index 5fa24c10..2607fb66 100644
--- a/web/src/js/components/common/ToggleInputButton.jsx
+++ b/web/src/js/components/common/ToggleInputButton.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import classnames from 'classnames'
import { Key } from '../../utils'
diff --git a/web/src/js/ducks/connection.js b/web/src/js/ducks/connection.js
new file mode 100644
index 00000000..ffa2c309
--- /dev/null
+++ b/web/src/js/ducks/connection.js
@@ -0,0 +1,44 @@
+export const ConnectionState = {
+ INIT: Symbol("init"),
+ FETCHING: Symbol("fetching"), // WebSocket is established, but still startFetching resources.
+ ESTABLISHED: Symbol("established"),
+ ERROR: Symbol("error"),
+ OFFLINE: Symbol("offline"), // indicates that there is no live (websocket) backend.
+}
+
+const defaultState = {
+ state: ConnectionState.INIT,
+ message: null,
+}
+
+export default function reducer(state = defaultState, action) {
+ switch (action.type) {
+
+ case ConnectionState.ESTABLISHED:
+ case ConnectionState.FETCHING:
+ case ConnectionState.ERROR:
+ case ConnectionState.OFFLINE:
+ return {
+ state: action.type,
+ message: action.message
+ }
+
+ default:
+ return state
+ }
+}
+
+export function startFetching() {
+ return { type: ConnectionState.FETCHING }
+}
+
+export function connectionEstablished() {
+ return { type: ConnectionState.ESTABLISHED }
+}
+
+export function connectionError(message) {
+ return { type: ConnectionState.ERROR, message }
+}
+export function setOffline() {
+ return { type: ConnectionState.OFFLINE }
+}
diff --git a/web/src/js/ducks/eventLog.js b/web/src/js/ducks/eventLog.js
index 73eaf2e8..8f9ec34d 100644
--- a/web/src/js/ducks/eventLog.js
+++ b/web/src/js/ducks/eventLog.js
@@ -8,7 +8,7 @@ export const TOGGLE_FILTER = 'EVENTS_TOGGLE_FILTER'
const defaultState = {
visible: false,
- filters: { debug: false, info: true, web: true },
+ filters: { debug: false, info: true, web: true, warn: true, error: true },
...reduceStore(undefined, {}),
}
diff --git a/web/src/js/ducks/flows.js b/web/src/js/ducks/flows.js
index 92408891..523ec396 100644
--- a/web/src/js/ducks/flows.js
+++ b/web/src/js/ducks/flows.js
@@ -1,5 +1,6 @@
import { fetchApi } from "../utils"
-import reduceStore, * as storeActions from "./utils/store"
+import reduceStore from "./utils/store"
+import * as storeActions from "./utils/store"
import Filt from "../filt/filt"
import { RequestUtils } from "../flow/utils"
@@ -29,8 +30,6 @@ export default function reduce(state = defaultState, action) {
case UPDATE:
case REMOVE:
case RECEIVE:
- // FIXME: Update state.selected on REMOVE:
- // The selected flow may have been removed, we need to select the next one in the view.
let storeAction = storeActions[action.cmd](
action.data,
makeFilter(state.filter),
@@ -152,22 +151,20 @@ export function setSort(column, desc) {
return { type: SET_SORT, sort: { column, desc } }
}
-export function selectRelative(shift) {
- return (dispatch, getState) => {
- let currentSelectionIndex = getState().flows.viewIndex[getState().flows.selected[0]]
- let minIndex = 0
- let maxIndex = getState().flows.view.length - 1
- let newIndex
- if (currentSelectionIndex === undefined) {
- newIndex = (shift < 0) ? minIndex : maxIndex
- } else {
- newIndex = currentSelectionIndex + shift
- newIndex = window.Math.max(newIndex, minIndex)
- newIndex = window.Math.min(newIndex, maxIndex)
- }
- let flow = getState().flows.view[newIndex]
- dispatch(select(flow ? flow.id : undefined))
+export function selectRelative(flows, shift) {
+ let currentSelectionIndex = flows.viewIndex[flows.selected[0]]
+ let minIndex = 0
+ let maxIndex = flows.view.length - 1
+ let newIndex
+ if (currentSelectionIndex === undefined) {
+ newIndex = (shift < 0) ? minIndex : maxIndex
+ } else {
+ newIndex = currentSelectionIndex + shift
+ newIndex = window.Math.max(newIndex, minIndex)
+ newIndex = window.Math.min(newIndex, maxIndex)
}
+ let flow = flows.view[newIndex]
+ return select(flow ? flow.id : undefined)
}
@@ -212,7 +209,7 @@ export function uploadContent(flow, file, type) {
const body = new FormData()
file = new window.Blob([file], { type: 'plain/text' })
body.append('file', file)
- return dispatch => fetchApi(`/flows/${flow.id}/${type}/content`, { method: 'post', body })
+ return dispatch => fetchApi(`/flows/${flow.id}/${type}/content`, { method: 'POST', body })
}
@@ -228,7 +225,7 @@ export function download() {
export function upload(file) {
const body = new FormData()
body.append('file', file)
- return dispatch => fetchApi('/flows/dump', { method: 'post', body })
+ return dispatch => fetchApi('/flows/dump', { method: 'POST', body })
}
diff --git a/web/src/js/ducks/index.js b/web/src/js/ducks/index.js
index 753075fa..0f2426ec 100644
--- a/web/src/js/ducks/index.js
+++ b/web/src/js/ducks/index.js
@@ -1,12 +1,14 @@
-import { combineReducers } from 'redux'
-import eventLog from './eventLog'
-import flows from './flows'
-import settings from './settings'
-import ui from './ui/index'
+import { combineReducers } from "redux"
+import eventLog from "./eventLog"
+import flows from "./flows"
+import settings from "./settings"
+import ui from "./ui/index"
+import connection from "./connection"
export default combineReducers({
eventLog,
flows,
settings,
+ connection,
ui,
})
diff --git a/web/src/js/ducks/ui/flow.js b/web/src/js/ducks/ui/flow.js
index ba604ea2..51ad4184 100644
--- a/web/src/js/ducks/ui/flow.js
+++ b/web/src/js/ducks/ui/flow.js
@@ -26,7 +26,7 @@ const defaultState = {
}
export default function reducer(state = defaultState, action) {
- let wasInEditMode = !!(state.modifiedFlow)
+ let wasInEditMode = state.modifiedFlow
let content = action.content || state.content
let isFullContentShown = content && content.length <= state.maxContentLines
@@ -89,14 +89,14 @@ export default function reducer(state = defaultState, action) {
...state,
tab: action.tab ? action.tab : 'request',
displayLarge: false,
- showFullContent: state.contentView == 'Edit'
+ showFullContent: state.contentView === 'Edit'
}
case SET_CONTENT_VIEW:
return {
...state,
contentView: action.contentView,
- showFullContent: action.contentView == 'Edit'
+ showFullContent: action.contentView === 'Edit'
}
case SET_CONTENT:
diff --git a/web/src/js/ducks/ui/header.js b/web/src/js/ducks/ui/header.js
index 6581149e..274d82aa 100644
--- a/web/src/js/ducks/ui/header.js
+++ b/web/src/js/ducks/ui/header.js
@@ -30,7 +30,7 @@ export default function reducer(state = defaultState, action) {
// Deselect
if (action.flowIds.length === 0 && state.isFlowSelected) {
let activeMenu = state.activeMenu
- if (activeMenu == 'Flow') {
+ if (activeMenu === 'Flow') {
activeMenu = 'Start'
}
return {
diff --git a/web/src/js/ducks/ui/keyboard.js b/web/src/js/ducks/ui/keyboard.js
index 30fd76e1..0e3491fa 100644
--- a/web/src/js/ducks/ui/keyboard.js
+++ b/web/src/js/ducks/ui/keyboard.js
@@ -9,39 +9,40 @@ export function onKeyDown(e) {
return () => {
}
}
- var key = e.keyCode
- var shiftKey = e.shiftKey
+ let key = e.keyCode,
+ shiftKey = e.shiftKey
e.preventDefault()
return (dispatch, getState) => {
- const flow = getState().flows.byId[getState().flows.selected[0]]
+ const flows = getState().flows,
+ flow = flows.byId[getState().flows.selected[0]]
switch (key) {
case Key.K:
case Key.UP:
- dispatch(flowsActions.selectRelative(-1))
+ dispatch(flowsActions.selectRelative(flows, -1))
break
case Key.J:
case Key.DOWN:
- dispatch(flowsActions.selectRelative(+1))
+ dispatch(flowsActions.selectRelative(flows, +1))
break
case Key.SPACE:
case Key.PAGE_DOWN:
- dispatch(flowsActions.selectRelative(+10))
+ dispatch(flowsActions.selectRelative(flows, +10))
break
case Key.PAGE_UP:
- dispatch(flowsActions.selectRelative(-10))
+ dispatch(flowsActions.selectRelative(flows, -10))
break
case Key.END:
- dispatch(flowsActions.selectRelative(+1e10))
+ dispatch(flowsActions.selectRelative(flows, +1e10))
break
case Key.HOME:
- dispatch(flowsActions.selectRelative(-1e10))
+ dispatch(flowsActions.selectRelative(flows, -1e10))
break
case Key.ESC:
diff --git a/web/src/js/filt/filt.js b/web/src/js/filt/filt.js
index 2252f957..26058649 100644
--- a/web/src/js/filt/filt.js
+++ b/web/src/js/filt/filt.js
@@ -1953,7 +1953,7 @@ module.exports = (function() {
function domain(regex){
regex = new RegExp(regex, "i");
function domainFilter(flow){
- return flow.request && regex.test(flow.request.host);
+ return flow.request && (regex.test(flow.request.host) || regex.test(flow.request.pretty_host));
}
domainFilter.desc = "domain matches " + regex;
return domainFilter;
diff --git a/web/src/js/urlState.js b/web/src/js/urlState.js
index ca9187b2..7802bdb8 100644
--- a/web/src/js/urlState.js
+++ b/web/src/js/urlState.js
@@ -15,7 +15,7 @@ const Query = {
SHOW_EVENTLOG: "e"
};
-function updateStoreFromUrl(store) {
+export function updateStoreFromUrl(store) {
const [path, query] = window.location.hash.substr(1).split("?", 2)
const path_components = path.substr(1).split("/")
@@ -50,7 +50,7 @@ function updateStoreFromUrl(store) {
}
}
-function updateUrlFromStore(store) {
+export function updateUrlFromStore(store) {
const state = store.getState()
let query = {
[Query.SEARCH]: state.flows.filter,
diff --git a/web/yarn.lock b/web/yarn.lock
index 6bdc7907..602b4916 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -1279,14 +1279,14 @@ content-type@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed"
-convert-source-map@1.X, convert-source-map@^1.2.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.4.0.tgz#e3dad195bf61bfe13a7a3c73e9876ec14a0268f3"
-
-convert-source-map@^1.1.0, convert-source-map@~1.1.0:
+convert-source-map@1.X, convert-source-map@^1.1.0, convert-source-map@~1.1.0:
version "1.1.3"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860"
+convert-source-map@^1.2.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.4.0.tgz#e3dad195bf61bfe13a7a3c73e9876ec14a0268f3"
+
core-js@^1.0.0:
version "1.2.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
@@ -1860,7 +1860,7 @@ fb-watchman@^2.0.0:
dependencies:
bser "^2.0.0"
-fbjs@^0.8.1, fbjs@^0.8.4:
+fbjs@^0.8.4, fbjs@^0.8.9:
version "0.8.9"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.9.tgz#180247fbd347dcc9004517b904f865400a0c8f14"
dependencies:
@@ -2461,14 +2461,10 @@ https-browserify@~0.0.0:
version "0.0.1"
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82"
-iconv-lite@0.4.13:
+iconv-lite@0.4.13, iconv-lite@~0.4.13:
version "0.4.13"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2"
-iconv-lite@~0.4.13:
- version "0.4.15"
- resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb"
-
ieee754@^1.1.4:
version "1.1.8"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
@@ -4068,6 +4064,12 @@ promise@^7.1.1:
dependencies:
asap "~2.0.3"
+prop-types@^15.5.0, prop-types@~15.5.7:
+ version "15.5.8"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.8.tgz#6b7b2e141083be38c8595aa51fc55775c7199394"
+ dependencies:
+ fbjs "^0.8.9"
+
prr@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a"
@@ -4154,12 +4156,13 @@ react-codemirror@^0.3.0:
lodash.debounce "^4.0.8"
react-dom@^15.4.2:
- version "15.4.2"
- resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.4.2.tgz#015363f05b0a1fd52ae9efdd3a0060d90695208f"
+ version "15.5.4"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.5.4.tgz#ba0c28786fd52ed7e4f2135fe0288d462aef93da"
dependencies:
- fbjs "^0.8.1"
+ fbjs "^0.8.9"
loose-envify "^1.1.0"
object-assign "^4.1.0"
+ prop-types "~15.5.7"
react-redux@^5.0.2:
version "5.0.2"
@@ -4171,6 +4174,13 @@ react-redux@^5.0.2:
lodash-es "^4.2.0"
loose-envify "^1.1.0"
+react-test-renderer@^15.5.4:
+ version "15.5.4"
+ resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-15.5.4.tgz#d4ebb23f613d685ea8f5390109c2d20fbf7c83bc"
+ dependencies:
+ fbjs "^0.8.9"
+ object-assign "^4.1.0"
+
react@^15.4.2:
version "15.4.2"
resolved "https://registry.yarnpkg.com/react/-/react-15.4.2.tgz#41f7991b26185392ba9bae96c8889e7e018397ef"
@@ -4289,6 +4299,10 @@ redux-logger@^2.8.1:
dependencies:
deep-diff "0.3.4"
+redux-mock-store@^1.2.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.2.3.tgz#1b3ad299da91cb41ba30d68e3b6f024475fb9e1b"
+
redux-thunk@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.2.0.tgz#e615a16e16b47a19a515766133d1e3e99b7852e5"