Merge branch 'completer_tests' of https://github.com/rcorre/qutebrowser into rcorre-completer_tests

This commit is contained in:
Florian Bruhin 2016-07-06 13:35:13 +02:00
commit b429b919b5
5 changed files with 266 additions and 17 deletions

View File

@ -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.")

View File

@ -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.")

View File

@ -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."""

View File

@ -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.

View 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)