Move tree tab moving and dragging tests to test_notree

This commit moves the tests around moving tree tabs into a unit test
targeting the notree library direction. The tests haven't changed but
they run much faster without having to spin up real tabs and windows.
I've left one complex test for each method in the end2end test to make
sure everything is still working there.

I've moved the existing `create_tree()` method into `test_notree.py` and
added a symmetric `tree_to_str()` method. I hope this makes the test
setup more readable than constructing Node objects directly or having
one fixed tree that all the tests operate on.

These tests run in about 150ms on my machine, vs about 20s when they
were using real tabs.

I've used a fancy typed NamedTuple to provide parameters for pytest
because it was quite difficult to discern the different tuple entries
when there was just three strings stacked on top each other. The keyword
arguments make it much more readable than the default pytest setup of
string parameter names and then a big list of tuples. Inspired by these
two posts:

https://til.simonwillison.net/pytest/namedtuple-parameterized-tests
https://mathspp.com/blog/til/better-test-parametrisation-in-pytest
This commit is contained in:
toofar 2025-04-27 12:28:51 +12:00
parent 5dd1077f2c
commit 1c51e60195
3 changed files with 400 additions and 290 deletions

View File

@ -417,67 +417,7 @@ Feature: Tree tab management
- about:blank?three (active) - about:blank?three (active)
""" """
## :tab-move, but with trees # Test a complex move case, most coverage is in test_notree.py
Scenario: Move tab out of a group
When I open about:blank?one
And I open about:blank?two in a new related tab
And I open about:blank?three in a new related tab
And I open about:blank?four in a new tab
And I run :tab-select ?three
And I run :tab-move 4
Then the following tabs should be open:
# one
# two
# three (active)
# four
"""
- about:blank?one
- about:blank?two
- about:blank?four
- about:blank?three (active)
"""
Scenario: Move multiple tabs out of a group
When I open about:blank?one
And I open about:blank?two in a new related tab
And I open about:blank?three in a new related tab
And I open about:blank?four in a new tab
And I open about:blank?five in a new related tab
And I run :tab-select ?two
And I run :tab-move 4
Then the following tabs should be open:
# one
# two (active)
# three
# four
# five
"""
- about:blank?one
- about:blank?four
- about:blank?five
- about:blank?two (active)
- about:blank?three
"""
Scenario: Move two sibling groups
When I open about:blank?one
And I open about:blank?two in a new related tab
And I open about:blank?three in a new tab
And I open about:blank?four in a new related tab
And I run :tab-select ?three
And I run :tab-move 1
Then the following tabs should be open:
# one
# two
# three (active)
# four
"""
- about:blank?three (active)
- about:blank?four
- about:blank?one
- about:blank?two
"""
Scenario: Move multiple tabs into another group Scenario: Move multiple tabs into another group
When I open about:blank?one When I open about:blank?one
And I open about:blank?two in a new related tab And I open about:blank?two in a new related tab
@ -500,207 +440,8 @@ Feature: Tree tab management
- about:blank?three - about:blank?three
""" """
Scenario: Move a tab a single step over a group # Test dragging upwards a couple of steps, most coverage is in
When I open about:blank?one # test_notree.py
And I open about:blank?two in a new tab
And I open about:blank?three in a new related tab
And I run :tab-select ?one
And I run :tab-move 2
Then the following tabs should be open:
# one (active)
# two
# three
"""
- about:blank?two
- about:blank?three
- about:blank?one
"""
## Move tabs via mouse drags
Scenario: Drag a tab down between siblings
When I open about:blank?one
And I open about:blank?two in a new tab
And I run :tab-select ?one
And I run :debug-mouse-move +
Then the following tabs should be open:
# one (active)
# two
"""
- about:blank?two
- about:blank?one (active)
"""
Scenario: Drag a tab down out of a group
When I open about:blank?one
And I open about:blank?two in a new related tab
And I open about:blank?three in a new related tab
And I open about:blank?four in a new tab
And I run :tab-select ?three
And I run :debug-mouse-move +
Then the following tabs should be open:
# one
# two
# three (active)
# four
"""
- about:blank?one
- about:blank?two
- about:blank?four
- about:blank?three (active)
"""
Scenario: Drag a tab down out of a group into another
When I open about:blank?one
And I open about:blank?two in a new related tab
And I open about:blank?three in a new related tab
And I open about:blank?four in a new tab
And I open about:blank?five in a new related tab
And I run :tab-select ?three
And I run :debug-mouse-move +
Then the following tabs should be open:
# one
# two
# three (active)
# four
# five
"""
- about:blank?one
- about:blank?two
- about:blank?four
- about:blank?three (active)
- about:blank?five
"""
Scenario: Drag a tab down a group
When I open about:blank?one
And I open about:blank?two in a new related tab
And I open about:blank?three in a new related tab
And I open about:blank?four in a new tab
And I run :tab-select ?two
And I run :debug-mouse-move +
Then the following tabs should be open:
# one
# two (active)
# three
# four
"""
- about:blank?one
- about:blank?three
- about:blank?two (active)
- about:blank?four
"""
Scenario: Drag a tab with children down a group
When I open about:blank?one
And I open about:blank?two in a new related tab
And I open about:blank?five in a new related tab
And I open about:blank?three in a new sibling tab
And I open about:blank?four in a new related tab
And I open about:blank?six in a new tab
And I run :tab-select ?two
And I run :debug-mouse-move +
Then the following tabs should be open:
# one
# two (active)
# three
# four
# five
# six
"""
- about:blank?one
- about:blank?three
- about:blank?two (active)
- about:blank?four
- about:blank?five
- about:blank?six
"""
## And dragging upwards
Scenario: Drag a tab up between siblings
When I open about:blank?one
And I open about:blank?two in a new tab
And I open about:blank?three in a new related tab
And I run :tab-select ?two
And I run :debug-mouse-move -
Then the following tabs should be open:
# one
# two (active)
# three
"""
- about:blank?two (active)
- about:blank?one
- about:blank?three
"""
Scenario: Drag a tab up out of a group
When I open about:blank?one
And I open about:blank?two in a new tab
And I open about:blank?three in a new related tab
And I open about:blank?four in a new related tab
And I run :tab-select ?three
And I run :debug-mouse-move -
Then the following tabs should be open:
# one
# two
# three (active)
# four
"""
- about:blank?one
- about:blank?three (active)
- about:blank?two
- about:blank?four
"""
Scenario: Drag a tab with children up into a group
When I open about:blank?one
And I open about:blank?foo in a new related tab
And I open about:blank?two in a new sibling tab
And I open about:blank?three in a new tab
And I open about:blank?four in a new related tab
And I open about:blank?five in a new tab
And I run :tab-select ?three
And I run :debug-mouse-move -
Then the following tabs should be open:
# one
# two
# foo
# three (active)
# four
# five
"""
- about:blank?one
- about:blank?two
- about:blank?three (active)
- about:blank?foo
- about:blank?four
- about:blank?five
"""
Scenario: Drag a tab up a group
When I open about:blank?one
And I open about:blank?two in a new related tab
And I open about:blank?five in a new related tab
And I open about:blank?three in a new sibling tab
And I open about:blank?six in a new related tab
And I open about:blank?four in a new tab
And I run :tab-select ?three
And I run :debug-mouse-move -
Then the following tabs should be open:
# one
# two
# three (active)
# six
# five
# four
"""
- about:blank?one
- about:blank?three (active)
- about:blank?two
- about:blank?six
- about:blank?five
- about:blank?four
"""
Scenario: Drag a tab with children up a group Scenario: Drag a tab with children up a group
When I open about:blank?one When I open about:blank?one
And I open about:blank?two in a new related tab And I open about:blank?two in a new related tab
@ -710,6 +451,7 @@ Feature: Tree tab management
And I open about:blank?six in a new tab And I open about:blank?six in a new tab
And I run :tab-select ?three And I run :tab-select ?three
And I run :debug-mouse-move - And I run :debug-mouse-move -
And I run :debug-mouse-move -
Then the following tabs should be open: Then the following tabs should be open:
# one # one
# two # two
@ -718,8 +460,8 @@ Feature: Tree tab management
# five # five
# six # six
""" """
- about:blank?one - about:blank?three (active)
- about:blank?three (active) - about:blank?one
- about:blank?two - about:blank?two
- about:blank?four - about:blank?four
- about:blank?five - about:blank?five

View File

@ -8,6 +8,8 @@ from qutebrowser.config.configtypes import NewTabPosition, NewChildPosition
from qutebrowser.misc.notree import Node from qutebrowser.misc.notree import Node
from qutebrowser.mainwindow import treetabbedbrowser, treetabwidget from qutebrowser.mainwindow import treetabbedbrowser, treetabwidget
from tests.unit.misc.test_notree import str_to_tree
@pytest.fixture @pytest.fixture
def mock_browser(mocker): def mock_browser(mocker):
@ -75,7 +77,7 @@ class TestPositionTab:
"""Test tree tab positioning. """Test tree tab positioning.
How to use the parameters above: How to use the parameters above:
* refer to the tree structure being passed to create_tree() below, that's * refer to the tree structure being passed to str_to_tree() below, that's
our starting state our starting state
* specify how the new node should be related to the current one * specify how the new node should be related to the current one
* specify cur_node by value, which is the tab currently focused when the * specify cur_node by value, which is the tab currently focused when the
@ -94,7 +96,7 @@ class TestPositionTab:
situations apart. But I went this route to avoid having to specify situations apart. But I went this route to avoid having to specify
multiple trees in the parameters. multiple trees in the parameters.
""" """
root = self.create_tree( root = str_to_tree(
""" """
- one - one
- two - two
@ -104,7 +106,7 @@ class TestPositionTab:
- six - six
- seven - seven
""", """,
) )[0]
new_node = Node("new", parent=root) new_node = Node("new", parent=root)
config_stub.val.tabs.new_position.stacking = False config_stub.val.tabs.new_position.stacking = False
@ -178,27 +180,6 @@ class TestPositionTab:
background=background, background=background,
) )
def create_tree(self, tree_str):
# Construct a notree.Node tree from the test string.
root = Node("root")
previous_indent = ''
previous_node = root
for line in tree_str.splitlines():
if not line.strip():
continue
indent, value = line.split("-")
node = Node(value.strip())
if len(indent) > len(previous_indent):
node.parent = previous_node
elif len(indent) == len(previous_indent):
node.parent = previous_node.parent
else:
# TODO: handle going up in jumps of more than one rank
node.parent = previous_node.parent.parent
previous_indent = indent
previous_node = node
return root
@pytest.mark.parametrize( @pytest.mark.parametrize(
" test_tree, relation, pos, expected", [ " test_tree, relation, pos, expected", [
("tree_one", "sibling", "next", "one,two,new1,new2,new3",), ("tree_one", "sibling", "next", "one,two,new1,new2,new3",),
@ -229,12 +210,12 @@ class TestPositionTab:
""" """
# Simpler tree here to make the assert string a bit simpler. # Simpler tree here to make the assert string a bit simpler.
# Tab "two" is hardcoded as cur_tab. # Tab "two" is hardcoded as cur_tab.
root = self.create_tree( root = str_to_tree(
""" """
- one - one
- two - two
""", """,
) )[0]
config_stub.val.tabs.new_position.stacking = True config_stub.val.tabs.new_position.stacking = True
for val in ["new1", "new2", "new3"]: for val in ["new1", "new2", "new3"]:

View File

@ -2,6 +2,10 @@
# #
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
"""Tests for misc.notree library.""" """Tests for misc.notree library."""
import itertools
import textwrap
from typing import NamedTuple, Optional
import pytest import pytest
from qutebrowser.misc.notree import TreeError, Node, TraverseOrder from qutebrowser.misc.notree import TreeError, Node, TraverseOrder
@ -294,3 +298,386 @@ def test_memoization(node):
assert node.children[0]._Node__modified is True assert node.children[0]._Node__modified is True
node.render() node.render()
assert node._Node__modified is False assert node._Node__modified is False
def str_to_tree(tree_str: str) -> tuple["Node", "Node"]:
"""Construct a notree.Node tree from a string.
Input strings can look like:
one (active)
two
Or
- one (active)
- two
You can use any indent to separate levels of the tree but it needs to be
consistent.
Returns a tuple of (tree_root, active_tab).
"""
root = Node("root")
previous_indent = ""
previous_node = root
indent_increment = None
active = None
for line in textwrap.dedent(tree_str).splitlines():
if not line.strip():
continue
indent = "".join(list(itertools.takewhile(lambda c: c.isspace(), line)))
if indent and not indent_increment:
indent_increment = indent
line = line.removeprefix(indent)
line = line.removeprefix("- ") # Strip optional dash prefix thing
parts = line.split()
value = parts[0]
node = Node(value)
if len(parts) > 1:
if parts[1] == "(active)":
active = node
if previous_node == root:
node.parent = root
elif len(indent) > len(previous_indent):
node.parent = previous_node
elif len(indent) == len(previous_indent):
node.parent = previous_node.parent
else:
if not indent:
level = 0
else:
level = int(len(indent) / len(indent_increment))
node.parent = previous_node.path[level]
previous_indent = indent
previous_node = node
return root, active
def tree_to_str(
node: Node,
active: Optional[bool] = None,
indent: str = "",
indent_increment: str = " ",
include_root: bool = False,
) -> None:
"""Serialize Node tree into a string.
Output string will look like:
one
two
three (active)
With two spaces of indentation marking levels of the tree. The reverse of
`str_to_tree()`.
"""
lines = []
if not node.parent and include_root is False:
next_indent = indent
else:
next_indent = indent + indent_increment
lines.append(f"{indent}{node.value}{' (active)' if node == active else ''}")
for child in node.children:
lines.append(
tree_to_str(
child, active, indent=next_indent, indent_increment=indent_increment
)
)
return "\n".join(lines)
class MoveTestArgs(NamedTuple):
description: str
before: str
to: str
after: str
@pytest.mark.parametrize(
MoveTestArgs._fields,
[
MoveTestArgs(
description="Move a tab out of a group",
before="""
one
two
three (active)
four
""",
to="four",
after="""
one
two
four
three (active)
""",
),
MoveTestArgs(
description="Move multiple tabs out of a group",
before="""
one
two (active)
three
four
five
""",
to="four",
after="""
one
four
five
two (active)
three
""",
),
MoveTestArgs(
description="Move two sibling groups",
before="""
one
two
three (active)
four
""",
to="one",
after="""
three (active)
four
one
two
""",
),
MoveTestArgs(
description="Move multiple tabs into another group",
before="""
one
two (active)
three
four
five
""",
to="five",
after="""
one
four
five
two (active)
three
""",
),
MoveTestArgs(
description="Move a tab a single step over a group",
before="""
one (active)
two
three
""",
to="two",
after="""
two
three
one (active)
""",
),
],
)
def test_move_recursive(description, before, to, after):
before, after = textwrap.dedent(before), textwrap.dedent(after).strip()
tree_root, active = str_to_tree(before)
nodes = list(tree_root.traverse(render_collapsed=False))
to_node = [node for node in nodes if node.value == to][0]
active.move_recursive(to_node)
assert tree_to_str(tree_root, active) == after
@pytest.mark.parametrize(
MoveTestArgs._fields,
[
# And now the drag downwards test cases
MoveTestArgs(
description="Drag a tab down between siblings",
before="""
one (active)
two
""",
to="+",
after="""
two
one (active)
""",
),
MoveTestArgs(
description="Drag a tab down out of a group",
before="""
one
two
three (active)
four
""",
to="+",
after="""
one
two
four
three (active)
""",
),
MoveTestArgs(
description="Drag a tab down out of a group into another",
before="""
one
two
three (active)
four
five
""",
to="+",
after="""
one
two
four
three (active)
five
""",
),
MoveTestArgs(
description="Drag a tab down a group",
before="""
one
two (active)
three
four
""",
to="+",
after="""
one
three
two (active)
four
""",
),
MoveTestArgs(
description="Drag a tab with children down a group",
before="""
one
two (active)
three
four
five
six
""",
to="+",
after="""
one
three
two (active)
four
five
six
""",
),
# And now the drag upwards test cases
MoveTestArgs(
description="Drag a tab up between siblings",
before="""
one
two (active)
three
""",
to="-",
after="""
two (active)
one
three
""",
),
MoveTestArgs(
description="Drag a tab up out of a group",
before="""
one
two
three (active)
four
""",
to="-",
after="""
one
three (active)
two
four
""",
),
MoveTestArgs(
description="Drag a tab with children up into a group",
before="""
one
two
foo
three (active)
four
five
""",
to="-",
after="""
one
two
three (active)
foo
four
five
""",
),
MoveTestArgs(
description="Drag a tab up a group",
before="""
one
two
three (active)
six
five
four
""",
to="-",
after="""
one
three (active)
two
six
five
four
""",
),
MoveTestArgs(
description="Drag a tab with children up a group",
before="""
one
two
three (active)
four
five
six
""",
to="-",
after="""
one
three (active)
two
four
five
six
""",
),
],
)
def test_drag(description, before, to, after):
before, after = textwrap.dedent(before), textwrap.dedent(after).strip()
tree_root, active = str_to_tree(before)
active.drag(to)
assert tree_to_str(tree_root, active) == after