1# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
2
3# Copyright 2015-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
4# Copyright 2015-2018 Daniel Schadt
5#
6# This file is part of qutebrowser.
7#
8# qutebrowser is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# qutebrowser is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with qutebrowser.  If not, see <https://www.gnu.org/licenses/>.
20
21"""Test the built-in directory browser."""
22
23import pathlib
24import dataclasses
25from typing import List
26
27import pytest
28import bs4
29
30from PyQt5.QtCore import QUrl
31
32from qutebrowser.utils import urlutils
33from helpers import testutils
34
35
36pytestmark = pytest.mark.qtwebengine_skip("Title is empty when parsing for "
37                                          "some reason?")
38
39
40class DirLayout:
41
42    """Provide a fake directory layout to test dirbrowser."""
43
44    LAYOUT = [
45        'folder0/file00',
46        'folder0/file01',
47        'folder1/folder10/file100',
48        'folder1/file10',
49        'folder1/file11',
50        'file0',
51        'file1',
52    ]
53
54    @classmethod
55    def layout_folders(cls):
56        """Return all folders in the root directory of the layout."""
57        folders = set()
58        for path in cls.LAYOUT:
59            parts = path.split('/')
60            if len(parts) > 1:
61                folders.add(parts[0])
62        folders = list(folders)
63        folders.sort()
64        return folders
65
66    @classmethod
67    def get_folder_content(cls, name):
68        """Return (folders, files) for the given folder in the root dir."""
69        folders = set()
70        files = set()
71        for path in cls.LAYOUT:
72            if not path.startswith(name + '/'):
73                continue
74            parts = path.split('/')
75            if len(parts) == 2:
76                files.add(parts[1])
77            else:
78                folders.add(parts[1])
79        folders = list(folders)
80        folders.sort()
81        files = list(files)
82        files.sort()
83        return (folders, files)
84
85    def __init__(self, factory):
86        self._factory = factory
87        self.base = factory.getbasetemp()
88        self.layout = factory.mktemp('layout')
89        self._mklayout()
90
91    def _mklayout(self):
92        for filename in self.LAYOUT:
93            path = self.layout / filename
94            path.parent.mkdir(exist_ok=True, parents=True)
95            path.touch()
96
97    def file_url(self):
98        """Return a file:// link to the directory."""
99        return urlutils.file_url(str(self.layout))
100
101    def path(self, *parts):
102        """Return the path to the given file inside the layout folder."""
103        return self.layout.joinpath(*parts)
104
105    def base_path(self):
106        """Return the path of the base temporary folder."""
107        return self.base
108
109
110@dataclasses.dataclass
111class Parsed:
112
113    path: str
114    parent: str
115    folders: List[str]
116    files: List[str]
117
118
119@dataclasses.dataclass
120class Item:
121
122    path: str
123    link: str
124    text: str
125
126
127def parse(quteproc):
128    """Parse the dirbrowser content from the given quteproc.
129
130    Args:
131        quteproc: The quteproc fixture.
132    """
133    html = quteproc.get_content(plain=False)
134    soup = bs4.BeautifulSoup(html, 'html.parser')
135
136    with testutils.ignore_bs4_warning():
137        print(soup.prettify())
138
139    title_prefix = 'Browse directory: '
140    # Strip off the title prefix to obtain the path of the folder that
141    # we're browsing
142    path = pathlib.Path(soup.title.string[len(title_prefix):])
143
144    container = soup('div', id='dirbrowserContainer')[0]
145
146    parent_elem = container('ul', class_='parent')
147    if not parent_elem:
148        parent = None
149    else:
150        parent = pathlib.Path(QUrl(parent_elem[0].li.a['href']).toLocalFile())
151
152    folders = []
153    files = []
154
155    for css_class, list_ in [('folders', folders), ('files', files)]:
156        for li in container('ul', class_=css_class)[0]('li'):
157            item_path = pathlib.Path(QUrl(li.a['href']).toLocalFile())
158            list_.append(Item(path=item_path, link=li.a['href'],
159                              text=str(li.a.string)))
160
161    return Parsed(path=path, parent=parent, folders=folders, files=files)
162
163
164@pytest.fixture(scope='module')
165def dir_layout(tmp_path_factory):
166    return DirLayout(tmp_path_factory)
167
168
169def test_parent_folder(dir_layout, quteproc):
170    quteproc.open_url(dir_layout.file_url())
171    page = parse(quteproc)
172    assert page.parent == dir_layout.base_path()
173
174
175def test_parent_with_slash(dir_layout, quteproc):
176    """Test the parent link with a URL that has a trailing slash."""
177    quteproc.open_url(dir_layout.file_url() + '/')
178    page = parse(quteproc)
179    assert page.parent == dir_layout.base_path()
180
181
182def test_parent_in_root_dir(dir_layout, quteproc):
183    # This actually works on windows
184    urlstr = urlutils.file_url(str(pathlib.Path('/')))
185    quteproc.open_url(urlstr)
186    page = parse(quteproc)
187    assert page.parent is None
188
189
190def test_enter_folder_smoke(dir_layout, quteproc):
191    quteproc.open_url(dir_layout.file_url())
192    quteproc.send_cmd(':hint all normal')
193    # a is the parent link, s is the first listed folder/file
194    quteproc.send_cmd(':hint-follow s')
195    expected_url = urlutils.file_url(str(dir_layout.path('folder0')))
196    quteproc.wait_for_load_finished_url(expected_url)
197    page = parse(quteproc)
198    assert page.path == dir_layout.path('folder0')
199
200
201@pytest.mark.parametrize('folder', DirLayout.layout_folders())
202def test_enter_folder(dir_layout, quteproc, folder):
203    quteproc.open_url(dir_layout.file_url())
204    quteproc.click_element_by_text(text=folder)
205    expected_url = urlutils.file_url(str(dir_layout.path(folder)))
206    quteproc.wait_for_load_finished_url(expected_url)
207    page = parse(quteproc)
208    assert page.path == dir_layout.path(folder)
209    assert page.parent == dir_layout.path()
210    folders, files = DirLayout.get_folder_content(folder)
211    foldernames = [item.text for item in page.folders]
212    assert foldernames == folders
213    filenames = [item.text for item in page.files]
214    assert filenames == files
215