Fix #4382, add console integration tests
diff --git a/mitmproxy/master.py b/mitmproxy/master.py
index 720e19c3c1..f34e66f673 100644
--- a/mitmproxy/master.py
+++ b/mitmproxy/master.py
@@ -73,7 +73,7 @@ def run_loop(self, loop):
 
         self.addons.trigger("done")
 
-    def run(self, func=None):
+    def run(self):
         loop = asyncio.get_event_loop()
         self.run_loop(loop.run_forever)
 
diff --git a/mitmproxy/tools/console/consoleaddons.py b/mitmproxy/tools/console/consoleaddons.py
index 7d31811d6f..6d15056026 100644
--- a/mitmproxy/tools/console/consoleaddons.py
+++ b/mitmproxy/tools/console/consoleaddons.py
@@ -60,18 +60,6 @@ def websocket_end(self, f):
             f.close_message,
             f.close_reason))
 
-    def tcp_message(self, f):
-        message = f.messages[-1]
-        direction = "->" if message.from_client else "<-"
-        ctx.log.info("{client_host}:{client_port} {direction} tcp {direction} {server_host}:{server_port}".format(
-            client_host=f.client_conn.peername[0],
-            client_port=f.client_conn.peername[1],
-            server_host=f.server_conn.address[0],
-            server_port=f.server_conn.address[1],
-            direction=direction,
-        ))
-        ctx.log.debug(strutils.bytes_to_escaped_str(message.content))
-
 
 class ConsoleAddon:
     """
diff --git a/mitmproxy/tools/console/flowdetailview.py b/mitmproxy/tools/console/flowdetailview.py
index aa3d449c86..0844ac4d62 100644
--- a/mitmproxy/tools/console/flowdetailview.py
+++ b/mitmproxy/tools/console/flowdetailview.py
@@ -69,7 +69,7 @@ def flowdetails(state, flow: mitmproxy.flow.Flow):
             ]
 
             if c.altnames:
-                parts.append(("Alt names", ", ".join(strutils.bytes_to_escaped_str(x) for x in c.altnames)))
+                parts.append(("Alt names", ", ".join(c.altnames)))
             text.extend(
                 common.format_keyvals(parts, indent=4)
             )
diff --git a/mitmproxy/tools/console/master.py b/mitmproxy/tools/console/master.py
index 940f379fd4..f3d9887b3a 100644
--- a/mitmproxy/tools/console/master.py
+++ b/mitmproxy/tools/console/master.py
@@ -48,7 +48,6 @@ def __init__(self, opts):
 
         self.view_stack = []
 
-        signals.call_in.connect(self.sig_call_in)
         self.addons.add(*addons.default_addons())
         self.addons.add(
             intercept.Intercept(),
@@ -193,6 +192,7 @@ def run(self):
                   "Please run mitmproxy in an interactive shell environment.", file=sys.stderr)
             sys.exit(1)
 
+        signals.call_in.connect(self.sig_call_in)
         self.ui = window.Screen()
         self.ui.set_terminal_properties(256)
         self.set_palette(self.options, None)
diff --git a/mitmproxy/tools/console/window.py b/mitmproxy/tools/console/window.py
index fb2e8c1e6a..bb8bdb28cf 100644
--- a/mitmproxy/tools/console/window.py
+++ b/mitmproxy/tools/console/window.py
@@ -1,18 +1,18 @@
 import re
 
 import urwid
+from mitmproxy.tools.console import commands
 from mitmproxy.tools.console import common
-from mitmproxy.tools.console import signals
-from mitmproxy.tools.console import statusbar
+from mitmproxy.tools.console import eventlog
 from mitmproxy.tools.console import flowlist
 from mitmproxy.tools.console import flowview
-from mitmproxy.tools.console import commands
+from mitmproxy.tools.console import grideditor
+from mitmproxy.tools.console import help
 from mitmproxy.tools.console import keybindings
 from mitmproxy.tools.console import options
 from mitmproxy.tools.console import overlay
-from mitmproxy.tools.console import help
-from mitmproxy.tools.console import grideditor
-from mitmproxy.tools.console import eventlog
+from mitmproxy.tools.console import signals
+from mitmproxy.tools.console import statusbar
 
 
 class StackWidget(urwid.Frame):
@@ -52,23 +52,23 @@ class WindowStack:
     def __init__(self, master, base):
         self.master = master
         self.windows = dict(
-            flowlist = flowlist.FlowListBox(master),
-            flowview = flowview.FlowView(master),
-            commands = commands.Commands(master),
-            keybindings = keybindings.KeyBindings(master),
-            options = options.Options(master),
-            help = help.HelpView(master),
-            eventlog = eventlog.EventLog(master),
-
-            edit_focus_query = grideditor.QueryEditor(master),
-            edit_focus_cookies = grideditor.CookieEditor(master),
-            edit_focus_setcookies = grideditor.SetCookieEditor(master),
-            edit_focus_setcookie_attrs = grideditor.CookieAttributeEditor(master),
+            flowlist=flowlist.FlowListBox(master),
+            flowview=flowview.FlowView(master),
+            commands=commands.Commands(master),
+            keybindings=keybindings.KeyBindings(master),
+            options=options.Options(master),
+            help=help.HelpView(master),
+            eventlog=eventlog.EventLog(master),
+
+            edit_focus_query=grideditor.QueryEditor(master),
+            edit_focus_cookies=grideditor.CookieEditor(master),
+            edit_focus_setcookies=grideditor.SetCookieEditor(master),
+            edit_focus_setcookie_attrs=grideditor.CookieAttributeEditor(master),
             edit_focus_multipart_form=grideditor.RequestMultipartEditor(master),
             edit_focus_urlencoded_form=grideditor.RequestUrlEncodedEditor(master),
-            edit_focus_path = grideditor.PathEditor(master),
-            edit_focus_request_headers = grideditor.RequestHeaderEditor(master),
-            edit_focus_response_headers = grideditor.ResponseHeaderEditor(master),
+            edit_focus_path=grideditor.PathEditor(master),
+            edit_focus_request_headers=grideditor.RequestHeaderEditor(master),
+            edit_focus_response_headers=grideditor.ResponseHeaderEditor(master),
         )
         self.stack = [base]
         self.overlay = None
@@ -127,8 +127,8 @@ def __init__(self, master):
         self.statusbar = statusbar.StatusBar(master)
         super().__init__(
             None,
-            header = None,
-            footer = urwid.AttrWrap(self.statusbar, "background")
+            header=None,
+            footer=urwid.AttrWrap(self.statusbar, "background")
         )
         self.master = master
         self.master.view.sig_view_refresh.connect(self.view_changed)
@@ -295,8 +295,8 @@ def mouse_event(self, *args, **kwargs):
         if not k:
             if args[1] == "mouse drag":
                 signals.status_message.send(
-                    message = "Hold down fn, shift, alt or ctrl to select text or use the --set console_mouse=false parameter.",
-                    expire = 1
+                    message="Hold down fn, shift, alt or ctrl to select text or use the --set console_mouse=false parameter.",
+                    expire=1
                 )
             elif args[1] == "mouse press" and args[2] == 4:
                 self.keypress(args[0], "up")
diff --git a/test/mitmproxy/tools/console/test_integration.py b/test/mitmproxy/tools/console/test_integration.py
new file mode 100644
index 0000000000..feac177467
--- /dev/null
+++ b/test/mitmproxy/tools/console/test_integration.py
@@ -0,0 +1,46 @@
+import re
+import sys
+from typing import List
+
+import pytest
+
+import mitmproxy.options
+from mitmproxy import master
+from mitmproxy.tools.console import window
+from mitmproxy.tools.console.master import ConsoleMaster
+from test.conftest import skip_windows
+
+
+def tokenize(input: str) -> List[str]:
+    keys = []
+    for i, k in enumerate(re.split("[<>]", input)):
+        if i % 2:
+            keys.append(k)
+        else:
+            keys.extend(k)
+    return keys
+
+
+class ConsoleTestMaster(ConsoleMaster):
+    def type(self, input: str) -> None:
+        for key in tokenize(input):
+            self.window.keypress(self.ui.get_cols_rows(), key)
+
+
+@pytest.fixture
+def console(monkeypatch):
+    monkeypatch.setattr(window.Screen, "get_cols_rows", lambda self: (120, 120))
+    monkeypatch.setattr(master.Master, "run_loop", lambda *_: True)
+    monkeypatch.setattr(ConsoleTestMaster, "sig_call_in", lambda *_, **__: True)
+    monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
+
+    opts = mitmproxy.options.Options()
+    m = ConsoleTestMaster(opts)
+    m.run()
+    return m
+
+
+@skip_windows
+def test_integration(tdata, console):
+    console.type(f":view.flows.load {tdata.path('mitmproxy/data/dumpfile-018.bin')}<enter>")
+    console.type("<enter><tab><tab>")

This PR fixes #4382, and (more importantly) adds a harness to drive integration tests for the UI. We can expand on tests here in the future, but we already catch problems like #4382 now!

/cc @Prinzhorn 😉 