diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py index ebdb1d10b..71202d131 100644 --- a/qutebrowser/completion/completer.py +++ b/qutebrowser/completion/completer.py @@ -19,7 +19,7 @@ """Completer attached to a CompletionView.""" -from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer +from PyQt5.QtCore import pyqtSlot, QObject, QTimer, QItemSelection from qutebrowser.config import config from qutebrowser.commands import cmdutils, runners @@ -42,15 +42,8 @@ class Completer(QObject): _last_text: The old command text so we avoid double completion updates. _signals_connected: Whether the signals are connected to update the completion when the command widget requests that. - - Signals: - next_prev_item: Emitted to select the next/previous item in the - completion. - arg0: True for the previous item, False for the next. """ - next_prev_item = pyqtSignal(bool) - def __init__(self, cmd, win_id, parent=None): super().__init__(parent) self._win_id = win_id @@ -61,25 +54,25 @@ class Completer(QObject): self._timer = QTimer() self._timer.setSingleShot(True) self._timer.setInterval(0) - self._timer.timeout.connect(self.update_completion) + self._timer.timeout.connect(self._update_completion) self._cursor_part = None self._last_cursor_pos = None self._last_text = None - objreg.get('config').changed.connect(self.on_auto_open_changed) - self.handle_signal_connections() + objreg.get('config').changed.connect(self._on_auto_open_changed) + self._handle_signal_connections() self._cmd.clear_completion_selection.connect( - self.handle_signal_connections) + self._handle_signal_connections) def __repr__(self): return utils.get_repr(self) @config.change_filter('completion', 'auto-open') - def on_auto_open_changed(self): - self.handle_signal_connections() + def _on_auto_open_changed(self): + self._handle_signal_connections() @pyqtSlot() - def handle_signal_connections(self): + def _handle_signal_connections(self): self._connect_signals(config.get('completion', 'auto-open')) def _connect_signals(self, connect=True): @@ -95,7 +88,7 @@ class Completer(QObject): """ connections = [ (self._cmd.update_completion, self.schedule_completion_update), - (self._cmd.textChanged, self.on_text_edited), + (self._cmd.textChanged, self._on_text_edited), ] if connect and not self._signals_connected: @@ -121,12 +114,11 @@ class Completer(QObject): if not config.get('completion', 'auto-open'): connected = self._connect_signals(True) if connected: - self.update_completion() + self._update_completion() def _model(self): """Convenience method to get the current completion model.""" - completion = objreg.get('completion', scope='window', - window=self._win_id) + completion = self.parent() return completion.model() def _get_completion_model(self, completion, parts, cursor_part): @@ -249,7 +241,8 @@ class Completer(QObject): else: return s - def selection_changed(self, selected, _deselected): + @pyqtSlot(QItemSelection) + def on_selection_changed(self, selected): """Change the completed part if a new item was selected. Called from the views selectionChanged method. @@ -258,6 +251,7 @@ class Completer(QObject): selected: New selection. _deselected: Previous selection. """ + self._open_completion_if_needed() indexes = selected.indexes() if not indexes: return @@ -265,7 +259,7 @@ class Completer(QObject): data = model.data(indexes[0]) if data is None: return - parts = self.split() + parts = self._split() try: needs_quoting = cmdutils.cmd_dict[parts[0]].maxsplit is None except KeyError: @@ -275,11 +269,11 @@ class Completer(QObject): if model.count() == 1 and config.get('completion', 'quick-complete'): # If we only have one item, we want to apply it immediately # and go on to the next part. - self.change_completed_part(data, immediate=True) + self._change_completed_part(data, immediate=True) else: log.completion.debug("Will ignore next completion update.") self._ignore_change = True - self.change_completed_part(data) + self._change_completed_part(data) @pyqtSlot() def schedule_completion_update(self): @@ -299,10 +293,10 @@ class Completer(QObject): self._last_text = self._cmd.text() @pyqtSlot() - def update_completion(self): + def _update_completion(self): """Check if completions are available and activate them.""" - self.update_cursor_part() - parts = self.split() + self._update_cursor_part() + parts = self._split() log.completion.debug( "Updating completion - prefix {}, parts {}, cursor_part {}".format( @@ -314,8 +308,7 @@ class Completer(QObject): self._ignore_change = False return - completion = objreg.get('completion', scope='window', - window=self._win_id) + completion = self.parent() if self._cmd.prefix() != ':': # This is a search or gibberish, so we don't need to complete @@ -354,7 +347,7 @@ class Completer(QObject): if completion.enabled: completion.show() - def split(self, keep=False): + def _split(self, keep=False): """Get the text split up in parts. Args: @@ -381,13 +374,13 @@ class Completer(QObject): return parts @pyqtSlot() - def update_cursor_part(self): + def _update_cursor_part(self): """Get the part index of the commandline where the cursor is over.""" cursor_pos = self._cmd.cursorPosition() snippet = slice(cursor_pos - 1, cursor_pos + 1) spaces = self._cmd.text()[snippet] == ' ' cursor_pos -= len(self._cmd.prefix()) - parts = self.split(keep=True) + parts = self._split(keep=True) log.completion.vdebug( "text: {}, parts: {}, cursor_pos after removing prefix '{}': " "{}".format(self._cmd.text(), parts, self._cmd.prefix(), @@ -429,7 +422,7 @@ class Completer(QObject): self._cursor_part, spaces)) return - def change_completed_part(self, newtext, immediate=False): + def _change_completed_part(self, newtext, immediate=False): """Change the part we're currently completing in the commandline. Args: @@ -438,7 +431,7 @@ class Completer(QObject): including a trailing space and we shouldn't continue completing the current item. """ - parts = self.split() + parts = self._split() log.completion.debug("changing part {} to '{}'".format( self._cursor_part, newtext)) try: @@ -465,23 +458,9 @@ class Completer(QObject): self._cmd.show_cmd.emit() @pyqtSlot() - def on_text_edited(self): + def _on_text_edited(self): """Reset _empty_item_idx if text was edited.""" self._empty_item_idx = None - # We also want to update the cursor part and emit update_completion + # We also want to update the cursor part and emit _update_completion # here, but that's already done for us by cursorPositionChanged # anyways, so we don't need to do it twice. - - @cmdutils.register(instance='completer', hide=True, - modes=[usertypes.KeyMode.command], scope='window') - def completion_item_prev(self): - """Select the previous completion item.""" - self._open_completion_if_needed() - self.next_prev_item.emit(True) - - @cmdutils.register(instance='completer', hide=True, - modes=[usertypes.KeyMode.command], scope='window') - def completion_item_next(self): - """Select the next completion item.""" - self._open_completion_if_needed() - self.next_prev_item.emit(False) diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index 80e87dbe4..9fc1ef5d7 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -24,10 +24,11 @@ subclasses to provide completions. """ from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy -from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel +from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, + QItemSelection) from qutebrowser.config import config, style -from qutebrowser.completion import completiondelegate, completer +from qutebrowser.completion import completiondelegate from qutebrowser.completion.models import base from qutebrowser.utils import qtutils, objreg, utils, usertypes from qutebrowser.commands import cmdexc, cmdutils @@ -50,6 +51,7 @@ class CompletionView(QTreeView): Signals: resize_completion: Emitted when the completion should be resized. + selection_changed: Emitted when the completion item selection changes. """ # Drawing the item foreground will be done by CompletionItemDelegate, so we @@ -102,16 +104,11 @@ class CompletionView(QTreeView): """ resize_completion = pyqtSignal() + selection_changed = pyqtSignal(QItemSelection) def __init__(self, win_id, parent=None): super().__init__(parent) self._win_id = win_id - objreg.register('completion', self, scope='window', window=win_id) - cmd = objreg.get('status-command', scope='window', window=win_id) - completer_obj = completer.Completer(cmd, win_id, self) - completer_obj.next_prev_item.connect(self.on_next_prev_item) - objreg.register('completer', completer_obj, scope='window', - window=win_id) self.enabled = config.get('completion', 'show') objreg.get('config').changed.connect(self.set_enabled) # FIXME handle new aliases. @@ -184,21 +181,17 @@ class CompletionView(QTreeView): # Item is a real item, not a category header -> success return idx - @pyqtSlot(bool) - def on_next_prev_item(self, prev): + def _next_prev_item(self, prev): """Handle a tab press for the CompletionView. Select the previous/next item and write the new text to the statusbar. - Called from the Completer's next_prev_item signal. + Helper for completion_item_next and completion_item_prev. Args: prev: True for prev item, False for next one. """ - if not self.isVisible(): - # No completion running at the moment, ignore keypress - return idx = self._next_idx(prev) qtutils.ensure_valid(idx) self.selectionModel().setCurrentIndex( @@ -262,9 +255,7 @@ class CompletionView(QTreeView): def selectionChanged(self, selected, deselected): """Extend selectionChanged to call completers selection_changed.""" super().selectionChanged(selected, deselected) - completer_obj = objreg.get('completer', scope='window', - window=self._win_id) - completer_obj.selection_changed(selected, deselected) + self.selection_changed.emit(selected) def resizeEvent(self, e): """Extend resizeEvent to adjust column size.""" @@ -279,6 +270,18 @@ class CompletionView(QTreeView): scrollbar.setValue(scrollbar.minimum()) super().showEvent(e) + @cmdutils.register(instance='completion', hide=True, + modes=[usertypes.KeyMode.command], scope='window') + def completion_item_prev(self): + """Select the previous completion item.""" + self._next_prev_item(True) + + @cmdutils.register(instance='completion', hide=True, + modes=[usertypes.KeyMode.command], scope='window') + def completion_item_next(self): + """Select the next completion item.""" + self._next_prev_item(False) + @cmdutils.register(instance='completion', hide=True, modes=[usertypes.KeyMode.command], scope='window') def completion_item_del(self): diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 79a5546d3..826472518 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -32,7 +32,7 @@ from qutebrowser.config import config from qutebrowser.utils import message, log, usertypes, qtutils, objreg, utils from qutebrowser.mainwindow import tabbedbrowser from qutebrowser.mainwindow.statusbar import bar -from qutebrowser.completion import completionwidget +from qutebrowser.completion import completionwidget, completer from qutebrowser.keyinput import modeman from qutebrowser.browser import commands, downloadview, hints from qutebrowser.browser.webkit import downloads @@ -131,23 +131,13 @@ class MainWindow(QWidget): self._vbox.setContentsMargins(0, 0, 0, 0) self._vbox.setSpacing(0) - log.init.debug("Initializing downloads...") - download_manager = downloads.DownloadManager(self.win_id, self) - objreg.register('download-manager', download_manager, scope='window', - window=self.win_id) - + self._init_downloadmanager() self._downloadview = downloadview.DownloadView(self.win_id) self.tabbed_browser = tabbedbrowser.TabbedBrowser(self.win_id) objreg.register('tabbed-browser', self.tabbed_browser, scope='window', window=self.win_id) - dispatcher = commands.CommandDispatcher(self.win_id, - self.tabbed_browser) - objreg.register('command-dispatcher', dispatcher, scope='window', - window=self.win_id) - self.tabbed_browser.destroyed.connect( - functools.partial(objreg.delete, 'command-dispatcher', - scope='window', window=self.win_id)) + self._init_command_dispatcher() # We need to set an explicit parent for StatusBar because it does some # show/hide magic immediately which would mean it'd show up as a @@ -157,7 +147,7 @@ class MainWindow(QWidget): self._add_widgets() self._downloadview.show() - self._completion = completionwidget.CompletionView(self.win_id, self) + self._init_completion() self._commandrunner = runners.CommandRunner(self.win_id, partial_match=True) @@ -190,6 +180,30 @@ class MainWindow(QWidget): objreg.get("app").new_window.emit(self) + def _init_downloadmanager(self): + log.init.debug("Initializing downloads...") + download_manager = downloads.DownloadManager(self.win_id, self) + objreg.register('download-manager', download_manager, scope='window', + window=self.win_id) + + def _init_completion(self): + self._completion = completionwidget.CompletionView(self.win_id, self) + cmd = objreg.get('status-command', scope='window', window=self.win_id) + completer_obj = completer.Completer(cmd, self.win_id, self._completion) + self._completion.selection_changed.connect( + completer_obj.on_selection_changed) + objreg.register('completion', self._completion, scope='window', + window=self.win_id) + + def _init_command_dispatcher(self): + dispatcher = commands.CommandDispatcher(self.win_id, + self.tabbed_browser) + objreg.register('command-dispatcher', dispatcher, scope='window', + window=self.win_id) + self.tabbed_browser.destroyed.connect( + functools.partial(objreg.delete, 'command-dispatcher', + scope='window', window=self.win_id)) + def __repr__(self): return utils.get_repr(self) diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index 0fd890fa0..749f0d4f0 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -268,11 +268,13 @@ def app_stub(stubs): @pytest.yield_fixture -def completion_widget_stub(win_registry): - stub = unittest.mock.Mock() - objreg.register('completion', stub, scope='window', window=0) - yield stub - objreg.delete('completion', scope='window', window=0) +def status_command_stub(stubs, qtbot, win_registry): + """Fixture which provides a fake status-command object.""" + cmd = stubs.StatusBarCommandStub() + objreg.register('status-command', cmd, scope='window', window=0) + qtbot.addWidget(cmd) + yield cmd + objreg.delete('status-command', scope='window', window=0) @pytest.fixture(scope='session') diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index 419d6bd78..5027f301b 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -381,6 +381,28 @@ class FakeTimer(QObject): return self._started +class InstaTimer(QObject): + + """Stub for a QTimer that fires instantly on start(). + + Useful to test a time-based event without inserting an artificial delay. + """ + + timeout = pyqtSignal() + + def __init__(self, parent=None): + super().__init__(parent) + + def start(self): + self.timeout.emit() + + def setSingleShot(self, yes): + pass + + def setInterval(self, interval): + pass + + class FakeConfigType: """A stub to provide valid_values for typ attribute of a SettingValue.""" @@ -391,7 +413,7 @@ class FakeConfigType: self.complete = lambda: [(val, '') for val in valid_values] -class FakeStatusbarCommand(QLineEdit): +class StatusBarCommandStub(QLineEdit): """Stub for the statusbar command prompt.""" diff --git a/tests/unit/completion/test_completer.py b/tests/unit/completion/test_completer.py index bfb1f0718..29196ea3b 100644 --- a/tests/unit/completion/test_completer.py +++ b/tests/unit/completion/test_completer.py @@ -22,6 +22,7 @@ import unittest.mock import pytest +from PyQt5.QtCore import QObject from PyQt5.QtGui import QStandardItemModel from qutebrowser.completion import completer @@ -39,19 +40,33 @@ class FakeCompletionModel(QStandardItemModel): self.kind = kind -@pytest.fixture -def cmd(stubs, qtbot): - """Create the statusbar command prompt the completer uses.""" - cmd = stubs.FakeStatusbarCommand() - qtbot.addWidget(cmd) - return cmd +class CompletionWidgetStub(QObject): + + """Stub for the CompletionView.""" + + def __init__(self, parent=None): + super().__init__(parent) + self.hide = unittest.mock.Mock() + self.show = unittest.mock.Mock() + self.set_pattern = unittest.mock.Mock() + self.model = unittest.mock.Mock() + self.set_model = unittest.mock.Mock() + self.enabled = unittest.mock.Mock() @pytest.fixture -def completer_obj(qtbot, cmd, config_stub): +def completion_widget_stub(): + return CompletionWidgetStub() + + +@pytest.fixture +def completer_obj(qtbot, status_command_stub, config_stub, monkeypatch, stubs, + completion_widget_stub): """Create the completer used for testing.""" + monkeypatch.setattr('qutebrowser.completion.completer.QTimer', + stubs.InstaTimer) config_stub.data = {'completion': {'auto-open': False}} - return completer.Completer(cmd, 0) + return completer.Completer(status_command_stub, 0, completion_widget_stub) @pytest.fixture(autouse=True) @@ -157,12 +172,12 @@ def _validate_cmd_prompt(cmd, txt): (':set -t -p |', usertypes.Completion.section), (':open -- |', None), ]) -def test_update_completion(txt, expected, cmd, completer_obj, +def test_update_completion(txt, expected, status_command_stub, completer_obj, completion_widget_stub): """Test setting the completion widget's model based on command text.""" # this test uses | as a placeholder for the current cursor position - _set_cmd_prompt(cmd, txt) - completer_obj.update_completion() + _set_cmd_prompt(status_command_stub, txt) + completer_obj.schedule_completion_update() if expected is None: assert not completion_widget_stub.set_model.called else: @@ -172,24 +187,6 @@ def test_update_completion(txt, expected, cmd, completer_obj, assert arg.srcmodel.kind == expected -def test_completion_item_prev(completer_obj, cmd, completion_widget_stub, - config_stub, qtbot): - """Test that completion_item_prev emits next_prev_item.""" - cmd.setText(':') - with qtbot.waitSignal(completer_obj.next_prev_item) as blocker: - completer_obj.completion_item_prev() - assert blocker.args == [True] - - -def test_completion_item_next(completer_obj, cmd, completion_widget_stub, - config_stub, qtbot): - """Test that completion_item_next emits next_prev_item.""" - cmd.setText(':') - with qtbot.waitSignal(completer_obj.next_prev_item) as blocker: - completer_obj.completion_item_next() - assert blocker.args == [False] - - @pytest.mark.parametrize('before, newtxt, quick_complete, count, after', [ (':foo |', 'bar', False, 1, ':foo bar|'), (':foo |', 'bar', True, 2, ':foo bar|'), @@ -199,10 +196,10 @@ def test_completion_item_next(completer_obj, cmd, completion_widget_stub, (':foo |', '', True, 1, ":foo '' |"), (':foo |', None, True, 1, ":foo |"), ]) -def test_selection_changed(before, newtxt, count, quick_complete, after, - completer_obj, cmd, completion_widget_stub, - config_stub): - """Test that change_completed_part modifies the cmd text properly. +def test_on_selection_changed(before, newtxt, count, quick_complete, after, + completer_obj, status_command_stub, + completion_widget_stub, config_stub): + """Test that on_selection_changed modifies the cmd text properly. The | represents the current cursor position in the cmd prompt. If quick-complete is True and there is only 1 completion (count == 1), @@ -215,9 +212,10 @@ def test_selection_changed(before, newtxt, count, quick_complete, after, indexes = [unittest.mock.Mock()] selection = unittest.mock.Mock() selection.indexes = unittest.mock.Mock(return_value=indexes) - completion_widget_stub.model = unittest.mock.Mock(return_value=model) - _set_cmd_prompt(cmd, before) - completer_obj.update_cursor_part() - completer_obj.selection_changed(selection, None) + completion_widget_stub.model.return_value = model + _set_cmd_prompt(status_command_stub, before) + # schedule_completion_update is needed to pick up the cursor position + completer_obj.schedule_completion_update() + completer_obj.on_selection_changed(selection) model.data.assert_called_with(indexes[0]) - _validate_cmd_prompt(cmd, after) + _validate_cmd_prompt(status_command_stub, after) diff --git a/tests/unit/completion/test_completionwidget.py b/tests/unit/completion/test_completionwidget.py new file mode 100644 index 000000000..9bc2a2e55 --- /dev/null +++ b/tests/unit/completion/test_completionwidget.py @@ -0,0 +1,147 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2016 Ryan Roden-Corrent (rcorre) +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + +"""Tests for the CompletionView Object.""" + +import unittest.mock + +import pytest +from PyQt5.QtGui import QStandardItem, QColor + +from qutebrowser.completion import completionwidget +from qutebrowser.completion.models import base, sortfilter + + +@pytest.fixture +def completionview(qtbot, status_command_stub, config_stub, win_registry, + mocker): + """Create the CompletionView used for testing.""" + config_stub.data = { + 'completion': { + 'show': True, + 'auto-open': True, + 'scrollbar-width': 12, + 'scrollbar-padding': 2, + 'shrink': False, + }, + 'colors': { + 'completion.fg': QColor(), + 'completion.bg': QColor(), + 'completion.alternate-bg': QColor(), + 'completion.category.fg': QColor(), + 'completion.category.bg': QColor(), + 'completion.category.border.top': QColor(), + 'completion.category.border.bottom': QColor(), + 'completion.item.selected.fg': QColor(), + 'completion.item.selected.bg': QColor(), + 'completion.item.selected.border.top': QColor(), + 'completion.item.selected.border.bottom': QColor(), + 'completion.match.fg': QColor(), + 'completion.scrollbar.fg': QColor(), + 'completion.scrollbar.bg': QColor(), + }, + 'fonts': { + 'completion': 'Comic Sans Monospace' + } + } + # mock the Completer that the widget creates in its constructor + mocker.patch('qutebrowser.completion.completer.Completer', autospec=True) + view = completionwidget.CompletionView(win_id=0) + qtbot.addWidget(view) + return view + + +def test_set_model(completionview): + """Ensure set_model actually sets the model and expands all categories.""" + model = base.BaseCompletionModel() + filtermodel = sortfilter.CompletionFilterModel(model) + for i in range(3): + model.appendRow(QStandardItem(str(i))) + completionview.set_model(filtermodel) + assert completionview.model() is filtermodel + for i in range(model.rowCount()): + assert completionview.isExpanded(filtermodel.index(i, 0)) + + +def test_set_pattern(completionview): + model = sortfilter.CompletionFilterModel(base.BaseCompletionModel()) + model.set_pattern = unittest.mock.Mock() + completionview.set_model(model) + completionview.set_pattern('foo') + model.set_pattern.assert_called_with('foo') + + +def test_maybe_resize_completion(completionview, config_stub, qtbot): + """Ensure completion is resized only if shrink is True.""" + with qtbot.assertNotEmitted(completionview.resize_completion): + completionview.maybe_resize_completion() + config_stub.data = {'completion': {'shrink': True}} + with qtbot.waitSignal(completionview.resize_completion): + completionview.maybe_resize_completion() + + +@pytest.mark.parametrize('tree, count, expected', [ + ([['Aa']], 1, 'Aa'), + ([['Aa']], -1, 'Aa'), + ([['Aa'], ['Ba']], 1, 'Aa'), + ([['Aa'], ['Ba']], -1, 'Ba'), + ([['Aa'], ['Ba']], 2, 'Ba'), + ([['Aa'], ['Ba']], -2, 'Aa'), + ([['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 3, 'Ac'), + ([['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 4, 'Ba'), + ([['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 6, 'Ca'), + ([['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], 7, 'Aa'), + ([['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], -1, 'Ca'), + ([['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], -2, 'Bb'), + ([['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], -4, 'Ac'), + ([[], ['Ba', 'Bb']], 1, 'Ba'), + ([[], ['Ba', 'Bb']], -1, 'Bb'), + ([[], [], ['Ca', 'Cb']], 1, 'Ca'), + ([[], [], ['Ca', 'Cb']], -1, 'Cb'), + ([['Aa'], []], 1, 'Aa'), + ([['Aa'], []], -1, 'Aa'), + ([['Aa'], [], []], 1, 'Aa'), + ([['Aa'], [], []], -1, 'Aa'), +]) +def test_completion_item_next_prev(tree, count, expected, completionview): + """Test that on_next_prev_item moves the selection properly. + + Args: + tree: Each list represents a completion category, with each string + being an item under that category. + count: Number of times to go forward (or back if negative). + expected: item data that should be selected after going back/forward. + """ + model = base.BaseCompletionModel() + for catdata in tree: + cat = QStandardItem() + model.appendRow(cat) + for name in catdata: + cat.appendRow(QStandardItem(name)) + filtermodel = sortfilter.CompletionFilterModel(model, + parent=completionview) + completionview.set_model(filtermodel) + if count < 0: + for _ in range(-count): + completionview.completion_item_prev() + else: + for _ in range(count): + completionview.completion_item_next() + idx = completionview.selectionModel().currentIndex() + assert filtermodel.data(idx) == expected