diff options
Diffstat (limited to 'web/src/js')
85 files changed, 2224 insertions, 137 deletions
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( + "<script>foo</script>" + ) + }) + + 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, |