Skip to content

Commit 4e48199

Browse files
pekkaklarckaaltat
authored andcommitted
Mega super hyper cleanup (robotframework#956)
* Cleanup. - Remove unnecessary coupling between Element and FormElement keywords. - Enhance related error message. - Enhance documentation of ElementKeywords (robotframework#925). - Add '.' at the end of error and log messages. - Introduce `ContextsAware.find_elements` helper. - Change default value of optional `message` from '' to None. - Use `is_noney` instead of `is_falsy` when default value is None. - General (and trivial) cleanup to code and tests. -Also added more XPath examples. This ought to cover enhancements proposed by robotframework#940. - Doc string to `find_element`. Mainly to give type hits to IDE but higher level APIs should get more docs in general. - Introduced ElementNotFound error that `ElementFinder.find` uses. - Move helpers to ContextAware and LibraryComponent to avoid unnecessary coupling between different library componets. - Remove unnecessary helpers when `find_element(locator).method()` works as well. - Rewriter internal logic with `Wait` keywords. This includes consistently handling non-existing elements. - Make unregistering strategy that hasn't been registered an error. - Consider `<input type="file">` text element consistently. - General cleanup here and there. - Move frame related kws to own lib component - "Element Should (Not) Be Enabled" and "Wait Until Element Is Enabled" now all validate that the element is enabled and not readonly. Also removed broken element type validation from the former keywords. - Doc that elements considered enabled cannot be read-only - Base class for SeLib exceptions - Test "Page Should Contain" with text spanning multiple elements. - User `find_elements` instead of `find_element` when possible. - Avoid `strategy=value` locator syntax in code. - Enhance error messages when radio buttons not found. - Cleanup `Choose File` test and test it with Firefox. - TableElementFinder._locator to public class variable. - Remove useless documentation. Fixes robotframework#958.
1 parent aaea8dc commit 4e48199

37 files changed

+1101
-1068
lines changed

src/SeleniumLibrary/__init__.py

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
CookieKeywords,
2626
ElementKeywords,
2727
FormElementKeywords,
28+
FrameKeywords,
2829
JavaScriptKeywords,
2930
RunOnFailureKeywords,
3031
ScreenshotKeywords,
@@ -64,10 +65,10 @@ class SeleniumLibrary(DynamicCore):
6465
= Locating elements =
6566
6667
All keywords in SeleniumLibrary that need to interact with an element
67-
on a web page take an argument named ``locator`` that specifies how
68-
to find the element. Most often the locator is given as a string using
69-
the locator syntax described below, but `using WebElements` is possible
70-
too.
68+
on a web page take an argument typically named ``locator`` that specifies
69+
how to find the element. Most often the locator is given as a string
70+
using the locator syntax described below, but `using WebElements` is
71+
possible too.
7172
7273
== Locator syntax ==
7374
@@ -100,17 +101,17 @@ class SeleniumLibrary(DynamicCore):
100101
| `Click Element` | name:foo | # Find element with name ``foo``. |
101102
| `Click Element` | default:name:foo | # Use default strategy with value ``name:foo``. |
102103
| `Click Element` | //foo | # Find element using XPath ``//foo``. |
103-
| `Click Element` | default://foo | # Use default strategy with value ``//foo``. |
104+
| `Click Element` | default: //foo | # Use default strategy with value ``//foo``. |
104105
105106
=== Explicit locator strategy ===
106107
107108
The explicit locator strategy is specified with a prefix using either
108109
syntax ``strategy:value`` or ``strategy=value``. The former syntax
109110
is preferred, because the latter is identical to Robot Framework's
110111
[http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#named-argument-syntax|
111-
named argument syntax] and that can cause problems. Notice that the
112-
``strategy:value`` syntax is olny supported by SeleniumLibrary 3.0 and
113-
newer, though.
112+
named argument syntax] and that can cause problems. Spaces around
113+
the separator are ignored, so ``id:foo``, ``id: foo`` and ``id : foo``
114+
are all equivalent.
114115
115116
Locator strategies that are supported by default are listed in the table
116117
below. In addition to them, it is possible to register `custom locators`.
@@ -135,20 +136,33 @@ class SeleniumLibrary(DynamicCore):
135136
prefix is only necessary if the locator value itself accidentally
136137
matches some of the explicit strategies.
137138
138-
Spaces around the separator are ignored, so ``id:foo``, ``id: foo``
139-
and ``id : foo`` are all equivalent.
139+
Different locator strategies have different pros and cons. Using ids,
140+
either explicitly like ``id:foo`` or by using the `default locator
141+
strategy` simply like ``foo``, is recommended when possible, because
142+
the syntax is simple and locating elements by an id is fast for browsers.
143+
If an element does not have an id or the id is not stable, other
144+
solutions need to be used. If an element has a unique tag name or class,
145+
using ``tag``, ``class`` or ``css`` strategy like ``tag:h1``,
146+
``class:example`` or ``css:h1.example`` is often an easy solution. In
147+
more complex cases using XPath expressions is typically the best
148+
approach. They are very powerful but a downside is that they can also
149+
get complex.
140150
141151
Examples:
142152
143-
| `Click Element` | id:container |
144-
| `Click Element` | css:div#container h1 |
145-
| `Click Element` | xpath: //div[@id="container"]//h1 |
153+
| `Click Element` | id:foo | # Element with id 'foo'. |
154+
| `Click Element` | css:div#foo h1 | # h1 element under div with id 'foo'. |
155+
| `Click Element` | xpath: //div[@id="foo"]//h1 | # Same as the above using XPath, not CSS. |
156+
| `Click Element` | xpath: //*[contains(text(), "example")] | # Element containing text 'example'. |
146157
147-
Notice that using the ``sizzle`` strategy or its alias ``jquery``
148-
requires that the system under test contains the jQuery library.
158+
*NOTE:*
149159
150-
Notice also that prior to SeleniumLibrary 3.0, table related keywords
151-
only supported ``xpath``, ``css`` and ``sizzle/jquery`` strategies.
160+
- The ``strategy:value`` syntax is only supported by SeleniumLibrary 3.0
161+
and newer.
162+
- Using the ``sizzle`` strategy or its alias ``jquery`` requires that
163+
the system under test contains the jQuery library.
164+
- Prior to SeleniumLibrary 3.0, table related keywords only supported
165+
``xpath``, ``css`` and ``sizzle/jquery`` strategies.
152166
153167
=== Implicit XPath strategy ===
154168
@@ -158,8 +172,8 @@ class SeleniumLibrary(DynamicCore):
158172
159173
Examples:
160174
161-
| `Click Element` | //div[@id="container"] |
162-
| `Click Element` | (//div)[2] |
175+
| `Click Element` | //div[@id="foo"]//h1 |
176+
| `Click Element` | (//div)[2] |
163177
164178
The support for the ``(//`` prefix is new in SeleniumLibrary 3.0.
165179
@@ -316,14 +330,15 @@ def __init__(self, timeout=5.0, implicit_wait=0.0,
316330
libraries = [
317331
AlertKeywords(self),
318332
BrowserManagementKeywords(self),
319-
RunOnFailureKeywords(self),
333+
CookieKeywords(self),
320334
ElementKeywords(self),
321-
TableElementKeywords(self),
322335
FormElementKeywords(self),
323-
SelectElementKeywords(self),
336+
FrameKeywords(self),
324337
JavaScriptKeywords(self),
325-
CookieKeywords(self),
338+
RunOnFailureKeywords(self),
326339
ScreenshotKeywords(self),
340+
SelectElementKeywords(self),
341+
TableElementKeywords(self),
327342
WaitingKeywords(self)
328343
]
329344
self._browsers = BrowserCache()

src/SeleniumLibrary/base/context.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616

17+
from SeleniumLibrary.utils import escape_xpath_value
18+
1719

1820
class ContextAware(object):
1921

@@ -37,11 +39,46 @@ def browsers(self):
3739
def element_finder(self):
3840
return self.ctx.element_finder
3941

42+
@property
43+
def table_element_finder(self):
44+
return self.ctx.table_element_finder
45+
4046
def find_element(self, locator, tag=None, first_only=True, required=True,
4147
parent=None):
48+
"""Find element matching `locator`.
49+
50+
:param locator: Locator to use when searching the element.
51+
See library documentation for the supported locator syntax.
52+
:param tag: Limit searching only to these elements.
53+
:param first_only: Return only first matching element if true,
54+
a list of elements otherwise.
55+
:param required: Raise `ElementNotFound` if element not found when
56+
true, return `None` otherwise.
57+
:param parent: Possible parent `WebElememt` element to search
58+
elements from. By default search starts from `WebDriver`.
59+
:return: Found `WebElement` or `None` if element not found and
60+
`required` is false.
61+
:rtype: selenium.webdriver.remote.webelement.WebElement
62+
:raises SeleniumLibrary.errors.ElementNotFound: If element not found
63+
and `required` is true.
64+
"""
4265
return self.element_finder.find(locator, tag, first_only,
4366
required, parent)
4467

45-
@property
46-
def table_element_finder(self):
47-
return self.ctx.table_element_finder
68+
def find_elements(self, locator, tag=None, parent=None):
69+
"""Find all elements matching `locator`.
70+
71+
Always returns a list of `WebElement` objects. If no matching element
72+
is found, the list is empty. Otherwise semantics are exactly same
73+
as with :meth:`find_element`.
74+
"""
75+
return self.find_element(locator, tag, False, False, parent)
76+
77+
def is_text_present(self, text):
78+
locator = "xpath://*[contains(., %s)]" % escape_xpath_value(text)
79+
return self.find_element(locator, required=False) is not None
80+
81+
def is_element_enabled(self, locator, tag=None):
82+
element = self.find_element(locator, tag)
83+
return (element.is_enabled() and
84+
element.get_attribute('readonly') is None)

src/SeleniumLibrary/base/librarycomponent.py

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,36 +25,45 @@
2525
from .robotlibcore import PY2
2626

2727

28-
LOG_LEVELS = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR']
29-
30-
3128
class LibraryComponent(ContextAware):
3229

33-
def __init__(self, ctx):
34-
ContextAware.__init__(self, ctx)
35-
3630
def info(self, msg, html=False):
3731
logger.info(msg, html)
3832

3933
def debug(self, msg, html=False):
4034
logger.debug(msg, html)
4135

4236
def log(self, msg, level='INFO', html=False):
43-
if level.upper() in LOG_LEVELS:
44-
logger.write(msg, level, html)
37+
if not is_noney(level):
38+
logger.write(msg, level.upper(), html)
4539

4640
def warn(self, msg, html=False):
4741
logger.warn(msg, html)
4842

43+
def log_source(self, loglevel='INFO'):
44+
self.ctx.log_source(loglevel)
45+
4946
def assert_page_contains(self, locator, tag=None, message=None,
5047
loglevel='INFO'):
51-
self.element_finder.assert_page_contains(locator, tag, message,
52-
loglevel)
48+
if not self.find_element(locator, tag, required=False):
49+
self.log_source(loglevel)
50+
if is_noney(message):
51+
message = ("Page should have contained %s '%s' but did not."
52+
% (tag or 'element', locator))
53+
raise AssertionError(message)
54+
logger.info("Current page contains %s '%s'."
55+
% (tag or 'element', locator))
5356

5457
def assert_page_not_contains(self, locator, tag=None, message=None,
5558
loglevel='INFO'):
56-
self.element_finder.assert_page_not_contains(locator, tag, message,
57-
loglevel)
59+
if self.find_element(locator, tag, required=False):
60+
self.log_source(loglevel)
61+
if is_noney(message):
62+
message = ("Page should not have contained %s '%s'."
63+
% (tag or 'element', locator))
64+
raise AssertionError(message)
65+
logger.info("Current page does not contain %s '%s'."
66+
% (tag or 'element', locator))
5867

5968
def get_timeout(self, timeout=None):
6069
if is_noney(timeout):

src/SeleniumLibrary/errors.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright 2008-2011 Nokia Networks
2+
# Copyright 2011-2016 Ryan Tomac, Ed Manlove and contributors
3+
# Copyright 2016- Robot Framework Foundation
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
18+
class SeleniumLibraryException(Exception):
19+
ROBOT_SUPPRESS_NAME = True
20+
21+
22+
class ElementNotFound(SeleniumLibraryException):
23+
pass

src/SeleniumLibrary/keywords/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from .cookie import CookieKeywords
2020
from .element import ElementKeywords
2121
from .formelement import FormElementKeywords
22+
from .frames import FrameKeywords
2223
from .javascript import JavaScriptKeywords
2324
from .runonfailure import RunOnFailureKeywords
2425
from .screenshot import ScreenshotKeywords

src/SeleniumLibrary/keywords/browsermanagement.py

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import time
1919
import types
2020

21-
from robot.errors import DataError
2221
from robot.utils import NormalizedDict
2322
from selenium import webdriver
2423
from selenium.common.exceptions import NoSuchWindowException
@@ -325,27 +324,6 @@ def set_window_position(self, x, y):
325324
"""
326325
self.browser.set_window_position(int(x), int(y))
327326

328-
@keyword
329-
def select_frame(self, locator):
330-
"""Sets frame identified by ``locator`` as the current frame.
331-
332-
Key attributes for frames are `id` and `name.` See `introduction` for
333-
details about locating elements.
334-
335-
See `Unselect Frame` to cancel the frame selection and return to the Main frame.
336-
337-
Please note that the frame search always start from the document root or main frame.
338-
339-
Example:
340-
| Select Frame | xpath: //frame[@name='top]/iframe[@name='left'] | # Selects the 'left' iframe |
341-
| Click Link | foo | # Clicks link 'foo' in 'left' iframe |
342-
| Unselect Frame | | # Returns to main frame |
343-
| Select Frame | left | # Selects the 'top' frame |
344-
"""
345-
self.info("Selecting frame '%s'." % locator)
346-
element = self.find_element(locator)
347-
self.browser.switch_to.frame(element)
348-
349327
@keyword
350328
def select_window(self, locator=None):
351329
"""Selects the window matching locator and return previous window handle.
@@ -393,14 +371,6 @@ def list_windows(self):
393371
"""Return all current window handles as a list."""
394372
return self.browser.window_handles
395373

396-
@keyword
397-
def unselect_frame(self):
398-
"""Sets the top frame as the current frame.
399-
400-
In practice cancels a previous `Select Frame` call.
401-
"""
402-
self.browser.switch_to.default_content()
403-
404374
@keyword
405375
def get_location(self):
406376
"""Returns the current browser URL."""
@@ -427,8 +397,8 @@ def location_should_be(self, url):
427397
"""Verifies that current URL is exactly ``url``."""
428398
actual = self.get_location()
429399
if actual != url:
430-
raise AssertionError("Location should have been '%s' but was '%s'"
431-
% (url, actual))
400+
raise AssertionError("Location should have been '%s' but was "
401+
"'%s'." % (url, actual))
432402
self.info("Current location is '%s'." % url)
433403

434404
@keyword
@@ -456,7 +426,7 @@ def log_source(self, loglevel='INFO'):
456426
(no logging).
457427
"""
458428
source = self.get_source()
459-
self.log(source, loglevel.upper())
429+
self.log(source, loglevel)
460430
return source
461431

462432
@keyword

0 commit comments

Comments
 (0)