diff --git a/spyder_notebook/notebookplugin.py b/spyder_notebook/notebookplugin.py
index 35c41625..ff95bf3b 100644
--- a/spyder_notebook/notebookplugin.py
+++ b/spyder_notebook/notebookplugin.py
@@ -6,6 +6,7 @@
 """Notebook plugin."""
 
 # Standard library imports
+import logging
 import os.path as osp
 
 # Spyder imports
@@ -20,6 +21,8 @@
 from spyder_notebook.widgets.main_widget import NotebookMainWidget
 from spyder_notebook.utils.localization import _
 
+logger = logging.getLogger(__name__)
+
 
 class NotebookPlugin(SpyderDockablePlugin):
     """Spyder Notebook plugin."""
@@ -94,10 +97,11 @@ def open_notebook(self, filenames=None):
         self.get_widget().open_notebook(filenames)
 
     # ------ Private API ------------------------------------------------------
-    def _open_console(self, kernel_id, tab_name):
+    def _open_console(self, connection_file, tab_name):
         """Open an IPython console as requested."""
+        logger.info(f'Opening console with {connection_file=}')
         ipyconsole = self.get_plugin(Plugins.IPythonConsole)
-        ipyconsole.create_client_for_kernel(kernel_id)
+        ipyconsole.create_client_for_kernel(connection_file)
         ipyclient = ipyconsole.get_current_client()
         ipyclient.allow_rename = False
         ipyconsole.rename_client_tab(ipyclient, tab_name)
diff --git a/spyder_notebook/tests/test_plugin.py b/spyder_notebook/tests/test_plugin.py
index e50988f1..cb7b31f3 100644
--- a/spyder_notebook/tests/test_plugin.py
+++ b/spyder_notebook/tests/test_plugin.py
@@ -11,17 +11,14 @@
 import json
 import os
 import os.path as osp
-import shutil
-import sys
 from unittest.mock import Mock
 
 # Third-party library imports
 from flaky import flaky
 import pytest
-import requests
-from qtpy.QtCore import Qt, QTimer
 from qtpy.QtWebEngineWidgets import WEBENGINE
-from qtpy.QtWidgets import QFileDialog, QApplication, QLineEdit, QMainWindow
+from qtpy.QtWidgets import QMainWindow
+import requests
 
 # Spyder imports
 from spyder.api.plugins import Plugins
@@ -35,7 +32,6 @@
 # =============================================================================
 NOTEBOOK_UP = 40000
 CALLBACK_TIMEOUT = 10000
-INTERACTION_CLICK = 100
 LOCATION = osp.realpath(osp.join(os.getcwd(), osp.dirname(__file__)))
 
 
@@ -57,24 +53,6 @@ def text_present(nbwidget, qtbot, text="Test"):
         return text in nbwidget.dom.toHtml()
 
 
-def manage_save_dialog(qtbot, fname, directory=LOCATION):
-    """
-    Manage the QFileDialog when saving.
-
-    You can use this with QTimer to manage the QFileDialog.
-    Before calling anything that may show a QFileDialog for save call:
-    QTimer.singleShot(1000, lambda: manage_save_dialog(qtbot))
-    """
-    top_level_widgets = QApplication.topLevelWidgets()
-    for w in top_level_widgets:
-        if isinstance(w, QFileDialog):
-            if directory is not None:
-                w.setDirectory(directory)
-            input_field = w.findChildren(QLineEdit)[0]
-            input_field.setText(fname)
-            qtbot.keyClick(w, Qt.Key_Enter)
-
-
 def is_kernel_up(kernel_id, sessions_url):
     """Determine if the kernel with the id is up."""
     sessions_req = requests.get(sessions_url).content.decode()
@@ -141,172 +119,6 @@ def fake_get_server(filename, interpreter, start):
 # =============================================================================
 # Tests
 # =============================================================================
-@flaky(max_runs=5)
-def test_shutdown_notebook_kernel(notebook, qtbot):
-    """Test that kernel is shutdown from server when closing a notebook."""
-    # Wait for prompt
-    nbwidget = notebook.get_widget().tabwidget.currentWidget().notebookwidget
-    qtbot.waitUntil(lambda: prompt_present(nbwidget, qtbot),
-                    timeout=NOTEBOOK_UP)
-
-    # Get kernel id for the client
-    client = notebook.get_widget().tabwidget.currentWidget()
-    qtbot.waitUntil(lambda: client.get_kernel_id() is not None,
-                    timeout=NOTEBOOK_UP)
-    kernel_id = client.get_kernel_id()
-    sessions_url = client.get_session_url()
-
-    # Close the current client
-    notebook.get_widget().tabwidget.close_client()
-
-    # Assert that the kernel is down for the closed client
-    assert not is_kernel_up(kernel_id, sessions_url)
-
-
-def test_file_in_temp_dir_deleted_after_notebook_closed(notebook, qtbot):
-    """Test that notebook file in temporary directory is deleted after the
-    notebook is closed."""
-    # Wait for prompt
-    nbwidget = notebook.get_widget().tabwidget.currentWidget().notebookwidget
-    qtbot.waitUntil(lambda: prompt_present(nbwidget, qtbot),
-                    timeout=NOTEBOOK_UP)
-
-    # Get file name
-    client = notebook.get_widget().tabwidget.currentWidget()
-    filename = client.get_filename()
-
-    # Close the current client
-    notebook.get_widget().tabwidget.close_client()
-
-    # Assert file is deleted
-    assert not osp.exists(filename)
-
-
-@flaky(max_runs=3)
-def test_close_nonexisting_notebook(notebook, qtbot):
-    """Test that we can close a tab if the notebook file does not exist.
-    Regression test for spyder-ide/spyder-notebook#187."""
-    # Set up tab with non-existingg notebook
-    filename = osp.join(LOCATION, 'does-not-exist.ipynb')
-    notebook.open_notebook(filenames=[filename])
-    nbwidget = notebook.get_widget().tabwidget.currentWidget().notebookwidget
-    qtbot.waitUntil(lambda: prompt_present(nbwidget, qtbot),
-                    timeout=NOTEBOOK_UP)
-    client = notebook.get_widget().tabwidget.currentWidget()
-
-    # Close tab
-    notebook.get_widget().tabwidget.close_client()
-
-    # Assert tab is closed (without raising an exception)
-    for client_index in range(notebook.get_widget().tabwidget.count()):
-        assert client != notebook.get_widget().tabwidget.widget(client_index)
-
-
-# TODO Find out what goes wrong on Mac
-@flaky(max_runs=3)
-@pytest.mark.skipif(sys.platform == 'darwin', reason='Prompt never comes up')
-def test_open_notebook_in_non_ascii_dir(notebook, qtbot, tmpdir):
-    """Test that a notebook can be opened from a non-ascii directory."""
-    # Move the test file to non-ascii directory
-    test_notebook = osp.join(LOCATION, 'test.ipynb')
-    test_notebook_non_ascii = osp.join(str(tmpdir), u'äöüß', 'test.ipynb')
-    os.mkdir(os.path.join(str(tmpdir), u'äöüß'))
-    shutil.copyfile(test_notebook, test_notebook_non_ascii)
-
-    # Wait for prompt
-    notebook.open_notebook(filenames=[test_notebook_non_ascii])
-    nbwidget = notebook.get_widget().tabwidget.currentWidget().notebookwidget
-    qtbot.waitUntil(lambda: prompt_present(nbwidget, qtbot),
-                    timeout=NOTEBOOK_UP)
-
-    # Assert that the In prompt has "Test" in it
-    # and the client has the correct name
-    qtbot.waitUntil(lambda: text_present(nbwidget, qtbot),
-                    timeout=NOTEBOOK_UP)
-    assert text_present(nbwidget, qtbot)
-    assert notebook.get_widget().tabwidget.currentWidget().get_short_name() ==\
-           "test"
-
-
-@flaky(max_runs=3)
-@pytest.mark.skipif(not sys.platform.startswith('linux'),
-                    reason='Test hangs on CI on Windows and MacOS')
-def test_save_notebook(notebook, qtbot, tmpdir):
-    """Test that a notebook can be saved."""
-    # Wait for prompt
-    nbwidget = notebook.get_widget().tabwidget.currentWidget().notebookwidget
-    qtbot.waitUntil(lambda: prompt_present(nbwidget, qtbot),
-                    timeout=NOTEBOOK_UP)
-
-    # Writes: a = "test"
-    qtbot.keyClick(nbwidget, Qt.Key_A, delay=INTERACTION_CLICK)
-    qtbot.keyClick(nbwidget, Qt.Key_Space, delay=INTERACTION_CLICK)
-    qtbot.keyClick(nbwidget, Qt.Key_Equal, delay=INTERACTION_CLICK)
-    qtbot.keyClick(nbwidget, Qt.Key_Space, delay=INTERACTION_CLICK)
-    qtbot.keyClick(nbwidget, Qt.Key_QuoteDbl, delay=INTERACTION_CLICK)
-    qtbot.keyClick(nbwidget, Qt.Key_T, delay=INTERACTION_CLICK)
-    qtbot.keyClick(nbwidget, Qt.Key_E, delay=INTERACTION_CLICK)
-    qtbot.keyClick(nbwidget, Qt.Key_S, delay=INTERACTION_CLICK)
-    qtbot.keyClick(nbwidget, Qt.Key_T, delay=INTERACTION_CLICK)
-    qtbot.keyClick(nbwidget, Qt.Key_QuoteDbl, delay=INTERACTION_CLICK)
-
-    # Save the notebook
-    name = osp.join(str(tmpdir), 'save.ipynb')
-    QTimer.singleShot(1000, lambda: manage_save_dialog(qtbot, fname=name))
-    notebook.get_widget().save_as()
-
-    # Wait for prompt
-    nbwidget = notebook.get_widget().tabwidget.currentWidget().notebookwidget
-    qtbot.waitUntil(lambda: prompt_present(nbwidget, qtbot),
-                    timeout=NOTEBOOK_UP)
-
-    # Assert that the In prompt has "test" in it
-    # and the client has the correct name
-    qtbot.waitUntil(lambda: text_present(nbwidget, qtbot, text="test"),
-                    timeout=NOTEBOOK_UP)
-    assert text_present(nbwidget, qtbot, text="test")
-    assert notebook.get_widget().tabwidget.currentWidget().get_short_name() ==\
-           "save"
-
-
-@flaky(max_runs=3)
-@pytest.mark.skipif(os.name == 'nt',
-                    reason='Test hangs often on CI on Windows')
-def test_save_notebook_as_with_error(mocker, notebook, qtbot, tmpdir):
-    """Test that errors are handled in save_as()."""
-    # Wait for prompt
-    nbwidget = notebook.get_widget().tabwidget.currentWidget().notebookwidget
-    qtbot.waitUntil(lambda: prompt_present(nbwidget, qtbot),
-                    timeout=NOTEBOOK_UP)
-
-    # Set up mocks
-    name = osp.join(str(tmpdir), 'save.ipynb')
-    mocker.patch('spyder_notebook.widgets.notebooktabwidget.getsavefilename',
-                 return_value=(name, 'ignored'))
-    mocker.patch('spyder_notebook.widgets.notebooktabwidget.nbformat.write',
-                 side_effect=PermissionError)
-    mock_critical = mocker.patch('spyder_notebook.widgets.notebooktabwidget'
-                                 '.QMessageBox.critical')
-
-    # Save the notebook
-    notebook.get_widget().save_as()
-
-    # Assert that message box is displayed (reporting error raised by write)
-    assert mock_critical.called
-
-
-@flaky(max_runs=3)
-def test_new_notebook(notebook, qtbot):
-    """Test that a new client is really a notebook."""
-    # Wait for prompt
-    nbwidget = notebook.get_widget().tabwidget.currentWidget().notebookwidget
-    qtbot.waitUntil(lambda: prompt_present(nbwidget, qtbot),
-                    timeout=NOTEBOOK_UP)
-
-    # Assert that we have one notebook and the welcome page
-    assert notebook.get_widget().tabwidget.count() == 2
-
-
 # Teardown sometimes fails on Mac with Python 3.8 due to NoProcessException
 # in shutdown_server() in notebookapp.py in external notebook library
 @flaky
diff --git a/spyder_notebook/widgets/main_widget.py b/spyder_notebook/widgets/main_widget.py
index e403d364..e76c7394 100644
--- a/spyder_notebook/widgets/main_widget.py
+++ b/spyder_notebook/widgets/main_widget.py
@@ -4,7 +4,11 @@
 # Licensed under the terms of the MIT License
 # (see LICENSE.txt for details)
 
+# Standard library imports
+import os.path as osp
+
 # Third-party imports
+from jupyter_core.paths import jupyter_runtime_dir
 from qtpy.QtCore import Signal
 from qtpy.QtWidgets import QMessageBox, QVBoxLayout
 
@@ -55,8 +59,8 @@ class NotebookMainWidget(PluginMainWidget):
 
     Parameters
     -----------
-    kernel_id: str
-        Id of the kernel to open a console for.
+    connection_file: str
+        Name of the connection file for the kernel to open a console for.
     tab_name: str
         Tab name to set for the created console.
     """
@@ -317,8 +321,10 @@ def open_console(self, client=None):
             )
             return
 
+        connection_file = f'kernel-{kernel_id}.json'
+        connection_file = osp.join(jupyter_runtime_dir(), connection_file)
         self.sig_open_console_requested.emit(
-            kernel_id,
+            connection_file,
             client.get_short_name()
         )
 
diff --git a/spyder_notebook/tests/test.ipynb b/spyder_notebook/widgets/tests/test.ipynb
similarity index 100%
rename from spyder_notebook/tests/test.ipynb
rename to spyder_notebook/widgets/tests/test.ipynb
diff --git a/spyder_notebook/widgets/tests/test_main_widget.py b/spyder_notebook/widgets/tests/test_main_widget.py
new file mode 100644
index 00000000..bff6e5f5
--- /dev/null
+++ b/spyder_notebook/widgets/tests/test_main_widget.py
@@ -0,0 +1,250 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+#
+
+"""Tests for NotebookMainWidget"""
+
+# Standard library imports
+import os
+import os.path as osp
+import shutil
+import sys
+from unittest.mock import MagicMock
+
+# Third-party library imports
+from flaky import flaky
+import pytest
+from qtpy.QtCore import Qt, QTimer
+from qtpy.QtWidgets import QFileDialog, QApplication, QLineEdit
+
+# Local imports
+from spyder_notebook.tests.test_plugin import (
+    is_kernel_up, prompt_present, text_present)
+from spyder_notebook.widgets.main_widget import NotebookMainWidget
+
+# =============================================================================
+# Constants
+# =============================================================================
+NOTEBOOK_UP = 40000
+INTERACTION_CLICK = 100
+LOCATION = osp.realpath(osp.join(os.getcwd(), osp.dirname(__file__)))
+
+
+# =============================================================================
+# Utility functions
+# =============================================================================
+def manage_save_dialog(qtbot, fname, directory=LOCATION):
+    """
+    Manage the QFileDialog when saving.
+
+    You can use this with QTimer to manage the QFileDialog.
+    Before calling anything that may show a QFileDialog for save call:
+    QTimer.singleShot(1000, lambda: manage_save_dialog(qtbot))
+    """
+    top_level_widgets = QApplication.topLevelWidgets()
+    for w in top_level_widgets:
+        if isinstance(w, QFileDialog):
+            if directory is not None:
+                w.setDirectory(directory)
+            input_field = w.findChildren(QLineEdit)[0]
+            input_field.setText(fname)
+            qtbot.keyClick(w, Qt.Key_Enter)
+
+
+# =============================================================================
+# Fixtures
+# =============================================================================
+@pytest.fixture
+def main_widget(qtbot):
+    """Set up a NotebookMainWidget, with no tabs."""
+    mock_plugin = MagicMock()
+    mock_plugin.CONF_SECTION = 'mock conf section'
+
+    main_widget = NotebookMainWidget('testwidget', mock_plugin, None)
+    main_widget.setup()
+    main_widget.show()  # Prompt only appears if widget is displayed
+    
+    yield main_widget
+
+    main_widget.close()
+
+
+# =============================================================================
+# Tests
+# =============================================================================
+@flaky(max_runs=3)
+def test_new_notebook(main_widget, qtbot):
+    """Test that a new client is really a notebook."""
+    # Create new notebook tab and check that there is a prompt
+    main_widget.create_new_client()
+    nbwidget = main_widget.tabwidget.currentWidget().notebookwidget
+    qtbot.waitUntil(lambda: prompt_present(nbwidget, qtbot),
+                    timeout=NOTEBOOK_UP)
+
+
+@flaky(max_runs=3)
+@pytest.mark.skipif(sys.platform == 'darwin', reason='Prompt never comes up')
+def test_open_notebook_in_non_ascii_dir(main_widget, qtbot, tmpdir):
+    """Test that a notebook can be opened from a non-ascii directory."""
+    # Copy the test file to non-ascii directory
+    test_notebook = osp.join(LOCATION, 'test.ipynb')
+    test_notebook_non_ascii = osp.join(str(tmpdir), u'äöüß', 'test.ipynb')
+    os.mkdir(os.path.join(str(tmpdir), u'äöüß'))
+    shutil.copyfile(test_notebook, test_notebook_non_ascii)
+
+    # Open the test notebook and wait for prompt
+    main_widget.open_notebook(filenames=[test_notebook_non_ascii])
+    client = main_widget.tabwidget.currentWidget()
+    nbwidget = client.notebookwidget
+    qtbot.waitUntil(lambda: prompt_present(nbwidget, qtbot),
+                    timeout=NOTEBOOK_UP)
+
+    # Assert that the In prompt has "Test" in it
+    # and the client has the correct name
+    qtbot.waitUntil(lambda: text_present(nbwidget, qtbot),
+                    timeout=NOTEBOOK_UP)
+    assert text_present(nbwidget, qtbot)
+    assert client.get_short_name() == "test"
+
+
+@flaky(max_runs=3)
+@pytest.mark.skipif(not sys.platform.startswith('linux'),
+                    reason='Test hangs on CI on Windows and MacOS')
+def test_save_notebook(main_widget, qtbot, tmpdir):
+    """Test that a notebook can be saved."""
+    # Create new notebook tab and wait for prompt
+    main_widget.create_new_client()
+    nbwidget = main_widget.tabwidget.currentWidget().notebookwidget
+    qtbot.waitUntil(lambda: prompt_present(nbwidget, qtbot),
+                    timeout=NOTEBOOK_UP)
+
+    # Writes: a = "test"
+    qtbot.keyClick(nbwidget, Qt.Key_A, delay=INTERACTION_CLICK)
+    qtbot.keyClick(nbwidget, Qt.Key_Space, delay=INTERACTION_CLICK)
+    qtbot.keyClick(nbwidget, Qt.Key_Equal, delay=INTERACTION_CLICK)
+    qtbot.keyClick(nbwidget, Qt.Key_Space, delay=INTERACTION_CLICK)
+    qtbot.keyClick(nbwidget, Qt.Key_QuoteDbl, delay=INTERACTION_CLICK)
+    qtbot.keyClick(nbwidget, Qt.Key_T, delay=INTERACTION_CLICK)
+    qtbot.keyClick(nbwidget, Qt.Key_E, delay=INTERACTION_CLICK)
+    qtbot.keyClick(nbwidget, Qt.Key_S, delay=INTERACTION_CLICK)
+    qtbot.keyClick(nbwidget, Qt.Key_T, delay=INTERACTION_CLICK)
+    qtbot.keyClick(nbwidget, Qt.Key_QuoteDbl, delay=INTERACTION_CLICK)
+
+    # Save the notebook
+    name = osp.join(str(tmpdir), 'save.ipynb')
+    QTimer.singleShot(1000, lambda: manage_save_dialog(qtbot, fname=name))
+    main_widget.save_as()
+
+    # Wait for prompt
+    client = main_widget.tabwidget.currentWidget()
+    nbwidget = client.notebookwidget
+    qtbot.waitUntil(lambda: prompt_present(nbwidget, qtbot),
+                    timeout=NOTEBOOK_UP)
+
+    # Assert that the In prompt has "test" in it
+    # and the client has the correct name
+    qtbot.waitUntil(lambda: text_present(nbwidget, qtbot, text="test"),
+                    timeout=NOTEBOOK_UP)
+    assert text_present(nbwidget, qtbot, text="test")
+    assert client.get_short_name() == "save"
+
+
+@flaky(max_runs=3)
+@pytest.mark.skipif(os.name == 'nt',
+                    reason='Test hangs often on CI on Windows')
+def test_save_notebook_as_with_error(main_widget, mocker, qtbot, tmpdir):
+    """Test that errors are handled in save_as()."""
+    # Create new notebook tab and wait for prompt
+    main_widget.create_new_client()
+    nbwidget = main_widget.tabwidget.currentWidget().notebookwidget
+    qtbot.waitUntil(lambda: prompt_present(nbwidget, qtbot),
+                    timeout=NOTEBOOK_UP)
+
+    # Set up mocks
+    name = osp.join(str(tmpdir), 'save.ipynb')
+    mocker.patch('spyder_notebook.widgets.notebooktabwidget.getsavefilename',
+                 return_value=(name, 'ignored'))
+    mocker.patch('spyder_notebook.widgets.notebooktabwidget.nbformat.write',
+                 side_effect=PermissionError)
+    mock_critical = mocker.patch('spyder_notebook.widgets.notebooktabwidget'
+                                 '.QMessageBox.critical')
+
+    # Save the notebook
+    main_widget.save_as()
+
+    # Assert that message box is displayed (reporting error raised by write)
+    assert mock_critical.called
+
+
+@flaky(max_runs=5)
+def test_shutdown_notebook_kernel(main_widget, qtbot):
+    """Test that kernel is shut down when closing a notebook."""
+    # Create new notebook tab and wait for prompt
+    main_widget.create_new_client()
+    client = main_widget.tabwidget.currentWidget()
+    nbwidget = client.notebookwidget
+    qtbot.waitUntil(lambda: prompt_present(nbwidget, qtbot),
+                    timeout=NOTEBOOK_UP)
+
+    # Get kernel id for the client
+    qtbot.waitUntil(lambda: client.get_kernel_id() is not None,
+                    timeout=NOTEBOOK_UP)
+    kernel_id = client.get_kernel_id()
+    
+    # Assert that kernel is up
+    sessions_url = client.get_session_url()
+    assert is_kernel_up(kernel_id, sessions_url)
+
+    # Close the current client
+    main_widget.tabwidget.close_client()
+
+    # Assert that the kernel is down for the closed client
+    assert not is_kernel_up(kernel_id, sessions_url)
+
+
+def test_file_in_temp_dir_deleted_after_notebook_closed(main_widget, qtbot):
+    """Test that notebook file in temporary directory is deleted after the
+    notebook is closed."""
+    # Create new notebook tab and wait for prompt
+    main_widget.create_new_client()
+    client = main_widget.tabwidget.currentWidget()
+    nbwidget = client.notebookwidget
+    qtbot.waitUntil(lambda: prompt_present(nbwidget, qtbot),
+                    timeout=NOTEBOOK_UP)
+
+    # Get file name
+    filename = client.get_filename()
+
+    # Close the current client
+    main_widget.tabwidget.close_client()
+
+    # Assert file is deleted
+    assert not osp.exists(filename)
+
+
+@flaky(max_runs=3)
+def test_close_nonexisting_notebook(main_widget, qtbot, tmpdir):
+    """Test that we can close a tab if the notebook file does not exist.
+    Regression test for spyder-ide/spyder-notebook#187."""
+    # Set up tab with non-existing notebook
+    test_notebook_original = osp.join(LOCATION, 'test.ipynb')
+    test_notebook = osp.join(str(tmpdir), 'test.ipynb')
+    shutil.copyfile(test_notebook_original, test_notebook)
+    main_widget.open_notebook(filenames=[test_notebook])
+
+    # Wait for prompt
+    client = main_widget.tabwidget.currentWidget()
+    nbwidget = client.notebookwidget
+    qtbot.waitUntil(lambda: prompt_present(nbwidget, qtbot),
+                    timeout=NOTEBOOK_UP)
+
+    os.remove(test_notebook)
+
+    # Close tab
+    main_widget.tabwidget.close_client()
+
+    # Assert tab is closed (without raising an exception)
+    for client_index in range(main_widget.tabwidget.count()):
+        assert client != main_widget.tabwidget.widget(client_index)