Repository: spyder-ide/spyder

Issue:
Selecting "tutorial" from help menu does not bring up Help pane automatically if not loaded, leading to user confusion
## Description

**What steps will reproduce the problem?**

1. Open help tab
2. click Spyder tutorial
3. Nothing happens

**What is the expected output? What do you see instead?**
When I click spyder tutorial, nothing happens. 

**Please provide any additional information below**



## Version and main components

* Spyder Version: 3.2.4 
* Python Version: 3.6.3
* Qt Versions: 5.6.2, PyQt5 5.6 on Darwin

## Dependencies
```
pyflakes >=0.6.0 :  1.6.0 (OK)
pycodestyle >=2.3:  2.3.1 (OK)
pygments >=2.0   :  2.2.0 (OK)
pandas >=0.13.1  :  0.20.3 (OK)
numpy >=1.7      :  1.13.3 (OK)
sphinx >=0.6.6   :  1.6.3 (OK)
rope >=0.9.4     :  0.10.5 (OK)
jedi >=0.9.0     :  0.10.2 (OK)
psutil >=0.3     :  5.4.0 (OK)
nbconvert >=4.0  :  5.3.1 (OK)
sympy >=0.7.3    :  1.1.1 (OK)
cython >=0.21    :  0.26.1 (OK)
qtconsole >=4.2.0:  4.3.1 (OK)
IPython >=4.0    :  6.1.0 (OK)
pylint >=0.25    :  1.7.4 (OK)

```



Discussion:
The Spyder tutorial opens in the Help pane.  It does seem that if the Help pane isn't already visible, that it doesn't open it.  Please click on `View -> Panes` and make sure that Help is selected.  If you click the Tutorial, the Help panel should show text that starts with 'Spyder is an Integrated Development Environment (IDE) for scientific computing using the Python programming language.'  

Resolving patch:
diff --git a/spyder/plugins/help.py b/spyder/plugins/help.py
--- a/spyder/plugins/help.py
+++ b/spyder/plugins/help.py
@@ -739,6 +739,10 @@ def show_plain_text(self, text):
 
     @Slot()
     def show_tutorial(self):
+        """Show the Spyder tutorial in the Help plugin, opening it if needed"""
+        if not self.dockwidget.isVisible():
+            self.dockwidget.show()
+            self.toggle_view_action.setChecked(True)
         tutorial_path = get_module_source_path('spyder.utils.help')
         tutorial = osp.join(tutorial_path, 'tutorial.rst')
         text = open(tutorial).read()


Test patch:
diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py
--- a/spyder/app/tests/test_mainwindow.py
+++ b/spyder/app/tests/test_mainwindow.py
@@ -35,6 +35,7 @@
 from spyder.config.base import get_home_dir
 from spyder.config.main import CONF
 from spyder.plugins import TabFilter
+from spyder.plugins.help import ObjectComboBox
 from spyder.plugins.runconfig import RunConfiguration
 from spyder.py3compat import PY2, to_text_string
 from spyder.utils.ipython.kernelspec import SpyderKernelSpec
@@ -48,9 +49,9 @@
     from urllib2 import urlopen, URLError
 
 
-#==============================================================================
+# =============================================================================
 # Constants
-#==============================================================================
+# =============================================================================
 # Location of this file
 LOCATION = osp.realpath(osp.join(os.getcwd(), osp.dirname(__file__)))
 
@@ -60,7 +61,7 @@
 
 # Need longer EVAL_TIMEOUT, because need to cythonize and C compile ".pyx" file
 # before import and eval it
-COMPILE_AND_EVAL_TIMEOUT=30000
+COMPILE_AND_EVAL_TIMEOUT = 30000
 
 # Time to wait for the IPython console to evaluate something (in
 # miliseconds)
@@ -69,9 +70,9 @@
 # Temporary directory
 TEMP_DIRECTORY = tempfile.gettempdir()
 
-#==============================================================================
+# =============================================================================
 # Utility functions
-#==============================================================================
+# =============================================================================
 def open_file_in_editor(main_window, fname, directory=None):
     """Open a file using the Editor and its open file dialog"""
     top_level_widgets = QApplication.topLevelWidgets()
@@ -119,9 +120,18 @@ def start_new_kernel(startup_timeout=60, kernel_name='python', spykernel=False,
     return km, kc
 
 
-#==============================================================================
+def find_desired_tab_in_window(tab_name, window):
+    all_tabbars = window.findChildren(QTabBar)
+    for current_tabbar in all_tabbars:
+        for tab_index in range(current_tabbar.count()):
+            if current_tabbar.tabText(tab_index) == str(tab_name):
+                return current_tabbar, tab_index
+    return None, None
+
+
+# =============================================================================
 # Fixtures
-#==============================================================================
+# =============================================================================
 @pytest.fixture
 def main_window(request):
     """Main Window fixture"""
@@ -157,9 +167,9 @@ def close_window():
     return window
 
 
-#==============================================================================
+# =============================================================================
 # Tests
-#==============================================================================
+# =============================================================================
 # IMPORTANT NOTE: Please leave this test to be the first one here to
 # avoid possible timeouts in Appveyor
 @pytest.mark.slow
@@ -1245,5 +1255,72 @@ def test_tabfilter_typeerror_full(main_window):
     assert mockEvent_instance.pos.call_count == 1
 
 
+@flaky(max_runs=3)
+@pytest.mark.slow
+def test_help_opens_when_show_tutorial_full(main_window, qtbot):
+    """Test fix for #6317 : 'Show tutorial' opens the help plugin if closed."""
+    HELP_STR = "Help"
+
+    # Wait until the window is fully up
+    shell = main_window.ipyconsole.get_current_shellwidget()
+    qtbot.waitUntil(lambda: shell._prompt_html is not None,
+                    timeout=SHELL_TIMEOUT)
+
+    help_pane_menuitem = None
+    for action in main_window.plugins_menu.actions():
+        if action.text() == HELP_STR:
+            help_pane_menuitem = action
+            break
+
+    # Test opening tutorial with Help plguin closed
+    try:
+        main_window.help.plugin_closed()
+    except Exception:
+        pass
+    qtbot.wait(500)
+    help_tabbar, help_index = find_desired_tab_in_window(HELP_STR, main_window)
+
+    assert help_tabbar is None and help_index is None
+    assert not isinstance(main_window.focusWidget(), ObjectComboBox)
+    assert not help_pane_menuitem.isChecked()
+
+    main_window.help.show_tutorial()
+    qtbot.wait(500)
+
+    help_tabbar, help_index = find_desired_tab_in_window(HELP_STR, main_window)
+    assert None not in (help_tabbar, help_index)
+    assert help_index == help_tabbar.currentIndex()
+    assert isinstance(main_window.focusWidget(), ObjectComboBox)
+    assert help_pane_menuitem.isChecked()
+
+    # Test opening tutorial with help plugin open, but not selected
+    help_tabbar.setCurrentIndex((help_tabbar.currentIndex() + 1)
+                                % help_tabbar.count())
+    qtbot.wait(500)
+    help_tabbar, help_index = find_desired_tab_in_window(HELP_STR, main_window)
+    assert None not in (help_tabbar, help_index)
+    assert help_index != help_tabbar.currentIndex()
+    assert not isinstance(main_window.focusWidget(), ObjectComboBox)
+    assert help_pane_menuitem.isChecked()
+
+    main_window.help.show_tutorial()
+    qtbot.wait(500)
+    help_tabbar, help_index = find_desired_tab_in_window(HELP_STR, main_window)
+    assert None not in (help_tabbar, help_index)
+    assert help_index == help_tabbar.currentIndex()
+    assert isinstance(main_window.focusWidget(), ObjectComboBox)
+    assert help_pane_menuitem.isChecked()
+
+    # Test opening tutorial with help plugin open and the active tab
+    qtbot.wait(500)
+    main_window.help.show_tutorial()
+    help_tabbar, help_index = find_desired_tab_in_window(HELP_STR, main_window)
+    qtbot.wait(500)
+    assert None not in (help_tabbar, help_index)
+    assert help_index == help_tabbar.currentIndex()
+    assert isinstance(main_window.focusWidget(), ObjectComboBox)
+    assert help_pane_menuitem.isChecked()
+
+
 if __name__ == "__main__":
     pytest.main()
diff --git a/spyder/plugins/tests/test_help.py b/spyder/plugins/tests/test_help.py
--- a/spyder/plugins/tests/test_help.py
+++ b/spyder/plugins/tests/test_help.py
@@ -1,25 +1,38 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Copyright © Spyder Project Contributors
-#
-# Licensed under the terms of the MIT License
-# (see spyder/__init__.py for details)
-# -----------------------------------------------------------------------------
-"""Test scripts for `findinfiles` plugin."""
-
-# 3rd party imports
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Copyright © Spyder Project Contributors
+#
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+# -----------------------------------------------------------------------------
+
+"""
+Tests for the Spyder `help` plugn, `help.py`.
+"""
+
+# Standard library imports
+try:
+    from unittest.mock import Mock, MagicMock
+except ImportError:
+    from mock import Mock, MagicMock  # Python 2
+
+# Third party imports
 from qtpy.QtWebEngineWidgets import WEBENGINE
-import pytest
-
-# Local imports
-from spyder.plugins.help import Help
-from spyder.utils.introspection.utils import default_info_response
-
-
-@pytest.fixture
-def help_plugin(qtbot):
-    """Help plugin fixture"""
-    help_plugin = Help()
+import pytest
+from flaky import flaky
+
+# Local imports
+from spyder.plugins.help import Help
+from spyder.utils.introspection.utils import default_info_response
+
+
+# =============================================================================
+# Fixtures
+# =============================================================================
+@pytest.fixture
+def help_plugin(qtbot):
+    """Help plugin fixture"""
+    help_plugin = Help()
     webview = help_plugin.rich_text.webview._webview
 
     if WEBENGINE:
@@ -28,9 +41,12 @@ def help_plugin(qtbot):
         help_plugin._webpage = webview.page().mainFrame()
 
     qtbot.addWidget(help_plugin)
-    return help_plugin
+    return help_plugin
 
 
+# =============================================================================
+# Utility functions
+# =============================================================================
 def check_text(widget, text):
     """Check if some text is present in a widget."""
     if WEBENGINE:
@@ -44,20 +60,25 @@ def callback(data):
             return False
     else:
         return text in widget.toHtml()
-
-
-def test_no_docs_message(help_plugin, qtbot):
+
+
+# =============================================================================
+# Tests
+# =============================================================================
+@flaky(max_runs=3)
+def test_no_docs_message(help_plugin, qtbot):
     """
     Test that no docs message is shown when instrospection plugins
     can't get any info.
     """
-    help_plugin.render_sphinx_doc(default_info_response())
+    help_plugin.render_sphinx_doc(default_info_response())
     qtbot.waitUntil(lambda: check_text(help_plugin._webpage,
                                        "No documentation available"),
-                                       timeout=2000)
-
-
-def test_no_further_docs_message(help_plugin, qtbot):
+                    timeout=4000)
+
+
+@flaky(max_runs=3)
+def test_no_further_docs_message(help_plugin, qtbot):
     """
     Test that no further docs message is shown when instrospection
     plugins can get partial info.
@@ -66,11 +87,44 @@ def test_no_further_docs_message(help_plugin, qtbot):
     info['name'] = 'foo'
     info['argspec'] = '(x, y)'
 
-    help_plugin.render_sphinx_doc(info)
+    help_plugin.render_sphinx_doc(info)
     qtbot.waitUntil(lambda: check_text(help_plugin._webpage,
                                        "No further documentation available"),
-                                       timeout=2000)
-
-
-if __name__ == "__main__":
+                    timeout=3000)
+
+
+def test_help_opens_when_show_tutorial_unit(help_plugin, qtbot,):
+    """Test fix for #6317 : 'Show tutorial' opens the help plugin if closed."""
+    MockDockwidget = MagicMock()
+    MockDockwidget.return_value.isVisible.return_value = False
+    mockDockwidget_instance = MockDockwidget()
+
+    MockAction = Mock()
+    mock_toggle_view_action = MockAction()
+    mock_show_rich_text = Mock()
+
+    help_plugin.dockwidget = mockDockwidget_instance
+    help_plugin.toggle_view_action = mock_toggle_view_action
+    help_plugin.show_rich_text = mock_show_rich_text
+
+    help_plugin.show_tutorial()
+    qtbot.wait(100)
+
+    assert mockDockwidget_instance.show.call_count == 1
+    assert mock_toggle_view_action.setChecked.call_count == 1
+    mock_toggle_view_action.setChecked.assert_called_once_with(True)
+    assert mock_show_rich_text.call_count == 1
+
+    MockDockwidget.return_value.isVisible.return_value = True
+    mockDockwidget_instance = MockDockwidget()
+    help_plugin.dockwidget = mockDockwidget_instance
+
+    help_plugin.show_tutorial()
+    qtbot.wait(100)
+    assert mockDockwidget_instance.show.call_count == 1
+    assert mock_toggle_view_action.setChecked.call_count == 1
+    assert mock_show_rich_text.call_count == 2
+
+
+if __name__ == "__main__":
     pytest.main()