Repository: spyder-ide/spyder

Issue:
Create a way to add tests for the whole app
This will need a fixture that initializes the app with a basic configuration, and maybe didn't display it (I'm not sure if it will be possible).

This is needed for add test to code like #4031


Discussion:
We need to display to be able to interact with it
@andfoy, this is needed to add a test for PR #3956.
Is this related to integration-like-tests?
What do you mean?
I refer if we want to check the interaction between two or more components of the program
Yep, we need to add tests for the whole window to check for the interaction of two plugins.

Resolving patch:
diff --git a/runtests.py b/runtests.py
--- a/runtests.py
+++ b/runtests.py
@@ -9,6 +9,7 @@
 """
 
 # Standard library imports
+import os
 import sys
 
 # Third party imports
@@ -16,6 +17,10 @@
 import pytest
 
 
+# To activate/deactivate certain things for pytest's only
+os.environ['SPYDER_PYTEST'] = 'True'
+
+
 def main():
     """
     Run pytest tests.
diff --git a/spyder/app/mainwindow.py b/spyder/app/mainwindow.py
--- a/spyder/app/mainwindow.py
+++ b/spyder/app/mainwindow.py
@@ -1948,14 +1948,17 @@ def get_focus_widget_properties(self):
         widget = QApplication.focusWidget()
         from spyder.widgets.shell import ShellBaseWidget
         from spyder.widgets.editor import TextEditBaseWidget
+        from spyder.widgets.ipythonconsole import ControlWidget
 
         # if focused widget isn't valid try the last focused
-        if not isinstance(widget, (ShellBaseWidget, TextEditBaseWidget)):
+        if not isinstance(widget, (ShellBaseWidget, TextEditBaseWidget,
+                                   ControlWidget)):
             widget = self.previous_focused_widget
 
         textedit_properties = None
-        if isinstance(widget, (ShellBaseWidget, TextEditBaseWidget)):
-            console = isinstance(widget, ShellBaseWidget)
+        if isinstance(widget, (ShellBaseWidget, TextEditBaseWidget,
+                               ControlWidget)):
+            console = isinstance(widget, (ShellBaseWidget, ControlWidget))
             not_readonly = not widget.isReadOnly()
             readwrite_editor = not_readonly and not console
             textedit_properties = (console, not_readonly, readwrite_editor)
@@ -2004,7 +2007,10 @@ def update_search_menu(self):
 
         widget, textedit_properties = self.get_focus_widget_properties()
         for action in self.editor.search_menu_actions:
-            action.setEnabled(self.editor.isAncestorOf(widget))
+            try:
+                action.setEnabled(self.editor.isAncestorOf(widget))
+            except RuntimeError:
+                pass
         if textedit_properties is None: # widget is not an editor/console
             return
         #!!! Below this line, widget is expected to be a QPlainTextEdit instance
@@ -2923,7 +2929,8 @@ def run_spyder(app, options, args):
     # the window
     app.focusChanged.connect(main.change_last_focused_widget)
 
-    app.exec_()
+    if not os.environ.get('SPYDER_PYTEST', None):
+        app.exec_()
     return main
 
 
diff --git a/spyder/widgets/variableexplorer/collectionseditor.py b/spyder/widgets/variableexplorer/collectionseditor.py
--- a/spyder/widgets/variableexplorer/collectionseditor.py
+++ b/spyder/widgets/variableexplorer/collectionseditor.py
@@ -1315,11 +1315,11 @@ def setup(self, data, title='', readonly=False, width=650, remote=False,
         buttons = QDialogButtonBox.Ok
         if not readonly:
             buttons = buttons | QDialogButtonBox.Cancel
-        bbox = QDialogButtonBox(buttons)
-        bbox.accepted.connect(self.accept)
+        self.bbox = QDialogButtonBox(buttons)
+        self.bbox.accepted.connect(self.accept)
         if not readonly:
-            bbox.rejected.connect(self.reject)
-        layout.addWidget(bbox)
+            self.bbox.rejected.connect(self.reject)
+        layout.addWidget(self.bbox)
 
         constant = 121
         row_height = 30


Test patch:
diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py
new file mode 100644
--- /dev/null
+++ b/spyder/app/tests/test_mainwindow.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+#
+
+"""
+Tests for the main window
+"""
+
+import pytest
+from qtpy.QtCore import Qt
+
+from spyder.app.cli_options import get_options
+from spyder.app.mainwindow import initialize, run_spyder
+
+
+@pytest.fixture
+def main_window():
+    app = initialize()
+    options, args = get_options()
+    widget = run_spyder(app, options, args)
+    return widget
+
+
+def test_issue_4066(main_window, qtbot):
+    """
+    Test for a segfault when these steps are followed:
+
+    1. Open an object present in the Variable Explorer (e.g. a list).
+    2. Delete that object in its corresponding console while its
+       editor is still opem.
+    3. Closing that editor by pressing its *Ok* button.
+    """
+    # Create the object
+    shell = main_window.ipyconsole.get_current_shellwidget()
+    qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=6000)
+    shell.execute('myobj = [1, 2, 3]')
+
+    # Open editor associated with that object and get a reference to it
+    nsb = main_window.variableexplorer.get_focus_widget()
+    qtbot.waitUntil(lambda: nsb.editor.model.rowCount() > 0, timeout=500)
+    nsb.editor.setFocus()
+    nsb.editor.edit_item()
+    obj_editor_id = list(nsb.editor.delegate._editors.keys())[0]
+    obj_editor = nsb.editor.delegate._editors[obj_editor_id]['editor']
+
+    # Move to the IPython console and delete that object
+    main_window.ipyconsole.get_focus_widget().setFocus()
+    shell.execute('del myobj')
+    qtbot.waitUntil(lambda: nsb.editor.model.rowCount() == 0, timeout=500)
+
+    # Close editor
+    ok_widget = obj_editor.bbox.button(obj_editor.bbox.Ok)
+    qtbot.mouseClick(ok_widget, Qt.LeftButton)
+
+
+def test_varexp_edit_inline(main_window, qtbot):
+    """
+    Test for errors when editing inline values in the Variable Explorer
+    and then moving to another plugin.
+
+    Note: Errors for this test don't appear related to it but instead they
+    are shown down the road. That's because they are generated by an
+    async C++ RuntimeError.
+    """
+    # Create object
+    shell = main_window.ipyconsole.get_current_shellwidget()
+    qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=6000)
+    shell.execute('a = 10')
+
+    # Edit object
+    main_window.variableexplorer.visibility_changed(True)
+    nsb = main_window.variableexplorer.get_focus_widget()
+    qtbot.waitUntil(lambda: nsb.editor.model.rowCount() > 0, timeout=500)
+    nsb.editor.setFocus()
+    nsb.editor.edit_item()
+
+    # Change focus to IPython console
+    main_window.ipyconsole.get_focus_widget().setFocus()
+
+
+if __name__ == "__main__":
+    pytest.main()
diff --git a/spyder/plugins/tests/test_ipythonconsole.py b/spyder/plugins/tests/test_ipythonconsole.py
--- a/spyder/plugins/tests/test_ipythonconsole.py
+++ b/spyder/plugins/tests/test_ipythonconsole.py
@@ -16,18 +16,16 @@
 # Qt Test Fixtures
 #--------------------------------
 @pytest.fixture
-def ipyconsole_bot(qtbot):
+def ipyconsole():
     widget = IPythonConsole(None, testing=True)
     widget.create_new_client()
-    qtbot.addWidget(widget)
-    return qtbot, widget
+    return widget
 
 
 # Tests
 #-------------------------------
 @pytest.mark.skipif(os.name == 'nt', reason="It's timing out on Windows")
-def test_sys_argv_clear(ipyconsole_bot):
-    qtbot, ipyconsole = ipyconsole_bot
+def test_sys_argv_clear(ipyconsole, qtbot):
     shell = ipyconsole.get_current_shellwidget()
     client = ipyconsole.get_current_client()