Merge branch 'completer_tests' of https://github.com/rcorre/qutebrowser into rcorre-completer_tests
This commit is contained in:
commit
b429b919b5
@ -22,7 +22,7 @@
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.commands import cmdexc, cmdutils, runners
|
||||
from qutebrowser.commands import cmdutils, runners
|
||||
from qutebrowser.utils import usertypes, log, objreg, utils
|
||||
from qutebrowser.completion.models import instances, sortfilter
|
||||
|
||||
@ -485,16 +485,3 @@ class Completer(QObject):
|
||||
"""Select the next completion item."""
|
||||
self._open_completion_if_needed()
|
||||
self.next_prev_item.emit(False)
|
||||
|
||||
@cmdutils.register(instance='completion', hide=True,
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def completion_item_del(self):
|
||||
"""Delete the current completion item."""
|
||||
completion = objreg.get('completion', scope='window',
|
||||
window=self._win_id)
|
||||
if not completion.currentIndex().isValid():
|
||||
raise cmdexc.CommandError("No item selected!")
|
||||
try:
|
||||
self.model().srcmodel.delete_cur_item(completion)
|
||||
except NotImplementedError:
|
||||
raise cmdexc.CommandError("Cannot delete this item.")
|
||||
|
@ -29,7 +29,8 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel
|
||||
from qutebrowser.config import config, style
|
||||
from qutebrowser.completion import completiondelegate, completer
|
||||
from qutebrowser.completion.models import base
|
||||
from qutebrowser.utils import qtutils, objreg, utils
|
||||
from qutebrowser.utils import qtutils, objreg, utils, usertypes
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
|
||||
|
||||
class CompletionView(QTreeView):
|
||||
@ -277,3 +278,14 @@ class CompletionView(QTreeView):
|
||||
if scrollbar is not None:
|
||||
scrollbar.setValue(scrollbar.minimum())
|
||||
super().showEvent(e)
|
||||
|
||||
@cmdutils.register(instance='completion', hide=True,
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def completion_item_del(self):
|
||||
"""Delete the current completion item."""
|
||||
if not self.currentIndex().isValid():
|
||||
raise cmdexc.CommandError("No item selected!")
|
||||
try:
|
||||
self.model().srcmodel.delete_cur_item(self)
|
||||
except NotImplementedError:
|
||||
raise cmdexc.CommandError("Cannot delete this item.")
|
||||
|
@ -255,6 +255,14 @@ def app_stub(stubs):
|
||||
objreg.delete('app')
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def stubs():
|
||||
"""Provide access to stub objects useful for testing."""
|
||||
|
@ -27,7 +27,7 @@ from unittest import mock
|
||||
from PyQt5.QtCore import pyqtSignal, QPoint, QProcess, QObject
|
||||
from PyQt5.QtNetwork import (QNetworkRequest, QAbstractNetworkCache,
|
||||
QNetworkCacheMetaData)
|
||||
from PyQt5.QtWidgets import QCommonStyle, QWidget
|
||||
from PyQt5.QtWidgets import QCommonStyle, QWidget, QLineEdit
|
||||
|
||||
from qutebrowser.browser.webkit import webview, history
|
||||
from qutebrowser.config import configexc
|
||||
@ -293,12 +293,13 @@ class FakeCommand:
|
||||
"""A simple command stub which has a description."""
|
||||
|
||||
def __init__(self, name='', desc='', hide=False, debug=False,
|
||||
deprecated=False):
|
||||
deprecated=False, completion=None):
|
||||
self.desc = desc
|
||||
self.name = name
|
||||
self.hide = hide
|
||||
self.debug = debug
|
||||
self.deprecated = deprecated
|
||||
self.completion = completion
|
||||
|
||||
|
||||
class FakeTimer(QObject):
|
||||
@ -359,6 +360,24 @@ class FakeConfigType:
|
||||
self.complete = lambda: [(val, '') for val in valid_values]
|
||||
|
||||
|
||||
class FakeStatusbarCommand(QLineEdit):
|
||||
|
||||
"""Stub for the statusbar command prompt."""
|
||||
|
||||
got_cmd = pyqtSignal(str)
|
||||
clear_completion_selection = pyqtSignal()
|
||||
hide_completion = pyqtSignal()
|
||||
update_completion = pyqtSignal()
|
||||
show_cmd = pyqtSignal()
|
||||
hide_cmd = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
def prefix(self):
|
||||
return self.text()[0]
|
||||
|
||||
|
||||
class ConfigStub(QObject):
|
||||
|
||||
"""Stub for the config module.
|
||||
|
223
tests/unit/completion/test_completer.py
Normal file
223
tests/unit/completion/test_completer.py
Normal file
@ -0,0 +1,223 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2016 Ryan Roden-Corrent (rcorre) <ryan@rcorre.net>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Tests for the Completer Object."""
|
||||
|
||||
import unittest.mock
|
||||
|
||||
import pytest
|
||||
from PyQt5.QtGui import QStandardItemModel
|
||||
|
||||
from qutebrowser.completion import completer
|
||||
from qutebrowser.utils import usertypes
|
||||
|
||||
|
||||
class FakeCompletionModel(QStandardItemModel):
|
||||
|
||||
"""Stub for a completion model."""
|
||||
|
||||
DUMB_SORT = None
|
||||
|
||||
def __init__(self, kind, parent=None):
|
||||
super().__init__(parent)
|
||||
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
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def completer_obj(qtbot, cmd, config_stub):
|
||||
"""Create the completer used for testing."""
|
||||
config_stub.data = {'completion': {'auto-open': False}}
|
||||
return completer.Completer(cmd, 0)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def instances(monkeypatch):
|
||||
"""Mock the instances module so get returns a fake completion model."""
|
||||
# populate a model for each completion type, with a nested structure for
|
||||
# option and value completion
|
||||
instances = {kind: FakeCompletionModel(kind)
|
||||
for kind in usertypes.Completion}
|
||||
instances[usertypes.Completion.option] = {
|
||||
'general': FakeCompletionModel(usertypes.Completion.option),
|
||||
}
|
||||
instances[usertypes.Completion.value] = {
|
||||
'general': {
|
||||
'ignore-case': FakeCompletionModel(usertypes.Completion.value),
|
||||
}
|
||||
}
|
||||
monkeypatch.setattr('qutebrowser.completion.completer.instances',
|
||||
instances)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def cmdutils_patch(monkeypatch, stubs):
|
||||
"""Patch the cmdutils module to provide fake commands."""
|
||||
cmds = {
|
||||
'set': [usertypes.Completion.section, usertypes.Completion.option,
|
||||
usertypes.Completion.value],
|
||||
'help': [usertypes.Completion.helptopic],
|
||||
'quickmark-load': [usertypes.Completion.quickmark_by_name],
|
||||
'bookmark-load': [usertypes.Completion.bookmark_by_url],
|
||||
'open': [usertypes.Completion.url],
|
||||
'buffer': [usertypes.Completion.tab],
|
||||
'session-load': [usertypes.Completion.sessions],
|
||||
'bind': [usertypes.Completion.empty, usertypes.Completion.command],
|
||||
'tab-detach': None,
|
||||
}
|
||||
cmd_utils = stubs.FakeCmdUtils({
|
||||
name: stubs.FakeCommand(completion=compl)
|
||||
for name, compl in cmds.items()
|
||||
})
|
||||
monkeypatch.setattr('qutebrowser.completion.completer.cmdutils',
|
||||
cmd_utils)
|
||||
|
||||
|
||||
def _set_cmd_prompt(cmd, txt):
|
||||
"""Set the command prompt's text and cursor position.
|
||||
|
||||
Args:
|
||||
cmd: The command prompt object.
|
||||
txt: The prompt text, using | as a placeholder for the cursor position.
|
||||
"""
|
||||
cmd.setText(txt.replace('|', ''))
|
||||
cmd.setCursorPosition(txt.index('|'))
|
||||
|
||||
|
||||
def _validate_cmd_prompt(cmd, txt):
|
||||
"""Interpret fake command prompt text using | as the cursor placeholder.
|
||||
|
||||
Args:
|
||||
cmd: The command prompt object.
|
||||
txt: The prompt text, using | as a placeholder for the cursor position.
|
||||
"""
|
||||
assert cmd.cursorPosition() == txt.index('|')
|
||||
assert cmd.text() == txt.replace('|', '')
|
||||
|
||||
|
||||
@pytest.mark.parametrize('txt, expected', [
|
||||
(':nope|', usertypes.Completion.command),
|
||||
(':nope |', None),
|
||||
(':set |', usertypes.Completion.section),
|
||||
(':set gen|', usertypes.Completion.section),
|
||||
(':set general |', usertypes.Completion.option),
|
||||
(':set what |', None),
|
||||
(':set general ignore-case |', usertypes.Completion.value),
|
||||
(':set general huh |', None),
|
||||
(':help |', usertypes.Completion.helptopic),
|
||||
(':quickmark-load |', usertypes.Completion.quickmark_by_name),
|
||||
(':bookmark-load |', usertypes.Completion.bookmark_by_url),
|
||||
(':open |', usertypes.Completion.url),
|
||||
(':buffer |', usertypes.Completion.tab),
|
||||
(':session-load |', usertypes.Completion.sessions),
|
||||
(':bind |', usertypes.Completion.empty),
|
||||
(':bind <c-x> |', usertypes.Completion.command),
|
||||
(':bind <c-x> foo|', usertypes.Completion.command),
|
||||
(':bind <c-x>| foo', usertypes.Completion.empty),
|
||||
(':set| general ', usertypes.Completion.command),
|
||||
(':|set general ', usertypes.Completion.command),
|
||||
(':set gene|ral ignore-case', usertypes.Completion.section),
|
||||
(':|', usertypes.Completion.command),
|
||||
(': |', usertypes.Completion.command),
|
||||
(':bookmark-load |', usertypes.Completion.bookmark_by_url),
|
||||
('/|', None),
|
||||
(':open -t|', None),
|
||||
(':open --tab|', None),
|
||||
(':open -t |', usertypes.Completion.url),
|
||||
(':open --tab |', usertypes.Completion.url),
|
||||
(':open | -t', usertypes.Completion.url),
|
||||
(':--foo --bar |', None),
|
||||
(':tab-detach |', None),
|
||||
(':bind --mode=caret <c-x> |', usertypes.Completion.command),
|
||||
pytest.mark.xfail(reason='issue #74')((':bind --mode caret <c-x> |',
|
||||
usertypes.Completion.command)),
|
||||
(':set -t -p |', usertypes.Completion.section),
|
||||
(':open -- |', None),
|
||||
])
|
||||
def test_update_completion(txt, expected, cmd, 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()
|
||||
if expected is None:
|
||||
assert not completion_widget_stub.set_model.called
|
||||
else:
|
||||
assert completion_widget_stub.set_model.call_count == 1
|
||||
arg = completion_widget_stub.set_model.call_args[0][0]
|
||||
# the outer model is just for sorting; srcmodel is the completion model
|
||||
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|'),
|
||||
(':foo |', 'bar', True, 1, ':foo bar |'),
|
||||
(':foo | bar', 'baz', False, 1, ':foo baz| bar'),
|
||||
(':foo |', 'bar baz', True, 1, ":foo 'bar baz' |"),
|
||||
(':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.
|
||||
|
||||
The | represents the current cursor position in the cmd prompt.
|
||||
If quick-complete is True and there is only 1 completion (count == 1),
|
||||
then we expect a space to be appended after the current word.
|
||||
"""
|
||||
config_stub.data['completion']['quick-complete'] = quick_complete
|
||||
model = unittest.mock.Mock()
|
||||
model.data = unittest.mock.Mock(return_value=newtxt)
|
||||
model.count = unittest.mock.Mock(return_value=count)
|
||||
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)
|
||||
model.data.assert_called_with(indexes[0])
|
||||
_validate_cmd_prompt(cmd, after)
|
Loading…
Reference in New Issue
Block a user