summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortoofar <toofar@spalge.com>2024-02-08 19:33:34 +1300
committerGitHub <noreply@github.com>2024-02-08 19:33:34 +1300
commit8637347641012a68482310bf6274d6cbbc4fa38c (patch)
tree087abc0f384803ccb11a8ff7c600d3b3cfd2b2e7
parent0d91c22acc7cd6bf822a5f4e9ab398baeaf74a80 (diff)
parent273345bff8d6369fcaaebe4ec4bd8a6951f4c618 (diff)
downloadqutebrowser-8637347641012a68482310bf6274d6cbbc4fa38c.tar.gz
qutebrowser-8637347641012a68482310bf6274d6cbbc4fa38c.zip
Merge pull request #8083 from pylbrecht/tree-tabs-e2e
Add end2end tests for tree tabs feature
-rw-r--r--qutebrowser/misc/sessions.py50
-rw-r--r--tests/end2end/features/conftest.py214
-rw-r--r--tests/end2end/features/test_treetabs_bdd.py6
-rw-r--r--tests/end2end/features/treetabs.feature27
4 files changed, 194 insertions, 103 deletions
diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py
index dac43d238..fe8e7ba24 100644
--- a/qutebrowser/misc/sessions.py
+++ b/qutebrowser/misc/sessions.py
@@ -121,6 +121,42 @@ class TabHistoryItem:
last_visited=self.last_visited)
+def reconstruct_tree_data(window_data):
+ """Return a dict usable as a tree from a window.
+
+ Returns a dict like:
+ {
+ 1: {'children': [2]},
+ 2: {
+ ...tab,
+ "treetab_node_data": {
+ "children": [],
+ "collapsed": False,
+ "parent": 1,
+ "uid": 2,
+ }
+ }
+ }
+
+ Which you can traverse by starting at the node with no "treetab_node_data"
+ attribute (the root) and pulling successive levels of children from the
+ dict using their `uid`s as keys.
+
+ The ...tab part represents the usual attributes for a tab when saved in a
+ session.
+ """
+ tree_data = {}
+ root = window_data['treetab_root']
+ tree_data[root['uid']] = {
+ 'children': root['children'],
+ 'tab': {},
+ 'collapsed': False
+ }
+ for tab in window_data['tabs']:
+ tree_data[tab['treetab_node_data']['uid']] = tab
+ return tree_data
+
+
class SessionManager(QObject):
"""Manager for sessions.
@@ -474,18 +510,6 @@ class SessionManager(QObject):
except ValueError as e:
raise SessionError(e)
- def _reconstruct_tree_data(self, window_data):
- tree_data = {}
- root = window_data['treetab_root']
- tree_data[root['uid']] = {
- 'children': root['children'],
- 'tab': {},
- 'collapsed': False
- }
- for tab in window_data['tabs']:
- tree_data[tab['treetab_node_data']['uid']] = tab
- return tree_data
-
def _load_tree(self, tabbed_browser, tree_data):
tree_keys = list(tree_data.keys())
if not tree_keys:
@@ -540,7 +564,7 @@ class SessionManager(QObject):
load_tree_tabs = 'treetab_root' in win.keys() and \
tabbed_browser.is_treetabbedbrowser
if load_tree_tabs:
- tree_data = self._reconstruct_tree_data(win)
+ tree_data = reconstruct_tree_data(win)
self._load_tree(tabbed_browser, tree_data)
else:
for i, tab in enumerate(tabs):
diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py
index 70a5fc205..6511bdd96 100644
--- a/tests/end2end/features/conftest.py
+++ b/tests/end2end/features/conftest.py
@@ -20,6 +20,7 @@ import pytest
import pytest_bdd as bdd
import qutebrowser
+from qutebrowser.misc import sessions
from qutebrowser.utils import log, utils, docutils, version
from qutebrowser.browser import pdfjs
from end2end.fixtures import testprocess
@@ -125,14 +126,14 @@ def set_setting_given(quteproc, server, opt, value):
@bdd.given(bdd.parsers.parse("I open {path}"))
-def open_path_given(quteproc, path):
+def open_path_given(quteproc, server, path):
"""Open a URL.
This is available as "Given:" step so it can be used as "Background:".
It always opens a new tab, unlike "When I open ..."
"""
- quteproc.open_path(path, new_tab=True)
+ open_path(quteproc, server, path, default_kwargs={"new_tab": True})
@bdd.given(bdd.parsers.parse("I run {command}"))
@@ -188,7 +189,7 @@ def clear_log_lines(quteproc):
@bdd.when(bdd.parsers.parse("I open {path}"))
-def open_path(quteproc, server, path):
+def open_path(quteproc, server, path, default_kwargs: dict = None):
"""Open a URL.
- If used like "When I open ... in a new tab", the URL is opened in a new
@@ -200,56 +201,40 @@ def open_path(quteproc, server, path):
path = path.replace('(port)', str(server.port))
path = testutils.substitute_testdata(path)
- new_tab = False
- related_tab = False
- new_bg_tab = False
- new_window = False
- private = False
- as_url = False
- wait = True
-
- related_tab_suffix = ' in a new related tab'
- related_background_tab_suffix = ' in a new related background tab'
- new_tab_suffix = ' in a new tab'
- new_bg_tab_suffix = ' in a new background tab'
- new_window_suffix = ' in a new window'
- private_suffix = ' in a private window'
- do_not_wait_suffix = ' without waiting'
- as_url_suffix = ' as a URL'
+ suffixes = {
+ "in a new tab": "new_tab",
+ "in a new related tab": ("new_tab", "related_tab"),
+ "in a new related background tab": ("new_bg_tab", "related_tab"),
+ "in a new background tab": "new_bg_tab",
+ "in a new window": "new_window",
+ "in a private window": "private",
+ "without waiting": {"wait": False},
+ "as a URL": "as_url",
+ }
+
+ def update_from_value(value, kwargs):
+ if isinstance(value, str):
+ kwargs[value] = True
+ elif isinstance(value, (tuple, list)):
+ for i in value:
+ update_from_value(i, kwargs)
+ elif isinstance(value, dict):
+ kwargs.update(value)
+ kwargs = {}
while True:
- if path.endswith(new_tab_suffix):
- path = path[:-len(new_tab_suffix)]
- new_tab = True
- elif path.endswith(related_tab_suffix):
- path = path[:-len(related_tab_suffix)]
- new_tab = True
- related_tab = True
- elif path.endswith(related_background_tab_suffix):
- path = path[:-len(related_background_tab_suffix)]
- new_bg_tab = True
- related_tab = True
- elif path.endswith(new_bg_tab_suffix):
- path = path[:-len(new_bg_tab_suffix)]
- new_bg_tab = True
- elif path.endswith(new_window_suffix):
- path = path[:-len(new_window_suffix)]
- new_window = True
- elif path.endswith(private_suffix):
- path = path[:-len(private_suffix)]
- private = True
- elif path.endswith(as_url_suffix):
- path = path[:-len(as_url_suffix)]
- as_url = True
- elif path.endswith(do_not_wait_suffix):
- path = path[:-len(do_not_wait_suffix)]
- wait = False
+ for suffix, value in suffixes.items():
+ if path.endswith(suffix):
+ path = path[:-len(suffix) - 1]
+ update_from_value(value, kwargs)
+ break
else:
break
- quteproc.open_path(path, related_tab=related_tab, new_tab=new_tab,
- new_bg_tab=new_bg_tab, new_window=new_window,
- private=private, as_url=as_url, wait=wait)
+ if not kwargs and default_kwargs:
+ kwargs.update(default_kwargs)
+
+ quteproc.open_path(path, **kwargs)
@bdd.when(bdd.parsers.parse("I set {opt} to {value}"))
@@ -620,55 +605,104 @@ def check_contents_json(quteproc, text):
assert actual == expected
-@bdd.then(bdd.parsers.parse("the following tabs should be open:\n{tabs}"))
-def check_open_tabs(quteproc, request, tabs):
- """Check the list of open tabs in the session.
+@bdd.then(bdd.parsers.parse("the following tabs should be open:\n{expected_tabs}"))
+def check_open_tabs(quteproc, request, expected_tabs):
+ """Check the list of open tabs in a one window session.
This is a lightweight alternative for "The session should look like: ...".
- It expects a list of URLs, with an optional "(active)" suffix.
+ It expects a tree of URLs in the form:
+ - data/numbers/1.txt
+ - data/numbers/2.txt (active)
+
+ Where the indentation is optional (but if present the indent should be two
+ spaces) and the suffix can be one or more of:
+
+ (active)
+ (pinned)
+ (collapsed)
"""
session = quteproc.get_session()
+ expected_tabs = expected_tabs.splitlines()
+ assert len(session['windows']) == 1
+ window = session['windows'][0]
+ assert len(window['tabs']) == len(expected_tabs)
+
active_suffix = ' (active)'
pinned_suffix = ' (pinned)'
- tabs = tabs.splitlines()
- assert len(session['windows']) == 1
- assert len(session['windows'][0]['tabs']) == len(tabs)
-
- # If we don't have (active) anywhere, don't check it
- has_active = any(active_suffix in line for line in tabs)
- has_pinned = any(pinned_suffix in line for line in tabs)
-
- for i, line in enumerate(tabs):
- line = line.strip()
- assert line.startswith('- ')
- line = line[2:] # remove "- " prefix
-
- active = False
- pinned = False
-
- while line.endswith(active_suffix) or line.endswith(pinned_suffix):
- if line.endswith(active_suffix):
- # active
- line = line[:-len(active_suffix)]
- active = True
- else:
- # pinned
- line = line[:-len(pinned_suffix)]
- pinned = True
-
- session_tab = session['windows'][0]['tabs'][i]
- current_page = session_tab['history'][-1]
- assert current_page['url'] == quteproc.path_to_url(line)
- if active:
- assert session_tab['active']
- elif has_active:
- assert 'active' not in session_tab
-
- if pinned:
- assert current_page['pinned']
- elif has_pinned:
- assert not current_page['pinned']
+ collapsed_suffix = ' (collapsed)'
+ # Don't check for states in the session if they aren't in the expected
+ # text.
+ has_active = any(active_suffix in line for line in expected_tabs)
+ has_pinned = any(pinned_suffix in line for line in expected_tabs)
+ has_collapsed = any(collapsed_suffix in line for line in expected_tabs)
+
+ def tab_to_str(tab, prefix="", collapsed=False):
+ """Convert a tab from a session file into a one line string."""
+ current = [
+ entry
+ for entry in tab["history"]
+ if entry.get("active")
+ ][0]
+ text = f"{prefix}- {current['url']}"
+ for suffix, state in {
+ active_suffix: tab.get("active") and has_active,
+ collapsed_suffix: collapsed and has_collapsed,
+ pinned_suffix: current["pinned"] and has_pinned,
+ }.items():
+ if state:
+ text += suffix
+ return text
+
+ def tree_to_str(node, tree_data, indentation=-1):
+ """Traverse a tree turning each node into an indented string."""
+ tree_node = node.get("treetab_node_data")
+ if tree_node: # root node doesn't have treetab_node_data
+ yield tab_to_str(
+ node,
+ prefix=" " * indentation,
+ collapsed=tree_node["collapsed"],
+ )
+ else:
+ tree_node = node
+
+ for uid in tree_node["children"]:
+ yield from tree_to_str(tree_data[uid], tree_data, indentation + 1)
+
+ is_tree_tab_window = "treetab_root" in window
+ if is_tree_tab_window:
+ tree_data = sessions.reconstruct_tree_data(window)
+ root = [node for node in tree_data.values() if "treetab_node_data" not in node][0]
+ actual = list(tree_to_str(root, tree_data))
+ else:
+ actual = [tab_to_str(tab) for tab in window["tabs"]]
+
+ def normalize(line):
+ """Normalize expected lines to match session lines.
+
+ Turn paths into URLs and sort suffixes.
+ """
+ prefix, rest = line.split("- ", maxsplit=1)
+ path = rest.split(" ", maxsplit=1)
+ path[0] = quteproc.path_to_url(path[0])
+ if len(path) == 2:
+ suffixes = path[1].split()
+ for s in suffixes:
+ assert s[0] == "("
+ assert s[-1] == ")"
+ path[1] = " ".join(sorted(suffixes))
+ return "- ".join((prefix, " ".join(path)))
+
+ expected_tabs = [
+ normalize(line)
+ for line in expected_tabs
+ ]
+ # Removed the hyphens from the start of lines so they don't get mixed in
+ # with the diff markers.
+ expected_tabs = [line.replace("- ", "") for line in expected_tabs]
+ actual = [line.replace("- ", "") for line in actual]
+ for idx, expected in enumerate(expected_tabs):
+ assert expected == actual[idx]
@bdd.then(bdd.parsers.re(r'the (?P<what>primary selection|clipboard) should '
diff --git a/tests/end2end/features/test_treetabs_bdd.py b/tests/end2end/features/test_treetabs_bdd.py
new file mode 100644
index 000000000..9cbb315d7
--- /dev/null
+++ b/tests/end2end/features/test_treetabs_bdd.py
@@ -0,0 +1,6 @@
+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+import pytest_bdd as bdd
+bdd.scenarios("treetabs.feature")
diff --git a/tests/end2end/features/treetabs.feature b/tests/end2end/features/treetabs.feature
new file mode 100644
index 000000000..f32ff3d07
--- /dev/null
+++ b/tests/end2end/features/treetabs.feature
@@ -0,0 +1,27 @@
+Feature: Tree tab management
+ Tests for various :tree-tab-* commands.
+
+ Background:
+ # Open a new tree tab enabled window, close everything else
+ Given I set tabs.tabs_are_windows to false
+ And I set tabs.tree_tabs to true
+ And I open about:blank?starting%20page in a new window
+ And I clean up open tabs
+ And I clear the log
+
+ Scenario: :tab-close --recursive
+ When I open data/numbers/1.txt
+ And I open data/numbers/2.txt in a new related tab
+ And I open data/numbers/3.txt in a new related tab
+ And I open data/numbers/4.txt in a new tab
+ And I run :tab-focus 1
+ And I run :tab-close --recursive
+ Then the following tabs should be open:
+ - data/numbers/4.txt
+
+ Scenario: Open a child tab
+ When I open data/numbers/1.txt
+ And I open data/numbers/2.txt in a new related tab
+ Then the following tabs should be open:
+ - data/numbers/1.txt
+ - data/numbers/2.txt (active)