Skip to content

Commit 00a181d

Browse files
authored
Merge pull request #681 from seleniumbase/support-the-contains-selector
Add support for the ":contains()" selector
2 parents ea28ff2 + b3ed66c commit 00a181d

File tree

10 files changed

+94
-41
lines changed

10 files changed

+94
-41
lines changed

help_docs/happy_customers.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* [MIT](https://web.mit.edu/)
1010
* [Harvard Medical School](https://hms.harvard.edu/)
1111
* [Mississippi State University](https://www.msstate.edu/)
12+
* [StreamSets](https://streamsets.com/)
1213
* [Akamai](https://www.akamai.com/)
1314
* [VMware](https://www.vmware.com/)
1415
* [Baidu](https://www.baidu.com/)
@@ -22,7 +23,7 @@
2223

2324
<h3>Case Study: (<i>HubSpot</i>)</h3>
2425

25-
In addition to using SeleniumBase for testing the UI of their content management system, HubSpot used SeleniumBase to automate the migration of website pages from one content management system to another, which saved them over one million US dollars and a significant amount of time.
26+
In addition to using SeleniumBase for testing the UI of their content management system, HubSpot used SeleniumBase to automate the migration of website pages from one content management system to another, which saved them over one million USD and a significant amount of time.
2627

2728
Learn how HubSpot uses SeleniumBase for website testing by reading: [Automated Testing with Selenium](https://dev.hubspot.com/blog/bid/88880/Automated-Integration-Testing-with-Selenium-at-HubSpot#hs_cos_wrapper_name)
2829

help_docs/method_summary.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,8 @@ self.inspect_html()
309309

310310
self.get_google_auth_password(totp_key=None)
311311

312+
self.convert_css_to_xpath(css)
313+
312314
self.convert_xpath_to_css(xpath)
313315

314316
self.convert_to_css_selector(selector, by)

integrations/github/ReadMe.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22

33
> **Table of Contents / Navigation:**
44
> - [**Actions/Workflows**](https://github.com/seleniumbase/SeleniumBase/blob/master/integrations/github/workflows/ReadMe.md)
5+
> - [**Extras/Action-Integrations**](https://github.com/seleniumbase/SeleniumBase/blob/master/integrations/github/workflows/extras.md)

integrations/github/workflows/ReadMe.md

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -51,35 +51,3 @@
5151
### Congratulations! You now know how to create and run browser tests with GitHub Actions/Workflows!
5252

5353
### **Study [SeleniumBase](https://github.com/seleniumbase/SeleniumBase) to learn more!**
54-
55-
----------
56-
----------
57-
58-
### **Integrations for GitHub Actions:**
59-
60-
### Slack Notifications - [rtCamp/action-slack-notify](https://github.com/rtCamp/action-slack-notify) can be used to send notifications to Slack.
61-
62-
**Usage:**
63-
* Create a slack integration webhook if you don't have one already.
64-
* Create a ``SLACK_WEBHOOK`` secret on your repository with the webhook token value.
65-
* For this particular action, ``SLACK_CHANNEL`` is an optional environment variable that defaults to the webhook token channel if not specified.
66-
* The following example shows how to put a link to your workflow as the ``SLACK_MESSAGE`` (Lets you see artifacts pushed up, such as from the SeleniumBase Presenter feature!):
67-
```
68-
- name: Slack notification
69-
uses: rtCamp/action-slack-notify@master
70-
env:
71-
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
72-
SLACK_CHANNEL: general
73-
SLACK_ICON_EMOJI: rocket
74-
SLACK_USERNAME: SeleniumBase
75-
SLACK_MESSAGE: 'Actions workflow completed successful! :tada: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}'
76-
```
77-
78-
### Uploading Artifacts:
79-
* Here's an example using [upload-artifact@v2](https://github.com/actions/upload-artifact) to push up a SeleniumBase-generated presentation as an artifact. (You can use this together with the Slack notification action to view the presentation directly from GitHub)
80-
```
81-
- uses: actions/upload-artifact@v2
82-
with:
83-
name: Click to download the presentation
84-
path: saved_presentations/my_presentation.html
85-
```
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
### <img src="https://seleniumbase.io/img/sb_icon.png" title="SeleniumBase" width="30" /> Integrations for GitHub Actions:
2+
3+
### Uploading Artifacts:
4+
* Here's an example using [upload-artifact@v2](https://github.com/actions/upload-artifact) to push up a SeleniumBase-generated artifact.
5+
```
6+
- uses: actions/upload-artifact@v2
7+
with:
8+
name: Click to download the presentation
9+
path: saved_presentations/my_presentation.html
10+
```
11+
12+
### Slack Notifications - [rtCamp/action-slack-notify](https://github.com/rtCamp/action-slack-notify) can be used to send notifications to Slack.
13+
14+
**Usage:**
15+
* Create a slack integration webhook if you don't have one already.
16+
* Create a ``SLACK_WEBHOOK`` secret on your repository with the webhook token value.
17+
* For this particular action, ``SLACK_CHANNEL`` is an optional environment variable that defaults to the webhook token channel if not specified.
18+
* The following example shows how to put a link to your workflow as the ``SLACK_MESSAGE`` (Lets you see artifacts pushed up, such as from the SeleniumBase Presenter feature!):
19+
```
20+
- name: Slack notification
21+
uses: rtCamp/action-slack-notify@master
22+
env:
23+
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
24+
SLACK_CHANNEL: general
25+
SLACK_ICON_EMOJI: rocket
26+
SLACK_USERNAME: SeleniumBase
27+
SLACK_MESSAGE: 'Actions workflow completed successful! :tada: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}'
28+
```

requirements.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
pip>=20.2.2
22
packaging>=20.4
33
setuptools>=44.1.1;python_version<"3.5"
4-
setuptools>=50.1.0;python_version>="3.5"
4+
setuptools>=50.2.0;python_version>="3.5"
55
setuptools-scm>=4.1.2
66
wheel>=0.35.1
77
six==1.15.0
@@ -17,6 +17,7 @@ selenium==3.141.0
1717
msedge-selenium-tools==3.141.2
1818
more-itertools==5.0.0;python_version<"3.5"
1919
more-itertools==8.5.0;python_version>="3.5"
20+
cssselect==1.1.0
2021
pluggy==0.13.1
2122
attrs>=20.1.0
2223
py==1.8.1;python_version<"3.5"
@@ -44,7 +45,7 @@ pyopenssl==19.1.0
4445
pygments==2.5.2;python_version<"3.5"
4546
pygments==2.6.1;python_version>="3.5"
4647
traitlets==4.3.3;python_version<"3.7"
47-
traitlets==5.0.2;python_version>="3.7"
48+
traitlets==5.0.3;python_version>="3.7"
4849
prompt-toolkit==1.0.18;python_version<"3.6"
4950
prompt-toolkit==3.0.7;python_version>="3.6"
5051
ipython==5.10.0;python_version<"3.5"

seleniumbase/fixtures/base_case.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def test_anything(self):
4545
from seleniumbase.core import log_helper
4646
from seleniumbase.core import tour_helper
4747
from seleniumbase.fixtures import constants
48+
from seleniumbase.fixtures import css_to_xpath
4849
from seleniumbase.fixtures import js_utils
4950
from seleniumbase.fixtures import page_actions
5051
from seleniumbase.fixtures import page_utils
@@ -1429,8 +1430,8 @@ def __select_option(self, dropdown_selector, option,
14291430
timeout = settings.SMALL_TIMEOUT
14301431
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
14311432
timeout = self.__get_new_timeout(timeout)
1432-
if page_utils.is_xpath_selector(dropdown_selector):
1433-
dropdown_by = By.XPATH
1433+
dropdown_selector, dropdown_by = self.__recalculate_selector(
1434+
dropdown_selector, dropdown_by)
14341435
self.wait_for_ready_state_complete()
14351436
element = self.wait_for_element_present(
14361437
dropdown_selector, by=dropdown_by, timeout=timeout)
@@ -2984,6 +2985,9 @@ def get_google_auth_password(self, totp_key=None):
29842985
totp = pyotp.TOTP(totp_key)
29852986
return str(totp.now())
29862987

2988+
def convert_css_to_xpath(self, css):
2989+
return css_to_xpath.convert_css_to_xpath(css)
2990+
29872991
def convert_xpath_to_css(self, xpath):
29882992
return xpath_to_css.convert_xpath_to_css(xpath)
29892993

@@ -5902,6 +5906,9 @@ def __recalculate_selector(self, selector, by):
59025906
name = page_utils.get_name_from_selector(selector)
59035907
selector = '[name="%s"]' % name
59045908
by = By.CSS_SELECTOR
5909+
if ":contains(" in selector and by == By.CSS_SELECTOR:
5910+
selector = self.convert_css_to_xpath(selector)
5911+
by = By.XPATH
59055912
return (selector, by)
59065913

59075914
def __looks_like_a_page_url(self, url):

seleniumbase/fixtures/css_to_xpath.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""
2+
Convert CSS selectors into XPath selectors
3+
"""
4+
5+
from cssselect import GenericTranslator
6+
7+
8+
def convert_css_to_xpath(css):
9+
""" Convert CSS Selectors to XPath Selectors.
10+
Example:
11+
convert_css_to_xpath('button:contains("Next")')
12+
Output => "//button[contains(., 'Next')]"
13+
"""
14+
xpath = GenericTranslator().css_to_xpath(css, prefix='//')
15+
return xpath

seleniumbase/fixtures/xpath_to_css.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def _filter_xpath_grouping(xpath):
7676

7777
def _get_raw_css_from_xpath(xpath):
7878
css = ""
79+
attr = ""
7980
position = 0
8081

8182
while position < len(xpath):
@@ -108,7 +109,9 @@ def _get_raw_css_from_xpath(xpath):
108109
attr = '[%s*="%s"]' % (match['cattr'].replace("@", ""),
109110
match['cvalue'])
110111
elif match['cattr'] == "text()":
111-
attr = ":contains(%s)" % match['cvalue']
112+
attr = ':contains("%s")' % match['cvalue']
113+
elif match['cattr'] == ".":
114+
attr = ':contains("%s")' % match['cvalue']
112115
else:
113116
attr = ""
114117

@@ -128,6 +131,20 @@ def _get_raw_css_from_xpath(xpath):
128131
def convert_xpath_to_css(xpath):
129132
if xpath[0] != '"' and xpath[-1] != '"' and xpath.count('"') % 2 == 0:
130133
xpath = _handle_brackets_in_strings(xpath)
134+
xpath = xpath.replace("descendant-or-self::*/", "descORself/")
135+
xpath = xpath.replace(" = '", "='")
136+
if " and contains(@" in xpath and xpath.count(" and contains(@") == 1:
137+
spot1 = xpath.find(" and contains(@")
138+
spot1 = spot1 + len(" and contains(@")
139+
spot2 = xpath.find(",", spot1)
140+
attr = xpath[spot1:spot2]
141+
swap = " and contains(@%s, " % attr
142+
if swap in xpath:
143+
swap_spot = xpath.find(swap)
144+
close_paren = xpath.find(']', swap_spot) - 1
145+
if close_paren > 1:
146+
xpath = xpath[:close_paren] + xpath[close_paren+1:]
147+
xpath = xpath.replace(swap, "_STAR_=")
131148

132149
if xpath.startswith('('):
133150
xpath = _filter_xpath_grouping(xpath)
@@ -149,4 +166,16 @@ def convert_xpath_to_css(xpath):
149166
css = css.replace('_STR_L_bracket_', '\\[')
150167
css = css.replace('_STR_R_bracket_', '\\]')
151168

169+
# Handle a lot of edge cases with conversion
170+
css = css.replace(" > descORself > ", ' ')
171+
css = css.replace(" descORself > ", ' ')
172+
css = css.replace("/descORself/*", ' ')
173+
css = css.replace("/descORself/", ' ')
174+
css = css.replace("descORself/", ' ')
175+
css = css.replace("_STAR_=", "*=")
176+
css = css.replace("]/", "] ")
177+
css = css.replace("] *[", "] > [")
178+
css = css.replace("\'", '"')
179+
css = css.replace("[@", '[')
180+
152181
return css

setup.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454

5555
setup(
5656
name='seleniumbase',
57-
version='1.49.1',
57+
version='1.49.2',
5858
description='Web Automation and Test Framework - https://seleniumbase.io',
5959
long_description=long_description,
6060
long_description_content_type='text/markdown',
@@ -93,7 +93,7 @@
9393
'pip>=20.2.2',
9494
'packaging>=20.4',
9595
'setuptools>=44.1.1;python_version<"3.5"',
96-
'setuptools>=50.1.0;python_version>="3.5"',
96+
'setuptools>=50.2.0;python_version>="3.5"',
9797
'setuptools-scm',
9898
'wheel>=0.35.1',
9999
'six',
@@ -109,6 +109,7 @@
109109
'msedge-selenium-tools==3.141.2',
110110
'more-itertools==5.0.0;python_version<"3.5"',
111111
'more-itertools==8.5.0;python_version>="3.5"',
112+
'cssselect==1.1.0',
112113
'pluggy==0.13.1',
113114
'attrs>=20.1.0',
114115
'py==1.8.1;python_version<"3.5"',
@@ -136,7 +137,7 @@
136137
'pygments==2.5.2;python_version<"3.5"',
137138
'pygments==2.6.1;python_version>="3.5"',
138139
'traitlets==4.3.3;python_version<"3.7"',
139-
'traitlets==5.0.2;python_version>="3.7"',
140+
'traitlets==5.0.3;python_version>="3.7"',
140141
'ipython==5.10.0;python_version<"3.5"',
141142
'prompt-toolkit==1.0.18;python_version<"3.6"',
142143
'prompt-toolkit==3.0.7;python_version>="3.6"',

0 commit comments

Comments
 (0)