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"',