Split WebElementWrapper into abstract/webkit parts

This commit is contained in:
Florian Bruhin 2016-08-08 12:52:04 +02:00
parent 743d2dc327
commit dfbadaf7c2
9 changed files with 470 additions and 304 deletions

View File

@ -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!")

View File

@ -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)

View File

@ -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)

View 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

View File

@ -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)))

View File

@ -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)

View File

@ -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)]

View File

@ -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)))

View File

@ -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