diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
index 4b04c3fbb51..cb56fdac482 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/python-package.yml
@@ -14,9 +14,9 @@ jobs:
runs-on: ubuntu-18.04
strategy:
fail-fast: false
- max-parallel: 6
+ max-parallel: 7
matrix:
- python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9]
+ python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10"]
steps:
- uses: actions/checkout@v1
diff --git a/examples/ReadMe.md b/examples/ReadMe.md
index 215b664a29a..6c5f4e6427f 100755
--- a/examples/ReadMe.md
+++ b/examples/ReadMe.md
@@ -43,6 +43,14 @@ pytest test_swag_labs.py --demo

+Run a Wordle-solver example:
+
+```bash
+pytest wordle_test.py
+```
+
+
+
Run an example test in Headless Mode: (invisible browser)
```bash
diff --git a/examples/wordle_test.py b/examples/wordle_test.py
new file mode 100644
index 00000000000..65d0ca25d51
--- /dev/null
+++ b/examples/wordle_test.py
@@ -0,0 +1,99 @@
+import ast
+import random
+import requests
+from seleniumbase import __version__
+from seleniumbase import BaseCase
+
+
+class WordleTests(BaseCase):
+
+ word_list = []
+
+ def initalize_word_list(self):
+ js_file = "https://www.powerlanguage.co.uk/wordle/main.e65ce0a5.js"
+ req_text = requests.get(js_file).text
+ start = req_text.find("var La=") + len("var La=")
+ end = req_text.find("],", start) + 1
+ word_string = req_text[start:end]
+ self.word_list = ast.literal_eval(word_string)
+
+ def modify_word_list(self, word, letter_status):
+ new_word_list = []
+ correct_letters = []
+ present_letters = []
+ for i in range(len(word)):
+ if letter_status[i] == "correct":
+ correct_letters.append(word[i])
+ for w in self.word_list:
+ if w[i] == word[i]:
+ new_word_list.append(w)
+ self.word_list = new_word_list
+ new_word_list = []
+ for i in range(len(word)):
+ if letter_status[i] == "present":
+ present_letters.append(word[i])
+ for w in self.word_list:
+ if word[i] in w and word[i] != w[i]:
+ new_word_list.append(w)
+ self.word_list = new_word_list
+ new_word_list = []
+ for i in range(len(word)):
+ if (
+ letter_status[i] == "absent"
+ and word[i] not in correct_letters
+ and word[i] not in present_letters
+ ):
+ for w in self.word_list:
+ if word[i] not in w:
+ new_word_list.append(w)
+ self.word_list = new_word_list
+ new_word_list = []
+
+ def skip_if_incorrect_env(self):
+ if self.headless:
+ message = "This test doesn't run in headless mode!"
+ print(message)
+ self.skip(message)
+ version = [int(i) for i in __version__.split(".") if i.isdigit()]
+ if version < [2, 4, 0]:
+ message = "This test requires SeleniumBase 2.4.0 or newer!"
+ print(message)
+ self.skip(message)
+
+ def test_wordle(self):
+ self.skip_if_incorrect_env()
+ self.open("https://www.powerlanguage.co.uk/wordle/")
+ self.click("game-app::shadow game-modal::shadow game-icon")
+ self.initalize_word_list()
+ keyboard_base = "game-app::shadow game-keyboard::shadow "
+ word = random.choice(self.word_list)
+ total_attempts = 0
+ success = False
+ for attempt in range(6):
+ total_attempts += 1
+ word = random.choice(self.word_list)
+ letters = []
+ for letter in word:
+ letters.append(letter)
+ button = 'button[data-key="%s"]' % letter
+ self.click(keyboard_base + button)
+ button = 'button[data-key="↵"]'
+ self.click(keyboard_base + button)
+ self.sleep(1) # Time for the animation
+ row = 'game-app::shadow game-row[letters="%s"]::shadow ' % word
+ tile = row + "game-tile:nth-of-type(%s)"
+ letter_status = []
+ for i in range(1, 6):
+ letter_eval = self.get_attribute(tile % str(i), "evaluation")
+ letter_status.append(letter_eval)
+ if letter_status.count("correct") == 5:
+ success = True
+ break
+ self.word_list.remove(word)
+ self.modify_word_list(word, letter_status)
+
+ self.save_screenshot_to_logs()
+ print('\nWord: "%s"\nAttempts: %s' % (word.upper(), total_attempts))
+ if not success:
+ self.fail("Unable to solve for the correct word in 6 attempts!")
+ self.sleep(3)
diff --git a/mkdocs_build/requirements.txt b/mkdocs_build/requirements.txt
index 30a3a706f2e..2020433f46f 100644
--- a/mkdocs_build/requirements.txt
+++ b/mkdocs_build/requirements.txt
@@ -1,4 +1,4 @@
-regex>=2021.11.10
+regex>=2022.1.18
tqdm>=4.62.3
docutils==0.18.1
python-dateutil==2.8.2
@@ -14,7 +14,7 @@ click==8.0.3;python_version>="3.6"
zipp==3.7.0;python_version>="3.7"
readme-renderer==32.0
pymdown-extensions==9.1;python_version>="3.6"
-importlib-metadata==4.10.0;python_version>="3.7"
+importlib-metadata==4.10.1;python_version>="3.7"
bleach==4.1.0
jsmin==3.0.1;python_version>="3.6"
lunr==0.6.1;python_version>="3.6"
diff --git a/requirements.txt b/requirements.txt
index a02932b84a5..6d18d74d45b 100755
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,9 +5,9 @@ packaging>=21.3;python_version>="3.6"
setuptools>=44.1.1;python_version<"3.5"
setuptools>=50.3.2;python_version>="3.5" and python_version<"3.6"
setuptools>=59.6.0;python_version>="3.6" and python_version<"3.7"
-setuptools>=60.3.1;python_version>="3.7"
+setuptools>=60.5.0;python_version>="3.7"
setuptools-scm>=5.0.2;python_version<"3.6"
-setuptools-scm>=6.3.2;python_version>="3.6"
+setuptools-scm>=6.4.2;python_version>="3.6"
tomli>=1.2.2;python_version>="3.6" and python_version<"3.7"
tomli>=2.0.0;python_version>="3.7"
wheel>=0.37.1
@@ -34,6 +34,7 @@ requests==2.25.1;python_version>="3.5" and python_version<"3.6"
requests==2.27.1;python_version>="3.6"
nose==1.3.7
sniffio==1.2.0;python_version>="3.7"
+h11==0.13.0;python_version>="3.7"
trio==0.19.0;python_version>="3.7"
trio-websocket==0.9.2;python_version>="3.7"
pyopenssl==21.0.0;python_version>="3.7"
@@ -49,7 +50,8 @@ filelock==3.2.1;python_version<"3.6"
filelock==3.4.1;python_version>="3.6" and python_version<"3.7"
filelock==3.4.2;python_version>="3.7"
fasteners==0.16;python_version<"3.5"
-fasteners==0.16.3;python_version>="3.5"
+fasteners==0.16.3;python_version>="3.5" and python_version<"3.6"
+fasteners==0.17.2;python_version>="3.6"
execnet==1.9.0
pluggy==0.13.1;python_version<"3.6"
pluggy==1.0.0;python_version>="3.6"
diff --git a/seleniumbase/__init__.py b/seleniumbase/__init__.py
index 8653b775395..ff12ae270ac 100755
--- a/seleniumbase/__init__.py
+++ b/seleniumbase/__init__.py
@@ -1,10 +1,17 @@
from seleniumbase.__version__ import __version__ # noqa
+from seleniumbase.core.browser_launcher import get_driver # noqa
from seleniumbase.fixtures.base_case import BaseCase # noqa
from seleniumbase.masterqa.master_qa import MasterQA # noqa
from seleniumbase.common import decorators # noqa
from seleniumbase.common import encryption # noqa
+import collections
import sys
if sys.version_info[0] >= 3:
from seleniumbase import translate # noqa
+if sys.version_info >= (3, 10):
+ collections.Callable = collections.abc.Callable # Lifeline for "nosetests"
+del collections # Undo "import collections" / Simplify "dir(seleniumbase)"
del sys # Undo "import sys" / Simplify "dir(seleniumbase)"
+
+version_info = [int(i) for i in __version__.split(".") if i.isdigit()] # noqa
diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py
index 4b4c3b2a5b0..afcbf9c9608 100755
--- a/seleniumbase/__version__.py
+++ b/seleniumbase/__version__.py
@@ -1,2 +1,2 @@
# seleniumbase package
-__version__ = "2.3.14"
+__version__ = "2.4.0"
diff --git a/seleniumbase/core/report_helper.py b/seleniumbase/core/report_helper.py
index d10ac98f9cc..1d07d0cdbd4 100755
--- a/seleniumbase/core/report_helper.py
+++ b/seleniumbase/core/report_helper.py
@@ -49,8 +49,9 @@ def process_failures(test, test_count, browser_type, duration):
bad_page_image = "failure_%s.png" % test_count
bad_page_data = "failure_%s.txt" % test_count
screenshot_path = "%s/%s" % (LATEST_REPORT_DIR, bad_page_image)
- with open(screenshot_path, "wb") as file:
- file.write(test._last_page_screenshot)
+ if hasattr(test, "_last_page_screenshot"):
+ with open(screenshot_path, "wb") as file:
+ file.write(test._last_page_screenshot)
page_actions.save_test_failure_data(
test.driver, bad_page_data, browser_type, folder=LATEST_REPORT_DIR
)
@@ -69,6 +70,8 @@ def process_failures(test, test_count, browser_type, duration):
exc_message = sys.last_value
except Exception:
exc_message = "(Unknown Exception)"
+ if not hasattr(test, "_last_page_url"):
+ test._last_page_url = "about:blank"
return '"%s","%s","%s","%s","%s","%s","%s","%s","%s","%s"' % (
test_count,
"FAILED!",
diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py
index ff50042a2dc..c03d8188dee 100755
--- a/seleniumbase/fixtures/base_case.py
+++ b/seleniumbase/fixtures/base_case.py
@@ -834,22 +834,30 @@ def open_if_not_url(self, url):
def is_element_present(self, selector, by=By.CSS_SELECTOR):
self.wait_for_ready_state_complete()
selector, by = self.__recalculate_selector(selector, by)
+ if self.__is_shadow_selector(selector):
+ return self.__is_shadow_element_present(selector)
return page_actions.is_element_present(self.driver, selector, by)
def is_element_visible(self, selector, by=By.CSS_SELECTOR):
self.wait_for_ready_state_complete()
selector, by = self.__recalculate_selector(selector, by)
+ if self.__is_shadow_selector(selector):
+ return self.__is_shadow_element_visible(selector)
return page_actions.is_element_visible(self.driver, selector, by)
def is_element_enabled(self, selector, by=By.CSS_SELECTOR):
self.wait_for_ready_state_complete()
selector, by = self.__recalculate_selector(selector, by)
+ if self.__is_shadow_selector(selector):
+ return self.__is_shadow_element_enabled(selector)
return page_actions.is_element_enabled(self.driver, selector, by)
def is_text_visible(self, text, selector="html", by=By.CSS_SELECTOR):
self.wait_for_ready_state_complete()
time.sleep(0.01)
selector, by = self.__recalculate_selector(selector, by)
+ if self.__is_shadow_selector(selector):
+ return self.__is_shadow_text_visible(text, selector)
return page_actions.is_text_visible(self.driver, text, selector, by)
def is_attribute_present(
@@ -860,6 +868,10 @@ def is_attribute_present(
self.wait_for_ready_state_complete()
time.sleep(0.01)
selector, by = self.__recalculate_selector(selector, by)
+ if self.__is_shadow_selector(selector):
+ return self.__is_shadow_attribute_present(
+ selector, attribute, value
+ )
return page_actions.is_attribute_present(
self.driver, selector, attribute, value, by
)
@@ -1259,6 +1271,8 @@ def get_attribute(
selector, by = self.__recalculate_selector(selector, by)
self.wait_for_ready_state_complete()
time.sleep(0.01)
+ if self.__is_shadow_selector(selector):
+ return self.__get_shadow_attribute(selector, attribute)
element = page_actions.wait_for_element_present(
self.driver, selector, by, timeout
)
@@ -5834,7 +5848,7 @@ def skip(self, reason=""):
self.__passed_then_skipped = True
self.__will_be_skipped = True
sb_config._results[test_id] = "Skipped"
- if self.with_db_reporting:
+ if hasattr(self, "with_db_reporting") and self.with_db_reporting:
if self.is_pytest:
self.__skip_reason = reason
else:
@@ -6004,6 +6018,10 @@ def __get_shadow_text(self, selector):
element = self.__get_shadow_element(selector)
return element.text
+ def __get_shadow_attribute(self, selector, attribute):
+ element = self.__get_shadow_element(selector)
+ return element.get_attribute(attribute)
+
def __wait_for_shadow_text_visible(self, text, selector):
start_ms = time.time() * 1000.0
stop_ms = start_ms + (settings.SMALL_TIMEOUT * 1000.0)
@@ -6132,6 +6150,36 @@ def __is_shadow_element_visible(self, selector):
except Exception:
return False
+ def __is_shadow_element_enabled(self, selector):
+ try:
+ element = self.__get_shadow_element(selector, timeout=0.1)
+ return element.is_enabled()
+ except Exception:
+ return False
+
+ def __is_shadow_text_visible(self, text, selector):
+ try:
+ element = self.__get_shadow_element(selector, timeout=0.1)
+ return element.is_displayed() and text in element.text
+ except Exception:
+ return False
+
+ def __is_shadow_attribute_present(self, selector, attribute, value=None):
+ try:
+ element = self.__get_shadow_element(selector, timeout=0.1)
+ found_value = element.get_attribute(attribute)
+ if found_value is None:
+ return False
+ if value is not None:
+ if found_value == value:
+ return True
+ else:
+ return False
+ else:
+ return True
+ except Exception:
+ return False
+
def __wait_for_shadow_element_present(self, selector):
element = self.__get_shadow_element(selector)
return element
diff --git a/seleniumbase/fixtures/page_actions.py b/seleniumbase/fixtures/page_actions.py
index 674cd4fd5ab..e7f513d3cc2 100755
--- a/seleniumbase/fixtures/page_actions.py
+++ b/seleniumbase/fixtures/page_actions.py
@@ -124,13 +124,12 @@ def is_attribute_present(
element = driver.find_element(by=by, value=selector)
found_value = element.get_attribute(attribute)
if found_value is None:
- raise Exception()
-
+ return False
if value is not None:
if found_value == value:
return True
else:
- raise Exception()
+ return False
else:
return True
except Exception:
diff --git a/setup.py b/setup.py
index 77e85178132..e92ef476e42 100755
--- a/setup.py
+++ b/setup.py
@@ -130,9 +130,9 @@
'setuptools>=44.1.1;python_version<"3.5"',
'setuptools>=50.3.2;python_version>="3.5" and python_version<"3.6"',
'setuptools>=59.6.0;python_version>="3.6" and python_version<"3.7"',
- 'setuptools>=60.3.1;python_version>="3.7"',
+ 'setuptools>=60.5.0;python_version>="3.7"',
'setuptools-scm>=5.0.2;python_version<"3.6"',
- 'setuptools-scm>=6.3.2;python_version>="3.6"',
+ 'setuptools-scm>=6.4.2;python_version>="3.6"',
'tomli>=1.2.2;python_version>="3.6" and python_version<"3.7"',
'tomli>=2.0.0;python_version>="3.7"',
"wheel>=0.37.1",
@@ -159,6 +159,7 @@
'requests==2.27.1;python_version>="3.6"',
"nose==1.3.7",
'sniffio==1.2.0;python_version>="3.7"',
+ 'h11==0.13.0;python_version>="3.7"',
'trio==0.19.0;python_version>="3.7"',
'trio-websocket==0.9.2;python_version>="3.7"',
'pyopenssl==21.0.0;python_version>="3.7"',
@@ -174,7 +175,8 @@
'filelock==3.4.1;python_version>="3.6" and python_version<"3.7"',
'filelock==3.4.2;python_version>="3.7"',
'fasteners==0.16;python_version<"3.5"',
- 'fasteners==0.16.3;python_version>="3.5"',
+ 'fasteners==0.16.3;python_version>="3.5" and python_version<"3.6"',
+ 'fasteners==0.17.2;python_version>="3.6"',
"execnet==1.9.0",
'pluggy==0.13.1;python_version<"3.6"',
'pluggy==1.0.0;python_version>="3.6"',