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:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- run: git rev-parse --abbrev-ref HEAD
|
- run: git rev-parse --abbrev-ref HEAD
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v2
|
||||||
- id: yarn-cache
|
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
|
||||||
- uses: actions/cache@v1
|
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.yarn-cache.outputs.dir }}
|
node-version: '14'
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
- 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: |
|
restore-keys: |
|
||||||
${{ runner.os }}-yarn-
|
${{ runner.OS }}-node-
|
||||||
|
${{ runner.OS }}-
|
||||||
- working-directory: ./web
|
- working-directory: ./web
|
||||||
run: yarn
|
run: npm ci
|
||||||
- working-directory: ./web
|
- working-directory: ./web
|
||||||
run: npm test
|
run: npm test
|
||||||
- uses: codecov/codecov-action@a1ed4b322b4b38cb846afb5a0ebfa17086917d27
|
- uses: codecov/codecov-action@a1ed4b322b4b38cb846afb5a0ebfa17086917d27
|
||||||
|
@ -134,7 +134,7 @@ formats = dict(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Export():
|
class Export:
|
||||||
def load(self, loader):
|
def load(self, loader):
|
||||||
loader.add_option(
|
loader.add_option(
|
||||||
"export_preserve_original_ip", bool, False,
|
"export_preserve_original_ip", bool, False,
|
||||||
|
@ -20,8 +20,8 @@ from mitmproxy import io
|
|||||||
from mitmproxy import log
|
from mitmproxy import log
|
||||||
from mitmproxy import optmanager
|
from mitmproxy import optmanager
|
||||||
from mitmproxy import version
|
from mitmproxy import version
|
||||||
|
from mitmproxy.addons import export
|
||||||
from mitmproxy.utils.strutils import always_str
|
from mitmproxy.utils.strutils import always_str
|
||||||
from mitmproxy.addons.export import curl_command
|
|
||||||
|
|
||||||
|
|
||||||
def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
||||||
@ -30,6 +30,8 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
flow: The original flow.
|
flow: The original flow.
|
||||||
|
|
||||||
|
Sync with web/src/flow.ts.
|
||||||
"""
|
"""
|
||||||
f = {
|
f = {
|
||||||
"id": flow.id,
|
"id": flow.id,
|
||||||
@ -43,31 +45,42 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
|||||||
if flow.client_conn:
|
if flow.client_conn:
|
||||||
f["client_conn"] = {
|
f["client_conn"] = {
|
||||||
"id": flow.client_conn.id,
|
"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,
|
"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_start": flow.client_conn.timestamp_start,
|
||||||
"timestamp_tls_setup": flow.client_conn.timestamp_tls_setup,
|
"timestamp_tls_setup": flow.client_conn.timestamp_tls_setup,
|
||||||
"timestamp_end": flow.client_conn.timestamp_end,
|
"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,
|
"cipher_name": flow.client_conn.cipher,
|
||||||
"alpn_proto_negotiated": always_str(flow.client_conn.alpn, "ascii", "backslashreplace"),
|
"alpn_proto_negotiated": always_str(flow.client_conn.alpn, "ascii", "backslashreplace"),
|
||||||
"tls_version": flow.client_conn.tls_version,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if flow.server_conn:
|
if flow.server_conn:
|
||||||
f["server_conn"] = {
|
f["server_conn"] = {
|
||||||
"id": flow.server_conn.id,
|
"id": flow.server_conn.id,
|
||||||
|
"peername": flow.server_conn.peername,
|
||||||
|
"sockname": flow.server_conn.sockname,
|
||||||
"address": flow.server_conn.address,
|
"address": flow.server_conn.address,
|
||||||
"ip_address": flow.server_conn.peername,
|
|
||||||
"source_address": flow.server_conn.sockname,
|
|
||||||
"tls_established": flow.server_conn.tls_established,
|
"tls_established": flow.server_conn.tls_established,
|
||||||
"sni": flow.server_conn.sni,
|
"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,
|
"tls_version": flow.server_conn.tls_version,
|
||||||
"timestamp_start": flow.server_conn.timestamp_start,
|
"timestamp_start": flow.server_conn.timestamp_start,
|
||||||
"timestamp_tcp_setup": flow.server_conn.timestamp_tcp_setup,
|
"timestamp_tcp_setup": flow.server_conn.timestamp_tcp_setup,
|
||||||
"timestamp_tls_setup": flow.server_conn.timestamp_tls_setup,
|
"timestamp_tls_setup": flow.server_conn.timestamp_tls_setup,
|
||||||
"timestamp_end": flow.server_conn.timestamp_end,
|
"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:
|
if flow.error:
|
||||||
f["error"] = flow.error.get_state()
|
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):
|
class APIError(tornado.web.HTTPError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -212,7 +219,7 @@ class IndexHandler(RequestHandler):
|
|||||||
def get(self):
|
def get(self):
|
||||||
token = self.xsrf_token # https://github.com/tornadoweb/tornado/issues/645
|
token = self.xsrf_token # https://github.com/tornadoweb/tornado/issues/645
|
||||||
assert token
|
assert token
|
||||||
self.render("index.html")
|
self.render("index.html", static=False, version=version.VERSION)
|
||||||
|
|
||||||
|
|
||||||
class FilterHelp(RequestHandler):
|
class FilterHelp(RequestHandler):
|
||||||
@ -274,8 +281,11 @@ class DumpFlows(RequestHandler):
|
|||||||
|
|
||||||
|
|
||||||
class ExportFlow(RequestHandler):
|
class ExportFlow(RequestHandler):
|
||||||
def post(self, flow_id):
|
def post(self, flow_id, format):
|
||||||
self.write(cURL_format_to_json(curl_command(self.flow)))
|
out = export.formats[format](self.flow)
|
||||||
|
self.write({
|
||||||
|
"export": always_str(out, "utf8", "backslashreplace")
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class ClearAll(RequestHandler):
|
class ClearAll(RequestHandler):
|
||||||
@ -448,42 +458,6 @@ class Events(RequestHandler):
|
|||||||
self.write([logentry_to_json(e) for e in self.master.events.data])
|
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):
|
class Options(RequestHandler):
|
||||||
def get(self):
|
def get(self):
|
||||||
self.write(optmanager.dump_dicts(self.master.options))
|
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):
|
class Application(tornado.web.Application):
|
||||||
master: "mitmproxy.tools.web.master.WebMaster"
|
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\-]+)/duplicate", DuplicateFlow),
|
||||||
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/replay", ReplayFlow),
|
(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\-]+)/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.data", FlowContent),
|
||||||
(
|
(
|
||||||
r"/flows/(?P<flow_id>[0-9a-f\-]+)/(?P<message>request|response)/content/(?P<content_view>[0-9a-zA-Z\-\_]+)(?:\.json)?",
|
r"/flows/(?P<flow_id>[0-9a-f\-]+)/(?P<message>request|response)/content/(?P<content_view>[0-9a-zA-Z\-\_]+)(?:\.json)?",
|
||||||
FlowContentView),
|
FlowContentView),
|
||||||
(r"/settings(?:\.json)?", Settings),
|
|
||||||
(r"/clear", ClearAll),
|
(r"/clear", ClearAll),
|
||||||
(r"/options(?:\.json)?", Options),
|
(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.events.sig_refresh.connect(self._sig_events_refresh)
|
||||||
|
|
||||||
self.options.changed.connect(self._sig_options_update)
|
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(*addons.default_addons())
|
||||||
self.addons.add(
|
self.addons.add(
|
||||||
@ -93,13 +92,6 @@ class WebMaster(master.Master):
|
|||||||
data=options_dict
|
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
|
def run(self): # pragma: no cover
|
||||||
AsyncIOMainLoop().install()
|
AsyncIOMainLoop().install()
|
||||||
iol = tornado.ioloop.IOLoop.instance()
|
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/vendor.css"/>
|
||||||
<link rel="stylesheet" href="/static/app.css"/>
|
<link rel="stylesheet" href="/static/app.css"/>
|
||||||
<link rel="icon" href="/static/images/favicon.ico" type="image/x-icon"/>
|
<link rel="icon" href="/static/images/favicon.ico" type="image/x-icon"/>
|
||||||
<script src="/static/static.js"></script>
|
<script src="/conf.js"></script>
|
||||||
<script src="/static/vendor.js"></script>
|
|
||||||
<script src="/static/app.js"></script>
|
<script src="/static/app.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -1,12 +1,19 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import io
|
||||||
|
import json
|
||||||
import json as _json
|
import json as _json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import typing
|
||||||
|
from contextlib import redirect_stdout
|
||||||
|
from pathlib import Path
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from mitmproxy.http import Headers
|
||||||
|
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
# workaround for
|
# workaround for
|
||||||
# https://github.com/tornadoweb/tornado/issues/2751
|
# https://github.com/tornadoweb/tornado/issues/2751
|
||||||
@ -18,7 +25,7 @@ import tornado.testing # noqa
|
|||||||
from tornado import httpclient # noqa
|
from tornado import httpclient # noqa
|
||||||
from tornado import websocket # 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.test import tflow # noqa
|
||||||
from mitmproxy.tools.web import app # noqa
|
from mitmproxy.tools.web import app # noqa
|
||||||
from mitmproxy.tools.web import master as webmaster # noqa
|
from mitmproxy.tools.web import master as webmaster # noqa
|
||||||
@ -35,7 +42,7 @@ def no_tornado_logging():
|
|||||||
logging.getLogger('tornado.general').disabled = False
|
logging.getLogger('tornado.general').disabled = False
|
||||||
|
|
||||||
|
|
||||||
def json(resp: httpclient.HTTPResponse):
|
def get_json(resp: httpclient.HTTPResponse):
|
||||||
return _json.loads(resp.body.decode())
|
return _json.loads(resp.body.decode())
|
||||||
|
|
||||||
|
|
||||||
@ -82,8 +89,8 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
|
|||||||
def test_flows(self):
|
def test_flows(self):
|
||||||
resp = self.fetch("/flows")
|
resp = self.fetch("/flows")
|
||||||
assert resp.code == 200
|
assert resp.code == 200
|
||||||
assert json(resp)[0]["request"]["contentHash"]
|
assert get_json(resp)[0]["request"]["contentHash"]
|
||||||
assert json(resp)[1]["error"]
|
assert get_json(resp)[1]["error"]
|
||||||
|
|
||||||
def test_flows_dump(self):
|
def test_flows_dump(self):
|
||||||
resp = self.fetch("/flows/dump")
|
resp = self.fetch("/flows/dump")
|
||||||
@ -251,7 +258,7 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
|
|||||||
f.revert()
|
f.revert()
|
||||||
|
|
||||||
def test_flow_content_view(self):
|
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": [
|
"lines": [
|
||||||
[["text", "content"]]
|
[["text", "content"]]
|
||||||
],
|
],
|
||||||
@ -261,17 +268,10 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
|
|||||||
def test_events(self):
|
def test_events(self):
|
||||||
resp = self.fetch("/events")
|
resp = self.fetch("/events")
|
||||||
assert resp.code == 200
|
assert resp.code == 200
|
||||||
assert json(resp)[0]["level"] == "info"
|
assert get_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
|
|
||||||
|
|
||||||
def test_options(self):
|
def test_options(self):
|
||||||
j = json(self.fetch("/options"))
|
j = get_json(self.fetch("/options"))
|
||||||
assert type(j) == dict
|
assert type(j) == dict
|
||||||
assert type(j['anticache']) == dict
|
assert type(j['anticache']) == dict
|
||||||
|
|
||||||
@ -296,18 +296,8 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
|
|||||||
self.master.options.anticomp = True
|
self.master.options.anticomp = True
|
||||||
|
|
||||||
r1 = yield ws_client.read_message()
|
r1 = yield ws_client.read_message()
|
||||||
r2 = yield ws_client.read_message()
|
response = _json.loads(r1)
|
||||||
j1 = _json.loads(r1)
|
assert response == {
|
||||||
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'] == {
|
|
||||||
"resource": "options",
|
"resource": "options",
|
||||||
"cmd": "update",
|
"cmd": "update",
|
||||||
"data": {
|
"data": {
|
||||||
@ -326,23 +316,69 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
|
|||||||
ws_client2 = yield websocket.websocket_connect(ws_url)
|
ws_client2 = yield websocket.websocket_connect(ws_url)
|
||||||
ws_client2.close()
|
ws_client2.close()
|
||||||
|
|
||||||
def _test_generate_tflow_js(self):
|
def test_generate_tflow_js(self):
|
||||||
_tflow = app.flow_to_json(tflow.tflow(resp=True, err=True))
|
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.
|
# 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['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['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)
|
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_json = re.sub(
|
||||||
tflow_path = os.path.join(web_root, 'src/js/__tests__/ducks/_tflow.js')
|
r'( {8}"(address|is_replay|alpn_proto_negotiated)":)',
|
||||||
|
r" //@ts-ignore\n\1",
|
||||||
|
tflow_json
|
||||||
|
).replace(": null", ": undefined")
|
||||||
|
|
||||||
content = (
|
content = (
|
||||||
f"/** Auto-generated by test_app.py:TestApp._test_generate_tflow_js */\n"
|
"/** Auto-generated by test_app.py:TestApp._test_generate_tflow_js */\n"
|
||||||
f"export default function(){{\n"
|
"import {HTTPFlow} from '../../flow';\n"
|
||||||
|
"export default function(): HTTPFlow {\n"
|
||||||
f" return {tflow_json}\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
|
# Quick Start
|
||||||
|
|
||||||
**Be sure to follow the Development Setup instructions found in the README.md,
|
- Install mitmproxy as described in [`../CONTRIBUTING.md`](../CONTRIBUTING.md)
|
||||||
and activate your virtualenv environment before proceeding.**
|
- 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
|
||||||
- Run `yarn` to install dependencies
|
[upgrade](https://github.com/nodesource/distributions/blob/master/README.md#installation-instructions).
|
||||||
- Run `yarn run gulp` to start live-compilation.
|
- Run `npm install` to install dependencies
|
||||||
- Run `mitmweb` and open http://localhost:8081/
|
- Run `npm start` to start live-compilation
|
||||||
|
- Run `mitmweb` after activating your Python virtualenv (see [`../CONTRIBUTING.md`](../CONTRIBUTING.md)).
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
- Run `yarn test` to run the test suite.
|
- Run `npm 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).
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@ -25,3 +19,18 @@ There are two components:
|
|||||||
- Server: [`mitmproxy/tools/web`](../mitmproxy/tools/web)
|
- Server: [`mitmproxy/tools/web`](../mitmproxy/tools/web)
|
||||||
|
|
||||||
- Client: `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 notify = require("gulp-notify");
|
||||||
const compilePeg = require("gulp-peg");
|
const compilePeg = require("gulp-peg");
|
||||||
const plumber = require("gulp-plumber");
|
const plumber = require("gulp-plumber");
|
||||||
|
const replace = require('gulp-replace');
|
||||||
const sourcemaps = require('gulp-sourcemaps');
|
const sourcemaps = require('gulp-sourcemaps');
|
||||||
const through = require("through2");
|
const through = require("through2");
|
||||||
|
|
||||||
@ -83,6 +84,9 @@ function peg() {
|
|||||||
return gulp.src(peg_src, {base: "src/"})
|
return gulp.src(peg_src, {base: "src/"})
|
||||||
.pipe(plumber(handleError))
|
.pipe(plumber(handleError))
|
||||||
.pipe(compilePeg())
|
.pipe(compilePeg())
|
||||||
|
.pipe(replace('module.exports = ',
|
||||||
|
'import * as flowutils from "../flow/utils"\n' +
|
||||||
|
'export default '))
|
||||||
.pipe(gulp.dest("src/"));
|
.pipe(gulp.dest("src/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
process.env.TZ = 'UTC';
|
module.exports = async () => {
|
||||||
|
|
||||||
module.exports = {
|
process.env.TZ = 'UTC';
|
||||||
|
|
||||||
|
return {
|
||||||
"testEnvironment": "jsdom",
|
"testEnvironment": "jsdom",
|
||||||
"testRegex": "__tests__/.*Spec.(js|ts)x?$",
|
"testRegex": "__tests__/.*Spec.(js|ts)x?$",
|
||||||
"roots": [
|
"roots": [
|
||||||
@ -15,5 +17,18 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
"collectCoverageFrom": [
|
"collectCoverageFrom": [
|
||||||
"src/js/**/*.{js,jsx,ts,tsx}"
|
"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": {
|
"dependencies": {
|
||||||
"@popperjs/core": "^2.9.2",
|
"@popperjs/core": "^2.9.2",
|
||||||
"bootstrap": "^3.3.7",
|
"bootstrap": "^3.4.1",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
|
"codemirror": "^5.62.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mock-xmlhttprequest": "^1.1.0",
|
"mock-xmlhttprequest": "^1.1.0",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-codemirror": "^1.0.0",
|
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-popper": "^2.2.5",
|
"react-popper": "^2.2.5",
|
||||||
"react-redux": "^7.2.4",
|
"react-redux": "^7.2.4",
|
||||||
"react-test-renderer": "^17.0.2",
|
"react-test-renderer": "^17.0.2",
|
||||||
"redux": "^4.1.0",
|
"redux": "^4.1.0",
|
||||||
"redux-logger": "^3.0.6",
|
|
||||||
"redux-mock-store": "^1.5.4",
|
"redux-mock-store": "^1.5.4",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"shallowequal": "^1.1.0",
|
"shallowequal": "^1.1.0",
|
||||||
"stable": "^0.1.8"
|
"stable": "^0.1.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.14.3",
|
"@testing-library/react": "^11.2.7",
|
||||||
"@babel/preset-env": "^7.14.4",
|
"@types/jest": "^26.0.23",
|
||||||
"@babel/preset-react": "^7.13.13",
|
"@types/redux-mock-store": "^1.0.2",
|
||||||
"@babel/preset-typescript": "^7.13.0",
|
"esbuild": "^0.12.9",
|
||||||
"babel-jest": "^27.0.2",
|
|
||||||
"esbuild": "^0.12.8",
|
|
||||||
"esbuild-jest": "^0.5.0",
|
"esbuild-jest": "^0.5.0",
|
||||||
"gulp": "^4.0.2",
|
"gulp": "^4.0.2",
|
||||||
"gulp-clean-css": "^4.3.0",
|
"gulp-clean-css": "^4.3.0",
|
||||||
"gulp-esbuild": "^0.8.1",
|
"gulp-esbuild": "^0.8.2",
|
||||||
"gulp-less": "^4.0.1",
|
"gulp-less": "^4.0.1",
|
||||||
"gulp-livereload": "^4.0.2",
|
"gulp-livereload": "^4.0.2",
|
||||||
"gulp-notify": "^4.0.0",
|
"gulp-notify": "^4.0.0",
|
||||||
"gulp-peg": "^0.2.0",
|
"gulp-peg": "^0.2.0",
|
||||||
"gulp-plumber": "^1.2.1",
|
"gulp-plumber": "^1.2.1",
|
||||||
|
"gulp-replace": "^1.1.3",
|
||||||
"gulp-sourcemaps": "^3.0.0",
|
"gulp-sourcemaps": "^3.0.0",
|
||||||
"jest": "^27.0.4",
|
"jest": "^27.0.4",
|
||||||
"through2": "^4.0.2"
|
"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;
|
padding: 3px 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -96,10 +96,6 @@
|
|||||||
|
|
||||||
.fa {
|
.fa {
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
|
|
||||||
&.pull-right {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.col-tls {
|
.col-tls {
|
||||||
@ -115,6 +111,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.col-path {
|
.col-path {
|
||||||
|
.fa {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.fa-repeat {
|
.fa-repeat {
|
||||||
color: green;
|
color: green;
|
||||||
}
|
}
|
||||||
@ -193,4 +193,8 @@
|
|||||||
.col-quickactions .fa-play {
|
.col-quickactions .fa-play {
|
||||||
transform: translate(1px, 2px);
|
transform: translate(1px, 2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.col-quickactions .fa-repeat {
|
||||||
|
transform: translate(-0px, 2px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,15 +104,6 @@ header {
|
|||||||
|
|
||||||
.filter-input {
|
.filter-input {
|
||||||
margin: 4px 0;
|
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 {
|
.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', () => {
|
it('should handle componentWillReceiveProps', () => {
|
||||||
contentLoader.updateContent = jest.fn()
|
contentLoader.updateContent = jest.fn()
|
||||||
contentLoader.componentWillReceiveProps({flow: tflow, message: tflow.request})
|
contentLoader.UNSAFE_componentWillReceiveProps({flow: tflow, message: tflow.request})
|
||||||
expect(contentLoader.updateContent).toBeCalled()
|
expect(contentLoader.updateContent).toBeCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ describe('ViewServer Component', () => {
|
|||||||
|
|
||||||
it('should handle componentWillReceiveProps', () => {
|
it('should handle componentWillReceiveProps', () => {
|
||||||
// case of fail to parse content
|
// case of fail to parse content
|
||||||
let viewSever = TestUtils.renderIntoDocument(
|
let viewServer = TestUtils.renderIntoDocument(
|
||||||
<PureViewServer
|
<PureViewServer
|
||||||
showFullContent={true}
|
showFullContent={true}
|
||||||
maxLines={10}
|
maxLines={10}
|
||||||
@ -64,10 +64,10 @@ describe('ViewServer Component', () => {
|
|||||||
content={JSON.stringify({lines: [['k1', 'v1']]})}
|
content={JSON.stringify({lines: [['k1', 'v1']]})}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
viewSever.componentWillReceiveProps({...viewSever.props, content: '{foo' })
|
viewServer.UNSAFE_componentWillReceiveProps({...viewServer.props, content: '{foo' })
|
||||||
let e = ''
|
let e = ''
|
||||||
try {JSON.parse('{foo') } catch(err){ e = err.message}
|
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
|
<div
|
||||||
className="view-options"
|
className="view-options"
|
||||||
>
|
>
|
||||||
|
<a
|
||||||
|
className="btn btn-default btn-xs pull-left"
|
||||||
|
href="#"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
<span>
|
<span>
|
||||||
<b>
|
<b>
|
||||||
View:
|
View:
|
||||||
</b>
|
</b>
|
||||||
edit
|
|
||||||
|
auto
|
||||||
|
|
||||||
|
<span
|
||||||
|
className="caret"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
className="btn btn-default btn-xs"
|
className="btn btn-default btn-xs"
|
||||||
@ -21,21 +32,9 @@ exports[`ContentViewOptions Component should render correctly 1`] = `
|
|||||||
/>
|
/>
|
||||||
</a>
|
</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>
|
</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', () => {
|
it('should render IconColumn', () => {
|
||||||
let iconColumn = renderer.create(<IconColumn flow={tflow}/>),
|
let tflow = TFlow(),
|
||||||
|
iconColumn = renderer.create(<IconColumn flow={tflow}/>),
|
||||||
tree = iconColumn.toJSON()
|
tree = iconColumn.toJSON()
|
||||||
// plain
|
// plain
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
@ -76,7 +77,8 @@ describe('Flowcolumns Components', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should render pathColumn', () => {
|
it('should render pathColumn', () => {
|
||||||
let pathColumn = renderer.create(<PathColumn flow={tflow}/>),
|
let tflow = TFlow(),
|
||||||
|
pathColumn = renderer.create(<PathColumn flow={tflow}/>),
|
||||||
tree = pathColumn.toJSON()
|
tree = pathColumn.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
|
|
||||||
@ -100,14 +102,14 @@ describe('Flowcolumns Components', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should render SizeColumn', () => {
|
it('should render SizeColumn', () => {
|
||||||
tflow = TFlow()
|
|
||||||
let sizeColumn = renderer.create(<SizeColumn flow={tflow}/>),
|
let sizeColumn = renderer.create(<SizeColumn flow={tflow}/>),
|
||||||
tree = sizeColumn.toJSON()
|
tree = sizeColumn.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render TimeColumn', () => {
|
it('should render TimeColumn', () => {
|
||||||
let timeColumn = renderer.create(<TimeColumn flow={tflow}/>),
|
let tflow = TFlow(),
|
||||||
|
timeColumn = renderer.create(<TimeColumn flow={tflow}/>),
|
||||||
tree = timeColumn.toJSON()
|
tree = timeColumn.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
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 React from 'react'
|
||||||
import renderer from 'react-test-renderer'
|
import FlowTableHead from '../../../components/FlowTable/FlowTableHead'
|
||||||
import ConnectedHead, { FlowTableHead } from '../../../components/FlowTable/FlowTableHead'
|
import {Provider} from 'react-redux'
|
||||||
import { Provider } from 'react-redux'
|
import {TStore} from '../../ducks/tutils'
|
||||||
import { TStore } from '../../ducks/tutils'
|
import {fireEvent, render, screen} from "@testing-library/react";
|
||||||
|
import {setSort} from "../../../ducks/flows";
|
||||||
|
|
||||||
|
|
||||||
describe('FlowTableHead Component', () => {
|
test("FlowTableHead Component", async () => {
|
||||||
let sortFn = jest.fn(),
|
|
||||||
store = TStore(),
|
const store = TStore(),
|
||||||
flowTableHead = renderer.create(
|
{asFragment} = render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<FlowTableHead setSort={sortFn} sortDesc={true}/>
|
<table>
|
||||||
</Provider>),
|
<thead>
|
||||||
tree =flowTableHead.toJSON()
|
<FlowTableHead/>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
</Provider>
|
||||||
|
)
|
||||||
|
expect(asFragment()).toMatchSnapshot()
|
||||||
|
|
||||||
it('should render correctly', () => {
|
fireEvent.click(screen.getByText("Size"))
|
||||||
expect(tree).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle click', () => {
|
expect(store.getActions()).toStrictEqual([
|
||||||
tree.children[0].props.onClick()
|
setSort("SizeColumn", false)
|
||||||
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()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
@ -110,7 +110,16 @@ exports[`Flowcolumns Components should render QuickActionsColumn 1`] = `
|
|||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<i
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -130,15 +139,17 @@ exports[`Flowcolumns Components should render StatusColumn 1`] = `
|
|||||||
className="col-status"
|
className="col-status"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"color": "darkred",
|
"color": "darkgreen",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
>
|
||||||
|
200
|
||||||
|
</td>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Flowcolumns Components should render TLSColumn 1`] = `
|
exports[`Flowcolumns Components should render TLSColumn 1`] = `
|
||||||
<td
|
<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
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`FlowTableHead Component should connect to state 1`] = `
|
exports[`FlowTableHead Component 1`] = `
|
||||||
<tr>
|
<DocumentFragment>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
<th
|
<th
|
||||||
className="col-tls"
|
class="col-tls"
|
||||||
onClick={[Function]}
|
/>
|
||||||
>
|
|
||||||
|
|
||||||
</th>
|
|
||||||
<th
|
<th
|
||||||
className="col-icon"
|
class="col-icon"
|
||||||
onClick={[Function]}
|
/>
|
||||||
>
|
|
||||||
|
|
||||||
</th>
|
|
||||||
<th
|
<th
|
||||||
className="col-path sort-desc"
|
class="col-path sort-desc"
|
||||||
onClick={[Function]}
|
|
||||||
>
|
>
|
||||||
Path
|
Path
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
className="col-method"
|
class="col-method"
|
||||||
onClick={[Function]}
|
|
||||||
>
|
>
|
||||||
Method
|
Method
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
className="col-status"
|
class="col-status"
|
||||||
onClick={[Function]}
|
|
||||||
>
|
>
|
||||||
Status
|
Status
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
className="col-size"
|
class="col-size"
|
||||||
onClick={[Function]}
|
|
||||||
>
|
>
|
||||||
Size
|
Size
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
className="col-time"
|
class="col-time"
|
||||||
onClick={[Function]}
|
|
||||||
>
|
>
|
||||||
Time
|
Time
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
className="col-quickactions"
|
class="col-quickactions"
|
||||||
onClick={[Function]}
|
/>
|
||||||
>
|
</tr>
|
||||||
|
</thead>
|
||||||
</th>
|
</table>
|
||||||
</tr>
|
</DocumentFragment>
|
||||||
`;
|
|
||||||
|
|
||||||
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>
|
|
||||||
`;
|
`;
|
||||||
|
@ -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
|
TLSv1.2
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<abbr
|
|
||||||
title="ALPN protocol negotiated"
|
|
||||||
>
|
|
||||||
ALPN:
|
|
||||||
</abbr>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
http/1.1
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
Resolved address:
|
Resolved address:
|
||||||
|
@ -189,69 +189,19 @@ exports[`Request Component should render correctly 1`] = `
|
|||||||
<table
|
<table
|
||||||
className="header-table"
|
className="header-table"
|
||||||
>
|
>
|
||||||
<tbody>
|
<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>
|
|
||||||
</table>
|
</table>
|
||||||
</article>
|
</article>
|
||||||
<footer>
|
<footer>
|
||||||
<div
|
<div
|
||||||
className="view-options"
|
className="view-options"
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="dropup pull-left"
|
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
className="btn btn-default btn-xs"
|
className="btn btn-default btn-xs pull-left"
|
||||||
href="#"
|
href="#"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
|
|
||||||
<b>
|
<b>
|
||||||
View:
|
View:
|
||||||
</b>
|
</b>
|
||||||
@ -261,45 +211,8 @@ exports[`Request Component should render correctly 1`] = `
|
|||||||
<span
|
<span
|
||||||
className="caret"
|
className="caret"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</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
|
<a
|
||||||
className="btn btn-default btn-xs"
|
className="btn btn-default btn-xs"
|
||||||
@ -542,17 +455,13 @@ exports[`Response Component should render correctly 1`] = `
|
|||||||
<footer>
|
<footer>
|
||||||
<div
|
<div
|
||||||
className="view-options"
|
className="view-options"
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="dropup pull-left"
|
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
className="btn btn-default btn-xs"
|
className="btn btn-default btn-xs pull-left"
|
||||||
href="#"
|
href="#"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
|
|
||||||
<b>
|
<b>
|
||||||
View:
|
View:
|
||||||
</b>
|
</b>
|
||||||
@ -562,45 +471,8 @@ exports[`Response Component should render correctly 1`] = `
|
|||||||
<span
|
<span
|
||||||
className="caret"
|
className="caret"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</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
|
<a
|
||||||
className="btn btn-default btn-xs"
|
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 React from 'react'
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import { FileMenu } from '../../../components/Header/FileMenu'
|
import FileMenu from '../../../components/Header/FileMenu'
|
||||||
|
import {Provider} from "react-redux";
|
||||||
global.confirm = jest.fn( s => true )
|
import {TStore} from "../../ducks/tutils";
|
||||||
|
|
||||||
describe('FileMenu Component', () => {
|
describe('FileMenu Component', () => {
|
||||||
let clearFn = jest.fn(),
|
|
||||||
loadFn = jest.fn(),
|
let store = TStore(),
|
||||||
saveFn = jest.fn(),
|
|
||||||
openModalFn = jest.fn(),
|
|
||||||
mockEvent = {
|
|
||||||
preventDefault: jest.fn(),
|
|
||||||
target: { files: ["foo", "bar "] }
|
|
||||||
},
|
|
||||||
createNodeMock = () => { return { click: jest.fn() }},
|
|
||||||
fileMenu = renderer.create(
|
fileMenu = renderer.create(
|
||||||
<FileMenu
|
<Provider store={store}>
|
||||||
clearFlows={clearFn}
|
<FileMenu/>
|
||||||
loadFlows={loadFn}
|
</Provider>
|
||||||
saveFlows={saveFn}
|
),
|
||||||
openModal={openModalFn}
|
|
||||||
/>,
|
|
||||||
{ createNodeMock }),
|
|
||||||
tree = fileMenu.toJSON()
|
tree = fileMenu.toJSON()
|
||||||
|
|
||||||
it('should render correctly', () => {
|
it('should render correctly', () => {
|
||||||
expect(tree).toMatchSnapshot()
|
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 React from 'react'
|
||||||
import renderer from 'react-test-renderer'
|
import MainMenu from '../../../components/Header/MainMenu'
|
||||||
import MainMenu, { setIntercept } from '../../../components/Header/MainMenu'
|
import {render} from "../../test-utils"
|
||||||
import { Provider } from 'react-redux'
|
|
||||||
import { update as updateSettings } from '../../../ducks/settings'
|
|
||||||
import { TStore } from '../../ducks/tutils'
|
|
||||||
|
|
||||||
describe('MainMenu Component', () => {
|
test("MainMenu", () => {
|
||||||
let store = TStore()
|
const {asFragment} = render(<MainMenu/>);
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
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' })
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import { MenuToggle, SettingsToggle, EventlogToggle } from '../../../components/Header/MenuToggle'
|
import {EventlogToggle, MenuToggle, OptionsToggle} from '../../../components/Header/MenuToggle'
|
||||||
import { Provider } from 'react-redux'
|
import {Provider} from 'react-redux'
|
||||||
import { REQUEST_UPDATE } from '../../../ducks/settings'
|
import {TStore} from '../../ducks/tutils'
|
||||||
import { TStore } from '../../ducks/tutils'
|
import * as optionsEditorActions from "../../../ducks/ui/optionsEditor"
|
||||||
|
import {fireEvent, render, screen} from "../../test-utils"
|
||||||
global.fetch = jest.fn()
|
|
||||||
|
|
||||||
describe('MenuToggle Component', () => {
|
describe('MenuToggle Component', () => {
|
||||||
it('should render correctly', () => {
|
it('should render correctly', () => {
|
||||||
@ -19,37 +18,26 @@ describe('MenuToggle Component', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('SettingToggle Component', () => {
|
test("OptionsToggle", async () => {
|
||||||
let store = TStore(),
|
const store = TStore(),
|
||||||
provider = renderer.create(
|
{asFragment} = render(
|
||||||
<Provider store={store}>
|
<OptionsToggle name='anticache'>toggle anticache</OptionsToggle>,
|
||||||
<SettingsToggle setting='anticache'>
|
{store}
|
||||||
<p>foo children</p>
|
);
|
||||||
</SettingsToggle>
|
|
||||||
</Provider>),
|
|
||||||
tree = provider.toJSON()
|
|
||||||
|
|
||||||
it('should render and connect to state', () => {
|
expect(asFragment()).toMatchSnapshot();
|
||||||
expect(tree).toMatchSnapshot()
|
fireEvent.click(screen.getByText("toggle anticache"));
|
||||||
})
|
expect(store.getActions()).toEqual([optionsEditorActions.startUpdate("anticache", true)])
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle change', () => {
|
test("EventlogToggle", async () => {
|
||||||
let menuToggle = tree.children[0].children[0]
|
const {asFragment, store} = render(
|
||||||
menuToggle.props.onChange()
|
<EventlogToggle/>
|
||||||
expect(store.getActions()).toEqual([{ type: REQUEST_UPDATE }])
|
);
|
||||||
})
|
expect(asFragment()).toMatchSnapshot();
|
||||||
})
|
|
||||||
|
expect(store.getState().eventLog.visible).toBeTruthy();
|
||||||
describe('EventlogToggle Component', () => {
|
fireEvent.click(screen.getByText("Display Event Log"));
|
||||||
let store = TStore(),
|
|
||||||
changFn = jest.fn(),
|
expect(store.getState().eventLog.visible).toBeFalsy();
|
||||||
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()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
@ -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
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`FileMenu Component should render correctly 1`] = `
|
exports[`FileMenu Component should render correctly 1`] = `
|
||||||
<div
|
<a
|
||||||
className="dropdown pull-left"
|
className="pull-left special"
|
||||||
|
href="#"
|
||||||
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<a
|
|
||||||
className="special"
|
|
||||||
href="#"
|
|
||||||
onClick={[Function]}
|
|
||||||
>
|
|
||||||
mitmproxy
|
mitmproxy
|
||||||
</a>
|
</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>
|
|
||||||
`;
|
`;
|
||||||
|
@ -1,122 +1,99 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`MainMenu Component should render and connect to state 1`] = `
|
exports[`MainMenu 1`] = `
|
||||||
<div
|
<DocumentFragment>
|
||||||
className="main-menu"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className="menu-group"
|
class="main-menu"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="menu-content"
|
class="menu-group"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="filter-input input-group"
|
class="menu-content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="filter-input input-group"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="input-group-addon"
|
class="input-group-addon"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
className="fa fa-fw fa-search"
|
class="fa fa-fw fa-search"
|
||||||
style={
|
style="color: black;"
|
||||||
Object {
|
|
||||||
"color": "black",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
className="form-control"
|
class="form-control"
|
||||||
onBlur={[Function]}
|
|
||||||
onChange={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
type="text"
|
type="text"
|
||||||
value="~u foo"
|
value="~d address"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="filter-input input-group"
|
class="filter-input input-group"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="input-group-addon"
|
class="input-group-addon"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
className="fa fa-fw fa-tag"
|
class="fa fa-fw fa-tag"
|
||||||
style={
|
style="color: rgb(0, 0, 0);"
|
||||||
Object {
|
|
||||||
"color": "hsl(48, 100%, 50%)",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
className="form-control"
|
class="form-control"
|
||||||
onBlur={[Function]}
|
|
||||||
onChange={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
placeholder="Highlight"
|
placeholder="Highlight"
|
||||||
type="text"
|
type="text"
|
||||||
value="~a bar"
|
value="~u /path"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="menu-legend"
|
class="menu-legend"
|
||||||
>
|
>
|
||||||
Find
|
Find
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="menu-group"
|
class="menu-group"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="menu-content"
|
class="menu-content"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="filter-input input-group"
|
class="filter-input input-group"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="input-group-addon"
|
class="input-group-addon"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
className="fa fa-fw fa-pause"
|
class="fa fa-fw fa-pause"
|
||||||
style={
|
style="color: rgb(68, 68, 68);"
|
||||||
Object {
|
|
||||||
"color": "hsl(208, 56%, 53%)",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
className="form-control"
|
class="form-control"
|
||||||
onBlur={[Function]}
|
|
||||||
onChange={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
placeholder="Intercept"
|
placeholder="Intercept"
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="btn-sm btn btn-default"
|
class="btn-sm btn btn-default"
|
||||||
onClick={[Function]}
|
|
||||||
title="[a]ccept all"
|
title="[a]ccept all"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
className="fa fa-fw fa-forward text-success"
|
class="fa fa-fw fa-forward text-success"
|
||||||
/>
|
/>
|
||||||
Resume All
|
Resume All
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="menu-legend"
|
class="menu-legend"
|
||||||
>
|
>
|
||||||
Intercept
|
Intercept
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
`;
|
`;
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`EventlogToggle Component should render and connect to state 1`] = `
|
exports[`EventlogToggle 1`] = `
|
||||||
<div
|
<DocumentFragment>
|
||||||
className="menu-entry"
|
<div
|
||||||
>
|
class="menu-entry"
|
||||||
|
>
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
checked={true}
|
checked=""
|
||||||
onChange={[Function]}
|
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
/>
|
/>
|
||||||
Display Event Log
|
Display Event Log
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`MenuToggle Component should render correctly 1`] = `
|
exports[`MenuToggle Component should render correctly 1`] = `
|
||||||
@ -32,19 +33,17 @@ exports[`MenuToggle Component should render correctly 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`SettingToggle Component should render and connect to state 1`] = `
|
exports[`OptionsToggle 1`] = `
|
||||||
<div
|
<DocumentFragment>
|
||||||
className="menu-entry"
|
<div
|
||||||
>
|
class="menu-entry"
|
||||||
|
>
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
checked={true}
|
|
||||||
onChange={[Function]}
|
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
/>
|
/>
|
||||||
<p>
|
toggle anticache
|
||||||
foo children
|
|
||||||
</p>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
`;
|
`;
|
||||||
|
@ -39,7 +39,7 @@ exports[`OptionMenu Component should render correctly 1`] = `
|
|||||||
>
|
>
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
checked={true}
|
checked={false}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
type="checkbox"
|
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 */
|
/** Auto-generated by test_app.py:TestApp._test_generate_tflow_js */
|
||||||
export default function(){
|
import {HTTPFlow} from '../../flow';
|
||||||
|
export default function(): HTTPFlow {
|
||||||
return {
|
return {
|
||||||
"client_conn": {
|
"client_conn": {
|
||||||
|
//@ts-ignore
|
||||||
"address": [
|
"address": [
|
||||||
"127.0.0.1",
|
"127.0.0.1",
|
||||||
22
|
22
|
||||||
],
|
],
|
||||||
|
"alpn": "http/1.1",
|
||||||
|
//@ts-ignore
|
||||||
"alpn_proto_negotiated": "http/1.1",
|
"alpn_proto_negotiated": "http/1.1",
|
||||||
|
"cipher": "cipher",
|
||||||
"cipher_name": "cipher",
|
"cipher_name": "cipher",
|
||||||
"id": "4a18d1a0-50a1-48dd-9aa6-d45d74282939",
|
"id": "4a18d1a0-50a1-48dd-9aa6-d45d74282939",
|
||||||
|
"peername": [
|
||||||
|
"127.0.0.1",
|
||||||
|
22
|
||||||
|
],
|
||||||
"sni": "address",
|
"sni": "address",
|
||||||
|
"sockname": [
|
||||||
|
"",
|
||||||
|
0
|
||||||
|
],
|
||||||
"timestamp_end": 946681206,
|
"timestamp_end": 946681206,
|
||||||
"timestamp_start": 946681200,
|
"timestamp_start": 946681200,
|
||||||
"timestamp_tls_setup": 946681201,
|
"timestamp_tls_setup": 946681201,
|
||||||
@ -22,8 +35,8 @@ export default function(){
|
|||||||
},
|
},
|
||||||
"id": "d91165be-ca1f-4612-88a9-c0f8696f3e29",
|
"id": "d91165be-ca1f-4612-88a9-c0f8696f3e29",
|
||||||
"intercepted": false,
|
"intercepted": false,
|
||||||
"is_replay": null,
|
"is_replay": undefined,
|
||||||
"marked": false,
|
"marked": "",
|
||||||
"modified": false,
|
"modified": false,
|
||||||
"request": {
|
"request": {
|
||||||
"contentHash": "ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73",
|
"contentHash": "ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73",
|
||||||
@ -40,6 +53,7 @@ export default function(){
|
|||||||
],
|
],
|
||||||
"host": "address",
|
"host": "address",
|
||||||
"http_version": "HTTP/1.1",
|
"http_version": "HTTP/1.1",
|
||||||
|
//@ts-ignore
|
||||||
"is_replay": false,
|
"is_replay": false,
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": "/path",
|
"path": "/path",
|
||||||
@ -47,13 +61,7 @@ export default function(){
|
|||||||
"pretty_host": "address",
|
"pretty_host": "address",
|
||||||
"scheme": "http",
|
"scheme": "http",
|
||||||
"timestamp_end": 946681201,
|
"timestamp_end": 946681201,
|
||||||
"timestamp_start": 946681200,
|
"timestamp_start": 946681200
|
||||||
"trailers": [
|
|
||||||
[
|
|
||||||
"trailer",
|
|
||||||
"qvalue"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"contentHash": "ab530a13e45914982b79f9b7e3fba994cfd1f3fb22f71cea1afbf02b460c6d1d",
|
"contentHash": "ab530a13e45914982b79f9b7e3fba994cfd1f3fb22f71cea1afbf02b460c6d1d",
|
||||||
@ -69,6 +77,7 @@ export default function(){
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"http_version": "HTTP/1.1",
|
"http_version": "HTTP/1.1",
|
||||||
|
//@ts-ignore
|
||||||
"is_replay": false,
|
"is_replay": false,
|
||||||
"reason": "OK",
|
"reason": "OK",
|
||||||
"status_code": 200,
|
"status_code": 200,
|
||||||
@ -82,17 +91,29 @@ export default function(){
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"server_conn": {
|
"server_conn": {
|
||||||
|
//@ts-ignore
|
||||||
"address": [
|
"address": [
|
||||||
"address",
|
"address",
|
||||||
22
|
22
|
||||||
],
|
],
|
||||||
"alpn_proto_negotiated": "http/1.1",
|
"alpn": undefined,
|
||||||
|
//@ts-ignore
|
||||||
|
"alpn_proto_negotiated": undefined,
|
||||||
|
"cipher": undefined,
|
||||||
"id": "f087e7b2-6d0a-41a8-a8f0-e1a4761395f8",
|
"id": "f087e7b2-6d0a-41a8-a8f0-e1a4761395f8",
|
||||||
"ip_address": [
|
"ip_address": [
|
||||||
"192.168.0.1",
|
"192.168.0.1",
|
||||||
22
|
22
|
||||||
],
|
],
|
||||||
|
"peername": [
|
||||||
|
"192.168.0.1",
|
||||||
|
22
|
||||||
|
],
|
||||||
"sni": "address",
|
"sni": "address",
|
||||||
|
"sockname": [
|
||||||
|
"address",
|
||||||
|
22
|
||||||
|
],
|
||||||
"source_address": [
|
"source_address": [
|
||||||
"address",
|
"address",
|
||||||
22
|
22
|
@ -6,7 +6,7 @@ describe('connection reducer', () => {
|
|||||||
it('should return initial state', () => {
|
it('should return initial state', () => {
|
||||||
expect(reduceConnection(undefined, {})).toEqual({
|
expect(reduceConnection(undefined, {})).toEqual({
|
||||||
state: ConnectionState.INIT,
|
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', () => {
|
describe('reduceState in js/ducks/index.js', () => {
|
||||||
it('should combine flow and header', () => {
|
it('should combine flow and header', () => {
|
||||||
let state = reduceState(undefined, {})
|
let state = rootReducer(undefined, {})
|
||||||
expect(state.hasOwnProperty('eventLog')).toBeTruthy()
|
expect(state.hasOwnProperty('eventLog')).toBeTruthy()
|
||||||
expect(state.hasOwnProperty('flows')).toBeTruthy()
|
expect(state.hasOwnProperty('flows')).toBeTruthy()
|
||||||
expect(state.hasOwnProperty('settings')).toBeTruthy()
|
|
||||||
expect(state.hasOwnProperty('connection')).toBeTruthy()
|
expect(state.hasOwnProperty('connection')).toBeTruthy()
|
||||||
expect(state.hasOwnProperty('ui')).toBeTruthy()
|
expect(state.hasOwnProperty('ui')).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
@ -1,23 +1,25 @@
|
|||||||
import reduceOptions, * as OptionsActions from '../../ducks/options'
|
import reduceOptions, * as OptionsActions from '../../ducks/options'
|
||||||
|
|
||||||
import configureStore from 'redux-mock-store'
|
import configureStore from 'redux-mock-store'
|
||||||
import thunk from 'redux-thunk'
|
import thunk from 'redux-thunk'
|
||||||
import * as OptionsEditorActions from '../../ducks/ui/optionsEditor'
|
import * as OptionsEditorActions from '../../ducks/ui/optionsEditor'
|
||||||
|
import {updateError} from "../../ducks/ui/optionsEditor";
|
||||||
|
|
||||||
const mockStore = configureStore([ thunk ])
|
const mockStore = configureStore([ thunk ])
|
||||||
|
|
||||||
describe('option reducer', () => {
|
describe('option reducer', () => {
|
||||||
it('should return initial state', () => {
|
it('should return initial state', () => {
|
||||||
expect(reduceOptions(undefined, {})).toEqual({})
|
expect(reduceOptions(undefined, {})).toEqual(OptionsActions.defaultState)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle receive action', () => {
|
it('should handle receive action', () => {
|
||||||
let action = { type: OptionsActions.RECEIVE, data: 'foo' }
|
let action = { type: OptionsActions.RECEIVE, data: {id: 'foo'} }
|
||||||
expect(reduceOptions(undefined, action)).toEqual('foo')
|
expect(reduceOptions(undefined, action)).toEqual({id: 'foo'})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle update action', () => {
|
it('should handle update action', () => {
|
||||||
let action = {type: OptionsActions.UPDATE, data: {id: 1} }
|
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', () => {
|
describe('sendUpdate', () => {
|
||||||
|
|
||||||
it('should handle error', () => {
|
it('should handle error', async () => {
|
||||||
let mockResponse = { status: 400, text: p => Promise.resolve('error') },
|
global.fetch = () => Promise.reject("fooerror");
|
||||||
promise = Promise.resolve(mockResponse)
|
await store.dispatch(OptionsActions.pureSendUpdate("bar", "error"))
|
||||||
global.fetch = r => { return promise }
|
|
||||||
OptionsActions.pureSendUpdate('bar', 'error')
|
|
||||||
expect(store.getActions()).toEqual([
|
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,
|
displayLarge: false,
|
||||||
viewDescription: '',
|
viewDescription: '',
|
||||||
showFullContent: false,
|
showFullContent: false,
|
||||||
modifiedFlow: false,
|
modifiedFlow: undefined,
|
||||||
contentView: 'Auto',
|
contentView: 'Auto',
|
||||||
tab: 'request',
|
tab: 'request',
|
||||||
content: [],
|
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 React from 'react'
|
||||||
import {render} from 'react-dom'
|
import {render} from 'react-dom'
|
||||||
import {applyMiddleware, compose, createStore} from 'redux'
|
|
||||||
import {Provider} from 'react-redux'
|
import {Provider} from 'react-redux'
|
||||||
import thunk from 'redux-thunk'
|
|
||||||
|
|
||||||
import ProxyApp from './components/ProxyApp'
|
import ProxyApp from './components/ProxyApp'
|
||||||
import rootReducer from './ducks/index'
|
|
||||||
import {add as addLog} from './ducks/eventLog'
|
import {add as addLog} from './ducks/eventLog'
|
||||||
import useUrlState from './urlState'
|
import useUrlState from './urlState'
|
||||||
import WebSocketBackend from './backends/websocket'
|
import WebSocketBackend from './backends/websocket'
|
||||||
import StaticBackend from './backends/static'
|
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)
|
useUrlState(store)
|
||||||
if (window.MITMWEB_STATIC) {
|
if (window.MITMWEB_STATIC) {
|
||||||
window.backend = new StaticBackend(store)
|
window.backend = new StaticBackend(store)
|
||||||
|
@ -12,7 +12,7 @@ export default class StaticBackend {
|
|||||||
|
|
||||||
onOpen() {
|
onOpen() {
|
||||||
this.fetchData("flows")
|
this.fetchData("flows")
|
||||||
this.fetchData("settings")
|
this.fetchData("options")
|
||||||
// this.fetchData("events") # TODO: Add events log to static viewer.
|
// this.fetchData("events") # TODO: Add events log to static viewer.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,6 @@ export default class WebsocketBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onOpen() {
|
onOpen() {
|
||||||
this.fetchData("settings")
|
|
||||||
this.fetchData("flows")
|
this.fetchData("flows")
|
||||||
this.fetchData("events")
|
this.fetchData("events")
|
||||||
this.fetchData("options")
|
this.fetchData("options")
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import Codemirror from 'react-codemirror';
|
import CodeMirror from "../../contrib/CodeMirror"
|
||||||
|
|
||||||
|
|
||||||
CodeEditor.propTypes = {
|
CodeEditor.propTypes = {
|
||||||
@ -15,7 +15,7 @@ export default function CodeEditor ( { content, onChange} ){
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="codeeditor" onKeyDown={e => e.stopPropagation()}>
|
<div className="codeeditor" onKeyDown={e => e.stopPropagation()}>
|
||||||
<Codemirror value={content} onChange={onChange} options={options}/>
|
<CodeMirror value={content} onChange={onChange} options={options}/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { MessageUtils } from '../../flow/utils.js'
|
import { MessageUtils } from '../../flow/utils'
|
||||||
|
|
||||||
export default function withContentLoader(View) {
|
export default function withContentLoader(View) {
|
||||||
|
|
||||||
@ -23,11 +23,11 @@ export default function withContentLoader(View) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
componentDidMount() {
|
||||||
this.updateContent(this.props)
|
this.updateContent(this.props)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
if (
|
if (
|
||||||
nextProps.message.content !== this.props.message.content ||
|
nextProps.message.content !== this.props.message.content ||
|
||||||
nextProps.message.contentHash !== this.props.message.contentHash ||
|
nextProps.message.contentHash !== this.props.message.contentHash ||
|
||||||
@ -51,7 +51,7 @@ export default function withContentLoader(View) {
|
|||||||
if (props.message.content !== undefined) {
|
if (props.message.content !== undefined) {
|
||||||
return this.setState({request: undefined, content: props.message.content})
|
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: ""})
|
return this.setState({request: undefined, content: ""})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,12 +38,12 @@ export class PureViewServer extends Component {
|
|||||||
setContent: PropTypes.func.isRequired
|
setContent: PropTypes.func.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount(){
|
UNSAFE_componentWillMount(){
|
||||||
this.setContentView(this.props)
|
this.setContentView(this.props)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps){
|
UNSAFE_componentWillReceiveProps(nextProps){
|
||||||
if (nextProps.content != this.props.content) {
|
if (nextProps.content !== this.props.content) {
|
||||||
this.setContentView(nextProps)
|
this.setContentView(nextProps)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -55,7 +55,7 @@ export class PureViewServer extends Component {
|
|||||||
this.data = {lines: [], description: err.message}
|
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)
|
props.setContent(this.data.lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { formatSize } from '../../utils.js'
|
import { formatSize } from '../../utils'
|
||||||
import UploadContentButton from './UploadContentButton'
|
import UploadContentButton from './UploadContentButton'
|
||||||
import DownloadContentButton from './DownloadContentButton'
|
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 FlowTableHead from './FlowTable/FlowTableHead'
|
||||||
import FlowRow from './FlowTable/FlowRow'
|
import FlowRow from './FlowTable/FlowRow'
|
||||||
import Filt from "../filt/filt"
|
import Filt from "../filt/filt"
|
||||||
import * as flowsActions from '../ducks/flows'
|
|
||||||
|
|
||||||
|
|
||||||
class FlowTable extends React.Component {
|
class FlowTable extends React.Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
selectFlow: PropTypes.func.isRequired,
|
|
||||||
flows: PropTypes.array.isRequired,
|
flows: PropTypes.array.isRequired,
|
||||||
rowHeight: PropTypes.number,
|
rowHeight: PropTypes.number,
|
||||||
highlight: PropTypes.string,
|
highlight: PropTypes.string,
|
||||||
@ -110,7 +108,6 @@ class FlowTable extends React.Component {
|
|||||||
flow={flow}
|
flow={flow}
|
||||||
selected={flow === selected}
|
selected={flow === selected}
|
||||||
highlighted={isHighlighted(flow)}
|
highlighted={isHighlighted(flow)}
|
||||||
onSelect={this.props.selectFlow}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<tr style={{ height: vScroll.paddingBottom }}/>
|
<tr style={{ height: vScroll.paddingBottom }}/>
|
||||||
@ -128,8 +125,5 @@ export default connect(
|
|||||||
flows: state.flows.view,
|
flows: state.flows.view,
|
||||||
highlight: state.flows.highlight,
|
highlight: state.flows.highlight,
|
||||||
selected: state.flows.byId[state.flows.selected[0]],
|
selected: state.flows.byId[state.flows.selected[0]],
|
||||||
}),
|
})
|
||||||
{
|
|
||||||
selectFlow: flowsActions.select,
|
|
||||||
}
|
|
||||||
)(PureFlowTable)
|
)(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 React from 'react'
|
||||||
import _ from 'lodash'
|
import {formatTimeDelta, formatTimeStamp} from '../../utils'
|
||||||
import { formatTimeStamp, formatTimeDelta } from '../../utils.js'
|
|
||||||
|
|
||||||
export function TimeStamp({ t, deltaTo, title }) {
|
export function TimeStamp({t, deltaTo, title}) {
|
||||||
return t ? (
|
return t ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td>{title}:</td>
|
<td>{title}:</td>
|
||||||
@ -20,7 +19,7 @@ export function TimeStamp({ t, deltaTo, title }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConnectionInfo({ conn }) {
|
export function ConnectionInfo({conn}) {
|
||||||
return (
|
return (
|
||||||
<table className="connection-table">
|
<table className="connection-table">
|
||||||
<tbody>
|
<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
|
// @todo We should fetch human-readable certificate representation from the server
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{flow.client_conn.cert && [
|
{flow.client_conn.cert && [
|
||||||
<h4 key="name">Client Certificate</h4>,
|
<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 && [
|
{flow.server_conn.cert && [
|
||||||
<h4 key="name">Server Certificate</h4>,
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Timing({ flow }) {
|
export function Timing({flow}) {
|
||||||
const { server_conn: sc, client_conn: cc, request: req, response: res } = flow
|
const {server_conn: sc, client_conn: cc, request: req, response: res} = flow
|
||||||
|
|
||||||
const timestamps = [
|
const timestamps = [
|
||||||
{
|
{
|
||||||
@ -142,7 +141,7 @@ export function Timing({ flow }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Details({ flow }) {
|
export default function Details({flow}) {
|
||||||
return (
|
return (
|
||||||
<section className="detail">
|
<section className="detail">
|
||||||
<h4>Client Connection</h4>
|
<h4>Client Connection</h4>
|
||||||
|
@ -3,8 +3,8 @@ import PropTypes from 'prop-types'
|
|||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
|
|
||||||
|
|
||||||
import { RequestUtils, isValidHttpVersion, parseUrl } from '../../flow/utils.js'
|
import { RequestUtils, isValidHttpVersion, parseUrl } from '../../flow/utils'
|
||||||
import { formatTimeStamp } from '../../utils.js'
|
import { formatTimeStamp } from '../../utils'
|
||||||
import ContentView from '../ContentView'
|
import ContentView from '../ContentView'
|
||||||
import ContentViewOptions from '../ContentView/ContentViewOptions'
|
import ContentViewOptions from '../ContentView/ContentViewOptions'
|
||||||
import ValidateEditor from '../ValueEditor/ValidateEditor'
|
import ValidateEditor from '../ValueEditor/ValidateEditor'
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import {formatSize} from '../utils'
|
||||||
import { connect } from 'react-redux'
|
|
||||||
import { formatSize } from '../utils.js'
|
|
||||||
import HideInStatic from '../components/common/HideInStatic'
|
import HideInStatic from '../components/common/HideInStatic'
|
||||||
|
import {useAppSelector} from "../ducks";
|
||||||
|
|
||||||
Footer.propTypes = {
|
export default function Footer() {
|
||||||
settings: PropTypes.object.isRequired,
|
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 (
|
return (
|
||||||
<footer>
|
<footer>
|
||||||
{mode && mode !== "regular" && (
|
{mode && mode !== "regular" && (
|
||||||
@ -22,7 +21,7 @@ function Footer({ settings }) {
|
|||||||
{showhost && (
|
{showhost && (
|
||||||
<span className="label label-success">showhost</span>
|
<span className="label label-success">showhost</span>
|
||||||
)}
|
)}
|
||||||
{no_upstream_cert && (
|
{!upstream_cert && (
|
||||||
<span className="label label-success">no-upstream-cert</span>
|
<span className="label label-success">no-upstream-cert</span>
|
||||||
)}
|
)}
|
||||||
{!rawtcp && (
|
{!rawtcp && (
|
||||||
@ -54,20 +53,14 @@ function Footer({ settings }) {
|
|||||||
{
|
{
|
||||||
server && (
|
server && (
|
||||||
<span className="label label-primary" title="HTTP Proxy Server Address">
|
<span className="label label-primary" title="HTTP Proxy Server Address">
|
||||||
{listen_host||"*"}:{listen_port}
|
{listen_host || "*"}:{listen_port}
|
||||||
</span>)
|
</span>)
|
||||||
}
|
}
|
||||||
</HideInStatic>
|
</HideInStatic>
|
||||||
<span className="label label-info" title="Mitmproxy Version">
|
<span className="label label-info" title="Mitmproxy Version">
|
||||||
v{version}
|
{version}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
|
||||||
state => ({
|
|
||||||
settings: state.settings,
|
|
||||||
})
|
|
||||||
)(Footer)
|
|
@ -1,16 +1,14 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import PropTypes from "prop-types"
|
import {ConnectionState} from "../../ducks/connection"
|
||||||
import { connect } from "react-redux"
|
import {useAppSelector} from "../../ducks";
|
||||||
import { ConnectionState } from "../../ducks/connection"
|
|
||||||
|
|
||||||
|
|
||||||
ConnectionIndicator.propTypes = {
|
export default React.memo(function ConnectionIndicator() {
|
||||||
state: PropTypes.symbol.isRequired,
|
|
||||||
message: PropTypes.string,
|
|
||||||
|
|
||||||
}
|
const connState = useAppSelector(state => state.connection.state),
|
||||||
export function ConnectionIndicator({ state, message }) {
|
message = useAppSelector(state => state.connection.message)
|
||||||
switch (state) {
|
|
||||||
|
switch (connState) {
|
||||||
case ConnectionState.INIT:
|
case ConnectionState.INIT:
|
||||||
return <span className="connection-indicator init">connecting…</span>;
|
return <span className="connection-indicator init">connecting…</span>;
|
||||||
case ConnectionState.FETCHING:
|
case ConnectionState.FETCHING:
|
||||||
@ -22,9 +20,8 @@ export function ConnectionIndicator({ state, message }) {
|
|||||||
title={message}>connection lost</span>;
|
title={message}>connection lost</span>;
|
||||||
case ConnectionState.OFFLINE:
|
case ConnectionState.OFFLINE:
|
||||||
return <span className="connection-indicator offline">offline</span>;
|
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() {
|
render() {
|
||||||
const { doc } = this.state
|
const { doc } = this.state
|
||||||
return !doc ? (
|
return !doc ? (
|
||||||
<i className="fa fa-spinner fa-spin"></i>
|
<i className="fa fa-spinner fa-spin"/>
|
||||||
) : (
|
) : (
|
||||||
<table className="table table-condensed">
|
<table className="table table-condensed">
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -46,7 +46,7 @@ export default class FilterDocs extends Component {
|
|||||||
<td colSpan="2">
|
<td colSpan="2">
|
||||||
<a href="https://mitmproxy.org/docs/latest/concepts-filters/"
|
<a href="https://mitmproxy.org/docs/latest/concepts-filters/"
|
||||||
target="_blank">
|
target="_blank">
|
||||||
<i className="fa fa-external-link"></i>
|
<i className="fa fa-external-link"/>
|
||||||
mitmproxy docs</a>
|
mitmproxy docs</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -1,12 +1,25 @@
|
|||||||
import React, { Component } from 'react'
|
import React, {Component} from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import { Key } from '../../utils.js'
|
import {Key} from '../../utils'
|
||||||
import Filt from '../../filt/filt'
|
import Filt from '../../filt/filt'
|
||||||
import FilterDocs from './FilterDocs'
|
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) {
|
constructor(props, context) {
|
||||||
super(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,
|
// Consider both focus and mouseover for showing/hiding the tooltip,
|
||||||
// because onBlur of the input is triggered before the click on 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.
|
// 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.onChange = this.onChange.bind(this)
|
||||||
this.onFocus = this.onFocus.bind(this)
|
this.onFocus = this.onFocus.bind(this)
|
||||||
@ -26,14 +39,13 @@ export default class FilterInput extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
this.setState({ value: nextProps.value })
|
this.setState({value: nextProps.value})
|
||||||
}
|
}
|
||||||
|
|
||||||
isValid(filt) {
|
isValid(filt) {
|
||||||
try {
|
try {
|
||||||
const str = filt == null ? this.state.value : filt
|
if (filt) {
|
||||||
if (str) {
|
Filt.parse(filt)
|
||||||
Filt.parse(str)
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -54,7 +66,7 @@ export default class FilterInput extends Component {
|
|||||||
|
|
||||||
onChange(e) {
|
onChange(e) {
|
||||||
const value = e.target.value
|
const value = e.target.value
|
||||||
this.setState({ value })
|
this.setState({value})
|
||||||
|
|
||||||
// Only propagate valid filters upwards.
|
// Only propagate valid filters upwards.
|
||||||
if (this.isValid(value)) {
|
if (this.isValid(value)) {
|
||||||
@ -63,19 +75,19 @@ export default class FilterInput extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onFocus() {
|
onFocus() {
|
||||||
this.setState({ focus: true })
|
this.setState({focus: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
onBlur() {
|
onBlur() {
|
||||||
this.setState({ focus: false })
|
this.setState({focus: false})
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseEnter() {
|
onMouseEnter() {
|
||||||
this.setState({ mousefocus: true })
|
this.setState({mousefocus: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseLeave() {
|
onMouseLeave() {
|
||||||
this.setState({ mousefocus: false })
|
this.setState({mousefocus: false})
|
||||||
}
|
}
|
||||||
|
|
||||||
onKeyDown(e) {
|
onKeyDown(e) {
|
||||||
@ -101,12 +113,12 @@ export default class FilterInput extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { type, color, placeholder } = this.props
|
const {type, color, placeholder} = this.props
|
||||||
const { value, focus, mousefocus } = this.state
|
const {value, focus, mousefocus} = this.state
|
||||||
return (
|
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">
|
<span className="input-group-addon">
|
||||||
<i className={'fa fa-fw fa-' + type} style={{ color }}/>
|
<i className={'fa fa-fw fa-' + type} style={{color}}/>
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
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