Split WebElementWrapper into abstract/webkit parts
This commit is contained in:
parent
743d2dc327
commit
dfbadaf7c2
qutebrowser/browser
tests/unit/browser/webkit
@ -40,7 +40,7 @@ import pygments.formatters
|
||||
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
|
||||
from qutebrowser.config import config, configexc
|
||||
from qutebrowser.browser import urlmarks, browsertab, inspector, navigate
|
||||
from qutebrowser.browser.webkit import webelem, downloads, mhtml
|
||||
from qutebrowser.browser.webkit import webkitelem, downloads, mhtml
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
|
||||
objreg, utils, typing, javascript)
|
||||
@ -1422,8 +1422,8 @@ class CommandDispatcher:
|
||||
tab = self._current_widget()
|
||||
page = tab._widget.page() # pylint: disable=protected-access
|
||||
try:
|
||||
elem = webelem.focus_elem(page.currentFrame())
|
||||
except webelem.IsNullError:
|
||||
elem = webkitelem.focus_elem(page.currentFrame())
|
||||
except webkitelem.IsNullError:
|
||||
raise cmdexc.CommandError("No element focused!")
|
||||
if not elem.is_editable(strict=True):
|
||||
raise cmdexc.CommandError("Focused element is not editable!")
|
||||
@ -1444,7 +1444,7 @@ class CommandDispatcher:
|
||||
"""
|
||||
try:
|
||||
elem.set_text(text, use_js=True)
|
||||
except webelem.IsNullError:
|
||||
except webkitelem.IsNullError:
|
||||
raise cmdexc.CommandError("Element vanished while editing!")
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher',
|
||||
@ -1456,8 +1456,8 @@ class CommandDispatcher:
|
||||
tab = self._current_widget()
|
||||
page = tab._widget.page() # pylint: disable=protected-access
|
||||
try:
|
||||
elem = webelem.focus_elem(page.currentFrame())
|
||||
except webelem.IsNullError:
|
||||
elem = webkitelem.focus_elem(page.currentFrame())
|
||||
except webkitelem.IsNullError:
|
||||
raise cmdexc.CommandError("No element focused!")
|
||||
if not elem.is_editable(strict=True):
|
||||
raise cmdexc.CommandError("Focused element is not editable!")
|
||||
|
@ -28,12 +28,11 @@ from string import ascii_lowercase
|
||||
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
|
||||
QTimer)
|
||||
from PyQt5.QtGui import QMouseEvent
|
||||
from PyQt5.QtWebKit import QWebElement
|
||||
from PyQt5.QtWebKitWidgets import QWebPage
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.keyinput import modeman, modeparsers
|
||||
from qutebrowser.browser.webkit import webelem
|
||||
from qutebrowser.browser import webelem
|
||||
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
|
||||
from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils
|
||||
|
||||
@ -374,7 +373,7 @@ class HintManager(QObject):
|
||||
for elem in self._context.all_elems:
|
||||
try:
|
||||
elem.label.remove_from_document()
|
||||
except webelem.IsNullError:
|
||||
except webelem.Error:
|
||||
pass
|
||||
text = self._get_text()
|
||||
message_bridge = objreg.get('message-bridge', scope='window',
|
||||
@ -516,7 +515,7 @@ class HintManager(QObject):
|
||||
|
||||
def _is_hidden(self, elem):
|
||||
"""Check if the element is hidden via display=none."""
|
||||
display = elem.style_property('display', QWebElement.InlineStyle)
|
||||
display = elem.style_property('display', strategy='inline')
|
||||
return display == 'none'
|
||||
|
||||
def _show_elem(self, elem):
|
||||
@ -767,7 +766,7 @@ class HintManager(QObject):
|
||||
else:
|
||||
# element doesn't match anymore -> hide it
|
||||
self._hide_elem(elem.label)
|
||||
except webelem.IsNullError:
|
||||
except webelem.Error:
|
||||
pass
|
||||
|
||||
def _filter_number_hints(self):
|
||||
@ -782,7 +781,7 @@ class HintManager(QObject):
|
||||
try:
|
||||
if not self._is_hidden(e.label):
|
||||
elems.append(e)
|
||||
except webelem.IsNullError:
|
||||
except webelem.Error:
|
||||
pass
|
||||
if not elems:
|
||||
# Whoops, filtered all hints
|
||||
@ -813,7 +812,7 @@ class HintManager(QObject):
|
||||
try:
|
||||
if not self._is_hidden(elem.label):
|
||||
visible[string] = elem
|
||||
except webelem.IsNullError:
|
||||
except webelem.Error:
|
||||
pass
|
||||
if not visible:
|
||||
# Whoops, filtered all hints
|
||||
@ -844,7 +843,7 @@ class HintManager(QObject):
|
||||
else:
|
||||
# element doesn't match anymore -> hide it
|
||||
self._hide_elem(elem.label)
|
||||
except webelem.IsNullError:
|
||||
except webelem.Error:
|
||||
pass
|
||||
|
||||
if config.get('hints', 'mode') == 'number':
|
||||
@ -961,7 +960,7 @@ class HintManager(QObject):
|
||||
e.label.remove_from_document()
|
||||
continue
|
||||
self._set_style_position(e.elem, e.label)
|
||||
except webelem.IsNullError:
|
||||
except webelem.Error:
|
||||
pass
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
|
@ -21,7 +21,7 @@
|
||||
|
||||
import posixpath
|
||||
|
||||
from qutebrowser.browser.webkit import webelem
|
||||
from qutebrowser.browser import webelem
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import (usertypes, objreg, urlutils, log, message,
|
||||
qtutils)
|
||||
|
370
qutebrowser/browser/webelem.py
Normal file
370
qutebrowser/browser/webelem.py
Normal file
@ -0,0 +1,370 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
"""Generic web element related code.
|
||||
|
||||
Module attributes:
|
||||
Group: Enum for different kinds of groups.
|
||||
SELECTORS: CSS selectors for different groups of elements.
|
||||
FILTERS: A dictionary of filter functions for the modes.
|
||||
The filter for "links" filters javascript:-links and a-tags
|
||||
without "href".
|
||||
"""
|
||||
|
||||
import collections.abc
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import log, usertypes, utils, qtutils
|
||||
|
||||
|
||||
Group = usertypes.enum('Group', ['all', 'links', 'images', 'url', 'prevnext',
|
||||
'focus', 'inputs'])
|
||||
|
||||
|
||||
SELECTORS = {
|
||||
Group.all: ('a, area, textarea, select, input:not([type=hidden]), button, '
|
||||
'frame, iframe, link, [onclick], [onmousedown], [role=link], '
|
||||
'[role=option], [role=button], img'),
|
||||
Group.links: 'a, area, link, [role=link]',
|
||||
Group.images: 'img',
|
||||
Group.url: '[src], [href]',
|
||||
Group.prevnext: 'a, area, button, link, [role=button]',
|
||||
Group.focus: '*:focus',
|
||||
Group.inputs: ('input[type=text], input[type=email], input[type=url], '
|
||||
'input[type=tel], input[type=number], '
|
||||
'input[type=password], input[type=search], '
|
||||
'input:not([type]), textarea'),
|
||||
}
|
||||
|
||||
|
||||
def filter_links(elem):
|
||||
return 'href' in elem and QUrl(elem['href']).scheme() != 'javascript'
|
||||
|
||||
|
||||
FILTERS = {
|
||||
Group.links: filter_links,
|
||||
Group.prevnext: filter_links,
|
||||
}
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
|
||||
"""Base class for WebElement errors."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AbstractWebElement(collections.abc.MutableMapping):
|
||||
|
||||
"""A wrapper around QtWebKit/QtWebEngine web element."""
|
||||
|
||||
def __eq__(self, other):
|
||||
raise NotImplementedError
|
||||
|
||||
def __str__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def __getitem__(self, key):
|
||||
raise NotImplementedError
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
raise NotImplementedError
|
||||
|
||||
def __delitem__(self, key):
|
||||
raise NotImplementedError
|
||||
|
||||
def __contains__(self, key):
|
||||
raise NotImplementedError
|
||||
|
||||
def __iter__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def __len__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def __repr__(self):
|
||||
try:
|
||||
html = self.debug_text()
|
||||
except Error:
|
||||
html = None
|
||||
return utils.get_repr(self, html=html)
|
||||
|
||||
def frame(self):
|
||||
"""Get the main frame of this element."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
raise NotImplementedError
|
||||
|
||||
def geometry(self):
|
||||
"""Get the geometry for this element."""
|
||||
raise NotImplementedError
|
||||
|
||||
def document_element(self):
|
||||
"""Get the document element of this element."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
raise NotImplementedError
|
||||
|
||||
def create_inside(self, tagname):
|
||||
"""Append the given element inside the current one."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
raise NotImplementedError
|
||||
|
||||
def find_first(self, selector):
|
||||
"""Find the first child based on the given CSS selector."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
raise NotImplementedError
|
||||
|
||||
def style_property(self, name, *, strategy):
|
||||
"""Get the element style resolved with the given strategy."""
|
||||
raise NotImplementedError
|
||||
|
||||
def classes(self):
|
||||
"""Get a list of classes assigned to this element."""
|
||||
raise NotImplementedError
|
||||
|
||||
def tag_name(self):
|
||||
"""Get the tag name of this element."""
|
||||
raise NotImplementedError
|
||||
|
||||
def outer_xml(self):
|
||||
"""Get the full HTML representation of this element."""
|
||||
raise NotImplementedError
|
||||
|
||||
def text(self, *, use_js=False):
|
||||
"""Get the plain text content for this element.
|
||||
|
||||
Args:
|
||||
use_js: Whether to use javascript if the element isn't
|
||||
content-editable.
|
||||
"""
|
||||
# FIXME:qtwebengine what to do about use_js with WebEngine?
|
||||
raise NotImplementedError
|
||||
|
||||
def set_text(self, text, *, use_js=False):
|
||||
"""Set the given plain text.
|
||||
|
||||
Args:
|
||||
use_js: Whether to use javascript if the element isn't
|
||||
content-editable.
|
||||
"""
|
||||
# FIXME:qtwebengine what to do about use_js with WebEngine?
|
||||
raise NotImplementedError
|
||||
|
||||
def set_inner_xml(self, xml):
|
||||
"""Set the given inner XML."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
raise NotImplementedError
|
||||
|
||||
def remove_from_document(self):
|
||||
"""Remove the node from the document."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
raise NotImplementedError
|
||||
|
||||
def set_style_property(self, name, value):
|
||||
"""Set the element style."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
raise NotImplementedError
|
||||
|
||||
def run_js_async(self, code, callback=None):
|
||||
"""Run the given JS snippet async on the element."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
raise NotImplementedError
|
||||
|
||||
def parent(self):
|
||||
"""Get the parent element of this element."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
raise NotImplementedError
|
||||
|
||||
def rect_on_view(self, *, elem_geometry=None, adjust_zoom=True,
|
||||
no_js=False):
|
||||
"""Get the geometry of the element relative to the webview.
|
||||
|
||||
Uses the getClientRects() JavaScript method to obtain the collection of
|
||||
rectangles containing the element and returns the first rectangle which
|
||||
is large enough (larger than 1px times 1px). If all rectangles returned
|
||||
by getClientRects() are too small, falls back to elem.rect_on_view().
|
||||
|
||||
Skipping of small rectangles is due to <a> elements containing other
|
||||
elements with "display:block" style, see
|
||||
https://github.com/The-Compiler/qutebrowser/issues/1298
|
||||
|
||||
Args:
|
||||
elem_geometry: The geometry of the element, or None.
|
||||
Calling QWebElement::geometry is rather expensive so
|
||||
we want to avoid doing it twice.
|
||||
adjust_zoom: Whether to adjust the element position based on the
|
||||
current zoom level.
|
||||
no_js: Fall back to the Python implementation
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def is_visible(self, mainframe):
|
||||
"""Check if the given element is visible in the given frame."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
raise NotImplementedError
|
||||
|
||||
def is_writable(self):
|
||||
"""Check whether an element is writable."""
|
||||
return not ('disabled' in self or 'readonly' in self)
|
||||
|
||||
def is_content_editable(self):
|
||||
"""Check if an element has a contenteditable attribute.
|
||||
|
||||
Args:
|
||||
elem: The QWebElement to check.
|
||||
|
||||
Return:
|
||||
True if the element has a contenteditable attribute,
|
||||
False otherwise.
|
||||
"""
|
||||
try:
|
||||
return self['contenteditable'].lower() not in ['false', 'inherit']
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def _is_editable_object(self):
|
||||
"""Check if an object-element is editable."""
|
||||
if 'type' not in self:
|
||||
log.webview.debug("<object> without type clicked...")
|
||||
return False
|
||||
objtype = self['type'].lower()
|
||||
if objtype.startswith('application/') or 'classid' in self:
|
||||
# Let's hope flash/java stuff has an application/* mimetype OR
|
||||
# at least a classid attribute. Oh, and let's hope images/...
|
||||
# DON'T have a classid attribute. HTML sucks.
|
||||
log.webview.debug("<object type='{}'> clicked.".format(objtype))
|
||||
return config.get('input', 'insert-mode-on-plugins')
|
||||
else:
|
||||
# Image/Audio/...
|
||||
return False
|
||||
|
||||
def _is_editable_input(self):
|
||||
"""Check if an input-element is editable.
|
||||
|
||||
Return:
|
||||
True if the element is editable, False otherwise.
|
||||
"""
|
||||
try:
|
||||
objtype = self['type'].lower()
|
||||
except KeyError:
|
||||
return self.is_writable()
|
||||
else:
|
||||
if objtype in ['text', 'email', 'url', 'tel', 'number', 'password',
|
||||
'search']:
|
||||
return self.is_writable()
|
||||
else:
|
||||
return False
|
||||
|
||||
def _is_editable_div(self):
|
||||
"""Check if a div-element is editable.
|
||||
|
||||
Return:
|
||||
True if the element is editable, False otherwise.
|
||||
"""
|
||||
# Beginnings of div-classes which are actually some kind of editor.
|
||||
div_classes = ('CodeMirror', # Javascript editor over a textarea
|
||||
'kix-', # Google Docs editor
|
||||
'ace_') # http://ace.c9.io/
|
||||
for klass in self.classes():
|
||||
if any([klass.startswith(e) for e in div_classes]):
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_editable(self, strict=False):
|
||||
"""Check whether we should switch to insert mode for this element.
|
||||
|
||||
Args:
|
||||
strict: Whether to do stricter checking so only fields where we can
|
||||
get the value match, for use with the :editor command.
|
||||
|
||||
Return:
|
||||
True if we should switch to insert mode, False otherwise.
|
||||
"""
|
||||
roles = ('combobox', 'textbox')
|
||||
log.misc.debug("Checking if element is editable: {}".format(
|
||||
repr(self)))
|
||||
tag = self.tag_name().lower()
|
||||
if self.is_content_editable() and self.is_writable():
|
||||
return True
|
||||
elif self.get('role', None) in roles and self.is_writable():
|
||||
return True
|
||||
elif tag == 'input':
|
||||
return self._is_editable_input()
|
||||
elif tag == 'textarea':
|
||||
return self.is_writable()
|
||||
elif tag in ['embed', 'applet']:
|
||||
# Flash/Java/...
|
||||
return config.get('input', 'insert-mode-on-plugins') and not strict
|
||||
elif tag == 'object':
|
||||
return self._is_editable_object() and not strict
|
||||
elif tag == 'div':
|
||||
return self._is_editable_div() and not strict
|
||||
else:
|
||||
return False
|
||||
|
||||
def is_text_input(self):
|
||||
"""Check if this element is some kind of text box."""
|
||||
roles = ('combobox', 'textbox')
|
||||
tag = self.tag_name().lower()
|
||||
return self.get('role', None) in roles or tag in ['input', 'textarea']
|
||||
|
||||
def remove_blank_target(self):
|
||||
"""Remove target from link."""
|
||||
elem = self
|
||||
for _ in range(5):
|
||||
if elem is None:
|
||||
break
|
||||
tag = elem.tag_name().lower()
|
||||
if tag == 'a' or tag == 'area':
|
||||
if elem.get('target', None) == '_blank':
|
||||
elem['target'] = '_top'
|
||||
break
|
||||
elem = elem.parent()
|
||||
|
||||
def debug_text(self):
|
||||
"""Get a text based on an element suitable for debug output."""
|
||||
return utils.compact_text(self.outer_xml(), 500)
|
||||
|
||||
def resolve_url(self, baseurl):
|
||||
"""Resolve the URL in the element's src/href attribute.
|
||||
|
||||
Args:
|
||||
baseurl: The URL to base relative URLs on as QUrl.
|
||||
|
||||
Return:
|
||||
A QUrl with the absolute URL, or None.
|
||||
"""
|
||||
if baseurl.isRelative():
|
||||
raise ValueError("Need an absolute base URL!")
|
||||
|
||||
for attr in ['href', 'src']:
|
||||
if attr in self:
|
||||
text = self[attr].strip()
|
||||
break
|
||||
else:
|
||||
return None
|
||||
|
||||
url = QUrl(text)
|
||||
if not url.isValid():
|
||||
return None
|
||||
if url.isRelative():
|
||||
url = baseurl.resolved(url)
|
||||
qtutils.ensure_valid(url)
|
||||
return url
|
@ -34,7 +34,7 @@ import email.message
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
from qutebrowser.browser.webkit import webelem, downloads
|
||||
from qutebrowser.browser.webkit import webkitelem, downloads
|
||||
from qutebrowser.utils import log, objreg, message, usertypes, utils, urlutils
|
||||
|
||||
try:
|
||||
@ -271,7 +271,7 @@ class _Downloader:
|
||||
elements = web_frame.findAllElements('link, script, img')
|
||||
|
||||
for element in elements:
|
||||
element = webelem.WebElementWrapper(element)
|
||||
element = webkitelem.WebKitElement(element)
|
||||
# Websites are free to set whatever rel=... attribute they want.
|
||||
# We just care about stylesheets and icons.
|
||||
if not _check_rel(element):
|
||||
@ -288,7 +288,7 @@ class _Downloader:
|
||||
|
||||
styles = web_frame.findAllElements('style')
|
||||
for style in styles:
|
||||
style = webelem.WebElementWrapper(style)
|
||||
style = webkitelem.WebKitElement(style)
|
||||
# The Mozilla Developer Network says:
|
||||
# type: This attribute defines the styling language as a MIME type
|
||||
# (charset should not be specified). This attribute is optional and
|
||||
@ -301,7 +301,7 @@ class _Downloader:
|
||||
|
||||
# Search for references in inline styles
|
||||
for element in web_frame.findAllElements('[style]'):
|
||||
element = webelem.WebElementWrapper(element)
|
||||
element = webkitelem.WebKitElement(element)
|
||||
style = element['style']
|
||||
for element_url in _get_css_imports(style, inline=True):
|
||||
self._fetch_url(web_url.resolved(QUrl(element_url)))
|
||||
|
@ -17,65 +17,26 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Utilities related to QWebElements.
|
||||
"""QtWebKit specific part of the web element API."""
|
||||
|
||||
Module attributes:
|
||||
Group: Enum for different kinds of groups.
|
||||
SELECTORS: CSS selectors for different groups of elements.
|
||||
FILTERS: A dictionary of filter functions for the modes.
|
||||
The filter for "links" filters javascript:-links and a-tags
|
||||
without "href".
|
||||
"""
|
||||
|
||||
import collections.abc
|
||||
|
||||
from PyQt5.QtCore import QRect, QUrl
|
||||
from PyQt5.QtCore import QRect
|
||||
from PyQt5.QtWebKit import QWebElement
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import log, usertypes, utils, javascript, qtutils
|
||||
from qutebrowser.utils import log, utils, javascript
|
||||
from qutebrowser.browser import webelem
|
||||
|
||||
|
||||
Group = usertypes.enum('Group', ['all', 'links', 'images', 'url', 'prevnext',
|
||||
'focus', 'inputs'])
|
||||
class IsNullError(webelem.Error):
|
||||
|
||||
|
||||
SELECTORS = {
|
||||
Group.all: ('a, area, textarea, select, input:not([type=hidden]), button, '
|
||||
'frame, iframe, link, [onclick], [onmousedown], [role=link], '
|
||||
'[role=option], [role=button], img'),
|
||||
Group.links: 'a, area, link, [role=link]',
|
||||
Group.images: 'img',
|
||||
Group.url: '[src], [href]',
|
||||
Group.prevnext: 'a, area, button, link, [role=button]',
|
||||
Group.focus: '*:focus',
|
||||
Group.inputs: ('input[type=text], input[type=email], input[type=url], '
|
||||
'input[type=tel], input[type=number], '
|
||||
'input[type=password], input[type=search], '
|
||||
'input:not([type]), textarea'),
|
||||
}
|
||||
|
||||
|
||||
def filter_links(elem):
|
||||
return 'href' in elem and QUrl(elem['href']).scheme() != 'javascript'
|
||||
|
||||
|
||||
FILTERS = {
|
||||
Group.links: filter_links,
|
||||
Group.prevnext: filter_links,
|
||||
}
|
||||
|
||||
|
||||
class IsNullError(Exception):
|
||||
|
||||
"""Gets raised by WebElementWrapper if an element is null."""
|
||||
"""Gets raised by WebKitElement if an element is null."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class WebElementWrapper(collections.abc.MutableMapping):
|
||||
class WebKitElement(webelem.AbstractWebElement):
|
||||
|
||||
"""A wrapper around QWebElement to make it more intelligent."""
|
||||
"""A wrapper around a QWebElement."""
|
||||
|
||||
def __init__(self, elem):
|
||||
if isinstance(elem, self.__class__):
|
||||
@ -85,7 +46,7 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
self._elem = elem
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, WebElementWrapper):
|
||||
if not isinstance(other, WebKitElement):
|
||||
return NotImplemented
|
||||
return self._elem == other._elem # pylint: disable=protected-access
|
||||
|
||||
@ -93,13 +54,6 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
self._check_vanished()
|
||||
return self._elem.toPlainText()
|
||||
|
||||
def __repr__(self):
|
||||
try:
|
||||
html = self.debug_text()
|
||||
except IsNullError:
|
||||
html = None
|
||||
return utils.get_repr(self, html=html)
|
||||
|
||||
def __getitem__(self, key):
|
||||
self._check_vanished()
|
||||
if key not in self:
|
||||
@ -134,24 +88,19 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
raise IsNullError('Element {} vanished!'.format(self._elem))
|
||||
|
||||
def frame(self):
|
||||
"""Get the main frame of this element."""
|
||||
# FIXME:qtwebengine how to get rid of this?
|
||||
self._check_vanished()
|
||||
return self._elem.webFrame()
|
||||
|
||||
def geometry(self):
|
||||
"""Get the geometry for this element."""
|
||||
self._check_vanished()
|
||||
return self._elem.geometry()
|
||||
|
||||
def document_element(self):
|
||||
"""Get the document element of this element."""
|
||||
self._check_vanished()
|
||||
elem = self._elem.webFrame().documentElement()
|
||||
return WebElementWrapper(elem)
|
||||
return WebKitElement(elem)
|
||||
|
||||
def create_inside(self, tagname):
|
||||
"""Append the given element inside the current one."""
|
||||
# It seems impossible to create an empty QWebElement for which isNull()
|
||||
# is false so we can work with it.
|
||||
# As a workaround, we use appendInside() with markup as argument, and
|
||||
@ -159,28 +108,40 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
# See: http://stackoverflow.com/q/7364852/2085149
|
||||
self._check_vanished()
|
||||
self._elem.appendInside('<{}></{}>'.format(tagname, tagname))
|
||||
return WebElementWrapper(self._elem.lastChild())
|
||||
return WebKitElement(self._elem.lastChild())
|
||||
|
||||
def find_first(self, selector):
|
||||
"""Find the first child based on the given CSS selector."""
|
||||
self._check_vanished()
|
||||
elem = self._elem.findFirst(selector)
|
||||
if elem.isNull():
|
||||
return None
|
||||
return WebElementWrapper(elem)
|
||||
return WebKitElement(elem)
|
||||
|
||||
def style_property(self, name, strategy):
|
||||
"""Get the element style resolved with the given strategy."""
|
||||
def style_property(self, name, *, strategy):
|
||||
self._check_vanished()
|
||||
return self._elem.styleProperty(name, strategy)
|
||||
strategies = {
|
||||
# FIXME:qtwebengine which ones do we actually need?
|
||||
'inline': QWebElement.InlineStyle,
|
||||
'computed': QWebElement.ComputedStyle,
|
||||
}
|
||||
qt_strategy = strategies[strategy]
|
||||
return self._elem.styleProperty(name, qt_strategy)
|
||||
|
||||
def classes(self):
|
||||
self._check_vanished()
|
||||
return self._elem.classes()
|
||||
|
||||
def tag_name(self):
|
||||
"""Get the tag name for the current element."""
|
||||
self._check_vanished()
|
||||
return self._elem.tagName()
|
||||
|
||||
def outer_xml(self):
|
||||
"""Get the full HTML representation of this element."""
|
||||
self._check_vanished()
|
||||
return self._elem.toOuterXml()
|
||||
|
||||
def text(self, *, use_js=False):
|
||||
"""Get the plain text content for this element.
|
||||
|
||||
Args:
|
||||
use_js: Whether to use javascript if the element isn't
|
||||
content-editable.
|
||||
"""
|
||||
self._check_vanished()
|
||||
if self.is_content_editable() or not use_js:
|
||||
return self._elem.toPlainText()
|
||||
@ -188,12 +149,6 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
return self._elem.evaluateJavaScript('this.value')
|
||||
|
||||
def set_text(self, text, *, use_js=False):
|
||||
"""Set the given plain text.
|
||||
|
||||
Args:
|
||||
use_js: Whether to use javascript if the element isn't
|
||||
content-editable.
|
||||
"""
|
||||
self._check_vanished()
|
||||
if self.is_content_editable() or not use_js:
|
||||
log.misc.debug("Filling element {} via set_text.".format(
|
||||
@ -206,158 +161,17 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
self._elem.evaluateJavaScript("this.value='{}'".format(text))
|
||||
|
||||
def set_inner_xml(self, xml):
|
||||
"""Set the given inner XML."""
|
||||
self._check_vanished()
|
||||
self._elem.setInnerXml(xml)
|
||||
|
||||
def remove_from_document(self):
|
||||
"""Remove the node from the document."""
|
||||
self._check_vanished()
|
||||
self._elem.removeFromDocument()
|
||||
|
||||
def set_style_property(self, name, value):
|
||||
"""Set the element style."""
|
||||
self._check_vanished()
|
||||
return self._elem.setStyleProperty(name, value)
|
||||
|
||||
def is_writable(self):
|
||||
"""Check whether an element is writable."""
|
||||
self._check_vanished()
|
||||
return not ('disabled' in self or 'readonly' in self)
|
||||
|
||||
def is_content_editable(self):
|
||||
"""Check if an element has a contenteditable attribute.
|
||||
|
||||
Args:
|
||||
elem: The QWebElement to check.
|
||||
|
||||
Return:
|
||||
True if the element has a contenteditable attribute,
|
||||
False otherwise.
|
||||
"""
|
||||
self._check_vanished()
|
||||
try:
|
||||
return self['contenteditable'].lower() not in ['false', 'inherit']
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def _is_editable_object(self):
|
||||
"""Check if an object-element is editable."""
|
||||
if 'type' not in self:
|
||||
log.webview.debug("<object> without type clicked...")
|
||||
return False
|
||||
objtype = self['type'].lower()
|
||||
if objtype.startswith('application/') or 'classid' in self:
|
||||
# Let's hope flash/java stuff has an application/* mimetype OR
|
||||
# at least a classid attribute. Oh, and let's hope images/...
|
||||
# DON'T have a classid attribute. HTML sucks.
|
||||
log.webview.debug("<object type='{}'> clicked.".format(objtype))
|
||||
return config.get('input', 'insert-mode-on-plugins')
|
||||
else:
|
||||
# Image/Audio/...
|
||||
return False
|
||||
|
||||
def _is_editable_input(self):
|
||||
"""Check if an input-element is editable.
|
||||
|
||||
Return:
|
||||
True if the element is editable, False otherwise.
|
||||
"""
|
||||
try:
|
||||
objtype = self['type'].lower()
|
||||
except KeyError:
|
||||
return self.is_writable()
|
||||
else:
|
||||
if objtype in ['text', 'email', 'url', 'tel', 'number', 'password',
|
||||
'search']:
|
||||
return self.is_writable()
|
||||
else:
|
||||
return False
|
||||
|
||||
def _is_editable_div(self):
|
||||
"""Check if a div-element is editable.
|
||||
|
||||
Return:
|
||||
True if the element is editable, False otherwise.
|
||||
"""
|
||||
# Beginnings of div-classes which are actually some kind of editor.
|
||||
div_classes = ('CodeMirror', # Javascript editor over a textarea
|
||||
'kix-', # Google Docs editor
|
||||
'ace_') # http://ace.c9.io/
|
||||
for klass in self._elem.classes():
|
||||
if any([klass.startswith(e) for e in div_classes]):
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_editable(self, strict=False):
|
||||
"""Check whether we should switch to insert mode for this element.
|
||||
|
||||
Args:
|
||||
strict: Whether to do stricter checking so only fields where we can
|
||||
get the value match, for use with the :editor command.
|
||||
|
||||
Return:
|
||||
True if we should switch to insert mode, False otherwise.
|
||||
"""
|
||||
self._check_vanished()
|
||||
roles = ('combobox', 'textbox')
|
||||
log.misc.debug("Checking if element is editable: {}".format(
|
||||
repr(self)))
|
||||
tag = self._elem.tagName().lower()
|
||||
if self.is_content_editable() and self.is_writable():
|
||||
return True
|
||||
elif self.get('role', None) in roles and self.is_writable():
|
||||
return True
|
||||
elif tag == 'input':
|
||||
return self._is_editable_input()
|
||||
elif tag == 'textarea':
|
||||
return self.is_writable()
|
||||
elif tag in ['embed', 'applet']:
|
||||
# Flash/Java/...
|
||||
return config.get('input', 'insert-mode-on-plugins') and not strict
|
||||
elif tag == 'object':
|
||||
return self._is_editable_object() and not strict
|
||||
elif tag == 'div':
|
||||
return self._is_editable_div() and not strict
|
||||
else:
|
||||
return False
|
||||
|
||||
def is_text_input(self):
|
||||
"""Check if this element is some kind of text box."""
|
||||
self._check_vanished()
|
||||
roles = ('combobox', 'textbox')
|
||||
tag = self._elem.tagName().lower()
|
||||
return self.get('role', None) in roles or tag in ['input', 'textarea']
|
||||
|
||||
def remove_blank_target(self):
|
||||
"""Remove target from link."""
|
||||
self._check_vanished()
|
||||
elem = self._elem
|
||||
for _ in range(5):
|
||||
if elem is None:
|
||||
break
|
||||
tag = elem.tagName().lower()
|
||||
if tag == 'a' or tag == 'area':
|
||||
if elem.attribute('target') == '_blank':
|
||||
elem.setAttribute('target', '_top')
|
||||
break
|
||||
elem = elem.parent()
|
||||
|
||||
def debug_text(self):
|
||||
"""Get a text based on an element suitable for debug output."""
|
||||
self._check_vanished()
|
||||
return utils.compact_text(self._elem.toOuterXml(), 500)
|
||||
|
||||
def outer_xml(self):
|
||||
"""Get the full HTML representation of this element."""
|
||||
self._check_vanished()
|
||||
return self._elem.toOuterXml()
|
||||
|
||||
def tag_name(self):
|
||||
"""Get the tag name for the current element."""
|
||||
self._check_vanished()
|
||||
return self._elem.tagName()
|
||||
|
||||
def run_js_async(self, code, callback=None):
|
||||
"""Run the given JS snippet async on the element."""
|
||||
self._check_vanished()
|
||||
@ -365,8 +179,16 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
if callback is not None:
|
||||
callback(result)
|
||||
|
||||
def parent(self):
|
||||
self._check_vanished()
|
||||
elem = self._elem.parent()
|
||||
if elem is None:
|
||||
return None
|
||||
return WebKitElement(elem)
|
||||
|
||||
def _rect_on_view_js(self, adjust_zoom):
|
||||
"""Javascript implementation for rect_on_view."""
|
||||
# FIXME:qtwebengine maybe we can reuse this?
|
||||
rects = self._elem.evaluateJavaScript("this.getClientRects()")
|
||||
if rects is None: # pragma: no cover
|
||||
# Depending on unknown circumstances, this might not work with JS
|
||||
@ -444,6 +266,8 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
current zoom level.
|
||||
no_js: Fall back to the Python implementation
|
||||
"""
|
||||
# FIXME:qtwebengine can we get rid of this with
|
||||
# find_all_elements(only_visible=True)?
|
||||
self._check_vanished()
|
||||
|
||||
# First try getting the element rect via JS, as that's usually more
|
||||
@ -500,33 +324,6 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
visible_in_frame = visible_on_screen
|
||||
return all([visible_on_screen, visible_in_frame])
|
||||
|
||||
def resolve_url(self, baseurl):
|
||||
"""Resolve the URL in the element's src/href attribute.
|
||||
|
||||
Args:
|
||||
baseurl: The URL to base relative URLs on as QUrl.
|
||||
|
||||
Return:
|
||||
A QUrl with the absolute URL, or None.
|
||||
"""
|
||||
if baseurl.isRelative():
|
||||
raise ValueError("Need an absolute base URL!")
|
||||
|
||||
for attr in ['href', 'src']:
|
||||
if attr in self:
|
||||
text = self[attr].strip()
|
||||
break
|
||||
else:
|
||||
return None
|
||||
|
||||
url = QUrl(text)
|
||||
if not url.isValid():
|
||||
return None
|
||||
if url.isRelative():
|
||||
url = baseurl.resolved(url)
|
||||
qtutils.ensure_valid(url)
|
||||
return url
|
||||
|
||||
|
||||
def get_child_frames(startframe):
|
||||
"""Get all children recursively of a given QWebFrame.
|
||||
@ -556,5 +353,5 @@ def focus_elem(frame):
|
||||
Args:
|
||||
frame: The QWebFrame to search in.
|
||||
"""
|
||||
elem = frame.findFirstElement(SELECTORS[Group.focus])
|
||||
return WebElementWrapper(elem)
|
||||
elem = frame.findFirstElement(webelem.SELECTORS[webelem.Group.focus])
|
||||
return WebKitElement(elem)
|
@ -31,7 +31,7 @@ from PyQt5.QtWebKit import QWebSettings
|
||||
from PyQt5.QtPrintSupport import QPrinter
|
||||
|
||||
from qutebrowser.browser import browsertab
|
||||
from qutebrowser.browser.webkit import webview, tabhistory, webelem
|
||||
from qutebrowser.browser.webkit import webview, tabhistory, webkitelem
|
||||
from qutebrowser.utils import qtutils, objreg, usertypes, utils
|
||||
|
||||
|
||||
@ -564,10 +564,10 @@ class WebKitTab(browsertab.AbstractTab):
|
||||
raise browsertab.WebTabError("No frame focused!")
|
||||
|
||||
elems = []
|
||||
frames = webelem.get_child_frames(mainframe)
|
||||
frames = webkitelem.get_child_frames(mainframe)
|
||||
for f in frames:
|
||||
for elem in f.findAllElements(selector):
|
||||
elems.append(webelem.WebElementWrapper(elem))
|
||||
elems.append(webkitelem.WebKitElement(elem))
|
||||
|
||||
if only_visible:
|
||||
elems = [e for e in elems if e.is_visible(mainframe)]
|
||||
|
@ -31,7 +31,7 @@ from qutebrowser.config import config
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.utils import message, log, usertypes, utils, qtutils, objreg
|
||||
from qutebrowser.browser import hints
|
||||
from qutebrowser.browser.webkit import webpage, webelem
|
||||
from qutebrowser.browser.webkit import webpage, webkitelem
|
||||
|
||||
|
||||
class WebView(QWebView):
|
||||
@ -196,13 +196,13 @@ class WebView(QWebView):
|
||||
if hitresult.isNull():
|
||||
# For some reason, the whole hit result can be null sometimes (e.g.
|
||||
# on doodle menu links). If this is the case, we schedule a check
|
||||
# later (in mouseReleaseEvent) which uses webelem.focus_elem.
|
||||
# later (in mouseReleaseEvent) which uses webkitelem.focus_elem.
|
||||
log.mouse.debug("Hitresult is null!")
|
||||
self._check_insertmode = True
|
||||
return
|
||||
try:
|
||||
elem = webelem.WebElementWrapper(hitresult.element())
|
||||
except webelem.IsNullError:
|
||||
elem = webkitelem.WebKitElement(hitresult.element())
|
||||
except webkitelem.IsNullError:
|
||||
# For some reason, the hit result element can be a null element
|
||||
# sometimes (e.g. when clicking the timetable fields on
|
||||
# http://www.sbb.ch/ ). If this is the case, we schedule a check
|
||||
@ -227,8 +227,8 @@ class WebView(QWebView):
|
||||
return
|
||||
self._check_insertmode = False
|
||||
try:
|
||||
elem = webelem.focus_elem(self.page().currentFrame())
|
||||
except (webelem.IsNullError, RuntimeError):
|
||||
elem = webkitelem.focus_elem(self.page().currentFrame())
|
||||
except (webkitelem.IsNullError, RuntimeError):
|
||||
log.mouse.debug("Element/page vanished!")
|
||||
return
|
||||
if elem.is_editable():
|
||||
@ -325,8 +325,8 @@ class WebView(QWebView):
|
||||
return
|
||||
frame = self.page().currentFrame()
|
||||
try:
|
||||
elem = webelem.WebElementWrapper(frame.findFirstElement(':focus'))
|
||||
except webelem.IsNullError:
|
||||
elem = webkitelem.WebKitElement(frame.findFirstElement(':focus'))
|
||||
except webkitelem.IsNullError:
|
||||
log.webview.debug("Focused element is null!")
|
||||
return
|
||||
log.modes.debug("focus element: {}".format(repr(elem)))
|
||||
|
@ -28,13 +28,14 @@ from PyQt5.QtCore import QRect, QPoint, QUrl
|
||||
from PyQt5.QtWebKit import QWebElement
|
||||
import pytest
|
||||
|
||||
from qutebrowser.browser.webkit import webelem
|
||||
from qutebrowser.browser import webelem
|
||||
from qutebrowser.browser.webkit import webkitelem
|
||||
|
||||
|
||||
def get_webelem(geometry=None, frame=None, *, null=False, style=None,
|
||||
attributes=None, tagname=None, classes=None,
|
||||
parent=None, js_rect_return=None, zoom_text_only=False):
|
||||
"""Factory for WebElementWrapper objects based on a mock.
|
||||
"""Factory for WebKitElement objects based on a mock.
|
||||
|
||||
Args:
|
||||
geometry: The geometry of the QWebElement as QRect.
|
||||
@ -117,7 +118,7 @@ def get_webelem(geometry=None, frame=None, *, null=False, style=None,
|
||||
return style_dict[name]
|
||||
|
||||
elem.styleProperty.side_effect = _style_property
|
||||
wrapped = webelem.WebElementWrapper(elem)
|
||||
wrapped = webkitelem.WebKitElement(elem)
|
||||
return wrapped
|
||||
|
||||
|
||||
@ -215,15 +216,14 @@ class TestSelectorsAndFilters:
|
||||
# Make sure setting HTML succeeded and there's a new element
|
||||
assert len(webframe.findAllElements('*')) == 3
|
||||
elems = webframe.findAllElements(webelem.SELECTORS[group])
|
||||
elems = [webelem.WebElementWrapper(e) for e in elems]
|
||||
elems = [webkitelem.WebKitElement(e) for e in elems]
|
||||
filterfunc = webelem.FILTERS.get(group, lambda e: True)
|
||||
elems = [e for e in elems if filterfunc(e)]
|
||||
assert bool(elems) == matching
|
||||
|
||||
class TestWebKitElement:
|
||||
|
||||
class TestWebElementWrapper:
|
||||
|
||||
"""Generic tests for WebElementWrapper.
|
||||
"""Generic tests for WebKitElement.
|
||||
|
||||
Note: For some methods, there's a dedicated test class with more involved
|
||||
tests.
|
||||
@ -235,13 +235,13 @@ class TestWebElementWrapper:
|
||||
|
||||
def test_nullelem(self):
|
||||
"""Test __init__ with a null element."""
|
||||
with pytest.raises(webelem.IsNullError):
|
||||
with pytest.raises(webkitelem.IsNullError):
|
||||
get_webelem(null=True)
|
||||
|
||||
def test_double_wrap(self, elem):
|
||||
"""Test wrapping a WebElementWrapper."""
|
||||
"""Test wrapping a WebKitElement."""
|
||||
with pytest.raises(TypeError) as excinfo:
|
||||
webelem.WebElementWrapper(elem)
|
||||
webkitelem.WebKitElement(elem)
|
||||
assert str(excinfo.value) == "Trying to wrap a wrapper!"
|
||||
|
||||
@pytest.mark.parametrize('code', [
|
||||
@ -257,7 +257,7 @@ class TestWebElementWrapper:
|
||||
lambda e: e.document_element(),
|
||||
lambda e: e.create_inside('span'),
|
||||
lambda e: e.find_first('span'),
|
||||
lambda e: e.style_property('visibility', QWebElement.ComputedStyle),
|
||||
lambda e: e.style_property('visibility', strategy='computed'),
|
||||
lambda e: e.text(),
|
||||
lambda e: e.set_text('foo'),
|
||||
lambda e: e.set_inner_xml(''),
|
||||
@ -285,16 +285,16 @@ class TestWebElementWrapper:
|
||||
"""Make sure methods check if the element is vanished."""
|
||||
elem._elem.isNull.return_value = True
|
||||
elem._elem.tagName.return_value = 'span'
|
||||
with pytest.raises(webelem.IsNullError):
|
||||
with pytest.raises(webkitelem.IsNullError):
|
||||
code(elem)
|
||||
|
||||
def test_str(self, elem):
|
||||
assert str(elem) == 'text'
|
||||
|
||||
@pytest.mark.parametrize('is_null, expected', [
|
||||
(False, "<qutebrowser.browser.webkit.webelem.WebElementWrapper "
|
||||
(False, "<qutebrowser.browser.webkit.webkitelem.WebKitElement "
|
||||
"html='<fakeelem/>'>"),
|
||||
(True, '<qutebrowser.browser.webkit.webelem.WebElementWrapper '
|
||||
(True, '<qutebrowser.browser.webkit.webkitelem.WebKitElement '
|
||||
'html=None>'),
|
||||
])
|
||||
def test_repr(self, elem, is_null, expected):
|
||||
@ -334,7 +334,7 @@ class TestWebElementWrapper:
|
||||
|
||||
def test_eq(self):
|
||||
one = get_webelem()
|
||||
two = webelem.WebElementWrapper(one._elem)
|
||||
two = webkitelem.WebKitElement(one._elem)
|
||||
assert one == two
|
||||
|
||||
def test_eq_other_type(self):
|
||||
@ -422,7 +422,7 @@ class TestWebElementWrapper:
|
||||
mock.assert_called_with(*args)
|
||||
|
||||
def test_style_property(self, elem):
|
||||
assert elem.style_property('foo', QWebElement.ComputedStyle) == 'bar'
|
||||
assert elem.style_property('foo', strategy='computed') == 'bar'
|
||||
|
||||
def test_document_element(self, stubs):
|
||||
doc_elem = get_webelem()
|
||||
@ -430,14 +430,14 @@ class TestWebElementWrapper:
|
||||
elem = get_webelem(frame=frame)
|
||||
|
||||
doc_elem_ret = elem.document_element()
|
||||
assert isinstance(doc_elem_ret, webelem.WebElementWrapper)
|
||||
assert isinstance(doc_elem_ret, webkitelem.WebKitElement)
|
||||
assert doc_elem_ret == doc_elem
|
||||
|
||||
def test_find_first(self, elem):
|
||||
result = get_webelem()
|
||||
elem._elem.findFirst.return_value = result._elem
|
||||
find_result = elem.find_first('')
|
||||
assert isinstance(find_result, webelem.WebElementWrapper)
|
||||
assert isinstance(find_result, webkitelem.WebKitElement)
|
||||
assert find_result == result
|
||||
|
||||
def test_create_inside(self, elem):
|
||||
@ -727,7 +727,7 @@ def test_focus_element(stubs):
|
||||
frame = stubs.FakeWebFrame(QRect(0, 0, 100, 100))
|
||||
elem = get_webelem()
|
||||
frame.focus_elem = elem._elem
|
||||
assert webelem.focus_elem(frame)._elem is elem._elem
|
||||
assert webkitelem.focus_elem(frame)._elem is elem._elem
|
||||
|
||||
|
||||
class TestRectOnView:
|
||||
@ -739,7 +739,7 @@ class TestRectOnView:
|
||||
This is needed for all the tests calling rect_on_view or is_visible.
|
||||
"""
|
||||
config_stub.data = {'ui': {'zoom-text-only': 'true'}}
|
||||
monkeypatch.setattr('qutebrowser.browser.webkit.webelem.config',
|
||||
monkeypatch.setattr('qutebrowser.browser.webkit.webkitelem.config',
|
||||
config_stub)
|
||||
return config_stub
|
||||
|
||||
@ -821,7 +821,7 @@ class TestGetChildFrames:
|
||||
def test_single_frame(self, stubs):
|
||||
"""Test get_child_frames with a single frame without children."""
|
||||
frame = stubs.FakeChildrenFrame()
|
||||
children = webelem.get_child_frames(frame)
|
||||
children = webkitelem.get_child_frames(frame)
|
||||
assert len(children) == 1
|
||||
assert children[0] is frame
|
||||
frame.childFrames.assert_called_once_with()
|
||||
@ -836,7 +836,7 @@ class TestGetChildFrames:
|
||||
child1 = stubs.FakeChildrenFrame()
|
||||
child2 = stubs.FakeChildrenFrame()
|
||||
parent = stubs.FakeChildrenFrame([child1, child2])
|
||||
children = webelem.get_child_frames(parent)
|
||||
children = webkitelem.get_child_frames(parent)
|
||||
assert len(children) == 3
|
||||
assert children[0] is parent
|
||||
assert children[1] is child1
|
||||
@ -858,7 +858,7 @@ class TestGetChildFrames:
|
||||
first = [stubs.FakeChildrenFrame(second[0:2]),
|
||||
stubs.FakeChildrenFrame(second[2:4])]
|
||||
root = stubs.FakeChildrenFrame(first)
|
||||
children = webelem.get_child_frames(root)
|
||||
children = webkitelem.get_child_frames(root)
|
||||
assert len(children) == 7
|
||||
assert children[0] is root
|
||||
for frame in [root] + first + second:
|
||||
@ -873,7 +873,7 @@ class TestIsEditable:
|
||||
def stubbed_config(self, config_stub, monkeypatch):
|
||||
"""Fixture to create a config stub with an input section."""
|
||||
config_stub.data = {'input': {}}
|
||||
monkeypatch.setattr('qutebrowser.browser.webkit.webelem.config',
|
||||
monkeypatch.setattr('qutebrowser.browser.webkit.webkitelem.config',
|
||||
config_stub)
|
||||
return config_stub
|
||||
|
Loading…
Reference in New Issue
Block a user