convert components in common and ContentView folder into typescript, and modified test

This commit is contained in:
zokutyou2@gmail.com 2021-07-17 12:29:11 +09:00
parent 7ff97d6e08
commit 6def195743
26 changed files with 321 additions and 388 deletions

View File

@ -19,16 +19,12 @@ describe('ViewImage Component', () => {
})
describe('ViewServer Component', () => {
let store = TStore(),
setContentViewDescFn = jest.fn(),
setContentFn = jest.fn()
let store = TStore()
it('should render correctly and connect to state', () => {
let provider = renderer.create(
<Provider store={store}>
<ViewServer
setContentViewDescription={setContentViewDescFn}
setContent={setContentFn}
flow={tflow}
message={tflow.response}
/>
@ -37,38 +33,17 @@ describe('ViewServer Component', () => {
expect(tree).toMatchSnapshot()
let viewServer = renderer.create(
<PureViewServer
showFullContent={true}
maxLines={10}
setContentViewDescription={setContentViewDescFn}
setContent={setContentViewDescFn}
flow={tflow}
message={tflow.response}
content={JSON.stringify({lines: [['k1', 'v1']]})}
/>
<Provider store={store}>
<PureViewServer
flow={tflow}
message={tflow.response}
content={JSON.stringify({lines: [['k1', 'v1']]})}
/>
</Provider>
)
tree = viewServer.toJSON()
expect(tree).toMatchSnapshot()
})
it('should handle componentWillReceiveProps', () => {
// case of fail to parse content
let viewServer = TestUtils.renderIntoDocument(
<PureViewServer
showFullContent={true}
maxLines={10}
setContentViewDescription={setContentViewDescFn}
setContent={setContentViewDescFn}
flow={tflow}
message={tflow.response}
content={JSON.stringify({lines: [['k1', 'v1']]})}
/>
)
viewServer.UNSAFE_componentWillReceiveProps({...viewServer.props, content: '{foo' })
let e = ''
try {JSON.parse('{foo') } catch(err){ e = err.message}
expect(viewServer.data).toEqual({ description: e, lines: [] })
})
})
describe('Edit Component', () => {

View File

@ -1,39 +1,22 @@
import React from 'react'
import renderer from 'react-test-renderer'
import { Provider } from 'react-redux'
import ConnectedComponent, { ShowFullContentButton } from '../../../components/ContentView/ShowFullContentButton'
import ShowFullContentButton from '../../../components/ContentView/ShowFullContentButton'
import { TStore } from '../../ducks/tutils'
describe('ShowFullContentButton Component', () => {
let store = TStore()
let setShowFullContentFn = jest.fn(),
showFullContentButton = renderer.create(
<ShowFullContentButton
setShowFullContent={setShowFullContentFn}
showFullContent={false}
visibleLines={10}
contentLines={20}
/>
<Provider store={store}>
<ShowFullContentButton />
</Provider>
),
tree = showFullContentButton.toJSON()
it('should render correctly', () => {
expect(tree).toMatchSnapshot()
})
it('should handle click', () => {
tree.children[0].props.onClick()
expect(setShowFullContentFn).toBeCalled()
})
it('should connect to state', () => {
let store = TStore(),
provider = renderer.create(
<Provider store={store}>
<ConnectedComponent/>
</Provider>
),
tree = provider.toJSON()
expect(tree).toMatchSnapshot()
})
})

View File

@ -4,23 +4,12 @@ exports[`ContentViewOptions Component should render correctly 1`] = `
<div
className="view-options"
>
<a
className="btn btn-default btn-xs pull-left"
href="#"
onClick={[Function]}
>
<span>
<b>
View:
</b>
auto
<span
className="caret"
/>
</span>
</a>
<span>
<b>
View:
</b>
edit
</span>
 
<a
className="btn btn-default btn-xs"
@ -32,9 +21,21 @@ exports[`ContentViewOptions Component should render correctly 1`] = `
/>
</a>
 
<a
className="btn btn-default btn-xs"
href="#"
onClick={[Function]}
title="Upload a file to replace the content."
>
<i
className="fa fa-fw fa-upload"
/>
<input
className="hidden"
onChange={[Function]}
type="file"
/>
</a>
 
<span>
foo
</span>
</div>
`;

View File

@ -34,19 +34,6 @@ exports[`ViewServer Component should render correctly and connect to state 1`] =
exports[`ViewServer Component should render correctly and connect to state 2`] = `
<div>
<pre>
<div>
<span
className="k"
>
1
</span>
<span
className="v"
>
1
</span>
</div>
</pre>
<pre />
</div>
`;

View File

@ -1,23 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ShowFullContentButton Component should connect to state 1`] = `null`;
exports[`ShowFullContentButton Component should render correctly 1`] = `
<div>
<button
className="view-all-content-btn btn-xs btn btn-default"
onClick={[Function]}
>
Show full content
</button>
<span
className="pull-right"
>
10
/
20
are visible  
</span>
</div>
`;
exports[`ShowFullContentButton Component should render correctly 1`] = `null`;

View File

@ -71,9 +71,7 @@ describe('Request Component', () => {
})
it('should handle uploadContent on flow request ContentViewOptions', () => {
// The line below shouldn't have .type, this is a workaround for https://github.com/facebook/react/issues/17301.
// If this test breaks, just remove it.
let contentViewOptions = provider.root.findByType(ContentViewOptions.type)
let contentViewOptions = provider.root.findByType(ContentViewOptions)
contentViewOptions.props.uploadContent('foo')
expect(fetch).toBeCalled()
fetch.mockClear()
@ -132,9 +130,7 @@ describe('Response Component', () => {
})
it('should handle updateContent on flow response ContentViewOptions', () => {
// The line below shouldn't have .type, this is a workaround for https://github.com/facebook/react/issues/17301.
// If this test breaks, just remove it.
let contentViewOptions = provider.root.findByType(ContentViewOptions.type)
let contentViewOptions = provider.root.findByType(ContentViewOptions)
contentViewOptions.props.uploadContent('foo')
expect(fetch).toBeCalled()
fetch.mockClear()

View File

@ -196,23 +196,12 @@ exports[`Request Component should render correctly 1`] = `
<div
className="view-options"
>
<a
className="btn btn-default btn-xs pull-left"
href="#"
onClick={[Function]}
>
<span>
<b>
View:
</b>
auto
<span
className="caret"
/>
</span>
</a>
<span>
<b>
View:
</b>
edit
</span>
 
<a
className="btn btn-default btn-xs"
@ -224,10 +213,22 @@ exports[`Request Component should render correctly 1`] = `
/>
</a>
 
<a
className="btn btn-default btn-xs"
href="#"
onClick={[Function]}
title="Upload a file to replace the content."
>
<i
className="fa fa-fw fa-upload"
/>
<input
className="hidden"
onChange={[Function]}
type="file"
/>
</a>
 
<span>
foo
</span>
</div>
</footer>
</section>
@ -456,23 +457,12 @@ exports[`Response Component should render correctly 1`] = `
<div
className="view-options"
>
<a
className="btn btn-default btn-xs pull-left"
href="#"
onClick={[Function]}
>
<span>
<b>
View:
</b>
auto
<span
className="caret"
/>
</span>
</a>
<span>
<b>
View:
</b>
edit
</span>
 
<a
className="btn btn-default btn-xs"
@ -484,10 +474,22 @@ exports[`Response Component should render correctly 1`] = `
/>
</a>
 
<a
className="btn btn-default btn-xs"
href="#"
onClick={[Function]}
title="Upload a file to replace the content."
>
<i
className="fa fa-fw fa-upload"
/>
<input
className="hidden"
onChange={[Function]}
type="file"
/>
</a>
 
<span>
foo
</span>
</div>
</footer>
</section>

View File

@ -21,7 +21,7 @@ type ResultProps = {
results: CommandResult[],
}
function getAvailableCommands(commands, input: string = "") {
function getAvailableCommands(commands: object, input: string = "") {
if (!commands) return []
let availableCommands: string[] = []
for (const [command, args] of Object.entries(commands)) {
@ -37,7 +37,7 @@ export function Results({results}: ResultProps) {
useEffect(() => {
if (resultElement) {
resultElement.current?.addEventListener('DOMNodeInserted', event => {
resultElement.current.addEventListener('DOMNodeInserted', event => {
const { currentTarget: target } = event;
target.scroll({ top: target.scrollHeight, behavior: 'auto' });
});
@ -83,7 +83,7 @@ export default function CommandBar() {
const [completionCandidate, setCompletionCandidate] = useState<string[]>([])
const [availableCommands, setAvailableCommands] = useState<string[]>([])
const [allCommands, setAllCommands] = useState({})
const [allCommands, setAllCommands] = useState<object>({})
const [nextArgs, setNextArgs] = useState<string[]>([])
const [currentArg, setCurrentArg] = useState<number>(0)
const [signatureHelp, setSignatureHelp] = useState<string>("")

View File

@ -1,19 +1,26 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { MessageUtils } from '../../flow/utils'
type ContentLoaderProps = {
content: string,
contentView: object,
flow: object,
message: {
content: string,
contentHash: string,
},
}
type ContentLoaderStates = {
content: string | undefined,
request: { abort: () => void }| undefined,
}
export default function withContentLoader(View) {
return class extends React.Component {
static displayName = View.displayName || View.name
static matches = View.matches
static propTypes = {
...View.propTypes,
content: PropTypes.string, // mark as non-required
flow: PropTypes.object.isRequired,
message: PropTypes.object.isRequired,
}
return class extends React.Component<ContentLoaderProps, ContentLoaderStates> {
static displayName: string = View.displayName || View.name
static matches: (message: any) => boolean = View.matches
constructor(props) {
super(props)
@ -45,6 +52,7 @@ export default function withContentLoader(View) {
updateContent(props) {
if (this.state.request) {
console.log("request:",this.state.request)
this.state.request.abort()
}
// We have a few special cases where we do not need to make an HTTP request.

View File

@ -1,32 +0,0 @@
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: PropTypes.object.isRequired,
message: PropTypes.object.isRequired,
}
function ContentViewOptions({ flow, message, uploadContent, readonly, contentViewDescription }) {
return (
<div className="view-options">
{readonly ? <ViewSelector message={message}/> : <span><b>View:</b> edit</span>}
&nbsp;
<DownloadContentButton flow={flow} message={message}/>
&nbsp;
{!readonly && <UploadContentButton uploadContent={uploadContent}/> }
&nbsp;
{readonly && <span>{contentViewDescription}</span>}
</div>
)
}
export default connect(
state => ({
contentViewDescription: state.ui.flow.viewDescription,
readonly: !state.ui.flow.modifiedFlow,
})
)(ContentViewOptions)

View File

@ -0,0 +1,27 @@
import React from 'react'
import ViewSelector from './ViewSelector'
import UploadContentButton from './UploadContentButton'
import DownloadContentButton from './DownloadContentButton'
import { useAppSelector } from "../../ducks";
type ContentViewOptionsProps = {
flow: object,
message: object,
uploadContent: () => void,
}
export default function ContentViewOptions({ flow, message, uploadContent }: ContentViewOptionsProps) {
const contentViewDescription = useAppSelector(state => state.ui.flow.viewDescription)
const readonly = useAppSelector(state => state.ui.flow.modifiedFlow);
return (
<div className="view-options">
{readonly ? <ViewSelector /> : <span><b>View:</b> edit</span>}
&nbsp;
<DownloadContentButton flow={flow} message={message}/>
&nbsp;
{!readonly && <UploadContentButton uploadContent={uploadContent}/> }
&nbsp;
{readonly && <span>{contentViewDescription}</span>}
</div>
)
}

View File

@ -1,99 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { setContentViewDescription, setContent } from '../../ducks/ui/flow'
import withContentLoader from './ContentLoader'
import { MessageUtils } from '../../flow/utils'
import CodeEditor from './CodeEditor'
const isImage = /^image\/(png|jpe?g|gif|webp|vnc.microsoft.icon|x-icon)$/i
ViewImage.matches = msg => isImage.test(MessageUtils.getContentType(msg))
ViewImage.propTypes = {
flow: PropTypes.object.isRequired,
message: PropTypes.object.isRequired,
}
function ViewImage({ flow, message }) {
return (
<div className="flowview-image">
<img src={MessageUtils.getContentURL(flow, message)} alt="preview" className="img-thumbnail"/>
</div>
)
}
Edit.propTypes = {
content: PropTypes.string.isRequired,
}
function Edit({ content, onChange }) {
return <CodeEditor content={content} onChange={onChange}/>
}
Edit = withContentLoader(Edit)
export class PureViewServer extends Component {
static propTypes = {
showFullContent: PropTypes.bool.isRequired,
maxLines: PropTypes.number.isRequired,
setContentViewDescription : PropTypes.func.isRequired,
setContent: PropTypes.func.isRequired
}
UNSAFE_componentWillMount(){
this.setContentView(this.props)
}
UNSAFE_componentWillReceiveProps(nextProps){
if (nextProps.content !== this.props.content) {
this.setContentView(nextProps)
}
}
setContentView(props){
try {
this.data = JSON.parse(props.content)
}catch(err) {
this.data = {lines: [], description: err.message}
}
props.setContentViewDescription(props.contentView !== this.data.description ? this.data.description : '')
props.setContent(this.data.lines)
}
render() {
const {content, contentView, message, maxLines} = this.props
let lines = this.props.showFullContent ? this.data.lines : this.data.lines.slice(0, maxLines)
return (
<div>
{ViewImage.matches(message) && <ViewImage {...this.props} />}
<pre>
{lines.map((line, i) =>
<div key={`line${i}`}>
{line.map((element, j) => {
let [style, text] = element
return (
<span key={`tuple${j}`} className={style}>
{text}
</span>
)
})}
</div>
)}
</pre>
</div>
)
}
}
const ViewServer = connect(
state => ({
showFullContent: state.ui.flow.showFullContent,
maxLines: state.ui.flow.maxContentLines
}),
{
setContentViewDescription,
setContent
}
)(withContentLoader(PureViewServer))
export { Edit, ViewServer, ViewImage }

View File

@ -0,0 +1,97 @@
import React, { useEffect, useState } from 'react'
import { setContentViewDescription, setContent } from '../../ducks/ui/flow'
import withContentLoader from './ContentLoader'
import { MessageUtils } from '../../flow/utils'
import CodeEditor from './CodeEditor'
import { useAppDispatch, useAppSelector } from "../../ducks";
const isImage = /^image\/(png|jpe?g|gif|webp|vnc.microsoft.icon|x-icon)$/i
ViewImage.matches = msg => isImage.test(MessageUtils.getContentType(msg))
type ViewImageProps = {
flow: object,
message: object,
}
function ViewImage({ flow, message }: ViewImageProps) {
return (
<div className="flowview-image">
<img src={MessageUtils.getContentURL(flow, message)} alt="preview" className="img-thumbnail"/>
</div>
)
}
type EditProps = {
content: string,
onChange: () => void,
}
function PureEdit({ content, onChange }: EditProps) {
return <CodeEditor content={content} onChange={onChange}/>
}
const Edit = withContentLoader(PureEdit)
type PureViewServerProps = {
flow: object,
message: object,
content: string,
}
type PureViewServerStates = {
lines: string[][],
description: string,
}
export function PureViewServer({flow, message, content}: PureViewServerProps) {
const [data, setData] = useState<PureViewServerStates>({
lines: [],
description: "",
})
const dispatch = useAppDispatch(),
showFullContent: boolean = useAppSelector(state => state.ui.flow.showFullContent),
maxLines: number = useAppSelector(state => state.ui.flow.maxContentLines)
let lines = showFullContent ? data.lines : data.lines?.slice(0, maxLines)
useEffect(() => {
setContentView({flow, message, content})
}, [flow, message, content])
const setContentView = (props) => {
try {
setData(JSON.parse(props.content))
}catch(err) {
setData({lines: [], description: err.message})
}
dispatch(setContentViewDescription(props.contentView !== data.description ? data.description : ''))
dispatch(setContent(data.lines))
}
return (
<div>
{ViewImage.matches(message) && <ViewImage flow={flow} message={message}/>}
<pre>
{lines.map((line, i) =>
<div key={`line${i}`}>
{line.map((element, j) => {
let [style, text] = element
return (
<span key={`tuple${j}`} className={style}>
{text}
</span>
)
})}
</div>
)}
</pre>
</div>
)
}
const ViewServer = withContentLoader(PureViewServer)
export { Edit, ViewServer, ViewImage }

View File

@ -1,13 +1,12 @@
import React from 'react'
import { MessageUtils } from "../../flow/utils"
import PropTypes from 'prop-types'
DownloadContentButton.propTypes = {
flow: PropTypes.object.isRequired,
message: PropTypes.object.isRequired,
type DownloadContentButtonProps = {
flow: object,
message: object,
}
export default function DownloadContentButton({ flow, message }) {
export default function DownloadContentButton({ flow, message }: DownloadContentButtonProps) {
return (
<a className="btn btn-default btn-xs"

View File

@ -3,7 +3,18 @@ import { formatSize } from '../../utils'
import UploadContentButton from './UploadContentButton'
import DownloadContentButton from './DownloadContentButton'
export function ContentEmpty({ flow, message }) {
interface ContentProps {
flow: { request: object },
message: { contentLength: number },
}
interface ContentTooLargeProps extends ContentProps {
onClick: () => void,
uploadContent: () => any,
}
export function ContentEmpty({ flow, message }: ContentProps) {
return (
<div className="alert alert-info">
No {flow.request === message ? 'request' : 'response'} content.
@ -11,7 +22,7 @@ export function ContentEmpty({ flow, message }) {
)
}
export function ContentMissing({ flow, message }) {
export function ContentMissing({ flow, message }: ContentProps) {
return (
<div className="alert alert-info">
{flow.request === message ? 'Request' : 'Response'} content missing.
@ -19,7 +30,7 @@ export function ContentMissing({ flow, message }) {
)
}
export function ContentTooLarge({ message, onClick, uploadContent, flow }) {
export function ContentTooLarge({ message, onClick, uploadContent, flow }: ContentTooLargeProps) {
return (
<div>
<div className="alert alert-warning">

View File

@ -1,39 +0,0 @@
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';
import { setShowFullContent } from '../../ducks/ui/flow'
ShowFullContentButton.propTypes = {
setShowFullContent: PropTypes.func.isRequired,
showFullContent: PropTypes.bool.isRequired
}
export function ShowFullContentButton ( {setShowFullContent, showFullContent, visibleLines, contentLines} ){
return (
!showFullContent &&
<div>
<Button className="view-all-content-btn btn-xs" onClick={() => setShowFullContent()}>
Show full content
</Button>
<span className="pull-right"> {visibleLines}/{contentLines} are visible &nbsp; </span>
</div>
)
}
export default connect(
state => ({
showFullContent: state.ui.flow.showFullContent,
visibleLines: state.ui.flow.maxContentLines,
contentLines: state.ui.flow.content.length
}),
{
setShowFullContent
}
)(ShowFullContentButton)

View File

@ -0,0 +1,22 @@
import React from 'react'
import Button from '../common/Button';
import { setShowFullContent } from '../../ducks/ui/flow'
import {useAppDispatch, useAppSelector} from "../../ducks";
export default function ShowFullContentButton() {
const dispatch = useAppDispatch(),
showFullContent = useAppSelector(state => state.ui.flow.showFullContent),
visibleLines = useAppSelector(state => state.ui.flow.maxContentLines),
contentLines = useAppSelector(state => state.ui.flow.content.length)
return (
!showFullContent &&
<div>
<Button className="view-all-content-btn btn-xs" onClick={() => dispatch(setShowFullContent())}>
Show full content
</Button>
<span className="pull-right"> {visibleLines}/{contentLines} are visible &nbsp; </span>
</div>
)
}

View File

@ -1,12 +1,11 @@
import React from 'react'
import PropTypes from 'prop-types'
import FileChooser from '../common/FileChooser'
UploadContentButton.propTypes = {
uploadContent: PropTypes.func.isRequired,
type UploadContentButtonProps = {
uploadContent: () => any,
}
export default function UploadContentButton({ uploadContent }) {
export default function UploadContentButton({ uploadContent }: UploadContentButtonProps) {
return (
<FileChooser

View File

@ -1,15 +1,16 @@
import React from "react"
import PropTypes from 'prop-types'
import classnames from "classnames"
Button.propTypes = {
onClick: PropTypes.func.isRequired,
children: PropTypes.node,
icon: PropTypes.string,
title: PropTypes.string,
type ButtonProps = {
onClick: () => void,
children?: React.ReactNode,
icon?: string,
disabled?: boolean,
className?: string,
title?: string,
}
export default function Button({ onClick, children, icon, disabled, className, title }) {
export default function Button({ onClick, children, icon, disabled, className, title }: ButtonProps) {
return (
<button className={classnames(className, 'btn btn-default')}
onClick={disabled ? undefined : onClick}

View File

@ -1,11 +1,11 @@
import React from "react"
import PropTypes from "prop-types"
DocsLink.propTypes = {
resource: PropTypes.string.isRequired,
type DocLinkProps = {
children: React.ReactNode,
resource: string
}
export default function DocsLink({ children, resource }) {
export default function DocsLink({ children, resource }: DocLinkProps) {
let url = `https://docs.mitmproxy.org/stable/${resource}`
return (
<a target="_blank" href={url}>

View File

@ -2,7 +2,7 @@ import React from "react";
type FileChooserProps = {
icon: string
text: string
text?: string
className?: string
title?: string
onOpenFile: (File) => void

View File

@ -1,5 +0,0 @@
import React from 'react'
export default function HideInStatic({ children }) {
return (window.MITMWEB_CONF && window.MITMWEB_CONF.static) ? null : [children]
}

View File

@ -0,0 +1,9 @@
import React from 'react'
type HideInStaticProps = {
children: React.ReactNode,
}
export default function HideInStatic({ children }: HideInStaticProps) {
return (window.MITMWEB_CONF && window.MITMWEB_CONF.static) ? null : [children]
}

View File

@ -2,14 +2,24 @@ import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import classnames from 'classnames'
export default class Splitter extends Component {
type SplitterStates = {
applied: boolean,
startX: number,
startY: number,
}
type SplitterProps = {
axis: string,
}
export default class Splitter extends Component<SplitterProps, SplitterStates> {
static defaultProps = { axis: 'x' }
constructor(props, context) {
super(props, context)
this.state = { applied: false, startX: false, startY: false }
this.state = { applied: false, startX: 0, startY: 0 }
this.onMouseMove = this.onMouseMove.bind(this)
this.onMouseDown = this.onMouseDown.bind(this)

View File

@ -1,13 +1,12 @@
import React from 'react'
import PropTypes from 'prop-types'
ToggleButton.propTypes = {
checked: PropTypes.bool.isRequired,
onToggle: PropTypes.func.isRequired,
text: PropTypes.string.isRequired
type ToggleButtonProps = {
checked: boolean,
onToggle: () => void,
text: string
}
export default function ToggleButton({ checked, onToggle, text }) {
export default function ToggleButton({ checked, onToggle, text }: ToggleButtonProps) {
return (
<div className={"btn btn-toggle " + (checked ? "btn-primary" : "btn-default")} onClick={onToggle}>
<i className={"fa fa-fw " + (checked ? "fa-check-square-o" : "fa-square-o")}/>

View File

@ -1,19 +1,21 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { Key } from '../../utils'
export default class ToggleInputButton extends Component {
type ToggleInputButtonProps = {
name: string,
txt: string,
onToggleChanged: (string) => void,
checked: boolean,
placeholder: string,
inputType: string,
}
static propTypes = {
name: PropTypes.string.isRequired,
txt: PropTypes.string,
onToggleChanged: PropTypes.func.isRequired,
checked: PropTypes.bool.isRequired,
placeholder: PropTypes.string.isRequired,
inputType: PropTypes.string
}
type ToggleInputButtonStates = {
txt: string,
}
export default class ToggleInputButton extends Component<ToggleInputButtonProps, ToggleInputButtonStates> {
constructor(props) {
super(props)
this.state = { txt: props.txt || '' }