web: convert everything to TypeScript

This commit is contained in:
Maximilian Hils 2021-08-23 11:01:43 +02:00
parent c5e3e3d636
commit 3589ec2f58
69 changed files with 679 additions and 730 deletions

View File

@ -1,18 +0,0 @@
import * as React from "react"
import renderer from 'react-test-renderer'
import FilterDocs from '../../../components/Header/FilterDocs'
describe('FilterDocs Component', () => {
let mockResponse = { json:
() => { return { commands: [['cmd1', 'foo'], ['cmd2', 'bar']]}}
},
promise = Promise.resolve(mockResponse)
global.fetch = r => { return promise }
let filterDocs = renderer.create(<FilterDocs/>),
tree = filterDocs.toJSON()
it('should render correctly when fetch success', () => {
expect(tree).toMatchSnapshot()
})
})

View File

@ -0,0 +1,20 @@
import * as React from "react"
import FilterDocs from '../../../components/Header/FilterDocs'
import {enableFetchMocks} from "jest-fetch-mock";
import {render, screen, waitFor} from "../../test-utils";
enableFetchMocks();
test("FilterDocs Component", async () => {
fetchMock.mockOnceIf("./filter-help", JSON.stringify({
commands: [['cmd1', 'foo'], ['cmd2', 'bar']]
}))
const {asFragment} = render(<FilterDocs selectHandler={() => 0}/>);
expect(asFragment()).toMatchSnapshot();
await waitFor(() => screen.getByText("cmd1"));
expect(asFragment()).toMatchSnapshot();
})

View File

@ -4,11 +4,12 @@ import FilterInput from '../../../components/Header/FilterInput'
import FilterDocs from '../../../components/Header/FilterDocs' import FilterDocs from '../../../components/Header/FilterDocs'
import TestUtil from 'react-dom/test-utils' import TestUtil from 'react-dom/test-utils'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import { Key } from '../../../utils'
describe('FilterInput Component', () => { describe('FilterInput Component', () => {
it('should render correctly', () => { it('should render correctly', () => {
let filterInput = renderer.create(<FilterInput type='foo' color='red' placeholder='bar'/>), let filterInput = renderer.create(
<FilterInput type='foo' color='red' placeholder='bar' onChange={() => undefined} value="42"/>
),
tree = filterInput.toJSON() tree = filterInput.toJSON()
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
}) })
@ -69,7 +70,7 @@ describe('FilterInput Component', () => {
it('should handle keyDown', () => { it('should handle keyDown', () => {
input.blur = jest.fn() input.blur = jest.fn()
let mockEvent = { let mockEvent = {
keyCode: Key.ESC, key: "Escape",
stopPropagation: jest.fn() stopPropagation: jest.fn()
} }
filterInput.onKeyDown(mockEvent) filterInput.onKeyDown(mockEvent)

View File

@ -1,7 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FilterDocs Component should render correctly when fetch success 1`] = `
<i
className="fa fa-spinner fa-spin"
/>
`;

View File

@ -0,0 +1,51 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FilterDocs Component 1`] = `
<DocumentFragment>
<i
class="fa fa-spinner fa-spin"
/>
</DocumentFragment>
`;
exports[`FilterDocs Component 2`] = `
<DocumentFragment>
<table
class="table table-condensed"
>
<tbody>
<tr>
<td>
cmd1
</td>
<td>
foo
</td>
</tr>
<tr>
<td>
cmd2
</td>
<td>
bar
</td>
</tr>
<tr>
<td
colspan="2"
>
<a
href="https://mitmproxy.org/docs/latest/concepts-filters/"
target="_blank"
>
<i
class="fa fa-external-link"
/>
  mitmproxy docs
</a>
</td>
</tr>
</tbody>
</table>
</DocumentFragment>
`;

View File

@ -24,6 +24,7 @@ exports[`FilterInput Component should render correctly 1`] = `
onKeyDown={[Function]} onKeyDown={[Function]}
placeholder="bar" placeholder="bar"
type="text" type="text"
value="42"
/> />
</div> </div>
`; `;

View File

@ -16,7 +16,7 @@ describe('Button Component', () => {
it('should be able to be disabled', () => { it('should be able to be disabled', () => {
let button = renderer.create( let button = renderer.create(
<Button className="classname" onClick={() => "onclick"} disabled="true" children="children"> <Button className="classname" onClick={() => "onclick"} disabled>
<a>foo</a> <a>foo</a>
</Button> </Button>
), ),

View File

@ -7,12 +7,12 @@ import TestUtils from 'react-dom/test-utils';
describe('Splitter Component', () => { describe('Splitter Component', () => {
it('should render correctly', () => { it('should render correctly', () => {
let splitter = renderer.create(<Splitter></Splitter>), let splitter = renderer.create(<Splitter/>),
tree = splitter.toJSON() tree = splitter.toJSON()
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
}) })
let splitter = TestUtils.renderIntoDocument(<Splitter></Splitter>), let splitter = TestUtils.renderIntoDocument(<Splitter/>),
dom = ReactDOM.findDOMNode(splitter), dom = ReactDOM.findDOMNode(splitter),
previousElementSibling = { previousElementSibling = {
offsetHeight: 0, offsetHeight: 0,
@ -56,15 +56,15 @@ describe('Splitter Component', () => {
splitter.onMouseMove({pageX: 10, pageY: 10}) splitter.onMouseMove({pageX: 10, pageY: 10})
expect(dom.style.transform).toEqual("translate(9px, 0px)") expect(dom.style.transform).toEqual("translate(9px, 0px)")
let splitterY = TestUtils.renderIntoDocument(<Splitter axis="y"></Splitter>) let splitterY = TestUtils.renderIntoDocument(<Splitter axis="y"/>)
splitterY.onMouseMove({pageX: 10, pageY: 10}) splitterY.onMouseMove({pageX: 10, pageY: 10})
expect(ReactDOM.findDOMNode(splitterY).style.transform).toEqual("translate(0px, 10px)") expect(ReactDOM.findDOMNode(splitterY).style.transform).toEqual("translate(0px, 10px)")
}) })
it('should handle resize', () => { it('should handle resize', () => {
window.setTimeout = jest.fn((event, time) => event()) let x = jest.spyOn(window, 'setTimeout');
splitter.onResize() splitter.onResize()
expect(window.setTimeout).toHaveBeenCalled() expect(x).toHaveBeenCalled()
}) })
it('should handle componentWillUnmount', () => { it('should handle componentWillUnmount', () => {

View File

@ -7,18 +7,14 @@ describe('ToggleButton Component', () => {
it('should render correctly', () => { it('should render correctly', () => {
let checkedButton = renderer.create( let checkedButton = renderer.create(
<ToggleButton checked={true} onToggle={mockFunc} text="foo"> <ToggleButton checked={true} onToggle={mockFunc} text="foo"/>),
text
</ToggleButton>),
tree = checkedButton.toJSON() tree = checkedButton.toJSON()
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
}) })
it('should handle click action', () => { it('should handle click action', () => {
let uncheckButton = renderer.create( let uncheckButton = renderer.create(
<ToggleButton checked={false} onToggle={mockFunc} text="foo"> <ToggleButton checked={false} onToggle={mockFunc} text="foo"/>),
text
</ToggleButton>),
tree = uncheckButton.toJSON() tree = uncheckButton.toJSON()
tree.props.onClick() tree.props.onClick()
expect(mockFunc).toBeCalled() expect(mockFunc).toBeCalled()

View File

@ -1,43 +0,0 @@
import * as 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")
})
})

View File

@ -3,7 +3,7 @@
exports[`Button Component should be able to be disabled 1`] = ` exports[`Button Component should be able to be disabled 1`] = `
<button <button
className="classname btn btn-default" className="classname btn btn-default"
disabled="true" disabled={true}
> >
<a> <a>
foo foo

View File

@ -1,31 +0,0 @@
// 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>
`;

View File

@ -14,7 +14,7 @@ describe('VirtualScroll', () => {
it('should calculate position with itemHeights', () => { it('should calculate position with itemHeights', () => {
expect(calcVScroll({itemCount: 5, itemHeights: [100, 100, 100, 100, 100], expect(calcVScroll({itemCount: 5, itemHeights: [100, 100, 100, 100, 100],
viewportHeight: 300, viewportTop: 0})).toEqual({ viewportHeight: 300, viewportTop: 0, rowHeight: 100})).toEqual({
start: 0, end: 4, paddingTop: 0, paddingBottom: 100 start: 0, end: 4, paddingTop: 0, paddingBottom: 100
}) })
}) })

View File

@ -4,7 +4,7 @@ import { ConnectionState } from "../../ducks/connection"
describe('connection reducer', () => { describe('connection reducer', () => {
it('should return initial state', () => { it('should return initial state', () => {
expect(reduceConnection(undefined, {})).toEqual({ expect(reduceConnection(undefined, {type: "other"})).toEqual({
state: ConnectionState.INIT, state: ConnectionState.INIT,
message: undefined, message: undefined,
}) })

View File

@ -2,7 +2,7 @@ import {rootReducer} from '../../ducks/index'
describe('reduceState in js/ducks/index.js', () => { describe('reduceState in js/ducks/index.js', () => {
it('should combine flow and header', () => { it('should combine flow and header', () => {
let state = rootReducer(undefined, {}) let state = rootReducer(undefined, {type: "other"})
expect(state.hasOwnProperty('eventLog')).toBeTruthy() expect(state.hasOwnProperty('eventLog')).toBeTruthy()
expect(state.hasOwnProperty('flows')).toBeTruthy() expect(state.hasOwnProperty('flows')).toBeTruthy()
expect(state.hasOwnProperty('connection')).toBeTruthy() expect(state.hasOwnProperty('connection')).toBeTruthy()

View File

@ -2,7 +2,7 @@ import reduceUI from '../../../ducks/ui/index'
describe('reduceUI in js/ducks/ui/index.js', () => { describe('reduceUI in js/ducks/ui/index.js', () => {
it('should combine flow and header', () => { it('should combine flow and header', () => {
let state = reduceUI(undefined, {}) let state = reduceUI(undefined, {type: "other"})
expect(state.hasOwnProperty('flow')).toBeTruthy() expect(state.hasOwnProperty('flow')).toBeTruthy()
}) })
}) })

View File

@ -1,164 +0,0 @@
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 * as modalActions from '../../../ducks/ui/modal'
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: 'timing', 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()
})
it('should close modal', () => {
store.getState().ui.modal.activeModal = true
store.dispatch(createKeyEvent(Key.ESC))
expect(store.getActions()).toEqual([ {type: modalActions.HIDE_MODAL} ])
})
})

View File

@ -0,0 +1,166 @@
import reduceFlows, * as flowsActions from "../../../ducks/flows";
import {onKeyDown} from '../../../ducks/ui/keyboard'
import * as UIActions from '../../../ducks/ui/flow'
import * as modalActions from '../../../ducks/ui/modal'
import {fetchApi} from '../../../utils'
import {TStore} from "../tutils";
jest.mock('../../../utils')
describe('onKeyDown', () => {
let flows = flowsActions.defaultState;
for (let i = 1; i <= 12; i++) {
flows = reduceFlows(flows, {
type: flowsActions.ADD,
data: {id: i + "", request: true, response: true},
cmd: 'add'
})
}
const store = TStore();
store.getState().flows = flows;
let createKeyEvent = (key, ctrlKey = false) => {
// @ts-ignore
return onKeyDown({key, ctrlKey, preventDefault: jest.fn()})
}
afterEach(() => {
store.clearActions()
// @ts-ignore
fetchApi.mockClear()
});
it('should handle cursor up', () => {
store.getState().flows = reduceFlows(flows, flowsActions.select("2"))
store.dispatch(createKeyEvent("k"))
expect(store.getActions()).toEqual([{flowIds: ["1"], type: flowsActions.SELECT}])
store.clearActions()
store.dispatch(createKeyEvent("ArrowUp"))
expect(store.getActions()).toEqual([{flowIds: ["1"], type: flowsActions.SELECT}])
})
it('should handle cursor down', () => {
store.dispatch(createKeyEvent("j"))
expect(store.getActions()).toEqual([{flowIds: ["3"], type: flowsActions.SELECT}])
store.clearActions()
store.dispatch(createKeyEvent("ArrowDown"))
expect(store.getActions()).toEqual([{flowIds: ["3"], type: flowsActions.SELECT}])
})
it('should handle page down', () => {
store.dispatch(createKeyEvent(" "))
expect(store.getActions()).toEqual([{flowIds: ["12"], type: flowsActions.SELECT}])
store.getState().flows = reduceFlows(flows, flowsActions.select("1"))
store.clearActions()
store.dispatch(createKeyEvent("PageDown"))
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("PageUp"))
expect(store.getActions()).toEqual([{flowIds: ["1"], type: flowsActions.SELECT}])
})
it('should handle select first', () => {
store.dispatch(createKeyEvent("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("End"))
expect(store.getActions()).toEqual([{flowIds: ["12"], type: flowsActions.SELECT}])
})
it('should handle deselect', () => {
store.dispatch(createKeyEvent("Escape"))
expect(store.getActions()).toEqual([{flowIds: [], type: flowsActions.SELECT}])
})
it('should handle switch to left tab', () => {
store.dispatch(createKeyEvent("ArrowLeft"))
expect(store.getActions()).toEqual([{tab: 'timing', type: UIActions.SET_TAB}])
})
it('should handle switch to right tab', () => {
store.dispatch(createKeyEvent("Tab"))
expect(store.getActions()).toEqual([{tab: 'response', type: UIActions.SET_TAB}])
store.clearActions()
store.dispatch(createKeyEvent("ArrowRight"))
expect(store.getActions()).toEqual([{tab: 'response', type: UIActions.SET_TAB}])
})
it('should handle delete action', () => {
store.dispatch(createKeyEvent("d"))
expect(fetchApi).toBeCalledWith('/flows/1', {method: 'DELETE'})
})
it('should handle duplicate action', () => {
store.dispatch(createKeyEvent("D"))
expect(fetchApi).toBeCalledWith('/flows/1/duplicate', {method: 'POST'})
})
it('should handle resume action', () => {
// resume all
store.dispatch(createKeyEvent("A"))
expect(fetchApi).toBeCalledWith('/flows/resume', {method: 'POST'})
// resume
store.getState().flows.byId[store.getState().flows.selected[0]].intercepted = true
store.dispatch(createKeyEvent("a"))
expect(fetchApi).toBeCalledWith('/flows/1/resume', {method: 'POST'})
})
it('should handle replay action', () => {
store.dispatch(createKeyEvent("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("v"))
expect(fetchApi).toBeCalledWith('/flows/1/revert', {method: 'POST'})
})
it('should handle kill action', () => {
// kill all
store.dispatch(createKeyEvent("X"))
expect(fetchApi).toBeCalledWith('/flows/kill', {method: 'POST'})
// kill
store.dispatch(createKeyEvent("x"))
expect(fetchApi).toBeCalledWith('/flows/1/kill', {method: 'POST'})
})
it('should handle clear action', () => {
store.dispatch(createKeyEvent("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("ArrowLeft"))
store.dispatch(createKeyEvent("Tab"))
store.dispatch(createKeyEvent("ArrowRight"))
store.dispatch(createKeyEvent("D"))
expect(fetchApi).not.toBeCalled()
})
it('should do nothing when Ctrl and undefined key is pressed ', () => {
store.dispatch(createKeyEvent("Backspace", true))
store.dispatch(createKeyEvent(0))
expect(fetchApi).not.toBeCalled()
})
it('should close modal', () => {
store.getState().ui.modal.activeModal = true
store.dispatch(createKeyEvent("Escape"))
expect(store.getActions()).toEqual([{type: modalActions.HIDE_MODAL}])
})
})

View File

@ -1,7 +1,6 @@
import reduceModal, * as ModalActions from '../../../ducks/ui/modal' import reduceModal, * as ModalActions from '../../../ducks/ui/modal'
describe('modal reducer', () => { describe('modal reducer', () => {
let state = undefined
it('should return the initial state', () => { it('should return the initial state', () => {
expect(reduceModal(undefined, {})).toEqual( expect(reduceModal(undefined, {})).toEqual(
@ -10,14 +9,14 @@ describe('modal reducer', () => {
}) })
it('should handle setActiveModal action', () => { it('should handle setActiveModal action', () => {
state = reduceModal(undefined, ModalActions.setActiveModal('foo')) let state = reduceModal(undefined, ModalActions.setActiveModal('foo'))
expect(state).toEqual( expect(state).toEqual(
{ activeModal: 'foo' } { activeModal: 'foo' }
) )
}) })
it('should handle hideModal action', () => { it('should handle hideModal action', () => {
state = reduceModal(state, ModalActions.hideModal()) let state = reduceModal(undefined, ModalActions.hideModal())
expect(state).toEqual( expect(state).toEqual(
{ activeModal: undefined } { activeModal: undefined }
) )

View File

@ -1,5 +1,6 @@
import reduceOptionsEditor, * as optionsEditorActions from '../../../ducks/ui/optionsEditor' import reduceOptionsEditor, * as optionsEditorActions from '../../../ducks/ui/optionsEditor'
import { HIDE_MODAL } from '../../../ducks/ui/modal' import { HIDE_MODAL } from '../../../ducks/ui/modal'
import {OptionsState} from "../../../ducks/_options_gen";
describe('optionsEditor reducer', () => { describe('optionsEditor reducer', () => {
@ -7,17 +8,17 @@ describe('optionsEditor reducer', () => {
expect(reduceOptionsEditor(undefined, {})).toEqual({}) expect(reduceOptionsEditor(undefined, {})).toEqual({})
}) })
let state = undefined
it('should handle option update start', () => { it('should handle option update start', () => {
state = reduceOptionsEditor(undefined, optionsEditorActions.startUpdate('foo', 'bar')) let state = reduceOptionsEditor(undefined, optionsEditorActions.startUpdate('foo', 'bar'))
expect(state).toEqual({ foo: {error: false, isUpdating: true, value: 'bar'}}) expect(state).toEqual({ foo: {error: false, isUpdating: true, value: 'bar'}})
}) })
it('should handle option update success', () => { it('should handle option update success', () => {
expect(reduceOptionsEditor(state, optionsEditorActions.updateSuccess('foo'))).toEqual({foo: undefined}) expect(reduceOptionsEditor(undefined, optionsEditorActions.updateSuccess('foo'))).toEqual({foo: undefined})
}) })
it('should handle option update error', () => { it('should handle option update error', () => {
let state = reduceOptionsEditor(undefined, optionsEditorActions.startUpdate('foo', 'bar'))
state = reduceOptionsEditor(state, optionsEditorActions.updateError('foo', 'errorMsg')) state = reduceOptionsEditor(state, optionsEditorActions.updateError('foo', 'errorMsg'))
expect(state).toEqual({ foo: {error: 'errorMsg', isUpdating: false, value: 'bar'}}) expect(state).toEqual({ foo: {error: 'errorMsg', isUpdating: false, value: 'bar'}})
// boolean type // boolean type

View File

@ -1,170 +0,0 @@
import {reduce} from '../../../ducks/utils/store'
import * as storeActions from '../../../ducks/utils/store'
describe('store reducer', () => {
it('should return initial state', () => {
expect(reduce(undefined, {})).toEqual({
byId: {},
list: [],
listIndex: {},
view: [],
viewIndex: {},
})
})
it('should handle add action', () => {
let a = {id: 1},
b = {id: 9},
state = reduce(undefined, {})
expect(state = reduce(state, storeActions.add(a))).toEqual({
byId: { 1: a },
listIndex: { 1: 0 },
list: [ a ],
view: [ a ],
viewIndex: { 1: 0 },
})
expect(state = reduce(state, storeActions.add(b))).toEqual({
byId: { 1: a, 9: b },
listIndex: { 1: 0, 9: 1 },
list: [ a, b ],
view: [ a, b ],
viewIndex: { 1: 0, 9: 1 },
})
// add item and sort them
let c = {id: 0}
expect(reduce(state, storeActions.add(c, undefined,
(a, b) => {return a.id - b.id}))).toEqual({
byId: {...state.byId, 0: c },
list: [...state.list, c ],
listIndex: {...state.listIndex, 0:2},
view: [c, ...state.view ],
viewIndex: {0: 0, 1: 1, 9: 2}
})
})
it('should not add the item with duplicated id', () => {
let a = {id: 1},
state = reduce(undefined, storeActions.add(a))
expect(reduce(state, storeActions.add(a))).toEqual(state)
})
it('should handle update action', () => {
let a = {id: 1, foo: "foo"},
updated = {...a, foo: "bar"},
state = reduce(undefined, storeActions.add(a))
expect(reduce(state, storeActions.update(updated))).toEqual({
byId: { 1: updated },
list: [ updated ],
listIndex: { 1: 0 },
view: [ updated ],
viewIndex: { 1: 0 },
})
})
it('should handle update action with filter', () => {
let a = {id: 0}, b = {id: 1},
state = reduce(undefined, storeActions.receive([a, b]))
state = reduce(state, storeActions.update(b,
item => {return item.id != 1}))
expect(state).toEqual({
byId: { 0: a, 1: b },
list: [ a, b ],
listIndex: { 0: 0, 1: 1 },
view: [ a ],
viewIndex: { 0: 0 }
})
expect(reduce(state, storeActions.update(b,
item => {return item.id != 0}))).toEqual({
byId: { 0: a, 1: b },
list: [ a, b ],
listIndex: { 0: 0, 1: 1 },
view: [ a, b ],
viewIndex: { 0: 0, 1: 1 }
})
})
it('should handle update action with sort', () => {
let a = {id: 2},
b = {id: 3},
state = reduce(undefined, storeActions.receive([a, b]))
expect(reduce(state, storeActions.update(b, undefined,
(a, b) => {return b.id - a.id}))).toEqual({
// sort by id in descending order
byId: { 2: a, 3: b },
list: [ a, b ],
listIndex: {2: 0, 3: 1},
view: [ b, a ],
viewIndex: { 2: 1, 3: 0 },
})
let state1 = reduce(undefined, storeActions.receive([b, a]))
expect(reduce(state1, storeActions.update(b, undefined,
(a, b) => {return a.id - b.id}))).toEqual({
// sort by id in ascending order
byId: { 2: a, 3: b },
list: [ b, a ],
listIndex: {2: 1, 3: 0},
view: [ a, b ],
viewIndex: { 2: 0, 3: 1 },
})
})
it('should set filter', () => {
let a = { id: 1 },
b = { id: 2 },
state = reduce(undefined, storeActions.receive([a, b]))
expect(reduce(state, storeActions.setFilter(
item => {return item.id != 1}
))).toEqual({
byId: { 1 :a, 2: b },
list: [ a, b ],
listIndex:{ 1: 0, 2: 1},
view: [ b ],
viewIndex: { 2: 0 },
})
})
it('should set sort', () => {
let a = { id: 1 },
b = { id: 2 },
state = reduce(undefined, storeActions.receive([a, b]))
expect(reduce(state, storeActions.setSort(
(a, b) => { return b.id - a.id }
))).toEqual({
byId: { 1: a, 2: b },
list: [ a, b ],
listIndex: {1: 0, 2: 1},
view: [ b, a ],
viewIndex: { 1: 1, 2: 0 },
})
})
it('should handle remove action', () => {
let a = { id: 1 }, b = { id: 2},
state = reduce(undefined, storeActions.receive([a, b]))
expect(reduce(state, storeActions.remove(1))).toEqual({
byId: { 2: b },
list: [ b ],
listIndex: { 2: 0 },
view: [ b ],
viewIndex: { 2: 0 },
})
expect(reduce(state, storeActions.remove(3))).toEqual(state)
})
it('should handle receive list', () => {
let a = { id: 1 }, b = { id: 2 },
list = [ a, b ]
expect(reduce(undefined, storeActions.receive(list))).toEqual({
byId: { 1: a, 2: b },
list: [ a, b ],
listIndex: {1: 0, 2: 1},
view: [ a, b ],
viewIndex: { 1: 0, 2: 1 },
})
})
})

View File

@ -0,0 +1,188 @@
import * as storeActions from '../../../ducks/utils/store'
import {Item, reduce} from '../../../ducks/utils/store'
describe('store reducer', () => {
it('should return initial state', () => {
expect(reduce(undefined, {})).toEqual({
byId: {},
list: [],
listIndex: {},
view: [],
viewIndex: {},
})
})
it('should handle add action', () => {
let a = {id: "1"},
b = {id: "9"},
state = reduce(undefined, {})
expect(state = reduce(state, storeActions.add(a))).toEqual({
byId: {"1": a},
listIndex: {"1": 0},
list: [a],
view: [a],
viewIndex: {"1": 0},
})
expect(state = reduce(state, storeActions.add(b))).toEqual({
byId: {"1": a, 9: b},
listIndex: {"1": 0, "9": 1},
list: [a, b],
view: [a, b],
viewIndex: {"1": 0, "9": 1},
})
// add item and sort them
let c = {id: "0"}
expect(reduce(state, storeActions.add(c, undefined,
(a, b) => {
return a.id > b.id ? 1 : -1
}))).toEqual({
byId: {...state.byId, "0": c},
list: [...state.list, c],
listIndex: {...state.listIndex, "0": 2},
view: [c, ...state.view],
viewIndex: {"0": 0, "1": 1, "9": 2}
})
})
it('should not add the item with duplicated id', () => {
let a = {id: "1"},
state = reduce(undefined, storeActions.add(a))
expect(reduce(state, storeActions.add(a))).toEqual(state)
})
it('should handle update action', () => {
interface TItem extends Item {
foo: string
}
let a: TItem = {id: "1", foo: "foo"},
updated = {...a, foo: "bar"},
state = reduce(undefined, storeActions.add(a))
expect(reduce(state, storeActions.update(updated))).toEqual({
byId: {1: updated},
list: [updated],
listIndex: {1: 0},
view: [updated],
viewIndex: {1: 0},
})
})
it('should handle update action with filter', () => {
let a = {id: "0"}, b = {id: "1"},
state = reduce(undefined, storeActions.receive([a, b]))
state = reduce(state, storeActions.update(b,
item => {
return item.id !== "1"
}))
expect(state).toEqual({
byId: {"0": a, "1": b},
list: [a, b],
listIndex: {"0": 0, "1": 1},
view: [a],
viewIndex: {"0": 0}
})
expect(reduce(state, storeActions.update(b,
item => {
return item.id !== "0"
}))).toEqual({
byId: {"0": a, "1": b},
list: [a, b],
listIndex: {"0": 0, "1": 1},
view: [a, b],
viewIndex: {"0": 0, "1": 1}
})
})
it('should handle update action with sort', () => {
let a = {id: "2"},
b = {id: "3"},
state = reduce(undefined, storeActions.receive([a, b]))
expect(reduce(state, storeActions.update(b, undefined,
(a, b) => {
return b.id > a.id ? 1 : -1
}))).toEqual({
// sort by id in descending order
byId: {"2": a, "3": b},
list: [a, b],
listIndex: {"2": 0, "3": 1},
view: [b, a],
viewIndex: {"2": 1, "3": 0},
})
let state1 = reduce(undefined, storeActions.receive([b, a]))
expect(reduce(state1, storeActions.update(b, undefined,
(a, b) => {
return a.id > b.id ? 1 : -1
}))).toEqual({
// sort by id in ascending order
byId: {"2": a, "3": b},
list: [b, a],
listIndex: {"2": 1, "3": 0},
view: [a, b],
viewIndex: {"2": 0, "3": 1},
})
})
it('should set filter', () => {
let a = {id: "1"},
b = {id: "2"},
state = reduce(undefined, storeActions.receive([a, b]))
expect(reduce(state, storeActions.setFilter(
item => {
return item.id !== "1"
}
))).toEqual({
byId: {"1": a, "2": b},
list: [a, b],
listIndex: {"1": 0, "2": 1},
view: [b],
viewIndex: {"2": 0},
})
})
it('should set sort', () => {
let a = {id: "1"},
b = {id: "2"},
state = reduce(undefined, storeActions.receive([a, b]))
expect(reduce(state, storeActions.setSort(
(a, b) => {
return b.id > a.id ? 1 : -1
}
))).toEqual({
byId: {1: a, 2: b},
list: [a, b],
listIndex: {1: 0, 2: 1},
view: [b, a],
viewIndex: {1: 1, 2: 0},
})
})
it('should handle remove action', () => {
let a = {id: "1"}, b = {id: "2"},
state = reduce(undefined, storeActions.receive([a, b]))
expect(reduce(state, storeActions.remove("1"))).toEqual({
byId: {"2": b},
list: [b],
listIndex: {"2": 0},
view: [b],
viewIndex: {"2": 0},
})
expect(reduce(state, storeActions.remove("3"))).toEqual(state)
})
it('should handle receive list', () => {
let a = {id: "1"}, b = {id: "2"},
list = [a, b]
expect(reduce(undefined, storeActions.receive(list))).toEqual({
byId: {"1": a, "2": b},
list: [a, b],
listIndex: {"1": 0, "2": 1},
view: [a, b],
viewIndex: {"1": 0, "2": 1},
})
})
})

View File

@ -1,16 +1,17 @@
import * as utils from '../../flow/utils' import * as utils from '../../flow/utils'
import {TFlow} from "../ducks/tutils";
describe('MessageUtils', () => { describe('MessageUtils', () => {
it('should be possible to get first header', () => { it('should be possible to get first header', () => {
let msg = { headers: [["foo", "bar"]]} let tflow = TFlow();
expect(utils.MessageUtils.get_first_header(msg, "foo")).toEqual("bar") expect(utils.MessageUtils.get_first_header(tflow.request, /header/)).toEqual("qvalue")
expect(utils.MessageUtils.get_first_header(msg, "123")).toEqual(undefined) expect(utils.MessageUtils.get_first_header(tflow.request, /123/)).toEqual(undefined)
}) })
it('should be possible to get Content-Type', () => { it('should be possible to get Content-Type', () => {
let type = "text/html", let tflow = TFlow();
msg = { headers: [["Content-Type", type]]} tflow.request.headers = [["Content-Type", "text/html"]];
expect(utils.MessageUtils.getContentType(msg)).toEqual(type) expect(utils.MessageUtils.getContentType(tflow.request)).toEqual("text/html");
}) })
it('should be possible to match header', () => { it('should be possible to match header', () => {
@ -21,28 +22,27 @@ describe('MessageUtils', () => {
}) })
it('should be possible to get content URL', () => { it('should be possible to get content URL', () => {
const flow = TFlow();
// request // request
let msg = "foo", view = "bar", let view = "bar";
flow = { request: msg, id: 1} expect(utils.MessageUtils.getContentURL(flow, flow.request, view)).toEqual(
expect(utils.MessageUtils.getContentURL(flow, msg, view)).toEqual( "./flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/request/content/bar.json"
"./flows/1/request/content/bar.json"
) )
expect(utils.MessageUtils.getContentURL(flow, msg, '')).toEqual( expect(utils.MessageUtils.getContentURL(flow, flow.request, '')).toEqual(
"./flows/1/request/content.data" "./flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/request/content.data"
) )
// response // response
flow = {response: msg, id: 2} expect(utils.MessageUtils.getContentURL(flow, flow.response, view)).toEqual(
expect(utils.MessageUtils.getContentURL(flow, msg, view)).toEqual( "./flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/response/content/bar.json"
"./flows/2/response/content/bar.json"
) )
}) })
}) })
describe('RequestUtils', () => { describe('RequestUtils', () => {
it('should be possible prettify url', () => { it('should be possible prettify url', () => {
let request = {port: 4444, scheme: "http", pretty_host: "foo", path: "/bar"} let flow = TFlow();
expect(utils.RequestUtils.pretty_url(request)).toEqual( expect(utils.RequestUtils.pretty_url(flow.request)).toEqual(
"http://foo:4444/bar" "http://address:22/path"
) )
}) })
}) })

View File

@ -63,10 +63,10 @@ describe('updateStoreFromUrl', () => {
describe('updateUrlFromStore', () => { describe('updateUrlFromStore', () => {
let initialState = { let initialState = {
flows: reduceFlows(undefined, {}), flows: reduceFlows(undefined, {type: "other"}),
ui: reduceUI(undefined, {}), ui: reduceUI(undefined, {type: "other"}),
eventLog: reduceEventLog(undefined, {}), eventLog: reduceEventLog(undefined, {type: "other"}),
commandBar: reduceCommandBar(undefined, {}), commandBar: reduceCommandBar(undefined, {type: "other"}),
} }
it('should update initial url', () => { it('should update initial url', () => {
@ -76,7 +76,7 @@ describe('updateUrlFromStore', () => {
}) })
it('should update url', () => { it('should update url', () => {
let flows = reduceFlows(undefined, flowsActions.select(123)), let flows = reduceFlows(undefined, flowsActions.select("123")),
state = { state = {
...initialState, ...initialState,
flows: reduceFlows(flows, flowsActions.setFilter('~u foo')) flows: reduceFlows(flows, flowsActions.setFilter('~u foo'))
@ -89,10 +89,10 @@ describe('updateUrlFromStore', () => {
describe('initialize', () => { describe('initialize', () => {
let initialState = { let initialState = {
flows: reduceFlows(undefined, {}), flows: reduceFlows(undefined, {type: "other"}),
ui: reduceUI(undefined, {}), ui: reduceUI(undefined, {type: "other"}),
eventLog: reduceEventLog(undefined, {}), eventLog: reduceEventLog(undefined, {type: "other"}),
commandBar: reduceCommandBar(undefined, {}), commandBar: reduceCommandBar(undefined, {type: "other"}),
} }
it('should handle initial state', () => { it('should handle initial state', () => {

View File

@ -1,6 +1,7 @@
import * as utils from '../utils' import * as utils from '../utils'
import {enableFetchMocks} from "jest-fetch-mock";
global.fetch = jest.fn() enableFetchMocks();
describe('formatSize', () => { describe('formatSize', () => {
it('should return 0 when 0 byte', () => { it('should return 0 when 0 byte', () => {
@ -19,9 +20,10 @@ describe('formatTimeDelta', () => {
}) })
}) })
describe('formatTimeSTamp', () => { describe('formatTimeStamp', () => {
it('should return formatted time', () => { it('should return formatted time', () => {
expect(utils.formatTimeStamp(1483228800, false)).toEqual("2017-01-01 00:00:00.000") expect(utils.formatTimeStamp(1483228800, {milliseconds: false})).toEqual("2017-01-01 00:00:00")
expect(utils.formatTimeStamp(1483228800, {milliseconds: true})).toEqual("2017-01-01 00:00:00.000")
}) })
}) })
@ -35,22 +37,22 @@ describe('reverseString', () => {
describe('fetchApi', () => { describe('fetchApi', () => {
it('should handle fetch operation', () => { it('should handle fetch operation', () => {
utils.fetchApi('http://foo/bar', {method: "POST"}) utils.fetchApi('http://foo/bar', {method: "POST"})
expect(fetch.mock.calls[0][0]).toEqual( expect(fetchMock.mock.calls[0][0]).toEqual(
"http://foo/bar" "http://foo/bar"
) )
fetch.mockClear() fetchMock.mockClear()
utils.fetchApi('http://foo?bar=1', {method: "POST"}) utils.fetchApi('http://foo?bar=1', {method: "POST"})
expect(fetch.mock.calls[0][0]).toEqual( expect(fetchMock.mock.calls[0][0]).toEqual(
"http://foo?bar=1" "http://foo?bar=1"
) )
}) })
it('should be possible to do put request', () => { it('should be possible to do put request', () => {
fetch.mockClear() fetchMock.mockClear()
utils.fetchApi.put("http://foo", [1, 2, 3], {}) utils.fetchApi.put("http://foo", [1, 2, 3], {})
expect(fetch.mock.calls[0]).toEqual( expect(fetchMock.mock.calls[0]).toEqual(
[ [
"http://foo", "http://foo",
{ {

View File

@ -1,32 +0,0 @@
/*
* This backend uses the REST API only to host static instances,
* without any Websocket connection.
*/
import { fetchApi } from "../utils"
export default class StaticBackend {
constructor(store) {
this.store = store
this.onOpen()
}
onOpen() {
this.fetchData("flows")
this.fetchData("options")
// this.fetchData("events") # TODO: Add events log to static viewer.
}
fetchData(resource) {
fetchApi(`./${resource}`)
.then(res => res.json())
.then(json => {
this.receive(resource, json)
})
}
receive(resource, data) {
let type = `${resource}_RECEIVE`.toUpperCase()
this.store.dispatch({ type, cmd: "receive", resource, data })
}
}

View File

@ -0,0 +1,37 @@
/*
* This backend uses the REST API only to host static instances,
* without any Websocket connection.
*/
import {fetchApi} from "../utils"
import {Store} from "redux";
import {RootState} from "../ducks";
export default class StaticBackend {
store: Store<RootState>
constructor(store) {
this.store = store
this.onOpen()
}
onOpen() {
this.fetchData("flows")
this.fetchData("options")
// this.fetchData("events") # TODO: Add events log to static viewer.
}
fetchData(resource) {
fetchApi(`./${resource}`)
.then(res => res.json())
.then(json => {
this.receive(resource, json)
})
}
receive(resource, data) {
let type = `${resource}_RECEIVE`.toUpperCase()
this.store.dispatch({type, cmd: "receive", resource, data})
}
}

View File

@ -5,10 +5,21 @@
*/ */
import {fetchApi} from "../utils" import {fetchApi} from "../utils"
import * as connectionActions from "../ducks/connection" import * as connectionActions from "../ducks/connection"
import {Store} from "redux";
import {RootState} from "../ducks";
const CMD_RESET = 'reset' const CMD_RESET = 'reset'
export default class WebsocketBackend { export default class WebsocketBackend {
activeFetches: {
flows?: []
events?: []
options?: []
}
store: Store<RootState>
socket: WebSocket
constructor(store) { constructor(store) {
this.activeFetches = {} this.activeFetches = {}
this.store = store this.store = store
@ -75,7 +86,7 @@ export default class WebsocketBackend {
console.error("websocket connection closed", closeEvent) console.error("websocket connection closed", closeEvent)
} }
onError() { onError(error) {
// FIXME // FIXME
console.error("websocket connection errored", arguments) console.error("websocket connection errored", arguments)
} }

View File

@ -1,6 +1,6 @@
import React, {useEffect, useRef, useState} from 'react' import React, {useEffect, useRef, useState} from 'react'
import classnames from 'classnames' import classnames from 'classnames'
import {fetchApi, Key, runCommand} from '../utils' import {fetchApi, runCommand} from '../utils'
import Filt from '../filt/command' import Filt from '../filt/command'
type CommandParameter = { type CommandParameter = {
@ -151,7 +151,7 @@ export default function CommandBar() {
} }
const onKeyDown = (e) => { const onKeyDown = (e) => {
if (e.keyCode === Key.ENTER) { if (e.key === "Enter") {
const [cmd, ...args] = Filt.parse(input); const [cmd, ...args] = Filt.parse(input);
setHistory([...history, input]); setHistory([...history, input]);
@ -184,7 +184,7 @@ export default function CommandBar() {
setCurrentCompletion(0) setCurrentCompletion(0)
setCompletionCandidate(availableCommands) setCompletionCandidate(availableCommands)
} }
if (e.keyCode === Key.UP) { if (e.key === "ArrowUp") {
let nextPos; let nextPos;
if (currentPos === undefined) { if (currentPos === undefined) {
nextPos = history.length - 1; nextPos = history.length - 1;
@ -195,7 +195,7 @@ export default function CommandBar() {
setOriginalInput(history[nextPos]) setOriginalInput(history[nextPos])
setCurrentPos(nextPos) setCurrentPos(nextPos)
} }
if (e.keyCode === Key.DOWN) { if (e.key === "ArrowDown") {
if (currentPos === undefined) { if (currentPos === undefined) {
return return
} else if (currentPos == history.length - 1) { } else if (currentPos == history.length - 1) {
@ -209,7 +209,7 @@ export default function CommandBar() {
setCurrentPos(nextPos) setCurrentPos(nextPos)
} }
} }
if (e.keyCode === Key.TAB) { if (e.key === "Tab") {
setInput(completionCandidate[currentCompletion]) setInput(completionCandidate[currentCompletion])
setCurrentCompletion((currentCompletion + 1) % completionCandidate.length) setCurrentCompletion((currentCompletion + 1) % completionCandidate.length)
e.preventDefault() e.preventDefault()

View File

@ -3,9 +3,19 @@ import PropTypes from 'prop-types'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import shallowEqual from 'shallowequal' import shallowEqual from 'shallowequal'
import AutoScroll from '../helpers/AutoScroll' import AutoScroll from '../helpers/AutoScroll'
import { calcVScroll } from '../helpers/VirtualScroll' import {calcVScroll, VScroll} from '../helpers/VirtualScroll'
import {EventLogItem} from "../../ducks/eventLog";
class EventLogList extends Component {
type EventLogListProps = {
events: EventLogItem[]
rowHeight: number
}
type EventLogListState = {
vScroll: VScroll
}
class EventLogList extends Component<EventLogListProps, EventLogListState> {
static propTypes = { static propTypes = {
events: PropTypes.array.isRequired, events: PropTypes.array.isRequired,
@ -16,6 +26,8 @@ class EventLogList extends Component {
rowHeight: 18, rowHeight: 18,
} }
heights: {[id: string]: number}
constructor(props) { constructor(props) {
super(props) super(props)
@ -70,14 +82,14 @@ class EventLogList extends Component {
return ( return (
<pre onScroll={this.onViewportUpdate}> <pre onScroll={this.onViewportUpdate}>
<div style={{ height: vScroll.paddingTop }}></div> <div style={{ height: vScroll.paddingTop }}/>
{events.slice(vScroll.start, vScroll.end).map(event => ( {events.slice(vScroll.start, vScroll.end).map(event => (
<div key={event.id} ref={node => this.setHeight(event.id, node)}> <div key={event.id} ref={node => this.setHeight(event.id, node)}>
<LogIcon event={event}/> <LogIcon event={event}/>
{event.message} {event.message}
</div> </div>
))} ))}
<div style={{ height: vScroll.paddingBottom }}></div> <div style={{ height: vScroll.paddingBottom }}/>
</pre> </pre>
) )
} }
@ -90,7 +102,7 @@ function LogIcon({ event }) {
warn: 'exclamation-triangle', warn: 'exclamation-triangle',
error: 'ban' error: 'ban'
}[event.level] || 'info' }[event.level] || 'info'
return <i className={`fa fa-fw fa-${icon}`}></i> return <i className={`fa fa-fw fa-${icon}`}/>
} }
export default AutoScroll(EventLogList) export default AutoScroll(EventLogList)

View File

@ -1,7 +1,6 @@
import React, {Component} from 'react' import React, {Component} from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import classnames from 'classnames' import classnames from 'classnames'
import {Key} from '../../utils'
import Filt from '../../filt/filt' import Filt from '../../filt/filt'
import FilterDocs from './FilterDocs' import FilterDocs from './FilterDocs'
@ -91,7 +90,7 @@ export default class FilterInput extends Component<FilterInputProps, FilterInput
} }
onKeyDown(e) { onKeyDown(e) {
if (e.keyCode === Key.ESC || e.keyCode === Key.ENTER) { if (e.key === "Escape" || e.key === "Enter") {
this.blur() this.blur()
// If closed using ESC/ENTER, hide the tooltip. // If closed using ESC/ENTER, hide the tooltip.
this.setState({mousefocus: false}) this.setState({mousefocus: false})

View File

@ -2,11 +2,10 @@ import React, { Component } from "react"
import PropTypes from "prop-types" import PropTypes from "prop-types"
import {connect} from "react-redux" import {connect} from "react-redux"
import {update as updateOptions} from "../../ducks/options" import {update as updateOptions} from "../../ducks/options"
import { Key } from "../../utils"
import classnames from 'classnames' import classnames from 'classnames'
const stopPropagation = e => { const stopPropagation = e => {
if (e.keyCode !== Key.ESC) { if (e.key !== "Escape") {
e.stopPropagation() e.stopPropagation()
} }
} }
@ -15,6 +14,7 @@ BooleanOption.propTypes = {
value: PropTypes.bool.isRequired, value: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
} }
function BooleanOption({value, onChange, ...props}) { function BooleanOption({value, onChange, ...props}) {
return ( return (
<div className="checkbox"> <div className="checkbox">
@ -34,6 +34,7 @@ StringOption.propTypes = {
value: PropTypes.string, value: PropTypes.string,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
} }
function StringOption({value, onChange, ...props}) { function StringOption({value, onChange, ...props}) {
return ( return (
<input type="text" <input type="text"
@ -43,6 +44,7 @@ function StringOption({ value, onChange, ...props }) {
/> />
) )
} }
function Optional(Component) { function Optional(Component) {
return function ({onChange, ...props}) { return function ({onChange, ...props}) {
return <Component return <Component
@ -56,6 +58,7 @@ NumberOption.propTypes = {
value: PropTypes.number.isRequired, value: PropTypes.number.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
} }
function NumberOption({value, onChange, ...props}) { function NumberOption({value, onChange, ...props}) {
return ( return (
<input type="number" <input type="number"
@ -70,6 +73,7 @@ ChoicesOption.propTypes = {
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
} }
export function ChoicesOption({value, onChange, choices, ...props}) { export function ChoicesOption({value, onChange, choices, ...props}) {
return ( return (
<select <select
@ -90,6 +94,7 @@ StringSequenceOption.propTypes = {
value: PropTypes.arrayOf(PropTypes.string).isRequired, value: PropTypes.arrayOf(PropTypes.string).isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
} }
function StringSequenceOption({value, onChange, ...props}) { function StringSequenceOption({value, onChange, ...props}) {
const height = Math.max(value.length, 1) const height = Math.max(value.length, 1)
return <textarea return <textarea
@ -132,6 +137,7 @@ function PureOption({ choices, type, value, onChange, name, error }) {
/> />
</div> </div>
} }
export default connect( export default connect(
(state, {name}) => ({ (state, {name}) => ({
...state.options_meta[name], ...state.options_meta[name],

View File

@ -1,55 +0,0 @@
import React, { Component } from 'react'
import classnames from 'classnames'
import { Key } from '../../utils'
type ToggleInputButtonProps = {
name: string,
txt: string,
onToggleChanged: Function,
checked: boolean,
placeholder: string,
inputType: string,
}
type ToggleInputButtonState = {
txt: string,
}
export default class ToggleInputButton extends Component<ToggleInputButtonProps, ToggleInputButtonState> {
constructor(props) {
super(props)
this.state = { txt: props.txt || '' }
}
onKeyDown(e) {
e.stopPropagation()
if (e.keyCode === Key.ENTER) {
this.props.onToggleChanged(this.state.txt)
}
}
render() {
const {checked, onToggleChanged, name, inputType, placeholder} = this.props
return (
<div className="input-group toggle-input-btn">
<span className="input-group-btn"
onClick={() => onToggleChanged(this.state.txt)}>
<div className={classnames('btn', checked ? 'btn-primary' : 'btn-default')}>
<span className={classnames('fa', checked ? 'fa-check-square-o' : 'fa-square-o')}/>
&nbsp;
{name}
</div>
</span>
<input
className="form-control"
placeholder={placeholder}
disabled={checked}
value={this.state.txt}
type={inputType || 'text'}
onChange={e => this.setState({ txt: e.target.value })}
onKeyDown={e => this.onKeyDown(e)}
/>
</div>
)
}
}

View File

@ -1,8 +1,6 @@
import React, {Component} from 'react' import React, {Component} from 'react'
import classnames from 'classnames' import classnames from 'classnames'
import {Key} from '../../utils'
export interface ValueEditorProps { export interface ValueEditorProps {
content: string content: string
onEditDone: (newVal: string) => void onEditDone: (newVal: string) => void
@ -157,13 +155,13 @@ export default class ValueEditor extends Component<ValueEditorProps> {
onKeyDown = (e: React.KeyboardEvent<HTMLSpanElement>) => { onKeyDown = (e: React.KeyboardEvent<HTMLSpanElement>) => {
EVENT_DEBUG && console.debug("keydown", e) EVENT_DEBUG && console.debug("keydown", e)
e.stopPropagation() e.stopPropagation()
switch (e.keyCode) { switch (e.key) {
case Key.ESC: case "Escape":
e.preventDefault() e.preventDefault()
this.resetValue() this.resetValue()
this.finishEditing() this.finishEditing()
break break
case Key.ENTER: case "Enter":
if (!e.shiftKey) { if (!e.shiftKey) {
e.preventDefault() e.preventDefault()
this.finishEditing() this.finishEditing()

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
const symShouldStick = Symbol("shouldStick"); const symShouldStick = Symbol("shouldStick") as any;
const isAtBottom = v => v.scrollTop + v.clientHeight === v.scrollHeight; const isAtBottom = v => v.scrollTop + v.clientHeight === v.scrollHeight;
export default Component => Object.assign(class AutoScrollWrapper extends Component { export default Component => Object.assign(class AutoScrollWrapper extends Component {

View File

@ -18,7 +18,23 @@
* - {number} paddingTop * - {number} paddingTop
* - {number} paddingBottom * - {number} paddingBottom
*/ */
export function calcVScroll(opts) {
type VScrollArgs = {
itemCount: number
rowHeight: number
viewportTop: number
viewportHeight: number
itemHeights?: number[]
}
export type VScroll = {
start: number
end: number
paddingTop: number
paddingBottom: number
}
export function calcVScroll(opts: VScrollArgs | undefined = undefined) {
if (!opts) { if (!opts) {
return { start: 0, end: 0, paddingTop: 0, paddingBottom: 0 }; return { start: 0, end: 0, paddingTop: 0, paddingBottom: 0 };
} }

View File

@ -1,5 +1,3 @@
import {Reducer} from "redux";
export const TOGGLE_VISIBILITY = 'COMMANDBAR_TOGGLE_VISIBILITY' export const TOGGLE_VISIBILITY = 'COMMANDBAR_TOGGLE_VISIBILITY'
interface CommandBarState { interface CommandBarState {
@ -10,7 +8,7 @@ export const defaultState: CommandBarState = {
visible: false, visible: false,
}; };
const reducer: Reducer<CommandBarState> = (state = defaultState, action): CommandBarState => { export default function reducer(state = defaultState, action): CommandBarState {
switch (action.type) { switch (action.type) {
case TOGGLE_VISIBILITY: case TOGGLE_VISIBILITY:
return { return {
@ -22,7 +20,6 @@ const reducer: Reducer<CommandBarState> = (state = defaultState, action): Comman
return state return state
} }
} }
export default reducer
export function toggleVisibility() { export function toggleVisibility() {
return {type: TOGGLE_VISIBILITY} return {type: TOGGLE_VISIBILITY}

View File

@ -2,7 +2,6 @@
* Conf houses properties about the current mitmproxy instance that are not options, * Conf houses properties about the current mitmproxy instance that are not options,
* e.g. the list of available content views or the current version. * e.g. the list of available content views or the current version.
*/ */
import {Reducer} from "redux";
interface ConfState { interface ConfState {
static: boolean static: boolean
@ -17,7 +16,6 @@ export const defaultState: ConfState = window.MITMWEB_CONF || {
contentViews: ["Auto", "Raw"], contentViews: ["Auto", "Raw"],
}; };
const reducer: Reducer<ConfState> = (state = defaultState, action): ConfState => { export default function reducer(state = defaultState, action): ConfState {
return state return state
} }
export default reducer

View File

@ -1,5 +1,3 @@
import {Reducer} from "redux";
export enum ConnectionState { export enum ConnectionState {
INIT = "CONNECTION_INIT", INIT = "CONNECTION_INIT",
FETCHING = "CONNECTION_FETCHING", // WebSocket is established, but still fetching resources. FETCHING = "CONNECTION_FETCHING", // WebSocket is established, but still fetching resources.
@ -19,7 +17,7 @@ const defaultState: ConnState = {
message: undefined, message: undefined,
} }
const reducer: Reducer<ConnState> = (state = defaultState, action) => { export default function reducer(state = defaultState, action): ConnState {
switch (action.type) { switch (action.type) {
case ConnectionState.ESTABLISHED: case ConnectionState.ESTABLISHED:
case ConnectionState.FETCHING: case ConnectionState.FETCHING:
@ -34,7 +32,6 @@ const reducer: Reducer<ConnState> = (state = defaultState, action) => {
return state return state
} }
} }
export default reducer
export function startFetching() { export function startFetching() {
return {type: ConnectionState.FETCHING} return {type: ConnectionState.FETCHING}

View File

@ -7,7 +7,7 @@ export const TOGGLE_FILTER = 'EVENTS_TOGGLE_FILTER'
type LogLevel = 'debug' | 'info' | 'web' | 'warn' | 'error'; type LogLevel = 'debug' | 'info' | 'web' | 'warn' | 'error';
interface EventLogItem extends store.Item { export interface EventLogItem extends store.Item {
message: string message: string
level: LogLevel level: LogLevel
} }

View File

@ -28,7 +28,7 @@ export interface FlowsState extends store.State<Flow> {
selected: string[], selected: string[],
} }
const defaultState: FlowsState = { export const defaultState: FlowsState = {
highlight: undefined, highlight: undefined,
filter: undefined, filter: undefined,
sort: {column: undefined, desc: false}, sort: {column: undefined, desc: false},
@ -36,7 +36,7 @@ const defaultState: FlowsState = {
...store.defaultState ...store.defaultState
} }
export default function reduce(state: FlowsState = defaultState, action): FlowsState { export default function reducer(state: FlowsState = defaultState, action): FlowsState {
switch (action.type) { switch (action.type) {
case ADD: case ADD:

View File

@ -1,7 +1,5 @@
import {fetchApi} from "../utils" import {fetchApi} from "../utils"
import * as optionsEditorActions from "./ui/optionsEditor" import * as optionsEditorActions from "./ui/optionsEditor"
import _ from "lodash"
import {Reducer} from "redux";
import {defaultState, Option, OptionsState} from "./_options_gen"; import {defaultState, Option, OptionsState} from "./_options_gen";
import {AppThunk} from "./index"; import {AppThunk} from "./index";
@ -11,8 +9,7 @@ export const REQUEST_UPDATE = 'REQUEST_UPDATE'
export {Option, defaultState} export {Option, defaultState}
const reducer: Reducer<OptionsState> = (state = defaultState, action) => { export default function reducer(state = defaultState, action): OptionsState {
switch (action.type) { switch (action.type) {
case RECEIVE: case RECEIVE:
let s = <OptionsState>{}; let s = <OptionsState>{};
@ -34,7 +31,6 @@ const reducer: Reducer<OptionsState> = (state = defaultState, action) => {
return state return state
} }
} }
export default reducer
export async function pureSendUpdate(option: Option, value, dispatch) { export async function pureSendUpdate(option: Option, value, dispatch) {
try { try {

View File

@ -1,5 +1,3 @@
import {Reducer} from "redux";
export const export const
SET_TAB = "UI_FLOWVIEW_SET_TAB", SET_TAB = "UI_FLOWVIEW_SET_TAB",
SET_CONTENT_VIEW_FOR = "SET_CONTENT_VIEW_FOR" SET_CONTENT_VIEW_FOR = "SET_CONTENT_VIEW_FOR"
@ -15,8 +13,7 @@ export const defaultState: UiFlowState = {
contentViewFor: {}, contentViewFor: {},
} }
const reducer: Reducer<UiFlowState> = (state = defaultState, action): UiFlowState => { export default function reducer(state = defaultState, action): UiFlowState {
switch (action.type) { switch (action.type) {
case SET_CONTENT_VIEW_FOR: case SET_CONTENT_VIEW_FOR:
@ -38,7 +35,6 @@ const reducer: Reducer<UiFlowState> = (state = defaultState, action): UiFlowStat
return state return state
} }
} }
export default reducer;
export function selectTab(tab) { export function selectTab(tab) {
return {type: SET_TAB, tab} return {type: SET_TAB, tab}

View File

@ -1,18 +1,16 @@
import { Key } from "../../utils"
import {selectTab} from "./flow" import {selectTab} from "./flow"
import * as flowsActions from "../flows" import * as flowsActions from "../flows"
import * as modalActions from "./modal" import * as modalActions from "./modal"
import {tabsForFlow} from "../../components/FlowView"; import {tabsForFlow} from "../../components/FlowView";
export function onKeyDown(e) { export function onKeyDown(e: KeyboardEvent) {
//console.debug("onKeyDown", e) //console.debug("onKeyDown", e)
if (e.ctrlKey || e.metaKey) { if (e.ctrlKey || e.metaKey) {
return () => { return () => {
} }
} }
let key = e.keyCode, const key = e.key;
shiftKey = e.shiftKey
e.preventDefault() e.preventDefault()
return (dispatch, getState) => { return (dispatch, getState) => {
@ -20,42 +18,42 @@ export function onKeyDown(e) {
flow = flows.byId[getState().flows.selected[0]] flow = flows.byId[getState().flows.selected[0]]
switch (key) { switch (key) {
case Key.K: case "k":
case Key.UP: case "ArrowUp":
dispatch(flowsActions.selectRelative(flows, -1)) dispatch(flowsActions.selectRelative(flows, -1))
break break
case Key.J: case "j":
case Key.DOWN: case "ArrowDown":
dispatch(flowsActions.selectRelative(flows, +1)) dispatch(flowsActions.selectRelative(flows, +1))
break break
case Key.SPACE: case " ":
case Key.PAGE_DOWN: case "PageDown":
dispatch(flowsActions.selectRelative(flows, +10)) dispatch(flowsActions.selectRelative(flows, +10))
break break
case Key.PAGE_UP: case "PageUp":
dispatch(flowsActions.selectRelative(flows, -10)) dispatch(flowsActions.selectRelative(flows, -10))
break break
case Key.END: case "End":
dispatch(flowsActions.selectRelative(flows, +1e10)) dispatch(flowsActions.selectRelative(flows, +1e10))
break break
case Key.HOME: case "Home":
dispatch(flowsActions.selectRelative(flows, -1e10)) dispatch(flowsActions.selectRelative(flows, -1e10))
break break
case Key.ESC: case "Escape":
if (getState().ui.modal.activeModal) { if (getState().ui.modal.activeModal) {
dispatch(modalActions.hideModal()) dispatch(modalActions.hideModal())
} else { } else {
dispatch(flowsActions.select(null)) dispatch(flowsActions.select(undefined))
} }
break break
case Key.LEFT: { case "ArrowLeft": {
if (!flow) break if (!flow) break
let tabs = tabsForFlow(flow), let tabs = tabsForFlow(flow),
currentTab = getState().ui.flow.tab, currentTab = getState().ui.flow.tab,
@ -64,8 +62,8 @@ export function onKeyDown(e) {
break break
} }
case Key.TAB: case "Tab":
case Key.RIGHT: { case "ArrowRight": {
if (!flow) break if (!flow) break
let tabs = tabsForFlow(flow), let tabs = tabsForFlow(flow),
currentTab = getState().ui.flow.tab, currentTab = getState().ui.flow.tab,
@ -74,57 +72,62 @@ export function onKeyDown(e) {
break break
} }
case Key.D: { case "d": {
if (!flow) { if (!flow) {
return return
} }
if (shiftKey) {
dispatch(flowsActions.duplicate(flow))
} else {
dispatch(flowsActions.remove(flow)) dispatch(flowsActions.remove(flow))
}
break break
} }
case Key.A: { case "D": {
if (shiftKey) { if (!flow) {
dispatch(flowsActions.resumeAll()) return
} else if (flow && flow.intercepted) { }
dispatch(flowsActions.duplicate(flow))
break
}
case "a": {
if (flow && flow.intercepted) {
dispatch(flowsActions.resume(flow)) dispatch(flowsActions.resume(flow))
} }
break break
} }
case "A": {
dispatch(flowsActions.resumeAll())
break
}
case Key.R: { case "r": {
if (!shiftKey && flow) { if (flow) {
dispatch(flowsActions.replay(flow)) dispatch(flowsActions.replay(flow))
} }
break break
} }
case Key.V: { case "v": {
if (!shiftKey && flow && flow.modified) { if (flow && flow.modified) {
dispatch(flowsActions.revert(flow)) dispatch(flowsActions.revert(flow))
} }
break break
} }
case Key.X: { case "x": {
if (shiftKey) { if (flow && flow.intercepted) {
dispatch(flowsActions.killAll())
} else if (flow && flow.intercepted) {
dispatch(flowsActions.kill(flow)) dispatch(flowsActions.kill(flow))
} }
break break
} }
case Key.Z: { case "X": {
if (!shiftKey) { dispatch(flowsActions.killAll())
dispatch(flowsActions.clear())
}
break break
} }
case "z": {
dispatch(flowsActions.clear())
break
}
default: default:
return return

View File

@ -52,7 +52,7 @@ export const defaultState = {
* } * }
* *
*/ */
export function reduce<S extends Item>(state: State<S> = defaultState, action) { export function reduce<S extends Item>(state: State<S> = defaultState, action): State<S> {
let {byId, list, listIndex, view, viewIndex} = state let {byId, list, listIndex, view, viewIndex} = state

View File

@ -5,28 +5,6 @@ import * as React from 'react'
window._ = _; window._ = _;
window.React = React; window.React = React;
export var Key = {
UP: 38,
DOWN: 40,
PAGE_UP: 33,
PAGE_DOWN: 34,
HOME: 36,
END: 35,
LEFT: 37,
RIGHT: 39,
ENTER: 13,
ESC: 27,
TAB: 9,
SPACE: 32,
BACKSPACE: 8,
SHIFT: 16
};
// Add A-Z
for (var i = 65; i <= 90; i++) {
Key[String.fromCharCode(i)] = i;
}
export var formatSize = function (bytes) { export var formatSize = function (bytes) {
if (bytes === 0) if (bytes === 0)
return "0"; return "0";