2014-06-19 09:04:37 +02:00
|
|
|
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
|
|
|
|
2015-01-03 15:51:31 +01:00
|
|
|
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
2014-05-05 15:16:27 +02:00
|
|
|
#
|
|
|
|
# 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/>.
|
|
|
|
|
2014-09-04 08:00:05 +02:00
|
|
|
# pylint: disable=protected-access
|
|
|
|
|
2014-05-05 15:16:27 +02:00
|
|
|
"""Tests for the webelement utils."""
|
|
|
|
|
2015-03-01 22:10:16 +01:00
|
|
|
from unittest import mock
|
2014-09-04 08:00:05 +02:00
|
|
|
import collections.abc
|
2014-05-05 15:16:27 +02:00
|
|
|
|
|
|
|
from PyQt5.QtCore import QRect, QPoint
|
2014-09-04 08:00:05 +02:00
|
|
|
from PyQt5.QtWebKit import QWebElement
|
2015-04-03 23:36:35 +02:00
|
|
|
import pytest
|
2014-05-05 15:16:27 +02:00
|
|
|
|
2014-09-08 10:30:05 +02:00
|
|
|
from qutebrowser.browser import webelem
|
2014-05-05 15:16:27 +02:00
|
|
|
|
|
|
|
|
2015-08-02 22:10:00 +02:00
|
|
|
def get_webelem(geometry=None, frame=None, null=False, style=None,
|
2014-09-04 08:00:05 +02:00
|
|
|
display='', attributes=None, tagname=None, classes=None):
|
|
|
|
"""Factory for WebElementWrapper objects based on a mock.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
geometry: The geometry of the QWebElement as QRect.
|
|
|
|
frame: The QWebFrame the element is in.
|
|
|
|
null: Whether the element is null or not.
|
2015-08-02 22:10:00 +02:00
|
|
|
style: A dict with the styleAttributes of the element.
|
2014-09-04 08:00:05 +02:00
|
|
|
attributes: Boolean HTML attributes to be added.
|
|
|
|
tagname: The tag name.
|
|
|
|
classes: HTML classes to be added.
|
|
|
|
"""
|
2015-03-01 22:10:16 +01:00
|
|
|
elem = mock.Mock()
|
2014-09-04 08:00:05 +02:00
|
|
|
elem.isNull.return_value = null
|
|
|
|
elem.geometry.return_value = geometry
|
|
|
|
elem.webFrame.return_value = frame
|
|
|
|
elem.tagName.return_value = tagname
|
|
|
|
elem.toOuterXml.return_value = '<fakeelem/>'
|
2015-08-02 22:10:00 +02:00
|
|
|
|
2014-09-04 08:00:05 +02:00
|
|
|
if attributes is not None:
|
|
|
|
if not isinstance(attributes, collections.abc.Mapping):
|
|
|
|
attributes = {e: None for e in attributes}
|
|
|
|
elem.hasAttribute.side_effect = lambda k: k in attributes
|
|
|
|
elem.attribute.side_effect = lambda k: attributes.get(k, '')
|
|
|
|
elem.attributeNames.return_value = list(attributes)
|
|
|
|
else:
|
|
|
|
elem.hasAttribute.return_value = False
|
|
|
|
elem.attribute.return_value = ''
|
|
|
|
elem.attributeNames.return_value = []
|
2015-08-02 22:10:00 +02:00
|
|
|
|
2014-09-04 08:00:05 +02:00
|
|
|
if classes is not None:
|
|
|
|
elem.classes.return_value = classes.split(' ')
|
|
|
|
else:
|
|
|
|
elem.classes.return_value = []
|
|
|
|
|
2015-08-02 22:10:00 +02:00
|
|
|
style_dict = {'visibility': '', 'display': ''}
|
|
|
|
if style is not None:
|
|
|
|
style_dict.update(style)
|
|
|
|
|
2014-09-04 08:00:05 +02:00
|
|
|
def _style_property(name, strategy):
|
|
|
|
"""Helper function to act as styleProperty method."""
|
|
|
|
if strategy != QWebElement.ComputedStyle:
|
|
|
|
raise ValueError("styleProperty called with strategy != "
|
|
|
|
"ComputedStyle ({})!".format(strategy))
|
2015-08-02 22:10:00 +02:00
|
|
|
return style_dict[name]
|
2014-09-04 08:00:05 +02:00
|
|
|
|
|
|
|
elem.styleProperty.side_effect = _style_property
|
|
|
|
wrapped = webelem.WebElementWrapper(elem)
|
|
|
|
if attributes is not None:
|
|
|
|
wrapped.update(attributes)
|
|
|
|
return wrapped
|
|
|
|
|
|
|
|
|
2015-04-04 17:05:44 +02:00
|
|
|
class TestWebElementWrapper:
|
2014-09-04 08:00:05 +02:00
|
|
|
|
|
|
|
"""Test WebElementWrapper."""
|
|
|
|
|
|
|
|
def test_nullelem(self):
|
|
|
|
"""Test __init__ with a null element."""
|
2015-04-03 23:36:35 +02:00
|
|
|
with pytest.raises(webelem.IsNullError):
|
2014-09-04 08:00:05 +02:00
|
|
|
get_webelem(null=True)
|
|
|
|
|
|
|
|
|
2015-08-02 22:10:00 +02:00
|
|
|
class TestIsVisible:
|
2014-05-12 11:41:35 +02:00
|
|
|
|
2015-08-02 22:10:00 +02:00
|
|
|
@pytest.fixture
|
|
|
|
def frame(self, stubs):
|
|
|
|
return stubs.FakeWebFrame(QRect(0, 0, 100, 100))
|
2014-05-05 15:16:27 +02:00
|
|
|
|
2015-08-02 22:10:00 +02:00
|
|
|
def test_nullelem(self, frame):
|
2014-05-12 11:41:35 +02:00
|
|
|
"""Passing an element with isNull() == True.
|
|
|
|
|
|
|
|
geometry() and webFrame() should not be called, and ValueError should
|
|
|
|
be raised.
|
|
|
|
"""
|
2014-09-04 08:00:05 +02:00
|
|
|
elem = get_webelem()
|
|
|
|
elem._elem.isNull.return_value = True
|
2015-04-03 23:36:35 +02:00
|
|
|
with pytest.raises(webelem.IsNullError):
|
2015-08-02 22:10:00 +02:00
|
|
|
elem.is_visible(frame)
|
2014-05-05 15:16:27 +02:00
|
|
|
|
2015-08-02 22:10:00 +02:00
|
|
|
def test_invalid_invisible(self, frame):
|
2014-05-12 11:41:35 +02:00
|
|
|
"""Test elements with an invalid geometry which are invisible."""
|
2015-08-02 22:10:00 +02:00
|
|
|
elem = get_webelem(QRect(0, 0, 0, 0), frame)
|
2015-04-03 23:36:35 +02:00
|
|
|
assert not elem.geometry().isValid()
|
|
|
|
assert elem.geometry().x() == 0
|
2015-08-02 22:10:00 +02:00
|
|
|
assert not elem.is_visible(frame)
|
2014-05-05 15:16:27 +02:00
|
|
|
|
2015-08-02 22:10:00 +02:00
|
|
|
def test_invalid_visible(self, frame):
|
2014-05-12 11:41:35 +02:00
|
|
|
"""Test elements with an invalid geometry which are visible.
|
|
|
|
|
|
|
|
This seems to happen sometimes in the real world, with real elements
|
|
|
|
which *are* visible, but don't have a valid geometry.
|
|
|
|
"""
|
2015-08-02 22:10:00 +02:00
|
|
|
elem = get_webelem(QRect(10, 10, 0, 0), frame)
|
2015-04-03 23:36:35 +02:00
|
|
|
assert not elem.geometry().isValid()
|
2015-08-02 22:10:00 +02:00
|
|
|
assert elem.is_visible(frame)
|
2014-05-12 11:07:08 +02:00
|
|
|
|
2015-08-02 22:10:00 +02:00
|
|
|
@pytest.mark.parametrize('geometry, visible', [
|
|
|
|
(QRect(5, 5, 4, 4), False),
|
|
|
|
(QRect(10, 10, 1, 1), True),
|
|
|
|
])
|
|
|
|
def test_scrolled(self, geometry, visible, stubs):
|
|
|
|
scrolled_frame = stubs.FakeWebFrame(QRect(0, 0, 100, 100),
|
|
|
|
scroll=QPoint(10, 10))
|
|
|
|
elem = get_webelem(geometry, scrolled_frame)
|
|
|
|
assert elem.is_visible(scrolled_frame) == visible
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('style, visible', [
|
|
|
|
({'visibility': 'visible'}, True),
|
|
|
|
({'visibility': 'hidden'}, False),
|
|
|
|
({'display': 'inline'}, True),
|
|
|
|
({'display': 'none'}, False),
|
|
|
|
({'visibility': 'visible', 'display': 'none'}, False),
|
|
|
|
({'visibility': 'hidden', 'display': 'inline'}, False),
|
|
|
|
])
|
|
|
|
def test_css_attributes(self, frame, style, visible):
|
|
|
|
elem = get_webelem(QRect(0, 0, 10, 10), frame, style=style)
|
|
|
|
assert elem.is_visible(frame) == visible
|
2014-05-12 11:07:08 +02:00
|
|
|
|
|
|
|
|
2015-04-04 17:05:44 +02:00
|
|
|
class TestIsVisibleIframe:
|
2014-05-12 12:16:41 +02:00
|
|
|
|
2014-05-27 13:06:13 +02:00
|
|
|
"""Tests for is_visible with a child frame.
|
|
|
|
|
|
|
|
Attributes:
|
|
|
|
frame: The FakeWebFrame we're using to test.
|
|
|
|
iframe: The iframe inside frame.
|
|
|
|
elem1-elem4: FakeWebElements to test.
|
|
|
|
"""
|
2014-05-12 12:16:41 +02:00
|
|
|
|
2015-08-02 22:10:00 +02:00
|
|
|
Objects = collections.namedtuple('Objects', ['frame', 'iframe', 'elems'])
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def objects(self, stubs):
|
2015-03-26 07:08:13 +01:00
|
|
|
"""Set up the following base situation.
|
|
|
|
|
|
|
|
0, 0 300, 0
|
|
|
|
##############################
|
|
|
|
# #
|
|
|
|
0,10 # iframe 100,10 #
|
|
|
|
#********** #
|
2015-08-02 22:10:00 +02:00
|
|
|
#*e * elems[0]: 0, 0 in iframe (visible)
|
2015-03-26 07:08:13 +01:00
|
|
|
#* * #
|
2015-08-02 22:10:00 +02:00
|
|
|
#* e * elems[1]: 20,90 in iframe (visible)
|
2015-03-26 07:08:13 +01:00
|
|
|
#********** #
|
|
|
|
0,110 #. .100,110 #
|
|
|
|
#. . #
|
2015-08-02 22:10:00 +02:00
|
|
|
#. e . elems[2]: 20,150 in iframe (not visible)
|
2015-03-26 07:08:13 +01:00
|
|
|
#.......... #
|
2015-08-02 22:10:00 +02:00
|
|
|
# e elems[3]: 30, 180 in main frame (visible)
|
2015-03-26 07:08:13 +01:00
|
|
|
# #
|
|
|
|
# frame #
|
|
|
|
##############################
|
|
|
|
300, 0 300, 300
|
2015-08-02 22:10:00 +02:00
|
|
|
|
|
|
|
Returns an Objects namedtuple with frame/iframe/elems attributes.
|
2014-05-12 12:16:41 +02:00
|
|
|
"""
|
2015-08-02 22:10:00 +02:00
|
|
|
frame = stubs.FakeWebFrame(QRect(0, 0, 300, 300))
|
|
|
|
iframe = stubs.FakeWebFrame(QRect(0, 10, 100, 100), parent=frame)
|
|
|
|
assert frame.geometry().contains(iframe.geometry())
|
|
|
|
elems = [
|
|
|
|
get_webelem(QRect(0, 0, 10, 10), iframe),
|
|
|
|
get_webelem(QRect(20, 90, 10, 10), iframe),
|
|
|
|
get_webelem(QRect(20, 150, 10, 10), iframe),
|
|
|
|
get_webelem(QRect(30, 180, 10, 10), frame),
|
|
|
|
]
|
|
|
|
|
|
|
|
assert elems[0].is_visible(frame)
|
|
|
|
assert elems[1].is_visible(frame)
|
|
|
|
assert not elems[2].is_visible(frame)
|
|
|
|
assert elems[3].is_visible(frame)
|
|
|
|
|
|
|
|
return self.Objects(frame=frame, iframe=iframe, elems=elems)
|
|
|
|
|
|
|
|
def test_iframe_scrolled(self, objects):
|
2014-05-12 12:16:41 +02:00
|
|
|
"""Scroll iframe down so elem3 gets visible and elem1/elem2 not."""
|
2015-08-02 22:10:00 +02:00
|
|
|
objects.iframe.scrollPosition.return_value = QPoint(0, 100)
|
|
|
|
assert not objects.elems[0].is_visible(objects.frame)
|
|
|
|
assert not objects.elems[1].is_visible(objects.frame)
|
|
|
|
assert objects.elems[2].is_visible(objects.frame)
|
|
|
|
assert objects.elems[3].is_visible(objects.frame)
|
2014-05-12 12:16:41 +02:00
|
|
|
|
2015-08-02 22:10:00 +02:00
|
|
|
def test_mainframe_scrolled_iframe_visible(self, objects):
|
2014-05-12 12:16:41 +02:00
|
|
|
"""Scroll mainframe down so iframe is partly visible but elem1 not."""
|
2015-08-02 22:10:00 +02:00
|
|
|
objects.frame.scrollPosition.return_value = QPoint(0, 50)
|
|
|
|
geom = objects.frame.geometry().translated(
|
|
|
|
objects.frame.scrollPosition())
|
|
|
|
assert not geom.contains(objects.iframe.geometry())
|
|
|
|
assert geom.intersects(objects.iframe.geometry())
|
|
|
|
assert not objects.elems[0].is_visible(objects.frame)
|
|
|
|
assert objects.elems[1].is_visible(objects.frame)
|
|
|
|
assert not objects.elems[2].is_visible(objects.frame)
|
|
|
|
assert objects.elems[3].is_visible(objects.frame)
|
|
|
|
|
|
|
|
def test_mainframe_scrolled_iframe_invisible(self, objects):
|
2014-05-12 12:16:41 +02:00
|
|
|
"""Scroll mainframe down so iframe is invisible."""
|
2015-08-02 22:10:00 +02:00
|
|
|
objects.frame.scrollPosition.return_value = QPoint(0, 110)
|
|
|
|
geom = objects.frame.geometry().translated(
|
|
|
|
objects.frame.scrollPosition())
|
|
|
|
assert not geom.contains(objects.iframe.geometry())
|
|
|
|
assert not geom.intersects(objects.iframe.geometry())
|
|
|
|
assert not objects.elems[0].is_visible(objects.frame)
|
|
|
|
assert not objects.elems[1].is_visible(objects.frame)
|
|
|
|
assert not objects.elems[2].is_visible(objects.frame)
|
|
|
|
assert objects.elems[3].is_visible(objects.frame)
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('attributes, writable', [
|
|
|
|
([], True),
|
|
|
|
(['disabled'], False),
|
|
|
|
(['readonly'], False),
|
|
|
|
(['disabled', 'readonly'], False),
|
|
|
|
])
|
|
|
|
def test_is_writable(attributes, writable):
|
|
|
|
elem = get_webelem(attributes=attributes)
|
|
|
|
assert elem.is_writable() == writable
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('before, after', [
|
|
|
|
('foo\\bar', r'foo\\bar'),
|
|
|
|
('foo\nbar', r'foo\nbar'),
|
|
|
|
("foo'bar", r"foo\'bar"),
|
|
|
|
('foo"bar', r'foo\"bar'),
|
|
|
|
])
|
|
|
|
def test_fake_escape(before, after):
|
|
|
|
"""Test javascript escaping."""
|
|
|
|
assert webelem.javascript_escape(before) == after
|
2014-05-05 15:16:27 +02:00
|
|
|
|
|
|
|
|
2015-04-04 17:05:44 +02:00
|
|
|
class TestGetChildFrames:
|
2014-05-12 13:26:11 +02:00
|
|
|
|
|
|
|
"""Check get_child_frames."""
|
|
|
|
|
2015-04-03 23:36:35 +02:00
|
|
|
def test_single_frame(self, stubs):
|
2014-05-12 13:26:11 +02:00
|
|
|
"""Test get_child_frames with a single frame without children."""
|
2014-08-26 19:10:14 +02:00
|
|
|
frame = stubs.FakeChildrenFrame()
|
2014-05-12 13:26:11 +02:00
|
|
|
children = webelem.get_child_frames(frame)
|
2015-04-03 23:36:35 +02:00
|
|
|
assert len(children) == 1
|
|
|
|
assert children[0] is frame
|
2014-05-12 13:26:11 +02:00
|
|
|
frame.childFrames.assert_called_once_with()
|
|
|
|
|
2015-04-03 23:36:35 +02:00
|
|
|
def test_one_level(self, stubs):
|
2015-03-26 07:08:13 +01:00
|
|
|
r"""Test get_child_frames with one level of children.
|
2014-05-12 13:26:11 +02:00
|
|
|
|
|
|
|
o parent
|
|
|
|
/ \
|
|
|
|
child1 o o child2
|
|
|
|
"""
|
2014-08-26 19:10:14 +02:00
|
|
|
child1 = stubs.FakeChildrenFrame()
|
|
|
|
child2 = stubs.FakeChildrenFrame()
|
|
|
|
parent = stubs.FakeChildrenFrame([child1, child2])
|
2014-05-12 13:26:11 +02:00
|
|
|
children = webelem.get_child_frames(parent)
|
2015-04-03 23:36:35 +02:00
|
|
|
assert len(children) == 3
|
|
|
|
assert children[0] is parent
|
|
|
|
assert children[1] is child1
|
|
|
|
assert children[2] is child2
|
2014-05-12 13:26:11 +02:00
|
|
|
parent.childFrames.assert_called_once_with()
|
|
|
|
child1.childFrames.assert_called_once_with()
|
|
|
|
child2.childFrames.assert_called_once_with()
|
|
|
|
|
2015-04-03 23:36:35 +02:00
|
|
|
def test_multiple_levels(self, stubs):
|
2015-03-26 07:08:13 +01:00
|
|
|
r"""Test get_child_frames with multiple levels of children.
|
2014-05-12 13:26:11 +02:00
|
|
|
|
|
|
|
o root
|
|
|
|
/ \
|
|
|
|
o o first
|
|
|
|
/\ /\
|
|
|
|
o o o o second
|
|
|
|
"""
|
2014-08-26 19:10:14 +02:00
|
|
|
second = [stubs.FakeChildrenFrame() for _ in range(4)]
|
|
|
|
first = [stubs.FakeChildrenFrame(second[0:2]),
|
|
|
|
stubs.FakeChildrenFrame(second[2:4])]
|
|
|
|
root = stubs.FakeChildrenFrame(first)
|
2014-05-12 13:26:11 +02:00
|
|
|
children = webelem.get_child_frames(root)
|
2015-04-03 23:36:35 +02:00
|
|
|
assert len(children) == 7
|
|
|
|
assert children[0] is root
|
2014-05-12 13:26:11 +02:00
|
|
|
for frame in [root] + first + second:
|
2015-04-03 23:36:35 +02:00
|
|
|
frame.childFrames.assert_called_once_with()
|
2014-05-12 13:26:11 +02:00
|
|
|
|
|
|
|
|
2015-04-04 17:05:44 +02:00
|
|
|
class TestIsEditable:
|
2014-06-23 20:31:47 +02:00
|
|
|
|
|
|
|
"""Tests for is_editable."""
|
|
|
|
|
2015-04-08 05:46:02 +02:00
|
|
|
@pytest.fixture
|
2015-05-18 23:32:01 +02:00
|
|
|
def stubbed_config(self, config_stub, monkeypatch):
|
2015-04-05 20:30:31 +02:00
|
|
|
"""Fixture to create a config stub with an input section."""
|
2015-05-07 07:58:22 +02:00
|
|
|
config_stub.data = {'input': {}}
|
2015-05-18 23:32:01 +02:00
|
|
|
monkeypatch.setattr('qutebrowser.browser.webelem.config', config_stub)
|
2015-05-07 07:58:22 +02:00
|
|
|
return config_stub
|
2014-06-23 20:31:47 +02:00
|
|
|
|
2015-08-02 22:10:00 +02:00
|
|
|
@pytest.mark.parametrize('tagname, attributes, editable', [
|
|
|
|
('input', {}, True),
|
|
|
|
('input', {'type': 'text'}, True),
|
|
|
|
('INPUT', {'TYPE': 'TEXT'}, True), # caps attributes/name
|
|
|
|
('input', {'type': 'email'}, True),
|
|
|
|
('input', {'type': 'url'}, True),
|
|
|
|
('input', {'type': 'tel'}, True),
|
|
|
|
('input', {'type': 'number'}, True),
|
|
|
|
('input', {'type': 'password'}, True),
|
|
|
|
('input', {'type': 'search'}, True),
|
|
|
|
('textarea', {}, True),
|
|
|
|
|
|
|
|
('input', {'type': 'button'}, False),
|
|
|
|
('input', {'type': 'checkbox'}, False),
|
|
|
|
('select', {}, False),
|
|
|
|
|
|
|
|
('input', {'disabled': None}, False),
|
|
|
|
('input', {'readonly': None}, False),
|
|
|
|
('textarea', {'disabled': None}, False),
|
|
|
|
('textarea', {'readonly': None}, False),
|
|
|
|
('object', {}, False),
|
|
|
|
('object', {'type': 'image/gif'}, False),
|
|
|
|
])
|
|
|
|
def test_is_editable(self, tagname, attributes, editable):
|
|
|
|
elem = get_webelem(tagname=tagname, attributes=attributes)
|
|
|
|
assert elem.is_editable() == editable
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('classes, editable', [
|
|
|
|
(None, False),
|
|
|
|
('foo-kix-bar', False),
|
|
|
|
('foo kix-foo', True),
|
|
|
|
('KIX-FOO', False),
|
|
|
|
('foo CodeMirror-foo', True),
|
|
|
|
])
|
|
|
|
def test_is_editable_div(self, tagname, classes, editable):
|
|
|
|
elem = get_webelem(tagname='div', classes=classes)
|
|
|
|
assert elem.is_editable() == editable
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('setting, tagname, attributes, editable', [
|
|
|
|
(True, 'embed', {}, True),
|
|
|
|
(True, 'embed', {}, True),
|
|
|
|
(False, 'applet', {}, False),
|
|
|
|
(False, 'applet', {}, False),
|
|
|
|
(True, 'object', {'type': 'application/foo'}, True),
|
|
|
|
(False, 'object', {'type': 'application/foo'}, False),
|
|
|
|
(True, 'object', {'type': 'foo', 'classid': 'foo'}, True),
|
|
|
|
(False, 'object', {'type': 'foo', 'classid': 'foo'}, False),
|
|
|
|
])
|
|
|
|
def test_is_editable_div(self, stubbed_config, setting, tagname,
|
|
|
|
attributes, editable):
|
|
|
|
stubbed_config.data['input']['insert-mode-on-plugins'] = setting
|
|
|
|
elem = get_webelem(tagname=tagname, attributes=attributes)
|
|
|
|
assert elem.is_editable() == editable
|