mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-25 09:37:37 +00:00
web: major upgrades
This commit brings a bunch of under-the-hood mitmweb improvements: - migrate large parts of the codebase to typescript - introduce modern react testing conventions - vendor react-codemirror to silence warnings - use esbuild for both bundles and tests - move from yarn to npm - various fixes across the board
This commit is contained in:
parent
d6fc9a7b27
commit
9b119c3dac
19
.github/workflows/main.yml
vendored
19
.github/workflows/main.yml
vendored
@ -137,17 +137,20 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- run: git rev-parse --abbrev-ref HEAD
|
||||
- uses: actions/setup-node@v1
|
||||
- id: yarn-cache
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
path: ${{ steps.yarn-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
node-version: '14'
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
# npm cache files are stored in `~/.npm` on Linux/macOS
|
||||
path: ~/.npm
|
||||
key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
${{ runner.OS }}-node-
|
||||
${{ runner.OS }}-
|
||||
- working-directory: ./web
|
||||
run: yarn
|
||||
run: npm ci
|
||||
- working-directory: ./web
|
||||
run: npm test
|
||||
- uses: codecov/codecov-action@a1ed4b322b4b38cb846afb5a0ebfa17086917d27
|
||||
|
@ -134,7 +134,7 @@ formats = dict(
|
||||
)
|
||||
|
||||
|
||||
class Export():
|
||||
class Export:
|
||||
def load(self, loader):
|
||||
loader.add_option(
|
||||
"export_preserve_original_ip", bool, False,
|
||||
|
@ -20,8 +20,8 @@ from mitmproxy import io
|
||||
from mitmproxy import log
|
||||
from mitmproxy import optmanager
|
||||
from mitmproxy import version
|
||||
from mitmproxy.addons import export
|
||||
from mitmproxy.utils.strutils import always_str
|
||||
from mitmproxy.addons.export import curl_command
|
||||
|
||||
|
||||
def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
||||
@ -30,6 +30,8 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
||||
|
||||
Args:
|
||||
flow: The original flow.
|
||||
|
||||
Sync with web/src/flow.ts.
|
||||
"""
|
||||
f = {
|
||||
"id": flow.id,
|
||||
@ -43,31 +45,42 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
||||
if flow.client_conn:
|
||||
f["client_conn"] = {
|
||||
"id": flow.client_conn.id,
|
||||
"address": flow.client_conn.peername,
|
||||
"peername": flow.client_conn.peername,
|
||||
"sockname": flow.client_conn.sockname,
|
||||
"tls_established": flow.client_conn.tls_established,
|
||||
"sni": flow.client_conn.sni,
|
||||
"cipher": flow.client_conn.cipher,
|
||||
"alpn": always_str(flow.client_conn.alpn, "ascii", "backslashreplace"),
|
||||
"tls_version": flow.client_conn.tls_version,
|
||||
"timestamp_start": flow.client_conn.timestamp_start,
|
||||
"timestamp_tls_setup": flow.client_conn.timestamp_tls_setup,
|
||||
"timestamp_end": flow.client_conn.timestamp_end,
|
||||
"sni": flow.client_conn.sni,
|
||||
|
||||
# Legacy properties
|
||||
"address": flow.client_conn.peername,
|
||||
"cipher_name": flow.client_conn.cipher,
|
||||
"alpn_proto_negotiated": always_str(flow.client_conn.alpn, "ascii", "backslashreplace"),
|
||||
"tls_version": flow.client_conn.tls_version,
|
||||
}
|
||||
|
||||
if flow.server_conn:
|
||||
f["server_conn"] = {
|
||||
"id": flow.server_conn.id,
|
||||
"peername": flow.server_conn.peername,
|
||||
"sockname": flow.server_conn.sockname,
|
||||
"address": flow.server_conn.address,
|
||||
"ip_address": flow.server_conn.peername,
|
||||
"source_address": flow.server_conn.sockname,
|
||||
"tls_established": flow.server_conn.tls_established,
|
||||
"sni": flow.server_conn.sni,
|
||||
"alpn_proto_negotiated": always_str(flow.client_conn.alpn, "ascii", "backslashreplace"),
|
||||
"cipher": flow.server_conn.cipher,
|
||||
"alpn": always_str(flow.server_conn.alpn, "ascii", "backslashreplace"),
|
||||
"tls_version": flow.server_conn.tls_version,
|
||||
"timestamp_start": flow.server_conn.timestamp_start,
|
||||
"timestamp_tcp_setup": flow.server_conn.timestamp_tcp_setup,
|
||||
"timestamp_tls_setup": flow.server_conn.timestamp_tls_setup,
|
||||
"timestamp_end": flow.server_conn.timestamp_end,
|
||||
# Legacy properties
|
||||
"ip_address": flow.server_conn.peername,
|
||||
"source_address": flow.server_conn.sockname,
|
||||
"alpn_proto_negotiated": always_str(flow.server_conn.alpn, "ascii", "backslashreplace"),
|
||||
}
|
||||
if flow.error:
|
||||
f["error"] = flow.error.get_state()
|
||||
@ -129,12 +142,6 @@ def logentry_to_json(e: log.LogEntry) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def cURL_format_to_json(cURL: str):
|
||||
return {
|
||||
"export": cURL
|
||||
}
|
||||
|
||||
|
||||
class APIError(tornado.web.HTTPError):
|
||||
pass
|
||||
|
||||
@ -212,7 +219,7 @@ class IndexHandler(RequestHandler):
|
||||
def get(self):
|
||||
token = self.xsrf_token # https://github.com/tornadoweb/tornado/issues/645
|
||||
assert token
|
||||
self.render("index.html")
|
||||
self.render("index.html", static=False, version=version.VERSION)
|
||||
|
||||
|
||||
class FilterHelp(RequestHandler):
|
||||
@ -274,8 +281,11 @@ class DumpFlows(RequestHandler):
|
||||
|
||||
|
||||
class ExportFlow(RequestHandler):
|
||||
def post(self, flow_id):
|
||||
self.write(cURL_format_to_json(curl_command(self.flow)))
|
||||
def post(self, flow_id, format):
|
||||
out = export.formats[format](self.flow)
|
||||
self.write({
|
||||
"export": always_str(out, "utf8", "backslashreplace")
|
||||
})
|
||||
|
||||
|
||||
class ClearAll(RequestHandler):
|
||||
@ -448,42 +458,6 @@ class Events(RequestHandler):
|
||||
self.write([logentry_to_json(e) for e in self.master.events.data])
|
||||
|
||||
|
||||
class Settings(RequestHandler):
|
||||
def get(self):
|
||||
self.write(dict(
|
||||
version=version.VERSION,
|
||||
mode=str(self.master.options.mode),
|
||||
intercept_active=self.master.options.intercept_active,
|
||||
intercept=self.master.options.intercept,
|
||||
showhost=self.master.options.showhost,
|
||||
upstream_cert=self.master.options.upstream_cert,
|
||||
rawtcp=self.master.options.rawtcp,
|
||||
http2=self.master.options.http2,
|
||||
websocket=self.master.options.websocket,
|
||||
anticache=self.master.options.anticache,
|
||||
anticomp=self.master.options.anticomp,
|
||||
stickyauth=self.master.options.stickyauth,
|
||||
stickycookie=self.master.options.stickycookie,
|
||||
stream=self.master.options.stream_large_bodies,
|
||||
contentViews=[v.name.replace(' ', '_') for v in contentviews.views],
|
||||
listen_host=self.master.options.listen_host,
|
||||
listen_port=self.master.options.listen_port,
|
||||
server=self.master.options.server,
|
||||
))
|
||||
|
||||
def put(self):
|
||||
update = self.json
|
||||
allowed_options = {
|
||||
"intercept", "showhost", "upstream_cert", "ssl_insecure",
|
||||
"rawtcp", "http2", "websocket", "anticache", "anticomp",
|
||||
"stickycookie", "stickyauth", "stream_large_bodies"
|
||||
}
|
||||
for k in update:
|
||||
if k not in allowed_options:
|
||||
raise APIError(400, f"Unknown setting {k}")
|
||||
self.master.options.update(**update)
|
||||
|
||||
|
||||
class Options(RequestHandler):
|
||||
def get(self):
|
||||
self.write(optmanager.dump_dicts(self.master.options))
|
||||
@ -514,6 +488,17 @@ class DnsRebind(RequestHandler):
|
||||
)
|
||||
|
||||
|
||||
class Conf(RequestHandler):
|
||||
def get(self):
|
||||
conf = {
|
||||
"static": False,
|
||||
"version": version.VERSION,
|
||||
"contentViews": [v.name for v in contentviews.views]
|
||||
}
|
||||
self.write(f"MITMWEB_CONF = {json.dumps(conf)};")
|
||||
self.set_header("content-type", "application/javascript")
|
||||
|
||||
|
||||
class Application(tornado.web.Application):
|
||||
master: "mitmproxy.tools.web.master.WebMaster"
|
||||
|
||||
@ -548,14 +533,14 @@ class Application(tornado.web.Application):
|
||||
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/duplicate", DuplicateFlow),
|
||||
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/replay", ReplayFlow),
|
||||
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/revert", RevertFlow),
|
||||
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/export", ExportFlow),
|
||||
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/export/(?P<format>[a-z][a-z_]+).json", ExportFlow),
|
||||
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/(?P<message>request|response)/content.data", FlowContent),
|
||||
(
|
||||
r"/flows/(?P<flow_id>[0-9a-f\-]+)/(?P<message>request|response)/content/(?P<content_view>[0-9a-zA-Z\-\_]+)(?:\.json)?",
|
||||
FlowContentView),
|
||||
(r"/settings(?:\.json)?", Settings),
|
||||
(r"/clear", ClearAll),
|
||||
(r"/options(?:\.json)?", Options),
|
||||
(r"/options/save", SaveOptions)
|
||||
(r"/options/save", SaveOptions),
|
||||
(r"/conf\.js", Conf),
|
||||
]
|
||||
)
|
||||
|
@ -28,7 +28,6 @@ class WebMaster(master.Master):
|
||||
self.events.sig_refresh.connect(self._sig_events_refresh)
|
||||
|
||||
self.options.changed.connect(self._sig_options_update)
|
||||
self.options.changed.connect(self._sig_settings_update)
|
||||
|
||||
self.addons.add(*addons.default_addons())
|
||||
self.addons.add(
|
||||
@ -93,13 +92,6 @@ class WebMaster(master.Master):
|
||||
data=options_dict
|
||||
)
|
||||
|
||||
def _sig_settings_update(self, options, updated):
|
||||
app.ClientConnection.broadcast(
|
||||
resource="settings",
|
||||
cmd="update",
|
||||
data={k: getattr(options, k) for k in updated}
|
||||
)
|
||||
|
||||
def run(self): # pragma: no cover
|
||||
AsyncIOMainLoop().install()
|
||||
iol = tornado.ioloop.IOLoop.instance()
|
||||
|
2
mitmproxy/tools/web/static/app.css
vendored
2
mitmproxy/tools/web/static/app.css
vendored
File diff suppressed because one or more lines are too long
327
mitmproxy/tools/web/static/app.js
vendored
327
mitmproxy/tools/web/static/app.js
vendored
File diff suppressed because one or more lines are too long
2
mitmproxy/tools/web/static/vendor.css
vendored
2
mitmproxy/tools/web/static/vendor.css
vendored
File diff suppressed because one or more lines are too long
@ -7,8 +7,7 @@
|
||||
<link rel="stylesheet" href="/static/vendor.css"/>
|
||||
<link rel="stylesheet" href="/static/app.css"/>
|
||||
<link rel="icon" href="/static/images/favicon.ico" type="image/x-icon"/>
|
||||
<script src="/static/static.js"></script>
|
||||
<script src="/static/vendor.js"></script>
|
||||
<script src="/conf.js"></script>
|
||||
<script src="/static/app.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -1,12 +1,19 @@
|
||||
import asyncio
|
||||
import io
|
||||
import json
|
||||
import json as _json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import typing
|
||||
from contextlib import redirect_stdout
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from mitmproxy.http import Headers
|
||||
|
||||
if sys.platform == 'win32':
|
||||
# workaround for
|
||||
# https://github.com/tornadoweb/tornado/issues/2751
|
||||
@ -18,7 +25,7 @@ import tornado.testing # noqa
|
||||
from tornado import httpclient # noqa
|
||||
from tornado import websocket # noqa
|
||||
|
||||
from mitmproxy import options # noqa
|
||||
from mitmproxy import options, optmanager # noqa
|
||||
from mitmproxy.test import tflow # noqa
|
||||
from mitmproxy.tools.web import app # noqa
|
||||
from mitmproxy.tools.web import master as webmaster # noqa
|
||||
@ -35,7 +42,7 @@ def no_tornado_logging():
|
||||
logging.getLogger('tornado.general').disabled = False
|
||||
|
||||
|
||||
def json(resp: httpclient.HTTPResponse):
|
||||
def get_json(resp: httpclient.HTTPResponse):
|
||||
return _json.loads(resp.body.decode())
|
||||
|
||||
|
||||
@ -82,8 +89,8 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
|
||||
def test_flows(self):
|
||||
resp = self.fetch("/flows")
|
||||
assert resp.code == 200
|
||||
assert json(resp)[0]["request"]["contentHash"]
|
||||
assert json(resp)[1]["error"]
|
||||
assert get_json(resp)[0]["request"]["contentHash"]
|
||||
assert get_json(resp)[1]["error"]
|
||||
|
||||
def test_flows_dump(self):
|
||||
resp = self.fetch("/flows/dump")
|
||||
@ -251,7 +258,7 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
|
||||
f.revert()
|
||||
|
||||
def test_flow_content_view(self):
|
||||
assert json(self.fetch("/flows/42/request/content/raw")) == {
|
||||
assert get_json(self.fetch("/flows/42/request/content/raw")) == {
|
||||
"lines": [
|
||||
[["text", "content"]]
|
||||
],
|
||||
@ -261,17 +268,10 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
|
||||
def test_events(self):
|
||||
resp = self.fetch("/events")
|
||||
assert resp.code == 200
|
||||
assert json(resp)[0]["level"] == "info"
|
||||
|
||||
def test_settings(self):
|
||||
assert json(self.fetch("/settings"))["mode"] == "regular"
|
||||
|
||||
def test_settings_update(self):
|
||||
assert self.put_json("/settings", {"anticache": True}).code == 200
|
||||
assert self.put_json("/settings", {"wtf": True}).code == 400
|
||||
assert get_json(resp)[0]["level"] == "info"
|
||||
|
||||
def test_options(self):
|
||||
j = json(self.fetch("/options"))
|
||||
j = get_json(self.fetch("/options"))
|
||||
assert type(j) == dict
|
||||
assert type(j['anticache']) == dict
|
||||
|
||||
@ -296,18 +296,8 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
|
||||
self.master.options.anticomp = True
|
||||
|
||||
r1 = yield ws_client.read_message()
|
||||
r2 = yield ws_client.read_message()
|
||||
j1 = _json.loads(r1)
|
||||
j2 = _json.loads(r2)
|
||||
response = dict()
|
||||
response[j1['resource']] = j1
|
||||
response[j2['resource']] = j2
|
||||
assert response['settings'] == {
|
||||
"resource": "settings",
|
||||
"cmd": "update",
|
||||
"data": {"anticomp": True},
|
||||
}
|
||||
assert response['options'] == {
|
||||
response = _json.loads(r1)
|
||||
assert response == {
|
||||
"resource": "options",
|
||||
"cmd": "update",
|
||||
"data": {
|
||||
@ -326,23 +316,69 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
|
||||
ws_client2 = yield websocket.websocket_connect(ws_url)
|
||||
ws_client2.close()
|
||||
|
||||
def _test_generate_tflow_js(self):
|
||||
_tflow = app.flow_to_json(tflow.tflow(resp=True, err=True))
|
||||
def test_generate_tflow_js(self):
|
||||
tf = tflow.tflow(resp=True, err=True)
|
||||
tf.request.trailers = Headers(trailer="qvalue")
|
||||
tf.response.trailers = Headers(trailer="qvalue")
|
||||
|
||||
_tflow = app.flow_to_json(tf)
|
||||
# Set some value as constant, so that _tflow.js would not change every time.
|
||||
_tflow['client_conn']['id'] = "4a18d1a0-50a1-48dd-9aa6-d45d74282939"
|
||||
_tflow['id'] = "d91165be-ca1f-4612-88a9-c0f8696f3e29"
|
||||
_tflow['client_conn']['id'] = "4a18d1a0-50a1-48dd-9aa6-d45d74282939"
|
||||
_tflow['server_conn']['id'] = "f087e7b2-6d0a-41a8-a8f0-e1a4761395f8"
|
||||
_tflow["request"]["trailers"] = [["trailer", "qvalue"]]
|
||||
_tflow["response"]["trailers"] = [["trailer", "qvalue"]]
|
||||
tflow_json = _json.dumps(_tflow, indent=4, sort_keys=True)
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
web_root = os.path.join(here, os.pardir, os.pardir, os.pardir, os.pardir, 'web')
|
||||
tflow_path = os.path.join(web_root, 'src/js/__tests__/ducks/_tflow.js')
|
||||
|
||||
tflow_json = re.sub(
|
||||
r'( {8}"(address|is_replay|alpn_proto_negotiated)":)',
|
||||
r" //@ts-ignore\n\1",
|
||||
tflow_json
|
||||
).replace(": null", ": undefined")
|
||||
|
||||
content = (
|
||||
f"/** Auto-generated by test_app.py:TestApp._test_generate_tflow_js */\n"
|
||||
f"export default function(){{\n"
|
||||
"/** Auto-generated by test_app.py:TestApp._test_generate_tflow_js */\n"
|
||||
"import {HTTPFlow} from '../../flow';\n"
|
||||
"export default function(): HTTPFlow {\n"
|
||||
f" return {tflow_json}\n"
|
||||
f"}}"
|
||||
"}"
|
||||
)
|
||||
(Path(__file__).parent / "../../../../web/src/js/__tests__/ducks/_tflow.ts").write_bytes(
|
||||
content.encode()
|
||||
)
|
||||
|
||||
def test_generate_options_js(self):
|
||||
o = options.Options()
|
||||
m = webmaster.WebMaster(o)
|
||||
opt: optmanager._Option
|
||||
|
||||
def ts_type(t):
|
||||
if t == bool:
|
||||
return "boolean"
|
||||
if t == str:
|
||||
return "string"
|
||||
if t == int:
|
||||
return "number"
|
||||
if t == typing.Sequence[str]:
|
||||
return "string[]"
|
||||
if t == typing.Optional[str]:
|
||||
return "string | undefined"
|
||||
raise RuntimeError(t)
|
||||
|
||||
with redirect_stdout(io.StringIO()) as s:
|
||||
|
||||
print("/** Auto-generated by test_app.py:TestApp.test_generate_options_js */")
|
||||
|
||||
print("export interface OptionsState {")
|
||||
for _, opt in m.options.items():
|
||||
print(f" {opt.name}: {ts_type(opt.typespec)}")
|
||||
print("}")
|
||||
print("")
|
||||
print("export type Option = keyof OptionsState")
|
||||
print("")
|
||||
print("export const defaultState: OptionsState = {")
|
||||
for _, opt in m.options.items():
|
||||
print(f" {opt.name}: {json.dumps(opt.default)},".replace(": null", ": undefined"))
|
||||
print("}")
|
||||
|
||||
(Path(__file__).parent / "../../../../web/src/js/ducks/_options_gen.ts").write_bytes(
|
||||
s.getvalue().encode()
|
||||
)
|
||||
with open(tflow_path, 'w', newline="\n") as f:
|
||||
f.write(content)
|
||||
|
@ -1,22 +1,16 @@
|
||||
# Quick Start
|
||||
|
||||
**Be sure to follow the Development Setup instructions found in the README.md,
|
||||
and activate your virtualenv environment before proceeding.**
|
||||
|
||||
- Run `yarn` to install dependencies
|
||||
- Run `yarn run gulp` to start live-compilation.
|
||||
- Run `mitmweb` and open http://localhost:8081/
|
||||
- Install mitmproxy as described in [`../CONTRIBUTING.md`](../CONTRIBUTING.md)
|
||||
- Run `node --version` to make sure that you have at least Node.js 14 or above. If you are on **Ubuntu <= 20.04**, you
|
||||
need to
|
||||
[upgrade](https://github.com/nodesource/distributions/blob/master/README.md#installation-instructions).
|
||||
- Run `npm install` to install dependencies
|
||||
- Run `npm start` to start live-compilation
|
||||
- Run `mitmweb` after activating your Python virtualenv (see [`../CONTRIBUTING.md`](../CONTRIBUTING.md)).
|
||||
|
||||
## Testing
|
||||
|
||||
- Run `yarn test` to run the test suite.
|
||||
|
||||
|
||||
## Advanced Tools
|
||||
|
||||
- `yarn run gulp` supports live-reloading if you install a matching
|
||||
[browser extension](http://livereload.com/extensions/).
|
||||
- You can debug application state using the [Redux DevTools](https://github.com/reduxjs/redux-devtools).
|
||||
- Run `npm test` to run the test suite.
|
||||
|
||||
## Architecture
|
||||
|
||||
@ -25,3 +19,18 @@ There are two components:
|
||||
- Server: [`mitmproxy/tools/web`](../mitmproxy/tools/web)
|
||||
|
||||
- Client: `web`
|
||||
|
||||
## Contributing
|
||||
|
||||
We very much appreciate any (small) improvements to mitmweb. Please do *not* include the compiled assets in
|
||||
[`mitmproxy/tools/web/static`](https://github.com/mitmproxy/mitmproxy/tree/main/mitmproxy/tools/web/static)
|
||||
in your pull request. Refreshing them on every commit would massively increase repository size. We will update these
|
||||
files before every release.
|
||||
|
||||
## Developer Tools
|
||||
|
||||
- `npm start` supports live-reloading if you install a matching
|
||||
[browser extension](http://livereload.com/extensions/).
|
||||
- You can debug application state using the
|
||||
[React DevTools](https://reactjs.org/blog/2019/08/15/new-react-devtools.html) and
|
||||
[Redux DevTools](https://github.com/reduxjs/redux-devtools) browser extensions.
|
||||
|
@ -1,4 +0,0 @@
|
||||
/* This currently is only used for jest. We use esbuild for actual bundling. */
|
||||
module.exports = {
|
||||
presets: ['@babel/preset-react', '@babel/preset-env', '@babel/preset-typescript'],
|
||||
};
|
@ -6,6 +6,7 @@ const cleanCSS = require('gulp-clean-css');
|
||||
const notify = require("gulp-notify");
|
||||
const compilePeg = require("gulp-peg");
|
||||
const plumber = require("gulp-plumber");
|
||||
const replace = require('gulp-replace');
|
||||
const sourcemaps = require('gulp-sourcemaps');
|
||||
const through = require("through2");
|
||||
|
||||
@ -83,6 +84,9 @@ function peg() {
|
||||
return gulp.src(peg_src, {base: "src/"})
|
||||
.pipe(plumber(handleError))
|
||||
.pipe(compilePeg())
|
||||
.pipe(replace('module.exports = ',
|
||||
'import * as flowutils from "../flow/utils"\n' +
|
||||
'export default '))
|
||||
.pipe(gulp.dest("src/"));
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
process.env.TZ = 'UTC';
|
||||
module.exports = async () => {
|
||||
|
||||
module.exports = {
|
||||
process.env.TZ = 'UTC';
|
||||
|
||||
return {
|
||||
"testEnvironment": "jsdom",
|
||||
"testRegex": "__tests__/.*Spec.(js|ts)x?$",
|
||||
"roots": [
|
||||
@ -15,5 +17,18 @@ module.exports = {
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"src/js/**/*.{js,jsx,ts,tsx}"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.[jt]sx?$": [
|
||||
"esbuild-jest",
|
||||
{
|
||||
"loaders": {
|
||||
".js": "tsx"
|
||||
},
|
||||
"format": "cjs",
|
||||
"sourcemap": true,
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
9696
web/package-lock.json
generated
Normal file
9696
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -8,40 +8,38 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.9.2",
|
||||
"bootstrap": "^3.3.7",
|
||||
"bootstrap": "^3.4.1",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "^5.62.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mock-xmlhttprequest": "^1.1.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^17.0.2",
|
||||
"react-codemirror": "^1.0.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-popper": "^2.2.5",
|
||||
"react-redux": "^7.2.4",
|
||||
"react-test-renderer": "^17.0.2",
|
||||
"redux": "^4.1.0",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-mock-store": "^1.5.4",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"shallowequal": "^1.1.0",
|
||||
"stable": "^0.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.14.3",
|
||||
"@babel/preset-env": "^7.14.4",
|
||||
"@babel/preset-react": "^7.13.13",
|
||||
"@babel/preset-typescript": "^7.13.0",
|
||||
"babel-jest": "^27.0.2",
|
||||
"esbuild": "^0.12.8",
|
||||
"@testing-library/react": "^11.2.7",
|
||||
"@types/jest": "^26.0.23",
|
||||
"@types/redux-mock-store": "^1.0.2",
|
||||
"esbuild": "^0.12.9",
|
||||
"esbuild-jest": "^0.5.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-clean-css": "^4.3.0",
|
||||
"gulp-esbuild": "^0.8.1",
|
||||
"gulp-esbuild": "^0.8.2",
|
||||
"gulp-less": "^4.0.1",
|
||||
"gulp-livereload": "^4.0.2",
|
||||
"gulp-notify": "^4.0.0",
|
||||
"gulp-peg": "^0.2.0",
|
||||
"gulp-plumber": "^1.2.1",
|
||||
"gulp-replace": "^1.1.3",
|
||||
"gulp-sourcemaps": "^3.0.0",
|
||||
"jest": "^27.0.4",
|
||||
"through2": "^4.0.2"
|
||||
|
@ -1,3 +1,9 @@
|
||||
.dropdown-menu > li > a {
|
||||
.dropdown-menu {
|
||||
|
||||
// setting a margin is not compatible with popper.
|
||||
margin: 0 !important;
|
||||
|
||||
> li > a {
|
||||
padding: 3px 10px;
|
||||
}
|
||||
}
|
||||
|
@ -96,10 +96,6 @@
|
||||
|
||||
.fa {
|
||||
line-height: inherit;
|
||||
|
||||
&.pull-right {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.col-tls {
|
||||
@ -115,6 +111,10 @@
|
||||
}
|
||||
|
||||
.col-path {
|
||||
.fa {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.fa-repeat {
|
||||
color: green;
|
||||
}
|
||||
@ -193,4 +193,8 @@
|
||||
.col-quickactions .fa-play {
|
||||
transform: translate(1px, 2px);
|
||||
}
|
||||
|
||||
.col-quickactions .fa-repeat {
|
||||
transform: translate(-0px, 2px);
|
||||
}
|
||||
}
|
||||
|
@ -104,15 +104,6 @@ header {
|
||||
|
||||
.filter-input {
|
||||
margin: 4px 0;
|
||||
|
||||
@media (max-width: @screen-xs-max) {
|
||||
> .form-control, > .input-group-addon, > .input-group-btn > .btn {
|
||||
height: 23.5px;
|
||||
padding: 1px 5px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-input .popover {
|
||||
|
@ -1,26 +0,0 @@
|
||||
jest.mock('react-codemirror')
|
||||
import React from 'react'
|
||||
import renderer from 'react-test-renderer'
|
||||
import CodeEditor from '../../../components/ContentView/CodeEditor'
|
||||
|
||||
describe('CodeEditor Component', () => {
|
||||
let content = "foo content",
|
||||
changeFn = jest.fn(),
|
||||
codeEditor = renderer.create(
|
||||
<CodeEditor content={content} onChange={changeFn}/>
|
||||
),
|
||||
tree = codeEditor.toJSON()
|
||||
|
||||
it('should render correctly', () => {
|
||||
// This actually does not render properly, but getting a full CodeMirror rendering
|
||||
// is cumbersome. This is hopefully good enough.
|
||||
// see: https://github.com/mitmproxy/mitmproxy/pull/2365#discussion_r119766850
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should handle key down', () => {
|
||||
let mockEvent = { stopPropagation: jest.fn() }
|
||||
tree.props.onKeyDown(mockEvent)
|
||||
expect(mockEvent.stopPropagation).toBeCalled()
|
||||
})
|
||||
})
|
@ -0,0 +1,14 @@
|
||||
jest.mock("../../../contrib/CodeMirror")
|
||||
import * as React from 'react';
|
||||
import CodeEditor from '../../../components/ContentView/CodeEditor'
|
||||
import {render} from '@testing-library/react'
|
||||
|
||||
|
||||
test("CodeEditor", async () => {
|
||||
|
||||
const changeFn = jest.fn(),
|
||||
{asFragment} = render(
|
||||
<CodeEditor content="foo" onChange={changeFn}/>
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
});
|
@ -42,7 +42,7 @@ describe('ContentLoader Component', () => {
|
||||
|
||||
it('should handle componentWillReceiveProps', () => {
|
||||
contentLoader.updateContent = jest.fn()
|
||||
contentLoader.componentWillReceiveProps({flow: tflow, message: tflow.request})
|
||||
contentLoader.UNSAFE_componentWillReceiveProps({flow: tflow, message: tflow.request})
|
||||
expect(contentLoader.updateContent).toBeCalled()
|
||||
})
|
||||
|
||||
|
@ -53,7 +53,7 @@ describe('ViewServer Component', () => {
|
||||
|
||||
it('should handle componentWillReceiveProps', () => {
|
||||
// case of fail to parse content
|
||||
let viewSever = TestUtils.renderIntoDocument(
|
||||
let viewServer = TestUtils.renderIntoDocument(
|
||||
<PureViewServer
|
||||
showFullContent={true}
|
||||
maxLines={10}
|
||||
@ -64,10 +64,10 @@ describe('ViewServer Component', () => {
|
||||
content={JSON.stringify({lines: [['k1', 'v1']]})}
|
||||
/>
|
||||
)
|
||||
viewSever.componentWillReceiveProps({...viewSever.props, content: '{foo' })
|
||||
viewServer.UNSAFE_componentWillReceiveProps({...viewServer.props, content: '{foo' })
|
||||
let e = ''
|
||||
try {JSON.parse('{foo') } catch(err){ e = err.message}
|
||||
expect(viewSever.data).toEqual({ description: e, lines: [] })
|
||||
expect(viewServer.data).toEqual({ description: e, lines: [] })
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -1,38 +0,0 @@
|
||||
import React from 'react'
|
||||
import renderer from 'react-test-renderer'
|
||||
import ConnectedComponent, { ViewSelector } from '../../../components/ContentView/ViewSelector'
|
||||
import { Provider } from 'react-redux'
|
||||
import { TStore } from '../../ducks/tutils'
|
||||
|
||||
|
||||
describe('ViewSelector Component', () => {
|
||||
let contentViews = ['Auto', 'Raw', 'Text'],
|
||||
activeView = 'Auto',
|
||||
setContentViewFn = jest.fn(),
|
||||
viewSelector = renderer.create(
|
||||
<ViewSelector contentViews={contentViews} activeView={activeView} setContentView={setContentViewFn}/>
|
||||
),
|
||||
tree = viewSelector.toJSON()
|
||||
|
||||
it('should render correctly', () => {
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should handle click', () => {
|
||||
let mockEvent = { preventDefault: jest.fn() },
|
||||
tab = tree.children[1].children[0].children[1]
|
||||
tab.props.onClick(mockEvent)
|
||||
expect(mockEvent.preventDefault).toBeCalled()
|
||||
})
|
||||
|
||||
it('should connect to state', () => {
|
||||
let store = TStore(),
|
||||
provider = renderer.create(
|
||||
<Provider store={store}>
|
||||
<ConnectedComponent/>
|
||||
</Provider>
|
||||
),
|
||||
tree = provider.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
})
|
@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import renderer from 'react-test-renderer'
|
||||
import ViewSelector from '../../../components/ContentView/ViewSelector'
|
||||
import { Provider } from 'react-redux'
|
||||
import { TStore } from '../../ducks/tutils'
|
||||
|
||||
|
||||
describe('ViewSelector Component', () => {
|
||||
let store = TStore(),
|
||||
viewSelector = renderer.create(
|
||||
<Provider store={store}>
|
||||
<ViewSelector/>
|
||||
</Provider>
|
||||
),
|
||||
tree = viewSelector.toJSON()
|
||||
|
||||
it('should render correctly', () => {
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
})
|
@ -1,8 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CodeEditor Component should render correctly 1`] = `
|
||||
<div
|
||||
className="codeeditor"
|
||||
onKeyDown={[Function]}
|
||||
/>
|
||||
`;
|
@ -0,0 +1,9 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CodeEditor 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="codeeditor"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
@ -4,12 +4,23 @@ 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>
|
||||
edit
|
||||
|
||||
auto
|
||||
|
||||
<span
|
||||
className="caret"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
className="btn btn-default btn-xs"
|
||||
@ -21,21 +32,9 @@ 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>
|
||||
`;
|
||||
|
@ -1,123 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ViewSelector Component should connect to state 1`] = `
|
||||
<div
|
||||
className="dropup pull-left"
|
||||
>
|
||||
<a
|
||||
className="btn btn-default btn-xs"
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span>
|
||||
|
||||
<b>
|
||||
View:
|
||||
</b>
|
||||
|
||||
auto
|
||||
|
||||
<span
|
||||
className="caret"
|
||||
/>
|
||||
|
||||
</span>
|
||||
</a>
|
||||
<ul
|
||||
className="dropdown-menu"
|
||||
role="menu"
|
||||
>
|
||||
<li>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
auto
|
||||
</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
raw
|
||||
</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
text
|
||||
</a>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ViewSelector Component should render correctly 1`] = `
|
||||
<div
|
||||
className="dropup pull-left"
|
||||
>
|
||||
<a
|
||||
className="btn btn-default btn-xs"
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span>
|
||||
|
||||
<b>
|
||||
View:
|
||||
</b>
|
||||
|
||||
auto
|
||||
|
||||
<span
|
||||
className="caret"
|
||||
/>
|
||||
|
||||
</span>
|
||||
</a>
|
||||
<ul
|
||||
className="dropdown-menu"
|
||||
role="menu"
|
||||
>
|
||||
<li>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
auto
|
||||
</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
raw
|
||||
</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
text
|
||||
</a>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1,21 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ViewSelector Component should render correctly 1`] = `
|
||||
<a
|
||||
className="btn btn-default btn-xs pull-left"
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span>
|
||||
<b>
|
||||
View:
|
||||
</b>
|
||||
|
||||
auto
|
||||
|
||||
<span
|
||||
className="caret"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
`;
|
@ -24,7 +24,8 @@ describe('Flowcolumns Components', () => {
|
||||
})
|
||||
|
||||
it('should render IconColumn', () => {
|
||||
let iconColumn = renderer.create(<IconColumn flow={tflow}/>),
|
||||
let tflow = TFlow(),
|
||||
iconColumn = renderer.create(<IconColumn flow={tflow}/>),
|
||||
tree = iconColumn.toJSON()
|
||||
// plain
|
||||
expect(tree).toMatchSnapshot()
|
||||
@ -76,7 +77,8 @@ describe('Flowcolumns Components', () => {
|
||||
})
|
||||
|
||||
it('should render pathColumn', () => {
|
||||
let pathColumn = renderer.create(<PathColumn flow={tflow}/>),
|
||||
let tflow = TFlow(),
|
||||
pathColumn = renderer.create(<PathColumn flow={tflow}/>),
|
||||
tree = pathColumn.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
|
||||
@ -100,14 +102,14 @@ describe('Flowcolumns Components', () => {
|
||||
})
|
||||
|
||||
it('should render SizeColumn', () => {
|
||||
tflow = TFlow()
|
||||
let sizeColumn = renderer.create(<SizeColumn flow={tflow}/>),
|
||||
tree = sizeColumn.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should render TimeColumn', () => {
|
||||
let timeColumn = renderer.create(<TimeColumn flow={tflow}/>),
|
||||
let tflow = TFlow(),
|
||||
timeColumn = renderer.create(<TimeColumn flow={tflow}/>),
|
||||
tree = timeColumn.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
|
||||
|
@ -1,26 +0,0 @@
|
||||
import React from 'react'
|
||||
import renderer from 'react-test-renderer'
|
||||
import FlowRow from '../../../components/FlowTable/FlowRow'
|
||||
import { TFlow, TStore } from '../../ducks/tutils'
|
||||
import { Provider } from 'react-redux'
|
||||
|
||||
describe('FlowRow Component', () => {
|
||||
let tFlow = new TFlow(),
|
||||
selectFn = jest.fn(),
|
||||
store = TStore(),
|
||||
flowRow = renderer.create(
|
||||
<Provider store={store} >
|
||||
<FlowRow flow={tFlow} onSelect={selectFn}/>
|
||||
</Provider>),
|
||||
tree = flowRow.toJSON()
|
||||
|
||||
it('should render correctly', () => {
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should handle click', () => {
|
||||
tree.props.onClick()
|
||||
expect(selectFn).toBeCalledWith(tFlow.id)
|
||||
})
|
||||
|
||||
})
|
21
web/src/js/__tests__/components/FlowTable/FlowRowSpec.tsx
Normal file
21
web/src/js/__tests__/components/FlowTable/FlowRowSpec.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
import FlowRow from '../../../components/FlowTable/FlowRow'
|
||||
import {testState} from '../../ducks/tutils'
|
||||
import {fireEvent, render, screen} from "../../test-utils";
|
||||
import {createAppStore} from "../../../ducks";
|
||||
|
||||
|
||||
test("FlowRow", async () => {
|
||||
const store = createAppStore(testState),
|
||||
tflow2 = store.getState().flows.view[1],
|
||||
{asFragment} = render(<table>
|
||||
<tbody>
|
||||
<FlowRow flow={tflow2} selected highlighted/>
|
||||
</tbody>
|
||||
</table>, {store})
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
expect(store.getState().flows.selected[0]).toBe(store.getState().flows.view[0].id)
|
||||
|
||||
fireEvent.click(screen.getByText("http://address:22/second"))
|
||||
expect(store.getState().flows.selected[0]).toBe(store.getState().flows.view[1].id)
|
||||
})
|
@ -1,35 +1,29 @@
|
||||
import React from 'react'
|
||||
import renderer from 'react-test-renderer'
|
||||
import ConnectedHead, { FlowTableHead } from '../../../components/FlowTable/FlowTableHead'
|
||||
import { Provider } from 'react-redux'
|
||||
import { TStore } from '../../ducks/tutils'
|
||||
import FlowTableHead from '../../../components/FlowTable/FlowTableHead'
|
||||
import {Provider} from 'react-redux'
|
||||
import {TStore} from '../../ducks/tutils'
|
||||
import {fireEvent, render, screen} from "@testing-library/react";
|
||||
import {setSort} from "../../../ducks/flows";
|
||||
|
||||
|
||||
describe('FlowTableHead Component', () => {
|
||||
let sortFn = jest.fn(),
|
||||
store = TStore(),
|
||||
flowTableHead = renderer.create(
|
||||
test("FlowTableHead Component", async () => {
|
||||
|
||||
const store = TStore(),
|
||||
{asFragment} = render(
|
||||
<Provider store={store}>
|
||||
<FlowTableHead setSort={sortFn} sortDesc={true}/>
|
||||
</Provider>),
|
||||
tree =flowTableHead.toJSON()
|
||||
<table>
|
||||
<thead>
|
||||
<FlowTableHead/>
|
||||
</thead>
|
||||
</table>
|
||||
</Provider>
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
|
||||
it('should render correctly', () => {
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
fireEvent.click(screen.getByText("Size"))
|
||||
|
||||
it('should handle click', () => {
|
||||
tree.children[0].props.onClick()
|
||||
expect(sortFn).toBeCalledWith('TLSColumn', false)
|
||||
})
|
||||
|
||||
it('should connect to state', () => {
|
||||
let store = TStore(),
|
||||
provider = renderer.create(
|
||||
<Provider store={store}>
|
||||
<ConnectedHead/>
|
||||
</Provider>),
|
||||
tree = provider.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
expect(store.getActions()).toStrictEqual([
|
||||
setSort("SizeColumn", false)
|
||||
]
|
||||
)
|
||||
})
|
||||
|
@ -110,7 +110,16 @@ exports[`Flowcolumns Components should render QuickActionsColumn 1`] = `
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-fw fa-ellipsis-h"
|
||||
className="fa fa-fw fa-repeat text-primary"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
className="quickaction"
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-fw fa-ellipsis-h text-muted"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
@ -130,15 +139,17 @@ exports[`Flowcolumns Components should render StatusColumn 1`] = `
|
||||
className="col-status"
|
||||
style={
|
||||
Object {
|
||||
"color": "darkred",
|
||||
"color": "darkgreen",
|
||||
}
|
||||
}
|
||||
/>
|
||||
>
|
||||
200
|
||||
</td>
|
||||
`;
|
||||
|
||||
exports[`Flowcolumns Components should render TLSColumn 1`] = `
|
||||
<td
|
||||
className="col-tls col-tls-http"
|
||||
className="col-tls col-tls-https"
|
||||
/>
|
||||
`;
|
||||
|
||||
|
@ -1,68 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FlowRow Component should render correctly 1`] = `
|
||||
<tr
|
||||
className="has-request has-response"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<td
|
||||
className="col-tls col-tls-http"
|
||||
/>
|
||||
<td
|
||||
className="col-icon"
|
||||
>
|
||||
<div
|
||||
className="resource-icon resource-icon-plain"
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
className="col-path"
|
||||
>
|
||||
<i
|
||||
className="fa fa-fw fa-exclamation pull-right"
|
||||
/>
|
||||
http://address:22/path
|
||||
</td>
|
||||
<td
|
||||
className="col-method"
|
||||
>
|
||||
GET
|
||||
</td>
|
||||
<td
|
||||
className="col-status"
|
||||
style={
|
||||
Object {
|
||||
"color": "darkgreen",
|
||||
}
|
||||
}
|
||||
>
|
||||
200
|
||||
</td>
|
||||
<td
|
||||
className="col-size"
|
||||
>
|
||||
14b
|
||||
</td>
|
||||
<td
|
||||
className="col-time"
|
||||
>
|
||||
3s
|
||||
</td>
|
||||
<td
|
||||
className="col-quickactions"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<div>
|
||||
<a
|
||||
className="quickaction"
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-fw fa-ellipsis-h"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
@ -0,0 +1,75 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FlowRow 1`] = `
|
||||
<DocumentFragment>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr
|
||||
class="selected highlighted has-request has-response"
|
||||
>
|
||||
<td
|
||||
class="col-tls col-tls-https"
|
||||
/>
|
||||
<td
|
||||
class="col-icon"
|
||||
>
|
||||
<div
|
||||
class="resource-icon resource-icon-plain"
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
class="col-path"
|
||||
>
|
||||
<i
|
||||
class="fa fa-fw fa-exclamation pull-right"
|
||||
/>
|
||||
http://address:22/second
|
||||
</td>
|
||||
<td
|
||||
class="col-method"
|
||||
>
|
||||
GET
|
||||
</td>
|
||||
<td
|
||||
class="col-status"
|
||||
style="color: darkgreen;"
|
||||
>
|
||||
200
|
||||
</td>
|
||||
<td
|
||||
class="col-size"
|
||||
>
|
||||
14b
|
||||
</td>
|
||||
<td
|
||||
class="col-time"
|
||||
>
|
||||
3s
|
||||
</td>
|
||||
<td
|
||||
class="col-quickactions"
|
||||
>
|
||||
<div>
|
||||
<a
|
||||
class="quickaction"
|
||||
href="#"
|
||||
>
|
||||
<i
|
||||
class="fa fa-fw fa-repeat text-primary"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
class="quickaction"
|
||||
href="#"
|
||||
>
|
||||
<i
|
||||
class="fa fa-fw fa-ellipsis-h text-muted"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</DocumentFragment>
|
||||
`;
|
@ -1,113 +1,46 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FlowTableHead Component should connect to state 1`] = `
|
||||
<tr>
|
||||
exports[`FlowTableHead Component 1`] = `
|
||||
<DocumentFragment>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
className="col-tls"
|
||||
onClick={[Function]}
|
||||
>
|
||||
|
||||
</th>
|
||||
class="col-tls"
|
||||
/>
|
||||
<th
|
||||
className="col-icon"
|
||||
onClick={[Function]}
|
||||
>
|
||||
|
||||
</th>
|
||||
class="col-icon"
|
||||
/>
|
||||
<th
|
||||
className="col-path sort-desc"
|
||||
onClick={[Function]}
|
||||
class="col-path sort-desc"
|
||||
>
|
||||
Path
|
||||
</th>
|
||||
<th
|
||||
className="col-method"
|
||||
onClick={[Function]}
|
||||
class="col-method"
|
||||
>
|
||||
Method
|
||||
</th>
|
||||
<th
|
||||
className="col-status"
|
||||
onClick={[Function]}
|
||||
class="col-status"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
className="col-size"
|
||||
onClick={[Function]}
|
||||
class="col-size"
|
||||
>
|
||||
Size
|
||||
</th>
|
||||
<th
|
||||
className="col-time"
|
||||
onClick={[Function]}
|
||||
class="col-time"
|
||||
>
|
||||
Time
|
||||
</th>
|
||||
<th
|
||||
className="col-quickactions"
|
||||
onClick={[Function]}
|
||||
>
|
||||
|
||||
</th>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
exports[`FlowTableHead Component should render correctly 1`] = `
|
||||
<tr>
|
||||
<th
|
||||
className="col-tls"
|
||||
onClick={[Function]}
|
||||
>
|
||||
|
||||
</th>
|
||||
<th
|
||||
className="col-icon"
|
||||
onClick={[Function]}
|
||||
>
|
||||
|
||||
</th>
|
||||
<th
|
||||
className="col-path"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Path
|
||||
</th>
|
||||
<th
|
||||
className="col-method"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Method
|
||||
</th>
|
||||
<th
|
||||
className="col-status"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
className="col-timestamp"
|
||||
onClick={[Function]}
|
||||
>
|
||||
TimeStamp
|
||||
</th>
|
||||
<th
|
||||
className="col-size"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Size
|
||||
</th>
|
||||
<th
|
||||
className="col-time"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Time
|
||||
</th>
|
||||
<th
|
||||
className="col-quickactions"
|
||||
onClick={[Function]}
|
||||
>
|
||||
|
||||
</th>
|
||||
</tr>
|
||||
class="col-quickactions"
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
@ -1,41 +0,0 @@
|
||||
// jest.mock('../../../ducks/ui/flow')
|
||||
import React from 'react'
|
||||
import renderer from 'react-test-renderer'
|
||||
import ToggleEdit from '../../../components/FlowView/ToggleEdit'
|
||||
import { Provider } from 'react-redux'
|
||||
import { startEdit, stopEdit } from '../../../ducks/ui/flow'
|
||||
import { TFlow, TStore } from '../../ducks/tutils'
|
||||
|
||||
global.fetch = jest.fn()
|
||||
let tflow = new TFlow()
|
||||
|
||||
describe('ToggleEdit Component', () => {
|
||||
let store = TStore(),
|
||||
provider = renderer.create(
|
||||
<Provider store={store}>
|
||||
<ToggleEdit/>
|
||||
</Provider>),
|
||||
tree = provider.toJSON()
|
||||
|
||||
afterEach(() => { store.clearActions() })
|
||||
|
||||
it('should render correctly', () => {
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should handle click on stopEdit', () => {
|
||||
tree.children[0].props.onClick()
|
||||
expect(fetch).toBeCalled()
|
||||
})
|
||||
|
||||
it('should handle click on startEdit', () => {
|
||||
store.getState().ui.flow.modifiedFlow = false
|
||||
let provider = renderer.create(
|
||||
<Provider store={store}>
|
||||
<ToggleEdit/>
|
||||
</Provider>),
|
||||
tree = provider.toJSON()
|
||||
tree.children[0].props.onClick()
|
||||
expect(store.getActions()).toEqual([startEdit(tflow)])
|
||||
})
|
||||
})
|
21
web/src/js/__tests__/components/FlowView/ToggleEditSpec.tsx
Normal file
21
web/src/js/__tests__/components/FlowView/ToggleEditSpec.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
import ToggleEdit from '../../../components/FlowView/ToggleEdit'
|
||||
import {TFlow} from '../../ducks/tutils'
|
||||
import {render} from "../../test-utils"
|
||||
import {fireEvent, screen} from "@testing-library/react";
|
||||
|
||||
let tflow = TFlow();
|
||||
|
||||
test("ToggleEdit", async () => {
|
||||
const {asFragment, store} = render(
|
||||
<ToggleEdit/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByTitle("Edit Flow"));
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
expect(store.getState().ui.flow.modifiedFlow).toBeTruthy();
|
||||
|
||||
fireEvent.click(screen.getByTitle("Finish Edit"));
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
expect(store.getState().ui.flow.modifiedFlow).toBeFalsy();
|
||||
});
|
@ -155,18 +155,6 @@ exports[`Details Component should render correctly 1`] = `
|
||||
TLSv1.2
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<abbr
|
||||
title="ALPN protocol negotiated"
|
||||
>
|
||||
ALPN:
|
||||
</abbr>
|
||||
</td>
|
||||
<td>
|
||||
http/1.1
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Resolved address:
|
||||
|
@ -189,69 +189,19 @@ exports[`Request Component should render correctly 1`] = `
|
||||
<table
|
||||
className="header-table"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
className="header-name"
|
||||
>
|
||||
<div
|
||||
className="inline-input readonly"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "trailer",
|
||||
}
|
||||
}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onInput={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onPaste={[Function]}
|
||||
/>
|
||||
<span
|
||||
className="header-colon"
|
||||
>
|
||||
:
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
className="header-value"
|
||||
>
|
||||
<div
|
||||
className="inline-input readonly"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "qvalue",
|
||||
}
|
||||
}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onInput={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onPaste={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody />
|
||||
</table>
|
||||
</article>
|
||||
<footer>
|
||||
<div
|
||||
className="view-options"
|
||||
>
|
||||
<div
|
||||
className="dropup pull-left"
|
||||
>
|
||||
<a
|
||||
className="btn btn-default btn-xs"
|
||||
className="btn btn-default btn-xs pull-left"
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span>
|
||||
|
||||
<b>
|
||||
View:
|
||||
</b>
|
||||
@ -261,45 +211,8 @@ exports[`Request Component should render correctly 1`] = `
|
||||
<span
|
||||
className="caret"
|
||||
/>
|
||||
|
||||
</span>
|
||||
</a>
|
||||
<ul
|
||||
className="dropdown-menu"
|
||||
role="menu"
|
||||
>
|
||||
<li>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
auto
|
||||
</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
raw
|
||||
</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
text
|
||||
</a>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<a
|
||||
className="btn btn-default btn-xs"
|
||||
@ -542,17 +455,13 @@ exports[`Response Component should render correctly 1`] = `
|
||||
<footer>
|
||||
<div
|
||||
className="view-options"
|
||||
>
|
||||
<div
|
||||
className="dropup pull-left"
|
||||
>
|
||||
<a
|
||||
className="btn btn-default btn-xs"
|
||||
className="btn btn-default btn-xs pull-left"
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span>
|
||||
|
||||
<b>
|
||||
View:
|
||||
</b>
|
||||
@ -562,45 +471,8 @@ exports[`Response Component should render correctly 1`] = `
|
||||
<span
|
||||
className="caret"
|
||||
/>
|
||||
|
||||
</span>
|
||||
</a>
|
||||
<ul
|
||||
className="dropdown-menu"
|
||||
role="menu"
|
||||
>
|
||||
<li>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
auto
|
||||
</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
raw
|
||||
</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
text
|
||||
</a>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<a
|
||||
className="btn btn-default btn-xs"
|
||||
|
@ -1,17 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ToggleEdit Component should render correctly 1`] = `
|
||||
<div
|
||||
className="edit-flow-container"
|
||||
>
|
||||
<a
|
||||
className="edit-flow"
|
||||
onClick={[Function]}
|
||||
title="Finish Edit"
|
||||
>
|
||||
<i
|
||||
className="fa fa-check"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1,35 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ToggleEdit 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="edit-flow-container"
|
||||
>
|
||||
<a
|
||||
class="edit-flow"
|
||||
title="Finish Edit"
|
||||
>
|
||||
<i
|
||||
class="fa fa-check"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`ToggleEdit 2`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="edit-flow-container"
|
||||
>
|
||||
<a
|
||||
class="edit-flow"
|
||||
title="Edit Flow"
|
||||
>
|
||||
<i
|
||||
class="fa fa-pencil"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
@ -1,55 +0,0 @@
|
||||
import React from 'react'
|
||||
import renderer from 'react-test-renderer'
|
||||
import ConnectedIndicator, { ConnectionIndicator } from '../../../components/Header/ConnectionIndicator'
|
||||
import { ConnectionState } from '../../../ducks/connection'
|
||||
import { Provider } from 'react-redux'
|
||||
import { TStore } from '../../ducks/tutils'
|
||||
|
||||
describe('ConnectionIndicator Component', () => {
|
||||
|
||||
it('should render INIT', () => {
|
||||
let connectionIndicator = renderer.create(
|
||||
<ConnectionIndicator state={ConnectionState.INIT}/>),
|
||||
tree = connectionIndicator.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should render FETCHING', () => {
|
||||
let connectionIndicator = renderer.create(
|
||||
<ConnectionIndicator state={ConnectionState.FETCHING}/>),
|
||||
tree = connectionIndicator.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should render ESTABLISHED', () => {
|
||||
let connectionIndicator = renderer.create(
|
||||
<ConnectionIndicator state={ConnectionState.ESTABLISHED}/>),
|
||||
tree = connectionIndicator.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should render ERROR', () => {
|
||||
let connectionIndicator = renderer.create(
|
||||
<ConnectionIndicator state={ConnectionState.ERROR} message="foo"/>),
|
||||
tree = connectionIndicator.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should render OFFLINE', () => {
|
||||
let connectionIndicator = renderer.create(
|
||||
<ConnectionIndicator state={ConnectionState.OFFLINE} />),
|
||||
tree = connectionIndicator.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should connect to state', () => {
|
||||
let store = TStore(),
|
||||
provider = renderer.create(
|
||||
<Provider store={store}>
|
||||
<ConnectedIndicator/>
|
||||
</Provider>),
|
||||
tree = provider.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
import ConnectionIndicator from '../../../components/Header/ConnectionIndicator'
|
||||
import * as connectionActions from '../../../ducks/connection'
|
||||
import {render} from "../../test-utils"
|
||||
|
||||
|
||||
test("ConnectionIndicator", async () => {
|
||||
const {asFragment, store} = render(<ConnectionIndicator/>);
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
|
||||
store.dispatch(connectionActions.startFetching())
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
|
||||
store.dispatch(connectionActions.connectionEstablished())
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
|
||||
store.dispatch(connectionActions.connectionError("wat"))
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
|
||||
store.dispatch(connectionActions.setOffline())
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
});
|
@ -1,52 +1,20 @@
|
||||
import React from 'react'
|
||||
import renderer from 'react-test-renderer'
|
||||
import { FileMenu } from '../../../components/Header/FileMenu'
|
||||
|
||||
global.confirm = jest.fn( s => true )
|
||||
import FileMenu from '../../../components/Header/FileMenu'
|
||||
import {Provider} from "react-redux";
|
||||
import {TStore} from "../../ducks/tutils";
|
||||
|
||||
describe('FileMenu Component', () => {
|
||||
let clearFn = jest.fn(),
|
||||
loadFn = jest.fn(),
|
||||
saveFn = jest.fn(),
|
||||
openModalFn = jest.fn(),
|
||||
mockEvent = {
|
||||
preventDefault: jest.fn(),
|
||||
target: { files: ["foo", "bar "] }
|
||||
},
|
||||
createNodeMock = () => { return { click: jest.fn() }},
|
||||
|
||||
let store = TStore(),
|
||||
fileMenu = renderer.create(
|
||||
<FileMenu
|
||||
clearFlows={clearFn}
|
||||
loadFlows={loadFn}
|
||||
saveFlows={saveFn}
|
||||
openModal={openModalFn}
|
||||
/>,
|
||||
{ createNodeMock }),
|
||||
<Provider store={store}>
|
||||
<FileMenu/>
|
||||
</Provider>
|
||||
),
|
||||
tree = fileMenu.toJSON()
|
||||
|
||||
it('should render correctly', () => {
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
||||
let ul = tree.children[1]
|
||||
|
||||
it('should clear flows', () => {
|
||||
let a = ul.children[0].children[1]
|
||||
a.props.onClick(mockEvent)
|
||||
expect(mockEvent.preventDefault).toBeCalled()
|
||||
expect(clearFn).toBeCalled()
|
||||
})
|
||||
|
||||
it('should load flows', () => {
|
||||
let fileChooser = ul.children[1].children[1],
|
||||
input = fileChooser.children[2]
|
||||
input.props.onChange(mockEvent)
|
||||
expect(loadFn).toBeCalledWith("foo")
|
||||
})
|
||||
|
||||
it('should save flows', () => {
|
||||
let a = ul.children[2].children[1]
|
||||
a.props.onClick(mockEvent)
|
||||
expect(saveFn).toBeCalled()
|
||||
})
|
||||
})
|
||||
|
@ -1,26 +1,8 @@
|
||||
jest.mock('../../../ducks/settings')
|
||||
|
||||
import React from 'react'
|
||||
import renderer from 'react-test-renderer'
|
||||
import MainMenu, { setIntercept } from '../../../components/Header/MainMenu'
|
||||
import { Provider } from 'react-redux'
|
||||
import { update as updateSettings } from '../../../ducks/settings'
|
||||
import { TStore } from '../../ducks/tutils'
|
||||
import MainMenu from '../../../components/Header/MainMenu'
|
||||
import {render} from "../../test-utils"
|
||||
|
||||
describe('MainMenu Component', () => {
|
||||
let store = TStore()
|
||||
|
||||
it('should render and connect to state', () => {
|
||||
let provider = renderer.create(
|
||||
<Provider store={store}>
|
||||
<MainMenu/>
|
||||
</Provider>),
|
||||
tree = provider.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should handle change on interceptInput', () => {
|
||||
setIntercept('foo')
|
||||
expect(updateSettings).toBeCalledWith({ intercept: 'foo' })
|
||||
})
|
||||
test("MainMenu", () => {
|
||||
const {asFragment} = render(<MainMenu/>);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
})
|
||||
|
@ -1,11 +1,10 @@
|
||||
import React from 'react'
|
||||
import renderer from 'react-test-renderer'
|
||||
import { MenuToggle, SettingsToggle, EventlogToggle } from '../../../components/Header/MenuToggle'
|
||||
import { Provider } from 'react-redux'
|
||||
import { REQUEST_UPDATE } from '../../../ducks/settings'
|
||||
import { TStore } from '../../ducks/tutils'
|
||||
|
||||
global.fetch = jest.fn()
|
||||
import {EventlogToggle, MenuToggle, OptionsToggle} from '../../../components/Header/MenuToggle'
|
||||
import {Provider} from 'react-redux'
|
||||
import {TStore} from '../../ducks/tutils'
|
||||
import * as optionsEditorActions from "../../../ducks/ui/optionsEditor"
|
||||
import {fireEvent, render, screen} from "../../test-utils"
|
||||
|
||||
describe('MenuToggle Component', () => {
|
||||
it('should render correctly', () => {
|
||||
@ -19,37 +18,26 @@ describe('MenuToggle Component', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('SettingToggle Component', () => {
|
||||
let store = TStore(),
|
||||
provider = renderer.create(
|
||||
<Provider store={store}>
|
||||
<SettingsToggle setting='anticache'>
|
||||
<p>foo children</p>
|
||||
</SettingsToggle>
|
||||
</Provider>),
|
||||
tree = provider.toJSON()
|
||||
test("OptionsToggle", async () => {
|
||||
const store = TStore(),
|
||||
{asFragment} = render(
|
||||
<OptionsToggle name='anticache'>toggle anticache</OptionsToggle>,
|
||||
{store}
|
||||
);
|
||||
|
||||
it('should render and connect to state', () => {
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
fireEvent.click(screen.getByText("toggle anticache"));
|
||||
expect(store.getActions()).toEqual([optionsEditorActions.startUpdate("anticache", true)])
|
||||
});
|
||||
|
||||
it('should handle change', () => {
|
||||
let menuToggle = tree.children[0].children[0]
|
||||
menuToggle.props.onChange()
|
||||
expect(store.getActions()).toEqual([{ type: REQUEST_UPDATE }])
|
||||
})
|
||||
})
|
||||
|
||||
describe('EventlogToggle Component', () => {
|
||||
let store = TStore(),
|
||||
changFn = jest.fn(),
|
||||
provider = renderer.create(
|
||||
<Provider store={store}>
|
||||
<EventlogToggle value={false} onChange={changFn}/>
|
||||
</Provider>
|
||||
),
|
||||
tree = provider.toJSON()
|
||||
it('should render and connect to state', () => {
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
test("EventlogToggle", async () => {
|
||||
const {asFragment, store} = render(
|
||||
<EventlogToggle/>
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
expect(store.getState().eventLog.visible).toBeTruthy();
|
||||
fireEvent.click(screen.getByText("Display Event Log"));
|
||||
|
||||
expect(store.getState().eventLog.visible).toBeFalsy();
|
||||
})
|
||||
|
@ -1,50 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ConnectionIndicator Component should connect to state 1`] = `
|
||||
<span
|
||||
className="connection-indicator established"
|
||||
>
|
||||
connected
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`ConnectionIndicator Component should render ERROR 1`] = `
|
||||
<span
|
||||
className="connection-indicator error"
|
||||
title="foo"
|
||||
>
|
||||
connection lost
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`ConnectionIndicator Component should render ESTABLISHED 1`] = `
|
||||
<span
|
||||
className="connection-indicator established"
|
||||
>
|
||||
connected
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`ConnectionIndicator Component should render FETCHING 1`] = `
|
||||
<span
|
||||
className="connection-indicator fetching"
|
||||
>
|
||||
fetching data…
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`ConnectionIndicator Component should render INIT 1`] = `
|
||||
<span
|
||||
className="connection-indicator init"
|
||||
>
|
||||
connecting…
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`ConnectionIndicator Component should render OFFLINE 1`] = `
|
||||
<span
|
||||
className="connection-indicator offline"
|
||||
>
|
||||
offline
|
||||
</span>
|
||||
`;
|
@ -0,0 +1,52 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ConnectionIndicator 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
class="connection-indicator established"
|
||||
>
|
||||
connected
|
||||
</span>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`ConnectionIndicator 2`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
class="connection-indicator fetching"
|
||||
>
|
||||
fetching data…
|
||||
</span>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`ConnectionIndicator 3`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
class="connection-indicator established"
|
||||
>
|
||||
connected
|
||||
</span>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`ConnectionIndicator 4`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
class="connection-indicator error"
|
||||
title="wat"
|
||||
>
|
||||
connection lost
|
||||
</span>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`ConnectionIndicator 5`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
class="connection-indicator offline"
|
||||
>
|
||||
offline
|
||||
</span>
|
||||
</DocumentFragment>
|
||||
`;
|
@ -1,80 +1,11 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FileMenu Component should render correctly 1`] = `
|
||||
<div
|
||||
className="dropdown pull-left"
|
||||
<a
|
||||
className="pull-left special"
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<a
|
||||
className="special"
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
mitmproxy
|
||||
</a>
|
||||
<ul
|
||||
className="dropdown-menu"
|
||||
role="menu"
|
||||
>
|
||||
<li>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-fw fa-trash"
|
||||
/>
|
||||
Clear All
|
||||
</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-fw fa-folder-open"
|
||||
/>
|
||||
Open...
|
||||
<input
|
||||
className="hidden"
|
||||
onChange={[Function]}
|
||||
type="file"
|
||||
/>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-fw fa-floppy-o"
|
||||
/>
|
||||
Save...
|
||||
</a>
|
||||
|
||||
</li>
|
||||
<li>
|
||||
|
||||
<hr
|
||||
className="divider"
|
||||
/>
|
||||
<a
|
||||
href="http://mitm.it/"
|
||||
target="_blank"
|
||||
>
|
||||
<i
|
||||
className="fa fa-fw fa-external-link"
|
||||
/>
|
||||
Install Certificates...
|
||||
</a>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
|
@ -1,122 +1,99 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MainMenu Component should render and connect to state 1`] = `
|
||||
<div
|
||||
className="main-menu"
|
||||
>
|
||||
exports[`MainMenu 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
className="menu-group"
|
||||
class="main-menu"
|
||||
>
|
||||
<div
|
||||
className="menu-content"
|
||||
class="menu-group"
|
||||
>
|
||||
<div
|
||||
className="filter-input input-group"
|
||||
class="menu-content"
|
||||
>
|
||||
<div
|
||||
class="filter-input input-group"
|
||||
>
|
||||
<span
|
||||
className="input-group-addon"
|
||||
class="input-group-addon"
|
||||
>
|
||||
<i
|
||||
className="fa fa-fw fa-search"
|
||||
style={
|
||||
Object {
|
||||
"color": "black",
|
||||
}
|
||||
}
|
||||
class="fa fa-fw fa-search"
|
||||
style="color: black;"
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
className="form-control"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
class="form-control"
|
||||
placeholder="Search"
|
||||
type="text"
|
||||
value="~u foo"
|
||||
value="~d address"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="filter-input input-group"
|
||||
class="filter-input input-group"
|
||||
>
|
||||
<span
|
||||
className="input-group-addon"
|
||||
class="input-group-addon"
|
||||
>
|
||||
<i
|
||||
className="fa fa-fw fa-tag"
|
||||
style={
|
||||
Object {
|
||||
"color": "hsl(48, 100%, 50%)",
|
||||
}
|
||||
}
|
||||
class="fa fa-fw fa-tag"
|
||||
style="color: rgb(0, 0, 0);"
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
className="form-control"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
class="form-control"
|
||||
placeholder="Highlight"
|
||||
type="text"
|
||||
value="~a bar"
|
||||
value="~u /path"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="menu-legend"
|
||||
class="menu-legend"
|
||||
>
|
||||
Find
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="menu-group"
|
||||
class="menu-group"
|
||||
>
|
||||
<div
|
||||
className="menu-content"
|
||||
class="menu-content"
|
||||
>
|
||||
<div
|
||||
className="filter-input input-group"
|
||||
class="filter-input input-group"
|
||||
>
|
||||
<span
|
||||
className="input-group-addon"
|
||||
class="input-group-addon"
|
||||
>
|
||||
<i
|
||||
className="fa fa-fw fa-pause"
|
||||
style={
|
||||
Object {
|
||||
"color": "hsl(208, 56%, 53%)",
|
||||
}
|
||||
}
|
||||
class="fa fa-fw fa-pause"
|
||||
style="color: rgb(68, 68, 68);"
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
className="form-control"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
class="form-control"
|
||||
placeholder="Intercept"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="btn-sm btn btn-default"
|
||||
onClick={[Function]}
|
||||
class="btn-sm btn btn-default"
|
||||
title="[a]ccept all"
|
||||
>
|
||||
<i
|
||||
className="fa fa-fw fa-forward text-success"
|
||||
class="fa fa-fw fa-forward text-success"
|
||||
/>
|
||||
Resume All
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="menu-legend"
|
||||
class="menu-legend"
|
||||
>
|
||||
Intercept
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
@ -1,18 +1,19 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EventlogToggle Component should render and connect to state 1`] = `
|
||||
<div
|
||||
className="menu-entry"
|
||||
>
|
||||
exports[`EventlogToggle 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="menu-entry"
|
||||
>
|
||||
<label>
|
||||
<input
|
||||
checked={true}
|
||||
onChange={[Function]}
|
||||
checked=""
|
||||
type="checkbox"
|
||||
/>
|
||||
Display Event Log
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`MenuToggle Component should render correctly 1`] = `
|
||||
@ -32,19 +33,17 @@ exports[`MenuToggle Component should render correctly 1`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SettingToggle Component should render and connect to state 1`] = `
|
||||
<div
|
||||
className="menu-entry"
|
||||
>
|
||||
exports[`OptionsToggle 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="menu-entry"
|
||||
>
|
||||
<label>
|
||||
<input
|
||||
checked={true}
|
||||
onChange={[Function]}
|
||||
type="checkbox"
|
||||
/>
|
||||
<p>
|
||||
foo children
|
||||
</p>
|
||||
toggle anticache
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
@ -39,7 +39,7 @@ exports[`OptionMenu Component should render correctly 1`] = `
|
||||
>
|
||||
<label>
|
||||
<input
|
||||
checked={true}
|
||||
checked={false}
|
||||
onChange={[Function]}
|
||||
type="checkbox"
|
||||
/>
|
||||
|
@ -1,30 +0,0 @@
|
||||
import React from 'react'
|
||||
import renderer from 'react-test-renderer'
|
||||
import Modal from '../../../components/Modal/Modal'
|
||||
import { Provider } from 'react-redux'
|
||||
import { TStore } from '../../ducks/tutils'
|
||||
|
||||
describe('Modal Component', () => {
|
||||
let store = TStore()
|
||||
|
||||
it('should render correctly', () => {
|
||||
// hide modal by default
|
||||
let provider = renderer.create(
|
||||
<Provider store={store}>
|
||||
<Modal/>
|
||||
</Provider>
|
||||
),
|
||||
tree = provider.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
|
||||
// option modal show up
|
||||
store.getState().ui.modal.activeModal = 'OptionModal'
|
||||
provider = renderer.create(
|
||||
<Provider store={store}>
|
||||
<Modal/>
|
||||
</Provider>
|
||||
)
|
||||
tree = provider.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
})
|
13
web/src/js/__tests__/components/Modal/ModalSpec.tsx
Normal file
13
web/src/js/__tests__/components/Modal/ModalSpec.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
import Modal from '../../../components/Modal/Modal'
|
||||
import {render} from "../../test-utils"
|
||||
import {setActiveModal} from "../../../ducks/ui/modal";
|
||||
|
||||
test("Modal Component", async () => {
|
||||
const {asFragment, store} = render(<Modal/>);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
store.dispatch(setActiveModal("OptionModal"));
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
})
|
@ -1,255 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Modal Component should render correctly 1`] = `<div />`;
|
||||
|
||||
exports[`Modal Component should render correctly 2`] = `
|
||||
<div>
|
||||
<div
|
||||
className="modal-backdrop fade in"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="options"
|
||||
className="modal modal-visible"
|
||||
id="optionsModal"
|
||||
role="dialog"
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div
|
||||
className="modal-dialog modal-lg"
|
||||
role="document"
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<button
|
||||
className="close"
|
||||
data-dismiss="modal"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
className="fa fa-fw fa-times"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
className="modal-title"
|
||||
>
|
||||
<h4>
|
||||
Options
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div
|
||||
className="form-horizontal"
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="col-xs-6"
|
||||
>
|
||||
<label
|
||||
htmlFor="booleanOption"
|
||||
>
|
||||
booleanOption
|
||||
</label>
|
||||
<div
|
||||
className="help-block small"
|
||||
>
|
||||
foo
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="col-xs-6"
|
||||
>
|
||||
<div
|
||||
className=""
|
||||
>
|
||||
<div
|
||||
className="checkbox"
|
||||
>
|
||||
<label>
|
||||
<input
|
||||
checked={false}
|
||||
name="booleanOption"
|
||||
onChange={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="checkbox"
|
||||
/>
|
||||
Enable
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="col-xs-6"
|
||||
>
|
||||
<label
|
||||
htmlFor="choiceOption"
|
||||
>
|
||||
choiceOption
|
||||
</label>
|
||||
<div
|
||||
className="help-block small"
|
||||
>
|
||||
foo
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="col-xs-6"
|
||||
>
|
||||
<div
|
||||
className=""
|
||||
>
|
||||
<select
|
||||
className="form-control"
|
||||
name="choiceOption"
|
||||
onChange={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
value="b"
|
||||
>
|
||||
<option
|
||||
value="a"
|
||||
>
|
||||
a
|
||||
</option>
|
||||
<option
|
||||
value="b"
|
||||
>
|
||||
b
|
||||
</option>
|
||||
<option
|
||||
value="c"
|
||||
>
|
||||
c
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div
|
||||
className="small"
|
||||
>
|
||||
Default:
|
||||
<strong>
|
||||
|
||||
a
|
||||
|
||||
</strong>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="col-xs-6"
|
||||
>
|
||||
<label
|
||||
htmlFor="intOption"
|
||||
>
|
||||
intOption
|
||||
</label>
|
||||
<div
|
||||
className="help-block small"
|
||||
>
|
||||
foo
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="col-xs-6"
|
||||
>
|
||||
<div
|
||||
className=""
|
||||
>
|
||||
<input
|
||||
className="form-control"
|
||||
name="intOption"
|
||||
onChange={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="number"
|
||||
value={1}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="small"
|
||||
>
|
||||
Default:
|
||||
<strong>
|
||||
|
||||
0
|
||||
|
||||
</strong>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="col-xs-6"
|
||||
>
|
||||
<label
|
||||
htmlFor="strOption"
|
||||
>
|
||||
strOption
|
||||
</label>
|
||||
<div
|
||||
className="help-block small"
|
||||
>
|
||||
foo
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="col-xs-6"
|
||||
>
|
||||
<div
|
||||
className="has-error"
|
||||
>
|
||||
<input
|
||||
className="form-control"
|
||||
name="strOption"
|
||||
onChange={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="text"
|
||||
value="str content"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="small text-danger"
|
||||
/>
|
||||
<div
|
||||
className="small"
|
||||
>
|
||||
Default:
|
||||
<strong>
|
||||
|
||||
null
|
||||
|
||||
</strong>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1,209 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Modal Component 1`] = `
|
||||
<DocumentFragment>
|
||||
<div />
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`Modal Component 2`] = `
|
||||
<DocumentFragment>
|
||||
<div>
|
||||
<div
|
||||
class="modal-backdrop fade in"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="options"
|
||||
class="modal modal-visible"
|
||||
id="optionsModal"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="modal-dialog modal-lg"
|
||||
role="document"
|
||||
>
|
||||
<div
|
||||
class="modal-content"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="modal-header"
|
||||
>
|
||||
<button
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="fa fa-fw fa-times"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="modal-title"
|
||||
>
|
||||
<h4>
|
||||
Options
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="modal-body"
|
||||
>
|
||||
<div
|
||||
class="form-horizontal"
|
||||
>
|
||||
<div
|
||||
class="form-group"
|
||||
>
|
||||
<div
|
||||
class="col-xs-6"
|
||||
>
|
||||
<label
|
||||
for="anticache"
|
||||
>
|
||||
anticache
|
||||
</label>
|
||||
<div
|
||||
class="help-block small"
|
||||
>
|
||||
Strip out request headers that might cause the server to return 304-not-modified.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="col-xs-6"
|
||||
>
|
||||
<div
|
||||
class=""
|
||||
>
|
||||
<div
|
||||
class="checkbox"
|
||||
>
|
||||
<label>
|
||||
<input
|
||||
name="anticache"
|
||||
type="checkbox"
|
||||
/>
|
||||
Enable
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="form-group"
|
||||
>
|
||||
<div
|
||||
class="col-xs-6"
|
||||
>
|
||||
<label
|
||||
for="body_size_limit"
|
||||
>
|
||||
body_size_limit
|
||||
</label>
|
||||
<div
|
||||
class="help-block small"
|
||||
>
|
||||
Byte size limit of HTTP request and response bodies. Understands k/m/g suffixes, i.e. 3m for 3 megabytes.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="col-xs-6"
|
||||
>
|
||||
<div
|
||||
class=""
|
||||
>
|
||||
<input
|
||||
class="form-control"
|
||||
name="body_size_limit"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="form-group"
|
||||
>
|
||||
<div
|
||||
class="col-xs-6"
|
||||
>
|
||||
<label
|
||||
for="connection_strategy"
|
||||
>
|
||||
connection_strategy
|
||||
</label>
|
||||
<div
|
||||
class="help-block small"
|
||||
>
|
||||
Determine when server connections should be established. When set to lazy, mitmproxy tries to defer establishing an upstream connection as long as possible. This makes it possible to use server replay while being offline. When set to eager, mitmproxy can detect protocols with server-side greetings, as well as accurately mirror TLS ALPN negotiation.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="col-xs-6"
|
||||
>
|
||||
<div
|
||||
class=""
|
||||
>
|
||||
<select
|
||||
class="form-control"
|
||||
name="connection_strategy"
|
||||
>
|
||||
<option
|
||||
value="eager"
|
||||
>
|
||||
eager
|
||||
</option>
|
||||
<option
|
||||
value="lazy"
|
||||
>
|
||||
lazy
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="form-group"
|
||||
>
|
||||
<div
|
||||
class="col-xs-6"
|
||||
>
|
||||
<label
|
||||
for="listen_port"
|
||||
>
|
||||
listen_port
|
||||
</label>
|
||||
<div
|
||||
class="help-block small"
|
||||
>
|
||||
Proxy service port.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="col-xs-6"
|
||||
>
|
||||
<div
|
||||
class=""
|
||||
>
|
||||
<input
|
||||
class="form-control"
|
||||
name="listen_port"
|
||||
type="number"
|
||||
value="8080"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="modal-footer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
@ -1,32 +0,0 @@
|
||||
import React from 'react'
|
||||
import renderer from 'react-test-renderer'
|
||||
import Dropdown, { Divider } from '../../../components/common/Dropdown'
|
||||
|
||||
describe('Dropdown Component', () => {
|
||||
let dropdown = renderer.create(<Dropdown text="open me">
|
||||
<a href="#">1</a>
|
||||
<a href="#">2</a>
|
||||
</Dropdown>)
|
||||
|
||||
it('should render correctly', () => {
|
||||
let tree = dropdown.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should handle open/close action', () => {
|
||||
let tree = dropdown.toJSON(),
|
||||
e = { preventDefault: jest.fn(), stopPropagation: jest.fn() }
|
||||
tree.children[0].props.onClick(e)
|
||||
expect(tree).toMatchSnapshot()
|
||||
|
||||
// click action when the state is open
|
||||
tree.children[0].props.onClick(e)
|
||||
|
||||
// open again
|
||||
tree.children[0].props.onClick(e)
|
||||
|
||||
// close
|
||||
document.body.click()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
})
|
53
web/src/js/__tests__/components/common/DropdownSpec.tsx
Normal file
53
web/src/js/__tests__/components/common/DropdownSpec.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import React from "react"
|
||||
import Dropdown, {Divider, MenuItem, SubMenu} from '../../../components/common/Dropdown'
|
||||
import {fireEvent, render, screen, waitFor} from '@testing-library/react'
|
||||
|
||||
|
||||
test('Dropdown', async () => {
|
||||
let onOpen = jest.fn();
|
||||
const {asFragment} = render(
|
||||
<Dropdown text="open me" onOpen={onOpen}>
|
||||
<MenuItem onClick={() => 0}>click me</MenuItem>
|
||||
<Divider/>
|
||||
<MenuItem onClick={() => 0}>click me</MenuItem>
|
||||
</Dropdown>
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
|
||||
fireEvent.click(screen.getByText("open me"))
|
||||
await waitFor(() => expect(onOpen).toBeCalledWith(true))
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
|
||||
/*
|
||||
onOpen.mockClear()
|
||||
fireEvent.click(document.body)
|
||||
await waitFor(() => expect(onOpen).toBeCalledWith(false))
|
||||
*/
|
||||
})
|
||||
|
||||
test('SubMenu', async () => {
|
||||
const {asFragment} = render(
|
||||
<SubMenu title="submenu">
|
||||
<MenuItem onClick={() => 0}>click me</MenuItem>
|
||||
</SubMenu>
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
|
||||
fireEvent.mouseEnter(screen.getByText("submenu"))
|
||||
await waitFor(() => screen.getByText("click me"))
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
|
||||
fireEvent.mouseLeave(screen.getByText("submenu"))
|
||||
expect(screen.queryByText("click me")).toBeNull()
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('MenuItem', async () => {
|
||||
let click = jest.fn();
|
||||
const {asFragment} = render(
|
||||
<MenuItem onClick={click}>click me</MenuItem>
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
fireEvent.click(screen.getByText("click me"))
|
||||
expect(click).toBeCalled()
|
||||
})
|
@ -1,38 +0,0 @@
|
||||
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()
|
||||
})
|
||||
})
|
13
web/src/js/__tests__/components/common/FileChooserSpec.tsx
Normal file
13
web/src/js/__tests__/components/common/FileChooserSpec.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
import FileChooser from '../../../components/common/FileChooser'
|
||||
import {render} from '@testing-library/react'
|
||||
|
||||
|
||||
test("FileChooser", async () => {
|
||||
const {asFragment} = render(
|
||||
<FileChooser icon="play" text="open" onOpenFile={() => 0}/>
|
||||
);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
|
||||
})
|
@ -1,162 +0,0 @@
|
||||
// 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>
|
||||
`;
|
@ -0,0 +1,118 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Dropdown 1`] = `
|
||||
<DocumentFragment>
|
||||
<a
|
||||
class=""
|
||||
href="#"
|
||||
>
|
||||
open me
|
||||
</a>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`Dropdown 2`] = `
|
||||
<DocumentFragment>
|
||||
<a
|
||||
class="open"
|
||||
href="#"
|
||||
>
|
||||
open me
|
||||
</a>
|
||||
<ul
|
||||
class="dropdown-menu show"
|
||||
data-popper-escaped="true"
|
||||
data-popper-placement="bottom"
|
||||
data-popper-reference-hidden="true"
|
||||
style="position: absolute; left: 0px; top: 0px; transform: translate(0px, 0px);"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
>
|
||||
click me
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
class="divider"
|
||||
role="separator"
|
||||
/>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
>
|
||||
click me
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`MenuItem 1`] = `
|
||||
<DocumentFragment>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
>
|
||||
click me
|
||||
</a>
|
||||
</li>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`SubMenu 1`] = `
|
||||
<DocumentFragment>
|
||||
<li>
|
||||
<a>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="fa fa-caret-right pull-right"
|
||||
/>
|
||||
submenu
|
||||
</a>
|
||||
</li>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`SubMenu 2`] = `
|
||||
<DocumentFragment>
|
||||
<li>
|
||||
<a>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="fa fa-caret-right pull-right"
|
||||
/>
|
||||
submenu
|
||||
</a>
|
||||
<ul
|
||||
class="dropdown-menu show"
|
||||
data-popper-escaped="true"
|
||||
data-popper-placement="right-start"
|
||||
data-popper-reference-hidden="true"
|
||||
style="position: absolute; left: 0px; top: 0px; transform: translate(0px, 0px);"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
>
|
||||
click me
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`SubMenu 3`] = `
|
||||
<DocumentFragment>
|
||||
<li>
|
||||
<a>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="fa fa-caret-right pull-right"
|
||||
/>
|
||||
submenu
|
||||
</a>
|
||||
</li>
|
||||
</DocumentFragment>
|
||||
`;
|
@ -1,19 +0,0 @@
|
||||
// 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>
|
||||
`;
|
@ -0,0 +1,18 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FileChooser 1`] = `
|
||||
<DocumentFragment>
|
||||
<a
|
||||
href="#"
|
||||
>
|
||||
<i
|
||||
class="fa fa-fw play"
|
||||
/>
|
||||
open
|
||||
<input
|
||||
class="hidden"
|
||||
type="file"
|
||||
/>
|
||||
</a>
|
||||
</DocumentFragment>
|
||||
`;
|
@ -1,15 +1,28 @@
|
||||
/** Auto-generated by test_app.py:TestApp._test_generate_tflow_js */
|
||||
export default function(){
|
||||
import {HTTPFlow} from '../../flow';
|
||||
export default function(): HTTPFlow {
|
||||
return {
|
||||
"client_conn": {
|
||||
//@ts-ignore
|
||||
"address": [
|
||||
"127.0.0.1",
|
||||
22
|
||||
],
|
||||
"alpn": "http/1.1",
|
||||
//@ts-ignore
|
||||
"alpn_proto_negotiated": "http/1.1",
|
||||
"cipher": "cipher",
|
||||
"cipher_name": "cipher",
|
||||
"id": "4a18d1a0-50a1-48dd-9aa6-d45d74282939",
|
||||
"peername": [
|
||||
"127.0.0.1",
|
||||
22
|
||||
],
|
||||
"sni": "address",
|
||||
"sockname": [
|
||||
"",
|
||||
0
|
||||
],
|
||||
"timestamp_end": 946681206,
|
||||
"timestamp_start": 946681200,
|
||||
"timestamp_tls_setup": 946681201,
|
||||
@ -22,8 +35,8 @@ export default function(){
|
||||
},
|
||||
"id": "d91165be-ca1f-4612-88a9-c0f8696f3e29",
|
||||
"intercepted": false,
|
||||
"is_replay": null,
|
||||
"marked": false,
|
||||
"is_replay": undefined,
|
||||
"marked": "",
|
||||
"modified": false,
|
||||
"request": {
|
||||
"contentHash": "ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73",
|
||||
@ -40,6 +53,7 @@ export default function(){
|
||||
],
|
||||
"host": "address",
|
||||
"http_version": "HTTP/1.1",
|
||||
//@ts-ignore
|
||||
"is_replay": false,
|
||||
"method": "GET",
|
||||
"path": "/path",
|
||||
@ -47,13 +61,7 @@ export default function(){
|
||||
"pretty_host": "address",
|
||||
"scheme": "http",
|
||||
"timestamp_end": 946681201,
|
||||
"timestamp_start": 946681200,
|
||||
"trailers": [
|
||||
[
|
||||
"trailer",
|
||||
"qvalue"
|
||||
]
|
||||
]
|
||||
"timestamp_start": 946681200
|
||||
},
|
||||
"response": {
|
||||
"contentHash": "ab530a13e45914982b79f9b7e3fba994cfd1f3fb22f71cea1afbf02b460c6d1d",
|
||||
@ -69,6 +77,7 @@ export default function(){
|
||||
]
|
||||
],
|
||||
"http_version": "HTTP/1.1",
|
||||
//@ts-ignore
|
||||
"is_replay": false,
|
||||
"reason": "OK",
|
||||
"status_code": 200,
|
||||
@ -82,17 +91,29 @@ export default function(){
|
||||
]
|
||||
},
|
||||
"server_conn": {
|
||||
//@ts-ignore
|
||||
"address": [
|
||||
"address",
|
||||
22
|
||||
],
|
||||
"alpn_proto_negotiated": "http/1.1",
|
||||
"alpn": undefined,
|
||||
//@ts-ignore
|
||||
"alpn_proto_negotiated": undefined,
|
||||
"cipher": undefined,
|
||||
"id": "f087e7b2-6d0a-41a8-a8f0-e1a4761395f8",
|
||||
"ip_address": [
|
||||
"192.168.0.1",
|
||||
22
|
||||
],
|
||||
"peername": [
|
||||
"192.168.0.1",
|
||||
22
|
||||
],
|
||||
"sni": "address",
|
||||
"sockname": [
|
||||
"address",
|
||||
22
|
||||
],
|
||||
"source_address": [
|
||||
"address",
|
||||
22
|
@ -6,7 +6,7 @@ describe('connection reducer', () => {
|
||||
it('should return initial state', () => {
|
||||
expect(reduceConnection(undefined, {})).toEqual({
|
||||
state: ConnectionState.INIT,
|
||||
message: null,
|
||||
message: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -1,11 +1,10 @@
|
||||
import reduceState from '../../ducks/index'
|
||||
import {rootReducer} from '../../ducks/index'
|
||||
|
||||
describe('reduceState in js/ducks/index.js', () => {
|
||||
it('should combine flow and header', () => {
|
||||
let state = reduceState(undefined, {})
|
||||
let state = rootReducer(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()
|
||||
})
|
||||
|
@ -1,23 +1,25 @@
|
||||
import reduceOptions, * as OptionsActions from '../../ducks/options'
|
||||
|
||||
import configureStore from 'redux-mock-store'
|
||||
import thunk from 'redux-thunk'
|
||||
import * as OptionsEditorActions from '../../ducks/ui/optionsEditor'
|
||||
import {updateError} from "../../ducks/ui/optionsEditor";
|
||||
|
||||
const mockStore = configureStore([ thunk ])
|
||||
|
||||
describe('option reducer', () => {
|
||||
it('should return initial state', () => {
|
||||
expect(reduceOptions(undefined, {})).toEqual({})
|
||||
expect(reduceOptions(undefined, {})).toEqual(OptionsActions.defaultState)
|
||||
})
|
||||
|
||||
it('should handle receive action', () => {
|
||||
let action = { type: OptionsActions.RECEIVE, data: 'foo' }
|
||||
expect(reduceOptions(undefined, action)).toEqual('foo')
|
||||
let action = { type: OptionsActions.RECEIVE, data: {id: 'foo'} }
|
||||
expect(reduceOptions(undefined, action)).toEqual({id: 'foo'})
|
||||
})
|
||||
|
||||
it('should handle update action', () => {
|
||||
let action = {type: OptionsActions.UPDATE, data: {id: 1} }
|
||||
expect(reduceOptions(undefined, action)).toEqual({id: 1})
|
||||
expect(reduceOptions(undefined, action)).toEqual({...OptionsActions.defaultState, id: 1})
|
||||
})
|
||||
})
|
||||
|
||||
@ -39,13 +41,11 @@ describe('option actions', () => {
|
||||
|
||||
describe('sendUpdate', () => {
|
||||
|
||||
it('should handle error', () => {
|
||||
let mockResponse = { status: 400, text: p => Promise.resolve('error') },
|
||||
promise = Promise.resolve(mockResponse)
|
||||
global.fetch = r => { return promise }
|
||||
OptionsActions.pureSendUpdate('bar', 'error')
|
||||
it('should handle error', async () => {
|
||||
global.fetch = () => Promise.reject("fooerror");
|
||||
await store.dispatch(OptionsActions.pureSendUpdate("bar", "error"))
|
||||
expect(store.getActions()).toEqual([
|
||||
{ type: OptionsEditorActions.OPTION_UPDATE_SUCCESS, option: 'foo'}
|
||||
OptionsEditorActions.updateError("bar", "fooerror")
|
||||
])
|
||||
})
|
||||
})
|
||||
|
@ -1,25 +0,0 @@
|
||||
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({})
|
||||
})
|
||||
})
|
@ -1,111 +0,0 @@
|
||||
import React from 'react'
|
||||
import { combineReducers, applyMiddleware, createStore as createReduxStore } from 'redux'
|
||||
import thunk from 'redux-thunk'
|
||||
import configureStore from 'redux-mock-store'
|
||||
import { ConnectionState } from '../../ducks/connection'
|
||||
import TFlow from './_tflow'
|
||||
|
||||
const mockStore = configureStore([thunk])
|
||||
|
||||
export function createStore(parts) {
|
||||
return createReduxStore(
|
||||
combineReducers(parts),
|
||||
applyMiddleware(...[thunk])
|
||||
)
|
||||
}
|
||||
|
||||
export { TFlow }
|
||||
|
||||
export function TStore(){
|
||||
let tflow = new TFlow()
|
||||
return mockStore({
|
||||
ui: {
|
||||
flow: {
|
||||
contentView: 'Auto',
|
||||
displayLarge: false,
|
||||
showFullContent: true,
|
||||
maxContentLines: 10,
|
||||
content: ['foo', 'bar'],
|
||||
viewDescription: 'foo',
|
||||
modifiedFlow: true,
|
||||
tab: 'request'
|
||||
},
|
||||
header: {
|
||||
tab: 'Start'
|
||||
},
|
||||
modal: {
|
||||
activeModal: undefined
|
||||
},
|
||||
optionsEditor: {
|
||||
booleanOption: { isUpdating: true, error: false },
|
||||
strOption: { error: true },
|
||||
intOption: {},
|
||||
choiceOption: {},
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
contentViews: ['Auto', 'Raw', 'Text'],
|
||||
anticache: true,
|
||||
anticomp: false
|
||||
},
|
||||
options: {
|
||||
booleanOption: {
|
||||
choices: null,
|
||||
default: false,
|
||||
help: "foo",
|
||||
type: "bool",
|
||||
value: false
|
||||
},
|
||||
strOption: {
|
||||
choices: null,
|
||||
default: null,
|
||||
help: "foo",
|
||||
type: "str",
|
||||
value: "str content"
|
||||
},
|
||||
intOption: {
|
||||
choices: null,
|
||||
default: 0,
|
||||
help: "foo",
|
||||
type: "int",
|
||||
value: 1
|
||||
},
|
||||
choiceOption: {
|
||||
choices: ['a', 'b', 'c'],
|
||||
default: 'a',
|
||||
help: "foo",
|
||||
type: "str",
|
||||
value: "b"
|
||||
},
|
||||
},
|
||||
flows: {
|
||||
selected: ["d91165be-ca1f-4612-88a9-c0f8696f3e29"],
|
||||
byId: {"d91165be-ca1f-4612-88a9-c0f8696f3e29": tflow},
|
||||
filter: '~u foo',
|
||||
highlight: '~a bar',
|
||||
sort: {
|
||||
desc: true,
|
||||
column: 'PathColumn'
|
||||
},
|
||||
view: [ tflow ]
|
||||
},
|
||||
connection: {
|
||||
state: ConnectionState.ESTABLISHED
|
||||
|
||||
},
|
||||
eventLog: {
|
||||
visible: true,
|
||||
filters: {
|
||||
debug: true,
|
||||
info: true,
|
||||
web: false,
|
||||
warn: true,
|
||||
error: true
|
||||
},
|
||||
view: [
|
||||
{ id: 1, level: 'info', message: 'foo' },
|
||||
{ id: 2, level: 'error', message: 'bar' }
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
120
web/src/js/__tests__/ducks/tutils.ts
Normal file
120
web/src/js/__tests__/ducks/tutils.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import {applyMiddleware, combineReducers, createStore as createReduxStore} from 'redux'
|
||||
import thunk from 'redux-thunk'
|
||||
import configureStore, {MockStoreCreator, MockStoreEnhanced} from 'redux-mock-store'
|
||||
import {ConnectionState} from '../../ducks/connection'
|
||||
import TFlow from './_tflow'
|
||||
import {RootState} from "../../ducks";
|
||||
import {HTTPFlow} from "../../flow";
|
||||
import {defaultState as defaultConf} from "../../ducks/conf"
|
||||
import {defaultState as defaultOptions} from "../../ducks/options"
|
||||
|
||||
const mockStoreCreator: MockStoreCreator<RootState> = configureStore([thunk])
|
||||
|
||||
export function createStore(parts) {
|
||||
return createReduxStore(
|
||||
combineReducers(parts),
|
||||
applyMiddleware(...[thunk])
|
||||
)
|
||||
}
|
||||
|
||||
export {TFlow}
|
||||
|
||||
const tflow1: HTTPFlow = TFlow();
|
||||
const tflow2: HTTPFlow = TFlow();
|
||||
tflow2.id = "flow2";
|
||||
tflow2.request.path = "/second";
|
||||
|
||||
export const testState: RootState = {
|
||||
conf: defaultConf,
|
||||
options_meta: {
|
||||
anticache: {
|
||||
"type": "bool",
|
||||
"default": false,
|
||||
"value": false,
|
||||
"help": "Strip out request headers that might cause the server to return 304-not-modified.",
|
||||
"choices": undefined
|
||||
},
|
||||
body_size_limit: {
|
||||
"type": "optional str",
|
||||
"default": undefined,
|
||||
"value": undefined,
|
||||
"help": "Byte size limit of HTTP request and response bodies. Understands k/m/g suffixes, i.e. 3m for 3 megabytes.",
|
||||
"choices": undefined,
|
||||
},
|
||||
connection_strategy: {
|
||||
"type": "str",
|
||||
"default": "eager",
|
||||
"value": "eager",
|
||||
"help": "Determine when server connections should be established. When set to lazy, mitmproxy tries to defer establishing an upstream connection as long as possible. This makes it possible to use server replay while being offline. When set to eager, mitmproxy can detect protocols with server-side greetings, as well as accurately mirror TLS ALPN negotiation.",
|
||||
"choices": [
|
||||
"eager",
|
||||
"lazy"
|
||||
]
|
||||
},
|
||||
listen_port: {
|
||||
"type": "int",
|
||||
"default": 8080,
|
||||
"value": 8080,
|
||||
"help": "Proxy service port.",
|
||||
"choices": undefined
|
||||
}
|
||||
},
|
||||
ui: {
|
||||
flow: {
|
||||
contentView: 'Auto',
|
||||
displayLarge: false,
|
||||
showFullContent: true,
|
||||
maxContentLines: 10,
|
||||
content: [[['foo', 'bar']]],
|
||||
viewDescription: 'foo',
|
||||
modifiedFlow: undefined,
|
||||
tab: 'request'
|
||||
},
|
||||
header: {
|
||||
tab: 'Start'
|
||||
},
|
||||
modal: {
|
||||
activeModal: undefined
|
||||
},
|
||||
optionsEditor: {
|
||||
booleanOption: {isUpdating: true, error: false},
|
||||
strOption: {error: true},
|
||||
intOption: {},
|
||||
choiceOption: {},
|
||||
}
|
||||
},
|
||||
options: defaultOptions,
|
||||
flows: {
|
||||
selected: [tflow1.id],
|
||||
byId: {[tflow1.id]: tflow1, [tflow2.id]: tflow2},
|
||||
filter: '~d address',
|
||||
highlight: '~u /path',
|
||||
sort: {
|
||||
desc: true,
|
||||
column: 'PathColumn'
|
||||
},
|
||||
view: [tflow1, tflow2]
|
||||
},
|
||||
connection: {
|
||||
state: ConnectionState.ESTABLISHED
|
||||
},
|
||||
eventLog: {
|
||||
visible: true,
|
||||
filters: {
|
||||
debug: true,
|
||||
info: true,
|
||||
web: false,
|
||||
warn: true,
|
||||
error: true
|
||||
},
|
||||
view: [
|
||||
{id: 1, level: 'info', message: 'foo'},
|
||||
{id: 2, level: 'error', message: 'bar'}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function TStore(): MockStoreEnhanced<RootState> {
|
||||
return mockStoreCreator(testState)
|
||||
}
|
@ -19,7 +19,7 @@ describe('flow reducer', () => {
|
||||
displayLarge: false,
|
||||
viewDescription: '',
|
||||
showFullContent: false,
|
||||
modifiedFlow: false,
|
||||
modifiedFlow: undefined,
|
||||
contentView: 'Auto',
|
||||
tab: 'request',
|
||||
content: [],
|
||||
|
24
web/src/js/__tests__/test-utils.tsx
Normal file
24
web/src/js/__tests__/test-utils.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React from 'react'
|
||||
import {render as rtlRender} from '@testing-library/react'
|
||||
import {Provider} from 'react-redux'
|
||||
// Import your own reducer
|
||||
import {createAppStore} from '../ducks'
|
||||
import {testState} from "./ducks/tutils";
|
||||
|
||||
// re-export everything
|
||||
export * from '@testing-library/react'
|
||||
|
||||
export function render(
|
||||
ui,
|
||||
{
|
||||
store = createAppStore(testState),
|
||||
...renderOptions
|
||||
} = {}
|
||||
) {
|
||||
function Wrapper({children}) {
|
||||
return <Provider store={store}>{children}</Provider>
|
||||
}
|
||||
|
||||
const ret = rtlRender(ui, {wrapper: Wrapper, ...renderOptions})
|
||||
return {...ret, store}
|
||||
}
|
@ -1,32 +1,15 @@
|
||||
import React from 'react'
|
||||
import {render} from 'react-dom'
|
||||
import {applyMiddleware, compose, createStore} from 'redux'
|
||||
import {Provider} from 'react-redux'
|
||||
import thunk from 'redux-thunk'
|
||||
|
||||
import ProxyApp from './components/ProxyApp'
|
||||
import rootReducer from './ducks/index'
|
||||
import {add as addLog} from './ducks/eventLog'
|
||||
import useUrlState from './urlState'
|
||||
import WebSocketBackend from './backends/websocket'
|
||||
import StaticBackend from './backends/static'
|
||||
import {logger} from 'redux-logger'
|
||||
import {store} from "./ducks";
|
||||
|
||||
|
||||
const middlewares = [thunk];
|
||||
|
||||
// logger must be last
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
middlewares.push(logger);
|
||||
}
|
||||
|
||||
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
const store = createStore(
|
||||
rootReducer,
|
||||
composeEnhancers(applyMiddleware(...middlewares))
|
||||
)
|
||||
|
||||
useUrlState(store)
|
||||
if (window.MITMWEB_STATIC) {
|
||||
window.backend = new StaticBackend(store)
|
||||
|
@ -12,7 +12,7 @@ export default class StaticBackend {
|
||||
|
||||
onOpen() {
|
||||
this.fetchData("flows")
|
||||
this.fetchData("settings")
|
||||
this.fetchData("options")
|
||||
// this.fetchData("events") # TODO: Add events log to static viewer.
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,6 @@ export default class WebsocketBackend {
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
this.fetchData("settings")
|
||||
this.fetchData("flows")
|
||||
this.fetchData("events")
|
||||
this.fetchData("options")
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Codemirror from 'react-codemirror';
|
||||
import CodeMirror from "../../contrib/CodeMirror"
|
||||
|
||||
|
||||
CodeEditor.propTypes = {
|
||||
@ -15,7 +15,7 @@ export default function CodeEditor ( { content, onChange} ){
|
||||
};
|
||||
return (
|
||||
<div className="codeeditor" onKeyDown={e => e.stopPropagation()}>
|
||||
<Codemirror value={content} onChange={onChange} options={options}/>
|
||||
<CodeMirror value={content} onChange={onChange} options={options}/>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { MessageUtils } from '../../flow/utils.js'
|
||||
import { MessageUtils } from '../../flow/utils'
|
||||
|
||||
export default function withContentLoader(View) {
|
||||
|
||||
@ -23,11 +23,11 @@ export default function withContentLoader(View) {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.updateContent(this.props)
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
if (
|
||||
nextProps.message.content !== this.props.message.content ||
|
||||
nextProps.message.contentHash !== this.props.message.contentHash ||
|
||||
@ -51,7 +51,7 @@ export default function withContentLoader(View) {
|
||||
if (props.message.content !== undefined) {
|
||||
return this.setState({request: undefined, content: props.message.content})
|
||||
}
|
||||
if (props.message.contentLength === 0 || props.message.contentLength === null) {
|
||||
if (props.message.contentLength === 0) {
|
||||
return this.setState({request: undefined, content: ""})
|
||||
}
|
||||
|
||||
|
@ -38,12 +38,12 @@ export class PureViewServer extends Component {
|
||||
setContent: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
UNSAFE_componentWillMount(){
|
||||
this.setContentView(this.props)
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps){
|
||||
if (nextProps.content != this.props.content) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps){
|
||||
if (nextProps.content !== this.props.content) {
|
||||
this.setContentView(nextProps)
|
||||
}
|
||||
}
|
||||
@ -55,7 +55,7 @@ export class PureViewServer extends Component {
|
||||
this.data = {lines: [], description: err.message}
|
||||
}
|
||||
|
||||
props.setContentViewDescription(props.contentView != this.data.description ? this.data.description : '')
|
||||
props.setContentViewDescription(props.contentView !== this.data.description ? this.data.description : '')
|
||||
props.setContent(this.data.lines)
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { formatSize } from '../../utils.js'
|
||||
import { formatSize } from '../../utils'
|
||||
import UploadContentButton from './UploadContentButton'
|
||||
import DownloadContentButton from './DownloadContentButton'
|
||||
|
||||
|
@ -1,38 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {connect} from 'react-redux'
|
||||
import {setContentView} from '../../ducks/ui/flow';
|
||||
import Dropdown, {MenuItem} from '../common/Dropdown'
|
||||
|
||||
|
||||
ViewSelector.propTypes = {
|
||||
contentViews: PropTypes.array.isRequired,
|
||||
activeView: PropTypes.string.isRequired,
|
||||
setContentView: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export function ViewSelector({contentViews, activeView, setContentView}) {
|
||||
let inner = <span><b>View:</b> {activeView.toLowerCase()} <span className="caret"/></span>
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
text={inner}
|
||||
className="btn btn-default btn-xs pull-left"
|
||||
options={{placement:"top-start"}}>
|
||||
{contentViews.map(name =>
|
||||
<MenuItem key={name} onClick={() => setContentView(name)}>
|
||||
{name.toLowerCase().replace('_', ' ')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
contentViews: state.settings.contentViews || [],
|
||||
activeView: state.ui.flow.contentView,
|
||||
}), {
|
||||
setContentView,
|
||||
}
|
||||
)(ViewSelector)
|
26
web/src/js/components/ContentView/ViewSelector.tsx
Normal file
26
web/src/js/components/ContentView/ViewSelector.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import {setContentView} from '../../ducks/ui/flow';
|
||||
import Dropdown, {MenuItem} from '../common/Dropdown'
|
||||
import {useAppDispatch, useAppSelector} from "../../ducks";
|
||||
|
||||
|
||||
export default React.memo(function ViewSelector() {
|
||||
const dispatch = useAppDispatch(),
|
||||
contentViews = useAppSelector(state => state.conf.contentViews || []),
|
||||
activeView = useAppSelector(state => state.ui.flow.contentView);
|
||||
|
||||
let inner = <span><b>View:</b> {activeView.toLowerCase()} <span className="caret"/></span>
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
text={inner}
|
||||
className="btn btn-default btn-xs pull-left"
|
||||
options={{placement: "top-start"}}>
|
||||
{contentViews.map(name =>
|
||||
<MenuItem key={name} onClick={() => dispatch(setContentView(name))}>
|
||||
{name.toLowerCase().replace('_', ' ')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Dropdown>
|
||||
)
|
||||
});
|
@ -8,13 +8,11 @@ import { calcVScroll } from './helpers/VirtualScroll'
|
||||
import FlowTableHead from './FlowTable/FlowTableHead'
|
||||
import FlowRow from './FlowTable/FlowRow'
|
||||
import Filt from "../filt/filt"
|
||||
import * as flowsActions from '../ducks/flows'
|
||||
|
||||
|
||||
class FlowTable extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
selectFlow: PropTypes.func.isRequired,
|
||||
flows: PropTypes.array.isRequired,
|
||||
rowHeight: PropTypes.number,
|
||||
highlight: PropTypes.string,
|
||||
@ -110,7 +108,6 @@ class FlowTable extends React.Component {
|
||||
flow={flow}
|
||||
selected={flow === selected}
|
||||
highlighted={isHighlighted(flow)}
|
||||
onSelect={this.props.selectFlow}
|
||||
/>
|
||||
))}
|
||||
<tr style={{ height: vScroll.paddingBottom }}/>
|
||||
@ -128,8 +125,5 @@ export default connect(
|
||||
flows: state.flows.view,
|
||||
highlight: state.flows.highlight,
|
||||
selected: state.flows.byId[state.flows.selected[0]],
|
||||
}),
|
||||
{
|
||||
selectFlow: flowsActions.select,
|
||||
}
|
||||
})
|
||||
)(PureFlowTable)
|
||||
|
@ -1,236 +0,0 @@
|
||||
import React, {useCallback, useState} from 'react'
|
||||
import {useDispatch} from 'react-redux'
|
||||
import classnames from 'classnames'
|
||||
import {RequestUtils, ResponseUtils} from '../../flow/utils.js'
|
||||
import {formatSize, formatTimeDelta, formatTimeStamp} from '../../utils.js'
|
||||
import * as flowActions from "../../ducks/flows";
|
||||
import { addInterceptFilter } from "../../ducks/settings"
|
||||
import Dropdown, {MenuItem, SubMenu} from "../common/Dropdown";
|
||||
import { fetchApi } from "../../utils"
|
||||
|
||||
export const defaultColumnNames = ["tls", "icon", "path", "method", "status", "size", "time"]
|
||||
|
||||
export function TLSColumn({flow}) {
|
||||
return (
|
||||
<td className={classnames('col-tls', flow.request.scheme === 'https' ? 'col-tls-https' : 'col-tls-http')}/>
|
||||
)
|
||||
}
|
||||
|
||||
TLSColumn.headerClass = 'col-tls'
|
||||
TLSColumn.headerName = ''
|
||||
|
||||
export function IconColumn({flow}) {
|
||||
return (
|
||||
<td className="col-icon">
|
||||
<div className={classnames('resource-icon', IconColumn.getIcon(flow))}/>
|
||||
</td>
|
||||
)
|
||||
}
|
||||
|
||||
IconColumn.headerClass = 'col-icon'
|
||||
IconColumn.headerName = ''
|
||||
|
||||
IconColumn.getIcon = flow => {
|
||||
if (!flow.response) {
|
||||
return 'resource-icon-plain'
|
||||
}
|
||||
|
||||
var contentType = ResponseUtils.getContentType(flow.response) || ''
|
||||
|
||||
// @todo We should assign a type to the flow somewhere else.
|
||||
if (flow.response.status_code === 304) {
|
||||
return 'resource-icon-not-modified'
|
||||
}
|
||||
if (300 <= flow.response.status_code && flow.response.status_code < 400) {
|
||||
return 'resource-icon-redirect'
|
||||
}
|
||||
if (contentType.indexOf('image') >= 0) {
|
||||
return 'resource-icon-image'
|
||||
}
|
||||
if (contentType.indexOf('javascript') >= 0) {
|
||||
return 'resource-icon-js'
|
||||
}
|
||||
if (contentType.indexOf('css') >= 0) {
|
||||
return 'resource-icon-css'
|
||||
}
|
||||
if (contentType.indexOf('html') >= 0) {
|
||||
return 'resource-icon-document'
|
||||
}
|
||||
|
||||
return 'resource-icon-plain'
|
||||
}
|
||||
|
||||
export function PathColumn({flow}) {
|
||||
|
||||
let err;
|
||||
if (flow.error) {
|
||||
if (flow.error.msg === "Connection killed.") {
|
||||
err = <i className="fa fa-fw fa-times pull-right"/>
|
||||
} else {
|
||||
err = <i className="fa fa-fw fa-exclamation pull-right"/>
|
||||
}
|
||||
}
|
||||
return (
|
||||
<td className="col-path">
|
||||
{flow.request.is_replay && (
|
||||
<i className="fa fa-fw fa-repeat pull-right"/>
|
||||
)}
|
||||
{flow.intercepted && (
|
||||
<i className="fa fa-fw fa-pause pull-right"/>
|
||||
)}
|
||||
{err}
|
||||
{RequestUtils.pretty_url(flow.request)}
|
||||
</td>
|
||||
)
|
||||
}
|
||||
|
||||
PathColumn.headerClass = 'col-path'
|
||||
PathColumn.headerName = 'Path'
|
||||
|
||||
export function MethodColumn({flow}) {
|
||||
return (
|
||||
<td className="col-method">{flow.request.method}</td>
|
||||
)
|
||||
}
|
||||
|
||||
MethodColumn.headerClass = 'col-method'
|
||||
MethodColumn.headerName = 'Method'
|
||||
|
||||
export function StatusColumn({flow}) {
|
||||
let color = 'darkred';
|
||||
|
||||
if (flow.response && 100 <= flow.response.status_code && flow.response.status_code < 200) {
|
||||
color = 'green'
|
||||
} else if (flow.response && 200 <= flow.response.status_code && flow.response.status_code < 300) {
|
||||
color = 'darkgreen'
|
||||
} else if (flow.response && 300 <= flow.response.status_code && flow.response.status_code < 400) {
|
||||
color = 'lightblue'
|
||||
} else if (flow.response && 400 <= flow.response.status_code && flow.response.status_code < 500) {
|
||||
color = 'lightred'
|
||||
} else if (flow.response && 500 <= flow.response.status_code && flow.response.status_code < 600) {
|
||||
color = 'lightred'
|
||||
}
|
||||
|
||||
return (
|
||||
<td className="col-status" style={{color: color}}>{flow.response && flow.response.status_code}</td>
|
||||
)
|
||||
}
|
||||
|
||||
StatusColumn.headerClass = 'col-status'
|
||||
StatusColumn.headerName = 'Status'
|
||||
|
||||
export function SizeColumn({flow}) {
|
||||
return (
|
||||
<td className="col-size">{formatSize(SizeColumn.getTotalSize(flow))}</td>
|
||||
)
|
||||
}
|
||||
|
||||
SizeColumn.getTotalSize = flow => {
|
||||
let total = flow.request.contentLength
|
||||
if (flow.response) {
|
||||
total += flow.response.contentLength || 0
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
SizeColumn.headerClass = 'col-size'
|
||||
SizeColumn.headerName = 'Size'
|
||||
|
||||
export function TimeColumn({flow}) {
|
||||
return (
|
||||
<td className="col-time">
|
||||
{flow.response ? (
|
||||
formatTimeDelta(1000 * (flow.response.timestamp_end - flow.request.timestamp_start))
|
||||
) : (
|
||||
'...'
|
||||
)}
|
||||
</td>
|
||||
)
|
||||
}
|
||||
|
||||
TimeColumn.headerClass = 'col-time'
|
||||
TimeColumn.headerName = 'Time'
|
||||
|
||||
export function TimeStampColumn({flow}) {
|
||||
return (
|
||||
<td className="col-start">
|
||||
{flow.request.timestamp_start ? (
|
||||
formatTimeStamp(flow.request.timestamp_start)
|
||||
) : (
|
||||
'...'
|
||||
)}
|
||||
</td>
|
||||
)
|
||||
}
|
||||
|
||||
TimeStampColumn.headerClass = 'col-timestamp'
|
||||
TimeStampColumn.headerName = 'TimeStamp'
|
||||
|
||||
export function QuickActionsColumn({flow, intercept}) {
|
||||
const dispatch = useDispatch()
|
||||
let [open, setOpen] = useState(false)
|
||||
|
||||
const exportAsCURL = useCallback(() => {
|
||||
if (!flow) {
|
||||
return
|
||||
}
|
||||
|
||||
fetchApi(`/flows/${flow.id}/export`, { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
navigator.clipboard.writeText(data.export)
|
||||
})
|
||||
}, [flow])
|
||||
|
||||
let forwardIntercept = null;
|
||||
if (flow.intercepted) {
|
||||
forwardIntercept = <a href="#" className="quickaction" onClick={() => dispatch(flowActions.resume(flow))}>
|
||||
<i className="fa fa-fw fa-play text-success"/>
|
||||
</a>;
|
||||
}
|
||||
|
||||
return (
|
||||
<td className={classnames("col-quickactions", {hover: open})} onClick={(e) => e.stopPropagation()}>
|
||||
<div>
|
||||
{forwardIntercept}
|
||||
<Dropdown text={<i className="fa fa-fw fa-ellipsis-h"/>} className="quickaction" onOpen={setOpen} options={{placement: "bottom-end"}}>
|
||||
<MenuItem onClick={() => exportAsCURL()}>Copy as cURL</MenuItem>
|
||||
<SubMenu title="Intercept requests like this">
|
||||
<MenuItem onClick={() =>{dispatch(addInterceptFilter(flow.request.host))}}>
|
||||
Intercept {flow.request.host}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() =>{dispatch(addInterceptFilter(flow.request.host + flow.request.path))}}>
|
||||
Intercept {flow.request.host + flow.request.path}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() =>{dispatch(addInterceptFilter(`~m POST & ${flow.request.host}`))}}>
|
||||
Intercept all POST requests from this host
|
||||
</MenuItem>
|
||||
</SubMenu>
|
||||
<MenuItem onClick={() =>{dispatch(flowActions.remove(flow))}}>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
</td>
|
||||
)
|
||||
}
|
||||
|
||||
QuickActionsColumn.headerClass = 'col-quickactions'
|
||||
QuickActionsColumn.headerName = ''
|
||||
|
||||
|
||||
export const columns = {};
|
||||
for (let col of [
|
||||
TLSColumn,
|
||||
IconColumn,
|
||||
PathColumn,
|
||||
MethodColumn,
|
||||
StatusColumn,
|
||||
TimeStampColumn,
|
||||
SizeColumn,
|
||||
TimeColumn,
|
||||
QuickActionsColumn,
|
||||
]) {
|
||||
columns[col.name.replace(/Column$/, "").toLowerCase()] = col;
|
||||
}
|
280
web/src/js/components/FlowTable/FlowColumns.tsx
Normal file
280
web/src/js/components/FlowTable/FlowColumns.tsx
Normal file
@ -0,0 +1,280 @@
|
||||
import React, {useState} from 'react'
|
||||
import {useDispatch} from 'react-redux'
|
||||
import classnames from 'classnames'
|
||||
import {RequestUtils, ResponseUtils} from '../../flow/utils'
|
||||
import {fetchApi, formatSize, formatTimeDelta, formatTimeStamp} from '../../utils'
|
||||
import * as flowActions from "../../ducks/flows";
|
||||
import {addInterceptFilter} from "../../ducks/options"
|
||||
import Dropdown, {MenuItem, SubMenu} from "../common/Dropdown";
|
||||
import {Flow} from "../../flow";
|
||||
|
||||
|
||||
type FlowColumnProps = {
|
||||
flow: Flow
|
||||
}
|
||||
|
||||
interface FlowColumn {
|
||||
(props: FlowColumnProps): JSX.Element;
|
||||
|
||||
headerClass: string;
|
||||
headerName: string;
|
||||
}
|
||||
|
||||
export const TLSColumn: FlowColumn = ({flow}) => {
|
||||
return (
|
||||
<td className={classnames('col-tls', flow.client_conn.tls_established ? 'col-tls-https' : 'col-tls-http')}/>
|
||||
)
|
||||
}
|
||||
|
||||
TLSColumn.headerClass = 'col-tls'
|
||||
TLSColumn.headerName = ''
|
||||
|
||||
export const IconColumn: FlowColumn = ({flow}) => {
|
||||
return (
|
||||
<td className="col-icon">
|
||||
<div className={classnames('resource-icon', getIcon(flow))}/>
|
||||
</td>
|
||||
)
|
||||
}
|
||||
|
||||
IconColumn.headerClass = 'col-icon'
|
||||
IconColumn.headerName = ''
|
||||
|
||||
const getIcon = (flow: Flow): string => {
|
||||
if (flow.type !== "http" || !flow.response) {
|
||||
return 'resource-icon-plain'
|
||||
}
|
||||
|
||||
var contentType = ResponseUtils.getContentType(flow.response) || ''
|
||||
|
||||
// @todo We should assign a type to the flow somewhere else.
|
||||
if (flow.response.status_code === 304) {
|
||||
return 'resource-icon-not-modified'
|
||||
}
|
||||
if (300 <= flow.response.status_code && flow.response.status_code < 400) {
|
||||
return 'resource-icon-redirect'
|
||||
}
|
||||
if (contentType.indexOf('image') >= 0) {
|
||||
return 'resource-icon-image'
|
||||
}
|
||||
if (contentType.indexOf('javascript') >= 0) {
|
||||
return 'resource-icon-js'
|
||||
}
|
||||
if (contentType.indexOf('css') >= 0) {
|
||||
return 'resource-icon-css'
|
||||
}
|
||||
if (contentType.indexOf('html') >= 0) {
|
||||
return 'resource-icon-document'
|
||||
}
|
||||
|
||||
return 'resource-icon-plain'
|
||||
}
|
||||
|
||||
export const PathColumn: FlowColumn = ({flow}) => {
|
||||
let err;
|
||||
if (flow.error) {
|
||||
if (flow.error.msg === "Connection killed.") {
|
||||
err = <i className="fa fa-fw fa-times pull-right"/>
|
||||
} else {
|
||||
err = <i className="fa fa-fw fa-exclamation pull-right"/>
|
||||
}
|
||||
}
|
||||
return (
|
||||
<td className="col-path">
|
||||
{flow.is_replay === "request" && (
|
||||
<i className="fa fa-fw fa-repeat pull-right"/>
|
||||
)}
|
||||
{flow.intercepted && (
|
||||
<i className="fa fa-fw fa-pause pull-right"/>
|
||||
)}
|
||||
{err}
|
||||
{flow.type === "http" ? RequestUtils.pretty_url(flow.request) : null}
|
||||
</td>
|
||||
)
|
||||
};
|
||||
|
||||
PathColumn.headerClass = 'col-path'
|
||||
PathColumn.headerName = 'Path'
|
||||
|
||||
export const MethodColumn: FlowColumn = ({flow}) => {
|
||||
return (
|
||||
<td className="col-method">{flow.type === "http" ? flow.request.method : flow.type.toLowerCase()}</td>
|
||||
)
|
||||
};
|
||||
|
||||
MethodColumn.headerClass = 'col-method'
|
||||
MethodColumn.headerName = 'Method'
|
||||
|
||||
export const StatusColumn: FlowColumn = ({flow}) => {
|
||||
let color = 'darkred';
|
||||
|
||||
if (flow.type !== "http" || !flow.response)
|
||||
return <td className="col-status"/>
|
||||
|
||||
if (100 <= flow.response.status_code && flow.response.status_code < 200) {
|
||||
color = 'green'
|
||||
} else if (200 <= flow.response.status_code && flow.response.status_code < 300) {
|
||||
color = 'darkgreen'
|
||||
} else if (300 <= flow.response.status_code && flow.response.status_code < 400) {
|
||||
color = 'lightblue'
|
||||
} else if (400 <= flow.response.status_code && flow.response.status_code < 500) {
|
||||
color = 'lightred'
|
||||
} else if (500 <= flow.response.status_code && flow.response.status_code < 600) {
|
||||
color = 'lightred'
|
||||
}
|
||||
|
||||
return (
|
||||
<td className="col-status" style={{color: color}}>{flow.response.status_code}</td>
|
||||
)
|
||||
}
|
||||
|
||||
StatusColumn.headerClass = 'col-status'
|
||||
StatusColumn.headerName = 'Status'
|
||||
|
||||
export const SizeColumn: FlowColumn = ({flow}) => {
|
||||
return (
|
||||
<td className="col-size">{formatSize(getTotalSize(flow))}</td>
|
||||
)
|
||||
};
|
||||
|
||||
const getTotalSize = (flow: Flow): number => {
|
||||
if (flow.type !== "http")
|
||||
return 0
|
||||
let total = flow.request.contentLength
|
||||
if (flow.response) {
|
||||
total += flow.response.contentLength || 0
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
SizeColumn.headerClass = 'col-size'
|
||||
SizeColumn.headerName = 'Size'
|
||||
|
||||
export const TimeColumn: FlowColumn = ({flow}) => {
|
||||
return (
|
||||
<td className="col-time">
|
||||
{flow.type === "http" && flow.response?.timestamp_end ? (
|
||||
formatTimeDelta(1000 * (flow.response.timestamp_end - flow.request.timestamp_start))
|
||||
) : (
|
||||
'...'
|
||||
)}
|
||||
</td>
|
||||
)
|
||||
}
|
||||
|
||||
TimeColumn.headerClass = 'col-time'
|
||||
TimeColumn.headerName = 'Time'
|
||||
|
||||
export const TimeStampColumn: FlowColumn = ({flow}) => {
|
||||
return (
|
||||
<td className="col-start">
|
||||
{flow.type === "http" && flow.request.timestamp_start ? (
|
||||
formatTimeStamp(flow.request.timestamp_start)
|
||||
) : (
|
||||
'...'
|
||||
)}
|
||||
</td>
|
||||
)
|
||||
}
|
||||
|
||||
TimeStampColumn.headerClass = 'col-timestamp'
|
||||
TimeStampColumn.headerName = 'TimeStamp'
|
||||
|
||||
export const QuickActionsColumn: FlowColumn = ({flow}) => {
|
||||
const dispatch = useDispatch()
|
||||
let [open, setOpen] = useState(false)
|
||||
|
||||
const copy = (format: string) => {
|
||||
if (!flow) {
|
||||
return
|
||||
}
|
||||
|
||||
fetchApi(`/flows/${flow.id}/export/${format}.json`, {method: 'POST'})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
navigator.clipboard.writeText(data.export)
|
||||
})
|
||||
}
|
||||
|
||||
let resume_or_replay: React.ReactNode | null = null;
|
||||
if (flow.intercepted) {
|
||||
resume_or_replay = <a href="#" className="quickaction" onClick={() => dispatch(flowActions.resume(flow))}>
|
||||
<i className="fa fa-fw fa-play text-success"/>
|
||||
</a>;
|
||||
} else {
|
||||
resume_or_replay = <a href="#" className="quickaction" onClick={() => dispatch(flowActions.replay(flow))}>
|
||||
<i className="fa fa-fw fa-repeat text-primary"/>
|
||||
</a>;
|
||||
}
|
||||
|
||||
if (flow.type !== "http")
|
||||
return <td className="col-quickactions"/>
|
||||
|
||||
const filt = (x) => dispatch(addInterceptFilter(x));
|
||||
const ct = flow.response && ResponseUtils.getContentType(flow.response);
|
||||
|
||||
return (
|
||||
<td className={classnames("col-quickactions", {hover: open})} onClick={(e) => 0/*e.stopPropagation()*/}>
|
||||
<div>
|
||||
{resume_or_replay}
|
||||
<Dropdown text={<i className="fa fa-fw fa-ellipsis-h text-muted"/>} className="quickaction"
|
||||
onOpen={setOpen}
|
||||
options={{placement: "bottom-end"}}>
|
||||
<SubMenu title="Copy...">
|
||||
<MenuItem onClick={() => copy("raw_request")}>Copy raw request</MenuItem>
|
||||
<MenuItem onClick={() => copy("raw_response")}>Copy raw response</MenuItem>
|
||||
<MenuItem onClick={() => copy("raw")}>Copy raw request and response</MenuItem>
|
||||
<MenuItem onClick={() => copy("curl")}>Copy as cURL</MenuItem>
|
||||
<MenuItem onClick={() => copy("httpie")}>Copy as HTTPie</MenuItem>
|
||||
</SubMenu>
|
||||
<SubMenu title="Intercept requests like this">
|
||||
<MenuItem onClick={() => filt(`~q ${flow.request.host}`)}>
|
||||
Requests to {flow.request.host}
|
||||
</MenuItem>
|
||||
{flow.request.path !== "/" &&
|
||||
<MenuItem onClick={() => filt(`~q ${flow.request.host}${flow.request.path}`)}>
|
||||
Requests to {flow.request.host + flow.request.path}
|
||||
</MenuItem>}
|
||||
{flow.request.method !== "GET" &&
|
||||
<MenuItem onClick={() => filt(`~q ~m ${flow.request.method} ${flow.request.host}`)}>
|
||||
{flow.request.method} requests to {flow.request.host}
|
||||
</MenuItem>}
|
||||
</SubMenu>
|
||||
<SubMenu title="Intercept responses like this">
|
||||
<MenuItem onClick={() => filt(`~s ${flow.request.host}`)}>
|
||||
Responses from {flow.request.host}
|
||||
</MenuItem>
|
||||
{flow.request.path !== "/" &&
|
||||
<MenuItem onClick={() => filt(`~s ${flow.request.host}${flow.request.path}`)}>
|
||||
Responses from {flow.request.host + flow.request.path}
|
||||
</MenuItem>}
|
||||
{!!ct &&
|
||||
<MenuItem onClick={() => filt(`~ts ${ct}`)}>
|
||||
Responses with a {ct} content type.
|
||||
</MenuItem>}
|
||||
</SubMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
</td>
|
||||
)
|
||||
}
|
||||
|
||||
QuickActionsColumn.headerClass = 'col-quickactions'
|
||||
QuickActionsColumn.headerName = ''
|
||||
|
||||
|
||||
export const columns: { [key: string]: FlowColumn } = {};
|
||||
for (let col of [
|
||||
TLSColumn,
|
||||
IconColumn,
|
||||
PathColumn,
|
||||
MethodColumn,
|
||||
StatusColumn,
|
||||
TimeStampColumn,
|
||||
SizeColumn,
|
||||
TimeColumn,
|
||||
QuickActionsColumn,
|
||||
]) {
|
||||
columns[col.name.replace(/Column$/, "").toLowerCase()] = col;
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import {defaultColumnNames} from './FlowColumns'
|
||||
import { pure } from '../../utils'
|
||||
import {getDisplayColumns} from './FlowTableHead'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
FlowRow.propTypes = {
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
flow: PropTypes.object.isRequired,
|
||||
highlighted: PropTypes.bool,
|
||||
selected: PropTypes.bool,
|
||||
}
|
||||
|
||||
function FlowRow({ flow, selected, highlighted, onSelect, displayColumnNames, intercept}) {
|
||||
const className = classnames({
|
||||
'selected': selected,
|
||||
'highlighted': highlighted,
|
||||
'intercepted': flow.intercepted,
|
||||
'has-request': flow.request,
|
||||
'has-response': flow.response,
|
||||
})
|
||||
|
||||
const displayColumns = getDisplayColumns(displayColumnNames)
|
||||
|
||||
return (
|
||||
<tr className={className} onClick={() => onSelect(flow.id)}>
|
||||
{displayColumns.map(Column => (
|
||||
<Column key={Column.name} flow={flow} intercept={intercept}/>
|
||||
))}
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
displayColumnNames: state.options["web_columns"] ? state.options["web_columns"].value : defaultColumnNames,
|
||||
intercept: state.settings.intercept,
|
||||
})
|
||||
)(pure(FlowRow))
|
45
web/src/js/components/FlowTable/FlowRow.tsx
Normal file
45
web/src/js/components/FlowTable/FlowRow.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import React, {useCallback} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import {Flow} from "../../flow";
|
||||
import {useAppDispatch, useAppSelector} from "../../ducks";
|
||||
import {select} from '../../ducks/flows'
|
||||
import {columns, QuickActionsColumn} from "./FlowColumns";
|
||||
|
||||
type FlowRowProps = {
|
||||
flow: Flow
|
||||
selected: boolean
|
||||
highlighted: boolean
|
||||
}
|
||||
|
||||
export default React.memo(function FlowRow({flow, selected, highlighted}: FlowRowProps) {
|
||||
const dispatch = useAppDispatch(),
|
||||
displayColumnNames = useAppSelector(state => state.options.web_columns),
|
||||
className = classnames({
|
||||
'selected': selected,
|
||||
'highlighted': highlighted,
|
||||
'intercepted': flow.intercepted,
|
||||
'has-request': flow.type === "http" && flow.request,
|
||||
'has-response': flow.type === "http" && flow.response,
|
||||
})
|
||||
|
||||
const onClick = useCallback(e => {
|
||||
// a bit of a hack to disable row selection for quickactions.
|
||||
let node = e.target;
|
||||
while (node.parentNode) {
|
||||
if (node.classList.contains("col-quickactions"))
|
||||
return
|
||||
node = node.parentNode;
|
||||
}
|
||||
dispatch(select(flow.id));
|
||||
}, [flow]);
|
||||
|
||||
const displayColumns = displayColumnNames.map(x => columns[x]).concat(QuickActionsColumn);
|
||||
|
||||
return (
|
||||
<tr className={className} onClick={onClick}>
|
||||
{displayColumns.map(Column => (
|
||||
<Column key={Column.name} flow={flow}/>
|
||||
))}
|
||||
</tr>
|
||||
)
|
||||
})
|
@ -1,50 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {connect} from 'react-redux'
|
||||
import classnames from 'classnames'
|
||||
import {columns, defaultColumnNames} from './FlowColumns'
|
||||
|
||||
import {setSort} from '../../ducks/flows'
|
||||
|
||||
FlowTableHead.propTypes = {
|
||||
setSort: PropTypes.func.isRequired,
|
||||
sortDesc: PropTypes.bool.isRequired,
|
||||
sortColumn: PropTypes.string,
|
||||
displayColumnNames: PropTypes.array,
|
||||
}
|
||||
|
||||
export function getDisplayColumns(displayColumnNames) {
|
||||
if (typeof displayColumnNames == "undefined") {
|
||||
return Object.values(columns)
|
||||
}
|
||||
return displayColumnNames.map(x => columns[x]).concat([columns.quickactions]);
|
||||
}
|
||||
|
||||
export function FlowTableHead({sortColumn, sortDesc, setSort, displayColumnNames}) {
|
||||
const sortType = sortDesc ? 'sort-desc' : 'sort-asc'
|
||||
|
||||
const displayColumns = getDisplayColumns(displayColumnNames)
|
||||
|
||||
return (
|
||||
<tr>
|
||||
{displayColumns.map(Column => (
|
||||
<th className={classnames(Column.headerClass, sortColumn === Column.name && sortType)}
|
||||
key={Column.name}
|
||||
onClick={() => setSort(Column.name, Column.name !== sortColumn ? false : !sortDesc)}>
|
||||
{Column.headerName}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
sortDesc: state.flows.sort.desc,
|
||||
sortColumn: state.flows.sort.column,
|
||||
displayColumnNames: state.options["web_columns"] ? state.options["web_columns"].value : defaultColumnNames,
|
||||
}),
|
||||
{
|
||||
setSort
|
||||
}
|
||||
)(FlowTableHead)
|
28
web/src/js/components/FlowTable/FlowTableHead.tsx
Normal file
28
web/src/js/components/FlowTable/FlowTableHead.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
import {columns, QuickActionsColumn} from './FlowColumns'
|
||||
|
||||
import {setSort} from '../../ducks/flows'
|
||||
import {useAppDispatch, useAppSelector} from "../../ducks";
|
||||
|
||||
export default React.memo(function FlowTableHead() {
|
||||
const dispatch = useAppDispatch(),
|
||||
sortDesc = useAppSelector(state => state.flows.sort.desc),
|
||||
sortColumn = useAppSelector(state => state.flows.sort.column),
|
||||
displayColumnNames = useAppSelector(state => state.options.web_columns);
|
||||
|
||||
const sortType = sortDesc ? 'sort-desc' : 'sort-asc'
|
||||
const displayColumns = displayColumnNames.map(x => columns[x]).concat(QuickActionsColumn);
|
||||
|
||||
return (
|
||||
<tr>
|
||||
{displayColumns.map(Column => (
|
||||
<th className={classnames(Column.headerClass, sortColumn === Column.name && sortType)}
|
||||
key={Column.name}
|
||||
onClick={() => dispatch(setSort(Column.name, Column.name !== sortColumn ? false : !sortDesc))}>
|
||||
{Column.headerName}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
)
|
||||
})
|
@ -1,8 +1,7 @@
|
||||
import React from 'react'
|
||||
import _ from 'lodash'
|
||||
import { formatTimeStamp, formatTimeDelta } from '../../utils.js'
|
||||
import {formatTimeDelta, formatTimeStamp} from '../../utils'
|
||||
|
||||
export function TimeStamp({ t, deltaTo, title }) {
|
||||
export function TimeStamp({t, deltaTo, title}) {
|
||||
return t ? (
|
||||
<tr>
|
||||
<td>{title}:</td>
|
||||
@ -20,7 +19,7 @@ export function TimeStamp({ t, deltaTo, title }) {
|
||||
)
|
||||
}
|
||||
|
||||
export function ConnectionInfo({ conn }) {
|
||||
export function ConnectionInfo({conn}) {
|
||||
return (
|
||||
<table className="connection-table">
|
||||
<tbody>
|
||||
@ -69,25 +68,25 @@ export function ConnectionInfo({ conn }) {
|
||||
)
|
||||
}
|
||||
|
||||
export function CertificateInfo({ flow }) {
|
||||
export function CertificateInfo({flow}) {
|
||||
// @todo We should fetch human-readable certificate representation from the server
|
||||
return (
|
||||
<div>
|
||||
{flow.client_conn.cert && [
|
||||
<h4 key="name">Client Certificate</h4>,
|
||||
<pre key="value" style={{ maxHeight: 100 }}>{flow.client_conn.cert}</pre>
|
||||
<pre key="value" style={{maxHeight: 100}}>{flow.client_conn.cert}</pre>
|
||||
]}
|
||||
|
||||
{flow.server_conn.cert && [
|
||||
<h4 key="name">Server Certificate</h4>,
|
||||
<pre key="value" style={{ maxHeight: 100 }}>{flow.server_conn.cert}</pre>
|
||||
<pre key="value" style={{maxHeight: 100}}>{flow.server_conn.cert}</pre>
|
||||
]}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function Timing({ flow }) {
|
||||
const { server_conn: sc, client_conn: cc, request: req, response: res } = flow
|
||||
export function Timing({flow}) {
|
||||
const {server_conn: sc, client_conn: cc, request: req, response: res} = flow
|
||||
|
||||
const timestamps = [
|
||||
{
|
||||
@ -142,7 +141,7 @@ export function Timing({ flow }) {
|
||||
)
|
||||
}
|
||||
|
||||
export default function Details({ flow }) {
|
||||
export default function Details({flow}) {
|
||||
return (
|
||||
<section className="detail">
|
||||
<h4>Client Connection</h4>
|
||||
|
@ -3,8 +3,8 @@ import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
|
||||
import { RequestUtils, isValidHttpVersion, parseUrl } from '../../flow/utils.js'
|
||||
import { formatTimeStamp } from '../../utils.js'
|
||||
import { RequestUtils, isValidHttpVersion, parseUrl } from '../../flow/utils'
|
||||
import { formatTimeStamp } from '../../utils'
|
||||
import ContentView from '../ContentView'
|
||||
import ContentViewOptions from '../ContentView/ContentViewOptions'
|
||||
import ValidateEditor from '../ValueEditor/ValidateEditor'
|
||||
|
@ -1,16 +1,15 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { formatSize } from '../utils.js'
|
||||
import {formatSize} from '../utils'
|
||||
import HideInStatic from '../components/common/HideInStatic'
|
||||
import {useAppSelector} from "../ducks";
|
||||
|
||||
Footer.propTypes = {
|
||||
settings: PropTypes.object.isRequired,
|
||||
}
|
||||
export default function Footer() {
|
||||
const version = useAppSelector(state => state.conf.version);
|
||||
let {
|
||||
mode, intercept, showhost, upstream_cert, rawtcp, http2, websocket, anticache, anticomp,
|
||||
stickyauth, stickycookie, stream_large_bodies, listen_host, listen_port, server
|
||||
} = useAppSelector(state => state.options);
|
||||
|
||||
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, server} = settings;
|
||||
return (
|
||||
<footer>
|
||||
{mode && mode !== "regular" && (
|
||||
@ -22,7 +21,7 @@ function Footer({ settings }) {
|
||||
{showhost && (
|
||||
<span className="label label-success">showhost</span>
|
||||
)}
|
||||
{no_upstream_cert && (
|
||||
{!upstream_cert && (
|
||||
<span className="label label-success">no-upstream-cert</span>
|
||||
)}
|
||||
{!rawtcp && (
|
||||
@ -54,20 +53,14 @@ function Footer({ settings }) {
|
||||
{
|
||||
server && (
|
||||
<span className="label label-primary" title="HTTP Proxy Server Address">
|
||||
{listen_host||"*"}:{listen_port}
|
||||
{listen_host || "*"}:{listen_port}
|
||||
</span>)
|
||||
}
|
||||
</HideInStatic>
|
||||
<span className="label label-info" title="Mitmproxy Version">
|
||||
v{version}
|
||||
{version}
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
settings: state.settings,
|
||||
})
|
||||
)(Footer)
|
@ -1,16 +1,14 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { connect } from "react-redux"
|
||||
import { ConnectionState } from "../../ducks/connection"
|
||||
import {ConnectionState} from "../../ducks/connection"
|
||||
import {useAppSelector} from "../../ducks";
|
||||
|
||||
|
||||
ConnectionIndicator.propTypes = {
|
||||
state: PropTypes.symbol.isRequired,
|
||||
message: PropTypes.string,
|
||||
export default React.memo(function ConnectionIndicator() {
|
||||
|
||||
}
|
||||
export function ConnectionIndicator({ state, message }) {
|
||||
switch (state) {
|
||||
const connState = useAppSelector(state => state.connection.state),
|
||||
message = useAppSelector(state => state.connection.message)
|
||||
|
||||
switch (connState) {
|
||||
case ConnectionState.INIT:
|
||||
return <span className="connection-indicator init">connecting…</span>;
|
||||
case ConnectionState.FETCHING:
|
||||
@ -22,9 +20,8 @@ export function ConnectionIndicator({ state, message }) {
|
||||
title={message}>connection lost</span>;
|
||||
case ConnectionState.OFFLINE:
|
||||
return <span className="connection-indicator offline">offline</span>;
|
||||
default:
|
||||
const exhaustiveCheck: never = connState;
|
||||
throw "unknown connection state";
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => state.connection,
|
||||
)(ConnectionIndicator)
|
||||
})
|
@ -1,63 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {connect} from 'react-redux'
|
||||
import FileChooser from '../common/FileChooser'
|
||||
import Dropdown, {Divider, MenuItem} from '../common/Dropdown'
|
||||
import * as flowsActions from '../../ducks/flows'
|
||||
import HideInStatic from "../common/HideInStatic";
|
||||
|
||||
FileMenu.propTypes = {
|
||||
clearFlows: PropTypes.func.isRequired,
|
||||
loadFlows: PropTypes.func.isRequired,
|
||||
saveFlows: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
FileMenu.onNewClick = (e, clearFlows) => {
|
||||
e.preventDefault();
|
||||
if (confirm('Delete all flows?'))
|
||||
clearFlows()
|
||||
}
|
||||
|
||||
export function FileMenu({clearFlows, loadFlows, saveFlows}) {
|
||||
return (
|
||||
<Dropdown className="pull-left special" text="mitmproxy" options={{"placement": "bottom-start"}}>
|
||||
<MenuItem onClick={e => FileMenu.onNewClick(e, clearFlows)}>
|
||||
<i className="fa fa-fw fa-trash"/>
|
||||
Clear All
|
||||
</MenuItem>
|
||||
<li>
|
||||
<FileChooser
|
||||
icon="fa-folder-open"
|
||||
text=" Open..."
|
||||
onOpenFile={file => loadFlows(file)}
|
||||
/>
|
||||
</li>
|
||||
<MenuItem onClick={e => {
|
||||
e.preventDefault();
|
||||
saveFlows();
|
||||
}}>
|
||||
<i className="fa fa-fw fa-floppy-o"/>
|
||||
Save...
|
||||
</MenuItem>
|
||||
|
||||
<HideInStatic>
|
||||
<Divider/>
|
||||
<li>
|
||||
<a href="http://mitm.it/" target="_blank">
|
||||
<i className="fa fa-fw fa-external-link"/>
|
||||
Install Certificates...
|
||||
</a>
|
||||
</li>
|
||||
</HideInStatic>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
{
|
||||
clearFlows: flowsActions.clear,
|
||||
loadFlows: flowsActions.upload,
|
||||
saveFlows: flowsActions.download,
|
||||
}
|
||||
)(FileMenu)
|
43
web/src/js/components/Header/FileMenu.tsx
Normal file
43
web/src/js/components/Header/FileMenu.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import React from 'react'
|
||||
import {useDispatch} from 'react-redux'
|
||||
import FileChooser from '../common/FileChooser'
|
||||
import Dropdown, {Divider, MenuItem} from '../common/Dropdown'
|
||||
import * as flowsActions from '../../ducks/flows'
|
||||
import HideInStatic from "../common/HideInStatic";
|
||||
|
||||
|
||||
export default React.memo(function FileMenu() {
|
||||
const dispatch = useDispatch();
|
||||
return (
|
||||
<Dropdown className="pull-left special" text="mitmproxy" options={{"placement": "bottom-start"}}>
|
||||
<MenuItem onClick={() => confirm('Delete all flows?') && dispatch(flowsActions.clear())}>
|
||||
<i className="fa fa-fw fa-trash"/> Clear All
|
||||
</MenuItem>
|
||||
<li>
|
||||
<FileChooser
|
||||
icon="fa-folder-open"
|
||||
text=" Open..."
|
||||
onClick={
|
||||
// stop event propagation: we must keep the input in DOM for upload to work.
|
||||
e => e.stopPropagation()
|
||||
}
|
||||
onOpenFile={file => {
|
||||
dispatch(flowsActions.upload(file));
|
||||
document.body.click(); // "restart" event propagation
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
<MenuItem onClick={() => dispatch(flowsActions.download())}>
|
||||
<i className="fa fa-fw fa-floppy-o"/> Save...
|
||||
</MenuItem>
|
||||
<HideInStatic>
|
||||
<Divider/>
|
||||
<li>
|
||||
<a href="http://mitm.it/" target="_blank">
|
||||
<i className="fa fa-fw fa-external-link"/> Install Certificates...
|
||||
</a>
|
||||
</li>
|
||||
</HideInStatic>
|
||||
</Dropdown>
|
||||
)
|
||||
});
|
@ -32,7 +32,7 @@ export default class FilterDocs extends Component {
|
||||
render() {
|
||||
const { doc } = this.state
|
||||
return !doc ? (
|
||||
<i className="fa fa-spinner fa-spin"></i>
|
||||
<i className="fa fa-spinner fa-spin"/>
|
||||
) : (
|
||||
<table className="table table-condensed">
|
||||
<tbody>
|
||||
@ -46,7 +46,7 @@ export default class FilterDocs extends Component {
|
||||
<td colSpan="2">
|
||||
<a href="https://mitmproxy.org/docs/latest/concepts-filters/"
|
||||
target="_blank">
|
||||
<i className="fa fa-external-link"></i>
|
||||
<i className="fa fa-external-link"/>
|
||||
mitmproxy docs</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -1,12 +1,25 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, {Component} from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import classnames from 'classnames'
|
||||
import { Key } from '../../utils.js'
|
||||
import {Key} from '../../utils'
|
||||
import Filt from '../../filt/filt'
|
||||
import FilterDocs from './FilterDocs'
|
||||
|
||||
export default class FilterInput extends Component {
|
||||
type FilterInputProps = {
|
||||
type: string
|
||||
color: any
|
||||
placeholder: string
|
||||
value: string
|
||||
onChange: (value) => void
|
||||
}
|
||||
|
||||
type FilterInputState = {
|
||||
value: string
|
||||
focus: boolean
|
||||
mousefocus: boolean
|
||||
}
|
||||
|
||||
export default class FilterInput extends Component<FilterInputProps, FilterInputState> {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
@ -14,7 +27,7 @@ export default class FilterInput extends Component {
|
||||
// Consider both focus and mouseover for showing/hiding the tooltip,
|
||||
// because onBlur of the input is triggered before the click on the tooltip
|
||||
// finalized, hiding the tooltip just as the user clicks on it.
|
||||
this.state = { value: this.props.value, focus: false, mousefocus: false }
|
||||
this.state = {value: this.props.value, focus: false, mousefocus: false}
|
||||
|
||||
this.onChange = this.onChange.bind(this)
|
||||
this.onFocus = this.onFocus.bind(this)
|
||||
@ -26,14 +39,13 @@ export default class FilterInput extends Component {
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
this.setState({ value: nextProps.value })
|
||||
this.setState({value: nextProps.value})
|
||||
}
|
||||
|
||||
isValid(filt) {
|
||||
try {
|
||||
const str = filt == null ? this.state.value : filt
|
||||
if (str) {
|
||||
Filt.parse(str)
|
||||
if (filt) {
|
||||
Filt.parse(filt)
|
||||
}
|
||||
return true
|
||||
} catch (e) {
|
||||
@ -54,7 +66,7 @@ export default class FilterInput extends Component {
|
||||
|
||||
onChange(e) {
|
||||
const value = e.target.value
|
||||
this.setState({ value })
|
||||
this.setState({value})
|
||||
|
||||
// Only propagate valid filters upwards.
|
||||
if (this.isValid(value)) {
|
||||
@ -63,19 +75,19 @@ export default class FilterInput extends Component {
|
||||
}
|
||||
|
||||
onFocus() {
|
||||
this.setState({ focus: true })
|
||||
this.setState({focus: true})
|
||||
}
|
||||
|
||||
onBlur() {
|
||||
this.setState({ focus: false })
|
||||
this.setState({focus: false})
|
||||
}
|
||||
|
||||
onMouseEnter() {
|
||||
this.setState({ mousefocus: true })
|
||||
this.setState({mousefocus: true})
|
||||
}
|
||||
|
||||
onMouseLeave() {
|
||||
this.setState({ mousefocus: false })
|
||||
this.setState({mousefocus: false})
|
||||
}
|
||||
|
||||
onKeyDown(e) {
|
||||
@ -101,12 +113,12 @@ export default class FilterInput extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { type, color, placeholder } = this.props
|
||||
const { value, focus, mousefocus } = this.state
|
||||
const {type, color, placeholder} = this.props
|
||||
const {value, focus, mousefocus} = this.state
|
||||
return (
|
||||
<div className={classnames('filter-input input-group', { 'has-error': !this.isValid() })}>
|
||||
<div className={classnames('filter-input input-group', {'has-error': !this.isValid(value)})}>
|
||||
<span className="input-group-addon">
|
||||
<i className={'fa fa-fw fa-' + type} style={{ color }}/>
|
||||
<i className={'fa fa-fw fa-' + type} style={{color}}/>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
@ -1,79 +0,0 @@
|
||||
import React from "react"
|
||||
import {connect} from "react-redux"
|
||||
import FilterInput from "./FilterInput"
|
||||
import {update as updateSettings} from "../../ducks/settings"
|
||||
import * as flowsActions from "../../ducks/flows"
|
||||
import {setFilter, setHighlight} from "../../ducks/flows"
|
||||
import Button from "../common/Button"
|
||||
|
||||
MainMenu.title = "Start"
|
||||
|
||||
export default function MainMenu() {
|
||||
return (
|
||||
<div className="main-menu">
|
||||
<div className="menu-group">
|
||||
<div className="menu-content">
|
||||
<FlowFilterInput/>
|
||||
<HighlightInput/>
|
||||
</div>
|
||||
<div className="menu-legend">Find</div>
|
||||
</div>
|
||||
|
||||
<div className="menu-group">
|
||||
<div className="menu-content">
|
||||
<InterceptInput/>
|
||||
<ResumeAll/>
|
||||
</div>
|
||||
<div className="menu-legend">Intercept</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function setIntercept(intercept) {
|
||||
return updateSettings({intercept})
|
||||
}
|
||||
|
||||
const InterceptInput = connect(
|
||||
state => ({
|
||||
value: state.settings.intercept || '',
|
||||
placeholder: 'Intercept',
|
||||
type: 'pause',
|
||||
color: 'hsl(208, 56%, 53%)'
|
||||
}),
|
||||
{onChange: setIntercept}
|
||||
)(FilterInput);
|
||||
|
||||
const FlowFilterInput = connect(
|
||||
state => ({
|
||||
value: state.flows.filter || '',
|
||||
placeholder: 'Search',
|
||||
type: 'search',
|
||||
color: 'black'
|
||||
}),
|
||||
{onChange: setFilter}
|
||||
)(FilterInput);
|
||||
|
||||
const HighlightInput = connect(
|
||||
state => ({
|
||||
value: state.flows.highlight || '',
|
||||
placeholder: 'Highlight',
|
||||
type: 'tag',
|
||||
color: 'hsl(48, 100%, 50%)'
|
||||
}),
|
||||
{onChange: setHighlight}
|
||||
)(FilterInput);
|
||||
|
||||
export function ResumeAll({resumeAll}) {
|
||||
return (
|
||||
<Button className="btn-sm" title="[a]ccept all"
|
||||
icon="fa-forward text-success" onClick={() => resumeAll()}>
|
||||
Resume All
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
ResumeAll = connect(
|
||||
null,
|
||||
{resumeAll: flowsActions.resumeAll}
|
||||
)(ResumeAll)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user