Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: SeleniumLibrary CI

on: [push, pull_request]
on: workflow_dispatch

jobs:
build:
Expand All @@ -9,10 +9,10 @@ jobs:
continue-on-error: true
strategy:
matrix:
python-version: [3.9.23, 3.13.5, 3.14.0-rc.3] # pypy-3.9
python-version: [3.13.10, 3.14.1] # 3.9.23, pypy-3.9
# python-version: [{earliest: 3.9}, {latest: 3.13.0}] # pypy-3.9
rf-version: [6.1.1, 7.3.2]
selenium-version: [4.28.1, 4.29.0, 4.30.0, 4.31.0, 4.32.0, 4.33.0, 4.34.2]
rf-version: [6.1.1, 7.3.2, 7.4.1]
selenium-version: [4.34.2, 4.35.0, 4.36.0, 4.37.0, 4.38.0, 4.39.0]
browser: [chrome] # firefox, chrome, headlesschrome, edge

steps:
Expand Down Expand Up @@ -84,7 +84,7 @@ jobs:
# xvfb-run --auto-servernum python atest/run.py --zip ${{ matrix.browser }}

- name: Run tests with latest python and latest robot framework
if: matrix.python-version == '3.13.0' && matrix.rf-version == '7.2.2'
if: matrix.python-version == '3.14.1' && matrix.rf-version == '7.4.1'
run: |
xvfb-run --auto-servernum python atest/run.py --zip ${{ matrix.browser }}

Expand Down
21 changes: 10 additions & 11 deletions .github/workflows/Select.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
name: Select Configurations
name: Selected Test Configuration Matrix

on: workflow_dispatch
on: [push, pull_request]

jobs:
test_config:
runs-on: ubuntu-latest
continue-on-error: true
strategy:
matrix:
config:
Expand All @@ -17,7 +18,12 @@ jobs:
python-version: 3.12.12
rf-version: 7.3.2
selenium-version: 4.38.0
browser: firefox
browser: chrome
- description: older_rf_version
python-version: 3.11.14
rf-version: 6.1.1
selenium-version: 4.37.0
browser: chrome

steps:
- uses: actions/checkout@v4
Expand All @@ -32,9 +38,8 @@ jobs:
- name: Setup ${{ matrix.config.browser }} browser
uses: browser-actions/setup-chrome@v1
with:
chrome-version: 138
chrome-version: latest
install-dependencies: true
install-chromedriver: true
id: setup-chrome
- run: |
echo Installed chromium version: ${{ steps.setup-chrome.outputs.chrome-version }}
Expand All @@ -61,12 +66,6 @@ jobs:
- name: Install RF ${{ matrix.config.rf-version }}
run: |
pip install -U --pre robotframework==${{ matrix.config.rf-version }}
- name: Install drivers via selenium-manager
run: |
SELENIUM_MANAGER_EXE=$(python -c 'from selenium.webdriver.common.selenium_manager import SeleniumManager; sm=SeleniumManager(); print(f"{str(sm._get_binary())}")')
echo "$SELENIUM_MANAGER_EXE"
echo "WEBDRIVERPATH=$($SELENIUM_MANAGER_EXE --browser chrome --debug | awk '/INFO[[:space:]]Driver path:/ {print $NF;exit}')" >> "$GITHUB_ENV"
echo "$WEBDRIVERPATH"

- name: Run tests under specified config
run: |
Expand Down
18 changes: 18 additions & 0 deletions atest/acceptance/keywords/content_assertions.robot
Original file line number Diff line number Diff line change
Expand Up @@ -206,14 +206,26 @@ Element Should Not Contain
Element Text Should Be
Element Text Should Be some_id This text is inside an identified element
Element Text Should Be some_id This TEXT IS INSIDE AN IDENTIFIED ELEMENT ignore_case=True
Element Text Should Be some_id This text is inside an identified element${SPACE} strip_spaces=True
Element Text Should Be some_id ${SPACE}This text is inside an identified element strip_spaces=LEADING
Element Text Should Be some_id This text is inside an identified element${SPACE} strip_spaces=TRAILING
Element Text Should Be some_id This${SPACE}${SPACE} text is inside an identified element collapse_spaces=True
Run Keyword And Expect Error
... The text of element 'some_id' should have been 'inside' but it was 'This text is inside an identified element'.
... Element Text Should Be some_id inside
Run Keyword And Expect Error
... The text of element 'some_id' should have been 'This text is inside an identified element ' but it was 'This text is inside an identified element'.
... Element Text Should Be some_id This text is inside an identified element${SPACE} strip_spaces=False
Run Keyword And Expect Error
... The text of element 'some_id' should have been 'This${SPACE}${SPACE} text is inside an identified element' but it was 'This text is inside an identified element'.
... Element Text Should Be some_id This${SPACE}${SPACE} text is inside an identified element collapse_spaces=False

Element Text Should Not Be
Element Text Should Not Be some_id Foo This text is inside an identified element
Element Text Should Not Be some_id This TEXT IS INSIDE AN IDENTIFIED ELEMENT ignore_case=False
Element Text Should Not Be some_id FOO This text is inside an identified element ignore_case=True
Element Text Should Not Be some_id This text is inside an identified element${SPACE} strip_spaces=False
Element Text Should Not Be some_id This text${SPACE}${SPACE} is inside an identified element collapse_spaces=False
Run Keyword And Expect Error
... The text of element 'some_id' was not supposed to be 'This text is inside an identified element'.
... Element Text Should Not Be some_id This text is inside an identified element
Expand All @@ -223,6 +235,12 @@ Element Text Should Not Be
Run Keyword And Expect Error
... The text of element 'some_id' was not supposed to be 'THIS TEXT is inside an identified element'.
... Element Text Should Not Be some_id THIS TEXT is inside an identified element ignore_case=True
Run Keyword And Expect Error
... The text of element 'some_id' was not supposed to be 'This text is inside an identified element '.
... Element Text Should Not Be some_id This text is inside an identified element${SPACE} strip_spaces=True
Run Keyword And Expect Error
... The text of element 'some_id' was not supposed to be 'This text${SPACE}${SPACE} is inside an identified element'.
... Element Text Should Not Be some_id This text${SPACE}${SPACE} is inside an identified element collapse_spaces=True

Get Text
${str} = Get Text some_id
Expand Down
20 changes: 13 additions & 7 deletions atest/acceptance/keywords/cookies.robot
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we not generate a expiry date in the future and use that instead of having to update this every year?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be good although I tend to push this off every year till it is that time of year. One problem is expiry dates can no longer be longer than a year. So it is not like we can set it so far into the future that it will just never expire.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about this?

*** Test Cases ***
Add Cookie With Python Calculation
    ${expiry} =    Evaluate    int((datetime.datetime.now() + datetime.timedelta(days=330)).timestamp())    modules=datetime
    Add Cookie    Cookie1    value1    expiry=${expiry}

Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,21 @@ Add Cookie When Secure Is False
Should Be Equal ${cookie.secure} ${False}

Add Cookie When Expiry Is Epoch
Add Cookie Cookie1 value1 expiry=1761755100
# To convert epoch to formatted string
# from time import strftime, localtime
# strftime('%Y-%m-%d %H:%M:%S', localtime(1793247900))
# To update time each September (as Chrome limits cookies to one year expiry date) use
# import datetime
# print (datetime.datetime.strptime("2027-10-29 12:25:00", "%Y-%m-%d %I:%M:%S").timestamp())
Add Cookie Cookie1 value1 expiry=1793247900
${cookie} = Get Cookie Cookie1
${expiry} = Convert Date ${1761755100} exclude_millis=True
${expiry} = Convert Date ${1793247900} exclude_millis=True
Should Be Equal As Strings ${cookie.expiry} ${expiry}

Add Cookie When Expiry Is Human Readable Data&Time
Add Cookie Cookie12 value12 expiry=2025-10-29 12:25:00
Add Cookie Cookie12 value12 expiry=2026-10-29 12:25:00
${cookie} = Get Cookie Cookie12
Should Be Equal As Strings ${cookie.expiry} 2025-10-29 12:25:00
Should Be Equal As Strings ${cookie.expiry} 2026-10-29 12:25:00

Delete Cookie
[Tags] Known Issue Safari
Expand Down Expand Up @@ -114,20 +120,20 @@ Test Get Cookie Keyword Logging
... domain=localhost
... secure=False
... httpOnly=False
... expiry=2025-09-01 *:25:00
... expiry=2026-09-01 *:25:00
... extra={'sameSite': 'Lax'}
${cookie} = Get Cookie far_future

*** Keywords ***
Add Cookies
# To update time each September (as Chrome limits cookies to one year expiry date) use
# import datetime
# print (datetime.datetime.strptime("2025-09-01 12:25:00", "%Y-%m-%d %I:%M:%S").timestamp())
# print (datetime.datetime.strptime("2027-09-01 12:25:00", "%Y-%m-%d %I:%M:%S").timestamp())
Delete All Cookies
Add Cookie test seleniumlibrary
${now} = Get Current Date
${tomorrow_thistime} = Add Time To Date ${now} 1 day
${tomorrow_thistime_datetime} = Convert Date ${tomorrow_thistime} datetime
Set Suite Variable ${tomorrow_thistime_datetime}
Add Cookie another value expiry=${tomorrow_thistime}
Add Cookie far_future timemachine expiry=1756700700 # 2025-09-01 12:25:00
Add Cookie far_future timemachine expiry=1788236700 # 2026-09-01 12:25:00
101 changes: 84 additions & 17 deletions src/SeleniumLibrary/__init__.pyi
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should create a new type for the locator, this is a lot of duplication.

from typing import TypeAlias, Union
from selenium.webdriver.remote.webelement import WebElement

Locator: TypeAlias = WebElement | str

Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,90 @@ class SeleniumLibrary:
def current_frame_should_not_contain(self, text: str, loglevel: str = 'TRACE'): ...
def delete_all_cookies(self): ...
def delete_cookie(self, name): ...
def double_click_element(self, locator: Union): ...
def drag_and_drop(self, locator: Union, target: Union): ...
def drag_and_drop_by_offset(self, locator: Union, xoffset: int, yoffset: int): ...
def element_attribute_value_should_be(self, locator: Union, attribute: str, expected: Optional, message: Optional[Optional] = None): ...
def element_should_be_disabled(self, locator: Union): ...
def element_should_be_enabled(self, locator: Union): ...
def element_should_be_focused(self, locator: Union): ...
def element_should_be_visible(self, locator: Union, message: Optional[Optional] = None): ...
def element_should_contain(self, locator: Union, expected: Optional, message: Optional[Optional] = None, ignore_case: bool = False): ...
def element_should_not_be_visible(self, locator: Union, message: Optional[Optional] = None): ...
def element_should_not_contain(self, locator: Union, expected: Optional, message: Optional[Optional] = None, ignore_case: bool = False): ...
def element_text_should_be(self, locator: Union, expected: Optional, message: Optional[Optional] = None, ignore_case: bool = False): ...
def element_text_should_not_be(self, locator: Union, not_expected: Optional, message: Optional[Optional] = None, ignore_case: bool = False): ...
def execute_async_javascript(self, *code: Any): ...
def execute_javascript(self, *code: Any): ...
def frame_should_contain(self, locator: Union, text: str, loglevel: str = 'TRACE'): ...
def get_action_chain_delay(self): ...
def double_click_element(
self, locator: Union[selenium.webdriver.remote.webelement.WebElement, str]
): ...
def drag_and_drop(
self,
locator: Union[selenium.webdriver.remote.webelement.WebElement, str],
target: Union[selenium.webdriver.remote.webelement.WebElement, str],
): ...
def drag_and_drop_by_offset(
self,
locator: Union[selenium.webdriver.remote.webelement.WebElement, str],
xoffset: int,
yoffset: int,
): ...
def element_attribute_value_should_be(
self,
locator: Union[selenium.webdriver.remote.webelement.WebElement, str],
attribute: str,
expected: Union[None, str],
message: Optional[str] = None,
): ...
def element_should_be_disabled(
self, locator: Union[selenium.webdriver.remote.webelement.WebElement, str]
): ...
def element_should_be_enabled(
self, locator: Union[selenium.webdriver.remote.webelement.WebElement, str]
): ...
def element_should_be_focused(
self, locator: Union[selenium.webdriver.remote.webelement.WebElement, str]
): ...
def element_should_be_visible(
self,
locator: Union[selenium.webdriver.remote.webelement.WebElement, str],
message: Optional[str] = None,
): ...
def element_should_contain(
self,
locator: Union[selenium.webdriver.remote.webelement.WebElement, str],
expected: Union[None, str],
message: Optional[str] = None,
ignore_case: bool = False,
): ...
def element_should_not_be_visible(
self,
locator: Union[selenium.webdriver.remote.webelement.WebElement, str],
message: Optional[str] = None,
): ...
def element_should_not_contain(
self,
locator: Union[selenium.webdriver.remote.webelement.WebElement, str],
expected: Union[None, str],
message: Optional[str] = None,
ignore_case: bool = False,
): ...
def element_text_should_be(
self,
locator: Union[selenium.webdriver.remote.webelement.WebElement, str],
expected: Union[None, str],
message: Optional[str] = None,
ignore_case: bool = False,
strip_spaces: Union[bool, str] = False,
collapse_spaces: bool = False,
): ...
def element_text_should_not_be(
self,
locator: Union[selenium.webdriver.remote.webelement.WebElement, str],
not_expected: Union[None, str],
message: Optional[str] = None,
ignore_case: bool = False,
strip_spaces: Union[bool, str] = False,
collapse_spaces: bool = False,
): ...
def execute_async_javascript(
self, *code: Union[selenium.webdriver.remote.webelement.WebElement, str]
): ...
def execute_javascript(
self, *code: Union[selenium.webdriver.remote.webelement.WebElement, str]
): ...
def frame_should_contain(
self,
locator: Union[selenium.webdriver.remote.webelement.WebElement, str],
text: str,
loglevel: str = "TRACE",
): ...
def get_all_links(self): ...
def get_browser_aliases(self): ...
def get_browser_ids(self): ...
Expand Down
60 changes: 58 additions & 2 deletions src/SeleniumLibrary/keywords/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import re

from collections import namedtuple
from typing import List, Optional, Tuple, Union

from SeleniumLibrary.utils import is_noney
from robot.utils import plural_or_not, is_truthy
from robot.utils import plural_or_not, is_truthy, is_string
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.remote.webelement import WebElement
Expand Down Expand Up @@ -334,6 +336,8 @@ def element_text_should_be(
expected: Union[None, str],
message: Optional[str] = None,
ignore_case: bool = False,
strip_spaces: Union[bool, str] = False,
collapse_spaces: bool = False,
):
"""Verifies that element ``locator`` contains exact the text ``expected``.

Expand All @@ -346,7 +350,19 @@ def element_text_should_be(
The ``ignore_case`` argument can be set to True to compare case
insensitive, default is False.

If ``strip_spaces`` is given a true value (see `Boolean arguments`)
and both arguments are strings, the comparison is done without leading
and trailing spaces. If ``strip_spaces`` is given a string value
``LEADING`` or ``TRAILING`` (case-insensitive), the comparison is done
without leading or trailing spaces, respectively.

If ``collapse_spaces`` is given a true value (see `Boolean arguments`) and both
arguments are strings, the comparison is done with all white spaces replaced by
a single space character.

``ignore_case`` argument is new in SeleniumLibrary 3.1.
``strip_spaces`` is new in SeleniumLibrary 5.x.x and
``collapse_spaces`` is new in SeleniumLibrary 5.x.x.

Use `Element Should Contain` if a substring match is desired.
"""
Expand All @@ -355,6 +371,12 @@ def element_text_should_be(
if ignore_case:
text = text.lower()
expected = expected.lower()
if strip_spaces:
text = self._strip_spaces(text, strip_spaces)
expected = self._strip_spaces(expected, strip_spaces)
if collapse_spaces:
text = self._collapse_spaces(text)
expected = self._collapse_spaces(expected)
if text != expected:
if message is None:
message = (
Expand All @@ -370,6 +392,8 @@ def element_text_should_not_be(
not_expected: Union[None, str],
message: Optional[str] = None,
ignore_case: bool = False,
strip_spaces: Union[bool, str] = False,
collapse_spaces: bool = False,
):
"""Verifies that element ``locator`` does not contain exact the text ``not_expected``.

Expand All @@ -382,7 +406,19 @@ def element_text_should_not_be(
The ``ignore_case`` argument can be set to True to compare case
insensitive, default is False.

New in SeleniumLibrary 3.1.1
If ``strip_spaces`` is given a true value (see `Boolean arguments`)
and both arguments are strings, the comparison is done without leading
and trailing spaces. If ``strip_spaces`` is given a string value
``LEADING`` or ``TRAILING`` (case-insensitive), the comparison is done
without leading or trailing spaces, respectively.

If ``collapse_spaces`` is given a true value (see `Boolean arguments`) and both
arguments are strings, the comparison is done with all white spaces replaced by
a single space character.

``ignore_case`` is new in SeleniumLibrary 3.1.1
``strip_spaces`` is new in SeleniumLibrary 5.x.x and
``collapse_spaces`` is new in SeleniumLibrary 5.x.x.
"""
self.info(
f"Verifying element '{locator}' does not contain exact text '{not_expected}'."
Expand All @@ -392,11 +428,31 @@ def element_text_should_not_be(
if ignore_case:
text = text.lower()
not_expected = not_expected.lower()
if strip_spaces:
text = self._strip_spaces(text, strip_spaces)
not_expected = self._strip_spaces(not_expected, strip_spaces)
if collapse_spaces:
text = self._collapse_spaces(text)
not_expected = self._collapse_spaces(not_expected)
if text == not_expected:
if message is None:
message = f"The text of element '{locator}' was not supposed to be '{before_not_expected}'."
raise AssertionError(message)

def _strip_spaces(self, value, strip_spaces):
if not is_string(value):
return value
if not is_string(strip_spaces):
return value.strip() if strip_spaces else value
if strip_spaces.upper() == 'LEADING':
return value.lstrip()
if strip_spaces.upper() == 'TRAILING':
return value.rstrip()
return value.strip() if is_truthy(strip_spaces) else value

def _collapse_spaces(self, value):
return re.sub(r'\s+', ' ', value) if is_string(value) else value

@keyword
def get_element_attribute(
self, locator: Union[WebElement, str], attribute: str
Expand Down
Loading