Skip to content

Commit a6c6068

Browse files
authored
Merge pull request #1181 from seleniumbase/enhanced-shadow-dom-support
Enhanced Shadow-DOM support and more
2 parents b9fdbbc + daf55e6 commit a6c6068

File tree

11 files changed

+185
-17
lines changed

11 files changed

+185
-17
lines changed

.github/workflows/python-package.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ jobs:
1414
runs-on: ubuntu-18.04
1515
strategy:
1616
fail-fast: false
17-
max-parallel: 6
17+
max-parallel: 7
1818
matrix:
19-
python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9]
19+
python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10"]
2020

2121
steps:
2222
- uses: actions/checkout@v1

examples/ReadMe.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ pytest test_swag_labs.py --demo
4343

4444
<img src="https://seleniumbase.io/cdn/gif/swag_demo_3.gif" /><br />
4545

46+
Run a Wordle-solver example:
47+
48+
```bash
49+
pytest wordle_test.py
50+
```
51+
52+
<img src="https://seleniumbase.io/cdn/gif/wordle.gif" /><br />
53+
4654
Run an example test in Headless Mode: (invisible browser)
4755

4856
```bash

examples/wordle_test.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import ast
2+
import random
3+
import requests
4+
from seleniumbase import __version__
5+
from seleniumbase import BaseCase
6+
7+
8+
class WordleTests(BaseCase):
9+
10+
word_list = []
11+
12+
def initalize_word_list(self):
13+
js_file = "https://www.powerlanguage.co.uk/wordle/main.e65ce0a5.js"
14+
req_text = requests.get(js_file).text
15+
start = req_text.find("var La=") + len("var La=")
16+
end = req_text.find("],", start) + 1
17+
word_string = req_text[start:end]
18+
self.word_list = ast.literal_eval(word_string)
19+
20+
def modify_word_list(self, word, letter_status):
21+
new_word_list = []
22+
correct_letters = []
23+
present_letters = []
24+
for i in range(len(word)):
25+
if letter_status[i] == "correct":
26+
correct_letters.append(word[i])
27+
for w in self.word_list:
28+
if w[i] == word[i]:
29+
new_word_list.append(w)
30+
self.word_list = new_word_list
31+
new_word_list = []
32+
for i in range(len(word)):
33+
if letter_status[i] == "present":
34+
present_letters.append(word[i])
35+
for w in self.word_list:
36+
if word[i] in w and word[i] != w[i]:
37+
new_word_list.append(w)
38+
self.word_list = new_word_list
39+
new_word_list = []
40+
for i in range(len(word)):
41+
if (
42+
letter_status[i] == "absent"
43+
and word[i] not in correct_letters
44+
and word[i] not in present_letters
45+
):
46+
for w in self.word_list:
47+
if word[i] not in w:
48+
new_word_list.append(w)
49+
self.word_list = new_word_list
50+
new_word_list = []
51+
52+
def skip_if_incorrect_env(self):
53+
if self.headless:
54+
message = "This test doesn't run in headless mode!"
55+
print(message)
56+
self.skip(message)
57+
version = [int(i) for i in __version__.split(".") if i.isdigit()]
58+
if version < [2, 4, 0]:
59+
message = "This test requires SeleniumBase 2.4.0 or newer!"
60+
print(message)
61+
self.skip(message)
62+
63+
def test_wordle(self):
64+
self.skip_if_incorrect_env()
65+
self.open("https://www.powerlanguage.co.uk/wordle/")
66+
self.click("game-app::shadow game-modal::shadow game-icon")
67+
self.initalize_word_list()
68+
keyboard_base = "game-app::shadow game-keyboard::shadow "
69+
word = random.choice(self.word_list)
70+
total_attempts = 0
71+
success = False
72+
for attempt in range(6):
73+
total_attempts += 1
74+
word = random.choice(self.word_list)
75+
letters = []
76+
for letter in word:
77+
letters.append(letter)
78+
button = 'button[data-key="%s"]' % letter
79+
self.click(keyboard_base + button)
80+
button = 'button[data-key="↵"]'
81+
self.click(keyboard_base + button)
82+
self.sleep(1) # Time for the animation
83+
row = 'game-app::shadow game-row[letters="%s"]::shadow ' % word
84+
tile = row + "game-tile:nth-of-type(%s)"
85+
letter_status = []
86+
for i in range(1, 6):
87+
letter_eval = self.get_attribute(tile % str(i), "evaluation")
88+
letter_status.append(letter_eval)
89+
if letter_status.count("correct") == 5:
90+
success = True
91+
break
92+
self.word_list.remove(word)
93+
self.modify_word_list(word, letter_status)
94+
95+
self.save_screenshot_to_logs()
96+
print('\nWord: "%s"\nAttempts: %s' % (word.upper(), total_attempts))
97+
if not success:
98+
self.fail("Unable to solve for the correct word in 6 attempts!")
99+
self.sleep(3)

mkdocs_build/requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
regex>=2021.11.10
1+
regex>=2022.1.18
22
tqdm>=4.62.3
33
docutils==0.18.1
44
python-dateutil==2.8.2
@@ -14,7 +14,7 @@ click==8.0.3;python_version>="3.6"
1414
zipp==3.7.0;python_version>="3.7"
1515
readme-renderer==32.0
1616
pymdown-extensions==9.1;python_version>="3.6"
17-
importlib-metadata==4.10.0;python_version>="3.7"
17+
importlib-metadata==4.10.1;python_version>="3.7"
1818
bleach==4.1.0
1919
jsmin==3.0.1;python_version>="3.6"
2020
lunr==0.6.1;python_version>="3.6"

requirements.txt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ packaging>=21.3;python_version>="3.6"
55
setuptools>=44.1.1;python_version<"3.5"
66
setuptools>=50.3.2;python_version>="3.5" and python_version<"3.6"
77
setuptools>=59.6.0;python_version>="3.6" and python_version<"3.7"
8-
setuptools>=60.3.1;python_version>="3.7"
8+
setuptools>=60.5.0;python_version>="3.7"
99
setuptools-scm>=5.0.2;python_version<"3.6"
10-
setuptools-scm>=6.3.2;python_version>="3.6"
10+
setuptools-scm>=6.4.2;python_version>="3.6"
1111
tomli>=1.2.2;python_version>="3.6" and python_version<"3.7"
1212
tomli>=2.0.0;python_version>="3.7"
1313
wheel>=0.37.1
@@ -34,6 +34,7 @@ requests==2.25.1;python_version>="3.5" and python_version<"3.6"
3434
requests==2.27.1;python_version>="3.6"
3535
nose==1.3.7
3636
sniffio==1.2.0;python_version>="3.7"
37+
h11==0.13.0;python_version>="3.7"
3738
trio==0.19.0;python_version>="3.7"
3839
trio-websocket==0.9.2;python_version>="3.7"
3940
pyopenssl==21.0.0;python_version>="3.7"
@@ -49,7 +50,8 @@ filelock==3.2.1;python_version<"3.6"
4950
filelock==3.4.1;python_version>="3.6" and python_version<"3.7"
5051
filelock==3.4.2;python_version>="3.7"
5152
fasteners==0.16;python_version<"3.5"
52-
fasteners==0.16.3;python_version>="3.5"
53+
fasteners==0.16.3;python_version>="3.5" and python_version<"3.6"
54+
fasteners==0.17.2;python_version>="3.6"
5355
execnet==1.9.0
5456
pluggy==0.13.1;python_version<"3.6"
5557
pluggy==1.0.0;python_version>="3.6"

seleniumbase/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
from seleniumbase.__version__ import __version__ # noqa
2+
from seleniumbase.core.browser_launcher import get_driver # noqa
23
from seleniumbase.fixtures.base_case import BaseCase # noqa
34
from seleniumbase.masterqa.master_qa import MasterQA # noqa
45
from seleniumbase.common import decorators # noqa
56
from seleniumbase.common import encryption # noqa
7+
import collections
68
import sys
79

810
if sys.version_info[0] >= 3:
911
from seleniumbase import translate # noqa
12+
if sys.version_info >= (3, 10):
13+
collections.Callable = collections.abc.Callable # Lifeline for "nosetests"
14+
del collections # Undo "import collections" / Simplify "dir(seleniumbase)"
1015
del sys # Undo "import sys" / Simplify "dir(seleniumbase)"
16+
17+
version_info = [int(i) for i in __version__.split(".") if i.isdigit()] # noqa

seleniumbase/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
# seleniumbase package
2-
__version__ = "2.3.14"
2+
__version__ = "2.4.0"

seleniumbase/core/report_helper.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,9 @@ def process_failures(test, test_count, browser_type, duration):
4949
bad_page_image = "failure_%s.png" % test_count
5050
bad_page_data = "failure_%s.txt" % test_count
5151
screenshot_path = "%s/%s" % (LATEST_REPORT_DIR, bad_page_image)
52-
with open(screenshot_path, "wb") as file:
53-
file.write(test._last_page_screenshot)
52+
if hasattr(test, "_last_page_screenshot"):
53+
with open(screenshot_path, "wb") as file:
54+
file.write(test._last_page_screenshot)
5455
page_actions.save_test_failure_data(
5556
test.driver, bad_page_data, browser_type, folder=LATEST_REPORT_DIR
5657
)
@@ -69,6 +70,8 @@ def process_failures(test, test_count, browser_type, duration):
6970
exc_message = sys.last_value
7071
except Exception:
7172
exc_message = "(Unknown Exception)"
73+
if not hasattr(test, "_last_page_url"):
74+
test._last_page_url = "about:blank"
7275
return '"%s","%s","%s","%s","%s","%s","%s","%s","%s","%s"' % (
7376
test_count,
7477
"FAILED!",

seleniumbase/fixtures/base_case.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -834,22 +834,30 @@ def open_if_not_url(self, url):
834834
def is_element_present(self, selector, by=By.CSS_SELECTOR):
835835
self.wait_for_ready_state_complete()
836836
selector, by = self.__recalculate_selector(selector, by)
837+
if self.__is_shadow_selector(selector):
838+
return self.__is_shadow_element_present(selector)
837839
return page_actions.is_element_present(self.driver, selector, by)
838840

839841
def is_element_visible(self, selector, by=By.CSS_SELECTOR):
840842
self.wait_for_ready_state_complete()
841843
selector, by = self.__recalculate_selector(selector, by)
844+
if self.__is_shadow_selector(selector):
845+
return self.__is_shadow_element_visible(selector)
842846
return page_actions.is_element_visible(self.driver, selector, by)
843847

844848
def is_element_enabled(self, selector, by=By.CSS_SELECTOR):
845849
self.wait_for_ready_state_complete()
846850
selector, by = self.__recalculate_selector(selector, by)
851+
if self.__is_shadow_selector(selector):
852+
return self.__is_shadow_element_enabled(selector)
847853
return page_actions.is_element_enabled(self.driver, selector, by)
848854

849855
def is_text_visible(self, text, selector="html", by=By.CSS_SELECTOR):
850856
self.wait_for_ready_state_complete()
851857
time.sleep(0.01)
852858
selector, by = self.__recalculate_selector(selector, by)
859+
if self.__is_shadow_selector(selector):
860+
return self.__is_shadow_text_visible(text, selector)
853861
return page_actions.is_text_visible(self.driver, text, selector, by)
854862

855863
def is_attribute_present(
@@ -860,6 +868,10 @@ def is_attribute_present(
860868
self.wait_for_ready_state_complete()
861869
time.sleep(0.01)
862870
selector, by = self.__recalculate_selector(selector, by)
871+
if self.__is_shadow_selector(selector):
872+
return self.__is_shadow_attribute_present(
873+
selector, attribute, value
874+
)
863875
return page_actions.is_attribute_present(
864876
self.driver, selector, attribute, value, by
865877
)
@@ -1259,6 +1271,8 @@ def get_attribute(
12591271
selector, by = self.__recalculate_selector(selector, by)
12601272
self.wait_for_ready_state_complete()
12611273
time.sleep(0.01)
1274+
if self.__is_shadow_selector(selector):
1275+
return self.__get_shadow_attribute(selector, attribute)
12621276
element = page_actions.wait_for_element_present(
12631277
self.driver, selector, by, timeout
12641278
)
@@ -5834,7 +5848,7 @@ def skip(self, reason=""):
58345848
self.__passed_then_skipped = True
58355849
self.__will_be_skipped = True
58365850
sb_config._results[test_id] = "Skipped"
5837-
if self.with_db_reporting:
5851+
if hasattr(self, "with_db_reporting") and self.with_db_reporting:
58385852
if self.is_pytest:
58395853
self.__skip_reason = reason
58405854
else:
@@ -6004,6 +6018,10 @@ def __get_shadow_text(self, selector):
60046018
element = self.__get_shadow_element(selector)
60056019
return element.text
60066020

6021+
def __get_shadow_attribute(self, selector, attribute):
6022+
element = self.__get_shadow_element(selector)
6023+
return element.get_attribute(attribute)
6024+
60076025
def __wait_for_shadow_text_visible(self, text, selector):
60086026
start_ms = time.time() * 1000.0
60096027
stop_ms = start_ms + (settings.SMALL_TIMEOUT * 1000.0)
@@ -6132,6 +6150,36 @@ def __is_shadow_element_visible(self, selector):
61326150
except Exception:
61336151
return False
61346152

6153+
def __is_shadow_element_enabled(self, selector):
6154+
try:
6155+
element = self.__get_shadow_element(selector, timeout=0.1)
6156+
return element.is_enabled()
6157+
except Exception:
6158+
return False
6159+
6160+
def __is_shadow_text_visible(self, text, selector):
6161+
try:
6162+
element = self.__get_shadow_element(selector, timeout=0.1)
6163+
return element.is_displayed() and text in element.text
6164+
except Exception:
6165+
return False
6166+
6167+
def __is_shadow_attribute_present(self, selector, attribute, value=None):
6168+
try:
6169+
element = self.__get_shadow_element(selector, timeout=0.1)
6170+
found_value = element.get_attribute(attribute)
6171+
if found_value is None:
6172+
return False
6173+
if value is not None:
6174+
if found_value == value:
6175+
return True
6176+
else:
6177+
return False
6178+
else:
6179+
return True
6180+
except Exception:
6181+
return False
6182+
61356183
def __wait_for_shadow_element_present(self, selector):
61366184
element = self.__get_shadow_element(selector)
61376185
return element

seleniumbase/fixtures/page_actions.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,12 @@ def is_attribute_present(
124124
element = driver.find_element(by=by, value=selector)
125125
found_value = element.get_attribute(attribute)
126126
if found_value is None:
127-
raise Exception()
128-
127+
return False
129128
if value is not None:
130129
if found_value == value:
131130
return True
132131
else:
133-
raise Exception()
132+
return False
134133
else:
135134
return True
136135
except Exception:

0 commit comments

Comments
 (0)