Merge branch 'dirbrowser-issue-1334' of https://github.com/Kingdread/qutebrowser into Kingdread-dirbrowser-issue-1334
This commit is contained in:
commit
6c7e2492e9
@ -57,9 +57,29 @@ def is_root(directory):
|
||||
Return:
|
||||
Whether the directory is a root directory or not.
|
||||
"""
|
||||
# If you're curious as why this works:
|
||||
# dirname('/') = '/'
|
||||
# dirname('/home') = '/'
|
||||
# dirname('/home/') = '/home'
|
||||
# dirname('/home/foo') = '/home'
|
||||
# basically, for files (no trailing slash) it removes the file part, and
|
||||
# for directories, it removes the trailing slash, so the only way for this
|
||||
# to be equal is if the directory is the root directory.
|
||||
return os.path.dirname(directory) == directory
|
||||
|
||||
|
||||
def parent_dir(directory):
|
||||
"""Return the parent directory for the given directory.
|
||||
|
||||
Args:
|
||||
directory: The path to the directory.
|
||||
|
||||
Return:
|
||||
The path to the parent directory.
|
||||
"""
|
||||
return os.path.normpath(os.path.join(directory, os.pardir))
|
||||
|
||||
|
||||
def dirbrowser_html(path):
|
||||
"""Get the directory browser web page.
|
||||
|
||||
@ -74,14 +94,14 @@ def dirbrowser_html(path):
|
||||
if is_root(path):
|
||||
parent = None
|
||||
else:
|
||||
parent = os.path.dirname(path)
|
||||
parent = parent_dir(path)
|
||||
|
||||
try:
|
||||
all_files = os.listdir(path)
|
||||
except OSError as e:
|
||||
html = jinja.render('error.html',
|
||||
title="Error while reading directory",
|
||||
url='file://{}'.format(path), error=str(e),
|
||||
url='file:///{}'.format(path), error=str(e),
|
||||
icon='')
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
|
@ -46,21 +46,21 @@ ul.files > li {
|
||||
<p id="dirbrowserTitleText">Browse directory: {{url}}</p>
|
||||
</div>
|
||||
|
||||
{% if parent %}
|
||||
{% if parent is not none %}
|
||||
<ul class="parent">
|
||||
<li><a href="{{parent}}">..</a></li>
|
||||
<li><a href="{{ file_url(parent) }}">..</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<ul class="folders">
|
||||
{% for item in directories %}
|
||||
<li><a href="file://{{item.absname}}">{{item.name}}</a></li>
|
||||
<li><a href="{{ file_url(item.absname) }}">{{item.name}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<ul class="files">
|
||||
{% for item in files %}
|
||||
<li><a href="file://{{item.absname}}">{{item.name}}</a></li>
|
||||
<li><a href="{{ file_url(item.absname) }}">{{item.name}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -74,6 +74,15 @@ def resource_url(path):
|
||||
return QUrl.fromLocalFile(image).toString(QUrl.FullyEncoded)
|
||||
|
||||
|
||||
def file_url(path):
|
||||
"""Return a file:// url (as string) to the given local path.
|
||||
|
||||
Arguments:
|
||||
path: The absolute path to the local file
|
||||
"""
|
||||
return QUrl.fromLocalFile(path).toString(QUrl.FullyEncoded)
|
||||
|
||||
|
||||
def render(template, **kwargs):
|
||||
"""Render the given template and pass the given arguments to it."""
|
||||
try:
|
||||
@ -85,5 +94,7 @@ def render(template, **kwargs):
|
||||
tb = traceback.format_exc()
|
||||
return err_template.format(pagename=template, traceback=tb)
|
||||
|
||||
|
||||
_env = jinja2.Environment(loader=Loader('html'), autoescape=_guess_autoescape)
|
||||
_env.globals['resource_url'] = resource_url
|
||||
_env.globals['file_url'] = file_url
|
||||
|
11
tests/integration/data/click_element.html
Normal file
11
tests/integration/data/click_element.html
Normal file
@ -0,0 +1,11 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>quteprocess.click_element test</title>
|
||||
</head>
|
||||
<body>
|
||||
<span onclick='console.log("click_element clicked")'>Test Element</span>
|
||||
<span onclick='console.log("click_element special chars")'>"Don't", he shouted</span>
|
||||
<span>Duplicate</span>
|
||||
<span>Duplicate</span>
|
||||
</body>
|
||||
</html>
|
@ -37,6 +37,7 @@ from PyQt5.QtCore import pyqtSignal, QUrl
|
||||
import testprocess
|
||||
from qutebrowser.misc import ipc
|
||||
from qutebrowser.utils import log, utils
|
||||
from qutebrowser.browser import webelem
|
||||
from helpers import utils as testutils
|
||||
|
||||
|
||||
@ -253,9 +254,14 @@ class QuteProc(testprocess.Process):
|
||||
path if path != '/' else '')
|
||||
|
||||
def wait_for_js(self, message):
|
||||
"""Wait for the given javascript console message."""
|
||||
self.wait_for(category='js', function='javaScriptConsoleMessage',
|
||||
message='[*] {}'.format(message))
|
||||
"""Wait for the given javascript console message.
|
||||
|
||||
Return:
|
||||
The LogLine.
|
||||
"""
|
||||
return self.wait_for(category='js',
|
||||
function='javaScriptConsoleMessage',
|
||||
message='[*] {}'.format(message))
|
||||
|
||||
def _is_error_logline(self, msg):
|
||||
"""Check if the given LogLine is some kind of error message."""
|
||||
@ -348,10 +354,14 @@ class QuteProc(testprocess.Process):
|
||||
def open_path(self, path, *, new_tab=False, new_window=False, port=None,
|
||||
https=False):
|
||||
"""Open the given path on the local webserver in qutebrowser."""
|
||||
url = self.path_to_url(path, port=port, https=https)
|
||||
self.open_url(url, new_tab=new_tab, new_window=new_window)
|
||||
|
||||
def open_url(self, url, *, new_tab=False, new_window=False):
|
||||
"""Open the given url in qutebrowser."""
|
||||
if new_tab and new_window:
|
||||
raise ValueError("new_tab and new_window given!")
|
||||
|
||||
url = self.path_to_url(path, port=port, https=https)
|
||||
if new_tab:
|
||||
self.send_cmd(':open -t ' + url)
|
||||
elif new_window:
|
||||
@ -418,6 +428,57 @@ class QuteProc(testprocess.Process):
|
||||
"""Press the given keys using :fake-key."""
|
||||
self.send_cmd(':fake-key -g "{}"'.format(keys))
|
||||
|
||||
def click_element(self, text):
|
||||
"""Click the element with the given text."""
|
||||
# Use Javascript and XPath to find the right element, use console.log
|
||||
# to return an error (no element found, ambiguous element)
|
||||
script = (
|
||||
'var _es = document.evaluate(\'//*[text()={text}]\', document, '
|
||||
'null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);'
|
||||
'if (_es.snapshotLength == 0) {{ console.log("qute:no elems"); }} '
|
||||
'else if (_es.snapshotLength > 1) {{ console.log("qute:ambiguous '
|
||||
'elems") }} '
|
||||
'else {{ console.log("qute:okay"); _es.snapshotItem(0).click() }}'
|
||||
).format(text=webelem.javascript_escape(_xpath_escape(text)))
|
||||
self.send_cmd(':jseval ' + script)
|
||||
message = self.wait_for_js('qute:*').message
|
||||
if message.endswith('qute:no elems'):
|
||||
raise ValueError('No element with {!r} found'.format(text))
|
||||
elif message.endswith('qute:ambiguous elems'):
|
||||
raise ValueError('Element with {!r} is not unique'.format(text))
|
||||
elif not message.endswith('qute:okay'):
|
||||
raise ValueError('Invalid response from qutebrowser: {}'
|
||||
.format(message))
|
||||
|
||||
|
||||
def _xpath_escape(text):
|
||||
"""Escape a string to be used in an XPath expression.
|
||||
|
||||
The resulting string should still be escaped with javascript_escape, to
|
||||
prevent javascript from interpreting the quotes.
|
||||
|
||||
This function is needed because XPath does not provide any character
|
||||
escaping mechanisms, so to get the string
|
||||
"I'm back", he said
|
||||
you have to use concat like
|
||||
concat('"I', "'m back", '", he said')
|
||||
|
||||
Args:
|
||||
text: Text to escape
|
||||
|
||||
Return:
|
||||
The string "escaped" as a concat() call.
|
||||
"""
|
||||
# Shortcut if at most a single quoting style is used
|
||||
if "'" not in text or '"' not in text:
|
||||
return repr(text)
|
||||
parts = re.split('([\'"])', text)
|
||||
# Python's repr() of strings will automatically choose the right quote
|
||||
# type. Since each part only contains one "type" of quote, no escaping
|
||||
# should be necessary.
|
||||
parts = [repr(part) for part in parts if part]
|
||||
return 'concat({})'.format(', '.join(parts))
|
||||
|
||||
|
||||
@pytest.yield_fixture(scope='module')
|
||||
def quteproc_process(qapp, httpbin, request):
|
||||
|
189
tests/integration/test_dirbrowser.py
Normal file
189
tests/integration/test_dirbrowser.py
Normal file
@ -0,0 +1,189 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2015-2016 Daniel Schadt
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
"""Test the built-in directory browser."""
|
||||
|
||||
import os
|
||||
import bs4
|
||||
import collections
|
||||
|
||||
import pytest
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from qutebrowser.utils import jinja
|
||||
|
||||
|
||||
class DirLayout:
|
||||
|
||||
"""Provide a fake directory layout to test dirbrowser."""
|
||||
|
||||
LAYOUT = [
|
||||
'folder0/file00',
|
||||
'folder0/file01',
|
||||
'folder1/folder10/file100',
|
||||
'folder1/file10',
|
||||
'folder1/file11',
|
||||
'file0',
|
||||
'file1',
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def layout_folders(cls):
|
||||
"""Return all folders in the root directory of the layout."""
|
||||
folders = set()
|
||||
for path in cls.LAYOUT:
|
||||
parts = path.split('/')
|
||||
if len(parts) > 1:
|
||||
folders.add(parts[0])
|
||||
folders = list(folders)
|
||||
folders.sort()
|
||||
return folders
|
||||
|
||||
@classmethod
|
||||
def get_folder_content(cls, name):
|
||||
"""Return (folders, files) for the given folder in the root dir."""
|
||||
folders = set()
|
||||
files = set()
|
||||
for path in cls.LAYOUT:
|
||||
if not path.startswith(name + '/'):
|
||||
continue
|
||||
parts = path.split('/')
|
||||
if len(parts) == 2:
|
||||
files.add(parts[1])
|
||||
else:
|
||||
folders.add(parts[1])
|
||||
folders = list(folders)
|
||||
folders.sort()
|
||||
files = list(files)
|
||||
files.sort()
|
||||
return (folders, files)
|
||||
|
||||
def __init__(self, factory):
|
||||
self._factory = factory
|
||||
self.base = factory.getbasetemp()
|
||||
self.layout = factory.mktemp('layout')
|
||||
self._mklayout()
|
||||
|
||||
def _mklayout(self):
|
||||
for filename in self.LAYOUT:
|
||||
self.layout.ensure(filename)
|
||||
|
||||
def file_url(self):
|
||||
"""Return a file:// link to the directory."""
|
||||
return jinja.file_url(str(self.layout))
|
||||
|
||||
def path(self, *parts):
|
||||
"""Return the path to the given file inside the layout folder."""
|
||||
return os.path.normpath(str(self.layout.join(*parts)))
|
||||
|
||||
def base_path(self):
|
||||
"""Return the path of the base temporary folder."""
|
||||
return os.path.normpath(str(self.base))
|
||||
|
||||
|
||||
Parsed = collections.namedtuple('Parsed', 'path, parent, folders, files')
|
||||
Item = collections.namedtuple('Item', 'path, link, text')
|
||||
|
||||
|
||||
def parse(quteproc):
|
||||
"""Parse the dirbrowser content from the given quteproc.
|
||||
|
||||
Args:
|
||||
quteproc: The quteproc fixture.
|
||||
"""
|
||||
html = quteproc.get_content(plain=False)
|
||||
soup = bs4.BeautifulSoup(html, 'html.parser')
|
||||
print(soup.prettify())
|
||||
title_prefix = 'Browse directory: '
|
||||
# Strip off the title prefix to obtain the path of the folder that
|
||||
# we're browsing
|
||||
path = soup.title.string[len(title_prefix):]
|
||||
path = os.path.normpath(path)
|
||||
|
||||
container = soup('div', id='dirbrowserContainer')[0]
|
||||
|
||||
parent_elem = container('ul', class_='parent')
|
||||
if not parent_elem:
|
||||
parent = None
|
||||
else:
|
||||
parent = QUrl(parent_elem[0].li.a['href']).toLocalFile()
|
||||
parent = os.path.normpath(parent)
|
||||
|
||||
folders = []
|
||||
files = []
|
||||
|
||||
for css_class, list_ in [('folders', folders), ('files', files)]:
|
||||
for li in container('ul', class_=css_class)[0]('li'):
|
||||
item_path = QUrl(li.a['href']).toLocalFile()
|
||||
item_path = os.path.normpath(item_path)
|
||||
list_.append(Item(path=item_path, link=li.a['href'],
|
||||
text=str(li.a.string)))
|
||||
|
||||
return Parsed(path=path, parent=parent, folders=folders,
|
||||
files=files)
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def dir_layout(tmpdir_factory):
|
||||
return DirLayout(tmpdir_factory)
|
||||
|
||||
|
||||
def test_parent_folder(dir_layout, quteproc):
|
||||
quteproc.open_url(dir_layout.file_url())
|
||||
page = parse(quteproc)
|
||||
assert page.parent == dir_layout.base_path()
|
||||
|
||||
|
||||
def test_parent_with_slash(dir_layout, quteproc):
|
||||
"""Test the parent link with an URL that has a trailing slash."""
|
||||
quteproc.open_url(dir_layout.file_url() + '/')
|
||||
page = parse(quteproc)
|
||||
assert page.parent == dir_layout.base_path()
|
||||
|
||||
|
||||
def test_parent_in_root_dir(dir_layout, quteproc):
|
||||
# This actually works on windows
|
||||
root_path = os.path.realpath('/')
|
||||
urlstr = QUrl.fromLocalFile(root_path).toString(QUrl.FullyEncoded)
|
||||
quteproc.open_url(urlstr)
|
||||
page = parse(quteproc)
|
||||
assert page.parent is None
|
||||
|
||||
|
||||
def test_enter_folder_smoke(dir_layout, quteproc):
|
||||
quteproc.open_url(dir_layout.file_url())
|
||||
quteproc.send_cmd(':hint all normal')
|
||||
# a is the parent link, s is the first listed folder/file
|
||||
quteproc.send_cmd(':follow-hint s')
|
||||
page = parse(quteproc)
|
||||
assert page.path == dir_layout.path('folder0')
|
||||
|
||||
|
||||
@pytest.mark.parametrize('folder', DirLayout.layout_folders())
|
||||
def test_enter_folder(dir_layout, quteproc, folder):
|
||||
quteproc.open_url(dir_layout.file_url())
|
||||
quteproc.click_element(text=folder)
|
||||
page = parse(quteproc)
|
||||
assert page.path == dir_layout.path(folder)
|
||||
assert page.parent == dir_layout.path()
|
||||
folders, files = DirLayout.get_folder_content(folder)
|
||||
foldernames = [item.text for item in page.folders]
|
||||
assert foldernames == folders
|
||||
filenames = [item.text for item in page.files]
|
||||
assert filenames == files
|
@ -158,3 +158,40 @@ def test_log_line_parse(data, attrs):
|
||||
def test_log_line_no_match():
|
||||
with pytest.raises(testprocess.InvalidLine):
|
||||
quteprocess.LogLine("Hello World!")
|
||||
|
||||
|
||||
class TestClickElement:
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def open_page(self, quteproc):
|
||||
quteproc.open_path('data/click_element.html')
|
||||
quteproc.wait_for_load_finished('data/click_element.html')
|
||||
|
||||
def test_click_element(self, quteproc):
|
||||
quteproc.click_element('Test Element')
|
||||
quteproc.wait_for_js('click_element clicked')
|
||||
|
||||
def test_click_special_chars(self, quteproc):
|
||||
quteproc.click_element('"Don\'t", he shouted')
|
||||
quteproc.wait_for_js('click_element special chars')
|
||||
|
||||
def test_duplicate(self, quteproc):
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
quteproc.click_element('Duplicate')
|
||||
assert 'not unique' in str(excinfo.value)
|
||||
|
||||
def test_nonexistent(self, quteproc):
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
quteproc.click_element('no element exists with this text')
|
||||
assert 'No element' in str(excinfo.value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('string, expected', [
|
||||
('Test', "'Test'"),
|
||||
("Don't", '"Don\'t"'),
|
||||
# This is some serious string escaping madness
|
||||
('"Don\'t", he said',
|
||||
"concat('\"', 'Don', \"'\", 't', '\"', ', he said')"),
|
||||
])
|
||||
def test_xpath_escape(string, expected):
|
||||
assert quteprocess._xpath_escape(string) == expected
|
||||
|
@ -28,6 +28,7 @@ from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtNetwork import QNetworkRequest
|
||||
|
||||
from qutebrowser.browser.network import filescheme
|
||||
from qutebrowser.utils import jinja
|
||||
|
||||
|
||||
@pytest.mark.parametrize('create_file, create_dir, filterfunc, expected', [
|
||||
@ -57,6 +58,8 @@ class TestIsRoot:
|
||||
|
||||
@pytest.mark.windows
|
||||
@pytest.mark.parametrize('directory, is_root', [
|
||||
('C:\\foo\\bar', False),
|
||||
('C:\\foo\\', False),
|
||||
('C:\\foo', False),
|
||||
('C:\\', True)
|
||||
])
|
||||
@ -65,6 +68,8 @@ class TestIsRoot:
|
||||
|
||||
@pytest.mark.posix
|
||||
@pytest.mark.parametrize('directory, is_root', [
|
||||
('/foo/bar', False),
|
||||
('/foo/', False),
|
||||
('/foo', False),
|
||||
('/', True)
|
||||
])
|
||||
@ -72,6 +77,38 @@ class TestIsRoot:
|
||||
assert filescheme.is_root(directory) == is_root
|
||||
|
||||
|
||||
class TestParentDir:
|
||||
|
||||
@pytest.mark.windows
|
||||
@pytest.mark.parametrize('directory, parent', [
|
||||
('C:\\foo\\bar', 'C:\\foo'),
|
||||
('C:\\foo', 'C:\\'),
|
||||
('C:\\foo\\', 'C:\\'),
|
||||
('C:\\', 'C:\\'),
|
||||
])
|
||||
def test_windows(self, directory, parent):
|
||||
assert filescheme.parent_dir(directory) == parent
|
||||
|
||||
@pytest.mark.posix
|
||||
@pytest.mark.parametrize('directory, parent', [
|
||||
('/home/foo', '/home'),
|
||||
('/home', '/'),
|
||||
('/home/', '/'),
|
||||
('/', '/'),
|
||||
])
|
||||
def test_posix(self, directory, parent):
|
||||
assert filescheme.parent_dir(directory) == parent
|
||||
|
||||
|
||||
def _file_url(path):
|
||||
"""Return a file:// url (as string) for the given LocalPath.
|
||||
|
||||
Arguments:
|
||||
path: The filepath as LocalPath (as handled by py.path)
|
||||
"""
|
||||
return jinja.file_url(str(path))
|
||||
|
||||
|
||||
class TestDirbrowserHtml:
|
||||
|
||||
Parsed = collections.namedtuple('Parsed', 'parent, folders, files')
|
||||
@ -87,7 +124,7 @@ class TestDirbrowserHtml:
|
||||
container = soup('div', id='dirbrowserContainer')[0]
|
||||
|
||||
parent_elem = container('ul', class_='parent')
|
||||
if len(parent_elem) == 0:
|
||||
if not parent_elem:
|
||||
parent = None
|
||||
else:
|
||||
parent = parent_elem[0].li.a.string
|
||||
@ -145,8 +182,8 @@ class TestDirbrowserHtml:
|
||||
parsed = parser(str(tmpdir))
|
||||
assert parsed.parent
|
||||
assert not parsed.folders
|
||||
foo_item = self.Item('file://' + str(foo_file), foo_file.relto(tmpdir))
|
||||
bar_item = self.Item('file://' + str(bar_file), bar_file.relto(tmpdir))
|
||||
foo_item = self.Item(_file_url(foo_file), foo_file.relto(tmpdir))
|
||||
bar_item = self.Item(_file_url(bar_file), bar_file.relto(tmpdir))
|
||||
assert parsed.files == [bar_item, foo_item]
|
||||
|
||||
def test_html_special_chars(self, tmpdir, parser):
|
||||
@ -154,8 +191,7 @@ class TestDirbrowserHtml:
|
||||
special_file.ensure()
|
||||
|
||||
parsed = parser(str(tmpdir))
|
||||
item = self.Item('file://' + str(special_file),
|
||||
special_file.relto(tmpdir))
|
||||
item = self.Item(_file_url(special_file), special_file.relto(tmpdir))
|
||||
assert parsed.files == [item]
|
||||
|
||||
def test_dirs(self, tmpdir, parser):
|
||||
@ -167,8 +203,8 @@ class TestDirbrowserHtml:
|
||||
parsed = parser(str(tmpdir))
|
||||
assert parsed.parent
|
||||
assert not parsed.files
|
||||
foo_item = self.Item('file://' + str(foo_dir), foo_dir.relto(tmpdir))
|
||||
bar_item = self.Item('file://' + str(bar_dir), bar_dir.relto(tmpdir))
|
||||
foo_item = self.Item(_file_url(foo_dir), foo_dir.relto(tmpdir))
|
||||
bar_item = self.Item(_file_url(bar_dir), bar_dir.relto(tmpdir))
|
||||
assert parsed.folders == [bar_item, foo_item]
|
||||
|
||||
def test_mixed(self, tmpdir, parser):
|
||||
@ -178,8 +214,8 @@ class TestDirbrowserHtml:
|
||||
bar_dir.ensure(dir=True)
|
||||
|
||||
parsed = parser(str(tmpdir))
|
||||
foo_item = self.Item('file://' + str(foo_file), foo_file.relto(tmpdir))
|
||||
bar_item = self.Item('file://' + str(bar_dir), bar_dir.relto(tmpdir))
|
||||
foo_item = self.Item(_file_url(foo_file), foo_file.relto(tmpdir))
|
||||
bar_item = self.Item(_file_url(bar_dir), bar_dir.relto(tmpdir))
|
||||
assert parsed.parent
|
||||
assert parsed.files == [foo_item]
|
||||
assert parsed.folders == [bar_item]
|
||||
|
Loading…
Reference in New Issue
Block a user