qutebrowser/tests/unit/completion/test_completionwidget.py
Ryan Roden-Corrent b61691684e Clear selection when setting completion pattern.
It doesn't make sense to have an active selection while you are
filtering by entering text. You should be in one of two states:

1. Tabbing through completions (valid selection)
2. Entering a filter pattern (invalid selection)

Fixes #2843, where a crash would occur after the following:

1. tab to an item other than the first
2. <backspace>
3. re-type last character
4. <ctrl-d>

This would try to delete an out of range index.
2017-07-22 18:06:16 -04:00

243 lines
9.9 KiB
Python

# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016-2017 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 CompletionView Object."""
from unittest import mock
import pytest
from PyQt5.QtGui import QColor
from qutebrowser.completion import completionwidget
from qutebrowser.completion.models import completionmodel, listcategory
from qutebrowser.commands import cmdexc
@pytest.fixture
def completionview(qtbot, status_command_stub, config_stub, win_registry,
mocker):
"""Create the CompletionView used for testing."""
config_stub.data = {
'completion': {
'show': 'always',
'scrollbar-width': 12,
'scrollbar-padding': 2,
'shrink': False,
'quick-complete': False,
'height': '50%',
},
'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',
'completion.category': 'Comic Sans Monospace bold',
}
}
# 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 = completionmodel.CompletionModel()
for i in range(3):
model.add_category(listcategory.ListCategory('', [('foo',)]))
completionview.set_model(model)
assert completionview.model() is model
for i in range(3):
assert completionview.isExpanded(model.index(i, 0))
def test_set_pattern(completionview):
model = completionmodel.CompletionModel()
model.set_pattern = mock.Mock(spec=[])
completionview.set_model(model)
completionview.set_pattern('foo')
model.set_pattern.assert_called_with('foo')
assert not completionview.selectionModel().currentIndex().isValid()
def test_set_pattern_no_model(completionview):
"""Ensure that setting a pattern with no model does not fail."""
completionview.set_pattern('foo')
def test_maybe_update_geometry(completionview, config_stub, qtbot):
"""Ensure completion is resized only if shrink is True."""
with qtbot.assertNotEmitted(completionview.update_geometry):
completionview._maybe_update_geometry()
config_stub.data['completion']['shrink'] = True
with qtbot.waitSignal(completionview.update_geometry):
completionview._maybe_update_geometry()
@pytest.mark.parametrize('which, tree, expected', [
('next', [['Aa']], ['Aa', None, None]),
('prev', [['Aa']], ['Aa', None, None]),
('next', [['Aa'], ['Ba']], ['Aa', 'Ba', 'Aa']),
('prev', [['Aa'], ['Ba']], ['Ba', 'Aa', 'Ba']),
('next', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']],
['Aa', 'Ab', 'Ac', 'Ba', 'Bb', 'Ca', 'Aa']),
('prev', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']],
['Ca', 'Bb', 'Ba', 'Ac', 'Ab', 'Aa', 'Ca']),
('next', [[], ['Ba', 'Bb']], ['Ba', 'Bb', 'Ba']),
('prev', [[], ['Ba', 'Bb']], ['Bb', 'Ba', 'Bb']),
('next', [[], [], ['Ca', 'Cb']], ['Ca', 'Cb', 'Ca']),
('prev', [[], [], ['Ca', 'Cb']], ['Cb', 'Ca', 'Cb']),
('next', [['Aa'], []], ['Aa', None]),
('prev', [['Aa'], []], ['Aa', None]),
('next', [['Aa'], [], []], ['Aa', None]),
('prev', [['Aa'], [], []], ['Aa', None]),
('next', [['Aa'], [], ['Ca', 'Cb']], ['Aa', 'Ca', 'Cb', 'Aa']),
('prev', [['Aa'], [], ['Ca', 'Cb']], ['Cb', 'Ca', 'Aa', 'Cb']),
('next', [[]], [None, None]),
('prev', [[]], [None, None]),
('next-category', [['Aa']], ['Aa', None, None]),
('prev-category', [['Aa']], ['Aa', None, None]),
('next-category', [['Aa'], ['Ba']], ['Aa', 'Ba', 'Aa']),
('prev-category', [['Aa'], ['Ba']], ['Ba', 'Aa', 'Ba']),
('next-category', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']],
['Aa', 'Ba', 'Ca', 'Aa']),
('prev-category', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']],
['Ca', 'Ba', 'Aa', 'Ca']),
('next-category', [[], ['Ba', 'Bb']], ['Ba', None, None]),
('prev-category', [[], ['Ba', 'Bb']], ['Ba', None, None]),
('next-category', [[], [], ['Ca', 'Cb']], ['Ca', None, None]),
('prev-category', [[], [], ['Ca', 'Cb']], ['Ca', None, None]),
('next-category', [['Aa'], [], []], ['Aa', None, None]),
('prev-category', [['Aa'], [], []], ['Aa', None, None]),
('next-category', [['Aa'], [], ['Ca', 'Cb']], ['Aa', 'Ca', 'Aa']),
('prev-category', [['Aa'], [], ['Ca', 'Cb']], ['Ca', 'Aa', 'Ca']),
('next-category', [[]], [None, None]),
('prev-category', [[]], [None, None]),
])
def test_completion_item_focus(which, tree, expected, completionview, qtbot):
"""Test that on_next_prev_item moves the selection properly.
Args:
which: the direction in which to move the selection.
tree: Each list represents a completion category, with each string
being an item under that category.
expected: expected argument from on_selection_changed for each
successive movement. None implies no signal should be
emitted.
"""
model = completionmodel.CompletionModel()
for catdata in tree:
cat = listcategory.ListCategory('', ((x,) for x in catdata))
model.add_category(cat)
completionview.set_model(model)
for entry in expected:
if entry is None:
with qtbot.assertNotEmitted(completionview.selection_changed):
completionview.completion_item_focus(which)
else:
with qtbot.waitSignal(completionview.selection_changed) as sig:
completionview.completion_item_focus(which)
assert sig.args == [entry]
@pytest.mark.parametrize('which', ['next', 'prev', 'next-category',
'prev-category'])
def test_completion_item_focus_no_model(which, completionview, qtbot):
"""Test that selectionChanged is not fired when the model is None.
Validates #1812: help completion repeatedly completes
"""
with qtbot.assertNotEmitted(completionview.selection_changed):
completionview.completion_item_focus(which)
model = completionmodel.CompletionModel()
completionview.set_model(model)
completionview.set_model(None)
with qtbot.assertNotEmitted(completionview.selection_changed):
completionview.completion_item_focus(which)
@pytest.mark.parametrize('show', ['always', 'auto', 'never'])
@pytest.mark.parametrize('rows', [[], ['Aa'], ['Aa', 'Bb']])
@pytest.mark.parametrize('quick_complete', [True, False])
def test_completion_show(show, rows, quick_complete, completionview,
config_stub):
"""Test that the completion widget is shown at appropriate times.
Args:
show: The completion show config setting.
rows: Each entry represents a completion category with only one item.
quick_complete: The completion quick-complete config setting.
"""
config_stub.data['completion']['show'] = show
config_stub.data['completion']['quick-complete'] = quick_complete
model = completionmodel.CompletionModel()
for name in rows:
cat = listcategory.ListCategory('', [(name,)])
model.add_category(cat)
assert not completionview.isVisible()
completionview.set_model(model)
assert completionview.isVisible() == (show == 'always' and len(rows) > 0)
completionview.completion_item_focus('next')
expected = (show != 'never' and len(rows) > 0 and
not (quick_complete and len(rows) == 1))
assert completionview.isVisible() == expected
completionview.set_model(None)
completionview.completion_item_focus('next')
assert not completionview.isVisible()
def test_completion_item_del(completionview):
"""Test that completion_item_del invokes delete_cur_item in the model."""
func = mock.Mock(spec=[])
model = completionmodel.CompletionModel()
cat = listcategory.ListCategory('', [('foo', 'bar')], delete_func=func)
model.add_category(cat)
completionview.set_model(model)
completionview.completion_item_focus('next')
completionview.completion_item_del()
func.assert_called_once_with(['foo', 'bar'])
def test_completion_item_del_no_selection(completionview):
"""Test that completion_item_del with an invalid index."""
func = mock.Mock(spec=[])
model = completionmodel.CompletionModel()
cat = listcategory.ListCategory('', [('foo',)], delete_func=func)
model.add_category(cat)
completionview.set_model(model)
with pytest.raises(cmdexc.CommandError, match='No item selected!'):
completionview.completion_item_del()
assert not func.called