diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000000..b8ee3a6a18f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +.git/ +.gitignore +.idea/ +.mypy_cache/ +.pytest_cache/ +.tox/ +.venv/ +.vscode/ +*.egg-info/ +*.log +*.py[cod] +__pycache__/ +build/ +dist/ +help_docs/ +node_modules/ +site/ diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000000..9719aee6e83 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: mdmintz diff --git a/.github/Workflows.md b/.github/Workflows.md new file mode 100755 index 00000000000..4cf274112d3 --- /dev/null +++ b/.github/Workflows.md @@ -0,0 +1,4 @@ +### SeleniumBase Workflows + +> **Table of Contents / Navigation:** +> - [**CI build**](workflows/python-package.yml) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 00000000000..d8ee75bff16 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,52 @@ +# Build and deploy a Jekyll site to GitHub Pages +name: Deploy docs to GitHub Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["master"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Build job + build: + if: github.repository == 'seleniumbase/SeleniumBase' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Build with Jekyll + uses: actions/jekyll-build-pages@v1 + with: + source: ./ + destination: ./_site + - name: Upload artifact + uses: actions/upload-pages-artifact@v4 + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + if: github.repository == 'seleniumbase/SeleniumBase' + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/python-nightly-mac.yml b/.github/workflows/python-nightly-mac.yml new file mode 100644 index 00000000000..483ae84f826 --- /dev/null +++ b/.github/workflows/python-nightly-mac.yml @@ -0,0 +1,65 @@ +name: Nightly Tests (macOS) + +on: + schedule: + - cron: "40 1 * * *" + workflow_dispatch: + branches: + +jobs: + build: + + env: + PY_COLORS: "1" + runs-on: macos-latest + strategy: + fail-fast: false + max-parallel: 6 + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] + + steps: + - uses: actions/checkout@v6 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade wheel + pip install -r requirements.txt + - name: Install SeleniumBase + run: | + pip install . + - name: Check the console scripts interface + run: | + seleniumbase + sbase + - name: Install chromedriver + run: | + seleniumbase install chromedriver + - name: Make sure pytest is working + run: | + echo "def test_1(): pass" > nothing.py + pytest nothing.py + - name: Make sure nosetests is working + run: | + echo "def test_2(): pass" > nothing2.py + nosetests nothing2.py + - name: Run pytest examples/unit_tests/verify_framework.py --browser=chrome --headless + run: | + pytest examples/unit_tests/verify_framework.py --browser=chrome --headless -v -s --junit-xml=junit/test-results.xml + - name: Run pytest examples/offline_examples --browser=chrome --headless --rs + run: | + pytest examples/offline_examples --rs --browser=chrome --headless -v -s --junit-xml=junit/test-results.xml + - name: Run pytest examples/boilerplate_test.py --browser=chrome --headless + run: | + pytest examples/boilerplates/boilerplate_test.py --browser=chrome --headless -v -s --junit-xml=junit/test-results.xml + - name: Run pytest examples/test_window_switching.py --browser=chrome --headless + run: | + pytest examples/test_window_switching.py --browser=chrome --headless -v -s --junit-xml=junit/test-results.xml + - name: Verify seleniumbase install from PyPI + run: | + pip install seleniumbase -U --no-deps --force-reinstall --no-cache-dir diff --git a/.github/workflows/python-nightly-ubuntu.yml b/.github/workflows/python-nightly-ubuntu.yml new file mode 100644 index 00000000000..562c8e7c21d --- /dev/null +++ b/.github/workflows/python-nightly-ubuntu.yml @@ -0,0 +1,65 @@ +name: Nightly Tests (Ubuntu) + +on: + schedule: + - cron: "30 1 * * *" + workflow_dispatch: + branches: + +jobs: + build: + + env: + PY_COLORS: "1" + runs-on: ubuntu-latest + strategy: + fail-fast: false + max-parallel: 6 + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] + + steps: + - uses: actions/checkout@v6 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade wheel + pip install -r requirements.txt + - name: Install SeleniumBase + run: | + pip install . + - name: Check the console scripts interface + run: | + seleniumbase + sbase + - name: Install chromedriver + run: | + seleniumbase install chromedriver + - name: Make sure pytest is working + run: | + echo "def test_1(): pass" > nothing.py + pytest nothing.py + - name: Make sure nosetests is working + run: | + echo "def test_2(): pass" > nothing2.py + nosetests nothing2.py + - name: Run pytest examples/unit_tests/verify_framework.py --browser=chrome --headless + run: | + pytest examples/unit_tests/verify_framework.py --browser=chrome --headless -v -s --junit-xml=junit/test-results.xml + - name: Run pytest examples/offline_examples --browser=chrome --headless --rs + run: | + pytest examples/offline_examples --rs --browser=chrome --headless -v -s --junit-xml=junit/test-results.xml + - name: Run pytest examples/boilerplate_test.py --browser=chrome --headless + run: | + pytest examples/boilerplates/boilerplate_test.py --browser=chrome --headless -v -s --junit-xml=junit/test-results.xml + - name: Run pytest examples/test_window_switching.py --browser=chrome --headless + run: | + pytest examples/test_window_switching.py --browser=chrome --headless -v -s --junit-xml=junit/test-results.xml + - name: Verify seleniumbase install from PyPI + run: | + pip install seleniumbase -U --no-deps --force-reinstall --no-cache-dir diff --git a/.github/workflows/python-nightly-windows.yml b/.github/workflows/python-nightly-windows.yml new file mode 100644 index 00000000000..8dcbb9fb103 --- /dev/null +++ b/.github/workflows/python-nightly-windows.yml @@ -0,0 +1,68 @@ +name: Nightly Tests (Windows) + +on: + schedule: + - cron: "50 1 * * *" + workflow_dispatch: + branches: + +jobs: + build: + + env: + PY_COLORS: "1" + runs-on: windows-latest + strategy: + fail-fast: false + max-parallel: 6 + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] + + steps: + - uses: actions/checkout@v6 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade wheel + pip install -r requirements.txt + - name: Install SeleniumBase + run: | + pip install . + - name: Check the console scripts interface + run: | + seleniumbase + sbase + - name: Install chromedriver + run: | + seleniumbase install chromedriver + - name: Get chrome-headless-shell + run: | + sbase get chs + - name: Make sure pytest is working + run: | + echo "def test_1(): pass" > nothing.py + pytest nothing.py + - name: Make sure nosetests is working + run: | + echo "def test_2(): pass" > nothing2.py + nosetests nothing2.py + - name: Run pytest examples/unit_tests/verify_framework.py --browser=chrome --headless + run: | + pytest examples/unit_tests/verify_framework.py --browser=chrome --headless -v -s --junit-xml=junit/test-results.xml + - name: Run pytest examples/offline_examples --browser=chrome --headless --rs + run: | + pytest examples/offline_examples --rs --browser=chrome --headless -v -s --junit-xml=junit/test-results.xml + - name: Run pytest examples/boilerplate_test.py --browser=chrome --headless + run: | + pytest examples/boilerplates/boilerplate_test.py --browser=chrome --headless -v -s --junit-xml=junit/test-results.xml + - name: Run pytest examples/test_window_switching.py --browser=chrome --headless + run: | + pytest examples/test_window_switching.py --browser=chrome --headless -v -s --junit-xml=junit/test-results.xml + - name: Verify seleniumbase install from PyPI + run: | + pip install seleniumbase -U --no-deps --force-reinstall --no-cache-dir diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 00000000000..415b346614e --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,94 @@ +name: Tests + +on: + pull_request: + branches: + push: + branches: + - master + workflow_dispatch: + branches: + +jobs: + build: + + env: + PY_COLORS: "1" + runs-on: ubuntu-latest + strategy: + fail-fast: false + max-parallel: 6 + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] + + steps: + - uses: actions/checkout@v6 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + - name: Install dependencies + run: | + python -m pip install --upgrade pip wheel setuptools + pip install -r requirements.txt + - name: Install SeleniumBase + run: | + pip install . + - name: Lint with flake8 + run: | + pip install flake8 + # Stop the build if there are flake8 issues + flake8 . --count --show-source --statistics --exclude=temp + - name: Install Chrome + run: | + sudo apt install google-chrome-stable + - name: Check the console scripts interface + run: | + seleniumbase + sbase + - name: Install chromedriver + run: | + seleniumbase install chromedriver + - name: Make sure pytest is working + run: | + pytest --help + echo "def test_1(): pass" > nothing.py + pytest nothing.py + - name: Make sure nosetests is working + run: | + echo "def test_2(): pass" > nothing2.py + nosetests nothing2.py + - name: Run pytest examples/unit_tests/verify_framework.py --browser=chrome --headless + run: | + pytest examples/unit_tests/verify_framework.py --browser=chrome --headless -v -s --junit-xml=junit/test-results.xml + - name: Run pytest examples/boilerplate_test.py --browser=chrome --headless + run: | + pytest examples/boilerplates/boilerplate_test.py --browser=chrome --headless -v -s --junit-xml=junit/test-results.xml + - name: Run pytest examples/test_demo_site.py --browser=chrome --xvfb + run: | + pytest examples/test_demo_site.py --browser=chrome --xvfb -v -s --junit-xml=junit/test-results.xml + - name: Run pytest examples/iframe_tests.py --browser=chrome --xvfb --rs --crumbs + run: | + pytest examples/iframe_tests.py --browser=chrome --xvfb --rs --crumbs -v -s --junit-xml=junit/test-results.xml + - name: Run pytest examples/test_mfa_login.py --browser=chrome --xvfb + run: | + pytest examples/test_mfa_login.py --browser=chrome --xvfb -v -s --junit-xml=junit/test-results.xml + - name: Run pytest examples/iframe_tests.py --browser=chrome --xvfb --rs + run: | + pytest examples/iframe_tests.py --browser=chrome --xvfb --rs -v -s --junit-xml=junit/test-results.xml + - name: Run pytest examples/test_window_switching.py --browser=chrome --headless + run: | + pytest examples/test_window_switching.py --browser=chrome --headless -v -s --junit-xml=junit/test-results.xml + - name: Run pytest examples/my_first_test.py --browser=chrome --headless + run: | + pytest examples/my_first_test.py --browser=chrome --headless -v -s --junit-xml=junit/test-results.xml + - name: Run pytest examples/test_inspect_html.py --browser=chrome --headless + run: | + pytest examples/test_inspect_html.py --browser=chrome --headless -v -s --junit-xml=junit/test-results.xml + - name: Run behave examples/behave_bdd/features/calculator.feature -D rs -D crumbs -D xvfb + run: | + behave examples/behave_bdd/features/calculator.feature -D rs -D crumbs -D xvfb -T -k + - name: Run behave examples/behave_bdd/features/realworld.feature -D rs -D crumbs -D xvfb + run: | + behave examples/behave_bdd/features/realworld.feature -D rs -D crumbs -D xvfb -T -k diff --git a/.gitignore b/.gitignore index cc79719f1c7..af3cf531762 100644 --- a/.gitignore +++ b/.gitignore @@ -4,21 +4,35 @@ # Packages *.egg *.egg-info -dist -build -ghostdriver.log +.eggs eggs -parts -bin -var -sdist develop-eggs -.installed.cfg +bin +build +dist lib lib64 +parts +sdist +var +.installed.cfg __pycache__ # Python3 pyvenv +.env +.venv +env/ +venv/ +ENV/ +VENV/ +env.bak/ +venv.bak/ +.sbase +.sbase* +seleniumbase_env +seleniumbase_venv +sbase_env +sbase_venv pyvenv.cfg .Python include @@ -26,6 +40,7 @@ pip-delete-this-directory.txt pip-selfcheck.json ipython.1.gz nosetests.1 +.noseids # Installer logs pip-log.txt @@ -34,6 +49,7 @@ pip-log.txt # Unit test / coverage reports .coverage .tox +coverage.xml nosetests.xml # py.test @@ -54,12 +70,58 @@ test-results.xml # Web Drivers chromedriver geckodriver +msedgedriver operadriver +uc_driver MicrosoftWebDriver.exe +headless_ie_selenium.exe IEDriverServer.exe chromedriver.exe geckodriver.exe +msedgedriver.exe operadriver.exe +uc_driver.exe + +# Chromium Zip Files +chrome-mac.zip +chrome-linux.zip +chrome-win.zip + +# Chromium folders +chrome-mac +chrome-linux +chrome-win + +# Chrome for Testing Zip Files +chrome-mac-arm64.zip +chrome-mac-x64.zip +chrome-linux64.zip +chrome-win64.zip +chrome-win32.zip + +# Chrome for Testing folders +chrome-mac-arm64 +chrome-mac-x64 +chrome-linux64 +chrome-win64 +chrome-win32 + +# Chrome-Headless-Shell Zip Files +chrome-headless-shell-mac-arm64.zip +chrome-headless-shell-mac-x64.zip +chrome-headless-shell-linux64.zip +chrome-headless-shell-win64.zip +chrome-headless-shell-win32.zip + +# Chrome-Headless-Shell folders +chrome-headless-shell-mac-arm64 +chrome-headless-shell-mac-x64 +chrome-headless-shell-linux64 +chrome-headless-shell-win64 +chrome-headless-shell-win32 + +# msedgedriver requirements +libc++.dylib # Logs logs @@ -67,27 +129,66 @@ latest_logs log_archives archived_logs geckodriver.log +ghostdriver.log +pytestdebug.log # Reports +reports/*.xml latest_report report_archives archived_reports html_report.html +last_report.html report.html report.xml +# Dashboard +dashboard.html +dashboard.json +dash_pie.json +dashboard.lock + +# Allure Reports / Results +allure_report +allure-report +allure_results +allure-results + +# Charts +saved_charts + +# Presentations +saved_presentations + # Tours tours_exported # Images images_exported +# Cookies +saved_cookies + +# Recordings +recordings + # Automated Visual Testing visual_baseline +# MkDocs WebSite Generator +site/* +mkdocs_build/*.md +mkdocs_build/*/*.md +mkdocs_build/*/*/*.md +mkdocs_build/*/*/*/*.md + +# macOS system files +.DS_Store + # Other selenium-server-standalone.jar proxy.zip +proxy.lock verbose_hub_server.dat verbose_node_server.dat ip_of_grid_hub.dat @@ -95,5 +196,5 @@ downloaded_files archived_files assets temp -temp* +temp_*/ node_modules diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 68c4fbee6a8..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,42 +0,0 @@ -language: python -sudo: false -matrix: - include: - - python: 2.7 - - python: 3.6 -addons: - firefox: "62.0.3" - chrome: stable -before_install: - - "sudo mysql -e 'CREATE DATABASE IF NOT EXISTS test_db;'" - - "sudo mysql -h 127.0.0.1 -u root test_db < seleniumbase/core/create_db_tables.sql" - - "mysqladmin -u root password test" - # - "mysqladmin -u root -p'old_password' password new_password" - - "sudo service mysql restart" -install: - - "pip install --upgrade pip" - - "pip install -r requirements.txt --upgrade" - - "pip install mysqlclient==1.4.2" - - "python setup.py develop" - - "sudo rm -f /etc/boto.cfg" -before_script: - - "flake8 seleniumbase/*.py && flake8 seleniumbase/*/*.py && flake8 seleniumbase/*/*/*.py && flake8 seleniumbase/*/*/*/*.py" - # - "wget https://chromedriver.storage.googleapis.com/2.40/chromedriver_linux64.zip && unzip chromedriver_linux64.zip && sudo cp chromedriver /usr/local/bin/ && sudo chmod +x /usr/local/bin/chromedriver" - # - "wget https://github.com/mozilla/geckodriver/releases/download/v0.21.0/geckodriver-v0.21.0-linux64.tar.gz -O /tmp/geckodriver.tar.gz && tar -C /opt -xzf /tmp/geckodriver.tar.gz && sudo chmod 755 /opt/geckodriver && sudo ln -fs /opt/geckodriver /usr/bin/geckodriver && sudo ln -fs /opt/geckodriver /usr/local/bin/geckodriver" - # - "wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 && tar -xvf ./phantomjs-2.1.1-linux-x86_64.tar.bz2 && export PATH=$PWD/phantomjs-2.1.1-linux-x86_64/bin:$PATH" - - "seleniumbase" - - "seleniumbase install chromedriver" - - "seleniumbase install geckodriver" - - "seleniumbase mkdir browser_tests" -script: - - "pytest browser_tests/boilerplates/boilerplate_test.py --headless --with-db_reporting" - - "pytest examples/my_first_test.py --browser=chrome -s --headless --with-db_reporting" - - "nosetests examples/boilerplates/boilerplate_test.py --browser=chrome --headless" - - "pytest examples/my_first_test.py --browser=firefox -s --headless --with-db_reporting" - - "pytest examples/my_first_test.py --browser=chrome -s --headless --with-db_reporting --demo_mode --demo_sleep=0.2" - - "pytest examples/tour_examples/google_tour.py -s --headless --with-db_reporting" - - "sudo mysql --password=test -e 'select test_address,browser,state,start_time,runtime from test_db.test_run_data'" -#after_script: -# - "sudo mysql -e 'DROP DATABASE test_db;'" -notifications: - email: false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100755 index 00000000000..cb17cfac134 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +

CHANGELOG

+ +## See: [SeleniumBase/releases](https://github.com/seleniumbase/SeleniumBase/releases) ๐Ÿ—‚๏ธ ๐Ÿ“‹ + +### (For CDP Mode, see the [CDP Mode docs](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/ReadMe.md)) + +### (For Stealthy Playwright, see the [Stealthy Playwright docs](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/playwright/ReadMe.md)) diff --git a/CNAME b/CNAME index eafccf498b9..baba10d1478 100644 --- a/CNAME +++ b/CNAME @@ -1 +1 @@ -seleniumbase.com \ No newline at end of file +seleniumbase.dev \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100755 index 00000000000..0e5097520ee --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,23 @@ +# Code of conduct + +(SeleniumBase uses a modified version of [Flutter's Code of conduct](https://github.com/flutter/flutter/blob/master/CODE_OF_CONDUCT.md)) + +The SeleniumBase project expects SeleniumBase's contributors to act professionally and respectfully. SeleniumBase contributors are expected to maintain the safety and dignity of SeleniumBase's social environments (such as GitHub and Gitter). + +Specifically: + +* Respect people, their identities, their culture, and their work. +* Be kind. Be courteous. Be welcoming. +* Listen. Consider and acknowledge people's points before responding. + +Should you experience anything that makes you feel unwelcome in SeleniumBase's community, please [contact us](https://gitter.im/seleniumbase/SeleniumBase). + +The SeleniumBase project will not tolerate harassment in SeleniumBase's community, even outside of SeleniumBase's public communication channels. + +## Questions + +It's always OK to ask questions. Seleniumbase is a big project, and we don't expect everyone to know everything about everything. + +!["I try not to make fun of people for admitting they don't know things, because for each thing 'everyone knows' by the time they're adults, every day there are, on average, 10,000 people in the US hearing about it for the first time. If I make fun of people, I train them not to tell me when they have those moments. And I miss out on the fun." "Diet coke and mentos thing? What's that?" "Oh, man! We're going to the grocery store." "Why?" "You're one of today's lucky 10,000."](https://imgs.xkcd.com/comics/ten_thousand.png) + +Source: _[xkcd, May 2012](https://xkcd.com/1053/)_ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100755 index 00000000000..2731366e7e9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# Contributing to SeleniumBase + +The SeleniumBase project welcomes meaningful contributions. + +There are many ways to help: + +## Bug Reports + +When opening a new issue or commenting on an existing issue, please make sure to provide concise, detailed instructions on how to reproduce the issue. If the issue can't be reproduced, it will be closed. Clearly describe the results you're seeing, and the results you're expecting. + +## Feature Requests + +If you find that SeleniumBase is missing something, feel free to open an issue with details describing what feature(s) you'd like added or changed. + +## Documentation + +SeleniumBase is a big software project, and documentation is key to +understanding how it works and how to use it properly. If you feel that important documentation is missing, please let us know, or submit a pull request. + +## Code Contributions + +The SeleniumBase project welcomes meaningful contributions. Given the complexity of the project, it may be easier to open an issue for a change you want made than to try implementing the change yourself. + +Recently, we've been moving toward the policy discussed in https://github.com/readme/featured/how-open-is-open-source, which is: "Open-Source, not Open-Contribution". As the article states, even small contributions (via pull requests) typically require hours of time to properly test and validate. We're still grateful for community involvement and when folks report bugs or suggest features. With some special exceptions, this project is mainly closed to contribution (via pull requests). + +## (A Note on Style Guide Rules) + +[flake8](https://github.com/PyCQA/flake8) is the law of the land. The only flake8 rule ignored is [W503](https://github.com/grantmcconnaughey/Flake8Rules/blob/master/_rules/W503.md). For more details on why W503 should be ignored, see [this explanation](https://peps.python.org/pep-0008/#should-a-line-break-before-or-after-a-binary-operator), or [this shorter explanation](https://github.com/PyCQA/flake8/issues/494) by Python expert [Anthony Sottile](https://github.com/asottile). + +-------- + +For questions about this document, reach out to [Michael Mintz](https://github.com/mdmintz). diff --git a/Dockerfile b/Dockerfile index b00e143a2ff..07b721c9f4d 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,85 +1,155 @@ # SeleniumBase Docker Image -FROM ubuntu:18.04 +FROM ubuntu:24.04 +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +ENV PYTHONUNBUFFERED=1 +ENV PYTHONIOENCODING=UTF-8 +ENV DEBIAN_FRONTEND=noninteractive -#======================================= -# Install Python and Basic Python Tools -#======================================= -RUN apt-get update && apt-get install -y python python-pip python-setuptools python-dev python-distribute +#====================== +# Locale Configuration +#====================== +RUN apt-get update +RUN apt-get install -y --no-install-recommends tzdata locales +RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen +ENV TZ=America/New_York +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone +ENV LANG=en_US.UTF-8 +ENV LANGUAGE=en_US:en +ENV LC_ALL=en_US.UTF-8 +RUN echo "LC_ALL=en_US.UTF-8" >> /etc/environment +RUN echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen +RUN echo "LANG=en_US.UTF-8" > /etc/locale.conf +RUN locale-gen en_US.UTF-8 + +#=========================== +# Fingerprint Configuration +#=========================== +RUN apt-get update +RUN apt install -y fonts-liberation fonts-noto-color-emoji libvulkan1 libnss3 libatk-bridge2.0-0 libcups2 libxcomposite1 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 +RUN apt install -y fonts-freefont-ttf fonts-dejavu-core fonts-ubuntu fonts-roboto fonts-droid-fallback + +#====================== +# Install Common Fonts +#====================== +RUN apt-get update +RUN apt-get install -y \ + fonts-liberation2 \ + fonts-font-awesome \ + fonts-terminus \ + fonts-powerline \ + fonts-open-sans \ + fonts-mononoki \ + fonts-lato + +#============================ +# Install Linux Dependencies +#============================ +RUN apt-get update +RUN apt-get install -y \ + dbus-x11 \ + libatk1.0-0 \ + libatspi2.0-0 \ + libdbus-1-3 \ + libdrm2 \ + libgtk-3-0 \ + libnspr4 \ + libasound2t64 \ + libu2f-udev \ + libwayland-client0 \ + libx11-6 \ + libx11-xcb1 \ + libxdamage1 \ + libxfixes3 \ + libxkbcommon0 + +#========================== +# Install useful utilities +#========================== +RUN apt-get update +RUN apt-get install -y xdg-utils ca-certificates x11vnc #================================= # Install Bash Command Line Tools #================================= +RUN apt-get update RUN apt-get -qy --no-install-recommends install \ + curl \ sudo \ unzip \ - wget \ - curl \ - libxi6 \ - libgconf-2-4 \ vim \ - xvfb \ - && rm -rf /var/lib/apt/lists/* + wget \ + xvfb #================ # Install Chrome #================ -RUN curl -sS -o - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \ - echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list && \ - apt-get -yqq update && \ - apt-get -yqq install google-chrome-stable && \ - rm -rf /var/lib/apt/lists/* +RUN apt-get update +RUN wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb +RUN apt-get install -y ./google-chrome-stable_current_amd64.deb +RUN rm ./google-chrome-stable_current_amd64.deb -#================== -# Configure Chrome -#================== -RUN dpkg-divert --add --rename --divert /opt/google/chrome/google-chrome.real /opt/google/chrome/google-chrome && \ - echo "#!/bin/bash\nexec /opt/google/chrome/google-chrome.real --disable-setuid-sandbox --no-sandbox \"\$@\"" > /opt/google/chrome/google-chrome && \ - chmod 755 /opt/google/chrome/google-chrome +#================ +# Install Python +#================ +RUN apt-get update && apt-get install -y software-properties-common +RUN add-apt-repository ppa:deadsnakes/ppa -y +RUN apt-get update +RUN apt-get install -y python3.13 python3.13-venv python3.13-dev build-essential +RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.13 1 +RUN apt-get clean && rm -rf /var/lib/apt/lists/* +RUN python3.13 -m ensurepip --upgrade +RUN python3.13 -m pip install --upgrade pip +RUN apt-get update +RUN apt-get install -y python3.13-tk python3.13-dev +RUN alias python=python3 +RUN echo "alias python=python3" >> ~/.bashrc +RUN rm /usr/bin/python3 +RUN ln -s python3.13 /usr/bin/python3 -#================= -# Install Firefox -#================= -RUN apt-get -qy --no-install-recommends install \ - $(apt-cache depends firefox | grep Depends | sed "s/.*ends:\ //" | tr '\n' ' ') \ - && rm -rf /var/lib/apt/lists/* \ - && cd /tmp \ - && wget --no-check-certificate -O firefox-esr.tar.bz2 \ - 'https://download.mozilla.org/?product=firefox-esr-latest&os=linux64&lang=en-US' \ - && tar -xjf firefox-esr.tar.bz2 -C /opt/ \ - && ln -s /opt/firefox/firefox /usr/bin/firefox \ - && rm -f /tmp/firefox-esr.tar.bz2 - -#=========================== -# Configure Virtual Display -#=========================== -RUN set -e -RUN echo "Starting X virtual framebuffer (Xvfb) in background..." -RUN Xvfb -ac :99 -screen 0 1280x1024x16 > /dev/null 2>&1 & -RUN export DISPLAY=:99 -RUN exec "$@" +#=============== +# Cleanup Lists +#=============== +RUN apt-get clean +RUN rm -rf /var/lib/apt/lists/* #===================== # Set up SeleniumBase #===================== +COPY sbase /SeleniumBase/sbase/ COPY seleniumbase /SeleniumBase/seleniumbase/ COPY examples /SeleniumBase/examples/ COPY integrations /SeleniumBase/integrations/ COPY requirements.txt /SeleniumBase/requirements.txt COPY setup.py /SeleniumBase/setup.py -RUN pip install --upgrade pip -RUN pip install --upgrade setuptools +COPY MANIFEST.in /SeleniumBase/MANIFEST.in +COPY pytest.ini /SeleniumBase/pytest.ini +COPY setup.cfg /SeleniumBase/setup.cfg +COPY virtualenv_install.sh /SeleniumBase/virtualenv_install.sh +RUN find . -name '*.pyc' -delete +RUN pip install --upgrade pip setuptools wheel RUN cd /SeleniumBase && ls && pip install -r requirements.txt --upgrade -RUN cd /SeleniumBase && python setup.py develop -RUN seleniumbase install chromedriver -RUN seleniumbase install geckodriver +RUN cd /SeleniumBase && pip install . +RUN pip install pyautogui +RUN pip install playwright +RUN seleniumbase get cft +RUN seleniumbase get chromium + +#======================= +# Download chromedriver +#======================= +RUN seleniumbase get chromedriver --path + +#============== +# Extra config +#============== +ENV DISPLAY=":99" +RUN Xvfb :99 -screen 1 1920x1080x16 -nolisten tcp & #========================================== # Create entrypoint and grab example tests #========================================== COPY integrations/docker/docker-entrypoint.sh / -COPY integrations/docker/run_docker_test_in_firefox.sh / COPY integrations/docker/run_docker_test_in_chrome.sh / RUN chmod +x *.sh -COPY integrations/docker/docker_config.cfg /SeleniumBase/examples/ ENTRYPOINT ["/docker-entrypoint.sh"] CMD ["/bin/bash"] diff --git a/LICENSE b/LICENSE index ee3af85576c..015b05a8790 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2019 Michael Mintz +Copyright (c) 2014-2026 Michael Mintz Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MANIFEST.in b/MANIFEST.in index 63c5b8be0b0..2c318a92e1c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,6 @@ include README.md include pytest.ini include setup.cfg -include .travis.yml include .gitignore include requirements.txt include install.sh diff --git a/README.md b/README.md index 8bfb891b0af..d8acaabc5ac 100755 --- a/README.md +++ b/README.md @@ -1,320 +1,1195 @@ -[](https://github.com/seleniumbase/SeleniumBase/blob/master/README.md) + + + + + + + + + +

SeleniumBase

+ +

All-in-one Browser Automation Framework:
Web Crawling / Testing / Scraping / Stealth

+ +

PyPI version SeleniumBase PyPI downloads SeleniumBase Docs SeleniumBase GitHub Actions YouTube Channel

+ +

+๐Ÿš€ Start | +๐Ÿฐ Features | +๐ŸŽ›๏ธ Options | +๐Ÿ“š Examples | +๐Ÿ’ป Scripts | +๐Ÿ—พ Locale +
+๐Ÿ“— API | +๐Ÿ“˜ Stealth API | + ๐Ÿ”  DesignPatterns | +๐Ÿ”ด Recorder | +๐Ÿ“Š Dashboard +
+๐ŸŽ–๏ธ GUI | +๐Ÿ“ฐ TestPage | +๐Ÿ‘ค UC Mode | +๐Ÿ™ CDP Mode | +๐Ÿ“ถ Charts | +๐Ÿ–ฅ๏ธ Farm +
+๐Ÿ‘๏ธ How | +๐Ÿš Migration | +๐ŸŽญ Stealthy Playwright | +๐Ÿ›‚ MasterQA | +๐ŸšŽ Tours +
+๐Ÿค– CI/CD | +๐ŸŸจ JSMgr | +๐ŸŒ Translator | +๐ŸŽž๏ธ Presenter | +๐Ÿ–ผ๏ธ Visual | +๐Ÿ—‚๏ธ CPlans +
+

+ +-------- + + + + +-------- + +๐Ÿ“ Here's a Python example that uses Pure CDP Mode (sb_cdp):
(It navigates to Browserscan where it bypasses bot-detection.) -[](https://github.com/seleniumbase/SeleniumBase/releases) [](https://dev.azure.com/seleniumbase/seleniumbase/_build/latest?definitionId=1&branchName=master) [Build Status](https://travis-ci.org/seleniumbase/SeleniumBase) [Join the Gitter Chat](https://gitter.im/seleniumbase/SeleniumBase) [MIT License](https://github.com/seleniumbase/SeleniumBase/blob/master/LICENSE) [GitHub Stars](https://github.com/seleniumbase/SeleniumBase/stargazers)
+```python +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +sb.goto("https://browserscan.net/bot-detection") +sb.sleep(3) +sb.quit() +``` -Reliable Browser Automation & Testing with [Selenium-WebDriver](https://www.seleniumhq.org/) and [Pytest](https://docs.pytest.org/en/latest/). +

+BrowserScan Test Results: Normal +
(All BrowserScan bot-detection tests passed successfully.) +

-
-(Above: [my_first_test.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/my_first_test.py) from [examples/](https://github.com/seleniumbase/SeleniumBase/tree/master/examples) running in demo mode, which adds JavaScript for highlighting page actions.)
+๐ŸŽญ Here's an example script that uses Stealthy Playwright Mode:
(Playwright connects to a stealthy SeleniumBase browser session.) + +```python +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(guest=True) +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://bot.sannysoft.com/") + page.wait_for_timeout(500) ``` -pytest my_first_test.py --demo_mode + +

+All Sannysoft tests passed successfully +
(All Sannysoft bot-detection tests passed successfully.) +

+ +-------- + +

๐Ÿ“ This example shows off multiple methods:

+ +```python +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +sb.goto("https://seleniumbase.io/demo_page") +sb.type("input", "Quickly type text!") +sb.press_keys("textarea", "Slowly type text!") +sb.click("#myButton") +sb.set_value("input#mySlider", "100") +sb.click_visible_elements("input.checkBoxClassB") +sb.select_option_by_text("#mySelect", "Set to 75%") +sb.hover_and_click("#myDropdown", "#dropOption2") +sb.click("#checkBox1") +sb.drag_and_drop("img#logo", "div#drop2") +sb.nested_click("iframe#myFrame3", ".fBox") +sb.highlight("#myButton") +sb.quit() ``` -## Quick Start: +-------- -(Requires **[Python/Pip](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/install_python_pip_git.md)** [Python versions](https://www.python.org/downloads/). Optionally, you may want to use a [Python virtual environment](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/virtualenv_instructions.md) to isolate Python dependencies between projects.) +

๐Ÿ“ This example expands the Browserscan script into a test demo:
(Assertions added and elements highlighted with JavaScript.)

-### Git clone and install SeleniumBase: +```python +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(locale="en", ad_block=True) +sb.goto("https://browserscan.net/bot-detection") +sb.flash("Test Results", duration=1.5, pause=0.5) +sb.assert_element('strong:contains("Normal")') +print("Bot Not Detected") +sb.flash('strong:contains("Normal")', pause=1) +sb.quit() ``` -git clone https://github.com/seleniumbase/SeleniumBase.git -cd SeleniumBase -pip install -r requirements.txt -python setup.py develop + +-------- + +

๐Ÿ“ This example scrapes Hacker News listings:

+ +```python +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +sb.goto("https://news.ycombinator.com/submitted?id=seleniumbase") +elements = sb.find_elements("span.titleline > a") +for element in elements: + print("* " + element.text) ``` -* ("``pip install -e .``" also works from the top-level SeleniumBase folder.) -SeleniumBase can also be installed from [PyPI](https://pypi.python.org/pypi/seleniumbase): [pypi](https://pypi.python.org/pypi/seleniumbase) +-------- + +๐Ÿ™ Stealthy CDP Mode examples are located in [./examples/cdp_mode/](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/cdp_mode). + +๐ŸŽญ Stealthy Playwright examples are located in [./examples/cdp_mode/playwright/](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/playwright/). + +-------- + +

โš™๏ธ Stealthy architecture flowchart:

+ +

+Stealthy architecture flowchart +
(For maximum stealth, use CDP Mode, which includes Stealthy Playwright Mode.) +

+ +-------- + +

๐ŸŒ CLI Options for Supported Chromium Browsers

+ +

๐Ÿ’ก You can set the Chromium browser to use via command line parameters:

+ +```zsh +python SCRIPT.py --chromium # Use the unbranded Chromium browser +python SCRIPT.py --cft # Use Chrome-for-testing +python SCRIPT.py --edge # Use Microsoft Edge +python SCRIPT.py --brave # Use Brave browser ``` -pip install seleniumbase + +Google Chrome is the default browser. Only unbranded Chromium and Chrome-for-Testing get downloaded automatically if not already present on the system. + +The Chromium browser can also be set via method args, eg: `cft=True`, `use_chromium=True`, `browser="edge"`, `browser="brave"`, etc. Eg: + +```python +sb = sb_cdp.Chrome(use_chromium=True) ``` -* (Add ``--upgrade`` to get the latest packages. Add ``--force-reinstall`` with ``--no-cache-dir`` to force a clean installation.) -You can also install a specific GitHub branch of SeleniumBase: +-------- + +

๐Ÿ“ This example saves Google Search results with UC + CDP Mode:
(Results are saved as PDF, HTML, and PNG files to ./latest_logs/)

+ +```python +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + url = "https://google.com/ncr" + sb.activate_cdp_mode(url) + sb.click_if_visible('button:contains("Accept all")') + sb.type('[name="q"]', "SeleniumBase GitHub page") + sb.click('[value="Google Search"]') + sb.sleep(4) # The "AI Overview" sometimes loads + print(sb.get_page_title()) + sb.save_as_pdf_to_logs() + sb.save_page_source_to_logs() + sb.save_screenshot_to_logs() + print("Logs have been saved to: ./latest_logs/") ``` -pip install git+https://github.com/seleniumbase/SeleniumBase.git@master#egg=seleniumbase + +-------- + +

๐Ÿ“ This example bypasses Cloudflare's challenge page with UC + CDP Mode:
(If the Turnstile isn't bypassed automatically, sb.solve_captcha() handles it.)

+ +```python +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en") as sb: + url = "https://gitlab.com/users/sign_in" + sb.activate_cdp_mode(url) + sb.sleep(2) + sb.solve_captcha() + # (The rest is for testing and demo purposes) + sb.assert_text("Username", '[for="user_login"]', timeout=3) + sb.assert_element('label[for="user_login"]') + sb.highlight('button:contains("Sign in")') + sb.highlight('h1:contains("GitLab")') + sb.post_message("SeleniumBase wasn't detected", duration=4) ``` -### Download a web driver: +

+SeleniumBase SeleniumBase +
(Successfully bypassed bot-detection on a Cloudflare challenge page.) +

-SeleniumBase can download a web driver to the [seleniumbase/drivers](https://github.com/seleniumbase/SeleniumBase/tree/master/seleniumbase/drivers) folder with the ``install`` command: +๐Ÿ’ก sb.solve_captcha() handles CAPTCHAs that aren't bypassed automatically.
(If no CAPTCHA is present on the current page, then nothing happens.) + +-------- + +

๐Ÿ“ This example handles a CAPTCHA page with Pure CDP Mode:

+ +```python +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(incognito=True) +sb.goto("https://gitlab.com/users/sign_in") +sb.sleep(2) +sb.solve_captcha() +sb.highlight('h1:contains("GitLab")') +sb.highlight('button:contains("Sign in")') +sb.quit() ``` -seleniumbase install chromedriver + +-------- + +

๐Ÿงช Comprehensive E2E Testing with pytest:

+ +๐Ÿ“š The [SeleniumBase/examples/](https://github.com/seleniumbase/SeleniumBase/tree/master/examples) folder includes over 150 ready-to-run examples of E2E testing. Examples that start with `test_` or end with `_test.py`/`_tests.py` run with `pytest`. Other examples run directly with raw `python` (those generally start with `raw_` to avoid confusion). + + + +

๐Ÿ“ This example tests an e-commerce site with pytest:

+ +```python +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) # Call pytest + +class MyTestClass(BaseCase): + def test_swag_labs(self): + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("div.inventory_list") + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_text("Backpack", "div.cart_item") + self.click("button#checkout") + self.type("input#first-name", "SeleniumBase") + self.type("input#last-name", "Automation") + self.type("input#postal-code", "77123") + self.click("input#continue") + self.click("button#finish") + self.assert_text("Thank you for your order!") ``` -* (You need a different web driver for each web browser you want to run automation on: ``chromedriver`` for Chrome, ``edgedriver`` for Edge, ``geckodriver`` for Firefox, ``operadriver`` for Opera, and ``iedriver`` for Internet Explorer.) -### Run a test on Chrome: +> `pytest test_get_swag.py` + +SeleniumBase Test + +-------- + +

๐Ÿ“ This example tests another e-commerce site with pytest:

+ +```zsh +pytest test_coffee_cart.py --demo ``` -cd examples -pytest my_first_test.py --browser=chrome + +

SeleniumBase Coffee Cart Test

+ +>

(--demo mode slows down tests and highlights actions)

+ +-------- + + + +

๐Ÿ“ This example covers multiple actions with pytest:

+ +```zsh +pytest test_demo_site.py ``` -* (Chrome is the default browser if not specified with ``--browser``) -**Check out [my_first_test.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/my_first_test.py) to see what a simple test looks like:** -* (By default, [CSS Selectors](https://www.w3schools.com/cssref/css_selectors.asp) are used for finding page elements.) +

SeleniumBase Example

+ +> Easy to type, click, select, toggle, drag & drop, and more. + +(For more examples, see the SeleniumBase/examples/ folder.) + +-------- + +

SeleniumBase

+ +
+

Explore the README:

+ +
+ +-------- + +
+ โ–ถ๏ธ How is SeleniumBase different from raw Selenium? (click to expand) +
+ +

๐Ÿ’ก SeleniumBase is a Python framework for browser automation and testing. SeleniumBase uses Selenium/WebDriver APIs and incorporates test-runners such as pytest, pynose, and behave to provide organized structure, test discovery, test execution, test state (eg. passed, failed, or skipped), and command-line options for changing default settings (eg. browser selection). With raw Selenium, you would need to set up your own options-parser for configuring tests from the command-line.

+ +

๐Ÿ’ก SeleniumBase's driver manager gives you more control over automatic driver downloads. (Use --driver-version=VER with your pytest run command to specify the version.) By default, SeleniumBase will download a driver version that matches your major browser version if not set.

+ +

๐Ÿ’ก SeleniumBase automatically detects between CSS Selectors and XPath, which means you don't need to specify the type of selector in your commands (but optionally you could).

+ +

๐Ÿ’ก SeleniumBase methods often perform multiple actions in a single method call. For example, self.type(selector, text) does the following:
1. Waits for the element to be visible.
2. Waits for the element to be interactive.
3. Clears the text field.
4. Types in the new text.
5. Presses Enter/Submit if the text ends in "\n".
With raw Selenium, those actions require multiple method calls.

+ +

๐Ÿ’ก SeleniumBase uses default timeout values when not set:
+โœ… self.click("button")
+With raw Selenium, methods would fail instantly (by default) if an element needed more time to load:
+โŒ self.driver.find_element(by="css selector", value="button").click()
+(Reliable code is better than unreliable code.)

+ +

๐Ÿ’ก SeleniumBase lets you change the explicit timeout values of methods:
+โœ… self.click("button", timeout=10)
+With raw Selenium, that requires more code:
+โŒ WebDriverWait(driver, 10).until(EC.element_to_be_clickable("css selector", "button")).click()
+(Simple code is better than complex code.)

+ +

๐Ÿ’ก SeleniumBase gives you clean error output when a test fails. With raw Selenium, error messages can get very messy.

+ +

๐Ÿ’ก SeleniumBase gives you the option to generate a dashboard and reports for tests. It also saves screenshots from failing tests to the ./latest_logs/ folder. Raw Selenium does not have these options out-of-the-box.

+ +

๐Ÿ’ก SeleniumBase includes desktop GUI apps for running tests, such as SeleniumBase Commander for pytest and SeleniumBase Behave GUI for behave.

+ +

๐Ÿ’ก SeleniumBase has its own Recorder / Test Generator for creating tests from manual browser actions.

-## Learn More: +

๐Ÿ’ก SeleniumBase comes with test case management software, ("CasePlans"), for organizing tests and step descriptions.

-#### **Automatic WebDriver abilities:**
-SeleniumBase automatically handles common WebDriver actions such as spinning up web browsers and saving screenshots during test failures. ([Read more about customizing test runs](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/customizing_test_runs.md).) +

๐Ÿ’ก SeleniumBase includes tools for building data apps, ("ChartMaker"), which can generate JavaScript from Python.

+ +
+
+ +-------- + +

๐Ÿ“š Learn about different ways of writing tests:

+ +

๐Ÿ“—๐Ÿ“ Here's test_simple_login.py, which uses BaseCase class inheritance, and runs with pytest or pynose. (Use self.driver to access Selenium's raw driver.)

-#### **Simplified code:**
-Instead of using messy WebDriver commands such as: ```python -self.driver.find_element_by_css_selector("textarea").send_keys("text") +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + +class TestSimpleLogin(BaseCase): + def test_simple_login(self): + self.goto("seleniumbase.io/simple/login") + self.type("#username", "demo_user") + self.type("#password", "secret_pass") + self.click('a:contains("Sign in")') + self.assert_exact_text("Welcome!", "h1") + self.assert_element("img#image1") + self.highlight("#image1") + self.click_link("Sign out") + self.assert_text("signed out", "#top_message") ``` -...you can do the following with SeleniumBase: + +

๐Ÿ“˜๐Ÿ“ Here's raw_login_sb.py, which uses the SB Context Manager. Runs with pure python. (Use sb.driver to access Selenium's raw driver.)

+ ```python -self.update_text("textarea", "text") -``` -(You can still use ``self.driver`` in your code.) +from seleniumbase import SB -#### **Run tests with Pytest or Nose in any browser:**
-(Using **Pytest** is strongly recommended) +with SB() as sb: + sb.goto("seleniumbase.io/simple/login") + sb.type("#username", "demo_user") + sb.type("#password", "secret_pass") + sb.click('a:contains("Sign in")') + sb.assert_exact_text("Welcome!", "h1") + sb.assert_element("img#image1") + sb.highlight("#image1") + sb.click_link("Sign out") + sb.assert_text("signed out", "#top_message") ``` -pytest my_first_test.py --browser=chrome -nosetests test_suite.py --browser=firefox +

๐Ÿ“™๐Ÿ“ Here's raw_login_driver.py, which uses the Driver Manager. Runs with pure python. (The driver is an improved version of Selenium's raw driver, with more methods.)

+ +```python +from seleniumbase import Driver + +driver = Driver() +try: + driver.goto("seleniumbase.io/simple/login") + driver.type("#username", "demo_user") + driver.type("#password", "secret_pass") + driver.click('a:contains("Sign in")') + driver.assert_exact_text("Welcome!", "h1") + driver.assert_element("img#image1") + driver.highlight("#image1") + driver.click_link("Sign out") + driver.assert_text("signed out", "#top_message") +finally: + driver.quit() ``` -Python methods that start with ``test_`` will automatically be run when using ``pytest`` or ``nosetests`` on a Python file, (or on folders containing Python files). -#### **No more flaky tests:**
-SeleniumBase methods automatically wait for page elements to finish loading before interacting with them (*up to a timeout limit*). This means you no longer need random ``time.sleep()`` statements in your scripts. +-------- -#### **Automated/manual hybrid mode:**
-SeleniumBase includes a solution called **[MasterQA](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/masterqa/ReadMe.md)**, which speeds up manual testing by having automation perform all the browser actions while the manual tester handles validatation. + +

Set up Python & Git:

-#### **Feature-Rich:**
-For a full list of SeleniumBase features, [Click Here](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/features_list.md). + +๐Ÿ”ต Add Python and Git to your System PATH. - - +๐Ÿ”ต Using a Python virtual env is recommended. -## Detailed Instructions: + +

Install SeleniumBase:

-**Here's how to run the example script on various web browsers:** +๐Ÿ”ต **How to install `seleniumbase` from [PyPI](https://pypi.org/project/seleniumbase/) using `pip`:** -First, install a web driver for each web browser you intend to use: +```zsh +pip install seleniumbase ``` -seleniumbase install chromedriver -seleniumbase install geckodriver -seleniumbase install edgedriver -seleniumbase install iedriver -seleniumbase install operadriver + +* (Add `--upgrade` OR `-U` to upgrade SeleniumBase.) +* (Add `--force-reinstall` to upgrade indirect packages.) + +๐Ÿ”ต **How to install `seleniumbase` from a GitHub clone:** + +```zsh +git clone https://github.com/seleniumbase/SeleniumBase.git +cd SeleniumBase/ +pip install -e . +``` + +๐Ÿ”ต **How to upgrade an existing install from a GitHub clone:** + +```zsh +git pull +pip install -e . +``` + +๐Ÿ”ต **Type `seleniumbase` or `sbase` to verify that SeleniumBase was installed successfully:** + +```zsh + ___ _ _ ___ +/ __| ___| |___ _ _ (_)_ _ _ __ | _ ) __ _ ______ +\__ \/ -_) / -_) ' \| | \| | ' \ | _ \/ _` (_-< -_) +|___/\___|_\___|_||_|_|\_,_|_|_|_\|___/\__,_/__|___| +---------------------------------------------------- + +โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ +โ”‚ * USAGE: "seleniumbase [COMMAND] [PARAMETERS]" โ”‚ +โ”‚ * OR: "sbase [COMMAND] [PARAMETERS]" โ”‚ +โ”‚ โ”‚ +โ”‚ COMMANDS: PARAMETERS / DESCRIPTIONS: โ”‚ +โ”‚ get / install [DRIVER_NAME] [OPTIONS] โ”‚ +โ”‚ methods (List common Python methods) โ”‚ +โ”‚ options (List common pytest options) โ”‚ +โ”‚ behave-options (List common behave options) โ”‚ +โ”‚ gui / commander [OPTIONAL PATH or TEST FILE] โ”‚ +โ”‚ behave-gui (SBase Commander for Behave) โ”‚ +โ”‚ caseplans [OPTIONAL PATH or TEST FILE] โ”‚ +โ”‚ mkdir [DIRECTORY] [OPTIONS] โ”‚ +โ”‚ mkfile [FILE.py] [OPTIONS] โ”‚ +โ”‚ mkrec / codegen [FILE.py] [OPTIONS] โ”‚ +โ”‚ recorder (Open Recorder Desktop App.) โ”‚ +โ”‚ record (If args: mkrec. Else: App.) โ”‚ +โ”‚ mkpres [FILE.py] [LANG] โ”‚ +โ”‚ mkchart [FILE.py] [LANG] โ”‚ +โ”‚ print [FILE] [OPTIONS] โ”‚ +โ”‚ translate [SB_FILE.py] [LANG] [ACTION] โ”‚ +โ”‚ convert [WEBDRIVER_UNITTEST_FILE.py] โ”‚ +โ”‚ extract-objects [SB_FILE.py] โ”‚ +โ”‚ inject-objects [SB_FILE.py] [OPTIONS] โ”‚ +โ”‚ objectify [SB_FILE.py] [OPTIONS] โ”‚ +โ”‚ revert-objects [SB_FILE.py] [OPTIONS] โ”‚ +โ”‚ encrypt / obfuscate โ”‚ +โ”‚ decrypt / unobfuscate โ”‚ +โ”‚ proxy (Start a basic proxy server) โ”‚ +โ”‚ download server (Get Selenium Grid JAR file) โ”‚ +โ”‚ grid-hub [start|stop] [OPTIONS] โ”‚ +โ”‚ grid-node [start|stop] --hub=[HOST/IP] โ”‚ +โ”‚ โ”‚ +โ”‚ * EXAMPLE => "sbase get chromedriver stable" โ”‚ +โ”‚ * For command info => "sbase help [COMMAND]" โ”‚ +โ”‚ * For info on all commands => "sbase --help" โ”‚ +โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ +``` + +

๐Ÿ”ต Downloading webdrivers:

+ +โœ… SeleniumBase automatically downloads webdrivers as needed, such as `chromedriver`. + +
+
+ โ–ถ๏ธ Here's sample output from a chromedriver download. (click to expand) + +```zsh +*** chromedriver to download = 149.0.7827.54 (Latest Stable) + +Downloading chromedriver-mac-arm64.zip from: +https://storage.googleapis.com/chrome-for-testing-public/149.0.7827.54/mac-arm64/chromedriver-mac-arm64.zip ... +Download Complete! + +Extracting ['chromedriver'] from chromedriver-mac-arm64.zip ... +Unzip Complete! + +The file [chromedriver] was saved to: +~/github/SeleniumBase/seleniumbase/drivers/ +chromedriver + +Making [chromedriver 149.0.7827.54] executable ... +[chromedriver 149.0.7827.54] is now ready for use! +``` + +
+ + + +

Basic Example / Usage:

+ +๐Ÿ”ต If you've cloned SeleniumBase, you can run tests from the [examples/](https://github.com/seleniumbase/SeleniumBase/tree/master/examples) folder. + +

Here's my_first_test.py:

+ +```zsh +cd examples/ +pytest my_first_test.py ``` -Next, choose between **pytest** and **nosetests** test runners. (Interchangeable) +SeleniumBase Test + +

Here's the full code for my_first_test.py:

+ +```python +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + +class MyTestClass(BaseCase): + def test_swag_labs(self): + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("div.inventory_list") + self.assert_exact_text("Products", "span.title") + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_exact_text("Your Cart", "span.title") + self.assert_text("Backpack", "div.cart_item") + self.click("button#checkout") + self.type("#first-name", "SeleniumBase") + self.type("#last-name", "Automation") + self.type("#postal-code", "77123") + self.click("input#continue") + self.assert_text("Checkout: Overview") + self.assert_text("Backpack", "div.cart_item") + self.assert_text("29.99", "div.inventory_item_price") + self.click("button#finish") + self.assert_exact_text("Thank you for your order!", "h2") + self.assert_element('img[alt="Pony Express"]') + self.js_click("a#logout_sidebar_link") + self.assert_element("div#login_button_container") +``` + +* By default, **[CSS Selectors](https://www.w3schools.com/cssref/css_selectors.asp)** are used for finding page elements. +* If you're new to CSS Selectors, games like [CSS Diner](http://flukeout.github.io/) can help you learn. +* For more reading, [here's an advanced guide on CSS attribute selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors). + + + +

Here are some common SeleniumBase methods:

+ +```python +self.goto(url) # Navigate the browser window to the URL. +self.open(url) # Same as `self.goto(url)` +self.activate_cdp_mode() # Activate CDP Mode from UC Mode. +self.type(selector, text) # Update the field with the text. +self.click(selector) # Click the element with the selector. +self.click_link(link_text) # Click the link containing text. +self.go_back() # Navigate back to the previous URL. +self.select_option_by_text(dropdown_selector, option) +self.hover_and_click(hover_selector, click_selector) +self.drag_and_drop(drag_selector, drop_selector) +self.get_text(selector) # Get the text from the element. +self.get_current_url() # Get the URL of the current page. +self.get_page_source() # Get the HTML of the current page. +self.get_attribute(selector, attribute) # Get element attribute. +self.get_title() # Get the title of the current page. +self.switch_to_frame(frame) # Switch into the iframe container. +self.switch_to_default_content() # Leave the iframe container. +self.open_new_window() # Open a new window in the same browser. +self.switch_to_window(window) # Switch to the browser window. +self.switch_to_default_window() # Switch to the original window. +self.get_new_driver(OPTIONS) # Open a new driver with OPTIONS. +self.switch_to_driver(driver) # Switch to the browser driver. +self.switch_to_default_driver() # Switch to the original driver. +self.wait_for_element(selector) # Wait until element is visible. +self.is_element_visible(selector) # Return element visibility. +self.is_text_visible(text, selector) # Return text visibility. +self.sleep(seconds) # Do nothing for the given amount of time. +self.save_screenshot(name) # Save a screenshot in .png format. +self.assert_element(selector) # Verify the element is visible. +self.assert_text(text, selector) # Verify text in the element. +self.assert_exact_text(text, selector) # Verify text is exact. +self.assert_title(title) # Verify the title of the web page. +self.assert_downloaded_file(file) # Verify file was downloaded. +self.assert_no_404_errors() # Verify there are no broken links. +self.assert_no_js_errors() # Verify there are no JS errors. +``` + +๐Ÿ”ต For the complete list of SeleniumBase methods, see: Method Summary + + + +

Fun Facts / Learn More:

+ +

โœ… SeleniumBase automatically handles common WebDriver actions such as launching web browsers before tests, saving screenshots during failures, and closing web browsers after tests.

+ +

โœ… SeleniumBase lets you customize tests via command-line options.

+ +

โœ… SeleniumBase uses simple syntax for commands. Example:

+ +```python +self.type("input", "dogs\n") # (The "\n" presses ENTER) ``` -cd examples/ -pytest my_first_test.py --browser=chrome +Most SeleniumBase scripts can be run with pytest, pynose, or pure python. Not all test runners can run all test formats. For example, tests that use the `sb` pytest fixture can only be run with `pytest`. (See Syntax Formats) There's also a Gherkin test format that runs with behave. + +```zsh +pytest coffee_cart_tests.py --rs +pytest test_sb_fixture.py --demo +pytest test_suite.py --rs --html=report.html --dashboard + +pynose basic_test.py --mobile +pynose test_suite.py --headless --report --show-report + +python raw_sb.py +python raw_test_scripts.py + +behave realworld.feature +behave calculator.feature -D rs -D dashboard +``` + +

โœ… pytest includes automatic test discovery. If you don't specify a specific file or folder to run, pytest will automatically search through all subdirectories for tests to run based on the following criteria:

+ +* Python files that start with `test_` or end with `_test.py`. +* Python methods that start with `test_`. + +With a SeleniumBase [pytest.ini](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/pytest.ini) file present, you can modify default discovery settings. The Python class name can be anything because `seleniumbase.BaseCase` inherits `unittest.TestCase` to trigger autodiscovery. + +

โœ… You can do a pre-flight check to see which tests would get discovered by pytest before the actual run:

-nosetests my_first_test.py --browser=firefox +```zsh +pytest --co -q ``` -(If no browser is specified, Chrome is used by default.) - **Use Demo Mode to help you see what tests are asserting.** +

โœ… You can be more specific when calling pytest or pynose on a file:

-If the example test is moving too fast for your eyes, you can run it in **Demo Mode** by adding ``--demo_mode`` on the command line, which pauses the browser briefly between actions, highlights page elements being acted on, and lets you know what test assertions are happening in real time: +```zsh +pytest [FILE_NAME.py]::[CLASS_NAME]::[METHOD_NAME] +pynose [FILE_NAME.py]:[CLASS_NAME].[METHOD_NAME] ``` -pytest my_first_test.py --demo_mode + +

โœ… No More Flaky Tests! SeleniumBase methods automatically wait for page elements to finish loading before interacting with them (up to a timeout limit).

+NO MORE FLAKY TESTS! + +โœ… SeleniumBase supports all major browsers and operating systems: +

Browsers: Chrome, Edge, Firefox, and Safari.

+

Systems: Linux/Ubuntu, macOS, and Windows.

+ +โœ… SeleniumBase works on all popular CI/CD platforms: +

GitHub Actions integration Jenkins integration Azure integration Google Cloud integration AWS integration Your Computer

+ +

โœ… SeleniumBase includes an automated/manual hybrid solution called MasterQA to speed up manual testing with automation while manual testers handle validation.

+ +

โœ… SeleniumBase supports running tests while offline (assuming webdrivers have previously been downloaded when online).

+ +

โœ… For a full list of SeleniumBase features, Click Here.

+ + + +

Demo Mode / Debugging:

+ +๐Ÿ”ต Demo Mode helps you see what a test is doing. If a test is moving too fast for your eyes, run it in Demo Mode to pause the browser briefly between actions, highlight page elements being acted on, and display assertions: + +```zsh +pytest my_first_test.py --demo ``` -You can use the following in your scripts to help you debug issues: +๐Ÿ”ต `time.sleep(seconds)` can be used to make a test wait at a specific spot: ```python -import time; time.sleep(5) # sleep for 5 seconds (add this after the line you want to pause on) -import ipdb; ipdb.set_trace() # waits for your command. n = next line of current method, c = continue, s = step / next executed line (will jump) -import pytest; pytest.set_trace() # similar to ipdb, but specific to pytest +import time; time.sleep(3) # Do nothing for 3 seconds. ``` -**To pause an active test that throws an exception or error, add ``--pdb -s``:** +๐Ÿ”ต **Debug Mode** with Python's built-in **[pdb](https://docs.python.org/3/library/pdb.html)** library helps you debug tests: -``` -pytest my_first_test.py --browser=chrome --pdb -s +```python +breakpoint() # Shortcut for "import pdb; pdb.set_trace()" ``` -The code above will leave your browser window open in case there's a failure. (ipdb commands: 'c', 's', 'n' => continue, step, next). +> (**`pdb`** commands: `n`, `c`, `s`, `u`, `d` => `next`, `continue`, `step`, `up`, `down`) -Here are some other useful **nosetest**-specific arguments: +๐Ÿ”ต To pause an active test that throws an exception or error, (*and keep the browser window open while **Debug Mode** begins in the console*), add **`--pdb`** as a `pytest` option: -``` ---logging-level=INFO # Hide DEBUG messages, which can be overwhelming. --x # Stop running the tests after the first failure is reached. --v # Prints the full test name rather than a dot for each test. ---with-id # If -v is also used, will number the tests for easy counting. +```zsh +pytest test_fail.py --pdb ``` -During test failures, logs and screenshots from the most recent test run will get saved to the ``latest_logs/`` folder. Those logs will get moved to ``archived_logs/`` if you have ARCHIVE_EXISTING_LOGS set to True in [settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py), otherwise log files with be cleaned up at the start of the next test run. The ``test_suite.py`` collection contains tests that fail on purpose so that you can see how logging works. +๐Ÿ”ต To start tests in Debug Mode, add **`--trace`** as a `pytest` option: +```zsh +pytest test_coffee_cart.py --trace ``` + +SeleniumBase test with the pdbp (Pdb+) debugger + + + +

๐Ÿ”ต Command-line Options:

+ + +โœ… Here are some useful command-line options that come with pytest: + +```zsh +-v # Verbose mode. Prints the full name of each test and shows more details. +-q # Quiet mode. Print fewer details in the console output when running tests. +-x # Stop running the tests after the first failure is reached. +--html=report.html # Creates a detailed pytest-html report after tests finish. +--co | --collect-only # Show what tests would get run. (Without running them) +--co -q # (Both options together!) - Do a dry run with full test names shown. +-n=NUM # Multithread the tests using that many threads. (Speed up test runs!) +-s # See print statements. (Should be on by default with pytest.ini present.) +--junit-xml=report.xml # Creates a junit-xml report after tests finish. +--pdb # If a test fails, enter Post Mortem Debug Mode. (Don't use with CI!) +--trace # Enter Debug Mode at the beginning of each test. (Don't use with CI!) +-m=MARKER # Run tests with the specified pytest marker. +``` + + +โœ… SeleniumBase provides additional pytest command-line options for tests: + +```zsh +--browser=BROWSER # (The web browser to use. Default: "chrome".) +--chrome # (Shortcut for "--browser=chrome". On by default.) +--edge # (Shortcut for "--browser=edge".) +--firefox # (Shortcut for "--browser=firefox".) +--safari # (Shortcut for "--browser=safari".) +--opera # (Shortcut for "--browser=opera".) +--brave # (Shortcut for "--browser=brave".) +--comet # (Shortcut for "--browser=comet".) +--chromium # (Shortcut for using base `Chromium`) +--settings-file=FILE # (Override default SeleniumBase settings.) +--env=ENV # (Set the test env. Access with "self.env" in tests.) +--account=STR # (Set account. Access with "self.account" in tests.) +--data=STRING # (Extra test data. Access with "self.data" in tests.) +--var1=STRING # (Extra test data. Access with "self.var1" in tests.) +--var2=STRING # (Extra test data. Access with "self.var2" in tests.) +--var3=STRING # (Extra test data. Access with "self.var3" in tests.) +--variables=DICT # (Extra test data. Access with "self.variables".) +--user-data-dir=DIR # (Set the Chrome user data directory to use.) +--protocol=PROTOCOL # (The Selenium Grid protocol: http|https.) +--server=SERVER # (The Selenium Grid server/IP used for tests.) +--port=PORT # (The Selenium Grid port used by the test server.) +--cap-file=FILE # (The web browser's desired capabilities to use.) +--cap-string=STRING # (The web browser's desired capabilities to use.) +--proxy=SERVER:PORT # (Connect to a proxy server:port as tests are running) +--proxy=USERNAME:PASSWORD@SERVER:PORT # (Use an authenticated proxy server) +--proxy-bypass-list=STRING # (";"-separated hosts to bypass, Eg "*.foo.com") +--proxy-pac-url=URL # (Connect to a proxy server using a PAC_URL.pac file.) +--proxy-pac-url=USERNAME:PASSWORD@URL # (Authenticated proxy with PAC URL.) +--proxy-driver # (If a driver download is needed, will use: --proxy=PROXY.) +--multi-proxy # (Allow multiple authenticated proxies when multi-threaded.) +--agent=STRING # (Modify the web browser's User-Agent string.) +--mobile # (Use the mobile device emulator while running tests.) +--metrics=STRING # (Set mobile metrics: "CSSWidth,CSSHeight,PixelRatio".) +--chromium-arg="ARG=N,ARG2" # (Set Chromium args, ","-separated, no spaces.) +--firefox-arg="ARG=N,ARG2" # (Set Firefox args, comma-separated, no spaces.) +--firefox-pref=SET # (Set a Firefox preference:value set, comma-separated.) +--extension-zip=ZIP # (Load a Chrome Extension .zip|.crx, comma-separated.) +--extension-dir=DIR # (Load a Chrome Extension directory, comma-separated.) +--disable-features="F1,F2" # (Disable features, comma-separated, no spaces.) +--binary-location=PATH # (Set path of the Chromium browser binary to use.) +--driver-version=VER # (Set the chromedriver or uc_driver version to use.) +--sjw # (Skip JS Waits for readyState to be "complete" or Angular to load.) +--wfa # (Wait for AngularJS to be done loading after specific web actions.) +--pls=PLS # (Set pageLoadStrategy on Chrome: "normal", "eager", or "none".) +--headless # (The default headless mode. Linux uses this mode by default.) +--headless1 # (Use Chrome's old headless mode. Fast, but has limitations.) +--headless2 # (Use Chrome's new headless mode, which supports extensions.) +--headed # (Run tests in headed/GUI mode on Linux OS, where not default.) +--xvfb # (Run tests using the Xvfb virtual display server on Linux OS.) +--xvfb-metrics=STRING # (Set Xvfb display size on Linux: "Width,Height".) +--locale=LOCALE_CODE # (Set the Language Locale Code for the web browser.) +--interval=SECONDS # (The autoplay interval for presentations & tour steps) +--start-page=URL # (The starting URL for the web browser when tests begin.) +--archive-logs # (Archive existing log files instead of deleting them.) +--archive-downloads # (Archive old downloads instead of deleting them.) +--time-limit=SECONDS # (Safely fail any test that exceeds the time limit.) +--slow # (Slow down the automation. Faster than using Demo Mode.) +--demo # (Slow down and visually see test actions as they occur.) +--demo-sleep=SECONDS # (Set the wait time after Slow & Demo Mode actions.) +--highlights=NUM # (Number of highlight animations for Demo Mode actions.) +--message-duration=SECONDS # (The time length for Messenger alerts.) +--check-js # (Check for JavaScript errors after page loads.) +--ad-block # (Block some types of display ads from loading.) +--host-resolver-rules=RULES # (Set host-resolver-rules, comma-separated.) +--block-images # (Block images from loading during tests.) +--do-not-track # (Indicate to websites that you don't want to be tracked.) +--verify-delay=SECONDS # (The delay before MasterQA verification checks.) +--ee | --esc-end # (Lets the user end the current test via the ESC key.) +--recorder # (Enables the Recorder for turning browser actions into code.) +--rec-sb-mgr # (A Recorder Mode that generates SB() context manager code.) +--rec-sb-cdp # (A Recorder Mode that generates Pure CDP Mode sb_cdp code.) +--rec-behave # (Same as Recorder Mode, but also generates behave-gherkin.) +--rec-sleep # (If the Recorder is enabled, also records self.sleep calls.) +--rec-print # (If the Recorder is enabled, prints output after tests end.) +--disable-cookies # (Disable Cookies on websites. Pages might break!) +--disable-js # (Disable JavaScript on websites. Pages might break!) +--disable-csp # (Disable the Content Security Policy of websites.) +--disable-ws # (Disable Web Security on Chromium-based browsers.) +--enable-ws # (Enable Web Security on Chromium-based browsers.) +--enable-sync # (Enable "Chrome Sync" on websites.) +--uc | --undetected # (Use undetected-chromedriver to evade bot-detection.) +--uc-cdp-events # (Capture CDP events when running in "--undetected" mode.) +--log-cdp # ("goog:loggingPrefs", {"performance": "ALL", "browser": "ALL"}) +--remote-debug # (Sync to Chrome Remote Debugger chrome://inspect/#devices) +--ftrace | --final-trace # (Debug Mode after each test. Don't use with CI!) +--dashboard # (Enable the SeleniumBase Dashboard. Saved at: dashboard.html) +--dash-title=STRING # (Set the title shown for the generated dashboard.) +--enable-3d-apis # (Enables WebGL and 3D APIs.) +--swiftshader # (Chrome "--use-gl=angle" / "--use-angle=swiftshader-webgl") +--incognito # (Enable Chrome's Incognito mode.) +--guest # (Enable Chrome's Guest mode.) +--dark # (Enable Chrome's Dark mode.) +--devtools # (Open Chrome's DevTools when the browser opens.) +--rs | --reuse-session # (Reuse browser session for all tests.) +--rcs | --reuse-class-session # (Reuse session for tests in class.) +--crumbs # (Delete all cookies between tests reusing a session.) +--disable-beforeunload # (Disable the "beforeunload" event on Chrome.) +--window-position=X,Y # (Set the browser's starting window position.) +--window-size=WIDTH,HEIGHT # (Set the browser's starting window size.) +--maximize # (Start tests with the browser window maximized.) +--screenshot # (Save a screenshot at the end of each test.) +--no-screenshot # (No screenshots saved unless tests directly ask it.) +--visual-baseline # (Set the visual baseline for Visual/Layout tests.) +--external-pdf # (Set Chromium "plugins.always_open_pdf_externally":True.) +--timeout-multiplier=MULTIPLIER # (Multiplies the default timeout values.) +--list-fail-page # (After each failing test, list the URL of the failure.) +``` + +(See the full list of command-line option definitions **[here](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/plugins/pytest_plugin.py)**. For detailed examples of command-line options, see **[customizing_test_runs.md](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/customizing_test_runs.md)**) + +-------- + +๐Ÿ”ต During test failures, logs and screenshots from the most recent test run will get saved to the `latest_logs/` folder. Those logs will get moved to `archived_logs/` if you add --archive_logs to command-line options, or have `ARCHIVE_EXISTING_LOGS` set to True in [settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py), otherwise log files with be cleaned up at the start of the next test run. The `test_suite.py` collection contains tests that fail on purpose so that you can see how logging works. + +```zsh cd examples/ -pytest test_suite.py --browser=chrome +pytest test_suite.py --chrome -pytest test_suite.py --browser=firefox +pytest test_suite.py --firefox ``` -If you want to run tests headlessly, use ``--headless``, which you'll need to do if your system lacks a GUI interface. Even if your system does have a GUI interface, it may still support headless browser automation. +An easy way to override seleniumbase/config/settings.py is by using a custom settings file. +Here's the command-line option to add to tests: (See [examples/custom_settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/custom_settings.py)) +`--settings_file=custom_settings.py` +(Settings include default timeout values, a two-factor auth key, DB credentials, S3 credentials, and other important settings used by tests.) + +๐Ÿ”ต To pass additional data from the command-line to tests, add `--data="ANY STRING"`. +Inside your tests, you can use `self.data` to access that. -To run Pytest multithreaded on multiple CPUs at the same time, add ``-n=NUM`` or ``-n NUM`` on the command line, where NUM is the number of CPUs you want to use. + +

Directory Configuration:

-If you want to pass additional data from the command line to your tests, you can use ``--data=STRING``. Now inside your tests, you can use ``self.data`` to access that. +๐Ÿ”ต When running tests with **`pytest`**, you'll want a copy of **[pytest.ini](https://github.com/seleniumbase/SeleniumBase/blob/master/pytest.ini)** in your root folders. When running tests with **`pynose`**, you'll want a copy of **[setup.cfg](https://github.com/seleniumbase/SeleniumBase/blob/master/setup.cfg)** in your root folders. These files specify default configuration details for tests. Test folders should also include a blank **[__init__.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/offline_examples/__init__.py)** file to allow your test files to import other files from that folder. -### **Test Directory Customization:** +๐Ÿ”ต `sbase mkdir DIR` creates a folder with config files and sample tests: -For running tests outside of the SeleniumBase repo with **Pytest**, you'll want a copy of **[pytest.ini](https://github.com/seleniumbase/SeleniumBase/blob/master/pytest.ini)** on the root folder. For running tests outside of the SeleniumBase repo with **Nosetests**, you'll want a copy of **[setup.cfg](https://github.com/seleniumbase/SeleniumBase/blob/master/setup.cfg)** on the root folder. (Subfolders should include a blank ``__init__.py`` file.) +```zsh +sbase mkdir ui_tests +``` -As a shortcut, you'll be able to run ``seleniumbase mkdir [DIRECTORY_NAME]`` to create a new folder that already contains necessary files and some example tests that you can run. Example: +> That new folder will have these files: +```zsh +ui_tests/ +โ”œโ”€โ”€ __init__.py +โ”œโ”€โ”€ my_first_test.py +โ”œโ”€โ”€ parameterized_test.py +โ”œโ”€โ”€ pytest.ini +โ”œโ”€โ”€ requirements.txt +โ”œโ”€โ”€ setup.cfg +โ”œโ”€โ”€ test_demo_site.py +โ””โ”€โ”€ boilerplates/ + โ”œโ”€โ”€ __init__.py + โ”œโ”€โ”€ base_test_case.py + โ”œโ”€โ”€ boilerplate_test.py + โ”œโ”€โ”€ classic_obj_test.py + โ”œโ”€โ”€ page_objects.py + โ”œโ”€โ”€ sb_fixture_test.py + โ””โ”€โ”€ samples/ + โ”œโ”€โ”€ __init__.py + โ”œโ”€โ”€ google_objects.py + โ”œโ”€โ”€ google_test.py + โ”œโ”€โ”€ sb_swag_test.py + โ””โ”€โ”€ swag_labs_test.py ``` -seleniumbase mkdir browser_tests -cd browser_tests -pytest my_first_test.py --browser=chrome + +ProTipโ„ข: You can also create a boilerplate folder without any sample tests in it by adding `-b` or `--basic` to the `sbase mkdir` command: + +```zsh +sbase mkdir ui_tests --basic ``` - -### **Creating Visual Test Suite Reports:** +> That new folder will have these files: -(NOTE: Several command line args are different for Pytest vs Nosetests) +```zsh +ui_tests/ +โ”œโ”€โ”€ __init__.py +โ”œโ”€โ”€ pytest.ini +โ”œโ”€โ”€ requirements.txt +โ””โ”€โ”€ setup.cfg +``` -#### **Pytest Reports:** +Of those files, the `pytest.ini` config file is the most important, followed by a blank `__init__.py` file. There's also a `setup.cfg` file (for pynose). Finally, the `requirements.txt` file can be used to help you install seleniumbase into your environments (if it's not already installed). -Using ``--html=report.html`` gives you a fancy report of the name specified after your test suite completes. +ProTipโ„ข: Add `--gha` to include a GitHub Actions `.yml` file with default settings: -``` -pytest test_suite.py --html=report.html +```zsh +ui_tests/ +โ””โ”€โ”€ .github + โ””โ”€โ”€ workflows/ + โ””โ”€โ”€ python-package.yml ``` -![](https://cdn2.hubspot.net/hubfs/100006/images/PytestReport.png "Example Pytest Report") +-------- -You can also use ``--junitxml=report.xml`` to get an xml report instead. Jenkins can use this file to display better reporting for your tests. +

Log files from failed tests:

-``` -pytest test_suite.py --junitxml=report.xml -``` +Let's try an example of a test that fails: -#### **Nosetest Reports:** +```python +""" test_fail.py """ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) -The ``--report`` option gives you a fancy report after your test suite completes. +class MyTestClass(BaseCase): + def test_find_army_of_robots_on_xkcd_desert_island(self): + self.goto("https://xkcd.com/731/") + self.assert_element("div#ARMY_OF_ROBOTS", timeout=1) # This should fail ``` -nosetests test_suite.py --report + +You can run it from the `examples/` folder like this: + +```zsh +pytest test_fail.py ``` - -(NOTE: You can add ``--show_report`` to immediately display Nosetest reports after the test suite completes. Only use ``--show_report`` when running tests locally because it pauses the test run.) +๐Ÿ”ต You'll notice that a logs folder, `./latest_logs/`, was created to hold information (and screenshots) about the failing test. During test runs, past results get moved to the archived_logs folder if you have ARCHIVE_EXISTING_LOGS set to True in [settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py), or if your run tests with `--archive-logs`. If you choose not to archive existing logs, they will be deleted and replaced by the logs of the latest test run. +-------- -### **Using a Proxy Server:** + +

SeleniumBase Dashboard:

-If you wish to use a proxy server for your browser tests (Chrome and Firefox only), you can add ``--proxy=IP_ADDRESS:PORT`` as an argument on the command line. +๐Ÿ”ต The `--dashboard` option for pytest generates a SeleniumBase Dashboard located at `dashboard.html`, which updates automatically as tests run and produce results. Example: -``` -pytest proxy_test.py --proxy=IP_ADDRESS:PORT +```zsh +pytest --dashboard --rs --headless ``` -If the proxy server that you wish to use requires authentication, you can do the following (Chrome only): +The SeleniumBase Dashboard -``` -pytest proxy_test.py --proxy=USERNAME:PASSWORD@IP_ADDRESS:PORT +๐Ÿ”ต Additionally, you can host your own SeleniumBase Dashboard Server on a port of your choice. Here's an example of that using Python's `http.server`: + +```zsh +python -m http.server 1948 ``` -To make things easier, you can add your frequently-used proxies to PROXY_LIST in [proxy_list.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/proxy_list.py), and then use ``--proxy=KEY_FROM_PROXY_LIST`` to use the IP_ADDRESS:PORT of that key. +๐Ÿ”ต Now you can navigate to `http://localhost:1948/dashboard.html` in order to view the dashboard as a web app. This requires two different terminal windows: one for running the server, and another for running the tests, which should be run from the same directory. (Use Ctrl+C to stop the http server.) -``` -pytest proxy_test.py --proxy=proxy1 +๐Ÿ”ต Here's a full example of what the SeleniumBase Dashboard may look like: + +```zsh +pytest test_suite.py test_image_saving.py --dashboard --rs --headless ``` +The SeleniumBase Dashboard -### **Changing the User-Agent:** +-------- -If you wish to change the User-Agent for your browser tests (Chrome and Firefox only), you can add ``--agent="USER AGENT STRING"`` as an argument on the command line. + +

Generating Test Reports:

-```bash -pytest user_agent_test.py --agent="Mozilla/5.0 (Nintendo 3DS; U; ; en) Version/1.7412.EU" -``` +

๐Ÿ”ต pytest HTML Reports:

+โœ… Using `--html=report.html` gives you a fancy report of the name specified after your test suite completes. -### **Building Guided Tours for Websites:** +```zsh +pytest test_suite.py --html=report.html +``` -Learn about [SeleniumBase Interactive Walkthroughs](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/tour_examples) (in the ``examples/tour_examples`` folder). It's great for prototyping a website onboarding experience. See the [Tours ReadMe](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/tour_examples/ReadMe.md) for more details. +Example Pytest Report +โœ… When combining pytest html reports with SeleniumBase Dashboard usage, the pie chart from the Dashboard will get added to the html report. Additionally, if you set the html report URL to be the same as the Dashboard URL when also using the dashboard, (example: `--dashboard --html=dashboard.html`), then the Dashboard will become an advanced html report when all the tests complete. - -### **Production Environments & Integrations:** +โœ… Here's an example of an upgraded html report: -Here are some things you can do to setup a production environment for your testing: +```zsh +pytest test_suite.py --dashboard --html=report.html +``` -* You can setup a [Jenkins](https://jenkins.io/) build server for running tests at regular intervals. Jenkins has many plugins available, such as [the Xvfb headless browser plugin](https://wiki.jenkins-ci.org/display/JENKINS/Xvfb+Plugin) for running tests on a machine with no GUI. If you have Xvfb running in the background, you can add ``--headless`` to your run command in order to utilize it. For more info about the Xvfb plugin, [read this](https://qxf2.com/blog/xvfb-plugin-for-jenkins-selenium/). For a real-world Jenkins example of headless browser automation in action, check out [the SeleniumBase Google Cloud ReadMe](https://github.com/seleniumbase/SeleniumBase/blob/master/integrations/google_cloud/ReadMe.md). +Dashboard Pytest HTML Report -* You can use [the Selenium Grid](https://github.com/SeleniumHQ/selenium/wiki/Grid2) to scale your testing by distributing tests on several machines with parallel execution. To do this, check out the SeleniumBase [selenium_grid folder](https://github.com/seleniumbase/SeleniumBase/tree/master/seleniumbase/utilities/selenium_grid), which should have everything you need. The [Selenium Grid ReadMe](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/utilities/selenium_grid/ReadMe.md) will help you get started. +If viewing pytest html reports in [Jenkins](https://www.jenkins.io/), you may need to [configure Jenkins settings](https://stackoverflow.com/a/46197356/7058266) for the html to render correctly. This is due to [Jenkins CSP changes](https://www.jenkins.io/doc/book/system-administration/security/configuring-content-security-policy/). -* If you're using the [SeleniumBase MySQL feature](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/mysql_installation.md) to save results from tests running on a server machine, you can install [MySQL Workbench](https://dev.mysql.com/downloads/tools/workbench/) to help you read & write from your DB more easily. You'll also need to install the MySQL Python client. Depending on your system, you may need to install additional requirements for this (such as on Windows). See [Stackoverflow](https://stackoverflow.com/questions/43102442/whats-the-difference-between-mysqldb-mysqlclient-and-mysql-connector-python) for more info. +You can also use `--junit-xml=report.xml` to get an xml report instead. Jenkins can use this file to display better reporting for your tests. +```zsh +pytest test_suite.py --junit-xml=report.xml ``` -pip install mysqlclient==1.4.2 + +

๐Ÿ”ต pynose Reports:

+ +The `--report` option gives you a fancy report after your test suite completes. + +```zsh +pynose test_suite.py --report ``` -* If you use [Slack](https://slack.com), you can easily have your Jenkins jobs display results there by using the [Jenkins Slack Plugin](https://github.com/jenkinsci/slack-plugin). Another way to send messages from your tests to Slack is by using [Slack's Incoming Webhooks API](https://api.slack.com/incoming-webhooks). +Example pynose Report + +(NOTE: You can add `--show-report` to immediately display pynose reports after the test suite completes. Only use `--show-report` when running tests locally because it pauses the test run.) + +

๐Ÿ”ต behave Dashboard & Reports:

- -* If you're using AWS, you can setup an [Amazon S3](https://aws.amazon.com/s3/) account for saving your log files and screenshots for future viewing. SeleniumBase already has [all the code you need to connect to S3](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/plugins/s3_logging_plugin.py). You'll need to modify [settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py) with connection details to your instance and the location in S3 where you want log files to be saved. You'll also need to add "``--with-s3_logging``" on the command line when you run your tests. +(The [behave_bdd/](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/behave_bdd) folder can be found in the [examples/](https://github.com/seleniumbase/SeleniumBase/tree/master/examples) folder.) -Here's an example of running tests with additional features enabled: +```zsh +behave behave_bdd/features/ -D dashboard -D headless ``` -pytest [YOUR_TEST_FILE].py --browser=chrome --with-db_reporting --with-s3_logging -s + + + +You can also use `--junit` to get `.xml` reports for each behave feature. Jenkins can use these files to display better reporting for your tests. + +```zsh +behave behave_bdd/features/ --junit -D rs -D headless ``` -(NOTE: If you haven't configured your MySQL or S3 connections in [settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py), don't use ``--with-db_reporting`` or ``--with-s3_logging``.) -When the testing_base plugin is used, if there's a test failure, the basic_test_info plugin records test logs, the page_source plugin records the page source of the last web page seen by the test, and the screen_shots plugin records the image of the last page seen by the test where the failure occurred. Make sure you always include testing_base whenever you include a plugin that logs test data. The db_reporting plugin records the status of all tests run into your MySQL DB. The s3_logging plugin uploads basic test info, screenshots, and page source into your S3 storage folder. +

๐Ÿ”ต Allure Reports:

+ +See: [https://allurereport.org/docs/pytest/](https://allurereport.org/docs/pytest/) -To simplify that long run command, you can create a ``*.cfg`` file, such as the one provided in the example, and enter your plugins there so that you can run everything by typing: +SeleniumBase no longer includes `allure-pytest` as part of installed dependencies. If you want to use it, install it first: +```zsh +pip install allure-pytest ``` -nosetests [YOUR_TEST_FILE].py --config=[MY_CONFIG_FILE].cfg + +Now your tests can create Allure results files, which can be processed by Allure Reports. + +```zsh +pytest test_suite.py --alluredir=allure_results ``` -You can simplify that even more by using a setup.cfg file, such as the one provided for you in the examples folder. If you kick off a test run from within the folder that setup.cfg is location in, that file will automatically be used as your configuration, meaning that you wouldn't have to type out all the plugins that you want to use (or include a config file) everytime you run tests. +-------- + +

Using a Proxy Server:

-If you tell pytest/nosetests to run an entire file, it will run every method in that python file that starts with "test". You can be more specific on what to run by doing something like the following: (Note that the syntax is different for pytest vs nosetests.) +If you wish to use a proxy server for your browser tests (Chromium or Firefox), you can add `--proxy=IP_ADDRESS:PORT` as an argument on the command line. +```zsh +pytest proxy_test.py --proxy=IP_ADDRESS:PORT ``` -pytest [YOUR_TEST_FILE].py::[SOME_CLASS_NAME]::test_[SOME_TEST_NAME] -nosetests [YOUR_TEST_FILE].py:[SOME_CLASS_NAME].test_[SOME_TEST_NAME] + +If the proxy server that you wish to use requires authentication, you can do the following (Chromium only): + +```zsh +pytest proxy_test.py --proxy=USERNAME:PASSWORD@IP_ADDRESS:PORT ``` -Let's try an example of a test that fails: -```python -""" test_fail.py """ -from seleniumbase import BaseCase +SeleniumBase also supports SOCKS4 and SOCKS5 proxies: -class MyTestClass(BaseCase): +```zsh +pytest proxy_test.py --proxy="socks4://IP_ADDRESS:PORT" - def test_find_army_of_robots_on_xkcd_desert_island(self): - self.open("https://xkcd.com/731/") - self.assert_element("div#ARMY_OF_ROBOTS", timeout=1) # This should fail +pytest proxy_test.py --proxy="socks5://IP_ADDRESS:PORT" ``` -You can run it from the ``examples`` folder like this: +To make things easier, you can add your frequently-used proxies to PROXY_LIST in [proxy_list.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/proxy_list.py), and then use `--proxy=KEY_FROM_PROXY_LIST` to use the IP_ADDRESS:PORT of that key. +```zsh +pytest proxy_test.py --proxy=proxy1 ``` -pytest test_fail.py + + +

Changing the User-Agent:

+ +๐Ÿ”ต If you wish to change the User-Agent for your browser tests (Chromium and Firefox only), you can add `--agent="USER AGENT STRING"` as an argument on the command-line. + +```zsh +pytest user_agent_test.py --agent="Mozilla/5.0 (Nintendo 3DS; U; ; en) Version/1.7412.EU" ``` -You'll notice that a logs folder, "latest_logs", was created to hold information about the failing test, and screenshots. Take a look at what you get. Remember, this data can be saved in your MySQL DB and in S3 if you include the necessary plugins in your run command (and if you set up the neccessary connections properly). For future test runs, past test results will get stored in the archived_logs folder if you have ARCHIVE_EXISTING_LOGS set to True in [settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py). +

Handling Pop-Up Alerts:

+ +๐Ÿ”ต self.accept_alert() automatically waits for and accepts alert pop-ups. self.dismiss_alert() automatically waits for and dismisses alert pop-ups. On occasion, some methods like self.click(SELECTOR) might dismiss a pop-up on its own because they call JavaScript to make sure that the readyState of the page is complete before advancing. If you're trying to accept a pop-up that got dismissed this way, use this workaround: Call self.find_element(SELECTOR).click() instead, (which will let the pop-up remain on the screen), and then use self.accept_alert() to accept the pop-up (more on that here). If pop-ups are intermittent, wrap code in a try/except block. + + +

Building Guided Tours for Websites:

+ +๐Ÿ”ต Learn about SeleniumBase Interactive Walkthroughs (in the `examples/tour_examples/` folder). It's great for prototyping a website onboarding experience. + + + + +-------- + +
+

Production Environments & Integrations:

+ +
+
+ โ–ถ๏ธ Here are some things you can do to set up a production environment for your testing. (click to expand) + + + +Here's an example of running tests with some additional features enabled: + +```zsh +pytest [YOUR_TEST_FILE.py] --with-db-reporting --with-s3-logging +``` + +
- -### **Detailed Method Specifications and Examples:** +

Detailed Method Specifications and Examples:

-#### Navigating to a web page (and related commands) +๐Ÿ”ต **Navigating to a web page: (and related commands)** ```python -self.open("https://xkcd.com/378/") # This method opens the specified page. +self.goto("https://xkcd.com/378/") # This method opens the specified page. self.go_back() # This method navigates the browser to the previous page. @@ -327,8 +1202,8 @@ self.get_current_url() # This method returns the current page URL. self.get_page_source() # This method returns the current page source. ``` -**ProTipโ„ข:** You may need to use the get_page_source() method along with Python's find() command to parse through the source to find something that Selenium wouldn't be able to. (You may want to brush up on your Python programming skills for that.) -Ex: +ProTipโ„ข: You can use the self.get_page_source() method with Python's find() command to parse through HTML to find something specific. (For more advanced parsing, see the BeautifulSoup example.) + ```python source = self.get_page_source() head_open_tag = source.find('') @@ -336,7 +1211,7 @@ head_close_tag = source.find('', head_open_tag) everything_inside_head = source[head_open_tag+len(''):head_close_tag] ``` -#### Clicking +๐Ÿ”ต **Clicking:** To click an element on the page: @@ -344,51 +1219,47 @@ To click an element on the page: self.click("div#my_id") ``` -**ProTipโ„ข:** In most web browsers, you can right-click on a page and select ``Inspect Element`` to see the CSS selector details that you'll need to create your own scripts. +**ProTipโ„ข:** In most web browsers, you can right-click on a page and select `Inspect Element` to see the CSS selector details that you'll need to create your own scripts. -#### Typing Text +๐Ÿ”ต **Typing Text:** -self.update_text(selector, text) # updates the text from the specified element with the specified value. An exception is raised if the element is missing or if the text field is not editable. Example: +self.type(selector, text) # updates the text from the specified element with the specified value. An exception is raised if the element is missing or if the text field is not editable. Example: ```python -self.update_text("input#id_value", "2012") +self.type("input#id_value", "2012") ``` -You can also use self.add_text() or the WebDriver .send_keys() command, but those won't clear the text box first if there's already text inside. -If you want to type in special keys, that's easy too. Here's an example: +You can also use self.add_text() or the WebDriver .send_keys() command, but those won't clear the text box first if there's already text inside. -```python -from selenium.webdriver.common.keys import Keys -self.find_element("textarea").send_keys(Keys.SPACE + Keys.BACK_SPACE + '\n') # the backspace should cancel out the space, leaving you with the newline -``` - -#### Getting the text from an element on a page +๐Ÿ”ต **Getting the text from an element on a page:** ```python text = self.get_text("header h2") ``` -#### Getting the attribute value from an element on a page +๐Ÿ”ต **Getting the attribute value from an element on a page:** ```python attribute = self.get_attribute("#comic img", "title") ``` -#### Asserting existance of an element on a page within some number of seconds: +๐Ÿ”ต **Asserting existence of an element on a page within some number of seconds:** ```python self.wait_for_element_present("div.my_class", timeout=10) ``` -(NOTE: You can also use: ``self.assert_element_present(ELEMENT)``) -#### Asserting visibility of an element on a page within some number of seconds: +(NOTE: You can also use: `self.assert_element_present(ELEMENT)`) + +๐Ÿ”ต **Asserting visibility of an element on a page within some number of seconds:** ```python self.wait_for_element_visible("a.my_class", timeout=5) ``` -(NOTE: The short versions of this are ``self.find_element(ELEMENT)`` and ``self.assert_element(ELEMENT)``. The find_element() version returns the element) -Since the line above returns the element, you can combine that with .click() as shown below: +(NOTE: The short versions of that are `self.find_element(ELEMENT)` and `self.assert_element(ELEMENT)`. The `find_element()` version returns the element.) + +Since the line above returns the element, you can combine that with `.click()` as shown below: ```python self.find_element("a.my_class", timeout=5).click() @@ -398,48 +1269,52 @@ self.find_element("a.my_class", timeout=5).click() self.click("a.my_class") # DO IT THIS WAY! ``` -**ProTipโ„ข:** You can use dots to signify class names (Ex: ``div.class_name``) as a simplified version of ``div[class="class_name"]`` within a CSS selector. +**ProTipโ„ข:** You can use dots to signify class names (Ex: `div.class_name`) as a simplified version of `div[class="class_name"]` within a CSS selector. -You can also use ``*=`` to search for any partial value in a CSS selector as shown below: +You can also use `*=` to search for any partial value in a CSS selector as shown below: ```python self.click('a[name*="partial_name"]') ``` -#### Asserting visibility of text inside an element on a page within some number of seconds: +๐Ÿ”ต **Asserting visibility of text inside an element on a page within some number of seconds:** ```python self.assert_text("Make it so!", "div#trek div.picard div.quotes") self.assert_text("Tea. Earl Grey. Hot.", "div#trek div.picard div.quotes", timeout=3) ``` -(NOTE: ``self.find_text(TEXT, ELEMENT)`` and ``self.wait_for_text(TEXT, ELEMENT)`` also do this. For backwords compatibility, older method names were kept, but the default timeout may be different.) -#### Asserting Anything +(NOTE: `self.find_text(TEXT, ELEMENT)` and `self.wait_for_text(TEXT, ELEMENT)` also do this. For backwards compatibility, older method names were kept, but the default timeout may be different.) + +๐Ÿ”ต **Asserting Anything:** ```python -self.assert_true(myvar1 == something) +self.assert_true(var1 == var2) + +self.assert_false(var1 == var2) self.assert_equal(var1, var2) ``` -#### Useful Conditional Statements (with creative examples in action) +๐Ÿ”ต **Useful Conditional Statements: (with creative examples)** + +โ“ `is_element_visible(selector):` (visible on the page) -is_element_visible(selector) # is an element visible on a page ```python -import logging if self.is_element_visible('div#warning'): - logging.debug("Red Alert: Something bad might be happening!") + print("Red Alert: Something bad might be happening!") ``` -is_element_present(selector) # is an element present on a page +โ“ `is_element_present(selector):` (present in the HTML) + ```python if self.is_element_present('div#top_secret img.tracking_cookie'): - self.contact_cookie_monster() # Not a real method unless you define it somewhere + self.contact_cookie_monster() # Not a real SeleniumBase method else: current_url = self.get_current_url() - self.contact_the_nsa(url=current_url, message="Dark Zone Found") # Not a real method unless you define it somewhere + self.contact_the_nsa(url=current_url, message="Dark Zone Found") # Not a real SeleniumBase method ``` -Another example: + ```python def is_there_a_cloaked_klingon_ship_on_this_page(): if self.is_element_present("div.ships div.klingon"): @@ -447,83 +1322,120 @@ def is_there_a_cloaked_klingon_ship_on_this_page(): return False ``` -is_text_visible(text, selector) # is text visible on a page +โ“ `is_text_visible(text, selector):` (text visible on element) + +```python +if self.is_text_visible("You Shall Not Pass!", "h1"): + self.goto("https://www.youtube.com/watch?v=3xYXUeSmb-Y") +``` + +
+
+ โ–ถ๏ธ Click for a longer example of is_text_visible(): + ```python def get_mirror_universe_captain_picard_superbowl_ad(superbowl_year): selector = "div.superbowl_%s div.commercials div.transcript div.picard" % superbowl_year - if self.is_text_visible("For the Love of Marketing and Earl Grey Tea!", selector): - return "Picard HubSpot Superbowl Ad 2015" + if self.is_text_visible("Yes, it was I who summoned you all here.", selector): + return "Picard Paramount+ Superbowl Ad 2020" + elif self.is_text_visible("Commander, signal the following: Our Network is Secure!"): + return "Picard Mirror Universe iboss Superbowl Ad 2018" + elif self.is_text_visible("For the Love of Marketing and Earl Grey Tea!", selector): + return "Picard Mirror Universe HubSpot Superbowl Ad 2015" elif self.is_text_visible("Delivery Drones... Engage", selector): - return "Picard Amazon Superbowl Ad 2015" + return "Picard Mirror Universe Amazon Superbowl Ad 2015" elif self.is_text_visible("Bing it on Screen!", selector): - return "Picard Microsoft Superbowl Ad 2015" + return "Picard Mirror Universe Microsoft Superbowl Ad 2015" elif self.is_text_visible("OK Glass, Make it So!", selector): - return "Picard Google Superbowl Ad 2015" + return "Picard Mirror Universe Google Superbowl Ad 2015" elif self.is_text_visible("Number One, I've Never Seen Anything Like It.", selector): - return "Picard Tesla Superbowl Ad 2015" + return "Picard Mirror Universe Tesla Superbowl Ad 2015" + elif self.is_text_visible("Let us make sure history never forgets the name ... Facebook", selector): + return "Picard Mirror Universe Facebook Superbowl Ad 2015" elif self.is_text_visible("""With the first link, the chain is forged. The first speech censored, the first thought forbidden, the first freedom denied, chains us all irrevocably.""", selector): - return "Picard Wikimedia Superbowl Ad 2015" - elif self.is_text_visible("Let us make sure history never forgets the name ... Facebook", selector): - return "Picard Facebook Superbowl Ad 2015" + return "Picard Mirror Universe Wikimedia Superbowl Ad 2015" else: raise Exception("Reports of my assimilation are greatly exaggerated.") ``` -#### Switching Tabs +
-What if your test opens up a new tab/window and now you have more than one page? No problem. You need to specify which one you currently want Selenium to use. Switching between tabs/windows is easy: -Ex: +โ“ `is_link_text_visible(link_text):` ```python -self.switch_to_window(1) # this switches to the new tab (0 is the first one) +if self.is_link_text_visible("Stop! Hammer time!"): + self.click_link("Stop! Hammer time!") ``` -**ProTipโ„ข:** iFrames follow the same principle as new windows - you need to specify the iFrame if you want to take action on something in there -Ex: +

๐Ÿ”ต Switching Tabs:

+ +

If your test opens up a new tab/window, you can switch to it. (SeleniumBase automatically switches to new tabs that don't open to about:blank URLs.)

```python -self.switch_to_frame('ContentManagerTextBody_ifr') -# Now you can act inside the iFrame -# .... Do something cool (here) -self.switch_to_default_content() # exit the iFrame when you're done +self.switch_to_window(1) # This switches to the new tab (0 is the first one) ``` -#### Handle Pop-Up Alerts +

๐Ÿ”ต How to handle iframes:

-What if your test makes an alert pop up in your browser? No problem. You need to switch to it and either accept it or dismiss it: -Ex: +๐Ÿ”ต iframes follow the same principle as new windows: You must first switch to the iframe if you want to perform actions in there: ```python -self.wait_for_and_accept_alert() +self.switch_to_frame("iframe") +# ... Now perform actions inside the iframe +self.switch_to_parent_frame() # Exit the current iframe +``` -self.wait_for_and_dismiss_alert() +To exit from multiple iframes, use `self.switch_to_default_content()`. (If inside a single iframe, this has the same effect as `self.switch_to_parent_frame()`.) + +```python +self.switch_to_frame('iframe[name="frame1"]') +self.switch_to_frame('iframe[name="frame2"]') +# ... Now perform actions inside the inner iframe +self.switch_to_default_content() # Back to the main page ``` -If you're not sure whether there's an alert before trying to accept or dismiss it, one way to handle that is to wrap your alert-handling code in a try/except block. Other methods such as .text and .send_keys() will also work with alerts. +๐Ÿ”ต You can also use a context manager to act inside iframes: -#### Executing Custom jQuery Scripts: +```python +with self.frame_switch("iframe"): + # ... Now perform actions while inside the code block +# You have left the iframe +``` + +This also works with nested iframes: + +```python +with self.frame_switch('iframe[name="frame1"]'): + with self.frame_switch('iframe[name="frame2"]'): + # ... Now perform actions while inside the code block + # You are now back inside the first iframe +# You have left all the iframes +``` -jQuery is a powerful JavaScript library that allows you to perform advanced actions in a web browser. +

๐Ÿ”ต How to execute custom jQuery scripts:

+ +

jQuery is a powerful JavaScript library that allows you to perform advanced actions in a web browser. If the web page you're on already has jQuery loaded, you can start executing jQuery scripts immediately. -You'd know this because the web page would contain something like the following in the HTML: +You'd know this because the web page would contain something like the following in the HTML:

```html - + ``` -It's OK if you want to use jQuery on a page that doesn't have it loaded yet. To do so, run the following command first: +๐Ÿ”ต It's OK if you want to use jQuery on a page that doesn't have it loaded yet. To do so, run the following command first: ```python self.activate_jquery() ``` -Some websites have a restrictive [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) to prevent users from loading jQuery and other external libraries onto their websites. If you need to use jQuery or another JS library on such a website, use Firefox with SeleniumBase, which overrides the CSP to allow loading of any JS library. +
+
+ โ–ถ๏ธ Here are some examples of using jQuery in your scripts. (click to expand) -Here are some examples of using jQuery in your scripts: ```python -self.execute_script('jQuery, window.scrollTo(0, 600)') # Scrolling the page +self.execute_script("jQuery, window.scrollTo(0, 600)") # Scrolling the page self.execute_script("jQuery('#annoying-widget').hide()") # Hiding elements on a page @@ -542,88 +1454,112 @@ self.execute_script("return jQuery('div#amazing')[0].text") # Returns the css " self.execute_script("return jQuery('textarea')[2].value") # Returns the css "value" of the 3rd textarea element on the page ``` -In the following example, JavaScript is used to plant code on a page that Selenium can then touch after that: +(Most of the above commands can be done directly with built-in SeleniumBase methods.) + +
+ +

๐Ÿ”ต How to handle a restrictive CSP:

+ +โ— Some websites have a restrictive [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) to prevent users from loading jQuery and other external libraries onto their websites. If you need to use jQuery or another JS library on those websites, add `--disable-csp` as a `pytest` command-line option to load a Chromium extension that bypasses the CSP. + +

๐Ÿ”ต More JavaScript fun:

+ +
+
+ โ–ถ๏ธ In this example, JavaScript creates a referral button on a page, which is then clicked. (click to expand) + ```python start_page = "https://xkcd.com/465/" destination_page = "https://github.com/seleniumbase/SeleniumBase" -self.open(start_page) +self.goto(start_page) referral_link = '''Free-Referral Button!''' % destination_page self.execute_script('''document.body.innerHTML = \"%s\"''' % referral_link) self.click("a.analytics") # Clicks the generated button ``` -(Due to popular demand, this traffic generation example has been baked into SeleniumBase with the ``self.generate_referral(start_page, end_page)`` and the ``self.generate_traffic(start_page, end_page, loops)`` methods.) -#### Using delayed asserts: +(Due to popular demand, this traffic generation example has been included in SeleniumBase with the self.generate_referral(start_page, end_page) and the self.generate_traffic(start_page, end_page, loops) methods.) -Let's say you want to verify multiple different elements on a web page in a single test, but you don't want the test to fail until you verified several elements at once so that you don't have to rerun the test to find more missing elements on the same page. That's where delayed asserts come in. Here's the example: +
+ +

๐Ÿ”ต How to use deferred asserts:

+ +

Let's say you want to verify multiple different elements on a web page in a single test, but you don't want the test to fail until you verified several elements at once so that you don't have to rerun the test to find more missing elements on the same page. That's where deferred asserts come in. Here's an example:

```python from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) -class MyTestClass(BaseCase): - - def test_delayed_asserts(self): - self.open('https://xkcd.com/993/') - self.wait_for_element('#comic') - self.delayed_assert_element('img[alt="Brand Identity"]') - self.delayed_assert_element('img[alt="Rocket Ship"]') # Will Fail - self.delayed_assert_element('#comicmap') - self.delayed_assert_text('Fake Item', '#middleContainer') # Will Fail - self.delayed_assert_text('Random', '#middleContainer') - self.delayed_assert_element('a[name="Super Fake !!!"]') # Will Fail - self.process_delayed_asserts() +class DeferredAssertTests(BaseCase): + def test_deferred_asserts(self): + self.goto("https://xkcd.com/993/") + self.wait_for_element("#comic") + self.deferred_assert_element('img[alt="Brand Identity"]') + self.deferred_assert_element('img[alt="Rocket Ship"]') # Will Fail + self.deferred_assert_element("#comicmap") + self.deferred_assert_text("Fake Item", "ul.comicNav") # Will Fail + self.deferred_assert_text("Random", "ul.comicNav") + self.deferred_assert_element('a[name="Super Fake !!!"]') # Will Fail + self.deferred_assert_exact_text("Brand Identity", "#ctitle") + self.deferred_assert_exact_text("Fake Food", "#comic") # Will Fail + self.process_deferred_asserts() ``` -``delayed_assert_element()`` and ``delayed_assert_text()`` will save any exceptions that would be raised. -To flush out all the failed delayed asserts into a single exception, make sure to call ``self.process_delayed_asserts()`` at the end of your test method. If your test hits multiple pages, you can call ``self.process_delayed_asserts()`` at the end of all your delayed asserts for a single page. This way, the screenshot from your log file will have the location where the delayed asserts were made. +deferred_assert_element() and deferred_assert_text() will save any exceptions that would be raised. +To flush out all the failed deferred asserts into a single exception, make sure to call self.process_deferred_asserts() at the end of your test method. If your test hits multiple pages, you can call self.process_deferred_asserts() before navigating to a new page so that the screenshot from your log files matches the URL where the deferred asserts were made. -#### Accessing raw WebDriver +

๐Ÿ”ต How to access raw WebDriver:

+ +

If you need access to any commands that come with standard WebDriver, you can call them directly like this:

-If you need access to any commands that come with standard WebDriver, you can call them directly like this: ```python self.driver.delete_all_cookies() capabilities = self.driver.capabilities -self.driver.find_elements_by_partial_link_text("GitHub") +self.driver.find_elements("partial link text", "GitHub") ``` + (In general, you'll want to use the SeleniumBase versions of methods when available.) -#### Retrying failing tests automatically +

๐Ÿ”ต How to retry failing tests automatically:

-You can use ``--reruns NUM`` to retry failing tests that many times. Use ``--reruns-delay SECONDS`` to wait that many seconds between retries. Example: -``` -pytest --reruns 5 --reruns-delay 1 +

You can use pytest --reruns=NUM to retry failing tests that many times. Add --reruns-delay=SECONDS to wait that many seconds between retries. Example:

+ +```zsh +pytest --reruns=1 --reruns-delay=1 ``` -Additionally, you can use the ``@retry_on_exception()`` decorator to specifically retry failing methods. (First import: ``from seleniumbase import decorators``) To learn more about SeleniumBase decorators, [click here](https://github.com/seleniumbase/SeleniumBase/tree/master/seleniumbase/common). +

You can use the @retry_on_exception() decorator to retry failing methods. (First import: from seleniumbase import decorators). To learn more about SeleniumBase decorators, click here.

-#### Checking Email: -Let's say you have a test that sends an email, and now you want to check that the email was received: +-------- -```python -from seleniumbase.fixtures.email_manager import EmailManager, EmailException -num_email_results = 0 -email_subject = "This is the subject to search for (maybe include a timestamp)" -email_manager = EmailManager("{YOUR SELENIUM GMAIL ACCOUNT EMAIL ADDRESS}") # the password for this would be stored in seleniumbase/config/settings.py -try: - html_text = email_manager.search(SUBJECT="%s" % email_subject, timeout=300) - num_email_results = len(html_text) -except EmailException: - num_email_results = 0 -self.assert_true(num_email_results) # true if not zero -``` +> "Catch bugs in QA before deploying code to Production!" + +

Catch bugs in QA before deploying code to Production!

-Now you can parse through the email if you're looking for specific text or want to navigate to a link listed there. +-------- +

Wrap-Up

-### Wrap-Up +

+

If you see something, say something!
+
+

-#### Congratulations on getting started with SeleniumBase! +

SeleniumBase

-**Questions or Comments?**
-[![Join the chat at https://gitter.im/seleniumbase/SeleniumBase](https://badges.gitter.im/seleniumbase/SeleniumBase.svg)](https://gitter.im/seleniumbase/SeleniumBase?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+ -If you see something, say something! We are very active in resolving issues. [Closed Issues](https://github.com/seleniumbase/SeleniumBase/issues?q=is%3Aissue+is%3Aclosed) +

+SeleniumBase Playlist on YouTube +SeleniumBase on GitHub +SeleniumBase on Discord +SeleniumBase on Facebook +

-[https://github.com/mdmintz](https://github.com/mdmintz)
+

https://github.com/mdmintz

-[](https://github.com/seleniumbase/SeleniumBase/blob/master/README.md)

[MIT License](https://github.com/seleniumbase/SeleniumBase/blob/master/LICENSE) +
+
Tested with SeleniumBase Views
+
SeleniumBase Docs
+
+
SeleniumBase PyPI downloads
+
YouTube Channel
diff --git a/SECURITY.md b/SECURITY.md new file mode 100755 index 00000000000..8bb81c8eb05 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,9 @@ +# Security Policy + +## Reporting a Vulnerability + +If you've found a security vulnerability in SeleniumBase, (or a dependency we use), please open an issue. + +[github.com/seleniumbase/SeleniumBase/issues](https://github.com/seleniumbase/SeleniumBase/issues) + +Please describe the results you're seeing, and the results you're expecting. diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c581444b63b..c38d7a81aa4 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -8,18 +8,20 @@ jobs: - job: 'Test' pool: - vmImage: 'Ubuntu-16.04' + vmImage: 'Ubuntu-22.04' strategy: matrix: - Python27: - python.version: '2.7' - Python35: - python.version: '3.5' - Python36: - python.version: '3.6' - Python37: + Python3_7: python.version: '3.7' - maxParallel: 4 + Python3_8: + python.version: '3.8' + Python3_9: + python.version: '3.9' + Python3_10: + python.version: '3.10' + Python3_11: + python.version: '3.11' + maxParallel: 5 steps: - task: UsePythonVersion@0 @@ -27,35 +29,57 @@ jobs: versionSpec: '$(python.version)' architecture: 'x64' - - script: python -m pip install --upgrade pip && pip install -r requirements.txt - displayName: 'Install dependencies' + - script: python -m pip install --upgrade pip && pip --version + displayName: 'Install/upgrade pip' - - script: python setup.py develop + - script: python -m pip install seleniumbase + displayName: 'Verify install from PyPI' + + #- script: python -m pip install -r requirements.txt --upgrade + # displayName: 'Install dependencies' + + - script: python -m pip install -e . displayName: 'Install SeleniumBase' - script: | sudo apt install google-chrome-stable - sudo apt-get install firefox - displayName: 'Install Chrome and Firefox' + displayName: 'Install Chrome' + + #- script: | + # sudo apt-get install firefox + # displayName: 'Install Firefox' + + - script: | + seleniumbase + sbase + displayName: 'Check the console scripts interface' - script: | seleniumbase install chromedriver - seleniumbase install geckodriver - displayName: 'Install chromedriver and geckodriver (Firefox Webdriver)' + displayName: 'Install chromedriver' - script: | echo "def test_1(): pass" > nothing.py pytest nothing.py displayName: 'Make sure pytest is working' - - script: pytest examples/boilerplates/boilerplate_test.py --browser=chrome --headless -v -s --junitxml=junit/test-results.xml + #- script: python -m pytest examples/unit_tests/verify_framework.py + # displayName: 'Run pytest verify_framework.py' + + - script: python -m pytest examples/boilerplates/boilerplate_test.py --browser=chrome --headless -v -s --junit-xml=junit/test-results.xml displayName: 'Run pytest boilerplate_test.py --browser=chrome --headless' - - script: pytest examples/my_first_test.py --browser=chrome --headless -v -s --junitxml=junit/test-results.xml - displayName: 'Run pytest my_first_test.py --browser=chrome --headless' + #- script: python -m pytest examples/boilerplates/boilerplate_test.py --browser=chrome --headless -v -s --junit-xml=junit/test-results.xml + # displayName: 'Run pytest boilerplate_test.py --browser=firefox --headless' + + #- script: python -m pytest examples/test_demo_site.py --browser=chrome --headless -v -s --junit-xml=junit/test-results.xml + # displayName: 'Run pytest test_demo_site.py --browser=chrome --headless' - - script: pytest examples/my_first_test.py --browser=firefox --headless -v -s --junitxml=junit/test-results.xml - displayName: 'Run pytest my_first_test.py --browser=firefox --headless' + #- script: python -m pytest examples/my_first_test.py --browser=chrome --headless -v -s --junit-xml=junit/test-results.xml + # displayName: 'Run pytest my_first_test.py --browser=chrome --headless' + + #- script: python -m pytest examples/test_inspect_html.py --browser=chrome --headless -v -s --junit-xml=junit/test-results.xml + # displayName: 'Run pytest test_inspect_html.py --browser=chrome --headless' - task: PublishTestResults@2 inputs: @@ -63,16 +87,16 @@ jobs: testRunTitle: 'Python $(python.version)' condition: succeededOrFailed() -- job: 'Publish' - dependsOn: 'Test' - pool: - vmImage: 'Ubuntu-16.04' +#- job: 'Publish' +# dependsOn: 'Test' +# pool: +# vmImage: 'Ubuntu-22.04' - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: '3.x' - architecture: 'x64' +# steps: +# - task: UsePythonVersion@0 +# inputs: +# versionSpec: '3.x' +# architecture: 'x64' - - script: python setup.py sdist - displayName: 'Build sdist' +# - script: python setup.py sdist +# displayName: 'Build sdist' diff --git a/examples/ReadMe.md b/examples/ReadMe.md old mode 100755 new mode 100644 index 93a7140a7c7..fc1f6e06a96 --- a/examples/ReadMe.md +++ b/examples/ReadMe.md @@ -1,70 +1,271 @@ -## Running SeleniumBase Scripts + -To run tests, make sure you've already installed SeleniumBase using ``pip install seleniumbase`` OR ``pip install -r requirements.txt`` + ``python setup.py install`` from the top-level directory. +

Example Tests:

-You can interchange **pytest** with **nosetests**, but using pytest is strongly recommended because developers stopped supporting nosetests. Chrome is the default browser if not specified. +

SeleniumBase Demo Page

-During test failures, logs and screenshots from the most recent test run will get saved to the ``latest_logs/`` folder. Those logs will get moved to ``archived_logs/`` if you have ARCHIVE_EXISTING_LOGS set to True in [settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py) +* Many SeleniumBase "tests" run with pytest. +* Chrome is the default browser if not specified. +* Tests are structured using [25 unique design patterns](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/syntax_formats.md). +* Logs from test failures are saved to `./latest_logs/`. +* Tests can be run with [multiple command-line options](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/customizing_test_runs.md). +* Examples can be found in [SeleniumBase/examples/](https://github.com/seleniumbase/SeleniumBase/tree/master/examples). +* Stealthy CDP Mode examples: [SeleniumBase/examples/cdp_mode/](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/cdp_mode). +* Stealthy Playwright examples: [./examples/cdp_mode/playwright/](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/playwright/ReadMe.md). -(NOTE: Many tests in this folder fail on purpose to demonstrate the built-in logging, screenshots, and reporting features.) +(NOTE: Some example tests fail on purpose to demonstrate [logging features](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/example_logs/ReadMe.md).) -**Here are some example run commands to help get you started:** +-------- + +

Example tests with run commands:

+ +Run an [example test](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/my_first_test.py): (Default option: `--chrome`) -Run the example test in Chrome: -```bash -pytest my_first_test.py --browser=chrome +```zsh +pytest my_first_test.py ``` -Run the example test in Firefox: -```bash -pytest my_first_test.py --browser=firefox +
+ +-------- + +Here's one way of changing the browser to Firefox: + +```zsh +pytest my_first_test.py --firefox ``` -Run the example test in Demo Mode (highlights page objects being acted on): -```bash -pytest my_first_test.py --browser=chrome --demo_mode +-------- + +Another [example test](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_demo_site.py) for a web page that has lots of different HTML items: + +```zsh +pytest test_demo_site.py +``` + +
+ +-------- + +Run an example test in `--demo` mode: (highlight assertions) + +```zsh +pytest test_swag_labs.py --demo +``` + +
+ +-------- + +Run [test_coffee_cart.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_coffee_cart.py) to test the [Coffee Cart](https://seleniumbase.io/coffee/) app: + +```zsh +pytest test_coffee_cart.py --demo +``` + +SeleniumBase Example + +You can debug tests easily with the included `pdbp` (Pdb+) debugger: + +```zsh +pytest test_coffee_cart.py --trace +``` + +

SeleniumBase test with the pdbp debugger

+ +-------- + +Run a [Wordle-solver example](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/wordle_test.py): + +```zsh +pytest wordle_test.py +``` + +
+ +-------- + +Run an example test in `--headless` mode: (invisible browser) + +```zsh +pytest my_first_test.py --headless +``` + +-------- + +Run an [example test](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_swag_labs.py) using Chrome's mobile device emulator: (default settings) + +```zsh +pytest test_swag_labs.py --mobile +``` + +
+ +-------- + +Run an [example test](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_xkcd.py) in `--demo` mode: (highlight assertions) + +```zsh +pytest test_xkcd.py --demo +``` + +
+ +-------- + +Run a [test suite](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_suite.py) with verbose output: (see more details) + +```zsh +pytest test_suite.py -v +``` + +-------- + +Run a test suite using multiple parallel processes (`-n=NUM`): + +```zsh +pytest test_suite.py -n=8 +``` + +-------- + +Run a [parameterized test](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/parameterized_test.py): (Generates multiple tests from one) + +```zsh +pytest parameterized_test.py -v ``` -Run the example test suite and generate an pytest report: (pytest-only) -```bash +-------- + +Run a test suite and generate a SeleniumBase Dashboard: + +```zsh +pytest test_suite.py --dashboard +``` + +-------- + +Run a test suite and generate a `pytest` report: + +```zsh pytest test_suite.py --html=report.html ``` -Run the example test suite and generate a nosetest report: (nosetests-only) -```bash -nosetests test_suite.py --report --show_report +-------- + +Run a [failing test](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_fail.py): (See the `latest_logs/` folder for logs and screenshots) + +```zsh +pytest test_fail.py +``` + +-------- + +Run a failing test that activates `pdb` debug mode on failure: + +```zsh +pytest test_fail.py --pdb -s +``` + +> (**`pdb`** commands: `n`, `c`, `s`, `u`, `d` => `next`, `continue`, `step`, `up`, `down`) + +-------- + +Run a test suite that demonstrates the use of `pytest` markers: + +```zsh +pytest -m marker_test_suite -v ``` -Run a test using a nosetest configuration file: (nosetests-only) -```bash -nosetests my_first_test.py --config=example_config.cfg +-------- + +Run a test suite that reuses the browser session between tests: + +```zsh +pytest test_suite.py --rs ``` -Run a test demonstrating the use of SeleniumBase Python decorators available: -```bash +-------- + +Run an [example test](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/rate_limiting_test.py) demonstrating the `rate_limited` Python decorator: + +```zsh pytest rate_limiting_test.py ``` -Run a failing test: (See the ``latest_logs/`` folder afterwards for logs and screenshots) -```bash -pytest test_fail.py --browser=chrome +-------- + +Run an [example test](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/upload_file_test.py) that demonstrates how to upload a file to a website: + +```zsh +pytest upload_file_test.py +``` + +-------- + +๐ŸŽ–๏ธ **SeleniumBase Commander** is a GUI for `pytest`: + +```zsh +sbase gui +``` + +
+ +-------- + +SeleniumBase tests can also be run with `pynose`: + +```zsh +pynose my_first_test.py +``` + +-------- + +Run an example test suite and generate a `pynose` test report: + +```zsh +pynose test_suite.py --report --show-report ``` -Run a failing test with Debugging-mode enabled: (If a test failure occurs, pdb activates) -```bash -pytest test_fail.py --browser=chrome --pdb -s +-------- + +Run an example test using a `pynose` configuration file: + +```zsh +pynose my_first_test.py --config=example_config.cfg ``` -For more advanced run commands, such as using a proxy server, see [../help_docs/customizing_test_runs.md](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/customizing_test_runs.md) +-------- + +For more advanced **run commands**, such as using a proxy server, see [../help_docs/customizing_test_runs.md](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/customizing_test_runs.md) + +-------- + +If you just need to perform some quick website verification on various devices, you can use the SeleniumBase Device Farm. Just plug in a website URL, and it will display how the website looks on four different devices: + +
-------- -To makes things easier, here's a simple GUI program that allows you to kick off a few example scripts by pressing a button: +To make things easier, here's a **simple GUI program** that allows you to run a few example tests by pressing a button: -```bash +```zsh python gui_test_runner.py ``` - + + +(The newer **[SeleniumBase Commander](https://seleniumbase.io/help_docs/commander/)** improves on that.) + +-------- + +

Join the SeleniumBase chat on Discord Join the SeleniumBase chat on Discord!

+ +Ask questions. Find answers. Learn how to automate! + +-------- + + -(NOTE: If you see any ``*.pyc`` files appear as you run tests, that's perfectly normal. Compiled bytecode is a natural result of running Python code.) + +Tested with SeleniumBase diff --git a/examples/__init__.py b/examples/__init__.py old mode 100755 new mode 100644 diff --git a/examples/basic_test.py b/examples/basic_test.py old mode 100755 new mode 100644 index 75cf35568f6..28a1587aaf4 --- a/examples/basic_test.py +++ b/examples/basic_test.py @@ -1,16 +1,20 @@ -""" -Same as my_first_test.py, but without the asserts. -""" - +"""Add an item to a shopping cart. Verify. Remove item. Verify.""" from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) class MyTestClass(BaseCase): - - def test_basic(self): - self.open("https://xkcd.com/353/") - self.click('a[rel="license"]') - self.open("https://xkcd.com/1481/") - self.click("link=Blag") - self.update_text("input#s", "Robots!\n") - self.open("https://xkcd.com/1319/") + def test_basics(self): + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("div.inventory_list") + self.assert_exact_text("Products", "span.title") + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_exact_text("Your Cart", "span.title") + self.assert_text("Backpack", "div.cart_item") + self.click('button:contains("Remove")') # HTML innerText + self.assert_text_not_visible("Backpack", "div.cart_item") + self.js_click("a#logout_sidebar_link") + self.assert_element("div#login_button_container") diff --git a/examples/behave_bdd/ReadMe.md b/examples/behave_bdd/ReadMe.md new file mode 100644 index 00000000000..a3e840b3a63 --- /dev/null +++ b/examples/behave_bdd/ReadMe.md @@ -0,0 +1,243 @@ + + +## [](https://github.com/seleniumbase/SeleniumBase/) ๐Ÿ Behave test runner for SeleniumBase ๐Ÿ + +๐Ÿ (Utilizes the [Behave BDD Python library](https://github.com/behave/behave). For more info, see the [Behave tutorial](https://behave.readthedocs.io/en/stable/tutorial/) and read about [Behave's Gherkin model](https://behave.readthedocs.io/en/stable/gherkin/).) + +๐Ÿ Behave examples with SeleniumBase: [SeleniumBase/examples/behave_bdd](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/behave_bdd) + +```zsh +> cd examples/behave_bdd/ +> behave features/realworld.feature -T -D dashboard -k + +Dashboard: /Users/michael/github/SeleniumBase/examples/behave_bdd/dashboard.html +******************************************************************************** +Feature: SeleniumBase scenarios for the RealWorld App # features/realworld.feature:1 + + Scenario: Verify RealWorld App (log in / sign out) # features/realworld.feature:3 + Given Open "seleniumbase.io/realworld/login" # ../../sbase/steps.py:10 + And Clear Session Storage # ../../sbase/steps.py:669 + When Type "demo_user" into "#username" # ../../sbase/steps.py:40 + And Type "secret_pass" into "#password" # ../../sbase/steps.py:40 + And Do MFA "GAXG2MTEOR3DMMDG" into "#totpcode" # ../../sbase/steps.py:322 + Then Assert exact text "Welcome!" in "h1" # ../../sbase/steps.py:157 + And Highlight "img#image1" # ../../sbase/steps.py:184 + And Click 'a:contains("This Page")' # ../../sbase/steps.py:27 + And Save screenshot to logs # ../../sbase/steps.py:239 + When Click link "Sign out" # ../../sbase/steps.py:195 + Then Assert element 'a:contains("Sign in")' # ../../sbase/steps.py:120 + And Assert text "You have been signed out!" # ../../sbase/steps.py:145 + โœ… Scenario Passed! + +- Dashboard: /Users/michael/github/SeleniumBase/examples/behave_bdd/dashboard.html +--- LogPath: /Users/michael/github/SeleniumBase/examples/behave_bdd/latest_logs/ +================================================================================== +1 feature passed, 0 failed, 0 skipped +1 scenario passed, 0 failed, 0 skipped +12 steps passed, 0 failed, 0 skipped, 0 undefined +Took 0m4.682s +``` + +๐Ÿ Another example, which uses higher-level Behave steps to simplify the ``.feature`` file: + +```zsh +> cd examples/behave_bdd/ +> behave features/calculator.feature:61 -T -D dashboard -k + +Dashboard: /Users/michael/github/SeleniumBase/examples/behave_bdd/dashboard.html +******************************************************************************** +Feature: SeleniumBase scenarios for the Calculator App # features/calculator.feature:1 + + Background: # features/calculator.feature:3 + + Scenario: 7.0 ร— (3 + 3) = 42 # features/calculator.feature:49 + Given Open the Calculator App # features/steps/calculator.py:4 + When Press C # features/steps/calculator.py:9 + And Press 7 # features/steps/calculator.py:79 + And Press . # features/steps/calculator.py:104 + And Press 0 # features/steps/calculator.py:94 + And Press ร— # features/steps/calculator.py:29 + And Press ( # features/steps/calculator.py:14 + And Press 3 # features/steps/calculator.py:59 + And Press + # features/steps/calculator.py:39 + And Press 3 # features/steps/calculator.py:59 + And Press ) # features/steps/calculator.py:19 + Then Verify output is "7.0ร—(3+3)" # features/steps/calculator.py:135 + When Press = # features/steps/calculator.py:44 + Then Verify output is "42" # features/steps/calculator.py:135 + โœ… Scenario Passed! + +- Dashboard: /Users/michael/github/SeleniumBase/examples/behave_bdd/dashboard.html +--- LogPath: /Users/michael/github/SeleniumBase/examples/behave_bdd/latest_logs/ +================================================================================== +1 feature passed, 0 failed, 0 skipped +1 scenario passed, 0 failed, 8 skipped +14 steps passed, 0 failed, 60 skipped, 0 undefined +Took 0m1.672s +``` + +๐Ÿโšช With the Dashboard enabled, you'll get one of these: + + + +### ๐Ÿ Behave-Gherkin files: + +๐Ÿ The ``*.feature`` files can use any step seen from: + +```zsh +behave --steps-catalog +``` + +๐Ÿ SeleniumBase includes several pre-made Behave steps, which you can use by creating a Python file with the following line in your ``features/steps/`` directory: + +```python +from seleniumbase.behave import steps # noqa +``` + +๐Ÿ Inside your ``features/environment.py`` file, you should have the following: + +```python +from seleniumbase import BaseCase +from seleniumbase.behave import behave_sb +behave_sb.set_base_class(BaseCase) # Accepts a BaseCase subclass +from seleniumbase.behave.behave_sb import before_all # noqa +from seleniumbase.behave.behave_sb import before_feature # noqa +from seleniumbase.behave.behave_sb import before_scenario # noqa +from seleniumbase.behave.behave_sb import before_step # noqa +from seleniumbase.behave.behave_sb import after_step # noqa +from seleniumbase.behave.behave_sb import after_scenario # noqa +from seleniumbase.behave.behave_sb import after_feature # noqa +from seleniumbase.behave.behave_sb import after_all # noqa +``` + +๐Ÿ If you've already created a subclass of ``BaseCase`` with custom methods, you can swap ``BaseCase`` in with your own subclass, which will allow you to easily use your own custom methods in your Behave step definitions. + +๐Ÿ Here's an example Python file in the ``features/steps/`` folder: + +```python +from behave import step + + +@step("Open the Swag Labs Login Page") +def go_to_swag_labs(context): + sb = context.sb + sb.goto("https://www.saucedemo.com") + sb.clear_local_storage() + + +@step("Login to Swag Labs with {user}") +def login_to_swag_labs(context, user): + sb = context.sb + sb.type("#user-name", user) + sb.type("#password", "secret_sauce\n") + + +@step("Verify that the current user is logged in") +def verify_logged_in(context): + sb = context.sb + sb.assert_element("#header_container") + sb.assert_element("#react-burger-menu-btn") + sb.assert_element("#shopping_cart_container") + + +@step('Add "{item}" to cart') +def add_item_to_cart(context, item): + sb = context.sb + sb.click('div.inventory_item:contains("%s") button[name*="add"]' % item) +``` + +๐Ÿ A ``*.feature`` file could look like this: + +```gherkin +Feature: SeleniumBase scenarios for the Swag Labs App + + Background: + Given Open the Swag Labs Login Page + + Scenario: User can order a backpack from the store + When Login to Swag Labs with standard_user + Then Verify that the current user is logged in + And Save price of "Backpack" to + When Add "Backpack" to Cart + Then Verify shopping cart badge shows 1 item(s) + When Click on shopping cart icon + And Click Checkout + And Enter checkout info: First, Last, 12345 + And Click Continue + Then Verify 1 "Backpack"(s) in cart + And Verify cost of "Backpack" is + And Verify item total is $29.99 + And Verify tax amount is $2.40 + And Verify total cost is $32.39 + When Click Finish + Then Verify order complete + When Logout from Swag Labs + Then Verify on Login page +``` + +๐Ÿ Here's another example of a ``*.feature`` file: + +```gherkin +Feature: SeleniumBase scenarios for the RealWorld App + + Scenario: Verify RealWorld App (log in / sign out) + Given Open "seleniumbase.io/realworld/login" + And Clear Session Storage + When Type "demo_user" into "#username" + And Type "secret_pass" into "#password" + And Do MFA "GAXG2MTEOR3DMMDG" into "#totpcode" + Then Assert text "Welcome!" in "h1" + And Highlight element "img#image1" + And Click 'a:contains("This Page")' + And Save screenshot to logs + When Click link "Sign out" + Then Assert element 'a:contains("Sign in")' + And Assert text "You have been signed out!" +``` + +๐Ÿ If there's a test failure, that's easy to spot: + +```zsh +Feature: SeleniumBase scenarios for the Fail Page # features/fail_page.feature:1 + + Scenario: Fail test on purpose to see what happens # features/fail_page.feature:3 + When Open the Fail Page # features/steps/fail_page.py:4 + Then Fail test on purpose # features/steps/fail_page.py:9 + Assertion Failed: This test fails on purpose! + Captured stdout: + >>> STEP FAILED: (#2) Fail test on purpose + Class / Feature: SeleniumBase scenarios for the Fail Page + Test / Scenario: Fail test on purpose to see what happens + + โŒ Scenario Failed! +``` + +๐Ÿ๐ŸŽ–๏ธ For convenience, the [SeleniumBase Behave GUI](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/behave_gui.md) lets you run ``behave`` scripts from a Desktop app. + +๐Ÿ๐ŸŽ–๏ธ To launch it, call ``sbase behave-gui`` or ``sbase gui-behave``: + +```zsh +sbase behave-gui +* Starting the SeleniumBase Behave Commander GUI App... +``` + + + +๐Ÿ๐ŸŽ–๏ธ You can customize the tests that show up there: + +```zsh +sbase behave-gui # all tests +sbase behave-gui -i=calculator # tests with "calculator" in the name +sbase behave-gui features/ # tests located in the "features/" folder +sbase behave-gui features/calculator.feature # tests in that feature +``` + +-------- + +
To learn more about SeleniumBase, check out the Docs Site:
+ +SeleniumBase.io Docs + +
All the code is on GitHub:
+ +SeleniumBase on GitHub diff --git a/examples/boilerplates/file_parsing/__init__.py b/examples/behave_bdd/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from examples/boilerplates/file_parsing/__init__.py rename to examples/behave_bdd/__init__.py diff --git a/examples/behave_bdd/behave.ini b/examples/behave_bdd/behave.ini new file mode 100644 index 00000000000..84fa8ee77ff --- /dev/null +++ b/examples/behave_bdd/behave.ini @@ -0,0 +1,3 @@ +[behave] +show_skipped=false +show_timings=false diff --git a/examples/behave_bdd/features/__init__.py b/examples/behave_bdd/features/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/behave_bdd/features/behave.ini b/examples/behave_bdd/features/behave.ini new file mode 100644 index 00000000000..84fa8ee77ff --- /dev/null +++ b/examples/behave_bdd/features/behave.ini @@ -0,0 +1,3 @@ +[behave] +show_skipped=false +show_timings=false diff --git a/examples/behave_bdd/features/calculator.feature b/examples/behave_bdd/features/calculator.feature new file mode 100644 index 00000000000..345b48c0f76 --- /dev/null +++ b/examples/behave_bdd/features/calculator.feature @@ -0,0 +1,83 @@ +Feature: SeleniumBase scenarios for the Calculator App + + Background: + Given Open the Calculator App + + Scenario: Pressing "C" outputs "0" + When Press C + Then Verify output is "0" + + Scenario: 1 + 2 + 3 + 4 + 5 = 15 + When Press C + And Press 1 + And Press + + And Press 2 + And Press + + And Press 3 + And Press + + And Press 4 + And Press + + And Press 5 + Then Verify output is "1+2+3+4+5" + When Press = + Then Verify output is "15" + + Scenario: 6 ร— 7 ร— 8 ร— 9 = 3024 + When Press C + And Press 6 + And Press ร— + And Press 7 + And Press ร— + And Press 8 + And Press ร— + And Press 9 + Then Verify output is "6ร—7ร—8ร—9" + When Press = + Then Verify output is "3024" + + Scenario: 44 - 11 = 33 + When Press C + And Press 4 + And Press 4 + And Press - + And Press 1 + And Press 1 + Then Verify output is "44-11" + When Press = + Then Verify output is "33" + + Scenario: 7.0 ร— (3 + 3) = 42 + When Press C + And Press 7 + And Press . + And Press 0 + And Press ร— + And Press ( + And Press 3 + And Press + + And Press 3 + And Press ) + Then Verify output is "7.0ร—(3+3)" + When Press = + Then Verify output is "42" + + Scenario: 4.5 ร— 68 = 306 + When Press C + And Evaluate [4.5 ร— 68] + Then Verify output is "306" + + Scenario Outline: รท = + When Press C + And Press [] + And Press รท + And Press [] + And Press = + Then Verify output is "" + Examples: + | First | Second | Result | + | 1948 | 4 | 487 | + | 21 | 0 | Error | + + Scenario: Save calculator screenshot to logs + Given Press [1337] + Given Save calculator screenshot to logs diff --git a/examples/behave_bdd/features/environment.py b/examples/behave_bdd/features/environment.py new file mode 100644 index 00000000000..09110f71937 --- /dev/null +++ b/examples/behave_bdd/features/environment.py @@ -0,0 +1,11 @@ +from seleniumbase import BaseCase +from seleniumbase.behave import behave_sb +behave_sb.set_base_class(BaseCase) # Accepts a BaseCase subclass +from seleniumbase.behave.behave_sb import before_all # noqa +from seleniumbase.behave.behave_sb import before_feature # noqa +from seleniumbase.behave.behave_sb import before_scenario # noqa +from seleniumbase.behave.behave_sb import before_step # noqa +from seleniumbase.behave.behave_sb import after_step # noqa +from seleniumbase.behave.behave_sb import after_scenario # noqa +from seleniumbase.behave.behave_sb import after_feature # noqa +from seleniumbase.behave.behave_sb import after_all # noqa diff --git a/examples/behave_bdd/features/fail_page.feature b/examples/behave_bdd/features/fail_page.feature new file mode 100644 index 00000000000..e09141e8867 --- /dev/null +++ b/examples/behave_bdd/features/fail_page.feature @@ -0,0 +1,5 @@ +Feature: SeleniumBase scenarios for the Fail Page + + Scenario: Fail test on purpose to see what happens + When Open the Fail Page + Then Fail test on purpose diff --git a/examples/behave_bdd/features/login_app.feature b/examples/behave_bdd/features/login_app.feature new file mode 100644 index 00000000000..5958d1b62b0 --- /dev/null +++ b/examples/behave_bdd/features/login_app.feature @@ -0,0 +1,12 @@ +Feature: SeleniumBase scenarios for the Simple App + + Scenario: Verify the Simple App (Login / Logout) + Given Open "seleniumbase.io/simple/login" + And Type "demo_user" into "#username" + And Type "secret_pass" into "#password" + And Click 'a:contains("Sign in")' + And Assert exact text "Welcome!" in "h1" + And Assert element "img#image1" + And Highlight "#image1" + And Click link "Sign out" + And Assert text "signed out" in "#top_message" diff --git a/examples/behave_bdd/features/realworld.feature b/examples/behave_bdd/features/realworld.feature new file mode 100644 index 00000000000..6c30d1caf15 --- /dev/null +++ b/examples/behave_bdd/features/realworld.feature @@ -0,0 +1,15 @@ +Feature: SeleniumBase scenarios for the RealWorld App + + Scenario: Verify RealWorld App (log in / sign out) + Given Open "seleniumbase.io/realworld/login" + And Clear Session Storage + When Type "demo_user" into "#username" + And Type "secret_pass" into "#password" + And Do MFA "GAXG2MTEOR3DMMDG" into "#totpcode" + Then Assert exact text "Welcome!" in "h1" + And Highlight "img#image1" + And Click 'a:contains("This Page")' + And Save screenshot to logs + When Click link "Sign out" + Then Assert element 'a:contains("Sign in")' + And Assert text "You have been signed out!" diff --git a/examples/behave_bdd/features/steps/__init__.py b/examples/behave_bdd/features/steps/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/behave_bdd/features/steps/calculator.py b/examples/behave_bdd/features/steps/calculator.py new file mode 100644 index 00000000000..cd0926c58ea --- /dev/null +++ b/examples/behave_bdd/features/steps/calculator.py @@ -0,0 +1,144 @@ +from behave import step + + +@step("Open the Calculator App") +def go_to_calculator(context): + context.sb.goto("https://seleniumbase.io/apps/calculator") + + +@step("Press C") +def press_c(context): + context.sb.click("button#clear") + + +@step("Press (") +def press_open_paren(context): + context.sb.click('button[id="("]') + + +@step("Press )") +def press_close_paren(context): + context.sb.click('button[id=")"]') + + +@step("Press รท") +def press_divide(context): + context.sb.click("button#divide") + + +@step("Press ร—") +def press_multiply(context): + context.sb.click("button#multiply") + + +@step("Press -") +def press_subtract(context): + context.sb.click("button#subtract") + + +@step("Press +") +def press_add(context): + context.sb.click("button#add") + + +@step("Press =") +def press_equal(context): + context.sb.click("button#equal") + + +@step("Press 1") +def press_1(context): + context.sb.click('button[id="1"]') + + +@step("Press 2") +def press_2(context): + context.sb.click('button[id="2"]') + + +@step("Press 3") +def press_3(context): + context.sb.click('button[id="3"]') + + +@step("Press 4") +def press_4(context): + context.sb.click('button[id="4"]') + + +@step("Press 5") +def press_5(context): + context.sb.click('button[id="5"]') + + +@step("Press 6") +def press_6(context): + context.sb.click('button[id="6"]') + + +@step("Press 7") +def press_7(context): + context.sb.click('button[id="7"]') + + +@step("Press 8") +def press_8(context): + context.sb.click('button[id="8"]') + + +@step("Press 9") +def press_9(context): + context.sb.click('button[id="9"]') + + +@step("Press 0") +def press_0(context): + context.sb.click('button[id="0"]') + + +@step("Press โ†") +def press_delete(context): + context.sb.click("button#delete") + + +@step("Press .") +def press_dot(context): + context.sb.click('button[id="."]') + + +@step("Press [{number}]") +def enter_number_into_calc(context, number): + sb = context.sb + for digit in number: + sb.click('button[id="%s"]' % digit) + + +@step("Evaluate [{equation}]") +def evaluate_equation(context, equation): + sb = context.sb + for key in equation: + if key == " ": + continue + elif key == "รท": + sb.click("button#divide") + elif key == "ร—": + sb.click("button#multiply") + elif key == "-": + sb.click("button#subtract") + elif key == "+": + sb.click("button#add") + else: + sb.click('button[id="%s"]' % key) + sb.click("button#equal") + + +@step('Verify output is "{output}"') +def verify_output(context, output): + sb = context.sb + sb.assert_exact_text(output, "#output") + + +@step("Save calculator screenshot to logs") +def save_calculator_screenshot_to_logs(context): + sb = context.sb + sb.save_screenshot_to_logs() diff --git a/examples/behave_bdd/features/steps/fail_page.py b/examples/behave_bdd/features/steps/fail_page.py new file mode 100644 index 00000000000..3761c50a07e --- /dev/null +++ b/examples/behave_bdd/features/steps/fail_page.py @@ -0,0 +1,11 @@ +from behave import step + + +@step("Open the Fail Page") +def go_to_error_page(context): + context.sb.goto("https://seleniumbase.io/error_page/") + + +@step("Fail test on purpose") +def fail_on_purpose(context): + context.sb.fail("This test fails on purpose!") diff --git a/examples/behave_bdd/features/steps/imported.py b/examples/behave_bdd/features/steps/imported.py new file mode 100644 index 00000000000..298058ef8d2 --- /dev/null +++ b/examples/behave_bdd/features/steps/imported.py @@ -0,0 +1 @@ +from seleniumbase.behave import steps # noqa diff --git a/examples/behave_bdd/features/steps/swag_labs.py b/examples/behave_bdd/features/steps/swag_labs.py new file mode 100644 index 00000000000..0ccbca3b639 --- /dev/null +++ b/examples/behave_bdd/features/steps/swag_labs.py @@ -0,0 +1,161 @@ +from behave import step + + +@step("Open the Swag Labs Login Page") +def go_to_swag_labs(context): + sb = context.sb + sb.goto("https://www.saucedemo.com") + sb.clear_local_storage() + + +@step("Login to Swag Labs with {user}") +def login_to_swag_labs(context, user): + sb = context.sb + sb.type("#user-name", user) + sb.type("#password", "secret_sauce\n") + + +@step("Verify that the current user is logged in") +def verify_logged_in(context): + sb = context.sb + sb.assert_element("#header_container") + sb.assert_element("#react-burger-menu-btn") + sb.assert_element("#shopping_cart_container") + + +@step('Add "{item}" to cart') +def add_item_to_cart(context, item): + sb = context.sb + sb.click('div.inventory_item:contains("%s") button[name*="add"]' % item) + + +@step('Save price of "{item}" to <{var}>') +def save_price_of_item(context, item, var): + sb = context.sb + price = sb.get_text( + 'div.inventory_item:contains("%s") .inventory_item_price' % item + ) + sb.variables[var] = price + + +@step('Remove "{item}" from cart') +def remove_item_to_cart(context, item): + sb = context.sb + sb.click('div.inventory_item:contains("%s") button[name*="remove"]' % item) + + +@step("Verify shopping cart badge shows {number} item(s)") +def verify_badge_number(context, number): + sb = context.sb + sb.assert_exact_text(number, "span.shopping_cart_badge") + + +@step("Verify shopping cart badge is missing") +def verify_badge_missing(context): + sb = context.sb + sb.assert_element_not_visible("span.shopping_cart_badge") + + +@step("Click on shopping cart icon") +def click_shopping_cart(context): + sb = context.sb + sb.click("#shopping_cart_container a") + + +@step("Click Checkout") +def click_checkout(context): + sb = context.sb + sb.click("#checkout") + + +@step("Enter checkout info: {first_name}, {last_name}, {zip_code}") +def enter_checkout_info(context, first_name, last_name, zip_code): + sb = context.sb + sb.type("#first-name", first_name) + sb.type("#last-name", last_name) + sb.type("#postal-code", zip_code) + + +@step("Click Continue") +def click_continue(context): + sb = context.sb + sb.click("input#continue") + + +@step('Verify {quantity} "{item}"(s) in cart') +def verify_item_in_cart(context, quantity, item): + sb = context.sb + sb.assert_exact_text( + quantity, 'div.cart_item:contains("%s") div.cart_quantity' % item + ) + + +@step('Verify cost of "{item}" is <{var}>') +def verify_cost_of_item(context, item, var): + sb = context.sb + earlier_price = sb.variables[var] + sb.assert_exact_text( + earlier_price, + 'div.cart_item_label:contains("%s") .inventory_item_price' % item, + ) + + +@step("Verify item total is {item_total}") +def verify_item_total(context, item_total): + sb = context.sb + sb.assert_exact_text( + "Item total: %s" % item_total, "div.summary_subtotal_label", timeout=1 + ) + + +@step("Verify tax amount is {tax_amount}") +def verify_tax_amount(context, tax_amount): + sb = context.sb + sb.assert_exact_text( + "Tax: %s" % tax_amount, "div.summary_tax_label", timeout=1 + ) + + +@step("Verify total cost is {total_cost}") +def verify_total_cost(context, total_cost): + sb = context.sb + sb.assert_exact_text( + "Total: %s" % total_cost, "div.summary_total_label", timeout=1 + ) + + +@step("Click Finish") +def click_finish(context): + sb = context.sb + sb.click("button#finish") + + +@step("Verify order complete") +def verify_order_complete(context): + sb = context.sb + sb.assert_exact_text("Thank you for your order!", "h2") + sb.assert_element('img[alt="Pony Express"]') + + +@step("Logout from Swag Labs") +def logout_from_swag_labs(context): + sb = context.sb + sb.js_click("a#logout_sidebar_link") + + +@step("Verify on Login page") +def verify_on_login_page(context): + sb = context.sb + sb.assert_element("#login-button") + + +@step("Sort items from Z to A") +def sort_items_from_z_to_a(context): + sb = context.sb + sb.select_option_by_text("select.product_sort_container", "Name (Z to A)") + + +@step('Verify "{item}" on top') +def verify_item_on_top(context, item): + sb = context.sb + sb.assert_text(item, "div.inventory_item_name") diff --git a/examples/behave_bdd/features/swag_labs.feature b/examples/behave_bdd/features/swag_labs.feature new file mode 100644 index 00000000000..63d2c572c16 --- /dev/null +++ b/examples/behave_bdd/features/swag_labs.feature @@ -0,0 +1,75 @@ +Feature: SeleniumBase scenarios for the Swag Labs App + + Background: + Given Open the Swag Labs Login Page + + Scenario: User can log in and log out successfully + When Login to Swag Labs with standard_user + Then Verify that the current user is logged in + When Logout from Swag Labs + Then Verify on Login page + + Scenario: User can order a backpack from the store + When Login to Swag Labs with standard_user + Then Verify that the current user is logged in + And Save price of "Backpack" to + When Add "Backpack" to Cart + Then Verify shopping cart badge shows 1 item(s) + When Click on shopping cart icon + And Click Checkout + And Enter checkout info: First, Last, 12345 + And Click Continue + Then Verify 1 "Backpack"(s) in cart + And Verify cost of "Backpack" is + And Verify item total is $29.99 + And Verify tax amount is $2.40 + And Verify total cost is $32.39 + When Click Finish + Then Verify order complete + When Logout from Swag Labs + Then Verify on Login page + + Scenario: User can order two items from the store + When Login to Swag Labs with standard_user + And Add "Bike Light" to Cart + And Add "Fleece Jacket" to Cart + Then Verify shopping cart badge shows 2 item(s) + When Click on shopping cart icon + And Click Checkout + And Enter checkout info: First, Last, 54321 + And Click Continue + Then Verify 1 "Bike Light"(s) in cart + Then Verify 1 "Fleece Jacket"(s) in cart + And Verify item total is $59.98 + And Verify tax amount is $4.80 + And Verify total cost is $64.78 + When Click Finish + Then Verify order complete + When Logout from Swag Labs + Then Verify on Login page + + Scenario: User can sort items by name from Z to A + When Login to Swag Labs with standard_user + And Sort items from Z to A + Then Verify "Test.allTheThings() T-Shirt" on top + When Logout from Swag Labs + Then Verify on Login page + + Scenario: User can add & remove 6 items to/from cart + When Login to Swag Labs with standard_user + And Add "Backpack" to Cart + And Add "Bike Light" to Cart + And Add "Bolt T-Shirt" to Cart + And Add "Fleece Jacket" to Cart + And Add "Onesie" to Cart + And Add "Test.allTheThings() T-Shirt" to Cart + Then Verify shopping cart badge shows 6 item(s) + When Remove "Backpack" from Cart + And Remove "Bike Light" from Cart + And Remove "Bolt T-Shirt" from Cart + And Remove "Fleece Jacket" from Cart + And Remove "Onesie" from Cart + And Remove "Test.allTheThings() T-Shirt" from Cart + Then Verify shopping cart badge is missing + When Logout from Swag Labs + Then Verify on Login page diff --git a/examples/boilerplates/ReadMe.md b/examples/boilerplates/ReadMe.md new file mode 100755 index 00000000000..8873621aa84 --- /dev/null +++ b/examples/boilerplates/ReadMe.md @@ -0,0 +1,20 @@ + + + + +

Example Boilerplates:

+ +* Boilerplate files are located in the [SeleniumBase => examples/boilerplates/](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/boilerplates) folder. +* Boilerplates can help you structure tests using common design patterns such as the Page Object Model. +* For all 20 SeleniumBase design patterns, see: [Syntax Formats](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/syntax_formats.md) + +

Boilerplate Files:

+ +* [base_test_case.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/boilerplates/base_test_case.py): This example demonstrates a test class inheriting [BaseCase](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/fixtures/base_case.py) for adding new methods. +* [page_objects.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/boilerplates/page_objects.py): This example demonstrates Page Objects for reusing commonly-used selectors in tests. +* [boilerplate_test.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/boilerplates/boilerplate_test.py): This example demonstrates inheritance of the above files for making a complete test. +* [classic_obj_test.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/boilerplates/classic_obj_test.py): This example demonstrates the classic Page Object Model for structuring tests. + +-------- + +
SeleniumBase
diff --git a/examples/boilerplates/__init__.py b/examples/boilerplates/__init__.py old mode 100755 new mode 100644 diff --git a/examples/boilerplates/base_test_case.py b/examples/boilerplates/base_test_case.py old mode 100755 new mode 100644 index 8f3f629ea5a..042d4cc2d38 --- a/examples/boilerplates/base_test_case.py +++ b/examples/boilerplates/base_test_case.py @@ -1,22 +1,26 @@ -''' -You can use this as a boilerplate for your test framework. -Define your customized library methods in a master class like this. -Then have all your test classes inherit it. -BaseTestCase will inherit SeleniumBase methods from BaseCase. -''' - +"""Use this as a boilerplate for your test framework. +Define customized library methods in a class like this. +Then have your test classes inherit it. +BaseTestCase inherits SeleniumBase methods from BaseCase.""" from seleniumbase import BaseCase class BaseTestCase(BaseCase): - def setUp(self): - super(BaseTestCase, self).setUp() - # Add custom setUp code for your tests AFTER the super().setUp() + super().setUp() + # <<< Run custom setUp() code for tests AFTER the super().setUp() >>> def tearDown(self): - # Add custom tearDown code for your tests BEFORE the super().tearDown() - super(BaseTestCase, self).tearDown() + self.save_teardown_screenshot() # If test fails, or if "--screenshot" + if self.has_exception(): + # <<< Run custom code if the test failed. >>> + pass + else: + # <<< Run custom code if the test passed. >>> + pass + # (Wrap unreliable tearDown() code in a try/except block.) + # <<< Run custom tearDown() code BEFORE the super().tearDown() >>> + super().tearDown() def login(self): # <<< Placeholder. Add your code here. >>> @@ -29,7 +33,7 @@ def example_method(self): pass -''' +""" # Now you can do something like this in your test files: from base_test_case import BaseTestCase @@ -39,4 +43,7 @@ class MyTests(BaseTestCase): def test_example(self): self.login() self.example_method() -''' + self.type("input", "Name") + self.click("form button") + ... +""" diff --git a/examples/boilerplates/boilerplate_test.py b/examples/boilerplates/boilerplate_test.py old mode 100755 new mode 100644 index 25d59c3818f..a40b7430e8a --- a/examples/boilerplates/boilerplate_test.py +++ b/examples/boilerplates/boilerplate_test.py @@ -1,9 +1,13 @@ -from .base_test_case import BaseTestCase -from .page_objects import Page +try: # Run with "pytest" (relative imports are valid) + from .base_test_case import BaseTestCase + from .page_objects import Page +except (ImportError, ValueError): # Run with "python" + from base_test_case import BaseTestCase + from page_objects import Page + BaseTestCase.main(__name__, __file__) class MyTestClass(BaseTestCase): - def test_boilerplate(self): self.login() self.example_method() diff --git a/examples/boilerplates/classic_obj_test.py b/examples/boilerplates/classic_obj_test.py new file mode 100644 index 00000000000..4055d61fc9e --- /dev/null +++ b/examples/boilerplates/classic_obj_test.py @@ -0,0 +1,18 @@ +"""Classic Page Object Model with BaseCase inheritance.""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class DataPage: + def go_to_data_url(self, sb): + sb.goto("data:text/html,

Hello!

") + + def add_input_text(self, sb, text): + sb.type("input", text) + + +class ObjTests(BaseCase): + def test_data_url_page(self): + DataPage().go_to_data_url(self) + self.assert_text("Hello!", "p") + DataPage().add_input_text(self, "Goodbye!") diff --git a/examples/boilerplates/page_objects.py b/examples/boilerplates/page_objects.py old mode 100755 new mode 100644 index a91b123d3fe..7008cc460f1 --- a/examples/boilerplates/page_objects.py +++ b/examples/boilerplates/page_objects.py @@ -1,8 +1,6 @@ -''' -Example of using the Page Object Pattern for tests, using CSS selectors. -Helps make your code more Readable, Maintainable, and Reusable. -Import a file like this at the top of your test files. -''' +"""Example of using the Page Object Pattern in tests. +Makes code more Readable, Maintainable, and Reusable. +Import files like this at the top of your test files.""" class Page(object): @@ -28,7 +26,7 @@ class CheckoutPage(object): shop_more = "#shop-more" -''' +""" # Now you can do something like this in your test files: from .base_test_case import BaseTestCase @@ -44,4 +42,4 @@ def test_example(self): self.click(CheckoutPage.buy_now) self.assert_element("#success") self.assert_text("Order Received!", "#h2") -''' +""" diff --git a/examples/boilerplates/samples/__init__.py b/examples/boilerplates/samples/__init__.py old mode 100755 new mode 100644 diff --git a/examples/boilerplates/samples/bing_objects.py b/examples/boilerplates/samples/bing_objects.py deleted file mode 100755 index baf65d36a0b..00000000000 --- a/examples/boilerplates/samples/bing_objects.py +++ /dev/null @@ -1,10 +0,0 @@ -''' -Bing.com page objects as CSS selectors -''' - - -class Page(object): - logo_box = '#sbox div[class*=logo]' - search_box = 'input.b_searchbox' - search_button = 'input[name="go"]' - search_results = '#b_results' diff --git a/examples/boilerplates/samples/bing_test.py b/examples/boilerplates/samples/bing_test.py deleted file mode 100755 index 9811b3f758e..00000000000 --- a/examples/boilerplates/samples/bing_test.py +++ /dev/null @@ -1,19 +0,0 @@ -''' -Bing.com testing example -''' - -from seleniumbase import BaseCase -from .bing_objects import Page - - -class BingTests(BaseCase): - - def test_bing(self): - self.open('https://bing.com') - self.assert_text('Bing', Page.logo_box) - self.update_text(Page.search_box, 'github') - self.assert_element('li[query="github"]') - self.click(Page.search_button) - self.assert_text('github.com', Page.search_results) - self.click_link_text('Images') - self.assert_element('img[alt="Image result for github"]') diff --git a/examples/boilerplates/samples/file_parsing/__init__.py b/examples/boilerplates/samples/file_parsing/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/boilerplates/file_parsing/parse_files.py b/examples/boilerplates/samples/file_parsing/parse_files.py old mode 100755 new mode 100644 similarity index 51% rename from examples/boilerplates/file_parsing/parse_files.py rename to examples/boilerplates/samples/file_parsing/parse_files.py index 6d2824dff5c..231e0c45ce6 --- a/examples/boilerplates/file_parsing/parse_files.py +++ b/examples/boilerplates/samples/file_parsing/parse_files.py @@ -1,50 +1,44 @@ -''' -Demonstration of parsing data from files. -In this example, login information is pulled for tests. -''' - +"""Example of parsing data from files.""" from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) class ParseTestCase(BaseCase): - - def setUp(self): - super(ParseTestCase, self).setUp() - def get_login_credentials(self, user_type): - # Example of parsing data from a file - f = open('qa_login_example.txt', "r") - file_data = f.read() - file_lines = file_data.split('\n') + # Example of parsing data from a file (Method 1) + with open("qa_login_example.txt") as f: + file_lines = [line.rstrip() for line in f] for line in file_lines: - line_items = line.split(',') + line_items = line.split(",") if line_items[0] == user_type: return line_items[1], line_items[2] - f.close() def get_all_login_credentials(self): # Example of parsing data from a file (Method 2) keys = {} - f = open("staging_login_example.txt") - file_data = f.read() - file_lines = file_data.split('\n') + with open("staging_login_example.txt") as f: + file_lines = [line.rstrip() for line in f] for line in file_lines: - line_items = line.split(',') - if line_items[0] == 'admin': - keys['admin'] = ( - {'username': line_items[1], 'password': line_items[2]}) - if line_items[0] == 'employee': - keys['employee'] = ( - {'username': line_items[1], 'password': line_items[2]}) - if line_items[0] == 'customer': - keys['customer'] = ( - {'username': line_items[1], 'password': line_items[2]}) - f.close() + line_items = line.split(",") + if line_items[0] == "admin": + keys["admin"] = { + "username": line_items[1], + "password": line_items[2], + } + if line_items[0] == "employee": + keys["employee"] = { + "username": line_items[1], + "password": line_items[2], + } + if line_items[0] == "customer": + keys["customer"] = { + "username": line_items[1], + "password": line_items[2], + } return keys class ParseTests(ParseTestCase): - def test_get_login_credentials(self): print("\nExample 1 of getting login info from parsing a config file:") print("") diff --git a/examples/boilerplates/file_parsing/qa_login_example.txt b/examples/boilerplates/samples/file_parsing/qa_login_example.txt old mode 100755 new mode 100644 similarity index 100% rename from examples/boilerplates/file_parsing/qa_login_example.txt rename to examples/boilerplates/samples/file_parsing/qa_login_example.txt diff --git a/examples/boilerplates/file_parsing/staging_login_example.txt b/examples/boilerplates/samples/file_parsing/staging_login_example.txt old mode 100755 new mode 100644 similarity index 100% rename from examples/boilerplates/file_parsing/staging_login_example.txt rename to examples/boilerplates/samples/file_parsing/staging_login_example.txt diff --git a/examples/boilerplates/samples/google_objects.py b/examples/boilerplates/samples/google_objects.py old mode 100755 new mode 100644 index acdc092a41e..ea69693e7e6 --- a/examples/boilerplates/samples/google_objects.py +++ b/examples/boilerplates/samples/google_objects.py @@ -1,16 +1,12 @@ -''' -Google.com page objects -''' +"""google.com page objects""" class HomePage(object): dialog_box = '[role="dialog"] div' - search_box = 'input[title="Search"]' - list_box = '[role="listbox"]' + search_box = '[title="Search"]' search_button = 'input[value="Google Search"]' - feeling_lucky_button = '''input[value="I'm Feeling Lucky"]''' + feeling_lucky_button = """input[value="I'm Feeling Lucky"]""" class ResultsPage(object): - google_logo = 'img[alt="Google"]' - search_results = 'div#center_col' + search_results = "div#center_col" diff --git a/examples/boilerplates/samples/google_test.py b/examples/boilerplates/samples/google_test.py old mode 100755 new mode 100644 index ce4aad92312..06c569433cd --- a/examples/boilerplates/samples/google_test.py +++ b/examples/boilerplates/samples/google_test.py @@ -1,21 +1,28 @@ -''' -Google.com testing example -''' - +"""google.com example test that uses page objects""" from seleniumbase import BaseCase -from .google_objects import HomePage, ResultsPage +try: + from .google_objects import HomePage, ResultsPage +except Exception: + from google_objects import HomePage, ResultsPage + BaseCase.main(__name__, __file__, "--uc") class GoogleTests(BaseCase): - def test_google_dot_com(self): - self.open('https://google.com') - self.update_text(HomePage.search_box, 'github') - self.assert_element(HomePage.list_box) + if self.headless: + self.goto_if_not_url("about:blank") + print("\n Skipping test in headless mode.") + self.skip("Skipping test in headless mode.") + if not self.undetectable: + self.get_new_driver(undetectable=True) + self.goto("https://google.com/ncr") + self.click_if_visible('button:contains("Accept all")') + self.assert_title_contains("Google") + self.sleep(0.05) + self.save_screenshot_to_logs() # ("./latest_logs" folder) + self.type(HomePage.search_box, "GitHub") self.assert_element(HomePage.search_button) self.assert_element(HomePage.feeling_lucky_button) self.click(HomePage.search_button) - self.assert_text('github.com', ResultsPage.search_results) - self.click_link_text('Images') - source = self.get_page_source() - self.assert_true("Image result for github" in source) + self.sleep(1) + self.assert_text("github.com", ResultsPage.search_results) diff --git a/examples/boilerplates/samples/sb_swag_test.py b/examples/boilerplates/samples/sb_swag_test.py new file mode 100644 index 00000000000..fed92469145 --- /dev/null +++ b/examples/boilerplates/samples/sb_swag_test.py @@ -0,0 +1,20 @@ +"""Classic Page Object Model with the "sb" fixture.""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class LoginPage: + def login_to_swag_labs(self, sb: BaseCase, username): + sb.goto("https://www.saucedemo.com") + sb.type("#user-name", username) + sb.type("#password", "secret_sauce") + sb.click('input[type="submit"]') + + +class MyTests: + def test_swag_labs_login(self, sb: BaseCase): + LoginPage().login_to_swag_labs(sb, "standard_user") + sb.assert_element("div.inventory_list") + sb.assert_element('div:contains("Sauce Labs Backpack")') + sb.js_click("a#logout_sidebar_link") + sb.assert_element("div#login_button_container") diff --git a/examples/boilerplates/samples/swag_labs_test.py b/examples/boilerplates/samples/swag_labs_test.py new file mode 100644 index 00000000000..002f1b55258 --- /dev/null +++ b/examples/boilerplates/samples/swag_labs_test.py @@ -0,0 +1,20 @@ +"""Classic Page Object Model with BaseCase inheritance.""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class LoginPage: + def login_to_swag_labs(self, sb: BaseCase, username): + sb.goto("https://www.saucedemo.com") + sb.type("#user-name", username) + sb.type("#password", "secret_sauce") + sb.click('input[type="submit"]') + + +class MyTests(BaseCase): + def test_swag_labs_login(self): + LoginPage().login_to_swag_labs(self, "standard_user") + self.assert_element("div.inventory_list") + self.assert_element('div:contains("Sauce Labs Backpack")') + self.js_click("a#logout_sidebar_link") + self.assert_element("div#login_button_container") diff --git a/examples/boilerplates/samples/test_page_objects.py b/examples/boilerplates/samples/test_page_objects.py new file mode 100644 index 00000000000..4f3b1a9a4a4 --- /dev/null +++ b/examples/boilerplates/samples/test_page_objects.py @@ -0,0 +1,53 @@ +"""An example using the Classic Page Object Model.""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__, "--uc") + + +class GooglePage: + def go_to_google(self, sb): + sb.goto("https://google.com/ncr") + sb.click_if_visible('button:contains("Accept all")') + + def assert_google_title(self, sb): + sb.assert_title_contains("Google") + + def hide_sign_in_pop_up(self, sb): + if not sb.is_element_visible("iframe"): + sb.sleep(1.5) # A slow pop-up might appear + sb.hide_elements('iframe') + sb.sleep(0.05) + + def do_search(self, sb, search_term): + sb.sleep(0.05) + sb.type('[name="q"]', search_term) + sb.click('[value="Google Search"]') + + def click_search_result(self, sb, content): + sb.click('a:contains("%s")' % content) + + +class SeleniumBaseIOPage: + def do_search_and_click(self, sb, search_term): + sb.sleep(0.05) + sb.type('form[name="search"] input', search_term) + sb.click("li.md-search-result__item h1:contains(%s)" % search_term) + + +class MyTests(BaseCase): + def test_page_objects(self): + if self.headless: + self.goto_if_not_url("about:blank") + print("\n Skipping test in headless mode.") + self.skip("Skipping test in headless mode.") + if not self.undetectable: + self.get_new_driver(undetectable=True) + search_term = "site:seleniumbase.io Docs" + expected_text = "SeleniumBase" + GooglePage().go_to_google(self) + GooglePage().assert_google_title(self) + GooglePage().hide_sign_in_pop_up(self) + GooglePage().do_search(self, search_term) + self.assert_text(expected_text, "#search") + GooglePage().click_search_result(self, expected_text) + SeleniumBaseIOPage().do_search_and_click(self, "Dashboard") + self.assert_text("Dashboard", "main h1") diff --git a/examples/boilerplates/sb_fixture_test.py b/examples/boilerplates/sb_fixture_test.py new file mode 100644 index 00000000000..847735fcc28 --- /dev/null +++ b/examples/boilerplates/sb_fixture_test.py @@ -0,0 +1,16 @@ +"""Classic Page Object Model with the "sb" fixture.""" + + +class DataPage: + def go_to_data_url(self, sb): + sb.goto("data:text/html,

Hello!

") + + def add_input_text(self, sb, text): + sb.type("input", text) + + +class ObjTests: + def test_data_url_page(self, sb): + DataPage().go_to_data_url(sb) + sb.assert_text("Hello!", "p") + DataPage().add_input_text(sb, "Goodbye!") diff --git a/examples/capabilities/ReadMe.md b/examples/capabilities/ReadMe.md old mode 100755 new mode 100644 index 31070aacf11..09211f8e928 --- a/examples/capabilities/ReadMe.md +++ b/examples/capabilities/ReadMe.md @@ -1,51 +1,118 @@ -### Using Desired Capabilities + -You can specify browser desired capabilities for webdriver when running SeleniumBase tests on a remote SeleniumGrid server such as [BrowserStack](https://www.browserstack.com/automate/capabilities), [Sauce Labs](https://wiki.saucelabs.com/display/DOCS/Platform+Configurator#/), or [TestingBot](https://testingbot.com/support/other/test-options). +## [](https://github.com/seleniumbase/SeleniumBase/) Using Desired Capabilities + +You can specify browser capabilities when running SeleniumBase tests on a remote Selenium Grid server (such as BrowserStack or Sauce Labs). Sample run commands may look like this when run from the [SeleniumBase/examples/](https://github.com/seleniumbase/SeleniumBase/tree/master/examples) folder: (The browser is now specified in the capabilities file.) -```bash -pytest my_first_test.py --browser=remote --server=USERNAME:KEY@hub.browserstack.com --port=80 --cap_file=capabilities/sample_cap_file_BS.py +```zsh +pytest test_demo_site.py --browser=remote --server=USERNAME:KEY@hub.browserstack.com --port=80 --cap_file=capabilities/sample_cap_file_BS.py ``` -```bash -pytest my_first_test.py --browser=remote --server=USERNAME:KEY@ondemand.saucelabs.com --port=80 --cap_file=capabilities/sample_cap_file_SL.py +```zsh +pytest test_demo_site.py --browser=remote --server=USERNAME:KEY@ondemand.us-east-1.saucelabs.com --port=443 --protocol=https --cap_file=capabilities/sample_cap_file_SL.py ``` -(Parameters: ``--browser=remote``, ``--server=SERVER``, ``--port=PORT``, and ``--cap_file=CAP_FILE.py``) +(Parameters: ``--browser=remote``, ``--server=SERVER``, ``--port=PORT``, ``--protocol=PROTOCOL``, and ``--cap_file=CAP_FILE.py``) + +Here's an example desired capabilities file for BrowserStack using the newer SDK format in a `.yml` / `.yaml` file: + +```yml +platforms: + - browserName: safari + osVersion: 17 + deviceName: iPhone 15 Pro Max +buildIdentifier: ${BUILD_NUMBER} +parallelsPerPlatform: 1 +projectName: My Project +browserstackLocal: true +debug: true +networkLogs: true +``` + +Here's an example desired capabilities file for BrowserStack using the legacy JSONWP format in a `.py` file: -Here's an example desired capabilities file: ```python desired_cap = { - 'os': 'OS X', - 'os_version': 'Sierra', - 'browser': 'Chrome', - 'browser_version': '70.0', - 'browserstack.local': 'false', - 'browserstack.selenium_version': '3.14.0', - 'browserstack.chrome.driver': '2.43' + "browser": "Chrome", + "os": "Windows", + "os_version": "11", + "browser_version": "latest", + "browserstack.console": "info", + "browserstack.debug": "true", + "browserstack.networkLogs": "true", + "browserstack.local": "true", } ``` -A desired capabilities file can also look like this: +Here's an example desired capabilities file for Sauce Labs: + ```python -caps = {} -caps['browserName'] = "chrome" -caps['platform'] = "macOS 10.12" -caps['version'] = "70.0" +capabilities = { + "browserName": "chrome", + "browserVersion": "latest", + "platformName": "macOS 10.14", + "sauce:options": {}, +} ``` -(You'll notice that the browser is now being specified in the capabilities file, rather than with ``--browser=BROWSER``) +(Note that the browser is now being specified in the capabilities file, rather than with ``--BROWSER`` when using a **remote** Selenium Grid. If using a **local** Selenium Grid, specify the browser, eg: ``--firefox``.) -You can generate desired capabilities for [BrowserStack](https://www.browserstack.com/automate/capabilities), [Sauce Labs](https://wiki.saucelabs.com/display/DOCS/Platform+Configurator#/), and [TestingBot](https://testingbot.com/support/other/test-options) by following those links to their respective websites. +
You can generate specific desired capabilities using:
+ + + +
Parsing desired capabilities:
+ +SeleniumBase has a desired capabilities parser that can capture all lines from the specified file in the following formats: + +```python +'KEY': 'VALUE' +'KEY': True +'KEY': False +caps['KEY'] = "VALUE" +caps['KEY'] = True +caps['KEY'] = False +``` -A regex parser was built into SeleniumBase to capture all lines from the specified desired capabilities file in the following formats: -``'KEY': 'VALUE'`` -``'KEY': True`` -``'KEY': False`` -``caps['KEY'] = "VALUE"`` -``caps['KEY'] = True`` -``caps['KEY'] = False`` (Each pair must be on a separate line. You can interchange single and double quotes.) -You can also swap ``--browser=remote`` with an actual browser, eg ``--browser=chrome``, which will combine the default SeleniumBase desired capabilities with those that were specified in the capabilities file when using ``--cap_file=FILE.py``. Capabilities will override other parameters, so if you set the browser to one thing and the capabilities browser to another, SeleniumBase will use the capabilities browser as the browser. You'll need default SeleniumBase desired capabilities when using a proxy server (not the same as a Selenium Grid server), when downloading files to a desired folder, for disabling some warnings on Chrome, for overriding a website's Content Security Policy on Firefox, and for other reasons. +You can also swap ``--browser=remote`` with an actual browser, eg ``--browser=chrome``, which will combine the default SeleniumBase desired capabilities with those that were specified in the capabilities file when using ``--cap_file=FILE.py``. Capabilities will override other parameters, so if you set the browser to one thing and the capabilities browser to another, SeleniumBase will use the capabilities browser. + +You'll need default SeleniumBase capabilities for: +* Using a proxy server (not the same as a Selenium Grid server) +* Downloading files to a desired folder +* Disabling some warnings on Chrome +* Overriding a website's Content Security Policy on Chrome +* Other possible reasons + +You can also set browser desired capabilities from a command-line string. Eg: + +```zsh +pytest test_swag_labs.py --cap-string='{"browserName":"chrome","name":"test1"}' --server="127.0.0.1" --browser=remote +``` + +(Enclose cap-string in single quotes. Enclose parameter keys in double quotes.) + +If you pass ``"*"`` into the ``"name"`` field of ``--cap-string``, the name will become the test identifier. Eg: + +```zsh +pytest my_first_test.py --cap-string='{"browserName":"chrome","name":"*"}' --server="127.0.0.1" --browser=chrome +``` + +Example name: ``"my_first_test.MyTestClass.test_basics"`` + +

Using a local Selenium Grid

+ +If using a local Selenium Grid with SeleniumBase, start up the Grid Hub and nodes first: + +```zsh +sbase grid-hub start +sbase grid-node start +``` + +(The Selenium Server JAR file will be automatically downloaded for first-time Grid users. You'll also need Java installed to start up the Grid.) diff --git a/examples/capabilities/mac_cap_file.py b/examples/capabilities/mac_cap_file.py new file mode 100644 index 00000000000..e5847c5c1f7 --- /dev/null +++ b/examples/capabilities/mac_cap_file.py @@ -0,0 +1,6 @@ +# Desired capabilities example file for generic macOS Grid nodes + +capabilities = { + "platformName": "MAC", + "browserVersion": "latest", +} diff --git a/examples/capabilities/sample_cap_file_BS.py b/examples/capabilities/sample_cap_file_BS.py old mode 100755 new mode 100644 index b8953b28327..41218235e10 --- a/examples/capabilities/sample_cap_file_BS.py +++ b/examples/capabilities/sample_cap_file_BS.py @@ -1,11 +1,12 @@ -# Desired capabilities example file for BrowserStack -# Generate from https://www.browserstack.com/automate/capabilities +# Desired capabilities example .py file for BrowserStack: +# https://www.browserstack.com/docs/automate/capabilities desired_cap = { - 'os': 'OS X', - 'os_version': 'Sierra', - 'browser': 'Chrome', - 'browser_version': '70.0', - 'browserstack.local': 'false', - 'browserstack.selenium_version': '3.14.0', - 'browserstack.chrome.driver': '2.43' + "browser": "Chrome", + "os": "Windows", + "os_version": "11", + "browser_version": "latest", + "browserstack.console": "info", + "browserstack.debug": "true", + "browserstack.networkLogs": "true", + "browserstack.local": "true", } diff --git a/examples/capabilities/sample_cap_file_BS.yml b/examples/capabilities/sample_cap_file_BS.yml new file mode 100644 index 00000000000..c1b3a9a0edd --- /dev/null +++ b/examples/capabilities/sample_cap_file_BS.yml @@ -0,0 +1,12 @@ +# Desired capabilities example YML file for BrowserStack: +# https://www.browserstack.com/docs/automate/capabilities +platforms: + - browserName: safari + osVersion: 17 + deviceName: iPhone 15 Pro Max +buildIdentifier: ${BUILD_NUMBER} +parallelsPerPlatform: 1 +projectName: My Project +browserstackLocal: true +debug: true +networkLogs: true diff --git a/examples/capabilities/sample_cap_file_SL.py b/examples/capabilities/sample_cap_file_SL.py old mode 100755 new mode 100644 index c9216b60637..c1f2fa2ff98 --- a/examples/capabilities/sample_cap_file_SL.py +++ b/examples/capabilities/sample_cap_file_SL.py @@ -1,6 +1,8 @@ # Desired capabilities example file for Sauce Labs -# Generate from https://wiki.saucelabs.com/display/DOCS/Platform+Configurator#/ -caps = {} -caps['browserName'] = "chrome" -caps['platform'] = "macOS 10.12" -caps['version'] = "70.0" +# Generate from https://saucelabs.com/products/platform-configurator +capabilities = { + "browserName": "chrome", + "browserVersion": "latest", + "platformName": "macOS 10.14", + "sauce:options": {}, +} diff --git a/examples/capabilities/selenoid_cap_file.py b/examples/capabilities/selenoid_cap_file.py new file mode 100644 index 00000000000..d0b82241055 --- /dev/null +++ b/examples/capabilities/selenoid_cap_file.py @@ -0,0 +1,14 @@ +# Desired capabilities example file for Selenoid Grid +# +# The same result can be achieved on the command-line. Eg: +# --cap-string='{"selenoid:options": {"enableVNC": true}}' + +capabilities = { + "acceptSslCerts": True, + "acceptInsecureCerts": True, + "screenResolution": "1920x1080x24", + "selenoid:options": { + "enableVNC": True, + "enableVideo": False, + }, +} diff --git a/examples/capabilities/win10_cap_file.py b/examples/capabilities/win10_cap_file.py new file mode 100644 index 00000000000..9759b84479b --- /dev/null +++ b/examples/capabilities/win10_cap_file.py @@ -0,0 +1,6 @@ +# Desired capabilities example file for Windows 10 Grid nodes + +capabilities = { + "platformName": "WIN10", + "browserVersion": "latest", +} diff --git a/examples/case_plans/basic_test.MyTestClass.test_basics.md b/examples/case_plans/basic_test.MyTestClass.test_basics.md new file mode 100644 index 00000000000..e210b55892e --- /dev/null +++ b/examples/case_plans/basic_test.MyTestClass.test_basics.md @@ -0,0 +1,9 @@ +``basic_test.py::MyTestClass::test_basics`` +--- +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Log in to https://www.saucedemo.com with ``standard_user``. | Login was successful. | +| 2 | Click on the ``Backpack`` ``ADD TO CART`` button. | The button text changed to ``REMOVE``. | +| 3 | Click on the cart icon. | The ``Backpack`` is seen in the cart. | +| 4 | Remove the ``Backpack`` from the cart. | The ``Backpack`` is no longer in the cart. | +| 5 | Log out from the website. | Logout was successful. | diff --git a/examples/case_plans/my_first_test.MyTestClass.test_swag_labs.md b/examples/case_plans/my_first_test.MyTestClass.test_swag_labs.md new file mode 100644 index 00000000000..ccdbaa386e0 --- /dev/null +++ b/examples/case_plans/my_first_test.MyTestClass.test_swag_labs.md @@ -0,0 +1,10 @@ +``my_first_test.py::MyTestClass::test_swag_labs`` +--- +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Log in to https://www.saucedemo.com with ``standard_user``. | Login was successful. | +| 2 | Click on the ``Backpack`` ``ADD TO CART`` button. | The button text changed to ``REMOVE``. | +| 3 | Click on the cart icon. | The ``Backpack`` is seen in the cart. | +| 4 | Click on the ``CHECKOUT`` button.
Enter user details and click ``CONTINUE``. | The ``Backpack`` is seen in the cart on the ``CHECKOUT: OVERVIEW`` page. | +| 5 | Click on the ``FINISH`` button. | There is a ``Thank you`` message. | +| 6 | Log out from the website. | Logout was successful. | diff --git a/examples/case_plans/shadow_root_test.ShadowRootTest.test_shadow_root.md b/examples/case_plans/shadow_root_test.ShadowRootTest.test_shadow_root.md new file mode 100644 index 00000000000..ac674cb171e --- /dev/null +++ b/examples/case_plans/shadow_root_test.ShadowRootTest.test_shadow_root.md @@ -0,0 +1,5 @@ +``shadow_root_test.py::ShadowRootTest::test_shadow_root`` +--- +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Open https://seleniumbase.io/other/shadow_dom.
Click each tab and verify the text contained within the Shadow Root sections. | Tab 1 text: ``Content Panel 1``
Tab 2 text: ``Content Panel 2``
Tab 3 text: ``Content Panel 3`` | diff --git a/examples/case_plans/test_assert_elements.ListAssertTests.test_assert_list_of_elements.md b/examples/case_plans/test_assert_elements.ListAssertTests.test_assert_list_of_elements.md new file mode 100644 index 00000000000..ef555dfd566 --- /dev/null +++ b/examples/case_plans/test_assert_elements.ListAssertTests.test_assert_list_of_elements.md @@ -0,0 +1,8 @@ +``test_assert_elements.py::ListAssertTests::test_assert_list_of_elements`` +--- +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Open https://seleniumbase.io/demo_page. | | +| 2 | Use ``self.assert_elements_present("head", "style", "script")`` to verify that multiple elements are present in the HTML. | The assertion is successful. | +| 3 | Use ``self.assert_elements("h1", "h2", "h3")`` to verify that multiple elements are visible. | The assertion is successful. | +| 4 | Use ``self.assert_elements(["#myDropdown", "#myButton", "#svgRect"])`` to verify that multiple elements are visible. | The assertion is successful. | diff --git a/examples/case_plans/test_calculator.CalculatorTests.test_6_times_7_plus_12_equals_54.md b/examples/case_plans/test_calculator.CalculatorTests.test_6_times_7_plus_12_equals_54.md new file mode 100644 index 00000000000..7053790ecea --- /dev/null +++ b/examples/case_plans/test_calculator.CalculatorTests.test_6_times_7_plus_12_equals_54.md @@ -0,0 +1,5 @@ +``test_calculator.py::CalculatorTests::test_6_times_7_plus_12_equals_54`` +--- +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Open https://seleniumbase.io/apps/calculator.
Perform the following calculation: ``6 ร— 7 + 12`` | The output is ``54`` after pressing ``=`` | diff --git a/examples/case_plans/test_demo_site.DemoSiteTests.test_demo_site.md b/examples/case_plans/test_demo_site.DemoSiteTests.test_demo_site.md new file mode 100644 index 00000000000..b3f19bdecbd --- /dev/null +++ b/examples/case_plans/test_demo_site.DemoSiteTests.test_demo_site.md @@ -0,0 +1,24 @@ +``test_demo_site.py::DemoSiteTests::test_demo_site`` +--- +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Open https://seleniumbase.io/demo_page | | +| 2 | Assert the title of the current web page.
Assert that a given element is visible on the page.
Assert that a text substring appears in an element's text. | The assertions were successful. | +| 3 | Type text into various text fields and then verify. | The assertions were successful. | +| 4 | Verify that a hover dropdown link changes page text. | The assertion was successful. | +| 5 | Verify that a button click changes text on the page. | The assertion was successful. | +| 6 | Verify that an SVG element is located on the page. | The assertion was successful. | +| 7 | Verify that a slider control updates a progress bar. | The assertion was successful. | +| 8 | Verify that a "select" option updates a meter bar. | The assertion was successful. | +| 9 | Assert an element located inside an iFrame. | The assertion was successful. | +| 10 | Assert text located inside an iFrame. | The assertion was successful. | +| 11 | Verify that clicking a radio button selects it. | The assertion was successful. | +| 12 | Verify that clicking an empty checkbox makes it selected. | The assertion was successful. | +| 13 | Verify clicking on multiple elements with one call. | The assertions were successful. | +| 14 | Verify that clicking an iFrame checkbox selects it. | The assertions were successful. | +| 15 | Verify that Drag and Drop works. | The assertion was successful. | +| 16 | Assert link text. | The assertion was successful. | +| 17 | Verify clicking on link text. | The action was successful. | +| 18 | Assert exact text in an element. | The assertion was successful. | +| 19 | Highlight a page element. | The action was successful. | +| 20 | Verify that Demo Mode works. | The assertion was successful. | diff --git a/examples/case_plans/test_login.SwagLabsLoginTests.test_swag_labs_login.md b/examples/case_plans/test_login.SwagLabsLoginTests.test_swag_labs_login.md new file mode 100644 index 00000000000..5cb6c20875e --- /dev/null +++ b/examples/case_plans/test_login.SwagLabsLoginTests.test_swag_labs_login.md @@ -0,0 +1,6 @@ +``test_login.py::SwagLabsLoginTests::test_swag_labs_login`` +--- +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Log in to https://www.saucedemo.com with ``standard_user``. | Login was successful. | +| 2 | Log out from the website. | Logout was successful. | diff --git a/examples/case_plans/test_mfa_login.TestMFALogin.test_mfa_login.md b/examples/case_plans/test_mfa_login.TestMFALogin.test_mfa_login.md new file mode 100644 index 00000000000..b637f85f7e8 --- /dev/null +++ b/examples/case_plans/test_mfa_login.TestMFALogin.test_mfa_login.md @@ -0,0 +1,7 @@ +``test_mfa_login.py::TestMFALogin::test_mfa_login`` +--- +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Open https://seleniumbase.io/realworld/login
Enter credentials and Sign In. | Sign In was successful. | +| 2 | Click the ``This Page`` button.
Save a screenshot to the logs. | | +| 3 | Click to Sign Out | Sign Out was successful. | diff --git a/examples/case_summary.md b/examples/case_summary.md new file mode 100644 index 00000000000..66c9e96fd0d --- /dev/null +++ b/examples/case_summary.md @@ -0,0 +1,157 @@ +

Summary of existing Case Plans

+ +| | | | +| - | -: | - | +| ๐Ÿ”ต | 12 | Case Plans with customized tables | +| โญ• | 0 | Case Plans using boilerplate code | +| ๐Ÿšง | 0 | Case Plans that are missing tables | + +-------- + +

๐Ÿ”Ž (Click rows to expand) ๐Ÿ”

+ +
+ ๐Ÿ”ต basic_test.py::MyTestClass::test_basics + +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Log in to https://www.saucedemo.com with ``standard_user``. | Login was successful. | +| 2 | Click on the ``Backpack`` ``ADD TO CART`` button. | The button text changed to ``REMOVE``. | +| 3 | Click on the cart icon. | The ``Backpack`` is seen in the cart. | +| 4 | Remove the ``Backpack`` from the cart. | The ``Backpack`` is no longer in the cart. | +| 5 | Log out from the website. | Logout was successful. | + +
+ +
+ ๐Ÿ”ต my_first_test.py::MyTestClass::test_swag_labs + +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Log in to https://www.saucedemo.com with ``standard_user``. | Login was successful. | +| 2 | Click on the ``Backpack`` ``ADD TO CART`` button. | The button text changed to ``REMOVE``. | +| 3 | Click on the cart icon. | The ``Backpack`` is seen in the cart. | +| 4 | Click on the ``CHECKOUT`` button.
Enter user details and click ``CONTINUE``. | The ``Backpack`` is seen in the cart on the ``CHECKOUT: OVERVIEW`` page. | +| 5 | Click on the ``FINISH`` button. | There is a ``Thank you`` message. | +| 6 | Log out from the website. | Logout was successful. | + +
+ +
+ ๐Ÿ”ต shadow_root_test.py::ShadowRootTest::test_shadow_root + +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Open https://seleniumbase.io/other/shadow_dom.
Click each tab and verify the text contained within the Shadow Root sections. | Tab 1 text: ``Content Panel 1``
Tab 2 text: ``Content Panel 2``
Tab 3 text: ``Content Panel 3`` | + +
+ +
+ ๐Ÿ”ต test_assert_elements.py::ListAssertTests::test_assert_list_of_elements + +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Open https://seleniumbase.io/demo_page. | | +| 2 | Use ``self.assert_elements_present("head", "style", "script")`` to verify that multiple elements are present in the HTML. | The assertion is successful. | +| 3 | Use ``self.assert_elements("h1", "h2", "h3")`` to verify that multiple elements are visible. | The assertion is successful. | +| 4 | Use ``self.assert_elements(["#myDropdown", "#myButton", "#svgRect"])`` to verify that multiple elements are visible. | The assertion is successful. | + +
+ +
+ ๐Ÿ”ต test_calculator.py::CalculatorTests::test_6_times_7_plus_12_equals_54 + +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Open https://seleniumbase.io/apps/calculator.
Perform the following calculation: ``6 ร— 7 + 12`` | The output is ``54`` after pressing ``=`` | + +
+ +
+ ๐Ÿ”ต test_demo_site.py::DemoSiteTests::test_demo_site + +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Open https://seleniumbase.io/demo_page | | +| 2 | Assert the title of the current web page.
Assert that a given element is visible on the page.
Assert that a text substring appears in an element's text. | The assertions were successful. | +| 3 | Type text into various text fields and then verify. | The assertions were successful. | +| 4 | Verify that a hover dropdown link changes page text. | The assertion was successful. | +| 5 | Verify that a button click changes text on the page. | The assertion was successful. | +| 6 | Verify that an SVG element is located on the page. | The assertion was successful. | +| 7 | Verify that a slider control updates a progress bar. | The assertion was successful. | +| 8 | Verify that a "select" option updates a meter bar. | The assertion was successful. | +| 9 | Assert an element located inside an iFrame. | The assertion was successful. | +| 10 | Assert text located inside an iFrame. | The assertion was successful. | +| 11 | Verify that clicking a radio button selects it. | The assertion was successful. | +| 12 | Verify that clicking an empty checkbox makes it selected. | The assertion was successful. | +| 13 | Verify clicking on multiple elements with one call. | The assertions were successful. | +| 14 | Verify that clicking an iFrame checkbox selects it. | The assertions were successful. | +| 15 | Verify that Drag and Drop works. | The assertion was successful. | +| 16 | Assert link text. | The assertion was successful. | +| 17 | Verify clicking on link text. | The action was successful. | +| 18 | Assert exact text in an element. | The assertion was successful. | +| 19 | Highlight a page element. | The action was successful. | +| 20 | Verify that Demo Mode works. | The assertion was successful. | + +
+ +
+ ๐Ÿ”ต test_login.py::SwagLabsLoginTests::test_swag_labs_login + +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Log in to https://www.saucedemo.com with ``standard_user``. | Login was successful. | +| 2 | Log out from the website. | Logout was successful. | + +
+ +
+ ๐Ÿ”ต test_mfa_login.py::TestMFALogin::test_mfa_login + +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Open https://seleniumbase.io/realworld/login
Enter credentials and Sign In. | Sign In was successful. | +| 2 | Click the ``This Page`` button.
Save a screenshot to the logs. | | +| 3 | Click to Sign Out | Sign Out was successful. | + +
+ +
+ ๐Ÿ”ต visual_testing/python_home_test.py::VisualLayoutTests::test_python_home_layout_change + +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Open https://python.org/.
Call ``check_window()`` with ``baseline=True``. | | +| 2 | Remove the ``Donate`` button using ``remove_element(SELECTOR)``.
Call ``check_window()`` with ``level=0``. | The test detects that the ``Donate`` button was removed. The test does not fail because the check was set to ``level=0`` (print-only).
A ``side_by_side_NAME.html`` file appears in the specific ``latest_logs/`` folder of the test. | + +
+ +
+ ๐Ÿ”ต visual_testing/test_layout_fail.py::VisualLayout_FixtureTests::test_python_home_change + +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Open https://python.org/.
Call ``check_window()`` with ``baseline=True``. | | +| 2 | Remove the ``Donate`` button using ``remove_element(SELECTOR)``.
Call ``check_window()`` with ``level=3``. | The test fails because the ``Donate`` button was removed.
A ``side_by_side.html`` file appears in the specific ``latest_logs/`` folder of the test. | + +
+ +
+ ๐Ÿ”ต visual_testing/test_layout_fail.py::VisualLayoutFailureTests::test_xkcd_logo_change + +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Open https://xkcd.com/554/.
Call ``check_window()`` with ``baseline=True``. | | +| 2 | Resize the logo using ``set_attribute()``.
Call ``check_window()`` with ``level=3``. | The test fails because the logo has changed.
A ``side_by_side.html`` file appears in the specific ``latest_logs/`` folder of the test. | + +
+ +
+ ๐Ÿ”ต visual_testing/xkcd_visual_test.py::VisualLayoutTests::test_xkcd_layout_change + +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Open https://xkcd.com/554/.
Call ``check_window()`` with ``baseline=True``. | | +| 2 | Resize the logo using ``set_attribute()``.
Call ``check_window()`` with ``level=0``. | The test detects that the logo has changed. The test does not fail because the check was set to ``level=0`` (print-only).
A ``side_by_side_NAME.html`` file appears in the specific ``latest_logs/`` folder of the test. | + +
diff --git a/examples/cdp_mode/ReadMe.md b/examples/cdp_mode/ReadMe.md new file mode 100644 index 00000000000..d97b008e340 --- /dev/null +++ b/examples/cdp_mode/ReadMe.md @@ -0,0 +1,796 @@ + + +

CDP Mode ๐Ÿ™

+ +๐Ÿ™ SeleniumBase CDP Mode is a stealth mode that uses the Chrome Devtools Protocol (via MyCDP) to control the web browser. CDP Mode can be used as a subset of UC Mode, or via Pure CDP Mode, which has sync and async formats. From CDP Mode, you can make Playwright stealthy (Stealthy Playwright Mode). + +---- + +

โš™๏ธ Stealthy architecture flowchart:

+ +Stealthy architecture flowchart + +---- + +### ๐ŸŽž๏ธ CDP Mode on YouTube: + + + +

(Watch "Undetectable Automation: 5th Edition" on YouTube! โ–ถ๏ธ)

+ +---- + +โ„น๏ธ Note the differences between UC Mode and CDP Mode: + +๐Ÿ‘ค UC Mode's stealth is based on a modified chromedriver (uc_driver) that avoids bot-detection by disconnecting and reconnecting WebDriver from the browser at strategic times. Due to advancements in anti-bot technology, more stealth was needed to bypass advanced bot-detection. (That's where CDP Mode comes in.) + +๐Ÿ™ CDP Mode includes multiple updates to the above, such as: + +* Using CDP directly, which is stealthier than WebDriver. +* Backwards compatibility for existing UC Mode scripts. +* More configuration options when launching browsers. +* The ability to use WebDriver and CDP calls together. +* Full access to call any advanced CDP library method. +* Can be used to make the Playwright library stealthy. + +---- + +### ๐Ÿ™ CDP Mode Usage (when used as a subset of UC Mode): + +* **`sb.activate_cdp_mode()` or `sb.activate_cdp_mode(url)`** + +That disconnects WebDriver from Chrome (which prevents detection), and gives you access to CDP Mode methods (which don't trigger anti-bot checks). + +> (Calling **`sb.goto(url)`** from UC Mode also activates CDP Mode now.) + +Simple example from [SeleniumBase/examples/cdp_mode/raw_gitlab.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/raw_gitlab.py): + +```python +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en") as sb: + sb.activate_cdp_mode() + sb.goto("https://gitlab.com/users/sign_in") + sb.sleep(2) + sb.solve_captcha() + sb.sleep(2) +``` + + + +(If the CAPTCHA wasn't bypassed automatically, then `sb.solve_captcha()` gets the job done.) + +---- + +Here's another example that calls `sb.solve_captcha()`: +([SeleniumBase/examples/cdp_mode/raw_planetmc.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/raw_planetmc.py)) + +```python +from seleniumbase import SB + +with SB(uc=True, test=True, guest=True) as sb: + sb.activate_cdp_mode() + sb.goto("www.planetminecraft.com/account/sign_in/") + sb.sleep(3) + sb.solve_captcha() + sb.wait_for_element_absent("input[disabled]") + sb.sleep(2) +``` + + + +In many cases, the CAPTCHA will be solved automatically without needing to call `solve_captcha()`. + +---- + +You can also use `PyAutoGUI` to click on elements with the mouse by calling `sb.gui_click_element(selector)`. (The `PyAutoGUI` methods start with `gui`.) + +โ„น๏ธ Note that `PyAutoGUI` is an optional dependency. If calling a method that needs it when not already installed, then `SeleniumBase` installs `PyAutoGUI` at runtime. + +---- + +### ๐Ÿ™ Here are a few common CDP Mode methods: + +* `goto(url)` (Navigate to the given URL) +* `click(selector)` (Uses the CDP API to click) +* `click_if_visible(selector)` (Click if visible) +* `solve_captcha()` (Uses CDP to click a CAPTCHA) +* `type(selector, text)` (Type text into a selector) +* `press_keys(selector, text)` (Human-speed `type`) +* `select_all(selector)` (Returns matching elements) +* `get_text(selector)` (Returns the element's text) + +To use WebDriver-only methods again, call: + +* **`sb.reconnect()`** or **`sb.connect()`** + +(Note that reconnecting allows anti-bots to detect you, so only reconnect if it is safe to do so.) + +To disconnect again, call: + +* **`sb.disconnect()`** + +While disconnected, if you call a WebDriver method, then SeleniumBase will attempt to use the CDP Mode version of that method (if available). For example, if you call `sb.click(selector)` instead of `sb.cdp.click(selector)`, then your WebDriver call will automatically redirect to the CDP Mode version. Not all WebDriver methods have a matching CDP Mode method. In that scenario, calling a WebDriver method while disconnected could raise an error, or make WebDriver automatically reconnect first. + +To find out if WebDriver is connected or disconnected, call: + +* **`sb.is_connected()`** + +Note: When CDP Mode is initialized from UC Mode, the WebDriver is disconnected from the browser. (The stealthy CDP Mode takes over.) + +---- + +### ๐Ÿ™ CDP Mode examples ([SeleniumBase/examples/cdp_mode](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/cdp_mode)) + +

+ +
+
+ โ–ถ๏ธ ๐Ÿ”– Example 1: (Pokemon site using Incapsula/Imperva protection with invisible reCAPTCHA) + +```python +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en", ad_block=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.pokemon.com/us/pokedex") + sb.sleep(1.5) + sb.click_if_visible("button#onetrust-accept-btn-handler") + sb.sleep(1.5) + sb.click('b:contains("Show Advanced Search")') + sb.sleep(2.5) + sb.click('span[data-type="type"][data-value="electric"]') + sb.sleep(0.7) + sb.scroll_into_view("a#advSearch") + sb.sleep(0.7) + sb.click("a#advSearch") + sb.sleep(1.2) + sb.click('img[src*="img/pokedex/detail/025.png"]') + sb.assert_text("Pikachu", 'div[class*="title"]') + sb.assert_element('img[alt="Pikachu"]') + sb.scroll_into_view("div.pokemon-ability-info") + sb.sleep(1.2) + sb.flash('div[class*="title"]') + sb.flash('img[alt="Pikachu"]') + sb.flash("div.pokemon-ability-info") + name = sb.get_text("label.styled-select") + info = sb.get_text("div.version-descriptions p.active") + print("*** %s: ***\n* %s" % (name, info)) + sb.sleep(2) + sb.highlight_overlay("div.pokemon-ability-info") + sb.sleep(2) + sb.goto("https://events.pokemon.com/EventLocator/") + sb.sleep(2) + sb.click('span:contains("Championship")') + sb.sleep(2) + events = sb.select_all("div.event-info__title") + print("*** Pokรฉmon Championship Events: ***") + for event in events: + print("* " + event.text) + sb.sleep(2) +``` + +
+ +> [SeleniumBase/examples/cdp_mode/raw_pokemon.py](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/cdp_mode/raw_pokemon.py) + + +
+
+ โ–ถ๏ธ ๐Ÿ”– Example 2: (Hyatt site using Kasada protection) + +```python +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en", guest=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.hyatt.com/") + sb.sleep(3.6) + sb.click_if_visible('button[aria-label="Close"]') + sb.sleep(0.1) + sb.click_if_visible("#onetrust-reject-all-handler") + sb.sleep(1.2) + location = "Anaheim, CA, USA" + sb.type('input[id="search-term"]', location) + sb.sleep(1.2) + sb.click('li[data-js="suggestion"]') + sb.sleep(0.6) + sb.click_if_visible('button[aria-label="Close"]') + sb.sleep(0.8) + sb.click("button.be-button-shop") + sb.sleep(1) + sb.click_if_visible('[label="Find Hotels"]') + sb.sleep(5.5) + card_info = 'div[data-booking-status="BOOKABLE"] [class*="HotelCard_info"]' + hotels = sb.select_all(card_info) + print("Hyatt Hotels in %s:" % location) + print("(" + sb.get_text('span[class*="summary_destination"]') + ")") + if len(hotels) == 0: + print("No availability over the selected dates!") + for hotel in hotels: + info = hotel.text.strip() + if "Avg/Night" in info and not info.startswith("Rates from"): + name = info.split(" (")[0].split(" + ")[0].split(" Award Cat")[0] + name = name.split(" Rates from :")[0] + price = "?" + if "Rates from : " in info: + price = info.split("Rates from : ")[1].split(" Avg/Night")[0] + print("* %s => %s" % (name, price)) +``` + +
+ +> [SeleniumBase/examples/cdp_mode/raw_hyatt.py](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/cdp_mode/raw_hyatt.py) + + +
+
+ โ–ถ๏ธ ๐Ÿ”– Example 3: (BestWestern site using DataDome protection) + +```python +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en", guest=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.bestwestern.com/en_US.html") + sb.sleep(3) + sb.click_if_visible(".onetrust-close-btn-handler") + sb.sleep(1) + sb.click("input#destination-input") + sb.sleep(2) + location = "Palm Springs, CA, USA" + sb.press_keys("input#destination-input", location) + sb.sleep(1) + sb.click("ul#google-suggestions li") + sb.sleep(1) + sb.click("button#btn-modify-stay-update") + sb.sleep(4) + sb.click("label#available-label") + sb.sleep(2.5) + print("Best Western Hotels in %s:" % location) + summary_details = sb.get_text("#summary-details-column") + dates = summary_details.split("DESTINATION")[-1] + dates = dates.split(" CHECK-OUT")[0].strip() + " CHECK-OUT" + dates = dates.replace(" ", " ") + print("(Dates: %s)" % dates) + flip_cards = sb.select_all(".flipCard") + for i, flip_card in enumerate(flip_cards): + hotel = flip_card.query_selector(".hotelName") + price = flip_card.query_selector(".priceSection") + if hotel and price: + print("* %s: %s => %s" % ( + i + 1, hotel.text.strip(), price.text.strip()) + ) +``` + +
+ +> [SeleniumBase/examples/cdp_mode/raw_bestwestern.py](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/cdp_mode/raw_bestwestern.py) + + +
+
+ โ–ถ๏ธ ๐Ÿ”– Example 4: (Walmart site using Akamai protection with PerimeterX) + +```python +from seleniumbase import SB + +with SB(uc=True, test=True, ad_block=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.walmart.com/") + sb.sleep(2.2) + continue_button = 'button:contains("Continue shopping")' + if sb.is_element_visible(continue_button): + sb.gui_click_element(continue_button) + sb.sleep(0.6) + sb.click('input[aria-label="Search"]') + sb.sleep(1.2) + search = "Settlers of Catan Board Game" + required_text = "Catan" + sb.press_keys('input[aria-label="Search"]', search + "\n") + sb.sleep(3.8) + sb.remove_elements('[data-testid="skyline-ad"]') + sb.remove_elements('[data-testid="sba-container"]') + print('*** Walmart Search for "%s":' % search) + print(' (Results must contain "%s".)' % required_text) + unique_item_text = [] + sb.click_if_visible('[data-automation-id="sb-btn-close-mark"]') + items = sb.find_elements('[data-item-id]') + for item in items: + if required_text.lower() in item.text.lower(): + description = item.query_selector( + '[data-automation-id="product-title"]' + ) + if description and description.text not in unique_item_text: + unique_item_text.append(description.text) + print("* " + description.text) + price = item.query_selector( + '[data-automation-id="product-price"]' + ) + if price: + price_text = price.text + price_text = price_text.split("current price Now ")[-1] + price_text = price_text.split("current price ")[-1] + price_text = price_text.split(" ")[0] + print(" (" + price_text + ")") + item.scroll_into_view() +``` + +
+ +> [SeleniumBase/examples/cdp_mode/raw_walmart.py](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/cdp_mode/raw_walmart.py) + + +
+
+ โ–ถ๏ธ ๐Ÿ”– Example 5: (Nike site using Shape Security) + +```python +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en", pls="none") as sb: + sb.activate_cdp_mode() + sb.goto("https://www.nike.com/") + sb.sleep(2.5) + sb.click('[data-testid="user-tools-container"] search') + sb.sleep(1.5) + search = "Nike Air Force 1" + sb.press_keys('input[type="search"]', search) + sb.sleep(4) + details = 'ul[data-testid*="products"] figure .details' + elements = sb.select_all(details) + if elements: + print('**** Found results for "%s": ****' % search) + for element in elements: + print("* " + element.text) + sb.sleep(2) +``` + +
+ +> [SeleniumBase/examples/cdp_mode/raw_nike.py](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/cdp_mode/raw_nike.py) + +

+ +(Note: Extra sb.sleep() calls have been added to prevent bot-detection because some sites will flag you as a bot if you perform actions too quickly.) + +(Note: Some sites may IP-block you for 36 hours or more if they catch you using regular Selenium WebDriver. Be extra careful when creating and/or modifying automation scripts that run on them.) + +---- + +### ๐Ÿ™ CDP Mode API / Methods + +```python +sb.get(url, **kwargs) +sb.open(url, **kwargs) # Same as sb.get(url, **kwargs) in CDP Mode +sb.goto(url, **kwargs) # Same as sb.get(url, **kwargs) in CDP Mode +sb.reload(ignore_cache=True, script_to_evaluate_on_load=None) +sb.goto_if_not_url(url) +sb.refresh(*args, **kwargs) +sb.get_event_loop() +sb.get_rd_host() # Returns the remote-debugging host +sb.get_rd_port() # Returns the remote-debugging port +sb.get_rd_url() # Returns the remote-debugging URL +sb.get_endpoint_url() # Same as sb.get_rd_url() +sb.get_port() # Same as sb.get_rd_port() +sb.get_websocket_url() # Returns the websocket URL +sb.add_handler(event, handler) +sb.find_element(selector, best_match=False, timeout=None) +sb.find(selector, best_match=False, timeout=None) +sb.locator(selector, best_match=False, timeout=None) +sb.find_element_by_text(text, tag_name=None, timeout=None) +sb.find_all(selector, timeout=None) +sb.find_elements_by_text(text, tag_name=None) +sb.select(selector, timeout=None) +sb.select_all(selector, timeout=None) +sb.find_elements(selector, timeout=None) +sb.find_visible_elements(selector, timeout=None) +sb.click(selector, timeout=None, scroll=True) +sb.click_if_visible(selector, timeout=0, scroll=True) +sb.click_visible_elements(selector, limit=0, scroll=True) +sb.click_nth_element(selector, number, scroll=True) +sb.click_nth_visible_element(selector, number, scroll=True) +sb.click_link(link_text) +sb.click_with_offset(selector, x, y, center=False, scroll=True) +sb.click_and_hold(selector, timeout=None, scroll=True, timeframe=2) +sb.drag_and_drop(drag_selector, drop_selector, timeout=None, scroll=True) +sb.go_back() +sb.go_forward() +sb.get_navigation_history() +sb.tile_windows(windows=None, max_columns=0) +sb.grant_permissions(permissions, origin=None) +sb.grant_all_permissions() +sb.reset_permissions() +sb.get_all_urls(absolute=True) +sb.get_all_cookies(*args, **kwargs) +sb.set_all_cookies(*args, **kwargs) +sb.save_cookies(*args, **kwargs) +sb.load_cookies(*args, **kwargs) +sb.clear_cookies() +sb.sleep(seconds) +sb.bring_active_window_to_front() +sb.bring_to_front() +sb.get_active_element() +sb.get_active_element_css() +sb.click_active_element() +sb.mouse_click(selector, timeout=None, scroll=True) +sb.nested_click(parent_selector, selector) +sb.get_nested_element(parent_selector, selector) +sb.select_option_by_text(dropdown_selector, option) +sb.select_option_by_index(dropdown_selector, option) +sb.select_option_by_value(dropdown_selector, option) +sb.flash(selector, duration=1, color="44CC88", pause=0) +sb.highlight(selector, scroll=True, timeout=None) +sb.focus(selector, timeout=None) +sb.highlight_overlay(selector) +sb.get_parent(element) +sb.remove_element(selector) +sb.remove_from_dom(selector) +sb.remove_elements(selector) +sb.send_keys(selector, text, timeout=None) +sb.press_keys(selector, text, timeout=None) +sb.type(selector, text, timeout=None) +sb.set_value(selector, text, timeout=None) +sb.clear_input(selector, timeout=None) +sb.clear(selector, timeout=None) +sb.submit(selector) +sb.evaluate(expression) +sb.execute_script(expression) +sb.js_dumps(obj_name) +sb.maximize() +sb.minimize() +sb.medimize() +sb.set_window_rect(x, y, width, height) +sb.reset_window_size() +sb.open_new_window(url=None, switch_to=True) +sb.switch_to_window(window) +sb.switch_to_newest_window() +sb.open_new_tab(url=None, switch_to=True) +sb.switch_to_tab(tab) +sb.switch_to_newest_tab() +sb.close_active_tab() +sb.get_active_tab() +sb.get_tabs() +sb.get_window() +sb.get_text(selector="body", timeout=None) +sb.get_title() +sb.get_current_url() +sb.get_origin() +sb.get_html(include_shadow_dom=True) +sb.get_page_source(include_shadow_dom=True) +sb.get_beautiful_soup(source=None) +sb.get_user_agent() +sb.get_cookie_string() +sb.get_locale_code() +sb.get_local_storage_item(key) +sb.get_session_storage_item(key) +sb.get_screen_rect() +sb.get_window_rect() +sb.get_window_size() +sb.get_window_position() +sb.get_element_rect(selector, timeout=None) +sb.get_element_size(selector, timeout=None) +sb.get_element_position(selector, timeout=None) +sb.get_gui_element_rect(selector, timeout=None) +sb.get_gui_element_center(selector, timeout=None) +sb.get_document() +sb.get_flattened_document() +sb.get_element_attributes(selector) +sb.get_element_attribute(selector, attribute) +sb.get_attribute(selector, attribute) +sb.get_element_html(selector, timeout=None) +sb.get_mfa_code(totp_key=None) +sb.enter_mfa_code(selector, totp_key=None, timeout=None) +sb.activate_messenger() +sb.set_messenger_theme(theme="default", location="default") +sb.post_message(message, duration=None, pause=True, style="info") +sb.download_file(file_url) +sb.save_file_as(file_url, new_file_name) +sb.assert_downloaded_file(file, timeout=None) +sb.get_path_of_downloaded_file(file) +sb.set_download_path(path) +sb.set_locale(locale) +sb.set_local_storage_item(key, value) +sb.set_session_storage_item(key, value) +sb.set_attributes(selector, attribute, value) +sb.is_attribute_present(selector, attribute, value=None) +sb.is_online() +sb.solve_captcha() +sb.click_captcha() +sb.gui_press_key(key) +sb.gui_press_keys(keys) +sb.gui_write(text) +sb.gui_click_x_y(x, y, timeframe=0.27) +sb.gui_click_element(selector, timeframe=0.27) +sb.gui_click_with_offset(selector, x, y, timeframe=0.27, center=False) +sb.gui_click_captcha() +sb.gui_drag_drop_points(x1, y1, x2, y2, timeframe=0.35) +sb.gui_drag_and_drop(drag_selector, drop_selector, timeframe=0.35) +sb.gui_click_and_hold(selector, timeframe=0.35) +sb.gui_move_to_element(selector, timeframe=0.27) +sb.gui_hover_x_y(x, y, timeframe=0.27) +sb.gui_hover_element(selector, timeframe=0.27) +sb.gui_hover_and_click(hover_selector, click_selector) +sb.hover_element(selector) +sb.hover_and_click(hover_selector, click_selector) +sb.internalize_links() +sb.is_checked(selector) +sb.is_selected(selector) +sb.check_if_unchecked(selector) +sb.select_if_unselected(selector) +sb.uncheck_if_checked(selector) +sb.unselect_if_selected(selector) +sb.is_element_present(selector) +sb.is_element_visible(selector) +sb.is_text_visible(text, selector="body") +sb.is_exact_text_visible(text, selector="body") +sb.wait_for_text(text, selector="body", timeout=None) +sb.wait_for_text_not_visible(text, selector="body", timeout=None) +sb.wait_for_element_visible(selector, timeout=None) +sb.wait_for_element(selector, timeout=None) +sb.wait_for_element_not_visible(selector, timeout=None) +sb.wait_for_element_absent(selector, timeout=None) +sb.wait_for_any_of_elements_visible(*args, **kwargs) +sb.wait_for_any_of_elements_present(*args, **kwargs) +sb.assert_any_of_elements_visible(*args, **kwargs) +sb.assert_any_of_elements_present(*args, **kwargs) +sb.assert_element(selector, timeout=None) +sb.assert_element_visible(selector, timeout=None) +sb.assert_element_present(selector, timeout=None) +sb.assert_element_absent(selector, timeout=None) +sb.assert_element_not_visible(selector, timeout=None) +sb.assert_element_attribute(selector, attribute, value=None) +sb.assert_title(title) +sb.assert_title_contains(substring) +sb.assert_url(url) +sb.assert_url_contains(substring) +sb.assert_text(text, selector="html", timeout=None) +sb.assert_exact_text(text, selector="html", timeout=None) +sb.assert_text_not_visible(text, selector="body", timeout=None) +sb.assert_true(expression, msg=None) +sb.assert_false(expression, msg=None) +sb.assert_equal(first, second) +sb.assert_not_equal(first, second) +sb.assert_in(first, second) +sb.assert_not_in(first, second) +sb.js_scroll_into_view(selector) +sb.scroll_into_view(selector) +sb.scroll_to_y(y) +sb.scroll_to_top() +sb.scroll_to_bottom() +sb.scroll_up(amount=25) +sb.scroll_down(amount=25) +sb.save_page_source(name, folder=None) +sb.save_as_html(name, folder=None) +sb.save_screenshot(name, folder=None, selector=None) +sb.print_to_pdf(name, folder=None) +sb.save_as_pdf(name, folder=None) +sb.save_as_pdf_to_logs(name=None) +sb.save_screenshot_to_logs(name=None, selector=None) +sb.save_page_source_to_logs(name=None) +sb.save_as_html_to_logs(name=None) +sb.save_data_to_logs(data, file_name=None) +sb.append_data_to_logs(data, file_name=None) +sb.quit() # (Pure CDP Mode only: `sb_cdp`) +``` + +โ„น๏ธ When available, calling `sb.METHOD()` redirects to `sb.cdp.METHOD()` from UC + CDP Mode. + +---- + + + +### ๐Ÿ™ Pure CDP Mode (sb_cdp) + +In Pure CDP Mode, the browser is launched using CDP, and all browser actions are performed using CDP. WebDriver isn't available at all, but SeleniumBase can still call PyAutoGUI methods when CDP isn't enough. + +๐Ÿ™ Here's how to initialize Pure CDP Mode with a starting URL: + +```python +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(URL) +``` + +๐Ÿ™ You can also initialize Pure CDP Mode and set the URL later: + +```python +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +sb.goto(URL) +``` + +Pure CDP Mode includes all methods from regular CDP Mode. To quit a Pure CDP Mode browser before Python goes out-of-scope, use `sb.quit()`. + +Basic example from [SeleniumBase/examples/cdp_mode/raw_cdp_turnstile.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/raw_cdp_turnstile.py): + +```python +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +sb.goto("https://seleniumbase.io/apps/turnstile") +sb.solve_captcha() +sb.assert_element("img#captcha-success") +sb.set_messenger_theme(location="top_left") +sb.post_message("SeleniumBase wasn't detected", duration=3) +sb.quit() +``` + +Another example: ([SeleniumBase/examples/cdp_mode/raw_cdp_methods.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/raw_cdp_methods.py)) + +```python +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +sb.goto("https://seleniumbase.io/demo_page") +sb.press_keys("input", "Text") +sb.highlight("button") +sb.type("textarea", "Here are some words") +sb.click("button") +sb.set_value("input#mySlider", "100") +sb.click_visible_elements("input.checkBoxClassB") +sb.select_option_by_text("#mySelect", "Set to 75%") +sb.gui_hover_and_click("#myDropdown", "#dropOption2") +sb.gui_click_element("#checkBox1") +sb.gui_drag_and_drop("img#logo", "div#drop2") +sb.nested_click("iframe#myFrame3", ".fBox") +sb.sleep(2) +sb.quit() +``` + +โ„น๏ธ Even if you don't call `sb.quit()`, the browser still quits after the script goes out-of-scope. + +---- + +### ๐Ÿ™ CDP Mode Async API / Methods + +Initialization: + +```python +from seleniumbase import cdp_driver + +driver = await cdp_driver.start_async() +page = await driver.get(url, **kwargs) +``` + +Methods: (Sometimes `page` is named `tab` in examples where `page` is already taken.) + +```python +await page.get(url="about:blank") +await page.open(url="about:blank") # Same as await page.get(url) in CDP Mode +await page.goto(url="about:blank") # Same as await page.get(url) in CDP Mode +await page.find(text, best_match=False, timeout=10) # text can be selector +await page.find_all(text, timeout=10) # text can be selector +await page.select(selector, timeout=10) +await page.select_all(selector, timeout=10, include_frames=False) +await page.query_selector(selector) +await page.query_selector_all(selector) +await page.find_element_by_text(text, best_match=False) +await page.find_elements_by_text(text) +await page.send_keys(selector, text, timeout=5) +await page.type(selector, text, timeout=5) +await page.click(selector, timeout=5) +await page.click_if_visible(selector, timeout=0) +await page.click_with_offset(selector, x, y, center=False, timeout=5) +await page.solve_captcha() +await page.click_captcha() # Same as solve_captcha() +await page.reload(ignore_cache=False, script_to_evaluate_on_load=None) +await page.evaluate(expression) +await page.js_dumps(obj_name) +await page.back() +await page.forward() +await page.get_window() +await page.get_content() +await page.maximize() +await page.minimize() +await page.fullscreen() +await page.medimize() +await page.set_window_size(left=0, top=0, width=1280, height=1024) +await page.set_window_rect(left=0, top=0, width=1280, height=1024) +await page.activate() +await page.bring_to_front() +await page.set_window_state(left=0, top=0, width=1280, height=720, state="normal") +await page.get_navigation_history() +await page.get_user_agent() +await page.get_cookie_string() +await page.get_locale_code() +await page.is_element_present(selector) +await page.is_element_visible(selector) +await page.is_online() +await page.open_external_inspector() # Open separate browser for debugging +await page.close() +await page.scroll_down(amount=25) +await page.scroll_up(amount=25) +await page.wait_for(selector="", text="", timeout=10) +await page.set_attributes(selector, attribute, value) +await page.internalize_links() +await page.download_file(url, filename=None) +await page.save_screenshot(filename="auto", format="png", full_page=False) +await page.print_to_pdf(filename="auto") +await page.set_download_path(path) +await page.get_all_linked_sources() +await page.get_all_urls(absolute=True) +await page.get_html() +await page.get_page_source() +await page.get_element_rect(selector, timeout=5) # (window-based) +await page.get_window_rect() +await page.get_gui_element_rect(selector, timeout=5) # (screen-based) +await page.get_title() +await page.get_current_url() +await page.get_origin() +await page.get_document() +await page.get_flattened_document() +await page.get_local_storage() +await page.set_local_storage(items) +``` + +---- + +### ๐Ÿ™ CDP Mode WebElement API / Methods + +After finding an element in CDP Mode, you can access `WebElement` methods: + +(Eg. After `element = sb.find_element(selector)`) + +```python +element.clear_input() +element.click() +element.click_with_offset(x, y, center=False) +element.click_and_hold(timeframe=2) +element.flash(duration=0.5, color="EE4488") +element.focus() +element.gui_click(timeframe=0.27) +element.highlight_overlay() +element.is_in_viewport() +element.mouse_click() +element.mouse_drag(destination) +element.mouse_move() +element.press_keys(text) +element.query_selector(selector) +element.querySelector(selector) +element.query_selector_all(selector) +element.querySelectorAll(selector) +element.remove_from_dom() +element.save_screenshot(*args, **kwargs) +element.save_to_dom() +element.scroll_into_view() +element.select_option() +element.send_file(*file_paths) +element.send_keys(text) +element.set_text(value) +element.type(text) +element.get_position() +element.get_html() +element.get_js_attributes() +element.get_attribute(attribute) +element.get_parent() +``` + +---- + +### ๐ŸŽž๏ธ YouTube videos about CDP Mode: + + +

(Watch "Undetectable Automation 4" on YouTube! โ–ถ๏ธ)

+ +---- + + +

(Watch "Undetectable Automation: 5th Edition" on YouTube! โ–ถ๏ธ)

+ +---- + + +

(Watch "Hacking websites with CDP" on YouTube! โ–ถ๏ธ)

+ +---- + + +

("Unlimited Free Web-Scraping with GitHub Actions" โ–ถ๏ธ)

+ +---- + +SeleniumBase + +
SeleniumBase
diff --git a/examples/cdp_mode/__init__.py b/examples/cdp_mode/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/cdp_mode/playwright/ReadMe.md b/examples/cdp_mode/playwright/ReadMe.md new file mode 100644 index 00000000000..33829d724e3 --- /dev/null +++ b/examples/cdp_mode/playwright/ReadMe.md @@ -0,0 +1,325 @@ + + +

Stealthy Playwright Mode ๐ŸŽญ

+ +๐ŸŽญ Stealthy Playwright Mode is a subset of **[SeleniumBase CDP Mode](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/ReadMe.md)** where [playwright-python](https://github.com/microsoft/playwright-python) attaches to a stealthy browser session via the remote-debugging URL. This lets Playwright bypass bot-detection while allowing APIs of both frameworks to work in tandem. Under the hood, Playwright achieves stealth via connect_over_cdp(). + +-------- + + +

(See Stealthy Playwright Mode on YouTube! โ–ถ๏ธ)

+ +-------- + +## ๐Ÿ› ๏ธ Installation + +To use **Stealthy Playwright Mode**, simply install the necessary Python packages: + +```zsh +pip install seleniumbase playwright +``` + +> **Note:** Just as standard Playwright can use `channel="chrome"` to bypass internal binary downloads, Stealthy Playwright Mode achieves the same by attaching to the system Chrome browser launched by SeleniumBase. This lets you skip the large `playwright install` step entirely. + +## ๐Ÿ’ป Usage + +**Stealthy Playwright Mode** comes in three different formats: +1. `sb_cdp` "sync" format +2. `SB()` "nested sync" format +3. `cdp_driver` "async" format + +### 1. The lightweight "sync" format (`sb_cdp`) + +Ideal for standalone scripts that primarily use Playwright but need SeleniumBase's stealth and CAPTCHA-solving power without the overhead of WebDriver. + +```python +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://example.com") +``` + +### 2. The full-suite "nested sync" format (`SB()`) + +Best for hybrid projects where you need to switch between WebDriver and Playwright APIs in the same session. + +```python +from playwright.sync_api import sync_playwright +from seleniumbase import SB + +with SB(uc=True) as sb: + sb.activate_cdp_mode() + endpoint_url = sb.get_endpoint_url() + + with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://example.com") +``` + +### 3. The "async" format (`cdp_driver`) + +Designed for modern asynchronous Python. This allows you to run multiple concurrent stealth sessions using `async/await` and Playwright's `async_api`. + +```python +import asyncio +from seleniumbase import cdp_driver +from playwright.async_api import async_playwright + +async def main(): + driver = await cdp_driver.start_async() + endpoint_url = driver.get_endpoint_url() + + async with async_playwright() as p: + browser = await p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + await page.goto("https://example.com") + +if __name__ == "__main__": + asyncio.run(main()) +``` + +### ๐Ÿ’ก Key differences of the 3 stealthy formats: + +- **`sb_cdp`**: Simplest setup. CDP launches a stealthy browser. (No WebDriver) + +- **`SB()`**: Maximum utility. Gives you the full range of APIs: WebDriver, CDP, and Playwright. (WebDriver launches a stealthy browser.) + +- **`cdp_driver`**: Best for performance. `asyncio` handles non-blocking tasks. CDP launches a stealthy browser. (No WebDriver) + +-------- + +### ๐ŸŽญ Converting regular Playwright scripts to Stealthy Playwright Mode: + +If you have a regular Playwright script that looks like this: + +```python +from playwright.sync_api import sync_playwright + +with sync_playwright() as p: + browser = p.chromium.launch(channel="chrome", headless=False) + page = browser.new_context().new_page() + page.goto("https://example.com") +``` + +Then the Stealthy Playwright Mode version of that would look like this: + +```python +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://example.com") +``` + +-------- + +### ๐ŸŽญ Stealthy Playwright Mode details: + +โ„น๏ธ The `sb_cdp` and `cdp_driver` formats don't use WebDriver at all, meaning that `chromedriver` isn't needed. From these two formats, Stealthy Playwright Mode can call [CDP Mode methods](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/cdp_mode_methods.md) and Playwright methods. + +โ„น๏ธ The `SB()` format requires WebDriver, therefore `chromedriver` will be downloaded, modified for stealth, and renamed as `uc_driver` if not already present. The `SB()` format has access to Selenium WebDriver methods via [the SeleniumBase API](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/method_summary.md). When using Stealthy Playwright Mode from the `SB()` format, all the APIs are accessible: Selenium, SeleniumBase, [UC Mode](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/uc_mode.md), [CDP Mode](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/ReadMe.md), and Playwright. + +โ„น๏ธ Default timeout values are different between Playwright and SeleniumBase. For instance, a 30-second default timeout in a Playwright method might only be 10 seconds in the equivalent SeleniumBase method. + +โ„น๏ธ When specifying custom timeout values, Playwright uses milliseconds, whereas SeleniumBase uses seconds. Eg. `page.wait_for_timeout(2000)` in Playwright is the equivalent of `sb.sleep(2)` in SeleniumBase. + +โ„น๏ธ Although hard sleeps are generally discouraged, they become a tactical tool in stealth mode because that extra waiting helps the automation look more human. Hard sleeps are used in multiple examples to prevent rate limits from being exceeded. + +๐ŸŒ  For additional stealth, use `sb.goto(URL)` instead of `page.goto(URL)`. + +-------- + +### ๐ŸŽญ A few examples of Stealthy Playwright Mode: + +๐ŸŽญ Here's an example that searches SeatGeek: + +```python +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(locale="en", ad_block=True) +sb.goto("https://seatgeek.com/") +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + input_field = 'input[name="search"]' + page.wait_for_selector(input_field) + page.wait_for_timeout(1600) + query = "Jerry Seinfeld" + search_box = page.locator(input_field) + search_box.press_sequentially(query, delay=80) + page.wait_for_timeout(1600) + page.click("li#active-result-item") + page.wait_for_timeout(4200) + print('*** SeatGeek Search for "%s":' % query) + items = page.locator('[data-testid="listing-item"]') + for i in range(items.count()): + item_text = items.nth(i).inner_text() + print(item_text.replace("\n\n", "\n")) +``` + +(From [examples/cdp_mode/playwright/raw_seatgeek_sync.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/playwright/raw_seatgeek_sync.py)) + +๐ŸŽญ Here's an example that searches Foot Locker: + +```python +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(locale="en", ad_block=True) +sb.goto("https://www.footlocker.com/") +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + input_field = 'input[name="query"]' + page.wait_for_selector(input_field) + sb.sleep(1.5) + sb.click_if_visible('button[id*="Agree"]') + sb.sleep(1.2) + page.click(input_field) + sb.sleep(0.5) + search = "Nike Shoes" + page.type(input_field, search) + sb.sleep(1.2) + page.click('ul[id*="typeahead"] li div') + sb.sleep(3.5) + elements = sb.select_all("a.ProductCard-link") + if elements: + print('**** Found results for "%s": ****' % search) + for element in elements: + print("------------------ >>>") + print("* " + element.text) + sb.sleep(2) +``` + +(From [examples/cdp_mode/playwright/raw_footlocker_sync.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/playwright/raw_footlocker_sync.py)) + +๐ŸŽญ Here's an example that solves the Bing CAPTCHA: + +```python +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(locale="en") +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://www.bing.com/turing/captcha/challenge") + page.wait_for_timeout(2000) + sb.solve_captcha() + page.wait_for_timeout(2000) +``` + +(From [examples/cdp_mode/playwright/raw_bing_cap_sync.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/playwright/raw_bing_cap_sync.py)) + +#### ๐ŸŽญ For all included examples, see [examples/cdp_mode/playwright](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/cdp_mode/playwright). + +-------- + +### ๐ŸŽญ More details about Stealthy Playwright Mode: + +Stealthy Playwright Mode uses the system's Chrome browser by default. There's also the option of setting `use_chromium=True` to use the unbranded Chromium browser instead, which still supports extensions. (With regular Playwright, you would generally need to run `playwright install` to download a special version of Chrome before running Playwright scripts, unless you set `channel="chrome"` to use the system's Chrome browser instead.) + +Playwright's `:has-text()` selector is the equivalent of SeleniumBase's `:contains()` selector, except for one small difference: `:has-text()` isn't case-sensitive, but `:contains()` is. + +In the sync formats, `get_endpoint_url()` also applies `nest-asyncio` so that nested event loops are allowed. (Python doesn't allow nested event loops by default). Without this, you'd get the error: `"Cannot run the event loop while another loop is running"` when calling CDP Mode methods (such as `solve_captcha()`) from within the Playwright context manager. This `nest-asyncio` call is done behind-the-scenes so that users don't need to handle this on their own. + +-------- + +### ๐ŸŽญ Proxy with auth in Stealthy Playwright Mode: + +To use an authenticated proxy in Stealthy Playwright Mode, **do these two things**:
**1.** Set the`proxy` arg when launching Chrome. +-- Eg: `sb_cdp.Chrome(proxy="USER:PASS@IP:PORT")` or `cdp_driver.start_async("USER:PASS@IP:PORT")`.
**2.** Open the URL with SeleniumBase **before** using `endpoint_url` to connect to the browser with Playwright. + +โš ๏ธ If any trouble with the above, set `use_chromium=True` so that you can use the base Chromium browser, which still allows extensions, unlike regular branded Chrome, which removed the `--load-extension` command-line switch. (*An extension is used to set the auth for the proxy, which is needed when CDP can't set the proxy alone, such as for navigation after the initial page load*). + +In the sync format, use `sb.goto(url)` to open the url before connecting Playwright: + +```python +sb = sb_cdp.Chrome(use_chromium=True, proxy="user:pass@server:port") +sb.goto(url) +endpoint_url = sb.get_endpoint_url() +# ... +``` + +In the async format, use, `driver.get(url)` to open the url before connecting Playwright: + +```python +driver = await cdp_driver.start_async(use_chromium=True, proxy="user:pass@server:port") +await driver.get(url) +endpoint_url = driver.get_endpoint_url() +# ... +``` + +Here's an example of using an authenticated proxy with Stealthy Playwright Mode:
(The URL is opened before attaching Playwright so that proxy settings take effect) + +```python +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(use_chromium=True, proxy="user:pass@server:port") +sb.goto(url) +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + # ... +``` + +(Fill in the `url` and the `proxy` details to complete the script.) + +Here's the same thing for the `async` format: + +```python +import asyncio +from playwright.async_api import async_playwright +from seleniumbase import cdp_driver + +async def main(): + driver = await cdp_driver.start_async(use_chromium=True, proxy="user:pass@server:port") + await driver.get(url) + endpoint_url = driver.get_endpoint_url() + + async with async_playwright() as p: + browser = await p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + # ... + +if __name__ == "__main__": + asyncio.run(main()) +``` + +(Fill in the `url` and the `proxy` details to complete the script.) + +-------- + +#### ๐ŸŽญ This flowchart shows how Stealthy Playwright Mode fits into CDP Mode: + +Stealthy architecture flowchart + +(See the [**CDP Mode** ReadMe](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/ReadMe.md) for more information about that.) + +#### ๐ŸŽญ See [examples/cdp_mode/playwright](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/cdp_mode/playwright) for Stealthy Playwight Mode examples. + +-------- + +SeleniumBasePlaywright diff --git a/examples/cdp_mode/playwright/__init__.py b/examples/cdp_mode/playwright/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/cdp_mode/playwright/raw_basic_async.py b/examples/cdp_mode/playwright/raw_basic_async.py new file mode 100644 index 00000000000..4972a8b2d3f --- /dev/null +++ b/examples/cdp_mode/playwright/raw_basic_async.py @@ -0,0 +1,22 @@ +import asyncio +from playwright.async_api import async_playwright +from seleniumbase import cdp_driver + + +async def main(): + driver = await cdp_driver.start_async() + endpoint_url = driver.get_endpoint_url() + + async with async_playwright() as p: + browser = await p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + await page.goto("https://seleniumbase.io/simple/login") + await page.fill("#username", "demo_user") + await page.fill("#password", "secret_pass") + await page.click("#log-in") + await page.wait_for_selector("h1") + await page.wait_for_timeout(1000) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/cdp_mode/playwright/raw_basic_nested.py b/examples/cdp_mode/playwright/raw_basic_nested.py new file mode 100644 index 00000000000..99ce8e96a18 --- /dev/null +++ b/examples/cdp_mode/playwright/raw_basic_nested.py @@ -0,0 +1,16 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import SB + +with SB(uc=True) as sb: + sb.activate_cdp_mode() + endpoint_url = sb.get_endpoint_url() + + with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://seleniumbase.io/simple/login") + page.fill("#username", "demo_user") + page.fill("#password", "secret_pass") + page.click("#log-in") + page.wait_for_selector("h1") + page.wait_for_timeout(1000) diff --git a/examples/cdp_mode/playwright/raw_basic_sync.py b/examples/cdp_mode/playwright/raw_basic_sync.py new file mode 100644 index 00000000000..d5494d2dfb2 --- /dev/null +++ b/examples/cdp_mode/playwright/raw_basic_sync.py @@ -0,0 +1,15 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://seleniumbase.io/simple/login") + page.fill("#username", "demo_user") + page.fill("#password", "secret_pass") + page.click("#log-in") + page.wait_for_selector("h1") + page.wait_for_timeout(1000) diff --git a/examples/cdp_mode/playwright/raw_bing_cap_async.py b/examples/cdp_mode/playwright/raw_bing_cap_async.py new file mode 100644 index 00000000000..9231017cfa3 --- /dev/null +++ b/examples/cdp_mode/playwright/raw_bing_cap_async.py @@ -0,0 +1,20 @@ +import asyncio +from playwright.async_api import async_playwright +from seleniumbase import cdp_driver + + +async def main(): + driver = await cdp_driver.start_async(locale="en") + endpoint_url = driver.get_endpoint_url() + + async with async_playwright() as p: + browser = await p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + await page.goto("https://www.bing.com/turing/captcha/challenge") + await page.wait_for_timeout(2000) + await driver.solve_captcha() + await page.wait_for_timeout(2000) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/cdp_mode/playwright/raw_bing_cap_nested.py b/examples/cdp_mode/playwright/raw_bing_cap_nested.py new file mode 100644 index 00000000000..6f752a06ccb --- /dev/null +++ b/examples/cdp_mode/playwright/raw_bing_cap_nested.py @@ -0,0 +1,14 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import SB + +with SB(uc=True, locale="en") as sb: + sb.activate_cdp_mode() + endpoint_url = sb.get_endpoint_url() + + with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://www.bing.com/turing/captcha/challenge") + page.wait_for_timeout(2000) + sb.solve_captcha() + page.wait_for_timeout(2000) diff --git a/examples/cdp_mode/playwright/raw_bing_cap_sync.py b/examples/cdp_mode/playwright/raw_bing_cap_sync.py new file mode 100644 index 00000000000..4f456f85450 --- /dev/null +++ b/examples/cdp_mode/playwright/raw_bing_cap_sync.py @@ -0,0 +1,13 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(locale="en") +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://www.bing.com/turing/captcha/challenge") + page.wait_for_timeout(2000) + sb.solve_captcha() + page.wait_for_timeout(2000) diff --git a/examples/cdp_mode/playwright/raw_browserscan_nested.py b/examples/cdp_mode/playwright/raw_browserscan_nested.py new file mode 100644 index 00000000000..58ee5e53202 --- /dev/null +++ b/examples/cdp_mode/playwright/raw_browserscan_nested.py @@ -0,0 +1,16 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import SB + +with SB(uc=True, locale="en", ad_block=True) as sb: + sb.activate_cdp_mode() + endpoint_url = sb.get_endpoint_url() + + with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://browserscan.net/bot-detection") + page.wait_for_timeout(500) + sb.flash("Test Results", duration=1.5, pause=0.5) + sb.assert_element('strong:contains("Normal")') + print("Bot Not Detected") + sb.flash('strong:contains("Normal")', pause=1) diff --git a/examples/cdp_mode/playwright/raw_browserscan_sync.py b/examples/cdp_mode/playwright/raw_browserscan_sync.py new file mode 100644 index 00000000000..8cc6fbb5ed2 --- /dev/null +++ b/examples/cdp_mode/playwright/raw_browserscan_sync.py @@ -0,0 +1,15 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(locale="en", ad_block=True) +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://browserscan.net/bot-detection") + page.wait_for_timeout(500) + sb.flash("Test Results", duration=1.5, pause=0.5) + sb.assert_element('strong:contains("Normal")') + print("Bot Not Detected") + sb.flash('strong:contains("Normal")', pause=1) diff --git a/examples/cdp_mode/playwright/raw_cf_cap_sync.py b/examples/cdp_mode/playwright/raw_cf_cap_sync.py new file mode 100644 index 00000000000..95f92f17f90 --- /dev/null +++ b/examples/cdp_mode/playwright/raw_cf_cap_sync.py @@ -0,0 +1,13 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(locale="en") +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://wsform.com/demo/") + page.wait_for_timeout(2500) + sb.solve_captcha() + page.wait_for_timeout(2500) diff --git a/examples/cdp_mode/playwright/raw_clearcote_sync.py b/examples/cdp_mode/playwright/raw_clearcote_sync.py new file mode 100644 index 00000000000..cf320e985f3 --- /dev/null +++ b/examples/cdp_mode/playwright/raw_clearcote_sync.py @@ -0,0 +1,28 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://www.clearcotelabs.com/audit") + sb.click('button[aria-label="Marker 1"]') + sb.click('button[aria-label="Marker 2"]') + sb.click('button[aria-label="Marker 3"]') + sb.press_keys("input", "audit") + sb.set_value('input[type="range"]', "100") + sb.click_and_hold("button[data-interaction-hold]") + sb.select_option_by_text("select", "Mouse") + sb.click('button:contains("Run the audit")') + sb.sleep(6) + sb.assert_element("div.text-successText", timeout=10) + try: + sb.assert_text("100", "div.text-successText", timeout=5) + sb.highlight('div.text-successText:contains("100")') + print(" โœ… The browser fingerprint is clean! Score: 100") + except Exception: + score = sb.get_text("div.text-successText") + print(f" โŒ Fingerprint tampering detected! Score: {score}") + sb.sleep(1) diff --git a/examples/cdp_mode/playwright/raw_etsy_sync.py b/examples/cdp_mode/playwright/raw_etsy_sync.py new file mode 100644 index 00000000000..81782f7fe20 --- /dev/null +++ b/examples/cdp_mode/playwright/raw_etsy_sync.py @@ -0,0 +1,33 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(ad_block=True) +sb.goto("https://www.etsy.com/") +sb.sleep(1) +search = "FIFA Keychains" +required_text = "Keychain" +sb.type('input[data-id="search-query"]', search) +sb.sleep(1) +sb.click('button[aria-label="Search"]') +sb.sleep(2) +sb.click_if_visible('button[aria-label="Ok"]') +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + items = page.locator("div.v2-listing-card__info") + num = 0 + print('*** Etsy search for "%s":' % search) + print(' (Results must contain "%s")' % required_text) + for i in range(items.count()): + title = items.nth(i).locator("h3.v2-listing-card__title").inner_text() + price = items.nth(i).locator("div.n-listing-card__price").inner_text() + if required_text.lower() in title.lower(): + num += 1 + title = " ".join(title.split()).strip() + price = price.replace("Sale Price", "").strip().split("\n")[0] + print(f"* <====== {num} ======>") + print(title) + print(price.strip().split("\n")[0]) + print(f"*** {num} total items found!") diff --git a/examples/cdp_mode/playwright/raw_fingerprint_nested.py b/examples/cdp_mode/playwright/raw_fingerprint_nested.py new file mode 100644 index 00000000000..391ff7ed6e2 --- /dev/null +++ b/examples/cdp_mode/playwright/raw_fingerprint_nested.py @@ -0,0 +1,17 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import SB + +with SB(uc=True, locale="en") as sb: + sb.activate_cdp_mode() + endpoint_url = sb.get_endpoint_url() + + with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://demo.fingerprint.com/playground") + page.wait_for_timeout(500) + sb.flash('a[href*="browser-bot-detection"]', duration=3, pause=1) + bot_row_selector = 'table:contains("Bot") tr:nth-of-type(3)' + print(sb.get_text(bot_row_selector)) + sb.assert_text("Bot Not detected", bot_row_selector) + sb.flash(bot_row_selector, duration=3, pause=2) diff --git a/examples/cdp_mode/playwright/raw_fingerprint_sync.py b/examples/cdp_mode/playwright/raw_fingerprint_sync.py new file mode 100644 index 00000000000..9f22a811016 --- /dev/null +++ b/examples/cdp_mode/playwright/raw_fingerprint_sync.py @@ -0,0 +1,15 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(locale="en") +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://demo.fingerprint.com/playground") + page.wait_for_timeout(500) + sb.flash('a[href*="browser-bot-detection"]', duration=3, pause=1) + bot_row_selector = 'table:contains("Bot") tr:nth-of-type(3)' + print(sb.get_text(bot_row_selector)) + sb.flash(bot_row_selector, duration=3, pause=2) diff --git a/examples/cdp_mode/playwright/raw_footlocker_sync.py b/examples/cdp_mode/playwright/raw_footlocker_sync.py new file mode 100644 index 00000000000..28e35dd6797 --- /dev/null +++ b/examples/cdp_mode/playwright/raw_footlocker_sync.py @@ -0,0 +1,29 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(locale="en", ad_block=True) +sb.goto("https://www.footlocker.com/") +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + input_field = 'input[name="query"]' + page.wait_for_selector(input_field) + sb.sleep(1.5) + sb.click_if_visible('button[id*="Agree"]') + sb.sleep(0.6) + page.click(input_field) + sb.sleep(0.6) + search = "Nike Shoes" + page.type(input_field, search) + sb.sleep(1.2) + page.click('ul[id*="typeahead"] li div') + sb.sleep(3.5) + elements = sb.select_all("a.ProductCard-link") + if elements: + print('**** Found results for "%s": ****' % search) + for element in elements: + print("------------------ >>>") + print("* " + element.text) + sb.sleep(2) diff --git a/examples/cdp_mode/playwright/raw_gas_info_async.py b/examples/cdp_mode/playwright/raw_gas_info_async.py new file mode 100644 index 00000000000..aa91fad817d --- /dev/null +++ b/examples/cdp_mode/playwright/raw_gas_info_async.py @@ -0,0 +1,39 @@ +import asyncio +from playwright.async_api import async_playwright +from seleniumbase import cdp_driver + + +async def main(): + driver = await cdp_driver.start_async() + endpoint_url = driver.get_endpoint_url() + tab = await driver.get("about:blank") + + async with async_playwright() as p: + browser = await p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + url = ( + "https://www.gassaferegister.co.uk/gas-safety" + "/gas-safety-certificates-records/building-regulations-certificate" + "/order-replacement-building-regulations-certificate/" + ) + await page.goto(url) + await page.wait_for_timeout(600) + await tab.solve_captcha() + await page.wait_for_selector("#SearchTerm") + await page.wait_for_timeout(2000) + allow_cookies = 'button:contains("Allow all cookies")' + await tab.click_if_visible(allow_cookies, timeout=3) + await page.wait_for_timeout(1000) + await page.fill("#SearchTerm", "Hydrogen") + await tab.click_if_visible(allow_cookies, timeout=1) + await page.click("button.search-button") + await page.wait_for_timeout(3000) + results = await tab.query_selector_all("div.search-result") + for result in results: + print(result.text.replace(" " * 12, " ").strip() + "\n") + await tab.scroll_down(50) + await page.wait_for_timeout(1000) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/cdp_mode/playwright/raw_gas_info_sync.py b/examples/cdp_mode/playwright/raw_gas_info_sync.py new file mode 100644 index 00000000000..88d4945b891 --- /dev/null +++ b/examples/cdp_mode/playwright/raw_gas_info_sync.py @@ -0,0 +1,32 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + url = ( + "https://www.gassaferegister.co.uk/gas-safety" + "/gas-safety-certificates-records/building-regulations-certificate" + "/order-replacement-building-regulations-certificate/" + ) + page.goto(url) + page.wait_for_timeout(600) + sb.solve_captcha() + page.wait_for_selector("#SearchTerm") + page.wait_for_timeout(2000) + allow_cookies = 'button:contains("Allow all cookies")' + sb.click_if_visible(allow_cookies, timeout=3) + page.wait_for_timeout(1000) + page.fill("#SearchTerm", "Hydrogen") + sb.click_if_visible(allow_cookies, timeout=1) + page.click("button.search-button") + page.wait_for_timeout(3000) + items = page.locator("div.search-result") + for i in range(items.count()): + item_text = items.nth(i).inner_text() + print(item_text.replace("\n\n", "\n") + "\n") + sb.scroll_to_bottom() + page.wait_for_timeout(3000) diff --git a/examples/cdp_mode/playwright/raw_gitlab_async.py b/examples/cdp_mode/playwright/raw_gitlab_async.py new file mode 100644 index 00000000000..cbe57d434a7 --- /dev/null +++ b/examples/cdp_mode/playwright/raw_gitlab_async.py @@ -0,0 +1,24 @@ +import asyncio +from playwright.async_api import async_playwright +from seleniumbase import cdp_driver + + +async def main(): + driver = await cdp_driver.start_async(locale="en", agent="headless") + endpoint_url = driver.get_endpoint_url() + + async with async_playwright() as p: + browser = await p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + await page.goto("https://gitlab.com/users/sign_in") + await page.wait_for_timeout(3000) + await driver.solve_captcha() + await page.wait_for_timeout(1000) + await page.locator('label[for="user_login"]').click() + await page.wait_for_selector('[data-testid="sign-in-button"]') + await page.locator("#user_login").fill("Username") + await page.wait_for_timeout(2000) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/cdp_mode/playwright/raw_gitlab_nested.py b/examples/cdp_mode/playwright/raw_gitlab_nested.py new file mode 100644 index 00000000000..03ebe32b3e4 --- /dev/null +++ b/examples/cdp_mode/playwright/raw_gitlab_nested.py @@ -0,0 +1,18 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import SB + +with SB(uc=True, locale="en") as sb: + sb.activate_cdp_mode() + endpoint_url = sb.get_endpoint_url() + + with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://gitlab.com/users/sign_in") + page.wait_for_timeout(3000) + sb.solve_captcha() + page.wait_for_timeout(1000) + page.locator('label[for="user_login"]').click() + page.wait_for_selector('[data-testid="sign-in-button"]') + page.locator("#user_login").fill("Username") + page.wait_for_timeout(2000) diff --git a/examples/cdp_mode/playwright/raw_gitlab_sync.py b/examples/cdp_mode/playwright/raw_gitlab_sync.py new file mode 100644 index 00000000000..b0d6a66684e --- /dev/null +++ b/examples/cdp_mode/playwright/raw_gitlab_sync.py @@ -0,0 +1,17 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(locale="en") +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://gitlab.com/users/sign_in") + page.wait_for_timeout(3000) + sb.solve_captcha() + page.wait_for_timeout(1000) + page.locator('label[for="user_login"]').click() + page.wait_for_selector('[data-testid="sign-in-button"]') + page.locator("#user_login").fill("Username") + page.wait_for_timeout(2000) diff --git a/examples/cdp_mode/playwright/raw_google_sync.py b/examples/cdp_mode/playwright/raw_google_sync.py new file mode 100644 index 00000000000..c463a0ce27c --- /dev/null +++ b/examples/cdp_mode/playwright/raw_google_sync.py @@ -0,0 +1,17 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://google.com/ncr") + sb.click_if_visible('button:contains("Accept all")') + page.type('[name="q"]', "SeleniumBase GitHub page") + sb.click('[value="Google Search"]') + sb.sleep(4) # The "AI Overview" sometimes loads + print(page.title()) + sb.save_as_pdf("google_page.pdf", folder="./downloaded_files/") + print("PDF saved to ./downloaded_files/google_page.pdf") diff --git a/examples/cdp_mode/playwright/raw_idealista_nested.py b/examples/cdp_mode/playwright/raw_idealista_nested.py new file mode 100644 index 00000000000..3ddbdcb2d57 --- /dev/null +++ b/examples/cdp_mode/playwright/raw_idealista_nested.py @@ -0,0 +1,24 @@ +"""(Bypasses the DataDome slider CAPTCHA)""" +from playwright.sync_api import sync_playwright +from seleniumbase import SB + +with SB(uc=True, locale="es") as sb: + url = "https://www.idealista.com/venta-viviendas/barcelona-provincia/" + sb.activate_cdp_mode(url) + sb.sleep(1) + sb.solve_captcha() + sb.sleep(2) + endpoint_url = sb.get_endpoint_url() + + with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.click("button#didomi-notice-agree-button") + page.wait_for_timeout(1000) + print("*** " + page.locator("h1").inner_text()) + items = page.locator("div.item-info-container") + for i in range(items.count()): + item = items.nth(i) + print(item.locator("a.item-link").text_content().strip()) + print(item.locator("span.item-price").text_content().strip()) + item_text = items.nth(i) diff --git a/examples/cdp_mode/playwright/raw_indeed_sync.py b/examples/cdp_mode/playwright/raw_indeed_sync.py new file mode 100644 index 00000000000..87c1d0ccf9f --- /dev/null +++ b/examples/cdp_mode/playwright/raw_indeed_sync.py @@ -0,0 +1,35 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +sb.goto("https://www.indeed.com/companies/search") +sb.sleep(2) +sb.solve_captcha() +sb.sleep(1) +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + search_box = "input#company-search" + if page.locator(search_box).count() == 0: + page.wait_for_timeout(2500) + sb.solve_captcha() + page.wait_for_timeout(1000) + company = "NASA Jet Propulsion Laboratory" + page.click(search_box) + page.fill(search_box, company) + page.click('button[type="submit"]') + page.click('a:has-text("%s")' % company) + name_header = 'div[itemprop="name"]' + page.wait_for_timeout(1000) + if page.locator(name_header).count() == 0: + page.wait_for_timeout(2500) + sb.solve_captcha() + page.wait_for_timeout(1000) + for i in range(10): + sb.scroll_down(12) + sb.sleep(0.14) + info = page.locator('[data-testid="AboutSection-section"]') + soup = sb.get_beautiful_soup(info.inner_html()).get_text("\n") + print("*** %s: ***\n%s" % (company, soup)) diff --git a/examples/cdp_mode/playwright/raw_nike_sync.py b/examples/cdp_mode/playwright/raw_nike_sync.py new file mode 100644 index 00000000000..fdfcfbbecbd --- /dev/null +++ b/examples/cdp_mode/playwright/raw_nike_sync.py @@ -0,0 +1,21 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://www.nike.com/") + page.click('[data-testid="user-tools-container"] search') + search = "Pegasus" + page.fill('input[type="search"]', search) + page.wait_for_timeout(4000) + details = 'ul[data-testid*="products"] figure .details' + items = page.locator(details) + if items: + print('**** Found results for "%s": ****' % search) + for i in range(items.count()): + item = items.nth(i) + print(item.inner_text()) diff --git a/examples/cdp_mode/playwright/raw_nordstrom_sync.py b/examples/cdp_mode/playwright/raw_nordstrom_sync.py new file mode 100644 index 00000000000..16ae7fced6e --- /dev/null +++ b/examples/cdp_mode/playwright/raw_nordstrom_sync.py @@ -0,0 +1,30 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(locale="en") +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://www.nordstrom.com/") + page.wait_for_timeout(2000) + page.click("input#keyword-search-input") + page.wait_for_timeout(800) + search = "cocktail dresses for women teal" + search_box = page.locator("input#keyword-search-input") + search_box.press_sequentially(search + "\n", delay=80) + page.wait_for_timeout(2200) + print('*** Nordstrom Search for "%s":' % search) + unique_item_text = [] + items = sb.find_elements("article") + for item in items: + description = item.query_selector("article h3") + if description and description.text not in unique_item_text: + unique_item_text.append(description.text) + price_text = "" + price = item.query_selector('div div span[aria-hidden="true"]') + if price: + price_text = price.text + print("* %s (%s)" % (description.text, price_text)) + item.flash(color="44CC88") diff --git a/examples/cdp_mode/playwright/raw_pixelscan_async.py b/examples/cdp_mode/playwright/raw_pixelscan_async.py new file mode 100644 index 00000000000..ed87506fec4 --- /dev/null +++ b/examples/cdp_mode/playwright/raw_pixelscan_async.py @@ -0,0 +1,29 @@ +import asyncio +import time +from playwright.async_api import async_playwright +from playwright.async_api import expect +from seleniumbase import cdp_driver + + +async def main(): + driver = await cdp_driver.start_async() + endpoint_url = driver.get_endpoint_url() + + async with async_playwright() as p: + browser = await p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + await page.goto("https://pixelscan.net/fingerprint-check") + time.sleep(4) + await expect( + page.locator("pxlscn-bot-detection") + ).to_contain_text("No automated behavior", timeout=4000) + await page.wait_for_selector("span.status-success", timeout=4000) + await expect( + page.locator("pxlscn-fingerprint-masking") + ).to_contain_text("No masking detected", timeout=4000) + time.sleep(2) + print("Bot Not Detected") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/cdp_mode/playwright/raw_pixelscan_sync.py b/examples/cdp_mode/playwright/raw_pixelscan_sync.py new file mode 100644 index 00000000000..c4c93b83ecd --- /dev/null +++ b/examples/cdp_mode/playwright/raw_pixelscan_sync.py @@ -0,0 +1,21 @@ +from playwright.sync_api import sync_playwright +from playwright.sync_api import expect +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://pixelscan.net/fingerprint-check") + sb.sleep(4) + expect( + page.locator("pxlscn-bot-detection") + ).to_contain_text("No automated behavior", timeout=4000) + page.wait_for_selector("span.status-success", timeout=4000) + expect( + page.locator("pxlscn-fingerprint-masking") + ).to_contain_text("No masking detected", timeout=4000) + sb.sleep(2) + print("Bot Not Detected") diff --git a/examples/cdp_mode/playwright/raw_planetmc_sync.py b/examples/cdp_mode/playwright/raw_planetmc_sync.py new file mode 100644 index 00000000000..ba37b5eb1ed --- /dev/null +++ b/examples/cdp_mode/playwright/raw_planetmc_sync.py @@ -0,0 +1,15 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://www.planetminecraft.com/account/sign_in/") + page.wait_for_timeout(2000) + sb.solve_captcha() + input_disabled = page.locator("input[disabled]") + input_disabled.wait_for(state="hidden", timeout=5000) + page.wait_for_timeout(2000) diff --git a/examples/cdp_mode/playwright/raw_reddit_sync.py b/examples/cdp_mode/playwright/raw_reddit_sync.py new file mode 100644 index 00000000000..4d614e1025e --- /dev/null +++ b/examples/cdp_mode/playwright/raw_reddit_sync.py @@ -0,0 +1,24 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(use_chromium=True) +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + search = "reddit+scraper" + url = f"https://www.reddit.com/r/webscraping/search/?q={search}" + sb.goto(url) + sb.solve_captcha() # Might not be needed + page.wait_for_timeout(1000) + post_title = '[data-testid="post-title"]' + page.wait_for_selector(post_title) + for i in range(8): + sb.scroll_down(25) + sb.sleep(0.2) + print('*** Reddit Posts for "%s":' % search) + items = page.locator(post_title) + for i in range(items.count()): + item_text = items.nth(i).inner_text() + print("* " + item_text) diff --git a/examples/cdp_mode/playwright/raw_sannysoft_nested.py b/examples/cdp_mode/playwright/raw_sannysoft_nested.py new file mode 100644 index 00000000000..0af95b21721 --- /dev/null +++ b/examples/cdp_mode/playwright/raw_sannysoft_nested.py @@ -0,0 +1,25 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import SB + +with SB(uc=True, guest=True) as sb: + sb.activate_cdp_mode() + endpoint_url = sb.get_endpoint_url() + + with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://bot.sannysoft.com/") + page.wait_for_timeout(500) + sb.flash("#user-agent-result.passed") + sb.flash("#webdriver-result.passed") + sb.flash("#advanced-webdriver-result.passed") + sb.flash("#chrome-result.passed") + sb.flash("#permissions-result.passed") + sb.flash("#plugins-length-result.passed") + sb.flash("#plugins-type-result.passed") + sb.flash("#languages-result.passed") + sb.flash("#webgl-vendor.passed") + sb.flash("#webgl-renderer.passed") + sb.flash("#broken-image-dimensions.passed") + print("Bot Not Detected") + sb.sleep(1) diff --git a/examples/cdp_mode/playwright/raw_sannysoft_sync.py b/examples/cdp_mode/playwright/raw_sannysoft_sync.py new file mode 100644 index 00000000000..34a894b8c8f --- /dev/null +++ b/examples/cdp_mode/playwright/raw_sannysoft_sync.py @@ -0,0 +1,24 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(guest=True) +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://bot.sannysoft.com/") + page.wait_for_timeout(500) + sb.flash("#user-agent-result.passed") + sb.flash("#webdriver-result.passed") + sb.flash("#advanced-webdriver-result.passed") + sb.flash("#chrome-result.passed") + sb.flash("#permissions-result.passed") + sb.flash("#plugins-length-result.passed") + sb.flash("#plugins-type-result.passed") + sb.flash("#languages-result.passed") + sb.flash("#webgl-vendor.passed") + sb.flash("#webgl-renderer.passed") + sb.flash("#broken-image-dimensions.passed") + print("Bot Not Detected") + sb.sleep(1) diff --git a/examples/cdp_mode/playwright/raw_seatgeek_sync.py b/examples/cdp_mode/playwright/raw_seatgeek_sync.py new file mode 100644 index 00000000000..3de2cf96af8 --- /dev/null +++ b/examples/cdp_mode/playwright/raw_seatgeek_sync.py @@ -0,0 +1,24 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(locale="en", ad_block=True) +sb.goto("https://seatgeek.com/") +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + input_field = 'input[name="search"]' + page.wait_for_selector(input_field) + page.wait_for_timeout(1600) + query = "Jerry Seinfeld" + search_box = page.locator(input_field) + search_box.press_sequentially(query, delay=80) + page.wait_for_timeout(1600) + page.click("li#active-result-item") + page.wait_for_timeout(4200) + print('*** SeatGeek Search for "%s":' % query) + items = page.locator('[data-testid="listing-item"]') + for i in range(items.count()): + item_text = items.nth(i).inner_text() + print(item_text.replace("\n\n", "\n")) diff --git a/examples/cdp_mode/playwright/raw_walmart_sync.py b/examples/cdp_mode/playwright/raw_walmart_sync.py new file mode 100644 index 00000000000..7fb2840ed28 --- /dev/null +++ b/examples/cdp_mode/playwright/raw_walmart_sync.py @@ -0,0 +1,50 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(locale="en", guest=True) +sb.goto("https://www.walmart.com/") +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + search_box = 'input[aria-label="Search"]' + search = "Settlers of Catan Board Game" + required_text = "Catan" + page.wait_for_timeout(1800) + sb.click(search_box) + page.wait_for_timeout(1200) + sb.press_keys(search_box, search + "\n") + sb.sleep(3.8) + sb.remove_elements('[data-testid="skyline-ad"]') + sb.remove_elements('[data-testid="sba-container"]') + print('*** Walmart Search for "%s":' % search) + print(' (Results must contain "%s".)' % required_text) + unique_item = [] + pop_up = '[data-automation-id="sb-btn-close-mark"]' + if page.locator(pop_up).count() > 0: + page.click(pop_up) + page.wait_for_timeout(1200) + page.wait_for_selector('[data-item-id]', timeout=10000) + page.wait_for_timeout(600) + for i in range(10): + sb.scroll_down(16) + items = page.locator('[data-item-id]') + for i in range(items.count()): + item = items.nth(i) + if required_text.lower() in item.inner_text().lower(): + description = item.locator('[data-automation-id="product-title"]') + if ( + description + and description.is_visible() + and description.inner_text() not in unique_item + ): + unique_item.append(description.inner_text()) + price = item.locator('[data-automation-id="product-price"]') + if price.count() > 0: + print("* " + description.inner_text()) + price_text = price.inner_text() + price_text = price_text.split("current price Now ")[-1] + price_text = price_text.split("current price ")[-1] + price_text = price_text.split(" ")[0] + print(" (" + price_text + ")") diff --git a/examples/cdp_mode/playwright/raw_yc_news_async.py b/examples/cdp_mode/playwright/raw_yc_news_async.py new file mode 100644 index 00000000000..331f16438db --- /dev/null +++ b/examples/cdp_mode/playwright/raw_yc_news_async.py @@ -0,0 +1,22 @@ +import asyncio +from playwright.async_api import async_playwright +from seleniumbase import cdp_driver + + +async def main(): + driver = await cdp_driver.start_async() + endpoint_url = driver.get_endpoint_url() + + async with async_playwright() as p: + browser = await p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + url = "https://news.ycombinator.com/submitted?id=seleniumbase" + await page.goto(url) + items = page.locator("span.titleline > a") + for i in range(await (items.count())): + item_text = await (items.nth(i)).inner_text() + print("* " + item_text) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/cdp_mode/playwright/raw_yc_news_sync.py b/examples/cdp_mode/playwright/raw_yc_news_sync.py new file mode 100644 index 00000000000..f6364ce83b8 --- /dev/null +++ b/examples/cdp_mode/playwright/raw_yc_news_sync.py @@ -0,0 +1,14 @@ +from playwright.sync_api import sync_playwright +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +endpoint_url = sb.get_endpoint_url() + +with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] + page.goto("https://news.ycombinator.com/submitted?id=seleniumbase") + items = page.locator("span.titleline > a") + for i in range(items.count()): + item_text = items.nth(i).inner_text() + print("* " + item_text) diff --git a/examples/cdp_mode/raw_ad_blocking.py b/examples/cdp_mode/raw_ad_blocking.py new file mode 100644 index 00000000000..ab1016a9d6b --- /dev/null +++ b/examples/cdp_mode/raw_ad_blocking.py @@ -0,0 +1,41 @@ +"""Test Ad-Blocking using mycdp.network.set_blocked_urls(). +tab.send() is the stealthy version of execute_cdp_cmd().""" +import mycdp +from seleniumbase import decorators +from seleniumbase import sb_cdp + + +async def block_urls(tab): + await tab.send(mycdp.network.enable()) + await tab.send(mycdp.network.set_blocked_urls( + urls=[ + "*.googlesyndication.com*", + "*.googletagmanager.com*", + "*.google-analytics.com*", + "*.amazon-adsystem.com*", + "*.adsafeprotected.com*", + "*.doubleclick.net*", + "*.fastclick.net*", + "*.snigelweb.com*", + "*.viously.com*", + "*.flashb.id*", + "*.2mdn.net*", + ] + )) + +with decorators.print_runtime("raw_ad_blocking.py"): + sb = sb_cdp.Chrome() + loop = sb.get_event_loop() + loop.run_until_complete(block_urls(sb.get_active_tab())) + sb.goto("https://www.w3schools.com/jquery/default.asp") + source = sb.get_page_source() + try: + sb.assert_false("doubleclick.net" in source) + sb.assert_false("google-analytics.com" in source) + except Exception: + sb.refresh() + source = sb.get_page_source() + sb.assert_false("doubleclick.net" in source) + sb.assert_false("google-analytics.com" in source) + sb.post_message("Blocking was successful!") + sb.quit() diff --git a/examples/cdp_mode/raw_ahrefs.py b/examples/cdp_mode/raw_ahrefs.py new file mode 100644 index 00000000000..3e61418e879 --- /dev/null +++ b/examples/cdp_mode/raw_ahrefs.py @@ -0,0 +1,18 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, incognito=True, locale="en") as sb: + sb.activate_cdp_mode() + sb.goto("https://ahrefs.com/website-authority-checker") + search_term = "github.com/seleniumbase/SeleniumBase" + sb.type('input[placeholder="Enter domain"]', search_term) + sb.scroll_down(36) + sb.click('span:contains("Check Authority")') + sb.sleep(2) + sb.solve_captcha() + sb.sleep(3) + sb.wait_for_text_not_visible("Checking", timeout=15) + sb.click_if_visible('button[data-cky-tag="close-button"]') + sb.highlight('p:contains("github.com/seleniumbase/SeleniumBase")') + sb.highlight('a:contains("Top 100 backlinks")') + sb.set_messenger_theme(location="bottom_center") + sb.post_message("SeleniumBase wasn't detected!") diff --git a/examples/cdp_mode/raw_albertsons.py b/examples/cdp_mode/raw_albertsons.py new file mode 100644 index 00000000000..f3ff09223a3 --- /dev/null +++ b/examples/cdp_mode/raw_albertsons.py @@ -0,0 +1,33 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en") as sb: + sb.activate_cdp_mode() + sb.goto("https://www.albertsons.com/recipes/") + sb.sleep(2.5) + sb.remove_element("div > div > article") + sb.scroll_into_view('input[type="search"]') + close_btn = ".notification-alert-wrapper__close-button" + sb.click_if_visible(close_btn) + sb.click("input#search-suggestion-input") + sb.sleep(0.2) + search = "Avocado Smoked Salmon" + required_text = "Salmon" + sb.press_keys("input#search-suggestion-input", search) + sb.sleep(0.8) + sb.click("#suggestion-0 a span") + sb.sleep(0.8) + sb.click_if_visible(close_btn) + sb.sleep(3.2) + print('*** Albertsons Search for "%s":' % search) + print(' (Results must contain "%s".)' % required_text) + unique_item_text = [] + item_selector = 'a[href*="/meal-plans-recipes/shop/"]' + items = sb.find_elements(item_selector) + for item in items: + sb.sleep(0.06) + if required_text in item.text: + item.flash(color="44CC88") + sb.sleep(0.025) + if item.text not in unique_item_text: + unique_item_text.append(item.text) + print("* " + item.text) diff --git a/examples/cdp_mode/raw_amazon.py b/examples/cdp_mode/raw_amazon.py new file mode 100644 index 00000000000..8281f2fe9ab --- /dev/null +++ b/examples/cdp_mode/raw_amazon.py @@ -0,0 +1,17 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, ad_block=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.amazon.com") + sb.sleep(2) + sb.click_if_visible('button[alt="Continue shopping"]') + sb.sleep(2) + sb.press_keys('input[role="searchbox"]', "TI-89\n") + sb.sleep(3) + for i in range(10): + sb.scroll_down(26) + print(sb.get_page_title()) + sb.save_as_pdf_to_logs() + sb.save_page_source_to_logs() + sb.save_screenshot_to_logs() + print("Logs have been saved to: ./latest_logs/") diff --git a/examples/cdp_mode/raw_antibot.py b/examples/cdp_mode/raw_antibot.py new file mode 100644 index 00000000000..788d1e53517 --- /dev/null +++ b/examples/cdp_mode/raw_antibot.py @@ -0,0 +1,15 @@ +"""CDP Mode bypasses bot-detection and performs stealthy actions. +sb.press_keys() is a slower sb.type() for human-like speed.""" +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://seleniumbase.io/antibot/login") + sb.press_keys("input#username", "demo_user") + sb.type("input#password", "secret_pass") + sb.click("button#myButton") + sb.sleep(1.4) + sb.click("a#log-in") + sb.assert_text("Welcome!", "h1") + sb.set_messenger_theme(location="bottom_center") + sb.post_message("SeleniumBase wasn't detected!") diff --git a/examples/cdp_mode/raw_async.py b/examples/cdp_mode/raw_async.py new file mode 100644 index 00000000000..58ecb833099 --- /dev/null +++ b/examples/cdp_mode/raw_async.py @@ -0,0 +1,71 @@ +import asyncio +import time +from contextlib import suppress +from seleniumbase import sb_cdp +from seleniumbase import cdp_driver + + +async def main(): + url = "https://seleniumbase.io/simple/login" + driver = await cdp_driver.start_async() + page = await driver.get(url, lang="en") + print(await page.get_title()) + await page.type("#username", "demo_user") + await page.type("#password", "secret_pass") + await page.click("#log-in") + print(await page.get_title()) + element = await page.select("h1") + assert element.text == "Welcome!" + top_nav = await page.select("div.topnav") + links = await top_nav.query_selector_all_async("a") + for nav_item in links: + print(nav_item.text) + driver.quit() + +if __name__ == "__main__": + # Call an async function with awaited methods + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + + # An example of wrapping all async calls with event loops + driver = cdp_driver.start_sync() + page = loop.run_until_complete(driver.get("about:blank")) + loop.run_until_complete(page.set_locale("en")) + loop.run_until_complete(page.get("https://www.pokemon.com/us")) + time.sleep(3) + print(loop.run_until_complete(page.evaluate("document.title"))) + with suppress(Exception): + selector = "button#onetrust-reject-all-handler" + element = loop.run_until_complete(page.select(selector, timeout=1)) + loop.run_until_complete(element.click_async()) + time.sleep(1) + element = loop.run_until_complete(page.select("span.icon_pokeball")) + loop.run_until_complete(element.click_async()) + time.sleep(2) + print(loop.run_until_complete(page.evaluate("document.title"))) + time.sleep(1) + driver.stop() + + # Call CDP methods via the simplified SB CDP API + sb = sb_cdp.Chrome("https://www.priceline.com/") + sb.sleep(2.5) + sb.internalize_links() # Don't open links in a new tab + sb.click("#link_header_nav_experiences") + sb.sleep(3.5) + sb.remove_elements("msm-cookie-banner") + sb.sleep(1.5) + location = "Amsterdam" + where_to = 'div[data-automation*="experiences"] input' + button = 'button[data-automation*="experiences-search"]' + sb.wait_for_text("Where to?") + sb.click(where_to) + sb.press_keys(where_to, location) + sb.sleep(1) + sb.click(button) + sb.sleep(3) + print(sb.get_title()) + print("************") + cards = sb.select_all('span[data-automation*="product-list-card"]') + for card in cards: + print("* %s" % card.text) + sb.quit() diff --git a/examples/cdp_mode/raw_basic_async.py b/examples/cdp_mode/raw_basic_async.py new file mode 100644 index 00000000000..876a00846b7 --- /dev/null +++ b/examples/cdp_mode/raw_basic_async.py @@ -0,0 +1,25 @@ +import asyncio +from seleniumbase import cdp_driver +from seleniumbase import decorators + + +async def main(): + url = "https://seleniumbase.io/simple/login" + driver = await cdp_driver.start_async() + page = await driver.get(url, lang="en") + print(await page.get_title()) + await page.type("#username", "demo_user") + await page.type("#password", "secret_pass") + await page.click("#log-in") + print(await page.get_title()) + element = await page.select("h1") + assert element.text == "Welcome!" + top_nav = await page.select("div.topnav") + links = await top_nav.query_selector_all_async("a") + for nav_item in links: + print(nav_item.text) + driver.quit() + +if __name__ == "__main__": + with decorators.print_runtime("raw_basic_async.py"): + asyncio.run(main()) diff --git a/examples/cdp_mode/raw_basic_cdp.py b/examples/cdp_mode/raw_basic_cdp.py new file mode 100644 index 00000000000..2ca49fa6dbb --- /dev/null +++ b/examples/cdp_mode/raw_basic_cdp.py @@ -0,0 +1,22 @@ +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +sb.goto("https://seleniumbase.io/simple/login") +sb.type("#username", "demo_user") +sb.type("#password", "secret_pass") +sb.click('a:contains("Sign in")') +sb.assert_exact_text("Welcome!", "h1") +sb.assert_element("img#image1") +sb.highlight("#image1") +top_nav = sb.find_element("div.topnav") +links = top_nav.query_selector_all("a") +for nav_item in links: + print(nav_item.text) +sb.save_as_pdf_to_logs() +sb.save_screenshot_to_logs() +sb.save_page_source_to_logs() +sb.save_data_to_logs("hello!") +sb.append_data_to_logs("more data!") +sb.click_link("Sign out") +sb.assert_text("signed out", "#top_message") +sb.quit() diff --git a/examples/cdp_mode/raw_basic_mobile.py b/examples/cdp_mode/raw_basic_mobile.py new file mode 100644 index 00000000000..559f1882e01 --- /dev/null +++ b/examples/cdp_mode/raw_basic_mobile.py @@ -0,0 +1,13 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, mobile=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://gitlab.com/users/sign_in") + sb.sleep(2) + sb.solve_captcha() + # (The rest is for testing and demo purposes) + sb.assert_text("Username", '[for="user_login"]', timeout=3) + sb.assert_element('label[for="user_login"]') + sb.highlight('button:contains("Sign in")') + sb.highlight('h1:contains("GitLab")') + sb.post_message("SeleniumBase wasn't detected", duration=4) diff --git a/examples/cdp_mode/raw_bestwestern.py b/examples/cdp_mode/raw_bestwestern.py new file mode 100644 index 00000000000..3e460c3872c --- /dev/null +++ b/examples/cdp_mode/raw_bestwestern.py @@ -0,0 +1,33 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en", guest=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.bestwestern.com/en_US.html") + sb.sleep(3) + sb.click_if_visible(".onetrust-close-btn-handler") + sb.sleep(1) + sb.click("input#destination-input") + sb.sleep(2) + location = "Palm Springs, CA, USA" + sb.press_keys("input#destination-input", location) + sb.sleep(1) + sb.click("ul#google-suggestions li") + sb.sleep(1) + sb.click("button#btn-modify-stay-update") + sb.sleep(4) + sb.click("label#available-label") + sb.sleep(2.5) + print("Best Western Hotels in %s:" % location) + summary_details = sb.get_text("#summary-details-column") + dates = summary_details.split("DESTINATION")[-1] + dates = dates.split(" CHECK-OUT")[0].strip() + " CHECK-OUT" + dates = dates.replace(" ", " ") + print("(Dates: %s)" % dates) + flip_cards = sb.select_all(".flipCard") + for i, flip_card in enumerate(flip_cards): + hotel = flip_card.query_selector(".hotelName") + price = flip_card.query_selector(".priceSection") + if hotel and price: + print("* %s: %s => %s" % ( + i + 1, hotel.text.strip(), price.text.strip()) + ) diff --git a/examples/cdp_mode/raw_browserscan.py b/examples/cdp_mode/raw_browserscan.py new file mode 100644 index 00000000000..26835b5b043 --- /dev/null +++ b/examples/cdp_mode/raw_browserscan.py @@ -0,0 +1,9 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en", ad_block=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://browserscan.net/bot-detection") + sb.flash("Test Results", duration=1.5, pause=0.5) + sb.assert_element('strong:contains("Normal")') + print("Bot Not Detected") + sb.flash('strong:contains("Normal")', pause=1) diff --git a/examples/cdp_mode/raw_canvas.py b/examples/cdp_mode/raw_canvas.py new file mode 100644 index 00000000000..69fe7d0dcc5 --- /dev/null +++ b/examples/cdp_mode/raw_canvas.py @@ -0,0 +1,35 @@ +"""Use SeleniumBase to interact with "canvas" elements.""" +from seleniumbase import SB + + +def get_canvas_pixel_colors_at_top_left(sb): + # Return the RGB colors of the canvas's top left pixel + color = sb.evaluate( + "document.querySelector('canvas').getContext('2d')" + ".getImageData(%s,%s,1,1).data;" % (0, 0) + ) + return [color["0"], color["1"], color["2"]] + + +with SB(uc=True, test=True) as sb: + # Testing click_with_offset() + sb.activate_cdp_mode() + sb.goto("https://seleniumbase.io/canvas/") + sb.assert_title_contains("Canvas") + sb.highlight("canvas") + rgb = get_canvas_pixel_colors_at_top_left(sb) + sb.assert_equal(rgb, [221, 242, 231]) # Looks greenish + sb.click_with_offset("canvas", 500, 350) + sb.highlight("canvas", loops=5) + rgb = get_canvas_pixel_colors_at_top_left(sb) + sb.assert_equal(rgb, [39, 43, 56]) # Blue by hamburger + +with SB(uc=True, test=True) as sb: + # Testing gui_click_with_offset() + sb.activate_cdp_mode() + sb.goto("https://seleniumbase.io/other/canvas") + sb.assert_title_contains("Canvas") + sb.click_with_offset("canvas", 0, 0, center=True) + sb.sleep(1) + sb.uc_gui_press_key("ENTER") + sb.sleep(0.5) diff --git a/examples/cdp_mode/raw_cdp.py b/examples/cdp_mode/raw_cdp.py new file mode 100644 index 00000000000..19772f48293 --- /dev/null +++ b/examples/cdp_mode/raw_cdp.py @@ -0,0 +1,39 @@ +"""Example of using CDP Mode without WebDriver""" +from seleniumbase import decorators +from seleniumbase import sb_cdp + + +@decorators.print_runtime("CDP Priceline Example") +def main(): + sb = sb_cdp.Chrome(lang="en") + sb.goto("https://www.priceline.com/") + sb.sleep(2) + sb.internalize_links() # Don't open links in a new tab + sb.click("#link_header_nav_experiences") + sb.sleep(3) + sb.remove_elements("msm-cookie-banner") + sb.sleep(1) + location = "Amsterdam" + where_to = 'div[data-automation*="experiences"] input' + button = 'button[data-automation*="experiences-search"]' + sb.wait_for_text("Where to?") + sb.click(where_to) + sb.press_keys(where_to, location) + sb.sleep(1) + sb.click(button) + sb.sleep(2) + sb.click_if_visible('button[aria-label="Close"]') + sb.sleep(1) + print(sb.get_title()) + print("************") + for i in range(8): + sb.scroll_down(50) + sb.sleep(0.2) + cards = sb.select_all('span[data-automation*="product-list-card"]') + for card in cards: + print("* %s" % card.text) + sb.quit() + + +if __name__ == "__main__": + main() diff --git a/examples/cdp_mode/raw_cdp_browserscan.py b/examples/cdp_mode/raw_cdp_browserscan.py new file mode 100644 index 00000000000..b25d44fc528 --- /dev/null +++ b/examples/cdp_mode/raw_cdp_browserscan.py @@ -0,0 +1,8 @@ +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(locale="en", ad_block=True) +sb.goto("https://browserscan.net/bot-detection") +sb.flash("Test Results", duration=1.5, pause=0.5) +sb.assert_element('strong:contains("Normal")') +print("Bot Not Detected") +sb.flash('strong:contains("Normal")', pause=1) diff --git a/examples/cdp_mode/raw_cdp_clearcote.py b/examples/cdp_mode/raw_cdp_clearcote.py new file mode 100644 index 00000000000..2938c4f0d5b --- /dev/null +++ b/examples/cdp_mode/raw_cdp_clearcote.py @@ -0,0 +1,27 @@ +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +sb.goto("https://www.clearcotelabs.com/audit") +sb.click('button[aria-label="Marker 1"]') +sb.click('button[aria-label="Marker 2"]') +sb.click('button[aria-label="Marker 3"]') +sb.press_keys("input", "audit") +sb.set_value('input[type="range"]', "100") +sb.click_and_hold("button[data-interaction-hold]") +sb.select_option_by_text("select", "Mouse") +sb.click('button:contains("Run the audit")') +sb.sleep(6) +sb.assert_element("div.text-successText", timeout=10) +try: + sb.assert_text("100", "div.text-successText", timeout=5) + sb.highlight('div.text-successText:contains("100")') + print(" โœ… The browser fingerprint is clean! Score: 100") +except Exception: + score = sb.get_text("div.text-successText") + print(f" โŒ Fingerprint tampering detected! Score: {score}") +sb.sleep(1) +folder = "downloaded_files" +file_name = "c_audit_results.pdf" +sb.save_as_pdf(file_name, folder) +print('"./%s/%s" was saved!' % (folder, file_name)) +sb.quit() diff --git a/examples/cdp_mode/raw_cdp_downloads.py b/examples/cdp_mode/raw_cdp_downloads.py new file mode 100644 index 00000000000..1c0612c1012 --- /dev/null +++ b/examples/cdp_mode/raw_cdp_downloads.py @@ -0,0 +1,83 @@ +import math +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() + +""" Part 1: Using sb.download_file(file_url) """ + +sb.goto("about:blank") +words_file = "wordle_words.txt" +words_link = ( + "https://seleniumbase.github.io/cdn/txt/%s" % words_file +) +sb.download_file(words_link) +sb.assert_downloaded_file(words_file) +words_path = sb.get_path_of_downloaded_file(words_file) +with open(words_path, "r") as f: + words_data = f.read() +print("%s | Download = %s bytes." % (words_file, len(words_data))) +sb.assert_true(len(words_data) > 100) # Verify file not empty +text = '"oasis","carom","cubit"' +sb.assert_in(text, words_data) # Verify file has expected data + +""" Part 2: Using click-initiated downloads """ + +sb.goto("https://pypi.org/project/sbvirtualdisplay/#files") +sb.assert_element("span#pip-command") +sb.assert_text("Download files", "div#files h2.page-title") +sb.assert_text("Download files", "a#files-tab") +pkg_header = sb.get_text("h1.package-header__name").strip() +pkg_name = pkg_header.replace(" ", "-") +whl_file = pkg_name + "-py3-none-any.whl" +tar_gz_file = pkg_name + ".tar.gz" + +# Click the links to download the files into: "./downloaded_files/" +whl_selector = 'div#files a[href$="%s"]' % whl_file +tar_selector = 'div#files a[href$="%s"]' % tar_gz_file +sb.click(whl_selector) # Download the "whl" file +sb.sleep(0.1) +sb.click(tar_selector) # Download the "tar" file + +# Verify that the downloaded files appear in the [Downloads Folder] +# (This only guarantees that the exact file name is in the folder.) +# (This does not guarantee that the downloaded files are complete.) +# (Later, we'll check that the files were downloaded successfully.) +sb.assert_downloaded_file(whl_file) +sb.assert_downloaded_file(tar_gz_file) + +sb.sleep(1) # Add more time to make sure downloads have completed + +# Get the actual size of the downloaded files (in bytes) +whl_path = sb.get_path_of_downloaded_file(whl_file) +with open(whl_path, "rb") as f: + whl_file_bytes = len(f.read()) +print("%s | Download = %s bytes." % (whl_file, whl_file_bytes)) +tar_gz_path = sb.get_path_of_downloaded_file(tar_gz_file) +with open(tar_gz_path, "rb") as f: + tar_gz_file_bytes = len(f.read()) +print("%s | Download = %s bytes." % (tar_gz_file, tar_gz_file_bytes)) + +# Check to make sure the downloaded files are not empty or too small +sb.assert_true(whl_file_bytes > 5000) +sb.assert_true(tar_gz_file_bytes > 5000) + +# Get file sizes in kB to compare actual values with displayed values +whl_file_kb = whl_file_bytes / 1000.0 +whl_line_fi = sb.get_text('a[href$=".whl"]').strip() +whl_line = sb.get_text('div.file:contains("%s")' % whl_line_fi) +whl_display_kb = float(whl_line.split("(")[1].split(" ")[0]) +tar_gz_file_kb = tar_gz_file_bytes / 1000.0 +tar_gz_line_fi = sb.get_text('a[href$=".tar.gz"]').strip() +tar_gz_line = sb.get_text('div.file:contains("%s")' % tar_gz_line_fi) +tar_gz_display_kb = float(tar_gz_line.split("(")[1].split(" ")[0]) + +# Verify downloaded files are the correct size (account for rounding) +sb.assert_true( + abs(math.floor(whl_file_kb) - math.floor(whl_display_kb)) < 2 +) +sb.assert_true( + abs(math.floor(tar_gz_file_kb) - math.floor(tar_gz_display_kb)) < 2 +) + +# Finally quit the browser +sb.quit() diff --git a/examples/cdp_mode/raw_cdp_drivers.py b/examples/cdp_mode/raw_cdp_drivers.py new file mode 100644 index 00000000000..5f826c4d24f --- /dev/null +++ b/examples/cdp_mode/raw_cdp_drivers.py @@ -0,0 +1,27 @@ +# An example of switching between multiple drivers +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + url1 = "https://seleniumbase.io/antibot/login" + sb.activate_cdp_mode(url1) + url2 = "https://seleniumbase.io/hobbit/login" + driver2 = sb.get_new_driver(undetectable=True) + sb.activate_cdp_mode(url2) + sb.sleep(1) + sb.switch_to_default_driver() + sb.assert_url_contains("antibot") + print(sb.get_current_url()) + sb.type("input#username", "demo_user") + sb.type("input#password", "secret_pass") + sb.click("button") + sb.sleep(1) + sb.click("a#log-in") + sb.assert_text("Welcome!", "h1") + sb.sleep(2) + sb.switch_to_driver(driver2) + sb.assert_url_contains("hobbit") + print(sb.get_current_url()) + sb.click("button") + sb.assert_text("Welcome to Middle Earth!") + sb.click("img") + sb.sleep(3) diff --git a/examples/cdp_mode/raw_cdp_etsy.py b/examples/cdp_mode/raw_cdp_etsy.py new file mode 100644 index 00000000000..23302863d03 --- /dev/null +++ b/examples/cdp_mode/raw_cdp_etsy.py @@ -0,0 +1,32 @@ +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(ad_block=True) +sb.goto("https://www.etsy.com/") +sb.sleep(1) +search = "FIFA Keychains" +required_text = "Keychain" +sb.type('input[data-id="search-query"]', search) +sb.sleep(1) +sb.click('button[aria-label="Search"]') +sb.sleep(2) +sb.click_if_visible('button[aria-label="Ok"]') +sb.sleep(1) +soup = sb.get_beautiful_soup() +items = soup.select("div.v2-listing-card__info") +num = 0 +print('*** Etsy search for "%s":' % search) +print(' (Results must contain "%s")' % required_text) +for item in items: + title_element = item.select_one("h3.v2-listing-card__title") + price_element = item.select_one("div.n-listing-card__price") + if title_element and price_element: + title = title_element.get_text() + price = price_element.get_text() + if required_text.lower() in title.lower(): + num += 1 + title = " ".join(title.split()).strip() + price = price.replace("Sale Price", "").strip().split("\n")[0] + print(f"* <====== {num} ======>") + print(title) + print(price.strip().split("\n")[0]) +print(f"*** {num} total items found!") diff --git a/examples/cdp_mode/raw_cdp_extended.py b/examples/cdp_mode/raw_cdp_extended.py new file mode 100644 index 00000000000..918111a8ee2 --- /dev/null +++ b/examples/cdp_mode/raw_cdp_extended.py @@ -0,0 +1,24 @@ +"""The long way of using CDP Mode without WebDriver""" +import asyncio +from seleniumbase import sb_cdp +from seleniumbase import cdp_driver + +url = "https://seleniumbase.io/demo_page" +loop = asyncio.new_event_loop() +driver = cdp_driver.start_sync() +page = loop.run_until_complete(driver.get(url)) +sb = sb_cdp.CDPMethods(loop, page, driver) + +sb.press_keys("input", "Text") +sb.highlight("button") +sb.type("textarea", "Here are some words") +sb.click("button") +sb.set_value("input#mySlider", "100") +sb.click_visible_elements("input.checkBoxClassB") +sb.select_option_by_text("#mySelect", "Set to 75%") +sb.hover_and_click("#myDropdown", "#dropOption2") +sb.click("#checkBox1") +sb.drag_and_drop("img#logo", "div#drop2") +sb.nested_click("iframe#myFrame3", ".fBox") +sb.sleep(1) +sb.quit() diff --git a/examples/cdp_mode/raw_cdp_facebook.py b/examples/cdp_mode/raw_cdp_facebook.py new file mode 100644 index 00000000000..f59993ec198 --- /dev/null +++ b/examples/cdp_mode/raw_cdp_facebook.py @@ -0,0 +1,17 @@ +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(ad_block=True) +sb.goto("https://www.facebook.com/SeleniumBase") +sb.sleep(3) +sb.click_if_visible('[aria-label="Close"] i', timeout=2) +sb.sleep(1) +sb.click_if_visible('[aria-label="Close"] i') +for i in range(18): + sb.scroll_down(14) + sb.sleep(0.12) +print(sb.get_page_title()) +sb.save_as_pdf_to_logs() +sb.save_page_source_to_logs() +sb.save_screenshot_to_logs() +print("Logs have been saved to: ./latest_logs/") +sb.quit() diff --git a/examples/cdp_mode/raw_cdp_fingerprint.py b/examples/cdp_mode/raw_cdp_fingerprint.py new file mode 100644 index 00000000000..f553daa6c35 --- /dev/null +++ b/examples/cdp_mode/raw_cdp_fingerprint.py @@ -0,0 +1,9 @@ +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(use_chromium=True) +sb.goto("https://demo.fingerprint.com/playground") +sb.wait_for_element('a[href*="browser-bot-detection"]') +sb.flash('a[href*="browser-bot-detection"]', duration=3, pause=1) +bot_row_selector = 'table:contains("Bot") tr:nth-of-type(3)' +print(sb.get_text(bot_row_selector)) +sb.flash(bot_row_selector, duration=3, pause=2) diff --git a/examples/cdp_mode/raw_cdp_gitlab.py b/examples/cdp_mode/raw_cdp_gitlab.py new file mode 100644 index 00000000000..8df2c581b99 --- /dev/null +++ b/examples/cdp_mode/raw_cdp_gitlab.py @@ -0,0 +1,9 @@ +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(incognito=True) +sb.goto("https://gitlab.com/users/sign_in") +sb.sleep(2) +sb.solve_captcha() +sb.highlight('h1:contains("GitLab")') +sb.highlight('button:contains("Sign in")') +sb.quit() diff --git a/examples/cdp_mode/raw_cdp_hyatt.py b/examples/cdp_mode/raw_cdp_hyatt.py new file mode 100644 index 00000000000..1607ad29dfa --- /dev/null +++ b/examples/cdp_mode/raw_cdp_hyatt.py @@ -0,0 +1,36 @@ +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(locale="en", guest=True) +sb.goto("https://www.hyatt.com/") +sb.sleep(3.6) +sb.click_if_visible('button[aria-label="Close"]') +sb.sleep(0.1) +sb.click_if_visible("#onetrust-reject-all-handler") +sb.sleep(1.2) +location = "Anaheim, CA, USA" +sb.type('input[id="search-term"]', location) +sb.sleep(1.2) +sb.click('li[data-js="suggestion"]') +sb.sleep(0.6) +sb.click_if_visible('button[aria-label="Close"]') +sb.sleep(0.8) +sb.click("button.be-button-shop") +sb.sleep(1) +sb.click_if_visible('[label="Find Hotels"]') +sb.sleep(6) +card_info = 'div[data-booking-status="BOOKABLE"] [class*="HotelCard_info"]' +hotels = sb.select_all(card_info) +print("Hyatt Hotels in %s:" % location) +print("(" + sb.get_text('span[class*="summary_destination"]') + ")") +if len(hotels) == 0: + print("No availability over the selected dates!") +for hotel in hotels: + info = hotel.text.strip() + if "Avg/Night" in info and not info.startswith("Rates from"): + name = info.split(" (")[0].split(" + ")[0].split(" Award Cat")[0] + name = name.split(" Rates from :")[0] + price = "?" + if "Rates from : " in info: + price = info.split("Rates from : ")[1].split(" Avg/Night")[0] + print("* %s => %s" % (name, price)) +sb.quit() diff --git a/examples/cdp_mode/raw_cdp_immoscout.py b/examples/cdp_mode/raw_cdp_immoscout.py new file mode 100644 index 00000000000..8c68e68a6ba --- /dev/null +++ b/examples/cdp_mode/raw_cdp_immoscout.py @@ -0,0 +1,25 @@ +"""An example that bypasses the DataDome Slider CAPTCHA. +(PyAutoGUI is installed at runtime if not yet installed.)""" +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(locale="en") +sb.goto("https://www.immoscout24.ch/en/real-estate/rent") +sb.sleep(1.6) +sb.solve_captcha() +sb.sleep(2.5) +sb.click_if_visible("#onetrust-accept-btn-handler", timeout=3) +sb.sleep(0.5) +sb.click('[data-cy="Locations_searchFieldOpener"]') +sb.sleep(0.6) +sb.press_keys('[data-cy="Locations_searchField"]', "Bern") +sb.sleep(0.6) +sb.click_if_visible('b:contains("Bern")', timeout=2) +sb.sleep(2.1) +sb.click('button[data-cy="SearchBar_button"]') +sb.sleep(3.5) +print("*** " + sb.get_text("h1")) +items = sb.find_elements('[data-test="result-list-item"]') +for item in items: + item.flash(color="44CC88") + print(item.query_selector('[class*="mainTitle"]').text) + print(item.query_selector("address").text) diff --git a/examples/cdp_mode/raw_cdp_login.py b/examples/cdp_mode/raw_cdp_login.py new file mode 100644 index 00000000000..6a9a68aceb7 --- /dev/null +++ b/examples/cdp_mode/raw_cdp_login.py @@ -0,0 +1,25 @@ +from seleniumbase import decorators +from seleniumbase import sb_cdp + + +def main(): + sb = sb_cdp.Chrome() + sb.goto("https://seleniumbase.io/simple/login") + sb.type("#username", "demo_user") + sb.type("#password", "secret_pass") + sb.click('a:contains("Sign in")') + sb.assert_exact_text("Welcome!", "h1") + sb.assert_element("img#image1") + sb.highlight("#image1") + top_nav = sb.find_element("div.topnav") + links = top_nav.query_selector_all("a") + for nav_item in links: + print(nav_item.text) + sb.click_link("Sign out") + sb.assert_text("signed out", "#top_message") + sb.quit() + + +if __name__ == "__main__": + with decorators.print_runtime("raw_cdp_login.py"): + main() diff --git a/examples/cdp_mode/raw_cdp_methods.py b/examples/cdp_mode/raw_cdp_methods.py new file mode 100644 index 00000000000..0f4cf5653eb --- /dev/null +++ b/examples/cdp_mode/raw_cdp_methods.py @@ -0,0 +1,16 @@ +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +sb.goto("https://seleniumbase.io/demo_page") +sb.type("input", "Quickly type text!") +sb.press_keys("textarea", "Slowly type text!") +sb.click("#myButton") +sb.set_value("input#mySlider", "100") +sb.click_visible_elements("input.checkBoxClassB") +sb.select_option_by_text("#mySelect", "Set to 75%") +sb.hover_and_click("#myDropdown", "#dropOption2") +sb.click("#checkBox1") +sb.drag_and_drop("img#logo", "div#drop2") +sb.nested_click("iframe#myFrame3", ".fBox") +sb.highlight("#myButton") +sb.quit() diff --git a/examples/cdp_mode/raw_cdp_mobile.py b/examples/cdp_mode/raw_cdp_mobile.py new file mode 100644 index 00000000000..5660965569a --- /dev/null +++ b/examples/cdp_mode/raw_cdp_mobile.py @@ -0,0 +1,21 @@ +import mycdp +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +tab = sb.get_active_tab() +loop = sb.get_event_loop() +loop.run_until_complete( + tab.send( + mycdp.emulation.set_device_metrics_override( + width=412, height=732, device_scale_factor=3, mobile=True + ) + ) +) +sb.goto("https://gitlab.com/users/sign_in") +sb.sleep(2) +sb.solve_captcha() +# (The rest is for testing and demo purposes) +sb.assert_text("Username", '[for="user_login"]', timeout=3) +sb.assert_element('label[for="user_login"]') +sb.highlight('button:contains("Sign in")') +sb.highlight('h1:contains("GitLab")') diff --git a/examples/cdp_mode/raw_cdp_nike.py b/examples/cdp_mode/raw_cdp_nike.py new file mode 100644 index 00000000000..d9769dd5407 --- /dev/null +++ b/examples/cdp_mode/raw_cdp_nike.py @@ -0,0 +1,17 @@ +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +sb.goto("https://www.nike.com/") +sb.sleep(1.2) +sb.click('[data-testid="user-tools-container"] search') +sb.sleep(1) +search = "Pegasus" +sb.press_keys('input[type="search"]', search) +sb.sleep(4) +details = 'ul[data-testid*="products"] figure .details' +elements = sb.select_all(details) +if elements: + print('**** Found results for "%s": ****' % search) + for element in elements: + print("* " + element.text) +sb.quit() diff --git a/examples/cdp_mode/raw_cdp_nordstrom.py b/examples/cdp_mode/raw_cdp_nordstrom.py new file mode 100644 index 00000000000..eea45c06aec --- /dev/null +++ b/examples/cdp_mode/raw_cdp_nordstrom.py @@ -0,0 +1,24 @@ +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(locale="en", guest=True) +sb.goto("https://www.nordstrom.com/") +sb.sleep(2.2) +sb.click("input#keyword-search-input") +sb.sleep(0.8) +search = "cocktail dresses for women teal" +sb.press_keys("input#keyword-search-input", search + "\n") +sb.sleep(2.2) +print('*** Nordstrom Search for "%s":' % search) +unique_item_text = [] +items = sb.find_elements("article") +for item in items: + description = item.query_selector("article h3") + if description and description.text not in unique_item_text: + unique_item_text.append(description.text) + price_text = "" + price = item.query_selector('div div span[aria-hidden="true"]') + if price: + price_text = price.text + print("* %s (%s)" % (description.text, price_text)) + item.flash(color="44CC88") +sb.quit() diff --git a/examples/cdp_mode/raw_cdp_patent.py b/examples/cdp_mode/raw_cdp_patent.py new file mode 100644 index 00000000000..34941f43310 --- /dev/null +++ b/examples/cdp_mode/raw_cdp_patent.py @@ -0,0 +1,10 @@ +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +sb.goto("https://www.lens.org/lens/patent/135-034-272-112-366/frontpage") +sb.sleep(4) +sb.solve_captcha() +sb.flash('[ng-if*="patent.title"]', duration=3, pause=2) +print("* " + sb.get_text('[ng-if*="patent.title"]') + " *") +print(sb.get_text("ol.claims")) +sb.quit() diff --git a/examples/cdp_mode/raw_cdp_pixelscan.py b/examples/cdp_mode/raw_cdp_pixelscan.py new file mode 100644 index 00000000000..3d9a683ac79 --- /dev/null +++ b/examples/cdp_mode/raw_cdp_pixelscan.py @@ -0,0 +1,15 @@ +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(guest=True, ad_block=True) +sb.goto("https://pixelscan.net/fingerprint-check") +sb.remove_element("div.header-ad") +sb.remove_element("pxlscn-dynamic-ad") +sb.sleep(1.8) +sb.assert_text("No automated behavior", "pxlscn-bot-detection") +sb.assert_text("No masking detected", "pxlscn-fingerprint-masking") +sb.assert_text("consistent", "span.status-success") +sb.sleep(0.5) +sb.highlight("span.status-success") +sb.highlight("pxlscn-fingerprint-masking p") +sb.highlight("pxlscn-bot-detection p") +print("Bot Not Detected") diff --git a/examples/cdp_mode/raw_cdp_recaptcha.py b/examples/cdp_mode/raw_cdp_recaptcha.py new file mode 100644 index 00000000000..b58537815fc --- /dev/null +++ b/examples/cdp_mode/raw_cdp_recaptcha.py @@ -0,0 +1,9 @@ +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(guest=True) +sb.goto("https://seleniumbase.io/apps/recaptcha") +sb.solve_captcha() +sb.assert_element("img#captcha-success") +sb.set_messenger_theme(location="top_left") +sb.post_message("SeleniumBase wasn't detected", duration=3) +sb.quit() diff --git a/examples/cdp_mode/raw_cdp_reddit.py b/examples/cdp_mode/raw_cdp_reddit.py new file mode 100644 index 00000000000..afcfaec0e30 --- /dev/null +++ b/examples/cdp_mode/raw_cdp_reddit.py @@ -0,0 +1,17 @@ +"""Reddit Search / Bypasses reCAPTCHA.""" +from seleniumbase import sb_cdp + +search = "reddit+scraper" +url = f"https://www.reddit.com/r/webscraping/search/?q={search}" +sb = sb_cdp.Chrome(use_chromium=True) +sb.goto(url) +sb.solve_captcha() # Might not be needed +post_title = '[data-testid="post-title"]' +sb.wait_for_element(post_title) +for i in range(8): + sb.scroll_down(25) + sb.sleep(0.2) +posts = sb.select_all(post_title) +print('*** Reddit Posts for "%s":' % search) +for post in posts: + print("* " + post.text) diff --git a/examples/cdp_mode/raw_cdp_sannysoft.py b/examples/cdp_mode/raw_cdp_sannysoft.py new file mode 100644 index 00000000000..ff0de4ab2bf --- /dev/null +++ b/examples/cdp_mode/raw_cdp_sannysoft.py @@ -0,0 +1,17 @@ +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +sb.goto("https://bot.sannysoft.com/") +sb.flash("#user-agent-result.passed") +sb.flash("#webdriver-result.passed") +sb.flash("#advanced-webdriver-result.passed") +sb.flash("#chrome-result.passed") +sb.flash("#permissions-result.passed") +sb.flash("#plugins-length-result.passed") +sb.flash("#plugins-type-result.passed") +sb.flash("#languages-result.passed") +sb.flash("#webgl-vendor.passed") +sb.flash("#webgl-renderer.passed") +sb.flash("#broken-image-dimensions.passed") +print("Bot Not Detected") +sb.sleep(1) diff --git a/examples/cdp_mode/raw_cdp_shadow.py b/examples/cdp_mode/raw_cdp_shadow.py new file mode 100644 index 00000000000..f83fdfc7862 --- /dev/null +++ b/examples/cdp_mode/raw_cdp_shadow.py @@ -0,0 +1,14 @@ +"""An example of displaying Shadow DOM inside HTML""" +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +sb.goto("https://seleniumbase.io/apps/turnstile") +element = sb.find_element("div.cf-turnstile div") +html_with_shadow_dom = element.get_html() +print(html_with_shadow_dom) +text_to_find = "Widget containing a Cloudflare security challenge" +sb.assert_true(text_to_find in html_with_shadow_dom) +sb.solve_captcha() +sb.assert_element("img#captcha-success", timeout=3) +sb.set_messenger_theme(location="top_left") +sb.post_message("SeleniumBase wasn't detected", duration=3) diff --git a/examples/cdp_mode/raw_cdp_tabs.py b/examples/cdp_mode/raw_cdp_tabs.py new file mode 100644 index 00000000000..9d5c2493741 --- /dev/null +++ b/examples/cdp_mode/raw_cdp_tabs.py @@ -0,0 +1,15 @@ +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +sb.goto("data:text/html,

Page A

") +sb.assert_text("Page A") +sb.open_new_tab() +sb.goto("data:text/html,

Page B

") +sb.assert_text("Page B") +sb.switch_to_tab(0) +sb.assert_text("Page A") +sb.assert_text_not_visible("Page B") +sb.switch_to_tab(1) +sb.assert_text("Page B") +sb.assert_text_not_visible("Page A") +sb.quit() diff --git a/examples/cdp_mode/raw_cdp_target.py b/examples/cdp_mode/raw_cdp_target.py new file mode 100644 index 00000000000..8e49149ebc9 --- /dev/null +++ b/examples/cdp_mode/raw_cdp_target.py @@ -0,0 +1,31 @@ +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +sb.goto("https://www.target.com/") +sb.sleep(1.5) +sb.click("input#search") +sb.sleep(0.5) +search = "Settlers of Catan Board Game" +required_text = "Catan" +sb.type("input#search", search) +sb.sleep(0.5) +sb.click('button[aria-label="search"]') +sb.sleep(2.5) +print('*** Target Search for "%s":' % search) +print(' (Results must contain "%s".)' % required_text) +unique_item_text = [] +items = sb.find_elements( + '[data-test*="ProductListing"], [data-test="product-details"]' +) +for item in items: + if required_text.lower() in item.text.lower(): + description = item.query_selector('h3, a[data-test*="Card/title"]') + if description and description.text not in unique_item_text: + unique_item_text.append(description.text) + print("* " + description.text) + price = item.query_selector( + '[style*="weight: 700;"], [data-test="current-price"]' + ) + if price: + print(" (" + price.text + ")") + item.scroll_into_view() diff --git a/examples/cdp_mode/raw_cdp_timezone.py b/examples/cdp_mode/raw_cdp_timezone.py new file mode 100644 index 00000000000..5843016a96f --- /dev/null +++ b/examples/cdp_mode/raw_cdp_timezone.py @@ -0,0 +1,16 @@ +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(ad_block=True, incognito=True) +url = "https://www.randymajors.org/what-time-zone-am-i-in" +sb.goto(url, tzone="Asia/Kolkata", geoloc=(26.863, 80.94)) +sb.remove_elements("#right-sidebar") +sb.sleep(2.5) +sb.remove_elements('[data-google-query-id]') +sb.remove_elements("iframe:not(#embedMapFrame)") +sb.sleep(2.5) +sb.goto(url, tzone="Asia/Tokyo", geoloc=(35.050681, 136.844728)) +sb.remove_elements("#right-sidebar") +sb.sleep(2.5) +sb.remove_elements('[data-google-query-id]') +sb.remove_elements("iframe:not(#embedMapFrame)") +sb.sleep(2.5) diff --git a/examples/cdp_mode/raw_cdp_turnstile.py b/examples/cdp_mode/raw_cdp_turnstile.py new file mode 100644 index 00000000000..a8366eebdf3 --- /dev/null +++ b/examples/cdp_mode/raw_cdp_turnstile.py @@ -0,0 +1,9 @@ +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +sb.goto("https://seleniumbase.io/apps/turnstile") +sb.solve_captcha() +sb.assert_element("img#captcha-success") +sb.set_messenger_theme(location="top_left") +sb.post_message("SeleniumBase wasn't detected", duration=3) +sb.quit() diff --git a/examples/cdp_mode/raw_cdp_walmart.py b/examples/cdp_mode/raw_cdp_walmart.py new file mode 100644 index 00000000000..47d2ff1ce9c --- /dev/null +++ b/examples/cdp_mode/raw_cdp_walmart.py @@ -0,0 +1,41 @@ +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(locale="en", guest=True) +sb.goto("https://www.walmart.com/") +sb.sleep(3) +continue_button = 'button:contains("Continue shopping")' +if sb.is_element_visible(continue_button): + sb.gui_click_element(continue_button) + sb.sleep(0.6) +sb.click('input[aria-label="Search"]') +sb.sleep(1.4) +search = "Settlers of Catan Board Game" +required_text = "Catan" +sb.press_keys('input[aria-label="Search"]', search + "\n") +sb.sleep(3.8) +sb.remove_elements('[data-testid="skyline-ad"]') +sb.remove_elements('[data-testid="sba-container"]') +print('*** Walmart Search for "%s":' % search) +print(' (Results must contain "%s".)' % required_text) +unique_item_text = [] +sb.click_if_visible('[data-automation-id="sb-btn-close-mark"]') +items = sb.find_elements('[data-item-id]') +for item in items: + if required_text.lower() in item.text.lower(): + description = item.query_selector( + '[data-automation-id="product-title"]' + ) + if description and description.text not in unique_item_text: + unique_item_text.append(description.text) + print("* " + description.text) + price = item.query_selector( + '[data-automation-id="product-price"]' + ) + if price: + price_text = price.text + price_text = price_text.split("current price Now ")[-1] + price_text = price_text.split("current price ")[-1] + price_text = price_text.split(" ")[0] + print(" (" + price_text + ")") + item.scroll_into_view() +sb.quit() diff --git a/examples/cdp_mode/raw_cdp_with_sb.py b/examples/cdp_mode/raw_cdp_with_sb.py new file mode 100644 index 00000000000..f43e741d792 --- /dev/null +++ b/examples/cdp_mode/raw_cdp_with_sb.py @@ -0,0 +1,32 @@ +"""Example of using CDP Mode with WebDriver""" +from seleniumbase import SB + + +with SB(uc=True, test=True, locale="en") as sb: + sb.activate_cdp_mode() + sb.goto("https://www.priceline.com/") + sb.sleep(2.5) + sb.internalize_links() # Don't open links in a new tab + sb.click_if_visible('[aria-label="Close Modal"]') + sb.sleep(0.2) + sb.click("#link_header_nav_experiences") + sb.sleep(3.5) + sb.remove_elements("msm-cookie-banner") + sb.sleep(1.5) + location = "Amsterdam" + where_to = 'div[data-automation*="experiences"] input' + button = 'button[data-automation*="experiences-search"]' + sb.wait_for_text("Where to?") + sb.gui_click_element(where_to) + sb.press_keys(where_to, location) + sb.sleep(1) + sb.gui_click_element(button) + sb.sleep(3) + print(sb.get_title()) + print("************") + for i in range(8): + sb.scroll_down(50) + sb.sleep(0.2) + cards = sb.select_all('span[data-automation*="product-list-card"]') + for card in cards: + print("* %s" % card.text) diff --git a/examples/cdp_mode/raw_cdp_xpath.py b/examples/cdp_mode/raw_cdp_xpath.py new file mode 100644 index 00000000000..f2c64942469 --- /dev/null +++ b/examples/cdp_mode/raw_cdp_xpath.py @@ -0,0 +1,16 @@ +"""Test that CDP Mode can use XPath selectors.""" +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +sb.goto("https://seleniumbase.io/demo_page") +sb.highlight('//input[@id="myTextInput"]') +sb.type('//*[@id="myTextInput"]', "XPath Test!") +sb.type('//input[@name="text" or @name="preText2"]', "Xpath Here!") +sb.sleep(0.5) +sb.highlight('//button[contains(text(),"(Green)")]') +sb.click('//button[starts-with(text(),"Click Me")]') +sb.assert_element('//button[contains(., "Purple")]') +sb.sleep(0.5) +sb.highlight("//table/tbody/tr/td/h3") +sb.highlight("//table/tbody/tr[1]/td[2]/h2") +sb.assert_text("SeleniumBase", "//table//h2") diff --git a/examples/cdp_mode/raw_cdp_yc_news.py b/examples/cdp_mode/raw_cdp_yc_news.py new file mode 100644 index 00000000000..49747c80162 --- /dev/null +++ b/examples/cdp_mode/raw_cdp_yc_news.py @@ -0,0 +1,7 @@ +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +sb.goto("https://news.ycombinator.com/submitted?id=seleniumbase") +elements = sb.find_elements("span.titleline > a") +for element in elements: + print("* " + element.text) diff --git a/examples/cdp_mode/raw_cdp_zillow.py b/examples/cdp_mode/raw_cdp_zillow.py new file mode 100644 index 00000000000..a87c83106a6 --- /dev/null +++ b/examples/cdp_mode/raw_cdp_zillow.py @@ -0,0 +1,23 @@ +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(use_chromium=True, ad_block=True) +sb.goto("https://www.zillow.com/") +sb.sleep(2) +search = "Bar Harbor ME homes on the waterfront" +sb.type('input[aria-label="Search"]', search) +sb.sleep(1) +sb.click('button[type="submit"]') +sb.sleep(2) +sb.click_if_visible('span:contains("Cancel")') +sb.sleep(0.5) +sb.click_if_visible('button[title="Close"]') +items = sb.find_visible_elements('[data-testid="property-card"]') +print('*** %s:' % search) +for i, item in enumerate(items): + print(f"* <====== {i + 1} ======>") + print(item.query_selector('[data-testid*="details"]').text) + print(item.query_selector('[data-testid*="address"]').text) + print(item.query_selector('[data-testid*="price"]').text) + item.scroll_into_view() +sb.sleep(2) +sb.quit() diff --git a/examples/cdp_mode/raw_cf.py b/examples/cdp_mode/raw_cf.py new file mode 100644 index 00000000000..beb23acf2ee --- /dev/null +++ b/examples/cdp_mode/raw_cf.py @@ -0,0 +1,24 @@ +"""Using CDP Mode to bypass CAPTCHAs in different ways. +PyAutoGUI is installed automatically if not already.""" +from seleniumbase import SB + +with SB(uc=True, test=True, guest=True) as sb: + url = "https://seleniumbase.io/apps/turnstile" + sb.activate_cdp_mode(url) + sb.sleep(3) + sb.uc_gui_handle_captcha() # PyAutoGUI Tabs + Spacebar + sb.sleep(3) + +with SB(uc=True, test=True, guest=True) as sb: + url = "https://seleniumbase.io/apps/turnstile" + sb.activate_cdp_mode(url) + sb.sleep(4) + sb.uc_gui_click_captcha() # PyAutoGUI mouse click + sb.sleep(3) + +with SB(uc=True, test=True, guest=True) as sb: + url = "https://seleniumbase.io/apps/turnstile" + sb.activate_cdp_mode(url) + sb.sleep(4) + sb.solve_captcha() # CDP Input.dispatchMouseEvent + sb.sleep(3) diff --git a/examples/cdp_mode/raw_cf_captcha.py b/examples/cdp_mode/raw_cf_captcha.py new file mode 100644 index 00000000000..2f65fe29b44 --- /dev/null +++ b/examples/cdp_mode/raw_cf_captcha.py @@ -0,0 +1,8 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, incognito=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://wsform.com/demo/") + sb.sleep(2.5) + sb.solve_captcha() + sb.sleep(3) diff --git a/examples/cdp_mode/raw_cf_clearance.py b/examples/cdp_mode/raw_cf_clearance.py new file mode 100644 index 00000000000..6cf2eed7760 --- /dev/null +++ b/examples/cdp_mode/raw_cf_clearance.py @@ -0,0 +1,22 @@ +from seleniumbase import sb_cdp + + +def get_cf_clearance_cookie(sb): + all_cookies = sb.get_all_cookies() + for cookie in all_cookies: + if cookie.name == "cf_clearance": + return cookie + return None + + +sb = sb_cdp.Chrome() +sb.goto("https://gitlab.com/users/sign_in") +sb.sleep(3) # Wait for CAPTCHA to load +sb.solve_captcha() # (Only if found) +sb.sleep(2) # Wait for CAPTCHA success +cf_cookie = get_cf_clearance_cookie(sb) +if cf_cookie: + print("cf_clearance cookie: %s" % cf_cookie.value) +else: + print("Didn't find the cf_clearance cookie!") +sb.quit() diff --git a/examples/cdp_mode/raw_chatgpt.py b/examples/cdp_mode/raw_chatgpt.py new file mode 100644 index 00000000000..e0db06c98e6 --- /dev/null +++ b/examples/cdp_mode/raw_chatgpt.py @@ -0,0 +1,28 @@ +from contextlib import suppress +from seleniumbase import SB + +with SB(uc=True, test=True, ad_block=True) as sb: + sb.activate_cdp_mode() + sb.open("https://chatgpt.com/") + sb.sleep(1) + close_1 = 'button[aria-label="Close dialog"]' + close_2 = 'button[data-testid="close-button"]' + sb.click_if_visible("%s, %s" % (close_1, close_2)) + query = "Compare Playwright to SeleniumBase in under 178 words" + sb.press_keys("#prompt-textarea, #mobile-composer-prompt", query) + sb.click('button[data-testid="send-button"], button[aria-label*="Send"]') + print('*** Input for ChatGPT: ***\n"%s"' % query) + sb.sleep(3) + with suppress(Exception): + # The "Stop" button disappears when ChatGPT is done typing a response + sb.wait_for_element_not_visible( + 'button[data-testid="stop-button"]', timeout=20 + ) + chat = sb.find_element( + '[data-message-author-role="assistant"] .markdown, ' + '[data-assistant-markdown]' + ) + soup = sb.get_beautiful_soup(chat.get_html()).text.strip() + soup = soup.replace("\n\n\n", "\n\n") + print("*** Response from ChatGPT: ***\n%s" % soup) + sb.sleep(3) diff --git a/examples/cdp_mode/raw_consecutive_c.py b/examples/cdp_mode/raw_consecutive_c.py new file mode 100644 index 00000000000..90df30c61bf --- /dev/null +++ b/examples/cdp_mode/raw_consecutive_c.py @@ -0,0 +1,15 @@ +# An example of bypassing 2 consecutive CF CAPTCHAs""" +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://agents.moderationinterface.com") + sb.sleep(3) + if not sb.is_element_present("#login-submit"): + sb.solve_captcha() + sb.sleep(2) + sb.wait_for_element("#login-submit", timeout=3) + sb.sleep(2) + sb.sleep(1) + sb.solve_captcha() + sb.sleep(2) diff --git a/examples/cdp_mode/raw_cookies_async.py b/examples/cdp_mode/raw_cookies_async.py new file mode 100644 index 00000000000..aef54198d2b --- /dev/null +++ b/examples/cdp_mode/raw_cookies_async.py @@ -0,0 +1,36 @@ +"""A script that loads cookies to bypass login.""" +import asyncio +import time +from seleniumbase import cdp_driver + + +# Log in to Swag Labs and save cookies +async def get_login_cookies(): + url = "https://www.saucedemo.com" + driver = await cdp_driver.start_async(incognito=True) + page = await driver.get(url) + await page.type("#user-name", "standard_user") + await page.type("#password", "secret_sauce") + await page.click('input[type="submit"]') + cookies = await driver.cookies.get_all() + driver.stop() + return cookies + + +# Load previously saved cookies to bypass login +async def login_with_cookies(cookies): + url_1 = "https://www.saucedemo.com" + url_2 = "https://www.saucedemo.com/inventory.html" + driver = await cdp_driver.start_async() + page = await driver.get(url_1) + await driver.cookies.set_all(cookies) + await driver.get(url_2) + await page.select("div.inventory_list") + time.sleep(2) + driver.stop() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + cookies = loop.run_until_complete(get_login_cookies()) + loop.run_until_complete(login_with_cookies(cookies)) diff --git a/examples/cdp_mode/raw_demo_site.py b/examples/cdp_mode/raw_demo_site.py new file mode 100644 index 00000000000..fbf6e4e5feb --- /dev/null +++ b/examples/cdp_mode/raw_demo_site.py @@ -0,0 +1,72 @@ +"""Example of using various CDP Mode commands.""" +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://seleniumbase.io/demo_page") + + # Assert various things + sb.assert_title("Web Testing Page") + sb.assert_element("tbody#tbodyId") + sb.assert_text("Demo Page", "h1") + + # Type text into various text fields and then assert + sb.type("#myTextInput", "This is Automated") + sb.type("textarea.area1", "Testing Time!\n") + sb.type('[name="preText2"]', "Typing Text!") + sb.assert_text("This is Automated", "#myTextInput") + sb.assert_text("Testing Time!\n", "textarea.area1") + sb.assert_text("Typing Text!", '[name="preText2"]') + + # Hover & click a dropdown element and assert results + sb.assert_text("Automation Practice", "h3") + sb.hover_and_click("#myDropdown", "#dropOption2") + sb.assert_text("Link Two Selected", "h3") + + # Click a button and then verify the expected results + sb.assert_text("This Text is Green", "#pText") + sb.click('button:contains("Click Me")') + sb.assert_text("This Text is Purple", "#pText") + + # Verify that a slider control updates a progress bar + sb.assert_element('progress[value="50"]') + sb.set_value("input#mySlider", "100") + sb.assert_element('progress[value="100"]') + + # Verify that a "select" option updates a meter bar + sb.assert_element('meter[value="0.25"]') + sb.select_option_by_text("#mySelect", "Set to 75%") + sb.assert_element('meter[value="0.75"]') + + # Verify that clicking a radio button selects it + sb.assert_false(sb.is_selected("#radioButton2")) + sb.click("#radioButton2") + sb.assert_true(sb.is_selected("#radioButton2")) + + # Verify that clicking a checkbox makes it selected + sb.assert_element_not_visible("img#logo") + sb.assert_false(sb.is_selected("#checkBox1")) + sb.click("#checkBox1") + sb.assert_true(sb.is_selected("#checkBox1")) + sb.assert_element("img#logo") + + # Verify clicking on multiple elements with one call + sb.assert_false(sb.is_selected("#checkBox2")) + sb.assert_false(sb.is_selected("#checkBox3")) + sb.assert_false(sb.is_selected("#checkBox4")) + sb.click_visible_elements("input.checkBoxClassB") + sb.assert_true(sb.is_selected("#checkBox2")) + sb.assert_true(sb.is_selected("#checkBox3")) + sb.assert_true(sb.is_selected("#checkBox4")) + + # Verify Drag and Drop + sb.assert_element_not_visible("div#drop2 img#logo") + sb.drag_and_drop("img#logo", "div#drop2") + sb.assert_element("div#drop2 img#logo") + + # Click inside an iframe and test highlighting + sb.flash("iframe#myFrame3") + sb.sleep(1) + sb.nested_click("iframe#myFrame3", ".fBox") + sb.sleep(0.5) + sb.highlight("iframe#myFrame3") diff --git a/examples/cdp_mode/raw_drag_and_drop.py b/examples/cdp_mode/raw_drag_and_drop.py new file mode 100644 index 00000000000..80427b4aae4 --- /dev/null +++ b/examples/cdp_mode/raw_drag_and_drop.py @@ -0,0 +1,21 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, incognito=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://seleniumbase.io/other/drag_and_drop") + sb.assert_element_not_visible("#div1 img#drag1") + sb.drag_and_drop("#drag1", "#div1") + sb.assert_element("#div1 img#drag1") + sb.sleep(1) + +with SB(uc=True, test=True, incognito=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://jqueryui.com/draggable/") + sb.switch_to_frame("iframe") + x, y = sb.get_gui_element_center("#draggable") + sb.switch_to_default_content() + sb.scroll_to_top() + sb.gui_drag_drop_points(x, y, x + 180, y + 90) + sb.gui_drag_drop_points(x + 180, y + 90, x + 60, y + 120) + sb.gui_drag_drop_points(x + 60, y + 120, x + 40, y + 40) + sb.sleep(1) diff --git a/examples/cdp_mode/raw_driver.py b/examples/cdp_mode/raw_driver.py new file mode 100644 index 00000000000..cf053c41eb0 --- /dev/null +++ b/examples/cdp_mode/raw_driver.py @@ -0,0 +1,11 @@ +import atexit +from seleniumbase import Driver + +driver = Driver(uc=True, guest=True) +atexit.register(driver.quit) +driver.activate_cdp_mode() +driver.open("www.planetminecraft.com/account") +driver.sleep(3) +driver.solve_captcha() +driver.wait_for_element_absent("input[disabled]") +driver.sleep(2) diff --git a/examples/cdp_mode/raw_easyjet.py b/examples/cdp_mode/raw_easyjet.py new file mode 100644 index 00000000000..d7c2ff517d3 --- /dev/null +++ b/examples/cdp_mode/raw_easyjet.py @@ -0,0 +1,48 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en", ad_block=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.easyjet.com/en/") + sb.sleep(1.8) + sb.click_if_visible("button#ensRejectAds", timeout=2) + sb.sleep(1) + input_from = 'input[name="from"]' + sb.click(input_from) + sb.sleep(1) + sb.clear(input_from) + sb.press_keys(input_from, "London Gatwick") + sb.sleep(1) + sb.click('span[data-testid="airport-name"]') + sb.sleep(1) + sb.press_keys('input[name="to"]', "Paris") + sb.sleep(1) + sb.click('span[data-testid="airport-name"]') + sb.sleep(1) + sb.click('input[name="when"]') + sb.sleep(1) + sb.click('[data-testid="month"]:last-of-type [aria-disabled="false"]') + sb.sleep(1) + sb.click('[data-testid="month"]:last-of-type [aria-disabled="false"]') + sb.sleep(1) + sb.click('button[data-testid="submit"]') + sb.sleep(4) + sb.connect() + sb.sleep(1) + for window in sb.driver.window_handles: + sb.switch_to_window(window) + if "/buy/flights" in sb.get_current_url(): + break + sb.click_if_visible("button#ensCloseBanner") + days = sb.find_elements('div[class*="FlightGridLayout_column"]') + for day in days: + if not day.text.strip(): + continue + print("**** " + " ".join(day.text.split("\n")[0:2]) + " ****") + fares = day.find_elements("css selector", 'button[class*="flightDet"]') + if not fares: + print("No flights today!") + for fare in fares: + info = fare.text + info = info.replace("LOWEST FARE\n", "") + info = info.replace("\n", " ") + print(info) diff --git a/examples/cdp_mode/raw_elal.py b/examples/cdp_mode/raw_elal.py new file mode 100644 index 00000000000..3274920e2d5 --- /dev/null +++ b/examples/cdp_mode/raw_elal.py @@ -0,0 +1,61 @@ +import re +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en", incognito=True) as sb: + sb.activate_cdp_mode() + sb.goto("www.elal.com/flight-deals/en-us/flights-from-boston-to-tel-aviv") + sb.sleep(5) + sb.click('button[data-att="search"]') + sb.sleep(5) + sb.click_if_visible("#onetrust-close-btn-container button") + sb.sleep(1) + view_other_dates = 'button[aria-label*="viewOtherDates.cta"]' + if sb.is_element_visible(view_other_dates): + sb.click(view_other_dates) + sb.sleep(5) + if sb.is_element_visible("flexible-search-calendar"): + print("*** Flight Calendar for El Al (Boston to Tel Aviv): ***") + print(sb.get_text("flexible-search-calendar")) + prices = [] + elements = sb.find_elements("span.matric-cell__content__price") + if elements: + print("*** Prices List: ***") + for element in elements: + prices.append(element.text) + prices.sort(key=lambda x: int(re.sub("[^0-9]", "", x))) + for price in prices: + print(price) + print("*** Lowest Price: ***") + lowest_price = prices[0] + print(lowest_price) + sb.scroll_down(12) + sb.sleep(1) + sb.find_element_by_text(lowest_price).click() + sb.sleep(2) + search_cell = 'button[aria-label*="Search.cell.buttonTitle"]' + sb.scroll_into_view(search_cell) + sb.sleep(1) + sb.click(search_cell) + sb.sleep(5) + else: + print("*** Lowest Prices: ***") + departure_prices = "#uiFlightPanel0 div.ui-bound__price__value" + return_prices = "#uiFlightPanel1 div.ui-bound__price__value" + elements = sb.find_elements(departure_prices) + for element in elements: + if "lowest price" in element.text: + print("Departure Flight:") + print(element.text) + break + elements = sb.find_elements(return_prices) + for element in elements: + if "lowest price" in element.text: + print("Return Flight:") + print(element.text) + break + dates = sb.find_elements('div[class*="flight-date"]') + if len(dates) == 2: + print("*** Departure Date: ***") + print(dates[0].text) + print("*** Return Date: ***") + print(dates[1].text) diff --git a/examples/cdp_mode/raw_facebook.py b/examples/cdp_mode/raw_facebook.py new file mode 100644 index 00000000000..45104119ee6 --- /dev/null +++ b/examples/cdp_mode/raw_facebook.py @@ -0,0 +1,17 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, ad_block=True) as sb: + sb.activate_cdp_mode() + sb.sleep(3) + sb.goto("https://www.facebook.com/SeleniumBase") + sb.sleep(1) + sb.click_if_visible('[aria-label="Close"] i', timeout=2) + sb.sleep(1) + sb.click_if_visible('[aria-label="Close"] i') + for i in range(18): + sb.scroll_down(14) + print(sb.get_page_title()) + sb.save_as_pdf_to_logs() + sb.save_page_source_to_logs() + sb.save_screenshot_to_logs() + print("Logs have been saved to: ./latest_logs/") diff --git a/examples/cdp_mode/raw_fingerprint.py b/examples/cdp_mode/raw_fingerprint.py new file mode 100644 index 00000000000..9a7c4a2137c --- /dev/null +++ b/examples/cdp_mode/raw_fingerprint.py @@ -0,0 +1,10 @@ +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://demo.fingerprint.com/playground") + sb.flash('a[href*="browser-bot-detection"]', duration=3, pause=1) + bot_row_selector = 'table:contains("Bot") tr:nth-of-type(3)' + print(sb.get_text(bot_row_selector)) + sb.assert_text("Bot Not detected", bot_row_selector) + sb.flash(bot_row_selector, duration=3, pause=2) diff --git a/examples/cdp_mode/raw_footlocker.py b/examples/cdp_mode/raw_footlocker.py new file mode 100644 index 00000000000..8601fbe2c1f --- /dev/null +++ b/examples/cdp_mode/raw_footlocker.py @@ -0,0 +1,22 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en", ad_block=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.footlocker.com/") + sb.sleep(2.5) + sb.click_if_visible('button[id*="Agree"]') + sb.sleep(0.6) + sb.click('input[name="query"]') + sb.sleep(0.6) + search = "Nike Shoes" + sb.press_keys('input[name="query"]', search) + sb.sleep(2.5) + sb.click('ul[id*="typeahead"] li div') + sb.sleep(3.5) + elements = sb.select_all("a.ProductCard-link") + if elements: + print('**** Found results for "%s": ****' % search) + for element in elements: + print("------------------ >>>") + print("* " + element.text) + sb.sleep(2) diff --git a/examples/cdp_mode/raw_form_turnstile.py b/examples/cdp_mode/raw_form_turnstile.py new file mode 100644 index 00000000000..471d53261cf --- /dev/null +++ b/examples/cdp_mode/raw_form_turnstile.py @@ -0,0 +1,20 @@ +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("seleniumbase.io/apps/form_turnstile") + sb.press_keys("#name", "SeleniumBase") + sb.press_keys("#email", "test@test.test") + sb.press_keys("#phone", "1-555-555-5555") + sb.click('[for="date"]') + sb.click("td.is-today button") + sb.click('div[class="select-wrapper"] input') + sb.click('span:contains("9:00 PM")') + sb.highlight_click('input[value="AR"] + span') + sb.click('input[value="cc"] + span') + sb.scroll_down(40) + sb.solve_captcha() + sb.highlight("img#captcha-success", timeout=3) + sb.highlight_click('button:contains("Request & Pay")') + sb.highlight("img#submit-success") + sb.highlight('button:contains("Success!")') diff --git a/examples/cdp_mode/raw_gas_records.py b/examples/cdp_mode/raw_gas_records.py new file mode 100644 index 00000000000..dcd1e343c8b --- /dev/null +++ b/examples/cdp_mode/raw_gas_records.py @@ -0,0 +1,26 @@ +"""(Bypasses the Imperva/Incapsula hCaptcha)""" +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + url = ( + "https://www.gassaferegister.co.uk/gas-safety" + "/gas-safety-certificates-records/building-regulations-certificate" + "/order-replacement-building-regulations-certificate/" + ) + sb.activate_cdp_mode(url) + sb.sleep(0.6) + sb.solve_captcha() + sb.wait_for_element("#SearchTerm", timeout=5) + sb.sleep(1.4) + allow_cookies = 'button:contains("Allow all cookies")' + sb.click_if_visible(allow_cookies, timeout=3) + sb.sleep(1) + sb.press_keys("#SearchTerm", "Hydrogen") + sb.click_if_visible(allow_cookies, timeout=1) + sb.click("button.search-button") + sb.sleep(3) + results = sb.find_elements("div.search-result") + for result in results: + print(result.text.replace(" " * 12, " ").strip() + "\n") + sb.scroll_to_bottom() + sb.sleep(1) diff --git a/examples/cdp_mode/raw_geolocation.py b/examples/cdp_mode/raw_geolocation.py new file mode 100644 index 00000000000..9dce857352a --- /dev/null +++ b/examples/cdp_mode/raw_geolocation.py @@ -0,0 +1,19 @@ +"""Geolocation example using CDP Mode without WebDriver""" +from seleniumbase import decorators +from seleniumbase import sb_cdp + + +@decorators.print_runtime("Geolocation CDP Example") +def main(): + location = (48.87645, 2.26340) + sb = sb_cdp.Chrome(geoloc=location) + sb.goto("https://www.openstreetmap.org/") + sb.sleep(2) + sb.click('a[aria-label="Show My Location"]') + sb.assert_url_contains("48.876450/2.263400") + sb.sleep(5) + sb.quit() + + +if __name__ == "__main__": + main() diff --git a/examples/cdp_mode/raw_geolocation_sb.py b/examples/cdp_mode/raw_geolocation_sb.py new file mode 100644 index 00000000000..e6d51f37630 --- /dev/null +++ b/examples/cdp_mode/raw_geolocation_sb.py @@ -0,0 +1,10 @@ +"""Geolocation example with CDP Mode""" +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + location = (31.774390, 35.222450) + sb.activate_cdp_mode(geoloc=location) + sb.goto("https://www.openstreetmap.org/") + sb.click('a[aria-label="Show My Location"]') + sb.assert_url_contains("31.774390/35.222450") + sb.sleep(5) diff --git a/examples/cdp_mode/raw_gettyimages.py b/examples/cdp_mode/raw_gettyimages.py new file mode 100644 index 00000000000..b102a78def0 --- /dev/null +++ b/examples/cdp_mode/raw_gettyimages.py @@ -0,0 +1,11 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en", pls="none") as sb: + sb.activate_cdp_mode("https://www.gettyimages.com/") + sb.click('label:contains("Editorial")') + sb.press_keys("form input", "comic con 2024 sci fi panel\n") + sb.sleep(3) + items = sb.find_elements("figure picture img") + for item in items: + item.flash(color="44CC88") + sb.sleep(0.08) diff --git a/examples/cdp_mode/raw_gitlab.py b/examples/cdp_mode/raw_gitlab.py new file mode 100644 index 00000000000..b677548cd23 --- /dev/null +++ b/examples/cdp_mode/raw_gitlab.py @@ -0,0 +1,12 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en") as sb: + sb.activate_cdp_mode() + sb.goto("https://gitlab.com/users/sign_in") + sb.sleep(2) + sb.solve_captcha() + # (The rest is for testing and demo purposes) + sb.assert_text("Username", '[for="user_login"]', timeout=3) + sb.assert_element('label[for="user_login"]') + sb.highlight('button:contains("Sign in")') + sb.highlight('h1:contains("GitLab")') diff --git a/examples/cdp_mode/raw_glassdoor.py b/examples/cdp_mode/raw_glassdoor.py new file mode 100644 index 00000000000..f7d7b63c9f7 --- /dev/null +++ b/examples/cdp_mode/raw_glassdoor.py @@ -0,0 +1,26 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, incognito=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.glassdoor.com/Reviews/index.htm") + sb.sleep(2.2) + sb.solve_captcha() + sb.sleep(0.6) + sb.highlight('[data-test="global-nav-glassdoor-logo"]') + sb.highlight('[data-test="site-header-companies"]') + sb.highlight('[data-test="search-button"]') + sb.highlight('[data-test="sign-in-button"]') + sb.highlight('[data-test="company-search-autocomplete"]') + sb.press_keys("#employer-autocomplete", "NASA Goddard\n") + sb.sleep(1) + sb.click_if_visible( + '[data-role-variant="primary"] span:contains("Search")' + ) + sb.sleep(1) + sb.click_if_visible('[aria-label*="NASA"] img') + sb.sleep(1) + print(sb.get_page_title()) + sb.save_as_pdf_to_logs() + sb.save_page_source_to_logs() + sb.save_screenshot_to_logs() + print("Logs have been saved to: ./latest_logs/") diff --git a/examples/cdp_mode/raw_hacker_news.py b/examples/cdp_mode/raw_hacker_news.py new file mode 100644 index 00000000000..7f49f1117ef --- /dev/null +++ b/examples/cdp_mode/raw_hacker_news.py @@ -0,0 +1,8 @@ +from seleniumbase import SB + +with SB(uc=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://news.ycombinator.com/submitted?id=seleniumbase") + elements = sb.find_elements("span.titleline > a") + for element in elements: + print("* " + element.text) diff --git a/examples/cdp_mode/raw_handle_alerts.py b/examples/cdp_mode/raw_handle_alerts.py new file mode 100644 index 00000000000..e769def556f --- /dev/null +++ b/examples/cdp_mode/raw_handle_alerts.py @@ -0,0 +1,18 @@ +"""An example of handling alerts in CDP Mode.""" +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://the-internet.herokuapp.com/javascript_alerts") + sb.click('button[onclick="jsAlert()"]') + sb.sleep(1) + sb.uc_gui_press_key("\n") # Accept Alert + sb.sleep(1) + sb.click('button[onclick="jsConfirm()"]') + sb.sleep(1) + sb.uc_gui_press_key("ESC") # Dismiss Alert + sb.sleep(1) + sb.click('button[onclick="jsPrompt()"]') + sb.sleep(1) + sb.uc_gui_write("Here is my prompt answer\n") + sb.sleep(1) diff --git a/examples/cdp_mode/raw_hilton.py b/examples/cdp_mode/raw_hilton.py new file mode 100644 index 00000000000..a65730314e0 --- /dev/null +++ b/examples/cdp_mode/raw_hilton.py @@ -0,0 +1,40 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en", use_chromium=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.hilton.com/en/") + sb.sleep(4.2) + location = "Sunnyvale, CA, USA" + location_input = "input#location-input" + sb.wait_for_element(location_input) + sb.sleep(1.2) + sb.click(location_input) + sb.sleep(1.2) + sb.press_keys(location_input, location) + sb.sleep(2) + sb.click('span:contains("Check-in")') + sb.sleep(1.2) + sb.click('button[aria-current="date"]') + sb.sleep(1.2) + sb.click_if_visible('[role="dialog"] button.btn--solid') + sb.sleep(1.4) + sb.click('button[data-testid="search-submit-button"]') + sb.sleep(6.4) + sb.reconnect() + for window in sb.driver.window_handles: + sb.switch_to_window(window) + if "/search/" in sb.get_current_url(): + break + hotel_card = 'li[data-testid*="hotel-card"]' + hotel_cards = sb.select_all(hotel_card) + print("Hilton Hotels in %s:" % location) + if not hotel_cards: + print("No availability over the selected dates!") + css = "css selector" # Reconnected Selenium - Using WebElement API + for i, card in enumerate(hotel_cards): + hotel = card.find_element(css, 'h3[data-testid*="PropertyName"]') + price = card.find_element(css, 'p[data-testid="rateItem"]') + if hotel and price: + print("* %s: %s => %s" % ( + i + 1, hotel.text.strip(), price.text.strip()) + ) diff --git a/examples/cdp_mode/raw_homedepot.py b/examples/cdp_mode/raw_homedepot.py new file mode 100644 index 00000000000..d184b936f8e --- /dev/null +++ b/examples/cdp_mode/raw_homedepot.py @@ -0,0 +1,42 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, guest=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.homedepot.com/") + sb.sleep(1.5) + sb.click_if_visible('[data-testid="CloseIcon"]', timeout=3) + sb.sleep(1.2) + search_box = "input#typeahead-search-field-input" + search = "Computer Chair" + category = "Gaming Chairs" + required_text = "Chair" + sb.click(search_box) + sb.sleep(0.8) + sb.press_keys(search_box, search) + sb.sleep(0.6) + sb.click("button#typeahead-search-icon-button") + sb.sleep(3.8) + sb.click('a[aria-label="%s"]' % category) + sb.sleep(3.8) + print('*** Home Depot Search for "%s":' % search) + print(' (Results must contain "%s".)' % required_text) + unique_item_text = [] + product_pod = 'div[data-testid="product-pod"]' + sb.wait_for_element(product_pod) + soup = sb.get_beautiful_soup() + items = soup.select(product_pod) + for item in items: + item_text = item.get_text() + if required_text in item_text: + description = item.select_one( + 'span[data-testid="attribute-product-label"]' + ) + if description: + description_text = description.get_text(strip=True) + if description_text not in unique_item_text: + unique_item_text.append(description_text) + print("* " + description_text) + price = item.select_one('[class*="sm:sui-text-4xl"]') + if price: + price_text = "$%s" % price.get_text(strip=True) + print(" (" + price_text + ")") diff --git a/examples/cdp_mode/raw_hyatt.py b/examples/cdp_mode/raw_hyatt.py new file mode 100644 index 00000000000..cc90bf45c9d --- /dev/null +++ b/examples/cdp_mode/raw_hyatt.py @@ -0,0 +1,36 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en", guest=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.hyatt.com/") + sb.sleep(3.6) + sb.click_if_visible('button[aria-label="Close"]') + sb.sleep(0.1) + sb.click_if_visible("#onetrust-reject-all-handler") + sb.sleep(1.2) + location = "Anaheim, CA, USA" + sb.type('input[id="search-term"]', location) + sb.sleep(1.2) + sb.click('li[data-js="suggestion"]') + sb.sleep(0.6) + sb.click_if_visible('button[aria-label="Close"]') + sb.sleep(0.8) + sb.click("button.be-button-shop") + sb.sleep(1) + sb.click_if_visible('[label="Find Hotels"]') + sb.sleep(5.5) + card_info = 'div[data-booking-status="BOOKABLE"] [class*="HotelCard_info"]' + hotels = sb.select_all(card_info) + print("Hyatt Hotels in %s:" % location) + print("(" + sb.get_text('span[class*="summary_destination"]') + ")") + if len(hotels) == 0: + print("No availability over the selected dates!") + for hotel in hotels: + info = hotel.text.strip() + if "Avg/Night" in info and not info.startswith("Rates from"): + name = info.split(" (")[0].split(" + ")[0].split(" Award Cat")[0] + name = name.split(" Rates from :")[0] + price = "?" + if "Rates from : " in info: + price = info.split("Rates from : ")[1].split(" Avg/Night")[0] + print("* %s => %s" % (name, price)) diff --git a/examples/cdp_mode/raw_idealista.py b/examples/cdp_mode/raw_idealista.py new file mode 100644 index 00000000000..51d1c040eef --- /dev/null +++ b/examples/cdp_mode/raw_idealista.py @@ -0,0 +1,17 @@ +"""An example that bypasses the DataDome Slider CAPTCHA. +(PyAutoGUI is installed at runtime if not yet installed.)""" +from seleniumbase import SB + +with SB(uc=True, test=True, locale="es") as sb: + sb.activate_cdp_mode() + sb.goto("https://www.idealista.com/venta-viviendas/barcelona-provincia/") + sb.sleep(1.5) + sb.solve_captcha() + sb.sleep(2) + sb.click_if_visible("button#didomi-notice-agree-button", timeout=3) + print("*** " + sb.get_text("h1")) + items = sb.find_elements("div.item-info-container") + for item in items: + item.flash(color="44CC88") + print(item.query_selector("a.item-link").text) + print(item.query_selector("span.item-price").text) diff --git a/examples/cdp_mode/raw_indeed.py b/examples/cdp_mode/raw_indeed.py new file mode 100644 index 00000000000..c3e29f57305 --- /dev/null +++ b/examples/cdp_mode/raw_indeed.py @@ -0,0 +1,34 @@ +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.indeed.com/companies/search") + sb.sleep(0.8) + sb.solve_captcha() + sb.sleep(0.8) + search_box = "input#company-search" + if not sb.is_element_present(search_box): + sb.solve_captcha() + sb.sleep(0.8) + company = "NASA Jet Propulsion Laboratory" + sb.click(search_box) + sb.sleep(0.1) + sb.press_keys(search_box, company) + sb.click('button[type="submit"]') + sb.click('a:contains("%s")' % company) + name_header = 'div[itemprop="name"]' + sb.sleep(1) + if not sb.is_element_present(name_header): + sb.sleep(2) + sb.solve_captcha() + sb.sleep(1) + sb.highlight(name_header) + sb.sleep(1) + sb.cdp.highlight('h2:contains("About the company")') + sb.sleep(1) + for i in range(10): + sb.scroll_down(12) + sb.sleep(0.14) + info = sb.find_element('[data-testid="AboutSection-section"]') + soup = sb.get_beautiful_soup(info.get_html()).get_text("\n").strip() + print("*** %s: ***\n%s" % (company, soup.replace("\n:", ":"))) diff --git a/examples/cdp_mode/raw_indeed_jobs.py b/examples/cdp_mode/raw_indeed_jobs.py new file mode 100644 index 00000000000..2529cf38eea --- /dev/null +++ b/examples/cdp_mode/raw_indeed_jobs.py @@ -0,0 +1,16 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, use_chromium=True, incognito=True) as sb: + sb.goto("https://www.indeed.com/?from=gnav-compui") + sb.sleep(0.6) + search = "AI Engineer" + sb.type('input[name="q"]', search) + sb.click('button[type="submit"]') + sb.sleep(3.6) + items = sb.find_visible_elements('[data-testid*="jobcard"]') + print('*** Indeed search for "%s":' % search) + for i, item in enumerate(items): + print(f"* <====== {i + 1} ======>") + print(item.text) + item.scroll_into_view() + print(f"*** {len(items)} total items found!") diff --git a/examples/cdp_mode/raw_iphey.py b/examples/cdp_mode/raw_iphey.py new file mode 100644 index 00000000000..93ca8aa0db9 --- /dev/null +++ b/examples/cdp_mode/raw_iphey.py @@ -0,0 +1,17 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, guest=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://iphey.com") + sb.sleep(7) + trustworthy = "#hero-status" + sb.assert_element(trustworthy) + sb.assert_text("Trustworthy", trustworthy) + sb.highlight(trustworthy, loops=10, scroll=False) + items = sb.find_elements('a[style*="check-circle"]') + sb.assert_true(len(items) == 5, "Expecting 5 checks!") + for item in items: + item.flash(color="44CC88") + sb.sleep(0.2) + sb.sleep(2) + sb.save_screenshot_to_logs() diff --git a/examples/cdp_mode/raw_kohls.py b/examples/cdp_mode/raw_kohls.py new file mode 100644 index 00000000000..07a92b6a650 --- /dev/null +++ b/examples/cdp_mode/raw_kohls.py @@ -0,0 +1,38 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en") as sb: + search = "Mickey Mouse Blanket" + req_1 = "Mickey" + req_2 = "Blanket" + s2 = "+".join(search.split(" ")) + q = "?submit-search=web-regular&search=%s" % s2 + url = "https://www.kohls.com/search.jsp%s" % q + sb.activate_cdp_mode(ad_block=True) + sb.goto(url) + sb.sleep(5) + item_selector = 'div[data-testid*="wallet-wrapper"]' + if not sb.is_element_present(item_selector): + item_selector = "li.products_grid" + for item in sb.find_elements(item_selector): + if "Sponsored" in item.text: + item.remove_from_dom() + sb.remove_elements("#tce-sticky-wrapper") + sb.remove_elements("li.sponsored-product") + sb.remove_elements("#tce-dec-ces-3-banner") + print('*** Kohls Search for "%s":' % search) + print(' (Results must contain "%s" and "%s".)' % (req_1, req_2)) + title_selector = "div.prod_nameBlock p" + if not sb.is_element_present(title_selector): + title_selector = 'a[class*="sm:text"][href*="/product/"]' + for item in sb.find_elements(title_selector): + if item: + item.flash(color="44CC88") + title = item.text + if title: + if ( + req_1.lower() in title.lower() + and req_2.lower() in title.lower() + ): + print("* " + title) + sb.sleep(0.1) + sb.sleep(1) diff --git a/examples/cdp_mode/raw_lesfurets.py b/examples/cdp_mode/raw_lesfurets.py new file mode 100644 index 00000000000..ec18ee20b46 --- /dev/null +++ b/examples/cdp_mode/raw_lesfurets.py @@ -0,0 +1,28 @@ +from seleniumbase import SB + +with SB(uc=True, ad_block=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.lesfurets.com/") + sb.sleep(2) + sb.click_if_visible("button#didomi-notice-agree-button", timeout=3) + sb.sleep(1) + sb.click('p:contains("Assurance moto")') + sb.click('label[for="CYLINDREE_CYLINDREE_INF"]') + sb.click('label[for="VEH_POSSESSION_false"]') + sb.click('label[for="VEH_TYPE_ACQUISITION_premier"]') + sb.click("button.userValidation") + sb.click('div[data-testid="DATE_CIRCULATION_VEHICULE-month"]') + sb.click('span:contains("Janvier")') + sb.click('div[data-testid="DATE_CIRCULATION_VEHICULE-year"]') + sb.click('div[data-value="2026"] span') + sb.click('div[data-testid="ACHAT_PREVU_VEHICULE-month"]') + sb.click('span:contains("Mars")') + sb.click('div[data-testid="ACHAT_PREVU_VEHICULE-year"]') + sb.click('div[data-value="2027"] span') + sb.click('div[data-testid="PERMIS"]') + sb.click('span:contains("Aucun")') + sb.click("button.DaisyBlockView-next") + sb.assert_element("div.VehiculePickerField") + sb.click('label:contains("Choisir votre vรฉhicule")') + sb.type("input#modele_picker", "2TWENTY") + sb.sleep(3) diff --git a/examples/cdp_mode/raw_linkedin.py b/examples/cdp_mode/raw_linkedin.py new file mode 100644 index 00000000000..f900da94da7 --- /dev/null +++ b/examples/cdp_mode/raw_linkedin.py @@ -0,0 +1,15 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, ad_block=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.linkedin.com/company/selenium") + sb.sleep(1) + sb.click_if_visible('button[aria-label="Dismiss"]', timeout=2) + sb.sleep(0.5) + for i in range(14): + sb.scroll_down(48) + print(sb.get_page_title()) + sb.save_as_pdf_to_logs() + sb.save_page_source_to_logs() + sb.save_screenshot_to_logs() + print("Logs have been saved to: ./latest_logs/") diff --git a/examples/cdp_mode/raw_mfa_login.py b/examples/cdp_mode/raw_mfa_login.py new file mode 100644 index 00000000000..bbc183ef86e --- /dev/null +++ b/examples/cdp_mode/raw_mfa_login.py @@ -0,0 +1,12 @@ +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +sb.goto("https://seleniumbase.io/realworld/login") +sb.type("#username", "demo_user") +sb.type("#password", "secret_pass") +sb.enter_mfa_code("#totpcode", "GAXG2MTEOR3DMMDG") +sb.assert_text("Welcome!", "h1") +sb.click('a:contains("This Page")') +sb.highlight("h1") +sb.highlight("img#image1") +sb.quit() diff --git a/examples/cdp_mode/raw_mobile_agents.py b/examples/cdp_mode/raw_mobile_agents.py new file mode 100644 index 00000000000..6f8bee60619 --- /dev/null +++ b/examples/cdp_mode/raw_mobile_agents.py @@ -0,0 +1,20 @@ +from seleniumbase import SB + +agent = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36" +agent += " (KHTML, like Gecko) Mobile Safari/537.36" + +sites = ["facebook", "twitter", "linkedin", "youtube", "firefox", "amazon"] +sites += ["chatgpt", "gmail", "perplexity", "snapchat", "tiktok", "roblox"] +urls = [f"https://www.{site}.com" for site in sites] + +for url in urls: + with SB(uc=True, test=True, mobile=True) as sb: + sb.set_window_position(20, 54) + sb.activate_cdp_mode() + sb.goto(url) + sb.sleep(2) + sb.get_new_driver() + sb.set_window_position(550, 54) + sb.activate_cdp_mode(agent=agent) + sb.goto(url) + sb.sleep(8) diff --git a/examples/cdp_mode/raw_mobile_async.py b/examples/cdp_mode/raw_mobile_async.py new file mode 100644 index 00000000000..1a087e59cc8 --- /dev/null +++ b/examples/cdp_mode/raw_mobile_async.py @@ -0,0 +1,29 @@ +import asyncio +import mycdp +import time +from seleniumbase import cdp_driver +from seleniumbase import decorators + + +async def main(): + url = "https://gitlab.com/users/sign_in" + driver = await cdp_driver.start_async() + await driver.page.send( + mycdp.emulation.set_device_metrics_override( + width=412, height=732, device_scale_factor=3, mobile=True + ) + ) + page = await driver.get(url, lang="en") + time.sleep(3) + await page.solve_captcha() + element = await page.select('label[for="user_login"]') + await element.flash_async(duration=1.5, color="44EE44") + time.sleep(1) + element = await page.select('[data-testid="sign-in-button"]') + await element.flash_async(duration=2, color="44EE44") + time.sleep(2) + driver.stop() + +if __name__ == "__main__": + with decorators.print_runtime("raw_mobile_async.py"): + asyncio.run(main()) diff --git a/examples/cdp_mode/raw_mobile_gitlab.py b/examples/cdp_mode/raw_mobile_gitlab.py new file mode 100644 index 00000000000..aadd7a03b98 --- /dev/null +++ b/examples/cdp_mode/raw_mobile_gitlab.py @@ -0,0 +1,24 @@ +import mycdp +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + url = "https://gitlab.com/users/sign_in" + sb.activate_cdp_mode() + tab = sb.get_active_tab() + loop = sb.get_event_loop() + loop.run_until_complete( + tab.send( + mycdp.emulation.set_device_metrics_override( + width=412, height=732, device_scale_factor=3, mobile=True + ) + ) + ) + sb.goto(url) + sb.sleep(2) + sb.solve_captcha() + # (The rest is for testing and demo purposes) + sb.assert_text("Username", '[for="user_login"]', timeout=3) + sb.assert_element('label[for="user_login"]') + sb.highlight('button:contains("Sign in")') + sb.highlight('h1:contains("GitLab")') + sb.post_message("SeleniumBase wasn't detected", duration=4) diff --git a/examples/cdp_mode/raw_mobile_roblox.py b/examples/cdp_mode/raw_mobile_roblox.py new file mode 100644 index 00000000000..e9eaa4b4365 --- /dev/null +++ b/examples/cdp_mode/raw_mobile_roblox.py @@ -0,0 +1,26 @@ +"""Test mobile emulation via mycdp.emulation.set_device_metrics_override(). +tab.send() is a stealthy version of Selenium's execute_cdp_cmd() method.""" +import mycdp +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + url = "https://www.roblox.com/" + agent = ( + "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 " + "(KHTML, like Gecko) Mobile Safari/537.36" + ) + sb.activate_cdp_mode(agent=agent) + tab = sb.get_active_tab() + loop = sb.get_event_loop() + loop.run_until_complete( + tab.send( + mycdp.emulation.set_device_metrics_override( + width=412, height=732, device_scale_factor=3, mobile=True + ) + ) + ) + sb.goto(url) + sb.assert_element("#download-the-app-container") + sb.assert_text("Roblox for Android") + sb.highlight('span:contains("Roblox for Android")', loops=8) + sb.highlight('span:contains("Continue in App")', loops=8) diff --git a/examples/cdp_mode/raw_mouser.py b/examples/cdp_mode/raw_mouser.py new file mode 100644 index 00000000000..0f88b404235 --- /dev/null +++ b/examples/cdp_mode/raw_mouser.py @@ -0,0 +1,20 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, guest=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.mouser.com/") + search_box = 'input[name="keyword"]' + sb.sleep(1.6) + sb.solve_captcha() + sb.sleep(1.8) + sb.wait_for_element(search_box) + sb.sleep(1.2) + sb.press_keys(search_box, "FLUKE-TC01B 25HZ") + sb.sleep(1.2) + sb.click('button[type="submit"]') + sb.sleep(3.2) + sb.wait_for_element("span#spnDescription") + soup = sb.get_beautiful_soup() + print(soup.select_one("h1").get_text(strip=True)) + print(soup.select_one("span#spnDescription").get_text(strip=True)) + print(soup.select_one("td.ext-price-col").get_text(strip=True)) diff --git a/examples/cdp_mode/raw_multi_async.py b/examples/cdp_mode/raw_multi_async.py new file mode 100644 index 00000000000..7cf5e0b8b01 --- /dev/null +++ b/examples/cdp_mode/raw_multi_async.py @@ -0,0 +1,29 @@ +# Testing multiple CDP drivers using the async API +import asyncio +from concurrent.futures import ThreadPoolExecutor +from random import randint +from seleniumbase import cdp_driver +from seleniumbase import decorators + + +async def main(url): + driver = await cdp_driver.start_async() + page = await driver.get(url) + await page.set_window_rect(randint(4, 600), randint(8, 410), 860, 500) + await page.sleep(0.5) + await page.type("input", "Text") + await page.click("button") + await page.sleep(2) + driver.stop() + + +def run_main(url): + asyncio.run(main(url)) + + +if __name__ == "__main__": + urls = ["https://seleniumbase.io/demo_page" for i in range(5)] + with decorators.print_runtime("raw_multi_async.py"): + with ThreadPoolExecutor(max_workers=len(urls)) as executor: + for url in urls: + executor.submit(run_main, url) diff --git a/examples/cdp_mode/raw_multi_c_async.py b/examples/cdp_mode/raw_multi_c_async.py new file mode 100644 index 00000000000..6257f8a6873 --- /dev/null +++ b/examples/cdp_mode/raw_multi_c_async.py @@ -0,0 +1,31 @@ +# Testing multiple CDP drivers using the async API +import asyncio +from concurrent.futures import ThreadPoolExecutor +from random import randint +from seleniumbase import cdp_driver +from seleniumbase import decorators + + +async def main(url): + driver = await cdp_driver.start_async() + page = await driver.get(url) + await page.set_window_rect(randint(4, 600), randint(8, 410), 860, 500) + await page.sleep(2.6) + await page.solve_captcha() + await page.sleep(2.2) + if not await page.is_element_visible("img#captcha-success"): + await page.solve_captcha() + await page.sleep(2.2) + driver.stop() + + +def run_main(url): + asyncio.run(main(url)) + + +if __name__ == "__main__": + urls = ["https://seleniumbase.io/apps/turnstile" for i in range(4)] + with decorators.print_runtime("raw_multi_c_async.py"): + with ThreadPoolExecutor(max_workers=len(urls)) as executor: + for url in urls: + executor.submit(run_main, url) diff --git a/examples/cdp_mode/raw_multi_captcha.py b/examples/cdp_mode/raw_multi_captcha.py new file mode 100644 index 00000000000..c3d005f515e --- /dev/null +++ b/examples/cdp_mode/raw_multi_captcha.py @@ -0,0 +1,26 @@ +# Testing multiple CDP drivers using the sync API +from concurrent.futures import ThreadPoolExecutor +from random import randint +from seleniumbase import decorators +from seleniumbase import sb_cdp + + +def main(url): + sb = sb_cdp.Chrome(lang="en", incognito=True) + sb.goto(url) + sb.set_window_rect(randint(4, 680), randint(8, 380), 840, 520) + sb.sleep(2.6) + sb.gui_click_captcha() + sb.sleep(2.2) + if not sb.is_element_visible("img#captcha-success"): + sb.solve_captcha() + sb.sleep(2.2) + sb.quit() + + +if __name__ == "__main__": + urls = ["https://seleniumbase.io/apps/turnstile" for i in range(5)] + with decorators.print_runtime("raw_multi_captcha.py"): + with ThreadPoolExecutor(max_workers=len(urls)) as executor: + for url in urls: + executor.submit(main, url) diff --git a/examples/cdp_mode/raw_multi_cdp.py b/examples/cdp_mode/raw_multi_cdp.py new file mode 100644 index 00000000000..20c464d2781 --- /dev/null +++ b/examples/cdp_mode/raw_multi_cdp.py @@ -0,0 +1,24 @@ +# Testing multiple CDP drivers using the sync API +from concurrent.futures import ThreadPoolExecutor +from random import randint +from seleniumbase import decorators +from seleniumbase import sb_cdp + + +def main(url): + sb = sb_cdp.Chrome(lang="en") + sb.goto(url) + sb.set_window_rect(randint(4, 680), randint(8, 380), 840, 520) + sb.press_keys("input", "Text") + sb.highlight("button") + sb.click("button") + sb.sleep(2) + sb.quit() + + +if __name__ == "__main__": + urls = ["https://seleniumbase.io/demo_page" for i in range(5)] + with decorators.print_runtime("raw_multi_cdp.py"): + with ThreadPoolExecutor(max_workers=len(urls)) as executor: + for url in urls: + executor.submit(main, url) diff --git a/examples/cdp_mode/raw_muse.py b/examples/cdp_mode/raw_muse.py new file mode 100644 index 00000000000..220d4601ae4 --- /dev/null +++ b/examples/cdp_mode/raw_muse.py @@ -0,0 +1,11 @@ +"""(Bypasses Friendly Captcha)""" +from seleniumbase import SB + +with SB(uc=True, test=True, guest=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://muse.jhu.edu/verify") + sb.sleep(0.6) + sb.solve_captcha() + sb.sleep(4) + sb.assert_element('#search_input') + sb.sleep(2.5) diff --git a/examples/cdp_mode/raw_muse_async.py b/examples/cdp_mode/raw_muse_async.py new file mode 100644 index 00000000000..490e81d712b --- /dev/null +++ b/examples/cdp_mode/raw_muse_async.py @@ -0,0 +1,17 @@ +"""(Bypasses Friendly Captcha)""" +import asyncio +from seleniumbase import cdp_driver + + +async def main(): + url = "https://muse.jhu.edu/verify" + driver = await cdp_driver.start_async(guest=True) + page = await driver.get(url) + await page.sleep(1.2) + await page.solve_captcha() + await page.sleep(4) + await page.find('#search_input') + await page.sleep(2.5) + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/cdp_mode/raw_mycdp_cookies.py b/examples/cdp_mode/raw_mycdp_cookies.py new file mode 100644 index 00000000000..1625a7a0d97 --- /dev/null +++ b/examples/cdp_mode/raw_mycdp_cookies.py @@ -0,0 +1,8 @@ +import mycdp +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode("https://learn.microsoft.com/en-us/") + tab = sb.get_active_tab() + loop = sb.get_event_loop() + print(loop.run_until_complete(tab.send(mycdp.storage.get_cookies()))) diff --git a/examples/cdp_mode/raw_nevada_search.py b/examples/cdp_mode/raw_nevada_search.py new file mode 100644 index 00000000000..fb66ebd0805 --- /dev/null +++ b/examples/cdp_mode/raw_nevada_search.py @@ -0,0 +1,20 @@ +"""Business Entity Search / Bypasses hCaptcha.""" +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.nvsilverflume.gov/home") + sb.sleep(3) + sb.click('a[href="/redirectToCenuity/be"]') + sb.sleep(3.6) + sb.assert_element('label:contains("Business Search")') + sb.click('input#BusinessSearch_Index_rdContains') + sb.sleep(0.6) + name_field = 'input[data-automation-id*="EntityName"]' + search = "Laser Tag" + sb.press_keys(name_field, search + "\n") + sb.sleep(6.5) + print('*** Business Search for "%s":' % search) + businesses = sb.select_all("td a[onclick]") + for business in businesses: + print(business.text) diff --git a/examples/cdp_mode/raw_nike.py b/examples/cdp_mode/raw_nike.py new file mode 100644 index 00000000000..2a8ebc77cbc --- /dev/null +++ b/examples/cdp_mode/raw_nike.py @@ -0,0 +1,18 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en", pls="none") as sb: + sb.activate_cdp_mode() + sb.goto("https://www.nike.com/") + sb.sleep(2.5) + sb.click('[data-testid="user-tools-container"] search') + sb.sleep(1.5) + search = "Nike Air Force 1" + sb.press_keys('input[type="search"]', search) + sb.sleep(4) + details = 'ul[data-testid*="products"] figure .details' + elements = sb.select_all(details) + if elements: + print('**** Found results for "%s": ****' % search) + for element in elements: + print("* " + element.text) + sb.sleep(2) diff --git a/examples/cdp_mode/raw_nordstrom.py b/examples/cdp_mode/raw_nordstrom.py new file mode 100644 index 00000000000..5d03acbc23f --- /dev/null +++ b/examples/cdp_mode/raw_nordstrom.py @@ -0,0 +1,24 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en") as sb: + sb.activate_cdp_mode() + sb.goto("https://www.nordstrom.com/") + sb.sleep(2.2) + sb.click("input#keyword-search-input") + sb.sleep(0.8) + search = "cocktail dresses for women teal" + sb.press_keys("input#keyword-search-input", search + "\n") + sb.sleep(2.2) + print('*** Nordstrom Search for "%s":' % search) + unique_item_text = [] + items = sb.find_elements("article") + for item in items: + description = item.query_selector("article h3") + if description and description.text not in unique_item_text: + unique_item_text.append(description.text) + price_text = "" + price = item.query_selector('div div span[aria-hidden="true"]') + if price: + price_text = price.text + print("* %s (%s)" % (description.text, price_text)) + item.flash(color="44CC88") diff --git a/examples/cdp_mode/raw_patent.py b/examples/cdp_mode/raw_patent.py new file mode 100644 index 00000000000..30744663c55 --- /dev/null +++ b/examples/cdp_mode/raw_patent.py @@ -0,0 +1,10 @@ +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.lens.org/lens/patent/135-034-272-112-366/frontpage") + sb.sleep(4) + sb.solve_captcha() + sb.flash('[ng-if*="patent.title"]', duration=3, pause=2) + print("* " + sb.get_text('[ng-if*="patent.title"]') + " *") + print(sb.get_text("ol.claims")) diff --git a/examples/cdp_mode/raw_pixelscan.py b/examples/cdp_mode/raw_pixelscan.py new file mode 100644 index 00000000000..a1f9422f1af --- /dev/null +++ b/examples/cdp_mode/raw_pixelscan.py @@ -0,0 +1,16 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, guest=True) as sb: + sb.activate_cdp_mode(ad_block=True) + sb.goto("https://pixelscan.net/fingerprint-check") + sb.remove_element("div.header-ad") + sb.remove_element("pxlscn-dynamic-ad") + sb.sleep(1.8) + sb.assert_text("No automated behavior", "pxlscn-bot-detection") + sb.assert_text("No masking detected", "pxlscn-fingerprint-masking") + sb.assert_text("consistent", "span.status-success") + sb.sleep(0.5) + sb.cdp.highlight("span.status-success") + sb.cdp.highlight("pxlscn-fingerprint-masking p") + sb.cdp.highlight("pxlscn-bot-detection p") + print("Bot Not Detected") diff --git a/examples/cdp_mode/raw_planetmc.py b/examples/cdp_mode/raw_planetmc.py new file mode 100644 index 00000000000..f37c97419f9 --- /dev/null +++ b/examples/cdp_mode/raw_planetmc.py @@ -0,0 +1,9 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, guest=True) as sb: + sb.activate_cdp_mode() + sb.goto("www.planetminecraft.com/account/sign_in/") + sb.sleep(3) + sb.solve_captcha() + sb.wait_for_element_absent("input[disabled]") + sb.sleep(2) diff --git a/examples/cdp_mode/raw_pokemon.py b/examples/cdp_mode/raw_pokemon.py new file mode 100644 index 00000000000..a084bc5e928 --- /dev/null +++ b/examples/cdp_mode/raw_pokemon.py @@ -0,0 +1,39 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en", ad_block=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.pokemon.com/us/pokedex") + sb.sleep(1.5) + sb.click_if_visible("button#onetrust-accept-btn-handler") + sb.sleep(1.5) + sb.click('b:contains("Show Advanced Search")') + sb.sleep(2.5) + sb.click('span[data-type="type"][data-value="electric"]') + sb.sleep(0.7) + sb.scroll_into_view("a#advSearch") + sb.sleep(0.7) + sb.click("a#advSearch") + sb.sleep(1.2) + sb.click('img[src*="img/pokedex/detail/025.png"]') + sb.assert_text("Pikachu", 'div[class*="title"]') + sb.assert_element('img[alt="Pikachu"]') + sb.scroll_into_view("div.pokemon-ability-info") + sb.sleep(1.2) + sb.flash('div[class*="title"]') + sb.flash('img[alt="Pikachu"]') + sb.flash("div.pokemon-ability-info") + name = sb.get_text("label.styled-select") + info = sb.get_text("div.version-descriptions p.active") + print("*** %s: ***\n* %s" % (name, info)) + sb.sleep(2) + sb.highlight_overlay("div.pokemon-ability-info") + sb.sleep(2) + sb.goto("https://events.pokemon.com/EventLocator/") + sb.sleep(2) + sb.click('span:contains("Championship")') + sb.sleep(2) + events = sb.select_all("div.event-info__title") + print("*** Pokรฉmon Championship Events: ***") + for event in events: + print("* " + event.text) + sb.sleep(2) diff --git a/examples/cdp_mode/raw_priceline.py b/examples/cdp_mode/raw_priceline.py new file mode 100644 index 00000000000..cbb4818268d --- /dev/null +++ b/examples/cdp_mode/raw_priceline.py @@ -0,0 +1,50 @@ +"""An example of bypassing PerimeterX detection on Priceline.""" +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en", guest=True, pls="none") as sb: + sb.activate_cdp_mode() + sb.goto("https://www.priceline.com") + sb.sleep(3) + input_selector = "div.location-input input" + sb.click(input_selector) + location = "Portland, OR" + selection = "Oregon, United States" # (Dropdown option) + sb.press_keys(input_selector, location) + sb.sleep(0.5) + sb.click(selection) + sb.sleep(0.4) + sb.scroll_down(25) + sb.sleep(0.2) + overlay_close = '[aria-label*="Overlay"] [title="Close"]' + calendar_close = 'button[aria-label="Dismiss calendar"]' + if not sb.is_element_visible(calendar_close): + calendar_close = '[data-mode="range"] span.px-1' + sb.click_if_visible(overlay_close) + sb.sleep(0.3) + sb.click(calendar_close) + sb.sleep(0.3) + sb.click_if_visible(overlay_close) + sb.sleep(0.3) + sb.click('form button[type="submit"]') + sb.sleep(4.8) + if len(sb.get_tabs()) > 1: + sb.close_active_tab() + sb.switch_to_newest_tab() + sb.sleep(0.6) + sb.sleep(0.8) + titles = [] + count = 0 + for y in range(1, 10): + sb.scroll_to_y(y * 500) + hotels = sb.find_elements('div[data-vis-key*="content"]') + for hotel in hotels: + title = hotel.query_selector("h3") + if title and title not in titles: + titles.append(title) + price = hotel.query_selector(".text-heading4") + if price: + count += 1 + price_text = price.text.replace(" ", "") + print("* %s: %s => %s" % (count, title.text, price_text)) + if not count: + print("No availability over the selected dates!") diff --git a/examples/cdp_mode/raw_print_to_pdf.py b/examples/cdp_mode/raw_print_to_pdf.py new file mode 100644 index 00000000000..e4b4e5901d2 --- /dev/null +++ b/examples/cdp_mode/raw_print_to_pdf.py @@ -0,0 +1,10 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, pls="none") as sb: + sb.activate_cdp_mode() + sb.goto("https://seleniumbase.io") + sb.assert_title("SeleniumBase Docs") + file_path = "downloaded_files/sb.pdf" + sb.print_to_pdf(file_path) + sb.assert_downloaded_file("sb.pdf") + sb.assert_pdf_text(file_path, "SeleniumBase") diff --git a/examples/cdp_mode/raw_proxy.py b/examples/cdp_mode/raw_proxy.py new file mode 100644 index 00000000000..526339b082a --- /dev/null +++ b/examples/cdp_mode/raw_proxy.py @@ -0,0 +1,52 @@ +from seleniumbase import decorators +from seleniumbase import sb_cdp + +# Change this to "ip:port" or "user:pass@ip:port" +proxy = None + + +@decorators.print_runtime("CDP Proxy Example") +def main(): + sb = sb_cdp.Chrome(lang="en", pls="none", proxy=proxy) + sb.goto("https://api.ipify.org/") + ip_address = sb.get_text("body") + if "ERR" in ip_address: + raise Exception("Failed to determine IP Address!") + print("\n\nMy IP Address = %s\n" % ip_address) + sb.goto("https://ipinfo.io/%s" % ip_address) + sb.sleep(2) + sb.wait_for_text(ip_address, "h1", timeout=20) + sb.wait_for_element('[href="/signup"]') + sb.wait_for_text("Hosted domains", timeout=20) + sb.highlight("h1") + pop_up = '[role="dialog"] span.cursor-pointer' + sb.click_if_visible(pop_up) + sb.highlight("section#summary") + sb.click_if_visible(pop_up) + sb.highlight("section#geolocation") + sb.click_if_visible(pop_up) + sb.sleep(2) + print("Displaying Host Info:") + text = sb.get_text("#summary").split("Hosted domains")[0] + rows = text.split("\n") + data = [] + for row in rows: + if row.strip() != "": + data.append(row.strip()) + print("\n".join(data).replace('\n"', ' "')) + print("\nDisplaying GeoLocation Info:") + text = sb.get_text("#geolocation") + text = text.split("IP Geolocation data")[0] + rows = text.split("\n") + data = [] + for row in rows: + if row.strip() != "": + data.append(row.strip()) + print("\n".join(data).replace('\n"', ' "')) + sb.click_if_visible(pop_up) + sb.sleep(3) + sb.quit() + + +if __name__ == "__main__": + main() diff --git a/examples/cdp_mode/raw_publication.py b/examples/cdp_mode/raw_publication.py new file mode 100644 index 00000000000..efcec71ec45 --- /dev/null +++ b/examples/cdp_mode/raw_publication.py @@ -0,0 +1,16 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en", ad_block=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.researchgate.net/search/publication") + sb.sleep(2.2) + shadow_head = "div.main-content div" + if sb.is_element_present(shadow_head): + sb.gui_click_element(shadow_head) + sb.sleep(1.5) + sb.assert_text("Discover the world's scientific knowledge") + sb.click_if_visible('button[aria-label="Close"]') + sb.highlight('h1[class*="nova"]') + sb.highlight('input[name="q"]') + sb.highlight("a.menu-item.selected") + sb.sleep(1) diff --git a/examples/cdp_mode/raw_radwell.py b/examples/cdp_mode/raw_radwell.py new file mode 100644 index 00000000000..df329526ae5 --- /dev/null +++ b/examples/cdp_mode/raw_radwell.py @@ -0,0 +1,19 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en", incognito=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.radwell.com/en-US/Search/Advanced/") + sb.sleep(1) + sb.press_keys("form#basicsearch input", "821C-PM-111DA-142") + sb.click('[value="Search Icon"]') + sb.sleep(2) + if not sb.is_element_visible("a.manufacturer-link"): + sb.solve_captcha() + sb.sleep(0.5) + sb.assert_text("MAC VALVES INC", "a.manufacturer-link") + sb.highlight("a.manufacturer-link") + description = sb.get_text("div.product-information") + description = description.replace(" ", "") + description = description.replace("\n \n", "\n") + print(description) + sb.sleep(1) diff --git a/examples/cdp_mode/raw_ralphlauren.py b/examples/cdp_mode/raw_ralphlauren.py new file mode 100644 index 00000000000..913b897d9b2 --- /dev/null +++ b/examples/cdp_mode/raw_ralphlauren.py @@ -0,0 +1,25 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en", guest=True) as sb: + sb.activate_cdp_mode() + sb.goto("www.ralphlauren.com.au/women/clothing/dresses-and-jumpsuits") + sb.sleep(2.2) + if not sb.is_element_present("div.product-data"): + sb.evaluate("window.location.reload();") + sb.sleep(2.2) + print("*** Ralph Lauren Search for Dresses:") + unique_item_text = [] + soup = sb.get_beautiful_soup() + items = soup.select('div.product-data') + for item in items: + description_element = item.select_one("a.name-link") + if description_element: + description_text = description_element.get_text(strip=True) + if description_text not in unique_item_text: + unique_item_text.append(description_text) + print(f"* {description_text}") + price_element = item.select_one('span[title="Price"]') + if price_element: + price_text = " ".join(price_element.get_text().split()) + print(f" ({price_text})") + sb.scroll_down(20) diff --git a/examples/cdp_mode/raw_reddit.py b/examples/cdp_mode/raw_reddit.py new file mode 100644 index 00000000000..d55bbb8fa6a --- /dev/null +++ b/examples/cdp_mode/raw_reddit.py @@ -0,0 +1,17 @@ +"""Reddit Search / Bypasses reCAPTCHA.""" +from seleniumbase import SB + +with SB(uc=True, test=True, use_chromium=True) as sb: + search = "reddit+scraper" + url = f"https://www.reddit.com/r/webscraping/search/?q={search}" + sb.activate_cdp_mode(url) + sb.solve_captcha() # Might not be needed + post_title = '[data-testid="post-title"]' + sb.wait_for_element(post_title) + for i in range(8): + sb.scroll_down(25) + sb.sleep(0.2) + posts = sb.select_all(post_title) + print('*** Reddit Posts for "%s":' % search) + for post in posts: + print("* " + post.text) diff --git a/examples/cdp_mode/raw_reddit_async.py b/examples/cdp_mode/raw_reddit_async.py new file mode 100644 index 00000000000..430c03a3b6f --- /dev/null +++ b/examples/cdp_mode/raw_reddit_async.py @@ -0,0 +1,23 @@ +import asyncio +from seleniumbase import cdp_driver + + +async def main(): + search = "reddit+scraper" + url = f"https://www.reddit.com/r/webscraping/search/?q={search}" + driver = await cdp_driver.start_async(use_chromium=True) + page = await driver.get(url) + await page.solve_captcha() # Might not be needed + post_title = '[data-testid="post-title"]' + await page.select(post_title) + for i in range(8): + await page.scroll_down(25) + await page.sleep(0.2) + posts = await page.select_all(post_title) + print('*** Reddit Posts for "%s":' % search) + for post in posts: + print("* " + post.text) + driver.stop() + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/cdp_mode/raw_req_async.py b/examples/cdp_mode/raw_req_async.py new file mode 100644 index 00000000000..eeb5c6fc42f --- /dev/null +++ b/examples/cdp_mode/raw_req_async.py @@ -0,0 +1,49 @@ +"""Using CDP.fetch.RequestPaused to filter content in real-time.""" +import asyncio +import colorama +import mycdp +import sys +from seleniumbase import decorators +from seleniumbase import cdp_driver + +c1 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX +c2 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX +cr = colorama.Style.RESET_ALL +if "linux" in sys.platform: + c1 = c2 = cr = "" + + +class RequestPausedTest(): + async def request_paused_handler(self, event, tab): + r = event.request + is_image = ".png" in r.url or ".jpg" in r.url or ".gif" in r.url + if not is_image: # Let the data through + tab.feed_cdp( + mycdp.fetch.continue_request(request_id=event.request_id) + ) + else: # Block the data (images) + TIMED_OUT = mycdp.network.ErrorReason.TIMED_OUT + s = f"{c1}BLOCKING{cr} | {c2}{r.method}{cr} | {r.url}" + print(f" >>> ------------\n{s}") + tab.feed_cdp( + mycdp.fetch.fail_request(event.request_id, TIMED_OUT) + ) + + async def start_test(self): + driver = await cdp_driver.start_async() + tab = await driver.get("about:blank") + tab.add_handler(mycdp.fetch.RequestPaused, self.request_paused_handler) + url = "https://gettyimages.com/photos/firefly-2003-nathan" + await driver.get(url) + await asyncio.sleep(5) + driver.stop() + + +@decorators.print_runtime("RequestPausedTest") +def main(): + test = RequestPausedTest() + asyncio.run(test.start_test()) + + +if __name__ == "__main__": + main() diff --git a/examples/cdp_mode/raw_req_mod.py b/examples/cdp_mode/raw_req_mod.py new file mode 100644 index 00000000000..f59ea6e4ab3 --- /dev/null +++ b/examples/cdp_mode/raw_req_mod.py @@ -0,0 +1,23 @@ +"""Using CDP.fetch.RequestPaused to modify requests in real-time.""" +import mycdp +from seleniumbase import SB + + +async def request_paused_handler(event, tab): + r = event.request + rid = event.request_id + is_image = ".png" in r.url or ".jpg" in r.url or ".gif" in r.url + if not is_image: # Let the data through + tab.feed_cdp(mycdp.fetch.continue_request(request_id=rid)) + else: # Modify the data (Change the image URL) + new_url = "https://seleniumbase.io/other/with_frakes.jpg" + tab.feed_cdp(mycdp.fetch.continue_request(request_id=rid, url=new_url)) + + +with SB(uc=True, test=True, locale="en", pls="none") as sb: + sb.activate_cdp_mode("about:blank") + sb.add_handler(mycdp.fetch.RequestPaused, request_paused_handler) + sb.goto("https://gettyimages.com/photos/jonathan-frakes-cast-2022") + new_size = "--width:100;--height:100;" + sb.set_attributes('[style*="--width:"]', "style", new_size) + sb.sleep(6) diff --git a/examples/cdp_mode/raw_req_sb.py b/examples/cdp_mode/raw_req_sb.py new file mode 100644 index 00000000000..121ffd3f2a1 --- /dev/null +++ b/examples/cdp_mode/raw_req_sb.py @@ -0,0 +1,31 @@ +"""Using CDP.fetch.RequestPaused to filter content in real-time.""" +import colorama +import mycdp +import sys +from seleniumbase import SB + +c1 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX +c2 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX +cr = colorama.Style.RESET_ALL +if "linux" in sys.platform: + c1 = c2 = cr = "" + + +async def request_paused_handler(event, tab): + r = event.request + is_image = ".png" in r.url or ".jpg" in r.url or ".gif" in r.url + if not is_image: # Let the data through + tab.feed_cdp(mycdp.fetch.continue_request(request_id=event.request_id)) + else: # Block the data (images) + TIMED_OUT = mycdp.network.ErrorReason.TIMED_OUT + s = f"{c1}BLOCKING{cr} | {c2}{r.method}{cr} | {r.url}" + print(f" >>> ------------\n{s}") + tab.feed_cdp(mycdp.fetch.fail_request(event.request_id, TIMED_OUT)) + + +with SB(uc=True, test=True, locale="en") as sb: + sb.activate_cdp_mode("about:blank") + sb.add_handler(mycdp.fetch.RequestPaused, request_paused_handler) + url = "https://gettyimages.com/photos/firefly-2003-nathan" + sb.goto(url) + sb.sleep(4) diff --git a/examples/cdp_mode/raw_res_nike.py b/examples/cdp_mode/raw_res_nike.py new file mode 100644 index 00000000000..987a42f1b86 --- /dev/null +++ b/examples/cdp_mode/raw_res_nike.py @@ -0,0 +1,43 @@ +"""Using CDP.network.RequestWillBeSent and CDP.network.ResponseReceived.""" +import colorama +import mycdp +import sys +from seleniumbase import SB + +c1 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX +c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX +cr = colorama.Style.RESET_ALL +if "linux" in sys.platform: + c1 = c2 = cr = "" + + +async def send_handler(event: mycdp.network.RequestWillBeSent): + r = event.request + s = f"{r.method} {r.url}" + for k, v in r.headers.items(): + s += f"\n\t{k} : {v}" + print(c1 + "*** ==> RequestWillBeSent <== ***" + cr) + print(s) + + +async def receive_handler(event: mycdp.network.ResponseReceived): + print(c2 + "*** ==> ResponseReceived <== ***" + cr) + print(event.response) + + +with SB(uc=True, test=True, locale="en", pls="none") as sb: + sb.activate_cdp_mode() + sb.goto("https://www.nike.com/") + sb.add_handler(mycdp.network.RequestWillBeSent, send_handler) + sb.add_handler(mycdp.network.ResponseReceived, receive_handler) + sb.sleep(2.5) + sb.click('[data-testid="user-tools-container"] search') + sb.sleep(1.5) + search = "Nike Air Force 1" + sb.press_keys('input[type="search"]', search) + sb.sleep(4) + elements = sb.select_all('ul[data-testid*="products"] figure .details') + if elements: + print('**** Found results for "%s": ****' % search) + for element in elements: + print("* " + element.text) diff --git a/examples/cdp_mode/raw_res_sb.py b/examples/cdp_mode/raw_res_sb.py new file mode 100644 index 00000000000..0459c6f0f67 --- /dev/null +++ b/examples/cdp_mode/raw_res_sb.py @@ -0,0 +1,34 @@ +"""Using CDP.network.RequestWillBeSent and CDP.network.ResponseReceived.""" +import colorama +import mycdp +import sys +from seleniumbase import SB + +c1 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX +c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX +cr = colorama.Style.RESET_ALL +if "linux" in sys.platform: + c1 = c2 = cr = "" + + +async def send_handler(event: mycdp.network.RequestWillBeSent): + r = event.request + s = f"{r.method} {r.url}" + for k, v in r.headers.items(): + s += f"\n\t{k} : {v}" + print(c1 + "*** ==> RequestWillBeSent <== ***" + cr) + print(s) + + +async def receive_handler(event: mycdp.network.ResponseReceived): + print(c2 + "*** ==> ResponseReceived <== ***" + cr) + print(event.response) + + +with SB(uc=True, test=True, locale="en") as sb: + sb.activate_cdp_mode("about:blank") + sb.add_handler(mycdp.network.RequestWillBeSent, send_handler) + sb.add_handler(mycdp.network.ResponseReceived, receive_handler) + url = "https://seleniumbase.io/apps/calculator" + sb.goto(url) + sb.sleep(1) diff --git a/examples/cdp_mode/raw_reuse_browser.py b/examples/cdp_mode/raw_reuse_browser.py new file mode 100644 index 00000000000..6706af0fdfc --- /dev/null +++ b/examples/cdp_mode/raw_reuse_browser.py @@ -0,0 +1,9 @@ +"""Test connecting to an existing browser.""" +from seleniumbase import sb_cdp + +sb1 = sb_cdp.Chrome("https://example.com") +port = sb1.get_rd_port() +sb2 = sb_cdp.Chrome(host="127.0.0.1", port=port) +print("The remote-debugging port: %s" % port) +assert sb1.get_rd_port() == sb2.get_rd_port() +assert sb1.get_current_url() == sb2.get_current_url() diff --git a/examples/cdp_mode/raw_sannysoft.py b/examples/cdp_mode/raw_sannysoft.py new file mode 100644 index 00000000000..e5876b4c3f5 --- /dev/null +++ b/examples/cdp_mode/raw_sannysoft.py @@ -0,0 +1,18 @@ +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://bot.sannysoft.com/") + sb.flash("#user-agent-result.passed") + sb.flash("#webdriver-result.passed") + sb.flash("#advanced-webdriver-result.passed") + sb.flash("#chrome-result.passed") + sb.flash("#permissions-result.passed") + sb.flash("#plugins-length-result.passed") + sb.flash("#plugins-type-result.passed") + sb.flash("#languages-result.passed") + sb.flash("#webgl-vendor.passed") + sb.flash("#webgl-renderer.passed") + sb.flash("#broken-image-dimensions.passed") + print("Bot Not Detected") + sb.sleep(1) diff --git a/examples/cdp_mode/raw_science.py b/examples/cdp_mode/raw_science.py new file mode 100644 index 00000000000..9bcfcf110b2 --- /dev/null +++ b/examples/cdp_mode/raw_science.py @@ -0,0 +1,15 @@ +from seleniumbase import SB + +with SB(uc=True, incognito=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://earth.esa.int/eogateway/search") + sb.sleep(1) + sb.click_if_visible('button:contains("Accept cookies")', timeout=3) + for i in range(20): + sb.scroll_to_bottom() + sb.click_if_visible('button:contains("READ MORE")') + sb.sleep(1) + elements = sb.find_elements("h4 a span") + for element in elements: + print(element.text) + print("*** Total entries: %s" % len(elements)) diff --git a/examples/cdp_mode/raw_seatgeek.py b/examples/cdp_mode/raw_seatgeek.py new file mode 100644 index 00000000000..61a976f88f3 --- /dev/null +++ b/examples/cdp_mode/raw_seatgeek.py @@ -0,0 +1,17 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, ad_block=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://seatgeek.com/") + input_field = 'input[name="search"]' + sb.wait_for_element(input_field) + sb.sleep(1.6) + query = "Jerry Seinfeld" + sb.press_keys(input_field, query) + sb.sleep(1.8) + sb.click("li#active-result-item") + sb.sleep(4.4) + print('*** SeatGeek Search for "%s":' % query) + item_selector = '[data-testid="listing-item"]' + for item in sb.find_elements(item_selector): + print(item.text) diff --git a/examples/cdp_mode/raw_sms_man.py b/examples/cdp_mode/raw_sms_man.py new file mode 100644 index 00000000000..10fe6a3bb99 --- /dev/null +++ b/examples/cdp_mode/raw_sms_man.py @@ -0,0 +1,13 @@ +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://sms-man.com/login") + sb.sleep(3) + if not sb.is_element_present('input[name="email"]'): + sb.solve_captcha() + sb.sleep(1) + sb.wait_for_element('[name="email"]', timeout=3) + sb.sleep(2) + sb.solve_captcha() + sb.sleep(2) diff --git a/examples/cdp_mode/raw_socialblade.py b/examples/cdp_mode/raw_socialblade.py new file mode 100644 index 00000000000..94ddfa12438 --- /dev/null +++ b/examples/cdp_mode/raw_socialblade.py @@ -0,0 +1,31 @@ +"""Bypass bot-detection to view SocialBlade ranks for YouTube""" +from seleniumbase import SB + +with SB(uc=True, test=True, ad_block=True, pls="none") as sb: + sb.activate_cdp_mode() + sb.goto("https://socialblade.com/") + sb.sleep(1.5) + if not sb.is_element_visible('input[placeholder*="Search"]'): + sb.solve_captcha() + sb.sleep(0.5) + channel_name = "michaelmintz" + channel_title = "Michael Mintz" + sb.press_keys('input[placeholder*="Search"]', channel_name) + sb.sleep(2) + sb.click('a:contains("%s")' % channel_title) + sb.sleep(2) + sb.remove_elements("#lngtd-top-sticky") + sb.sleep(1.5) + name = sb.get_text("h3") + ch_name = name.split(" ")[-1] + name = name.split(" @")[0] + link = "https://www.youtube.com/%s" % ch_name + print("********** SocialBlade Stats for %s: **********" % name) + print(">>> (Link: %s) <<<" % link) + print(sb.get_text('[class*="grid lg:hidden"]')) + print("********** SocialBlade Ranks: **********") + print(sb.get_text('[class*="gap-3 flex-1"]')) + for i in range(17): + sb.scroll_down(6) + sb.sleep(0.1) + sb.sleep(2) diff --git a/examples/cdp_mode/raw_softpedia.py b/examples/cdp_mode/raw_softpedia.py new file mode 100644 index 00000000000..4d37c5c40a1 --- /dev/null +++ b/examples/cdp_mode/raw_softpedia.py @@ -0,0 +1,26 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, ad_block=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.softpedia.com/") + search_box = 'input[name="search_term"]' + search = "3D Model Lab" + sb.click(search_box) + sb.press_keys(search_box, search + "\n") + sb.sleep(2) + sb.remove_elements("#adcontainer1") + sb.sleep(2.5) + print('*** Softpedia Search for "%s":' % search) + links = [] + item_container = 'div[style="min-height:100px;"]' + sb.wait_for_element(item_container) + items = sb.find_elements(item_container) + for item in items: + result = item.query_selector("h4 a") + links.append(result.get_attribute("href")) + print("* " + result.text) + print(item.query_selector("p").get_attribute("title")) + for link in links: + sb.goto(link) + sb.remove_elements("div.ad") + sb.sleep(2) diff --git a/examples/cdp_mode/raw_southwest.py b/examples/cdp_mode/raw_southwest.py new file mode 100644 index 00000000000..529df5c50b2 --- /dev/null +++ b/examples/cdp_mode/raw_southwest.py @@ -0,0 +1,48 @@ +"""An example of bypassing bot-detection on Southwest.com. +(PyAutoGUI is installed at runtime if it's not installed.)""" +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en", ad_block=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.southwest.com/air/booking/") + sb.sleep(2.8) + origin = "DEN" + destination = "LAX" + sb.click_if_visible("button#onetrust-accept-btn-handler") + sb.sleep(0.5) + sb.gui_click_element("input#originationAirportCode") + sb.sleep(0.2) + sb.clear("input#originationAirportCode") + sb.uc_gui_press_keys(" " + "\n") + sb.sleep(0.5) + sb.gui_click_element("input#originationAirportCode") + sb.sleep(0.4) + sb.uc_gui_press_keys(origin + "\n") + sb.sleep(0.4) + sb.gui_click_element("h1") + sb.gui_click_element("input#destinationAirportCode") + sb.sleep(0.2) + sb.clear("input#destinationAirportCode") + sb.uc_gui_press_keys(destination + "\n") + sb.sleep(0.4) + sb.gui_click_element("h1") + sb.click_if_visible("button#onetrust-accept-btn-handler") + sb.sleep(0.1) + sb.click('form button[data-test="submitField"]') + sb.sleep(2.5) + sb.click('button[aria-labelledby*="nearby-airport-drawer-"]') + sb.sleep(4) + day = sb.get_text('[aria-current="true"] span[class*="cal"]') + print("**** Flights from %s to %s ****" % (origin, destination)) + flights = sb.find_elements("li.air-booking-select-detail") + for flight in flights: + info = flight.text + departs = info.split("Departs")[-1].split("M")[0].strip() + "M" + arrives = info.split("Arrives")[-1].split("M")[0].strip() + "M" + stops = flight.query_selector(".flight-stops-badge").text + duration = flight.query_selector('[class*="flight-duration"]').text + p_elm = flight.query_selector('span.currency span[aria-hidden]') + if not p_elm: + continue + price = p_elm.text + print(f"* {day}, {departs} -> {arrives} ({stops}: {duration}) {price}") diff --git a/examples/cdp_mode/raw_stopandshop.py b/examples/cdp_mode/raw_stopandshop.py new file mode 100644 index 00000000000..40b94650a84 --- /dev/null +++ b/examples/cdp_mode/raw_stopandshop.py @@ -0,0 +1,34 @@ +"""Test Stop & Shop search. Non-US IPs might be blocked.""" +from seleniumbase import SB + +with SB(uc=True, test=True, guest=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://stopandshop.com/") + sb.sleep(1.6) + sb.solve_captcha() + sb.sleep(1.8) + sb.click_if_visible("#optly-popup-refresh-btn") + query = "Fresh Turkey" + required_text = "Turkey" + search_box = 'input[type="search"]' + sb.wait_for_element(search_box) + sb.sleep(1.2) + sb.press_keys(search_box, query) + sb.sleep(1.2) + sb.click("button.search-btn") + sb.sleep(3.2) + print('*** Stop & Shop Search for "%s":' % query) + print(' (Results must contain "%s".)' % required_text) + print(' (Results cannot contain "Out of Stock")') + unique_item_text = [] + item_selector = "div.product-tile_content" + items = sb.find_elements(item_selector) + for item in items: + sb.sleep(0.06) + if "Out of Stock" not in item.text: + if required_text in item.text: + item.flash(color="44CC88") + sb.sleep(0.025) + if item.text not in unique_item_text: + unique_item_text.append(item.text) + print("* " + item.text) diff --git a/examples/cdp_mode/raw_tab_switching.py b/examples/cdp_mode/raw_tab_switching.py new file mode 100644 index 00000000000..66fe0628607 --- /dev/null +++ b/examples/cdp_mode/raw_tab_switching.py @@ -0,0 +1,15 @@ +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("data:text/html,

Page A

") + sb.assert_text("Page A") + sb.open_new_tab() + sb.goto("data:text/html,

Page B

") + sb.assert_text("Page B") + sb.switch_to_tab(0) + sb.assert_text("Page A") + sb.assert_text_not_visible("Page B") + sb.switch_to_tab(1) + sb.assert_text("Page B") + sb.assert_text_not_visible("Page A") diff --git a/examples/cdp_mode/raw_target.py b/examples/cdp_mode/raw_target.py new file mode 100644 index 00000000000..403c12a111c --- /dev/null +++ b/examples/cdp_mode/raw_target.py @@ -0,0 +1,32 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, ad_block=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.target.com/") + sb.sleep(1.5) + sb.click("input#search") + sb.sleep(0.5) + search = "Settlers of Catan Board Game" + required_text = "Catan" + sb.type("input#search", search) + sb.sleep(0.5) + sb.click('button[aria-label="search"]') + sb.sleep(2.5) + print('*** Target Search for "%s":' % search) + print(' (Results must contain "%s".)' % required_text) + unique_item_text = [] + items = sb.find_elements( + '[data-test*="ProductListing"], [data-test="product-details"]' + ) + for item in items: + if required_text.lower() in item.text.lower(): + description = item.query_selector('h3, a[data-test*="Card/title"]') + if description and description.text not in unique_item_text: + unique_item_text.append(description.text) + print("* " + description.text) + price = item.query_selector( + '[style*="weight: 700;"], [data-test="current-price"]' + ) + if price: + print(" (" + price.text + ")") + item.scroll_into_view() diff --git a/examples/cdp_mode/raw_theaters.py b/examples/cdp_mode/raw_theaters.py new file mode 100644 index 00000000000..34e5876abdd --- /dev/null +++ b/examples/cdp_mode/raw_theaters.py @@ -0,0 +1,18 @@ +"""Simple web-scraping example in CDP Mode""" +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en", ad_block=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://architectureofcities.com/roman-theaters") + sb.click_if_visible("#cn-close-notice") + sb.click_if_visible('[aria-label="Reject All"]') + sb.click_if_visible('span:contains("Continue")') + sb.sleep(1) + print("*** " + sb.get_text("h1") + " ***") + for item in sb.find_elements("h3"): + if item.text and "." in item.text: + item.flash(color="44CC88") + sb.scroll_down(34) + print("* " + item.text.replace(" ", " ")) + sb.sleep(0.15) + sb.sleep(1) diff --git a/examples/cdp_mode/raw_tiktok.py b/examples/cdp_mode/raw_tiktok.py new file mode 100644 index 00000000000..75043de866c --- /dev/null +++ b/examples/cdp_mode/raw_tiktok.py @@ -0,0 +1,19 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, guest=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.tiktok.com/@startrek?lang=en") + sb.sleep(2.5) + sb.click_if_visible('button[data-close-button="true"]') + sb.sleep(0.3) + print("*** " + sb.get_text('h2[data-e2e="user-bio"]')) + sb.click_if_visible('button:contains("Refresh")') + sb.sleep(0.3) + for i in range(33): + sb.scroll_by_y(33) + sb.sleep(0.03) + items = sb.find_elements("picture img") + for i, item in enumerate(items): + print("* %s: %s" % (i, str(item.get_attribute("alt")))) + print("*** %s total items found!" % len(items)) + sb.sleep(1) diff --git a/examples/cdp_mode/raw_timezone.py b/examples/cdp_mode/raw_timezone.py new file mode 100644 index 00000000000..dadba4f28ae --- /dev/null +++ b/examples/cdp_mode/raw_timezone.py @@ -0,0 +1,34 @@ +"""Timezone example using CDP Mode without WebDriver""" +import mycdp +from seleniumbase import decorators +from seleniumbase import sb_cdp + + +async def request_paused_handler(event, tab): + r = event.request + is_image = ".png" in r.url or ".jpg" in r.url or ".gif" in r.url + if not is_image: # Let the data through + tab.feed_cdp(mycdp.fetch.continue_request(request_id=event.request_id)) + else: # Block the data (images) + TIMED_OUT = mycdp.network.ErrorReason.TIMED_OUT + tab.feed_cdp(mycdp.fetch.fail_request(event.request_id, TIMED_OUT)) + + +@decorators.print_runtime("Timezone CDP Example") +def main(): + url = "https://www.randymajors.org/what-time-zone-am-i-in" + sb = sb_cdp.Chrome( + url, + ad_block=True, + lang="bn", + tzone="Asia/Kolkata", + geoloc=(26.855323, 80.937710) + ) + sb.add_handler(mycdp.fetch.RequestPaused, request_paused_handler) + sb.remove_elements("#right-sidebar") + sb.sleep(6) + sb.quit() + + +if __name__ == "__main__": + main() diff --git a/examples/cdp_mode/raw_timezone_sb.py b/examples/cdp_mode/raw_timezone_sb.py new file mode 100644 index 00000000000..f5e0ea2fe14 --- /dev/null +++ b/examples/cdp_mode/raw_timezone_sb.py @@ -0,0 +1,11 @@ +"""An example of changing settings during CDP Mode""" +from seleniumbase import SB + +with SB(uc=True, test=True, pls="eager", ad_block=True) as sb: + url = "https://www.randymajors.org/what-time-zone-am-i-in" + sb.activate_cdp_mode(url, tzone="Asia/Kolkata", geoloc=(26.863, 80.94)) + sb.remove_elements("#right-sidebar") + sb.sleep(5) + sb.goto(url, tzone="Asia/Tokyo", geoloc=(35.050681, 136.844728)) + sb.remove_elements("#right-sidebar") + sb.sleep(5) diff --git a/examples/cdp_mode/raw_totalwine.py b/examples/cdp_mode/raw_totalwine.py new file mode 100644 index 00000000000..9166dbdc4d2 --- /dev/null +++ b/examples/cdp_mode/raw_totalwine.py @@ -0,0 +1,34 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en") as sb: + url = "https://www.totalwine.com/" + sb.activate_cdp_mode() + sb.goto(url) + sb.sleep(1.8) + search_box = 'input[data-at="header-search-text"]' + search = "The Land by Psagot Cabernet" + sb.click_if_visible("#onetrust-close-btn-container button") + sb.sleep(0.6) + sb.click_if_visible('button[aria-label="Close modal"]') + sb.sleep(0.6) + sb.click(search_box) + sb.sleep(0.6) + sb.click_if_visible('button[aria-label="Close modal"]') + sb.sleep(0.6) + sb.press_keys(search_box, search) + sb.sleep(0.6) + sb.click_if_visible('button[aria-label="Close modal"]') + sb.sleep(0.6) + sb.click('button[data-at="header-search-button"]') + sb.sleep(1.8) + sb.click_if_visible('button[aria-label="Close modal"]') + sb.sleep(0.6) + sb.click('img[data-at="product-search-productimage"]') + sb.sleep(2.2) + print('*** Total Wine Search for "%s":' % search) + print(sb.get_text('h1[data-at="product-name-title"]')) + print(sb.get_text("#priceContainer div")) + print("Product Highlights:") + print(sb.get_text('p[class*="productInformationReview"]')) + print("Product Details:") + print(sb.get_text('div[data-at="origin-details-table-container"]')) diff --git a/examples/cdp_mode/raw_trails.py b/examples/cdp_mode/raw_trails.py new file mode 100644 index 00000000000..4dc2f180b92 --- /dev/null +++ b/examples/cdp_mode/raw_trails.py @@ -0,0 +1,42 @@ +from seleniumbase import SB + +with SB(uc=True, ad_block=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.alltrails.com/") + sb.sleep(3.5) + sb.click_if_visible("button.osano-cm-close") + sb.sleep(0.5) + search_box = 'input[aria-describedby="search-instructions"]' + search_term = "Thundering Brook Falls" + sb.wait_for_element(search_box) + sb.sleep(1.5) + sb.type(search_box, search_term + " Trail") + sb.sleep(1.5) + sb.click('a span:contains("%s")' % search_term) + sb.sleep(3.5) + sb.click('button:contains("more")') + sb.sleep(0.7) + print("Description: (%s)\n" % sb.get_text("h1")) + print(sb.get_text('div[class*="Description_description"]')) + sb.scroll_to_bottom() + sb.sleep(1.5) + sb.click_if_visible('[aria-label="Close"]') + sb.sleep(1.2) + summary = '[class*="ReviewSummary"] span' + print("\nReview Summary:\n\n%s" % sb.get_text(summary)) + reviews = sb.select_all('p[class*="styles_reviewText"]') + print("\nReviews:") + for review in reviews: + print("\n" + review.text) + folder = "images_exported" + file_name = "thundering_brook_falls.png" + sb.scroll_to_top() + sb.sleep(0.3) + sb.click_if_visible('[aria-label="Close"]') + sb.sleep(1.2) + sb.save_screenshot(file_name, folder, selector="body") + print('\n"./%s/%s" was saved!' % (folder, file_name)) + folder = "downloaded_files" + file_name = "thundering_brook_falls.html" + sb.save_page_source(file_name, folder) + print('"./%s/%s" was saved!' % (folder, file_name)) diff --git a/examples/cdp_mode/raw_turnstile.py b/examples/cdp_mode/raw_turnstile.py new file mode 100644 index 00000000000..172c6958d6c --- /dev/null +++ b/examples/cdp_mode/raw_turnstile.py @@ -0,0 +1,10 @@ +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://seleniumbase.io/apps/turnstile") + sb.sleep(0.5) + sb.solve_captcha() + sb.assert_element("img#captcha-success", timeout=3) + sb.set_messenger_theme(location="top_left") + sb.post_message("SeleniumBase wasn't detected", duration=3) diff --git a/examples/cdp_mode/raw_united.py b/examples/cdp_mode/raw_united.py new file mode 100644 index 00000000000..f40336183e1 --- /dev/null +++ b/examples/cdp_mode/raw_united.py @@ -0,0 +1,56 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en", ad_block=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.united.com/en/us") + sb.sleep(3.5) + origin_input = 'input[placeholder="Origin"]' + origin = "JFK" + destination_input = 'input[placeholder="Destination"]' + destination = "MCO" + sb.wait_for_element(origin_input, timeout=20) + sb.sleep(0.5) + sb.click(origin_input) + sb.sleep(0.5) + sb.type(origin_input, origin) + sb.sleep(1.2) + sb.click('strong:contains("%s")' % origin) + sb.sleep(0.6) + sb.click_if_visible('button[class*="__close--"]') + sb.sleep(0.6) + sb.click(destination_input) + sb.sleep(0.5) + sb.type(destination_input, destination) + sb.sleep(1.2) + sb.click('strong:contains("%s")' % destination) + sb.sleep(1.2) + sb.click('button[aria-label="Find flights"]') + sb.sleep(4) + sb.click_if_visible('button[class*="__close--"]') + sb.sleep(2) + flights = sb.find_elements('div[class*="CardContainer__block"]') + print("**** Flights from %s to %s ****" % (origin, destination)) + print(" (" + sb.get_text("h2.atm-c-heading") + ")") + if not flights: + print("* No flights found!") + for flight in flights: + flight_info = flight.text.split(" Destination")[0] + part_1 = flight_info.split(" Departing at")[0] + part_2 = flight_info.split("Arriving at ")[-1] + part_2 = part_2.split(" Duration")[0] + part_3 = flight.text.split(" Destination")[-1].split(" Aircraft")[0] + parts = "%s - %s %s" % (part_1, part_2, part_3) + print("* " + parts) + for category in ["ECONOMY"]: + prices = sb.find_elements('[aria-describedby="%s"]' % category) + full_prices = [] + for item in prices: + item_text = item.text + if "Not available" not in item.text: + full_prices.append("$%s" % item.text.split("$")[-1]) + else: + full_prices.append("N/A") + print("**** %s Prices:" % category) + print(full_prices) + sb.scroll_down(50) + sb.sleep(1.5) diff --git a/examples/cdp_mode/raw_walmart.py b/examples/cdp_mode/raw_walmart.py new file mode 100644 index 00000000000..1b47ea4fa2c --- /dev/null +++ b/examples/cdp_mode/raw_walmart.py @@ -0,0 +1,41 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, ad_block=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.walmart.com/") + sb.sleep(2.2) + continue_button = 'button:contains("Continue shopping")' + if sb.is_element_visible(continue_button): + sb.gui_click_element(continue_button) + sb.sleep(0.6) + sb.click('input[aria-label="Search"]') + sb.sleep(1.2) + search = "Settlers of Catan Board Game" + required_text = "Catan" + sb.press_keys('input[aria-label="Search"]', search + "\n") + sb.sleep(3.8) + sb.remove_elements('[data-testid="skyline-ad"]') + sb.remove_elements('[data-testid="sba-container"]') + print('*** Walmart Search for "%s":' % search) + print(' (Results must contain "%s".)' % required_text) + unique_item_text = [] + sb.click_if_visible('[data-automation-id="sb-btn-close-mark"]') + items = sb.find_elements('[data-item-id]') + for item in items: + if required_text.lower() in item.text.lower(): + description = item.query_selector( + '[data-automation-id="product-title"]' + ) + if description and description.text not in unique_item_text: + unique_item_text.append(description.text) + print("* " + description.text) + price = item.query_selector( + '[data-automation-id="product-price"]' + ) + if price: + price_text = price.text + price_text = price_text.split("current price Now ")[-1] + price_text = price_text.split("current price ")[-1] + price_text = price_text.split(" ")[0] + print(" (" + price_text + ")") + item.scroll_into_view() diff --git a/examples/cdp_mode/raw_wsform.py b/examples/cdp_mode/raw_wsform.py new file mode 100644 index 00000000000..387dba7f8ef --- /dev/null +++ b/examples/cdp_mode/raw_wsform.py @@ -0,0 +1,12 @@ +"""CDP Mode for bypassing bot-detection & CAPTCHAs. +Note: sb.uc_gui_click_captcha() requires PyAutoGUI, +which is installed automatically if not already.""" +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en", incognito=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://wsform.com/demo/") + sb.sleep(2) + sb.scroll_into_view("div.grid") + sb.uc_gui_click_captcha() # PyAutoGUI mouse click + sb.sleep(2) diff --git a/examples/cdp_mode/raw_xhr_async.py b/examples/cdp_mode/raw_xhr_async.py new file mode 100644 index 00000000000..2ee00bb054e --- /dev/null +++ b/examples/cdp_mode/raw_xhr_async.py @@ -0,0 +1,91 @@ +"""CDP.network.ResponseReceived with CDP.network.ResourceType.XHR.""" +import ast +import asyncio +import colorama +import mycdp +import sys +import time +from seleniumbase import cdp_driver + +xhr_requests = [] +last_xhr_request = None +c1 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX +c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX +cr = colorama.Style.RESET_ALL +if "linux" in sys.platform: + c1 = c2 = cr = "" + + +def listenXHR(page): + async def handler(evt): + # Get AJAX requests + if evt.type_ is mycdp.network.ResourceType.XHR: + xhr_requests.append([evt.response.url, evt.request_id]) + global last_xhr_request + last_xhr_request = time.time() + page.add_handler(mycdp.network.ResponseReceived, handler) + + +async def receiveXHR(page, requests): + responses = [] + retries = 0 + max_retries = 5 + # Wait at least 2 seconds after last XHR request for more + while True: + if last_xhr_request is None or retries > max_retries: + break + if time.time() - last_xhr_request <= 2: + retries = retries + 1 + time.sleep(2) + continue + else: + break + await page + # Loop through gathered requests and get response body + for request in requests: + try: + res = await page.send(mycdp.network.get_response_body(request[1])) + if res is None: + continue + responses.append({ + "url": request[0], + "body": res[0], + "is_base64": res[1], + }) + except Exception as e: + print("Error getting response:", e) + return responses + + +async def crawl(): + driver = await cdp_driver.start_async() + tab = await driver.get("about:blank") + listenXHR(tab) + + # Change url to something that makes ajax requests + tab = await driver.get("https://learn.microsoft.com/en-us/") + time.sleep(1.5) + for i in range(18): + await tab.scroll_down(3) + time.sleep(0.02) + + xhr_responses = await receiveXHR(tab, xhr_requests) + for response in xhr_responses: + print(c1 + "*** ==> XHR Request URL <== ***" + cr) + print(f'{response["url"]}') + is_base64 = response["is_base64"] + b64_data = "Base64 encoded data" + try: + headers = ast.literal_eval(response["body"])["headers"] + print(c2 + "*** ==> XHR Response Headers <== ***" + cr) + print(headers if not is_base64 else b64_data) + except Exception: + response_body = response["body"] + print(c2 + "*** ==> XHR Response Body <== ***" + cr) + print(response_body if not is_base64 else b64_data) + + +if __name__ == "__main__": + print("<============= START: XHR Example =============>") + asyncio.run(crawl()) + print("<============== END: XHR Example ==============>") diff --git a/examples/cdp_mode/raw_xhr_sb.py b/examples/cdp_mode/raw_xhr_sb.py new file mode 100644 index 00000000000..7130bcbff18 --- /dev/null +++ b/examples/cdp_mode/raw_xhr_sb.py @@ -0,0 +1,84 @@ +"""CDP.network.ResponseReceived with CDP.network.ResourceType.XHR.""" +import ast +import colorama +import mycdp +import sys +import time +from seleniumbase import SB + +xhr_requests = [] +last_xhr_request = None +c1 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX +c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX +cr = colorama.Style.RESET_ALL +if "linux" in sys.platform: + c1 = c2 = cr = "" + + +def listenXHR(page): + async def handler(evt): + # Get AJAX requests + if evt.type_ is mycdp.network.ResourceType.XHR: + xhr_requests.append([evt.response.url, evt.request_id]) + global last_xhr_request + last_xhr_request = time.time() + page.add_handler(mycdp.network.ResponseReceived, handler) + + +async def receiveXHR(page, requests): + responses = [] + retries = 0 + max_retries = 5 + # Wait at least 2 seconds after last XHR request for more + while True: + if last_xhr_request is None or retries > max_retries: + break + if time.time() - last_xhr_request <= 2: + retries = retries + 1 + time.sleep(2) + continue + else: + break + await page + # Loop through gathered requests and get response body + for request in requests: + try: + res = await page.send(mycdp.network.get_response_body(request[1])) + if res is None: + continue + responses.append({ + "url": request[0], + "body": res[0], + "is_base64": res[1], + }) + except Exception as e: + print("Error getting response:", e) + return responses + + +with SB(uc=True, test=True, locale="en") as sb: + sb.activate_cdp_mode("about:blank") + tab = sb.cdp.page + listenXHR(tab) + + # Change url to something that makes ajax requests + sb.goto("https://learn.microsoft.com/en-us/") + time.sleep(1) + for i in range(9): + sb.scroll_down(6) + + loop = sb.get_event_loop() + xhr_responses = loop.run_until_complete(receiveXHR(tab, xhr_requests)) + for response in xhr_responses: + print(c1 + "*** ==> XHR Request URL <== ***" + cr) + print(f'{response["url"]}') + is_base64 = response["is_base64"] + b64_data = "Base64 encoded data" + try: + headers = ast.literal_eval(response["body"])["headers"] + print(c2 + "*** ==> XHR Response Headers <== ***" + cr) + print(headers if not is_base64 else b64_data) + except Exception: + response_body = response["body"] + print(c2 + "*** ==> XHR Response Body <== ***" + cr) + print(response_body if not is_base64 else b64_data) diff --git a/examples/cdp_mode/raw_xpath.py b/examples/cdp_mode/raw_xpath.py new file mode 100644 index 00000000000..70d90a8532b --- /dev/null +++ b/examples/cdp_mode/raw_xpath.py @@ -0,0 +1,18 @@ +"""Test that CDP Mode can autodetect and use XPath selectors. +Also shows that CDP Mode methods can be called via sb.cdp.""" +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode() + sb.cdp.goto("https://seleniumbase.io/demo_page") + sb.cdp.highlight('//input[@id="myTextInput"]') + sb.cdp.type('//*[@id="myTextInput"]', "XPath Test!") + sb.cdp.type('//input[@name="text" or @name="preText2"]', "Xpath Here!") + sb.cdp.sleep(0.5) + sb.cdp.highlight('//button[contains(text(),"(Green)")]') + sb.cdp.click('//button[starts-with(text(),"Click Me")]') + sb.cdp.assert_element('//button[contains(., "Purple")]') + sb.cdp.sleep(0.5) + sb.cdp.highlight("//table/tbody/tr/td/h3") + sb.cdp.highlight("//table/tbody/tr[1]/td[2]/h2") + sb.cdp.assert_text("SeleniumBase", "//table//h2") diff --git a/examples/cdp_mode/raw_zoro.py b/examples/cdp_mode/raw_zoro.py new file mode 100644 index 00000000000..247dd7adcdf --- /dev/null +++ b/examples/cdp_mode/raw_zoro.py @@ -0,0 +1,38 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en") as sb: + url = "https://www.zoro.com/" + sb.activate_cdp_mode() + sb.goto(url) + sb.sleep(1.2) + search_box = "input#searchInput" + search = "Flir Thermal Camera" + required_text = "Camera" + if not sb.is_element_present(search_box): + sb.evaluate("window.location.reload();") + sb.sleep(1.2) + sb.click(search_box) + sb.sleep(1.2) + sb.press_keys(search_box, search) + sb.sleep(0.6) + sb.click('button[data-za="searchButton"]') + sb.sleep(3.2) + sb.wait_for_element('[data-za="product-cards-list"]', timeout=5) + print('*** Zoro Search for "%s":' % search) + print(' (Results must contain "%s")' % required_text) + unique_item_text = [] + soup = sb.get_beautiful_soup() + items = soup.select('[data-za="search-product-card"]') + for item in items: + item_text = item.get_text() + if required_text.lower() in item_text.lower(): + description_element = item.select_one("h2") + if description_element: + description_text = description_element.get_text(strip=True) + if description_text.lower() not in unique_item_text: + unique_item_text.append(description_text.lower()) + print("* " + description_text) + price_element = item.select_one("div.price-main") + if price_element: + price_text = " ".join(price_element.get_text().split()) + print(" (" + price_text + ")") diff --git a/examples/chart_maker/ReadMe.md b/examples/chart_maker/ReadMe.md new file mode 100644 index 00000000000..63bea47e6f4 --- /dev/null +++ b/examples/chart_maker/ReadMe.md @@ -0,0 +1,344 @@ + + +

๐Ÿ“Š ChartMaker ๐Ÿ“ถ

+ +

SeleniumBase ChartMaker lets you use Python to generate HTML charts.

+ +
+ +([Click to see a presentation with multiple charts](https://seleniumbase.io/other/chart_presentation.html)) + +Here's how to run a simple pie chart presentation from [GitHub => seleniumbase/SeleniumBase/examples/chart_maker](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/chart_maker): + +```zsh +cd examples/chart_maker +pytest my_chart.py +``` + +Here's the code for that pie chart presentation ([GitHub => seleniumbase/SeleniumBase/examples/chart_maker/my_chart.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/chart_maker/my_chart.py)): + +```python +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + +class MyChartMakerClass(BaseCase): + def test_chart_maker(self): + self.create_presentation() + self.create_pie_chart(title="Automated Tests") + self.add_data_point("Passed", 7, color="#95d96f") + self.add_data_point("Untested", 2, color="#eaeaea") + self.add_data_point("Failed", 1, color="#f1888f") + self.add_slide("

Pie Chart

" + self.extract_chart()) + self.begin_presentation(filename="my_chart.html") +``` + +Here's how to run an example presentation with multiple charts: + +```zsh +cd examples/chart_maker +pytest chart_presentation.py +``` + +Here are screenshots from the examples: + +
+ +
+ +
+ +
+ +
+ +
+ + +

Here's a line chart example:

+ +```python +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + +class MyChartMakerClass(BaseCase): + def test_chart_maker(self): + self.create_presentation() + self.create_line_chart( + title="Time Outside", subtitle="Last Week", unit="Minutes") + self.add_data_point("Sun", 5) + self.add_data_point("Mon", 10) + self.add_data_point("Tue", 20) + self.add_data_point("Wed", 40) + self.add_data_point("Thu", 80) + self.add_data_point("Fri", 65) + self.add_data_point("Sat", 50) + self.add_slide("

Line Chart

" + self.extract_chart()) + self.begin_presentation(filename="line_chart.html", interval=8) +``` + +This example is from [test_line_chart.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/chart_maker/test_line_chart.py), which you can run from the ``examples/chart_maker`` folder with the following command: + +```zsh +pytest test_line_chart.py +``` + +Because that presentation above has an ``interval`` set to ``8``, it will automatically advance to the next slide after 8 seconds. (Or exit if there are no more slides.) + + +

For a more advanced example (multiple charts in a presentation):

+ +```python +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + +class MyChartMakerClass(BaseCase): + def test_chart_maker_presentation(self): + self.create_presentation(theme="sky") + + self.create_pie_chart(title="Automated Tests") + self.add_data_point("Passed", 7, color="#95d96f") + self.add_data_point("Untested", 2, color="#eaeaea") + self.add_data_point("Failed", 1, color="#f1888f") + self.add_slide("

Pie Chart

" + self.extract_chart()) + + self.create_bar_chart(title="Language") + self.add_data_point("Python", 33, color="Orange") + self.add_data_point("JavaScript", 27, color="Teal") + self.add_data_point("HTML + CSS", 21, color="Purple") + self.add_slide("

Bar Chart

" + self.extract_chart()) + + self.create_column_chart(title="Colors") + self.add_data_point("Red", 10, color="Red") + self.add_data_point("Green", 25, color="Green") + self.add_data_point("Blue", 15, color="Blue") + self.add_slide("

Column Chart

" + self.extract_chart()) + + self.create_line_chart(title="Last Week's Data") + self.add_data_point("Sun", 5) + self.add_data_point("Mon", 10) + self.add_data_point("Tue", 20) + self.add_data_point("Wed", 40) + self.add_data_point("Thu", 80) + self.add_data_point("Fri", 65) + self.add_data_point("Sat", 50) + self.add_slide("

Line Chart

" + self.extract_chart()) + + self.begin_presentation(filename="chart_presentation.html") +``` + +Here's how to run that example: + +```zsh +cd examples/chart_maker +pytest chart_presentation.py +``` + +(Press the Right Arrow to advance to the next slide in that chart presentation) + +([Click to see a live example of that presentation](https://seleniumbase.io/other/chart_presentation.html)) + +Multi-Series charts can also be created. Try the available examples to learn more. + + +

ChartMaker API

+ +```python +self.create_pie_chart( + chart_name=None, title=None, subtitle=None, + data_name=None, unit=None, libs=True): +""" Creates a JavaScript pie chart using "HighCharts". + @Params + chart_name - If creating multiple charts, + use this to select which one. + title - The title displayed for the chart. + subtitle - The subtitle displayed for the chart. + data_name - The series name. Useful for multi-series charts. + If no data_name, will default to using "Series 1". + unit - The description label given to the chart's y-axis values. + libs - The option to include Chart libraries (JS and CSS files). + Should be set to True (default) for the first time creating + a chart on a web page. If creating multiple charts on the + same web page, you won't need to re-import the libraries + when creating additional charts. + labels - If True, displays labels on the chart for data points. + legend - If True, displays the data point legend on the chart. +""" +``` + +```python +self.create_bar_chart( + chart_name=None, title=None, subtitle=None, + data_name=None, unit=None, libs=True): +""" Creates a JavaScript bar chart using "HighCharts". + @Params + chart_name - If creating multiple charts, + use this to select which one. + title - The title displayed for the chart. + subtitle - The subtitle displayed for the chart. + data_name - The series name. Useful for multi-series charts. + If no data_name, will default to using "Series 1". + unit - The description label given to the chart's y-axis values. + libs - The option to include Chart libraries (JS and CSS files). + Should be set to True (default) for the first time creating + a chart on a web page. If creating multiple charts on the + same web page, you won't need to re-import the libraries + when creating additional charts. + labels - If True, displays labels on the chart for data points. + legend - If True, displays the data point legend on the chart. +""" +``` + +```python +self.create_column_chart( + chart_name=None, title=None, subtitle=None, + data_name=None, unit=None, libs=True): +""" Creates a JavaScript column chart using "HighCharts". + @Params + chart_name - If creating multiple charts, + use this to select which one. + title - The title displayed for the chart. + subtitle - The subtitle displayed for the chart. + data_name - The series name. Useful for multi-series charts. + If no data_name, will default to using "Series 1". + unit - The description label given to the chart's y-axis values. + libs - The option to include Chart libraries (JS and CSS files). + Should be set to True (default) for the first time creating + a chart on a web page. If creating multiple charts on the + same web page, you won't need to re-import the libraries + when creating additional charts. + labels - If True, displays labels on the chart for data points. + legend - If True, displays the data point legend on the chart. +""" +``` + +```python +self.create_line_chart( + chart_name=None, title=None, subtitle=None, + data_name=None, unit=None, zero=False, libs=True): +""" Creates a JavaScript line chart using "HighCharts". + @Params + chart_name - If creating multiple charts, + use this to select which one. + title - The title displayed for the chart. + subtitle - The subtitle displayed for the chart. + data_name - The series name. Useful for multi-series charts. + If no data_name, will default to using "Series 1". + unit - The description label given to the chart's y-axis values. + zero - If True, the y-axis always starts at 0. (Default: False). + libs - The option to include Chart libraries (JS and CSS files). + Should be set to True (default) for the first time creating + a chart on a web page. If creating multiple charts on the + same web page, you won't need to re-import the libraries + when creating additional charts. + labels - If True, displays labels on the chart for data points. + legend - If True, displays the data point legend on the chart. +""" +``` + +```python +self.create_area_chart( + chart_name=None, title=None, subtitle=None, + data_name=None, unit=None, zero=False, libs=True): +""" Creates a JavaScript area chart using "HighCharts". + @Params + chart_name - If creating multiple charts, + use this to select which one. + title - The title displayed for the chart. + subtitle - The subtitle displayed for the chart. + data_name - The series name. Useful for multi-series charts. + If no data_name, will default to using "Series 1". + unit - The description label given to the chart's y-axis values. + zero - If True, the y-axis always starts at 0. (Default: False). + libs - The option to include Chart libraries (JS and CSS files). + Should be set to True (default) for the first time creating + a chart on a web page. If creating multiple charts on the + same web page, you won't need to re-import the libraries + when creating additional charts. + labels - If True, displays labels on the chart for data points. + legend - If True, displays the data point legend on the chart. +""" +``` + +If creating multiple charts at the same time, you can pass the ``chart_name`` parameter to distinguish between different charts. + + +

Adding a data point to a chart:

+ +```python +self.add_data_point(label, value, color=None, chart_name=None): +""" Add a data point to a SeleniumBase-generated chart. + @Params + label - The label name for the data point. + value - The numeric value of the data point. + color - The HTML color of the data point. + Can be an RGB color. Eg: "#55ACDC". + Can also be a named color. Eg: "Teal". + chart_name - If creating multiple charts, + use this to select which one. +""" +``` + + +

Adding a new data series to an existing chart:

+ +```python +self.add_series_to_chart(self, data_name=None, chart_name=None): +""" Add a new data series to an existing chart. + This allows charts to have multiple data sets. + @Params + data_name - Set the series name. Useful for multi-series charts. + chart_name - If creating multiple charts, + use this to select which one. +""" +``` + + +

Saving a chart to a file:

+ +```python +self.save_chart(chart_name=None, filename=None): +""" Saves a SeleniumBase-generated chart to a file for later use. + @Params + chart_name - If creating multiple charts at the same time, + use this to select the one you wish to use. + filename - The name of the HTML file that you wish to + save the chart to. (filename must end in ".html") +""" +``` + +The full HTML of the chart is saved to the ``saved_charts/`` folder. + + +

Extracting the HTML of a chart:

+ +```python +self.extract_chart(chart_name=None): +""" Extracts the HTML from a SeleniumBase-generated chart. + @Params + chart_name - If creating multiple charts at the same time, + use this to select the one you wish to use. +""" +``` + + +

Displaying a chart in the browser window:

+ +```python +self.display_chart(chart_name=None, filename=None): +""" Displays a SeleniumBase-generated chart in the browser window. + @Params + chart_name - If creating multiple charts at the same time, + use this to select the one you wish to use. + filename - The name of the HTML file that you wish to + save the chart to. (filename must end in ".html") + interval - The delay time for auto-advancing charts. (in seconds) + If set to 0 (default), auto-advancing is disabled. +""" +``` + +All methods have the optional ``chart_name`` argument, which is only needed when storing multiple charts at the same time. + +-------- + + +

(Video: Pie Charts on Pi Day)

diff --git a/examples/chart_maker/chart_presentation.py b/examples/chart_maker/chart_presentation.py new file mode 100644 index 00000000000..ae0b31a6b50 --- /dev/null +++ b/examples/chart_maker/chart_presentation.py @@ -0,0 +1,37 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class ChartMakerPresentation(BaseCase): + def test_chart_maker_presentation(self): + self.create_presentation(theme="sky", transition="zoom") + + self.create_pie_chart(title="Automated Tests") + self.add_data_point("Passed", 7, color="#95d96f") + self.add_data_point("Untested", 2, color="#eaeaea") + self.add_data_point("Failed", 1, color="#f1888f") + self.add_slide("

Pie Chart

" + self.extract_chart()) + + self.create_bar_chart(title="Language", legend=False) + self.add_data_point("Python", 33, color="Orange") + self.add_data_point("JavaScript", 27, color="Teal") + self.add_data_point("HTML + CSS", 21, color="Purple") + self.add_slide("

Bar Chart

" + self.extract_chart()) + + self.create_column_chart(title="Colors", legend=False) + self.add_data_point("Red", 10, color="Red") + self.add_data_point("Green", 25, color="Green") + self.add_data_point("Blue", 15, color="Blue") + self.add_slide("

Column Chart

" + self.extract_chart()) + + self.create_line_chart(title="Last Week's Data") + self.add_data_point("Sun", 5) + self.add_data_point("Mon", 10) + self.add_data_point("Tue", 20) + self.add_data_point("Wed", 40) + self.add_data_point("Thu", 80) + self.add_data_point("Fri", 65) + self.add_data_point("Sat", 50) + self.add_slide("

Line Chart

" + self.extract_chart()) + + self.begin_presentation(filename="chart_presentation.html") diff --git a/examples/chart_maker/my_chart.py b/examples/chart_maker/my_chart.py new file mode 100644 index 00000000000..5eb64403852 --- /dev/null +++ b/examples/chart_maker/my_chart.py @@ -0,0 +1,13 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class MyChartMakerClass(BaseCase): + def test_chart_maker(self): + self.create_presentation() + self.create_pie_chart(title="Automated Tests") + self.add_data_point("Passed", 7, color="#95d96f") + self.add_data_point("Untested", 2, color="#eaeaea") + self.add_data_point("Failed", 1, color="#f1888f") + self.add_slide("

Pie Chart

" + self.extract_chart()) + self.begin_presentation(filename="my_chart.html") diff --git a/examples/chart_maker/pie_charts.py b/examples/chart_maker/pie_charts.py new file mode 100644 index 00000000000..363cfff0a6c --- /dev/null +++ b/examples/chart_maker/pie_charts.py @@ -0,0 +1,45 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class PieCharts(BaseCase): + def test_pie_charts(self): + self.create_presentation(theme="serif", transition="convex") + + self.create_pie_chart(labels=False) + self.add_data_point("Meringue Cream", 3, color="#f1eeea") + self.add_data_point("Lemon Filling", 3, color="#e9d655") + self.add_data_point("Graham Cracker Crust", 1, color="#9d5b34") + self.add_slide("

Lemon Meringue Pie

" + self.extract_chart()) + + self.create_pie_chart(labels=False) + self.add_data_point("Blueberries", 1, color="#5c81b7") + self.add_data_point("Blueberry Filling", 2, color="#12405e") + self.add_data_point("Golden Brown Crust", 1, color="#cd7b54") + self.add_slide("

Blueberry Pie

" + self.extract_chart()) + + self.create_pie_chart(labels=False) + self.add_data_point("Strawberries", 1, color="#ff282c") + self.add_data_point("Kiwis", 1, color="#a9c208") + self.add_data_point("Apricots", 1, color="#f47a14") + self.add_data_point("Raspberries", 1, color="#b10019") + self.add_data_point("Black Berries", 1, color="#44001e") + self.add_data_point("Blueberries", 1, color="#5c81b7") + self.add_data_point("Custard", 3, color="#eee896") + self.add_data_point("Golden Crust", 4, color="#dca422") + self.add_slide("

Fruit Tart Pie

" + self.extract_chart()) + + self.create_pie_chart(labels=False) + self.add_data_point("Apple Crust", 4, color="#b66327") + self.add_data_point("Apple Filling", 5, color="#c5903e") + self.add_data_point("Cinnamon", 1, color="#76210d") + self.add_data_point("Whipped Cream", 2, color="#f2f2f2") + self.add_slide("

Apple Pie

" + self.extract_chart()) + + self.create_pie_chart(labels=False) + self.add_data_point("Sponge Cake", 4, color="#e0d5a0") + self.add_data_point("Custard", 3, color="#eee896") + self.add_data_point("Chocolate", 1, color="#5c3625") + self.add_slide("

Boston Cream Pie

" + self.extract_chart()) + + self.begin_presentation(filename="pie_charts.html") diff --git a/examples/chart_maker/test_area_chart.py b/examples/chart_maker/test_area_chart.py new file mode 100644 index 00000000000..94b8cb49d30 --- /dev/null +++ b/examples/chart_maker/test_area_chart.py @@ -0,0 +1,19 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class MyChartMakerClass(BaseCase): + def test_area_chart(self): + self.create_presentation(theme="moon") + self.create_area_chart( + title="Time Outside", subtitle="Last Week", unit="Minutes" + ) + self.add_data_point("Sun", 5) + self.add_data_point("Mon", 10) + self.add_data_point("Tue", 20) + self.add_data_point("Wed", 40) + self.add_data_point("Thu", 80) + self.add_data_point("Fri", 65) + self.add_data_point("Sat", 50) + self.add_slide("

Area Chart

" + self.extract_chart()) + self.begin_presentation(filename="line_chart.html", interval=4) diff --git a/examples/chart_maker/test_display_chart.py b/examples/chart_maker/test_display_chart.py new file mode 100644 index 00000000000..b53057e24f0 --- /dev/null +++ b/examples/chart_maker/test_display_chart.py @@ -0,0 +1,33 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class MyChartMakerClass(BaseCase): + def test_display_chart(self): + self.create_pie_chart(title="Pie Chart") + self.add_data_point("Passed", 7, color="#95d96f") + self.add_data_point("Untested", 2, color="#eaeaea") + self.add_data_point("Failed", 1, color="#f1888f") + self.display_chart(filename="pie_chart.html", interval=2.5) + + self.create_bar_chart(title="Bar Chart", legend=False) + self.add_data_point("Python", 33, color="Orange") + self.add_data_point("JavaScript", 27, color="Teal") + self.add_data_point("HTML + CSS", 21, color="Purple") + self.display_chart(filename="bar_chart.html", interval=2.5) + + self.create_column_chart(title="Column Chart", legend=False) + self.add_data_point("Red", 10, color="Red") + self.add_data_point("Green", 25, color="Green") + self.add_data_point("Blue", 15, color="Blue") + self.display_chart(filename="column_chart.html", interval=2.5) + + self.create_line_chart(title="Line Chart") + self.add_data_point("Sun", 5) + self.add_data_point("Mon", 10) + self.add_data_point("Tue", 20) + self.add_data_point("Wed", 40) + self.add_data_point("Thu", 80) + self.add_data_point("Fri", 65) + self.add_data_point("Sat", 50) + self.display_chart(filename="line_chart.html", interval=2.5) diff --git a/examples/chart_maker/test_line_chart.py b/examples/chart_maker/test_line_chart.py new file mode 100644 index 00000000000..47ed7eba662 --- /dev/null +++ b/examples/chart_maker/test_line_chart.py @@ -0,0 +1,19 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class MyChartMakerClass(BaseCase): + def test_line_chart(self): + self.create_presentation() + self.create_line_chart( + title="Time Outside", subtitle="Last Week", unit="Minutes" + ) + self.add_data_point("Sun", 5) + self.add_data_point("Mon", 10) + self.add_data_point("Tue", 20) + self.add_data_point("Wed", 40) + self.add_data_point("Thu", 80) + self.add_data_point("Fri", 65) + self.add_data_point("Sat", 50) + self.add_slide("

Line Chart

" + self.extract_chart()) + self.begin_presentation(filename="line_chart.html", interval=4) diff --git a/examples/chart_maker/test_multi_series.py b/examples/chart_maker/test_multi_series.py new file mode 100644 index 00000000000..db5f72748b7 --- /dev/null +++ b/examples/chart_maker/test_multi_series.py @@ -0,0 +1,48 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class MyChartMakerClass(BaseCase): + def test_multi_series(self): + self.create_presentation(theme="league") + + self.create_line_chart( + title="Fruit Sold Last Week", data_name="Apples", unit="Count" + ) + self.add_data_point("Sun", 33) + self.add_data_point("Mon", 16) + self.add_data_point("Tue", 19) + self.add_data_point("Wed", 28) + self.add_data_point("Thu", 20) + self.add_data_point("Fri", 30) + self.add_data_point("Sat", 36) + + self.add_series_to_chart(data_name="Oranges") + self.add_data_point("Sun", 22) + self.add_data_point("Mon", 27) + self.add_data_point("Tue", 23) + self.add_data_point("Wed", 21) + self.add_data_point("Thu", 26) + self.add_data_point("Fri", 17) + self.add_data_point("Sat", 25) + + self.add_series_to_chart(data_name="Strawberries") + self.add_data_point("Sun", 41) + self.add_data_point("Mon", 32) + self.add_data_point("Tue", 38) + self.add_data_point("Wed", 33) + self.add_data_point("Thu", 31) + self.add_data_point("Fri", 42) + self.add_data_point("Sat", 40) + + self.add_series_to_chart(data_name="Cherries") + self.add_data_point("Sun", 28) + self.add_data_point("Mon", 37) + self.add_data_point("Tue", 29) + self.add_data_point("Wed", 24) + self.add_data_point("Thu", 34) + self.add_data_point("Fri", 26) + self.add_data_point("Sat", 31) + + self.add_slide("

Multi-Series Line Chart

" + self.extract_chart()) + self.begin_presentation(filename="multi_series_chart.html", interval=4) diff --git a/examples/chart_maker/test_save_chart.py b/examples/chart_maker/test_save_chart.py new file mode 100644 index 00000000000..6f4f62758d4 --- /dev/null +++ b/examples/chart_maker/test_save_chart.py @@ -0,0 +1,33 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class MyChartMakerClass(BaseCase): + def test_save_chart(self): + self.create_pie_chart(title="Pie Chart") + self.add_data_point("Passed", 7, color="#95d96f") + self.add_data_point("Untested", 2, color="#eaeaea") + self.add_data_point("Failed", 1, color="#f1888f") + self.save_chart(filename="pie_chart.html") + + self.create_bar_chart(title="Bar Chart") + self.add_data_point("Python", 33, color="Orange") + self.add_data_point("JavaScript", 27, color="Teal") + self.add_data_point("HTML + CSS", 21, color="Purple") + self.save_chart(filename="bar_chart.html") + + self.create_column_chart(title="Column Chart") + self.add_data_point("Red", 10, color="Red") + self.add_data_point("Green", 25, color="Green") + self.add_data_point("Blue", 15, color="Blue") + self.save_chart(filename="column_chart.html") + + self.create_line_chart(title="Line Chart") + self.add_data_point("Sun", 5) + self.add_data_point("Mon", 10) + self.add_data_point("Tue", 20) + self.add_data_point("Wed", 40) + self.add_data_point("Thu", 80) + self.add_data_point("Fri", 65) + self.add_data_point("Sat", 50) + self.save_chart(filename="line_chart.html") diff --git a/examples/coffee_cart_tests.py b/examples/coffee_cart_tests.py new file mode 100644 index 00000000000..39eb5763c5a --- /dev/null +++ b/examples/coffee_cart_tests.py @@ -0,0 +1,91 @@ +"""Use SeleniumBase to test the Coffee Cart App.""" +from parameterized import parameterized +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class CoffeeCartTests(BaseCase): + def test_1_verify_nav_link_to_coffee_cart(self): + self.goto("https://seleniumbase.io/help_docs/customizing_test_runs/") + self.js_click('nav a:contains("Coffee Cart")') + self.assert_title("Coffee Cart") + self.assert_element('h4:contains("Espresso")') + + def test_buy_one_cappuccino(self): + self.goto("https://seleniumbase.io/coffee/") + self.assert_title("Coffee Cart") + self.click('div[data-test="Cappuccino"]') + self.assert_exact_text("cart (1)", 'a[aria-label="Cart page"]') + self.click('a[aria-label="Cart page"]') + self.assert_exact_text("Total: $19.00", 'button[data-test="checkout"]') + self.click('button[data-test="checkout"]') + self.type("input#name", "Selenium Coffee") + self.type("input#email", "test@test.test") + self.click("button#submit-payment") + self.assert_text("Thanks for your purchase.", "div#app div.success") + self.assert_exact_text("cart (0)", 'a[aria-label="Cart page"]') + self.assert_exact_text("Total: $0.00", 'button[data-test="checkout"]') + + @parameterized.expand([[False], [True]]) + def test_coffee_promo_with_preview(self, accept_promo): + self.goto("https://seleniumbase.io/coffee/") + self.assert_title("Coffee Cart") + self.click('div[data-test="Espresso"]') + self.click('div[data-test="Americano"]') + self.click('div[data-test="Cafe_Latte"]') + self.assert_exact_text("cart (3)", 'a[aria-label="Cart page"]') + promo = False + total_string = "Total: $33.00" + if self.is_element_visible("div.promo"): + self.assert_text("Get an extra cup of Mocha for $4.", "div.promo") + if accept_promo: + self.click("div.promo button.yes") + self.assert_exact_text("cart (4)", 'a[aria-label="Cart page"]') + promo = True + total_string = "Total: $37.00" + else: + self.click("div.promo button.no") + checkout_button = 'button[data-test="checkout"]' + if promo and not self.browser == "safari": + self.hover(checkout_button) + if not self.is_element_visible("ul.cart-preview"): + self.highlight(checkout_button) + self.post_message("STOP moving the mouse!
Hover blocked!") + self.hover(checkout_button) + self.assert_text("(Discounted) Mocha", "ul.cart-preview") + self.assert_exact_text(total_string, checkout_button) + self.click(checkout_button) + self.type("input#name", "Selenium Coffee") + self.type("input#email", "test@test.test") + self.click("button#submit-payment") + self.assert_text("Thanks for your purchase.", "div#app div.success") + + def test_context_click_add_coffee(self): + self.goto("https://seleniumbase.io/coffee/") + self.assert_title("Coffee Cart") + self.context_click('div[data-test="Espresso_Macchiato"]') + self.click('form button:contains("Yes")') + self.assert_exact_text("cart (1)", 'a[aria-label="Cart page"]') + self.click('a[aria-label="Cart page"]') + self.assert_exact_text("Total: $12.00", 'button[data-test="checkout"]') + self.click('button[data-test="checkout"]') + self.type("input#name", "Selenium Coffee") + self.type("input#email", "test@test.test") + self.click("button#submit-payment") + self.assert_text("Thanks for your purchase.", "div#app div.success") + + def test_remove_added_coffee(self): + self.goto("https://seleniumbase.io/coffee/") + self.assert_title("Coffee Cart") + self.assert_exact_text("cart (0)", 'a[aria-label="Cart page"]') + self.assert_exact_text("Total: $0.00", "button.pay") + self.wait_for_element('div[class="cup-body"]') + self.click_visible_elements('div[class="cup-body"]', limit=6) + self.assert_exact_text("cart (6)", 'a[aria-label="Cart page"]') + self.assert_exact_text("Total: $74.00", 'button[data-test="checkout"]') + self.click('a[aria-label="Cart page"]') + self.click_visible_elements("button.delete") + self.assert_text("No coffee, go add some.", "div#app") + self.click('a[aria-label="Menu page"]') + self.assert_exact_text("cart (0)", 'a[aria-label="Cart page"]') + self.assert_exact_text("Total: $0.00", 'button[data-test="checkout"]') diff --git a/examples/custom_settings.py b/examples/custom_settings.py new file mode 100644 index 00000000000..480d1bf22a4 --- /dev/null +++ b/examples/custom_settings.py @@ -0,0 +1,124 @@ +""" +To override default settings stored in seleniumbase/config/settings.py, +change the values here and add "--settings=custom_settings.py" when running. +""" + +# Default timeout values for waiting for page elements to appear. +MINI_TIMEOUT = 2 +SMALL_TIMEOUT = 7 +LARGE_TIMEOUT = 10 +EXTREME_TIMEOUT = 30 + +# Default page load timeout. +# In global Selenium settings, this value is set to 300 seconds by default. +# When the timeout is exceeded: "Timed out receiving message from renderer" +PAGE_LOAD_TIMEOUT = 120 + +# Default page load strategy. +# ["normal", "eager", "none"] +# Selenium default = "normal" +PAGE_LOAD_STRATEGY = "normal" + +# If True, existing logs from past test runs will be saved and take up space. +# If False, only the logs from the most recent test run will be saved locally. +# You can also archive existing logs on the command line with: "--archive_logs" +ARCHIVE_EXISTING_LOGS = False + +# If True, existing downloads from past runs will be saved and take up space. +# If False, only the downloads from the most recent run will be saved locally. +ARCHIVE_EXISTING_DOWNLOADS = False + +# If True, the last page screenshot will include the and background. +# If False, the last page screenshot will only include the section. +# Depending on the screen size, including a background could make the +# appear very small in the screenshot, which may require manually zooming in +# on the to see page details if you decide to include the background. +SCREENSHOT_WITH_BACKGROUND = False + +# If True, switch to new tabs automatically if a click opens a new one. +# (Only happens if the initial tab is still on same URL as before.) +SWITCH_TO_NEW_TABS_ON_CLICK = True + +""" +These methods add JS waits, such as self.wait_for_ready_state_complete(), +which waits for document.readyState to be "complete" after Selenium actions. +""" +# Called after self.goto(url), NOT driver.get(url) +WAIT_FOR_RSC_ON_PAGE_LOADS = True +# Called after self.click(selector), NOT element.click() +WAIT_FOR_RSC_ON_CLICKS = False +# Wait for AngularJS calls to complete after various browser actions. +WAIT_FOR_ANGULARJS = True +# Skip ALL calls to wait_for_ready_state_complete() and wait_for_angularjs(). +SKIP_JS_WAITS = False + +# Changing the default behavior of Demo Mode. Activate with: --demo_mode +DEFAULT_DEMO_MODE_TIMEOUT = 0.5 +HIGHLIGHTS = 4 +DEFAULT_MESSAGE_DURATION = 2.55 + +# Disabling the Content Security Policy of the browser by default. +DISABLE_CSP_ON_FIREFOX = True +DISABLE_CSP_ON_CHROME = False + +# If True and --proxy=IP_ADDRESS:PORT is invalid, then error immediately. +RAISE_INVALID_PROXY_STRING_EXCEPTION = True + +# Default browser coordinates when opening new windows for tests. +WINDOW_START_X = 20 +WINDOW_START_Y = 54 + +# Default browser resolutions when opening new windows for tests. +# (Headless resolutions take priority, and include all browsers.) +# (Firefox starts maximized by default when running in GUI Mode.) +CHROME_START_WIDTH = 1250 +CHROME_START_HEIGHT = 840 +HEADLESS_START_WIDTH = 1440 +HEADLESS_START_HEIGHT = 1880 + +# If True, hides messages related to downloading drivers. +# If False, you'll see details about downloading drivers. +# (This only affects driver downloads coming from tests.) +# (Eg. Using "sbase get chromedriver" won't hide output.) +HIDE_DRIVER_DOWNLOADS = False + +# Changing the default behavior of MasterQA Mode. +MASTERQA_DEFAULT_VALIDATION_MESSAGE = "Does the page look good?" +MASTERQA_WAIT_TIME_BEFORE_VERIFY = 0.5 +MASTERQA_START_IN_FULL_SCREEN_MODE = False +MASTERQA_MAX_IDLE_TIME_BEFORE_QUIT = 600 + +# Google Authenticator +# (For 2-factor authentication using a time-based one-time password algorithm) +# (See https://github.com/pyotp/pyotp and https://pypi.org/project/pyotp/ ) +# (Also works with Authy and other compatible apps.) +# Usage: "self.get_google_auth_password()" (output based on timestamp) +# Usage with override: "self.get_google_auth_password(totp_key=TOTP_KEY)" +TOTP_KEY = "base32secretABCD" + +# MySQL DB Credentials +# (For saving data from tests to a MySQL DB) +# Usage: "--with-db_reporting" +DB_HOST = "127.0.0.1" +DB_PORT = 3306 +DB_USERNAME = "root" +DB_PASSWORD = "test" +DB_SCHEMA = "test_db" + +# Amazon S3 Bucket Credentials +# (For saving screenshots and other log files from tests) +# (Bucket names are unique across all existing bucket names in Amazon S3) +# Usage: "--with-s3_logging" +S3_LOG_BUCKET = "[S3 BUCKET NAME]" +S3_BUCKET_URL = "https://s3.amazonaws.com/[S3 BUCKET NAME]/" +S3_SELENIUM_ACCESS_KEY = "[S3 ACCESS KEY]" +S3_SELENIUM_SECRET_KEY = "[S3 SECRET KEY]" + +# Encryption Settings +# (Used for string/password obfuscation) +# (You should reset the Encryption Key for every clone of SeleniumBase) +ENCRYPTION_KEY = "Pg^.l!8UdJ+Y7dMIe&fl*%!p9@ej]/#tL~3E4%6?" +# These tokens are added to the beginning and end of obfuscated passwords. +# Helps identify which strings/passwords have been obfuscated. +OBFUSCATION_START_TOKEN = "$^*ENCRYPT=" +OBFUSCATION_END_TOKEN = "?&#$" diff --git a/examples/delayed_assert_test.py b/examples/delayed_assert_test.py deleted file mode 100755 index 1e83480577b..00000000000 --- a/examples/delayed_assert_test.py +++ /dev/null @@ -1,17 +0,0 @@ -import pytest -from seleniumbase import BaseCase - - -class MyTestClass(BaseCase): - - @pytest.mark.expected_failure - def test_delayed_asserts(self): - self.open('https://xkcd.com/993/') - self.wait_for_element('#comic') - self.delayed_assert_element('img[alt="Brand Identity"]') - self.delayed_assert_element('img[alt="Rocket Ship"]') # Will Fail - self.delayed_assert_element('#comicmap') - self.delayed_assert_text('Fake Item', '#middleContainer') # Will Fail - self.delayed_assert_text('Random', '#middleContainer') - self.delayed_assert_element('a[name="Super Fake !!!"]') # Will Fail - self.process_delayed_asserts() diff --git a/examples/desktop_apps/ReadMe.md b/examples/desktop_apps/ReadMe.md new file mode 100644 index 00000000000..0d3b016831e --- /dev/null +++ b/examples/desktop_apps/ReadMe.md @@ -0,0 +1,5 @@ +

SeleniumBase

+ +

Desktop Apps

+ +* **Recorder** (Run using `python recorder.py` or `sbase recorder`) diff --git a/examples/desktop_apps/recorder.py b/examples/desktop_apps/recorder.py new file mode 100644 index 00000000000..60c4b16cb22 --- /dev/null +++ b/examples/desktop_apps/recorder.py @@ -0,0 +1,12 @@ +""" Run this file using ``python recorder.py`` """ + +import os + + +def open_recorder_desktop_app(): + command = "sbase recorder" + os.system(command) + + +if __name__ == "__main__": + open_recorder_desktop_app() diff --git a/examples/dialog_boxes/ReadMe.md b/examples/dialog_boxes/ReadMe.md new file mode 100644 index 00000000000..e4aa10a4f59 --- /dev/null +++ b/examples/dialog_boxes/ReadMe.md @@ -0,0 +1,180 @@ + + +

Dialog Boxes ๐Ÿ›‚

+ +SeleniumBase Dialog Boxes let your users provide input in the middle of automation scripts. + +* This feature utilizes the [jquery-confirm](https://craftpip.github.io/jquery-confirm/) library. +* A Python API is used to call the JavaScript API. + +SeleniumBase + +

โ†•๏ธ (Example: dialog_box_tour.py) โ†•๏ธ

+ +SeleniumBase + +

Here's how to run that example:

+ +```zsh +cd examples/dialog_boxes +pytest test_dialog_boxes.py +``` + +

Here's a code snippet from that:

+ +```python +self.goto("https://xkcd.com/1920/") +skip_button = ["SKIP", "red"] # Can be a [text, color] list or tuple. +buttons = ["Fencing", "Football", "Metaball", "Go/Chess", skip_button] +message = "Choose a sport:" +choice = self.get_jqc_button_input(message, buttons) +if choice == "Fencing": + self.goto("https://xkcd.com/1424/") +``` + +* You can create forms that include buttons and input fields. + +

Here's a simple form with only buttons as input:

+ +```python +choice = self.get_jqc_button_input("Ready?", ["YES", "NO"]) +print(choice) # This prints "YES" or "NO" + +# You may want to customize the color of buttons: +buttons = [("YES", "green"), ("NO", "red")] +choice = self.get_jqc_button_input("Ready?", buttons) +``` + +

Here's a simple form with an input field:

+ +```python +text = self.get_jqc_text_input("Enter text:", ["Search"]) +print(text) # This prints the text entered +``` + +

This form has an input field and buttons:

+ +```python +message = "Type your name and choose a language:" +buttons = ["Python", "JavaScript"] +text, choice = self.get_jqc_form_inputs(message, buttons) +print("Your name is: %s" % text) +print("You picked %s!" % choice) +``` + +

You can customize options if you want:

+ +```python +# Themes: bootstrap, modern, material, supervan, light, dark, seamless +options = [("theme", "modern"), ("width", "50%")] +self.get_jqc_text_input("You Won!", ["OK"], options) +``` + +

Default options can be set with set_jqc_theme():

+ +```python +self.set_jqc_theme("light", color="green", width="38%") + +# To reset jqc theme settings to factory defaults: +self.reset_jqc_theme() +``` + +

All methods for Dialog Boxes:

+ +```python +self.get_jqc_button_input(message, buttons, options=None) + +self.get_jqc_text_input(message, button=None, options=None) + +self.get_jqc_form_inputs(message, buttons, options=None) + +self.set_jqc_theme(theme, color=None, width=None) + +self.reset_jqc_theme() + +self.activate_jquery_confirm() # Automatic for jqc methods +``` + +

Detailed method summaries for Dialog Boxes:

+ +```python +self.get_jqc_button_input(message, buttons, options=None) +""" +Pop up a jquery-confirm box and return the text of the button clicked. +If running in headless mode, the last button text is returned. +@Params +message: The message to display in the jquery-confirm dialog. +buttons: A list of tuples for text and color. + Example: [("Yes!", "green"), ("No!", "red")] + Available colors: blue, green, red, orange, purple, default, dark. + A simple text string also works: "My Button". (Uses default color.) +options: A list of tuples for options to set. + Example: [("theme", "bootstrap"), ("width", "450px")] + Available theme options: bootstrap, modern, material, supervan, + light, dark, and seamless. + Available colors: (For the BORDER color, NOT the button color.) + "blue", "default", "green", "red", "purple", "orange", "dark". + Example option for changing the border color: ("color", "default") + Width can be set using percent or pixels. Eg: "36.0%", "450px". +""" + +self.get_jqc_text_input(message, button=None, options=None) +""" +Pop up a jquery-confirm box and return the text submitted by the input. +If running in headless mode, the text returned is "" by default. +@Params +message: The message to display in the jquery-confirm dialog. +button: A 2-item list or tuple for text and color. Or just the text. + Example: ["Submit", "blue"] -> (default button if not specified) + Available colors: blue, green, red, orange, purple, default, dark. + A simple text string also works: "My Button". (Uses default color.) +options: A list of tuples for options to set. + Example: [("theme", "bootstrap"), ("width", "450px")] + Available theme options: bootstrap, modern, material, supervan, + light, dark, and seamless. + Available colors: (For the BORDER color, NOT the button color.) + "blue", "default", "green", "red", "purple", "orange", "dark". + Example option for changing the border color: ("color", "default") + Width can be set using percent or pixels. Eg: "36.0%", "450px". +""" + +self.get_jqc_form_inputs(message, buttons, options=None) +""" +Pop up a jquery-confirm box and return the input/button texts as tuple. +If running in headless mode, returns the ("", buttons[-1][0]) tuple. +@Params +message: The message to display in the jquery-confirm dialog. +buttons: A list of tuples for text and color. + Example: [("Yes!", "green"), ("No!", "red")] + Available colors: blue, green, red, orange, purple, default, dark. + A simple text string also works: "My Button". (Uses default color.) +options: A list of tuples for options to set. + Example: [("theme", "bootstrap"), ("width", "450px")] + Available theme options: bootstrap, modern, material, supervan, + light, dark, and seamless. + Available colors: (For the BORDER color, NOT the button color.) + "blue", "default", "green", "red", "purple", "orange", "dark". + Example option for changing the border color: ("color", "default") + Width can be set using percent or pixels. Eg: "36.0%", "450px". +""" + +self.set_jqc_theme(theme, color=None, width=None) +""" Sets the default jquery-confirm theme and width (optional). +Available themes: "bootstrap", "modern", "material", "supervan", + "light", "dark", and "seamless". +Available colors: (This sets the BORDER color, NOT the button color.) + "blue", "default", "green", "red", "purple", "orange", "dark". +Width can be set using percent or pixels. Eg: "36.0%", "450px". +""" + +self.reset_jqc_theme() +""" Resets the jqc theme settings to factory defaults. """ + +self.activate_jquery_confirm() # Automatic for jqc methods +""" See https://craftpip.github.io/jquery-confirm/ for usage. """ +``` + +-------- + +

โœ… ๐Ÿ›‚ Automated/Manual Hybrid Mode (MasterQA)

+

MasterQA uses SeleniumBase Dialog Boxes to speed up manual testing by having automation perform all the browser actions while the manual tester handles validation. See the MasterQA GitHub page for examples.

diff --git a/examples/dialog_boxes/dialog_box_tour.py b/examples/dialog_boxes/dialog_box_tour.py new file mode 100644 index 00000000000..d2b935deebd --- /dev/null +++ b/examples/dialog_boxes/dialog_box_tour.py @@ -0,0 +1,123 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class DialogBoxTests(BaseCase): + def test_dialog_boxes(self): + self.goto("https://xkcd.com/1920/") + self.assert_element('img[alt="Emoji Sports"]') + self.highlight("#comic img") + + skip_button = ["SKIP", "red"] # Can be a [text, color] list or tuple. + buttons = ["Fencing", "Football", "Metaball", "Go/Chess", skip_button] + message = "Choose a sport:" + choice = None + while choice != "STOP": + choice = self.get_jqc_button_input(message, buttons) + if choice == "Fencing": + self.goto("https://xkcd.com/1424/") + buttons.remove("Fencing") + elif choice == "Football": + self.goto("https://xkcd.com/1107/") + buttons.remove("Football") + elif choice == "Metaball": + self.goto("https://xkcd.com/1507/") + buttons.remove("Metaball") + elif choice == "Go/Chess": + self.goto("https://xkcd.com/1287/") + buttons.remove("Go/Chess") + else: + break + self.highlight("#comic img") + if len(buttons) == 2: + message = "One Sport Remaining:" + if len(buttons) == 1: + message = "Part One Complete. You saw all 4 sports!" + btn_text_1 = "NEXT Tutorial Please!" + btn_text_2 = "WAIT, Go/Chess is a sport?" + buttons = [(btn_text_1, "green"), (btn_text_2, "purple")] + choice_2 = self.get_jqc_button_input(message, buttons) + if choice_2 == btn_text_2: + self.goto_if_not_url("https://xkcd.com/1287/") + message = "Brain sports count as sports!

" + message += "Are you ready for more?" + self.get_jqc_button_input(message, ["Let's Go!"]) + break + + self.goto("https://xkcd.com/1117/") + sb_banner_logo = "//seleniumbase.io/cdn/img/sb_logo_10.png" + self.set_attributes("#news img", "src", sb_banner_logo) + options = [("theme", "material"), ("width", "52%")] + message = 'With one button, you can press "Enter/Return", "Y", or "1".' + self.get_jqc_button_input(message, ["OK"], options) + + self.goto("https://xkcd.com/556/") + self.set_attributes("#news img", "src", sb_banner_logo) + options = [("theme", "bootstrap"), ("width", "52%")] + message = 'If the lowercase button text is "yes" or "no", ' + message += '

you can use the "Y" or "N" keys as shortcuts. ' + message += "


Other shortcuts include:

" + message += '"1": 1st button, "2": 2nd button, etc. Got it?' + buttons = [("YES", "green"), ("NO", "red")] + choice = self.get_jqc_button_input(message, buttons, options) + + message = 'You said "%s"!

' % choice + if choice == "YES": + message += "Wonderful! Let's continue with the next example..." + else: + message += "You can learn more from SeleniumBase Docs..." + choice = self.get_jqc_button_input(message, ["OK"], options) + + self.goto("https://seleniumbase.io") + self.set_jqc_theme("light", color="green", width="38%") + message = "This is the SeleniumBase Docs website!

" + message += "What would you like to search for?
" + text = self.get_jqc_text_input(message, ["Search"]) + self.type('input[aria-label="Search"]', text + "\n") + self.wait_for_ready_state_complete() + self.set_jqc_theme("bootstrap", color="red", width="32%") + if self.is_text_visible("No matching documents", ".md-search-result"): + self.get_jqc_button_input("Your search had no results!", ["OK"]) + elif self.is_text_visible("Type to start searching", "div.md-search"): + self.get_jqc_button_input("You did not do a search!", ["OK"]) + else: + self.click_if_visible("a.md-search-result__link") + self.set_jqc_theme("bootstrap", color="green", width="32%") + self.get_jqc_button_input("You found search results!", ["OK"]) + + self.goto("https://seleniumbase.io/help_docs/ReadMe/") + self.highlight("h1") + self.slow_scroll_to('article p a[href*="/examples/ReadMe/"]') + zoom_in = 'article p a[href*="/examples/ReadMe/"]{zoom: 1.8;}' + self.add_css_style(zoom_in) + self.highlight_click('article p a[href*="/examples/ReadMe/"]') + self.highlight("h1") + + self.set_jqc_theme("bootstrap", color="green", width="52%") + message = 'See the "SeleniumBase/examples" section for more info!' + self.get_jqc_button_input(message, ["OK"]) + + self.set_jqc_theme("bootstrap", color="purple", width="56%") + message = "Now let's combine form inputs with multiple button options!" + message += "

" + message += "Pick something to search. Then pick the site to search on." + buttons = ["SeleniumBase.io", "Wikipedia.org"] + text, choice = self.get_jqc_form_inputs(message, buttons) + if choice == "SeleniumBase.io": + self.goto("https://seleniumbase.io/") + self.highlight_type('input[aria-label="Search"]', text + "\n") + else: + self.goto("https://en.wikipedia.org/wiki/Special:Search") + self.highlight_type('input[id*="search"]', text) + self.sleep(1) + self.click("#searchform button") + self.wait_for_ready_state_complete() + self.sleep(1) + self.highlight("body") + self.reset_jqc_theme() + self.get_jqc_button_input("Here are your results.", ["OK"]) + message = "

You've reached the end of this tutorial!


" + message += "Now you know about SeleniumBase Dialog Boxes!
" + message += "
Check out SeleniumBase on GitHub for more!" + self.set_jqc_theme("modern", color="purple", width="56%") + self.get_jqc_button_input(message, ["Goodbye!"]) diff --git a/examples/edge_test.py b/examples/edge_test.py new file mode 100644 index 00000000000..b9b1a931f97 --- /dev/null +++ b/examples/edge_test.py @@ -0,0 +1,20 @@ +"""This test is only for Microsoft Edge (Chromium)!""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__, "--edge") + + +class EdgeTests(BaseCase): + def test_edge(self): + if self.browser != "edge": + self.goto_if_not_url("about:blank") + print("\n This test is only for Microsoft Edge (Chromium)!") + print(' (Run this test using "--edge" or "--browser=edge")') + self.skip('Use "--edge" or "--browser=edge"') + elif self.headless: + self.goto_if_not_url("about:blank") + print("\n This test is NOT designed for Headless Mode!") + self.skip('Do NOT use "--headless" with this test!') + self.goto("edge://settings/help") + self.assert_element("app-shell") + self.assert_text("Microsoft Edge", "app-shell") + self.sleep(2) diff --git a/examples/example_config.cfg b/examples/example_config.cfg old mode 100755 new mode 100644 diff --git a/examples/example_logs/ReadMe.md b/examples/example_logs/ReadMe.md old mode 100755 new mode 100644 index 1fbabf6394a..030669f7e69 --- a/examples/example_logs/ReadMe.md +++ b/examples/example_logs/ReadMe.md @@ -1,39 +1,125 @@ -#### Logging, Screenshots, and Reports examples + -Log files in [example_logs/](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/example_logs) were generated when [test_fail.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_fail.py) ran and failed. By default, logs are saved to ``latest_logs/``. If ARCHIVE_EXISTING_LOGS is set to True in [settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py), past logs get saved to ``archived_logs/``. +## [](https://github.com/seleniumbase/SeleniumBase/) Logs, The Dashboard, and Reports: -```bash -pytest test_fail.py --browser=chrome + +

(The Dashboard Tutorial on YouTube)

-nosetests test_fail.py --browser=firefox +๐Ÿ”ต During test failures, logs and screenshots from the most recent test run will get saved to the ``latest_logs/`` folder. If ``--archive-logs`` is specified (or if ARCHIVE_EXISTING_LOGS is set to True in [settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py)), test logs will also get archived to the ``archived_logs/`` folder. Otherwise, the log files will be cleaned out when the next test run begins (by default). + +```zsh +pytest test_fail.py +``` + +(Log files in [SeleniumBase/examples/example_logs](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/example_logs) were generated when [test_fail.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_fail.py) ran and failed.) + +Examples of expected log files generated during failures: + + +In addition to log files, you can also generate dashboards and test reports. + +-------- + +

The SeleniumBase Dashboard:

+ +๐Ÿ”ต The ``--dashboard`` option for pytest generates a SeleniumBase Dashboard located at ``dashboard.html``, which updates automatically as tests run and produce results. Example: + +```zsh +pytest --dashboard --rs --headless +``` + +The SeleniumBase Dashboard + +๐Ÿ”ต Additionally, you can host your own SeleniumBase Dashboard Server on a port of your choice. Here's an example of that using Python 3's ``http.server``: + +```zsh +python -m http.server 1948 ``` -**Expected log files generated during failures:** -* [basic_test_info.txt](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/example_logs/basic_test_info.txt) -* [page_source.html](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/example_logs/page_source.html) -* [screenshot.png](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/example_logs/screenshot.png) +๐Ÿ”ต Now you can navigate to ``http://localhost:1948/dashboard.html`` in order to view the dashboard as a web app. This requires two different terminal windows: one for running the server, and another for running the tests, which should be run from the same directory. (Use Ctrl+C to stop the http server.) + +๐Ÿ”ต Here's a full example of what the SeleniumBase Dashboard may look like: ---- -**In addition to logging, you can also generate test reports:** +```zsh +pytest test_suite.py test_image_saving.py --dashboard --rs --headless +``` + +The SeleniumBase Dashboard -Reports are most useful when running large test suites. Pytest and Nosetest reports are handled differently. +-------- -#### **Pytest Reports:** +

Pytest Reports:

-Using ``--html=report.html`` gives you a fancy report of the name specified after your test suite completes. +๐Ÿ”ต Using ``--html=report.html`` gives you a fancy report of the name specified after your test suite completes. -```bash +```zsh pytest test_suite.py --html=report.html ``` -![](https://cdn2.hubspot.net/hubfs/100006/images/PytestReport.png "Example Pytest Report") -#### **Nosetest Reports:** +Example Pytest Report + +๐Ÿ”ต When combining pytest html reports with SeleniumBase Dashboard usage, the pie chart from the Dashboard will get added to the html report. Additionally, if you set the html report URL to be the same as the Dashboard URL when also using the dashboard, (example: ``--dashboard --html=dashboard.html``), then the Dashboard will become an advanced html report when all the tests complete. + +๐Ÿ”ต Here's an example of an upgraded html report: + +```zsh +pytest test_suite.py --dashboard --html=report.html +``` + +Dashboard Pytest HTML Report + +-------- -The ``--report`` option gives you a fancy report after your test suite completes. (Requires ``--with-testing_base`` to also be set when ``--report`` is used because it's part of that plugin.) +If viewing ``pytest-html`` reports in [Jenkins](https://www.jenkins.io/), you may need to [configure Jenkins settings](https://stackoverflow.com/a/46197356/7058266) for the HTML to render correctly. This is due to [Jenkins CSP changes](https://www.jenkins.io/doc/book/security/configuring-content-security-policy/). That setting can be changed from ``Manage Jenkins`` > ``Script Console`` by running: -```bash -nosetests test_suite.py --report --browser=chrome +```js +System.setProperty("hudson.model.DirectoryBrowserSupport.CSP", "") ``` - -(NOTE: You can add ``--show_report`` to immediately display Nosetest reports after the test suite completes. Only use ``--show_report`` when running tests locally because it pauses the test run.) \ No newline at end of file +-------- + +You can also use ``--junit-xml=report.xml`` to get an xml report instead. Jenkins can use this file to display better reporting for your tests. + +```zsh +pytest test_suite.py --junit-xml=report.xml +``` + +-------- + +

pynose Test Reports:

+ +The ``pynose`` ``--report`` option gives you a fancy report after your tests complete. + +```zsh +pynose test_suite.py --report +``` + +Example pynose Test Report + +(NOTE: You can add ``--show-report`` to immediately display pynose reports after the test suite completes. Only use ``--show-report`` when running tests locally because it pauses the test run.) + +-------- + +

๐Ÿโšช Behave Dashboard & Reports:

+ +(The [behave_bdd/](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/behave_bdd) folder can be found in the [examples/](https://github.com/seleniumbase/SeleniumBase/tree/master/examples) folder.) + +```zsh +behave behave_bdd/features/ -D dashboard -D headless +``` + + + +You can also use ``--junit`` to get ``.xml`` reports for each Behave feature. Jenkins can use these files to display better reporting for your tests. + +```zsh +behave behave_bdd/features/ --junit -D rs -D headless +``` + +-------- + +
SeleniumBase
diff --git a/examples/example_logs/basic_test_info.txt b/examples/example_logs/basic_test_info.txt index a68b3185c21..6fa28811ba0 100644 --- a/examples/example_logs/basic_test_info.txt +++ b/examples/example_logs/basic_test_info.txt @@ -1,13 +1,31 @@ -Last_Page: https://xkcd.com/731/ -Browser: firefox -Traceback: File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 615, in run - testMethod() - File "/Users/michael/github/SeleniumBase/examples/test_fail.py", line 12, in test_find_army_of_robots_on_xkcd_desert_island +test_fail.py::FailingTests::test_find_army_of_robots_on_xkcd_desert_island +-------------------------------------------------------------------- +Last Page: https://xkcd.com/731/ + Duration: 1.63s + Browser: Chrome 108.0.5359.124 + Driver: chromedriver 108.0.5359.71 +Timestamp: 1672785363 (Unix Timestamp) + Date: Tuesday, January 3, 2023 + Time: 5:36:03 PM (EDT, UTC-05:00) +-------------------------------------------------------------------- +Traceback: File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/unittest/case.py", line 57, in testPartExecutor + yield + File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/unittest/case.py", line 623, in run + self._callTestMethod(testMethod) + File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/unittest/case.py", line 579, in _callTestMethod + if method() is not None: + ^^^^^^^^ + File "/Users/michael/github/SeleniumBase/examples/test_fail.py", line 16, in test_find_army_of_robots_on_xkcd_desert_island self.assert_element("div#ARMY_OF_ROBOTS", timeout=1) - File "/Users/michael/github/SeleniumBase/seleniumbase/fixtures/base_case.py", line 2166, in assert_element + File "/Users/michael/github/SeleniumBase/seleniumbase/fixtures/base_case.py", line 8279, in assert_element self.wait_for_element_visible(selector, by=by, timeout=timeout) - File "/Users/michael/github/SeleniumBase/seleniumbase/fixtures/base_case.py", line 2143, in wait_for_element_visible - self.driver, selector, by, timeout) - File "/Users/michael/github/SeleniumBase/seleniumbase/fixtures/page_actions.py", line 239, in wait_for_element_visible - selector, timeout, plural)) -Exception: Element {div#ARMY_OF_ROBOTS} was not visible after 1 second! \ No newline at end of file + File "/Users/michael/github/SeleniumBase/seleniumbase/fixtures/base_case.py", line 7718, in wait_for_element_visible + return page_actions.wait_for_element_visible( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/Users/michael/github/SeleniumBase/seleniumbase/fixtures/page_actions.py", line 428, in wait_for_element_visible + timeout_exception(NoSuchElementException, message) + File "/Users/michael/github/SeleniumBase/seleniumbase/fixtures/page_actions.py", line 191, in timeout_exception + raise exc(msg) + +Exception: Message: + Element {div#ARMY_OF_ROBOTS} was not present after 1 second! diff --git a/examples/get_ip_with_bing.py b/examples/get_ip_with_bing.py deleted file mode 100755 index 5a3c132e1c7..00000000000 --- a/examples/get_ip_with_bing.py +++ /dev/null @@ -1,12 +0,0 @@ -import time -from seleniumbase import BaseCase - - -class MyTestClass(BaseCase): - - def test_get_ip_address_with_bing(self): - self.open('https://www.bing.com/search?q=what+is+my+ip') - ip_address = self.get_text("#b_results li.b_top") - print("\n\n%s\n" % ip_address) - print("\nThe browser will close automatically in 7 seconds...") - time.sleep(7) diff --git a/examples/github_test.py b/examples/github_test.py old mode 100755 new mode 100644 index f5d868a8695..e9feda8a1c4 --- a/examples/github_test.py +++ b/examples/github_test.py @@ -1,24 +1,19 @@ from seleniumbase import BaseCase -import time +BaseCase.main(__name__, __file__) class GitHubTests(BaseCase): - - # Selenium can trigger GitHub's abuse detection mechanism: - # "You have triggered an abuse detection mechanism." - # "Please wait a few minutes before you try again." - # To avoid this, slow down Selenium actions. - def slow_click(self, css_selector): - time.sleep(1.05) - self.click(css_selector) - def test_github(self): - self.open("https://github.com/") - self.update_text("input.header-search-input", "SeleniumBase\n") - self.slow_click('a[href="/seleniumbase/SeleniumBase"]') + if self.headless or self.page_load_strategy == "none": + self.goto_if_not_url("about:blank") + print("\n Unsupported mode for this test.") + self.skip("Unsupported mode for this test.") + self.goto("https://github.com/seleniumbase/SeleniumBase") + self.click_if_visible('[data-action="click:signup-prompt#dismiss"]') + self.highlight('[class*="PageLayout-Content-"][data-width="large"]') + self.highlight('[class*="PageLayout-PaneWrapper"]') self.assert_element("div.repository-content") - self.assert_text("SeleniumBase", "h1") - self.slow_click('a[title="seleniumbase"]') - self.slow_click('a[title="fixtures"]') - self.slow_click('a[title="base_case.py"]') - self.assert_text("Code", "nav a.selected") + self.assert_text("SeleniumBase", "strong a") + self.js_click('a[title="seleniumbase"]') + self.slow_click('td[class*="large"] a[title="fixtures"]') + self.highlight('td[class*="large"] a[title="base_case.py"]', loops=8) diff --git a/examples/gui_test_runner.py b/examples/gui_test_runner.py old mode 100755 new mode 100644 index 8a25eb5056e..dc98fbe6a7b --- a/examples/gui_test_runner.py +++ b/examples/gui_test_runner.py @@ -1,109 +1,121 @@ -''' -GUI TEST RUNNER +"""GUI TEST RUNNER Run by Typing: "python gui_test_runner.py" -''' - -try: - # Python 2 - from Tkinter import Tk, Frame, Button, Label -except Exception: - # Python 3 - from tkinter import Tk, Frame, Button, Label -import os +(Use Python 3)""" +import subprocess +from tkinter import Tk, Frame, Button, Label class App: - def __init__(self, master): - frame = Frame() + frame = Frame(master) frame.pack() - root.title("Select Test Job To Run") self.label = Label(root, width=40).pack() self.title = Label(frame, text="", fg="black").pack() self.title1 = Label( - frame, text="Basic Test Run in Chrome:", fg="blue").pack() + frame, + text=("Run a Test in Chrome (default):"), + fg="blue", + ).pack() self.run1 = Button( - frame, command=self.run_1, - text=("pytest my_first_test.py" - " --browser=chrome")).pack() + frame, + command=self.run_1, + text=("pytest my_first_test.py"), + fg="green", + ).pack() self.title2 = Label( - frame, text="Basic Test Run in Firefox:", fg="blue").pack() + frame, + text=("Run a Test in Firefox:"), + fg="blue", + ).pack() self.run2 = Button( - frame, command=self.run_2, - text=("pytest my_first_test.py" - " --browser=firefox")).pack() + frame, + command=self.run_2, + text=("pytest my_first_test.py --firefox"), + fg="green", + ).pack() self.title3 = Label( - frame, text="Basic Test Run in Demo Mode:", fg="blue").pack() + frame, + text="Run a Test with Demo Mode:", + fg="blue", + ).pack() self.run3 = Button( - frame, command=self.run_3, - text=("pytest my_first_test.py" - " --browser=chrome --demo_mode")).pack() + frame, + command=self.run_3, + text=("pytest my_first_test.py --demo_mode"), + fg="green", + ).pack() self.title4 = Label( frame, - text="Basic Failing Test Run with Screenshots:", - fg="blue").pack() + text="Run a Parameterized Test and reuse session:", + fg="blue", + ).pack() self.run4 = Button( - frame, command=self.run_4, - text=("nosetests test_fail.py" - " --browser=chrome --demo_mode")).pack() + frame, + command=self.run_4, + text=("pytest parameterized_test.py --rs"), + fg="green", + ).pack() self.title5 = Label( frame, - text="Basic Failing Test Suite Run with Test Report:", - fg="blue").pack() + text="Run a Failing Test with a Test Report:", + fg="blue", + ).pack() self.run5 = Button( - frame, command=self.run_5, - text=("nosetests test_suite.py --report --show_report")).pack() + frame, + command=self.run_5, + text=("pytest test_fail.py --html=report.html"), + fg="red", + ).pack() self.title6 = Label( frame, - text="Basic Failing Test Run showing the Multiple-Checks feature:", - fg="blue").pack() + text="Run a Failing Test Suite with the Dashboard:", + fg="blue", + ).pack() self.run6 = Button( - frame, command=self.run_6, - text=("nosetests delayed_assert_test.py --browser=chrome")).pack() + frame, + command=self.run_6, + text=("pytest test_suite.py --rs --dashboard"), + fg="red", + ).pack() self.title7 = Label( frame, - text="Use MySQL DB Reporting: (See ReadMe.md for Setup Steps!)", - fg="blue").pack() + text="Run a Failing Test with Deferred Asserts:", + fg="blue", + ).pack() self.run7 = Button( - frame, command=self.run_7, - text=("nosetests test_suite.py" - " --browser=chrome --with-db_reporting")).pack() + frame, + command=self.run_7, + text=("pytest test_deferred_asserts.py"), + fg="red", + ).pack() self.end_title = Label(frame, text="", fg="black").pack() self.quit = Button(frame, text="QUIT", command=frame.quit).pack() def run_1(self): - os.system( - 'pytest my_first_test.py --browser=chrome') + subprocess.Popen("pytest my_first_test.py", shell=True) def run_2(self): - os.system( - 'pytest my_first_test.py --browser=firefox') + subprocess.Popen("pytest my_first_test.py --firefox", shell=True) def run_3(self): - os.system( - 'pytest my_first_test.py --demo_mode' - ' --browser=chrome') + subprocess.Popen("pytest my_first_test.py --demo_mode", shell=True) def run_4(self): - os.system( - 'nosetests test_fail.py --browser=chrome --demo_mode') + subprocess.Popen("pytest parameterized_test.py --rs", shell=True) def run_5(self): - os.system( - 'nosetests test_suite.py --report --show_report') + subprocess.Popen("pytest test_fail.py --html=report.html", shell=True) def run_6(self): - os.system( - 'nosetests delayed_assert_test.py --browser=chrome') + subprocess.Popen("pytest test_suite.py --rs --dashboard", shell=True) def run_7(self): - os.system( - 'nosetests test_suite.py' - ' --browser=chrome --with-db_reporting') + subprocess.Popen("pytest test_deferred_asserts.py", shell=True) if __name__ == "__main__": root = Tk() - root.minsize(532, 444) + root.title("Select Test Job To Run") + root.minsize(320, 420) app = App(root) root.mainloop() diff --git a/examples/hack_the_planet.py b/examples/hack_the_planet.py new file mode 100644 index 00000000000..217d860dcbf --- /dev/null +++ b/examples/hack_the_planet.py @@ -0,0 +1,303 @@ +""" Video Link: https://youtu.be/1s-Tj65AKZA """ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class HackTests(BaseCase): + def test_all_your_base_are_belong_to_us(self): + self.set_window_size(1250, 740) + ayb = "ALL YOUR BASE" + abtu = "ARE BELONG TO US" + aybabtu = "%s %s" % (ayb, abtu) + sb_banner_logo = "//seleniumbase.github.io/cdn/img/sb_logo_10.png" + sb_dashboard_logo = "//seleniumbase.github.io/img/dash_pie_3.png" + wiki = "https://en.wikipedia.org/wiki/All_your_base_are_belong_to_us" + + self.goto(wiki) + self.click_if_visible('button[aria-label="Close"]') + self.set_text_content("h1#firstHeading", aybabtu) + self.set_text_content("#ca-history a", aybabtu) + self.set_text_content("#n-mainpage-description a", "ALL") + self.set_text_content("#n-contents a", "YOUR") + self.set_text_content("#n-currentevents a", "BASE") + self.set_text_content("#n-randompage a", "ARE") + self.set_text_content("#n-aboutsite a", "BELONG") + self.set_text_content("#n-contactpage a", "TO US") + self.highlight("h1#firstHeading", loops=5, scroll=False) + zoom_in = "#ca-history a{zoom: 1.8;-moz-transform: scale(1.8);}" + self.add_css_style(zoom_in) + self.highlight("#ca-history a", loops=5, scroll=False) + zoom_in = "img[src*=Ayb]{zoom: 1.6;-moz-transform: scale(1.6);}" + self.add_css_style(zoom_in) + self.highlight("img[src*=Ayb]", loops=10, scroll=False) + + if not self.headless: + self.goto("https://www.apple.com/store") + self.set_text_content("div.rs-shop-subheader", aybabtu) + self.set_text_content('#shelf-1 a[href*="mac"]', "ALL") + self.set_text_content('#shelf-1 a[href*="iphone"]', "YOUR") + self.set_text_content('#shelf-1 a[href*="ipad"]', "BASE") + self.remove_element('#shelf-1 [role="listitem"]:nth-child(5)') + self.set_text_content('#shelf-1 a[href*="watch"]', "ARE") + self.set_text_content('#shelf-1 a[href*="airpods"]', "BELONG") + self.set_text_content('#shelf-1 a[href*="airtag"]', "TO") + self.set_text_content('#shelf-1 a[href*="tv"]', "US") + self.set_text_content('#shelf-1 a[href*="homepod"]', ".") + self.set_text_content("#shelf-2_section h2", ayb + ". ") + self.set_text_content("#shelf-2_section span", abtu + ". ") + self.highlight("div.rs-shop-subheader", loops=6, scroll=False) + self.highlight("#shelf-1", loops=2, scroll=False) + self.highlight('#shelf-1 a[href*="mac"]', loops=1, scroll=False) + self.highlight('#shelf-1 a[href*="iphone"]', loops=1, scroll=False) + self.highlight('#shelf-1 a[href*="ipad"]', loops=3, scroll=False) + self.highlight('#shelf-1 a[href*="watch"]', loops=1, scroll=False) + self.highlight('#shelf-1 a[href*="airpod"]', loops=1, scroll=False) + self.highlight('#shelf-1 a[href*="airtag"]', loops=1, scroll=False) + self.highlight('#shelf-1 a[href*="tv"]', loops=3, scroll=False) + self.highlight("#shelf-2_section h2", loops=5, scroll=False) + self.highlight("#shelf-2_section span", loops=6, scroll=False) + + self.goto("https://google.com/ncr") + self.click_if_visible('button:contains("Accept all")') + self.hide_elements("iframe") + if self.is_element_visible('a[href*="about.google"]'): + self.set_text_content('a[href*="about.google"]', ayb) + if self.is_element_visible('a[href*="store.google"]'): + self.set_text_content('a[href*="store.google"]', abtu) + self.set_text_content('a[href*="mail.google.com"]', ayb) + self.set_text_content('a[href*="google.com/img"]', abtu) + self.set_attributes('[value="Google Search"]', "value", ayb) + self.set_attributes('[value="I\'m Feeling Lucky"]', "value", abtu) + self.hide_elements("iframe") + zoom_in = "a{zoom: 1.2;-moz-transform: scale(1.2);}" + self.add_css_style(zoom_in) + zoom_in = ( + '[value="ALL YOUR BASE"]{zoom: 1.3;-moz-transform: scale(1.3);}' + '[value="ARE BELONG TO US"]{zoom: 1.3;-moz-transform: scale(1.3);}' + ) + self.add_css_style(zoom_in) + self.hide_elements("iframe") + if self.is_element_visible('a[href*="about.google"]'): + self.highlight('a[href*="about.google"]', loops=3) + if self.is_element_visible('a[href*="store.google"]'): + self.highlight('a[href*="store.google"]', loops=3) + self.highlight('a[href*="mail.google.com"]', loops=3) + self.highlight('a[href*="google.com/img"]', loops=3) + self.highlight('form[role="search"]', loops=8) + + self.goto("https://github.com/solutions/use-case/devops") + self.set_text_content("#hero-section-brand-heading", aybabtu) + self.highlight("#hero-section-brand-heading", loops=14, scroll=False) + + self.goto("https://dev.to/top/infinity") + self.click_if_visible('button[aria-label="Close campaign banner"]') + self.click_if_visible('svg[aria-label="Close campaign banner"]') + self.click_if_visible('button[id*="sponsorship-close-trigger"]') + if self.is_element_visible('main div:contains("Pinned")'): + self.hide_elements('main div:contains("Pinned")') + if self.is_element_visible('[data-type-of="in_house"]'): + self.hide_elements('[data-type-of="in_house"]') + self.set_text_content('nav a[data-text="Relevant"]', "ALL") + self.set_text_content('nav a[data-text="Latest"]', "YOUR") + self.set_text_content('nav a[data-text="Top"]', "BASE") + self.set_text_content('nav a[data-text="Week"]', "ARE") + self.set_text_content('nav a[data-text="Month"]', "BELONG") + self.set_text_content('nav a[data-text="Year"]', "TO") + self.set_text_content('nav a[data-text="Infinity"]', "US") + self.click_if_visible('button[id*="sponsorship-close-trigger"]') + self.set_text_content('aside a[class*="tful"]', aybabtu) + self.set_text_content('aside a[aria-label="Create new account"]', ayb) + self.set_text_content('aside a[aria-label="Log in"]', abtu) + self.set_text_content('aside a[class*="tful"]:nth-child(2)', aybabtu) + self.set_text_content('aside a[class*="tful"]:nth-child(3)', aybabtu) + self.set_text_content('aside a[class*="tful"]:nth-child(4)', aybabtu) + self.set_text_content('aside a[class*="tful"]:nth-child(5)', aybabtu) + self.set_attribute("a.crayons-avatar img", "src", sb_dashboard_logo) + self.set_text_content(".profile-preview-card button", "SeleniumBase") + if self.is_element_visible('h2 a[href*="simonh"]'): + self.set_text_content('h2 a[href*="simonh"]', aybabtu) + if self.is_element_visible('main h2 a[id*="article"]'): + self.set_text_content('main h2 a[id*="article"]', aybabtu) + self.highlight('[aria-label="Primary sidebar"] div div', scroll=False) + self.highlight('nav a[data-text="Relevant"]', loops=1, scroll=False) + self.highlight('nav a[data-text="Latest"]', loops=1, scroll=False) + self.highlight('nav a[data-text="Top"]', loops=2, scroll=False) + self.highlight('nav a[data-text="Week"]', loops=1, scroll=False) + self.highlight('nav a[data-text="Month"]', loops=1, scroll=False) + self.highlight('nav a[data-text="Year"]', loops=1, scroll=False) + self.highlight('nav a[data-text="Infinity"]', loops=3, scroll=False) + if self.is_element_visible('main h2 a[id*="article"]'): + self.highlight('main h2 a[id*="article"]', loops=7, scroll=False) + self.highlight("section.crayons-card", loops=7, scroll=False) + + self.goto("https://store.steampowered.com/") + self.set_text_content('a[href*="steamcommunity.com/"]', " ") + self.set_text_content('div.content a[href*="/about/"]', " ") + self.set_text_content('div.content a[href*="help.steam"]', aybabtu) + zoom_in = '[href*="help.steam"]{zoom: 1.5;-moz-transform: scale(1.5);}' + self.add_css_style(zoom_in) + self.highlight('div.content a[href*="help.steam"]', loops=12) + + self.goto("https://xkcd.com/286/") + self.set_text_content('a[href="/archive"]', "ALL") + self.set_text_content('a[href*="what-if"]', "YOUR BASE") + self.set_text_content('a[href*="/about"]', abtu) + self.remove_element('li:contains("Feed")') + self.remove_element('li:contains("TW")') + self.remove_element('li:contains("Books")') + self.remove_element('li:contains("What")') + self.remove_element('li:contains("WI")') + self.set_attributes("#countdown img", "src", sb_banner_logo) + self.set_text_content("#ctitle", aybabtu) + self.set_text_content('a[rel="prev"]', "All") + self.set_text_content('a[href*="random"]', "Your") + self.set_text_content('a[rel="next"]', "Base") + self.highlight("#topLeft ul", loops=5, scroll=False) + self.highlight('a[href="/archive"]', loops=1, scroll=False) + self.highlight('a[href*="what-if"]', loops=3, scroll=False) + self.highlight('a[href*="/about"]', loops=5, scroll=False) + self.highlight('a[rel="prev"]', loops=1, scroll=False) + self.highlight('a[href*="random"]', loops=1, scroll=False) + self.highlight('a[rel="next"]', loops=3, scroll=False) + self.highlight("#ctitle", loops=7, scroll=False) + + self.goto("https://www.nintendo.com/whatsnew/") + self.set_text_content("main section h1", aybabtu) + self.highlight("main section h1", loops=10, scroll=False) + + if not self.headless: + self.goto("https://support.gog.com/hc/en-us?product=gog") + self.set_text_content("div.intro-title", aybabtu) + self.set_text_content("h4", aybabtu) + self.highlight("div.intro-title", loops=8, scroll=False) + self.highlight("h4", loops=8, scroll=False) + + self.goto( + "https://slack.com/help/articles/204379773-Upload-a-Slack-icon" + ) + self.set_text_content("h1", aybabtu) + self.set_text_content("h1.article_title", aybabtu) + self.highlight("h1", loops=4, scroll=False) + self.highlight("h1.article_title", loops=5, scroll=False) + + self.goto("https://kubernetes.io/") + self.set_text_content('nav a[href="/docs/home/"]', "ALL") + self.set_text_content('nav a[href="/blog/"]', "YOUR") + self.set_text_content('nav a[href="/training/"]', "BASE") + self.set_text_content('nav a[href="/careers/"]', "ARE") + self.set_text_content('nav a[href="/partners/"]', "BELONG") + self.set_text_content('nav a[href="/community/"]', "TO") + self.set_text_content("nav a.dropdown-toggle", "US") + if self.is_element_visible("h1"): + self.set_text_content("h1", aybabtu) + self.highlight("nav ul.navbar-nav", loops=3, scroll=False) + self.highlight('nav a[href="/docs/home/"]', loops=1, scroll=False) + self.highlight('nav a[href="/blog/"]', loops=1, scroll=False) + self.highlight('nav a[href="/training/"]', loops=2, scroll=False) + self.highlight('nav a[href="/careers/"]', loops=1, scroll=False) + self.highlight('nav a[href="/partners/"]', loops=1, scroll=False) + self.highlight('nav a[href="/community/"]', loops=1, scroll=False) + self.highlight("nav a.dropdown-toggle", loops=2, scroll=False) + if self.is_element_visible("h1"): + self.highlight("h1", loops=6, scroll=False) + + self.goto("https://www.selenium.dev/") + if self.is_element_visible('button[data-dismiss="alert"] span'): + self.js_click('button[data-dismiss="alert"] span', scroll=False) + self.set_attributes("a.dropdown-toggle", "class", "nav-link") + self.set_text_content('li a:contains("About")', "ALL") + self.set_text_content('li a:contains("Downloads")', "YOUR") + self.set_text_content('li a:contains("Documentation")', "BASE") + self.set_text_content('li a:contains("Projects")', "ARE") + self.set_text_content('li a:contains("Support")', "BELONG") + self.set_text_content('li a:contains("Blog")', "TO") + self.set_text_content('li a:contains("English")', "US") + self.set_text_content("div.mx-auto p", aybabtu) + self.set_text_content("h2", aybabtu) + if self.is_element_visible('button[data-dismiss="alert"] span'): + self.js_click('button[data-dismiss="alert"] span', scroll=False) + zoom_in = "div.mx-auto p{zoom: 1.1;-moz-transform: scale(1.1);}" + self.add_css_style(zoom_in) + self.highlight("div#main_navbar", loops=1, scroll=False) + self.highlight('li a:contains("ALL")', loops=1, scroll=False) + self.highlight('li a:contains("YOUR")', loops=1, scroll=False) + self.highlight('li a:contains("BASE")', loops=2, scroll=False) + self.highlight('li a:contains("ARE")', loops=1, scroll=False) + self.highlight('li a:contains("BELONG")', loops=1, scroll=False) + self.highlight('li a:contains("TO")', loops=1, scroll=False) + self.highlight('li a:contains("US")', loops=2, scroll=False) + self.highlight("div.mx-auto p", loops=6, scroll=False) + self.highlight("h2", loops=8, scroll=False) + + self.goto("https://www.python.org/") + self.set_text_content('a[class="donate-button"]', ayb) + self.set_text_content("#about a", "ALL") + self.set_text_content("#downloads a", "YOUR") + self.set_text_content("#documentation a", "BASE") + self.set_text_content("#community a", "ARE") + self.set_text_content("#success-stories a", "BELONG") + self.set_text_content("#news a", "TO") + self.set_text_content("#events a", "US") + self.highlight('a[class="donate-button"]', loops=4, scroll=False) + self.highlight("nav#mainnav", loops=5, scroll=False) + self.highlight("#about a", loops=1, scroll=False) + self.highlight("#downloads a", loops=1, scroll=False) + self.highlight("#documentation a", loops=2, scroll=False) + self.highlight("#community a", loops=1, scroll=False) + self.highlight("#success-stories a", loops=1, scroll=False) + self.highlight("#news a", loops=1, scroll=False) + self.highlight("#events a", loops=2, scroll=False) + + self.goto("https://docs.pytest.org/") + self.set_text_content("h1", "pytest: " + aybabtu) + self.highlight("h1", loops=10, scroll=False) + + self.goto("https://wordpress.com/") + zoom_out = "h1{zoom: 0.8;-moz-transform: scale(0.8);}" + self.add_css_style(zoom_out) + zoom_in = "a.wp-element-button{zoom: 1.4;-moz-transform: scale(1.4);}" + self.add_css_style(zoom_in) + self.set_text_content("h1", aybabtu) + self.set_text_content("a.wp-element-button", "Use SeleniumBase!") + self.highlight("h1", loops=8, scroll=False) + self.highlight("a.wp-element-button", loops=8, scroll=False) + + self.goto("https://seleniumbase.com/") + self.set_text_content("h1", aybabtu) + self.highlight("h1", loops=10, scroll=False) + + self.goto("https://pypi.org/") + self.set_text_content('a[href="/sponsors/"]', aybabtu) + self.set_text_content("h1", aybabtu) + self.set_value("input#search", aybabtu, scroll=False) + self.highlight('a[href="/sponsors/"]', loops=6, scroll=False) + self.highlight("h1", loops=6, scroll=False) + self.highlight("input#search", loops=8, scroll=False) + + self.goto("https://status.iboss.com/ibcloud/app/cloudStatus.html") + self.wait_for_element_clickable('div[translate*="cloudStatus"]') + self.set_text_content('div[translate*="cloudStatus"]', ayb) + self.set_text_content('div[translate*="maintenance"]', "ARE") + self.set_text_content('div[translate*="advisory"]', "BELONG") + self.set_text_content('div[translate*="incident"]', "TO US") + self.set_text_content("h1", "Cloud Status - " + aybabtu) + self.highlight("nav div.ibcloud-header-contents", loops=3) + self.highlight('div[translate*="cloudStatus"]', loops=4) + self.highlight('div[translate*="maintenance"]', loops=1) + self.highlight('div[translate*="advisory"]', loops=1) + self.highlight('div[translate*="incident"]', loops=3) + self.highlight("h1", loops=9, scroll=False) + + self.goto("https://git-scm.com/") + self.set_text_content("span#tagline", aybabtu) + self.highlight("span#tagline", loops=10, scroll=False) + + self.goto("https://pragprog.com/") + self.set_text_content("header p", aybabtu) + zoom_in = "header p{zoom: 1.35;-moz-transform: scale(1.35);}" + self.add_css_style(zoom_in) + self.highlight("header p", loops=10, scroll=False) + + self.goto("https://seleniumbase.io/") + self.set_text_content("h1", aybabtu) + self.highlight("h1", loops=10, scroll=False) diff --git a/examples/handle_alert_test.py b/examples/handle_alert_test.py new file mode 100644 index 00000000000..570e329c918 --- /dev/null +++ b/examples/handle_alert_test.py @@ -0,0 +1,22 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class HandleAlertTests(BaseCase): + def test_alerts(self): + self.goto("about:blank") + self.execute_script('window.alert("ALERT!!!");') + self.sleep(1) # Not needed (Lets you see the alert pop up) + self.assert_true(self.is_alert_present()) + self.accept_alert() + self.sleep(1) # Not needed (Lets you see the alert go away) + self.execute_script('window.prompt("My Prompt","defaultText");') + self.sleep(1) # Not needed (Lets you see the alert pop up) + alert = self.switch_to_alert() + self.assert_equal(alert.text, "My Prompt") # Not input field + self.dismiss_alert() + self.sleep(1) # Not needed (Lets you see the alert go away) + self.assert_false(self.is_alert_present()) + if self.browser == "safari" and self._reuse_session: + # Alerts can freeze Safari if reusing the browser session + self.driver.quit() diff --git a/examples/iframe_tests.py b/examples/iframe_tests.py new file mode 100644 index 00000000000..1de66c38587 --- /dev/null +++ b/examples/iframe_tests.py @@ -0,0 +1,52 @@ +"""Use SeleniumBase methods to interact with iframes.""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class FrameTests(BaseCase): + def test_iframe_basics(self): + self.goto("https://seleniumbase.io/w3schools/iframes.html") + self.assert_title("iframe Testing") + self.click("button#runbtn") + self.switch_to_frame("iframeResult") # Enter the iframe + self.assert_text("HTML Iframes", "h2") + self.switch_to_frame('[title*="Iframe"]') # Enter iframe inside iframe + self.assert_text("This page is displayed in an iframe", "h1") + self.switch_to_parent_frame() # Exit only the inner iframe + self.assert_text("Use CSS width & height to specify", "p") + self.switch_to_frame('[title*="Iframe"]') # Enter iframe inside iframe + self.assert_text("seleniumbase.io/w3schools/iframes", "a") + self.switch_to_default_content() # Exit all iframes + self.click("button#runbtn") + self.switch_to_frame("iframeResult") # Go back inside 1st iframe + self.highlight('iframe[title="Iframe Example"]') + + def test_iframes_with_context_manager(self): + self.goto("https://seleniumbase.io/w3schools/iframes.html") + self.assert_title("iframe Testing") + self.click("button#runbtn") + with self.frame_switch("iframeResult"): + self.assert_text("HTML Iframes", "h2") + with self.frame_switch('[title*="Iframe"]'): + self.assert_text("This page is displayed in an iframe", "h1") + self.assert_text("Use CSS width & height to specify", "p") + with self.frame_switch('[title*="Iframe"]'): + self.assert_text("seleniumbase.io/w3schools/iframes", "a") + self.click("button#runbtn") + with self.frame_switch("iframeResult"): + self.highlight('iframe[title="Iframe Example"]') + + def test_set_content_to_frame(self): + self.goto("https://seleniumbase.io/w3schools/iframes.html") + self.assert_title("iframe Testing") + self.click("button#runbtn") + self.set_content_to_frame("iframeResult") + self.highlight('iframe[title="Iframe Example"]') + self.set_content_to_frame("iframe") + self.assert_element_not_visible("iframe") + self.highlight("body") + self.set_content_to_parent() + self.highlight('iframe[title="Iframe Example"]') + self.set_content_to_default() + self.click("button#runbtn") + self.highlight("#iframeResult") diff --git a/examples/image_test.py b/examples/image_test.py deleted file mode 100755 index aeda74a4843..00000000000 --- a/examples/image_test.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Pull an image from a website and save it as a PNG file. -""" - -from seleniumbase import BaseCase - - -class ImageTest(BaseCase): - - def test_pull_image_from_website(self): - self.open("https://xkcd.com/1117/") - selector = "#comic" - file_name = "comic.png" - folder = "images_exported" - self.save_element_as_image_file(selector, file_name, folder) - print('"%s/%s" has been saved!' % (folder, file_name)) diff --git a/examples/ip_cow_test.py b/examples/ip_cow_test.py deleted file mode 100755 index c96cb20bf1b..00000000000 --- a/examples/ip_cow_test.py +++ /dev/null @@ -1,13 +0,0 @@ -import time -from seleniumbase import BaseCase - - -class MyTestClass(BaseCase): - - def test_ip_cow(self): - self.open('https://www.ipcow.com/') - ip_data = self.get_text("form table") - print("\n\n*** IP and Browser Data: ***") - print(ip_data) - print("\nThe browser will close automatically in 7 seconds...") - time.sleep(7) diff --git a/examples/locale_code_test.py b/examples/locale_code_test.py new file mode 100644 index 00000000000..323eb63f0e1 --- /dev/null +++ b/examples/locale_code_test.py @@ -0,0 +1,22 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class LocaleCodeTests(BaseCase): + def test_locale_code(self): + self.goto("about:blank") + locale_code = self.get_locale_code() # navigator.language + print("\nYour Browser's Locale Code: %s" % locale_code) + if self.browser == "chrome" and not self.headless: + self.goto("chrome://settings/languages") + language_info = self.get_text( + "settings-ui::shadow " + "settings-main::shadow " + "settings-languages-page-index::shadow " + "settings-languages-page::shadow " + "settings-section " + "#languagesSection div.start div" + ) + print("Language info (chrome://settings/languages):") + print(language_info) + self.sleep(1) diff --git a/examples/master_qa/ReadMe.md b/examples/master_qa/ReadMe.md old mode 100755 new mode 100644 index 2a959d16877..9db88a2286f --- a/examples/master_qa/ReadMe.md +++ b/examples/master_qa/ReadMe.md @@ -1,7 +1,62 @@ -## MasterQA + -MasterQA combines web automation with manual verification to create a flexible testing experience that greatly enhances pure manual QA. +![](https://seleniumbase.github.io/cdn/img/masterqa_logo.png "MasterQA") -### More info on MasterQA can be found [here](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/masterqa/ReadMe.md). +

MasterQA combines automation with manual verification steps.

-![](https://cdn2.hubspot.net/hubfs/100006/images/masterqa6.gif "MasterQA")
+![](https://seleniumbase.github.io/cdn/gif/masterqa6.gif "MasterQA") + +Here's code from [basic_masterqa_test_0.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/master_qa/basic_masterqa_test_0.py): + +```python +from seleniumbase import MasterQA +MasterQA.main(__name__, __file__) + +class MasterQATests(MasterQA): + def test_masterqa(self): + self.goto("https://xkcd.com/1700/") + self.verify("Do you see a webcomic?") + self.goto("https://seleniumbase.io/demo_page") + self.highlight('table') + self.verify("Do you see elements in a table?") + self.goto("https://seleniumbase.io/devices/") + self.highlight("div.mockup-wrapper") + self.verify("Do you see 4 computer devices?") +``` + +After each automation checkpoint, a pop-up window will ask the user questions for each verification command. + +When the test run completes, as seen from [this longer example](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/master_qa/masterqa_test_1.py), you'll reach the results page that appears after answering all the verification questions. (Failed verifications generate links to screenshots and log files.) + +![](https://seleniumbase.github.io/cdn/img/mqa_hybrid.png "MasterQA") + +You may have noticed the `Incomplete Test Runs` row on the results page. If the value for that is not zero, it means that one of the automated steps failed. This could happen if you tell your script to perform an action on an element that doesn't exist. Now that we're mixing automation with manual QA, it's good to tell apart the failures from each. The results_table CSV file contains a spreadsheet with the details of each failure (if any) for both manual and automated steps. + +**How to run the example tests from scratch:** + +```zsh +git clone https://github.com/seleniumbase/SeleniumBase.git +cd SeleniumBase +pip install . +cd examples/master_qa +pytest basic_masterqa_test_0.py +pytest masterqa_test_1.py +``` + +At the end of your test run, you'll receive a report with results, screenshots, and log files. Close the Results Page window when you're done. + +**Check out [masterqa_test_1.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/master_qa/masterqa_test_1.py) to learn how to write your own MasterQA tests:** + +You'll notice that tests are written the same way as regular [SeleniumBase](https://github.com/seleniumbase/SeleniumBase) tests, with the key difference being a different import: `from seleniumbase import MasterQA` rather than `from seleniumbase import BaseCase`. Now your Python test class will import `MasterQA` instead of `BaseCase`. + +To add a manual verification step, use `self.verify()` in the code after each part of your test that needs a manual verification step. If you want to include a custom question, add text inside that call (in quotes). Example: + +```python +self.verify() + +self.verify("Can you find the moon?") +``` + +-------- + +MasterQA is powered by [SeleniumBase](https://github.com/seleniumbase/SeleniumBase). diff --git a/examples/master_qa/__init__.py b/examples/master_qa/__init__.py old mode 100755 new mode 100644 diff --git a/examples/master_qa/basic_masterqa_test.py b/examples/master_qa/basic_masterqa_test.py deleted file mode 100755 index 39580e118de..00000000000 --- a/examples/master_qa/basic_masterqa_test.py +++ /dev/null @@ -1,12 +0,0 @@ -from seleniumbase import MasterQA - - -class MasterQATests(MasterQA): - - def test_masterqa(self): - self.open("https://xkcd.com/1700/") - self.verify("Do you see a webcomic?") - self.highlight_click('link=Blag') - self.verify('Do you see a blog archive?') - self.highlight_update_text("input#s", "Dragons\n") - self.verify('Do you see "dragons" in the search results?') diff --git a/examples/master_qa/basic_masterqa_test_0.py b/examples/master_qa/basic_masterqa_test_0.py new file mode 100644 index 00000000000..05b162fd689 --- /dev/null +++ b/examples/master_qa/basic_masterqa_test_0.py @@ -0,0 +1,15 @@ +from seleniumbase import MasterQA +MasterQA.main(__name__, __file__) + + +class MasterQATests(MasterQA): + def test_masterqa(self): + self.goto("https://seleniumbase.io/devices/") + self.highlight("div.mockup-wrapper") + self.verify("Do you see 4 computer devices?") + self.goto("https://seleniumbase.io/demo_page") + self.highlight("table") + self.verify("Do you see elements in a table?") + self.goto("https://xkcd.com/1700/") + self.highlight("#comic") + self.verify("Do you see a webcomic?") diff --git a/examples/master_qa/masterqa_test.py b/examples/master_qa/masterqa_test_1.py old mode 100755 new mode 100644 similarity index 62% rename from examples/master_qa/masterqa_test.py rename to examples/master_qa/masterqa_test_1.py index 379fed5caec..4a97f6dafd1 --- a/examples/master_qa/masterqa_test.py +++ b/examples/master_qa/masterqa_test_1.py @@ -1,25 +1,27 @@ from seleniumbase import MasterQA +MasterQA.main(__name__, __file__) class MasterQATests(MasterQA): - def test_xkcd(self): - self.open("https://xkcd.com/1512/") + self.goto("https://xkcd.com/1512/") for i in range(4): self.click('a[rel="next"]') for i in range(3): self.click('a[rel="prev"]') self.verify() - self.open("https://xkcd.com/1520/") + self.goto("https://xkcd.com/1520/") for i in range(2): self.click('a[rel="next"]') self.verify("Can you find the moon?") self.click('a[rel="next"]') self.verify("Do the drones look safe?") - self.click_link_text('Blag') - self.update_text("input#s", "Robots!\n") - self.verify("Does it say 'Hooray robots' on the page?") - self.open("https://xkcd.com/213/") + + self.goto("https://seleniumbase.io/devices/") + self.type("input#urlInput", "seleniumbase.io/error_page\n") + self.verify("Do you see Octocat in a Jedi knight robe?") + + self.goto("https://xkcd.com/213/") for i in range(5): self.click('a[rel="prev"]') self.verify("Does the page say 'Abnormal Expressions'?") diff --git a/examples/master_qa/pytest.ini b/examples/master_qa/pytest.ini new file mode 100644 index 00000000000..0b8fa185bab --- /dev/null +++ b/examples/master_qa/pytest.ini @@ -0,0 +1,47 @@ +[pytest] + +# Display console output. Disable cacheprovider: +addopts = --capture=tee-sys -p no:cacheprovider + +# Skip these directories during test collection: +norecursedirs = .* build dist recordings temp assets + +# Ignore DeprecationWarning, PytestUnknownMarkWarning +filterwarnings = + ignore::pytest.PytestWarning + ignore:.*U.*mode is deprecated:DeprecationWarning + +# Configure the junit_family option explicitly: +junit_family = legacy + +# Set pytest discovery rules: +# (Most of the rules here are similar to the default rules.) +# (Inheriting unittest.TestCase could override these rules.) +python_files = test_*.py *_test.py *_tests.py *_suite.py *_test_*.py +python_classes = Test* *Test* *Test *Tests *Suite +python_functions = test_* + +# Common pytest markers used in examples: +# (pytest may require marker registration to prevent warnings.) +# (Future versions may turn those marker warnings into errors.) +markers = + marker1: custom marker + marker2: custom marker + marker3: custom marker + marker_test_suite: custom marker + expected_failure: custom marker + local: custom marker + remote: custom marker + offline: custom marker + develop: custom marker + qa: custom marker + ci: custom marker + e2e: custom marker + ready: custom marker + smoke: custom marker + deploy: custom marker + active: custom marker + master: custom marker + release: custom marker + staging: custom marker + production: custom marker diff --git a/examples/migration/__init__.py b/examples/migration/__init__.py new file mode 100755 index 00000000000..e69de29bb2d diff --git a/examples/migration/protractor/ReadMe.md b/examples/migration/protractor/ReadMe.md new file mode 100644 index 00000000000..50e5602d42f --- /dev/null +++ b/examples/migration/protractor/ReadMe.md @@ -0,0 +1,25 @@ +## Support for migrating from Protractor to SeleniumBase + +๐Ÿ”ต The Protractor/Angular tests from [github.com/angular/protractor/tree/master/example](https://github.com/angular/protractor/tree/master/example) have been migrated to SeleniumBase and placed in this directory. + +๐Ÿ”ต Protractor tests that end in `.spec.js` will now end in `_test.py` for the conversion to SeleniumBase/Python format with pytest auto-discovery. + +โœ… Here's a test run with `pytest` using `--reuse-session` mode and Chromium `--guest` mode: + +```zsh +$ pytest --rs -v --guest +=========================== test session starts ============================ +platform darwin -- Python 3.11.9, pytest-8.3.3, pluggy-1.5.0 -- /Users/michael/.virtualenvs/sbase11/bin/python +metadata: {'Python': '3.11.9', 'Platform': 'macOS-13.2.1-arm64-arm-64bit', 'Packages': {'pytest': '8.3.3', 'pluggy': '1.5.0'}, 'Plugins': {'cov': '6.0.0', 'html': '2.0.1', 'metadata': '3.1.1', 'seleniumbase': '4.33.2', 'ordering': '0.6', 'rerunfailures': '15.0', 'xdist': '3.6.1'}} +rootdir: /Users/michael/github/SeleniumBase/examples +configfile: pytest.ini +plugins: html-2.0.1, metadata-3.1.1, seleniumbase-4.33.2, ordering-0.6, rerunfailures-15.0, xdist-3.6.1 +collected 4 items + +example_test.py::AngularJSHomePageTests::test_greet_user PASSED +example_test.py::AngularJSHomePageTests::test_todo_list PASSED +input_test.py::AngularMaterialInputTests::test_invalid_input PASSED +mat_paginator_test.py::AngularMaterialPaginatorTests::test_pagination PASSED + +============================ 4 passed in 4.24s ============================= +``` diff --git a/examples/migration/protractor/__init__.py b/examples/migration/protractor/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/migration/protractor/example_spec.js b/examples/migration/protractor/example_spec.js new file mode 100644 index 00000000000..37d29724d93 --- /dev/null +++ b/examples/migration/protractor/example_spec.js @@ -0,0 +1,37 @@ +describe('angularjs homepage', function() { + it('should greet the named user', function() { + browser.get('http://www.angularjs.org'); + + element(by.model('yourName')).sendKeys('Julie'); + + var greeting = element(by.binding('yourName')); + + expect(greeting.getText()).toEqual('Hello Julie!'); + }); + + describe('todo list', function() { + var todoList; + + beforeEach(function() { + browser.get('http://www.angularjs.org'); + + todoList = element.all(by.repeater('todo in todoList.todos')); + }); + + it('should list todos', function() { + expect(todoList.count()).toEqual(2); + expect(todoList.get(1).getText()).toEqual('build an AngularJS app'); + }); + + it('should add a todo', function() { + var addTodo = element(by.model('todoList.todoText')); + var addButton = element(by.css('[value="add"]')); + + addTodo.sendKeys('write a protractor test'); + addButton.click(); + + expect(todoList.count()).toEqual(3); + expect(todoList.get(2).getText()).toEqual('write a protractor test'); + }); + }); +}); diff --git a/examples/migration/protractor/example_test.py b/examples/migration/protractor/example_test.py new file mode 100644 index 00000000000..b6ded63feaf --- /dev/null +++ b/examples/migration/protractor/example_test.py @@ -0,0 +1,24 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class AngularJSHomePageTests(BaseCase): + def test_greet_user(self): + self.goto("http://www.angularjs.org") + self.type('[ng-model="yourName"]', "Julie") + self.assert_exact_text("Hello Julie!", "h1.ng-binding") + + def test_todo_list(self): + self.goto("http://www.angularjs.org") + todo_selector = '[ng-repeat="todo in todoList.todos"]' + # Verify that the todos are listed + self.wait_for_element(todo_selector) + todos = self.find_visible_elements(todo_selector) + self.assert_equal(len(todos), 2) + self.assert_equal(todos[1].text.strip(), "build an AngularJS app") + # Verify adding a new todo + self.type('[ng-model="todoList.todoText"]', "write a protractor test") + self.click('[value="add"]') + todos = self.find_visible_elements(todo_selector) + self.assert_equal(len(todos), 3) + self.assert_equal(todos[2].text.strip(), "write a protractor test") diff --git a/examples/migration/protractor/input_spec.js b/examples/migration/protractor/input_spec.js new file mode 100644 index 00000000000..00e51a614d7 --- /dev/null +++ b/examples/migration/protractor/input_spec.js @@ -0,0 +1,15 @@ +describe('angular-material input component page', function() { + const EC = protractor.ExpectedConditions; + + it('Should change input component value', async() => { + await browser.get('https://material.angular.io/components/input/examples'); + + await browser.wait(EC.elementToBeClickable($('.mat-button-wrapper > .mat-icon')), 5000); + + const emailInputField = $$('#mat-input-1').get(1); + + await emailInputField.sendKeys('invalid'); + + expect($('mat-error').isPresent()).toBe(true); + }); +}); diff --git a/examples/migration/protractor/input_test.py b/examples/migration/protractor/input_test.py new file mode 100644 index 00000000000..59eb491f48c --- /dev/null +++ b/examples/migration/protractor/input_test.py @@ -0,0 +1,10 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class AngularMaterialInputTests(BaseCase): + def test_invalid_input(self): + # Test that there's an error for an invalid input + self.goto("https://material.angular.io/components/input/examples") + self.type('input[type="email"]', "invalid") + self.assert_element("mat-error") diff --git a/examples/migration/protractor/mat_paginator_spec.js b/examples/migration/protractor/mat_paginator_spec.js new file mode 100644 index 00000000000..e103a47daf4 --- /dev/null +++ b/examples/migration/protractor/mat_paginator_spec.js @@ -0,0 +1,31 @@ +describe('angular-material paginator component page', () => { + const EC = protractor.ExpectedConditions; + + beforeAll(async() => { + await browser.get('https://material.angular.io/components/paginator/examples'); + + await browser.wait(EC.elementToBeClickable($('.mat-button-wrapper > .mat-icon')), 5000); + }); + + it('Should navigate to next page', async() => { + await $('button[aria-label=\'Next page\']').click(); + + await expect($('.mat-paginator-range-label').getAttribute('innerText')).toEqual('Page 2 of 10'); + }); + + it('Should navigate to previous page', async() => { + await $('button[aria-label=\'Previous page\']').click(); + + await expect($('.mat-paginator-range-label').getAttribute('innerText')).toEqual('Page 1 of 10'); + }); + + it('Should change list length to 5 items per page', async() => { + await $('mat-select > div').click(); + + const fiveItemsOption = $$('mat-option > .mat-option-text').first(); + + await fiveItemsOption.click(); + + await expect($('.mat-paginator-range-label').getAttribute('innerText')).toEqual('Page 1 of 20'); + }); +}); diff --git a/examples/migration/protractor/mat_paginator_test.py b/examples/migration/protractor/mat_paginator_test.py new file mode 100644 index 00000000000..194e702553c --- /dev/null +++ b/examples/migration/protractor/mat_paginator_test.py @@ -0,0 +1,29 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class AngularMaterialPaginatorTests(BaseCase): + def test_pagination(self): + self.goto("https://material.angular.io/components/paginator/examples") + self.click_if_visible("button.mat-mdc-button") + self.scroll_to("div.mat-mdc-paginator-page-size") + # Set pagination to 5 items per page + self.click("mat-select > div") + self.click("mat-option:nth-of-type(1)") + # Verify navigation to the next page + self.click('button[aria-label="Next page"]') + self.assert_exact_text( + "6 โ€“ 10 of 50", ".mat-mdc-paginator-range-label" + ) + # Verify navigation to the previous page + self.click('button[aria-label="Previous page"]') + self.assert_exact_text( + "1 โ€“ 5 of 50", ".mat-mdc-paginator-range-label" + ) + # Set pagination to 10 items per page + self.click("mat-select > div") + self.click("mat-option:nth-of-type(2)") + # Verify page with correct number of pages + self.assert_exact_text( + "1 โ€“ 10 of 50", ".mat-mdc-paginator-range-label" + ) diff --git a/examples/migration/raw_selenium/ReadMe.md b/examples/migration/raw_selenium/ReadMe.md new file mode 100644 index 00000000000..6fe330d40f8 --- /dev/null +++ b/examples/migration/raw_selenium/ReadMe.md @@ -0,0 +1,81 @@ + + +## โœ… Support for migrating from raw Selenium to SeleniumBase + + +### ๐Ÿ”ต Here are some examples that can help you understand how to migrate from raw Selenium to SeleniumBase + +The five main examples in the [examples/migration/raw_selenium](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/migration/raw_selenium) folder are: + +* [flaky_messy_raw.py](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/migration/raw_selenium/flaky_messy_raw.py) +* [long_messy_raw.py](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/migration/raw_selenium/long_messy_raw.py) +* [messy_raw.py](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/migration/raw_selenium/messy_raw.py) +* [refined_raw.py](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/migration/raw_selenium/refined_raw.py) +* [simple_sbase.py](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/migration/raw_selenium/simple_sbase.py) + +Each of these examples is structured as a test that can be run with `pytest`. They all inherit `unittest.TestCase` either directly, or via `seleniumbase.BaseCase`, which extends it. This provides automatically-called `setUp()` and `tearDown()` methods before and after each test. + +> These examples show the evolution of tests from raw Selenium to SeleniumBase. By understanding common progressions of Selenium engineers, you can avoid making the same mistakes as they did, and learn to write good tests efficiently without the long learning curve. + +* [flaky_messy_raw.py](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/migration/raw_selenium/flaky_messy_raw.py) + +This is common example of how newcomers to Selenium write tests (assuming they've already learned how to break out reusuable code into `setUp()` and `tearDown()` methods). It uses `find_element()` calls, which can lead to flaky tests because those calls fail if a page element is slow to load. + +* [long_messy_raw.py](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/migration/raw_selenium/long_messy_raw.py) + +At the next stage of learning, newcomers to Selenium realize that their tests are flaky, so they start replacing existing `find_element()` calls with `WebDriverWait` and internal Selenium `expected_conditions` methods, such as `visibility_of_element_located` and `element_to_be_clickable`. This can result in long/messy tests that are unmaintainable if not written carefully. + +* [messy_raw.py](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/migration/raw_selenium/messy_raw.py) + +By this stage, newcomers to Selenium have evolved into legitimate test automation engineers. They have become better at writing reusable code, so they've broken down the long `WebDriverWait` and `expected_conditions` calls into shorter method calls, which are easier to read, but could still be improved on for better maintainability. Here, individual page actions are still written out as multiple lines of code, (or multiple method calls per line), which isn't efficient. + +* [refined_raw.py](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/migration/raw_selenium/refined_raw.py) + +By now, the test automation engineer has become an expert in breaking out code into reusable methods, and the test itself has been simplified down to a single page action per line. The code is easy to read and easy to maintain. The error output is also simplified. The journey of writing a complete test automation framework for Selenium has begun. + +* [simple_sbase.py](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/migration/raw_selenium/simple_sbase.py) + +With a complete test automation framework built, most of the hard work is already done for you. By importing `BaseCase` into your test classes, your tests gain access to all SeleniumBase methods, which can simplify your code. SeleniumBase also provides a lot of additional functionality that isn't included with raw Selenium. + + +### ๐Ÿ”ต How is SeleniumBase different from raw Selenium? + +
+ +

๐Ÿ’ก SeleniumBase is a Python framework for browser automation and testing. SeleniumBase uses Selenium/WebDriver APIs and incorporates test-runners such as pytest, pynose, and behave to provide organized structure, test discovery, test execution, test state (eg. passed, failed, or skipped), and command-line options for changing default settings (eg. browser selection). With raw Selenium, you would need to set up your own options-parser for configuring tests from the command-line.

+ +

๐Ÿ’ก SeleniumBase's driver manager gives you more control over automatic driver downloads. (Use --driver-version=VER with your pytest run command to specify the version.) By default, SeleniumBase will download a driver version that matches your major browser version if not set.

+ +

๐Ÿ’ก SeleniumBase automatically detects between CSS Selectors and XPath, which means you don't need to specify the type of selector in your commands (but optionally you could).

+ +

๐Ÿ’ก SeleniumBase methods often perform multiple actions in a single method call. For example, self.type(selector, text) does the following:
1. Waits for the element to be visible.
2. Waits for the element to be interactive.
3. Clears the text field.
4. Types in the new text.
5. Presses Enter/Submit if the text ends in "\n".
With raw Selenium, those actions require multiple method calls.

+ +

๐Ÿ’ก SeleniumBase uses default timeout values when not set:
+โœ… self.click("button")
+With raw Selenium, methods would fail instantly (by default) if an element needed more time to load:
+โŒ self.driver.find_element(by="css selector", value="button").click()
+(Reliable code is better than unreliable code.)

+ +

๐Ÿ’ก SeleniumBase lets you change the explicit timeout values of methods:
+โœ… self.click("button", timeout=10)
+With raw Selenium, that requires more code:
+โŒ WebDriverWait(driver, 10).until(EC.element_to_be_clickable("css selector", "button")).click()
+(Simple code is better than complex code.)

+ +

๐Ÿ’ก SeleniumBase gives you clean error output when a test fails. With raw Selenium, error messages can get very messy.

+ +

๐Ÿ’ก SeleniumBase gives you the option to generate a dashboard and reports for tests. It also saves screenshots from failing tests to the ./latest_logs/ folder. Raw Selenium does not have these options out-of-the-box.

+ +

๐Ÿ’ก SeleniumBase includes desktop GUI apps for running tests, such as SeleniumBase Commander for pytest and SeleniumBase Behave GUI for behave.

+ +

๐Ÿ’ก SeleniumBase has its own Recorder / Test Generator for creating tests from manual browser actions.

+ +

๐Ÿ’ก SeleniumBase comes with test case management software, ("Case Plans"), for organizing tests and step descriptions.

+ +

๐Ÿ’ก SeleniumBase includes tools for building data apps, ("Chart Maker"), which can generate JavaScript from Python.

+ +
+ +-------- + +[](https://github.com/seleniumbase/SeleniumBase) diff --git a/examples/migration/raw_selenium/__init__.py b/examples/migration/raw_selenium/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/migration/raw_selenium/flaky_messy_raw.py b/examples/migration/raw_selenium/flaky_messy_raw.py new file mode 100644 index 00000000000..9ae26626a1f --- /dev/null +++ b/examples/migration/raw_selenium/flaky_messy_raw.py @@ -0,0 +1,77 @@ +"""Flaky Raw Selenium Example - (ONLY Selenium / NO SeleniumBase)""" +import sys +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.common.by import By +from unittest import TestCase + + +class FlakyMessyRawSelenium(TestCase): + def setUp(self): + self.driver = None + options = webdriver.ChromeOptions() + options.add_argument("--disable-notifications") + if "linux" in sys.platform: + options.add_argument("--headless") + options.add_experimental_option( + "excludeSwitches", ["enable-automation", "enable-logging"], + ) + prefs = { + "credentials_enable_service": False, + "profile.password_manager_enabled": False, + "profile.password_manager_leak_detection": False, + } + options.add_experimental_option("prefs", prefs) + service = Service(service_args=["--disable-build-check"]) + self.driver = webdriver.Chrome(options=options, service=service) + + def tearDown(self): + if self.driver: + try: + if self.driver.service.process: + self.driver.quit() + except Exception: + pass + + def is_element_visible(self, selector, by="css selector"): + try: + element = self.driver.find_element(by, selector) + if element.is_displayed(): + return True + except Exception: + pass + return False + + def test_add_item_to_cart(self): + self.driver.get("https://www.saucedemo.com") + by_css = By.CSS_SELECTOR # "css selector" + element = self.driver.find_element(by_css, "#user-name") + element.clear() + element.send_keys("standard_user") + element = self.driver.find_element(by_css, "#password") + element.clear() + element.send_keys("secret_sauce") + element.submit() + self.driver.find_element(by_css, "div.inventory_list") + element = self.driver.find_element(by_css, "span.title") + self.assertEqual(element.text, "Products") + + self.driver.find_element(by_css, 'button[name*="backpack"]').click() + self.driver.find_element(by_css, "#shopping_cart_container a").click() + element = self.driver.find_element(by_css, "span.title") + self.assertEqual(element.text, "Your Cart") + element = self.driver.find_element(by_css, "div.cart_item") + self.assertIn("Backpack", element.text) + + self.driver.find_element(by_css, "#remove-sauce-labs-backpack").click() + self.assertFalse(self.is_element_visible("div.cart_item")) + + self.driver.find_element(by_css, "#react-burger-menu-btn").click() + self.driver.find_element(by_css, "a#logout_sidebar_link").click() + self.driver.find_element(by_css, "input#login-button") + + +# When run with "python" instead of "pytest" or "python -m unittest" +if __name__ == "__main__": + from unittest import main + main() diff --git a/examples/migration/raw_selenium/long_messy_raw.py b/examples/migration/raw_selenium/long_messy_raw.py new file mode 100644 index 00000000000..63bfa97d43f --- /dev/null +++ b/examples/migration/raw_selenium/long_messy_raw.py @@ -0,0 +1,101 @@ +"""Long & Messy Raw Selenium Example - (ONLY Selenium / NO SeleniumBase)""" +import sys +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from unittest import TestCase + + +class LongMessyRawSelenium(TestCase): + def setUp(self): + self.driver = None + options = webdriver.ChromeOptions() + options.add_argument("--disable-notifications") + if "linux" in sys.platform: + options.add_argument("--headless") + options.add_experimental_option( + "excludeSwitches", ["enable-automation", "enable-logging"], + ) + prefs = { + "credentials_enable_service": False, + "profile.password_manager_enabled": False, + "profile.password_manager_leak_detection": False, + } + options.add_experimental_option("prefs", prefs) + service = Service(service_args=["--disable-build-check"]) + self.driver = webdriver.Chrome(options=options, service=service) + + def tearDown(self): + if self.driver: + try: + if self.driver.service.process: + self.driver.quit() + except Exception: + pass + + def test_add_item_to_cart(self): + self.driver.get("https://www.saucedemo.com") + by_css = By.CSS_SELECTOR # "css selector" + element = WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable((by_css, "#user-name")) + ) + element.clear() + element.send_keys("standard_user") + element = WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable((by_css, "#password")) + ) + element.clear() + element.send_keys("secret_sauce") + element.submit() + WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located((by_css, "div.inventory_list")) + ) + element = WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located((by_css, "span.title")) + ) + self.assertEqual(element.text, "Products") + + element = WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable((by_css, 'button[name*="backpack"]')) + ) + element.click() + element = WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable((by_css, "#shopping_cart_container a")) + ) + element.click() + element = WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located((by_css, "span.title")) + ) + self.assertEqual(element.text, "Your Cart") + element = WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located((by_css, "div.cart_item")) + ) + self.assertIn("Backpack", element.text) + + element = WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable((by_css, "#remove-sauce-labs-backpack")) + ) + element.click() + WebDriverWait(self.driver, 10).until( + EC.invisibility_of_element((by_css, "div.cart_item")) + ) + + element = WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable((by_css, "#react-burger-menu-btn")) + ) + element.click() + element = WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable((by_css, "a#logout_sidebar_link")) + ) + element.click() + WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located((by_css, "input#login-button")) + ) + + +# When run with "python" instead of "pytest" or "python -m unittest" +if __name__ == "__main__": + from unittest import main + main() diff --git a/examples/migration/raw_selenium/messy_raw.py b/examples/migration/raw_selenium/messy_raw.py new file mode 100644 index 00000000000..37d5d97476a --- /dev/null +++ b/examples/migration/raw_selenium/messy_raw.py @@ -0,0 +1,89 @@ +"""Messy Raw Selenium Example - (ONLY Selenium / NO SeleniumBase)""" +import sys +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from unittest import TestCase + + +class MessyRawSelenium(TestCase): + def setUp(self): + self.driver = None + options = webdriver.ChromeOptions() + options.add_argument("--disable-notifications") + if "linux" in sys.platform: + options.add_argument("--headless") + options.add_experimental_option( + "excludeSwitches", ["enable-automation", "enable-logging"], + ) + prefs = { + "credentials_enable_service": False, + "profile.password_manager_enabled": False, + "profile.password_manager_leak_detection": False, + } + options.add_experimental_option("prefs", prefs) + service = Service(service_args=["--disable-build-check"]) + self.driver = webdriver.Chrome(options=options, service=service) + + def tearDown(self): + if self.driver: + try: + if self.driver.service.process: + self.driver.quit() + except Exception: + pass + + def wait_for_element_visible( + self, selector, by="css selector", timeout=10 + ): + return WebDriverWait(self.driver, timeout).until( + EC.visibility_of_element_located((by, selector)) + ) + + def wait_for_element_clickable( + self, selector, by="css selector", timeout=10 + ): + return WebDriverWait(self.driver, timeout).until( + EC.element_to_be_clickable((by, selector)) + ) + + def wait_for_element_not_visible( + self, selector, by="css selector", timeout=10 + ): + return WebDriverWait(self.driver, timeout).until( + EC.invisibility_of_element((by, selector)) + ) + + def test_add_item_to_cart(self): + self.driver.get("https://www.saucedemo.com") + element = self.wait_for_element_clickable("#user-name") + element.clear() + element.send_keys("standard_user") + element = self.wait_for_element_clickable("#password") + element.clear() + element.send_keys("secret_sauce") + element.submit() + self.wait_for_element_visible("div.inventory_list") + element = self.wait_for_element_visible("span.title") + self.assertEqual(element.text, "Products") + + self.wait_for_element_clickable('button[name*="backpack"]').click() + self.wait_for_element_clickable("#shopping_cart_container a").click() + element = self.wait_for_element_visible("span.title") + self.assertEqual(element.text, "Your Cart") + element = self.wait_for_element_visible("div.cart_item") + self.assertIn("Backpack", element.text) + + self.wait_for_element_clickable("#remove-sauce-labs-backpack").click() + self.wait_for_element_not_visible("div.cart_item") + + self.wait_for_element_clickable("#react-burger-menu-btn").click() + self.wait_for_element_clickable("a#logout_sidebar_link").click() + self.wait_for_element_visible("input#login-button") + + +# When run with "python" instead of "pytest" or "python -m unittest" +if __name__ == "__main__": + from unittest import main + main() diff --git a/examples/migration/raw_selenium/pytest.ini b/examples/migration/raw_selenium/pytest.ini new file mode 100644 index 00000000000..a1f9578ce5e --- /dev/null +++ b/examples/migration/raw_selenium/pytest.ini @@ -0,0 +1,47 @@ +[pytest] + +# Display console output. Disable cacheprovider: +addopts = --capture=tee-sys -p no:cacheprovider + +# Skip these directories during test collection: +norecursedirs = .* build dist recordings temp assets + +# Ignore DeprecationWarning, PytestUnknownMarkWarning +filterwarnings = + ignore::pytest.PytestWarning + ignore:.*U.*mode is deprecated:DeprecationWarning + +# Configure the junit_family option explicitly: +junit_family = legacy + +# Set pytest discovery rules: +# (Most of the rules here are similar to the default rules.) +# (Inheriting unittest.TestCase could override these rules.) +python_files = *.py +python_classes = Test* *Test* *Test *Tests *Suite +python_functions = test_* + +# Common pytest markers used in examples: +# (pytest may require marker registration to prevent warnings.) +# (Future versions may turn those marker warnings into errors.) +markers = + marker1: custom marker + marker2: custom marker + marker3: custom marker + marker_test_suite: custom marker + expected_failure: custom marker + local: custom marker + remote: custom marker + offline: custom marker + develop: custom marker + qa: custom marker + ci: custom marker + e2e: custom marker + ready: custom marker + smoke: custom marker + deploy: custom marker + active: custom marker + master: custom marker + release: custom marker + staging: custom marker + production: custom marker diff --git a/examples/migration/raw_selenium/refined_raw.py b/examples/migration/raw_selenium/refined_raw.py new file mode 100644 index 00000000000..94dbf94a9ef --- /dev/null +++ b/examples/migration/raw_selenium/refined_raw.py @@ -0,0 +1,131 @@ +"""Refined Raw Selenium Example - (ONLY Selenium / NO SeleniumBase)""" +import sys +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from unittest import TestCase + + +class RefinedRawSelenium(TestCase): + def setUp(self): + self.driver = None + options = webdriver.ChromeOptions() + options.add_argument("--disable-notifications") + if "linux" in sys.platform: + options.add_argument("--headless") + options.add_experimental_option( + "excludeSwitches", ["enable-automation", "enable-logging"], + ) + prefs = { + "credentials_enable_service": False, + "profile.password_manager_enabled": False, + "profile.password_manager_leak_detection": False, + } + options.add_experimental_option("prefs", prefs) + service = Service(service_args=["--disable-build-check"]) + self.driver = webdriver.Chrome(options=options, service=service) + + def tearDown(self): + if self.driver: + try: + if self.driver.service.process: + self.driver.quit() + except Exception: + pass + + def wait_for_element_visible( + self, selector, by="css selector", timeout=10 + ): + try: + return WebDriverWait(self.driver, timeout).until( + EC.visibility_of_element_located((by, selector)) + ) + except Exception: + raise Exception( + "Element {%s} was not visible after %s seconds!" + % (selector, timeout) + ) + + def wait_for_element_clickable( + self, selector, by="css selector", timeout=10 + ): + try: + return WebDriverWait(self.driver, timeout).until( + EC.element_to_be_clickable((by, selector)) + ) + except Exception: + raise Exception( + "Element {%s} was not visible/clickable after %s seconds!" + % (selector, timeout) + ) + + def wait_for_element_not_visible( + self, selector, by="css selector", timeout=10 + ): + try: + return WebDriverWait(self.driver, timeout).until( + EC.invisibility_of_element((by, selector)) + ) + except Exception: + raise Exception( + "Element {%s} was still visible after %s seconds!" + % (selector, timeout) + ) + + def goto(self, url): + self.driver.get(url) + + def click(self, selector, by="css selector", timeout=7): + el = self.wait_for_element_clickable(selector, by=by, timeout=timeout) + el.click() + + def type(self, selector, text, by="css selector", timeout=10): + el = self.wait_for_element_clickable(selector, by=by, timeout=timeout) + el.clear() + if not text.endswith("\n"): + el.send_keys(text) + else: + el.send_keys(text[:-1]) + el.submit() + + def assert_element(self, selector, by="css selector", timeout=7): + self.wait_for_element_visible(selector, by=by, timeout=timeout) + + def assert_text(self, text, selector="html", by="css selector", timeout=7): + el = self.wait_for_element_visible(selector, by=by, timeout=timeout) + self.assertIn(text, el.text) + + def assert_exact_text(self, text, selector, by="css selector", timeout=7): + el = self.wait_for_element_visible(selector, by=by, timeout=timeout) + self.assertEqual(text, el.text) + + def assert_element_not_visible( + self, selector, by="css selector", timeout=7 + ): + self.wait_for_element_not_visible(selector, by=by, timeout=timeout) + + def test_add_item_to_cart(self): + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("div.inventory_list") + self.assert_text("Products", "span.title") + + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_exact_text("Your Cart", "span.title") + self.assert_text("Backpack", "div.cart_item") + + self.click("#remove-sauce-labs-backpack") + self.assert_element_not_visible("div.cart_item") + + self.click("#react-burger-menu-btn") + self.click("a#logout_sidebar_link") + self.assert_element("input#login-button") + + +# When run with "python" instead of "pytest" or "python -m unittest" +if __name__ == "__main__": + from unittest import main + main() diff --git a/examples/migration/raw_selenium/simple_sbase.py b/examples/migration/raw_selenium/simple_sbase.py new file mode 100644 index 00000000000..917f82d9e92 --- /dev/null +++ b/examples/migration/raw_selenium/simple_sbase.py @@ -0,0 +1,24 @@ +"""Clean SeleniumBase Example - (Uses simple, reliable methods)""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class CleanSeleniumBase(BaseCase): + def test_add_item_to_cart(self): + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("div.inventory_list") + self.assert_text("Products", "span.title") + + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_exact_text("Your Cart", "span.title") + self.assert_text("Backpack", "div.cart_item") + + self.click("#remove-sauce-labs-backpack") + self.assert_element_not_visible("div.cart_item") + + self.click("#react-burger-menu-btn") + self.click("a#logout_sidebar_link") + self.assert_element("input#login-button") diff --git a/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/ReadMe.md b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/ReadMe.md new file mode 100644 index 00000000000..8f59e98fe63 --- /dev/null +++ b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/ReadMe.md @@ -0,0 +1,384 @@ + + +## ๐ŸŽญ Migrating raw Selenium to Playwright (Hybrid approach) + +Migrating tests from raw Selenium to Playwright can seem like an overwhelming effort. Fortunately, SeleniumBase provides a hybrid approach where the same test can call APIs from multiple different frameworks, allowing you to convert raw Selenium code to Playwright code in phases. + +### ๐Ÿ—บ๏ธ The Hybrid Mapping + +To understand how the Hybrid approach works, it helps to identify the role each component plays during the test execution: + +| Component | Role in Migration | Purpose | +| :--- | :--- | :--- | +| **`self.driver`** | **Legacy Engine** | Executes your existing Raw Selenium code without changes. | +| **`self.page`** | **Modern Engine** | Executes new Playwright code for speed and better stability. | +| **`self`** | **The Bridge** | Uses SeleniumBase to manage the browser and CDP connection. | + +By utilizing the Chrome DevTools Protocol (CDP), SeleniumBase allows these two engines to share the same browser session. Actions taken by the `driver` are immediately visible to the `page`, and vice versa. + +Let's say your starting point is a test script like this: + +```python +"""Raw Selenium TestCase Example""" +import sys +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from unittest import TestCase + + +class RawSeleniumTestCase(TestCase): + def setUp(self): + self.driver = None + options = webdriver.ChromeOptions() + options.add_argument("--disable-notifications") + if "linux" in sys.platform or "--headless" in sys.argv: + options.add_argument("--headless") + options.add_experimental_option( + "excludeSwitches", ["enable-automation", "enable-logging"], + ) + prefs = { + "credentials_enable_service": False, + "profile.password_manager_enabled": False, + "profile.password_manager_leak_detection": False, + } + options.add_experimental_option("prefs", prefs) + service = Service(service_args=["--disable-build-check"]) + self.driver = webdriver.Chrome(options=options, service=service) + + def tearDown(self): + if self.driver: + try: + if self.driver.service.process: + self.driver.quit() + except Exception: + pass + + def is_element_visible(self, selector, by="css selector"): + try: + element = self.driver.find_element(by, selector) + if element.is_displayed(): + return True + except Exception: + pass + return False + + def test_add_item_to_cart(self): + self.driver.get("https://www.saucedemo.com") + by_css = By.CSS_SELECTOR # "css selector" + element = self.driver.find_element(by_css, "#user-name") + element.send_keys("standard_user") + element = self.driver.find_element(by_css, "#password") + element.send_keys("secret_sauce") + element.submit() + self.driver.find_element(by_css, "div.inventory_list") + element = self.driver.find_element(by_css, "span.title") + self.assertEqual(element.text, "Products") + + self.driver.find_element(by_css, 'button[name*="backpack"]').click() + self.driver.find_element(by_css, "#shopping_cart_container a").click() + element = self.driver.find_element(by_css, "span.title") + self.assertEqual(element.text, "Your Cart") + element = self.driver.find_element(by_css, "div.cart_item") + self.assertIn("Backpack", element.text) + + self.driver.find_element(by_css, "#remove-sauce-labs-backpack").click() + self.assertFalse(self.is_element_visible("div.cart_item")) + + self.driver.find_element(by_css, "#react-burger-menu-btn").click() + element = WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable((by_css, "a#logout_sidebar_link")) + ) + element.click() + self.driver.find_element(by_css, "input#login-button") +``` + +From here, instead of having to convert the entire script over to Playwright at once, you could swap out partitions to get to this: + +```python +"""Hybrid Fixture Example. Uses Selenium, Playwright, and SeleniumBase.""" +from playwright.sync_api import sync_playwright, expect +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__, "--uc") + + +class HybridFixture(BaseCase): + def setUp(self): + super().setUp() + self.activate_cdp_mode() + self.reconnect() # Reconnects WebDriver to use Selenium + endpoint_url = self.cdp.get_endpoint_url() + self.playwright = sync_playwright().start() + browser = self.playwright.chromium.connect_over_cdp(endpoint_url) + self.page = browser.contexts[0].pages[0] + + def tearDown(self): + self.save_teardown_screenshot() # If failure or "--screenshot" + self.playwright.stop() + super().tearDown() + + def test_add_item_to_cart(self): + driver = self.driver # Selenium API + page = self.page # Playwright API + + # This section uses Selenium + driver.get("https://www.saucedemo.com") + by_css = By.CSS_SELECTOR # "css selector" + element = driver.find_element(by_css, "#user-name") + element.send_keys("standard_user") + element = driver.find_element(by_css, "#password") + element.send_keys("secret_sauce") + element.submit() + driver.find_element(by_css, "div.inventory_list") + element = driver.find_element(by_css, "span.title") + self.assertEqual(element.text, "Products") + + # This section uses Playwright + page.click('button[name*="backpack"]') + page.click("#shopping_cart_container a") + expect(page.locator("span.title")).to_have_text("Your Cart") + expect(page.locator("div.cart_item")).to_contain_text("Backpack") + + # This section uses SeleniumBase + self.click("#remove-sauce-labs-backpack") + self.assert_element_not_visible("div.cart_item") + + # This section uses Selenium + driver.find_element(by_css, "#react-burger-menu-btn").click() + element = WebDriverWait(driver, 10).until( + EC.element_to_be_clickable((by_css, "a#logout_sidebar_link")) + ) + element.click() + driver.find_element(by_css, "input#login-button") +``` + +The script above combines the APIs of raw Selenium, Playwright, and SeleniumBase, which gives you the flexibility to use any of them at any point in your test. SeleniumBase makes this possible with its adaptive API. + +Now that you've included Playwright in your script, you can utilize some of its advanced features, such as the Full Trace Viewer. Here's an updated version of the hybrid script above that includes the Tracing feature: + +```python +""" +Hybrid Fixture Example with Playwright Tracing. +Uses Selenium, Playwright, and SeleniumBase. +Trace logs are saved to: `latest_logs/[MODULE.CLASS.METHOD]/trace.zip` +To open Trace Viewer: `playwright show-trace [PATH_TO_LOGS]/trace.zip` +""" +import os +from playwright.sync_api import sync_playwright, expect +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__, "--uc") + + +class HybridFixtureWithTracing(BaseCase): + def setUp(self): + super().setUp() + self.activate_cdp_mode() + self.reconnect() # Reconnects WebDriver to use Selenium + endpoint_url = self.cdp.get_endpoint_url() + self.playwright = sync_playwright().start() + browser = self.playwright.chromium.connect_over_cdp(endpoint_url) + self.context = browser.contexts[0] + self.context.tracing.start( + snapshots=True, screenshots=True, sources=True + ) + self.page = self.context.pages[0] + + def tearDown(self): + self.save_teardown_screenshot() # If failure or "--screenshot" + log_folder = "latest_logs" + test_id = "%s.%s.%s" % ( + self.__class__.__module__, + self.__class__.__name__, + self._testMethodName, + ) + trace_path = os.path.join(log_folder, test_id, "trace.zip") + self.context.tracing.stop(path=trace_path) + self.playwright.stop() + super().tearDown() + + def test_add_item_to_cart(self): + driver = self.driver # Selenium API + page = self.page # Playwright API + + # This section uses Selenium + driver.get("https://www.saucedemo.com") + by_css = By.CSS_SELECTOR # "css selector" + element = driver.find_element(by_css, "#user-name") + element.send_keys("standard_user") + element = driver.find_element(by_css, "#password") + element.send_keys("secret_sauce") + element.submit() + driver.find_element(by_css, "div.inventory_list") + element = driver.find_element(by_css, "span.title") + self.assertEqual(element.text, "Products") + + # This section uses Playwright + page.click('button[name*="backpack"]') + page.click("#shopping_cart_container a") + expect(page.locator("span.title")).to_have_text("Your Cart") + expect(page.locator("div.cart_item")).to_contain_text("Backpack") + + # This section uses SeleniumBase + self.click("#remove-sauce-labs-backpack") + self.assert_element_not_visible("div.cart_item") + + # This section uses Selenium + driver.find_element(by_css, "#react-burger-menu-btn").click() + element = WebDriverWait(driver, 10).until( + EC.element_to_be_clickable((by_css, "a#logout_sidebar_link")) + ) + element.click() + driver.find_element(by_css, "input#login-button") +``` + +Note that this hybrid mode gives you some flexibility in how you structure your test class. You could do it without the `setUp()` and `tearDown()` methods by using a context manager for Playwright like this: + +```python +"""Hybrid TestCase Example. Uses Selenium, Playwright, and SeleniumBase.""" +from playwright.sync_api import sync_playwright, expect +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__, "--uc") + + +class HybridTestCase(BaseCase): + def test_add_item_to_cart(self): + self.activate_cdp_mode() + self.reconnect() # Reconnects WebDriver to use Selenium + driver = self.driver # Selenium API + endpoint_url = self.cdp.get_endpoint_url() + with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] # Playwright API + + # This section uses Selenium + driver.get("https://www.saucedemo.com") + by_css = By.CSS_SELECTOR # "css selector" + element = driver.find_element(by_css, "#user-name") + element.send_keys("standard_user") + element = driver.find_element(by_css, "#password") + element.send_keys("secret_sauce") + element.submit() + driver.find_element(by_css, "div.inventory_list") + element = driver.find_element(by_css, "span.title") + self.assertEqual(element.text, "Products") + + # This section uses Playwright + page.click('button[name*="backpack"]') + page.click("#shopping_cart_container a") + expect(page.locator("span.title")).to_have_text("Your Cart") + expect(page.locator("div.cart_item")).to_contain_text("Backpack") + + # This section uses SeleniumBase + self.click("#remove-sauce-labs-backpack") + self.assert_element_not_visible("div.cart_item") + + # This section uses Selenium + driver.find_element(by_css, "#react-burger-menu-btn").click() + element = WebDriverWait(driver, 10).until( + EC.element_to_be_clickable((by_css, "a#logout_sidebar_link")) + ) + element.click() + driver.find_element(by_css, "input#login-button") +``` + +Eventually, once you've completed converting your script over to Playwright, you can remove the Selenium-specific code, and you'll have a full Playwright script that looks something like this: + +```python +"""Playwright TestCase Example""" +import sys +from playwright.sync_api import sync_playwright, expect +from unittest import TestCase + + +class PlaywrightTestCase(TestCase): + @classmethod + def setUpClass(cls): + cls.playwright = sync_playwright().start() + headless = "linux" in sys.platform or "--headless" in sys.argv + # Launch browser once for the whole class + cls.browser = cls.playwright.chromium.launch( + channel="chrome", headless=headless + ) + + @classmethod + def tearDownClass(cls): + cls.browser.close() + cls.playwright.stop() + + def setUp(self): + # Create new session for every test + self.context = self.browser.new_context() + self.page = self.context.new_page() + + def tearDown(self): + # Close the session (browser runs until tearDownClass) + self.page.close() + self.context.close() + + def test_add_item_to_cart(self): + page = self.page + + page.goto("https://www.saucedemo.com") + page.fill("#user-name", "standard_user") + page.fill("#password", "secret_sauce") + page.press("#password", "Enter") + expect(page.locator("div.inventory_list")).to_be_visible() + expect(page.locator("span.title")).to_contain_text("Products") + + page.click('button[name*="backpack"]') + page.click("#shopping_cart_container a") + expect(page.locator("span.title")).to_have_text("Your Cart") + expect(page.locator("div.cart_item")).to_contain_text("Backpack") + + page.click("#remove-sauce-labs-backpack") + expect(page.locator("div.cart_item")).to_be_hidden() + + page.click("#react-burger-menu-btn") + page.click("a#logout_sidebar_link") + expect(page.locator("input#login-button")).to_be_visible() +``` + +Or, you might decide to migrate over to SeleniumBase, and then your script for that test would look something like this: + +```python +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class SeleniumBaseTestCase(BaseCase): + def test_add_item_to_cart(self): + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("div.inventory_list") + self.assert_text("Products", "span.title") + + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_exact_text("Your Cart", "span.title") + self.assert_text("Backpack", "div.cart_item") + + self.click("#remove-sauce-labs-backpack") + self.assert_element_not_visible("div.cart_item") + + self.click("#react-burger-menu-btn") + self.click("a#logout_sidebar_link") + self.assert_element("input#login-button") +``` + +You've got lots of options and flexibility when the time comes to deciding how you want to migrate from raw Selenium to more modern frameworks like Playwright or SeleniumBase. + +-------- + +[](https://github.com/seleniumbase/SeleniumBase) diff --git a/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/__init__.py b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/non_test_scripts/ReadMe.md b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/non_test_scripts/ReadMe.md new file mode 100644 index 00000000000..7487a7af462 --- /dev/null +++ b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/non_test_scripts/ReadMe.md @@ -0,0 +1,84 @@ + + +## ๐Ÿ› ๏ธ Standalone Automation Scripts (Non-Test) + +This folder contains examples for users who need to perform web automation **without** using a test runner like `pytest` or `unittest`. + +By using the hybrid approach seen in [raw_hybrid_script.py](./raw_hybrid_script.py), SeleniumBase has effectively bridged the gap between raw Selenium and Playwright in order to ease the migration process. + +```python +"""Hybrid Script Example. Uses Selenium, Playwright, and SeleniumBase.""" +from playwright.sync_api import sync_playwright, expect +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from seleniumbase import SB + + +with SB(uc=True) as sb: # SeleniumBase API + sb.activate_cdp_mode() + sb.reconnect() # Reconnects WebDriver to use Selenium + driver = sb.driver # Selenium API + endpoint_url = sb.get_endpoint_url() + with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] # Playwright API + + # This section uses Selenium + driver.get("https://www.saucedemo.com") + by_css = By.CSS_SELECTOR # "css selector" + element = driver.find_element(by_css, "#user-name") + element.send_keys("standard_user") + element = driver.find_element(by_css, "#password") + element.send_keys("secret_sauce") + element.submit() + driver.find_element(by_css, "div.inventory_list") + element = driver.find_element(by_css, "span.title") + assert element.text == "Products" + + # This section uses Playwright + page.click('button[name*="backpack"]') + page.click("#shopping_cart_container a") + expect(page.locator("span.title")).to_have_text("Your Cart") + expect(page.locator("div.cart_item")).to_contain_text("Backpack") + + # This section uses SeleniumBase + sb.click("#remove-sauce-labs-backpack") + sb.assert_element_not_visible("div.cart_item") + + # This section uses Selenium + driver.find_element(by_css, "#react-burger-menu-btn").click() + element = WebDriverWait(driver, 10).until( + EC.element_to_be_clickable((by_css, "a#logout_sidebar_link")) + ) + element.click() + driver.find_element(by_css, "input#login-button") +``` + +This folder also includes standalone scripts for raw Selenium, Playwright, and SeleniumBase, which serve as foundational components for the hybrid script that integrates all three. + +### ๐Ÿ’ก Why use Standalone Scripts? +While test runners are excellent for verification and reporting, standalone scripts are often preferred for: +* **Web Scraping:** Extracting data from sites without the overhead of a test framework. +* **DevOps/Utility Tasks:** Automating repetitive browser-based tasks or health checks. +* **Rapid Prototyping:** Testing out a new automation flow quickly before formalizing it into a test suite. + +### ๐ŸŒ‰ The "SB" Advantage +The `raw_hybrid_script.py` example demonstrates how to use the **`SB()` context manager**. This is a powerful feature of SeleniumBase that allows you to: +1. **Bridge Engines:** Effortlessly connect Playwright to a browser session already managed by SeleniumBase. +2. **Stealth by Default:** Use `uc=True` (Undetected Mode) to bypass bot-detection systems that standard Playwright configurations often fail. +3. **Automatic Cleanup:** The context manager handles closing the browser and cleaning up processes automatically, even if the script encounters an error. + +--- + +### ๐Ÿš€ Quick Start + +To run any of these examples, simply execute them with Python: + +```bash +python raw_hybrid_script.py +``` + +-------- + +[](https://github.com/seleniumbase/SeleniumBase) diff --git a/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/non_test_scripts/__init__.py b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/non_test_scripts/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/non_test_scripts/raw_hybrid_script.py b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/non_test_scripts/raw_hybrid_script.py new file mode 100644 index 00000000000..9e4210786dc --- /dev/null +++ b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/non_test_scripts/raw_hybrid_script.py @@ -0,0 +1,46 @@ +"""Hybrid Script Example. Uses Selenium, Playwright, and SeleniumBase.""" +from playwright.sync_api import sync_playwright, expect +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from seleniumbase import SB + + +with SB(uc=True) as sb: # SeleniumBase API + sb.activate_cdp_mode() + sb.reconnect() # Reconnects WebDriver to use Selenium + driver = sb.driver # Selenium API + endpoint_url = sb.get_endpoint_url() + with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] # Playwright API + + # This section uses Selenium + driver.get("https://www.saucedemo.com") + by_css = By.CSS_SELECTOR # "css selector" + element = driver.find_element(by_css, "#user-name") + element.send_keys("standard_user") + element = driver.find_element(by_css, "#password") + element.send_keys("secret_sauce") + element.submit() + driver.find_element(by_css, "div.inventory_list") + element = driver.find_element(by_css, "span.title") + assert element.text == "Products" + + # This section uses Playwright + page.click('button[name*="backpack"]') + page.click("#shopping_cart_container a") + expect(page.locator("span.title")).to_have_text("Your Cart") + expect(page.locator("div.cart_item")).to_contain_text("Backpack") + + # This section uses SeleniumBase + sb.click("#remove-sauce-labs-backpack") + sb.assert_element_not_visible("div.cart_item") + + # This section uses Selenium + driver.find_element(by_css, "#react-burger-menu-btn").click() + element = WebDriverWait(driver, 10).until( + EC.element_to_be_clickable((by_css, "a#logout_sidebar_link")) + ) + element.click() + driver.find_element(by_css, "input#login-button") diff --git a/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/non_test_scripts/raw_script_pw.py b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/non_test_scripts/raw_script_pw.py new file mode 100644 index 00000000000..1b9c1966b24 --- /dev/null +++ b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/non_test_scripts/raw_script_pw.py @@ -0,0 +1,23 @@ +from playwright.sync_api import sync_playwright, expect + +with sync_playwright() as p: + browser = p.chromium.launch(channel="chrome", headless=False) + page = browser.new_context().new_page() + page.goto("https://www.saucedemo.com") + page.fill("#user-name", "standard_user") + page.fill("#password", "secret_sauce") + page.press("#password", "Enter") + expect(page.locator("div.inventory_list")).to_be_visible() + expect(page.locator("span.title")).to_contain_text("Products") + + page.click('button[name*="backpack"]') + page.click("#shopping_cart_container a") + expect(page.locator("span.title")).to_have_text("Your Cart") + expect(page.locator("div.cart_item")).to_contain_text("Backpack") + + page.click("#remove-sauce-labs-backpack") + expect(page.locator("div.cart_item")).to_be_hidden() + + page.click("#react-burger-menu-btn") + page.click("a#logout_sidebar_link") + expect(page.locator("input#login-button")).to_be_visible() diff --git a/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/non_test_scripts/raw_script_sb.py b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/non_test_scripts/raw_script_sb.py new file mode 100644 index 00000000000..6c1b36a226b --- /dev/null +++ b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/non_test_scripts/raw_script_sb.py @@ -0,0 +1,20 @@ +from seleniumbase import SB + +with SB() as sb: + sb.goto("https://www.saucedemo.com") + sb.type("#user-name", "standard_user") + sb.type("#password", "secret_sauce\n") + sb.assert_element("div.inventory_list") + sb.assert_text("Products", "span.title") + + sb.click('button[name*="backpack"]') + sb.click("#shopping_cart_container a") + sb.assert_exact_text("Your Cart", "span.title") + sb.assert_text("Backpack", "div.cart_item") + + sb.click("#remove-sauce-labs-backpack") + sb.assert_element_not_visible("div.cart_item") + + sb.click("#react-burger-menu-btn") + sb.click("a#logout_sidebar_link") + sb.assert_element("input#login-button") diff --git a/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/non_test_scripts/raw_script_se.py b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/non_test_scripts/raw_script_se.py new file mode 100644 index 00000000000..186fd70baad --- /dev/null +++ b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/non_test_scripts/raw_script_se.py @@ -0,0 +1,62 @@ +import sys +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + + +def is_element_visible(driver, selector, by="css selector"): + try: + element = driver.find_element(by, selector) + if element.is_displayed(): + return True + except Exception: + pass + return False + + +options = webdriver.ChromeOptions() +options.add_argument("--disable-notifications") +if "linux" in sys.platform: + options.add_argument("--headless") +options.add_experimental_option( + "excludeSwitches", ["enable-automation", "enable-logging"], +) +prefs = { + "credentials_enable_service": False, + "profile.password_manager_enabled": False, + "profile.password_manager_leak_detection": False, +} +options.add_experimental_option("prefs", prefs) +service = Service(service_args=["--disable-build-check"]) + + +with webdriver.Chrome(options=options, service=service) as driver: + driver.get("https://www.saucedemo.com") + by_css = By.CSS_SELECTOR # "css selector" + element = driver.find_element(by_css, "#user-name") + element.send_keys("standard_user") + element = driver.find_element(by_css, "#password") + element.send_keys("secret_sauce") + element.submit() + driver.find_element(by_css, "div.inventory_list") + element = driver.find_element(by_css, "span.title") + assert element.text == "Products" + + driver.find_element(by_css, 'button[name*="backpack"]').click() + driver.find_element(by_css, "#shopping_cart_container a").click() + element = driver.find_element(by_css, "span.title") + assert element.text == "Your Cart" + element = driver.find_element(by_css, "div.cart_item") + assert "Backpack" in element.text + + driver.find_element(by_css, "#remove-sauce-labs-backpack").click() + assert not is_element_visible(driver, "div.cart_item") + + driver.find_element(by_css, "#react-burger-menu-btn").click() + element = WebDriverWait(driver, 10).until( + EC.element_to_be_clickable((by_css, "a#logout_sidebar_link")) + ) + element.click() + driver.find_element(by_css, "input#login-button") diff --git a/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/raw_case_pw.py b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/raw_case_pw.py new file mode 100644 index 00000000000..b98eae29431 --- /dev/null +++ b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/raw_case_pw.py @@ -0,0 +1,57 @@ +"""Playwright TestCase Example""" +import sys +from playwright.sync_api import sync_playwright, expect +from unittest import TestCase + + +class PlaywrightTestCase(TestCase): + @classmethod + def setUpClass(cls): + cls.playwright = sync_playwright().start() + headless = "linux" in sys.platform or "--headless" in sys.argv + # Launch browser once for the whole class + cls.browser = cls.playwright.chromium.launch( + channel="chrome", headless=headless + ) + + @classmethod + def tearDownClass(cls): + cls.browser.close() + cls.playwright.stop() + + def setUp(self): + # Create new session for every test + self.context = self.browser.new_context() + self.page = self.context.new_page() + + def tearDown(self): + # Close the session (browser runs until tearDownClass) + self.page.close() + self.context.close() + + def test_add_item_to_cart(self): + page = self.page + + page.goto("https://www.saucedemo.com") + page.fill("#user-name", "standard_user") + page.fill("#password", "secret_sauce") + page.press("#password", "Enter") + expect(page.locator("div.inventory_list")).to_be_visible() + expect(page.locator("span.title")).to_contain_text("Products") + + page.click('button[name*="backpack"]') + page.click("#shopping_cart_container a") + expect(page.locator("span.title")).to_have_text("Your Cart") + expect(page.locator("div.cart_item")).to_contain_text("Backpack") + + page.click("#remove-sauce-labs-backpack") + expect(page.locator("div.cart_item")).to_be_hidden() + + page.click("#react-burger-menu-btn") + page.click("a#logout_sidebar_link") + expect(page.locator("input#login-button")).to_be_visible() + + +if __name__ == "__main__": + from pytest import main + main([__file__, "-s"]) diff --git a/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/raw_case_pw_with_tracing.py b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/raw_case_pw_with_tracing.py new file mode 100644 index 00000000000..76d0450c043 --- /dev/null +++ b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/raw_case_pw_with_tracing.py @@ -0,0 +1,73 @@ +""" +Playwright TestCase Example with Tracing. +Trace logs are saved to: `latest_logs/[TESTPATH]/trace.zip` +To open Trace Viewer: `playwright show-trace TRACEPATH.zip` +""" +import os +import sys +from playwright.sync_api import sync_playwright, expect +from unittest import TestCase + + +class PlaywrightTestCaseWithTracing(TestCase): + @classmethod + def setUpClass(cls): + cls.playwright = sync_playwright().start() + headless = "linux" in sys.platform or "--headless" in sys.argv + # Launch browser once for the whole class + cls.browser = cls.playwright.chromium.launch( + channel="chrome", headless=headless + ) + + @classmethod + def tearDownClass(cls): + cls.browser.close() + cls.playwright.stop() + + def setUp(self): + # Create new session for every test + self.context = self.browser.new_context() + self.context.tracing.start( + snapshots=True, screenshots=True, sources=True + ) + self.page = self.context.new_page() + + def tearDown(self): + # Close the session (browser runs until tearDownClass) + log_folder = "latest_logs" + test_id = "%s.%s.%s" % ( + self.__class__.__module__, + self.__class__.__name__, + self._testMethodName, + ) + trace_path = os.path.join(log_folder, test_id, "trace.zip") + self.context.tracing.stop(path=trace_path) + self.page.close() + self.context.close() + + def test_add_item_to_cart(self): + page = self.page + + page.goto("https://www.saucedemo.com") + page.fill("#user-name", "standard_user") + page.fill("#password", "secret_sauce") + page.press("#password", "Enter") + expect(page.locator("div.inventory_list")).to_be_visible() + expect(page.locator("span.title")).to_contain_text("Products") + + page.click('button[name*="backpack"]') + page.click("#shopping_cart_container a") + expect(page.locator("span.title")).to_have_text("Your Cart") + expect(page.locator("div.cart_item")).to_contain_text("Backpack") + + page.click("#remove-sauce-labs-backpack") + expect(page.locator("div.cart_item")).to_be_hidden() + + page.click("#react-burger-menu-btn") + page.click("a#logout_sidebar_link") + expect(page.locator("input#login-button")).to_be_visible() + + +if __name__ == "__main__": + from pytest import main + main([__file__, "-s"]) diff --git a/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/raw_case_sb.py b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/raw_case_sb.py new file mode 100644 index 00000000000..b6f8d815f27 --- /dev/null +++ b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/raw_case_sb.py @@ -0,0 +1,24 @@ +"""SeleniumBase TestCase Example""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class SeleniumBaseTestCase(BaseCase): + def test_add_item_to_cart(self): + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("div.inventory_list") + self.assert_text("Products", "span.title") + + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_exact_text("Your Cart", "span.title") + self.assert_text("Backpack", "div.cart_item") + + self.click("#remove-sauce-labs-backpack") + self.assert_element_not_visible("div.cart_item") + + self.click("#react-burger-menu-btn") + self.click("a#logout_sidebar_link") + self.assert_element("input#login-button") diff --git a/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/raw_case_se.py b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/raw_case_se.py new file mode 100644 index 00000000000..d324752304a --- /dev/null +++ b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/raw_case_se.py @@ -0,0 +1,79 @@ +"""Raw Selenium TestCase Example""" +import sys +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from unittest import TestCase + + +class RawSeleniumTestCase(TestCase): + def setUp(self): + self.driver = None + options = webdriver.ChromeOptions() + options.add_argument("--disable-notifications") + if "linux" in sys.platform or "--headless" in sys.argv: + options.add_argument("--headless") + options.add_experimental_option( + "excludeSwitches", ["enable-automation", "enable-logging"], + ) + prefs = { + "credentials_enable_service": False, + "profile.password_manager_enabled": False, + "profile.password_manager_leak_detection": False, + } + options.add_experimental_option("prefs", prefs) + service = Service(service_args=["--disable-build-check"]) + self.driver = webdriver.Chrome(options=options, service=service) + + def tearDown(self): + if self.driver: + try: + if self.driver.service.process: + self.driver.quit() + except Exception: + pass + + def is_element_visible(self, selector, by="css selector"): + try: + element = self.driver.find_element(by, selector) + if element.is_displayed(): + return True + except Exception: + pass + return False + + def test_add_item_to_cart(self): + self.driver.get("https://www.saucedemo.com") + by_css = By.CSS_SELECTOR # "css selector" + element = self.driver.find_element(by_css, "#user-name") + element.send_keys("standard_user") + element = self.driver.find_element(by_css, "#password") + element.send_keys("secret_sauce") + element.submit() + self.driver.find_element(by_css, "div.inventory_list") + element = self.driver.find_element(by_css, "span.title") + self.assertEqual(element.text, "Products") + + self.driver.find_element(by_css, 'button[name*="backpack"]').click() + self.driver.find_element(by_css, "#shopping_cart_container a").click() + element = self.driver.find_element(by_css, "span.title") + self.assertEqual(element.text, "Your Cart") + element = self.driver.find_element(by_css, "div.cart_item") + self.assertIn("Backpack", element.text) + + self.driver.find_element(by_css, "#remove-sauce-labs-backpack").click() + self.assertFalse(self.is_element_visible("div.cart_item")) + + self.driver.find_element(by_css, "#react-burger-menu-btn").click() + element = WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable((by_css, "a#logout_sidebar_link")) + ) + element.click() + self.driver.find_element(by_css, "input#login-button") + + +if __name__ == "__main__": + from pytest import main + main([__file__, "-s"]) diff --git a/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/raw_hybrid_case.py b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/raw_hybrid_case.py new file mode 100644 index 00000000000..282c4a6d546 --- /dev/null +++ b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/raw_hybrid_case.py @@ -0,0 +1,48 @@ +"""Hybrid TestCase Example. Uses Selenium, Playwright, and SeleniumBase.""" +from playwright.sync_api import sync_playwright, expect +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__, "--uc") + + +class HybridTestCase(BaseCase): + def test_add_item_to_cart(self): + self.activate_cdp_mode() + self.reconnect() # Reconnects WebDriver to use Selenium + driver = self.driver # Selenium API + endpoint_url = self.get_endpoint_url() + with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(endpoint_url) + page = browser.contexts[0].pages[0] # Playwright API + + # This section uses Selenium + driver.get("https://www.saucedemo.com") + by_css = By.CSS_SELECTOR # "css selector" + element = driver.find_element(by_css, "#user-name") + element.send_keys("standard_user") + element = driver.find_element(by_css, "#password") + element.send_keys("secret_sauce") + element.submit() + driver.find_element(by_css, "div.inventory_list") + element = driver.find_element(by_css, "span.title") + self.assertEqual(element.text, "Products") + + # This section uses Playwright + page.click('button[name*="backpack"]') + page.click("#shopping_cart_container a") + expect(page.locator("span.title")).to_have_text("Your Cart") + expect(page.locator("div.cart_item")).to_contain_text("Backpack") + + # This section uses SeleniumBase + self.click("#remove-sauce-labs-backpack") + self.assert_element_not_visible("div.cart_item") + + # This section uses Selenium + driver.find_element(by_css, "#react-burger-menu-btn").click() + element = WebDriverWait(driver, 10).until( + EC.element_to_be_clickable((by_css, "a#logout_sidebar_link")) + ) + element.click() + driver.find_element(by_css, "input#login-button") diff --git a/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/raw_hybrid_fixture.py b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/raw_hybrid_fixture.py new file mode 100644 index 00000000000..bd0373c93f9 --- /dev/null +++ b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/raw_hybrid_fixture.py @@ -0,0 +1,57 @@ +"""Hybrid Fixture Example. Uses Selenium, Playwright, and SeleniumBase.""" +from playwright.sync_api import sync_playwright, expect +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__, "--uc") + + +class HybridFixture(BaseCase): + def setUp(self): + super().setUp() + self.activate_cdp_mode() + self.reconnect() # Reconnects WebDriver to use Selenium + endpoint_url = self.get_endpoint_url() + self.playwright = sync_playwright().start() + browser = self.playwright.chromium.connect_over_cdp(endpoint_url) + self.page = browser.contexts[0].pages[0] + + def tearDown(self): + self.save_teardown_screenshot() # If failure or "--screenshot" + self.playwright.stop() + super().tearDown() + + def test_add_item_to_cart(self): + driver = self.driver # Selenium API + page = self.page # Playwright API + + # This section uses Selenium + driver.get("https://www.saucedemo.com") + by_css = By.CSS_SELECTOR # "css selector" + element = driver.find_element(by_css, "#user-name") + element.send_keys("standard_user") + element = driver.find_element(by_css, "#password") + element.send_keys("secret_sauce") + element.submit() + driver.find_element(by_css, "div.inventory_list") + element = driver.find_element(by_css, "span.title") + self.assertEqual(element.text, "Products") + + # This section uses Playwright + page.click('button[name*="backpack"]') + page.click("#shopping_cart_container a") + expect(page.locator("span.title")).to_have_text("Your Cart") + expect(page.locator("div.cart_item")).to_contain_text("Backpack") + + # This section uses SeleniumBase + self.click("#remove-sauce-labs-backpack") + self.assert_element_not_visible("div.cart_item") + + # This section uses Selenium + driver.find_element(by_css, "#react-burger-menu-btn").click() + element = WebDriverWait(driver, 10).until( + EC.element_to_be_clickable((by_css, "a#logout_sidebar_link")) + ) + element.click() + driver.find_element(by_css, "input#login-button") diff --git a/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/raw_hybrid_fixture_tracing.py b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/raw_hybrid_fixture_tracing.py new file mode 100644 index 00000000000..0f5dfff5c1d --- /dev/null +++ b/examples/migration/raw_selenium/to_playwright/01_hybrid_approach/raw_hybrid_fixture_tracing.py @@ -0,0 +1,75 @@ +""" +Hybrid Fixture Example with Playwright Tracing. +Uses Selenium, Playwright, and SeleniumBase. +Trace logs are saved to: `latest_logs/[MODULE.CLASS.METHOD]/trace.zip` +To open Trace Viewer: `playwright show-trace [PATH_TO_LOGS]/trace.zip` +""" +import os +from playwright.sync_api import sync_playwright, expect +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__, "--uc") + + +class HybridFixtureWithTracing(BaseCase): + def setUp(self): + super().setUp() + self.activate_cdp_mode() + self.reconnect() # Reconnects WebDriver to use Selenium + endpoint_url = self.get_endpoint_url() + self.playwright = sync_playwright().start() + browser = self.playwright.chromium.connect_over_cdp(endpoint_url) + self.context = browser.contexts[0] + self.context.tracing.start( + snapshots=True, screenshots=True, sources=True + ) + self.page = self.context.pages[0] + + def tearDown(self): + self.save_teardown_screenshot() # If failure or "--screenshot" + log_folder = "latest_logs" + test_id = "%s.%s.%s" % ( + self.__class__.__module__, + self.__class__.__name__, + self._testMethodName, + ) + trace_path = os.path.join(log_folder, test_id, "trace.zip") + self.context.tracing.stop(path=trace_path) + self.playwright.stop() + super().tearDown() + + def test_add_item_to_cart(self): + driver = self.driver # Selenium API + page = self.page # Playwright API + + # This section uses Selenium + driver.get("https://www.saucedemo.com") + by_css = By.CSS_SELECTOR # "css selector" + element = driver.find_element(by_css, "#user-name") + element.send_keys("standard_user") + element = driver.find_element(by_css, "#password") + element.send_keys("secret_sauce") + element.submit() + driver.find_element(by_css, "div.inventory_list") + element = driver.find_element(by_css, "span.title") + self.assertEqual(element.text, "Products") + + # This section uses Playwright + page.click('button[name*="backpack"]') + page.click("#shopping_cart_container a") + expect(page.locator("span.title")).to_have_text("Your Cart") + expect(page.locator("div.cart_item")).to_contain_text("Backpack") + + # This section uses SeleniumBase + self.click("#remove-sauce-labs-backpack") + self.assert_element_not_visible("div.cart_item") + + # This section uses Selenium + driver.find_element(by_css, "#react-burger-menu-btn").click() + element = WebDriverWait(driver, 10).until( + EC.element_to_be_clickable((by_css, "a#logout_sidebar_link")) + ) + element.click() + driver.find_element(by_css, "input#login-button") diff --git a/examples/migration/raw_selenium/to_playwright/02_adapter_pattern/ReadMe.md b/examples/migration/raw_selenium/to_playwright/02_adapter_pattern/ReadMe.md new file mode 100644 index 00000000000..1c50157df1a --- /dev/null +++ b/examples/migration/raw_selenium/to_playwright/02_adapter_pattern/ReadMe.md @@ -0,0 +1,444 @@ + + +## ๐ŸŽญ Migrating raw Selenium to Playwright (Adapter Pattern) + +In the "Adapter Pattern" design, the test class acts as an interface while the underlying framework becomes a swappable engine. This allows tests to stay the same because all framework-specific code gets extracted into reusable fixtures. + +When converting raw Selenium tests to Playwright using the Adapter Pattern, all you have to do is modify the fixtures without changing the tests themselves. + +> ๐Ÿ’ก Notice: In every single example below, the code inside test_add_item_to_cart remains 100% identical. Only the underlying "engine" methods change. + +Here's a raw Selenium test script using the Adapter Pattern: + +```python +import sys +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from unittest import TestCase + + +class RawSeleniumAdapter(TestCase): + def setUp(self): + self.driver = None + options = webdriver.ChromeOptions() + options.add_argument("--disable-notifications") + if "linux" in sys.platform: + options.add_argument("--headless") + options.add_experimental_option( + "excludeSwitches", ["enable-automation", "enable-logging"], + ) + prefs = { + "credentials_enable_service": False, + "profile.password_manager_enabled": False, + "profile.password_manager_leak_detection": False, + } + options.add_experimental_option("prefs", prefs) + service = Service(service_args=["--disable-build-check"]) + self.driver = webdriver.Chrome(options=options, service=service) + + def tearDown(self): + if self.driver: + try: + if self.driver.service.process: + self.driver.quit() + except Exception: + pass + + def wait_for_element_visible( + self, selector, by="css selector", timeout=10 + ): + try: + return WebDriverWait(self.driver, timeout).until( + EC.visibility_of_element_located((by, selector)) + ) + except Exception: + raise Exception( + "Element (%s) was not visible after %s seconds!" + % (selector, timeout) + ) + + def wait_for_element_clickable( + self, selector, by="css selector", timeout=10 + ): + try: + return WebDriverWait(self.driver, timeout).until( + EC.element_to_be_clickable((by, selector)) + ) + except Exception: + raise Exception( + "Element (%s) was not visible/clickable after %s seconds!" + % (selector, timeout) + ) + + def wait_for_element_not_visible( + self, selector, by="css selector", timeout=10 + ): + try: + return WebDriverWait(self.driver, timeout).until( + EC.invisibility_of_element((by, selector)) + ) + except Exception: + raise Exception( + "Element (%s) was still visible after %s seconds!" + % (selector, timeout) + ) + + def goto(self, url): + self.driver.get(url) + + def click(self, selector, by="css selector", timeout=7): + el = self.wait_for_element_clickable(selector, by=by, timeout=timeout) + el.click() + + def type(self, selector, text, by="css selector", timeout=10): + el = self.wait_for_element_clickable(selector, by=by, timeout=timeout) + el.clear() + if not text.endswith("\n"): + el.send_keys(text) + else: + el.send_keys(text[:-1]) + el.submit() + + def assert_element(self, selector, by="css selector", timeout=7): + self.wait_for_element_visible(selector, by=by, timeout=timeout) + + def assert_text(self, text, selector="html", by="css selector", timeout=7): + el = self.wait_for_element_visible(selector, by=by, timeout=timeout) + self.assertIn(text, el.text) + + def assert_exact_text(self, text, selector, by="css selector", timeout=7): + el = self.wait_for_element_visible(selector, by=by, timeout=timeout) + self.assertEqual(text, el.text) + + def assert_element_not_visible( + self, selector, by="css selector", timeout=7 + ): + self.wait_for_element_not_visible(selector, by=by, timeout=timeout) + + def test_add_item_to_cart(self): + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("div.inventory_list") + self.assert_text("Products", "span.title") + + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_exact_text("Your Cart", "span.title") + self.assert_text("Backpack", "div.cart_item") + + self.click("#remove-sauce-labs-backpack") + self.assert_element_not_visible("div.cart_item") + + self.click("#react-burger-menu-btn") + self.click("a#logout_sidebar_link") + self.assert_element("input#login-button") +``` + +That example converts into this Playwright test script: + +```python +import sys +from playwright.sync_api import sync_playwright, expect +from unittest import TestCase + + +class PlaywrightAdapter(TestCase): + @classmethod + def setUpClass(cls): + cls.playwright = sync_playwright().start() + headless = "linux" in sys.platform + # Launch browser once for the whole class + cls.browser = cls.playwright.chromium.launch( + channel="chrome", headless=headless + ) + + @classmethod + def tearDownClass(cls): + cls.browser.close() + cls.playwright.stop() + + def setUp(self): + # Create new session for every test + self.context = self.browser.new_context() + self.page = self.context.new_page() + + def tearDown(self): + # Close the session (browser runs until tearDownClass) + self.page.close() + self.context.close() + + def goto(self, url): + self.page.goto(url) + + def click(self, selector, timeout=7): + timeout = float(timeout) * 1000.0 + self.page.click(selector, timeout=timeout) + + def type(self, selector, text, timeout=10): + timeout = float(timeout) * 1000.0 + if text.endswith("\n"): + self.page.fill(selector, text[:-1], timeout=timeout) + self.page.press(selector, "Enter") + else: + self.page.fill(selector, text, timeout=timeout) + + def assert_element(self, selector, timeout=7): + timeout = float(timeout) * 1000.0 + expect(self.page.locator(selector)).to_be_visible(timeout=timeout) + + def assert_text(self, text, selector="html", timeout=7): + timeout = float(timeout) * 1000.0 + expect(self.page.locator(selector)).to_contain_text( + text, timeout=timeout + ) + + def assert_exact_text(self, text, selector, timeout=7): + timeout = float(timeout) * 1000.0 + expect(self.page.locator(selector)).to_have_text(text, timeout=timeout) + + def assert_element_not_visible(self, selector, timeout=7): + timeout = float(timeout) * 1000.0 + expect(self.page.locator(selector)).to_be_hidden(timeout=timeout) + + def test_add_item_to_cart(self): + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("div.inventory_list") + self.assert_text("Products", "span.title") + + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_exact_text("Your Cart", "span.title") + self.assert_text("Backpack", "div.cart_item") + + self.click("#remove-sauce-labs-backpack") + self.assert_element_not_visible("div.cart_item") + + self.click("#react-burger-menu-btn") + self.click("a#logout_sidebar_link") + self.assert_element("input#login-button") +``` + +(Note that the test portion stayed exactly the same.) + +Migrating from raw Selenium to Playwright provides a number of advantages, such as improved debugging options with the Full Trace Viewer. Here's a modified version of the above example with the Tracing code added in: + +```python +""" +Playwright Adapter Example with Tracing. +Trace logs are saved to: `latest_logs/[MODULE.CLASS.METHOD]/trace.zip` +To open Trace Viewer: `playwright show-trace [PATH_TO_LOGS]/trace.zip` +""" +import os +import sys +from playwright.sync_api import sync_playwright, expect +from unittest import TestCase + + +class PlaywrightAdapterWithTracing(TestCase): + @classmethod + def setUpClass(cls): + cls.playwright = sync_playwright().start() + headless = "linux" in sys.platform + # Launch browser once for the whole class + cls.browser = cls.playwright.chromium.launch( + channel="chrome", headless=headless + ) + + @classmethod + def tearDownClass(cls): + cls.browser.close() + cls.playwright.stop() + + def setUp(self): + # Create new session for every test + self.context = self.browser.new_context() + self.context.tracing.start( + snapshots=True, screenshots=True, sources=True + ) + self.page = self.context.new_page() + + def tearDown(self): + # Close the session (browser runs until tearDownClass) + log_folder = "latest_logs" + test_id = "%s.%s.%s" % ( + self.__class__.__module__, + self.__class__.__name__, + self._testMethodName, + ) + trace_path = os.path.join(log_folder, test_id, "trace.zip") + self.context.tracing.stop(path=trace_path) + self.page.close() + self.context.close() + + def goto(self, url): + self.page.goto(url) + + def click(self, selector, timeout=7): + timeout = float(timeout) * 1000.0 + self.page.click(selector, timeout=timeout) + + def type(self, selector, text, timeout=10): + timeout = float(timeout) * 1000.0 + if text.endswith("\n"): + self.page.fill(selector, text[:-1], timeout=timeout) + self.page.press(selector, "Enter") + else: + self.page.fill(selector, text, timeout=timeout) + + def assert_element(self, selector, timeout=7): + timeout = float(timeout) * 1000.0 + # expect() has built-in retry logic + expect(self.page.locator(selector)).to_be_visible(timeout=timeout) + + def assert_text(self, text, selector="html", timeout=7): + timeout = float(timeout) * 1000.0 + # to_contain_text is the equivalent of "assertIn" + expect(self.page.locator(selector)).to_contain_text( + text, timeout=timeout + ) + + def assert_exact_text(self, text, selector, timeout=7): + timeout = float(timeout) * 1000.0 + expect(self.page.locator(selector)).to_have_text(text, timeout=timeout) + + def assert_element_not_visible(self, selector, timeout=7): + timeout = float(timeout) * 1000.0 + expect(self.page.locator(selector)).to_be_hidden(timeout=timeout) + + def test_add_item_to_cart(self): + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("div.inventory_list") + self.assert_text("Products", "span.title") + + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_exact_text("Your Cart", "span.title") + self.assert_text("Backpack", "div.cart_item") + + self.click("#remove-sauce-labs-backpack") + self.assert_element_not_visible("div.cart_item") + + self.click("#react-burger-menu-btn") + self.click("a#logout_sidebar_link") + self.assert_element("input#login-button") +``` + +You can also implement the Adapter Pattern with SeleniumBase: + +```python +from seleniumbase import SB +from unittest import TestCase + + +class SeleniumBaseAdapter(TestCase): + def setUp(self): + self.sb_context = SB() + self.sb = self.sb_context.__enter__() + + def tearDown(self): + self.sb_context.__exit__(None, None, None) + + def goto(self, url): + self.sb.goto(url) + + def click(self, selector, by="css selector", timeout=7): + self.sb.click(selector=selector, by=by, timeout=timeout) + + def type(self, selector, text, by="css selector", timeout=10): + self.sb.type(selector, text=text, by=by, timeout=timeout) + + def assert_element(self, selector, by="css selector", timeout=7): + self.sb.assert_element(selector, by=by, timeout=timeout) + + def assert_text(self, text, selector="html", by="css selector", timeout=7): + self.sb.assert_text(text, selector=selector, by=by, timeout=timeout) + + def assert_exact_text(self, text, selector, by="css selector", timeout=7): + self.sb.assert_exact_text( + text, selector=selector, by=by, timeout=timeout + ) + + def assert_element_not_visible( + self, selector, by="css selector", timeout=7 + ): + self.sb.assert_element_not_visible(selector, by=by, timeout=timeout) + + def test_add_item_to_cart(self): + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("div.inventory_list") + self.assert_text("Products", "span.title") + + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_exact_text("Your Cart", "span.title") + self.assert_text("Backpack", "div.cart_item") + + self.click("#remove-sauce-labs-backpack") + self.assert_element_not_visible("div.cart_item") + + self.click("#react-burger-menu-btn") + self.click("a#logout_sidebar_link") + self.assert_element("input#login-button") +``` + +Since the target interface for that is the same as the one included with SeleniumBase's `BaseCase` class, you can remove the boilerplate code entirely to give you a very simplified script: + +```python +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class SeleniumBaseTestCase(BaseCase): + def test_add_item_to_cart(self): + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("div.inventory_list") + self.assert_text("Products", "span.title") + + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_exact_text("Your Cart", "span.title") + self.assert_text("Backpack", "div.cart_item") + + self.click("#remove-sauce-labs-backpack") + self.assert_element_not_visible("div.cart_item") + + self.click("#react-burger-menu-btn") + self.click("a#logout_sidebar_link") + self.assert_element("input#login-button") +``` + +This demonstrates that if you extract out all framework-specific code into reusable fixtures, then you can switch frameworks easily without having to directly modify the tests themselves. + +By using the Adapter Pattern, you're making it easy to scale up quickly if you need to switch frameworks, because instead of having to modify 1,000 tests, you'll only need to update *maybe* 10 methods. + +> โš ๏ธ Note on Iframes: While the Adapter Pattern handles simple elements seamlessly, switching from Selenium to Playwright may require updating iframe logic. Selenium uses a "focus-shift" model (`switch_to.frame`), whereas Playwright uses a "targeting" model (`page.frame_locator`). If your suite relies heavily on iframes, your adapter methods for those specific interactions will need to account for this architectural difference. + +-------- + +### ๐Ÿ—บ๏ธ Pattern Mapping + +To understand how the Adapter Pattern applies to browser automation, it helps to map the testing components to their roles in the design pattern: + +| Component | Role in the Pattern | Implementation in these Examples | +| :--- | :--- | :--- | +| **Client** | The consumer of the service. | The `test_add_item_to_cart()` method. | +| **Target (Interface)** | The set of commands the Client uses. | Methods like `goto()`, `click()`, `type()`, and `assert_element()`. | +| **Adapter** | The wrapper that translates commands. | Classes like `PlaywrightAdapter` or `SeleniumBaseAdapter`. | +| **Adaptee** | The underlying "engine" being adapted. | The specific library (`playwright-python` or `selenium`). | + +By keeping the **Client** code focused purely on the **Target** interface, you ensure that the **Adaptee** can be swapped out with minimal friction. + +-------- + +[](https://github.com/seleniumbase/SeleniumBase) diff --git a/examples/migration/raw_selenium/to_playwright/02_adapter_pattern/__init__.py b/examples/migration/raw_selenium/to_playwright/02_adapter_pattern/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/migration/raw_selenium/to_playwright/02_adapter_pattern/raw_pw_adapter.py b/examples/migration/raw_selenium/to_playwright/02_adapter_pattern/raw_pw_adapter.py new file mode 100644 index 00000000000..8f9fe8acd52 --- /dev/null +++ b/examples/migration/raw_selenium/to_playwright/02_adapter_pattern/raw_pw_adapter.py @@ -0,0 +1,87 @@ +"""Playwright Adapter Example.""" +import sys +from playwright.sync_api import sync_playwright, expect +from unittest import TestCase + + +class PlaywrightAdapter(TestCase): + @classmethod + def setUpClass(cls): + cls.playwright = sync_playwright().start() + headless = "linux" in sys.platform + # Launch browser once for the whole class + cls.browser = cls.playwright.chromium.launch( + channel="chrome", headless=headless + ) + + @classmethod + def tearDownClass(cls): + cls.browser.close() + cls.playwright.stop() + + def setUp(self): + # Create new session for every test + self.context = self.browser.new_context() + self.page = self.context.new_page() + + def tearDown(self): + # Close the session (browser runs until tearDownClass) + self.page.close() + self.context.close() + + def goto(self, url): + self.page.goto(url) + + def click(self, selector, timeout=7): + timeout = float(timeout) * 1000.0 + self.page.click(selector, timeout=timeout) + + def type(self, selector, text, timeout=10): + timeout = float(timeout) * 1000.0 + if text.endswith("\n"): + self.page.fill(selector, text[:-1], timeout=timeout) + self.page.press(selector, "Enter") + else: + self.page.fill(selector, text, timeout=timeout) + + def assert_element(self, selector, timeout=7): + timeout = float(timeout) * 1000.0 + expect(self.page.locator(selector)).to_be_visible(timeout=timeout) + + def assert_text(self, text, selector="html", timeout=7): + timeout = float(timeout) * 1000.0 + expect(self.page.locator(selector)).to_contain_text( + text, timeout=timeout + ) + + def assert_exact_text(self, text, selector, timeout=7): + timeout = float(timeout) * 1000.0 + expect(self.page.locator(selector)).to_have_text(text, timeout=timeout) + + def assert_element_not_visible(self, selector, timeout=7): + timeout = float(timeout) * 1000.0 + expect(self.page.locator(selector)).to_be_hidden(timeout=timeout) + + def test_add_item_to_cart(self): + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("div.inventory_list") + self.assert_text("Products", "span.title") + + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_exact_text("Your Cart", "span.title") + self.assert_text("Backpack", "div.cart_item") + + self.click("#remove-sauce-labs-backpack") + self.assert_element_not_visible("div.cart_item") + + self.click("#react-burger-menu-btn") + self.click("a#logout_sidebar_link") + self.assert_element("input#login-button") + + +if __name__ == "__main__": + from pytest import main + main([__file__, "-s"]) diff --git a/examples/migration/raw_selenium/to_playwright/02_adapter_pattern/raw_pw_adapter_with_tracing.py b/examples/migration/raw_selenium/to_playwright/02_adapter_pattern/raw_pw_adapter_with_tracing.py new file mode 100644 index 00000000000..2bc48b01e61 --- /dev/null +++ b/examples/migration/raw_selenium/to_playwright/02_adapter_pattern/raw_pw_adapter_with_tracing.py @@ -0,0 +1,105 @@ +""" +Playwright Adapter Example with Tracing. +Trace logs are saved to: `latest_logs/[MODULE.CLASS.METHOD]/trace.zip` +To open Trace Viewer: `playwright show-trace [PATH_TO_LOGS]/trace.zip` +""" +import os +import sys +from playwright.sync_api import sync_playwright, expect +from unittest import TestCase + + +class PlaywrightAdapterWithTracing(TestCase): + @classmethod + def setUpClass(cls): + cls.playwright = sync_playwright().start() + headless = "linux" in sys.platform + # Launch browser once for the whole class + cls.browser = cls.playwright.chromium.launch( + channel="chrome", headless=headless + ) + + @classmethod + def tearDownClass(cls): + cls.browser.close() + cls.playwright.stop() + + def setUp(self): + # Create new session for every test + self.context = self.browser.new_context() + self.context.tracing.start( + snapshots=True, screenshots=True, sources=True + ) + self.page = self.context.new_page() + + def tearDown(self): + # Close the session (browser runs until tearDownClass) + log_folder = "latest_logs" + test_id = "%s.%s.%s" % ( + self.__class__.__module__, + self.__class__.__name__, + self._testMethodName, + ) + trace_path = os.path.join(log_folder, test_id, "trace.zip") + self.context.tracing.stop(path=trace_path) + self.page.close() + self.context.close() + + def goto(self, url): + self.page.goto(url) + + def click(self, selector, timeout=7): + timeout = float(timeout) * 1000.0 + self.page.click(selector, timeout=timeout) + + def type(self, selector, text, timeout=10): + timeout = float(timeout) * 1000.0 + if text.endswith("\n"): + self.page.fill(selector, text[:-1], timeout=timeout) + self.page.press(selector, "Enter") + else: + self.page.fill(selector, text, timeout=timeout) + + def assert_element(self, selector, timeout=7): + timeout = float(timeout) * 1000.0 + # expect() has built-in retry logic + expect(self.page.locator(selector)).to_be_visible(timeout=timeout) + + def assert_text(self, text, selector="html", timeout=7): + timeout = float(timeout) * 1000.0 + # to_contain_text is the equivalent of "assertIn" + expect(self.page.locator(selector)).to_contain_text( + text, timeout=timeout + ) + + def assert_exact_text(self, text, selector, timeout=7): + timeout = float(timeout) * 1000.0 + expect(self.page.locator(selector)).to_have_text(text, timeout=timeout) + + def assert_element_not_visible(self, selector, timeout=7): + timeout = float(timeout) * 1000.0 + expect(self.page.locator(selector)).to_be_hidden(timeout=timeout) + + def test_add_item_to_cart(self): + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("div.inventory_list") + self.assert_text("Products", "span.title") + + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_exact_text("Your Cart", "span.title") + self.assert_text("Backpack", "div.cart_item") + + self.click("#remove-sauce-labs-backpack") + self.assert_element_not_visible("div.cart_item") + + self.click("#react-burger-menu-btn") + self.click("a#logout_sidebar_link") + self.assert_element("input#login-button") + + +if __name__ == "__main__": + from pytest import main + main([__file__, "-s"]) diff --git a/examples/migration/raw_selenium/to_playwright/02_adapter_pattern/raw_sb_adapter.py b/examples/migration/raw_selenium/to_playwright/02_adapter_pattern/raw_sb_adapter.py new file mode 100644 index 00000000000..1dfb853726b --- /dev/null +++ b/examples/migration/raw_selenium/to_playwright/02_adapter_pattern/raw_sb_adapter.py @@ -0,0 +1,61 @@ +"""SeleniumBase Adapter Example.""" +from seleniumbase import SB +from unittest import TestCase + + +class SeleniumBaseAdapter(TestCase): + def setUp(self): + self.sb_context = SB() + self.sb = self.sb_context.__enter__() + + def tearDown(self): + self.sb_context.__exit__(None, None, None) + + def goto(self, url): + self.sb.goto(url) + + def click(self, selector, by="css selector", timeout=7): + self.sb.click(selector=selector, by=by, timeout=timeout) + + def type(self, selector, text, by="css selector", timeout=10): + self.sb.type(selector, text=text, by=by, timeout=timeout) + + def assert_element(self, selector, by="css selector", timeout=7): + self.sb.assert_element(selector, by=by, timeout=timeout) + + def assert_text(self, text, selector="html", by="css selector", timeout=7): + self.sb.assert_text(text, selector=selector, by=by, timeout=timeout) + + def assert_exact_text(self, text, selector, by="css selector", timeout=7): + self.sb.assert_exact_text( + text, selector=selector, by=by, timeout=timeout + ) + + def assert_element_not_visible( + self, selector, by="css selector", timeout=7 + ): + self.sb.assert_element_not_visible(selector, by=by, timeout=timeout) + + def test_add_item_to_cart(self): + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("div.inventory_list") + self.assert_text("Products", "span.title") + + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_exact_text("Your Cart", "span.title") + self.assert_text("Backpack", "div.cart_item") + + self.click("#remove-sauce-labs-backpack") + self.assert_element_not_visible("div.cart_item") + + self.click("#react-burger-menu-btn") + self.click("a#logout_sidebar_link") + self.assert_element("input#login-button") + + +if __name__ == "__main__": + from pytest import main + main([__file__, "-s"]) diff --git a/examples/migration/raw_selenium/to_playwright/02_adapter_pattern/raw_se_adapter.py b/examples/migration/raw_selenium/to_playwright/02_adapter_pattern/raw_se_adapter.py new file mode 100644 index 00000000000..a6507b250ec --- /dev/null +++ b/examples/migration/raw_selenium/to_playwright/02_adapter_pattern/raw_se_adapter.py @@ -0,0 +1,130 @@ +"""Raw Selenium Adapter Example.""" +import sys +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from unittest import TestCase + + +class RawSeleniumAdapter(TestCase): + def setUp(self): + self.driver = None + options = webdriver.ChromeOptions() + options.add_argument("--disable-notifications") + if "linux" in sys.platform or "--headless" in sys.argv: + options.add_argument("--headless") + options.add_experimental_option( + "excludeSwitches", ["enable-automation", "enable-logging"], + ) + prefs = { + "credentials_enable_service": False, + "profile.password_manager_enabled": False, + "profile.password_manager_leak_detection": False, + } + options.add_experimental_option("prefs", prefs) + service = Service(service_args=["--disable-build-check"]) + self.driver = webdriver.Chrome(options=options, service=service) + + def tearDown(self): + if self.driver: + try: + if self.driver.service.process: + self.driver.quit() + except Exception: + pass + + def wait_for_element_visible( + self, selector, by="css selector", timeout=10 + ): + try: + return WebDriverWait(self.driver, timeout).until( + EC.visibility_of_element_located((by, selector)) + ) + except Exception: + raise Exception( + "Element {%s} was not visible after %s seconds!" + % (selector, timeout) + ) + + def wait_for_element_clickable( + self, selector, by="css selector", timeout=10 + ): + try: + return WebDriverWait(self.driver, timeout).until( + EC.element_to_be_clickable((by, selector)) + ) + except Exception: + raise Exception( + "Element {%s} was not visible/clickable after %s seconds!" + % (selector, timeout) + ) + + def wait_for_element_not_visible( + self, selector, by="css selector", timeout=10 + ): + try: + return WebDriverWait(self.driver, timeout).until( + EC.invisibility_of_element((by, selector)) + ) + except Exception: + raise Exception( + "Element {%s} was still visible after %s seconds!" + % (selector, timeout) + ) + + def goto(self, url): + self.driver.get(url) + + def click(self, selector, by="css selector", timeout=7): + el = self.wait_for_element_clickable(selector, by=by, timeout=timeout) + el.click() + + def type(self, selector, text, by="css selector", timeout=10): + el = self.wait_for_element_clickable(selector, by=by, timeout=timeout) + el.clear() + if not text.endswith("\n"): + el.send_keys(text) + else: + el.send_keys(text[:-1]) + el.submit() + + def assert_element(self, selector, by="css selector", timeout=7): + self.wait_for_element_visible(selector, by=by, timeout=timeout) + + def assert_text(self, text, selector="html", by="css selector", timeout=7): + el = self.wait_for_element_visible(selector, by=by, timeout=timeout) + self.assertIn(text, el.text) + + def assert_exact_text(self, text, selector, by="css selector", timeout=7): + el = self.wait_for_element_visible(selector, by=by, timeout=timeout) + self.assertEqual(text, el.text) + + def assert_element_not_visible( + self, selector, by="css selector", timeout=7 + ): + self.wait_for_element_not_visible(selector, by=by, timeout=timeout) + + def test_add_item_to_cart(self): + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("div.inventory_list") + self.assert_text("Products", "span.title") + + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_exact_text("Your Cart", "span.title") + self.assert_text("Backpack", "div.cart_item") + + self.click("#remove-sauce-labs-backpack") + self.assert_element_not_visible("div.cart_item") + + self.click("#react-burger-menu-btn") + self.click("a#logout_sidebar_link") + self.assert_element("input#login-button") + + +if __name__ == "__main__": + from pytest import main + main([__file__, "-s"]) diff --git a/examples/migration/raw_selenium/to_playwright/ReadMe.md b/examples/migration/raw_selenium/to_playwright/ReadMe.md new file mode 100644 index 00000000000..5802d66ed70 --- /dev/null +++ b/examples/migration/raw_selenium/to_playwright/ReadMe.md @@ -0,0 +1,63 @@ + + +## ๐ŸŽญ Migrating from Raw Selenium to Playwright + +This section provides a comprehensive guide for migrating legacy Selenium test suites to Playwright using **SeleniumBase** as the bridge. + +Instead of a "Big Bang" rewrite that halts development, these strategies allow you to modernize your automation stack incrementally, maintaining test coverage while gaining the speed and debugging power of Playwright. + +--- + +### ๐Ÿ Choose Your Migration Strategy + +#### **Strategy 1: The Hybrid Approach (Recommended)** +**Best for:** Teams with massive legacy suites that need to modernize in phases. +* **The Concept:** Run Selenium and Playwright side-by-side in the same browser session. +* **The Benefit:** Use Playwright's advantages (like the Trace Viewer) for new features while keeping legacy Selenium code for stable modules. +* **Feature Highlight:** Frameworks are bridged via CDP (Chrome DevTools Protocol), sharing cookies, local storage, and session state. +* **[Read the Hybrid Migration Guide](./01_hybrid_approach/ReadMe.md)** + +#### **Strategy 2: The Adapter Pattern** +**Best for:** Projects undergoing a full architectural revamp for a "clean break" from framework-specific code. +* **The Concept:** Extract browser interactions into a standardized interface. +* **The Benefit:** Your test logic remains 100% identical while the underlying engine can be swapped from Selenium to Playwright with a single configuration change. +* **[Read the Adapter Pattern Guide](./02_adapter_pattern/ReadMe.md)** + +--- + +### ๐Ÿ› ๏ธ Frameworks / Migration Tools at a Glance + +| Feature | Selenium (Legacy) | Playwright (Modern) | SeleniumBase (The Bridge) | +| :--- | :--- | :--- | :--- | +| **Execution Speed** | Moderate | Fast | Fast | +| **Auto-Waiting** | Manual / Expected Conditions | Built-in | Built-in | +| **Debugging** | Logs / Screenshots | Trace Viewer / Inspector | Dashboard / Logs / Screenshots | +| **Stealth Abilities** | No | No | **UC Mode / CDP Mode** | +| **Hybrid Mode** | No | No | **Yes (CDP Bridge)** | + +--- + +### โš ๏ธ A Note on Python Performance +It is important to note that the Python version of Playwright is architecturally different from its JavaScript / TypeScript counterpart. If you are migrating solely for raw execution speed, consider the following trade-offs: + +| Metric | Playwright (JS / TS) | Playwright (Python) | +| :--- | :--- | :--- | +| **Execution Speed** | **Faster** (Native) | **Slightly Slower** (Socket overhead) | +| **Cold Start** | Instant | 200ms - 500ms delay (Node.js init) | +| **Concurrency** | **High** (Native Workers) | **Medium** (Relies on `pytest-xdist`) | +| **Resource Usage** | Lower | Higher (Python + Node processes) | + +--- + +### ๐Ÿš€ Getting Started + +1. **[Read the Hybrid Migration Guide](./01_hybrid_approach/ReadMe.md)** +2. **[Read the Adapter Pattern Guide](./02_adapter_pattern/ReadMe.md)** + +--- + +*"Modernization is a journey, not a destination. Switch at your own pace."* + +-------- + +[](https://github.com/seleniumbase/SeleniumBase) diff --git a/examples/migration/raw_selenium/to_playwright/__init__.py b/examples/migration/raw_selenium/to_playwright/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/multiple_cdp_drivers.py b/examples/multiple_cdp_drivers.py new file mode 100644 index 00000000000..7f2924428ce --- /dev/null +++ b/examples/multiple_cdp_drivers.py @@ -0,0 +1,14 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__, "--uc") + + +class MultipleDriversTest(BaseCase): + def test_multiple_drivers(self): + url1 = "https://seleniumbase.io/demo_page" + self.activate_cdp_mode(url1) + driver1 = self.driver + url2 = "https://seleniumbase.io/coffee/" + driver2 = self.get_new_driver(undetectable=True) + self.activate_cdp_mode(url2) + print("\n" + driver1.get_current_url()) + print(driver2.get_current_url()) diff --git a/examples/my_first_test.py b/examples/my_first_test.py old mode 100755 new mode 100644 index aa0d4a2f259..e98cfb029e3 --- a/examples/my_first_test.py +++ b/examples/my_first_test.py @@ -1,86 +1,150 @@ +"""A complete end-to-end test for an e-commerce website.""" from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) class MyTestClass(BaseCase): + def test_swag_labs(self): + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("div.inventory_list") + self.assert_exact_text("Products", "span.title") + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_exact_text("Your Cart", "span.title") + self.assert_text("Backpack", "div.cart_item") + self.click("button#checkout") + self.type("#first-name", "SeleniumBase") + self.type("#last-name", "Automation") + self.type("#postal-code", "77123") + self.click("input#continue") + self.assert_text("Checkout: Overview") + self.assert_text("Backpack", "div.cart_item") + self.assert_text("29.99", "div.inventory_item_price") + self.click("button#finish") + self.assert_exact_text("Thank you for your order!", "h2") + self.assert_element('img[alt="Pony Express"]') + self.js_click("a#logout_sidebar_link") + self.assert_element("div#login_button_container") - def test_basic(self): - self.open("https://xkcd.com/353/") # Navigate browser to page - self.assert_element('img[alt="Python"]') # Assert element on page - self.click('a[rel="license"]') # Click element on page - self.assert_text("free to copy", "div center") # Assert text in area - self.open("https://xkcd.com/1481/") - title = self.get_attribute("#comic img", "title") # Get an attribute - self.assert_true("86,400 seconds per day" in title) - self.click("link=Blag") # Click on link - self.assert_text("The blag of the webcomic", "h2") - self.update_text("input#s", "Robots!\n") # Type text - self.assert_text("Hooray robots!", "#content") - self.open("https://xkcd.com/1319/") - self.assert_exact_text("Automation", "#ctitle") - #### - - ####################################################################### - # - # **** NOTES / USEFUL INFO **** - # - # 1. By default, CSS Selectors are used to identify elements. - # Other options include: "LINK_TEXT", "PARTIAL_LINK_TEXT", "NAME", - # "CLASS_NAME", and "ID", but most of those can be expressed as CSS. - # Here's an example of changing the "by": - # [ - # from selenium.webdriver.common.by import By - # ... - # self.click('Next', by=By.PARTIAL_LINK_TEXT) - # ] - # XPath is used by default if the arg starts with "/", "./", or "(": - # [ - # self.click('/html/body/div[3]/div[4]/p[2]/a') - # ] - # - # If you're completely new to CSS selectors, right-click on a - # web page and select "Inspect" to see the CSS in the html. - # - # 2. Most methods have the optional `timeout` argument. Ex: - # [ - # self.get_text('div center', timeout=15) - # ] - # The `timeout` argument tells the method how many seconds to wait - # for an element to appear before raising an exception. This is - # useful if a web page needs additional time to load an element. - # If you don't specify a `timeout`, a default timeout is used. - # Default timeouts are configured in seleniumbase/config/settings.py - # - # 3. There's usually more than one way to do the same thing. Ex: - # [ - # self.assert_text('free to copy', 'div center') - # ] - # Is the same as: - # [ - # text = self.get_text("div center") - # self.assert_true("free to copy" in text) - # ] - # Or: - # [ - # text = self.find_element('div center').text - # assert("free to copy" in text) - # ] - # - # And the following line: - # [ - # title = self.get_attribute("#comic img", "title") - # ] - # Can also be written as: - # [ - # element = self.find_element("#comic img") - # title = element.get_attribute("title") - # ] - # - # For backwards-compatibilty, some SeleniumBase methods that do the - # same thing have multiple names, kept on from previous versions. - # Ex: wait_for_element_visible() is the same as find_element(). - # Both search for and return the element, and raise an exception if - # the element does not appear on the page within the timeout limit. - # And assert_element() also does this (minus returning the element). - # - # (See seleniumbase/fixtures/base_case.py for the full method list.) +####################################################################### +# +# **** NOTES / USEFUL INFO **** +# +# 1. By default, page elements are identified by "css selector". +# CSS Guide: "https://www.w3schools.com/cssref/css_selectors.asp". +# Other selectors include: "link text", "partial link text", "name", +# "class name", and "id", but most of those can be expressed as CSS. +# +# Here's an example of changing the "by": +# [ +# self.click('Next', by="partial link text") +# ] +# +# XPath is used by default if the arg starts with "/", "./", or "(": +# [ +# self.click('/html/body/div[3]/div[4]/p[2]/a') +# ] +# +# If you're completely new to CSS selectors, right-click on a +# web page and select "Inspect" to see the CSS in the html. +# +# 2. Most methods have the optional "timeout" argument. +# Here's an example of changing the "timeout": +# [ +# self.assert_element('img[alt="Python"]', timeout=15) +# ] +# The "timeout" argument tells the method how many seconds to wait +# for an element to appear before failing the test. This is +# useful if a web page needs additional time to load an element. +# If you don't specify a "timeout", a default timeout is used. +# Default timeouts are configured in seleniumbase/config/settings.py +# +# 3. SeleniumBase methods often perform multiple actions. +# Example: self.type(SELECTOR, TEXT) does the following: +# * Waits for the element to be visible +# * Waits for the element to be interactive +# * Clears the text field +# * Types in the new text +# * Presses Enter/Return if the text ends in "\n": element.submit() +# +# 4. There are duplicate method names that exist for the same method: +# (This makes it easier to switch over from other test frameworks.) +# Example: +# self.goto() = self.open() = self.visit() +# self.type() = self.update_text() = self.fill() +# self.send_keys() = self.add_text() +# self.get_element() = self.wait_for_element_present() +# self.find_element() = self.wait_for_element_visible() +# = self.wait_for_element() +# self.assert_element() = self.assert_element_visible() +# self.assert_text() = self.assert_text_visible() +# self.find_text() = self.wait_for_text_visible() +# = self.wait_for_text() +# self.click_link("LinkText") = self.click("link=LinkText") +# = self.click_link_text("LinkText") +# = self.click('a:contains("LinkText")') +# * self.get(url) is SPECIAL: * +# If {url} is a valid URL, self.get() works just like self.goto() +# Otherwise {url} becomes a selector for calling self.get_element() +# +# 5. There's usually more than one way to do the same thing. +# Example 1: +# [ +# self.assert_text("xkcd: volume 0", "h3") +# ] +# Is the same as: +# [ +# text = self.get_text("h3") +# self.assert_true("xkcd: volume 0" in text) +# ] +# Is also the same as: +# [ +# element = self.find_element("h3") +# text = element.text +# self.assert_true("xkcd: volume 0" in text) +# ] +# +# Example 2: +# [ +# self.assert_exact_text("xkcd.com", "h2") +# ] +# Is the same as: +# [ +# text = self.get_text("h2").strip() +# self.assert_true("xkcd.com".strip() == text) +# ] +# Is also the same as: +# [ +# element = self.find_element("h2") +# text = element.text.strip() +# self.assert_true("xkcd.com".strip() == text) +# ] +# +# Example 3: +# [ +# title = self.get_attribute("#comic img", "title") +# ] +# Is the same as: +# [ +# element = self.find_element("#comic img") +# title = element.get_attribute("title") +# ] +# +# 6. self.assert_exact_text(text) ignores leading and trailing +# whitespace in the text assertion. +# So, self.assert_exact_text("Some Text") accepts [" Some Text "]. +# +# 7. self.js_click(selector) can be used to click on hidden elements. +# +# 8. self.goto(url) will automatically complete URLs missing a prefix. +# Example: google.com will become https://google.com before opened. +# +# 9. For the full method list, see one of the following: +# * SeleniumBase/seleniumbase/fixtures/base_case.py +# * SeleniumBase/help_docs/method_summary.md +# +# 10. BaseCase.main(__name__, __file__) enables "python" to run pytest, +# which is useful if someone forgets that tests run with "pytest". diff --git a/examples/nth_child_test.py b/examples/nth_child_test.py new file mode 100644 index 00000000000..3e098efb3f0 --- /dev/null +++ b/examples/nth_child_test.py @@ -0,0 +1,37 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class NthChildSelectorTests(BaseCase): + def test_locate_rows_with_colors(self): + self.goto("https://xkcd.com/color/rgb/") + tbody = "center > table tbody" + if self.headed: + self.demo_mode = True + self.demo_sleep = 0.5 + self.message_duration = 2.0 + else: + self.demo_mode = False + self.message_duration = 0.1 + self.highlight(tbody) + self.post_message("Part 1: Assert text in given row.") + self.assert_text("teal", tbody + " tr:nth-child(2)") + self.assert_text("aqua", tbody + " tr:nth-child(4)") + self.assert_text("mint", tbody + " tr:nth-child(14)") + self.assert_text("jade", tbody + " tr:nth-child(36)") + soup = self.get_beautiful_soup(self.get_page_source()) + self.post_message("Part 2: Find row with given text.") + self.locate_first_row_with_color("rust", tbody, soup) + self.locate_first_row_with_color("azure", tbody, soup) + self.locate_first_row_with_color("topaz", tbody, soup) + + def locate_first_row_with_color(self, color, tbody, soup): + rows = soup.body.table.find_all("tr") + num_rows = len(rows) + for row in range(num_rows): + row_selector = tbody + " tr:nth-child(%s)" % (row + 1) + if color in rows[row].text: + message = '"%s" found on row %s' % (color, row + 1) + self.post_message_and_highlight(message, row_selector) + return # Found row and done + self.post_error_message('"%s" could not be found on any row!' % color) diff --git a/examples/offline_examples/__init__.py b/examples/offline_examples/__init__.py new file mode 100755 index 00000000000..e69de29bb2d diff --git a/examples/offline_examples/demo_page.html b/examples/offline_examples/demo_page.html new file mode 100644 index 00000000000..356b2088ee5 --- /dev/null +++ b/examples/offline_examples/demo_page.html @@ -0,0 +1,625 @@ + + + + + Web Testing Page + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Demo Page

+
+

SeleniumBase

+
+ + +

Automation Practice

+
Text Input Field:Textarea: + +
Pre-Filled Text Field: + + Button: + +
Placeholder Text Field:Read-Only Text Field: + +
HTML SVG with rect: + + + + + + +

Paragraph with Text:

This Text is Green

Input Slider Control: + +
Select Dropdown: + +
Image in iFrame:
+ +    + + RadioButton 1: + RadioButton 2: +
+ CheckBox: + + + CheckBoxes: + + + + + Pre-Check Box: + + + CheckBox in iFrame:   + +
+ URL Link: + + seleniumbase.com + + Link with Text: + + + SeleniumBase on GitHub +
+ SeleniumBase Docs: + + seleniumbase.io + + The Demo Page: + + + SeleniumBase Demo Page +
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/offline_examples/load_html_test.py b/examples/offline_examples/load_html_test.py new file mode 100644 index 00000000000..d3dd34e3fa2 --- /dev/null +++ b/examples/offline_examples/load_html_test.py @@ -0,0 +1,21 @@ +import pytest +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +@pytest.mark.offline # Can be run with: "pytest -m offline" +class OfflineTests(BaseCase): + def test_load_html_string(self): + html = "

Hello

  

" + self.load_html_string(html) # Open "data:text/html," then replace html + self.assert_text("Hello", "h2") + self.assert_text("OK!", "button") + self.type("input", "Goodbye") + self.click("button") + new_html = '

Checkbox

Check Me!

' + self.set_content(new_html) # Same as load_html_string(), but keeps URL + self.assert_text("Checkbox", "h3") + self.assert_text("Check Me!", "p") + self.assert_false(self.is_selected("input")) + self.click("input") + self.assert_true(self.is_selected("input")) diff --git a/examples/offline_examples/test_demo_page.py b/examples/offline_examples/test_demo_page.py new file mode 100755 index 00000000000..94e7c2f8534 --- /dev/null +++ b/examples/offline_examples/test_demo_page.py @@ -0,0 +1,116 @@ +import os +import pytest +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +@pytest.mark.offline # Can be run with: "pytest -m offline" +class OfflineTests(BaseCase): + def test_demo_page(self): + # Load a local html file into the web browser + dir_path = os.path.dirname(os.path.abspath(__file__)) + file_path = os.path.join(dir_path, "demo_page.html") + self.load_html_file(file_path) + + # Assert the title of the current web page + self.assert_title("Web Testing Page") + + # Assert that an element is visible on the page + self.assert_element("tbody#tbodyId") + + # Assert that a text substring appears in an element + self.assert_text("Demo Page", "h1") + + # Type text into various text fields and then assert + self.type("#myTextInput", "This is Automated") + self.type("textarea.area1", "Testing Time!\n") + self.type('[name="preText2"]', "Typing Text!") + self.assert_text("This is Automated", "#myTextInput") + self.assert_text("Testing Time!\n", "textarea.area1") + self.assert_text("Typing Text!", '[name="preText2"]') + + # Hover & click a dropdown element and assert results + self.assert_text("Automation Practice", "h3") + try: + self.hover_and_click("#myDropdown", "#dropOption2", timeout=1) + except Exception: + # Someone probably moved the mouse while the test ran + self.hover_and_js_click("#myDropdown", "#dropOption2") + self.assert_text("Link Two Selected", "h3") + + # Click a button and then verify the expected results + self.assert_text("This Text is Green", "#pText") + self.click('button:contains("Click Me")') + self.assert_text("This Text is Purple", "#pText") + + # Assert that the given SVG is visible on the page + self.assert_element('svg[name="svgName"]') + + # Verify that a slider control updates a progress bar + self.assert_element('progress[value="50"]') + self.set_value("input#mySlider", "100") + self.assert_element('progress[value="100"]') + + # Verify that a "select" option updates a meter bar + self.assert_element('meter[value="0.25"]') + self.select_option_by_text("#mySelect", "Set to 75%") + self.assert_element('meter[value="0.75"]') + + # Assert an element located inside an iframe + self.assert_false(self.is_element_visible("img")) + self.switch_to_frame("#myFrame1") + self.assert_true(self.is_element_visible("img")) + self.switch_to_default_content() + + # Assert text located inside an iframe + self.assert_false(self.is_text_visible("iFrame Text")) + self.switch_to_frame("#myFrame2") + self.assert_true(self.is_text_visible("iFrame Text")) + self.switch_to_default_content() + + # Verify that clicking a radio button selects it + self.assert_false(self.is_selected("#radioButton2")) + self.click("#radioButton2") + self.assert_true(self.is_selected("#radioButton2")) + + # Verify that clicking a checkbox makes it selected + self.assert_element_not_visible("img#logo") + self.assert_false(self.is_selected("#checkBox1")) + self.click("#checkBox1") + self.assert_true(self.is_selected("#checkBox1")) + self.assert_element("img#logo") + + # Verify clicking on multiple elements with one call + self.assert_false(self.is_selected("#checkBox2")) + self.assert_false(self.is_selected("#checkBox3")) + self.assert_false(self.is_selected("#checkBox4")) + self.click_visible_elements("input.checkBoxClassB") + self.assert_true(self.is_selected("#checkBox2")) + self.assert_true(self.is_selected("#checkBox3")) + self.assert_true(self.is_selected("#checkBox4")) + + # Verify that clicking an iframe checkbox selects it + self.assert_false(self.is_element_visible(".fBox")) + self.switch_to_frame("#myFrame3") + self.assert_true(self.is_element_visible(".fBox")) + self.assert_false(self.is_selected(".fBox")) + self.click(".fBox") + self.assert_true(self.is_selected(".fBox")) + self.switch_to_default_content() + + # Verify Drag and Drop + self.assert_element_not_visible("div#drop2 img#logo") + self.drag_and_drop("img#logo", "div#drop2") + self.assert_element("div#drop2 img#logo") + + # Assert link text - Use click_link() to click + self.assert_link_text("seleniumbase.com") + self.assert_link_text("SeleniumBase on GitHub") + self.assert_link_text("seleniumbase.io") + self.assert_link_text("SeleniumBase Demo Page") + + # Assert exact text + self.assert_exact_text("Demo Page", "h1") + + # Highlight a page element (Also asserts visibility) + self.highlight("h2") diff --git a/examples/offline_examples/test_extended_driver.py b/examples/offline_examples/test_extended_driver.py new file mode 100755 index 00000000000..44dd08cce2c --- /dev/null +++ b/examples/offline_examples/test_extended_driver.py @@ -0,0 +1,61 @@ +import os +import pytest +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +@pytest.mark.offline # Can be run with: "pytest -m offline" +class OfflineTests(BaseCase): + def test_extended_driver(self): + # Load a local html file into the web browser + dir_path = os.path.dirname(os.path.abspath(__file__)) + file_path = os.path.join(dir_path, "demo_page.html") + self.load_html_file(file_path) + + # Get the raw driver + driver = self.driver + + # Assert that an element is visible on the page + driver.assert_element("tbody#tbodyId") + + # Assert that a text substring appears in an element + driver.assert_text("Demo Page", "h1") + + # Type text into various text fields and then assert + driver.type("#myTextInput", "This is Automated") + driver.type("textarea.area1", "Testing Time!\n") + driver.type('[name="preText2"]', "Typing Text!") + driver.assert_text("This is Automated", "#myTextInput") + driver.assert_text("Testing Time!\n", "textarea.area1") + driver.assert_text("Typing Text!", '[name="preText2"]') + + # Hover & click a dropdown element and assert results + driver.assert_text("Automation Practice", "h3") + driver.js_click("#dropOption2") + driver.assert_text("Link Two Selected", "h3") + + # Click a button and then verify the expected results + driver.assert_text("This Text is Green", "#pText") + driver.click('button:contains("Click Me")') + driver.assert_text("This Text is Purple", "#pText") + + # Assert that the given SVG is visible on the page + driver.assert_element('svg[name="svgName"]') + + # Assert an element located inside an iframe + self.assert_false(driver.is_element_visible("img")) + driver.switch_to.frame("myFrame1") + self.assert_true(driver.is_element_visible("img")) + driver.switch_to.default_content() + + # Assert text located inside an iframe + self.assert_false(driver.is_text_visible("iFrame Text")) + driver.switch_to.frame("myFrame2") + self.assert_true(driver.is_text_visible("iFrame Text")) + driver.switch_to.default_content() + + # Assert exact text + driver.assert_exact_text("Demo Page", "h1") + + # Highlight a page element (Also asserts visibility) + driver.highlight("h2") diff --git a/examples/offline_examples/test_handle_alerts.py b/examples/offline_examples/test_handle_alerts.py new file mode 100755 index 00000000000..13a9c9b4585 --- /dev/null +++ b/examples/offline_examples/test_handle_alerts.py @@ -0,0 +1,22 @@ +import pytest +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +@pytest.mark.offline # Can be run with: "pytest -m offline" +class OfflineTests(BaseCase): + def test_alerts(self): + self.goto("data:,") + self.execute_script('window.alert("ALERT!!!");') + self.sleep(1) # Not needed (Lets you see the alert pop up) + self.accept_alert() + self.sleep(1) # Not needed (Lets you see the alert go away) + self.execute_script('window.prompt("My Prompt","defaultText");') + self.sleep(1) # Not needed (Lets you see the alert pop up) + alert = self.switch_to_alert() + self.assert_equal(alert.text, "My Prompt") # Not input field + self.dismiss_alert() + self.sleep(1) # Not needed (Lets you see the alert go away) + if self.browser == "safari" and self._reuse_session: + # Alerts can freeze Safari if reusing the browser session + self.driver.quit() diff --git a/examples/offline_examples/test_request_fixture.py b/examples/offline_examples/test_request_fixture.py new file mode 100644 index 00000000000..d710dff1241 --- /dev/null +++ b/examples/offline_examples/test_request_fixture.py @@ -0,0 +1,26 @@ +import pytest + + +# Use the pytest "request" fixture to get the "sb" fixture (no class) +@pytest.mark.offline +def test_request_fixture(request): + sb = request.getfixturevalue("sb") + sb.goto("data:text/html,

Hello

") + sb.assert_element("html > body") + sb.assert_text("Hello", "body p") + sb.type("input", "Goodbye") + sb.click("body p") + sb.tearDown() + + +# Use the pytest "request" fixture to get the "sb" fixture (in class) +@pytest.mark.offline +class RequestTests: + def test_request_fixture_in_class(self, request): + sb = request.getfixturevalue("sb") + sb.goto("data:text/html,

Hello

") + sb.assert_element("html > body") + sb.assert_text("Hello", "body p") + sb.type("input", "Goodbye") + sb.click("body p") + sb.tearDown() diff --git a/examples/offline_examples/test_user_agent.py b/examples/offline_examples/test_user_agent.py new file mode 100755 index 00000000000..2489f3b13a3 --- /dev/null +++ b/examples/offline_examples/test_user_agent.py @@ -0,0 +1,43 @@ +import pytest +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +@pytest.mark.offline # Can be run with: "pytest -m offline" +class OfflineTests(BaseCase): + def test_get_user_agent(self): + self.goto("data:,") + user_agent = self.get_user_agent() + print('\nUser Agent = "%s"' % user_agent) + + # Now change the user-agent using "execute_cdp_cmd()" + if not self.is_chromium(): + msg = "\n* execute_cdp_cmd() is only for Chromium browsers" + print(msg) + self.skip(msg) + print("\n--------------------------") + if not self.headless: + self.goto("chrome://version/") + self.highlight("#useragent", loops=6) + self.sleep(0.8) + try: + self.execute_cdp_cmd( + "Network.setUserAgentOverride", + { + "userAgent": "Mozilla/5.0 " + "(Nintendo Switch; WifiWebAuthApplet) " + "AppleWebKit/606.4 (KHTML, like Gecko) " + "NF/6.0.1.15.4 NintendoBrowser/5.1.0.20393" + }, + ) + new_user_agent = self.get_user_agent() + print('\nOverrided User Agent = "%s"' % new_user_agent) + finally: + # Reset the user-agent back to the original + self.execute_cdp_cmd( + "Network.setUserAgentOverride", + {"userAgent": user_agent}, + ) + print("\n--------------------------") + user_agent = self.get_user_agent() + print('\nUser Agent = "%s"' % user_agent) diff --git a/examples/old_wordle_script.py b/examples/old_wordle_script.py new file mode 100644 index 00000000000..dbc4a396a73 --- /dev/null +++ b/examples/old_wordle_script.py @@ -0,0 +1,125 @@ +"""Solve the Wordle game using SeleniumBase. +This test runs on archived versions of Wordle, containing Shadow-DOM.""" +import ast +import random +import requests +from seleniumbase import version_tuple +from seleniumbase import BaseCase + +if __name__ == "__main__": + from pytest import main + main([__file__, "--sjw", "--pls=none"]) + + +class WordleTests(BaseCase): + + word_list = [] + + def initialize_word_list(self): + txt_file = "https://seleniumbase.github.io/cdn/txt/wordle_words.txt" + word_string = requests.get(txt_file, timeout=3).text + 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": + if ( + 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) + else: + for w in self.word_list: + if word[i] != w[i]: + 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) + if not self.is_chromium(): + message = "This test requires a Chromium-based browser!" + print(message) + self.skip(message) + if version_tuple < (4, 0, 0): + message = "This test requires SeleniumBase 4.0.0 or newer!" + print(message) + self.skip(message) + + def test_wordle(self): + self.skip_if_incorrect_env() + random.seed() + year = "2022" + month = random.randint(3, 5) + day = random.randint(1, 30) + date = str(year) + "0" + str(month) + str(day) + archive = "https://web.archive.org/web/" + url = "https://www.nytimes.com/games/wordle/index.html" + past_wordle = archive + date + "/" + url + print("\n" + past_wordle) + self.goto(past_wordle) + self.wait_for_element("#wm-ipp-base") + self.remove_elements("#wm-ipp-base") + self.click("game-app::shadow game-modal::shadow game-icon") + self.initialize_word_list() + keyboard_base = "game-app::shadow game-keyboard::shadow " + word = random.choice(self.word_list) + num_attempts = 0 + found_word = False + for attempt in range(6): + num_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.one-and-a-half" + self.click(keyboard_base + button) + row = 'game-app::shadow game-row[letters="%s"]::shadow ' % word + tile = row + "game-tile:nth-of-type(%s)" + self.wait_for_element(tile % "5" + '::shadow [data-state$="t"]') + self.wait_for_element( + tile % "5" + '::shadow [data-animation="idle"]' + ) + 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: + found_word = True + break + self.word_list.remove(word) + self.modify_word_list(word, letter_status) + + self.save_screenshot_to_logs() + if found_word: + print('Word: "%s"\nAttempts: %s' % (word.upper(), num_attempts)) + else: + print('Final guess: "%s" (Not the correct word!)' % word.upper()) + self.fail("Unable to solve for the correct word in 6 attempts!") + self.sleep(3) diff --git a/examples/parameterized_test.py b/examples/parameterized_test.py index bab9e9cc214..7ba5d7614bd 100644 --- a/examples/parameterized_test.py +++ b/examples/parameterized_test.py @@ -1,15 +1,20 @@ -from seleniumbase import BaseCase from parameterized import parameterized +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) -class GoogleTestClass(BaseCase): - - @parameterized.expand([ - ["pypi", "https://pypi.org"], - ["wikipedia", "https://www.wikipedia.org"], - ["seleniumbase", "https://github.com/seleniumbase/SeleniumBase"], - ]) - def test_parameterized_google_search(self, search_term, expected_url): - self.open('https://google.com') - self.update_text('input[title="Search"]', search_term + '\n') - self.assert_text(expected_url, '#search') +class SearchTests(BaseCase): + @parameterized.expand( + [ + ["SeleniumBase Commander", "Commander", "GUI / Commander"], + ["SeleniumBase Recorder", "Recorder", "Recorder Mode"], + ["SeleniumBase Syntax", "Syntax", "Syntax Formats"], + ] + ) + def test_parameterized_search(self, search_term, keyword, title_text): + self.goto("https://seleniumbase.io/help_docs/how_it_works/") + self.assert_title_contains("SeleniumBase Docs") + self.type('input[aria-label="Search"]', search_term) + self.click('mark:contains("%s")' % keyword) + self.assert_title_contains(title_text) + self.save_screenshot_to_logs() diff --git a/examples/performance_test.py b/examples/performance_test.py new file mode 100644 index 00000000000..825ae317f5d --- /dev/null +++ b/examples/performance_test.py @@ -0,0 +1,52 @@ +"""Performance test example. + +Uses decorators.print_runtime(), which prints the runtime duration +of a method or "with"-block after the method (or block) completes. +Also raises an exception when exceeding the time "limit" if set. + +Arguments -> + description # Optional - Shows description in print output. + limit # Optional - Fail if the duration is above the limit. + +Method / Function example usage -> + from seleniumbase import decorators + + @decorators.print_runtime("My Method") + def my_method(): + # code ... + # code ... + +"with"-block example usage -> + from seleniumbase import decorators + + with decorators.print_runtime("My Code Block"): + # code ... + # code ... """ +from seleniumbase import BaseCase +from seleniumbase import decorators +BaseCase.main(__name__, __file__) + + +class PerformanceClass(BaseCase): + @decorators.print_runtime("Open Swag Labs and Log In") + def login_to_swag_labs(self): + print() + with decorators.print_runtime("Open Swag Labs"): + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + + def test_performance_of_swag_labs(self): + self.login_to_swag_labs() + self.assert_element("div.inventory_list") + self.assert_exact_text("Products", "span.title") + with decorators.print_runtime("Add backpack and see cart"): + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_text("Backpack", "div.cart_item") + with decorators.print_runtime("Remove backpack from cart"): + self.click('button:contains("Remove")') # HTML innerText + self.assert_text_not_visible("Backpack", "div.cart_item") + with decorators.print_runtime("Log out from Swag Labs", 3): + self.js_click("a#logout_sidebar_link") + self.assert_element("div#login_button_container") diff --git a/examples/presenter/ReadMe.md b/examples/presenter/ReadMe.md new file mode 100644 index 00000000000..c686e811963 --- /dev/null +++ b/examples/presenter/ReadMe.md @@ -0,0 +1,285 @@ + + +

๐Ÿ“‘ Presenter ๐ŸŽž๏ธ

+ +

SeleniumBase Presenter (slide-maker) lets you use Python to generate HTML presentations.

+ +Here's a sample presentation: + +
+ +([Click on the image/GIF for the actual presentation](https://seleniumbase.io/other/presenter.html)) + +([Here's the code for that presentation](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/presenter/my_presentation.py)) + +Slides can include HTML, code, images, and iframes. + +Here's how to run the example presentation: + +```zsh +cd examples/presenter +pytest my_presentation.py +``` + +Here's a presentation with a chart: + +
+ +([Click on the image/GIF for the actual presentation](https://seleniumbase.io/other/core_presentation.html)) + +([Here's the code for that presentation](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/presenter/core_presentation.py)) + +Here's how to run that example: + +```zsh +cd examples/presenter +pytest core_presentation.py +``` + +

Creating a new presentation:

+ +```python +self.create_presentation(name=None, theme="serif", transition="default") +""" Creates a Reveal-JS presentation that you can add slides to. + @Params + name - If creating multiple presentations at the same time, + use this to specify the name of the current presentation. + theme - Set a theme with a unique style for the presentation. + Valid themes: "serif" (default), "sky", "white", "black", + "simple", "league", "moon", "night", + "beige", "blood", and "solarized". + transition - Set a transition between slides. + Valid transitions: "none" (default), "slide", "fade", + "zoom", "convex", and "concave". +""" +``` + +If creating multiple presentations at the same time, you can pass the ``name`` parameter to distinguish between different presentations. +Notes are disabled by default. You can enable notes by specifying: +``show_notes=True`` + +

Adding a slide to a presentation:

+ +```python +self.add_slide(content=None, image=None, code=None, iframe=None, + content2=None, notes=None, transition=None, name=None) +""" Allows the user to add slides to a presentation. + @Params + content - The HTML content to display on the presentation slide. + image - Attach an image (from a URL link) to the slide. + code - Attach code of any programming language to the slide. + Language-detection will be used to add syntax formatting. + iframe - Attach an iFrame (from a URL link) to the slide. + content2 - HTML content to display after adding an image or code. + notes - Additional notes to include with the slide. + ONLY SEEN if show_notes is set for the presentation. + transition - Set a transition between slides. (overrides previous) + Valid transitions: "none" (default), "slide", "fade", + "zoom", "convex", and "concave". + name - If creating multiple presentations at the same time, + use this to select the presentation to add slides to. +""" +``` + +

Running a presentation:

+ +```python +self.begin_presentation( + filename="my_presentation.html", show_notes=False, interval=0) +""" Begin a Reveal-JS Presentation in the web browser. + @Params + name - If creating multiple presentations at the same time, + use this to select the one you wish to add slides to. + filename - The name of the HTML file that you wish to + save the presentation to. (filename must end in ".html") + show_notes - When set to True, the Notes feature becomes enabled, + which allows presenters to see notes next to slides. + interval - The delay time between autoplaying slides. (in seconds) + If set to 0 (default), autoplay is disabled. +""" +``` + +Before the presentation is run, the full HTML is saved to the ``saved_presentations/`` folder. + + +All methods have the optional ``name`` argument, which is only needed if you're creating multiple presentations at once. + +

Here's an example of using SeleniumBase Presenter:

+ +```python +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + +class MyPresenterClass(BaseCase): + def test_presenter(self): + self.create_presentation(theme="serif") + self.add_slide( + '

Welcome


\n' + '

Press the Right Arrow

') + self.add_slide( + '

SeleniumBase Presenter


\n' + '' + '' + '' + '

\n

Create presentations with Python

') + self.add_slide( + '

Make slides using HTML:


\n' + '' + '\n\n' + '\n' + '' + '\n' + '\n' + '\n' + '
Row ABCRow XYZ
Value ONEValue TWO
Value THREEValue FOUR

\n

(HTML table example)

') + self.add_slide( + '

Keyboard Shortcuts:

\n' + '\n' + '\n' + '\n' + '' + '\n\n' + '\n' + '\n' + '\n' + '
KeyAction
=>Next Slide (N also works)
<=Previous Slide (P also works)
FFull Screen Mode
OOverview Mode Toggle
escExit Full Screen / Overview Mode
.Pause/Resume Toggle
spaceNext Slide (alternative)
') + self.add_slide( + '

Add images to slides:

', + image="https://seleniumbase.github.io/other/seagulls.jpg") + self.add_slide( + '

Add code to slides:

', + code=( + 'from seleniumbase import BaseCase\n\n' + 'class MyTestClass(BaseCase):\n\n' + ' def test_basics(self):\n' + ' self.goto("https://store.xkcd.com/search")\n' + ' self.type(\'input[name="q"]\', "xkcd book\\n")\n' + ' self.assert_text("xkcd: volume 0", "h3")\n' + ' self.goto("https://xkcd.com/353/")\n' + ' self.assert_title("xkcd: Python")\n' + ' self.assert_element(\'img[alt="Python"]\')\n' + ' self.click(\'a[rel="license"]\')\n' + ' self.assert_text("free to copy and reuse")\n' + ' self.go_back()\n' + ' self.click_link("About")\n' + ' self.assert_exact_text("xkcd.com", "h2")')) + self.add_slide( + "

Highlight code in slides:

", + code=( + 'from seleniumbase import BaseCase\n\n' + 'class MyTestClass(BaseCase):\n\n' + ' def test_basics(self):\n' + ' self.goto("https://store.xkcd.com/search")\n' + ' self.type(\'input[name="q"]\', "xkcd book\\n")\n' + ' self.assert_text("xkcd: volume 0", "h3")')) + self.add_slide( + '

Add iFrames to slides:

', + iframe="https://seleniumbase.io/demo_page") + self.add_slide( + '

Getting started is easy:

', + code=( + 'from seleniumbase import BaseCase\n\n' + 'class MyPresenterClass(BaseCase):\n\n' + ' def test_presenter(self):\n' + ' self.create_presentation(theme="serif")\n' + ' self.add_slide("Welcome to Presenter!")\n' + ' self.add_slide(\n' + ' "Add code to slides:",\n' + ' code=(\n' + ' "from seleniumbase import BaseCase\\n\\n"\n' + ' "class MyPresenterClass(BaseCase):\\n\\n"\n' + ' " def test_presenter(self):\\n"\n' + ' " self.create_presentation()\\n"))\n' + ' self.begin_presentation(\n' + ' filename="demo.html", show_notes=True)')) + self.add_slide( + '

Include notes with slides:


', + code=('self.add_slide("[Your HTML goes here]",\n' + ' code="[Your software code goes here]",\n' + ' content2="[Additional HTML goes here]",\n' + ' notes="[Attached speaker notes go here]"\n' + ' "[Note A! -- Note B! -- Note C! ]")'), + notes='

  • Note A!
  • Note B!
  • Note C!
  • Note D!

', + content2="

(Notes can include HTML tags)

") + self.add_slide( + '

Multiple themes available:

', + code=( + 'self.create_presentation(theme="serif")\n\n' + 'self.create_presentation(theme="sky")\n\n' + 'self.create_presentation(theme="simple")\n\n' + 'self.create_presentation(theme="white")\n\n' + 'self.create_presentation(theme="moon")\n\n' + 'self.create_presentation(theme="black")\n\n' + 'self.create_presentation(theme="night")\n\n' + 'self.create_presentation(theme="beige")\n\n' + 'self.create_presentation(theme="league")')) + self.add_slide( + '

The End

', + image="https://seleniumbase.github.io/img/sb_logo_10.png") + self.begin_presentation( + filename="presenter.html", show_notes=True, interval=0) +``` + +That example is from [my_presentation.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/presenter/my_presentation.py), which you can run from the ``examples/presenter`` folder with the following command: + +```zsh +pytest my_presentation.py +``` + +

Saving a presentation:

+ +If you want to save the presentation you created as an HTML file, use: + +```python +self.save_presentation(filename="my_presentation.html", show_notes=True) +``` + +Presentations automatically get saved when calling: + +```python +self.begin_presentation(show_notes=True) +``` + +

Special abilities:

+ +If you want to highlight multiple lines at different times in the same slide with the `` / `` tags, you can use the new ``-``, ``-`` tags, which will generate multiple HTML slides from one Python slide. + +Example: + +```python +self.add_slide( + code=( +

Highlight this on the 1st generated slide

+

Highlight this on the 2nd generated slide

+

Highlight this on the 3rd generated slide

+

Highlight this on the 4th generated slide

+ ) +) +``` + +Those should automatically get converted to `` ... `` on their turn: + +Eg. First generated slide: + +```html +

Highlight this on the first generated slide

+

Highlight this on the second generated slide

+

Highlight this on the third generated slide

+

Highlight this on the fourth generated slide>

+``` + +Eg. Second generated slide: + +```html +

Highlight this on the first generated slide

+

Highlight this on the second generated slide

+

Highlight this on the third generated slide

+

Highlight this on the fourth generated slide>

+``` + +Etc... + +-------- + +

SeleniumBase

diff --git a/examples/presenter/core_presentation.py b/examples/presenter/core_presentation.py new file mode 100644 index 00000000000..c120db4b962 --- /dev/null +++ b/examples/presenter/core_presentation.py @@ -0,0 +1,96 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class PresentationWithChart(BaseCase): + def test_seleniumbase_chart(self): + self.create_presentation(theme="league", transition="slide") + self.create_pie_chart(title="The 4 core areas of SeleniumBase:") + self.add_data_point("Basic API (test methods)", 1) + self.add_data_point("Command-line options (pytest options)", 1) + self.add_data_point("The Console Scripts interface", 1) + self.add_data_point("Advanced API (Tours, Charts, & Presentations)", 1) + self.add_slide("

SeleniumBase core areas

" + self.extract_chart()) + self.add_slide( + "

Basic API (test methods). Example test:

", + code=( + "from seleniumbase import BaseCase\n\n" + "class TestMFALogin(BaseCase):\n\n" + " def test_mfa_login(self):\n" + ' self.goto("seleniumbase.io/realworld/login")\n' + ' self.type("#username", "demo_user")\n' + ' self.type("#password", "secret_pass")\n' + ' self.enter_mfa_code(' + '"#totpcode", "GAXG2MTEOR3DMMDG")\n' + ' self.assert_text("Welcome!", "h1")\n' + ' self.highlight("img#image1")\n' + " self.click('a:contains(\"This Page\")')\n" + " self.save_screenshot_to_logs()\n" + ' self.click_link("Sign out")\n' + ' self.assert_element(\'a:contains("Sign in")\')\n' + ), + ) + self.add_slide( + "

Command-line options. Examples:

", + code=( + "$ pytest my_first_test.py\n" + "$ pytest test_swag_labs.py --mobile\n" + "$ pytest edge_test.py --browser=edge\n" + "$ pytest basic_test.py --headless\n" + "$ pytest my_first_test.py --demo --guest\n" + "$ pytest basic_test.py --slow\n" + "$ pytest -v -m marker2 --headless --save-screenshot\n" + "$ pytest parameterized_test.py --reuse-session\n" + "$ pytest test_suite.py --html=report.html --rs\n" + "$ pytest test_suite.py --dashboard --html=report.html\n" + "$ pytest github_test.py --demo --disable-csp\n" + "$ pytest test_suite.py -n=2 --rs --crumbs\n" + "$ pytest test_demo_site.py --incognito\n" + "$ pytest verify_undetected.py --uc\n" + "$ pytest basic_test.py --sjw --pls=none\n" + ), + ) + self.add_slide( + "

The Console Scripts interface. Examples:

", + code=( + "$ sbase get chromedriver\n" + "$ sbase mkdir new_test_folder\n" + "$ sbase mkfile new_test.py\n" + "$ sbase mkpres new_presentation.py\n" + "$ sbase mkchart new_chart.py\n" + "$ sbase print basic_test.py -n\n" + "$ sbase translate basic_test.py -p --ru -n\n" + "$ sbase grid-hub start\n" + '$ sbase grid-node start --hub="127.0.0.1"\n' + "$ sbase grid-node stop\n" + "$ sbase grid-hub stop\n" + "$ sbase recorder\n" + "$ sbase commander\n" + "$ sbase methods\n" + "$ sbase options\n" + ), + ) + self.add_slide( + '

Advanced API. "Presenter" example:

', + code=( + "from seleniumbase import BaseCase\n\n" + "class MyPresenterClass(BaseCase):\n" + " def test_presenter(self):\n" + ' self.create_presentation(theme="serif")\n' + ' self.add_slide("Welcome to Presenter!")\n' + " self.add_slide(\n" + ' "Add code to slides:",\n' + " code=(\n" + ' "from seleniumbase import BaseCase\\n\\n"\n' + ' "class MyPresenterClass(BaseCase):\\n\\n"\n' + ' " def test_presenter(self):\\n"\n' + ' " self.create_presentation()\\n"))\n' + " self.begin_presentation(\n" + ' filename="demo.html", show_notes=True)' + ), + ) + self.add_slide( + "

The End

", + image="https://seleniumbase.io/cdn/img/sb_logo_g.png", + ) + self.begin_presentation(filename="core_presentation.html") diff --git a/examples/presenter/edge_presentation.py b/examples/presenter/edge_presentation.py new file mode 100644 index 00000000000..228bccb6708 --- /dev/null +++ b/examples/presenter/edge_presentation.py @@ -0,0 +1,670 @@ +from seleniumbase import BaseCase + +if __name__ == "__main__": + from pytest import main + main([__file__, "--edge", "-s", "--disable-csp"]) + + +class EdgePresentationClass(BaseCase): + def test_presentation(self): + if not self.browser == "edge" or not self.disable_csp: + self.driver.quit() + self.get_new_driver(browser="edge", disable_csp=True) + self.demo_mode = False + self.maximize_window() + self._output_file_saves = False + self.create_presentation(theme="serif", transition="fade") + self.add_slide( + "

A deep dive into:

" + "

Browser automation

" + "

on Edge, with Python!

\n" + "


\n" + "

Presented by Michael Mintz

\n" + ) + self.begin_presentation(filename="edge_presentation.html") + self.sleep(0.25) + self.goto("https://www.bostoncodecamp.com/CC34/Schedule/SessionGrid") + self.highlight("h2", loops=8) + if self.is_element_visible('[data-sessionid="467776"]'): + self.highlight('div[data-sessionid="467776"]', loops=10) + self.create_tour(theme="driverjs") + self.add_tour_step( + "

Here we are

", '[data-sessionid="467776"]' + ) + self.play_tour() + self.click('a[onclick*="467776"]') + self.create_tour(theme="hopscotch") + self.add_tour_step( + "

What to expect

", + "div.sz-modal-session", + alignment="left", + ) + self.play_tour() + self.sleep(0.25) + self.goto("data:,") + self.create_presentation(theme="beige", transition="fade") + self.add_slide( + "

About the presenter:

\n" + "
    \n" + "
  • I created SeleniumBase (for Python).
  • \n" + "
  • I lead the Automation Team at iboss.
  • \n" + "
\n", + image="https://seleniumbase.io/other/iboss_booth.png", + ) + self.add_slide( + "

By the end of this presentation, you'll learn:

" + "

\n" + "
    \n" + "
  • How to automate on Edge using Microsoft's WebDriver.
  • " + "
    \n" + "
  • How Python frameworks can simplify Edge automation.
  • " + "
    \n" + "
\n", + ) + self.begin_presentation(filename="edge_presentation.html") + self.sleep(0.25) + self.goto( + "https://learn.microsoft.com/en-us/microsoft-edge/" + "test-and-automation/test-and-automation" + ) + self.wait_for_element("h1") + self.sleep(1) + self.create_tour(theme="default") + self.add_tour_step("

Let's begin the overview!

") + self.play_tour() + if self.is_element_visible('button[data-bi-name="close"]'): + self.click('button[data-bi-name="close"]') + self.wait_for_element_not_visible('button[data-bi-name="close"]') + self.highlight("h1#test-and-automation-in-microsoft-edge") + self.create_tour(theme="driverjs") + self.add_tour_step( + "", "h1#test-and-automation-in-microsoft-edge", alignment="right" + ) + self.add_tour_step("", "nav#center-doc-outline ~ p", alignment="right") + self.add_tour_step( + "", 'table[aria-label*="Test and automation"]', alignment="right" + ) + self.add_tour_step("A framework", "#playwright", alignment="left") + self.add_tour_step("Another framework", "#puppeteer", alignment="left") + self.add_tour_step("Today's framework", "#webdriver", alignment="left") + self.add_tour_step( + "", 'a[href="../webdriver-chromium/"]', alignment="right" + ) + self.play_tour() + self.highlight('a:contains("Use WebDriver to automate")') + self.goto( + "https://learn.microsoft.com/en-us/" + "microsoft-edge/webdriver-chromium/?tabs=python" + ) + self.wait_for_element("h1") + self.create_tour(theme="driverjs") + self.add_tour_step( + "", "#use-webdriver-to-automate-microsoft-edge", alignment="right" + ) + self.add_tour_step( + "", 'div[data-heading-level="h2"] ~ p', alignment="right" + ) + self.add_tour_step( + "", 'div[data-heading-level="h2"] ~ ul', alignment="right" + ) + self.add_tour_step( + "", 'table[aria-label="Table 1"]', alignment="right" + ) + self.add_tour_step( + "", "#download-microsoft-edge-webdriver", alignment="right" + ) + self.add_tour_step( + "", 'img[src*="microsoft-edge-version"]', alignment="right" + ) + self.play_tour() + self.highlight('img[src*="microsoft-edge-version"]') + self.highlight('img[src*="microsoft-edge-driver-install"]', loops=8) + self.highlight('p:contains("that matches your version")', loops=8) + + self.create_tour(theme="driverjs") + self.add_tour_step( + "", '[href*="microsoft-edge/tools/webdriver"]', alignment="right" + ) + self.play_tour() + self.highlight('[href*="microsoft-edge/tools/webdriver"]') + + self.get_new_driver(browser="edge", disable_csp=True) + self.maximize_window() + self.goto( + "https://developer.microsoft.com/en-us/" + "microsoft-edge/tools/webdriver/" + ) + self.wait_for_element("div.common-heading") + self.scroll_to("div.common-heading") + zoom_in = 'div.h1{zoom: 1.02;-moz-transform: scale(1.02);}' + self.add_css_style(zoom_in) + self.highlight("div.common-heading", loops=8) + self.create_tour(theme="hopscotch") + self.add_tour_step("", "div.common-heading", alignment="right") + self.play_tour() + + self.create_tour(theme="hopscotch") + self.add_tour_step( + "", "div.block-heading--sixtyforty", alignment="left" + ) + self.play_tour() + self.quit_extra_driver() + + self.switch_to_default_driver() + self.create_tour(theme="hopscotch") + self.add_tour_step( + "", 'img[src*="microsoft-edge-driver-install"]', alignment="left" + ) + self.play_tour() + self.highlight('p:contains("After the download completes")', loops=10) + self.sleep(0.5) + + self.create_tour(theme="hopscotch") + self.add_tour_step( + "", "#choose-a-webdriver-testing-framework", alignment="left" + ) + self.add_tour_step("", "#using-selenium-4", alignment="left") + self.add_tour_step( + "", "#automate-microsoft-edge-with-webdriver", alignment="left" + ) + self.add_tour_step("", "#automate-microsoft-edge", alignment="left") + self.add_tour_step("", "#tabgroup_1", alignment="left") + self.add_tour_step( + "", '[id*="configure-the-edge-webdriver-serv"]', alignment="left" + ) + self.add_tour_step("", "#tabgroup_2", alignment="left") + self.add_tour_step( + "", "#configure-microsoft-edge-options", alignment="left" + ) + self.add_tour_step( + "", "#choose-specific-browser-binaries", alignment="left" + ) + self.add_tour_step("", "#tabgroup_3", alignment="left") + self.add_tour_step( + "", "#pass-extra-command-line-arguments", alignment="left" + ) + self.add_tour_step("", "#tabgroup_4", alignment="left") + self.add_tour_step( + "", "#other-webdriver-installation-options", alignment="left" + ) + self.add_tour_step( + "", 'code[data-author-content*="docker run"]', alignment="left" + ) + self.add_tour_step( + "", "#opt-out-of-diagnostic-data-collection", alignment="left" + ) + self.add_tour_step( + "", "#developer-tools-availability-policy", alignment="left" + ) + self.play_tour() + self.sleep(0.25) + self.goto("data:,") + + self.create_presentation(theme="sky", transition="fade") + self.add_slide( + "

How do you get Selenium?

\n" + "
\n" + "

(for Python)


\n" + "

pip install selenium

", + image="https://seleniumbase.io/other/selenium_pypi.png", + ) + self.add_slide( + "

What are some building blocks?

\n" + "

\n", + code=( + "from selenium import webdriver\n\n" + "driver = webdriver.Edge()\n\n" + 'driver.get("http://selenium.dev")\n\n' + "element = driver.find_element" + '("css selector", "#docsearch span")\n\n' + "element.click()\n\n" + "elem_2 = driver.find_element" + '("css selector", "#docsearch-input")\n\n' + 'elem_2.send_keys("Python")\n\n' + "driver.quit()\n\n" + ), + ) + self.add_slide( + "

Is Selenium really a framework, or just a library?

\n" + "

\n" + "

Given that Selenium uses WebDriver APIs for interacting with" + " websites, but lacks essential features for structuring tests," + " (and more...), Selenium is really: JUST A LIBRARY!

\n", + image="https://seleniumbase.io/other/selenium_slogan.png" + ) + self.add_slide( + "

JUST A LIBRARY, continued...

\n" + "

\n" + "

Technically, Selenium consists of multiple language bindings" + " for interacting with WebDriver APIs. These bindings include:" + " C#, Java, JS, Python, and Ruby.

\n", + image="https://seleniumbase.io/other/library_books.jpg" + ) + self.add_slide( + "

Test frameworks wrap Selenium to improve things!


\n" + "
" + '' + '(Where does a framework fit in?)\n', + image="https://seleniumbase.io/other/with_a_framework.png", + ) + self.add_slide( + "

What are some disadvantages of using raw Selenium " + "without extra libraries or frameworks?


" + "

\n" + "
", + image="https://seleniumbase.io/other/sel_and_py_2.png", + ) + self.add_slide( + "

What are some disadvantages of using raw Selenium " + "without extra libraries or frameworks?


" + "

\n" + "The default timeout is 0: If an element isn't immediately " + "ready to be interacted with, you'll get errors when trying " + "to interact with those elements.\n" + "

\n", + image="https://seleniumbase.io/other/messy_stacktrace.png", + ) + self.add_slide( + "

What are some disadvantages of using raw Selenium " + "without extra libraries or frameworks?


" + "


\n" + "The command statements can get a bit too long:

\n" + "

" + "driver.find_element(By.CSS_SELECTOR, CSS_SELECTOR).click()" + "


" + "

This is better:

" + "

self.click(CSS_SELECTOR)


", + ) + self.add_slide( + "

What are some disadvantages of using raw Selenium " + "without extra libraries or frameworks?



\n" + "No HTML reports, dashboards, screenshots...
" + "

A test framework can provide those!


", + ) + self.add_slide( + "
Raw Selenium disadvantages, continued...

" + "
No HTML reports, dashboards, screenshots...
\n" + "A test framework can provide those!", + image="https://seleniumbase.io/cdn/img/dash_report.png", + ) + self.add_slide( + "

Raw Selenium disadvantages, continued...


\n
\n" + "

It takes multiple lines of code to do simple tasks:" + "

\n
\n"
+            'element = driver.find_element("css selector", "#password")\n'
+            "element.clear()\n"
+            'element.send_keys("secret_sauce")\n'
+            'element.submit()\n'
+            "
\n
\n" + "

But with a framework, do all that in ONE line:" + '

\n
self.type("#password", "secret_sauce\\n")
' + ) + self.add_slide( + "

What else can test frameworks provide?


\n" + "
    \n" + "
  • Driver management.
  • \n" + "
  • Advanced methods. Eg. " + "
    self.assert_no_broken_links()
  • \n" + "
  • Test assertions. Eg. " + "
    self.assert_text(TEXT, SELECTOR)
  • \n" + "
  • Command-line options. Eg. " + "
    pytest --browser=edge --html=report.html
  • \n" + "
  • Advanced tools (Eg. test recorders)
  • \n" + "
  • Easy to read error messages. Eg. " + '
    Element "h2" was not visible after 10s!
  • ' + "
\n", + ) + self.add_slide( + "

What about test runners?


\n" + "

Python includes powerful test runners, such as pytest." + "

\n", + image="https://seleniumbase.io/other/invoke_pytest.png", + ) + self.add_slide( + "

What can pytest do?


\n" + "
    \n" + "
  • Auto-collect tests to run.
  • \n" + "
  • Use markers for organizing tests.
  • \n" + "
  • Generate test reports.
  • \n" + "
  • Provide test assertions.
  • \n" + "
  • Multithread your tests.
  • \n" + "
  • Use a large number of existing plugins.
  • \n" + "
\n", + ) + self.add_slide( + "

What about complete frameworks?


\n" + "

SeleniumBase combines the best of both " + "Selenium and pytest " + "into a super framework.

\n", + image="https://seleniumbase.io/cdn/img/sb_logo_10c.png", + ) + self.add_slide( + "

SeleniumBase features. (You already saw this!)" + "


\n" + "
    \n" + "
  • Driver management.
  • \n" + "
  • Advanced methods. Eg. " + "
    self.assert_no_broken_links()
  • \n" + "
  • Test assertions. Eg. " + "
    self.assert_text(TEXT, SELECTOR)
  • \n" + "
  • Command-line options. Eg. " + "
    pytest --browser=edge --html=report.html
  • \n" + "
  • Advanced tools (Eg. test recorders)
  • \n" + "
  • Easy to read error messages. Eg. " + '
    Element "h2" was not visible after 10s!
  • ' + "
\n", + ) + self.add_slide( + "

How do you get SeleniumBase?

\n" + "

\n" + "

pip install seleniumbase

", + image="https://seleniumbase.io/other/seleniumbase_github.png", + ) + code = ( + "from seleniumbase import BaseCase\n" + "BaseCase.main(__name__, __file__)\n\n" + "class MyTestClass(BaseCase):\n" + " def test_basics(self):\n" + ' self.goto("https://www.saucedemo.com")\n' + ' self.type("#user-name", "standard_user")\n' + ' self.type("#password", "secret_sauce\\n")\n' + ' self.assert_element("div.inventory_list")\n' + ' self.assert_exact_text("Products", "span.title")\n' + " self.click('button[name*=\"backpack\"]')\n" + ' self.click("#shopping_cart_container a")\n' + ' self.assert_exact_text("Your Cart", "span.title")\n' + ' self.assert_text("Backpack", "div.cart_item")\n' + ' self.click("button#checkout")\n' + ' self.type("#first-name", "SeleniumBase")\n' + ' self.type("#last-name", "Automation")\n' + ' self.type("#postal-code", "77123")\n' + ' self.click("input#continue")\n' + ' self.assert_text("Checkout: Overview")' + ) + self.add_slide( + "SeleniumBase example test
", + code=code, + ) + self.add_slide( + "SeleniumBase example test (Time to run this!)
", + code=code, + ) + self.begin_presentation(filename="edge_presentation.html") + self.sleep(0.25) + + self.get_new_driver(browser="edge") + self.maximize_window() + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("div.inventory_list") + self.assert_exact_text("Products", "span.title") + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_exact_text("Your Cart", "span.title") + self.assert_text("Backpack", "div.cart_item") + self.click("button#checkout") + self.type("#first-name", "SeleniumBase") + self.type("#last-name", "Automation") + self.type("#postal-code", "77123") + self.click("input#continue") + self.assert_text("Checkout: Overview") + self.quit_extra_driver() + + self.create_presentation(theme="serif", transition="fade") + self.add_slide( + '(Now to run that same test in "--demo" mode!)
', + code=code, + ) + self.begin_presentation(filename="edge_presentation.html") + self.sleep(0.25) + self.get_new_driver(browser="edge") + self.maximize_window() + self.demo_mode = True + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("div.inventory_list") + self.assert_exact_text("Products", "span.title") + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_exact_text("Your Cart", "span.title") + self.assert_text("Backpack", "div.cart_item") + self.click("button#checkout") + self.type("#first-name", "SeleniumBase") + self.type("#last-name", "Automation") + self.type("#postal-code", "77123") + self.click("input#continue") + self.assert_text("Checkout: Overview") + self.demo_mode = False + self.quit_extra_driver() + + self.create_presentation(theme="serif", transition="fade") + self.add_slide( + "

What about Edge tests using a mobile emulator?


" + "

pytest --edge --mobile

" + "

Another demo...

", + ) + self.begin_presentation(filename="edge_presentation.html") + self.sleep(0.25) + self.get_new_driver(browser="edge", is_mobile=True) + self.maximize_window() + self.goto("https://www.roblox.com/") + self.assert_element("#download-the-app-container") + self.assert_text("Roblox for Android") + self.assert_text("Continue in App", "a.content-action-emphasis") + self.highlight('span:contains("Roblox for Android")', loops=8) + self.highlight("a.content-action-emphasis", loops=8) + self.quit_extra_driver() + + self.create_presentation(theme="serif", transition="fade") + self.add_slide( + "

What about 2-Factor Auth?



\n" + "

Another demo...

", + ) + self.begin_presentation(filename="edge_presentation.html") + self.sleep(0.25) + self.get_new_driver(browser="edge") + self.maximize_window() + self.demo_mode = True + self.goto("https://seleniumbase.io/realworld/login") + self.type("#username", "demo_user") + self.type("#password", "secret_pass") + self.enter_mfa_code("#totpcode", "GAXG2MTEOR3DMMDG") # 6-digit + self.assert_text("Welcome!", "h1") + self.highlight("img#image1") # A fancier assert_element() call + self.click('a:contains("This Page")') # Use :contains() on any tag + self.save_screenshot_to_logs() # ("./latest_logs" folder for test) + self.click_link("Sign out") # Link must be "a" tag. Not "button". + self.assert_element('a:contains("Sign in")') + self.assert_exact_text("You have been signed out!", "#top_message") + self.demo_mode = False + self.quit_extra_driver() + + self.create_presentation(theme="serif", transition="fade") + self.add_slide( + "

Need some coffee?



\n" + "

Another demo...

", + ) + self.begin_presentation(filename="edge_presentation.html") + self.sleep(0.25) + self.get_new_driver(browser="edge") + self.maximize_window() + self.demo_mode = True + self.goto("https://seleniumbase.io/coffee/") + self.assert_title("Coffee Cart") + self.click('div[data-sb="Cappuccino"]') + self.click('div[data-sb="Flat-White"]') + self.click('div[data-sb="Cafe-Latte"]') + self.click('a[aria-label="Cart page"]') + self.assert_exact_text("Total: $53.00", "button.pay") + self.click("button.pay") + self.type("input#name", "Selenium Coffee") + self.type("input#email", "test@test.test") + self.click("button#submit-payment") + self.assert_text("Thanks for your purchase.", "#app .success") + self.demo_mode = False + self.quit_extra_driver() + + self.create_presentation(theme="serif", transition="fade") + self.add_slide( + "

Let's have some fun!



\n" + "

Another demo...

", + ) + self.begin_presentation(filename="edge_presentation.html") + self.sleep(0.25) + self.get_new_driver(browser="edge") + self.maximize_window() + self.goto("https://seleniumbase.io/error_page/") + self.highlight('img[alt="500 Error"]') + self.highlight("img#parallax_octocat") + self.highlight("#parallax_error_text") + self.highlight('img[alt*="404"]') + self.highlight("img#octobi_wan_catnobi") + self.highlight("img#speeder") + self.quit_extra_driver() + self.create_presentation(theme="serif", transition="fade") + self.add_slide( + "

Let's learn more...



\n" + ) + self.add_slide( + "

Common SeleniumBase methods:


", + code=( + "self.goto(url) # Navigate the browser window to the URL.\n" + "self.type(selector, text) # Update field with the text.\n" + "self.click(selector) # Click element with the selector.\n" + "self.click_link(link_text) # Click link containing text.\n" + "self.check_if_unchecked(selector) # Check checkbox.\n" + "self.uncheck_if_checked(selector) # Uncheck checkbox.\n" + "self.select_option_by_text(dropdown_selector, option)\n" + "self.hover_and_click(hover_selector, click_selector)\n" + "self.drag_and_drop(drag_selector, drop_selector)\n" + "self.choose_file(selector, file_path) # Upload a file.\n" + "self.switch_to_frame(frame) # Switch into the iframe.\n" + "self.switch_to_default_content() # Exit all iframes.\n" + "self.switch_to_parent_frame() # Exit current iframe.\n" + "self.open_new_window() # Open new window in same browser.\n" + "self.switch_to_window(window) # Switch to browser window.\n" + "self.switch_to_default_window() # Go to original window.\n" + "self.assert_element(selector) # Verify element visible.\n" + "self.assert_text(text, selector) # Substring assertion.\n" + "self.assert_exact_text(text, selector) # String assert." + ), + ) + self.add_slide( + "

Common command-line options:


" + "
\n"
+            '--browser=BROWSER'
+            ''
+            '  (Choose web browser. Default: "chrome".)'
+            '\n'
+            '--edge / --firefox / --safari'
+            ''
+            '  (Browser Shortcut.)'
+            '\n'
+            '--headless'
+            ''
+            '  (Run tests headlessly.  Default on Linux OS.)'
+            '\n'
+            '--demo'
+            ''
+            '  (Slow down and see test actions as they occur.)'
+            '\n'
+            '--slow'
+            ''
+            '  (Slow down the automation. Faster than Demo Mode.)'
+            '\n'
+            '--rs / --reuse-session'
+            ''
+            '  (Reuse browser session for tests.)'
+            '\n'
+            '--rcs / --reuse-class-session'
+            ''
+            '  (RS, but for class tests.)'
+            '\n'
+            '--crumbs'
+            ''
+            '  (Clear cookies between tests reusing a session.)'
+            '\n'
+            '--maximize'
+            ''
+            '  (Start tests with the web browser maximized.)'
+            '\n'
+            '--dashboard'
+            ''
+            '  (Enable the SB Dashboard at dashboard.html)'
+            '\n'
+            '--uc'
+            ''
+            '  (Enable undetected-chromedriver mode.)'
+            '\n'
+            '--incognito'
+            ''
+            '  (Enable Incognito mode.)'
+            '\n'
+            '--guest'
+            ''
+            '  (Enable Guest mode.)'
+            '\n'
+            '-m=MARKER'
+            ''
+            '  (Run tests with the specified pytest marker.)'
+            '\n'
+            '-n=NUM'
+            ''
+            '  (Multithread the tests using that many threads.)'
+            '\n'
+            '-v'
+            ''
+            '  (Verbose mode. Print the full names of each test run.)'
+            '\n'
+            '--html=report.html'
+            ''
+            '  (Create a detailed pytest-html report.)'
+            '\n'
+            '--co / --collect-only'
+            ''
+            '  (Only show discovered tests. No run.)'
+            '\n'
+            '--co -q'
+            ''
+            '  (Only show full names of discovered tests. No run.)'
+            '\n'
+            '-x'
+            ''
+            '  (Stop running tests after the first failure is reached.)'
+            '\n'
+            "
" + ) + self.add_slide( + "

Common console scripts:


", + code=( + "sbase get [DRIVER] [OPTIONS] # Eg. chromedriver\n" + "sbase methods # List common Python methods\n" + "sbase options # List common pytest options\n" + "sbase gui # Open the SB GUI for pytest\n" + "sbase caseplans # Open the SB Case Plans App\n" + "sbase mkdir [DIRECTORY] # Create a test directory\n" + "sbase mkfile [FILE.py] # Create a test file\n" + "sbase codegen [FILE.py] [OPTIONS] # Record a test\n" + "sbase recorder # Open the SB Recorder App\n" + "sbase mkpres # Create a Presentation boilerplate\n" + "sbase mkchart # Create a Chart boilerplate\n" + "sbase print [FILE] # Print file to console\n" + "sbase translate [FILE.py] [OPTIONS] # Translate\n" + "sbase extract-objects [SB_FILE.py] # Get objects\n" + "sbase inject-objects [SB_FILE.py] # Swap selectors\n" + "sbase objectify [SB_FILE.py] # Get & swap objects\n" + "sbase revert-objects [SB_FILE.py] # Undo objectify\n" + "sbase download server # Get Selenium Grid JAR file\n" + "sbase grid-hub [start|stop] [OPTIONS] # Start Grid\n" + "sbase grid-node [start|stop] --hub=[IP] # Add Node" + ), + ) + self.add_slide( + "

Live Demo Time!


" + "

(Let's head over to GitHub...)

", + image="https://seleniumbase.io/other/sbase_qr_code_s.png", + ) + self.begin_presentation(filename="edge_presentation.html") diff --git a/examples/presenter/fundamentals.py b/examples/presenter/fundamentals.py new file mode 100644 index 00000000000..115316adc7f --- /dev/null +++ b/examples/presenter/fundamentals.py @@ -0,0 +1,416 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class MyTestClass(BaseCase): + def test_presentation(self): + self.create_presentation(theme="serif", transition="fade") + self.add_slide( + "

Python Selenium:

\n" + "

Fundamentals to Frameworks

" + "

(with SeleniumBase)

\n" + "


\n" + "

Presented by Michael Mintz

\n" + "

SeleniumConf 2023 - Chicago

\n" + ) + self.add_slide( + "

Welcome to Chicago!


\n", + image="https://seleniumbase.io/other/chicago_skyline.png", + ) + self.add_slide( + "

A few shout-outs before starting:



\n" + "
    \n" + "
  • The Selenium Org (made everything possible)

  • " + "
  • Conference organizers (made today possible)

  • " + "
  • My wife (a major supporter of my work)

  • " + "
  • iboss (my employer)
  • \n" + "
", + ) + self.add_slide( + "

About me:

\n" + "
    \n" + "
  • I created the SeleniumBase framework.
  • \n" + "
  • I lead the Automation Team at iboss.
  • \n" + "
", + image="https://seleniumbase.io/other/iboss_booth.png", + ) + self.add_slide( + "

" + "This is the ONLY Python session at SeleniumConf!" + "



\n" + "

\n" + "If one Python session is not enough, come see me afterwards.\n" + "

\n", + image="https://seleniumbase.io/other/python_3d_logo.png", + ) + self.add_slide( + "

By the end of this presentation, you'll learn:

" + "

\n" + "
    \n" + "
  • Python Selenium fundamentals.

  • \n" + "
  • How test frameworks can improve on the fundamentals.
  • " + "
    \n" + "
  • How SeleniumBase makes Python Web Automation easier.
  • " + "
    \n" + "
", + ) + self.add_slide( + "

The Format:

" + "
\n" + "
    \n" + "
  • Slides.
  • \n" + "
  • ReadMe files.
  • \n" + "
  • LOTS of live demos!!!
  • \n" + "
", + image="https://seleniumbase.io/other/presentation_parts.png", + ) + self.add_slide( + "

What does Selenium provide?

\n" + "

\n" + "

Selenium provides an automation library!

\n" + "
(It does NOT provide a test framework.)
\n", + image="https://seleniumbase.io/other/selenium_slogan.png", + ) + self.add_slide( + "

How do you get Selenium?

\n" + "
\n" + "

(for Python)


\n" + "

pip install selenium

", + image="https://seleniumbase.io/other/selenium_pypi.png", + ) + self.add_slide( + "

What are some building blocks?

\n" + "

\n", + code=( + "from selenium import webdriver\n\n" + "driver = webdriver.Edge()\n\n" + 'driver.get("http://selenium.dev")\n\n' + "element = driver.find_element" + '("css selector", "#docsearch span")\n\n' + "element.click()\n\n" + "elem_2 = driver.find_element" + '("css selector", "#docsearch-input")\n\n' + 'elem_2.send_keys("Python")\n\n' + "driver.quit()\n\n" + ), + ) + self.add_slide( + "

Launching a browser can get more complicated:

\n" + "

\n", + code=( + "from selenium import webdriver\n\n" + "options = webdriver.ChromeOptions()\n" + 'options.add_argument("--disable-notifications")\n' + "options.add_experimental_option(\n " + '"excludeSwitches", ["enable-automation", "enable-logging"]\n' + ')\n' + "prefs = {\n" + ' "credentials_enable_service": False,\n' + ' "profile.password_manager_enabled": False,\n' + "}\n" + 'options.add_experimental_option("prefs", prefs)\n' + "driver = webdriver.Chrome(options=options)\n" + ), + ) + self.add_slide( + "

Test frameworks wrap Selenium to improve things!


\n" + "
" + '' + '(Where does a framework fit in?)\n', + image="https://seleniumbase.io/other/with_a_framework.png", + ) + self.add_slide( + "

What are some disadvantages of using raw Selenium " + "without additional libraries or frameworks?


" + "

\n" + "
", + image="https://seleniumbase.io/other/sel_and_py_2.png", + ) + self.add_slide( + "

What are some disadvantages of using raw Selenium " + "without additional libraries or frameworks?


" + "

\n" + "The default timeout is 0: If an element isn't immediately " + "ready to be interacted with, you'll get errors when trying " + "to interact with those elements.\n" + "

\n", + image="https://seleniumbase.io/other/messy_stacktrace.png", + ) + self.add_slide( + "

What are some disadvantages of using raw Selenium " + "without extra libraries or frameworks?


" + "


\n" + "The command statements can get a bit too long:

\n" + "

" + "driver.find_element(By.CSS_SELECTOR, CSS_SELECTOR).click()" + "


" + "

This is better:

" + "

self.click(CSS_SELECTOR)


", + ) + self.add_slide( + "

What are some disadvantages of using raw Selenium " + "without additional libraries or frameworks?


" + "


\n" + "No HTML reports, dashboards, results, screenshots..." + "


A test framework can provide those!
", + ) + self.add_slide( + "

Raw Selenium disadvantages, continued...
\n" + "No HTML reports, dashboards, results, screenshots...
" + "A test framework can provide those!
", + image="https://seleniumbase.io/cdn/img/dash_report.png", + ) + self.add_slide( + "

Raw Selenium disadvantages, continued...


\n" + "
\n" + "

It takes multiple lines of code to do simple tasks:" + "

\n
\n"
+            'element = driver.find_element("css selector", "#password")\n'
+            "element.clear()\n"
+            'element.send_keys("secret_sauce")\n'
+            'element.submit()\n'
+            "
\n
\n" + "

But with a framework, do all that in ONE line:

\n" + '
self.type("#password", "secret_sauce\\n")
' + ) + self.add_slide( + "

Raw Selenium disadvantages, continued...


\n" + "
\n" + "

It takes multiple lines of code to do simple tasks:

\n" + "
\n"
+            'element = driver.find_element("css selector", "#password")\n'
+            "element.clear()\n"
+            'element.send_keys("secret_sauce")\n'
+            'element.submit()\n'
+            "
\n
\n" + "

But with a framework, do all that in ONE line:" + "

\n" + '
self.type("#password", "secret_sauce\\n")
' + ) + self.add_slide( + "

What else can test frameworks provide?


\n" + "
    \n" + "
  • Driver management.
  • \n" + "
  • Advanced methods. Eg. " + "
    self.assert_no_broken_links()
  • \n" + "
  • Test assertions. Eg. " + "
    self.assert_text(TEXT, SELECTOR)
  • \n" + "
  • Command-line options. Eg. " + "
    pytest --browser=edge --html=report.html
  • \n" + "
  • Advanced tools (Eg. test recorders)
  • \n" + "
  • Easy to read error messages. Eg. " + '
    Element "h2" was not visible after 10s!
  • ' + "
", + ) + self.add_slide( + "

What about test runners?


\n" + "

Python includes powerful test runners, such as pytest." + "

\n", + image="https://seleniumbase.io/other/invoke_pytest.png", + ) + self.add_slide( + "

What can pytest do?


\n" + "
    \n" + "
  • Auto-collect tests to run.
  • \n" + "
  • Use markers for organizing tests.
  • \n" + "
  • Generate test reports.
  • \n" + "
  • Provide test assertions.
  • \n" + "
  • Multithread your tests.
  • \n" + "
  • Use a large number of existing plugins.
  • \n" + "
", + ) + self.add_slide( + "

What about complete frameworks?


\n" + "

SeleniumBase combines the best of both " + "Selenium and pytest " + "into a super framework.

\n", + image="https://seleniumbase.io/cdn/img/sb_logo_10c.png", + ) + self.add_slide( + "

SeleniumBase features. (You already saw this slide!)" + "


\n" + "
    \n" + "
  • Driver management.
  • \n" + "
  • Advanced methods. Eg. " + "
    self.assert_no_broken_links()
  • \n" + "
  • Test assertions. Eg. " + "
    self.assert_text(TEXT, SELECTOR)
  • \n" + "
  • Command-line options. Eg. " + "
    pytest --browser=edge --html=report.html
  • \n" + "
  • Advanced tools (Eg. test recorders)
  • \n" + "
  • Easy to read error messages. Eg. " + '
    Element "h2" was not visible after 10s!
  • ' + "
", + ) + self.add_slide( + "

How do you get SeleniumBase?

\n" + "

\n" + "

pip install seleniumbase

", + image="https://seleniumbase.io/other/seleniumbase_github.png", + ) + self.add_slide( + "

SeleniumBase example test:


", + code=( + "from seleniumbase import BaseCase\n" + "BaseCase.main(__name__, __file__)\n\n" + "class MyTestClass(BaseCase):\n" + " def test_basics(self):\n" + ' self.goto("https://www.saucedemo.com")\n' + ' self.type("#user-name", "standard_user")\n' + ' self.type("#password", "secret_sauce\\n")\n' + ' self.assert_element("div.inventory_list")\n' + ' self.assert_exact_text("Products", "span.title")\n' + " self.click('button[name*=\"backpack\"]')\n" + ' self.click("#shopping_cart_container a")\n' + ' self.assert_exact_text("Your Cart", "span.title")\n' + ' self.assert_text("Backpack", "div.cart_item")\n' + ' self.click("button#checkout")\n' + ' self.type("#first-name", "SeleniumBase")\n' + ' self.type("#last-name", "Automation")\n' + ' self.type("#postal-code", "77123")\n' + ' self.click("input#continue")\n' + ' self.assert_text("Checkout: Overview")' + ), + ) + self.add_slide( + "

Common SeleniumBase methods:


", + code=( + "self.goto(url) # Navigate the browser window to the URL.\n" + "self.type(selector, text) # Update field with the text.\n" + "self.click(selector) # Click element with the selector.\n" + "self.click_link(link_text) # Click link containing text.\n" + "self.check_if_unchecked(selector) # Check checkbox.\n" + "self.uncheck_if_checked(selector) # Uncheck checkbox.\n" + "self.select_option_by_text(dropdown_selector, option)\n" + "self.hover_and_click(hover_selector, click_selector)\n" + "self.drag_and_drop(drag_selector, drop_selector)\n" + "self.choose_file(selector, file_path) # Upload a file.\n" + "self.switch_to_frame(frame) # Switch into the iframe.\n" + "self.switch_to_default_content() # Exit all iframes.\n" + "self.switch_to_parent_frame() # Exit current iframe.\n" + "self.open_new_window() # Open new window in same browser.\n" + "self.switch_to_window(window) # Switch to browser window.\n" + "self.switch_to_default_window() # Go to original window.\n" + "self.assert_element(selector) # Verify element visible.\n" + "self.assert_text(text, selector) # Substring assertion.\n" + "self.assert_exact_text(text, selector) # String assert." + ), + ) + self.add_slide( + "

Common command-line options:


" + "
\n"
+            '--browser=BROWSER'
+            ''
+            '  (Choose web browser. Default: "chrome".)'
+            '\n'
+            '--edge / --firefox / --safari'
+            ''
+            '  (Browser Shortcut.)'
+            '\n'
+            '--headless'
+            ''
+            '  (Run tests headlessly.  Default on Linux OS.)'
+            '\n'
+            '--demo'
+            ''
+            '  (Slow down and see test actions as they occur.)'
+            '\n'
+            '--slow'
+            ''
+            '  (Slow down the automation. Faster than Demo Mode.)'
+            '\n'
+            '--reuse-session / --rs'
+            ''
+            '  (Reuse browser session for tests.)'
+            '\n'
+            '--reuse-class-session / --rcs'
+            ''
+            '  (RS, but for class tests.)'
+            '\n'
+            '--crumbs'
+            ''
+            '  (Clear cookies between tests reusing a session.)'
+            '\n'
+            '--maximize'
+            ''
+            '  (Start tests with the web browser maximized.)'
+            '\n'
+            '--dashboard'
+            ''
+            '  (Enable the SB Dashboard at dashboard.html)'
+            '\n'
+            '--uc'
+            ''
+            '  (Enable undetected-chromedriver mode.)'
+            '\n'
+            '--incognito'
+            ''
+            '  (Enable Incognito mode.)'
+            '\n'
+            '--guest'
+            ''
+            '  (Enable Guest mode.)'
+            '\n'
+            '-m=MARKER'
+            ''
+            '  (Run tests with the specified pytest marker.)'
+            '\n'
+            '-n=NUM'
+            ''
+            '  (Multithread the tests using that many threads.)'
+            '\n'
+            '-v'
+            ''
+            '  (Verbose mode. Print the full names of each test run.)'
+            '\n'
+            '--html=report.html'
+            ''
+            '  (Create a detailed pytest-html report.)'
+            '\n'
+            '--collect-only / --co'
+            ''
+            '  (Only show discovered tests. No run.)'
+            '\n'
+            '--co -q'
+            ''
+            '  (Only show full names of discovered tests. No run.)'
+            '\n'
+            '-x'
+            ''
+            '  (Stop running tests after the first failure is reached.)'
+            '\n'
+            "
" + ) + self.add_slide( + "

Common console scripts:


", + code=( + "sbase get [DRIVER] [OPTIONS] # Eg. chromedriver\n" + "sbase methods # List common Python methods\n" + "sbase options # List common pytest options\n" + "sbase gui # Open the SB GUI for pytest\n" + "sbase caseplans # Open the SB Case Plans App\n" + "sbase mkdir [DIRECTORY] # Create a test directory\n" + "sbase mkfile [FILE.py] # Create a test file\n" + "sbase codegen [FILE.py] [OPTIONS] # Record a test\n" + "sbase recorder # Open the SB Recorder App\n" + "sbase mkpres # Create a Presentation boilerplate\n" + "sbase mkchart # Create a Chart boilerplate\n" + "sbase print [FILE] # Print file to console\n" + "sbase translate [FILE.py] [OPTIONS] # Translate\n" + "sbase extract-objects [SB_FILE.py] # Get objects\n" + "sbase inject-objects [SB_FILE.py] # Swap selectors\n" + "sbase objectify [SB_FILE.py] # Get & swap objects\n" + "sbase revert-objects [SB_FILE.py] # Undo objectify\n" + "sbase download server # Get Selenium Grid JAR file\n" + "sbase grid-hub [start|stop] [OPTIONS] # Start Grid\n" + "sbase grid-node [start|stop] --hub=[IP] # Add Node" + ), + ) + self.add_slide( + "

Live Demo Time!



" + "

(Starting with raw Selenium, and evolving that into " + "SeleniumBase.)

", + image="https://seleniumbase.io/other/python_3d_logo.png", + ) + self.begin_presentation(filename="fundamentals.html") diff --git a/examples/presenter/hacking_with_cdp.py b/examples/presenter/hacking_with_cdp.py new file mode 100644 index 00000000000..5d4da1a0e01 --- /dev/null +++ b/examples/presenter/hacking_with_cdp.py @@ -0,0 +1,146 @@ +# https://www.youtube.com/watch?v=vt2zsdiNh3U +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class UCPresentationClass(BaseCase): + def test_hacking_with_cdp(self): + self.goto("data:,") + self.set_window_position(4, 40) + self._output_file_saves = False + self.create_presentation(theme="serif", transition="none") + self.add_slide("

Press SPACE to begin!

\n") + self.add_slide( + "

Coming up on the Hacker Show...

\n" + "
    \n" + '' + "
", + ) + self.add_slide( + "

Coming up on the Hacker Show...

\n" + "

    \n" + "
  • Intercepting requests/responses/XHR with CDP." + "

  • \n" + "

    \n" + "

    \n" + "

    \n" + "

    \n" + "
", + ) + self.add_slide( + "

Coming up on the Hacker Show...

\n" + "

    \n" + "
  • Intercepting requests/responses/XHR with CDP." + "

  • \n" + "
  • Modifying requests: CDP.Fetch.continueRequest." + "

  • \n" + "

    \n" + "

    \n" + "

    \n" + "
", + ) + self.add_slide( + "

Coming up on the Hacker Show...

\n" + "

    \n" + "
  • Intercepting requests/responses/XHR with CDP." + "

  • \n" + "
  • Modifying requests: CDP.Fetch.continueRequest." + "

  • \n" + "
  • Controlling browsers via remote-debugging-port" + "

  • \n" + "

    \n" + "

    \n" + "
", + ) + self.add_slide( + "

Coming up on the Hacker Show...

\n" + "

    \n" + "
  • Intercepting requests/responses/XHR with CDP." + "

  • \n" + "
  • Modifying requests: CDP.Fetch.continueRequest." + "

  • \n" + "
  • Controlling browsers via remote-debugging-port" + "

  • \n" + "
  • Bypassing CAPTCHAs & anti-bot defenses." + "

  • \n" + "

    \n" + "
", + ) + self.add_slide( + "

Coming up on the Hacker Show...

\n" + "

    \n" + "
  • Intercepting requests/responses/XHR with CDP." + "

  • \n" + "
  • Modifying requests: CDP.Fetch.continueRequest." + "

  • \n" + "
  • Controlling browsers via remote-debugging-port" + "

  • \n" + "
  • Bypassing CAPTCHAs & anti-bot defenses." + "

  • \n" + "
  • And live demos of all the above... with Python!" + "

  • \n" + "
", + ) + self.add_slide( + "

Get ready for some
serious hacking!

" + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '

The remote-debugging-port

' + '' + ) + self.add_slide( + "

Let's get to the fun part...

" + '' + ) + self.begin_presentation(filename="uc_presentation.html") diff --git a/examples/presenter/multi_uc.py b/examples/presenter/multi_uc.py new file mode 100644 index 00000000000..dfed5714a03 --- /dev/null +++ b/examples/presenter/multi_uc.py @@ -0,0 +1,17 @@ +"""Part of the UC presentation. (Upgraded for CDP Mode.)""" +import pytest +from random import randint +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__, "--uc", "-n3") + + +@pytest.mark.parametrize("", [[]] * 3) +def test_multi_threaded(sb): + sb.activate_cdp_mode() # If not UC Mode, then 2nd browser + sb.set_window_rect(randint(4, 680), randint(8, 380), 840, 520) + sb.goto("https://gitlab.com/users/sign_in") + sb.sleep(2) + sb.solve_captcha() + sb.assert_text("Username", '[for="user_login"]', timeout=3) + sb.post_message("SeleniumBase wasn't detected", duration=4) + sb._print("\n Success: SeleniumBase wasn't detected! ") diff --git a/examples/presenter/my_presentation.py b/examples/presenter/my_presentation.py new file mode 100644 index 00000000000..92e7db478aa --- /dev/null +++ b/examples/presenter/my_presentation.py @@ -0,0 +1,138 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class MyPresenterClass(BaseCase): + def test_presenter(self): + self.create_presentation(theme="serif", transition="none") + self.add_slide( + "

Welcome


\n" "

Press the Right Arrow

" + ) + self.add_slide( + "

SeleniumBase Presenter


\n" + '' + '' + '' + "

\n

Create presentations with Python

" + ) + self.add_slide( + "

Make slides using HTML:


\n" + '' + '\n\n' + "\n" + '' + "\n" + '\n' + "\n" + "
Row ABCRow XYZ
Value ONEValue TWO
Value THREEValue FOUR

\n

(HTML table example)

" + ) + self.add_slide( + "

Keyboard Shortcuts:

\n" + '\n' + "\n" + "\n" + "" + "\n\n" + "\n" + "\n" + "\n" + "
KeyAction
=>Next Slide (N also works)
<=Previous Slide (P also works)
FFull Screen Mode
OOverview Mode Toggle
escExit Full Screen / Overview Mode
.Pause/Resume Toggle
spaceNext Slide (alternative)
" + ) + self.add_slide( + "

Add images to slides:

", + image="https://seleniumbase.github.io/other/seagulls.jpg", + ) + self.add_slide( + "

Add code to slides:

", + code=( + "from seleniumbase import BaseCase\n" + "BaseCase.main(__name__, __file__)\n\n" + "class MyTestClass(BaseCase):\n" + " def test_basics(self):\n" + ' self.goto("https://xkcd.com/353/")\n' + ' self.assert_title("xkcd: Python")\n' + " self.assert_element('img[alt=\"Python\"]')\n" + " self.click('a[rel=\"license\"]')\n" + ' self.assert_text("free to copy and reuse")\n' + " self.go_back()\n" + ' self.click_link("About")\n' + ' self.assert_exact_text("xkcd.com", "h2")' + ), + ) + self.add_slide( + "

Highlight code in slides:

", + code=( + "from seleniumbase import BaseCase\n" + "BaseCase.main(__name__, __file__)\n\n" + "class MyTestClass(BaseCase):\n" + " def test_basics(self):\n" + ' self.goto("https://xkcd.com/353/")\n' + ' self.assert_title("xkcd: Python")\n' + " self.assert_element('img[alt=\"Python\"]')\n" + " self.click('a[rel=\"license\"]')\n" + ' self.assert_text("free to copy and reuse")\n' + " self.go_back()\n" + ' self.click_link("About")\n' + ' self.assert_exact_text("xkcd.com", "h2")' + ), + ) + self.add_slide( + "

Add iFrames to slides:

", + iframe="https://seleniumbase.io/demo_page", + ) + self.add_slide( + "

Getting started is easy:

", + code=( + "from seleniumbase import BaseCase\n" + "BaseCase.main(__name__, __file__)\n\n" + "class MyPresenterClass(BaseCase):\n" + " def test_presenter(self):\n" + ' self.create_presentation(theme="serif")\n' + ' self.add_slide("Welcome to Presenter!")\n' + " self.add_slide(\n" + ' "Add code to slides:",\n' + " code=(\n" + ' "from seleniumbase import BaseCase\\n\\n"\n' + ' "class MyPresenterClass(BaseCase):\\n\\n"\n' + ' " def test_presenter(self):\\n"\n' + ' " self.create_presentation()\\n"))\n' + " self.begin_presentation(\n" + ' filename="demo.html", show_notes=True)' + ), + ) + self.add_slide( + "

Include notes with slides:


", + code=( + 'self.add_slide("[Your HTML goes here]",\n' + ' code="[Your software code goes here]",\n' + ' content2="[Additional HTML goes here]",\n' + ' notes="[Attached speaker notes go here]"\n' + ' "[Note A! -- Note B! -- Note C! ]")' + ), + notes="

  • Note A!
  • Note B!
  • Note C!
  • Note D!

", + content2="

(Notes can include HTML tags)

", + ) + self.add_slide( + "

Multiple themes available:

", + code=( + 'self.create_presentation(theme="serif")\n\n' + 'self.create_presentation(theme="sky")\n\n' + 'self.create_presentation(theme="simple")\n\n' + 'self.create_presentation(theme="white")\n\n' + 'self.create_presentation(theme="moon")\n\n' + 'self.create_presentation(theme="black")\n\n' + 'self.create_presentation(theme="night")\n\n' + 'self.create_presentation(theme="beige")\n\n' + 'self.create_presentation(theme="league")' + ), + ) + self.add_slide( + "

The End

", + image="https://seleniumbase.github.io/img/sb_logo_10.png", + ) + self.begin_presentation( + filename="presenter.html", show_notes=True, interval=0 + ) diff --git a/examples/presenter/py_virtual_envs.py b/examples/presenter/py_virtual_envs.py new file mode 100644 index 00000000000..89608e011b8 --- /dev/null +++ b/examples/presenter/py_virtual_envs.py @@ -0,0 +1,194 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class PythonVirtualEnvs(BaseCase): + def test_py_virtual_envs(self): + self.create_presentation(theme="serif", transition="slide") + self.add_slide( + "

Python Virtual Environments:


\n" + "

What, Why, and How



\n" + "

Presented by Michael Mintz

\n" + "

Granite State Code Camp - Sat, Nov 14, 2020

" + ) + self.add_slide( + "

About me:

\n" + "
    " + "
  • I created the SeleniumBase framework.
  • " + "
  • I'm currently the DevOps Lead at iboss.
  • " + "
\n", + image="https://seleniumbase.io/other/iboss_booth.png", + ) + self.add_slide( + "

Topics & tools covered by this presentation:

" + "

\n" + "
    " + "
  • Overview of Virtual Environments
  • " + "
  • Python package management
  • " + '
  • Python 3 "venv"
  • ' + "
  • virtualenv / virtualenvwrapper
  • " + '
  • pip / "pip install"
  • ' + "
  • requirements.txt files
  • " + "
  • setup.py files
  • " + "
" + ) + self.add_slide( + "

Topics & tools that are NOT covered here:


\n" + "
    " + '
  • "conda"
  • ' + '
  • "pipenv"
  • ' + '
  • "poetry"
  • ' + '
  • "pipx"
  • ' + "

" + "

(Other Python package management tools)

" + ) + self.add_slide( + "

What is a Python virtual environment?



\n" + "

A Python virtual environment is a partitioned directory" + " where a Python interpreter, libraries/packages, and scripts" + " can be installed and isolated from those installed in other" + " virtual environments or the global environment.

" + ) + self.add_slide( + "

Why should we use Python virtual environments?" + "



\n" + "

We should use Python virtual environments because different" + " Python projects can have conflicting Python dependencies that" + " cannot coexist in the same env.

" + ) + self.add_slide( + "

Why? - continued



\n" + "

Example: Project A and Project B both depend on" + " different versions of the same Python library!

" + "

Therefore, installing the second project requirements" + " would overwrite the first one, causing it to break.

", + code=( + "# Project A requirement:\n" + "urllib3==1.25.3\n\n" + "# Project B requirement:\n" + "urllib3==1.26.2" + ), + ) + self.add_slide( + "

Why? - continued



\n" + "

It is also possible that Project A and Project B" + " require different versions of Python installed!

", + code=( + "# Project A requirement:\n" + "Python-3.8\n\n" + "# Project B requirement:\n" + "Python-2.7" + ), + ) + self.add_slide( + "

How do we create and use Python virtual envs?" + "



\n" + "
There are tools/scripts we can use:

" + "
    " + '
  • The Python 3 "venv" command
  • ' + "
  • virtualenv / virtualenvwrapper
  • " + "
" + ) + self.add_slide( + '

Python 3 "venv"



\n' + '"venv" creates virtual environments in the location where run' + " (generally with Python projects).", + code=( + "# Mac / Linux\n" + "python3 -m venv ENV_NAME\n" + "source ENV_NAME/bin/activate\n\n" + "# Windows\n" + "py -m venv ENV_NAME\n" + "call ENV_NAME\\Scripts\\activate\n\n" + '# (Type "deactivate" to leave a virtual environment.)' + ), + ) + self.add_slide( + '

"mkvirtualenv" (from virtualenvwrapper)


\n' + '
"mkvirtualenv" creates virtual environments in one place' + " (generally in your home directory).", + code=( + "# Mac / Linux\n" + "python3 -m pip install virtualenvwrapper\n" + "export WORKON_HOME=$HOME/.virtualenvs\n" + "source `which virtualenvwrapper.sh`\n" + "mkvirtualenv ENV_NAME\n\n" + "# Windows\n" + "py -m pip install virtualenvwrapper-win\n" + "mkvirtualenv ENV_NAME\n\n" + '# (Type "deactivate" to leave a virtual environment.)' + ), + ) + self.add_slide( + "

List of commands from virtualenvwrapper

" + "

", + code=( + "# Create a virtual environment:\n" + "mkvirtualenv ENV_NAME\n\n" + "# Exit your virtual environment:\n" + "deactivate\n\n" + "# Re-enter a virtual environment:\n" + "workon ENV_NAME\n\n" + "# List all virtual environments:\n" + "workon\n\n" + "# Delete a virtual environment:\n" + "rmvirtualenv ENV_NAME" + ), + ) + self.add_slide( + "

Determining if you are in a virtual env

" + "

" + "

When activated, the name of your virtual env" + " will appear in parentheses on the left side of your" + " command prompt.

", + code=( + "# Example of how it may look on a Windows machine:\n" + "C:\\Users\\Michael\\github> mkvirtualenv my_env\n" + "(my_env) C:\\Users\\Michael\\github>" + ), + ) + self.add_slide( + '

Installing packages with "pip install"



' + "

Once you have created a Python virtual environment and are" + " inside, it is now safe to install packages from PyPI," + " setup.py files, and/or requirements.txt files.

\n", + code=( + "# Install a package from PyPI:\n" + "pip install seleniumbase\n\n" + "# Install packages from a folder with setup.py:\n" + "pip install . # Normal installation\n" + "pip install -e . # Editable install\n\n" + "# Install packages from a requirements.txt file:\n" + "pip install -r requirements.txt\n" + ), + ) + self.add_slide( + '

Other useful "pip" commands



', + code=( + "# See which Python packages are installed:\n" + "pip list\n\n" + "# See which installed Python packages are outdated:\n" + "pip list --outdated\n\n" + "# Create a requirements file from installed packages:\n" + "pip freeze > my_requirements.txt" + ), + ) + self.add_slide( + "

Live Demo Time!



", + image="https://seleniumbase.io/other/python_3d_logo.png", + ) + self.add_slide( + "

The End. Questions?



\n" + "

Where to find me:

" + "" + ) + self.begin_presentation( + filename="py_virtual_envs.html", show_notes=False, interval=0 + ) diff --git a/examples/presenter/ua_presentation_5.py b/examples/presenter/ua_presentation_5.py new file mode 100644 index 00000000000..8d9032314a3 --- /dev/null +++ b/examples/presenter/ua_presentation_5.py @@ -0,0 +1,1093 @@ +# https://www.youtube.com/watch?v=R9HNsnbYh8o +# https://seleniumbase.io/other/ua_presentation_5.html +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class UA_Presentation_Class(BaseCase): + def test_presentation_5(self): + self.goto("data:,") + self.set_window_position(4, 40) + self._output_file_saves = False + self.create_presentation(theme="serif", transition="none") + self.add_slide( + "

Undetectable Automation:
5th Edition

\n" + "


" + "

Press SPACE to continue!

\n" + ) + self.add_slide( + '' + ) + self.add_slide( + "

Now on YouTube!

\n" + "
" + '

' + 'https://youtube.com/watch?v=R9HNsnbYh8o

\n' + '' + '' + ) + self.add_slide( + '' + '' + ) + self.add_slide( + "

This continues the series that started here...

" + '' + ) + self.add_slide( + "

๐Ÿ”น We're going to be using... ๐Ÿ”น


" + '' + ) + self.add_slide( + "

๐Ÿ”น To bypass all the CAPTCHAs... ๐Ÿ”น

" + "

" + '' + ) + self.add_slide( + "

๐Ÿ”น In all the places... ๐Ÿ”น



" + "โœ… Local Desktop Machine - (Eg. Mac/Windows)" + "

" + "โœ… Docker Container - (Eg. ubuntu:24.04)" + "

" + "โœ… Linux Server - (Eg. GitHub Actions)

" + ) + self.add_slide( + "

๐Ÿ”น With live demos on major sites... ๐Ÿ”น" + "


" + "โœ… TikTok

" + "โœ… Facebook

" + "โœ… Amazon

" + "โœ… LinkedIn

" + "โœ… And many more!

" + ) + self.add_slide( + '' + ) + self.add_slide( + "
Let's warm up with a few live demos
" + "of bypassing Cloudflare's Turnstile...


" + '
' + '' + ) + self.add_slide( + "With the tools I'll be covering in this video,
" + "you be able to web-scrape sites with confidence.
" + "

" + '' + '' + ) + self.add_slide( + "

This continues my Undetectable Automation series:

" + '' + '' + ) + self.add_slide( + "

That all started with...

" + '' + ) + self.add_slide( + "
The Automation Entertainment Industry was born.
" + '' + ) + self.add_slide( + "

The field changes rapidly, so it's important to
" + "note that the first three videos are out-of-date.

" + '' + ) + self.add_slide( + "The first three used a modified Chromedriver
" + 'called "Undetected Chromedriver" for stealth.
' + '' + ) + self.add_slide( + "At some point, modifying Chromedriver
wasn't enough to make " + "WebDriver actions
stealthy because anti-bot services " + "improved.
" + '' + ) + self.add_slide( + "That led to the rise of CDP in order to bypass CAPTCHAs " + " and bot-detection services again.
" + '' + '
' + "
(This was the focus the 4th Undetectable Automation " + "video.)
" + ) + self.add_slide( + "Lots of changes have happened since the last
" + "video in the Undetectable Automation series:
" + '' + '' + ) + self.add_slide( + "

Here are some advancements since then:" + "



" + "
    \n" + 'โœ… Stealthy Playwright Mode.' + "
    \n" + 'โœ… Stealthy Docker automation.' + "
    \n" + 'โœ… Stealthy headless automation.' + "
    \n" + 'โœ… Stealthy mobile emulation.' + "
    \n" + 'โœ… Imperva-based hCaptcha can be bypassed.' + "
    \n" + 'โœ… Friendly Captcha can be bypassed.' + "
    \n" + 'โœ… DataDome Slider can be bypassed.' + "
    \n" + 'โœ… Unbranded Chromium support.' + "
    \n" + 'โœ… Pure CDP Mode.' + "
    \n" + "\n


\n" + ) + self.add_slide( + '' + '' + "
" + ) + self.add_slide( + '' + ) + self.add_slide( + "

Compared to standard JS, CDP actions are stealthy!

" + '' + ) + self.add_slide( + "

๐Ÿ”น CDP alone isn't enough for stealth... ๐Ÿ”น" + "


" + "โœ… The web browser's fingerprint must look like
" + "one coming from a human-controlled browser.
" + "

" + '' + ) + self.add_slide( + "
Browsers launched with regular Playwright or WebDriver" + "
have a poisoned fingerprint that results in bot-detection." + "

" + '' + ) + self.add_slide( + "
" + "To avoid this issue, you can launch the system's default " + "
browser... and THEN attach the automation framework!" + "

" + '' + ) + self.add_slide( + "

Some special configuration is needed" + "
to make this work in a stealthy way.

" + '' + ) + self.add_slide( + "

And you may need special methods" + "
for handling CAPTCHAs successfully

" + '' + ) + self.add_slide( + "

โคต๏ธ (That brings us to SeleniumBase..." + "
a framework that takes care of" + "
a lot of that work for you...)

" + ) + self.add_slide( + '' + '' + ) + self.add_slide( + '' + '' + ) + self.add_slide( + "Contrary to logical thinking,
" + "SeleniumBase no longer uses Selenium
" + "for some things (such as CDP Mode).
" + "
" + '

' + "
If that sounds confusing, " + "note that JavaScript does not use Java.
" + ) + self.add_slide( + '' + ) + self.add_slide( + "

SeleniumBase CDP Mode comes in 3 formats:" + "



" + "
    \n" + 'โœ… SB() "nested sync" format.' + "

    \n" + 'โœ… sb_cdp "sync" format. (Pure CDP)' + "

    \n" + 'โœ… cdp_driver "async" format. (Pure CDP)' + "

    \n" + '"Stealthy Playwright Mode" can be' + "
    initiated from any of these formats.
    " + "\n


\n" + ) + self.add_slide( + '' + ) + self.add_slide( + '

sb_cdp "sync" Stealthly Playwright Mode ' + 'example:

' + "
", + code=( + "from playwright.sync_api import sync_playwright" + "\n" + "from seleniumbase import sb_cdp\n\n" + 'sb = sb_cdp.Chrome()\n' + "endpoint_url = sb.get_endpoint_url()\n\n" + "with sync_playwright() as p:\n" + " browser = p.chromium.connect_over_cdp(endpoint_url)" + "\n" + " page = browser.contexts[0].pages[0]\n" + ' page.goto("https://copilot.microsoft.com")\n' + ' page.wait_for_selector("textarea#userInput")' + '\n' + ' query = "Playwright Python connect_over_cdp() ' + 'sync example"\n' + ' page.fill("textarea#userInput", query)\n' + ' page.wait_for_timeout(2000)\n' + ' page.click(\'button[' + 'data-testid="submit-button"]\')\n' + ' sb.sleep(5.25)\n' + ' sb.solve_captcha()\n' + " ...\n" + ), + ) + self.add_slide( + "
" + "It's time for that live demo where we use Stealthy
" + "Playwright Mode to bypass Copilot's CAPTCHA...
" + "

" + '' + ) + self.add_slide( + "
" + "Are you ready for another live demo?

" + "Let's do some Walmart scraping now...
" + ) + self.add_slide( + "
" + "Let's do another live demo
before returning to the learning." + "

" + "This time, we're web-scraping TikTok...

" + ) + self.add_slide( + "In addition to using CDP for controlling Chrome in a" + " stealthy way, you can also achieve stealth by using" + " tools that can control the mouse and keyboard." + "

PyAutoGUI is one such tool:" + '' + ) + self.add_slide( + "PyAutoGUI requires a headed browser to work." + "

" + " Since most Linux machines have headless displays that" + " don't support headed browsers, an external tool called" + " Xvfb must be used in order to simulate a headed browser" + " in a headless Linux environment..." + ) + self.add_slide( + '' + ) + self.add_slide( + "
Xvfb is automatically used when needed in SeleniumBase." + "
This makes stealthy automation easy from Linux servers." + "
" + '' + ) + self.add_slide( + "

Here's another GitHub Actions example:

" + '' + ) + self.add_slide( + "

I have a full YouTube tutorial on that:

" + '' + '' + ) + self.add_slide( + "

Same story with stealthy Docker automation:

" + '' + '' + ) + self.add_slide( + "

Note that Docker requires extra config to be stealthy!" + "

" + "These changes can be made from the Dockerfile." + "

" + '' + ) + self.add_slide( + "

For instance, some standard fonts are not
" + "installed from the default Docker config.

" + '' + ) + self.add_slide( + "

This is a problem because websites can see
" + "which fonts are installed on your system.

" + '' + ) + self.add_slide( + "

If your system is missing standard fonts,
" + "then websites know you're running from a server,
" + "and therefore they know you're using automation.

" + '' + ) + self.add_slide( + "

The SeleniumBase Dockerfile" + " has you covered:

" + '' + '' + ) + self.add_slide( + "

It would be a big mistake to think that you can
" + "be stealthy in a regular Docker container without
" + "changing the default fonts and configuration...

" + '' + ) + self.add_slide( + "

In the spirit of online shopping,
" + "the next demo is on Nordstrom...

" + ) + self.add_slide( + "

And now for some Ralph Lauren...

" + ) + self.add_slide( + "

Let's wrap up this quick" + "
shopping session on Kohls...

" + ) + self.add_slide( + "
Now that we have the ingredients for stealth..." + "

" + "
    \n" + 'โœ… An automation framework that uses' + '
    a browser with a natural fingerprint.
    ' + "

    \n" + "โœ… CDP methods for performing natural actions" + '
    (eg. Input.dispatchMouseEvent).' + "


    \n" + "โœ… PyAutoGUI for performing trickier actions" + "
    (eg. drag-and-drop with a real mouse).
    " + "

    \n" + "โœ… Xvfb integration for using headed browsers" + "
    on Linux systems that don't have a GUI.
    " + "\n


\n" + ) + self.add_slide( + "We can use all that to bypass different CAPTCHAs!" + "

" + "Get ready for some live demos of that!" + "

" + '' + ) + self.add_slide( + "


๐Ÿ”น One method call does it all: ๐Ÿ”น" + "




" + "
    \n" + '

    โœ… sb.solve_captcha()

    ' + "

    \n" + "\n


\n" + ) + self.add_slide( + "

sb.solve_captcha() handles all of these:" + "



" + "
    \n" + 'โœ… Cloudflare Turnstile' + "

    \n" + "โœ… Friendly Captcha" + "

    \n" + "โœ… DataDome Slider Captcha" + "

    \n" + "โœ… Imperva-based hCaptcha" + "\n


\n" + ) + self.add_slide( + "It may seem a bit odd or illegal that
" + "we're bypassing all these CAPTCHAs...
" + "

" + "but things may not actually be as they seem..." + '' + ) + self.add_slide( + "

Important Notice:

" + "(Know the laws and legal implications of scraping!)" + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + "Now that we all know scraping public data is legal," + "

" + "let's go web-scraping on Facebook's public pages..." + ) + self.add_slide( + "If a site wants to hide its public data from scrapers," + "

" + "then it'll need to hide that data from public view..." + ) + self.add_slide( + "Note that if automation performs actions too quickly,
" + "websites may detect this as bot-traffic and block you...
" + "

" + "To avoid this:

" + "โœ… Space out your actions so that the" + "
automation moves at human-like speed.
" + "

" + "โœ… Random sleep() calls can help." + ) + self.add_slide( + "

This may happen when some anti-bots detect you:" + '

' + ) + self.add_slide( + "

If that happens, your bot might not get a chance
" + "to solve a CAPTCHA to prove its humanity.

" + '' + "


In other cases, your bot may face a challenge..." + "

" + ) + self.add_slide( + "

This is what happens if hCAPTCHA detects bots:" + "

" + '' + ) + self.add_slide( + "

This is what happens if reCAPTCHA detects bots:" + "

" + '' + ) + self.add_slide( + "

And this is what happens when Gandalf blocks you:" + "

" + '' + '' + ) + self.add_slide( + "

Get ready for a web-scraping
" + "live demo on Amazon.com ...

" + ) + self.add_slide( + "

Let's take a look at that Amazon-scraping script:" + "

" + "
", + code=( + "from seleniumbase import SB" + "\n\n" + "with SB(uc=True, test=True, ad_block=True) as sb:" + "\n" + " sb.activate_cdp_mode()\n" + ' sb.goto("https://www.amazon.com")\n' + " sb.sleep(2)\n" + " sb.click_if_visible('button" + "[alt=\"Continue shopping\"]')" + "\n" + " sb.sleep(2)\n" + ' sb.press_keys(\'input' + '[role="searchbox"]\', "TI-89\\n")\n' + ' sb.sleep(3)' + '\n' + ' for i in range(16):\n' + ' sb.scroll_down(16)\n' + ' print(sb.get_page_title())\n' + ' sb.save_as_pdf_to_logs()\n' + ' sb.save_page_source_to_logs()\n' + ' sb.save_screenshot_to_logs()\n' + ' print("Logs have been saved to: ' + './latest_logs/")\n' + ), + ) + self.add_slide( + "

Let's run two live demos on Nike.com:" + "



" + "
    \n" + '1. Using Regular Chrome.' + "

    \n" + '2. Using Headless Chrome.' + "\n


\n" + ) + self.add_slide( + "

Let's take a look at that Nike-scraping script:" + "

" + "
", + code=( + "from seleniumbase import SB" + "\n\n" + '' + 'with SB(uc=True, test=True, locale="en", pls="none") as sb:' + "\n" + ' sb.activate_cdp_mode()\n' + ' sb.goto("https://www.nike.com/")\n' + " sb.sleep(2.5)\n" + " " + 'sb.click(\'[data-testid="user-tools-container"] search\')' + "\n" + " sb.sleep(1.5)\n" + ' search = "Nike Air Force 1"\n' + ' sb.press_keys(\'input' + '[type="search"]\', search)\n' + ' sb.sleep(4)' + '\n' + ' details = \'ul[data-testid*="products"] ' + 'figure .details\'\n' + ' elements = sb.select_all(details)\n' + ' if elements:\n' + ' print(\'**** Found results for ' + '"%s": ****\' %% search)\n' + ' for element in elements:\n' + ' print("* " + element.text)\n' + ), + ) + self.add_slide( + "

Here's a friendly reminder that
" + "those sb.sleep() calls are strategic.

" + "Automating too quickly can get you into
" + "trouble on sites with bot-protection...
" + '' + ) + self.add_slide( + "

" + "Sending too much traffic to a website from the same
" + "IP address could also stop your automation runners.

" + '' + ) + self.add_slide( + "

And some IP ranges are flagged " + "by anti-bot services.


" + 'โ›” Eg. "Non-residential" IP ranges (such as AWS)' + "

" + '' + ) + self.add_slide( + "Scraping a site from a non-residential
" + "IP range is a sure way to get caught...
" + ) + self.add_slide( + "

This would not be a real video about
" + "web-scraping unless I mentioned proxies.

" + "SeleniumBase has a proxy option for that." + "" + '' + ) + self.add_slide( + "

" + "Here's how to configure a proxy with SeleniumBase:" + "



    \n" + '
  • Proxy via command-line:' + '
    pytest --proxy="host:port"' + '
    pytest --proxy="user:pass@host:port"' + '' + '

  • \n' + '
  • Proxy via method arg:' + '
    SB(proxy="host:port")' + '
    SB(proxy="user:pass@host:port")' + '
\n' + ) + self.add_slide( + "
If you don't have a proxy, it's easy to get one.
" + "Lots of providers out there, like Bright Data.
" + "(Bright Data blogs about SeleniumBase a lot.)
" + "
" + '' + '' + ) + self.add_slide( + "

You could also launch your own
" + "proxy server with SeleniumBase:

" + '

"sbase proxy"

' + "

(That's it!)

" + '' + ) + self.add_slide( + '

More configuration options for "sbase proxy":' + '

' + '' + ) + self.add_slide( + "

Since websites can detect your geolocation,
" + "it may be a good idea to change it at times.

" + "Fortunately, changing your geolocation/timezone
" + "can be done faster than changing your hair color.
" + '' + ) + self.add_slide( + "

" + "๐ŸŒ Here's how to configure those with SeleniumBase:" + "



    \n" + '
  • Changing geolocation via method arg:' + '
    geoloc=(26.863, 80.94)' + '
    geoloc=(35.050681, 136.844728)' + '
    ' + '

  • \n' + '
  • Changing timezone via method arg:' + '
    tzone="Asia/Kolkata"' + '
    tzone="Asia/Tokyo"
    ' + '
\n' + ) + self.add_slide( + "

๐ŸŒ Timezone/Geolocation ๐ŸŒ


" + "

Let's run a live demo to show how that works:" + "



" + "
    \n" + "1. First we'll go to India." + "

    \n" + "2. Then we'll go to Japan." + "\n


\n" + ) + self.add_slide( + "

Another cool feature of CDP is the ability
" + "to intercept & modify requests in real time.

" + '' + ) + self.add_slide( + '' + '' + ) + self.add_slide( + '' + '' + ) + self.add_slide( + '' + '' + ) + self.add_slide( + "

Time for a live demo of that...

" + "
(Intercepting/modifying requests)" + ) + self.add_slide( + '' + '' + ) + self.add_slide( + "As you can see, SeleniumBase has all
" + "the CDP features you're looking for.

" + '' + '' + ) + self.add_slide( + "If you're looking for powerful multi-threading,
" + "then ThreadPoolExecutor has you covered.
" + '' + '' + ) + self.add_slide( + "

Sometimes you can defeat CAPTCHAs in advance
" + "if you already have the required cookies loaded
" + "in your web browser. (Eg: cf_clearance)
" + "

" + '' + ) + self.add_slide( + "

Getting the cf_clearance cookie is easy:" + "

Just go to a CF-protected site and take it!" + "" + '' + '' + ) + self.add_slide( + "

" + "Get ready for a live demo of
" + "stealthy mobile emulation...

" + ) + self.add_slide( + "

For more info on stealthy mobile emulation,
" + " check out the YouTube video on it:" + "

" + '' + '' + ) + self.add_slide( + "

There's also a follow-up mobile video
" + " that deals with changing the User Agent:" + "

" + '' + '' + ) + self.add_slide( + "

" + "SeleniumBase has an option for using unbranded
" + "Chromium in place of regular Google Chrome.
" + "



\n" + '' + ) + self.add_slide( + "

" + "Here's how to use unbranded Chromium in scripts:" + "



    \n" + '
  • Chromium via command-line:' + '
    pytest --use-chromium
    ' + '' + '

  • \n' + '
  • Chromium via method arg:' + '
    SB(use_chromium=True)
    ' + '


\n' + "(Chromium is automatically downloaded
" + "if it hasn't yet been used by SeleniumBase)
" + ) + self.add_slide( + "

" + "SeleniumBase can download Chromium in advance:" + "



\n" + '

โœ… sbase get chromium

' + '' + ) + self.add_slide( + "" + "Unlike Google Chrome, unbranded Chromium still
" + "supports loading extensions via command flags.
" + "
\n" + '' + ) + self.add_slide( + "" + "Additionally, unbranded Chromium may be stealthier
" + "than regular Google Chrome on certain websites.
" + "


\n" + "

Get ready for a web-scraping
" + "live demo on Reddit.com ...

" + "

" + ) + self.add_slide( + '' + '' + ) + self.add_slide( + "reCAPTCHA comes in many flavors:

" + '' + "

Some flavors are stronger than others." + ) + self.add_slide( + "

๐Ÿ” Google reCAPTCHA ๐Ÿ”


" + '' + '' + "

" + ) + self.add_slide( + "

๐Ÿ” Google reCAPTCHA ๐Ÿ”


" + '

' + "

reCAPTCHA is very effective at catching bots.

" + "




" + ) + self.add_slide( + "

๐Ÿ” Google reCAPTCHA ๐Ÿ”


" + '' + "

" + "

Even though Google reCAPTCHA looks similar
" + "to Cloudflare Turnstile, they are very different!

" + '' + "

" + ) + self.add_slide( + "" + "This may explain why Google reCAPTCHA is better:" + "

" + "

Since Google makes both reCAPTCHA and Chrome," + " reCAPTCHA has access to more data than other CAPTCHAs," + " and can therefore detect bots better." + "



" + ) + self.add_slide( + "Many websites have an out-of-date reCAPTCHA:" + "
" + '' + "

" + "(Sites like these are vulnerable to web-scrapers)" + "
" + ) + self.add_slide( + "

Get ready for a web-scraping
" + "live demo on Pokemon.com ...

" + ) + self.add_slide( + "

๐Ÿ” Google reCAPTCHA ๐Ÿ”



" + "In the case of encountering an Enterprise V3
" + "reCAPTCHA... that's a very different story...
" + "


" + '' + "

" + ) + self.add_slide( + "

" + "Note: Calling goto(url) from UC Mode
" + "automatically activates CDP Mode now.
" + "


    \n" + "This change was made because the original
    " + "UC Mode wasn't stealthy enough anymore.


    " + "This makes older UC Mode scripts work again.

    " + "
    " + "That's especially important since out-of-date
    " + "Undetectable Automation videos still get views.

    " + "
    " + "sb.driver.get(url) keeps you in UC Mode." + "
" + ) + self.add_slide( + '' + ) + self.add_slide( + "

" + "Naming conventions used in the SeleniumBase repo:" + "



\n" + "
    " + 'โœ… If a file starts with "test_" or ends with' + '
    "_test.py", ' + 'then it runs with "pytest".
    ' + "

    " + 'โœ… If a file starts with "raw_",
    ' + 'then it runs directly with raw "python".
    ' + "

    " + 'โœ… (Note that SeleniumBase was originally just a test ' + 'framework before it gained stealth abilities.)' + "
" + ) + self.add_slide( + "

" + "Finding stealthy examples in the SeleniumBase repo:" + "



\n" + 'โœ… See "SeleniumBase/examples/cdp_mode/"
' + 'for all examples made for stealth & CAPTCHAs.


' + 'โœ… For the Stealthy Playwright Mode examples, see:
' + '"SeleniumBase/examples/cdp_mode/playwright/"
' + ) + self.add_slide( + "

" + "Let's make a quick trip to the SeleniumBase repo:" + "


\n" + '' + '' + ) + self.add_slide( + "" + "It's time for more CAPTCHA-bypass!" + "

\n" + "Get ready for a live demo of
" + "bypassing a Cloudflare CAPTCHA
" + "on Cloudflare's own login page ..." + "
" + "

" + '' + ) + self.add_slide( + "
" + "If bypassing CAPTCHAs isn't exciting enough for you,
" + "then treat every CAPTCHA-bypass like scoring a goal...
" + "
" + '' + ) + self.add_slide( + "

๐Ÿค– You've reached the AI section of this video! ๐Ÿค–" + "



" + '' + ) + self.add_slide( + "

๐Ÿค– I've actually been using a lot of AI already! ๐Ÿค–" + "



" + "โœ… Many illustrations were AI-generated.
" + '' + ) + self.add_slide( + "

๐Ÿค– Thanks for all the free artwork, Gemini! ๐Ÿค–" + "


" + '' + ) + self.add_slide( + "

Let's use Gemini's power to bypass CAPTCHAs!" + "



" + '' + ) + self.add_slide( + "

Every year the AI models get better and better" + "



" + '' + ) + self.add_slide( + "

Definitely an improvement over last year's models" + "


" + '' + ) + self.add_slide( + "

I also covered AI in my earlier video:" + "

" + 'Can AI tools help you with web-scraping & CAPTCHA-bypass?' + '
' + "


" + '' + '
' + "
" + '(Spoiler alert: It depends on which AI you ask!)' + '
' + ) + self.add_slide( + "

It's time for the next live demo!

" + "


" + "

For that, we're scraping LinkedIn...

" + '' + ) + self.add_slide( + "

As you can see ...

" + "

" + "

SeleniumBase works well for web-scraping." + "

" + '' + '' + ) + self.add_slide( + "

" + "If you need help creating scripts,
" + "just ask the AI! (maybe Gemini)
" + "


\n" + '
' + 'โœ… Or you can replicate a bunch of
' + 'AI agents to do the work for you...

' + ) + self.add_slide( + "

โ“ Questions? โ“

" + "https://github.com/seleniumbase/SeleniumBase/discussions" + "

" + "

๐Ÿ“Œ Found a bug? ๐Ÿž

" + "https://github.com/seleniumbase/SeleniumBase/issues" + "
" + ) + self.add_slide( + "

๐Ÿ“Š Final remarks ๐Ÿ“ฃ



" + "

๐Ÿ› ๏ธ SeleniumBase gives you ๐Ÿ› ๏ธ
" + "the tools you need to succeed!" + "


" + "And tools to build lots of bots..." + "


" + ) + self.add_slide( + "
๐Ÿ The End ๐Ÿ
" + '' + '' + ) + self.begin_presentation(filename="ua_presentation.html") diff --git a/examples/presenter/uc_presentation.py b/examples/presenter/uc_presentation.py new file mode 100644 index 00000000000..fa0793384e5 --- /dev/null +++ b/examples/presenter/uc_presentation.py @@ -0,0 +1,560 @@ +# https://www.youtube.com/watch?v=5dMFI3e85ig +import os +import subprocess +from contextlib import suppress +from seleniumbase import BaseCase +from seleniumbase import SB +BaseCase.main(__name__, __file__) + + +class UCPresentationClass(BaseCase): + def test_presentation(self): + self.goto("data:,") + self._output_file_saves = False + self.create_presentation(theme="beige", transition="fade") + self.add_slide( + "

A deep dive into undetectable automation, with:

" + "
SeleniumBase UC Mode" + " and undetected-chromedriver
", + image="https://seleniumbase.io/cdn/img/uc_mode_phases_3.png", + ) + self.add_slide( + '' + ) + self.add_slide( + "

๐Ÿ”น The Objective ๐Ÿ”น



" + "

By the end of this presentation, you'll learn how to" + " create bots that appear as humans to websites.


" + "

(These bots won't get detected or blocked.)

" + "

Here's a live demo of that...

" + ) + self.begin_presentation(filename="uc_presentation.html") + + self.get_new_driver(undetectable=True) + url = "https://gitlab.com/users/sign_in" + try: + self.uc_open_with_reconnect(url, reconnect_time=3) + try: + self.assert_text("Username", '[for="user_login"]', timeout=3) + self.post_message("SeleniumBase wasn't detected", duration=4) + except Exception: + self.uc_open_with_reconnect(url, reconnect_time=4) + self.assert_text("Username", '[for="user_login"]', timeout=3) + self.post_message("SeleniumBase wasn't detected", duration=4) + finally: + self.quit_extra_driver() + + if os.path.exists("multi_uc.py"): + self.create_presentation(theme="beige", transition="fade") + self.add_slide( + "

๐Ÿ”น There's a lot more to come! ๐Ÿ”น



" + "

If one bot isn't enough, how about several?" + "



" + "

Here's a demo of multithreaded bots in parallel..." + "

" + ) + self.begin_presentation(filename="uc_presentation.html") + subprocess.Popen("pytest multi_uc.py --uc -n3", shell=True).wait() + self.create_presentation(theme="serif", transition="fade") + self.add_slide( + "

Not just an army of bots, but an army of bots
" + "that look just like humans using web browsers.


" + "

(That's how they weren't detected!)

" + ) + self.begin_presentation(filename="uc_presentation.html") + + self.create_presentation(theme="serif", transition="fade") + self.add_slide( + "If this is what you came here for, stick around
to" + " learn how to do the things you just saw.
" + "

You may find it easier to build a Selenium
" + "bot than to navigate an obstacle course.

", + image="https://seleniumbase.io/other/yeah_you_passed.png", + ) + self.add_slide( + "

But first, you may be wondering who I am...

" + "

Or maybe you're one of over one million people
" + "that I've already helped on Stack Overview:

" + '' + ) + self.add_slide( + "

About the presenter (Michael Mintz):

\n" + "
    \n" + "
  • I created SeleniumBase (for Python).
  • \n" + "
  • I lead the Automation Team at iboss.
  • \n" + "
\n", + image="https://seleniumbase.io/other/iboss_booth.png", + ) + self.add_slide( + "

I've been doing video podcasts since 2012!

" + "

(That's when I first co-hosted the
Marketing Update" + " on HubSpot TV)

", + image="https://seleniumbase.io/other/hub_tv.png", + ) + self.add_slide( + "

I spoke at Selenium Conference 2023:

" + "

(As the dedicated Python Selenium speaker)

", + image="https://seleniumbase.io/other/me_se_conf.jpg", + ) + self.add_slide( + "

Here's me with the creators
" + "of Selenium / WebDriver:

", + image="https://seleniumbase.io/other/selenium_builders.jpg", + ) + self.add_slide( + "SeleniumBase Fun Fact:" + "
The 1st SB GitHub issue was from a Tesla engineer:", + image="https://seleniumbase.io/other/first_issue.png", + ) + self.add_slide( + "

Now, let me explain how we got here...

" + "

And by here, I mean a time when lots of companies" + " have been building services to detect and block bots:

", + image="https://seleniumbase.io/other/verify_human.png", + ) + self.add_slide( + "

In the early days, there were few bots,
" + "and those bots didn't look human at all.


" + "

Those early bots were mostly innocent, and
" + "most websites didn't care if they were around.

" + ) + self.add_slide( + "

At some point, the number of bots grew by a lot...


" + "

Many bots were programmed with bad intentions...


" + "

And sometimes you couldn't tell apart the good bots" + " from the bad ones until it was already too late...

", + ) + self.add_slide( + "

Then came Google reCAPTCHA v1...

" + "

Although intended as a defense against bots,
" + " humans on a web browser also got hit by it.

", + image="https://seleniumbase.io/other/recaptcha_v1.png", + ) + self.add_slide( + "

Then Google made improvements:

" + "

(reCAPTCHA v2 / reCAPTCHA v3)

", + image="https://seleniumbase.io/other/recaptcha_v2a.png", + ) + self.add_slide( + "

And that annoyed a lot of people...

", + image="https://seleniumbase.io/other/recaptcha_v2b.png", + ) + self.add_slide( + "

Then came the next iteration of anti-bot security:

" + "

Cloudflare Turnstile CAPTCHA-replacement." + "

That changed the game in many ways.

", + image="https://seleniumbase.io/other/check_if_secure.png", + ) + self.add_slide( + "For awhile, Cloudflare's Turnstile did a decent
" + "job filtering out bot traffic from human traffic." + "

Then undetected-chromedriver arrived:

", + image="https://seleniumbase.io/other/undetected_ch.png", + ) + self.add_slide( + '' + ) + self.add_slide( + "

undetected-chromedriver was found to be
" + "incredibly effective at getting past the Turnstile:

", + image="https://seleniumbase.io/other/connection_secure.png", + ) + self.add_slide( + "

The maintainer of undetected-chromedriver:

" + "

He appears to be very busy with various projects.

" + '' + ) + self.add_slide( + "

The biggest challenge for undetected-chromedriver" + " has been adapting to breaking changes caused by:

" + "
    \n" + "
  • New versions of Chrome.
  • \n" + "
  • New versions of Selenium.
  • \n" + "
  • New versions of Cloudflare.
  • \n" + "


\n" + "

Those can brake things until updates are released:

" + '' + ) + self.add_slide( + "

Thankfully, undetected-chromedriver has
" + "supporters helping to figure out and fix things.

" + '' + ) + self.add_slide( + "

Sometimes all that help isn't enough...

" + "

That's where seleniumbase UC Mode comes in:

" + '' + ) + self.add_slide( + "

SeleniumBase UC Mode is a modified fork of
" + "undetected-chromedriver with multiple changes.

" + "

UC Mode includes bug fixes and additional features, such as" + " multithreading support via pytest-xdist.

" + '' + ) + self.add_slide( + "

UC Mode is one of many SeleniumBase modes:" + "

\n
    \n" + "
  • UC Mode (--uc / uc=True)
  • \n" + "
  • Slow Mode (--slow)
  • \n" + "
  • Demo Mode (--demo)
  • \n" + '
  • Proxy Mode' + ' (--proxy="h:p"/"u:p@h:p")
  • \n' + "
  • Debug Mode " + "(--pdb/--trace/--ftrace)
  • \n" + "
  • Mobile Mode " + "(--mobile / mobile=True)
  • \n" + "
  • Recorder Mode " + "(--rec / --rec-behave)
  • \n" + "
  • Multithreaded Mode " + "(pytest -n4 / -n8)
  • \n" + "
\n" + "

And more! (You can even combine modes!)

\n" + ) + self.add_slide( + "

โ„น๏ธ: UC Mode is not enabled by default.
" + " It must be activated by switching it on:

" + "
    \n" + "
  • --uc  " + " (pytest command-line option)
  • \n" + "
  • uc=True  " + " (SB/driver manager formats)
  • \n" + "




\n

" + "Then websites can no longer detect chromedriver.

" + ) + self.add_slide( + "

Here's an example script that uses UC Mode:

\n" + "
(Note that SeleniumBase has driver methods" + "
that aren't included with standard Selenium.)
\n" + "

\n", + code=( + "from seleniumbase import Driver\n\n" + "driver = Driver(uc=True)\n" + "try:\n" + ' driver.get("https://gitlab.com/users/sign_in")' + '\n' + " driver.sleep(4)\n" + " # DO MORE STUFF\n" + "finally:\n" + " driver.quit()\n" + ), + ) + self.add_slide( + "

For reference, here's a script in a different
" + "SeleniumBase format, with more methods:

" + "
(Get ready for another live demo...)
", + code=( + "from seleniumbase import SB\n\n" + "with SB(uc=True) as sb:\n" + ' sb.get("seleniumbase.io/simple/login")\n' + ' sb.type("#username", "demo_user")\n' + ' sb.type("#password", "secret_pass")\n' + ' sb.click(\'a:contains("Sign in")\')\n' + ' sb.assert_exact_text("Welcome!", "h1")\n' + ' sb.assert_element("img#image1")\n' + ' sb.highlight("#image1")\n' + ' sb.click_link("Sign out")\n' + ' sb.assert_text("signed out", "#top_message")' + "\n" + ), + ) + self.begin_presentation(filename="uc_presentation.html") + + with suppress(Exception): + with SB(uc=True) as sb: + sb.get("https://seleniumbase.io/simple/login") + sb.type("#username", "demo_user") + sb.type("#password", "secret_pass") + sb.click('a:contains("Sign in")') + sb.assert_exact_text("Welcome!", "h1") + sb.assert_element("img#image1") + sb.highlight("#image1") + sb.click_link("Sign out") + sb.assert_text("signed out", "#top_message") + + self.create_presentation(theme="serif", transition="fade") + self.add_slide( + "

Was that too fast for you?

" + "

Let's run that again in Demo Mode:

", + code=( + "from seleniumbase import SB\n\n" + "with SB(uc=True, demo=True) as sb:\n" + ' sb.get(' + '"https://seleniumbase.io/simple/login")\n' + ' sb.type("#username", "demo_user")\n' + ' sb.type("#password", "secret_pass")\n' + ' sb.click(\'a:contains("Sign in")\')\n' + ' sb.assert_exact_text("Welcome!", "h1")\n' + ' sb.assert_element("img#image1")\n' + ' sb.highlight("#image1")\n' + ' sb.click_link("Sign out")\n' + ' sb.assert_text("signed out", "#top_message")\n' + ), + ) + self.begin_presentation(filename="uc_presentation.html") + + with suppress(Exception): + with SB(uc=True, demo=True) as sb: + sb.get("https://seleniumbase.io/simple/login") + sb.type("#username", "demo_user") + sb.type("#password", "secret_pass") + sb.click('a:contains("Sign in")') + sb.assert_exact_text("Welcome!", "h1") + sb.assert_element("img#image1") + sb.highlight("#image1") + sb.click_link("Sign out") + sb.assert_text("signed out", "#top_message") + + self.create_presentation(theme="serif", transition="fade") + self.add_slide( + "

Note the differences between undetected-chromedriver and" + " SeleniumBase UC Mode.


SeleniumBase UC Mode:" + "

    \n" + "
  • Has driver version-detection & management." + "
  • \n" + "
  • Allows mismatched browser/driver versions." + "
  • \n" + "
  • Changes the user agent to prevent detection." + "
  • \n" + "
  • Hides chromedriver from Chrome as needed." + "
  • \n" + "
  • Allows for multithreaded tests in parallel." + "
  • \n" + "
  • Adjusts configuration based on the environment." + "
  • \n" + "
  • Has options for proxy and proxy-with-auth." + "
  • \n
\n" + ) + self.add_slide( + "

Here's another UC Mode script:

" + "

(Get ready for another live demo...)

" + "

", + code=( + "from seleniumbase import SB\n\n" + "with SB(uc=True) as sb:\n" + ' url = "https://gitlab.com/users/sign_in"\n' + " sb.uc_open_with_reconnect(url, 4)\n\n" + " ...\n" + ), + ) + self.begin_presentation(filename="uc_presentation.html") + + with suppress(Exception): + with SB(uc=True) as sb: + url = "https://gitlab.com/users/sign_in" + sb.uc_open_with_reconnect(url, 4) + sb.assert_text("Username", '[for="user_login"]', timeout=3) + sb.assert_element('[for="user_login"]') + sb.highlight('button:contains("Sign in")') + sb.highlight('h1:contains("GitLab")') + sb.post_message("SeleniumBase wasn't detected", duration=4) + + self.create_presentation(theme="serif", transition="fade") + self.add_slide( + "

Now let's learn how UC Mode works in general.

" + "

First, there are several things that need to happen for" + " browsers to remain undetected from anti-bot services.

\n", + image="https://seleniumbase.io/other/yeah_you_passed.png", + ) + self.add_slide( + "Requirements for avoiding detection (UC Mode)
" + "
    \n" + "
  • Modify chromedriver to rename driver variables" + " that appear in the Chrome DevTools console.
  • \n" + "
  • Launch Chrome before attaching chromedriver.
    " + "(Don't launch Chrome with chromedriver)
  • \n" + "
  • Don't use Selenium-specific Chrome options." + "
  • \n
  • If using headless Chrome, change" + " HeadlessChrome to Chrome in the User Agent.
  • \n" + "
  • If using a custom user_data_dir, don't let that" + " folder be used with non-UC-Mode Chrome.
  • \n" + "
  • Disconnect chromedriver briefly from Chrome before" + " loading websites with detection services.
  • \n" + "
" + ) + self.add_slide( + "Requirements, continued... / Good news:
\n" + "

Most of those things are already done automatically" + " when using UC Mode with default settings.

\n" + "

The part that's your responsibility, (if setting a" + " custom user_data_dir), is making sure that" + " the u_d_d is only used by UC Mode Chrome instances. If you" + ' "cross the streams", UC Mode can be detected.

' + "

(UC Mode takes care of the other requirements.)" + "

" + ) + self.add_slide( + "

With those things done, your bot can appear human.

\n" + "
But if anyone looks too closely at what your bot does,
" + 'it may raise suspicion, even if already marked "not a bot".
', + image="https://seleniumbase.io/other/other_anti_bots.jpg", + ) + self.add_slide( + "

There are additional methods that you can use" + " to have a better experience when using UC Mode:

\n" + "

Note that driver.get(url) has" + " been modified from the original to
reconnect automatically" + " if a web page is using bot-detection software.
" + "

", + code=( + "driver.default_get(url)\n\n" + "driver.uc_open(url)\n\n" + "driver.uc_open_with_tab(url)\n\n" + "driver.uc_open_with_reconnect(url, reconnect_time)\n\n" + 'driver.uc_click(selector, by="css selector", timeout=7)\n' + ), + ) + self.add_slide( + "

There are additional methods that you can use" + " to have a better experience when using UC Mode:

" + "

Since driver.get(url) has" + " been modified, driver.default_get(url)" + " exists to do a regular get(url)," + " which may be useful if revisiting a website.
" + "

", + code=( + "driver.default_get(url)\n\n" + "driver.uc_open(url)\n\n" + "driver.uc_open_with_tab(url)\n\n" + "driver.uc_open_with_reconnect(url, reconnect_time)\n\n" + 'driver.uc_click(selector, by="css selector", timeout=7)\n' + ), + ) + self.add_slide( + "

There are additional methods that you can use" + " to have a better experience when using UC Mode:

" + "

driver.uc_open(url) will" + " open a URL in the same tab with a disconnect.
" + "(This might not be enough to bypass detection.)
" + "

", + code=( + "driver.default_get(url)\n\n" + "driver.uc_open(url)\n\n" + "driver.uc_open_with_tab(url)\n\n" + "driver.uc_open_with_reconnect(url, reconnect_time)\n\n" + 'driver.uc_click(selector, by="css selector", timeout=7)\n' + ), + ) + self.add_slide( + "

There are additional methods that you can use" + " to have a better experience when using UC Mode:

\n" + "

driver.uc_open_with_tab(url)" + " opens a URL in a new tab with a disconnect. Similar to the new" + " driver.get(url), but without the pre-check." + "


", + code=( + "driver.default_get(url)\n\n" + "driver.uc_open(url)\n\n" + "driver.uc_open_with_tab(url)\n\n" + "driver.uc_open_with_reconnect(url, reconnect_time)\n\n" + 'driver.uc_click(selector, by="css selector", timeout=7)\n' + ), + ) + self.add_slide( + "

There are additional methods that you can use" + " to have a better experience when using UC Mode:

\n" + "

driver.uc_open_with_tab(url) opens" + " a URL in a new tab with a disconnect. Similar to the new" + " driver.get(url), but without the pre-check." + "


\n" + "
As a reminder, the driver.get(url)" + " pre-check checks to see if a URL has bot-detection software" + " on it before opening the URL in a new tab with a disconnect." + "
\n" + "
This pre-check is done using" + " requests.get(URL)
before opening" + " a URL in the UC Mode web browser.
" + "
If the response code is a" + ' "403" (Forbidden),
' + "then the URL is opened with a disconnect." + "
" + ) + self.add_slide( + "

Customizing the default disconnect/reconnect time

" + "
\n" + "

Here's a method for a custom reconnect time
" + "when opening a page that tries to detect bots:

" + "
driver.uc_open_with_reconnect(url, reconnect_time)"
+            "
" + "
(The default reconnect_time is slightly more than 2 seconds.)" + "


", + code=( + "# Example:\n" + "driver.uc_open_with_reconnect(\n" + ' "https://steamdb.info/login/", reconnect_time=6\n)' + "" + "\n\n" + "# Short form example:\n" + "driver.uc_open_with_reconnect(" + '"https://steamdb.info/login/", 6)\n' + ), + ) + self.add_slide( + "

Clicking with a disconnect/reconnect


\n" + "

If your bot needs to click a button on a website that has" + " anti-bot services, you might be able to do it with this special" + " method, which forces a short disconnect:

\n" + "
driver.uc_click(selector)
" + '
(Defaults: by="css selector", timeout=7)
' + "

\n", + code=( + "# Examples:\n" + 'driver.uc_click("button")\n\n' + 'driver.uc_click("button#id", timeout=10)\n' + ), + ) + self.add_slide( + "

Links to UC Mode code



\n", + ) + self.add_slide( + "

Things to keep in mind


    \n" + "
  • You may need to adjust default settings
    " + "for your bot to remain undetected.

  • \n" + "
  • Once your bot enters a website,
    " + "it should continue to act accordingly.

  • \n" + "
  • Improvise if your bot makes any mistakes.

  • \n" + "
  • Your bot should look human to avoid detection.
\n", + ) + self.add_slide( + "

Ethical concerns


    \n" + "
  • Don't use bots for evil purposes.

  • \n" + "
  • Do use bots with honorable intentions.

  • \n" + "
  • Do use bots for automating tedious manual tasks.

  • \n" + "
  • Do take the time to train & configure your bots.
\n", + ) + self.add_slide( + "

๐Ÿ”น Final remarks ๐Ÿ”น



    \n" + "
  • Not all bots are created equal.

  • \n" + "
  • SeleniumBase UC Mode lets bots appear human.

  • \n" + "
  • Visit SeleniumBase on GitHub for more info:\n" + "https://github.com/seleniumbase/SeleniumBase

\n", + ) + self.add_slide( + "

โ“ Questions? โ“

" + "
https://github.com/seleniumbase/SeleniumBase/discussions
" + "

๐Ÿ“Œ Found a bug? ๐Ÿž

" + "https://github.com/seleniumbase/SeleniumBase/issues

" + "

๐Ÿ”ฐ Perfection takes practice. Keep iterating! ๐Ÿ”ฐ

" + ) + self.add_slide( + "

The End

", + image="https://seleniumbase.io/other/sb_github.png" + ) + self.begin_presentation(filename="uc_presentation.html") diff --git a/examples/presenter/uc_presentation_3.py b/examples/presenter/uc_presentation_3.py new file mode 100644 index 00000000000..a1e051e636d --- /dev/null +++ b/examples/presenter/uc_presentation_3.py @@ -0,0 +1,783 @@ +# https://www.youtube.com/watch?v=-EpZlhGWo9k +import sys +from contextlib import suppress +from seleniumbase import BaseCase +from seleniumbase import SB +BaseCase.main(__name__, __file__) + + +class UCPresentationClass(BaseCase): + def test_presentation_3(self): + self.goto("data:,") + self.set_window_position(4, 40) + self._output_file_saves = False + self.goto("https://seleniumbase.io/other/uc3_title.jpg") + self.create_presentation(theme="serif", transition="fade") + self.add_slide( + '' + ) + self.begin_presentation(filename="uc_presentation.html") + + self.goto("https://seleniumbase.io/other/uc3_title.jpg") + self.sleep(2.5) + + self.create_presentation(theme="serif", transition="fade") + self.add_slide( + '' + '

(with SeleniumBase UC Mode)

' + ) + self.add_slide( + "

This is the follow-up to my previous video:

" + '' + ) + self.add_slide( + '' + ) + self.add_slide( + "

Which was the follow-up to an earlier video:

" + '' + ) + self.add_slide( + '' + ) + self.add_slide( + "
There, you learned the basics of bypassing CAPTCHAs:
" + '' + ) + self.add_slide( + "

Here's a LIVE DEMO of bypassing a CAPTCHA:

" + ) + self.begin_presentation(filename="uc_presentation.html") + + with suppress(Exception): + with SB(uc=True) as sb: + url = "https://gitlab.com/users/sign_in" + sb.uc_open_with_reconnect(url, 4) + sb.assert_text("Username", '[for="user_login"]', timeout=3) + sb.assert_element('[for="user_login"]') + sb.highlight('button:contains("Sign in")') + sb.highlight('h1:contains("GitLab")') + sb.post_message("SeleniumBase wasn't detected", duration=4) + + self.create_presentation(theme="serif", transition="none") + self.add_slide( + "

The code for the previous live demo:

" + "

", + code=( + "from seleniumbase import SB\n\n" + "with SB(uc=True) as sb:\n" + ' url = "https://gitlab.com/users/sign_in"\n' + " sb.uc_open_with_reconnect(url, 4)\n\n" + " ...\n" + ), + ) + self.add_slide( + "

The code for the previous live demo:

" + "

", + code=( + "from seleniumbase import SB\n\n" + "with SB(uc=True) as sb:\n" + ' url = "https://gitlab.com/users/sign_in"\n' + " sb.uc_open_with_reconnect(url, 4)\n\n" + ' sb.assert_text("Username", \'[for="user_login"]\',' + ' timeout=3)\n' + ' sb.assert_element(\'[for="user_login"]\')\n' + ' sb.highlight(\'button:contains("Sign in")\')' + '\n' + ' sb.highlight(\'h1:contains("GitLab")\')' + '\n' + ' sb.post_message("SeleniumBase wasn\'t detected",' + ' duration=4)\n' + ), + ) + self.add_slide( + "

Even if using UC Mode, you may still need to" + " click the CAPTCHA checkbox in order to bypass it.

" + "
" + "

(That's not a problem because there are special
" + "UC Mode methods for handling that situation.)
" + ) + self.add_slide( + "

Special UC Mode methods for clicking CAPTCHAs:" + "


" + "

    \n" + "
  • sb.uc_gui_handle_captcha()" + "
  • \n" + "PyAutoGUI uses the TAB key with SPACEBAR.

    \n\n" + "
  • sb.uc_gui_click_captcha()" + "
  • \n\n" + "PyAutoGUI clicks CAPTCHA with the mouse.
    \n" + "(Note that you'll need to use this one on Linux!)\n" + "
\n\n\n\n" + "


" + ) + self.add_slide( + "

When is clicking the CAPTCHA checkbox required?" + "


 
" + "

  • They've seen your IP Address too many times." + "

  • " + "
  • They don't accept your User-Agent string." + "
    (UC Mode gives you a good one by default)
  • " + "
    " + "
  • You're using Linux. (Likely a server)
  • " + "
    " + "
  • You're using a VPN. (If detected)
  • " + "
    " + ) + self.add_slide( + "

    For testing purposes...


    " + "I'll use a bad User-Agent for some Live Demos..." + "

    " + "This will force me to click the CAPTCHA to bypass it." + "" + ) + self.add_slide( + "

    On the topic of live demos,

    " + "

    I'll run some of them now:

    " + "

    " + "

    Get ready for a live demo of:

    " + "

    Bypassing Cloudflare with TAB + SPACEBAR...

    " + ) + self.begin_presentation(filename="uc_presentation.html") + + agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/126.0.0.0" + if "linux" in sys.platform or "win32" in sys.platform: + agent = None # Use the default UserAgent + + with suppress(Exception): + with SB(uc=True, test=True, agent=agent) as sb: + url = "https://gitlab.com/users/sign_in" + sb.uc_open_with_reconnect(url, 4) + sb.uc_gui_handle_captcha() # Only if needed + sb.assert_element('label[for="user_login"]') + sb.set_messenger_theme(location="bottom_center") + sb.post_message("SeleniumBase wasn't detected!") + + self.create_presentation(theme="serif", transition="none") + self.add_slide( + "

    The code for the previous live demo:

    " + "

    ", + code=( + "from seleniumbase import SB\n\n" + "with SB(uc=True) as sb:\n" + ' url = "https://gitlab.com/users/sign_in"\n' + " sb.uc_open_with_reconnect(url, 4)\n" + " sb.uc_gui_handle_captcha()\n\n" + " ...\n\n\n\n\n\n" + ), + ) + self.add_slide( + "

    The code for the previous live demo:

    " + "

    ", + code=( + "from seleniumbase import SB\n\n" + "with SB(uc=True) as sb:\n" + ' url = "https://gitlab.com/users/sign_in"\n' + " sb.uc_open_with_reconnect(url, 4)\n" + " sb.uc_gui_handle_captcha()\n\n" + ' sb.assert_text("Username", \'[for="user_login"]\',' + ' timeout=3)\n' + ' sb.assert_element(\'[for="user_login"]\')\n' + ' sb.set_messenger_theme(location="bottom_center")' + '\n' + ' sb.post_message("SeleniumBase wasn\'t detected!")' + '' + ), + ) + self.add_slide( + "

    Live demos, continued...

    " + "

    " + "

    Get ready for a live demo of:

    " + "

    Bypassing Cloudflare with a mouse click...

    " + ) + self.begin_presentation(filename="uc_presentation.html") + + with suppress(Exception): + with SB(uc=True, test=True, agent=agent) as sb: + url = "https://gitlab.com/users/sign_in" + sb.uc_open_with_reconnect(url, 4) + sb.uc_gui_click_captcha() # Only if needed + sb.assert_element('label[for="user_login"]') + sb.set_messenger_theme(location="bottom_center") + sb.post_message("SeleniumBase wasn't detected!") + + self.create_presentation(theme="serif", transition="none") + self.add_slide( + "

    The code for the previous live demo:

    " + "

    ", + code=( + "from seleniumbase import SB\n\n" + "with SB(uc=True) as sb:\n" + ' url = "https://gitlab.com/users/sign_in"\n' + " sb.uc_open_with_reconnect(url, 4)\n" + " sb.uc_gui_click_captcha()\n\n" + " ...\n\n\n\n\n" + ), + ) + self.add_slide( + "

    The code for the previous live demo:

    " + "

    ", + code=( + "from seleniumbase import SB\n\n" + "with SB(uc=True) as sb:\n" + ' url = "https://gitlab.com/users/sign_in"\n' + " sb.uc_open_with_reconnect(url, 4)\n" + " sb.uc_gui_click_captcha()\n\n" + ' sb.assert_text("Username", \'[for="user_login"]\',' + ' timeout=3)\n' + ' sb.assert_element(\'[for="user_login"]\')\n' + ' sb.set_messenger_theme(location="bottom_center")' + '\n' + ' sb.post_message("SeleniumBase wasn\'t detected!")' + '\n' + ), + ) + self.add_slide( + "

    Quick recap of what you just learned:" + "


    " + "
      \n" + "
    • Activate UC Mode with SB(uc=True)" + "

    • \n" + "
    • Navigate with stealth by calling " + "sb.uc_open_with_reconnect(url)" + "

    • \n" + "
    • Use sb.uc_gui_handle_captcha()" + " or sb.uc_gui_click_captcha()" + " to bypass CAPTCHAs as needed.
    • \n" + "
    \n" + "


    (It's that easy!)


    \n" + ) + self.add_slide( + "

    Things can get more complicated

    " + "

    " + "
      \n" + "
    • Previous tutorials mentioned this method:
      " + "sb.uc_click(selector)" + "

    • \n" + "Although this method can no longer click a
      " + "CAPTCHA directly, it should be used when
      " + "clicking on something else that causes a
      " + "CAPTCHA to appear after that.
      \n" + "

      " + "
    • Here's a live demo of that...

    • \n" + "
    \n" + "



    \n" + ) + self.begin_presentation(filename="uc_presentation.html") + + with suppress(Exception): + with SB(uc=True, incognito=True, locale="en") as sb: + url = "https://ahrefs.com/website-authority-checker" + input_field = 'input[placeholder="Enter domain"]' + submit_button = 'span:contains("Check Authority")' + sb.uc_open_with_reconnect(url) # The bot-check is later + sb.type(input_field, "github.com/seleniumbase/SeleniumBase") + sb.reconnect(0.1) + sb.uc_click(submit_button, reconnect_time=4) + sb.uc_gui_click_captcha() + sb.wait_for_text_not_visible("Checking", timeout=10) + sb.highlight('p:contains(".com/seleniumbase/SeleniumBase")') + sb.highlight('a:contains("Top 100 backlinks")') + sb.set_messenger_theme(location="bottom_center") + sb.post_message("SeleniumBase wasn't detected!") + + self.create_presentation(theme="serif", transition="none") + self.add_slide( + "

    The code for the previous live demo can be
    " + "found in the SeleniumBase GitHub repo:

    " + "github.com/seleniumbase/SeleniumBase
    " + "



    " + '(See the "examples" folder for all examples)

    \n' + ) + self.add_slide( + "

    Live demos, continued...

    " + "

    " + "

    Get ready for another live demo of" + " using the uc_click(selector) method" + " to bypass a Cloudflare CAPTCHA on" + " steamdb.info ...

    " + ) + self.begin_presentation(filename="uc_presentation.html") + + with suppress(Exception): + with SB(uc=True, test=True, disable_csp=True) as sb: + url = "https://steamdb.info/" + sb.uc_open_with_reconnect(url, 3) + sb.uc_click("a.header-login span", 3) + sb.uc_gui_click_captcha() + sb.assert_text("Sign in", "button#js-sign-in", timeout=3) + sb.uc_click("button#js-sign-in", 2) + sb.highlight("div.page_content form") + sb.highlight('button:contains("Sign in")', scroll=False) + sb.set_messenger_theme(location="top_center") + sb.post_message("SeleniumBase wasn't detected", duration=4) + + self.create_presentation(theme="serif", transition="none") + self.add_slide( + "

    The code for the previous live demo:

    " + "

    ", + code=( + "from seleniumbase import SB\n\n" + "with SB(uc=True, disable_csp=True) as sb:\n" + ' url = "https://steamdb.info/"\n' + " sb.uc_open_with_reconnect(url, 3)\n" + ' sb.uc_click("a.header-login span", 3)\n\n' + " ...\n\n\n\n\n\n\n\n" + ), + ) + self.add_slide( + "

    The code for the previous live demo:

    " + "

    ", + code=( + "from seleniumbase import SB\n\n" + "with SB(uc=True) as sb:\n" + ' url = "https://steamdb.info/"\n' + " sb.uc_open_with_reconnect(url, 3)\n" + ' sb.uc_click("a.header-login span", 3)\n\n' + " sb.uc_gui_click_captcha()\n" + ' sb.assert_text("Sign in", "button#js-sign-in",' + ' timeout=3)\n' + ' sb.uc_click("button#js-sign-in", 2)\n' + ' sb.highlight("div.page_content form")\n' + ' sb.highlight(\'button:contains("Sign in")\',' + ' scroll=False)\n' + ' sb.set_messenger_theme(location="top_center")' + '\n' + ' sb.post_message("SeleniumBase wasn\'t detected!")' + '\n' + ), + ) + self.add_slide( + "

    ๐Ÿ‘ค Important information ๐Ÿ‘ค

    " + "
    " + "
      \n" + "
    • UC Mode now requires PyAutoGUI for all" + " features to work.
    • \n" + "
    • PyAutoGUI may require enabling admin-level permissions" + " for controlling the mouse and the keyboard.
    • \n" + "

      " + "
    • PyAutoGUI doesn't support Headless Mode." + "
    • \n

      " + "
    • UC Mode now includes a special virtual display" + " on Linux so that you no longer need to use Headless Mode" + " in GUI-less environments.
    • \n" + "
    \n" + "

    \n" + ) + self.add_slide( + "

    ๐Ÿ‘ค General information ๐Ÿ‘ค

    " + "

    " + "

    Don't assume that all CAPTCHA services" + " are secure, even if they say they are...

    " + "



    " + ) + self.add_slide( + "

    ๐Ÿ‘ค General information ๐Ÿ‘ค

    " + "

    " + "

    Don't assume that all CAPTCHA services" + " are secure, even if they say they are...


    " + "

    (Looking at you, Cloudflare!)


    " + "

    \n" + ) + self.add_slide( + "

    ๐Ÿ‘ค General information ๐Ÿ‘ค

    " + "

    " + "

    " + "On the other hand,
    " + "some CAPTCHA services are quite good..." + "

    " + "



    " + ) + self.begin_presentation(filename="uc_presentation.html") + + with suppress(Exception): + with SB(uc=True, test=True) as sb: + url = "https://seleniumbase.io/apps/recaptcha" + sb.uc_open_with_reconnect(url) + sb.uc_gui_click_captcha() # Try with PyAutoGUI Click + sb.assert_element("img#captcha-success", timeout=3) + sb.set_messenger_theme(location="top_left") + sb.post_message("SeleniumBase wasn't detected") + + self.create_presentation(theme="serif", transition="none") + self.add_slide( + "

    ๐Ÿ‘ค General information ๐Ÿ‘ค

    " + "

    " + "

    On the other hand,
    " + "some CAPTCHA services are quite good..." + "


    " + "

    (Well done, Google reCAPTCHA!)


    " + "

    \n" + ) + self.add_slide( + "

    ๐Ÿ‘ค General information ๐Ÿ‘ค

    " + "

    " + "

    " + "However, the real reason UC Mode is popular," + " which you saw earlier," + " is because of the Cloudflare-bypass capabilities." + " That's where the reputation comes from..." + "

    " + "



    " + ) + self.add_slide( + "

    ๐Ÿ‘ค Catching up ๐Ÿ‘ค

    " + "

    " + "

    " + "If this is your first tutorial on UC Mode or SeleniumBase," + " then here are some important
    things to know to" + " understand things better..." + "

    " + "



    " + ) + self.add_slide( + "

    ๐Ÿ‘ค What is SeleniumBase? ๐Ÿ‘ค

    " + "

    " + "

    " + "SeleniumBase is a complete framework for web automation" + " and testing with Python and Selenium." + "

    " + "Although there are many different features,
    " + "the most popular one today is UC Mode,
    " + "which enables Selenium browsers to appear
    " + "as human-controlled browsers to websites." + "

    " + "
    " + ) + self.add_slide( + "

    ๐Ÿ‘ค Structuring Scripts / Tests ๐Ÿ‘ค

    " + "

    " + "

    " + "There are different ways of stucturing SeleniumBase scripts." + ' (Internally called: "The 25 Syntax Formats")' + "

    " + 'Most examples use Syntax Format 1: "BaseCase direct class' + ' inheritance", which uses the "pytest" test runner.' + "

    " + 'The next one in popularity is Syntax Format 21: "SeleniumBase SB"' + ' (Python context manager)",
    which is ideal and recommended' + " for UC Mode." + "

    " + "
    " + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + "
    ๐Ÿ“Š The SeleniumBase GitHub Page ๐Ÿ“Š
    " + '' + ) + self.add_slide( + "

    About me: (Michael Mintz)

    \n" + "
      \n" + "
    • I created the SeleniumBase framework." + "
    • \n" + "
    • I lead the Automation Team at iboss." + "
    • \n" + "
    ", + image="https://seleniumbase.io/other/iboss_booth.png", + ) + self.add_slide( + "

    About me: (Michael Mintz)

    \n" + "
      \n" + "
    • I've reached over 2 million developers
      " + " on Stack Overflow.
    • \n" + "
    " + '' + ) + self.add_slide( + "

    ๐Ÿ‘ค The Great CAPTCHA Duel: ๐Ÿ‘ค

    " + "

    " + "

    " + "Throughout the past few years, Cloudflare has pushed a lot" + " of changes to their Turnstile CAPTCHA." + "

    " + "In order to keep UC Mode working, I had to push a lot of" + " updates to counter those changes." + "

    " + "It has been an epic duel..." + "

    " + "
    " + ) + self.add_slide( + "

    ๐Ÿ‘ค The Great CAPTCHA Duel: ๐Ÿ‘ค

    " + "

    " + "

    " + "Sometimes Cloudflare pushed multiple
    " + "changes at the same time..." + "


    " + "That's when I had to make multiple
    " + "UC Mode updates to counter those changes..." + "


    " + "Sometimes I received a little assistance from GitHub..." + "

    " + "
    " + ) + self.add_slide( + "

    ๐Ÿ‘ค Timeline of major Cloudflare updates ๐Ÿ‘ค

    " + "

    " + "

    " + "March 20, 2024: Cloudflare pushed a major update" + " where they could detect UC Mode Selenium clicks." + "

    " + "Outcome: UC Mode's uc_click(selector) method" + " was updated to click on CAPTCHAs
    via JavaScript using" + " window.setTimeout()." + "


    " + "
    " + ) + self.add_slide( + "

    ๐Ÿ‘ค Timeline of major Cloudflare updates ๐Ÿ‘ค

    " + "

    " + "

    " + "May 10, 2024: Cloudflare pushed a major update" + " where CSS Selectors of CAPTCHAs were updated." + "

    " + "Outcome: I had to update all the UC Mode
    " + 'examples to change "span.mark" to just "span".' + "


    " + "
    " + ) + self.add_slide( + "

    ๐Ÿ‘ค Timeline of major Cloudflare updates ๐Ÿ‘ค

    " + "

    " + "

    " + "June 7, 2024: Cloudflare pushed an update where CAPTCHAs" + " could detect JavaScript clicks.
    (This was a major setback!)" + "


    " + "Outcome: I had to add new UC Mode methods for clicking on" + " CAPTCHAs with PyAutoGUI." + "

    " + "
    " + ) + self.add_slide( + "

    ๐Ÿ‘ค Timeline of major Cloudflare updates ๐Ÿ‘ค

    " + "

    " + "

    " + "July 8, 2024: Cloudflare made an update where CAPTCHAs" + " were hidden behind Shadow-DOM.
    " + "(They went for a killing blow!)" + "


    " + "Outcome: I updated existing UC Mode methods so they could" + " determine the CAPTCHA coordinates
    for" + " PyAutoGUI." + " (Same-day delivery, thanks to an advanced warning" + " on Discord a few days earlier.)" + "


    " + "
    " + ) + self.add_slide( + "

    ๐Ÿ‘ค Timeline of major Cloudflare updates ๐Ÿ‘ค

    " + "

    " + "

    " + "July 25, 2024: Cloudflare made updates to the
    " + "CSS Selectors that come before Shadow-DOM." + "


    " + "Outcome: I updated existing UC Mode methods." + "

    " + 'Note: You can use "uc_gui_handle_captcha()" or
    ' + '"uc_gui_click_captcha()" for any CAPTCHA now.
    ' + '(On Linux, only "uc_gui_click_captcha" works.)' + "

    " + "
    " + ) + self.add_slide( + "

    ๐Ÿ‘ค Timeline of major Cloudflare updates ๐Ÿ‘ค

    " + "

    " + "

    " + "Only minor changes from Cloudflare
    " + "have been shipped since then so far..." + "


    " + "Remember: Give me space to work
    " + "on UC Mode updates as needed..." + "


    " + "...because you never know when they'll strike next..." + "

    " + "
    " + ) + self.add_slide( + "

    ๐Ÿ‘ค Theories on how Cloudflare detects JS ๐Ÿ‘ค

    " + "

    " + "

    " + "A month before Cloudflare added JS-detection,
    " + "a GitHub repo named Brotector was released." + "


    " + "Brotector is capable of detecting both Selenium & JS." + "

    " + "Based on experiements, Brotector's detection mechanisms" + " appear to get the same results as Cloudflare's detection" + " mechanisms.
    (It appears that Cloudflare learned from them.)" + "

    " + "
    " + ) + self.add_slide( + "
    Brotector info
    " + '' + ) + self.add_slide( + "
    Brotector info
    " + '' + ) + self.add_slide( + "

    ๐Ÿ‘ค Using Brotector to make UC Mode better ๐Ÿ‘ค

    " + "

    " + "

    " + "In order to make UC Mode better, I decided to build my own" + " open-source CAPTCHA using Brotector:
    " + '"The Brotector CAPTCHA"' + "


    " + "Unlike Cloudflare's detection system, which only scans for" + " bots on page loads and CAPTCHA-clicks, the Brotector CAPTCHA" + " continuously scans for bots." + "

    " + "This makes it a more powerful anti-bot system." + "

    " + "
    " + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + "

    Here's a LIVE DEMO of Brotector CAPTCHA:

    " + ) + self.begin_presentation(filename="uc_presentation.html") + + with suppress(Exception): + with SB(test=True) as sb: + url = "https://seleniumbase.io/hobbit/login" + sb.goto(url) + sb.click_if_visible("button") + sb.assert_text("Gandalf blocked you!", "h1") + sb.click("img") + sb.highlight("h1") + sb.sleep(3) # Gandalf: "You Shall Not Pass!" + + self.create_presentation(theme="serif", transition="none") + self.add_slide( + "

    Here's a LIVE DEMO of UC Mode bypassing" + " Brotector CAPTCHA:

    " + ) + self.begin_presentation(filename="uc_presentation.html") + + with suppress(Exception): + with SB(uc=True, test=True) as sb: + url = "https://seleniumbase.io/hobbit/login" + sb.uc_open_with_disconnect(url, 2.2) + sb.uc_gui_press_keys("\t ") + sb.reconnect(1.5) + sb.assert_text("Welcome to Middle Earth!", "h1") + sb.set_messenger_theme(location="bottom_center") + sb.post_message("SeleniumBase wasn't detected!") + sb.click("img") + sb.sleep(5.888) # Cool animation happening now! + + self.create_presentation(theme="serif", transition="none") + self.add_slide( + "

    ๐Ÿ‘ค What happens when Cloudflare adds real-time
    " + "bot-detection, like Brotector already has?
    ๐Ÿ‘ค

    " + "

    " + "

    " + "Currently, UC Mode uses Selenium to locate the CAPTCHA checkbox" + " before the PyAutoGUI click.
    (This is fine" + " for now because CF only scans
    " + "during page loads and CAPTCHA clicks.)" + "


    " + "There's already a plan in place for the day
    " + "Cloudflare adds real-time bot-scanning..." + "

    " + "
    " + ) + self.add_slide( + "

    ๐Ÿ‘ค The plan to handle real-time bot-scanning ๐Ÿ‘ค

    " + "

    " + "

    " + '

    sb.uc_gui_click_captcha(frame="iframe", retry=False,'
    +            ' blind=True)

    ' + 'Set the third arg, `blind`, to `True` to force a retry' + ' (if the first click failed) by clicking at the last known' + ' coordinates of the CAPTCHA checkbox without confirming first' + ' with Selenium that a CAPTCHA is still on the page.' + ' (The page will need to reload first.)' + "

    " + "
    " + ) + self.add_slide( + "

    ๐Ÿ‘ค Field trip to the UC Mode help docs ๐Ÿ‘ค

    " + "

    " + "

    " + "Let's take a look at the UC Mode docs
    " + "from the SeleniumBase GitHub repo..." + "

    " + '' + '' + ) + self.add_slide( + "

    ๐Ÿ‘ค Study, study, study! ๐Ÿ‘ค

    " + "

    " + "

    " + "There's lots of important information in the UC Mode docs," + " so study well to avoid falling into traps..." + "

    " + "Sometimes you might still be able to
    " + "get out of a trap you fell into..." + "


    " + "Once you bypass a CAPTCHA, be ready for anything!" + "

    " + "
    " + ) + self.add_slide( + "

    ๐Ÿ‘ค There's more to come ๐Ÿ‘ค

    " + "

    " + "

    " + "As usual, export more UC Mode updates,
    " + "but new projects are classified until released." + "

    " + "
    " + ) + self.add_slide( + "

    โ“ Questions? โ“

    " + "https://github.com/seleniumbase/SeleniumBase/discussions" + "

    " + "

    ๐Ÿ“Œ Found a bug? ๐Ÿž

    " + "https://github.com/seleniumbase/SeleniumBase/issues" + "
    " + ) + self.add_slide( + "

    ๐Ÿ“Š Final remarks ๐Ÿ“ฃ



    " + "

    " + "๐Ÿ› ๏ธ SeleniumBase gives you ๐Ÿ› ๏ธ
    " + "the tools you need to succeed!" + "

    " + "


    " + "And tools to build lots of bots..." + "


    " + ) + self.add_slide( + "
    ๐Ÿ The End ๐Ÿ
    " + '' + ) + self.begin_presentation(filename="uc_presentation.html") diff --git a/examples/presenter/uc_presentation_4.py b/examples/presenter/uc_presentation_4.py new file mode 100644 index 00000000000..acd98e21977 --- /dev/null +++ b/examples/presenter/uc_presentation_4.py @@ -0,0 +1,1072 @@ +# https://www.youtube.com/watch?v=Mr90iQmNsKM +from contextlib import suppress +from seleniumbase import BaseCase +from seleniumbase import SB +BaseCase.main(__name__, __file__) + + +class UCPresentationClass(BaseCase): + def test_presentation_4(self): + self.goto("data:,") + self.set_window_position(4, 40) + self._output_file_saves = False + self.create_presentation(theme="serif", transition="fade") + self.add_slide( + '' + ) + self.add_slide( + "

    This continues my Undetectable Automation series:

    " + '' + ) + self.begin_presentation(filename="uc_presentation.html") + + with suppress(Exception): + self.goto("https://www.bostoncodecamp.com/CC37/info") + self.create_tour(theme="hopscotch") + self.add_tour_step( + "

    Good Afternoon and Welcome!

    ", 'h1.wow' + ) + self.add_tour_step( + "

    PSA: Visit our sponsors later.

    ", + '[href*="/Sponsors"]', + ) + self.add_tour_step( + "

    Let's check out the schedule...

    ", + '[href*="/Schedule/SessionGrid"]' + ) + self.play_tour() + + with suppress(Exception): + self.goto( + "https://www.bostoncodecamp.com/CC37/Schedule/SessionGrid" + ) + self.highlight("h2", loops=8) + if self.is_element_visible('[data-sessionid="765448"]'): + self.highlight('div[data-sessionid="765448"]', loops=10) + self.create_tour(theme="driverjs") + self.add_tour_step( + "

    Here we are

    ", '[data-sessionid="765448"]' + ) + self.play_tour() + self.click('a[onclick*="765448"]') + self.create_tour(theme="hopscotch") + self.add_tour_step( + "

    What to expect

    ", + "div.sz-modal-session", + alignment="left", + ) + self.play_tour() + + self.create_presentation(theme="serif", transition="none") + self.add_slide( + "

    Last time...

    " + ) + self.add_slide( + "

    Last time...

    " + '' + ) + self.add_slide( + "

    This time...

    " + '' + ) + self.add_slide( + "Note: There are different kinds of reCAPTCHA,
    " + "and not all of them are created equal.
    " + '' + ) + self.add_slide( + "

    This is what happens when you fail reCAPTCHA:

    " + '' + ) + self.add_slide( + "

    This is what happens when you fail hCAPTCHA:

    " + '' + ) + self.add_slide( + "

    If you like puppies, hCAPTCHA has you covered:

    " + '' + ) + self.add_slide( + "

    This is what happens when some anti-bots detect you:

    " + '' + ) + self.add_slide( + "

    And this is what happens when Gandalf blocks you:

    " + '' + ) + self.add_slide( + "

    No joke... There's a Hobbit CAPTCHA

    " + '' + ) + self.add_slide( + "

    Important Notice:

    " + "(Know the laws and legal implications!)" + '' + ) + self.add_slide( + "

    ๐Ÿ”น By the end of this presentation... ๐Ÿ”น



    " + "โœ… You'll learn which anti-bot systems work,
    " + "and which ones don't. (Hint: Most don't work.)

    " + "โœ… There will be multiple live demos." + "

    " + "โœ… You'll learn how to bypass weak defenses." + "

    " + "โœ… You'll learn powerful web-scraping techniques." + "

    " + ) + self.add_slide( + "

    But first, a little about me...

    " + '' + ) + self.add_slide( + "

    About me: (Michael Mintz)

    \n" + "
      \n" + "
    • I created the SeleniumBase framework." + "
    • \n" + "
    • I lead the Automation Team at iboss." + "
    • \n" + "
    ", + image="https://seleniumbase.io/other/iboss_booth.png", + ) + self.add_slide( + "

    In my spare time,

    " + "

    I may be found...

    " + ) + self.add_slide( + "

    Spending time with entrepreneurs...

    " + '' + ) + self.add_slide( + "

    Spending time with celebrities...

    " + '' + ) + self.add_slide( + "

    Spending time with politicians...

    " + '' + ) + self.add_slide( + "

    Spending time with philanthropists...

    " + '' + ) + self.add_slide( + "

    Speaking at conferences...

    " + '' + ) + self.add_slide( + "

    Attending conferences as a guest...

    " + '' + ) + self.add_slide( + "

    Jet-skiing in Key West...

    " + '' + ) + self.add_slide( + "

    And working on SeleniumBase...

    " + '' + ) + self.add_slide( + "

    Enough about me...


    " + "

    Let's begin the presentation!



    " + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '' + '' + "
    " + ) + self.add_slide( + '' + ) + self.add_slide( + "Playwright using CDP" + '' + ) + self.add_slide( + "Selenium using CDP" + '' + ) + self.add_slide( + "

    Microsoft still supports Selenium,
    " + "even though they have Playwright.

    " + '' + ) + self.add_slide( + '' + ) + self.add_slide( + "As a birthday gift, BrightData invested a lot of money into " + "Selenium (making them an official sponsor)." + '' + ) + self.add_slide( + "That's great news for the Selenium community!" + '' + ) + self.add_slide( + "Now, let's get back to CDP..." + '' + ) + self.add_slide( + "There are lots of GitHub repos using CDP.
    " + "(This repo tracks some of them)" + '' + ) + self.add_slide( + "The first major Python implementation of CDP:" + '' + ) + self.add_slide( + '' + ) + self.add_slide( + "PyCDP was the key ingredient to stealthy automation." + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + "In addition to using CDP for controlling Chrome in a" + " stealthy way, you can also achieve stealth by using" + " tools that can control the mouse and keyboard.

    " + "PyAutoGUI is one such tool:" + '' + ) + self.add_slide( + '' + ) + self.add_slide( + "PyAutoGUI requires a headed browser to work.

    " + " Since most Linux machines have headless displays that" + " don't support headed browsers, an external tool called" + " Xvfb must be used in order to simulate a headed browser" + " in a headless Linux environment..." + ) + self.add_slide( + '' + ) + self.add_slide( + "

    To have a completely stealthy framework" + " for clicking CAPTCHAs & bypassing anti-bot systems," + " you need:


    " + "

      \n" + '
    • A framework that uses a "regular" browser
      ' + '(to hide evidence of automation activity)' + '
      ' + "

    • \n" + "
    • CDP capabilities for performing stealthy actions" + "

    • \n" + "
    • PyAutoGUI for performing tricky actions
      " + "(eg. clicking Shadow-root CAPTCHAs)
      " + "

    • \n" + "
    • Xvfb integration for headless Linux systems" + "
    • \n

    \n" + ) + self.add_slide( + "SeleniumBase CDP Mode simplifies all that for you:

    " + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + "List of sites with their invisible anti-bot services:" + '' + ) + self.add_slide( + "

    Let's get started with live demos of bypassing" + " physical CAPTCHAs:

    " + ) + self.begin_presentation(filename="uc_presentation.html") + + self.create_presentation(theme="serif", transition="none") + self.add_slide( + "

    Up first...




    " + "planetminecraft.com/account/sign_in/" + "





    " + ) + self.begin_presentation(filename="uc_presentation.html") + + with SB(uc=True, test=True, guest=True) as sb: + sb.activate_cdp_mode() + sb.goto("www.planetminecraft.com/account/sign_in/") + sb.sleep(3) + sb.solve_captcha() + sb.wait_for_element_absent("input[disabled]") + sb.sleep(2) + + self.create_presentation(theme="serif", transition="none") + self.add_slide( + "

    Up next...




    " + "wsform.com/demo/" + "





    " + ) + self.begin_presentation(filename="uc_presentation.html") + + with SB(uc=True, test=True, locale="en") as sb: + url = "https://wsform.com/demo/" + sb.activate_cdp_mode(url) + sb.sleep(3.5) + sb.solve_captcha() + sb.sleep(2.5) + + self.create_presentation(theme="serif", transition="none") + self.add_slide( + "

    Up next...




    " + "gitlab.com/users/sign_in" + "





    " + ) + self.begin_presentation(filename="uc_presentation.html") + + with SB(uc=True, test=True, locale="en") as sb: + sb.activate_cdp_mode() + sb.goto("https://gitlab.com/users/sign_in") + sb.sleep(2) + sb.solve_captcha() + # (The rest is for testing and demo purposes) + sb.assert_element('label[for="user_login"]') + sb.assert_element('input[data-testid*="username"]') + sb.assert_element('input[data-testid*="password"]') + sb.set_messenger_theme(location="bottom_center") + sb.post_message("SeleniumBase wasn't detected!") + + self.create_presentation(theme="serif", transition="none") + self.add_slide( + "

    The code for the previous live demo:

    " + "

    ", + code=( + "from seleniumbase import SB\n\n" + "with SB(uc=True) as sb:\n" + ' url = "https://gitlab.com/users/sign_in"\n' + " sb.activate_cdp_mode(url)\n" + " sb.sleep(2)\n" + " sb.solve_captcha()\n\n" + " ...\n\n\n\n\n" + ), + ) + self.add_slide( + "

    The code for the previous live demo:

    " + "

    ", + code=( + "from seleniumbase import SB\n\n" + "with SB(uc=True) as sb:\n" + ' url = "https://gitlab.com/users/sign_in"\n' + " sb.activate_cdp_mode(url)\n" + " sb.sleep(2)\n" + " sb.solve_captcha()\n\n" + ' sb.assert_text("Username", \'[for="user_login"]\',' + ' timeout=3)\n' + ' sb.assert_element(\'[for="user_login"]\')\n' + ' sb.set_messenger_theme(location="bottom_center")' + '\n' + ' sb.post_message("SeleniumBase wasn\'t detected!")' + '\n' + ), + ) + self.add_slide( + "

    Up next...




    " + "bing.com/turing/captcha/challenge" + "





    " + ) + self.begin_presentation(filename="uc_presentation.html") + + with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.bing.com/turing/captcha/challenge") + sb.sleep(2) + sb.solve_captcha() + sb.sleep(2) + + self.create_presentation(theme="serif", transition="none") + self.add_slide("

    Having fun yet?!?

    ") + self.add_slide( + "

    If you're not yet concerned about online security,
    " + " then you probably need to see more live demos...

    ") + self.add_slide( + "

    Time for live demos of bypassing
    " + "some invisible anti-bot services:

    " + ) + self.add_slide( + "

    Up next...




    " + "pokemon.com/us" + "


    " + "(Protected by Imperva / Incapsula)" + "


    " + ) + self.begin_presentation(filename="uc_presentation.html") + + with SB(uc=True, test=True, locale="en", ad_block=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.pokemon.com/us/pokedex") + sb.sleep(1.5) + sb.click_if_visible("button#onetrust-accept-btn-handler") + sb.sleep(1.5) + sb.click('b:contains("Show Advanced Search")') + sb.sleep(2.5) + sb.click('span[data-type="type"][data-value="electric"]') + sb.sleep(0.7) + sb.scroll_into_view("a#advSearch") + sb.sleep(0.7) + sb.click("a#advSearch") + sb.sleep(1.2) + sb.click('img[src*="img/pokedex/detail/025.png"]') + sb.assert_text("Pikachu", 'div[class*="title"]') + sb.assert_element('img[alt="Pikachu"]') + sb.scroll_into_view("div.pokemon-ability-info") + sb.sleep(1.2) + sb.flash('div[class*="title"]') + sb.flash('img[alt="Pikachu"]') + sb.flash("div.pokemon-ability-info") + name = sb.get_text("label.styled-select") + info = sb.get_text("div.version-descriptions p.active") + print("*** %s: ***\n* %s" % (name, info)) + sb.sleep(2) + sb.highlight_overlay("div.pokemon-ability-info") + sb.sleep(2) + sb.goto("https://events.pokemon.com/EventLocator/") + sb.sleep(2) + sb.click('span:contains("Championship")') + sb.sleep(2) + events = sb.select_all("div.event-info__title") + print("*** Pokรฉmon Championship Events: ***") + for event in events: + print("* " + event.text) + sb.sleep(2) + + self.create_presentation(theme="serif", transition="none") + self.add_slide( + "

    Up next...




    " + "walmart.com" + "


    " + "(Protected by Akamai + PerimeterX)" + "


    " + ) + self.begin_presentation(filename="uc_presentation.html") + + with SB(uc=True, test=True, ad_block=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.walmart.com/") + sb.sleep(2.2) + continue_button = 'button:contains("Continue shopping")' + if sb.is_element_visible(continue_button): + sb.gui_click_element(continue_button) + sb.sleep(0.6) + sb.click('input[aria-label="Search"]') + sb.sleep(1.2) + search = "Settlers of Catan Board Game" + required_text = "Catan" + sb.press_keys('input[aria-label="Search"]', search + "\n") + sb.sleep(3.8) + sb.remove_elements('[data-testid="skyline-ad"]') + sb.remove_elements('[data-testid="sba-container"]') + print('*** Walmart Search for "%s":' % search) + print(' (Results must contain "%s".)' % required_text) + unique_item_text = [] + sb.click_if_visible('[data-automation-id="sb-btn-close-mark"]') + items = sb.find_elements('[data-item-id]') + for item in items: + if required_text.lower() in item.text.lower(): + description = item.query_selector( + '[data-automation-id="product-title"]' + ) + if ( + description + and description.text not in unique_item_text + ): + unique_item_text.append(description.text) + print("* " + description.text) + price = item.query_selector( + '[data-automation-id="product-price"]' + ) + if price: + price_text = price.text + price_text = price_text.split( + "current price Now " + )[-1] + price_text = price_text.split("current price ")[-1] + price_text = price_text.split(" ")[0] + print(" (" + price_text + ")") + item.scroll_into_view() + + self.create_presentation(theme="serif", transition="none") + self.add_slide( + "

    Up next...




    " + "albertsons.com/recipes/" + "


    " + "(Protected by Imperva / Incapsula)" + "


    " + ) + self.begin_presentation(filename="uc_presentation.html") + + with SB(uc=True, test=True, locale="en") as sb: + sb.activate_cdp_mode() + sb.goto("https://www.albertsons.com/recipes/") + sb.sleep(2.5) + sb.remove_element("div > div > article") + sb.scroll_into_view('input[type="search"]') + close_btn = ".notification-alert-wrapper__close-button" + sb.click_if_visible(close_btn) + sb.click("input#search-suggestion-input") + sb.sleep(0.2) + search = "Avocado Smoked Salmon" + required_text = "Salmon" + sb.press_keys("input#search-suggestion-input", search) + sb.sleep(0.8) + sb.click("#suggestion-0 a span") + sb.sleep(0.8) + sb.click_if_visible(close_btn) + sb.sleep(3.2) + print('*** Albertsons Search for "%s":' % search) + print(' (Results must contain "%s".)' % required_text) + unique_item_text = [] + item_selector = 'a[href*="/meal-plans-recipes/shop/"]' + items = sb.find_elements(item_selector) + for item in items: + sb.sleep(0.06) + if required_text in item.text: + item.flash(color="44CC88") + sb.sleep(0.025) + if item.text not in unique_item_text: + unique_item_text.append(item.text) + print("* " + item.text) + + self.create_presentation(theme="serif", transition="none") + self.add_slide( + "

    Up next...




    " + "easyjet.com/en/" + "


    " + "(Protected by Akamai)" + "


    " + ) + self.begin_presentation(filename="uc_presentation.html") + + with SB(uc=True, test=True, locale="en", ad_block=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.easyjet.com/en/") + sb.sleep(1.8) + sb.click_if_visible("button#ensRejectAds", timeout=2) + sb.sleep(1) + sb.click('input[name="from"]') + sb.sleep(1) + sb.type('input[name="from"]', "London Gatwick") + sb.sleep(1) + sb.click('span[data-testid="airport-name"]') + sb.sleep(1) + sb.type('input[name="to"]', "Paris") + sb.sleep(1) + sb.click('span[data-testid="airport-name"]') + sb.sleep(1) + sb.click('input[name="when"]') + sb.sleep(1) + sb.click( + '[data-testid="month"]:last-of-type [aria-disabled="false"]' + ) + sb.sleep(1) + sb.click( + '[data-testid="month"]:last-of-type [aria-disabled="false"]' + ) + sb.sleep(1) + sb.click('button[data-testid="submit"]') + sb.sleep(4) + sb.connect() + sb.sleep(1) + for window in sb.driver.window_handles: + sb.switch_to_window(window) + if "/buy/flights" in sb.get_current_url(): + break + sb.click_if_visible("button#ensCloseBanner") + days = sb.find_elements('div[class*="FlightGridLayout_column"]') + for day in days: + if not day.text.strip(): + continue + print("**** " + " ".join(day.text.split("\n")[0:2]) + " ****") + fares = day.find_elements( + "css selector", 'button[class*="flightDet"]' + ) + if not fares: + print("No flights today!") + for fare in fares: + info = fare.text + info = info.replace("LOWEST FARE\n", "") + info = info.replace("\n", " ") + print(info) + + self.create_presentation(theme="serif", transition="none") + self.add_slide( + "

    Up next...




    " + "hyatt.com" + "


    " + "(Protected by Kasada)" + "


    " + ) + self.begin_presentation(filename="uc_presentation.html") + + with SB(uc=True, test=True, locale="en", ad_block=True) as sb: + url = "https://www.hyatt.com/" + sb.activate_cdp_mode() + sb.goto("https://www.hyatt.com/") + sb.sleep(3.6) + sb.click_if_visible('button[aria-label="Close"]') + sb.sleep(0.1) + sb.click_if_visible("#onetrust-reject-all-handler") + sb.sleep(1.2) + location = "Anaheim, CA, USA" + sb.type('input[id="search-term"]', location) + sb.sleep(1.2) + sb.click('li[data-js="suggestion"]') + sb.sleep(0.6) + sb.click_if_visible('button[aria-label="Close"]') + sb.sleep(0.8) + sb.click("button.be-button-shop") + sb.sleep(1) + sb.click_if_visible('[label="Find Hotels"]') + sb.sleep(5.5) + card_info = ( + 'div[data-booking-status="BOOKABLE"] [class*="HotelCard_info"]' + ) + hotels = sb.select_all(card_info) + destination_selector = 'span[class*="summary_destination"]' + print("Hyatt Hotels in %s:" % location) + print("(" + sb.get_text(destination_selector) + ")") + if len(hotels) == 0: + print("No availability over the selected dates!") + for hotel in hotels: + info = hotel.text.strip() + if "Avg/Night" in info and not info.startswith("Rates from"): + name = info.split(" (")[0] + name = name.split(" + ")[0].split(" Award Cat")[0] + name = name.split(" Rates from :")[0] + price = "?" + if "Rates from : " in info: + price = info.split("Rates from : ")[1] + price = price.split(" Avg/Night")[0] + print("* %s => %s" % (name, price)) + + self.create_presentation(theme="serif", transition="none") + self.add_slide( + "

    Up next...




    " + "bestwestern.com/en_US.html" + "


    " + "(Protected by DataDome)" + "


    " + ) + self.begin_presentation(filename="uc_presentation.html") + + with SB(uc=True, test=True, locale="en", guest=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.bestwestern.com/en_US.html") + sb.sleep(3) + sb.click_if_visible(".onetrust-close-btn-handler") + sb.sleep(1) + sb.click("input#destination-input") + sb.sleep(2) + location = "Palm Springs, CA, USA" + sb.press_keys("input#destination-input", location) + sb.sleep(1) + sb.click("ul#google-suggestions li") + sb.sleep(1) + sb.click("button#btn-modify-stay-update") + sb.sleep(4) + sb.click("label#available-label") + sb.sleep(2.5) + print("Best Western Hotels in %s:" % location) + summary_details = sb.get_text("#summary-details-column") + dates = summary_details.split("DESTINATION")[-1] + dates = dates.split(" CHECK-OUT")[0].strip() + " CHECK-OUT" + dates = dates.replace(" ", " ") + print("(Dates: %s)" % dates) + flip_cards = sb.select_all(".flipCard") + for i, flip_card in enumerate(flip_cards): + hotel = flip_card.query_selector(".hotelName") + price = flip_card.query_selector(".priceSection") + if hotel and price: + print("* %s: %s => %s" % ( + i + 1, hotel.text.strip(), price.text.strip()) + ) + + self.create_presentation(theme="serif", transition="none") + self.add_slide( + "

    Up next...




    " + "priceline.com" + "


    " + "(Protected by DataDome)" + "


    " + ) + self.begin_presentation(filename="uc_presentation.html") + + with SB(uc=True, test=True, locale="en", guest=True, pls="none") as sb: + sb.activate_cdp_mode() + sb.goto("https://www.priceline.com") + sb.sleep(3) + input_selector = "div.location-input input" + sb.gui_hover_element(input_selector) + sb.mouse_click(input_selector) + location = "Portland, OR" + selection = "Oregon, United States" # (Dropdown option) + sb.gui_hover_element(input_selector) + sb.press_keys(input_selector, location) + sb.sleep(0.5) + sb.click(selection) + sb.sleep(0.4) + sb.scroll_down(25) + sb.sleep(0.2) + overlay_close = '[aria-label*="Overlay"] [title="Close"]' + calendar_close = 'button[aria-label="Dismiss calendar"]' + if not sb.is_element_visible(calendar_close): + calendar_close = '[data-mode="range"] span.px-1' + sb.click_if_visible(overlay_close) + sb.sleep(0.3) + sb.click(calendar_close) + sb.sleep(0.3) + sb.click_if_visible(overlay_close) + sb.sleep(0.3) + sb.click('form button[type="submit"]') + sb.sleep(4.8) + if len(sb.get_tabs()) > 1: + sb.close_active_tab() + sb.switch_to_newest_tab() + sb.sleep(0.6) + sb.sleep(0.8) + for y in range(1, 9): + sb.scroll_to_y(y * 200) + sb.sleep(0.4) + hotels = sb.find_elements('div[data-vis-key*="content"]') + count = 0 + for hotel in hotels: + title = hotel.query_selector("h3") + if title: + price = hotel.query_selector(".text-heading4") + if price: + count += 1 + price_text = price.text.replace(" ", "") + print( + "* %s: %s => %s" % (count, title.text, price_text) + ) + if not count: + print("No availability over the selected dates!") + + self.create_presentation(theme="serif", transition="none") + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + "

    Up next...




    " + "nike.com" + "


    " + "(Protected by Shape Security)" + "


    " + ) + self.begin_presentation(filename="uc_presentation.html") + + with SB(uc=True, test=True, locale="en", pls="none") as sb: + sb.activate_cdp_mode() + sb.goto("https://www.nike.com/") + sb.sleep(2.5) + sb.click('[data-testid="user-tools-container"] search') + sb.sleep(1.5) + search = "Nike Air Force 1" + sb.press_keys('input[type="search"]', search) + sb.sleep(4) + details = 'ul[data-testid*="products"] figure .details' + elements = sb.select_all(details) + if elements: + print('**** Found results for "%s": ****' % search) + for element in elements: + print("* " + element.text) + sb.sleep(2) + + self.create_presentation(theme="serif", transition="none") + self.add_slide( + "

    Up next...




    " + "nordstrom.com" + "


    " + "(Protected by Shape Security)" + "


    " + ) + self.begin_presentation(filename="uc_presentation.html") + + with SB(uc=True, test=True, locale="en") as sb: + sb.activate_cdp_mode() + sb.goto("https://www.nordstrom.com/") + sb.sleep(2.2) + sb.click("input#keyword-search-input") + sb.sleep(0.8) + search = "cocktail dresses for women teal" + sb.press_keys("input#keyword-search-input", search + "\n") + sb.sleep(2.2) + for i in range(17): + sb.scroll_down(16) + sb.sleep(0.14) + print('*** Nordstrom Search for "%s":' % search) + unique_item_text = [] + items = sb.find_elements("article") + for item in items: + description = item.query_selector("article h3") + if description and description.text not in unique_item_text: + unique_item_text.append(description.text) + price_text = "" + price = item.query_selector( + 'div div span[aria-hidden="true"]' + ) + if price: + price_text = price.text + print("* %s (%s)" % (description.text, price_text)) + + self.create_presentation(theme="serif", transition="none") + self.add_slide( + "

    CDP is powerful, as you can see.

    " + "

    (Especially when used for stealth!)

    " + '' + ) + self.add_slide( + "

    Out of the following 9 anti-bot defense systems...

    " + '' + ) + self.add_slide( + "

    These are weak: (Can't detect stealthy CDP)

    " + '' + ) + self.add_slide( + "

    And these are strong: (CDP is detected)

    " + '' + ) + self.add_slide( + "

    What is Microsoft's stance
    on stealthy CDP?
    " + "



    Officially...

    " + ) + self.add_slide( + '' + ) + self.add_slide( + "

    What is Microsoft's stance
    on stealthy CDP?

    " + "

    Unofficially...

    " + ) + self.add_slide( + "

    There are external repos
    using Playwright for stealth." + "



    " + "And Microsoft employees are
    endorsing them via GitHub Stars." + "

    " + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + "And steathy CDP works well in GitHub Actions." + '' + ) + self.add_slide( + "Why does stealthy CDP work in GitHub Actions,
    " + "but not in other kinds of services like AWS?" + ) + self.add_slide( + "

    Answer:

    " + 'GitHub Actions runs in a
    ' + '"residential IP address" space!' + "

    " + ) + self.add_slide( + '' + ) + self.add_slide( + "

    People can use residential proxies
    " + "to get a residential IP address.

    " + ) + self.add_slide( + "

    Legal info:

    " + '' + ) + self.add_slide("

    To summarize that...

    ") + self.add_slide( + "

    Scraping public data is probably legal.
    " + "(Think Light Side)

    " + '' + ) + self.add_slide( + "

    Scraping private data is probably NOT legal.
    " + "(Think Dark Side)

    " + '' + ) + self.add_slide( + "

    If you break local and/or international laws,
    " + "then bounty hunters may come after you.

    " + '' + ) + self.add_slide( + "

    Let's get back to SeleniumBase

    " + '' + ) + self.add_slide( + "

    SeleniumBase includes a special Chrome extension:

    " + '

    The "Recorder"

    ' + "

    (You can generate complete scripts with it.)

    " + "

    sbase recorder --uc

    " + '' + ) + self.add_slide( + '' + ) + self.begin_presentation(filename="uc_presentation.html") + + # import sys + # from seleniumbase.console_scripts import sb_recorder + # sys.argv.append("--uc") + # sb_recorder.main() + + self.create_presentation(theme="serif", transition="none") + self.add_slide( + "

    How does one make an automation Recorder?

    " + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + "And that's the secret to building a test recorder!" + '' + ) + self.add_slide( + "Also note that there are more stealth CDP repos
    " + "other than the ones that you have already seen." + '' + ) + self.add_slide( + "

    ๐Ÿ‘ค Field trip to the CDP Mode help docs ๐Ÿ‘ค

    " + "

    " + "

    Let's take a look at the CDP Mode docs
    " + "from the SeleniumBase GitHub repo...

    " + '' + '' + ) + self.add_slide( + "

    โ“ Questions? โ“

    " + "https://github.com/seleniumbase/SeleniumBase/discussions" + "

    " + "

    ๐Ÿ“Œ Found a bug? ๐Ÿž

    " + "https://github.com/seleniumbase/SeleniumBase/issues" + "
    " + ) + self.add_slide( + "

    ๐Ÿ“Š Final remarks ๐Ÿ“ฃ



    " + "

    ๐Ÿ› ๏ธ SeleniumBase gives you ๐Ÿ› ๏ธ
    " + "the tools you need to succeed!" + "


    " + "And tools to build lots of bots..." + "


    " + ) + self.add_slide( + "
    ๐Ÿ The End ๐Ÿ
    " + '' + ) + self.begin_presentation(filename="uc_presentation.html") diff --git a/examples/presenter/web_scraping_on_gha.py b/examples/presenter/web_scraping_on_gha.py new file mode 100644 index 00000000000..fe2e06fdb4e --- /dev/null +++ b/examples/presenter/web_scraping_on_gha.py @@ -0,0 +1,301 @@ +from contextlib import suppress +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class UCPresentationClass(BaseCase): + def test_hacking_with_cdp(self): + self.goto("data:,") + self.set_window_position(4, 40) + self._output_file_saves = False + self.create_presentation(theme="serif", transition="none") + self.add_slide("

    Press SPACE to continue!

    \n") + self.add_slide( + "

    Before we begin


    " + "

    (Here's the GitHub page)

    ", + image="https://seleniumbase.io/other/sbase_qr_code.png", + ) + self.begin_presentation(filename="uc_presentation.html") + + with suppress(Exception): + self.goto("https://www.bostoncodecamp.com/CC38/info") + self.create_tour(theme="hopscotch") + self.add_tour_step( + "

    Good Afternoon and Welcome!

    ", 'h1.wow' + ) + self.add_tour_step( + "

    PSA: Visit our sponsors later.

    ", + '[href*="/Sponsors"]', + ) + self.add_tour_step( + "

    Let's check out the schedule...

    ", + '[href*="/Schedule/SessionGrid"]' + ) + self.play_tour() + + with suppress(Exception): + self.goto( + "https://www.bostoncodecamp.com/CC38/Schedule/SessionGrid" + ) + self.highlight("h2", loops=8) + if self.is_element_visible('[data-sessionid="869465"]'): + self.highlight( + 'div[data-sessionid="869465"]', loops=10, scroll=False + ) + self.create_tour(theme="driverjs") + self.add_tour_step( + "

    Here we are

    ", '[data-sessionid="869465"]' + ) + self.play_tour() + self.click('a[onclick*="869465"]') + self.create_tour(theme="hopscotch") + self.add_tour_step( + "

    What to expect

    ", + "div.sz-modal-session", + alignment="left", + ) + self.play_tour() + + self.create_presentation(theme="serif", transition="none") + self.add_slide("

    Press SPACE to begin!

    \n") + self.add_slide( + "

    Coming up... on the Hacker Show:

    \n" + "
      \n" + '
    \n' + "

    \n" + "

    \n", + ) + self.add_slide( + "

    Coming up... on the Hacker Show:

    \n" + "

      \n" + "
    • Unlimited free web-scraping w/ GitHub Actions \n" + "
    • \n" + "
    " + '', + ) + self.add_slide( + "

    Coming up... on the Hacker Show:

    \n" + "

      \n" + "
    • Unlimited free web-scraping w/ GitHub Actions " + "

    • \n" + "
    • Using GitHub Secrets to hide within open-source" + "
    \n" + '\n' + "

    \n" + "

    \n", + ) + self.add_slide( + "

    Coming up... on the Hacker Show:

    \n" + "

      \n" + "
    • Unlimited free web-scraping w/ GitHub Actions " + "

    • \n" + "
    • Using GitHub Secrets to hide within open-source" + "

    • \n" + "
    • Launching your own, free, local proxy server" + "
    \n" + '' + "

    \n" + ) + self.add_slide( + "

    Coming up... on the Hacker Show:

    \n" + "

      \n" + "
    • Unlimited free web-scraping w/ GitHub Actions " + "

    • \n" + "
    • Using GitHub Secrets to hide within open-source" + "

    • \n" + "
    • Launching your own, free, local proxy server" + "

    • \n" + '
    • Using "iptables" to make a proxy server public' + "
    \n" + '' + "

    \n", + ) + self.add_slide( + "

    Coming up... on the Hacker Show:

    \n" + "

      \n" + "
    • Unlimited free web-scraping w/ GitHub Actions " + "

    • \n" + "
    • Using GitHub Secrets to hide within open-source" + "

    • \n" + "
    • Launching your own, free, local proxy server" + "

    • \n" + '
    • Using "iptables" to make a proxy server public' + "

    • \n" + "
    • And multiple live demos after the previews" + "

    • \n" + "
    ", + ) + self.add_slide( + "

    Get ready for some serious hacking!

    " + '' + ) + self.add_slide( + '' + ) + self.add_slide( + "

    And YES, that means bypassing bot-detection!" + '' + ) + self.add_slide( + "

    But first, a little bit about me...

    " + '' + ) + self.add_slide( + "

    About me: (Michael Mintz)

    \n" + "
      \n" + "
    • I created the SeleniumBase framework." + "
    • \n" + "
    • And I lead the Automation Team at iboss." + "
    • \n" + "
    " + '' + ) + self.add_slide( + "

    Fun Fact


    \n" + "

    I once showed SeleniumBase to Sam Altman at MIT.
    " + "(Sam Altman cofounded OpenAI with Elon Musk.)" + '' + ) + self.add_slide( + "

    Recently, SeleniumBase was trending on GitHub:" + "

    \n" + '' + ) + self.add_slide( + "

    The recent popularity can be attributed to CDP Mode," + "
    which provides advanced stealth during automation.

    " + '' + ) + self.add_slide( + "

    That stealth is enough to bypass bot-detection
    " + "while web-scraping from GitHub Actions:

    " + '' + ) + self.add_slide( + "

    GitHub Actions is free for public repositories:

    " + '' + ) + self.add_slide( + '' + ) + self.add_slide( + "

    To hide sensitive information while using" + "
    GitHub Actions for open-source projects," + "
    there's a feature called: GitHub Secrets.

    " + "
    " + "

    That removes the limitation" + r"
    of being 100% open-source," + r"
    while still being 100% free.

    " + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + '' + ) + self.add_slide( + "

    And that's the secret
    to GitHub Secrets!

    " + ) + self.add_slide( + "

    Up next:

    " + "
    " + "

    Instant proxy server

    " + "
    " + "

    (Faster to launch than making Instant Coffee!)" + ) + self.add_slide( + '

    "sbase proxy"

    ' + "
    " + "

    (That's it!)

    " + "
    " + '' + ) + self.add_slide( + '

    More configuration options for "sbase proxy":

    ' + '' + ) + self.add_slide( + "

    The proxy server code comes from proxy.py:

    " + '' + ) + self.add_slide( + "

    " + "Here's how to configure a proxy with SeleniumBase:" + "



      \n" + '
    • Proxy Mode via pytest:' + '
      pytest --proxy="host:port"' + '
      pytest --proxy="user:pass@host:port"' + '

    • \n' + '
    • Proxy Mode via SB() manager:' + '
      SB(proxy="host:port")' + '
      SB(proxy="user:pass@host:port")' + '
    • \n' + ) + self.add_slide( + "

      That's the secret to
      instant proxy servers!

      " + "

      (And how to use them with SeleniumBase)

      " + ) + self.add_slide( + "

      How about opening up a" + "
      proxy server to the world?


      " + ) + self.add_slide( + "

      For that, there's iptables


      " + '' + ) + self.add_slide( + '' + ) + self.add_slide( + "

      And that's the secret to
      making a server public!

      " + '' + ) + self.add_slide( + "

      Let's move on to
      some live demos

      " + '' + ) + self.add_slide( + "

      Live Demo Time!


      " + "

      (Let's head over to GitHub...)

      ", + image="https://seleniumbase.io/other/sbase_qr_code.png", + ) + self.begin_presentation(filename="uc_presentation.html") diff --git a/examples/proxy_test.py b/examples/proxy_test.py old mode 100755 new mode 100644 index f14f0fefe51..d222cb144b1 --- a/examples/proxy_test.py +++ b/examples/proxy_test.py @@ -1,15 +1,49 @@ -import time +from seleniumbase.config import settings from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) -class MyTestClass(BaseCase): - +class ProxyTests(BaseCase): def test_proxy(self): - self.open('https://ipinfo.io/') - ip_address = self.get_text("div.home-ip-details span.value")[1:-1] - self.open('https://ipinfo.io/%s' % ip_address) - print("\n\nIP Address = %s\n" % ip_address) + if self.headless or self.recorder_mode or self.browser == "safari": + self.goto_if_not_url("about:blank") + print("\n Unsupported mode for this test.") + self.skip("Unsupported mode for this test.") + settings.SKIP_JS_WAITS = True + self.goto("https://api.ipify.org/") + ip_address = self.get_text("body") + if "ERR" in ip_address: + raise Exception("Failed to determine IP Address!") + print("\n\nMy IP Address = %s\n" % ip_address) + self.goto("https://ipinfo.io/%s" % ip_address) + self.sleep(2) + self.wait_for_text(ip_address, "h1", timeout=20) + self.wait_for_element_present('[href="/signup"]') + self.wait_for_text("Hosted domains", timeout=20) + self.highlight("h1") + pop_up = '[role="dialog"] span.cursor-pointer' + self.click_if_visible(pop_up) + self.highlight("section#summary") + self.click_if_visible(pop_up) + self.highlight("section#geolocation") + self.click_if_visible(pop_up) + self.sleep(2) print("Displaying Host Info:") - print(self.get_text('ul.address-list')) - print("\nThe browser will close automatically in 7 seconds...") - time.sleep(7) + text = self.get_text("#summary").split("Hosted domains")[0] + rows = text.split("\n") + data = [] + for row in rows: + if row.strip() != "": + data.append(row.strip()) + print("\n".join(data).replace('\n"', ' "')) + print("\nDisplaying GeoLocation Info:") + text = self.get_text("#geolocation") + text = text.split("IP Geolocation data")[0] + rows = text.split("\n") + data = [] + for row in rows: + if row.strip() != "": + data.append(row.strip()) + print("\n".join(data).replace('\n"', ' "')) + self.click_if_visible(pop_up) + self.sleep(3) diff --git a/examples/pytest.ini b/examples/pytest.ini new file mode 100644 index 00000000000..38ebd219597 --- /dev/null +++ b/examples/pytest.ini @@ -0,0 +1,47 @@ +[pytest] + +# Display console output. Disable cacheprovider: +addopts = --capture=tee-sys -p no:cacheprovider + +# Skip these directories during test collection: +norecursedirs = .* build dist recordings temp assets + +# Ignore DeprecationWarning, PytestUnknownMarkWarning +filterwarnings = + ignore::pytest.PytestWarning + ignore:.*U.*mode is deprecated:DeprecationWarning + +# Configure the junit_family option explicitly: +junit_family = legacy + +# Set pytest discovery rules: +# (Most of the rules here are similar to the default rules.) +# (Inheriting unittest.TestCase could override these rules.) +python_files = test_*.py *_test.py *_tests.py *_suite.py +python_classes = Test* *Test* *Test *Tests *Suite +python_functions = test_* + +# Common pytest markers used in examples: +# (pytest may require marker registration to prevent warnings.) +# (Future versions may turn those marker warnings into errors.) +markers = + marker1: custom marker + marker2: custom marker + marker3: custom marker + marker_test_suite: custom marker + expected_failure: custom marker + local: custom marker + remote: custom marker + offline: custom marker + develop: custom marker + qa: custom marker + ci: custom marker + e2e: custom marker + ready: custom marker + smoke: custom marker + deploy: custom marker + active: custom marker + master: custom marker + release: custom marker + staging: custom marker + production: custom marker diff --git a/examples/rate_limiting_test.py b/examples/rate_limiting_test.py old mode 100755 new mode 100644 index 07e4cec2603..8db1f8271ec --- a/examples/rate_limiting_test.py +++ b/examples/rate_limiting_test.py @@ -1,19 +1,21 @@ -""" -This test demonstrates the use of the "rate_limited" decorator. -You can use this decorator on any method to rate-limit it. -""" - -import unittest +"""This test demonstrates the use of the "rate_limited" decorator. +You can use this decorator on any method to rate-limit it.""" +from seleniumbase import BaseCase from seleniumbase import decorators -class MyTestClass(unittest.TestCase): - - @decorators.rate_limited(3.5) # The arg is max calls per second +class RateLimitingTests(BaseCase): + @decorators.rate_limited(4.2) # The arg is max calls per second def print_item(self, item): print(item) def test_rate_limited_printing(self): - print("\nRunning rate-limited print test:") + if self._multithreaded or self.recorder_mode: + self.goto_if_not_url("about:blank") + print("\n Unsupported mode for this test.") + self.skip("Unsupported mode for this test.") + message = "Running rate-limited print() on the command line" + self.goto("data:text/html,

      %s

      " % message) + print("\n%s:" % message) for item in range(1, 11): self.print_item(item) diff --git a/examples/raw_ahrefs.py b/examples/raw_ahrefs.py new file mode 100644 index 00000000000..3e61418e879 --- /dev/null +++ b/examples/raw_ahrefs.py @@ -0,0 +1,18 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, incognito=True, locale="en") as sb: + sb.activate_cdp_mode() + sb.goto("https://ahrefs.com/website-authority-checker") + search_term = "github.com/seleniumbase/SeleniumBase" + sb.type('input[placeholder="Enter domain"]', search_term) + sb.scroll_down(36) + sb.click('span:contains("Check Authority")') + sb.sleep(2) + sb.solve_captcha() + sb.sleep(3) + sb.wait_for_text_not_visible("Checking", timeout=15) + sb.click_if_visible('button[data-cky-tag="close-button"]') + sb.highlight('p:contains("github.com/seleniumbase/SeleniumBase")') + sb.highlight('a:contains("Top 100 backlinks")') + sb.set_messenger_theme(location="bottom_center") + sb.post_message("SeleniumBase wasn't detected!") diff --git a/examples/raw_antibot_login.py b/examples/raw_antibot_login.py new file mode 100644 index 00000000000..6950fdbf81d --- /dev/null +++ b/examples/raw_antibot_login.py @@ -0,0 +1,17 @@ +"""UC Mode has PyAutoGUI methods for stealthy actions. +PyAutoGUI gets installed automatically if not already. +This old UC Mode code is made obsolete by CDP Mode.""" +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + url = "https://seleniumbase.io/antibot/login" + sb.uc_open_with_disconnect(url, 2.15) + sb.uc_gui_write("\t" + "demo_user") + sb.uc_gui_write("\t" + "secret_pass") + sb.uc_gui_press_keys("\t" + " ") # For Single-char keys + sb.sleep(1.4) + sb.uc_gui_press_keys(["\t", "ENTER"]) # Multi-char keys + sb.reconnect(1.8) + sb.assert_text("Welcome!", "h1") + sb.set_messenger_theme(location="bottom_center") + sb.post_message("SeleniumBase wasn't detected!") diff --git a/examples/raw_bing_captcha.py b/examples/raw_bing_captcha.py new file mode 100644 index 00000000000..ca235d37c76 --- /dev/null +++ b/examples/raw_bing_captcha.py @@ -0,0 +1,8 @@ +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.bing.com/turing/captcha/challenge") + sb.sleep(2) + sb.solve_captcha() + sb.sleep(2) diff --git a/examples/raw_block.py b/examples/raw_block.py new file mode 100644 index 00000000000..d64716848f2 --- /dev/null +++ b/examples/raw_block.py @@ -0,0 +1,11 @@ +"""If Brotector catches you, Gandalf blocks you!""" +from seleniumbase import SB + +with SB(test=True) as sb: + url = "https://seleniumbase.io/hobbit/login" + sb.goto(url) + sb.click_if_visible("button") + sb.assert_text("Gandalf blocked you!", "h1") + sb.click("img") + sb.highlight("h1") + sb.sleep(3) # Gandalf: "You Shall Not Pass!" diff --git a/examples/raw_brotector_captcha.py b/examples/raw_brotector_captcha.py new file mode 100644 index 00000000000..c2032b1d6aa --- /dev/null +++ b/examples/raw_brotector_captcha.py @@ -0,0 +1,8 @@ +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://seleniumbase.io/apps/brotector") + sb.click("button span#mySpan") + sb.assert_text("SUCCESS", "label#pText") + sb.highlight("label#pText") diff --git a/examples/raw_call.py b/examples/raw_call.py new file mode 100644 index 00000000000..8d5092be78e --- /dev/null +++ b/examples/raw_call.py @@ -0,0 +1,9 @@ +"""Can run with "python" instead of using "pytest" directly. +Usage: "python raw_call.py". +Two examples: pytest.main() and subprocess.call().""" +import pytest +import subprocess + +if __name__ == "__main__": + pytest.main(["test_coffee_cart.py", "--chrome", "-v"]) + subprocess.call(["pytest", "test_mfa_login.py", "--chrome", "-v"]) diff --git a/examples/raw_cdp_drivers.py b/examples/raw_cdp_drivers.py new file mode 100644 index 00000000000..e105fabe56e --- /dev/null +++ b/examples/raw_cdp_drivers.py @@ -0,0 +1,17 @@ +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + url1 = "https://seleniumbase.io/demo_page" + sb.activate_cdp_mode(url1) + driver1 = sb.driver + url2 = "https://seleniumbase.io/coffee/" + driver2 = sb.get_new_driver(undetectable=True) + sb.activate_cdp_mode(url2) + print(driver1.get_current_url()) + print(driver2.get_current_url()) + sb.switch_to_default_driver() + sb.assert_url_contains("demo_page") + print(sb.get_current_url()) + sb.switch_to_driver(driver2) + sb.assert_url_contains("coffee") + print(sb.get_current_url()) diff --git a/examples/raw_cdp_logging.py b/examples/raw_cdp_logging.py new file mode 100644 index 00000000000..9d893a760e9 --- /dev/null +++ b/examples/raw_cdp_logging.py @@ -0,0 +1,14 @@ +"""NOTE: This uses plain UC Mode, which was replaced by UC + CDP Mode. +PyAutoGUI is installed automatically for uc_gui methods if not already.""" +from rich.pretty import pprint +from seleniumbase import Driver + +driver = Driver(uc=True, log_cdp=True) +try: + url = "seleniumbase.io/apps/turnstile" + driver.uc_open_with_reconnect(url, 2) + driver.uc_gui_handle_captcha() + driver.sleep(2) + pprint(driver.get_log("performance")) +finally: + driver.quit() diff --git a/examples/raw_cf.py b/examples/raw_cf.py new file mode 100644 index 00000000000..b6bbdcd47df --- /dev/null +++ b/examples/raw_cf.py @@ -0,0 +1,9 @@ +"""SB Manager using CDP Mode for bypassing CAPTCHAs.""" +from seleniumbase import SB + +with SB(uc=True, test=True, guest=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://wsform.com/demo/") + sb.sleep(2) + sb.solve_captcha() + sb.sleep(3) diff --git a/examples/raw_cookies.py b/examples/raw_cookies.py new file mode 100644 index 00000000000..15f9e612d4c --- /dev/null +++ b/examples/raw_cookies.py @@ -0,0 +1,19 @@ +"""A SeleniumBase test that loads cookies to bypass login.""" +from seleniumbase import SB + +# Log in to Swag Labs and save cookies +with SB(test=True) as sb: + sb.goto("https://www.saucedemo.com") + sb.wait_for_element("div.login_logo") + sb.type("#user-name", "standard_user") + sb.type("#password", "secret_sauce") + sb.click('input[type="submit"]') + sb.highlight("div.inventory_list", loops=6) + sb.save_cookies(name="cookies.txt") + +# Load previously saved cookies to bypass login +with SB(test=True) as sb: + sb.goto("https://www.saucedemo.com") + sb.load_cookies(name="cookies.txt") + sb.goto("https://www.saucedemo.com/inventory.html") + sb.highlight("div.inventory_list", loops=12) diff --git a/examples/raw_detection.py b/examples/raw_detection.py new file mode 100644 index 00000000000..0a1ce1f05d9 --- /dev/null +++ b/examples/raw_detection.py @@ -0,0 +1,12 @@ +"""The Brotector CAPTCHA in action.""" +from seleniumbase import SB + +with SB(test=True) as sb: + sb.goto("https://seleniumbase.io/antibot/login") + sb.highlight("h4", loops=6) + sb.type("#username", "demo_user") + sb.type("#password", "secret_pass") + sb.click_if_visible("button span") + sb.highlight("label#pText") + sb.highlight("table#detections") + sb.sleep(4.4) # Add time to read the table diff --git a/examples/raw_driver_context.py b/examples/raw_driver_context.py new file mode 100644 index 00000000000..a902d1aa05c --- /dev/null +++ b/examples/raw_driver_context.py @@ -0,0 +1,20 @@ +"""DriverContext() example. (Runs with "python").""" +from seleniumbase import DriverContext + +with DriverContext() as driver: + driver.goto("seleniumbase.io/") + driver.highlight('img[alt="SeleniumBase"]', loops=6) + +with DriverContext(browser="chrome", incognito=True) as driver: + driver.goto("seleniumbase.io/apps/calculator") + driver.click('[id="4"]') + driver.click('[id="2"]') + driver.assert_text("42", "#output") + driver.highlight("#output", loops=6) + +with DriverContext() as driver: + driver.goto("seleniumbase.io/demo_page") + driver.highlight("h2") + driver.type("#myTextInput", "Automation") + driver.click("#checkBox1") + driver.highlight("img", loops=6) diff --git a/examples/raw_driver_manager.py b/examples/raw_driver_manager.py new file mode 100644 index 00000000000..df51b2740eb --- /dev/null +++ b/examples/raw_driver_manager.py @@ -0,0 +1,22 @@ +"""Driver() manager example. (Runs with "python").""" +from seleniumbase import Driver + +driver = Driver() +try: + driver.goto("seleniumbase.io/demo_page") + driver.highlight("h2") + driver.type("#myTextInput", "Automation") + driver.click("#checkBox1") + driver.highlight("img", loops=6) +finally: + driver.quit() + +driver = Driver(browser="chrome", headless=False) +try: + driver.goto("seleniumbase.io/apps/calculator") + driver.click('[id="4"]') + driver.click('[id="2"]') + driver.assert_text("42", "#output") + driver.highlight("#output", loops=6) +finally: + driver.quit() diff --git a/examples/raw_file_call.py b/examples/raw_file_call.py new file mode 100644 index 00000000000..66d46798c88 --- /dev/null +++ b/examples/raw_file_call.py @@ -0,0 +1,22 @@ +"""Call a file with "python" instead of "pytest" directly. +Added pytest args from the command-line won't be included. +To run, use: "python raw_file_call.py".""" +from seleniumbase import BaseCase +if __name__ == "__main__": + from pytest import main + main([__file__, "-s"]) + + +class TinyMceTest(BaseCase): + def test_tinymce(self): + self.goto("https://seleniumbase.io/tinymce/") + self.wait_for_element("div.mce-container-body") + self.click('span:contains("File")') + self.click('span:contains("New document")') + self.click('span:contains("Paragraph")') + self.click('span:contains("Heading 1")') + with self.frame_switch("iframe"): + self.add_text("#tinymce", "SeleniumBase!") + self.highlight("#tinymce") + self.post_message("SeleniumBase is fast!", duration=1.5) + self.post_message("And SeleniumBase is fun!", duration=1.5) diff --git a/examples/raw_form_turnstile.py b/examples/raw_form_turnstile.py new file mode 100644 index 00000000000..442ce766f4d --- /dev/null +++ b/examples/raw_form_turnstile.py @@ -0,0 +1,22 @@ +"""CDP Mode for bypassing bot-detection & CAPTCHAs. +sb.press_keys() is a slower version of sb.type()""" +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("seleniumbase.io/apps/form_turnstile") + sb.press_keys("#name", "SeleniumBase") + sb.press_keys("#email", "test@test.test") + sb.press_keys("#phone", "1-555-555-5555") + sb.click('[for="date"]') + sb.click("td.is-today button") + sb.click('div[class="select-wrapper"] input') + sb.click('span:contains("9:00 PM")') + sb.highlight_click('input[value="AR"] + span') + sb.click('input[value="cc"] + span') + sb.scroll_down(40) + sb.solve_captcha() + sb.highlight("img#captcha-success", timeout=3) + sb.highlight_click('button:contains("Request & Pay")') + sb.highlight("img#submit-success") + sb.highlight('button:contains("Success!")') diff --git a/examples/raw_games.py b/examples/raw_games.py new file mode 100644 index 00000000000..c50eb141b70 --- /dev/null +++ b/examples/raw_games.py @@ -0,0 +1,18 @@ +"""SB Manager using CDP Mode for evading bot-detection.""" +from seleniumbase import SB + +with SB(uc=True, test=True, disable_csp=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://steamdb.info/") + sb.sleep(1) + sb.click("a.header-login span") + sb.sleep(2) + sb.solve_captcha() + sb.assert_text("Sign in", "button#js-sign-in", timeout=4) + sb.sleep(0.5) + sb.click("button#js-sign-in") + sb.sleep(0.5) + sb.highlight("div.page_content form") + sb.highlight('button:contains("Sign in")', scroll=False) + sb.set_messenger_theme(location="top_center") + sb.post_message("SeleniumBase wasn't detected", duration=4) diff --git a/examples/raw_gitlab.py b/examples/raw_gitlab.py new file mode 100644 index 00000000000..facdf4a724b --- /dev/null +++ b/examples/raw_gitlab.py @@ -0,0 +1,13 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, locale="en") as sb: + sb.activate_cdp_mode() + sb.goto("https://gitlab.com/users/sign_in") + sb.sleep(2) + sb.solve_captcha() + # (The rest is for testing and demo purposes) + sb.assert_element('label[for="user_login"]') + sb.assert_element('input[data-testid*="username"]') + sb.assert_element('input[data-testid*="password"]') + sb.set_messenger_theme(location="bottom_center") + sb.post_message("SeleniumBase wasn't detected!") diff --git a/examples/raw_google.py b/examples/raw_google.py new file mode 100644 index 00000000000..105772755a2 --- /dev/null +++ b/examples/raw_google.py @@ -0,0 +1,14 @@ +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://google.com/ncr") + sb.click_if_visible('button:contains("Accept all")') + sb.type('[name="q"]', "SeleniumBase GitHub page") + sb.click('[value="Google Search"]') + sb.sleep(4) # The "AI Overview" sometimes loads + print(sb.get_page_title()) + sb.save_as_pdf_to_logs() + sb.save_page_source_to_logs() + sb.save_screenshot_to_logs() + print("Logs have been saved to: ./latest_logs/") diff --git a/examples/raw_gui_click.py b/examples/raw_gui_click.py new file mode 100644 index 00000000000..b78923aa665 --- /dev/null +++ b/examples/raw_gui_click.py @@ -0,0 +1,24 @@ +"""CDP Mode for bypassing bot-detection & CAPTCHAs. +Note: sb.uc_gui_click_captcha() requires PyAutoGUI, +which is installed automatically if not already.""" +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("seleniumbase.io/apps/form_turnstile") + sb.press_keys("#name", "SeleniumBase") + sb.press_keys("#email", "test@test.test") + sb.press_keys("#phone", "1-555-555-5555") + sb.click('[for="date"]') + sb.click("td.is-today button") + sb.click('div[class="select-wrapper"] input') + sb.click('span:contains("9:00 PM")') + sb.highlight_click('input[value="AR"] + span') + sb.click('input[value="cc"] + span') + sb.scroll_to('div[class*="cf-turnstile"]') + sb.scroll_down(40) + sb.uc_gui_click_captcha() # PyAutoGUI mouse click + sb.highlight("img#captcha-success", timeout=3) + sb.highlight_click('button:contains("Request & Pay")') + sb.highlight("img#submit-success") + sb.highlight('button:contains("Success!")') diff --git a/examples/raw_hobbit.py b/examples/raw_hobbit.py new file mode 100644 index 00000000000..c642633352f --- /dev/null +++ b/examples/raw_hobbit.py @@ -0,0 +1,11 @@ +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://seleniumbase.io/hobbit/login") + sb.click("button span#mySpan") + sb.assert_text("Welcome to Middle Earth!", "h1") + sb.set_messenger_theme(location="bottom_center") + sb.post_message("SeleniumBase wasn't detected!") + sb.click("img") + sb.sleep(5.888) # Cool animation happening now! diff --git a/examples/raw_invisible_captcha.py b/examples/raw_invisible_captcha.py new file mode 100644 index 00000000000..182afa88d21 --- /dev/null +++ b/examples/raw_invisible_captcha.py @@ -0,0 +1,9 @@ +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://seleniumbase.io/apps/invisible_recaptcha") + sb.sleep(1) + sb.assert_element("img#captcha-success", timeout=3) + sb.set_messenger_theme(location="top_left") + sb.post_message("SeleniumBase wasn't detected", duration=3) diff --git a/examples/raw_login_context.py b/examples/raw_login_context.py new file mode 100644 index 00000000000..04ef143c316 --- /dev/null +++ b/examples/raw_login_context.py @@ -0,0 +1,12 @@ +from seleniumbase import DriverContext + +with DriverContext() as driver: + driver.goto("seleniumbase.io/simple/login") + driver.type("#username", "demo_user") + driver.type("#password", "secret_pass") + driver.click('a:contains("Sign in")') + driver.assert_exact_text("Welcome!", "h1") + driver.assert_element("img#image1") + driver.highlight("#image1") + driver.click_link("Sign out") + driver.assert_text("signed out", "#top_message") diff --git a/examples/raw_login_driver.py b/examples/raw_login_driver.py new file mode 100644 index 00000000000..a587f6e2600 --- /dev/null +++ b/examples/raw_login_driver.py @@ -0,0 +1,15 @@ +from seleniumbase import Driver + +driver = Driver() +try: + driver.goto("seleniumbase.io/simple/login") + driver.type("#username", "demo_user") + driver.type("#password", "secret_pass") + driver.click('a:contains("Sign in")') + driver.assert_exact_text("Welcome!", "h1") + driver.assert_element("img#image1") + driver.highlight("#image1") + driver.click_link("Sign out") + driver.assert_text("signed out", "#top_message") +finally: + driver.quit() diff --git a/examples/raw_login_sb.py b/examples/raw_login_sb.py new file mode 100644 index 00000000000..659464982c1 --- /dev/null +++ b/examples/raw_login_sb.py @@ -0,0 +1,12 @@ +from seleniumbase import SB + +with SB() as sb: + sb.goto("seleniumbase.io/simple/login") + sb.type("#username", "demo_user") + sb.type("#password", "secret_pass") + sb.click('a:contains("Sign in")') + sb.assert_exact_text("Welcome!", "h1") + sb.assert_element("img#image1") + sb.highlight("#image1") + sb.click_link("Sign out") + sb.assert_text("signed out", "#top_message") diff --git a/examples/raw_main_call.py b/examples/raw_main_call.py new file mode 100644 index 00000000000..80494f0a1ad --- /dev/null +++ b/examples/raw_main_call.py @@ -0,0 +1,17 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class TinyMceTest(BaseCase): + def test_tinymce(self): + self.goto("https://seleniumbase.io/tinymce/") + self.wait_for_element("div.mce-container-body") + self.click('span:contains("File")') + self.click('span:contains("New document")') + self.click('span:contains("Paragraph")') + self.click('span:contains("Heading 1")') + with self.frame_switch("iframe"): + self.add_text("#tinymce", "SeleniumBase!") + self.highlight("#tinymce") + self.post_message("SeleniumBase is fast!", duration=1.5) + self.post_message("And SeleniumBase is fun!", duration=1.5) diff --git a/examples/raw_mobile.py b/examples/raw_mobile.py new file mode 100644 index 00000000000..7ab32c1fdf2 --- /dev/null +++ b/examples/raw_mobile.py @@ -0,0 +1,11 @@ +from seleniumbase import Driver + +driver = Driver(mobile=True) +try: + driver.goto("https://www.roblox.com/") + driver.assert_element("#download-the-app-container") + driver.assert_text("Roblox for Android") + driver.highlight('span:contains("Roblox for Android")', loops=8) + driver.highlight('span:contains("Continue in App")', loops=8) +finally: + driver.quit() diff --git a/examples/raw_multi_drivers.py b/examples/raw_multi_drivers.py new file mode 100644 index 00000000000..485bb80abd5 --- /dev/null +++ b/examples/raw_multi_drivers.py @@ -0,0 +1,27 @@ +import sys +import threading +from concurrent.futures import ThreadPoolExecutor +from random import randint, seed +from seleniumbase import Driver +sys.argv.append("-n") # Tell SeleniumBase to do thread-locking as needed + + +def launch_driver(url): + seed(len(threading.enumerate())) # Random seed for browser placement + driver = Driver() + try: + driver.set_window_rect(randint(4, 720), randint(8, 410), 700, 500) + driver.get(url=url) + if driver.is_element_visible("h1"): + driver.highlight("h1", loops=9) + else: + driver.sleep(2.2) + finally: + driver.quit() + + +if __name__ == "__main__": + urls = ['https://seleniumbase.io/demo_page' for i in range(4)] + with ThreadPoolExecutor(max_workers=len(urls)) as executor: + for url in urls: + executor.submit(launch_driver, url) diff --git a/examples/raw_multi_sb.py b/examples/raw_multi_sb.py new file mode 100644 index 00000000000..04350ff5260 --- /dev/null +++ b/examples/raw_multi_sb.py @@ -0,0 +1,24 @@ +import sys +import threading +from concurrent.futures import ThreadPoolExecutor +from random import randint, seed +from seleniumbase import SB +sys.argv.append("-n") # Tell SeleniumBase to do thread-locking as needed + + +def launch_driver(url): + seed(len(threading.enumerate())) # Random seed for browser placement + with SB() as sb: + sb.set_window_rect(randint(4, 720), randint(8, 410), 700, 500) + sb.goto(url=url) + if sb.is_element_visible("h1"): + sb.highlight("h1", loops=9) + else: + sb.sleep(2.2) + + +if __name__ == "__main__": + urls = ['https://seleniumbase.io/demo_page' for i in range(4)] + with ThreadPoolExecutor(max_workers=len(urls)) as executor: + for url in urls: + executor.submit(launch_driver, url) diff --git a/examples/raw_no_context_mgr.py b/examples/raw_no_context_mgr.py new file mode 100644 index 00000000000..ae4a1063d4d --- /dev/null +++ b/examples/raw_no_context_mgr.py @@ -0,0 +1,16 @@ +"""SB() without the context manager `with` block.""" +from seleniumbase import SB + +sb_context = SB() +sb = sb_context.__enter__() +sb.goto("data:text/html,

      Test Page

      ") +sb.highlight("h1", loops=8) +sb_context.__exit__(None, None, None) + +"""Same example using `with`: +from seleniumbase import SB + +with SB() as sb: + sb.goto("data:text/html,

      Test Page

      ") + sb.highlight("h1", loops=8) +""" diff --git a/examples/raw_order_tickets.py b/examples/raw_order_tickets.py new file mode 100644 index 00000000000..4f6f6491fd2 --- /dev/null +++ b/examples/raw_order_tickets.py @@ -0,0 +1,17 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, ad_block=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://www.ticketmaster.com") + input_field = 'input[name="q"]' + sb.wait_for_element(input_field) + sb.sleep(1.6) + query = "Jerry Seinfeld" + sb.press_keys(input_field, query) + sb.sleep(1.6) + sb.click('a:contains("%s")' % query) + sb.sleep(4.2) + print('*** TicketMaster Search for "%s":' % query) + item_selector = '[data-testid="eventList"] li' + for item in sb.find_elements(item_selector): + print("* " + item.text) diff --git a/examples/raw_parameter_script.py b/examples/raw_parameter_script.py old mode 100755 new mode 100644 index 0ec0369a830..cafaf670fd0 --- a/examples/raw_parameter_script.py +++ b/examples/raw_parameter_script.py @@ -1,20 +1,24 @@ """ The main purpose of this file is to demonstrate running SeleniumBase scripts without the use of Pytest by calling the script directly - with Python or from a Python interactive interpreter. Based on - whether relative imports work or don't, the script can autodetect - how this file was run. With pure Python, it will initialize - all the variables that would've been automatically initialized - by the Pytest plugin. The setUp() and tearDown() methods are also - now called from the script itself. + with Python or from a Python interactive interpreter. + Based on whether relative imports work or don't work, this script + can autodetect how this file was run. With pure Python, it + initializes all the variables that would've been automatically + initialized by the Pytest plugin. The setUp() and tearDown() methods + are also now called from the script itself. + + (Note: The SB() and Driver() formats make this example obsolete.) One big advantage to running tests with Pytest is that most of this is done for you automatically, with the option to update any of the - parameters through command line parsing. Pytest also provides you + parameters through command-line parsing. Pytest also provides you with other plugins, such as ones for generating test reports, handling multithreading, and parametrized tests. Depending on your specific needs, you may need to call SeleniumBase commands without - using Pytest, and this example shows you how. """ + using Pytest, and this example shows you one way of doing that. +""" +pure_python = False try: # Running with Pytest / (Finds test methods to run using autodiscovery) # Example run command: "pytest raw_parameter_script.py" @@ -23,41 +27,119 @@ except (ImportError, ValueError): # Running with pure Python OR from a Python interactive interpreter # Example run command: "python raw_parameter_script.py" - from my_first_test import MyTestClass # (relative imports DON'T work) + from my_first_test import MyTestClass # (relative imports do not work) + + pure_python = True - b = MyTestClass("test_basic") - b.browser = "chrome" - b.headless = False - b.servername = "localhost" - b.port = 4444 - b.data = None - b.environment = "test" - b.user_agent = None - b.database_env = "test" - b.log_path = "latest_logs/" - b.archive_logs = False - b.disable_csp = False - b.visual_baseline = False - b.save_screenshot_after_test = False - b.timeout_multiplier = None - b.pytest_html_report = None - b.report_on = False - b.with_db_reporting = False - b.with_s3_logging = False - b.js_checking_on = False - b.is_pytest = False - b.demo_mode = False - b.demo_sleep = 1 - b.message_duration = 2 - b.proxy_string = None - b.ad_block_on = False - b.highlights = None - b.check_js = False - b.cap_file = None +if pure_python: + sb = MyTestClass("test_swag_labs") + sb.browser = "chrome" + sb.is_behave = False + sb.headless = False + sb.headless1 = False + sb.headless2 = False + sb.headed = False + sb.xvfb = False + sb.xvfb_metrics = None + sb.start_page = None + sb.locale_code = None + sb.protocol = "http" + sb.servername = "localhost" + sb.port = 4444 + sb.data = None + sb.var1 = None + sb.var2 = None + sb.var3 = None + sb.variables = {} + sb.account = None + sb.environment = "test" + sb.env = "test" # should match sb.environment + sb.user_agent = None + sb.incognito = False + sb.guest_mode = False + sb.dark_mode = False + sb.devtools = False + sb.mobile_emulator = False + sb.device_metrics = None + sb.extension_zip = None + sb.extension_dir = None + sb.database_env = "test" + sb.archive_logs = False + sb.disable_csp = False + sb.disable_ws = False + sb.enable_ws = False + sb.enable_sync = False + sb.use_auto_ext = False + sb.undetectable = False + sb.uc_cdp_events = False + sb.uc_subprocess = False + sb.no_sandbox = False + sb.disable_js = False + sb.disable_gpu = False + sb.log_cdp_events = False + sb._multithreaded = False + sb._reuse_session = False + sb._crumbs = False + sb._final_debug = False + sb.esc_end = False + sb.enable_3d_apis = False + sb.window_position = None + sb.window_size = None + sb.maximize_option = False + sb.visual_baseline = False + sb.disable_cookies = False + sb.disable_features = None + sb._disable_beforeunload = False + sb.save_screenshot_after_test = False + sb.no_screenshot_after_test = False + sb.host_resolver_rules = None + sb.page_load_strategy = None + sb.timeout_multiplier = None + sb.pytest_html_report = None + sb.with_db_reporting = False + sb.with_s3_logging = False + sb.js_checking_on = False + sb.recorder_mode = False + sb.recorder_ext = False + sb.record_sleep = False + sb.rec_behave = False + sb.rec_sb_mgr = False + sb.rec_sb_cdp = False + sb.rec_print = False + sb.report_on = False + sb.is_pytest = False + sb.slow_mode = False + sb.demo_mode = False + sb.time_limit = None + sb.demo_sleep = None + sb.dashboard = False + sb._dash_initialized = False + sb.message_duration = None + sb.binary_location = None + sb.driver_version = None + sb.block_images = False + sb.do_not_track = False + sb.external_pdf = False + sb.remote_debug = False + sb.settings_file = None + sb.user_data_dir = None + sb.chromium_arg = None + sb.firefox_arg = None + sb.firefox_pref = None + sb.proxy_string = None + sb.proxy_bypass_list = None + sb.proxy_pac_url = None + sb._swiftshader = False + sb.multi_proxy = False + sb.ad_block_on = False + sb.highlights = None + sb.interval = None + sb.cap_file = None + sb.cap_string = None - b.setUp() + sb.setUp() try: - b.test_basic() + sb.test_swag_labs() finally: - b.tearDown() - del b + sb.tearDown() + del sb diff --git a/examples/raw_performance_logs.py b/examples/raw_performance_logs.py new file mode 100644 index 00000000000..6a1e0ec5f9a --- /dev/null +++ b/examples/raw_performance_logs.py @@ -0,0 +1,7 @@ +from rich.pretty import pprint +from seleniumbase import SB + +with SB(log_cdp=True) as sb: + sb.goto("seleniumbase.io/demo_page") + sb.sleep(1) + pprint(sb.driver.get_log("performance")) diff --git a/examples/raw_pixelscan.py b/examples/raw_pixelscan.py new file mode 100644 index 00000000000..a1f9422f1af --- /dev/null +++ b/examples/raw_pixelscan.py @@ -0,0 +1,16 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, guest=True) as sb: + sb.activate_cdp_mode(ad_block=True) + sb.goto("https://pixelscan.net/fingerprint-check") + sb.remove_element("div.header-ad") + sb.remove_element("pxlscn-dynamic-ad") + sb.sleep(1.8) + sb.assert_text("No automated behavior", "pxlscn-bot-detection") + sb.assert_text("No masking detected", "pxlscn-fingerprint-masking") + sb.assert_text("consistent", "span.status-success") + sb.sleep(0.5) + sb.cdp.highlight("span.status-success") + sb.cdp.highlight("pxlscn-fingerprint-masking p") + sb.cdp.highlight("pxlscn-bot-detection p") + print("Bot Not Detected") diff --git a/examples/raw_pyautogui.py b/examples/raw_pyautogui.py new file mode 100644 index 00000000000..61949d937c9 --- /dev/null +++ b/examples/raw_pyautogui.py @@ -0,0 +1,9 @@ +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://seleniumbase.io/apps/turnstile") + sb.uc_gui_handle_captcha() # Cycle with TAB, then SPACEBAR + sb.assert_element("img#captcha-success", timeout=3) + sb.set_messenger_theme(location="top_left") + sb.post_message("SeleniumBase wasn't detected", duration=3) diff --git a/examples/raw_robot.py b/examples/raw_robot.py new file mode 100644 index 00000000000..ff8923deb2f --- /dev/null +++ b/examples/raw_robot.py @@ -0,0 +1,17 @@ +from seleniumbase import SB + +with SB(enable_3d_apis=True, test=True) as sb: + sb.goto("threejs.org/examples/#webgl_animation_skinning_morph") + sb.switch_to_frame("iframe#viewer") + sb.set_text_content("#info p", "Hi, I'm Michael Mintz") + sb.add_css_style("#info p{zoom: 2.54}") + sb.sleep(0.8) + sb.click('button:contains("Wave")') + sb.highlight("#info p") + sb.select_option_by_text("select", "Idle") + sb.click('button:contains("ThumbsUp")') + sb.set_text_content("#info p", "I created SeleniumBase") + sb.highlight("#info p") + sb.sleep(0.8) + sb.click('button:contains("Jump")') + sb.sleep(1.5) diff --git a/examples/raw_sb.py b/examples/raw_sb.py new file mode 100644 index 00000000000..b313e8b1f9a --- /dev/null +++ b/examples/raw_sb.py @@ -0,0 +1,14 @@ +"""SB() context manager example. (Runs with "python").""" +from seleniumbase import SB + +with SB() as sb: # By default, browser="chrome" if not set. + sb.goto("https://seleniumbase.io/realworld/login") + sb.type("#username", "demo_user") + sb.type("#password", "secret_pass") + sb.enter_mfa_code("#totpcode", "GAXG2MTEOR3DMMDG") # 6-digit + sb.assert_text("Welcome!", "h1") + sb.highlight("img#image1") # A fancier assert_element() call + sb.click('a:contains("This Page")') # Use :contains() on any tag + sb.click_link("Sign out") # Link must be "a" tag. Not "button". + sb.assert_element('a:contains("Sign in")') + sb.assert_exact_text("You have been signed out!", "#top_message") diff --git a/examples/raw_test_scripts.py b/examples/raw_test_scripts.py new file mode 100644 index 00000000000..7cdbccb6b2a --- /dev/null +++ b/examples/raw_test_scripts.py @@ -0,0 +1,22 @@ +"""Context Manager Test. Runs with "python". (pytest not needed)""" +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.goto("https://google.com/ncr") + sb.click_if_visible('button:contains("Accept all")') + sb.type('[name="q"]', "SeleniumBase on GitHub") + sb.click('[value="Google Search"]') + sb.highlight('a[href*="github.com/seleniumbase"]') + sb.sleep(0.5) + +with SB(test=True, rtf=True, demo=True) as sb: + sb.goto("seleniumbase.github.io/demo_page") + sb.type("#myTextInput", "This is Automated") + sb.assert_text("This is Automated", "#myTextInput") + sb.assert_text("This Text is Green", "#pText") + sb.click('button:contains("Click Me")') + sb.assert_text("This Text is Purple", "#pText") + sb.click("#checkBox1") + sb.assert_element_not_visible("div#drop2 img#logo") + sb.drag_and_drop("img#logo", "div#drop2") + sb.assert_element("div#drop2 img#logo") diff --git a/examples/raw_turnstile.py b/examples/raw_turnstile.py new file mode 100644 index 00000000000..172c6958d6c --- /dev/null +++ b/examples/raw_turnstile.py @@ -0,0 +1,10 @@ +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.activate_cdp_mode() + sb.goto("https://seleniumbase.io/apps/turnstile") + sb.sleep(0.5) + sb.solve_captcha() + sb.assert_element("img#captcha-success", timeout=3) + sb.set_messenger_theme(location="top_left") + sb.post_message("SeleniumBase wasn't detected", duration=3) diff --git a/examples/raw_uc_driver.py b/examples/raw_uc_driver.py new file mode 100644 index 00000000000..6d733c302e7 --- /dev/null +++ b/examples/raw_uc_driver.py @@ -0,0 +1,16 @@ +"""UC Mode Driver for evading bot-detection.""" +from seleniumbase import Driver + +driver = Driver(uc=True) +driver.get("https://browserscan.net/bot-detection") +driver.assert_element('strong:contains("Normal")') +driver.sleep(1) +driver.get("https://bot.sannysoft.com/") +driver.assert_element("#user-agent-result.passed") +driver.assert_element("#webdriver-result.passed") +driver.assert_element("#advanced-webdriver-result.passed") +driver.assert_element("#permissions-result.passed") +driver.assert_element("#plugins-length-result.passed") +driver.assert_element("#plugins-type-result.passed") +driver.sleep(1) +driver.quit() diff --git a/examples/raw_uc_events.py b/examples/raw_uc_events.py new file mode 100644 index 00000000000..e55e63debda --- /dev/null +++ b/examples/raw_uc_events.py @@ -0,0 +1,29 @@ +"""NOTE: This uses plain UC Mode, which was replaced by UC + CDP Mode. +PyAutoGUI is installed automatically for uc_gui methods if not already.""" +from rich.pretty import pprint +from seleniumbase import SB + + +def add_cdp_listener(sb): + # (To print everything, use "*". Otherwise select specific headers.) + # self.driver.add_cdp_listener("*", lambda data: print(pformat(data))) + sb.driver.add_cdp_listener( + "Network.requestWillBeSentExtraInfo", + lambda data: pprint(data) + ) + + +def click_turnstile_and_verify(sb): + sb.uc_gui_handle_captcha() + sb.assert_element("img#captcha-success", timeout=3) + sb.highlight("img#captcha-success", loops=8) + + +with SB(uc_cdp_events=True, test=True) as sb: + url = "seleniumbase.io/apps/turnstile" + sb.uc_open_with_reconnect(url, 2) + add_cdp_listener(sb) + click_turnstile_and_verify(sb) + sb.sleep(1) + sb.refresh() + sb.sleep(1.2) diff --git a/examples/raw_uc_mode.py b/examples/raw_uc_mode.py new file mode 100644 index 00000000000..29c1e2ec27b --- /dev/null +++ b/examples/raw_uc_mode.py @@ -0,0 +1,15 @@ +"""SB Manager using UC Mode for evading bot-detection. +Note that plain UC Mode was replaced by the newer CDP Mode: +Instead of uc_open_with_reconnect, use activate_cdp_mode(). +Instead of uc_gui_click_captcha(), use solve_captcha().""" +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + url = "https://gitlab.com/users/sign_in" + sb.uc_open_with_reconnect(url, 4) + sb.uc_gui_click_captcha() + sb.assert_text("Username", '[for="user_login"]', timeout=3) + sb.assert_element('label[for="user_login"]') + sb.highlight('button:contains("Sign in")') + sb.highlight('h1:contains("GitLab")') + sb.post_message("SeleniumBase wasn't detected", duration=4) diff --git a/examples/sb_fixture_tests.py b/examples/sb_fixture_tests.py new file mode 100644 index 00000000000..98448a159c2 --- /dev/null +++ b/examples/sb_fixture_tests.py @@ -0,0 +1,29 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +# "sb" pytest fixture test in a method with no class +def test_sb_fixture_with_no_class(sb: BaseCase): + sb.goto("seleniumbase.io/simple/login") + sb.type("#username", "demo_user") + sb.type("#password", "secret_pass") + sb.click('a:contains("Sign in")') + sb.assert_exact_text("Welcome!", "h1") + sb.assert_element("img#image1") + sb.highlight("#image1") + sb.click_link("Sign out") + sb.assert_text("signed out", "#top_message") + + +# "sb" pytest fixture test in a method inside a class +class Test_SB_Fixture: + def test_sb_fixture_inside_class(self, sb: BaseCase): + sb.goto("seleniumbase.io/simple/login") + sb.type("#username", "demo_user") + sb.type("#password", "secret_pass") + sb.click('a:contains("Sign in")') + sb.assert_exact_text("Welcome!", "h1") + sb.assert_element("img#image1") + sb.highlight("#image1") + sb.click_link("Sign out") + sb.assert_text("signed out", "#top_message") diff --git a/examples/setup.cfg b/examples/setup.cfg old mode 100755 new mode 100644 index f77b1f966db..98d235b5ef0 --- a/examples/setup.cfg +++ b/examples/setup.cfg @@ -1,6 +1,15 @@ +[flake8] +# W503 (line break before binary operator) can be ignored. +exclude=recordings,temp +ignore=W503 + [nosetests] -# This is the config file for default values used during nosetest runs -# nocapture=1 displays print statements from output. Undo this by using: --nologcapture -# logging-level=INFO keeps the logs much cleaner than using DEBUG +# nocapture=1 (Display print statements from output) +# (Undo this by using: "--nologcapture") +# logging-level=INFO (Shorter logs than using DEBUG) nocapture=1 -logging-level=INFO \ No newline at end of file +logging-level=INFO + +[behave] +show_skipped=false +show_timings=false diff --git a/examples/shadow_root_test.py b/examples/shadow_root_test.py new file mode 100644 index 00000000000..275febcf980 --- /dev/null +++ b/examples/shadow_root_test.py @@ -0,0 +1,23 @@ +"""Piercing through shadow-root elements with the "::shadow" selector. +To confirm that "::shadow" works, print text and assert exact text.""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class ShadowRootTest(BaseCase): + def test_shadow_root(self): + if self.recorder_mode or not self.is_chromium(): + self.goto_if_not_url("about:blank") + print("\n Unsupported mode for this test.") + self.skip("Unsupported mode for this test.") + self.goto("https://seleniumbase.io/other/shadow_dom") + print("") + self.click("button.tab_1") + print(self.get_text("fancy-tabs::shadow #panels")) + self.assert_exact_text("Content Panel 1", "fancy-tabs::shadow #panels") + self.click("button.tab_2") + print(self.get_text("fancy-tabs::shadow #panels")) + self.assert_exact_text("Content Panel 2", "fancy-tabs::shadow #panels") + self.click("button.tab_3") + print(self.get_text("fancy-tabs::shadow #panels")) + self.assert_exact_text("Content Panel 3", "fancy-tabs::shadow #panels") diff --git a/examples/swag_labs_suite.py b/examples/swag_labs_suite.py deleted file mode 100755 index ca0cfca0b68..00000000000 --- a/examples/swag_labs_suite.py +++ /dev/null @@ -1,89 +0,0 @@ -import pytest -from parameterized import parameterized -from seleniumbase import BaseCase - - -class SwagLabsTests(BaseCase): - - def login(self, user="standard_user"): - """ Login to Swag Labs and assert that the login was successful. """ - if user not in (["standard_user", "problem_user"]): - raise Exception("Invalid user!") - self.open("https://www.saucedemo.com/") - self.update_text("#user-name", user) - self.update_text("#password", "secret_sauce") - self.click('input[type="submit"]') - self.assert_text("Products", "div.product_label") - self.assert_element("#inventory_container") - - @parameterized.expand([ - ["standard_user"], - ["problem_user"], - ]) - @pytest.mark.run(order=1) - def test_swag_labs_basic_functional_flow(self, user): - """ This test checks for basic functional flow in the Swag Labs store. - The test is parameterized, and receives the user to use for login. - """ - self.login(user) - - # Verify that the "Test.allTheThings() T-Shirt" appears on the page - item_name = "Test.allTheThings() T-Shirt" - self.assert_text(item_name) - - # Verify that a reverse-alphabetical sort works as expected - self.select_option_by_value("select.product_sort_container", "za") - if item_name not in self.get_text("div.inventory_item"): - raise Exception('Sort Failed! Expecting "%s" on top!' % item_name) - - # Add the "Test.allTheThings() T-Shirt" to the cart - self.assert_exact_text("ADD TO CART", "button.btn_inventory") - item_price = self.get_text("div.inventory_item_price") - self.click("button.btn_inventory") - self.assert_exact_text("REMOVE", "button.btn_inventory") - self.assert_exact_text("1", "span.shopping_cart_badge") - - # Verify your cart - self.click("#shopping_cart_container path") - self.assert_exact_text("Your Cart", "div.subheader") - self.assert_text(item_name, "div.inventory_item_name") - self.assert_exact_text("1", "div.cart_quantity") - self.assert_exact_text("REMOVE", "button.cart_button") - self.assert_element("link=CONTINUE SHOPPING") - - # Checkout - Add info - self.click("link=CHECKOUT") - self.assert_exact_text("Checkout: Your Information", "div.subheader") - self.assert_element("a.cart_cancel_link") - self.update_text("#first-name", "SeleniumBase") - self.update_text("#last-name", "Rocks") - self.update_text("#postal-code", "01720") - - # Checkout - Overview - self.click("input.btn_primary") - self.assert_exact_text("Checkout: Overview", "div.subheader") - self.assert_element("link=CANCEL") - self.assert_text(item_name, "div.inventory_item_name") - self.assert_text(item_price, "div.inventory_item_price") - self.assert_exact_text("1", "div.summary_quantity") - - # Finish Checkout and verify item is no longer in cart - self.click("link=FINISH") - self.assert_exact_text("THANK YOU FOR YOUR ORDER", "h2") - self.assert_element("div.pony_express") - self.click("#shopping_cart_container path") - self.assert_element_absent("div.inventory_item_name") - self.click("link=CONTINUE SHOPPING") - self.assert_element_absent("span.shopping_cart_badge") - - @parameterized.expand([ - ["standard_user"], - ["problem_user"], - ]) - @pytest.mark.run(order=2) - def test_swag_labs_products_page_resource_verification(self, user): - """ This test checks for 404 errors on the Swag Labs products page. - The test is parameterized, and receives the user to use for login. - """ - self.login(user) - self.assert_no_404_errors() diff --git a/examples/swag_labs_user_tests.py b/examples/swag_labs_user_tests.py new file mode 100644 index 00000000000..3ac90263a76 --- /dev/null +++ b/examples/swag_labs_user_tests.py @@ -0,0 +1,94 @@ +from parameterized import parameterized +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class SwagLabsTests(BaseCase): + def login_to_swag_labs(self, username="standard_user"): + """Login to Swag Labs and verify success.""" + url = "https://www.saucedemo.com" + self.goto(url) + self.wait_for_element("div.login_logo") + if username not in self.get_text("#login_credentials"): + self.fail("Invalid user for login: %s" % username) + self.type("#user-name", username) + self.type("#password", "secret_sauce") + self.click('input[type="submit"]') + self.assert_element("div.inventory_list") + self.assert_element('div:contains("Sauce Labs Backpack")') + + @parameterized.expand( + [ + ["standard_user"], + ["problem_user"], + ] + ) + def test_swag_labs_user_flows(self, username): + """This parameterized test checks basic user actions on the website. + It also shows you how SeleniumBase can help you catch bugs.""" + self.login_to_swag_labs(username=username) + if username == "problem_user": + print("\n(This test should fail)") + + # Verify that the "Test.allTheThings() T-Shirt" appears on the page + item_name = "Test.allTheThings() T-Shirt" + self.assert_text(item_name) + + # Verify that a reverse-alphabetical sort works as expected + self.select_option_by_value("select.product_sort_container", "za") + if item_name not in self.get_text("div.inventory_item"): + self.fail('Sort Failed! Expecting "%s" on top!' % item_name) + + # Add the "Test.allTheThings() T-Shirt" to the cart + self.assert_exact_text("Add to cart", "button.btn_inventory") + item_price = self.get_text("div.inventory_item_price") + self.click("button.btn_inventory") + self.assert_exact_text("Remove", "button.btn_inventory") + self.assert_exact_text("1", "span.shopping_cart_badge") + + # Verify your cart + self.click("#shopping_cart_container a") + self.assert_element('span:contains("Your Cart")') + self.assert_text(item_name, "div.inventory_item_name") + self.assert_exact_text("1", "div.cart_quantity") + self.assert_exact_text("Remove", "button.cart_button") + self.assert_element("button#continue-shopping") + + # Checkout - Add info + self.click("button#checkout") + self.assert_element('span:contains("Checkout: Your Information")') + self.assert_element("button#cancel") + self.type("#first-name", "SeleniumBase") + self.type("#last-name", "Rocks") + self.type("#postal-code", "01720") + + # Checkout - Overview + self.click("input#continue") + self.assert_element('span:contains("Checkout: Overview")') + self.assert_element("button#cancel") + self.assert_text(item_name, "div.inventory_item_name") + self.assert_text(item_price, "div.inventory_item_price") + self.assert_exact_text("1", "div.cart_quantity") + + # Finish Checkout and verify that the cart is now empty + self.click("button#finish") + self.assert_exact_text("Thank you for your order!", "h2") + self.assert_element("img.pony_express") + self.click("#shopping_cart_container a") + self.assert_element_absent("div.inventory_item_name") + self.click("button#continue-shopping") + self.assert_element_absent("span.shopping_cart_badge") + + def tearDown(self): + self.save_teardown_screenshot() # Only if a test fails + # Reset App State and Logout if the controls are present + try: + self.wait_for_ready_state_complete() + if self.is_element_visible("#react-burger-menu-btn"): + self.click("#react-burger-menu-btn") + self.wait_for_element("a#reset_sidebar_link") + self.js_click_if_present("a#reset_sidebar_link") + self.js_click_if_present("a#logout_sidebar_link") + except Exception: + pass + super().tearDown() diff --git a/examples/test_3d_apis.py b/examples/test_3d_apis.py new file mode 100644 index 00000000000..75d73811042 --- /dev/null +++ b/examples/test_3d_apis.py @@ -0,0 +1,17 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__, "--enable-3d-apis") + + +class ThreeJSTests(BaseCase): + def test_animation(self): + if self.headless: + self.goto_if_not_url("about:blank") + self.skip("Skip this test in headless mode!") + if self.is_chromium() and not self.enable_3d_apis: + self.get_new_driver(enable_3d_apis=True) # --enable-3d-apis + url = "https://threejs.org/examples/#webgl_animation_skinning_morph" + self.goto(url) + self.switch_to_frame("iframe#viewer") + self.sleep(0.8) + self.click('button:contains("Wave")') + self.sleep(3) diff --git a/examples/test_apple_site.py b/examples/test_apple_site.py new file mode 100644 index 00000000000..86fcd69d998 --- /dev/null +++ b/examples/test_apple_site.py @@ -0,0 +1,36 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class AppleTests(BaseCase): + def test_apple_developer_site_webdriver_instructions(self): + if self.headed: + self.demo_mode = True + self.demo_sleep = 0.5 + self.message_duration = 2.0 + if self.is_chromium() and not self.disable_csp: + self.get_new_driver(browser=self.browser, disable_csp=True) + if self.headless: + if self._multithreaded or self.undetectable or self.recorder_mode: + self.goto_if_not_url("about:blank") + print("\n Unsupported mode for this test.") + self.skip("Unsupported mode for this test.") + elif self.is_chromium(): + self.get_new_driver(browser=self.browser, headless2=True) + self.goto("https://developer.apple.com/search/") + title = "Testing with WebDriver in Safari" + self.type("input#searchField", title + "\n") + self.click("link=%s" % title) + self.assert_element("nav.documentation-nav") + self.assert_text(title, "h1") + self.assert_text("Enable WebDriver and run a test.", "div.abstract") + if self.demo_mode: + self.highlight("div.content h2") + else: + self.assert_element("div.content h2") + h3 = "div.content h3:nth-of-type(%s)" + self.assert_text("Make Sure You Have Safariโ€™s WebDriver", h3 % "1") + self.assert_text("Get the Correct Selenium Library", h3 % "2") + self.assert_text("Configure Safari to Enable WebDriver", h3 % "3") + self.assert_text("Write a WebDriver Testing Suite", h3 % "4") + self.assert_text("Run Your Test", h3 % "5") diff --git a/examples/test_assert_elements.py b/examples/test_assert_elements.py new file mode 100644 index 00000000000..ef9bf089277 --- /dev/null +++ b/examples/test_assert_elements.py @@ -0,0 +1,14 @@ +"""Assert that multiple elements are present or visible: +HTML Presence: assert_elements_present() +HTML Visibility: assert_elements() <> assert_elements_visible()""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class ListAssertTests(BaseCase): + def test_assert_list_of_elements(self): + self.goto("https://seleniumbase.io/demo_page") + self.assert_elements_present("head", "style", "script") + self.assert_elements("h1", "h2", "h3") + my_list = ["#myDropdown", "#myButton", "#mySlider"] + self.assert_elements(my_list) diff --git a/examples/test_calculator.py b/examples/test_calculator.py new file mode 100644 index 00000000000..4bf3fc6639e --- /dev/null +++ b/examples/test_calculator.py @@ -0,0 +1,16 @@ +"""Test the SeleniumBase Calculator App""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class CalculatorTests(BaseCase): + def test_6_times_7_plus_12_equals_54(self): + self.goto("seleniumbase.io/apps/calculator") + self.click('button[id="6"]') + self.click("button#multiply") + self.click('button[id="7"]') + self.click("button#add") + self.click('button[id="1"]') + self.click('button[id="2"]') + self.click("button#equal") + self.assert_exact_text("54", "input#output") diff --git a/examples/test_canvas.py b/examples/test_canvas.py new file mode 100644 index 00000000000..34e1f291b21 --- /dev/null +++ b/examples/test_canvas.py @@ -0,0 +1,49 @@ +"""Use SeleniumBase methods to interact with "canvas" elements.""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class CanvasTests(BaseCase): + def get_canvas_pixel_colors_at_top_left(self): + # Return the RGB colors of the canvas's top left pixel + x = 0 + y = 0 + if self.browser == "safari": + x = 1 + y = 1 + color = self.execute_script( + "return document.querySelector('canvas').getContext('2d')" + ".getImageData(%s,%s,1,1).data;" % (x, y) + ) + if self.is_chromium(): + return [color[0], color[1], color[2]] + else: + return [color["0"], color["1"], color["2"]] + + def test_canvas_click_from_center(self): + self.goto("https://seleniumbase.io/other/canvas") + self.assert_title_contains("Canvas") + self.click_with_offset("canvas", 0, 0, mark=True, center=True) + self.sleep(0.55) # Not needed (Lets you see the alert pop up) + alert = self.switch_to_alert() + self.assert_equal(alert.text, "You clicked on the square!") + self.accept_alert() + self.sleep(0.55) # Not needed (Lets you see the alert go away) + if self.browser == "safari" and self._reuse_session: + # Alerts can freeze Safari if reusing the browser session + self.driver.quit() + + def test_click_with_offset(self): + self.goto("https://seleniumbase.io/canvas/") + if self.undetectable: + self.goto_if_not_url("about:blank") + print("\n Skip this test in undetectable mode.") + self.skip("Skip this test in undetectable mode.") + self.assert_title_contains("Canvas") + self.highlight("canvas") + rgb = self.get_canvas_pixel_colors_at_top_left() + self.assert_equal(rgb, [221, 242, 231]) # Looks greenish + self.click_with_offset("canvas", 500, 350) + self.highlight("canvas", loops=5) + rgb = self.get_canvas_pixel_colors_at_top_left() + self.assert_equal(rgb, [39, 42, 56]) # Blue by hamburger diff --git a/examples/test_cdp_ad_blocking.py b/examples/test_cdp_ad_blocking.py new file mode 100644 index 00000000000..652c9db694e --- /dev/null +++ b/examples/test_cdp_ad_blocking.py @@ -0,0 +1,37 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class CDPNetworkBlockingTests(BaseCase): + def test_cdp_network_blocking(self): + self.goto("about:blank") + if self._reuse_session or not self.is_chromium(): + message = "Skipping test if reusing session or not Chromium!" + print(message) + self.skip(message) + self.execute_cdp_cmd("Network.enable", {}) + self.execute_cdp_cmd( + "Network.setBlockedURLs", {"urls": [ + "*.googlesyndication.com*", + "*.googletagmanager.com*", + "*.google-analytics.com*", + "*.amazon-adsystem.com*", + "*.adsafeprotected.com*", + "*.doubleclick.net*", + "*.fastclick.net*", + "*.snigelweb.com*", + "*.viously.com*", + "*.flashb.id*", + "*.2mdn.net*", + ]}) + self.goto("https://www.w3schools.com/jquery/default.asp") + source = self.get_page_source() + try: + self.assert_false("doubleclick.net" in source) + self.assert_false("google-analytics.com" in source) + except Exception: + self.refresh() + source = self.get_page_source() + self.assert_false("doubleclick.net" in source) + self.assert_false("google-analytics.com" in source) + self.post_message("Blocking was successful!") diff --git a/examples/test_checkboxes.py b/examples/test_checkboxes.py new file mode 100644 index 00000000000..2a9468565d5 --- /dev/null +++ b/examples/test_checkboxes.py @@ -0,0 +1,20 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class CheckboxTests(BaseCase): + def test_checkboxes_and_radio_buttons(self): + self.goto("https://seleniumbase.io/w3schools/checkboxes") + self.click("button#runbtn") + self.switch_to_frame("iframeResult") + checkbox = "input#vehicle2" + self.assert_false(self.is_selected(checkbox)) + self.click(checkbox) + self.assert_true(self.is_selected(checkbox)) + self.goto("https://seleniumbase.io/w3schools/radio_buttons") + self.click("button#runbtn") + self.switch_to_frame("iframeResult") + option_button = "input#css" + self.assert_false(self.is_selected(option_button)) + self.click(option_button) + self.assert_true(self.is_selected(option_button)) diff --git a/examples/test_chinese_pdf.py b/examples/test_chinese_pdf.py new file mode 100644 index 00000000000..c5d4d4a13db --- /dev/null +++ b/examples/test_chinese_pdf.py @@ -0,0 +1,20 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class ChinesePDFTests(BaseCase): + def test_chinese_pdf(self): + self.goto("data:,") + pdf = "https://seleniumbase.io/cdn/pdf/unittest_zh.pdf" + + # Get and print PDF text + pdf_text = self.get_pdf_text(pdf, page=2, nav=True) + print("\n" + pdf_text) + + # Assert PDF contains the expected text on Page 2 + self.assert_pdf_text(pdf, "ไธชๆต‹่ฏ•็ฑป", page=2) + + # Assert PDF contains the expected text on any of the pages + self.assert_pdf_text(pdf, "่ฟ่กŒๅ•ๅ…ƒๆต‹่ฏ•") + self.assert_pdf_text(pdf, "็ญ‰ๅพ…ๆต‹่ฏ•็ป“ๆŸๅŽๆ˜พ็คบๆ‰€ๆœ‰็ป“ๆžœ") + self.assert_pdf_text(pdf, "ๆต‹่ฏ•็š„ๆ‰ง่กŒ่ทŸๆ–นๆณ•็š„้กบๅบๆฒกๆœ‰ๅ…ณ็ณป") diff --git a/examples/test_chromedriver.py b/examples/test_chromedriver.py new file mode 100644 index 00000000000..dbaedad5c85 --- /dev/null +++ b/examples/test_chromedriver.py @@ -0,0 +1,54 @@ +"""This test is only for Chrome! +(Verify that your chromedriver is compatible with your version of Chrome.)""" +import colorama +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class ChromedriverTests(BaseCase): + def test_chromedriver_matches_chrome(self): + self.goto("about:blank") + if self.browser != "chrome": + print("\n This test is only for Chrome!") + self.skip("This test is only for Chrome!") + chrome_version = self.get_chrome_version() + major_chrome_version = chrome_version.split(".")[0] + chromedriver_version = self.get_chromedriver_version() + major_chromedriver_version = chromedriver_version.split(".")[0] + c1 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + c4 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + c5 = colorama.Fore.RED + colorama.Back.LIGHTGREEN_EX + cr = colorama.Style.RESET_ALL + pr_chromedriver_version = c3 + chromedriver_version + cr + pr_chrome_version = c2 + chrome_version + cr + message = ( + "\n" + "* Your version of chromedriver is: %s\n" + "*\n* And your version of Chrome is: %s" + % (pr_chromedriver_version, pr_chrome_version) + ) + print(message) + if major_chromedriver_version < major_chrome_version: + install_sb = ( + "seleniumbase get chromedriver %s" % major_chrome_version + ) + pr_install_sb = c1 + install_sb + cr + up_msg = "You may want to upgrade your version of chromedriver:" + up_msg = c4 + up_msg + cr + message = "*\n* %s\n*\n* >>> %s" % (up_msg, pr_install_sb) + print(message) + elif major_chromedriver_version > major_chrome_version: + up_msg = "You may want to upgrade your version of Chrome:" + up_msg = c5 + up_msg + cr + up_url = c1 + "chrome://settings/help" + cr + message = "*\n* %s\n*\n* See: %s" % (up_msg, up_url) + print(message) + else: + up_msg = ( + "Success! Your chromedriver is compatible with your Chrome!" + ) + up_msg = c1 + up_msg + cr + message = "*\n* %s\n" % up_msg + print(message) diff --git a/examples/test_coffee_cart.py b/examples/test_coffee_cart.py new file mode 100644 index 00000000000..789295de2bb --- /dev/null +++ b/examples/test_coffee_cart.py @@ -0,0 +1,23 @@ +"""Use SeleniumBase to test the Coffee Cart App.""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class CoffeeCartTest(BaseCase): + def test_coffee_cart(self): + self.goto("https://seleniumbase.io/coffee/") + self.assert_title("Coffee Cart") + self.assert_element('button:contains("Total: $0.00")') + self.click('div[data-sb="Cappuccino"]') + self.assert_exact_text("cart (1)", 'a[aria-label="Cart page"]') + self.click('div[data-sb="Flat-White"]') + self.assert_exact_text("cart (2)", 'a[aria-label="Cart page"]') + self.click('div[data-sb="Cafe-Latte"]') + self.assert_exact_text("cart (3)", 'a[aria-label="Cart page"]') + self.click('a[aria-label="Cart page"]') + self.assert_exact_text("Total: $53.00", "button.pay") + self.click("button.pay") + self.type("input#name", "Selenium Coffee") + self.type("input#email", "test@test.test") + self.click("button#submit-payment") + self.assert_text("Thanks for your purchase.", "#app .success") diff --git a/examples/test_console_logging.py b/examples/test_console_logging.py new file mode 100644 index 00000000000..c94682faab4 --- /dev/null +++ b/examples/test_console_logging.py @@ -0,0 +1,15 @@ +"""Use SeleniumBase methods to interact with console logs.""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class TestConsoleLogging(BaseCase): + def test_console_logging(self): + self.goto("https://seleniumbase.io/demo_page") + self.wait_for_element_visible("h2") + self.start_recording_console_logs() + self.console_log_string("Hello World!") + self.console_log_script('document.querySelector("h2").textContent') + console_logs = [log[0] for log in self.get_recorded_console_logs()] + self.assert_in("Hello World!", console_logs) + self.assert_in("SeleniumBase", console_logs) diff --git a/examples/test_contains_selector.py b/examples/test_contains_selector.py new file mode 100644 index 00000000000..99583f195b5 --- /dev/null +++ b/examples/test_contains_selector.py @@ -0,0 +1,13 @@ +"""TAG:contains("TEXT") is a special, non-standard CSS Selector +that gets converted to XPath: '//TAG[contains(., "TEXT")]' +before it's used by Selenium calls. Also part of jQuery.""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class ContainsSelectorTests(BaseCase): + def test_contains_selector(self): + self.goto("https://xkcd.com/2207/") + self.assert_element('div.box div:contains("Math Work")') + self.click('a:contains("Next")') + self.assert_element('div div:contains("Drone Fishing")') diff --git a/examples/test_cycle_elements.py b/examples/test_cycle_elements.py new file mode 100644 index 00000000000..37034caa16e --- /dev/null +++ b/examples/test_cycle_elements.py @@ -0,0 +1,16 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class CycleTests(BaseCase): + def test_cycle_elements_with_tab_and_press_enter(self): + """ + Test pressing the tab key to cycle through elements. + Then click on the active element and verify actions. + This can all be performed by using a single command. + The "\t" is the tab key. The "\n" is the RETURN key. + """ + self.goto("seleniumbase.io/demo_page") + self.assert_text("This Text is Green", "#pText") + self.send_keys("html", "\t\t\t\t\n") + self.assert_text("This Text is Purple", "#pText") diff --git a/examples/test_decryption.py b/examples/test_decryption.py new file mode 100644 index 00000000000..7311a529138 --- /dev/null +++ b/examples/test_decryption.py @@ -0,0 +1,21 @@ +"""This test demonstrates the use of password encryption/decryption. +(Technically considered to be obfuscation/unobfuscation.)""" +from seleniumbase import BaseCase +from seleniumbase import encryption +BaseCase.main(__name__, __file__) + + +class DecryptionTests(BaseCase): + def test_decrypt_password(self): + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + + encrypted_password = "$^*ENCRYPT=S3BDTAdCWzMmKEY8Gjg=?&#$" + print("\nEncrypted Password = %s" % encrypted_password) + password = encryption.decrypt(encrypted_password) + print("Decrypted Password = %s" % password) + self.type("#password", password) + + self.click('input[type="submit"]') + self.assert_element("#inventory_container") + self.assert_element('div:contains("Sauce Labs Backpack")') diff --git a/examples/test_deferred_asserts.py b/examples/test_deferred_asserts.py new file mode 100644 index 00000000000..4028463b227 --- /dev/null +++ b/examples/test_deferred_asserts.py @@ -0,0 +1,23 @@ +"""This test shows the use of SeleniumBase deferred asserts. +Deferred asserts won't raise exceptions from failures until +process_deferred_asserts() is called, or the test completes.""" +import pytest +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class DeferredAssertTests(BaseCase): + @pytest.mark.expected_failure + def test_deferred_asserts(self): + self.goto("https://xkcd.com/993/") + self.wait_for_element("#comic") + print("\n(This test should fail)") + self.deferred_assert_element('img[alt="Brand Identity"]') + self.deferred_assert_element('img[alt="Rocket Ship"]') # Will Fail + self.deferred_assert_element("#comicmap") + self.deferred_assert_text("Fake Item", "ul.comicNav") # Will Fail + self.deferred_assert_text("Random", "ul.comicNav") + self.deferred_assert_element('a[name="Super Fake !!!"]') # Will Fail + self.deferred_assert_exact_text("Brand Identity", "#ctitle") + self.deferred_assert_exact_text("Fake Food", "#comic") # Will Fail + self.process_deferred_asserts() diff --git a/examples/test_demo_site.py b/examples/test_demo_site.py new file mode 100644 index 00000000000..188945e35f2 --- /dev/null +++ b/examples/test_demo_site.py @@ -0,0 +1,119 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class DemoSiteTests(BaseCase): + def test_demo_site(self): + # Open a web page in the active browser window + self.goto("https://seleniumbase.io/demo_page") + + # Assert the title of the current web page + self.assert_title("Web Testing Page") + + # Assert that an element is visible on the page + self.assert_element("tbody#tbodyId") + + # Assert that a text substring appears in an element + self.assert_text("Demo Page", "h1") + + # Type text into various text fields and then assert + self.type("#myTextInput", "This is Automated") + self.type("textarea.area1", "Testing Time!\n") + self.type('[name="preText2"]', "Typing Text!") + self.assert_text("This is Automated", "#myTextInput") + self.assert_text("Testing Time!\n", "textarea.area1") + self.assert_text("Typing Text!", '[name="preText2"]') + + # Hover & click a dropdown element and assert results + self.assert_text("Automation Practice", "h3") + try: + self.hover_and_click("#myDropdown", "#dropOption2", timeout=1) + except Exception: + # Someone probably moved the mouse while the test ran + self.hover_and_js_click("#myDropdown", "#dropOption2") + self.assert_text("Link Two Selected", "h3") + + # Click a button and then verify the expected results + self.assert_text("This Text is Green", "#pText") + self.click('button:contains("Click Me")') + self.assert_text("This Text is Purple", "#pText") + + # Assert that the given SVG is visible on the page + self.assert_element('svg[name="svgName"]') + + # Verify that a slider control updates a progress bar + self.assert_element('progress[value="50"]') + self.set_value("input#mySlider", "100") + self.assert_element('progress[value="100"]') + + # Verify that a "select" option updates a meter bar + self.assert_element('meter[value="0.25"]') + self.select_option_by_text("#mySelect", "Set to 75%") + self.assert_element('meter[value="0.75"]') + + # Assert an element located inside an iframe + self.assert_false(self.is_element_visible("img")) + self.switch_to_frame("#myFrame1") + self.assert_true(self.is_element_visible("img")) + self.switch_to_default_content() + + # Assert text located inside an iframe + self.assert_false(self.is_text_visible("iFrame Text")) + self.switch_to_frame("#myFrame2") + self.assert_true(self.is_text_visible("iFrame Text")) + self.switch_to_default_content() + + # Verify that clicking a radio button selects it + self.assert_false(self.is_selected("#radioButton2")) + self.click("#radioButton2") + self.assert_true(self.is_selected("#radioButton2")) + + # Verify that clicking a checkbox makes it selected + self.assert_element_not_visible("img#logo") + self.assert_false(self.is_selected("#checkBox1")) + self.click("#checkBox1") + self.assert_true(self.is_selected("#checkBox1")) + self.assert_element("img#logo") + + # Verify clicking on multiple elements with one call + self.assert_false(self.is_selected("#checkBox2")) + self.assert_false(self.is_selected("#checkBox3")) + self.assert_false(self.is_selected("#checkBox4")) + self.click_visible_elements("input.checkBoxClassB") + self.assert_true(self.is_selected("#checkBox2")) + self.assert_true(self.is_selected("#checkBox3")) + self.assert_true(self.is_selected("#checkBox4")) + + # Verify that clicking an iframe checkbox selects it + self.assert_false(self.is_element_visible(".fBox")) + self.switch_to_frame("#myFrame3") + self.assert_true(self.is_element_visible(".fBox")) + self.assert_false(self.is_selected(".fBox")) + self.click(".fBox") + self.assert_true(self.is_selected(".fBox")) + self.switch_to_default_content() + + # Verify Drag and Drop + self.assert_element_not_visible("div#drop2 img#logo") + self.drag_and_drop("img#logo", "div#drop2") + self.assert_element("div#drop2 img#logo") + + # Assert link text + self.assert_link_text("seleniumbase.com") + self.assert_link_text("SeleniumBase on GitHub") + self.assert_link_text("seleniumbase.io") + + # Click link text + self.click_link("SeleniumBase Demo Page") + + # Assert exact text + self.assert_exact_text("Demo Page", "h1") + + # Highlight a page element (Also asserts visibility) + self.highlight("h2") + + # Actions with Demo Mode enabled + if self.headed: + self.activate_demo_mode() + self.type("input", "Have a Nice Day!") + self.assert_text("SeleniumBase", "h2") diff --git a/examples/test_detect_404s.py b/examples/test_detect_404s.py new file mode 100644 index 00000000000..1fbc96b6866 --- /dev/null +++ b/examples/test_detect_404s.py @@ -0,0 +1,9 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class BrokenLinkTests(BaseCase): + def test_link_checking(self): + self.goto("https://seleniumbase.io/other/broken_page.html") + print("\n(This test should fail)") + self.assert_no_404_errors() diff --git a/examples/test_docs_site.py b/examples/test_docs_site.py new file mode 100644 index 00000000000..c938341e493 --- /dev/null +++ b/examples/test_docs_site.py @@ -0,0 +1,22 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class DocsSiteTests(BaseCase): + def test_docs(self): + self.goto("https://seleniumbase.io/help_docs/customizing_test_runs/") + self.assert_text("Command Line Options", "h1") + self.js_click('a[href$="/examples/example_logs/ReadMe/"]') + self.assert_text("Dashboard / Reports", "h1") + self.js_click('a[href$="/seleniumbase/console_scripts/ReadMe/"]') + self.assert_text("Console Scripts", "h1") + self.js_click('a[href$="/help_docs/syntax_formats/"]') + self.assert_text("Syntax Formats", "h1") + self.js_click('a[href$="/recorder_mode/"]') + self.assert_text("Recorder Mode", "h1") + self.js_click('a[href$="/method_summary/"]') + self.assert_text("API Reference", "h1") + self.js_click('a[href$="/uc_mode/"]') + self.assert_text("UC Mode", "h1") + self.js_click('img[alt="logo"]') + self.assert_element('img[alt="SeleniumBase"]') diff --git a/examples/test_double_click.py b/examples/test_double_click.py new file mode 100644 index 00000000000..f62d9dace87 --- /dev/null +++ b/examples/test_double_click.py @@ -0,0 +1,21 @@ +"""Test double_click() after switching into iframes.""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class DoubleClickTests(BaseCase): + def test_switch_to_frame_and_double_click(self): + self.goto("https://seleniumbase.io/w3schools/double_click") + self.assert_title("Double Click Testing") + self.click("button#runbtn") + self.switch_to_frame("iframe#iframeResult") + self.double_click('[ondblclick="myFunction()"]') + self.assert_text("Hello World", "#demo") + + def test_switch_to_frame_of_element_and_double_click(self): + self.goto("https://seleniumbase.io/w3schools/double_click") + self.assert_title("Double Click Testing") + self.click("button#runbtn") + self.switch_to_frame_of_element('[ondblclick="myFunction()"]') + self.double_click('[ondblclick="myFunction()"]') + self.assert_text("Hello World", "#demo") diff --git a/examples/test_download_files.py b/examples/test_download_files.py new file mode 100644 index 00000000000..e62b1207edf --- /dev/null +++ b/examples/test_download_files.py @@ -0,0 +1,106 @@ +"""Use SeleniumBase to download files and verify.""" +import math +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class DownloadTests(BaseCase): + def test_download_chromedriver_notes(self): + if self._multithreaded: + self.goto_if_not_url("about:blank") + self.skip("Skipping test in multi-threaded mode.") + self.goto("https://chromedriver.chromium.org/downloads") + notes_file = "notes.txt" + notes_link = ( + "https://chromedriver.storage.googleapis.com" + "/101.0.4951.41/%s" % notes_file + ) + self.download_file(notes_link) + self.assert_downloaded_file(notes_file) + notes_path = self.get_path_of_downloaded_file(notes_file) + with open(notes_path, "r") as f: + notes_data = f.read() + self.assert_true(len(notes_data) > 100) # Verify file not empty + text = "Switching to nested frame fails with chrome/chromedriver 100" + self.assert_in(text, notes_data) # Verify file has expected data + + def test_download_files_from_pypi(self): + self.goto("https://pypi.org/project/sbvirtualdisplay/#files") + self.assert_element("span#pip-command") + self.assert_text("Download files", "div#files h2.page-title") + self.assert_text("Download files", "a#files-tab") + pkg_header = self.get_text('h1[class*="header__name"]').strip() + pkg_name = pkg_header.replace(" ", "-") + whl_file = pkg_name + "-py3-none-any.whl" + tar_gz_file = pkg_name + ".tar.gz" + + # Click the links to download the files into: "./downloaded_files/" + # (If using Safari, IE, or Chromium Guest Mode: download directly.) + # (The default Downloads Folder can't be changed when using those.) + # (Use self.get_browser_downloads_folder() to get the folder used.) + whl_selector = 'div#files a[href$="%s"]' % whl_file + tar_selector = 'div#files a[href$="%s"]' % tar_gz_file + if ( + self.browser == "safari" + or self.browser == "ie" + or self.browser == "edge" + or (self.is_chromium() and self.guest_mode) + or (self.is_chromium() and (self.headless or self.headless2)) + ): + whl_href = self.get_attribute(whl_selector, "href") + tar_href = self.get_attribute(tar_selector, "href") + self.download_file(whl_href) + self.download_file(tar_href) + else: + self.js_click(whl_selector) # Download the "whl" file + self.sleep(0.1) + self.js_click(tar_selector) # Download the "tar" file + + # Verify that the downloaded files appear in the [Downloads Folder] + # (This only guarantees that the exact file name is in the folder.) + # (This does not guarantee that the downloaded files are complete.) + # (Later, we'll check that the files were downloaded successfully.) + self.assert_downloaded_file(whl_file) + self.assert_downloaded_file(tar_gz_file) + + self.sleep(1) # Add more time to make sure downloads have completed + + # Get the actual size of the downloaded files (in bytes) + whl_path = self.get_path_of_downloaded_file(whl_file) + with open(whl_path, "rb") as f: + whl_file_bytes = len(f.read()) + print("\n%s | Download = %s bytes." % (whl_file, whl_file_bytes)) + tar_gz_path = self.get_path_of_downloaded_file(tar_gz_file) + with open(tar_gz_path, "rb") as f: + tar_gz_file_bytes = len(f.read()) + print("%s | Download = %s bytes." % (tar_gz_file, tar_gz_file_bytes)) + + # Check to make sure the downloaded files are not empty or too small + self.assert_true(whl_file_bytes > 5000) + self.assert_true(tar_gz_file_bytes > 5000) + + # Get file sizes in kB to compare actual values with displayed values + whl_file_kb = whl_file_bytes / 1000.0 + whl_line_fi = self.get_text('a[href$=".whl"]').strip() + whl_line = self.get_text('div.file:contains("%s")' % whl_line_fi) + whl_display_kb = float(whl_line.split("(")[1].split(" ")[0]) + tar_gz_file_kb = tar_gz_file_bytes / 1000.0 + tar_gz_line_fi = self.get_text('a[href$=".tar.gz"]').strip() + tar_gz_line = self.get_text('div.file:contains("%s")' % tar_gz_line_fi) + tar_gz_display_kb = float(tar_gz_line.split("(")[1].split(" ")[0]) + + # Verify downloaded files are the correct size (account for rounding) + self.assert_true( + abs(math.floor(whl_file_kb) - math.floor(whl_display_kb)) < 2 + ) + self.assert_true( + abs(math.floor(tar_gz_file_kb) - math.floor(tar_gz_display_kb)) < 2 + ) + + # Delete the downloaded files from the [Downloads Folder] + self.delete_downloaded_file_if_present(whl_file) + self.delete_downloaded_file_if_present(tar_gz_file) + + # Verify that the downloaded files have been successfully deleted + self.assert_false(self.is_downloaded_file_present(whl_file)) + self.assert_false(self.is_downloaded_file_present(tar_gz_file)) diff --git a/examples/test_download_images.py b/examples/test_download_images.py new file mode 100644 index 00000000000..3f76999b824 --- /dev/null +++ b/examples/test_download_images.py @@ -0,0 +1,58 @@ +"""Use SeleniumBase to download images and verify.""" +import os +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class DownloadImages(BaseCase): + def test_download_images_directly(self): + if self._multithreaded: + self.goto_if_not_url("about:blank") + self.skip("Skipping test in multi-threaded mode.") + self.goto("seleniumbase.io/examples/chart_maker/ReadMe") + img_elements_with_src = self.find_elements("img[src]") + unique_src_values = [] + for img in img_elements_with_src: + src = img.get_attribute("src") + if src not in unique_src_values: + unique_src_values.append(src) + print() + for src in unique_src_values: + if src.split(".")[-1] not in ["png", "jpg", "jpeg"]: + continue + self.download_file(src) # Goes to downloaded_files/ + filename = src.split("/")[-1] + self.assert_downloaded_file(filename) + folder = "downloaded_files" + file_path = os.path.join(folder, filename) + print(file_path) + + def test_download_images_via_screenshot(self): + if self.recorder_mode: + self.goto("about:blank") + self.skip("Skipping test in Recorder Mode.") + self.goto("seleniumbase.io/error_page/") + img_elements_with_src = self.find_elements("img[src]") + unique_src_values = [] + for img in img_elements_with_src: + src = img.get_attribute("src") + if src not in unique_src_values: + unique_src_values.append(src) + print() + count = 0 + for src in unique_src_values: + self.goto(src) + if not self.headless and not self.headless2: + self.sleep(0.3) + image = self.find_element("img") + if src.startswith("data:") or ";base64" in src: + # Special Cases: SVGs, etc. Convert to PNG. + count += 1 + filename = "svg_image_%s.png" % count + else: + filename = src.split("/")[-1] + folder = "downloaded_files" + file_path = os.path.join(folder, filename) + image.screenshot(file_path) + self.assert_downloaded_file(filename) + print(file_path) diff --git a/examples/test_drag_and_drop.py b/examples/test_drag_and_drop.py new file mode 100644 index 00000000000..5ff556b0a5e --- /dev/null +++ b/examples/test_drag_and_drop.py @@ -0,0 +1,22 @@ +"""Test drag_and_drop() on different pages.""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class DragAndDropTests(BaseCase): + def test_drag_and_drop(self): + self.goto("https://seleniumbase.io/other/drag_and_drop") + self.assert_element_not_visible("#div1 img#drag1") + self.drag_and_drop("#drag1", "#div1") + self.assert_element("#div1 img#drag1") + self.sleep(0.8) + + def test_w3schools_drag_and_drop(self): + self.goto("https://seleniumbase.io/w3schools/drag_drop") + self.assert_url_contains("drag_drop") + self.click("button#runbtn") + self.switch_to_frame("iframeResult") + self.assert_element_not_visible("#div1 img#drag1") + self.drag_and_drop("#drag1", "#div1") + self.assert_element("#div1 img#drag1") + self.sleep(0.8) diff --git a/examples/test_error_page.py b/examples/test_error_page.py new file mode 100644 index 00000000000..e502acc8d05 --- /dev/null +++ b/examples/test_error_page.py @@ -0,0 +1,17 @@ +"""Test an error page with the "highlight() command, which uses a +JavaScript animation to point out page objects that are found. +If an element isn't visible, the test fails with an exception.""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class ErrorPageTests(BaseCase): + def test_error_page(self): + self.goto("https://seleniumbase.io/error_page/") + self.highlight('img[alt="500 Error"]') + self.highlight("img#parallax_octocat") + self.highlight("#parallax_error_text") + self.highlight('img[alt*="404"]') + self.highlight("img#octobi_wan_catnobi") + self.highlight("img#speeder") + self.save_screenshot_after_test = True diff --git a/examples/test_event_firing.py b/examples/test_event_firing.py new file mode 100644 index 00000000000..33d565a0765 --- /dev/null +++ b/examples/test_event_firing.py @@ -0,0 +1,39 @@ +"""Test EventFiringWebDriver with AbstractEventListener""" +from selenium.webdriver.support.events import EventFiringWebDriver +from selenium.webdriver.support.events import AbstractEventListener +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class MyListener(AbstractEventListener): + def before_navigate_to(self, url, driver): + print("Before navigating to: %s" % url) + + def after_navigate_to(self, url, driver): + print("After navigating to: %s" % url) + + def before_find(self, by, value, driver): + print('Before find "%s" (by = %s)' % (value, by)) + + def after_find(self, by, value, driver): + print('After find "%s" (by = %s)' % (value, by)) + + def before_click(self, element, driver): + print('Before clicking on element with text: "%s"' % element.text) + + def after_click(self, element, driver): + print("Click complete!") + + +class EventFiringTests(BaseCase): + def test_event_firing_webdriver(self): + self.driver = EventFiringWebDriver(self.driver, MyListener()) + print("\n* EventFiringWebDriver example *") + self.goto("https://xkcd.com/1862/") + self.click("link=About") + self.goto("https://xkcd.com/1820/") + self.assert_text("Security Advice", "#ctitle") + self.click('a:contains("Next >")') + self.assert_text("Incinerator", "#ctitle") + self.click('a[rel="next"]') + self.assert_text("Existential Bug Reports", "#ctitle") diff --git a/examples/test_fail.py b/examples/test_fail.py old mode 100755 new mode 100644 index 2d322168820..65636534558 --- a/examples/test_fail.py +++ b/examples/test_fail.py @@ -1,14 +1,16 @@ -""" This test was made to fail on purpose to demonstrate the - logging capabilities of the SeleniumBase Test Framework """ - +""" This test fails on purpose to demonstrate + the logging capabilities of SeleniumBase. + >>> pytest test_fail.py --html=report.html + This creates ``report.html`` with details. + (Also find log files in ``latest_logs/``)""" import pytest from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) -class MyTestClass(BaseCase): - +class FailingTests(BaseCase): @pytest.mark.expected_failure def test_find_army_of_robots_on_xkcd_desert_island(self): - self.open("https://xkcd.com/731/") - print("\n(This test fails on purpose)") + self.goto("https://xkcd.com/731/") + print("\n(This test should fail)") self.assert_element("div#ARMY_OF_ROBOTS", timeout=1) diff --git a/examples/test_geolocation.py b/examples/test_geolocation.py new file mode 100644 index 00000000000..cd07fa359d0 --- /dev/null +++ b/examples/test_geolocation.py @@ -0,0 +1,46 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class TestGeolocation(BaseCase): + def tearDown(self): + self.save_teardown_screenshot() # If test fails, or if "--screenshot" + if self.is_chromium() and not self._multithreaded: + # Reset Permissions and GeolocationOverride + try: + self.goto("about:blank") + self.execute_cdp_cmd("Emulation.setGeolocationOverride", {}) + self.execute_cdp_cmd("Browser.resetPermissions", {}) + except Exception: + pass + super().tearDown() + + def test_geolocation(self): + self.goto("about:blank") + if self._multithreaded: + self.skip("Skipping test in multi-threaded mode.") + if not self.is_chromium(): + print("\n* execute_cdp_cmd() is only for Chromium browsers") + self.skip("execute_cdp_cmd() is only for Chromium browsers") + self.execute_cdp_cmd( + "Browser.grantPermissions", + { + "origin": "https://www.randymajors.org/", + "permissions": ["geolocation"], + }, + ) + self.execute_cdp_cmd( + "Emulation.setGeolocationOverride", + { + "latitude": 48.87645, + "longitude": 2.26340, + "accuracy": 100, + }, + ) + self.goto("https://www.randymajors.org/what-time-zone-am-i-in") + self.ad_block() + self.assert_text("Paris, France", "#statecountrylabel") + self.assert_text("Central European", "#currentlabel") + self.save_screenshot_to_logs() + if self.headed: + self.sleep(4) diff --git a/examples/test_get_coffee.py b/examples/test_get_coffee.py new file mode 100644 index 00000000000..8c2f72cc90b --- /dev/null +++ b/examples/test_get_coffee.py @@ -0,0 +1,20 @@ +"""Use SeleniumBase to get coffee""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class GetCoffeeTest(BaseCase): + def test_get_coffee(self): + self.goto("https://seleniumbase.io/coffee/") + self.assert_title("Coffee Cart") + self.assert_exact_text("cart (0)", 'a[aria-label="Cart page"]') + self.assert_element('div[data-sb="Mocha"]') + self.click('div[data-sb="Mocha"]') + self.assert_link_text("cart (1)") + self.click_link_text("cart (1)") + self.assert_exact_text("Total: $8.00", "button.pay") + self.click("button.pay") + self.type("input#name", "Selenium Coffee") + self.type("input#email", "test@test.test") + self.click("button#submit-payment") + self.assert_text("Thanks", "#app div.success") diff --git a/examples/test_get_locale_code.py b/examples/test_get_locale_code.py new file mode 100644 index 00000000000..dd26e350ada --- /dev/null +++ b/examples/test_get_locale_code.py @@ -0,0 +1,12 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class LocaleTests(BaseCase): + def test_get_locale_code(self): + self.goto("about:blank") + locale_code = self.get_locale_code() + message = '\nLocale Code = "%s"' % locale_code + print(message) + self.set_messenger_theme(theme="flat", location="top_center") + self.post_message(message, duration=4) diff --git a/examples/test_get_pdf_text.py b/examples/test_get_pdf_text.py new file mode 100644 index 00000000000..6acf0d7c093 --- /dev/null +++ b/examples/test_get_pdf_text.py @@ -0,0 +1,17 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__, "--uc") + + +class PDFTests(BaseCase): + def test_get_pdf_text(self): + self.goto("data:,") + if self.headless: + self.skip("Skip this test in headless mode!") + if not self.undetectable or not self.external_pdf: + self.activate_cdp_mode(external_pdf=True) + pdf = ( + "https://nostarch.com/download/" + "Automate_the_Boring_Stuff_sample_ch17.pdf" + ) + pdf_text = self.get_pdf_text(pdf, page=1, nav=True) + print("\n" + pdf_text) diff --git a/examples/test_get_swag.py b/examples/test_get_swag.py new file mode 100644 index 00000000000..9f254a12ae4 --- /dev/null +++ b/examples/test_get_swag.py @@ -0,0 +1,20 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class MyTestClass(BaseCase): + def test_swag_labs(self): + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("div.inventory_list") + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_text("Backpack", "div.cart_item") + self.click("button#checkout") + self.type("input#first-name", "SeleniumBase") + self.type("input#last-name", "Automation") + self.type("input#postal-code", "77123") + self.click("input#continue") + self.click("button#finish") + self.assert_text("Thank you for your order!") diff --git a/examples/test_get_user_agent.py b/examples/test_get_user_agent.py new file mode 100644 index 00000000000..84b4640049d --- /dev/null +++ b/examples/test_get_user_agent.py @@ -0,0 +1,11 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class UserAgentTests(BaseCase): + def test_get_user_agent(self): + self.goto("data:,") + user_agent = self.get_user_agent() + print('\nUser Agent = "%s"' % user_agent) + self.set_messenger_theme(theme="flat", location="top_center") + self.post_message(user_agent, duration=4) diff --git a/examples/test_hack_search.py b/examples/test_hack_search.py new file mode 100644 index 00000000000..0c8b69036e0 --- /dev/null +++ b/examples/test_hack_search.py @@ -0,0 +1,35 @@ +""" Testing the "self.set_attribute()" and "self.set_attributes()" methods + to modify a Google search into becoming a Bing search. + set_attribute() -> Modifies the attribute of the first matching element. + set_attributes() -> Modifies the attribute of all matching elements. """ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class HackingTests(BaseCase): + def test_hack_search(self): + if self.headless or self.browser != "chrome": + self.goto_if_not_url("about:blank") + self.skip('Skip test if headless or not chrome.') + if not self.undetectable: + self.get_new_driver(undetectable=True) + self.goto("https://google.com/ncr") + self.click_if_visible('button:contains("Accept all")') + self.hide_elements("iframe") + self.assert_element('[title="Search"]') + self.sleep(0.5) + self.set_attribute('[action="/search"]', "action", "//bing.com/search") + self.set_attributes('[value="Google Search"]', "value", "Bing Search") + self.type('[title="Search"]', "SeleniumBase GitHub Page URL") + self.sleep(0.5) + self.js_click('[value="Bing Search"]') + self.highlight("h1.b_logo", loops=8) + source = self.get_page_source() + self.assert_true("github.com/seleniumbase/SeleniumBase" in source) + self.internalize_links() + self.click('a:contains("seleniumbase/SeleniumBase")') + self.js_click('a[title="examples"]') + self.highlight('#repo-content-turbo-frame') + self.js_click('a[title="test_hack_search.py"]') + self.assert_text("test_hack_search.py", "#file-name-id-wide") + self.highlight("#file-name-id-wide") diff --git a/examples/test_highlight_elements.py b/examples/test_highlight_elements.py new file mode 100644 index 00000000000..94d439793fc --- /dev/null +++ b/examples/test_highlight_elements.py @@ -0,0 +1,11 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class HighlightTest(BaseCase): + def test_highlight_inputs(self): + self.goto("https://seleniumbase.io/demo_page") + if self.headed: + self.highlight_elements("input", loops=2) # Default: 4 + else: + self.highlight_elements("input", loops=1, limit=3) diff --git a/examples/test_image_saving.py b/examples/test_image_saving.py new file mode 100644 index 00000000000..acc6e60834f --- /dev/null +++ b/examples/test_image_saving.py @@ -0,0 +1,83 @@ +""" Image-saving with "save_element_as_image_file()". + + Also shown are ways of ordering tests. (Currently commented out) + For ordering tests, add the marker "@pytest.mark.run(order=NUM)" + before a test definition or class definition. + This changes the global test order when running "pytest". + Eg: If you want a test to always run first before any test + from all discovered files, add "@pytest.mark.run(order=0)". + For local class/module test-ordering, name your tests + using alphabetical order to set the order desired. + Eg: "def test_AAAAA" will run before "def test_ZZZZZ". + You can also add in numbers to force a specific order. + Eg: "def test_1_ZZZ" will run before "def test_2_AAA". +""" +import os +# import pytest # For ordering tests globally with @pytest.mark.run() +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class ImageTests(BaseCase): + # @pytest.mark.run(order=1) + def test_1_save_element_as_image_file(self): + """Pull an image from a website and save it as a PNG file.""" + self.goto("https://xkcd.com/1117/") + selector = "#comic" + file_name = "comic.png" + folder = "images_exported" + self.save_element_as_image_file(selector, file_name, folder) + file_path = os.path.join(folder, file_name) + self.assert_true(os.path.exists(file_path)) + print('\n"%s" was saved!' % file_path) + + # @pytest.mark.run(order=2) + def test_2_add_text_overlay_to_image(self): + """Add a text overlay to an image.""" + self.goto("https://xkcd.com/1117/") + selector = "#comic" + file_name = "image_overlay.png" + folder = "images_exported" + overlay_text = 'This is an XKCD comic!\nTitle: "My Sky"' + self.save_element_as_image_file( + selector, file_name, folder, overlay_text + ) + file_path = os.path.join(folder, file_name) + self.assert_true(os.path.exists(file_path)) + print('\n"%s" was saved!' % file_path) + + # @pytest.mark.run(order=3) + def test_3_add_text_overlay_to_page_section(self): + """Add a text overlay to a section of a page.""" + self.goto("https://xkcd.com/2200/") + selector = "#middleContainer" + file_name = "section_overlay.png" + folder = "images_exported" + overlay_text = ( + "Welcome to %s\n" + "This is a comment added to the image.\n" + "Unreachable states come from logic errors." + % self.get_current_url() + ) + self.save_element_as_image_file( + selector, file_name, folder, overlay_text + ) + file_path = os.path.join(folder, file_name) + self.assert_true(os.path.exists(file_path)) + print('\n"%s" was saved!' % file_path) + + # @pytest.mark.run(order=4) + def test_4_add_text_overlay_to_full_page(self): + """Add a text overlay to a full page.""" + self.goto("https://xkcd.com/1922/") + self.remove_element("#bottom") + selector = "body" + file_name = "page_overlay.png" + folder = "images_exported" + overlay_text = "A text overlay on %s" % self.get_current_url() + self.save_element_as_image_file( + selector, file_name, folder, overlay_text + ) + file_path = os.path.join(folder, file_name) + self.assert_true(os.path.exists(file_path)) + print('\n"%s" was saved!' % file_path) diff --git a/examples/test_inspect_html.py b/examples/test_inspect_html.py new file mode 100644 index 00000000000..847466680fd --- /dev/null +++ b/examples/test_inspect_html.py @@ -0,0 +1,11 @@ +"""Uses the SeleniumBase implementation of HTML-Inspector to inspect the HTML. +See https://github.com/philipwalton/html-inspector for more details. +(Only works on Chrome and Chromium-based browsers.)""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class HtmlInspectorTests(BaseCase): + def test_html_inspector(self): + self.goto("https://xkcd.com/1144/") + self.inspect_html() diff --git a/examples/test_login.py b/examples/test_login.py new file mode 100644 index 00000000000..20a0834d6bd --- /dev/null +++ b/examples/test_login.py @@ -0,0 +1,15 @@ +"""A SeleniumBase test for verifying the Login Page on Swag Labs""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class SwagLabsLoginTests(BaseCase): + def test_swag_labs_login(self): + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce") + self.click('input[type="submit"]') + self.assert_element("div.inventory_list") + self.assert_element('div.inventory_item:contains("Bike")') + self.js_click("a#logout_sidebar_link") + self.assert_element("div#login_button_container") diff --git a/examples/test_markers.py b/examples/test_markers.py old mode 100755 new mode 100644 index 443c577a21b..aabae458b99 --- a/examples/test_markers.py +++ b/examples/test_markers.py @@ -8,32 +8,30 @@ pytest test_markers.py -v -m "not marker2" # Runs A, D (The "-v" will display the names of tests as they run.) - (Add "--collect-only" to display names of tests without running them.) -""" - + (Add "--collect-only" to display names of tests without running them.)""" import pytest from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) @pytest.mark.marker_test_suite class MarkerTestSuite(BaseCase): - @pytest.mark.marker1 def test_A(self): - self.open("https://xkcd.com/1319/") + self.goto("https://xkcd.com/1319/") self.assert_text("Automation", "div#ctitle") @pytest.mark.marker2 def test_B(self): - self.open("https://www.xkcd.com/1700/") + self.goto("https://www.xkcd.com/1700/") self.assert_text("New Bug", "div#ctitle") @pytest.mark.marker2 @pytest.mark.marker3 # Tests can have multiple markers def test_C(self): - self.open("https://xkcd.com/844/") + self.goto("https://xkcd.com/844/") self.assert_text("Good Code", "div#ctitle") def test_D(self): - self.open("https://xkcd.com/2021/") + self.goto("https://xkcd.com/2021/") self.assert_text("Software Development", "div#ctitle") diff --git a/examples/test_mfa_login.py b/examples/test_mfa_login.py new file mode 100644 index 00000000000..5af0c3205d8 --- /dev/null +++ b/examples/test_mfa_login.py @@ -0,0 +1,17 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class TestMFALogin(BaseCase): + def test_mfa_login(self): + self.goto("https://seleniumbase.io/realworld/login") + self.type("#username", "demo_user") + self.type("#password", "secret_pass") + self.enter_mfa_code("#totpcode", "GAXG2MTEOR3DMMDG") # 6-digit + self.assert_text("Welcome!", "h1") + self.highlight("img#image1") # A fancier assert_element() call + self.click('a:contains("This Page")') # Use :contains() on any tag + self.save_screenshot_to_logs() # ("./latest_logs" folder for test) + self.click_link("Sign out") # Link must be "a" tag. Not "button". + self.assert_element('a:contains("Sign in")') + self.assert_exact_text("You have been signed out!", "#top_message") diff --git a/examples/test_multiple_drivers.py b/examples/test_multiple_drivers.py new file mode 100644 index 00000000000..806fe19c4cf --- /dev/null +++ b/examples/test_multiple_drivers.py @@ -0,0 +1,19 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class MultipleDriversTest(BaseCase): + def test_multiple_drivers(self): + if self.browser == "safari": + self.goto_if_not_url("about:blank") + print("\n Safari doesn't support multiple drivers.") + self.skip("Safari doesn't support multiple drivers.") + self.goto("data:text/html,

      Driver 1

      ") + driver2 = self.get_new_driver() + self.goto("data:text/html,

      Driver 2

      ") + self.switch_to_default_driver() # Driver 1 + self.highlight("h1") + self.assert_text("Driver 1", "h1") + self.switch_to_driver(driver2) # Driver 2 + self.highlight("h1") + self.assert_text("Driver 2", "h1") diff --git a/examples/test_null.py b/examples/test_null.py new file mode 100644 index 00000000000..10c47a98aaa --- /dev/null +++ b/examples/test_null.py @@ -0,0 +1,7 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class NullTests(BaseCase): + def test_null(self): + pass diff --git a/examples/test_override_driver.py b/examples/test_override_driver.py new file mode 100644 index 00000000000..89ff8e809aa --- /dev/null +++ b/examples/test_override_driver.py @@ -0,0 +1,31 @@ +from selenium import webdriver +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class OverrideDriverTest(BaseCase): + def get_new_driver(self, *args, **kwargs): + """This method overrides get_new_driver() from BaseCase.""" + options = webdriver.ChromeOptions() + options.add_argument("--disable-notifications") + if self.headless: + options.add_argument("--headless=new") + options.add_argument("--disable-gpu") + options.add_experimental_option( + "excludeSwitches", ["enable-automation", "enable-logging"], + ) + prefs = { + "credentials_enable_service": False, + "profile.password_manager_enabled": False, + } + options.add_experimental_option("prefs", prefs) + return webdriver.Chrome(options=options) + + def test_driver_override(self): + self.goto("https://seleniumbase.io/demo_page") + self.type("#myTextInput", "This is Automated") + self.set_value("input#mySlider", "100") + self.select_option_by_text("#mySelect", "Set to 100%") + self.click("#checkBox1") + self.drag_and_drop("img#logo", "div#drop2") + self.click('button:contains("Click Me")') diff --git a/examples/test_override_sb_fixture.py b/examples/test_override_sb_fixture.py new file mode 100644 index 00000000000..c1040306bc1 --- /dev/null +++ b/examples/test_override_sb_fixture.py @@ -0,0 +1,89 @@ +"""Overriding the "sb" fixture to override the driver.""" +import pytest +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +@pytest.fixture() +def sb(request): + from selenium import webdriver + from seleniumbase import BaseCase + from seleniumbase import config as sb_config + from seleniumbase.core import session_helper + + class BaseClass(BaseCase): + def get_new_driver(self, *args, **kwargs): + """This method overrides get_new_driver() from BaseCase.""" + options = webdriver.ChromeOptions() + options.add_argument("--disable-notifications") + if self.headless: + options.add_argument("--headless=new") + options.add_argument("--disable-gpu") + options.add_experimental_option( + "excludeSwitches", ["enable-automation", "enable-logging"], + ) + prefs = { + "credentials_enable_service": False, + "profile.password_manager_enabled": False, + } + options.add_experimental_option("prefs", prefs) + return webdriver.Chrome(options=options) + + def setUp(self): + super().setUp() + + def base_method(self): + pass + + def tearDown(self): + self.save_teardown_screenshot() + super().tearDown() + + if request.cls: + if sb_config.reuse_class_session: + the_class = str(request.cls).split(".")[-1].split("'")[0] + if the_class != sb_config._sb_class: + session_helper.end_reused_class_session_as_needed() + sb_config._sb_class = the_class + request.cls.sb = BaseClass("base_method") + request.cls.sb.setUp() + request.cls.sb._needs_tearDown = True + request.cls.sb._using_sb_fixture = True + request.cls.sb._using_sb_fixture_class = True + sb_config._sb_node[request.node.nodeid] = request.cls.sb + yield request.cls.sb + if request.cls.sb._needs_tearDown: + request.cls.sb.tearDown() + request.cls.sb._needs_tearDown = False + else: + sb = BaseClass("base_method") + sb.setUp() + sb._needs_tearDown = True + sb._using_sb_fixture = True + sb._using_sb_fixture_no_class = True + sb_config._sb_node[request.node.nodeid] = sb + yield sb + if sb._needs_tearDown: + sb.tearDown() + sb._needs_tearDown = False + + +def test_override_fixture_no_class(sb: BaseCase): + sb.goto("https://seleniumbase.io/demo_page") + sb.type("#myTextInput", "This is Automated") + sb.set_value("input#mySlider", "100") + sb.select_option_by_text("#mySelect", "Set to 100%") + sb.click("#checkBox1") + sb.drag_and_drop("img#logo", "div#drop2") + sb.click('button:contains("Click Me")') + + +class TestOverride: + def test_override_fixture_inside_class(self, sb: BaseCase): + sb.goto("https://seleniumbase.io/demo_page") + sb.type("#myTextInput", "This is Automated") + sb.set_value("input#mySlider", "100") + sb.select_option_by_text("#mySelect", "Set to 100%") + sb.click("#checkBox1") + sb.drag_and_drop("img#logo", "div#drop2") + sb.click('button:contains("Click Me")') diff --git a/examples/test_parse_soup.py b/examples/test_parse_soup.py new file mode 100644 index 00000000000..9cf35413824 --- /dev/null +++ b/examples/test_parse_soup.py @@ -0,0 +1,39 @@ +import re +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class SoupParsingTests(BaseCase): + def click_menu_item(self, text): + # Use BeautifulSoup to parse the selector ID from element text. + # Then click on the element with the ID. + # (This is useful when the selector ID is auto-generated.) + pattern = re.compile(text) + soup = self.get_beautiful_soup() + the_id = soup.find(string=pattern).parent.parent.attrs["id"] + self.click("#%s" % the_id) + + def test_beautiful_soup_parsing(self): + if self.headless: + self.goto_if_not_url("about:blank") + self.skip("Skip this test in headless mode!") + self.goto("https://seleniumbase.io/tinymce/") + self.wait_for_element("div.mce-container-body") + self.click_menu_item("File") + self.click_menu_item("New document") + self.click_menu_item("Paragraph") + self.click_menu_item("Heading 2") + self.switch_to_frame("iframe") + self.add_text("#tinymce", "Automate anything with SeleniumBase!\n") + self.switch_to_default_content() + self.click("button i.mce-i-image") + self.type('input[aria-label="Width"].mce-textbox', "300") + image_url = "https://seleniumbase.github.io/img/sb_logo_10.png" + self.type("input.mce-textbox", image_url + "\n") + with self.frame_switch("iframe"): + self.click("h2") + self.post_message("Automate anything with SeleniumBase!") + self.click_menu_item("File") + self.click_menu_item("Preview") + with self.frame_switch('iframe[sandbox="allow-scripts"]'): + self.post_message("Learn SeleniumBase Today!") diff --git a/examples/test_pdf_asserts.py b/examples/test_pdf_asserts.py new file mode 100644 index 00000000000..39eb614c3f2 --- /dev/null +++ b/examples/test_pdf_asserts.py @@ -0,0 +1,22 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class PDFAssertTests(BaseCase): + def test_assert_pdf_text(self): + self.goto("data:,") + if self.headless: + self.skip("Skip this test in headless mode!") + if not self.undetectable or not self.external_pdf: + self.activate_cdp_mode(external_pdf=True) + # Assert PDF contains the expected text on Page 1 + self.assert_pdf_text( + "https://nostarch.com/download/Automate_the_Boring_Stuff_dTOC.pdf", + "Programming Is a Creative Activity", + page=1, + ) + # Assert PDF contains the expected text on any of the pages + self.assert_pdf_text( + "https://nostarch.com/download/Automate_the_Boring_Stuff_dTOC.pdf", + "Extracting Text from PDFs", + ) diff --git a/examples/test_pytest_parametrize.py b/examples/test_pytest_parametrize.py new file mode 100644 index 00000000000..e13496a70fc --- /dev/null +++ b/examples/test_pytest_parametrize.py @@ -0,0 +1,24 @@ +import pytest + + +@pytest.mark.parametrize( + "value", ["List of Features", "Command Line Options"] +) +def test_sb_fixture_with_no_class(sb, value): + sb.goto("seleniumbase.io/help_docs/install/") + sb.type('input[aria-label="Search"]', value) + sb.click("nav h1 mark") + sb.assert_title_contains(value) + sb.assert_text(value, "div.md-content") + + +class Test_SB_Fixture: + @pytest.mark.parametrize( + "value", ["Console Scripts", "API Reference"] + ) + def test_sb_fixture_inside_class(self, sb, value): + sb.goto("seleniumbase.io/help_docs/install/") + sb.type('input[aria-label="Search"]', value) + sb.click("nav h1 mark") + sb.assert_title_contains(value) + sb.assert_text(value, "div.md-content") diff --git a/examples/test_repeat_tests.py b/examples/test_repeat_tests.py new file mode 100644 index 00000000000..71a1e86aaed --- /dev/null +++ b/examples/test_repeat_tests.py @@ -0,0 +1,36 @@ +""" Tests to demonstrate how to repeat the same test multiple times. + The 1st example uses the "parameterized" library. + The 2nd example uses "pytest.mark.parametrize()". (NO class) + The 3rd example uses "pytest.mark.parametrize()". (in class) """ +import pytest +from parameterized import parameterized +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__, "-n6") + +url = "data:text/html,

      Hello

       

      " + + +class RepeatTests(BaseCase): + @parameterized.expand([[]] * 2) + def test_repeat_this_test_with_parameterized(self): + self.goto(url) + self.type("input", "SeleniumBase is fun") + self.click('button:contains("OK!")') + self.assert_text("Hello", "h2") + + +@pytest.mark.parametrize("", [[]] * 2) +def test_repeat_this_test_with_pytest_parametrize(sb): + sb.goto(url) + sb.type("input", "SeleniumBase is fun") + sb.click('button:contains("OK!")') + sb.assert_text("Hello", "h2") + + +class RepeatTestsWithPytest: + @pytest.mark.parametrize("", [[]] * 2) + def test_repeat_test_with_pytest_parametrize(self, sb): + sb.goto(url) + sb.type("input", "SeleniumBase is fun") + sb.click('button:contains("OK!")') + sb.assert_text("Hello", "h2") diff --git a/examples/test_request_sb_fixture.py b/examples/test_request_sb_fixture.py new file mode 100644 index 00000000000..bb1c7bc5a7d --- /dev/null +++ b/examples/test_request_sb_fixture.py @@ -0,0 +1,26 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +# Use the pytest "request" fixture to get the "sb" fixture (no class) +def test_request_sb_fixture(request): + sb: BaseCase = request.getfixturevalue("sb") + sb.goto("https://seleniumbase.io/demo_page") + sb.assert_text("SeleniumBase", "#myForm h2") + sb.assert_element("input#myTextInput") + sb.type("#myTextarea", "This is me") + sb.click("#myButton") + sb.tearDown() + + +# Use the pytest "request" fixture to get the "sb" fixture (in class) +class Test_Request_Fixture: + def test_request_sb_fixture_in_class(self, request): + sb: BaseCase = request.getfixturevalue("sb") + sb.goto("https://seleniumbase.io/demo_page") + sb.assert_element("input#myTextInput") + sb.type("#myTextarea", "Automated") + sb.assert_text("This Text is Green", "#pText") + sb.click("#myButton") + sb.assert_text("This Text is Purple", "#pText") + sb.tearDown() diff --git a/examples/test_roblox_mobile.py b/examples/test_roblox_mobile.py new file mode 100644 index 00000000000..072d45ac09c --- /dev/null +++ b/examples/test_roblox_mobile.py @@ -0,0 +1,23 @@ +"""Mobile device test for Chromium-based browsers +Example: "pytest test_roblox_mobile.py --mobile" +""" +from seleniumbase import BaseCase + +if __name__ == "__main__": + from pytest import main + main([__file__, "--mobile", "-s"]) + + +class RobloxTests(BaseCase): + def test_roblox_mobile_site(self): + if not self.mobile_emulator: + self.goto_if_not_url("about:blank") + print("\n This test is only for mobile-device web browsers!") + print(' (Use "--mobile" to run this test in Mobile Mode!)') + self.skip('Use "--mobile" to run this test in Mobile Mode!') + self.goto("https://www.roblox.com/") + self.assert_element("#download-the-app-container") + self.assert_text("Roblox for Android") + self.assert_text("Continue in App", "a.content-action-emphasis") + self.highlight('span:contains("Roblox for Android")', loops=8) + self.highlight("a.content-action-emphasis", loops=8) diff --git a/examples/test_save_screenshots.py b/examples/test_save_screenshots.py new file mode 100644 index 00000000000..74bb0b20dfa --- /dev/null +++ b/examples/test_save_screenshots.py @@ -0,0 +1,40 @@ +import os +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class ScreenshotTests(BaseCase): + def test_save_screenshot(self): + self.goto("https://seleniumbase.io/demo_page") + # "./downloaded_files" is a special SeleniumBase folder for downloads + self.save_screenshot("demo_page.png", folder="./downloaded_files") + self.assert_downloaded_file("demo_page.png") + print('\n"%s/%s" was saved!' % ("downloaded_files", "demo_page.png")) + + def test_save_screenshot_to_logs(self): + self.goto("https://seleniumbase.io/demo_page") + self.save_screenshot_to_logs() + # "self.log_path" is the absolute path to the "./latest_logs" folder. + # Each test that generates log files will create a subfolder in there + test_logpath = os.path.join(self.log_path, self.test_id) + expected_screenshot = os.path.join(test_logpath, "_1_screenshot.png") + self.assert_true(os.path.exists(expected_screenshot)) + print('\n"%s" was saved!' % (expected_screenshot)) + + self.goto("https://seleniumbase.io/tinymce/") + self.save_screenshot_to_logs() + expected_screenshot = os.path.join(test_logpath, "_2_screenshot.png") + self.assert_true(os.path.exists(expected_screenshot)) + print('"%s" was saved!' % (expected_screenshot)) + + self.goto("https://seleniumbase.io/error_page/") + self.save_screenshot_to_logs("error_page") + expected_screenshot = os.path.join(test_logpath, "_3_error_page.png") + self.assert_true(os.path.exists(expected_screenshot)) + print('"%s" was saved!' % (expected_screenshot)) + + self.goto("https://seleniumbase.io/devices/") + self.save_screenshot_to_logs("devices") + expected_screenshot = os.path.join(test_logpath, "_4_devices.png") + self.assert_true(os.path.exists(expected_screenshot)) + print('"%s" was saved!' % (expected_screenshot)) diff --git a/examples/test_sb_fixture.py b/examples/test_sb_fixture.py new file mode 100644 index 00000000000..dac599cac7d --- /dev/null +++ b/examples/test_sb_fixture.py @@ -0,0 +1,19 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +# "sb" pytest fixture test in a method with no class +def test_sb_fixture_with_no_class(sb: BaseCase): + sb.goto("seleniumbase.io/help_docs/install/") + sb.type('input[aria-label="Search"]', "GUI Commander") + sb.click('mark:contains("Commander")') + sb.assert_title_contains("GUI / Commander") + + +# "sb" pytest fixture test in a method inside a class +class Test_SB_Fixture: + def test_sb_fixture_inside_class(self, sb: BaseCase): + sb.goto("seleniumbase.io/help_docs/install/") + sb.type('input[aria-label="Search"]', "GUI Commander") + sb.click('mark:contains("Commander")') + sb.assert_title_contains("GUI / Commander") diff --git a/examples/test_scrape_bing.py b/examples/test_scrape_bing.py new file mode 100644 index 00000000000..ad97405bd15 --- /dev/null +++ b/examples/test_scrape_bing.py @@ -0,0 +1,31 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class ScrapeBingTests(BaseCase): + def test_scrape_bing(self): + if self._multithreaded: + self.goto_if_not_url("about:blank") + self.skip("Skipping test in multi-threaded mode.") + self.goto("www.bing.com/search?q=SeleniumBase+GitHub&qs=n&form=QBRE") + self.wait_for_element("main h2 a") + self.sleep(0.5) + soup = self.get_beautiful_soup() + titles = [item.text for item in soup.select("main h2 a")] + print("\nSearch Result Headers:") + for title in titles: + if ( + "seleniumbase/" in title.lower() + or "SeleniumBase Docs" in title + ): + print(" " + title) + links = [item["href"] for item in soup.select("main h2 a")] + print("Search Result Links:") + for link in links: + if ( + "github.com/seleniumbase" in link.lower() + or "https://seleniumbase.io/" in link.lower() + ): + print(" " + link) + self.click_if_visible('a[href="https://github.com/seleniumbase"]') + print("Last Page = " + self.get_current_url()) diff --git a/examples/test_select_options.py b/examples/test_select_options.py new file mode 100644 index 00000000000..d4885c808a8 --- /dev/null +++ b/examples/test_select_options.py @@ -0,0 +1,41 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class SelectTestClass(BaseCase): + def test_base(self): + self.goto("https://seleniumbase.io/demo_page") + + expected_option_texts = [ + "Set to 25%", "Set to 50%", "Set to 75%", "Set to 100%" + ] + option_texts = self.get_select_options("select#mySelect") + self.assert_equal(option_texts, expected_option_texts) + + expected_option_indexes = ["0", "1", "2", "3"] + option_indexes = self.get_select_options( + "select#mySelect", attribute="index" + ) + self.assert_equal(option_indexes, expected_option_indexes) + + expected_option_values = ["25%", "50%", "75%", "100%"] + option_values = self.get_select_options( + "select#mySelect", attribute="value" + ) + self.assert_equal(option_values, expected_option_values) + + for index, option_text in enumerate(option_texts): + self.select_option_by_text("#mySelect", option_text) + selected_value = self.get_attribute("#mySelect", "value") + self.assert_equal(selected_value, option_values[index]) + + for index, option_value in enumerate(option_values): + self.select_option_by_value("#mySelect", option_value) + selected_value = self.get_attribute("#mySelect", "value") + self.assert_equal(selected_value, option_values[index]) + + for index, option_index in enumerate(option_indexes): + self.select_option_by_index("#mySelect", option_index) + # assert_attribute() combines get_attribute() and assert_equal() + # It also highlights the element when Demo Mode is enabled. + self.assert_attribute("#mySelect", "value", option_values[index]) diff --git a/examples/test_shadow_dom.py b/examples/test_shadow_dom.py new file mode 100644 index 00000000000..8e5e0a91f4c --- /dev/null +++ b/examples/test_shadow_dom.py @@ -0,0 +1,76 @@ +"""Shadow DOM test. +First download files from PyPI. +Then search for them on a multi-layered Shadow DOM page. +This uses the "::shadow" selector for piercing shadow-root elements. +Here's the URL that contains Shadow DOM: chrome://downloads/ """ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class ShadowDomTests(BaseCase): + def download_tar_file_from_pypi(self, package): + self.goto("https://pypi.org/project/%s/#files" % package) + pkg_header = self.get_text("h1.package-header__name").strip() + pkg_name = pkg_header.replace(" ", "-") + tar_file = pkg_name + ".tar.gz" + tar_selector = 'div#files a[href$="%s"]' % tar_file + self.delete_downloaded_file_if_present(tar_file, browser=True) + self.click(tar_selector) + return tar_file + + def test_shadow_dom(self): + if not self.browser == "chrome" or self.headless or self.recorder_mode: + self.goto_if_not_url("about:blank") + print("\n Unsupported mode for this test.") + self.skip("Unsupported mode for this test.") + + # Download Python package files from PyPI + file_name_1 = self.download_tar_file_from_pypi("sbase") + file_name_2 = self.download_tar_file_from_pypi("seleniumbase") + self.assert_downloaded_file(file_name_1, browser=True) + self.assert_downloaded_file(file_name_2, browser=True) + + # Navigate to the Chrome downloads page. + self.goto("chrome://downloads/") + + # Shadow DOM selectors + search_icon = ( + "downloads-manager::shadow downloads-toolbar::shadow" + " cr-toolbar::shadow cr-toolbar-search-field::shadow" + " cr-icon-button" + ) + search_input = ( + "downloads-manager::shadow downloads-toolbar::shadow" + " cr-toolbar::shadow cr-toolbar-search-field::shadow" + " #searchInput" + ) + clear_search_icon = ( + "downloads-manager::shadow downloads-toolbar::shadow" + " cr-toolbar::shadow cr-toolbar-search-field::shadow" + " #clearSearch" + ) + file_link = ( + "downloads-manager::shadow downloads-item::shadow #fileLink" + ) + remove_button = ( + "downloads-manager::shadow #downloadsList" + " downloads-item::shadow #quick-remove" + ) + no_downloads_area = "downloads-manager::shadow #no-downloads" + + self.assert_element(search_icon) + self.type(search_input, "sbase") + self.assert_text(file_name_1, file_link) + print("\n Download 1: %s" % self.get_text(file_link)) + self.type(search_input, "seleniumbase") + self.assert_text(file_name_2, file_link) + print(" Download 2: %s" % self.get_text(file_link)) + self.click(clear_search_icon) + self.type(search_input, "fake-file.zzz") + self.assert_text("No search results found", no_downloads_area) + self.click(clear_search_icon) + self.assert_element(remove_button) + + # Delete the downloaded files from the [Downloads Folder] + self.delete_downloaded_file_if_present(file_name_1, browser=True) + self.delete_downloaded_file_if_present(file_name_2, browser=True) diff --git a/examples/test_show_file_choosers.py b/examples/test_show_file_choosers.py new file mode 100644 index 00000000000..9e29b483757 --- /dev/null +++ b/examples/test_show_file_choosers.py @@ -0,0 +1,28 @@ +"""self.show_file_choosers() is used to show hidden file-upload fields. +Verify that one can choose a file after the hidden input is visible.""" +import os +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class FileUpload(BaseCase): + def test_show_file_choosers(self): + self.goto("https://seleniumbase.io/apps/img_upload") + self.wait_for_element('img[alt="ImgBB"]') + choose_file_selector = 'input[type="file"]' + uploaded_image = "#anywhere-upload-queue li.queue-item" + self.assert_element_not_visible(choose_file_selector) + self.show_file_choosers() + self.highlight(choose_file_selector) + self.assert_element(choose_file_selector) + self.assert_attribute(choose_file_selector, "value", "") + self.assert_element_not_visible(uploaded_image) + dir_name = os.path.dirname(os.path.abspath(__file__)) + my_file = "screenshot.png" + file_path = os.path.join(dir_name, "example_logs/%s" % my_file) + self.choose_file(choose_file_selector, file_path) + if self.browser != "safari": + seen_path = "%s\\%s" % ("C:\\fakepath", my_file) + self.assert_attribute(choose_file_selector, "value", seen_path) + self.demo_mode = True + self.assert_element(uploaded_image) diff --git a/examples/test_simple_login.py b/examples/test_simple_login.py new file mode 100644 index 00000000000..330023710c6 --- /dev/null +++ b/examples/test_simple_login.py @@ -0,0 +1,15 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class TestSimpleLogin(BaseCase): + def test_simple_login(self): + self.goto("https://seleniumbase.io/simple/login") + self.type("#username", "demo_user") + self.type("#password", "secret_pass") + self.click('a:contains("Sign in")') + self.assert_exact_text("Welcome!", "h1") + self.assert_element("img#image1") + self.highlight("#image1") + self.click_link("Sign out") + self.assert_text("signed out", "#top_message") diff --git a/examples/test_suite.py b/examples/test_suite.py old mode 100755 new mode 100644 index e5c22df88ac..dc9dacf2d74 --- a/examples/test_suite.py +++ b/examples/test_suite.py @@ -1,32 +1,31 @@ -''' NOTE: This test suite contains 2 passing tests and 2 failing tests. ''' - +"""This test suite contains 2 passing tests and 2 failing tests.""" import pytest from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) class MyTestSuite(BaseCase): - def test_1(self): - self.open("https://xkcd.com/1663/") - self.assert_text("Garden", "div#ctitle", timeout=3) - for p in range(4): + self.goto("https://xkcd.com/1722/") + self.assert_text("Debugging", "div#ctitle", timeout=4) + for p in range(3): self.click('a[rel="next"]') - self.assert_text("Algorithms", "div#ctitle", timeout=3) + self.assert_text("Linear Regression", "div#ctitle", timeout=4) @pytest.mark.expected_failure def test_2(self): - print("\n(This test fails on purpose)") - self.open("https://xkcd.com/1675/") - raise Exception("FAKE EXCEPTION: This test fails on purpose.") + print("\n(This test should fail)") + self.goto("https://xkcd.com/1373/") + self.assert_text("FakeText", "div#ctitle", timeout=0.4) def test_3(self): - self.open("https://xkcd.com/1406/") - self.assert_text("Universal Converter Box", "div#ctitle", timeout=3) - self.open("https://xkcd.com/608/") - self.assert_text("Form", "div#ctitle", timeout=3) + self.goto("https://xkcd.com/2224/") + self.assert_text("Software Updates", "div#ctitle", timeout=4) + self.goto("https://xkcd.com/608/") + self.assert_exact_text("Form", "div#ctitle", timeout=4) @pytest.mark.expected_failure def test_4(self): - print("\n(This test fails on purpose)") - self.open("https://xkcd.com/1670/") - self.assert_element("FakeElement.DoesNotExist", timeout=0.5) + print("\n(This test should fail)") + self.goto("https://xkcd.com/2224/") + self.assert_element("FakeElement.DoesNotExist", timeout=0.4) diff --git a/examples/test_swag_labs.py b/examples/test_swag_labs.py new file mode 100644 index 00000000000..794eefaaf0e --- /dev/null +++ b/examples/test_swag_labs.py @@ -0,0 +1,81 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class SwagLabsTests(BaseCase): + def login_to_swag_labs(self, username="standard_user"): + """Login to Swag Labs and verify success.""" + self.goto("https://www.saucedemo.com") + if username not in self.get_text("#login_credentials"): + self.fail("Invalid user for login: %s" % username) + self.type("#user-name", username) + self.type("#password", "secret_sauce") + self.click('input[type="submit"]') + self.assert_element("div.inventory_list") + self.assert_element('.inventory_item:contains("Sauce Labs Backpack")') + + def test_swag_labs_basic_flow(self): + """This test checks functional flow of the Swag Labs store.""" + self.login_to_swag_labs(username="standard_user") + + # Verify that the "Test.allTheThings() T-Shirt" appears on the page + item_name = "Test.allTheThings() T-Shirt" + self.assert_text(item_name) + + # Verify that a reverse-alphabetical sort works as expected + self.select_option_by_value("select.product_sort_container", "za") + if item_name not in self.get_text("div.inventory_item"): + self.fail('Sort Failed! Expecting "%s" on top!' % item_name) + + # Add the "Test.allTheThings() T-Shirt" to the cart + self.assert_exact_text("Add to cart", "button.btn_inventory") + item_price = self.get_text("div.inventory_item_price") + self.click("button.btn_inventory") + self.assert_exact_text("Remove", "button.btn_inventory") + self.assert_exact_text("1", "span.shopping_cart_badge") + + # Verify your cart + self.click("#shopping_cart_container a") + self.assert_element('span:contains("Your Cart")') + self.assert_text(item_name, "div.inventory_item_name") + self.assert_exact_text("1", "div.cart_quantity") + self.assert_exact_text("Remove", "button.cart_button") + self.assert_element("button#continue-shopping") + + # Checkout - Add info + self.click("button#checkout") + self.assert_element('span:contains("Checkout: Your Information")') + self.assert_element("button#cancel") + self.type("#first-name", "SeleniumBase") + self.type("#last-name", "Rocks") + self.type("#postal-code", "01720") + + # Checkout - Overview + self.click("input#continue") + self.assert_element('span:contains("Checkout: Overview")') + self.assert_element("button#cancel") + self.assert_text(item_name, "div.inventory_item_name") + self.assert_text(item_price, "div.inventory_item_price") + self.assert_exact_text("1", "div.cart_quantity") + + # Finish Checkout and verify that the cart is now empty + self.click("button#finish") + self.assert_exact_text("Thank you for your order!", "h2") + self.click("#shopping_cart_container a") + self.assert_element_absent("div.inventory_item_name") + self.click("button#continue-shopping") + self.assert_element_absent("span.shopping_cart_badge") + + def tearDown(self): + self.save_teardown_screenshot() # Only if a test fails + # Reset App State and Logout if the controls are present + try: + self.wait_for_ready_state_complete() + if self.is_element_visible("#react-burger-menu-btn"): + self.click("#react-burger-menu-btn") + self.wait_for_element("a#reset_sidebar_link") + self.js_click_if_present("a#reset_sidebar_link") + self.js_click_if_present("a#logout_sidebar_link") + except Exception: + pass + super().tearDown() diff --git a/examples/test_tinymce.py b/examples/test_tinymce.py new file mode 100644 index 00000000000..733fc755a1e --- /dev/null +++ b/examples/test_tinymce.py @@ -0,0 +1,29 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class TinyMceTests(BaseCase): + def test_tinymce(self): + if self.headless: + self.goto_if_not_url("about:blank") + self.skip("Skip this test in headless mode!") + self.goto("https://seleniumbase.io/tinymce/") + self.wait_for_element("div.mce-container-body") + self.click('span:contains("File")') + self.click('span:contains("New document")') + self.click('span:contains("Paragraph")') + self.click('span:contains("Heading 2")') + self.switch_to_frame("iframe") + self.add_text("#tinymce", "Automate anything with SeleniumBase!\n") + self.switch_to_parent_frame() + self.click("button i.mce-i-image") + self.type('input[aria-label="Width"].mce-textbox', "300") + image_url = "https://seleniumbase.github.io/img/sb_logo_10.png" + self.type("input.mce-textbox", image_url + "\n") + with self.frame_switch("iframe"): + self.click("h2") + self.post_message("Automate anything with SeleniumBase!") + self.click('span:contains("File")') + self.click('span:contains("Preview")') + self.switch_to_frame('iframe[sandbox="allow-scripts"]') + self.post_message("Learn SeleniumBase Today!") diff --git a/examples/test_todomvc.py b/examples/test_todomvc.py new file mode 100644 index 00000000000..c04022d3b0b --- /dev/null +++ b/examples/test_todomvc.py @@ -0,0 +1,38 @@ +from parameterized import parameterized +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class TodoMVC(BaseCase): + def setUp(self): + super().setUp() + self.open_new_window() + + def tearDown(self): + self.save_teardown_screenshot() + self.driver.close() + super().tearDown() + + @parameterized.expand([["jquery"], ["react"], ["vue"]]) + def test_todomvc(self, framework): + self.goto("https://todomvc.com/") + self.clear_local_storage() + self.click('a[href*="examples/%s/dist"]' % framework) + self.assert_element("section.todoapp") + self.assert_text("todos", "header h1") + self.wait_for_ready_state_complete() + title = self.get_title() + self.assert_in(framework, title.lower()) + new_todo_input = "input.new-todo" + todo_count_span = "span.todo-count" + self.wait_for_ready_state_complete() + self.type(new_todo_input, "Learn Python\n") + self.type(new_todo_input, "Learn JavaScript\n") + self.type(new_todo_input, "Learn SeleniumBase\n") + self.assert_text("3 items left", todo_count_span) + self.check_if_unchecked("ul.todo-list li input") + self.check_if_unchecked("ul.todo-list li:nth-of-type(2) input") + self.check_if_unchecked("ul.todo-list li:nth-of-type(3) input") + self.assert_text("0 items left", todo_count_span) + self.click("button.clear-completed") + self.assert_element_not_visible(todo_count_span) diff --git a/examples/test_url_asserts.py b/examples/test_url_asserts.py new file mode 100644 index 00000000000..e6a2f0e6e5f --- /dev/null +++ b/examples/test_url_asserts.py @@ -0,0 +1,12 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class URLTestClass(BaseCase): + def test_url_asserts(self): + self.goto("https://seleniumbase.io/help_docs/how_it_works/") + self.assert_url("https://seleniumbase.io/help_docs/how_it_works/") + self.assert_title_contains("How it Works") + self.js_click('nav a:contains("Coffee Cart")') + self.assert_url_contains("/coffee") + self.assert_title("Coffee Cart") diff --git a/examples/test_usefixtures.py b/examples/test_usefixtures.py new file mode 100644 index 00000000000..65d00b5d527 --- /dev/null +++ b/examples/test_usefixtures.py @@ -0,0 +1,23 @@ +import pytest +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +@pytest.mark.usefixtures("sb") +class Test_UseFixtures: + def test_usefixtures_on_class(self): + if not hasattr(self, "sb"): + print("This test is for pytest only!") + return + sb: BaseCase = self.sb + sb.goto("https://seleniumbase.io/realworld/login") + sb.type("#username", "demo_user") + sb.type("#password", "secret_pass") + sb.enter_mfa_code("#totpcode", "GAXG2MTEOR3DMMDG") # 6-digit + sb.assert_text("Welcome!", "h1") + sb.highlight("img#image1") # A fancier assert_element() call + sb.click('a:contains("This Page")') # Use :contains() on any tag + sb.save_screenshot_to_logs() # In "./latest_logs/" folder. + sb.click_link("Sign out") # Link must be "a" tag. Not "button". + sb.assert_element('a:contains("Sign in")') + sb.assert_exact_text("You have been signed out!", "#top_message") diff --git a/examples/test_verify_chromedriver.py b/examples/test_verify_chromedriver.py new file mode 100644 index 00000000000..57fd8fa4ca4 --- /dev/null +++ b/examples/test_verify_chromedriver.py @@ -0,0 +1,33 @@ +import sys +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class ChromedriverTests(BaseCase): + def test_fail_if_versions_dont_match(self): + self.goto("about:blank") + if self.browser != "chrome": + print("\n This test is only for Chrome!") + self.skip("This test is only for Chrome!") + chrome_version = self.get_chrome_version() + major_chrome_version = chrome_version.split(".")[0] + chromedriver_version = self.get_chromedriver_version() + major_chromedriver_version = chromedriver_version.split(".")[0] + install_sb = "sbase get chromedriver %s" % major_chrome_version + arg_join = " ".join(sys.argv) + message = ( + 'Your version of chromedriver: "%s"\n ' + 'does not match your version of Chrome: "%s"\n' + 'Run this command to fix that: "%s"' + % (chromedriver_version, chrome_version, install_sb) + ) + if "--driver-version=" in arg_join or "--driver-version=" in arg_join: + if int(major_chromedriver_version) != int(major_chrome_version): + print("\nWarning -> " + message) + elif int(major_chromedriver_version) != int(major_chrome_version): + raise Exception(message) + else: + print( + "\n* Chrome version: {%s}\n* Driver version: {%s}" + % (chromedriver_version, chrome_version) + ) diff --git a/examples/test_window_switching.py b/examples/test_window_switching.py new file mode 100644 index 00000000000..900f98c046f --- /dev/null +++ b/examples/test_window_switching.py @@ -0,0 +1,24 @@ +""" +Sometimes tests open new tabs/windows, and you'll need +to switch to them first in order to interact with them. +The starting window is window(0). Then increments by 1. +""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class TabSwitchingTests(BaseCase): + def test_switch_to_tabs(self): + self.goto("about:blank") + self.get_new_driver() + self.goto("data:text/html,

      Page A

      ") + self.assert_text("Page A") + self.open_new_window() + self.goto("data:text/html,

      Page B

      ") + self.assert_text("Page B") + self.switch_to_window(0) + self.assert_text("Page A") + self.assert_text_not_visible("Page B") + self.switch_to_window(-1) + self.assert_text("Page B") + self.assert_text_not_visible("Page A") diff --git a/examples/test_xfail.py b/examples/test_xfail.py new file mode 100644 index 00000000000..480de20c473 --- /dev/null +++ b/examples/test_xfail.py @@ -0,0 +1,14 @@ +"""Testing the @pytest.mark.xfail marker. +https://docs.pytest.org/en/latest/skipping.html +(The test is expected to fail, but don't fail the entire build for it.)""" +import pytest +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class XFailTests(BaseCase): + @pytest.mark.xfail + def test_xfail(self): + self.goto("https://xkcd.com/376/") + self.sleep(1) # Time to read the comic + self.fail("There is a known bug here!") diff --git a/examples/test_xkcd.py b/examples/test_xkcd.py new file mode 100644 index 00000000000..546c2aef6e3 --- /dev/null +++ b/examples/test_xkcd.py @@ -0,0 +1,16 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class MyTestClass(BaseCase): + def test_xkcd(self): + self.goto("https://xkcd.com/353/") + self.assert_title("xkcd: Python") + self.assert_element('img[alt="Python"]') + self.click('a[rel="license"]') + self.assert_text("free to copy and reuse") + self.go_back() + self.click_link("About") + self.assert_exact_text("xkcd.com", "h2") + self.click_link("comic #249") + self.assert_element('img[alt*="Chess"]') diff --git a/examples/time_limit_test.py b/examples/time_limit_test.py new file mode 100644 index 00000000000..a0e75470c2a --- /dev/null +++ b/examples/time_limit_test.py @@ -0,0 +1,28 @@ +import pytest +from seleniumbase import BaseCase +from seleniumbase import decorators +BaseCase.main(__name__, __file__) + + +class TimeLimitTests(BaseCase): + @pytest.mark.expected_failure + def test_runtime_limit_decorator(self): + """This test fails on purpose to show the runtime_limit() decorator + for code blocks that run longer than the time limit specified.""" + print("\n(This test should fail)") + self.goto("https://xkcd.com/2511") + with decorators.runtime_limit(0.7): + self.sleep(0.95) + + @pytest.mark.expected_failure + def test_set_time_limit_method(self): + """This test fails on purpose to show the set_time_limit() method + for tests that run longer than the time limit specified (seconds). + The time-limit clock starts after the browser has fully launched, + which is after pytest starts it's own internal clock for tests. + Usage: (inside tests) => self.set_time_limit(SECONDS) + Usage: (command-line) => --time-limit=SECONDS""" + self.set_time_limit(2.2) # Fail test if time exceeds 2.2 seconds + print("\n(This test should fail)") + self.goto("https://xkcd.com/1658") + self.sleep(3) diff --git a/examples/tour_examples/ReadMe.md b/examples/tour_examples/ReadMe.md old mode 100755 new mode 100644 index cb8a89dc281..472e614d2cb --- a/examples/tour_examples/ReadMe.md +++ b/examples/tour_examples/ReadMe.md @@ -1,20 +1,39 @@ -## SeleniumBase Website Tours + -SeleniumBase Tours utilize your choice of 4 different JavaScript libraries for prototyping tours, demos, walkthroughs, and onboarding experiences on any website: **[Shepherd](https://shipshapecode.github.io/shepherd/docs/welcome/)**, **[Bootstrap Tour](http://bootstraptour.com/)**, **[IntroJS](https://introjs.com/)**, and **[Hopscotch](http://linkedin.github.io/hopscotch/)**. Choose your favorite one to use! +

      ๐ŸŒ Interactive Product Tours ๐ŸšŽ

      -Example tour: +SeleniumBase Tours utilize 5 JavaScript libraries for creating interactive walkthroughs on **any website**: +> **[IntroJS](https://introjs.com/)**, **[Bootstrap Tour](http://bootstraptour.com/)**, **[DriverJS](https://kamranahmed.info/driver.js/)**, **[Shepherd](https://shepherdjs.dev/)**, and **[Hopscotch](https://linkedinattic.github.io/hopscotch/)**. + +A tour demo: (with autoplay) + +
      + +[SeleniumBase maps_introjs_tour.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/tour_examples/maps_introjs_tour.py) + +```zsh +cd examples/tour_examples +pytest maps_introjs_tour.py --interval=1 ``` + +Here's a longer version: + +
      + +[SeleniumBase google_tour.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/tour_examples/google_tour.py) + +```zsh cd examples/tour_examples pytest google_tour.py ``` -
      +> (From [GitHub => SeleniumBase/examples/tour_examples](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/tour_examples)) ### Creating a new tour: -#### To create a tour utilizing the Shepherd JS Library, use one of the following: +#### To create a tour utilizing the Shepherd Library, use one of the following: ``self.create_shepherd_tour()`` @@ -32,7 +51,7 @@ OR ``self.create_tour(theme="bootstrap")`` -#### To create a tour utilizing the Intro JS Library, use one of the following: +#### To create a tour utilizing the IntroJS Library, use one of the following: ``self.create_introjs_tour()`` @@ -40,7 +59,15 @@ OR ``self.create_tour(theme="introjs")`` -#### To create a tour utilizing the Hopscotch JS Library, use one of the following: +#### To create a tour utilizing the DriverJS Library, use one of the following: + +``self.create_driverjs_tour()`` + +OR + +``self.create_tour(theme="driverjs")`` + +#### To create a tour utilizing the Hopscotch Library, use one of the following: ``self.create_hopscotch_tour()`` @@ -54,7 +81,7 @@ OR ``self.add_tour_step(message, css_selector, title, alignment, theme)`` -With the ``self.add_tour_step()`` method, you must first pass a message to display. You can then specify a web element to attach to (by using [CSS selectors](https://www.w3schools.com/cssref/css_selectors.asp)). If no element is specified, the tour step will tether to the top of the screen by default. You can also add an optional title above the message to display with the tour step, as well as change the theme for that step (Shepherd tours only), and even specify the alignment (which is the side of the element that you want the tour message to tether to). +> With the ``self.add_tour_step()`` method, you must first pass a message to display. You can then specify a web element to attach to (by using [CSS selectors](https://www.w3schools.com/cssref/css_selectors.asp)). If no element is specified, the tour step will tether to the top of the screen by default. You can also add an optional title above the message to display with the tour step, as well as change the theme for that step (Shepherd tours only), and even specify the alignment (which is the side of the element that you want the tour message to tether to). ### Playing a tour: @@ -63,43 +90,122 @@ You can play a tour by calling: ``self.play_tour(interval)`` - If you specify an interval (optional), the tour will automatically walk through each step after that many seconds have passed. +> If you specify an ``interval`` (optional), the tour will automatically walk through each step after that many seconds have passed. All methods have the optional ``name`` argument, which is only needed if you're creating multiple tours at once. Then, when you're adding a step or playing a tour, SeleniumBase knows which tour you're referring too. You can avoid using the ``name`` arg for multiple tours if you play a tour before creating a new one. -### Here's an example of using SeleniumBase Tours: +### Here's how the code looks: ```python from seleniumbase import BaseCase +BaseCase.main(__name__, __file__, "--uc") -class MyTourClass(BaseCase): +class MyTourClass(BaseCase): def test_google_tour(self): - self.open('https://google.com') - self.wait_for_element('input[title="Search"]') - + if not self.undetectable: + self.get_new_driver(undetectable=True) + self.goto("https://google.com/ncr") + self.click_if_visible('button:contains("Accept all")') + self.wait_for_element('[title="Search"]') + self.hide_elements("iframe") + + # Create a website tour using the ShepherdJS library with "dark" theme + # Same as: self.create_shepherd_tour(theme="dark") self.create_tour(theme="dark") - self.add_tour_step( - "Click to begin the Google Tour!", title="SeleniumBase Tours") - self.add_tour_step( - "Type in your search query here.", 'input[title="Search"]') + self.add_tour_step("Welcome to Google!", title="SeleniumBase Tours") + self.add_tour_step("Type in your query here.", '[title="Search"]') self.play_tour() - self.highlight_update_text('input[title="Search"]', "Google") + self.highlight_type('[title="Search"]', "Google") self.wait_for_element('[role="listbox"]') # Wait for autocomplete + # Create a website tour using the ShepherdJS library with "light" theme + # Same as: self.create_shepherd_tour(theme="light") self.create_tour(theme="light") + self.add_tour_step("Then click to search.", '[value="Google Search"]') + self.add_tour_step("Or press [ENTER] after entry.", '[title="Search"]') + self.play_tour() + + self.highlight_type('[title="Search"]', "GitHub\n") + self.ad_block() + self.wait_for_element("#search") + + # Create a website tour using the Bootstrap Tour JS library + # Same as: self.create_bootstrap_tour() + self.create_tour(theme="bootstrap") + self.add_tour_step("3-second autoplay...") + self.add_tour_step("Here's the next tour:") + self.play_tour(interval=3) # Tour automatically continues after 3 sec + + self.goto("https://www.google.com/maps/@42.3591234,-71.0915634,15z") + self.wait_for_element('[name="q"]', timeout=20) + self.wait_for_element('[aria-label="Interactive map"]', timeout=20) + self.wait_for_element('[aria-label="Zoom in"]', timeout=20) + self.wait_for_element('[aria-label="Zoom out"]') + self.wait_for_element('[jsaction*="minimap.main;"]') + self.sleep(0.5) + + # Create a website tour using the IntroJS library + # Same as: self.create_introjs_tour() + self.create_tour(theme="introjs") + self.add_tour_step("Welcome to Google Maps", title="SeleniumBase Tour") + self.add_tour_step( + "The location goes here.", '[name="q"]', title="Search Box" + ) + self.add_tour_step( + "Then click here to show it on the map.", + '[aria-label="Search"]', + alignment="bottom", + ) self.add_tour_step( - "Then click here to search.", 'input[value="Google Search"]') + "Or click here to get driving directions.", + 'button[aria-label="Directions"]', + alignment="bottom", + ) self.add_tour_step( - "Or press [ENTER] after typing a query here.", '[title="Search"]') + "Use this button to switch to Satellite view.", + 'button[jsaction*="minimap.main;"]', + alignment="right", + ) + self.add_tour_step( + "Click here to zoom in.", + '[aria-label="Zoom in"]', + alignment="left", + ) + self.add_tour_step( + "Or click here to zoom out.", + '[aria-label="Zoom out"]', + alignment="left", + ) + if self.is_element_visible('button[jsaction*="settings.open;"]'): + self.add_tour_step( + "Use the Menu button to see more options.", + 'button[jsaction*="settings.open;"]', + alignment="right", + ) + elif self.is_element_visible('button[jsaction="navigationrail.more"]'): + self.add_tour_step( + "Use the Menu button to see more options.", + 'button[jsaction="navigationrail.more"]', + alignment="right", + ) + self.add_tour_step( + "Or click here to see more Google apps.", + '[aria-label="Google apps"]', + alignment="left", + ) + self.add_tour_step( + "Thanks for using SeleniumBase Tours!", title="End of Guided Tour" + ) + self.export_tour() # The default name for exports is "my_tour.js" self.play_tour() ``` -#### This example was taken from [google_tour.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/tour_examples/google_tour.py), which you can run from the ``examples/tour_examples`` folder with the following command: +#### That code is from [google_tour.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/tour_examples/google_tour.py), which you can run from the ``tour_examples/`` folder with the following command: -```bash +```zsh pytest google_tour.py ``` @@ -113,4 +219,10 @@ OR ``self.export_tour(name=None, filename="my_tour.js")`` -(``name`` is optional unless you gave custom names to your tours when you created them. ``filename`` is the name of the file to save the JavaScript to.) Once you've exported your tour, you can use it outside of SeleniumBase. You can even copy the tour's JavaScript code to the Console of your web browser to play the tour from there (you need to be on the correct web page for it to work). +> (``name`` is optional unless you gave custom names to your tours when you created them. ``filename`` is the name of the file to save the JavaScript to.) Once you've exported your tour, you can use it outside of SeleniumBase. You can even copy the tour's JavaScript code to the Console of your web browser to play the tour from there (you need to be on the correct web page for it to work). + +-------- + +
      + +

      SeleniumBase

      diff --git a/examples/tour_examples/bootstrap_google_tour.py b/examples/tour_examples/bootstrap_google_tour.py old mode 100755 new mode 100644 index f59bfa44a9c..f2c4a78dd03 --- a/examples/tour_examples/bootstrap_google_tour.py +++ b/examples/tour_examples/bootstrap_google_tour.py @@ -1,66 +1,95 @@ from seleniumbase import BaseCase +BaseCase.main(__name__, __file__, "--uc") class MyTourClass(BaseCase): - def test_google_tour(self): - self.open('https://google.com') - self.wait_for_element('input[title="Search"]') + if not self.undetectable: + self.get_new_driver(undetectable=True) + self.goto("https://google.com/ncr") + self.click_if_visible('button:contains("Accept all")') + self.wait_for_element('[title="Search"]') + self.hide_elements("iframe") self.create_bootstrap_tour() # OR self.create_tour(theme="bootstrap") - self.add_tour_step( - "Click to begin the Google Tour!", title="SeleniumBase Tours") - self.add_tour_step( - "Type in your search query here.", 'input[title="Search"]') + self.add_tour_step("Welcome to Google!", title="SeleniumBase Tours") + self.add_tour_step("Type in your query here.", '[title="Search"]') self.play_tour() - self.highlight_update_text('input[title="Search"]', "Google") + self.highlight_type('[title="Search"]', "Google") self.wait_for_element('[role="listbox"]') # Wait for autocomplete self.create_bootstrap_tour() - self.add_tour_step( - "Then click here to search.", 'input[value="Google Search"]') - self.add_tour_step( - "Or press [ENTER] after typing a query here.", '[title="Search"]') + self.add_tour_step("Then click to search.", '[value="Google Search"]') + self.add_tour_step("Or press [ENTER] after entry.", '[title="Search"]') self.play_tour() - self.highlight_update_text('input[title="Search"]', "GitHub\n") + self.highlight_type('[title="Search"]', "GitHub\n") + self.ad_block() self.wait_for_element("#search") self.create_bootstrap_tour() - self.add_tour_step( - "Search results appear here!", title="(5-second autoplay on)") - self.add_tour_step("Let's take another tour:") - self.play_tour(interval=5) # Tour automatically continues after 5 sec + self.add_tour_step("3-second autoplay...") + self.add_tour_step("Here's the next tour:") + self.play_tour(interval=3) # Tour automatically continues after 3 sec - self.open("https://www.google.com/maps/@42.3598616,-71.0912631,15z") - self.wait_for_element('input#searchboxinput') + self.goto("https://www.google.com/maps/@42.3591234,-71.0915634,15z") + self.wait_for_element('[name="q"]', timeout=20) + self.wait_for_element('[aria-label="Interactive map"]', timeout=20) + self.wait_for_element('[aria-label="Zoom in"]', timeout=20) + self.wait_for_element('[aria-label="Zoom out"]') + self.wait_for_element('[jsaction*="minimap.main;"]') + self.sleep(0.5) self.create_bootstrap_tour() - self.add_tour_step("Welcome to Google Maps!") + self.add_tour_step("Welcome to Google Maps", title="SeleniumBase Tour") self.add_tour_step( - "Type in a location here.", "#searchboxinput", title="Search Box") + "The location goes here.", '[name="q"]', title="Search Box" + ) self.add_tour_step( "Then click here to show it on the map.", - "#searchbox-searchbutton", alignment="bottom") + '[aria-label="Search"]', + alignment="bottom", + ) self.add_tour_step( "Or click here to get driving directions.", - "#searchbox-directions", alignment="bottom") + 'button[aria-label="Directions"]', + alignment="bottom", + ) self.add_tour_step( "Use this button to switch to Satellite view.", - "div.widget-minimap", alignment="right") - self.add_tour_step( - "Click here to zoom in.", "#widget-zoom-in", alignment="left") + 'button[jsaction*="minimap.main;"]', + alignment="right", + ) self.add_tour_step( - "Or click here to zoom out.", "#widget-zoom-out", alignment="left") + "Click here to zoom in.", + '[aria-label="Zoom in"]', + alignment="left", + ) self.add_tour_step( - "Use the Menu button to see more options.", - ".searchbox-hamburger-container", alignment="right") + "Or click here to zoom out.", + '[aria-label="Zoom out"]', + alignment="left", + ) + if self.is_element_visible('button[jsaction*="settings.open;"]'): + self.add_tour_step( + "Use the Menu button to see more options.", + 'button[jsaction*="settings.open;"]', + alignment="right", + ) + elif self.is_element_visible('button[jsaction="navigationrail.more"]'): + self.add_tour_step( + "Use the Menu button to see more options.", + 'button[jsaction="navigationrail.more"]', + alignment="right", + ) self.add_tour_step( - "Or click here to see more Google apps.", '[title="Google apps"]', - alignment="left") + "Or click here to see more Google apps.", + '[aria-label="Google apps"]', + alignment="left", + ) self.add_tour_step( - "Thanks for trying out SeleniumBase Tours!", - title="End of Guided Tour") + "Thanks for using SeleniumBase Tours!", title="End of Guided Tour" + ) self.export_tour(filename="bootstrap_google_maps_tour.js") self.play_tour() diff --git a/examples/tour_examples/bootstrap_xkcd_tour.py b/examples/tour_examples/bootstrap_xkcd_tour.py new file mode 100644 index 00000000000..50af52f1c09 --- /dev/null +++ b/examples/tour_examples/bootstrap_xkcd_tour.py @@ -0,0 +1,20 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class MyTestClass(BaseCase): + def test_bootstrap_tour(self): + self.goto("https://xkcd.com/1117/") + self.assert_element('img[alt="My Sky"]') + self.create_bootstrap_tour() + self.add_tour_step("Welcome to XKCD!") + self.add_tour_step("This is the XKCD logo.", "#masthead img") + self.add_tour_step("Here's the daily webcomic.", "#comic img") + self.add_tour_step("This is the title.", "#ctitle", alignment="top") + self.add_tour_step("Click here for the next comic.", 'a[rel="next"]') + self.add_tour_step("Click here for the previous one.", 'a[rel="prev"]') + self.add_tour_step("Learn about the author here.", 'a[rel="author"]') + self.add_tour_step("Click for a random comic.", 'a[href*="/random/"]') + self.add_tour_step("Thanks for taking this tour!") + self.export_tour(filename="bootstrap_xkcd_tour.js") + self.play_tour() diff --git a/examples/tour_examples/driverjs_maps_tour.py b/examples/tour_examples/driverjs_maps_tour.py new file mode 100644 index 00000000000..7a95aa86124 --- /dev/null +++ b/examples/tour_examples/driverjs_maps_tour.py @@ -0,0 +1,64 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class MyTestClass(BaseCase): + def test_create_tour(self): + self.goto("https://www.google.com/maps/@42.3591234,-71.0915634,15z") + self.wait_for_element('[name="q"]', timeout=20) + self.wait_for_element('[aria-label="Interactive map"]', timeout=20) + self.wait_for_element('[aria-label="Zoom in"]', timeout=20) + self.wait_for_element('[aria-label="Zoom out"]') + self.wait_for_element('[jsaction*="minimap.main;"]') + self.sleep(0.5) + + # Create a website tour using the DriverJS library + # Same as: self.create_driverjs_tour() + self.create_tour(theme="driverjs") + self.add_tour_step("Welcome to Google Maps", title="SeleniumBase Tour") + self.add_tour_step( + "The location goes here.", '[name="q"]', title="Search Box" + ) + self.add_tour_step( + "Then click here to show it on the map.", + '[aria-label="Search"]', + alignment="bottom", + ) + self.add_tour_step( + "Or click here to get driving directions.", + 'button[aria-label="Directions"]', + alignment="bottom", + ) + self.add_tour_step( + "Use this button to switch to Satellite view.", + 'button[jsaction*="minimap.main;"]', + alignment="right", + ) + self.add_tour_step( + "Click here to zoom in.", "#widget-zoom-in", alignment="left" + ) + self.add_tour_step( + "Or click here to zoom out.", "#widget-zoom-out", alignment="left" + ) + if self.is_element_visible('button[jsaction*="settings.open;"]'): + self.add_tour_step( + "Use the Menu button to see more options.", + 'button[jsaction*="settings.open;"]', + alignment="right", + ) + elif self.is_element_visible('button[jsaction="navigationrail.more"]'): + self.add_tour_step( + "Use the Menu button to see more options.", + 'button[jsaction="navigationrail.more"]', + alignment="right", + ) + self.add_tour_step( + "Or click here to see more Google apps.", + '[aria-label="Google apps"]', + alignment="left", + ) + self.add_tour_step( + "Thanks for using SeleniumBase Tours!", title="End of Guided Tour" + ) + self.export_tour() # The default name for exports is "my_tour.js" + self.play_tour(interval=0) # If interval > 0, autoplay after N seconds diff --git a/examples/tour_examples/google_tour.py b/examples/tour_examples/google_tour.py old mode 100755 new mode 100644 index 66cd3dd5e80..314642bded4 --- a/examples/tour_examples/google_tour.py +++ b/examples/tour_examples/google_tour.py @@ -1,75 +1,103 @@ from seleniumbase import BaseCase +BaseCase.main(__name__, __file__, "--uc") class MyTourClass(BaseCase): - def test_google_tour(self): - self.open('https://google.com') - self.wait_for_element('input[title="Search"]') + if not self.undetectable: + self.get_new_driver(undetectable=True) + self.goto("https://google.com/ncr") + self.click_if_visible('button:contains("Accept all")') + self.wait_for_element('[title="Search"]') + self.hide_elements("iframe") # Create a website tour using the ShepherdJS library with "dark" theme # Same as: self.create_shepherd_tour(theme="dark") self.create_tour(theme="dark") - self.add_tour_step( - "Click to begin the Google Tour!", title="SeleniumBase Tours") - self.add_tour_step( - "Type in your search query here.", 'input[title="Search"]') + self.add_tour_step("Welcome to Google!", title="SeleniumBase Tours") + self.add_tour_step("Type in your query here.", '[title="Search"]') self.play_tour() - self.highlight_update_text('input[title="Search"]', "Google") + self.highlight_type('[title="Search"]', "Google") self.wait_for_element('[role="listbox"]') # Wait for autocomplete # Create a website tour using the ShepherdJS library with "light" theme # Same as: self.create_shepherd_tour(theme="light") self.create_tour(theme="light") - self.add_tour_step( - "Then click here to search.", 'input[value="Google Search"]') - self.add_tour_step( - "Or press [ENTER] after typing a query here.", '[title="Search"]') + self.add_tour_step("Then click to search.", '[value="Google Search"]') + self.add_tour_step("Or press [ENTER] after entry.", '[title="Search"]') self.play_tour() - self.highlight_update_text('input[title="Search"]', "GitHub\n") + self.highlight_type('[title="Search"]', "GitHub\n") + self.ad_block() self.wait_for_element("#search") # Create a website tour using the Bootstrap Tour JS library # Same as: self.create_bootstrap_tour() self.create_tour(theme="bootstrap") - self.add_tour_step( - "Search results appear here!", title="(5-second autoplay on)") - self.add_tour_step("Let's take another tour:", theme="light") - self.play_tour(interval=5) # Tour automatically continues after 5 sec + self.add_tour_step("3-second autoplay...") + self.add_tour_step("Here's the next tour:") + self.play_tour(interval=3) # Tour automatically continues after 3 sec - self.open("https://www.google.com/maps/@42.3598616,-71.0912631,15z") - self.wait_for_element('input#searchboxinput') + self.goto("https://www.google.com/maps/@42.3591234,-71.0915634,15z") + self.wait_for_element('[name="q"]', timeout=20) + self.wait_for_element('[aria-label="Interactive map"]', timeout=20) + self.wait_for_element('[aria-label="Zoom in"]', timeout=20) + self.wait_for_element('[aria-label="Zoom out"]') + self.wait_for_element('[jsaction*="minimap.main;"]') + self.sleep(0.5) # Create a website tour using the IntroJS library # Same as: self.create_introjs_tour() self.create_tour(theme="introjs") + self.add_tour_step("Welcome to Google Maps", title="SeleniumBase Tour") self.add_tour_step( - "Welcome to Google Maps!") - self.add_tour_step( - "Type in a location here.", "#searchboxinput", title="Search Box") + "The location goes here.", '[name="q"]', title="Search Box" + ) self.add_tour_step( "Then click here to show it on the map.", - "#searchbox-searchbutton", alignment="bottom") + '[aria-label="Search"]', + alignment="bottom", + ) self.add_tour_step( "Or click here to get driving directions.", - "#searchbox-directions", alignment="bottom") + 'button[aria-label="Directions"]', + alignment="bottom", + ) self.add_tour_step( "Use this button to switch to Satellite view.", - "div.widget-minimap", alignment="right") - self.add_tour_step( - "Click here to zoom in.", "#widget-zoom-in", alignment="left") + 'button[jsaction*="minimap.main;"]', + alignment="right", + ) self.add_tour_step( - "Or click here to zoom out.", "#widget-zoom-out", alignment="left") + "Click here to zoom in.", + '[aria-label="Zoom in"]', + alignment="left", + ) self.add_tour_step( - "Use the Menu button to see more options.", - ".searchbox-hamburger-container", alignment="right") + "Or click here to zoom out.", + '[aria-label="Zoom out"]', + alignment="left", + ) + if self.is_element_visible('button[jsaction*="settings.open;"]'): + self.add_tour_step( + "Use the Menu button to see more options.", + 'button[jsaction*="settings.open;"]', + alignment="right", + ) + elif self.is_element_visible('button[jsaction="navigationrail.more"]'): + self.add_tour_step( + "Use the Menu button to see more options.", + 'button[jsaction="navigationrail.more"]', + alignment="right", + ) self.add_tour_step( - "Or click here to see more Google apps.", '[title="Google apps"]', - alignment="left") + "Or click here to see more Google apps.", + '[aria-label="Google apps"]', + alignment="left", + ) self.add_tour_step( - "Thanks for trying out SeleniumBase Tours!", - title="End of Guided Tour") - self.export_tour() + "Thanks for using SeleniumBase Tours!", title="End of Guided Tour" + ) + self.export_tour() # The default name for exports is "my_tour.js" self.play_tour() diff --git a/examples/tour_examples/hopscotch_google_tour.py b/examples/tour_examples/hopscotch_google_tour.py old mode 100755 new mode 100644 index 35a7bca83ce..871b8f8ee21 --- a/examples/tour_examples/hopscotch_google_tour.py +++ b/examples/tour_examples/hopscotch_google_tour.py @@ -1,67 +1,95 @@ from seleniumbase import BaseCase +BaseCase.main(__name__, __file__, "--uc") class MyTourClass(BaseCase): - def test_google_tour(self): - self.open('https://google.com') - self.wait_for_element('input[title="Search"]') + if not self.undetectable: + self.get_new_driver(undetectable=True) + self.goto("https://google.com/ncr") + self.click_if_visible('button:contains("Accept all")') + self.wait_for_element('[title="Search"]') + self.hide_elements("iframe") self.create_hopscotch_tour() # OR self.create_tour(theme="hopscotch") - self.add_tour_step( - "Click to begin the Google Tour!", title="SeleniumBase Tours") - self.add_tour_step( - "Type in your search query here.", 'input[title="Search"]') + self.add_tour_step("Welcome to Google!", title="SeleniumBase Tours") + self.add_tour_step("Type in your query here.", '[title="Search"]') self.play_tour() - self.highlight_update_text('input[title="Search"]', "Google") + self.highlight_type('[title="Search"]', "Google") self.wait_for_element('[role="listbox"]') # Wait for autocomplete self.create_hopscotch_tour() - self.add_tour_step( - "Then click here to search.", 'input[value="Google Search"]', - alignment="top") - self.add_tour_step( - "Or press [ENTER] after typing a query here.", '[title="Search"]') + self.add_tour_step("Then click to search.", '[value="Google Search"]') + self.add_tour_step("Or press [ENTER] after entry.", '[title="Search"]') self.play_tour() - self.highlight_update_text('input[title="Search"]', "GitHub\n") + self.highlight_type('[title="Search"]', "GitHub\n") + self.ad_block() self.wait_for_element("#search") self.create_hopscotch_tour() - self.add_tour_step( - "Search results appear here!", title="(5-second autoplay on)") - self.add_tour_step("Let's take another tour:") - self.play_tour(interval=5) # Tour automatically continues after 5 sec + self.add_tour_step("3-second autoplay...") + self.add_tour_step("Here's the next tour:") + self.play_tour(interval=3) # Tour automatically continues after 3 sec - self.open("https://www.google.com/maps/@42.3598616,-71.0912631,15z") - self.wait_for_element('input#searchboxinput') + self.goto("https://www.google.com/maps/@42.3591234,-71.0915634,15z") + self.wait_for_element('[name="q"]', timeout=20) + self.wait_for_element('[aria-label="Interactive map"]', timeout=20) + self.wait_for_element('[aria-label="Zoom in"]', timeout=20) + self.wait_for_element('[aria-label="Zoom out"]') + self.wait_for_element('[jsaction*="minimap.main;"]') + self.sleep(0.5) self.create_hopscotch_tour() - self.add_tour_step("Welcome to Google Maps!") + self.add_tour_step("Welcome to Google Maps", title="SeleniumBase Tour") self.add_tour_step( - "Type in a location here.", "#searchboxinput", title="Search Box") + "The location goes here.", '[name="q"]', title="Search Box" + ) self.add_tour_step( "Then click here to show it on the map.", - "#searchbox-searchbutton", alignment="bottom") + '[aria-label="Search"]', + alignment="bottom", + ) self.add_tour_step( "Or click here to get driving directions.", - "#searchbox-directions", alignment="bottom") + 'button[aria-label="Directions"]', + alignment="bottom", + ) self.add_tour_step( "Use this button to switch to Satellite view.", - "div.widget-minimap", alignment="right") - self.add_tour_step( - "Click here to zoom in.", "#widget-zoom-in", alignment="left") + 'button[jsaction*="minimap.main;"]', + alignment="right", + ) self.add_tour_step( - "Or click here to zoom out.", "#widget-zoom-out", alignment="left") + "Click here to zoom in.", + '[aria-label="Zoom in"]', + alignment="left", + ) self.add_tour_step( - "Use the Menu button to see more options.", - ".searchbox-hamburger-container", alignment="right") + "Or click here to zoom out.", + '[aria-label="Zoom out"]', + alignment="left", + ) + if self.is_element_visible('button[jsaction*="settings.open;"]'): + self.add_tour_step( + "Use the Menu button to see more options.", + 'button[jsaction*="settings.open;"]', + alignment="right", + ) + elif self.is_element_visible('button[jsaction="navigationrail.more"]'): + self.add_tour_step( + "Use the Menu button to see more options.", + 'button[jsaction="navigationrail.more"]', + alignment="right", + ) self.add_tour_step( - "Or click here to see more Google apps.", '[title="Google apps"]', - alignment="left") + "Or click here to see more Google apps.", + '[aria-label="Google apps"]', + alignment="left", + ) self.add_tour_step( - "Thanks for trying out SeleniumBase Tours!", - title="End of Guided Tour") + "Thanks for using SeleniumBase Tours!", title="End of Guided Tour" + ) self.export_tour(filename="hopscotch_google_maps_tour.js") self.play_tour() diff --git a/examples/tour_examples/introjs_google_tour.py b/examples/tour_examples/introjs_google_tour.py old mode 100755 new mode 100644 index 71aac272548..e6f7f1c2cb0 --- a/examples/tour_examples/introjs_google_tour.py +++ b/examples/tour_examples/introjs_google_tour.py @@ -1,66 +1,96 @@ from seleniumbase import BaseCase +BaseCase.main(__name__, __file__, "--uc") class MyTourClass(BaseCase): - def test_google_tour(self): - self.open('https://google.com') - self.wait_for_element('input[title="Search"]') + if not self.undetectable: + self.get_new_driver(undetectable=True) + self.goto("https://google.com/ncr") + self.click_if_visible('button:contains("Accept all")') + self.wait_for_element('[title="Search"]') + self.hide_elements("iframe") self.create_introjs_tour() # OR self.create_tour(theme="introjs") - self.add_tour_step( - "Click to begin the Google Tour!", title="SeleniumBase Tours") - self.add_tour_step( - "Type in your search query here.", 'input[title="Search"]') + self.add_tour_step("Welcome to Google!", title="SeleniumBase Tours") + self.add_tour_step("Type in your query here.", '[title="Search"]') self.play_tour() - self.highlight_update_text('input[title="Search"]', "Google") + self.highlight_type('[title="Search"]', "Google") self.wait_for_element('[role="listbox"]') # Wait for autocomplete self.create_introjs_tour() - self.add_tour_step( - "Then click here to search.", 'input[value="Google Search"]') - self.add_tour_step( - "Or press [ENTER] after typing a query here.", '[title="Search"]') + self.add_tour_step("Then click to search.", '[value="Google Search"]') + self.add_tour_step("Or press [ENTER] after entry.", '[title="Search"]') self.play_tour() - self.highlight_update_text('input[title="Search"]', "GitHub\n") + self.highlight_type('[title="Search"]', "GitHub\n") + self.ad_block() self.wait_for_element("#search") self.create_introjs_tour() - self.add_tour_step( - "Search results appear here!", title="(5-second autoplay on)") - self.add_tour_step("Let's take another tour:") - self.play_tour(interval=5) # Tour automatically continues after 5 sec + self.add_tour_step("3-second autoplay...") + self.add_tour_step("Here's the next tour:") + self.play_tour(interval=3) # Tour automatically continues after 3 sec - self.open("https://www.google.com/maps/@42.3598616,-71.0912631,15z") - self.wait_for_element('input#searchboxinput') + self.goto("https://www.google.com/maps/@42.3591234,-71.0915634,15z") + self.wait_for_element('[name="q"]', timeout=20) + self.wait_for_element('[aria-label="Interactive map"]', timeout=20) + self.wait_for_element('[aria-label="Zoom in"]', timeout=20) + self.wait_for_element('[aria-label="Zoom out"]') + self.wait_for_element('[jsaction*="minimap.main;"]') + self.sleep(0.5) + self.set_introjs_colors("#f26721", "#db5409") self.create_introjs_tour() - self.add_tour_step("Welcome to Google Maps!") + self.add_tour_step("Welcome to Google Maps", title="SeleniumBase Tour") self.add_tour_step( - "Type in a location here.", "#searchboxinput", title="Search Box") + "The location goes here.", '[name="q"]', title="Search Box" + ) self.add_tour_step( "Then click here to show it on the map.", - "#searchbox-searchbutton", alignment="bottom") + '[aria-label="Search"]', + alignment="bottom", + ) self.add_tour_step( "Or click here to get driving directions.", - "#searchbox-directions", alignment="bottom") + 'button[aria-label="Directions"]', + alignment="bottom", + ) self.add_tour_step( "Use this button to switch to Satellite view.", - "div.widget-minimap", alignment="right") - self.add_tour_step( - "Click here to zoom in.", "#widget-zoom-in", alignment="left") + 'button[jsaction*="minimap.main;"]', + alignment="right", + ) self.add_tour_step( - "Or click here to zoom out.", "#widget-zoom-out", alignment="left") + "Click here to zoom in.", + '[aria-label="Zoom in"]', + alignment="left", + ) self.add_tour_step( - "Use the Menu button to see more options.", - ".searchbox-hamburger-container", alignment="right") + "Or click here to zoom out.", + '[aria-label="Zoom out"]', + alignment="left", + ) + if self.is_element_visible('button[jsaction*="settings.open;"]'): + self.add_tour_step( + "Use the Menu button to see more options.", + 'button[jsaction*="settings.open;"]', + alignment="right", + ) + elif self.is_element_visible('button[jsaction="navigationrail.more"]'): + self.add_tour_step( + "Use the Menu button to see more options.", + 'button[jsaction="navigationrail.more"]', + alignment="right", + ) self.add_tour_step( - "Or click here to see more Google apps.", '[title="Google apps"]', - alignment="left") + "Or click here to see more Google apps.", + '[aria-label="Google apps"]', + alignment="left", + ) self.add_tour_step( - "Thanks for trying out SeleniumBase Tours!", - title="End of Guided Tour") + "Thanks for using SeleniumBase Tours!", title="End of Guided Tour" + ) self.export_tour(filename="introjs_google_maps_tour.js") self.play_tour() diff --git a/examples/tour_examples/maps_introjs_tour.py b/examples/tour_examples/maps_introjs_tour.py new file mode 100644 index 00000000000..b73ac6f5679 --- /dev/null +++ b/examples/tour_examples/maps_introjs_tour.py @@ -0,0 +1,69 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class MyTourClass(BaseCase): + def test_google_maps_tour(self): + self.goto("https://www.google.com/maps/@42.3591234,-71.0915634,15z") + self.wait_for_element('[name="q"]', timeout=20) + self.wait_for_element('[aria-label="Interactive map"]', timeout=20) + self.wait_for_element('[aria-label="Zoom in"]', timeout=20) + self.wait_for_element('[aria-label="Zoom out"]') + self.wait_for_element('[jsaction*="minimap.main;"]') + self.sleep(0.5) + + self.create_tour(theme="introjs") + self.add_tour_step( + "Welcome to Google Maps", title="โœ… SeleniumBase Tours ๐ŸŒŽ" + ) + self.add_tour_step( + "The location goes here.", '[name="q"]', title="Search Box" + ) + self.add_tour_step( + "Then click here to show it on the map.", + '[aria-label="Search"]', + alignment="bottom", + ) + self.add_tour_step( + "Or click here to get driving directions.", + 'button[aria-label="Directions"]', + alignment="bottom", + ) + self.add_tour_step( + "Use this button to switch to Satellite view.", + 'button[jsaction*="minimap.main;"]', + alignment="right", + ) + self.add_tour_step( + "Click here to zoom in.", + '[aria-label="Zoom in"]', + alignment="left", + ) + self.add_tour_step( + "Or click here to zoom out.", + '[aria-label="Zoom out"]', + alignment="left", + ) + if self.is_element_visible('button[jsaction*="settings.open;"]'): + self.add_tour_step( + "Use the Menu button to see more options.", + 'button[jsaction*="settings.open;"]', + alignment="right", + ) + elif self.is_element_visible('button[jsaction="navigationrail.more"]'): + self.add_tour_step( + "Use the Menu button to see more options.", + 'button[jsaction="navigationrail.more"]', + alignment="right", + ) + self.add_tour_step( + "Or click here to see more Google apps.", + '[aria-label="Google apps"]', + alignment="left", + ) + self.add_tour_step( + "Thanks for using SeleniumBase Tours!", + title="๐Ÿšƒ End of Guided Tour ๐Ÿšƒ", + ) + self.export_tour(filename="maps_introjs_tour.js") + self.play_tour() diff --git a/examples/tour_examples/octocat_tour.py b/examples/tour_examples/octocat_tour.py new file mode 100644 index 00000000000..a26f50d505e --- /dev/null +++ b/examples/tour_examples/octocat_tour.py @@ -0,0 +1,23 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class MyTourClass(BaseCase): + def test_octocat_tour(self): + self.maximize_window() + self.goto("https://seleniumbase.io/error_page/") + self.wait_for_element("#parallax_octocat") + self.create_tour(theme="bootstrap") + self.add_tour_step("Welcome to the Octocat Tour!") + self.add_tour_step("This is Octocat", "#parallax_octocat") + self.add_tour_step("This is Octobi-Wan Catnobi", "#octobi_wan_catnobi") + self.add_tour_step("

      Ooops!!!

      ", "#parallax_error_text") + self.add_tour_step("This is a Star Wars speeder.", "#speeder") + self.add_tour_step("This is a sign with a 500-Error", "#parallax_sign") + self.add_tour_step( + "This is not the page you're looking for.", 'img[alt*="404"]' + ) + self.add_tour_step("Have a great day!", title="๐Ÿ˜ƒ โ˜€๏ธ ๐Ÿ˜") + self.add_tour_step("And may the Force be with you!", title="โญ") + self.export_tour(filename="octocat_tour.js") + self.play_tour() diff --git a/examples/tour_examples/shepherd_google_tour.py b/examples/tour_examples/shepherd_google_tour.py old mode 100755 new mode 100644 index fe4bb92713a..38f2c7a1338 --- a/examples/tour_examples/shepherd_google_tour.py +++ b/examples/tour_examples/shepherd_google_tour.py @@ -1,70 +1,99 @@ from seleniumbase import BaseCase +BaseCase.main(__name__, __file__, "--uc") class MyTourClass(BaseCase): - def test_google_tour(self): - self.open('https://google.com') - self.wait_for_element('input[title="Search"]') + if not self.undetectable: + self.get_new_driver(undetectable=True) + self.goto("https://google.com/ncr") + self.click_if_visible('button:contains("Accept all")') + self.wait_for_element('[title="Search"]') + self.hide_elements("iframe") self.create_shepherd_tour(theme="dark") - self.add_tour_step( - "Click to begin the Google Tour!", title="SeleniumBase Tours") - self.add_tour_step( - "Type in your search query here.", 'input[title="Search"]') + self.add_tour_step("Welcome to Google!", title="SeleniumBase Tours") + self.add_tour_step("Type in your query here.", '[title="Search"]') self.play_tour() - self.highlight_update_text('input[title="Search"]', "Google") + self.highlight_type('[title="Search"]', "Google") self.wait_for_element('[role="listbox"]') # Wait for autocomplete self.create_shepherd_tour(theme="light") - self.add_tour_step( - "Then click here to search.", 'input[value="Google Search"]') - self.add_tour_step( - "Or press [ENTER] after typing a query here.", '[title="Search"]') + self.add_tour_step("Then click to search.", '[value="Google Search"]') + self.add_tour_step("Or press [ENTER] after entry.", '[title="Search"]') self.play_tour() - self.highlight_update_text('input[title="Search"]', "GitHub\n") + self.highlight_type('[title="Search"]', "GitHub\n") + self.ad_block() self.wait_for_element("#search") self.create_shepherd_tour(theme="square-dark") - self.add_tour_step( - "Search results appear here!", title="(5-second autoplay on)") - self.add_tour_step( - "The next tour begins in a few seconds...", theme="square", - title="(5-second autoplay on)") - self.play_tour(interval=5) # Tour automatically continues after 5 sec + self.add_tour_step("3-second autoplay...") + self.add_tour_step("Here's the next tour:") + self.play_tour(interval=3) # Tour automatically continues after 3 sec - self.open("https://www.google.com/maps/@42.3598616,-71.0912631,15z") - self.wait_for_element('input#searchboxinput') + self.goto("https://www.google.com/maps/@42.3591234,-71.0915634,15z") + self.wait_for_element('[name="q"]', timeout=20) + self.wait_for_element('[aria-label="Interactive map"]', timeout=20) + self.wait_for_element('[aria-label="Zoom in"]', timeout=20) + self.wait_for_element('[aria-label="Zoom out"]') + self.wait_for_element('[jsaction*="minimap.main;"]') + self.sleep(0.5) self.create_shepherd_tour(theme="dark") + self.add_tour_step("Welcome to Google Maps!") self.add_tour_step( - "Welcome to Google Maps!") - self.add_tour_step( - "Type in a location here.", "#searchboxinput", title="Search Box") + "The location goes here.", '[name="q"]', title="Search Box" + ) self.add_tour_step( "Then click here to show it on the map.", - "#searchbox-searchbutton", alignment="bottom") + '[aria-label="Search"]', + alignment="bottom", + ) self.add_tour_step( "Or click here to get driving directions.", - "#searchbox-directions", alignment="bottom", theme="dark") + 'button[aria-label="Directions"]', + alignment="bottom", + theme="dark", + ) self.add_tour_step( "Use this button to switch to Satellite view.", - "div.widget-minimap", alignment="right") - self.add_tour_step( - "Click here to zoom in.", "#widget-zoom-in", alignment="left") - self.add_tour_step( - "Or click here to zoom out.", "#widget-zoom-out", - alignment="left", theme="light") - self.add_tour_step( - "Use the Menu button to see more options.", - ".searchbox-hamburger-container", alignment="right") - self.add_tour_step( - "Or click here to see more Google apps.", '[title="Google apps"]', - alignment="left") - self.add_tour_step( - "Thanks for trying out SeleniumBase Tours!", - title="End of Guided Tour", theme="light") + 'button[jsaction*="minimap.main;"]', + alignment="right", + ) + self.add_tour_step( + "Click here to zoom in.", + '[aria-label="Zoom in"]', + alignment="left", + ) + self.add_tour_step( + "Or click here to zoom out.", + '[aria-label="Zoom out"]', + alignment="left", + theme="light", + ) + if self.is_element_visible('button[jsaction*="settings.open;"]'): + self.add_tour_step( + "Use the Menu button to see more options.", + 'button[jsaction*="settings.open;"]', + alignment="right", + ) + elif self.is_element_visible('button[jsaction="navigationrail.more"]'): + self.add_tour_step( + "Use the Menu button to see more options.", + 'button[jsaction="navigationrail.more"]', + alignment="right", + ) + self.add_tour_step( + "Or click here to see more Google apps.", + '[aria-label="Google apps"]', + alignment="left", + ) + self.add_tour_step( + "Thanks for using SeleniumBase Tours!", + title="End of Guided Tour", + theme="light", + ) self.export_tour(filename="shepherd_google_maps_tour.js") self.play_tour() diff --git a/examples/tour_examples/xkcd_tour.py b/examples/tour_examples/xkcd_tour.py old mode 100755 new mode 100644 index d9c596cba29..2ff138c99d0 --- a/examples/tour_examples/xkcd_tour.py +++ b/examples/tour_examples/xkcd_tour.py @@ -1,12 +1,12 @@ from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) class MyTestClass(BaseCase): - - def test_basic(self): - self.open('https://xkcd.com/1117/') + def test_create_tour(self): + self.goto("https://xkcd.com/1117/") self.assert_element('img[alt="My Sky"]') - self.create_shepherd_tour() + self.create_tour(theme="dark") self.add_tour_step("Welcome to XKCD!") self.add_tour_step("This is the XKCD logo.", "#masthead img") self.add_tour_step("Here's the daily webcomic.", "#comic img") @@ -17,5 +17,5 @@ def test_basic(self): self.add_tour_step("Click here for the license.", 'a[rel="license"]') self.add_tour_step("Click for a random comic.", 'a[href*="/random/"]') self.add_tour_step("Thanks for taking this tour!") - self.export_tour(filename="xkcd_tour.js") # This exports the tour - self.play_tour() # This plays the tour + self.export_tour(filename="xkcd_tour.js") + self.play_tour() diff --git a/examples/translations/ReadMe.md b/examples/translations/ReadMe.md new file mode 100644 index 00000000000..c47c29c9979 --- /dev/null +++ b/examples/translations/ReadMe.md @@ -0,0 +1,100 @@ + + + + +

      ๐ŸŒ Translated Tests ๐Ÿˆบ

      + +SeleniumBase supports the following 10 languages: English, Chinese, Dutch, French, Italian, Japanese, Korean, Portuguese, Russian, and Spanish. (Examples can be found in SeleniumBase/examples/translations) + +Multi-language tests run with **pytest** like other tests. Test methods have a one-to-one mapping to supported languages. Here's an example of a translated test: + +```python +# Chinese Translation +from seleniumbase.translate.chinese import ็ก’ๆต‹่ฏ•็”จไพ‹ + +class ๆˆ‘็š„ๆต‹่ฏ•็ฑป(็ก’ๆต‹่ฏ•็”จไพ‹): + def test_ไพ‹ๅญ1(self): + self.ๅผ€ๅฏ("https://zh.wikipedia.org/wiki/") + self.ๆ–ญ่จ€ๆ ‡้ข˜("็ปดๅŸบ็™พ็ง‘๏ผŒ่‡ช็”ฑ็š„็™พ็ง‘ๅ…จไนฆ") + self.ๆ–ญ่จ€ๅ…ƒ็ด ('a[title="Wikipedia:ๅ…ณไบŽ"]') + self.ๅฆ‚ๆžœๅฏ่ง่ฏทๅ•ๅ‡ป('button[aria-label="ๅ…ณ้—ญ"]') + self.ๅฆ‚ๆžœๅฏ่ง่ฏทๅ•ๅ‡ป('button[aria-label="้—œ้–‰"]') + self.ๆ–ญ่จ€ๅ…ƒ็ด ('span:contains("ๅˆ›ๅปบ่ดฆๅท")') + self.ๆ–ญ่จ€ๅ…ƒ็ด ('span:contains("็™ปๅฝ•")') + self.่พ“ๅ…ฅๆ–‡ๆœฌ('input[name="search"]', "่ˆž้พ") + self.ๅ•ๅ‡ป('button:contains("ๆœ็ดข")') + self.ๆ–ญ่จ€ๆ–‡ๆœฌ("่ˆž้พ", "#firstHeading") + self.ๆ–ญ่จ€ๅ…ƒ็ด ('img[src*="Chinese_draak.jpg"]') +``` + +Here's another example: + +```python +# Japanese Translation +from seleniumbase.translate.japanese import ใ‚ปใƒฌใƒ‹ใ‚ฆใƒ ใƒ†ใ‚นใƒˆใ‚ฑใƒผใ‚น + +class ็งใฎใƒ†ใ‚นใƒˆใ‚ฏใƒฉใ‚น(ใ‚ปใƒฌใƒ‹ใ‚ฆใƒ ใƒ†ใ‚นใƒˆใ‚ฑใƒผใ‚น): + def test_ไพ‹1(self): + self.ใ‚’้–‹ใ("https://ja.wikipedia.org/wiki/") + self.ใƒ†ใ‚ญใ‚นใƒˆใ‚’็ขบ่ชใ™ใ‚‹("ใ‚ฆใ‚ฃใ‚ญใƒšใƒ‡ใ‚ฃใ‚ข") + self.่ฆ็ด ใ‚’็ขบ่ชใ™ใ‚‹('[title*="ใ‚ฆใ‚ฃใ‚ญใƒšใƒ‡ใ‚ฃใ‚ขใธใ‚ˆใ†ใ“ใ"]') + self.JSๅ…ฅๅŠ›('input[name="search"]', "ใ‚ขใƒ‹ใƒก") + self.ใ‚ฏใƒชใƒƒใ‚ฏใ—ใฆ("#searchform button") + self.ใƒ†ใ‚ญใ‚นใƒˆใ‚’็ขบ่ชใ™ใ‚‹("ใ‚ขใƒ‹ใƒก", "#firstHeading") + self.JSๅ…ฅๅŠ›('input[name="search"]', "ๅฏฟๅธ") + self.ใ‚ฏใƒชใƒƒใ‚ฏใ—ใฆ("#searchform button") + self.ใƒ†ใ‚ญใ‚นใƒˆใ‚’็ขบ่ชใ™ใ‚‹("ๅฏฟๅธ", "#firstHeading") + self.่ฆ็ด ใ‚’็ขบ่ชใ™ใ‚‹('img[src*="Various_sushi"]') +``` + + +

      Translation API ๐Ÿˆบ

      + +You can use SeleniumBase to selectively translate the method names of any test from one language to another with the console scripts interface. Additionally, the `import` line at the top of the Python file will change to import the new `BaseCase`. Example: `BaseCase` becomes `CasoDeTeste` when a test is translated into Portuguese. + +```zsh +seleniumbase translate +``` + +```zsh +* Usage: +seleniumbase translate [SB_FILE.py] [LANGUAGE] [ACTION] + +* Languages: +`--en` / `--English` | `--zh` / `--Chinese` +`--nl` / `--Dutch` | `--fr` / `--French` +`--it` / `--Italian` | `--ja` / `--Japanese` +`--ko` / `--Korean` | `--pt` / `--Portuguese` +`--ru` / `--Russian` | `--es` / `--Spanish` + +* Actions: +`-p` / `--print` (Print translation output to the screen) +`-o` / `--overwrite` (Overwrite the file being translated) +`-c` / `--copy` (Copy the translation to a new `.py` file) + +* Options: +`-n` (include line Numbers when using the Print action) + +* Examples: +Translate test_1.py into Chinese and only print the output: +>>> seleniumbase translate test_1.py --zh -p +Translate test_2.py into Portuguese and overwrite the file: +>>> seleniumbase translate test_2.py --pt -o +Translate test_3.py into Dutch and make a copy of the file: +>>> seleniumbase translate test_3.py --nl -c + +* Output: +Translates a SeleniumBase Python file into the language +specified. Method calls and `import` lines get swapped. +Both a language and an action must be specified. +The `-p` action can be paired with one other action. +When running with `-c` (or `--copy`) the new file name +will be the original name appended with an underscore +plus the 2-letter language code of the new language. +(Example: Translating `test_1.py` into Japanese with +`-c` will create a new file called `test_1_ja.py`.) +``` + +-------- + +

      diff --git a/examples/translations/chinese_test_1.py b/examples/translations/chinese_test_1.py new file mode 100644 index 00000000000..d801b47b221 --- /dev/null +++ b/examples/translations/chinese_test_1.py @@ -0,0 +1,28 @@ +# Chinese Language Test +from seleniumbase.translate.chinese import ็ก’ๆต‹่ฏ•็”จไพ‹ +็ก’ๆต‹่ฏ•็”จไพ‹.main(__name__, __file__) + + +class ๆˆ‘็š„ๆต‹่ฏ•็ฑป(็ก’ๆต‹่ฏ•็”จไพ‹): + def test_ไพ‹ๅญ1(self): + self.่ฎฟ้—ฎ("https://zh.wikipedia.org/wiki/") + self.ๆ–ญ่จ€ๆ ‡้ข˜("็ปดๅŸบ็™พ็ง‘๏ผŒ่‡ช็”ฑ็š„็™พ็ง‘ๅ…จไนฆ") + self.ๆ–ญ่จ€ๅ…ƒ็ด ('a[title="Wikipedia:ๅ…ณไบŽ"]') + self.ๅฆ‚ๆžœๅฏ่ง่ฏทๅ•ๅ‡ป('button[aria-label="ๅ…ณ้—ญ"]') + self.ๅฆ‚ๆžœๅฏ่ง่ฏทๅ•ๅ‡ป('button[aria-label="้—œ้–‰"]') + self.ๆ–ญ่จ€ๅ…ƒ็ด ('span:contains("ๅˆ›ๅปบ่ดฆๅท")') + self.ๆ–ญ่จ€ๅ…ƒ็ด ('span:contains("็™ปๅฝ•")') + self.่พ“ๅ…ฅๆ–‡ๆœฌ('input[name="search"]', "่ˆž้พ") + self.ๅ•ๅ‡ป('button:contains("ๆœ็ดข")') + self.ๆ–ญ่จ€ๆ–‡ๆœฌ("่ˆž้พ", "#firstHeading") + self.ๆ–ญ่จ€ๅ…ƒ็ด ('img[src*="Chinese_draak.jpg"]') + self.ๅ›žๅŽป() + self.่พ“ๅ…ฅๆ–‡ๆœฌ('input[name="search"]', "้บปๅฉ†่ฑ†่…") + self.ๅ•ๅ‡ป('button:contains("ๆœ็ดข")') + self.ๆ–ญ่จ€ๆ–‡ๆœฌ("้บปๅฉ†่ฑ†่…", "#firstHeading") + self.ๆ–ญ่จ€ๅ…ƒ็ด ('figure:contains("ไธ€ๅฎถไธญ้ค้คจ็š„้บปๅฉ†่ฑ†่…")') + self.ๅ›žๅŽป() + self.่พ“ๅ…ฅๆ–‡ๆœฌ('input[name="search"]', "็ฒพๆญฆ่‹ฑ้›„") + self.ๅ•ๅ‡ป('button:contains("ๆœ็ดข")') + self.ๆ–ญ่จ€ๅ…ƒ็ด ('img[src*="Fist_of_legend.jpg"]') + self.ๆ–ญ่จ€ๆ–‡ๆœฌ("ๆŽ่ฟžๆฐ", 'li a[title="ๆŽ่ฟžๆฐ"]') diff --git a/examples/translations/dutch_test_1.py b/examples/translations/dutch_test_1.py new file mode 100644 index 00000000000..c362de27450 --- /dev/null +++ b/examples/translations/dutch_test_1.py @@ -0,0 +1,22 @@ +# Dutch Language Test +from seleniumbase.translate.dutch import Testgeval +Testgeval.main(__name__, __file__) + + +class MijnTestklasse(Testgeval): + def test_voorbeeld_1(self): + self.bezoek("https://nl.wikipedia.org/wiki/Hoofdpagina") + self.controleren_element('a[title*="Welkom voor nieuwkomers"]') + self.controleren_tekst("Welkom op Wikipedia", "td.hp-welkom") + self.typ("#searchform input", "Stroopwafel") + self.klik("#searchform button") + self.controleren_tekst("Stroopwafel", "#firstHeading") + self.controleren_element('img[src*="Stroopwafels"]') + self.typ("#searchform input", "Rijksmuseum Amsterdam") + self.klik("#searchform button") + self.controleren_tekst("Rijksmuseum", "#firstHeading") + self.controleren_element('img[src*="Rijksmuseum"]') + self.terug() + self.controleren_url_bevat("Stroopwafel") + self.vooruit() + self.controleren_url_bevat("Rijksmuseum") diff --git a/examples/translations/english_test_1.py b/examples/translations/english_test_1.py new file mode 100644 index 00000000000..44c9b1505db --- /dev/null +++ b/examples/translations/english_test_1.py @@ -0,0 +1,18 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class MyTestClass(BaseCase): + def test_example_1(self): + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("#inventory_container") + self.assert_exact_text("Products", "span.title") + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_exact_text("Your Cart", "span.title") + self.assert_text("Backpack", "div.cart_item") + self.click('button:contains("Remove")') # HTML innerText + self.assert_text_not_visible("Backpack", "div.cart_item") + self.js_click("a#logout_sidebar_link") diff --git a/examples/translations/french_test_1.py b/examples/translations/french_test_1.py new file mode 100644 index 00000000000..30baddb454d --- /dev/null +++ b/examples/translations/french_test_1.py @@ -0,0 +1,23 @@ +# French Language Test +from seleniumbase.translate.french import CasDeBase +CasDeBase.main(__name__, __file__) + + +class MaClasseDeTest(CasDeBase): + def test_exemple_1(self): + self.visiter("https://fr.wikipedia.org/wiki/") + self.vรฉrifier_texte("Wikipรฉdia") + self.vรฉrifier_รฉlรฉment('[alt="Wikipรฉdia"]') + self.cliquer_si_affichรฉ('button[aria-label="Fermer"]') + self.js_taper("#searchform input", "Crรจme brรปlรฉe") + self.cliquer("#searchform button") + self.vรฉrifier_texte("Crรจme brรปlรฉe", "#firstHeading") + self.vรฉrifier_รฉlรฉment('img[alt*="Crรจme brรปlรฉe"]') + self.js_taper("#searchform input", "Jardin des Tuileries") + self.cliquer("#searchform button") + self.vรฉrifier_texte("Jardin des Tuileries", "#firstHeading") + self.vรฉrifier_รฉlรฉment('img[alt*="Jardin des Tuileries"]') + self.retour() + self.vรฉrifier_url_contient("brรปlรฉe") + self.en_avant() + self.vรฉrifier_url_contient("Jardin") diff --git a/examples/translations/italian_test_1.py b/examples/translations/italian_test_1.py new file mode 100644 index 00000000000..6f2e9a51ac0 --- /dev/null +++ b/examples/translations/italian_test_1.py @@ -0,0 +1,22 @@ +# Italian Language Test +from seleniumbase.translate.italian import CasoDiProva +CasoDiProva.main(__name__, __file__) + + +class MiaClasseDiTest(CasoDiProva): + def test_esempio_1(self): + self.visita("https://it.wikipedia.org/wiki/") + self.verificare_testo("Wikipedia") + self.verificare_elemento('a[title="Lingua italiana"]') + self.digitare('input[name="search"]', "Pizza") + self.fare_clic("#searchform button") + self.verificare_testo("Pizza", "#firstHeading") + self.verificare_elemento('figure img[src*="pizza"]') + self.digitare('input[name="search"]', "Colosseo") + self.fare_clic("#searchform button") + self.verificare_testo("Colosseo", "#firstHeading") + self.verificare_elemento('figure img[src*="Colosseo"]') + self.indietro() + self.verificare_url_contiene("Pizza") + self.avanti() + self.verificare_url_contiene("Colosseo") diff --git a/examples/translations/japanese_test_1.py b/examples/translations/japanese_test_1.py new file mode 100644 index 00000000000..67b5e73684c --- /dev/null +++ b/examples/translations/japanese_test_1.py @@ -0,0 +1,23 @@ +# Japanese Language Test +from seleniumbase.translate.japanese import ใ‚ปใƒฌใƒ‹ใ‚ฆใƒ ใƒ†ใ‚นใƒˆใ‚ฑใƒผใ‚น +ใ‚ปใƒฌใƒ‹ใ‚ฆใƒ ใƒ†ใ‚นใƒˆใ‚ฑใƒผใ‚น.main(__name__, __file__) + + +class ็งใฎใƒ†ใ‚นใƒˆใ‚ฏใƒฉใ‚น(ใ‚ปใƒฌใƒ‹ใ‚ฆใƒ ใƒ†ใ‚นใƒˆใ‚ฑใƒผใ‚น): + def test_ไพ‹1(self): + self.ใ‚’่จชๅ•("https://ja.wikipedia.org/wiki/") + self.ใƒ†ใ‚ญใ‚นใƒˆใ‚’็ขบ่ชใ™ใ‚‹("ใ‚ฆใ‚ฃใ‚ญใƒšใƒ‡ใ‚ฃใ‚ข") + self.่ฆ็ด ใ‚’็ขบ่ชใ™ใ‚‹('[title*="ใ‚ฆใ‚ฃใ‚ญใƒšใƒ‡ใ‚ฃใ‚ขใธใ‚ˆใ†ใ“ใ"]') + self.JSๅ…ฅๅŠ›('input[name="search"]', "ใ‚ขใƒ‹ใƒก") + self.ใ‚ฏใƒชใƒƒใ‚ฏใ—ใฆ("#searchform button") + self.ใƒ†ใ‚ญใ‚นใƒˆใ‚’็ขบ่ชใ™ใ‚‹("ใ‚ขใƒ‹ใƒก", "#firstHeading") + self.JSๅ…ฅๅŠ›('input[name="search"]', "ๅฏฟๅธ") + self.ใ‚ฏใƒชใƒƒใ‚ฏใ—ใฆ("#searchform button") + self.ใƒ†ใ‚ญใ‚นใƒˆใ‚’็ขบ่ชใ™ใ‚‹("ๅฏฟๅธ", "#firstHeading") + self.่ฆ็ด ใ‚’็ขบ่ชใ™ใ‚‹('img[src*="Various_sushi"]') + self.JSๅ…ฅๅŠ›("#searchInput", "ใƒฌใ‚ดใƒฉใƒณใƒ‰ใƒปใ‚ธใƒฃใƒ‘ใƒณ") + self.ใ‚ฏใƒชใƒƒใ‚ฏใ—ใฆ("#searchform button") + self.่ฆ็ด ใ‚’็ขบ่ชใ™ใ‚‹('img[src*="LEGOLAND_JAPAN"]') + self.ใƒชใƒณใ‚ฏใƒ†ใ‚ญใ‚นใƒˆใ‚’็ขบ่ชใ™ใ‚‹("ๅๅคๅฑ‹ๅŸŽ") + self.ใƒชใƒณใ‚ฏใƒ†ใ‚ญใ‚นใƒˆใ‚’ใ‚ฏใƒชใƒƒใ‚ฏใ—ใพใ™("ใƒ†ใƒผใƒžใƒ‘ใƒผใ‚ฏ") + self.ใƒ†ใ‚ญใ‚นใƒˆใ‚’็ขบ่ชใ™ใ‚‹("ใƒ†ใƒผใƒžใƒ‘ใƒผใ‚ฏ", "#firstHeading") diff --git a/examples/translations/korean_test_1.py b/examples/translations/korean_test_1.py new file mode 100644 index 00000000000..b3b1b6452aa --- /dev/null +++ b/examples/translations/korean_test_1.py @@ -0,0 +1,21 @@ +# Korean Language Test +from seleniumbase.translate.korean import ์…€๋ ˆ๋Š„_ํ…Œ์ŠคํŠธ_์ผ€์ด์Šค +์…€๋ ˆ๋Š„_ํ…Œ์ŠคํŠธ_์ผ€์ด์Šค.main(__name__, __file__) + + +class ํ…Œ์ŠคํŠธ_ํด๋ž˜์Šค(์…€๋ ˆ๋Š„_ํ…Œ์ŠคํŠธ_์ผ€์ด์Šค): + def test_์‹ค์‹œ์˜ˆ_1(self): + self.๋ฐฉ๋ฌธ("https://ko.wikipedia.org/wiki/") + self.ํ…์ŠคํŠธ_ํ™•์ธ("์œ„ํ‚ค๋ฐฑ๊ณผ") + self.์š”์†Œ_ํ™•์ธ('[title="์œ„ํ‚ค๋ฐฑ๊ณผ:์†Œ๊ฐœ"]') + self.JS_์ž…๋ ฅ("#searchform input", "๊น€์น˜") + self.ํด๋ฆญ("#searchform button") + self.ํ…์ŠคํŠธ_ํ™•์ธ("๊น€์น˜", "#firstHeading") + self.์š”์†Œ_ํ™•์ธ('img[src*="Various_kimchi.jpg"]') + self.๋งํฌ_ํ…์ŠคํŠธ_ํ™•์ธ("ํ•œ๊ตญ ์š”๋ฆฌ") + self.JS_์ž…๋ ฅ("#searchform input", "๋น„๋น”๋ฐฅ") + self.ํด๋ฆญ("#searchform button") + self.ํ…์ŠคํŠธ_ํ™•์ธ("๋น„๋น”๋ฐฅ", "#firstHeading") + self.์š”์†Œ_ํ™•์ธ('img[src*="Dolsot-bibimbap.jpg"]') + self.๋งํฌ_ํ…์ŠคํŠธ๋ฅผ_ํด๋ฆญํ•ฉ๋‹ˆ๋‹ค("๋Œ์†ฅ๋น„๋น”๋ฐฅ") + self.ํ…์ŠคํŠธ_ํ™•์ธ("๋Œ์†ฅ๋น„๋น”๋ฐฅ", "#firstHeading") diff --git a/examples/translations/portuguese_test_1.py b/examples/translations/portuguese_test_1.py new file mode 100644 index 00000000000..c4e54ee872c --- /dev/null +++ b/examples/translations/portuguese_test_1.py @@ -0,0 +1,25 @@ +# Portuguese Language Test +from seleniumbase.translate.portuguese import CasoDeTeste +CasoDeTeste.main(__name__, __file__) + + +class MinhaClasseDeTeste(CasoDeTeste): + def test_exemplo_1(self): + self.visitar("https://pt.wikipedia.org/wiki/") + self.verificar_texto("Wikipรฉdia") + self.verificar_elemento('[title="Lรญngua portuguesa"]') + self.digitar("#searchform input", "Joรฃo Pessoa") + self.clique("#searchform button") + self.verificar_texto("Joรฃo Pessoa", "#firstHeading") + self.verificar_elemento('img[alt*="Joรฃo Pessoa"]') + self.digitar("#searchform input", "Florianรณpolis") + self.clique("#searchform button") + self.verificar_texto("Florianรณpolis", "h1#firstHeading") + self.verificar_elemento('td:contains("Avenida Beira-Mar")') + self.voltar() + self.verificar_url_contรฉm("Joรฃo_Pessoa") + self.atualizar_a_pรกgina() + self.js_digitar("#searchform input", "Teatro Amazonas") + self.clique("#searchform button") + self.verificar_texto("Teatro Amazonas", "#firstHeading") + self.verificar_texto_do_link("Festival Amazonas de ร“pera") diff --git a/examples/translations/pytest.ini b/examples/translations/pytest.ini new file mode 100644 index 00000000000..0b8fa185bab --- /dev/null +++ b/examples/translations/pytest.ini @@ -0,0 +1,47 @@ +[pytest] + +# Display console output. Disable cacheprovider: +addopts = --capture=tee-sys -p no:cacheprovider + +# Skip these directories during test collection: +norecursedirs = .* build dist recordings temp assets + +# Ignore DeprecationWarning, PytestUnknownMarkWarning +filterwarnings = + ignore::pytest.PytestWarning + ignore:.*U.*mode is deprecated:DeprecationWarning + +# Configure the junit_family option explicitly: +junit_family = legacy + +# Set pytest discovery rules: +# (Most of the rules here are similar to the default rules.) +# (Inheriting unittest.TestCase could override these rules.) +python_files = test_*.py *_test.py *_tests.py *_suite.py *_test_*.py +python_classes = Test* *Test* *Test *Tests *Suite +python_functions = test_* + +# Common pytest markers used in examples: +# (pytest may require marker registration to prevent warnings.) +# (Future versions may turn those marker warnings into errors.) +markers = + marker1: custom marker + marker2: custom marker + marker3: custom marker + marker_test_suite: custom marker + expected_failure: custom marker + local: custom marker + remote: custom marker + offline: custom marker + develop: custom marker + qa: custom marker + ci: custom marker + e2e: custom marker + ready: custom marker + smoke: custom marker + deploy: custom marker + active: custom marker + master: custom marker + release: custom marker + staging: custom marker + production: custom marker diff --git a/examples/translations/russian_test_1.py b/examples/translations/russian_test_1.py new file mode 100644 index 00000000000..5d5343d0901 --- /dev/null +++ b/examples/translations/russian_test_1.py @@ -0,0 +1,22 @@ +# Russian Language Test +from seleniumbase.translate.russian import ะขะตัั‚ะะฐะกะตะปะตะฝ +ะขะตัั‚ะะฐะกะตะปะตะฝ.main(__name__, __file__) + + +class ะœะพะนะขะตัั‚ะพะฒั‹ะนะšะปะฐัั(ะขะตัั‚ะะฐะกะตะปะตะฝ): + def test_ะฟั€ะธะผะตั€_1(self): + self.ะฟะพัะตั‚ะธั‚ัŒ("https://ru.wikipedia.org/wiki/") + self.ะฟะพะดั‚ะฒะตั€ะดะธั‚ัŒ_ัะปะตะผะตะฝั‚('[title="ะ ัƒััะบะธะน ัะทั‹ะบ"]') + self.ะฟะพะดั‚ะฒะตั€ะดะธั‚ัŒ_ั‚ะตะบัั‚("ะ’ะธะบะธะฟะตะดะธั", "div.main-wikimedia-header") + self.ะฒะฒะตะดะธั‚ะต("#searchInput", "ะœะ“ะฃ") + self.ะฝะฐะถะผะธั‚ะต("#searchform button") + self.ะฟะพะดั‚ะฒะตั€ะดะธั‚ัŒ_ั‚ะตะบัั‚("ัƒะฝะธะฒะตั€ัะธั‚ะตั‚", "#firstHeading") + self.ะฟะพะดั‚ะฒะตั€ะดะธั‚ัŒ_ัะปะตะผะตะฝั‚('img[alt*="ะ“ะปะฐะฒะฝะพะต ะทะดะฐะฝะธะต ะœะ“ะฃ"]') + self.ะฒะฒะตะดะธั‚ะต("#searchInput", "ะฟั€ะธะบะปัŽั‡ะตะฝะธั ะจัƒั€ะธะบะฐ") + self.ะฝะฐะถะผะธั‚ะต("#searchform button") + self.ะฟะพะดั‚ะฒะตั€ะดะธั‚ัŒ_ั‚ะตะบัั‚("ะžะฟะตั€ะฐั†ะธั ยซะซยป ะธ ะดั€ัƒะณะธะต ะฟั€ะธะบะปัŽั‡ะตะฝะธั ะจัƒั€ะธะบะฐ") + self.ะฟะพะดั‚ะฒะตั€ะดะธั‚ัŒ_ัะปะตะผะตะฝั‚('img[alt="ะŸะพัั‚ะตั€ ั„ะธะปัŒะผะฐ"]') + self.ะฝะฐะทะฐะด() + self.ะฟะพะดั‚ะฒะตั€ะดะธั‚ัŒ_URL_ัะพะดะตั€ะถะธั‚("ัƒะฝะธะฒะตั€ัะธั‚ะตั‚") + self.ะฒะฟะตั€ะตะด() + self.ะฟะพะดั‚ะฒะตั€ะดะธั‚ัŒ_URL_ัะพะดะตั€ะถะธั‚("ะจัƒั€ะธะบะฐ") diff --git a/examples/translations/spanish_test_1.py b/examples/translations/spanish_test_1.py new file mode 100644 index 00000000000..aa371a9c108 --- /dev/null +++ b/examples/translations/spanish_test_1.py @@ -0,0 +1,22 @@ +# Spanish Language Test +from seleniumbase.translate.spanish import CasoDePrueba +CasoDePrueba.main(__name__, __file__) + + +class MiClaseDePrueba(CasoDePrueba): + def test_ejemplo_1(self): + self.visita("https://es.wikipedia.org/wiki/") + self.verificar_texto("Wikipedia") + self.verificar_elemento('[title="Wikipedia:Bienvenidos"]') + self.escriba('[name="search"]', "Parque de Atracciones Tibidabo") + self.haga_clic('button:contains("Buscar")') + self.verificar_texto("Tibidabo", "#firstHeading") + self.verificar_elemento('img[src*="Tibidabo"]') + self.escriba('input[name="search"]', "Palma de Mallorca") + self.haga_clic('button:contains("Buscar")') + self.verificar_texto("Palma de Mallorca", "#firstHeading") + self.verificar_elemento('img[src*="Palma"]') + self.volver() + self.verificar_url_contiene("Tibidabo") + self.adelante() + self.verificar_url_contiene("Mallorca") diff --git a/examples/uc_cdp_events.py b/examples/uc_cdp_events.py new file mode 100644 index 00000000000..947e1d21214 --- /dev/null +++ b/examples/uc_cdp_events.py @@ -0,0 +1,31 @@ +"""NOTE: This uses plain UC Mode, which was replaced by UC + CDP Mode. +PyAutoGUI is installed automatically for uc_gui methods if not already.""" +from rich.pretty import pprint +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__, "--uc", "--uc-cdp") + + +class CDPTests(BaseCase): + def add_cdp_listener(self): + # (To print everything, use "*". Otherwise select specific headers.) + # self.driver.add_cdp_listener("*", lambda data: print(pformat(data))) + self.driver.add_cdp_listener( + "Network.requestWillBeSentExtraInfo", + lambda data: pprint(data) + ) + + def click_turnstile_and_verify(sb): + sb.uc_gui_handle_captcha() + sb.assert_element("img#captcha-success", timeout=3) + sb.highlight("img#captcha-success", loops=8) + + def test_display_cdp_events(self): + if not (self.undetectable and self.uc_cdp_events): + self.get_new_driver(undetectable=True, uc_cdp_events=True) + url = "seleniumbase.io/apps/turnstile" + self.uc_open_with_reconnect(url, 2) + self.add_cdp_listener() + self.click_turnstile_and_verify() + self.sleep(1) + self.refresh() + self.sleep(1.2) diff --git a/examples/unit_tests/ReadMe.md b/examples/unit_tests/ReadMe.md new file mode 100644 index 00000000000..48152526a0d --- /dev/null +++ b/examples/unit_tests/ReadMe.md @@ -0,0 +1,5 @@ + + +

      pytest-specific unit tests

      + +The tests in this folder are for basic verification of the SeleniumBase framework with pytest. diff --git a/examples/unit_tests/pytest.ini b/examples/unit_tests/pytest.ini new file mode 100644 index 00000000000..5fc7a520f3d --- /dev/null +++ b/examples/unit_tests/pytest.ini @@ -0,0 +1,48 @@ +[pytest] + +# Display console output. Disable cacheprovider: +addopts = --capture=tee-sys -p no:cacheprovider + +# Skip these directories during test collection: +norecursedirs = .* build dist recordings temp assets + +# Ignore DeprecationWarning, PytestUnknownMarkWarning +filterwarnings = + ignore::pytest.PytestWarning + ignore:.*U.*mode is deprecated:DeprecationWarning + ignore::pytest.PytestAssertRewriteWarning + +# Configure the junit_family option explicitly: +junit_family = legacy + +# Set pytest discovery rules: +# (Most of the rules here are similar to the default rules.) +# (Inheriting unittest.TestCase could override these rules.) +python_files = test_*.py *_test.py *_tests.py *_suite.py +python_classes = Test* *Test* *Test *Tests *Suite +python_functions = test_* + +# Common pytest markers used in examples: +# (pytest may require marker registration to prevent warnings.) +# (Future versions may turn those marker warnings into errors.) +markers = + marker1: custom marker + marker2: custom marker + marker3: custom marker + marker_test_suite: custom marker + expected_failure: custom marker + local: custom marker + remote: custom marker + offline: custom marker + develop: custom marker + qa: custom marker + ci: custom marker + e2e: custom marker + ready: custom marker + smoke: custom marker + deploy: custom marker + active: custom marker + master: custom marker + release: custom marker + staging: custom marker + production: custom marker diff --git a/examples/unit_tests/verify_framework.py b/examples/unit_tests/verify_framework.py new file mode 100644 index 00000000000..d9c78252781 --- /dev/null +++ b/examples/unit_tests/verify_framework.py @@ -0,0 +1,194 @@ +""" SeleniumBase Verification """ +import sys + +headless = "--headless" +if "win32" in sys.platform: + headless = "--chs" + +if __name__ == "__main__": + from pytest import main + main([__file__, "-v", "-s"]) + + +def test_simple_cases(pytester): + """Verify a simple passing test and a simple failing test. + The failing test is marked as xfail to have it skipped.""" + pytester.makepyfile( + """ + import pytest + from seleniumbase import BaseCase + class MyTestCase(BaseCase): + def test_passing(self): + self.assert_equal('yes', 'yes') + @pytest.mark.xfail + def test_failing(self): + self.assert_equal('yes', 'no') + """ + ) + result = pytester.inline_run(headless, "--rs", "-v") + assert result.matchreport("test_passing").passed + assert result.matchreport("test_failing").skipped + + +def test_basecase(pytester): + pytester.makepyfile( + """ + from seleniumbase import BaseCase + class MyTest(BaseCase): + def test_basecase(self): + self.goto("data:text/html,

      Hello

      ") + self.assert_element("html > body") # selector + self.assert_text("Hello", "body p") # text, selector + self.type("input", "Goodbye") # selector, text + self.click("body p") # selector + """ + ) + result = pytester.inline_run(headless, "-v") + assert result.matchreport("test_basecase").passed + + +def test_run_with_dashboard(pytester): + pytester.makepyfile( + """ + from seleniumbase import BaseCase + class MyTestCase(BaseCase): + def test_1_passing(self): + self.assert_equal('yes', 'yes') + def test_2_failing(self): + self.assert_equal('yes', 'no') + def test_3_skipped(self): + self.skip("Skip!") + """ + ) + result = pytester.inline_run(headless, "--rs", "--dashboard", "-v") + assert result.matchreport("test_1_passing").passed + assert result.matchreport("test_2_failing").failed + assert result.matchreport("test_3_skipped").skipped + + +def test_sb_fixture(pytester): + pytester.makepyfile( + """ + def test_sb_fixture(sb): + sb.goto("data:text/html,

      Hello

      ") + sb.assert_element("html > body") # selector + sb.assert_text("Hello", "body p") # text, selector + sb.type("input", "Goodbye") # selector, text + sb.click("body p") # selector + """ + ) + result = pytester.inline_run(headless, "-v") + assert result.matchreport("test_sb_fixture").passed + + +def test_request_sb_fixture(pytester): + pytester.makepyfile( + """ + def test_request_sb_fixture(request): + sb = request.getfixturevalue('sb') + sb.goto("data:text/html,

      Hello

      ") + sb.assert_element("html > body") # selector + sb.assert_text("Hello", "body p") # text, selector + sb.type("input", "Goodbye") # selector, text + sb.click("body p") # selector + sb.tearDown() + """ + ) + result = pytester.inline_run(headless, "-v") + assert result.matchreport("test_request_sb_fixture").passed + + +def check_outcome_field(outcomes, field_name, expected_value): + field_value = outcomes.get(field_name, 0) + assert field_value == expected_value, ( + "outcomes.%s has an unexpected value! " + 'Expected "%s" but got "%s"!' + % (field_name, expected_value, field_value) + ) + + +def assert_outcomes( + result, + passed=1, + skipped=0, + failed=0, + xfailed=0, + xpassed=0, + rerun=0, +): + outcomes = result.parseoutcomes() + check_outcome_field(outcomes, "passed", passed) + check_outcome_field(outcomes, "skipped", skipped) + check_outcome_field(outcomes, "failed", failed) + check_outcome_field(outcomes, "xfailed", xfailed) + check_outcome_field(outcomes, "xpassed", xpassed) + check_outcome_field(outcomes, "rerun", rerun) + + +def test_rerun_failures(pytester): + pytester.makepyfile( + """ + from seleniumbase import BaseCase + class MyTestCase(BaseCase): + def test_passing(self): + self.assert_equal('yes', 'yes') + def test_failing(self): + self.assert_equal('yes', 'no') + """ + ) + result = pytester.runpytest(headless, "--reruns=1", "--rs", "-v") + assert_outcomes(result, passed=1, failed=1, rerun=1) + + +def test_browser_launcher(pytester): + pytester.makepyfile( + """ + import sys + from seleniumbase import get_driver + b = None + if "win32" in sys.platform: + b = "chs" + def test_browser_launcher(): + success = False + try: + driver = get_driver("chrome", headless=True, binary_location=b) + driver.get("data:text/html,

      Data URL

      ") + source = driver.page_source + assert "Data URL" in source + success = True # No errors + finally: + driver.quit() + assert success + """ + ) + result = pytester.inline_run(headless, "-v") + assert result.matchreport("test_browser_launcher").passed + + +def test_framework_components(pytester): + pytester.makepyfile( + """ + import sys + from seleniumbase import get_driver + from seleniumbase import js_utils + from seleniumbase import page_actions + b = None + if "win32" in sys.platform: + b = "chs" + def test_framework_components(): + success = False + try: + driver = get_driver("chrome", headless=True, binary_location=b) + driver.get('data:text/html,

      Data URL

      ') + source = driver.page_source + assert "Data URL" in source + assert page_actions.is_element_visible(driver, "h1.top") + js_utils.highlight_with_js(driver, "h1.top", 2, "") + success = True # No errors + finally: + driver.quit() + assert success + """ + ) + result = pytester.inline_run(headless, "-v", "-s") + assert result.matchreport("test_framework_components").passed diff --git a/examples/upgrade_chromedriver.py b/examples/upgrade_chromedriver.py new file mode 100644 index 00000000000..03ef4190fc9 --- /dev/null +++ b/examples/upgrade_chromedriver.py @@ -0,0 +1,99 @@ +"""This script installs the chromedriver version that matches your Chrome. +On newer versions of Python, you may replace "testdir" with "pytester". +(Run with "pytest")""" +import subprocess + + +class TestUpgradeChromedriver: + def basic_run(self, testdir): + testdir.makepyfile( + """ + from seleniumbase import BaseCase + class MyTestCase(BaseCase): + def test_passing(self): + pass + """ + ) + return testdir + + def upgrade_chromedriver(self, testdir): + testdir.makepyfile( + """ + import subprocess + from seleniumbase import BaseCase + class MyTestCase(BaseCase): + def test_upgrade(self): + chrome_version = self.get_chrome_version() + major_chrome_ver = chrome_version.split(".")[0] + chromedriver_ver = self.get_chromedriver_version() + major_chromedriver_ver = chromedriver_ver.split(".")[0] + if major_chromedriver_ver != major_chrome_ver: + subprocess.check_call( + "sbase get chromedriver %s" % major_chrome_ver, + shell=True + ) + """ + ) + return testdir + + def print_versions_of_chromedriver_and_chrome(self, testdir): + testdir.makepyfile( + """ + from seleniumbase import BaseCase + class MyTestCase(BaseCase): + def test_print_versions(self): + chrome_version = self.get_chrome_version() + major_chrome_ver = chrome_version.split(".")[0] + chromedriver_ver = self.get_chromedriver_version() + major_chromedriver_ver = chromedriver_ver.split(".")[0] + print( + "\\n* Now using chromedriver %s with Chrome %s" + % (chromedriver_ver, chrome_version) + ) + if major_chromedriver_ver == major_chrome_ver: + print( + "* SUCCESS: " + "The chromedriver version is compatible " + "with Chrome!" + ) + elif major_chromedriver_ver < major_chrome_ver: + print("* !!! Version Mismatch !!!") + print( + "* The version of chromedriver is too low!\\n" + "* Try upgrading to chromedriver %s manually:\\n" + "* >>> sbase get chromedriver %s <<<" + % (major_chrome_ver, major_chrome_ver) + ) + else: + print("* !!! Version Mismatch !!!") + print( + "* The version of chromedriver is too high!\\n" + "* Try downgrading to chromedriver %s manually:\\n" + "* >>> sbase get chromedriver %s <<<" + % (major_chrome_ver, major_chrome_ver) + ) + """ + ) + return testdir + + def test_upgrade_chromedriver(self, testdir): + # Find out if the installed chromedriver version works with Chrome + subprocess.check_call("seleniumbase get chromedriver", shell=True) + testdir = self.basic_run(testdir) + result = testdir.inline_run("--headless", "-s") # Upgrades as needed + try: + assert result.matchreport("test_passing").passed + except Exception: + # Install the compatibility version of chromedriver + install_command = "seleniumbase get chromedriver 72.0.3626.69" + subprocess.check_call(install_command, shell=True) + testdir = self.upgrade_chromedriver(testdir) + testdir.inline_run("--headless", "-s") + # Print the final installed versions of chromedriver and Chrome + testdir = self.print_versions_of_chromedriver_and_chrome(testdir) + testdir.inline_run("--headless", "-s") + + +if __name__ == "__main__": + from pytest import main + main([__file__]) diff --git a/examples/upload_file_test.py b/examples/upload_file_test.py new file mode 100644 index 00000000000..e8c8e660c0d --- /dev/null +++ b/examples/upload_file_test.py @@ -0,0 +1,21 @@ +"""Testing the self.choose_file() and self.assert_attribute() methods.""" +import os +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class FileUploadButtonTests(BaseCase): + def test_file_upload_button(self): + self.goto("https://seleniumbase.io/w3schools/file_upload") + self.click("button#runbtn") + self.switch_to_frame("iframeResult") + zoom_in = 'input[type="file"]{zoom: 1.6;-moz-transform: scale(1.6);}' + self.add_css_style(zoom_in) + self.highlight('input[type="file"]') + dir_name = os.path.dirname(os.path.abspath(__file__)) + my_file = "screenshot.png" + file_path = os.path.join(dir_name, "example_logs/%s" % my_file) + self.assert_attribute("#myFile", "value", "") + self.choose_file('input[type="file"]', file_path) + self.assert_attribute("#myFile", "value", "C:\\fakepath\\%s" % my_file) + self.highlight('input[type="file"]') diff --git a/examples/user_agent_test.py b/examples/user_agent_test.py old mode 100755 new mode 100644 index 11bc40fe439..a0ee97027aa --- a/examples/user_agent_test.py +++ b/examples/user_agent_test.py @@ -1,14 +1,51 @@ -import time from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) -class MyTestClass(BaseCase): - +class UserAgentTests(BaseCase): def test_user_agent(self): - self.open('https://www.whatsmyua.info/') - user_agent = self.get_text("#custom-ua-string") - print("\n\nUser-Agent = %s\n" % user_agent) - print("Displaying User-Agent Info:") - print(self.get_text("#useragent")) - print("\nThe browser will close automatically in 7 seconds...") - time.sleep(7) + self.goto("data:text/html,

      ") + user_agent_detected = self.get_user_agent() + self.set_text_content("h1", user_agent_detected) + self.highlight("h1") + original_user_agent = user_agent_detected + if not self.user_agent: + # Using the built-in user-agent string + print("\n\nUser-Agent: %s" % user_agent_detected) + else: + # User-agent was overridden using: --agent=STRING + print("\n\nUser-Agent override: %s" % user_agent_detected) + if self.headed: + self.sleep(2.75) + + # Now change the user-agent using "execute_cdp_cmd()" + if not self.is_chromium(): + msg = "\n* execute_cdp_cmd() is only for Chromium browsers" + print(msg) + self.skip(msg) + print("\n--------------------------") + try: + self.execute_cdp_cmd( + "Network.setUserAgentOverride", + { + "userAgent": "Mozilla/5.0 " + "(Nintendo Switch; WifiWebAuthApplet) " + "AppleWebKit/606.4 (KHTML, like Gecko) " + "NF/6.0.1.15.4 NintendoBrowser/5.1.0.20393" + }, + ) + self.goto("about:blank") + self.sleep(0.1) # Enough to see that page was refreshed + self.goto("data:text/html,

      ") + user_agent_detected = self.get_user_agent() + self.set_text_content("h1", user_agent_detected) + self.highlight("h1") + print("\nUser-Agent override: %s" % user_agent_detected) + if self.headed: + self.sleep(2.75) + finally: + # Reset the user-agent back to the original + self.execute_cdp_cmd( + "Network.setUserAgentOverride", + {"userAgent": original_user_agent}, + ) diff --git a/examples/verify_undetected.py b/examples/verify_undetected.py new file mode 100644 index 00000000000..541ea83007a --- /dev/null +++ b/examples/verify_undetected.py @@ -0,0 +1,16 @@ +"""Verify if your browser is detectable by anti-bot services. +To evade detection, add --uc as a pytest command-line option. +UC + CDP Mode is used: CDP Mode is activated from UC Mode.""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__, "--uc") + + +class UndetectedTest(BaseCase): + def test_browser_is_undetected(self): + self.activate_cdp_mode() # If not UC Mode, then 2nd browser + self.goto("https://gitlab.com/users/sign_in") + self.sleep(2) + self.solve_captcha() # (Only runs when a CAPTCHA is visible) + self.assert_text("Username", '[for="user_login"]', timeout=3) + self.post_message("SeleniumBase wasn't detected", duration=4) + self._print("\n Success: SeleniumBase wasn't detected! ") diff --git a/examples/visual_testing/ReadMe.md b/examples/visual_testing/ReadMe.md old mode 100755 new mode 100644 index 431f557ca3c..384a1edf95c --- a/examples/visual_testing/ReadMe.md +++ b/examples/visual_testing/ReadMe.md @@ -1,51 +1,63 @@ -### Automated Visual Testing (Layout Change Detection) + -Automated visual testing can help you detect when something has changed the layout of a web page. Rather than comparing screenshots, a more effective way is to detect layout differences by comparing HTML tags and properties. If a change is detected, it could mean that something broke on the webpage, or possibly something harmless like a website redesign or dynamic content. +

      SeleniumBase

      + +

      Automated Visual Regression Testing

      + +Automated Visual Regression Testing can help you detect when the layout of a web page has changed. Instead of comparing pixels from screenshots, layout differences can be detected by comparing HTML tags and attributes with a baseline. If a change is detected, it could mean that something broke, the web page was redesigned, or dynamic content changed. + + +

      (Watch the tutorial on YouTube)

      To handle automated visual testing, SeleniumBase uses the ``self.check_window()`` method, which can set visual baselines for comparison and then compare the latest versions of web pages to the existing baseline. -The first time a test calls ``self.check_window()`` with a unique "name" parameter, the visual baseline is set, which means a folder is created with the following files: -* page_url.txt -> The URL of the current window -* screenshot.png -> A screenshot of the current window -* tags_level1.txt -> HTML tags from the window -* tags_level2.txt -> HTML tags + attributes from the window -* tags_level3.txt -> HTML tags + attributes/values from the window +The first time a test calls ``self.check_window()`` with a unique ``name`` parameter, the visual baseline is set, which means a folder is created with the following files: + +
    • page_url.txt -> The URL of the current window
    • +
    • baseline.png -> The baseline screenshot (PNG)
    • +
    • tags_level1.txt -> HTML tags from the window
    • +
    • tags_level2.txt -> HTML tags + attribute names
    • +
    • tags_level3.txt -> HTML tags + attribute names+values
    • -After the first time ``self.check_window()`` is called, later calls will compare the HTML tags and properties of the latest window to the ones from the first call (or to the ones from the call when the baseline was last reset). +After the first time ``self.check_window()`` is called, later calls will compare the HTML tags and attributes of the latest window to the ones from the first call (*or to the ones from the call when the baseline was last reset*). Additionally, a ``latest.png`` screenshot is saved in the same folder, which can help you determine if/when the existing baseline needs to be reset. Here's an example call: + +```python +self.check_window(name="first_test)", level=3) ``` -self.check_window(name="first_test)", level=1) -``` + On the first run (or if the baseline is being set/reset) the "level" doesn't matter because that's only used for comparing the current layout to the existing baseline. Here's how the level system works: -* level=0 -> - DRY RUN ONLY - Will perform a comparison to the baseline, and print out any differences that are found, but won't fail the test even if differences exist. -* level=1 -> - HTML tags are compared to tags_level1.txt -* level=2 -> - HTML tags are compared to tags_level1.txt and - HTML tags/attributes are compared to tags_level2.txt -* level=3 -> - HTML tags are compared to tags_level1.txt and - HTML tags + attributes are compared to tags_level2.txt and - HTML tags + attributes/values are compared to tags_level3.txt - -As shown, Level-3 is the most strict, Level-1 is the least strict. If the comparisons from the latest window to the existing baseline don't match, the current test will fail, except for Level-0 tests. + +
    • level=0 -> + DRY RUN ONLY - Will perform a comparison to the baseline, and print out any differences that are found, but won't fail the test even if differences exist.
    • +
    • level=1 -> + HTML tags are compared to tags_level1.txt
    • +
    • level=2 -> + HTML tags and attribute names are compared to tags_level2.txt
    • +
    • level=3 -> + HTML tags and attribute names+values are compared to tags_level3.txt
    • + +As shown, Level-3 is the most strict, Level-1 is the least strict. If the comparisons from the latest window to the existing baseline don't match, the current test will fail, except for Level-0 checks, which print Level-3 results without failing the test. You can reset the visual baseline on the command line by adding the following parameter at runtime: -``--visual_baseline`` + +```zsh +--visual_baseline +``` As long as ``--visual_baseline`` is used on the command line while running tests, the ``self.check_window()`` method cannot fail because it will rebuild the visual baseline rather than comparing the html tags of the latest run to the existing baseline. If there are any expected layout changes to a website that you're testing, you'll need to reset the baseline to prevent unnecessary failures. ``self.check_window()`` will fail with "Page Domain Mismatch Failure" if the domain of the current URL doesn't match the domain of the baseline URL. -If you want to use ``self.check_window()`` to compare a web page to a later version of itself from within the same test run, you can add the parameter ``baseline=True`` to the first time you call ``self.check_window()`` in a test to use that as the baseline. This only makes sense if you're calling ``self.check_window()`` more than once with the same "name" parameter in the same test. +If you want to use ``self.check_window()`` to compare a web page to a later version of itself in the same test, add the ``baseline=True`` parameter to your first ``self.check_window()`` call to use that as the baseline. (This only makes sense if you're calling ``self.check_window()`` more than once with the same "name" parameter in the same test.) + +Automated Visual Testing with ``self.check_window()`` is not very effective for websites that have dynamic content because that changes the layout and structure of web pages. For those pages, you're much better off using regular SeleniumBase functional testing, unless you can remove the dynamic content before performing the comparison, (such as by using ``self.ad_block()`` to remove dynamic ad content on a web page). -Automated Visual Testing with ``self.check_window()`` is not very effective for websites that have dynamic content because that changes the layout and structure of web pages. For those pages, you're much better off using regular SeleniumBase functional testing. +Example usage of ``self.check_window()`` with different levels: -Example usage of ``self.check_window()``: ```python self.check_window(name="testing", level=0) self.check_window(name="xkcd_home", level=1) @@ -57,31 +69,124 @@ Example usage of ``self.check_window()``: self.check_window(name="helloworld", level=3) ``` -Full example test: +Here's an example where clicking a button makes a hidden element visible: + ```python from seleniumbase import BaseCase - +BaseCase.main(__name__, __file__) class VisualLayoutTest(BaseCase): - - def test_applitools_helloworld(self): - self.open('https://applitools.com/helloworld?diff1') - print('Creating baseline in "visual_baseline" folder...') + def test_applitools_layout_change_failure(self): + self.goto('https://applitools.com/helloworld?diff1') + print('\nCreating baseline in "visual_baseline" folder.') self.check_window(name="helloworld", baseline=True) + # Click a button that changes the text of an element self.click('a[href="?diff1"]') - # Verify html tags match previous version - self.check_window(name="helloworld", level=1) - # Verify html tags + attributes match previous version - self.check_window(name="helloworld", level=2) - # Verify html tags + attributes + values match previous version - self.check_window(name="helloworld", level=3) - # Change the page enough for a Level-3 comparison to fail + # Click a button that makes a hidden element visible self.click("button") - self.check_window(name="helloworld", level=1) - self.check_window(name="helloworld", level=2) - with self.assertRaises(Exception): - self.check_window(name="helloworld", level=3) - # Now that we know the exception was raised as expected, - # let's print out the comparison results by running in Level-0. - self.check_window(name="helloworld", level=0) + self.check_window(name="helloworld", level=3) +``` + +Here's the output of that: (Text changes do not impact visual comparisons) + +``` +AssertionError: +First differing element 39: +['div', [['class', ['section', 'hidden-section', 'image-section']]]] +['div', [['class', ['section', 'image-section']]]] + +- ['div', [['class', ['section', 'hidden-section', 'image-section']]]], +? ------------------ ++ ['div', [['class', ['section', 'image-section']]]], +* +*** Exception: Visual Diff Failure: +* HTML tag attribute values don't match the baseline! +``` + +Here's an example where a button is removed from a web page: + +```python +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + +class VisualLayoutTest(BaseCase): + def test_python_home_layout_change_failure(self): + self.goto('https://python.org/') + print('\nCreating baseline in "visual_baseline" folder.') + self.check_window(name="python_home", baseline=True) + # Remove the "Donate" button + self.remove_element('a.donate-button') + self.check_window(name="python_home", level=3) +``` + +Here's the output of that: + +``` +AssertionError: +First differing element 33: +['a', [['class', ['donate-button']], ['href', '/psf/donations/']]] +['div', [['class', ['options-bar']]]] + +- ['a', [['class', ['donate-button']], ['href', '/psf/donations/']]], +- 'display: list-item; opacity: 0.995722;']]], +? ------------------- ++ 'display: list-item;']]], +* +*** Exception: Visual Diff Failure: +* HTML tag attribute values don't match the baseline! +``` + +Here's the ``side_by_side.html`` file for that, (from the ``./latest_logs/`` folder), which shows a visual comparison of the two screenshots as a result of the missing "Donate" button: + + + +Here's another example, where a web site logo is resized: + +```python +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + +class VisualLayoutTest(BaseCase): + def test_xkcd_layout_change_failure(self): + self.goto('https://xkcd.com/554/') + print('\nCreating baseline in "visual_baseline" folder.') + self.check_window(name="xkcd_554", baseline=True) + # Change height: (83 -> 130) , Change width: (185 -> 120) + self.set_attribute('[alt="xkcd.com logo"]', "height", "130") + self.set_attribute('[alt="xkcd.com logo"]', "width", "120") + self.check_window(name="xkcd_554", level=3) +``` + +Here's the output of that: + +``` +AssertionError: +First differing element 22: +['img[30 chars]['height', '83'], ['src', '/s/0b7742.png'], ['width', '185']]] +['img[30 chars]['height', '130'], ['src', '/s/0b7742.png'], ['width', '120']]] + +- ['height', '83'], +? ^ ++ ['height', '130'], +? ^ + +- ['width', '185']]], +? ^^ ++ ['width', '120']]], +? ^^ +* +*** Exception: Visual Diff Failure: +* HTML tag attribute values don't match the baseline! ``` + +To run the example (from [examples/visual_testing/](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/visual_testing/)) with a pytest HTML Report, use: + +```zsh +pytest test_layout_fail.py --html=report.html +``` + +Here's what the pytest HTML Report looks like:
      +[](https://seleniumbase.github.io/cdn/img/visual_testing_report_2.png) + +-------- + +In conclusion, open source automated visual testing tools are being built directly into test frameworks, and this trend is growing. Just like many years ago when free Wi-Fi at coffee shops replaced Internet cafes that charged money for Internet access, open source tools for visual testing will replace their paid counterparts in time. You'll remember this next time you're sipping your Starbucksยฎ Pumpkin Spice Latte with your free Internet access, instead of paying for Internet at cybercafes. diff --git a/examples/visual_testing/__init__.py b/examples/visual_testing/__init__.py old mode 100755 new mode 100644 diff --git a/examples/visual_testing/case_plans/python_home_test.VisualLayoutTests.test_python_home_layout_change.md b/examples/visual_testing/case_plans/python_home_test.VisualLayoutTests.test_python_home_layout_change.md new file mode 100644 index 00000000000..83b7cd48455 --- /dev/null +++ b/examples/visual_testing/case_plans/python_home_test.VisualLayoutTests.test_python_home_layout_change.md @@ -0,0 +1,6 @@ +``python_home_test.py::VisualLayoutTests::test_python_home_layout_change`` +--- +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Open https://python.org/.
      Call ``check_window()`` with ``baseline=True``. | | +| 2 | Remove the ``Donate`` button using ``remove_element(SELECTOR)``.
      Call ``check_window()`` with ``level=0``. | The test detects that the ``Donate`` button was removed. The test does not fail because the check was set to ``level=0`` (print-only).
      A ``side_by_side_NAME.html`` file appears in the specific ``latest_logs/`` folder of the test. | diff --git a/examples/visual_testing/case_plans/test_layout_fail.VisualLayoutFailureTests.test_xkcd_logo_change.md b/examples/visual_testing/case_plans/test_layout_fail.VisualLayoutFailureTests.test_xkcd_logo_change.md new file mode 100644 index 00000000000..8818d9240c7 --- /dev/null +++ b/examples/visual_testing/case_plans/test_layout_fail.VisualLayoutFailureTests.test_xkcd_logo_change.md @@ -0,0 +1,6 @@ +``test_layout_fail.py::VisualLayoutFailureTests::test_xkcd_logo_change`` +--- +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Open https://xkcd.com/554/.
      Call ``check_window()`` with ``baseline=True``. | | +| 2 | Resize the logo using ``set_attribute()``.
      Call ``check_window()`` with ``level=3``. | The test fails because the logo has changed.
      A ``side_by_side.html`` file appears in the specific ``latest_logs/`` folder of the test. | diff --git a/examples/visual_testing/case_plans/test_layout_fail.VisualLayout_FixtureTests.test_python_home_change.md b/examples/visual_testing/case_plans/test_layout_fail.VisualLayout_FixtureTests.test_python_home_change.md new file mode 100644 index 00000000000..972ddb32965 --- /dev/null +++ b/examples/visual_testing/case_plans/test_layout_fail.VisualLayout_FixtureTests.test_python_home_change.md @@ -0,0 +1,6 @@ +``test_layout_fail.py::VisualLayout_FixtureTests::test_python_home_change`` +--- +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Open https://python.org/.
      Call ``check_window()`` with ``baseline=True``. | | +| 2 | Remove the ``Donate`` button using ``remove_element(SELECTOR)``.
      Call ``check_window()`` with ``level=3``. | The test fails because the ``Donate`` button was removed.
      A ``side_by_side.html`` file appears in the specific ``latest_logs/`` folder of the test. | diff --git a/examples/visual_testing/case_plans/xkcd_visual_test.VisualLayoutTests.test_xkcd_layout_change.md b/examples/visual_testing/case_plans/xkcd_visual_test.VisualLayoutTests.test_xkcd_layout_change.md new file mode 100644 index 00000000000..5cfdb9e813b --- /dev/null +++ b/examples/visual_testing/case_plans/xkcd_visual_test.VisualLayoutTests.test_xkcd_layout_change.md @@ -0,0 +1,6 @@ +``xkcd_visual_test.py::VisualLayoutTests::test_xkcd_layout_change`` +--- +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Open https://xkcd.com/554/.
      Call ``check_window()`` with ``baseline=True``. | | +| 2 | Resize the logo using ``set_attribute()``.
      Call ``check_window()`` with ``level=0``. | The test detects that the logo has changed. The test does not fail because the check was set to ``level=0`` (print-only).
      A ``side_by_side_NAME.html`` file appears in the specific ``latest_logs/`` folder of the test. | diff --git a/examples/visual_testing/layout_test.py b/examples/visual_testing/layout_test.py old mode 100755 new mode 100644 index 0e9843113ca..bfd81e5b960 --- a/examples/visual_testing/layout_test.py +++ b/examples/visual_testing/layout_test.py @@ -1,25 +1,24 @@ from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) -class VisualLayoutTest(BaseCase): - - def test_applitools_helloworld(self): - self.open('https://applitools.com/helloworld?diff1') - print('Creating baseline in "visual_baseline" folder...') - self.check_window(name="helloworld", baseline=True) - self.click('a[href="?diff1"]') - # Verify html tags match previous version - self.check_window(name="helloworld", level=1) - # Verify html tags + attributes match previous version - self.check_window(name="helloworld", level=2) - # Verify html tags + attributes + values match previous version - self.check_window(name="helloworld", level=3) - # Change the page enough for a Level-3 comparison to fail - self.click("button") - self.check_window(name="helloworld", level=1) - self.check_window(name="helloworld", level=2) - with self.assertRaises(Exception): - self.check_window(name="helloworld", level=3) - # Now that we know the exception was raised as expected, - # let's print out the comparison results by running in Level-0. - self.check_window(name="helloworld", level=0) +class VisualLayoutTests(BaseCase): + def test_xkcd_layout_change(self): + self.demo_mode = False # (It would interfere with html comparisons) + self.goto("https://xkcd.com/1424/") + print('\nCreating baseline in "visual_baseline" folder.') + self.sleep(0.08) + self.check_window(name="xkcd", baseline=True) + # Go to a different comic + self.goto("https://xkcd.com/1425/") + # Verify html tags match the baseline + self.check_window(name="xkcd", level=1) + # Verify html tags and attribute names match the baseline + self.check_window(name="xkcd", level=2) + # Verify html tags and attribute values don't match the baseline + with self.assert_raises(Exception): + self.check_window(name="xkcd", level=3) + # Now that we know the Exception was raised as expected, + # let's print out the comparison results by running a Level-0 check. + # (NOTE: Running with level-0 will print but NOT raise an Exception.) + self.check_window(name="xkcd", level=0) diff --git a/examples/visual_testing/python_home_test.py b/examples/visual_testing/python_home_test.py new file mode 100644 index 00000000000..f73d716d9fe --- /dev/null +++ b/examples/visual_testing/python_home_test.py @@ -0,0 +1,12 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class VisualLayoutTests(BaseCase): + def test_python_home_layout_change(self): + self.goto("https://python.org/") + print('\nCreating baseline in "visual_baseline" folder.') + self.check_window(name="python_home", baseline=True) + # Remove the "Donate" button + self.remove_element("a.donate-button") + self.check_window(name="python_home", level=0) diff --git a/examples/visual_testing/test_layout_fail.py b/examples/visual_testing/test_layout_fail.py new file mode 100644 index 00000000000..32dbe11d783 --- /dev/null +++ b/examples/visual_testing/test_layout_fail.py @@ -0,0 +1,26 @@ +"""Visual Layout Testing with different Syntax Formats""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class VisualLayout_FixtureTests: + def test_python_home_change(self, sb): + sb.goto("https://python.org/") + print('\nCreating baseline in "visual_baseline" folder.') + sb.check_window(name="python_home", baseline=True) + # Remove the "Donate" button + sb.remove_element("a.donate-button") + print("(This test should fail)") # due to missing button + sb.check_window(name="python_home", level=3) + + +class VisualLayoutFailureTests(BaseCase): + def test_xkcd_logo_change(self): + self.goto("https://xkcd.com/554/") + print('\nCreating baseline in "visual_baseline" folder.') + self.check_window(name="xkcd_554", baseline=True) + # Change height: (83 -> 110) , Change width: (185 -> 120) + self.set_attribute('[alt="xkcd.com logo"]', "height", "110") + self.set_attribute('[alt="xkcd.com logo"]', "width", "120") + print("(This test should fail)") # due to a resized logo + self.check_window(name="xkcd_554", level=3) diff --git a/examples/visual_testing/xkcd_visual_test.py b/examples/visual_testing/xkcd_visual_test.py new file mode 100644 index 00000000000..0f8a37d8333 --- /dev/null +++ b/examples/visual_testing/xkcd_visual_test.py @@ -0,0 +1,13 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class VisualLayoutTests(BaseCase): + def test_xkcd_layout_change(self): + self.goto("https://xkcd.com/554/") + print('\nCreating baseline in "visual_baseline" folder.') + self.check_window(name="xkcd_554", baseline=True) + # Change height: (83 -> 130) , Change width: (185 -> 120) + self.set_attribute('[alt="xkcd.com logo"]', "height", "130") + self.set_attribute('[alt="xkcd.com logo"]', "width", "120") + self.check_window(name="xkcd_554", level=0) diff --git a/examples/wordle_test.py b/examples/wordle_test.py new file mode 100644 index 00000000000..72158d29daa --- /dev/null +++ b/examples/wordle_test.py @@ -0,0 +1,107 @@ +"""Solve Wordle with SeleniumBase.""" +import ast +import random +import requests +from seleniumbase import BaseCase + + +class WordleTests(BaseCase): + word_list = [] + + def initialize_word_list(self): + txt_file = "https://seleniumbase.github.io/cdn/txt/wordle_words.txt" + word_string = requests.get(txt_file, timeout=3).text + 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": + if ( + 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) + else: + for w in self.word_list: + if word[i] != w[i]: + new_word_list.append(w) + self.word_list = new_word_list + new_word_list = [] + + def test_wordle(self): + if self.headless: + self.goto_if_not_url("about:blank") + self.skip("Skip this test in headless mode!") + self.goto("https://www.nytimes.com/games/wordle/index.html") + self.click_if_visible("button.purr-blocker-card__button", timeout=2) + self.click_if_visible('button:contains("Play")', timeout=2) + self.click_if_visible('button[class*="Skip-module"]', timeout=4) + self.click_if_visible('svg[data-testid="icon-close"]', timeout=2) + self.remove_elements('div[class*="Ad-module]') + self.initialize_word_list() + random.seed() + word = random.choice(self.word_list) + num_attempts = 0 + found_word = False + for attempt in range(6): + num_attempts += 1 + if len(self.word_list) == 0: + self.fail("Today's word was not found in my dictionary!") + word = random.choice(self.word_list) + letters = [] + for letter in word: + letters.append(letter) + button = 'button[data-key="%s"]' % letter + self.click(button) + button = 'button[class*="oneAndAHalf"]' + self.click(button) + row = ( + 'div[class*="Board"] div[class*="Row-module"]:nth-of-type(%s) ' + % num_attempts + ) + tile = row + 'div:nth-child(%s) div[class*="module_tile__"]' + self.wait_for_element(tile % "5" + '[data-state$="t"]') + self.wait_for_element(tile % "5" + '[data-animation="idle"]') + letter_status = [] + for i in range(1, 6): + letter_eval = self.get_attribute(tile % str(i), "data-state") + letter_status.append(letter_eval) + if letter_status.count("correct") == 5: + found_word = True + break + self.word_list.remove(word) + self.modify_word_list(word, letter_status) + + self.save_screenshot_to_logs() + if found_word: + print('\nWord: "%s"\nAttempts: %s' % (word.upper(), num_attempts)) + else: + print('Final guess: "%s" (Not the correct word!)' % word.upper()) + self.fail("Unable to solve for the correct word in 6 attempts!") + self.sleep(3) + + +if __name__ == "__main__": + from pytest import main + main([__file__, "-s"]) diff --git a/examples/xpath_test.py b/examples/xpath_test.py new file mode 100644 index 00000000000..73da88eaef9 --- /dev/null +++ b/examples/xpath_test.py @@ -0,0 +1,13 @@ +"""Test that SeleniumBase can autodetect and use xpath selectors.""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class XPathTests(BaseCase): + def test_xpath(self): + self.goto("https://seleniumbase.io/demo_page") + self.assert_element('//h1[(text()="Demo Page")]') + self.type('//*[@id="myTextInput"]', "XPath Test!") + self.click('//button[starts-with(text(),"Click Me")]') + self.assert_element('//button[contains(., "Purple")]') + self.assert_text("SeleniumBase", "//table/tbody/tr[1]/td[2]/h2") diff --git a/examples/youtube_search_test.py b/examples/youtube_search_test.py new file mode 100644 index 00000000000..64ea6a8adc5 --- /dev/null +++ b/examples/youtube_search_test.py @@ -0,0 +1,32 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class YouTubeSearchTests(BaseCase): + def test_youtube_autocomplete_results(self): + """Verify YouTube autocomplete search results.""" + if self.headless or self.browser == "safari": + self.goto_if_not_url("about:blank") + print("\n Unsupported mode for this test.") + self.skip("Unsupported mode for this test.") + self.goto("https://www.youtube.com/c/MichaelMintz") + search_term = "seleniumbase" + search_selector = 'input[name="search_query"]' + results_selector = '[role="listbox"]' + self.click_if_visible('button[aria-label="Close"]') + self.sleep(0.25) + self.double_click(search_selector) + self.sleep(0.25) + self.type(search_selector, search_term) + self.sleep(0.25) + # First verify that an autocomplete result exists + self.assert_element(results_selector) + self.sleep(0.25) + top_results = self.get_text(results_selector) + # Now verify that the autocomplete result is good + self.assert_true( + search_term in top_results, + 'Expected text "%s" not found in top results! ' + 'Actual text was "%s"!' % (search_term, top_results), + ) + self.sleep(1) diff --git a/help_docs/ReadMe.md b/help_docs/ReadMe.md old mode 100755 new mode 100644 index ffd19da7082..3b84b74897d --- a/help_docs/ReadMe.md +++ b/help_docs/ReadMe.md @@ -1,16 +1,164 @@ -### SeleniumBase Help Documents - -> **Table of Contents / Navigation:** -> - [**SeleniumBase Features List**](features_list.md) -> - [**How SeleniumBase Works**](how_it_works.md) -> - [**Webdriver Installation**](webdriver_installation.md) -> - [**Installing Python, Pip, & Git**](install_python_pip_git.md) -> - [**Python Virtual Env Tutorial**](virtualenv_instructions.md) -> - [**Verify Webdriver Installation**](verify_webdriver.md) -> - [**The Command Line Tutorial**](customizing_test_runs.md) -> - [**The Console Scripts Tutorial**](/seleniumbase/console_scripts/ReadMe.md) -> - [**SeleniumBase Method Summary**](method_summary.md) -> - [**MySQL Installation Overview**](mysql_installation.md) -> - [**Safari Driver Detailed Info**](using_safari_driver.md) -> - [**Seeing Hidden Files on macOS**](hidden_files_info.md) -> - [**SeleniumBase Case Studies**](happy_customers.md) + + +

      SeleniumBase

      + +

      Help Docs

      + +

      +๐Ÿš€ Start | +๐Ÿ“Š Dashboard +
      +๐Ÿฐ Features | +๐ŸŽ›๏ธ Options +
      +๐Ÿ“š Examples | +๐Ÿ“ฑ Emulator +
      +๐Ÿ’ป Console Scripts | +๐ŸŒ Grid +
      +๐Ÿ“— Methods / APIs | +๐ŸšŽ Tours +
      +๐Ÿ”  Syntax Formats | +๐Ÿค– CI/CD +
      +โ™ป๏ธ Boilerplates | +๐Ÿ—พ Locale Codes +
      +๐ŸŸจ JS Manager | +๐Ÿ–ผ๏ธ Visual Testing +
      +๐ŸŒ Translator | +๐Ÿ›‚ Dialog Boxes +
      +๐Ÿ”ด Recorder | +๐Ÿ–ฅ๏ธ Device Farm +
      +๐ŸŽญ Stealthy Playwright Mode +
      +๐ŸŽž๏ธ Slides | +๐Ÿ“ถ Chart Maker +
      +๐ŸŽ–๏ธ GUI | +๐Ÿ‘ค UC Mode +
      +๐Ÿ“˜ Stealth API +
      +๐Ÿ™ CDP Mode +

      + +-------- + +

      Table of Contents (seleniumbase.io)

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +-------- + +

      Demo Pages / Web Examples

      + + + + + + + + + + + + + + + + + + + + +-------- + +

      Presentations

      + + + + + + +-------- + +

      GitHub Pages (seleniumbase.dev)

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +-------- + +

      +SeleniumBase +

      + diff --git a/help_docs/behave_gui.md b/help_docs/behave_gui.md new file mode 100644 index 00000000000..bdfc63b59d7 --- /dev/null +++ b/help_docs/behave_gui.md @@ -0,0 +1,51 @@ + + +

      SeleniumBase Behave GUI / Commander ๐Ÿ๐ŸŽ–๏ธ

      + +๐Ÿ๐ŸŽ–๏ธ The SeleniumBase Behave GUI lets you run behave scripts from a Desktop GUI.
      + +๐Ÿ๐ŸŽ–๏ธ To launch it, call ``sbase behave-gui`` or ``sbase gui-behave``: + +```zsh +> sbase behave-gui +* Starting the SeleniumBase Behave Commander GUI App... +``` + + + +๐Ÿ๐ŸŽ–๏ธ SeleniumBase Behave GUI loads the same tests that are found by: + +```zsh +behave -d +``` + +๐Ÿ๐ŸŽ–๏ธ You can customize which tests are loaded by passing additional args: + +```zsh +sbase behave-gui [OPTIONAL PATH or TEST FILE] +``` + +๐Ÿ๐ŸŽ–๏ธ Here are examples of customizing test collection: + +```zsh +sbase behave-gui # all tests +sbase behave-gui -i=calculator # tests with "calculator" in the name +sbase behave-gui features/ # tests located in the "features/" folder +sbase behave-gui features/calculator.feature # tests in that feature +``` + +๐Ÿ๐ŸŽ–๏ธ Once launched, you can further customize which tests to run and what settings to use. There are various controls for changing settings, modes, and other "behave" command line options that are specific to SeleniumBase. You can also set additional options that don't have a visible toggle. When you're ready to run the selected tests with the specified options, click on the Run Selected Tests button. + +๐Ÿโšช With the Dashboard enabled, you'll get one of these: + + + +-------- + +
      To learn more about SeleniumBase, check out the Docs Site:
      + +SeleniumBase.io Docs + +
      All the code is on GitHub:
      + +SeleniumBase on GitHub diff --git a/help_docs/case_plans.md b/help_docs/case_plans.md new file mode 100644 index 00000000000..ad1772f62ba --- /dev/null +++ b/help_docs/case_plans.md @@ -0,0 +1,227 @@ + + +

      SeleniumBase Case Plans ๐Ÿ—‚๏ธ

      + + + +๐Ÿ—‚๏ธ SeleniumBase Case Plans is Test Case Management Software that uses Markdown tables for displaying test plans directly in GitHub (and other source code management systems that support Markdown format). + +๐Ÿ—‚๏ธ The ``case_summary.md`` file is generated from individual Case Plans that exist in the ``case_plans/`` folders of your repository. (See the example below to learn how the Case Summary file may look.) + +-------- + +> **Example of a ``case_summary.md`` file:** + +

      Summary of existing Case Plans

      + +| | | | +| - | -: | - | +| ๐Ÿ”ต | 8 | Case Plans with customized tables | +| โญ• | 2 | Case Plans using boilerplate code | +| ๐Ÿšง | 1 | Case Plan that is missing a table | + +-------- + +

      ๐Ÿ”Ž (Click rows to expand) ๐Ÿ”

      + +
      + ๐Ÿ”ต basic_test.py::MyTestClass::test_basics + +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Log in to https://www.saucedemo.com with ``standard_user``. | Login was successful. | +| 2 | Click on the ``Backpack`` ``ADD TO CART`` button. | The button text changed to ``REMOVE``. | +| 3 | Click on the cart icon. | The ``Backpack`` is seen in the cart. | +| 4 | Remove the ``Backpack`` from the cart. | The ``Backpack`` is no longer in the cart. | +| 5 | Log out from the website. | Logout was successful. | + +
      + +
      + ๐Ÿ”ต list_assert_test.py::MyTestClass::test_assert_list_of_elements + +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Open https://seleniumbase.io/demo_page. | | +| 2 | Use ``self.assert_elements_present("head", "style", "script")`` to verify that multiple elements are present in the HTML. | The assertion is successful. | +| 3 | Use ``self.assert_elements("h1", "h2", "h3")`` to verify that multiple elements are visible. | The assertion is successful. | +| 4 | Use ``self.assert_elements(["#myDropdown", "#myButton", "#svgRect"])`` to verify that multiple elements are visible. | The assertion is successful. | + +
      + +
      + โญ• locale_code_test.py::LocaleCodeTests::test_locale_code + +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Perform Action 1 | Verify Action 1 | +| 2 | Perform Action 2 | Verify Action 2 | + +
      + +
      + ๐Ÿ”ต my_first_test.py::MyTestClass::test_swag_labs + +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Log in to https://www.saucedemo.com with ``standard_user``. | Login was successful. | +| 2 | Click on the ``Backpack`` ``ADD TO CART`` button. | The button text changed to ``REMOVE``. | +| 3 | Click on the cart icon. | The ``Backpack`` is seen in the cart. | +| 4 | Click on the ``CHECKOUT`` button.
      Enter user details and click ``CONTINUE``. | The ``Backpack`` is seen in the cart on the ``CHECKOUT: OVERVIEW`` page. | +| 5 | Click on the ``FINISH`` button. | There is a ``Thank You`` message and a ``Pony Express`` delivery logo. | +| 6 | Log out from the website. | Logout was successful. | + +
      + +
      + โญ• proxy_test.py::ProxyTests::test_proxy + +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Perform Action 1 | Verify Action 1 | +| 2 | Perform Action 2 | Verify Action 2 | + +
      + +
      + ๐Ÿ”ต shadow_root_test.py::ShadowRootTest::test_shadow_root + +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Open https://seleniumbase.io/other/shadow_dom.
      Click each tab and verify the text contained within the Shadow Root sections. | Tab 1 text: ``Content Panel 1``
      Tab 2 text: ``Content Panel 2``
      Tab 3 text: ``Content Panel 3`` | + +
      + +๐Ÿšง test_agent.py::UserAgentTests::test_user_agent + +
      + ๐Ÿ”ต test_calculator.py::CalculatorTests::test_6_times_7_plus_12_equals_54 + +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Open https://seleniumbase.io/apps/calculator.
      Perform the following calculation: ``6 ร— 7 + 12`` | The output is ``54`` after pressing ``=`` | + +
      + +
      + ๐Ÿ”ต test_demo_site.py::DemoSiteTests::test_demo_site + +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Open https://seleniumbase.io/demo_page | | +| 2 | Assert the title of the current web page.
      Assert that a given element is visible on the page.
      Assert that a text substring appears in an element's text. | The assertions were successful. | +| 3 | Type text into various text fields and then verify. | The assertions were successful. | +| 4 | Verify that a hover dropdown link changes page text. | The assertion was successful. | +| 5 | Verify that a button click changes text on the page. | The assertion was successful. | +| 6 | Verify that an SVG element is located on the page. | The assertion was successful. | +| 7 | Verify that a slider control updates a progress bar. | The assertion was successful. | +| 8 | Verify that a "select" option updates a meter bar. | The assertion was successful. | +| 9 | Assert an element located inside an iFrame. | The assertion was successful. | +| 10 | Assert text located inside an iFrame. | The assertion was successful. | +| 11 | Verify that clicking a radio button selects it. | The assertion was successful. | +| 12 | Verify that clicking an empty checkbox makes it selected. | The assertion was successful. | +| 13 | Verify clicking on multiple elements with one call. | The assertions were successful. | +| 14 | Verify that clicking an iFrame checkbox selects it. | The assertions were successful. | +| 15 | Verify that Drag and Drop works. | The assertion was successful. | +| 16 | Assert link text. | The assertion was successful. | +| 17 | Verify clicking on link text. | The action was successful. | +| 18 | Assert exact text in an element. | The assertion was successful. | +| 19 | Highlight a page element. | The action was successful. | +| 20 | Verify that Demo Mode works. | The assertion was successful. | + +
      + +
      + ๐Ÿ”ต test_login.py::SwagLabsLoginTests::test_swag_labs_login + +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Log in to https://www.saucedemo.com with ``standard_user``. | Login was successful. | +| 2 | Log out from the website. | Logout was successful. | + +
      + +
      + ๐Ÿ”ต test_mfa_login.py::TestMFALogin::test_mfa_login + +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Open https://seleniumbase.io/realworld/login
      Enter credentials and Sign In. | Sign In was successful. | +| 2 | Click the ``This Page`` button.
      Save a screenshot to the logs. | | +| 3 | Click to Sign Out | Sign Out was successful. | + +
      + +-------- + +๐Ÿ—‚๏ธ Before you can generate a ``case_summary.md`` file that includes your existing Case Plans, first you'll need to select which existing tests you want to create boilerplate Case Plans from. For that, you can use the SeleniumBase Case Plans GUI: + +```zsh +sbase caseplans +``` + + + +๐Ÿ—‚๏ธ Once you are running the Case Plans GUI, select the existing tests that need Case Plans, and then click: ``Generate boilerplate Case Plans for selected tests missing them``. For each selected test that didn't already have a Case Plan file, one will be generated. Each new Case Plan file starts with default boilerplate code with a Markdown table. Eg: + +```zsh +``proxy_test.py::ProxyTests::test_proxy`` +--- +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Perform Action 1 | Verify Action 1 | +| 2 | Perform Action 2 | Verify Action 2 | + +``` + +๐Ÿ—‚๏ธ When rendered as a Markdown table, the result looks like this: + +``proxy_test.py::ProxyTests::test_proxy`` +--- +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Perform Action 1 | Verify Action 1 | +| 2 | Perform Action 2 | Verify Action 2 | + +๐Ÿ—‚๏ธ Markdown tables are flexible, but must be constructed correctly to be displayed. For a Markdown table to render, it's important that you place pipes (``|``), dashes (``-``), and spaces in the correct locations. If you want a line break in a step, use ``
      ``. If you want an empty step, put a space between pipes, eg: ``| |``. + +๐Ÿ—‚๏ธ Here's an example of a Case Plan for [my_first_test.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/my_first_test.py): + +``my_first_test.py::MyTestClass::test_swag_labs`` +--- +| # | Step Description | Expected Result | +| - | ---------------- | --------------- | +| 1 | Log in to https://www.saucedemo.com with ``standard_user``. | Login was successful. | +| 2 | Click on the ``Backpack`` ``ADD TO CART`` button. | The button text changed to ``REMOVE``. | +| 3 | Click on the cart icon. | The ``Backpack`` is seen in the cart. | +| 4 | Click on the ``CHECKOUT`` button.
      Enter user details and click ``CONTINUE``. | The ``Backpack`` is seen in the cart on the ``CHECKOUT: OVERVIEW`` page. | +| 5 | Click on the ``FINISH`` button. | There is a ``Thank you`` message. | +| 6 | Log out from the website. | Logout was successful. | + +๐Ÿ—‚๏ธ After you've created some Case Plans, you can use the ``Generate Summary of existing Case Plans`` button in the Case Plans GUI to generate the Case Plans Summary file. + + + +๐Ÿ—‚๏ธ The generated Case Plans summary file, ``case_summary.md``, gets created in the same location where the Case Plans GUI was launched. This is NOT the same location where individual Case Plan boilerplates are generated, which is in the ``case_plans/`` folders. The ``case_plans/`` folders are generated where individual tests live, which means that if you have your tests in multiple folders, then you could also have multiple ``case_plans/`` folders. A ``case_summary.md`` file may look like this when rendered: + + + +๐Ÿ—‚๏ธ When calling ``sbase caseplans``, you can provide additional arguments to limit the tests that appear in the list. The same discovery rules apply as when using ``pytest``. Eg: + +```zsh +sbase caseplans +sbase caseplans -k agent +sbase caseplans -m marker2 +sbase caseplans test_suite.py +sbase caseplans offline_examples/ +``` + +-------- + +
      To learn more about SeleniumBase, check out the Docs Site:
      + +SeleniumBase.io Docs + +
      All the code is on GitHub:
      + +SeleniumBase on GitHub diff --git a/help_docs/cdp_mode_methods.md b/help_docs/cdp_mode_methods.md new file mode 100644 index 00000000000..55fa1d7fc86 --- /dev/null +++ b/help_docs/cdp_mode_methods.md @@ -0,0 +1,420 @@ + + +

      SeleniumBase CDP Mode Methods (CDP Mode API Reference)

      + +Here's a list of SeleniumBase CDP Mode method definitions, which are defined in **[sb_cdp.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/core/sb_cdp.py)** + +### ๐Ÿ™ CDP Mode API / Methods + +```python +sb.get(url, **kwargs) +sb.open(url, **kwargs) # Same as sb.get(url, **kwargs) in CDP Mode +sb.goto(url, **kwargs) # Same as sb.get(url, **kwargs) in CDP Mode +sb.reload(ignore_cache=False, script_to_evaluate_on_load=None) +sb.refresh(*args, **kwargs) +sb.goto_if_not_url(url) +sb.get_event_loop() +sb.get_rd_host() # Returns the remote-debugging host +sb.get_rd_port() # Returns the remote-debugging port +sb.get_rd_url() # Returns the remote-debugging URL +sb.get_endpoint_url() # Same as sb.get_rd_url() +sb.get_port() # Same as sb.get_rd_port() +sb.get_websocket_url() # Returns the websocket URL +sb.add_handler(event, handler) +sb.find_element(selector, best_match=False, timeout=None) +sb.find(selector, best_match=False, timeout=None) +sb.locator(selector, best_match=False, timeout=None) +sb.find_element_by_text(text, tag_name=None, timeout=None) +sb.find_all(selector, timeout=None) +sb.find_elements_by_text(text, tag_name=None) +sb.select(selector, timeout=None) +sb.select_all(selector, timeout=None) +sb.find_elements(selector, timeout=None) +sb.find_visible_elements(selector, timeout=None) +sb.click(selector, timeout=None, scroll=True) +sb.click_if_visible(selector, timeout=0, scroll=True) +sb.click_visible_elements(selector, limit=0, scroll=True) +sb.click_nth_element(selector, number, scroll=True) +sb.click_nth_visible_element(selector, number, scroll=True) +sb.click_link(link_text) +sb.click_with_offset(selector, x, y, center=False, scroll=True) +sb.click_and_hold(selector, timeout=None, scroll=True, timeframe=2) +sb.drag_and_drop(drag_selector, drop_selector, timeout=None, scroll=True) +sb.go_back() +sb.go_forward() +sb.get_navigation_history() +sb.tile_windows(windows=None, max_columns=0) +sb.grant_permissions(permissions, origin=None) +sb.grant_all_permissions() +sb.reset_permissions() +sb.get_all_urls(absolute=True) +sb.get_all_cookies(*args, **kwargs) +sb.set_all_cookies(*args, **kwargs) +sb.save_cookies(*args, **kwargs) +sb.load_cookies(*args, **kwargs) +sb.clear_cookies() +sb.sleep(seconds) +sb.bring_active_window_to_front() +sb.bring_to_front() +sb.get_active_element() +sb.get_active_element_css() +sb.click_active_element() +sb.mouse_click(selector, timeout=None, scroll=True) +sb.nested_click(parent_selector, selector) +sb.get_nested_element(parent_selector, selector) +sb.select_option_by_text(dropdown_selector, option) +sb.select_option_by_index(dropdown_selector, option) +sb.select_option_by_value(dropdown_selector, option) +sb.flash(selector, duration=1, color="44CC88", pause=0) +sb.highlight(selector, scroll=True, timeout=None) +sb.focus(selector, timeout=None) +sb.highlight_overlay(selector) +sb.get_parent(element) +sb.remove_element(selector) +sb.remove_from_dom(selector) +sb.remove_elements(selector) +sb.send_keys(selector, text, timeout=None) +sb.press_keys(selector, text, timeout=None) +sb.type(selector, text, timeout=None) +sb.set_value(selector, text, timeout=None) +sb.clear_input(selector, timeout=None) +sb.clear(selector, timeout=None) +sb.submit(selector) +sb.evaluate(expression) +sb.execute_script(expression) +sb.js_dumps(obj_name) +sb.maximize() +sb.minimize() +sb.medimize() +sb.set_window_rect(x, y, width, height) +sb.reset_window_size() +sb.open_new_window(url=None, switch_to=True) +sb.switch_to_window(window) +sb.switch_to_newest_window() +sb.open_new_tab(url=None, switch_to=True) +sb.switch_to_tab(tab) +sb.switch_to_newest_tab() +sb.close_active_tab() +sb.get_active_tab() +sb.get_tabs() +sb.get_window() +sb.get_text(selector="body", timeout=None) +sb.get_title() +sb.get_current_url() +sb.get_origin() +sb.get_html(include_shadow_dom=True) +sb.get_page_source(include_shadow_dom=True) +sb.get_beautiful_soup(source=None) +sb.get_user_agent() +sb.get_cookie_string() +sb.get_locale_code() +sb.get_local_storage_item(key) +sb.get_session_storage_item(key) +sb.get_screen_rect() +sb.get_window_rect() +sb.get_window_size() +sb.get_window_position() +sb.get_element_rect(selector, timeout=None) +sb.get_element_size(selector, timeout=None) +sb.get_element_position(selector, timeout=None) +sb.get_gui_element_rect(selector, timeout=None) +sb.get_gui_element_center(selector, timeout=None) +sb.get_document() +sb.get_flattened_document() +sb.get_element_attributes(selector) +sb.get_element_attribute(selector, attribute) +sb.get_attribute(selector, attribute) +sb.get_element_html(selector, timeout=None) +sb.get_mfa_code(totp_key=None) +sb.enter_mfa_code(selector, totp_key=None, timeout=None) +sb.activate_messenger() +sb.set_messenger_theme(theme="default", location="default") +sb.post_message(message, duration=None, pause=True, style="info") +sb.download_file(file_url) +sb.save_file_as(file_url, new_file_name) +sb.assert_downloaded_file(file, timeout=None) +sb.get_path_of_downloaded_file(file) +sb.set_download_path(path) +sb.set_locale(locale) +sb.set_local_storage_item(key, value) +sb.set_session_storage_item(key, value) +sb.set_attributes(selector, attribute, value) +sb.is_attribute_present(selector, attribute, value=None) +sb.is_online() +sb.solve_captcha() +sb.click_captcha() +sb.gui_press_key(key) +sb.gui_press_keys(keys) +sb.gui_write(text) +sb.gui_click_x_y(x, y, timeframe=0.27) +sb.gui_click_element(selector, timeframe=0.27) +sb.gui_click_with_offset(selector, x, y, timeframe=0.27, center=False) +sb.gui_click_captcha() +sb.gui_drag_drop_points(x1, y1, x2, y2, timeframe=0.35) +sb.gui_drag_and_drop(drag_selector, drop_selector, timeframe=0.35) +sb.gui_click_and_hold(selector, timeframe=0.35) +sb.gui_move_to_element(selector, timeframe=0.27) +sb.gui_hover_x_y(x, y, timeframe=0.27) +sb.gui_hover_element(selector, timeframe=0.27) +sb.gui_hover_and_click(hover_selector, click_selector) +sb.hover_element(selector) +sb.hover_and_click(hover_selector, click_selector) +sb.internalize_links() +sb.is_checked(selector) +sb.is_selected(selector) +sb.check_if_unchecked(selector) +sb.select_if_unselected(selector) +sb.uncheck_if_checked(selector) +sb.unselect_if_selected(selector) +sb.is_element_present(selector) +sb.is_element_visible(selector) +sb.is_text_visible(text, selector="body") +sb.is_exact_text_visible(text, selector="body") +sb.wait_for_text(text, selector="body", timeout=None) +sb.wait_for_text_not_visible(text, selector="body", timeout=None) +sb.wait_for_element_visible(selector, timeout=None) +sb.wait_for_element(selector, timeout=None) +sb.wait_for_element_not_visible(selector, timeout=None) +sb.wait_for_element_absent(selector, timeout=None) +sb.wait_for_any_of_elements_visible(*args, **kwargs) +sb.wait_for_any_of_elements_present(*args, **kwargs) +sb.assert_any_of_elements_visible(*args, **kwargs) +sb.assert_any_of_elements_present(*args, **kwargs) +sb.assert_element(selector, timeout=None) +sb.assert_element_visible(selector, timeout=None) +sb.assert_element_present(selector, timeout=None) +sb.assert_element_absent(selector, timeout=None) +sb.assert_element_not_visible(selector, timeout=None) +sb.assert_element_attribute(selector, attribute, value=None) +sb.assert_title(title) +sb.assert_title_contains(substring) +sb.assert_url(url) +sb.assert_url_contains(substring) +sb.assert_text(text, selector="html", timeout=None) +sb.assert_exact_text(text, selector="html", timeout=None) +sb.assert_text_not_visible(text, selector="body", timeout=None) +sb.assert_true(expression, msg=None) +sb.assert_false(expression, msg=None) +sb.assert_equal(first, second) +sb.assert_not_equal(first, second) +sb.assert_in(first, second) +sb.assert_not_in(first, second) +sb.js_scroll_into_view(selector) +sb.scroll_into_view(selector) +sb.scroll_to_y(y) +sb.scroll_to_top() +sb.scroll_to_bottom() +sb.scroll_up(amount=25) +sb.scroll_down(amount=25) +sb.save_page_source(name, folder=None) +sb.save_as_html(name, folder=None) +sb.save_screenshot(name, folder=None, selector=None) +sb.print_to_pdf(name, folder=None) +sb.save_as_pdf(name, folder=None) +sb.save_as_pdf_to_logs(name=None) +sb.save_screenshot_to_logs(name=None, selector=None) +sb.save_page_source_to_logs(name=None) +sb.save_as_html_to_logs(name=None) +sb.save_data_to_logs(data, file_name=None) +sb.append_data_to_logs(data, file_name=None) +sb.quit() # (Pure CDP Mode only: `sb_cdp`) +``` + +โ„น๏ธ When available, calling `sb.METHOD()` redirects to `sb.cdp.METHOD()` from UC + CDP Mode. + +-------- + + + +### ๐Ÿ™ Pure CDP Mode (sb_cdp) + +In Pure CDP Mode, the browser is launched using CDP, and all browser actions are performed using CDP. WebDriver isn't available at all, but SeleniumBase can still call PyAutoGUI methods when CDP isn't enough. + +๐Ÿ™ Here's how to initialize Pure CDP Mode with a starting URL: + +```python +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(URL) +``` + +๐Ÿ™ You can also initialize Pure CDP Mode and set the URL later: + +```python +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +sb.goto(URL) +``` + +Pure CDP Mode includes all methods from regular CDP Mode. To quit a Pure CDP Mode browser before Python goes out-of-scope, use `sb.quit()`. + +Basic example from [SeleniumBase/examples/cdp_mode/raw_cdp_turnstile.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/raw_cdp_turnstile.py): + +```python +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +sb.goto("https://seleniumbase.io/apps/turnstile") +sb.solve_captcha() +sb.assert_element("img#captcha-success") +sb.set_messenger_theme(location="top_left") +sb.post_message("SeleniumBase wasn't detected", duration=3) +sb.quit() +``` + +Another example: ([SeleniumBase/examples/cdp_mode/raw_cdp_methods.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/raw_cdp_methods.py)) + +```python +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome() +sb.goto("https://seleniumbase.io/demo_page") +sb.press_keys("input", "Text") +sb.highlight("button") +sb.type("textarea", "Here are some words") +sb.click("button") +sb.set_value("input#mySlider", "100") +sb.click_visible_elements("input.checkBoxClassB") +sb.select_option_by_text("#mySelect", "Set to 75%") +sb.gui_hover_and_click("#myDropdown", "#dropOption2") +sb.gui_click_element("#checkBox1") +sb.gui_drag_and_drop("img#logo", "div#drop2") +sb.nested_click("iframe#myFrame3", ".fBox") +sb.sleep(2) +sb.quit() +``` + +โ„น๏ธ Even if you don't call `sb.quit()`, the browser still quits after the script goes out-of-scope. + +-------- + +### ๐Ÿ™ CDP Mode Async API / Methods + +Initialization: + +```python +from seleniumbase import cdp_driver + +driver = await cdp_driver.start_async() +page = await driver.get(url, **kwargs) +``` + +Methods: (Sometimes `page` is named `tab` in examples where `page` is already taken.) + +```python +await page.get(url="about:blank") +await page.open(url="about:blank") # Same as await page.get(url) in CDP Mode +await page.goto(url="about:blank") # Same as await page.get(url) in CDP Mode +await page.find(text, best_match=False, timeout=10) # text can be selector +await page.find_all(text, timeout=10) # text can be selector +await page.select(selector, timeout=10) +await page.select_all(selector, timeout=10, include_frames=False) +await page.query_selector(selector) +await page.query_selector_all(selector) +await page.find_element_by_text(text, best_match=False) +await page.find_elements_by_text(text) +await page.send_keys(selector, text, timeout=5) +await page.type(selector, text, timeout=5) +await page.click(selector, timeout=5) +await page.click_if_visible(selector, timeout=0) +await page.click_with_offset(selector, x, y, center=False, timeout=5) +await page.solve_captcha() +await page.click_captcha() # Same as solve_captcha() +await page.reload(ignore_cache=True, script_to_evaluate_on_load=None) +await page.evaluate(expression) +await page.js_dumps(obj_name) +await page.back() +await page.forward() +await page.get_window() +await page.get_content() +await page.maximize() +await page.minimize() +await page.fullscreen() +await page.medimize() +await page.set_window_size(left=0, top=0, width=1280, height=1024) +await page.set_window_rect(left=0, top=0, width=1280, height=1024) +await page.activate() +await page.bring_to_front() +await page.set_window_state(left=0, top=0, width=1280, height=720, state="normal") +await page.get_navigation_history() +await page.get_user_agent() +await page.get_cookie_string() +await page.get_locale_code() +await page.is_element_present(selector) +await page.is_element_visible(selector) +await page.is_online() +await page.open_external_inspector() # Open separate browser for debugging +await page.close() +await page.scroll_down(amount=25) +await page.scroll_up(amount=25) +await page.wait_for(selector="", text="", timeout=10) +await page.set_attributes(selector, attribute, value) +await page.internalize_links() +await page.download_file(url, filename=None) +await page.save_screenshot(filename="auto", format="png", full_page=False) +await page.print_to_pdf(filename="auto") +await page.set_download_path(path) +await page.get_all_linked_sources() +await page.get_all_urls(absolute=True) +await page.get_html() +await page.get_page_source() +await page.get_element_rect(selector, timeout=5) # (window-based) +await page.get_window_rect() +await page.get_gui_element_rect(selector, timeout=5) # (screen-based) +await page.get_title() +await page.get_current_url() +await page.get_origin() +await page.get_document() +await page.get_flattened_document() +await page.get_local_storage() +await page.set_local_storage(items) +``` + +-------- + +### ๐Ÿ™ CDP Mode WebElement API / Methods + +After finding an element in CDP Mode, you can access `WebElement` methods: + +(Eg. After `element = sb.find_element(selector)`) + +```python +element.clear_input() +element.click() +element.click_with_offset(x, y, center=False) +element.click_and_hold(timeframe=2) +element.flash(duration=0.5, color="EE4488") +element.focus() +element.gui_click(timeframe=0.27) +element.highlight_overlay() +element.is_in_viewport() +element.mouse_click() +element.mouse_drag(destination) +element.mouse_move() +element.press_keys(text) +element.query_selector(selector) +element.querySelector(selector) +element.query_selector_all(selector) +element.querySelectorAll(selector) +element.remove_from_dom() +element.save_screenshot(*args, **kwargs) +element.save_to_dom() +element.scroll_into_view() +element.select_option() +element.send_file(*file_paths) +element.send_keys(text) +element.set_text(value) +element.type(text) +element.get_position() +element.get_html() +element.get_js_attributes() +element.get_attribute(attribute) +element.get_parent() +``` + +-------- + +SeleniumBase + +
      SeleniumBase
      diff --git a/help_docs/commander.md b/help_docs/commander.md new file mode 100644 index 00000000000..351a4da2d6d --- /dev/null +++ b/help_docs/commander.md @@ -0,0 +1,57 @@ + + +

      SeleniumBase Commander ๐ŸŽ–๏ธ

      + +๐ŸŽ–๏ธ SeleniumBase Commander lets you run pytest scripts from a Desktop GUI.
      + +๐ŸŽ–๏ธ To launch it, call ``sbase commander`` or ``sbase gui``: + +```zsh +sbase gui +* Starting the SeleniumBase Commander Desktop App... +``` + + + +๐ŸŽ–๏ธ SeleniumBase Commander loads the same tests that are found by: + +```zsh +pytest --co -q +``` + +๐ŸŽ–๏ธ You can customize which tests are loaded by passing additional args: + +```zsh +sbase commander [OPTIONAL PATH or TEST FILE] +sbase gui [OPTIONAL PATH or TEST FILE] +``` + +๐ŸŽ–๏ธ Here are examples of customizing test collection: + +```zsh +sbase gui +sbase gui -k agent +sbase gui -m marker2 +sbase gui test_suite.py +sbase gui offline_examples/ +``` + +๐ŸŽ–๏ธ Once launched, you can further customize which tests to run and what settings to use. There are various controls for changing settings, modes, and other pytest command line options that are specific to SeleniumBase. You can also set additional options that don't have a visible toggle. When you're ready to run the selected tests with the specified options, click on the Run Selected Tests button. + +-------- + +

      Other SeleniumBase Commanders:

      + +* ๐Ÿ๐ŸŽ–๏ธ [SeleniumBase Behave GUI / Commander](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/behave_gui.md) + + + +-------- + +
      To learn more about SeleniumBase, check out the Docs Site:
      + +SeleniumBase.io Docs + +
      All the code is on GitHub:
      + +SeleniumBase on GitHub diff --git a/help_docs/customizing_test_runs.md b/help_docs/customizing_test_runs.md old mode 100755 new mode 100644 index 54073193380..7a7b3d999c4 --- a/help_docs/customizing_test_runs.md +++ b/help_docs/customizing_test_runs.md @@ -1,192 +1,892 @@ -### Customizing test runs with **pytest** (or nosetests) + -In addition to [settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py) (which lets you customize SeleniumBase global properties) you can customize test runs from the command line: + -* Choose the browser for tests to use (Default: Chrome) -* Choose betweeen pytest & nose unittest runners -* Choose whether to enter Debug Mode on failures -* Choose additional variables to pass into tests -* Choose the User-Agent for the browser to use -* Change the automation speed (with Demo Mode) -* Choose whether to run tests multi-threaded -* Choose whether to retry failing tests -* Choose a BrowserStack server to run on -* Choose a Sauce Labs server to run on -* Choose a TestingBot server to run on -* Choose a CrossBrowserTesting server -* Choose a Selenium Grid to connect to -* Choose a database to save results to -* Choose a proxy server to connect to +

      Options / Customization

      -...and more! +

      ๐ŸŽ›๏ธ SeleniumBase has different ways of setting options, depending on which format you're using. Options can be set via the command-line or method call.

      -#### **Examples:** +
      +

      + +
      -(These are run from the **[examples](https://github.com/seleniumbase/SeleniumBase/tree/master/examples)** folder.) +-------- -```bash + +

      01. pytest command-line options

      + +๐ŸŽ›๏ธ SeleniumBase's [pytest plugin](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/plugins/pytest_plugin.py) lets you customize test runs from the CLI (Command-Line Interface), which adds options for setting/enabling the browser type, Dashboard Mode, Demo Mode, Headless Mode, Mobile Mode, Multi-threading Mode, Recorder Mode, UC Mode (stealth), reuse-session mode, Proxy Mode, and more. + +๐ŸŽ›๏ธ Here are some examples of configuring tests, which can be run from the [examples/](https://github.com/seleniumbase/SeleniumBase/tree/master/examples) folder: + +```zsh +# Run a test in Chrome (default browser) pytest my_first_test.py -pytest my_first_test.py --demo_mode --browser=chrome +# Run a test in Edge +pytest test_swag_labs.py --edge + +# Run a test in Demo Mode (highlight assertions) +pytest test_demo_site.py --demo + +# Run a test in Headless Mode (invisible browser) +pytest test_demo_site.py --headless + +# Run tests multi-threaded using [n] threads +pytest test_suite.py -n4 -pytest my_first_test.py --browser=firefox +# Reuse the browser session for all tests ("--reuse-session") +pytest test_suite.py --rs +# Reuse the browser session, but erase cookies between tests +pytest test_suite.py --rs --crumbs + +# Create a real-time dashboard for test results +pytest test_suite.py --dashboard + +# Create a pytest-html report after tests are done pytest test_suite.py --html=report.html -nosetests test_suite.py --report --show_report +# Rerun failing tests more times +pytest test_suite.py --reruns=1 + +# Activate Debug Mode at the start ("n": next. "c": continue) +pytest test_null.py --trace -s + +# Activate Debug Mode on failures ("n": next. "c": continue) +pytest test_fail.py --pdb -s + +# Activate Debug Mode at the end ("n": next. "c": continue) +pytest test_fail.py --ftrace -s -pytest test_suite.py --headless -n 4 +# Activate Recorder/Debug Mode as the test begins ("c" to continue) +pytest test_null.py --recorder --trace -s -pytest test_suite.py --reruns 1 --reruns-delay 2 +# Pass extra data into tests (retrieve by calling self.data) +pytest my_first_test.py --data="ABC" +# Run tests on a local Selenium Grid +pytest test_suite.py --server="127.0.0.1" + +# Run tests on a remote Selenium Grid pytest test_suite.py --server=IP_ADDRESS --port=4444 +# Run tests on a remote Selenium Grid with authentication +pytest test_suite.py --server=USERNAME:KEY@IP_ADDRESS --port=80 + +# Run tests through a proxy server pytest proxy_test.py --proxy=IP_ADDRESS:PORT +# Run tests through a proxy server with authentication pytest proxy_test.py --proxy=USERNAME:PASSWORD@IP_ADDRESS:PORT -pytest user_agent_test.py --agent="USER-AGENT STRING" +# Run tests while setting the web browser's User Agent +pytest user_agent_test.py --agent="USER-AGENT-STRING" -pytest test_fail.py --pdb -s +# Run tests using Chrome's mobile device emulator (default settings) +pytest test_swag_labs.py --mobile + +# Run mobile tests specifying CSS Width, CSS Height, and Pixel-Ratio +pytest test_swag_labs.py --mobile --metrics="360,640,2" + +# Run tests using UC Mode to evade bot-detection services +pytest verify_undetected.py --uc + +# Run tests while changing SeleniumBase default settings +pytest my_first_test.py --settings-file=custom_settings.py ``` -You can interchange **pytest** with **nosetests**, but using pytest is strongly recommended because developers stopped supporting nosetests. Chrome is the default browser if not specified. +๐ŸŽ›๏ธ You can interchange `pytest` with `nosetests` for most tests, but using `pytest` is recommended. (`chrome` is the default browser if not specified.) -(NOTE: If you're using **pytest** for running tests outside of the SeleniumBase repo, **you'll want a copy of [pytest.ini](https://github.com/seleniumbase/SeleniumBase/blob/master/pytest.ini) at the base of the new folder structure**. If using **nosetests**, the same applies for [setup.cfg](https://github.com/seleniumbase/SeleniumBase/blob/master/setup.cfg).) +๐ŸŽ›๏ธ If you're using `pytest` for running tests outside of the SeleniumBase repo, you'll want a copy of [pytest.ini](https://github.com/seleniumbase/SeleniumBase/blob/master/pytest.ini) at the base of the new folder structure. If using `nosetests`, the same applies for [setup.cfg](https://github.com/seleniumbase/SeleniumBase/blob/master/setup.cfg). -#### **Running tests on [BrowserStack](https://www.browserstack.com/automate#)'s Selenium Grid, the [Sauce Labs](https://saucelabs.com/products/open-source-frameworks/selenium) Selenium Grid, the [TestingBot](https://testingbot.com/features) Selenium Grid, (or your own):** +๐ŸŽ›๏ธ Here are some useful command-line options that come with `pytest`: -Here's how to connect to a BrowserStack Selenium Grid server for running tests: +```zsh +-v # Verbose mode. Prints the full name of each test and shows more details. +-q # Quiet mode. Print fewer details in the console output when running tests. +-x # Stop running the tests after the first failure is reached. +--html=report.html # Creates a detailed pytest-html report after tests finish. +--co | --collect-only # Show what tests would get run. (Without running them) +--co -q # (Both options together!) - Do a dry run with full test names shown. +-n=NUM # Multithread the tests using that many threads. (Speed up test runs!) +-s # See print statements. (Should be on by default with pytest.ini present.) +--junit-xml=report.xml # Creates a junit-xml report after tests finish. +--pdb # If a test fails, enter Post Mortem Debug Mode. (Don't use with CI!) +--trace # Enter Debug Mode at the beginning of each test. (Don't use with CI!) +-m=MARKER # Run tests with the specified pytest marker. +``` -```bash -pytest my_first_test.py --server=USERNAME:KEY@hub.browserstack.com --port=80 +๐ŸŽ›๏ธ SeleniumBase provides additional `pytest` command-line options for tests: + +```zsh +--browser=BROWSER # (The web browser to use. Default: "chrome".) +--chrome # (Shortcut for "--browser=chrome". On by default.) +--edge # (Shortcut for "--browser=edge".) +--firefox # (Shortcut for "--browser=firefox".) +--safari # (Shortcut for "--browser=safari".) +--opera # (Shortcut for "--browser=opera".) +--brave # (Shortcut for "--browser=brave".) +--comet # (Shortcut for "--browser=comet".) +--chromium # (Shortcut for using base `Chromium`) +--settings-file=FILE # (Override default SeleniumBase settings.) +--env=ENV # (Set the test env. Access with "self.env" in tests.) +--account=STR # (Set account. Access with "self.account" in tests.) +--data=STRING # (Extra test data. Access with "self.data" in tests.) +--var1=STRING # (Extra test data. Access with "self.var1" in tests.) +--var2=STRING # (Extra test data. Access with "self.var2" in tests.) +--var3=STRING # (Extra test data. Access with "self.var3" in tests.) +--variables=DICT # (Extra test data. Access with "self.variables".) +--user-data-dir=DIR # (Set the Chrome user data directory to use.) +--protocol=PROTOCOL # (The Selenium Grid protocol: http|https.) +--server=SERVER # (The Selenium Grid server/IP used for tests.) +--port=PORT # (The Selenium Grid port used by the test server.) +--cap-file=FILE # (The web browser's desired capabilities to use.) +--cap-string=STRING # (The web browser's desired capabilities to use.) +--proxy=SERVER:PORT # (Connect to a proxy server:port as tests are running) +--proxy=USERNAME:PASSWORD@SERVER:PORT # (Use an authenticated proxy server) +--proxy-bypass-list=STRING # (";"-separated hosts to bypass, Eg "*.foo.com") +--proxy-pac-url=URL # (Connect to a proxy server using a PAC_URL.pac file.) +--proxy-pac-url=USERNAME:PASSWORD@URL # (Authenticated proxy with PAC URL.) +--proxy-driver # (If a driver download is needed, will use: --proxy=PROXY.) +--multi-proxy # (Allow multiple authenticated proxies when multi-threaded.) +--agent=STRING # (Modify the web browser's User-Agent string.) +--mobile # (Use the mobile device emulator while running tests.) +--metrics=STRING # (Set mobile metrics: "CSSWidth,CSSHeight,PixelRatio".) +--chromium-arg="ARG=N,ARG2" # (Set Chromium args, ","-separated, no spaces.) +--firefox-arg="ARG=N,ARG2" # (Set Firefox args, comma-separated, no spaces.) +--firefox-pref=SET # (Set a Firefox preference:value set, comma-separated.) +--extension-zip=ZIP # (Load a Chrome Extension .zip|.crx, comma-separated.) +--extension-dir=DIR # (Load a Chrome Extension directory, comma-separated.) +--disable-features="F1,F2" # (Disable features, comma-separated, no spaces.) +--binary-location=PATH # (Set path of the Chromium browser binary to use.) +--driver-version=VER # (Set the chromedriver or uc_driver version to use.) +--sjw # (Skip JS Waits for readyState to be "complete" or Angular to load.) +--wfa # (Wait for AngularJS to be done loading after specific web actions.) +--pls=PLS # (Set pageLoadStrategy on Chrome: "normal", "eager", or "none".) +--headless # (The default headless mode. Linux uses this mode by default.) +--headless1 # (Use Chrome's old headless mode. Fast, but has limitations.) +--headless2 # (Use Chrome's new headless mode, which supports extensions.) +--headed # (Run tests in headed/GUI mode on Linux OS, where not default.) +--xvfb # (Run tests using the Xvfb virtual display server on Linux OS.) +--xvfb-metrics=STRING # (Set Xvfb display size on Linux: "Width,Height".) +--locale=LOCALE_CODE # (Set the Language Locale Code for the web browser.) +--interval=SECONDS # (The autoplay interval for presentations & tour steps) +--start-page=URL # (The starting URL for the web browser when tests begin.) +--archive-logs # (Archive existing log files instead of deleting them.) +--archive-downloads # (Archive old downloads instead of deleting them.) +--time-limit=SECONDS # (Safely fail any test that exceeds the time limit.) +--slow # (Slow down the automation. Faster than using Demo Mode.) +--demo # (Slow down and visually see test actions as they occur.) +--demo-sleep=SECONDS # (Set the wait time after Slow & Demo Mode actions.) +--highlights=NUM # (Number of highlight animations for Demo Mode actions.) +--message-duration=SECONDS # (The time length for Messenger alerts.) +--check-js # (Check for JavaScript errors after page loads.) +--ad-block # (Block some types of display ads from loading.) +--host-resolver-rules=RULES # (Set host-resolver-rules, comma-separated.) +--block-images # (Block images from loading during tests.) +--do-not-track # (Indicate to websites that you don't want to be tracked.) +--verify-delay=SECONDS # (The delay before MasterQA verification checks.) +--ee | --esc-end # (Lets the user end the current test via the ESC key.) +--recorder # (Enables the Recorder for turning browser actions into code.) +--rec-sb-mgr # (A Recorder Mode that generates SB() context manager code.) +--rec-sb-cdp # (A Recorder Mode that generates Pure CDP Mode sb_cdp code.) +--rec-behave # (Same as Recorder Mode, but also generates behave-gherkin.) +--rec-sleep # (If the Recorder is enabled, also records self.sleep calls.) +--rec-print # (If the Recorder is enabled, prints output after tests end.) +--disable-cookies # (Disable Cookies on websites. Pages might break!) +--disable-js # (Disable JavaScript on websites. Pages might break!) +--disable-csp # (Disable the Content Security Policy of websites.) +--disable-ws # (Disable Web Security on Chromium-based browsers.) +--enable-ws # (Enable Web Security on Chromium-based browsers.) +--enable-sync # (Enable "Chrome Sync" on websites.) +--uc | --undetected # (Use undetected-chromedriver to evade bot-detection.) +--uc-cdp-events # (Capture CDP events when running in "--undetected" mode.) +--log-cdp # ("goog:loggingPrefs", {"performance": "ALL", "browser": "ALL"}) +--remote-debug # (Sync to Chrome Remote Debugger chrome://inspect/#devices) +--ftrace | --final-trace # (Debug Mode after each test. Don't use with CI!) +--dashboard # (Enable the SeleniumBase Dashboard. Saved at: dashboard.html) +--dash-title=STRING # (Set the title shown for the generated dashboard.) +--enable-3d-apis # (Enables WebGL and 3D APIs.) +--swiftshader # (Chrome "--use-gl=angle" / "--use-angle=swiftshader-webgl") +--incognito # (Enable Chrome's Incognito mode.) +--guest # (Enable Chrome's Guest mode.) +--dark # (Enable Chrome's Dark mode.) +--devtools # (Open Chrome's DevTools when the browser opens.) +--rs | --reuse-session # (Reuse browser session for all tests.) +--rcs | --reuse-class-session # (Reuse session for tests in class.) +--crumbs # (Delete all cookies between tests reusing a session.) +--disable-beforeunload # (Disable the "beforeunload" event on Chrome.) +--window-position=X,Y # (Set the browser's starting window position.) +--window-size=WIDTH,HEIGHT # (Set the browser's starting window size.) +--maximize # (Start tests with the browser window maximized.) +--screenshot # (Save a screenshot at the end of each test.) +--no-screenshot # (No screenshots saved unless tests directly ask it.) +--visual-baseline # (Set the visual baseline for Visual/Layout tests.) +--external-pdf # (Set Chromium "plugins.always_open_pdf_externally":True.) +--timeout-multiplier=MULTIPLIER # (Multiplies the default timeout values.) +--list-fail-page # (After each failing test, list the URL of the failure.) ``` -Here's how to connect to a Sauce Labs Selenium Grid server for running tests: +(For more details, see the full list of command-line options **[here](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/plugins/pytest_plugin.py)**.) + +๐ŸŽ›๏ธ You can also view a list of popular `pytest` options for SeleniumBase by typing: -```bash -pytest my_first_test.py --server=USERNAME:KEY@ondemand.saucelabs.com --port=80 +```zsh +seleniumbase options ``` -Here's how to connect to a TestingBot Selenium Grid server for running tests: +Or the short form: -```bash -pytest my_first_test.py --server=USERNAME:KEY@hub.testingbot.com --port=80 +```zsh +sbase options ``` -Here's how to connect to a CrossBrowserTesting Selenium Grid server for running tests: -```bash -pytest my_first_test.py --server=USERNAME:KEY@hub.crossbrowsertesting.com --port=80 -``` +-------- -Or you can create your own Selenium Grid for test distribution. ([See this ReadMe for details](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/utilities/selenium_grid/ReadMe.md)) +

      Example tests using Logging:

      -#### **Example tests using Logging:** +To see logging abilities, you can run a test suite that includes tests that fail on purpose: -```bash -pytest test_suite.py --browser=chrome +```zsh +pytest test_suite.py ``` -(During test failures, logs and screenshots from the most recent test run will get saved to the ``latest_logs/`` folder. Those logs will get moved to ``archived_logs/`` if you have ARCHIVE_EXISTING_LOGS set to True in [settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py), otherwise log files with be cleaned up at the start of the next test run.) -#### **Demo Mode:** +๐Ÿ”ต During test failures, logs and screenshots from the most recent test run will get saved to the `latest_logs/` folder. If `--archive-logs` is specified (or if ARCHIVE_EXISTING_LOGS is set to True in [settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py)), test logs will also get archived to the `archived_logs/` folder. Otherwise, the log files will be cleaned out when the next test run begins (by default). + +

      Demo Mode:

      + +๐Ÿ”ต If any test is moving too fast for your eyes to see what's going on, you can run it in **Demo Mode** by adding `--demo` on the command line, which pauses the browser briefly between actions, highlights page elements being acted on, and lets you know what test assertions are happening in real-time: -If any test is moving too fast for your eyes to see what's going on, you can run it in **Demo Mode** by adding ``--demo_mode`` on the command line, which pauses the browser briefly between actions, highlights page elements being acted on, and lets you know what test assertions are happening in real time: +```zsh +pytest my_first_test.py --demo +``` + +๐Ÿ”ต You can override the default wait time by either updating [settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py) or by using `--demo-sleep=NUM` when using Demo Mode. (NOTE: If you use `--demo-sleep=NUM` without using `--demo`, nothing will happen.) -```bash -pytest my_first_test.py --browser=chrome --demo_mode +```zsh +pytest my_first_test.py --demo --demo-sleep=1.2 ``` -You can override the default wait time by either updating [settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py) or by using ``--demo_sleep={NUM}`` when using Demo Mode. (NOTE: If you use ``--demo_sleep={NUM}`` without using ``--demo_mode``, nothing will happen.) +

      Passing additional data to tests:

      + +If you want to pass additional data from the command line to your tests, you can use `--data=STRING`. Now inside your tests, you can use `self.data` to access that. -```bash -pytest my_first_test.py --browser=chrome --demo_mode --demo_sleep=1.2 +

      Running tests multithreaded:

      + +To run `pytest` with multiple processes, add `-n=NUM`, `-n NUM`, or `-nNUM` on the command line, where `NUM` is the number of CPUs you want to use. + +```zsh +pytest -n=8 +pytest -n 8 +pytest -n8 ``` -#### **Passing additional data to tests:** +

      How to retry failing tests automatically:

      -If you want to pass additional data from the command line to your tests, you can use ``--data=STRING``. Now inside your tests, you can use ``self.data`` to access that. +

      You can use pytest --reruns=NUM to retry failing tests that many times. Add --reruns-delay=SECONDS to wait that many seconds between retries. Example:

      -#### **Running tests multithreaded:** +```zsh +pytest --reruns=1 --reruns-delay=1 +``` -To run Pytest multithreaded on multiple CPUs at the same time, add ``-n=NUM`` or ``-n NUM`` on the command line, where NUM is the number of CPUs you want to use. +

      Debugging tests:

      -#### **Retrying failing tests automatically:** +๐Ÿ”ต Use `breakpoint()` in your scripts to help you debug issues: -You can use ``--reruns NUM`` to retry failing tests that many times. Use ``--reruns-delay SECONDS`` to wait that many seconds between retries. Example: +```python +breakpoint() # Debug Mode. n: next, c: continue, s: step, u: up, d: down. ``` -pytest --reruns 5 --reruns-delay 1 + +๐Ÿ”ต To pause an active test that throws an exception or error, (*and keep the browser window open while **Debug Mode** begins in the console*), add **`--pdb`** as a `pytest` option: + +```zsh +pytest test_fail.py --pdb ``` -#### **Debugging tests:** +๐Ÿ”ต To start tests in Debug Mode, add **`--trace`** as a `pytest` option: + +```zsh +pytest test_coffee_cart.py --trace +``` + +(**`pdb`** commands: `n`, `c`, `s`, `u`, `d` => `next`, `continue`, `step`, `up`, `down`). + +

      Combinations of options:

      + +๐ŸŽ›๏ธ There are times when you'll want to combine various command-line options for added effect. +For instance, the multi-process option, `-n8`, can be customized by adding: +`--dist=loadscope` or `--dist=loadfile` to it. +There's more info on that here: [pytest-xdist](https://pypi.org/project/pytest-xdist/2.5.0/): + +* `-n8 --dist=loadscope`: Tests are grouped by module for test functions and by class for test methods. Groups are distributed to available workers as whole units. This guarantees that all tests in a group run in the same process. This can be useful if you have expensive module-level or class-level fixtures. Grouping by class takes priority over grouping by module. + +* `-n8 --dist=loadfile`: Tests are grouped by their containing file. Groups are distributed to available workers as whole units. This guarantees that all tests in a file run in the same worker. + +
      + โ–ถ๏ธ -n8 --dist=loadgroup (click to expand) +
      + +
      • Tests are grouped by the xdist_group mark. Groups are distributed to available workers as whole units. This guarantees that all tests with the same xdist_group name run in the same worker.
      -**You can use the following code snippets in your scripts to help you debug issues:** ```python -import time; time.sleep(5) # sleep for 5 seconds (add this after the line you want to pause on) -import ipdb; ipdb.set_trace() # waits for your command. n = next line of current method, c = continue, s = step / next executed line (will jump) -import pytest; pytest.set_trace() # similar to ipdb, but specific to pytest +@pytest.mark.xdist_group(name="group1") +def test_1(): + pass + +class Test: + @pytest.mark.xdist_group("group1") + def test_2(): + pass ``` -**To pause an active test that throws an exception or error, add ``--pdb -s``:** +

      This makes test_1 and Test::test_2 run in the same worker. Tests without the xdist_group mark are distributed normally.

      + +
      +
      + +๐ŸŽ›๏ธ You might also want to combine multiple options at once. For example: -```bash -pytest my_first_test.py --browser=chrome --pdb -s +```zsh +pytest --headless -n8 --dashboard --html=report.html -v --rs --crumbs ``` -The code above will leave your browser window open in case there's a failure. (ipdb commands: 'c', 's', 'n' => continue, step, next). +The above not only runs tests in parallel processes, but it also tells tests in the same process to share the same browser session, runs the tests in headless mode, displays the full name of each test on a separate line, creates a real-time dashboard of the test results, and creates a full report after all tests complete. -#### **Pytest Reports:** +-------- -Using ``--html=report.html`` gives you a fancy report of the name specified after your test suite completes. +๐ŸŽ›๏ธ For extra speed, run your tests using `chrome-headless-shell`: -```bash +First, get `chrome-headless-shell` if you don't already have it: + +```zsh +sbase get chs +``` + +Then, run scripts with `--chs` / `chs=True`: + +```zsh +pytest --chs -n8 --dashboard --html=report.html -v --rs +``` + +That makes your tests run very quickly in headless mode. + +-------- + +

      The SeleniumBase Dashboard:

      + +๐Ÿ”ต The `--dashboard` option for pytest generates a SeleniumBase Dashboard located at `dashboard.html`, which updates automatically as tests run and produce results. Example: + +```zsh +pytest --dashboard --rs --headless +``` + +The SeleniumBase Dashboard + +๐Ÿ”ต Additionally, you can host your own SeleniumBase Dashboard Server on a port of your choice. Here's an example of that using Python 3's `http.server`: + +```zsh +python -m http.server 1948 +``` + +๐Ÿ”ต Now you can navigate to `http://localhost:1948/dashboard.html` in order to view the dashboard as a web app. This requires two different terminal windows: one for running the server, and another for running the tests, which should be run from the same directory. (Use Ctrl+C to stop the http server.) + +๐Ÿ”ต Here's a full example of what the SeleniumBase Dashboard may look like: + +```zsh +pytest test_suite.py --dashboard --rs --headless +``` + +The SeleniumBase Dashboard + +-------- + +

      Pytest Reports:

      + +๐Ÿ”ต Using `--html=report.html` gives you a fancy report of the name specified after your test suite completes. + +```zsh pytest test_suite.py --html=report.html ``` -![](https://cdn2.hubspot.net/hubfs/100006/images/PytestReport.png "Example Pytest Report") -#### **Nosetest Reports:** +Example Pytest Report + +๐Ÿ”ต When combining pytest html reports with SeleniumBase Dashboard usage, the pie chart from the Dashboard will get added to the html report. Additionally, if you set the html report URL to be the same as the Dashboard URL when also using the dashboard, (example: `--dashboard --html=dashboard.html`), then the Dashboard will become an advanced html report when all the tests complete. + +๐Ÿ”ต Here's an example of an upgraded html report: + +```zsh +pytest test_suite.py --dashboard --html=report.html +``` + +Dashboard Pytest HTML Report + +If viewing pytest html reports in [Jenkins](https://www.jenkins.io/), you may need to [configure Jenkins settings](https://stackoverflow.com/a/46197356/7058266) for the html to render correctly. This is due to [Jenkins CSP changes](https://www.jenkins.io/doc/book/system-administration/security/configuring-content-security-policy/). + +You can also use `--junit-xml=report.xml` to get an xml report instead. Jenkins can use this file to display better reporting for your tests. + +```zsh +pytest test_suite.py --junit-xml=report.xml +``` + +-------- -The ``--report`` option gives you a fancy report after your test suite completes. +

      Nosetest Reports:

      -```bash +The `--report` option gives you a fancy report after your test suite completes. + +```zsh nosetests test_suite.py --report ``` - -(NOTE: You can add ``--show_report`` to immediately display Nosetest reports after the test suite completes. Only use ``--show_report`` when running tests locally because it pauses the test run.) +Example Nosetest Report -Here are some other useful **nosetest**-specific arguments: +(NOTE: You can add `--show_report` to immediately display Nosetest reports after the test suite completes. Only use `--show_report` when running tests locally because it pauses the test run.) -```bash ---logging-level=INFO # Hide DEBUG messages, which can be overwhelming. --x # Stop running the tests after the first failure is reached. --v # Prints the full test name rather than a dot for each test. ---with-id # If -v is also used, will number the tests for easy counting. +-------- + +

      Language Locale Codes

      + +You can specify a Language Locale Code to customize web pages on supported websites. With SeleniumBase, you can change the web browser's Locale on the command line by doing this: + +```zsh +pytest --locale=CODE # Example: --locale=ru ``` -#### **Using a Proxy Server:** +Visit ๐Ÿ—พ Locales for a full list of codes. + +-------- + +

      Changing the default driver version:

      + +๐Ÿ”ต By default, SeleniumBase will make sure that the major driver version matches the major browser version for Chromium tests. (Eg. If Chrome `149.X` is installed and you have chromedriver `149.X`, then nothing happens, but if you had chromedriver `148.X` instead, then SeleniumBase would download chromedriver `149.X` to match the browser version.) + +๐ŸŽ›๏ธ To change this default behavior, you can use: + +```zsh +pytest --driver-version=VER +``` -If you wish to use a proxy server for your browser tests (Chrome and Firefox only), you can add ``--proxy=IP_ADDRESS:PORT`` as an argument on the command line. +The `VER` in `--driver-version=VER` can be: +* A major driver version. Eg. `149`. (milestone) +* An exact driver version. Eg. `149.0.7827.155`. +* `"browser"` (exact match on browser version) +* `"keep"` (keep using the driver you already have) +* `"latest"` / `"stable"` (latest stable version) +* `"previous"` / `"latest-1"` (latest minus one) +* `"beta"` (latest beta version) +* `"dev"` (latest dev version) +* `"canary"` (latest canary version) +* `"mlatest"` (latest version for the milestone) -```bash +Note that different options could lead to the same result. (Eg. If you have the latest version of a browser for a milestone, then `"browser"` and `"mlatest"` should give you the same driver if the latest driver version for that milestone matches the browser version.) + +-------- + +

      Setting the binary location:

      + +๐Ÿ”ต By default, SeleniumBase uses the browser binary detected on the System PATH. + +๐ŸŽ›๏ธ To change this default behavior, you can use: + +```zsh +pytest --binary-location=PATH +``` + +The `PATH` in `--binary-location=PATH` / `--bl=PATH` can be: +* A relative or exact path to the browser binary. +* `"cft"` as a special option for `Chrome for Testing`. +* `"chs"` as a special option for `Chrome-Headless-Shell`. + +Before using the `"cft"` / `"chs"` options, call `sbase get cft` / `sbase get chs` in order to download the specified binaries into the `seleniumbase/drivers` folder. The default version is the latest stable version on https://googlechromelabs.github.io/chrome-for-testing/. You can change that by specifying the arg as a parameter. (Eg. `sbase get cft 131`, `sbase get chs 132`, etc.) + +With the `SB()` and `Driver()` formats, the binary location is set via the `binary_location` parameter. + +-------- + +๐ŸŽ›๏ธ To use the special `Chromium` binary: + +```zsh +sbase get chromium +``` + +Then, run scripts with `--chromium` / `use_chromium=True`: + +```zsh +pytest --chromium -n8 --dashboard --html=report.html -v --rs --headless +``` + +-------- + +๐ŸŽ›๏ธ To use the special `Chrome for Testing` binary: + +```zsh +sbase get cft +``` + +Then, run scripts with `--cft` / `cft=True`: + +```zsh +pytest --cft -n8 --dashboard --html=report.html -v --rs --headless +``` + +-------- + +(Note that `--chs` / `chs=True` activates `Chrome-Headless-Shell`) + +`Chrome-Headless-Shell` is the fastest version of Chrome, designed specifically for headless automation. (This mode is NOT compatible with UC Mode!) + +-------- + +

      Customizing default settings:

      + +๐ŸŽ›๏ธ An easy way to override [seleniumbase/config/settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py) is by using a custom settings file. +Here's the command-line option to add to tests: (See [examples/custom_settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/custom_settings.py)) + +```zsh +pytest --settings-file=custom_settings.py +``` + +(Settings include default timeout values, a two-factor auth key, DB credentials, S3 credentials, and other important settings used by tests.) + +-------- + +

      Running tests on a remote Selenium Grid:

      + +๐ŸŒ SeleniumBase lets you run tests on remote Selenium Grids such as [BrowserStack](https://www.browserstack.com/automate#)'s Selenium Grid, [Sauce Labs](https://saucelabs.com/products/platform-configurator)'s Selenium Grid, other Grids, and even your own Grid: + +๐ŸŒ For setting browser desired capabilities while running Selenium remotely, see the ReadMe located here: https://github.com/seleniumbase/SeleniumBase/tree/master/examples/capabilities + +Here's how to connect to a BrowserStack Selenium Grid server for running tests: + +```zsh +pytest test_demo_site.py --server=USERNAME:KEY@hub.browserstack.com --port=80 +``` + +Here's how to connect to a Sauce Labs Selenium Grid server for running tests: + +```zsh +pytest test_demo_site.py --server=USERNAME:KEY@ondemand.us-east-1.saucelabs.com --port=443 --protocol=https +``` + +๐ŸŒ Or you can create your own Selenium Grid for test distribution. ([See this ReadMe for details](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/utilities/selenium_grid/ReadMe.md)) + +๐ŸŒ To use a server on the `https` protocol, add `--protocol=https`: (*Now automatic if the port is 443.*) + +```zsh +pytest test_demo_site.py --protocol=https --server=IP_ADDRESS --port=PORT +``` + +-------- + +

      Using a Proxy Server:

      + +๐ŸŒ If you wish to use a proxy server for your browser tests (Chromium or Firefox), you can add `--proxy=IP_ADDRESS:PORT` as an argument on the command line. + +```zsh pytest proxy_test.py --proxy=IP_ADDRESS:PORT ``` -If the proxy server that you wish to use requires authentication, you can do the following (Chrome only): +๐ŸŒ If the proxy server that you wish to use requires authentication, you can do the following (Chromium only): -```bash +```zsh pytest proxy_test.py --proxy=USERNAME:PASSWORD@IP_ADDRESS:PORT ``` -To make things easier, you can add your frequently-used proxies to PROXY_LIST in [proxy_list.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/proxy_list.py), and then use ``--proxy=KEY_FROM_PROXY_LIST`` to use the IP_ADDRESS:PORT of that key. +๐ŸŒ SeleniumBase also supports SOCKS4 and SOCKS5 proxies: + +```zsh +pytest proxy_test.py --proxy="socks4://IP_ADDRESS:PORT" + +pytest proxy_test.py --proxy="socks5://IP_ADDRESS:PORT" +``` + +To make things easier, you can add your frequently-used proxies to PROXY_LIST in [proxy_list.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/proxy_list.py), and then use `--proxy=KEY_FROM_PROXY_LIST` to use the IP_ADDRESS:PORT of that key. -```bash +```zsh pytest proxy_test.py --proxy=proxy1 ``` -#### **Changing the User-Agent:** +-------- + +

      Changing the User-Agent:

      -If you wish to change the User-Agent for your browser tests (Chrome and Firefox only), you can add ``--agent="USER-AGENT STRING"`` as an argument on the command line. +๐Ÿ”ค If you wish to change the User-Agent for your browser tests (Chrome and Firefox only), you can add `--agent="USER-AGENT-STRING"` as an argument on the command line. -```bash +```zsh pytest user_agent_test.py --agent="Mozilla/5.0 (Nintendo 3DS; U; ; en) Version/1.7412.EU" ``` + +

      Mobile Device Testing:

      + +๐Ÿ“ฑ Use `--mobile` to quickly run your tests using Chrome's mobile device emulator with default values for device metrics (CSS Width, CSS Height, Pixel-Ratio) and a default value set for the user agent. To configure the mobile device metrics, use `--metrics="CSS_Width,CSS_Height,Pixel_Ratio"` to set those values. You'll also be able to set the user agent with `--agent="USER-AGENT-STRING"` (a default user agent will be used if not specified). To find real values for device metrics, [see this GitHub Gist](https://gist.github.com/sidferreira/3f5fad525e99b395d8bd882ee0fd9d00). For a list of available user agent strings, [check out this page](https://developers.whatismybrowser.com/useragents/explore/). + +```zsh +# Run tests using Chrome's mobile device emulator (default settings) +pytest test_swag_labs.py --mobile + +# Run mobile tests specifying CSS Width, CSS Height, and Pixel-Ratio +pytest test_swag_labs.py --mobile --metrics="412,732,3" + +# Run mobile tests specifying the user agent +pytest test_swag_labs.py --mobile --agent="Mozilla/5.0 (Linux; Android 9; Pixel 3 XL)" +``` + +-------- + + +

      02. SB() method options

      + +```python +test=None # Test Mode: Output, Logging, Continue on failure unless "rtf". +rtf=None # Shortcut / Duplicate of "raise_test_failure". +raise_test_failure=None # If "test" mode, raise Exception on 1st failure. +browser=None # Choose from "chrome", "edge", "firefox", or "safari". +headless=None # Use the default headless mode for Chromium and Firefox. +headless1=None # Use Chromium's old headless mode. (Fast, but limited) +headless2=None # Use Chromium's new headless mode. (Has more features) +locale_code=None # Set the Language Locale Code for the web browser. +protocol=None # The Selenium Grid protocol: "http" or "https". +servername=None # The Selenium Grid server/IP used for tests. +port=None # The Selenium Grid port used by the test server. +proxy=None # Use proxy. Format: "SERVER:PORT" or "USER:PASS@SERVER:PORT". +proxy_bypass_list=None # Skip proxy when using the listed domains. +proxy_pac_url=None # Use PAC file. (Format: URL or USERNAME:PASSWORD@URL) +multi_proxy=None # Allow multiple proxies with auth when multi-threaded. +agent=None # Modify the web browser's User-Agent string. +cap_file=None # The desired capabilities to use with a Selenium Grid. +cap_string=None # The desired capabilities to use with a Selenium Grid. +recorder_ext=None # Enables the SeleniumBase Recorder Chromium extension. +disable_cookies=None # Disable Cookies on websites. (Pages might break!) +disable_js=None # Disable JavaScript on websites. (Pages might break!) +disable_csp=None # Disable the Content Security Policy of websites. +enable_ws=None # Enable Web Security on Chromium-based browsers. +enable_sync=None # Enable "Chrome Sync" on websites. +use_auto_ext=None # Use Chrome's automation extension. +undetectable=None # Use undetected-chromedriver to evade bot-detection. +uc_cdp_events=None # Capture CDP events in undetected-chromedriver mode. +uc_subprocess=None # Use undetected-chromedriver as a subprocess. +log_cdp_events=None # Capture {"performance": "ALL", "browser": "ALL"} +incognito=None # Enable Chromium's Incognito mode. +guest_mode=None # Enable Chromium's Guest mode. +dark_mode=None # Enable Chromium's Dark mode. +devtools=None # Open Chromium's DevTools when the browser opens. +remote_debug=None # Enable Chrome's Debugger on "http://localhost:9222". +enable_3d_apis=None # Enable WebGL and 3D APIs. +swiftshader=None # Chrome: --use-gl=angle / --use-angle=swiftshader-webgl +ad_block_on=None # Block some types of display ads from loading. +host_resolver_rules=None # Set host-resolver-rules, comma-separated. +block_images=None # Block images from loading during tests. +do_not_track=None # Tell websites that you don't want to be tracked. +chromium_arg=None # "ARG=N,ARG2" (Set Chromium args, ","-separated.) +firefox_arg=None # "ARG=N,ARG2" (Set Firefox args, comma-separated.) +firefox_pref=None # SET (Set Firefox PREFERENCE:VALUE set, ","-separated) +user_data_dir=None # Set the Chrome user data directory to use. +extension_zip=None # Load a Chrome Extension .zip|.crx, comma-separated. +extension_dir=None # Load a Chrome Extension directory, comma-separated. +disable_features=None # "F1,F2" (Disable Chrome features, ","-separated.) +binary_location=None # Set path of the Chromium browser binary to use. +driver_version=None # Set the chromedriver or uc_driver version to use. +skip_js_waits=None # Skip JS Waits (readyState=="complete" and Angular). +wait_for_angularjs=None # Wait for AngularJS to load after some actions. +external_pdf=None # Set Chrome "plugins.always_open_pdf_externally":True. +window_position=None # Set the browser's starting window position: "X,Y" +window_size=None # Set the browser's starting window size: "Width,Height" +is_mobile=None # Use the mobile device emulator while running tests. +mobile=None # Shortcut / Duplicate of "is_mobile". +device_metrics=None # Set mobile metrics: "CSSWidth,CSSHeight,PixelRatio" +xvfb=None # Run tests using the Xvfb virtual display server on Linux OS. +xvfb_metrics=None # Set Xvfb display size on Linux: "Width,Height". +start_page=None # The starting URL for the web browser when tests begin. +rec_print=None # If Recorder is enabled, prints output after tests end. +rec_behave=None # Like Recorder Mode, but also generates behave-gherkin. +record_sleep=None # If Recorder enabled, also records self.sleep calls. +data=None # Extra test data. Access with "self.data" in tests. +var1=None # Extra test data. Access with "self.var1" in tests. +var2=None # Extra test data. Access with "self.var2" in tests. +var3=None # Extra test data. Access with "self.var3" in tests. +variables=None # DICT (Extra test data. Access with "self.variables") +account=None # Set account. Access with "self.account" in tests. +environment=None # Set the test env. Access with "self.env" in tests. +headed=None # Run tests in headed/GUI mode on Linux, where not default. +maximize=None # Start tests with the browser window maximized. +disable_ws=None # Reverse of "enable_ws". (None and False are different) +disable_beforeunload=None # Disable the "beforeunload" event on Chromium. +settings_file=None # A file for overriding default SeleniumBase settings. +position=None # Shortcut / Duplicate of "window_position". +size=None # Shortcut / Duplicate of "window_size". +uc=None # Shortcut / Duplicate of "undetectable". +undetected=None # Shortcut / Duplicate of "undetectable". +uc_cdp=None # Shortcut / Duplicate of "uc_cdp_events". +uc_sub=None # Shortcut / Duplicate of "uc_subprocess". +locale=None # Shortcut / Duplicate of "locale_code". +log_cdp=None # Shortcut / Duplicate of "log_cdp_events". +ad_block=None # Shortcut / Duplicate of "ad_block_on". +server=None # Shortcut / Duplicate of "servername". +guest=None # Shortcut / Duplicate of "guest_mode". +pls=None # Shortcut / Duplicate of "page_load_strategy". +sjw=None # Shortcut / Duplicate of "skip_js_waits". +wfa=None # Shortcut / Duplicate of "wait_for_angularjs". +cft=None # Use "Chrome for Testing" +chs=None # Use "Chrome-Headless-Shell" +use_chromium=None # Use base "Chromium" +save_screenshot=None # Save a screenshot at the end of each test. +no_screenshot=None # No screenshots saved unless tests directly ask it. +page_load_strategy=None # Set Chrome PLS to "normal", "eager", or "none". +timeout_multiplier=None # Multiplies the default timeout values. +js_checking_on=None # Check for JavaScript errors after page loads. +slow=None # Slow down the automation. Faster than using Demo Mode. +demo=None # Slow down and visually see test actions as they occur. +demo_sleep=None # SECONDS (Set wait time after Slow & Demo Mode actions.) +message_duration=None # SECONDS (The time length for Messenger alerts.) +highlights=None # Number of highlight animations for Demo Mode actions. +interval=None # SECONDS (Autoplay interval for SB Slides & Tour steps.) +time_limit=None # SECONDS (Safely fail tests that exceed the time limit.) +``` + +Example: [SeleniumBase/examples/raw_robot.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/raw_robot.py) + +-------- + + +

      03. Driver() method options

      + +```python +browser=None # Choose from "chrome", "edge", "firefox", or "safari". +headless=None # Use the default headless mode for Chromium and Firefox. +headless1=None # Use Chromium's old headless mode. (Fast, but limited) +headless2=None # Use Chromium's new headless mode. (Has more features) +headed=None # Run tests in headed/GUI mode on Linux, where not default. +locale_code=None # Set the Language Locale Code for the web browser. +protocol=None # The Selenium Grid protocol: "http" or "https". +servername=None # The Selenium Grid server/IP used for tests. +port=None # The Selenium Grid port used by the test server. +proxy=None # Use proxy. Format: "SERVER:PORT" or "USER:PASS@SERVER:PORT". +proxy_bypass_list=None # Skip proxy when using the listed domains. +proxy_pac_url=None # Use PAC file. (Format: URL or USERNAME:PASSWORD@URL) +multi_proxy=None # Allow multiple proxies with auth when multi-threaded. +agent=None # Modify the web browser's User-Agent string. +cap_file=None # The desired capabilities to use with a Selenium Grid. +cap_string=None # The desired capabilities to use with a Selenium Grid. +recorder_ext=None # Enables the SeleniumBase Recorder Chromium extension. +disable_cookies=None # Disable Cookies on websites. (Pages might break!) +disable_js=None # Disable JavaScript on websites. (Pages might break!) +disable_csp=None # Disable the Content Security Policy of websites. +enable_ws=None # Enable Web Security on Chromium-based browsers. +disable_ws=None # Reverse of "enable_ws". (None and False are different) +enable_sync=None # Enable "Chrome Sync" on websites. +use_auto_ext=None # Use Chrome's automation extension. +undetectable=None # Use undetected-chromedriver to evade bot-detection. +uc_cdp_events=None # Capture CDP events in undetected-chromedriver mode. +uc_subprocess=None # Use undetected-chromedriver as a subprocess. +log_cdp_events=None # Capture {"performance": "ALL", "browser": "ALL"} +no_sandbox=None # (DEPRECATED) - "--no-sandbox" is always used now. +disable_gpu=None # (DEPRECATED) - GPU is disabled if not "swiftshader". +incognito=None # Enable Chromium's Incognito mode. +guest_mode=None # Enable Chromium's Guest mode. +dark_mode=None # Enable Chromium's Dark mode. +devtools=None # Open Chromium's DevTools when the browser opens. +remote_debug=None # Enable Chrome's Debugger on "http://localhost:9222". +enable_3d_apis=None # Enable WebGL and 3D APIs. +swiftshader=None # Chrome: --use-gl=angle / --use-angle=swiftshader-webgl +ad_block_on=None # Block some types of display ads from loading. +host_resolver_rules=None # Set host-resolver-rules, comma-separated. +block_images=None # Block images from loading during tests. +do_not_track=None # Tell websites that you don't want to be tracked. +chromium_arg=None # "ARG=N,ARG2" (Set Chromium args, ","-separated.) +firefox_arg=None # "ARG=N,ARG2" (Set Firefox args, comma-separated.) +firefox_pref=None # SET (Set Firefox PREFERENCE:VALUE set, ","-separated) +user_data_dir=None # Set the Chrome user data directory to use. +extension_zip=None # Load a Chrome Extension .zip|.crx, comma-separated. +extension_dir=None # Load a Chrome Extension directory, comma-separated. +disable_features=None # "F1,F2" (Disable Chrome features, ","-separated.) +binary_location=None # Set path of the Chromium browser binary to use. +driver_version=None # Set the chromedriver or uc_driver version to use. +page_load_strategy=None # Set Chrome PLS to "normal", "eager", or "none". +external_pdf=None # Set Chrome "plugins.always_open_pdf_externally":True. +window_position=None # Set the browser's starting window position: "X,Y" +window_size=None # Set the browser's starting window size: "Width,Height" +is_mobile=None # Use the mobile device emulator while running tests. +mobile=None # Shortcut / Duplicate of "is_mobile". +d_width=None # Set device width +d_height=None # Set device height +d_p_r=None # Set device pixel ratio +position=None # Shortcut / Duplicate of "window_position". +size=None # Shortcut / Duplicate of "window_size". +uc=None # Shortcut / Duplicate of "undetectable". +undetected=None # Shortcut / Duplicate of "undetectable". +uc_cdp=None # Shortcut / Duplicate of "uc_cdp_events". +uc_sub=None # Shortcut / Duplicate of "uc_subprocess". +locale=None # Shortcut / Duplicate of "locale_code". +log_cdp=None # Shortcut / Duplicate of "log_cdp_events". +ad_block=None # Shortcut / Duplicate of "ad_block_on". +server=None # Shortcut / Duplicate of "servername". +guest=None # Shortcut / Duplicate of "guest_mode". +pls=None # Shortcut / Duplicate of "page_load_strategy". +use_chromium=None # Use base "Chromium" +cft=None # Use "Chrome for Testing" +chs=None # Use "Chrome-Headless-Shell" +``` + +Example: [SeleniumBase/examples/raw_driver_manager.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/raw_driver_manager.py) + +-------- + + +

      04. sb_cdp.Chrome() method options

      + +```python +url: Optional[str] = None +user_data_dir: Optional[PathLike] = None +headless: Optional[bool] = False +incognito: Optional[bool] = False +guest: Optional[bool] = False +browser_executable_path: Optional[PathLike] = None +browser_args: Optional[List[str]] = None +xvfb_metrics: Optional[List[str]] = None # "Width,Height" for Linux +ad_block: Optional[bool] = False +sandbox: Optional[bool] = True +lang: Optional[str] = None # Set the Language Locale Code +host: Optional[str] = None # Chrome remote-debugging-host +port: Optional[int] = None # Chrome remote-debugging-port +xvfb: Optional[int] = None # Use a special virtual display on Linux +headed: Optional[bool] = None # Override default Xvfb mode on Linux +expert: Optional[bool] = None # Open up closed Shadow-root elements +agent: Optional[str] = None # Set the user-agent string +proxy: Optional[str] = None # "host:port" or "user:pass@host:port" +tzone: Optional[str] = None # Eg "America/New_York", "Asia/Kolkata" +geoloc: Optional[list | tuple] = None # Eg (48.87645, 2.26340) +extension_dir: Optional[str] = None # Chrome extension directory +platform: Optional[str] = None # Set the Platform. Eg: "MacIntel" +``` + +Example: [SeleniumBase/examples/cdp_mode/raw_geolocation.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/raw_geolocation.py) + +-------- + + +

      05. activate_cdp_mode() method options

      + +```python +url: Optional[str] = None # The URL to navigate to +lang: Optional[str] = None # Set the Language Locale Code +agent: Optional[str] = None # Set the user-agent string +tzone: Optional[str] = None # Eg "America/New_York", "Asia/Kolkata" +geoloc: Optional[list | tuple] = None # Eg (48.87645, 2.26340) +platform: Optional[str] = None # Set the Platform. Eg: "MacIntel" +``` + +Example: [SeleniumBase/examples/cdp_mode/raw_geolocation_sb.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/raw_geolocation_sb.py) + +Note that if CDP Mode is already active, the options above can also be used when calling `sb.cdp.goto()`. (The `url` arg is required in this case.) + +-------- + +[](https://github.com/seleniumbase/SeleniumBase) diff --git a/help_docs/demo_mode.md b/help_docs/demo_mode.md new file mode 100644 index 00000000000..cdd9c913ede --- /dev/null +++ b/help_docs/demo_mode.md @@ -0,0 +1,56 @@ + + +

      Demo Mode ๐ŸŽฆ

      + +

      SeleniumBase Example

      + +

      ๐Ÿ”ต Demo Mode helps you see what a test is doing.

      + +

      ๐Ÿ‡๐Ÿ’จ ๐Ÿ‘€ If a test runs too fast for your eyes, use Demo Mode to slow it down, highlight actions, and display assertions. Example usage:

      + +```zsh +cd examples/ +pytest test_coffee_cart.py --demo +``` + +

      SeleniumBase Coffee Cart Test

      + +>

      (--demo mode slows down tests and highlights actions)

      + +-------- + +Another example: + +```zsh +pytest my_first_test.py --demo +``` + +-------- + +

      Here's how to run test_swag_labs.py from examples/ in Demo Mode:

      + + +```zsh +pytest test_swag_labs.py --demo +``` + +

      SeleniumBase Example

      + +-------- + +

      Here's an example that only uses the highlight() method for highlighting browser actions:

      + + +

      (test_error_page.py from examples/)

      + +```zsh +pytest test_error_page.py +``` + +

      SeleniumBase Example

      + +-------- + +Here's an example of a mobile test in Demo Mode: + +

      SeleniumBase Example

      diff --git a/help_docs/desired_capabilities.md b/help_docs/desired_capabilities.md new file mode 100644 index 00000000000..5fa7447a4bf --- /dev/null +++ b/help_docs/desired_capabilities.md @@ -0,0 +1,118 @@ + + +

      Using Desired Capabilities

      + +You can specify browser capabilities when running SeleniumBase tests on a remote Selenium Grid server (such as BrowserStack or Sauce Labs). + +Sample run commands may look like this when run from the [SeleniumBase/examples/](https://github.com/seleniumbase/SeleniumBase/tree/master/examples) folder: (The browser is now specified in the capabilities file.) + +```zsh +pytest test_demo_site.py --browser=remote --server=USERNAME:KEY@hub.browserstack.com --port=80 --cap_file=capabilities/sample_cap_file_BS.py +``` + +```zsh +pytest test_demo_site.py --browser=remote --server=USERNAME:KEY@ondemand.us-east-1.saucelabs.com --port=443 --protocol=https --cap_file=capabilities/sample_cap_file_SL.py +``` + +(Parameters: ``--browser=remote``, ``--server=SERVER``, ``--port=PORT``, and ``--cap_file=CAP_FILE.py``) + +Here's an example desired capabilities file for BrowserStack using the newer SDK format in a `.yml` / `.yaml` file: + +```yml +platforms: + - browserName: safari + osVersion: 17 + deviceName: iPhone 15 Pro Max +buildIdentifier: ${BUILD_NUMBER} +parallelsPerPlatform: 1 +projectName: My Project +browserstackLocal: true +debug: true +networkLogs: true +``` + +Here's an example desired capabilities file for BrowserStack using the legacy JSONWP format in a `.py` file: + +```python +desired_cap = { + "browser": "Chrome", + "os": "Windows", + "os_version": "11", + "browser_version": "latest", + "browserstack.console": "info", + "browserstack.debug": "true", + "browserstack.networkLogs": "true", + "browserstack.local": "true", +} +``` + +Here's an example desired capabilities file for Sauce Labs: + +```python +capabilities = { + "browserName": "chrome", + "browserVersion": "latest", + "platformName": "macOS 10.14", + "sauce:options": {}, +} +``` + +(Note that the browser is now being specified in the capabilities file, rather than with ``--BROWSER`` when using a **remote** Selenium Grid. If using a **local** Selenium Grid, specify the browser, eg: ``--firefox``.) + +
      You can generate specific desired capabilities using:
      + + + +
      Parsing desired capabilities:
      + +SeleniumBase has a desired capabilities parser that can capture all lines from the specified file in the following formats: + +```python +'KEY': 'VALUE' +'KEY': True +'KEY': False +caps['KEY'] = "VALUE" +caps['KEY'] = True +caps['KEY'] = False +``` + +(Each pair must be on a separate line. You can interchange single and double quotes.) + +You can also swap ``--browser=remote`` with an actual browser, eg ``--browser=chrome``, which will combine the default SeleniumBase desired capabilities with those that were specified in the capabilities file when using ``--cap_file=FILE.py``. Capabilities will override other parameters, so if you set the browser to one thing and the capabilities browser to another, SeleniumBase will use the capabilities browser. + +You'll need default SeleniumBase capabilities for: +* Using a proxy server (not the same as a Selenium Grid server) +* Downloading files to a desired folder +* Disabling some warnings on Chrome +* Overriding a website's Content Security Policy on Chrome +* Other possible reasons + +You can also set browser desired capabilities from a command-line string. Eg: + +```zsh +pytest test_swag_labs.py --cap-string='{"browserName":"chrome","name":"test1"}' --server="127.0.0.1" --browser=remote +``` + +(Enclose cap-string in single quotes. Enclose parameter keys in double quotes.) + +If you pass ``"*"`` into the ``"name"`` field of ``--cap-string``, the name will become the test identifier. Eg: + +```zsh +pytest my_first_test.py --cap-string='{"browserName":"chrome","name":"*"}' --server="127.0.0.1" --browser=chrome +``` + +Example name: ``"my_first_test.MyTestClass.test_basics"`` + +

      Using a local Selenium Grid

      + +If using a local Selenium Grid with SeleniumBase, start up the Grid Hub and nodes first: + +```zsh +sbase grid-hub start +sbase grid-node start +``` + +(The Selenium Server JAR file will be automatically downloaded for first-time Grid users. You'll also need Java installed to start up the Grid.) diff --git a/help_docs/features_list.md b/help_docs/features_list.md old mode 100755 new mode 100644 index 15e8c196932..dd8106376a3 --- a/help_docs/features_list.md +++ b/help_docs/features_list.md @@ -1,23 +1,48 @@ + + -## **SeleniumBase Features:** -* A complete test automation framework for building & running reliable testing scripts. -* Uses [Pytest](https://docs.pytest.org/en/latest/) and [Nose](http://nose.readthedocs.io/en/latest/) runners for test discovery, organization, execution, and logging. -* Includes [console scripts](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/console_scripts/ReadMe.md) that save you time by installing web drivers automatically, etc. -* Includes a [website tour builder](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/tour_examples/ReadMe.md) for creating and running walkthroughs on any website. -* Works on multiple platforms such as macOS, Windows, Linux, and [Docker](https://github.com/seleniumbase/SeleniumBase/blob/master/integrations/docker/ReadMe.md). -* Uses a [flexible command-line interface](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/customizing_test_runs.md) to customize & configure test runs. -* Can run tests using multiple concurrent threads. (Use ``-n THREAD_COUNT``) -* Has Python libraries for helping you do much more with Selenium/WebDriver. -* Has [Plugins](https://github.com/seleniumbase/SeleniumBase/tree/master/seleniumbase/plugins) for logging data and screenshots automatically. ([Click to learn more](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/example_logs/ReadMe.md)) -* Uses a [global config file](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py) for configuring SeleniumBase to your specific needs. -* Backwards-compatible with [WebDriver](http://www.seleniumhq.org/projects/webdriver/). (Use ``self.driver`` anywhere.) -* Can run tests through a proxy server. (Use ``--proxy=IP_ADDRESS:PORT``) -* Can use an authenticated proxy server. (``--proxy=USERNAME:PASSWORD@IP_ADDRESS:PORT``) -* Can change the web browser's user agent string. (Use ``--agent=USER_AGENT_STRING``) -* Can handle Google Authenticator logins by using the [Python one-time password library](https://pyotp.readthedocs.io/en/latest/). -* Includes a hybrid-automation solution called **[MasterQA](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/masterqa/ReadMe.md)** to speed up manual testing. -* Integrates with [MySQL](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/core/testcase_manager.py), [Selenium Grid](https://github.com/seleniumbase/SeleniumBase/tree/master/seleniumbase/utilities/selenium_grid), [Azure](https://github.com/seleniumbase/SeleniumBase/blob/master/integrations/azure/jenkins/ReadMe.md), [Google Cloud](https://github.com/seleniumbase/SeleniumBase/tree/master/integrations/google_cloud/ReadMe.md), [Amazon S3](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/plugins/s3_logging_plugin.py), and [Docker](https://github.com/seleniumbase/SeleniumBase/blob/master/integrations/docker/ReadMe.md). -* Has the ability to connect to a [BrowserStack](https://www.browserstack.com/automate#), [Sauce Labs](https://saucelabs.com/products/web-testing/cross-browser-testing), or [TestingBot](https://testingbot.com/features) Selenium Grid. -* Includes a [tool to convert Selenium IDE recordings](https://github.com/seleniumbase/SeleniumBase/tree/master/seleniumbase/utilities/selenium_ide) into clean, robust SeleniumBase scripts. -* Written in Python, but can also make JavaScript calls using ``self.execute_script()``. -* Includes useful Python decorators and password obfuscation methods. ([Learn more here](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/common/ReadMe.md)) + +

      SeleniumBase Features: ๐Ÿฐ

      + +* A powerful Python framework for browser automation and E2E UI testing. +* Has [CDP Mode](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/ReadMe.md) for bypassing bot-detection. (`activate_cdp_mode(url)`) +* [Stealthy Playwright Mode](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/playwright/ReadMe.md) lets you bypass bot-detection with Playwright. +* There's [Recorder Mode](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/recorder_mode.md) for instantly generating browser tests in Python. +* Supports [pytest](https://docs.pytest.org/en/latest/), [unittest](https://docs.python.org/3/library/unittest.html), [nose](http://nose.readthedocs.io/en/latest/), and [behave](https://behave.readthedocs.io/en/stable/index.html) for finding & running tests. +* Automatic smart-waiting improves reliability and prevents flaky tests. +* All the code is open source. Look inside to learn about any feature. +* Powerful logging tools for [dashboards, reports, and screenshots](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/example_logs/ReadMe.md). +* Can run tests in Headless Mode to hide the browser. (`--headless`) +* Can run tests multithreaded from parallel browsers. (`-n NUM_THREADS`) +* Can run tests from a shared browser session. (`--reuse-session`/`--rs`) +* Can run tests using [Chromium's mobile device emulator](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/mobile_testing.md). (`--mobile`) +* Can run tests through a proxy server. (`--proxy=IP_ADDRESS:PORT`) +* Can run tests with proxy settings via PAC URL. (`--proxy-pac-url=URL.pac`) +* Can run tests through an authenticated proxy server. (`--proxy=USER:PASS@HOST:PORT`) +* Can run tests with proxy+auth via PAC URL. (`--proxy-pac-url=USER:PASS@URL.pac`) +* Can run tests with a customized browser user agent. (`--agent=USER_AGENT_STRING`) +* Can set a Chromium User Data Directory to load. (`--user-data-dir=DIR`) +* Can load Chromium Extension folders. (`--extension-dir=DIR`) +* Powerful [console scripts](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/console_scripts/ReadMe.md). (Type **`seleniumbase`** or **`sbase`** to use.) +* Has the ability to translate tests into [multiple spoken languages](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/translations). +* Has a flexible [command-line interface](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/customizing_test_runs.md) for customizing test runs. +* Has a [global config file](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py) for configuring settings as needed. +* Includes a tool for [creating interactive web presentations](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/presenter/ReadMe.md). +* Includes [Chart Maker](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/chart_maker/ReadMe.md), a tool for creating interactive charts. +* Includes a [dialog box builder](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/dialog_boxes/ReadMe.md) to allow user-input during automation. +* Includes a [website tour builder](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/tour_examples/ReadMe.md) for creating interactive walkthroughs. +* Includes a GUI for running pytest scripts: [SeleniumBase Commander](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/commander.md). +* Includes integrations for [GitHub Actions](https://seleniumbase.io/integrations/github/workflows/ReadMe/), [Google Cloud](https://github.com/seleniumbase/SeleniumBase/tree/master/integrations/google_cloud/ReadMe.md), [Azure](https://github.com/seleniumbase/SeleniumBase/blob/master/integrations/azure/jenkins/ReadMe.md), [S3](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/plugins/s3_logging_plugin.py), and [Docker](https://github.com/seleniumbase/SeleniumBase/blob/master/integrations/docker/ReadMe.md). +* Can handle Google Authenticator logins with [Python's one-time password library](https://pyotp.readthedocs.io/en/latest/). +* Can load and make assertions on PDF files from websites or the local file system. +* Can inspect HTML to find issues and points of interest with the [HTML Inspector](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/html_inspector.md). +* Is backwards-compatible with Python [WebDriver](https://www.selenium.dev/projects/) methods. (Use: `self.driver`) +* Can execute JavaScript code from Python calls. (Use: `self.execute_script()`) +* Can pierce through [Shadow DOM selectors](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/shadow_dom.md). (Add `::shadow` to CSS fragments.) +* Includes a hybrid-automation solution, [MasterQA](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/masterqa/ReadMe.md), to speed up manual testing. +* Includes [Test Case Management Software](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/case_plans.md) with Markdown technology. +* Includes useful [Python decorators and password obfuscation methods](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/common/ReadMe.md). + +-------- + + diff --git a/help_docs/handling_iframes.md b/help_docs/handling_iframes.md new file mode 100644 index 00000000000..b8996d0479b --- /dev/null +++ b/help_docs/handling_iframes.md @@ -0,0 +1,63 @@ + + +

      How to handle iframes

      + +๐Ÿ–ผ๏ธ iframes follow the same principle as new windows: You must first switch to the iframe if you want to perform actions in there: + +```python +self.switch_to_frame("iframe") +# ... Now perform actions inside the iframe +self.switch_to_parent_frame() # Exit the current iframe +``` + +To exit from multiple iframes, use ``self.switch_to_default_content()``. (If inside a single iframe, this has the same effect as ``self.switch_to_parent_frame()``.) + +```python +self.switch_to_frame('iframe[name="frame1"]') +self.switch_to_frame('iframe[name="frame2"]') +# ... Now perform actions inside the inner iframe +self.switch_to_default_content() # Back to the main page +``` + +๐Ÿ–ผ๏ธ You can also use a context manager to act inside iframes: + +```python +with self.frame_switch("iframe"): + # ... Now perform actions while inside the code block +# You have left the iframe +``` + +This also works with nested iframes: + +```python +with self.frame_switch('iframe[name="frame1"]'): + with self.frame_switch('iframe[name="frame2"]'): + # ... Now perform actions while inside the code block + # You are now back inside the first iframe +# You have left all the iframes +``` + +๐Ÿ–ผ๏ธ In special cases, you may want to set the page to the content of an iframe: + +```python +self.set_content_to_frame("iframe") +``` + +To back out of one call of that, use: + +```python +self.set_content_to_parent() +``` + +To back out of all nested calls of that, use: + +```python +self.set_content_to_default() +``` + +๐Ÿ–ผ๏ธ See [SeleniumBase/examples/iframe_tests.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/iframe_tests.py) for tests that use all available iframe commands. + +-------- + +

      diff --git a/help_docs/happy_customers.md b/help_docs/happy_customers.md old mode 100755 new mode 100644 index bdf771f5c05..adc8eb2acce --- a/help_docs/happy_customers.md +++ b/help_docs/happy_customers.md @@ -1,21 +1,137 @@ -#### Some organizations that've grown their automation with SeleniumBase include: -* [HubSpot](http://www.hubspot.com/) -* [Veracode](http://www.veracode.com/) -* [CA Technologies](https://www.ca.com/) -* [Broadcom](https://www.broadcom.com/) + + +

      SeleniumBase

      + +### Businesses who have used SeleniumBase: + +* [MIT](https://web.mit.edu/) +* [Intel](https://www.intel.com/) +* [Sony](https://www.sony.com/) +* [Tesla](https://www.tesla.com/) * [iboss](https://www.iboss.com/) -* [MIT](http://web.mit.edu/) +* [Apple](https://www.apple.com/) +* [Oracle](https://www.oracle.com/) * [Akamai](https://www.akamai.com/) -* [VMware](http://www.vmware.com/) -* [Jana](http://jana.com/) -* [Raid The Room](http://raidtheroom.com/) -* [Harvard Medical School](http://hms.harvard.edu/) +* [Allstate](https://www.allstate.com/) +* [Amazon](https://www.amazon.com/) +* [Sequoia](https://www.sequoia.com/) +* [HubSpot](https://www.hubspot.com/) +* [Streamlit](https://streamlit.io/) +* [Autodesk](https://www.autodesk.com/) +* [Veracode](https://www.veracode.com/) +* [Microsoft](https://www.microsoft.com/) +* [Medtronic](https://www.medtronic.com/) +* [Broadcom](https://www.broadcom.com/) +* [Snowflake](https://www.snowflake.com/) +* [PlayStation](https://www.playstation.com/) +* [Bright Data](https://brightdata.com/) +* [GlobalLogic](https://www.globallogic.com/) +* [Thoma Bravo](https://www.thomabravo.com/) +* [ASICS Digital](https://www.asicsdigital.com/) +* [Expedia Group](https://lifeatexpediagroup.com/) +* [Deutsche Bank](https://www.db.com/) +* [The Very Group](https://www.theverygroup.com/) +* [ActiveCampaign](https://www.activecampaign.com/) +* [Blue Trail Software](https://www.bluetrail.software/) +* [Ness Technologies](https://www.ness-tech.co.il/) +* [Johnson & Johnson](https://www.jnj.com/) +* [AutomationAnywhere](https://www.automationanywhere.com/) +* [Harvard Medical School](https://hms.harvard.edu/) +* [Singapore Institute of Tech](https://www.singaporetech.edu.sg) +* [Mississippi State University](https://www.msstate.edu/) +* [The New York Public Libary](https://www.nypl.org/) +* [CCC Intelligent Solutions](https://www.cccis.com/) +* [Medical Home Network](https://www.medicalhomenetwork.org/) +* [Boehringer Ingelheim](https://www.boehringer-ingelheim.com/) +* [Queen's University](https://www.queensu.ca/) +* [L1NNA Laboratory](https://l1nna.com/) +* [Canfield Scientific](https://www.canfieldsci.com/) +* [Thomson Reuters](https://www.thomsonreuters.com/) +* [Wellesley College](https://www.wellesley.edu/) +* [CA Technologies](https://tinyurl.com/ca-technologies) +* [Momin Solutions](https://www.mominsolutions.com/) +* [Optum Financial](https://www.optum.com/financial-services.html) +* [ActiveCampaign](https://www.activecampaign.com/) +* [Sphere Partners](https://www.sphereinc.com/) +* [MadeiraMadeira](https://www.madeiramadeira.com.br/) +* [Raid The Room](https://raidtheroom.com/) +* [First American](https://www.firstam.com/) +* [Origin Energy](https://www.originenergy.com.au/) +* [InterSystems](https://www.intersystems.com/) +* [Develop Soft](https://www.developsoft.com/) +* [AstraZeneca](https://www.astrazeneca.com/) +* [Ping Identity](https://www.pingidentity.com/) +* [Scale Media](https://scalemedia.com/) +* [CaptivateIQ](https://www.captivateiq.com/) +* [StreamSets](https://streamsets.com/) +* [Visual Data](https://www.visualdatamedia.com/) +* [Betterteem](https://www.betterteem.com/) +* [SenseTime](https://www.sensetime.com/en) +* [Rubiscape](https://www.rubiscape.com/) +* [Aeturnum](https://aeturnum.com/) +* [Cellebrite](https://www.cellebrite.com/en/home/) +* [RaySecur](https://www.raysecur.com/) +* [Payability](https://www.payability.com/) +* [Ben Fatto](https://benfatto.net.br/en/) +* [BW Legal](https://www.bwlegal.co.uk/) +* [GeoNode](http://geonode.org/) +* [Empower](https://empower.me/) +* [CardFree](https://cardfree.com/) +* [Softrams](https://www.softrams.com/) +* [Invensity](https://www.invensity.com/) +* [SunCorp](https://www.suncorpgroup.com.au/) +* [Logitech](https://www.logitech.com/) +* [Modulos](https://www.modulos.ai/) +* [Igrowker](https://igrowker.com/) +* [axxessio](https://www.axxessio.com/en/) +* [StorkJet](https://storkjet.com/) +* [Networx](https://www.networx.com/) +* [Elevatus](https://www.elevatus.io/) +* [VMware](https://www.vmware.com/) +* [Rakuten](https://global.rakuten.com/corp/about/) +* [Nagarro](https://www.nagarro.com/en) +* [Ark PES](https://www.arkpes.com/) +* [SkillsVR](https://skillsvr.com/) +* [Evereve](https://evereve.com/) +* [Planeks](https://www.planeks.net/) +* [Upwork](https://www.upwork.com/) +* [Yandex](https://yandex.ru/) +* [Picsart](https://picsart.com/) +* [Serquo](https://serquo.com/) +* [WPILib](https://wpilib.org/) +* [QBurst](https://www.qburst.com/) +* [Kinetik](https://kinetik.care/) +* [Exadel](https://exadel.com/) +* [netLex](https://netlex.io/en/) +* [XP Inc](https://www.xpinc.com/) +* [Roche](https://www.roche.com/) +* [Alokin](https://alokin.in/) +* [Stride](https://stride.ai/) +* [Cubic](https://www.cubic.com/) +* [Baidu](https://www.baidu.com/) +* [Intive](https://intive.com/) +* [Seek](https://www.seek.com.au/) +* [Vultr](https://www.vultr.com/) +* [HqO](https://www.hqo.co/) +* [PDS](https://www.pdsinc.com/) +* [Pico](https://trypico.com/) +* [Iver](https://www.iver.com/) * And more... -**Case Study** (*HubSpot*): +-------- + +### Case Study: + +> **HubSpot**: + +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 their old CMS to their new one, which saved them over one million USD and a significant amount of time. + +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) + +For more reading about automation at HubSpot, see: [The Classic "QA Team" is Obsolete](https://product.hubspot.com/blog/the-classic-qa-team-is-obsolete#hs_cos_wrapper_name) -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. +-------- -Learn how HubSpot uses SeleniumBase for website testing by reading: [Automated Testing with Selenium](http://dev.hubspot.com/blog/bid/88880/Automated-Integration-Testing-with-Selenium-at-HubSpot#hs_cos_wrapper_name) +### How is this list generated? -For more reading about automation at HubSpot, see: [The Classic "QA Team" is Obsolete](http://product.hubspot.com/blog/the-classic-qa-team-is-obsolete#hs_cos_wrapper_name) +Most of these rows come from [LinkedIn search results](https://www.linkedin.com/search/results/people/?keywords=seleniumbase&origin=SWITCH_SEARCH_VERTICAL) of profile descriptions, where employees mentioned that they have used SeleniumBase at their company. These details may also be found in other public networks and social media sites, such as GitHub (where organizations could have public repos that use SeleniumBase). diff --git a/help_docs/hidden_files_info.md b/help_docs/hidden_files_info.md old mode 100755 new mode 100644 index addf7f8e333..b0b72d24c61 --- a/help_docs/hidden_files_info.md +++ b/help_docs/hidden_files_info.md @@ -1,8 +1,22 @@ -### Info about hidden files on macOS + -Depending on your macOS settings, some files may be hidden from view in your Finder window, such as ``.gitignore``. To view all files, run the following command and then reopen the Finder window: -```bash +## Showing hidden files on macOS + +Depending on your macOS settings, some files may be hidden from view in your Finder window, such as `.gitignore`. + +* On newer versions of macOS, use the following in a Finder window to view hidden files: + +Press the **โ€œCommandโ€ + โ€œShiftโ€ + โ€œ.โ€ (period)** keys at the same time. + +(The hidden files will show up as translucent in the folder. If you want to obscure the files again, press the same โ€œCommandโ€ + โ€œShiftโ€ + โ€œ.โ€ (period) combination.) + +* On older versions of macOS, use the following command in a Terminal window to view hidden files, and then reopen the Finder window: + +```zsh defaults write com.apple.finder AppleShowAllFiles -bool true ``` -More info on that **[here](https://www.defaults-write.com/show-hidden-files-in-os-x-finder/)**. +More info on that can be found here: diff --git a/help_docs/how_it_works.md b/help_docs/how_it_works.md old mode 100755 new mode 100644 index a811ff3760e..4c0fff154e0 --- a/help_docs/how_it_works.md +++ b/help_docs/how_it_works.md @@ -1,16 +1,124 @@ + + +

      How the SeleniumBase Test Framework Works ๐Ÿ‘๏ธ

      + -### ![http://seleniumbase.com](https://cdn2.hubspot.net/hubfs/100006/images/super_logo_tiny.png "SeleniumBase") **How SeleniumBase Works:** -At the core, SeleniumBase works by extending [Pytest](https://docs.pytest.org/en/latest/) and [Nosetests](http://nose.readthedocs.io/en/latest/) as a direct plugin to each one. SeleniumBase automatically spins up web browsers for tests, and then gives those tests access to the SeleniumBase libraries through the BaseCase class, [found here](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/fixtures/base_case.py). Now you can use "``--browser=chrome``" to specify the web browser to use (Default = "Chrome"). You can also include additional plugins for additional features such as "``--demo_mode``" (for highlighting elements & changing the speed of test runs) and "``--proxy=IP_ADDRESS:PORT``" (for using a proxy server during test runs). There are also other plugins available such as "``--with-db_reporting``", "``--with-s3_logging``", and more. +๐Ÿ‘๏ธ๐Ÿ”Ž The primary [SeleniumBase syntax format](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/syntax_formats.md) works by extending [pytest](https://docs.pytest.org/en/latest/) as a direct plugin. SeleniumBase automatically spins up web browsers for tests, and then gives those tests access to the SeleniumBase libraries through the [BaseCase class](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/fixtures/base_case.py). Tests are also given access to [SeleniumBase command-line options](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/customizing_test_runs.md) and [SeleniumBase methods](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/method_summary.md). + +๐Ÿ‘๏ธ๐Ÿ”Ž `pytest` uses a feature called test discovery to automatically find and run Python methods that start with `test_` when those methods are located in Python files that start with `test_` or end with `_test.py`/`_tests.py`. -(NOTE: Pytest and Nosetests work by automatically running any Python method that starts with "``test``" from the file that you specified on the command line. You can also run all tests from a specific class in a file, or even pick out an individual test to run.) +๐Ÿ‘๏ธ๐Ÿ”Ž The primary [SeleniumBase syntax format](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/syntax_formats.md) starts by importing `BaseCase`: -To use SeleniumBase calls you need the following: ```python from seleniumbase import BaseCase ``` -And then have your test classes inherit BaseCase: + +๐Ÿ‘๏ธ๐Ÿ”Ž This next line activates `pytest` when a file is called directly with `python` by accident: + +```python +BaseCase.main(__name__, __file__) +``` + +๐Ÿ‘๏ธ๐Ÿ”Ž Classes can inherit `BaseCase` to gain SeleniumBase functionality: + +```python +class MyTestClass(BaseCase): +``` + +๐Ÿ‘๏ธ๐Ÿ”Ž Test methods inside `BaseCase` classes become SeleniumBase tests: (These tests automatically launch a web browser before starting, and quit the web browser after ending. Default settings can be changed via command-line options.) + +```python +class MyTestClass(BaseCase): + def test_abc(self): + # ... +``` + +๐Ÿ‘๏ธ๐Ÿ”Ž [SeleniumBase APIs](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/method_summary.md) can be called from tests via `self`: + ```python class MyTestClass(BaseCase): + def test_abc(self): + self.goto("https://example.com") +``` + +๐Ÿ‘๏ธ๐Ÿ”Ž Here's what a full test might look like: + +```python +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + +class TestSimpleLogin(BaseCase): + def test_simple_login(self): + self.goto("seleniumbase.io/simple/login") + self.type("#username", "demo_user") + self.type("#password", "secret_pass") + self.click('a:contains("Sign in")') + self.assert_exact_text("Welcome!", "h1") + self.assert_element("img#image1") + self.highlight("#image1") + self.click_link("Sign out") + self.assert_text("signed out", "#top_message") +``` + +(See the example, [test_simple_login.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_simple_login.py), for reference.) + +๐Ÿ‘๏ธ๐Ÿ”Ž Here are some examples of running tests with `pytest`: + +```zsh +pytest test_mfa_login.py +pytest --headless -n8 --dashboard --html=report.html -v --rs --crumbs +pytest -m marker2 +pytest -k agent +pytest offline_examples/ +``` + +๐Ÿ‘๏ธ๐Ÿ”Ž Here's a [SeleniumBase syntax format](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/syntax_formats.md) that uses the raw `driver`. Unlike the format mentioned earlier, it can be run with `python` instead of `pytest`. The `driver` includes original `driver` methods and new ones added by SeleniumBase: + +```python +from seleniumbase import Driver + +driver = Driver() +try: + driver.goto("seleniumbase.io/simple/login") + driver.type("#username", "demo_user") + driver.type("#password", "secret_pass") + driver.click('a:contains("Sign in")') + driver.assert_exact_text("Welcome!", "h1") + driver.assert_element("img#image1") + driver.highlight("#image1") + driver.click_link("Sign out") + driver.assert_text("signed out", "#top_message") +finally: + driver.quit() ``` -(*See the example test, [my_first_test.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/my_first_test.py), for reference.*) + +(See the example, [raw_login_driver.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/raw_login_driver.py), for reference.) + +๐Ÿ‘๏ธ๐Ÿ”Ž Note that regular SeleniumBase formats (ones that use `BaseCase`, the `SB` context manager, or the `sb` `pytest` fixture) have more methods than the improved `driver` format. The regular formats also have more features. Some features, (such as the [SeleniumBase dashboard](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/example_logs/ReadMe.md)), require a `pytest` format. + +-------- + +### โœ… No More Flaky Tests! + +

      SeleniumBase methods automatically wait for page elements to finish loading before interacting with them (up to a timeout limit).

      +NO MORE FLAKY TESTS! + +**There are three layers of protection that provide reliability for tests using SeleniumBase:** + +* **(1)**: Selenium's default `pageLoadStrategy` is `normal`: This strategy causes Selenium to wait for the full page to load, with HTML content and sub-resources downloaded and parsed. + +* **(2)**: SeleniumBase includes methods such as `wait_for_ready_state_complete()`, which run inside other SeleniumBase methods to ensure that it's safe to proceed with the next command. + +* **(3)**: SeleniumBase methods automatically wait for elements to be visible and interactable before interacting with those elements. + +**If you want to speed up your tests and you think the third level of protection is enough by itself, you can use command-line options to remove the first, the second, or both of those first two levels of protection:** + +* `--pls=none` --> Set `pageLoadStrategy` to "none": This strategy causes Selenium to return immediately after the initial HTML content is fully received by the browser. + +* `--sjw` --> Skip JS Waits, such as `wait_for_ready_state_complete()`. + +-------- + +

      SeleniumBase

      +

      diff --git a/help_docs/html_inspector.md b/help_docs/html_inspector.md new file mode 100644 index 00000000000..b448fb25eed --- /dev/null +++ b/help_docs/html_inspector.md @@ -0,0 +1,35 @@ + + +

      The HTML Inspector ๐Ÿ•ต๏ธ

      + +๐Ÿ•ต๏ธ HTML Inspector provides useful info about a web page. + +๐Ÿ•ต๏ธ (Based on: [github.com/philipwalton/html-inspector](https://github.com/philipwalton/html-inspector)) + +๐Ÿ•ต๏ธ Example: [examples/test_inspect_html.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_inspect_html.py) (Chromium-only) + +```python +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + +class HtmlInspectorTests(BaseCase): + def test_html_inspector(self): + self.goto("https://xkcd.com/1144/") + self.inspect_html() +``` + +---- + +```zsh +pytest test_inspect_html.py +============== test session starts ============== + +* HTML Inspection Results: https://xkcd.com/1144/ +โš ๏ธ 'property' is not a valid attribute of the element. +โš ๏ธ Do not use
      or elements without any attributes. +โš ๏ธ 'srcset' is not a valid attribute of the element. +โš ๏ธ The 'border' attribute is no longer valid on the element. +โš ๏ธ The
      element is obsolete. +โš ๏ธ The id 'comicLinks' appears more than once in the document. +* (See the Console output for details!) +``` diff --git a/help_docs/install.md b/help_docs/install.md old mode 100755 new mode 100644 index 4073cda5403..a7d987a719b --- a/help_docs/install.md +++ b/help_docs/install.md @@ -1,20 +1,55 @@ -## ![http://seleniumbase.com](https://cdn2.hubspot.net/hubfs/100006/images/super_logo_tiny.png "SeleniumBase") SeleniumBase Installation + -If you're installing SeleniumBase from a cloned copy on your machine, use: -``` -pip install -U -r requirements.txt +

      SeleniumBase Installation

      + +

      If installing seleniumbase directly from PyPI, (the Python Package Index), use:

      -python setup.py install +```zsh +pip install seleniumbase ``` -If you're installing SeleniumBase directly [from PyPI (the Python Package Index)](https://pypi.python.org/pypi/seleniumbase), use: -```bash +

      To upgrade an existing seleniumbase install from PyPI:

      + +```zsh pip install -U seleniumbase ``` -If you're installing SeleniumBase directly [from GitHub](https://github.com/seleniumbase/SeleniumBase), use: -```bash -pip install -U -e git+https://github.com/seleniumbase/SeleniumBase.git@master#egg=seleniumbase +

      If installing seleniumbase from a Git clone, use:

      + +```zsh +git clone https://github.com/seleniumbase/SeleniumBase.git +cd SeleniumBase/ +pip install . ``` -(If you're not using a virtual environment, you may need to add ``--user`` to your ``pip`` command if you're getting errors during installation.) +

      For a development mode install in editable mode, use:

      + +```zsh +git clone https://github.com/seleniumbase/SeleniumBase.git +cd SeleniumBase/ +pip install -e . +``` + +

      To upgrade an existing seleniumbase install from GitHub:

      + +```zsh +git pull # To pull the latest version +pip install -e . # Or "pip install ." +``` + +

      If installing seleniumbase from a GitHub branch, use:

      + +```zsh +pip install git+https://github.com/seleniumbase/SeleniumBase.git@master#egg=seleniumbase +``` + +

      pip install can be customized:

      + +* (Add ``--upgrade`` OR ``-U`` to upgrade SeleniumBase.) +* (Add ``--force-reinstall`` to upgrade indirect libraries.) + +(If you're not using a virtual environment, you may need to add ``--user`` to your ``pip`` command if you're seeing errors during installation.) + +-------- + +[](https://github.com/seleniumbase/SeleniumBase/) diff --git a/help_docs/install_python_pip_git.md b/help_docs/install_python_pip_git.md old mode 100755 new mode 100644 index b9342da251f..7fcedd7c760 --- a/help_docs/install_python_pip_git.md +++ b/help_docs/install_python_pip_git.md @@ -1,4 +1,6 @@ -## Installation instructions for Git, Python, and Pip + + +## Installation instructions for `Git`, `Python`, and `pip` ### [Git](http://www.git-scm.com) @@ -8,50 +10,49 @@ You can [download Git from here](http://git-scm.com/downloads). (You can also download SeleniumBase from GitHub without using git-related commands.) -### [Python 2.7 or 3.x](https://www.python.org/downloads/) - -#### macOS: - -Python should already come preinstalled. You can use both Python 2.7 or Python 3.6+ with SeleniumBase. If you have [Homebrew](https://brew.sh/) installed, you can use: ``brew install python3`` to install Python 3. Or you can just get everything from [https://www.python.org/downloads/](https://www.python.org/downloads/). +### [Python](https://www.python.org) -The official docs.python-guide.org instructions here: [Installing Python 2 on Mac OS X](https://docs.python-guide.org/starting/install/osx/) and [Installing Python 3 on Mac OS X](https://docs.python-guide.org/starting/install3/osx/#install3-osx). (NOTE: Apple has rebranded OS X as macOS but this has not been reflected in the official docs.python-guide.org instructions yet.) +You can download Python from [https://www.python.org/downloads/](https://www.python.org/downloads/) if it's not already preinstalled on your machine. -#### Windows: +### [pip](https://en.wikipedia.org/wiki/Pip_%28package_manager%29) -You can [download Python 2.7 from here](https://www.python.org/downloads/release/python-2713/) OR [download Python 3.6.6 from here](https://www.python.org/downloads/release/python-366/). +**`pip` already comes with Python!** (It lets you install packages, such as `seleniumbase`.) -### [Pip](https://en.wikipedia.org/wiki/Pip_%28package_manager%29) +โš ๏ธ If something went wrong with your `pip` installation, try this: -You might already have pip and setuptools installed, but if you don't: - -On macOS / Windows / Linux, run the following command: -```bash +```zsh python -m ensurepip --default-pip ``` If your existing version of pip is old, upgrade to the latest version: -```bash + +```zsh python -m pip install --upgrade pip setuptools ``` -On CentOS 7 and some versions of Linux, you may need to install pip with ``yum``: -```bash +On CentOS 7 and some versions of Linux, you may need to install pip with `yum`: + +```zsh yum -y update yum -y install python-pip ``` -If you're having any trouble getting pip, you can [GET PIP HERE](https://pip.pypa.io/en/latest/installing/). +If you're having any trouble getting pip, you can [GET PIP HERE](https://pip.pypa.io/en/latest/installation/). -When done, make sure the location of pip is on your path, which is `$PATH` for macOS/Linux. (On Windows, it's the System Variables `Path` within System Environment Variables. Ex: Add "C:/Python27/Scripts/" to the end of the `Path` variable.) +When done, make sure the location of pip is on your path, which is `$PATH` for macOS/Linux. (On Windows, it's the System Variables `Path` within System Environment Variables.) You can also get pip (or fix pip) by using: -```bash + +```zsh curl https://bootstrap.pypa.io/get-pip.py | python ``` + * (If you get SSL errors while trying to install packages with pip, see [this Stackoverflow post](https://stackoverflow.com/questions/49768770/not-able-to-install-python-packages-ssl-tlsv1-alert-protocol-version), which tells you to run the above command.) **Keep Pip and Setuptools up-to-date:** -``` + +```zsh python -m pip install -U pip setuptools ``` -* (Depending on your user permissions, you may need to add ``--user`` to the command if you're not inside a [Python virtual environment](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/virtualenv_instructions.md), or use "[sudo](https://en.wikipedia.org/wiki/Sudo)" on a UNIX-based OS if you're getting errors during installation.) + +* (Depending on your user permissions, you may need to add `--user` to the command if you're not inside a [Python virtual environment](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/virtualenv_instructions.md), or use "[sudo](https://en.wikipedia.org/wiki/Sudo)" on a UNIX-based OS if you're getting errors during installation.) diff --git a/help_docs/js_package_manager.md b/help_docs/js_package_manager.md new file mode 100644 index 00000000000..695936f3a81 --- /dev/null +++ b/help_docs/js_package_manager.md @@ -0,0 +1,140 @@ + + +

      JS Package Manager and Code Generators

      + +

      ๐ŸŸจ SeleniumBase lets you load JavaScript packages from any CDN link into any website via Python.

      + +๐ŸŽจ The following SeleniumBase solutions utilize this feature: + +๐ŸŽฆ (Demo Mode) + +๐ŸšŽ (Tour Maker) + +๐ŸŽž๏ธ (Presentation Maker) + +๐Ÿ“Š (Chart Maker / Dashboard) + +๐Ÿ›‚ (Dialog Boxes / MasterQA) + +-------- + +

      ๐Ÿ—บ๏ธ Here's an example of loading a website-tour library into the browser for a Google Maps tour:

      + +
      + +

      ๐Ÿ—บ๏ธ This example is from maps_introjs_tour.py. (The --interval=1 makes the tour go automatically to the next step after 1 second.)

      + +```zsh +cd examples/tour_examples +pytest maps_introjs_tour.py --interval=1 +``` + +

      ๐ŸŸจ SeleniumBase includes powerful JS code generators for converting Python into JavaScript for using the supported JS packages. A few lines of Python in your tests might generate hundreds of lines of JavaScript.

      + +

      ๐Ÿ—บ๏ธ Here is some tour code in Python from maps_introjs_tour.py that expands into a lot of JavaScript.

      + +```python +self.goto("https://www.google.com/maps/@42.3591234,-71.0915634,15z") +self.create_tour(theme="introjs") +self.add_tour_step("Welcome to Google Maps!", title="SeleniumBase Tours") +self.add_tour_step("Enter Location", "#searchboxinput", title="Search Box") +self.add_tour_step("See it", "#searchbox-searchbutton", alignment="bottom") +self.add_tour_step("Thanks for using Tours!", title="End of Guided Tour") +self.export_tour(filename="maps_introjs_tour.js") +self.play_tour() +``` + +

      ๐ŸŸจ For existing features, SeleniumBase already takes care of loading all the necessary JS and CSS files into the web browser. To load other packages, here are a few useful methods that you should know about:

      + +```python +self.add_js_link(js_link) +``` + +

      ๐ŸŸจ This example loads the IntroJS JavaScript library:

      + +```python +self.add_js_link("https://cdn.jsdelivr.net/npm/intro.js@5.1.0/intro.min.js") +``` + +

      ๐ŸŸจ If you're wondering how SeleniumBase does this, here's the code from js_utils.py that loads JavaScript packages:

      + +```python +def add_js_link(driver, js_link): + script_to_add_js = ( + """function injectJS(link) { + var body = document.getElementsByTagName("body")[0]; + var script = document.createElement("script"); + script.src = link; + script.defer; + script.type="text/javascript"; + script.crossorigin = "anonymous"; + script.onload = function() { null }; + body.appendChild(script); + } + injectJS("%s");""") + js_link = escape_quotes_if_needed(js_link) + driver.execute_script(script_to_add_js % js_link) +``` + +

      ๐ŸŸจ Now that you've loaded JavaScript into the browser, you may also want to load some CSS to go along with it:

      + +```python +self.add_css_link(css_link) +``` + +

      ๐ŸŸจ Here's code that loads the IntroJS CSS:

      + +```python +self.add_css_link("https://cdnjs.cloudflare.com/ajax/libs/intro.js/2.9.3/introjs.css") +``` + +

      ๐ŸŸจ And here's the Python WebDriver code that makes this possible:

      + +```python +def add_css_link(driver, css_link): + script_to_add_css = ( + """function injectCSS(css) { + var head = document.getElementsByTagName("head")[0]; + var link = document.createElement("link"); + link.rel = "stylesheet"; + link.type = "text/css"; + link.href = css; + link.crossorigin = "anonymous"; + head.appendChild(link); + } + injectCSS("%s");""") + css_link = escape_quotes_if_needed(css_link) + driver.execute_script(script_to_add_css % css_link) +``` + +
      ๐Ÿ—บ๏ธ Website tours are just one of the many uses of the JS Package Manager.
      +

      ๐Ÿ›‚ The following example shows the JqueryConfirm package loaded into a website for creating fancy dialog boxes:

      + +SeleniumBase + +

      โ†•๏ธ (Example: dialog_box_tour.py) โ†•๏ธ

      + +SeleniumBase + +

      Here's how to run that example:

      + +```zsh +cd examples/dialog_boxes +pytest test_dialog_boxes.py +``` + +

      (Example from the Dialog Boxes ReadMe)

      + +
      ๐ŸŸจ Since packages are loaded directly from a CDN link, you won't need other package managers like NPM, Bower, or Yarn to get the packages that you need into the websites that you want.
      + +-------- + + + +
      To learn more about SeleniumBase, check out the Docs Site:
      + +SeleniumBase.io Docs + +
      All the code is on GitHub:
      + +SeleniumBase on GitHub diff --git a/help_docs/locale_codes.md b/help_docs/locale_codes.md new file mode 100644 index 00000000000..1f5e2982cf0 --- /dev/null +++ b/help_docs/locale_codes.md @@ -0,0 +1,120 @@ + + +

      Language Locale Codes

      + +You can specify a Language Locale Code to customize web pages on supported websites. With SeleniumBase, you can change the web browser's Locale on the command-line by doing this: + +```zsh +pytest --locale=CODE # Example: --locale=ru +``` + +From the `SB()` and `Driver()` formats, you can also set the `locale` arg like this: + +```python +locale="CODE" # Example: SB(locale="en") +``` + +

      List of Language Locale Codes:

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      LanguageCode
      Afrikaansaf
      Amharicam
      Arabicar
      Arabic (Egypt)ar_eg
      Arabic (Saudi Arabia)ar_sa
      Basqueeu
      Belarusianbe
      Bengalibn
      Bulgarianbg
      Catalanca
      Chinesezh
      Chinese (China Mainland)zh_cn
      Chinese (Hong Kong)zh_hk
      Chinese (Taiwan)zh_tw
      Croatianhr
      Czechcs
      Danishda
      Dutchnl
      Englishen
      English (United States)en_us
      English (Australia)en_au
      English (Canada)en_ca
      English (United Kingdom)en_gb
      English (Ireland)en_ie
      English (India)en_in
      English (Singapore)en_sg
      English (South Africa)en_za
      Estonianet
      Farsifa
      Filipinofil
      Finnishfi
      Frenchfr
      French (Canada)fr_ca
      French (Switzerland)fr_ch
      Galiciangl
      Germande
      German (Austria)de_at
      Greekel
      Gujaratigu
      Hebrewhe
      Hindihi
      Hungarianhu
      Icelandicis
      Indonesianid
      Italianit
      Japaneseja
      Kannadakn
      Koreanko
      Laolo
      Latvianlv
      Lingalaln
      Lithuanianlt
      Malayms
      Malayalamml
      Marathimr
      Norwegianno
      Polishpl
      Portuguesept
      Portuguese (Brazil)pt_br
      Portuguese (Portugal)pt_pt
      Romanianro
      Russianru
      Serbiansr
      Slovaksk
      Sloveniansl
      Spanishes
      Spanish (Latin America)es_419
      Spanish (Argentina)es_ar
      Spanish (Chile)es_cl
      Spanish (Colombia)es_co
      Spanish (Costa Rica)es_cr
      Spanish (Dominican Rep.)es_do
      Spanish (Ecuador)es_ec
      Spanish (El Salvador)es_sv
      Spanish (Guatemala)es_gt
      Spanish (Honduras)es_hn
      Spanish (Mexico)es_mx
      Spanish (Nicaragua)es_ni
      Spanish (Panama)es_pa
      Spanish (Peru)es_pe
      Spanish (Puerto Rico)es_pr
      Spanish (Paraguay)es_py
      Spanish (United States)es_us
      Spanish (Uruguay)es_uy
      Spanish (Venezuela)es_ve
      Swahilisw
      Swedishsv
      Swiss Germangsw
      Tagalogtl
      Tamilta
      Telugute
      Thaith
      Turkishtr
      Ukrainianuk
      Urduur
      Vietnamesevi
      Zuluzu
      diff --git a/help_docs/method_summary.md b/help_docs/method_summary.md old mode 100755 new mode 100644 index f99da23ece3..cd9d4dc382e --- a/help_docs/method_summary.md +++ b/help_docs/method_summary.md @@ -1,388 +1,803 @@ -### SeleniumBase method summary + -Here's a summary of SeleniumBase method definitions, which are defined in [base_case.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/fixtures/base_case.py) +

      SeleniumBase Methods (API Reference)

      -In order to maintain backwards compatibility with scripts using earlier verions of SeleniumBase, some methods that had their names shortened can also be called by their original method name. *(Ex: wait_for_element_visible was later shortened to wait_for_element and then to find_element, but the longer method names remained to keep older scripts from failing.)* + +

      (Common API Methods on YouTube)

      -```python -self.open(url) - -self.open_url(url) - -self.click(selector, by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT) - -self.double_click(selector, by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT) - -self.click_chain(selectors_list, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT, spacing=0) - -self.is_link_text_present(link_text) - -self.get_link_attribute(link_text, attribute, hard_fail) - -self.wait_for_link_text_present(link_text, timeout=settings.SMALL_TIMEOUT) - -self.click_link_text(link_text, timeout=settings.SMALL_TIMEOUT) - -self.click_link(link_text, timeout=settings.SMALL_TIMEOUT) - -self.click_partial_link_text(partial_link_text, timeout=settings.SMALL_TIMEOUT) - -self.get_text(selector, by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT) +Here's a list of SeleniumBase method definitions, which are defined in **[base_case.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/fixtures/base_case.py)** -self.get_attribute(selector, attribute, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT) - -self.set_attribute(selector, attribute, value, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT) - -self.remove_attribute(selector, attribute, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT) - -self.get_property_value(selector, property, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT) - -self.refresh_page() +For backwards compatibility, older versions of method names have remained to keep older scripts working. *(E.g: wait_for_element_visible was shortened to wait_for_element and then to find_element.)* +```python +self.goto(url) +# Duplicates: self.open_url(url), self.visit(url), visit_url(url), +# self.open(url), self.go_to(url) +self.get(url) +# If the url parameter is a URL: Perform self.goto(url) +# Otherwise: return self.get_element(URL_AS_A_SELECTOR) +self.click(selector, by="css selector", timeout=None, delay=0, scroll=True) +self.slow_click(selector, by="css selector", timeout=None) +self.double_click(selector, by="css selector", timeout=None) +self.context_click(selector, by="css selector", timeout=None) +# Duplicates: +# self.right_click(selector, by="css selector", timeout=None) +self.click_chain(selectors_list, by="css selector", timeout=None, spacing=0) +self.type(selector, text, by="css selector", timeout=None) +# Duplicates: +# self.update_text(selector, text, by="css selector", timeout=None) +# self.input(selector, text, by="css selector", timeout=None) +# self.fill(selector, text, by="css selector", timeout=None) +# self.write(selector, text, by="css selector", timeout=None) +self.send_keys(selector, text, by="css selector", timeout=None) +# Duplicates: +# self.add_text(selector, text, by="css selector", timeout=None) +self.press_keys(selector, text, by="css selector", timeout=None) +self.submit(selector, by="css selector") +self.clear(selector, by="css selector", timeout=None) +self.focus(selector, by="css selector", timeout=None) self.refresh() - +# Duplicates: self.refresh_page(), self.reload_page(), self.reload() self.get_current_url() - +self.get_origin() +self.get_html() self.get_page_source() - -self.get_page_title() - self.get_title() - +# Duplicates: self.get_page_title() +self.get_user_agent() +self.get_locale_code() self.go_back() - self.go_forward() - -self.get_image_url(selector, by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT) - -self.add_text(selector, new_value, timeout=settings.SMALL_TIMEOUT) - -self.send_keys(selector, new_value, timeout=settings.SMALL_TIMEOUT) - -self.update_text_value(selector, new_value, - timeout=settings.SMALL_TIMEOUT, retry=False) - -self.update_text(selector, new_value, timeout=settings.SMALL_TIMEOUT, - retry=False) - -self.is_element_present(selector, by=By.CSS_SELECTOR) - -self.is_element_visible(selector, by=By.CSS_SELECTOR) - +self.goto_start_page() +# Duplicates: self.open_start_page() +self.goto_if_not_url(url) +# Duplicates: self.open_if_not_url() +self.is_element_present(selector, by="css selector") +self.is_element_visible(selector, by="css selector") +self.is_element_clickable(selector, by="css selector") +self.is_element_enabled(selector, by="css selector") +self.is_text_visible(text, selector="html", by="css selector") +self.is_exact_text_visible(text, selector="html", by="css selector") +self.is_non_empty_text_visible(selector="html", by="css selector") +self.is_attribute_present(selector, attribute, value=None, by="css selector") self.is_link_text_visible(link_text) - self.is_partial_link_text_visible(partial_link_text) - -self.is_text_visible(text, selector="html", by=By.CSS_SELECTOR) - -self.find_elements(selector, by=By.CSS_SELECTOR, limit=0) - -self.find_visible_elements(selector, by=By.CSS_SELECTOR, limit=0) - -self.click_visible_elements(selector, by=By.CSS_SELECTOR, limit=0) - -self.is_element_in_an_iframe(selector, by=By.CSS_SELECTOR) - -self.switch_to_frame_of_element(selector, by=By.CSS_SELECTOR) - -self.execute_script(script) - +self.is_link_text_present(link_text) +self.is_partial_link_text_present(link_text) +self.get_link_attribute(link_text, attribute, hard_fail=True) +# Duplicates: +# self.get_link_text_attribute(link_text, attribute, hard_fail=True) +self.get_partial_link_text_attribute(link_text, attribute, hard_fail=True) +self.click_link(link_text, timeout=None) +# Duplicates: +# self.click_link_text(link_text, timeout=None) +self.click_partial_link(partial_link_text, timeout=None) +# Duplicates: +# self.click_partial_link_text(partial_link_text, timeout=None) +self.get_text(selector="body", by="css selector", timeout=None) +self.get_attribute(selector, attribute, by="css selector", timeout=None, hard_fail=True) +self.set_attribute(selector, attribute, value, by="css selector", timeout=None, scroll=False) +self.set_attributes(selector, attribute, value, by="css selector") +# Duplicates: +# self.set_attribute_all(selector, attribute, value, by="css selector") +self.remove_attribute(selector, attribute, by="css selector", timeout=None) +self.remove_attributes(selector, attribute, by="css selector") +self.internalize_links() +self.get_parent(element, by="css selector", timeout=None) +self.get_property(selector, property, by="css selector", timeout=None) +self.get_text_content(selector="html", by="css selector", timeout=None) +self.get_property_value(selector, property, by="css selector", timeout=None) +self.get_image_url(selector, by="css selector", timeout=None) +self.find_elements(selector, by="css selector", limit=0) +# Duplicates: +# self.select_all(selector, by="css selector", limit=0) +self.find_visible_elements(selector, by="css selector", limit=0) +self.click_visible_elements( + selector, by="css selector", limit=0, timeout=None, scroll=True) +self.click_nth_visible_element( + selector, number, by="css selector", timeout=None, scroll=True) +self.click_if_visible(selector, by="css selector", timeout=0, scroll=True) +self.click_active_element() +self.click_with_offset( + selector, x, y, by="css selector", mark=None, timeout=None, center=None, scroll=True) +self.double_click_with_offset( + selector, x, y, by="css selector", mark=None, timeout=None, center=None, scroll=True) +self.is_checked(selector, by="css selector", timeout=None) +# Duplicates: +# self.is_selected(selector, by="css selector", timeout=None) +self.check_if_unchecked(selector, by="css selector") +# Duplicates: +# self.select_if_unselected(selector, by="css selector") +self.uncheck_if_checked(selector, by="css selector") +# Duplicates: +# self.unselect_if_selected(selector, by="css selector") +self.is_element_in_an_iframe(selector, by="css selector") +self.switch_to_frame_of_element(selector, by="css selector") +self.hover(selector, by="css selector", timeout=None) +# Duplicates: +# self.hover_element(selector, by="css selector", timeout=None) +# self.hover_on_element(selector, by="css selector", timeout=None) +# self.hover_over_element(selector, by="css selector", timeout=None) +self.hover_and_click( + hover_selector, click_selector, + hover_by="css selector", click_by="css selector", + timeout=None, js_click=False) +self.hover_and_js_click( + hover_selector, click_selector, + hover_by="css selector", click_by="css selector", + timeout=None) +self.hover_and_double_click( + hover_selector, click_selector, + hover_by="css selector", click_by="css selector", + timeout=None) +self.drag_and_drop( + drag_selector, drop_selector, + drag_by="css selector", drop_by="css selector", + timeout=None, jquery=False) +self.drag_and_drop_with_offset( + selector, x, y, by="css selector", timeout=None) +self.select_option_by_text( + dropdown_selector, option, dropdown_by="css selector", timeout=None) +self.select_option_by_index( + dropdown_selector, option, dropdown_by="css selector", timeout=None) +self.select_option_by_value( + dropdown_selector, option, dropdown_by="css selector", timeout=None) +self.get_select_options( + dropdown_selector, attribute="text", by="css selector", timeout=None) +self.load_html_string(html_string, new_page=True) +self.set_content(html_string, new_page=False) +self.load_html_file(html_file, new_page=True) +self.open_html_file(html_file) +self.evaluate(expression) +self.execute_script(script, *args, **kwargs) +self.execute_cdp_cmd(script, *args, **kwargs) +self.execute_async_script(script, timeout=None) +self.safe_execute_script(script, *args, **kwargs) +self.get_element_at_x_y(x, y) +self.get_gui_element_rect(selector, by="css selector") +self.get_gui_element_center(selector, by="css selector") +self.get_screen_rect() +self.get_window_rect() +self.get_window_size() +self.get_window_position() +self.set_window_rect(x, y, width, height) self.set_window_size(width, height) - +self.set_window_position(x, y) self.maximize_window() - -self.add_css_link(css_link) - -self.add_js_link(js_link) - -self.add_css_style(css_style) - -self.add_js_code_from_link(js_link) - -self.add_meta_tag(http_equiv=None, content=None) - +# Duplicates: self.maximize() +self.minimize_window() +# Duplicates: self.minimize() +self.reset_window_size() +self.switch_to_frame(frame="iframe", timeout=None, invisible=False) +self.switch_to_default_content() +self.switch_to_parent_frame() +with self.frame_switch(frame, timeout=None): + # Indented Code Block for Context Manager (Must use "with") +self.set_content_to_frame(frame, timeout=None) +self.set_content_to_default(nested=False) +# Duplicates: self.set_content_to_default_content(nested=False) +self.set_content_to_parent() +# Duplicates: self.set_content_to_parent_frame() +self.open_new_window(switch_to=True) +# Duplicates: self.open_new_tab(switch_to=True) +self.switch_to_window(window, timeout=None) +# Duplicates: self.switch_to_tab(tab, timeout=None) +self.switch_to_default_window() +# Duplicates: self.switch_to_default_tab() +self.switch_to_newest_window() +# Duplicates: self.switch_to_newest_tab() +self.get_new_driver( + browser=None, + headless=None, + locale_code=None, + protocol=None, + servername=None, + port=None, + proxy=None, + proxy_bypass_list=None, + proxy_pac_url=None, + multi_proxy=None, + agent=None, + switch_to=True, + cap_file=None, + cap_string=None, + recorder_ext=None, + disable_cookies=None, + disable_js=None, + disable_csp=None, + enable_ws=None, + enable_sync=None, + use_auto_ext=None, + undetectable=None, + uc_cdp_events=None, + uc_subprocess=None, + log_cdp_events=None, + no_sandbox=None, + disable_gpu=None, + headless1=None, + headless2=None, + incognito=None, + guest_mode=None, + dark_mode=None, + devtools=None, + remote_debug=None, + enable_3d_apis=None, + swiftshader=None, + ad_block_on=None, + host_resolver_rules=None, + block_images=None, + do_not_track=None, + chromium_arg=None, + firefox_arg=None, + firefox_pref=None, + user_data_dir=None, + extension_zip=None, + extension_dir=None, + disable_features=None, + binary_location=None, + driver_version=None, + page_load_strategy=None, + external_pdf=None, + is_mobile=None, + d_width=None, + d_height=None, + d_p_r=None, +) +self.switch_to_driver(driver) +self.switch_to_default_driver() +self.save_screenshot(name, folder=None, selector=None, by="css selector") +self.save_screenshot_to_logs(name=None, selector=None, by="css selector") +self.save_as_pdf_to_logs(name=None) +self.save_data_to_logs(data, file_name=None) +self.append_data_to_logs(data, file_name=None) +self.save_page_source_to_logs(name=None) +# Duplicates: self.save_as_html_to_logs(name, folder=None) +self.save_page_source(name, folder=None) +# Duplicates: self.save_as_html(name, folder=None) +self.save_cookies(name="cookies.txt") +self.load_cookies(name="cookies.txt", expiry=False) +self.delete_all_cookies() +# Duplicates: self.clear_all_cookies() +self.delete_saved_cookies(name="cookies.txt") +self.get_saved_cookies(name="cookies.txt") +self.get_cookie(name) +self.get_cookies() +self.get_cookie_string() +self.add_cookie(cookie_dict, expiry=False) +self.add_cookies(cookies, expiry=False) +self.wait_for_ready_state_complete(timeout=None) +self.wait_for_angularjs(timeout=None) +self.sleep(seconds) +# Duplicates: self.wait(seconds) +self.install_addon(xpi_file) self.activate_jquery() - -self.create_tour(name=None, theme=None) - -self.create_shepherd_tour(name=None, theme=None) - -self.create_bootstrap_tour(name=None) - -self.create_hopscotch_tour(name=None) - -self.create_introjs_tour(name=None) - -self.add_tour_step(message, selector=None, name=None, - title=None, theme=None, alignment=None) - -self.play_tour(name=None) - -self.export_tour(name=None, filename="my_tour.js", url=None) - -self.activate_jquery_confirm() - -self.activate_messenger() - -self.post_message(message, duration=None, pause=True, style="info") - -self.post_success_message(message, duration=None, pause=True) - -self.post_error_message(message, duration=None, pause=True) - -self.set_messenger_theme(theme="default", location="default", - max_messages="default") - -self.bring_to_front(selector, by=By.CSS_SELECTOR) - -self.highlight(selector, by=By.CSS_SELECTOR, loops=4, scroll=True) - -self.scroll_to(selector, by=By.CSS_SELECTOR) - -self.slow_scroll_to(selector, by=By.CSS_SELECTOR) - +self.activate_demo_mode() +self.deactivate_demo_mode() +self.activate_design_mode() +self.deactivate_design_mode() +self.activate_recorder() +self.save_recorded_actions() +self.bring_active_window_to_front() +self.bring_to_front(selector, by="css selector") +self.flash(selector, duration=1, color="44CC88", pause=0) +self.highlight_click(selector, by="css selector", loops=3, scroll=True, timeout=None) +self.highlight_type(selector, text, by="css selector", loops=3, scroll=True, timeout=None) +# Duplicates: +# self.highlight_update_text( +# selector, text, by="css selector", loops=3, scroll=True, timeout=None) +self.highlight_if_visible(selector, by="css selector", loops=4, scroll=True) +self.highlight(selector, by="css selector", loops=4, scroll=True, timeout=None) +self.highlight_elements(selector, by="css selector", loops=4, scroll=True, limit=0) +self.press_up_arrow(selector="html", times=1, by="css selector") +self.press_down_arrow(selector="html", times=1, by="css selector") +self.press_left_arrow(selector="html", times=1, by="css selector") +self.press_right_arrow(selector="html", times=1, by="css selector") +self.scroll_to(selector, by="css selector", timeout=None) +# Duplicates: +# self.scroll_to_element(selector, by="css selector") +self.slow_scroll_to(selector, by="css selector", timeout=None) +# Duplicates: +# self.slow_scroll_to_element(selector, by="css selector") +self.scroll_into_view(selector, by="css selector", timeout=None) +self.scroll_to_top() +self.scroll_to_bottom() +self.scroll_to_y(y) +self.scroll_by_y(y) +self.scroll_up(amount=25) +self.scroll_down(amount=25) self.click_xpath(xpath) - -self.js_click(selector, by=By.CSS_SELECTOR) - -self.jquery_click(selector, by=By.CSS_SELECTOR) - -self.submit(selector, by=By.CSS_SELECTOR) - -self.hide_element(selector, by=By.CSS_SELECTOR) - -self.hide_elements(selector, by=By.CSS_SELECTOR) - -self.show_element(selector, by=By.CSS_SELECTOR) - -self.show_elements(selector, by=By.CSS_SELECTOR) - -self.remove_element(selector, by=By.CSS_SELECTOR) - -self.remove_elements(selector, by=By.CSS_SELECTOR) - +self.js_click(selector, by="css selector", all_matches=False, timeout=None, scroll=True) +self.js_click_if_present(selector, by="css selector", timeout=0) +self.js_click_if_visible(selector, by="css selector", timeout=0) +self.js_click_all(selector, by="css selector", timeout=None) +self.jquery_click(selector, by="css selector", timeout=None) +self.jquery_click_all(selector, by="css selector", timeout=None) +self.hide_element(selector, by="css selector") +self.hide_elements(selector, by="css selector") +self.show_element(selector, by="css selector") +self.show_elements(selector, by="css selector") +self.remove_element(selector, by="css selector") +self.remove_elements(selector, by="css selector") self.ad_block() - +# Duplicates: self.block_ads() +self.show_file_choosers() +self.disable_beforeunload() self.get_domain_url(url) - +self.get_active_element_css() self.get_beautiful_soup(source=None) - self.get_unique_links() - -self.get_link_status_code(link, allow_redirects=False, timeout=5) - +self.get_link_status_code(link, allow_redirects=False, timeout=5, verify=False) self.assert_link_status_code_is_not_404(link) - -self.assert_no_404_errors(multithreaded=True) - +self.assert_no_404_errors(multithreaded=True, timeout=None) +# Duplicates: +# self.assert_no_broken_links(multithreaded=True, timeout=None) self.print_unique_links_with_status_codes() - -self.safe_execute_script(script) - +self.get_pdf_text( + pdf, page=None, maxpages=None, password=None, + codec='utf-8', wrap=False, nav=False, override=False, caching=True) +self.assert_pdf_text( + pdf, text, page=None, maxpages=None, password=None, + codec='utf-8', wrap=True, nav=False, override=False, caching=True) self.create_folder(folder) - -self.save_element_as_image_file(selector, file_name, folder=None) - +self.choose_file(selector, file_path, by="css selector", timeout=None) +self.save_element_as_image_file(selector, file_name, folder=None, overlay_text="") self.download_file(file_url, destination_folder=None) - self.save_file_as(file_url, new_file_name, destination_folder=None) - self.save_data_as(data, file_name, destination_folder=None) - -self.get_downloads_folder(file) - -self.get_path_of_downloaded_file(file) - -self.is_downloaded_file_present(file) - -self.assert_downloaded_file(file) - +self.append_data_to_file(data, file_name, destination_folder=None) +self.get_file_data(file_name, folder=None) +self.print_to_pdf(name, folder=None) +# Duplicates: self.save_as_pdf(name, folder=None) +self.get_downloads_folder() +self.get_browser_downloads_folder() +self.get_downloaded_files(regex=None, browser=False) +self.get_path_of_downloaded_file(file, browser=False) +self.get_data_from_downloaded_file(file, timeout=None, browser=False) +self.is_downloaded_file_present(file, browser=False) +self.is_downloaded_file_regex_present(regex, browser=False) +self.delete_downloaded_file_if_present(file, browser=False) +# Duplicates: self.delete_downloaded_file(file, browser=False) +self.assert_downloaded_file(file, timeout=None, browser=False) +self.assert_downloaded_file_regex(regex, timeout=None, browser=False) +self.assert_data_in_downloaded_file(data, file, timeout=None, browser=False) self.assert_true(expr, msg=None) - self.assert_false(expr, msg=None) - self.assert_equal(first, second, msg=None) - self.assert_not_equal(first, second, msg=None) - -self.assert_no_js_errors() - -self.get_google_auth_password(totp_key=None) - +self.assert_in(first, second, msg=None) +self.assert_not_in(first, second, msg=None) +self.assert_raises(*args, **kwargs) +self.wait_for_attribute(selector, attribute, value=None, by="css selector", timeout=None) +self.assert_attribute(selector, attribute, value=None, by="css selector", timeout=None) +self.assert_title(title) +self.assert_title_contains(substring) +self.assert_url(url) +self.assert_url_contains(substring) +self.assert_no_js_errors(exclude=[]) +self.inspect_html() +self.is_valid_url(url) +self.is_alert_present() +self.is_online() +self.is_chromium() +self.get_chrome_version() +self.get_chromium_version() +self.get_chromedriver_version() +self.get_chromium_driver_version() +self.get_mfa_code(totp_key=None) +# Duplicates: +# self.get_totp_code(totp_key=None) +# self.get_google_auth_password(totp_key=None) +# self.get_google_auth_code(totp_key=None) +self.enter_mfa_code(selector, totp_key=None, by="css selector", timeout=None) +# Duplicates: +# self.enter_totp_code(selector, totp_key=None, by="css selector", timeout=None) +self.convert_css_to_xpath(css) self.convert_xpath_to_css(xpath) - self.convert_to_css_selector(selector, by) +self.set_value(selector, text, by="css selector", timeout=None, scroll=True) +self.js_update_text(selector, text, by="css selector", timeout=None) +# Duplicates: +# self.js_type(selector, text, by="css selector", timeout=None) +# self.set_text(selector, text, by="css selector", timeout=None) +self.set_text_content(selector, text, by="css selector", timeout=None, scroll=False) +self.jquery_update_text(selector, text, by="css selector", timeout=None) +# Duplicates: +# self.jquery_type(selector, text, by="css selector", timeout=None) +self.get_value(selector, by="css selector", timeout=None) +self.set_time_limit(time_limit) +self.set_default_timeout(timeout) +self.reset_default_timeout() +self.fail(msg=None) +self.skip(reason="") + +############ + +self.start_recording_console_logs() +self.console_log_string(string) +self.console_log_script(script) +self.get_recorded_console_logs() + +############ + +self.set_local_storage_item(key, value) +self.get_local_storage_item(key) +self.remove_local_storage_item(key) +self.clear_local_storage() +# Duplicates: self.delete_local_storage() +self.get_local_storage_keys() +self.get_local_storage_items() +self.set_session_storage_item(key, value) +self.get_session_storage_item(key) +self.remove_session_storage_item(key) +self.clear_session_storage() +# Duplicates: self.delete_session_storage() +self.get_session_storage_keys() +self.get_session_storage_items() + +############ -self.set_value(selector, new_value, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT) - -self.js_update_text(selector, new_value, by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT) - -self.jquery_update_text_value(selector, new_value, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT) - -self.jquery_update_text(selector, new_value, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT) - -self.hover_on_element(selector, by=By.CSS_SELECTOR) +self.add_css_link(css_link) +self.add_js_link(js_link) +self.add_css_style(css_style) +self.add_js_code_from_link(js_link) +self.add_js_code(js_code) +self.add_meta_tag(http_equiv=None, content=None) -self.hover_and_click(hover_selector, click_selector, - hover_by=By.CSS_SELECTOR, click_by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT) +############ + +self.create_presentation(name=None, theme="default", transition="default") +self.add_slide( + content=None, image=None, code=None, iframe=None, + content2=None, notes=None, transition=None, name=None) +self.save_presentation(name=None, filename=None, show_notes=False, interval=0) +self.begin_presentation(name=None, filename=None, show_notes=False, interval=0) + +############ + +self.create_pie_chart( + chart_name=None, title=None, subtitle=None, + data_name=None, unit=None, libs=True, + labels=True, legend=True) +self.create_bar_chart( + chart_name=None, title=None, subtitle=None, + data_name=None, unit=None, libs=True, + labels=True, legend=True) +self.create_column_chart( + chart_name=None, title=None, subtitle=None, + data_name=None, unit=None, libs=True, + labels=True, legend=True) +self.create_line_chart( + chart_name=None, title=None, subtitle=None, + data_name=None, unit=None, zero=False, libs=True, + labels=True, legend=True) +self.create_area_chart( + chart_name=None, title=None, subtitle=None, + data_name=None, unit=None, zero=False, libs=True, + labels=True, legend=True) +self.add_series_to_chart(data_name=None, chart_name=None) +self.add_data_point(label, value, color=None, chart_name=None) +self.save_chart(chart_name=None, filename=None, folder=None) +self.display_chart(chart_name=None, filename=None, interval=0) +self.extract_chart(chart_name=None) + +############ -self.select_option_by_text(dropdown_selector, option, - dropdown_by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT) +self.create_tour(name=None, theme=None) +self.create_shepherd_tour(name=None, theme=None) +self.create_bootstrap_tour(name=None) +self.create_hopscotch_tour(name=None) +self.create_introjs_tour(name=None) +self.set_introjs_colors(theme_color=None, hover_color=None) +self.add_tour_step(message, selector=None, name=None, title=None, theme=None, alignment=None) +self.play_tour(name=None, interval=0) +# Duplicates: self.start_tour(name=None, interval=0): +self.export_tour(name=None, filename="my_tour.js", url=None) -self.select_option_by_index(dropdown_selector, option, - dropdown_by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT) +############ -self.select_option_by_value(dropdown_selector, option, - dropdown_by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT) +self.activate_jquery_confirm() +self.set_jqc_theme(theme, color=None, width=None) +self.reset_jqc_theme() +self.get_jqc_button_input(message, buttons, options=None) +self.get_jqc_text_input(message, button=None, options=None) +self.get_jqc_form_inputs(message, buttons, options=None) -######## +############ -self.generate_referral(start_page, destination_page) +self.activate_messenger() +self.post_message(message, duration=None, pause=True, style="info") +self.post_message_and_highlight(message, selector, by="css selector") +self.post_success_message(message, duration=None, pause=True) +self.post_error_message(message, duration=None, pause=True) +self.set_messenger_theme(theme="default", location="default", max_messages="default") -self.generate_traffic(start_page, destination_page, loops=1) +############ +self.generate_referral(start_page, destination_page, selector=None) +self.generate_traffic(start_page, destination_page, loops=1, selector=None) self.generate_referral_chain(pages) - self.generate_traffic_chain(pages, loops=1) -######## - -self.wait_for_element_present(selector, by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT) - -self.assert_element_present(selector, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT) - -######## - -self.wait_for_element_visible(selector, by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT) - -self.wait_for_element(selector, by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT) - -self.find_element(selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT) - -self.assert_element( - selector, by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT) - -self.assert_element_visible( - selector, by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT) - -######## - -self.wait_for_text_visible(text, selector="html", by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT) - -self.wait_for_exact_text_visible(text, selector="html", by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT) - -self.wait_for_text(text, selector="html", by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT) - -self.find_text(text, selector="html", by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT) - -self.assert_text_visible(text, selector="html", by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT) - -self.assert_text(text, selector="html", by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT) - -self.assert_exact_text(text, selector="html", by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT) - -######## - -self.wait_for_link_text_visible(link_text, timeout=settings.LARGE_TIMEOUT) - -self.wait_for_link_text(link_text, timeout=settings.LARGE_TIMEOUT) - -self.find_link_text(link_text, timeout=settings.LARGE_TIMEOUT) - -self.assert_link_text(link_text, timeout=settings.SMALL_TIMEOUT) - -######## - -self.wait_for_partial_link_text(partial_link_text, - timeout=settings.LARGE_TIMEOUT) - -self.find_partial_link_text(partial_link_text, - timeout=settings.LARGE_TIMEOUT) - -self.assert_partial_link_text(partial_link_text, - timeout=settings.SMALL_TIMEOUT) - -######## - -self.wait_for_element_absent(selector, by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT) - -self.assert_element_absent(selector, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT) - -######## - -self.wait_for_element_not_visible(selector, by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT) - -self.assert_element_not_visible(selector, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT) - -######## - -self.wait_for_ready_state_complete(timeout=settings.EXTREME_TIMEOUT) - -self.wait_for_and_accept_alert(timeout=settings.LARGE_TIMEOUT) - -self.wait_for_and_dismiss_alert(timeout=settings.LARGE_TIMEOUT) - -self.wait_for_and_switch_to_alert(timeout=settings.LARGE_TIMEOUT) - -self.switch_to_frame(frame, timeout=settings.SMALL_TIMEOUT) - -self.switch_to_default_content() - -self.open_new_window(switch_to=True) - -self.switch_to_window(window, timeout=settings.SMALL_TIMEOUT) - -self.switch_to_default_window() - -self.check_window(name="default", level=0, baseline=False) - -self.save_screenshot(name, folder=None) - -self.get_new_driver(browser=None, headless=None, servername=None, port=None, - proxy=None, switch_to=True, cap_file=None) - -self.switch_to_driver(driver) - -self.switch_to_default_driver() - -######## - -self.delayed_assert_element(selector, by=By.CSS_SELECTOR, - timeout=settings.MINI_TIMEOUT) +############ + +self.select(selector, by="css selector", timeout=None) +self.get_element(selector, by="css selector", timeout=None) +# Duplicates: +# self.wait_for_selector(selector, by="css selector", timeout=None) +# self.locator(selector, by="css selector", timeout=None) +# self.wait_for_element_present(selector, by="css selector", timeout=None) +self.wait_for_query_selector(selector, by="css selector", timeout=None) +self.assert_element_present(selector, by="css selector", timeout=None) +self.assert_elements_present(*args, **kwargs) + +############ + +self.find_element(selector, by="css selector", timeout=None) +# Duplicates: +# self.wait_for_element(selector, by="css selector", timeout=None) +# self.wait_for_element_visible(selector, by="css selector", timeout=None) +self.assert_element(selector, by="css selector", timeout=None) +# Duplicates: +# self.assert_element_visible(selector, by="css selector", timeout=None) +self.assert_elements(*args, **kwargs) +# Duplicates: +# self.assert_elements_visible(*args, **kwargs) + +############ + +self.wait_for_any_of_elements_visible(*args, **kwargs) +self.wait_for_any_of_elements_present(*args, **kwargs) +self.assert_any_of_elements_visible(*args, **kwargs) +self.assert_any_of_elements_present(*args, **kwargs) + +############ + +self.find_text(text, selector="html", by="css selector", timeout=None) +# Duplicates: +# self.wait_for_text(text, selector="html", by="css selector", timeout=None) +# self.wait_for_text_visible(text, selector="html", by="css selector", timeout=None) +self.find_exact_text(text, selector="html", by="css selector", timeout=None) +# Duplicates: +# self.wait_for_exact_text(text, selector="html", by="css selector", timeout=None) +# self.wait_for_exact_text_visible(text, selector="html", by="css selector", timeout=None) +self.find_non_empty_text(selector="html", by="css selector", timeout=None) +# Duplicates: +# self.wait_for_non_empty_text(selector="html", by="css selector", timeout=None) +# self.wait_for_non_empty_text_visible(selector="html", by="css selector", timeout=None) +self.assert_text(text, selector="html", by="css selector", timeout=None) +# Duplicates: +# self.assert_text_visible(text, selector="html", by="css selector", timeout=None) +self.assert_exact_text(text, selector="html", by="css selector", timeout=None) + +############ + +self.wait_for_link_text_present(link_text, timeout=None) +self.wait_for_partial_link_text_present(link_text, timeout=None) +self.find_link_text(link_text, timeout=None) +# Duplicates: +# self.wait_for_link_text(link_text, timeout=None) +# self.wait_for_link_text_visible(link_text, timeout=None) +self.assert_link_text(link_text, timeout=None) +# Duplicates: self.assert_link(link_text, timeout=None) + +############ + +self.find_partial_link_text(partial_link_text, timeout=None) +# Duplicates: +# self.wait_for_partial_link_text(partial_link_text, timeout=None) +self.assert_partial_link_text(partial_link_text, timeout=None) + +############ + +self.wait_for_element_absent(selector, by="css selector", timeout=None) +# Duplicates: +# self.wait_for_element_not_present(selector, by="css selector", timeout=None) +self.assert_element_absent(selector, by="css selector", timeout=None) +# Duplicates: +# self.assert_element_not_present(selector, by="css selector", timeout=None) + +############ + +self.wait_for_element_clickable(selector, by="css selector", timeout=None) + +############ + +self.wait_for_element_not_visible(selector, by="css selector", timeout=None) +self.assert_element_not_visible(selector, by="css selector", timeout=None) + +############ + +self.wait_for_text_not_visible(text, selector="html", by="css selector", timeout=None) +self.wait_for_exact_text_not_visible(text, selector="html", by="css selector", timeout=None) +self.assert_text_not_visible(text, selector="html", by="css selector", timeout=None) +self.assert_exact_text_not_visible(text, selector="html", by="css selector", timeout=None) +self.assert_non_empty_text(selector="html", by="css selector", timeout=None) + +############ + +self.wait_for_attribute_not_present( + selector, attribute, value=None, by="css selector", timeout=None) +self.assert_attribute_not_present( + selector, attribute, value=None, by="css selector", timeout=None) + +############ + +self.accept_alert(timeout=None) +# Duplicates: self.wait_for_and_accept_alert(timeout=None) +self.dismiss_alert(timeout=None) +# Duplicates: self.wait_for_and_dismiss_alert(timeout=None) +self.switch_to_alert(timeout=None) +# Duplicates: self.wait_for_and_switch_to_alert(timeout=None) + +############ + +self.quit_extra_driver(driver=None) + +############ + +self.check_window(name="default", level=0, baseline=False, check_domain=True, full_diff=False) + +############ + +self.deferred_assert_element(selector, by="css selector", timeout=None, fs=False) +# Duplicates: +# self.delayed_assert_element( +# selector, by="css selector", timeout=None, fs=False) +self.deferred_assert_element_present(selector, by="css selector", timeout=None, fs=False) +# Duplicates: +# self.delayed_assert_element_present( +# selector, by="css selector", timeout=None, fs=False) +self.deferred_assert_text(text, selector="html", by="css selector", timeout=None, fs=False) +# Duplicates: +# self.delayed_assert_text( +# text, selector="html", by="css selector", timeout=None, fs=False) +self.deferred_assert_exact_text( + text, selector="html", by="css selector", timeout=None, fs=False) +# Duplicates: +# self.delayed_assert_exact_text( +# text, selector="html", by="css selector", timeout=None, fs=False) +self.deferred_assert_non_empty_text( + selector="html", by="css selector", timeout=None, fs=False) +# Duplicates: +# self.delayed_assert_non_empty_text( +# selector="html", by="css selector", timeout=None, fs=False) +self.deferred_check_window( + name="default", level=0, baseline=False, check_domain=True, full_diff=False, fs=False) +# Duplicates: +# self.delayed_check_window( +# name="default", level=0, baseline=False, +# check_domain=True, full_diff=False, fs=False) +self.process_deferred_asserts(print_only=False) +# Duplicates: self.process_delayed_asserts(print_only=False) + +############ + +self.fail(msg=None) # Inherited from "unittest" +self._check_browser() # Fails test cleanly if the active window is closed +self._print(TEXT) # Calls Python's print() / Allows for translations + +############ + +# **** UC Mode methods. (uc=True / --uc) **** + +# (Mainly for CDP Mode) - (For all CDP methods, see the CDP Mode Docs) + +self.activate_cdp_mode(url=None, **kwargs) # Activate CDP Mode on URL +self.reconnect(timeout=0.1) # disconnect() + sleep(timeout) + connect() +self.disconnect() # Stops the webdriver service to prevent detection +self.connect() # Starts the webdriver service to allow actions again + +# (For regular UC Mode) + +self.uc_open(url) # (Open in same tab with default reconnect_time) +self.uc_open_with_tab(url) # (New tab with default reconnect_time) +self.uc_open_with_reconnect(url, reconnect_time=None) # (New tab) +self.uc_open_with_disconnect(url, timeout=None) # New tab + sleep() +self.uc_click(selector) # A stealthy click for evading bot-detection +self.uc_gui_press_key(key) # Use PyAutoGUI to press the keyboard key +self.uc_gui_press_keys(keys) # Use PyAutoGUI to press a list of keys +self.uc_gui_write(text) # Similar to uc_gui_press_keys(), but faster +self.uc_gui_click_x_y(x, y, timeframe=0.25) # PyAutoGUI click screen +self.uc_gui_click_captcha(frame="iframe", retry=False, blind=False) +self.uc_gui_handle_captcha(frame="iframe") + +############ + +# "driver"-specific methods added (or modified) by SeleniumBase + +driver.default_get(url) # Because driver.get(url) works differently in UC Mode +driver.goto(url) # Like driver.get(), but allows partial URLs without protocol +driver.open(url) # Same as driver.goto(url) +driver.click(selector) +driver.click_link(link_text) +driver.click_if_visible(selector) +driver.click_active_element() +driver.send_keys(selector, text) +driver.press_keys(selector, text) +driver.type(selector, text) +driver.submit(selector) +driver.assert_element(selector) +driver.assert_element_present(selector) +driver.assert_element_not_visible(selector) +driver.assert_text(text, selector) +driver.assert_exact_text(text, selector) +driver.find_element(selector) +driver.find_elements(selector) +driver.select(selector) +driver.select_all(selector) +driver.wait_for_element(selector) +driver.wait_for_element_visible(selector) +driver.wait_for_element_present(selector) +driver.wait_for_selector(selector) +driver.wait_for_text(text, selector) +driver.wait_for_exact_text(text, selector) +driver.wait_for_and_accept_alert() +driver.wait_for_and_dismiss_alert() +driver.is_element_present(selector) +driver.is_element_visible(selector) +driver.is_text_visible(text, selector) +driver.is_exact_text_visible(text, selector) +driver.is_attribute_present(selector, attribute) +driver.get_text(selector="body") +driver.js_click(selector) +driver.get_active_element_css() +driver.get_locale_code() +driver.get_origin() +driver.get_user_agent() +driver.highlight(selector) +driver.highlight_click(selector) +driver.highlight_if_visible(selector) +driver.sleep(seconds) +driver.locator(selector) +driver.get_attribute(selector, attribute) +driver.get_parent(element) +driver.get_current_url() +driver.get_page_source() +driver.get_title() +driver.switch_to_newest_tab() +driver.switch_to_frame(frame="iframe") +driver.is_cdp_mode_active() +driver.is_connected() # UC / CDP Mode can disconnect WebDriver + +# "driver"-specific methods added (or modified) by SeleniumBase for UC Mode: + +driver.get(url) # If UC Mode and site detects bots, then uc_open_with_tab(url) +driver.uc_open(url) # (Open in same tab with default reconnect_time) +driver.uc_open_with_tab(url) # (New tab with default reconnect_time) +driver.uc_open_with_reconnect(url, reconnect_time=None) # (New tab) +driver.uc_open_with_disconnect(url, timeout=None) # New tab + sleep() +driver.uc_activate_cdp_mode(url=None, **kwargs) # Activate CDP Mode on URL +driver.reconnect(timeout=0.1) # disconnect() + sleep(timeout) + connect() +driver.disconnect() # Stops the webdriver service to prevent detection +driver.connect() # Starts the webdriver service to allow actions again +driver.uc_click(selector) # A stealthy click for evading bot-detection +driver.uc_gui_press_key(key) # Use PyAutoGUI to press the keyboard key +driver.uc_gui_press_keys(keys) # Use PyAutoGUI to press a list of keys +driver.uc_gui_write(text) # Similar to uc_gui_press_keys(), but faster +driver.uc_gui_click_x_y(x, y, timeframe=0.25) # PyAutoGUI click screen +driver.uc_gui_click_captcha(frame="iframe", retry=False, blind=False) +driver.uc_gui_handle_captcha(frame="iframe") +``` -self.delayed_assert_text(text, selector="html", by=By.CSS_SELECTOR, - timeout=settings.MINI_TIMEOUT) +-------- -self.process_delayed_asserts() -``` +

      ๐Ÿ”ต Examples

      ---- +โœ… Test Folder: [SeleniumBase/examples](https://github.com/seleniumbase/SeleniumBase/tree/master/examples) -Example Test: (From [my_first_test.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/my_first_test.py)) +* [my_first_test.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/my_first_test.py) +* [test_demo_site.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_demo_site.py) +* [test_coffee_cart.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_coffee_cart.py) +* [coffee_cart_tests.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/coffee_cart_tests.py) +* [parameterized_test.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/parameterized_test.py) +* [test_deferred_asserts.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_deferred_asserts.py) +* [test_error_page.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_error_page.py) +* [test_login.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_login.py) +* [test_markers.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_markers.py) +* [test_swag_labs.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_swag_labs.py) +* [test_simple_login.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_simple_login.py) +* [test_suite.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_suite.py) +* [test_tinymce.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_tinymce.py) +* And many more... -![](https://cdn2.hubspot.net/hubfs/100006/images/SampleCode2.png "SeleniumBase Python Code") +[](https://github.com/seleniumbase/SeleniumBase/blob/master/README.md) diff --git a/help_docs/mobile_testing.md b/help_docs/mobile_testing.md new file mode 100644 index 00000000000..36c6c1831bb --- /dev/null +++ b/help_docs/mobile_testing.md @@ -0,0 +1,81 @@ + + +

      Mobile Mode / Mobile Testing

      + +Use `--mobile` to run SeleniumBase tests using Chrome's mobile device emulator with default values for Device Metrics and User-Agent. + +Here's an example mobile test: + +[SeleniumBase/examples/test_roblox_mobile.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_roblox_mobile.py) + +```zsh +pytest test_roblox_mobile.py --mobile +``` + +To configure Device Metrics, use: + +```zsh +--metrics="CSS_Width,CSS_Height,Pixel_Ratio" +``` + +To configure the User-Agent, use: + +```zsh +--agent="USER-AGENT-STRING" +``` + +To find real values for Device Metrics, see: + +* [Device Metrics List](https://gist.github.com/sidferreira/3f5fad525e99b395d8bd882ee0fd9d00) + +To find real User-Agent strings, see: + +* [User Agent Strings List](https://developers.whatismybrowser.com/useragents/explore/) + +-------- + +Here's another example of a mobile test: + +[SeleniumBase/examples/test_swag_labs.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_swag_labs.py) + +```zsh +pytest test_swag_labs.py --mobile +``` + +SeleniumBase Mobile Testing + +Here's an example of configuring mobile settings for that test: + +```zsh +# Run tests using Chrome's mobile device emulator (default settings) +pytest test_swag_labs.py --mobile + +# Run mobile tests specifying CSS Width, CSS Height, and Pixel-Ratio +pytest test_swag_labs.py --mobile --metrics="360,640,2" + +# Run mobile tests specifying the user agent +pytest test_swag_labs.py --mobile --agent="Mozilla/5.0 (Linux; Android 9; Pixel 3 XL)" +``` + +-------- + +For some [SeleniumBase Syntax Formats](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/syntax_formats.md), you can also use `mobile=True` to run tests in Mobile Mode: + +```python +from seleniumbase import Driver + +driver = Driver(mobile=True) +try: + driver.goto("https://www.roblox.com/") + driver.assert_element("#download-the-app-container") + driver.assert_text("Roblox for Android") + driver.highlight('span:contains("Roblox for Android")', loops=8) + driver.highlight('span:contains("Continue in App")', loops=8) +finally: + driver.quit() +``` + +-------- + +

      diff --git a/help_docs/mysql_installation.md b/help_docs/mysql_installation.md old mode 100755 new mode 100644 index d9008f79fca..fc82c8314e7 --- a/help_docs/mysql_installation.md +++ b/help_docs/mysql_installation.md @@ -1,54 +1,94 @@ -### MySQL Installation Instructions + +## MySQL Installation Instructions -#### [MySQL](http://www.mysql.com/) (OPTIONAL) -(NOTE: If you're using this test framework from a local development machine and don't plan on writing to a MySQL DB from your local test runs, you can skip this step.) +### [MySQL](http://www.mysql.com/) (OPTIONAL) -##### macOS: -```bash -brew install MySQL +(NOTE: If you don't plan on using the SeleniumBase MySQL DB feature, then you can skip this section.) + +-------- + +### GitHub Actions Ubuntu Linux MySQL Setup: + +```zsh +sudo /etc/init.d/mysql start +mysql -e 'CREATE DATABASE IF NOT EXISTS test_db;' -uroot -proot +wget https://raw.githubusercontent.com/seleniumbase/SeleniumBase/master/seleniumbase/core/create_db_tables.sql +sudo mysql -h 127.0.0.1 -uroot -proot test_db < create_db_tables.sql +sudo mysql -e 'ALTER USER "root"@"localhost" IDENTIFIED BY "test";' -uroot -proot +sudo service mysql restart ``` -##### Windows: -[Download MySQL here](http://dev.mysql.com/downloads/windows/) +Have SeleniumBase tests write to the MySQL DB: -That installs the MySQL library so that you can use database commands in your code. To make that useful, you'll want to have a MySQL DB that you can connect to. +```zsh +pytest --with-db_reporting +``` -#### Install the MySQL-Python connector +Query MySQL DB Results: -```bash -pip install MySQL-python==1.2.5 +```zsh +mysql -e 'select test_address,browser,state,start_time,runtime from test_db.test_run_data;' -uroot -ptest ``` -#### Access your MySQL DB +-------- -If you want a visual tool to help make your MySQL life easier, [try MySQL Workbench](http://dev.mysql.com/downloads/workbench/). +### Standard Ubuntu Linux MySQL Setup: -#### Prepare your MySQL DB +```zsh +sudo apt update +sudo apt install mysql-server +sudo mysql_secure_installation +sudo mysql -e 'CREATE DATABASE IF NOT EXISTS test_db;' +sudo mysql -h 127.0.0.1 -u root test_db < seleniumbase/core/create_db_tables.sql +sudo service mysql restart +``` -You can use the [testcaserepository.sql](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/core/testcaserepository.sql) file to create the necessary tables for storing test data. +To change the password from `root` to `test`: -#### Configure your MySQL DB for SeleniumBase +```zsh +mysqladmin -u root -p'root' password 'test' +sudo service mysql restart +``` -You'll want to update your [settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py) file with your MySQL DB credentials so that tests can write to the database when they run. +### MacOS MySQL Setup: + +```zsh +brew install mysql +``` -#### Allow tests to write to your MySQL database +Then start the MySQL service: -Add the ``--with-db_reporting`` argument on the command line when you want tests to write to your MySQL database. -Example: -```bash -pytest my_first_test.py --with-db_reporting +```zsh +brew services start mysql ``` -#### Windows mysql-python troubleshooting: +(Continue with additional steps below to set up your DB.) -If you're having trouble with Windows mysql-python installation using pip, you can also try the following steps to install from an alternative source: +### Windows MySQL Setup: -* Download the unofficial ``.whl`` format of MySQL-Python and Mysqlclient from [here](http://www.lfd.uci.edu/~gohlke/pythonlibs/#mysql-python). +[Download MySQL here](http://dev.mysql.com/downloads/windows/) +Follow the steps from the MySQL Downloads page. + +(Continue with additional steps below to set up your DB.) + +### Access your MySQL DB: + +If you want a visual tool to help make your MySQL life easier, [try MySQL Workbench](http://dev.mysql.com/downloads/workbench/). -* Open a console and then cd to where you've downloaded the MySQL-Python .whl file. +### Prepare your MySQL DB: -* Run the command ``pip install FILENAME.whl`` +Use the [create_db_tables.sql](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/core/create_db_tables.sql) file to create the necessary tables for storing test data. -* If pip.exe is not recognized, you may find it in the "Scripts" directory from where python has been installed. +### Configure your MySQL DB for SeleniumBase: + +Update your [settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py) file with your MySQL DB credentials so that tests can write to the database when they run. + +### Have SeleniumBase tests write to your MySQL DB: + +Add the `--with-db_reporting` argument on the command-line when you want tests to write to your MySQL database. Example: + +```zsh +pytest --with-db_reporting +``` diff --git a/help_docs/recorder_mode.md b/help_docs/recorder_mode.md new file mode 100644 index 00000000000..49851057cc5 --- /dev/null +++ b/help_docs/recorder_mode.md @@ -0,0 +1,136 @@ + + +

      Recorder Mode ๐Ÿ”ด/โบ๏ธ

      + + +

      (Watch the tutorial on YouTube)

      + +๐Ÿ”ด SeleniumBase Recorder Mode lets you record & export browser actions into test automation scripts.
      + +โบ๏ธ Recorder Mode can be activated from the command-line interface or the desktop Recorder App. To launch the desktop app, run: `sbase recorder`: + + + +โบ๏ธ To make a new recording from the command-line interface, use `sbase mkrec`: + +```zsh +sbase mkrec TEST_NAME.py --url=URL +``` + +If the file already exists, you'll get an error. If no URL is provided, you'll start on a blank page and will need to navigate somewhere for the Recorder to activate. (The Recorder captures events on URLs that start with `https`, `http`, or `file`.) The command above runs an empty test that stops at a breakpoint so that you can perform manual browser actions for the Recorder. When you have finished recording, type "`c`" on the command-line and press `[ENTER]` to continue from the breakpoint. The test will complete and a file called `TEST_NAME_rec.py` will be automatically created in the `./recordings` folder. That file will get copied back to the original folder with the name you gave it. (For headed Linux machines, add `--gui` to prevent the default headless mode on Linux.) + +Example: + +```zsh +sbase mkrec new_test.py --url=wikipedia.org + +* RECORDING initialized: new_test.py + +pytest new_test.py --rec -q -s --url=wikipedia.org + +>>>>>>>>>>>>>>>>>> PDB set_trace >>>>>>>>>>>>>>>>> + +> PATH_TO_YOUR_CURRENT_DIRECTORY/new_test.py(9) + . + 5 def test_recording(self): + 6 if self.recorder_ext: + 7 # When done recording actions, + 8 # type "c", and press [Enter]. + 9 -> breakpoint() + return None +(Pdb+) c + +>>>>>>>>>>>>>>>>>> PDB continue >>>>>>>>>>>>>>>>>> + +>>> RECORDING SAVED as: recordings/new_test_rec.py +************************************************** + +*** RECORDING COPIED to: new_test.py +``` + +๐Ÿ”ด You can also activate Recorder Mode from the Recorder Desktop App: + +```zsh +sbase recorder +* Starting the SeleniumBase Recorder Desktop App... +``` + + + +โบ๏ธ While a recording is in progress, you can press the `[ESC]` key to pause the Recorder. To resume the recording, you can hit the `[~`]` key, which is located directly below the `[ESC]` key on most keyboards. + +โบ๏ธ From within Recorder Mode there are two additional modes: "Assert Element Mode" and "Assert Text Mode". To switch into "Assert Element Mode", press the `[^]-key (SHIFT+6 on standard QWERTY keyboards)`: The border will become purple, and you'll be able to click on elements to assert from your test. To switch into "Assert Text Mode", press the `[&]-key (SHIFT+7 on standard QWERTY keyboards)`: The border will become teal, and you'll be able to click on elements for asserting text from your test. + +โบ๏ธ While using either of the two special Assertion Modes, certain actions such as clicking on links won't have any effect. This lets you make assertions on elements without navigating away from the page, etc. To add an assertion for buttons without triggering default "click" behavior, mouse-down on the button and then mouse-up somewhere else. (This prevents a detected click while still recording the assert.) To return back to the original Recorder Mode, press any key other than `[SHIFT]` or `[BACKSPACE]` (Eg: Press `[CONTROL]`, etc.). Press `[ESC]` once to leave the Assertion Modes, but it'll stop the Recorder if you press it again. + +โบ๏ธ For extra flexibility, the `sbase mkrec` command can be split into four separate commands: + +```zsh +sbase mkfile TEST_NAME.py --rec + +pytest TEST_NAME.py --rec -q -s + +sbase print ./recordings/TEST_NAME_rec.py -n + +cp ./recordings/TEST_NAME_rec.py ./TEST_NAME.py +``` + +The first command creates a boilerplate test with a breakpoint; the second command runs the test with the Recorder activated; the third command prints the completed test to the console; and the fourth command replaces the initial boilerplate with the completed test. If you're just experimenting with the Recorder, you can run the second command as many times as you want, and it'll override previous recordings saved to `./recordings/TEST_NAME_rec.py`. (Note that `-s` is needed to allow breakpoints, unless you already have a `pytest.ini` file present where you set it. The `-q` is optional, which shortens `pytest` console output.) + +โบ๏ธ You can also use the Recorder to add code to an existing test. To do that, you'll first need to create a breakpoint in your code to insert manual browser actions: + +```python +breakpoint() +``` + +Now you'll be able to run your test with `pytest`, and it will stop at the breakpoint for you to add in actions: (Press `c` and `ENTER` on the command-line to continue from the breakpoint.) + +```zsh +pytest TEST_NAME.py --rec -s +``` + +โบ๏ธ You can also set a breakpoint at the start of your test by adding `--trace` as a `pytest` command-line option: (This is useful when running Recorder Mode without any `pdb` breakpoints.) + +```zsh +pytest TEST_NAME.py --trace --rec -s +``` + +โบ๏ธ After the test completes, a file called `TEST_NAME_rec.py` will be automatically created in the `./recordings` folder, which will include the actions performed by the test, and the manual actions that you added in. + +โบ๏ธ Here's a command-line notification for a completed recording: + +```zsh +>>> RECORDING SAVED as: recordings/TEST_NAME_rec.py +*************************************************** +``` + +โบ๏ธ When running additional tests from the same Python module, Recordings will get added to the file that was created from the first test: + +```zsh +>>> RECORDING ADDED to: recordings/TEST_NAME_rec.py +*************************************************** +``` + +โบ๏ธ Recorder Mode works by saving your recorded actions into the browser's sessionStorage. SeleniumBase then reads from the browser's sessionStorage to take the raw data and generate a full test from it. Keep in mind that sessionStorage is only present while the browser tab remains in the same domain/origin. (The sessionStorage of that tab goes away if you leave that domain/origin.) To compensate, links to web pages of different domain/origin will automatically open a new tab for you in Recorder Mode. + +โบ๏ธ Additionally, the SeleniumBase self.goto(URL) method will also open a new tab for you in Recorder Mode if the domain/origin is different from the current URL. If you need to navigate to a different domain/origin from within the same tab, call self.save_recorded_actions() first, which saves the recorded data for later. When a recorded test completes, SeleniumBase scans the sessionStorage data of all open browser tabs for generating the completed script. + +โบ๏ธ As an alternative to activating Recorder Mode with the \-\-rec command-line arg, you can also call self.activate_recorder() from your tests. Using the Recorder this way is only useful for tests that stay on the same URL. This is because the standard Recorder Mode functions as a Chrome extension and persists wherever the browser goes. (This version only stays on the page where called.) + +โบ๏ธ (Note that same domain/origin is not the same as same URL. Example: https://xkcd.com/353 and https://xkcd.com/1537 are two different URLs with the same domain/origin. That means both URLs share the same sessionStorage, and that changes persist to different URLs of the same domain/origin. If you want to find out a website's origin during a test, just call: self.get_origin(), which returns the value of window.location.origin from the browser's console.) + +โบ๏ธ Inside recorded tests, you might find the self.goto_if_not_url(url) method, which opens the URL given if the browser is not currently on that page. SeleniumBase uses this method in recorded scripts when the Recorder detects that a browser action changed the current URL. This method prevents an unnecessary page load and shows what page the test visited after a browser action. + +โบ๏ธ By launching the Recorder App with sbase recorder --ee, you can end the recording by pressing {SHIFT+ESC} instead of the usual way of ending the recording by typing c from a breakpoint() and pressing Enter. Those buttons don't need to be pressed at the same time, but SHIFT must be pressed directly before ESC. + +โบ๏ธ Use sbase recorder --uc to launch the Recorder App with UC Mode enabled. (The driver will be disconnected from Chrome, but the Recorder extension will still capture any browser actions.) + +-------- + +
      To learn more about SeleniumBase, check out the Docs Site:
      + +SeleniumBase.io Docs + +
      All the code is on GitHub:
      + +SeleniumBase on GitHub diff --git a/help_docs/shadow_dom.md b/help_docs/shadow_dom.md new file mode 100644 index 00000000000..32f7fc80fae --- /dev/null +++ b/help_docs/shadow_dom.md @@ -0,0 +1,30 @@ + + +

      SeleniumBase

      + +## Shadow DOM support / Shadow-root interaction + +๐Ÿ”ต SeleniumBase lets you pierce through open Shadow DOM selectors (to interact with elements inside) by adding `::shadow` to CSS fragments that include a shadow-root element. For multi-layered shadow-roots, you must individually pierce through each shadow-root element that you want to get through. + +๐Ÿ”ต Here are some examples of Shadow DOM selectors: + +```python +css_1 = "downloads-manager::shadow #no-downloads" + +css_2 = "downloads-manager::shadow #downloadsList downloads-item::shadow #file-link" + +css_3 = "downloads-manager::shadow downloads-toolbar::shadow cr-toolbar::shadow cr-toolbar-search-field::shadow cr-icon-button" + +css_4 = "downloads-manager::shadow downloads-toolbar::shadow cr-toolbar::shadow cr-toolbar-search-field::shadow #searchInput" + +css_5 = "downloads-manager::shadow downloads-toolbar::shadow cr-toolbar::shadow cr-toolbar-search-field::shadow #clearSearch" +``` + +๐Ÿ”ต The shadow-root (`::shadow`) elements are transitional, and therefore cannot be the final part of your CSS selectors. Complete your CSS selectors by including an element that's inside a shadow-root. + +๐Ÿ”ต NOTE: `::shadow` selectors only exist within SeleniumBase. (They are not part of standard CSS.) + +๐Ÿ”ต Here are some examples of tests that interact with Shadow DOM elements: +* [examples/shadow_root_test.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/shadow_root_test.py) +* [examples/test_shadow_dom.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_shadow_dom.py) +* [examples/old_wordle_script.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/old_wordle_script.py) diff --git a/help_docs/syntax_formats.md b/help_docs/syntax_formats.md new file mode 100644 index 00000000000..c9c48db81d8 --- /dev/null +++ b/help_docs/syntax_formats.md @@ -0,0 +1,1005 @@ + + + + +

      The 25 Syntax Formats / Design Patterns

      + +

      ๐Ÿ”  SeleniumBase supports multiple ways of structuring tests:

      + +
      +

      + +
      + +-------- + + +

      1. BaseCase direct class inheritance

      + +In this format, (which is used by most of the tests in the SeleniumBase examples folder), BaseCase is imported at the top of a Python file, followed by a Python class inheriting BaseCase. Then, any test method defined in that class automatically gains access to SeleniumBase methods, including the setUp() and tearDown() methods that are automatically called for opening and closing web browsers at the start and end of tests. + +To run a test of this format, use **`pytest`** or `pynose`. Adding `BaseCase.main(__name__, __file__)` enables `python` to run `pytest` on your file indirectly. Here's an example: + +```python +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + +class MyTestClass(BaseCase): + def test_demo_site(self): + self.goto("https://seleniumbase.io/demo_page") + self.type("#myTextInput", "This is Automated") + self.click("#myButton") + self.assert_element("tbody#tbodyId") + self.assert_text("Automation Practice", "h3") + self.click_link("SeleniumBase Demo Page") + self.assert_exact_text("Demo Page", "h1") + self.assert_no_js_errors() +``` + +(See examples/test_demo_site.py for the full test.) + +Using `BaseCase` inheritance is a great starting point for anyone learning SeleniumBase, and it follows good object-oriented programming principles. + + +

      2. BaseCase subclass inheritance

      + +There are situations where you may want to customize the setUp and tearDown of your tests. Maybe you want to have all your tests login to a specific web site first, or maybe you want to have your tests report results through an API call depending on whether a test passed or failed. This can be done by creating a subclass of BaseCase and then carefully creating custom setUp() and tearDown() methods that don't overwrite the critical functionality of the default SeleniumBase setUp() and tearDown() methods. Afterwards, your test classes will inherit the subclass of BaseCase with the added functionality, rather than directly inheriting BaseCase itself. Here's an example of that: + +```python +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + +class BaseTestCase(BaseCase): + def setUp(self): + super().setUp() + # <<< Run custom setUp() code for tests AFTER the super().setUp() >>> + + def tearDown(self): + self.save_teardown_screenshot() # On failure or "--screenshot" + if self.has_exception(): + # <<< Run custom code if the test failed. >>> + pass + else: + # <<< Run custom code if the test passed. >>> + pass + # (Wrap unreliable tearDown() code in a try/except block.) + # <<< Run custom tearDown() code BEFORE the super().tearDown() >>> + super().tearDown() + + def login(self): + # <<< Placeholder. Add your code here. >>> + # Reduce duplicate code in tests by having reusable methods like this. + # If the UI changes, the fix can be applied in one place. + pass + + def example_method(self): + # <<< Placeholder. Add your code here. >>> + pass + +class MyTests(BaseTestCase): + def test_example(self): + self.login() + self.example_method() + self.type("input", "Name") + self.click("form button") + # ... +``` + +(See examples/boilerplates/base_test_case.py for more info.) + + +

      3. The "sb" pytest fixture (no class)

      + +The pytest framework comes with a unique system called fixtures, which replaces import statements at the top of Python files by importing libraries directly into test definitions. More than just being an import, a pytest fixture can also automatically call predefined setUp() and tearDown() methods at the beginning and end of test methods. To work, sb is added as an argument to each test method definition that needs SeleniumBase functionality. This means you no longer need import statements in your Python files to use SeleniumBase. If using other pytest fixtures in your tests, you may need to use the SeleniumBase fixture (instead of BaseCase class inheritance) for compatibility reasons. Here's an example of the sb fixture in a test that does not use Python classes: + +```python +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + +def test_sb_fixture_with_no_class(sb: BaseCase): + sb.goto("seleniumbase.io/help_docs/install/") + sb.type('input[aria-label="Search"]', "GUI Commander") + sb.click('mark:contains("Commander")') + sb.assert_title_contains("GUI / Commander") +``` + +(See the top of examples/test_sb_fixture.py for the test.) + + +

      4. The "sb" pytest fixture (in class)

      + +The sb pytest fixture can also be used inside of a class. There is a slight change to the syntax because that means test methods must also include self in their argument definitions when test methods are defined. (The self argument represents the class object, and is used in every test method that lives inside of a class.) Once again, no import statements are needed in your Python files for this to work. Here's an example of using the sb fixture in a test method that lives inside of a Python class: + +```python +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + +class Test_SB_Fixture: + def test_sb_fixture_inside_class(self, sb: BaseCase): + sb.goto("seleniumbase.io/help_docs/install/") + sb.type('input[aria-label="Search"]', "GUI Commander") + sb.click('mark:contains("Commander")') + sb.assert_title_contains("GUI / Commander") +``` + +(See the bottom of examples/test_sb_fixture.py for the test.) + + +

      5. Page Object Model with BaseCase

      + +With SeleniumBase, you can use Page Objects to break out code from tests, but remember, the self variable (from test methods that inherit BaseCase) contains the driver and all other framework-specific variable definitions. Therefore, that self must be passed as an arg into any outside class method in order to call SeleniumBase methods from there. In the example below, the self variable from the test method is passed into the sb arg of the Page Object class method because the self arg of the Page Object class method is already being used for its own class. Every Python class method definition must include the self as the first arg. + +```python +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + +class LoginPage: + def login_to_swag_labs(self, sb: BaseCase, username): + sb.goto("https://www.saucedemo.com") + sb.type("#user-name", username) + sb.type("#password", "secret_sauce") + sb.click('input[type="submit"]') + +class MyTests(BaseCase): + def test_swag_labs_login(self): + LoginPage().login_to_swag_labs(self, "standard_user") + self.assert_element("div.inventory_list") + self.assert_element('div:contains("Sauce Labs Backpack")') +``` + +(See examples/boilerplates/samples/swag_labs_test.py for the full test.) + + +

      6. Page Object Model with the "sb" fixture

      + +This is similar to the classic Page Object Model with BaseCase inheritance, except that this time we pass the sb pytest fixture from the test into the sb arg of the page object class method, (instead of passing self). Now that you're using sb as a pytest fixture, you no longer need to import BaseCase anywhere in your code. See the example below: + +```python +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + +class LoginPage: + def login_to_swag_labs(self, sb: BaseCase, username): + sb.goto("https://www.saucedemo.com") + sb.type("#user-name", username) + sb.type("#password", "secret_sauce") + sb.click('input[type="submit"]') + +class MyTests: + def test_swag_labs_login(self, sb: BaseCase): + LoginPage().login_to_swag_labs(sb, "standard_user") + sb.assert_element("div.inventory_list") + sb.assert_element('div:contains("Sauce Labs Backpack")') + sb.js_click("a#logout_sidebar_link") + sb.assert_element("div#login_button_container") +``` + +(See examples/boilerplates/samples/sb_swag_test.py for the full test.) + + +

      7. Using "request" to get "sb" (no class)

      + +The pytest request fixture can be used to retrieve other pytest fixtures from within tests, such as the sb fixture. This allows you to have more control over when fixtures get initialized because the fixture no longer needs to be loaded at the very beginning of test methods. This is done by calling request.getfixturevalue('sb') from the test. Here's an example of using the pytest request fixture to load the sb fixture in a test method that does not use Python classes: + +```python +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + +def test_request_sb_fixture(request): + sb: BaseCase = request.getfixturevalue("sb") + sb.goto("https://seleniumbase.io/demo_page") + sb.assert_text("SeleniumBase", "#myForm h2") + sb.assert_element("input#myTextInput") + sb.type("#myTextarea", "This is me") + sb.click("#myButton") + sb.tearDown() +``` + +(See the top of examples/test_request_sb_fixture.py for the test.) + + +

      8. Using "request" to get "sb" (in class)

      + +The pytest request fixture can also be used to get the sb fixture from inside a Python class. Here's an example of that: + +```python +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + +class Test_Request_Fixture: + def test_request_sb_fixture_in_class(self, request): + sb: BaseCase = request.getfixturevalue("sb") + sb.goto("https://seleniumbase.io/demo_page") + sb.assert_element("input#myTextInput") + sb.type("#myTextarea", "Automated") + sb.assert_text("This Text is Green", "#pText") + sb.click("#myButton") + sb.assert_text("This Text is Purple", "#pText") + sb.tearDown() +``` + +(See the bottom of examples/test_request_sb_fixture.py for the test.) + + +

      9. Overriding the driver via BaseCase

      + +When you want to use SeleniumBase methods via BaseCase, but you want total freedom to control how you spin up your web browsers, this is the format you want. Although SeleniumBase gives you plenty of command-line options to change how your browsers are launched, this format gives you more control when the existing options aren't enough. Here's an example of that: + +```python +from selenium import webdriver +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + +class OverrideDriverTest(BaseCase): + def get_new_driver(self, *args, **kwargs): + """This method overrides get_new_driver() from BaseCase.""" + options = webdriver.ChromeOptions() + options.add_argument("--disable-3d-apis") + options.add_argument("--disable-notifications") + if self.headless: + options.add_argument("--headless=new") + options.add_argument("--disable-gpu") + options.add_experimental_option( + "excludeSwitches", ["enable-automation", "enable-logging"], + ) + prefs = { + "credentials_enable_service": False, + "profile.password_manager_enabled": False, + } + options.add_experimental_option("prefs", prefs) + return webdriver.Chrome(options=options) + + def test_simple(self): + self.goto("https://seleniumbase.io/demo_page") + self.assert_text("Demo Page", "h1") +``` + +(From examples/test_override_driver.py) + + +

      10. Overriding the driver via "sb" fixture

      + +When you want to use SeleniumBase methods via the `sb` pytest fixture, but you want total freedom to control how you spin up your web browsers, this is the format you want. Although SeleniumBase gives you plenty of command-line options to change how your browsers are launched, this format gives you more control when the existing options aren't enough. + +```python +"""Overriding the "sb" fixture to override the driver.""" +import pytest +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + +@pytest.fixture() +def sb(request): + from selenium import webdriver + from seleniumbase import BaseCase + from seleniumbase import config as sb_config + from seleniumbase.core import session_helper + + class BaseClass(BaseCase): + def get_new_driver(self, *args, **kwargs): + """This method overrides get_new_driver() from BaseCase.""" + options = webdriver.ChromeOptions() + if self.headless: + options.add_argument("--headless=new") + options.add_argument("--disable-gpu") + options.add_experimental_option( + "excludeSwitches", ["enable-automation"], + ) + return webdriver.Chrome(options=options) + + def setUp(self): + super().setUp() + + def base_method(self): + pass + + def tearDown(self): + self.save_teardown_screenshot() # On failure or "--screenshot" + super().tearDown() + + if request.cls: + if sb_config.reuse_class_session: + the_class = str(request.cls).split(".")[-1].split("'")[0] + if the_class != sb_config._sb_class: + session_helper.end_reused_class_session_as_needed() + sb_config._sb_class = the_class + request.cls.sb = BaseClass("base_method") + request.cls.sb.setUp() + request.cls.sb._needs_tearDown = True + request.cls.sb._using_sb_fixture = True + request.cls.sb._using_sb_fixture_class = True + sb_config._sb_node[request.node.nodeid] = request.cls.sb + yield request.cls.sb + if request.cls.sb._needs_tearDown: + request.cls.sb.tearDown() + request.cls.sb._needs_tearDown = False + else: + sb = BaseClass("base_method") + sb.setUp() + sb._needs_tearDown = True + sb._using_sb_fixture = True + sb._using_sb_fixture_no_class = True + sb_config._sb_node[request.node.nodeid] = sb + yield sb + if sb._needs_tearDown: + sb.tearDown() + sb._needs_tearDown = False + +def test_override_fixture_no_class(sb: BaseCase): + sb.goto("https://seleniumbase.io/demo_page") + sb.type("#myTextInput", "This is Automated") + +class TestOverride: + def test_override_fixture_inside_class(self, sb: BaseCase): + sb.goto("https://seleniumbase.io/demo_page") + sb.type("#myTextInput", "This is Automated") +``` + +(From examples/test_override_sb_fixture.py) + + +

      11. BaseCase with Chinese translations

      + +This format is similar to the English version with BaseCase inheritance, but there's a different import statement, and method names have been translated into Chinese. Here's an example of that: + +```python +from seleniumbase.translate.chinese import ็ก’ๆต‹่ฏ•็”จไพ‹ +็ก’ๆต‹่ฏ•็”จไพ‹.main(__name__, __file__) + +class ๆˆ‘็š„ๆต‹่ฏ•็ฑป(็ก’ๆต‹่ฏ•็”จไพ‹): + def test_ไพ‹ๅญ1(self): + self.ๅผ€ๅฏ("https://zh.wikipedia.org/wiki/") + self.ๆ–ญ่จ€ๆ ‡้ข˜("็ปดๅŸบ็™พ็ง‘๏ผŒ่‡ช็”ฑ็š„็™พ็ง‘ๅ…จไนฆ") + self.ๆ–ญ่จ€ๅ…ƒ็ด ('a[title="Wikipedia:ๅ…ณไบŽ"]') + self.ๅฆ‚ๆžœๅฏ่ง่ฏทๅ•ๅ‡ป('button[aria-label="ๅ…ณ้—ญ"]') + self.ๅฆ‚ๆžœๅฏ่ง่ฏทๅ•ๅ‡ป('button[aria-label="้—œ้–‰"]') + self.ๆ–ญ่จ€ๅ…ƒ็ด ('span:contains("ๅˆ›ๅปบ่ดฆๅท")') + self.ๆ–ญ่จ€ๅ…ƒ็ด ('span:contains("็™ปๅฝ•")') + self.่พ“ๅ…ฅๆ–‡ๆœฌ('input[name="search"]', "่ˆž้พ") + self.ๅ•ๅ‡ป('button:contains("ๆœ็ดข")') + self.ๆ–ญ่จ€ๆ–‡ๆœฌ("่ˆž้พ", "#firstHeading") + self.ๆ–ญ่จ€ๅ…ƒ็ด ('img[src*="Chinese_draak.jpg"]') + self.ๅ›žๅŽป() + self.่พ“ๅ…ฅๆ–‡ๆœฌ('input[name="search"]', "้บปๅฉ†่ฑ†่…") + self.ๅ•ๅ‡ป('button:contains("ๆœ็ดข")') + self.ๆ–ญ่จ€ๆ–‡ๆœฌ("้บปๅฉ†่ฑ†่…", "#firstHeading") + self.ๆ–ญ่จ€ๅ…ƒ็ด ('figure:contains("ไธ€ๅฎถไธญ้ค้คจ็š„้บปๅฉ†่ฑ†่…")') + self.ๅ›žๅŽป() + self.่พ“ๅ…ฅๆ–‡ๆœฌ('input[name="search"]', "็ฒพๆญฆ่‹ฑ้›„") + self.ๅ•ๅ‡ป('button:contains("ๆœ็ดข")') + self.ๆ–ญ่จ€ๅ…ƒ็ด ('img[src*="Fist_of_legend.jpg"]') + self.ๆ–ญ่จ€ๆ–‡ๆœฌ("ๆŽ่ฟžๆฐ", 'li a[title="ๆŽ่ฟžๆฐ"]') +``` + +(See examples/translations/chinese_test_1.py for the Chinese test.) + + +

      12. BaseCase with Dutch translations

      + +This format is similar to the English version with BaseCase inheritance, but there's a different import statement, and method names have been translated into Dutch. Here's an example of that: + +```python +from seleniumbase.translate.dutch import Testgeval +Testgeval.main(__name__, __file__) + +class MijnTestklasse(Testgeval): + def test_voorbeeld_1(self): + self.openen("https://nl.wikipedia.org/wiki/Hoofdpagina") + self.controleren_element('a[title*="Welkom voor nieuwkomers"]') + self.controleren_tekst("Welkom op Wikipedia", "td.hp-welkom") + self.typ("#searchform input", "Stroopwafel") + self.klik("#searchform button") + self.controleren_tekst("Stroopwafel", "#firstHeading") + self.controleren_element('img[src*="Stroopwafels"]') + self.typ("#searchform input", "Rijksmuseum Amsterdam") + self.klik("#searchform button") + self.controleren_tekst("Rijksmuseum", "#firstHeading") + self.controleren_element('img[src*="Rijksmuseum"]') + self.terug() + self.controleren_url_bevat("Stroopwafel") + self.vooruit() + self.controleren_url_bevat("Rijksmuseum") +``` + +(See examples/translations/dutch_test_1.py for the Dutch test.) + + +

      13. BaseCase with French translations

      + +This format is similar to the English version with BaseCase inheritance, but there's a different import statement, and method names have been translated into French. Here's an example of that: + +```python +from seleniumbase.translate.french import CasDeBase +CasDeBase.main(__name__, __file__) + +class MaClasseDeTest(CasDeBase): + def test_exemple_1(self): + self.ouvrir("https://fr.wikipedia.org/wiki/") + self.vรฉrifier_texte("Wikipรฉdia") + self.vรฉrifier_รฉlรฉment('[alt="Wikipรฉdia"]') + self.cliquer_si_affichรฉ('button[aria-label="Fermer"]') + self.js_taper("#searchform input", "Crรจme brรปlรฉe") + self.cliquer("#searchform button") + self.vรฉrifier_texte("Crรจme brรปlรฉe", "#firstHeading") + self.vรฉrifier_รฉlรฉment('img[alt*="Crรจme brรปlรฉe"]') + self.js_taper("#searchform input", "Jardin des Tuileries") + self.cliquer("#searchform button") + self.vรฉrifier_texte("Jardin des Tuileries", "#firstHeading") + self.vรฉrifier_รฉlรฉment('img[alt*="Jardin des Tuileries"]') + self.retour() + self.vรฉrifier_url_contient("brรปlรฉe") + self.en_avant() + self.vรฉrifier_url_contient("Jardin") +``` + +(See examples/translations/french_test_1.py for the French test.) + + +

      14. BaseCase with Italian translations

      + +This format is similar to the English version with BaseCase inheritance, but there's a different import statement, and method names have been translated into Italian. Here's an example of that: + +```python +from seleniumbase.translate.italian import CasoDiProva +CasoDiProva.main(__name__, __file__) + +class MiaClasseDiTest(CasoDiProva): + def test_esempio_1(self): + self.apri("https://it.wikipedia.org/wiki/") + self.verificare_testo("Wikipedia") + self.verificare_elemento('a[title="Lingua italiana"]') + self.digitare('input[name="search"]', "Pizza") + self.fare_clic("#searchform button") + self.verificare_testo("Pizza", "#firstHeading") + self.verificare_elemento('figure img[src*="pizza"]') + self.digitare('input[name="search"]', "Colosseo") + self.fare_clic("#searchform button") + self.verificare_testo("Colosseo", "#firstHeading") + self.verificare_elemento('figure img[src*="Colosseo"]') + self.indietro() + self.verificare_url_contiene("Pizza") + self.avanti() + self.verificare_url_contiene("Colosseo") +``` + +(See examples/translations/italian_test_1.py for the Italian test.) + + +

      15. BaseCase with Japanese translations

      + +This format is similar to the English version with BaseCase inheritance, but there's a different import statement, and method names have been translated into Japanese. Here's an example of that: + +```python +from seleniumbase.translate.japanese import ใ‚ปใƒฌใƒ‹ใ‚ฆใƒ ใƒ†ใ‚นใƒˆใ‚ฑใƒผใ‚น +ใ‚ปใƒฌใƒ‹ใ‚ฆใƒ ใƒ†ใ‚นใƒˆใ‚ฑใƒผใ‚น.main(__name__, __file__) + +class ็งใฎใƒ†ใ‚นใƒˆใ‚ฏใƒฉใ‚น(ใ‚ปใƒฌใƒ‹ใ‚ฆใƒ ใƒ†ใ‚นใƒˆใ‚ฑใƒผใ‚น): + def test_ไพ‹1(self): + self.ใ‚’้–‹ใ("https://ja.wikipedia.org/wiki/") + self.ใƒ†ใ‚ญใ‚นใƒˆใ‚’็ขบ่ชใ™ใ‚‹("ใ‚ฆใ‚ฃใ‚ญใƒšใƒ‡ใ‚ฃใ‚ข") + self.่ฆ็ด ใ‚’็ขบ่ชใ™ใ‚‹('[title*="ใ‚ฆใ‚ฃใ‚ญใƒšใƒ‡ใ‚ฃใ‚ขใธใ‚ˆใ†ใ“ใ"]') + self.JSๅ…ฅๅŠ›('input[name="search"]', "ใ‚ขใƒ‹ใƒก") + self.ใ‚ฏใƒชใƒƒใ‚ฏใ—ใฆ("#searchform button") + self.ใƒ†ใ‚ญใ‚นใƒˆใ‚’็ขบ่ชใ™ใ‚‹("ใ‚ขใƒ‹ใƒก", "#firstHeading") + self.JSๅ…ฅๅŠ›('input[name="search"]', "ๅฏฟๅธ") + self.ใ‚ฏใƒชใƒƒใ‚ฏใ—ใฆ("#searchform button") + self.ใƒ†ใ‚ญใ‚นใƒˆใ‚’็ขบ่ชใ™ใ‚‹("ๅฏฟๅธ", "#firstHeading") + self.่ฆ็ด ใ‚’็ขบ่ชใ™ใ‚‹('img[src*="Various_sushi"]') + self.JSๅ…ฅๅŠ›("#searchInput", "ใƒฌใ‚ดใƒฉใƒณใƒ‰ใƒปใ‚ธใƒฃใƒ‘ใƒณ") + self.ใ‚ฏใƒชใƒƒใ‚ฏใ—ใฆ("#searchform button") + self.่ฆ็ด ใ‚’็ขบ่ชใ™ใ‚‹('img[src*="LEGOLAND_JAPAN"]') + self.ใƒชใƒณใ‚ฏใƒ†ใ‚ญใ‚นใƒˆใ‚’็ขบ่ชใ™ใ‚‹("ๅๅคๅฑ‹ๅŸŽ") + self.ใƒชใƒณใ‚ฏใƒ†ใ‚ญใ‚นใƒˆใ‚’ใ‚ฏใƒชใƒƒใ‚ฏใ—ใพใ™("ใƒ†ใƒผใƒžใƒ‘ใƒผใ‚ฏ") + self.ใƒ†ใ‚ญใ‚นใƒˆใ‚’็ขบ่ชใ™ใ‚‹("ใƒ†ใƒผใƒžใƒ‘ใƒผใ‚ฏ", "#firstHeading") +``` + +(See examples/translations/japanese_test_1.py for the Japanese test.) + + +

      16. BaseCase with Korean translations

      + +This format is similar to the English version with BaseCase inheritance, but there's a different import statement, and method names have been translated into Korean. Here's an example of that: + +```python +from seleniumbase.translate.korean import ์…€๋ ˆ๋Š„_ํ…Œ์ŠคํŠธ_์ผ€์ด์Šค +์…€๋ ˆ๋Š„_ํ…Œ์ŠคํŠธ_์ผ€์ด์Šค.main(__name__, __file__) + +class ํ…Œ์ŠคํŠธ_ํด๋ž˜์Šค(์…€๋ ˆ๋Š„_ํ…Œ์ŠคํŠธ_์ผ€์ด์Šค): + def test_์‹ค์‹œ์˜ˆ_1(self): + self.์—ด๊ธฐ("https://ko.wikipedia.org/wiki/") + self.ํ…์ŠคํŠธ_ํ™•์ธ("์œ„ํ‚ค๋ฐฑ๊ณผ") + self.์š”์†Œ_ํ™•์ธ('[title="์œ„ํ‚ค๋ฐฑ๊ณผ:์†Œ๊ฐœ"]') + self.JS_์ž…๋ ฅ("#searchform input", "๊น€์น˜") + self.ํด๋ฆญ("#searchform button") + self.ํ…์ŠคํŠธ_ํ™•์ธ("๊น€์น˜", "#firstHeading") + self.์š”์†Œ_ํ™•์ธ('img[src*="Various_kimchi.jpg"]') + self.๋งํฌ_ํ…์ŠคํŠธ_ํ™•์ธ("ํ•œ๊ตญ ์š”๋ฆฌ") + self.JS_์ž…๋ ฅ("#searchform input", "๋น„๋น”๋ฐฅ") + self.ํด๋ฆญ("#searchform button") + self.ํ…์ŠคํŠธ_ํ™•์ธ("๋น„๋น”๋ฐฅ", "#firstHeading") + self.์š”์†Œ_ํ™•์ธ('img[src*="Dolsot-bibimbap.jpg"]') + self.๋งํฌ_ํ…์ŠคํŠธ๋ฅผ_ํด๋ฆญํ•ฉ๋‹ˆ๋‹ค("๋Œ์†ฅ๋น„๋น”๋ฐฅ") + self.ํ…์ŠคํŠธ_ํ™•์ธ("๋Œ์†ฅ๋น„๋น”๋ฐฅ", "#firstHeading") +``` + +(See examples/translations/korean_test_1.py for the Korean test.) + + +

      17. BaseCase with Portuguese translations

      + +This format is similar to the English version with BaseCase inheritance, but there's a different import statement, and method names have been translated into Portuguese. Here's an example of that: + +```python +from seleniumbase.translate.portuguese import CasoDeTeste +CasoDeTeste.main(__name__, __file__) + +class MinhaClasseDeTeste(CasoDeTeste): + def test_exemplo_1(self): + self.abrir("https://pt.wikipedia.org/wiki/") + self.verificar_texto("Wikipรฉdia") + self.verificar_elemento('[title="Lรญngua portuguesa"]') + self.digitar("#searchform input", "Joรฃo Pessoa") + self.clique("#searchform button") + self.verificar_texto("Joรฃo Pessoa", "#firstHeading") + self.verificar_elemento('img[alt*="Joรฃo Pessoa"]') + self.digitar("#searchform input", "Florianรณpolis") + self.clique("#searchform button") + self.verificar_texto("Florianรณpolis", "h1#firstHeading") + self.verificar_elemento('td:contains("Avenida Beira-Mar")') + self.voltar() + self.verificar_url_contรฉm("Joรฃo_Pessoa") + self.atualizar_a_pรกgina() + self.js_digitar("#searchform input", "Teatro Amazonas") + self.clique("#searchform button") + self.verificar_texto("Teatro Amazonas", "#firstHeading") + self.verificar_texto_do_link("Festival Amazonas de ร“pera") +``` + +(See examples/translations/portuguese_test_1.py for the Portuguese test.) + + +

      18. BaseCase with Russian translations

      + +This format is similar to the English version with BaseCase inheritance, but there's a different import statement, and method names have been translated into Russian. Here's an example of that: + +```python +from seleniumbase.translate.russian import ะขะตัั‚ะะฐะกะตะปะตะฝ +ะขะตัั‚ะะฐะกะตะปะตะฝ.main(__name__, __file__) + +class ะœะพะนะขะตัั‚ะพะฒั‹ะนะšะปะฐัั(ะขะตัั‚ะะฐะกะตะปะตะฝ): + def test_ะฟั€ะธะผะตั€_1(self): + self.ะพั‚ะบั€ั‹ั‚ัŒ("https://ru.wikipedia.org/wiki/") + self.ะฟะพะดั‚ะฒะตั€ะดะธั‚ัŒ_ัะปะตะผะตะฝั‚('[title="ะ ัƒััะบะธะน ัะทั‹ะบ"]') + self.ะฟะพะดั‚ะฒะตั€ะดะธั‚ัŒ_ั‚ะตะบัั‚("ะ’ะธะบะธะฟะตะดะธั", "div.main-wikimedia-header") + self.ะฒะฒะตะดะธั‚ะต("#searchInput", "ะœะ“ะฃ") + self.ะฝะฐะถะผะธั‚ะต("#searchButton") + self.ะฟะพะดั‚ะฒะตั€ะดะธั‚ัŒ_ั‚ะตะบัั‚("ัƒะฝะธะฒะตั€ัะธั‚ะตั‚", "#firstHeading") + self.ะฟะพะดั‚ะฒะตั€ะดะธั‚ัŒ_ัะปะตะผะตะฝั‚('img[alt*="ะ“ะปะฐะฒะฝะพะต ะทะดะฐะฝะธะต ะœะ“ะฃ"]') + self.ะฒะฒะตะดะธั‚ะต("#searchInput", "ะฟั€ะธะบะปัŽั‡ะตะฝะธั ะจัƒั€ะธะบะฐ") + self.ะฝะฐะถะผะธั‚ะต("#searchButton") + self.ะฟะพะดั‚ะฒะตั€ะดะธั‚ัŒ_ั‚ะตะบัั‚("ะžะฟะตั€ะฐั†ะธั ยซะซยป ะธ ะดั€ัƒะณะธะต ะฟั€ะธะบะปัŽั‡ะตะฝะธั ะจัƒั€ะธะบะฐ") + self.ะฟะพะดั‚ะฒะตั€ะดะธั‚ัŒ_ัะปะตะผะตะฝั‚('img[alt="ะŸะพัั‚ะตั€ ั„ะธะปัŒะผะฐ"]') + self.ะฝะฐะทะฐะด() + self.ะฟะพะดั‚ะฒะตั€ะดะธั‚ัŒ_URL_ัะพะดะตั€ะถะธั‚("ัƒะฝะธะฒะตั€ัะธั‚ะตั‚") + self.ะฒะฟะตั€ะตะด() + self.ะฟะพะดั‚ะฒะตั€ะดะธั‚ัŒ_URL_ัะพะดะตั€ะถะธั‚("ะจัƒั€ะธะบะฐ") +``` + +(See examples/translations/russian_test_1.py for the Russian test.) + + +

      19. BaseCase with Spanish translations

      + +This format is similar to the English version with BaseCase inheritance, but there's a different import statement, and method names have been translated into Spanish. Here's an example of that: + +```python +from seleniumbase.translate.spanish import CasoDePrueba +CasoDePrueba.main(__name__, __file__) + +class MiClaseDePrueba(CasoDePrueba): + def test_ejemplo_1(self): + self.abrir("https://es.wikipedia.org/wiki/") + self.verificar_texto("Wikipedia") + self.verificar_elemento('[title="Wikipedia:Bienvenidos"]') + self.escriba('[name="search"]', "Parque de Atracciones Tibidabo") + self.haga_clic('button:contains("Buscar")') + self.verificar_texto("Tibidabo", "#firstHeading") + self.verificar_elemento('img[src*="Tibidabo"]') + self.escriba('input[name="search"]', "Palma de Mallorca") + self.haga_clic('button:contains("Buscar")') + self.verificar_texto("Palma de Mallorca", "#firstHeading") + self.verificar_elemento('img[src*="Palma"]') + self.volver() + self.verificar_url_contiene("Tibidabo") + self.adelante() + self.verificar_url_contiene("Mallorca") +``` + +(See examples/translations/spanish_test_1.py for the Spanish test.) + + +

      20. Gherkin syntax with "behave" BDD runner

      + +With [Behave's BDD Gherkin format](https://behave.readthedocs.io/en/stable/gherkin.html), you can use natural language to write tests that work with SeleniumBase methods. Behave tests are run by calling `behave` on the command-line. This requires some special files in a specific directory structure. Here's an example of that structure: + +```zsh +features/ +โ”œโ”€โ”€ __init__.py +โ”œโ”€โ”€ behave.ini +โ”œโ”€โ”€ environment.py +โ”œโ”€โ”€ feature_file.feature +โ””โ”€โ”€ steps/ + โ”œโ”€โ”€ __init__.py + โ”œโ”€โ”€ imported.py + โ””โ”€โ”€ step_file.py +``` + +A `*.feature` file might look like this: + +```gherkin +Feature: SeleniumBase scenarios for the RealWorld App + + Scenario: Verify RealWorld App (log in / sign out) + Given Open "seleniumbase.io/realworld/login" + And Clear Session Storage + When Type "demo_user" into "#username" + And Type "secret_pass" into "#password" + And Do MFA "GAXG2MTEOR3DMMDG" into "#totpcode" + Then Assert exact text "Welcome!" in "h1" + And Highlight "img#image1" + And Click 'a:contains("This Page")' + And Save screenshot to logs + When Click link "Sign out" + Then Assert element 'a:contains("Sign in")' + And Assert text "You have been signed out!" +``` + +(From examples/behave_bdd/features/realworld.feature) + +You'll need the `environment.py` file for tests to work. Here it is: + +```python +from seleniumbase import BaseCase +from seleniumbase.behave import behave_sb +behave_sb.set_base_class(BaseCase) # Accepts a BaseCase subclass +from seleniumbase.behave.behave_sb import before_all # noqa +from seleniumbase.behave.behave_sb import before_feature # noqa +from seleniumbase.behave.behave_sb import before_scenario # noqa +from seleniumbase.behave.behave_sb import before_step # noqa +from seleniumbase.behave.behave_sb import after_step # noqa +from seleniumbase.behave.behave_sb import after_scenario # noqa +from seleniumbase.behave.behave_sb import after_feature # noqa +from seleniumbase.behave.behave_sb import after_all # noqa +``` + +(From examples/behave_bdd/features/environment.py) + +Inside that file, you can use `BaseCase` (or a subclass) for the inherited class. + +For your `behave` tests to have access to SeleniumBase Behave steps, you can create an `imported.py` file with the following line: + +```python +from seleniumbase.behave import steps # noqa +``` + +That will allow you to use lines like this in your `*.feature` files: + +```gherkin +Feature: SeleniumBase scenarios for the RealWorld App + + Scenario: Verify RealWorld App (log in / sign out) + Given Open "seleniumbase.io/realworld/login" + And Clear Session Storage + When Type "demo_user" into "#username" + And Type "secret_pass" into "#password" + And Do MFA "GAXG2MTEOR3DMMDG" into "#totpcode" + Then Assert exact text "Welcome!" in "h1" + And Highlight "img#image1" + And Click 'a:contains("This Page")' + And Save screenshot to logs +``` + +You can also create your own step files (Eg. `step_file.py`): + +```python +from behave import step + +@step("Open the Swag Labs Login Page") +def go_to_swag_labs(context): + sb = context.sb + sb.goto("https://www.saucedemo.com") + sb.clear_local_storage() + +@step("Login to Swag Labs with {user}") +def login_to_swag_labs(context, user): + sb = context.sb + sb.type("#user-name", user) + sb.type("#password", "secret_sauce\n") +``` + +(For more information, see the SeleniumBase Behave BDD ReadMe.) + + +

      21. SeleniumBase SB (Python context manager)

      + +This format provides a pure Python way of using SeleniumBase without a test runner. Options can be passed via method instantiation or from the command-line. When setting the test option to True (or calling python --test), then standard test logging will occur, such as screenshots and reports for failing tests. All the usual SeleniumBase options are available, such as customizing the browser settings, etc. Here are some examples: + +```python +from seleniumbase import SB + +with SB() as sb: + sb.goto("seleniumbase.io/simple/login") + sb.type("#username", "demo_user") + sb.type("#password", "secret_pass") + sb.click('a:contains("Sign in")') + sb.assert_exact_text("Welcome!", "h1") + sb.assert_element("img#image1") + sb.highlight("#image1") + sb.click_link("Sign out") + sb.assert_text("signed out", "#top_message") +``` + +(See examples/raw_login_sb.py for the test.) + +Here's another example, which uses test mode: + +```python +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + sb.goto("https://google.com/ncr") + sb.type('[name="q"]', "SeleniumBase on GitHub\n") + sb.highlight('a[href*="github.com/seleniumbase"]') + sb.sleep(0.5) + +with SB(test=True, rtf=True, demo=True) as sb: + sb.goto("seleniumbase.github.io/demo_page") + sb.type("#myTextInput", "This is Automated") + sb.assert_text("This is Automated", "#myTextInput") + sb.assert_text("This Text is Green", "#pText") + sb.click('button:contains("Click Me")') + sb.assert_text("This Text is Purple", "#pText") + sb.click("#checkBox1") + sb.assert_element_not_visible("div#drop2 img#logo") + sb.drag_and_drop("img#logo", "div#drop2") + sb.assert_element("div#drop2 img#logo") +``` + +(See examples/raw_test_scripts.py for the test.) + +Here's another example, which uses [CDP Mode](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/ReadMe.md) from the SeleniumBase SB format: + +```python +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + url = "www.planetminecraft.com/account/sign_in/" + sb.activate_cdp_mode(url) + sb.sleep(2) + sb.solve_captcha() + sb.wait_for_element_absent("input[disabled]") + sb.sleep(2) +``` + +(See examples/cdp_mode/raw_planetmc.py for the test.) + + +

      22. The driver manager (via context manager)

      + +This pure Python format gives you a raw webdriver instance in a with block. The SeleniumBase Driver Manager will automatically make sure that your driver is compatible with your browser version. It gives you full access to customize driver options via method args or via the command-line. The driver will automatically call quit() after the code leaves the with block. Here are some examples: + +```python +"""DriverContext() example. (Runs with "python").""" +from seleniumbase import DriverContext + +with DriverContext() as driver: + driver.goto("seleniumbase.io/") + driver.highlight('img[alt="SeleniumBase"]', loops=6) + +with DriverContext(browser="chrome", incognito=True) as driver: + driver.goto("seleniumbase.io/apps/calculator") + driver.click('[id="4"]') + driver.click('[id="2"]') + driver.assert_text("42", "#output") + driver.highlight("#output", loops=6) + +with DriverContext() as driver: + driver.goto("seleniumbase.io/demo_page") + driver.highlight("h2") + driver.type("#myTextInput", "Automation") + driver.click("#checkBox1") + driver.highlight("img", loops=6) +``` + +(See examples/raw_driver_context.py for an example.) + + +

      23. The driver manager (via direct import)

      + +Another way of running Selenium tests with pure `python` (as opposed to using `pytest` or `pynose`) is by using this format, which bypasses [BaseCase](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/fixtures/base_case.py) methods while still giving you a flexible driver with a manager. SeleniumBase includes helper files such as [page_actions.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/fixtures/page_actions.py), which may help you get around some of the limitations of bypassing `BaseCase`. Here's an example: + +```python +"""Driver() example. (Runs with "python").""" +from seleniumbase import Driver + +driver = Driver() +try: + driver.goto("seleniumbase.io/demo_page") + driver.highlight("h2") + driver.type("#myTextInput", "Automation") + driver.click("#checkBox1") + driver.highlight("img", loops=6) +finally: + driver.quit() + +driver = Driver(browser="chrome", headless=False) +try: + driver.goto("seleniumbase.io/apps/calculator") + driver.click('[id="4"]') + driver.click('[id="2"]') + driver.assert_text("42", "#output") + driver.highlight("#output", loops=6) +finally: + driver.quit() +``` + +(From examples/raw_driver_manager.py) + +Here's an example of basic login with the `Driver()` format: + +```python +from seleniumbase import Driver + +driver = Driver() +try: + driver.goto("seleniumbase.io/simple/login") + driver.type("#username", "demo_user") + driver.type("#password", "secret_pass") + driver.click('a:contains("Sign in")') + driver.assert_exact_text("Welcome!", "h1") + driver.assert_element("img#image1") + driver.highlight("#image1") + driver.click_link("Sign out") + driver.assert_text("signed out", "#top_message") +finally: + driver.quit() +``` + +(From examples/raw_login_driver.py) + +The `Driver()` manager format can be used as a drop-in replacement for virtually every Python/selenium framework, as it uses the raw `driver` instance for handling commands. The `Driver()` method simplifies the work of managing drivers with optimal settings, and it can be configured with multiple args. The `Driver()` also accepts command-line options (such as `python --headless`) so that you don't need to modify your tests directly to use different settings. These command-line options only take effect if the associated method args remain unset (or set to `None`) for the specified options. + +When using the `Driver()` format, you may need to activate a Virtual Display on your own if you want to run headed tests in a headless Linux environment. (See https://github.com/mdmintz/sbVirtualDisplay for details.) One such example of this is using an authenticated proxy, which is configured via a Chrome extension that is generated at runtime. (Note that regular headless mode in Chrome doesn't support extensions.) + +Set `uc=True` in order to make the `driver` stealthy: +`driver = Driver(uc=True)` + + +

      24. Pure CDP Mode (Async API. No Selenium)

      + +This format provides a pure CDP way of using SeleniumBase (without Selenium/WebDriver or a test runner). The async/await API is used. Here's an example: + +```python +import asyncio +from seleniumbase import cdp_driver + +async def main(): + url = "https://seleniumbase.io/simple/login" + driver = await cdp_driver.start_async() + page = await driver.get(url, lang="en") + print(await page.get_title()) + await page.type("#username", "demo_user") + await page.type("#password", "secret_pass") + await page.click("#log-in") + print(await page.get_title()) + element = await page.select("h1") + assert element.text == "Welcome!" + top_nav = await page.select("div.topnav") + links = await top_nav.query_selector_all_async("a") + for nav_item in links: + print(nav_item.text) + driver.quit() + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) +``` + +(See examples/cdp_mode/raw_basic_async.py for the test.) + + +

      25. Pure CDP Mode (Sync API. No Selenium)

      + +This format provides a pure CDP way of using SeleniumBase (without Selenium/WebDriver or a test runner). The expanded sb_cdp Sync API is used. Here's an example: + +```python +from seleniumbase import sb_cdp + +url = "https://seleniumbase.io/simple/login" +sb = sb_cdp.Chrome(url) +sb.type("#username", "demo_user") +sb.type("#password", "secret_pass") +sb.click('a:contains("Sign in")') +sb.assert_exact_text("Welcome!", "h1") +sb.assert_element("img#image1") +sb.highlight("#image1") +top_nav = sb.find_element("div.topnav") +links = top_nav.query_selector_all("a") +for nav_item in links: + print(nav_item.text) +sb.click_link("Sign out") +sb.assert_text("signed out", "#top_message") +sb.quit() +``` + +(See examples/cdp_mode/raw_basic_cdp.py for the test.) + +Here's a Pure CDP Mode example that bypasses bot-detection to scrape data from a website: + +```python +from seleniumbase import sb_cdp + +def main(): + url = "https://www.priceline.com/" + sb = sb_cdp.Chrome(url, lang="en") + sb.sleep(2) + sb.internalize_links() # Don't open links in a new tab + sb.click("#link_header_nav_experiences") + sb.sleep(3) + sb.remove_elements("msm-cookie-banner") + sb.sleep(1) + location = "Amsterdam" + where_to = 'div[data-automation*="experiences"] input' + button = 'button[data-automation*="experiences-search"]' + sb.wait_for_text("Where to?") + sb.click(where_to) + sb.press_keys(where_to, location) + sb.sleep(1) + sb.click(button) + sb.sleep(2) + sb.click_if_visible('button[aria-label="Close"]') + sb.sleep(1) + print(sb.get_title()) + print("************") + for i in range(8): + sb.scroll_down(50) + sb.sleep(0.2) + cards = sb.select_all('span[data-automation*="product-list-card"]') + for card in cards: + print("* %s" % card.text) + sb.quit() + +if __name__ == "__main__": + main() +``` + +(See examples/cdp_mode/raw_cdp.py for the test.) + +-------- + +

      diff --git a/help_docs/thank_you.md b/help_docs/thank_you.md new file mode 100644 index 00000000000..ec860135f81 --- /dev/null +++ b/help_docs/thank_you.md @@ -0,0 +1,36 @@ + + + + + +

      ๐Ÿ›ฌ๐Ÿ›ซ Thank you for flying with SeleniumBase! ๐Ÿฆ…๐Ÿš€

      + +-------- + +

      ๐Ÿ›ฉ๏ธ SeleniumBase Playlist on YouTube:

      + +

      SeleniumBase Playlist on YouTube

      + +

      ๐Ÿ›ฉ๏ธ SeleniumBase GitHub Home Page:

      + +

      SeleniumBase on GitHub

      + +

      ๐Ÿ›ฉ๏ธ SeleniumBase Discord Channel:

      + +

      SeleniumBase on Discord

      + +

      ๐Ÿ›ฉ๏ธ SeleniumBase Gitter Chat:

      + +

      SeleniumBase on Gitter

      + +

      Other Social Media Links:

      + +

      +SeleniumBase on Facebook +SeleniumBase on Twitter +SeleniumBase on Instagram +

      + +-------- + +

      SeleniumBase

      diff --git a/help_docs/translations.md b/help_docs/translations.md new file mode 100644 index 00000000000..c47c29c9979 --- /dev/null +++ b/help_docs/translations.md @@ -0,0 +1,100 @@ + + + + +

      ๐ŸŒ Translated Tests ๐Ÿˆบ

      + +SeleniumBase supports the following 10 languages: English, Chinese, Dutch, French, Italian, Japanese, Korean, Portuguese, Russian, and Spanish. (Examples can be found in SeleniumBase/examples/translations) + +Multi-language tests run with **pytest** like other tests. Test methods have a one-to-one mapping to supported languages. Here's an example of a translated test: + +```python +# Chinese Translation +from seleniumbase.translate.chinese import ็ก’ๆต‹่ฏ•็”จไพ‹ + +class ๆˆ‘็š„ๆต‹่ฏ•็ฑป(็ก’ๆต‹่ฏ•็”จไพ‹): + def test_ไพ‹ๅญ1(self): + self.ๅผ€ๅฏ("https://zh.wikipedia.org/wiki/") + self.ๆ–ญ่จ€ๆ ‡้ข˜("็ปดๅŸบ็™พ็ง‘๏ผŒ่‡ช็”ฑ็š„็™พ็ง‘ๅ…จไนฆ") + self.ๆ–ญ่จ€ๅ…ƒ็ด ('a[title="Wikipedia:ๅ…ณไบŽ"]') + self.ๅฆ‚ๆžœๅฏ่ง่ฏทๅ•ๅ‡ป('button[aria-label="ๅ…ณ้—ญ"]') + self.ๅฆ‚ๆžœๅฏ่ง่ฏทๅ•ๅ‡ป('button[aria-label="้—œ้–‰"]') + self.ๆ–ญ่จ€ๅ…ƒ็ด ('span:contains("ๅˆ›ๅปบ่ดฆๅท")') + self.ๆ–ญ่จ€ๅ…ƒ็ด ('span:contains("็™ปๅฝ•")') + self.่พ“ๅ…ฅๆ–‡ๆœฌ('input[name="search"]', "่ˆž้พ") + self.ๅ•ๅ‡ป('button:contains("ๆœ็ดข")') + self.ๆ–ญ่จ€ๆ–‡ๆœฌ("่ˆž้พ", "#firstHeading") + self.ๆ–ญ่จ€ๅ…ƒ็ด ('img[src*="Chinese_draak.jpg"]') +``` + +Here's another example: + +```python +# Japanese Translation +from seleniumbase.translate.japanese import ใ‚ปใƒฌใƒ‹ใ‚ฆใƒ ใƒ†ใ‚นใƒˆใ‚ฑใƒผใ‚น + +class ็งใฎใƒ†ใ‚นใƒˆใ‚ฏใƒฉใ‚น(ใ‚ปใƒฌใƒ‹ใ‚ฆใƒ ใƒ†ใ‚นใƒˆใ‚ฑใƒผใ‚น): + def test_ไพ‹1(self): + self.ใ‚’้–‹ใ("https://ja.wikipedia.org/wiki/") + self.ใƒ†ใ‚ญใ‚นใƒˆใ‚’็ขบ่ชใ™ใ‚‹("ใ‚ฆใ‚ฃใ‚ญใƒšใƒ‡ใ‚ฃใ‚ข") + self.่ฆ็ด ใ‚’็ขบ่ชใ™ใ‚‹('[title*="ใ‚ฆใ‚ฃใ‚ญใƒšใƒ‡ใ‚ฃใ‚ขใธใ‚ˆใ†ใ“ใ"]') + self.JSๅ…ฅๅŠ›('input[name="search"]', "ใ‚ขใƒ‹ใƒก") + self.ใ‚ฏใƒชใƒƒใ‚ฏใ—ใฆ("#searchform button") + self.ใƒ†ใ‚ญใ‚นใƒˆใ‚’็ขบ่ชใ™ใ‚‹("ใ‚ขใƒ‹ใƒก", "#firstHeading") + self.JSๅ…ฅๅŠ›('input[name="search"]', "ๅฏฟๅธ") + self.ใ‚ฏใƒชใƒƒใ‚ฏใ—ใฆ("#searchform button") + self.ใƒ†ใ‚ญใ‚นใƒˆใ‚’็ขบ่ชใ™ใ‚‹("ๅฏฟๅธ", "#firstHeading") + self.่ฆ็ด ใ‚’็ขบ่ชใ™ใ‚‹('img[src*="Various_sushi"]') +``` + + +

      Translation API ๐Ÿˆบ

      + +You can use SeleniumBase to selectively translate the method names of any test from one language to another with the console scripts interface. Additionally, the `import` line at the top of the Python file will change to import the new `BaseCase`. Example: `BaseCase` becomes `CasoDeTeste` when a test is translated into Portuguese. + +```zsh +seleniumbase translate +``` + +```zsh +* Usage: +seleniumbase translate [SB_FILE.py] [LANGUAGE] [ACTION] + +* Languages: +`--en` / `--English` | `--zh` / `--Chinese` +`--nl` / `--Dutch` | `--fr` / `--French` +`--it` / `--Italian` | `--ja` / `--Japanese` +`--ko` / `--Korean` | `--pt` / `--Portuguese` +`--ru` / `--Russian` | `--es` / `--Spanish` + +* Actions: +`-p` / `--print` (Print translation output to the screen) +`-o` / `--overwrite` (Overwrite the file being translated) +`-c` / `--copy` (Copy the translation to a new `.py` file) + +* Options: +`-n` (include line Numbers when using the Print action) + +* Examples: +Translate test_1.py into Chinese and only print the output: +>>> seleniumbase translate test_1.py --zh -p +Translate test_2.py into Portuguese and overwrite the file: +>>> seleniumbase translate test_2.py --pt -o +Translate test_3.py into Dutch and make a copy of the file: +>>> seleniumbase translate test_3.py --nl -c + +* Output: +Translates a SeleniumBase Python file into the language +specified. Method calls and `import` lines get swapped. +Both a language and an action must be specified. +The `-p` action can be paired with one other action. +When running with `-c` (or `--copy`) the new file name +will be the original name appended with an underscore +plus the 2-letter language code of the new language. +(Example: Translating `test_1.py` into Japanese with +`-c` will create a new file called `test_1_ja.py`.) +``` + +-------- + +

      diff --git a/help_docs/uc_mode.md b/help_docs/uc_mode.md new file mode 100644 index 00000000000..c5ef46d7d18 --- /dev/null +++ b/help_docs/uc_mode.md @@ -0,0 +1,379 @@ + + +

      UC Mode ๐Ÿ‘ค

      + +๐Ÿ‘ค SeleniumBase UC Mode (Undetected-Chromedriver Mode) allows bots to appear human, which lets them evade detection from anti-bot services that try to block them or trigger CAPTCHAs on various websites. + +> ### (For the successor to plain UC Mode, see **[CDP Mode ๐Ÿ™](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/ReadMe.md)**) + +--- + + +

      (Watch the 1st UC Mode tutorial on YouTube! โ–ถ๏ธ)

      + +---- + + +

      (Watch the 2nd UC Mode tutorial on YouTube! โ–ถ๏ธ)

      + +---- + + +

      (Watch the 3rd UC Mode tutorial on YouTube! โ–ถ๏ธ)

      + +---- + + +

      (Watch the 4th UC Mode tutorial on YouTube! โ–ถ๏ธ)

      + +---- + +๐Ÿ‘ค UC Mode is based on [undetected-chromedriver](https://github.com/ultrafunkamsterdam/undetected-chromedriver). UC Mode includes multiple updates, fixes, and improvements, such as having special uc_*() methods for bypassing CAPTCHAs. + +๐Ÿ‘ค Here's a simple example with the Driver manager: + +```python +from seleniumbase import Driver + +driver = Driver(uc=True) +url = "https://gitlab.com/users/sign_in" +driver.uc_open_with_reconnect(url, 4) +driver.uc_gui_click_captcha() +driver.quit() +``` + + + +๐Ÿ‘ค Here's an example with the SB manager (which has more methods and functionality than the Driver format): + +```python +from seleniumbase import SB + +with SB(uc=True) as sb: + url = "https://gitlab.com/users/sign_in" + sb.uc_open_with_reconnect(url, 4) + sb.uc_gui_click_captcha() +``` + +(Note: If running UC Mode scripts on headless Linux machines, then you'll need to use the SB manager instead of the Driver manager because the SB manager includes a special virtual display that allows for PyAutoGUI actions.) + +๐Ÿ‘ค Here's a longer example: (Note that sb.uc_gui_click_captcha() performs a special click using PyAutoGUI if a CAPTCHA is detected.) + +```python +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + url = "https://gitlab.com/users/sign_in" + sb.uc_open_with_reconnect(url, 4) + sb.uc_gui_click_captcha() + sb.assert_text("Username", '[for="user_login"]', timeout=3) + sb.assert_element('label[for="user_login"]') + sb.highlight('button:contains("Sign in")') + sb.highlight('h1:contains("GitLab")') + sb.post_message("SeleniumBase wasn't detected", duration=4) +``` + +๐Ÿ‘ค Here's an example where clicking the checkbox is required, even for humans:
      (Commonly seen on forms that are CAPTCHA-protected.) + + + +```python +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + url = "https://seleniumbase.io/apps/turnstile" + sb.uc_open_with_reconnect(url, reconnect_time=2) + sb.uc_gui_handle_captcha() + sb.assert_element("img#captcha-success", timeout=3) + sb.set_messenger_theme(location="top_left") + sb.post_message("SeleniumBase wasn't detected", duration=3) +``` + + + +If running on a Linux server, `uc_gui_handle_captcha()` might not be good enough. Switch to `uc_gui_click_captcha()` to be more stealthy. Note that these methods auto-detect between CF Turnstile and Google reCAPTCHA. + +Sometimes you need to add incognito=True with uc=True to maximize your anti-detection abilities. (Some websites can detect you if you don't do that.) + +๐Ÿ‘ค Here's an example where the CAPTCHA appears after submitting a form: + +```python +from seleniumbase import SB + +with SB(uc=True, test=True, incognito=True, locale="en") as sb: + url = "https://ahrefs.com/website-authority-checker" + input_field = 'input[placeholder="Enter domain"]' + submit_button = 'span:contains("Check Authority")' + sb.uc_open_with_reconnect(url) # The bot-check is later + sb.type(input_field, "github.com/seleniumbase/SeleniumBase") + sb.uc_click(submit_button, reconnect_time=3.25) + sb.uc_gui_click_captcha() + sb.wait_for_text_not_visible("Checking", timeout=15) + sb.click_if_visible('button[data-cky-tag="close-button"]') + sb.highlight('p:contains("github.com/seleniumbase/SeleniumBase")') + sb.highlight('a:contains("Top 100 backlinks")') + sb.set_messenger_theme(location="bottom_center") + sb.post_message("SeleniumBase wasn't detected!") +``` + + + +-------- + +๐Ÿ‘ค On Linux, use `sb.uc_gui_click_captcha()` to handle CAPTCHAs (Cloudflare Turnstiles): + +```python +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + url = "https://www.virtualmanager.com/en/login" + sb.uc_open_with_reconnect(url, 4) + print(sb.get_page_title()) + sb.uc_gui_click_captcha() # Only used if needed + print(sb.get_page_title()) + sb.assert_element('input[name*="email"]') + sb.assert_element('input[name*="login"]') + sb.set_messenger_theme(location="bottom_center") + sb.post_message("SeleniumBase wasn't detected!") +``` + +uc_gui_click_captcha on Linux + +The 2nd print() should output Virtual Manager, which means that the automation successfully passed the Turnstile. + +(Note: UC Mode is detectable in Headless Mode, so don't combine those options. Instead, use xvfb=True / `--xvfb`on Linux for the special virtual display, which is enabled by default when not changing headed/headless settings.) + +-------- + +๐Ÿ‘ค In UC Mode, driver.get(url) has been modified from its original version: If anti-bot services are detected from a requests.get(url) call that's made before navigating to the website, then driver.uc_open_with_reconnect(url) will be used instead. To open a URL normally in UC Mode, use driver.default_get(url). + +-------- + +### ๐Ÿ‘ค Here are some examples that use UC Mode: +* [SeleniumBase/examples/verify_undetected.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/verify_undetected.py) +* [SeleniumBase/examples/raw_turnstile.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/raw_turnstile.py) +* [SeleniumBase/examples/raw_form_turnstile.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/raw_form_turnstile.py) +* [SeleniumBase/examples/raw_uc_mode.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/raw_uc_mode.py) + + + +-------- + +๐Ÿ‘ค Here's an example where incognito=True is needed for bypassing detection: + +```python +from seleniumbase import SB + +with SB(uc=True, incognito=True, test=True) as sb: + sb.driver.uc_open_with_reconnect("https://pixelscan.net/", 10) + sb.sleep(2) +``` + + + +-------- + +### ๐Ÿ‘ค Here are the SeleniumBase UC Mode methods: (**`--uc`** / **`uc=True`**) + +```python +driver.uc_open(url) + +driver.uc_open_with_tab(url) + +driver.uc_open_with_reconnect(url, reconnect_time=None) + +driver.uc_open_with_disconnect(url, timeout=None) + +driver.reconnect(timeout) + +driver.disconnect() + +driver.connect() + +driver.uc_click( + selector, by="css selector", + timeout=settings.SMALL_TIMEOUT, reconnect_time=None) + +driver.uc_gui_press_key(key) + +driver.uc_gui_press_keys(keys) + +driver.uc_gui_write(text) + +driver.uc_gui_click_x_y(x, y, timeframe=0.25) + +driver.uc_gui_click_captcha(frame="iframe", retry=False, blind=False) +# driver.uc_gui_click_cf(frame="iframe", retry=False, blind=False) +# driver.uc_gui_click_rc(frame="iframe", retry=False, blind=False) + +driver.uc_gui_handle_captcha(frame="iframe") +# driver.uc_gui_handle_cf(frame="iframe") +# driver.uc_gui_handle_rc(frame="iframe") +``` + +(Note that the reconnect_time is used to specify how long the driver should be disconnected from Chrome to prevent detection before reconnecting again.) + +๐Ÿ‘ค Since driver.get(url) is slower in UC Mode for bypassing detection, use driver.default_get(url) for a standard page load instead: + +```python +driver.default_get(url) # Faster, but Selenium can be detected +``` + +๐Ÿ‘ค Here are some examples of using those special UC Mode methods: (Use self.driver for BaseCase formats. Use sb.driver for SB() formats): + +```python +url = "https://gitlab.com/users/sign_in" +driver.uc_open_with_reconnect(url, reconnect_time=3) +driver.uc_open_with_reconnect(url, 3) + +driver.reconnect(5) +driver.reconnect(timeout=5) +``` + +๐Ÿ‘ค You can also set the reconnect_time / timeout to "breakpoint" as a valid option. This allows the user to perform manual actions (until typing c and pressing ENTER to continue from the breakpoint): + +```python +url = "https://gitlab.com/users/sign_in" +driver.uc_open_with_reconnect(url, reconnect_time="breakpoint") +driver.uc_open_with_reconnect(url, "breakpoint") + +driver.reconnect(timeout="breakpoint") +driver.reconnect("breakpoint") +``` + +(Note that while the special UC Mode breakpoint is active, you can't use Selenium commands in the browser, and the browser can't detect Selenium.) + +-------- + +๐Ÿ‘ค On Linux, use xvfb=True / `--xvfb` to activate a special virtual display. This allows you to run a regular browser in an environment that has no GUI. This is important for two reasons: One: UC Mode is detectable in headless mode. Two: pyautogui doesn't work in headless mode. (Note that some methods such as uc_gui_click_captcha() require pyautogui for performing special actions.) + +-------- + +๐Ÿ‘ค uc_gui_click_captcha() auto-detects the CAPTCHA type before trying to click it. This is a generic method for both CF Turnstile and Google reCAPTCHA. It will use the code from uc_gui_click_cf() and uc_gui_click_rc() as needed. + +๐Ÿ‘ค uc_gui_click_cf(frame="iframe", retry=False, blind=False) has three args. (All optional). The first one, frame, lets you specify the selector above the iframe in case the CAPTCHA is not located in the first iframe on the page. (In the case of Shadow-DOM, specify the selector of an element that's above the Shadow-DOM.) The second one, retry, lets you retry the click after reloading the page if the first one didn't work (and a CAPTCHA is still present after the page reload). The third arg, blind, (if True), will retry after a page reload (if the first click failed) by clicking at the last known coordinates of the CAPTCHA checkbox without confirming first with Selenium that a CAPTCHA is still on the page. + +๐Ÿ‘ค uc_gui_click_rc(frame="iframe", retry=False, blind=False) is for reCAPTCHA. This may only work a few times before not working anymore... not because Selenium was detected, but because reCAPTCHA uses advanced AI to detect unusual activity, unlike the CF Turnstile, which only uses basic detection. + +-------- + +๐Ÿ‘ค To find out if UC Mode will work at all on a specific site (before adjusting for timing), load your site with the following script: + +```python +from seleniumbase import SB + +with SB(uc=True) as sb: + sb.uc_open_with_reconnect(URL, reconnect_time="breakpoint") +``` + +(If you remain undetected while loading the page and performing manual actions, then you know you can create a working script once you swap the breakpoint with a time and add special methods like sb.uc_click as needed.) + +-------- + +๐Ÿ‘ค Multithreaded UC Mode: + +If you're using pytest for multithreaded UC Mode (which requires using one of the pytest [syntax formats](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/syntax_formats.md)), then all you have to do is set the number of threads when your script runs. (-n NUM) Eg: + +```zsh +pytest --uc -n 4 +``` + +(Then pytest-xdist is automatically used to spin up and process the threads.) + +If you don't want to use pytest for multithreading, then you'll need to do a little more work. That involves using a different multithreading library, (eg. concurrent.futures), and making sure that thread-locking is done correctly for processes that share resources. To handle that thread-locking, include sys.argv.append("-n") in your SeleniumBase file. + +Here's a sample script that uses concurrent.futures for spinning up multiple processes: + +```python +import sys +from concurrent.futures import ThreadPoolExecutor +from seleniumbase import Driver +sys.argv.append("-n") # Tell SeleniumBase to do thread-locking as needed + +def launch_driver(url): + driver = Driver(uc=True) + try: + driver.get(url=url) + driver.sleep(2) + finally: + driver.quit() + +urls = ['https://seleniumbase.io/demo_page' for i in range(3)] +with ThreadPoolExecutor(max_workers=len(urls)) as executor: + for url in urls: + executor.submit(launch_driver, url) +``` + +-------- + +๐Ÿ‘ค What makes UC Mode work? + +Here are the 3 primary things that UC Mode does to make bots appear human: + +
        +
      • Modifies chromedriver to rename Chrome DevTools Console variables.
      • +
      • Launches Chrome browsers before attaching chromedriver to them.
      • +
      • Disconnects chromedriver from Chrome during stealthy actions.
      • +
      + +For example, if the Chrome DevTools Console variables aren't renamed, you can expect to find them easily when using selenium for browser automation: + + + +(If those variables are still there, then websites can easily detect your bots.) + +If you launch Chrome using chromedriver, then there will be settings that make your browser look like a bot. (Instead, UC Mode connects chromedriver to Chrome after the browser is launched, which makes Chrome look like a normal, human-controlled web browser.) + +While chromedriver is connected to Chrome, website services can detect it. Thankfully, raw selenium already includes driver.service.stop() for stopping the chromedriver service, driver.service.start() for starting the chromedriver service, and driver.start_session(capabilities) for reviving the active browser session with the given capabilities. (SeleniumBase UC Mode methods automatically use those raw selenium methods as needed.) + +Links to those raw Selenium method definitions have been provided for reference (but you don't need to call those methods directly): + + + +Also note that chromedriver isn't detectable in a browser tab if it never touches that tab. Here's a JS command that lets you open a URL in a new tab (from your current tab): + +
        +
      • window.open("URL"); --> (Info: W3Schools)
      • +
      + +The above JS method is used within SeleniumBase UC Mode methods for opening URLs in a stealthy way. Since some websites try to detect if your browser is a bot on the initial page load, this allows you to bypass detection in those situations. After a few seconds (customizable), UC Mode tells chromedriver to connect to that tab so that automated commands can now be issued. At that point, chromedriver could be detected if websites are looking for it (but generally websites only look for it during specific events, such as page loads, form submissions, and button clicks). + +Avoiding detection while clicking is easy if you schedule your clicks to happen at a future point when the chromedriver service has been stopped. Here's a JS command that lets you schedule events (such as clicks) to happen in the future: + +
    • window.setTimeout(function() { SCRIPT }, MS); --> (Info: W3Schools)
    • + +The above JS method is used within the SeleniumBase UC Mode method: sb.uc_click(selector) so that clicking can be done in a stealthy way. UC Mode schedules your click, disconnects chromedriver from Chrome, waits a little (customizable), and reconnects. + +-------- + +๐Ÿ† Choosing the right CAPTCHA service for your business / website: + + + +As an ethical hacker / cybersecurity researcher who builds bots that bypass CAPTCHAs for sport, the CAPTCHA service that I personally recommend for keeping bots out is Google reCAPTCHA: + + + +Since Google makes Chrome, Google's own reCAPTCHA service has access to more data than other CAPTCHA services, and can therefore use that data to make better decisions about whether or not web activity is coming from real humans or automated bots. + +-------- + +โš–๏ธ Legal implications of web-scraping: + +Based on the following article, https://nubela.co/blog/meta-lost-the-scraping-legal-battle-to-bright-data/, (which outlines a court case where social-networking company: Meta lost the legal battle to data-scraping company: Bright Data), it was determined that web scraping is 100% legal in the eyes of the courts as long as: +1. The scraping is only done with public data and not private data. +2. The scraping isnโ€™t done while logged in on the site being scraped. + +If the above criteria are met, then scrape away! (According to the article) + +(Note: I'm not a lawyer, so I can't officially offer legal advice, but I can direct people to existing articles online where people can find their own answers.) + +-------- + +SeleniumBase + +
      SeleniumBase
      diff --git a/help_docs/useful_grep_commands.md b/help_docs/useful_grep_commands.md new file mode 100644 index 00000000000..6cfcae23659 --- /dev/null +++ b/help_docs/useful_grep_commands.md @@ -0,0 +1,37 @@ + + +## Useful grep commands + +There are several useful **grep** commands for helping you find and/or replace text in multiple files. Examples: + +#### List all files containing `self.get_new_driver(`, ignoring ".pyc" files, from the current directory: + +`grep -rl "self.get_new_driver(" * --exclude=\*.pyc` +OR +`grep -rl * -e "self.get_new_driver(" --exclude=\*.pyc` + +To only search `.py` files, use `--include=\*.py`: + +`grep -rl "self.get_new_driver(" * --include=\*.py` + +-------- + +#### Replace all occurrences of "foo_abc" with "bar_xyz" on Linux, for Python files from the current directory: + +`sed -i 's/foo_abc/bar_xyz/g' *.py` + +#### Replace all occurrences of "foo_abc" with "bar_xyz" on macOS, for Python files from the current directory: + +`sed -i '' 's/foo_abc/bar_xyz/g' *.py` + +-------- + +#### Find all chromedriver processes (this combines `ps` with `grep`): + +`ps -ef |grep chromedriver` + +-------- + +#### References: +* https://stackoverflow.com/questions/16956810/how-do-i-find-all-files-containing-specific-text-on-linux +* https://stackoverflow.com/questions/11392478/how-to-replace-a-string-in-multiple-files-in-linux-command-line/20721292#20721292 diff --git a/help_docs/using_safari_driver.md b/help_docs/using_safari_driver.md old mode 100755 new mode 100644 index dbd0a0f9d15..fdd245a7e99 --- a/help_docs/using_safari_driver.md +++ b/help_docs/using_safari_driver.md @@ -1,18 +1,11 @@ -### Info about using Safari Driver for running automated tests on macOS + -(NOTE: SafariDriver requires Safari 10 running on OS X 10.11 El Capitan or greater) +## Using Safari's WebDriver for running browser tests on macOS -You can find info on Safari Driver if you scroll down to that section on the [SeleniumHQ downloads page](https://www.seleniumhq.org/download/). +*(NOTE: Safari's WebDriver requires macOS 10.13 "High Sierra" or later.)* -That page will tell you to [download the required Safari Driver browser extension (SafariDriver.safariextz) here at this link](http://selenium-release.storage.googleapis.com/index.html?path=2.48/). +You can find the official Apple documentation regarding "Testing with WebDriver in Safari" on the following page: [https://developer.apple.com/documentation/webkit/testing_with_webdriver_in_safari](https://developer.apple.com/documentation/webkit/testing_with_webdriver_in_safari) -For that to work, you'll need to [download the Standalone Selenium Server from here](http://docs.seleniumhq.org/download/) and put that JAR file in ``/usr/local/bin/``. To make the next step easier, rename the downloaded JAR file to ``selenium-server-standalone.jar`` (if it's not already called that). +Run `safaridriver --enable` once in a terminal to enable Safari's WebDriver. (If youโ€™re upgrading from a previous macOS release, you may need to prefix the command with `sudo`.) -Next, configure the Selenium Server JAR file into your PATH like this: - -```bash -export SELENIUM_SERVER_JAR=/usr/local/bin/selenium-server-standalone.jar -export PATH=$PATH:/usr/local/bin/selenium-server-standalone.jar -``` - -Now you're ready to run automated tests on Safari if you use ``--browser=safari`` on the command line when running your tests/scripts with SeleniumBase. +Now you can use `--safari` to run your **SeleniumBase** tests on Safari. diff --git a/help_docs/verify_webdriver.md b/help_docs/verify_webdriver.md old mode 100755 new mode 100644 index 7cb53e6b5b1..a2cefedee2a --- a/help_docs/verify_webdriver.md +++ b/help_docs/verify_webdriver.md @@ -1,23 +1,69 @@ -### Verify that web drivers were successfully installed + -*You can do this by checking inside a Python command prompt. (NOTE: xkcd is a webcomic)* +

      Verifying that web drivers are installed

      + +When using SeleniumBase, the driver is automatically downloaded to the `seleniumbase/drivers` folder as needed, and doesn't need to be on the System Path when running tests. + +Drivers can also be manually downloaded to the `seleniumbase/drivers` folder with commands such as: + +```zsh +sbase get chromedriver +sbase get geckodriver +sbase get edgedriver +``` + +-------- + +If you want to check that you have the correct driver installed on your System PATH (which is no longer necessary unless using the Selenium Grid), then continue reading below: + +*This assumes you've already downloaded a driver to your **System PATH** with a command such as:* + +```zsh +sbase get chromedriver --path +``` + +(The above `--path` addition is for Linux/Mac only, which uses `/usr/local/bin/`. The "Path" is different on Windows, and you'll need to manually copy the driver to your System Path, which is defined in the Control Panel's System Environment Variables.) + +*You can verify that the correct drivers exist on your System Path by checking inside a Python command prompt.* #### Verifying ChromeDriver -```bash + +```zsh python ->>> from selenium import webdriver ->>> browser = webdriver.Chrome() ->>> browser.get("http://xkcd.com/1337/") ->>> browser.close() +``` + +```python +>>> from seleniumbase import get_driver +>>> driver = get_driver("chrome", headless=False) +>>> driver.get("https://www.google.com/chrome") +>>> driver.quit() +>>> exit() +``` + +#### Verifying Geckodriver (Firefox WebDriver) + +```zsh +python +``` + +```python +>>> from seleniumbase import get_driver +>>> driver = get_driver("firefox", headless=False) +>>> driver.get("https://www.mozilla.org/firefox") +>>> driver.quit() >>> exit() ``` -#### Verifying FirefoxDriver (Geckodriver) -```bash +#### Verifying WebDriver for Safari + +```zsh python ->>> from selenium import webdriver ->>> browser = webdriver.Firefox() ->>> browser.get("http://xkcd.com/1337/") ->>> browser.close() +``` + +```python +>>> from seleniumbase import get_driver +>>> driver = get_driver("safari", headless=False) +>>> driver.get("https://www.apple.com/safari") +>>> driver.quit() >>> exit() ``` diff --git a/help_docs/virtualenv_instructions.md b/help_docs/virtualenv_instructions.md old mode 100755 new mode 100644 index f5bb0407a02..24b50ac1f39 --- a/help_docs/virtualenv_instructions.md +++ b/help_docs/virtualenv_instructions.md @@ -1,75 +1,129 @@ + + ## Virtual Environment Tutorial -### **Step 1**: First install [VirtualEnv](http://virtualenv.readthedocs.org/en/latest/) and [VirtualEnvWrapper](http://virtualenvwrapper.readthedocs.org/en/latest/) (if not installed): +There are multiple ways of creating a **[Python virtual environment](https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/#creating-a-virtual-environment)**. This tutorial covers two of those: -#### macOS / Linux: +* The ``venv`` command (included with Python 3+). +* The virtualenvwrapper ``mkvirtualenv`` command. -(*Shortcut*: Run "``source virtualenv_install.sh``" from the top-level SeleniumBase folder to perform the following steps.) +``venv`` creates virtual environments in the location where run (generally with Python projects). -```bash -python -m pip install --upgrade virtualenv -python -m pip install --upgrade virtualenvwrapper -source `which virtualenvwrapper.sh` +``mkvirtualenv`` creates virtual environments in one place (generally in your home directory). + +(The [Python Software Foundation](https://www.python.org/psf/) recommends ``venv`` for creating virtual environments.) + + +

      Option 1: Using "venv"

      + +> macOS/Linux terminal (``python3 -m venv ENV``) + +```zsh +python3 -m venv sbase_env +source sbase_env/bin/activate +``` + +> Windows CMD prompt (``py -m venv ENV``): + +```zsh +py -m venv sbase_env +call sbase_env\\Scripts\\activate +``` + +To exit a virtual env, type ``deactivate``. + +-------- + +

      Option 2: Using virtualenvwrapper

      + +> macOS/Linux terminal: + +```zsh +python3 -m pip install virtualenvwrapper --force-reinstall export WORKON_HOME=$HOME/.virtualenvs +source `which virtualenvwrapper.sh` ``` -If you add ``source `which virtualenvwrapper.sh` `` to your local bash file (``~/.bash_profile`` on macOS, or ``~/.bashrc`` on Linux), virtualenvwrapper commands such as ``mkvirtualenv`` will be available whenever you open a new command prompt. +(*Shortcut*: Run ``source virtualenv_install.sh`` from the top-level SeleniumBase folder to perform the above steps.) -#### Windows: +(If you add ``source `which virtualenvwrapper.sh` `` to your local bash file (``~/.bash_profile`` on macOS, or ``~/.bashrc`` on Linux), virtualenvwrapper commands such as ``mkvirtualenv`` will be available whenever you open a new command prompt.) -(*Shortcut*: Run "``virtualenv_install.bat``" from the top-level SeleniumBase folder to perform the following steps.) +> Windows CMD prompt: -```bash -python -m pip install --upgrade virtualenv -python -m pip install --upgrade virtualenvwrapper-win +```zsh +py -m pip install virtualenvwrapper-win --force-reinstall --user ``` -### **Step 2**: Now create a virtual environment: +(*Shortcut*: Run ``win_virtualenv.bat`` from the top-level SeleniumBase folder to perform the above step.) + -#### macOS / Linux / Windows: +

      Create a virtual environment:

      -* Using ``mkvirtualenv``: -```bash -mkvirtualenv seleniumbase_venv +* ``mkvirtualenv ENV``: + +```zsh +mkvirtualenv sbase_env ``` -(If you have multiple versions of Python installed on your machine, and you want your virtual environment to use a specific Python version, add ``--python=PATH_TO_PYTHON_EXE`` with the Python executable to use.) -* Using ``virtualenv``: -```bash -virtualenv seleniumbase_venv -source seleniumbase_venv/bin/activate +(If you have multiple versions of Python installed on your machine, and you want your virtual environment to use a specific Python version, add ``--python=PATH_TO_PYTHON_EXE`` to your ``mkvirtualenv`` command with the Python executable to use.) + + +

      virtualenvwrapper commands:

      + +Creating a virtual environment: + +```zsh +mkvirtualenv sbase_env ``` -* (Python 3) Using ``mvenv``: -```bash -python3 -mvenv seleniumbase_venv -source seleniumbase_venv/bin/activate +Leaving your virtual environment: + +```zsh +deactivate ``` ---- +Returning to a virtual environment: -If you ever need to leave your virtual environment, use the following command: +```zsh +workon sbase_env +``` -```bash -deactivate +Listing all virtual environments: + +```zsh +workon +``` + +Deleting a virtual environment: + +```zsh +rmvirtualenv sbase_env ``` -You can always jump back into your virtual environment later: +-------- -```bash -workon seleniumbase_venv +If the ``python`` and ``python3`` versions don't match (*while in a virtualenv on macOS or Linux*), the following command will sync the versions: + +```zsh +alias python=python3 ``` -To list all existing virtual environments: +(To remove an alias, use: ``unalias NAME``) + +-------- -```bash -lsvirtualenv +To verify the ``python`` version, use: + +```zsh +python --version ``` -To delete a virtual environment: +To see the PATH of your ``python`` (macOS/Linux), use: -```bash -rmvirtualenv VIRTUAL_ENV_NAME +```zsh +which python ``` -
      [python-guide.org/en/latest/dev/virtualenvs](http://docs.python-guide.org/en/latest/dev/virtualenvs/) has more information about Python virtual environments. +-------- + +> [python-guide.org/en/latest/dev/virtualenvs](http://docs.python-guide.org/en/latest/dev/virtualenvs/) has more information about Python virtual environments. For specific details about VirtualEnv and VirtualEnvWrapper, see [http://virtualenv.readthedocs.org/en/latest/](http://virtualenv.readthedocs.org/en/latest/) and [http://virtualenvwrapper.readthedocs.org/en/latest/](http://virtualenvwrapper.readthedocs.org/en/latest/). diff --git a/help_docs/webdriver_installation.md b/help_docs/webdriver_installation.md old mode 100755 new mode 100644 index 8bf06507a0d..c7aebbf72be --- a/help_docs/webdriver_installation.md +++ b/help_docs/webdriver_installation.md @@ -1,66 +1,105 @@ -## Installing Google Chromedriver, Firefox Geckodriver, Microsoft Edge Driver, etc. + +

      Installing webdrivers

      -To run web automation, you'll need to download a web driver for each browser you plan on using and place those on your System **[PATH](http://java.com/en/download/help/path.xml)**. Additionaly, you can place drivers in the [SeleniumBase `drivers` folder](https://github.com/seleniumbase/SeleniumBase/blob/master/drivers). If you plan on taking the latter option, here are some commands that'll automatically download the driver you need into the ``drivers`` folder once you've installed SeleniumBase: +To run web automation, you need webdrivers for each browser you plan on using. With SeleniumBase, drivers are downloaded automatically (as needed) into the SeleniumBase `drivers/` folder. -```bash -seleniumbase install chromedriver -seleniumbase install geckodriver -seleniumbase install edgedriver -seleniumbase install iedriver -seleniumbase install operadriver +๐ŸŽ›๏ธ You can also download drivers manually with these commands: + +```zsh +seleniumbase get chromedriver +seleniumbase get geckodriver +seleniumbase get edgedriver ``` -If you plan on using the [Selenium Grid integration](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/utilities/selenium_grid/ReadMe.md) (which allows for remote webdriver), you'll need to put the drivers on your System PATH. On macOS and Linux, ``/usr/local/bin`` is a good PATH spot. On Windows, you may need to set the System PATH under Environment Variables to include the location where you placed the driver files. As a shortcut, you could place the driver files into your Python ``Scripts/`` folder in the location where you have Python installed, which should already be on your System PATH. +After running the commands above, web drivers will get downloaded into the `seleniumbase/drivers/` folder. SeleniumBase uses those drivers during tests. (The drivers don't come with SeleniumBase by default.) -Here's where you can go to manually install web drivers from the source: +If the necessary driver is not found in this location while running tests, SeleniumBase will instead look for the driver on the System PATH. If the necessary driver is not on the System PATH either, SeleniumBase will automatically attempt to download the required driver. -* For Chrome, get [Chromedriver](https://sites.google.com/a/chromium.org/chromedriver/downloads) on your System PATH. +๐ŸŽ›๏ธ You can also download specific versions of drivers. Examples: -* For Firefox, get [Geckodriver](https://github.com/mozilla/geckodriver/releases) on your System PATH. +```zsh +sbase get chromedriver 114 +sbase get chromedriver 114.0.5735.90 +sbase get chromedriver stable +sbase get chromedriver beta +sbase get chromedriver dev +sbase get chromedriver canary +sbase get chromedriver previous # One major version before the stable version +sbase get chromedriver mlatest # Milestone latest version for detected browser +sbase get edgedriver 115.0.1901.183 +``` -* For Microsoft Edge, get [Edge Driver (Microsoft WebDriver)](https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/) on your System PATH. +(NOTE: `sbase` is a shortcut for `seleniumbase`) -* For Safari, get [Safari Driver](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/using_safari_driver.md) on your System PATH. +-------- -* For Opera, get [Opera Chromium Driver](https://github.com/operasoftware/operachromiumdriver/releases) on your System PATH.. +If you plan on using the [Selenium Grid integration](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/utilities/selenium_grid/ReadMe.md) (which allows for ``remote`` webdriver), you'll need to put the drivers on your System PATH. On macOS and Linux, ``/usr/local/bin`` is a good PATH spot. On Windows, you may need to set the System PATH under Environment Variables to include the location where you placed the driver files. As a shortcut, you could place the driver files into your Python ``Scripts/`` folder in the location where you have Python installed, which should already be on your System PATH. -* For PhantomJS headless browser automation, get [PhantomJS](http://phantomjs.org/download.html) on your System PATH. (NOTE: PhantomJS is no longer officially supported by SeleniumHQ) +Here's where you can go to manually get web drivers from the source: -**macOS**: +* For Chrome, get [Chromedriver](https://sites.google.com/a/chromium.org/chromedriver/downloads) on your System PATH. + +* For Edge, get [Edge Driver (Microsoft WebDriver)](https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/) on your System PATH. + +* For Firefox, get [Geckodriver](https://github.com/mozilla/geckodriver/releases) on your System PATH. + +* For Safari, get [Safari Driver](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/using_safari_driver.md) on your System PATH. -* You can also install drivers by using ``brew`` (aka ``homebrew``), but you'll need to install that first. [Brew installation instructions are here](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/install_python_pip_git.md). +**macOS shortcuts**: -```bash -brew cask install chromedriver +๐ŸŽ›๏ธ You can also install drivers by using ``brew`` (aka ``homebrew``): + +```zsh +brew install --cask chromedriver brew install geckodriver ``` -(NOTE: If your existing version of chromedriver is less than 2.40, **upgrading is required** in order to keep up with the latest version of Chrome!) +๐ŸŽ›๏ธ You can also upgrade existing webdrivers: -```bash -brew cask upgrade chromedriver +```zsh +brew upgrade --cask chromedriver brew upgrade geckodriver ``` -**Linux**: +**Linux shortcuts**: -If you still need the web drivers, here are some scripts to help you install chromedriver and geckodriver on a Linux machine: +๐ŸŽ›๏ธ If you still need drivers, these scripts download `chromedriver` and `geckodriver` to a Linux machine: -```bash -wget http://chromedriver.storage.googleapis.com/2.40/chromedriver_linux64.zip +```zsh +wget https://chromedriver.storage.googleapis.com/114.0.5735.90/chromedriver_linux64.zip unzip chromedriver_linux64.zip mv chromedriver /usr/local/bin/ chmod +x /usr/local/bin/chromedriver ``` -```bash -wget https://github.com/mozilla/geckodriver/releases/download/v0.21.0/geckodriver-v0.21.0-linux64.tar.gz -tar xvfz geckodriver-v0.21.0-linux64.tar.gz +```zsh +wget https://github.com/mozilla/geckodriver/releases/download/v0.35.0/geckodriver-v0.35.0-linux64.tar.gz +tar xvfz geckodriver-v0.35.0-linux64.tar.gz mv geckodriver /usr/local/bin/ chmod +x /usr/local/bin/geckodriver ``` -* If you wish to verify that web drivers are working, **[follow these instructions](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/verify_webdriver.md)**. +To verify that web drivers are working, **[follow these instructions](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/verify_webdriver.md)**. + +-------- + +**Browser Binaries**: + +๐ŸŽ›๏ธ Use the `sbase get` command to download the `Chrome for Testing` and `Chrome-Headless-Shell` browser binaries. Example: + +```zsh +sbase get cft # (For `Chrome for Testing`) +sbase get chs # (For `Chrome-Headless-Shell`) +``` + +Those commands download those binaries into the `seleniumbase/drivers` folder. +To use the binaries from there in SeleniumBase scripts, set the `binary_location` to `cft` or `chs`. + +(Source: https://googlechromelabs.github.io/chrome-for-testing/) + +-------- + + diff --git a/install.bat b/install.bat deleted file mode 100755 index 362818a3702..00000000000 --- a/install.bat +++ /dev/null @@ -1,5 +0,0 @@ -@ECHO OFF -python -m pip install --upgrade pip -pip install -e . --upgrade --no-cache-dir --progress-bar off -seleniumbase install chromedriver -seleniumbase install geckodriver diff --git a/install.sh b/install.sh index b5712783f06..3cbbfa87f1f 100755 --- a/install.sh +++ b/install.sh @@ -1,5 +1,2 @@ #!/usr/bin/env bash -python -m pip install --upgrade pip -pip install -e . --upgrade --no-cache-dir --progress-bar off -seleniumbase install chromedriver -seleniumbase install geckodriver +pip install -e . --use-pep517 --config-settings="editable_mode=compat" diff --git a/integrations/__init__.py b/integrations/__init__.py old mode 100755 new mode 100644 diff --git a/integrations/azure/azure_pipelines/ReadMe.md b/integrations/azure/azure_pipelines/ReadMe.md old mode 100755 new mode 100644 index f968b8cba64..d7836a570cd --- a/integrations/azure/azure_pipelines/ReadMe.md +++ b/integrations/azure/azure_pipelines/ReadMe.md @@ -13,12 +13,17 @@ * **Set up a new plan (it's free) and follow the steps...** -![](https://cdn2.hubspot.net/hubfs/100006/images/github_azure_pipelines_1.png "GitHub Azure Pipelines") +![](https://seleniumbase.github.io/cdn/img/azure/github_azure_pipelines_1.png "GitHub Azure Pipelines") -![](https://cdn2.hubspot.net/hubfs/100006/images/github_azure_pipelines_2.png "GitHub Azure Pipelines") +---------- + +![](https://seleniumbase.github.io/cdn/img/azure/github_azure_pipelines_2.png "GitHub Azure Pipelines") + +---------- -![](https://cdn2.hubspot.net/hubfs/100006/images/github_azure_pipelines_3.png "GitHub Azure Pipelines") +![](https://seleniumbase.github.io/cdn/img/azure/github_azure_pipelines_3.png "GitHub Azure Pipelines") +---------- ### Step 2. Go to Microsoft Azure DevOps to set up your environment @@ -26,36 +31,47 @@ * **Follow the steps...** -#### "Start free with Pipelines" if you don't already have an account: +#### Select "Start free with GitHub >": + +![](https://seleniumbase.github.io/cdn/img/azure/azure_devops_1a.png "Azure DevOps") -![](https://cdn2.hubspot.net/hubfs/100006/images/azure_devops_1.png "Azure DevOps") +---------- #### Give your new project a name and set visibility to public (for your SeleniumBase fork): -![](https://cdn2.hubspot.net/hubfs/100006/images/azure_devops_2.png "Azure DevOps") +![](https://seleniumbase.github.io/cdn/img/azure/azure_devops_2.png "Azure DevOps") + +---------- #### Select that your code is hosted on GitHub: -![](https://cdn2.hubspot.net/hubfs/100006/images/azure_devops_3.png "Azure DevOps") +![](https://seleniumbase.github.io/cdn/img/azure/azure_devops_3.png "Azure DevOps") + +---------- #### Select your fork of SeleniumBase as your repository: -![](https://cdn2.hubspot.net/hubfs/100006/images/azure_devops_4.png "Azure DevOps") +![](https://seleniumbase.github.io/cdn/img/azure/azure_devops_4.png "Azure DevOps") + +---------- #### Copy the [azure-pipelines.yml](https://github.com/seleniumbase/SeleniumBase/blob/master/azure-pipelines.yml) file from SeleniumBase into the azure-pipelines.yml box to create your new pipeline: -![](https://cdn2.hubspot.net/hubfs/100006/images/azure_devops_5.png "Azure DevOps") +![](https://seleniumbase.github.io/cdn/img/azure/azure_devops_5.png "Azure DevOps") #### When you're done copying, click "Run". +---------- ### Step 3. Congratulations! Your browser tests are now running! * **Here's what a SeleniumBase sample run in Azure Pipelines may look like:** -[https://dev.azure.com/seleniumbase/seleniumbase/\_build/results?buildId=15](https://dev.azure.com/seleniumbase/seleniumbase/_build/results?buildId=15) +[https://dev.azure.com/seleniumbase/seleniumbase/\_build/results?buildId=234](https://dev.azure.com/seleniumbase/seleniumbase/_build/results?buildId=234) -![](https://cdn2.hubspot.net/hubfs/100006/images/azure_devops_6.png "Azure DevOps") +![](https://seleniumbase.github.io/cdn/img/azure/azure_devops_6.png "Azure DevOps") + +---------- #### Every time you create a pull request now, Azure Pipelines will run your tests automatically. diff --git a/integrations/azure/jenkins/ReadMe.md b/integrations/azure/jenkins/ReadMe.md old mode 100755 new mode 100644 index 96cdb2580dd..6cf5840734c --- a/integrations/azure/jenkins/ReadMe.md +++ b/integrations/azure/jenkins/ReadMe.md @@ -1,4 +1,6 @@ -### Building a browser-based test automation server with [Jenkins on Azure](https://azuremarketplace.microsoft.com/en-au/marketplace/apps/azure-oss.jenkins) by using [SeleniumBase](https://github.com/seleniumbase/SeleniumBase) +### Building a browser-based test automation server with Jenkins on Azure by using SeleniumBase + +(For the official Microsoft tutorial, see [Get Started: Install Jenkins on an Azure Linux VM](https://docs.microsoft.com/en-us/azure/developer/jenkins/configure-on-linux-vm), and then continue with [Step 4](#step4) below to resume SeleniumBase setup after you've created your Jenkins instance.) ---------- @@ -6,117 +8,130 @@ * **(You'll be using your own repository eventually.)** - ### Step 1. Find Jenkins in the Azure Marketplace #### Search for ["Jenkins" in the Azure Marketplace](https://portal.azure.com/#blade/Microsoft_Azure_Marketplace/GalleryFeaturedMenuItemBlade/selectedMenuItemId/home/searchQuery/jenkins/resetMenuId/) and select the ``Jenkins (Publisher: Microsoft)`` result to get to the Jenkins Start page. -![](https://cdn2.hubspot.net/hubfs/100006/images/jenkins_on_azure_01.png "Jenkins on Azure") +![](https://seleniumbase.github.io/cdn/img/azure/jenkins_on_azure_01.png "Jenkins on Azure") +---------- ### Step 2. Launch a Jenkins instance #### Click "Create" and follow the steps... -![](https://cdn2.hubspot.net/hubfs/100006/images/jenkins_on_azure_02.png "Jenkins on Azure") +![](https://seleniumbase.github.io/cdn/img/azure/jenkins_on_azure_02.png "Jenkins on Azure") + +---------- #### Continue to "Additional Settings" when you're done with "Basics". -![](https://cdn2.hubspot.net/hubfs/100006/images/jenkins_on_azure_03.png "Jenkins on Azure") +![](https://seleniumbase.github.io/cdn/img/azure/jenkins_on_azure_03.png "Jenkins on Azure") + +---------- #### On the "Additional Settings" section, set the Size to "B2s": -![](https://cdn2.hubspot.net/hubfs/100006/images/jenkins_on_azure_04.png "Jenkins on Azure") +![](https://seleniumbase.github.io/cdn/img/azure/jenkins_on_azure_04.png "Jenkins on Azure") + +---------- #### Once you've reached Step 5, click "Create" to complete the setup. -![](https://cdn2.hubspot.net/hubfs/100006/images/jenkins_on_azure_05.png "Jenkins on Azure") +![](https://seleniumbase.github.io/cdn/img/azure/jenkins_on_azure_05.png "Jenkins on Azure") +---------- ### Step 3. Inspect your new Jenkins instance to SSH into the new machine #### Once your new Jenkins instance has finished launching, you should be able to see the main page: -![](https://cdn2.hubspot.net/hubfs/100006/images/jenkins_on_azure_06.png "Jenkins on Azure") +![](https://seleniumbase.github.io/cdn/img/azure/jenkins_on_azure_06.png "Jenkins on Azure") + +---------- #### On the main page, you should be able to find the Public IP Address. * **Use that IP Address to SSH into the machine:** -```bash +```zsh ssh USERNAME@IP_ADDRESS ``` -![](https://cdn2.hubspot.net/hubfs/100006/images/jenkins_on_azure_07.png "Jenkins on Azure") +![](https://seleniumbase.github.io/cdn/img/azure/jenkins_on_azure_07.png "Jenkins on Azure") + +---------- + ### Step 4. Clone the SeleniumBase repository from the root ("/") directory. -```bash +```zsh cd / sudo git clone https://github.com/seleniumbase/SeleniumBase.git ``` - ### Step 5. Enter the "linux" folder -```bash +```zsh cd SeleniumBase/integrations/linux/ ``` ### Step 6. Give the "jenkins" user sudo access (See [jenkins_permissions.sh](https://github.com/seleniumbase/SeleniumBase/blob/master/integrations/linux/jenkins_permissions.sh) for details) -```bash +```zsh ./jenkins_permissions.sh ``` ### Step 7. Become the "jenkins" user and enter a "bash" shell -```bash +```zsh sudo su jenkins bash ``` ### Step 8. Install dependencies (See [Linuxfile.sh](https://github.com/seleniumbase/SeleniumBase/blob/master/integrations/linux/Linuxfile.sh) for details) -```bash +```zsh ./Linuxfile.sh ``` ### Step 9. Start up the headless browser display mechanism: Xvfb (See [Xvfb_launcher.sh](https://github.com/seleniumbase/SeleniumBase/blob/master/integrations/linux/Xvfb_launcher.sh) for details) -```bash +```zsh ./Xvfb_launcher.sh ``` ### Step 10. Go to the SeleniumBase directory -```bash +```zsh cd /SeleniumBase ``` ### Step 11. Install the [requirements](https://github.com/seleniumbase/SeleniumBase/blob/master/requirements.txt) for SeleniumBase -```bash +```zsh sudo pip install -r requirements.txt --upgrade ``` ### Step 12. Install SeleniumBase (Make sure you already installed the requirements above) -```bash +```zsh sudo python setup.py develop ``` ### Step 13. Install chromedriver -```bash +```zsh sudo seleniumbase install chromedriver ``` ### Step 14. Run an [example test](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/my_first_test.py) in Chrome to verify installation (May take up to 10 seconds) -![](https://cdn2.hubspot.net/hubfs/100006/images/jenkins_on_azure_08.png "Jenkins on Azure") +![](https://seleniumbase.github.io/cdn/img/azure/jenkins_on_azure_08.png "Jenkins on Azure") + +---------- -```bash +```zsh pytest examples/my_first_test.py --headless --browser=chrome ``` @@ -126,7 +141,9 @@ pytest examples/my_first_test.py --headless --browser=chrome (Depending on your version of Jenkins, you may see the following screen, or nothing at all.) -![](https://cdn2.hubspot.net/hubfs/100006/images/jenkins_on_azure_09.png "Jenkins on Azure") +![](https://seleniumbase.github.io/cdn/img/azure/jenkins_on_azure_09.png "Jenkins on Azure") + +---------- #### Initially, Jenkins uses only ``http``, which makes it less secure. @@ -136,33 +153,37 @@ pytest examples/my_first_test.py --headless --browser=chrome ``ssh -L 127.0.0.1:8080:localhost:8080 USERNAME@DNS_NAME`` - ### Step 16. Login to Jenkins #### If you've correctly set up SSH Port Forwarding, the url will be ``http://127.0.0.1:8080/`` -![](https://cdn2.hubspot.net/hubfs/100006/images/jenkins_on_azure_10.png "Jenkins on Azure") +![](https://seleniumbase.github.io/cdn/img/azure/jenkins_on_azure_10.png "Jenkins on Azure") + +---------- #### You'll need to get the password from the SSH terminal on the Linux machine to log in: -```bash +```zsh sudo cat /var/lib/jenkins/secrets/initialAdminPassword ``` - ### Step 17. Customize Jenkins -![](https://cdn2.hubspot.net/hubfs/100006/images/jenkins_on_azure_11.png "Jenkins on Azure") +![](https://seleniumbase.github.io/cdn/img/azure/jenkins_on_azure_11.png "Jenkins on Azure") +---------- ### Step 18. Create an Admin user -![](https://cdn2.hubspot.net/hubfs/100006/images/jenkins_on_azure_12.png "Jenkins on Azure") +![](https://seleniumbase.github.io/cdn/img/azure/jenkins_on_azure_12.png "Jenkins on Azure") + +---------- #### Once Jenkins has finished loading, the top left of the page should look like this: -![](https://cdn2.hubspot.net/hubfs/100006/images/jenkins_on_azure_13.png "Jenkins on Azure") +![](https://seleniumbase.github.io/cdn/img/azure/jenkins_on_azure_13.png "Jenkins on Azure") +---------- ### Step 19. Create a new Jenkins job @@ -171,45 +192,54 @@ sudo cat /var/lib/jenkins/secrets/initialAdminPassword * **Select "Freestyle project"** * **Click "OK"** -![](https://cdn2.hubspot.net/hubfs/100006/images/jenkins_on_azure_14.png "Jenkins on Azure") +![](https://seleniumbase.github.io/cdn/img/azure/jenkins_on_azure_14.png "Jenkins on Azure") +---------- ### Step 20. Setup your new Jenkins job * **Under "Source Code Management", select "Git".** * **For the "Repository URL", put: ``https://github.com/seleniumbase/SeleniumBase.git``. (You'll eventually be using your own clone of the repository here.)** -![](https://cdn2.hubspot.net/hubfs/100006/images/jenkins_on_azure_15.png "Jenkins on Azure") +![](https://seleniumbase.github.io/cdn/img/azure/jenkins_on_azure_15.png "Jenkins on Azure") + +---------- * **Under "Build", click the "Add build step" dropdown.** * **Select "Execute shell".** * **For the "Command", paste:** -```bash + +```zsh cd examples pytest my_first_test.py --headless ``` -![](https://cdn2.hubspot.net/hubfs/100006/images/jenkins_on_azure_16.png "Jenkins on Azure") +![](https://seleniumbase.github.io/cdn/img/azure/jenkins_on_azure_16.png "Jenkins on Azure") + +---------- #### Click "Save" when you're done. * **You'll see the following page after that:** -![](https://cdn2.hubspot.net/hubfs/100006/images/jenkins_on_azure_18.png "Jenkins on Azure") +![](https://seleniumbase.github.io/cdn/img/azure/jenkins_on_azure_18.png "Jenkins on Azure") +---------- ### Step 21. Run your new Jenkins job * **Click on "Build Now"** * **(If everything was done correctly, you'll see a blue dot appear after a few seconds, indicating that the test job passed.)** -![](https://cdn2.hubspot.net/hubfs/100006/images/jenkins_on_azure_19.png "Jenkins on Azure") +![](https://seleniumbase.github.io/cdn/img/azure/jenkins_on_azure_19.png "Jenkins on Azure") +---------- ### Step 22. See the top Jenkins page for an overview of all jobs -![](https://cdn2.hubspot.net/hubfs/100006/images/jenkins_on_azure_17.png "Jenkins on Azure") +![](https://seleniumbase.github.io/cdn/img/azure/jenkins_on_azure_17.png "Jenkins on Azure") +---------- ### Step 23. Future Work diff --git a/integrations/behave/ReadMe.md b/integrations/behave/ReadMe.md new file mode 100644 index 00000000000..39cf9e0bd51 --- /dev/null +++ b/integrations/behave/ReadMe.md @@ -0,0 +1,241 @@ +

      ๐Ÿ Behave test runner for SeleniumBase ๐Ÿ

      + +๐Ÿ (Utilizes the [Behave BDD Python library](https://github.com/behave/behave). For more info, see the [Behave tutorial](https://behave.readthedocs.io/en/stable/tutorial.html) and read about [Behave's Gherkin model](https://behave.readthedocs.io/en/stable/gherkin.html).) + +๐Ÿ Behave examples with SeleniumBase: [SeleniumBase/examples/behave_bdd](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/behave_bdd) + +```zsh +> cd examples/behave_bdd/ +> behave features/realworld.feature -T -D dashboard -k + +Dashboard: /Users/michael/github/SeleniumBase/examples/behave_bdd/dashboard.html +******************************************************************************** +Feature: SeleniumBase scenarios for the RealWorld App # features/realworld.feature:1 + + Scenario: Verify RealWorld App (log in / sign out) # features/realworld.feature:3 + Given Open "seleniumbase.io/realworld/login" # ../../sbase/steps.py:10 + And Clear Session Storage # ../../sbase/steps.py:669 + When Type "demo_user" into "#username" # ../../sbase/steps.py:40 + And Type "secret_pass" into "#password" # ../../sbase/steps.py:40 + And Do MFA "GAXG2MTEOR3DMMDG" into "#totpcode" # ../../sbase/steps.py:322 + Then Assert exact text "Welcome!" in "h1" # ../../sbase/steps.py:157 + And Highlight "img#image1" # ../../sbase/steps.py:184 + And Click 'a:contains("This Page")' # ../../sbase/steps.py:27 + And Save screenshot to logs # ../../sbase/steps.py:239 + When Click link "Sign out" # ../../sbase/steps.py:195 + Then Assert element 'a:contains("Sign in")' # ../../sbase/steps.py:120 + And Assert text "You have been signed out!" # ../../sbase/steps.py:145 + โœ… Scenario Passed! + +- Dashboard: /Users/michael/github/SeleniumBase/examples/behave_bdd/dashboard.html +--- LogPath: /Users/michael/github/SeleniumBase/examples/behave_bdd/latest_logs/ +================================================================================== +1 feature passed, 0 failed, 0 skipped +1 scenario passed, 0 failed, 0 skipped +12 steps passed, 0 failed, 0 skipped, 0 undefined +Took 0m4.682s +``` + +๐Ÿ Another example, which uses higher-level Behave steps to simplify the ``.feature`` file: + +```zsh +> cd examples/behave_bdd/ +> behave features/calculator.feature:61 -T -D dashboard -k + +Dashboard: /Users/michael/github/SeleniumBase/examples/behave_bdd/dashboard.html +******************************************************************************** +Feature: SeleniumBase scenarios for the Calculator App # features/calculator.feature:1 + + Background: # features/calculator.feature:3 + + Scenario: 7.0 ร— (3 + 3) = 42 # features/calculator.feature:49 + Given Open the Calculator App # features/steps/calculator.py:4 + When Press C # features/steps/calculator.py:9 + And Press 7 # features/steps/calculator.py:79 + And Press . # features/steps/calculator.py:104 + And Press 0 # features/steps/calculator.py:94 + And Press ร— # features/steps/calculator.py:29 + And Press ( # features/steps/calculator.py:14 + And Press 3 # features/steps/calculator.py:59 + And Press + # features/steps/calculator.py:39 + And Press 3 # features/steps/calculator.py:59 + And Press ) # features/steps/calculator.py:19 + Then Verify output is "7.0ร—(3+3)" # features/steps/calculator.py:135 + When Press = # features/steps/calculator.py:44 + Then Verify output is "42" # features/steps/calculator.py:135 + โœ… Scenario Passed! + +- Dashboard: /Users/michael/github/SeleniumBase/examples/behave_bdd/dashboard.html +--- LogPath: /Users/michael/github/SeleniumBase/examples/behave_bdd/latest_logs/ +================================================================================== +1 feature passed, 0 failed, 0 skipped +1 scenario passed, 0 failed, 8 skipped +14 steps passed, 0 failed, 60 skipped, 0 undefined +Took 0m1.672s +``` + +๐Ÿโšช With the Dashboard enabled, you'll get one of these: + + + +### ๐Ÿ Behave-Gherkin files: + +๐Ÿ The ``*.feature`` files can use any step seen from: + +```zsh +behave --steps-catalog +``` + +๐Ÿ SeleniumBase includes several pre-made Behave steps, which you can use by creating a Python file with the following line in your ``features/steps/`` directory: + +```python +from seleniumbase.behave import steps # noqa +``` + +๐Ÿ Inside your ``features/environment.py`` file, you should have the following: + +```python +from seleniumbase import BaseCase +from seleniumbase.behave import behave_sb +behave_sb.set_base_class(BaseCase) # Accepts a BaseCase subclass +from seleniumbase.behave.behave_sb import before_all # noqa +from seleniumbase.behave.behave_sb import before_feature # noqa +from seleniumbase.behave.behave_sb import before_scenario # noqa +from seleniumbase.behave.behave_sb import before_step # noqa +from seleniumbase.behave.behave_sb import after_step # noqa +from seleniumbase.behave.behave_sb import after_scenario # noqa +from seleniumbase.behave.behave_sb import after_feature # noqa +from seleniumbase.behave.behave_sb import after_all # noqa +``` + +๐Ÿ If you've already created a subclass of ``BaseCase`` with custom methods, you can swap ``BaseCase`` in with your own subclass, which will allow you to easily use your own custom methods in your Behave step definitions. + +๐Ÿ Here's an example Python file in the ``features/steps/`` folder: + +```python +from behave import step + + +@step("Open the Swag Labs Login Page") +def go_to_swag_labs(context): + sb = context.sb + sb.goto("https://www.saucedemo.com") + sb.clear_local_storage() + + +@step("Login to Swag Labs with {user}") +def login_to_swag_labs(context, user): + sb = context.sb + sb.type("#user-name", user) + sb.type("#password", "secret_sauce\n") + + +@step("Verify that the current user is logged in") +def verify_logged_in(context): + sb = context.sb + sb.assert_element("#header_container") + sb.assert_element("#react-burger-menu-btn") + sb.assert_element("#shopping_cart_container") + + +@step('Add "{item}" to cart') +def add_item_to_cart(context, item): + sb = context.sb + sb.click('div.inventory_item:contains("%s") button[name*="add"]' % item) +``` + +๐Ÿ A ``*.feature`` file could look like this: + +```gherkin +Feature: SeleniumBase scenarios for the Swag Labs App + + Background: + Given Open the Swag Labs Login Page + + Scenario: User can order a backpack from the store + When Login to Swag Labs with standard_user + Then Verify that the current user is logged in + And Save price of "Backpack" to + When Add "Backpack" to Cart + Then Verify shopping cart badge shows 1 item(s) + When Click on shopping cart icon + And Click Checkout + And Enter checkout info: First, Last, 12345 + And Click Continue + Then Verify 1 "Backpack"(s) in cart + And Verify cost of "Backpack" is + And Verify item total is $29.99 + And Verify tax amount is $2.40 + And Verify total cost is $32.39 + When Click Finish + Then Verify order complete + When Logout from Swag Labs + Then Verify on Login page +``` + +๐Ÿ Here's another example of a ``*.feature`` file: + +```gherkin +Feature: SeleniumBase scenarios for the RealWorld App + + Scenario: Verify RealWorld App (log in / sign out) + Given Open "seleniumbase.io/realworld/login" + And Clear Session Storage + When Type "demo_user" into "#username" + And Type "secret_pass" into "#password" + And Do MFA "GAXG2MTEOR3DMMDG" into "#totpcode" + Then Assert text "Welcome!" in "h1" + And Highlight element "img#image1" + And Click 'a:contains("This Page")' + And Save screenshot to logs + When Click link "Sign out" + Then Assert element 'a:contains("Sign in")' + And Assert text "You have been signed out!" +``` + +๐Ÿ If there's a test failure, that's easy to spot: + +```zsh +Feature: SeleniumBase scenarios for the Fail Page # features/fail_page.feature:1 + + Scenario: Fail test on purpose to see what happens # features/fail_page.feature:3 + When Open the Fail Page # features/steps/fail_page.py:4 + Then Fail test on purpose # features/steps/fail_page.py:9 + Assertion Failed: This test fails on purpose! + Captured stdout: + >>> STEP FAILED: (#2) Fail test on purpose + Class / Feature: SeleniumBase scenarios for the Fail Page + Test / Scenario: Fail test on purpose to see what happens + + โŒ Scenario Failed! +``` + +๐Ÿ๐ŸŽ–๏ธ For convenience, the [SeleniumBase Behave GUI](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/behave_gui.md) lets you run ``behave`` scripts from a Desktop app. + +๐Ÿ๐ŸŽ–๏ธ To launch it, call ``sbase behave-gui`` or ``sbase gui-behave``: + +```zsh +sbase behave-gui +* Starting the SeleniumBase Behave Commander GUI App... +``` + + + +๐Ÿ๐ŸŽ–๏ธ You can customize the tests that show up there: + +```zsh +sbase behave-gui # all tests +sbase behave-gui -i=calculator # tests with "calculator" in the name +sbase behave-gui features/ # tests located in the "features/" folder +sbase behave-gui features/calculator.feature # tests in that feature +``` + +-------- + +
      To learn more about SeleniumBase, check out the Docs Site:
      + +SeleniumBase.io Docs + +
      All the code is on GitHub:
      + +SeleniumBase on GitHub diff --git a/integrations/behave/behave.ini b/integrations/behave/behave.ini new file mode 100644 index 00000000000..84fa8ee77ff --- /dev/null +++ b/integrations/behave/behave.ini @@ -0,0 +1,3 @@ +[behave] +show_skipped=false +show_timings=false diff --git a/integrations/behave/features/__init__.py b/integrations/behave/features/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/integrations/behave/features/behave.ini b/integrations/behave/features/behave.ini new file mode 100644 index 00000000000..84fa8ee77ff --- /dev/null +++ b/integrations/behave/features/behave.ini @@ -0,0 +1,3 @@ +[behave] +show_skipped=false +show_timings=false diff --git a/integrations/behave/features/calculator.feature b/integrations/behave/features/calculator.feature new file mode 100644 index 00000000000..ae9aa6378bf --- /dev/null +++ b/integrations/behave/features/calculator.feature @@ -0,0 +1,83 @@ +Feature: SeleniumBase scenarios for the Calculator App + + Background: + Given Open the Calculator App + + Scenario: Pressing "C" outputs "0" + When Press C + Then Verify output is "0" + + Scenario: 1 + 2 + 3 + 4 + 5 = "15" + When Press C + And Press 1 + And Press + + And Press 2 + And Press + + And Press 3 + And Press + + And Press 4 + And Press + + And Press 5 + Then Verify output is "1+2+3+4+5" + When Press = + Then Verify output is "15" + + Scenario: 6 ร— 7 ร— 8 ร— 9 = "3024" + When Press C + And Press 6 + And Press ร— + And Press 7 + And Press ร— + And Press 8 + And Press ร— + And Press 9 + Then Verify output is "6ร—7ร—8ร—9" + When Press = + Then Verify output is "3024" + + Scenario: 44 - 11 = "33" + When Press C + And Press 4 + And Press 4 + And Press - + And Press 1 + And Press 1 + Then Verify output is "44-11" + When Press = + Then Verify output is "33" + + Scenario: 7.0 ร— (3 + 3) = "42" + When Press C + And Press 7 + And Press . + And Press 0 + And Press ร— + And Press ( + And Press 3 + And Press + + And Press 3 + And Press ) + Then Verify output is "7.0ร—(3+3)" + When Press = + Then Verify output is "42" + + Scenario: 4.5 ร— 68 = "306" + When Press C + And Evaluate [4.5 ร— 68] + Then Verify output is "306" + + Scenario Outline: รท = + When Press C + And Press [] + And Press รท + And Press [] + And Press = + Then Verify output is "" + Examples: + | First | Second | Result | + | 1948 | 4 | 487 | + | 21 | 0 | Error | + + Scenario: Save calculator screenshot to logs + Given Press [1337] + Given Save calculator screenshot to logs diff --git a/integrations/behave/features/environment.py b/integrations/behave/features/environment.py new file mode 100644 index 00000000000..09110f71937 --- /dev/null +++ b/integrations/behave/features/environment.py @@ -0,0 +1,11 @@ +from seleniumbase import BaseCase +from seleniumbase.behave import behave_sb +behave_sb.set_base_class(BaseCase) # Accepts a BaseCase subclass +from seleniumbase.behave.behave_sb import before_all # noqa +from seleniumbase.behave.behave_sb import before_feature # noqa +from seleniumbase.behave.behave_sb import before_scenario # noqa +from seleniumbase.behave.behave_sb import before_step # noqa +from seleniumbase.behave.behave_sb import after_step # noqa +from seleniumbase.behave.behave_sb import after_scenario # noqa +from seleniumbase.behave.behave_sb import after_feature # noqa +from seleniumbase.behave.behave_sb import after_all # noqa diff --git a/integrations/behave/features/fail_page.feature b/integrations/behave/features/fail_page.feature new file mode 100644 index 00000000000..e09141e8867 --- /dev/null +++ b/integrations/behave/features/fail_page.feature @@ -0,0 +1,5 @@ +Feature: SeleniumBase scenarios for the Fail Page + + Scenario: Fail test on purpose to see what happens + When Open the Fail Page + Then Fail test on purpose diff --git a/integrations/behave/features/realworld.feature b/integrations/behave/features/realworld.feature new file mode 100644 index 00000000000..b246fba1ed9 --- /dev/null +++ b/integrations/behave/features/realworld.feature @@ -0,0 +1,12 @@ +Feature: SeleniumBase scenarios for the RealWorld App + + Scenario: Verify RealWorld App (log in / sign out) + Given Open the RealWorld Login Page + When Login to the RealWorld App + Then Assert exact text "Welcome!" in "h1" + When Highlight element "img#image1" + And Click element 'a:contains("This Page")' + Then Save a screenshot to the logs + When Click link "Sign out" + Then Assert element 'a:contains("Sign in")' + And Assert text "You have been signed out!" diff --git a/integrations/behave/features/steps/__init__.py b/integrations/behave/features/steps/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/integrations/behave/features/steps/calculator.py b/integrations/behave/features/steps/calculator.py new file mode 100644 index 00000000000..cd0926c58ea --- /dev/null +++ b/integrations/behave/features/steps/calculator.py @@ -0,0 +1,144 @@ +from behave import step + + +@step("Open the Calculator App") +def go_to_calculator(context): + context.sb.goto("https://seleniumbase.io/apps/calculator") + + +@step("Press C") +def press_c(context): + context.sb.click("button#clear") + + +@step("Press (") +def press_open_paren(context): + context.sb.click('button[id="("]') + + +@step("Press )") +def press_close_paren(context): + context.sb.click('button[id=")"]') + + +@step("Press รท") +def press_divide(context): + context.sb.click("button#divide") + + +@step("Press ร—") +def press_multiply(context): + context.sb.click("button#multiply") + + +@step("Press -") +def press_subtract(context): + context.sb.click("button#subtract") + + +@step("Press +") +def press_add(context): + context.sb.click("button#add") + + +@step("Press =") +def press_equal(context): + context.sb.click("button#equal") + + +@step("Press 1") +def press_1(context): + context.sb.click('button[id="1"]') + + +@step("Press 2") +def press_2(context): + context.sb.click('button[id="2"]') + + +@step("Press 3") +def press_3(context): + context.sb.click('button[id="3"]') + + +@step("Press 4") +def press_4(context): + context.sb.click('button[id="4"]') + + +@step("Press 5") +def press_5(context): + context.sb.click('button[id="5"]') + + +@step("Press 6") +def press_6(context): + context.sb.click('button[id="6"]') + + +@step("Press 7") +def press_7(context): + context.sb.click('button[id="7"]') + + +@step("Press 8") +def press_8(context): + context.sb.click('button[id="8"]') + + +@step("Press 9") +def press_9(context): + context.sb.click('button[id="9"]') + + +@step("Press 0") +def press_0(context): + context.sb.click('button[id="0"]') + + +@step("Press โ†") +def press_delete(context): + context.sb.click("button#delete") + + +@step("Press .") +def press_dot(context): + context.sb.click('button[id="."]') + + +@step("Press [{number}]") +def enter_number_into_calc(context, number): + sb = context.sb + for digit in number: + sb.click('button[id="%s"]' % digit) + + +@step("Evaluate [{equation}]") +def evaluate_equation(context, equation): + sb = context.sb + for key in equation: + if key == " ": + continue + elif key == "รท": + sb.click("button#divide") + elif key == "ร—": + sb.click("button#multiply") + elif key == "-": + sb.click("button#subtract") + elif key == "+": + sb.click("button#add") + else: + sb.click('button[id="%s"]' % key) + sb.click("button#equal") + + +@step('Verify output is "{output}"') +def verify_output(context, output): + sb = context.sb + sb.assert_exact_text(output, "#output") + + +@step("Save calculator screenshot to logs") +def save_calculator_screenshot_to_logs(context): + sb = context.sb + sb.save_screenshot_to_logs() diff --git a/integrations/behave/features/steps/fail_page.py b/integrations/behave/features/steps/fail_page.py new file mode 100644 index 00000000000..3761c50a07e --- /dev/null +++ b/integrations/behave/features/steps/fail_page.py @@ -0,0 +1,11 @@ +from behave import step + + +@step("Open the Fail Page") +def go_to_error_page(context): + context.sb.goto("https://seleniumbase.io/error_page/") + + +@step("Fail test on purpose") +def fail_on_purpose(context): + context.sb.fail("This test fails on purpose!") diff --git a/integrations/behave/features/steps/real_world.py b/integrations/behave/features/steps/real_world.py new file mode 100644 index 00000000000..f349cbadec7 --- /dev/null +++ b/integrations/behave/features/steps/real_world.py @@ -0,0 +1,100 @@ +from behave import step + + +@step("Open the RealWorld Login Page") +def go_to_realworld(context): + sb = context.sb + context.sb.goto("https://seleniumbase.io/realworld/login") + sb.clear_session_storage() + + +@step("Login to the RealWorld App") +def login_to_realworld(context): + sb = context.sb + sb.type("#username", "demo_user") + sb.type("#password", "secret_pass") + sb.enter_mfa_code("#totpcode", "GAXG2MTEOR3DMMDG") # 6-digit + + +@step('Highlight element {selector}') +def highlight(context, selector): + if selector.startswith('"') or selector.startswith("'"): + selector = selector[1:] + if selector.endswith('"') or selector.endswith("'"): + selector = selector[:-1] + sb = context.sb + sb.highlight(selector) + + +@step('Click element {selector}') +def click(context, selector): + if selector.startswith('"') or selector.startswith("'"): + selector = selector[1:] + if selector.endswith('"') or selector.endswith("'"): + selector = selector[:-1] + sb = context.sb + sb.click(selector) + + +@step('Click link {link}') +def click_link(context, link): + if link.startswith('"') or link.startswith("'"): + link = link[1:] + if link.endswith('"') or link.endswith("'"): + link = link[:-1] + sb = context.sb + sb.click_link(link) + + +@step('Save a screenshot to the logs') +def save_screenshot_to_logs(context): + sb = context.sb + sb.save_screenshot_to_logs() + + +@step('Assert element {selector}') +def assert_element(context, selector): + if selector.startswith('"') or selector.startswith("'"): + selector = selector[1:] + if selector.endswith('"') or selector.endswith("'"): + selector = selector[:-1] + sb = context.sb + sb.assert_element(selector) + + +@step('Assert text {text} in {selector}') +def assert_text_in_selector(context, text, selector): + if text.startswith('"') or text.startswith("'"): + text = text[1:] + if text.endswith('"') or text.endswith("'"): + text = text[:-1] + if selector.startswith('"') or selector.startswith("'"): + selector = selector[1:] + if selector.endswith('"') or selector.endswith("'"): + selector = selector[:-1] + sb = context.sb + sb.assert_text(text, selector) + + +@step('Assert text {text}') +def assert_text(context, text): + if text.startswith('"') or text.startswith("'"): + text = text[1:] + if text.endswith('"') or text.endswith("'"): + text = text[:-1] + sb = context.sb + sb.assert_text(text) + + +@step('Assert exact text {text} in {selector}') +def assert_exact_text(context, text, selector): + if text.startswith('"') or text.startswith("'"): + text = text[1:] + if text.endswith('"') or text.endswith("'"): + text = text[:-1] + if selector.startswith('"') or selector.startswith("'"): + selector = selector[1:] + if selector.endswith('"') or selector.endswith("'"): + selector = selector[:-1] + sb = context.sb + sb.assert_exact_text(text, selector) diff --git a/integrations/behave/features/steps/swag_labs.py b/integrations/behave/features/steps/swag_labs.py new file mode 100644 index 00000000000..189b6f4f758 --- /dev/null +++ b/integrations/behave/features/steps/swag_labs.py @@ -0,0 +1,168 @@ +from behave import step + + +@step("Open the Swag Labs Login Page") +def go_to_swag_labs(context): + sb = context.sb + sb.goto("https://www.saucedemo.com") + sb.clear_local_storage() + + +@step("Login to Swag Labs with {user}") +def login_to_swag_labs(context, user): + sb = context.sb + sb.type("#user-name", user) + sb.type("#password", "secret_sauce\n") + + +@step("Verify that the current user is logged in") +def verify_logged_in(context): + sb = context.sb + sb.assert_element("#header_container") + sb.assert_element("#react-burger-menu-btn") + sb.assert_element("#shopping_cart_container") + + +@step('Add "{item}" to cart') +def add_item_to_cart(context, item): + sb = context.sb + sb.click('div.inventory_item:contains("%s") button[name*="add"]' % item) + + +@step('Save price of "{item}" to <{var}>') +def save_price_of_item(context, item, var): + sb = context.sb + price = sb.get_text( + 'div.inventory_item:contains("%s") .inventory_item_price' % item + ) + sb.variables[var] = price + + +@step('Remove "{item}" from cart') +def remove_item_to_cart(context, item): + sb = context.sb + sb.click('div.inventory_item:contains("%s") button[name*="remove"]' % item) + + +@step("Verify shopping cart badge shows {number} item(s)") +def verify_badge_number(context, number): + sb = context.sb + sb.assert_exact_text(number, "span.shopping_cart_badge") + + +@step("Verify shopping cart badge is missing") +def verify_badge_missing(context): + sb = context.sb + sb.assert_element_not_visible("span.shopping_cart_badge") + + +@step("Click on shopping cart icon") +def click_shopping_cart(context): + sb = context.sb + sb.click("#shopping_cart_container a") + + +@step("Click Checkout") +def click_checkout(context): + sb = context.sb + sb.click("#checkout") + + +@step("Enter checkout info: {first_name}, {last_name}, {zip_code}") +def enter_checkout_info(context, first_name, last_name, zip_code): + sb = context.sb + sb.type("#first-name", first_name) + sb.type("#last-name", last_name) + sb.type("#postal-code", zip_code) + + +@step("Click Continue") +def click_continue(context): + sb = context.sb + sb.click("input#continue") + + +@step('Verify {quantity} "{item}"(s) in cart') +def verify_item_in_cart(context, quantity, item): + sb = context.sb + sb.assert_exact_text( + quantity, + 'div.cart_item:contains("%s") div.cart_quantity' % item + ) + + +@step('Verify cost of "{item}" is <{var}>') +def verify_cost_of_item(context, item, var): + sb = context.sb + earlier_price = sb.variables[var] + sb.assert_exact_text( + earlier_price, + 'div.cart_item_label:contains("%s") .inventory_item_price' % item + ) + + +@step("Verify item total is {item_total}") +def verify_item_total(context, item_total): + sb = context.sb + sb.assert_exact_text( + "Item total: %s" % item_total, + "div.summary_subtotal_label", + timeout=1 + ) + + +@step("Verify tax amount is {tax_amount}") +def verify_tax_amount(context, tax_amount): + sb = context.sb + sb.assert_exact_text( + "Tax: %s" % tax_amount, + "div.summary_tax_label", + timeout=1 + ) + + +@step("Verify total cost is {total_cost}") +def verify_total_cost(context, total_cost): + sb = context.sb + sb.assert_exact_text( + "Total: %s" % total_cost, + "div.summary_total_label", + timeout=1 + ) + + +@step("Click Finish") +def click_finish(context): + sb = context.sb + sb.click("button#finish") + + +@step("Verify order complete") +def verify_order_complete(context): + sb = context.sb + sb.assert_exact_text("Thank you for your order!", "h2") + sb.assert_element('img[alt="Pony Express"]') + + +@step("Logout from Swag Labs") +def logout_from_swag_labs(context): + sb = context.sb + sb.js_click("a#logout_sidebar_link") + + +@step("Verify on Login page") +def verify_on_login_page(context): + sb = context.sb + sb.assert_element("#login-button") + + +@step("Sort items from Z to A") +def sort_items_from_z_to_a(context): + sb = context.sb + sb.select_option_by_text("select.product_sort_container", "Name (Z to A)") + + +@step('Verify "{item}" on top') +def verify_item_on_top(context, item): + sb = context.sb + sb.assert_text(item, "div.inventory_item_name") diff --git a/integrations/behave/features/swag_labs.feature b/integrations/behave/features/swag_labs.feature new file mode 100644 index 00000000000..63d2c572c16 --- /dev/null +++ b/integrations/behave/features/swag_labs.feature @@ -0,0 +1,75 @@ +Feature: SeleniumBase scenarios for the Swag Labs App + + Background: + Given Open the Swag Labs Login Page + + Scenario: User can log in and log out successfully + When Login to Swag Labs with standard_user + Then Verify that the current user is logged in + When Logout from Swag Labs + Then Verify on Login page + + Scenario: User can order a backpack from the store + When Login to Swag Labs with standard_user + Then Verify that the current user is logged in + And Save price of "Backpack" to + When Add "Backpack" to Cart + Then Verify shopping cart badge shows 1 item(s) + When Click on shopping cart icon + And Click Checkout + And Enter checkout info: First, Last, 12345 + And Click Continue + Then Verify 1 "Backpack"(s) in cart + And Verify cost of "Backpack" is + And Verify item total is $29.99 + And Verify tax amount is $2.40 + And Verify total cost is $32.39 + When Click Finish + Then Verify order complete + When Logout from Swag Labs + Then Verify on Login page + + Scenario: User can order two items from the store + When Login to Swag Labs with standard_user + And Add "Bike Light" to Cart + And Add "Fleece Jacket" to Cart + Then Verify shopping cart badge shows 2 item(s) + When Click on shopping cart icon + And Click Checkout + And Enter checkout info: First, Last, 54321 + And Click Continue + Then Verify 1 "Bike Light"(s) in cart + Then Verify 1 "Fleece Jacket"(s) in cart + And Verify item total is $59.98 + And Verify tax amount is $4.80 + And Verify total cost is $64.78 + When Click Finish + Then Verify order complete + When Logout from Swag Labs + Then Verify on Login page + + Scenario: User can sort items by name from Z to A + When Login to Swag Labs with standard_user + And Sort items from Z to A + Then Verify "Test.allTheThings() T-Shirt" on top + When Logout from Swag Labs + Then Verify on Login page + + Scenario: User can add & remove 6 items to/from cart + When Login to Swag Labs with standard_user + And Add "Backpack" to Cart + And Add "Bike Light" to Cart + And Add "Bolt T-Shirt" to Cart + And Add "Fleece Jacket" to Cart + And Add "Onesie" to Cart + And Add "Test.allTheThings() T-Shirt" to Cart + Then Verify shopping cart badge shows 6 item(s) + When Remove "Backpack" from Cart + And Remove "Bike Light" from Cart + And Remove "Bolt T-Shirt" from Cart + And Remove "Fleece Jacket" from Cart + And Remove "Onesie" from Cart + And Remove "Test.allTheThings() T-Shirt" from Cart + Then Verify shopping cart badge is missing + When Logout from Swag Labs + Then Verify on Login page diff --git a/integrations/brython/ReadMe.md b/integrations/brython/ReadMe.md new file mode 100644 index 00000000000..83882d21a32 --- /dev/null +++ b/integrations/brython/ReadMe.md @@ -0,0 +1,34 @@ +## Getting Started with Brython + +* Brython was designed for replacing JavaScript with Python. +* This tutorial will show you how to get started quickly. + +### Here's the web app you'll be creating: + +Brython Demo + +### 0. Install ``brython``: + +```zsh +pip install brython +``` + +### 1. Get a web server up and running: + +Run from [SeleniumBase/integrations/brython](https://github.com/seleniumbase/SeleniumBase/tree/master/integrations/brython): + +```zsh +python -m http.server +``` + +(You can stop the server by using Ctrl+C) + +### 2. Navigate to [http://localhost:8000/](http://localhost:8000/) + +Now click on the examples to see Brython in action. + +### 3. For more info, see the following: + +* https://brython.info/ +* https://pypi.org/project/brython/ +* https://github.com/brython-dev/brython diff --git a/integrations/brython/index.html b/integrations/brython/index.html new file mode 100644 index 00000000000..94eabf8602e --- /dev/null +++ b/integrations/brython/index.html @@ -0,0 +1,44 @@ + + + + + + + + + + + +

      + + + +
      + +

      + Library / Indexed DB Example +

      +
      + + + \ No newline at end of file diff --git a/integrations/brython/index.py b/integrations/brython/index.py new file mode 100644 index 00000000000..6e3e5cf67c0 --- /dev/null +++ b/integrations/brython/index.py @@ -0,0 +1,5 @@ +from browser import document + + +def setup_page(): + document["topHeader"].textContent = "Brython Examples:" diff --git a/integrations/brython/library.html b/integrations/brython/library.html new file mode 100644 index 00000000000..312fcb69061 --- /dev/null +++ b/integrations/brython/library.html @@ -0,0 +1,203 @@ + + + + + + + + + + +

      Library

      + +
      TitleAuthorISBNActions
      + + + + \ No newline at end of file diff --git a/integrations/docker/ReadMe.md b/integrations/docker/ReadMe.md old mode 100755 new mode 100644 index 8763214a8b0..ffa67f347d3 --- a/integrations/docker/ReadMe.md +++ b/integrations/docker/ReadMe.md @@ -1,52 +1,50 @@ ## Docker setup instructions for SeleniumBase -#### 1. Install the Docker Toolbox: +#### 1. Install the Docker Desktop: You can get that from here: -https://www.docker.com/products/docker-toolbox +https://www.docker.com/products/docker-desktop/ You might also want to install the Docker Engine: -https://docs.docker.com/engine/installation/ +https://docs.docker.com/engine/install/ -#### 2. Create your SeleniumBase Docker environment: +#### 2. Go to the SeleniumBase home directory on the command line, which is where [Dockerfile](https://github.com/seleniumbase/SeleniumBase/blob/master/Dockerfile) is located. (This assumes you've already cloned the SeleniumBase repo.) - docker-machine create --driver virtualbox seleniumbase +#### 3. Create your Docker image from your Dockerfile: (Get ready to wait awhile) -##### (If your Docker environment ever goes down for any reason, you can bring it back up with a restart.) +**(Windows / Linux / Intel macOS)** - docker-machine restart seleniumbase - -#### 3. Configure your shell: + docker build -t seleniumbase . - eval "$(docker-machine env seleniumbase)" +**(Apple Silicon macOS, eg. M1/M2/M3/M4):** -#### 4. Go to the SeleniumBase home directory on the command line, which is where [Dockerfile](https://github.com/seleniumbase/SeleniumBase/blob/master/Dockerfile) is located. (This assumes you've already cloned the SeleniumBase repo.) +Users should first [Enable Rosetta in Docker Desktop](https://stackoverflow.com/a/76586216/7058266). (Otherwise Chrome will crash on launch with errors such as: `"InvalidSessionIdException"` and `"Unable to receive message from renderer"`) -#### 5. Create your Docker image from your Dockerfile: (Get ready to wait awhile) +Enable Rosetta - docker build -t seleniumbase . +Then you can run these commands: -#### 6. Run [the example test](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/my_first_test.py) with Chrome inside your Docker: (Once the test completes after a few seconds, you'll automatically exit the Docker shell) + export DOCKER_DEFAULT_PLATFORM=linux/amd64 - docker run seleniumbase ./run_docker_test_in_chrome.sh + docker build --platform linux/amd64 -t seleniumbase . -#### 7. Now run the same test with Firefox inside your Docker: +#### 4. Run [the example test](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/my_first_test.py) with Chrome inside your Docker: (Once the test completes after a few seconds, you'll automatically exit the Docker shell) - docker run seleniumbase ./run_docker_test_in_firefox.sh + docker run seleniumbase ./run_docker_test_in_chrome.sh -#### 8. You can also enter Docker and stay inside the shell: +#### 5. You can also enter Docker and stay inside the shell: docker run -i -t seleniumbase -#### 9. Now you can run the example test from inside the Docker shell: +#### 6. Now you can run the example test from inside the Docker shell: ./run_docker_test_in_chrome.sh -#### 10. When you're satisfied, you may exit the Docker shell: +#### 7. When you're satisfied, you may exit the Docker shell: exit -#### 11. (Optional) Since Docker images and containers take up a lot of space, you may want to clean up your machine from time to time when theyโ€™re not being used: +#### 8. (Optional) Since Docker images and containers take up a lot of space, you may want to clean up your machine from time to time when theyโ€™re not being used: Details on that can be found here: http://stackoverflow.com/questions/17236796/how-to-remove-old-docker-containers @@ -63,15 +61,10 @@ If you want to completely remove all of your Docker containers and images, use t docker rm -f $(docker ps -a -q) docker rmi -f $(docker images -q) -Finally, if you want to wipe out your SeleniumBase Docker virtualbox, use these commands: - - docker-machine kill seleniumbase - docker-machine rm seleniumbase - For more cleanup commands, check out: https://codefresh.io/blog/everyday-hacks-docker/ -#### 13. (Optional) More reading on Docker can be found here: +#### 9. (Optional) More reading on Docker can be found here: * https://docs.docker.com * https://docs.docker.com/get-started/ * https://docs.docker.com/docker-for-mac/ diff --git a/integrations/docker/docker_config.cfg b/integrations/docker/docker_config.cfg deleted file mode 100755 index 9812ff3f0f4..00000000000 --- a/integrations/docker/docker_config.cfg +++ /dev/null @@ -1,3 +0,0 @@ -[nosetests] -nocapture=1 -logging-level=INFO diff --git a/integrations/docker/run_docker_test_in_firefox.sh b/integrations/docker/run_docker_test_in_firefox.sh deleted file mode 100755 index c0e19a77c1c..00000000000 --- a/integrations/docker/run_docker_test_in_firefox.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -set -e -# Run example test from inside Docker image -echo "Running example SeleniumBase test from Docker with headless Firefox..." -cd /SeleniumBase/examples/ && pytest my_first_test.py --browser=firefox --headless -exec "$@" diff --git a/integrations/github/ReadMe.md b/integrations/github/ReadMe.md new file mode 100644 index 00000000000..0880e63ebe8 --- /dev/null +++ b/integrations/github/ReadMe.md @@ -0,0 +1,5 @@ +### GitHub Integrations + +> **Table of Contents / Navigation:** +> - [**Actions/Workflows**](https://github.com/seleniumbase/SeleniumBase/blob/master/integrations/github/workflows/ReadMe.md) +> - [**Extras/Action-Integrations**](https://github.com/seleniumbase/SeleniumBase/blob/master/integrations/github/workflows/extras.md) diff --git a/integrations/github/workflows/ReadMe.md b/integrations/github/workflows/ReadMe.md new file mode 100644 index 00000000000..949ae68e31d --- /dev/null +++ b/integrations/github/workflows/ReadMe.md @@ -0,0 +1,53 @@ +## Running browser tests on [GitHub Actions](https://github.com/seleniumbase/SeleniumBase/actions) with [SeleniumBase](https://github.com/seleniumbase/SeleniumBase) + +![](https://seleniumbase.github.io/cdn/img/gha/github_workflows_7.png "GitHub Actions") + +---------- + +### Step 0. Create a fork of [SeleniumBase](https://github.com/seleniumbase/SeleniumBase) on GitHub to help you get started. + +* **(You'll be using your own repo eventually.)** + +![](https://seleniumbase.github.io/cdn/img/gha/github_workflows_2.png "Create a fork of SeleniumBase") + +---------- + +### Step 1. From the GitHub Actions tab, choose to set up a Python package Workflow. + +![](https://seleniumbase.github.io/cdn/img/gha/github_workflows_1.png "GitHub Actions") + +---------- + +### Step 2. Add your workflow ``.yml`` script. + +* **(If using a SeleniumBase fork, the script from https://github.com/seleniumbase/SeleniumBase/blob/master/.github/workflows/python-package.yml already exists to help guide you.)** + +![](https://seleniumbase.github.io/cdn/img/gha/github_workflows_9.png "GitHub Actions") + +### Step 3. Commit your changes to GitHub. + +![](https://seleniumbase.github.io/cdn/img/gha/github_workflows_4.png "GitHub Actions") + +---------- + +### Step 4. Your tests will now run on every pull request and on every commit to the ``master`` branch. + +* **(See https://github.com/seleniumbase/SeleniumBase/actions for the SeleniumBase example.)** + +![](https://seleniumbase.github.io/cdn/img/gha/github_workflows_5.png "GitHub Actions") + +* **(You can click inside each build for more details.)** + +![](https://seleniumbase.github.io/cdn/img/gha/github_workflows_6.png "GitHub Actions") + +* **(You can also see the specific steps being performed by each command.)** + +![](https://seleniumbase.github.io/cdn/img/gha/github_workflows_7.png "GitHub Actions") + +* **(You'll notice that web browsers such as Chrome and Firefox get installed for tests to use. SeleniumBase uses pytest for running tests while using Selenium to interact with web browsers.)** + +---------- + +### Congratulations! You now know how to create and run browser tests with GitHub Actions! + +### **Study [SeleniumBase](https://github.com/seleniumbase/SeleniumBase) to learn more!** diff --git a/integrations/github/workflows/extras.md b/integrations/github/workflows/extras.md new file mode 100644 index 00000000000..c7ba77a6198 --- /dev/null +++ b/integrations/github/workflows/extras.md @@ -0,0 +1,30 @@ +

      Integrations for GitHub Actions:

      + +### Uploading Artifacts: +* Here's an example using [upload-artifact@v6](https://github.com/actions/upload-artifact) to push up a SeleniumBase-generated artifact. + +```yml + - uses: actions/upload-artifact@v6 + with: + name: Click to download the presentation + path: saved_presentations/my_presentation.html +``` + +### Slack Notifications - [rtCamp/action-slack-notify](https://github.com/rtCamp/action-slack-notify) can be used to send notifications to Slack. + +Usage: +* Create a slack integration webhook if you don't have one already. +* Create a ``SLACK_WEBHOOK`` secret on your repository with the webhook token value. +* For this particular action, ``SLACK_CHANNEL`` is an optional environment variable that defaults to the webhook token channel if not specified. +* 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!): + +```yml + - name: Slack notification + uses: rtCamp/action-slack-notify@master + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + SLACK_CHANNEL: general + SLACK_ICON_EMOJI: rocket + SLACK_USERNAME: SeleniumBase + SLACK_MESSAGE: 'Actions workflow completed successful! :tada: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}' +``` diff --git a/integrations/google_cloud/ReadMe.md b/integrations/google_cloud/ReadMe.md old mode 100755 new mode 100644 index 6df79465468..f0e61d5684d --- a/integrations/google_cloud/ReadMe.md +++ b/integrations/google_cloud/ReadMe.md @@ -1,6 +1,9 @@ ### Building a browser-based test automation server on the [Google Cloud Platform](https://cloud.google.com/) by using [SeleniumBase](https://github.com/seleniumbase/SeleniumBase) -(This tutorial, [from a previous GCP Meetup live demo](http://www.meetup.com/Boston-Google-Cloud-Meetup/events/230839686/?showDescription=true), will teach you how to setup a Linux server for running automated browser tests. The cost of running this server is [$14.20/month on Google Cloud](https://console.cloud.google.com/launcher/details/bitnami-launchpad/jenkins) (enough to handle **6 parallel tests**). This is amazingly **20 times LESS expensive** than using competitors such as [BrowserStack](https://www.browserstack.com/pricing) or [Sauce Labs](https://saucelabs.com/pricing) **for the SAME number of parallel tests**.) +(This tutorial, [from a previous Google Cloud Meetup](https://www.meetup.com/Boston-Google-Cloud-Meetup/events/230839686/?showDescription=true), will teach you how to setup a Linux server for running automated browser tests. The cost of running this server is about [$13.60/month on Google Cloud](https://console.cloud.google.com/launcher/details/bitnami-launchpad/jenkins) (enough to handle **5 parallel tests**). This is less expensive than using other platforms.) + + +

      (SeleniumBase Google Cloud Video)

      #### Step 1. Open the Google Cloud Platform Cloud Launcher @@ -9,7 +12,7 @@ #### Step 2. Launch a Jenkins instance -![](http://cdn2.hubspot.net/hubfs/100006/images/gcp_cloud_launcher_jenkins_3.png "Finding Jenkins") +![](https://seleniumbase.github.io/cdn/img/gcp/gcp_cloud_launcher_jenkins.png "Finding Jenkins") * Under "Cloud Launcher", Click on "Jenkins Certified by Bitnami" * Click on "Launch on Compute Engine" @@ -19,84 +22,84 @@ #### Step 3. Connect with your new Jenkins instance -![](http://cdn2.hubspot.net/hubfs/100006/images/gcp_ssh.png "SSH into your Jenkins instance") +![](https://seleniumbase.github.io/cdn/img/gcp/gcp_ssh.png "SSH into your Jenkins instance") * SSH into your new instance by selecting: "SSH" => "Open in browser window" from the instance page. #### Step 4. Clone the SeleniumBase repository from the root ("/") directory. -```bash +```zsh cd / sudo git clone https://github.com/seleniumbase/SeleniumBase.git ``` #### Step 5. Enter the "linux" folder -```bash +```zsh cd SeleniumBase/integrations/linux/ ``` #### Step 6. Give Jenkins (aka "tomcat" user) sudo access (See [tomcat_permissions.sh](https://github.com/seleniumbase/SeleniumBase/blob/master/integrations/linux/tomcat_permissions.sh) for details) -```bash +```zsh ./tomcat_permissions.sh ``` #### Step 7. Become "tomcat" (the Jenkins user) and enter a "bash" shell -```bash +```zsh sudo su tomcat bash ``` #### Step 8. Install dependencies (See [Linuxfile.sh](https://github.com/seleniumbase/SeleniumBase/blob/master/integrations/linux/Linuxfile.sh) for details) -```bash +```zsh ./Linuxfile.sh ``` #### Step 9. Start up the headless browser display mechanism: Xvfb (See [Xvfb_launcher.sh](https://github.com/seleniumbase/SeleniumBase/blob/master/integrations/linux/Xvfb_launcher.sh) for details) -```bash +```zsh ./Xvfb_launcher.sh ``` #### Step 10. Go to the SeleniumBase directory -```bash +```zsh cd /SeleniumBase ``` #### Step 11. Install the [requirements](https://github.com/seleniumbase/SeleniumBase/blob/master/requirements.txt) for SeleniumBase -```bash +```zsh sudo pip install -r requirements.txt --upgrade ``` -#### Step 12. Install SeleniumBase (Make sure you already installed the requirements above) +#### Step 12. Install SeleniumBase -```bash +```zsh sudo python setup.py develop ``` -#### Step 13. Run an [example test](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/my_first_test.py) in Chrome to verify installation (May take up to 10 seconds) +#### Step 13. Run an [example test](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/my_first_test.py) on Chrome to verify installation (May take up to 10 seconds) -![](http://cdn2.hubspot.net/hubfs/100006/images/gcp_bitnami.png "Linux SSH Terminal") +![](https://seleniumbase.github.io/cdn/img/gcp/gcp_bitnami.png "Linux SSH Terminal") -```bash -pytest examples/my_first_test.py --headless --browser=chrome +```zsh +pytest examples/my_first_test.py --headless ``` -#### Step 14. If you like nosetests better than pytest, that works too +#### Step 14. If you prefer using nosetests, that works too -```bash -nosetests examples/my_first_test.py --headless --browser=chrome +```zsh +nosetests examples/my_first_test.py --headless ``` #### Step 15. You can also verify that the example test runs on Firefox -```bash -nosetests examples/my_first_test.py --headless --browser=firefox +```zsh +pytest examples/my_first_test.py --headless --browser=firefox ``` #### Step 16. Login to Jenkins @@ -105,7 +108,7 @@ nosetests examples/my_first_test.py --headless --browser=firefox #### Step 17. Create a new Jenkins job -![](http://cdn2.hubspot.net/hubfs/100006/images/gcp_jenkins_new_job_2.png "Create a Jenkins job") +![](https://seleniumbase.github.io/cdn/img/gcp/gcp_jenkins_new_job.png "Create a Jenkins job") * Click on "New Item" * Give your new Jenkins job a name (ex: "My_First_Test") @@ -118,9 +121,11 @@ nosetests examples/my_first_test.py --headless --browser=firefox * For the "Repository URL", put: ``https://github.com/seleniumbase/SeleniumBase.git``. (You'll eventually be using your own clone of the repository here.) * Under "Build", click the "Add build step" dropdown and then select "Execute shell". * For the "Command", put: -```bash -pytest examples/my_first_test.py --headless --browser=chrome + +```zsh +pytest examples/my_first_test.py --headless ``` + * Click "Save" when you're done. #### Step 19. Run your new Jenkins job @@ -134,11 +139,11 @@ If you have a web application that you want to test, you'll be able to create Se #### Congratulations! You're now well on your way to becoming a build & release / automation engineer! -## MySQL DB setup instructions +### MySQL DB setup instructions #### Step 21. Return to the Google Cloud Launcher and launch a MySQL Instance -![](http://cdn2.hubspot.net/hubfs/100006/images/gcp_mysql.png "Finding MySQL") +![](https://seleniumbase.github.io/cdn/img/gcp/gcp_mysql.png "Finding MySQL") * Under "Featured Solutions", Click on "MySQL" * Click on "Launch on Compute Engine" @@ -156,7 +161,7 @@ If you have a web application that you want to test, you'll be able to create Se #### Step 23. Get a MySQL GUI tool so that you can connect to your MySQL Instance -* You can download [MySQL Workbench](http://dev.mysql.com/downloads/tools/workbench/) for this. +* You can download [MySQL Workbench](https://dev.mysql.com/downloads/tools/workbench/) for this. #### Step 24. Create a new connection to your MySQL Instance @@ -178,8 +183,8 @@ If you have a web application that you want to test, you'll be able to create Se * For the "Execute shell", use the following as your updated "Command": -```bash -pytest examples/test_suite.py --headless --browser=chrome --with-db_reporting --with-testing_base +```zsh +pytest examples/test_suite.py --headless --with-db_reporting ``` * Click "Save" when you're done. @@ -189,4 +194,4 @@ pytest examples/test_suite.py --headless --browser=chrome --with-db_reporting -- * Click on "Build Now" * If all goes well, you should be seeing new rows appear in your MySQL DB tables. -#### Step 30. Congratulations! If you made it this far, you're awesome! +#### Step 30. Congratulations! You've successfully completed this tutorial! diff --git a/integrations/katalon/ReadMe.md b/integrations/katalon/ReadMe.md deleted file mode 100755 index ca4b51d487b..00000000000 --- a/integrations/katalon/ReadMe.md +++ /dev/null @@ -1 +0,0 @@ -### The ReadMe for the Katalon / Selenium IDE conversion tool has been moved to: [seleniumbase/utilities/selenium_ide/ReadMe.md](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/utilities/selenium_ide/ReadMe.md) and all related code has been moved to [seleniumbase/utilities/selenium_ide](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/utilities/selenium_ide) diff --git a/integrations/linux/Linuxfile.sh b/integrations/linux/Linuxfile.sh index beb82535833..91bc8100774 100755 --- a/integrations/linux/Linuxfile.sh +++ b/integrations/linux/Linuxfile.sh @@ -61,14 +61,6 @@ sudo apt-get install -y --force-yes libmysqlclient-dev sudo apt-get install -y --force-yes python-dev sudo apt-get install -y --force-yes python-MySQLdb -# Install PhantomJS -# cd ~ -# export PHANTOM_JS="phantomjs-2.1.1-linux-x86_64" -# sudo wget https://bitbucket.org/ariya/phantomjs/downloads/$PHANTOM_JS.tar.bz2 -# sudo tar xvjf $PHANTOM_JS.tar.bz2 -# sudo mv -f $PHANTOM_JS /usr/local/share -# sudo ln -sf /usr/local/share/$PHANTOM_JS/bin/phantomjs /usr/local/bin - # Install Chrome cd /tmp sudo wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb diff --git a/integrations/linux/ReadMe.md b/integrations/linux/ReadMe.md old mode 100755 new mode 100644 diff --git a/integrations/node_js/ReadMe.md b/integrations/node_js/ReadMe.md old mode 100755 new mode 100644 index 8d5296744b1..f72ff4ea8d2 --- a/integrations/node_js/ReadMe.md +++ b/integrations/node_js/ReadMe.md @@ -1,42 +1,48 @@ -## Creating a SeleniumBase Test Launcher using NodeJS +

      Creating a Test Runner with NodeJS + Express

      -Great news: It's really easy to create a customized web app for kicking off SeleniumBase jobs using NodeJS. This tutorial will walk you through all the steps that you need. (I'll assume that you've already installed SeleniumBase by following the instructions from the [top-level ReadMe](https://github.com/seleniumbase/SeleniumBase/blob/master/README.md) file.) +You can create a customized web app for running SeleniumBase tests by using NodeJS and Express. (This tutorial assumes that you've already installed [SeleniumBase](https://github.com/seleniumbase/SeleniumBase). -#### 1. Install NodeJS + + +#### 0. Clone SeleniumBase from GitHub + +* You'll need to work with the files located in the [integrations/node_js](https://github.com/seleniumbase/SeleniumBase/tree/master/integrations/node_js) folder. + +#### 1. Install NodeJS (if not installed) * Navigate to [https://nodejs.org/en/](https://nodejs.org/en/) * Click to download and install NodeJS -#### 2. Install Express for NodeJS +#### 2. Upgrade NodeJS (if using an older version) -```bash -npm install -g express +```zsh +npm install -g npm@latest ``` -#### 3. Install the Example Test Launcher for SeleniumBase from the ``integrations/node_js`` folder +#### 3. Install the example Test Runner for SeleniumBase from [integrations/node_js](https://github.com/seleniumbase/SeleniumBase/tree/master/integrations/node_js). (If dependencies were already installed, you can use `npm ci` for a speed improvement over `npm i` / `npm install` because `npm ci` uses `npm-shrinkwrap.json`, which is generated via ``npm shrinkwrap``.) -```bash +```zsh npm install ``` -(You should see a ``node_modules`` folder appear in your ``node_js`` folder.) +(You should see a `node_modules` folder appear in your `node_js` folder.) -#### 4. Run the NodeJS server for your SeleniumBase Test Launcher web app +#### 4. Run the NodeJS server for your SeleniumBase Test Runner web app -```bash +```zsh node server.js ``` -(You can always stop the server by using ``CTRL-C``.) +(You can stop the server by using Ctrl+C) -#### 5. Open the SeleniumBase Test Launcher web app +#### 5. Open the SeleniumBase Test Runner web app * Navigate to [http://127.0.0.1:3000/](http://127.0.0.1:3000/) #### 6. Run an example test -Click on one of the buttons to run a SeleniumBase example test +Click on a button to run a SeleniumBase example test. -#### 7. Enjoy your web app +#### 7. Expand your web app -Congratulations! You now have a web app for kicking off SeleniumBase tests! NodeJS makes it easy! +Now that you have a web app for running SeleniumBase tests, you can expand it to run any script that you want after pressing a button. diff --git a/integrations/node_js/__init__.py b/integrations/node_js/__init__.py old mode 100755 new mode 100644 diff --git a/integrations/node_js/index.html b/integrations/node_js/index.html index 9920d82620e..dc6c64ebbad 100644 --- a/integrations/node_js/index.html +++ b/integrations/node_js/index.html @@ -1,27 +1,34 @@ - + +
      -

      Select an option:

      -
      - +

      Select a script to run:

      + +
      -
      - + +
      -
      - + +
      -
      - + +
      diff --git a/integrations/node_js/my_first_test.py b/integrations/node_js/my_first_test.py old mode 100755 new mode 100644 index c3e14e1a730..68875df12a7 --- a/integrations/node_js/my_first_test.py +++ b/integrations/node_js/my_first_test.py @@ -2,18 +2,24 @@ class MyTestClass(BaseCase): - - def test_basic(self): - self.open('http://xkcd.com/353/') - self.assert_element('img[alt="Python"]') - self.click('a[rel="license"]') - self.assert_text('free to copy', 'div center') - self.open("http://xkcd.com/1481/") - title = self.get_attribute("#comic img", "title") - self.assert_true("86,400 seconds per day" in title) - self.click('link=Blag') - self.assert_text('The blag of the webcomic', 'h2') - self.update_text('input#s', 'Robots!\n') - self.assert_text('Hooray robots!', '#content') - self.open('http://xkcd.com/1319/') - self.assert_exact_text('Automation', '#ctitle') + def test_swag_labs(self): + self.goto("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("#inventory_container") + self.assert_text("Products", "span.title") + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_text("Your Cart", "span.title") + self.assert_text("Backpack", "div.cart_item") + self.click("button#checkout") + self.type("#first-name", "SeleniumBase") + self.type("#last-name", "Automation") + self.type("#postal-code", "77123") + self.click("input#continue") + self.assert_text("Checkout: Overview") + self.assert_text("Backpack", "div.cart_item") + self.click("button#finish") + self.assert_exact_text("Thank you for your order!", "h2") + self.assert_element('img[alt="Pony Express"]') + self.js_click("a#logout_sidebar_link") diff --git a/integrations/node_js/npm-shrinkwrap.json b/integrations/node_js/npm-shrinkwrap.json new file mode 100644 index 00000000000..2fc839d54a8 --- /dev/null +++ b/integrations/node_js/npm-shrinkwrap.json @@ -0,0 +1,707 @@ +{ + "name": "app", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "app", + "version": "0.0.0", + "dependencies": { + "express": "~4.21.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/integrations/node_js/package.json b/integrations/node_js/package.json old mode 100755 new mode 100644 index 219c2792415..9a00008425b --- a/integrations/node_js/package.json +++ b/integrations/node_js/package.json @@ -6,6 +6,6 @@ "start": "node ./bin/www" }, "dependencies": { - "express": "~4.16.4" + "express": "~4.21.0" } } \ No newline at end of file diff --git a/integrations/node_js/server.js b/integrations/node_js/server.js index 24f7cfa988b..4a7162f6863 100644 --- a/integrations/node_js/server.js +++ b/integrations/node_js/server.js @@ -1,53 +1,43 @@ -var http = require('http'); -var express = require('express'); -var path = require('path'); -var app = express(); -var exec = require('child_process').exec; - -function run_my_first_test_in_chrome() { - exec("pytest my_first_test.py --browser=chrome -s"); -} - -function run_my_first_test_in_firefox() { - exec("pytest my_first_test.py --browser=firefox -s"); -} - -function run_my_first_test_in_chrome_with_demo_mode() { - exec("pytest my_first_test.py --browser=chrome --demo_mode -s"); -} - -function run_my_first_test_in_firefox_with_demo_mode() { - exec("pytest my_first_test.py --browser=firefox --demo_mode -s"); +const http = require('http'); +const express = require('express'); +const path = require('path'); +const app = express(); +const exec = require('child_process').exec; +var server_info = '\nServer running at http://127.0.0.1:3000/ (CTRL+C to stop)'; + +function run_command(command) { + console.log("\n" + command); + exec(command, (err, stdout, stderr) => console.log(stdout, server_info)); } app.get('/', function(req, res) { res.sendFile(path.join(__dirname + '/index.html')); -}) +}); -app.get('/run_my_first_test_in_firefox', function(req, res) { +app.get('/run_my_first_test', function(req, res) { res.sendFile(path.join(__dirname + '/index.html')); res.redirect('/'); - run_my_first_test_in_firefox() -}) + run_command("pytest my_first_test.py"); +}); -app.get('/run_my_first_test_in_chrome', function(req, res) { +app.get('/run_test_demo_site', function(req, res) { res.sendFile(path.join(__dirname + '/index.html')); res.redirect('/'); - run_my_first_test_in_chrome() -}) + run_command("pytest test_demo_site.py"); +}); -app.get('/run_my_first_test_in_firefox_with_demo_mode', function(req, res) { +app.get('/run_my_first_test_with_demo_mode', function(req, res) { res.sendFile(path.join(__dirname + '/index.html')); res.redirect('/'); - run_my_first_test_in_firefox_with_demo_mode() -}) + run_command("pytest my_first_test.py --demo_mode"); +}); -app.get('/run_my_first_test_in_chrome_with_demo_mode', function(req, res) { +app.get('/run_test_demo_site_with_demo_mode', function(req, res) { res.sendFile(path.join(__dirname + '/index.html')); res.redirect('/'); - run_my_first_test_in_chrome_with_demo_mode() -}) + run_command("pytest test_demo_site.py --demo_mode"); +}); app.listen(3000, "127.0.0.1", function() { - console.log('Server running at http://127.0.0.1:3000/ (CTRL-C to stop)'); + console.log(server_info); }); diff --git a/integrations/node_js/test_demo_site.py b/integrations/node_js/test_demo_site.py new file mode 100644 index 00000000000..cf9964c984c --- /dev/null +++ b/integrations/node_js/test_demo_site.py @@ -0,0 +1,119 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class DemoSiteTests(BaseCase): + def test_demo_site(self): + # Open a web page in the active browser window + self.goto("https://seleniumbase.io/demo_page") + + # Assert the title of the current web page + self.assert_title("Web Testing Page") + + # Assert that an element is visible on the page + self.assert_element("tbody#tbodyId") + + # Assert that a text substring appears in an element + self.assert_text("Demo Page", "h1") + + # Type text into various text fields and then assert + self.type("#myTextInput", "This is Automated") + self.type("textarea.area1", "Testing Time!\n") + self.type('[name="preText2"]', "Typing Text!") + self.assert_text("This is Automated", "#myTextInput") + self.assert_text("Testing Time!\n", "textarea.area1") + self.assert_text("Typing Text!", '[name="preText2"]') + + # Hover & click a dropdown element and assert results + self.assert_text("Automation Practice", "h3") + try: + self.hover_and_click("#myDropdown", "#dropOption2", timeout=1) + except Exception: + # Someone moved the mouse while the test ran + self.js_click("#dropOption2") + self.assert_text("Link Two Selected", "h3") + + # Click a button and then verify the expected results + self.assert_text("This Text is Green", "#pText") + self.click('button:contains("Click Me")') + self.assert_text("This Text is Purple", "#pText") + + # Assert that the given SVG is visible on the page + self.assert_element('svg[name="svgName"]') + + # Verify that a slider control updates a progress bar + self.assert_element('progress[value="50"]') + self.set_value("input#mySlider", "100") + self.assert_element('progress[value="100"]') + + # Verify that a "select" option updates a meter bar + self.assert_element('meter[value="0.25"]') + self.select_option_by_text("#mySelect", "Set to 75%") + self.assert_element('meter[value="0.75"]') + + # Assert an element located inside an iframe + self.assert_false(self.is_element_visible("img")) + self.switch_to_frame("#myFrame1") + self.assert_true(self.is_element_visible("img")) + self.switch_to_default_content() + + # Assert text located inside an iframe + self.assert_false(self.is_text_visible("iFrame Text")) + self.switch_to_frame("#myFrame2") + self.assert_true(self.is_text_visible("iFrame Text")) + self.switch_to_default_content() + + # Verify that clicking a radio button selects it + self.assert_false(self.is_selected("#radioButton2")) + self.click("#radioButton2") + self.assert_true(self.is_selected("#radioButton2")) + + # Verify that clicking a checkbox makes it selected + self.assert_element_not_visible("img#logo") + self.assert_false(self.is_selected("#checkBox1")) + self.click("#checkBox1") + self.assert_true(self.is_selected("#checkBox1")) + self.assert_element("img#logo") + + # Verify clicking on multiple elements with one call + self.assert_false(self.is_selected("#checkBox2")) + self.assert_false(self.is_selected("#checkBox3")) + self.assert_false(self.is_selected("#checkBox4")) + self.click_visible_elements("input.checkBoxClassB") + self.assert_true(self.is_selected("#checkBox2")) + self.assert_true(self.is_selected("#checkBox3")) + self.assert_true(self.is_selected("#checkBox4")) + + # Verify that clicking an iframe checkbox selects it + self.assert_false(self.is_element_visible(".fBox")) + self.switch_to_frame("#myFrame3") + self.assert_true(self.is_element_visible(".fBox")) + self.assert_false(self.is_selected(".fBox")) + self.click(".fBox") + self.assert_true(self.is_selected(".fBox")) + self.switch_to_default_content() + + # Verify Drag and Drop + self.assert_element_not_visible("div#drop2 img#logo") + self.drag_and_drop("img#logo", "div#drop2") + self.assert_element("div#drop2 img#logo") + + # Assert link text + self.assert_link_text("seleniumbase.com") + self.assert_link_text("SeleniumBase on GitHub") + self.assert_link_text("seleniumbase.io") + + # Click link text + self.click_link("SeleniumBase Demo Page") + + # Assert exact text + self.assert_exact_text("Demo Page", "h1") + + # Highlight a page element (Also asserts visibility) + self.highlight("h2") + + # Actions with Demo Mode enabled + if self.headed: + self.activate_demo_mode() + self.type("input", "Have a Nice Day!") + self.assert_text("SeleniumBase", "h2") diff --git a/integrations/selenium_grid/ReadMe.md b/integrations/selenium_grid/ReadMe.md old mode 100755 new mode 100644 diff --git a/integrations/selenium_ide/ReadMe.md b/integrations/selenium_ide/ReadMe.md deleted file mode 100755 index ca4b51d487b..00000000000 --- a/integrations/selenium_ide/ReadMe.md +++ /dev/null @@ -1 +0,0 @@ -### The ReadMe for the Katalon / Selenium IDE conversion tool has been moved to: [seleniumbase/utilities/selenium_ide/ReadMe.md](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/utilities/selenium_ide/ReadMe.md) and all related code has been moved to [seleniumbase/utilities/selenium_ide](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/utilities/selenium_ide) diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000000..da3719892fc --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,200 @@ +# Project information +site_name: SeleniumBase Docs +site_url: https://seleniumbase.io +site_author: Michael Mintz +site_description: A complete framework for end-to-end testing with Python, pytest, behave-BDD, and WebDriver. +# Repository information +repo_name: seleniumbase/SeleniumBase +repo_url: https://github.com/seleniumbase/SeleniumBase +edit_uri: "" +site_dir: "site" +docs_dir: "mkdocs_build" +# Copyright +copyright: Copyright © 2014 - 2026 Michael Mintz +# Extensions +markdown_extensions: + - admonition + - md_in_html + - tables + - toc: + permalink: true + - pymdownx.highlight: + linenums: false + - pymdownx.tabbed: + alternate_style: true + - pymdownx.highlight + - pymdownx.superfences + - pymdownx.inlinehilite + # - pymdownx.details + # - pymdownx.snippets +# Configuration +theme: + name: material + logo: img/logo3c.png + favicon: img/green_logo3.png + language: en + include_homepage_in_sidebar: true + sticky_navigation: true + collapse_navigation: true + # titles_only: false + include_search_page: false + search_index_only: true + static_templates: + - 404.html + features: + # - search.highlight + # - toc.integrate + - toc.hide + # - navigation.indexes + - toc.follow + - navigation.sections + # - navigation.expand + # - navigation.tabs + - navigation.top + - navigation.tracking + - navigation.instant + palette: + scheme: default + primary: blue + accent: green + font: + text: Roboto + code: Roboto Mono + icon: + logo: img/sb_logo_10.png +# Plugins +plugins: + - search: + separator: '[\s]+' + lang: en + - exclude-search: + exclude: + - CODE_OF_CONDUCT.md + - CONTRIBUTING.md + - SECURITY.md + - examples/case_summary.md + - examples/migration/raw_selenium/ReadMe.md + - help_docs/ReadMe.md + - help_docs/verify_webdriver.md + - help_docs/webdriver_installation.md + - seleniumbase/masterqa/ReadMe.md + - seleniumbase/utilities/selenium_ide/ReadMe.md + - seleniumbase/examples/chart_maker/ReadMe.md + # - minify: + # minify_html: true + # minify_css: true + # minify_js: true + - mkdocs-simple-hooks: + hooks: + on_pre_build: mkdocs_build.prepare:main +# Page tree +nav: + - โœ… SeleniumBase README: README.md + - ๐Ÿฐ List of Features: help_docs/features_list.md + - ๐Ÿ™ CDP Mode: examples/cdp_mode/ReadMe.md + - ๐ŸŽญ Stealthy Playwright: examples/cdp_mode/playwright/ReadMe.md + - ๐Ÿ“š Running Example Tests: examples/ReadMe.md + - ๐ŸŽ›๏ธ Command Line Options: help_docs/customizing_test_runs.md + - ๐Ÿ’ป Console Scripts: seleniumbase/console_scripts/ReadMe.md + - ๐Ÿ“Š Dashboard / Reports: examples/example_logs/ReadMe.md + - ๐Ÿ”  Syntax Formats: help_docs/syntax_formats.md + - ๐ŸŽ–๏ธ GUI / Commander: help_docs/commander.md + - ๐Ÿ”ด Recorder Mode: help_docs/recorder_mode.md + - ๐Ÿ“— API Reference: help_docs/method_summary.md + - ๐Ÿ“˜ CDP Mode APIs: help_docs/cdp_mode_methods.md + - Python Setup / Install: + - ๐Ÿ‰ Get Python, pip, & git: help_docs/install_python_pip_git.md + - โš™๏ธ Virtualenv Instructions: help_docs/virtualenv_instructions.md + - ๐Ÿ„ Install SeleniumBase: help_docs/install.md + - ๐Ÿ‘๏ธ How it Works: help_docs/how_it_works.md + - JS Manager / JS Tools: + - ๐ŸŸจ JS Package Manager: help_docs/js_package_manager.md + - ๐ŸŽฆ Demo Mode: help_docs/demo_mode.md + - ๐ŸšŽ Tour Maker: examples/tour_examples/ReadMe.md + - ๐Ÿ›‚ Dialog Boxes: examples/dialog_boxes/ReadMe.md + - ๐Ÿ›‚ MasterQA Mode: examples/master_qa/ReadMe.md + - ๐Ÿ“ถ Chart Maker: examples/chart_maker/ReadMe.md + - ๐ŸŽž๏ธ Presentation Maker: examples/presenter/ReadMe.md + - Integrations: + - ๐Ÿณ Docker: integrations/docker/ReadMe.md + - ๐Ÿ‘ค UC Mode: help_docs/uc_mode.md + - ๐Ÿค– GitHub CI: integrations/github/workflows/ReadMe.md + - ๐Ÿ›‚ MasterQA: seleniumbase/masterqa/ReadMe.md + - ๐Ÿ—‚๏ธ Case Plans: help_docs/case_plans.md + - ๐Ÿ“ฑ Mobile Mode: help_docs/mobile_testing.md + - ๐ŸŒ Selenium Grid: seleniumbase/utilities/selenium_grid/ReadMe.md + - ๐Ÿ–ผ๏ธ Visual Testing: examples/visual_testing/ReadMe.md + - ๐Ÿค– Azure Pipelines: integrations/azure/azure_pipelines/ReadMe.md + - ๐Ÿค– Jenkins on Azure: integrations/azure/jenkins/ReadMe.md + - ๐Ÿค– Jenkins on Google Cloud: integrations/google_cloud/ReadMe.md + - ๐Ÿ•ต๏ธ The HTML Inspector: help_docs/html_inspector.md + - ๐Ÿค– NodeJS Test Runners: https://github.com/seleniumbase/SeleniumBase/tree/master/integrations/node_js + - Presentations: + - ๐Ÿ”ฐ Fundamentals Demo: https://seleniumbase.io/other/fundamentals.html + - ๐Ÿฅท Undetectable Automation 5: https://seleniumbase.io/other/ua_presentation_5.html + - โš™๏ธ Python Virtual Envs: https://seleniumbase.io/other/py_virtual_envs.html + - โœ… Core Presentation: https://seleniumbase.io/other/core_presentation.html + - ๐ŸŽž๏ธ Presenter Demo: https://seleniumbase.io/other/presenter.html + - ๐Ÿ“Š Chart Maker Demo: https://seleniumbase.io/other/chart_presentation.html + - Demo Pages: + - ๐Ÿต Coffee Cart (Test App): https://seleniumbase.io/coffee/ + - ๐Ÿ“‘ Demo Page (Test Page): https://seleniumbase.io/demo_page + - ๐Ÿ”‘ Simple App (Test Page): https://seleniumbase.io/simple/login + - ๐Ÿ”‘ MFA Login (Test App): https://seleniumbase.io/realworld/login + - ๐Ÿ” Anti-bot (Test Page): https://seleniumbase.io/antibot/login + - ๐Ÿ” Hobbit App (Test Page): https://seleniumbase.io/hobbit/login + - ๐Ÿ“ TinyMCE (Test Page): https://seleniumbase.io/tinymce/ + - ๐Ÿ”ข Calculator (Test App): https://seleniumbase.io/apps/calculator + - ๐Ÿ“ฑ Device Farm (Virtual): https://seleniumbase.io/devices/ + - โš ๏ธ Error Page (Test Page): https://seleniumbase.io/error_page/ + - โš ๏ธ Page with broken links: https://seleniumbase.io/other/broken_page + - โ†”๏ธ Drag & Drop (Test Page): https://seleniumbase.io/other/drag_and_drop + - ๐Ÿ–ผ๏ธ Canvas Test Page One: https://seleniumbase.io/canvas/ + - ๐Ÿ–ผ๏ธ Canvas Test Page Two: https://seleniumbase.io/other/canvas + - ๐Ÿ‘ค Shadow DOM/Root Page: https://seleniumbase.io/other/shadow_dom + - ๐Ÿ–ฅ๏ธ SeleniumBase in iframe: https://seleniumbase.io/w3schools/sbase + - ๐Ÿ–ฅ๏ธ W3Schools Playground: https://seleniumbase.io/w3schools/ + - ๐Ÿ–ฅ๏ธ W3Schools iframes: https://seleniumbase.io/w3schools/iframes + - ๐Ÿ—ƒ๏ธ W3Schools file upload: https://seleniumbase.io/w3schools/file_upload + - ๐Ÿ–ฒ๏ธ W3Schools doubleclick: https://seleniumbase.io/w3schools/double_click + - โ†”๏ธ W3Schools drag & drop: https://seleniumbase.io/w3schools/drag_drop + - โ˜‘๏ธ W3Schools checkboxes: https://seleniumbase.io/w3schools/checkboxes + - ๐Ÿ”˜ W3Schools radio buttons: https://seleniumbase.io/w3schools/radio_buttons + - Pages with CAPTCHAs: + - ๐Ÿ”‘ CF Turnstile Test: https://seleniumbase.io/apps/turnstile + - ๐Ÿ”‘ CF Turnstile on Form: https://seleniumbase.io/apps/form_turnstile + - ๐Ÿ” reCAPTCHA v2 Test: https://seleniumbase.io/apps/recaptcha + - ๐Ÿ” reCAPTCHA v2 on Form: https://seleniumbase.io/apps/form_recaptcha + - ๐Ÿ” reCAPTCHA, invisible: https://seleniumbase.io/apps/invisible_recaptcha + - ๐Ÿ”‘ Brotector CAPTCHA: https://seleniumbase.io/apps/brotector + - Additional Help Docs: + - ๐Ÿ“‘ Table of Contents: help_docs/ReadMe.md + - ๐Ÿ–ผ๏ธ How to handle iframes: help_docs/handling_iframes.md + - ๐Ÿ” Decorators / Security: seleniumbase/common/ReadMe.md + - ๐Ÿ—‚๏ธ Case Plans (examples): examples/case_summary.md + - ๐Ÿงญ Using Safari Driver: help_docs/using_safari_driver.md + - ๐Ÿ‘ค Shadow DOM Support: help_docs/shadow_dom.md + - ๐Ÿ‘ฅ macOS Hidden Files: help_docs/hidden_files_info.md + - ๐Ÿ—„๏ธ MySQL Instructions: help_docs/mysql_installation.md + - ๐Ÿ“ƒ Desired Capabilities: help_docs/desired_capabilities.md + - ๐Ÿ“œ Useful grep commands: help_docs/useful_grep_commands.md + - โš™๏ธ Downloading drivers: help_docs/webdriver_installation.md + - โœ… Selenium Migration: examples/migration/raw_selenium/ReadMe.md + - โœ”๏ธ Verifying drivers: help_docs/verify_webdriver.md + - Behave-BDD Integration: + - ๐Ÿ Behave-BDD ReadMe: examples/behave_bdd/ReadMe.md + - ๐Ÿ Behave-BDD GUI App: help_docs/behave_gui.md + - Languages: + - ๐ŸŒ Translations: help_docs/translations.md + - ๐Ÿ—พ Locale Codes: help_docs/locale_codes.md + - Other: + - ๐Ÿ“บ YouTube Link: https://www.youtube.com/playlist?list=PLp9uKicxkBc5UIlGi2BuE3aWC7JyXpD3m + - ๐Ÿ“‹ Case Studies: help_docs/happy_customers.md + - ๐Ÿ™ Thank You: help_docs/thank_you.md +# Google Analytics +extra: + analytics: + provider: google + property: G-P5KFWRNLRN + # property: UA-167313767-1 +# google_analytics: ['G-P5KFWRNLRN', 'seleniumbase.io'] \ No newline at end of file diff --git a/mkdocs_build/ReadMe.txt b/mkdocs_build/ReadMe.txt new file mode 100755 index 00000000000..ced6a91f722 --- /dev/null +++ b/mkdocs_build/ReadMe.txt @@ -0,0 +1,3 @@ +Help Docs Home Page: + +https://seleniumbase.io diff --git a/mkdocs_build/docs_instructions.txt b/mkdocs_build/docs_instructions.txt new file mode 100644 index 00000000000..a1ed6211755 --- /dev/null +++ b/mkdocs_build/docs_instructions.txt @@ -0,0 +1,5 @@ +Preparing the SeleniumBase Docs website with "mkdocs" + +Usage: (from the top-level SeleniumBase folder) +pip install -r mkdocs_build/requirements.txt +mkdocs build (OR "mkdocs build --strict" to fail on warnings) diff --git a/mkdocs_build/img/dash_pie.png b/mkdocs_build/img/dash_pie.png new file mode 100644 index 00000000000..66981129121 Binary files /dev/null and b/mkdocs_build/img/dash_pie.png differ diff --git a/mkdocs_build/img/dash_pie_2.png b/mkdocs_build/img/dash_pie_2.png new file mode 100644 index 00000000000..83448690fde Binary files /dev/null and b/mkdocs_build/img/dash_pie_2.png differ diff --git a/mkdocs_build/img/dash_pie_3.png b/mkdocs_build/img/dash_pie_3.png new file mode 100644 index 00000000000..e5a3dbac432 Binary files /dev/null and b/mkdocs_build/img/dash_pie_3.png differ diff --git a/mkdocs_build/img/favicon.ico b/mkdocs_build/img/favicon.ico new file mode 100644 index 00000000000..6a99a7ed411 Binary files /dev/null and b/mkdocs_build/img/favicon.ico differ diff --git a/mkdocs_build/img/grad_logo.png b/mkdocs_build/img/grad_logo.png new file mode 100644 index 00000000000..9822c820b28 Binary files /dev/null and b/mkdocs_build/img/grad_logo.png differ diff --git a/mkdocs_build/img/green_icon.png b/mkdocs_build/img/green_icon.png new file mode 100644 index 00000000000..23a994a655c Binary files /dev/null and b/mkdocs_build/img/green_icon.png differ diff --git a/mkdocs_build/img/green_logo.png b/mkdocs_build/img/green_logo.png new file mode 100644 index 00000000000..ed45d7bdd33 Binary files /dev/null and b/mkdocs_build/img/green_logo.png differ diff --git a/mkdocs_build/img/green_logo2.png b/mkdocs_build/img/green_logo2.png new file mode 100644 index 00000000000..24b8d6fac58 Binary files /dev/null and b/mkdocs_build/img/green_logo2.png differ diff --git a/mkdocs_build/img/green_logo3.png b/mkdocs_build/img/green_logo3.png new file mode 100644 index 00000000000..3db0d67370b Binary files /dev/null and b/mkdocs_build/img/green_logo3.png differ diff --git a/mkdocs_build/img/logo3.png b/mkdocs_build/img/logo3.png new file mode 100644 index 00000000000..0f30bbd10e4 Binary files /dev/null and b/mkdocs_build/img/logo3.png differ diff --git a/mkdocs_build/img/logo3a.png b/mkdocs_build/img/logo3a.png new file mode 100644 index 00000000000..3167c1c8256 Binary files /dev/null and b/mkdocs_build/img/logo3a.png differ diff --git a/mkdocs_build/img/logo3b.png b/mkdocs_build/img/logo3b.png new file mode 100644 index 00000000000..5f835baff23 Binary files /dev/null and b/mkdocs_build/img/logo3b.png differ diff --git a/mkdocs_build/img/logo3c.png b/mkdocs_build/img/logo3c.png new file mode 100644 index 00000000000..c6153c35e7f Binary files /dev/null and b/mkdocs_build/img/logo3c.png differ diff --git a/mkdocs_build/img/logo4.png b/mkdocs_build/img/logo4.png new file mode 100644 index 00000000000..5ccde2a7a04 Binary files /dev/null and b/mkdocs_build/img/logo4.png differ diff --git a/mkdocs_build/img/logo5.png b/mkdocs_build/img/logo5.png new file mode 100644 index 00000000000..2a02c681248 Binary files /dev/null and b/mkdocs_build/img/logo5.png differ diff --git a/mkdocs_build/img/logo6.png b/mkdocs_build/img/logo6.png new file mode 100644 index 00000000000..9c82be57ac1 Binary files /dev/null and b/mkdocs_build/img/logo6.png differ diff --git a/mkdocs_build/img/logo7.png b/mkdocs_build/img/logo7.png new file mode 100644 index 00000000000..a99178a232b Binary files /dev/null and b/mkdocs_build/img/logo7.png differ diff --git a/mkdocs_build/img/sb_icon.png b/mkdocs_build/img/sb_icon.png new file mode 100644 index 00000000000..212ce72ee40 Binary files /dev/null and b/mkdocs_build/img/sb_icon.png differ diff --git a/mkdocs_build/img/sb_logo_10.png b/mkdocs_build/img/sb_logo_10.png new file mode 100644 index 00000000000..8e8d92f7237 Binary files /dev/null and b/mkdocs_build/img/sb_logo_10.png differ diff --git a/mkdocs_build/img/sb_logo_7.png b/mkdocs_build/img/sb_logo_7.png new file mode 100644 index 00000000000..cc2f9a8a47d Binary files /dev/null and b/mkdocs_build/img/sb_logo_7.png differ diff --git a/mkdocs_build/img/side_by_side.png b/mkdocs_build/img/side_by_side.png new file mode 100644 index 00000000000..684c53dacb7 Binary files /dev/null and b/mkdocs_build/img/side_by_side.png differ diff --git a/mkdocs_build/img/social/share_facebook.svg b/mkdocs_build/img/social/share_facebook.svg new file mode 100644 index 00000000000..db2bfbd8798 --- /dev/null +++ b/mkdocs_build/img/social/share_facebook.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mkdocs_build/img/social/share_github.svg b/mkdocs_build/img/social/share_github.svg new file mode 100644 index 00000000000..0b8f71dc014 --- /dev/null +++ b/mkdocs_build/img/social/share_github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mkdocs_build/img/social/share_gitter.svg b/mkdocs_build/img/social/share_gitter.svg new file mode 100644 index 00000000000..c1d6a744989 --- /dev/null +++ b/mkdocs_build/img/social/share_gitter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mkdocs_build/img/social/share_instagram.svg b/mkdocs_build/img/social/share_instagram.svg new file mode 100644 index 00000000000..e53e855ba97 --- /dev/null +++ b/mkdocs_build/img/social/share_instagram.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mkdocs_build/img/social/share_twitter.svg b/mkdocs_build/img/social/share_twitter.svg new file mode 100644 index 00000000000..2d33cf756c7 --- /dev/null +++ b/mkdocs_build/img/social/share_twitter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mkdocs_build/index.txt b/mkdocs_build/index.txt new file mode 100644 index 00000000000..d6231c3e662 --- /dev/null +++ b/mkdocs_build/index.txt @@ -0,0 +1,13 @@ + + + + + SeleniumBase + + +

      SeleniumBase

      +

      Docs Home Page:

      + +

      https://seleniumbase.io

      + + diff --git a/mkdocs_build/prepare.py b/mkdocs_build/prepare.py new file mode 100644 index 00000000000..9600d50f192 --- /dev/null +++ b/mkdocs_build/prepare.py @@ -0,0 +1,197 @@ +""" For preparing the mkdocs-generated seleniumbase.io website. """ + +import os +import re +from pathlib import Path + +GITHUB_URL = r"https://github.com/seleniumbase/SeleniumBase/blob/master/" +ROOT_DIR = Path(__file__).parents[1] +URL_PATTERN = re.compile( + r"(?:\(|{}[\w/.]+\.md)(?:\)|\")".format(GITHUB_URL) +) +MD_PATH_PATTERN = re.compile(r"\[.*\]\((?P[\w\\._/]+\.md)\)") +HEADER_PATTERN = re.compile( + r"^(?P#+)\s*(<[\w\s=\":/.]+>)?\s*\**(?P
      .*[\w`]):?\**\s*$", + flags=re.MULTILINE, +) + +PROCESSED_PATHS = set() + + +def normalize_path(path): + path = Path(path).absolute().relative_to(ROOT_DIR) + return str(path).replace("\\", "/") + + +def read_file(file_name): + path = ROOT_DIR / file_name + with path.open() as file_handle: + content = file_handle.read() + return content + + +def process_file(file_name): + content = read_file(file_name) + urls = URL_PATTERN.findall(content) + # content = content.replace("
      ", " \n") + content = re.sub(HEADER_PATTERN, r"\g \g
      ", content) + directory = "/".join(normalize_path(file_name).split("/")[:-1]) + + paths = set() + + md_paths = MD_PATH_PATTERN.findall(content) + for md_path in md_paths: + path = md_path.lstrip("/") + if (ROOT_DIR / directory / path).exists(): + path = ROOT_DIR / directory / path + else: + path = ROOT_DIR / path + path = path.resolve().relative_to(ROOT_DIR) + paths.add(normalize_path(path)) + content = content.replace("(" + md_path + ")", normalize_path(path)) + + for url in urls: + path = url[len(GITHUB_URL) :] # noqa: E203 + paths.add(path) + content = content.replace( + url, normalize_path(os.path.relpath(path, directory)) + ) + + output_path = ROOT_DIR / "mkdocs_build" / file_name + if not output_path.parent.is_dir(): + os.makedirs(output_path.parent) + + with output_path.open("w+") as output_file: + output_file.write(content) + PROCESSED_PATHS.add(normalize_path(file_name)) + + for path in paths: + if path not in PROCESSED_PATHS: + process_file(normalize_path(path)) + + +def main(*args, **kwargs): + files_to_process = ["README.md"] + scanned_dir_list = [] + scanned_dir_list.append("help_docs") + scanned_dir_list.append("examples") + scanned_dir_list.append("examples/cdp_mode") + scanned_dir_list.append("examples/cdp_mode/playwright") + scanned_dir_list.append("examples/master_qa") + scanned_dir_list.append("examples/presenter") + scanned_dir_list.append("examples/behave_bdd") + scanned_dir_list.append("examples/chart_maker") + scanned_dir_list.append("examples/example_logs") + scanned_dir_list.append("examples/tour_examples") + scanned_dir_list.append("examples/visual_testing") + scanned_dir_list.append("integrations/google_cloud") + scanned_dir_list.append("seleniumbase/console_scripts") + scanned_dir_list.append("examples/migration/raw_selenium") + for scanned_dir in scanned_dir_list: + for dir_ in os.listdir(ROOT_DIR / scanned_dir): + files_to_process.append(os.path.join(scanned_dir, dir_)) + + video_embed = ( + '
      ' + ) + + updated_files_to_process = [] + for file_ in files_to_process: + if file_.endswith(".md"): + updated_files_to_process.append(file_) + + for file_ in updated_files_to_process: + process_file(file_) + + for file_ in updated_files_to_process: + readme_file = "./mkdocs_build/" + file_ + with open(readme_file, mode="r", encoding="utf-8") as f: + all_code = f.read() + code_lines = all_code.split("\n") + + changed = False + seleniumbase_lines = [] + for line in code_lines: + if ' href="' in line and '.md"' in line: + changed = True + line = line.replace('.md"', '/"') + if "" in line: + changed = True + new_lines = [] + new_lines.append("---") + new_lines.append("hide:") + new_lines.append(" - toc") + new_lines.append("---") + for line in new_lines: + seleniumbase_lines.append(line) + continue + if "" in line: + changed = True + line = ( + r'

      " + ) + alt_link_badge = ( + '' + 'SeleniumBase Docs' + ) + back_to_gh = ( + r'' + r'SeleniumBase on GitHub' + r"" + ) + if alt_link_badge in line: + line = line.replace(alt_link_badge, back_to_gh) + changed = True + if "/help_docs/uc_mode/" in line and file_.count("/") >= 2: + line = line.replace( + "/help_docs/uc_mode/", "/../help_docs/uc_mode/" + ) + changed = True + if "" in line: + changed = True + continue + if "" in line and "watch?v=" in line: + start_pt = line.find("watch?v=") + len("watch?v=") + end_pt = line.find('"', start_pt + 1) + yt_code = line[start_pt:end_pt] + changed = True + line = video_embed.replace("yt_code", yt_code) + if "" in line: + changed = True + line = ( + '
      ' + "

      โœ… Reliable Browser Testing

      " + "
      " + ) + if "" in line: + changed = True + line = ( + '

      SeleniumBase Docs ' + '

      ' + ) + seleniumbase_lines.append(line) + if changed: + out_file = open(readme_file, mode="w+", encoding="utf-8") + out_file.writelines("\r\n".join(seleniumbase_lines)) + out_file.close() diff --git a/mkdocs_build/requirements.txt b/mkdocs_build/requirements.txt new file mode 100644 index 00000000000..5ab22ff43f3 --- /dev/null +++ b/mkdocs_build/requirements.txt @@ -0,0 +1,21 @@ +# mkdocs dependencies for generating the seleniumbase.io website +# Minimum Python version: 3.10 (for generating docs only) + +regex>=2026.7.19 +pymdown-extensions>=10.21.3 +pipdeptree>=4.2.0 +python-dateutil>=2.8.2 +click>=8.4.2 +Markdown==3.10.3 +ghp-import==2.1.0 +watchdog==6.0.0 +cairocffi==1.7.1 +pathspec==1.1.1 +Babel==2.18.0 +paginate==0.5.7 +mkdocs==1.6.1 +mkdocs-material==9.6.23 +mkdocs-exclude-search==0.6.6 +mkdocs-simple-hooks==0.1.5 +mkdocs-get-deps==0.2.2 +mkdocs-material-extensions==1.3.1 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000000..9052279d869 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,80 @@ +[build-system] +requires = ["setuptools>=70.2.0", "wheel>=0.44.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "seleniumbase" +readme = "README.md" +dynamic = [ + "version", + "license", + "authors", + "scripts", + "keywords", + "classifiers", + "description", + "maintainers", + "entry-points", + "dependencies", + "requires-python", + "optional-dependencies", +] + +[project.urls] +"Homepage" = "https://github.com/seleniumbase/SeleniumBase" +"Changelog" = "https://github.com/seleniumbase/SeleniumBase/releases" +"Download" = "https://pypi.org/project/seleniumbase/#files" +"Blog" = "https://seleniumbase.com/" +"Discord" = "https://discord.gg/EdhQTn3EyE" +"PyPI" = "https://pypi.org/project/seleniumbase/" +"Source" = "https://github.com/seleniumbase/SeleniumBase" +"Repository" = "https://github.com/seleniumbase/SeleniumBase" +"Documentation" = "https://seleniumbase.io/" + +[tool.setuptools] +packages = [ + "seleniumbase", + "sbase", + "seleniumbase.behave", + "seleniumbase.common", + "seleniumbase.config", + "seleniumbase.console_scripts", + "seleniumbase.core", + "seleniumbase.drivers", + "seleniumbase.drivers.cft_drivers", + "seleniumbase.drivers.chs_drivers", + "seleniumbase.drivers.opera_drivers", + "seleniumbase.drivers.brave_drivers", + "seleniumbase.drivers.comet_drivers", + "seleniumbase.drivers.chromium_drivers", + "seleniumbase.extensions", + "seleniumbase.fixtures", + "seleniumbase.js_code", + "seleniumbase.masterqa", + "seleniumbase.plugins", + "seleniumbase.resources", + "seleniumbase.translate", + "seleniumbase.undetected", + "seleniumbase.undetected.cdp_driver", + "seleniumbase.utilities", + "seleniumbase.utilities.selenium_grid", + "seleniumbase.utilities.selenium_ide", +] + +[tool.pytest.ini_options] +addopts = ["--capture=tee-sys", "-p no:cacheprovider"] +norecursedirs = [".*", "build", "dist", "recordings", "temp", "assets"] +filterwarnings = [ + "ignore::pytest.PytestWarning", + "ignore:.*U.*mode is deprecated:DeprecationWarning", +] +junit_family = ["legacy"] +python_files = ["test_*.py", "*_test.py", "*_tests.py", "*_suite.py"] +python_classes = ["Test*", "*Test*", "*Test", "*Tests", "*Suite"] +python_functions = ["test_*"] +markers = [ + "marker1", "marker2", "marker3", "marker_test_suite", + "local", "remote", "offline", "expected_failure", + "qa", "ci", "e2e", "smoke", "ready", "master", "deploy", + "develop", "staging", "production", "release", "active", +] diff --git a/pytest.ini b/pytest.ini index b5125e7b57f..38ebd219597 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,17 +1,47 @@ [pytest] -# Let console output be seen. Don't override the pytest plugin. -addopts = --capture=no --ignore conftest.py -p no:cacheprovider +# Display console output. Disable cacheprovider: +addopts = --capture=tee-sys -p no:cacheprovider -# Ignore warnings such as DeprecationWarning and pytest.PytestUnknownMarkWarning -filterwarnings = ignore::pytest.PytestWarning +# Skip these directories during test collection: +norecursedirs = .* build dist recordings temp assets -# Here are the pytest markers used in the example tests: -# (pytest v4.5.0 and newer requires marker registration to prevent warnings.) -# (Future versions of pytest may turn those marker warnings into errors.) +# Ignore DeprecationWarning, PytestUnknownMarkWarning +filterwarnings = + ignore::pytest.PytestWarning + ignore:.*U.*mode is deprecated:DeprecationWarning + +# Configure the junit_family option explicitly: +junit_family = legacy + +# Set pytest discovery rules: +# (Most of the rules here are similar to the default rules.) +# (Inheriting unittest.TestCase could override these rules.) +python_files = test_*.py *_test.py *_tests.py *_suite.py +python_classes = Test* *Test* *Test *Tests *Suite +python_functions = test_* + +# Common pytest markers used in examples: +# (pytest may require marker registration to prevent warnings.) +# (Future versions may turn those marker warnings into errors.) markers = marker1: custom marker marker2: custom marker marker3: custom marker marker_test_suite: custom marker expected_failure: custom marker + local: custom marker + remote: custom marker + offline: custom marker + develop: custom marker + qa: custom marker + ci: custom marker + e2e: custom marker + ready: custom marker + smoke: custom marker + deploy: custom marker + active: custom marker + master: custom marker + release: custom marker + staging: custom marker + production: custom marker diff --git a/requirements.txt b/requirements.txt index 2bafebe6dd7..2e0918d6085 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,29 +1,90 @@ -pip>=19.1.1 -setuptools>=41.0.1 -six -nose -ipdb -unittest2 -idna==2.8 -chardet==3.0.4 -urllib3==1.25.3 -requests>=2.22.0 -selenium==3.141.0 -pluggy>=0.12.0 -pytest>=4.5.0 -pytest-cov>=2.7.1 -pytest-forked>=1.0.2 -pytest-html>=1.20.0 -pytest-metadata>=1.8.0 -pytest-ordering>=0.6 -pytest-rerunfailures>=7.0 -pytest-xdist>=1.28.0 -parameterized>=0.7.0 -beautifulsoup4>=4.6.0 -pyopenssl>=19.0.0 -colorama>=0.4.1 -pyotp>=2.2.7 -boto>=2.49.0 -flake8>=3.7.7 -certifi>=2019.3.9 -PyVirtualDisplay==0.2.1 +pip>=26.0.1;python_version<"3.10" +pip>=26.2.1;python_version>="3.10" +packaging>=26.3 +setuptools~=70.2;python_version<"3.10" +setuptools>=84.0.0;python_version>="3.10" +wheel>=0.47.0 +attrs>=26.1.0 +certifi>=2026.7.22 +exceptiongroup>=1.3.1 +websockets~=15.0.1;python_version<"3.10" +websockets~=16.1.1;python_version=="3.10" +websockets>=17.0.1;python_version>="3.11" +filelock~=3.19.1;python_version<"3.10" +filelock>=3.32.2;python_version>="3.10" +fasteners>=0.20 +mycdp>=1.4.0 +pynose>=1.5.5 +platformdirs~=4.4.0;python_version<"3.10" +platformdirs>=4.11.2;python_version>="3.10" +typing-extensions>=4.16.0 +sbvirtualdisplay>=1.4.0 +MarkupSafe>=3.0.3 +Jinja2>=3.1.6 +six>=1.17.0 +parse>=1.22.1 +parse-type>=0.6.6 +colorama>=0.4.6 +psutil>=7.2.2 +pyyaml>=6.0.3 +pygments>=2.20.0 +pyreadline3>=3.5.4;platform_system=="Windows" +tabcompleter>=1.4.1 +pdbp>=1.8.2 +idna>=3.18 +charset-normalizer>=3.4.9,<4 +urllib3>=1.26.20,<2;python_version<"3.10" +urllib3>=2.7.0,<3;python_version>="3.10" +requests~=2.32.5;python_version<"3.10" +requests~=2.34.2;python_version>="3.10" +sniffio==1.3.1 +h11==0.16.0 +outcome==1.3.0.post0 +trio>=0.31.0,<1;python_version<"3.10" +trio>=0.33.0,<1;python_version>="3.10" +trio-websocket~=0.12.2 +wsproto==1.2.0;python_version<"3.10" +wsproto~=1.3.2;python_version>="3.10" +websocket-client~=1.9.0 +selenium==4.32.0;python_version<"3.10" +selenium==4.46.0;python_version>="3.10" +cssselect==1.3.0;python_version<"3.10" +cssselect>=1.5.0,<2;python_version>="3.10" +sortedcontainers==2.4.0 +execnet==2.1.1;python_version<"3.10" +execnet==2.1.2;python_version>="3.10" +iniconfig==2.1.0;python_version<"3.10" +iniconfig==2.3.0;python_version>="3.10" +pluggy==1.6.0 +pytest==8.4.2;python_version<"3.11" +pytest==9.1.1;python_version>="3.11" +pytest-html==4.0.2 +pytest-metadata==3.1.1 +pytest-ordering==0.6 +pytest-rerunfailures==16.0.1;python_version<"3.10" +pytest-rerunfailures==16.3;python_version=="3.10" +pytest-rerunfailures==16.4;python_version>="3.11" +pytest-xdist==3.8.0 +parameterized==0.9.0 +behave==1.2.6 +soupsieve~=2.8.4;python_version<"3.10" +soupsieve~=2.9.2;python_version>="3.10" +beautifulsoup4~=4.15.0 +pyotp~=2.10.0 +python-xlib==0.33;platform_system=="Linux" +PyAutoGUI>=0.9.54;platform_system=="Linux" +markdown-it-py==3.0.0;python_version<"3.10" +markdown-it-py==4.2.0;python_version>="3.10" +mdurl==0.1.2 +rich>=15.0.0,<16 + +# --- Testing Requirements --- # +# ("pip install -r requirements.txt" also installs this, but "pip install -e ." won't.) + +coverage>=7.10.7;python_version<"3.10" +coverage>=7.15.4;python_version>="3.10" +pytest-cov>=7.1.0 +flake8==7.3.0 +mccabe==0.7.0 +pyflakes==3.4.0 +pycodestyle==2.14.0 diff --git a/sbase/ReadMe.txt b/sbase/ReadMe.txt new file mode 100644 index 00000000000..20fd677357b --- /dev/null +++ b/sbase/ReadMe.txt @@ -0,0 +1,2 @@ +"SBase" is the short name of "SeleniumBase". +Use with console scripts: "python -m sbase". diff --git a/sbase/__init__.py b/sbase/__init__.py new file mode 100644 index 00000000000..ff1a31a52a3 --- /dev/null +++ b/sbase/__init__.py @@ -0,0 +1,14 @@ +from seleniumbase import __version__ # noqa +from seleniumbase import BaseCase # noqa +from seleniumbase import decorators # noqa +from seleniumbase import Driver # noqa +from seleniumbase import DriverContext # noqa +from seleniumbase import encryption # noqa +from seleniumbase import get_driver # noqa +from seleniumbase import js_utils # noqa +from seleniumbase import shared_utils # noqa +from seleniumbase import MasterQA # noqa +from seleniumbase import page_actions # noqa +from seleniumbase import page_utils # noqa +from seleniumbase import SB # noqa +from seleniumbase import translate # noqa diff --git a/sbase/__main__.py b/sbase/__main__.py new file mode 100644 index 00000000000..754adfaefd6 --- /dev/null +++ b/sbase/__main__.py @@ -0,0 +1,22 @@ +import os +import sys + +# Remove "" and current working directory from the first entry +# of sys.path (if present) to avoid using the current directory +# in SeleniumBase commands when invoked as "python -m sbase " +if sys.path[0] in ("", os.getcwd()): + sys.path.pop(0) + +if __package__ == "": + path = os.path.dirname(os.path.dirname(__file__)) + sys.path.insert(0, path) + +if __name__ == "__main__": + import warnings + from seleniumbase.console_scripts.run import main + + warnings.filterwarnings( + "ignore", category=DeprecationWarning, module=".*packaging\\.version" + ) + main() + sys.exit() diff --git a/sbase/steps.py b/sbase/steps.py new file mode 100644 index 00000000000..7525686fa00 --- /dev/null +++ b/sbase/steps.py @@ -0,0 +1,1216 @@ +from behave import step + + +def normalize_text(text): + text = text.replace("\\\\", "\\").replace("\\t", "\t").replace("\\n", "\n") + text = text.replace('\\"', '"').replace("\\'", "'") + return text + + +@step("Open '{url}'") +@step('Open "{url}"') +@step("Open URL '{url}'") +@step('Open URL "{url}"') +@step("User opens '{url}'") +@step('User opens "{url}"') +@step("User opens URL '{url}'") +@step('User opens URL "{url}"') +@step("User goes to '{url}'") +@step('User goes to "{url}"') +@step("User goes to URL '{url}'") +@step('User goes to URL "{url}"') +def open_url(context, url): + sb = context.sb + sb.goto(url) + + +@step("Click '{selector}'") +@step('Click "{selector}"') +@step("Click element '{selector}'") +@step('Click element "{selector}"') +@step("User clicks '{selector}'") +@step('User clicks "{selector}"') +@step("User clicks element '{selector}'") +@step('User clicks element "{selector}"') +def click_element(context, selector): + sb = context.sb + sb.click(selector) + + +@step("Type text '{text}' into '{selector}'") +@step('Type text "{text}" into "{selector}"') +@step("Type text '{text}' into \"{selector}\"") +@step('Type text "{text}" into \'{selector}\'') +@step("Type text '{text}' in '{selector}'") +@step('Type text "{text}" in "{selector}"') +@step("Type text '{text}' in \"{selector}\"") +@step('Type text "{text}" in \'{selector}\'') +@step("Type '{text}' into '{selector}'") +@step('Type "{text}" into "{selector}"') +@step("Type '{text}' into \"{selector}\"") +@step('Type "{text}" into \'{selector}\'') +@step("Type '{text}' in '{selector}'") +@step('Type "{text}" in "{selector}"') +@step("Type '{text}' in \"{selector}\"") +@step('Type "{text}" in \'{selector}\'') +@step("In '{selector}' type '{text}'") +@step('In "{selector}" type "{text}"') +@step("In '{selector}' type \"{text}\"") +@step('In "{selector}" type \'{text}\'') +@step("Into '{selector}' type '{text}'") +@step('Into "{selector}" type "{text}"') +@step("Into '{selector}' type \"{text}\"") +@step('Into "{selector}" type \'{text}\'') +@step("Find '{selector}' and type '{text}'") +@step('Find "{selector}" and type "{text}"') +@step("Find '{selector}' and type \"{text}\"") +@step('Find "{selector}" and type \'{text}\'') +@step("User types '{text}' in '{selector}'") +@step('User types "{text}" in "{selector}"') +@step("User types '{text}' in \"{selector}\"") +@step('User types "{text}" in \'{selector}\'') +@step("User types '{text}' into '{selector}'") +@step('User types "{text}" into "{selector}"') +@step("User types '{text}' into \"{selector}\"") +@step('User types "{text}" into \'{selector}\'') +def type_text(context, selector, text): + sb = context.sb + text = normalize_text(text) + sb.type(selector, text) + + +@step("Add text '{text}' into '{selector}'") +@step('Add text "{text}" into "{selector}"') +@step("Add text '{text}' into \"{selector}\"") +@step('Add text "{text}" into \'{selector}\'') +@step("Add text '{text}' in '{selector}'") +@step('Add text "{text}" in "{selector}"') +@step("Add text '{text}' in \"{selector}\"") +@step('Add text "{text}" in \'{selector}\'') +@step("Add '{text}' into '{selector}'") +@step('Add "{text}" into "{selector}"') +@step("Add '{text}' into \"{selector}\"") +@step('Add "{text}" into \'{selector}\'') +@step("Add '{text}' in '{selector}'") +@step('Add "{text}" in "{selector}"') +@step("Add '{text}' in \"{selector}\"") +@step('Add "{text}" in \'{selector}\'') +@step("Into '{selector}' add '{text}'") +@step('Into "{selector}" add "{text}"') +@step("Into '{selector}' add \"{text}\"") +@step('Into "{selector}" add \'{text}\'') +@step("In '{selector}' add '{text}'") +@step('In "{selector}" add "{text}"') +@step("In '{selector}' add \"{text}\"") +@step('In "{selector}" add \'{text}\'') +@step("User adds '{text}' in '{selector}'") +@step('User adds "{text}" in "{selector}"') +@step("User adds '{text}' in \"{selector}\"") +@step('User adds "{text}" in \'{selector}\'') +@step("User adds '{text}' into '{selector}'") +@step('User adds "{text}" into "{selector}"') +@step("User adds '{text}' into \"{selector}\"") +@step('User adds "{text}" into \'{selector}\'') +def add_text(context, text, selector): + sb = context.sb + text = normalize_text(text) + sb.add_text(selector, text) + + +@step("Assert element '{selector}'") +@step('Assert element "{selector}"') +@step("Assert element '{selector}' is visible") +@step('Assert element "{selector}" is visible') +@step("Element '{selector}' should be visible") +@step('Element "{selector}" should be visible') +def assert_element(context, selector): + sb = context.sb + sb.assert_element(selector) + + +@step("Assert text '{text}' in '{selector}'") +@step('Assert text "{text}" in "{selector}"') +@step("Assert text '{text}' in \"{selector}\"") +@step('Assert text "{text}" in \'{selector}\'') +@step("Text in '{selector}' should contain '{text}'") +@step('Text in "{selector}" should contain "{text}"') +@step("Text in '{selector}' should contain \"{text}\"") +@step('Text in "{selector}" should contain \'{text}\'') +def assert_text_in_element(context, text, selector): + sb = context.sb + text = normalize_text(text) + sb.assert_text(text, selector) + + +@step("Assert text '{text}'") +@step('Assert text "{text}"') +@step("Assert text '{text}' is visible") +@step('Assert text "{text}" is visible') +@step("Text '{text}' should be visible") +@step('Text "{text}" should be visible') +def assert_text(context, text): + sb = context.sb + text = normalize_text(text) + sb.assert_text(text) + + +@step("Assert exact text '{text}' in '{selector}'") +@step('Assert exact text "{text}" in "{selector}"') +@step("Assert exact text '{text}' in \"{selector}\"") +@step('Assert exact text "{text}" in \'{selector}\'') +@step("Text in '{selector}' should be '{text}'") +@step('Text in "{selector}" should be "{text}"') +@step("Text in '{selector}' should be \"{text}\"") +@step('Text in "{selector}" should be \'{text}\'') +def assert_exact_text(context, text, selector): + sb = context.sb + text = normalize_text(text) + sb.assert_exact_text(text, selector) + + +@step("Assert non-empty text in '{selector}'") +@step('Assert non-empty text in "{selector}"') +@step("Assert text in '{selector}' is not empty") +@step('Assert text in "{selector}" is not empty') +@step("Text in '{selector}' should be non-empty") +@step('Text in "{selector}" should be non-empty') +@step("Text in '{selector}' should not be empty") +@step('Text in "{selector}" should not be empty') +def assert_non_empty_text(context, selector): + sb = context.sb + sb.assert_non_empty_text(selector) + + +@step("Highlight '{selector}'") +@step('Highlight "{selector}"') +@step("Highlight element '{selector}'") +@step('Highlight element "{selector}"') +@step("Use JS to highlight '{selector}'") +@step('Use JS to highlight "{selector}"') +def highlight_element(context, selector): + sb = context.sb + sb.highlight(selector) + + +@step("Click link '{link}'") +@step('Click link "{link}"') +@step("User clicks link '{link}'") +@step('User clicks link "{link}"') +def click_link(context, link): + sb = context.sb + sb.click_link(link) + + +@step("JS click '{selector}'") +@step('JS click "{selector}"') +@step("JS click element '{selector}'") +@step('JS click element "{selector}"') +@step("Use JS to click '{selector}'") +@step('Use JS to click "{selector}"') +def js_click(context, selector): + sb = context.sb + sb.js_click(selector) + + +@step("Save screenshot as '{name}'") +@step('Save screenshot as "{name}"') +@step("User saves screenshot as '{name}'") +@step('User saves screenshot as "{name}"') +def save_screenshot_as(context, name): + sb = context.sb + name = normalize_text(name) + sb.save_screenshot(name) + + +@step("Save screenshot to '{folder}' as '{name}'") +@step('Save screenshot to "{folder}" as "{name}"') +@step("Save screenshot to '{folder}' as \"{name}\"") +@step('Save screenshot to "{folder}" as \'{name}\'') +@step("User saves screenshot to '{folder}' as '{name}'") +@step('User saves screenshot to "{folder}" as "{name}"') +@step("User saves screenshot to '{folder}' as \"{name}\"") +@step('User saves screenshot to "{folder}" as \'{name}\'') +def save_screenshot_to_folder_as(context, name, folder): + sb = context.sb + name = normalize_text(name) + sb.save_screenshot(name, folder) + + +@step("Save screenshot to logs") +@step("Save a screenshot to the logs") +@step("User saves screenshot to logs") +@step("User saves a screenshot to the logs") +def save_screenshot_to_logs(context): + sb = context.sb + sb.save_screenshot_to_logs() + + +@step("Refresh page") +@step("Reload page") +@step("User refreshes the page") +@step("User reloads the page") +def refresh_page(context): + sb = context.sb + sb.refresh_page() + + +@step("Go back") +@step("User goes back") +@step("User navigates back") +def go_back(context): + sb = context.sb + sb.go_back() + + +@step("Go forward") +@step("User goes forward") +@step("User navigates forward") +def go_forward(context): + sb = context.sb + sb.go_forward() + + +@step("Set value of '{selector}' to '{text}'") +@step('Set value of "{selector}" to "{text}"') +@step("Set value of \"{selector}\" to '{text}'") +@step('Set value of \'{selector}\' to "{text}"') +@step("User sets value of '{selector}' to '{text}'") +@step('User sets value of "{selector}" to "{text}"') +@step("User sets value of \"{selector}\" to '{text}'") +@step('User sets value of \'{selector}\' to "{text}"') +def set_value(context, selector, text): + sb = context.sb + text = normalize_text(text) + sb.set_value(selector, text) + + +@step("Switch to iframe '{frame}'") +@step('Switch to iframe "{frame}"') +@step("Switch to frame '{frame}'") +@step('Switch to frame "{frame}"') +@step("User switches to iframe '{frame}'") +@step('User switches to iframe "{frame}"') +@step("User switches to frame '{frame}'") +@step('User switches to frame "{frame}"') +def switch_to_frame(context, frame): + sb = context.sb + sb.switch_to_frame(frame) + + +@step("Switch to default content") +@step("Exit from iframes") +@step("Exit from frames") +@step("User switches to default content") +@step("User exits from iframes") +@step("User exits from frames") +def switch_to_default_content(context): + sb = context.sb + sb.switch_to_default_content() + + +@step("Switch to parent frame") +@step("Exit current iframe") +@step("Exit current frame") +@step("User switches to parent frame") +@step("User exits current iframe") +@step("User exits current frame") +def switch_to_parent_frame(context): + sb = context.sb + sb.switch_to_parent_frame() + + +@step("Into '{selector}' enter MFA code '{totp_key}'") +@step('Into "{selector}" enter MFA code "{totp_key}"') +@step("Into '{selector}' enter MFA code \"{totp_key}\"") +@step('Into "{selector}" enter MFA code \'{totp_key}\'') +@step("Into '{selector}' do MFA '{totp_key}'") +@step('Into "{selector}" do MFA "{totp_key}"') +@step("Into '{selector}' do MFA \"{totp_key}\"") +@step('Into "{selector}" do MFA \'{totp_key}\'') +@step("Do MFA '{totp_key}' into '{selector}'") +@step('Do MFA "{totp_key}" into "{selector}"') +@step("Do MFA \"{totp_key}\" into '{selector}'") +@step('Do MFA \'{totp_key}\' into "{selector}"') +@step("Enter MFA code '{totp_key}' into '{selector}'") +@step('Enter MFA code "{totp_key}" into "{selector}"') +@step("Enter MFA code \"{totp_key}\" into '{selector}'") +@step('Enter MFA code \'{totp_key}\' into "{selector}"') +@step("User enters MFA code '{totp_key}' into '{selector}'") +@step('User enters MFA code "{totp_key}" into "{selector}"') +@step("User enters MFA code \"{totp_key}\" into '{selector}'") +@step('User enters MFA code \'{totp_key}\' into "{selector}"') +def enter_mfa_code(context, selector, totp_key): + sb = context.sb + sb.enter_mfa_code(selector, totp_key) + + +@step("Open if not '{url}'") +@step('Open if not "{url}"') +@step("Open if not URL '{url}'") +@step('Open if not URL "{url}"') +@step("User opens '{url}' if not on page") +@step('User opens "{url}" if not on page') +@step("User opens URL '{url}' if not on page") +@step('User opens URL "{url}" if not on page') +def open_if_not_url(context, url): + sb = context.sb + sb.open_if_not_url(url) + + +@step("Select if unselected '{selector}'") +@step('Select if unselected "{selector}"') +@step("Select '{selector}' if unselected") +@step('Select "{selector}" if unselected') +@step("User selects '{selector}' if unselected") +@step('User selects "{selector}" if unselected') +def select_if_unselected(context, selector): + sb = context.sb + sb.select_if_unselected(selector) + + +@step("Unselect if selected '{selector}'") +@step('Unselect if selected "{selector}"') +@step("Unselect '{selector}' if selected") +@step('Unselect "{selector}" if selected') +@step("User unselects '{selector}' if selected") +@step('User unselects "{selector}" if selected') +def unselect_if_selected(context, selector): + sb = context.sb + sb.unselect_if_selected(selector) + + +@step("Check if unchecked '{selector}'") +@step('Check if unchecked "{selector}"') +@step("Check '{selector}' if unchecked") +@step('Check "{selector}" if unchecked') +@step("User checks '{selector}' if unchecked") +@step('User checks "{selector}" if unchecked') +def check_if_unchecked(context, selector): + sb = context.sb + sb.check_if_unchecked(selector) + + +@step("Uncheck if checked '{selector}'") +@step('Uncheck if checked "{selector}"') +@step("Uncheck '{selector}' if checked") +@step('Uncheck "{selector}" if checked') +@step("User unchecks '{selector}' if checked") +@step('User unchecks "{selector}" if checked') +def uncheck_if_checked(context, selector): + sb = context.sb + sb.uncheck_if_checked(selector) + + +@step("Drag '{drag_selector}' into '{drop_selector}'") +@step('Drag "{drag_selector}" into "{drop_selector}"') +@step("Drag '{drag_selector}' into \"{drop_selector}\"") +@step('Drag "{drag_selector}" into \'{drop_selector}\'') +@step("User drags '{drag_selector}' into '{drop_selector}'") +@step('User drags "{drag_selector}" into "{drop_selector}"') +@step("User drags '{drag_selector}' into \"{drop_selector}\"") +@step('User drags "{drag_selector}" into \'{drop_selector}\'') +def drag_and_drop(context, drag_selector, drop_selector): + sb = context.sb + sb.drag_and_drop(drag_selector, drop_selector) + + +@step("Hover '{hover_selector}' and click '{click_selector}'") +@step('Hover "{hover_selector}" and click "{click_selector}"') +@step("Hover '{hover_selector}' and click \"{click_selector}\"") +@step('Hover "{hover_selector}" and click \'{click_selector}\'') +@step("User hovers '{hover_selector}' and clicks '{click_selector}'") +@step('User hovers "{hover_selector}" and clicks "{click_selector}"') +@step("User hovers '{hover_selector}' and clicks \"{click_selector}\"") +@step('User hovers "{hover_selector}" and clicks \'{click_selector}\'') +def hover_and_click(context, hover_selector, click_selector): + sb = context.sb + sb.hover_and_click(hover_selector, click_selector) + + +@step("Find '{selector}' and select '{text}'") +@step('Find "{selector}" and select "{text}"') +@step("Find '{selector}' and select \"{text}\"") +@step('Find "{selector}" and select \'{text}\'') +@step("User selects '{text}' in '{selector}'") +@step('User selects "{text}" in "{selector}"') +@step("User selects \"{text}\" in '{selector}'") +@step('User selects \'{text}\' in "{selector}"') +@step("User finds '{selector}' and selects '{text}'") +@step('User finds "{selector}" and selects "{text}"') +@step("User finds '{selector}' and selects \"{text}\"") +@step('User finds "{selector}" and selects \'{text}\'') +def select_option_by_text(context, selector, text): + sb = context.sb + text = normalize_text(text) + sb.select_option_by_text(selector, text) + + +@step("Find '{selector}' and select '{text}' by {option}") +@step('Find "{selector}" and select "{text}" by {option}') +@step("Find '{selector}' and select \"{text}\" by {option}") +@step('Find "{selector}" and select \'{text}\' by {option}') +@step("User finds '{selector}' and selects '{text}' by {option}") +@step('User finds "{selector}" and selects "{text}" by {option}') +@step("User finds '{selector}' and selects \"{text}\" by {option}") +@step('User finds "{selector}" and selects \'{text}\' by {option}') +def select_option_by_option(context, selector, text, option): + sb = context.sb + text = normalize_text(text) + if option.startswith("'") or option.startswith('"'): + option = option[1:] + if option.endswith("'") or option.endswith('"'): + option = option[:-1] + if option == "text": + sb.select_option_by_text(selector, text) + elif option == "index": + sb.select_option_by_index(selector, text) + elif option == "value": + sb.select_option_by_value(selector, text) + else: + raise Exception("Unknown option: %s" % option) + + +@step("Wait for '{selector}' to be visible") +@step('Wait for "{selector}" to be visible') +@step("Wait for element '{selector}'") +@step('Wait for element "{selector}"') +@step("User waits for '{selector}' to be visible") +@step('User waits for "{selector}" to be visible') +@step("User waits for element '{selector}'") +@step('User waits for element "{selector}"') +def wait_for_element(context, selector): + sb = context.sb + sb.wait_for_element(selector) + + +@step("Wait for text '{text}' in '{selector}'") +@step('Wait for text "{text}" in "{selector}"') +@step("Wait for text '{text}' in \"{selector}\"") +@step('Wait for text "{text}" in \'{selector}\'') +@step("Wait for '{selector}' to have text '{text}'") +@step('Wait for "{selector}" to have text "{text}"') +@step('Wait for "{selector}" to have text \'{text}\'') +@step("Wait for '{selector}' to have text \"{text}\"") +@step("User waits for text '{text}' in '{selector}'") +@step('User waits for text "{text}" in "{selector}"') +@step("User waits for text '{text}' in \"{selector}\"") +@step('User waits for text "{text}" in \'{selector}\'') +@step("User waits for '{selector}' to have text '{text}'") +@step('User waits for "{selector}" to have text "{text}"') +@step('User waits for "{selector}" to have text \'{text}\'') +@step("User waits for '{selector}' to have text \"{text}\"") +def wait_for_text_in_element(context, text, selector): + sb = context.sb + text = normalize_text(text) + sb.wait_for_text(text, selector) + + +@step("Wait for exact text '{text}' in '{selector}'") +@step('Wait for exact text "{text}" in "{selector}"') +@step("Wait for exact text '{text}' in \"{selector}\"") +@step('Wait for exact text "{text}" in \'{selector}\'') +@step("Wait for '{selector}' to have exact text '{text}'") +@step('Wait for "{selector}" to have exact text "{text}"') +@step('Wait for "{selector}" to have exact text \'{text}\'') +@step("Wait for '{selector}' to have exact text \"{text}\"") +@step("User waits for exact text '{text}' in '{selector}'") +@step('User waits for exact text "{text}" in "{selector}"') +@step("User waits for exact text '{text}' in \"{selector}\"") +@step('User waits for exact text "{text}" in \'{selector}\'') +@step("User waits for '{selector}' to have exact text '{text}'") +@step('User waits for "{selector}" to have exact text "{text}"') +@step('User waits for "{selector}" to have exact text \'{text}\'') +@step("User waits for '{selector}' to have exact text \"{text}\"") +def wait_for_exact_text_in_element(context, text, selector): + sb = context.sb + text = normalize_text(text) + sb.wait_for_exact_text(text, selector) + + +@step("Wait for non-empty text in '{selector}'") +@step('Wait for non-empty text in "{selector}"') +@step("Wait for '{selector}' to have non-empty text") +@step('Wait for "{selector}" to have non-empty text') +@step("User waits for non-empty text in '{selector}'") +@step('User waits for non-empty text in "{selector}"') +@step("User waits for '{selector}' to have non-empty text") +@step('User waits for "{selector}" to have non-empty text') +@step("Wait for '{selector}' to not have text") +@step('Wait for "{selector}" to not have text') +@step("Wait for text in '{selector}' to not be empty") +@step('Wait for text in "{selector}" to not be empty') +@step("User waits for '{selector}' to not have text") +@step('User waits for "{selector}" to not have text') +@step("User waits for text in '{selector}' to not be empty") +@step('User waits for text in "{selector}" to not be empty') +def wait_for_non_empty_text_in_element(context, selector): + sb = context.sb + sb.wait_for_non_empty_text(selector) + + +@step("Wait for text '{text}'") +@step('Wait for text "{text}"') +@step("User waits for text '{text}'") +@step('User waits for text "{text}"') +def wait_for_text(context, text): + sb = context.sb + text = normalize_text(text) + sb.wait_for_text(text) + + +@step("Double click '{selector}'") +@step('Double click "{selector}"') +@step("Double click element '{selector}'") +@step('Double click element "{selector}"') +@step("User double clicks '{selector}'") +@step('User double clicks "{selector}"') +@step("User double clicks element '{selector}'") +@step('User double clicks element "{selector}"') +def double_click_element(context, selector): + sb = context.sb + sb.double_click(selector) + + +@step("Slow click '{selector}'") +@step('Slow click "{selector}"') +@step("Slow click element '{selector}'") +@step('Slow click element "{selector}"') +@step("User slow clicks '{selector}'") +@step('User slow clicks "{selector}"') +@step("User slow clicks element '{selector}'") +@step('User slow clicks element "{selector}"') +def slow_click_element(context, selector): + sb = context.sb + sb.slow_click(selector) + + +@step("Clear text field '{selector}'") +@step('Clear text field "{selector}"') +@step("Clear text in '{selector}'") +@step('Clear text in "{selector}"') +@step("User clears text field '{selector}'") +@step('User clears text field "{selector}"') +@step("User clears text in '{selector}'") +@step('User clears text in "{selector}"') +def clear_text_field(context, selector): + sb = context.sb + sb.clear(selector) + + +@step("Maximize window") +@step("Maximize the window") +@step("User maximizes window") +@step("User maximizes the window") +def maximize_window(context): + sb = context.sb + sb.maximize_window() + + +@step("Get new driver") +@step("User gets new driver") +def get_new_driver(context): + sb = context.sb + sb.get_new_driver() + + +@step("Switch to default driver") +@step("User switches to default driver") +def switch_to_default_driver(context): + sb = context.sb + sb.switch_to_default_driver() + + +@step("Press up arrow") +@step("User presses up arrow") +def press_up_arrow(context): + sb = context.sb + sb.press_up_arrow() + + +@step("Press down arrow") +@step("User presses down arrow") +def press_down_arrow(context): + sb = context.sb + sb.press_down_arrow() + + +@step("Press left arrow") +@step("User presses left arrow") +def press_left_arrow(context): + sb = context.sb + sb.press_left_arrow() + + +@step("Press right arrow") +@step("User presses right arrow") +def press_right_arrow(context): + sb = context.sb + sb.press_right_arrow() + + +@step("Clear all cookies") +@step("Delete all cookies") +@step("User clears all cookies") +@step("User deletes all cookies") +def delete_all_cookies(context): + sb = context.sb + sb.delete_all_cookies() + + +@step("Clear Local Storage") +@step("Delete Local Storage") +@step("User clears Local Storage") +@step("User deletes Local Storage") +def clear_local_storage(context): + sb = context.sb + sb.clear_local_storage() + + +@step("Clear Session Storage") +@step("Delete Session Storage") +@step("User clears Session Storage") +@step("User deletes Session Storage") +def clear_session_storage(context): + sb = context.sb + sb.clear_session_storage() + + +@step("JS click all '{selector}'") +@step('JS click all "{selector}"') +@step("Use JS to click all '{selector}'") +@step('Use JS to click all "{selector}"') +def js_click_all(context, selector): + sb = context.sb + sb.js_click_all(selector) + + +@step("Click '{selector}' at ({px},{py})") +@step('Click "{selector}" at ({px},{py})') +@step("Click '{selector}' at ({px}, {py})") +@step('Click "{selector}" at ({px}, {py})') +@step("User clicks '{selector}' at ({px},{py})") +@step('User clicks "{selector}" at ({px},{py})') +@step("User clicks '{selector}' at ({px}, {py})") +@step('User clicks "{selector}" at ({px}, {py})') +def click_with_offset(context, selector, px, py): + sb = context.sb + sb.click_with_offset(selector, px, py) + + +@step("In '{selector}' choose file '{file_path}'") +@step('In "{selector}" choose file "{file_path}"') +@step("In '{selector}' choose file \"{file_path}\"") +@step('In "{selector}" choose file \'{file_path}\'') +@step("Into '{selector}' choose file '{file_path}'") +@step('Into "{selector}" choose file "{file_path}"') +@step("Into '{selector}' choose file \"{file_path}\"") +@step('Into "{selector}" choose file \'{file_path}\'') +@step("User chooses file '{file_path}' for '{selector}'") +@step('User chooses file "{file_path}" for "{selector}" ') +@step("User chooses file \"{file_path}\" for '{selector}' ") +@step('User chooses file \'{file_path}\' for "{selector}" ') +def choose_file(context, selector, file_path): + sb = context.sb + sb.choose_file(selector, file_path) + + +@step("Set content to frame '{frame}'") +@step('Set content to frame "{frame}"') +@step("User sets content to frame '{frame}'") +@step('User sets content to frame "{frame}"') +def set_content_to_frame(context, frame): + sb = context.sb + sb.set_content_to_frame(frame) + + +@step("Set content to default") +@step("User sets content to default") +def set_content_to_default(context): + sb = context.sb + sb.set_content_to_default() + + +@step("Set content to parent") +@step("User sets content to parent") +def set_content_to_parent(context): + sb = context.sb + sb.set_content_to_parent() + + +@step("Assert element present '{selector}'") +@step('Assert element present "{selector}"') +@step("Element '{selector}' should be present") +@step('Element "{selector}" should be present') +def assert_element_present(context, selector): + sb = context.sb + sb.assert_element_present(selector) + + +@step("Assert element not visible '{selector}'") +@step('Assert element not visible "{selector}"') +@step("Element '{selector}' should not be visible") +@step('Element "{selector}" should not be visible') +def assert_element_not_visible(context, selector): + sb = context.sb + sb.assert_element_not_visible(selector) + + +@step("Assert link text '{text}'") +@step('Assert link text "{text}"') +@step("Link text '{text}' should be visible") +@step('Link text "{text}" should be visible') +def assert_link_text(context, text): + sb = context.sb + text = normalize_text(text) + sb.assert_link_text(text) + + +@step("Assert title '{title}'") +@step('Assert title "{title}"') +@step("The title should be '{title}'") +@step('The title should be "{title}"') +def assert_title(context, title): + sb = context.sb + title = normalize_text(title) + sb.assert_title(title) + + +@step("Assert downloaded file '{file}'") +@step('Assert downloaded file "{file}"') +@step("File '{file}' should be in downloads") +@step('File "{file}" should be in downloads') +def assert_downloaded_file(context, file): + sb = context.sb + sb.assert_downloaded_file(file) + + +@step("Download '{file}' to downloads") +@step('Download "{file}" to downloads') +@step("Download file '{file}' to downloads") +@step('Download file "{file}" to downloads') +@step("User downloads '{file}' to downloads") +@step('User downloads "{file}" to downloads') +def download_file(context, file): + sb = context.sb + sb.download_file(file) + + +@step("Download '{file}' to '{destination}'") +@step('Download "{file}" to "{destination}"') +@step("Download file '{file}' to '{destination}'") +@step('Download file "{file}" to "{destination}"') +@step("User downloads '{file}' to '{destination}'") +@step('User downloads "{file}" to "{destination}"') +def download_file_to_destination(context, file, destination): + sb = context.sb + sb.download_file(file, destination) + + +@step("In '{selector}' assert attribute \'{attribute}\'") +@step('In "{selector}" assert attribute \"{attribute}\"') +@step("In \"{selector}\" assert attribute '{attribute}'") +@step('In \'{selector}\' assert attribute "{attribute}"') +def assert_attribute(context, selector, attribute): + sb = context.sb + sb.assert_attribute(selector, attribute) + + +@step("In '{selector}' assert attribute/value '{attribute}'/'{value}'") +@step('In "{selector}" assert attribute/value "{attribute}"/"{value}"') +@step("In \"{selector}\" assert attribute/value '{attribute}'/\"{value}\"") +@step('In \'{selector}\' assert attribute/value "{attribute}"/\'{value}\'') +@step("In '{selector}' assert attribute/value '{attribute}'/\"{value}\"") +@step('In "{selector}" assert attribute/value "{attribute}"/\'{value}\'') +@step("In \"{selector}\" assert attribute/value '{attribute}'/'{value}'") +@step('In \'{selector}\' assert attribute/value "{attribute}"/"{value}"') +def assert_attribute_has_value(context, selector, attribute, value): + sb = context.sb + value = normalize_text(value) + sb.assert_attribute(selector, attribute, value) + + +@step("Show file choosers") +@step("Show hidden file choosers") +@step("Use JS to show file choosers") +@step("Use JS to show hidden file choosers") +def show_file_choosers(context): + sb = context.sb + sb.show_file_choosers() + + +@step("Sleep for {seconds} seconds") +@step("Wait for {seconds} seconds") +@step("User sleeps for {seconds} seconds") +@step("User waits for {seconds} seconds") +def sleep(context, seconds): + sb = context.sb + sb.sleep(float(seconds)) + + +@step("Activate Demo Mode") +@step("User activates Demo Mode") +def activate_demo_mode(context): + sb = context.sb + sb.activate_demo_mode() + + +@step("Deactivate Demo Mode") +@step("User deactivates Demo Mode") +def deactivate_demo_mode(context): + sb = context.sb + sb.deactivate_demo_mode() + + +@step("Deferred assert element '{selector}'") +@step('Deferred assert element "{selector}"') +def deferred_assert_element(context, selector): + sb = context.sb + sb.deferred_assert_element(selector) + + +@step("Deferred assert element present '{selector}'") +@step('Deferred assert element present "{selector}"') +def deferred_assert_element_present(context, selector): + sb = context.sb + sb.deferred_assert_element_present(selector) + + +@step("Deferred assert text '{text}' in '{selector}'") +@step('Deferred assert text "{text}" in "{selector}"') +@step("Deferred assert text '{text}' in \"{selector}\"") +@step('Deferred assert text "{text}" in \'{selector}\'') +def deferred_assert_text_in_element(context, text, selector): + sb = context.sb + text = normalize_text(text) + sb.deferred_assert_text(text, selector) + + +@step("Deferred assert text '{text}'") +@step('Deferred assert text "{text}"') +def deferred_assert_text(context, text): + sb = context.sb + text = normalize_text(text) + sb.deferred_assert_text(text) + + +@step("Deferred assert exact text '{text}' in '{selector}'") +@step('Deferred assert exact text "{text}" in "{selector}"') +@step("Deferred assert exact text '{text}' in \"{selector}\"") +@step('Deferred assert exact text "{text}" in \'{selector}\'') +def deferred_assert_exact_text(context, text, selector): + sb = context.sb + text = normalize_text(text) + sb.deferred_assert_exact_text(text, selector) + + +@step("Deferred assert non-empty text in '{selector}'") +@step('Deferred assert non-empty text in "{selector}"') +def deferred_assert_non_empty_text(context, selector): + sb = context.sb + sb.deferred_assert_non_empty_text(selector) + + +@step("Process deferred asserts") +def process_deferred_asserts(context): + sb = context.sb + sb.process_deferred_asserts() + + +@step("Assert text not visible '{text}' in '{selector}'") +@step('Assert text not visible "{text}" in "{selector}"') +@step("Assert text not visible '{text}' in \"{selector}\"") +@step('Assert text not visible "{text}" in \'{selector}\'') +@step("Text '{text}' should not be visible in '{selector}'") +@step('Text "{text}" should not be visible in "{selector}"') +@step("Text '{text}' should not be visible in \"{selector}\"") +@step('Text "{text}" should not be visible in \'{selector}\'') +def assert_text_not_visible_in_element(context, text, selector): + sb = context.sb + text = normalize_text(text) + sb.assert_text_not_visible(text, selector) + + +@step("Assert text not visible '{text}'") +@step('Assert text not visible "{text}"') +@step("Text '{text}' should not be visible") +@step('Text "{text}" should not be visible') +def assert_text_not_visible(context, text): + sb = context.sb + text = normalize_text(text) + sb.assert_text_not_visible(text) + + +@step("Assert exact text not visible '{text}' in '{selector}'") +@step('Assert exact text not visible "{text}" in "{selector}"') +@step("Assert exact text not visible '{text}' in \"{selector}\"") +@step('Assert exact text not visible "{text}" in \'{selector}\'') +@step("Exact text '{text}' should not be visible in '{selector}'") +@step('Exact text "{text}" should not be visible in "{selector}"') +@step("Exact text '{text}' should not be visible in \"{selector}\"") +@step('Exact text "{text}" should not be visible in \'{selector}\'') +def assert_exact_text_not_visible_in_element(context, text, selector): + sb = context.sb + text = normalize_text(text) + sb.assert_exact_text_not_visible(text, selector) + + +@step("Assert exact text not visible '{text}'") +@step('Assert exact text not visible "{text}"') +@step("Exact text '{text}' should not be visible") +@step('Exact text "{text}" should not be visible') +def assert_exact_text_not_visible(context, text): + sb = context.sb + text = normalize_text(text) + sb.assert_exact_text_not_visible(text) + + +@step("Assert title contains '{substring}'") +@step('Assert title contains "{substring}"') +@step("The title should contain '{substring}'") +@step('The title should contain "{substring}"') +def assert_title_contains(context, substring): + sb = context.sb + substring = normalize_text(substring) + sb.assert_title_contains(substring) + + +@step("Open new tab") +@step("Open new window") +@step("User opens new tab") +@step("User opens new window") +def open_new_window(context): + sb = context.sb + sb.open_new_window() + + +@step("Accept alert") +@step("User accepts alert") +def accept_alert(context): + sb = context.sb + sb.accept_alert() + + +@step("Dismiss alert") +@step("User dismisses alert") +def dismiss_alert(context): + sb = context.sb + sb.dismiss_alert() + + +@step("Assert URL '{url}'") +@step('Assert URL "{url}"') +@step("The URL should be '{url}'") +@step('The URL should be "{url}"') +def assert_url(context, url): + sb = context.sb + url = normalize_text(url) + sb.assert_url(url) + + +@step("Assert URL contains '{substring}'") +@step('Assert URL contains "{substring}"') +@step("The URL should contain '{substring}'") +@step('The URL should contain "{substring}"') +def assert_url_contains(context, substring): + sb = context.sb + substring = normalize_text(substring) + sb.assert_url_contains(substring) + + +@step("Hover '{selector}'") +@step('Hover "{selector}"') +@step("Hover over '{selector}'") +@step('Hover over "{selector}"') +@step("Hover element '{selector}'") +@step('Hover element "{selector}"') +@step("User hovers over '{selector}'") +@step('User hovers over "{selector}"') +@step("User hovers over element '{selector}'") +@step('User hovers over element "{selector}"') +def hover(context, selector): + sb = context.sb + sb.hover(selector) + + +@step("Context click '{selector}'") +@step('Context click "{selector}"') +@step("Context click element '{selector}'") +@step('Context click element "{selector}"') +@step("Right click '{selector}'") +@step('Right click "{selector}"') +@step("Right click element '{selector}'") +@step('Right click element "{selector}"') +@step("User right clicks '{selector}'") +@step('User right clicks "{selector}"') +@step("User right clicks element '{selector}'") +@step('User right clicks element "{selector}"') +def context_click(context, selector): + sb = context.sb + sb.context_click(selector) + + +@step("JS type '{text}' in '{selector}'") +@step('JS type "{text}" in "{selector}"') +@step("JS type '{text}' in \"{selector}\"") +@step('JS type "{text}" in \'{selector}\'') +@step("JS type '{text}' into '{selector}'") +@step('JS type "{text}" into "{selector}"') +@step("JS type '{text}' into \"{selector}\"") +@step('JS type "{text}" into \'{selector}\'') +@step("JS type text '{text}' in '{selector}'") +@step('JS type text "{text}" in "{selector}"') +@step("JS type text '{text}' in \"{selector}\"") +@step('JS type text "{text}" in \'{selector}\'') +@step("JS type text '{text}' into '{selector}'") +@step('JS type text "{text}" into "{selector}"') +@step("JS type text '{text}' into \"{selector}\"") +@step('JS type text "{text}" into \'{selector}\'') +@step("Use JS to type '{text}' in '{selector}'") +@step('Use JS to type "{text}" in "{selector}"') +@step("Use JS to type '{text}' in \"{selector}\"") +@step('Use JS to type "{text}" in \'{selector}\'') +@step("Use JS to type '{text}' into '{selector}'") +@step('Use JS to type "{text}" into "{selector}"') +@step("Use JS to type '{text}' into \"{selector}\"") +@step('Use JS to type "{text}" into \'{selector}\'') +def js_type(context, text, selector): + sb = context.sb + text = normalize_text(text) + sb.js_type(selector, text) + + +@step("jQuery click '{selector}'") +@step('jQuery click "{selector}"') +@step("jQuery click element '{selector}'") +@step('jQuery click element "{selector}"') +@step("Use jQuery to click '{selector}'") +@step('Use jQuery to click "{selector}"') +def jquery_click(context, selector): + sb = context.sb + sb.jquery_click(selector) + + +@step("jQuery click all '{selector}'") +@step('jQuery click all "{selector}"') +@step("Use jQuery to click all '{selector}'") +@step('Use jQuery to click all "{selector}"') +def jquery_click_all(context, selector): + sb = context.sb + sb.jquery_click_all(selector) + + +@step("jQuery type '{text}' in '{selector}'") +@step('jQuery type "{text}" in "{selector}"') +@step("jQuery type '{text}' in \"{selector}\"") +@step('jQuery type "{text}" in \'{selector}\'') +@step("jQuery type '{text}' into '{selector}'") +@step('jQuery type "{text}" into "{selector}"') +@step("jQuery type '{text}' into \"{selector}\"") +@step('jQuery type "{text}" into \'{selector}\'') +@step("jQuery type text '{text}' in '{selector}'") +@step('jQuery type text "{text}" in "{selector}"') +@step("jQuery type text '{text}' in \"{selector}\"") +@step('jQuery type text "{text}" in \'{selector}\'') +@step("jQuery type text '{text}' into '{selector}'") +@step('jQuery type text "{text}" into "{selector}"') +@step("jQuery type text '{text}' into \"{selector}\"") +@step('jQuery type text "{text}" into \'{selector}\'') +@step("Use jQuery to type '{text}' in '{selector}'") +@step('Use jQuery to type "{text}" in "{selector}"') +@step("Use jQuery to type '{text}' in \"{selector}\"") +@step('Use jQuery to type "{text}" in \'{selector}\'') +@step("Use jQuery to type '{text}' into '{selector}'") +@step('Use jQuery to type "{text}" into "{selector}"') +@step("Use jQuery to type '{text}' into \"{selector}\"") +@step('Use jQuery to type "{text}" into \'{selector}\'') +def jquery_type(context, text, selector): + sb = context.sb + text = normalize_text(text) + sb.jquery_type(selector, text) + + +@step("Press keys '{text}' in '{selector}'") +@step('Press keys "{text}" in "{selector}"') +@step("Press keys '{text}' in \"{selector}\"") +@step('Press keys "{text}" in \'{selector}\'') +@step("Press keys '{text}' into '{selector}'") +@step('Press keys "{text}" into "{selector}"') +@step("Press keys '{text}' into \"{selector}\"") +@step('Press keys "{text}" into \'{selector}\'') +@step("In '{selector}' press keys '{text}'") +@step('In "{selector}" press keys "{text}"') +@step("In '{selector}' press keys \"{text}\"") +@step('In "{selector}" press keys \'{text}\'') +@step("Into '{selector}' press keys '{text}'") +@step('Into "{selector}" press keys "{text}"') +@step("Into '{selector}' press keys \"{text}\"") +@step('Into "{selector}" press keys \'{text}\'') +@step("Find '{selector}' and press keys '{text}'") +@step('Find "{selector}" and press keys "{text}"') +@step("Find '{selector}' and press keys \"{text}\"") +@step('Find "{selector}" and press keys \'{text}\'') +@step("User presses keys '{text}' in '{selector}'") +@step('User presses keys "{text}" in "{selector}"') +@step("User presses keys '{text}' in \"{selector}\"") +@step('User presses keys "{text}" in \'{selector}\'') +@step("User presses keys '{text}' into '{selector}'") +@step('User presses keys "{text}" into "{selector}"') +@step("User presses keys '{text}' into \"{selector}\"") +@step('User presses keys "{text}" into \'{selector}\'') +def press_keys(context, text, selector): + sb = context.sb + text = normalize_text(text) + sb.press_keys(selector, text) + + +@step("Find '{selector}' and set {attribute} to '{value}'") +@step('Find "{selector}" and set {attribute} to "{value}"') +@step("Find '{selector}' and set {attribute} to \"{value}\"") +@step('Find "{selector}" and set {attribute} to \'{value}\'') +def set_attribute(context, selector, attribute, value): + sb = context.sb + value = normalize_text(value) + if attribute.startswith("'") or attribute.startswith('"'): + attribute = attribute[1:] + if attribute.endswith("'") or attribute.endswith('"'): + attribute = attribute[:-1] + sb.set_attribute(selector, attribute, value) + + +@step("Find all '{selector}' and set {attribute} to '{value}'") +@step('Find all "{selector}" and set {attribute} to "{value}"') +@step("Find all '{selector}' and set {attribute} to \"{value}\"") +@step('Find all "{selector}" and set {attribute} to \'{value}\'') +def set_attributes(context, selector, attribute, value): + sb = context.sb + value = normalize_text(value) + if attribute.startswith("'") or attribute.startswith('"'): + attribute = attribute[1:] + if attribute.endswith("'") or attribute.endswith('"'): + attribute = attribute[:-1] + sb.set_attributes(selector, attribute, value) + + +@step("Save as PDF to logs") +@step("Save as PDF to the logs") +@step("User saves page as PDF to logs") +@step("User saves page as PDF to the logs") +def save_as_pdf_to_logs(context): + sb = context.sb + sb.save_as_pdf_to_logs() + + +@step("Save page source to logs") +@step("Save the page source to the logs") +@step("User saves page source to logs") +@step("User saves the page source to the logs") +def save_page_source_to_logs(context): + sb = context.sb + sb.save_page_source_to_logs() + + +@step("Activate CDP Mode") +@step("User activates CDP Mode") +def activate_cdp_mode(context): + sb = context.sb + sb.activate_cdp_mode() diff --git a/seleniumbase/ReadMe.md b/seleniumbase/ReadMe.md new file mode 100644 index 00000000000..a4827dc9a99 --- /dev/null +++ b/seleniumbase/ReadMe.md @@ -0,0 +1,25 @@ +SeleniumBase + +

      Framework Folders

      + +* [fixtures](https://github.com/seleniumbase/SeleniumBase/tree/master/seleniumbase/fixtures): Includes [base_case.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/fixtures/base_case.py), where SeleniumBase test methods are defined. +* [core](https://github.com/seleniumbase/SeleniumBase/tree/master/seleniumbase/core): Includes [browser_launcher.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/core/browser_launcher.py), which is used for spinning up browsers for tests. +* [plugins](https://github.com/seleniumbase/SeleniumBase/tree/master/seleniumbase/plugins): Includes [pytest_plugin.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/plugins/pytest_plugin.py), which is used to add pytest command-line options. +* [console_scripts](https://github.com/seleniumbase/SeleniumBase/tree/master/seleniumbase/console_scripts): Includes [run.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/console_scripts/run.py), which is used to call SeleniumBase console scripts. +* [drivers](https://github.com/seleniumbase/SeleniumBase/tree/master/seleniumbase/drivers): This is the folder where web drivers get downloaded when installing them. +* [config](https://github.com/seleniumbase/SeleniumBase/tree/master/seleniumbase/config): Includes [settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py), which provides default configuration options for tests. +* [js_code](https://github.com/seleniumbase/SeleniumBase/tree/master/seleniumbase/js_code): This folder contains JavaScript code for various SeleniumBase components. +* [undetected](https://github.com/seleniumbase/SeleniumBase/tree/master/seleniumbase/undetected): This folder contains code for preventing sites from detecting Selenium. +* [extensions](https://github.com/seleniumbase/SeleniumBase/tree/master/seleniumbase/extensions): This folder contains Chromium extensions that can be used by tests. +* [common](https://github.com/seleniumbase/SeleniumBase/tree/master/seleniumbase/common): This folder contains Python decorators that can be used by tests. +* [utilities](https://github.com/seleniumbase/SeleniumBase/tree/master/seleniumbase/utilities): This folder contains code for spinning up your own Selenium Grid. +* [resources](https://github.com/seleniumbase/SeleniumBase/tree/master/seleniumbase/resources): This folder contains copies of JavaScript resources used by tests. +* [translate](https://github.com/seleniumbase/SeleniumBase/tree/master/seleniumbase/translate): This folder contains code for translating tests into different languages. +* [behave](https://github.com/seleniumbase/SeleniumBase/tree/master/seleniumbase/behave): This folder contains code for integrating with the Behave BDD test runner. +* [masterqa](https://github.com/seleniumbase/SeleniumBase/tree/master/seleniumbase/masterqa): MasterQA is a tool for combining automation with manual verification. + +-------- + + + +
      SeleniumBase
      diff --git a/seleniumbase/__init__.py b/seleniumbase/__init__.py old mode 100755 new mode 100644 index eb96b1ce4b2..f4bab6f2bbd --- a/seleniumbase/__init__.py +++ b/seleniumbase/__init__.py @@ -1,3 +1,55 @@ +import collections +import os +import pdb +import sys +from contextlib import suppress +from seleniumbase.__version__ import __version__ +from seleniumbase.common import decorators # noqa +from seleniumbase.common import encryption # noqa +from seleniumbase.core import colored_traceback +from seleniumbase.core import sb_cdp # noqa +from seleniumbase.core.browser_launcher import get_driver # noqa +from seleniumbase.fixtures import js_utils # noqa +from seleniumbase.fixtures import page_actions # noqa +from seleniumbase.fixtures import page_utils # noqa +from seleniumbase.fixtures import shared_utils from seleniumbase.fixtures.base_case import BaseCase # noqa from seleniumbase.masterqa.master_qa import MasterQA # noqa -from seleniumbase.common import decorators # noqa +from seleniumbase.plugins.sb_manager import SB # noqa +from seleniumbase.plugins.driver_manager import Driver # noqa +from seleniumbase.plugins.driver_manager import DriverContext # noqa +from seleniumbase.undetected import cdp_driver # noqa +from seleniumbase import translate # noqa + +with suppress(Exception): + import colorama + +with suppress(Exception): + import pdbp # (Pdb+) --- Python Debugger Plus + +with suppress(Exception): + shared_utils.fix_colorama_if_windows() + colorama.init(autoreset=True) + +if sys.version_info[0] < 3 and "pdbp" in locals(): + # With Python3, "import pdbp" is all you need + for key in pdbp.__dict__.keys(): + # Replace pdb with pdbp + pdb.__dict__[key] = pdbp.__dict__[key] + if hasattr(pdb, "DefaultConfig"): + # Here's how to customize Pdb+ options + pdb.DefaultConfig.filename_color = pdb.Color.fuchsia + pdb.DefaultConfig.line_number_color = pdb.Color.turquoise + pdb.DefaultConfig.truncate_long_lines = False + pdb.DefaultConfig.sticky_by_default = True +colored_traceback.add_hook() +os.environ["SE_AVOID_STATS"] = "true" # Disable Selenium Manager stats +if sys.version_info >= (3, 10): + collections.Callable = collections.abc.Callable # Lifeline for nosetests +del collections # Undo "import collections" / Simplify "dir(seleniumbase)" +del os # Undo "import os" / Simplify "dir(seleniumbase)" +del sys # Undo "import sys" / Simplify "dir(seleniumbase)" + +version_list = [int(i) for i in __version__.split(".") if i.isdigit()] +version_tuple = tuple(version_list) +version_info = version_tuple # noqa diff --git a/seleniumbase/__main__.py b/seleniumbase/__main__.py new file mode 100644 index 00000000000..8ad899769fa --- /dev/null +++ b/seleniumbase/__main__.py @@ -0,0 +1,22 @@ +import os +import sys + +# Remove "" and current working directory from the first entry +# of sys.path (if present) to avoid using the current directory +# in SeleniumBase commands when invoked as "python -m seleniumbase " +if sys.path[0] in ("", os.getcwd()): + sys.path.pop(0) + +if __package__ == "": + path = os.path.dirname(os.path.dirname(__file__)) + sys.path.insert(0, path) + +if __name__ == "__main__": + import warnings + from seleniumbase.console_scripts.run import main + + warnings.filterwarnings( + "ignore", category=DeprecationWarning, module=".*packaging\\.version" + ) + main() + sys.exit() diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py new file mode 100755 index 00000000000..8997f2184ad --- /dev/null +++ b/seleniumbase/__version__.py @@ -0,0 +1,2 @@ +# seleniumbase package +__version__ = "4.51.12" diff --git a/seleniumbase/behave/__init__.py b/seleniumbase/behave/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seleniumbase/behave/behave_helper.py b/seleniumbase/behave/behave_helper.py new file mode 100644 index 00000000000..1f5e4618816 --- /dev/null +++ b/seleniumbase/behave/behave_helper.py @@ -0,0 +1,555 @@ +"""Generating Gherkin-formatted code from the Recorder.""" + + +def generate_gherkin(srt_actions): + sb_actions = [] + for action in srt_actions: + if action[0] == "begin" or action[0] == "_url_": + if "%" in action[2]: + try: + from urllib.parse import unquote + + action[2] = unquote(action[2], errors="strict") + except Exception: + pass + if '"' not in action[2]: + sb_actions.append('Open "%s"' % action[2]) + elif "'" not in action[2]: + sb_actions.append("Open '%s'" % action[2]) + else: + sb_actions.append('Open "%s"' % action[2].replace('"', '\\"')) + elif action[0] == "f_url": + if "%" in action[2]: + try: + from urllib.parse import unquote + + action[2] = unquote(action[2], errors="strict") + except Exception: + pass + if '"' not in action[2]: + sb_actions.append('Open if not "%s"' % action[2]) + elif "'" not in action[2]: + sb_actions.append("Open if not '%s'" % action[2]) + else: + sb_actions.append( + 'Open if not "%s"' % action[2].replace('"', '\\"') + ) + elif action[0] == "click": + if '"' not in action[1]: + sb_actions.append('Click "%s"' % action[1]) + else: + sb_actions.append("Click '%s'" % action[1]) + elif action[0] == "dbclk": + if '"' not in action[1]: + sb_actions.append('Double click "%s"' % action[1]) + else: + sb_actions.append("Double click '%s'" % action[1]) + elif action[0] == "js_cl": + if '"' not in action[1]: + sb_actions.append('JS click "%s"' % action[1]) + else: + sb_actions.append("JS click '%s'" % action[1]) + elif action[0] == "js_ca": + if '"' not in action[1]: + sb_actions.append('JS click all "%s"' % action[1]) + else: + sb_actions.append("JS click all '%s'" % action[1]) + elif action[0] == "jq_cl": + if '"' not in action[1]: + sb_actions.append('jQuery click "%s"' % action[1]) + else: + sb_actions.append("jQuery click '%s'" % action[1]) + elif action[0] == "jq_ca": + if '"' not in action[1]: + sb_actions.append('jQuery click all "%s"' % action[1]) + else: + sb_actions.append("jQuery click all '%s'" % action[1]) + elif action[0] == "r_clk": + if '"' not in action[1]: + sb_actions.append('Context click "%s"' % action[1]) + else: + sb_actions.append("Context click '%s'" % action[1]) + elif action[0] == "canva": + selector = action[1][0] + p_x = action[1][1] + p_y = action[1][2] + if '"' not in selector: + sb_actions.append( + 'Click "%s" at (%s, %s)' % (selector, p_x, p_y) + ) + else: + sb_actions.append( + "Click '%s' at (%s, %s)" % (selector, p_x, p_y) + ) + elif action[0] == "input": + text = action[2].replace("\n", "\\n") + if '"' not in text and '"' not in action[1]: + sb_actions.append('Type "%s" into "%s"' % (text, action[1])) + elif '"' in text and '"' not in action[1]: + sb_actions.append('Type \'%s\' into "%s"' % (text, action[1])) + elif '"' not in text and '"' in action[1]: + sb_actions.append('Type "%s" into \'%s\'' % (text, action[1])) + elif '"' in text and '"' in action[1]: + sb_actions.append("Type '%s' into '%s'" % (text, action[1])) + elif action[0] == "js_ty": + text = action[2].replace("\n", "\\n") + if '"' not in text and '"' not in action[1]: + sb_actions.append('JS type "%s" in "%s"' % (text, action[1])) + elif '"' in text and '"' not in action[1]: + sb_actions.append('JS type \'%s\' in "%s"' % (text, action[1])) + elif '"' not in text and '"' in action[1]: + sb_actions.append('JS type "%s" in \'%s\'' % (text, action[1])) + elif '"' in text and '"' in action[1]: + sb_actions.append("JS type '%s' in '%s'" % (text, action[1])) + elif action[0] == "jq_ty": + text = action[2].replace("\n", "\\n") + if '"' not in text and '"' not in action[1]: + sb_actions.append( + 'jQuery type "%s" in "%s"' % (text, action[1]) + ) + elif '"' in text and '"' not in action[1]: + sb_actions.append( + 'jQuery type \'%s\' in "%s"' % (text, action[1]) + ) + elif '"' not in text and '"' in action[1]: + sb_actions.append( + 'jQuery type "%s" in \'%s\'' % (text, action[1]) + ) + elif '"' in text and '"' in action[1]: + sb_actions.append( + "jQuery type '%s' in '%s'" % (text, action[1]) + ) + elif action[0] == "pkeys": + text = action[2].replace("\n", "\\n") + if '"' not in text and '"' not in action[1]: + sb_actions.append( + 'Press keys "%s" in "%s"' % (text, action[1]) + ) + elif '"' in text and '"' not in action[1]: + sb_actions.append( + 'Press keys \'%s\' in "%s"' % (text, action[1]) + ) + elif '"' not in text and '"' in action[1]: + sb_actions.append( + 'Press keys "%s" in \'%s\'' % (text, action[1]) + ) + elif '"' in text and '"' in action[1]: + sb_actions.append( + "Press keys '%s' in '%s'" % (text, action[1]) + ) + elif action[0] == "hover": + if '"' not in action[1]: + sb_actions.append('Hover "%s"' % action[1]) + else: + sb_actions.append("Hover '%s'" % action[1]) + elif action[0] == "e_mfa": + text = action[2].replace("\n", "\\n") + if '"' not in action[1] and '"' not in text: + sb_actions.append('Into "%s" do MFA "%s"' % (action[1], text)) + elif '"' not in action[1] and '"' in text: + sb_actions.append( + 'Into "%s" do MFA \'%s\'' % (action[1], text) + ) + elif '"' in action[1] and '"' not in text: + sb_actions.append( + 'Into \'%s\' do MFA "%s"' % (action[1], text) + ) + elif '"' in action[1] and '"' in text: + sb_actions.append("Into '%s' do MFA '%s'" % (action[1], text)) + elif action[0] == "h_clk": + if '"' not in action[1] and '"' not in action[2]: + sb_actions.append( + 'Hover "%s" and click "%s"' % (action[1], action[2]) + ) + elif '"' not in action[1] and '"' in action[2]: + sb_actions.append( + 'Hover "%s" and click \'%s\'' % (action[1], action[2]) + ) + elif '"' in action[1] and '"' not in action[2]: + sb_actions.append( + 'Hover \'%s\' and click "%s"' % (action[1], action[2]) + ) + elif '"' in action[1] and '"' in action[2]: + sb_actions.append( + "Hover '%s' and click '%s'" % (action[1], action[2]) + ) + elif action[0] == "ddrop": + if '"' not in action[1] and '"' not in action[2]: + sb_actions.append( + 'Drag "%s" into "%s"' % (action[1], action[2]) + ) + elif '"' not in action[1] and '"' in action[2]: + sb_actions.append( + 'Drag "%s" into \'%s\'' % (action[1], action[2]) + ) + elif '"' in action[1] and '"' not in action[2]: + sb_actions.append( + 'Drag \'%s\' into "%s"' % (action[1], action[2]) + ) + elif '"' in action[1] and '"' in action[2]: + sb_actions.append( + "Drag '%s' into '%s'" % (action[1], action[2]) + ) + elif action[0] == "s_opt": + if '"' not in action[1] and '"' not in action[2]: + sb_actions.append( + 'Find "%s" and select "%s"' % (action[1], action[2]) + ) + elif '"' not in action[1] and '"' in action[2]: + sb_actions.append( + 'Find "%s" and select \'%s\'' % (action[1], action[2]) + ) + elif '"' in action[1] and '"' not in action[2]: + sb_actions.append( + 'Find \'%s\' and select "%s"' % (action[1], action[2]) + ) + elif '"' in action[1] and '"' in action[2]: + sb_actions.append( + "Find '%s' and select '%s'" % (action[1], action[2]) + ) + elif action[0] == "set_v": + if '"' not in action[1] and '"' not in action[2]: + sb_actions.append( + 'Set value of "%s" to "%s"' % (action[1], action[2]) + ) + elif '"' not in action[1] and '"' in action[2]: + sb_actions.append( + 'Set value of "%s" to \'%s\'' % (action[1], action[2]) + ) + elif '"' in action[1] and '"' not in action[2]: + sb_actions.append( + 'Set value of \'%s\' to "%s"' % (action[1], action[2]) + ) + elif '"' in action[1] and '"' in action[2]: + sb_actions.append( + "Set value of '%s' to '%s'" % (action[1], action[2]) + ) + elif action[0] == "cho_f": + action[2] = action[2].replace("\\", "\\\\") + if '"' not in action[1] and '"' not in action[2]: + sb_actions.append( + 'Into "%s" choose file "%s"' % (action[1], action[2]) + ) + elif '"' not in action[1] and '"' in action[2]: + sb_actions.append( + 'Into "%s" choose file \'%s\'' % (action[1], action[2]) + ) + elif '"' in action[1] and '"' not in action[2]: + sb_actions.append( + 'Into \'%s\' choose file "%s"' % (action[1], action[2]) + ) + elif '"' in action[1] and '"' in action[2]: + sb_actions.append( + "Into '%s' choose file '%s'" % (action[1], action[2]) + ) + elif action[0] == "sw_fr": + method = "Switch to frame" + if '"' not in action[1]: + sb_actions.append('%s "%s"' % (method, action[1])) + else: + sb_actions.append("%s '%s'" % (method, action[1])) + elif action[0] == "sw_dc": + sb_actions.append("Switch to default content") + elif action[0] == "sw_pf": + sb_actions.append("Switch to parent frame") + elif action[0] == "s_c_f": + method = "Set content to frame" + if '"' not in action[1]: + sb_actions.append('%s "%s"' % (method, action[1])) + else: + sb_actions.append("%s '%s'" % (method, action[1])) + elif action[0] == "s_c_d": + nested = action[1] + if nested: + sb_actions.append("Set content to parent") + else: + sb_actions.append("Set content to default") + elif action[0] == "sleep": + sb_actions.append("Sleep for %s seconds" % action[1]) + elif action[0] == "wf_el": + method = "Wait for element" + if '"' not in action[1]: + sb_actions.append('%s "%s"' % (method, action[1])) + elif "'" not in action[1]: + sb_actions.append("%s '%s'" % (method, action[1])) + else: + sb_actions.append( + "%s '%s'" % (method, action[1].replace("'", "\\'")) + ) + elif action[0] == "as_el": + method = "Assert element" + if '"' not in action[1]: + sb_actions.append('%s "%s"' % (method, action[1])) + elif "'" not in action[1]: + sb_actions.append("%s '%s'" % (method, action[1])) + else: + sb_actions.append( + "%s '%s'" % (method, action[1].replace("'", "\\'")) + ) + elif action[0] == "as_ep": + method = "Assert element present" + if '"' not in action[1]: + sb_actions.append('%s "%s"' % (method, action[1])) + elif "'" not in action[1]: + sb_actions.append("%s '%s'" % (method, action[1])) + else: + sb_actions.append( + "%s '%s'" % (method, action[1].replace("'", "\\'")) + ) + elif action[0] == "asenv": + method = "Assert element not visible" + if '"' not in action[1]: + sb_actions.append('%s "%s"' % (method, action[1])) + elif "'" not in action[1]: + sb_actions.append("%s '%s'" % (method, action[1])) + else: + sb_actions.append( + "%s '%s'" % (method, action[1].replace("'", "\\'")) + ) + elif action[0] == "s_at_" or action[0] == "s_ats": + start = "Find" + if action[0] == "s_ats": + start = "Find all" + if '"' not in action[1][0]: + sb_actions.append( + '%s "%s" and set %s to "%s"' + % (start, action[1][0], action[1][1], action[1][2]) + ) + elif "'" not in action[1][0]: + sb_actions.append( + "%s '%s' and set %s to \"%s\"" + % (start, action[1][0], action[1][1], action[1][2]) + ) + else: + sb_actions.append( + '%s "%s" and set %s to "%s")' + % ( + start.replace('"', '\\"'), + action[1][0], action[1][1], action[1][2] + ) + ) + elif action[0] == "acc_a": + sb_actions.append("Accept alert") + elif action[0] == "dis_a": + sb_actions.append("Dismiss alert") + elif action[0] == "hi_li": + method = "Highlight" + if '"' not in action[1]: + sb_actions.append('%s "%s"' % (method, action[1])) + else: + sb_actions.append("%s '%s'" % (method, action[1])) + elif action[0] == "as_lt": + method = "Assert link text" + if '"' not in action[1]: + sb_actions.append('%s "%s"' % (method, action[1])) + else: + sb_actions.append("%s '%s'" % (method, action[1])) + elif action[0] == "as_ti": + method = "Assert title" + if '"' not in action[1]: + sb_actions.append('%s "%s"' % (method, action[1])) + else: + sb_actions.append("%s '%s'" % (method, action[1])) + elif action[0] == "as_tc": + method = "Assert title contains" + if '"' not in action[1]: + sb_actions.append('%s "%s"' % (method, action[1])) + else: + sb_actions.append("%s '%s'" % (method, action[1])) + elif action[0] == "a_url": + method = "Assert URL" + if '"' not in action[1]: + sb_actions.append('%s "%s"' % (method, action[1])) + else: + sb_actions.append("%s '%s'" % (method, action[1])) + elif action[0] == "a_u_c": + method = "Assert URL contains" + if '"' not in action[1]: + sb_actions.append('%s "%s"' % (method, action[1])) + else: + sb_actions.append("%s '%s'" % (method, action[1])) + elif action[0] == "as_df": + method = "Assert downloaded file" + if '"' not in action[1]: + sb_actions.append('%s "%s"' % (method, action[1])) + else: + sb_actions.append("%s '%s'" % (method, action[1])) + elif action[0] == "do_fi": + method = "Download file" + file_url = action[1][0] + dest = action[1][1] + if not dest: + sb_actions.append('%s "%s" to downloads' % (method, file_url)) + else: + sb_actions.append('%s "%s" to "%s"' % (method, file_url, dest)) + elif action[0] == "as_at": + if ('"' not in action[1][0]) and action[1][2]: + sb_actions.append( + 'In "%s" assert attribute/value "%s"/"%s"' + % (action[1][0], action[1][1], action[1][2]) + ) + elif ('"' not in action[1][0]) and not action[1][2]: + sb_actions.append( + 'In "%s" assert attribute "%s"' + % (action[1][0], action[1][1]) + ) + elif ('"' in action[1][0]) and action[1][2]: + sb_actions.append( + 'In \'%s\' assert attribute/value "%s"/"%s"' + % (action[1][0], action[1][1], action[1][2]) + ) + else: + sb_actions.append( + 'In \'%s\' assert attribute "%s"' + % (action[1][0], action[1][1]) + ) + elif ( + action[0] == "as_te" + or action[0] == "as_et" + or action[0] == "astnv" + or action[0] == "aetnv" + or action[0] == "da_te" + or action[0] == "da_et" + ): + import unicodedata + + action[1][0] = unicodedata.normalize("NFKC", action[1][0]) + action[1][0] = action[1][0].replace("\n", "\\n") + action[1][0] = action[1][0].replace("\u00B6", "") + method = "Assert text" + if action[0] == "as_et": + method = "Assert exact text" + elif action[0] == "astnv": + method = "Assert text not visible" + elif action[0] == "aetnv": + method = "Assert exact text not visible" + elif action[0] == "da_te": + method = "Deferred assert text" + elif action[0] == "da_et": + method = "Deferred assert exact text" + if action[1][1] != "html": + if '"' not in action[1][0] and '"' not in action[1][1]: + sb_actions.append( + '%s "%s" in "%s"' + % (method, action[1][0], action[1][1]) + ) + elif '"' not in action[1][0] and '"' in action[1][1]: + sb_actions.append( + '%s "%s" in \'%s\'' + % (method, action[1][0], action[1][1]) + ) + elif '"' in action[1] and '"' not in action[1][1]: + sb_actions.append( + '%s \'%s\' in "%s"' + % (method, action[1][0], action[1][1]) + ) + elif '"' in action[1] and '"' in action[1][1]: + sb_actions.append( + "%s '%s' in '%s'" + % (method, action[1][0], action[1][1]) + ) + else: + if '"' not in action[1][0]: + sb_actions.append('%s "%s"' % (method, action[1][0])) + else: + sb_actions.append("%s '%s'" % (method, action[1][0])) + elif action[0] == "asnet": + method = "Assert non-empty text in" + if '"' not in action[1]: + sb_actions.append('%s "%s"' % (method, action[1])) + elif "'" not in action[1]: + sb_actions.append("%s '%s'" % (method, action[1])) + else: + sb_actions.append( + "%s '%s'" % (method, action[1].replace("'", "\\'")) + ) + elif action[0] == "da_el": + method = "Deferred assert element" + if '"' not in action[1]: + sb_actions.append('%s "%s"' % (method, action[1])) + elif "'" not in action[1]: + sb_actions.append("%s '%s'" % (method, action[1])) + else: + sb_actions.append( + "%s '%s'" % (method, action[1].replace("'", "\\'")) + ) + elif action[0] == "da_ep": + method = "Deferred assert element present" + if '"' not in action[1]: + sb_actions.append('%s "%s"' % (method, action[1])) + elif "'" not in action[1]: + sb_actions.append("%s '%s'" % (method, action[1])) + else: + sb_actions.append( + "%s '%s'" % (method, action[1].replace("'", "\\'")) + ) + elif action[0] == "danet": + method = "Deferred assert non-empty text in" + if '"' not in action[1]: + sb_actions.append('%s "%s"' % (method, action[1])) + elif "'" not in action[1]: + sb_actions.append("%s '%s'" % (method, action[1])) + else: + sb_actions.append( + "%s '%s'" % (method, action[1].replace("'", "\\'")) + ) + elif action[0] == "s_scr": + method = "Save screenshot as" + if '"' not in action[1]: + sb_actions.append('%s "%s"' % (method, action[1])) + else: + sb_actions.append("%s '%s'" % (method, action[1])) + elif action[0] == "ss_tf": + if '"' not in action[2] and '"' not in action[1]: + sb_actions.append( + 'Save screenshot to "%s" as "%s"' + % (action[2], action[1]) + ) + elif '"' not in action[2] and '"' in action[1]: + sb_actions.append( + 'Save screenshot to "%s" as \'%s\'' + % (action[2], action[1]) + ) + elif '"' in action[2] and '"' not in action[1]: + sb_actions.append( + 'Save screenshot to \'%s\' as "%s"' + % (action[2], action[1]) + ) + elif '"' in action[2] and '"' in action[1]: + sb_actions.append( + "Save screenshot to '%s' as '%s'" + % (action[2], action[1]) + ) + elif action[0] == "ss_tl": + sb_actions.append("Save screenshot to logs") + elif action[0] == "pdftl": + sb_actions.append("Save as PDF to logs") + elif action[0] == "spstl": + sb_actions.append("Save page source to logs") + elif action[0] == "sh_fc": + sb_actions.append("Show file choosers") + elif action[0] == "pr_da": + sb_actions.append("Process deferred asserts") + elif action[0] == "a_d_m": + sb_actions.append("Activate Demo Mode") + elif action[0] == "d_d_m": + sb_actions.append("Deactivate Demo Mode") + elif action[0] == "c_l_s": + sb_actions.append("Clear Local Storage") + elif action[0] == "c_s_s": + sb_actions.append("Clear Session Storage") + elif action[0] == "d_a_c": + sb_actions.append("Delete all cookies") + elif action[0] == "go_bk": + sb_actions.append("Go back") + elif action[0] == "go_fw": + sb_actions.append("Go forward") + elif action[0] == "c_box": + method = "Check if unchecked" + if action[2] == "no": + method = "Uncheck if checked" + if '"' not in action[1]: + sb_actions.append('%s "%s"' % (method, action[1])) + else: + sb_actions.append("%s '%s'" % (method, action[1])) + return sb_actions diff --git a/seleniumbase/behave/behave_sb.py b/seleniumbase/behave/behave_sb.py new file mode 100644 index 00000000000..c58f654ac33 --- /dev/null +++ b/seleniumbase/behave/behave_sb.py @@ -0,0 +1,1455 @@ +""" +The SeleniumBase-Behave Connector configures command-line options. +****************************************************************** +Examples: +behave -D browser=edge -D dashboard -D headless +behave -D rs -D dashboard +behave -D agent="User Agent String" -D demo +**************************************************************** +-D browser=BROWSER (The web browser to use. Default: "chrome".) +-D chrome (Shortcut for "-D browser=chrome". Default.) +-D edge (Shortcut for "-D browser=edge".) +-D firefox (Shortcut for "-D browser=firefox".) +-D safari (Shortcut for "-D browser=safari".) +-D cft (Shortcut for using `Chrome for Testing`) +-D chs (Shortcut for using `Chrome-Headless-Shell`) +-D settings-file=FILE (Override default SeleniumBase settings.) +-D env=ENV (Set the test env. Access with "self.env" in tests.) +-D account=STR (Set account. Access with "self.account" in tests.) +-D data=STRING (Extra test data. Access with "self.data" in tests.) +-D var1=STRING (Extra test data. Access with "self.var1" in tests.) +-D var2=STRING (Extra test data. Access with "self.var2" in tests.) +-D var3=STRING (Extra test data. Access with "self.var3" in tests.) +-D variables=DICT (A test var dict. Access with "self.variables".) +-D user-data-dir=DIR (Set the Chrome user data directory to use.) +-D protocol=PROTOCOL (The Selenium Grid protocol: http|https.) +-D server=SERVER (The Selenium Grid server/IP used for tests.) +-D port=PORT (The Selenium Grid port used by the test server.) +-D cap-file=FILE (The web browser's desired capabilities to use.) +-D cap-string=STRING (The web browser's desired capabilities to use.) +-D proxy=SERVER:PORT (Connect to a proxy server:port as tests are running) +-D proxy=USERNAME:PASSWORD@SERVER:PORT (Use an authenticated proxy server) +-D proxy-bypass-list=STRING (";"-separated hosts to bypass, Eg "*.foo.com") +-D proxy-pac-url=URL (Connect to a proxy server using a PAC_URL.pac file.) +-D proxy-pac-url=USERNAME:PASSWORD@URL (Authenticated proxy with PAC URL.) +-D multi-proxy (Allow multiple authenticated proxies when multi-threaded.) +-D agent=STRING (Modify the web browser's User-Agent string.) +-D mobile (Use the mobile device emulator while running tests.) +-D metrics=STRING (Set mobile metrics: "CSSWidth,CSSHeight,PixelRatio".) +-D chromium-arg=ARG (Add a Chromium arg for Chrome/Edge, comma-separated.) +-D firefox-arg=ARG (Add a Firefox arg for Firefox, comma-separated.) +-D firefox-pref=SET (Set a Firefox preference:value set, comma-separated.) +-D extension-zip=ZIP (Load a Chrome Extension .zip|.crx, comma-separated.) +-D extension-dir=DIR (Load a Chrome Extension directory, comma-separated.) +-D binary-location=PATH (Set path of the Chromium browser binary to use.) +-D driver-version=VER (Set the chromedriver or uc_driver version to use.) +-D sjw (Skip JS Waits for readyState to be "complete" or Angular to load.) +-D wfa (Wait for AngularJS to be done loading after specific web actions.) +-D pls=PLS (Set pageLoadStrategy on Chrome: "normal", "eager", or "none".) +-D headless (The default headless mode. Linux uses this mode by default.) +-D headless1 (Use Chrome's old headless mode. Fast, but has limitations.) +-D headless2 (Use Chrome's new headless mode, which supports extensions.) +-D headed (Run tests in headed/GUI mode on Linux OS, where not default.) +-D xvfb (Run tests using the Xvfb virtual display server on Linux OS.) +-D xvfb-metrics=STRING (Set Xvfb display size on Linux: "Width,Height".) +-D locale=LOCALE_CODE (Set the Language Locale Code for the web browser.) +-D pdb (Activate Post Mortem Debug Mode if a test fails.) +-D interval=SECONDS (The autoplay interval for presentations & tour steps) +-D start-page=URL (The starting URL for the web browser when tests begin.) +-D archive-logs (Archive existing log files instead of deleting them.) +-D archive-downloads (Archive old downloads instead of deleting them.) +-D time-limit=SECONDS (Safely fail any test that exceeds the time limit.) +-D slow (Slow down the automation. Faster than using Demo Mode.) +-D demo (Slow down and visually see test actions as they occur.) +-D demo-sleep=SECONDS (Set the wait time after Slow & Demo Mode actions.) +-D highlights=NUM (Number of highlight animations for Demo Mode actions.) +-D message-duration=SECONDS (The time length for Messenger alerts.) +-D check-js (Check for JavaScript errors after page loads.) +-D ad-block (Block some types of display ads from loading.) +-D block-images (Block images from loading during tests.) +-D do-not-track (Indicate to websites that you don't want to be tracked.) +-D verify-delay=SECONDS (The delay before MasterQA verification checks.) +-D recorder (Enables the Recorder for turning browser actions into code.) +-D rec-behave (Same as Recorder Mode, but also generates behave-gherkin.) +-D rec-sleep (If the Recorder is enabled, also records self.sleep calls.) +-D rec-print (If the Recorder is enabled, prints output after tests end.) +-D disable-cookies (Disable Cookies on websites. Pages might break!) +-D disable-js (Disable JavaScript on websites. Pages might break!) +-D disable-csp (Disable the Content Security Policy of websites.) +-D disable-ws (Disable Web Security on Chromium-based browsers.) +-D enable-ws (Enable Web Security on Chromium-based browsers.) +-D enable-sync (Enable "Chrome Sync".) +-D uc | -D undetected (Use undetected-chromedriver to evade bot-detection) +-D uc-cdp-events (Capture CDP events when running in "-D undetected" mode) +-D log-cdp ("goog:loggingPrefs", {"performance": "ALL", "browser": "ALL"}) +-D remote-debug (Sync to Chrome Remote Debugger chrome://inspect/#devices) +-D dashboard (Enable the SeleniumBase Dashboard. Saved at: dashboard.html) +-D dash-title=STRING (Set the title shown for the generated dashboard.) +-D enable-3d-apis (Enables WebGL and 3D APIs.) +-D swiftshader (Use Chrome's SwiftShader Graphics Library.) +-D incognito (Enable Chrome's Incognito mode.) +-D guest (Enable Chrome's Guest mode.) +-D dark (Enable Chrome's Dark mode.) +-D devtools (Open Chrome's DevTools when the browser opens.) +-D rs | -D reuse-session (Reuse browser session for all tests.) +-D rcs | -D reuse-class-session (Reuse session for tests in class/feature) +-D crumbs (Delete all cookies between tests reusing a session.) +-D disable-beforeunload (Disable the "beforeunload" event on Chrome.) +-D window-position=X,Y (Set the browser's starting window position.) +-D window-size=WIDTH,HEIGHT (Set the browser's starting window size.) +-D maximize (Start tests with the browser window maximized.) +-D screenshot (Save a screenshot at the end of each test.) +-D no-screenshot (No screenshots saved unless tests directly ask it.) +-D visual-baseline (Set the visual baseline for Visual/Layout tests.) +-D external-pdf (Set Chromium "plugins.always_open_pdf_externally":True.) +-D timeout-multiplier=MULTIPLIER (Multiplies the default timeout values.) +""" +import ast +import colorama +import os +import re +import sys +from contextlib import suppress +from seleniumbase import config as sb_config +from seleniumbase.config import settings +from seleniumbase.core import detect_b_ver +from seleniumbase.core import download_helper +from seleniumbase.core import log_helper +from seleniumbase.core import proxy_helper +from seleniumbase.core import session_helper +from seleniumbase.fixtures import constants +from seleniumbase.fixtures import shared_utils + +is_linux = shared_utils.is_linux() +is_windows = shared_utils.is_windows() +sb_config.__base_class = None + + +def set_base_class(base_class): + """ + # You can override the base class from Behave's environment.py file. + # If not using SeleniumBase's BaseCase class, then use a subclass. + from seleniumbase import BaseCase + from seleniumbase.plugins import behave_plugin + behave_plugin.set_base_class(BaseCase) + """ + sb_config.__base_class = base_class + + +def get_configured_sb(context): + if not sb_config.__base_class: + from seleniumbase import BaseCase + + sb_config.__base_class = BaseCase + sb_config.__base_class.test_method = {} + sb = sb_config.__base_class("test_method") + + # Set default values + sb.browser = "chrome" + sb.is_behave = True + sb.headless = False + sb.headless1 = False + sb.headless2 = False + sb.headless_active = False + sb.headed = False + sb.xvfb = False + sb.xvfb_metrics = None + sb.start_page = None + sb.locale_code = None + sb.pdb_option = False + sb.protocol = "http" + sb.servername = "localhost" + sb.port = 4444 + sb.data = None + sb.var1 = None + sb.var2 = None + sb.var3 = None + sb.variables = {} + sb.account = None + sb.environment = "test" + sb.env = "test" + sb.user_agent = None + sb.incognito = False + sb.guest_mode = False + sb.dark_mode = False + sb.devtools = False + sb.mobile_emulator = False + sb.device_metrics = None + sb.extension_zip = None + sb.extension_dir = None + sb.binary_location = None + sb_config.binary_location = None + sb.driver_version = None + sb.page_load_strategy = None + sb.database_env = "test" + sb.log_path = constants.Logs.LATEST + os.sep + sb.archive_logs = False + sb.disable_cookies = False + sb.disable_js = False + sb.disable_csp = False + sb.disable_ws = False + sb.enable_ws = False + sb.enable_sync = False + sb.use_auto_ext = False + sb.undetectable = False + sb.uc_cdp_events = False + sb.uc_subprocess = False + sb.log_cdp_events = False + sb.no_sandbox = False + sb.disable_gpu = False + sb._multithreaded = False + sb._reuse_session = False + sb._reuse_class_session = False + sb._crumbs = False + sb._disable_beforeunload = False + sb.visual_baseline = False + sb.window_position = None + sb.window_size = None + sb.maximize_option = False + sb.is_context_manager = False + sb.save_screenshot_after_test = False + sb.no_screenshot_after_test = False + sb.timeout_multiplier = None + sb.pytest_html_report = None + sb.with_db_reporting = False + sb.with_s3_logging = False + sb.js_checking_on = False + sb.recorder_mode = False + sb.recorder_ext = False + sb.record_sleep = False + sb.rec_behave = False + sb.rec_print = False + sb.report_on = False + sb.is_pytest = False + sb.slow_mode = False + sb.demo_mode = False + sb.time_limit = None + sb.demo_sleep = None + sb.dashboard = False + sb.dash_title = None + sb._dash_initialized = False + sb.message_duration = None + sb.block_images = False + sb.do_not_track = False + sb.external_pdf = False + sb.remote_debug = False + sb.settings_file = None + sb.user_data_dir = None + sb.chromium_arg = None + sb.firefox_arg = None + sb.firefox_pref = None + sb.disable_features = None + sb.proxy_string = None + sb.proxy_bypass_list = None + sb.proxy_pac_url = None + sb.multi_proxy = False + sb.host_resolver_rules = None + sb.enable_3d_apis = False + sb.swiftshader = False + sb.ad_block_on = False + sb.is_nosetest = False + sb.highlights = None + sb.interval = None + sb.cap_file = None + sb.cap_string = None + + # Set a few sb_config vars early in case parsing args fails + sb_config.dashboard = None + sb_config._has_logs = None + sb_config._has_exception = None + sb_config.save_screenshot = None + sb_config.reuse_class_session = None + + browsers = set() # To error if selecting more than one + valid_browsers = constants.ValidBrowsers.valid_browsers + valid_envs = constants.ValidEnvs.valid_envs + # Process command-line options + userdata = context.config.userdata + for key in userdata.keys(): + # Convert --ARG to ARG, etc. + if key.startswith("--"): + key = key[2:] + if key.startswith("-"): + key = key[1:] + low_key = key.lower() + # Handle: -D browser=BROWSER + if low_key == "browser": + browser = userdata[key].lower() + if browser in valid_browsers: + sb.browser = browser + browsers.add(browser) + elif browser == "true": + raise Exception( + '\nThe "browser" argument requires a value!' + "\nChoose from %s." + '\nEg. -D browser="edge"' % valid_browsers + ) + else: + raise Exception( + '\n"%s" is not a valid "browser" selection!' + "\nChoose from %s." + '\nEg. -D browser="edge"' % (browser, valid_browsers) + ) + continue + # Handle: -D BROWSER + if low_key in valid_browsers: + browser = low_key + sb.browser = browser + browsers.add(browser) + continue + # Handle: -D headless + if low_key == "headless": + sb.headless = True + continue + # Handle: -D headless2 + if low_key == "headless1": + sb.headless1 = True + sb.headless = True + continue + # Handle: -D headless2 + if low_key == "headless2": + sb.headless2 = True + continue + # Handle: -D headed / gui + if low_key in ["headed", "gui"]: + sb.headed = True + continue + # Handle: -D xvfb + if low_key == "xvfb": + sb.xvfb = True + continue + # Handle: -D xvfb-metrics=STR / xvfb_metrics=STR + if low_key in ["xvfb-metrics", "xvfb_metrics"]: + xvfb_metrics = userdata[key] + if xvfb_metrics == "true": + xvfb_metrics = sb.xvfb_metrics # revert to default + sb.xvfb_metrics = xvfb_metrics + continue + # Handle: -D start-page=URL / start_page=URL / url=URL + if low_key in ["start-page", "start_page", "url"]: + start_page = userdata[key] + if start_page == "true": + start_page = sb.start_page # revert to default + sb.start_page = start_page + continue + # Handle: -D locale-code=CODE / locale_code=CODE / locale=CODE + if low_key in ["locale-code", "locale_code", "locale"]: + sb.start_page = userdata[key] + continue + # Handle: -D pdb / ipdb + if low_key in ["pdb", "ipdb"]: + sb.pdb_option = True + continue + # Handle: -D protocol=PROTOCOL + if low_key == "protocol": + protocol = userdata[key].lower() + if protocol in ["http", "https"]: + sb.protocol = protocol + elif protocol == "true": + raise Exception( + '\nThe Selenium Grid "protocol" argument requires a value!' + '\nChoose from ["http", "https"]' + '\nEg. -D protocol="https"' + ) + else: + raise Exception( + '\n"%s" is not a valid Selenium Grid "protocol" selection!' + '\nChoose from ["http", "https"]' + '\nEg. -D protocol="https"' % protocol + ) + continue + # Handle: -D server=SERVERNAME / servername=SERVERNAME + if low_key in ["server", "servername"]: + servername = userdata[key] + if servername == "true": + servername = sb.servername # revert to default + sb.servername = servername + continue + # Handle: -D port=PORT + if low_key == "port": + port = int(userdata[key]) + if port == "true": + port = sb.port # revert to default + sb.port = port + continue + # Handle: -D data=DATA + if low_key == "data": + sb.data = userdata[key] + continue + # Handle: -D var1=DATA + if low_key == "var1": + sb.var1 = userdata[key] + continue + # Handle: -D var2=DATA + if low_key == "var2": + sb.var2 = userdata[key] + continue + # Handle: -D var3=DATA + if low_key == "var3": + sb.var3 = userdata[key] + continue + # Handle: -D variables="{'KEY':'VALUE','KEY2':'VALUE2'}" + if low_key == "variables": + variables = userdata[key] + if variables and isinstance(variables, str) and len(variables) > 0: + bad_input = False + if ( + not variables.startswith("{") + or not variables.endswith("}") + ): + bad_input = True + else: + try: + variables = ast.literal_eval(variables) + if not isinstance(variables, dict): + bad_input = True + except Exception: + bad_input = True + if bad_input: + raise Exception( + '\nExpecting a Python dictionary for "variables"!' + "\nEg. -D variables=\"{'KEY':'VALUE', 'KEY2':123}\"" + ) + else: + variables = {} + continue + # Handle: -D account=ACCOUNT + if low_key == "account": + sb.account = userdata[key] + continue + # Handle: -D env=ENVIRONMENT + if low_key == "environment": + environment = userdata[key].lower() + if environment in valid_envs: + sb.environment = environment + sb.env = environment + elif environment == "true": + raise Exception( + '\nThe "env" argument requires a value!' + "\nChoose from %s." + '\nEg. -D env="production"' % valid_envs + ) + else: + raise Exception( + '\n"%s" is not a valid "env" selection!' + "\nChoose from %s." + '\nEg. -D env="production"' % (environment, valid_envs) + ) + continue + # Handle: -D user-agent=STRING / user_agent=STRING / agent=STRING + if low_key in ["user-agent", "user_agent", "agent"]: + user_agent = userdata[key] + if user_agent == "true": + user_agent = sb.user_agent # revert to default + sb.user_agent = user_agent + continue + # Handle: -D incognito / incognito-mode / incognito_mode + if low_key in ["incognito", "incognito-mode", "incognito_mode"]: + sb.incognito = True + continue + # Handle: -D guest / guest-mode / guest_mode + if low_key in ["guest", "guest-mode", "guest_mode"]: + sb.guest_mode = True + continue + # Handle: -D dark / dark-mode / dark_mode + if low_key in ["dark", "dark-mode", "dark_mode"]: + sb.dark_mode = True + continue + # Handle: -D devtools / open-devtools / open_devtools + if low_key in ["devtools", "open-devtools", "open_devtools"]: + sb.devtools = True + continue + # Handle: -D mobile / mobile-emulator / mobile_emulator + if low_key in ["mobile", "mobile-emulator", "mobile_emulator"]: + sb.mobile_emulator = True + continue + # Handle: -D metrics=STR / device-metrics=STR / device_metrics=STR + if low_key in ["metrics", "device-metrics", "device_metrics"]: + device_metrics = userdata[key] + if device_metrics == "true": + device_metrics = sb.device_metrics # revert to default + sb.device_metrics = device_metrics + continue + # Handle: -D crx=ZIP / extension-zip=ZIP / extension_zip=ZIP + if low_key in ["crx", "extension-zip", "extension_zip"]: + extension_zip = userdata[key] + if extension_zip == "true": + extension_zip = sb.extension_zip # revert to default + sb.extension_zip = extension_zip + continue + # Handle: -D extension-dir=DIR / extension_dir=DIR + if low_key in ["extension-dir", "extension_dir"]: + extension_dir = userdata[key] + if extension_dir == "true": + extension_dir = sb.extension_dir # revert to default + sb.extension_dir = extension_dir + continue + # Handle: -D binary-location=PATH / binary_location=PATH / bl=PATH + if low_key in ["binary-location", "binary_location", "bl"]: + binary_location = userdata[key] + if binary_location == "true": + binary_location = sb.binary_location # revert to default + sb.binary_location = binary_location + sb_config.binary_location = binary_location + continue + # Handle: -D use-chromium / chromium + if ( + low_key in ["use-chromium", "chromium"] + and not sb_config.binary_location + ): + binary_location = "_chromium_" + sb.binary_location = binary_location + sb_config.binary_location = binary_location + continue + # Handle: -D cft + if low_key in ["cft"] and not sb_config.binary_location: + binary_location = "cft" + sb.binary_location = binary_location + sb_config.binary_location = binary_location + continue + # Handle: -D chs + if low_key in ["chs"] and not sb_config.binary_location: + binary_location = "chs" + sb.binary_location = binary_location + sb_config.binary_location = binary_location + continue + # Handle: -D driver-version=VER / driver_version=VER + if low_key in ["driver-version", "driver_version"]: + driver_version = userdata[key] + if driver_version == "true": + driver_version = sb.driver_version # revert to default + sb.driver_version = driver_version + continue + # Handle: -D pls=PLS / page-load-strategy=PLS / page_load_strategy=PLS + if low_key in ["pls", "page-load-strategy", "page_load_strategy"]: + page_load_strategy = userdata[key].lower() + if page_load_strategy in ["normal", "eager", "none"]: + sb.page_load_strategy = page_load_strategy + elif page_load_strategy == "true": + raise Exception( + '\nThe "pls" / "page-load-strategy" arg requires a value!' + '\nChoose from ["normal", "eager", "none"]' + '\nEg. -D pls="none"' + ) + else: + raise Exception( + '\n"%s" is not a valid "pls" / "page-load-strategy" value!' + '\nChoose from ["normal", "eager", "none"]' + '\nEg. -D pls="none"' % page_load_strategy + ) + continue + # Handle: -D database-env=ENVIRONMENT / database_env=ENVIRONMENT + if low_key in ["database-env", "database_env"]: + database_env = userdata[key].lower() + if database_env in valid_envs: + sb.database_env = database_env + elif database_env == "true": + raise Exception( + '\nThe "database-env" argument requires a value!' + "\nChoose from %s." + '\nEg. -D database-env="production"' % valid_envs + ) + else: + raise Exception( + '\n"%s" is not a valid "database-env" selection!' + "\nChoose from %s." + '\nEg. -D database-env="production"' + % (environment, valid_envs) + ) + continue + # Handle: -D archive-logs / archive_logs + if low_key in ["archive-logs", "archive_logs"]: + sb.archive_logs = True + continue + # Handle: -D disable-cookies / disable_cookies + if low_key in ["disable-cookies", "disable_cookies"]: + sb.disable_cookies = True + continue + # Handle: -D disable-js / disable_js + if low_key in ["disable-js", "disable_js"]: + sb.disable_js = True + continue + # Handle: -D disable-csp / disable_csp / dcsp + if low_key in ["disable-csp", "disable_csp", "dcsp"]: + sb.disable_csp = True + continue + # Handle: -D disable-ws / disable_ws / dws + if low_key in ["disable-ws", "disable_ws", "dws"]: + sb.disable_ws = True + continue + # Handle: -D enable-ws / enable_ws + if low_key in ["enable-ws", "enable_ws"]: + sb.enable_ws = True + continue + # Handle: -D enable-sync / enable_sync + if low_key in ["enable-sync", "enable_sync"]: + sb.enable_sync = True + continue + # Handle: -D use-auto-ext / use_auto_ext / auto-ext + if low_key in ["use-auto-ext", "use_auto_ext", "auto-ext"]: + sb.use_auto_ext = True + continue + # Handle: -D undetected / undetectable / uc + if low_key in ["undetected", "undetectable", "uc"]: + sb.undetectable = True + continue + # Handle: -D uc-cdp-events / uc_cdp_events / uc-cdp + if low_key in ["uc-cdp-events", "uc_cdp_events", "uc-cdp"]: + sb.uc_cdp_events = True + sb.undetectable = True + continue + # Handle: -D uc-subprocess / uc_subprocess / uc-sub + if low_key in ["uc-subprocess", "uc_subprocess", "uc-sub"]: + sb.uc_subprocess = True + sb.undetectable = True + continue + # Handle: -D log-cdp-events / log_cdp_events / log-cdp + if low_key in ["log-cdp-events", "log_cdp_events", "log-cdp"]: + sb.log_cdp_events = True + continue + # Handle: -D no-sandbox / no_sandbox + if low_key in ["no-sandbox", "no_sandbox"]: + sb.no_sandbox = True + continue + # Handle: -D disable-gpu / disable_gpu + if low_key in ["disable-gpu", "disable_gpu"]: + sb.disable_gpu = True + continue + # Handle: -D rs / reuse-session / reuse_session + if low_key in ["rs", "reuse-session", "reuse_session"]: + sb._reuse_session = True + continue + # Handle: -D rcs / rfs / reuse-class-session / reuse-feature-session + if low_key in [ + "rcs", "rfs", "reuse-class-session", "reuse-feature-session" + ]: + sb._reuse_session = True + sb._reuse_class_session = True + continue + # Handle: -D crumbs + if low_key == "crumbs": + sb._crumbs = True + continue + # Handle: -D disable-beforeunload / disable_beforeunload + if low_key in ["disable-beforeunload", "disable_beforeunload"]: + sb._disable_beforeunload = True + continue + # Handle: -D sjw / skip-js-waits / skip_js_waits + if low_key in ["sjw", "skip-js-waits", "skip_js_waits"]: + settings.SKIP_JS_WAITS = True + continue + # Handle: -D wfa / wait-for-angularjs / wait_for_angularjs + if low_key in ["wfa", "wait-for-angularjs", "wait_for_angularjs"]: + settings.WAIT_FOR_ANGULARJS = True + continue + # Handle: -D visual-baseline / visual_baseline + if low_key in ["visual-baseline", "visual_baseline"]: + sb.visual_baseline = True + continue + # Handle: -D window-position=X,Y / window_position=X,Y + if low_key in ["window-position", "window_position"]: + window_position = userdata[key] + if window_position == "true": + window_position = sb.window_position # revert to default + sb.window_position = window_position + continue + # Handle: -D window-size=Width,Height / window_size=Width,Height + if low_key in ["window-size", "window_size"]: + window_size = userdata[key] + if window_size == "true": + window_size = sb.window_size # revert to default + sb.window_size = window_size + continue + # Handle: -D maximize / fullscreen / maximize-window + if low_key in [ + "maximize", "fullscreen", "maximize-window", "maximize_window" + ]: + sb.maximize_option = True + continue + # Handle: -D screenshot / save-screenshot / save_screenshot / ss + if low_key in [ + "screenshot", "save-screenshot", "save_screenshot", "ss" + ]: + sb.save_screenshot_after_test = True + continue + # Handle: -D no-screenshot / no_screenshot / ns + if low_key in ["no-screenshot", "no_screenshot", "ns"]: + sb.no_screenshot_after_test = True + continue + # Handle: -D timeout-multiplier=FLOAT / timeout_multiplier=FLOAT + if low_key in ["timeout-multiplier", "timeout_multiplier"]: + timeout_multiplier = userdata[key] + if timeout_multiplier == "true": + timeout_multiplier = sb.timeout_multiplier # revert to default + sb.timeout_multiplier = timeout_multiplier + continue + # Handle: -D with-db-reporting / with-db_reporting + if low_key in ["with-db-reporting", "with-db_reporting"]: + sb.with_db_reporting = True + continue + # Handle: -D with-s3-logging / with-s3_logging + if low_key in ["with-s3-logging", "with-s3_logging"]: + sb.with_s3_logging = True + continue + # Handle: -D check-js / check_js + if low_key in ["check-js", "check_js"]: + sb.js_checking_on = True + continue + # Handle: -D recorder / record / rec / codegen + if low_key in ["recorder", "record", "rec", "codegen"]: + sb.recorder_mode = True + sb.recorder_ext = True + continue + # Handle: -D rec-behave / rec-gherkin + if low_key in ["rec-behave", "rec-gherkin"]: + sb.rec_behave = True + sb.recorder_mode = True + sb.recorder_ext = True + continue + # Handle: -D record-sleep / record_sleep / rec-sleep / rec_sleep + if low_key in ["record-sleep", "rec-sleep"]: + sb.record_sleep = True + sb.recorder_mode = True + sb.recorder_ext = True + continue + # Handle: -D rec-print + if low_key in ["rec-print"]: + sb.rec_print = True + sb.recorder_mode = True + sb.recorder_ext = True + continue + # Handle: -D slow / slowmo / slow-mode / slow_mode + if low_key in ["slow", "slowmo", "slow-mode", "slow_mode"]: + sb.slow_mode = True + continue + # Handle: -D demo / demo-mode / demo_mode + if low_key in ["demo", "demo-mode", "demo_mode"]: + sb.demo_mode = True + continue + # Handle: -D time-limit / time_limit / timelimit + if low_key in ["time-limit", "time_limit", "timelimit"]: + time_limit = userdata[key] + if time_limit == "true": + time_limit = sb.time_limit # revert to default + sb.time_limit = time_limit + continue + # Handle: -D demo-sleep / demo_sleep + if low_key in ["demo-sleep", "demo_sleep"]: + demo_sleep = userdata[key] + if demo_sleep == "true": + demo_sleep = sb.demo_sleep # revert to default + sb.demo_sleep = demo_sleep + continue + # Handle: -D dashboard + if low_key == "dashboard": + sb.dashboard = True + continue + # Handle: -D dash-title=TITLE / dash_title=TITLE + if low_key in ["dash-title", "dash_title"]: + sb.dash_title = userdata[key] + continue + # Handle: -D message-duration / message_duration + if low_key in ["message-duration", "message_duration"]: + message_duration = userdata[key] + if message_duration == "true": + message_duration = sb.message_duration # revert to default + sb.message_duration = message_duration + continue + # Handle: -D block-images / block_images + if low_key in ["block-images", "block_images"]: + sb.block_images = True + continue + # Handle: -D do-not-track / do_not_track + if low_key in ["do-not-track", "do_not_track"]: + sb.do_not_track = True + continue + # Handle: -D external-pdf / external_pdf + if low_key in ["external-pdf", "external_pdf"]: + sb.external_pdf = True + continue + # Handle: -D remote-debug / remote_debug / remote-debugger + if low_key in ["remote-debug", "remote_debug", "remote-debugger"]: + sb.remote_debug = True + continue + # Handle: -D settings=FILE / settings-file=FILE / settings_file=FILE + if low_key in ["settings", "settings-file", "settings_file"]: + settings_file = userdata[key] + if settings_file == "true": + settings_file = sb.settings_file # revert to default + sb.settings_file = settings_file + continue + # Handle: -D user-data-dir=DIR / user_data_dir=DIR + if low_key in ["user-data-dir", "user_data_dir"]: + user_data_dir = userdata[key] + if user_data_dir == "true": + user_data_dir = sb.user_data_dir # revert to default + sb.user_data_dir = user_data_dir + continue + # Handle: -D chromium-arg="ARG=N,ARG2" / chromium_arg="ARG=N,ARG2" + if low_key in ["chromium-arg", "chromium_arg"]: + chromium_arg = userdata[key] + if chromium_arg == "true": + chromium_arg = sb.chromium_arg # revert to default + sb.chromium_arg = chromium_arg + continue + # Handle: -D firefox-arg="ARG=N,ARG2" / firefox_arg="ARG=N,ARG2" + if low_key in ["firefox-arg", "firefox_arg"]: + firefox_arg = userdata[key] + if firefox_arg == "true": + firefox_arg = sb.firefox_arg # revert to default + sb.firefox_arg = firefox_arg + continue + # Handle: -D firefox-pref="PREF:VAL" / firefox_pref="PREF:VAL" + if low_key in ["firefox-pref", "firefox_pref"]: + firefox_pref = userdata[key] + if firefox_pref == "true": + firefox_pref = sb.firefox_pref # revert to default + sb.firefox_pref = firefox_pref + continue + # Handle: -D disable-features="F1,F2" / disable_features="F1,F2" + if low_key in ["disable-features", "disable_features"]: + disable_features = userdata[key] + if disable_features == "true": + disable_features = sb.disable_features # revert to default + sb.disable_features = disable_features + continue + # Handle: -D proxy=SERVER:PORT / proxy=USERNAME:PASSWORD@SERVER:PORT + if low_key in ["proxy", "proxy-server", "proxy-string"]: + proxy_string = userdata[key] + if proxy_string == "true": + proxy_string = sb.proxy_string # revert to default + sb.proxy_string = proxy_string + continue + # Handle: -D proxy-bypass-list="DOMAIN1;D2" / proxy_bypass_list="D1;D2" + if low_key in ["proxy-bypass-list", "proxy_bypass_list"]: + proxy_bypass_list = userdata[key] + if proxy_bypass_list == "true": + proxy_bypass_list = sb.proxy_bypass_list # revert to default + sb.proxy_bypass_list = proxy_bypass_list + continue + # Handle: -D proxy-pac-url=URL / proxy-pac-url=USERNAME:PASSWORD@URL + if low_key in ["proxy-pac-url", "pac-url"]: + proxy_pac_url = userdata[key] + if proxy_pac_url == "true": + proxy_pac_url = sb.proxy_pac_url # revert to default + sb.proxy_pac_url = proxy_pac_url + continue + # Handle: -D multi-proxy / multi_proxy + if low_key in ["multi-proxy", "multi_proxy"]: + sb.multi_proxy = True + continue + # Handle: -D host-resolver-rules=RULES / host_resolver_rules=RULES + if low_key in ["host-resolver-rules", "host_resolver_rules"]: + host_resolver_rules = userdata[key] + if host_resolver_rules == "true": + host_resolver_rules = sb.host_resolver_rules + sb.host_resolver_rules = host_resolver_rules + continue + # Handle: -D enable-3d-apis / enable_3d_apis + if low_key in ["enable-3d-apis", "enable_3d_apis"]: + sb.enable_3d_apis = True + continue + # Handle: -D swiftshader + if low_key == "swiftshader": + sb.swiftshader = True + continue + # Handle: -D adblock / ad-block / ad_block / block-ads / block_ads + if low_key in [ + "adblock", "ad-block", "ad_block", "block-ads", "block_ads" + ]: + sb.ad_block_on = True + continue + # Handle: -D highlights=NUM + if low_key == "highlights": + highlights = userdata[key] + if highlights == "true": + highlights = sb.highlights # revert to default + sb.highlights = highlights + continue + # Handle: -D interval=SECONDS + if low_key == "interval": + interval = userdata[key] + if interval == "true": + interval = sb.interval # revert to default + sb.interval = interval + continue + # Handle: -D cap-file=FILE / cap_file=FILE + if low_key in ["cap-file", "cap_file"]: + cap_file = userdata[key] + if cap_file == "true": + cap_file = sb.cap_file # revert to default + sb.cap_file = cap_file + continue + # Handle: -D cap-string=STRING / cap_string=STRING + if low_key == "cap_string": + cap_string = userdata[key] + if cap_string == "true": + cap_string = sb.cap_string # revert to default + sb.cap_string = cap_string + continue + + # Fail immediately if trying to set more than one default browser. + if len(browsers) > 1: + raise Exception( + "\nOnly ONE default browser is allowed!\n" + "%s browsers were selected: %s" % (len(browsers), browsers) + ) + if sb.browser in ["opera", "brave", "comet"]: + bin_loc = detect_b_ver.get_binary_location(sb.browser) + if bin_loc and os.path.exists(bin_loc): + sb_config._cdp_browser = sb.browser + sb_config._cdp_bin_loc = bin_loc + sb_config.binary_location = bin_loc + sb.binary_location = bin_loc + # Recorder Mode can still optimize scripts in "-D headless2" mode. + if sb.recorder_ext and sb.headless: + sb.headless = False + sb.headless1 = False + sb.headless2 = True + if sb.headless2 and sb.browser == "firefox": + sb.headless2 = False # Only for Chromium browsers + sb.headless = True # Firefox has regular headless + elif sb.browser not in ["chrome", "edge"]: + sb.headless2 = False # Only for Chromium browsers + if ( + sb.binary_location + and sb.binary_location.lower() == "chs" + and sb.browser == "chrome" + ): + sb.headless = True + sb.headless1 = False + sb.headless2 = False + # Recorder Mode only supports Chromium browsers. + if sb.recorder_ext and (sb.browser not in ["chrome", "edge"]): + raise Exception( + "\n\n Recorder Mode ONLY supports Chrome and Edge!" + '\n (Your browser choice was: "%s")\n' % sb.browser + ) + # The Xvfb virtual display server is for Linux OS Only. + if sb.xvfb and not is_linux: + sb.xvfb = False + if ( + is_linux + and not sb.headed + and not sb.headless + and not sb.headless2 + and not sb.xvfb + ): + if not sb.undetectable: + print( + '(Linux uses "-D headless" by default. ' + 'To override, use "-D headed" / "-D gui". ' + 'For Xvfb mode instead, use "-D xvfb". ' + "Or you can hide this info by using" + '"-D headless" / "-D headless2" / "-D uc".)' + ) + sb.headless = True + else: + sb.xvfb = True + # Recorder Mode can still optimize scripts in --headless2 mode. + if sb.recorder_mode and sb.headless: + sb.headless = False + sb.headless1 = False + sb.headless2 = True + if not sb.headless and not sb.headless2: + sb.headed = True + if sb.browser == "safari" and sb.headless: + sb.headless = False # Safari doesn't support headless mode + sb.headless1 = False + if sb.save_screenshot_after_test and sb.no_screenshot_after_test: + sb.save_screenshot_after_test = False # "no_screenshot" has priority + if sb.servername != "localhost": + # Using Selenium Grid + # (Set -D server="127.0.0.1" for localhost Grid) + # If the port is "443", the protocol is "https" + if str(sb.port) == "443": + sb.protocol = "https" + if ( + (sb.enable_ws is None and sb.disable_ws is None) + or (sb.disable_ws is not None and not sb.disable_ws) + or (sb.enable_ws is not None and sb.enable_ws) + ): + sb.enable_ws = True + sb.disable_ws = False + else: + sb.enable_ws = False + sb.disable_ws = True + if sb.window_position: + window_position = sb.window_position + if window_position.count(",") != 1: + message = ( + '\n\n window_position expects an "x,y" string!' + '\n (Your input was: "%s")\n' % window_position + ) + raise Exception(message) + window_position = window_position.replace(" ", "") + win_x = None + win_y = None + try: + win_x = int(window_position.split(",")[0]) + win_y = int(window_position.split(",")[1]) + except Exception: + message = ( + '\n\n Expecting integer values for "x,y"!' + '\n (window_position input was: "%s")\n' + % window_position + ) + raise Exception(message) + settings.WINDOW_START_X = win_x + settings.WINDOW_START_Y = win_y + if sb.window_size: + window_size = sb.window_size + if window_size.count(",") != 1: + message = ( + '\n\n window_size expects a "width,height" string!' + '\n (Your input was: "%s")\n' % window_size + ) + raise Exception(message) + window_size = window_size.replace(" ", "") + width = None + height = None + try: + width = int(window_size.split(",")[0]) + height = int(window_size.split(",")[1]) + except Exception: + message = ( + '\n\n Expecting integer values for "width,height"!' + '\n (window_size input was: "%s")\n' % window_size + ) + raise Exception(message) + settings.CHROME_START_WIDTH = width + settings.CHROME_START_HEIGHT = height + settings.HEADLESS_START_WIDTH = width + settings.HEADLESS_START_HEIGHT = height + + # Set sb_config + sb_config.browser = sb.browser + sb_config.headless = sb.headless + sb_config.headless_active = False + sb_config.headed = sb.headed + sb_config.is_behave = True + sb_config.is_pytest = False + sb_config.is_nosetest = False + sb_config.is_context_manager = False + sb_config.window_position = sb.window_position + sb_config.window_size = sb.window_size + sb_config.maximize_option = sb.maximize_option + sb_config.xvfb = sb.xvfb + sb_config.xvfb_metrics = sb.xvfb_metrics + sb_config.reuse_class_session = sb._reuse_class_session + sb_config.save_screenshot = sb.save_screenshot_after_test + sb_config.no_screenshot = sb.no_screenshot_after_test + sb_config._has_logs = False + sb_config.variables = sb.variables + sb_config.dashboard = sb.dashboard + sb_config.dash_title = sb.dash_title + sb_config.pdb_option = sb.pdb_option + sb_config.rec_behave = sb.rec_behave + sb_config.rec_print = sb.rec_print + sb_config.disable_cookies = sb.disable_cookies + sb_config.disable_js = sb.disable_js + sb_config.disable_csp = sb.disable_csp + sb_config.record_sleep = sb.record_sleep + sb_config._is_timeout_changed = False + sb_config._SMALL_TIMEOUT = settings.SMALL_TIMEOUT + sb_config._LARGE_TIMEOUT = settings.LARGE_TIMEOUT + sb_config._recorded_actions = {} + sb_config._behave_recorded_actions = {} + # Dashboard-specific variables + sb_config._results = {} # SBase Dashboard test results + sb_config._duration = {} # SBase Dashboard test duration + sb_config._display_id = {} # SBase Dashboard display ID + sb_config._d_t_log_path = {} # SBase Dashboard test log path + sb_config._dash_html = None # SBase Dashboard HTML copy + sb_config._test_id = None # SBase Dashboard test id + sb_config._latest_display_id = None # The latest SBase display id + sb_config._dashboard_initialized = False # Becomes True after init + sb_config._has_exception = False # This becomes True if any test fails + sb_config._multithreaded = False # This becomes True if multithreading + sb_config._only_unittest = True # If any test uses BaseCase, becomes False + sb_config._sbase_detected = False # Becomes True during SeleniumBase tests + sb_config._extra_dash_entries = [] # Dashboard entries for non-SBase tests + sb_config._using_html_report = False # Becomes True when using html report + sb_config._dash_is_html_report = False # Dashboard becomes the html report + sb_config._saved_dashboard_pie = None # Copy of pie chart for html report + sb_config._dash_final_summary = None # Dash status to add to html report + sb_config._html_report_name = None # The name of the pytest html report + + if sb_config.dash_title: + constants.Dashboard.TITLE = sb_config.dash_title.replace("_", " ") + + log_helper.log_folder_setup( + constants.Logs.LATEST + "/", sb.archive_logs + ) + download_helper.reset_downloads_folder() + proxy_helper.remove_proxy_zip_if_present() + return sb + + +def calculate_test_id(file_name, scenario_name): + file_name = file_name.replace("/", ".").replace("\\", ".") + scenario_name = re.sub(r"[^\w" + r"_ " + r"]", "", scenario_name) + scenario_name = scenario_name.replace(" ", "_") + if " -- @" in scenario_name: + scenario_name = scenario_name.split(" # ")[0].rstrip() + test_id = "%s.%s" % (file_name, scenario_name) + return test_id + + +def calculate_display_id(file_name, line_num, scenario_name): + if " -- @" in scenario_name: + scenario_name = scenario_name.split(" # ")[0].rstrip() + display_id = "%s:%s => %s" % (file_name, line_num, scenario_name) + return display_id + + +def get_test_id(): + file_name = sb_config.behave_scenario.filename + file_name = file_name.replace("/", ".").replace("\\", ".") + scenario_name = sb_config.behave_scenario.name + if " -- @" in scenario_name: + scenario_name = scenario_name.split(" # ")[0].rstrip() + scenario_name = re.sub(r"[^\w" + r"_ " + r"]", "", scenario_name) + scenario_name = scenario_name.replace(" ", "_") + test_id = "%s.%s" % (file_name, scenario_name) + return test_id + + +def get_display_id(): + file_name = sb_config.behave_scenario.filename + line_num = str(sb_config.behave_scenario.line) + scenario_name = sb_config.behave_scenario.name + if " -- @" in scenario_name: + scenario_name = scenario_name.split(" # ")[0].rstrip() + display_id = "%s:%s => %s" % (file_name, line_num, scenario_name) + return display_id + + +def _get_test_ids_(): + test_id = get_test_id() + display_id = get_display_id() + return test_id, display_id + + +def dashboard_pre_processing(): + import subprocess + + command_args = sys.argv[1:] + command_string = " ".join(command_args) + command_string = command_string.replace("--quiet", "") + command_string = command_string.replace("-q", "") + proc = subprocess.Popen( + "behave -d %s --show-source" % command_string, + stdout=subprocess.PIPE, + shell=True, + ) + (output, error) = proc.communicate() + filename_count = 0 + filename_list = [] + feature_count = 0 + feature_list = [] + scenario_count = 0 + scenario_list = [] + sb_config.item_count = 0 + sb_config.item_count_passed = 0 + sb_config.item_count_failed = 0 + sb_config.item_count_skipped = 0 + sb_config.item_count_untested = 0 + filename = None + feature_name = None + scenario_name = None + if is_windows: + output = output.decode("latin1") + else: + output = output.decode("utf-8") + for row in output.replace("\r", "").split("\n"): + if row.startswith("Feature: "): + filename_count += 1 + feature_count += 1 + feature_name = row.split("Feature: ")[1] + if " # features/" in feature_name: + filename = feature_name.split(" # features/")[-1] + filename = "features/" + filename.split(":")[0] + feature_name = feature_name.split(" # features/")[0] + elif " # features\\" in feature_name: + filename = feature_name.split(" # features\\")[-1] + filename = "features\\" + filename.split(":")[0] + feature_name = feature_name.split(" # features\\")[0] + else: + filename = feature_name.split(" # ")[-1] + filename = filename.split(":")[0] + feature_name = feature_name.split(" # ")[-1] + filename = filename.strip() + filename_list.append(filename) + feature_name = feature_name.strip() + feature_list.append(feature_name) # Maybe filename is good enough + elif ( + row.startswith(" Scenario: ") + or row.startswith(" Scenario Outline: ") + ): + line_num = row.split(":")[-1] + scenario_count += 1 + scenario_name = None + if row.startswith(" Scenario: "): + scenario_name = row.split(" Scenario: ")[-1] + else: + scenario_name = row.split(" Scenario Outline: ")[-1] + if " -- @" in scenario_name: + scenario_name = scenario_name.split(" # ")[0].rstrip() + elif " # features/" in scenario_name: + scenario_name = scenario_name.split(" # features/")[0] + else: + scenario_name = scenario_name.split(" # ")[0] + scenario_name = scenario_name.strip() + scenario_list.append(scenario_name) + # Dashboard row preparation + test_id = calculate_test_id(filename, scenario_name) + display_id = calculate_display_id( + filename, line_num, scenario_name + ) + sb_config._results[test_id] = "Untested" + sb_config._duration[test_id] = "-" + sb_config._display_id[test_id] = display_id + sb_config._d_t_log_path[test_id] = None + # Set the total number of dashboard entries + sb_config.item_count = scenario_count + + +def _create_dashboard_assets_(): + from seleniumbase.js_code.live_js import live_js + from seleniumbase.core.style_sheet import get_pytest_style + + abs_path = os.path.abspath(".") + assets_folder = os.path.join(abs_path, "assets") + if not os.path.exists(assets_folder): + os.makedirs(assets_folder) + pytest_style_css = os.path.join(assets_folder, "pytest_style.css") + add_pytest_style_css = True + if os.path.exists(pytest_style_css): + existing_pytest_style = None + with open(pytest_style_css, mode="r") as f: + existing_pytest_style = f.read() + if existing_pytest_style == get_pytest_style(): + add_pytest_style_css = False + if add_pytest_style_css: + out_file = open(pytest_style_css, mode="w+", encoding="utf-8") + out_file.writelines(get_pytest_style()) + out_file.close() + live_js_file = os.path.join(assets_folder, "live.js") + add_live_js_file = True + if os.path.exists(live_js_file): + existing_live_js = None + with open(live_js_file, mode="r") as f: + existing_live_js = f.read() + if existing_live_js == live_js: + add_live_js_file = False + if add_live_js_file: + out_file = open(live_js_file, mode="w+", encoding="utf-8") + out_file.writelines(live_js) + out_file.close() + + +def behave_dashboard_prepare(): + """Print the dashboard path if at least one test runs.""" + if sb_config.item_count > 0: + _create_dashboard_assets_() + # Output Dashboard info to the console + sb_config.item_count_untested = sb_config.item_count + dash_path = os.path.join(os.getcwd(), "dashboard.html") + star_len = len("Dashboard: ") + len(dash_path) + with suppress(Exception): + terminal_size = os.get_terminal_size().columns + if terminal_size > 30 and star_len > terminal_size: + star_len = terminal_size + stars = "*" * star_len + c1 = "" + cr = "" + if not is_linux: + c1 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX + cr = colorama.Style.RESET_ALL + print("Dashboard: %s%s%s\n%s" % (c1, dash_path, cr, stars)) + + +def _perform_behave_unconfigure_(): + if hasattr(sb_config, "multi_proxy") and not sb_config.multi_proxy: + proxy_helper.remove_proxy_zip_if_present() + if hasattr(sb_config, "reuse_session") and sb_config.reuse_session: + # Close the shared browser session + if sb_config.shared_driver: + try: + if ( + not is_windows + or sb_config.browser == "ie" + or sb_config.shared_driver.service.process + ): + sb_config.shared_driver.quit() + except AttributeError: + pass + except Exception: + pass + sb_config.shared_driver = None + if hasattr(sb_config, "archive_logs"): + log_helper.archive_logs_if_set( + constants.Logs.LATEST + "/", sb_config.archive_logs + ) + log_helper.clear_empty_logs() + # Dashboard post-processing: Disable time-based refresh and stamp complete + if not hasattr(sb_config, "dashboard") or not sb_config.dashboard: + # Done with "behave_unconfigure" unless using the Dashboard + return + stamp = "\n" + find_it = constants.Dashboard.META_REFRESH_HTML + swap_with = "" # Stop refreshing the page after the run is done + find_it_2 = "Awaiting results... (Refresh the page for updates)" + swap_with_2 = ( + "Test Run ENDED: Some results UNREPORTED due to skipped tearDown()" + ) + find_it_3 = 'Untested' + swap_with_3 = 'Unreported' + # These use caching to prevent extra method calls + DASH_PIE_PNG_1 = constants.Dashboard.get_dash_pie_1() + DASH_PIE_PNG_2 = constants.Dashboard.get_dash_pie_2() + find_it_4 = 'href="%s"' % DASH_PIE_PNG_1 + swap_with_4 = 'href="%s"' % DASH_PIE_PNG_2 + try: + abs_path = os.path.abspath(".") + dashboard_path = os.path.join(abs_path, "dashboard.html") + # Part 1: Finalizing the dashboard / integrating html report + if os.path.exists(dashboard_path): + the_html_d = None + with open(dashboard_path, mode="r", encoding="utf-8") as f: + the_html_d = f.read() + if sb_config._multithreaded and "-c" in sys.argv: + # Threads have "-c" in sys.argv, except for the last + raise Exception('Break out of "try" block.') + if sb_config._multithreaded: + dash_pie_loc = constants.Dashboard.DASH_PIE + pie_path = os.path.join(abs_path, dash_pie_loc) + if os.path.exists(pie_path): + import json + + with open(pie_path, mode="r") as f: + dash_pie = f.read().strip() + sb_config._saved_dashboard_pie = json.loads(dash_pie) + # If the test run doesn't complete by itself, stop refresh + the_html_d = the_html_d.replace(find_it, swap_with) + the_html_d = the_html_d.replace(find_it_2, swap_with_2) + the_html_d = the_html_d.replace(find_it_3, swap_with_3) + the_html_d = the_html_d.replace(find_it_4, swap_with_4) + the_html_d += stamp + with open(dashboard_path, mode="w", encoding="utf-8") as f: + f.write(the_html_d) # Finalize the dashboard + except KeyboardInterrupt: + pass + except Exception: + pass + + +def do_final_driver_cleanup_as_needed(): + with suppress(Exception): + if hasattr(sb_config, "last_driver") and sb_config.last_driver: + if ( + not is_windows + or sb_config.browser == "ie" + or sb_config.last_driver.service.process + ): + sb_config.last_driver.quit() + + +def _perform_behave_terminal_summary_(): + latest_logs_dir = os.path.join( + os.getcwd(), constants.Logs.LATEST + os.sep + ) + dash_path = os.path.join(os.getcwd(), "dashboard.html") + equals_len = len("Dashboard: ") + len(dash_path) + with suppress(Exception): + terminal_size = os.get_terminal_size().columns + if terminal_size > 30 and equals_len > terminal_size: + equals_len = terminal_size + equals = "=" * (equals_len + 2) + c2 = "" + cr = "" + if not is_linux: + c2 = colorama.Fore.MAGENTA + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + if sb_config.dashboard: + # Print link a second time because the first one may be off-screen + print("%s- Dashboard:%s %s" % (c2, cr, dash_path)) + if ( + sb_config._has_exception + or sb_config.save_screenshot + or sb_config._has_logs + ): + # Log files are generated during test failures and Screenshot Mode + print("%s--- LogPath:%s %s" % (c2, cr, latest_logs_dir)) + if ( + sb_config.dashboard + and not (sb_config._has_exception or sb_config.save_screenshot) + ): + print("%s" % equals) + elif ( + not sb_config.dashboard + and (sb_config._has_exception or sb_config.save_screenshot) + ): + print("%s" % equals[2:]) + elif ( + sb_config.dashboard + and (sb_config._has_exception or sb_config.save_screenshot) + ): + print("%s" % equals[2:]) + + +########################################### + + +def before_all(context): + context.sb = get_configured_sb(context) + if context.sb.dashboard: + dashboard_pre_processing() + behave_dashboard_prepare() + + +def before_feature(context, feature): + sb_config.behave_feature = feature + session_helper.end_reused_class_session_as_needed() + + +def before_scenario(context, scenario): + sb_config.behave_context = context + sb_config.behave_scenario = scenario + sb_config.behave_line_num = scenario.line + sb_config.behave_step_count = 0 + context.sb.setUp() + + +def before_step(context, step): + sb_config.behave_step_count += 1 + sb_config.behave_step = step + + +def after_step(context, step): + sb_config.behave_step = step + if step.status == "failed": + number = sb_config.behave_step_count + print(">>> STEP FAILED: (#%s) %s" % (number, step.name)) + print("Class / Feature: ", sb_config.behave_feature.name) + print("Test / Scenario: ", sb_config.behave_scenario.name) + + +def after_scenario(context, scenario): + sb = context.sb + sb_config.last_driver = sb.driver + sb_config.behave_context = context + sb_config.behave_scenario = scenario + sb.tearDown() + + +def after_feature(context, feature): + sb_config.feature = feature + session_helper.end_reused_class_session_as_needed() + + +def after_all(context): + _perform_behave_unconfigure_() + do_final_driver_cleanup_as_needed() + _perform_behave_terminal_summary_() diff --git a/seleniumbase/behave/steps.py b/seleniumbase/behave/steps.py new file mode 100644 index 00000000000..cbb1b67edb4 --- /dev/null +++ b/seleniumbase/behave/steps.py @@ -0,0 +1,10 @@ +from sbase import steps # noqa +""" +This is a proxy for importing SeleniumBase-Behave steps. +A short path name shortens the output of "behave" tests. +See "../../sbase/steps.py" for the actual list of steps. +Eg. + BEHAVE STEP NAME # ../../sbase/steps.py:11 0.111s + BEHAVE STEP NAME # ../../sbase/steps.py:74 0.550s + BEHAVE STEP NAME # ../../sbase/steps.py:67 0.385s +""" diff --git a/seleniumbase/common/ReadMe.md b/seleniumbase/common/ReadMe.md old mode 100755 new mode 100644 index b4770f4b90f..a0b9a521baa --- a/seleniumbase/common/ReadMe.md +++ b/seleniumbase/common/ReadMe.md @@ -1,19 +1,25 @@ -## Using methods from the "common" folder. + + +## [seleniumbase/common](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/common) decorators and security. ### Part 1: Decorators - (from [decorators.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/common/decorators.py)) #### Use these Python decorators with your test methods as needed: -* @retry_on_exception(tries=6, delay=1, backoff=2, max_delay=32) +* ``@print_runtime(description=None, limit=None)`` + +* ``@runtime_limit(limit, description=None)`` + +* ``@retry_on_exception(tries=6, delay=1, backoff=2, max_delay=32)`` -* @rate_limited(max_per_second) +* ``@rate_limited(max_per_second)`` Example demonstrating a rate-limited printing functionality: + ```python import unittest from seleniumbase import decorators - class MyTestClass(unittest.TestCase): @decorators.rate_limited(3.5) # The arg is max calls per second @@ -34,13 +40,14 @@ Often in your tests, you may need to login to a website to perform testing. This #### Usage: -* First, set your custom encryption/decryption key in your local clone of [settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py). (If you modify they key later, you'll need to encrypt all your passwords again.) +* First, set your custom encryption/decryption key in your local clone of [settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py). (If you modify the key later, you'll need to encrypt all your passwords again.) * Next, use [obfuscate.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/common/obfuscate.py) to obfuscate/encrypt passwords into coded strings: -```bash + +```zsh python obfuscate.py -Enter password to obfuscate: (CTRL-C to exit) +Enter password to obfuscate: (CTRL+C to exit) Password: ********* Verify password: Password: ********* @@ -48,12 +55,17 @@ Password: ********* Here is the obfuscated password: $^*ENCRYPT=RXlYMSJWTz8HSwM=?&#$ ``` -(You can also use [unobfuscate.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/common/unobfuscate.py) to encrypt passwords without having them masked while typing them. Or you can use it to decrypt an obfuscated pasword.) + +(You can also use [unobfuscate.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/common/unobfuscate.py) to encrypt passwords without having them masked while typing them. Or you can use it to decrypt an obfuscated password.) * Finally, in your tests you can now decrypt obfuscated passwords for use in login methods like this: + ```python -from seleniumbase.common import encryption +from seleniumbase import encryption ... password = encryption.decrypt('$^*ENCRYPT=RXlYMSJWTz8HSwM=?&#$') ``` + (You'll notice that encrypted strings have a common start token and end token. This is to help tell them apart from non-encrypted strings. You can customize these tokens in [settings.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py). The current default setting is `$^*ENCRYPT=` for the start token and `?&#$` for the end token.) + +See [test_decryption.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/test_decryption.py) for an example of decrypting encrypted passwords in tests. diff --git a/seleniumbase/common/__init__.py b/seleniumbase/common/__init__.py old mode 100755 new mode 100644 diff --git a/seleniumbase/common/decorators.py b/seleniumbase/common/decorators.py old mode 100755 new mode 100644 index 827b956a945..8d0b6916996 --- a/seleniumbase/common/decorators.py +++ b/seleniumbase/common/decorators.py @@ -1,21 +1,135 @@ -import inspect +import colorama import logging import math -import threading +import sys import time import warnings +from contextlib import contextmanager from functools import wraps +from seleniumbase.common.exceptions import TimeoutException + +c1 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX +cr = colorama.Style.RESET_ALL +if "linux" in sys.platform: + c1 = cr = "" + + +@contextmanager +def print_runtime(description=None, limit=None): + """Print the runtime duration of a method or "with"-block after completion. + If limit, fail if the runtime duration exceeds the limit after completion. + + Method / Function example usage -> + from seleniumbase import decorators + + @decorators.print_runtime("My Method") + def my_method(): + # code ... + # code ... + + "with"-block example usage -> + from seleniumbase import decorators + + with decorators.print_runtime("My Code Block"): + # code ... + # code ... """ + if not description: + description = "Code Block" + description = str(description) + if limit: + limit = float("%.2f" % limit) + if limit < 0.01: + limit = 0.01 # Minimum runtime limit + exception = None + start_time = time.time() + try: + yield + except Exception as e: + exception = e + raise + finally: + end_time = time.time() + run_time = end_time - start_time + name = description + info = c1 + "" + cr + # Print times with a statistically significant number of decimal places + if run_time < 0.00001: + print("%s - {%s} ran for %.8f seconds." % (info, name, run_time)) + elif run_time < 0.0001: + print("%s - {%s} ran for %.7f seconds." % (info, name, run_time)) + elif run_time < 0.001: + print("%s - {%s} ran for %.6f seconds." % (info, name, run_time)) + elif run_time < 0.01: + print("%s - {%s} ran for %.5f seconds." % (info, name, run_time)) + elif run_time < 0.1: + print("%s - {%s} ran for %.4f seconds." % (info, name, run_time)) + elif run_time < 1: + print("%s - {%s} ran for %.3f seconds." % (info, name, run_time)) + else: + print("%s - {%s} ran for %.2f seconds." % (info, name, run_time)) + if limit and limit > 0 and run_time > limit: + message = ( + "\n {%s} duration of %.2fs exceeded the time limit of %.2fs!" + % (name, run_time, limit) + ) + if exception: + message = exception.msg + "\nAND " + message + raise TimeoutException(message) + + +@contextmanager +def runtime_limit(limit, description=None): + """Fail if the runtime duration of a method or "with"-block exceeds limit. + (The failure won't occur until after the method or "with"-block completes.) + + Method / Function example usage -> + from seleniumbase import decorators + + @decorators.runtime_limit(4.5) + def my_method(): + # code ... + # code ... + + "with"-block example usage -> + from seleniumbase import decorators + + with decorators.runtime_limit(32): + # code ... + # code ... """ + limit = float("%.2f" % limit) + if limit < 0.01: + limit = 0.01 # Minimum runtime limit + if not description: + description = "Code Block" + description = str(description) + exception = None + start_time = time.time() + try: + yield + except Exception as e: + exception = e + raise + finally: + end_time = time.time() + run_time = end_time - start_time + # Fail if the runtime of the code block exceeds the limit + if limit and limit > 0 and run_time > limit: + message = ( + "\n {%s} duration of %.2fs exceeded the time limit of %.2fs!" + % (description, run_time, limit) + ) + if exception: + message = exception.msg + "\nAND " + message + raise TimeoutException(message) def retry_on_exception(tries=6, delay=1, backoff=2, max_delay=32): - ''' - Decorator for implementing exponential backoff for retrying on failures. + """Decorator for implementing exponential backoff for retrying on failures. tries: Max number of tries to execute the wrapped function before failing. delay: Delay time in seconds before the FIRST retry. backoff: Multiplier to extend the initial delay by for each retry. - max_delay: Max time in seconds to wait between retries. - ''' + max_delay: Max time in seconds to wait between retries.""" tries = math.floor(tries) if tries < 1: raise ValueError('"tries" must be greater than or equal to 1.') @@ -36,21 +150,26 @@ def function_to_retry(*args, **kwargs): except Exception as e: if local_delay > max_delay: local_delay = max_delay - logging.exception('%s: Retrying in %d seconds...' - % (str(e), local_delay)) + logging.exception( + "%s: Retrying in %d seconds..." % (str(e), local_delay) + ) time.sleep(local_delay) local_tries -= 1 local_delay *= backoff return func(*args, **kwargs) + return function_to_retry + return decorated_function_with_retry def rate_limited(max_per_second): - """ This decorator limits how often a method can get called in a second. - If the limit is exceeded, the call will be held in a queue until - enough time has passed. - Useful when trying to avoid overloading a system with rapid calls. """ + """This decorator limits how often a method can get called in a second. + If the limit is exceeded, the call will be held in a queue until + enough time has passed. + Useful when trying to avoid overloading a system with rapid calls.""" + import threading + min_interval = 1.0 / float(max_per_second) def decorate(func): @@ -60,41 +179,47 @@ def decorate(func): def rate_limited_function(*args, **kargs): try: rate_lock.acquire(True) - elapsed = time.clock() - last_time_called[0] + elapsed = None + elapsed = time.process_time() - last_time_called[0] wait_time_remaining = min_interval - elapsed if wait_time_remaining > 0: time.sleep(wait_time_remaining) - last_time_called[0] = time.clock() + last_time_called[0] = time.process_time() finally: rate_lock.release() return func(*args, **kargs) + return rate_limited_function + return decorate def deprecated(message=None): - """ This decorator marks methods as deprecated. - A warning is displayed if the method is called. """ + """This decorator marks methods as deprecated. + A warning is displayed if the method is called.""" + import inspect def decorated_method_to_deprecate(func): if inspect.isclass(func): # Handle a deprecated class differently from a deprecated method - msg = "Class {}() is DEPRECATED! *** ".format(func.__name__) + msg = "Class {}() is DEPRECATED!".format(func.__name__) if message: - msg += "<> %s <>" % message - warnings.simplefilter('always', DeprecationWarning) # See Warnings + msg += " *** %s ***" % message + warnings.simplefilter("always", DeprecationWarning) # See Warnings warnings.warn(msg, category=DeprecationWarning, stacklevel=2) - warnings.simplefilter('default', DeprecationWarning) # Set Default + warnings.simplefilter("default", DeprecationWarning) # Set Default return func @wraps(func) def new_func(*args, **kwargs): - msg = "Method {}() is DEPRECATED! *** ".format(func.__name__) + msg = "Method {}() is DEPRECATED!".format(func.__name__) if message: - msg += "<> %s <>" % message - warnings.simplefilter('always', DeprecationWarning) # See Warnings + msg += " *** %s ***" % message + warnings.simplefilter("always", DeprecationWarning) # See Warnings warnings.warn(msg, category=DeprecationWarning, stacklevel=2) - warnings.simplefilter('default', DeprecationWarning) # Set Default + warnings.simplefilter("default", DeprecationWarning) # Set Default return func(*args, **kwargs) + return new_func + return decorated_method_to_deprecate diff --git a/seleniumbase/common/encryption.py b/seleniumbase/common/encryption.py old mode 100755 new mode 100644 index 93373a0fc60..e7e5a7a9a33 --- a/seleniumbase/common/encryption.py +++ b/seleniumbase/common/encryption.py @@ -1,6 +1,4 @@ -# -\*- coding: utf-8 -\*- - -''' This is mainly for string obfuscation. ''' +"""This is mainly for string obfuscation.""" import base64 import codecs @@ -13,16 +11,17 @@ def str_xor(string, key): raise Exception("2nd arg of str_xor() must be a string of length > 0!") if len(string) > len(key): difference = len(string) - len(key) - key = key + ( - ((difference / len(key)) * key) + key) + key = key + (((difference / len(key)) * key) + key) result = None try: result = "".join( - [chr(ord(c1) ^ ord(c2)) for (c1, c2) in zip(string, key)]) + [chr(ord(c1) ^ ord(c2)) for (c1, c2) in zip(string, key)] + ) except Exception: - string = string.decode('utf-8') + string = string.decode("utf-8") result = "".join( - [chr(ord(c1) ^ ord(c2)) for (c1, c2) in zip(string, key)]) + [chr(ord(c1) ^ ord(c2)) for (c1, c2) in zip(string, key)] + ) return result @@ -31,22 +30,22 @@ def is_obfuscated(string): # Obfuscated strings have a common predefined start token and end token. start_token = settings.OBFUSCATION_START_TOKEN end_token = settings.OBFUSCATION_END_TOKEN - return (string.startswith(start_token) and string.endswith(end_token)) + return string.startswith(start_token) and string.endswith(end_token) def shuffle_string(string): if len(string) < 2: return string - return (string[1::2] + string[::2]) + return string[1::2] + string[::2] def reverse_shuffle_string(string): if len(string) < 2: return string new_string = "" - odd = (len(string) % 2 == 1) - part1 = string[:int(len(string) / 2):1] - part2 = string[int(len(string) / 2)::1] + odd = len(string) % 2 == 1 + part1 = string[: int(len(string) / 2) : 1] # noqa: E203 + part2 = string[int(len(string) / 2) :: 1] # noqa: E203 for c in range(len(part1)): new_string += part2[c] new_string += part1[c] @@ -71,8 +70,8 @@ def blend_strings(string1, string2): return new_string -def rotate(l, n): - return l[n:] + l[:n] +def rotate(string, n): + return string[n:] + string[:n] def ord_string_sum(string): @@ -81,7 +80,7 @@ def ord_string_sum(string): for c in string: count += ord(c) except Exception: - string = string.decode('utf-8') + string = string.decode("utf-8") for c in string: count += ord(c) return count @@ -97,14 +96,18 @@ def decrypt(string): already_encrypted = False if is_obfuscated(string): already_encrypted = True - string = string[len(start_token):-len(end_token)] + string = string[len(start_token) : -len(end_token)] # noqa: E203 string = base64.b64decode(codecs.encode(string)) # Obfuscate the key used for string obfuscation - hd1 = hashlib.sha256(str(encryption_key).encode('utf-8')).hexdigest() - hd2 = hashlib.sha256(str(encryption_key[::-1]).encode('utf-8')).hexdigest() + hd1 = hashlib.sha256(str(encryption_key).encode("utf-8")).hexdigest() + hd2 = hashlib.sha256(str(encryption_key[::-1]).encode("utf-8")).hexdigest() b64_key = base64.b64encode(codecs.encode(encryption_key * 8)) - xor_key = "".join([chr(ord(str(c3)) - int(c1, 16) - int(c2, 16)) for ( - c1, c2, c3) in zip(hd1, hd2, b64_key.decode("utf-8"))]) + xor_key = "".join( + [ + chr(ord(str(c3)) - int(c1, 16) - int(c2, 16)) + for (c1, c2, c3) in zip(hd1, hd2, b64_key.decode("utf-8")) + ] + ) xor_key = blend_strings(xor_key, encryption_key) if len(xor_key) % 7 == 0: xor_key = xor_key + encryption_key[-1] @@ -118,18 +121,30 @@ def decrypt(string): rem4 = (len(string) + ord_string_sum(string)) % 2 if len(string) % 2 != 0: if rem3 == 1: - string = (chr(ord(string[-1]) - 5 - rem1) + string + '' - '' + chr(ord(string[-1]) - 13 - rem1)) + string = ( + chr(ord(string[-1]) - 5 - rem1) + + string + + chr(ord(string[-1]) - 13 - rem1) + ) else: - string = (chr(ord(string[-1]) - 11 - rem1) + string + '' - '' + chr(ord(string[-1]) - 23 - rem1)) + string = ( + chr(ord(string[-1]) - 11 - rem1) + + string + + chr(ord(string[-1]) - 23 - rem1) + ) elif len(string) > 1: if rem4 == 1: - string = (chr(ord(string[0]) - 19 + rem2) + string + '' - '' + chr(ord(string[0]) - 7 - rem2)) + string = ( + chr(ord(string[0]) - 19 + rem2) + + string + + chr(ord(string[0]) - 7 - rem2) + ) else: - string = (chr(ord(string[0]) - 26 + rem2) + string + '' - '' + chr(ord(string[0]) - 12 - rem2)) + string = ( + chr(ord(string[0]) - 26 + rem2) + + string + + chr(ord(string[0]) - 12 - rem2) + ) rem5 = (len(string) + ord_string_sum(string)) % 23 string = rotate(string, rem5) result = str_xor(shuffle_string(string)[::-1], xor_key) diff --git a/seleniumbase/common/exceptions.py b/seleniumbase/common/exceptions.py new file mode 100644 index 00000000000..0e10fb66864 --- /dev/null +++ b/seleniumbase/common/exceptions.py @@ -0,0 +1,117 @@ +""" SeleniumBase Exceptions + LinkTextNotFoundException => Called when expected link text is not visible. + NoSuchFileException => Called when self.assert_downloaded_file(...) fails. + NoSuchOptionException => Called when select_option_by_*() lacks the option. + NotConnectedException => Called when Internet is not reachable when needed. + NotUsingChromeException => Used by Chrome-only methods if not using Chrome. + NotUsingChromiumException => Used by Chromium-only methods if not Chromium. + OutOfScopeException => Used by BaseCase methods when setUp() is skipped. + ProxyConnectionException => Called when the proxy connection failed. + TextNotVisibleException => Called when the expected text is not visible. + TimeLimitExceededException => Called when exceeding "--time-limit=SECONDS". + TimeoutException => Called when some timeout limit has been exceeded. + VisualException => Called when there's a Visual Diff Assertion Failure. +""" + + +class LinkTextNotFoundException(Exception): + pass + + +class NoSuchFileException(Exception): + pass + + +class NoSuchOptionException(Exception): + pass + + +class NotConnectedException(Exception): + pass + + +class NotUsingChromeException(Exception): + pass + + +class NotUsingChromiumException(Exception): + pass + + +class OutOfScopeException(Exception): + pass + + +class ProxyConnectionException(Exception): + pass + + +class TextNotVisibleException(Exception): + pass + + +class TimeLimitExceededException(Exception): + pass + + +class TimeoutException(Exception): + pass + + +class VisualException(Exception): + pass + + +""" Selenium Exceptions (Simplified for SeleniumBase) """ + + +class WebDriverException(Exception): + """Base webdriver exception.""" + + def __init__(self, msg=None, screen=None, stacktrace=None): + super().__init__() + self.msg = msg + self.screen = screen + self.stacktrace = stacktrace + + def __str__(self): + exception_msg = "Message: %s\n" % self.msg + if self.screen: + exception_msg += "Screenshot: available via screen\n" + if self.stacktrace: + stacktrace = "\n".join(self.stacktrace) + exception_msg += "Stacktrace:\n%s" % stacktrace + return exception_msg + + +class InvalidSwitchToTargetException(WebDriverException): + """Thrown when frame or window target to be switched doesn't exist.""" + + +class NoSuchFrameException(InvalidSwitchToTargetException): + """Thrown when frame target to be switched doesn't exist.""" + + +class NoSuchWindowException(InvalidSwitchToTargetException): + """Thrown when window target to be switched doesn't exist.""" + + +class NoSuchElementException(WebDriverException): + """Thrown when element could not be found.""" + + +class NoSuchAttributeException(WebDriverException): + """Thrown when the attribute of element could not be found.""" + + +class InvalidElementStateException(WebDriverException): + """Thrown when a command could not be completed because the element is in + an invalid state.""" + + +class NoAlertPresentException(WebDriverException): + """Thrown when switching to no presented alert.""" + + +class ElementNotVisibleException(InvalidElementStateException): + """Thrown when an element is present in the DOM, but not visible.""" diff --git a/seleniumbase/common/obfuscate.py b/seleniumbase/common/obfuscate.py old mode 100755 new mode 100644 index 34f0a406433..397ef108a27 --- a/seleniumbase/common/obfuscate.py +++ b/seleniumbase/common/obfuscate.py @@ -14,8 +14,8 @@ def main(): try: - while(1): - print("\nEnter password to obfuscate: (CTRL-C to exit)") + while 1: + print("\nEnter password to obfuscate: (CTRL+C to exit)") password = getpass.getpass() print("Verify password:") verify_password = getpass.getpass() @@ -23,9 +23,15 @@ def main(): print("*** ERROR: Passwords don't match! .. Please try again!") continue print("\nHere is the obfuscated password:") - time.sleep(0.07) - print(encryption.decrypt(password)) - time.sleep(0.21) + time.sleep(0.2) + encrypted_password = encryption.decrypt(password) + print(encrypted_password) + time.sleep(0.2) + print("\nInside a test, use the following to decrypt it:\n") + time.sleep(0.2) + print(" from seleniumbase import encryption") + print(' encryption.decrypt("%s")' % encrypted_password) + time.sleep(0.2) except KeyboardInterrupt: print("\nExiting...\n") diff --git a/seleniumbase/common/unobfuscate.py b/seleniumbase/common/unobfuscate.py old mode 100755 new mode 100644 index cefc46f0f64..abcf9306015 --- a/seleniumbase/common/unobfuscate.py +++ b/seleniumbase/common/unobfuscate.py @@ -13,15 +13,12 @@ def main(): + input_method = input try: - # Python 2 has the raw_input() method. Python 3 does not. - input_method = raw_input # noqa: ignore=F821 - except Exception: - input_method = input # Using Python 3 - try: - while(1): + while 1: code = input_method( - '\nEnter obfuscated/encrypted string: (CTRL-C to exit):\n') + "\nEnter obfuscated/encrypted string: (CTRL+C to exit):\n" + ) print("\nHere is the unobfuscated string/password:") time.sleep(0.07) print(encryption.decrypt(code)) diff --git a/seleniumbase/config/__init__.py b/seleniumbase/config/__init__.py old mode 100755 new mode 100644 diff --git a/seleniumbase/config/ad_block_list.py b/seleniumbase/config/ad_block_list.py old mode 100755 new mode 100644 index 107e0cf6ede..8ed954b54a0 --- a/seleniumbase/config/ad_block_list.py +++ b/seleniumbase/config/ad_block_list.py @@ -17,59 +17,101 @@ """ AD_BLOCK_LIST = [ + '[aria-label="Ads"]', + '[src*="adservice."]', + '[src*="adclick"]', + '[src*="doubleclick"]', + '[src*="snigelweb.com"]', + '[src*="tagservices.com"]', + '[src*="adsby"]', + '[src*="adroll.com"]', + '[src*="pagead"]', + '[src*="3lift"]', + '[src*="smartads."]', + '[src*="ad_nexus"]', + '[src*="/ads/"]', + '[src*="moatads.com"]', + '[src*="adsystem"]', + '[src*="connectad"]', + '[src*="/adservice."]', + '[src*="syndication.com"]', + '[src*="/ads."]', + '[src*="lijit"]', + '[src*="pagead"]', + '[src*="adnxs.com"]', + '[src*="onetag-sys.com"]', + '[src*="indexww.com"]', + '[src*="3lift.com"]', + '[src*="rubiconproject.com"]', + '[src*="brealtime.com"]', + '[src*="33across.com"]', + '[src*="adsrvr"]', + '[type="data-doubleclick"]', + "iframe[data-google-container-id]", + 'iframe[src*="doubleclick"]', + 'iframe[src*="/AdServer/"]', + 'iframe[src*="openx.net"]', + 'iframe[onload*="doWithAds"]', + 'iframe[id*="_ads_frame"]', + 'iframe[style="height:0px;width:0px;display:none;"]', '[aria-label="Ad"]', - '[class^="sponsored-content"]', + '[aria-label="Timeline: Trending now"]', + '[aria-label="Timeline: Carousel"]', + '[aria-roledescription="carousel"]', + '[aria-label="Who to follow"]', + '[class*="sponsored-content"]', + '[class*="adsbygoogle"]', + '[class^="adroll"]', '[data-ad-details*="Advertisement"]', '[data-native_ad*="placement"]', '[data-provider="dianomi"]', '[data-type="ad"]', '[data-track-event-label*="-taboola-"]', '[data-ad-feedback-beacon*="AD_"]', - '[data-ad-feedback-beacon]', - '[href*="doubleclick.net/"]', + "[data-ad-feedback-beacon]", + '[data-dcm-click-tracker*="/adclick."]', + "[data-google-av-adk]", + "[data-google-query-id]", + '[data-ylk*="sponsored_cluster"]', + "[data-google-av-cxn]", + "[data-ad-client]", + "[data-ad-slot]", + '[href*="doubleclick"]', '[href*="amazon-adsystem"]', + '[alt="Advertisement"]', + '[alt$=" Ad"]', '[id*="-ad-"]', '[id*="_ads_"]', - '[alt="Advertisement"]', '[id*="AdFrame"]', '[id*="carbonads"]', '[id^="ad-"]', '[id^="my-ads"]', '[id^="outbrain_widget"]', '[id^="taboola-"]', + '[id^="google_ads_frame"]', + '[id^="google_ads_iframe"]', + '[id="tryitLeaderboard"]', '[id="dianomiRightRail"]', - '[src*="smartads."]', - '[src*="ad_nexus"]', - '[src*="/ads/"]', - '[data-dcm-click-tracker*="/adclick."]', - '[data-google-query-id^="C"]', '[allow*="advertising.com"]', - '[data-ylk*="pkgt:sponsored_cluster"]', - '[data-ad-slot]', - 'iframe[onload*="doWithAds"]', - 'iframe[src*="doubleclick.net"]', - 'iframe[id*="google_ads_frame"]', - 'ins.adsbygoogle', - 'li.strm-ad-clusters', - 'li.js-stream-ad', - 'div.after_ad', - 'div.ad-container', - 'div.ad_module', - 'div.ad-subnav-container', - 'div.ad-wrapper', - 'div.data-ad-container', - 'div.l-ad', - 'div.right-ad', - 'div.wx-adWrapper', - 'div.image > a > img[src*="HomepageAd-"]', - 'img[src*="HomepageAd-"]', - 'img.img_ad', + "ins.adsby", + "li.strm-ad-clusters", + "li.js-stream-ad", + "div.after_ad", + "div.ad-container", + "div.ad_module", + "div.ad-subnav-container", + "div.ad-wrapper", + "div.adroll-block", + "div.data-ad-container", + "div.GoogleActiveViewElement", + "div.l-ad", + "div.right-ad", + "div.wx-adWrapper", + 'div.image > a > img[src*="HomepageAd"]', + 'img[src*="HomepageAd"]', + "img.img_ad", 'link[href*="/adservice."]', - 'script[src*="/adservice."]', - 'script[type="data-doubleclick"]', - 'script[src*="doubleclick.net"]', - 'script[src*="googletagservices.com/"]', - 'script[src*="ad.doubleclick.net/"]', - 'script[src*="/pagead/"]', - 'section.dianomi-ad', + "section.dianomi-ad", + "ytd-promoted-video-renderer", + "ytd-video-masthead-ad-v3-renderer", ] diff --git a/seleniumbase/config/proxy_list.py b/seleniumbase/config/proxy_list.py old mode 100755 new mode 100644 index 11b0aabbaad..be0948e4a40 --- a/seleniumbase/config/proxy_list.py +++ b/seleniumbase/config/proxy_list.py @@ -15,11 +15,15 @@ Example proxies in PROXY_LIST below are not guaranteed to be active or secure. If you don't already have a proxy server to connect to, you can try finding one from one of following sites: +* https://www.sslproxies.org/ * https://www.us-proxy.org/ +* https://hidemy.name/en/proxy-list/ """ PROXY_LIST = { - "example1": "158.69.138.8:1080", # (Example) - set your own proxy here + "example1": "65.109.191.98:3080", # (Example) - set your own proxy here + "example2": "157.254.194.57:1080", # (Example) + "example3": "socks4://142.54.228.193:4145", # (Example) "proxy1": None, "proxy2": None, "proxy3": None, diff --git a/seleniumbase/config/settings.py b/seleniumbase/config/settings.py old mode 100755 new mode 100644 index 208243cba5d..15b32fbadb5 --- a/seleniumbase/config/settings.py +++ b/seleniumbase/config/settings.py @@ -1,9 +1,8 @@ """ You'll probably want to customize this to your own environment and needs. -If you used ``python setup.py install`` instead of ``python setup.py develop``, -you may need to rerun ``python setup.py install`` in order for your changes -to take effect (unless you switch to using ``develop``). +For changes to take effect immediately, use Python's Develop Mode. +Develop Mode Install: "pip install -e ." (from the top-level directory) """ @@ -13,10 +12,22 @@ # Different methods/actions in base_case.py use different timeouts. # If the element to be acted on does not appear in time, the test fails. MINI_TIMEOUT = 2 -SMALL_TIMEOUT = 6 +SMALL_TIMEOUT = 7 LARGE_TIMEOUT = 10 EXTREME_TIMEOUT = 30 +# Default page load timeout. +# If a page takes longer than this to load, you'll get the following error: +# selenium.common.exceptions.TimeoutException: +# Message: timeout: Timed out receiving message from renderer: PLT +# In global Selenium settings, this value is set to 300 seconds by default. +PAGE_LOAD_TIMEOUT = 120 + +# Default page load strategy. +# ["normal", "eager", "none"] +# Selenium default = "normal" +PAGE_LOAD_STRATEGY = "normal" + # If True, existing logs from past test runs will be saved and take up space. # If False, only the logs from the most recent test run will be saved locally. # You can also archive existing logs on the command line with: "--archive_logs" @@ -26,6 +37,13 @@ # If False, only the downloads from the most recent run will be saved locally. ARCHIVE_EXISTING_DOWNLOADS = False +# If True, the last page screenshot will include the and background. +# If False, the last page screenshot will only include the section. +# Depending on the screen size, including a background could make the +# appear very small in the screenshot, which may require manually zooming in +# on the to see page details if you decide to include the background. +SCREENSHOT_WITH_BACKGROUND = False + # Default names for files saved during test failures. # (These files will get saved to the "latest_logs/" folder.) SCREENSHOT_NAME = "screenshot.png" @@ -39,20 +57,27 @@ HTML_REPORT = "report.html" RESULTS_TABLE = "results_table.csv" -''' -This adds wait_for_ready_state_complete() after various browser actions. -Setting this to True may improve reliability at the cost of speed. -''' -# Called after self.open(url) or self.open_url(url), NOT self.driver.open(url) +""" +If True, switch to new tabs/windows automatically if a click opens a new one. +(This switch only happens if the initial tab is still on same URL as before, +which prevents a situation where a click opens up a new URL in the same tab, +where a pop-up might open up a new tab on its own, leading to a double open. +If False, the browser will stay on the current tab where the click happened. +""" +SWITCH_TO_NEW_TABS_ON_CLICK = True + +""" +These methods add global waits, such as self.wait_for_ready_state_complete(), +which waits for document.readyState to be "complete" after browser actions. +""" +# Called after self.goto(url), NOT driver.get(url) WAIT_FOR_RSC_ON_PAGE_LOADS = True # Called after self.click(selector), NOT element.click() -WAIT_FOR_RSC_ON_CLICKS = True - -''' -This adds wait_for_angularjs() after various browser actions. -(Requires WAIT_FOR_RSC_ON_PAGE_LOADS and WAIT_FOR_RSC_ON_CLICKS to also be on.) -''' +WAIT_FOR_RSC_ON_CLICKS = False +# Wait for AngularJS calls to complete after various browser actions. WAIT_FOR_ANGULARJS = True +# Skip all calls to wait_for_ready_state_complete() and wait_for_angularjs(). +SKIP_JS_WAITS = False # Default time to wait after each browser action performed during Demo Mode. # Use Demo Mode when you want others to see what your automation is doing. @@ -77,8 +102,6 @@ # If True, the Content Security Policy will be disabled on Chrome. # If False, each website's default Content Security Policy will be used. # (A website's CSP may prevent SeleniumBase from loading custom JavaScript.) -# If using demo_mode or MasterQA, this value will become True regardless, -# with the exception of running in headless mode, in which case it'll be False. # You can also disable the CSP on the command line by using "--disable_csp". DISABLE_CSP_ON_CHROME = False @@ -87,6 +110,24 @@ # (This applies when using --proxy=[PROXY_STRING] for using a proxy server.) RAISE_INVALID_PROXY_STRING_EXCEPTION = True +# Default browser coordinates when opening new windows for tests. +WINDOW_START_X = 20 +WINDOW_START_Y = 54 + +# Default browser resolutions when opening new windows for tests. +# (Headless resolutions take priority, and include all browsers.) +# (Firefox starts maximized by default when running in GUI Mode.) +CHROME_START_WIDTH = 1280 +CHROME_START_HEIGHT = 840 +HEADLESS_START_WIDTH = 1440 +HEADLESS_START_HEIGHT = 1880 + +# If True, hides messages related to downloading drivers. +# If False, you'll see details about downloading drivers. +# (This only affects driver downloads coming from tests.) +# (If calling "sbase get chromedriver", then won't hide.) +HIDE_DRIVER_DOWNLOADS = False + # #####>>>>>----- MasterQA SETTINGS -----<<<<<##### # ##### (Used when importing MasterQA as the parent class) @@ -100,7 +141,7 @@ # If True, the automation will start in full-screen mode MASTERQA_START_IN_FULL_SCREEN_MODE = False -# The maximimum idle time allowed (in seconds) before timing out and exiting +# The maximum idle time allowed (in seconds) before timing out and exiting MASTERQA_MAX_IDLE_TIME_BEFORE_QUIT = 600 @@ -111,12 +152,16 @@ # (For 2-factor authentication using a time-based one-time password algorithm) # (See https://github.com/pyotp/pyotp and https://pypi.org/project/pyotp/ ) # (Also works with Authy and other compatible apps.) +# Usage: "self.get_google_auth_password()" (output based on timestamp) +# Usage with override: "self.get_google_auth_password(totp_key=TOTP_KEY)" TOTP_KEY = "base32secretABCD" # MySQL DB Credentials -# (For saving data from tests) +# (For saving data from tests to a MySQL DB) +# Usage: "--with-db_reporting" DB_HOST = "127.0.0.1" +DB_PORT = 3306 DB_USERNAME = "root" DB_PASSWORD = "test" DB_SCHEMA = "test_db" @@ -125,6 +170,7 @@ # Amazon S3 Bucket Credentials # (For saving screenshots and other log files from tests) # (Bucket names are unique across all existing bucket names in Amazon S3) +# Usage: "--with-s3_logging" S3_LOG_BUCKET = "[S3 BUCKET NAME]" S3_BUCKET_URL = "https://s3.amazonaws.com/[S3 BUCKET NAME]/" S3_SELENIUM_ACCESS_KEY = "[S3 ACCESS KEY]" @@ -139,21 +185,3 @@ # Helps identify which strings/passwords have been obfuscated. OBFUSCATION_START_TOKEN = "$^*ENCRYPT=" OBFUSCATION_END_TOKEN = "?&#$" - - -# #####>>>>>----- OPTIONAL SETTINGS -----<<<<<##### -# ##### (For reading emails, notifying people via chat apps, etc.) - -# Default Email Credentials -# (If tests send out emails, you can scan and verify them by using IMAP) -# Here's a list of imap strings for known email providers: -# - Gmail: imap.gmail.com -# - Outlook/Live: imap-mail.outlook.com -# - Yahoo Mail: imap.mail.yahoo.com -# - AT&T: imap.mail.att.net -# - Comcast: imap.comcast.net -# - Verizon: incoming.verizon.net -EMAIL_USERNAME = "[TEST ACCOUNT GMAIL USERNAME]@gmail.com" -EMAIL_PASSWORD = "[TEST ACCOUNT GMAIL PASSWORD]" -EMAIL_IMAP_STRING = "imap.gmail.com" -EMAIL_IMAP_PORT = 993 diff --git a/seleniumbase/console_scripts/ReadMe.md b/seleniumbase/console_scripts/ReadMe.md old mode 100755 new mode 100644 index e4eb1d4639f..f2c287ea977 --- a/seleniumbase/console_scripts/ReadMe.md +++ b/seleniumbase/console_scripts/ReadMe.md @@ -1,144 +1,796 @@ -## Console Scripts + + +

      Console Scripts ๐Ÿ’ป

      + +๐ŸŒŸ SeleniumBase console scripts can do many things, such as downloading web drivers, creating test directories with config files, activating the SeleniumBase Recorder, launching the SeleniumBase Commander, translating tests into other languages, running a Selenium Grid, and more. + +* Usage: `seleniumbase [COMMAND] [PARAMETERS]` + +* (simplified): `sbase [COMMAND] [PARAMETERS]` + +* To list all commands: `seleniumbase --help` + +(For running tests, [use pytest with SeleniumBase](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/customizing_test_runs.md).) + +```zsh +COMMANDS: + get / install [DRIVER] [OPTIONS] + methods (List common Python methods) + options (List common pytest options) + behave-options (List common behave options) + gui / commander [OPTIONAL PATH or TEST FILE] + behave-gui (SBase Commander for Behave) + caseplans [OPTIONAL PATH or TEST FILE] + mkdir [DIRECTORY] [OPTIONS] + mkfile [FILE.py] [OPTIONS] + mkrec / codegen [FILE.py] [OPTIONS] + recorder (Open Recorder Desktop App.) + record (If args: mkrec. Else: App.) + mkpres [FILE.py] [LANG] + mkchart [FILE.py] [LANG] + print [FILE] [OPTIONS] + translate [SB_FILE.py] [LANG] [ACTION] + convert [WEBDRIVER_UNITTEST_FILE.py] + extract-objects [SB_FILE.py] + inject-objects [SB_FILE.py] [OPTIONS] + objectify [SB_FILE.py] [OPTIONS] + revert-objects [SB_FILE.py] [OPTIONS] + encrypt / obfuscate + decrypt / unobfuscate + proxy (Start a basic proxy server) + download server (Get Selenium Grid JAR file) + grid-hub [start|stop] [OPTIONS] + grid-node [start|stop] --hub=[HOST/IP] + * (EXAMPLE: "sbase get chromedriver") * + + Type "sbase help [COMMAND]" for specific command info. + For info on all commands, type: "seleniumbase --help". + Use "pytest" for running tests. +``` + +

      get / install

      -SeleniumBase console scripts help you get things done more easily, such as installing web drivers, creating a test directory with necessary configuration files, converting old Webdriver unittest scripts into SeleniumBase code, and using the Selenium Grid. +* Usage: + +```zsh +sbase get [DRIVER] [OPTIONS] +sbase install [DRIVER] [OPTIONS] +``` + +* Examples: + +```zsh +sbase get chromedriver +sbase get geckodriver +sbase get edgedriver +sbase get chromedriver 149 +sbase get chromedriver 149.0.7827.155 +sbase get chromedriver stable +sbase get chromedriver beta +sbase get chromedriver -p +sbase get chromium +sbase get chromium --revision=1639046 +sbase get cft 149 +sbase get chs +``` + +* Drivers: + +```zsh +`chromedriver` , `cft`, `uc_driver` +`edgedriver` , `chs` , `geckodriver` +``` + +* Options: + +```zsh +NUM: A specific driver version or major version integer. + If not set, the driver version matches the browser. + +`-p` / `--path`: Also copy the driver to /usr/local/bin. +``` -For running tests from the command line, [use **pytest** with SeleniumBase](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/customizing_test_runs.md). +* Output: + +Downloads the webdriver to `seleniumbase/drivers/` + +(`chromedriver` is required for Chrome automation)
      +(`geckodriver` is required for Firefox automation)
      +(`edgedriver` is required for Edge automation) -### install +

      methods

      * Usage: -``seleniumbase install [DRIVER_NAME]`` - (Drivers: chromedriver, geckodriver, edgedriver, - iedriver, operadriver) -* Example: -``seleniumbase install chromedriver`` +```zsh +sbase methods +``` + +* Output: + +Displays common SeleniumBase Python methods. + +

      options

      + +* Usage: + +```zsh +sbase options +``` + +* Output: + +Displays common pytest command-line options +that are available when using SeleniumBase. + +```zsh +--browser=BROWSER (The web browser to use. Default is "chrome") +--edge / --firefox / --safari (Shortcut for browser selection.) +--headless (Run tests headlessly. Default mode on Linux OS.) +--demo (Slow down and visually see test actions as they occur.) +--slow (Slow down the automation. Faster than using Demo Mode.) +--rs / --reuse-session (Reuse browser session between tests.) +--crumbs (Clear all cookies between tests reusing a session.) +--maximize (Start tests with the web browser window maximized.) +--dashboard (Enable SeleniumBase\'s Dashboard at dashboard.html) +--incognito (Enable Chromium\'s Incognito mode.) +--guest (Enable Chromium\'s Guest Mode.) +--dark (Enable Chromium\'s Dark Mode.) +--uc (Use undetected-chromedriver to evade detection.) +-m=MARKER (Run tests with the specified pytest marker.) +-n=NUM (Multithread the tests using that many threads.) +-v (Verbose mode. Print the full names of each test run.) +--html=report.html (Create a detailed pytest-html report.) +--collect-only / --co (Only show discovered tests. No run.) +--co -q (Only show full names of discovered tests. No run.) +-x (Stop running tests after the first failure is reached.) +--pdb (Enter the Post Mortem Debug Mode after any test fails.) +--trace (Enter Debug Mode immediately after starting any test.) + | Debug Mode Commands >>> help / h: List all commands. | + | n: Next line of method. s: Step through. c: Continue. | + | return / r: Run until method returns. j: Jump to line. | + | where / w: Show stack spot. u: Up stack. d: Down stack. | + | longlist / ll: See code. dir(): List namespace objects. | +--help / -h (Display list of all available pytest options.) +--final-debug (Enter Final Debug Mode after each test ends.) +--recorder / --rec (Save browser actions as Python scripts.) +--rec-sb-mgr (Save Recorder actions as SB() context manager.) +--rec-sb-cdp (Save Recorder actions as Pure CDP Mode sb_cdp.) +--rec-behave / --rec-gherkin (Save actions as Gherkin code.) +--rec-print (Display recorded scripts when they are created.) +--save-screenshot (Save a screenshot at the end of each test.) +--archive-logs (Archive old log files instead of deleting them.) +--check-js (Check for JavaScript errors after page loads.) +--start-page=URL (The browser start page when tests begin.) +--agent=STRING (Modify the web browser\'s User-Agent string.) +--mobile (Use Chromium\'s mobile device emulator during tests.) +--metrics=STRING (Set mobile "CSSWidth,CSSHeight,PixelRatio".) +--ad-block (Block some types of display ads after page loads.) +--settings-file=FILE (Override default SeleniumBase settings.) +--env=ENV (Set the test env. Access with "self.env" in tests.) +--data=DATA (Extra test data. Access with "self.data" in tests.) +--disable-csp (Disable the Content Security Policy of websites.) +--remote-debug (Sync to Ch-R-Debugger chrome://inspect/#devices) +--server=SERVER (The Selenium Grid server/IP used for tests.) +--port=PORT (The Selenium Grid port used by the test server.) +--proxy=SERVER:PORT (Connect to a proxy server:port for tests.) +--proxy=USER:PASS@SERVER:PORT (Use authenticated proxy server.) + +For the full list of command-line options, type: "pytest --help". +``` + +

      behave-options

      + +* Usage: + +```zsh +sbase behave-options +``` * Output: -Installs the specified webdriver. -(chromedriver is required for Google Chrome automation) -(geckodriver is required for Mozilla Firefox automation) -(edgedriver is required for Microsoft Edge automation) -(iedriver is required for Internet Explorer automation) -(operadriver is required for Opera Browser automation) -### mkdir +Displays common Behave command-line options +that are available when using SeleniumBase. + +```zsh +-D browser=BROWSER (The web browser to use. Default is "chrome") +-D headless (Run tests headlessly. Default mode on Linux OS.) +-D demo (Slow down and visually see test actions as they occur.) +-D slow (Slow down the automation. Faster than using Demo Mode.) +-D reuse-session / -D rs (Reuse browser session between tests.) +-D crumbs (Clear all cookies between tests reusing a session.) +-D maximize (Start tests with the web browser window maximized.) +-D dashboard (Enable SeleniumBase\'s Dashboard at dashboard.html) +-D incognito (Enable Chromium\'s Incognito Mode.) +-D guest (Enable Chromium\'s Guest Mode.) +-D dark (Enable Chromium\'s Dark Mode.) +-D uc (Use undetected-chromedriver to evade detection.) +--no-snippets / -q (Quiet mode. Don\'t print snippets.) +--dry-run / -d (Dry run. Only show discovered tests.) +--stop (Stop running tests after the first failure is reached.) +-D pdb (Enter the Post Mortem Debug Mode after any test fails.) + | Debug Mode Commands >>> help / h: List all commands. | + | n: Next line of method. s: Step through. c: Continue. | + | return / r: Run until method returns. j: Jump to line. | + | where / w: Show stack spot. u: Up stack. d: Down stack. | + | longlist / ll: See code. dir(): List namespace objects. | +-D recorder (Record browser actions to generate test scripts.) +-D rec-print (Display recorded scripts when they are created.) +-D save-screenshot (Save a screenshot at the end of each test.) +-D archive-logs (Archive old log files instead of deleting them.) +-D check-js (Check for JavaScript errors after page loads.) +-D start-page=URL (The browser start page when tests begin.) +-D agent=STRING (Modify the web browser\'s User-Agent string.) +-D mobile (Use Chromium\'s mobile device emulator during tests.) +-D metrics=STRING (Set mobile "CSSWidth,CSSHeight,PixelRatio".) +-D ad-block (Block some types of display ads after page loads.) +-D settings-file=FILE (Override default SeleniumBase settings.) +-D env=ENV (Set the test env. Access with "self.env" in tests.) +-D data=DATA (Extra test data. Access with "self.data" in tests.) +-D disable-csp (Disable the Content Security Policy of websites.) +-D remote-debug (Sync to Ch-R-Debugger chrome://inspect/#devices) +-D server=SERVER (The Selenium Grid server/IP used for tests.) +-D port=PORT (The Selenium Grid port used by the test server.) +-D proxy=SERVER:PORT (Connect to a proxy server:port for tests.) +-D proxy=USER:PASS@SERVER:PORT (Use authenticated proxy server.) + +For the full list of command-line options, type: "behave --help". +``` + +

      gui / commander

      * Usage: -``seleniumbase mkdir [DIRECTORY_NAME]`` + +```zsh +sbase gui [OPTIONAL PATH or TEST FILE] +sbase commander [OPTIONAL PATH or TEST FILE] +``` + +

      behave-gui

      + +* Usage: + +```zsh +sbase behave-gui [OPTIONAL PATH or TEST FILE] +sbase gui-behave [OPTIONAL PATH or TEST FILE] +``` + +* Examples: + +```zsh +sbase behave-gui +sbase behave-gui -i=calculator +sbase behave-gui features/ +sbase behave-gui features/calculator.feature +``` + +* Output: + +Launches SeleniumBase Commander / GUI for Behave. + +

      caseplans

      + +* Usage: + +```zsh +sbase caseplans [OPTIONAL PATH or TEST FILE] +``` + +* Examples: + +```zsh +sbase caseplans +sbase caseplans -k agent +sbase caseplans -m marker2 +sbase caseplans test_suite.py +sbase caseplans offline_examples/ +``` + +* Output: + +Launches the SeleniumBase Case Plans Generator. + +

      mkdir

      + +* Usage: + +```zsh +sbase mkdir [DIRECTORY] [OPTIONS] +``` * Example: -``seleniumbase mkdir browser_tests`` + +```zsh +sbase mkdir ui_tests +``` + +* Options: + +```zsh +-b / --basic (Only config files. No tests added.) +--gha (Include GitHub Actions YML with defaults.) +``` * Output: -Creates a new folder for running SeleniumBase scripts. + +Creates a new folder for running SBase scripts. The new folder contains default config files, -sample tests for helping new users get started, and -Python boilerplates for setting up customized +sample tests for helping new users get started, +and Python boilerplates for setting up customized test frameworks. -### convert +```zsh +ui_tests/ +โ”œโ”€โ”€ __init__.py +โ”œโ”€โ”€ my_first_test.py +โ”œโ”€โ”€ parameterized_test.py +โ”œโ”€โ”€ pytest.ini +โ”œโ”€โ”€ requirements.txt +โ”œโ”€โ”€ setup.cfg +โ”œโ”€โ”€ test_demo_site.py +โ””โ”€โ”€ boilerplates/ + โ”œโ”€โ”€ __init__.py + โ”œโ”€โ”€ base_test_case.py + โ”œโ”€โ”€ boilerplate_test.py + โ”œโ”€โ”€ classic_obj_test.py + โ”œโ”€โ”€ page_objects.py + โ”œโ”€โ”€ sb_fixture_test.py + โ””โ”€โ”€ samples/ + โ”œโ”€โ”€ __init__.py + โ”œโ”€โ”€ google_objects.py + โ”œโ”€โ”€ google_test.py + โ”œโ”€โ”€ sb_swag_test.py + โ””โ”€โ”€ swag_labs_test.py +``` + +If running with the `-b` or `--basic` option: + +```zsh +ui_tests/ +โ”œโ”€โ”€ __init__.py +โ”œโ”€โ”€ pytest.ini +โ”œโ”€โ”€ requirements.txt +โ””โ”€โ”€ setup.cfg +``` + +If running with the `--gha` option, this is added: + +```zsh +ui_tests/ +โ””โ”€โ”€ .github + โ””โ”€โ”€ workflows/ + โ””โ”€โ”€ python-package.yml +``` + +

      mkfile

      * Usage: -``seleniumbase convert [PYTHON_WEBDRIVER_UNITTEST_FILE]`` + +```zsh +sbase mkfile [FILE.py] [OPTIONS] +``` + +* Example: + +```zsh +sbase mkfile new_test.py +``` + +* Options: + +```zsh +--uc (UC Mode boilerplate using SB context manager) +-b / --basic (Basic boilerplate / single-line test) +-r / --rec (Adds Pdb+ breakpoint for Recorder Mode) +--url=URL (Makes the test start on a specific page) +``` + +* Language Options: + +```zsh +--en / --English | --zh / --Chinese +--nl / --Dutch | --fr / --French +--it / --Italian | --ja / --Japanese +--ko / --Korean | --pt / --Portuguese +--ru / --Russian | --es / --Spanish +``` + +* Syntax Formats: + +```zsh +--bc / --basecase (BaseCase class inheritance) +--pf / --pytest-fixture (sb pytest fixture) +--cf / --class-fixture (class + sb pytest fixture) +--cm / --context-manager (SB context manager) +--dc / --driver-context (DriverContext manager) +--dm / --driver-manager (Driver manager) +``` * Output: -Converts a Selenium IDE exported WebDriver unittest file -into a SeleniumBase file. Adds ``_SB`` to the new -file name while keeping the original file intact. -Works with Katalon Recorder scripts. -See: http://www.katalon.com/automation-recorder -### extract-objects +Creates a new SBase test file with boilerplate code. +If the file already exists, an error is raised. +By default, uses English with BaseCase inheritance, +and creates a boilerplate with common SeleniumBase +methods: "open", "type", "click", "assert_element", +and "assert_text". If using the basic boilerplate +option, only the "open" method is included. Only the +BaseCase format supports Languages or Recorder Mode. +UC Mode automatically uses English with SB() format. + +

      mkrec / record / codegen

      * Usage: -``seleniumbase extract-objects [SELENIUMBASE_PYTHON_FILE]`` + +```zsh +sbase mkrec [FILE.py] [OPTIONS] +sbase codegen [FILE.py] [OPTIONS] +``` + +* Examples: + +```zsh +sbase mkrec new_test.py +sbase mkrec new_test.py --url=seleniumbase.io +sbase codegen new_test.py +sbase codegen new_test.py --url=wikipedia.org +``` + +* Options: + +```zsh +--url=URL (Sets the initial start page URL.) +--edge (Use Edge browser instead of Chrome.) +--gui / --headed (Use headed mode on Linux.) +--uc / --undetected (Use undetectable mode.) +--ee (Use SHIFT + ESC to end the recording.) +--overwrite (Overwrite file when it exists.) +--behave (Also output Behave/Gherkin files.) +``` * Output: + +Creates a new SeleniumBase test using the Recorder. +If the filename already exists, an error is raised. + +

      recorder

      + +* Usage: + +```zsh +sbase recorder [OPTIONS] +``` + +* Options: + +```zsh +--uc / --undetected (Use undetectable mode.) +--behave (Also output Behave/Gherkin files.) +``` + +* Output: + +Launches the SeleniumBase Recorder Desktop App. + +

      mkpres

      + +* Usage: + +```zsh +sbase mkpres [FILE.py] [LANG] +``` + +* Example: + +```zsh +sbase mkpres new_presentation.py --en +``` + +* Language Options: + +```zsh +--en / --English | --zh / --Chinese +--nl / --Dutch | --fr / --French +--it / --Italian | --ja / --Japanese +--ko / --Korean | --pt / --Portuguese +--ru / --Russian | --es / --Spanish +``` + +* Output: + +Creates a new presentation with 3 example slides. +If the file already exists, an error is raised. +By default, the slides are written in English, +and use "serif" theme with "slide" transition. +The slides can be used as a basic boilerplate. + +

      mkchart

      + +* Usage: + +```zsh +sbase mkchart [FILE.py] [LANG] +``` + +* Example: + +```zsh +sbase mkchart new_chart.py --en +``` + +* Language Options: + +```zsh +--en / --English | --zh / --Chinese +--nl / --Dutch | --fr / --French +--it / --Italian | --ja / --Japanese +--ko / --Korean | --pt / --Portuguese +--ru / --Russian | --es / --Spanish +``` + +* Output: + +Creates a new SeleniumBase chart presentation. +If the file already exists, an error is raised. +By default, the slides are written in English, +and use a "sky" theme with "slide" transition. +The chart can be used as a basic boilerplate. + +

      print

      + +* Usage: + +```zsh +sbase print [FILE] [OPTIONS] +``` + +* Options: + +```zsh +-n (Add line Numbers to the rows) +``` + +* Output: + +Prints the code/text of any file +with syntax-highlighting. + +

      translate

      + +* Usage: + +```zsh +sbase translate [SB_FILE.py] [LANGUAGE] [ACTION] +``` + +* Languages: + +```zsh +--en / --English | --zh / --Chinese +--nl / --Dutch | --fr / --French +--it / --Italian | --ja / --Japanese +--ko / --Korean | --pt / --Portuguese +--ru / --Russian | --es / --Spanish +``` + +* Actions: + +```zsh +-p / --print (Print translation output to the screen) +-o / --overwrite (Overwrite the file being translated) +-c / --copy (Copy the translation to a new `.py` file) +``` + +* Options: + +```zsh +-n (include line Numbers when using the Print action) +``` + +* Output: + +Translates a SeleniumBase Python file into the language +specified. Method calls and "import" lines get swapped. +Both a language and an action must be specified. +The `-p` action can be paired with one other action. +When running with `-c` (or `--copy`), the new file name +will be the original name appended with an underscore +plus the 2-letter language code of the new language. +(Example: Translating "test_1.py" into Japanese with +`-c` will create a new file called "test_1_ja.py".) + +

      extract-objects

      + +* Usage: + +```zsh +sbase extract-objects [SB_FILE.py] +``` + +* Output: + Creates page objects based on selectors found in a seleniumbase Python file and saves those objects to the "page_objects.py" file in the same folder as the tests. -### inject-objects +

      inject-objects

      * Usage: -``seleniumbase inject-objects [SELENIUMBASE_PYTHON_FILE] [OPTIONS]`` + +```zsh +sbase inject-objects [SB_FILE.py] [OPTIONS] +``` * Options: -``-c``, ``--comments`` (Add object selectors to the comments.) + +```zsh +-c / --comments (Add object selectors to the comments.) +``` * Output: + Takes the page objects found in the "page_objects.py" file and uses those to replace matching selectors in the selected seleniumbase Python file. -### objectify +

      objectify

      * Usage: -``seleniumbase objectify [SELENIUMBASE_PYTHON_FILE] [OPTIONS]`` + +```zsh +sbase objectify [SB_FILE.py] [OPTIONS] +``` * Options: -``-c``, ``--comments`` (Add object selectors to the comments.) + +```zsh +-c / --comments (Add object selectors to the comments.) +``` * Output: + A modified version of the file where the selectors have been replaced with variable names defined in "page_objects.py", supporting the Page Object Pattern. (This has the same outcome as combining -``extract-objects`` with ``inject-objects``) +`extract-objects` with `inject-objects`) -### revert-objects +

      revert-objects

      * Usage: -``seleniumbase revert-objects [SELENIUMBASE_PYTHON_FILE] [OPTIONS]`` + +```zsh +sbase revert-objects [SB_FILE.py] [OPTIONS] +``` * Options: -``-c``, ``--comments`` (Keep existing comments for the lines.) + +```zsh +-c / --comments (Keep existing comments for the lines.) +``` * Output: -Reverts the changes made by ``seleniumbase objectify ...`` or -``seleniumbase inject-objects ...`` when run against a + +Reverts the changes made by `seleniumbase objectify ...` or +`seleniumbase inject-objects ...` when run against a seleniumbase Python file. Objects will get replaced by selectors stored in the "page_objects.py" file. -### download +

      convert

      * Usage: -``seleniumbase download [ITEM]`` - (Options: server) -* Example: -``seleniumbase download server`` +```zsh +sbase convert [WEBDRIVER_UNITTEST_FILE.py] +``` * Output: -Downloads the specified item. -(server is required for using your own Selenium Grid) -### grid-hub +Converts a Selenium IDE exported WebDriver unittest +file into a SeleniumBase file. Adds `_SB` to the +new filename while keeping the original file intact. +Works on both Selenium IDE & Katalon Recorder scripts. + +

      encrypt / obfuscate

      * Usage: -``seleniumbase grid-hub {start|stop|restart}`` + +`sbase encrypt` / `sbase obfuscate` + +* Output: + +Runs the password encryption/obfuscation tool. +(Where you can enter a password to encrypt/obfuscate.) + +

      decrypt / unobfuscate

      + +* Usage: + +`sbase decrypt` / `sbase unobfuscate` + +* Output: + +Runs the password decryption/unobfuscation tool. +(Where you can enter an encrypted password to decrypt.) + +

      proxy

      + +* Usage: + +```zsh +sbase proxy [OPTIONS] +``` * Options: -``-v``, ``--verbose`` (Increases verbosity of logging output.) + +```zsh +--hostname=HOSTNAME (Set `hostname`) (Default: `127.0.0.1`) +--port=PORT (Set `port`) (Default: `8899`) +--help / -h (Display available `proxy` options.) +``` * Output: + +Launch a basic proxy server on the current machine. +(Uses `127.0.0.1:8899` as the default address.) + +

      download

      + +* Usage: + +```zsh +sbase download server +``` + +* Output: + +Downloads the Selenium Server JAR file for Grid usage. +(That JAR file is required when using a Selenium Grid) + +

      grid-hub

      + +* Usage: + +```zsh +sbase grid-hub [start|stop|restart] [OPTIONS] +``` + +* Options: + +```zsh +-v / --verbose (Increases verbosity of logging output.) +--timeout=TIMEOUT (Close idle browser windows after TIMEOUT seconds.) +``` + +* Output: + Controls the Selenium Grid Hub server, which allows for running tests on multiple machines in parallel to speed up test runs and reduce the total time of test suite execution. You can start, restart, or stop the Grid Hub server. -### grid-node +

      grid-node

      * Usage: -``seleniumbase grid-node {start|stop|restart} [OPTIONS]`` + +```zsh +sbase grid-node [start|stop|restart] [OPTIONS] +``` * Options: -``--hub=HUB_IP`` (The Grid Hub IP Address to connect to.) (Default: ``127.0.0.1``) -``-v``, ``--verbose`` (Increases verbosity of logging output.) + +```zsh +--hub=HUB_IP (Grid Hub IP Address. Default: `127.0.0.1`) +-v / --verbose (Increases verbosity of logging output.) +``` * Output: + Controls the Selenium Grid node, which serves as a worker machine for your Selenium Grid Hub server. You can start, restart, or stop the Grid node. + +-------- + + diff --git a/seleniumbase/console_scripts/__init__.py b/seleniumbase/console_scripts/__init__.py old mode 100755 new mode 100644 diff --git a/seleniumbase/console_scripts/logo_helper.py b/seleniumbase/console_scripts/logo_helper.py old mode 100755 new mode 100644 index 707e1b1e493..e85f19bbaff --- a/seleniumbase/console_scripts/logo_helper.py +++ b/seleniumbase/console_scripts/logo_helper.py @@ -3,18 +3,19 @@ http://www.patorjk.com/software/taag/#p=display&f=Slant&t=SeleniumBase """ import colorama +import os +from contextlib import suppress -r''' - ______ __ _ ____ - / ____/__ / /__ ____ (_)_ ______ ___ / _ \____ ________ - \__ \/ _ \/ / _ \/ __ \/ / / / / __ `__ \/ /_) / __ \/ ___/ _ \ - ___/ / __/ / __/ / / / / /_/ / / / / / / /_) / (_/ /__ / __/ -/____/\___/_/\___/_/ /_/_/\__,_/_/ /_/ /_/_____/\__,_/____/\___/ -''' +r""" + ______ __ _ ____ + / ____/__ / /__ ____ (_)_ ______ ___ / _ \____ ________ + \__ \/ _ \/ / _ \/ __ \/ / / / / __ `__ \ / /_) / __ \/ ___/ _ \ + ___/ / __/ / __/ / / / / /_/ / / / / / // /_) / (_/ /__ / __/ +/____/\___/_/\___/_/ /_/_/\__,_/_/ /_/ /_//_____/\__,_/____/\___/ +""" def get_seleniumbase_logo(): - colorama.init(autoreset=True) c1 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX c3 = colorama.Back.CYAN @@ -24,39 +25,84 @@ def get_seleniumbase_logo(): sb += cr sb += "\n" sb += c1 - sb += " ______ __ _ " + sb += " ______ __ _ " sb += c2 sb += "____ " sb += cr sb += "\n" sb += c1 - sb += " / ____/__ / /__ ____ (_)_ ______ ___ " + sb += " / ____/__ / /__ ____ (_)_ ______ ___ " sb += c2 sb += "/ _ \\____ ________ " sb += cr sb += "\n" sb += c1 - sb += " \\__ \\/ _ \\/ / _ \\/ __ \\/ / / / / __ `__ \\" + sb += " \\__ \\/ _ \\/ / _ \\/ __ \\/ / / / / __ `__ \\ " sb += c2 sb += "/ /_) / __ \\/ ___/ _ \\" sb += cr sb += "\n" sb += c1 - sb += " ___/ / __/ / __/ / / / / /_/ / / / / / " + sb += " ___/ / __/ / __/ / / / / /_/ / / / / / /" sb += c2 sb += "/ /_) / (_/ /__ / __/" sb += cr sb += "\n" sb += c1 - sb += "/____/\\___/_/\\___/_/ /_/_/\\__,_/_/ /_/ /_" + sb += "/____/\\___/_/\\___/_/ /_/_/\\__,_/_/ /_/ /_/" sb += c2 sb += "/_____/\\__,_/____/\\___/ " sb += cr sb += "\n" sb += c3 - sb += " " + sb += " " sb += c4 sb += " " sb += cr sb += cr + with suppress(Exception): + terminal_width = os.get_terminal_size().columns + if isinstance(terminal_width, int) and terminal_width >= 66: + return sb + + # If the logo is wider than the screen width, use a smaller one: + r""" + ___ _ _ ___ + / __| ___| |___ _ _ (_)_ _ _ __ | _ ) __ _ ______ + \__ \/ -_) / -_) ' \| | \| | ' \ | _ \/ _` (_-< -_) + |___/\___|_\___|_||_|_|\_,_|_|_|_\|___/\__,_/__|___| + """ + sb = " " + sb += cr + sb += "\n" + sb += c1 + sb += " ___ _ _ " + sb += c2 + sb += " ___ " + sb += cr + sb += "\n" + sb += c1 + sb += "/ __| ___| |___ _ _ (_)_ _ _ __ " + sb += c2 + sb += "| _ ) __ _ ______ " + sb += cr + sb += "\n" + sb += c1 + sb += "\\__ \\/ -_) / -_) ' \\| | \\| | ' \\ " + sb += c2 + sb += "| _ \\/ _` (_-< -_)" + sb += cr + sb += "\n" + sb += c1 + sb += "|___/\\___|_\\___|_||_|_|\\_,_|_|_|_\\" + sb += c2 + sb += "|___/\\__,_/__|___|" + sb += cr + sb += "\n" + sb += c3 + sb += " " + sb += c4 + sb += " " + sb += cr + sb += cr return sb diff --git a/seleniumbase/console_scripts/rich_helper.py b/seleniumbase/console_scripts/rich_helper.py new file mode 100644 index 00000000000..e53240a780e --- /dev/null +++ b/seleniumbase/console_scripts/rich_helper.py @@ -0,0 +1,90 @@ +from rich.console import Console +from rich.markdown import Markdown +from rich.syntax import Syntax + + +def process_syntax(code, lang, theme, line_numbers, code_width, word_wrap): + syntax = Syntax( + code, + lang, + theme=theme, + line_numbers=line_numbers, + code_width=code_width, + word_wrap=word_wrap, + ) + return syntax + + +def get_code_without_tag(code, tag): + # Remove TAG from Code/HTML, but keep inner text. + # Eg: Hello! becomes: "Hello!" + tag_start = "<%s " % tag + tag_solo = "<%s>" % tag + tag_end = "" % tag + while tag_start in code: + start = code.find(tag_start) + if start == -1: + break + end = code.find(">", start + 1) + 1 + if end == 0: + break + code = code[:start] + code[end:] + code = code.replace(tag_solo, "") + code = code.replace(tag_end, "") + return code + + +def display_markdown(code): + try: + markdown = Markdown(code) + console = Console() + console.print(markdown) + return True # Success + except Exception: + return False # Failure + + +def display_code(code): + try: + console = Console() + console.print(code) + return True # Success + except Exception: + return False # Failure + + +def fix_emoji_spacing(code): + try: + # Fix the display width of certain emojis that take up two spaces + double_width_emojis = [ + "๐Ÿ‘๏ธ", + "๐Ÿ—บ๏ธ", + "๐Ÿ–ผ๏ธ", + "๐Ÿ—„๏ธ", + "โ™ป๏ธ", + "๐Ÿ—‚๏ธ", + "๐Ÿ–ฅ๏ธ", + "๐Ÿ•น๏ธ", + "๐ŸŽž๏ธ", + "๐ŸŽ›๏ธ", + "๐ŸŽ–๏ธ", + "โ˜€๏ธ", + "โบ๏ธ", + "โ–ถ๏ธ", + "โ†˜๏ธ", + "โฌ‡๏ธ", + "โ†™๏ธ", + "โฌ…๏ธ", + "โ†–๏ธ", + "โฌ†๏ธ", + "โ†—๏ธ", + "โžก๏ธ", + ] + for emoji in double_width_emojis: + if emoji in code: + code = code.replace(emoji, emoji + " ") + code = code.replace("โœ…<", "โœ… <") + code = code.replace("โŒ<", "โŒ <") + except Exception: + pass + return code diff --git a/seleniumbase/console_scripts/run.py b/seleniumbase/console_scripts/run.py index 9772da513db..4eb97262a61 100644 --- a/seleniumbase/console_scripts/run.py +++ b/seleniumbase/console_scripts/run.py @@ -3,226 +3,982 @@ Usage: seleniumbase [COMMAND] [PARAMETERS] + OR sbase [COMMAND] [PARAMETERS] Examples: -seleniumbase install chromedriver -seleniumbase mkdir browser_tests -seleniumbase convert my_old_webdriver_unittest.py -seleniumbase download server -seleniumbase grid-hub start -seleniumbase grid-node start --hub=127.0.0.1 +sbase get chromedriver +sbase methods +sbase options +sbase commander +sbase behave-gui +sbase behave-options +sbase caseplans +sbase mkdir ui_tests +sbase mkfile new_test.py +sbase mkrec new_test.py +sbase mkrec new_test.py --url=wikipedia.org +sbase codegen new_test.py --url=wikipedia.org +sbase recorder +sbase record new_test.py +sbase record +sbase mkpres new_presentation.py +sbase mkchart new_chart.py +sbase convert webdriver_unittest_file.py +sbase print my_first_test.py -n +sbase translate my_first_test.py --zh -p +sbase extract-objects my_first_test.py +sbase inject-objects my_first_test.py +sbase objectify my_first_test.py +sbase revert-objects my_first_test.py +sbase encrypt +sbase decrypt +sbase proxy +sbase proxy --hostname=127.0.0.1 --port=8899 +sbase download server +sbase grid-hub start +sbase grid-node start --hub=127.0.0.1 """ - +import colorama import sys -from seleniumbase.console_scripts import logo_helper -from seleniumbase.console_scripts import sb_mkdir -from seleniumbase.console_scripts import sb_install -from seleniumbase.utilities.selenium_grid import download_selenium_server -from seleniumbase.utilities.selenium_grid import grid_hub -from seleniumbase.utilities.selenium_grid import grid_node -from seleniumbase.utilities.selenium_ide import convert_ide -from seleniumbase.utilities.selenium_ide import objectify - - -def show_usage(): - show_basic_usage() - print('Type "seleniumbase --help" for details on all commands.') - print('Type "seleniumbase help [COMMAND]" for specific command info.') - print('* (Use "pytest" for running tests) *\n') +import time +from contextlib import suppress +from seleniumbase.config import settings +from seleniumbase.fixtures import constants +from seleniumbase.fixtures import shared_utils def show_basic_usage(): + from seleniumbase.console_scripts import logo_helper + seleniumbase_logo = logo_helper.get_seleniumbase_logo() print(seleniumbase_logo) + time.sleep(0.044) print("") - print('Usage: "seleniumbase [COMMAND] [PARAMETERS]"') - print("Commands:") - print(" install [DRIVER_NAME]") - print(" mkdir [NEW_TEST_DIRECTORY_NAME]") - print(" convert [PYTHON_WEBDRIVER_UNITTEST_FILE]") - print(" extract-objects [SELENIUMBASE_PYTHON_FILE]") - print(" inject-objects [SELENIUMBASE_PYTHON_FILE] [OPTIONS]") - print(" objectify [SELENIUMBASE_PYTHON_FILE] [OPTIONS]") - print(" revert-objects [SELENIUMBASE_PYTHON_FILE]") - print(" download [ITEM]") - print(" grid-hub [start|stop|restart] [OPTIONS]") - print(" grid-node [start|stop|restart] --hub=[HUB_IP] [OPTIONS]") - print(' * (EXAMPLE: "seleniumbase install chromedriver") *') + time.sleep(0.034) + show_package_location() + time.sleep(0.034) + show_version_info() + time.sleep(0.034) print("") + time.sleep(0.468) # Enough time to see the logo & version + sc = "" + sc += "โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ\n" + sc += 'โ”‚ * USAGE: "seleniumbase [COMMAND] [PARAMETERS]" โ”‚\n' + sc += 'โ”‚ * OR: "sbase [COMMAND] [PARAMETERS]" โ”‚\n' + sc += "โ”‚ โ”‚\n" + sc += "โ”‚ COMMANDS: PARAMETERS / DESCRIPTIONS: โ”‚\n" + sc += "โ”‚ get / install [DRIVER_NAME] [OPTIONS] โ”‚\n" + sc += "โ”‚ methods (List common Python methods) โ”‚\n" + sc += "โ”‚ options (List common pytest options) โ”‚\n" + sc += "โ”‚ behave-options (List common behave options) โ”‚\n" + sc += "โ”‚ gui / commander [OPTIONAL PATH or TEST FILE] โ”‚\n" + sc += "โ”‚ behave-gui (SBase Commander for Behave) โ”‚\n" + sc += "โ”‚ caseplans [OPTIONAL PATH or TEST FILE] โ”‚\n" + sc += "โ”‚ mkdir [DIRECTORY] [OPTIONS] โ”‚\n" + sc += "โ”‚ mkfile [FILE.py] [OPTIONS] โ”‚\n" + sc += "โ”‚ mkrec / codegen [FILE.py] [OPTIONS] โ”‚\n" + sc += "โ”‚ recorder (Open Recorder Desktop App.) โ”‚\n" + sc += "โ”‚ record (If args: mkrec. Else: App.) โ”‚\n" + sc += "โ”‚ mkpres [FILE.py] [LANG] โ”‚\n" + sc += "โ”‚ mkchart [FILE.py] [LANG] โ”‚\n" + sc += "โ”‚ print [FILE] [OPTIONS] โ”‚\n" + sc += "โ”‚ translate [SB_FILE.py] [LANG] [ACTION] โ”‚\n" + sc += "โ”‚ convert [WEBDRIVER_UNITTEST_FILE.py] โ”‚\n" + sc += "โ”‚ extract-objects [SB_FILE.py] โ”‚\n" + sc += "โ”‚ inject-objects [SB_FILE.py] [OPTIONS] โ”‚\n" + sc += "โ”‚ objectify [SB_FILE.py] [OPTIONS] โ”‚\n" + sc += "โ”‚ revert-objects [SB_FILE.py] [OPTIONS] โ”‚\n" + sc += "โ”‚ encrypt / obfuscate โ”‚\n" + sc += "โ”‚ decrypt / unobfuscate โ”‚\n" + sc += "โ”‚ proxy (Start a basic proxy server) โ”‚\n" + sc += "โ”‚ download server (Get Selenium Grid JAR file) โ”‚\n" + sc += "โ”‚ grid-hub [start|stop] [OPTIONS] โ”‚\n" + sc += "โ”‚ grid-node [start|stop] --hub=[HOST/IP] โ”‚\n" + sc += "โ”‚ โ”‚\n" + sc += 'โ”‚ * EXAMPLE => "sbase get chromedriver stable" โ”‚\n' + sc += 'โ”‚ * For command info => "sbase help [COMMAND]" โ”‚\n' + sc += 'โ”‚ * For info on all commands => "sbase --help" โ”‚\n' + sc += "โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ" + sc += "" + bordered_sc = sc + if "linux" not in sys.platform: + c1 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + c4 = colorama.Fore.MAGENTA + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = sc.replace("seleniumbase", c1 + "selenium" + c2 + "base" + cr) + sc = sc.replace("sbase", c1 + "s" + c2 + "base" + cr) + sc = sc.replace("[COMMAND]", c3 + "[COMMAND]" + cr) + sc = sc.replace("--help", c4 + "--help" + cr) + sc = sc.replace("help", c4 + "help" + cr) + with suppress(Exception): + print(sc) + return + sc = bordered_sc.replace("โ•ฎ\n", "") + sc = sc.replace("โ•ญ", "").replace("โ•ฎ", "").replace("โ”‚", "") + sc = sc.replace("โ•ฐ", "").replace("โ•ฏ", "").replace("โ”€", "") + if "linux" not in sys.platform: + sc = sc.replace("seleniumbase", c1 + "selenium" + c2 + "base" + cr) + sc = sc.replace("sbase", c1 + "s" + c2 + "base" + cr) + sc = sc.replace("[COMMAND]", c3 + "[COMMAND]" + cr) + sc = sc.replace("--help", c4 + "--help" + cr) + sc = sc.replace("help", c4 + "help" + cr) + print(sc) def show_install_usage(): - print(" ** install **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = " " + c2 + "** " + c3 + "get / install" + c2 + " **" + cr + print(sc) + print("") + print("Usage:") + print(" seleniumbase install [DRIVER/BINARY] [OPTIONS]") + print(" OR: seleniumbase get [DRIVER/BINARY] [OPTIONS]") + print(" OR: sbase install [DRIVER/BINARY] [OPTIONS]") + print(" OR: sbase get [DRIVER/BINARY] [OPTIONS]") + print(" (Drivers: chromedriver, uc_driver,") + print(" edgedriver, geckodriver)") + print(" (Binaries: cft, chs, chromium)") + print("Options:") + print(" VERSION Driver/binary version to download.") + print(" (Otherwise detects the version)") + print(" --revision The Chromium revision to download.") + print(" (Otherwise downloads the latest)") + print(" -p OR --path Also copy driver to /usr/local/bin") + print("Examples:") + print(" sbase get chromedriver") + print(" sbase get geckodriver") + print(" sbase get edgedriver") + print(" sbase get chromedriver 149") + print(" sbase get chromedriver 149.0.7827.155") + print(" sbase get chromedriver stable") + print(" sbase get chromedriver beta") + print(" sbase get chromedriver -p") + print(" sbase get chromium") + print(" sbase get chromium --revision=1639046") + print(" sbase get cft 149") + print(" sbase get chs") + print("Output:") + print(" Downloads driver/binary to seleniumbase/drivers/") + print(" (chromedriver is for Selenium Chrome automation)") + print(" (geckodriver is for Selenium Firefox automation)") + print(" (edgedriver is for Selenium MS__Edge automation)") + print(" (cft is for the `Chrome for Testing` binary exe)") + print(" (chs is for the `Chrome-Headless-Shell` binary.)") + print("") + + +def show_commander_usage(): + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = " " + c2 + "** " + c3 + "commander" + c2 + " **" + cr + print(sc) print("") print(" Usage:") - print(" seleniumbase install [DRIVER_NAME]") - print(" (Drivers: chromedriver, geckodriver, edgedriver") - print(" iedriver, operadriver)") - print(" Example:") - print(" seleniumbase install chromedriver") + print(" seleniumbase commander [OPTIONAL PATH or TEST FILE]") + print(" OR: sbase commander [OPTIONAL PATH or TEST FILE]") + print(" OR: seleniumbase gui [OPTIONAL PATH or TEST FILE]") + print(" OR: sbase gui [OPTIONAL PATH or TEST FILE]") + print(" Examples:") + print(" sbase gui") + print(" sbase gui -k agent") + print(" sbase gui -m marker2") + print(" sbase gui test_suite.py") + print(" sbase gui offline_examples/") print(" Output:") - print(" Installs the specified webdriver.") - print(" (chromedriver is required for Chrome automation)") - print(" (geckodriver is required for Firefox automation)") - print(" (edgedriver is required for Microsoft Edge automation)") - print(" (iedriver is required for InternetExplorer automation)") - print(" (operadriver is required for Opera Browser automation)") + print(" Launches SeleniumBase Commander | GUI for pytest.") + print("") + + +def show_behave_gui_usage(): + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = " " + c2 + "** " + c3 + "behave-gui" + c2 + " **" + cr + print(sc) + print("") + print(" Usage:") + print(" seleniumbase behave-gui [OPTIONAL PATH or TEST FILE]") + print(" seleniumbase gui-behave [OPTIONAL PATH or TEST FILE]") + print(" OR: sbase behave-gui [OPTIONAL PATH or TEST FILE]") + print(" OR: sbase gui-behave [OPTIONAL PATH or TEST FILE]") + print(" Examples:") + print(" sbase behave-gui") + print(" sbase behave-gui features/") + print(" sbase behave-gui features/calculator.feature") + print(" Output:") + print(" Launches SeleniumBase Commander | GUI for Behave.") + print("") + + +def show_caseplans_usage(): + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = " " + c2 + "** " + c3 + "caseplans" + c2 + " **" + cr + print(sc) + print("") + print(" Usage:") + print(" seleniumbase caseplans [OPTIONAL PATH or TEST FILE]") + print(" OR: sbase caseplans [OPTIONAL PATH or TEST FILE]") + print(" Examples:") + print(" sbase caseplans") + print(" sbase caseplans -k agent") + print(" sbase caseplans -m marker2") + print(" sbase caseplans test_suite.py") + print(" sbase caseplans offline_examples/") + print(" Output:") + print(" Launches the SeleniumBase Case Plans Generator.") print("") def show_mkdir_usage(): - print(" ** mkdir **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = " " + c2 + "** " + c3 + "mkdir" + c2 + " **" + cr + print(sc) print("") print(" Usage:") - print(" seleniumbase mkdir [DIRECTORY_NAME]") + print(" seleniumbase mkdir [DIRECTORY] [OPTIONS]") + print(" OR: sbase mkdir [DIRECTORY] [OPTIONS]") print(" Example:") - print(" seleniumbase mkdir browser_tests") + print(" sbase mkdir ui_tests") + print(" Options:") + print(" -b / --basic (Only config files. No tests added.)") + print(" --gha (Include GitHub Actions YML with defaults.)") print(" Output:") - print(" Creates a new folder for running SeleniumBase scripts.") - print(" The new folder contains default config files,") - print(" sample tests for helping new users get started, and") - print(" Python boilerplates for setting up customized") - print(" test frameworks.") + print(" Creates a new folder for running SBase scripts.") + print(" The new folder contains default config files,") + print(" sample tests for helping new users get started,") + print(" and Python boilerplates for setting up customized") + print(" test frameworks.") + print("") + + +def show_mkfile_usage(): + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = " " + c2 + "** " + c3 + "mkfile" + c2 + " **" + cr + print(sc) + print("") + print(" Usage:") + print(" seleniumbase mkfile [FILE.py] [OPTIONS]") + print(" OR: sbase mkfile [FILE.py] [OPTIONS]") + print(" Example:") + print(" sbase mkfile new_test.py") + print(" Options:") + print(" --uc (UC Mode boilerplate using SB context manager)") + print(" -b / --basic (Basic boilerplate / single-line test)") + print(" -r / --rec (Adds Pdb+ breakpoint for Recorder Mode)") + print(" --url=URL (Makes the test start on a specific page)") + print(" Language Options:") + print(" --en / --English | --zh / --Chinese") + print(" --nl / --Dutch | --fr / --French") + print(" --it / --Italian | --ja / --Japanese") + print(" --ko / --Korean | --pt / --Portuguese") + print(" --ru / --Russian | --es / --Spanish") + print(" Syntax Formats:") + print(" --bc / --basecase (BaseCase class inheritance)") + print(" --pf / --pytest-fixture (sb pytest fixture)") + print(" --cf / --class-fixture (class + sb pytest fixture)") + print(" --cm / --context-manager (SB context manager)") + print(" --dc / --driver-context (DriverContext manager)") + print(" --dm / --driver-manager (Driver manager)") + print(" Output:") + print(" Creates a new SBase test file with boilerplate code.") + print(" If the file already exists, an error is raised.") + print(" By default, uses English with BaseCase inheritance,") + print(" and creates a boilerplate with common SeleniumBase") + print(' methods: "goto", "type", "click", "assert_element",') + print(' and "assert_text". If using the basic boilerplate') + print(' option, only the "goto" method is included. Only the') + print(" BaseCase format supports Languages or Recorder Mode.") + print(" UC Mode automatically uses English with SB() format.") + print("") + + +def show_mkrec_usage(): + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = " " + c2 + "** " + c3 + "mkrec" + c2 + " **" + cr + print(sc) + print("") + print(" Usage:") + print(" seleniumbase mkrec [FILE.py] [OPTIONS]") + print(" OR: sbase mkrec [FILE.py] [OPTIONS]") + print(" Examples:") + print(" sbase mkrec new_test.py") + print(" sbase mkrec new_test.py --url=wikipedia.org") + print(" Options:") + print(" --url=URL (Sets the initial start page URL.)") + print(" --edge (Use Edge browser instead of Chrome.)") + print(" --gui / --headed (Use headed mode on Linux.)") + print(" --uc / --undetected (Use undetectable mode.)") + print(" --ee (Use SHIFT + ESC to end the recording.)") + print(" --overwrite (Overwrite file when it exists.)") + print(" --behave (Also output Behave/Gherkin files.)") + print(" Output:") + print(" Creates a new SeleniumBase test using the Recorder.") + print(" If the filename already exists, an error is raised.") + print("") + + +def show_codegen_usage(): + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = " " + c2 + "** " + c3 + "codegen" + c2 + " **" + cr + print(sc) + print("") + print(" Usage:") + print(" seleniumbase codegen [FILE.py] [OPTIONS]") + print(" OR: sbase codegen [FILE.py] [OPTIONS]") + print(" Examples:") + print(" sbase codegen new_test.py") + print(" sbase codegen new_test.py --url=wikipedia.org") + print(" Options:") + print(" --url=URL (Sets the initial start page URL.)") + print(" --edge (Use Edge browser instead of Chrome.)") + print(" --gui / --headed (Use headed mode on Linux.)") + print(" --uc / --undetected (Use undetectable mode.)") + print(" --ee (Use SHIFT + ESC to end the recording.)") + print(" --overwrite (Overwrite file when it exists.)") + print(" --behave (Also output Behave/Gherkin files.)") + print(" Output:") + print(" Creates a new SeleniumBase test using the Recorder.") + print(" If the filename already exists, an error is raised.") + print("") + + +def show_recorder_usage(): + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = " " + c2 + "** " + c3 + "recorder" + c2 + " **" + cr + print(sc) + print("") + print(" Usage:") + print(" seleniumbase recorder [OPTIONS]") + print(" OR: sbase recorder [OPTIONS]") + print(" Options:") + print(" --uc / --undetected (Use undetectable mode.)") + print(" --behave (Also output Behave/Gherkin files.)") + print(" Output:") + print(" Launches the SeleniumBase Recorder Desktop App.") + print("") + + +def show_mkpres_usage(): + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = " " + c2 + "** " + c3 + "mkpres" + c2 + " **" + cr + print(sc) + print("") + print(" Usage:") + print(" seleniumbase mkpres [FILE.py] [LANG]") + print(" OR: sbase mkpres [FILE.py] [LANG]") + print(" Example:") + print(" sbase mkpres new_presentation.py --en") + print(" Language Options:") + print(" --en / --English | --zh / --Chinese") + print(" --nl / --Dutch | --fr / --French") + print(" --it / --Italian | --ja / --Japanese") + print(" --ko / --Korean | --pt / --Portuguese") + print(" --ru / --Russian | --es / --Spanish") + print(" Output:") + print(" Creates a new presentation with 3 example slides.") + print(" If the file already exists, an error is raised.") + print(" By default, the slides are written in English,") + print(' and use "serif" theme with "slide" transition.') + print(" The slides can be used as a basic boilerplate.") + print("") + + +def show_mkchart_usage(): + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = " " + c2 + "** " + c3 + "mkchart" + c2 + " **" + cr + print(sc) + print("") + print(" Usage:") + print(" seleniumbase mkchart [FILE.py] [LANG]") + print(" OR: sbase mkchart [FILE.py] [LANG]") + print(" Example:") + print(" sbase mkchart new_chart.py --en") + print(" Language Options:") + print(" --en / --English | --zh / --Chinese") + print(" --nl / --Dutch | --fr / --French") + print(" --it / --Italian | --ja / --Japanese") + print(" --ko / --Korean | --pt / --Portuguese") + print(" --ru / --Russian | --es / --Spanish") + print(" Output:") + print(" Creates a new SeleniumBase chart presentation.") + print(" If the file already exists, an error is raised.") + print(" By default, the slides are written in English,") + print(' and use a "sky" theme with "slide" transition.') + print(" The chart can be used as a basic boilerplate.") print("") def show_convert_usage(): - print(" ** convert **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = " " + c2 + "** " + c3 + "convert" + c2 + " **" + cr + print(sc) + print("") + print(" Usage:") + print(" seleniumbase convert [WEBDRIVER_UNITTEST_FILE.py]") + print(" OR: sbase convert [WEBDRIVER_UNITTEST_FILE.py]") + print(" Output:") + print(" Converts a Selenium IDE exported WebDriver unittest") + print(" file into a SeleniumBase file. Adds _SB to the new") + print(" file name while keeping the original file intact.") + print(" (Works with Katalon Recorder Selenium scripts.)") + print("") + + +def show_print_usage(): + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = " " + c2 + "** " + c3 + "print" + c2 + " **" + cr + print(sc) print("") print(" Usage:") - print(" seleniumbase convert [PYTHON_WEBDRIVER_UNITTEST_FILE]") + print(" seleniumbase print [FILE] [OPTIONS]") + print(" OR: sbase print [FILE] [OPTIONS]") + print(" Options:") + print(" -n (Add line Numbers to the rows)") + print(" Output:") + print(" Prints the code/text of any file") + print(" with syntax-highlighting.") + print("") + + +def show_translate_usage(): + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = " " + c2 + "** " + c3 + "translate" + c2 + " **" + cr + print(sc) + print("") + print(" Usage:") + print(" seleniumbase translate [SB_FILE.py] [LANG] [ACTION]") + print(" OR: sbase translate [SB_FILE.py] [LANG] [ACTION]") + print(" Languages:") + print(" --en / --English | --zh / --Chinese") + print(" --nl / --Dutch | --fr / --French") + print(" --it / --Italian | --ja / --Japanese") + print(" --ko / --Korean | --pt / --Portuguese") + print(" --ru / --Russian | --es / --Spanish") + print(" Actions:") + print(" -p / --print (Print translation output to the screen)") + print(" -o / --overwrite (Overwrite the file being translated)") + print(" -c / --copy (Copy the translation to a new .py file)") + print(" Options:") + print(" -n (include line Numbers when using the Print action)") print(" Output:") - print(" Converts a Selenium IDE exported WebDriver unittest") - print(" file into a SeleniumBase file. Adds _SB to the new") - print(" file name while keeping the original file intact.") - print(" Works with Katalon Recorder scripts.") - print(" See: http://www.katalon.com/automation-recorder") + print(" Translates a SeleniumBase Python file into the language") + print(' specified. Method calls and "import" lines get swapped.') + print(" Both a language and an action must be specified.") + print(' The "-p" action can be paired with one other action.') + print(' When running with "-c" (or "--copy"), the new file name') + print(" will be the original name appended with an underscore") + print(" plus the 2-letter language code of the new language.") + print(' (Example: Translating "test_1.py" into Japanese with') + print(' "-c" will create a new file called "test_1_ja.py".)') print("") def show_extract_objects_usage(): - print(" ** extract-objects **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = " " + c2 + "** " + c3 + "extract-objects" + c2 + " **" + cr + print(sc) print("") print(" Usage:") - print(" seleniumbase extract-objects [SELENIUMBASE_PYTHON_FILE]") + print(" seleniumbase extract-objects [SB_FILE.py]") + print(" OR: sbase extract-objects [SB_FILE.py]") print(" Output:") - print(" Creates page objects based on selectors found in a") - print(" seleniumbase Python file and saves those objects to the") - print(' "page_objects.py" file in the same folder as the tests.') + print(" Creates page objects based on selectors found in a") + print(" seleniumbase Python file and saves those objects to the") + print(' "page_objects.py" file in the same folder as the tests.') print("") def show_inject_objects_usage(): - print(" ** inject-objects **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = " " + c2 + "** " + c3 + "inject-objects" + c2 + " **" + cr + print(sc) print("") print(" Usage:") - print(" seleniumbase inject-objects [SELENIUMBASE_PYTHON_FILE]") + print(" seleniumbase inject-objects [SB_FILE.py] [OPTIONS]") + print(" OR: sbase inject-objects [SB_FILE.py] [OPTIONS]") print(" Options:") - print(" -c, --comments (Add object selectors to the comments.)") - print(" (Default: No added comments.)") + print(" -c, --comments (Add object selectors to the comments.)") + print(" (Default: No added comments.)") print(" Output:") - print(' Takes the page objects found in the "page_objects.py"') - print(' file and uses those to replace matching selectors in') - print(' the selected seleniumbase Python file.') + print(' Takes the page objects found in the "page_objects.py"') + print(" file and uses those to replace matching selectors in") + print(" the selected seleniumbase Python file.") print("") def show_objectify_usage(): - print(" ** objectify **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = " " + c2 + "** " + c3 + "objectify" + c2 + " **" + cr + print(sc) print("") print(" Usage:") - print(" seleniumbase objectify [SELENIUMBASE_PYTHON_FILE]") + print(" seleniumbase objectify [SB_FILE.py] [OPTIONS]") + print(" OR: sbase objectify [SB_FILE.py] [OPTIONS]") print(" Options:") - print(" -c, --comments (Add object selectors to the comments.)") - print(" (Default: No added comments.)") + print(" -c, --comments (Add object selectors to the comments.)") + print(" (Default: No added comments.)") print(" Output:") - print(' A modified version of the file where the selectors') - print(' have been replaced with variable names defined in') - print(' "page_objects.py", supporting the Page Object Pattern.') + print(" A modified version of the file where the selectors") + print(" have been replaced with variable names defined in") + print(' "page_objects.py", supporting the Page Object Pattern.') print("") - print(' (seleniumbase "objectify" has the same outcome as') - print(' combining "extract-objects" with "inject-objects")') + print(' (seleniumbase "objectify" has the same outcome as') + print(' combining "extract-objects" with "inject-objects")') print("") def show_revert_objects_usage(): - print(" ** revert-objects **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = " " + c2 + "** " + c3 + "revert-objects" + c2 + " **" + cr + print(sc) print("") print(" Usage:") - print(" seleniumbase revert-objects [SELENIUMBASE_PYTHON_FILE]") + print(" seleniumbase revert-objects [SB_FILE.py] [OPTIONS]") + print(" OR: sbase revert-objects [SB_FILE.py] [OPTIONS]") print(" Options:") - print(" -c, --comments (Keep existing comments for the lines.)") - print(" (Default: No comments are kept.)") + print(" -c, --comments (Keep existing comments for the lines.)") + print(" (Default: No comments are kept.)") + print(" Output:") + print(' Reverts the changes made by "seleniumbase objectify" or') + print(' "seleniumbase inject-objects" when run against a') + print(" seleniumbase Python file. Objects will get replaced by") + print(' selectors stored in the "page_objects.py" file.') + print("") + + +def show_encrypt_usage(): + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = " " + c2 + "** " + c3 + "encrypt OR obfuscate" + c2 + " **" + cr + print(sc) + print("") + print(" Usage:") + print(" seleniumbase encrypt || seleniumbase obfuscate") + print(" --OR--") + print(" sbase encrypt || sbase obfuscate") + print(" Output:") + print(" Runs the password encryption/obfuscation tool.") + print(" (Where you can enter a password to encrypt/obfuscate.)") + print("") + + +def show_decrypt_usage(): + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = " " + c2 + "** " + c3 + "decrypt OR unobfuscate" + c2 + " **" + cr + print(sc) + print("") + print(" Usage:") + print(" seleniumbase decrypt || seleniumbase unobfuscate") + print(" --OR--") + print(" sbase decrypt || sbase unobfuscate") print(" Output:") - print(' Reverts the changes made by "seleniumbase objectify" or') - print(' "seleniumbase inject-objects" when run against a') - print(' seleniumbase Python file. Objects will get replaced by') - print(' selectors stored in the "page_objects.py" file.') + print(" Runs the password decryption/unobfuscation tool.") + print(" (Where you can enter an encrypted password to decrypt.)") print("") def show_download_usage(): - print(" ** download **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = " " + c2 + "** " + c3 + "download" + c2 + " **" + cr + print(sc) print("") print(" Usage:") - print(" seleniumbase download [ITEM]") - print(" (Choices: server)") - print(" Example:") - print(" seleniumbase download server") + print(" seleniumbase download server") + print(" OR: sbase download server") print(" Output:") - print(" Downloads the specified item.") - print(" (server is required for using your own Selenium Grid)") + print(" Downloads the Selenium Standalone Server.") + print(" (Server is required for using your own Selenium Grid.)") print("") def show_grid_hub_usage(): - print(" ** grid-hub **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = " " + c2 + "** " + c3 + "grid-hub" + c2 + " **" + cr + print(sc) print("") print(" Usage:") - print(" seleniumbase grid-hub {start|stop|restart}") + print(" seleniumbase grid-hub {start|stop|restart} [OPTIONS]") + print(" OR: sbase grid-hub {start|stop|restart} [OPTIONS]") print(" Options:") - print(" -v, --verbose (Increase verbosity of logging output.)") - print(" (Default: Quiet logging / not verbose.)") + print(" -v, --verbose (Increase verbosity of logging output.)") + print(" (Default: Quiet logging / not verbose.)") + print(" --timeout=TIMEOUT (Close idle browser after TIMEOUT.)") + print(" (The default TIMEOUT: 230 seconds.)") + print(" (Use --timeout=0 to skip timeouts.)") + print(" Example:") + print(" seleniumbase grid-hub start") print(" Output:") - print(" Controls the Selenium Grid Hub Server, which allows") - print(" for running tests on multiple machines in parallel") - print(" to speed up test runs and reduce the total time") - print(" of test suite execution.") - print(" You can start, restart, or stop the Grid Hub server.") + print(" Controls the Selenium Grid Hub Server, which allows") + print(" for running tests on multiple machines in parallel") + print(" to speed up test runs and reduce the total time") + print(" of test suite execution.") + print(' You can "start" or "stop" the Grid Hub server.') print("") def show_grid_node_usage(): - print(" ** grid-node **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = " " + c2 + "** " + c3 + "grid-node" + c2 + " **" + cr + print(sc) print("") print(" Usage:") - print(" seleniumbase grid-node {start|stop|restart} [OPTIONS]") + print(" seleniumbase grid-node {start|stop|restart} [OPTIONS]") + print(" OR: sbase grid-node {start|stop|restart} [OPTIONS]") print(" Options:") - print(" --hub=HUB_IP (The Grid Hub IP Address to connect to.)") - print(" (Default: 127.0.0.1 if not set)") - print(" -v, --verbose (Increase verbosity of logging output.)") - print(" (Default: Quiet logging / not verbose.)") + print(" --hub=[HOST/IP] (The Grid Hub Hostname / IP Address.)") + print(" (Default: 127.0.0.1 if not set.)") + print(" -v, --verbose (Increase verbosity of logging output.)") + print(" (Default: Quiet logging / Not verbose.)") + print(" Example:") + print(" seleniumbase grid-node start --hub=127.0.0.1") print(" Output:") - print(" Controls the Selenium Grid node, which serves as a") - print(" worker machine for your Selenium Grid Hub server.") - print(" You can start, restart, or stop the Grid node.") + print(" Controls the Selenium Grid node, which serves as a") + print(" worker machine for your Selenium Grid Hub server.") + print(' You can "start" or "stop" the Grid node.') + print("") + + +def get_version_info(): + # from pkg_resources import get_distribution + # version = get_distribution("seleniumbase").version + from seleniumbase import __version__ + + version_info = None + c1 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sb_text = c1 + "selenium" + c2 + "base" + cr + version_info = "%s %s%s%s" % (sb_text, c3, __version__, cr) + return version_info + + +def show_version_info(): + version_info = get_version_info() + print("%s" % version_info) + + +def get_package_location(): + # from pkg_resources import get_distribution + # location = get_distribution("seleniumbase").location + import os + import seleniumbase + + location = os.path.dirname(os.path.realpath(seleniumbase.__file__)) + if location.endswith("seleniumbase"): + location = location[0 : -len("seleniumbase")] # noqa: E203 + return location + + +def show_package_location(): + location = get_package_location() + print("%s" % location) + + +def show_methods(): + c1 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + c4 = colorama.Fore.MAGENTA + colorama.Back.LIGHTYELLOW_EX + c5 = colorama.Fore.LIGHTRED_EX + colorama.Back.LIGHTGREEN_EX + cr = colorama.Style.RESET_ALL + sc = ( + "\n " + c2 + " ** " + c3 + " SeleniumBase Python Methods " + "" + c2 + " ** " + cr + ) + print(sc) + print("") + line = "Here are some common methods that come with SeleniumBase:" + line = c1 + line + cr + print(line) + line = "(Some optional args are not shown here)" + print(line) + print("") + sbm = "" + sbm += "*.goto(url) => Navigate the browser window to the URL.\n" + sbm += "*.type(selector, text) => Update the field with the text.\n" + sbm += "*.click(selector) => Click the element with the selector.\n" + sbm += "*.click_link(link_text) => Click the link containing text.\n" + sbm += "*.check_if_unchecked(selector) => Check checkbox if unchecked.\n" + sbm += "*.uncheck_if_checked(selector) => Uncheck checkbox if checked.\n" + sbm += "*.select_option_by_text(dropdown_selector, option)\n" + sbm += "*.hover_and_click(hover_selector, click_selector)\n" + sbm += "*.drag_and_drop(drag_selector, drop_selector)\n" + sbm += "*.choose_file(selector, file_path) => Choose a file to upload.\n" + sbm += "*.get_text(selector) => Get the text from the element.\n" + sbm += "*.get_current_url() => Get the URL of the current page.\n" + sbm += "*.get_page_source() => Get the HTML of the current page.\n" + sbm += "*.get_attribute(selector, attribute) => Get element attribute.\n" + sbm += "*.get_title() => Get the title of the current page.\n" + sbm += "*.go_back() => Navigate to the previous page in history.\n" + sbm += "*.switch_to_frame(frame) => Switch into the iframe container.\n" + sbm += "*.switch_to_default_content() => Exit all iframe containers.\n" + sbm += "*.switch_to_parent_frame() => Exit from the current iframe.\n" + sbm += "*.open_new_window() => Open a new window in the same browser.\n" + sbm += "*.switch_to_window(window) => Switch to the browser window.\n" + sbm += "*.switch_to_default_window() => Switch to the original window.\n" + sbm += "*.get_new_driver(OPTIONS) => Open a new driver with OPTIONS.\n" + sbm += "*.switch_to_driver(driver) => Switch to the browser driver.\n" + sbm += "*.switch_to_default_driver() => Switch to the original driver.\n" + sbm += "*.wait_for_element(selector) => Wait until element is visible.\n" + sbm += "*.wait_for_element_present(selector) => Until element in HTML.\n" + sbm += "*.is_element_visible(selector) => Return element visibility.\n" + sbm += "*.is_element_present(selector) => Return element is in HTML.\n" + sbm += "*.is_text_visible(text, selector) => Return text visibility.\n" + sbm += "*.is_checked(selector) => Return whether the box is checked.\n" + sbm += "*.sleep(seconds) => Do nothing for the given amount of time.\n" + sbm += "*.save_screenshot(name) => Save a screenshot in .png format.\n" + sbm += "*.assert_element(selector) => Verify the element is visible.\n" + sbm += "*.assert_text(text, selector) => Verify text in the element.\n" + sbm += "*.assert_exact_text(text, selector) => Verify text is exact.\n" + sbm += "*.assert_url(url) => Verify that the current URL is the URL.\n" + sbm += "*.assert_url_contains(substring) => Verify substring in URL.\n" + sbm += "*.assert_title(title) => Verify the title of the web page.\n" + sbm += "*.assert_title_contains(substring) => Verify STR in title.\n" + sbm += "*.assert_downloaded_file(file) => Verify file was downloaded.\n" + sbm += "*.assert_no_404_errors() => Verify there are no broken links.\n" + sbm += "*.assert_no_js_errors() => Verify there are no JS errors.\n" + sbm = sbm.replace("*.", "self." + c1).replace("(", cr + "(") + sbm = sbm.replace("self.", c2 + "self" + c5 + "." + cr) + sbm = sbm.replace("(", c3 + "(" + c4) + sbm = sbm.replace(")", c3 + ")" + cr) + print(sbm) + + +def show_options(): + c1 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + c4 = colorama.Fore.MAGENTA + colorama.Back.LIGHTYELLOW_EX + c5 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = "\n " + c2 + " ** " + c3 + " pytest CLI Options " + c2 + " ** " + cr + print(sc) + print("") + line = "Here are some common pytest options to use with SeleniumBase:" + line = c1 + line + cr + print(line) + line = '(Some options are Chromium-specific, e.g. "--guest --mobile")' + print(line) + op = "\n" + op += '--browser=BROWSER (Choice of web browser. Default is "chrome")\n' + op += "--edge / --firefox / --safari (Shortcut for browser selection)\n" + op += "--headless (Run tests headlessly. Default setting on Linux OS)\n" + op += "--demo (Slow down and visually see test actions as they occur)\n" + op += "--slow (Slow down the automation. Faster than using Demo Mode)\n" + op += "--rs / --reuse-session (Reuse browser session between tests.)\n" + op += "--reuse-class-session / --rcs (RS, but for class tests only.)\n" + op += "--crumbs (Clear all cookies between tests reusing a session.)\n" + op += "--maximize (Start tests with the browser window maximized)\n" + op += "--dashboard (Enable SeleniumBase Dashboard at dashboard.html)\n" + op += "--incognito (Enable Chromium's Incognito Mode.)\n" + op += "--guest (Enable Chromium's Guest Mode.)\n" + op += "--dark (Enable Chromium's Dark Mode.)\n" + op += "--uc (Use undetected-chromedriver to evade detection.)\n" + op += "-m=MARKER (Run tests with the specified pytest marker.)\n" + op += "-n=NUM (Multithread the tests using that many threads.)\n" + op += "-v (Verbose mode. Print the full names of each test run.)\n" + op += "--html=report.html (Create a detailed pytest-html report.)\n" + op += "--collect-only / --co (Only show discovered tests. No run.)\n" + op += "--co -q (Only show full names of discovered tests. No run.)\n" + op += "-x (Stop running tests after the first failure is reached.)\n" + op += "--pdb (Enter Post Mortem Debug Mode after any test fails.)\n" + op += "--trace (Enter Debug Mode immediately after starting tests.)\n" + op += " | Debug Mode Commands >>> help / h: List all commands. |\n" + op += " | n: Next line of method. s: Step into. c: Continue. |\n" + op += " | where / w: Show stack spot. u: Up stack. d: Down stack. |\n" + op += " | return / r: Run until method returns. j: Jump to line. |\n" + op += " | longlist / ll: See code. dir(): List namespace objects. |\n" + op += "--help / -h (Display list of all available pytest options.)\n" + op += "--ftrace / --final-trace (Enter Debug Mode after tests end.)\n" + op += "--recorder / --rec (Save browser actions as Python scripts.)\n" + op += "--rec-sb-mgr (Save Recorder actions as SB() context manager.)\n" + op += "--rec-sb-cdp (Save Recorder actions as Pure CDP Mode sb_cdp.)\n" + op += "--rec-behave / --rec-gherkin (Save actions as Gherkin code.)\n" + op += "--rec-print (Display recorded scripts when they are created.)\n" + op += "--save-screenshot (Save a screenshot at the end of each test.)\n" + op += "--archive-logs (Archive logs after tests to prevent deletion.)\n" + op += "--check-js (Check for JavaScript errors after page loads.)\n" + op += "--start-page=URL (The browser start page when tests begin.)\n" + op += "--agent=STRING (Modify the web browser's User-Agent string.)\n" + op += "--mobile (Use Chromium's mobile device emulator during tests.)\n" + op += '--metrics=STRING (Set mobile "CSSWidth,CSSHeight,PixelRatio".)\n' + op += "--ad-block (Block certain types of iframe ads from appearing.)\n" + op += "--settings-file=FILE (Override default SeleniumBase settings.)\n" + op += '--env=ENV (Set the test env. Use "self.env" to access.)\n' + op += '--data=DATA (Extra test data. Use "self.data" to access.)\n' + op += "--disable-csp (Disable the Content Security Policy of sites.)\n" + op += "--remote-debug (Sync to Ch_Debugger chrome://inspect/#devices)\n" + op += "--server=SERVER (The Selenium Grid server/IP used for tests.)\n" + op += "--port=PORT (The Selenium Grid port used by the test server.)\n" + op += "--proxy=SERVER:PORT (Connect to a proxy server:port for tests)\n" + op += "--proxy=USER:PASS@SERVER:PORT (Use authenticated proxy server)\n" + op += cr + op = op.replace("\n-", "\n" + c1 + "-").replace(" (", cr + " (") + op = op.replace(" / -", cr + " / " + c1 + "-") + op = op.replace("=", c2 + "=" + c3) + op = op.replace(" | ", " |" + c3 + " ").replace("|\n", cr + "|\n") + op = op.replace(": ", c5 + ":" + c3 + " ") + op = op.replace("Debug Mode Commands", c5 + "Debug Mode Commands" + c3) + op = op.replace(">>>", c4 + ">>>" + c3) + print(op) + line = "To view all " + c3 + "pytest" + cr + line += " " + c2 + "command-line options" + cr + line += ', type: "' + c3 + "pytest" + cr + " " + c1 + "--help" + cr + '".' + print(line) + print("") + + +def show_behave_options(): + c1 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + c4 = colorama.Fore.MAGENTA + colorama.Back.LIGHTYELLOW_EX + c5 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = "\n " + c2 + " ** " + c3 + " Behave CLI Options " + c2 + " ** " + cr + print(sc) + print("") + line = 'Here are some common "behave" options to use with SeleniumBase:' + line = c1 + line + cr + print(line) + line = '(Some options are Chromium-specific, e.g. "-D guest -D mobile")' + print(line) + op = "\n" + op += '-D browser=BROWSER (Choice of web browser. Default is "chrome")\n' + op += "-D headless (Run tests headlessly. Default mode on Linux OS.)\n" + op += "-D demo (Slow down and visually see test actions as they occur)\n" + op += "-D slow (Slow down the automation. Faster than using Demo Mode)\n" + op += "-D reuse-session / -D rs (Reuse browser session between tests.)\n" + op += "-D crumbs (Clear all cookies between tests reusing a session.)\n" + op += "-D maximize (Start tests with the web browser window maximized)\n" + op += "-D dashboard (Enable SeleniumBase Dashboard at dashboard.html)\n" + op += "-D incognito (Enable Chromium's Incognito Mode.)\n" + op += "-D guest (Enable Chromium's Guest Mode.)\n" + op += "-D dark (Enable Chromium's Dark Mode.)\n" + op += "-D uc (Use undetected-chromedriver to evade detection.)\n" + op += "--no-snippets / -q (Quiet mode. Don't print snippets.)\n" + op += "--dry-run / -d (Dry run. Only show discovered tests.)\n" + op += "--stop (Stop running tests after the first failure is reached.)\n" + op += "-D pdb (Enter the Post Mortem Debug Mode after any test fails.)\n" + op += " | Debug Mode Commands >>> help / h: List all commands. |\n" + op += " | n: Next line of method. s: Step through. c: Continue. |\n" + op += " | return / r: Run until method returns. j: Jump to line. |\n" + op += " | where / w: Show stack spot. u: Up stack. d: Down stack. |\n" + op += " | longlist / ll: See code. dir(): List namespace objects. |\n" + op += "-D recorder (Record browser actions to generate test scripts.)\n" + op += "-D rec-print (Display recorded scripts when they are created.)\n" + op += "-D save-screenshot (Save a screenshot at the end of each test.)\n" + op += "-D archive-logs (Archive log files instead of deleting them.)\n" + op += "-D check-js (Check for JavaScript errors after page loads.)\n" + op += "-D start-page=URL (The browser start page when tests begin.)\n" + op += "-D agent=STRING (Modify the web browser's User-Agent string.)\n" + op += "-D mobile (Use Chromium's mobile device emulator during tests.)\n" + op += '-D metrics=STRING (Set mobile "CSSWidth,CSSHeight,PixelRatio".)\n' + op += "-D ad-block (Block some types of display ads after page loads.)\n" + op += "-D settings-file=FILE (Override default SeleniumBase settings.)\n" + op += '-D env=ENV (Set the test env. Access using "self.env" in tests)\n' + op += '-D data=DATA (Extra test data. Access using "self.data".)\n' + op += "-D disable-csp (Disable the Content Security Policy of sites.)\n" + op += "-D remote-debug (Sync Ch-R-Debugger chrome://inspect/#devices)\n" + op += "-D server=SERVER (The Selenium Grid server/IP used for tests.)\n" + op += "-D port=PORT (The Selenium Grid port used by the test server.)\n" + op += "-D proxy=SERVER:PORT (Connect to a proxy server:port for tests)\n" + op += "-D proxy=USER:PASS@SERVER:PORT (Use authenticated proxy server)\n" + op += cr + op = op.replace("\n-", "\n" + c1 + "-").replace(" (", cr + " (") + op = op.replace(" / -", cr + " / " + c1 + "-") + op = op.replace("=", c2 + "=" + c3) + op = op.replace(" | ", " |" + c3 + " ").replace("|\n", cr + "|\n") + op = op.replace(": ", c5 + ":" + c3 + " ") + op = op.replace("Debug Mode Commands", c5 + "Debug Mode Commands" + c3) + op = op.replace(">>>", c4 + ">>>" + c3) + print(op) + line = "For the full list of " + c2 + "command-line options" + cr + line += ', type: "' + c3 + "behave" + cr + " " + c1 + "--help" + cr + '".' + print(line) print("") def show_detailed_help(): + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + c6 = colorama.Back.CYAN + cr = colorama.Style.RESET_ALL show_basic_usage() - print("More Info:") - print("") + print(c6 + " " + c2 + " Commands: " + c6 + " ") + print(cr) show_install_usage() + show_commander_usage() + show_behave_gui_usage() + show_caseplans_usage() show_mkdir_usage() + show_mkfile_usage() + show_mkrec_usage() + show_codegen_usage() + show_recorder_usage() + show_mkpres_usage() + show_mkchart_usage() show_convert_usage() + show_print_usage() + show_translate_usage() show_extract_objects_usage() show_inject_objects_usage() show_objectify_usage() show_revert_objects_usage() + show_encrypt_usage() + show_decrypt_usage() show_download_usage() show_grid_hub_usage() show_grid_node_usage() + print( + '* (Use "' + c3 + "pytest" + cr + '" or "' + c3 + '' + '' + "python" + cr + '" for running tests) *\n' + ) def main(): @@ -230,7 +986,7 @@ def main(): command_args = None num_args = len(sys.argv) if num_args == 1: - show_usage() + show_basic_usage() return elif num_args == 2: command = sys.argv[1] @@ -240,80 +996,308 @@ def main(): command_args = sys.argv[2:] command = command.lower() - if command == "install": + if command == "get" or command == "install": if len(command_args) >= 1: - sb_install.main() + from seleniumbase.console_scripts import sb_install + + need_retry = False + need_another_retry = False + retry_msg_1 = "* Unable to download driver! Retrying in 3s..." + retry_msg_2 = "** Unable to download driver! Retrying in 5s..." + if " --proxy=" in " ".join(sys.argv): + from seleniumbase.core import proxy_helper + for arg in sys.argv: + if arg.startswith("--proxy="): + proxy_string = arg.split("--proxy=")[1] + if "@" in proxy_string: + proxy_string = proxy_string.split("@")[1] + proxy_helper.validate_proxy_string(proxy_string) + break + try: + settings.HIDE_DRIVER_DOWNLOADS = False + sb_install.main() + except Exception as e: + invalid_run_cmd = constants.Warnings.INVALID_RUN_COMMAND + if invalid_run_cmd in str(e): + raise + print() + print(retry_msg_1) + time.sleep(3) + print() + need_retry = True + if need_retry: + try: + sb_install.main() + except Exception: + print(retry_msg_2) + time.sleep(5) + print() + need_another_retry = True + if need_another_retry: + sb_install.main() else: show_basic_usage() show_install_usage() + elif command == "commander" or command == "gui": + from seleniumbase.console_scripts import sb_commander + sb_commander.main() + elif command == "behave-gui" or command == "gui-behave": + from seleniumbase.console_scripts import sb_behave_gui + sb_behave_gui.main() + elif ( + command == "caseplans" + or command == "case-plans" + or command == "case_plans" + ): + from seleniumbase.console_scripts import sb_caseplans + sb_caseplans.main() + elif ( + command == "recorder" + or (command == "record" and len(command_args) == 0) + ): + from seleniumbase.console_scripts import sb_recorder + sb_recorder.main() + elif ( + command == "mkrec" + or command == "codegen" + or (command == "record" and len(command_args) >= 1) + ): + if len(command_args) >= 1: + from seleniumbase.console_scripts import sb_mkrec + sb_mkrec.main() + else: + show_basic_usage() + if command == "codegen": + show_codegen_usage() + else: + show_mkrec_usage() + elif command == "mkdir": + if len(command_args) >= 1: + from seleniumbase.console_scripts import sb_mkdir + sb_mkdir.main() + else: + show_basic_usage() + show_mkdir_usage() + elif command == "mkfile": + if len(command_args) >= 1: + from seleniumbase.console_scripts import sb_mkfile + sb_mkfile.main() + else: + show_basic_usage() + show_mkfile_usage() + elif command == "mkpres": + if len(command_args) >= 1: + from seleniumbase.console_scripts import sb_mkpres + sb_mkpres.main() + else: + show_basic_usage() + show_mkpres_usage() + elif command == "mkchart": + if len(command_args) >= 1: + from seleniumbase.console_scripts import sb_mkchart + sb_mkchart.main() + else: + show_basic_usage() + show_mkchart_usage() elif command == "convert": if len(command_args) == 1: + from seleniumbase.utilities.selenium_ide import convert_ide convert_ide.main() else: show_basic_usage() show_convert_usage() + elif command == "print": + if len(command_args) >= 1: + from seleniumbase.console_scripts import sb_print + sb_print.main() + else: + show_basic_usage() + show_print_usage() + elif command == "translate": + if len(command_args) >= 1: + from seleniumbase.translate import translator + translator.main() + else: + show_basic_usage() + show_translate_usage() elif command == "extract-objects" or command == "extract_objects": if len(command_args) >= 1: - objectify.extract_objects() + from seleniumbase.console_scripts import sb_objectify + sb_objectify.extract_objects() else: show_basic_usage() show_extract_objects_usage() elif command == "inject-objects" or command == "inject_objects": if len(command_args) >= 1: - objectify.inject_objects() + from seleniumbase.console_scripts import sb_objectify + + sb_objectify.inject_objects() else: show_basic_usage() show_inject_objects_usage() elif command == "objectify": if len(command_args) >= 1: - objectify.objectify() + from seleniumbase.console_scripts import sb_objectify + sb_objectify.objectify() else: show_basic_usage() show_objectify_usage() elif command == "revert-objects" or command == "revert_objects": if len(command_args) >= 1: - objectify.revert_objects() + from seleniumbase.console_scripts import sb_objectify + sb_objectify.revert_objects() else: show_basic_usage() show_revert_objects_usage() - elif command == "mkdir": - if len(command_args) >= 1: - sb_mkdir.main() + elif command == "encrypt" or command == "obfuscate": + if len(command_args) >= 0: + from seleniumbase.common import obfuscate + obfuscate.main() else: show_basic_usage() - show_mkdir_usage() + show_encrypt_usage() + elif command == "decrypt" or command == "unobfuscate": + if len(command_args) >= 0: + from seleniumbase.common import unobfuscate + unobfuscate.main() + else: + show_basic_usage() + show_decrypt_usage() elif command == "download": if len(command_args) >= 1 and command_args[0].lower() == "server": + from seleniumbase.utilities.selenium_grid import ( + download_selenium_server, + ) download_selenium_server.main(force_download=True) else: show_basic_usage() show_download_usage() elif command == "grid-hub" or command == "grid_hub": if len(command_args) >= 1: + from seleniumbase.utilities.selenium_grid import grid_hub grid_hub.main() else: show_basic_usage() show_grid_hub_usage() elif command == "grid-node" or command == "grid_node": if len(command_args) >= 1: + from seleniumbase.utilities.selenium_grid import grid_node grid_node.main() else: show_basic_usage() show_grid_node_usage() - elif command == "help" or command == "--help": + elif command == "version" or command == "--version" or command == "-v": + if len(command_args) == 0: + from seleniumbase.console_scripts import logo_helper + seleniumbase_logo = logo_helper.get_seleniumbase_logo() + print(seleniumbase_logo) + print("") + show_package_location() + show_version_info() + print("") + else: + show_basic_usage() + elif command == "methods" or command == "--methods": + show_methods() + elif command == "options" or command == "--options": + show_options() + elif command == "behave-options" or command == "--behave-options": + show_behave_options() + elif command == "proxy" or command == "--proxy": + import fasteners + import os + import warnings + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=UserWarning) + pip_find_lock = fasteners.InterProcessLock( + constants.PipInstall.FINDLOCK + ) + with pip_find_lock: + try: + from proxy import proxy # noqa: F401 + except Exception: + shared_utils.pip_install( + "proxy.py", version=constants.ProxyPy.VER + ) + os.system("proxy %s" % " ".join(sys.argv[2:])) + elif command == "help" or command == "--help" or command == "-h": if len(command_args) >= 1: - if command_args[0] == "install": + if command_args[0] == "get": + print("") + show_install_usage() + return + elif command_args[0] == "install": print("") show_install_usage() return + elif command_args[0] == "commander": + print("") + show_commander_usage() + return + elif command_args[0] == "gui": + print("") + show_commander_usage() + return + elif command_args[0] == "behave-gui": + print("") + show_behave_gui_usage() + return + elif command_args[0] == "gui-behave": + print("") + show_behave_gui_usage() + return + elif command_args[0] == "caseplans": + print("") + show_caseplans_usage() + return + elif command_args[0] == "case-plans": + print("") + show_caseplans_usage() + return + elif command_args[0] == "case_plans": + print("") + show_caseplans_usage() + return elif command_args[0] == "mkdir": print("") show_mkdir_usage() return + elif command_args[0] == "mkfile": + print("") + show_mkfile_usage() + return + elif command_args[0] == "mkrec": + print("") + show_mkrec_usage() + return + elif command_args[0] == "codegen": + print("") + show_codegen_usage() + return + elif command_args[0] == "recorder": + print("") + show_recorder_usage() + return + elif command_args[0] == "mkpres": + print("") + show_mkpres_usage() + return + elif command_args[0] == "mkchart": + print("") + show_mkchart_usage() + return elif command_args[0] == "convert": print("") show_convert_usage() return + elif command_args[0] == "print": + print("") + show_print_usage() + return + elif command_args[0] == "translate": + print("") + show_translate_usage() + return elif command_args[0] == "extract-objects": print("") show_extract_objects_usage() @@ -330,6 +1314,22 @@ def main(): print("") show_revert_objects_usage() return + elif command_args[0] == "encrypt": + print("") + show_encrypt_usage() + return + elif command_args[0] == "obfuscate": + print("") + show_encrypt_usage() + return + elif command_args[0] == "decrypt": + print("") + show_decrypt_usage() + return + elif command_args[0] == "unobfuscate": + print("") + show_decrypt_usage() + return elif command_args[0] == "download": print("") show_download_usage() @@ -344,7 +1344,16 @@ def main(): return show_detailed_help() else: - show_usage() + show_basic_usage() + c5 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + c7 = colorama.Fore.BLACK + colorama.Back.MAGENTA + cr = colorama.Style.RESET_ALL + invalid_cmd = "===> INVALID COMMAND: >> %s <<\n" % command + invalid_cmd = invalid_cmd.replace(">> ", ">>" + c5 + " ") + invalid_cmd = invalid_cmd.replace(" <<", " " + cr + "<<") + invalid_cmd = invalid_cmd.replace(">>", c7 + ">>" + cr) + invalid_cmd = invalid_cmd.replace("<<", c7 + "<<" + cr) + print(invalid_cmd) if __name__ == "__main__": diff --git a/seleniumbase/console_scripts/sb_behave_gui.py b/seleniumbase/console_scripts/sb_behave_gui.py new file mode 100644 index 00000000000..fbdbfd0e993 --- /dev/null +++ b/seleniumbase/console_scripts/sb_behave_gui.py @@ -0,0 +1,484 @@ +""" +Launches SeleniumBase Behave Commander | GUI for Behave. + +Usage: + seleniumbase behave-gui [OPTIONAL PATH or TEST FILE] + sbase behave-gui [OPTIONAL PATH or TEST FILE] + +Examples: + sbase behave-gui + sbase behave-gui features/ + sbase behave-gui features/calculator.feature + +Output: + Launches SeleniumBase Behave Commander | GUI for Behave. +""" +import colorama +import subprocess +import sys +import tkinter as tk +from seleniumbase.fixtures import shared_utils +from tkinter.scrolledtext import ScrolledText + + +def set_colors(use_colors): + c0 = "" + c1 = "" + c2 = "" + c3 = "" + c4 = "" + c5 = "" + c6 = "" + cr = "" + if use_colors: + c0 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX + c1 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c2 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + c3 = colorama.Fore.BLACK + colorama.Back.LIGHTGREEN_EX + c4 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + c5 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + c6 = colorama.Fore.MAGENTA + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + return c0, c1, c2, c3, c4, c5, c6, cr + + +def send_window_to_front(root): + root.lift() + root.attributes("-topmost", True) + root.after_idle(root.attributes, "-topmost", False) + + +def do_behave_run( + root, + tests, + selected_tests, + command_string, + browser_string, + rs_string, + quiet_mode, + demo_mode, + mobile_mode, + dashboard, + headless, + save_screenshots, + additional_options, +): + total_tests = len(tests) + total_selected_tests = 0 + test_to_run = None + for selected_test in selected_tests: + if selected_tests[selected_test].get(): + total_selected_tests += 1 + + full_run_command = '"%s" -m behave' % sys.executable + if total_selected_tests == 0 or total_tests == total_selected_tests: + if command_string: + full_run_command += " " + full_run_command += command_string + else: + for test_number, test in enumerate(tests): + if selected_tests[test_number].get(): + full_run_command += " " + test_to_run = test + if test.startswith("(GROUP) "): + test_to_run = test.split("(GROUP) ")[1] + full_run_command += test_to_run.split(" => ")[0] + else: + full_run_command += test.split(" => ")[0] + + if "(-D edge)" in browser_string: + full_run_command += " -D edge" + elif "(-D firefox)" in browser_string: + full_run_command += " -D firefox" + elif "(-D safari)" in browser_string: + full_run_command += " -D safari" + + if "(-D rs)" in rs_string: + full_run_command += " -D rs" + elif "(-D rs -D crumbs)" in rs_string: + full_run_command += " -D rs -D crumbs" + elif "(-D rcs)" in rs_string: + full_run_command += " -D rcs" + elif "(-D rcs -D crumbs)" in rs_string: + full_run_command += " -D rcs -D crumbs" + + if quiet_mode: + full_run_command += " --quiet" + + if demo_mode: + full_run_command += " -D demo" + + if mobile_mode: + full_run_command += " -D mobile" + + if dashboard: + full_run_command += " -D dashboard" + + if headless: + full_run_command += " -D headless" + elif shared_utils.is_linux(): + full_run_command += " -D gui" + + if save_screenshots: + full_run_command += " -D screenshot" + + additional_options_list = additional_options.split(" ") + dash_T_needed = False + if ( + "-T" not in additional_options_list + and "--no-timings" not in additional_options_list + and "--show-timings" not in additional_options_list + ): + dash_T_needed = True + dash_k_needed = False + if ( + "-k" not in additional_options_list + and "--no-skipped" not in additional_options_list + and "--show-skipped" not in additional_options_list + ): + dash_k_needed = True + additional_options = additional_options.strip() + if additional_options: + full_run_command += " " + full_run_command += additional_options + if dash_T_needed: + full_run_command += " -T" + if dash_k_needed: + full_run_command += " -k" + + print(full_run_command) + if not additional_options or " " not in additional_options: + subprocess.Popen(full_run_command, shell=True) + else: + proc = subprocess.Popen( + full_run_command, stderr=subprocess.PIPE, shell=True + ) + (output, error) = proc.communicate() + if error and proc.returncode == 2: + if str(error).startswith("b'") and str(error).endswith("\\n'"): + error = str(error)[2:-3] + elif str(error).startswith("b'") and str(error).endswith("'"): + error = str(error)[2:-1] + else: + error = str(error) + error = error.replace("\\n", "\n") + print(error) + send_window_to_front(root) + + +def create_tkinter_gui(tests, command_string, t_count, f_count, s_tests): + root = tk.Tk() + root.title("SeleniumBase Behave Commander | GUI for Behave") + if shared_utils.is_windows(): + root.minsize(820, 640) + else: + root.minsize(820, 656) + tk.Label(root, text="").pack() + + options_list = [ + "Use Chrome Browser (Default)", + "Use Edge Browser (-D edge)", + "Use Firefox Browser (-D firefox)", + ] + if shared_utils.is_mac(): + options_list.append("Use Safari Browser (-D safari)") + brx = tk.StringVar(root) + brx.set(options_list[0]) + question_menu = tk.OptionMenu(root, brx, *options_list) + question_menu.pack() + + options_list = [ + "New Session Per Test (Default)", + "Reuse Session for ALL the tests (-D rs)", + "Reuse Session and clear cookies (-D rs -D crumbs)", + "Reuse Session in the SAME class/feature (-D rcs)", + "Reuse Session in class and clear cookies (-D rcs -D crumbs)", + ] + rsx = tk.StringVar(root) + rsx.set(options_list[0]) + question_menu = tk.OptionMenu(root, rsx, *options_list) + question_menu.pack() + + qmx = tk.IntVar() + chk = tk.Checkbutton( + root, text="Quiet Mode (--quiet)", variable=qmx, pady=0 + ) + chk.pack() + + dmx = tk.IntVar() + chk = tk.Checkbutton( + root, text="Demo Mode (-D demo)", variable=dmx, pady=0 + ) + chk.pack() + + mmx = tk.IntVar() + chk = tk.Checkbutton( + root, text="Mobile Mode (-D mobile)", variable=mmx, pady=0 + ) + chk.pack() + + dbx = tk.IntVar() + chk = tk.Checkbutton( + root, text="Dashboard (-D dashboard)", variable=dbx, pady=0 + ) + chk.pack() + chk.select() + + hbx = tk.IntVar() + chk = tk.Checkbutton( + root, text="Headless Browser (-D headless)", variable=hbx, pady=0 + ) + chk.pack() + + ssx = tk.IntVar() + chk = tk.Checkbutton( + root, text="Save Screenshots (-D screenshot)", variable=ssx, pady=0 + ) + chk.pack() + + tk.Label(root, text="").pack() + plural = "s" + if f_count == 1: + plural = "" + run_display = ( + "Select from %s rows (%s feature%s with %s scenarios): " + "(All tests will run if none are selected)" + % (len(tests), f_count, plural, t_count) + ) + if t_count == 1: + run_display = "Only ONE TEST was found and will be run:" + tests = s_tests + tk.Label(root, text=run_display, bg="yellow", fg="magenta").pack() + text_area = ScrolledText( + root, width=100, height=12, wrap="word", state=tk.DISABLED + ) + text_area.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + count = 0 + ara = {} + for row in tests: + row += " " * 200 + ara[count] = tk.IntVar() + cb = None + if shared_utils.is_windows(): + cb = tk.Checkbutton( + text_area, + text=(row), + bg="white", + fg="black", + anchor="w", + pady=0, + borderwidth=1, + highlightthickness=1, + variable=ara[count], + ) + else: + cb = tk.Checkbutton( + text_area, + text=(row), + bg="white", + fg="black", + anchor="w", + pady=0, + variable=ara[count], + ) + text_area.window_create("end", window=cb) + text_area.insert("end", "\n") + count += 1 + + tk.Label(root, text="").pack() + additional_options = "" + aopts = tk.StringVar(value=additional_options) + tk.Label( + root, + text='Additional "behave" Options: (Eg. "-D incognito --junit")', + bg="yellow", fg="blue", + ).pack() + entry = tk.Entry(root, textvariable=aopts) + entry.pack() + entry.focus() + entry.bind( + "", + ( + lambda _: do_behave_run( + root, + tests, + ara, + command_string, + brx.get(), + rsx.get(), + qmx.get(), + dmx.get(), + mmx.get(), + dbx.get(), + hbx.get(), + ssx.get(), + aopts.get(), + ) + ), + ) + tk.Button( + root, + text="Run Selected Tests", + fg="green", + command=lambda: do_behave_run( + root, + tests, + ara, + command_string, + brx.get(), + rsx.get(), + qmx.get(), + dmx.get(), + mmx.get(), + dbx.get(), + hbx.get(), + ssx.get(), + aopts.get(), + ), + ).pack() + tk.Label(root, text="\n").pack() + + # Bring form window to front + send_window_to_front(root) + # Use decoy to set correct focus on main window + decoy = tk.Tk() + decoy.geometry("1x1") + decoy.iconify() + decoy.update() + decoy.deiconify() + decoy.destroy() + # Start tkinter + root.mainloop() + + +def main(): + use_colors = True + if shared_utils.is_linux(): + use_colors = False + c0, c1, c2, c3, c4, c5, c6, cr = set_colors(use_colors) + command_args = sys.argv[2:] + command_string = " ".join(command_args) + message = "" + message += c2 + message += "*" + message += c4 + message += " Starting the " + message += c0 + message += "Selenium" + message += c1 + message += "Base" + message += c2 + message += " " + message += c6 + message += "Behave" + message += c4 + message += " " + message += c3 + message += "Commander" + message += c4 + message += " GUI App" + message += c2 + message += "..." + message += cr + print(message) + command_string = command_string.replace("--quiet", "") + command_string = command_string.replace("-q", "") + proc = subprocess.Popen( + '"%s" -m behave -d %s --show-source' + % (sys.executable, command_string), + stdout=subprocess.PIPE, + shell=True, + ) + (output, error) = proc.communicate() + if error: + error_msg = "Error collecting tests: %s" % str(error) + error_msg = c5 + error_msg + cr + print(error_msg) + return + filename = None + feature_name = None + scenario_name = None + f_tests = [] # Features + s_tests = [] # Scenarios + tests = [] # All tests + file_scenario_count = {} + f_count = 0 + s_count = 0 + t_count = 0 + if shared_utils.is_windows(): + output = output.decode("latin1") + else: + output = output.decode("utf-8") + for row in output.replace("\r", "").split("\n"): + if row.startswith("Feature: "): + if f_count > 0: + file_scenario_count[str(f_count)] = s_count + f_count += 1 + s_count = 0 + elif row.startswith(" Scenario: "): + s_count += 1 + file_scenario_count[str(f_count)] = s_count + elif row.startswith(" Scenario Outline: "): + s_count += 1 + file_scenario_count[str(f_count)] = s_count + file_scenario_count[str(f_count)] = s_count + f_count = 0 + s_count = 0 + for row in output.replace("\r", "").split("\n"): + if row.startswith("Feature: "): + f_count += 1 + feature_name = row.split("Feature: ")[1] + if " # features/" in feature_name: + filename = feature_name.split(" # features/")[-1] + filename = "features/" + filename.split(":")[0] + feature_name = feature_name.split(" # features/")[0] + elif " # features\\" in feature_name: + filename = feature_name.split(" # features\\")[-1] + filename = "features\\" + filename.split(":")[0] + feature_name = feature_name.split(" # features\\")[0] + else: + filename = feature_name.split(" # ")[-1] + filename = filename.split(":")[0] + feature_name = feature_name.split(" # ")[-1] + s_count = file_scenario_count[str(f_count)] + filename = filename.strip() + t_name = "(GROUP) %s => %s" % (filename, feature_name) + t_name += " <> (%s Total)" % s_count + f_tests.append(t_name) + elif ( + row.startswith(" Scenario: ") + or row.startswith(" Scenario Outline: ") + ): + t_count += 1 + line_num = row.split(":")[-1] + scenario_name = None + if row.startswith(" Scenario: "): + scenario_name = row.split(" Scenario: ")[-1] + else: + scenario_name = row.split(" Scenario Outline: ")[-1] + if " -- @" in scenario_name: + scenario_name = scenario_name.split(" # ")[0].rstrip() + elif " # features/" in scenario_name: + scenario_name = scenario_name.split(" # features/")[0] + else: + scenario_name = scenario_name.split(" # ")[0] + scenario_name = scenario_name.strip() + s_tests.append("%s:%s => %s" % (filename, line_num, scenario_name)) + tests = f_tests + s_tests + if not tests: + err_msg_0 = c5 + "ERROR:" + cr + "\n" + err_msg_1 = ' No "behave" tests found! Expecting "*.feature" files!' + err_msg_1 = c6 + err_msg_1 + cr + "\n" + err_msg_2 = ' "*.feature" files would live in a "features/" folder.' + err_msg_2 = c6 + err_msg_2 + cr + "\n" + err_msg_3 = "Exiting SBase Behave Commander..." + err_msg_3 = c5 + err_msg_3 + cr + error_msg = err_msg_0 + err_msg_1 + err_msg_2 + err_msg_3 + print(error_msg) + return + + create_tkinter_gui(tests, command_string, t_count, f_count, s_tests) + + +if __name__ == "__main__": + print('To open SBase Behave Commander, type "sbase behave-gui"') diff --git a/seleniumbase/console_scripts/sb_caseplans.py b/seleniumbase/console_scripts/sb_caseplans.py new file mode 100644 index 00000000000..68e1b2804f4 --- /dev/null +++ b/seleniumbase/console_scripts/sb_caseplans.py @@ -0,0 +1,547 @@ +""" +Launches the SeleniumBase Case Plans Generator. + +Usage: + seleniumbase caseplans [OPTIONAL PATH or TEST FILE] + sbase caseplans [OPTIONAL PATH or TEST FILE] + +Examples: + sbase caseplans + sbase caseplans -k agent + sbase caseplans -m marker2 + sbase caseplans test_suite.py + sbase caseplans offline_examples/ + +Output: + Launches the SeleniumBase Case Plans Generator. +""" +import colorama +import os +import subprocess +import sys +import tkinter as tk +from seleniumbase.fixtures import shared_utils +from tkinter import messagebox +from tkinter.scrolledtext import ScrolledText + + +def set_colors(use_colors): + c0 = "" + c1 = "" + c2 = "" + c3 = "" + c4 = "" + c5 = "" + cr = "" + if use_colors: + c0 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX + c1 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c2 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + c3 = colorama.Fore.BLACK + colorama.Back.LIGHTCYAN_EX + c4 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + c5 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + return c0, c1, c2, c3, c4, c5, cr + + +def send_window_to_front(root): + root.lift() + root.attributes("-topmost", True) + root.after_idle(root.attributes, "-topmost", False) + + +def show_no_case_plans_warning(): + messagebox.showwarning( + "No existing Case Plans found!", + "\nNo existing Case Plans found!!\n\nCreate some boilerplates first!", + ) + + +def get_test_id(display_id): + """The id used in various places such as the test log path.""" + return ( + display_id.replace(".py::", ".").replace("::", ".").replace(" ", "_") + ) + + +def generate_case_plan_boilerplates( + root, + tests, + selected_tests, + tests_with_case_plan, + tests_without_case_plan, +): + total_tests = len(tests) + total_selected_tests = 0 + for selected_test in selected_tests: + if selected_tests[selected_test].get(): + total_selected_tests += 1 + + test_cases = [] + case_plans_to_create = [] + if total_selected_tests == 0: + messagebox.showwarning( + "No tests were selected!", + "\nโ„น๏ธ No tests were selected!\nSelect tests for Case Plans!", + ) + send_window_to_front(root) + return + elif total_tests == total_selected_tests: + for test in tests: + test_cases.append(test) + else: + for test_number, test in enumerate(tests): + if selected_tests[test_number].get(): + test_cases.append(test) + + for test_case in test_cases: + if ( + test_case in tests_without_case_plan + and test_case not in tests_with_case_plan + ): + case_plans_to_create.append(test_case) + + new_plans = 0 + for case_plan in case_plans_to_create: + parts = case_plan.split("/") + test_address = None + folder_path = None + if len(parts) == 1: + test_address = parts[0] + if len(parts) > 1: + test_address = parts[-1] + folder_path = "/".join(parts[0:-1]) + test_id = get_test_id(test_address) + case_id = test_id + ".md" + full_folder_path = None + if len(parts) == 1: + full_folder_path = "case_plans" + if not os.path.exists(full_folder_path): + os.makedirs(full_folder_path) + else: + full_folder_path = os.path.join(folder_path, "case_plans") + if not os.path.exists(full_folder_path): + os.makedirs(full_folder_path) + + data = [] + data.append("``%s``" % test_address) + data.append("---") + data.append("| # | Step Description | Expected Result |") + data.append("| - | ---------------- | --------------- |") + data.append("| 1 | Perform Action 1 | Verify Action 1 |") + data.append("| 2 | Perform Action 2 | Verify Action 2 |") + data.append("") + file_name = case_id + file_path = os.path.join(full_folder_path, file_name) + if not os.path.exists(file_path): + out_file = open(file_path, mode="w+", encoding="utf-8") + out_file.writelines("\r\n".join(data)) + out_file.close() + new_plans += 1 + print("Created %s" % file_path) + + if new_plans == 1: + messagebox.showinfo( + "A new Case Plan was generated!", + '\nโœ… %s new boilerplate Case Plan was generated!' % new_plans, + ) + elif new_plans >= 2: + messagebox.showinfo( + "New Case Plans were generated!", + '\nโœ… %s new boilerplate Case Plans were generated!' % new_plans, + ) + else: + messagebox.showwarning( + "No new Case Plans were generated!", + "\nโ„น๏ธ No new boilerplates were generated!\n\n" + "The selected tests already have Case Plans!", + ) + send_window_to_front(root) + + +def view_summary_of_existing_case_plans(root, tests): + case_data_storage = [] + case_to_test_hash = {} + full_t = [] + for test_index, test in enumerate(tests): + full_t.append(test) + parts = test.strip().split("/") + test_address = None + folder_path = None + if len(parts) == 1: + test_address = parts[0] + folder_path = "." + if len(parts) > 1: + test_address = parts[-1] + folder_path = "/".join(parts[0:-1]) + test_id = get_test_id(test_address) + case_id = test_id + ".md" + case_path = None + if len(parts) == 1: + case_path = os.path.join("case_plans", case_id) + else: + case_path = os.path.join(folder_path, "case_plans", case_id) + if os.path.exists(case_path): + f = open(case_path, mode="r") + case_data = f.read() + f.close() + case_data_storage.append(case_data) + case_to_test_hash[len(case_data_storage) - 1] = test_index + + full_plan = [] + if len(case_data_storage) > 0: + full_plan.append( + "

      Summary of existing Case Plans

      " + ) + full_plan.append("") + full_plan.append("| | |") + full_plan.append("| - | - |") + full_plan.append("| ๐Ÿ”ต | Plans with customized step definitions. |") + full_plan.append("| โญ• | Plans using default boilerplate code. |") + full_plan.append("| ๐Ÿšง | Plans under construction with no table. |") + full_plan.append("") + full_plan.append("--------") + else: + show_no_case_plans_warning() + send_window_to_front(root) + return + full_plan.append("") + full_plan.append("

      ๐Ÿ”Ž (Click rows to expand) ๐Ÿ”

      ") + full_plan.append("") + + full_plan = [] + num_ready_cases = 0 + num_boilerplate = 0 + num_in_progress = 0 + for case_index, case_data in enumerate(case_data_storage): + icon = "๐Ÿ”ต" + table_missing = False + if "| 1 | Perform Action 1 | Verify Action 1 |" in case_data: + # Still using raw boilerplate code. (Missing real test steps) + icon = "โญ•" + if case_data.count("|") < 9 or case_data.count("-") < 3: + # Not enough characters for a minimal Markdown case plan file. + # The dash(es) on line 2, and the Markdown table are required. + # This is what a minimal case plan file might look like: + """ + TEST_ADDRESS + - + | Steps | Results | + | - | - | + | Step1 | Result1 | + """ + icon = "๐Ÿšง" + table_missing = True + lines = case_data.split("\n") + if len(lines) >= 3 and not table_missing: + first_line = lines[0] + first_line = first_line.strip() + if not (first_line.startswith("``") and first_line.endswith("``")): + first_line = "``%s``" % tests[case_to_test_hash[case_index]] + lines.insert(0, first_line) + else: + first_line = "``%s``" % tests[case_to_test_hash[case_index]] + lines[0] = first_line + lines.insert(0, "
      ") + lines[1] = ( + " %s " % icon + + first_line[2:-2] + + "" + ) + if ( + lines[2].strip().startswith("-") + and lines[2].strip().endswith("-") + ): + lines[2] = "" + elif lines[2].strip() != "": + lines.insert(2, "") + if lines[-1].strip() != "": + lines.append("") + lines.append("
      ") + full_plan.append("\r\n".join(lines)) + else: + # No existing Case Plan found. / File is missing boilerplate. + icon = "๐Ÿšง" + lines = [] + first_line = tests[case_to_test_hash[case_index]] + first_line = "%s %s" % (icon, first_line) + lines.insert(0, first_line) + full_plan.append("\r\n".join(lines)) + full_plan.append("") + if icon == "๐Ÿ”ต": + num_ready_cases += 1 + elif icon == "โญ•": + num_boilerplate += 1 + elif icon == "๐Ÿšง": + num_in_progress += 1 + + msg_ready_cases = "%s Case Plans with customized tables" % num_ready_cases + if num_ready_cases == 1: + msg_ready_cases = "1 Case Plan with a customized table" + msg_boilerplate = "%s Case Plans using boilerplate code" % num_boilerplate + if num_boilerplate == 1: + msg_boilerplate = "1 Case Plan using boilerplate code" + msg_in_progress = "%s Case Plans that are missing tables" % num_in_progress + if num_in_progress == 1: + msg_in_progress = "1 Case Plan that is missing a table" + + msg_r = " ".join(msg_ready_cases.split(" ")[1:]) + msg_b = " ".join(msg_boilerplate.split(" ")[1:]) + msg_i = " ".join(msg_in_progress.split(" ")[1:]) + + plan_head = [] + if len(case_data_storage) > 0: + plan_head.append( + "

      Summary of existing Case Plans

      " + ) + plan_head.append("") + plan_head.append("| | | |") + plan_head.append("| - | -: | - |") + plan_head.append("| ๐Ÿ”ต | %s | %s |" % (num_ready_cases, msg_r)) + plan_head.append("| โญ• | %s | %s |" % (num_boilerplate, msg_b)) + plan_head.append("| ๐Ÿšง | %s | %s |" % (num_in_progress, msg_i)) + plan_head.append("") + plan_head.append("--------") + else: + show_no_case_plans_warning() + send_window_to_front(root) + return + plan_head.append("") + plan_head.append("

      ๐Ÿ”Ž (Click rows to expand) ๐Ÿ”

      ") + plan_head.append("") + + for row in full_plan: + plan_head.append(row) + full_plan = plan_head + + file_path = "case_summary.md" + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(full_plan)) + file.close() + + if num_ready_cases < 10: + msg_ready_cases = " %s" % msg_ready_cases + if num_ready_cases < 100: + msg_ready_cases = " %s" % msg_ready_cases + if num_boilerplate < 10: + msg_boilerplate = " %s" % msg_boilerplate + if num_boilerplate < 100: + msg_boilerplate = " %s" % msg_boilerplate + if num_in_progress < 10: + msg_in_progress = " %s" % msg_in_progress + if num_in_progress < 100: + msg_in_progress = " %s" % msg_in_progress + gen_message = ( + '๐Ÿ—‚๏ธ Summary generated at "case_summary.md":' + '\n๐Ÿ”ต %s' + '\nโญ• %s' + '\n๐Ÿšง %s' + % (msg_ready_cases, msg_boilerplate, msg_in_progress) + ) + print(gen_message) + if num_ready_cases < 10: + msg_ready_cases = " %s" % msg_ready_cases + if num_ready_cases < 100: + msg_ready_cases = " %s" % msg_ready_cases + if num_boilerplate < 10: + msg_boilerplate = " %s" % msg_boilerplate + if num_boilerplate < 100: + msg_boilerplate = " %s" % msg_boilerplate + if num_in_progress < 10: + msg_in_progress = " %s" % msg_in_progress + if num_in_progress < 100: + msg_in_progress = " %s" % msg_in_progress + messagebox.showinfo( + "Case Plans Summary generated!", + '\nSummary generated at "case_summary.md"' + '\n๐Ÿ”ต %s' + '\nโญ• %s' + '\n๐Ÿšง %s' + % (msg_ready_cases, msg_boilerplate, msg_in_progress) + ) + send_window_to_front(root) + + +def create_tkinter_gui(tests, command_string): + root = tk.Tk() + root.title("SeleniumBase Case Plans Generator") + if shared_utils.is_windows(): + root.minsize(820, 618) + else: + root.minsize(820, 652) + tk.Label(root, text="").pack() + run_display = ( + "Select from %s tests found: " + "(Boilerplate Case Plans will be generated as needed)" + % len(tests) + ) + if len(tests) == 1: + run_display = ( + "Select from 1 test found: " + "(Boilerplate Case Plans will be generated as needed)" + ) + run_display_2 = "(Tests with existing Case Plans are already checked)" + tk.Label(root, text=run_display, bg="yellow", fg="green").pack() + tk.Label(root, text=run_display_2, bg="yellow", fg="magenta").pack() + text_area = ScrolledText( + root, width=100, height=12, wrap="word", state=tk.DISABLED + ) + text_area.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + count = 0 + ara = {} + tests_with_case_plan = [] + tests_without_case_plan = [] + for row in tests: + row += " " * 200 + ara[count] = tk.IntVar() + cb = None + if shared_utils.is_windows(): + cb = tk.Checkbutton( + text_area, + text=(row), + bg="white", + fg="black", + anchor="w", + pady=0, + borderwidth=1, + highlightthickness=1, + variable=ara[count], + ) + else: + cb = tk.Checkbutton( + text_area, + text=(row), + bg="white", + fg="black", + anchor="w", + pady=0, + variable=ara[count], + ) + parts = row.strip().split("/") + test_address = None + folder_path = None + if len(parts) == 1: + test_address = parts[0] + if len(parts) > 1: + test_address = parts[-1] + folder_path = "/".join(parts[0:-1]) + test_id = get_test_id(test_address) + case_id = test_id + ".md" + case_path = None + if len(parts) == 1: + case_path = os.path.join("case_plans", case_id) + else: + case_path = os.path.join(folder_path, "case_plans", case_id) + if os.path.exists(case_path): + cb.select() + tests_with_case_plan.append(row.strip()) + else: + tests_without_case_plan.append(row.strip()) + text_area.window_create("end", window=cb) + text_area.insert("end", "\n") + count += 1 + + tk.Label(root, text="").pack() + tk.Button( + root, + text=( + "Generate boilerplate Case Plans " + "for selected tests missing them"), + fg="green", + command=lambda: generate_case_plan_boilerplates( + root, + tests, + ara, + tests_with_case_plan, + tests_without_case_plan, + ), + ).pack() + + tk.Label(root, text="").pack() + try: + tk.Button( + root, + text=("Generate Summary of existing Case Plans"), + fg="teal", + command=lambda: view_summary_of_existing_case_plans(root, tests), + ).pack() + except Exception: + tk.Button( + root, + text=("Generate Summary of existing Case Plans"), + fg="green", + command=lambda: view_summary_of_existing_case_plans(root, tests), + ).pack() + tk.Label(root, text="\n").pack() + + # Bring form window to front + send_window_to_front(root) + # Use decoy to set correct focus on main window + decoy = tk.Tk() + decoy.geometry("1x1") + decoy.iconify() + decoy.update() + decoy.deiconify() + decoy.destroy() + # Start tkinter + root.mainloop() + + +def main(): + use_colors = True + if shared_utils.is_linux(): + use_colors = False + c0, c1, c2, c3, c4, c5, cr = set_colors(use_colors) + command_args = sys.argv[2:] + command_string = " ".join(command_args) + message = "" + message += c2 + message += "*" + message += c4 + message += " Starting the " + message += c0 + message += "Selenium" + message += c1 + message += "Base" + message += c2 + message += " " + message += c3 + message += "Case Plans" + message += c4 + message += " Generator" + message += c2 + message += "..." + message += cr + print(message) + + proc = subprocess.Popen( + '"%s" -m pytest --collect-only -q --rootdir="./" %s' + % (sys.executable, command_string), + stdout=subprocess.PIPE, + shell=True, + ) + (output, error) = proc.communicate() + if error: + error_msg = "Error collecting tests: %s" % str(error) + error_msg = c5 + error_msg + cr + print(error_msg) + return + tests = [] + if shared_utils.is_windows(): + output = output.decode("latin1") + else: + output = output.decode("utf-8") + for row in output.replace("\r", "").split("\n"): + if ("::") in row: + tests.append(row) + if not tests: + error_msg = "No tests found! Exiting the Case Plans Generator..." + error_msg = c5 + "ERROR: " + error_msg + cr + print(error_msg) + return + + create_tkinter_gui(tests, command_string) + + +if __name__ == "__main__": + print('To open the Case Plans Generator, type "sbase caseplans"') diff --git a/seleniumbase/console_scripts/sb_commander.py b/seleniumbase/console_scripts/sb_commander.py new file mode 100644 index 00000000000..b0cd67978c6 --- /dev/null +++ b/seleniumbase/console_scripts/sb_commander.py @@ -0,0 +1,467 @@ +""" +Launches SeleniumBase Commander | GUI for pytest. + +Usage: + seleniumbase commander [OPTIONAL PATH or TEST FILE] + sbase commander [OPTIONAL PATH or TEST FILE] + seleniumbase gui [OPTIONAL PATH or TEST FILE] + sbase gui [OPTIONAL PATH or TEST FILE] + +Examples: + sbase gui + sbase gui -k agent + sbase gui -m marker2 + sbase gui test_suite.py + sbase gui offline_examples/ + +Output: + Launches SeleniumBase Commander | GUI for pytest. +""" +import colorama +import os +import subprocess +import sys +import tkinter as tk +from seleniumbase.fixtures import shared_utils +from tkinter.scrolledtext import ScrolledText + + +def set_colors(use_colors): + c0 = "" + c1 = "" + c2 = "" + c3 = "" + c4 = "" + c5 = "" + cr = "" + if use_colors: + c0 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX + c1 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c2 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + c3 = colorama.Fore.BLACK + colorama.Back.LIGHTGREEN_EX + c4 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + c5 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + return c0, c1, c2, c3, c4, c5, cr + + +def send_window_to_front(root): + root.lift() + root.attributes("-topmost", True) + root.after_idle(root.attributes, "-topmost", False) + + +def do_pytest_run( + root, + tests, + selected_tests, + command_string, + browser_string, + rs_string, + thread_string, + verbose, + demo_mode, + mobile_mode, + dashboard, + html_report, + headless, + save_screenshots, + additional_options, +): + cleaned_tests = [] + for test in tests: + if test.startswith("(FILE) "): + clean_test = test.split("(FILE) ")[1].split(" => ")[0] + cleaned_tests.append(clean_test) + else: + cleaned_tests.append(test) + tests = cleaned_tests + total_tests = len(tests) + total_selected_tests = 0 + for selected_test in selected_tests: + if selected_tests[selected_test].get(): + total_selected_tests += 1 + + full_run_command = '"%s" -m pytest' % sys.executable + if total_selected_tests == 0 or total_tests == total_selected_tests: + if command_string: + full_run_command += " " + full_run_command += command_string + else: + for test_number, test in enumerate(tests): + if selected_tests[test_number].get(): + full_run_command += " " + if ' ' not in test: + full_run_command += test + elif '"' not in test: + full_run_command += '"%s"' % test + else: + full_run_command += test.replace(" ", "\\ ") + + if "(--edge)" in browser_string: + full_run_command += " --edge" + elif "(--firefox)" in browser_string: + full_run_command += " --firefox" + elif "(--safari)" in browser_string: + full_run_command += " --safari" + + if "(--rs)" in rs_string: + full_run_command += " --rs" + elif "(--rs --crumbs)" in rs_string: + full_run_command += " --rs --crumbs" + elif "(--rcs)" in rs_string: + full_run_command += " --rcs" + elif "(--rcs --crumbs)" in rs_string: + full_run_command += " --rcs --crumbs" + + if "(-n=2)" in thread_string: + full_run_command += " -n=2" + elif "(-n=3)" in thread_string: + full_run_command += " -n=3" + elif "(-n=4)" in thread_string: + full_run_command += " -n=4" + elif "(-n=5)" in thread_string: + full_run_command += " -n=5" + elif "(-n=6)" in thread_string: + full_run_command += " -n=6" + elif "(-n=7)" in thread_string: + full_run_command += " -n=7" + elif "(-n=8)" in thread_string: + full_run_command += " -n=8" + + if demo_mode: + full_run_command += " --demo" + + if mobile_mode: + full_run_command += " --mobile" + + if dashboard: + full_run_command += " --dashboard" + + if html_report: + full_run_command += " --html=report.html" + + if headless: + full_run_command += " --headless" + elif shared_utils.is_linux(): + full_run_command += " --gui" + + if save_screenshots: + full_run_command += " --screenshot" + + capture_needed = False + if "--capture" not in additional_options: + capture_needed = True + + additional_options = additional_options.strip() + if additional_options: + full_run_command += " " + full_run_command += additional_options + + if verbose: + full_run_command += " -v" + + if capture_needed: + full_run_command += " --capture=tee-sys" + + print(full_run_command) + subprocess.Popen(full_run_command, shell=True) + send_window_to_front(root) + + +def create_tkinter_gui(tests, command_string, files, solo_tests): + root = tk.Tk() + root.title("SeleniumBase Commander | GUI for pytest") + if shared_utils.is_windows(): + root.minsize(820, 696) + else: + root.minsize(820, 702) + tk.Label(root, text="").pack() + + options_list = [ + "Use Chrome Browser (Default)", + "Use Edge Browser (--edge)", + "Use Firefox Browser (--firefox)", + ] + if shared_utils.is_mac(): + options_list.append("Use Safari Browser (--safari)") + brx = tk.StringVar(root) + brx.set(options_list[0]) + question_menu = tk.OptionMenu(root, brx, *options_list) + question_menu.pack() + + options_list = [ + "New Session Per Test (Default)", + "Reuse Session for ALL tests in thread (--rs)", + "Reuse Session and also clear cookies (--rs --crumbs)", + "Reuse Session for tests with same CLASS (--rcs)", + "Reuse Session for class and clear cookies (--rcs --crumbs)", + ] + rsx = tk.StringVar(root) + rsx.set(options_list[0]) + question_menu = tk.OptionMenu(root, rsx, *options_list) + question_menu.pack() + + options_list = [ + "Number of Threads: 1 (Default)", + "Number of Threads: 2 (-n=2)", + "Number of Threads: 3 (-n=3)", + "Number of Threads: 4 (-n=4)", + ] + try: + if int(os.cpu_count()) >= 8: + options_list.append("Number of Threads: 5 (-n=5)") + options_list.append("Number of Threads: 6 (-n=6)") + options_list.append("Number of Threads: 7 (-n=7)") + options_list.append("Number of Threads: 8 (-n=8)") + except Exception: + pass + + ntx = tk.StringVar(root) + ntx.set(options_list[0]) + question_menu = tk.OptionMenu(root, ntx, *options_list) + question_menu.pack() + + vox = tk.IntVar() + chk = tk.Checkbutton( + root, text="Verbose Output (-v)", variable=vox, pady=0 + ) + chk.pack() + chk.select() + + dmx = tk.IntVar() + chk = tk.Checkbutton( + root, text="Demo Mode (--demo)", variable=dmx, pady=0 + ) + chk.pack() + + mmx = tk.IntVar() + chk = tk.Checkbutton( + root, text="Mobile Mode (--mobile)", variable=mmx, pady=0 + ) + chk.pack() + + dbx = tk.IntVar() + chk = tk.Checkbutton( + root, text="Dashboard (--dashboard)", variable=dbx, pady=0 + ) + chk.pack() + chk.select() + + hrx = tk.IntVar() + chk = tk.Checkbutton( + root, text="Report (--html=report.html)", variable=hrx, pady=0 + ) + chk.pack() + chk.select() + + hbx = tk.IntVar() + chk = tk.Checkbutton( + root, text="Headless Browser (--headless)", variable=hbx, pady=0 + ) + chk.pack() + + ssx = tk.IntVar() + chk = tk.Checkbutton( + root, text="Save Screenshots (--screenshot)", variable=ssx, pady=0 + ) + chk.pack() + + tk.Label(root, text="").pack() + run_display = ( + "Select from %s rows (%s files with %s tests): " + "(All tests will run if none are selected)" + % (len(tests), len(files), len(solo_tests)) + ) + if len(solo_tests) == 1: + run_display = "Only ONE TEST was found and will be run:" + tests = solo_tests + elif len(files) == 1: + run_display = ( + "Select from %s tests: " + "(All tests will run if none are selected)" + % (len(solo_tests)) + ) + tests = solo_tests + tk.Label(root, text=run_display, bg="yellow", fg="magenta").pack() + text_area = ScrolledText( + root, width=100, height=12, wrap="word", state=tk.DISABLED + ) + text_area.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + count = 0 + ara = {} + for row in tests: + row += " " * 200 + ara[count] = tk.IntVar() + cb = None + if shared_utils.is_windows(): + cb = tk.Checkbutton( + text_area, + text=(row), + bg="white", + fg="black", + anchor="w", + pady=0, + borderwidth=1, + highlightthickness=1, + variable=ara[count], + ) + else: + cb = tk.Checkbutton( + text_area, + text=(row), + bg="white", + fg="black", + anchor="w", + pady=0, + variable=ara[count], + ) + text_area.window_create("end", window=cb) + text_area.insert("end", "\n") + count += 1 + + tk.Label(root, text="").pack() + additional_options = "" + aopts = tk.StringVar(value=additional_options) + tk.Label( + root, + text='Additional "pytest" Options: (Eg. "--incognito --slow")', + bg="yellow", fg="blue", + ).pack() + entry = tk.Entry(root, textvariable=aopts) + entry.pack() + entry.focus() + entry.bind( + "", + ( + lambda _: do_pytest_run( + root, + tests, + ara, + command_string, + brx.get(), + rsx.get(), + ntx.get(), + vox.get(), + dmx.get(), + mmx.get(), + dbx.get(), + hrx.get(), + hbx.get(), + ssx.get(), + aopts.get(), + ) + ), + ) + tk.Button( + root, + text="Run Selected Tests", + fg="green", + command=lambda: do_pytest_run( + root, + tests, + ara, + command_string, + brx.get(), + rsx.get(), + ntx.get(), + vox.get(), + dmx.get(), + mmx.get(), + dbx.get(), + hrx.get(), + hbx.get(), + ssx.get(), + aopts.get(), + ), + ).pack() + tk.Label(root, text="\n").pack() + + # Bring form window to front + send_window_to_front(root) + # Use decoy to set correct focus on main window + decoy = tk.Tk() + decoy.geometry("1x1") + decoy.iconify() + decoy.update() + decoy.deiconify() + decoy.destroy() + # Start tkinter + root.mainloop() + + +def main(): + use_colors = True + if shared_utils.is_linux(): + use_colors = False + c0, c1, c2, c3, c4, c5, cr = set_colors(use_colors) + command_args = sys.argv[2:] + command_string = " ".join(command_args) + message = "" + message += c2 + message += "*" + message += c4 + message += " Starting the " + message += c0 + message += "Selenium" + message += c1 + message += "Base" + message += c2 + message += " " + message += c3 + message += "Commander" + message += c4 + message += " Desktop App" + message += c2 + message += "..." + message += cr + print(message) + + proc = subprocess.Popen( + '"%s" -m pytest --collect-only -q --rootdir="./" %s' + % (sys.executable, command_string), + stdout=subprocess.PIPE, + shell=True, + ) + (output, error) = proc.communicate() + if error: + error_msg = "Error collecting tests: %s" % str(error) + error_msg = c5 + error_msg + cr + print(error_msg) + return + tests = [] + if shared_utils.is_windows(): + output = output.decode("latin1") + else: + output = output.decode("utf-8") + for row in output.replace("\r", "").split("\n"): + if ("::") in row: + tests.append(row) + if not tests: + error_msg = "No tests found! Exiting SeleniumBase Commander..." + error_msg = c5 + "ERROR: " + error_msg + cr + print(error_msg) + return + groups = [] + for row in tests: + if row.count("::") >= 1: + g_name = "(FILE) %s" % row.split("::")[0] + groups.append(g_name) + files = [] + used_files = [] + for row in groups: + if row not in used_files: + used_files.append(row) + plural = "s" + if groups.count(row) == 1: + plural = "" + f_row = "%s => (%s Test%s)" % (row, groups.count(row), plural) + files.append(f_row) + solo_tests = tests + tests = [*files, *tests] + + create_tkinter_gui(tests, command_string, files, solo_tests) + + +if __name__ == "__main__": + print('To open SBase Commander, type "sbase commander" or "sbase gui"') diff --git a/seleniumbase/console_scripts/sb_install.py b/seleniumbase/console_scripts/sb_install.py old mode 100755 new mode 100644 index 32915830d72..30e297340d2 --- a/seleniumbase/console_scripts/sb_install.py +++ b/seleniumbase/console_scripts/sb_install.py @@ -1,46 +1,119 @@ """ -Installs the specified web driver. +Downloads the specified webdriver to "seleniumbase/drivers/" Usage: - seleniumbase install {chromedriver|geckodriver|edgedriver| - iedriver|operadriver} + seleniumbase install [DRIVER/BINARY] [OPTIONS] + OR: seleniumbase get [DRIVER/BINARY] [OPTIONS] + OR: sbase install [DRIVER/BINARY] [OPTIONS] + OR: sbase get [DRIVER/BINARY] [OPTIONS] + (Drivers: chromedriver, uc_driver, + edgedriver, geckodriver) + (Binaries: cft, chs, chromium) +Options: + VERSION Driver/binary version to download. + (Otherwise detects the version) + --revision The Chromium revision to download. + (Otherwise downloads the latest) + -p OR --path Also copy driver to /usr/local/bin +Examples: + sbase get chromedriver + sbase get geckodriver + sbase get edgedriver + sbase get chromedriver 149 + sbase get chromedriver 149.0.7827.155 + sbase get chromedriver stable + sbase get chromedriver beta + sbase get chromedriver -p + sbase get chromium + sbase get chromium --revision=1639046 + sbase get cft 149 + sbase get chs Output: - Installs the specified webdriver. - (chromedriver is required for Chrome automation) - (geckodriver is required for Firefox automation) - (edgedriver is required for MS Edge automation) - (iedriver is required for Internet Explorer automation) - (operadriver is required for Opera Browser automation) + Downloads driver/binary to seleniumbase/drivers/ + (chromedriver is for Selenium Chrome automation) + (geckodriver is for Selenium Firefox automation) + (edgedriver is for Selenium MS__Edge automation) + (cft is for the `Chrome for Testing` binary exe) + (chs is for the `Chrome-Headless-Shell` binary.) """ - +import colorama +import logging import os import platform import requests import shutil +import subprocess import sys +import time import tarfile +import urllib3 import zipfile +from contextlib import suppress +from seleniumbase.fixtures import constants +from seleniumbase.fixtures import shared_utils +from seleniumbase import config as sb_config from seleniumbase import drivers # webdriver storage folder for SeleniumBase +from seleniumbase.drivers import cft_drivers # chrome-for-testing +from seleniumbase.drivers import chs_drivers # chrome-headless-shell +from seleniumbase.drivers import chromium_drivers # base chromium + +urllib3.disable_warnings() +ARCH = platform.architecture()[0] +IS_ARM_MAC = shared_utils.is_arm_mac() +IS_MAC = shared_utils.is_mac() +IS_ARM_LINUX = shared_utils.is_arm_linux() +IS_LINUX = shared_utils.is_linux() +IS_WINDOWS = shared_utils.is_windows() DRIVER_DIR = os.path.dirname(os.path.realpath(drivers.__file__)) +DRIVER_DIR_CFT = os.path.dirname(os.path.realpath(cft_drivers.__file__)) +DRIVER_DIR_CHS = os.path.dirname(os.path.realpath(chs_drivers.__file__)) +DRIVER_DIR_CHROMIUM = os.path.dirname( + os.path.realpath(chromium_drivers.__file__) +) +LOCAL_PATH = "/usr/local/bin/" # On Mac and Linux systems +DEFAULT_CHROMEDRIVER_VERSION = "114.0.5735.90" # (If can't find LATEST_STABLE) +DEFAULT_GECKODRIVER_VERSION = "v0.36.0" +DEFAULT_EDGEDRIVER_VERSION = "115.0.1901.183" # (If can't find LATEST_STABLE) def invalid_run_command(): - exp = (" ** install **\n\n") - exp += " Usage:\n" - exp += " seleniumbase install [DRIVER_NAME]\n" - exp += " (Drivers: chromedriver, geckodriver, edgedriver,\n" - exp += " iedriver, operadriver)\n" - exp += " Example:\n" - exp += " seleniumbase install chromedriver\n" - exp += " Output:\n" - exp += " Installs the specified webdriver.\n" - exp += " (chromedriver is required for Chrome automation)\n" - exp += " (geckodriver is required for Firefox automation)\n" - exp += " (edgedriver is required for Microsoft Edge automation)\n" - exp += " (iedriver is required for InternetExplorer automation)\n" - exp += " (operadriver is required for Opera Browser automation)\n" + exp = " ** get / install **\n\n" + exp += "Usage:\n" + exp += " seleniumbase install [DRIVER/BINARY] [OPTIONS]\n" + exp += " OR sbase install [DRIVER/BINARY] [OPTIONS]\n" + exp += " OR seleniumbase get [DRIVER/BINARY] [OPTIONS]\n" + exp += " OR sbase get [DRIVER/BINARY] [OPTIONS]\n" + exp += " (Drivers: chromedriver, uc_driver,\n" + exp += " edgedriver, geckodriver)\n" + exp += " (Binaries: cft, chs, chromium)\n" + exp += "Options:\n" + exp += " VERSION Driver/binary version to download.\n" + exp += " (Otherwise detects the version)\n" + exp += " --revision The Chromium revision to download.\n" + exp += " (Otherwise downloads the latest)\n" + exp += " -p OR --path Also copy driver to /usr/local/bin\n" + exp += "Examples:\n" + exp += " sbase get chromedriver\n" + exp += " sbase get geckodriver\n" + exp += " sbase get edgedriver\n" + exp += " sbase get chromedriver 149\n" + exp += " sbase get chromedriver 149.0.7827.155\n" + exp += " sbase get chromedriver stable\n" + exp += " sbase get chromedriver beta\n" + exp += " sbase get chromedriver -p\n" + exp += " sbase get chromium\n" + exp += " sbase get chromium --revision=1639046\n" + exp += " sbase get cft 149\n" + exp += " sbase get chs\n" + exp += "Output:\n" + exp += " Downloads driver/binary to seleniumbase/drivers/\n" + exp += " (chromedriver is for Selenium Chrome automation)\n" + exp += " (geckodriver is for Selenium Firefox automation)\n" + exp += " (edgedriver is for Selenium MS__Edge automation)\n" + exp += " (cft is for the `Chrome for Testing` binary exe)\n" + exp += " (chs is for the `Chrome-Headless-Shell` binary.)\n" print("") - raise Exception('INVALID RUN COMMAND!\n\n%s' % exp) + raise Exception("%s\n\n%s" % (constants.Warnings.INVALID_RUN_COMMAND, exp)) def make_executable(file_path): @@ -50,212 +123,1329 @@ def make_executable(file_path): os.chmod(file_path, mode) -def main(): +def get_proxy_info(): + use_proxy = None + protocol = "http" + proxy_string = None + user_and_pass = None + if " --proxy=" in " ".join(sys.argv): + from seleniumbase.core import proxy_helper + for arg in sys.argv: + if arg.startswith("--proxy="): + proxy_string = arg.split("--proxy=")[1] + if "@" in proxy_string: + # Format => username:password@hostname:port + try: + user_and_pass = proxy_string.split("@")[0] + proxy_string = proxy_string.split("@")[1] + except Exception: + raise Exception( + "The format for using a proxy server with auth " + 'is: "username:password@hostname:port". If not ' + 'using auth, the format is: "hostname:port".' + ) + if proxy_string.endswith(":443"): + protocol = "https" + elif "socks4" in proxy_string: + protocol = "socks4" + elif "socks5h" in proxy_string: + protocol = "socks5h" + elif "socks5" in proxy_string: + protocol = "socks5" + proxy_string = proxy_helper.validate_proxy_string(proxy_string) + if user_and_pass: + proxy_string = "%s@%s" % (user_and_pass, proxy_string) + use_proxy = True + break + return (use_proxy, protocol, proxy_string) + + +def requests_get(url): + use_proxy, protocol, proxy_string = get_proxy_info() + proxies = None + response = None + if use_proxy: + proxies = {protocol: proxy_string} + try: + response = requests.get(url, proxies=proxies, timeout=1.25) + except Exception: + # Prevent SSLCertVerificationError / CERTIFICATE_VERIFY_FAILED + url = url.replace("https://", "http://") + time.sleep(0.04) + response = requests.get(url, proxies=proxies, timeout=2.75) + return response + + +def requests_get_with_retry(url): + use_proxy, protocol, proxy_string = get_proxy_info() + proxies = None + response = None + if use_proxy: + proxies = {protocol: proxy_string} + try: + response = requests.get(url, proxies=proxies, timeout=1.35) + except Exception: + time.sleep(1) + try: + response = requests.get(url, proxies=proxies, timeout=2.45) + except Exception: + time.sleep(1) + response = requests.get(url, proxies=proxies, timeout=3.55) + return response + + +def get_cft_known_good_versions(): + if getattr(sb_config, "cft_kgv_json", None): + return sb_config.cft_kgv_json + cft_ngv_url = ( + "https://googlechromelabs.github.io/" + "chrome-for-testing/known-good-versions.json" + ) + sb_config.cft_kgv_json = requests_get(cft_ngv_url) + return sb_config.cft_kgv_json + + +def get_cft_latest_versions_per_milestone(): + if getattr(sb_config, "cft_lvpm_json", None): + return sb_config.cft_lvpm_json + cft_lvpm_url = ( + "https://googlechromelabs.github.io/" + "chrome-for-testing/latest-versions-per-milestone.json" + ) + sb_config.cft_lvpm_json = requests_get(cft_lvpm_url) + return sb_config.cft_lvpm_json + + +def get_cft_latest_version_from_milestone(milestone): + url_request = get_cft_latest_versions_per_milestone() + return url_request.json()["milestones"][milestone]["version"] + + +def get_chromium_channel_revision(platform_code, channel): + """Snapshots only exist for revisions where a build occurred. + Therefore, not all found revisions will lead to snapshots.""" + platform_key = None + if platform_code in ["Mac_Arm", "Mac"]: + platform_key = "Mac" + elif platform_code in ["Linux_x64"]: + platform_key = "Linux" + elif platform_code in ["Win_x64"]: + platform_key = "Windows" + elif platform_code in ["Win"]: + platform_key = "Win32" + channel_key = None + if channel.lower() == "stable": + channel_key = "Stable" + elif channel.lower() == "beta": + channel_key = "Beta" + elif channel.lower() == "dev": + channel_key = "Dev" + elif channel.lower() == "canary": + channel_key = "Canary" + base_url = "https://chromiumdash.appspot.com/fetch_releases" + url = f"{base_url}?channel={channel_key}&platform={platform_key}&num=1" + url_request = requests_get_with_retry(url) + data = None + if url_request.ok: + data = url_request.text + else: + raise Exception("Could not determine Chromium revision!") + if data: + try: + import ast + + result = ast.literal_eval(data) + revision = result[0]["chromium_main_branch_position"] + return str(revision) + except Exception: + return get_latest_chromedriver_version(platform_code) + else: + return get_latest_chromedriver_version(platform_code) + + +def get_chromium_latest_revision(platform_code): + base_url = "https://storage.googleapis.com/chromium-browser-snapshots" + url = f"{base_url}/{platform_code}/LAST_CHANGE" + url_request = requests_get_with_retry(url) + if url_request.ok: + latest_revision = url_request.text + else: + raise Exception("Could not determine latest Chromium revision!") + return latest_revision + + +def get_latest_chromedriver_version(channel="Stable"): + try: + if getattr(sb_config, "cft_lkgv_json", None): + return sb_config.cft_lkgv_json["channels"][channel]["version"] + req = requests_get( + "https://googlechromelabs.github.io/" + "chrome-for-testing/last-known-good-versions.json" + ) + if req and req.ok: + sb_config.cft_lkgv_json = req.json() + return req.json()["channels"][channel]["version"] + except Exception: + pass + # If a problem with Chrome-for-Testing JSON API: Fall back + return DEFAULT_CHROMEDRIVER_VERSION + + +def get_latest_stable_chromedriver_version(): + return get_latest_chromedriver_version(channel="Stable") + + +def get_latest_beta_chromedriver_version(): + return get_latest_chromedriver_version(channel="Beta") + + +def get_latest_dev_chromedriver_version(): + return get_latest_chromedriver_version(channel="Dev") + + +def get_latest_canary_chromedriver_version(): + return get_latest_chromedriver_version(channel="Canary") + + +def log_d(message): + """If setting sb_config.settings.HIDE_DRIVER_DOWNLOADS to True, + output from driver downloads are logged instead of printed.""" + if getattr(sb_config.settings, "HIDE_DRIVER_DOWNLOADS", None): + logging.debug(message) + else: + print(message) + + +def main(override=None, intel_for_uc=None, force_uc=None): + if override: + found_proxy = None + if getattr(sb_config, "proxy_driver", None): + if " --proxy=" in " ".join(sys.argv): + for arg in sys.argv: + if arg.startswith("--proxy="): + found_proxy = arg + break + if override == "chromedriver": + sys.argv = ["seleniumbase", "get", "chromedriver"] + elif override.startswith("chromedriver "): + extra = override.split("chromedriver ")[1] + sys.argv = ["seleniumbase", "get", "chromedriver", extra] + elif override == "edgedriver": + sys.argv = ["seleniumbase", "get", "edgedriver"] + elif override.startswith("edgedriver "): + extra = override.split("edgedriver ")[1] + sys.argv = ["seleniumbase", "get", "edgedriver", extra] + elif override == "geckodriver": + sys.argv = ["seleniumbase", "get", "geckodriver"] + elif override.startswith("geckodriver "): + extra = override.split("geckodriver ")[1] + sys.argv = ["seleniumbase", "get", "geckodriver", extra] + elif override == "iedriver": + sys.argv = ["seleniumbase", "get", "iedriver"] + elif override.startswith("iedriver "): + extra = override.split("iedriver ")[1] + sys.argv = ["seleniumbase", "get", "iedriver", extra] + elif override == "chromium": + sys.argv = ["seleniumbase", "get", "chromium"] + elif override.startswith("chromium "): + extra = override.split("chromium ")[1] + sys.argv = ["seleniumbase", "get", "chromium", extra] + elif override == "cft": + sys.argv = ["seleniumbase", "get", "cft"] + elif override.startswith("cft "): + extra = override.split("cft ")[1] + sys.argv = ["seleniumbase", "get", "cft", extra] + elif override == "chs": + sys.argv = ["seleniumbase", "get", "chs"] + elif override.startswith("chs "): + extra = override.split("chs ")[1] + sys.argv = ["seleniumbase", "get", "chs", extra] + if found_proxy: + sys.argv.append(found_proxy) + num_args = len(sys.argv) - if sys.argv[0].split('/')[-1].lower() == "seleniumbase" or ( - sys.argv[0].split('\\')[-1].lower() == "seleniumbase"): - if num_args < 3 or num_args > 3: + if ( + "sbase" in sys.argv[0].lower() + or ("seleniumbase" in sys.argv[0].lower()) + ): + if num_args < 3 or num_args > 5: invalid_run_command() else: invalid_run_command() - name = sys.argv[num_args - 1].lower() + name = sys.argv[2].lower() + if force_uc: + name = "uc_driver" file_name = None download_url = None + headless_ie_url = None + headless_ie_exists = False + headless_ie_file_name = None downloads_folder = DRIVER_DIR - sys_plat = sys.platform + if ( + hasattr(sb_config, "settings") + and getattr(sb_config.settings, "NEW_DRIVER_DIR", None) + and os.path.exists(sb_config.settings.NEW_DRIVER_DIR) + ): + downloads_folder = sb_config.settings.NEW_DRIVER_DIR + elif override == "cft" or name == "cft": + downloads_folder = DRIVER_DIR_CFT + elif override == "chs" or name == "chs": + downloads_folder = DRIVER_DIR_CHS + elif override == "chromium": + downloads_folder = DRIVER_DIR_CHROMIUM expected_contents = None platform_code = None - inner_folder = None - - if name == "chromedriver": - latest_version = "2.46" - if "darwin" in sys_plat: - file_name = "chromedriver_mac64.zip" - elif "linux" in sys_plat: - latest_version = "2.40" # Linux machines may need the old driver + copy_to_path = False + latest_version = "" + use_version = "" + new_file = "" + f_name = "" + c1 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + c4 = colorama.Fore.LIGHTRED_EX + colorama.Back.LIGHTWHITE_EX + c5 = colorama.Fore.RED + colorama.Back.LIGHTWHITE_EX + c6 = colorama.Fore.LIGHTYELLOW_EX + colorama.Back.CYAN + cr = colorama.Style.RESET_ALL + if IS_LINUX: + c1 = "" + c2 = "" + c3 = "" + c4 = "" + c5 = "" + c6 = "" + cr = "" + + if name == "chromedriver" or name == "uc_driver": + if name == "uc_driver" and IS_ARM_MAC: + intel_for_uc = True # uc_driver is generated from chromedriver + last = "https://chromedriver.storage.googleapis.com/LATEST_RELEASE" + use_version = DEFAULT_CHROMEDRIVER_VERSION # Until get correct VER + + if ( + not override + and ( + num_args == 3 + or (num_args == 4 and "-p" in sys.argv[3].lower()) + ) + ): + major_chrome_version = None + try: + from seleniumbase.core import detect_b_ver + + br_app = "google-chrome" + major_chrome_version = ( + detect_b_ver.get_browser_version_from_os(br_app) + ).split(".")[0] + if int(major_chrome_version) < 72: + major_chrome_version = None + except Exception: + major_chrome_version = None + if major_chrome_version and major_chrome_version.isnumeric(): + num_args += 1 + sys.argv.insert(3, major_chrome_version) + + get_latest = False + get_v_latest = False + get_previous = False + get_beta = False + get_canary = False + if num_args == 4 or num_args == 5: + if "-p" not in sys.argv[3].lower(): + use_version = sys.argv[3] + uv_low = use_version.lower() + if uv_low == "latest" or uv_low == "stable": + uv_low = "latest" # If "stable", rename + get_latest = True + elif uv_low == "latest-1" or uv_low == "previous": + uv_low = "latest-1" # If "previous", rename + get_previous = True + elif uv_low == "beta": + get_beta = True + elif uv_low == "dev": + use_version = get_latest_dev_chromedriver_version() + sys.argv[3] = use_version + elif uv_low == "canary": + get_canary = True + elif uv_low.isdigit() and int(uv_low) > 69: + get_v_latest = True + else: + copy_to_path = True + if num_args == 5: + if "-p" in sys.argv[4].lower(): + copy_to_path = True + else: + invalid_run_command() + if IS_MAC: + if IS_ARM_MAC and not intel_for_uc: + use_version = use_version.lower() + if ( + use_version == "latest" + or use_version == "stable" + or use_version == "latest-1" + or use_version == "previous" + or use_version == "beta" + or use_version == "canary" + ): + use_version = get_latest_stable_chromedriver_version() + if use_version == "latest-1" or use_version == "previous": + use_version = str(int(use_version.split(".")[0]) - 1) + elif use_version == "beta": + use_version = str(int(use_version.split(".")[0]) + 1) + elif use_version == "canary": + use_version = str(int(use_version.split(".")[0]) + 2) + if ( + IS_ARM_MAC + and not intel_for_uc + and int(use_version.split(".")[0]) > 105 + ): + file_name = "chromedriver_mac_arm64.zip" + else: + file_name = "chromedriver_mac64.zip" + elif IS_LINUX: file_name = "chromedriver_linux64.zip" - elif "win32" in sys_plat or "win64" in sys_plat or "x64" in sys_plat: + elif IS_WINDOWS: file_name = "chromedriver_win32.zip" # Works for win32 / win_x64 + if not get_latest and not get_v_latest and num_args < 4: + get_latest = True else: - raise Exception("Cannot determine which version of Chromedriver " - "to download!") - download_url = ("http://chromedriver.storage.googleapis.com/" - "%s/%s" % (latest_version, file_name)) - # Forcing Chromedriver v2.40 for now, even though it's not the latest. - get_latest = False - if get_latest: - last = "http://chromedriver.storage.googleapis.com/LATEST_RELEASE" - print('\nLocating the latest version of Chromedriver...') - latest_version = requests.get(last).text - if not requests.get(download_url).ok: - fallback_version = "2.40" - download_url = ("http://chromedriver.storage.googleapis.com/" - "%s/%s" % (fallback_version, file_name)) + raise Exception( + "Cannot determine which version of chromedriver to download!" + ) + found_chromedriver = False + cft = False + if get_latest or get_previous or get_beta or get_canary: + use_version = get_latest_stable_chromedriver_version() + found_chromedriver = True + if get_previous and int(use_version.split(".")[0]) >= 115: + get_v_latest = True + use_version = str(int(use_version.split(".")[0]) - 1) + elif get_beta and int(use_version.split(".")[0]) >= 115: + get_v_latest = True + use_version = get_latest_beta_chromedriver_version() + use_version = use_version.split(".")[0] + elif get_canary and int(use_version.split(".")[0]) >= 115: + get_v_latest = True + use_version = get_latest_canary_chromedriver_version() + use_version = use_version.split(".")[0] + force_cft = False + if ( + use_version.split(".")[0].isnumeric() + and int(use_version.split(".")[0]) >= 115 + ): + force_cft = True + if (get_v_latest or force_cft): + if get_v_latest: + if not force_cft: + url_req = requests_get(last) + if url_req.ok: + latest_version = url_req.text + else: + latest_version = get_latest_stable_chromedriver_version() + force_cft = False + if not force_cft and int(use_version) < 115: + last = last + "_" + use_version + url_request = requests_get(last) + if url_request.ok: + found_chromedriver = True + use_version = url_request.text + if use_version == latest_version: + get_latest = True + else: + url_request = None + cft = True + if force_cft: + url_request = get_cft_known_good_versions() + if ( + url_request.ok + and '"version":"%s"' % use_version in url_request.text + ): + fver = use_version + found_chromedriver = True + else: + url_request = get_cft_latest_versions_per_milestone() + if not force_cft and url_request.ok: + try: + fver = get_cft_latest_version_from_milestone( + use_version + ) + except KeyError: + use_version = str(int(use_version) - 1) + fver = get_cft_latest_version_from_milestone( + use_version + ) + found_chromedriver = True + use_version = str(fver) + if use_version == latest_version: + get_latest = True + download_url = ( + "https://chromedriver.storage.googleapis.com/" + "%s/%s" % (use_version, file_name) + ) + plat_arch = "" + if cft: + if IS_MAC: + if ( + IS_ARM_MAC + and not intel_for_uc + ): + platform_code = "mac-arm64" + file_name = "chromedriver-mac-arm64.zip" + else: + platform_code = "mac-x64" + file_name = "chromedriver-mac-x64.zip" + elif IS_LINUX: + platform_code = "linux64" + file_name = "chromedriver-linux64.zip" + elif IS_WINDOWS: + if "64" in ARCH: + platform_code = "win64" + file_name = "chromedriver-win64.zip" + else: + platform_code = "win32" + file_name = "chromedriver-win32.zip" + plat_arch = file_name.split(".zip")[0] + download_url = ( + "https://storage.googleapis.com/chrome-for-testing-public/" + "%s/%s/%s" % (use_version, platform_code, file_name) + ) + url_request = None + if not found_chromedriver: + url_req = requests_get(last) + if url_req.ok: + latest_version = url_req.text + if use_version == latest_version: + get_latest = True + url_request = requests_get(download_url) + if found_chromedriver or url_request.ok: + p_version = use_version + p_version = c3 + use_version + cr + latest_stable = get_latest_stable_chromedriver_version() + latest_beta = get_latest_beta_chromedriver_version() + latest_dev = get_latest_dev_chromedriver_version() + latest_canary = get_latest_canary_chromedriver_version() + vint = True + int_use_ver = None + int_latest_ver = None + try: + int_use_ver = int(use_version.split(".")[0]) + int_latest_ver = int(latest_stable.split(".")[0]) + except Exception: + vint = False + on_cft = False + if int_latest_ver > 115: + on_cft = True + if cft and on_cft and use_version == latest_stable: + p_version = p_version + " " + c2 + "(Latest Stable)" + cr + " " + elif cft and on_cft and use_version == latest_beta: + p_version = p_version + " " + c2 + "(Latest Beta)" + cr + " " + elif cft and on_cft and use_version == latest_dev: + p_version = p_version + " " + c2 + "(Latest Dev)" + cr + " " + elif cft and on_cft and use_version == latest_canary: + p_version = p_version + " " + c2 + "(Latest Canary)" + cr + " " + elif not vint: + pass + elif vint and cft and on_cft and int_use_ver == int_latest_ver: + p_version = p_version + " " + c2 + "(Stable)" + cr + elif vint and cft and on_cft and int_use_ver == int_latest_ver + 1: + p_version = p_version + " " + c2 + "(Beta)" + cr + elif vint and cft and on_cft and int_use_ver == int_latest_ver + 2: + p_version = p_version + " " + c2 + "(Dev / Canary)" + cr + elif vint and cft and on_cft and int_use_ver == int_latest_ver - 1: + p_version = p_version + " " + c6 + "(Previous Version)" + cr + elif cft and not on_cft: + pass else: - download_url = ("http://chromedriver.storage.googleapis.com/" - "%s/%s" % (latest_version, file_name)) - print("Found %s" % download_url) + not_latest = c5 + "(" + c4 + "Legacy Version" + c5 + ")" + cr + p_version = p_version + " " + not_latest + msg = c2 + "chromedriver to download" + cr + log_d("\n*** %s = %s" % (msg, p_version)) + else: + raise Exception("Could not find chromedriver to download!\n") + if not get_latest: + pass + elif name == "chrome" or name == "cft": + set_version = None + found_version = None + use_version = None + major_version = None + if num_args >= 4: + set_version = sys.argv[3] + if ( + set_version + and set_version.split(".")[0].isnumeric() + and int(set_version.split(".")[0]) >= 113 + ): + major_version = set_version.split(".")[0] + elif ( + not set_version + or set_version.lower() == "latest" + or set_version.lower() == "stable" + ): + found_version = get_latest_stable_chromedriver_version() + elif ( + set_version and ( + set_version.lower() == "latest-1" + or set_version.lower() == "previous" + ) + ): + found_version = get_latest_stable_chromedriver_version() + major_version = str(int(found_version.split(".")[0]) - 1) + found_version = None + elif (set_version and set_version.lower() == "beta"): + found_version = get_latest_beta_chromedriver_version() + elif (set_version and set_version.lower() == "dev"): + found_version = get_latest_dev_chromedriver_version() + elif (set_version and set_version.lower() == "canary"): + found_version = get_latest_canary_chromedriver_version() + if found_version and found_version.split(".")[0].isnumeric(): + major_version = found_version.split(".")[0] + use_version = found_version + if not use_version: + use_version = get_cft_latest_version_from_milestone(major_version) + msg = c2 + "Chrome for Testing to download" + cr + p_version = c3 + use_version + cr + log_d("\n*** %s = %s" % (msg, p_version)) + if IS_MAC: + if IS_ARM_MAC: + platform_code = "mac-arm64" + file_name = "chrome-mac-arm64.zip" + else: + platform_code = "mac-x64" + file_name = "chrome-mac-x64.zip" + elif IS_LINUX: + platform_code = "linux64" + file_name = "chrome-linux64.zip" + elif IS_WINDOWS: + if "64" in ARCH: + platform_code = "win64" + file_name = "chrome-win64.zip" + else: + platform_code = "win32" + file_name = "chrome-win32.zip" + plat_arch = file_name.split(".zip")[0] + download_url = ( + "https://storage.googleapis.com/chrome-for-testing-public/" + "%s/%s/%s" % (use_version, platform_code, file_name) + ) + elif name == "chromium": + if IS_MAC: + if IS_ARM_MAC: + platform_code = "Mac_Arm" + else: + platform_code = "Mac" + file_name = "chrome-mac.zip" + elif IS_LINUX: + platform_code = "Linux_x64" + file_name = "chrome-linux.zip" + elif IS_WINDOWS: + if "64" in ARCH: + platform_code = "Win_x64" + else: + platform_code = "Win" + file_name = "chrome-win.zip" + revision = get_chromium_latest_revision(platform_code) + arg_join = " ".join(sys.argv) + if "--revision" in arg_join: + rev_str = None + if "--revision=" in arg_join: + rev_str = arg_join.split("--revision=")[1].split(" ")[0] + elif "--revision " in arg_join: + rev_str = arg_join.split("--revision ")[1].split(" ")[0] + if rev_str: + if rev_str.startswith('"') and rev_str.endswith('"'): + rev_str = rev_str[1:-1] + elif rev_str.startswith("'") and rev_str.endswith("'"): + rev_str = rev_str[1:-1] + with suppress(Exception): + if int(rev_str) > 1000000: + revision = rev_str + else: + print( + "Invalid revision number! Defaulting to %s!" + % revision + ) + msg = c2 + "Chromium revision to download" + cr + p_version = c3 + revision + cr + log_d("\n*** %s = %s" % (msg, p_version)) + download_url = ( + "https://storage.googleapis.com/chromium-browser-snapshots/" + "%s/%s/%s" % (platform_code, revision, file_name) + ) + downloads_folder = DRIVER_DIR_CHROMIUM + elif name == "chrome-headless-shell" or name == "chs": + set_version = None + found_version = None + use_version = None + major_version = None + if num_args >= 4: + set_version = sys.argv[3] + if ( + set_version + and set_version.split(".")[0].isnumeric() + and int(set_version.split(".")[0]) >= 113 + ): + major_version = set_version.split(".")[0] + elif ( + not set_version + or set_version.lower() == "latest" + or set_version.lower() == "stable" + ): + found_version = get_latest_stable_chromedriver_version() + elif ( + set_version and ( + set_version.lower() == "latest-1" + or set_version.lower() == "previous" + ) + ): + found_version = get_latest_stable_chromedriver_version() + major_version = str(int(found_version.split(".")[0]) - 1) + found_version = None + elif (set_version and set_version.lower() == "beta"): + found_version = get_latest_beta_chromedriver_version() + elif (set_version and set_version.lower() == "dev"): + found_version = get_latest_dev_chromedriver_version() + elif (set_version and set_version.lower() == "canary"): + found_version = get_latest_canary_chromedriver_version() + if found_version and found_version.split(".")[0].isnumeric(): + major_version = found_version.split(".")[0] + use_version = found_version + if not use_version: + use_version = get_cft_latest_version_from_milestone(major_version) + msg = c2 + "Chrome-Headless-Shell to download" + cr + p_version = c3 + use_version + cr + log_d("\n*** %s = %s" % (msg, p_version)) + if IS_MAC: + if IS_ARM_MAC: + platform_code = "mac-arm64" + file_name = "chrome-headless-shell-mac-arm64.zip" + else: + platform_code = "mac-x64" + file_name = "chrome-headless-shell-mac-x64.zip" + elif IS_LINUX: + platform_code = "linux64" + file_name = "chrome-headless-shell-linux64.zip" + elif IS_WINDOWS: + if "64" in ARCH: + platform_code = "win64" + file_name = "chrome-headless-shell-win64.zip" + else: + platform_code = "win32" + file_name = "chrome-headless-shell-win32.zip" + plat_arch = file_name.split(".zip")[0] + download_url = ( + "https://storage.googleapis.com/chrome-for-testing-public/" + "%s/%s/%s" % (use_version, platform_code, file_name) + ) elif name == "geckodriver" or name == "firefoxdriver": - latest_version = "v0.24.0" - if "darwin" in sys_plat: - file_name = "geckodriver-%s-macos.tar.gz" % latest_version - elif "linux" in sys_plat: - arch = platform.architecture()[0] - if "64" in arch: - file_name = "geckodriver-%s-linux64.tar.gz" % latest_version + use_version = DEFAULT_GECKODRIVER_VERSION + found_geckodriver = False + if num_args == 4 or num_args == 5: + if "-p" not in sys.argv[3].lower(): + use_version = sys.argv[3] + if use_version.lower() == "latest": + last = ( + "https://api.github.com/repos/" + "mozilla/geckodriver/releases/latest" + ) + url_request = requests_get(last) + if url_request.ok: + found_geckodriver = True + use_version = url_request.json()["tag_name"] + else: + use_version = DEFAULT_GECKODRIVER_VERSION + else: + copy_to_path = True + if num_args == 5: + if "-p" in sys.argv[4].lower(): + copy_to_path = True else: - file_name = "geckodriver-%s-linux32.tar.gz" % latest_version - elif "win32" in sys_plat or "win64" in sys_plat or "x64" in sys_plat: - file_name = "geckodriver-%s-win64.zip" % latest_version + invalid_run_command() + if IS_MAC: + if IS_ARM_MAC: + file_name = "geckodriver-%s-macos-aarch64.tar.gz" % use_version + else: + file_name = "geckodriver-%s-macos.tar.gz" % use_version + elif IS_LINUX: + if "64" in ARCH: + if "aarch64" in platform.processor(): + file_name = ( + "geckodriver-%s-linux-aarch64.tar.gz" % use_version + ) + else: + file_name = "geckodriver-%s-linux64.tar.gz" % use_version + else: + file_name = "geckodriver-%s-linux32.tar.gz" % use_version + elif IS_WINDOWS: + file_name = "geckodriver-%s-win64.zip" % use_version else: - raise Exception("Cannot determine which version of Geckodriver " - "(Firefox Driver) to download!") - - download_url = ("https://github.com/mozilla/geckodriver/" - "releases/download/" - "%s/%s" % (latest_version, file_name)) - elif name == "edgedriver" or name == "microsoftwebdriver": + raise Exception( + "Cannot determine which version of geckodriver to download!" + ) + download_url = ( + "https://github.com/mozilla/geckodriver/" + "releases/download/" + "%s/%s" % (use_version, file_name) + ) + url_request = None + if not found_geckodriver: + url_request = requests_get(download_url) + if found_geckodriver or url_request.ok: + msg = c2 + "geckodriver to download" + cr + p_version = c3 + use_version + cr + log_d("\n*** %s = %s" % (msg, p_version)) + else: + raise Exception( + "\nCould not find the specified geckodriver " + "version to download!\n" + ) + elif name == "edgedriver" or name == "msedgedriver": name = "edgedriver" - version_code = "F/8/A/F8AF50AB-3C3A-4BC4-8773-DC27B32988DD" - if "win32" in sys_plat or "win64" in sys_plat or "x64" in sys_plat: - file_name = "MicrosoftWebDriver.exe" + last = ( + "https://msedgewebdriverstorage.blob.core.windows.net" + "/edgewebdriver/LATEST_STABLE" + ) + + if ( + not override + and ( + num_args == 3 + or (num_args == 4 and "-p" in sys.argv[3].lower()) + ) + ): + use_version = "latest" + major_edge_version = None + try: + from seleniumbase.core import detect_b_ver + + br_app = "edge" + major_edge_version = ( + detect_b_ver.get_browser_version_from_os(br_app) + ).split(".")[0] + if int(major_edge_version) < 80: + major_edge_version = None + except Exception: + major_edge_version = None + if major_edge_version and major_edge_version.isnumeric(): + num_args += 1 + sys.argv.insert(3, major_edge_version) + use_version = major_edge_version + + get_latest = False + if num_args == 3: + get_latest = True + if num_args == 4 and "-p" in sys.argv[3].lower(): + get_latest = True + if num_args == 4 or num_args == 5: + if "-p" not in sys.argv[3].lower(): + use_version = sys.argv[3] + if use_version.lower() == "latest": + use_version = DEFAULT_EDGEDRIVER_VERSION + get_latest = True + else: + copy_to_path = True + if num_args == 5: + if "-p" in sys.argv[4].lower(): + copy_to_path = True + else: + invalid_run_command() + if get_latest: + url_request = requests_get_with_retry(last) + if url_request.ok: + use_version = url_request.text.split("\r")[0].split("\n")[0] + use_version = use_version.split(".")[0] + else: + use_version = DEFAULT_EDGEDRIVER_VERSION + suffix = None + if IS_WINDOWS and "64" in ARCH: + file_name = "edgedriver_win64.zip" + suffix = "WINDOWS" + elif IS_WINDOWS: + file_name = "edgedriver_win32.zip" + suffix = "WINDOWS" + elif IS_MAC: + if IS_ARM_MAC and int(use_version.split(".")[0]) > 104: + file_name = "edgedriver_mac64_m1.zip" + else: + file_name = "edgedriver_mac64.zip" + suffix = "MACOS" + elif IS_LINUX: + file_name = "edgedriver_linux64.zip" + suffix = "LINUX" else: - raise Exception("Sorry! Microsoft WebDriver / EdgeDriver is " - "only for Windows-based operating systems!") - download_url = ("https://download.microsoft.com/download/" - "%s/%s" % (version_code, file_name)) + raise Exception( + "Cannot determine which version of EdgeDriver to download!" + ) + if use_version.isdigit(): + edgedriver_st = "https://msedgedriver.microsoft.com/LATEST_RELEASE" + use_version = "%s_%s_%s" % (edgedriver_st, use_version, suffix) + url_request = requests_get_with_retry(use_version) + if url_request.ok: + use_version = url_request.text.split("\r")[0].split("\n")[0] + if ( + int(use_version.split(".")[0]) == 115 + and use_version.startswith("115.0") + and use_version != "115.0.1901.183" + ): + use_version = "115.0.1901.183" + download_url = "https://msedgedriver.microsoft.com/%s/%s" % ( + use_version, + file_name, + ) + if not get_latest and not use_version == DEFAULT_EDGEDRIVER_VERSION: + url_request = requests_get_with_retry(download_url) + if not url_request.ok: + raise Exception( + "Could not find version [%s] of EdgeDriver!" % use_version + ) + msg = c2 + "edgedriver to download" + cr + p_version = c3 + use_version + cr + log_d("\n*** %s = %s" % (msg, p_version)) elif name == "iedriver": - major_version = "3.14" - full_version = "3.14.0" - if "win32" in sys_plat: - file_name = "IEDriverServer_Win32_%s.zip" % full_version - elif "win64" in sys_plat or "x64" in sys_plat: + full_version = "4.14.0" + use_version = full_version + if IS_WINDOWS and "64" in ARCH: file_name = "IEDriverServer_x64_%s.zip" % full_version + elif IS_WINDOWS: + file_name = "IEDriverServer_Win32_%s.zip" % full_version else: - raise Exception("Sorry! IEDriver is only for " - "Windows-based operating systems!") - download_url = ("http://selenium-release.storage.googleapis.com/" - "%s/%s" % (major_version, file_name)) - elif name == "operadriver" or name == "operachromiumdriver": - name = "operadriver" - latest_version = "v.2.40" - if "darwin" in sys_plat: - file_name = "operadriver_mac64.zip" - platform_code = "mac64" - inner_folder = "operadriver_%s/" % platform_code - expected_contents = (['operadriver_mac64/', - 'operadriver_mac64/operadriver', - 'operadriver_mac64/sha512_sum']) - elif "linux" in sys_plat: - file_name = "operadriver_linux64.zip" - platform_code = "linux64" - inner_folder = "operadriver_%s/" % platform_code - expected_contents = (['operadriver_linux64/', - 'operadriver_linux64/operadriver', - 'operadriver_linux64/sha512_sum']) - elif "win32" in sys_plat: - file_name = "operadriver_win32.zip" - platform_code = "win32" - inner_folder = "operadriver_%s/" % platform_code - expected_contents = (['operadriver_win32/', - 'operadriver_win32/operadriver.exe', - 'operadriver_win32/sha512_sum']) - elif "win64" in sys_plat or "x64" in sys_plat: - file_name = "operadriver_win64.zip" - platform_code = "win64" - inner_folder = "operadriver_%s/" % platform_code - expected_contents = (['operadriver_win64/', - 'operadriver_win64/operadriver.exe', - 'operadriver_win64/sha512_sum']) - else: - raise Exception("Cannot determine which version of Operadriver " - "to download!") - - download_url = ("https://github.com/operasoftware/operachromiumdriver/" - "releases/download/" - "%s/%s" % (latest_version, file_name)) + raise Exception( + "Sorry! IEDriver is only for " + "Windows-based systems!" + ) + download_url = ( + "https://github.com/SeleniumHQ/selenium/" + "releases/download/selenium-" + "%s/%s" % (full_version, file_name) + ) + headless_ie_version = "v1.4" + headless_ie_file_name = "headless-selenium-for-win-v1-4.zip" + headless_ie_url = ( + "https://github.com/kybu/headless-selenium-for-win/" + "releases/download/" + "%s/%s" % (headless_ie_version, headless_ie_file_name) + ) + url_request = requests_get_with_retry(headless_ie_url) + if url_request.ok: + headless_ie_exists = True + msg = c2 + "HeadlessIEDriver to download" + cr + p_version = c3 + headless_ie_version + cr + log_d("\n*** %s = %s" % (msg, p_version)) else: invalid_run_command() if file_name is None or download_url is None: invalid_run_command() - file_path = downloads_folder + '/' + file_name + file_path = os.path.join(downloads_folder, file_name) if not os.path.exists(downloads_folder): - os.mkdir(downloads_folder) + os.makedirs(downloads_folder) + + driver_name = None # The name of the driver executable + driver_contents = [] # The contents of the driver zip file + + if headless_ie_exists: + headless_ie_file_path = os.path.join( + downloads_folder, headless_ie_file_name + ) + log_d( + "\nDownloading %s from:\n%s ..." + % (headless_ie_file_name, headless_ie_url) + ) + remote_file = requests_get_with_retry(headless_ie_url) + with open(headless_ie_file_path, "wb") as file: + file.write(remote_file.content) + log_d("%sDownload Complete!%s\n" % (c1, cr)) + zip_file_path = headless_ie_file_path + zip_ref = zipfile.ZipFile(zip_file_path, "r") + contents = zip_ref.namelist() + h_ie_fn = headless_ie_file_name.split(".zip")[0] + expected_contents = [ + "%s/" % h_ie_fn, + "%s/ruby_example/" % h_ie_fn, + "%s/ruby_example/Gemfile" % h_ie_fn, + "%s/ruby_example/Gemfile.lock" % h_ie_fn, + "%s/ruby_example/ruby_example.rb" % h_ie_fn, + "%s/desktop_utils.exe" % h_ie_fn, + "%s/headless_ie_selenium.exe" % h_ie_fn, + "%s/README.md" % h_ie_fn, + ] + if len(contents) > 8: + raise Exception("Unexpected content in HeadlessIEDriver Zip file!") + for content in contents: + if content not in expected_contents: + raise Exception( + "Expected file [%s] missing from [%s]" + % (content, expected_contents) + ) + # Zip file is valid. Proceed. + driver_path = None + driver_file = None + filename = None + for f_name in contents: + # Remove existing version if exists + str_name = str(f_name) + new_file = os.path.join(downloads_folder, str_name) + if str_name == "%s/headless_ie_selenium.exe" % h_ie_fn: + driver_file = str_name + driver_path = new_file + filename = "headless_ie_selenium.exe" + if os.path.exists(new_file): + os.remove(new_file) + if not driver_file or not driver_path or not filename: + raise Exception("headless_ie_selenium.exe missing from Zip file!") + log_d("Extracting %s from %s ..." % (filename, headless_ie_file_name)) + zip_ref.extractall(downloads_folder) + zip_ref.close() + os.remove(zip_file_path) + shutil.copy2(driver_path, os.path.join(downloads_folder, filename)) + log_d("%sUnzip Complete!%s\n" % (c2, cr)) + to_remove = [ + "%s/%s/ruby_example/Gemfile" % (downloads_folder, h_ie_fn), + "%s/%s/ruby_example/Gemfile.lock" % (downloads_folder, h_ie_fn), + "%s/%s/ruby_example/ruby_example.rb" % (downloads_folder, h_ie_fn), + "%s/%s/desktop_utils.exe" % (downloads_folder, h_ie_fn), + "%s/%s/headless_ie_selenium.exe" % (downloads_folder, h_ie_fn), + "%s/%s/README.md" % (downloads_folder, h_ie_fn), + ] + for file_to_remove in to_remove: + if os.path.exists(file_to_remove): + os.remove(file_to_remove) + if os.path.exists("%s/%s/ruby_example/" % (downloads_folder, h_ie_fn)): + # Only works if the directory is empty + os.rmdir("%s/%s/ruby_example/" % (downloads_folder, h_ie_fn)) + if os.path.exists(os.path.join(downloads_folder, h_ie_fn)): + # Only works if the directory is empty + os.rmdir(os.path.join(downloads_folder, h_ie_fn)) + driver_path = os.path.join(downloads_folder, filename) + log_d( + "The file [%s] was saved to:\n%s%s%s\n" + % (filename, c3, driver_path, cr) + ) + log_d("Making [%s %s] executable ..." % (driver_file, use_version)) + make_executable(driver_path) + log_d( + "%s[%s %s] is now ready for use!%s" + % (c1, driver_file, use_version, cr) + ) - print('\nDownloading %s from:\n%s ...' % (file_name, download_url)) - remote_file = requests.get(download_url) - with open(file_path, 'wb') as file: + log_d("\nDownloading %s from:\n%s ..." % (file_name, download_url)) + remote_file = requests_get_with_retry(download_url) + with open(file_path, "wb") as file: file.write(remote_file.content) - print('Download Complete!\n') if file_name.endswith(".zip"): zip_file_path = file_path - zip_ref = zipfile.ZipFile(zip_file_path, 'r') + zip_ref = zipfile.ZipFile(zip_file_path, "r") contents = zip_ref.namelist() - if len(contents) == 1: - if name == "operadriver": - raise Exception("Zip file for OperaDriver is missing content!") + log_d("%sDownload Complete!%s\n" % (c1, cr)) + if ( + len(contents) >= 1 + and name in ["chromedriver", "uc_driver", "geckodriver"] + ): for f_name in contents: - # remove existing version if exists - new_file = downloads_folder + '/' + str(f_name) + if ( + (name == "chromedriver" or name == "uc_driver") + and ( + f_name.split("/")[-1] == "chromedriver" + or f_name.split("/")[-1] == "chromedriver.exe" + ) + ): + driver_name = f_name.split("/")[-1] + driver_contents = [driver_name] + # Remove existing version if exists + new_file = os.path.join(downloads_folder, str(f_name)) + if name == "uc_driver": + if new_file.endswith("drivers/chromedriver"): + new_file = new_file.replace( + "drivers/chromedriver", "drivers/uc_driver" + ) + elif new_file.endswith("drivers/chromedriver.exe"): + new_file = new_file.replace( + "drivers/chromedriver.exe", "drivers/uc_driver.exe" + ) + elif "drivers/%s/chromedriver" % plat_arch in new_file: + new_file = new_file.replace( + "drivers/%s/chromedriver" % plat_arch, + "drivers/%s/uc_driver" % plat_arch + ) + elif "drivers/%s/chromedriver.exe" % plat_arch in new_file: + new_file = new_file.replace( + "drivers/%s/chromedriver.exe" % plat_arch, + "drivers/%s/uc_driver.exe" % plat_arch + ) if "Driver" in new_file or "driver" in new_file: if os.path.exists(new_file): os.remove(new_file) # Technically the old file now - print('Extracting %s from %s ...' % (contents, file_name)) - zip_ref.extractall(downloads_folder) + if driver_contents: + contents = driver_contents + log_d("Extracting %s from %s ..." % (contents, file_name)) + if name == "uc_driver": + f_name = "uc_driver" + new_file = os.path.join(downloads_folder, f_name) + if os.path.exists(new_file): + os.remove(new_file) + zipinfos = zip_ref.infolist() + for zipinfo in zipinfos: + if zipinfo.filename.split("/")[-1] == "chromedriver": + zipinfo.filename = "uc_driver" + zip_ref.extract(zipinfo, downloads_folder) + elif zipinfo.filename.split("/")[-1] == "chromedriver.exe": + zipinfo.filename = "uc_driver.exe" + zip_ref.extract(zipinfo, downloads_folder) + contents = zip_ref.namelist() + if driver_contents: + contents = driver_contents + elif name == "chromedriver" or name == "uc_driver": + zipinfos = zip_ref.infolist() + for zipinfo in zipinfos: + if zipinfo.filename.split("/")[-1] == "chromedriver": + zipinfo.filename = "chromedriver" + elif zipinfo.filename.split("/")[-1] == ( + "chromedriver.exe" + ): + zipinfo.filename = "chromedriver.exe" + if ( + zipinfo.filename.split("/")[-1] == "chromedriver" + or zipinfo.filename.split("/")[-1] == ( + "chromedriver.exe" + ) + ): + zip_ref.extract(zipinfo, downloads_folder) + contents = zip_ref.namelist() + if driver_contents: + contents = driver_contents + else: + zip_ref.extractall(downloads_folder) zip_ref.close() os.remove(zip_file_path) - print('Unzip Complete!\n') + log_d("%sUnzip Complete!%s\n" % (c2, cr)) for f_name in contents: - new_file = downloads_folder + '/' + str(f_name) - print("%s saved!\n" % new_file) - print("Making %s executable ..." % new_file) + if name == "uc_driver": + if IS_WINDOWS: + f_name = "uc_driver.exe" + else: + f_name = "uc_driver" + new_file = os.path.join(downloads_folder, str(f_name)) + pr_file = c3 + new_file + cr + d_folder = os.sep.join(pr_file.split(os.sep)[:-1]) + os.sep + d_file = pr_file.split(os.sep)[-1] + d_ff = c3 + d_folder + cr + "\n" + c3 + d_file + cr + log_d("The file [%s] was saved to:\n%s\n" % (f_name, d_ff)) + log_d("Making [%s %s] executable ..." % (f_name, use_version)) make_executable(new_file) - print("%s is now ready for use!" % new_file) - print("") - elif name == "operadriver": - if len(contents) != 3: - raise Exception("Unexpected content in OperaDriver Zip file!") - elif sorted(contents) != sorted(expected_contents): - raise Exception("Unexpected content in OperaDriver Zip file!") + log_d( + "%s[%s %s] is now ready for use!%s" % + (c1, f_name, use_version, cr) + ) + if copy_to_path and os.path.exists(LOCAL_PATH): + path_file = LOCAL_PATH + f_name + shutil.copy2(new_file, path_file) + make_executable(path_file) + log_d("Also copied to: %s%s%s" % (c3, path_file, cr)) + log_d("") + elif ( + name == "edgedriver" + or name == "msedgedriver" + or name == "iedriver" + ): + if IS_MAC or IS_LINUX: + # Mac / Linux + expected_contents = [ + "Driver_Notes/", + "Driver_Notes/EULA", + "Driver_Notes/LICENSE", + "Driver_Notes/credits.html", + "msedgedriver", + "libc++.dylib", + ] + else: + # Windows + expected_contents = [ + "Driver_Notes/", + "Driver_Notes/credits.html", + "Driver_Notes/EULA", + "Driver_Notes/LICENSE", + "msedgedriver.exe", + ] + if name == "iedriver": + expected_contents = ["IEDriverServer.exe"] + if len(contents) > 5: + raise Exception("Unexpected content in EdgeDriver Zip file!") + for content in contents: + if content not in expected_contents: + raise Exception( + "Expected file [%s] missing from [%s]" + % (content, expected_contents) + ) # Zip file is valid. Proceed. driver_path = None driver_file = None for f_name in contents: - # remove existing version if exists - str_name = str(f_name).split(inner_folder)[1] - new_file = downloads_folder + '/' + str_name - if str_name == "operadriver" or str_name == "operadriver.exe": + # Remove existing version if exists + str_name = str(f_name) + new_file = os.path.join(downloads_folder, str_name) + if ( + ((IS_MAC or IS_LINUX) and str_name == "msedgedriver") + or ( + str_name == "msedgedriver.exe" + or str_name == "IEDriverServer.exe" + ) + ): driver_file = str_name driver_path = new_file if os.path.exists(new_file): os.remove(new_file) if not driver_file or not driver_path: - raise Exception("Operadriver missing from Zip file!") - print('Extracting %s from %s ...' % (contents, file_name)) + if str_name == "IEDriverServer.exe": + raise Exception("IEDriverServer missing from Zip file!") + raise Exception("msedgedriver missing from Zip file!") + log_d("Extracting %s from %s ..." % (contents, file_name)) zip_ref.extractall(downloads_folder) zip_ref.close() os.remove(zip_file_path) - print('Unzip Complete!\n') - inner_driver = downloads_folder + '/' + inner_folder + driver_file - inner_sha = downloads_folder + '/' + inner_folder + "sha512_sum" - shutil.copyfile(inner_driver, driver_path) - print("%s saved!\n" % driver_path) - print("Making %s executable ..." % driver_path) + log_d("%sUnzip Complete!%s\n" % (c2, cr)) + to_remove = [ + "%s/Driver_Notes/credits.html" % downloads_folder, + "%s/Driver_Notes/EULA" % downloads_folder, + "%s/Driver_Notes/LICENSE" % downloads_folder, + ] + for file_to_remove in to_remove: + if os.path.exists(file_to_remove): + os.remove(file_to_remove) + if os.path.exists(os.path.join(downloads_folder, "Driver_Notes/")): + # Only works if the directory is empty + os.rmdir(os.path.join(downloads_folder, "Driver_Notes/")) + driver_base = os.sep.join(driver_path.split(os.sep)[:-1]) + driver_file = driver_path.split(os.sep)[-1] + pr_driver_base = c3 + driver_base + cr + pr_sep = c3 + os.sep + cr + pr_driver_file = c3 + driver_file + cr + log_d( + "The file [%s] was saved to:\n%s%s\n%s\n" + % (driver_file, pr_driver_base, pr_sep, pr_driver_file) + ) + log_d("Making [%s %s] executable ..." % (driver_file, use_version)) make_executable(driver_path) - print("%s is now ready for use!" % driver_path) - # clean up extra files - if os.path.exists(inner_driver): - os.remove(inner_driver) - if os.path.exists(inner_sha): - os.remove(inner_sha) - if os.path.exists(downloads_folder + '/' + inner_folder): - # only works if the directory is empty - os.rmdir(downloads_folder + '/' + inner_folder) - print("") + log_d( + "%s[%s %s] is now ready for use!%s" + % (c1, driver_file, use_version, cr) + ) + if copy_to_path and os.path.exists(LOCAL_PATH): + path_file = LOCAL_PATH + f_name + shutil.copy2(new_file, path_file) + make_executable(path_file) + log_d("Also copied to: %s%s%s" % (c3, path_file, cr)) + log_d("") + elif name == "chrome" or name == "cft": + # Zip file is valid. Proceed. + driver_path = None + driver_file = None + base_path = os.sep.join(zip_file_path.split(os.sep)[:-1]) + folder_name = contents[0].split("/")[0] + folder_path = os.path.join(base_path, folder_name) + if IS_MAC or IS_LINUX: + if ( + "chrome-" in folder_path + and "drivers" in folder_path + and os.path.exists(folder_path) + ): + shutil.rmtree(folder_path) + subprocess.run( + ["unzip", zip_file_path, "-d", downloads_folder] + ) + elif IS_WINDOWS: + cmd = ( + f'Expand-Archive -Path "{zip_file_path}" ' + f'-DestinationPath "{downloads_folder}" -Force' + ) + subprocess.run(["powershell", "-Command", cmd]) + else: + zip_ref.extractall(downloads_folder) + zip_ref.close() + with suppress(Exception): + os.remove(zip_file_path) + log_d("%sUnzip Complete!%s\n" % (c2, cr)) + pr_base_path = c3 + base_path + cr + pr_sep = c3 + os.sep + cr + pr_folder_name = c3 + folder_name + cr + log_d( + "Chrome for Testing was saved inside:\n%s%s\n%s\n" + % (pr_base_path, pr_sep, pr_folder_name) + ) + elif name == "chromium": + # Zip file is valid. Proceed. + driver_path = None + driver_file = None + base_path = os.sep.join(zip_file_path.split(os.sep)[:-1]) + folder_name = contents[0].split("/")[0] + folder_path = os.path.join(base_path, folder_name) + if IS_MAC or IS_LINUX: + if ( + "chromium" in folder_path + and "drivers" in folder_path + and os.path.exists(folder_path) + ): + shutil.rmtree(folder_path) + subprocess.run( + ["unzip", zip_file_path, "-d", downloads_folder] + ) + elif IS_WINDOWS: + cmd = ( + f'Expand-Archive -Path "{zip_file_path}" ' + f'-DestinationPath "{downloads_folder}" -Force' + ) + subprocess.run(["powershell", "-Command", cmd]) + else: + zip_ref.extractall(downloads_folder) + zip_ref.close() + with suppress(Exception): + os.remove(zip_file_path) + log_d("%sUnzip Complete!%s\n" % (c2, cr)) + pr_base_path = c3 + base_path + cr + pr_sep = c3 + os.sep + cr + pr_folder_name = c3 + folder_name + cr + log_d( + "Chromium was saved inside:\n%s%s\n%s\n" + % (pr_base_path, pr_sep, pr_folder_name) + ) + elif name == "chrome-headless-shell" or name == "chs": + # Zip file is valid. Proceed. + driver_path = None + driver_file = None + base_path = os.sep.join(zip_file_path.split(os.sep)[:-1]) + folder_name = contents[0].split("/")[0] + folder_path = os.path.join(base_path, folder_name) + if IS_MAC or IS_LINUX: + if ( + "chrome-headless-shell-" in folder_path + and "drivers" in folder_path + and os.path.exists(folder_path) + ): + shutil.rmtree(folder_path) + subprocess.run( + ["unzip", zip_file_path, "-d", downloads_folder] + ) + elif IS_WINDOWS: + cmd = ( + f'Expand-Archive -Path "{zip_file_path}" ' + f'-DestinationPath "{downloads_folder}" -Force' + ) + subprocess.run(["powershell", "-Command", cmd]) + else: + zip_ref.extractall(downloads_folder) + zip_ref.close() + with suppress(Exception): + os.remove(zip_file_path) + log_d("%sUnzip Complete!%s\n" % (c2, cr)) + pr_base_path = c3 + base_path + cr + pr_sep = c3 + os.sep + cr + pr_folder_name = c3 + folder_name + cr + log_d( + "Chrome-Headless-Shell was saved inside:\n%s%s\n%s\n" + % (pr_base_path, pr_sep, pr_folder_name) + ) elif len(contents) == 0: raise Exception("Zip file %s is empty!" % zip_file_path) else: @@ -265,24 +1455,37 @@ def main(): tar = tarfile.open(file_path) contents = tar.getnames() if len(contents) == 1: + log_d("%sDownload Complete!%s\n" % (c1, cr)) for f_name in contents: - # remove existing version if exists - new_file = downloads_folder + '/' + str(f_name) + # Remove existing version if exists + new_file = os.path.join(downloads_folder, str(f_name)) if "Driver" in new_file or "driver" in new_file: if os.path.exists(new_file): os.remove(new_file) # Technically the old file now - print('Extracting %s from %s ...' % (contents, file_name)) - tar.extractall(downloads_folder) + log_d("Extracting %s from %s ..." % (contents, file_name)) + if sys.version_info < (3, 12): + tar.extractall(downloads_folder) + else: + tar.extractall(downloads_folder, filter="fully_trusted") tar.close() os.remove(tar_file_path) - print('Unzip Complete!\n') + log_d("%sUnzip Complete!%s\n" % (c2, cr)) for f_name in contents: - new_file = downloads_folder + '/' + str(f_name) - print("%s saved!\n" % new_file) - print("Making %s executable ..." % new_file) + new_file = os.path.join(downloads_folder, str(f_name)) + pr_file = c3 + new_file + cr + log_d("The file [%s] was saved to:\n%s\n" % (f_name, pr_file)) + log_d("Making [%s %s] executable ..." % (f_name, use_version)) make_executable(new_file) - print("%s is now ready for use!" % new_file) - print("") + log_d( + "%s[%s %s] is now ready for use!%s" + % (c1, f_name, use_version, cr) + ) + if copy_to_path and os.path.exists(LOCAL_PATH): + path_file = LOCAL_PATH + f_name + shutil.copy2(new_file, path_file) + make_executable(path_file) + log_d("Also copied to: %s%s%s" % (c3, path_file, cr)) + log_d("") elif len(contents) == 0: raise Exception("Tar file %s is empty!" % tar_file_path) else: @@ -290,9 +1493,11 @@ def main(): else: # Not a .zip file or a .tar.gz file. Just a direct download. if "Driver" in file_name or "driver" in file_name: - print("Making %s executable ..." % file_path) + log_d("%sDownload Complete!%s\n" % (c1, cr)) + log_d("Making [%s] executable ..." % file_name) make_executable(file_path) - print("%s is now ready for use!\n" % file_path) + log_d("%s[%s] is now ready for use!%s" % (c1, file_name, cr)) + log_d("Location of [%s]:\n%s\n" % (file_name, file_path)) if __name__ == "__main__": diff --git a/seleniumbase/console_scripts/sb_mkchart.py b/seleniumbase/console_scripts/sb_mkchart.py new file mode 100644 index 00000000000..54d8fa0ccd4 --- /dev/null +++ b/seleniumbase/console_scripts/sb_mkchart.py @@ -0,0 +1,273 @@ +""" +Creates a new SeleniumBase chart presentation with boilerplate code. + +Usage: + seleniumbase mkchart [FILE.py] [LANG] + or sbase mkchart [FILE.py] [LANG] + +Example: + sbase mkchart new_chart.py --en + +Language Options: + --en / --English | --zh / --Chinese + --nl / --Dutch | --fr / --French + --it / --Italian | --ja / --Japanese + --ko / --Korean | --pt / --Portuguese + --ru / --Russian | --es / --Spanish + +Output: + Creates a new SeleniumBase chart presentation. + If the file already exists, an error is raised. + By default, the slides are written in English, + and use a "sky" theme with "slide" transition. + The chart can be used as a basic boilerplate. +""" +import colorama +import os +import sys + + +def invalid_run_command(msg=None): + exp = " ** mkchart **\n\n" + exp += " Usage:\n" + exp += " seleniumbase mkchart [FILE.py] [LANG]\n" + exp += " OR sbase mkchart [FILE.py] [LANG]\n" + exp += " Example:\n" + exp += " sbase mkchart new_chart.py --en\n" + exp += " Language Options:\n" + exp += " --en / --English | --zh / --Chinese\n" + exp += " --nl / --Dutch | --fr / --French\n" + exp += " --it / --Italian | --ja / --Japanese\n" + exp += " --ko / --Korean | --pt / --Portuguese\n" + exp += " --ru / --Russian | --es / --Spanish\n" + exp += " Output:\n" + exp += " Creates a new SeleniumBase chart presentation.\n" + exp += " If the file already exists, an error is raised.\n" + exp += " By default, the slides are written in English,\n" + exp += ' and use a "sky" theme with "slide" transition.\n' + exp += " The chart can be used as a basic boilerplate.\n" + if not msg: + raise Exception("INVALID RUN COMMAND!\n\n%s" % exp) + elif msg == "help": + print("\n%s" % exp) + sys.exit() + else: + raise Exception("INVALID RUN COMMAND!\n\n%s\n%s\n" % (exp, msg)) + + +def main(): + c1 = "" + c5 = "" + c7 = "" + cr = "" + if "linux" not in sys.platform: + c1 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX + c5 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + c7 = colorama.Fore.BLACK + colorama.Back.MAGENTA + cr = colorama.Style.RESET_ALL + + help_me = False + error_msg = None + invalid_cmd = None + language = "English" + + command_args = sys.argv[2:] + file_name = command_args[0] + if file_name == "-h" or file_name == "--help": + invalid_run_command("help") + elif not file_name.endswith(".py"): + error_msg = 'File name must end with ".py"!' + elif "*" in file_name or len(str(file_name)) < 4: + error_msg = "Invalid file name!" + elif file_name.startswith("-"): + error_msg = 'File name cannot start with "-"!' + elif "/" in str(file_name) or "\\" in str(file_name): + error_msg = "File must be created in the current directory!" + elif os.path.exists(os.getcwd() + "/" + file_name): + error_msg = 'File "%s" already exists in this directory!' % file_name + if error_msg: + error_msg = c5 + "ERROR: " + error_msg + cr + invalid_run_command(error_msg) + + if len(command_args) >= 2: + options = command_args[1:] + for option in options: + option = option.lower() + if option == "-h" or option == "--help": + help_me = True + elif option == "--en" or option == "--english": + language = "English" + elif option == "--zh" or option == "--chinese": + language = "Chinese" + elif option == "--nl" or option == "--dutch": + language = "Dutch" + elif option == "--fr" or option == "--french": + language = "French" + elif option == "--it" or option == "--italian": + language = "Italian" + elif option == "--ja" or option == "--japanese": + language = "Japanese" + elif option == "--ko" or option == "--korean": + language = "Korean" + elif option == "--pt" or option == "--portuguese": + language = "Portuguese" + elif option == "--ru" or option == "--russian": + language = "Russian" + elif option == "--es" or option == "--spanish": + language = "Spanish" + else: + invalid_cmd = "\n===> INVALID OPTION: >> %s <<\n" % option + invalid_cmd = invalid_cmd.replace(">> ", ">>" + c5 + " ") + invalid_cmd = invalid_cmd.replace(" <<", " " + cr + "<<") + invalid_cmd = invalid_cmd.replace(">>", c7 + ">>" + cr) + invalid_cmd = invalid_cmd.replace("<<", c7 + "<<" + cr) + help_me = True + break + if help_me: + invalid_run_command(invalid_cmd) + + dir_name = os.getcwd() + file_path = "%s/%s" % (dir_name, file_name) + html_name = file_name.replace(".py", ".html") + class_name = "MyTestClass" + item = "Item" + select_option = "Select option" + chart_options = '"pie", "bar", "column", "line", "area"' + + if language == "Chinese": + class_name = "ๆˆ‘็š„ๆต‹่ฏ•็ฑป" + item = "็›ฎ็š„" + select_option = "้€‰ๆ‹ฉ้€‰้กน" + chart_options = '"้ฅผๅ›พ", "ๆกๅฝขๅ›พ", "ๆŸฑๅฝขๅ›พ", "ๆŠ˜็บฟๅ›พ", "้ข็งฏๅ›พ"' + elif language == "Dutch": + class_name = "MijnTestklasse" + item = "Voorwerp" + select_option = "Optie selecteren" + chart_options = '"cirkel", "staaf", "kolom", "lijn", "vlak"' + elif language == "French": + class_name = "MaClasseDeTest" + item = "Objet" + select_option = "Sรฉlectionner option" + chart_options = '"secteurs" "barres" "colonnes" "linรฉaire" "aires"' + elif language == "Italian": + class_name = "MiaClasseDiTest" + item = "Oggetto" + select_option = "Selezionare opzione" + chart_options = '"torta", "barre", "colonne", "linee", "area"' + elif language == "Japanese": + class_name = "็งใฎใƒ†ใ‚นใƒˆใ‚ฏใƒฉใ‚น" + item = "็‰ฉไฝ“" + select_option = "ใงใ‚ชใƒ—ใ‚ทใƒงใƒณใ‚’้ธๆŠž" + chart_options = '"ๅ††", "ๆฃ’", "็ธฆๆฃ’", "ๆŠ˜ใ‚Œ็ทš", "้ข"' + elif language == "Korean": + class_name = "ํ…Œ์ŠคํŠธ_ํด๋ž˜์Šค" + item = "๋ฌผ์ฒด" + select_option = "์˜ต์…˜ ์„ ํƒ" + chart_options = '"์›ํ˜•", "๋ง‰๋Œ€", "์—ด", "์„ ", "์˜์—ญ"' + elif language == "Portuguese": + class_name = "MinhaClasseDeTeste" + item = "Objeto" + select_option = "Selecionar opรงรฃo" + chart_options = '"pizza", "barras", "colunas", "linhas", "รกrea"' + elif language == "Russian": + class_name = "ะœะพะนะขะตัั‚ะพะฒั‹ะนะšะปะฐัั" + item = "ะ’ะตั‰ัŒ" + select_option = "ะ’ั‹ะฑั€ะฐั‚ัŒ ะพะฟั†ะธัŽ" + chart_options = '"ะบั€ัƒะณะพะฒัƒัŽ" "ะฑะฐั€" "ัั‚ะพะปะฑั‡ะฐั‚ัƒัŽ" "ะปะธะฝะตะนะฝัƒัŽ" "ะพะฑะปะฐัั‚ะธ"' + elif language == "Spanish": + class_name = "MiClaseDePrueba" + item = "Objeto" + select_option = "Seleccionar opciรณn" + chart_options = '"circular", "barras", "columnas", "lรญneas", "รกrea"' + + import_line = "from seleniumbase import BaseCase" + main_line = "BaseCase.main(__name__, __file__)" + parent_class = "BaseCase" + if language != "English": + from seleniumbase.translate.master_dict import MD_F + + import_line = MD_F.get_import_line(language) + parent_class = MD_F.get_lang_parent_class(language) + class_line = "class %s(%s):" % (class_name, parent_class) + settings = 'theme="sky", transition="slide"' + chart_settings = 'title="Chart 1"' + add_slide = '"

      Chart Demo

      " + self.extract_chart()' + data = [] + data.append("%s" % import_line) + data.append("%s" % main_line) + data.append("") + data.append("") + data.append("%s" % class_line) + data.append(" def test_chart_presentation(self):") + data.append(" self.create_presentation(%s)" % settings) + data.append("") + data.append(" # %s => %s" % (select_option, chart_options)) + data.append(" self.create_pie_chart(%s)" % chart_settings) + data.append(' self.add_data_point("%s A", 36)' % item) + data.append(' self.add_data_point("%s B", 33)' % item) + data.append(' self.add_data_point("%s C", 27)' % item) + data.append(' self.add_data_point("%s D", 21)' % item) + data.append(' self.add_data_point("%s E", 18)' % item) + data.append(' self.add_data_point("%s F", 15)' % item) + data.append(" self.add_slide(%s)" % add_slide) + data.append("") + data.append(' self.begin_presentation(filename="%s")' % html_name) + data.append("") + + new_data = [] + if language == "English": + new_data = data + else: + from seleniumbase.translate.master_dict import MD + from seleniumbase.translate.master_dict import MD_L_Codes + + md = MD.md + lang_codes = MD_L_Codes.lang + nl_code = lang_codes[language] + dl_code = lang_codes["English"] + for line in data: + found_swap = False + replace_count = line.count("self.") # Total possible replacements + for key in md.keys(): + original = "self." + md[key][dl_code] + "(" + if original in line: + replacement = "self." + md[key][nl_code] + "(" + new_line = line.replace(original, replacement) + found_swap = True + replace_count -= 1 + if replace_count == 0: + break # Done making replacements + else: + # There might be another method to replace in the line. + # Example: self.assert_true("Name" in self.get_title()) + line = new_line + continue + if main_line in line: + new_main = "%s.main(__name__, __file__)" % parent_class + new_line = line.replace(main_line, new_main) + found_swap = True + if found_swap: + if new_line.endswith(" # noqa"): # Remove flake8 skip + new_line = new_line[0 : -len(" # noqa")] + new_data.append(new_line) + continue + new_data.append(line) + data = new_data + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() + if " " not in file_name: + os.system("sbase print %s -n" % file_name) + elif '"' not in file_name: + os.system('sbase print "%s" -n' % file_name) + else: + os.system("sbase print '%s' -n" % file_name) + success = ( + "\n" + c1 + '* Chart Presentation: "' + file_name + '" was created! *' + "" + cr + "\n" + ) + print(success) + + +if __name__ == "__main__": + invalid_run_command() diff --git a/seleniumbase/console_scripts/sb_mkdir.py b/seleniumbase/console_scripts/sb_mkdir.py old mode 100755 new mode 100644 index 74b4cebdb81..67fa4e49e43 --- a/seleniumbase/console_scripts/sb_mkdir.py +++ b/seleniumbase/console_scripts/sb_mkdir.py @@ -2,264 +2,856 @@ Creates a new folder for running SeleniumBase scripts. Usage: -seleniumbase mkdir [DIRECTORY_NAME] + seleniumbase mkdir [DIRECTORY] [OPTIONS] + OR sbase mkdir [DIRECTORY] [OPTIONS] + +Example: + sbase mkdir ui_tests + +Options: + -b / --basic (Only config files. No tests added.) + --gha (Include GitHub Actions YML with defaults.) + Output: -Creates a new folder for running SeleniumBase scripts. -The new folder contains default config files, -sample tests for helping new users get started, and -Python boilerplates for setting up customized -test frameworks. + Creates a new folder for running SBase scripts. + The new folder contains default config files, + sample tests for helping new users get started, + and Python boilerplates for setting up customized + test frameworks. """ - -import codecs +import colorama import os import sys -def invalid_run_command(): - exp = (" ** mkdir **\n\n") +def invalid_run_command(msg=None): + exp = " ** mkdir **\n\n" exp += " Usage:\n" - exp += " seleniumbase mkdir [DIRECTORY_NAME]\n" + exp += " seleniumbase mkdir [DIRECTORY] [OPTIONS]\n" + exp += " OR sbase mkdir [DIRECTORY] [OPTIONS]\n" exp += " Example:\n" - exp += " seleniumbase mkdir browser_tests\n" + exp += " sbase mkdir ui_tests\n" + exp += " Options:\n" + exp += " -b / --basic (Only config files. No tests added.)\n" + exp += " --gha (Include GitHub Actions YML with defaults.)\n" exp += " Output:\n" - exp += " Creates a new folder for running SeleniumBase scripts.\n" - exp += " The new folder contains default config files,\n" - exp += " sample tests for helping new users get started, and\n" - exp += " Python boilerplates for setting up customized\n" - exp += " test frameworks.\n" - print("") - raise Exception('INVALID RUN COMMAND!\n\n%s' % exp) + exp += " Creates a new folder for running SBase scripts.\n" + exp += " The new folder contains default config files,\n" + exp += " sample tests for helping new users get started,\n" + exp += " and Python boilerplates for setting up customized\n" + exp += " test frameworks.\n" + if not msg: + raise Exception("INVALID RUN COMMAND!\n\n%s" % exp) + elif msg == "help": + print("\n%s" % exp) + sys.exit() + else: + raise Exception("INVALID RUN COMMAND!\n\n%s\n%s\n" % (exp, msg)) def main(): - num_args = len(sys.argv) - if sys.argv[0].split('/')[-1] == "seleniumbase" or ( - sys.argv[0].split('\\')[-1] == "seleniumbase"): - if num_args < 3 or num_args > 3: - invalid_run_command() - else: - invalid_run_command() - dir_name = sys.argv[num_args - 1] - if len(str(dir_name)) < 2: - raise Exception('Directory name length must be at least 2 ' - 'characters long!') - if os.path.exists(os.getcwd() + '/' + dir_name): - raise Exception('Directory "%s" already exists ' - 'in the current path!\n' % dir_name) - else: - os.mkdir(dir_name) + c1 = "" + c5 = "" + c7 = "" + cr = "" + if "linux" not in sys.platform: + c1 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX + c5 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + c7 = colorama.Fore.BLACK + colorama.Back.MAGENTA + cr = colorama.Style.RESET_ALL - data = [] - data.append("[pytest]") - data.append("addopts = --capture=no --ignore conftest.py " - "-p no:cacheprovider") - data.append("filterwarnings = ignore::pytest.PytestWarning") - file_path = "%s/%s" % (dir_name, "pytest.ini") - file = codecs.open(file_path, "w+", "utf-8") - file.writelines("\r\n".join(data)) - file.close() + gha = False + basic = False + help_me = False + error_msg = None + invalid_cmd = None - data = [] - data.append("[nosetests]") - data.append("nocapture=1") - data.append("logging-level=INFO") - data.append("") - data.append("[bdist_wheel]") - data.append("universal=1") - file_path = "%s/%s" % (dir_name, "config.cfg") - file = codecs.open(file_path, "w+", "utf-8") - file.writelines("\r\n".join(data)) - file.close() + command_args = sys.argv[2:] + dir_name = command_args[0] + if dir_name == "-h" or dir_name == "--help": + invalid_run_command("help") + elif len(str(dir_name)) < 2: + error_msg = "Directory name length must be at least 2 characters long!" + elif "/" in str(dir_name) or "\\" in str(dir_name): + error_msg = 'Directory name must not include slashes ("/", "\\")!' + elif dir_name.startswith("-"): + error_msg = 'Directory name cannot start with "-"!' + elif os.path.exists(os.getcwd() + "/" + dir_name): + error_msg = ( + 'Directory "%s" already exists in this directory!' % dir_name + ) + if error_msg: + error_msg = c5 + "ERROR: " + error_msg + cr + invalid_run_command(error_msg) - data = [] - data.append("from seleniumbase import BaseCase") - data.append("") - data.append("") - data.append("class MyTestClass(BaseCase):") - data.append("") - data.append(" def test_basic(self):") - data.append(" self.open('https://xkcd.com/353/')") - data.append(" self.assert_element('img[alt=\"Python\"]')") - data.append(" self.click('a[rel=\"license\"]')") - data.append(" self.assert_text('free to copy', 'div center')") - data.append(" self.open(\"https://xkcd.com/1481/\")") - data.append( - " title = self.get_attribute(\"#comic img\", \"title\")") - data.append( - " self.assert_true(\"86,400 seconds per day\" in title)") - data.append(" self.click('link=Blag')") - data.append( - " self.assert_text('The blag of the webcomic', 'h2')") - data.append(" self.update_text('input#s', 'Robots!\\n')") - data.append(" self.assert_text('Hooray robots!', '#content')") - data.append(" self.open('https://xkcd.com/1319/')") - data.append(" self.assert_exact_text('Automation', '#ctitle')") - data.append("") - file_path = "%s/%s" % (dir_name, "my_first_test.py") - file = codecs.open(file_path, "w+", "utf-8") - file.writelines("\r\n".join(data)) - file.close() + if len(command_args) >= 2: + options = command_args[1:] + for option in options: + option = option.lower() + if option == "-h" or option == "--help": + help_me = True + elif option == "-b" or option == "--basic": + basic = True + elif option == "--gha": + gha = True + else: + invalid_cmd = "\n===> INVALID OPTION: >> %s <<\n" % option + invalid_cmd = invalid_cmd.replace(">> ", ">>" + c5 + " ") + invalid_cmd = invalid_cmd.replace(" <<", " " + cr + "<<") + invalid_cmd = invalid_cmd.replace(">>", c7 + ">>" + cr) + invalid_cmd = invalid_cmd.replace("<<", c7 + "<<" + cr) + help_me = True + break + if help_me: + invalid_run_command(invalid_cmd) - dir_name_2 = dir_name + "/" + "boilerplates" - os.mkdir(dir_name_2) + os.mkdir(dir_name) - data = [] - data.append("") - file_path = "%s/%s" % (dir_name_2, "__init__.py") - file = codecs.open(file_path, "w+", "utf-8") - file.writelines("\r\n".join(data)) - file.close() + data = [] + seleniumbase_req = "seleniumbase" + try: + from seleniumbase import __version__ - data = [] - data.append("from seleniumbase import BaseCase") - data.append("") - data.append("") - data.append("class BaseTestCase(BaseCase):") - data.append("") - data.append(" def setUp(self):") - data.append(" super(BaseTestCase, self).setUp()") - data.append(" # Add custom setUp code AFTER the super() line") - data.append("") - data.append(" def tearDown(self):") - data.append(" # Add custom code BEFORE the super() line") - data.append(" super(BaseTestCase, self).tearDown()") - data.append("") - data.append(" def login(self):") - data.append(" # <<< Placeholder. Add your code here. >>>") - data.append(" pass") - data.append("") - data.append(" def example_method(self):") - data.append(" # <<< Placeholder. Add your code here. >>>") - data.append(" pass") - data.append("") - file_path = "%s/%s" % (dir_name_2, "base_test_case.py") - file = codecs.open(file_path, "w+", "utf-8") - file.writelines("\r\n".join(data)) - file.close() + seleniumbase_req = "seleniumbase>=%s" % str(__version__) + except Exception: + pass + data.append(seleniumbase_req) + data.append("") + file_path = "%s/%s" % (dir_name, "requirements.txt") + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() - data = [] - data.append("class Page(object):") - data.append(' html = "html"') - data.append("") - file_path = "%s/%s" % (dir_name_2, "page_objects.py") - file = codecs.open(file_path, "w+", "utf-8") - file.writelines("\r\n".join(data)) - file.close() + data = [] + data.append("[pytest]") + data.append("addopts = --capture=tee-sys -p no:cacheprovider") + data.append("norecursedirs = .* build dist recordings temp assets") + data.append("filterwarnings =") + data.append(" ignore::pytest.PytestWarning") + data.append(" ignore:.*U.*mode is deprecated:DeprecationWarning") + data.append("junit_family = legacy") + data.append("python_files = test_*.py *_test.py *_tests.py *_suite.py") + data.append("python_classes = Test* *Test* *Test *Tests *Suite") + data.append("python_functions = test_*") + data.append("markers =") + data.append(" marker1: custom marker") + data.append(" marker2: custom marker") + data.append(" marker3: custom marker") + data.append(" marker_test_suite: custom marker") + data.append(" expected_failure: custom marker") + data.append(" local: custom marker") + data.append(" remote: custom marker") + data.append(" offline: custom marker") + data.append(" develop: custom marker") + data.append(" qa: custom marker") + data.append(" ci: custom marker") + data.append(" e2e: custom marker") + data.append(" ready: custom marker") + data.append(" smoke: custom marker") + data.append(" deploy: custom marker") + data.append(" active: custom marker") + data.append(" master: custom marker") + data.append(" release: custom marker") + data.append(" staging: custom marker") + data.append(" production: custom marker") + data.append("") + file_path = "%s/%s" % (dir_name, "pytest.ini") + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() - data = [] - data.append("from .base_test_case import BaseTestCase") - data.append("from .page_objects import Page") - data.append("") - data.append("") - data.append("class MyTestClass(BaseTestCase):") - data.append("") - data.append(" def test_boilerplate(self):") - data.append(" self.login()") - data.append(" self.example_method()") - data.append(" self.assert_element(Page.html)") - data.append("") - file_path = "%s/%s" % (dir_name_2, "boilerplate_test.py") - file = codecs.open(file_path, "w+", "utf-8") - file.writelines("\r\n".join(data)) - file.close() + data = [] + data.append("[flake8]") + data.append("exclude=recordings,temp") + data.append("ignore=W503") + data.append("") + data.append("[nosetests]") + data.append("nocapture=1") + data.append("logging-level=INFO") + data.append("") + data.append("[behave]") + data.append("show_skipped=false") + data.append("show_timings=false") + file_path = "%s/%s" % (dir_name, "setup.cfg") + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() - dir_name_3 = dir_name_2 + "/" + "samples" - os.mkdir(dir_name_3) + data = [] + data.append("") + file_path = "%s/%s" % (dir_name, "__init__.py") + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() - data = [] - data.append("") - file_path = "%s/%s" % (dir_name_3, "__init__.py") - file = codecs.open(file_path, "w+", "utf-8") - file.writelines("\r\n".join(data)) - file.close() + data = [] + data.append("*.py[cod]") + data.append("*.egg") + data.append("*.egg-info") + data.append("dist") + data.append("build") + data.append(".eggs") + data.append("eggs") + data.append("parts") + data.append("bin") + data.append("var") + data.append("sdist") + data.append("develop-eggs") + data.append(".installed.cfg") + data.append("lib") + data.append("lib64") + data.append("__pycache__") + data.append(".env") + data.append(".venv") + data.append("env/") + data.append("venv/") + data.append("ENV/") + data.append("VENV/") + data.append("env.bak/") + data.append("venv.bak/") + data.append(".sbase") + data.append(".sbase*") + data.append("seleniumbase_env") + data.append("seleniumbase_venv") + data.append("sbase_env") + data.append("sbase_venv") + data.append("pyvenv.cfg") + data.append(".Python") + data.append("include") + data.append("pip-delete-this-directory.txt") + data.append("pip-selfcheck.json") + data.append("ipython.1.gz") + data.append("nosetests.1") + data.append(".noseids") + data.append("pip-log.txt") + data.append(".swp") + data.append(".coverage") + data.append(".tox") + data.append("coverage.xml") + data.append("nosetests.xml") + data.append(".cache/*") + data.append(".pytest_cache/*") + data.append(".pytest_config") + data.append("junit") + data.append("test-results.xml") + data.append(".idea") + data.append(".project") + data.append(".pydevproject") + data.append(".vscode") + data.append("chromedriver") + data.append("geckodriver") + data.append("msedgedriver") + data.append("operadriver") + data.append("uc_driver") + data.append("MicrosoftWebDriver.exe") + data.append("headless_ie_selenium.exe") + data.append("IEDriverServer.exe") + data.append("chromedriver.exe") + data.append("geckodriver.exe") + data.append("msedgedriver.exe") + data.append("operadriver.exe") + data.append("uc_driver.exe") + data.append("chrome-mac.zip") + data.append("chrome-linux.zip") + data.append("chrome-win.zip") + data.append("chrome-mac") + data.append("chrome-linux") + data.append("chrome-win") + data.append("chrome-mac-arm64.zip") + data.append("chrome-mac-x64.zip") + data.append("chrome-linux64.zip") + data.append("chrome-win64.zip") + data.append("chrome-win32.zip") + data.append("chrome-mac-arm64") + data.append("chrome-mac-x64") + data.append("chrome-linux64") + data.append("chrome-win64") + data.append("chrome-win32") + data.append("chrome-headless-shell-mac-arm64.zip") + data.append("chrome-headless-shell-mac-x64.zip") + data.append("chrome-headless-shell-linux64.zip") + data.append("chrome-headless-shell-win64.zip") + data.append("chrome-headless-shell-win32.zip") + data.append("chrome-headless-shell-mac-arm64") + data.append("chrome-headless-shell-mac-x64") + data.append("chrome-headless-shell-linux64") + data.append("chrome-headless-shell-win64") + data.append("chrome-headless-shell-win32") + data.append("libc++.dylib") + data.append("logs") + data.append("latest_logs") + data.append("log_archives") + data.append("archived_logs") + data.append("geckodriver.log") + data.append("ghostdriver.log") + data.append("pytestdebug.log") + data.append("reports/*.xml") + data.append("latest_report") + data.append("report_archives") + data.append("archived_reports") + data.append("html_report.html") + data.append("last_report.html") + data.append("report.html") + data.append("report.xml") + data.append("dashboard.html") + data.append("dashboard.json") + data.append("dash_pie.json") + data.append("dashboard.lock") + data.append("allure_report") + data.append("allure-report") + data.append("allure_results") + data.append("allure-results") + data.append("saved_charts") + data.append("saved_presentations") + data.append("tours_exported") + data.append("images_exported") + data.append("saved_cookies") + data.append("recordings") + data.append("visual_baseline") + data.append(".DS_Store") + data.append("selenium-server-standalone.jar") + data.append("proxy.zip") + data.append("proxy.lock") + data.append("verbose_hub_server.dat") + data.append("verbose_node_server.dat") + data.append("ip_of_grid_hub.dat") + data.append("downloaded_files") + data.append("archived_files") + data.append("assets") + data.append("temp") + data.append("temp_*/") + data.append("node_modules") + file_path = "%s/%s" % (dir_name, ".gitignore") + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() - data = [] - data.append("from seleniumbase import BaseCase") - data.append("from .bing_objects import Page") - data.append("") - data.append("") - data.append("class BingTests(BaseCase):") - data.append("") - data.append(" def test_bing(self):") - data.append(" self.open('https://bing.com')") - data.append(" self.assert_text('Bing', Page.logo_box)") - data.append(" self.update_text(Page.search_box, 'github')") - data.append(" self.assert_element('li[query=\"github\"]')") - data.append(" self.click(Page.search_button)") - data.append( - " self.assert_text('github.com', Page.search_results)") - data.append(" self.click_link_text('Images')") - data.append(" self.assert_element('img[alt*=\"github\"]')") - data.append("") - file_path = "%s/%s" % (dir_name_3, "bing_test.py") - file = codecs.open(file_path, "w+", "utf-8") - file.writelines("\r\n".join(data)) - file.close() + if gha: + dir_name_b = dir_name + "/" + ".github" + os.mkdir(dir_name_b) + dir_name_c = dir_name_b + "/" + "workflows" + os.mkdir(dir_name_c) data = [] - data.append("class Page(object):") - data.append(" logo_box = '#sbox div[class*=logo]'") - data.append(" search_box = 'input.b_searchbox'") - data.append(" search_button = 'input[name=\"go\"]'") - data.append(" search_results = '#b_results'") + data.append("name: CI build") + data.append("on:") + data.append(" push:") + data.append(" branches:") + data.append(" pull_request:") + data.append(" branches:") + data.append(" workflow_dispatch:") + data.append(" branches:") + data.append("jobs:") + data.append(" build:") + data.append(" env:") + data.append(' PY_COLORS: "1"') + data.append(" strategy:") + data.append(" fail-fast: false") + data.append(" max-parallel: 15") + data.append(" matrix:") + data.append(" os: [ubuntu-latest]") + data.append(' python-version: ["3.x"]') + data.append(" runs-on: ${{ matrix.os }}") + data.append(" steps:") + data.append(" - uses: actions/checkout@v6") + data.append(" - name: Set up Python ${{ matrix.python-version }}") + data.append(" uses: actions/setup-python@v6") + data.append(" with:") + data.append(" python-version: ${{ matrix.python-version }}") + data.append(" - name: Install dependencies") + data.append(" run: |") + data.append(" python -m pip install --upgrade pip") + data.append(" pip install -r requirements.txt") + data.append(" - name: Install Chrome") + data.append(" if: matrix.os == 'ubuntu-latest'") + data.append(" run: sudo apt install google-chrome-stable") + data.append(" - name: Download chromedriver") + data.append(" run: sbase get chromedriver") + data.append(" - name: Run pytest") + data.append(" run: pytest -v -s --rs --crumbs --reruns=1") + data.append(" - name: Upload artifacts") + data.append(" if: ${{ always() }}") + data.append(" uses: actions/upload-artifact@v6") + data.append(" with:") + data.append(" name: seleniumbase-artifacts") + data.append(" path: ./latest_logs/") + data.append(" if-no-files-found: ignore") data.append("") - file_path = "%s/%s" % (dir_name_3, "bing_objects.py") - file = codecs.open(file_path, "w+", "utf-8") + file_path = "%s/%s" % (dir_name_c, "python-package.yml") + file = open(file_path, mode="w+", encoding="utf-8") file.writelines("\r\n".join(data)) file.close() + if basic: data = [] - data.append("from seleniumbase import BaseCase") - data.append("from .google_objects import HomePage, ResultsPage") - data.append("") - data.append("") - data.append("class GoogleTests(BaseCase):") - data.append("") - data.append(" def test_google_dot_com(self):") - data.append(" self.open('https://google.com')") - data.append( - " self.update_text(HomePage.search_box, 'github')") - data.append(" self.assert_element(HomePage.list_box)") - data.append(" self.assert_element(HomePage.search_button)") - data.append( - " self.assert_element(HomePage.feeling_lucky_button)") - data.append(" self.click(HomePage.search_button)") - data.append( - " self.assert_text('github.com', " - "ResultsPage.search_results)") - data.append(" self.click_link_text('Images')") - data.append(" source = self.get_page_source()") - data.append( - " self.assert_true('Image result for github' in source)") - data.append("") - file_path = "%s/%s" % (dir_name_3, "google_test.py") - file = codecs.open(file_path, "w+", "utf-8") + data.append(" %s/" % dir_name) + if gha: + data.append(" โ”œโ”€โ”€ .github") + data.append(" โ”‚ โ””โ”€โ”€ workflows/") + data.append(" โ”‚ โ””โ”€โ”€ python-package.yml") + data.append(" โ”œโ”€โ”€ __init__.py") + data.append(" โ”œโ”€โ”€ pytest.ini") + data.append(" โ”œโ”€โ”€ requirements.txt") + data.append(" โ””โ”€โ”€ setup.cfg") + file_path = "%s/%s" % (dir_name, "outline.rst") + file = open(file_path, mode="w+", encoding="utf-8") file.writelines("\r\n".join(data)) file.close() + os.system("sbase print %s -n" % file_path) + os.remove(file_path) + success = ( + "\n" + c1 + '* Directory "' + dir_name + '" was created ' + "with config files! *" + cr + "\n" + ) + print(success) + return - data = [] - data.append("class HomePage(object):") - data.append(" dialog_box = '[role=\"dialog\"] div'") - data.append(" search_box = 'input[title=\"Search\"]'") - data.append(" list_box = '[role=\"listbox\"]'") - data.append(" search_button = 'input[value=\"Google Search\"]'") - data.append( - " feeling_lucky_button = " - "'''input[value=\"I'm Feeling Lucky\"]'''") - data.append("") - data.append("") - data.append("class ResultsPage(object):") - data.append(" google_logo = 'img[alt=\"Google\"]'") - data.append(" search_results = 'div#center_col'") - data.append("") - file_path = "%s/%s" % (dir_name_3, "google_objects.py") - file = codecs.open(file_path, "w+", "utf-8") - file.writelines("\r\n".join(data)) - file.close() - print('''\n* Directory "%s" was created with config files ''' - '''and sample tests! *\n''' % dir_name) + data = [] + data.append("from seleniumbase import BaseCase") + data.append("BaseCase.main(__name__, __file__)") + data.append("") + data.append("") + data.append("class MyTestClass(BaseCase):") + data.append(" def test_swag_labs(self):") + data.append(' self.goto("https://www.saucedemo.com")') + data.append(' self.type("#user-name", "standard_user")') + data.append(' self.type("#password", "secret_sauce\\n")') + data.append(' self.assert_element("div.inventory_list")') + data.append(' self.assert_text("Products", "span.title")') + data.append(" self.click('button[name*=\"backpack\"]')") + data.append(' self.click("#shopping_cart_container a")') + data.append(' self.assert_text("Your Cart", "span.title")') + data.append(' self.assert_text("Backpack", "div.cart_item")') + data.append(' self.click("button#checkout")') + data.append(' self.type("#first-name", "SeleniumBase")') + data.append(' self.type("#last-name", "Automation")') + data.append(' self.type("#postal-code", "77123")') + data.append(' self.click("input#continue")') + data.append(' self.assert_text("Checkout: Overview")') + data.append(' self.assert_text("Backpack", "div.cart_item")') + data.append(' self.click("button#finish")') + data.append( + ' self.assert_exact_text("Thank you for your order!", "h2")' + ) + data.append(" self.assert_element('img[alt=\"Pony Express\"]')") + data.append(' self.js_click("a#logout_sidebar_link")') + data.append(' self.assert_element("div#login_button_container")') + data.append("") + file_path = "%s/%s" % (dir_name, "my_first_test.py") + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() + + data = [] + data.append("from seleniumbase import BaseCase") + data.append("BaseCase.main(__name__, __file__)") + data.append("") + data.append("") + data.append("class DemoSiteTests(BaseCase):") + data.append(" def test_demo_site(self):") + data.append(' self.goto("https://seleniumbase.io/demo_page.html")') + data.append(' self.assert_title("Web Testing Page")') + data.append(' self.assert_element("tbody#tbodyId")') + data.append(' self.assert_text("Demo Page", "h1")') + data.append(' self.type("#myTextInput", "This is Automated")') + data.append(' self.type("textarea.area1", "Testing Time!\\n")') + data.append(' self.type(\'[name="preText2"]\', "Typing Text!")') + data.append( + ' self.assert_text("This is Automated", "#myTextInput")' + ) + data.append( + ' self.assert_text("Testing Time!\\n", "textarea.area1")' + ) + data.append( + ' self.assert_text("Typing Text!", \'[name="preText2"]\')' + ) + data.append(' self.assert_text("Automation Practice", "h3")') + data.append( + ' self.hover_and_js_click("#myDropdown", "#dropOption2")' + ) + data.append(' self.assert_text("Link Two Selected", "h3")') + data.append(' self.assert_text("This Text is Green", "#pText")') + data.append(' self.click("#myButton")') + data.append(' self.assert_text("This Text is Purple", "#pText")') + data.append(" self.assert_element('svg[name=\"svgName\"]')") + data.append(" self.assert_element('progress[value=\"50\"]')") + data.append(' self.press_right_arrow("#myslider", times=5)') + data.append(" self.assert_element('progress[value=\"100\"]')") + data.append(" self.assert_element('meter[value=\"0.25\"]')") + data.append( + ' self.select_option_by_text("#mySelect", "Set to 75%")' + ) + data.append(" self.assert_element('meter[value=\"0.75\"]')") + data.append(' self.assert_false(self.is_element_visible("img"))') + data.append(' self.switch_to_frame("#myFrame1")') + data.append(' self.assert_true(self.is_element_visible("img"))') + data.append(" self.switch_to_default_content()") + data.append( + ' self.assert_false(self.is_text_visible("iFrame Text"))' + ) + data.append(' self.switch_to_frame("#myFrame2")') + data.append( + ' self.assert_true(self.is_text_visible("iFrame Text"))' + ) + data.append(" self.switch_to_default_content()") + data.append(' self.assert_false(self.is_selected("#radioButton2"))') + data.append(' self.click("#radioButton2")') + data.append(' self.assert_true(self.is_selected("#radioButton2"))') + data.append(' self.assert_false(self.is_selected("#checkBox1"))') + data.append(' self.click("#checkBox1")') + data.append(' self.assert_true(self.is_selected("#checkBox1"))') + data.append(' self.assert_false(self.is_selected("#checkBox2"))') + data.append(' self.assert_false(self.is_selected("#checkBox3"))') + data.append(' self.assert_false(self.is_selected("#checkBox4"))') + data.append(' self.click_visible_elements("input.checkBoxClassB")') + data.append(' self.assert_true(self.is_selected("#checkBox2"))') + data.append(' self.assert_true(self.is_selected("#checkBox3"))') + data.append(' self.assert_true(self.is_selected("#checkBox4"))') + data.append(' self.assert_false(self.is_element_visible(".fBox"))') + data.append(' self.switch_to_frame("#myFrame3")') + data.append(' self.assert_true(self.is_element_visible(".fBox"))') + data.append(' self.assert_false(self.is_selected(".fBox"))') + data.append(' self.click(".fBox")') + data.append(' self.assert_true(self.is_selected(".fBox"))') + data.append(" self.switch_to_default_content()") + data.append( + ' self.assert_element_not_visible("div#drop2 img#logo")' + ) + data.append(' self.drag_and_drop("img#logo", "div#drop2")') + data.append(' self.assert_element("div#drop2 img#logo")') + data.append(' self.assert_link_text("seleniumbase.com")') + data.append(' self.assert_link_text("SeleniumBase on GitHub")') + data.append(' self.assert_link_text("seleniumbase.io")') + data.append(' self.click_link("SeleniumBase Demo Page")') + data.append(' self.assert_exact_text("Demo Page", "h1")') + data.append(' self.highlight("h2")') + data.append(' if self.headed:') + data.append(" self.activate_demo_mode()") + data.append(' self.type("input", "Have a Nice Day!")') + data.append(' self.assert_text("SeleniumBase", "h2")') + data.append("") + file_path = "%s/%s" % (dir_name, "test_demo_site.py") + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() + + data = [] + data.append("from parameterized import parameterized") + data.append("from seleniumbase import BaseCase") + data.append("BaseCase.main(__name__, __file__)") + data.append("") + data.append("") + data.append("class SearchTests(BaseCase):") + data.append(" @parameterized.expand(") + data.append(" [") + data.append( + " " + '["SeleniumBase Commander", "Commander", "GUI / Commander"],' + ) + data.append( + ' ["SeleniumBase Recorder", "Recorder", "Recorder Mode"],' + ) + data.append( + ' ["SeleniumBase Syntax", "Syntax", "Syntax Formats"],' + ) + data.append(" ]") + data.append(" )") + data.append( + " def test_parameterized_search(" + "self, search_term, keyword, title_text):" + ) + data.append( + ' self.goto("https://seleniumbase.io/help_docs/how_it_works/")' + ) + data.append( + ' self.type(\'[aria-label="Search"]\', search_term)' + ) + data.append(' self.click(\'mark:contains("%s")\' % keyword)') + data.append(' self.assert_title_contains(title_text)') + data.append(' self.save_screenshot_to_logs()') + data.append("") + file_path = "%s/%s" % (dir_name, "parameterized_test.py") + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() + + dir_name_2 = dir_name + "/" + "boilerplates" + os.mkdir(dir_name_2) + + data = [] + data.append("") + file_path = "%s/%s" % (dir_name_2, "__init__.py") + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() + + data = [] + data.append("from seleniumbase import BaseCase") + data.append("BaseCase.main(__name__, __file__)") + data.append("") + data.append("") + data.append("class BaseTestCase(BaseCase):") + data.append(" def setUp(self):") + data.append(" super().setUp()") + data.append(" # <<< Run custom code AFTER the super() line >>>") + data.append("") + data.append(" def tearDown(self):") + data.append(" self.save_teardown_screenshot()") + data.append(" if self.has_exception():") + data.append(" # <<< Run custom code if the test failed. >>>") + data.append(" pass") + data.append(" else:") + data.append(" # <<< Run custom code if the test passed. >>>") + data.append(" pass") + data.append(" # (Wrap unreliable code in a try/except block.)") + data.append(" # <<< Run custom code BEFORE the super() line >>>") + data.append(" super().tearDown()") + data.append("") + data.append(" def login(self):") + data.append(" # <<< Placeholder. Add your code here. >>>") + data.append(" pass") + data.append("") + data.append(" def example_method(self):") + data.append(" # <<< Placeholder. Add your code here. >>>") + data.append(" pass") + data.append("") + file_path = "%s/%s" % (dir_name_2, "base_test_case.py") + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() + + data = [] + data.append("class Page(object):") + data.append(' html = "html"') + data.append("") + file_path = "%s/%s" % (dir_name_2, "page_objects.py") + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() + + data = [] + data.append("from .base_test_case import BaseTestCase") + data.append("from .page_objects import Page") + data.append("") + data.append("") + data.append("class MyTestClass(BaseTestCase):") + data.append(" def test_boilerplate(self):") + data.append(" self.login()") + data.append(" self.example_method()") + data.append(" self.assert_element(Page.html)") + data.append("") + file_path = "%s/%s" % (dir_name_2, "boilerplate_test.py") + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() + + data = [] + data.append("from seleniumbase import BaseCase") + data.append("BaseCase.main(__name__, __file__)") + data.append("") + data.append("") + data.append("class DataPage:") + data.append(" def go_to_data_url(self, sb):") + data.append(' sb.goto("data:text/html,

      Hello!

      ")') + data.append("") + data.append(" def add_input_text(self, sb, text):") + data.append(' sb.type("input", text)') + data.append("") + data.append("") + data.append("class ObjTests(BaseCase):") + data.append(" def test_data_url_page(self):") + data.append(" DataPage().go_to_data_url(self)") + data.append(' self.assert_text("Hello!", "p")') + data.append(' DataPage().add_input_text(self, "Goodbye!")') + data.append("") + file_path = "%s/%s" % (dir_name_2, "classic_obj_test.py") + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() + + data = [] + data.append("class DataPage:") + data.append(" def go_to_data_url(self, sb):") + data.append(' sb.goto("data:text/html,

      Hello!

      ")') + data.append("") + data.append(" def add_input_text(self, sb, text):") + data.append(' sb.type("input", text)') + data.append("") + data.append("") + data.append("class ObjTests:") + data.append(" def test_data_url_page(self, sb):") + data.append(" DataPage().go_to_data_url(sb)") + data.append(' sb.assert_text("Hello!", "p")') + data.append(' DataPage().add_input_text(sb, "Goodbye!")') + data.append("") + file_path = "%s/%s" % (dir_name_2, "sb_fixture_test.py") + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() + + dir_name_3 = dir_name_2 + "/" + "samples" + os.mkdir(dir_name_3) + + data = [] + data.append("") + file_path = "%s/%s" % (dir_name_3, "__init__.py") + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() + + data = [] + data.append("from seleniumbase import BaseCase") + data.append("from .google_objects import HomePage, ResultsPage") + data.append('BaseCase.main(__name__, __file__, "--uc")') + data.append("") + data.append("") + data.append("class GoogleTests(BaseCase):") + data.append(" def test_google_dot_com(self):") + data.append(" if self.headless:") + data.append(' self.skip("Skipping test in headless mode.")') + data.append(" if not self.undetectable:") + data.append(" self.get_new_driver(undetectable=True)") + data.append(' self.goto("https://google.com/ncr")') + data.append( + " sb.click_if_visible('button:contains(\"Accept all\")')" + ) + data.append(' self.assert_title_contains("Google")') + data.append(" self.save_screenshot_to_logs()") + data.append(' self.type(HomePage.search_box, "GitHub")') + data.append(" self.assert_element(HomePage.search_button)") + data.append(" self.assert_element(HomePage.feeling_lucky_button)") + data.append(" self.click(HomePage.search_button)") + data.append(" self.sleep(1)") + data.append( + ' self.assert_text("github.com", ResultsPage.search_results)' + ) + data.append("") + file_path = "%s/%s" % (dir_name_3, "google_test.py") + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() + + data = [] + data.append("class HomePage(object):") + data.append(" dialog_box = '[role=\"dialog\"] div'") + data.append(" search_box = '[title=\"Search\"]'") + data.append(" search_button = 'input[value=\"Google Search\"]'") + data.append( + ' feeling_lucky_button = """input[value="I\'m Feeling Lucky"]"""' + ) + data.append("") + data.append("") + data.append("class ResultsPage(object):") + data.append(' search_results = "div#center_col"') + data.append("") + file_path = "%s/%s" % (dir_name_3, "google_objects.py") + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() + + data = [] + data.append('""" Classic Page Object Model with BaseCase inheritance """') + data.append("") + data.append("from seleniumbase import BaseCase") + data.append("BaseCase.main(__name__, __file__)") + data.append("") + data.append("") + data.append("class LoginPage:") + data.append(" def login_to_swag_labs(self, sb, username):") + data.append(' sb.goto("https://www.saucedemo.com/")') + data.append(' sb.type("#user-name", username)') + data.append(' sb.type("#password", "secret_sauce")') + data.append(" sb.click('input[type=\"submit\"]')") + data.append("") + data.append("") + data.append("class MyTests(BaseCase):") + data.append(" def test_swag_labs_login(self):") + data.append( + ' LoginPage().login_to_swag_labs(self, "standard_user")' + ) + data.append(' self.assert_element("#inventory_container")') + data.append( + " self.assert_element('div:contains(\"Sauce Labs Backpack\")')" + ) + data.append(' self.js_click("a#logout_sidebar_link")') + data.append(' self.assert_element("div#login_button_container")') + data.append("") + file_path = "%s/%s" % (dir_name_3, "swag_labs_test.py") + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() + + data = [] + data.append('""" Classic Page Object Model with the "sb" fixture """') + data.append("") + data.append("") + data.append("class LoginPage:") + data.append(" def login_to_swag_labs(self, sb, username):") + data.append(' sb.goto("https://www.saucedemo.com/")') + data.append(' sb.type("#user-name", username)') + data.append(' sb.type("#password", "secret_sauce")') + data.append(" sb.click('input[type=\"submit\"]')") + data.append("") + data.append("") + data.append("class MyTests:") + data.append(" def test_swag_labs_login(self, sb):") + data.append(' LoginPage().login_to_swag_labs(sb, "standard_user")') + data.append(' sb.assert_element("div.inventory_list")') + data.append( + " sb.assert_element('div:contains(\"Sauce Labs Backpack\")')" + ) + data.append(' sb.js_click("a#logout_sidebar_link")') + data.append(' sb.assert_element("div#login_button_container")') + data.append("") + file_path = "%s/%s" % (dir_name_3, "sb_swag_test.py") + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() + + data = [] + data.append(" %s/" % dir_name) + if gha: + data.append(" โ”œโ”€โ”€ .github") + data.append(" โ”‚ โ””โ”€โ”€ workflows/") + data.append(" โ”‚ โ””โ”€โ”€ python-package.yml") + data.append(" โ”œโ”€โ”€ __init__.py") + data.append(" โ”œโ”€โ”€ my_first_test.py") + data.append(" โ”œโ”€โ”€ parameterized_test.py") + data.append(" โ”œโ”€โ”€ pytest.ini") + data.append(" โ”œโ”€โ”€ requirements.txt") + data.append(" โ”œโ”€โ”€ setup.cfg") + data.append(" โ”œโ”€โ”€ test_demo_site.py") + data.append(" โ””โ”€โ”€ boilerplates/") + data.append(" โ”œโ”€โ”€ __init__.py") + data.append(" โ”œโ”€โ”€ base_test_case.py") + data.append(" โ”œโ”€โ”€ boilerplate_test.py") + data.append(" โ”œโ”€โ”€ classic_obj_test.py") + data.append(" โ”œโ”€โ”€ page_objects.py") + data.append(" โ”œโ”€โ”€ sb_fixture_test.py") + data.append(" โ””โ”€โ”€ samples/") + data.append(" โ”œโ”€โ”€ __init__.py") + data.append(" โ”œโ”€โ”€ google_objects.py") + data.append(" โ”œโ”€โ”€ google_test.py") + data.append(" โ”œโ”€โ”€ sb_swag_test.py") + data.append(" โ””โ”€โ”€ swag_labs_test.py") + file_path = "%s/%s" % (dir_name, "outline.rst") + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() + if " " not in file_path: + os.system("sbase print %s -n" % file_path) + elif '"' not in file_path: + os.system('sbase print "%s" -n' % file_path) + else: + os.system("sbase print '%s' -n" % file_path) + os.remove(file_path) + + success = ( + "\n" + c1 + '* Directory "' + dir_name + '" was created ' + "with config files and sample tests! *" + cr + "\n" + ) + print(success) if __name__ == "__main__": - main() + invalid_run_command() diff --git a/seleniumbase/console_scripts/sb_mkfile.py b/seleniumbase/console_scripts/sb_mkfile.py new file mode 100644 index 00000000000..8e5bb873604 --- /dev/null +++ b/seleniumbase/console_scripts/sb_mkfile.py @@ -0,0 +1,432 @@ +""" +Creates a new SeleniumBase test file with boilerplate code. + +Usage: + seleniumbase mkfile [FILE.py] [OPTIONS] + or sbase mkfile [FILE.py] [OPTIONS] + +Example: + sbase mkfile new_test.py + +Options: + --uc (UC Mode boilerplate using SB context manager) + -b / --basic (Basic boilerplate / single-line test) + -r / --rec (Adds Pdb+ breakpoint for Recorder Mode) + --url=URL (Makes the test start on a specific page) + +Language Options: + --en / --English | --zh / --Chinese + --nl / --Dutch | --fr / --French + --it / --Italian | --ja / --Japanese + --ko / --Korean | --pt / --Portuguese + --ru / --Russian | --es / --Spanish + +Syntax Formats: + --bc / --basecase (BaseCase class inheritance) + --pf / --pytest-fixture (sb pytest fixture) + --cf / --class-fixture (class + sb pytest fixture) + --cm / --context-manager (SB context manager) + --dc / --driver-context (DriverContext manager) + --dm / --driver-manager (Driver manager) + +Output: + Creates a new SBase test file with boilerplate code. + If the file already exists, an error is raised. + By default, uses English with BaseCase inheritance, + and creates a boilerplate with common SeleniumBase + methods: "open", "type", "click", "assert_element", + and "assert_text". If using the basic boilerplate + option, only the "open" method is included. Only the + BaseCase format supports Languages or Recorder Mode. + UC Mode automatically uses English with SB() format. +""" +import colorama +import os +import sys + + +def invalid_run_command(msg=None): + exp = " ** mkfile **\n\n" + exp += " Usage:\n" + exp += " seleniumbase mkfile [FILE.py] [OPTIONS]\n" + exp += " OR sbase mkfile [FILE.py] [OPTIONS]\n" + exp += " Example:\n" + exp += " sbase mkfile new_test.py\n" + exp += " Options:\n" + exp += " --uc (UC Mode boilerplate using SB context manager)\n" + exp += " -b / --basic (Basic boilerplate / single-line test)\n" + exp += " -r / --rec (Adds Pdb+ breakpoint for Recorder Mode)\n" + exp += " --url=URL (Makes the test start on a specific page)\n" + exp += " Language Options:\n" + exp += " --en / --English | --zh / --Chinese\n" + exp += " --nl / --Dutch | --fr / --French\n" + exp += " --it / --Italian | --ja / --Japanese\n" + exp += " --ko / --Korean | --pt / --Portuguese\n" + exp += " --ru / --Russian | --es / --Spanish\n" + exp += " Syntax Formats:\n" + exp += " --bc / --basecase (BaseCase class inheritance)\n" + exp += " --pf / --pytest-fixture (sb pytest fixture)\n" + exp += " --cf / --class-fixture (class + sb pytest fixture)\n" + exp += " --cm / --context-manager (SB context manager)\n" + exp += " --dc / --driver-context (DriverContext manager)\n" + exp += " --dm / --driver-manager (Driver manager)\n" + exp += " Output:\n" + exp += " Creates a new SBase test file with boilerplate code.\n" + exp += " If the file already exists, an error is raised.\n" + exp += " By default, uses English with BaseCase inheritance,\n" + exp += " and creates a boilerplate with common SeleniumBase\n" + exp += ' methods: "open", "type", "click", "assert_element",\n' + exp += ' and "assert_text". If using the basic boilerplate\n' + exp += ' option, only the "open" method is included. Only the\n' + exp += " BaseCase format supports Languages or Recorder Mode.\n" + exp += " UC Mode automatically uses English with SB() format.\n" + if not msg: + raise Exception("INVALID RUN COMMAND!\n\n%s" % exp) + elif msg == "help": + print("\n%s" % exp) + sys.exit() + else: + raise Exception("INVALID RUN COMMAND!\n\n%s\n%s\n" % (exp, msg)) + + +def main(): + c1 = "" + c5 = "" + c7 = "" + cr = "" + if "linux" not in sys.platform: + c1 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX + c5 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + c7 = colorama.Fore.BLACK + colorama.Back.MAGENTA + cr = colorama.Style.RESET_ALL + + basic = False + use_uc = False + help_me = False + recorder = False + error_msg = None + start_page = None + invalid_cmd = None + syntax = "BaseCase" + language = "English" + + command_args = sys.argv[2:] + file_name = command_args[0] + if file_name == "-h" or file_name == "--help": + invalid_run_command("help") + elif not file_name.endswith(".py"): + error_msg = 'File name must end with ".py"!' + elif "*" in file_name or len(str(file_name)) < 4: + error_msg = "Invalid file name!" + elif file_name.startswith("-"): + error_msg = 'File name cannot start with "-"!' + elif "/" in str(file_name) or "\\" in str(file_name): + error_msg = "File must be created in the current directory!" + elif os.path.exists(os.getcwd() + "/" + file_name): + error_msg = 'File "%s" already exists in this directory!' % file_name + if error_msg: + error_msg = c5 + "ERROR: " + error_msg + cr + invalid_run_command(error_msg) + + if len(command_args) >= 2: + options = command_args[1:] + for option in options: + option = option.lower() + if option == "-h" or option == "--help": + help_me = True + elif option.startswith("--url=") and len(option) > 6: + from seleniumbase.fixtures import page_utils + start_page = option.split("--url=")[1] + if not page_utils.is_valid_url(start_page): + if page_utils.is_valid_url("https://" + start_page): + start_page = "https://" + start_page + else: + raise Exception("Invalid URL: %s" % start_page) + basic = True + elif option == "-b" or option == "--basic": + basic = True + elif option == "-r" or option == "--rec": + recorder = True + elif option == "--record" or option == "--recorder": + recorder = True + elif use_uc: + # UC must use English & ContextManager formats + continue + elif option == "--en" or option == "--english": + language = "English" + elif option == "--zh" or option == "--chinese": + language = "Chinese" + elif option == "--nl" or option == "--dutch": + language = "Dutch" + elif option == "--fr" or option == "--french": + language = "French" + elif option == "--it" or option == "--italian": + language = "Italian" + elif option == "--ja" or option == "--japanese": + language = "Japanese" + elif option == "--ko" or option == "--korean": + language = "Korean" + elif option == "--pt" or option == "--portuguese": + language = "Portuguese" + elif option == "--ru" or option == "--russian": + language = "Russian" + elif option == "--es" or option == "--spanish": + language = "Spanish" + elif option == "--bc" or option == "--basecase": + syntax = "BaseCase" + elif option == "--pf" or option == "--pytest-fixture": + syntax = "PytestFixture" + elif option == "--cf" or option == "--class-fixture": + syntax = "ClassFixture" + elif option == "--cm" or option == "--context-manager": + syntax = "ContextManager" + elif option == "--dc" or option == "--driver-context": + syntax = "DriverContext" + elif option == "--dm" or option == "--driver-manager": + syntax = "DriverManager" + elif option == "--uc" or option == "--cdp": + basic = True + language = "English" + syntax = "ContextManager" + use_uc = True + else: + invalid_cmd = "\n===> INVALID OPTION: >> %s <<\n" % option + invalid_cmd = invalid_cmd.replace(">> ", ">>" + c5 + " ") + invalid_cmd = invalid_cmd.replace(" <<", " " + cr + "<<") + invalid_cmd = invalid_cmd.replace(">>", c7 + ">>" + cr) + invalid_cmd = invalid_cmd.replace("<<", c7 + "<<" + cr) + help_me = True + break + if help_me: + invalid_run_command(invalid_cmd) + + dir_name = os.getcwd() + file_path = "%s/%s" % (dir_name, file_name) + + body = "body" + para1 = "html body > p" + para2 = "p" + hello = "Hello" + goodbye = "Goodbye" + class_name = "MyTestClass" + if language == "Chinese": + hello = "ไฝ ๅฅฝ" + goodbye = "ๅ†่ง" + class_name = "ๆˆ‘็š„ๆต‹่ฏ•็ฑป" + elif language == "Dutch": + hello = "Hallo" + goodbye = "Dag" + class_name = "MijnTestklasse" + elif language == "French": + hello = "Bonjour" + goodbye = "Au revoir" + class_name = "MaClasseDeTest" + elif language == "Italian": + hello = "Ciao" + goodbye = "Addio" + class_name = "MiaClasseDiTest" + elif language == "Japanese": + hello = "ใ“ใ‚“ใซใกใฏ" + goodbye = "ใ•ใ‚ˆใ†ใชใ‚‰" + class_name = "็งใฎใƒ†ใ‚นใƒˆใ‚ฏใƒฉใ‚น" + elif language == "Korean": + hello = "์—ฌ๋ณด์„ธ์š”" + goodbye = "์•ˆ๋…•" + class_name = "ํ…Œ์ŠคํŠธ_ํด๋ž˜์Šค" + elif language == "Portuguese": + hello = "Olรก" + goodbye = "Tchau" + class_name = "MinhaClasseDeTeste" + elif language == "Russian": + hello = "ะŸั€ะธะฒะตั‚" + goodbye = "ะ”ะพ ัะฒะธะดะฐะฝะธั" + class_name = "ะœะพะนะขะตัั‚ะพะฒั‹ะนะšะปะฐัั" + elif language == "Spanish": + hello = "Hola" + goodbye = "Adiรณs" + class_name = "MiClaseDePrueba" + url = "" + if start_page: + url = start_page + elif basic: + url = "about:blank" + elif language not in ["English", "Dutch", "French", "Italian"]: + url = "data:text/html,

      %s
      " % hello + else: + url = "data:text/html,

      %s
      " % hello + + import_line = "from seleniumbase import BaseCase" + main_line = "BaseCase.main(__name__, __file__)" + parent_class = "BaseCase" + if language != "English": + from seleniumbase.translate.master_dict import MD_F + + import_line = MD_F.get_import_line(language) + parent_class = MD_F.get_lang_parent_class(language) + class_line = "class %s(%s):" % (class_name, parent_class) + + data = [] + data.append("%s" % import_line) + if not recorder: + data.append(main_line) + data.append("") + data.append("") + data.append("%s" % class_line) + data.append(" def test_base(self):") + if not recorder: + data.append(' self.goto("%s")' % url) + else: + data.append(" if self.recorder_ext and not self.xvfb:") + data.append(" breakpoint()") + if not basic and not recorder: + data.append( + ' self.type("input", "%s")' " # selector, text" % goodbye + ) + data.append(' self.click("%s") # selector' % para1) + data.append(' self.assert_element("%s") # selector' % body) + data.append( + ' self.assert_text("%s", "%s")' + " # text, selector" % (hello, para2) + ) + data.append("") + + new_data = [] + if language == "English" and syntax == "BaseCase": + new_data = data + elif language == "English" and syntax == "PytestFixture": + data = [] + data.append("def test_base(sb):") + data.append(' sb.goto("data:text/html,

      Hello
      ")') + if not basic: + data.append(' sb.type("input", "Goodbye") # selector, text') + data.append(' sb.click("html body > p") # selector') + data.append(' sb.assert_element("body") # selector') + data.append(' sb.assert_text("Hello", "p") # text, selector') + data.append("") + new_data = data + elif language == "English" and syntax == "ClassFixture": + data = [] + data.append("class %s:" % class_name) + data.append(" def test_base(self, sb):") + data.append(' sb.goto("data:text/html,

      Hello
      ")') + if not basic: + data.append( + ' sb.type("input", "Goodbye") # selector, text' + ) + data.append(' sb.click("html body > p") # selector') + data.append(' sb.assert_element("body") # selector') + data.append( + ' sb.assert_text("Hello", "p") # text, selector' + ) + data.append("") + new_data = data + elif language == "English" and syntax == "ContextManager": + data = [] + data.append("from seleniumbase import SB") + data.append("") + if use_uc: + data.append('with SB(uc=True) as sb:') + else: + data.append('with SB(browser="chrome") as sb:') + if use_uc: + data.append(' url = "%s"' % url) + data.append(" sb.activate_cdp_mode(url)") + data.append(" sb.solve_captcha()") + else: + data.append(' sb.goto("%s")' % url) + if not basic: + data.append(' sb.type("input", "Goodbye") # selector, text') + data.append(' sb.click("html body > p") # selector') + data.append(' sb.assert_element("input") # selector') + data.append(' sb.assert_text("Hello", "p") # text, selector') + data.append(' sb.highlight("p") # selector') + data.append(" sb.sleep(0.5) # seconds") + data.append("") + new_data = data + elif language == "English" and syntax == "DriverContext": + data = [] + data.append("from seleniumbase import DriverContext") + data.append("") + data.append('with DriverContext(browser="chrome") as driver:') + data.append(' driver.get("%s")' % url) + if not basic: + data.append(' driver.type("input", "Goodbye") # sel, text') + data.append(' driver.click("html body > p") # selector') + data.append(' driver.assert_element("input") # selector') + data.append(' driver.assert_text("Hello", "p") # text, sel') + data.append(' driver.highlight("p") # selector') + data.append(" driver.sleep(0.5) # seconds") + data.append("") + new_data = data + elif language == "English" and syntax == "DriverManager": + data = [] + data.append("from seleniumbase import Driver") + data.append("") + data.append('driver = Driver(browser="chrome")') + data.append("try:") + data.append(' driver.get("%s")' % url) + if not basic: + data.append(' driver.type("input", "Goodbye") # sel, text') + data.append(' driver.click("html body > p") # selector') + data.append(' driver.assert_element("input") # selector') + data.append(' driver.assert_text("Hello", "p") # text, sel') + data.append(' driver.highlight("p") # selector') + data.append(" driver.sleep(0.5) # seconds") + data.append("finally:") + data.append(" driver.quit()") + data.append("") + new_data = data + else: + from seleniumbase.translate.master_dict import MD + from seleniumbase.translate.master_dict import MD_L_Codes + + md = MD.md + lang_codes = MD_L_Codes.lang + nl_code = lang_codes[language] + dl_code = lang_codes["English"] + for line in data: + found_swap = False + replace_count = line.count("self.") # Total possible replacements + for key in md.keys(): + original = "self." + md[key][dl_code] + "(" + if original in line: + replacement = "self." + md[key][nl_code] + "(" + new_line = line.replace(original, replacement) + found_swap = True + replace_count -= 1 + if replace_count == 0: + break # Done making replacements + else: + # There might be another method to replace in the line. + # Example: self.assert_true("Name" in self.get_title()) + line = new_line + continue + if main_line in line: + new_main = "%s.main(__name__, __file__)" % parent_class + new_line = line.replace(main_line, new_main) + found_swap = True + if found_swap: + if new_line.endswith(" # noqa"): # Remove flake8 skip + new_line = new_line[0 : -len(" # noqa")] + new_data.append(new_line) + continue + new_data.append(line) + data = new_data + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() + if " " not in file_name: + os.system("sbase print %s -n" % file_name) + elif '"' not in file_name: + os.system('sbase print "%s" -n' % file_name) + else: + os.system("sbase print '%s' -n" % file_name) + success = ( + "\n" + c1 + '* Test file: "' + file_name + '" was created! *' + "" + cr + "\n" + ) + print(success) + + +if __name__ == "__main__": + invalid_run_command() diff --git a/seleniumbase/console_scripts/sb_mkpres.py b/seleniumbase/console_scripts/sb_mkpres.py new file mode 100644 index 00000000000..f29978d7f93 --- /dev/null +++ b/seleniumbase/console_scripts/sb_mkpres.py @@ -0,0 +1,292 @@ +""" +Creates a new SeleniumBase presentation with boilerplate code. + +Usage: + seleniumbase mkpres [FILE.py] [LANG] + or sbase mkpres [FILE.py] [LANG] + +Example: + sbase mkpres new_presentation.py --en + +Language Options: + --en / --English | --zh / --Chinese + --nl / --Dutch | --fr / --French + --it / --Italian | --ja / --Japanese + --ko / --Korean | --pt / --Portuguese + --ru / --Russian | --es / --Spanish + +Output: + Creates a new presentation with 3 example slides. + If the file already exists, an error is raised. + By default, the slides are written in English, + and use "serif" theme with "slide" transition. + The slides can be used as a basic boilerplate. +""" +import colorama +import os +import sys + + +def invalid_run_command(msg=None): + exp = " ** mkpres **\n\n" + exp += " Usage:\n" + exp += " seleniumbase mkpres [FILE.py] [LANG]\n" + exp += " OR sbase mkpres [FILE.py] [LANG]\n" + exp += " Example:\n" + exp += " sbase mkpres new_presentation.py --en\n" + exp += " Language Options:\n" + exp += " --en / --English | --zh / --Chinese\n" + exp += " --nl / --Dutch | --fr / --French\n" + exp += " --it / --Italian | --ja / --Japanese\n" + exp += " --ko / --Korean | --pt / --Portuguese\n" + exp += " --ru / --Russian | --es / --Spanish\n" + exp += " Output:\n" + exp += " Creates a new presentation with 3 example slides.\n" + exp += " If the file already exists, an error is raised.\n" + exp += " By default, the slides are written in English,\n" + exp += ' and use "serif" theme with "slide" transition.\n' + exp += " The slides can be used as a basic boilerplate.\n" + if not msg: + raise Exception("INVALID RUN COMMAND!\n\n%s" % exp) + elif msg == "help": + print("\n%s" % exp) + sys.exit() + else: + raise Exception("INVALID RUN COMMAND!\n\n%s\n%s\n" % (exp, msg)) + + +def main(): + c1 = "" + c5 = "" + c7 = "" + cr = "" + if "linux" not in sys.platform: + c1 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX + c5 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + c7 = colorama.Fore.BLACK + colorama.Back.MAGENTA + cr = colorama.Style.RESET_ALL + + help_me = False + error_msg = None + invalid_cmd = None + language = "English" + + command_args = sys.argv[2:] + file_name = command_args[0] + if file_name == "-h" or file_name == "--help": + invalid_run_command("help") + elif not file_name.endswith(".py"): + error_msg = 'File name must end with ".py"!' + elif "*" in file_name or len(str(file_name)) < 4: + error_msg = "Invalid file name!" + elif file_name.startswith("-"): + error_msg = 'File name cannot start with "-"!' + elif "/" in str(file_name) or "\\" in str(file_name): + error_msg = "File must be created in the current directory!" + elif os.path.exists(os.getcwd() + "/" + file_name): + error_msg = 'File "%s" already exists in this directory!' % file_name + if error_msg: + error_msg = c5 + "ERROR: " + error_msg + cr + invalid_run_command(error_msg) + + if len(command_args) >= 2: + options = command_args[1:] + for option in options: + option = option.lower() + if option == "-h" or option == "--help": + help_me = True + elif option == "--en" or option == "--english": + language = "English" + elif option == "--zh" or option == "--chinese": + language = "Chinese" + elif option == "--nl" or option == "--dutch": + language = "Dutch" + elif option == "--fr" or option == "--french": + language = "French" + elif option == "--it" or option == "--italian": + language = "Italian" + elif option == "--ja" or option == "--japanese": + language = "Japanese" + elif option == "--ko" or option == "--korean": + language = "Korean" + elif option == "--pt" or option == "--portuguese": + language = "Portuguese" + elif option == "--ru" or option == "--russian": + language = "Russian" + elif option == "--es" or option == "--spanish": + language = "Spanish" + else: + invalid_cmd = "\n===> INVALID OPTION: >> %s <<\n" % option + invalid_cmd = invalid_cmd.replace(">> ", ">>" + c5 + " ") + invalid_cmd = invalid_cmd.replace(" <<", " " + cr + "<<") + invalid_cmd = invalid_cmd.replace(">>", c7 + ">>" + cr) + invalid_cmd = invalid_cmd.replace("<<", c7 + "<<" + cr) + help_me = True + break + if help_me: + invalid_run_command(invalid_cmd) + + dir_name = os.getcwd() + file_path = "%s/%s" % (dir_name, file_name) + html_name = file_name.replace(".py", ".html") + + hello = "Hello" + press_right_arrow = "Press Right Arrow" + add_text = "Add Text" + goodbye = "Goodbye" + class_name = "MyTestClass" + + if language == "Chinese": + hello = "ไฝ ๅฅฝ" + press_right_arrow = "ๆŒ‰ๅ‘ๅณ็ฎญๅคด" + add_text = "ๆทปๅŠ ๆ–‡ๆœฌ" + goodbye = "ๅ†่ง" + class_name = "ๆˆ‘็š„ๆต‹่ฏ•็ฑป" + elif language == "Dutch": + hello = "Hallo" + press_right_arrow = "Druk op pijl rechts" + add_text = "Tekst Toevoegen" + goodbye = "Dag" + class_name = "MijnTestklasse" + elif language == "French": + hello = "Bonjour" + press_right_arrow = "Appuyer sur flรจche droite" + add_text = "Ajouter Texte" + goodbye = "Au revoir" + class_name = "MaClasseDeTest" + elif language == "Italian": + hello = "Ciao" + press_right_arrow = "Premere la freccia destra" + add_text = "Aggiungi Testo" + goodbye = "Addio" + class_name = "MiaClasseDiTest" + elif language == "Japanese": + hello = "ใ“ใ‚“ใซใกใฏ" + press_right_arrow = "ๅณ็Ÿขๅฐใ‚’ๆŠผใ—ใพใ™" + add_text = "ใƒ†ใ‚ญใ‚นใƒˆใ‚’่ฟฝๅŠ " + goodbye = "ใ•ใ‚ˆใ†ใชใ‚‰" + class_name = "็งใฎใƒ†ใ‚นใƒˆใ‚ฏใƒฉใ‚น" + elif language == "Korean": + hello = "์—ฌ๋ณด์„ธ์š”" + press_right_arrow = "์˜ค๋ฅธ์ชฝ ํ™”์‚ดํ‘œ๋ฅผ ๋ˆ„๋ฅด์‹ญ์‹œ์˜ค" + add_text = "ํ…์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€" + goodbye = "์•ˆ๋…•" + class_name = "ํ…Œ์ŠคํŠธ_ํด๋ž˜์Šค" + elif language == "Portuguese": + hello = "Olรก" + press_right_arrow = "Pressione a seta direita" + add_text = "Adicionar Texto" + goodbye = "Tchau" + class_name = "MinhaClasseDeTeste" + elif language == "Russian": + hello = "ะŸั€ะธะฒะตั‚" + press_right_arrow = "ะะฐะถะผะธั‚ะต ัั‚ั€ะตะปะบัƒ ะฒะฟั€ะฐะฒะพ" + add_text = "ะ”ะพะฑะฐะฒะธั‚ัŒ ะขะตะบัั‚" + goodbye = "ะ”ะพ ัะฒะธะดะฐะฝะธั" + class_name = "ะœะพะนะขะตัั‚ะพะฒั‹ะนะšะปะฐัั" + elif language == "Spanish": + hello = "Hola" + press_right_arrow = "Presione la flecha derecha" + add_text = "Agregar Texto" + goodbye = "Adiรณs" + class_name = "MiClaseDePrueba" + + import_line = "from seleniumbase import BaseCase" + main_line = "BaseCase.main(__name__, __file__)" + parent_class = "BaseCase" + if language != "English": + from seleniumbase.translate.master_dict import MD_F + + import_line = MD_F.get_import_line(language) + parent_class = MD_F.get_lang_parent_class(language) + class_line = "class %s(%s):" % (class_name, parent_class) + settings = 'theme="serif", transition="slide"' + img_src_1 = 'src="https://seleniumbase.github.io/cdn/gif/chart_pres.gif"' + img_src_2 = 'src="https://seleniumbase.github.io/cdn/img/sb_logo_10.png"' + hello_page = ( + "\n '

      %s



      '" + "\n '

      %s

      '" + "" % (hello, press_right_arrow) + ) + add_text_page = ( + "\n '

      * %s *

      '" + "\n ''" + "" % (add_text, img_src_1) + ) + goodbye_page = ( + "\n '

      %s

      '" + "\n ''" + "" % (goodbye, img_src_2) + ) + + data = [] + data.append("%s" % import_line) + data.append("%s" % main_line) + data.append("") + data.append("") + data.append("%s" % class_line) + data.append(" def test_presentation(self):") + data.append(" self.create_presentation(%s)" % settings) + data.append(" self.add_slide(%s)" % hello_page) + data.append(" self.add_slide(%s)" % add_text_page) + data.append(" self.add_slide(%s)" % goodbye_page) + data.append(' self.begin_presentation(filename="%s")' % html_name) + data.append("") + + new_data = [] + if language == "English": + new_data = data + else: + from seleniumbase.translate.master_dict import MD + from seleniumbase.translate.master_dict import MD_L_Codes + + md = MD.md + lang_codes = MD_L_Codes.lang + nl_code = lang_codes[language] + dl_code = lang_codes["English"] + for line in data: + found_swap = False + replace_count = line.count("self.") # Total possible replacements + for key in md.keys(): + original = "self." + md[key][dl_code] + "(" + if original in line: + replacement = "self." + md[key][nl_code] + "(" + new_line = line.replace(original, replacement) + found_swap = True + replace_count -= 1 + if replace_count == 0: + break # Done making replacements + else: + # There might be another method to replace in the line. + # Example: self.assert_true("Name" in self.get_title()) + line = new_line + continue + if main_line in line: + new_main = "%s.main(__name__, __file__)" % parent_class + new_line = line.replace(main_line, new_main) + found_swap = True + if found_swap: + if new_line.endswith(" # noqa"): # Remove flake8 skip + new_line = new_line[0 : -len(" # noqa")] + new_data.append(new_line) + continue + new_data.append(line) + data = new_data + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() + if " " not in file_name: + os.system("sbase print %s -n" % file_name) + elif '"' not in file_name: + os.system('sbase print "%s" -n' % file_name) + else: + os.system("sbase print '%s' -n" % file_name) + success = ( + "\n" + c1 + '* Presentation: "' + file_name + '" was created! *' + "" + cr + "\n" + ) + print(success) + + +if __name__ == "__main__": + invalid_run_command() diff --git a/seleniumbase/console_scripts/sb_mkrec.py b/seleniumbase/console_scripts/sb_mkrec.py new file mode 100644 index 00000000000..1b265ee00a6 --- /dev/null +++ b/seleniumbase/console_scripts/sb_mkrec.py @@ -0,0 +1,353 @@ +""" +** mkrec / record / codegen ** + +Creates a new SeleniumBase test file using the Recorder. + +Usage: + seleniumbase mkrec [FILE.py] [OPTIONS] + sbase mkrec [FILE.py] [OPTIONS] + seleniumbase record [FILE.py] [OPTIONS] + sbase record [FILE.py] [OPTIONS] + seleniumbase codegen [FILE.py] [OPTIONS] + sbase codegen [FILE.py] [OPTIONS] + +Examples: + sbase mkrec new_test.py + sbase mkrec new_test.py --url=seleniumbase.io + sbase codegen new_test.py + sbase codegen new_test.py --url=wikipedia.org + +Options: + --url=URL (Sets the initial start page URL.) + --edge (Use Edge browser instead of Chrome.) + --gui / --headed (Use headed mode on Linux.) + --uc / --undetected (Use undetectable mode.) + --overwrite (Overwrite file when it exists.) + --behave (Also output Behave/Gherkin files.) + +Output: + Creates a new SeleniumBase test using the Recorder. + If the filename already exists, an error is raised. +""" +import colorama +import shutil +import os +import sys + + +def invalid_run_command(msg=None): + exp = " ** mkrec / record / codegen **\n\n" + exp += " Usage:\n" + exp += " seleniumbase mkrec [FILE.py]\n" + exp += " OR: sbase mkrec [FILE.py]\n" + exp += " Examples:\n" + exp += " sbase mkrec new_test.py\n" + exp += " sbase mkrec new_test.py --url=wikipedia.org\n" + exp += " Options:\n" + exp += " --url=URL (Sets the initial start page URL.)\n" + exp += " --edge (Use Edge browser instead of Chrome.)\n" + exp += " --gui / --headed (Use headed mode on Linux.)\n" + exp += " --uc / --undetected (Use undetectable mode.)\n" + exp += " --overwrite (Overwrite file when it exists.)\n" + exp += " --behave (Also output Behave/Gherkin files.)\n" + exp += " Output:\n" + exp += " Creates a new SeleniumBase test using the Recorder.\n" + exp += " If the filename already exists, an error is raised.\n" + if not msg: + raise Exception("INVALID RUN COMMAND!\n\n%s" % exp) + elif msg == "help": + print("\n%s" % exp) + sys.exit() + else: + raise Exception("INVALID RUN COMMAND!\n\n%s\n%s\n" % (exp, msg)) + + +def set_colors(use_colors): + c0 = "" + c1 = "" + c2 = "" + c5 = "" + c7 = "" + cr = "" + if use_colors: + c0 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX + c1 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + c2 = colorama.Fore.LIGHTRED_EX + colorama.Back.LIGHTYELLOW_EX + c5 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + c7 = colorama.Fore.BLACK + colorama.Back.MAGENTA + cr = colorama.Style.RESET_ALL + return c0, c1, c2, c5, c7, cr + + +def main(): + help_me = False + error_msg = None + invalid_cmd = None + use_edge = False + use_opera = False + use_brave = False + use_comet = False + use_chromium = False + use_uc = False + esc_end = False + start_page = None + next_is_url = False + use_colors = True + force_gui = False + rec_behave = False + rec_sb_mgr = False + rec_sb_cdp = False + + sys_executable = sys.executable + if " " in sys_executable: + sys_executable = "python" + + if "linux" in sys.platform: + use_colors = False + c0, c1, c2, c5, c7, cr = set_colors(use_colors) + + command_args = sys.argv[2:] + file_name = command_args[0] + if file_name == "-h" or file_name == "--help": + invalid_run_command("help") + elif not file_name.endswith(".py"): + error_msg = 'File name must end with ".py"!' + elif "*" in file_name or len(str(file_name)) < 4: + error_msg = "Invalid file name!" + elif file_name.startswith("-"): + error_msg = 'File name cannot start with "-"!' + elif "/" in str(file_name) or "\\" in str(file_name): + error_msg = "File must be created in the current directory!" + elif file_name == "abc.py": + error_msg = '"abc.py" is a reserved Python module! Use another name!' + if error_msg: + error_msg = c5 + "ERROR: " + error_msg + cr + invalid_run_command(error_msg) + + dir_name = os.getcwd() + file_path = os.path.join(dir_name, file_name) + + if ( + "--overwrite" in " ".join(command_args).lower() + and os.path.exists(file_path) + ): + os.remove(file_path) + if os.path.exists(file_path): + error_msg = 'File "%s" already exists in this directory!' % file_name + error_msg = c5 + "ERROR: " + error_msg + cr + invalid_run_command(error_msg) + + if len(command_args) >= 2: + options = command_args[1:] + for option in options: + if option.lower() == "-h" or option.lower() == "--help": + help_me = True + elif option.lower() == "--edge": + use_edge = True + elif option.lower() == "--opera": + use_opera = True + elif option.lower() == "--brave": + use_brave = True + elif option.lower() == "--comet": + use_comet = True + elif option.lower() == "--use-chromium": + use_chromium = True + elif option.lower() == "--ee": + esc_end = True + elif option.lower() in ("--gui", "--headed"): + if "linux" in sys.platform: + force_gui = True + elif option.lower() in ( + "--uc", "--cdp", "--undetected", "--undetectable" + ): + use_uc = True + elif option.lower() in ("--rec-behave", "--behave", "--gherkin"): + rec_behave = True + elif option.lower() in ("--rec-sb-mgr"): + rec_sb_mgr = True + elif option.lower() in ("--rec-sb-cdp"): + rec_sb_cdp = True + elif option.lower().startswith("--url="): + start_page = option[len("--url="):] + elif option.lower() == "--url": + next_is_url = True + elif next_is_url: + start_page = option + next_is_url = False + elif option.lower() == "--overwrite": + pass # Already handled if file existed + else: + invalid_cmd = "\n===> INVALID OPTION: >> %s <<\n" % option + invalid_cmd = invalid_cmd.replace(">> ", ">>" + c5 + " ") + invalid_cmd = invalid_cmd.replace(" <<", " " + cr + "<<") + invalid_cmd = invalid_cmd.replace(">>", c7 + ">>" + cr) + invalid_cmd = invalid_cmd.replace("<<", c7 + "<<" + cr) + help_me = True + break + if help_me: + invalid_run_command(invalid_cmd) + + data = [] + data.append("from seleniumbase import BaseCase") + data.append("") + data.append("") + data.append("class RecorderTest(BaseCase):") + data.append(" def test_recording(self):") + if use_uc: + data.append(" if self.undetectable:") + if ( + start_page + and ( + start_page.startswith("http:") + or start_page.startswith("https:") + or start_page.startswith("file:") + ) + ): + used_sp = start_page + if '"' not in start_page: + used_sp = '"%s"' % start_page + elif "'" not in start_page: + used_sp = "'%s'" % start_page + data.append( + " self.activate_cdp_mode(\n" + " %s,\n" + " recorder=True,\n" + " )" % used_sp + ) + else: + data.append(" self.disconnect()") + data.append(" if self.recorder_ext:") + data.append(" # When done recording actions,") + data.append(' # type "c", and press [Enter].') + data.append(" breakpoint()") + data.append("") + + if esc_end: + msg = ">>> Use [SHIFT + ESC] in the browser to end recording!" + d2 = [] + d2.append("from seleniumbase import BaseCase") + d2.append("") + d2.append("") + d2.append("class RecorderTest(BaseCase):") + d2.append(" def test_recording(self):") + d2.append(" if self.recorder_ext:") + d2.append(" print(") + d2.append(' "\\n\\n%s\\n"' % msg) + d2.append(" )") + d2.append(' script = self._get_rec_shift_esc_script()') + d2.append(' esc = "return document.sb_esc_end;"') + d2.append(" start_time = self.time()") + d2.append(" last_handles_num = self._get_num_handles()") + d2.append(" for i in range(1200):") + d2.append(" try:") + d2.append(" self.execute_script(script)") + d2.append(" handles_num = self._get_num_handles()") + d2.append(" if handles_num < 1:") + d2.append(" return") + d2.append(" elif handles_num != last_handles_num:") + d2.append(" self.switch_to_window(-1)") + d2.append(" last_handles_num = handles_num") + d2.append(' if self.execute_script(esc) == "yes":') + d2.append(" return") + d2.append(" elif self.time() - start_time > 600:") + d2.append(" return") + d2.append(" self.sleep(0.5)") + d2.append(" except Exception:") + d2.append(" return") + d2.append("") + data = d2 + + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() + success = ( + "\n" + c0 + "* RECORDING initialized:" + cr + " " + "" + c1 + file_name + "" + cr + "\n" + ) + print(success) + run_cmd = None + if ( + not start_page + or ( + use_uc + and ( + start_page.startswith("http:") + or start_page.startswith("https:") + or start_page.startswith("file:") + ) + and not esc_end + ) + ): + run_cmd = "%s -m pytest %s --rec -q -s" % (sys_executable, file_name) + else: + run_cmd = "%s -m pytest %s --rec -q -s --url=%s" % ( + sys_executable, file_name, start_page + ) + if '"' not in start_page: + run_cmd = '%s -m pytest %s --rec -q -s --url="%s"' % ( + sys_executable, file_name, start_page + ) + elif "'" not in start_page: + run_cmd = "%s -m pytest %s --rec -q -s --url='%s'" % ( + sys_executable, file_name, start_page + ) + if use_edge: + run_cmd += " --edge" + elif use_opera: + run_cmd += " --opera" + elif use_brave: + run_cmd += " --brave" + elif use_comet: + run_cmd += " --comet" + elif use_chromium: + run_cmd += " --use-chromium" + if force_gui: + run_cmd += " --gui" + if use_uc: + run_cmd += " --uc" + if rec_behave: + run_cmd += " --rec-behave" + if rec_sb_mgr: + run_cmd += " --rec-sb-mgr" + if rec_sb_cdp: + run_cmd += " --rec-sb-cdp" + print(run_cmd) + os.system(run_cmd) + if os.path.exists(file_path): + os.remove(file_path) + recorded_filename = file_name[:-3] + "_rec.py" + recordings_dir = os.path.join(dir_name, "recordings") + recorded_file = os.path.join(recordings_dir, recorded_filename) + prefix = "%s -m " % sys_executable + if " " not in recorded_file: + os.system("%sseleniumbase print %s -n" % (prefix, recorded_file)) + elif '"' not in recorded_file: + os.system('%sseleniumbase print "%s" -n' % (prefix, recorded_file)) + else: + os.system("%sseleniumbase print '%s' -n" % (prefix, recorded_file)) + shutil.copy(recorded_file, file_path) + success = ( + "\n" + c2 + "***" + cr + " RECORDING COPIED to: " + "" + c1 + file_name + cr + "\n" + ) + print(success) + if rec_behave: + recorded_filename = file_name[:-3] + "_rec.feature" + recordings_dir = os.path.join(dir_name, "recordings") + features_dir = os.path.join(recordings_dir, "features") + recorded_file = os.path.join(features_dir, recorded_filename) + if " " not in recorded_file: + os.system("%sseleniumbase print %s -n" % (prefix, recorded_file)) + elif '"' not in recorded_file: + os.system('%sseleniumbase print "%s" -n' % (prefix, recorded_file)) + else: + os.system("%sseleniumbase print '%s' -n" % (prefix, recorded_file)) + success = ( + "\n" + c2 + "***" + cr + " BEHAVE RECORDING at: " + "" + c1 + os.path.relpath(recorded_file) + cr + "\n" + ) + print(success) + + +if __name__ == "__main__": + invalid_run_command() diff --git a/seleniumbase/console_scripts/sb_objectify.py b/seleniumbase/console_scripts/sb_objectify.py new file mode 100644 index 00000000000..61f63e59966 --- /dev/null +++ b/seleniumbase/console_scripts/sb_objectify.py @@ -0,0 +1,3198 @@ +""" +Converts a SeleniumBase Python file into one that uses the Page Object Pattern. + +Usage: + seleniumbase objectify [SELENIUMBASE_PYTHON_FILE].py +Output: + A modified version of the file where the selectors + have been replaced with variable names defined in + "page_objects.py", supporting the Page Object Pattern. +""" +import os +import re +import sys + +PAGE_OBJECTS_FILE = "page_objects.py" # Don't change this. It's hard-coded. +p_o_import = "from .page_objects import " +if not os.path.exists("__init__.py"): + p_o_import = "from page_objects import " + + +def invalid_run_command(shell_command): + if shell_command == "objectify": + invalid_objectify_run_command() + elif shell_command == "inject-objects": + invalid_inject_objects_run_command() + elif shell_command == "extract-objects": + invalid_extract_objects_run_command() + elif shell_command == "revert-objects": + invalid_revert_objects_run_command() + else: + invalid_objectify_run_command() + + +def invalid_objectify_run_command(): + exp = " ** objectify **\n\n" + exp += " Usage:\n" + exp += " seleniumbase objectify [SELENIUMBASE_PYTHON_FILE]\n" + exp += " Options:\n" + exp += " -c, --comments (Add object selectors to the comments.)\n" + exp += " (Default: No added comments.)\n" + exp += " Output:\n" + exp += " Converts a SeleniumBase Python file into one that uses\n" + exp += " the Page Object Pattern by converting page selectors\n" + exp += ' into objects stored in a "page_objects.py" file that is\n' + exp += " autogenerated and stored in the same folder as tests.\n" + exp += ' (seleniumbase "objectify" has the same outcome as\n' + exp += ' combining "extract-objects" with "inject-objects")\n' + raise Exception("INVALID RUN COMMAND!\n\n%s" % exp) + + +def invalid_inject_objects_run_command(): + exp = " ** inject-objects **\n\n" + exp += " Usage:\n" + exp += " seleniumbase inject-objects [SELENIUMBASE_PYTHON_FILE]\n" + exp += " Options:\n" + exp += " -c, --comments (Add object selectors to the comments.)\n" + exp += " (Default: No added comments.)\n" + exp += " Output:\n" + exp += ' Takes the page objects found in the "page_objects.py"\n' + exp += " file and uses those to replace matching selectors in\n" + exp += " the selected seleniumbase Python file.\n" + raise Exception("INVALID RUN COMMAND!\n\n%s" % exp) + + +def invalid_extract_objects_run_command(): + exp = " ** extract-objects **\n\n" + exp += " Usage:\n" + exp += " seleniumbase extract-objects [SELENIUMBASE_PYTHON_FILE]\n" + exp += " Output:\n" + exp += " Creates page objects based on selectors found in a\n" + exp += " seleniumbase Python file and saves those objects to the\n" + exp += ' "page_objects.py" file in the same folder as the tests.\n' + raise Exception("INVALID RUN COMMAND!\n\n%s" % exp) + + +def invalid_revert_objects_run_command(): + exp = " ** revert-objects **\n\n" + exp += " Usage:\n" + exp += " seleniumbase revert-objects [SELENIUMBASE_PYTHON_FILE]\n" + exp += " Options:\n" + exp += " -c, --comments (Keep existing comments for the lines.)\n" + exp += " (Default: No comments are kept.)\n" + exp += " Output:\n" + exp += ' Reverts the changes made by "seleniumbase objectify" or\n' + exp += ' "seleniumbase inject-objects" when run against a\n' + exp += " seleniumbase Python file. Objects will get replaced by\n" + exp += ' selectors stored in the "page_objects.py" file.\n' + raise Exception("INVALID RUN COMMAND!\n\n%s" % exp) + + +def remove_extra_slashes(selector): + if selector.count('\\"') > 0: + if selector.count('\\"') == selector.count('"'): + selector = selector.replace('\\"', '"') + elif selector.count('\\"') == selector[1:-1].count('"') and ( + "'" not in selector[1:-1] + ): + selector = "'" + selector[1:-1].replace('\\"', '"') + "'" + else: + pass + if selector.count("\\'") > 0: + if selector.count("\\'") == selector.count("'"): + selector = selector.replace("\\'", "'") + elif selector.count("\\'") == selector[1:-1].count("'") and ( + '"' not in selector[1:-1] + ): + selector = '"' + selector[1:-1].replace("\\'", "'") + '"' + else: + pass + return selector + + +def create_objects_file(selector_list_dict=None): + data = [] + if selector_list_dict: + data.append("# PAGE OBJECTS FILE >>> (autogenerated)") + data.append("") + for key in selector_list_dict.keys(): + if key == "None": + if len(selector_list_dict["None"]) > 0: + for pair in selector_list_dict["None"]: + data.append("%s = %s" % (pair[0], pair[1])) + data.append("") + else: + pass + else: + data.append("") + data.append("class %s(object):" % key) + for pair in selector_list_dict[key]: + data.append(" %s = %s" % (pair[0], pair[1])) + data.append("") + else: + data.append("") + data.append("class Page(object):") + data.append(' html = "html"') + data.append("") + file_path = PAGE_OBJECTS_FILE + file = open(file_path, mode="w+", encoding="utf-8") + file.writelines("\r\n".join(data)) + file.close() + if not selector_list_dict: + print('\n>>> ["%s"] was created!' % file_path) + else: + print('\n>>> ["%s"] was updated!' % file_path) + + +def scan_objects_file(): + if not os.path.exists(PAGE_OBJECTS_FILE): + create_objects_file() + + page_selectors = {} + with open(PAGE_OBJECTS_FILE, mode="r", encoding="utf-8") as f: + all_code = f.read() + + var_names = [] + selectors = [] + current_class = "None" + selector_list_dict = {} # Key = class name / Values are name-value tuples + selector_list_dict[current_class] = [] + selector_list_dict["Page"] = [] + code_lines = all_code.split("\n") + for line in code_lines: + line = line.rstrip() + # Handle: class CLASSNAME(object): OR class CLASSNAME(): + data = re.match(r"""^(\s*)class\s+([\S]+)\((object|)\):\s*$""", line) + if data: + whitespace = data.group(1) + name = "%s" % data.group(2) + current_class = name + selector_list_dict[current_class] = [] + page_selectors["class %s" % name] = "." + + # Handle: SELECTOR_NAME = "SELECTOR" + data = re.match(r"""^(\s*)(\S+)\s*=\s*([\S\s]+)\s*$""", line) + if data: + whitespace = data.group(1) + name = data.group(2) + selector = data.group(3) + selector = remove_extra_slashes(selector) + page_selectors[name] = selector + var_names.append(name) + selectors.append(selector) + if whitespace == "": + current_class = "None" + if (len(selector) > 2 and selector[0] == "'") and ( + selector[-1] == "'" and '"' not in selector[1:-1] + ): + selector = '"' + selector[1:-1] + '"' + if "\\[" in selector or "\\]" in selector or "\\." in selector: + if selector[0] != "r": + selector = "r" + selector + selector_list_dict[current_class].append((name, selector)) + + if "class Page" not in page_selectors.keys(): + page_selectors["class Page"] = "." + + if len(selector_list_dict["Page"]) == 0: + selector_list_dict["Page"].append(("html", '"html"')) + return var_names, selectors, selector_list_dict + + +def optimize_selector(selector): + if (len(selector) > 2 and selector[0] == "'") and ( + selector[-1] == "'" and '"' not in selector[1:-1] + ): + selector = '"' + selector[1:-1] + '"' + if "\\[" in selector or "\\]" in selector or "\\." in selector: + if selector[0] != "r": + selector = "r" + selector + return selector + + +def get_next_var_name(existing_names): + base_name = "css_" + for i in range(1, 99999): + new_name = "%s%s" % (base_name, str(i)) + if new_name not in existing_names: + return new_name + raise Exception("Out of range! (Selector name generation)") + + +def process_test_file( + code_lines, selector_dict=None, object_dict=None, add_comments=False +): + + seleniumbase_lines = [] + page_selectors = [] + changed = [] # The classes of page_objects.py to add to the test import + + for line in code_lines: + line = line.rstrip() + + # Keep lines with "%s" in them as they were + if r"%s" in line: + seleniumbase_lines.append(line) + continue + + # Handle self.drag_and_drop(SELECTOR, SELECTOR) + if not object_dict: + data = re.match( + r"""^(\s*)self\.drag_and_drop""" + r"""\((r?['"][\S\s]+['"]),\s?([\S\s]+)\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.drag_and_drop""" + r"""\(([\S]+),\s?([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + selector1 = "%s" % data.group(2) + selector1 = remove_extra_slashes(selector1) + page_selectors.append(selector1) + selector2 = "%s" % data.group(3) + selector2 = remove_extra_slashes(selector2) + page_selectors.append(selector2) + comments = data.group(4) + command = """%sself.drag_and_drop(%s, %s)%s""" % ( + whitespace, + selector1, + selector2, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s, %s" % (selector1, selector2) + selector1 = optimize_selector(selector1) + selector2 = optimize_selector(selector2) + if selector1 in selector_dict.keys() and ( + selector2 in selector_dict.keys() + ): + selector_object1 = selector_dict[selector1] + selector_object2 = selector_dict[selector2] + changed.append(selector_object1.split(".")[0]) + changed.append(selector_object2.split(".")[0]) + command = """%sself.drag_and_drop(%s, %s)%s""" % ( + whitespace, + selector_object1, + selector_object2, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name1 = selector1 + object_name2 = selector2 + if object_name1 in object_dict.keys() and ( + object_name2 in object_dict.keys() + ): + selector_object1 = object_dict[object_name1] + selector_object2 = object_dict[object_name2] + changed.append(object_name1.split(".")[0]) + changed.append(object_name2.split(".")[0]) + command = """%sself.drag_and_drop(%s, %s)%s""" % ( + whitespace, + selector_object1, + selector_object2, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.hover_and_click(SELECTOR, SELECTOR) + if not object_dict: + data = re.match( + r"""^(\s*)self\.hover_and_click""" + r"""\((r?['"][\S\s]+['"]),\s?([\S\s]+)\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.hover_and_click""" + r"""\(([\S]+),\s?([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + selector1 = "%s" % data.group(2) + selector1 = remove_extra_slashes(selector1) + page_selectors.append(selector1) + selector2 = "%s" % data.group(3) + selector2 = remove_extra_slashes(selector2) + page_selectors.append(selector2) + comments = data.group(4) + command = """%sself.hover_and_click(%s, %s)%s""" % ( + whitespace, + selector1, + selector2, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s, %s" % (selector1, selector2) + selector1 = optimize_selector(selector1) + selector2 = optimize_selector(selector2) + if selector1 in selector_dict.keys() and ( + selector2 in selector_dict.keys() + ): + selector_object1 = selector_dict[selector1] + selector_object2 = selector_dict[selector2] + changed.append(selector_object1.split(".")[0]) + changed.append(selector_object2.split(".")[0]) + command = """%sself.hover_and_click(%s, %s)%s""" % ( + whitespace, + selector_object1, + selector_object2, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name1 = selector1 + object_name2 = selector2 + if object_name1 in object_dict.keys() and ( + object_name2 in object_dict.keys() + ): + selector_object1 = object_dict[object_name1] + selector_object2 = object_dict[object_name2] + changed.append(object_name1.split(".")[0]) + changed.append(object_name2.split(".")[0]) + command = """%sself.hover_and_click(%s, %s)%s""" % ( + whitespace, + selector_object1, + selector_object2, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.*_click(SELECTOR, OTHER_ARGS) + if not object_dict: + data = re.match( + r"""^(\s*)self\.(\S*)_click""" + r"""\((r?['"][\S\s]+['"]),([\S\s]*)\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.(\S*)_click""" + r"""\(([\S]+),([\S\s]*)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + action = data.group(2) + selector = "%s" % data.group(3) + selector = remove_extra_slashes(selector) + other_args = data.group(4) + page_selectors.append(selector) + comments = data.group(5) + command = """%sself.%s_click(%s,%s)%s""" % ( + whitespace, + action, + selector, + other_args, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.%s_click(%s,%s)%s""" % ( + whitespace, + action, + selector_object, + other_args, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.%s_click(%s,%s)%s""" % ( + whitespace, + action, + selector_object, + other_args, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.*_click(SELECTOR) + if not object_dict: + data = re.match( + r"""^(\s*)self\.(\S*)_click""" + r"""\((r?['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.(\S*)_click""" + r"""\(([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + action = data.group(2) + selector = "%s" % data.group(3) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(4) + command = """%sself.%s_click(%s)%s""" % ( + whitespace, + action, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.%s_click(%s)%s""" % ( + whitespace, + action, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.%s_click(%s)%s""" % ( + whitespace, + action, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.click(SELECTOR) + if not object_dict: + data = re.match( + r"""^(\s*)self\.click""" + r"""\((r?['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.click""" + r"""\(([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + selector = "%s" % data.group(2) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(3) + command = """%sself.click(%s)%s""" % ( + whitespace, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.click(%s)%s""" % ( + whitespace, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.click(%s)%s""" % ( + whitespace, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.js_click_*(SELECTOR) * = all/if_present/if_visible + if not object_dict: + data = re.match( + r"""^(\s*)self\.js_click_(\S*)""" + r"""\((r?['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.js_click_(\S*)""" + r"""\(([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + by_type = data.group(2) + selector = "%s" % data.group(3) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(4) + command = """%sself.js_click_%s(%s)%s""" % ( + whitespace, + by_type, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.js_click_%s(%s)%s""" % ( + whitespace, + by_type, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.js_click_%s(%s)%s""" % ( + whitespace, + by_type, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.click(SELECTOR, OTHER_ARGS) + if not object_dict: + data = re.match( + r"""^(\s*)self\.click""" + r"""\((r?['"][\S\s]+['"]),([\S\s]*)\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.click""" + r"""\(([\S]+),([\S\s]*)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + selector = "%s" % data.group(2) + selector = remove_extra_slashes(selector) + other_args = data.group(3) + page_selectors.append(selector) + comments = data.group(4) + command = """%sself.click(%s,%s)%s""" % ( + whitespace, + selector, + other_args, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.click(%s,%s)%s""" % ( + whitespace, + selector_object, + other_args, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.click(%s,%s)%s""" % ( + whitespace, + selector_object, + other_args, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.click_if_visible(SELECTOR) + if not object_dict: + data = re.match( + r"""^(\s*)self\.click_if_visible""" + r"""\((r?['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.click_if_visible""" + r"""\(([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + selector = "%s" % data.group(2) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(3) + command = """%sself.click_if_visible(%s)%s""" % ( + whitespace, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.click_if_visible(%s)%s""" % ( + whitespace, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.click_if_visible(%s)%s""" % ( + whitespace, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.hover(SELECTOR) + if not object_dict: + data = re.match( + r"""^(\s*)self\.hover""" + r"""\((r?['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.hover""" + r"""\(([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + selector = "%s" % data.group(2) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(3) + command = """%sself.hover(%s)%s""" % ( + whitespace, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.hover(%s)%s""" % ( + whitespace, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.hover(%s)%s""" % ( + whitespace, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.*_element(SELECTOR) + if not object_dict: + data = re.match( + r"""^(\s*)self\.(\S*)_element""" + r"""\((r?['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.(\S*)_element""" + r"""\(([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + action = data.group(2) + selector = "%s" % data.group(3) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(4) + command = """%sself.%s_element(%s)%s""" % ( + whitespace, + action, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.%s_element(%s)%s""" % ( + whitespace, + action, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.%s_element(%s)%s""" % ( + whitespace, + action, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.*_elements(SELECTOR) + if not object_dict: + data = re.match( + r"""^(\s*)self\.(\S*)_elements""" + r"""\((r?['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.(\S*)_elements""" + r"""\(([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + action = data.group(2) + selector = "%s" % data.group(3) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(4) + command = """%sself.%s_elements(%s)%s""" % ( + whitespace, + action, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.%s_elements(%s)%s""" % ( + whitespace, + action, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.%s_elements(%s)%s""" % ( + whitespace, + action, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.set_text_content(SELECTOR, OTHER_ARGS) + if not object_dict: + data = re.match( + r"""^(\s*)self\.set_text_content""" + r"""\((r?['"][\S\s]+['"]),([\S\s]*)\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.set_text_content""" + r"""\(([\S]+),([\S\s]*)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + selector = "%s" % data.group(2) + selector = remove_extra_slashes(selector) + other_args = data.group(3) + page_selectors.append(selector) + comments = data.group(4) + command = """%sself.set_text_content(%s,%s)%s""" % ( + whitespace, + selector, + other_args, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.set_text_content(%s,%s)%s""" % ( + whitespace, + selector_object, + other_args, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.set_text_content(%s,%s)%s""" % ( + whitespace, + selector_object, + other_args, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.highlight(SELECTOR, OTHER_ARGS) + if not object_dict: + data = re.match( + r"""^(\s*)self\.highlight""" + r"""\((r?['"][\S\s]+['"]),([\S\s]*)\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.highlight""" + r"""\(([\S]+),([\S\s]*)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + selector = "%s" % data.group(2) + selector = remove_extra_slashes(selector) + other_args = data.group(3) + page_selectors.append(selector) + comments = data.group(4) + command = """%sself.highlight(%s,%s)%s""" % ( + whitespace, + selector, + other_args, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.highlight(%s,%s)%s""" % ( + whitespace, + selector_object, + other_args, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.highlight(%s,%s)%s""" % ( + whitespace, + selector_object, + other_args, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.highlight(SELECTOR) + if not object_dict: + data = re.match( + r"""^(\s*)self\.highlight""" + r"""\((r?['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.highlight""" + r"""\(([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + selector = "%s" % data.group(2) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(3) + command = """%sself.highlight(%s)%s""" % ( + whitespace, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.highlight(%s)%s""" % ( + whitespace, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.highlight(%s)%s""" % ( + whitespace, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.check_if_unchecked(SELECTOR) + if not object_dict: + data = re.match( + r"""^(\s*)self\.check_if_unchecked""" + r"""\((r?['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.check_if_unchecked""" + r"""\(([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + selector = "%s" % data.group(2) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(3) + command = """%sself.check_if_unchecked(%s)%s""" % ( + whitespace, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.check_if_unchecked(%s)%s""" % ( + whitespace, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.check_if_unchecked(%s)%s""" % ( + whitespace, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.uncheck_if_checked(SELECTOR) + if not object_dict: + data = re.match( + r"""^(\s*)self\.uncheck_if_checked""" + r"""\((r?['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.uncheck_if_checked""" + r"""\(([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + selector = "%s" % data.group(2) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(3) + command = """%sself.uncheck_if_checked(%s)%s""" % ( + whitespace, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.uncheck_if_checked(%s)%s""" % ( + whitespace, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.uncheck_if_checked(%s)%s""" % ( + whitespace, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.select_if_unselected(SELECTOR) + if not object_dict: + data = re.match( + r"""^(\s*)self\.select_if_unselected""" + r"""\((r?['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.select_if_unselected""" + r"""\(([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + selector = "%s" % data.group(2) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(3) + command = """%sself.select_if_unselected(%s)%s""" % ( + whitespace, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.select_if_unselected(%s)%s""" % ( + whitespace, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.select_if_unselected(%s)%s""" % ( + whitespace, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.unselect_if_selected(SELECTOR) + if not object_dict: + data = re.match( + r"""^(\s*)self\.unselect_if_selected""" + r"""\((r?['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.unselect_if_selected""" + r"""\(([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + selector = "%s" % data.group(2) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(3) + command = """%sself.unselect_if_selected(%s)%s""" % ( + whitespace, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.unselect_if_selected(%s)%s""" % ( + whitespace, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.unselect_if_selected(%s)%s""" % ( + whitespace, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.switch_to_frame(SELECTOR) + if not object_dict: + data = re.match( + r"""^(\s*)self\.switch_to_frame""" + r"""\((r?['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.switch_to_frame""" + r"""\(([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + selector = "%s" % data.group(2) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(3) + command = """%sself.switch_to_frame(%s)%s""" % ( + whitespace, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.switch_to_frame(%s)%s""" % ( + whitespace, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.switch_to_frame(%s)%s""" % ( + whitespace, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle with self.frame_switch(SELECTOR): + if not object_dict: + data = re.match( + r"""^(\s*)(\S*)\sself\.frame_switch""" + r"""\((r?['"][\S\s]+['"])\):([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)(\S*)\sself\.frame_switch""" + r"""\(([\S]+)\):([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + if_type = data.group(2) + selector = "%s" % data.group(3) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(4) + command = """%s%s self.frame_switch(%s):%s""" % ( + whitespace, + if_type, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%s%s self.frame_switch(%s):%s""" % ( + whitespace, + if_type, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%s%s self.frame_switch(%s):%s""" % ( + whitespace, + if_type, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.assert_element_*(SELECTOR) *= present/not_visible/absent + if not object_dict: + data = re.match( + r"""^(\s*)self\.assert_element_(\S*)""" + r"""\((r?['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.assert_element_(\S*)""" + r"""\(([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + by_type = data.group(2) + selector = "%s" % data.group(3) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(4) + command = """%sself.assert_element_%s(%s)%s""" % ( + whitespace, + by_type, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.assert_element_%s(%s)%s""" % ( + whitespace, + by_type, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.assert_element_%s(%s)%s""" % ( + whitespace, + by_type, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.wait_for_element_*(SELECTOR) * = present/visible + if not object_dict: + data = re.match( + r"""^(\s*)self\.wait_for_element_(\S*)""" + r"""\((r?['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.wait_for_element_(\S*)""" + r"""\(([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + by_type = data.group(2) + selector = "%s" % data.group(3) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(4) + command = """%sself.wait_for_element_%s(%s)%s""" % ( + whitespace, + by_type, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.wait_for_element_%s(%s)%s""" % ( + whitespace, + by_type, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.wait_for_element_%s(%s)%s""" % ( + whitespace, + by_type, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.update_text(SELECTOR, TEXT) + if not object_dict: + data = re.match( + r"""^(\s*)self\.update_text""" + r"""\((r?['"][\S\s]+['"]),\s?([\S\s]+)\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.update_text""" + r"""\(([\S]+),\s?([\S\s]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + selector = "%s" % data.group(2) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + text = data.group(3) + comments = data.group(4) + command = """%sself.update_text(%s, %s)%s""" % ( + whitespace, + selector, + text, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.update_text(%s, %s)%s""" % ( + whitespace, + selector_object, + text, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.update_text(%s, %s)%s""" % ( + whitespace, + selector_object, + text, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.type(SELECTOR, TEXT) + if not object_dict: + data = re.match( + r"""^(\s*)self\.type""" + r"""\((r?['"][\S\s]+['"]),\s?([\S\s]+)\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.type""" + r"""\(([\S]+),\s?([\S\s]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + selector = "%s" % data.group(2) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + text = data.group(3) + comments = data.group(4) + command = """%sself.type(%s, %s)%s""" % ( + whitespace, + selector, + text, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.type(%s, %s)%s""" % ( + whitespace, + selector_object, + text, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.type(%s, %s)%s""" % ( + whitespace, + selector_object, + text, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.input(SELECTOR, TEXT) + if not object_dict: + data = re.match( + r"""^(\s*)self\.input""" + r"""\((r?['"][\S\s]+['"]),\s?([\S\s]+)\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.input""" + r"""\(([\S]+),\s?([\S\s]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + selector = "%s" % data.group(2) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + text = data.group(3) + comments = data.group(4) + command = """%sself.input(%s, %s)%s""" % ( + whitespace, + selector, + text, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.input(%s, %s)%s""" % ( + whitespace, + selector_object, + text, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.input(%s, %s)%s""" % ( + whitespace, + selector_object, + text, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.write(SELECTOR, TEXT) + if not object_dict: + data = re.match( + r"""^(\s*)self\.write""" + r"""\((r?['"][\S\s]+['"]),\s?([\S\s]+)\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.write""" + r"""\(([\S]+),\s?([\S\s]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + selector = "%s" % data.group(2) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + text = data.group(3) + comments = data.group(4) + command = """%sself.write(%s, %s)%s""" % ( + whitespace, + selector, + text, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.write(%s, %s)%s""" % ( + whitespace, + selector_object, + text, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.write(%s, %s)%s""" % ( + whitespace, + selector_object, + text, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.add_text(SELECTOR, TEXT) + if not object_dict: + data = re.match( + r"""^(\s*)self\.add_text""" + r"""\((r?['"][\S\s]+['"]),\s?([\S\s]+)\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.add_text""" + r"""\(([\S]+),\s?([\S\s]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + selector = "%s" % data.group(2) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + text = data.group(3) + comments = data.group(4) + command = """%sself.add_text(%s, %s)%s""" % ( + whitespace, + selector, + text, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.add_text(%s, %s)%s""" % ( + whitespace, + selector_object, + text, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.add_text(%s, %s)%s""" % ( + whitespace, + selector_object, + text, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.send_keys(SELECTOR, TEXT) + if not object_dict: + data = re.match( + r"""^(\s*)self\.send_keys""" + r"""\((r?['"][\S\s]+['"]),\s?([\S\s]+)\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.send_keys""" + r"""\(([\S]+),\s?([\S\s]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + selector = "%s" % data.group(2) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + text = data.group(3) + comments = data.group(4) + command = """%sself.send_keys(%s, %s)%s""" % ( + whitespace, + selector, + text, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.send_keys(%s, %s)%s""" % ( + whitespace, + selector_object, + text, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.send_keys(%s, %s)%s""" % ( + whitespace, + selector_object, + text, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.set_value(SELECTOR, TEXT) + if not object_dict: + data = re.match( + r"""^(\s*)self\.set_value""" + r"""\((r?['"][\S\s]+['"]),\s?([\S\s]+)\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.set_value""" + r"""\(([\S]+),\s?([\S\s]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + selector = "%s" % data.group(2) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + text = data.group(3) + comments = data.group(4) + command = """%sself.set_value(%s, %s)%s""" % ( + whitespace, + selector, + text, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.set_value(%s, %s)%s""" % ( + whitespace, + selector_object, + text, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.set_value(%s, %s)%s""" % ( + whitespace, + selector_object, + text, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.press_*_arrow(SELECTOR) + if not object_dict: + data = re.match( + r"""^(\s*)self\.press_(\S*)_arrow""" + r"""\((r?['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.press_(\S*)_arrow""" + r"""\(([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + arrow = "%s" % data.group(2) + selector = "%s" % data.group(3) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(4) + command = """%sself.press_%s_arrow(%s)%s""" % ( + whitespace, + arrow, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.press_%s_arrow(%s)%s""" % ( + whitespace, + arrow, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.press_%s_arrow(%s)%s""" % ( + whitespace, + arrow, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.press_*_arrow(SELECTOR, TIMES) + if not object_dict: + data = re.match( + r"""^(\s*)self\.press_(\S*)_arrow""" + r"""\((r?['"][\S\s]+['"]),\s?([\S\s]+)\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.press_(\S*)_arrow""" + r"""\(([\S]+),\s?([\S\s]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + arrow = "%s" % data.group(2) + selector = "%s" % data.group(3) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + times = data.group(4) + comments = data.group(5) + command = """%sself.press_%s_arrow(%s, %s)%s""" % ( + whitespace, + arrow, + selector, + times, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.press_%s_arrow(%s, %s)%s""" % ( + whitespace, + arrow, + selector_object, + times, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.press_%s_arrow(%s, %s)%s""" % ( + whitespace, + arrow, + selector_object, + times, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.assert_text(TEXT, SELECTOR) + if not object_dict: + data = re.match( + r"""^(\s*)self\.assert_text""" + r"""\(([\S\s]+),\s?(r?['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.assert_text""" + r"""\(([\S\s]+),\s?([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + text = data.group(2) + selector = "%s" % data.group(3) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(4) + command = """%sself.assert_text(%s, %s)%s""" % ( + whitespace, + text, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.assert_text(%s, %s)%s""" % ( + whitespace, + text, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.assert_text(%s, %s)%s""" % ( + whitespace, + text, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.assert_exact_text(TEXT, SELECTOR) + if not object_dict: + data = re.match( + r"""^(\s*)self\.assert_exact_text""" + r"""\(([\S\s]+),\s?(r?['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.assert_exact_text""" + r"""\(([\S\s]+),\s?([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + text = data.group(2) + selector = "%s" % data.group(3) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(4) + command = """%sself.assert_exact_text(%s, %s)%s""" % ( + whitespace, + text, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.assert_exact_text(%s, %s)%s""" % ( + whitespace, + text, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.assert_exact_text(%s, %s)%s""" % ( + whitespace, + text, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.find_text(TEXT, SELECTOR) + if not object_dict: + data = re.match( + r"""^(\s*)self\.find_text""" + r"""\(([\S\s]+),\s?(r?['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.find_text""" + r"""\(([\S\s]+),\s?([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + text = data.group(2) + selector = "%s" % data.group(3) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(4) + command = """%sself.find_text(%s, %s)%s""" % ( + whitespace, + text, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.find_text(%s, %s)%s""" % ( + whitespace, + text, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.find_text(%s, %s)%s""" % ( + whitespace, + text, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle if/elif self.is_text_*(TEXT, SELECTOR): * = present/visible + if not object_dict: + data = re.match( + r"""^(\s*)(\S*)\sself\.is_text_(\S*)""" + r"""\(([\S\s]+),\s?(r?['"][\S\s]+['"])\):([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)(\S*)\sself\.is_text_(\S*)""" + r"""\(([\S\s]+),\s?([\S]+)\):([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + if_type = data.group(2) + by_type = data.group(3) + text = data.group(4) + selector = "%s" % data.group(5) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(6) + command = """%s%s self.is_text_%s(%s, %s):%s""" % ( + whitespace, + if_type, + by_type, + text, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%s%s self.is_text_%s(%s, %s):%s""" % ( + whitespace, + if_type, + by_type, + text, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%s%s self.is_text_%s(%s, %s):%s""" % ( + whitespace, + if_type, + by_type, + text, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.wait_for_text(TEXT, SELECTOR) + if not object_dict: + data = re.match( + r"""^(\s*)self\.wait_for_text""" + r"""\(([\S\s]+),\s?(r?['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.wait_for_text""" + r"""\(([\S\s]+),\s?([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + text = data.group(2) + selector = "%s" % data.group(3) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(4) + command = """%sself.wait_for_text(%s, %s)%s""" % ( + whitespace, + text, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.wait_for_text(%s, %s)%s""" % ( + whitespace, + text, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.wait_for_text(%s, %s)%s""" % ( + whitespace, + text, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.wait_for_text_visible(TEXT, SELECTOR) + if not object_dict: + data = re.match( + r"""^(\s*)self\.wait_for_text_visible""" + r"""\(([\S\s]+),\s?(r?['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.wait_for_text_visible""" + r"""\(([\S\s]+),\s?([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + text = data.group(2) + selector = "%s" % data.group(3) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(4) + command = """%sself.wait_for_text(%s, %s)%s""" % ( + whitespace, + text, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.wait_for_text(%s, %s)%s""" % ( + whitespace, + text, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.wait_for_text(%s, %s)%s""" % ( + whitespace, + text, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.wait_for_text_not_visible(TEXT, SELECTOR) + if not object_dict: + data = re.match( + r"""^(\s*)self\.wait_for_text_not_visible""" + r"""\(([\S\s]+),\s?(r?['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.wait_for_text_not_visible""" + r"""\(([\S\s]+),\s?([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + text = data.group(2) + selector = "%s" % data.group(3) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(4) + command = """%sself.wait_for_text_not_visible(%s, %s)%s""" % ( + whitespace, + text, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = "%sself.wait_for_text_not_visible(%s, %s)%s" % ( + whitespace, + text, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = "%sself.wait_for_text_not_visible(%s, %s)%s" % ( + whitespace, + text, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle if/elif self.is_element_*(SELECTOR): * = present/visible + if not object_dict: + data = re.match( + r"""^(\s*)(\S*)\sself\.is_element_(\S*)""" + r"""\((r?['"][\S\s]+['"])\):([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)(\S*)\sself\.is_element_(\S*)""" + r"""\(([\S]+)\):([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + if_type = data.group(2) + by_type = data.group(3) + selector = "%s" % data.group(4) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(5) + command = """%s%s self.is_element_%s(%s):%s""" % ( + whitespace, + if_type, + by_type, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%s%s self.is_element_%s(%s):%s""" % ( + whitespace, + if_type, + by_type, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%s%s self.is_element_%s(%s):%s""" % ( + whitespace, + if_type, + by_type, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle if/elif self.is_selected(SELECTOR): + if not object_dict: + data = re.match( + r"""^(\s*)(\S*)\sself\.is_selected""" + r"""\((r?['"][\S\s]+['"])\):([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)(\S*)\sself\.is_selected""" + r"""\(([\S]+)\):([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + if_type = data.group(2) + selector = "%s" % data.group(3) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(4) + command = """%s%s self.is_selected(%s):%s""" % ( + whitespace, + if_type, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%s%s self.is_selected(%s):%s""" % ( + whitespace, + if_type, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%s%s self.is_selected(%s):%s""" % ( + whitespace, + if_type, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.assert*(self.is_selected(SELECTOR)) + if not object_dict: + data = re.match( + r"""^(\s*)self\.assert(\S*)\(self\.is_selected""" + r"""\((r?['"][\S\s]+['"])\)\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.assert(\S*)\(self\.is_selected""" + r"""\(([\S]+)\)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + a_type = data.group(2) + selector = "%s" % data.group(3) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(4) + command = """%sself.assert%s(self.is_selected(%s))%s""" % ( + whitespace, + a_type, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.assert%s(self.is_selected(%s))%s""" % ( + whitespace, + a_type, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.assert%s(self.is_selected(%s))%s""" % ( + whitespace, + a_type, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.assert*(self.is_element_*(SELECTOR)) + if not object_dict: + data = re.match( + r"""^(\s*)self\.assert(\S*)\(self\.is_element_(\S*)""" + r"""\((r?['"][\S\s]+['"])\)\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.assert(\S*)\(self\.is_element_(\S*)""" + r"""\(([\S]+)\)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + a_type = data.group(2) + v_type = data.group(3) + selector = "%s" % data.group(4) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(5) + command = """%sself.assert%s(self.is_element_%s(%s))%s""" % ( + whitespace, + a_type, + v_type, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = "%sself.assert%s(self.is_element_%s(%s))%s" % ( + whitespace, + a_type, + v_type, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = "%sself.assert%s(self.is_element_%s(%s))%s" % ( + whitespace, + a_type, + v_type, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.set_attribute(SELECTOR, ATTRIBUTE, VALUE) + if not object_dict: + data = re.match( + r"""^(\s*)self\.set_attribute""" + r"""\((r?['"][\S\s]+['"])""" + r""",\s?([\S\s]+),\s?([\S\s]+)\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.set_attribute""" + r"""\(([\S]+),\s?([\S\s]+),\s?([\S\s]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + selector = "%s" % data.group(2) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + attribute = data.group(3) + value = data.group(4) + comments = data.group(5) + command = """%sself.set_attribute(%s, %s, %s)%s""" % ( + whitespace, + selector, + attribute, + value, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.set_attribute(%s, %s, %s)%s""" % ( + whitespace, + selector_object, + attribute, + value, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.set_attribute(%s, %s, %s)%s""" % ( + whitespace, + selector_object, + attribute, + value, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.set_attributes(SELECTOR, ATTRIBUTE, VALUE) + if not object_dict: + data = re.match( + r"""^(\s*)self\.set_attributes""" + r"""\((r?['"][\S\s]+['"]),""" + r"""\s?([\S\s]+),\s?([\S\s]+)\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.set_attributes""" + r"""\(([\S]+),\s?([\S\s]+),\s?([\S\s]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + selector = "%s" % data.group(2) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + attribute = data.group(3) + value = data.group(4) + comments = data.group(5) + command = """%sself.set_attributes(%s, %s, %s)%s""" % ( + whitespace, + selector, + attribute, + value, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.set_attributes(%s, %s, %s)%s""" % ( + whitespace, + selector_object, + attribute, + value, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.set_attributes(%s, %s, %s)%s""" % ( + whitespace, + selector_object, + attribute, + value, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle VAR = self.get_attribute(SELECTOR, ATTRIBUTE) + if not object_dict: + data = re.match( + r"""^(\s*)(\S*)\s?=\s?self\.get_attribute""" + r"""\((r?['"][\S\s]+['"]),\s?(['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)(\S*)\s?=\s?self\.get_attribute""" + r"""\(([\S]+),\s?(['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + var_name = data.group(2) + selector = "%s" % data.group(3) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + attribute = data.group(4) + comments = data.group(5) + command = """%s%s = self.get_attribute(%s, %s)%s""" % ( + whitespace, + var_name, + selector, + attribute, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%s%s = self.get_attribute(%s, %s)%s""" % ( + whitespace, + var_name, + selector_object, + attribute, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%s%s = self.get_attribute(%s, %s)%s""" % ( + whitespace, + var_name, + selector_object, + attribute, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle VAR = self.get_text(SELECTOR) + if not object_dict: + data = re.match( + r"""^(\s*)(\S*)\s?=\s?self\.get_text""" + r"""\((r?['"][\S\s]+['"])\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)(\S*)\s?=\s?self\.get_text""" + r"""\(([\S]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + var_name = data.group(2) + selector = "%s" % data.group(3) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(4) + command = """%s%s = self.get_text(%s)%s""" % ( + whitespace, + var_name, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%s%s = self.get_text(%s)%s""" % ( + whitespace, + var_name, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%s%s = self.get_text(%s)%s""" % ( + whitespace, + var_name, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle if VAR [in|not in] self.get_text(SELECTOR): + if not object_dict: + data = re.match( + r"""^(\s*)if\s(\S*\s?\S*)\sin\s?self\.get_text""" + r"""\((r?['"][\S\s]+['"])\):([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)if\s(\S*\s?\S*)\sin\s?self\.get_text""" + r"""\(([\S]+)\):([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + var_prefix = data.group(2) + selector = "%s" % data.group(3) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + comments = data.group(4) + command = """%sif %s in self.get_text(%s):%s""" % ( + whitespace, + var_prefix, + selector, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sif %s in self.get_text(%s):%s""" % ( + whitespace, + var_prefix, + selector_object, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sif %s in self.get_text(%s):%s""" % ( + whitespace, + var_prefix, + selector_object, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.*_element(SELECTOR, OTHER_ARGS) * = remove/hide + if not object_dict: + data = re.match( + r"""^(\s*)self\.(\S*)_element""" + r"""\((r?['"][\S\s]+['"]),([\S\s]*)\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.(\S*)_element""" + r"""\(([\S]+),([\S\s]*)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + action = data.group(2) + selector = "%s" % data.group(3) + selector = remove_extra_slashes(selector) + other_args = data.group(4) + page_selectors.append(selector) + comments = data.group(5) + command = """%sself.%s_element(%s,%s)%s""" % ( + whitespace, + action, + selector, + other_args, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.%s_element(%s,%s)%s""" % ( + whitespace, + action, + selector_object, + other_args, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.%s_element(%s,%s)%s""" % ( + whitespace, + action, + selector_object, + other_args, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.*_elements(SELECTOR, OTHER_ARGS) * = remove/hide + if not object_dict: + data = re.match( + r"""^(\s*)self\.(\S*)_elements""" + r"""\((r?['"][\S\s]+['"]),([\S\s]*)\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.(\S*)_elements""" + r"""\(([\S]+),([\S\s]*)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + action = data.group(2) + selector = "%s" % data.group(3) + selector = remove_extra_slashes(selector) + other_args = data.group(4) + page_selectors.append(selector) + comments = data.group(5) + command = """%sself.%s_elements(%s,%s)%s""" % ( + whitespace, + action, + selector, + other_args, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.%s_elements(%s,%s)%s""" % ( + whitespace, + action, + selector_object, + other_args, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.%s_elements(%s,%s)%s""" % ( + whitespace, + action, + selector_object, + other_args, + comments, + ) + seleniumbase_lines.append(command) + continue + + # Handle self.select_option_by_*(SELECTOR, TEXT) * = index/value/text + if not object_dict: + data = re.match( + r"""^(\s*)self\.select_option_by_(\S*)""" + r"""\((r?['"][\S\s]+['"]),\s?([\S\s]+)\)([\S\s]*)$""", + line, + ) + else: + data = re.match( + r"""^(\s*)self\.select_option_by_(\S*)""" + r"""\(([\S]+),\s?([\S\s]+)\)([\S\s]*)$""", + line, + ) + if data: + whitespace = data.group(1) + by_type = data.group(2) + selector = "%s" % data.group(3) + selector = remove_extra_slashes(selector) + page_selectors.append(selector) + text = data.group(4) + comments = data.group(5) + command = """%sself.select_option_by_%s(%s, %s)%s""" % ( + whitespace, + by_type, + selector, + text, + comments, + ) + if selector_dict: + if add_comments: + comments = " # %s" % selector + selector = optimize_selector(selector) + if selector in selector_dict.keys(): + selector_object = selector_dict[selector] + changed.append(selector_object.split(".")[0]) + command = """%sself.select_option_by_%s(%s, %s)%s""" % ( + whitespace, + by_type, + selector_object, + text, + comments, + ) + if object_dict: + if not add_comments: + comments = "" + object_name = selector + if object_name in object_dict.keys(): + selector_object = object_dict[object_name] + changed.append(object_name.split(".")[0]) + command = """%sself.select_option_by_%s(%s, %s)%s""" % ( + whitespace, + by_type, + selector_object, + text, + comments, + ) + seleniumbase_lines.append(command) + continue + + # seleniumbase_lines.append("### " + line) # untouched lines (Debug) + seleniumbase_lines.append(line) + continue + + return seleniumbase_lines, page_selectors, changed + + +def extract_objects(): + main(shell_command="extract-objects") + + +def inject_objects(): + main(shell_command="inject-objects") + + +def objectify(): + main(shell_command="objectify") + + +def revert_objects(): + main(shell_command="revert-objects") + + +def main(shell_command): + expected_arg = "[A SeleniumBase Python file]" + num_args = len(sys.argv) + command_args = sys.argv[2:] + + add_comments = False + if shell_command == "objectify" or ( + shell_command == "inject-objects" + or (shell_command == "revert-objects") + ): + if len(command_args) >= 2: + options = command_args[1:] + for option in options: + if option == "-c" or option == "--comments": + add_comments = True + else: + invalid_run_command(shell_command) + + if ( + sys.argv[0].split("/")[-1] == "seleniumbase" + or (sys.argv[0].split("\\")[-1] == "seleniumbase") + or (sys.argv[0].split("/")[-1] == "sbase") + or (sys.argv[0].split("\\")[-1] == "sbase") + ): + if num_args < 3: + invalid_run_command(shell_command) + elif num_args > 3: + if shell_command == "extract-objects": + invalid_run_command(shell_command) + else: + pass + else: + pass + else: + invalid_run_command(shell_command) + + seleniumbase_file = command_args[0] + if not seleniumbase_file.endswith(".py"): + raise Exception( + "\n\n`%s` is not a Python file!\n\n" + "Expecting: %s\n" % (seleniumbase_file, expected_arg) + ) + + with open(seleniumbase_file, mode="r", encoding="utf-8") as f: + all_code = f.read() + if "def test_" not in all_code: + raise Exception( + "\n\n`%s` is not a valid SeleniumBase unittest file!\n" + "\nExpecting: %s\n" % (seleniumbase_file, expected_arg) + ) + code_lines = all_code.split("\n") + seleniumbase_lines, page_selectors, changed = process_test_file(code_lines) + var_names, existing_selectors, selector_list_dict = scan_objects_file() + new_page_selectors = [] + + for selector in page_selectors: + selector = optimize_selector(selector) + if selector not in existing_selectors: + new_page_selectors.append(selector) + var_name = get_next_var_name(var_names) + var_names.append(var_name) + selector_list_dict["Page"].append((var_name, selector)) + existing_selectors.append(selector) + + # print(new_page_selectors) # (For debugging) + # print(selector_list_dict) # (For debugging) + + if shell_command == "extract-objects" or shell_command == "objectify": + create_objects_file(selector_list_dict) + + if shell_command == "extract-objects": + print("") + return + + selector_dict = {} # Key: selector, Value: object + object_dict = {} # Key: object, Value: selector + for key in selector_list_dict.keys(): + for pair in selector_list_dict[key]: + selector_dict[pair[1]] = "%s.%s" % (str(key), str(pair[0])) + object_name = "%s.%s" % (str(key), str(pair[0])) + object_dict[object_name] = pair[1] + + good_sel_dict = {} + aa, bb, cc = scan_objects_file() + for s_key in selector_dict.keys(): + if s_key in bb: + good_sel_dict[s_key] = selector_dict[s_key] + + if shell_command == "inject-objects" or shell_command == "objectify": + seleniumbase_lines, page_selectors, changed = process_test_file( + code_lines, selector_dict=good_sel_dict, add_comments=add_comments + ) + added_classes = [] + for item in changed: + if item not in added_classes: + added_classes.append(item) + for line in seleniumbase_lines: + if p_o_import in line: + token = line.split(p_o_import)[1].strip() + if token in added_classes: + # Don't import page_objects classes if already imported + added_classes.remove(token) + if added_classes: + sb_lines = [] + fit_in = False + for line in seleniumbase_lines: + if line.startswith("from") and "import" in line and not fit_in: + fit_in = True + for add_me in added_classes: + import_line = "%s%s" % (p_o_import, add_me) + sb_lines.append(import_line) + sb_lines.append(line) + seleniumbase_lines = sb_lines + + if shell_command == "revert-objects": + seleniumbase_lines, page_selectors, changed = process_test_file( + code_lines, object_dict=object_dict, add_comments=add_comments + ) + removed_classes = [] + for item in changed: + if item not in removed_classes: + removed_classes.append(item) + if removed_classes: + sb_lines = [] + for line in seleniumbase_lines: + if p_o_import in line: + token = line.split(p_o_import)[1].strip() + if token in removed_classes: + continue + sb_lines.append(line) + seleniumbase_lines = sb_lines + + seleniumbase_code = "" + for line in seleniumbase_lines: + seleniumbase_code += line + seleniumbase_code += "\n" + seleniumbase_code = seleniumbase_code[:-1] + # print (seleniumbase_code) # (For debugging) + + # Create SeleniumBase test file + base_file_name = seleniumbase_file.split(".py")[0] + converted_file_name = base_file_name + ".py" # Change end to make a copy + out_file = open(converted_file_name, mode="w+", encoding="utf-8") + out_file.writelines(seleniumbase_code) + out_file.close() + print('\n>>> ["%s"] was updated!\n' % converted_file_name) + + +if __name__ == "__main__": + invalid_objectify_run_command() diff --git a/seleniumbase/console_scripts/sb_print.py b/seleniumbase/console_scripts/sb_print.py new file mode 100644 index 00000000000..d6833930b76 --- /dev/null +++ b/seleniumbase/console_scripts/sb_print.py @@ -0,0 +1,636 @@ +""" +Prints the code/text of any file with syntax-highlighting + +Usage: + seleniumbase print [FILE] [OPTIONS] + OR: sbase print [FILE] [OPTIONS] +Options: + -n (Add line Numbers to the rows) +Output: + Prints the code/text of any file + with syntax-highlighting. +""" +import colorama +import os +import sys + + +def invalid_run_command(msg=None): + exp = " ** print **\n\n" + exp += " Usage:\n" + exp += " seleniumbase print [FILE] [OPTIONS]\n" + exp += " OR: sbase print [FILE] [OPTIONS]\n" + exp += " Options:\n" + exp += " -n (Add line Numbers to the rows)\n" + exp += " Output:\n" + exp += " Prints the code/text of any file\n" + exp += " with syntax-highlighting.\n" + if not msg: + raise Exception("INVALID RUN COMMAND!\n\n%s" % exp) + else: + raise Exception("INVALID RUN COMMAND!\n%s\n\n%s" % (msg, exp)) + + +def sc_ranges(): + # Get the ranges of special double-width characters. + special_char_ranges = [ + {"from": ord("\u4e00"), "to": ord("\u9FFF")}, + {"from": ord("\u3040"), "to": ord("\u30ff")}, + {"from": ord("\uac00"), "to": ord("\ud7a3")}, + {"from": ord("\uff01"), "to": ord("\uff60")}, + ] + return special_char_ranges + + +def is_char_wide(char): + # Returns True if the special character is Chinese, Japanese, or Korean. + sc = any( + [range["from"] <= ord(char) <= range["to"] for range in sc_ranges()] + ) + return sc + + +def get_width(line): + # Return the true width of the line. Not the same as line length. + # Chinese/Japanese/Korean characters take up two spaces of width. + line_length = len(line) + for char in line: + if is_char_wide(char): + line_length += 1 + return line_length + + +def main(): + c5 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + c7 = colorama.Fore.BLACK + colorama.Back.MAGENTA + cr = colorama.Style.RESET_ALL + line_numbers = False + word_wrap = True # Always use word wrap now + help_me = False + invalid_cmd = None + is_python_file = False + code_lang = None + + command_args = sys.argv[2:] + file_to_print = command_args[0] + if file_to_print.lower().endswith(".py"): + is_python_file = True + code_lang = "python" + elif file_to_print.lower().endswith(".js"): + code_lang = "javascript" + elif file_to_print.lower().endswith(".md"): + code_lang = "markdown" + elif file_to_print.lower().endswith(".html"): + code_lang = "html" + elif file_to_print.lower().endswith(".css"): + code_lang = "css" + elif file_to_print.lower().endswith(".go"): + code_lang = "go" + elif file_to_print.lower().endswith(".java"): + code_lang = "java" + elif file_to_print.lower().endswith(".feature"): + code_lang = "gherkin" + elif file_to_print.lower().endswith(".txt"): + code_lang = "javascript" + elif file_to_print.lower().endswith(".yml"): + code_lang = "javascript" + elif file_to_print.lower().endswith(".in"): + code_lang = "javascript" + elif "." not in file_to_print: + code_lang = "markdown" + else: + code_lang = file_to_print.split(".")[-1].lower() + + if len(command_args) >= 2: + options = command_args[1:] + for option in options: + option = option.lower() + if option == "-n": + line_numbers = True + elif option == "-w": + word_wrap = True + else: + invalid_cmd = "\n===> INVALID OPTION: >> %s <<\n" % option + invalid_cmd = invalid_cmd.replace(">> ", ">>" + c5 + " ") + invalid_cmd = invalid_cmd.replace(" <<", " " + cr + "<<") + invalid_cmd = invalid_cmd.replace(">>", c7 + ">>" + cr) + invalid_cmd = invalid_cmd.replace("<<", c7 + "<<" + cr) + help_me = True + break + + if help_me: + invalid_run_command(invalid_cmd) + + all_code = None + with open( + file_to_print, mode="r+", encoding="utf-8", errors="ignore" + ) as f: + all_code = f.read() + all_code = all_code.replace("\t", " ") + code_lines = all_code.split("\n") + + console_width = None # width of console output when running script + used_width = None # code_width and few spaces on right for padding + magic_syntax = None # the syntax generated by rich.syntax.Syntax() + try: + console_width = os.get_terminal_size().columns + if console_width: + console_width = int(console_width) + except Exception: + console_width = None + + from seleniumbase.console_scripts import rich_helper + + the_code = "\n".join(code_lines) + code_width = 1 + w = 0 # line number whitespace + if line_numbers: + w = 4 + num_lines = len(code_lines) + if num_lines >= 10: + w = 5 + if num_lines >= 100: + w = 6 + if num_lines >= 1000: + w = 7 + if num_lines >= 10000: + w = 8 + + if is_python_file: + new_sb_lines = [] + for line in code_lines: + if line.endswith(" # noqa") and line.count(" # noqa") == 1: + line = line.replace(" # noqa", "") + line_length2 = len(line) # Normal Python string length used + line_length = get_width(line) # Special characters count 2X + if line_length > code_width: + code_width = line_length + + if console_width: + # If line is larger than console_width, try to optimize it. + # Smart Python word wrap to be used with valid indentation. + if line_length + w > console_width: # 5 is line number ws + if line.strip().startswith("#"): + new_sb_lines.append(line) + continue + elif ( + line.count(" # ") == 1 + and get_width(line.split(" # ")[0]) + w + <= console_width + ): + # Line is short enough once comment is removed + line = line.split(" # ")[0] + new_sb_lines.append(line) + continue + elif ( + line.count(" # ") == 1 + and get_width(line.split(" # ")[0]) + w + <= console_width + ): + # L-Length good if removing bad flake8 comment + line = line.split(" # ")[0] + new_sb_lines.append(line) + continue + if line.startswith("from") and " import " in line: + line1 = line.split(" import ")[0] + " \\" + line2 = " import " + line.split(" import ")[1] + new_sb_lines.append(line1) + new_sb_lines.append(line2) + continue + if ( + line.count("(") >= 1 + and line.count("(") == line.count(")") + ): + whitespace = line_length2 - len(line.lstrip()) + new_ws = line[0:whitespace] + " " + first_paren = line.find("(") + line1 = line[:first_paren + 1] + line2 = new_ws + line[first_paren + 1:] + if ("):") not in line2: + new_sb_lines.append(line1) + if get_width(line2) + w > console_width: + if line2.count('", "') == 1: + line2a = line2.split('", "')[0] + '",' + line2b = ( + new_ws + + '"' + + (line2.split('", "')[1]) + ) + new_sb_lines.append(line2a) + new_sb_lines.append(line2b) + continue + elif line2.count("', '") == 1: + line2a = line2.split("', '")[0] + "'," + line2b = ( + new_ws + + "'" + + (line2.split("', '")[1]) + ) + new_sb_lines.append(line2a) + new_sb_lines.append(line2b) + continue + elif line2.count("://") == 1 and ( + line2.count('")') == 1 + ): + line2a = line2.split("://")[0] + '://"' + line2b = ( + new_ws + + '"' + + (line2.split("://")[1]) + ) + new_sb_lines.append(line2a) + if get_width(line2b) + w > ( + console_width + ): + if line2b.count("/") > 0: + slash_one = line2b.find("/") + slash_one_p1 = slash_one + 1 + line2b1 = ( + line2b[: slash_one + 1] + + '"' + ) + line2b2 = ( + new_ws + + '"' + + (line2b[slash_one_p1:]) + ) + new_sb_lines.append(line2b1) + if line2b2.count(") # ") == 1: + line2b2 = ( + line2b2.split(") # ")[ + 0 + ] + + ")" + ) + new_sb_lines.append(line2b2) + continue + new_sb_lines.append(line2b) + continue + elif line2.count("://") == 1 and ( + line2.count("')") == 1 + ): + line2a = line2.split("://")[0] + "://'" + line2b = ( + new_ws + + "'" + + (line2.split("://")[1]) + ) + new_sb_lines.append(line2a) + if get_width(line2b) + w > ( + console_width + ): + if line2b.count("/") > 0: + slash_one = line2b.find("/") + slash_one_p1 = slash_one + 1 + line2b1 = ( + line2b[: slash_one + 1] + + "'" + ) + line2b2 = ( + new_ws + + "'" + + (line2b[slash_one_p1:]) + ) + new_sb_lines.append(line2b1) + if line2b2.count(") # ") == 1: + line2b2 = ( + line2b2.split(") # ")[ + 0 + ] + + ")" + ) + new_sb_lines.append(line2b2) + continue + new_sb_lines.append(line2b) + continue + elif line2.count(", ") == 1: + line2a = line2.split(", ")[0] + "," + line2b = new_ws + line2.split(", ")[1] + new_sb_lines.append(line2a) + new_sb_lines.append(line2b) + continue + elif line2.count('="') == 1 and ( + line2.lstrip().startswith("'") + ): + line2a = line2.split('="')[0] + "='" + line2b = ( + new_ws + + "'\"" + + (line2.split('="')[1]) + ) + new_sb_lines.append(line2a) + new_sb_lines.append(line2b) + continue + elif line2.count("='") == 1 and ( + line2.lstrip().startswith('"') + ): + line2a = line2.split("='")[0] + '="' + line2b = ( + new_ws + + "\"'" + + (line2.split("='")[1]) + ) + new_sb_lines.append(line2a) + new_sb_lines.append(line2b) + continue + new_sb_lines.append(line2) + elif get_width(line2) + 4 + w <= console_width: + line2 = " " + line2 + new_sb_lines.append(line1) + new_sb_lines.append(line2) + else: + new_sb_lines.append(line) + continue + if line.count('("') == 1: + whitespace = line_length2 - len(line.lstrip()) + new_ws = line[0:whitespace] + " " + line1 = line.split('("')[0] + "(" + line2 = new_ws + '"' + line.split('("')[1] + if ("):") not in line2: + new_sb_lines.append(line1) + if get_width(line2) + w > console_width: + if line2.count('" in self.') == 1: + line2a = ( + line2.split('" in self.')[0] + + '" in' + ) + line2b = ( + new_ws + + "self." + + (line2.split('" in self.')[1]) + ) + new_sb_lines.append(line2a) + new_sb_lines.append(line2b) + continue + new_sb_lines.append(line2) + elif get_width(line2) + 4 + w <= console_width: + line2 = " " + line2 + new_sb_lines.append(line1) + new_sb_lines.append(line2) + else: + new_sb_lines.append(line) + continue + if line.count("('") == 1: + whitespace = line_length2 - len(line.lstrip()) + new_ws = line[0:whitespace] + " " + line1 = line.split("('")[0] + "(" + line2 = new_ws + "'" + line.split("('")[1] + if ("):") not in line2: + new_sb_lines.append(line1) + if get_width(line2) + w > console_width: + if line2.count("' in self.") == 1: + line2a = ( + line2.split("' in self.")[0] + + "' in" + ) + line2b = ( + new_ws + + "self." + + (line2.split("' in self.")[1]) + ) + new_sb_lines.append(line2a) + new_sb_lines.append(line2b) + continue + new_sb_lines.append(line2) + elif get_width(line2) + 4 + w <= console_width: + line2 = " " + line2 + new_sb_lines.append(line1) + new_sb_lines.append(line2) + else: + new_sb_lines.append(line) + continue + if line.count('= "') == 1 and line.count("://") == 1: + whitespace = line_length2 - len(line.lstrip()) + new_ws = line[0:whitespace] + " " + line1 = line.split("://")[0] + '://" \\' + line2 = new_ws + '"' + line.split("://")[1] + new_sb_lines.append(line1) + if get_width(line2) + w > console_width: + if line2.count("/") > 0: + slash_one = line2.find("/") + slash_one_p1 = slash_one + 1 + line2a = line2[: slash_one + 1] + '" \\' + line2b = ( + new_ws + '"' + line2[slash_one_p1:] + ) + new_sb_lines.append(line2a) + new_sb_lines.append(line2b) + continue + new_sb_lines.append(line2) + continue + if line.count("= '") == 1 and line.count("://") == 1: + whitespace = line_length2 - len(line.lstrip()) + new_ws = line[0:whitespace] + " " + line1 = line.split("://")[0] + "://' \\" + line2 = new_ws + "'" + line.split("://")[1] + new_sb_lines.append(line1) + if get_width(line2) + w > console_width: + if line2.count("/") > 0: + slash_one = line2.find("/") + slash_one_p1 = slash_one + 1 + line2a = line2[: slash_one + 1] + "' \\" + line2b = ( + new_ws + "'" + line2[slash_one_p1:] + ) + new_sb_lines.append(line2a) + new_sb_lines.append(line2b) + continue + new_sb_lines.append(line2) + continue + if line.count("(self.") == 1 and ("):") not in line: + whitespace = line_length2 - len(line.lstrip()) + new_ws = line[0:whitespace] + " " + line1 = line.split("(self.")[0] + "(" + line2 = new_ws + "self." + line.split("(self.")[1] + if get_width(line1) + w <= console_width: + new_sb_lines.append(line1) + new_sb_lines.append(line2) + continue + if line.count(" == ") == 1 and not ( + line.endswith(":") or (": #") in line + ): + whitespace = line_length2 - len(line.lstrip()) + new_ws = line[0:whitespace] + " " + line1 = line.split(" == ")[0] + " == (" + line2 = new_ws + line.split(" == ")[1] + ")" + if get_width(line1) + w <= console_width and ( + get_width(line2) + w <= console_width + ): + new_sb_lines.append(line1) + new_sb_lines.append(line2) + continue + if line.count(" == ") == 1 and line.endswith(":"): + whitespace = line_length2 - len(line.lstrip()) + new_ws = line[0:whitespace] + " " + line1 = line.split(" == ")[0] + " == (" + line2 = new_ws + line.split(" == ")[1][:-1] + "):" + if get_width(line1) + w <= console_width and ( + get_width(line2) + w <= console_width + ): + new_sb_lines.append(line1) + new_sb_lines.append(line2) + continue + if ( + line.count(" == ") == 1 + and (line.count(": #") == 1) + and (line.find(" == ") < line.find(": #")) + ): + whitespace = line_length2 - len(line.lstrip()) + new_ws = line[0:whitespace] + " " + comments = " #" + line.split(": #")[1] + line0 = line.split(": #")[0] + ":" + line1 = line0.split(" == ")[0] + " == (" + line2 = new_ws + line0.split(" == ")[1][:-1] + "):" + if get_width(line1) + w <= console_width and ( + get_width(line2) + w <= console_width + ): + new_sb_lines.append(line1) + if ( + get_width(line2 + comments) + w + <= console_width + ): + new_sb_lines.append(line2 + comments) + else: + new_sb_lines.append(line2) + continue + if line.count(" % ") == 1 and ("):") not in line: + whitespace = line_length2 - len(line.lstrip()) + new_ws = line[0:whitespace] + " " + line1 = line.split(" % ")[0] + " \\" + line2 = new_ws + "% " + line.split(" % ")[1] + if get_width(line1) + w <= console_width: + new_sb_lines.append(line1) + new_sb_lines.append(line2) + continue + if line.count(" = ") == 1 and (" # ") not in line: + whitespace = line_length2 - len(line.lstrip()) + new_ws = line[0:whitespace] + " " + line1 = line.split(" = ")[0] + " = (" + line2 = new_ws + line.split(" = ")[1] + ")" + if get_width(line1) + w <= console_width and ( + get_width(line2) + w <= console_width + ): + new_sb_lines.append(line1) + new_sb_lines.append(line2) + continue + elif get_width(line1) + w <= console_width: + if line2.count(" % ") == 1 and not ( + line2.endswith(":") + ): + whitespace = line_length2 - len( + line2.lstrip() + ) + line2a = line2.split(" % ")[0] + " \\" + line2b = ( + new_ws + "% " + line2.split(" % ")[1] + ) + if get_width(line2a) + w <= console_width: + if ( + get_width(line2b) + w + <= console_width + ): + new_sb_lines.append(line1) + new_sb_lines.append(line2a) + new_sb_lines.append(line2b) + continue + if ( + line.count(" = ") == 1 + and (line.count(" # ") == 1) + and (line.find(" = ") < line.find(" # ")) + ): + whitespace = line_length2 - len(line.lstrip()) + new_ws = line[0:whitespace] + " " + comments = " # " + line.split(" # ")[1] + line0 = line.split(" # ")[0] + line1 = line0.split(" = ")[0] + " = (" + line2 = new_ws + line0.split(" = ")[1] + ")" + if get_width(line1) + w <= console_width and ( + get_width(line2) + w <= console_width + ): + new_sb_lines.append(line1) + if ( + get_width(line2 + comments) + w + <= console_width + ): + new_sb_lines.append(line2 + comments) + else: + new_sb_lines.append(line2) + continue + new_sb_lines.append(line) + + if new_sb_lines: + code_lines = new_sb_lines + the_code = "\n".join(code_lines) + + if code_lang != "python": + for line in code_lines: + line_length = get_width(line) + if line_length > code_width: + code_width = line_length + + extra_r_spaces = 2 + if console_width and (code_width + extra_r_spaces < console_width): + used_width = code_width + extra_r_spaces + + the_code = rich_helper.fix_emoji_spacing(the_code) + the_theme = "monokai" + if file_to_print.split(os.sep)[-1].startswith("."): + the_theme = "tango" + + magic_syntax = rich_helper.process_syntax( + the_code, + code_lang, + theme=the_theme, + line_numbers=line_numbers, + code_width=used_width, + word_wrap=word_wrap, + ) + # ---------------------------------------- + dash_length = 62 # May change + if used_width and used_width + w < console_width: + dash_length = used_width + w + elif console_width: + dash_length = console_width + dashes = "-" * dash_length + print(dashes) + print_success = False + if code_lang == "markdown" and not line_numbers: + all_code = rich_helper.fix_emoji_spacing(all_code) + if "*" not in all_code and "*" not in all_code: + if "*" not in all_code and "*" not in all_code: + all_code = all_code.replace("", "**") + all_code = all_code.replace("", "**") + if "`" not in all_code and "`" not in all_code: + if "`" not in all_code and "`" not in all_code: + all_code = all_code.replace('', "``") + all_code = all_code.replace("", "``") + all_code = all_code.replace("", "``") + # Display ALL tags as an

      because the font size is fixed + all_code = all_code.replace("\n

      ", "\n# ").replace("

      ", "") + all_code = all_code.replace("\n

      ", "\n# ").replace("

      ", "") + all_code = all_code.replace("\n

      ", "\n# ").replace("

      ", "") + all_code = all_code.replace("\n

      ", "\n# ").replace("

      ", "") + all_code = rich_helper.get_code_without_tag(all_code, "summary") + all_code = rich_helper.get_code_without_tag(all_code, "details") + all_code = rich_helper.get_code_without_tag(all_code, "span") + all_code = rich_helper.get_code_without_tag(all_code, "div") + all_code = rich_helper.get_code_without_tag(all_code, "img") + all_code = rich_helper.get_code_without_tag(all_code, "li") + all_code = rich_helper.get_code_without_tag(all_code, "ul") + all_code = rich_helper.get_code_without_tag(all_code, "a") + all_code = rich_helper.get_code_without_tag(all_code, "p") + all_code = all_code.replace("
      ", "\n") + print_success = rich_helper.display_markdown(all_code) + if all_code.endswith("\n"): + print() # Because "rich" skips the last line if new-line + elif magic_syntax: + print_success = rich_helper.display_code(magic_syntax) + if not magic_syntax or not print_success: + for line in code_lines: + print(line) + print(dashes) + # ---------------------------------------- + + +if __name__ == "__main__": + invalid_run_command() diff --git a/seleniumbase/console_scripts/sb_recorder.py b/seleniumbase/console_scripts/sb_recorder.py new file mode 100644 index 00000000000..43f9680119e --- /dev/null +++ b/seleniumbase/console_scripts/sb_recorder.py @@ -0,0 +1,542 @@ +""" +** recorder ** + +Launches the SeleniumBase Recorder Desktop App. + +Usage: + seleniumbase recorder [OPTIONS] + sbase recorder [OPTIONS] + +Options: + --uc / --undetected (Use undetectable mode.) + --cdp (Same as "--uc" and "--undetectable".) + --behave (Also output Behave/Gherkin files.) + +Output: + Launches the SeleniumBase Recorder Desktop App. +""" +import colorama +import os +import subprocess +import sys +import tkinter as tk +from tkinter import ttk +from contextlib import suppress +from seleniumbase import config as sb_config +from seleniumbase.core import detect_b_ver +from seleniumbase.fixtures import page_utils +from seleniumbase.fixtures import shared_utils +from tkinter import messagebox + +sb_config.rec_subprocess_p = None +sb_config.rec_subprocess_used = False +sys_executable = sys.executable +if " " in sys_executable: + sys_executable = "python" + + +def set_colors(use_colors): + c0 = "" + c1 = "" + c2 = "" + c3 = "" + c4 = "" + cr = "" + if use_colors: + c0 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX + c1 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c2 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + c3 = colorama.Fore.LIGHTRED_EX + colorama.Back.LIGHTYELLOW_EX + c4 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + return c0, c1, c2, c3, c4, cr + + +def send_window_to_front(window): + window.lift() + window.attributes("-topmost", True) + window.after_idle(window.attributes, "-topmost", False) + + +def show_already_recording_warning(): + messagebox.showwarning( + "SeleniumBase Recorder: Already Running!", + "Please finalize the active recording from the terminal\n" + 'where you opened the Recorder: Type "c" and hit Enter.', + ) + + +def file_name_error(file_name): + error_msg = None + if not file_name.endswith(".py"): + error_msg = 'File name must end with ".py"!' + elif "*" in file_name or len(str(file_name)) < 4: + error_msg = "Invalid file name!" + elif file_name.startswith("-"): + error_msg = 'File name cannot start with "-"!' + elif "/" in str(file_name) or "\\" in str(file_name): + error_msg = "File must be created in the current directory!" + return error_msg + + +def do_recording( + file_name, + url, + overwrite_enabled, + brx, + ucb, + output_format, + window, +): + poll = None + if sb_config.rec_subprocess_used: + poll = sb_config.rec_subprocess_p.poll() + if not sb_config.rec_subprocess_used or poll is not None: + pass + else: + show_already_recording_warning() + send_window_to_front(window) + poll = sb_config.rec_subprocess_p.poll() + if poll is None: + return + + file_name = file_name.strip() + error_msg = file_name_error(file_name) + if error_msg: + messagebox.showwarning( + "Invalid filename", "Invalid filename: %s" % error_msg + ) + return + + url = url.strip() + if not page_utils.is_valid_url(url): + if page_utils.is_valid_url("https://" + url): + url = "https://" + url + if "edge" in brx.lower() and ucb: + messagebox.showwarning( + "Invalid selection", + "MS Edge cannot be combined with UC Mode " + "because it uses msedgedriver, not chromedriver!", + ) + elif not page_utils.is_valid_url(url): + messagebox.showwarning( + "Invalid URL", "Enter a valid URL! (Eg. seleniumbase.io)" + ) + else: + if os.path.exists(os.getcwd() + "/" + file_name): + if not overwrite_enabled: + msgbox = tk.messagebox.askquestion( + "Overwrite?", + 'Are you sure you want to overwrite "%s"?' % file_name, + icon="warning", + ) + if msgbox == "yes": + os.remove(file_name) + else: + tk.messagebox.showinfo("Cancelled", "Recording Cancelled!") + return + else: + os.remove(file_name) + add_on = "" + if "Behave" in output_format: + add_on = " --rec-behave" + elif "SB()" in output_format: + add_on = " --rec-sb-mgr" + elif "sb_cdp" in output_format: + add_on = " --rec-sb-cdp" + command = ( + "%s -m seleniumbase mkrec %s --url=%s --gui" + % (sys_executable, file_name, url) + ) + if '"' not in url: + command = ( + '%s -m seleniumbase mkrec %s --url="%s" --gui' + % (sys_executable, file_name, url) + ) + elif "'" not in url: + command = ( + "%s -m seleniumbase mkrec %s --url='%s' --gui" + % (sys_executable, file_name, url) + ) + if "edge" in brx.lower(): + command += " --edge" + elif "opera" in brx.lower(): + command += " --opera" + elif "brave" in brx.lower(): + command += " --brave" + elif "comet" in brx.lower(): + command += " --comet" + elif "chromium" in brx.lower(): + command += " --use-chromium" + if ucb: + command += " --uc" + command_args = sys.argv[2:] + if "--ee" in command_args: + command += " --ee" + command += add_on + poll = None + if sb_config.rec_subprocess_used: + poll = sb_config.rec_subprocess_p.poll() + if not sb_config.rec_subprocess_used or poll is not None: + sb_config.rec_subprocess_p = subprocess.Popen(command, shell=True) + sb_config.rec_subprocess_used = True + else: + show_already_recording_warning() + send_window_to_front(window) + + +def do_playback(file_name, brx, window, demo_mode=False): + file_name = file_name.strip() + error_msg = file_name_error(file_name) + if error_msg: + messagebox.showwarning( + "Invalid filename", "Invalid filename: %s" % error_msg + ) + return + if not os.path.exists(os.getcwd() + "/" + file_name): + messagebox.showwarning( + "File does not exist", + 'File "%s" does not exist in the current directory!' % file_name, + ) + return + # command = "%s -m pytest %s -q -s" % (sys_executable, file_name) + command = "%s %s -q -s" % (sys_executable, file_name) + if shared_utils.is_linux(): + command += " --gui" + if "edge" in brx.lower(): + command += " --edge" + elif "opera" in brx.lower(): + command += " --opera" + elif "brave" in brx.lower(): + command += " --brave" + elif "comet" in brx.lower(): + command += " --comet" + elif "chromium" in brx.lower(): + command += " --use-chromium" + if demo_mode: + command += " --demo" + command_args = sys.argv[2:] + if ( + "--uc" in command_args + or "--cdp" in command_args + or "--undetected" in command_args + or "--undetectable" in command_args + ): + command += " --uc" + poll = None + if sb_config.rec_subprocess_used: + poll = sb_config.rec_subprocess_p.poll() + if not sb_config.rec_subprocess_used or poll is not None: + print(command) + subprocess.Popen(command, shell=True) + else: + messagebox.showwarning( + "SeleniumBase Recorder: Already Running!", + "Please finalize the active recording from the terminal\n" + 'where you opened the Recorder: Type "c" and hit Enter.', + ) + send_window_to_front(window) + + +def create_tkinter_gui(): + default_file_name = "new_recording.py" + window = tk.Tk() + window.title("SeleniumBase Recorder App") + window.geometry("344x560") + my_font = ("TkDefaultFont", 11) + frame = tk.Frame(window) + frame.pack() + + fname = tk.StringVar(value=default_file_name) + label = tk.Label(window, text="Enter filename to save recording as:") + label.pack(pady=(20, 0.2)) + entry = tk.Entry(window, textvariable=fname) + entry.config(width=22) + entry.pack(pady=(0.6, 0.2)) + cbx = tk.IntVar() + chk = tk.Checkbutton(window, text="Overwrite existing files", variable=cbx) + chk.pack() + chk.select() + use_stealth = False + use_behave = False + use_sb_mgr = False + use_sb_cdp = False + command_args = sys.argv[2:] + if ( + "--uc" in command_args + or "--cdp" in command_args + or "--stealth" in command_args + or "--undetected" in command_args + or "--undetectable" in command_args + ): + use_stealth = True + if ( + "--rec-behave" in command_args + or "--behave" in command_args + or "--gherkin" in command_args + ): + use_behave = True + if "--rec-sb-mgr" in command_args: + use_sb_mgr = True + if "--rec-sb-cdp" in command_args: + use_sb_cdp = True + + tk.Label(window, text="\nSelect a web browser to use:").pack() + br_count = 2 + br_order = {"chrome": 0, "chromium": 1} + options_list = ["Google Chrome"] + options_list.append("Chromium Browser") + if shared_utils.is_windows(): + options_list.append("MS Edge (No Stealth)") + br_order["edge"] = br_count + br_count += 1 + else: + with suppress(Exception): + if os.path.exists(detect_b_ver.get_binary_location("edge")): + options_list.append("MS Edge (No Stealth)") + br_order["edge"] = br_count + br_count += 1 + with suppress(Exception): + if os.path.exists(detect_b_ver.get_binary_location("opera")): + options_list.append("Opera Browser") + br_order["opera"] = br_count + br_count += 1 + with suppress(Exception): + if os.path.exists(detect_b_ver.get_binary_location("brave")): + options_list.append("Brave Browser") + br_order["brave"] = br_count + br_count += 1 + with suppress(Exception): + if os.path.exists(detect_b_ver.get_binary_location("comet")): + options_list.append("Comet Browser") + br_order["comet"] = br_count + br_count += 1 + brx = tk.StringVar(window) + if "--use-chromium" in command_args or "--chromium" in command_args: + brx.set(options_list[1]) + elif ( + "--edge" in command_args + and "edge" in br_order + ): + brx.set(options_list[2]) + use_stealth = False + elif "--opera" in command_args and "opera" in br_order: + brx.set(options_list[br_order["opera"]]) + elif "--brave" in command_args and "brave" in br_order: + brx.set(options_list[br_order["brave"]]) + elif "--comet" in command_args and "comet" in br_order: + brx.set(options_list[br_order["comet"]]) + else: + brx.set(options_list[0]) + question_menu = tk.OptionMenu(window, brx, *options_list) + question_menu.config(width=16) + question_menu.pack(pady=(0.6, 0.2)) + + ucb = tk.IntVar() + chkb = tk.Checkbutton( + window, text="Stealth Mode / UC + CDP Mode", variable=ucb + ) + chkb.pack(pady=(0.4, 0.4)) + if use_stealth: + chkb.select() + # chkb.config(state=tk.DISABLED) + + tk.Label(window, text="\nSelect an output format to use:").pack() + options_list = ["pytest format / BaseCase"] + options_list.append("Context Manager / SB()") + options_list.append("Pure CDP Mode / sb_cdp") + if use_behave: + options_list.append("BehaveBDD Gherkin File") + frx = tk.StringVar(window) + if use_behave and not use_sb_mgr and not use_sb_cdp: + frx.set(options_list[3]) + elif use_sb_mgr: + frx.set(options_list[1]) + elif use_sb_cdp: + frx.set(options_list[2]) + else: + frx.set(options_list[0]) + question_menu = tk.OptionMenu(window, frx, *options_list) + question_menu.config(width=21) + question_menu.pack(pady=(0.6, 0.2)) + + url = tk.StringVar() + label = tk.Label(window, text="Enter a URL to start recording on:") + label.pack(pady=(20, 0.2)) + entry = tk.Entry(window, textvariable=url) + entry.config(width=24) + entry.pack() + entry.focus() + entry.bind( + "", + ( + lambda _: do_recording( + fname.get(), + url.get(), + cbx.get(), + brx.get(), + ucb.get(), + frx.get(), + window, + ) + ), + ) + # Automatically set focus on URL field when clicking back into the app + window.bind( + "", lambda event: entry.focus_set() + if event.widget == window + else None + ) + style = ttk.Style() + style.configure( + "Record.TButton", + foreground="red", + font=("TkDefaultFont", 12, "bold"), + width="8", + padding=(4, 3, 4, 1) + ) + ttk.Button( + window, + text="Record", + style="Record.TButton", + command=lambda: do_recording( + fname.get(), + url.get(), + cbx.get(), + brx.get(), + ucb.get(), + frx.get(), + window, + ), + ).pack(pady=0.2) + label = tk.Label(window, text="Playback recording (Normal Mode):") + label.pack(pady=(17, 0)) + + style.configure( + "Playback.TButton", + foreground="green", + font=("TkDefaultFont", 11, "bold"), + width="8", + padding=(4, 3, 4, 1) + ) + ttk.Button( + window, + text="Playback", + style="Playback.TButton", + command=lambda: do_playback(fname.get(), brx.get(), window), + ).pack(pady=0.2) + label = tk.Label(window, text="Playback recording (Demo Mode):") + label.pack(pady=(13, 0)) + try: + style.configure( + "PlaybackDemo.TButton", + foreground="teal", + font=("TkDefaultFont", 11, "bold"), + width="18", + padding=(4, 3, 4, 1) + ) + ttk.Button( + window, + text="Playback (Demo Mode)", + style="PlaybackDemo.TButton", + command=lambda: do_playback( + fname.get(), brx.get(), window, demo_mode=True + ), + ).pack(pady=0.2) + except Exception: + style.configure( + "PlaybackDemo.TButton", + foreground="blue", + font=("TkDefaultFont", 11, "bold"), + padding=(4, 3, 4, 1) + ) + ttk.Button( + window, + text="Playback (Demo Mode)", + style="PlaybackDemo.TButton", + command=lambda: do_playback( + fname.get(), brx.get(), window, demo_mode=True + ), + ).pack(pady=0.2) + + # Bring form window to front + send_window_to_front(window) + # Use decoy to set correct focus on main window + decoy = tk.Tk() + decoy.geometry("1x1") + decoy.iconify() + decoy.update() + decoy.deiconify() + decoy.destroy() + # Start tkinter + for widget in window.winfo_children(): + if isinstance( + widget, (tk.Label, tk.Entry, tk.Checkbutton, tk.OptionMenu) + ): + widget.configure(font=my_font) + window.deiconify() # Force the OS to redraw it actively on top + window.focus_force() # Force keyboard focus into the entry fields + window.mainloop() + end_program() + + +def recorder_still_running(): + poll = None + if sb_config.rec_subprocess_used: + try: + poll = sb_config.rec_subprocess_p.poll() + except Exception: + return False + else: + return False + if poll is not None: + return False + return True + + +def show_still_running_warning(): + """Give the user a chance to end the recording safely via the + pytest pdb Debug Mode so that processes such as chromedriver + and Python don't remain open and hanging in the background.""" + messagebox.showwarning( + "SeleniumBase Recorder: Still Running!", + "Please finalize the active recording from the terminal\n" + 'where you opened the Recorder: Type "c" and hit Enter.\n' + "(Then you can safely close this alert.)", + ) + + +def end_program(): + if recorder_still_running(): + show_still_running_warning() + + +def main(): + use_colors = True + if shared_utils.is_linux(): + use_colors = False + c0, c1, c2, c3, c4, cr = set_colors(use_colors) + message = "" + message += c2 + message += "*" + message += c4 + message += " Starting the " + message += c0 + message += "Selenium" + message += c1 + message += "Base" + message += c2 + message += " " + message += c3 + message += "Recorder" + message += c4 + message += " Desktop App" + message += c2 + message += "..." + message += cr + print(message) + create_tkinter_gui() + + +if __name__ == "__main__": + print('To open the Recorder Desktop App: "seleniumbase recorder"') diff --git a/seleniumbase/core/__init__.py b/seleniumbase/core/__init__.py old mode 100755 new mode 100644 diff --git a/seleniumbase/core/application_manager.py b/seleniumbase/core/application_manager.py old mode 100755 new mode 100644 index c6b9112b6a4..8e89a8e2f36 --- a/seleniumbase/core/application_manager.py +++ b/seleniumbase/core/application_manager.py @@ -1,24 +1,18 @@ -""" -Method for generating application strings used in the Testcase Database. -""" - import time class ApplicationManager: - """ - This class contains methods to generate application strings. - """ + """Generating application strings for the Testcase Database.""" @classmethod def generate_application_string(cls, test): - """ Generate an application string based on some of the given information - that can be pulled from the test object: app_env, start_time. """ + """Generate a string based on some of the given information + that's pulled from the test object: app_env, start_time.""" - app_env = 'test' - if hasattr(test, 'env'): + app_env = "test" + if hasattr(test, "env"): app_env = test.env - elif hasattr(test, 'environment'): + elif hasattr(test, "environment"): app_env = test.environment start_time = int(time.time() * 1000) diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py old mode 100755 new mode 100644 index 41c77e3ef6d..fe4d359a22a --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -1,520 +1,6193 @@ +import fasteners +import json +import logging import os -import random +import platform import re +import shutil +import subprocess import sys -import threading import time +import types +import urllib3 import warnings +from contextlib import suppress +from filelock import FileLock from selenium import webdriver -from selenium.common.exceptions import WebDriverException -from selenium.webdriver.common.desired_capabilities import DesiredCapabilities -from seleniumbase.config import proxy_list +from selenium.common.exceptions import ElementClickInterceptedException +from selenium.common.exceptions import InvalidSessionIdException +from selenium.common.exceptions import SessionNotCreatedException +from selenium.webdriver.chrome.service import Service as ChromeService +from selenium.webdriver.common import utils as common_utils +from seleniumbase import config as sb_config +from seleniumbase import decorators +from seleniumbase import drivers # webdriver storage folder for SeleniumBase +from seleniumbase.drivers import cft_drivers # chrome-for-testing +from seleniumbase.drivers import chs_drivers # chrome-headless-shell +from seleniumbase.drivers import opera_drivers # still uses chromedriver +from seleniumbase.drivers import brave_drivers # still uses chromedriver +from seleniumbase.drivers import comet_drivers # still uses chromedriver +from seleniumbase.drivers import chromium_drivers # still uses chromedriver +from seleniumbase import extensions # browser extensions storage folder from seleniumbase.config import settings +from seleniumbase.core import detect_b_ver from seleniumbase.core import download_helper from seleniumbase.core import proxy_helper -from seleniumbase.core import capabilities_parser +from seleniumbase.core import sb_driver +from seleniumbase.core import sb_cdp from seleniumbase.fixtures import constants -from seleniumbase.fixtures import page_utils -from seleniumbase import drivers # webdriver storage folder for SeleniumBase -from seleniumbase import extensions # browser extensions storage folder +from seleniumbase.fixtures import js_utils +from seleniumbase.fixtures import page_actions +from seleniumbase.fixtures import shared_utils + +urllib3.disable_warnings() DRIVER_DIR = os.path.dirname(os.path.realpath(drivers.__file__)) +DRIVER_DIR_CFT = os.path.dirname(os.path.realpath(cft_drivers.__file__)) +DRIVER_DIR_CHS = os.path.dirname(os.path.realpath(chs_drivers.__file__)) +DRIVER_DIR_OPERA = os.path.dirname(os.path.realpath(opera_drivers.__file__)) +DRIVER_DIR_BRAVE = os.path.dirname(os.path.realpath(brave_drivers.__file__)) +DRIVER_DIR_COMET = os.path.dirname(os.path.realpath(comet_drivers.__file__)) +DRIVER_DIR_CHROMIUM = os.path.dirname( + os.path.realpath(chromium_drivers.__file__) +) +# Make sure that the SeleniumBase DRIVER_DIR is at the top of the System PATH +# (Changes to the System PATH with os.environ only last during the test run) +if not os.environ["PATH"].startswith(DRIVER_DIR): + # Remove existing SeleniumBase DRIVER_DIR from System PATH if present + os.environ["PATH"] = os.environ["PATH"].replace(DRIVER_DIR, "") + # If two path separators are next to each other, replace with just one + os.environ["PATH"] = os.environ["PATH"].replace( + os.pathsep + os.pathsep, os.pathsep + ) + # Put the SeleniumBase DRIVER_DIR at the beginning of the System PATH + os.environ["PATH"] = DRIVER_DIR + os.pathsep + os.environ["PATH"] EXTENSIONS_DIR = os.path.dirname(os.path.realpath(extensions.__file__)) -DISABLE_CSP_ZIP_PATH = "%s/%s" % (EXTENSIONS_DIR, "disable_csp.zip") +DISABLE_CSP_ZIP_PATH = os.path.join(EXTENSIONS_DIR, "disable_csp.zip") +AD_BLOCK_ZIP_PATH = os.path.join(EXTENSIONS_DIR, "ad_block.zip") +RECORDER_ZIP_PATH = os.path.join(EXTENSIONS_DIR, "recorder.zip") +SBASE_EXT_ZIP_PATH = os.path.join(EXTENSIONS_DIR, "sbase_ext.zip") +DOWNLOADS_FOLDER = download_helper.get_downloads_folder() PROXY_ZIP_PATH = proxy_helper.PROXY_ZIP_PATH -PROXY_ZIP_PATH_2 = proxy_helper.PROXY_ZIP_PATH_2 -PLATFORM = sys.platform -IS_WINDOWS = False +PROXY_ZIP_LOCK = proxy_helper.PROXY_ZIP_LOCK +PROXY_DIR_PATH = proxy_helper.PROXY_DIR_PATH +PROXY_DIR_LOCK = proxy_helper.PROXY_DIR_LOCK LOCAL_CHROMEDRIVER = None LOCAL_GECKODRIVER = None LOCAL_EDGEDRIVER = None LOCAL_IEDRIVER = None -LOCAL_OPERADRIVER = None -if "darwin" in PLATFORM or "linux" in PLATFORM: - LOCAL_CHROMEDRIVER = DRIVER_DIR + '/chromedriver' - LOCAL_GECKODRIVER = DRIVER_DIR + '/geckodriver' - LOCAL_OPERADRIVER = DRIVER_DIR + '/operadriver' -elif "win32" in PLATFORM or "win64" in PLATFORM or "x64" in PLATFORM: - IS_WINDOWS = True - LOCAL_EDGEDRIVER = DRIVER_DIR + '/MicrosoftWebDriver.exe' - LOCAL_IEDRIVER = DRIVER_DIR + '/IEDriverServer.exe' - LOCAL_CHROMEDRIVER = DRIVER_DIR + '/chromedriver.exe' - LOCAL_GECKODRIVER = DRIVER_DIR + '/geckodriver.exe' - LOCAL_OPERADRIVER = DRIVER_DIR + '/operadriver.exe' +LOCAL_HEADLESS_IEDRIVER = None +LOCAL_UC_DRIVER = None +ARCH = platform.architecture()[0] +IS_ARM_MAC = shared_utils.is_arm_mac() +IS_MAC = shared_utils.is_mac() +IS_LINUX = shared_utils.is_linux() +IS_WINDOWS = shared_utils.is_windows() +if IS_MAC or IS_LINUX: + LOCAL_CHROMEDRIVER = DRIVER_DIR + "/chromedriver" + LOCAL_GECKODRIVER = DRIVER_DIR + "/geckodriver" + LOCAL_EDGEDRIVER = DRIVER_DIR + "/msedgedriver" + LOCAL_UC_DRIVER = DRIVER_DIR + "/uc_driver" +elif IS_WINDOWS: + LOCAL_EDGEDRIVER = DRIVER_DIR + "/msedgedriver.exe" + LOCAL_IEDRIVER = DRIVER_DIR + "/IEDriverServer.exe" + LOCAL_HEADLESS_IEDRIVER = DRIVER_DIR + "/headless_ie_selenium.exe" + LOCAL_CHROMEDRIVER = DRIVER_DIR + "/chromedriver.exe" + LOCAL_GECKODRIVER = DRIVER_DIR + "/geckodriver.exe" + LOCAL_UC_DRIVER = DRIVER_DIR + "/uc_driver.exe" else: # Cannot determine system pass # SeleniumBase will use web drivers from the System PATH by default -def make_executable(file_path): - # Set permissions to: "If you can read it, you can execute it." - mode = os.stat(file_path).st_mode - mode |= (mode & 0o444) >> 2 # copy R bits to X - os.chmod(file_path, mode) +def log_d(message): + """If setting sb_config.settings.HIDE_DRIVER_DOWNLOADS to True, + output from driver downloads are logged instead of printed.""" + if getattr(settings, "HIDE_DRIVER_DOWNLOADS", None): + logging.debug(message) + else: + print(message) + + +def override_driver_dir(driver_dir): + if ( + driver_dir + and isinstance(driver_dir, str) + and os.path.exists(driver_dir) + ): + driver_dir = os.path.realpath(driver_dir) + sb_config.settings.NEW_DRIVER_DIR = driver_dir + if ( + not os.environ["PATH"].startswith(driver_dir) + and len(driver_dir) >= 4 + ): + os.environ["PATH"] = os.environ["PATH"].replace(driver_dir, "") + os.environ["PATH"] = os.environ["PATH"].replace( + os.pathsep + os.pathsep, os.pathsep + ) + os.environ["PATH"] = driver_dir + os.pathsep + os.environ["PATH"] + elif ( + not driver_dir + or not isinstance(driver_dir, str) + or not os.path.exists(driver_dir) + ): + bad_dir = "" + if driver_dir and isinstance(driver_dir, str): + bad_dir = os.path.realpath(driver_dir) + log_d( + "\n* Warning: Cannot set driver_dir to nonexistent directory:\n%s" + "\n* Will use the default folder instead:\n%s" + % (bad_dir, DRIVER_DIR) + ) def make_driver_executable_if_not(driver_path): # Verify driver has executable permissions. If not, add them. permissions = oct(os.stat(driver_path)[0])[-3:] - if '4' in permissions or '6' in permissions: + if "4" in permissions or "6" in permissions: # We want at least a '5' or '7' to make sure it's executable - make_executable(driver_path) + shared_utils.make_executable(driver_path) + + +def extend_driver( + driver, proxy_auth=False, use_uc=True, recorder_ext=False +): + # Extend the driver with new methods + driver.default_find_element = driver.find_element + driver.default_find_elements = driver.find_elements + driver.default_add_cookie = driver.add_cookie + driver.default_get_cookie = driver.get_cookie + driver.default_delete_cookie = driver.delete_cookie + driver.default_back = driver.back + driver.default_forward = driver.forward + driver.default_refresh = driver.refresh + DM = sb_driver.DriverMethods(driver) + driver.find_element = DM.find_element + driver.find_elements = DM.find_elements + driver.add_cookie = DM.add_cookie + driver.get_cookie = DM.get_cookie + driver.delete_cookie = DM.delete_cookie + driver.back = DM.back + driver.forward = DM.forward + driver.refresh = DM.refresh + driver.locator = DM.locator + driver.select = DM.select + driver.select_all = DM.select_all + page = types.SimpleNamespace() + page.open = DM.open_url + page.goto = DM.open_url + page.click = DM.click + page.click_link = DM.click_link + page.click_if_visible = DM.click_if_visible + page.click_active_element = DM.click_active_element + page.send_keys = DM.send_keys + page.press_keys = DM.press_keys + page.type = DM.update_text + page.submit = DM.submit + page.assert_element = DM.assert_element_visible + page.assert_element_present = DM.assert_element_present + page.assert_element_not_visible = DM.assert_element_not_visible + page.assert_text = DM.assert_text + page.assert_exact_text = DM.assert_exact_text + page.assert_non_empty_text = DM.assert_non_empty_text + page.assert_text_not_visible = DM.assert_text_not_visible + page.wait_for_element = DM.wait_for_element + page.wait_for_text = DM.wait_for_text + page.wait_for_exact_text = DM.wait_for_exact_text + page.wait_for_non_empty_text = DM.wait_for_non_empty_text + page.wait_for_text_not_visible = DM.wait_for_text_not_visible + page.wait_for_and_accept_alert = DM.wait_for_and_accept_alert + page.wait_for_and_dismiss_alert = DM.wait_for_and_dismiss_alert + page.is_element_present = DM.is_element_present + page.is_element_visible = DM.is_element_visible + page.is_text_visible = DM.is_text_visible + page.is_exact_text_visible = DM.is_exact_text_visible + page.is_attribute_present = DM.is_attribute_present + page.is_non_empty_text_visible = DM.is_non_empty_text_visible + page.get_text = DM.get_text + page.find_element = DM.find_element + page.find_elements = DM.find_elements + page.locator = DM.locator + page.select = DM.select + page.select_all = DM.select_all + page.get_current_url = DM.get_current_url + page.get_page_source = DM.get_page_source + page.get_title = DM.get_title + page.get_page_title = DM.get_title + page.switch_to_default_window = DM.switch_to_default_window + page.switch_to_newest_window = DM.switch_to_newest_window + page.open_new_window = DM.open_new_window + page.open_new_tab = DM.open_new_tab + page.switch_to_window = DM.switch_to_window + page.switch_to_tab = DM.switch_to_tab + page.switch_to_frame = DM.switch_to_frame + driver.page = page + js = types.SimpleNamespace() + js.js_click = DM.js_click + js.get_active_element_css = DM.get_active_element_css + js.get_locale_code = DM.get_locale_code + js.get_origin = DM.get_origin + js.get_user_agent = DM.get_user_agent + js.highlight = DM.highlight + driver.js = js + driver.open = DM.open_url + driver.goto = DM.open_url + driver.click = DM.click + driver.click_link = DM.click_link + driver.click_if_visible = DM.click_if_visible + driver.click_active_element = DM.click_active_element + driver.send_keys = DM.send_keys + driver.press_keys = DM.press_keys + driver.type = DM.update_text + driver.submit = DM.submit + driver.assert_element = DM.assert_element_visible + driver.assert_element_present = DM.assert_element_present + driver.assert_element_not_visible = DM.assert_element_not_visible + driver.assert_text = DM.assert_text + driver.assert_exact_text = DM.assert_exact_text + driver.assert_non_empty_text = DM.assert_non_empty_text + driver.assert_text_not_visible = DM.assert_text_not_visible + driver.wait_for_element = DM.wait_for_element + driver.wait_for_element_visible = DM.wait_for_element_visible + driver.wait_for_element_present = DM.wait_for_element_present + driver.wait_for_element_absent = DM.wait_for_element_absent + driver.wait_for_element_not_visible = DM.wait_for_element_not_visible + driver.wait_for_selector = DM.wait_for_selector + driver.wait_for_text = DM.wait_for_text + driver.wait_for_exact_text = DM.wait_for_exact_text + driver.wait_for_non_empty_text = DM.wait_for_non_empty_text + driver.wait_for_text_not_visible = DM.wait_for_text_not_visible + driver.wait_for_and_accept_alert = DM.wait_for_and_accept_alert + driver.wait_for_and_dismiss_alert = DM.wait_for_and_dismiss_alert + driver.is_element_present = DM.is_element_present + driver.is_element_visible = DM.is_element_visible + driver.is_text_visible = DM.is_text_visible + driver.is_exact_text_visible = DM.is_exact_text_visible + driver.is_attribute_present = DM.is_attribute_present + driver.is_non_empty_text_visible = DM.is_non_empty_text_visible + driver.is_valid_url = DM.is_valid_url + driver.is_alert_present = DM.is_alert_present + driver.is_online = DM.is_online + driver.is_connected = DM.is_connected + driver.is_uc_mode_active = DM.is_uc_mode_active + driver.is_cdp_mode_active = DM.is_cdp_mode_active + driver.js_click = DM.js_click + driver.get_text = DM.get_text + driver.get_active_element_css = DM.get_active_element_css + driver.get_locale_code = DM.get_locale_code + driver.get_screen_rect = DM.get_screen_rect + driver.get_origin = DM.get_origin + driver.get_user_agent = DM.get_user_agent + driver.get_cookie_string = DM.get_cookie_string + driver.highlight = DM.highlight + driver.highlight_click = DM.highlight_click + driver.highlight_if_visible = DM.highlight_if_visible + driver.sleep = time.sleep + driver.get_attribute = DM.get_attribute + driver.get_parent = DM.get_parent + driver.get_current_url = DM.get_current_url + driver.get_page_source = DM.get_page_source + driver.get_title = DM.get_title + driver.get_page_title = DM.get_title + driver.switch_to_default_window = DM.switch_to_default_window + driver.switch_to_newest_window = DM.switch_to_newest_window + driver.open_new_window = DM.open_new_window + driver.switch_to_newest_tab = DM.switch_to_newest_tab + driver.open_new_tab = DM.open_new_tab + driver.switch_to_window = DM.switch_to_window + driver.switch_to_tab = DM.switch_to_tab + driver.switch_to_frame = DM.switch_to_frame + driver.reset_window_size = DM.reset_window_size + driver.stop = driver.quit + if recorder_ext: + from seleniumbase.js_code.recorder_js import recorder_js + recorder_code = ( + """window.onload = function() { %s };""" % recorder_js + ) + driver.execute_cdp_cmd( + "Page.addScriptToEvaluateOnNewDocument", {"source": recorder_code} + ) + completed_loads = [] + for ext_dir in sb_config._ext_dirs: + if ext_dir not in completed_loads: + completed_loads.append(ext_dir) + if not use_uc and os.path.exists(os.path.realpath(ext_dir)): + with suppress(Exception): + driver.webextension.install(os.path.realpath(ext_dir)) + driver._is_using_auth = False + if proxy_auth: + driver._is_using_auth = True + if not use_uc and os.path.exists(proxy_helper.PROXY_DIR_PATH): + with suppress(Exception): + driver.webextension.install(proxy_helper.PROXY_DIR_PATH) + # Proxy needs a moment to load in Manifest V3 + if use_uc: + time.sleep(0.14) + else: + time.sleep(0.28) + return driver + + +@decorators.rate_limited(4) +def requests_get(url, proxy_string=None): + import requests + + protocol = "http" + proxies = None + if proxy_string: + if proxy_string.endswith(":443"): + protocol = "https" + elif "socks4" in proxy_string: + protocol = "socks4" + elif "socks5h" in proxy_string: + protocol = "socks5h" + elif "socks5" in proxy_string: + protocol = "socks5" + proxies = {protocol: proxy_string} + response = None + try: + response = requests.get(url, proxies=proxies, timeout=1.25) + except Exception: + # Prevent SSLCertVerificationError / CERTIFICATE_VERIFY_FAILED + url = url.replace("https://", "http://") + time.sleep(0.04) + response = requests.get(url, proxies=proxies, timeout=2.75) + return response + + +def get_latest_chromedriver_version(): + from seleniumbase.console_scripts import sb_install + return sb_install.get_latest_stable_chromedriver_version() + + +def chromedriver_on_path(): + paths = os.environ["PATH"].split(os.pathsep) + for path in paths: + if ( + not IS_WINDOWS + and os.path.exists(os.path.join(path, "chromedriver")) + ): + return os.path.join(path, "chromedriver") + elif ( + IS_WINDOWS + and os.path.exists(os.path.join(path, "chromedriver.exe")) + ): + return os.path.join(path, "chromedriver.exe") + return None + + +def get_uc_driver_version(full=False, local_uc_driver=None): + if not local_uc_driver: + local_uc_driver = LOCAL_UC_DRIVER + uc_driver_version = None + if os.path.exists(local_uc_driver): + with suppress(Exception): + output = subprocess.check_output( + '"%s" --version' % local_uc_driver, shell=True + ) + if IS_WINDOWS: + output = output.decode("latin1") + else: + output = output.decode("utf-8") + full_version = output.split(" ")[1] + output = output.split(" ")[1].split(".")[0] + if int(output) >= 72: + if full: + uc_driver_version = full_version + else: + uc_driver_version = output + return uc_driver_version + + +def find_chromedriver_version_to_use(use_version, driver_version): + # Note: https://chromedriver.chromium.org/downloads stops at 114. + # Future drivers are part of the Chrome-for-Testing collection. + if ( + driver_version + and str(driver_version).split(".")[0].isdigit() + and int(str(driver_version).split(".")[0]) >= 72 + ): + use_version = str(driver_version) + elif driver_version and not str(driver_version).split(".")[0].isdigit(): + from seleniumbase.console_scripts import sb_install + driver_version = driver_version.lower() + if driver_version == "stable" or driver_version == "latest": + use_version = sb_install.get_latest_stable_chromedriver_version() + elif driver_version == "beta": + use_version = sb_install.get_latest_beta_chromedriver_version() + elif driver_version == "dev": + use_version = sb_install.get_latest_dev_chromedriver_version() + elif driver_version == "canary": + use_version = sb_install.get_latest_canary_chromedriver_version() + elif driver_version == "previous" or driver_version == "latest-1": + use_version = sb_install.get_latest_stable_chromedriver_version() + use_version = str(int(use_version.split(".")[0]) - 1) + elif driver_version == "mlatest": + if use_version.split(".")[0].isdigit(): + major = use_version.split(".")[0] + if int(major) >= 115: + use_version = ( + sb_install.get_cft_latest_version_from_milestone(major) + ) + return use_version + + +def find_edgedriver_version_to_use(use_version, driver_version): + if ( + driver_version + and str(driver_version).split(".")[0].isdigit() + and int(str(driver_version).split(".")[0]) >= 80 + ): + use_version = str(driver_version) + return use_version + + +def has_captcha(text): + if ( + "403 Forbidden" in text + or "Permission Denied" in text + or 'id="challenge-error-text"' in text + or "/challenge-platform/h/b/" in text + or "Just a moment..." in text + or 'action="/?__cf_chl_f_tk' in text + or 'id="challenge-widget-' in text + or 'src="chromedriver.js"' in text + or "com/recaptcha/api.js" in text + or 'class="g-recaptcha"' in text + or 'content="Pixelscan"' in text + or 'id="challenge-form"' in text + or "window._cf_chl_opt" in text + or "/turnstile/" in text + ): + return True + return False + + +def __is_cdp_swap_needed(driver): + """If the driver is disconnected, use a CDP method when available.""" + return shared_utils.is_cdp_swap_needed(driver) + + +def uc_execute_cdp_cmd(driver, *args, **kwargs): + if not driver.is_connected(): + driver.connect() + return driver.default_execute_cdp_cmd(*args, **kwargs) + + +def updated_get(driver, url): + if url and ":" not in url and "." in url: + url = "https:" + url + driver.default_get(url) + + +def uc_special_open_if_cf( + driver, + url, + proxy_string=None, + mobile_emulator=None, + device_width=None, + device_height=None, + device_pixel_ratio=None, +): + if url and ":" not in url and "." in url: + url = "https:" + url + if url.startswith("http:") or url.startswith("https:"): + special = False + with suppress(Exception): + req_get = requests_get(url, proxy_string) + status_str = str(req_get.status_code) + if ( + status_str.startswith("3") + or status_str.startswith("4") + or status_str.startswith("5") + or has_captcha(req_get.text) + ): + special = True + if status_str == "403" or status_str == "429": + time.sleep(0.06) # Forbidden / Blocked! (Wait first!) + if special and not hasattr(driver, "cdp_base"): + time.sleep(0.05) + with driver: + driver.execute_script('window.open("%s","_blank");' % url) + driver.close() + if mobile_emulator: + page_actions.switch_to_window( + driver, driver.window_handles[-1], 2 + ) + uc_metrics = {} + if ( + isinstance(device_width, int) + and isinstance(device_height, int) + and isinstance(device_pixel_ratio, (int, float)) + ): + uc_metrics["width"] = device_width + uc_metrics["height"] = device_height + uc_metrics["pixelRatio"] = device_pixel_ratio + else: + uc_metrics["width"] = constants.Mobile.WIDTH + uc_metrics["height"] = constants.Mobile.HEIGHT + uc_metrics["pixelRatio"] = constants.Mobile.RATIO + set_device_metrics_override = dict( + { + "width": uc_metrics["width"], + "height": uc_metrics["height"], + "deviceScaleFactor": uc_metrics["pixelRatio"], + "mobile": True + } + ) + with suppress(Exception): + driver.execute_cdp_cmd( + 'Emulation.setDeviceMetricsOverride', + set_device_metrics_override + ) + if not mobile_emulator: + page_actions.switch_to_window( + driver, driver.window_handles[-1], 2 + ) + else: + driver.default_get(url) # The original one + else: + driver.default_get(url) # The original one + return None + + +def uc_open(driver, url): + url = shared_utils.fix_url_as_needed(url) + if __is_cdp_swap_needed(driver): + driver.cdp.get(url) + time.sleep(0.3) + return + if (url.startswith("http:") or url.startswith("https:")): + with driver: + script = 'window.location.href = "%s";' % url + js_utils.call_me_later(driver, script, 5) + else: + driver.default_get(url) # The original one + return None + + +def uc_open_with_tab(driver, url): + url = shared_utils.fix_url_as_needed(url) + if __is_cdp_swap_needed(driver): + driver.cdp.get(url) + time.sleep(0.3) + return + if (url.startswith("http:") or url.startswith("https:")): + if not hasattr(driver, "cdp_base"): + with driver: + driver.execute_script('window.open("%s","_blank");' % url) + driver.close() + else: + driver.cdp.open(url) + page_actions.switch_to_window(driver, driver.window_handles[-1], 2) + else: + driver.default_get(url) # The original one + return None + + +def uc_open_with_reconnect(driver, url, reconnect_time=None): + """Open a url, disconnect chromedriver, wait, and reconnect.""" + if ( + hasattr(sb_config, "_cdp_browser") + and sb_config._cdp_browser in ["comet", "opera"] + ): + if not __is_cdp_swap_needed(driver): + if not driver.current_url.startswith( + ("about", "data", "chrome") + ): + driver.get("about:blank") + uc_activate_cdp_mode(driver, url) + else: + driver.cdp.open(url) + return + url = shared_utils.fix_url_as_needed(url) + if __is_cdp_swap_needed(driver): + driver.cdp.get(url) + time.sleep(0.3) + return + if not reconnect_time: + reconnect_time = constants.UC.RECONNECT_TIME + if (url.startswith("http:") or url.startswith("https:")): + script = 'window.open("%s","_blank");' % url + if not hasattr(driver, "cdp_base"): + driver.execute_script(script) + time.sleep(0.05) + driver.close() + else: + driver.cdp.open(url) + if reconnect_time == "disconnect": + driver.disconnect() + time.sleep(0.008) + else: + driver.reconnect(reconnect_time) + time.sleep(0.004) + try: + page_actions.switch_to_window( + driver, driver.window_handles[-1], 2 + ) + except InvalidSessionIdException: + time.sleep(0.05) + page_actions.switch_to_window( + driver, driver.window_handles[-1], 2 + ) + else: + driver.default_get(url) # The original one + return None + + +def uc_open_with_cdp_mode(driver, url=None, **kwargs): + """Activate CDP Mode with the URL and kwargs.""" + import asyncio + from seleniumbase.undetected.cdp_driver import cdp_util + + time.sleep(0.012) + try: + driver.disconnect() + except Exception: + time.sleep(0.012) + driver.disconnect() + + cdp_details = driver._get_cdp_details() + cdp_host = cdp_details[1].split("://")[1].split(":")[0] + cdp_port = int(cdp_details[1].split("://")[1].split(":")[1].split("/")[0]) + + url = shared_utils.fix_url_as_needed(url) + url_protocol = url.split(":")[0] + safe_url = True + if url_protocol not in ["about", "data", "chrome"]: + safe_url = False + + if ( + getattr(driver, "_is_using_cdp", None) + and getattr(driver, "cdp", None) + and hasattr(driver.cdp, "loop") + ): + # CDP Mode was already initialized + driver.cdp.open(url, **kwargs) + if not safe_url: + time.sleep(constants.UC.CDP_MODE_OPEN_WAIT) + if IS_WINDOWS: + time.sleep(constants.UC.EXTRA_WINDOWS_WAIT) + else: + time.sleep(0.012) + return + + headless = False + headed = None + xvfb = None + xvfb_metrics = None + binary_location = None + if hasattr(sb_config, "headless"): + headless = sb_config.headless + if hasattr(sb_config, "headed"): + headed = sb_config.headed + if hasattr(sb_config, "xvfb"): + xvfb = sb_config.xvfb + if hasattr(sb_config, "xvfb_metrics"): + xvfb_metrics = sb_config.xvfb_metrics + if hasattr(sb_config, "binary_location"): + binary_location = sb_config.binary_location + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + driver.cdp_base = loop.run_until_complete( + cdp_util.start( + host=cdp_host, + port=cdp_port, + headless=headless, + headed=headed, + xvfb=xvfb, + xvfb_metrics=xvfb_metrics, + browser_executable_path=binary_location, + mobile=getattr(sb_config, "_cdp_mobile_mode", None), + ) + ) + loop.run_until_complete(driver.cdp_base.wait(0)) + + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + + if ( + "chrome-extension://" in str(driver.cdp_base.main_tab) + and len(driver.cdp_base.tabs) >= 2 + ): + with suppress(Exception): + loop.run_until_complete(driver.cdp_base.main_tab.close()) + + for tab in driver.cdp_base.tabs[-1::-1]: + if "chrome-extension://" not in str(tab): + with gui_lock: + with suppress(Exception): + shared_utils.make_writable( + constants.MultiBrowser.PYAUTOGUILOCK + ) + loop.run_until_complete(tab.activate()) + break + + page_tab = None + if "chrome-extension://" not in str(driver.cdp_base.tabs[-1]): + page_tab = driver.cdp_base.tabs[-1] + else: + for tab in driver.cdp_base.tabs: + if "chrome-extension://" not in str(tab): + page_tab = tab + break + if page_tab: + loop.run_until_complete(page_tab.aopen()) + with gui_lock: + with suppress(Exception): + shared_utils.make_writable( + constants.MultiBrowser.PYAUTOGUILOCK + ) + loop.run_until_complete(page_tab.activate()) + + loop.run_until_complete(driver.cdp_base.update_targets()) + page = loop.run_until_complete( + driver.cdp_base.get(url, **kwargs) + ) + with gui_lock: + with suppress(Exception): + shared_utils.make_writable(constants.MultiBrowser.PYAUTOGUILOCK) + loop.run_until_complete(page.activate()) + loop.run_until_complete(page.wait()) + if not safe_url: + time.sleep(constants.UC.CDP_MODE_OPEN_WAIT) + if IS_WINDOWS: + time.sleep(constants.UC.EXTRA_WINDOWS_WAIT) + else: + time.sleep(0.012) + cdp = types.SimpleNamespace() + CDPM = sb_cdp.CDPMethods(loop, page, driver) + cdp.get = CDPM.get + cdp.open = CDPM.open + cdp.goto = CDPM.goto + cdp.reload = CDPM.reload + cdp.refresh = CDPM.refresh + cdp.goto_if_not_url = CDPM.goto_if_not_url + cdp.add_handler = CDPM.add_handler + cdp.get_event_loop = CDPM.get_event_loop + cdp.get_rd_host = CDPM.get_rd_host + cdp.get_rd_port = CDPM.get_rd_port + cdp.get_rd_url = CDPM.get_rd_url + cdp.get_endpoint_url = CDPM.get_endpoint_url + cdp.get_websocket_url = CDPM.get_websocket_url + cdp.get_port = CDPM.get_port + cdp.find_element = CDPM.find_element + cdp.find = CDPM.find_element + cdp.locator = CDPM.find_element + cdp.find_element_by_text = CDPM.find_element_by_text + cdp.find_all = CDPM.find_all + cdp.find_elements_by_text = CDPM.find_elements_by_text + cdp.select = CDPM.select + cdp.select_all = CDPM.select_all + cdp.find_elements = CDPM.find_elements + cdp.find_visible_elements = CDPM.find_visible_elements + cdp.click_nth_element = CDPM.click_nth_element + cdp.click_nth_visible_element = CDPM.click_nth_visible_element + cdp.click_link = CDPM.click_link + cdp.go_back = CDPM.go_back + cdp.go_forward = CDPM.go_forward + cdp.get_navigation_history = CDPM.get_navigation_history + cdp.tile_windows = CDPM.tile_windows + cdp.grant_permissions = CDPM.grant_permissions + cdp.grant_all_permissions = CDPM.grant_all_permissions + cdp.reset_permissions = CDPM.reset_permissions + cdp.get_all_urls = CDPM.get_all_urls + cdp.get_all_cookies = CDPM.get_all_cookies + cdp.set_all_cookies = CDPM.set_all_cookies + cdp.save_cookies = CDPM.save_cookies + cdp.load_cookies = CDPM.load_cookies + cdp.clear_cookies = CDPM.clear_cookies + cdp.sleep = CDPM.sleep + cdp.bring_active_window_to_front = CDPM.bring_active_window_to_front + cdp.bring_to_front = CDPM.bring_active_window_to_front + cdp.get_active_element = CDPM.get_active_element + cdp.get_active_element_css = CDPM.get_active_element_css + cdp.click = CDPM.click + cdp.click_active_element = CDPM.click_active_element + cdp.click_if_visible = CDPM.click_if_visible + cdp.click_visible_elements = CDPM.click_visible_elements + cdp.click_with_offset = CDPM.click_with_offset + cdp.click_and_hold = CDPM.click_and_hold + cdp.drag_and_drop = CDPM.drag_and_drop + cdp.mouse_click = CDPM.mouse_click + cdp.get_parent = CDPM.get_parent + cdp.remove_element = CDPM.remove_element + cdp.remove_from_dom = CDPM.remove_from_dom + cdp.remove_elements = CDPM.remove_elements + cdp.send_keys = CDPM.send_keys + cdp.press_keys = CDPM.press_keys + cdp.type = CDPM.type + cdp.clear_input = CDPM.clear_input + cdp.clear = CDPM.clear_input + cdp.set_value = CDPM.set_value + cdp.submit = CDPM.submit + cdp.evaluate = CDPM.evaluate + cdp.execute_script = CDPM.execute_script + cdp.js_dumps = CDPM.js_dumps + cdp.maximize = CDPM.maximize + cdp.minimize = CDPM.minimize + cdp.medimize = CDPM.medimize + cdp.set_window_rect = CDPM.set_window_rect + cdp.reset_window_size = CDPM.reset_window_size + cdp.activate_messenger = CDPM.activate_messenger + cdp.set_messenger_theme = CDPM.set_messenger_theme + cdp.post_message = CDPM.post_message + cdp.download_file = CDPM.download_file + cdp.save_file_as = CDPM.save_file_as + cdp.assert_downloaded_file = CDPM.assert_downloaded_file + cdp.get_path_of_downloaded_file = CDPM.get_path_of_downloaded_file + cdp.set_download_path = CDPM.set_download_path + cdp.set_locale = CDPM.set_locale + cdp.set_local_storage_item = CDPM.set_local_storage_item + cdp.set_session_storage_item = CDPM.set_session_storage_item + cdp.set_attributes = CDPM.set_attributes + cdp.is_attribute_present = CDPM.is_attribute_present + cdp.is_online = CDPM.is_online + cdp.solve_captcha = CDPM.solve_captcha + cdp.click_captcha = CDPM.click_captcha + cdp.gui_press_key = CDPM.gui_press_key + cdp.gui_press_keys = CDPM.gui_press_keys + cdp.gui_write = CDPM.gui_write + cdp.gui_click_x_y = CDPM.gui_click_x_y + cdp.gui_click_element = CDPM.gui_click_element + cdp.gui_click_with_offset = CDPM.gui_click_with_offset + cdp.gui_click_captcha = CDPM.gui_click_captcha + cdp.gui_drag_drop_points = CDPM.gui_drag_drop_points + cdp.gui_drag_and_drop = CDPM.gui_drag_and_drop + cdp.gui_click_and_hold = CDPM.gui_click_and_hold + cdp.gui_move_to_element = CDPM.gui_move_to_element + cdp.gui_hover_element = CDPM.gui_hover_element + cdp.gui_hover_x_y = CDPM.gui_hover_x_y + cdp.gui_hover_and_click = CDPM.gui_hover_and_click + cdp.hover_element = CDPM.hover_element + cdp.hover_and_click = CDPM.hover_and_click + cdp.internalize_links = CDPM.internalize_links + cdp.open_new_window = CDPM.open_new_window + cdp.switch_to_window = CDPM.switch_to_window + cdp.switch_to_newest_window = CDPM.switch_to_newest_window + cdp.open_new_tab = CDPM.open_new_tab + cdp.switch_to_tab = CDPM.switch_to_tab + cdp.switch_to_newest_tab = CDPM.switch_to_newest_tab + cdp.close_active_tab = CDPM.close_active_tab + cdp.get_active_tab = CDPM.get_active_tab + cdp.get_tabs = CDPM.get_tabs + cdp.get_window = CDPM.get_window + cdp.get_element_attributes = CDPM.get_element_attributes + cdp.get_element_attribute = CDPM.get_element_attribute + cdp.get_attribute = CDPM.get_attribute + cdp.get_element_html = CDPM.get_element_html + cdp.get_element_rect = CDPM.get_element_rect + cdp.get_element_size = CDPM.get_element_size + cdp.get_element_position = CDPM.get_element_position + cdp.get_gui_element_rect = CDPM.get_gui_element_rect + cdp.get_gui_element_center = CDPM.get_gui_element_center + cdp.get_html = CDPM.get_html + cdp.get_page_source = CDPM.get_page_source + cdp.get_beautiful_soup = CDPM.get_beautiful_soup + cdp.get_user_agent = CDPM.get_user_agent + cdp.get_cookie_string = CDPM.get_cookie_string + cdp.get_locale_code = CDPM.get_locale_code + cdp.get_local_storage_item = CDPM.get_local_storage_item + cdp.get_session_storage_item = CDPM.get_session_storage_item + cdp.get_text = CDPM.get_text + cdp.get_title = CDPM.get_title + cdp.get_page_title = CDPM.get_title + cdp.get_current_url = CDPM.get_current_url + cdp.get_origin = CDPM.get_origin + cdp.get_nested_element = CDPM.get_nested_element + cdp.get_document = CDPM.get_document + cdp.get_flattened_document = CDPM.get_flattened_document + cdp.get_screen_rect = CDPM.get_screen_rect + cdp.get_window_rect = CDPM.get_window_rect + cdp.get_window_size = CDPM.get_window_size + cdp.get_mfa_code = CDPM.get_mfa_code + cdp.nested_click = CDPM.nested_click + cdp.select_option_by_text = CDPM.select_option_by_text + cdp.select_option_by_index = CDPM.select_option_by_index + cdp.select_option_by_value = CDPM.select_option_by_value + cdp.enter_mfa_code = CDPM.enter_mfa_code + cdp.flash = CDPM.flash + cdp.highlight = CDPM.highlight + cdp.focus = CDPM.focus + cdp.highlight_overlay = CDPM.highlight_overlay + cdp.get_window_position = CDPM.get_window_position + cdp.check_if_unchecked = CDPM.check_if_unchecked + cdp.uncheck_if_checked = CDPM.uncheck_if_checked + cdp.select_if_unselected = CDPM.select_if_unselected + cdp.unselect_if_selected = CDPM.unselect_if_selected + cdp.is_checked = CDPM.is_checked + cdp.is_selected = CDPM.is_selected + cdp.is_element_present = CDPM.is_element_present + cdp.is_element_visible = CDPM.is_element_visible + cdp.is_text_visible = CDPM.is_text_visible + cdp.is_exact_text_visible = CDPM.is_exact_text_visible + cdp.wait_for_text = CDPM.wait_for_text + cdp.wait_for_text_not_visible = CDPM.wait_for_text_not_visible + cdp.wait_for_element_visible = CDPM.wait_for_element_visible + cdp.wait_for_element = CDPM.wait_for_element + cdp.wait_for_element_not_visible = CDPM.wait_for_element_not_visible + cdp.wait_for_element_absent = CDPM.wait_for_element_absent + cdp.wait_for_any_of_elements_visible = ( + CDPM.wait_for_any_of_elements_visible + ) + cdp.wait_for_any_of_elements_present = ( + CDPM.wait_for_any_of_elements_present + ) + cdp.assert_any_of_elements_visible = CDPM.assert_any_of_elements_visible + cdp.assert_any_of_elements_present = CDPM.assert_any_of_elements_present + cdp.assert_element = CDPM.assert_element + cdp.assert_element_visible = CDPM.assert_element_visible + cdp.assert_element_present = CDPM.assert_element_present + cdp.assert_element_absent = CDPM.assert_element_absent + cdp.assert_element_not_visible = CDPM.assert_element_not_visible + cdp.assert_element_attribute = CDPM.assert_element_attribute + cdp.assert_title = CDPM.assert_title + cdp.assert_title_contains = CDPM.assert_title_contains + cdp.assert_url = CDPM.assert_url + cdp.assert_url_contains = CDPM.assert_url_contains + cdp.assert_text = CDPM.assert_text + cdp.assert_exact_text = CDPM.assert_exact_text + cdp.assert_text_not_visible = CDPM.assert_text_not_visible + cdp.assert_true = CDPM.assert_true + cdp.assert_false = CDPM.assert_false + cdp.assert_equal = CDPM.assert_equal + cdp.assert_not_equal = CDPM.assert_not_equal + cdp.assert_in = CDPM.assert_in + cdp.assert_not_in = CDPM.assert_not_in + cdp.js_scroll_into_view = CDPM.js_scroll_into_view + cdp.scroll_into_view = CDPM.scroll_into_view + cdp.scroll_to_y = CDPM.scroll_to_y + cdp.scroll_by_y = CDPM.scroll_by_y + cdp.scroll_to_top = CDPM.scroll_to_top + cdp.scroll_to_bottom = CDPM.scroll_to_bottom + cdp.scroll_up = CDPM.scroll_up + cdp.scroll_down = CDPM.scroll_down + cdp.save_page_source = CDPM.save_page_source + cdp.save_as_html = CDPM.save_as_html + cdp.save_screenshot = CDPM.save_screenshot + cdp.print_to_pdf = CDPM.print_to_pdf + cdp.save_as_pdf = CDPM.save_as_pdf + cdp.save_as_pdf_to_logs = CDPM.save_as_pdf_to_logs + cdp.save_screenshot_to_logs = CDPM.save_screenshot_to_logs + cdp.save_page_source_to_logs = CDPM.save_page_source_to_logs + cdp.save_as_html_to_logs = CDPM.save_as_html_to_logs + cdp.save_data_to_logs = CDPM.save_data_to_logs + cdp.append_data_to_logs = CDPM.append_data_to_logs + cdp.page = page # async world + cdp.driver = driver.cdp_base # async world + cdp.tab = cdp.page # shortcut (original) + cdp.browser = driver.cdp_base # shortcut (original) + cdp.util = cdp_util # shortcut (original) + core_items = types.SimpleNamespace() + core_items.browser = cdp.browser + core_items.tab = cdp.tab + core_items.util = cdp.util + cdp._swap_driver = CDPM._swap_driver + cdp.core = core_items + cdp.loop = cdp.get_event_loop() + driver.cdp = cdp + driver.solve_captcha = CDPM.solve_captcha + driver.click_captcha = CDPM.click_captcha + driver.find_element_by_text = CDPM.find_element_by_text + driver.flash = CDPM.flash + driver._is_using_cdp = True + if ( + getattr(sb_config, "_cdp_proxy", None) + and "@" in sb_config._cdp_proxy + ): + time.sleep(0.077) + loop.run_until_complete(page.wait(0.25)) + time.sleep(0.022) + + +def uc_activate_cdp_mode(driver, url=None, **kwargs): + uc_open_with_cdp_mode(driver, url=url, **kwargs) + + +def uc_open_with_disconnect(driver, url, timeout=None): + """Open a url and disconnect chromedriver. + Then waits for the duration of the timeout. + Note: You can't perform Selenium actions again + until after you've called driver.connect().""" + url = shared_utils.fix_url_as_needed(url) + if __is_cdp_swap_needed(driver): + driver.cdp.get(url) + time.sleep(0.3) + return + if not driver.is_connected(): + driver.connect() + if (url.startswith("http:") or url.startswith("https:")): + script = 'window.open("%s","_blank");' % url + driver.execute_script(script) + time.sleep(0.05) + driver.close() + driver.disconnect() + min_timeout = 0.008 + if timeout and not str(timeout).replace(".", "", 1).isdigit(): + timeout = min_timeout + if not timeout or timeout < min_timeout: + timeout = min_timeout + time.sleep(timeout) + else: + driver.default_get(url) # The original one + return None + + +def uc_click( + driver, + selector, + by="css selector", + timeout=settings.SMALL_TIMEOUT, + reconnect_time=None, +): + if __is_cdp_swap_needed(driver): + driver.cdp.click(selector) + return + with suppress(Exception): + rct = float(by) # Add shortcut: driver.uc_click(selector, RCT) + if not reconnect_time: + reconnect_time = rct + by = "css selector" + element = driver.wait_for_selector(selector, by=by, timeout=timeout) + tag_name = element.tag_name + if not tag_name == "span" and not tag_name == "input": # Must be "visible" + element = driver.wait_for_element(selector, by=by, timeout=timeout) + try: + element.uc_click( + driver, + selector, + by=by, + reconnect_time=reconnect_time, + tag_name=tag_name, + ) + except ElementClickInterceptedException: + time.sleep(0.16) + driver.js_click(selector, by=by, timeout=timeout) + if not reconnect_time: + driver.reconnect(0.1) + else: + driver.reconnect(reconnect_time) + + +def verify_pyautogui_has_a_headed_browser(driver): + """PyAutoGUI requires a headed browser so that it can + focus on the correct element when performing actions.""" + if getattr(driver, "_is_hidden", None): + raise Exception( + "PyAutoGUI can't be used in headless mode!" + ) + + +def __install_pyautogui_if_missing(): + try: + import pyautogui + with suppress(Exception): + use_pyautogui_ver = constants.PyAutoGUI.VER + u_pv = shared_utils.make_version_tuple(use_pyautogui_ver) + pv = shared_utils.make_version_tuple(pyautogui.__version__) + if pv < u_pv: + del pyautogui # To get newer ver + shared_utils.pip_install("pyautogui", version="Latest") + import pyautogui + except Exception: + print("\nPyAutoGUI required! Installing now...") + shared_utils.pip_install("pyautogui", version="Latest") + try: + import pyautogui + except Exception: + if ( + IS_LINUX + and hasattr(sb_config, "xvfb") + and hasattr(sb_config, "headed") + and hasattr(sb_config, "headless") + and hasattr(sb_config, "headless2") + and (not sb_config.headed or sb_config.xvfb) + and not (sb_config.headless or sb_config.headless2) + ): + from sbvirtualdisplay import Display + xvfb_width = 1366 + xvfb_height = 768 + if ( + getattr(sb_config, "_xvfb_width", None) + and isinstance(sb_config._xvfb_width, int) + and getattr(sb_config, "_xvfb_height", None) + and isinstance(sb_config._xvfb_height, int) + ): + xvfb_width = sb_config._xvfb_width + xvfb_height = sb_config._xvfb_height + if xvfb_width < 1024: + xvfb_width = 1024 + sb_config._xvfb_width = xvfb_width + if xvfb_height < 768: + xvfb_height = 768 + sb_config._xvfb_height = xvfb_height + with suppress(Exception): + _xvfb_display = Display( + visible=True, + size=(xvfb_width, xvfb_height), + backend="xvfb", + use_xauth=True, + ) + if "--debug-display" in sys.argv: + print( + "Starting VDisplay from browser_launcher: (%s, %s)" + % (xvfb_width, xvfb_height) + ) + _xvfb_display.start() + sb_config._virtual_display = _xvfb_display + sb_config.headless_active = True + if ( + getattr(sb_config, "reuse_session", None) + and hasattr(sb_config, "_vd_list") + and isinstance(sb_config._vd_list, list) + ): + sb_config._vd_list.append(_xvfb_display) + + +def install_pyautogui_if_missing(driver): + verify_pyautogui_has_a_headed_browser(driver) + pip_find_lock = fasteners.InterProcessLock( + constants.PipInstall.FINDLOCK + ) + try: + with pip_find_lock: + pass + except Exception: + # Since missing permissions, skip the locks + __install_pyautogui_if_missing() + return + with pip_find_lock: # Prevent issues with multiple processes + with suppress(Exception): + shared_utils.make_writable(constants.PipInstall.FINDLOCK) + __install_pyautogui_if_missing() + + +def get_configured_pyautogui(pyautogui_copy): + if ( + IS_LINUX + and hasattr(pyautogui_copy, "_pyautogui_x11") + and "DISPLAY" in os.environ.keys() + ): + if ( + getattr(sb_config, "_pyautogui_x11_display", None) + and hasattr(pyautogui_copy._pyautogui_x11, "_display") + and ( + sb_config._pyautogui_x11_display + == pyautogui_copy._pyautogui_x11._display + ) + ): + pass + else: + import Xlib.display + pyautogui_copy._pyautogui_x11._display = ( + Xlib.display.Display(os.environ['DISPLAY']) + ) + sb_config._pyautogui_x11_display = ( + pyautogui_copy._pyautogui_x11._display + ) + return pyautogui_copy + + +def uc_gui_press_key(driver, key): + install_pyautogui_if_missing(driver) + import pyautogui + pyautogui = get_configured_pyautogui(pyautogui) + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: + pyautogui.press(key) + + +def uc_gui_press_keys(driver, keys): + install_pyautogui_if_missing(driver) + import pyautogui + pyautogui = get_configured_pyautogui(pyautogui) + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: + for key in keys: + pyautogui.press(key) + + +def uc_gui_write(driver, text): + install_pyautogui_if_missing(driver) + import pyautogui + pyautogui = get_configured_pyautogui(pyautogui) + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: + pyautogui.write(text) + + +def get_gui_element_position(driver, selector): + if __is_cdp_swap_needed(driver): + element_rect = driver.cdp.get_gui_element_rect(selector) + return (element_rect["x"], element_rect["y"]) + element = driver.wait_for_element_present(selector, timeout=3) + element_rect = element.rect + window_rect = driver.get_window_rect() + window_bottom_y = window_rect["y"] + window_rect["height"] + viewport_height = driver.execute_script("return window.innerHeight;") + viewport_x = window_rect["x"] + element_rect["x"] + viewport_y = window_bottom_y - viewport_height + element_rect["y"] + y_scroll_offset = driver.execute_script("return window.pageYOffset;") + viewport_y = viewport_y - y_scroll_offset + return (viewport_x, viewport_y) + + +def _uc_gui_click_x_y(driver, x, y, timeframe=0.25, uc_lock=False): + install_pyautogui_if_missing(driver) + import pyautogui + pyautogui = get_configured_pyautogui(pyautogui) + screen_width, screen_height = pyautogui.size() + if x < 0 or y < 0 or x > screen_width or y > screen_height: + raise Exception( + "PyAutoGUI cannot click on point (%s, %s)" + " outside screen. (Width: %s, Height: %s)" + % (x, y, screen_width, screen_height) + ) + if uc_lock: + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: # Prevent issues with multiple processes + pyautogui.moveTo(x, y, timeframe, pyautogui.easeOutQuad) + if timeframe >= 0.25: + time.sleep(0.056) # Wait if moving at human-speed + if "--debug" in sys.argv: + print(" <DEBUG> pyautogui.click(%s, %s)" % (x, y)) + pyautogui.click(x=x, y=y) + else: + # Called from a method where the gui_lock is already active + pyautogui.moveTo(x, y, timeframe, pyautogui.easeOutQuad) + if timeframe >= 0.25: + time.sleep(0.056) # Wait if moving at human-speed + if "--debug" in sys.argv: + print(" <DEBUG> pyautogui.click(%s, %s)" % (x, y)) + pyautogui.click(x=x, y=y) + + +def uc_gui_click_x_y(driver, x, y, timeframe=0.25): + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: # Prevent issues with multiple processes + install_pyautogui_if_missing(driver) + import pyautogui + pyautogui = get_configured_pyautogui(pyautogui) + connected = True + width_ratio = 1.0 + if IS_WINDOWS: + connected = driver.is_connected() + if ( + not connected + and not getattr(sb_config, "_saved_width_ratio", None) + and not __is_cdp_swap_needed(driver) + ): + driver.reconnect(0.1) + if IS_WINDOWS and not __is_cdp_swap_needed(driver): + window_rect = driver.get_window_rect() + width = window_rect["width"] + height = window_rect["height"] + win_x = window_rect["x"] + win_y = window_rect["y"] + scr_width = pyautogui.size().width + driver.maximize_window() + win_width = driver.get_window_size()["width"] + width_ratio = round(float(scr_width) / float(win_width), 2) + 0.01 + if width_ratio < 0.45 or width_ratio > 2.55: + width_ratio = 1.01 + sb_config._saved_width_ratio = width_ratio + driver.minimize_window() + driver.set_window_rect(win_x, win_y, width, height) + elif IS_WINDOWS and __is_cdp_swap_needed(driver): + window_rect = driver.cdp.get_window_rect() + width = window_rect["width"] + height = window_rect["height"] + win_x = window_rect["x"] + win_y = window_rect["y"] + scr_width = pyautogui.size().width + driver.cdp.maximize() + win_width = driver.cdp.get_window_size()["width"] + width_ratio = round(float(scr_width) / float(win_width), 2) + 0.01 + if width_ratio < 0.45 or width_ratio > 2.55: + width_ratio = 1.01 + sb_config._saved_width_ratio = width_ratio + driver.cdp.minimize() + driver.cdp.set_window_rect(win_x, win_y, width, height) + if IS_WINDOWS: + x = x * (width_ratio + 0.03) + y = y * (width_ratio - 0.03) + _uc_gui_click_x_y(driver, x, y, timeframe=timeframe, uc_lock=False) + return + with suppress(Exception): + page_actions.switch_to_window( + driver, driver.current_window_handle, 2, uc_lock=False + ) + _uc_gui_click_x_y(driver, x, y, timeframe=timeframe, uc_lock=False) + + +def _on_a_cf_turnstile_page(driver): + source = driver.get_page_source() + if ( + ( + 'data-callback="onCaptchaSuccess"' in source + and 'title="reCAPTCHA"' not in source + and 'id="recaptcha-token"' not in source + ) + or "/challenge-platform/h/b/" in source + or 'id="challenge-widget-' in source + or "challenges.cloudf" in source + or "cf-turnstile-" in source + ): + return True + return False + + +def _on_a_g_recaptcha_page(driver): + source = driver.get_page_source() + if ( + 'id="recaptcha-token"' in source + or 'title="reCAPTCHA"' in source + ): + return True + return False + + +def _uc_gui_click_captcha( + driver, + frame="iframe", + retry=False, + blind=False, + ctype=None, +): + cdp_mode_on_at_start = __is_cdp_swap_needed(driver) + if cdp_mode_on_at_start: + return driver.cdp.gui_click_captcha() + _on_a_captcha_page = None + if ctype == "cf_t": + if not _on_a_cf_turnstile_page(driver): + return False + else: + _on_a_captcha_page = _on_a_cf_turnstile_page + elif ctype == "g_rc": + if not _on_a_g_recaptcha_page(driver): + return False + else: + _on_a_captcha_page = _on_a_g_recaptcha_page + else: + if _on_a_g_recaptcha_page(driver): + ctype = "g_rc" + _on_a_captcha_page = _on_a_g_recaptcha_page + elif _on_a_cf_turnstile_page(driver): + ctype = "cf_t" + _on_a_captcha_page = _on_a_cf_turnstile_page + else: + return False + install_pyautogui_if_missing(driver) + import pyautogui + pyautogui = get_configured_pyautogui(pyautogui) + i_x = None + i_y = None + x = None + y = None + visible_iframe = True + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: # Prevent issues with multiple processes + needs_switch = False + width_ratio = 1.0 + is_in_frame = js_utils.is_in_frame(driver) + if is_in_frame and driver.is_element_present("#challenge-stage"): + driver.switch_to.parent_frame() + needs_switch = True + is_in_frame = js_utils.is_in_frame(driver) + if not is_in_frame: + # Make sure the window is on top + if __is_cdp_swap_needed(driver): + driver.cdp.bring_active_window_to_front() + else: + page_actions.switch_to_window( + driver, driver.current_window_handle, 2, uc_lock=False + ) + if IS_WINDOWS and not __is_cdp_swap_needed(driver): + window_rect = driver.get_window_rect() + width = window_rect["width"] + height = window_rect["height"] + win_x = window_rect["x"] + win_y = window_rect["y"] + scr_width = pyautogui.size().width + driver.maximize_window() + win_width = driver.get_window_size()["width"] + width_ratio = round(float(scr_width) / float(win_width), 2) + 0.01 + if width_ratio < 0.45 or width_ratio > 2.55: + width_ratio = 1.01 + sb_config._saved_width_ratio = width_ratio + driver.minimize_window() + driver.set_window_rect(win_x, win_y, width, height) + elif IS_WINDOWS and __is_cdp_swap_needed(driver): + window_rect = driver.cdp.get_window_rect() + width = window_rect["width"] + height = window_rect["height"] + win_x = window_rect["x"] + win_y = window_rect["y"] + scr_width = pyautogui.size().width + driver.cdp.maximize() + win_width = driver.cdp.get_window_size()["width"] + width_ratio = round(float(scr_width) / float(win_width), 2) + 0.01 + if width_ratio < 0.45 or width_ratio > 2.55: + width_ratio = 1.01 + sb_config._saved_width_ratio = width_ratio + driver.cdp.minimize() + driver.cdp.set_window_rect(win_x, win_y, width, height) + if ctype == "cf_t": + if ( + driver.is_element_present(".cf-turnstile-wrapper iframe") + or driver.is_element_present( + '[data-callback="onCaptchaSuccess"] iframe' + ) + ): + pass + else: + visible_iframe = False + if ( + frame != "iframe" + and driver.is_element_present( + "%s .cf-turnstile-wrapper" % frame + ) + ): + frame = "%s .cf-turnstile-wrapper" % frame + elif ( + frame != "iframe" + and driver.is_element_present( + '%s [name*="cf-turnstile"]' % frame + ) + and driver.is_element_present("%s div" % frame) + ): + frame = "%s div" % frame + elif driver.is_element_present("#challenge-form div > div"): + frame = "#challenge-form div > div" + elif ( + driver.is_element_present( + '[style="display: grid;"] div div' + ) + ): + frame = '[style="display: grid;"] div div' + elif ( + driver.is_element_present('[name*="cf-turnstile-"]') + and driver.is_element_present( + ".spacer + div div:not([class])" + ) + ): + frame = '.spacer + div div:not([class])' + elif ( + driver.is_element_present(".spacer div:not([class])") + ): + frame = ".spacer div:not([class])" + elif ( + driver.is_element_present( + '[data-testid*="challenge-"] div' + ) + ): + frame = '[data-testid*="challenge-"] div' + elif driver.is_element_present( + "div#turnstile-widget div:not([class])" + ): + frame = "div#turnstile-widget div:not([class])" + elif driver.is_element_present( + "ngx-turnstile div:not([class])" + ): + frame = "ngx-turnstile div:not([class])" + elif driver.is_element_present( + 'form div:not([class]):has(input[name*="cf-turn"])' + ): + frame = 'form div:not([class]):has(input[name*="cf-turn"])' + elif ( + driver.is_element_present('[src*="/turnstile/"]') + and driver.is_element_present("form div:not(:has(*))") + ): + frame = "form div:not(:has(*))" + elif ( + driver.is_element_present('[src*="/turnstile/"]') + and driver.is_element_present( + "body > div#check > div:not([class])" + ) + ): + frame = "body > div#check > div:not([class])" + elif driver.is_element_present(".cf-turnstile-wrapper"): + frame = ".cf-turnstile-wrapper" + elif driver.is_element_present('[class="cf-turnstile"]'): + frame = '[class="cf-turnstile"]' + elif driver.is_element_present( + '[id*="turnstile"] div:not([class])' + ): + frame = '[id*="turnstile"] div:not([class])' + elif driver.is_element_present( + '[class*="turnstile"] div:not([class])' + ): + frame = '[class*="turnstile"] div:not([class])' + elif driver.is_element_present( + '[data-callback="onCaptchaSuccess"]' + ): + frame = '[data-callback="onCaptchaSuccess"]' + elif driver.is_element_present( + "div:not([class]):not([id]):not([aria-label]) > " + "div:not([class]):not([id]):not([aria-label])" + ): + frame = ( + "div:not([class]):not([id]):not([aria-label]) > " + "div:not([class]):not([id]):not([aria-label])" + ) + else: + return False + if ( + driver.is_element_present("form") + and ( + driver.is_element_present('form[class*="center"]') + or driver.is_element_present('form[class*="right"]') + or driver.is_element_present('form div[class*="center"]') + or driver.is_element_present('form div[class*="right"]') + ) + ): + script = ( + """var $elements = document.querySelectorAll( + 'form[class], form div[class]'); + var index = 0, length = $elements.length; + for(; index < length; index++){ + the_class = $elements[index].getAttribute('class'); + new_class = the_class.replaceAll('center', 'left'); + new_class = new_class.replaceAll('right', 'left'); + $elements[index].setAttribute('class', new_class);}""" + ) + if __is_cdp_swap_needed(driver): + driver.cdp.evaluate(script) + else: + driver.execute_script(script) + elif ( + driver.is_element_present("form") + and ( + driver.is_element_present('form div[style*="center"]') + or driver.is_element_present('form div[style*="right"]') + ) + ): + script = ( + """var $elements = document.querySelectorAll( + 'form[style], form div[style]'); + var index = 0, length = $elements.length; + for(; index < length; index++){ + the_style = $elements[index].getAttribute('style'); + new_style = the_style.replaceAll('center', 'left'); + new_style = new_style.replaceAll('right', 'left'); + $elements[index].setAttribute('style', new_style);}""" + ) + if __is_cdp_swap_needed(driver): + driver.cdp.evaluate(script) + else: + driver.execute_script(script) + elif ( + driver.is_element_present( + 'form [id*="turnstile"] div:not([class])' + ) + or driver.is_element_present( + 'form [class*="turnstile"] div:not([class])' + ) + ): + script = ( + """var $elements = document.querySelectorAll( + 'form [id*="turnstile"]'); + var index = 0, length = $elements.length; + for(; index < length; index++){ + $elements[index].setAttribute('align', 'left');} + var $elements = document.querySelectorAll( + 'form [class*="turnstile"]'); + var index = 0, length = $elements.length; + for(; index < length; index++){ + $elements[index].setAttribute('align', 'left');}""" + ) + if __is_cdp_swap_needed(driver): + driver.cdp.evaluate(script) + else: + driver.execute_script(script) + elif ( + driver.is_element_present( + '[style*="text-align: center;"] div:not([class])' + ) + ): + script = ( + """var $elements = document.querySelectorAll( + '[style*="text-align: center;"]'); + var index = 0, length = $elements.length; + for(; index < length; index++){ + the_style = $elements[index].getAttribute('style'); + new_style = the_style.replaceAll('center', 'left'); + $elements[index].setAttribute('style', new_style);}""" + ) + if __is_cdp_swap_needed(driver): + driver.cdp.evaluate(script) + else: + driver.execute_script(script) + if not is_in_frame or needs_switch: + # Currently not in frame (or nested frame outside CF one) + try: + i_x, i_y = get_gui_element_position(driver, frame) + if visible_iframe: + driver.switch_to_frame(frame) + except Exception: + if visible_iframe: + if driver.is_element_present("iframe"): + i_x, i_y = get_gui_element_position(driver, "iframe") + if driver.is_connected(): + driver.switch_to_frame("iframe") + else: + return False + if not i_x or not i_y: + return False + try: + if ctype == "g_rc" and not driver.is_connected(): + x = (i_x + 29) * width_ratio + y = (i_y + 35) * width_ratio + elif visible_iframe: + selector = "span" + if ctype == "g_rc": + selector = "span.recaptcha-checkbox" + if not driver.is_connected(): + selector = "iframe" + element = driver.wait_for_element_present( + selector, timeout=2.5 + ) + x = i_x + element.rect["x"] + (element.rect["width"] / 2.0) + x += 0.5 + y = i_y + element.rect["y"] + (element.rect["height"] / 2.0) + y += 0.5 + else: + x = (i_x + 28) * width_ratio + if not IS_WINDOWS: + y = (i_y + 32) * width_ratio + else: + y = (i_y + 22) * width_ratio + if driver.is_connected(): + driver.switch_to.default_content() + except Exception: + if driver.is_connected(): + try: + driver.switch_to.default_content() + except Exception: + return False + if x and y: + sb_config._saved_cf_x_y = (x, y) + if not __is_cdp_swap_needed(driver): + if driver.is_element_present(".footer .clearfix .ray-id"): + driver.uc_open_with_disconnect( + driver.get_current_url(), 3.8 + ) + else: + driver.disconnect() + with suppress(Exception): + _uc_gui_click_x_y(driver, x, y, timeframe=0.32) + if __is_cdp_swap_needed(driver): + time.sleep(float(constants.UC.RECONNECT_TIME) / 2.0) + return True + reconnect_time = (float(constants.UC.RECONNECT_TIME) / 2.0) + 0.6 + if IS_LINUX: + reconnect_time = constants.UC.RECONNECT_TIME + 0.2 + if not x or not y: + reconnect_time = 1 # Make it quick (it already failed) + driver.reconnect(reconnect_time) + caught = False + if ( + driver.is_element_present(".footer .clearfix .ray-id") + and not driver.is_element_visible("#challenge-success-text") + ): + blind = True + caught = True + if blind: + retry = True + if retry and x and y and (caught or _on_a_captcha_page(driver)): + with gui_lock: # Prevent issues with multiple processes + # Make sure the window is on top + if __is_cdp_swap_needed(driver): + driver.cdp.bring_active_window_to_front() + else: + page_actions.switch_to_window( + driver, driver.current_window_handle, 2, uc_lock=False + ) + if driver.is_element_present("iframe"): + try: + driver.switch_to_frame(frame) + except Exception: + try: + driver.switch_to_frame("iframe") + except Exception: + return False + checkbox_success = None + if ctype == "cf_t": + checkbox_success = "#success-icon" + elif ctype == "g_rc": + checkbox_success = "span.recaptcha-checkbox-checked" + else: + return False # If line is reached, ctype wasn't set + if driver.is_element_visible("#success-icon"): + driver.switch_to.parent_frame(checkbox_success) + return True + if blind: + driver.uc_open_with_disconnect(driver.get_current_url(), 3.8) + if __is_cdp_swap_needed(driver) and _on_a_captcha_page(driver): + _uc_gui_click_x_y(driver, x, y, timeframe=0.32) + else: + time.sleep(0.1) + else: + driver.uc_open_with_reconnect(driver.get_current_url(), 3.8) + if _on_a_captcha_page(driver): + driver.disconnect() + _uc_gui_click_x_y(driver, x, y, timeframe=0.32) + if not cdp_mode_on_at_start: + driver.reconnect(reconnect_time) + return True + + +def uc_gui_click_captcha(driver, frame="iframe", retry=False, blind=False): + _uc_gui_click_captcha( + driver, + frame=frame, + retry=retry, + blind=blind, + ctype=None, + ) + + +def uc_gui_click_rc(driver, frame="iframe", retry=False, blind=False): + _uc_gui_click_captcha( + driver, + frame=frame, + retry=retry, + blind=blind, + ctype="g_rc", + ) + + +def uc_gui_click_cf(driver, frame="iframe", retry=False, blind=False): + _uc_gui_click_captcha( + driver, + frame=frame, + retry=retry, + blind=blind, + ctype="cf_t", + ) + + +def _uc_gui_handle_captcha_(driver, frame="iframe", ctype=None): + if ctype == "cf_t": + if not _on_a_cf_turnstile_page(driver): + return + elif ctype == "g_rc": + if not _on_a_g_recaptcha_page(driver): + return + else: + if _on_a_g_recaptcha_page(driver): + ctype = "g_rc" + elif _on_a_cf_turnstile_page(driver): + ctype = "cf_t" + else: + return + if not driver.is_connected() and not __is_cdp_swap_needed(driver): + driver.connect() + time.sleep(2) + install_pyautogui_if_missing(driver) + import pyautogui + pyautogui = get_configured_pyautogui(pyautogui) + visible_iframe = True + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: # Prevent issues with multiple processes + needs_switch = False + if not __is_cdp_swap_needed(driver): + is_in_frame = js_utils.is_in_frame(driver) + else: + is_in_frame = False + selector = "#challenge-stage" + if ctype == "g_rc": + selector = "#recaptcha-token" + if is_in_frame and driver.is_element_present(selector): + driver.switch_to.parent_frame() + needs_switch = True + is_in_frame = js_utils.is_in_frame(driver) + if not is_in_frame and not __is_cdp_swap_needed(driver): + # Make sure the window is on top + page_actions.switch_to_window( + driver, driver.current_window_handle, 2, uc_lock=False + ) + if IS_WINDOWS and hasattr(pyautogui, "getActiveWindowTitle"): + py_a_g_title = pyautogui.getActiveWindowTitle() or "" + window_title = driver.get_title() + if not py_a_g_title.startswith(window_title): + window_rect = driver.get_window_rect() + width = window_rect["width"] + height = window_rect["height"] + win_x = window_rect["x"] + win_y = window_rect["y"] + driver.minimize_window() + driver.set_window_rect(win_x, win_y, width, height) + time.sleep(0.33) + tab_up_first = False + if ctype == "cf_t": + if ( + driver.is_element_present(".cf-turnstile-wrapper iframe") + or driver.is_element_present( + '[data-callback="onCaptchaSuccess"] iframe' + ) + ): + pass + else: + visible_iframe = False + if driver.is_element_present(".cf-turnstile-wrapper"): + frame = ".cf-turnstile-wrapper" + elif driver.is_element_present( + '[data-callback="onCaptchaSuccess"]' + ): + frame = '[data-callback="onCaptchaSuccess"]' + elif ( + driver.is_element_present('[name*="cf-turnstile-"]') + and driver.is_element_present(".spacer div:not([class])") + ): + frame = ".spacer div:not([class])" + elif ( + driver.is_element_present('script[src*="challenges.c"]') + and driver.is_element_present( + '[data-testid*="challenge-"] div' + ) + ): + frame = '[data-testid*="challenge-"] div' + elif driver.is_element_present( + 'form div:not([class]):has(input[name*="cf-turn"])' + ): + frame = 'form div:not([class]):has(input[name*="cf-turn"])' + tab_up_first = True + elif ( + driver.is_element_present('[src*="/turnstile/"]') + and driver.is_element_present("form div:not(:has(*))") + ): + frame = "form div:not(:has(*))" + tab_up_first = True + elif ( + driver.is_element_present('[src*="/turnstile/"]') + and driver.is_element_present( + "body > div#check > div:not([class])" + ) + ): + frame = "body > div#check > div:not([class])" + elif driver.is_element_present( + "div#turnstile-widget div:not([class])" + ): + frame = "div#turnstile-widget div:not([class])" + elif driver.is_element_present(".cf-turnstile-wrapper"): + frame = ".cf-turnstile-wrapper" + elif driver.is_element_present('[class="cf-turnstile"]'): + frame = '[class="cf-turnstile"]' + elif driver.is_element_present( + "div:not([class]) > div:not([class])" + ): + frame = "div:not([class]) > div:not([class])" + else: + return + else: + if ( + driver.is_element_present('iframe[title="reCAPTCHA"]') + and frame == "iframe" + ): + frame = 'iframe[title="reCAPTCHA"]' + if not __is_cdp_swap_needed(driver): + if not is_in_frame or needs_switch: + # Currently not in frame (or nested frame outside CF one) + try: + if visible_iframe or ctype == "g_rc": + driver.switch_to_frame(frame) + except Exception: + if visible_iframe or ctype == "g_rc": + if driver.is_element_present("iframe"): + driver.switch_to_frame("iframe") + else: + return + try: + selector = "div.cf-turnstile" + if ctype == "g_rc": + selector = "span#recaptcha-anchor" + found_checkbox = False + if tab_up_first: + for i in range(10): + pyautogui.hotkey("shift", "tab") + time.sleep(0.027) + if ctype == "g_rc": + if js_utils.get_active_element_css(driver) == "body": + break + tab_count = 0 + for i in range(34): + pyautogui.press("\t") + tab_count += 1 + time.sleep(0.027) + active_element_css = js_utils.get_active_element_css(driver) + if ( + active_element_css.startswith(selector) + or active_element_css.endswith(" div") + or (ctype == "g_rc" and "frame[name" in active_element_css) + ): + found_checkbox = True + sb_config._saved_cf_tab_count = tab_count + break + time.sleep(0.02) + if not found_checkbox: + return + except Exception: + try: + driver.switch_to.default_content() + except Exception: + return + if ( + ( + driver.is_element_present(".footer .clearfix .ray-id") + or driver.is_element_present("script[data-cf-beacon]") + ) + and getattr(sb_config, "_saved_cf_tab_count", None) + and not __is_cdp_swap_needed(driver) + ): + driver.uc_open_with_disconnect(driver.current_url, 3.8) + with suppress(Exception): + if "--debug" in sys.argv: + if sb_config._saved_cf_tab_count == 1: + print(' <DEBUG> pyautogui.press("\\t")') + else: + print( + ' <DEBUG> pyautogui.press("\\t") * %s' + % sb_config._saved_cf_tab_count + ) + for i in range(sb_config._saved_cf_tab_count): + pyautogui.press("\t") + time.sleep(0.027) + if "--debug" in sys.argv: + print(' <DEBUG> pyautogui.press(" ")') + pyautogui.press(" ") + else: + driver.disconnect() + pyautogui.press(" ") + reconnect_time = (float(constants.UC.RECONNECT_TIME) / 2.0) + 0.6 + if IS_LINUX: + reconnect_time = constants.UC.RECONNECT_TIME + 0.2 + driver.reconnect(reconnect_time) + + +def _uc_gui_handle_captcha(driver, frame="iframe", ctype=None): + _uc_gui_handle_captcha_(driver, frame=frame, ctype=ctype) + if ( + driver.is_element_present(".footer .clearfix .ray-id") + and not driver.is_element_visible("#challenge-success-text") + ): + driver.uc_open_with_reconnect(driver.current_url, 3.8) + _uc_gui_handle_captcha_(driver, frame=frame, ctype=ctype) + + +def uc_gui_handle_captcha(driver, frame="iframe"): + _uc_gui_handle_captcha(driver, frame=frame, ctype=None) + + +def uc_gui_handle_cf(driver, frame="iframe"): + _uc_gui_handle_captcha(driver, frame=frame, ctype="cf_t") + + +def uc_gui_handle_rc(driver, frame="iframe"): + _uc_gui_handle_captcha(driver, frame=frame, ctype="g_rc") + + +def uc_switch_to_frame(driver, frame="iframe", reconnect_time=None): + from selenium.webdriver.remote.webelement import WebElement + if isinstance(frame, WebElement): + if not reconnect_time: + driver.reconnect(0.1) + else: + driver.reconnect(reconnect_time) + driver.switch_to.frame(frame) + else: + iframe = driver.locator(frame) + if not reconnect_time: + driver.reconnect(0.1) + else: + driver.reconnect(reconnect_time) + driver.switch_to.frame(iframe) + + +def edgedriver_on_path(): + return os.path.exists(LOCAL_EDGEDRIVER) + + +def geckodriver_on_path(): + paths = os.environ["PATH"].split(os.pathsep) + for path in paths: + if (not IS_WINDOWS) and os.path.exists(path + "/geckodriver"): + return True + elif IS_WINDOWS and os.path.exists(path + "/geckodriver.exe"): + return True + return False + + +def iedriver_on_path(): + paths = os.environ["PATH"].split(os.pathsep) + for path in paths: + if os.path.exists(path + "/IEDriverServer.exe"): + return True + return False + + +def headless_iedriver_on_path(): + return os.path.exists(LOCAL_HEADLESS_IEDRIVER) + + +def get_valid_binary_names_for_browser(browser): + if browser == constants.Browser.GOOGLE_CHROME: + if IS_LINUX: + return constants.ValidBinaries.valid_chrome_binaries_on_linux + elif IS_MAC: + return constants.ValidBinaries.valid_chrome_binaries_on_macos + elif IS_WINDOWS: + return constants.ValidBinaries.valid_chrome_binaries_on_windows + else: + raise Exception("Could not determine OS, or unsupported!") + elif browser == constants.Browser.EDGE: + if IS_LINUX: + return constants.ValidBinaries.valid_edge_binaries_on_linux + elif IS_MAC: + return constants.ValidBinaries.valid_edge_binaries_on_macos + elif IS_WINDOWS: + return constants.ValidBinaries.valid_edge_binaries_on_windows + else: + raise Exception("Could not determine OS, or unsupported!") + else: + raise Exception("Invalid combination for OS browser binaries!") + + +def _special_binary_exists(location, name): + filename = str(location).split("/")[-1].split("\\")[-1] + return ( + location + and str(name).lower() in filename.lower() + and os.path.exists(location) + ) + + +def _repair_chromedriver(chrome_options, headless_options, mcv=None): + if mcv: + subprocess.check_call( + "sbase get chromedriver %s" % mcv, shell=True + ) + return + driver = None + subprocess.check_call( + "sbase get chromedriver 72.0.3626.69", shell=True + ) + try: + service = ChromeService(executable_path=LOCAL_CHROMEDRIVER) + driver = webdriver.Chrome( + service=service, + options=headless_options, + ) + except Exception: + subprocess.check_call( + "sbase get chromedriver latest-1", shell=True + ) + return + chrome_version = None + if "version" in driver.capabilities: + chrome_version = driver.capabilities["version"] + else: + chrome_version = driver.capabilities["browserVersion"] + major_chrome_ver = chrome_version.split(".")[0] + chrome_dict = driver.capabilities["chrome"] + driver.quit() + chromedriver_ver = chrome_dict["chromedriverVersion"] + chromedriver_ver = chromedriver_ver.split(" ")[0] + major_chromedriver_ver = chromedriver_ver.split(".")[0] + if ( + major_chromedriver_ver != major_chrome_ver + and int(major_chrome_ver) >= 73 + ): + subprocess.check_call( + "sbase get chromedriver %s" % major_chrome_ver, shell=True + ) + return + + +def _repair_edgedriver(edge_version): + log_d( + "\nWarning: msedgedriver version doesn't match Edge version!" + "\nAttempting to install a matching version of msedgedriver:" + ) + subprocess.check_call( + "sbase get edgedriver %s" % edge_version, shell=True + ) + return + + +def _mark_driver_repaired(): + abs_path = os.path.abspath(".") + driver_repaired_lock = constants.MultiBrowser.DRIVER_REPAIRED + file_path = os.path.join(abs_path, driver_repaired_lock) + if not os.path.exists(DOWNLOADS_FOLDER): + os.makedirs(DOWNLOADS_FOLDER) + out_file = open(file_path, mode="w+", encoding="utf-8") + out_file.writelines("") + out_file.close() + + +def _was_driver_repaired(): + abs_path = os.path.abspath(".") + driver_repaired_lock = constants.MultiBrowser.DRIVER_REPAIRED + file_path = os.path.join(abs_path, driver_repaired_lock) + return os.path.exists(file_path) + + +def _set_proxy_filenames(): + DOWNLOADS_DIR = constants.Files.DOWNLOADS_FOLDER + for num in range(1000): + PROXY_ZIP_PATH = os.path.join(DOWNLOADS_DIR, "proxy_%s.zip" % num) + PROXY_DIR_PATH = os.path.join(DOWNLOADS_DIR, "proxy_ext_dir_%s" % num) + if os.path.exists(PROXY_ZIP_PATH) or os.path.exists(PROXY_DIR_PATH): + continue + proxy_helper.PROXY_ZIP_PATH = PROXY_ZIP_PATH + proxy_helper.PROXY_DIR_PATH = PROXY_DIR_PATH + return + # Exceeded upper bound. Use Defaults: + PROXY_ZIP_PATH = os.path.join(DOWNLOADS_DIR, "proxy.zip") + PROXY_DIR_PATH = os.path.join(DOWNLOADS_DIR, "proxy_ext_dir") + proxy_helper.PROXY_ZIP_PATH = PROXY_ZIP_PATH + proxy_helper.PROXY_DIR_PATH = PROXY_DIR_PATH def _add_chrome_proxy_extension( - chrome_options, proxy_string, proxy_user, proxy_pass): - """ Implementation of https://stackoverflow.com/a/35293284 for - https://stackoverflow.com/questions/12848327/ - (Run Selenium on a proxy server that requires authentication.) """ - if not "".join(sys.argv) == "-c": + chrome_options, + proxy_string, + proxy_user, + proxy_pass, + proxy_scheme, + proxy_bypass_list=None, + zip_it=True, + multi_proxy=False, +): + """Implementation of https://stackoverflow.com/a/35293284/7058266 + for https://stackoverflow.com/q/12848327/7058266 + (Run Selenium on a proxy server that requires authentication.)""" + zip_it = False + args = " ".join(sys.argv) + bypass_list = proxy_bypass_list + if ( + not ("-n" in sys.argv or " -n=" in args or args == "-c") + and not multi_proxy + ): # Single-threaded - proxy_helper.create_proxy_zip(proxy_string, proxy_user, proxy_pass) + if zip_it: + proxy_zip_lock = fasteners.InterProcessLock(PROXY_ZIP_LOCK) + with proxy_zip_lock: + proxy_helper.create_proxy_ext( + proxy_string, + proxy_user, + proxy_pass, + proxy_scheme, + bypass_list, + ) + proxy_zip = proxy_helper.PROXY_ZIP_PATH + chrome_options.add_extension(proxy_zip) + else: + proxy_dir_lock = fasteners.InterProcessLock(PROXY_DIR_LOCK) + with proxy_dir_lock: + proxy_helper.create_proxy_ext( + proxy_string, + proxy_user, + proxy_pass, + proxy_scheme, + bypass_list, + zip_it=False, + ) + proxy_dir_path = proxy_helper.PROXY_DIR_PATH + chrome_options = add_chrome_ext_dir( + chrome_options, proxy_dir_path + ) else: - # Pytest multi-threaded test - lock = threading.Lock() - with lock: - time.sleep(random.uniform(0.02, 0.15)) - if not os.path.exists(PROXY_ZIP_PATH): - proxy_helper.create_proxy_zip( - proxy_string, proxy_user, proxy_pass) - time.sleep(random.uniform(0.1, 0.2)) - proxy_zip = PROXY_ZIP_PATH - if not os.path.exists(PROXY_ZIP_PATH): - # Handle "Permission denied" on the default proxy.zip path - proxy_zip = PROXY_ZIP_PATH_2 - chrome_options.add_extension(proxy_zip) + # Multi-threaded + if zip_it: + proxy_zip_lock = fasteners.InterProcessLock(PROXY_ZIP_LOCK) + with proxy_zip_lock: + with suppress(Exception): + shared_utils.make_writable(PROXY_ZIP_LOCK) + if multi_proxy: + _set_proxy_filenames() + if not os.path.exists(proxy_helper.PROXY_ZIP_PATH): + proxy_helper.create_proxy_ext( + proxy_string, + proxy_user, + proxy_pass, + proxy_scheme, + bypass_list, + ) + proxy_zip = proxy_helper.PROXY_ZIP_PATH + chrome_options.add_extension(proxy_zip) + else: + proxy_dir_lock = fasteners.InterProcessLock(PROXY_DIR_LOCK) + with proxy_dir_lock: + with suppress(Exception): + shared_utils.make_writable(PROXY_DIR_LOCK) + if multi_proxy: + _set_proxy_filenames() + if not os.path.exists(proxy_helper.PROXY_DIR_PATH): + proxy_helper.create_proxy_ext( + proxy_string, + proxy_user, + proxy_pass, + proxy_scheme, + bypass_list, + zip_it=False, + ) + chrome_options = add_chrome_ext_dir( + chrome_options, proxy_helper.PROXY_DIR_PATH + ) + return chrome_options + + +def is_using_uc(undetectable, browser_name): + if undetectable and browser_name == constants.Browser.GOOGLE_CHROME: + return True + return False + + +def _unzip_to_new_folder(zip_file, folder): + proxy_dir_lock = fasteners.InterProcessLock(PROXY_DIR_LOCK) + with proxy_dir_lock: + with suppress(Exception): + shared_utils.make_writable(PROXY_DIR_LOCK) + if not os.path.exists(folder): + import zipfile + zip_ref = zipfile.ZipFile(zip_file, "r") + os.makedirs(folder) + zip_ref.extractall(folder) + zip_ref.close() + + +def add_chrome_ext_dir(chrome_options, dir_path): + option_exists = False + for arg in chrome_options.arguments: + if arg.startswith("--load-extension="): + option_exists = True + chrome_options.arguments.remove(arg) + chrome_options.add_argument( + "%s,%s" % (arg, os.path.realpath(dir_path)) + ) + if not option_exists: + chrome_options.add_argument( + "--load-extension=%s" % os.path.realpath(dir_path) + ) return chrome_options def _add_chrome_disable_csp_extension(chrome_options): - """ Disable Chrome's Content-Security-Policy with a browser extension. - See https://github.com/PhilGrayson/chrome-csp-disable for details. """ - disable_csp_zip = DISABLE_CSP_ZIP_PATH - chrome_options.add_extension(disable_csp_zip) + """Disable Chrome's Content-Security-Policy with a browser extension. + See https://github.com/PhilGrayson/chrome-csp-disable for details.""" + chrome_options.add_extension(DISABLE_CSP_ZIP_PATH) + return chrome_options + + +def _add_chrome_ad_block_extension(chrome_options): + """Block Ads on Chromium Browsers with a browser extension. + See https://github.com/slingamn/simpleblock for details.""" + chrome_options.add_extension(AD_BLOCK_ZIP_PATH) + return chrome_options + + +def _add_chrome_recorder_extension(chrome_options): + """The SeleniumBase Recorder Chrome/Edge extension. + https://seleniumbase.io/help_docs/recorder_mode/""" + chrome_options.add_extension(RECORDER_ZIP_PATH) return chrome_options def _set_chrome_options( - downloads_path, headless, proxy_string, proxy_auth, - proxy_user, proxy_pass, user_agent, disable_csp): + browser_name, + downloads_path, + headless, + locale_code, + proxy_string, + proxy_auth, + proxy_user, + proxy_pass, + proxy_scheme, + proxy_bypass_list, + proxy_pac_url, + multi_proxy, + user_agent, + recorder_ext, + disable_cookies, + disable_js, + disable_csp, + enable_ws, + enable_sync, + use_auto_ext, + undetectable, + uc_cdp_events, + uc_subprocess, + log_cdp_events, + no_sandbox, + disable_gpu, + headless1, + headless2, + incognito, + guest_mode, + dark_mode, + devtools, + remote_debug, + enable_3d_apis, + swiftshader, + ad_block_on, + host_resolver_rules, + block_images, + do_not_track, + chromium_arg, + user_data_dir, + extension_zip, + extension_dir, + disable_features, + binary_location, + driver_version, + page_load_strategy, + external_pdf, + servername, + mobile_emulator, + device_width, + device_height, + device_pixel_ratio, +): chrome_options = webdriver.ChromeOptions() - prefs = { - "download.default_directory": downloads_path, - "local_discovery.notifications_enabled": False, - "credentials_enable_service": False, - "profile": { - "password_manager_enabled": False + if is_using_uc(undetectable, browser_name): + from seleniumbase import undetected + chrome_options = undetected.ChromeOptions() + elif browser_name == constants.Browser.EDGE: + chrome_options = webdriver.edge.options.Options() + prefs = {} + prefs["download.default_directory"] = downloads_path + prefs["download.directory_upgrade"] = True + prefs["download.prompt_for_download"] = False + prefs["download_bubble.partial_view_enabled"] = False + prefs["credentials_enable_service"] = False + prefs["autofill.credit_card_enabled"] = False + prefs["local_discovery.notifications_enabled"] = False + prefs["safebrowsing.enabled"] = False # Prevent PW "data breach" pop-ups + prefs["safebrowsing.disable_download_protection"] = True + prefs["omnibox-max-zero-suggest-matches"] = 0 + prefs["omnibox-use-existing-autocomplete-client"] = 0 + prefs["omnibox-trending-zero-prefix-suggestions-on-ntp"] = 0 + prefs["omnibox-local-history-zero-suggest-beyond-ntp"] = 0 + prefs["omnibox-on-focus-suggestions-contextual-web"] = 0 + prefs["omnibox-on-focus-suggestions-srp"] = 0 + prefs["omnibox-zero-suggest-prefetching"] = 0 + prefs["omnibox-zero-suggest-prefetching-on-srp"] = 0 + prefs["omnibox-zero-suggest-prefetching-on-web"] = 0 + prefs["omnibox-zero-suggest-in-memory-caching"] = 0 + prefs["content_settings.exceptions.automatic_downloads.*.setting"] = 1 + prefs["default_content_setting_values.notifications"] = 0 + prefs["default_content_settings.popups"] = 0 + prefs["managed_default_content_settings.popups"] = 0 + prefs["profile.password_manager_enabled"] = False + prefs["profile.password_manager_leak_detection"] = False + prefs["profile.default_content_setting_values.notifications"] = 2 + prefs["profile.default_content_settings.popups"] = 0 + prefs["profile.managed_default_content_settings.popups"] = 0 + prefs["profile.default_content_setting_values.automatic_downloads"] = 1 + prefs["NewTabPage.FooterVisible"] = False + if locale_code: + prefs["intl.accept_languages"] = locale_code + sb_config._cdp_locale = locale_code + else: + sb_config._cdp_locale = None + if block_images: + prefs["profile.managed_default_content_settings.images"] = 2 + if disable_cookies: + prefs["profile.default_content_setting_values.cookies"] = 2 + if disable_js: + prefs["profile.managed_default_content_settings.javascript"] = 2 + if do_not_track: + prefs["enable_do_not_track"] = True + if external_pdf: + prefs["plugins.always_open_pdf_externally"] = True + pdf_settings = { + "recentDestinations": [ + {"id": "Save as PDF", "origin": "local", "account": ""} + ], + "selectedDestinationId": "Save as PDF", + "version": 2, } - } + app_state = "printing.print_preview_sticky_settings.appState" + prefs[app_state] = json.dumps(pdf_settings) + # Implementation of https://stackoverflow.com/q/65705775/7058266 + prefs["webrtc.ip_handling_policy"] = "disable_non_proxied_udp" + prefs["webrtc.multiple_routes_enabled"] = False + prefs["webrtc.nonproxied_udp_enabled"] = False chrome_options.add_experimental_option("prefs", prefs) - chrome_options.add_argument("--test-type") - chrome_options.add_argument("--log-level=3") - chrome_options.add_argument("--no-first-run") - chrome_options.add_argument("--ignore-certificate-errors") - chrome_options.add_argument("--allow-file-access-from-files") - chrome_options.add_argument("--allow-insecure-localhost") - chrome_options.add_argument("--allow-running-insecure-content") + if enable_sync: + chrome_options.add_experimental_option( + "excludeSwitches", + ["enable-automation", "enable-logging", "disable-sync"], + ) + chrome_options.add_argument("--enable-sync") + else: + chrome_options.add_experimental_option( + "excludeSwitches", + ["enable-automation", "enable-logging", "enable-blink-features"], + ) + if log_cdp_events: + chrome_options.set_capability( + "goog:loggingPrefs", {"performance": "ALL", "browser": "ALL"} + ) + if host_resolver_rules: + chrome_options.add_argument( + "--host-resolver-rules=%s" % host_resolver_rules + ) + if mobile_emulator and not is_using_uc(undetectable, browser_name): + emulator_settings = {} + device_metrics = {} + if ( + isinstance(device_width, int) + and isinstance(device_height, int) + and isinstance(device_pixel_ratio, (int, float)) + ): + device_metrics["width"] = device_width + device_metrics["height"] = device_height + device_metrics["pixelRatio"] = device_pixel_ratio + else: + device_metrics["width"] = constants.Mobile.WIDTH + device_metrics["height"] = constants.Mobile.HEIGHT + device_metrics["pixelRatio"] = constants.Mobile.RATIO + emulator_settings["deviceMetrics"] = device_metrics + if user_agent: + emulator_settings["userAgent"] = user_agent + chrome_options.add_experimental_option( + "mobileEmulation", emulator_settings + ) + # Handle Window Position + if (headless or headless2) and IS_WINDOWS: + # https://stackoverflow.com/a/78999088/7058266 + chrome_options.add_argument("--window-position=-2400,-2400") + else: + if ( + hasattr(settings, "WINDOW_START_X") + and isinstance(settings.WINDOW_START_X, int) + and hasattr(settings, "WINDOW_START_Y") + and isinstance(settings.WINDOW_START_Y, int) + ): + chrome_options.add_argument( + "--window-position=%s,%s" % ( + settings.WINDOW_START_X, settings.WINDOW_START_Y + ) + ) + # Handle Window Size + if headless or headless2: + if ( + hasattr(settings, "HEADLESS_START_WIDTH") + and isinstance(settings.HEADLESS_START_WIDTH, int) + and hasattr(settings, "HEADLESS_START_HEIGHT") + and isinstance(settings.HEADLESS_START_HEIGHT, int) + ): + chrome_options.add_argument( + "--window-size=%s,%s" % ( + settings.HEADLESS_START_WIDTH, + settings.HEADLESS_START_HEIGHT, + ) + ) + else: + if ( + hasattr(settings, "CHROME_START_WIDTH") + and isinstance(settings.CHROME_START_WIDTH, int) + and hasattr(settings, "CHROME_START_HEIGHT") + and isinstance(settings.CHROME_START_HEIGHT, int) + ): + chrome_options.add_argument( + "--window-size=%s,%s" % ( + settings.CHROME_START_WIDTH, + settings.CHROME_START_HEIGHT, + ) + ) + if ( + not proxy_auth + and not disable_csp + and not ad_block_on + and not recorder_ext + and (not extension_zip and not extension_dir) + ): + if ( + binary_location + and isinstance(binary_location, str) + and ( + binary_location.lower().endswith("comet") + or binary_location.lower().endswith("comet.exe") + ) + ): + # AI browsers don't like Incognito / Guest Mode + incognito = False + guest_mode = False + if incognito: + # Use Chrome's Incognito Mode + # Incognito Mode prevents Chrome extensions from loading, + # so if using extensions or a feature that uses extensions, + # then Chrome's Incognito mode will be disabled instead. + chrome_options.add_argument("--incognito") + elif guest_mode: + # Use Chrome's Guest Mode + # Guest mode prevents Chrome extensions from loading, + # so if using extensions or a feature that uses extensions, + # then Chrome's Guest Mode will be disabled instead. + chrome_options.add_argument("--guest") + else: + pass + if dark_mode: + chrome_options.add_argument("--enable-features=WebContentsForceDark") + if user_data_dir and not is_using_uc(undetectable, browser_name): + abs_path = os.path.abspath(user_data_dir) + chrome_options.add_argument("--user-data-dir=%s" % abs_path) + if extension_zip: + # Can be a comma-separated list of .ZIP or .CRX files + extension_zip_list = extension_zip.split(",") + for extension_zip_item in extension_zip_list: + abs_path = os.path.realpath(extension_zip_item) + if os.path.exists(abs_path): + try: + abs_path_dir = os.path.join( + DOWNLOADS_FOLDER, abs_path.split(".")[0] + ) + _unzip_to_new_folder(abs_path, abs_path_dir) + chrome_options = add_chrome_ext_dir( + chrome_options, abs_path_dir + ) + sb_config._ext_dirs.append(abs_path_dir) + except Exception: + with suppress(Exception): + chrome_options.add_extension(abs_path) + if extension_dir: + # load-extension input can be a comma-separated list + abs_path = ( + ",".join(os.path.realpath(p) for p in extension_dir.split(",")) + ) + chrome_options = add_chrome_ext_dir(chrome_options, abs_path) + for p in extension_dir.split(","): + sb_config._ext_dirs.append(os.path.realpath(p)) + if ( + page_load_strategy + and page_load_strategy.lower() in ["eager", "none"] + ): + # Only change it if not "normal", which is the default. + chrome_options.page_load_strategy = page_load_strategy.lower() + elif ( + not page_load_strategy + and getattr(settings, "PAGE_LOAD_STRATEGY", None) + and settings.PAGE_LOAD_STRATEGY.lower() in ["eager", "none"] + ): + # Only change it if not "normal", which is the default. + chrome_options.page_load_strategy = settings.PAGE_LOAD_STRATEGY.lower() + if headless2: + if servername and servername != "localhost": + # The Grid Node will need Chrome 109 or newer + chrome_options.add_argument("--headless=new") + else: + pass # Processed After Version Check + elif headless: + if not undetectable: + if headless1: + chrome_options.add_argument("--headless=old") + else: + chrome_options.add_argument("--headless") + if undetectable and servername and servername != "localhost": + # The Grid Node will need Chrome 109 or newer + chrome_options.add_argument("--headless=new") + else: + pass # Processed After Version Check + if (settings.DISABLE_CSP_ON_CHROME or disable_csp) and not headless: + # Headless Chrome does not support extensions, which are required + # for disabling the Content Security Policy on Chrome. + disable_csp_zip = DISABLE_CSP_ZIP_PATH + disable_csp_dir = os.path.join(DOWNLOADS_FOLDER, "disable_csp") + _unzip_to_new_folder(disable_csp_zip, disable_csp_dir) + chrome_options = add_chrome_ext_dir( + chrome_options, disable_csp_dir + ) + sb_config._ext_dirs.append(disable_csp_dir) + if ad_block_on and not headless: + # Headless Chrome does not support extensions. + ad_block_zip = AD_BLOCK_ZIP_PATH + ad_block_dir = os.path.join(DOWNLOADS_FOLDER, "ad_block") + _unzip_to_new_folder(ad_block_zip, ad_block_dir) + chrome_options = add_chrome_ext_dir(chrome_options, ad_block_dir) + sb_config._ext_dirs.append(ad_block_dir) + if recorder_ext and not headless: + recorder_zip = RECORDER_ZIP_PATH + recorder_dir = os.path.join(DOWNLOADS_FOLDER, "recorder") + _unzip_to_new_folder(recorder_zip, recorder_dir) + chrome_options = add_chrome_ext_dir(chrome_options, recorder_dir) + sb_config._ext_dirs.append(recorder_dir) + if chromium_arg and "sbase" in chromium_arg: + sbase_ext_zip = SBASE_EXT_ZIP_PATH + sbase_ext_dir = os.path.join(DOWNLOADS_FOLDER, "sbase_ext") + _unzip_to_new_folder(sbase_ext_zip, sbase_ext_dir) + chrome_options = add_chrome_ext_dir(chrome_options, sbase_ext_dir) + sb_config._ext_dirs.append(sbase_ext_dir) + if proxy_string: + if proxy_auth: + zip_it = True + if is_using_uc(undetectable, browser_name): + zip_it = False # undetected-chromedriver needs a folder ext + chrome_options = _add_chrome_proxy_extension( + chrome_options, + proxy_string, + proxy_user, + proxy_pass, + proxy_scheme, + proxy_bypass_list, + zip_it, + multi_proxy, + ) + chrome_options.add_argument("--proxy-server=%s" % proxy_string) + if proxy_bypass_list: + chrome_options.add_argument( + "--proxy-bypass-list=%s" % proxy_bypass_list + ) + elif proxy_pac_url: + if proxy_auth: + zip_it = True # undetected-chromedriver needs a folder ext + if is_using_uc(undetectable, browser_name): + zip_it = False + chrome_options = _add_chrome_proxy_extension( + chrome_options, + None, + proxy_user, + proxy_pass, + proxy_scheme, + proxy_bypass_list, + zip_it, + multi_proxy, + ) + chrome_options.add_argument("--proxy-pac-url=%s" % proxy_pac_url) + if ( + not is_using_uc(undetectable, browser_name) + or not enable_ws + or proxy_string + ): + chrome_options.add_argument("--ignore-certificate-errors") + chrome_options.add_argument("--ignore-ssl-errors=yes") + if not enable_ws: + chrome_options.add_argument("--disable-web-security") + if ( + IS_LINUX + or (IS_MAC and not is_using_uc(undetectable, browser_name)) + ): + chrome_options.add_argument("--no-sandbox") + if remote_debug: + # To access the Debugger, go to: chrome://inspect/#devices + # while a Chromium driver is running. + # Info: https://chromedevtools.github.io/devtools-protocol/ + args = " ".join(sys.argv) + debug_port = 9222 + if ("-n" in sys.argv or " -n=" in args or args == "-c"): + debug_port = common_utils.free_port() + chrome_options.add_argument("--remote-debugging-port=%s" % debug_port) + if swiftshader: + chrome_options.add_argument("--use-gl=angle") + chrome_options.add_argument("--use-angle=swiftshader-webgl") + elif ( + not is_using_uc(undetectable, browser_name) + and not enable_3d_apis + ): + chrome_options.add_argument("--disable-gpu") + if ( + (not IS_LINUX and is_using_uc(undetectable, browser_name)) + or ( + IS_MAC + and binary_location + and "chrome-headless-shell" in binary_location + ) + ): + chrome_options.add_argument("--disable-dev-shm-usage") + chrome_options.add_argument("--disable-application-cache") + if IS_LINUX: + chrome_options.add_argument("--disable-dev-shm-usage") + if is_using_uc(undetectable, browser_name): + chrome_options.add_argument("--disable-application-cache") + chrome_options.add_argument("--disable-setuid-sandbox") + if not binary_location: + if os.path.exists("/bin/google-chrome"): + binary_location = "/bin/google-chrome" + elif os.path.exists("/usr/bin/google-chrome-stable"): + binary_location = "/usr/bin/google-chrome-stable" + elif os.path.exists("/usr/bin/google-chrome"): + binary_location = "/usr/bin/google-chrome" + elif os.path.exists("/usr/bin/google-chrome-stable"): + binary_location = "/usr/bin/google-chrome-stable" + else: + br_app = "google-chrome" + binary_loc = detect_b_ver.get_binary_location(br_app, True) + if os.path.exists(binary_loc): + binary_location = binary_loc + extra_disabled_features = [] + if chromium_arg: + # Can be a comma-separated list of Chromium args or a list + chromium_arg_list = None + if isinstance(chromium_arg, (list, tuple)): + chromium_arg_list = chromium_arg + else: + chromium_arg_list = chromium_arg.split(",") + for chromium_arg_item in chromium_arg_list: + chromium_arg_item = chromium_arg_item.strip() + if not chromium_arg_item.startswith("--"): + if chromium_arg_item.startswith("-"): + chromium_arg_item = "-" + chromium_arg_item + else: + chromium_arg_item = "--" + chromium_arg_item + if "remote-debugging-port=" in chromium_arg_item: + with suppress(Exception): + # Extra processing for UC Mode + chrome_options._remote_debugging_port = int( + chromium_arg_item.split("remote-debugging-port=")[1] + ) + if "set-binary" in chromium_arg_item and not binary_location: + br_app = "google-chrome" + binary_loc = detect_b_ver.get_binary_location( + br_app, is_using_uc(undetectable, browser_name) + ) + if os.path.exists(binary_loc): + binary_location = binary_loc + elif "disable-features=" in chromium_arg_item: + d_f = chromium_arg_item.split("disable-features=")[-1] + extra_disabled_features.append(d_f) + elif len(chromium_arg_item) >= 3: + chrome_options.add_argument(chromium_arg_item) + if disable_features: + extra_disabled_features.extend(disable_features.split(",")) + if devtools and not headless: + chrome_options.add_argument("--auto-open-devtools-for-tabs") if user_agent: chrome_options.add_argument("--user-agent=%s" % user_agent) - chrome_options.add_argument("--disable-infobars") + chrome_options.add_argument("--safebrowsing-disable-download-protection") + chrome_options.add_argument("--disable-search-engine-choice-screen") + chrome_options.add_argument("--disable-browser-side-navigation") chrome_options.add_argument("--disable-save-password-bubble") chrome_options.add_argument("--disable-single-click-autofill") + chrome_options.add_argument("--allow-file-access-from-files") + chrome_options.add_argument("--disable-prompt-on-repost") + chrome_options.add_argument("--dns-prefetch-disable") chrome_options.add_argument("--disable-translate") - chrome_options.add_argument("--disable-web-security") - if (settings.DISABLE_CSP_ON_CHROME or disable_csp) and not headless: - # Headless Chrome doesn't support extensions, which are required - # for disabling the Content Security Policy on Chrome - chrome_options = _add_chrome_disable_csp_extension(chrome_options) - if proxy_string: - if proxy_auth: - chrome_options = _add_chrome_proxy_extension( - chrome_options, proxy_string, proxy_user, proxy_pass) - chrome_options.add_argument('--proxy-server=%s' % proxy_string) + if binary_location: + chrome_options.binary_location = binary_location + if not enable_3d_apis and not is_using_uc(undetectable, browser_name): + chrome_options.add_argument("--disable-3d-apis") + if headless or headless2 or is_using_uc(undetectable, browser_name): + chrome_options.add_argument("--disable-renderer-backgrounding") + chrome_options.add_argument("--disable-background-networking") + chrome_options.add_argument("--disable-backgrounding-occluded-windows") + chrome_options.add_argument("--disable-client-side-phishing-detection") + chrome_options.add_argument("--disable-device-discovery-notifications") + chrome_options.add_argument("--disable-oopr-debug-crash-dump") + chrome_options.add_argument("--disable-top-sites") + chrome_options.add_argument("--ash-no-nudges") + chrome_options.add_argument("--no-crash-upload") + chrome_options.add_argument("--deny-permission-prompts") + chrome_options.add_argument( + '--simulate-outdated-no-au="Tue, 31 Dec 2099 23:59:59 GMT"' + ) + chrome_options.add_argument("--disable-ipc-flooding-protection") + chrome_options.add_argument("--disable-password-generation") + chrome_options.add_argument("--disable-domain-reliability") + chrome_options.add_argument("--disable-breakpad") + included_disabled_features = [] + included_disabled_features.append("OptimizationHintsFetching") + included_disabled_features.append("Translate") + included_disabled_features.append("ComponentUpdater") + included_disabled_features.append("OmniboxUIFeedback") + included_disabled_features.append("OmniboxPopupShortcut") + included_disabled_features.append("OptimizationTargetPrediction") + included_disabled_features.append("OptimizationGuideModelDownloading") + included_disabled_features.append("DownloadBubble") + included_disabled_features.append("DownloadBubbleV2") + included_disabled_features.append("InsecureDownloadWarnings") + included_disabled_features.append("InterestFeedContentSuggestions") + included_disabled_features.append("PrivacySandboxSettings4") + included_disabled_features.append("SidePanelPinning") + included_disabled_features.append("UserAgentClientHint") + included_disabled_features.append("DisableLoadExtensionCommandLineSwitch") + included_disabled_features.append("NetworkPrediction") + included_disabled_features.append("WebAuthentication") + included_disabled_features.append("PasskeyAuth") + included_disabled_features.append("MediaRouter") + included_disabled_features.append("DialMediaRouteProvider") + included_disabled_features.append("WebRtcHideLocalIpsWithMdns") + if is_using_uc(undetectable, browser_name): + included_disabled_features.append("IsolateOrigins") + included_disabled_features.append("site-per-process") + for item in extra_disabled_features: + if item not in included_disabled_features: + included_disabled_features.append(item) + d_f_string = ",".join(included_disabled_features) + chrome_options.add_argument("--disable-features=%s" % d_f_string) + chrome_options.add_argument("--enable-unsafe-extension-debugging") + if ( + hasattr(sb_config, "_cdp_browser") + and sb_config._cdp_browser == "brave" + ): + pass # Looks like "--test-type" breaks Brave now + else: + chrome_options.add_argument("--test-type") + if proxy_auth or sb_config._ext_dirs: + if not is_using_uc(undetectable, browser_name): + chrome_options.add_argument("--remote-debugging-pipe") + chrome_options.enable_webextensions = True + chrome_options.enable_bidi = True + if ( + is_using_uc(undetectable, browser_name) + and ( + not headless + or IS_LINUX # switches to Xvfb (non-headless) + ) + ): + chrome_options.add_argument("--no-pings") + chrome_options.add_argument("--homepage=chrome://version/") + chrome_options.add_argument("--animation-duration-scale=0") + chrome_options.add_argument("--wm-window-animations-disabled") + chrome_options.add_argument("--enable-privacy-sandbox-ads-apis") + chrome_options.add_argument("--disable-auto-reload") + chrome_options.add_argument("--disable-background-timer-throttling") + # Prevent new tabs opened by Selenium from being blocked: + chrome_options.add_argument("--disable-popup-blocking") + # Skip remaining options that trigger anti-bot services + return chrome_options + chrome_options.add_argument("--log-level=3") + chrome_options.add_argument("--no-first-run") + chrome_options.add_argument("--allow-insecure-localhost") + chrome_options.add_argument("--allow-running-insecure-content") + chrome_options.add_argument("--disable-infobars") + chrome_options.add_argument("--disable-notifications") + chrome_options.add_argument( + "--disable-autofill-keyboard-accessory-view[8]" + ) + chrome_options.add_argument("--homepage=about:blank") + chrome_options.add_argument("--dom-automation") + chrome_options.add_argument("--disable-hang-monitor") return chrome_options -def _create_firefox_profile( - downloads_path, proxy_string, user_agent, disable_csp): - profile = webdriver.FirefoxProfile() - profile.accept_untrusted_certs = True - profile.set_preference("reader.parse-on-load.enabled", False) - profile.set_preference("pdfjs.disabled", True) - profile.set_preference("app.update.auto", False) - profile.set_preference("app.update.enabled", False) - profile.set_preference("extensions.update.enabled", False) - profile.set_preference("devtools.errorconsole.enabled", True) - profile.set_preference( - "datareporting.healthreport.logging.consoleEnabled", False) - profile.set_preference("datareporting.healthreport.service.enabled", False) - profile.set_preference( - "datareporting.healthreport.service.firstRun", False) - profile.set_preference("datareporting.healthreport.uploadEnabled", False) - profile.set_preference("datareporting.policy.dataSubmissionEnabled", False) - profile.set_preference( - "datareporting.policy.dataSubmissionPolicyAccepted", False) +def _set_firefox_options( + downloads_path, + headless, + locale_code, + proxy_string, + proxy_bypass_list, + proxy_pac_url, + user_agent, + disable_cookies, + disable_js, + disable_csp, + firefox_arg, + firefox_pref, + external_pdf, +): + blank_p = "about:blank" + options = webdriver.FirefoxOptions() + options.accept_untrusted_certs = True + options.set_preference("reader.parse-on-load.enabled", False) + options.set_preference("browser.startup.homepage", blank_p) + options.set_preference("startup.homepage_welcome_url", blank_p) + options.set_preference("startup.homepage_welcome_url.additional", blank_p) + options.set_preference("browser.newtab.url", blank_p) + options.set_preference("trailhead.firstrun.branches", "nofirstrun-empty") + options.set_preference("browser.aboutwelcome.enabled", False) + options.set_preference("app.update.auto", False) + options.set_preference("app.update.enabled", False) + options.set_preference("browser.formfill.enable", False) + options.set_preference("browser.privatebrowsing.autostart", True) + options.set_preference("dom.webnotifications.enabled", False) + options.set_preference("dom.disable_beforeunload", True) + options.set_preference("browser.contentblocking.database.enabled", True) + options.set_preference("extensions.systemAddon.update.enabled", False) + options.set_preference("extensions.update.autoUpdateDefault", False) + options.set_preference("extensions.update.enabled", False) + options.set_preference("datareporting.healthreport.service.enabled", False) + options.set_preference("datareporting.healthreport.uploadEnabled", False) + options.set_preference("datareporting.policy.dataSubmissionEnabled", False) + options.set_preference("browser.search.update", False) + options.set_preference("privacy.trackingprotection.enabled", False) + options.set_preference("toolkit.telemetry.enabled", False) + options.set_preference("toolkit.telemetry.unified", False) + options.set_preference("toolkit.telemetry.archive.enabled", False) if proxy_string: - proxy_server = proxy_string.split(':')[0] - proxy_port = proxy_string.split(':')[1] - profile.set_preference("network.proxy.type", 1) - profile.set_preference("network.proxy.http", proxy_server) - profile.set_preference("network.proxy.http_port", int(proxy_port)) - profile.set_preference("network.proxy.ssl", proxy_server) - profile.set_preference("network.proxy.ssl_port", int(proxy_port)) + socks_proxy = False + socks_ver = 0 + chunks = proxy_string.split(":") + if len(chunks) == 3 and ( + chunks[0] == "socks4" + or chunks[0] == "socks5" + or chunks[0] == "socks5h" + ): + socks_proxy = True + socks_ver = int(chunks[0][5]) + if chunks[1].startswith("//") and len(chunks[1]) > 2: + chunks[1] = chunks[1][2:] + proxy_server = chunks[1] + proxy_port = chunks[2] + else: + proxy_server = proxy_string.split(":")[0] + proxy_port = proxy_string.split(":")[1] + options.set_preference("network.proxy.type", 1) + if socks_proxy: + options.set_preference("network.proxy.socks", proxy_server) + options.set_preference("network.proxy.socks_port", int(proxy_port)) + options.set_preference("network.proxy.socks_version", socks_ver) + else: + options.set_preference("network.proxy.http", proxy_server) + options.set_preference("network.proxy.http_port", int(proxy_port)) + options.set_preference("network.proxy.ssl", proxy_server) + options.set_preference("network.proxy.ssl_port", int(proxy_port)) + if proxy_bypass_list: + options.set_preference("no_proxies_on", proxy_bypass_list) + elif proxy_pac_url: + options.set_preference("network.proxy.type", 2) + options.set_preference("network.proxy.autoconfig_url", proxy_pac_url) if user_agent: - profile.set_preference("general.useragent.override", user_agent) - profile.set_preference( - "security.mixed_content.block_active_content", False) + options.set_preference("general.useragent.override", user_agent) + options.set_preference( + "security.mixed_content.block_active_content", False + ) + options.set_preference("security.warn_submit_insecure", False) + if disable_cookies: + options.set_preference("network.cookie.cookieBehavior", 2) + if disable_js: + options.set_preference("javascript.enabled", False) if settings.DISABLE_CSP_ON_FIREFOX or disable_csp: - profile.set_preference("security.csp.enable", False) - profile.set_preference( - "browser.download.manager.showAlertOnComplete", False) - profile.set_preference("browser.shell.checkDefaultBrowser", False) - profile.set_preference("browser.startup.page", 0) - profile.set_preference("browser.privatebrowsing.autostart", True) - profile.set_preference("browser.download.panel.shown", False) - profile.set_preference( - "browser.download.animateNotifications", False) - profile.set_preference("browser.download.dir", downloads_path) - profile.set_preference("browser.download.folderList", 2) - profile.set_preference( + options.set_preference("security.csp.enable", False) + options.set_preference( + "browser.download.manager.showAlertOnComplete", False + ) + if headless and not IS_LINUX: + options.add_argument("--headless") + elif headless and IS_LINUX: + # This assumes Xvfb is running, which prevents many Linux issues. + # If not, we'll fix this later during the error-handling process. + # To override this feature: ``pytest --firefox-arg="-headless"``. + pass + if locale_code: + options.set_preference("intl.accept_languages", locale_code) + options.set_preference("browser.shell.checkDefaultBrowser", False) + options.set_preference("browser.startup.page", 0) + options.set_preference("browser.download.panel.shown", False) + options.set_preference("browser.download.animateNotifications", False) + options.set_preference("browser.download.dir", downloads_path) + options.set_preference("browser.download.folderList", 2) + options.set_preference("browser.helperApps.alwaysAsk.force", False) + options.set_preference("browser.download.manager.showWhenStarting", False) + options.set_preference( "browser.helperApps.neverAsk.saveToDisk", - ("application/pdf, application/zip, application/octet-stream, " - "text/csv, text/xml, application/xml, text/plain, " - "text/octet-stream, " - "application/" - "vnd.openxmlformats-officedocument.spreadsheetml.sheet")) - return profile - - -def display_proxy_warning(proxy_string): - message = ('\n\nWARNING: Proxy String ["%s"] is NOT in the expected ' - '"ip_address:port" or "server:port" format, ' - '(OR the key does not exist in ' - 'seleniumbase.config.proxy_list.PROXY_LIST).' - % proxy_string) - if settings.RAISE_INVALID_PROXY_STRING_EXCEPTION: - raise Exception(message) - else: - message += ' *** DEFAULTING to NOT USING a Proxy Server! ***' - warnings.simplefilter('always', Warning) # See Warnings - warnings.warn(message, category=Warning, stacklevel=2) - warnings.simplefilter('default', Warning) # Set Default - - -def validate_proxy_string(proxy_string): - if proxy_string in proxy_list.PROXY_LIST.keys(): - proxy_string = proxy_list.PROXY_LIST[proxy_string] - if not proxy_string: - return None - valid = False - val_ip = re.match( - r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+$', proxy_string) - if not val_ip: - if proxy_string.startswith('http://'): - proxy_string = proxy_string.split('http://')[1] - elif proxy_string.startswith('https://'): - proxy_string = proxy_string.split('https://')[1] - elif '://' in proxy_string: - proxy_string = proxy_string.split('://')[1] - chunks = proxy_string.split(':') - if len(chunks) == 2: - if re.match(r'^\d+$', chunks[1]): - if page_utils.is_valid_url('http://' + proxy_string): - valid = True + ( + "application/pdf,application/zip,application/octet-stream," + "text/csv,text/xml,application/xml,text/plain,application/json," + "text/octet-stream,application/x-gzip,application/x-tar," + "application/java-archive,text/x-java-source,java," + "application/javascript,video/jpeg,audio/x-aac,image/svg+xml," + "application/x-font-woff,application/x-7z-compressed," + "application/mp4,video/mp4,audio/mp4,video/x-msvideo," + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ), + ) + if external_pdf: + options.set_preference("pdfjs.disabled", True) else: - proxy_string = val_ip.group() - valid = True - if not valid: - display_proxy_warning(proxy_string) - proxy_string = None - return proxy_string + options.set_preference("pdfjs.disabled", False) + if firefox_arg: + # Can be a comma-separated list of Firefox args + firefox_arg_list = firefox_arg.split(",") + for firefox_arg_item in firefox_arg_list: + firefox_arg_item = firefox_arg_item.strip() + if not firefox_arg_item.startswith("-"): + if firefox_arg_item.count(os.sep) == 0: + firefox_arg_item = "--" + firefox_arg_item + if len(firefox_arg_item) >= 3: + options.add_argument(firefox_arg_item) + if firefox_pref: + # Can be a comma-separated list of Firefox preference:value pairs + firefox_pref_list = firefox_pref.split(",") + for firefox_pref_item in firefox_pref_list: + f_pref = None + f_pref_value = None + needs_conversion = False + if firefox_pref_item.count(":") == 0: + f_pref = firefox_pref_item + f_pref_value = True + elif firefox_pref_item.count(":") == 1: + f_pref = firefox_pref_item.split(":")[0] + f_pref_value = firefox_pref_item.split(":")[1] + needs_conversion = True + elif firefox_pref_item.count("://") == 1: + f_pref = firefox_pref_item.split(":")[0] + f_pref_value = ":".join(firefox_pref_item.split(":")[1:]) + else: # More than one ":" in the set without a URL. + raise Exception( + 'Incorrect formatting for Firefox "pref:value" set!' + ) + if needs_conversion: + if f_pref_value.lower() == "true" or len(f_pref_value) == 0: + f_pref_value = True + elif f_pref_value.lower() == "false": + f_pref_value = False + elif f_pref_value.isdigit(): + f_pref_value = int(f_pref_value) + elif f_pref_value.replace(".", "", 1).isdigit(): + f_pref_value = float(f_pref_value) + else: + pass # keep as string + if len(f_pref) >= 1: + options.set_preference(f_pref, f_pref_value) + return options -def get_driver(browser_name, headless=False, use_grid=False, - servername='localhost', port=4444, proxy_string=None, - user_agent=None, cap_file=None, disable_csp=None): +def get_driver( + browser_name=None, + headless=False, + locale_code=None, + use_grid=False, + protocol="http", + servername="localhost", + port=4444, + proxy_string=None, + proxy_bypass_list=None, + proxy_pac_url=None, + multi_proxy=None, + user_agent=None, + cap_file=None, + cap_string=None, + recorder_ext=False, + disable_cookies=False, + disable_js=False, + disable_csp=False, + enable_ws=False, + enable_sync=False, + use_auto_ext=False, + undetectable=False, + uc_cdp_events=False, + uc_subprocess=False, + log_cdp_events=False, + no_sandbox=False, + disable_gpu=False, + headless1=False, + headless2=False, + incognito=False, + guest_mode=False, + dark_mode=False, + devtools=False, + remote_debug=False, + enable_3d_apis=False, + swiftshader=False, + ad_block_on=False, + host_resolver_rules=None, + block_images=False, + do_not_track=False, + chromium_arg=None, + firefox_arg=None, + firefox_pref=None, + user_data_dir=None, + extension_zip=None, + extension_dir=None, + disable_features=None, + binary_location=None, + driver_version=None, + page_load_strategy=None, + external_pdf=False, + test_id=None, + mobile_emulator=False, + device_width=None, + device_height=None, + device_pixel_ratio=None, + browser=None, # A duplicate of browser_name to avoid confusion +): + sb_config._ext_dirs = [] + sb_config._cdp_launched = False + driver_dir = DRIVER_DIR + if binary_location == "_chromium_": + driver_dir = DRIVER_DIR_CHROMIUM + elif binary_location == "cft": + driver_dir = DRIVER_DIR_CFT + elif binary_location == "chs": + driver_dir = DRIVER_DIR_CHS + elif _special_binary_exists(binary_location, "opera"): + driver_dir = DRIVER_DIR_OPERA + sb_config._cdp_browser = "opera" + elif _special_binary_exists(binary_location, "brave"): + driver_dir = DRIVER_DIR_BRAVE + sb_config._cdp_browser = "brave" + elif _special_binary_exists(binary_location, "comet"): + driver_dir = DRIVER_DIR_COMET + sb_config._cdp_browser = "comet" + if ( + hasattr(sb_config, "settings") + and getattr(sb_config.settings, "NEW_DRIVER_DIR", None) + and os.path.exists(sb_config.settings.NEW_DRIVER_DIR) + ): + driver_dir = sb_config.settings.NEW_DRIVER_DIR + if not browser_name: + if browser: + browser_name = browser + else: + browser_name = "chrome" # The default if not specified + if browser_name in constants.ChromiumSubs.chromium_subs: + browser_name = "chrome" + browser_name = browser_name.lower() + if ( + hasattr(sb_config, "_cdp_browser") + and sb_config._cdp_browser == "brave" + ): + undetectable = True + if is_using_uc(undetectable, browser_name): + if ad_block_on: + sb_config.ad_block_on = True + else: + sb_config.ad_block_on = False + if disable_csp: + sb_config.disable_csp = True + else: + sb_config.disable_csp = False + if mobile_emulator: + # For stealthy mobile mode, see the CDP Mode examples + # to learn how to properly configure it. + user_agent = None # Undo the override + mobile_emulator = False # Instead, set from CDP Mode + sb_config._cdp_mobile_mode = True + else: + sb_config._cdp_mobile_mode = False + if headless2 and browser_name == constants.Browser.FIREFOX: + headless2 = False # Only for Chromium + headless = True + if binary_location and isinstance(binary_location, str): + binary_location = binary_location.strip() + if ( + is_using_uc(undetectable, browser_name) + and binary_location + and isinstance(binary_location, str) + and binary_location.lower() == "chs" + ): + raise Exception("UC Mode can't be used with Chrome-Headless-Shell!") + if ( + binary_location + and isinstance(binary_location, str) + and ( + browser_name == constants.Browser.GOOGLE_CHROME + or browser_name == constants.Browser.EDGE + ) + ): + if ( + binary_location.lower() == "_chromium_" + and browser_name == constants.Browser.GOOGLE_CHROME + ): + binary_folder = None + if IS_MAC: + binary_folder = "chrome-mac" + elif IS_LINUX: + binary_folder = "chrome-linux" + elif IS_WINDOWS: + binary_folder = "chrome-win" + if binary_folder: + binary_location = os.path.join(driver_dir, binary_folder) + if not os.path.exists(binary_location): + from seleniumbase.console_scripts import sb_install + args = " ".join(sys.argv) + if not ( + "-n" in sys.argv or " -n=" in args or args == "-c" + ): + # (Not multithreaded) + sys_args = sys.argv # Save a copy of current sys args + log_d("\nWarning: Chromium binary not found...") + try: + sb_install.main(override="chromium") + except Exception as e: + log_d("\nWarning: Chrome download failed: %s" % e) + sys.argv = sys_args # Put back the original sys args + else: + chrome_fixing_lock = fasteners.InterProcessLock( + constants.MultiBrowser.DRIVER_FIXING_LOCK + ) + with chrome_fixing_lock: + with suppress(Exception): + shared_utils.make_writable( + constants.MultiBrowser.DRIVER_FIXING_LOCK + ) + if not os.path.exists(binary_location): + sys_args = sys.argv # Save a copy of sys args + log_d( + "\nWarning: Chromium binary not found..." + ) + sb_install.main(override="chromium") + sys.argv = sys_args # Put back original args + else: + binary_location = None + if ( + binary_location.lower() == "cft" + and browser_name == constants.Browser.GOOGLE_CHROME + ): + binary_folder = None + if IS_MAC: + if IS_ARM_MAC: + binary_folder = "chrome-mac-arm64" + else: + binary_folder = "chrome-mac-x64" + elif IS_LINUX: + binary_folder = "chrome-linux64" + elif IS_WINDOWS: + if "64" in ARCH: + binary_folder = "chrome-win64" + else: + binary_folder = "chrome-win32" + if binary_folder: + binary_location = os.path.join(driver_dir, binary_folder) + if not os.path.exists(binary_location): + from seleniumbase.console_scripts import sb_install + args = " ".join(sys.argv) + if not ( + "-n" in sys.argv or " -n=" in args or args == "-c" + ): + # (Not multithreaded) + sys_args = sys.argv # Save a copy of current sys args + log_d( + "\nWarning: Chrome for Testing binary not found..." + ) + try: + sb_install.main(override="cft") + except Exception as e: + log_d("\nWarning: Chrome download failed: %s" % e) + sys.argv = sys_args # Put back the original sys args + else: + chrome_fixing_lock = fasteners.InterProcessLock( + constants.MultiBrowser.DRIVER_FIXING_LOCK + ) + with chrome_fixing_lock: + with suppress(Exception): + shared_utils.make_writable( + constants.MultiBrowser.DRIVER_FIXING_LOCK + ) + if not os.path.exists(binary_location): + sys_args = sys.argv # Save a copy of sys args + log_d( + "\nWarning: " + "Chrome for Testing binary not found..." + ) + sb_install.main(override="cft") + sys.argv = sys_args # Put back original args + else: + binary_location = None + if ( + binary_location.lower() == "chs" + and browser_name == constants.Browser.GOOGLE_CHROME + ): + binary_folder = None + if IS_MAC: + if IS_ARM_MAC: + binary_folder = "chrome-headless-shell-mac-arm64" + else: + binary_folder = "chrome-headless-shell-mac-x64" + elif IS_LINUX: + binary_folder = "chrome-headless-shell-linux64" + elif IS_WINDOWS: + if "64" in ARCH: + binary_folder = "chrome-headless-shell-win64" + else: + binary_folder = "chrome-headless-shell-win32" + if binary_folder: + binary_location = os.path.join(driver_dir, binary_folder) + if not os.path.exists(binary_location): + from seleniumbase.console_scripts import sb_install + args = " ".join(sys.argv) + if not ( + "-n" in sys.argv or " -n=" in args or args == "-c" + ): + # (Not multithreaded) + sys_args = sys.argv # Save a copy of current sys args + log_d( + "\nWarning: " + "Chrome-Headless-Shell binary not found..." + ) + try: + sb_install.main(override="chs") + except Exception as e: + log_d( + "\nWarning: " + "Chrome-Headless-Shell download failed: %s" % e + ) + sys.argv = sys_args # Put back the original sys args + else: + chrome_fixing_lock = fasteners.InterProcessLock( + constants.MultiBrowser.DRIVER_FIXING_LOCK + ) + with chrome_fixing_lock: + with suppress(Exception): + shared_utils.make_writable( + constants.MultiBrowser.DRIVER_FIXING_LOCK + ) + if not os.path.exists(binary_location): + sys_args = sys.argv # Save a copy of sys args + log_d( + "\nWarning: " + "Chrome-Headless-Shell binary not found..." + ) + sb_install.main(override="chs") + sys.argv = sys_args # Put back original args + else: + binary_location = None + if not os.path.exists(binary_location): + log_d( + "\nWarning: The Chromium binary specified (%s) was NOT found!" + "\n(Will use default settings...)\n" % binary_location + ) + binary_location = None + elif binary_location.endswith("/") or binary_location.endswith("\\"): + log_d( + "\nWarning: The Chromium binary path must be a full path " + "that includes the browser filename at the end of it!" + "\n(Will use default settings...)\n" % binary_location + ) + # Example of a valid binary location path - MacOS: + # "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" + binary_location = None + else: + binary_name = binary_location.split("/")[-1].split("\\")[-1] + valid_names = get_valid_binary_names_for_browser(browser_name) + if binary_name == "Google Chrome for Testing.app": + binary_name = "Google Chrome for Testing" + binary_location += "/Contents/MacOS/Google Chrome for Testing" + elif binary_name in ["chrome-mac-arm64", "chrome-mac-x64"]: + binary_name = "Google Chrome for Testing" + binary_location += "/Google Chrome for Testing.app" + binary_location += "/Contents/MacOS/Google Chrome for Testing" + elif binary_name == "Chromium.app": + binary_name = "Chromium" + binary_location += "/Contents/MacOS/Chromium" + elif binary_name in ["chrome-mac"]: + binary_name = "Chromium" + binary_location += "/Chromium.app" + binary_location += "/Contents/MacOS/Chromium" + elif binary_name == "chrome-linux64": + binary_name = "chrome" + binary_location += "/chrome" + elif binary_name == "chrome-linux": + binary_name = "chrome" + binary_location += "/chrome" + elif binary_name in ["chrome-win32", "chrome-win64"]: + binary_name = "chrome.exe" + binary_location += "\\chrome.exe" + elif binary_name in ["chrome-win"]: + binary_name = "chrome.exe" + binary_location += "\\chrome.exe" + elif binary_name in [ + "chrome-headless-shell-mac-arm64", + "chrome-headless-shell-mac-x64", + ]: + binary_name = "chrome-headless-shell" + binary_location += "/chrome-headless-shell" + elif binary_name == "chrome-headless-shell-linux64": + binary_name = "chrome-headless-shell" + binary_location += "/chrome-headless-shell" + elif binary_name in [ + "chrome-headless-shell-win32", + "chrome-headless-shell-win64", + ]: + binary_name = "chrome-headless-shell.exe" + binary_location += "\\chrome-headless-shell.exe" + if binary_name not in valid_names: + log_d( + "\nWarning: The Chromium binary specified is NOT valid!" + '\nExpecting "%s" to be found in %s for the browser / OS!' + "\n(Will use default settings...)\n" + "" % (binary_name, valid_names) + ) + binary_location = None + elif binary_location.lower() == "chs": + headless = True + if (uc_cdp_events or uc_subprocess) and not undetectable: + undetectable = True + if mobile_emulator and not user_agent: + # Use a Pixel user agent by default if not specified + user_agent = constants.Mobile.AGENT + if page_load_strategy and page_load_strategy.lower() == "none": + settings.PAGE_LOAD_STRATEGY = "none" proxy_auth = False proxy_user = None proxy_pass = None + proxy_scheme = "http" if proxy_string: + # (The line below is for the Chrome 142 proxy auth fix) + sb_config._cdp_proxy = proxy_string username_and_password = None if "@" in proxy_string: # Format => username:password@hostname:port try: - username_and_password = proxy_string.split('@')[0] - proxy_string = proxy_string.split('@')[1] - proxy_user = username_and_password.split(':')[0] - proxy_pass = username_and_password.split(':')[1] + username_and_password = proxy_string.split("@")[0] + proxy_string = proxy_string.split("@")[1] + proxy_user = username_and_password.split(":")[0] + proxy_pass = username_and_password.split(":")[1] except Exception: raise Exception( - 'The format for using a proxy server with authentication ' + "The format for using a proxy server with authentication " 'is: "username:password@hostname:port". If using a proxy ' - 'server without auth, the format is: "hostname:port".') - if browser_name != constants.Browser.GOOGLE_CHROME: + 'server without auth, the format is: "hostname:port".' + ) + if browser_name != constants.Browser.GOOGLE_CHROME and ( + browser_name != constants.Browser.EDGE + ): raise Exception( - "Chrome is required when using a proxy server that has " - "authentication! (If using a proxy server without auth, " - "either Chrome or Firefox may be used.)") - proxy_string = validate_proxy_string(proxy_string) - if proxy_string and proxy_user and proxy_pass: + "Chrome or Edge is required when using a proxy server " + "that has authentication! (If using a proxy server " + "without auth, Chrome, Edge, or Firefox may be used.)" + ) + proxy_string, proxy_scheme = proxy_helper.validate_proxy_string( + proxy_string, keep_scheme=True + ) + if proxy_user and not proxy_pass: + proxy_pass = "" + if proxy_string and proxy_user: proxy_auth = True + elif proxy_pac_url: + username_and_password = None + if "@" in proxy_pac_url: + # Format => username:password@PAC_URL.pac + try: + username_and_password = proxy_pac_url.split("@")[0] + proxy_pac_url = proxy_pac_url.split("@")[1] + proxy_user = username_and_password.split(":")[0] + proxy_pass = username_and_password.split(":")[1] + except Exception: + raise Exception( + "The format for using a PAC URL with authentication " + 'is: "username:password@PAC_URL.pac". If using a PAC ' + 'URL without auth, the format is: "PAC_URL.pac".' + ) + if browser_name != constants.Browser.GOOGLE_CHROME and ( + browser_name != constants.Browser.EDGE + ): + raise Exception( + "Chrome or Edge is required when using a PAC URL " + "that has authentication! (If using a PAC URL " + "without auth, Chrome, Edge, or Firefox may be used.)" + ) + if not proxy_pac_url.lower().endswith(".pac"): + raise Exception('The proxy PAC URL must end with ".pac"!') + if proxy_user and not proxy_pass: + proxy_pass = "" + if proxy_pac_url and proxy_user: + proxy_auth = True + if ( + is_using_uc(undetectable, browser_name) + and not IS_LINUX + and headless + ): + headless = False + headless2 = True + if ( + headless + and ( + proxy_auth + or disable_cookies + or disable_js + or ad_block_on + or disable_csp + or recorder_ext + or extension_zip + or extension_dir + ) + and ( + browser_name == constants.Browser.GOOGLE_CHROME + or browser_name == constants.Browser.EDGE + ) + ): + # Headless Chrome/Edge doesn't support extensions, which are + # required when using a proxy server that has authentication, + # or when using other SeleniumBase extensions (eg: Recorder). + # Instead, base_case.py will use the SBVirtualDisplay when not + # using Chrome's built-in headless mode. See link for details: + # https://bugs.chromium.org/p/chromium/issues/detail?id=706008 + headless = False + if not IS_LINUX: + # Use the new headless mode on Chrome if not using Linux: + # bugs.chromium.org/p/chromium/issues/detail?id=706008#c36 + # Although Linux is technically supported, there are a lot + # of old versions of Chrome on Linux server machines, and + # this mode requires a recent version of Chrome to work. + # Specify "--headless2" as a pytest arg to use on Linux. + headless2 = True + if ( + browser_name == constants.Browser.GOOGLE_CHROME + and user_data_dir + and len(user_data_dir) < 3 + ): + raise Exception( + "Name length of Chrome's User Data Directory must be >= 3." + ) if use_grid: return get_remote_driver( - browser_name, headless, servername, port, proxy_string, proxy_auth, - proxy_user, proxy_pass, user_agent, cap_file, disable_csp) + browser_name, + headless, + locale_code, + protocol, + servername, + port, + proxy_string, + proxy_auth, + proxy_user, + proxy_pass, + proxy_scheme, + proxy_bypass_list, + proxy_pac_url, + multi_proxy, + user_agent, + cap_file, + cap_string, + recorder_ext, + disable_cookies, + disable_js, + disable_csp, + enable_ws, + enable_sync, + use_auto_ext, + undetectable, + uc_cdp_events, + uc_subprocess, + log_cdp_events, + no_sandbox, + disable_gpu, + headless1, + headless2, + incognito, + guest_mode, + dark_mode, + devtools, + remote_debug, + enable_3d_apis, + swiftshader, + ad_block_on, + host_resolver_rules, + block_images, + do_not_track, + chromium_arg, + firefox_arg, + firefox_pref, + user_data_dir, + extension_zip, + extension_dir, + disable_features, + binary_location, + driver_version, + page_load_strategy, + external_pdf, + test_id, + mobile_emulator, + device_width, + device_height, + device_pixel_ratio, + ) else: return get_local_driver( - browser_name, headless, proxy_string, proxy_auth, - proxy_user, proxy_pass, user_agent, disable_csp) + browser_name, + headless, + locale_code, + servername, + proxy_string, + proxy_auth, + proxy_user, + proxy_pass, + proxy_scheme, + proxy_bypass_list, + proxy_pac_url, + multi_proxy, + user_agent, + recorder_ext, + disable_cookies, + disable_js, + disable_csp, + enable_ws, + enable_sync, + use_auto_ext, + undetectable, + uc_cdp_events, + uc_subprocess, + log_cdp_events, + no_sandbox, + disable_gpu, + headless1, + headless2, + incognito, + guest_mode, + dark_mode, + devtools, + remote_debug, + enable_3d_apis, + swiftshader, + ad_block_on, + host_resolver_rules, + block_images, + do_not_track, + chromium_arg, + firefox_arg, + firefox_pref, + user_data_dir, + extension_zip, + extension_dir, + disable_features, + binary_location, + driver_version, + page_load_strategy, + external_pdf, + mobile_emulator, + device_width, + device_height, + device_pixel_ratio, + ) def get_remote_driver( - browser_name, headless, servername, port, proxy_string, proxy_auth, - proxy_user, proxy_pass, user_agent, cap_file, disable_csp): - downloads_path = download_helper.get_downloads_folder() - download_helper.reset_downloads_folder() - address = "http://%s:%s/wd/hub" % (servername, port) + browser_name, + headless, + locale_code, + protocol, + servername, + port, + proxy_string, + proxy_auth, + proxy_user, + proxy_pass, + proxy_scheme, + proxy_bypass_list, + proxy_pac_url, + multi_proxy, + user_agent, + cap_file, + cap_string, + recorder_ext, + disable_cookies, + disable_js, + disable_csp, + enable_ws, + enable_sync, + use_auto_ext, + undetectable, + uc_cdp_events, + uc_subprocess, + log_cdp_events, + no_sandbox, + disable_gpu, + headless1, + headless2, + incognito, + guest_mode, + dark_mode, + devtools, + remote_debug, + enable_3d_apis, + swiftshader, + ad_block_on, + host_resolver_rules, + block_images, + do_not_track, + chromium_arg, + firefox_arg, + firefox_pref, + user_data_dir, + extension_zip, + extension_dir, + disable_features, + binary_location, + driver_version, + page_load_strategy, + external_pdf, + test_id, + mobile_emulator, + device_width, + device_height, + device_pixel_ratio, +): + from selenium.webdriver.common.options import ArgOptions + # Construct the address for connecting to a Selenium Grid + if servername.startswith("https://"): + protocol = "https" + servername = servername.split("https://")[1] + elif "://" in servername: + servername = servername.split("://")[1] + server_with_port = "" + if ":" not in servername: + col_port = ":" + str(port) + first_slash = servername.find("/") + if first_slash != -1: + server_with_port = ( + servername[:first_slash] + col_port + servername[first_slash:] + ) + else: + server_with_port = servername + col_port + else: + server_with_port = servername + address = "%s://%s" % (protocol, server_with_port) + if not address.endswith("/wd/hub"): + if address.endswith("/"): + address += "wd/hub" + else: + address += "/wd/hub" + downloads_path = DOWNLOADS_FOLDER desired_caps = {} + extra_caps = {} if cap_file: + from seleniumbase.core import capabilities_parser desired_caps = capabilities_parser.get_desired_capabilities(cap_file) + if cap_string: + try: + extra_caps = json.loads(str(cap_string)) + except Exception as e: + p1 = "Invalid input format for --cap-string:\n %s" % e + p2 = "The --cap-string input was: %s" % cap_string + p3 = ( + "Enclose cap-string in SINGLE quotes; " + "keys and values in DOUBLE quotes." + ) + p4 = ( + """Here's an example of correct cap-string usage:\n """ + """--cap-string='{"browserName":"chrome","name":"test1"}'""" + ) + raise Exception("%s\n%s\n%s\n%s" % (p1, p2, p3, p4)) + for cap_key in extra_caps.keys(): + desired_caps[cap_key] = extra_caps[cap_key] + if cap_file or cap_string: + if "name" in desired_caps.keys(): + if desired_caps["name"] == "*": + desired_caps["name"] = test_id if browser_name == constants.Browser.GOOGLE_CHROME: chrome_options = _set_chrome_options( - downloads_path, headless, proxy_string, proxy_auth, - proxy_user, proxy_pass, user_agent, disable_csp) - if headless: - if not proxy_auth: - # Headless Chrome doesn't support extensions, which are - # required when using a proxy server that has authentication. - # Instead, base_case.py will use PyVirtualDisplay when not - # using Chrome's built-in headless mode. See link for details: - # https://bugs.chromium.org/p/chromium/issues/detail?id=706008 - chrome_options.add_argument("--headless") - chrome_options.add_argument("--disable-gpu") - chrome_options.add_argument("--no-sandbox") - capabilities = chrome_options.to_capabilities() + browser_name, + downloads_path, + headless, + locale_code, + proxy_string, + proxy_auth, + proxy_user, + proxy_pass, + proxy_scheme, + proxy_bypass_list, + proxy_pac_url, + multi_proxy, + user_agent, + recorder_ext, + disable_cookies, + disable_js, + disable_csp, + enable_ws, + enable_sync, + use_auto_ext, + undetectable, + uc_cdp_events, + uc_subprocess, + log_cdp_events, + no_sandbox, + disable_gpu, + headless1, + headless2, + incognito, + guest_mode, + dark_mode, + devtools, + remote_debug, + enable_3d_apis, + swiftshader, + ad_block_on, + host_resolver_rules, + block_images, + do_not_track, + chromium_arg, + user_data_dir, + extension_zip, + extension_dir, + disable_features, + binary_location, + driver_version, + page_load_strategy, + external_pdf, + servername, + mobile_emulator, + device_width, + device_height, + device_pixel_ratio, + ) + capabilities = webdriver.ChromeOptions().to_capabilities() + # Set custom desired capabilities + selenoid = False + selenoid_options = None + screen_resolution = None + browser_version = None + platform_name = None + extension_capabilities = {} for key in desired_caps.keys(): capabilities[key] = desired_caps[key] - return webdriver.Remote( + if key == "selenoid:options": + selenoid = True + selenoid_options = desired_caps[key] + elif key == "screenResolution": + screen_resolution = desired_caps[key] + elif key == "version" or key == "browserVersion": + browser_version = desired_caps[key] + elif key == "platform" or key == "platformName": + platform_name = desired_caps[key] + elif re.match("[a-zA-Z0-9]*:[a-zA-Z0-9]*", key): + extension_capabilities[key] = desired_caps[key] + cap_str = str(desired_caps).lower() + if "browserstack" in cap_str or "bstack" in cap_str: + chrome_options.set_capability("bstack:options", desired_caps) + chrome_options.set_capability("cloud:options", capabilities) + if selenoid: + snops = selenoid_options + chrome_options.set_capability("selenoid:options", snops) + if screen_resolution: + scres = screen_resolution + chrome_options.set_capability("screenResolution", scres) + if browser_version: + br_vers = browser_version + chrome_options.set_capability("browserVersion", br_vers) + if platform_name: + plat_name = platform_name + chrome_options.set_capability("platformName", plat_name) + if extension_capabilities: + for key in extension_capabilities: + ext_caps = extension_capabilities + chrome_options.set_capability(key, ext_caps[key]) + driver = webdriver.Remote( command_executor=address, - desired_capabilities=capabilities) + options=chrome_options, + ) + return extend_driver(driver) elif browser_name == constants.Browser.FIREFOX: - try: - # Use Geckodriver for Firefox if it's on the PATH - profile = _create_firefox_profile( - downloads_path, proxy_string, user_agent, disable_csp) - firefox_capabilities = DesiredCapabilities.FIREFOX.copy() - firefox_capabilities['marionette'] = True - if headless: - firefox_capabilities['moz:firefoxOptions'] = ( - {'args': ['-headless']}) - for key in desired_caps.keys(): - firefox_capabilities[key] = desired_caps[key] - capabilities = firefox_capabilities - address = "http://%s:%s/wd/hub" % (servername, port) - return webdriver.Remote( - command_executor=address, - desired_capabilities=capabilities, - browser_profile=profile) - except WebDriverException: - # Don't use Geckodriver: Only works for old versions of Firefox - profile = _create_firefox_profile( - downloads_path, proxy_string, user_agent, disable_csp) - firefox_capabilities = DesiredCapabilities.FIREFOX.copy() - firefox_capabilities['marionette'] = False - if headless: - firefox_capabilities['moz:firefoxOptions'] = ( - {'args': ['-headless']}) - for key in desired_caps.keys(): - firefox_capabilities[key] = desired_caps[key] - capabilities = firefox_capabilities - return webdriver.Remote( - command_executor=address, - desired_capabilities=capabilities, - browser_profile=profile) - elif browser_name == constants.Browser.INTERNET_EXPLORER: - capabilities = webdriver.DesiredCapabilities.INTERNETEXPLORER + firefox_options = _set_firefox_options( + downloads_path, + headless, + locale_code, + proxy_string, + proxy_bypass_list, + proxy_pac_url, + user_agent, + disable_cookies, + disable_js, + disable_csp, + firefox_arg, + firefox_pref, + external_pdf, + ) + capabilities = webdriver.FirefoxOptions().to_capabilities() + capabilities["marionette"] = True + if IS_LINUX and headless: + capabilities["moz:firefoxOptions"] = {"args": ["-headless"]} + # Set custom desired capabilities + selenoid = False + selenoid_options = None + screen_resolution = None + browser_version = None + platform_name = None + extension_capabilities = {} for key in desired_caps.keys(): capabilities[key] = desired_caps[key] - return webdriver.Remote( + if key == "selenoid:options": + selenoid = True + selenoid_options = desired_caps[key] + elif key == "screenResolution": + screen_resolution = desired_caps[key] + elif key == "version" or key == "browserVersion": + browser_version = desired_caps[key] + elif key == "platform" or key == "platformName": + platform_name = desired_caps[key] + elif re.match("[a-zA-Z0-9]*:[a-zA-Z0-9]*", key): + extension_capabilities[key] = desired_caps[key] + cap_str = str(desired_caps).lower() + if "browserstack" in cap_str or "bstack" in cap_str: + firefox_options.set_capability("bstack:options", desired_caps) + firefox_options.set_capability("cloud:options", capabilities) + if selenoid: + snops = selenoid_options + firefox_options.set_capability("selenoid:options", snops) + if screen_resolution: + scres = screen_resolution + firefox_options.set_capability("screenResolution", scres) + if browser_version: + br_vers = browser_version + firefox_options.set_capability("browserVersion", br_vers) + if platform_name: + plat_name = platform_name + firefox_options.set_capability("platformName", plat_name) + if extension_capabilities: + for key in extension_capabilities: + ext_caps = extension_capabilities + firefox_options.set_capability(key, ext_caps[key]) + driver = webdriver.Remote( + command_executor=address, + options=firefox_options, + ) + return extend_driver(driver) + elif browser_name == constants.Browser.INTERNET_EXPLORER: + capabilities = webdriver.DesiredCapabilities.INTERNETEXPLORER + remote_options = ArgOptions() + remote_options.set_capability("cloud:options", desired_caps) + driver = webdriver.Remote( command_executor=address, - desired_capabilities=capabilities) + options=remote_options, + ) + return extend_driver(driver) elif browser_name == constants.Browser.EDGE: - capabilities = webdriver.DesiredCapabilities.EDGE + edge_options = _set_chrome_options( + browser_name, + downloads_path, + headless, + locale_code, + proxy_string, + proxy_auth, + proxy_user, + proxy_pass, + proxy_scheme, + proxy_bypass_list, + proxy_pac_url, + multi_proxy, + user_agent, + recorder_ext, + disable_cookies, + disable_js, + disable_csp, + enable_ws, + enable_sync, + use_auto_ext, + undetectable, + uc_cdp_events, + uc_subprocess, + log_cdp_events, + no_sandbox, + disable_gpu, + headless1, + headless2, + incognito, + guest_mode, + dark_mode, + devtools, + remote_debug, + enable_3d_apis, + swiftshader, + ad_block_on, + host_resolver_rules, + block_images, + do_not_track, + chromium_arg, + user_data_dir, + extension_zip, + extension_dir, + disable_features, + binary_location, + driver_version, + page_load_strategy, + external_pdf, + servername, + mobile_emulator, + device_width, + device_height, + device_pixel_ratio, + ) + capabilities = webdriver.EdgeOptions().to_capabilities() + # Set custom desired capabilities + selenoid = False + selenoid_options = None + screen_resolution = None + browser_version = None + platform_name = None + extension_capabilities = {} for key in desired_caps.keys(): capabilities[key] = desired_caps[key] - return webdriver.Remote( + if key == "selenoid:options": + selenoid = True + selenoid_options = desired_caps[key] + elif key == "screenResolution": + screen_resolution = desired_caps[key] + elif key == "version" or key == "browserVersion": + browser_version = desired_caps[key] + elif key == "platform" or key == "platformName": + platform_name = desired_caps[key] + elif re.match("[a-zA-Z0-9]*:[a-zA-Z0-9]*", key): + extension_capabilities[key] = desired_caps[key] + edge_options.set_capability("cloud:options", capabilities) + if selenoid: + snops = selenoid_options + edge_options.set_capability("selenoid:options", snops) + if screen_resolution: + scres = screen_resolution + edge_options.set_capability("screenResolution", scres) + if browser_version: + br_vers = browser_version + edge_options.set_capability("browserVersion", br_vers) + if platform_name: + plat_name = platform_name + edge_options.set_capability("platformName", plat_name) + if extension_capabilities: + for key in extension_capabilities: + ext_caps = extension_capabilities + edge_options.set_capability(key, ext_caps[key]) + driver = webdriver.Remote( command_executor=address, - desired_capabilities=capabilities) + options=edge_options, + ) + return extend_driver(driver) elif browser_name == constants.Browser.SAFARI: capabilities = webdriver.DesiredCapabilities.SAFARI - for key in desired_caps.keys(): - capabilities[key] = desired_caps[key] - return webdriver.Remote( + remote_options = ArgOptions() + remote_options.set_capability("cloud:options", desired_caps) + driver = webdriver.Remote( command_executor=address, - desired_capabilities=capabilities) - elif browser_name == constants.Browser.OPERA: - capabilities = webdriver.DesiredCapabilities.OPERA - for key in desired_caps.keys(): - capabilities[key] = desired_caps[key] - return webdriver.Remote( - command_executor=address, - desired_capabilities=capabilities) - elif browser_name == constants.Browser.PHANTOM_JS: - capabilities = webdriver.DesiredCapabilities.PHANTOMJS - for key in desired_caps.keys(): - capabilities[key] = desired_caps[key] - with warnings.catch_warnings(): - # Ignore "PhantomJS has been deprecated" UserWarning - warnings.simplefilter("ignore", category=UserWarning) - return webdriver.Remote( - command_executor=address, - desired_capabilities=capabilities) - elif browser_name == constants.Browser.ANDROID: - capabilities = webdriver.DesiredCapabilities.ANDROID - for key in desired_caps.keys(): - capabilities[key] = desired_caps[key] - return webdriver.Remote( - command_executor=address, - desired_capabilities=capabilities) - elif browser_name == constants.Browser.IPHONE: - capabilities = webdriver.DesiredCapabilities.IPHONE - for key in desired_caps.keys(): - capabilities[key] = desired_caps[key] - return webdriver.Remote( - command_executor=address, - desired_capabilities=capabilities) - elif browser_name == constants.Browser.IPAD: - capabilities = webdriver.DesiredCapabilities.IPAD - for key in desired_caps.keys(): - capabilities[key] = desired_caps[key] - return webdriver.Remote( - command_executor=address, - desired_capabilities=capabilities) + options=remote_options, + ) + return extend_driver(driver) elif browser_name == constants.Browser.REMOTE: - return webdriver.Remote( + remote_options = ArgOptions() + for cap_name, cap_value in desired_caps.items(): + remote_options.set_capability(cap_name, cap_value) + cap_str = str(desired_caps).lower() + if "browserstack" in cap_str or "bstack" in cap_str: + remote_options.set_capability("bstack:options", desired_caps) + driver = webdriver.Remote( command_executor=address, - desired_capabilities=desired_caps) + options=remote_options, + ) + return extend_driver(driver) def get_local_driver( - browser_name, headless, proxy_string, proxy_auth, - proxy_user, proxy_pass, user_agent, disable_csp): - ''' - Spins up a new web browser and returns the driver. - Can also be used to spin up additional browsers for the same test. - ''' - downloads_path = download_helper.get_downloads_folder() - download_helper.reset_downloads_folder() - + browser_name, + headless, + locale_code, + servername, + proxy_string, + proxy_auth, + proxy_user, + proxy_pass, + proxy_scheme, + proxy_bypass_list, + proxy_pac_url, + multi_proxy, + user_agent, + recorder_ext, + disable_cookies, + disable_js, + disable_csp, + enable_ws, + enable_sync, + use_auto_ext, + undetectable, + uc_cdp_events, + uc_subprocess, + log_cdp_events, + no_sandbox, + disable_gpu, + headless1, + headless2, + incognito, + guest_mode, + dark_mode, + devtools, + remote_debug, + enable_3d_apis, + swiftshader, + ad_block_on, + host_resolver_rules, + block_images, + do_not_track, + chromium_arg, + firefox_arg, + firefox_pref, + user_data_dir, + extension_zip, + extension_dir, + disable_features, + binary_location, + driver_version, + page_load_strategy, + external_pdf, + mobile_emulator, + device_width, + device_height, + device_pixel_ratio, +): + """Spins up a new web browser and returns the driver. + Can also be used to spin up additional browsers for the same test.""" + downloads_path = DOWNLOADS_FOLDER + driver_dir = DRIVER_DIR + special_chrome = False + if binary_location: + if ( + binary_location == "_chromium_" + or "chromium_drivers" in binary_location + ): + special_chrome = True + driver_dir = DRIVER_DIR_CHROMIUM + elif binary_location == "cft" or "cft_drivers" in binary_location: + special_chrome = True + driver_dir = DRIVER_DIR_CFT + elif binary_location == "chs" or "chs_drivers" in binary_location: + special_chrome = True + driver_dir = DRIVER_DIR_CHS + elif _special_binary_exists(binary_location, "opera"): + special_chrome = True + driver_dir = DRIVER_DIR_OPERA + elif _special_binary_exists(binary_location, "brave"): + special_chrome = True + driver_dir = DRIVER_DIR_BRAVE + elif _special_binary_exists(binary_location, "comet"): + special_chrome = True + driver_dir = DRIVER_DIR_COMET + if ( + hasattr(sb_config, "settings") + and getattr(sb_config.settings, "NEW_DRIVER_DIR", None) + and os.path.exists(sb_config.settings.NEW_DRIVER_DIR) + ): + driver_dir = sb_config.settings.NEW_DRIVER_DIR + elif special_chrome: + override_driver_dir(driver_dir) + if IS_MAC or IS_LINUX: + local_chromedriver = driver_dir + "/chromedriver" + local_geckodriver = driver_dir + "/geckodriver" + local_edgedriver = driver_dir + "/msedgedriver" + local_uc_driver = driver_dir + "/uc_driver" + elif IS_WINDOWS: + local_edgedriver = driver_dir + "/msedgedriver.exe" + local_iedriver = driver_dir + "/IEDriverServer.exe" + local_headless_iedriver = driver_dir + "/headless_ie_selenium.exe" + local_chromedriver = driver_dir + "/chromedriver.exe" + local_geckodriver = driver_dir + "/geckodriver.exe" + local_uc_driver = driver_dir + "/uc_driver.exe" + b_path = binary_location + use_uc = is_using_uc(undetectable, browser_name) if browser_name == constants.Browser.FIREFOX: - try: + from selenium.webdriver.firefox.service import Service as FFService + firefox_options = _set_firefox_options( + downloads_path, + headless, + locale_code, + proxy_string, + proxy_bypass_list, + proxy_pac_url, + user_agent, + disable_cookies, + disable_js, + disable_csp, + firefox_arg, + firefox_pref, + external_pdf, + ) + if local_geckodriver and os.path.exists(local_geckodriver): try: - # Use Geckodriver for Firefox if it's on the PATH - profile = _create_firefox_profile( - downloads_path, proxy_string, user_agent, disable_csp) - firefox_capabilities = DesiredCapabilities.FIREFOX.copy() - firefox_capabilities['marionette'] = True - options = webdriver.FirefoxOptions() - if headless: - options.add_argument('-headless') - if LOCAL_GECKODRIVER and os.path.exists(LOCAL_GECKODRIVER): - make_driver_executable_if_not(LOCAL_GECKODRIVER) - firefox_driver = webdriver.Firefox( - firefox_profile=profile, - capabilities=firefox_capabilities, - options=options, - executable_path=LOCAL_GECKODRIVER) + make_driver_executable_if_not(local_geckodriver) + except Exception as e: + logging.debug( + "\nWarning: Could not make geckodriver" + " executable: %s" % e + ) + elif not geckodriver_on_path(): + from seleniumbase.console_scripts import sb_install + args = " ".join(sys.argv) + if not ("-n" in sys.argv or " -n=" in args or args == "-c"): + # (Not multithreaded) + sys_args = sys.argv # Save a copy of current sys args + log_d("\nWarning: geckodriver not found. Getting it now:") + try: + sb_install.main(override="geckodriver") + except Exception as e: + log_d("\nWarning: Could not install geckodriver: %s" % e) + sys.argv = sys_args # Put back the original sys args + else: + geckodriver_fixing_lock = fasteners.InterProcessLock( + constants.MultiBrowser.DRIVER_FIXING_LOCK + ) + with geckodriver_fixing_lock: + with suppress(Exception): + shared_utils.make_writable( + constants.MultiBrowser.DRIVER_FIXING_LOCK + ) + if not geckodriver_on_path(): + sys_args = sys.argv # Save a copy of sys args + log_d( + "\nWarning: geckodriver not found. " + "Getting it now:" + ) + sb_install.main(override="geckodriver") + sys.argv = sys_args # Put back original sys args + # Launch Firefox + if os.path.exists(local_geckodriver): + service = FFService( + executable_path=local_geckodriver, + log_output=os.devnull, + ) + try: + driver = webdriver.Firefox( + service=service, + options=firefox_options, + ) + return extend_driver(driver) + except BaseException as e: + if ( + "geckodriver unexpectedly exited" in str(e) + or "Process unexpectedly closed" in str(e) + or "Failed to read marionette port" in str(e) + or "A connection attempt failed" in str(e) + or "Expected browser binary" in str(e) + or hasattr(e, "msg") and ( + "geckodriver unexpectedly exited" in e.msg + or "Process unexpectedly closed" in e.msg + or "Failed to read marionette port" in e.msg + or "A connection attempt failed" in e.msg + or "Expected browser binary" in e.msg + ) + ): + time.sleep(0.1) + if ( + IS_LINUX + and headless + and ( + "unexpected" in str(e) + or ( + hasattr(e, "msg") and "unexpected" in e.msg + ) + ) + ): + firefox_options.add_argument("-headless") + driver = webdriver.Firefox( + service=service, + options=firefox_options, + ) + return extend_driver(driver) else: - firefox_driver = webdriver.Firefox( - firefox_profile=profile, - capabilities=firefox_capabilities, - options=options) - except WebDriverException: - # Don't use Geckodriver: Only works for old versions of Firefox - profile = _create_firefox_profile( - downloads_path, proxy_string, user_agent, disable_csp) - firefox_capabilities = DesiredCapabilities.FIREFOX.copy() - firefox_capabilities['marionette'] = False - firefox_driver = webdriver.Firefox( - firefox_profile=profile, capabilities=firefox_capabilities) - return firefox_driver - except Exception as e: - if headless: - raise Exception(e) - return webdriver.Firefox() + raise # Not an obvious fix. + else: + service = FFService(log_output=os.devnull) + try: + driver = webdriver.Firefox( + service=service, + options=firefox_options, + ) + return extend_driver(driver) + except BaseException as e: + if ( + "geckodriver unexpectedly exited" in str(e) + or "Process unexpectedly closed" in str(e) + or "Failed to read marionette port" in str(e) + or "A connection attempt failed" in str(e) + or "Expected browser binary" in str(e) + or hasattr(e, "msg") and ( + "geckodriver unexpectedly exited" in e.msg + or "Process unexpectedly closed" in e.msg + or "Failed to read marionette port" in e.msg + or "A connection attempt failed" in e.msg + or "Expected browser binary" in e.msg + ) + ): + time.sleep(0.1) + if ( + IS_LINUX + and headless + and ( + "unexpected" in str(e) + or ( + hasattr(e, "msg") and "unexpected" in e.msg + ) + ) + ): + firefox_options.add_argument("-headless") + driver = webdriver.Firefox( + service=service, + options=firefox_options, + ) + return extend_driver(driver) + else: + raise # Not an obvious fix. elif browser_name == constants.Browser.INTERNET_EXPLORER: if not IS_WINDOWS: raise Exception( - "IE Browser is for Windows-based operating systems only!") + "IE Browser is for Windows-based systems only!" + ) from selenium.webdriver.ie.options import Options + from selenium.webdriver.ie.service import Service ie_options = Options() - ie_options.ignore_protected_mode_settings = False + ie_options.add_argument("--guest") + ie_options.attach_to_edge_chrome = True + ie_options.ignore_protected_mode_settings = True ie_options.ignore_zoom_level = True ie_options.require_window_focus = False ie_options.native_events = True ie_options.full_page_screenshot = True ie_options.persistent_hover = True - ie_capabilities = ie_options.to_capabilities() - if LOCAL_IEDRIVER and os.path.exists(LOCAL_IEDRIVER): - make_driver_executable_if_not(LOCAL_IEDRIVER) - return webdriver.Ie( - capabilities=ie_capabilities, - executable_path=LOCAL_IEDRIVER) - else: - return webdriver.Ie(capabilities=ie_capabilities) + if local_iedriver and os.path.exists(local_iedriver): + try: + make_driver_executable_if_not(local_iedriver) + except Exception as e: + logging.debug( + "\nWarning: Could not make IEDriver executable: %s" % e + ) + elif not iedriver_on_path(): + from seleniumbase.console_scripts import sb_install + args = " ".join(sys.argv) + if not ("-n" in sys.argv or " -n=" in args or args == "-c"): + # (Not multithreaded) + sys_args = sys.argv # Save a copy of current sys args + log_d("\nWarning: IEDriver not found. Getting it now:") + sb_install.main(override="iedriver") + sys.argv = sys_args # Put back the original sys args + if local_headless_iedriver and os.path.exists(local_headless_iedriver): + try: + make_driver_executable_if_not(local_headless_iedriver) + except Exception as e: + logging.debug( + "\nWarning: Could not make HeadlessIEDriver executable: %s" + % e + ) + elif not headless_iedriver_on_path(): + from seleniumbase.console_scripts import sb_install + args = " ".join(sys.argv) + if not ("-n" in sys.argv or " -n=" in args or args == "-c"): + # (Not multithreaded) + sys_args = sys.argv # Save a copy of current sys args + log_d("\nWarning: HeadlessIEDriver not found. Getting it now:") + sb_install.main(override="iedriver") + sys.argv = sys_args # Put back the original sys args + d_b_c = "--disable-build-check" + logger = logging.getLogger("selenium") + logger.setLevel("INFO") + if not headless: + warnings.simplefilter("ignore", category=DeprecationWarning) + service = Service(service_args=[d_b_c], log_output=os.devnull) + driver = webdriver.Ie(service=service, options=ie_options) + return extend_driver(driver) + else: + warnings.simplefilter("ignore", category=DeprecationWarning) + service = Service( + executable_path=local_iedriver, + service_args=[d_b_c], + log_output=os.devnull, + ) + driver = webdriver.Ie(service=service, options=ie_options) + return extend_driver(driver) elif browser_name == constants.Browser.EDGE: - if not IS_WINDOWS: - raise Exception( - "Edge Browser is for Windows-based operating systems only!") - edge_capabilities = DesiredCapabilities.EDGE.copy() - if LOCAL_EDGEDRIVER and os.path.exists(LOCAL_EDGEDRIVER): - make_driver_executable_if_not(LOCAL_EDGEDRIVER) - return webdriver.Edge( - capabilities=edge_capabilities, - executable_path=LOCAL_EDGEDRIVER) - else: - return webdriver.Edge(capabilities=edge_capabilities) - elif browser_name == constants.Browser.SAFARI: - return webdriver.Safari() - elif browser_name == constants.Browser.OPERA: - if LOCAL_OPERADRIVER and os.path.exists(LOCAL_OPERADRIVER): - make_driver_executable_if_not(LOCAL_OPERADRIVER) - return webdriver.Opera(executable_path=LOCAL_OPERADRIVER) - else: - return webdriver.Opera() - elif browser_name == constants.Browser.PHANTOM_JS: - with warnings.catch_warnings(): - # Ignore "PhantomJS has been deprecated" UserWarning - warnings.simplefilter("ignore", category=UserWarning) - return webdriver.PhantomJS() - elif browser_name == constants.Browser.GOOGLE_CHROME: + from selenium.webdriver.edge.service import Service as EdgeService + prefs = { + "download.default_directory": downloads_path, + "download.directory_upgrade": True, + "download.prompt_for_download": False, + "credentials_enable_service": False, + "autofill.credit_card_enabled": False, + "local_discovery.notifications_enabled": False, + "safebrowsing.disable_download_protection": True, + "safebrowsing.enabled": False, # Prevent PW "data breach" pop-ups + "omnibox-max-zero-suggest-matches": 0, + "omnibox-use-existing-autocomplete-client": 0, + "omnibox-trending-zero-prefix-suggestions-on-ntp": 0, + "omnibox-local-history-zero-suggest-beyond-ntp": 0, + "omnibox-on-focus-suggestions-contextual-web": 0, + "omnibox-on-focus-suggestions-srp": 0, + "omnibox-zero-suggest-prefetching": 0, + "omnibox-zero-suggest-prefetching-on-srp": 0, + "omnibox-zero-suggest-prefetching-on-web": 0, + "omnibox-zero-suggest-in-memory-caching": 0, + "content_settings.exceptions.automatic_downloads.*.setting": 1, + "default_content_setting_values.notifications": 0, + "default_content_settings.popups": 0, + "managed_default_content_settings.popups": 0, + "profile.password_manager_enabled": False, + "profile.password_manager_leak_detection": False, + "profile.default_content_setting_values.notifications": 2, + "profile.default_content_settings.popups": 0, + "profile.managed_default_content_settings.popups": 0, + "profile.default_content_setting_values.automatic_downloads": 1, + } + use_version = "latest" + major_edge_version = None + saved_mev = None + use_br_version_for_edge = False + use_exact_version_for_edge = False try: - chrome_options = _set_chrome_options( - downloads_path, headless, proxy_string, proxy_auth, - proxy_user, proxy_pass, user_agent, disable_csp) - if headless: - # Headless Chrome doesn't support extensions, which are - # required when using a proxy server that has authentication. - # Instead, base_case.py will use PyVirtualDisplay when not - # using Chrome's built-in headless mode. See link for details: - # https://bugs.chromium.org/p/chromium/issues/detail?id=706008 - if not proxy_auth: - chrome_options.add_argument("--headless") - chrome_options.add_argument("--disable-gpu") - chrome_options.add_argument("--no-sandbox") - if LOCAL_CHROMEDRIVER and os.path.exists(LOCAL_CHROMEDRIVER): - make_driver_executable_if_not(LOCAL_CHROMEDRIVER) - return webdriver.Chrome( - executable_path=LOCAL_CHROMEDRIVER, options=chrome_options) + if binary_location: + try: + major_edge_version = ( + detect_b_ver.get_browser_version_from_binary( + binary_location + ) + ) + saved_mev = major_edge_version + major_edge_version = saved_mev.split(".")[0] + if len(major_edge_version) < 2: + major_edge_version = None + except Exception: + major_edge_version = None + if not major_edge_version: + br_app = "edge" + major_edge_version = ( + detect_b_ver.get_browser_version_from_os(br_app) + ) + saved_mev = major_edge_version + major_edge_version = major_edge_version.split(".")[0] + if int(major_edge_version) < 80: + major_edge_version = None + elif int(major_edge_version) >= 115: + if ( + driver_version == "browser" + and saved_mev + and len(saved_mev.split(".")) == 4 + ): + driver_version = saved_mev + use_br_version_for_edge = True + except Exception: + major_edge_version = None + if driver_version and "." in driver_version: + use_exact_version_for_edge = True + if use_br_version_for_edge: + major_edge_version = saved_mev + if major_edge_version: + use_version = major_edge_version + edge_driver_version = None + edgedriver_upgrade_needed = False + if os.path.exists(local_edgedriver): + with suppress(Exception): + output = subprocess.check_output( + '"%s" --version' % local_edgedriver, shell=True + ) + if IS_WINDOWS: + output = output.decode("latin1") + else: + output = output.decode("utf-8") + if output.split(" ")[0] == "MSEdgeDriver": + # MSEdgeDriver VERSION + output = output.split(" ")[1] + if use_exact_version_for_edge: + edge_driver_version = output.split(" ")[0] + output = output.split(".")[0] + elif output.split(" ")[0] == "Microsoft": + output = output.split(" ")[3] + if use_exact_version_for_edge: + edge_driver_version = output.split(" ")[0] + output = output.split(".")[0] + else: + output = 0 + if int(output) >= 2: + if not use_exact_version_for_edge: + edge_driver_version = output + if driver_version == "keep": + driver_version = edge_driver_version + use_version = find_edgedriver_version_to_use( + use_version, driver_version + ) + local_edgedriver_exists = False + if local_edgedriver and os.path.exists(local_edgedriver): + local_edgedriver_exists = True + if ( + use_version != "latest" + and edge_driver_version + and use_version != edge_driver_version + ): + edgedriver_upgrade_needed = True else: - return webdriver.Chrome(options=chrome_options) + try: + make_driver_executable_if_not(local_edgedriver) + except Exception as e: + logging.debug( + "\nWarning: Could not make edgedriver" + " executable: %s" % e + ) + if not local_edgedriver_exists or edgedriver_upgrade_needed: + from seleniumbase.console_scripts import sb_install + args = " ".join(sys.argv) + if not ("-n" in sys.argv or " -n=" in args or args == "-c"): + # (Not multithreaded) + msg = "Microsoft Edge Driver not found." + if edgedriver_upgrade_needed: + msg = "Microsoft Edge Driver update needed." + sys_args = sys.argv # Save a copy of current sys args + log_d("\n%s Getting it now:" % msg) + sb_install.main(override="edgedriver %s" % use_version) + sys.argv = sys_args # Put back the original sys args + else: + edgedriver_fixing_lock = fasteners.InterProcessLock( + constants.MultiBrowser.DRIVER_FIXING_LOCK + ) + with edgedriver_fixing_lock: + with suppress(Exception): + shared_utils.make_writable( + constants.MultiBrowser.DRIVER_FIXING_LOCK + ) + msg = "Microsoft Edge Driver not found." + if edgedriver_upgrade_needed: + msg = "Microsoft Edge Driver update needed." + sys_args = sys.argv # Save a copy of current sys args + log_d("\n%s Getting it now:" % msg) + sb_install.main(override="edgedriver %s" % use_version) + sys.argv = sys_args # Put back the original sys args + + # For Microsoft Edge (Chromium) version 80 or higher + Edge = webdriver.Edge + EdgeOptions = webdriver.EdgeOptions + if local_edgedriver and os.path.exists(local_edgedriver): + try: + make_driver_executable_if_not(local_edgedriver) + except Exception as e: + logging.debug( + "\nWarning: Could not make edgedriver" + " executable: %s" % e + ) + edge_options = EdgeOptions() + edge_options.use_chromium = True + if locale_code: + prefs["intl.accept_languages"] = locale_code + if block_images: + prefs["profile.managed_default_content_settings.images"] = 2 + if disable_cookies: + prefs["profile.default_content_setting_values.cookies"] = 2 + if disable_js: + prefs["profile.managed_default_content_settings.javascript"] = 2 + if do_not_track: + prefs["enable_do_not_track"] = True + if external_pdf: + prefs["plugins.always_open_pdf_externally"] = True + pdce = "user_experience_metrics.personalization_data_consent_enabled" + prefs[pdce] = True # Remove "Personalize your web experience" prompt + edge_options.add_experimental_option("prefs", prefs) + edge_options.add_argument( + "--disable-blink-features=AutomationControlled" + ) + edge_options.add_experimental_option( + "excludeSwitches", ["enable-automation", "enable-logging"] + ) + if log_cdp_events: + edge_options.set_capability( + "ms:loggingPrefs", {"performance": "ALL", "browser": "ALL"} + ) + if host_resolver_rules: + edge_options.add_argument( + "--host-resolver-rules=%s" % host_resolver_rules + ) + if not enable_sync: + edge_options.add_argument("--disable-sync") + if ( + not recorder_ext and not disable_csp and not proxy_auth + ): + edge_options.add_argument("--guest") + if dark_mode: + edge_options.add_argument("--enable-features=WebContentsForceDark") + if headless1: + # developer.chrome.com/blog/removing-headless-old-from-chrome + with suppress(Exception): + if int(str(use_version).split(".")[0]) >= 132: + headless1 = False + headless2 = True + if headless2: + try: + if use_version == "latest" or int(use_version) >= 109: + edge_options.add_argument("--headless=new") + else: + edge_options.add_argument("--headless=chrome") + except Exception: + edge_options.add_argument("--headless=new") + elif headless and undetectable: + # (For later: UC Mode doesn't support Edge now) + with suppress(Exception): + if int(use_version) >= 109: + edge_options.add_argument("--headless=new") + elif ( + int(use_version) >= 96 + and int(use_version) <= 108 + ): + edge_options.add_argument("--headless=chrome") + else: + pass # Will need Xvfb on Linux + elif headless: + if ( + "--headless" not in edge_options.arguments + and "--headless=old" not in edge_options.arguments + ): + if headless1: + edge_options.add_argument("--headless=old") + else: + edge_options.add_argument("--headless") + if mobile_emulator and not use_uc: + emulator_settings = {} + device_metrics = {} + if ( + isinstance(device_width, int) + and isinstance(device_height, int) + and isinstance(device_pixel_ratio, (int, float)) + ): + device_metrics["width"] = device_width + device_metrics["height"] = device_height + device_metrics["pixelRatio"] = device_pixel_ratio + else: + device_metrics["width"] = constants.Mobile.WIDTH + device_metrics["height"] = constants.Mobile.HEIGHT + device_metrics["pixelRatio"] = constants.Mobile.RATIO + emulator_settings["deviceMetrics"] = device_metrics + if user_agent: + emulator_settings["userAgent"] = user_agent + edge_options.add_experimental_option( + "mobileEmulation", emulator_settings + ) + # Handle Window Position + if (headless or headless2) and IS_WINDOWS: + # https://stackoverflow.com/a/78999088/7058266 + edge_options.add_argument("--window-position=-2400,-2400") + else: + if ( + hasattr(settings, "WINDOW_START_X") + and isinstance(settings.WINDOW_START_X, int) + and hasattr(settings, "WINDOW_START_Y") + and isinstance(settings.WINDOW_START_Y, int) + ): + edge_options.add_argument( + "--window-position=%s,%s" % ( + settings.WINDOW_START_X, settings.WINDOW_START_Y + ) + ) + # Handle Window Size + if headless or headless2: + if ( + hasattr(settings, "HEADLESS_START_WIDTH") + and isinstance(settings.HEADLESS_START_WIDTH, int) + and hasattr(settings, "HEADLESS_START_HEIGHT") + and isinstance(settings.HEADLESS_START_HEIGHT, int) + ): + edge_options.add_argument( + "--window-size=%s,%s" % ( + settings.HEADLESS_START_WIDTH, + settings.HEADLESS_START_HEIGHT, + ) + ) + else: + if ( + hasattr(settings, "CHROME_START_WIDTH") + and isinstance(settings.CHROME_START_WIDTH, int) + and hasattr(settings, "CHROME_START_HEIGHT") + and isinstance(settings.CHROME_START_HEIGHT, int) + ): + edge_options.add_argument( + "--window-size=%s,%s" % ( + settings.CHROME_START_WIDTH, + settings.CHROME_START_HEIGHT, + ) + ) + if user_data_dir and not use_uc: + abs_path = os.path.abspath(user_data_dir) + edge_options.add_argument("--user-data-dir=%s" % abs_path) + if extension_zip: + # Can be a comma-separated list of .ZIP or .CRX files + extension_zip_list = extension_zip.split(",") + for extension_zip_item in extension_zip_list: + abs_path = os.path.realpath(extension_zip_item) + edge_options.add_extension(abs_path) + if extension_dir: + # load-extension input can be a comma-separated list + abs_path = ( + ",".join(os.path.realpath(p) for p in extension_dir.split(",")) + ) + edge_options = add_chrome_ext_dir(edge_options, abs_path) + edge_options.add_argument("--disable-infobars") + edge_options.add_argument("--disable-notifications") + edge_options.add_argument("--disable-save-password-bubble") + edge_options.add_argument("--disable-single-click-autofill") + edge_options.add_argument( + "--disable-autofill-keyboard-accessory-view[8]" + ) + edge_options.add_argument("--safebrowsing-disable-download-protection") + edge_options.add_argument("--disable-search-engine-choice-screen") + edge_options.add_argument("--disable-browser-side-navigation") + edge_options.add_argument("--disable-translate") + if not enable_ws: + edge_options.add_argument("--disable-web-security") + edge_options.add_argument("--homepage=about:blank") + edge_options.add_argument("--dns-prefetch-disable") + edge_options.add_argument("--dom-automation") + edge_options.add_argument("--disable-hang-monitor") + edge_options.add_argument("--disable-prompt-on-repost") + if not enable_3d_apis: + edge_options.add_argument("--disable-3d-apis") + if headless or headless2 or use_uc: + edge_options.add_argument("--disable-renderer-backgrounding") + edge_options.add_argument("--disable-backgrounding-occluded-windows") + edge_options.add_argument("--disable-client-side-phishing-detection") + edge_options.add_argument("--disable-oopr-debug-crash-dump") + edge_options.add_argument("--disable-top-sites") + edge_options.add_argument("--ash-no-nudges") + edge_options.add_argument("--no-crash-upload") + edge_options.add_argument("--deny-permission-prompts") + if ( + page_load_strategy + and page_load_strategy.lower() in ["eager", "none"] + ): + # Only change it if not "normal", which is the default. + edge_options.page_load_strategy = page_load_strategy.lower() + elif ( + not page_load_strategy + and getattr(settings, "PAGE_LOAD_STRATEGY", None) + and settings.PAGE_LOAD_STRATEGY.lower() in ["eager", "none"] + ): + # Only change it if not "normal", which is the default. + edge_options.page_load_strategy = ( + settings.PAGE_LOAD_STRATEGY.lower() + ) + if (settings.DISABLE_CSP_ON_CHROME or disable_csp) and not headless: + # Headless Edge doesn't support extensions, which are required + # for disabling the Content Security Policy on Edge + edge_options = _add_chrome_disable_csp_extension(edge_options) + if ad_block_on and not headless: + edge_options = _add_chrome_ad_block_extension(edge_options) + if recorder_ext and not headless: + edge_options = _add_chrome_recorder_extension(edge_options) + if proxy_string: + if proxy_auth: + edge_options = _add_chrome_proxy_extension( + edge_options, + proxy_string, + proxy_user, + proxy_pass, + proxy_scheme, + proxy_bypass_list, + zip_it=True, + multi_proxy=multi_proxy, + ) + edge_options.add_argument("--proxy-server=%s" % proxy_string) + if proxy_bypass_list: + edge_options.add_argument( + "--proxy-bypass-list=%s" % proxy_bypass_list + ) + elif proxy_pac_url: + if proxy_auth: + edge_options = _add_chrome_proxy_extension( + edge_options, + None, + proxy_user, + proxy_pass, + proxy_scheme, + proxy_bypass_list, + zip_it=True, + multi_proxy=multi_proxy, + ) + edge_options.add_argument("--proxy-pac-url=%s" % proxy_pac_url) + edge_options.add_argument("--test-type") + edge_options.add_argument("--log-level=3") + edge_options.add_argument("--no-first-run") + edge_options.add_argument("--ignore-certificate-errors") + edge_options.add_argument("--ignore-ssl-errors=yes") + if devtools and not headless: + edge_options.add_argument("--auto-open-devtools-for-tabs") + edge_options.add_argument("--allow-file-access-from-files") + edge_options.add_argument("--allow-insecure-localhost") + edge_options.add_argument("--allow-running-insecure-content") + if user_agent: + edge_options.add_argument("--user-agent=%s" % user_agent) + if IS_LINUX or (IS_MAC and not use_uc): + edge_options.add_argument("--no-sandbox") + if remote_debug: + # To access the Debugger, go to: edge://inspect/#devices + # while a Chromium driver is running. + # Info: https://chromedevtools.github.io/devtools-protocol/ + args = " ".join(sys.argv) + free_port = 9222 + if ("-n" in sys.argv or " -n=" in args or args == "-c"): + free_port = common_utils.free_port() + edge_options.add_argument("--remote-debugging-port=%s" % free_port) + if swiftshader: + edge_options.add_argument("--use-gl=angle") + edge_options.add_argument("--use-angle=swiftshader-webgl") + elif not use_uc and not enable_3d_apis: + edge_options.add_argument("--disable-gpu") + if IS_LINUX: + edge_options.add_argument("--disable-dev-shm-usage") + extra_disabled_features = [] + set_binary = False + if chromium_arg: + # Can be a comma-separated list of Chromium args + chromium_arg_list = None + if isinstance(chromium_arg, (list, tuple)): + chromium_arg_list = chromium_arg + else: + chromium_arg_list = chromium_arg.split(",") + for chromium_arg_item in chromium_arg_list: + chromium_arg_item = chromium_arg_item.strip() + if not chromium_arg_item.startswith("--"): + if chromium_arg_item.startswith("-"): + chromium_arg_item = "-" + chromium_arg_item + else: + chromium_arg_item = "--" + chromium_arg_item + if "set-binary" in chromium_arg_item: + set_binary = True + elif "disable-features=" in chromium_arg_item: + d_f = chromium_arg_item.split("disable-features=")[-1] + extra_disabled_features.append(d_f) + elif len(chromium_arg_item) >= 3: + edge_options.add_argument(chromium_arg_item) + if disable_features: + extra_disabled_features.extend(disable_features.split(",")) + edge_options.add_argument( + '--simulate-outdated-no-au="Tue, 31 Dec 2099 23:59:59 GMT"' + ) + edge_options.add_argument("--disable-ipc-flooding-protection") + edge_options.add_argument("--disable-password-generation") + edge_options.add_argument("--disable-domain-reliability") + edge_options.add_argument("--disable-breakpad") + included_disabled_features = [] + included_disabled_features.append("OptimizationHintsFetching") + included_disabled_features.append("Translate") + included_disabled_features.append("ComponentUpdater") + included_disabled_features.append("OptimizationTargetPrediction") + included_disabled_features.append("OptimizationGuideModelDownloading") + included_disabled_features.append("InsecureDownloadWarnings") + included_disabled_features.append("InterestFeedContentSuggestions") + included_disabled_features.append("PrivacySandboxSettings4") + included_disabled_features.append("SidePanelPinning") + included_disabled_features.append("UserAgentClientHint") + included_disabled_features.append( + "DisableLoadExtensionCommandLineSwitch" + ) + included_disabled_features.append("NetworkPrediction") + included_disabled_features.append("WebAuthentication") + included_disabled_features.append("PasskeyAuth") + included_disabled_features.append("msAutofillEdgeCoupons") + included_disabled_features.append("msShoppingTrigger") + included_disabled_features.append("msEdgeShoppingUI") + included_disabled_features.append("msEntityExtraction") + included_disabled_features.append("msEntityExtractionProactive") + included_disabled_features.append("msWebAssist") + for item in extra_disabled_features: + if item not in included_disabled_features: + included_disabled_features.append(item) + d_f_string = ",".join(included_disabled_features) + edge_options.add_argument("--disable-features=%s" % d_f_string) + if (set_binary or IS_LINUX) and not binary_location: + br_app = "edge" + binary_loc = detect_b_ver.get_binary_location(br_app) + if os.path.exists(binary_loc): + binary_location = binary_loc + if binary_location: + edge_options.binary_location = binary_location + service = EdgeService( + executable_path=local_edgedriver, + log_output=os.devnull, + service_args=["--disable-build-check"], + ) + try: + driver = Edge(service=service, options=edge_options) except Exception as e: - if headless: - raise Exception(e) - if LOCAL_CHROMEDRIVER and os.path.exists(LOCAL_CHROMEDRIVER): - make_driver_executable_if_not(LOCAL_CHROMEDRIVER) - return webdriver.Chrome(executable_path=LOCAL_CHROMEDRIVER) + if not hasattr(e, "msg"): + raise + auto_upgrade_edgedriver = False + edge_version = None + if ( + "This version of MSEdgeDriver only supports" in e.msg + or "This version of Microsoft Edge WebDriver" in e.msg + ): + if "Current browser version is " in e.msg: + auto_upgrade_edgedriver = True + edge_version = e.msg.split( + "Current browser version is " + )[1].split(" ")[0] + elif "only supports MSEdge version " in e.msg: + auto_upgrade_edgedriver = True + edge_version = e.msg.split( + "only supports MSEdge version " + )[1].split(" ")[0] + elif "DevToolsActivePort file doesn't exist" in e.msg: + # https://stackoverflow.com/a/56638103/7058266 + args = " ".join(sys.argv) + free_port = 9222 + if ("-n" in sys.argv or " -n=" in args or args == "-c"): + free_port = common_utils.free_port() + edge_options.add_argument( + "--remote-debugging-port=%s" % free_port + ) + driver = Edge(service=service, options=edge_options) + return extend_driver(driver) + if not auto_upgrade_edgedriver: + raise # Not an obvious fix. else: - return webdriver.Chrome() + pass # Try upgrading EdgeDriver to match Edge. + args = " ".join(sys.argv) + if "-n" in sys.argv or " -n=" in args or args == "-c": + edgedriver_fixing_lock = fasteners.InterProcessLock( + constants.MultiBrowser.DRIVER_FIXING_LOCK + ) + with edgedriver_fixing_lock: + with suppress(Exception): + shared_utils.make_writable( + constants.MultiBrowser.DRIVER_FIXING_LOCK + ) + with suppress(Exception): + if not _was_driver_repaired(): + _repair_edgedriver(edge_version) + _mark_driver_repaired() + else: + with suppress(Exception): + if not _was_driver_repaired(): + _repair_edgedriver(edge_version) + _mark_driver_repaired() + driver = Edge(service=service, options=edge_options) + return extend_driver(driver) + elif browser_name == constants.Browser.SAFARI: + args = " ".join(sys.argv) + if ("-n" in sys.argv or " -n=" in args or args == "-c"): + # Skip if multithreaded + raise Exception("Can't run Safari tests in multithreaded mode!") + warnings.simplefilter("ignore", category=DeprecationWarning) + from selenium.webdriver.safari.options import Options as SafariOptions + from selenium.webdriver.safari.service import Service as SafariService + service = SafariService(quiet=False) + options = SafariOptions() + if ( + page_load_strategy + and page_load_strategy.lower() in ["eager", "none"] + ): + # Only change it if not "normal", which is the default. + options.page_load_strategy = page_load_strategy.lower() + elif ( + not page_load_strategy + and getattr(settings, "PAGE_LOAD_STRATEGY", None) + and settings.PAGE_LOAD_STRATEGY.lower() in ["eager", "none"] + ): + # Only change it if not "normal", which is the default. + options.page_load_strategy = settings.PAGE_LOAD_STRATEGY.lower() + driver = webdriver.safari.webdriver.WebDriver( + service=service, options=options + ) + return extend_driver(driver) + elif browser_name == constants.Browser.GOOGLE_CHROME: + set_chromium = None + if _special_binary_exists(binary_location, "opera"): + set_chromium = "opera" + local_chromedriver = DRIVER_DIR_OPERA + "/chromedriver" + if IS_WINDOWS: + local_chromedriver = DRIVER_DIR_OPERA + "/chromedriver.exe" + if _special_binary_exists(binary_location, "brave"): + set_chromium = "brave" + local_chromedriver = DRIVER_DIR_BRAVE + "/chromedriver" + if IS_WINDOWS: + local_chromedriver = DRIVER_DIR_BRAVE + "/chromedriver.exe" + if _special_binary_exists(binary_location, "comet"): + set_chromium = "comet" + local_chromedriver = DRIVER_DIR_COMET + "/chromedriver" + if IS_WINDOWS: + local_chromedriver = DRIVER_DIR_COMET + "/chromedriver.exe" + try: + chrome_options = _set_chrome_options( + browser_name, + downloads_path, + headless, + locale_code, + proxy_string, + proxy_auth, + proxy_user, + proxy_pass, + proxy_scheme, + proxy_bypass_list, + proxy_pac_url, + multi_proxy, + user_agent, + recorder_ext, + disable_cookies, + disable_js, + disable_csp, + enable_ws, + enable_sync, + use_auto_ext, + undetectable, + uc_cdp_events, + uc_subprocess, + log_cdp_events, + no_sandbox, + disable_gpu, + headless1, + headless2, + incognito, + guest_mode, + dark_mode, + devtools, + remote_debug, + enable_3d_apis, + swiftshader, + ad_block_on, + host_resolver_rules, + block_images, + do_not_track, + chromium_arg, + user_data_dir, + extension_zip, + extension_dir, + disable_features, + binary_location, + driver_version, + page_load_strategy, + external_pdf, + servername, + mobile_emulator, + device_width, + device_height, + device_pixel_ratio, + ) + if ( + (binary_location and "chromium_drivers" in binary_location) + or (binary_location and "Chromium.app" in binary_location) + ): + chrome_options.add_argument("--use-mock-keychain") + use_version = "latest" + major_chrome_version = None + saved_mcv = None + full_ch_version = None + full_ch_driver_version = None + use_br_version_for_uc = False + try: + if chrome_options.binary_location: + try: + major_chrome_version = ( + detect_b_ver.get_browser_version_from_binary( + chrome_options.binary_location, + ) + ) + saved_mcv = major_chrome_version + major_chrome_version = saved_mcv.split(".")[0] + if len(major_chrome_version) < 2: + major_chrome_version = None + except Exception: + major_chrome_version = None + if not major_chrome_version: + br_app = "google-chrome" + full_ch_version = ( + detect_b_ver.get_browser_version_from_os(br_app) + ) + saved_mcv = full_ch_version + major_chrome_version = full_ch_version.split(".")[0] + if int(major_chrome_version) < 67: + major_chrome_version = None + elif ( + int(major_chrome_version) >= 67 + and int(major_chrome_version) <= 72 + ): + # chromedrivers 2.41 - 2.46 could be swapped with 72 + major_chrome_version = "72" + elif int(major_chrome_version) >= 115: + if ( + driver_version == "browser" + and saved_mcv + and len(saved_mcv.split(".")) == 4 + ): + driver_version = saved_mcv + if use_uc: + use_br_version_for_uc = True + if ( + (headless or headless2) + and IS_WINDOWS + and major_chrome_version + and int(major_chrome_version) >= 117 + and not use_uc + and not (remote_debug or devtools) + and not (proxy_string or multi_proxy or proxy_pac_url) + and (not chromium_arg or "debug" not in chromium_arg) + and (not servername or servername == "localhost") + ): + # Hide the "DevTools listening on ..." message. + # https://bugs.chromium.org + # /p/chromedriver/issues/detail?id=4403#c35 + # (Only when the remote debugging port is not needed.) + chrome_options.add_argument("--remote-debugging-pipe") + except Exception: + major_chrome_version = None + if major_chrome_version: + use_version = major_chrome_version + if ( + set_chromium == "opera" + and use_version.isnumeric() + and int(use_version) < 130 + ): + use_version = "130" # Special case + ch_driver_version = None + path_chromedriver = chromedriver_on_path() + if os.path.exists(local_chromedriver): + with suppress(Exception): + output = subprocess.check_output( + '"%s" --version' % local_chromedriver, shell=True + ) + if IS_WINDOWS: + output = output.decode("latin1") + else: + output = output.decode("utf-8") + full_ch_driver_version = output.split(" ")[1] + output = full_ch_driver_version.split(".")[0] + if int(output) >= 2: + ch_driver_version = output + if driver_version == "keep": + driver_version = ch_driver_version + elif path_chromedriver and not set_chromium: + try: + make_driver_executable_if_not(path_chromedriver) + except Exception as e: + logging.debug( + "\nWarning: Could not make chromedriver" + " executable: %s" % e + ) + with suppress(Exception): + output = subprocess.check_output( + '"%s" --version' % path_chromedriver, shell=True + ) + if IS_WINDOWS: + output = output.decode("latin1") + else: + output = output.decode("utf-8") + full_ch_driver_version = output.split(" ")[1] + output = full_ch_driver_version.split(".")[0] + if int(output) >= 2: + ch_driver_version = output + if driver_version == "keep": + use_version = ch_driver_version + disable_build_check = True + uc_driver_version = None + if use_uc: + if use_br_version_for_uc or driver_version == "mlatest": + uc_driver_version = get_uc_driver_version( + full=True, local_uc_driver=local_uc_driver + ) + full_ch_driver_version = uc_driver_version + else: + uc_driver_version = get_uc_driver_version( + local_uc_driver=local_uc_driver + ) + if multi_proxy: + sb_config.multi_proxy = True + if uc_driver_version and driver_version == "keep": + driver_version = uc_driver_version + if ( + hasattr(sb_config, "_cdp_browser") + and sb_config._cdp_browser == "opera" + ): + use_version = "132" + if use_uc: + use_version = "144" + use_version = find_chromedriver_version_to_use( + use_version, driver_version + ) + if headless1: + # developer.chrome.com/blog/removing-headless-old-from-chrome + with suppress(Exception): + if int(str(use_version).split(".")[0]) >= 132: + headless1 = False + headless2 = True + if headless2: + try: + if ( + use_version == "latest" + or int(str(use_version).split(".")[0]) >= 109 + ): + chrome_options.add_argument("--headless=new") + else: + chrome_options.add_argument("--headless=chrome") + except Exception: + chrome_options.add_argument("--headless=new") + elif headless and undetectable: + try: + int_use_version = int(str(use_version).split(".")[0]) + if int_use_version >= 109: + chrome_options.add_argument("--headless=new") + elif ( + int_use_version >= 96 + and int_use_version <= 108 + ): + chrome_options.add_argument("--headless=chrome") + else: + pass # Will need Xvfb on Linux + except Exception: + pass # Will need Xvfb on Linux + elif headless: + if ( + "--headless" not in chrome_options.arguments + and "--headless=old" not in chrome_options.arguments + ): + if headless1: + chrome_options.add_argument("--headless=old") + else: + chrome_options.add_argument("--headless") + if local_chromedriver and os.path.exists(local_chromedriver): + try: + make_driver_executable_if_not(local_chromedriver) + except Exception as e: + logging.debug( + "\nWarning: Could not make chromedriver" + " executable: %s" % e + ) + make_uc_driver_from_chromedriver = False + local_ch_exists = ( + local_chromedriver and os.path.exists(local_chromedriver) + ) + """If no local_chromedriver, but path_chromedriver, and the + browser version nearly matches the driver version, then use + the path_chromedriver instead of downloading a new driver. + Eg. 116.0.* for both is close, but not 116.0.* and 116.1.*""" + browser_driver_close_match = False + if ( + path_chromedriver + and full_ch_version + and full_ch_driver_version + ): + full_ch_v_p = full_ch_version.split(".")[0:2] + full_ch_driver_v_p = full_ch_driver_version.split(".")[0:2] + if ( + full_ch_v_p == full_ch_driver_v_p + or driver_version == "keep" + ): + browser_driver_close_match = True + one_off_chromium = False + if ( + hasattr(sb_config, "binary_location") + and sb_config.binary_location == "_chromium_" + ): + with suppress(Exception): + one_off_chromium_ver = int(use_version.split(".")[0]) - 1 + if one_off_chromium_ver == int(ch_driver_version): + one_off_chromium = True + # If not ARM MAC and need to use uc_driver (and it's missing), + # and already have chromedriver with the correct version, + # then copy chromedriver to uc_driver (and it'll get patched). + if ( + not IS_ARM_MAC + and use_uc + and ( + ( + (local_ch_exists or path_chromedriver) + and use_version == ch_driver_version + and ( + not os.path.exists(local_uc_driver) + or uc_driver_version != use_version + ) + ) + or ( + local_ch_exists + and use_version == "latest" + and not os.path.exists(local_uc_driver) + ) + ) + ): + make_uc_driver_from_chromedriver = True + elif ( + (use_uc and not os.path.exists(local_uc_driver)) + or (not use_uc and not path_chromedriver) + or ( + not use_uc + and use_version != "latest" # Browser version detected + and (ch_driver_version or not local_ch_exists) + and ( + ( + use_version.split(".")[0] != ch_driver_version + and not one_off_chromium + ) + or ( + not local_ch_exists + and use_version.isnumeric() + and int(use_version) >= 115 + and not browser_driver_close_match + and not one_off_chromium + ) + ) + ) + or ( + use_uc + and use_version != "latest" # Browser version detected + and uc_driver_version != use_version + and not one_off_chromium + ) + or ( + full_ch_driver_version # Also used for the uc_driver + and driver_version + and len(str(driver_version).split(".")) == 4 + and full_ch_driver_version != driver_version + and not one_off_chromium + ) + ): + # chromedriver download needed in the seleniumbase/drivers dir + from seleniumbase.console_scripts import sb_install + args = " ".join(sys.argv) + if not ("-n" in sys.argv or " -n=" in args or args == "-c"): + # (Not multithreaded) + sys_args = sys.argv # Save a copy of current sys args + msg = "chromedriver update needed. Getting it now:" + if not path_chromedriver: + msg = "chromedriver not found. Getting it now:" + if use_uc and not os.path.exists(local_uc_driver): + msg = "uc_driver not found. Getting it now:" + if use_uc and os.path.exists(local_uc_driver): + msg = "uc_driver update needed. Getting it now:" + log_d("\nWarning: %s" % msg) + force_uc = False + intel_for_uc = False + if use_uc: + force_uc = True + if IS_ARM_MAC and use_uc: + intel_for_uc = True # Use Intel's driver for UC Mode + try: + sb_install.main( + override="chromedriver %s" % use_version, + intel_for_uc=intel_for_uc, + force_uc=force_uc, + ) + except Exception: + d_latest = get_latest_chromedriver_version() + if ( + d_latest + and use_version != "latest" + and int(use_version) > int(d_latest.split(".")[0]) + ): + disable_build_check = True + d_latest_major = d_latest.split(".")[0] + if ( + not path_chromedriver + or ( + ch_driver_version + and ( + int(ch_driver_version) + < int(d_latest_major) + ) + ) + ): + sb_install.main(override="chromedriver latest") + sys.argv = sys_args # Put back the original sys args + else: + # (Multithreaded) + chromedriver_fixing_lock = fasteners.InterProcessLock( + constants.MultiBrowser.DRIVER_FIXING_LOCK + ) + with chromedriver_fixing_lock: + with suppress(Exception): + shared_utils.make_writable( + constants.MultiBrowser.DRIVER_FIXING_LOCK + ) + msg = "chromedriver update needed. Getting it now:" + if not path_chromedriver: + msg = "chromedriver not found. Getting it now:" + if use_uc and not os.path.exists(local_uc_driver): + msg = "uc_driver not found. Getting it now:" + if use_uc and os.path.exists(local_uc_driver): + msg = "uc_driver update needed. Getting it now:" + force_uc = False + intel_for_uc = False + if use_uc: + force_uc = True + if IS_ARM_MAC and use_uc: + intel_for_uc = True # Use Intel driver for UC Mode + if os.path.exists(local_chromedriver): + with suppress(Exception): + output = subprocess.check_output( + '"%s" --version' % local_chromedriver, + shell=True, + ) + if IS_WINDOWS: + output = output.decode("latin1") + else: + output = output.decode("utf-8") + full_ch_driver_version = output.split(" ")[1] + output = full_ch_driver_version.split(".")[0] + if int(output) >= 2: + ch_driver_version = output + if ( + ( + not use_uc + and not os.path.exists(local_chromedriver) + ) + or (use_uc and not os.path.exists(local_uc_driver)) + or ( + not use_uc + and ( + use_version.split(".")[0] + != ch_driver_version + ) + ) + or ( + use_uc + and ( + use_version.split(".")[0] + != get_uc_driver_version( + local_uc_driver=local_uc_driver + ) + ) + ) + ): + log_d("\nWarning: %s" % msg) + sys_args = sys.argv # Save a copy of sys args + try: + sb_install.main( + override="chromedriver %s" % use_version, + intel_for_uc=intel_for_uc, + force_uc=force_uc, + ) + except Exception: + d_latest = get_latest_chromedriver_version() + if ( + d_latest + and use_version != "latest" + and ( + int(use_version) + > int(d_latest.split(".")[0]) + ) + ): + disable_build_check = True + d_latest_major = d_latest.split(".")[0] + if ( + not path_chromedriver + or ( + ch_driver_version + and ( + int(ch_driver_version) + < int(d_latest_major) + ) + ) + ): + sb_install.main( + override="chromedriver latest" + ) + finally: + sys.argv = sys_args # Put back original args + service_args = [] + if disable_build_check: + service_args = ["--disable-build-check"] + if use_uc: + uc_lock = fasteners.InterProcessLock( + constants.MultiBrowser.DRIVER_FIXING_LOCK + ) + with uc_lock: # Avoid multithreaded issues + with suppress(Exception): + shared_utils.make_writable( + constants.MultiBrowser.DRIVER_FIXING_LOCK + ) + if make_uc_driver_from_chromedriver: + if os.path.exists(local_chromedriver): + with suppress(Exception): + make_driver_executable_if_not( + local_chromedriver + ) + shutil.copy2(local_chromedriver, local_uc_driver) + elif os.path.exists(path_chromedriver): + with suppress(Exception): + make_driver_executable_if_not( + path_chromedriver + ) + shutil.copy2(path_chromedriver, local_uc_driver) + try: + make_driver_executable_if_not(local_uc_driver) + except Exception as e: + logging.debug( + "\nWarning: Could not make uc_driver" + " executable: %s" % e + ) + if not headless or not IS_LINUX or use_uc: + uc_activated = False + try: + if os.path.exists(local_chromedriver) or use_uc: + if headless and not IS_LINUX: + undetectable = False # No support for headless + use_uc = is_using_uc(undetectable, browser_name) + if use_uc: + from seleniumbase import undetected + from urllib.error import URLError + if IS_LINUX: + if "--headless" in ( + chrome_options.arguments + ): + chrome_options.arguments.remove( + "--headless" + ) + if "--headless=old" in ( + chrome_options.arguments + ): + chrome_options.arguments.remove( + "--headless=old" + ) + uc_chrome_version = None + if ( + use_version.isnumeric() + and int(use_version) >= 72 + ): + uc_chrome_version = int(use_version) + elif ( + str(use_version).split(".")[0].isnumeric() + and int(str(use_version).split(".")[0]) >= 72 + ): + uc_chrome_version = ( + int(str(use_version).split(".")[0]) + ) + cdp_events = uc_cdp_events + cert = "unable to get local issuer certificate" + mac_certificate_error = False + if ( + use_version.isnumeric() + and int(use_version) <= 74 + ): + chrome_options.add_experimental_option( + "w3c", True + ) + if ( + (not user_agent or "Headless" in user_agent) + and uc_chrome_version + and uc_chrome_version >= 117 + and (headless or headless2) + and hasattr(sb_config, "uc_agent_cache") + ): + user_agent = sb_config.uc_agent_cache + chrome_options.add_argument( + "--user-agent=%s" % user_agent + ) + with suppress(Exception): + if ( + ( + not user_agent + or "Headless" in user_agent + ) + and uc_chrome_version + and uc_chrome_version >= 117 + and (headless or headless2) + and chromium_arg != "decoy" + ): + from seleniumbase.console_scripts import ( + sb_install + ) + sb_config.uc_user_agent_cache = True + headless_options = _set_chrome_options( + browser_name, + downloads_path, + True, # headless + locale_code, + None, # proxy_string + None, # proxy_auth + None, # proxy_user + None, # proxy_pass + None, # proxy_scheme + None, # proxy_bypass_list + None, # proxy_pac_url + None, # multi_proxy + None, # user_agent + None, # recorder_ext + disable_cookies, + disable_js, + disable_csp, + enable_ws, + enable_sync, + use_auto_ext, + False, # undetectable + False, # uc_cdp_events + False, # uc_subprocess + False, # log_cdp_events + no_sandbox, + disable_gpu, + False, # headless1 + False, # headless2 + incognito, + guest_mode, + dark_mode, + None, # devtools + remote_debug, + enable_3d_apis, + swiftshader, + None, # ad_block_on + None, # host_resolver_rules + block_images, + do_not_track, + None, # chromium_arg + None, # user_data_dir + None, # extension_zip + None, # extension_dir + None, # disable_features + binary_location, + driver_version, + page_load_strategy, + external_pdf, + servername, + mobile_emulator, + device_width, + device_height, + device_pixel_ratio, + ) + if ( + not path_chromedriver + or ( + ch_driver_version + and use_version + and ( + int(ch_driver_version) + < int(str( + use_version).split(".")[0] + ) + ) + ) + ): + sb_install.main( + override="chromedriver %s" + % use_version, + intel_for_uc=False, + force_uc=False, + ) + d_b_c = "--disable-build-check" + if os.path.exists(local_chromedriver): + service = ChromeService( + executable_path=local_chromedriver, + log_output=os.devnull, + service_args=[d_b_c], + ) + driver = webdriver.Chrome( + service=service, + options=headless_options, + ) + else: + service = ChromeService( + log_output=os.devnull, + service_args=[d_b_c], + ) + driver = webdriver.Chrome( + service=service, + options=headless_options, + ) + with suppress(Exception): + user_agent = driver.execute_script( + "return navigator.userAgent;" + ) + if ( + major_chrome_version + and full_ch_version + and full_ch_version.count(".") == 3 + and full_ch_version in user_agent + ): + mcv = major_chrome_version + user_agent = user_agent.replace( + "Chrome/%s" % full_ch_version, + "Chrome/%s.0.0.0" % mcv + ) + user_agent = user_agent.replace( + "Headless", "" + ) + chrome_options.add_argument( + "--user-agent=%s" % user_agent + ) + sb_config.uc_agent_cache = user_agent + driver.quit() + uc_path = None + if os.path.exists(local_uc_driver): + uc_path = local_uc_driver + uc_path = os.path.realpath(uc_path) + try: + driver = undetected.Chrome( + options=chrome_options, + user_data_dir=user_data_dir, + driver_executable_path=uc_path, + browser_executable_path=b_path, + enable_cdp_events=cdp_events, + headless=False, # Xvfb needed! + version_main=uc_chrome_version, + use_subprocess=True, # Always! + ) + uc_activated = True + except URLError as e: + if ( + IS_MAC + and hasattr(e, "args") + and isinstance(e.args, (list, tuple)) + and len(e.args) > 0 + and cert in e.args[0] + ): + mac_certificate_error = True + else: + raise + except SessionNotCreatedException: + time.sleep(0.2) + driver = undetected.Chrome( + options=chrome_options, + user_data_dir=user_data_dir, + driver_executable_path=uc_path, + browser_executable_path=b_path, + enable_cdp_events=cdp_events, + headless=False, # Xvfb needed! + version_main=uc_chrome_version, + use_subprocess=True, # Always! + ) + uc_activated = True + if mac_certificate_error: + cf_lock_path = ( + constants.MultiBrowser.CERT_FIXING_LOCK + ) + cf_lock = fasteners.InterProcessLock( + constants.MultiBrowser.CERT_FIXING_LOCK + ) + if not os.path.exists(cf_lock_path): + # Avoid multithreaded issues + with cf_lock: + with suppress(Exception): + shared_utils.make_writable( + cf_lock_path + ) + # Install Python Certificates (MAC) + os.system( + r"bash /Applications/Python*/" + r"Install\ " + r"Certificates.command" + ) + driver = undetected.Chrome( + options=chrome_options, + user_data_dir=user_data_dir, + driver_executable_path=uc_path, + browser_executable_path=b_path, + enable_cdp_events=cdp_events, + headless=False, # Xvfb needed! + version_main=uc_chrome_version, + use_subprocess=True, # Always! + ) + uc_activated = True + else: + if ( + use_version.isnumeric() + and int(use_version) <= 74 + ): + chrome_options.add_experimental_option( + "w3c", True + ) + service = ChromeService( + executable_path=local_chromedriver, + log_output=os.devnull, + service_args=service_args, + ) + driver = webdriver.Chrome( + service=service, + options=chrome_options, + ) + else: + service = ChromeService( + log_output=os.devnull, + service_args=service_args, + ) + driver = webdriver.Chrome( + service=service, + options=chrome_options, + ) + except Exception as e: + if not hasattr(e, "msg"): + raise + auto_upgrade_chromedriver = False + if "This version of ChromeDriver only supports" in e.msg: + auto_upgrade_chromedriver = True + elif "Chrome version must be between" in e.msg: + auto_upgrade_chromedriver = True + elif "Missing or invalid capabilities" in e.msg: + chrome_options.add_experimental_option("w3c", True) + service = ChromeService( + log_output=os.devnull, + service_args=service_args, + ) + with warnings.catch_warnings(): + warnings.simplefilter( + "ignore", category=DeprecationWarning + ) + driver = webdriver.Chrome( + service=service, options=chrome_options + ) + return extend_driver( + driver, proxy_auth, use_uc, recorder_ext + ) + if not auto_upgrade_chromedriver: + raise # Not an obvious fix. + else: + pass # Try upgrading ChromeDriver to match Chrome. + mcv = None # Major Chrome Version + if "Current browser version is " in e.msg: + line = e.msg.split("Current browser version is ")[1] + browser_version = line.split(" ")[0] + major_chrome_version = browser_version.split(".")[0] + if ( + major_chrome_version.isnumeric() + and int(major_chrome_version) >= 86 + ): + mcv = major_chrome_version + mcv = find_chromedriver_version_to_use( + mcv, driver_version + ) + headless_options = _set_chrome_options( + browser_name, + downloads_path, + True, # headless + locale_code, + None, # proxy_string + None, # proxy_auth + None, # proxy_user + None, # proxy_pass + None, # proxy_scheme + None, # proxy_bypass_list + None, # proxy_pac_url + None, # multi_proxy + None, # user_agent + None, # recorder_ext + disable_cookies, + disable_js, + disable_csp, + enable_ws, + enable_sync, + use_auto_ext, + False, # undetectable + False, # uc_cdp_events + False, # uc_subprocess + False, # log_cdp_events + no_sandbox, + disable_gpu, + False, # headless1 + False, # headless2 + incognito, + guest_mode, + dark_mode, + None, # devtools + remote_debug, + enable_3d_apis, + swiftshader, + None, # ad_block_on + None, # host_resolver_rules + block_images, + do_not_track, + None, # chromium_arg + None, # user_data_dir + None, # extension_zip + None, # extension_dir + None, # disable_features + binary_location, + driver_version, + page_load_strategy, + external_pdf, + servername, + mobile_emulator, + device_width, + device_height, + device_pixel_ratio, + ) + args = " ".join(sys.argv) + if "-n" in sys.argv or " -n=" in args or args == "-c": + chromedriver_fixing_lock = fasteners.InterProcessLock( + constants.MultiBrowser.DRIVER_FIXING_LOCK + ) + with chromedriver_fixing_lock: + with suppress(Exception): + shared_utils.make_writable( + constants.MultiBrowser.DRIVER_FIXING_LOCK + ) + if not _was_driver_repaired(): + _repair_chromedriver( + chrome_options, headless_options, mcv + ) + _mark_driver_repaired() + else: + if not _was_driver_repaired(): + _repair_chromedriver( + chrome_options, headless_options, mcv + ) + _mark_driver_repaired() + if os.path.exists(local_chromedriver): + service = ChromeService( + executable_path=local_chromedriver, + log_output=os.devnull, + service_args=["--disable-build-check"], + ) + driver = webdriver.Chrome( + service=service, + options=chrome_options, + ) + else: + service = ChromeService( + log_output=os.devnull, + service_args=["--disable-build-check"], + ) + driver = webdriver.Chrome( + service=service, + options=chrome_options, + ) + driver.default_get = driver.get # Save copy of original + driver.cdp = None # Set a placeholder + driver._is_using_uc = False + driver._is_using_cdp = False + driver._is_connected = True + if uc_activated: + driver.get = lambda url: uc_special_open_if_cf( + driver, + url, + proxy_string, + mobile_emulator, + device_width, + device_height, + device_pixel_ratio, + ) + driver.uc_open = lambda url: uc_open(driver, url) + driver.uc_open_with_tab = ( + lambda url: uc_open_with_tab(driver, url) + ) + driver.uc_open_with_reconnect = ( + lambda *args, **kwargs: uc_open_with_reconnect( + driver, *args, **kwargs + ) + ) + driver.uc_open_with_disconnect = ( + lambda *args, **kwargs: uc_open_with_disconnect( + driver, *args, **kwargs + ) + ) + driver.uc_click = lambda *args, **kwargs: uc_click( + driver, *args, **kwargs + ) + driver.uc_activate_cdp_mode = ( + lambda *args, **kwargs: uc_activate_cdp_mode( + driver, *args, **kwargs + ) + ) + driver.activate_cdp_mode = driver.uc_activate_cdp_mode + driver.uc_open_with_cdp_mode = ( + lambda *args, **kwargs: uc_open_with_cdp_mode( + driver, *args, **kwargs + ) + ) + driver.uc_gui_press_key = ( + lambda *args, **kwargs: uc_gui_press_key( + driver, *args, **kwargs + ) + ) + driver.uc_gui_press_keys = ( + lambda *args, **kwargs: uc_gui_press_keys( + driver, *args, **kwargs + ) + ) + driver.uc_gui_write = ( + lambda *args, **kwargs: uc_gui_write( + driver, *args, **kwargs + ) + ) + driver.uc_gui_click_x_y = ( + lambda *args, **kwargs: uc_gui_click_x_y( + driver, *args, **kwargs + ) + ) + driver.uc_gui_click_captcha = ( + lambda *args, **kwargs: uc_gui_click_captcha( + driver, *args, **kwargs + ) + ) + driver.uc_gui_click_cf = ( + lambda *args, **kwargs: uc_gui_click_cf( + driver, *args, **kwargs + ) + ) + driver.uc_gui_click_rc = ( + lambda *args, **kwargs: uc_gui_click_rc( + driver, *args, **kwargs + ) + ) + driver.uc_gui_handle_captcha = ( + lambda *args, **kwargs: uc_gui_handle_captcha( + driver, *args, **kwargs + ) + ) + driver.uc_gui_handle_cf = ( + lambda *args, **kwargs: uc_gui_handle_cf( + driver, *args, **kwargs + ) + ) + driver.uc_gui_handle_rc = ( + lambda *args, **kwargs: uc_gui_handle_rc( + driver, *args, **kwargs + ) + ) + driver.uc_switch_to_frame = ( + lambda *args, **kwargs: uc_switch_to_frame( + driver, *args, **kwargs + ) + ) + driver.default_execute_cdp_cmd = driver.execute_cdp_cmd + driver.execute_cdp_cmd = ( + lambda *args, **kwargs: uc_execute_cdp_cmd( + driver, *args, **kwargs + ) + ) + driver._is_hidden = (headless or headless2) + driver._is_using_uc = True + with suppress(Exception): + if int(uc_driver_version) >= 133: + for window_handle in driver.window_handles: + driver.switch_to.window(window_handle) + if driver.current_url.startswith( + "chrome-extension://" + ): + driver.close() + time.sleep(0.003) + driver.switch_to.window(driver.window_handles[0]) + time.sleep(0.003) + # seleniumbase/SeleniumBase/discussions/4190 + if getattr(sb_config, "skip_133_patch", None): + # To skip the connect() patch for Chrome 133+: + # from seleniumbase import config as sb_config + # sb_config.skip_133_patch = True + # (Do the above before launching the browser.) + pass + else: + # This fixes an issue on Chrome 133+ + # (Some people might not need it though.) + driver.connect() + time.sleep(0.003) + if mobile_emulator: + uc_metrics = {} + if ( + isinstance(device_width, int) + and isinstance(device_height, int) + and isinstance(device_pixel_ratio, (int, float)) + ): + uc_metrics["width"] = device_width + uc_metrics["height"] = device_height + uc_metrics["pixelRatio"] = device_pixel_ratio + else: + uc_metrics["width"] = constants.Mobile.WIDTH + uc_metrics["height"] = constants.Mobile.HEIGHT + uc_metrics["pixelRatio"] = constants.Mobile.RATIO + set_device_metrics_override = dict( + { + "width": uc_metrics["width"], + "height": uc_metrics["height"], + "deviceScaleFactor": uc_metrics["pixelRatio"], + "mobile": True + } + ) + with suppress(Exception): + driver.execute_cdp_cmd( + 'Emulation.setDeviceMetricsOverride', + set_device_metrics_override + ) + else: + driver.get = lambda url: updated_get(driver, url) + return extend_driver( + driver, proxy_auth, use_uc, recorder_ext + ) + else: # Running headless on Linux (and not using --uc) + try: + driver = webdriver.Chrome(options=chrome_options) + return extend_driver( + driver, proxy_auth, use_uc, recorder_ext + ) + except Exception as e: + if not hasattr(e, "msg"): + raise + auto_upgrade_chromedriver = False + if "This version of ChromeDriver only supports" in e.msg: + auto_upgrade_chromedriver = True + elif "Chrome version must be between" in e.msg: + auto_upgrade_chromedriver = True + elif "Missing or invalid capabilities" in e.msg: + chrome_options.add_experimental_option("w3c", True) + service = ChromeService( + log_output=os.devnull, + service_args=["--disable-build-check"], + ) + with warnings.catch_warnings(): + warnings.simplefilter( + "ignore", category=DeprecationWarning + ) + driver = webdriver.Chrome( + service=service, options=chrome_options + ) + return extend_driver( + driver, proxy_auth, use_uc, recorder_ext + ) + mcv = None # Major Chrome Version + if "Current browser version is " in e.msg: + line = e.msg.split("Current browser version is ")[1] + browser_version = line.split(" ")[0] + major_chrome_version = browser_version.split(".")[0] + if ( + major_chrome_version.isnumeric() + and int(major_chrome_version) >= 86 + ): + mcv = major_chrome_version + if auto_upgrade_chromedriver: + args = " ".join(sys.argv) + if "-n" in sys.argv or " -n=" in args or args == "-c": + chromedr_fixing_lock = fasteners.InterProcessLock( + constants.MultiBrowser.DRIVER_FIXING_LOCK + ) + D_F_L = constants.MultiBrowser.DRIVER_FIXING_LOCK + with chromedr_fixing_lock: + with suppress(Exception): + shared_utils.make_writable(D_F_L) + if not _was_driver_repaired(): + with suppress(Exception): + _repair_chromedriver( + chrome_options, chrome_options, mcv + ) + _mark_driver_repaired() + else: + if not _was_driver_repaired(): + with suppress(Exception): + _repair_chromedriver( + chrome_options, chrome_options, mcv + ) + _mark_driver_repaired() + with suppress(Exception): + service = ChromeService( + log_output=os.devnull, + service_args=["--disable-build-check"], + ) + driver = webdriver.Chrome( + service=service, + options=chrome_options, + ) + return extend_driver( + driver, proxy_auth, use_uc, recorder_ext + ) + # Use the virtual display on Linux during headless errors + logging.debug( + "\nWarning: Chrome failed to launch in" + " headless mode. Attempting to use the" + " SeleniumBase virtual display on Linux..." + ) + if "--headless" in chrome_options.arguments: + chrome_options.arguments.remove("--headless") + if "--headless=old" in chrome_options.arguments: + chrome_options.arguments.remove("--headless=old") + service = ChromeService( + log_output=os.devnull, + service_args=["--disable-build-check"] + ) + driver = webdriver.Chrome( + service=service, options=chrome_options + ) + return extend_driver( + driver, proxy_auth, use_uc, recorder_ext + ) + except Exception as original_exception: + if use_uc: + raise + # Try again if Chrome didn't launch + with suppress(Exception): + service = ChromeService(service_args=["--disable-build-check"]) + driver = webdriver.Chrome( + service=service, options=chrome_options + ) + return extend_driver( + driver, proxy_auth, use_uc, recorder_ext + ) + if user_data_dir: + print("\nUnable to set user_data_dir while starting Chrome!\n") + raise + elif mobile_emulator: + print("\nFailed to start Chrome's mobile device emulator!\n") + raise + elif extension_zip or extension_dir: + print("\nUnable to load extension while starting Chrome!\n") + raise + elif headless or headless2 or IS_LINUX or proxy_string: + raise + # Try running without any options (bare bones Chrome launch) + if local_chromedriver and os.path.exists(local_chromedriver): + try: + make_driver_executable_if_not(local_chromedriver) + except Exception as e: + logging.debug( + "\nWarning: Could not make chromedriver" + " executable: %s" % e + ) + service = ChromeService( + log_output=os.devnull, + service_args=["--disable-build-check"] + ) + try: + driver = webdriver.Chrome(service=service) + return extend_driver( + driver, proxy_auth, use_uc, recorder_ext + ) + except Exception: + raise original_exception else: raise Exception( - "%s is not a valid browser option for this system!" % browser_name) + "%s is not a valid browser option for this system!" % browser_name + ) diff --git a/seleniumbase/core/capabilities_parser.py b/seleniumbase/core/capabilities_parser.py old mode 100755 new mode 100644 index d02cc9e927e..a1dab1cd248 --- a/seleniumbase/core/capabilities_parser.py +++ b/seleniumbase/core/capabilities_parser.py @@ -1,18 +1,35 @@ import re +import ast +import json +import yaml # Requires pyyaml -def get_desired_capabilities(cap_file): - if not cap_file.endswith('.py'): - raise Exception("\n\n`%s` is not a Python file!\n\n" % cap_file) +def _analyze_ast(contents): + try: + return ast.literal_eval(contents) + except SyntaxError: + pass + try: + # Remove all comments + contents = re.sub(re.compile(r"/\*.*?\*/", re.DOTALL), "", contents) + contents = re.sub(re.compile(r"#.*?\n"), "", contents) - f = open(cap_file, 'r') - all_code = f.read() - f.close() + # Remove anything before dict declaration like: "caps = { ..." + match = re.match(r"^([^{]+)", contents) + if match: + contents = contents.replace(match.group(1), "") + + return ast.literal_eval(contents) + except SyntaxError: + pass + + return False - desired_capabilities = {} - num_capabilities = 0 - code_lines = all_code.split('\n') +def _analyze_manual(contents): + capabilities = {} + + code_lines = contents.split("\n") for line in code_lines: if "desired_cap = {" in line: line = line.split("desired_cap = {")[1] @@ -22,8 +39,7 @@ def get_desired_capabilities(cap_file): if data: key = data.group(1) value = data.group(2) - desired_capabilities[key] = value - num_capabilities += 1 + capabilities[key] = value continue # "KEY" : "VALUE" @@ -31,68 +47,59 @@ def get_desired_capabilities(cap_file): if data: key = data.group(1) value = data.group(2) - desired_capabilities[key] = value - num_capabilities += 1 + capabilities[key] = value continue # 'KEY' : "VALUE" data = re.match( - r'''^\s*'([\S\s]+)'\s*:\s*"([\S\s]+)"\s*[,}]?\s*$''', line) + r"""^\s*'([\S\s]+)'\s*:\s*"([\S\s]+)"\s*[,}]?\s*$""", line + ) if data: key = data.group(1) value = data.group(2) - desired_capabilities[key] = value - num_capabilities += 1 + capabilities[key] = value continue # "KEY" : 'VALUE' data = re.match( - r'''^\s*"([\S\s]+)"\s*:\s*'([\S\s]+)'\s*[,}]?\s*$''', line) + r"""^\s*"([\S\s]+)"\s*:\s*'([\S\s]+)'\s*[,}]?\s*$""", line + ) if data: key = data.group(1) value = data.group(2) - desired_capabilities[key] = value - num_capabilities += 1 + capabilities[key] = value continue # "KEY" : True - data = re.match( - r'''^\s*"([\S\s]+)"\s*:\s*True\s*[,}]?\s*$''', line) + data = re.match(r"""^\s*"([\S\s]+)"\s*:\s*True\s*[,}]?\s*$""", line) if data: key = data.group(1) value = True - desired_capabilities[key] = value - num_capabilities += 1 + capabilities[key] = value continue # 'KEY' : True - data = re.match( - r'''^\s*'([\S\s]+)'\s*:\s*True\s*[,}]?\s*$''', line) + data = re.match(r"""^\s*'([\S\s]+)'\s*:\s*True\s*[,}]?\s*$""", line) if data: key = data.group(1) value = True - desired_capabilities[key] = value - num_capabilities += 1 + capabilities[key] = value continue # "KEY" : False - data = re.match( - r'''^\s*"([\S\s]+)"\s*:\s*False\s*[,}]?\s*$''', line) + data = re.match(r"""^\s*"([\S\s]+)"\s*:\s*False\s*[,}]?\s*$""", line) if data: key = data.group(1) value = False - desired_capabilities[key] = value - num_capabilities += 1 + capabilities[key] = value continue # 'KEY' : False - data = re.match( - r'''^\s*'([\S\s]+)'\s*:\s*False\s*[,}]?\s*$''', line) + data = re.match(r"""^\s*'([\S\s]+)'\s*:\s*False\s*[,}]?\s*$""", line) if data: key = data.group(1) value = False - desired_capabilities[key] = value - num_capabilities += 1 + capabilities[key] = value continue # caps['KEY'] = 'VALUE' @@ -100,8 +107,7 @@ def get_desired_capabilities(cap_file): if data: key = data.group(1) value = data.group(2) - desired_capabilities[key] = value - num_capabilities += 1 + capabilities[key] = value continue # caps["KEY"] = "VALUE" @@ -109,71 +115,102 @@ def get_desired_capabilities(cap_file): if data: key = data.group(1) value = data.group(2) - desired_capabilities[key] = value - num_capabilities += 1 + capabilities[key] = value continue # caps['KEY'] = "VALUE" data = re.match( - r'''^\s*caps\['([\S\s]+)'\]\s*=\s*"([\S\s]+)"\s*$''', line) + r"""^\s*caps\['([\S\s]+)'\]\s*=\s*"([\S\s]+)"\s*$""", line + ) if data: key = data.group(1) value = data.group(2) - desired_capabilities[key] = value - num_capabilities += 1 + capabilities[key] = value continue # caps["KEY"] = 'VALUE' data = re.match( - r'''^\s*caps\["([\S\s]+)"\]\s*=\s*'([\S\s]+)'\s*$''', line) + r"""^\s*caps\["([\S\s]+)"\]\s*=\s*'([\S\s]+)'\s*$""", line + ) if data: key = data.group(1) value = data.group(2) - desired_capabilities[key] = value - num_capabilities += 1 + capabilities[key] = value continue # caps["KEY"] = True - data = re.match( - r'''^\s*caps\["([\S\s]+)"\]\s*=\s*True\s*$''', line) + data = re.match(r"""^\s*caps\["([\S\s]+)"\]\s*=\s*True\s*$""", line) if data: key = data.group(1) value = True - desired_capabilities[key] = value - num_capabilities += 1 + capabilities[key] = value continue # caps['KEY'] = True - data = re.match( - r'''^\s*caps\['([\S\s]+)'\]\s*=\s*True\s*$''', line) + data = re.match(r"""^\s*caps\['([\S\s]+)'\]\s*=\s*True\s*$""", line) if data: key = data.group(1) value = True - desired_capabilities[key] = value - num_capabilities += 1 + capabilities[key] = value continue # caps["KEY"] = False - data = re.match( - r'''^\s*caps\["([\S\s]+)"\]\s*=\s*False\s*$''', line) + data = re.match(r"""^\s*caps\["([\S\s]+)"\]\s*=\s*False\s*$""", line) if data: key = data.group(1) value = False - desired_capabilities[key] = value - num_capabilities += 1 + capabilities[key] = value continue # caps['KEY'] = False - data = re.match( - r'''^\s*caps\['([\S\s]+)'\]\s*=\s*False\s*$''', line) + data = re.match(r"""^\s*caps\['([\S\s]+)'\]\s*=\s*False\s*$""", line) if data: key = data.group(1) value = False - desired_capabilities[key] = value - num_capabilities += 1 + capabilities[key] = value continue - if num_capabilities == 0: - raise Exception("Unable to parse desired capabilities file!") + return capabilities - return desired_capabilities + +def _read_file(file): + f = open(file, "r") + data = f.read() + f.close() + + return data + + +def _parse_py_file(cap_file): + all_code = _read_file(cap_file) + capabilities = _analyze_ast(all_code) + if not capabilities: + capabilities = _analyze_manual(all_code) + return capabilities + + +def _parse_json_file(cap_file): + all_code = _read_file(cap_file) + return json.loads(all_code) + + +def _parse_yaml_file(cap_file): + all_code = _read_file(cap_file) + return yaml.safe_load(all_code) + + +def get_desired_capabilities(cap_file): + if cap_file.endswith(".py"): + capabilities = _parse_py_file(cap_file) + elif cap_file.endswith(".json"): + capabilities = _parse_json_file(cap_file) + elif (cap_file.endswith(".yml") or cap_file.endswith(".yaml")): + capabilities = _parse_yaml_file(cap_file) + else: + raise Exception( + '\n\n`%s` must end in ".py", ".json", ".yml", or ".yaml"!\n' + % cap_file + ) + if len(capabilities.keys()) == 0: + raise Exception("Unable to parse desired capabilities file!") + return capabilities diff --git a/seleniumbase/core/colored_traceback.py b/seleniumbase/core/colored_traceback.py new file mode 100644 index 00000000000..7da890ebd10 --- /dev/null +++ b/seleniumbase/core/colored_traceback.py @@ -0,0 +1,67 @@ +import sys + + +def add_hook(always=False, style="default", debug=False): + import os + + if os.environ.get("NO_COLOR", ""): + return # https://no-color.org + isatty = getattr(sys.stderr, "isatty", lambda: False) + if always or isatty(): + colorizer = Colorizer(style, debug) + sys.excepthook = colorizer.colorize_traceback + + +class Colorizer(object): + def __init__(self, style, debug=False): + self.style = style + self.debug = debug + + def colorize_traceback(self, type, value, tb): + import traceback + import pygments.lexers + + tb_text = "".join(traceback.format_exception(type, value, tb)) + lexer_name = "py3tb" + lexer = pygments.lexers.get_lexer_by_name(lexer_name) + tb_colored = pygments.highlight(tb_text, lexer, self.formatter) + self.stream.write(tb_colored) + + @property + def formatter(self): + from pygments.formatters import get_formatter_by_name + import pygments.util + + colors = _get_term_color_support() + if self.debug: + sys.stderr.write("Detected support for %s colors\n" % colors) + if colors == 256: + fmt_options = {"style": self.style} + elif self.style in ("light", "dark"): + fmt_options = {"bg": self.style} + else: + fmt_options = {"bg": "dark"} + fmt_alias = "terminal256" if colors == 256 else "terminal" + try: + return get_formatter_by_name(fmt_alias, **fmt_options) + except pygments.util.ClassNotFound as ex: + if self.debug: + sys.stderr.write(str(ex) + "\n") + return get_formatter_by_name(fmt_alias) + + @property + def stream(self): + try: + import colorama + except ImportError: + return sys.stderr + return colorama.AnsiToWin32(sys.stderr) + + +def _get_term_color_support(): + try: + import curses + except ImportError: + return 16 + curses.setupterm() + return curses.tigetnum("colors") diff --git a/seleniumbase/core/detect_b_ver.py b/seleniumbase/core/detect_b_ver.py new file mode 100644 index 00000000000..4966a79b29b --- /dev/null +++ b/seleniumbase/core/detect_b_ver.py @@ -0,0 +1,603 @@ +"""Detect the browser version before launching tests. +Eg. detect_b_ver.get_browser_version_from_os("google-chrome")""" +import datetime +import os +import platform +import re +import subprocess +import sys + + +class File(object): + def __init__(self, stream): + self.content = stream.content + self.__stream = stream + self.__temp_name = "driver" + + @property + def filename(self): + try: + filename = re.findall( + "filename=(.+)", self.__stream.headers["content-disposition"] + )[0] + except KeyError: + filename = "%s.zip" % self.__temp_name + except IndexError: + filename = "%s.exe" % self.__temp_name + + if '"' in filename: + filename = filename.replace('"', "") + + return filename + + +class OSType(object): + LINUX = "linux" + MAC = "mac" + WIN = "win" + + +class ChromeType(object): + GOOGLE = "google-chrome" + MSEDGE = "edge" + OPERA = "opera" + BRAVE = "brave" + COMET = "comet" + + +PATTERN = { + ChromeType.GOOGLE: r"\d+\.\d+\.\d+", + ChromeType.MSEDGE: r"\d+\.\d+\.\d+", +} + + +def os_name(): + if "linux" in sys.platform: + return OSType.LINUX + elif "darwin" in sys.platform: + return OSType.MAC + elif "win32" in sys.platform: + return OSType.WIN + else: + raise Exception("Could not determine the OS type!") + + +def os_architecture(): + if platform.machine().endswith("64"): + return 64 + else: + return 32 + + +def os_type(): + return "%s%s" % (os_name(), os_architecture()) + + +def is_arch(os_sys_type): + if '_m1' in os_sys_type: + return True + return platform.processor() != 'i386' + + +def is_mac_os(os_sys_type): + return OSType.MAC in os_sys_type + + +def get_date_diff(date1, date2, date_format): + a = datetime.datetime.strptime(date1, date_format) + b = datetime.datetime.strptime( + str(date2.strftime(date_format)), date_format) + return (b - a).days + + +def linux_browser_apps_to_cmd(*apps): + """Create 'browser --version' command from browser app names.""" + ignore_errors_cmd_part = " 2>/dev/null" if os.getenv( + "WDM_LOG_LEVEL") == "0" else "" + return " || ".join( + "%s --version%s" % (i, ignore_errors_cmd_part) for i in apps + ) + + +def chrome_on_linux_path(chromium_ok=False, browser_type=None): + if browser_type and browser_type != ChromeType.GOOGLE: + return "" + if os_name() != OSType.LINUX: + return "" + paths = [ + "/bin/google-chrome", + "/bin/google-chrome-stable", + "/usr/bin/google-chrome", + "/usr/bin/google-chrome-stable" + ] + for path in paths: + try: + if ( + os.path.exists(path) + and os.access(path, os.R_OK) + and os.access(path, os.X_OK) + ): + return path + except Exception: + pass + paths = os.environ["PATH"].split(os.pathsep) + binaries = [] + binaries.append("google-chrome") + binaries.append("google-chrome-stable") + binaries.append("google-chrome-beta") + binaries.append("google-chrome-dev") + binaries.append("google-chrome-unstable") + binaries.append("chrome") + binaries.append("chromium") + binaries.append("chromium-browser") + for binary in binaries: + for path in paths: + full_path = os.path.join(path, binary) + try: + if ( + os.path.exists(full_path) + and os.access(full_path, os.R_OK) + and os.access(full_path, os.X_OK) + ): + return full_path + except Exception: + pass + if chromium_ok: + paths = [ + "/bin/chromium", + "/bin/chromium-browser", + "/usr/bin/chromium", + "/usr/bin/chromium-browser" + ] + for path in paths: + try: + if ( + os.path.exists(path) + and os.access(path, os.R_OK) + and os.access(path, os.X_OK) + ): + return path + except Exception: + pass + return "/usr/bin/google-chrome" + + +def edge_on_linux_path(browser_type=None): + if browser_type and browser_type != ChromeType.MSEDGE: + return "" + if os_name() != OSType.LINUX: + return "" + paths = os.environ["PATH"].split(os.pathsep) + binaries = [] + binaries.append("microsoft-edge") + binaries.append("microsoft-edge-stable") + binaries.append("microsoft-edge-beta") + binaries.append("microsoft-edge-dev") + for binary in binaries: + for path in paths: + full_path = os.path.join(path, binary) + if os.path.exists(full_path) and os.access(full_path, os.X_OK): + return full_path + return "/usr/bin/microsoft-edge" + + +def opera_on_linux_path(browser_type=None): + if browser_type and browser_type != ChromeType.OPERA: + return "" + if os_name() != OSType.LINUX: + return "" + paths = os.environ["PATH"].split(os.pathsep) + binaries = [] + binaries.append("opera") + binaries.append("opera-stable") + for binary in binaries: + for path in paths: + full_path = os.path.join(path, binary) + if os.path.exists(full_path) and os.access(full_path, os.X_OK): + return full_path + return "/usr/bin/opera-stable" + + +def brave_on_linux_path(browser_type=None): + if browser_type and browser_type != ChromeType.BRAVE: + return "" + if os_name() != OSType.LINUX: + return "" + paths = os.environ["PATH"].split(os.pathsep) + binaries = [] + binaries.append("brave-browser") + binaries.append("brave") + binaries.append("brave-browser-stable") + for binary in binaries: + for path in paths: + full_path = os.path.join(path, binary) + if os.path.exists(full_path) and os.access(full_path, os.X_OK): + return full_path + return "/usr/bin/brave-browser" + + +def comet_on_linux_path(browser_type=None): + if browser_type and browser_type != ChromeType.COMET: + return "" + if os_name() != OSType.LINUX: + return "" + return "" # Comet Browser isn't supported on Linux yet + + +def chrome_on_windows_path(browser_type=None): + if browser_type and browser_type != ChromeType.GOOGLE: + return "" + if os_name() != OSType.WIN: + return "" + candidates = [] + for item in map( + os.environ.get, + ( + "PROGRAMFILES", + "PROGRAMFILES(X86)", + "LOCALAPPDATA", + "PROGRAMW6432", + ), + ): + for subitem in ( + "Google/Chrome/Application", + "Google/Chrome Beta/Application", + "Google/Chrome Canary/Application", + ): + try: + candidates.append(os.sep.join((item, subitem, "chrome.exe"))) + except TypeError: + pass + for candidate in candidates: + if os.path.exists(candidate) and os.access(candidate, os.X_OK): + return os.path.normpath(candidate) + return "" + + +def edge_on_windows_path(browser_type=None): + if browser_type and browser_type != ChromeType.MSEDGE: + return "" + if os_name() != OSType.WIN: + return "" + candidates = [] + for item in map( + os.environ.get, + ( + "PROGRAMFILES", + "PROGRAMFILES(X86)", + "LOCALAPPDATA", + "PROGRAMW6432", + ), + ): + for subitem in ( + "Microsoft/Edge/Application", + "Microsoft/Edge Beta/Application", + "Microsoft/Edge Canary/Application", + ): + try: + candidates.append(os.sep.join((item, subitem, "msedge.exe"))) + except TypeError: + pass + for candidate in candidates: + if os.path.exists(candidate) and os.access(candidate, os.X_OK): + return os.path.normpath(candidate) + return "" + + +def opera_on_windows_path(browser_type=None): + if browser_type and browser_type != ChromeType.OPERA: + return "" + if os_name() != OSType.WIN: + return "" + candidates = [] + for item in map( + os.environ.get, + ( + "PROGRAMFILES", + "PROGRAMFILES(X86)", + "LOCALAPPDATA", + "PROGRAMW6432", + ), + ): + for subitem in ( + "Programs/Opera", + "Opera", + "Opera/Application", + ): + try: + candidates.append(os.sep.join((item, subitem, "opera.exe"))) + except TypeError: + pass + for candidate in candidates: + if os.path.exists(candidate) and os.access(candidate, os.X_OK): + return os.path.normpath(candidate) + return "" + + +def brave_on_windows_path(browser_type=None): + if browser_type and browser_type != ChromeType.BRAVE: + return "" + if os_name() != OSType.WIN: + return "" + candidates = [] + for item in map( + os.environ.get, + ( + "PROGRAMFILES", + "PROGRAMFILES(X86)", + "LOCALAPPDATA", + "PROGRAMW6432", + ), + ): + for subitem in ( + "BraveSoftware/Brave-Browser/Application", + ): + try: + candidates.append(os.sep.join((item, subitem, "brave.exe"))) + except TypeError: + pass + for candidate in candidates: + if os.path.exists(candidate) and os.access(candidate, os.X_OK): + return os.path.normpath(candidate) + return "" + + +def comet_on_windows_path(browser_type=None): + if browser_type and browser_type != ChromeType.COMET: + return "" + if os_name() != OSType.WIN: + return "" + candidates = [] + for item in map( + os.environ.get, + ( + "LOCALAPPDATA", + "PROGRAMFILES", + "PROGRAMFILES(X86)", + "PROGRAMW6432", + ), + ): + for subitem in ( + "Perplexity/Comet/Application", + "Comet/Application", + "Programs/Comet", + ): + try: + candidates.append(os.sep.join((item, subitem, "comet.exe"))) + except TypeError: + pass + for candidate in candidates: + if os.path.exists(candidate) and os.access(candidate, os.X_OK): + return os.path.normpath(candidate) + return "" + + +def windows_browser_apps_to_cmd(*apps): + """Create analogue of browser --version command for windows.""" + powershell = determine_powershell() + first_hit_template = "$tmp = {expression}; if ($tmp) {{echo $tmp; Exit;}};" + script = "$ErrorActionPreference='silentlycontinue'; " + " ".join( + first_hit_template.format(expression=e) for e in apps + ) + return '%s -NoProfile "%s"' % (powershell, script) + + +def get_binary_location(browser_type, chromium_ok=False): + """Return the full path of the browser binary.""" + if browser_type.lower() == "chrome": + browser_type = "google-chrome" + elif browser_type.lower() == "msedge": + browser_type = "edge" + else: + browser_type = browser_type.lower() + cmd_mapping = { + ChromeType.GOOGLE: { + OSType.LINUX: chrome_on_linux_path(chromium_ok, browser_type), + OSType.MAC: r"/Applications/Google Chrome.app" + r"/Contents/MacOS/Google Chrome", + OSType.WIN: chrome_on_windows_path(browser_type), + }, + ChromeType.MSEDGE: { + OSType.LINUX: edge_on_linux_path(browser_type), + OSType.MAC: r"/Applications/Microsoft Edge.app" + r"/Contents/MacOS/Microsoft Edge", + OSType.WIN: edge_on_windows_path(browser_type), + }, + ChromeType.OPERA: { + OSType.LINUX: opera_on_linux_path(browser_type), + OSType.MAC: r"/Applications/Opera.app" + r"/Contents/MacOS/Opera", + OSType.WIN: opera_on_windows_path(browser_type), + }, + ChromeType.BRAVE: { + OSType.LINUX: brave_on_linux_path(browser_type), + OSType.MAC: r"/Applications/Brave Browser.app" + r"/Contents/MacOS/Brave Browser", + OSType.WIN: brave_on_windows_path(browser_type), + }, + ChromeType.COMET: { + OSType.LINUX: comet_on_linux_path(browser_type), + OSType.MAC: r"/Applications/Comet.app" + r"/Contents/MacOS/Comet", + OSType.WIN: comet_on_windows_path(browser_type), + }, + } + return cmd_mapping[browser_type][os_name()] + + +def get_browser_version_from_binary(binary_location): + try: + if not os.path.exists(binary_location): + return None + path = binary_location + pattern = r"\d+\.\d+\.\d+" + quad_pattern = r"\d+\.\d+\.\d+\.\d+" + if os_name() == OSType.WIN: + path = path.replace(r"\ ", r" ").replace("\\", "\\\\") + cmd_mapping = ( + '''powershell -command "&{(Get-Item -Path '%s')''' + '''.VersionInfo.FileVersion}"''' % path + ) + quad_version = read_version_from_cmd(cmd_mapping, quad_pattern) + if quad_version and len(str(quad_version)) >= 9: # Eg. 122.0.0.0 + return quad_version + return read_version_from_cmd(cmd_mapping, pattern) + if binary_location.count(r"\ ") != binary_location.count(" "): + binary_location = binary_location.replace(" ", r"\ ") + cmd_mapping = binary_location + " --version" + quad_version = read_version_from_cmd(cmd_mapping, quad_pattern) + if quad_version and len(str(quad_version)) >= 9: + return quad_version + return read_version_from_cmd(cmd_mapping, pattern) + except Exception: + return None + + +def get_browser_version_from_os(browser_type): + """Return installed browser version.""" + cmd_mapping = { + ChromeType.GOOGLE: { + OSType.LINUX: linux_browser_apps_to_cmd( + "google-chrome", + "google-chrome-stable", + "chrome", + "chromium", + "chromium-browser", + "google-chrome-beta", + "google-chrome-dev", + "google-chrome-unstable", + ), + OSType.MAC: r"/Applications/Google\ Chrome.app" + r"/Contents/MacOS/Google\ Chrome --version", + OSType.WIN: windows_browser_apps_to_cmd( + r'(Get-Item -Path "$env:PROGRAMFILES\Google\Chrome' + r'\Application\chrome.exe").VersionInfo.FileVersion', + r'(Get-Item -Path "$env:PROGRAMFILES (x86)\Google\Chrome' + r'\Application\chrome.exe").VersionInfo.FileVersion', + r'(Get-Item -Path "$env:LOCALAPPDATA\Google\Chrome' + r'\Application\chrome.exe").VersionInfo.FileVersion', + r'(Get-ItemProperty -Path Registry::"HKCU\SOFTWARE' + r'\Google\Chrome\BLBeacon").version', + r'(Get-ItemProperty -Path Registry::"HKLM\SOFTWARE' + r'\Wow6432Node\Microsoft\Windows' + r'\CurrentVersion\Uninstall\Google Chrome").version', + ), + }, + ChromeType.MSEDGE: { + OSType.LINUX: linux_browser_apps_to_cmd( + "microsoft-edge", + "microsoft-edge-stable", + "microsoft-edge-beta", + "microsoft-edge-dev", + ), + OSType.MAC: r"/Applications/Microsoft\ Edge.app" + r"/Contents/MacOS/Microsoft\ Edge --version", + OSType.WIN: windows_browser_apps_to_cmd( + # stable edge + r'(Get-Item -Path "$env:PROGRAMFILES\Microsoft\Edge' + r'\Application\msedge.exe").VersionInfo.FileVersion', + r'(Get-Item -Path "$env:PROGRAMFILES (x86)\Microsoft' + r'\Edge\Application\msedge.exe").VersionInfo.FileVersion', + r'(Get-ItemProperty -Path Registry::"HKCU\SOFTWARE' + r'\Microsoft\Edge\BLBeacon").version', + r'(Get-ItemProperty -Path Registry::"HKLM\SOFTWARE' + r'\Microsoft\EdgeUpdate\Clients' + r'\{56EB18F8-8008-4CBD-B6D2-8C97FE7E9062}").pv', + # beta edge + r'(Get-Item -Path "$env:LOCALAPPDATA\Microsoft\Edge Beta' + r'\Application\msedge.exe").VersionInfo.FileVersion', + r'(Get-Item -Path "$env:PROGRAMFILES\Microsoft\Edge Beta' + r'\Application\msedge.exe").VersionInfo.FileVersion', + r'(Get-Item -Path "$env:PROGRAMFILES (x86)\Microsoft\Edge Beta' + r'\Application\msedge.exe").VersionInfo.FileVersion', + r'(Get-ItemProperty -Path Registry::"HKCU\SOFTWARE\Microsoft' + r'\Edge Beta\BLBeacon").version', + # dev edge + r'(Get-Item -Path "$env:LOCALAPPDATA\Microsoft\Edge Dev' + r'\Application\msedge.exe").VersionInfo.FileVersion', + r'(Get-Item -Path "$env:PROGRAMFILES\Microsoft\Edge Dev' + r'\Application\msedge.exe").VersionInfo.FileVersion', + r'(Get-Item -Path "$env:PROGRAMFILES (x86)\Microsoft\Edge Dev' + r'\Application\msedge.exe").VersionInfo.FileVersion', + r'(Get-ItemProperty -Path Registry::"HKCU\SOFTWARE\Microsoft' + r'\Edge Dev\BLBeacon").version', + # canary edge + r'(Get-Item -Path "$env:LOCALAPPDATA\Microsoft\Edge SxS' + r'\Application\msedge.exe").VersionInfo.FileVersion', + r'(Get-ItemProperty -Path Registry::"HKCU\SOFTWARE' + r'\Microsoft\Edge SxS\BLBeacon").version', + # highest edge + r"(Get-Item (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft" + r"\Windows\CurrentVersion\App Paths\msedge.exe')." + r"'(Default)').VersionInfo.ProductVersion", + r"[System.Diagnostics.FileVersionInfo]::GetVersionInfo((" + r"Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows" + r"\CurrentVersion\App Paths\msedge.exe')." + r"'(Default)').ProductVersion", + r"Get-AppxPackage -Name *MicrosoftEdge.* | Foreach Version", + r'(Get-ItemProperty -Path Registry::"HKLM\SOFTWARE\Wow6432Node' + r'\Microsoft\Windows\CurrentVersion\Uninstall' + r'\Microsoft Edge").version', + ), + }, + } + try: + cmd_mapping = cmd_mapping[browser_type][os_name()] + pattern = PATTERN[browser_type] + quad_pattern = r"\d+\.\d+\.\d+\.\d+" + quad_version = read_version_from_cmd(cmd_mapping, quad_pattern) + if quad_version and len(str(quad_version)) >= 9: # Eg. 115.0.0.0 + return quad_version + version = read_version_from_cmd(cmd_mapping, pattern) + return version + except Exception: + raise Exception( + "Can not find browser %s installed in your system!" % browser_type + ) + + +def format_version(browser_type, version): + if not version or version == 'latest': + return 'latest' + try: + pattern = PATTERN[browser_type] + result = re.search(pattern, version) + return result.group(0) if result else version + except Exception: + return "latest" + + +def get_browser_version(browser_type, metadata): + pattern = PATTERN[browser_type] + version_from_os = metadata['version'] + result = re.search(pattern, version_from_os) + version = result.group(0) if version_from_os else None + return version + + +def read_version_from_cmd(cmd, pattern): + with subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, + shell=True, + ) as stream: + stdout = stream.communicate()[0].decode() + version = re.search(pattern, stdout) + version = version.group(0) if version else None + return version + + +def determine_powershell(): + """Returns "True" if runs in Powershell and "False" if another console.""" + cmd = "(dir 2>&1 *`|echo CMD);&<# rem #>echo powershell" + with subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, + shell=True, + ) as stream: + stdout = stream.communicate()[0].decode() + return "" if stdout == "powershell" else "powershell" diff --git a/seleniumbase/core/download_helper.py b/seleniumbase/core/download_helper.py old mode 100755 new mode 100644 index 10e239f9bbc..53bac022f33 --- a/seleniumbase/core/download_helper.py +++ b/seleniumbase/core/download_helper.py @@ -1,18 +1,17 @@ import os import shutil -import sys import time from seleniumbase.config import settings from seleniumbase.fixtures import constants -# Folder for saving downloaded files. -# If initiated by WebDriver clicks, works ONLY for Chrome and Firefox. -# Browser used doesn't matter if done with self.download_file(file_url) -# or self.save_file_as(file_url, new_file_name) +# The "downloads_folder" is a folder for saving downloaded files. +# Works for downloads initiated by Chromium and Firefox WebDriver clicks. +# Browser type doesn't matter if using self.download_file(file_url) +# or self.save_file_as(file_url, new_file_name) +# The "downloads_folder" is cleaned out at the start of each pytest run, +# but there is an option to save existing files in "archived_files". DOWNLOADS_DIR = constants.Files.DOWNLOADS_FOLDER -ARCHIVE_DIR = constants.Files.ARCHIVED_DOWNLOADS_FOLDER - -abs_path = os.path.abspath('.') +abs_path = os.path.abspath(".") downloads_path = os.path.join(abs_path, DOWNLOADS_DIR) @@ -21,30 +20,55 @@ def get_downloads_folder(): def reset_downloads_folder(): - ''' Clears the downloads folder. - If settings.ARCHIVE_EXISTING_DOWNLOADS is set to True, archives it. ''' + """Clears the downloads folder. + If settings.ARCHIVE_EXISTING_DOWNLOADS is set to True, archives it.""" + downloads_dir = constants.Files.DOWNLOADS_FOLDER + archive_dir = constants.Files.ARCHIVED_DOWNLOADS_FOLDER + if downloads_dir.endswith("/"): + downloads_dir = downloads_dir[:-1] + if downloads_dir.startswith("/"): + downloads_dir = downloads_dir[1:] + if archive_dir.endswith("/"): + archive_dir = archive_dir[:-1] + if archive_dir.startswith("/"): + archive_dir = archive_dir[1:] + if len(downloads_dir) < 10 or len(archive_dir) < 10: + return # Prevent accidental deletions if constants are renamed + archived_downloads_folder = os.path.join(os.getcwd(), archive_dir) + os.sep if os.path.exists(downloads_path) and not os.listdir(downloads_path) == []: - archived_downloads_folder = os.path.join(downloads_path, '..', - ARCHIVE_DIR) - if not "".join(sys.argv) == "-c": - # Only move files if the test run is not multi-threaded. - # (Running tests with "-n NUM" will create threads that only - # have "-c" in the sys.argv list. Easy to catch.) - reset_downloads_folder_assistant(archived_downloads_folder) + reset_downloads_folder_assistant(archived_downloads_folder) + if os.path.exists(downloads_path) and os.listdir(downloads_path) == []: + try: + os.rmdir(downloads_path) + except OSError: + pass + if ( + os.path.exists(archived_downloads_folder) + and os.listdir(archived_downloads_folder) == [] + ): + try: + os.rmdir(archived_downloads_folder) + except OSError: + pass def reset_downloads_folder_assistant(archived_downloads_folder): if not os.path.exists(archived_downloads_folder): try: - os.makedirs(archived_downloads_folder) + os.makedirs(archived_downloads_folder, exist_ok=True) except Exception: pass # Should only be reachable during multi-threaded test runs new_archived_downloads_sub_folder = "%s/downloads_%s" % ( - archived_downloads_folder, int(time.time())) + archived_downloads_folder, + int(time.time()), + ) if os.path.exists(downloads_path): if not os.listdir(downloads_path) == []: - shutil.move(downloads_path, new_archived_downloads_sub_folder) - os.makedirs(downloads_path) + try: + shutil.move(downloads_path, new_archived_downloads_sub_folder) + os.makedirs(downloads_path, exist_ok=True) + except Exception: + pass if not settings.ARCHIVE_EXISTING_DOWNLOADS: try: shutil.rmtree(new_archived_downloads_sub_folder) diff --git a/seleniumbase/core/encoded_images.py b/seleniumbase/core/encoded_images.py new file mode 100644 index 00000000000..1e0338a9e41 --- /dev/null +++ b/seleniumbase/core/encoded_images.py @@ -0,0 +1,213 @@ +""" Instructions for generating encoded images: + > import base64 + > with open("YOUR_FILE.png", "rb") as image_file: + > encoded_string = base64.b64encode(image_file.read()) +""" + + +def get_dash_pie_png1(): + DASH_PIE_PNG_1 = ( + "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAEcAAABHCAYAAABVsFofAAA" + "AAXNSR0IArs4c6QAABqxJREFUeAHtnOtPFFcUwO/sLo9GUKIkUtNWDK+a2qZGm2LShDFS" + "oeWhqUatxrSoMSS06R/QD0DSP0MTP1Cb9pMCpUCx0EIaWiBChBYLUWwMEndBEBamuztze" + "8+yd5jdndl57Mzu7IZN8N69j3PP+c05587s3hWh7dc2ASMEGCOTjM7xN37RAnMZhJvlZL" + "TVBeYGi58VRvUxeIDBzK/QfqOsvSWq36IGy+EAEAKjgujPqtmgCCdyYgiW1aAsgUOAsCH" + "vUAUitVszHOkkAMU7W28cvDMgbTaj7jJDCJWxBQWztE1fifUNh9GYYbFDYK/+UxcMPTO9" + "yaFfG/kZgcamfuIt/aSXlR9hcStAQqiZQOq/9vdpU3SIGw7kFAIGLnn8ChlwnCjkm57UD" + "5Ci+nQ2xAUn5C3NOtdMzHACKV5AhnJOKLfEfWUspwSAHtZjRnAcN5KwdXuOlWDMiCo54C" + "RhG8pDuuBYCWbTKKvwkE3NACDNcKwHI3fNzW3TC0gTnHQAQzHrAaQJTuj+hcq3sBQslL0" + "lGgBtvVOuqcKB7Vp5usk9iWETVFrLNh8TDgmnFiKJDUpLt3/INq92Jx0TTujhMd2wiPao" + "5R9FOM+72r/hXS5OlJSmFezkm5VMk4XzeOYBu15a9PXLhrqhQH7ekNLktGiPEV6ycBjGG" + "aTp3fVqpfvzs4denKvqRlkZs5bDwJavILuAkvdEwQGvIfeT5G/zxaPMPO8bRdXzXza4No" + "6W3hUyMjy0L21KBe+JgoOQg5UzWkCuwkW28pS78cIsV7a/T25MKrfJeU8UHIZBigkKjPd" + "n5ZZ76moqlxrqu/ndu4bNBZKkuAIjiPdE2hIG5/HMVEvkAKX363teq35+5Vzx0qkPf8TZ" + "mU+VxulqTyIb0PPaw/oWqb5hcBiGqZB2qtV5lJG/XlJS86zpM447UtpJ8tGy2hw792Mm+" + "C2JqGIYHGkiFkdoqAhMRrHneGWt+/qFaV/R6wMapthzSERoiXA2d6n4dPa/klu+ePpk+d" + "Llms7A7l3j8UlLzmzpI4UIR2mX0qsiz2Rlr+/dX+u+cr5g9cSxAcHpnNMuI8lJBxQlX/N" + "QfSVwaJM5JY9cBSuHD7Oery6tce8UdQqZmeqPIjZgI807Ihy1LdwoMp8j55DnZFWt5+rZ" + "0cDePSqPIragw1JbRTi0warStyPvA/flT46+uPhxt39n7qRV65gpN2FwQGmy9Wd79xVWu" + "69/mrNa8f4An+FaMNMYs2UlFA5VHh5FVt47wi42XVzg3j7QJ340YoOoojpC6ZK+SXTd58" + "p511P1EdpxbH4g5+5gDmK8+YnWIdZ6SfGcSIW8O/exy5cq16YP8rbQh+qXVM8BJZzIvzA" + "mDE904K4qcuTLVq+kwpnFI6MdQk/OGvJW2YpKSJmkwBHQ6tB3gTb+EfNvhR2hUJ0SCodB" + "/NxQoH3sF+bPM3YLIQpEWiYEDskryxNoZPQnvq+QYzbOSBWwc12EgzFqteIRYkGYHP8e3" + "8l+ibyVdgZBdSN7Qiuti3Bog1mlA60N3xbauBn8hDVLZqLliHAOlLzVMjc71RyvAg4UmB" + "sReqe68GBNvLKSPT/szoLAMXwD70S+tSl8H7bmAg5tvJlsw4yuf7OsXWQieg4IM5p3/hN" + "W7rXhW2geLZwwqpQt5pED31I9wuAgJJBOh+bQcuL18R/wt2t/4UepDSVEBE7CS+GILkQb" + "tYQWyStPx/AfU718X5mP4Qrp3FQvpSEFtkR4TuzQcmI/N43uj3fg3jwvJrf8UWhTFw8xJ" + "cxrwBJZ8+S8J4BW+27zt5gnaD4tQijyMsqdVY7yHJgkTcwu5J28x/dM/YZGzkcKTJv3JB" + "HLHeKWhUMT84Twe08v/rmIQ1z6giFXODIR04suG1bQCd8bk5sezTsXFZhqJeQapZ8hKcI" + "BI+F3A6lmrF59I3co6fyYH0tCkpIOTre6mn0x4UCSArdLNyhBexSSsNTWmGFFBwYPNEec" + "QKB9KVkSMDdLO1SjQhMcAJA2gDSCAZtjhhUMoC8tpOlYO5dK27aczprhwGS1BCa3gJ3a5" + "O6CY+mnOayoEDjcEzx5mUo5SEcoUTuh1A2HTk6ZHGQQDNipK6woGCghBxGyrdI2u9VBv3" + "hypWHPoSCCYabxx110TiJKvflFTqe44YDQ4CFDcpbODs9i4C1Kz0pyAGK1mQJHukCychF" + "AQYJD9qMHqX566qbDgcUT6UlmekokOEvgSBexApQVXiLVmdYth0MXoiX9fUHwSKuWeyWy" + "FSfjf12i+m6X2wT0E/gfn/18pZirCc8AAAAASUVORK5CYII=" + ) + return DASH_PIE_PNG_1 + + +def get_dash_pie_png2(): + DASH_PIE_PNG_2 = ( + "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAEcAAABHCAYAAABVsFofAAA" + "AAXNSR0IArs4c6QAACQ9JREFUeAHlnH9sFEUUx9/uXXtXKHCWlhatWEoFKddGkWDVCoeQ" + "AELxD0QgoIjxZ4yJJmIIicFGJcZo4l8kBhJjAlhA/hABIeHHIS3yo0SsRyUCpWApx6/Yw" + "PW6pXe37ndxjuuy1929vd9O0s7d3Mzse5+8ebMzO285SmJaufnUx7hcx1VabeSyeXbOPX" + "yoeBBtPl844WMjbc3U5cw01tMWQG7c5Kb2CKKr01MkN2lv1dPybh2H1Ax/SPc7rxGDlWh" + "QCYGzcusfrhtdltUMSNc1Ivy9NHiXrOBHZcflXPlvQ12g/VDF5TJluddTTpf/KJeLu/Yt" + "kkEBVhiUI1j/+YIqt7Kd2e9xhcOgXLk4yNVxalAYyFRHJ01xXNKUNRocZUMGi4EqneCn4" + "lF+eejF05riBuf1da0HGJSHus7QuyMO6gISqbheOJFtAOr89nlkv+WULWrMpOvu4XGyJN" + "Nw4FPgYFsPFFGsUJiyG+r6pGHlLWPfjeTMmqhlETFLWvd65TQjfSjrmoIDaznXXOiCg13" + "/wHrDlqIUxgwc1lekJT0++7rbDKCY4MC3dHj5A7CWOv8uiuZgmcB68w1zJct5ODbLUV5D" + "tqSNq6hy2jUqLQlNi8Vh88pOtb4DTGvLkAOHtxTRl7nr4wZG67pGfy9xttHIJWsIckJey" + "G20D0NwGBhL0yX6a0K96WFkVFij9QFo2vrX6OzFtpgA6YYTCWbr2E1G5dRVX9RVy3ilJ1" + "d9HRMgXXCSAeaOyonCQxQLIE04yQNj3CKMtjAKaEA42QSGgTQCaEA4mK497kGUKB/DBL6" + "bh+5+TOAnAIJe0G+gy0SFgxs83Mfg5i5pKTlsZHXGv7eGoB/0jKafKhwsCXDnixs8PQvG" + "aJ2nczmmeWGIh7AehPtQk1UVDtZKWBLE685X7cLpUKblf+6Bw4bTp7MONQatViEdlEikD" + "KPnbSdsr2D/SXmdfnBgXjCzUflXaMF8f23fsqcbA4WORmWjbPo+0PDqBwf0QPHdeedk/W" + "1DemdwS6ucvS88sZtsOWcTDiVx94ADih7NesJwIq1mivNWuDOe+hy5pdys4NuTrT2Txv4" + "Yysm5Hv4xSz5Es54wHArxLt/VQVRdfkVVZZ6CZfbagueDb9ScFcY9tFe1UgYXqllPGE54" + "hlp0eUAVrTl9NbbZI2f4l0/ZHSwYdmTAyoZ/TNG4kuSE9eAhAB4KMLFlOLivwWOTpdNbW" + "Llmnjfs9ixxWVWFUDdxp2jP7dBsoKdC6tjI0jmmNxA4gAcKZDh4rgRqU50+uZLefxYxUG" + "gbkzsn9OYkQXh87A7JH3XpbZuO9UZW3bEe8IB8MhyYEuBEOmIjwvNcqML2TMHc4Gs1p2+" + "PedBtpG061VUOLR6z1M2rg6l6tLojNiK81dZXw9eV1QhLntoRKBh20kjbdKlrL/cQeIAL" + "rzVLGRXaQn12W1FoLvdyVUng2UfcIYulXX8fKXY6kqD20R7CrA0u4dlKvwL6avIUKLFUO" + "1zBd2p9QvWYHaHcXO2lSOrZUKTf4fVO4fqQ3FvLyvc6bc8WzQ0un9QcKB6usRRJPZ1Iv2" + "O9V53ElFjzQrXBxZWC4A3stuzylObclJ7fpnlK2LBS01v2RyXiLP7V6vzAlHHuYI7Vq1Y" + "vXcqSCocpjaWIZeJ9LvGtp71C1ei94a2R1I8qJqKcJ21Y9bvqf18slt5HLdOLqXfyKHdo" + "hyefuO5CtXqpKkuJ5SiVlbZGXL1LJvpOjw+mhTxMvpRaDoQIkdV7kL/w+2bx8EyK6VgDU" + "yX+eUrhHONuNG+ho/k+sXtm/FUz32NK4Pg4a+NasSnYRhenmlchcT0kFU6Qs7ZvE8+d2E" + "/H5qfbEFJDnBQ4IcrpauL+bt5GzWUC1zNfTZB0LLM898o7XLdvsKv9gt/wfo4ehU5xPSe" + "/4nYLzXR2coACBXrapLLOb9/PoJI8J413+usTZjk9XO6RtVyjcEa84EqlsmauzePcLg47" + "b9hXbaafcNsQWdq3896d79PGmkwGA4XiZjlByvEd4zubN4tHSwSxZ06YVoZ9wMHvyhevy" + "TEW8h1p6QiqL6sk+qRhZEyqdHHcvs+4/Ue/E90ugXoeiamTNGiEE6gIG0BsBcS5Yzl8yJ" + "0/wr+6pa1YKhr40QwasdTL5Z38hmv0tYpt01lZJuc4CV8hhQrgBDz0kC0HZ3SHjuimlvO" + "Ao50kv9Kxj7+0Z4W4xSGBqdVukRk1hDYngQM7sxxe6OkZWpJfEY5yN46s4H/ybZXWQrc5" + "oSwz1NaWElM4XAs4sNphOFqzlk+07P2Cczd9S/trusXujPUrTHFlLpx3kuRapLEUcrPfw" + "nBQoGY9t3mbZxPXvvkDvmHGBerMCt/ClGc5HDGibhCWxIYUfusHB9RA745jJnLznXs+FH" + "+w/0LHF7KOsjGHI0akDXPETMd+cECt0tldfzOvmJ5o+IcaxKaZAgkVrHI25vJywe4k6B1" + "pNdC1HxwUMN+Dm6H/Q4KeWCGoRfjdAwdAEILjdPnp1zXvZTUf6Ac9oa+aoqpw2PAqkcwN" + "ZpeNKZoTjtRVFQ4qwMwQL4mwQHSUTQn6XJYC1bQi+TS3tHH09sTPhS4EduFRaaYnvWCgZ" + "1TLYRAQI4lQwD+/XsWKMjqPNm2rKaUJB42yxUHDAVeMKqfK6lu6Yj41hxUjisM8iJVESC" + "BOXmbSEMNQguVjZtILBnrrhoPKDBAOcmeKD2I+JpYoYUNwGCCcdIeTpuoGemxx+h5Jlm9" + "DpNlWa1aCXmrJMBzWCWYxhB15BY8cP8nK0yWHf8F9Gm5HYg28jxkOIOC8bqtn8Gr4IZyl" + "SwcrgrVgSSD7F6yXTLxvxxQcGdB/jhqHDBGjhQi4VDhrQMGeDKwFa6VY30IQafmm4TBAO" + "H3JXviBCLhkWRKzFOziYbtFXl2bsJa4w4nsEL4IMVvMknCuN96gmJVgzxdQZEvB9qa0H6" + "XcdoiUzejnuFiO8qKY8pkl4cAzA4V6iC9AwpFWPcNPnooj3rqEtsxKsBmO3UszfgX9RUs" + "JgRN5sUhQKGfv7UI4Af60kvJ9Xagvb4LH2UrU5Eg4HOVFWUQKe8mZ8nfld/byMpQnykKU" + "12Tf/wU491oPtPXjKQAAAABJRU5ErkJggg==" + ) + return DASH_PIE_PNG_2 + + +def get_dash_pie_png3(): + DASH_PIE_PNG_3 = ( + "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAEcAAABHCAYAAABVsFofAAA" + "AAXNSR0IArs4c6QAACThJREFUeAHlnH9sE+cZx587O3FIUuIxnARKISQRtImTdgy1gbIQ" + "IAKmAv2DsaWiW9UJbaumSZ20SRPSpk3b+GPqH/zVaVKkaVLpAqhSoRSxKgRXJClh6caPJ" + "KvWNE1oSAxetSS1k3Ni+3bf13nds3P2nX//2CvZ53vv/fE8Hz/v8/64e0+w/OqPMmUotF" + "RuYjU9JgePRqudFWZpjmZY8hsPJ4xmSzqdOekSdAoAkNVkpQq5gqbu2ljq/hGdTBGXrTY" + "bWYNZ6Zh9E3FY6QaVFjgbyiqooawmBOS+i2hY+Xy37DJT+73Gv0eoHzx947Bv/Hr9dE3k" + "RedQLU3frWXR4+c6FFBBWBzUiGecJj2zkdmSPk8pHA5FeLCeJgdKQ0B2W6eotfF+wsJW2" + "8cIHxZe6CYOi4NqbFxPDVVTrOml0ppSBmd/5ZPEoWya+Zheq3w/KSCxSIZgLYMavXiESo" + "btrOntb7JSqiwpaTjwKXCwIz02CkHZkqiVxN83BEGdDlnTTE8HcUt67+HtWIx1ryUFB9b" + "iuVtPcLCdj3ZSa8JQdOXUTcCtydk0RtyS9u8lSgZQQnDgW3aUPsms5fD8ZYrmYHU1ikwQ" + "v+FElsB8U7U9aEn3z5ykY3sq6IP52wk5bHFF6ToRANPo3kn952z0WnEn/bJGu+fRKSbtl" + "2FJ646fYnJCXsgdb4gLDgdj6rtP/278DbVaE/Ut8YqZWHoA2tN5gkbvjbE/NF5AhuGowZ" + "zf8mZi0urkSkGr0qxhx8nTCQEyBCcTYIJapQsPUSKAdOFkDozmn57SyHgBxYRTSGA45Xg" + "AxYSD7nrIUUrp8jFc4C+PgS9/pvEXAEEv6BcrRIWDAd7INRsb3MUqIKXXMsOGifzEq6eY" + "ftAzWtCEgykBRr4Y4OV6dx1NMb14dPPSI0NsPgj3oRU04WCuNK5MCXJ1gKelSCJxev5nB" + "RzenH538Hqv32yWEqk0n/JsPnKRJodL2fpTpNxhcGBeWHbYWP6Ajh2d37X00rO9vrXW3s" + "hMhXQeq3mFwcHqHSj+5MgnTH/LI9524cUmu/dbz1whS9Fo2qGkbwwYU/Ro1hOCo7aaVvs" + "XocJEWrIWbxAO+l952rywfcuFQFHRf0IXC+RHNOtRwbGS+2EpNdc+0FRZJH9Nya41z/t/" + "0DIqbd3UrZkojyO1rCcEJ9RDdUzHVNFctNRi+ea69vmXW6/411TciJk47otZaleKnLCeG" + "eUmAO6S8MDgYFwzNWSjF/fd4fG6x1UViwfll5rqpcPb3pVLiid1MxhJkD02TDrrvi7GAT" + "wQGBzcVwK13XY3izT6ZZJ9ay11xc8Ffrhdkr6+5ZLij4J33owWkGPp1ilLrOAAHggMDkw" + "JkWpHHI/cohCot3xjzSH/iZaPFusec8STN5fSRjYtEb3U3MMyat6s7YjjEd5sWWoRD9e0" + "SMd3XvKtqbgVT95cSVtSO8R4gIsCJ3YvFa/QJloqsdgCh4TvNVX79j7uCJhM48bLyLLTU" + "QQt2TzEem1wCfVWxhUwllIkX7Wp2drm//Eut9RcdylQXKw/Fck+G1L7HdFoF24MycpUZt" + "Frt+y1HfK/vH3QV/VVnalI9umo/Y55pTrpiTGvCuzyv9AgSU7fFdPloQ1Fc1/Y01NT6kp" + "NW7PSEpH5o2r5oPj95nJf61aHv8js1EqXK3EZhcOVxlTEtO0rbfKPnnVKTZu7Q0sj2W9V" + "XER2zFizCqt1+cRk8j5l2ldF3qc3OgKXhspJ8KzVSpetuKxYTqSyytJIm/f4NvdHT/hzQ" + "h4uX1YtB0IEyOx8X5y4fVbuP0ACFys3jlmFc1P4fPAcDZS7Zc+B3MARLkVW4LgFc+/rcp" + "9/jO7tDhcnt84yCscvmMffkj/5sIduHs21JqT1t2QEToCKZvqEzwbfosEaSVg4qiVILsa" + "JnwkTVNNA9NuudWmRb1hYuPUL4YLzDF1vl2ihPi2VpLDQf/61nfEAl7RZzoJQfON1oVf6" + "WJ5oS6HsGS1KxHO76+0ueuNqc0oqDpBp/KLofPendKYln8EARsosx09F7pvi1OBZeaBak" + "heeSwnpLBQyc7WDGr7tovOK0bARabJ+Z0YQrv5e6Bn4i+xoU/zK41nQKSVV4sl47LHA3g" + "oEZjmTnhlqrJynO2NVSlTsWzMs1/KXV1h1609Cr3tEHtunjs/X358qT8LXN87TsLKXAmE" + "ZziztqPRQvwNw9IPiVyavifeGLwT+sXWRpKf0c+RHCmnMTqu3u2jSFbSc0ETPSNNS/Io0" + "IHx+4+fiO+7zylxoUZBq8kNtfSnVXThPHYKj12u5ZVP3HwRH35+pp8Uje/LWr3DFI4/Sp" + "3YqV1wLXAwPITiI0LKeRdEy9KYwfvZnYlf7BE0VhG/hyvMjHHGJsmorK9uS1Pu2wrrySM" + "fsEKf+9rb8YZ1E0nd4QYV4jHTEXMcwywG12aoRmltVRc90/Ze65L4DCph6nrgQj/A11SV" + "2prfaaqBrGBxEcN+DwdD/Q4CemCFo7fBbAQdAsAXH3jZPH5x6taD5QD+mp6KvVtCEw5sX" + "zA1mV4ghmhNW66oJBwlgZmVNymOAdzrYFkF1pnz/zTbQKhvVHt07GnMnX1Q4AIAtgChgW" + "ikIBRZCMAoGusaEgwQA1LDHRf86fRKneR/QbW9Q5k/YQawXdOGggEJx0HDA9Rtrabi8P2" + "ywFw2SIThw0CgQBaOCfGtikPfaic64wACYIThIqAaUTz6I+5idygKWUYuBvggm8+5Dv2a" + "/DHzNLXnJW+6iuq0Bmrr4PDld8+xhHwNZs5IEwxB39yusU3nHdZMgfzzBsOXwQmFBvBez" + "Tnfk7EARzR/y6XXXXC+to5DM+3PwvG7Fgwa28xbP0n1NebdEtgOsBVMCjHwxT9SaFhiVM" + "Sk4qARPXWJTO7YGYI8WdsDh0bFMB0DBmgxG9ZgrJfoWArXcScNBYQCEpy/ZCz+UrY/YAZ" + "cpS+KWghuTWKxK1lpSDkddIDazYc8WtyQ815tqUNxKsOYLKLAULNRhPQo+MVUhJZYTKYz" + "akvAAOAeFdNhfgIBHWo00P9YVL791iS+jcCtZrdwUAJRk/AoTJspXWuCo61KDQjw2oCBg" + "OwE+egH3kfj7umAhCOmwEi050g4nslK+I4W/5CzyeuQ5f3kZ4tNlIZF18vP/AXYR+dvV3" + "FCCAAAAAElFTkSuQmCC" + ) + return DASH_PIE_PNG_3 + + +def get_report_favicon(): + REPORT_FAVICON = ( + "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAA" + "AAXNSR0IArs4c6QAAAM9JREFUKBVjnNS8goEUwESKYpBaFiD+z/hTJz2EoM4rM9cw/mcH" + "aYCA2Nl3YUwEvTBFkZGREchnBEIwAGv4z/6f4T8DWAIk+P8/RA6ZBCkAA4QNi1IUkVVgY" + "f9nBwpCNTiLcGNRgSp0FcxF2CC3RhVVAcOjkNtAEYg4hA3kIjRAPYWmCeRdFIDQ8BBsHo" + "okmIMmDtXw5s0bTKVoIhCrQBogLHaPUGQVP7avgnA5PMOAjJ87VkO4ZCUNiFa4GRAu3K9" + "o4lA/LJ+xF6KOIEmykwBQHy74EMZM3QAAAABJRU5ErkJggg==" + ) + return REPORT_FAVICON + + +def get_side_by_side_png(): + SIDE_BY_SIDE_PNG = ( + "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAA" + "AAXNSR0IArs4c6QAAAIVJREFUKBVjvPLvPwMpgIkUxSC1LBANOo0oGq/Ug7hYBUm2gWQN" + "jBBPa/1lRHbTNWZQSGAVhPqBaRWyeoYrkSAuozaKIMMNEJdkJ5GsAeqkf2Eotl8D8/5fR" + "RGEcGB+aEGJb0g8MGEVxGIIXiFy/eC8CuJsmOH1WkAWVkFoxMEUEqZJdhIAo3Aj/iHmzl" + "MAAAAASUVORK5CYII=" + ) + return SIDE_BY_SIDE_PNG + + +def get_no_screenshot_png(): + NO_SCREENSHOT = ( + "iVBORw0KGgoAAAANSUhEUgAAAIwAAAA8CAAAAACRYQ2XAAAF10lEQVRo3u3WeVATVxwH8" + "AiGIDEkEEjEoCCpXGVsFI3EE5DBitd4Y6nngK1a79pRUYeq1FIto9JWO9ra4mCZoiCKR4" + "tarMihYAEJJIpXNgmQmJBAIHe+/QORadC2M870mNn9a/f3e2/eZ/f33ttHwX/oopAYEkN" + "iSAyJITEkhsSQGBJDYkgMiSExJIbEkBgSQ2JIDIn59zA6RbP5H8Los7bkWQHAdDyt+SXt" + "avfEDA1OOiZ/nbFUn33R1TdqNZidMYrRlKDbAKCfwqzr2+PaCCpPGDqQmtDwGpjGIKGmb" + "/RO4jd2J4wyikJZ1AagPZ59r0+HZwnuaU81rdenUFJeo1SSYJG2b7TIfYPDGTN24BCP47" + "0Ys/LRk97XqOWKVABwk/OmDECHrInoBGwarV2jtALWZkJh7G5pkBNqBwCLqsOhJuSdAGB" + "rJQiVHZAEj9NqCaIdAGCUP5TpAIchx+O9FosTRhiUzhop7cHc/2AYx1d0qqfElaxxLQCg" + "O/SpCiidNciHt6gazQsTs8cKCDzYyucMXVpqA6wXZvtxBOkEUBO140sBxz+xDlCnh3K5E" + "ZntkISMOjyW4zf9hgOoT/HncGN/MJu38Vy8hNXOmGE171PWGrsx8gS3Obs3D2MesnVnm8" + "JoS660WgDYgV9DGMtS51Mny+WRnjxufOvjONr01JXM4GtAHtdvzbZJ/ZN0KGOxeYt3jKc" + "s6LCl0d/enRpFOwhJmPugWamzXSfI8GDSgPlp64d6f2fNm08dk/bYGRPYJB3BKkB7PLse" + "B11W6IGr/nxxd9aSNYjiEbZ83+VmoCOp/14zNHNpZxRCSqJEa9lJ3aiD9RA1yfRkxOCLg" + "HKm22mUe3seM6NmOL9BPT6wDqiKmN0hDaPuMUAxmXkV21w3G4BCL4ECl+mbnVeTUhh4H1" + "+7x7QY4tli/QT2bQDGpe65z9Om0r0iH9d+nhMvQcofLgVQtqeSGM2rBBRRgTWwOWTh/NY" + "c6mqz3YYztFWOcla0Bmifyi1XTxiwX2EyN9TbJSEhUsC62qNQNX5IFQBTIq0QRfSN9r4Y" + "KXQLXfa1TWWLiYDRrQAcabSs3lmvFWd/OJISIalkxeq6I0TkWy1ALY/9bkpyynIeW7zPR" + "ZicnJwyq9/0zgpmkgnomMG55cjyceUvTi8zQRIs0gC2TR4FDcOi1ACwg3LklRhU83kXEt" + "gNTbxoDQD77ucYx0+ZjwDALp5Ey7nBmGHowYxUAVW+jIkx0dExcQubUl2Gx0RHR8fG7TR" + "VMJeZgY4ZnFJYSlIFTCp3u1EaLNJ2Y2p4E9sAYDsl69UYWwZ1ushHrOSHEwCMK3rKtIly" + "FABg30Q9WsUWPQPw21f3iEiBChAHhorb9Xrt3WpLhstHOr1eryxvQnkP5paxUQV1xX4er" + "/r+C8yDEAEBwLyYdv7VGLROdXXn1pvmuZ0CIA0PqO3OnugX3wwAzQnuZ5RCn2tA10pmvi" + "JSoAI08Yx8ALWjEw1FA2I1AI7xs3ox5XWj1poA0wJWyYMeTH77NM8CADWDwwgU0Tf2mcB" + "jAqUAUOJP4dzDOa/Q7xt/eYe65fl++zTaZW5OcXH2HGqMzHGAFpdf8rGnSCaPFKgAfEuP" + "Olt3fppbJnTz3NaV3s0cHFL3AuNbphzne+ThoxOBo568+DJncZIe+WNj8UzqJzYUMybmq" + "Z0w44PvA4Bll/sQMYwZXGbQIMYSRY/59jQ6neVFZ8y9B2g3env7McZchzxKqALQmcH1Cv" + "TxXqcFGhPofgEDwwuBCt9kM2CY41+G/ADGG8M9A/IgjZisBWxbvQrQtc+HFcT1TFYDD0V" + "uAeV/xHRdKujeppW5eTrAcvPA+vSCtt61pD63d8WqzMs6AOj8+Wjm6UdA58UiIwBYbny+" + "9fDVTgBoydm562Q9AFVumR2wluSp4LhzaPWarCoH9IVXzICj6rQMMF0/sGF/kR6A4252r" + "uqvzjNWp2eH+U/+kbYXd3b7S9IWy98YgDx2/k8wvwNEPGrBGochUwAAAABJRU5ErkJggg" + "==" + ) + return NO_SCREENSHOT diff --git a/seleniumbase/core/jqc_helper.py b/seleniumbase/core/jqc_helper.py new file mode 100644 index 00000000000..49bcf8cc139 --- /dev/null +++ b/seleniumbase/core/jqc_helper.py @@ -0,0 +1,308 @@ +"""This module contains methods for opening jquery-confirm boxes. +These helper methods SHOULD NOT be called directly from tests.""" +from seleniumbase.fixtures import constants +from seleniumbase.fixtures import js_utils + + +form_code = """'<form align="center" action="" class="jqc_form">' + + '<div class="form-group">' + + '<input style="font-size:20px; background-color: #f8fdfd; ' + + ' width: 84%%; border: 1px solid blue; ' + + ' box-shadow:inset 0 0 2px 2px #f4fafa;"' + + ' type="text" class="jqc_input" />' + + '</div>' + + '</form>'""" + + +def jquery_confirm_button_dialog(driver, message, buttons, options=None): + js_utils.activate_jquery_confirm(driver) + # These defaults will be overwritten later if set + theme = constants.JqueryConfirm.DEFAULT_THEME + border_color = constants.JqueryConfirm.DEFAULT_COLOR + width = constants.JqueryConfirm.DEFAULT_WIDTH + if options: + for option in options: + if option[0].lower() == "theme": + theme = option[1] + elif option[0].lower() == "color": + border_color = option[1] + elif option[0].lower() == "width": + width = option[1] + else: + raise Exception('Unknown option: "%s"' % option[0]) + if not message: + message = "" + key_row = "" + if len(buttons) == 1: # There's only one button as an option + key_row = "keys: ['enter', 'y', '1']," # Shortcut: "Enter","Y","1" + b_html = """button_%s: { + btnClass: 'btn-%s', + text: '<b>%s</b>', + %s + action: function(){ + jqc_status = '%s'; + $jqc_status = jqc_status; + jconfirm.lastButtonText = jqc_status; + } + },""" + all_buttons = "" + btn_count = 0 + for button in buttons: + btn_count += 1 + text = button[0] + text = js_utils.escape_quotes_if_needed(text) + if len(buttons) > 1 and text.lower() == "yes": + key_row = "keys: ['y']," + if btn_count < 10: + key_row = "keys: ['y', '%s']," % btn_count + elif len(buttons) > 1 and text.lower() == "no": + key_row = "keys: ['n']," + if btn_count < 10: + key_row = "keys: ['n', '%s']," % btn_count + elif len(buttons) > 1: + if btn_count < 10: + key_row = "keys: ['%s']," % btn_count + color = button[1] + if not color: + color = "blue" + new_button = b_html % (btn_count, color, text, key_row, text) + all_buttons += new_button + + content = '<div></div><font color="#0066ee">%s</font>' % (message) + content = js_utils.escape_quotes_if_needed(content) + overlay_opacity = "0.32" + if theme.lower() == "supervan": + overlay_opacity = "0.56" + if theme.lower() == "bootstrap": + overlay_opacity = "0.64" + if theme.lower() == "modern": + overlay_opacity = "0.5" + if theme.lower() == "material": + overlay_opacity = "0.4" + jqcd = """jconfirm({ + boxWidth: '%s', + useBootstrap: false, + containerFluid: true, + bgOpacity: %s, + type: '%s', + theme: '%s', + animationBounce: 1, + typeAnimated: true, + animation: 'scale', + draggable: true, + dragWindowGap: 1, + container: 'body', + title: '%s', + content: '<div></div>', + buttons: { + %s + } + });""" % ( + width, + overlay_opacity, + border_color, + theme, + content, + all_buttons, + ) + driver.execute_script(jqcd) + + +def jquery_confirm_text_dialog(driver, message, button=None, options=None): + js_utils.activate_jquery_confirm(driver) + # These defaults will be overwritten later if set + theme = constants.JqueryConfirm.DEFAULT_THEME + border_color = constants.JqueryConfirm.DEFAULT_COLOR + width = constants.JqueryConfirm.DEFAULT_WIDTH + + if not message: + message = "" + if button: + if not isinstance(button, (list, tuple)) or len(button) != 2: + raise Exception('"button" should be a (text, color) tuple!') + else: + button = ("Submit", "blue") + if options: + for option in options: + if option[0].lower() == "theme": + theme = option[1] + elif option[0].lower() == "color": + border_color = option[1] + elif option[0].lower() == "width": + width = option[1] + else: + raise Exception('Unknown option: "%s"' % option[0]) + btn_text = button[0] + btn_color = button[1] + if not btn_color: + btn_color = "blue" + content = '<div></div><font color="#0066ee">%s</font>' % (message) + content = js_utils.escape_quotes_if_needed(content) + overlay_opacity = "0.32" + if theme.lower() == "supervan": + overlay_opacity = "0.56" + if theme.lower() == "bootstrap": + overlay_opacity = "0.64" + if theme.lower() == "modern": + overlay_opacity = "0.5" + if theme.lower() == "material": + overlay_opacity = "0.4" + jqcd = """jconfirm({ + boxWidth: '%s', + useBootstrap: false, + containerFluid: true, + bgOpacity: %s, + type: '%s', + theme: '%s', + animationBounce: 1, + typeAnimated: true, + animation: 'scale', + draggable: true, + dragWindowGap: 1, + container: 'body', + title: '%s', + content: '<div></div>' + + %s, + buttons: { + formSubmit: { + btnClass: 'btn-%s', + text: '%s', + action: function () { + jqc_input = this.$content.find('.jqc_input').val(); + $jqc_input = this.$content.find('.jqc_input').val(); + jconfirm.lastInputText = jqc_input; + $jqc_status = '%s'; // There is only one button + }, + }, + }, + onContentReady: function () { + var jc = this; + this.$content.find('form.jqc_form').on('submit', function (e) { + // User submits the form by pressing "Enter" in the field + e.preventDefault(); + jc.$$formSubmit.trigger('click'); // Click the button + }); + } + });""" % ( + width, + overlay_opacity, + border_color, + theme, + content, + form_code, + btn_color, + btn_text, + btn_text, + ) + driver.execute_script(jqcd) + + +def jquery_confirm_full_dialog(driver, message, buttons, options=None): + js_utils.activate_jquery_confirm(driver) + # These defaults will be overwritten later if set + theme = constants.JqueryConfirm.DEFAULT_THEME + border_color = constants.JqueryConfirm.DEFAULT_COLOR + width = constants.JqueryConfirm.DEFAULT_WIDTH + + if not message: + message = "" + btn_count = 0 + b_html = """button_%s: { + btnClass: 'btn-%s', + text: '%s', + action: function(){ + jqc_input = this.$content.find('.jqc_input').val(); + $jqc_input = this.$content.find('.jqc_input').val(); + jconfirm.lastInputText = jqc_input; + $jqc_status = '%s'; + } + },""" + b1_html = """formSubmit: { + btnClass: 'btn-%s', + text: '%s', + action: function(){ + jqc_input = this.$content.find('.jqc_input').val(); + $jqc_input = this.$content.find('.jqc_input').val(); + jconfirm.lastInputText = jqc_input; + jqc_status = '%s'; + $jqc_status = jqc_status; + jconfirm.lastButtonText = jqc_status; + } + },""" + one_button_trigger = "" + if len(buttons) == 1: + # If there's only one button, allow form submit with "Enter/Return" + one_button_trigger = "jc.$$formSubmit.trigger('click');" + all_buttons = "" + for button in buttons: + text = button[0] + text = js_utils.escape_quotes_if_needed(text) + color = button[1] + if not color: + color = "blue" + btn_count += 1 + if len(buttons) == 1: + new_button = b1_html % (color, text, text) + else: + new_button = b_html % (btn_count, color, text, text) + all_buttons += new_button + if options: + for option in options: + if option[0].lower() == "theme": + theme = option[1] + elif option[0].lower() == "color": + border_color = option[1] + elif option[0].lower() == "width": + width = option[1] + else: + raise Exception('Unknown option: "%s"' % option[0]) + + content = '<div></div><font color="#0066ee">%s</font>' % (message) + content = js_utils.escape_quotes_if_needed(content) + overlay_opacity = "0.32" + if theme.lower() == "supervan": + overlay_opacity = "0.56" + if theme.lower() == "bootstrap": + overlay_opacity = "0.64" + if theme.lower() == "modern": + overlay_opacity = "0.5" + if theme.lower() == "material": + overlay_opacity = "0.4" + jqcd = """jconfirm({ + boxWidth: '%s', + useBootstrap: false, + containerFluid: true, + bgOpacity: %s, + type: '%s', + theme: '%s', + animationBounce: 1, + typeAnimated: true, + animation: 'scale', + draggable: true, + dragWindowGap: 1, + container: 'body', + title: '%s', + content: '<div></div>' + + %s, + buttons: { + %s + }, + onContentReady: function () { + var jc = this; + this.$content.find('form.jqc_form').on('submit', function (e) { + // User submits the form by pressing "Enter" in the field + e.preventDefault(); + %s + }); + } + });""" % ( + width, + overlay_opacity, + border_color, + theme, + content, + form_code, + all_buttons, + one_button_trigger, + ) + driver.execute_script(jqcd) diff --git a/seleniumbase/core/log_helper.py b/seleniumbase/core/log_helper.py old mode 100755 new mode 100644 index d6c49e0fc28..ce537b6790c --- a/seleniumbase/core/log_helper.py +++ b/seleniumbase/core/log_helper.py @@ -1,132 +1,598 @@ -import codecs import os import shutil import sys import time -import traceback +from contextlib import suppress +from seleniumbase import config as sb_config from seleniumbase.config import settings +from seleniumbase.fixtures import constants +from seleniumbase.fixtures import shared_utils + +python3_11_or_newer = False +if sys.version_info >= (3, 11): + python3_11_or_newer = True +py311_patch2 = constants.PatchPy311.PATCH + + +def __is_cdp_swap_needed(driver): + """If the driver is disconnected, use a CDP method when available.""" + return shared_utils.is_cdp_swap_needed(driver) def log_screenshot(test_logpath, driver, screenshot=None, get=False): screenshot_name = settings.SCREENSHOT_NAME - screenshot_path = "%s/%s" % (test_logpath, screenshot_name) + screenshot_path = os.path.join(test_logpath, screenshot_name) + screenshot_skipped = constants.Warnings.SCREENSHOT_SKIPPED + screenshot_warning = constants.Warnings.SCREENSHOT_UNDEFINED + if ( + getattr(sb_config, "no_screenshot", None) + or screenshot == screenshot_skipped + ): + if get: + return screenshot + return try: if not screenshot: - element = driver.find_element_by_tag_name('body') + element = driver.find_element("tag name", "body") screenshot = element.screenshot_as_base64 - with open(screenshot_path, "wb") as file: - file.write(screenshot) + if screenshot != screenshot_warning: + with open(screenshot_path, mode="wb") as file: + file.write(screenshot) + with suppress(Exception): + shared_utils.make_writable(screenshot_path) + else: + print("WARNING: %s" % screenshot_warning) if get: return screenshot except Exception: try: driver.get_screenshot_as_file(screenshot_path) except Exception: - print("WARNING: Unable to get screenshot for failure logs!") + print("WARNING: %s" % screenshot_warning) + + +def get_master_time(): + """Returns (timestamp, the_date, the_time)""" + import datetime + + timestamp = str(int(time.time())) + " (Unix Timestamp)" + now = datetime.datetime.now() + utc_offset = -time.timezone / 3600.0 + utc_offset += time.daylight + utc_str = "UTC+0" + if utc_offset > 0: + if utc_offset < 10: + utc_str = "UTC+0%s" % utc_offset + else: + utc_str = "UTC+%s" % utc_offset + elif utc_offset < 0: + if utc_offset > -10: + utc_str = "UTC-0%s" % abs(utc_offset) + else: + utc_str = "UTC-%s" % abs(utc_offset) + utc_str = utc_str.replace(".5", ".3").replace(".", ":") + "0" + time_zone = "" + try: + time_zone = "(" + time.tzname[time.daylight] + ", " + utc_str + ")" + except Exception: + time_zone = "(" + utc_str + ")" + # Use [Day-of-Week, Month Day, Year] format when time zone < GMT/UTC-3 + the_date = now.strftime("%A, %B %d, %Y").replace(" 0", " ") + if utc_offset >= -3: + # Use [Day-of-Week, Day Month Year] format when time zone >= GMT/UTC-3 + the_date = now.strftime("%A, %d %B %Y").replace(" 0", " ") + the_time = now.strftime("%I:%M:%S %p ") + time_zone + if the_time.startswith("0"): + the_time = the_time[1:] + return timestamp, the_date, the_time + + +def get_browser_version(driver): + if ( + python3_11_or_newer + and py311_patch2 + and hasattr(sb_config, "_browser_version") + ): + return sb_config._browser_version + driver_capabilities = driver.capabilities + if "version" in driver_capabilities: + browser_version = driver_capabilities["version"] + else: + browser_version = driver_capabilities["browserVersion"] + return browser_version + + +def get_driver_name_and_version(driver, browser): + if hasattr(sb_config, "_driver_name_version"): + return sb_config._driver_name_version + if driver.capabilities["browserName"].lower() == "chrome": + cap_dict = driver.capabilities["chrome"] + return ("chromedriver", cap_dict["chromedriverVersion"].split(" ")[0]) + elif driver.capabilities["browserName"].lower() == "msedge": + cap_dict = driver.capabilities["msedge"] + return ("msedgedriver", cap_dict["msedgedriverVersion"].split(" ")[0]) + elif driver.capabilities["browserName"].lower() == "firefox": + return ("geckodriver", driver.capabilities["moz:geckodriverVersion"]) + elif browser == "safari": + return ("safaridriver", get_browser_version(driver)) + elif browser == "ie": + return ("iedriver", get_browser_version(driver)) + else: + return None + +def log_test_failure_data(test, test_logpath, driver, browser, url=None): + import traceback -def log_test_failure_data(test, test_logpath, driver, browser): + browser_displayed = browser + driver_displayed = None + browser_version = None + driver_version = None + driver_name = None + duration = None + exc_message = None + try: + browser_version = get_browser_version(driver) + except Exception: + pass + try: + driver_name, driver_version = get_driver_name_and_version( + driver, browser + ) + except Exception: + pass + try: + duration = "%.2fs" % (time.time() - (sb_config.start_time_ms / 1000.0)) + except Exception: + duration = "(Unknown Duration)" + if browser_version: + headless = "" + if test.headless and browser in ["chrome", "edge", "firefox"]: + headless = " / headless" + if test.headless2 and browser in ["chrome", "edge"]: + headless = " / headless2" + if browser and len(browser) > 1: + # Capitalize the first letter + browser = "%s%s" % (browser[0].upper(), browser[1:]) + browser_displayed = "%s %s%s" % (browser, browser_version, headless) + if driver_name and driver_version: + driver_displayed = "%s %s" % (driver_name, driver_version) + else: + browser_displayed = browser + driver_displayed = "(Unknown Driver)" + if not driver_displayed: + driver_displayed = "(Unknown Driver)" basic_info_name = settings.BASIC_INFO_NAME - basic_file_path = "%s/%s" % (test_logpath, basic_info_name) - log_file = codecs.open(basic_file_path, "w+", "utf-8") - last_page = get_last_page(driver) + basic_file_path = os.path.join(test_logpath, basic_info_name) + if url: + last_page = url + else: + last_page = get_last_page(driver) + sb_config._fail_page = last_page + timestamp, the_date, the_time = get_master_time() + test_id = get_test_id(test) # pytest runnable display_id (with the "::") data_to_save = [] - data_to_save.append("Last_Page: %s" % last_page) - data_to_save.append("Browser: %s " % browser) - if sys.version.startswith('3') and hasattr(test, '_outcome'): - if test._outcome.errors: - try: - exc_message = test._outcome.errors[0][1][1] - traceback_address = test._outcome.errors[0][1][2] - traceback_list = traceback.format_list( - traceback.extract_tb(traceback_address)[1:]) - traceback_message = ''.join(traceback_list).strip() - except Exception: - exc_message = "(Unknown Exception)" - traceback_message = "(Unknown Traceback)" - data_to_save.append("Traceback: " + traceback_message) - data_to_save.append("Exception: " + str(exc_message)) + data_to_save.append("%s" % test_id) + data_to_save.append( + "--------------------------------------------------------------------" + ) + data_to_save.append("Last Page: %s" % last_page) + data_to_save.append(" Duration: %s" % duration) + data_to_save.append(" Browser: %s" % browser_displayed) + data_to_save.append(" Driver: %s" % driver_displayed) + data_to_save.append("Timestamp: %s" % timestamp) + data_to_save.append(" Date: %s" % the_date) + data_to_save.append(" Time: %s" % the_time) + data_to_save.append( + "--------------------------------------------------------------------" + ) + if hasattr(test, "_outcome") and getattr(test._outcome, "errors", None): + try: + exc_message = test._outcome.errors[-1][1][1] + traceback_address = test._outcome.errors[-1][1][2] + traceback_list = traceback.format_list( + traceback.extract_tb(traceback_address)[1:] + ) + updated_list = [] + counter = 0 + for traceback_item in traceback_list: + if "self._callTestMethod(testMethod)" in traceback_item: + counter = 1 + updated_list.append(traceback_item) # In case not cleared + continue + elif ( + ", in _callTestMethod" in traceback_item.strip() + and "method()" in traceback_item.strip() + and counter == 1 + ): + counter = 0 + updated_list = [] + continue + else: + counter = 0 + updated_list.append(traceback_item) + traceback_list = updated_list + traceback_message = "".join(traceback_list).strip() + except Exception: + exc_message = "(Unknown Exception)" + traceback_message = "(Unknown Traceback)" + traceback_message = str(traceback_message).strip() + data_to_save.append("Traceback:\n %s" % traceback_message) + data_to_save.append("Exception: %s" % exc_message) else: - data_to_save.append("Traceback: " + ''.join( - traceback.format_exception(sys.exc_info()[0], - sys.exc_info()[1], - sys.exc_info()[2]))) - log_file.writelines("\r\n".join(data_to_save)) - log_file.close() + traceback_message = None + if getattr(test, "is_behave", None): + if sb_config.behave_scenario.status.name == "failed": + if ( + hasattr(sb_config, "behave_step") + and getattr(sb_config.behave_step, "error_message", None) + ): + traceback_message = sb_config.behave_step.error_message + else: + format_exception = traceback.format_exception( + sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2] + ) + if format_exception: + updated_list = [] + for line in format_exception: + if "sb_manager.py" in line and "yield sb" in line: + continue + updated_list.append(line) + format_exception = updated_list + traceback_message = "".join(format_exception) + if ( + not traceback_message + or len(str(traceback_message)) < 30 + or traceback_message.endswith("StopIteration\n") + ): + good_stack = [] + the_stacks = [] + if hasattr(sys, "last_traceback"): + the_stacks = traceback.format_list( + traceback.extract_tb(sys.last_traceback) + ) + elif hasattr(sb_config, "_excinfo_tb"): + the_stacks = traceback.format_list( + traceback.extract_tb(sb_config._excinfo_tb) + ) + else: + message = None + if getattr(test, "is_behave", None): + message = "Behave step was not implemented or skipped!" + else: + message = "Traceback not found!" + the_stacks = [message] + for stack in the_stacks: + if "/site-packages/pluggy/" not in stack: + if "/site-packages/_pytest/" not in stack: + good_stack.append(stack) + traceback_message = str("".join(good_stack)).strip() + data_to_save.append("Traceback:\n %s" % traceback_message) + if hasattr(sys, "last_value"): + last_value = sys.last_value + if last_value: + data_to_save.append("Exception: %s" % last_value) + elif hasattr(sb_config, "_excinfo_value"): + data_to_save.append("Exception: %s" % sb_config._excinfo_value) + else: + data_to_save.append("Traceback:\n %s" % traceback_message) + if getattr(test, "is_nosetest", None): + # Also save the data for the report + sb_config._report_test_id = test_id + sb_config._report_fail_page = last_page + sb_config._report_duration = duration + sb_config._report_browser = browser_displayed + sb_config._report_driver = driver_displayed + sb_config._report_timestamp = timestamp + sb_config._report_date = the_date + sb_config._report_time = the_time + sb_config._report_traceback = traceback_message + sb_config._report_exception = exc_message + if not os.path.exists(test_logpath): + with suppress(Exception): + os.makedirs(test_logpath) + with suppress(Exception): + log_file = open(basic_file_path, mode="w+", encoding="utf-8") + log_file.writelines("\r\n".join(data_to_save)) + log_file.close() + shared_utils.make_writable(basic_file_path) -def log_page_source(test_logpath, driver): +def log_skipped_test_data(test, test_logpath, driver, browser, reason): + browser_displayed = browser + driver_displayed = None + browser_version = None + driver_version = None + driver_name = None + with suppress(Exception): + browser_version = get_browser_version(driver) + with suppress(Exception): + driver_name, driver_version = get_driver_name_and_version( + driver, browser + ) + if browser_version: + headless = "" + if test.headless and browser in ["chrome", "edge", "firefox"]: + headless = " / headless" + if test.headless2 and browser in ["chrome", "edge"]: + headless = " / headless2" + if browser and len(browser) > 1: + # Capitalize the first letter + browser = "%s%s" % (browser[0].upper(), browser[1:]) + browser_displayed = "%s %s%s" % (browser, browser_version, headless) + if driver_name and driver_version: + driver_displayed = "%s %s" % (driver_name, driver_version) + else: + browser_displayed = browser + driver_displayed = "(Unknown Driver)" + if not driver_displayed: + driver_displayed = "(Unknown Driver)" + timestamp, the_date, the_time = get_master_time() + test_id = get_test_id(test) # pytest runnable display_id (with the "::") + data_to_save = [] + data_to_save.append("%s" % test_id) + data_to_save.append( + "--------------------------------------------------------------------" + ) + data_to_save.append(" Outcome: SKIPPED") + data_to_save.append(" Browser: %s" % browser_displayed) + data_to_save.append(" Driver: %s" % driver_displayed) + data_to_save.append(" Timestamp: %s" % timestamp) + data_to_save.append(" Date: %s" % the_date) + data_to_save.append(" Time: %s" % the_time) + data_to_save.append( + "--------------------------------------------------------------------" + ) + data_to_save.append(" * Skip Reason: %s" % reason) + data_to_save.append("") + file_path = os.path.join(test_logpath, "skip_reason.txt") + with suppress(Exception): + log_file = open(file_path, mode="w+", encoding="utf-8") + log_file.writelines("\r\n".join(data_to_save)) + log_file.close() + shared_utils.make_writable(file_path) + + +def log_page_source(test_logpath, driver, source=None): html_file_name = settings.PAGE_SOURCE_NAME + if source: + page_source = source + else: + try: + page_source = None + if __is_cdp_swap_needed(driver): + page_source = driver.cdp.get_page_source() + else: + page_source = driver.page_source + page_source = get_html_source_with_base_href(driver, page_source) + except Exception: + source = constants.Warnings.PAGE_SOURCE_UNDEFINED + page_source = constants.Warnings.PAGE_SOURCE_UNDEFINED + if source == constants.Warnings.PAGE_SOURCE_UNDEFINED: + page_source = ( + "<h3>Warning: " + + source + + ( + "</h3>\n<h4>The browser window was either unreachable, " + "unresponsive, or closed prematurely!</h4>" + ) + ) + if not os.path.exists(test_logpath): + with suppress(Exception): + os.makedirs(test_logpath) + html_file_path = os.path.join(test_logpath, html_file_name) + with suppress(Exception): + html_file = open(html_file_path, mode="w+", encoding="utf-8") + html_file.write(page_source) + html_file.close() + shared_utils.make_writable(html_file_path) + + +def get_test_id(test): + if getattr(test, "is_behave", None): + file_name = sb_config.behave_scenario.filename + line_num = sb_config.behave_line_num + scenario_name = sb_config.behave_scenario.name + if " -- @" in scenario_name: + scenario_name = scenario_name.split(" -- @")[0] + test_id = "%s:%s => %s" % (file_name, line_num, scenario_name) + return test_id + elif getattr(test, "is_context_manager", None): + filename = test.__class__.__module__.split(".")[-1] + ".py" + classname = test.__class__.__name__ + methodname = test._testMethodName + context_id = None + if filename == "base_case.py" or methodname == "runTest": + import traceback + + stack_base = traceback.format_stack()[0].split(", in ")[0] + test_base = stack_base.split(", in ")[0].split(os.sep)[-1] + if getattr(test, "cm_filename", None): + filename = test.cm_filename + else: + filename = test_base.split('"')[0] + classname = "SB" + methodname = ".py:" + test_base.split(", line ")[-1] + context_id = filename.split(".")[0] + methodname + ":" + classname + return context_id + test_id = None try: - page_source = driver.page_source + test_id = get_test_name(test) except Exception: - # Since we can't get the page source from here, skip saving it - return - html_file_path = "%s/%s" % (test_logpath, html_file_name) - html_file = codecs.open(html_file_path, "w+", "utf-8") - rendered_source = get_html_source_with_base_href(driver, page_source) - html_file.write(rendered_source) - html_file.close() + test_id = "%s.%s.%s" % ( + test.__class__.__module__, + test.__class__.__name__, + test._testMethodName, + ) + if test._sb_test_identifier and len(str(test._sb_test_identifier)) > 6: + test_id = test._sb_test_identifier + return test_id + + +def get_test_name(test): + if "PYTEST_CURRENT_TEST" in os.environ: + full_name = os.environ["PYTEST_CURRENT_TEST"] + if "] " in full_name: + test_name = full_name.split("] ")[0] + "]" + else: + test_name = full_name.split(" ")[0] + elif test.is_pytest: + test_name = "%s.py::%s::%s" % ( + test.__class__.__module__.split(".")[-1], + test.__class__.__name__, + test._testMethodName, + ) + else: + test_name = "%s.py:%s.%s" % ( + test.__class__.__module__.split(".")[-1], + test.__class__.__name__, + test._testMethodName, + ) + if test._sb_test_identifier and len(str(test._sb_test_identifier)) > 6: + test_name = test._sb_test_identifier + return test_name def get_last_page(driver): try: - last_page = driver.current_url + last_page = None + if __is_cdp_swap_needed(driver): + last_page = driver.cdp.get_current_url() + else: + last_page = driver.current_url except Exception: - last_page = '[WARNING! Browser Not Open!]' + last_page = "[WARNING! Browser Not Open!]" if len(last_page) < 5: - last_page = '[WARNING! Browser Not Open!]' + last_page = "[WARNING! Browser Not Open!]" return last_page def get_base_url(full_url): - protocol = full_url.split('://')[0] - simple_url = full_url.split('://')[1] - base_url = simple_url.split('/')[0] + protocol = full_url.split("://")[0] + simple_url = full_url.split("://")[1] + base_url = simple_url.split("/")[0] full_base_url = "%s://%s" % (protocol, base_url) return full_base_url def get_base_href_html(full_url): - ''' The base href line tells the html what the base page really is. - This is important when trying to open the page outside it's home. ''' + """The base href line tells the html what the base page really is. + This is important when trying to open the page outside it's home.""" base_url = get_base_url(full_url) return '<base href="%s">' % base_url def get_html_source_with_base_href(driver, page_source): - ''' Combines the domain base href with the html source. - This is needed for the page html to render correctly. ''' + """Combines the domain base href with the html source. + Also adds on the meta charset, which may get dropped. + This is needed for the page html to render correctly.""" last_page = get_last_page(driver) - if '://' in last_page: + meta_charset = '<meta charset="utf-8">' + if "://" in last_page: base_href_html = get_base_href_html(last_page) - return '%s\n%s' % (base_href_html, page_source) - return '' + if ' charset="' not in page_source: + return "%s\n%s\n%s" % (base_href_html, meta_charset, page_source) + else: + return "%s\n%s" % (base_href_html, page_source) + return "" + + +def copytree(src, dst, symlinks=False, ignore=None): + if not os.path.exists(dst): + os.makedirs(dst) + for item in os.listdir(src): + s = os.path.join(src, item) + d = os.path.join(dst, item) + if os.path.isdir(s): + copytree(s, d, symlinks, ignore) + else: + if not os.path.exists(d) or ( + os.stat(s).st_mtime - os.stat(d).st_mtime > 1 + ): + shutil.copy2(s, d) + + +def archive_logs_if_set(log_path, archive_logs=False): + """Handle Logging""" + arg_join = " ".join(sys.argv) + if ("-n" in sys.argv) or ("-n=" in arg_join) or (arg_join == "-c"): + return # Skip if multithreaded + if log_path.endswith("/"): + log_path = log_path[:-1] + if not os.path.exists(log_path): + try: + os.makedirs(log_path) + except Exception: + pass # Only reachable during multi-threaded runs + else: + if settings.ARCHIVE_EXISTING_LOGS or archive_logs: + if len(os.listdir(log_path)) > 0: + saved_folder = "%s/../%s/" % (log_path, constants.Logs.SAVED) + archived_folder = os.path.realpath(saved_folder) + "/" + log_path = os.path.realpath(log_path) + "/" + if not os.path.exists(archived_folder): + try: + os.makedirs(archived_folder) + except Exception: + pass # Only reachable during multi-threaded runs + time_id = str(int(time.time())) + archived_logs = "%slogs_%s" % (archived_folder, time_id) + copytree(log_path, archived_logs) def log_folder_setup(log_path, archive_logs=False): - """ Handle Logging """ + """Clean up logs to prepare for another run""" if log_path.endswith("/"): log_path = log_path[:-1] + if log_path.startswith("/"): + log_path = log_path[1:] + if constants.Logs.SAVED.endswith("/"): + constants.Logs.SAVED = constants.Logs.SAVED[:-1] + if constants.Logs.SAVED.startswith("/"): + constants.Logs.SAVED = constants.Logs.SAVED[1:] + if len(log_path) < 10 or len(constants.Logs.SAVED) < 10: + return # Prevent accidental deletions if constants are renamed if not os.path.exists(log_path): try: os.makedirs(log_path) except Exception: - pass # Should only be reachable during multi-threaded runs + pass # Only reachable during multi-threaded runs else: - archived_folder = "%s/../archived_logs/" % log_path + saved_folder = "%s/../%s/" % (log_path, constants.Logs.SAVED) + archived_folder = os.path.realpath(saved_folder) + "/" if not os.path.exists(archived_folder): try: os.makedirs(archived_folder) except Exception: - pass # Should only be reachable during multi-threaded runs - if not "".join(sys.argv) == "-c": - # Only move log files if the test run is not multi-threaded. - # (Running tests with "-n NUM" will create threads that only - # have "-c" in the sys.argv list. Easy to catch.) - archived_logs = "%slogs_%s" % ( - archived_folder, int(time.time())) - shutil.move(log_path, archived_logs) - os.makedirs(log_path) + pass # Only reachable during multi-threaded runs + archived_logs = "%slogs_%s" % (archived_folder, int(time.time())) + if len(os.listdir(log_path)) > 0: + try: + shutil.move(log_path, archived_logs) + os.makedirs(log_path) + except Exception: + pass # A file was probably open at the time if not settings.ARCHIVE_EXISTING_LOGS and not archive_logs: shutil.rmtree(archived_logs) + else: + a_join = " ".join(sys.argv) + if ("-n" in sys.argv) or ("-n=" in a_join) or (a_join == "-c"): + # Logs are saved/archived now if tests are multithreaded + pass + else: + shutil.rmtree(archived_logs) # (Archive test run later) + + +def clear_empty_logs(): + latest_logs_dir = os.path.join(os.getcwd(), constants.Logs.LATEST) + os.sep + archived_folder = os.path.join(os.getcwd(), constants.Logs.SAVED) + os.sep + if os.path.exists(latest_logs_dir) and not os.listdir(latest_logs_dir): + try: + os.rmdir(latest_logs_dir) + except OSError: + pass + if os.path.exists(archived_folder) and not os.listdir(archived_folder): + try: + os.rmdir(archived_folder) + except OSError: + pass diff --git a/seleniumbase/core/mysql.py b/seleniumbase/core/mysql.py old mode 100755 new mode 100644 index b7305df20ea..328a927c32f --- a/seleniumbase/core/mysql.py +++ b/seleniumbase/core/mysql.py @@ -1,63 +1,84 @@ -""" -Wrapper for MySQL DB functions to make life easier. -""" +"""Wrapper for MySQL DB functions""" -import time -from seleniumbase.core import mysql_conf as conf +class DatabaseManager: + """This class wraps MySQL database methods for easy use.""" -class DatabaseManager(): - """ - This class wraps MySQL database methods for easy use. - """ + def __init__(self, database_env="test", conf_creds=None): + """Create a connection to the MySQL DB.""" + import fasteners + import time + from seleniumbase import config as sb_config + from seleniumbase.config import settings + from seleniumbase.core import settings_parser + from seleniumbase.fixtures import constants + from seleniumbase.fixtures import shared_utils - def __init__(self, database_env='test', conf_creds=None): - """ - Gets database information from mysql_conf.py and creates a connection. - """ - import MySQLdb - db_server, db_user, db_pass, db_schema = \ - conf.APP_CREDS[conf.Apps.TESTCASE_REPOSITORY][database_env] + pip_find_lock = fasteners.InterProcessLock( + constants.PipInstall.FINDLOCK + ) + with pip_find_lock: + try: + import cryptography # noqa: F401 + import pymysql + except Exception: + shared_utils.pip_install("PyMySQL[rsa]", version="1.1.1") + import pymysql + db_server = settings.DB_HOST + db_port = settings.DB_PORT + db_user = settings.DB_USERNAME + db_pass = settings.DB_PASSWORD + db_schema = settings.DB_SCHEMA + if getattr(sb_config, "settings_file", None): + override = settings_parser.set_settings(sb_config.settings_file) + if "DB_HOST" in override.keys(): + db_server = override["DB_HOST"] + if "DB_PORT" in override.keys(): + db_port = override["DB_PORT"] + if "DB_USERNAME" in override.keys(): + db_user = override["DB_USERNAME"] + if "DB_PASSWORD" in override.keys(): + db_pass = override["DB_PASSWORD"] + if "DB_SCHEMA" in override.keys(): + db_schema = override["DB_SCHEMA"] retry_count = 3 backoff = 1.2 # Time to wait (in seconds) between retries. count = 0 while count < retry_count: try: - self.conn = MySQLdb.connect(host=db_server, - user=db_user, - passwd=db_pass, - db=db_schema) + self.conn = pymysql.connect( + host=db_server, + port=db_port, + user=db_user, + password=db_pass, + database=db_schema, + ) self.conn.autocommit(True) self.cursor = self.conn.cursor() return except Exception: time.sleep(backoff) count = count + 1 - if retry_count == 3: - raise Exception("Unable to connect to Database after 3 retries.") + if retry_count == 3: + print("Unable to connect to Database after 3 retries.") + raise def query_fetch_all(self, query, values): - """ - Executes a db query, gets all the values, and closes the connection. - """ + """Execute db query, get all the values, and close the connection.""" self.cursor.execute(query, values) retval = self.cursor.fetchall() self.__close_db() return retval def query_fetch_one(self, query, values): - """ - Executes a db query, gets the first value, and closes the connection. - """ + """Execute db query, get the first value, and close the connection.""" self.cursor.execute(query, values) retval = self.cursor.fetchone() self.__close_db() return retval def execute_query(self, query, values): - """ - Executes a query to the test_db and closes the connection afterwards. - """ + """Execute db query, close the connection, and return the results.""" retval = self.cursor.execute(query, values) self.__close_db() return retval diff --git a/seleniumbase/core/mysql_conf.py b/seleniumbase/core/mysql_conf.py deleted file mode 100755 index 07c0206e585..00000000000 --- a/seleniumbase/core/mysql_conf.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -This file organizes connection details to the Testcase Database. -""" - -from seleniumbase.config import settings - -# Environments -TEST = "test" - - -class Apps: - TESTCASE_REPOSITORY = "testcase_repository" - - -APP_CREDS = { - - Apps.TESTCASE_REPOSITORY: { - TEST: ( - settings.DB_HOST, - settings.DB_USERNAME, - settings.DB_PASSWORD, - settings.DB_SCHEMA) - }, - -} diff --git a/seleniumbase/core/nest_asyncio.py b/seleniumbase/core/nest_asyncio.py new file mode 100644 index 00000000000..24bafd3c7b8 --- /dev/null +++ b/seleniumbase/core/nest_asyncio.py @@ -0,0 +1,360 @@ +"""Patch asyncio to allow nested event loops.""" +import asyncio +import asyncio.events as events +import os +import sys +import threading +from contextlib import contextmanager, suppress +from heapq import heappop + +_run_close_loop = True + + +class PatchedNestAsyncio: + pass + + +def apply(loop=None, *, run_close_loop=False, error_on_mispatched=False): + global _run_close_loop + _patch_asyncio(error_on_mispatched=error_on_mispatched) + _patch_policy() + _patch_tornado() + loop = loop or _get_event_loop() + if loop is not None: + _patch_loop(loop) + _run_close_loop &= run_close_loop + + +if sys.version_info < (3, 14, 0): + def _get_event_loop(): + return asyncio.get_event_loop() +else: + def _get_event_loop(): + try: + return asyncio.get_event_loop() + except RuntimeError: + return None + + +if sys.version_info < (3, 12, 0): + def run(main, *, debug=False): + loop = asyncio.get_event_loop() + loop.set_debug(debug) + task = asyncio.ensure_future(main) + try: + return loop.run_until_complete(task) + finally: + if not task.done(): + task.cancel() + with suppress(asyncio.CancelledError): + loop.run_until_complete(task) +else: + def run(main, *, debug=False, loop_factory=None): + new_event_loop = False + set_event_loop = None + try: + loop = asyncio.get_running_loop() + except RuntimeError: + if not _run_close_loop: + loop = _get_event_loop() + if loop is None: + if loop_factory is None: + loop_factory = asyncio.new_event_loop + loop = loop_factory() + asyncio.set_event_loop(loop) + else: + if loop_factory is None: + loop = asyncio.new_event_loop() + set_event_loop = _get_event_loop() + asyncio.set_event_loop(loop) + else: + loop = loop_factory() + new_event_loop = True + _patch_loop(loop) + + loop.set_debug(debug) + task = asyncio.ensure_future(main, loop=loop) + try: + return loop.run_until_complete(task) + finally: + if not task.done(): + task.cancel() + with suppress(asyncio.CancelledError): + loop.run_until_complete(task) + if set_event_loop: + asyncio.set_event_loop(set_event_loop) + if new_event_loop: + # Avoid ResourceWarning: unclosed event loop + loop.close() + + +def _patch_asyncio(*, error_on_mispatched=False): + """Patch asyncio module to use pure Python tasks and futures.""" + + def _get_event_loop(stacklevel=3): + loop = events._get_running_loop() + if loop is None: + if sys.version_info < (3, 14, 0): + policy = events.get_event_loop_policy() + else: + policy = events._get_event_loop_policy() + loop = policy.get_event_loop() + return loop + + if hasattr(asyncio, "_nest_patched"): + if not hasattr(asyncio, "_nest_asyncio"): + if error_on_mispatched: + raise RuntimeError("asyncio was already patched!") + elif sys.version_info >= (3, 12, 0): + import warnings + warnings.warn("asyncio was already patched!") + return + + asyncio.tasks.Task = asyncio.tasks._PyTask + asyncio.Task = asyncio.tasks._CTask = asyncio.tasks.Task + asyncio.Future = asyncio.futures._CFuture = asyncio.futures.Future = ( + asyncio.futures._PyFuture + ) + + # ----------------------------------------------------------------- + # Python 3.14+ FIX: Hybrid Task Visibility. + # Playwright background loops natively run _CTasks. + # Patched loops run _PyTasks. + # We must keep both environments synchronized and visible to current_task() + if sys.version_info >= (3, 14, 0): + _c_current_task = asyncio.current_task + _c_all_tasks = asyncio.all_tasks + _c_swap_current_task = ( + getattr(asyncio.tasks, "_swap_current_task", None) + ) + + def _patched_current_task(loop=None): + # 1. Check C-level state (finds Playwright / Extension tasks) + t = _c_current_task(loop) + if t is not None: + return t + # 2. Check pure-Python state (finds patched nest_asyncio tasks) + if hasattr(asyncio.tasks, "_py_current_task"): + return asyncio.tasks._py_current_task(loop) + return None + + def _patched_all_tasks(loop=None): + tasks = _c_all_tasks(loop) or set() + if hasattr(asyncio.tasks, "_py_all_tasks"): + tasks.update(asyncio.tasks._py_all_tasks(loop) or set()) + return tasks + + def _patched_swap_current_task(loop, task): + old_c = None + if _c_swap_current_task is not None: + try: + old_c = _c_swap_current_task(loop, None) + except (KeyError, RuntimeError): + pass + + old_py = None + if hasattr(asyncio.tasks, "_py_swap_current_task"): + try: + old_py = asyncio.tasks._py_swap_current_task(loop, None) + except (KeyError, RuntimeError): + pass + + if task is not None: + if _c_swap_current_task is not None: + try: + _c_swap_current_task(loop, task) + except (TypeError, ValueError): + pass + if hasattr(asyncio.tasks, "_py_swap_current_task"): + try: + asyncio.tasks._py_swap_current_task(loop, task) + except (TypeError, ValueError): + pass + + return old_c or old_py + + asyncio.tasks.current_task = _patched_current_task + asyncio.current_task = _patched_current_task + asyncio.tasks.all_tasks = _patched_all_tasks + asyncio.all_tasks = _patched_all_tasks + if _c_swap_current_task is not None: + asyncio.tasks._swap_current_task = _patched_swap_current_task + + for func in ( + "_enter_task", + "_leave_task", + "_register_task", + "_unregister_task", + "_register_eager_task", + "_unregister_eager_task", + ): + py_func = f"_py{func}" + if hasattr(asyncio.tasks, py_func): + setattr(asyncio.tasks, func, getattr(asyncio.tasks, py_func)) + # ----------------------------------------------------------------- + + asyncio.get_event_loop = _get_event_loop + events._get_event_loop = events.get_event_loop = asyncio.get_event_loop + asyncio.run = run + asyncio._nest_patched = True + asyncio._nest_asyncio = PatchedNestAsyncio() + + +def _patch_policy(): + """Patch the policy to always return a patched loop.""" + + def get_event_loop(self): + if self._local._loop is None: + loop = self.new_event_loop() + _patch_loop(loop) + self.set_event_loop(loop) + return self._local._loop + + if sys.version_info < (3, 14, 0): + policy = events.get_event_loop_policy() + else: + policy = events._get_event_loop_policy() + policy.__class__.get_event_loop = get_event_loop + + +def _patch_loop(loop): + """Patch loop to make it reentrant.""" + + def run_forever(self): + with manage_run(self), manage_asyncgens(self): + while True: + self._run_once() + if self._stopping: + break + self._stopping = False + + def run_until_complete(self, future): + with manage_run(self): + f = asyncio.ensure_future(future, loop=self) + if f is not future: + f._log_destroy_pending = False + while not f.done(): + self._run_once() + if self._stopping: + break + if not f.done(): + raise RuntimeError("Loop stopped before Future completed!") + return f.result() + + def _run_once(self): + """Simplified re-implementation of asyncio's _run_once.""" + ready = self._ready + scheduled = self._scheduled + while scheduled and scheduled[0]._cancelled: + heappop(scheduled) + timeout = ( + 0 + if ready or self._stopping + else min(max(scheduled[0]._when - self.time(), 0), 86400) + if scheduled + else None + ) + event_list = self._selector.select(timeout) + self._process_events(event_list) + end_time = self.time() + self._clock_resolution + while scheduled and scheduled[0]._when < end_time: + handle = heappop(scheduled) + ready.append(handle) + for _ in range(len(ready)): + if not ready: + break + handle = ready.popleft() + if not handle._cancelled: + if sys.version_info < (3, 14, 0): + curr_task = curr_tasks.pop(self, None) + else: + try: + curr_task = asyncio.tasks._swap_current_task( + self, None + ) + except KeyError: + curr_task = None + try: + handle._run() + finally: + if curr_task is not None: + if sys.version_info < (3, 14, 0): + curr_tasks[self] = curr_task + else: + asyncio.tasks._swap_current_task(self, curr_task) + handle = None + + @contextmanager + def manage_run(self): + self._check_closed() + old_thread_id = self._thread_id + old_running_loop = events._get_running_loop() + try: + self._thread_id = threading.get_ident() + events._set_running_loop(self) + self._num_runs_pending += 1 + if self._is_proactorloop: + if self._self_reading_future is None: + self.call_soon(self._loop_self_reading) + yield + finally: + self._thread_id = old_thread_id + events._set_running_loop(old_running_loop) + self._num_runs_pending -= 1 + if self._is_proactorloop: + if ( + self._num_runs_pending == 0 + and self._self_reading_future is not None + ): + ov = self._self_reading_future._ov + self._self_reading_future.cancel() + if ov is not None: + self._proactor._unregister(ov) + self._self_reading_future = None + + @contextmanager + def manage_asyncgens(self): + old_agen_hooks = sys.get_asyncgen_hooks() + try: + self._set_coroutine_origin_tracking(self._debug) + if self._asyncgens is not None: + sys.set_asyncgen_hooks( + firstiter=self._asyncgen_firstiter_hook, + finalizer=self._asyncgen_finalizer_hook, + ) + yield + finally: + self._set_coroutine_origin_tracking(False) + if self._asyncgens is not None: + sys.set_asyncgen_hooks(*old_agen_hooks) + + def _check_running(self): + """Do not throw exception if loop is already running.""" + pass + + if hasattr(loop, "_nest_patched"): + return + if not isinstance(loop, asyncio.BaseEventLoop): + raise ValueError("Can't patch loop of type %s" % type(loop)) + cls = loop.__class__ + cls.run_forever = run_forever + cls.run_until_complete = run_until_complete + cls._run_once = _run_once + cls._check_running = _check_running + cls._num_runs_pending = 1 if loop.is_running() else 0 + cls._is_proactorloop = os.name == "nt" and issubclass( + cls, asyncio.ProactorEventLoop + ) + curr_tasks = asyncio.tasks._current_tasks + cls._nest_patched = True + cls._nest_asyncio = PatchedNestAsyncio() + + +def _patch_tornado(): + """If tornado is imported before nest_asyncio, + make tornado aware of the pure-Python asyncio Future.""" + if "tornado" in sys.modules: + import tornado.concurrent as tc # type: ignore + tc.Future = asyncio.Future + if asyncio.Future not in tc.FUTURES: + tc.FUTURES += (asyncio.Future,) diff --git a/seleniumbase/core/proxy_helper.py b/seleniumbase/core/proxy_helper.py old mode 100755 new mode 100644 index b7f7e0f445c..7c3c06ecc99 --- a/seleniumbase/core/proxy_helper.py +++ b/seleniumbase/core/proxy_helper.py @@ -1,94 +1,236 @@ import os -import threading +import re +import warnings import zipfile +from contextlib import suppress +from seleniumbase.config import proxy_list +from seleniumbase.config import settings from seleniumbase.fixtures import constants -from seleniumbase import drivers -DRIVER_DIR = os.path.dirname(os.path.realpath(drivers.__file__)) -PROXY_ZIP_PATH = "%s/%s" % (DRIVER_DIR, "proxy.zip") +from seleniumbase.fixtures import page_utils +from seleniumbase.fixtures import shared_utils + DOWNLOADS_DIR = constants.Files.DOWNLOADS_FOLDER -PROXY_ZIP_PATH_2 = "%s/%s" % (DOWNLOADS_DIR, "proxy.zip") +PROXY_ZIP_PATH = os.path.join(DOWNLOADS_DIR, "proxy.zip") +PROXY_ZIP_LOCK = os.path.join(DOWNLOADS_DIR, "proxy.lock") +PROXY_DIR_PATH = os.path.join(DOWNLOADS_DIR, "proxy_ext_dir") +PROXY_DIR_LOCK = os.path.join(DOWNLOADS_DIR, "proxy_dir.lock") -def create_proxy_zip(proxy_string, proxy_user, proxy_pass): - """ Implementation of https://stackoverflow.com/a/35293284 for - https://stackoverflow.com/questions/12848327/ - (Run Selenium on a proxy server that requires authentication.) - Solution involves creating & adding a Chrome extension on the fly. - * CHROME-ONLY for now! * +def create_proxy_ext( + proxy_string, + proxy_user, + proxy_pass, + proxy_scheme="http", + bypass_list=None, + zip_it=True, +): + """Implementation of https://stackoverflow.com/a/35293284 for + https://stackoverflow.com/questions/12848327/ + (Run Selenium on a proxy server that requires authentication.) + Solution involves creating & adding a Chromium extension at runtime. + CHROMIUM-ONLY! *** Only Chrome and Edge browsers are supported. *** """ - proxy_host = proxy_string.split(':')[0] - proxy_port = proxy_string.split(':')[1] - background_js = ( - """var config = {\n""" - """ mode: "fixed_servers",\n""" - """ rules: {\n""" - """ singleProxy: {\n""" - """ scheme: "http",\n""" - """ host: "%s",\n""" - """ port: parseInt("%s")\n""" - """ },\n""" - """ }\n""" - """ };\n""" - """chrome.proxy.settings.set(""" - """{value: config, scope: "regular"}, function() {""" - """});\n""" - """function callbackFn(details) {\n""" - """ return {\n""" - """ authCredentials: {\n""" - """ username: "%s",\n""" - """ password: "%s"\n""" - """ }\n""" - """ };\n""" - """}\n""" - """chrome.webRequest.onAuthRequired.addListener(\n""" - """ callbackFn,\n""" - """ {urls: ["<all_urls>"]},\n""" - """ ['blocking']\n""" - """);""" % (proxy_host, proxy_port, proxy_user, proxy_pass)) + background_js = None + if not bypass_list: + bypass_list = "" + if proxy_string: + proxy_protocol = "" + if proxy_string.count("://") == 1: + proxy_protocol = proxy_string.split("://")[0] + "://" + proxy_string = proxy_string.split("://")[1] + proxy_host = proxy_protocol + proxy_string.split(":")[0] + proxy_port = proxy_string.split(":")[1] + background_js = ( + """var config = {\n""" + """ mode: "fixed_servers",\n""" + """ rules: {\n""" + """ singleProxy: {\n""" + """ scheme: "%s",\n""" + """ host: "%s",\n""" + """ port: parseInt("%s")\n""" + """ },\n""" + """ bypassList: ["%s"]\n""" + """ }\n""" + """ };\n""" + """chrome.proxy.settings.set(""" + """{value: config, scope: "regular"}, function() {""" + """});\n""" + """function callbackFn(details) {\n""" + """ return {\n""" + """ authCredentials: {\n""" + """ username: "%s",\n""" + """ password: "%s"\n""" + """ }\n""" + """ };\n""" + """}\n""" + """chrome.webRequest.onAuthRequired.addListener(\n""" + """ callbackFn,\n""" + """ {urls: ["<all_urls>"]},\n""" + """ ['blocking']\n""" + """);""" % ( + proxy_scheme, + proxy_host, + proxy_port, + bypass_list, + proxy_user, + proxy_pass, + ) + ) + else: + background_js = ( + """var config = {\n""" + """ mode: "fixed_servers",\n""" + """ rules: {\n""" + """ },\n""" + """ bypassList: ["%s"]\n""" + """ };\n""" + """chrome.proxy.settings.set(""" + """{value: config, scope: "regular"}, function() {""" + """});\n""" + """function callbackFn(details) {\n""" + """ return {\n""" + """ authCredentials: {\n""" + """ username: "%s",\n""" + """ password: "%s"\n""" + """ }\n""" + """ };\n""" + """}\n""" + """chrome.webRequest.onAuthRequired.addListener(\n""" + """ callbackFn,\n""" + """ {urls: ["<all_urls>"]},\n""" + """ ['blocking']\n""" + """);""" % (bypass_list, proxy_user, proxy_pass) + ) manifest_json = ( - '''{\n''' - '''"version": "1.0.0",\n''' - '''"manifest_version": 2,\n''' - '''"name": "Chrome Proxy",\n''' - '''"permissions": [\n''' - ''' "proxy",\n''' - ''' "tabs",\n''' - ''' "unlimitedStorage",\n''' - ''' "storage",\n''' - ''' "<all_urls>",\n''' - ''' "webRequest",\n''' - ''' "webRequestBlocking"\n''' - '''],\n''' - '''"background": {\n''' - ''' "scripts": ["background.js"]\n''' - '''},\n''' - '''"minimum_chrome_version":"22.0.0"\n''' - '''}''') - lock = threading.RLock() # Support multi-threaded test runs with Pytest - with lock: - try: - zf = zipfile.ZipFile(PROXY_ZIP_PATH, mode='w') - except IOError: - # Handle "Permission denied" on the default proxy.zip path - abs_path = os.path.abspath('.') - downloads_path = os.path.join(abs_path, DOWNLOADS_DIR) - if not os.path.exists(downloads_path): - os.mkdir(downloads_path) - zf = zipfile.ZipFile(PROXY_ZIP_PATH_2, mode='w') + """{\n""" + """"version": "1.0.0",\n""" + """"manifest_version": 3,\n""" + """"name": "Chrome Proxy",\n""" + """"permissions": [\n""" + """ "proxy",\n""" + """ "tabs",\n""" + """ "unlimitedStorage",\n""" + """ "storage",\n""" + """ "webRequest",\n""" + """ "webRequestAuthProvider"\n""" + """],\n""" + """"host_permissions": [\n""" + """ "<all_urls>"\n""" + """],\n""" + """"background": {\n""" + """ "service_worker": "background.js"\n""" + """},\n""" + """"minimum_chrome_version":"88.0.0"\n""" + """}""" + ) + abs_path = os.path.abspath(".") + downloads_path = os.path.join(abs_path, DOWNLOADS_DIR) + if not os.path.exists(downloads_path): + os.mkdir(downloads_path) + if zip_it: + zf = zipfile.ZipFile(PROXY_ZIP_PATH, mode="w") zf.writestr("background.js", background_js) zf.writestr("manifest.json", manifest_json) zf.close() + with suppress(Exception): + shared_utils.make_writable(PROXY_ZIP_PATH) + else: + proxy_ext_dir = PROXY_DIR_PATH + if not os.path.exists(proxy_ext_dir): + os.mkdir(proxy_ext_dir) + with suppress(Exception): + shared_utils.make_writable(proxy_ext_dir) + manifest_file = os.path.join(proxy_ext_dir, "manifest.json") + with open(manifest_file, mode="w") as f: + f.write(manifest_json) + with suppress(Exception): + shared_utils.make_writable(manifest_json) + proxy_host = proxy_string.split(":")[0] + proxy_port = proxy_string.split(":")[1] + background_file = os.path.join(proxy_ext_dir, "background.js") + with open(background_file, mode="w") as f: + f.write(background_js) + with suppress(Exception): + shared_utils.make_writable(background_js) def remove_proxy_zip_if_present(): - """ Remove Chrome extension zip file used for proxy server authentication. - Used in the implementation of https://stackoverflow.com/a/35293284 - for https://stackoverflow.com/questions/12848327/ + """Remove Chromium extension zip file used for proxy server authentication. + Used in the implementation of https://stackoverflow.com/a/35293284 + for https://stackoverflow.com/questions/12848327/ """ - try: - if os.path.exists(PROXY_ZIP_PATH): + if os.path.exists(PROXY_ZIP_PATH): + with suppress(Exception): os.remove(PROXY_ZIP_PATH) - elif os.path.exists(PROXY_ZIP_PATH_2): - os.remove(PROXY_ZIP_PATH_2) - except Exception: - pass + if os.path.exists(PROXY_ZIP_LOCK): + with suppress(Exception): + os.remove(PROXY_ZIP_LOCK) + + +def validate_proxy_string(proxy_string, keep_scheme=False): + if proxy_string in proxy_list.PROXY_LIST.keys(): + proxy_string = proxy_list.PROXY_LIST[proxy_string] + if not proxy_string: + return None + proxy_scheme = "http" + if proxy_string.startswith("https://"): + proxy_scheme = "https" + elif proxy_string.startswith("socks4://"): + proxy_scheme = "socks4" + elif proxy_string.startswith("socks5h://"): + proxy_scheme = "socks5h" + elif proxy_string.startswith("socks5://"): + proxy_scheme = "socks5" + valid = False + val_ip = re.match( + r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+$", proxy_string + ) + if not val_ip: + if proxy_string.startswith("http://"): + proxy_string = proxy_string.split("http://")[1] + elif proxy_string.startswith("https://"): + proxy_string = proxy_string.split("https://")[1] + elif "://" in proxy_string: + if ( + not proxy_string.startswith("socks4://") + and not proxy_string.startswith("socks5://") + and not proxy_string.startswith("socks5h://") + ): + proxy_string = proxy_string.split("://")[1] + chunks = proxy_string.split(":") + if len(chunks) == 2: + if re.match(r"^\d+$", chunks[1]): + if page_utils.is_valid_url("http://" + proxy_string): + valid = True + elif len(chunks) == 3: + if re.match(r"^\d+$", chunks[2]): + if page_utils.is_valid_url("http:" + ":".join(chunks[1:])): + if chunks[0] in [ + "http", "https", "socks4", "socks5", "socks5h" + ]: + valid = True + else: + proxy_string = val_ip.group() + valid = True + if not valid: + __display_proxy_warning(proxy_string) + proxy_string = None + if keep_scheme: + return (proxy_string, proxy_scheme) + return proxy_string + + +def __display_proxy_warning(proxy_string): + message = ( + '\nWARNING: Proxy String ["%s"] is NOT in the expected ' + '"ip_address:port" or "server:port" format, ' + "(OR the key does not exist in " + "seleniumbase.config.proxy_list.PROXY_LIST)." % proxy_string + ) + if settings.RAISE_INVALID_PROXY_STRING_EXCEPTION: + raise Exception(message) + else: + message += " *** DEFAULTING to NOT USING a Proxy Server! ***" + warnings.simplefilter("always", Warning) # See Warnings + warnings.warn(message, category=Warning, stacklevel=2) + warnings.simplefilter("default", Warning) # Set Default diff --git a/seleniumbase/core/recorder_helper.py b/seleniumbase/core/recorder_helper.py new file mode 100644 index 00000000000..a74ddc10ae2 --- /dev/null +++ b/seleniumbase/core/recorder_helper.py @@ -0,0 +1,592 @@ +"""Generating SeleniumBase Python code from the Recorder""" + + +def generate_sbase_code(srt_actions): + sb_actions = [] + for action in srt_actions: + if action[0] == "begin" or action[0] == "_url_": + if "%" in action[2]: + try: + from urllib.parse import unquote + + action[2] = unquote(action[2], errors="strict") + except Exception: + pass + if '"' not in action[2]: + sb_actions.append('self.goto("%s")' % action[2]) + elif "'" not in action[2]: + sb_actions.append("self.goto('%s')" % action[2]) + else: + sb_actions.append( + 'self.goto("%s")' % action[2].replace('"', '\\"') + ) + elif action[0] == "f_url": + if "%" in action[2]: + try: + from urllib.parse import unquote + + action[2] = unquote(action[2], errors="strict") + except Exception: + pass + if '"' not in action[2]: + sb_actions.append('self.goto_if_not_url("%s")' % action[2]) + elif "'" not in action[2]: + sb_actions.append("self.goto_if_not_url('%s')" % action[2]) + else: + sb_actions.append( + 'self.goto_if_not_url("%s")' + % action[2].replace('"', '\\"') + ) + elif action[0] == "click": + method = "click" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + else: + sb_actions.append("self.%s('%s')" % (method, action[1])) + elif action[0] == "dbclk": + method = "double_click" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + else: + sb_actions.append("self.%s('%s')" % (method, action[1])) + elif action[0] == "js_cl": + method = "js_click" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + else: + sb_actions.append("self.%s('%s')" % (method, action[1])) + elif action[0] == "js_ca": + method = "js_click_all" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + else: + sb_actions.append("self.%s('%s')" % (method, action[1])) + elif action[0] == "jq_cl": + method = "jquery_click" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + else: + sb_actions.append("self.%s('%s')" % (method, action[1])) + elif action[0] == "jq_ca": + method = "jquery_click_all" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + else: + sb_actions.append("self.%s('%s')" % (method, action[1])) + elif action[0] == "r_clk": + method = "context_click" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + else: + sb_actions.append("self.%s('%s')" % (method, action[1])) + elif action[0] == "canva": + method = "click_with_offset" + selector = action[1][0] + p_x = action[1][1] + p_y = action[1][2] + if '"' not in selector: + sb_actions.append( + 'self.%s("%s", %s, %s)' % (method, selector, p_x, p_y) + ) + else: + sb_actions.append( + "self.%s('%s', %s, %s)" % (method, selector, p_x, p_y) + ) + elif ( + action[0] == "input" + or action[0] == "js_ty" + or action[0] == "jq_ty" + or action[0] == "pkeys" + ): + method = "type" + if action[0] == "js_ty": + method = "js_type" + elif action[0] == "jq_ty": + method = "jquery_type" + elif action[0] == "pkeys": + method = "press_keys" + text = action[2].replace("\n", "\\n") + if '"' not in action[1] and '"' not in text: + sb_actions.append( + 'self.%s("%s", "%s")' % (method, action[1], text) + ) + elif '"' not in action[1] and '"' in text: + sb_actions.append( + 'self.%s("%s", \'%s\')' % (method, action[1], text) + ) + elif '"' in action[1] and '"' not in text: + sb_actions.append( + 'self.%s(\'%s\', "%s")' % (method, action[1], text) + ) + elif '"' in action[1] and '"' in text: + sb_actions.append( + "self.%s('%s', '%s')" % (method, action[1], text) + ) + elif action[0] == "hover": + method = "hover" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + else: + sb_actions.append("self.%s('%s')" % (method, action[1])) + elif action[0] == "e_mfa": + method = "enter_mfa_code" + text = action[2].replace("\n", "\\n") + if '"' not in action[1] and '"' not in text: + sb_actions.append( + 'self.%s("%s", "%s")' % (method, action[1], text) + ) + elif '"' not in action[1] and '"' in text: + sb_actions.append( + 'self.%s("%s", \'%s\')' % (method, action[1], text) + ) + elif '"' in action[1] and '"' not in text: + sb_actions.append( + 'self.%s(\'%s\', "%s")' % (method, action[1], text) + ) + elif '"' in action[1] and '"' in text: + sb_actions.append( + "self.%s('%s', '%s')" % (method, action[1], text) + ) + elif action[0] == "h_clk": + method = "hover_and_click" + if '"' not in action[1] and '"' not in action[2]: + sb_actions.append( + 'self.%s("%s", "%s")' % (method, action[1], action[2]) + ) + elif '"' not in action[1] and '"' in action[2]: + sb_actions.append( + 'self.%s("%s", \'%s\')' + % (method, action[1], action[2]) + ) + elif '"' in action[1] and '"' not in action[2]: + sb_actions.append( + 'self.%s(\'%s\', "%s")' + % (method, action[1], action[2]) + ) + elif '"' in action[1] and '"' in action[2]: + sb_actions.append( + "self.%s('%s', '%s')" % (method, action[1], action[2]) + ) + elif action[0] == "ddrop": + method = "drag_and_drop" + if '"' not in action[1] and '"' not in action[2]: + sb_actions.append( + 'self.%s("%s", "%s")' % (method, action[1], action[2]) + ) + elif '"' not in action[1] and '"' in action[2]: + sb_actions.append( + 'self.%s("%s", \'%s\')' + % (method, action[1], action[2]) + ) + elif '"' in action[1] and '"' not in action[2]: + sb_actions.append( + 'self.%s(\'%s\', "%s")' + % (method, action[1], action[2]) + ) + elif '"' in action[1] and '"' in action[2]: + sb_actions.append( + "self.%s('%s', '%s')" % (method, action[1], action[2]) + ) + elif action[0] == "s_opt": + method = "select_option_by_text" + if '"' not in action[1] and '"' not in action[2]: + sb_actions.append( + 'self.%s("%s", "%s")' % (method, action[1], action[2]) + ) + elif '"' not in action[1] and '"' in action[2]: + sb_actions.append( + 'self.%s("%s", \'%s\')' + % (method, action[1], action[2]) + ) + elif '"' in action[1] and '"' not in action[2]: + sb_actions.append( + 'self.%s(\'%s\', "%s")' + % (method, action[1], action[2]) + ) + elif '"' in action[1] and '"' in action[2]: + sb_actions.append( + "self.%s('%s', '%s')" % (method, action[1], action[2]) + ) + elif action[0] == "set_v": + method = "set_value" + if '"' not in action[1] and '"' not in action[2]: + sb_actions.append( + 'self.%s("%s", "%s")' % (method, action[1], action[2]) + ) + elif '"' not in action[1] and '"' in action[2]: + sb_actions.append( + 'self.%s("%s", \'%s\')' + % (method, action[1], action[2]) + ) + elif '"' in action[1] and '"' not in action[2]: + sb_actions.append( + 'self.%s(\'%s\', "%s")' + % (method, action[1], action[2]) + ) + elif '"' in action[1] and '"' in action[2]: + sb_actions.append( + "self.%s('%s', '%s')" % (method, action[1], action[2]) + ) + elif action[0] == "cho_f": + method = "choose_file" + action[2] = action[2].replace("\\", "\\\\") + if '"' not in action[1] and '"' not in action[2]: + sb_actions.append( + 'self.%s("%s", "%s")' % (method, action[1], action[2]) + ) + elif '"' not in action[1] and '"' in action[2]: + sb_actions.append( + 'self.%s("%s", \'%s\')' + % (method, action[1], action[2]) + ) + elif '"' in action[1] and '"' not in action[2]: + sb_actions.append( + 'self.%s(\'%s\', "%s")' + % (method, action[1], action[2]) + ) + elif '"' in action[1] and '"' in action[2]: + sb_actions.append( + "self.%s('%s', '%s')" % (method, action[1], action[2]) + ) + elif action[0] == "sw_fr": + method = "switch_to_frame" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + else: + sb_actions.append("self.%s('%s')" % (method, action[1])) + elif action[0] == "sw_dc": + sb_actions.append("self.switch_to_default_content()") + elif action[0] == "sw_pf": + sb_actions.append("self.switch_to_parent_frame()") + elif action[0] == "s_c_f": + method = "set_content_to_frame" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + else: + sb_actions.append("self.%s('%s')" % (method, action[1])) + elif action[0] == "s_c_d": + method = "set_content_to_default" + nested = action[1] + if nested: + method = "set_content_to_parent" + sb_actions.append("self.%s()" % method) + else: + sb_actions.append("self.%s()" % method) + elif action[0] == "sleep": + method = "sleep" + sb_actions.append("self.%s(%s)" % (method, action[1])) + elif action[0] == "wf_el": + method = "wait_for_element" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + elif "'" not in action[1]: + sb_actions.append("self.%s('%s')" % (method, action[1])) + else: + sb_actions.append( + 'self.%s("""%s""")' % (method, action[1]) + ) + elif action[0] == "as_el": + method = "assert_element" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + elif "'" not in action[1]: + sb_actions.append("self.%s('%s')" % (method, action[1])) + else: + sb_actions.append( + 'self.%s("""%s""")' % (method, action[1]) + ) + elif action[0] == "as_ep": + method = "assert_element_present" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + elif "'" not in action[1]: + sb_actions.append("self.%s('%s')" % (method, action[1])) + else: + sb_actions.append( + 'self.%s("""%s""")' % (method, action[1]) + ) + elif action[0] == "asenv": + method = "assert_element_not_visible" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + elif "'" not in action[1]: + sb_actions.append("self.%s('%s')" % (method, action[1])) + else: + sb_actions.append( + 'self.%s("""%s""")' % (method, action[1]) + ) + elif action[0] == "s_at_" or action[0] == "s_ats": + method = "set_attribute" + if action[0] == "s_ats": + method = "set_attributes" + if '"' not in action[1][0]: + sb_actions.append( + 'self.%s("%s", "%s", "%s")' + % (method, action[1][0], action[1][1], action[1][2]) + ) + elif "'" not in action[1][0]: + sb_actions.append( + "self.%s('%s', \"%s\", \"%s\")" + % (method, action[1][0], action[1][1], action[1][2]) + ) + else: + sb_actions.append( + 'self.%s("""%s""", "%s", "%s")' + % (method, action[1][0], action[1][1], action[1][2]) + ) + elif action[0] == "acc_a": + sb_actions.append("self.accept_alert()") + elif action[0] == "dis_a": + sb_actions.append("self.dismiss_alert()") + elif action[0] == "hi_li": + method = "highlight" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + else: + sb_actions.append("self.%s('%s')" % (method, action[1])) + elif action[0] == "as_lt": + method = "assert_link_text" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + else: + sb_actions.append("self.%s('%s')" % (method, action[1])) + elif action[0] == "as_ti": + method = "assert_title" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + else: + sb_actions.append("self.%s('%s')" % (method, action[1])) + elif action[0] == "as_tc": + method = "assert_title_contains" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + else: + sb_actions.append("self.%s('%s')" % (method, action[1])) + elif action[0] == "a_url": + method = "assert_url" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + else: + sb_actions.append("self.%s('%s')" % (method, action[1])) + elif action[0] == "a_u_c": + method = "assert_url_contains" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + else: + sb_actions.append("self.%s('%s')" % (method, action[1])) + elif action[0] == "as_df": + method = "assert_downloaded_file" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + else: + sb_actions.append("self.%s('%s')" % (method, action[1])) + elif action[0] == "do_fi": + method = "download_file" + file_url = action[1][0] + dest = action[1][1] + if not dest: + sb_actions.append('self.%s("%s")' % (method, file_url)) + else: + sb_actions.append( + 'self.%s("%s", "%s")' % (method, file_url, dest) + ) + elif action[0] == "as_at": + method = "assert_attribute" + if ('"' not in action[1][0]) and action[1][2]: + sb_actions.append( + 'self.%s("%s", "%s", "%s")' + % (method, action[1][0], action[1][1], action[1][2]) + ) + elif ('"' not in action[1][0]) and not action[1][2]: + sb_actions.append( + 'self.%s("%s", "%s")' + % (method, action[1][0], action[1][1]) + ) + elif ('"' in action[1][0]) and action[1][2]: + sb_actions.append( + 'self.%s(\'%s\', "%s", "%s")' + % (method, action[1][0], action[1][1], action[1][2]) + ) + else: + sb_actions.append( + 'self.%s(\'%s\', "%s")' + % (method, action[1][0], action[1][1]) + ) + elif ( + action[0] == "as_te" + or action[0] == "as_et" + or action[0] == "astnv" + or action[0] == "aetnv" + or action[0] == "da_te" + or action[0] == "da_et" + ): + import unicodedata + + text_list = False + try: + action[1][0] = unicodedata.normalize("NFKC", action[1][0]) + action[1][0] = action[1][0].replace("\n", "\\n") + action[1][0] = action[1][0].replace("\u00B6", "") + except Exception: + text_list = True + + method = "assert_text" + if action[0] == "as_et": + method = "assert_exact_text" + elif action[0] == "astnv": + method = "assert_text_not_visible" + elif action[0] == "aetnv": + method = "assert_exact_text_not_visible" + elif action[0] == "da_te": + method = "deferred_assert_text" + elif action[0] == "da_et": + method = "deferred_assert_exact_text" + if action[1][1] != "html": + if text_list and '"' not in action[1][1]: + sb_actions.append( + 'self.%s(%s, "%s")' + % (method, action[1][0], action[1][1]) + ) + elif text_list and "'" not in action[1][1]: + sb_actions.append( + "self.%s(%s, '%s')" + % (method, action[1][0], action[1][1]) + ) + elif '"' not in action[1][0] and '"' not in action[1][1]: + sb_actions.append( + 'self.%s("%s", "%s")' + % (method, action[1][0], action[1][1]) + ) + elif '"' not in action[1][0] and '"' in action[1][1]: + sb_actions.append( + 'self.%s("%s", \'%s\')' + % (method, action[1][0], action[1][1]) + ) + elif '"' in action[1] and '"' not in action[1][1]: + sb_actions.append( + 'self.%s(\'%s\', "%s")' + % (method, action[1][0], action[1][1]) + ) + elif '"' in action[1] and '"' in action[1][1]: + sb_actions.append( + "self.%s('%s', '%s')" + % (method, action[1][0], action[1][1]) + ) + else: + if text_list: + sb_actions.append( + 'self.%s(%s)' % (method, action[1][0]) + ) + elif '"' not in action[1][0]: + sb_actions.append( + 'self.%s("%s")' % (method, action[1][0]) + ) + else: + sb_actions.append( + "self.%s('%s')" % (method, action[1][0]) + ) + elif action[0] == "asnet": + method = "assert_non_empty_text" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + elif "'" not in action[1]: + sb_actions.append("self.%s('%s')" % (method, action[1])) + else: + sb_actions.append( + 'self.%s("""%s""")' % (method, action[1]) + ) + elif action[0] == "da_el": + method = "deferred_assert_element" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + elif "'" not in action[1]: + sb_actions.append("self.%s('%s')" % (method, action[1])) + else: + sb_actions.append( + 'self.%s("""%s""")' % (method, action[1]) + ) + elif action[0] == "da_ep": + method = "deferred_assert_element_present" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + elif "'" not in action[1]: + sb_actions.append("self.%s('%s')" % (method, action[1])) + else: + sb_actions.append( + 'self.%s("""%s""")' % (method, action[1]) + ) + elif action[0] == "danet": + method = "deferred_assert_non_empty_text" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + elif "'" not in action[1]: + sb_actions.append("self.%s('%s')" % (method, action[1])) + else: + sb_actions.append( + 'self.%s("""%s""")' % (method, action[1]) + ) + elif action[0] == "s_scr": + method = "save_screenshot" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + else: + sb_actions.append("self.%s('%s')" % (method, action[1])) + elif action[0] == "ss_tf": + method = "save_screenshot" + action[2] = action[1][1] + action[1] = action[1][0] + if '"' not in action[1] and '"' not in action[2]: + sb_actions.append( + 'self.%s("%s", "%s")' % (method, action[1], action[2]) + ) + elif '"' not in action[1] and '"' in action[2]: + sb_actions.append( + 'self.%s("%s", \'%s\')' + % (method, action[1], action[2]) + ) + elif '"' in action[1] and '"' not in action[2]: + sb_actions.append( + 'self.%s(\'%s\', "%s")' + % (method, action[1], action[2]) + ) + elif '"' in action[1] and '"' in action[2]: + sb_actions.append( + "self.%s('%s', '%s')" % (method, action[1], action[2]) + ) + elif action[0] == "ss_tl": + method = "save_screenshot_to_logs" + sb_actions.append("self.%s()" % method) + elif action[0] == "pdftl": + method = "save_as_pdf_to_logs" + sb_actions.append("self.%s()" % method) + elif action[0] == "spstl": + method = "save_page_source_to_logs" + sb_actions.append("self.%s()" % method) + elif action[0] == "sh_fc": + method = "show_file_choosers" + sb_actions.append("self.%s()" % method) + elif action[0] == "pr_da": + sb_actions.append("self.process_deferred_asserts()") + elif action[0] == "a_d_m": + sb_actions.append("self.activate_demo_mode()") + elif action[0] == "d_d_m": + sb_actions.append("self.deactivate_demo_mode()") + elif action[0] == "c_l_s": + sb_actions.append("self.clear_local_storage()") + elif action[0] == "c_s_s": + sb_actions.append("self.clear_session_storage()") + elif action[0] == "d_a_c": + sb_actions.append("self.delete_all_cookies()") + elif action[0] == "go_bk": + sb_actions.append("self.go_back()") + elif action[0] == "go_fw": + sb_actions.append("self.go_forward()") + elif action[0] == "c_box": + method = "check_if_unchecked" + if action[2] == "no": + method = "uncheck_if_checked" + if '"' not in action[1]: + sb_actions.append('self.%s("%s")' % (method, action[1])) + else: + sb_actions.append("self.%s('%s')" % (method, action[1])) + return sb_actions diff --git a/seleniumbase/core/report_helper.py b/seleniumbase/core/report_helper.py old mode 100755 new mode 100644 index bef7f125706..d408721a841 --- a/seleniumbase/core/report_helper.py +++ b/seleniumbase/core/report_helper.py @@ -2,26 +2,14 @@ import shutil import sys import time -from selenium import webdriver +from seleniumbase import config as sb_config from seleniumbase.config import settings -from seleniumbase.core.style_sheet import style -from seleniumbase.fixtures import page_actions -from seleniumbase import drivers +from seleniumbase.core.style_sheet import get_report_style LATEST_REPORT_DIR = settings.LATEST_REPORT_DIR ARCHIVE_DIR = settings.REPORT_ARCHIVE_DIR HTML_REPORT = settings.HTML_REPORT RESULTS_TABLE = settings.RESULTS_TABLE -DRIVER_DIR = os.path.dirname(os.path.realpath(drivers.__file__)) -PLATFORM = sys.platform -LOCAL_CHROMEDRIVER = None -LOCAL_GECKODRIVER = None -if "darwin" in PLATFORM or "linux" in PLATFORM: - LOCAL_CHROMEDRIVER = DRIVER_DIR + '/chromedriver' - LOCAL_GECKODRIVER = DRIVER_DIR + '/geckodriver' -elif "win32" in PLATFORM or "win64" in PLATFORM or "x64" in PLATFORM: - LOCAL_CHROMEDRIVER = DRIVER_DIR + '/chromedriver.exe' - LOCAL_GECKODRIVER = DRIVER_DIR + '/geckodriver.exe' def get_timestamp(): @@ -29,54 +17,123 @@ def get_timestamp(): def process_successes(test, test_count, duration): - return( - '"%s","%s","%s","%s","%s","%s","%s","%s","%s","%s"' % ( - test_count, - "Passed!", - "*", - "*", - "*", - test.browser, - get_timestamp()[:-3], - duration, - test.id(), - "*")) + return '"%s","%s","%s","%s","%s","%s","%s","%s","%s","%s"' % ( + test_count, + "Passed!", + "*", + "*", + "*", + test.browser, + get_timestamp()[:-3], + duration, + test.id(), + "*", + ) -def process_failures(test, test_count, browser_type, duration): +def save_test_failure_data(test, name, folder=None): + """ + Saves failure data to the current directory, or to a subfolder if provided. + If {name} does not end in ".txt", it will get added to it. + If the folder provided doesn't exist, it will get created. + """ + if not name.endswith(".txt"): + name = name + ".txt" + if folder: + abs_path = os.path.abspath(".") + file_path = os.path.join(abs_path, folder) + if not os.path.exists(file_path): + os.makedirs(file_path) + failure_data_file_path = os.path.join(file_path, name) + else: + failure_data_file_path = name + failure_data_file = open( + failure_data_file_path, mode="w+", encoding="utf-8" + ) + data_to_save = [] + if not hasattr(sb_config, "_report_test_id"): + exc_message = "(Unknown Exception)" + traceback_message = "" + if hasattr(sb_config, "_report_traceback"): + traceback_message = str(sb_config._report_traceback) + if hasattr(sb_config, "_report_exception"): + if type(sb_config._report_exception) is tuple: + exc_message = str(sb_config._report_exception[1].message) + else: + exc_message = str(sb_config._report_exception) + data_to_save.append(test.id()) + data_to_save.append( + "----------------------------------------------------------------" + ) + data_to_save.append("Last Page: %s" % test._last_page_url) + data_to_save.append(" Browser: %s" % test.browser) + data_to_save.append("Timestamp: %s" % get_timestamp()[:-3]) + data_to_save.append( + "----------------------------------------------------------------" + ) + data_to_save.append("Traceback: %s" % traceback_message) + data_to_save.append("Exception: %s" % exc_message) + failure_data_file.writelines("\r\n".join(data_to_save)) + failure_data_file.close() + return + data_to_save.append(sb_config._report_test_id) + data_to_save.append( + "--------------------------------------------------------------------" + ) + data_to_save.append("Last Page: %s" % sb_config._report_fail_page) + data_to_save.append(" Duration: %s" % sb_config._report_duration) + data_to_save.append(" Browser: %s" % sb_config._report_browser) + data_to_save.append(" Driver: %s" % sb_config._report_driver) + data_to_save.append("Timestamp: %s" % sb_config._report_timestamp) + data_to_save.append(" Date: %s" % sb_config._report_date) + data_to_save.append(" Time: %s" % sb_config._report_time) + data_to_save.append( + "--------------------------------------------------------------------" + ) + data_to_save.append("Traceback: %s" % sb_config._report_traceback) + data_to_save.append("Exception: %s" % sb_config._report_exception) + failure_data_file.writelines("\r\n".join(data_to_save)) + failure_data_file.close() + + +def process_failures(test, test_count, 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) - page_actions.save_test_failure_data( - test.driver, bad_page_data, browser_type, folder=LATEST_REPORT_DIR) - exc_info = '(Unknown Failure)' - exception = sys.exc_info()[1] - if exception: - if hasattr(exception, 'msg'): - exc_info = exception.msg - elif hasattr(exception, 'message'): - exc_info = exception.message - else: - pass - return( - '"%s","%s","%s","%s","%s","%s","%s","%s","%s","%s"' % ( - test_count, - "FAILED!", - bad_page_data, - bad_page_image, - test._last_page_url, - test.browser, - get_timestamp()[:-3], - duration, - test.id(), - exc_info)) + screenshot_path = os.path.join(LATEST_REPORT_DIR, bad_page_image) + if getattr(test, "_last_page_screenshot", None): + with open(screenshot_path, mode="wb") as file: + file.write(test._last_page_screenshot) + save_test_failure_data(test, bad_page_data, folder=LATEST_REPORT_DIR) + exc_message = None + if hasattr(test, "_outcome") and getattr(test._outcome, "errors", None): + try: + exc_message = test._outcome.errors[-1][1][1] + except Exception: + exc_message = "(Unknown Exception)" + else: + try: + 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!", + bad_page_data, + bad_page_image, + test._last_page_url, + test.browser, + get_timestamp()[:-3], + duration, + test.id(), + exc_message, + ) def clear_out_old_report_logs(archive_past_runs=True, get_log_folder=False): - abs_path = os.path.abspath('.') - file_path = abs_path + "/%s" % LATEST_REPORT_DIR + abs_path = os.path.abspath(".") + file_path = os.path.join(abs_path, LATEST_REPORT_DIR) if not os.path.exists(file_path): try: os.makedirs(file_path) @@ -85,32 +142,38 @@ def clear_out_old_report_logs(archive_past_runs=True, get_log_folder=False): if archive_past_runs: archive_timestamp = int(time.time()) - if not os.path.exists("%s/../%s/" % (file_path, ARCHIVE_DIR)): - os.makedirs("%s/../%s/" % (file_path, ARCHIVE_DIR)) - archive_dir = "%s/../%s/report_%s" % ( - file_path, ARCHIVE_DIR, archive_timestamp) + archive_dir_root = os.path.join(file_path, "..", ARCHIVE_DIR) + if not os.path.exists(archive_dir_root): + os.makedirs(archive_dir_root) + archive_dir = os.path.join( + archive_dir_root, "report_%s" % archive_timestamp + ) shutil.move(file_path, archive_dir) os.makedirs(file_path) if get_log_folder: return archive_dir else: # Just delete bad pages to make room for the latest run. - filelist = [f for f in os.listdir( - "./%s" % LATEST_REPORT_DIR) if f.startswith("failure_") or ( - f == HTML_REPORT) or (f.startswith("automation_failure")) or ( - f == RESULTS_TABLE)] + filelist = [ + f + for f in os.listdir(os.path.join(".", LATEST_REPORT_DIR)) + if f.startswith("failure_") + or (f == HTML_REPORT) + or (f.startswith("automation_failure")) + or (f == RESULTS_TABLE) + ] for f in filelist: - os.remove("%s/%s" % (file_path, f)) + os.remove(os.path.join(file_path, f)) def add_bad_page_log_file(page_results_list): - abs_path = os.path.abspath('.') - file_path = abs_path + "/%s" % LATEST_REPORT_DIR - log_file = "%s/%s" % (file_path, RESULTS_TABLE) - f = open(log_file, 'w') - h_p1 = '''"Num","Result","Stacktrace","Screenshot",''' - h_p2 = '''"URL","Browser","Epoch Time","Duration",''' - h_p3 = '''"Test Case Address","Additional Info"\n''' + abs_path = os.path.abspath(".") + file_path = os.path.join(abs_path, LATEST_REPORT_DIR) + log_file = os.path.join(file_path, RESULTS_TABLE) + f = open(log_file, mode="w") + h_p1 = '"Num","Result","Stacktrace","Screenshot",' + h_p2 = '"URL","Browser","Epoch Time","Duration",' + h_p3 = '"Test Case Address","Additional Info"\n' page_header = h_p1 + h_p2 + h_p3 f.write(page_header) for line in page_results_list: @@ -120,27 +183,32 @@ def add_bad_page_log_file(page_results_list): def archive_new_report_logs(): log_string = clear_out_old_report_logs(get_log_folder=True) - log_folder = log_string.split('/')[-1] - abs_path = os.path.abspath('.') - file_path = abs_path + "/%s" % ARCHIVE_DIR - report_log_path = "%s/%s" % (file_path, log_folder) + log_folder = log_string.split("/")[-1] + abs_path = os.path.abspath(".") + file_path = os.path.join(abs_path, ARCHIVE_DIR) + report_log_path = os.path.join(file_path, log_folder) return report_log_path def add_results_page(html): - abs_path = os.path.abspath('.') - file_path = abs_path + "/%s" % LATEST_REPORT_DIR + abs_path = os.path.abspath(".") + file_path = os.path.join(abs_path, LATEST_REPORT_DIR) results_file_name = HTML_REPORT - results_file = "%s/%s" % (file_path, results_file_name) - f = open(results_file, 'w') + results_file = os.path.join(file_path, results_file_name) + f = open(results_file, mode="w") f.write(html) f.close() return results_file -def build_report(report_log_path, page_results_list, - successes, failures, browser_type, - show_report): +def build_report( + report_log_path, + page_results_list, + successes, + failures, + browser_type, + show_report, +): web_log_path = "file://%s" % report_log_path successes_count = len(successes) @@ -151,110 +219,122 @@ def build_report(report_log_path, page_results_list, if failures_count > 0: tf_color = "#EE3A3A" - summary_table = '''<div><table><thead><tr> + summary_table = """<div><table><thead><tr> <th>TESTING SUMMARY</th> <th>        </th> </tr></thead><tbody> <tr style="color:#00BB00"><td>TESTS PASSING: <td>%s</tr> <tr style="color:%s" ><td>TESTS FAILING: <td>%s</tr> <tr style="color:#4D4DDD"><td>TOTAL TESTS: <td>%s</tr> - </tbody></table>''' % (successes_count, - tf_color, - failures_count, - total_test_count) + </tbody></table>""" % ( + successes_count, + tf_color, + failures_count, + total_test_count, + ) - summary_table = '''<h1 id="ContextHeader" class="sectionHeader" title=""> - %s</h1>''' % summary_table + summary_table = ( + """<h1 id="ContextHeader" class="sectionHeader" title=""> + %s</h1>""" + % summary_table + ) - log_link_shown = '../%s%s/' % ( - ARCHIVE_DIR, web_log_path.split(ARCHIVE_DIR)[1]) - csv_link = '%s/%s' % (web_log_path, RESULTS_TABLE) - csv_link_shown = '%s' % RESULTS_TABLE - log_table = '''<p><p><p><p><h2><table><tbody> + log_link_shown = os.path.join( + "..", "%s%s" % (ARCHIVE_DIR, web_log_path.split(ARCHIVE_DIR)[1]) + ) + csv_link = os.path.join(web_log_path, RESULTS_TABLE) + csv_link_shown = "%s" % RESULTS_TABLE + log_table = """<p><p><p><p><h2><table><tbody> <tr><td>LOG FILES LINK:  <td><a href="%s">%s</a></tr> <tr><td>RESULTS TABLE:  <td><a href="%s">%s</a></tr> - </tbody></table></h2><p><p><p><p>''' % ( - web_log_path, log_link_shown, csv_link, csv_link_shown) + </tbody></table></h2><p><p><p><p>""" % ( + web_log_path, + log_link_shown, + csv_link, + csv_link_shown, + ) - failure_table = '<h2><table><tbody></div>' + failure_table = "<h2><table><tbody></div>" any_screenshots = False for line in page_results_list: - line = line.split(',') + line = line.split(",") if line[1] == '"FAILED!"': if not any_screenshots: any_screenshots = True - failure_table += '''<thead><tr> + failure_table += """<thead><tr> <th>STACKTRACE  </th> <th>SCREENSHOT  </th> <th>LOCATION OF FAILURE</th> - </tr></thead>''' + </tr></thead>""" display_url = line[4] + actual_url = line[4] + if len(display_url) < 7: + display_url = sb_config._report_fail_page + actual_url = sb_config._report_fail_page if len(display_url) > 60: - display_url = display_url[0:58] + '...' - line = '<a href="%s">%s</a>' % ( - "file://" + report_log_path + '/' + line[2], line[2]) + ''' + display_url = display_url[0:58] + "..." + line = ( + '<a href="%s">%s</a>' + % ("file://" + report_log_path + "/" + line[2], line[2]) + + """    - ''' + '<td><a href="%s">%s</a>' % ( - "file://" + report_log_path + '/' + line[3], line[3]) + ''' + """ + + '<td><a href="%s">%s</a>' + % ("file://" + report_log_path + "/" + line[3], line[3]) + + """    - ''' + '<td><a href="%s">%s</a>' % (line[4], display_url) - line = line.replace('"', '') - failure_table += '<tr><td>%s</tr>\n' % line - failure_table += '</tbody></table></h2>' + """ + + '<td><a href="%s">%s</a>' % (actual_url, display_url) + ) + line = line.replace('"', "") + failure_table += "<tr><td>%s</tr>\n" % line + failure_table += "</tbody></table></h2>" - failing_list = '' + failing_list = "" if failures: - failing_list = '<h2><table><tbody>' - failing_list += '''<thead><tr><th>LIST OF FAILING TESTS + failing_list = "<h2><table><tbody>" + failing_list += """<thead><tr><th>LIST OF FAILING TESTS          - </th></tr></thead>''' + </th></tr></thead>""" for failure in failures: failing_list += '<tr style="color:#EE3A3A"><td>%s</tr>\n' % failure - failing_list += '</tbody></table></h2>' + failing_list += "</tbody></table></h2>" - passing_list = '' + passing_list = "" if successes: - passing_list = '<h2><table><tbody>' - passing_list += '''<thead><tr><th>LIST OF PASSING TESTS + passing_list = "<h2><table><tbody>" + passing_list += """<thead><tr><th>LIST OF PASSING TESTS          - </th></tr></thead>''' + </th></tr></thead>""" for success in successes: passing_list += '<tr style="color:#00BB00"><td>%s</tr>\n' % success - passing_list += '</tbody></table></h2>' + passing_list += "</tbody></table></h2>" - table_view = '%s%s%s%s%s' % ( - summary_table, log_table, failure_table, failing_list, passing_list) - report_html = '<html><head>%s</head><body>%s</body></html>' % ( - style, table_view) + table_view = "%s%s%s%s%s" % ( + summary_table, + log_table, + failure_table, + failing_list, + passing_list, + ) + report_html = "<html><head>%s</head><body>%s</body></html>" % ( + get_report_style(), + table_view, + ) results_file = add_results_page(report_html) - archived_results_file = report_log_path + '/' + HTML_REPORT + archived_results_file = report_log_path + "/" + HTML_REPORT shutil.copyfile(results_file, archived_results_file) print("\n* The latest html report page is located at:\n" + results_file) print( - "\n* Files saved for this report are located at:\n" + report_log_path) + "\n* Files saved for this report are located at:\n" + report_log_path + ) print("") if show_report: - browser = None - profile = webdriver.FirefoxProfile() - profile.set_preference("app.update.auto", False) - profile.set_preference("app.update.enabled", False) - profile.set_preference("browser.privatebrowsing.autostart", True) - if browser_type == 'firefox': - if LOCAL_GECKODRIVER and os.path.exists(LOCAL_GECKODRIVER): - browser = webdriver.Firefox( - firefox_profile=profile, executable_path=LOCAL_GECKODRIVER) - else: - browser = webdriver.Firefox(firefox_profile=profile) - else: - chrome_options = webdriver.ChromeOptions() - chrome_options.add_argument("--disable-infobars") - if LOCAL_CHROMEDRIVER and os.path.exists(LOCAL_CHROMEDRIVER): - browser = webdriver.Chrome( - executable_path=LOCAL_CHROMEDRIVER, options=chrome_options) - else: - browser = webdriver.Chrome(options=chrome_options) - browser.get("file://%s" % archived_results_file) + from seleniumbase import get_driver + + driver = get_driver(browser_type, headless=False) + driver.get("file://%s" % archived_results_file) print("\n*** Close the html report window to continue. ***") - while len(browser.window_handles): + while len(driver.window_handles): time.sleep(0.1) - browser.quit() + driver.quit() diff --git a/seleniumbase/core/s3_manager.py b/seleniumbase/core/s3_manager.py old mode 100755 new mode 100644 index 27206515b0f..4b201935c69 --- a/seleniumbase/core/s3_manager.py +++ b/seleniumbase/core/s3_manager.py @@ -1,42 +1,52 @@ -""" -Manager for dealing with uploading/managing files on Amazon S3 -""" -from boto.s3.connection import S3Connection -from boto.s3.key import Key -from seleniumbase.config import settings +"""Methods for uploading/managing files on Amazon S3.""" already_uploaded_files = [] class S3LoggingBucket(object): - """ - A class to upload log files from tests to Amazon S3. - Those files can then be shared easily. - """ + """A class for uploading log files from tests to Amazon S3. + Those files can then be shared easily.""" + from seleniumbase.config import settings - def __init__(self, - log_bucket=settings.S3_LOG_BUCKET, - bucket_url=settings.S3_BUCKET_URL, - selenium_access_key=settings.S3_SELENIUM_ACCESS_KEY, - selenium_secret_key=settings.S3_SELENIUM_SECRET_KEY): + def __init__( + self, + log_bucket=settings.S3_LOG_BUCKET, + bucket_url=settings.S3_BUCKET_URL, + selenium_access_key=settings.S3_SELENIUM_ACCESS_KEY, + selenium_secret_key=settings.S3_SELENIUM_SECRET_KEY, + ): + import fasteners + from seleniumbase.fixtures import constants + from seleniumbase.fixtures import shared_utils - self.conn = S3Connection(selenium_access_key, - selenium_secret_key) - self.bucket = self.conn.get_bucket(log_bucket) + pip_find_lock = fasteners.InterProcessLock( + constants.PipInstall.FINDLOCK + ) + with pip_find_lock: + try: + import boto3 + except Exception: + shared_utils.pip_install("boto3") + import boto3 + self.conn = boto3.Session( + aws_access_key_id=selenium_access_key, + aws_secret_access_key=selenium_secret_key, + ) + self.bucket = log_bucket self.bucket_url = bucket_url - def get_key(self, _name): - """ Create a new Key instance with the given name. """ - return Key(bucket=self.bucket, name=_name) + def get_key(self, file_name): + """Create a new S3 connection instance with the given name.""" + return self.conn.resource("s3").Object(self.bucket, file_name) def get_bucket(self): - """ Return the bucket being used. """ + """Return the bucket being used.""" return self.bucket def upload_file(self, file_name, file_path): - """ Upload a given file from the file_path to the bucket - with the new name/path file_name. """ - upload_key = Key(bucket=self.bucket, name=file_name) + """Upload a given file from the file_path to the bucket + with the new name/path file_name.""" + upload_key = self.get_key(file_name) content_type = "text/plain" if file_name.endswith(".html"): content_type = "text/html" @@ -44,19 +54,19 @@ def upload_file(self, file_name, file_path): content_type = "image/jpeg" elif file_name.endswith(".png"): content_type = "image/png" - upload_key.set_contents_from_filename( + upload_key.Bucket().upload_file( file_path, - headers={"Content-Type": content_type}) - upload_key.url = \ - upload_key.generate_url(expires_in=3600).split("?")[0] - try: - upload_key.make_public() - except Exception: - pass + file_name, + ExtraArgs={"ACL": "public-read", "ContentType": content_type}, + ) + + def upload_index_file( + self, test_address, timestamp, data_path, save_data_to_logs + ): + """Create an index.html file with links to all the log files + that were just uploaded.""" + import os - def upload_index_file(self, test_address, timestamp): - """ Create an index.html file with links to all the log files - that were just uploaded. """ global already_uploaded_files already_uploaded_files = list(set(already_uploaded_files)) already_uploaded_files.sort() @@ -64,17 +74,23 @@ def upload_index_file(self, test_address, timestamp): index = self.get_key(file_name) index_str = [] for completed_file in already_uploaded_files: - index_str.append("<a href='" + self.bucket_url + "" - "%s'>%s</a>" % (completed_file, completed_file)) - index.set_contents_from_string( - "<br>".join(index_str), - headers={"Content-Type": "text/html"}) - index.make_public() + index_str.append( + "<a href='" + self.bucket_url + "" + "%s'>%s</a>" % (completed_file, completed_file) + ) + index_page = str("<br>".join(index_str)) + save_data_to_logs(index_page, "index.html") + file_path = os.path.join(data_path, "index.html") + index.Bucket().upload_file( + file_path, + file_name, + ExtraArgs={"ACL": "public-read", "ContentType": "text/html"}, + ) return "%s%s" % (self.bucket_url, file_name) def save_uploaded_file_names(self, files): - """ Keep a record of all file names that've been uploaded. Upload log - files related to each test after its execution. Once done, use - already_uploaded_files to create an index file. """ - global already_uploaded_files + """Keep a record of all file names that have been uploaded. + Upload log files related to each test after its execution. + Once done, use already_uploaded_files to create an index file.""" + global already_uploaded_files # noqa already_uploaded_files.extend(files) diff --git a/seleniumbase/core/sb_cdp.py b/seleniumbase/core/sb_cdp.py new file mode 100644 index 00000000000..713809a0cc8 --- /dev/null +++ b/seleniumbase/core/sb_cdp.py @@ -0,0 +1,3895 @@ +"""The CDP Mode API (sync format), which wraps the async format.""" +import asyncio +import fasteners +import mycdp +import os +import random +import re +import sys +import time +import traceback +from contextlib import suppress +from filelock import FileLock +from seleniumbase import config as sb_config +from seleniumbase.config import settings +from seleniumbase.fixtures import constants +from seleniumbase.fixtures import js_utils +from seleniumbase.fixtures import page_utils +from seleniumbase.fixtures import shared_utils +from seleniumbase.undetected.cdp_driver import cdp_util +from seleniumbase.undetected.cdp_driver import tab as cdp_tab + + +class CDPMethods(): + def __init__(self, loop, page, driver): + self.loop = loop + self.page = page + self.driver = driver + self.__initialize_variables() + + def __initialize_variables(self): + self.__page_source_count = 0 + self.__screenshot_count = 0 + self.__saved_pdf_count = 0 + self.__logs_data_count = 0 + self.__last_data_file = None + # Set variables that may be useful to developers + self.log_path = constants.Logs.LATEST + self.log_abspath = os.path.abspath(self.log_path) + self.data_path = os.path.join(self.log_path, self.__get_script_id()) + self.data_abspath = os.path.abspath(self.data_path) + + def _swap_driver(self, driver): + self.driver = driver + self.page = driver.cdp.page + self.loop = driver.cdp.loop + + def __slow_mode_pause_if_set(self): + if ( + (hasattr(sb_config, "demo_mode") and sb_config.demo_mode) + or "--demo" in sys.argv + ): + time.sleep(0.48) + elif ( + (hasattr(sb_config, "slow_mode") and sb_config.slow_mode) + or "--slow" in sys.argv + ): + time.sleep(0.24) + + def __add_light_pause(self): + time.sleep(0.007) + + def __convert_to_css_if_xpath(self, selector): + if page_utils.is_xpath_selector(selector): + with suppress(Exception): + css = js_utils.convert_to_css_selector(selector, "xpath") + if css: + return css + return selector + + def __add_sync_methods(self, element): + if not element: + return element + element.clear_input = lambda: self.__clear_input(element) + element.click = lambda: self.__click(element) + element.flash = lambda *args, **kwargs: self.__flash( + element, *args, **kwargs + ) + element.focus = lambda: self.__focus(element) + element.gui_click = ( + lambda *args, **kwargs: self.__gui_click(element, *args, **kwargs) + ) + element.highlight_overlay = lambda: self.__highlight_overlay(element) + element.is_in_viewport = lambda: self.__is_in_viewport(element) + element.mouse_click = lambda: self.__mouse_click(element) + element.click_with_offset = ( + lambda *args, **kwargs: self.__mouse_click_with_offset_async( + element, *args, **kwargs + ) + ) + element.click_and_hold = ( + lambda *args, **kwargs: ( + self.__mouse_click_and_hold(element, *args, **kwargs) + ) + ) + element.mouse_drag = ( + lambda destination: self.__mouse_drag(element, destination) + ) + element.mouse_move = lambda: self.__mouse_move(element) + element.press_keys = lambda text: self.__press_keys(element, text) + element.query_selector = ( + lambda selector: self.__query_selector(element, selector) + ) + element.querySelector = element.query_selector + element.query_selector_all = ( + lambda selector: self.__query_selector_all(element, selector) + ) + element.querySelectorAll = element.query_selector_all + element.remove_from_dom = lambda: self.__remove_from_dom(element) + element.save_screenshot = ( + lambda *args, **kwargs: self.__save_screenshot( + element, *args, **kwargs) + ) + element.save_to_dom = lambda: self.__save_to_dom(element) + element.scroll_into_view = lambda: self.__scroll_into_view(element) + element.select_option = lambda: self.__select_option(element) + element.send_file = ( + lambda *file_paths: self.__send_file(element, *file_paths) + ) + element.send_keys = lambda text: self.__send_keys(element, text) + element.set_text = lambda value: self.__set_text(element, value) + element.set_value = lambda value: self.__set_value(element, value) + element.type = lambda text: self.__type(element, text) + element.get_position = lambda: self.__get_position(element) + element.get_html = lambda: self.__get_html(element) + element.get_js_attributes = lambda: self.__get_js_attributes(element) + element.get_attribute = ( + lambda attribute: self.__get_attribute(element, attribute) + ) + # element.get_parent() should come last + element.get_parent = lambda: self.__get_parent(element) + return element + + def get(self, url, **kwargs): + url = shared_utils.fix_url_as_needed(url) + driver = self.driver + if hasattr(driver, "cdp_base"): + driver = driver.cdp_base + load_timeout = 60.0 + wait_timeout = 50.0 + if hasattr(sb_config, "_cdp_proxy") and sb_config._cdp_proxy: + load_timeout = 90.0 + wait_timeout = 75.0 + try: + task = self.page.get(url, **kwargs) + self.loop.run_until_complete( + asyncio.wait_for(task, timeout=load_timeout) + ) + except asyncio.TimeoutError: + print("Timeout loading %s" % url) + except RuntimeError: + self.loop.run_until_complete( + self.page.get(url, **kwargs) + ) + url_protocol = url.split(":")[0] + safe_url = True + if url_protocol not in ["about", "data", "chrome"]: + safe_url = False + if not safe_url: + time.sleep(constants.UC.CDP_MODE_OPEN_WAIT) + if shared_utils.is_windows(): + time.sleep(constants.UC.EXTRA_WINDOWS_WAIT) + else: + time.sleep(0.012) + self.__slow_mode_pause_if_set() + try: + self.loop.run_until_complete( + asyncio.wait_for(self.page.wait(), timeout=wait_timeout) + ) + except asyncio.TimeoutError: + pass + except Exception: + pass + + def activate_cdp_mode(self, url=None, **kwargs): + """If calling this method from here, then it was likely done + by accident because CDP Mode is already active at this point. + If URL, then navigate there with any kwargs that were passed. + If no URL (but kwargs are included) then refresh with kwargs. + If no URL and no kwargs either, then do nothing.""" + if url: + self.get(url, **kwargs) + elif kwargs: + self.get(self.get_current_url(), **kwargs) + + def open(self, url, **kwargs): + self.get(url, **kwargs) + + def goto(self, url, **kwargs): + self.get(url, **kwargs) + + def reload(self, ignore_cache=False, script_to_evaluate_on_load=None): + self.loop.run_until_complete( + self.page.reload( + ignore_cache=ignore_cache, + script_to_evaluate_on_load=script_to_evaluate_on_load, + ) + ) + + def refresh(self, *args, **kwargs): + self.reload(*args, **kwargs) + + def goto_if_not_url(self, url): + """Opens the url in the browser if it's not the current url (*). + Parameters tagged on by search engines are ignored for this method. + Eg. If the current url is: + * https://www.bing.com/search?q=SeleniumBase&source=hp + And the url privided by this method call is: + * https://www.bing.com/search?q=SeleniumBase + Then the urls will be considered the same, + and no open() action will be performed. + This method is primarily used by Recorder Mode script generation, + where both clicks and opens are recorded. So if a click() action + leads to an goto() action, then the script generator will attempt + to convert the goto() action into goto_if_not_url() so that the + same page isn't opened again if the user is already on the page.""" + current_url = self.get_current_url() + if current_url != url: + if ( + "?q=" not in current_url + or "&" not in current_url + or current_url.find("?q=") >= current_url.find("&") + or current_url.split("&")[0] != url + ): + self.goto(url) + + def get_event_loop(self): + return self.loop + + def get_rd_host(self): + """Returns the remote-debugging host (likely 127.0.0.1)""" + driver = self.driver + if hasattr(driver, "cdp_base"): + driver = driver.cdp_base + return driver.config.host + + def get_rd_port(self): + """Returns the remote-debugging port (commonly 9222)""" + driver = self.driver + if hasattr(driver, "cdp_base"): + driver = driver.cdp_base + return driver.config.port + + def get_rd_url(self): + """Returns the remote-debugging URL, which is used for + allowing the Playwright integration to launch stealthy, + and also applies nest-asyncio for nested event loops so + that SeleniumBase methods can be called from Playwright + without encountering event loop error messages such as: + Cannot run the event loop while another loop is running. + Also sets an environment variable to hide this warning: + Deprecation: "url.parse() behavior is not standardized". + (github.com/microsoft/playwright-python/issues/3016)""" + from seleniumbase.core import nest_asyncio + nest_asyncio.apply() + os.environ["NODE_NO_WARNINGS"] = "1" + driver = self.driver + if hasattr(driver, "cdp_base"): + driver = driver.cdp_base + host = driver.config.host + port = driver.config.port + return f"http://{host}:{port}" + + def get_endpoint_url(self): + """Same as get_rd_url(), which returns the remote-debugging URL.""" + return self.get_rd_url() + + def get_port(self): + """Same as get_rd_port(), which returns the remote-debugging port.""" + return self.get_rd_port() + + def get_websocket_url(self): + """Returns the websocket URL of the active tab. + The websocket URL starts with `ws://`.""" + return self.get_active_tab().websocket_url + + def add_handler(self, event, handler): + self.page.add_handler(event, handler) + + def find_element(self, selector, best_match=False, timeout=None): + """Similar to select(), but also finds elements by text content. + When using text-based searches, if best_match=False, then will + find the first element with the text. If best_match=True, then + if multiple elements have that text, then will use the element + with the closest text-length to the text being searched for.""" + if not timeout: + timeout = settings.SMALL_TIMEOUT + self.__add_light_pause() + selector = self.__convert_to_css_if_xpath(selector) + early_failure = False + if (":contains(") in selector: + selector, _ = page_utils.recalculate_selector( + selector, by="css selector", xp_ok=True + ) + failure = False + try: + if early_failure: + raise Exception("Failed!") + if ( + "contains(" not in selector + and not page_utils.is_xpath_selector(selector) + and not re.findall(r"\.\s", selector) + and not re.findall(r"\.$", selector) + and not re.findall(r"#\s", selector) + and not re.findall(r"#$", selector) + and ( + selector in ["html", "body"] + or "[" in selector + or re.findall(r"\.\S", selector) + or re.findall(r"#\S", selector) + or " > " in selector + ) + ): + element = self.loop.run_until_complete( + self.page.select(selector, timeout=timeout) + ) + else: + element = self.loop.run_until_complete( + self.page.find( + selector, best_match=best_match, timeout=timeout + ) + ) + except Exception: + failure = True + plural = "s" + if timeout == 1: + plural = "" + message = "\n Element {%s} was not found after %s second%s!" % ( + selector, + timeout, + plural, + ) + if failure: + raise Exception(message) + element = self.__add_sync_methods(element) + self.__slow_mode_pause_if_set() + return element + + def find_element_by_text(self, text, tag_name=None, timeout=None): + """Returns an element by matching text. + Optionally, provide a tag_name to narrow down the search to an + element with the given tag. (Eg: a, button, div, script, span)""" + if not timeout: + timeout = settings.SMALL_TIMEOUT + if tag_name: + try: + return self.find_element( + '%s:contains("%s")' % (tag_name, text), timeout=timeout + ) + except Exception: + pass # The exception will be raised later + else: + self.__add_light_pause() + self.assert_text(text, timeout=timeout) + elements = self.loop.run_until_complete( + self.page.find_elements_by_text(text=text) + ) + for element in elements: + if element: + element = self.__add_sync_methods(element) + return self.__add_sync_methods(element) + plural = "s" + if timeout == 1: + plural = "" + raise Exception( + "Text {%s} with tag {%s} was not found after %s second%s!" + % (text, tag_name, timeout, plural) + ) + + def find_all(self, selector, timeout=None): + if not timeout: + timeout = settings.SMALL_TIMEOUT + self.__add_light_pause() + selector = self.__convert_to_css_if_xpath(selector) + elements = self.loop.run_until_complete( + self.page.find_all(selector, timeout=timeout) + ) + updated_elements = [] + for element in elements: + element = self.__add_sync_methods(element) + updated_elements.append(element) + return updated_elements + + def find_elements_by_text(self, text, tag_name=None): + """Returns a list of elements by matching text. + Optionally, provide a tag_name to narrow down the search to only + elements with the given tag. (Eg: a, button, div, script, span)""" + self.__add_light_pause() + elements = self.loop.run_until_complete( + self.page.find_elements_by_text(text=text) + ) + updated_elements = [] + if tag_name: + tag_name = tag_name.lower().strip() + for element in elements: + if element and not tag_name: + element = self.__add_sync_methods(element) + if element not in updated_elements: + updated_elements.append(element) + elif ( + element + and tag_name in element.tag_name.lower() + and text.strip() in element.text + ): + element = self.__add_sync_methods(element) + if element not in updated_elements: + updated_elements.append(element) + elif ( + element + and element.parent + and tag_name in element.parent.tag_name.lower() + and text.strip() in element.parent.text + ): + element = self.__add_sync_methods(element.parent) + if element not in updated_elements: + updated_elements.append(element) + elif ( + element + and element.parent + and element.parent.parent + and tag_name in element.parent.parent.tag_name.lower() + and text.strip() in element.parent.parent.text + ): + element = self.__add_sync_methods(element.parent.parent) + if element not in updated_elements: + updated_elements.append(element) + return updated_elements + + def select(self, selector, timeout=None): + """Similar to find_element().""" + if not timeout: + timeout = settings.SMALL_TIMEOUT + self.__add_light_pause() + selector = self.__convert_to_css_if_xpath(selector) + if (":contains(" in selector): + return self.find_element(selector, timeout=timeout) + failure = False + message = "" + try: + element = self.loop.run_until_complete( + self.page.select(selector, timeout=timeout) + ) + except Exception: + failure = True + plural = "s" + if timeout == 1: + plural = "" + msg = "\n Element {%s} was not found after %s second%s!" + message = msg % (selector, timeout, plural) + if failure: + raise Exception(message) + element = self.__add_sync_methods(element) + self.__slow_mode_pause_if_set() + return element + + def select_all(self, selector, timeout=None): + if not timeout: + timeout = settings.MINI_TIMEOUT + self.__add_light_pause() + selector = self.__convert_to_css_if_xpath(selector) + if not self.is_element_present(selector): + time.sleep(1) + timeout = timeout - 1 + if timeout < 1: + timeout = 1 + try: + self.select(selector, timeout=timeout) + except Exception: + return [] + elements = self.loop.run_until_complete( + self.page.select_all(selector, timeout=0.1) + ) + updated_elements = [] + for element in elements: + element = self.__add_sync_methods(element) + updated_elements.append(element) + return updated_elements + + def find_elements(self, selector, timeout=None): + if not timeout: + timeout = settings.MINI_TIMEOUT + return self.select_all(selector, timeout=timeout) + + def find_visible_elements(self, selector, timeout=None): + if not timeout: + timeout = settings.MINI_TIMEOUT + visible_elements = [] + elements = self.select_all(selector, timeout=timeout) + for element in elements: + with suppress(Exception): + position = element.get_position() + if (position.width != 0 or position.height != 0): + visible_elements.append(element) + return visible_elements + + def click_nth_element(self, selector, number, scroll=True): + elements = self.select_all(selector) + if len(elements) < number: + raise Exception( + "Not enough matching {%s} elements to " + "click number %s!" % (selector, number) + ) + number = number - 1 + if number < 0: + number = 0 + element = elements[number] + if scroll: + element.scroll_into_view() + try: + element.mouse_click() # Simulated click (NO PyAutoGUI) + except Exception: + element.click() # Standard CDP click (Can be detected) + + def click_nth_visible_element(self, selector, number, scroll=True): + """Finds all matching page elements and clicks the nth visible one. + Example: self.click_nth_visible_element('[type="checkbox"]', 5) + (Clicks the 5th visible checkbox on the page.)""" + elements = self.find_visible_elements(selector) + if len(elements) < number: + raise Exception( + "Not enough matching {%s} elements to " + "click number %s!" % (selector, number) + ) + number = number - 1 + if number < 0: + number = 0 + element = elements[number] + if scroll: + element.scroll_into_view() + try: + element.mouse_click() # Simulated click (NO PyAutoGUI) + except Exception: + element.click() # Standard CDP click (Can be detected) + + def click_link(self, link_text): + self.find_elements_by_text(link_text, "a")[0].click() + + def go_back(self): + self.loop.run_until_complete(self.page.back()) + + def go_forward(self): + self.loop.run_until_complete(self.page.forward()) + + def get_navigation_history(self): + return self.loop.run_until_complete(self.page.get_navigation_history()) + + def __clear_input(self, element): + return ( + self.loop.run_until_complete(element.clear_input_async()) + ) + + def __click(self, element): + result = ( + self.loop.run_until_complete(element.click_async()) + ) + self.loop.run_until_complete(self.page.wait(0.2)) + return result + + def __flash(self, element, *args, **kwargs): + element.scroll_into_view() + if len(args) < 3 and "x_offset" not in kwargs: + x_offset = self.__get_x_scroll_offset() + kwargs["x_offset"] = x_offset + if len(args) < 3 and "y_offset" not in kwargs: + y_offset = self.__get_y_scroll_offset() + kwargs["y_offset"] = y_offset + return ( + self.loop.run_until_complete( + element.flash_async(*args, **kwargs) + ) + ) + + def __focus(self, element): + return ( + self.loop.run_until_complete(element.focus_async()) + ) + + def __gui_click(self, element, timeframe=None): + element.scroll_into_view() + self.__add_light_pause() + position = element.get_position() + x = position.x + y = position.y + e_width = position.width + e_height = position.height + # Relative to window + element_rect = {"height": e_height, "width": e_width, "x": x, "y": y} + window_rect = self.get_window_rect() + w_bottom_y = window_rect["y"] + window_rect["height"] + viewport_height = window_rect["innerHeight"] + x = window_rect["x"] + element_rect["x"] + y = w_bottom_y - viewport_height + element_rect["y"] + y_scroll_offset = window_rect["pageYOffset"] + y = y - y_scroll_offset + x = x + window_rect["scrollX"] + y = y + window_rect["scrollY"] + # Relative to screen + element_rect = {"height": e_height, "width": e_width, "x": x, "y": y} + e_width = element_rect["width"] + e_height = element_rect["height"] + e_x = element_rect["x"] + e_y = element_rect["y"] + x, y = ((e_x + e_width / 2.0) + 0.5), ((e_y + e_height / 2.0) + 0.5) + if not timeframe or not isinstance(timeframe, (int, float)): + timeframe = 0.25 + if timeframe > 3: + timeframe = 3 + self.gui_click_x_y(x, y, timeframe=timeframe) + return self.loop.run_until_complete(self.page.wait(0.2)) + + def __highlight_overlay(self, element): + return ( + self.loop.run_until_complete(element.highlight_overlay_async()) + ) + + def __is_in_viewport(self, element): + return ( + self.loop.run_until_complete(element.is_in_viewport_async()) + ) + + def __mouse_click(self, element, timeframe=0): + result = ( + self.loop.run_until_complete( + element.mouse_click_async(timeframe=timeframe) + ) + ) + self.loop.run_until_complete(self.page.wait(0.2)) + return result + + def __mouse_click_and_hold(self, element, timeframe=2): + result = ( + self.loop.run_until_complete( + element.mouse_click_async(timeframe=timeframe) + ) + ) + self.loop.run_until_complete(self.page.wait(0.2)) + return result + + def __mouse_click_with_offset_async(self, element, *args, **kwargs): + result = ( + self.loop.run_until_complete( + element.mouse_click_with_offset_async(*args, **kwargs) + ) + ) + self.loop.run_until_complete(self.page.wait(0.2)) + return result + + def __mouse_drag(self, element, destination): + return ( + self.loop.run_until_complete(element.mouse_drag_async(destination)) + ) + + def __mouse_move(self, element): + return ( + self.loop.run_until_complete(element.mouse_move_async()) + ) + + def __press_keys(self, element, text): + element.scroll_into_view() + submit = False + if text.endswith("\n") or text.endswith("\r"): + submit = True + text = text[:-1] + for key in text: + element.send_keys(key) + time.sleep(float(0.0422 + (random.random() / 112.0))) + if submit: + element.send_keys("\r\n") + time.sleep(0.044) + self.__slow_mode_pause_if_set() + return self.loop.run_until_complete(self.page.sleep(0.025)) + + def __query_selector(self, element, selector): + selector = self.__convert_to_css_if_xpath(selector) + element2 = self.loop.run_until_complete( + element.query_selector_async(selector) + ) + element2 = self.__add_sync_methods(element2) + return element2 + + def __query_selector_all(self, element, selector): + selector = self.__convert_to_css_if_xpath(selector) + elements = self.loop.run_until_complete( + element.query_selector_all_async(selector) + ) + updated_elements = [] + for element in elements: + element = self.__add_sync_methods(element) + updated_elements.append(element) + self.__slow_mode_pause_if_set() + return updated_elements + + def __remove_from_dom(self, element): + return ( + self.loop.run_until_complete(element.remove_from_dom_async()) + ) + + def __save_screenshot(self, element, *args, **kwargs): + return ( + self.loop.run_until_complete( + element.save_screenshot_async(*args, **kwargs) + ) + ) + + def __save_to_dom(self, element): + return ( + self.loop.run_until_complete(element.save_to_dom_async()) + ) + + def __scroll_into_view(self, element): + self.loop.run_until_complete(element.scroll_into_view_async()) + self.__add_light_pause() + return None + + def __select_option(self, element): + return ( + self.loop.run_until_complete(element.select_option_async()) + ) + + def __send_file(self, element, *file_paths): + return ( + self.loop.run_until_complete(element.send_file_async(*file_paths)) + ) + + def __send_keys(self, element, text): + return ( + self.loop.run_until_complete(element.send_keys_async(text)) + ) + + def __set_text(self, element, value): + return ( + self.loop.run_until_complete(element.set_text_async(value)) + ) + + def __set_value(self, element, value): + return ( + self.loop.run_until_complete(element.set_value_async(value)) + ) + + def __type(self, element, text): + with suppress(Exception): + element.clear_input() + element.send_keys(text) + + def __get_position(self, element): + return ( + self.loop.run_until_complete(element.get_position_async()) + ) + + def __get_html(self, element): + return ( + self.loop.run_until_complete(element.get_html_async()) + ) + + def __get_js_attributes(self, element): + return ( + self.loop.run_until_complete(element.get_js_attributes_async()) + ) + + def __get_attribute(self, element, attribute): + try: + return element.get_js_attributes()[attribute] + except Exception: + if not attribute: + raise + try: + attribute_str = element.get_js_attributes() + locate = ' %s="' % attribute + if locate in attribute_str.outerHTML: + outer_html = attribute_str.outerHTML + attr_start = outer_html.find(locate) + len(locate) + attr_end = outer_html.find('"', attr_start) + value = outer_html[attr_start:attr_end] + return value + except Exception: + pass + return None + + def __get_parent(self, element): + return self.__add_sync_methods(element.parent) + + def __get_x_scroll_offset(self): + x_scroll_offset = self.loop.run_until_complete( + self.page.evaluate("window.pageXOffset") + ) + return x_scroll_offset or 0 + + def __get_y_scroll_offset(self): + y_scroll_offset = self.loop.run_until_complete( + self.page.evaluate("window.pageYOffset") + ) + return y_scroll_offset or 0 + + def tile_windows(self, windows=None, max_columns=0): + """Tile windows and return the grid of tiled windows.""" + driver = self.driver + if hasattr(driver, "cdp_base"): + driver = driver.cdp_base + return self.loop.run_until_complete( + driver.tile_windows(windows, max_columns) + ) + + def grant_permissions(self, permissions, origin=None): + """Grant specific permissions to the current window. + Applies to all origins if no origin is specified.""" + driver = self.driver + if hasattr(driver, "cdp_base"): + driver = driver.cdp_base + return self.loop.run_until_complete( + driver.grant_permissions(permissions, origin) + ) + + def grant_all_permissions(self): + """Grant all permissions to the current window for all origins.""" + driver = self.driver + if hasattr(driver, "cdp_base"): + driver = driver.cdp_base + return self.loop.run_until_complete(driver.grant_all_permissions()) + + def reset_permissions(self): + """Reset permissions for all origins on the current window.""" + driver = self.driver + if hasattr(driver, "cdp_base"): + driver = driver.cdp_base + return self.loop.run_until_complete(driver.reset_permissions()) + + def get_all_urls(self, absolute=True): + """ + Convenience function that returns all links (a,link,img,script,meta). + :param absolute: + Try to build all the links in absolute form + instead of "as is", often relative. + :return: List of URLs. + """ + return self.loop.run_until_complete( + self.page.get_all_urls(absolute=absolute) + ) + + def get_all_cookies(self, *args, **kwargs): + driver = self.driver + if hasattr(driver, "cdp_base"): + driver = driver.cdp_base + return self.loop.run_until_complete( + driver.cookies.get_all(*args, **kwargs) + ) + + def set_all_cookies(self, *args, **kwargs): + driver = self.driver + if hasattr(driver, "cdp_base"): + driver = driver.cdp_base + return self.loop.run_until_complete( + driver.cookies.set_all(*args, **kwargs) + ) + + def save_cookies(self, *args, **kwargs): + driver = self.driver + if hasattr(driver, "cdp_base"): + driver = driver.cdp_base + return self.loop.run_until_complete( + driver.cookies.save(*args, **kwargs) + ) + + def load_cookies(self, *args, **kwargs): + driver = self.driver + if hasattr(driver, "cdp_base"): + driver = driver.cdp_base + return self.loop.run_until_complete( + driver.cookies.load(*args, **kwargs) + ) + + def clear_cookies(self): + driver = self.driver + if hasattr(driver, "cdp_base"): + driver = driver.cdp_base + return self.loop.run_until_complete(driver.cookies.clear()) + + def sleep(self, seconds): + time.sleep(seconds) + + def bring_active_window_to_front(self): + self.loop.run_until_complete(self.page.bring_to_front()) + self.__add_light_pause() + + def get_active_element(self): + return self.loop.run_until_complete( + self.page.js_dumps("document.activeElement") + ) + + def get_active_element_css(self): + from seleniumbase.js_code import active_css_js + + js_code = active_css_js.get_active_element_css + js_code = js_code.replace("return getBestSelector", "getBestSelector") + return self.loop.run_until_complete(self.page.evaluate(js_code)) + + def click(self, selector, timeout=None, scroll=True): + if not timeout: + timeout = settings.SMALL_TIMEOUT + self.__slow_mode_pause_if_set() + element = self.find_element(selector, timeout=timeout) + if scroll: + element.scroll_into_view() + try: + element.mouse_click() # Simulated click (NO PyAutoGUI) + except Exception: + element.click() # Standard CDP click (Can be detected) + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.wait(0.2)) + + def click_active_element(self): + self.loop.run_until_complete( + self.page.evaluate("document.activeElement.click()") + ) + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.wait(0.2)) + + def click_if_visible(self, selector, timeout=0, scroll=True): + if self.is_element_visible(selector): + with suppress(Exception): + self.click(selector, timeout=1, scroll=scroll) + elif timeout == 0: + return + else: + with suppress(Exception): + self.find_element(selector, timeout=timeout) + if self.is_element_visible(selector): + self.click(selector, timeout=1) + + def click_visible_elements(self, selector, limit=0, scroll=True): + """Finds all matching page elements and clicks visible ones in order. + If a click reloads or opens a new page, the clicking will stop. + If no matching elements appear, an Exception will be raised. + If "limit" is set and > 0, will only click that many elements. + Also clicks elements that become visible from previous clicks. + Works best for actions such as clicking all checkboxes on a page. + Example: self.click_visible_elements('input[type="checkbox"]')""" + elements = self.select_all(selector) + click_count = 0 + for element in elements: + if limit and limit > 0 and click_count >= limit: + return + try: + width = 0 + height = 0 + try: + position = element.get_position() + width = position.width + height = position.height + except Exception: + continue + if (width != 0 or height != 0): + if scroll: + element.scroll_into_view() + try: + element.mouse_click() # Simulated click (NO PyAutoGUI) + except Exception: + element.click() # Standard CDP click (Can be detected) + click_count += 1 + time.sleep(0.044) + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.wait(0.2)) + except Exception: + break + + def click_and_hold(self, selector, timeout=None, scroll=True, timeframe=2): + """(Simulate a mouse click & hold)""" + if not timeout: + timeout = settings.SMALL_TIMEOUT + self.__slow_mode_pause_if_set() + element = self.find_element(selector, timeout=timeout) + if scroll: + element.scroll_into_view() + element.click_and_hold(timeframe) + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.wait(0.2)) + + def drag_and_drop( + self, drag_selector, drop_selector, timeout=None, scroll=True, + ): + """(Simulate a mouse drag & drop)""" + if not timeout: + timeout = settings.SMALL_TIMEOUT + self.__slow_mode_pause_if_set() + self.find_element(drag_selector, timeout=timeout) + drop_target = self.find_element(drop_selector, timeout=timeout) + if scroll: + drop_target.scroll_into_view() + script = f"""(() => {{ + var e = document.querySelector("{drag_selector}"), + n = document.querySelector("{drop_selector}"), + i = 0, o = 0, r = 1, + a = '', + s = 'alt' === a || '\ue00a' === a, + l = 'ctrl' === a || '\ue009' === a, + c = 'shift' === a || '\ue008' === a, + u = e.ownerDocument, + f = e.getBoundingClientRect(), + g = n ? n.getBoundingClientRect() : f, + p = f.left + f.width / 2, + d = f.top + f.height / 2, + h = g.left + (i || g.width / 2), + m = g.top + (o || g.height / 2), + v = u.elementFromPoint(p, d), + y = u.elementFromPoint(h, m); + if (!v || !y) {{ + var E = new Error( + 'source or target element is not interactable'); + throw E.code = 15, E; + }} + var _ = {{ + constructor: DataTransfer, + effectAllowed: null, + dropEffect: null, + types: [], + files: Object.setPrototypeOf([], null), + _items: Object.setPrototypeOf([], {{ + add: function(t, e) {{ + this[this.length] = {{ + _data: '' + t, + kind: 'string', + type: e, + getAsFile: function() {{}}, + getAsString: function(t) {{ t(this._data); }} + }}; + _.types.push(e); + }}, + remove: function(t) {{ + Array.prototype.splice.call(this, 65535 & t, 1); + _.types.splice(65535 & t, 1); + }}, + clear: function(t, e) {{ + this.length = 0; + _.types.length = 0; + }} + }}), + setData: function(t, e) {{ + this.clearData(t); this._items.add(e, t); }}, + getData: function(t) {{ + for ( + var e = this._items.length; + e-- && this._items[e].type !== t;); + return e >= 0 ? this._items[e]._data : null; + }}, + clearData: function(t) {{ + for ( + var e = this._items.length; + e-- && this._items[e].type !== t;); + this._items.remove(e); + }}, + setDragImage: function(t) {{}} + }}; + function w(t, e, n, i) {{ + for (var o = 0; o < e.length; ++o) {{ + var r = u.createEvent('MouseEvent'); + r.initMouseEvent( + e[o], !0, !0, u.defaultView, + 0, 0, 0, p, d, l, s, c, !1, 0, null); + t.dispatchEvent(r); + }} + i && setTimeout(i, n); + }} + function D(t, e, n, i) {{ + var o = u.createEvent('DragEvent'); + o.initMouseEvent( + e, !0, !0, u.defaultView, 0, 0, + 0, p, d, l, s, c, !1, 0, null); + Object.setPrototypeOf(o, null); + o.dataTransfer = _; + Object.setPrototypeOf(o, DragEvent.prototype); + t.dispatchEvent(o); + i && setTimeout(i, n); + }} + 'items' in DataTransfer.prototype && (_.items = _._items); + w(v, ['pointerdown', 'mousedown'], 1, function() {{ + for (var t = v; t && !t.draggable;) t = t.parentElement; + if (t && t.contains(v)) {{ + var e = y.getBoundingClientRect(); + D(v, 'dragstart', r, function() {{ + var t = y.getBoundingClientRect(); + p = t.left + h - e.left; + d = t.top + m - e.top; + D(y, 'dragenter', 1, function() {{ + D(y, 'dragover', r, function() {{ + D( + u.elementFromPoint(p, d), + 'drop', 1, function() {{ + D(v, 'dragend', 1, function() {{ + w( + u.elementFromPoint(p, d), + ['mouseup', 'pointerup']); + }}); + }}); + }}); + }}); + }}); + }} + }}); + }})();""" + self.evaluate(script) + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.wait(0.2)) + + def mouse_click(self, selector, timeout=None, scroll=True): + """(Simulate a mouse click)""" + if not timeout: + timeout = settings.SMALL_TIMEOUT + self.__slow_mode_pause_if_set() + element = self.find_element(selector, timeout=timeout) + if scroll: + element.scroll_into_view() + element.mouse_click() + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.wait(0.2)) + + def nested_click(self, parent_selector, selector): + """ + Find parent element and click on child element inside it. + (This can be used to click on elements inside an iframe.) + """ + element = self.find_element(parent_selector) + element.query_selector(selector).mouse_click() + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.wait(0.2)) + + def get_nested_element(self, parent_selector, selector): + """(Can be used to find an element inside an iframe)""" + element = self.find_element(parent_selector) + return element.query_selector(selector) + + def select_option_by_text(self, dropdown_selector, option): + element = self.find_element(dropdown_selector) + element.scroll_into_view() + options = element.query_selector_all("option") + for found_option in options: + if found_option.text.strip() == option.strip(): + try: + self.type(dropdown_selector, option) + except Exception: + found_option.select_option() + return + raise Exception( + "Unable to find text option {%s} in dropdown {%s}!" + % (dropdown_selector, option) + ) + + def select_option_by_index(self, dropdown_selector, option): + element = self.find_element(dropdown_selector) + element.scroll_into_view() + options = element.query_selector_all("option") + count = 0 + for found_option in options: + if count == int(option): + found_option.select_option() + return + count += 1 + raise Exception( + "Unable to find index option {%s} in dropdown {%s}!" + % (dropdown_selector, option) + ) + + def select_option_by_value(self, dropdown_selector, option): + element = self.find_element(dropdown_selector) + element.scroll_into_view() + options = element.query_selector_all("option") + for found_option in options: + if ( + "value" in found_option.attrs + and str(found_option.attrs["value"]) == str(option) + ): + found_option.select_option() + return + raise Exception( + "Unable to find value option {%s} in dropdown {%s}!" + % (dropdown_selector, option) + ) + + def flash( + self, + selector, # The CSS Selector to flash + duration=1, # (seconds) flash duration + color="44CC88", # RGB hex flash color + pause=0, # (seconds) If 0, the next action starts during flash + ): + """Paint a quickly-vanishing dot over an element.""" + selector = self.__convert_to_css_if_xpath(selector) + element = self.find_element(selector) + element.scroll_into_view() + x_offset = self.__get_x_scroll_offset() + y_offset = self.__get_y_scroll_offset() + element.flash(duration, color, x_offset, y_offset) + if pause and isinstance(pause, (int, float)): + time.sleep(pause) + + def highlight(self, selector, scroll=True, timeout=None): + """Highlight an element with multi-colors.""" + if not timeout: + timeout = settings.SMALL_TIMEOUT + selector = self.__convert_to_css_if_xpath(selector) + element = self.find_element(selector, timeout=timeout) + if scroll: + element.scroll_into_view() + x_offset = self.__get_x_scroll_offset() + y_offset = self.__get_y_scroll_offset() + element.flash(0.46, "44CC88", x_offset, y_offset) + time.sleep(0.15) + element.flash(0.42, "8844CC", x_offset, y_offset) + time.sleep(0.15) + element.flash(0.38, "CC8844", x_offset, y_offset) + time.sleep(0.15) + element.flash(0.30, "44CC88", x_offset, y_offset) + time.sleep(0.30) + + def focus(self, selector, timeout=None): + if not timeout: + timeout = settings.SMALL_TIMEOUT + element = self.find_element(selector, timeout=timeout) + element.scroll_into_view() + element.focus() + + def highlight_overlay(self, selector): + self.find_element(selector).highlight_overlay() + + def get_parent(self, element): + if isinstance(element, str): + element = self.select(element) + return self.__add_sync_methods(element.parent) + + def remove_element(self, selector): + self.select(selector).remove_from_dom() + + def remove_from_dom(self, selector): + self.select(selector).remove_from_dom() + + def remove_elements(self, selector): + """Remove all elements on the page that match the selector.""" + css_selector = self.__convert_to_css_if_xpath(selector) + css_selector = re.escape(css_selector) # Add "\\" to special chars + css_selector = js_utils.escape_quotes_if_needed(css_selector) + js_code = ( + """var $elements = document.querySelectorAll('%s'); + var index = 0, length = $elements.length; + for(; index < length; index++){ + $elements[index].remove();}""" + % css_selector + ) + with suppress(Exception): + self.loop.run_until_complete(self.page.evaluate(js_code)) + + def send_keys(self, selector, text, timeout=None): + if not timeout: + timeout = settings.SMALL_TIMEOUT + self.__slow_mode_pause_if_set() + element = self.select(selector, timeout=timeout) + element.scroll_into_view() + if text.endswith("\n") or text.endswith("\r"): + text = text[:-1] + "\r\n" + elif ( + element.tag_name == "textarea" + and "\n" in text + and "\r" not in text + ): + text = text.replace("\n", "\r") + element.send_keys(text) + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.sleep(0.025)) + + def press_keys(self, selector, text, timeout=None): + """Similar to send_keys(), but presses keys at human speed.""" + if not timeout: + timeout = settings.SMALL_TIMEOUT + self.__slow_mode_pause_if_set() + element = self.select(selector, timeout=timeout) + element.scroll_into_view() + submit = False + if text.endswith("\n") or text.endswith("\r"): + submit = True + text = text[:-1] + elif ( + element.tag_name == "textarea" + and "\n" in text + and "\r" not in text + ): + text = text.replace("\n", "\r") + for key in text: + try: + element.send_keys(key) + except AttributeError: + element = self.select(selector, timeout=0.1) + element.send_keys(key) + time.sleep(float(0.0422 + (random.random() / 112.0))) + if submit: + element.send_keys("\r\n") + time.sleep(0.044) + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.sleep(0.025)) + + def type(self, selector, text, timeout=None): + """Similar to send_keys(), but clears the text field first.""" + if not timeout: + timeout = settings.SMALL_TIMEOUT + self.__slow_mode_pause_if_set() + element = self.select(selector, timeout=timeout) + element.scroll_into_view() + with suppress(Exception): + element.clear_input() + if text.endswith("\n") or text.endswith("\r"): + text = text[:-1] + "\r\n" + elif ( + element.tag_name == "textarea" + and "\n" in text + and "\r" not in text + ): + text = text.replace("\n", "\r") + element.send_keys(text) + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.sleep(0.025)) + + def clear_input(self, selector, timeout=None): + if not timeout: + timeout = settings.SMALL_TIMEOUT + self.__slow_mode_pause_if_set() + element = self.select(selector, timeout=timeout) + element.scroll_into_view() + element.clear_input() + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.sleep(0.025)) + + def clear(self, selector, timeout=None): + """Same as clear_input(), which clears the text field.""" + self.clear_input(selector=selector, timeout=timeout) + + def set_value(self, selector, text, timeout=None): + """Similar to send_keys(), but clears the text field first.""" + if not timeout: + timeout = settings.SMALL_TIMEOUT + self.__slow_mode_pause_if_set() + selector = self.__convert_to_css_if_xpath(selector) + element = self.select(selector, timeout=timeout) + element.scroll_into_view() + press_enter = False + if text.endswith("\n"): + text = text[:-1] + press_enter = True + value = js_utils.escape_quotes_if_needed(re.escape(text)) + css_selector = re.escape(selector) + css_selector = js_utils.escape_quotes_if_needed(css_selector) + set_value_script = ( + """m_elm = document.querySelector('%s');""" + """m_elm.value = '%s';""" % (css_selector, value) + ) + self.loop.run_until_complete(self.page.evaluate(set_value_script)) + the_type = self.get_element_attribute(selector, "type") + if the_type == "range": + # Some input sliders need a mouse event to trigger listeners. + with suppress(Exception): + mouse_move_script = ( + """m_elm = document.querySelector('%s');""" + """m_evt = new Event('mousemove');""" + """m_elm.dispatchEvent(m_evt);""" % css_selector + ) + self.loop.run_until_complete( + self.page.evaluate(mouse_move_script) + ) + elif press_enter: + self.__add_light_pause() + self.send_keys(selector, "\n") + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.sleep(0.025)) + + def submit(self, selector): + submit_script = ( + """elm = document.querySelector('%s'); + const event = new KeyboardEvent("keydown", { + key: "Enter", + keyCode: 13, + code: "Enter", + which: 13, + bubbles: true + }); + elm.dispatchEvent(event);""" % selector + ) + self.loop.run_until_complete(self.page.evaluate(submit_script)) + + def evaluate(self, expression): + """Run a JavaScript expression and return the result.""" + expression = expression.strip() + exp_list = expression.split("\n") + if exp_list and exp_list[-1].strip().startswith("return "): + expression = ( + "\n".join(exp_list[0:-1]) + "\n" + + exp_list[-1].strip()[len("return "):] + ).strip() + return self.loop.run_until_complete(self.page.evaluate(expression)) + + def execute_script(self, expression): + return self.evaluate(expression) + + def js_dumps(self, obj_name): + """Similar to evaluate(), but for dictionary results.""" + if obj_name.startswith("return "): + obj_name = obj_name[len("return "):] + return self.loop.run_until_complete(self.page.js_dumps(obj_name)) + + def maximize(self): + try: + if self.get_window()[1].window_state.value == "maximized": + return + elif self.get_window()[1].window_state.value == "minimized": + self.loop.run_until_complete(self.page.maximize()) + time.sleep(0.044) + return self.loop.run_until_complete(self.page.maximize()) + except Exception: + with suppress(Exception): + width = self.evaluate("screen.availWidth;") + height = self.evaluate("screen.availHeight;") + self.__set_window_rect(0, 0, width, height) + return + + def minimize(self): + if self.get_window()[1].window_state.value != "minimized": + return self.loop.run_until_complete(self.page.minimize()) + + def medimize(self): + if self.get_window()[1].window_state.value == "minimized": + self.loop.run_until_complete(self.page.medimize()) + time.sleep(0.044) + return self.loop.run_until_complete(self.page.medimize()) + + def __set_window_rect(self, x, y, width, height, uc_lock=False): + if uc_lock: + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: + self.__make_sure_pyautogui_lock_is_writable() + if self.get_window()[1].window_state.value == "minimized": + self.loop.run_until_complete( + self.page.set_window_size( + left=x, top=y, width=width, height=height) + ) + time.sleep(0.044) + return self.loop.run_until_complete( + self.page.set_window_size( + left=x, top=y, width=width, height=height) + ) + else: + if self.get_window()[1].window_state.value == "minimized": + self.loop.run_until_complete( + self.page.set_window_size( + left=x, top=y, width=width, height=height) + ) + time.sleep(0.044) + return self.loop.run_until_complete( + self.page.set_window_size( + left=x, top=y, width=width, height=height) + ) + + def set_window_rect(self, x, y, width, height): + return self.__set_window_rect(x, y, width, height, uc_lock=True) + + def reset_window_size(self): + x = settings.WINDOW_START_X + y = settings.WINDOW_START_Y + width = settings.CHROME_START_WIDTH + height = settings.CHROME_START_HEIGHT + self.__set_window_rect(x, y, width, height, uc_lock=True) + self.__add_light_pause() + + def open_new_window(self, url=None, switch_to=True): + return self.open_new_tab(url=url, switch_to=switch_to) + + def switch_to_window(self, window): + self.switch_to_tab(window) + + def switch_to_newest_window(self): + self.switch_to_tab(-1) + + def open_new_tab(self, url=None, switch_to=True, **kwargs): + driver = self.driver + if not isinstance(url, str): + url = "about:blank" + if hasattr(driver, "cdp_base"): + try: + self.loop.run_until_complete( + self.page.get(url, new_tab=True, **kwargs) + ) + except Exception: + original_targets = self.loop.run_until_complete( + self.page.send(mycdp.target.get_targets()) + ) + tab_url = driver.cdp_base.tabs[0].websocket_url + if not self.driver.is_connected(): + self.driver.connect() + self.driver.open_new_tab() + targets = self.loop.run_until_complete( + self.page.send(mycdp.target.get_targets()) + ) + new_targets = [] + for target in targets: + if target not in original_targets: + new_targets.append(target) + if new_targets: + found_target = new_targets[0] + t_str = str(new_targets[0]) + target_id = ( + t_str.split("target_id=TargetID('")[-1].split("')")[0] + ) + pre_tab_url = tab_url.split("/page/")[0] + "/page/" + new_tab_url = pre_tab_url + target_id + new_tab = cdp_tab.Tab( + new_tab_url, found_target, driver.cdp_base + ) + driver.cdp_base.targets.append(new_tab) + driver.cdp_base.tabs.append(new_tab) + self.driver.disconnect() + self.switch_to_newest_tab() + self.goto(url) + return + elif getattr(sb_config, "guest_mode", None): + print(" open_new_tab() failed! (Known Guest Mode issue)") + if switch_to: + self.switch_to_newest_tab() + return + + target_id = self.loop.run_until_complete( + self.page.send(mycdp.target.create_target(url)) + ) + if not target_id and getattr(sb_config, "guest_mode", None): + print(" open_new_tab() failed! (Known Guest Mode issue)") + found_target = None + targets = self.loop.run_until_complete( + self.page.send(mycdp.target.get_targets()) + ) + if target_id: + for target in targets: + if str(target_id) in str(target): + found_target = target + break + if found_target: + tab_url = driver.tabs[0].websocket_url + pre_tab_url = tab_url.split("/page/")[0] + "/page/" + new_tab_url = pre_tab_url + target_id + new_tab = cdp_tab.Tab(new_tab_url, found_target, driver) + driver.targets.append(new_tab) + driver.tabs.append(new_tab) + if switch_to: + self.switch_to_newest_tab() + + def switch_to_tab(self, tab): + driver = self.driver + if hasattr(driver, "cdp_base"): + driver = driver.cdp_base + if isinstance(tab, int): + self.page = driver.tabs[tab] + elif isinstance(tab, cdp_util.Tab): + self.page = tab + else: + raise Exception("`tab` must be an int or a Tab type!") + self.bring_active_window_to_front() + + def switch_to_newest_tab(self): + self.switch_to_tab(-1) + + def close_active_tab(self): + """Close the active tab. + The active tab is the one currenly controlled by CDP. + The active tab MIGHT NOT be the currently visible tab! + (If a page opens a new tab, the new tab WON'T be active) + To switch the active tab, call: sb.switch_to_tab(tab)""" + return self.loop.run_until_complete(self.page.close()) + + def get_active_tab(self): + """Return the active tab. + The active tab is the one currenly controlled by CDP. + The active tab MIGHT NOT be the currently visible tab! + (If a page opens a new tab, the new tab WON'T be active) + To switch the active tab, call: sb.switch_to_tab(tab)""" + return self.page + + def get_tabs(self): + driver = self.driver + if hasattr(driver, "cdp_base"): + driver = driver.cdp_base + return driver.tabs + + def get_window(self): + return self.loop.run_until_complete(self.page.get_window()) + + def get_text(self, selector="body", timeout=None): + if not timeout: + timeout = settings.SMALL_TIMEOUT + return self.find_element(selector, timeout=timeout).text_all + + def get_title(self): + """Returns the title of the current web page.""" + return self.loop.run_until_complete( + self.page.evaluate("document.title") + ) + + def get_page_title(self): + """Same as get_title(), which returns the current page title.""" + return self.get_title() + + def get_current_url(self): + return self.loop.run_until_complete( + self.page.evaluate("window.location.href") + ) + + def get_origin(self): + return self.loop.run_until_complete( + self.page.evaluate("window.location.origin") + ) + + def get_html(self, include_shadow_dom=True): + return self.get_page_source( + include_shadow_dom=include_shadow_dom + ) + + def get_page_source(self, include_shadow_dom=True): + if include_shadow_dom: + return self.find_element("html").get_html() + try: + source = self.loop.run_until_complete( + self.page.evaluate("document.documentElement.outerHTML") + ) + except Exception: + time.sleep(constants.UC.CDP_MODE_OPEN_WAIT) + source = self.loop.run_until_complete( + self.page.evaluate("document.documentElement.outerHTML") + ) + return source + + def get_beautiful_soup(self, source=None): + """BeautifulSoup is a toolkit for dissecting an HTML document + and extracting what you need. It's great for screen-scraping! + See: https://www.crummy.com/software/BeautifulSoup/bs4/doc/ """ + from bs4 import BeautifulSoup + + if not source: + with suppress(Exception): + self.wait_for_element_visible( + "body", timeout=settings.MINI_TIMEOUT + ) + source = self.get_page_source() + return BeautifulSoup(source, "html.parser") + + def get_user_agent(self): + return self.loop.run_until_complete( + self.page.evaluate("navigator.userAgent") + ) + + def get_cookie_string(self): + return self.loop.run_until_complete( + self.page.evaluate("document.cookie") + ) + + def get_locale_code(self): + return self.loop.run_until_complete( + self.page.evaluate("navigator.language || navigator.languages[0]") + ) + + def get_local_storage_item(self, key): + js_code = """localStorage.getItem('%s');""" % key + with suppress(Exception): + return self.loop.run_until_complete(self.page.evaluate(js_code)) + + def get_session_storage_item(self, key): + js_code = """sessionStorage.getItem('%s');""" % key + with suppress(Exception): + return self.loop.run_until_complete(self.page.evaluate(js_code)) + + def get_screen_rect(self): + coordinates = self.loop.run_until_complete( + self.page.js_dumps("window.screen") + ) + return coordinates + + def get_window_rect(self): + coordinates = {} + innerWidth = self.loop.run_until_complete( + self.page.evaluate("window.innerWidth") + ) + innerHeight = self.loop.run_until_complete( + self.page.evaluate("window.innerHeight") + ) + outerWidth = self.loop.run_until_complete( + self.page.evaluate("window.outerWidth") + ) + outerHeight = self.loop.run_until_complete( + self.page.evaluate("window.outerHeight") + ) + pageXOffset = self.loop.run_until_complete( + self.page.evaluate("window.pageXOffset") + ) + pageYOffset = self.loop.run_until_complete( + self.page.evaluate("window.pageYOffset") + ) + scrollX = self.loop.run_until_complete( + self.page.evaluate("window.scrollX") + ) + scrollY = self.loop.run_until_complete( + self.page.evaluate("window.scrollY") + ) + screenLeft = self.loop.run_until_complete( + self.page.evaluate("window.screenLeft") + ) + screenTop = self.loop.run_until_complete( + self.page.evaluate("window.screenTop") + ) + x = self.loop.run_until_complete( + self.page.evaluate("window.screenX") + ) + y = self.loop.run_until_complete( + self.page.evaluate("window.screenY") + ) + coordinates["innerWidth"] = innerWidth + coordinates["innerHeight"] = innerHeight + coordinates["outerWidth"] = outerWidth + coordinates["outerHeight"] = outerHeight + coordinates["width"] = outerWidth + coordinates["height"] = outerHeight + coordinates["pageXOffset"] = pageXOffset if pageXOffset else 0 + coordinates["pageYOffset"] = pageYOffset if pageYOffset else 0 + coordinates["scrollX"] = scrollX if scrollX else 0 + coordinates["scrollY"] = scrollY if scrollY else 0 + coordinates["screenLeft"] = screenLeft if screenLeft else 0 + coordinates["screenTop"] = screenTop if screenTop else 0 + coordinates["x"] = x if x else 0 + coordinates["y"] = y if y else 0 + return coordinates + + def get_window_size(self): + coordinates = {} + outerWidth = self.loop.run_until_complete( + self.page.evaluate("window.outerWidth") + ) + outerHeight = self.loop.run_until_complete( + self.page.evaluate("window.outerHeight") + ) + coordinates["width"] = outerWidth + coordinates["height"] = outerHeight + return coordinates + + def get_window_position(self): + coordinates = {} + x = self.loop.run_until_complete( + self.page.evaluate("window.screenX") + ) + y = self.loop.run_until_complete( + self.page.evaluate("window.screenY") + ) + coordinates["x"] = x if x else 0 + coordinates["y"] = y if y else 0 + return coordinates + + def get_element_rect(self, selector, timeout=None): + if not timeout: + timeout = settings.SMALL_TIMEOUT + selector = self.__convert_to_css_if_xpath(selector) + element = self.select(selector, timeout=timeout) + self.__add_light_pause() + coordinates = None + if ":contains(" in selector: + position = element.get_position() + x = position.x + y = position.y + width = position.width + height = position.height + coordinates = {"x": x, "y": y, "width": width, "height": height} + else: + coordinates = self.loop.run_until_complete( + self.page.js_dumps( + """document.querySelector('%s').getBoundingClientRect()""" + % js_utils.escape_quotes_if_needed(re.escape(selector)) + ) + ) + return coordinates + + def get_element_size(self, selector, timeout=None): + if not timeout: + timeout = settings.SMALL_TIMEOUT + element_rect = self.get_element_rect(selector, timeout=timeout) + coordinates = {} + coordinates["width"] = element_rect["width"] + coordinates["height"] = element_rect["height"] + return coordinates + + def get_element_position(self, selector, timeout=None): + if not timeout: + timeout = settings.SMALL_TIMEOUT + element_rect = self.get_element_rect(selector, timeout=timeout) + coordinates = {} + coordinates["x"] = element_rect["x"] + coordinates["y"] = element_rect["y"] + return coordinates + + def get_gui_element_rect(self, selector, timeout=None): + """(Coordinates are relative to the screen. Not the window.)""" + if not timeout: + timeout = settings.SMALL_TIMEOUT + element_rect = self.get_element_rect(selector, timeout=timeout) + e_width = element_rect["width"] + e_height = element_rect["height"] + window_rect = self.get_window_rect() + w_bottom_y = window_rect["y"] + window_rect["height"] + viewport_height = window_rect["innerHeight"] + x = window_rect["x"] + element_rect["x"] + y = w_bottom_y - viewport_height + element_rect["y"] + y_scroll_offset = window_rect["pageYOffset"] + if ( + hasattr(sb_config, "_cdp_browser") + and sb_config._cdp_browser == "opera" + ): + # Handle special case where Opera side panel shifts coordinates + x_offset = window_rect["outerWidth"] - window_rect["innerWidth"] + if x_offset > 56: + x_offset = 56 + elif x_offset < 22: + x_offset = 0 + x = x + x_offset + y = y - y_scroll_offset + x = x + window_rect["scrollX"] + y = y + window_rect["scrollY"] + return ({"height": e_height, "width": e_width, "x": x, "y": y}) + + def get_gui_element_center(self, selector, timeout=None): + """(Coordinates are relative to the screen. Not the window.)""" + if not timeout: + timeout = settings.SMALL_TIMEOUT + element_rect = self.get_gui_element_rect(selector, timeout=timeout) + e_width = element_rect["width"] + e_height = element_rect["height"] + e_x = element_rect["x"] + e_y = element_rect["y"] + return ((e_x + e_width / 2.0) + 0.5, (e_y + e_height / 2.0) + 0.5) + + def get_document(self): + return self.loop.run_until_complete(self.page.get_document()) + + def get_flattened_document(self): + return self.loop.run_until_complete(self.page.get_flattened_document()) + + def get_element_attributes(self, selector): + selector = self.__convert_to_css_if_xpath(selector) + return self.loop.run_until_complete( + self.page.js_dumps( + """document.querySelector('%s')""" + % js_utils.escape_quotes_if_needed(re.escape(selector)) + ) + ) + + def get_element_attribute(self, selector, attribute): + """Find an element and return the value of an attribute. + Raises an exception if there's no such element or attribute.""" + attributes = self.get_element_attributes(selector) + with suppress(Exception): + return attributes[attribute] + locate = ' %s="' % attribute + value = self.get_attribute(selector, attribute) + if not value and locate not in attributes: + raise KeyError(attribute) + return value + + def get_attribute(self, selector, attribute): + """Find an element and return the value of an attribute. + If the element doesn't exist: Raises an exception. + If the attribute doesn't exist: Returns None.""" + return self.find_element(selector).get_attribute(attribute) + + def get_element_html(self, selector, timeout=None): + """Find an element and return the outerHTML.""" + if not timeout: + timeout = settings.SMALL_TIMEOUT + selector = self.__convert_to_css_if_xpath(selector) + self.find_element(selector, timeout=timeout) + self.__add_light_pause() + return self.loop.run_until_complete( + self.page.evaluate( + """document.querySelector('%s').outerHTML""" + % js_utils.escape_quotes_if_needed(re.escape(selector)) + ) + ) + + def get_mfa_code(self, totp_key=None): + """Returns a time-based one-time password based on the Google + Authenticator algorithm for multi-factor authentication.""" + return shared_utils.get_mfa_code(totp_key) + + def enter_mfa_code(self, selector, totp_key=None, timeout=None): + if not timeout: + timeout = settings.SMALL_TIMEOUT + mfa_code = self.get_mfa_code(totp_key) + self.type(selector, mfa_code + "\n", timeout=timeout) + + def activate_messenger(self): + js_utils.activate_messenger(self) + self.__add_light_pause() + + def set_messenger_theme( + self, theme="default", location="default", max_messages="default" + ): + """Sets a theme for posting messages. + Themes: ["flat", "future", "block", "air", "ice"] + Locations: ["top_left", "top_center", "top_right", + "bottom_left", "bottom_center", "bottom_right"] + max_messages: The limit of concurrent messages to display.""" + if not theme: + theme = "default" # "flat" + if not location: + location = "default" # "bottom_right" + if not max_messages: + max_messages = "default" # "8" + else: + max_messages = str(max_messages) # Value must be in string format + js_utils.set_messenger_theme( + self, + theme=theme, + location=location, + max_messages=max_messages, + ) + self.__add_light_pause() + + def post_message(self, message, duration=None, pause=True, style="info"): + """Post a message on the screen with Messenger. + Arguments: + message: The message to display. + duration: The time until the message vanishes. (Default: 2.55s) + pause: If True, the program waits until the message completes. + style: "info", "success", or "error".""" + driver = self.driver + if hasattr(driver, "cdp_base"): + driver = driver.cdp_base + if style not in ["info", "success", "error"]: + style = "info" + if not duration: + duration = settings.DEFAULT_MESSAGE_DURATION + if ( + ( + driver.config.headless + or (hasattr(sb_config, "xvfb") and sb_config.xvfb) + ) + and float(duration) > 0.75 + ): + duration = 0.75 + try: + js_utils.post_message(self, message, duration, style=style) + except Exception: + print(" * %s message: %s" % (style.upper(), message)) + if pause: + duration = float(duration) + 0.15 + time.sleep(float(duration)) + + def download_file(self, file_url): + """Download a file from a URL. + The default download location is: "./downloaded_files/".""" + self.loop.run_until_complete( + self.page.download_file(file_url) + ) + + def save_file_as(self, file_url, new_file_name): + """Download a file from a URL and rename it. + The default download location is: "./downloaded_files/".""" + self.loop.run_until_complete( + self.page.download_file(file_url, new_file_name) + ) + + def assert_downloaded_file(self, file, timeout=None): + """Asserts that the file exists in SeleniumBase's [Downloads Folder]. + For browser click-initiated downloads, SeleniumBase will override + the system [Downloads Folder] to be "./downloaded_files/". + @Params + file - The filename of the downloaded file. + timeout - The time (seconds) to wait for the download to complete. + browser - If True, uses the path set by click-initiated downloads.""" + downloads_folder = constants.Files.DOWNLOADS_FOLDER + abs_path = os.path.abspath(".") + downloads_path = os.path.join(abs_path, downloads_folder) + if not timeout: + timeout = settings.LARGE_TIMEOUT + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (timeout * 1000.0) + downloaded_file_path = os.path.join(downloads_path, file) + found = False + for x in range(int(timeout)): + shared_utils.check_if_time_limit_exceeded() + try: + self.assert_true( + os.path.exists(downloaded_file_path), + "File [%s] was not found in the downloads folder [%s]!" + % (file, downloads_path), + ) + found = True + break + except Exception: + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + time.sleep(1) + if not found and not os.path.exists(downloaded_file_path): + plural = "s" + if timeout == 1: + plural = "" + message = ( + "File {%s} was not found in the downloads folder {%s} " + "after %s second%s! (Or the download didn't complete!)" + % (file, downloads_path, timeout, plural) + ) + from seleniumbase.common.exceptions import NoSuchFileException + raise NoSuchFileException(message) + + def get_path_of_downloaded_file(self, file): + """This assumes the default location of SeleniumBase downloads, + which is the "./downloaded_files/" folder where scripts run.""" + downloads_folder = constants.Files.DOWNLOADS_FOLDER + abs_path = os.path.abspath(".") + downloads_path = os.path.join(abs_path, downloads_folder) + return os.path.join(downloads_path, file) + + def set_download_path(self, path): + """Set a new download path for click-initiated downloads. + (For Pure CDP Mode sync format only! -> sb_cdp.) + The default download location is: "./downloaded_files/". + Convenience methods such as assert_downloaded_file(file) + will still expect the default location.""" + self.loop.run_until_complete(self.page.set_download_path(path)) + + def set_locale(self, locale): + """(Settings will take effect on the next page load)""" + self.loop.run_until_complete(self.page.set_locale(locale)) + + def set_local_storage_item(self, key, value): + js_code = """localStorage.setItem('%s','%s');""" % (key, value) + with suppress(Exception): + self.loop.run_until_complete(self.page.evaluate(js_code)) + + def set_session_storage_item(self, key, value): + js_code = """sessionStorage.setItem('%s','%s');""" % (key, value) + with suppress(Exception): + self.loop.run_until_complete(self.page.evaluate(js_code)) + + def set_attributes(self, selector, attribute, value): + """This method uses JavaScript to set/update a common attribute. + All matching selectors from querySelectorAll() are used. + Example => (Make all links on a website redirect to Google): + self.set_attributes("a", "href", "https://google.com")""" + attribute = re.escape(attribute) + attribute = js_utils.escape_quotes_if_needed(attribute) + value = re.escape(value) + value = js_utils.escape_quotes_if_needed(value) + css_selector = self.__convert_to_css_if_xpath(selector) + css_selector = re.escape(css_selector) # Add "\\" to special chars + css_selector = js_utils.escape_quotes_if_needed(css_selector) + js_code = ( + """var $elements = document.querySelectorAll('%s'); + var index = 0, length = $elements.length; + for(; index < length; index++){ + $elements[index].setAttribute('%s','%s');}""" % ( + css_selector, + attribute, + value, + ) + ) + with suppress(Exception): + self.loop.run_until_complete(self.page.evaluate(js_code)) + + def is_attribute_present(self, selector, attribute, value=None): + try: + element = self.find_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 is_online(self): + js_code = "navigator.onLine;" + return self.loop.run_until_complete(self.page.evaluate(js_code)) + + def __make_sure_pyautogui_lock_is_writable(self): + with suppress(Exception): + shared_utils.make_writable(constants.MultiBrowser.PYAUTOGUILOCK) + + def __verify_pyautogui_has_a_headed_browser(self): + """PyAutoGUI requires a headed browser so that it can + focus on the correct element when performing actions.""" + driver = self.driver + if hasattr(driver, "cdp_base"): + driver = driver.cdp_base + if driver.config.headless: + raise Exception( + "PyAutoGUI can't be used in headless mode!" + ) + + def __install_pyautogui_if_missing(self): + self.__verify_pyautogui_has_a_headed_browser() + driver = self.driver + if hasattr(driver, "cdp_base"): + driver = driver.cdp_base + pip_find_lock = fasteners.InterProcessLock( + constants.PipInstall.FINDLOCK + ) + with pip_find_lock: # Prevent issues with multiple processes + with suppress(Exception): + shared_utils.make_writable(constants.PipInstall.FINDLOCK) + try: + import pyautogui + with suppress(Exception): + use_pyautogui_ver = constants.PyAutoGUI.VER + u_pv = shared_utils.make_version_tuple(use_pyautogui_ver) + pv = shared_utils.make_version_tuple(pyautogui.__version__) + if pv < u_pv: + del pyautogui # To get newer ver + shared_utils.pip_install("pyautogui", version="Latest") + import pyautogui + except Exception: + print("\nPyAutoGUI required! Installing now...") + shared_utils.pip_install("pyautogui", version="Latest") + try: + import pyautogui + except Exception: + if ( + shared_utils.is_linux() + and ( + not sb_config.headed + or (hasattr(sb_config, "xvfb") and sb_config.xvfb) + ) + and not driver.config.headless + and ( + not hasattr(sb_config, "_virtual_display") + or not sb_config._virtual_display + ) + ): + from sbvirtualdisplay import Display + xvfb_width = 1366 + xvfb_height = 768 + if ( + hasattr(sb_config, "_xvfb_width") + and sb_config._xvfb_width + and isinstance(sb_config._xvfb_width, int) + and hasattr(sb_config, "_xvfb_height") + and sb_config._xvfb_height + and isinstance(sb_config._xvfb_height, int) + ): + xvfb_width = sb_config._xvfb_width + xvfb_height = sb_config._xvfb_height + if xvfb_width < 1024: + xvfb_width = 1024 + sb_config._xvfb_width = xvfb_width + if xvfb_height < 768: + xvfb_height = 768 + sb_config._xvfb_height = xvfb_height + with suppress(Exception): + xvfb_display = Display( + visible=True, + size=(xvfb_width, xvfb_height), + backend="xvfb", + use_xauth=True, + ) + if "--debug-display" in sys.argv: + print( + "Starting VDisplay from sb_cdp: (%s, %s)" + % (xvfb_width, xvfb_height) + ) + xvfb_display.start() + + def __get_configured_pyautogui(self, pyautogui_copy): + if ( + shared_utils.is_linux() + and hasattr(pyautogui_copy, "_pyautogui_x11") + and "DISPLAY" in os.environ.keys() + ): + if ( + hasattr(sb_config, "_pyautogui_x11_display") + and sb_config._pyautogui_x11_display + and hasattr(pyautogui_copy._pyautogui_x11, "_display") + and ( + sb_config._pyautogui_x11_display + == pyautogui_copy._pyautogui_x11._display + ) + ): + pass + else: + import Xlib.display + pyautogui_copy._pyautogui_x11._display = ( + Xlib.display.Display(os.environ['DISPLAY']) + ) + sb_config._pyautogui_x11_display = ( + pyautogui_copy._pyautogui_x11._display + ) + return pyautogui_copy + + def gui_press_key(self, key): + self.__install_pyautogui_if_missing() + import pyautogui + pyautogui = self.__get_configured_pyautogui(pyautogui) + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: + self.__make_sure_pyautogui_lock_is_writable() + pyautogui.press(key) + time.sleep(0.044) + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.sleep(0.025)) + + def gui_press_keys(self, keys): + self.__install_pyautogui_if_missing() + import pyautogui + pyautogui = self.__get_configured_pyautogui(pyautogui) + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: + self.__make_sure_pyautogui_lock_is_writable() + for key in keys: + pyautogui.press(key) + time.sleep(float(0.0422 + (random.random() / 112.0))) + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.sleep(0.025)) + + def gui_write(self, text): + self.__install_pyautogui_if_missing() + import pyautogui + pyautogui = self.__get_configured_pyautogui(pyautogui) + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: + self.__make_sure_pyautogui_lock_is_writable() + pyautogui.write(text) + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.sleep(0.025)) + + def __gui_click_x_y(self, x, y, timeframe=0.27, uc_lock=False): + self.__install_pyautogui_if_missing() + import pyautogui + pyautogui = self.__get_configured_pyautogui(pyautogui) + screen_width, screen_height = pyautogui.size() + if x < 0 or y < 0 or x > screen_width or y > screen_height: + raise Exception( + "PyAutoGUI cannot click on point (%s, %s)" + " outside screen. (Width: %s, Height: %s)" + % (x, y, screen_width, screen_height) + ) + if uc_lock: + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: # Prevent issues with multiple processes + self.__make_sure_pyautogui_lock_is_writable() + pyautogui.moveTo(x, y, timeframe, pyautogui.easeOutQuad) + if timeframe >= 0.25: + time.sleep(0.056) # Wait if moving at human-speed + if "--debug" in sys.argv: + print(" <DEBUG> pyautogui.click(%s, %s)" % (x, y)) + pyautogui.click(x=x, y=y) + else: + # Called from a method where the gui_lock is already active + pyautogui.moveTo(x, y, timeframe, pyautogui.easeOutQuad) + if timeframe >= 0.25: + time.sleep(0.056) # Wait if moving at human-speed + if "--debug" in sys.argv: + print(" <DEBUG> pyautogui.click(%s, %s)" % (x, y)) + pyautogui.click(x=x, y=y) + + def gui_click_x_y(self, x, y, timeframe=0.27): + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: # Prevent issues with multiple processes + self.__make_sure_pyautogui_lock_is_writable() + self.__install_pyautogui_if_missing() + import pyautogui + pyautogui = self.__get_configured_pyautogui(pyautogui) + width_ratio = 1.0 + if shared_utils.is_windows(): + window_rect = self.get_window_rect() + width = window_rect["width"] + height = window_rect["height"] + win_x = window_rect["x"] + win_y = window_rect["y"] + scr_width = pyautogui.size().width + win_width = self.evaluate("screen.availWidth;") + width_ratio = round(float(scr_width) / float(win_width), 2) + width_ratio += 0.01 + if width_ratio < 0.45 or width_ratio > 2.55: + width_ratio = 1.01 + sb_config._saved_width_ratio = width_ratio + with suppress(Exception): + self.get_window() # If this fails, skip the rest + self.minimize() + self.__add_light_pause() + self.__set_window_rect(win_x, win_y, width, height) + self.__add_light_pause() + x = x * (width_ratio + 0.03) + y = y * (width_ratio - 0.03) + self.bring_active_window_to_front() + self.__gui_click_x_y(x, y, timeframe=timeframe, uc_lock=False) + + def gui_click_element(self, selector, timeframe=0.27): + self.__slow_mode_pause_if_set() + x, y = self.get_gui_element_center(selector) + self.__add_light_pause() + self.gui_click_x_y(x, y, timeframe=timeframe) + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.wait(0.2)) + + def gui_click_with_offset( + self, selector, x, y, timeframe=0.27, center=False + ): + """Click an element at an {X,Y}-offset location. + {0,0} is the top-left corner of the element. + If center==True, {0,0} becomes the center of the element. + The timeframe is the time spent moving the mouse.""" + if center: + px, py = self.get_gui_element_center(selector) + self.gui_click_x_y(px + x, py + y, timeframe=timeframe) + else: + element_rect = self.get_gui_element_rect(selector) + px = element_rect["x"] + py = element_rect["y"] + self.gui_click_x_y(px + x, py + y, timeframe=timeframe) + + def click_with_offset(self, selector, x, y, center=False, scroll=True): + element = self.find_element(selector) + if scroll: + element.scroll_into_view() + if "--debug" in sys.argv: + displayed_selector = "`%s`" % selector + if '"' not in selector: + displayed_selector = '"%s"' % selector + elif "'" not in selector: + displayed_selector = "'%s'" % selector + print( + " <DEBUG> sb.click_with_offset(%s, %s, %s, center=%s)" + % (displayed_selector, x, y, center) + ) + element.click_with_offset(x=x, y=y, center=center) + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.wait(0.2)) + + def stop(self): + """Same as quit()""" + self.quit() + + def quit(self): + """Quit the browser in the Pure CDP Mode Sync format.""" + driver = self.driver + if hasattr(driver, "cdp_base"): + driver = driver.cdp_base + if ( + self.driver.config.uses_custom_data_dir + and shared_utils.is_windows() + ): + # Let Windows save the session to a custom use_data_dir + self.loop.run_until_complete( + driver.connection.send(mycdp.browser.close()) + ) + time.sleep(0.3) + driver.quit() + + def _on_a_cf_turnstile_page(self, source=None): + if not source or len(source) < 400: + time.sleep(0.2) + source = self.get_page_source() + if ( + ( + 'data-callback="onCaptchaSuccess"' in source + and 'title="reCAPTCHA"' not in source + and 'id="recaptcha-token"' not in source + ) + or "/challenge-platform/h/b/" in source + or 'id="challenge-widget-' in source + or "challenges.cloudf" in source + or "cf-turnstile-" in source + ): + return True + return False + + def _on_an_incapsula_hcaptcha_page(self, *args, **kwargs): + self.loop.run_until_complete(self.page.wait(0.1)) + if ( + self.is_element_visible('iframe[src*="_Incapsula_Resource?"]') + or self.is_element_visible("iframe[data-hcaptcha-widget-id]") + ): + return True + return False + + def _on_a_datadome_slider_page(self, *args, **kwargs): + self.loop.run_until_complete(self.page.wait(0.05)) + if ( + self.is_element_visible( + 'body > iframe[src*="/geo.captcha-delivery.com/captcha/"], ' + 'body > iframe[src*="/geo.captcha-delivery.com/interstitial/"]' + ) + ): + return True + return False + + def _on_a_friendly_captcha_page(self, *args, **kwargs): + self.loop.run_until_complete(self.page.wait(0.05)) + if self.is_element_visible('iframe[data--frc-frame-id]'): + return True + return False + + def _on_a_g_recaptcha_page(self, *args, **kwargs): + time.sleep(0.4) # reCAPTCHA may need a moment to appear + self.loop.run_until_complete(self.page.wait(0.1)) + source = self.get_page_source() + if ( + ( + 'id="recaptcha-token"' in source + or 'title="reCAPTCHA"' in source + ) + and self.is_element_visible('iframe[title="reCAPTCHA"]') + ): + try: + self.loop.run_until_complete(self.page.wait(0.1)) + except Exception: + time.sleep(0.1) + return True + elif "com/recaptcha/api.js" in source: + time.sleep(1.2) # Maybe still loading + try: + self.loop.run_until_complete(self.page.wait(0.1)) + except Exception: + time.sleep(0.1) + return True + return False + + def __gui_click_recaptcha(self, use_cdp=False): + selector = None + if self.is_element_visible('iframe[title="reCAPTCHA"]'): + selector = 'iframe[title="reCAPTCHA"]' + else: + return False + time.sleep(0.25) + self.loop.run_until_complete(self.page.wait(0.1)) + time.sleep(0.25) + with suppress(Exception): + element_rect = self.get_element_rect(selector, timeout=0.1) + e_x = element_rect["x"] + e_y = element_rect["y"] + window_rect = self.get_window_rect() + win_width = window_rect["innerWidth"] + win_height = window_rect["innerHeight"] + if ( + e_x > 1040 + and e_y > 640 + and abs(win_width - e_x) < 110 + and abs(win_height - e_y) < 110 + ): + # Probably the invisible reCAPTCHA in the bottom right corner + return False + gui_element_rect = self.get_gui_element_rect(selector, timeout=1) + gui_e_x = gui_element_rect["x"] + gui_e_y = gui_element_rect["y"] + x_offset = 26 + y_offset = 35 + if shared_utils.is_windows(): + x_offset = 29 + x = gui_e_x + x_offset + y = gui_e_y + y_offset + sb_config._saved_cf_x_y = (x, y) + time.sleep(0.08) + if use_cdp: + time.sleep(0.03) + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: # Prevent issues with multiple processes + self.bring_active_window_to_front() + time.sleep(0.056) + self.click_with_offset(selector, x_offset, y_offset) + time.sleep(0.056) + else: + self.gui_click_x_y(x, y) + return True + return False + + def __gui_slide_datadome_captcha(self): + iframe = ( + 'body > iframe[src*="/geo.captcha-delivery.com/captcha/"], ' + 'body > iframe[src*="/geo.captcha-delivery.com/interstitial/"]' + ) + if not self.is_element_visible(iframe): + return False + src = self.get_attribute(iframe, "src") + tab = self.get_active_tab() + time.sleep(0.05) + self.open_new_tab(url=src) + time.sleep(0.42) + self.loop.run_until_complete(self.page.wait(0.12)) + time.sleep(0.25) + x1, y1 = self.get_gui_element_center("div.slider") + x2, y2 = self.get_gui_element_center("div.sliderTarget") + self.close_active_tab() + self.switch_to_tab(tab) + if shared_utils.is_windows(): + time.sleep(0.48) + self.loop.run_until_complete(self.page.wait(0.1)) + x2 = x2 + 22.5 # Overshoot drop to maximize compatibility + y1 = y1 + 3 + y2 = y2 + 3 + self.gui_drag_drop_points(x1, y1, x2, y2, timeframe=0.55) + time.sleep(0.25) + self.loop.run_until_complete(self.page.wait(0.2)) + time.sleep(0.15) + return True + + def __cdp_click_incapsula_hcaptcha(self, use_cdp=True): + selector = "iframe[data-hcaptcha-widget-id]" + if self.is_element_visible('iframe[src*="_Incapsula_Resource?"]'): + outer_selector = 'iframe[src*="_Incapsula_Resource?"]' + element = self.get_nested_element(outer_selector, selector) + if not element: + return False + elif self.is_element_visible(selector): + element = self.select(selector, timeout=0.1) + if not element: + return False + else: + return False + time.sleep(0.05) + self.loop.run_until_complete(self.page.wait(0.1)) + time.sleep(0.05) + x_offset = 30 + y_offset = 36 + was_clicked = False + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: # Prevent issues with multiple processes + self.bring_active_window_to_front() + time.sleep(0.056) + if "--debug" in sys.argv: + displayed_selector = "`%s`" % selector + if '"' not in selector: + displayed_selector = '"%s"' % selector + elif "'" not in selector: + displayed_selector = "'%s'" % selector + print( + " <DEBUG> click_with_offset(%s, %s, %s)" + % (displayed_selector, x_offset, y_offset) + ) + with suppress(Exception): + element.click_with_offset(x_offset, y_offset) + was_clicked = True + time.sleep(0.075) + if was_clicked: + # Wait a moment for the click to succeed + time.sleep(0.75) + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.wait(0.1)) + if "--debug" in sys.argv: + print(" <DEBUG> hCaptcha was clicked!") + return True + if "--debug" in sys.argv: + print(" <DEBUG> hCaptcha was NOT clicked!") + return False + + def __gui_click_friendly_captcha(self, use_cdp=False): + selector = 'iframe[data--frc-frame-id]' + if self.is_element_visible('iframe[data--frc-frame-id]'): + element = self.select('iframe[data--frc-frame-id]') + else: + return False + time.sleep(0.05) + self.loop.run_until_complete(self.page.wait(0.1)) + time.sleep(0.05) + with suppress(Exception): + element_rect = self.get_element_rect(selector, timeout=0.1) + e_x = element_rect["x"] + e_y = element_rect["y"] + window_rect = self.get_window_rect() + win_width = window_rect["innerWidth"] + win_height = window_rect["innerHeight"] + if ( + e_x > 1040 + and e_y > 640 + and abs(win_width - e_x) < 110 + and abs(win_height - e_y) < 110 + ): + return False + gui_element_rect = self.get_gui_element_rect(selector, timeout=1) + gui_e_x = gui_element_rect["x"] + gui_e_y = gui_element_rect["y"] + x_offset = 27 + y_offset = 34 + x = gui_e_x + x_offset + y = gui_e_y + y_offset + sb_config._saved_cf_x_y = (x, y) + time.sleep(0.08) + if use_cdp: + time.sleep(0.03) + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: # Prevent issues with multiple processes + self.bring_active_window_to_front() + time.sleep(0.06) + element.mouse_move() + time.sleep(0.08) + self.click_with_offset(selector, x_offset, y_offset) + time.sleep(0.08) + element.mouse_move() + time.sleep(0.25) + return True + else: + self.gui_click_x_y(x, y) + return True + return False + + def solve_captcha(self): + self.__click_captcha(use_cdp=True) + + def click_captcha(self): + """Same as solve_captcha()""" + self.__click_captcha(use_cdp=True) + + def gui_click_captcha(self): + """Use PyAutoGUI to click the CAPTCHA""" + self.__click_captcha(use_cdp=False) + + def __click_captcha(self, use_cdp=False): + """Uses PyAutoGUI unless use_cdp == True""" + time.sleep(0.075) + self.loop.run_until_complete(self.page.wait(0.1)) + time.sleep(0.025) + source = self.get_page_source() + if self._on_a_cf_turnstile_page(source): + pass + elif self._on_a_g_recaptcha_page(source): + result = self.__gui_click_recaptcha(use_cdp) + return result + elif self._on_an_incapsula_hcaptcha_page(): + result = self.__cdp_click_incapsula_hcaptcha() + return result + elif self._on_a_datadome_slider_page(): + result = self.__gui_slide_datadome_captcha() + return result + elif self._on_a_friendly_captcha_page(): + result = self.__gui_click_friendly_captcha(use_cdp) + return result + else: + return False + selector = None + if self.is_element_present('[class="cf-turnstile"]'): + selector = '[class="cf-turnstile"]' + elif self.is_element_present("#challenge-form div > div"): + selector = "#challenge-form div > div" + elif self.is_element_present('[style="display: grid;"] div div'): + selector = '[style="display: grid;"] div div' + time.sleep(0.025) + self.set_attributes(selector, "style", "text-align: left;") + time.sleep(0.025) + elif self.is_element_present("[class*=spacer] + div div"): + selector = '[class*=spacer] + div div' + elif self.is_element_present(".spacer div:not([class])"): + selector = ".spacer div:not([class])" + elif self.is_element_present('[data-testid*="challenge-"] div'): + selector = '[data-testid*="challenge-"] div' + elif self.is_element_present("div#turnstile-widget div:not([class])"): + selector = "div#turnstile-widget div:not([class])" + elif self.is_element_present("ngx-turnstile div:not([class])"): + selector = "ngx-turnstile div:not([class])" + elif self.is_element_present( + 'form div:not([class]):has(input[name*="cf-turn"])' + ): + selector = 'form div:not([class]):has(input[name*="cf-turn"])' + elif self.is_element_present("body > div#check > div:not([class])"): + selector = "body > div#check > div:not([class])" + elif self.is_element_present(".cf-turnstile-wrapper"): + selector = ".cf-turnstile-wrapper" + elif self.is_element_present('[id*="turnstile"] div:not([class])'): + selector = '[id*="turnstile"] div:not([class])' + elif self.is_element_present('[class*="turnstile"] div:not([class])'): + selector = '[class*="turnstile"] div:not([class])' + elif self.is_element_present("iframe[data-hcaptcha-widget-id]"): + selector = "iframe[data-hcaptcha-widget-id]" + elif self.is_element_present('[data-callback="onCaptchaSuccess"]'): + selector = '[data-callback="onCaptchaSuccess"]' + elif self.is_element_present('[class*="captcha"] div:not([class])'): + selector = '[class*="captcha"] div:not([class])' + elif self.is_element_present("form div:not(:has(*))"): + selector = "form div:not(:has(*))" + elif self.is_element_present( + "div:not([class]):not([id]):not([aria-label]) > " + "div:not([class]):not([id]):not([aria-label])" + ): + selector = ( + "div:not([class]):not([id]):not([aria-label]) > " + "div:not([class]):not([id]):not([aria-label])" + ) + else: + return False + if not selector: + return False + if ( + self.is_element_present("form") + and ( + self.is_element_present('form[class*="center"]') + or self.is_element_present('form[class*="right"]') + or self.is_element_present('form div[class*="center"]') + or self.is_element_present('form div[class*="right"]') + ) + ): + script = ( + """var $elements = document.querySelectorAll( + 'form[class], form div[class]'); + var index = 0, length = $elements.length; + for(; index < length; index++){ + the_class = $elements[index].getAttribute('class'); + new_class = the_class.replaceAll('center', 'left'); + new_class = new_class.replaceAll('right', 'left'); + $elements[index].setAttribute('class', new_class);}""" + ) + with suppress(Exception): + self.loop.run_until_complete(self.page.evaluate(script)) + self.loop.run_until_complete(self.page.wait(0.1)) + elif ( + self.is_element_present("form") + and ( + self.is_element_present('form div[style*="center"]') + or self.is_element_present('form div[style*="right"]') + ) + ): + script = ( + """var $elements = document.querySelectorAll( + 'form[style], form div[style]'); + var index = 0, length = $elements.length; + for(; index < length; index++){ + the_style = $elements[index].getAttribute('style'); + new_style = the_style.replaceAll('center', 'left'); + new_style = new_style.replaceAll('right', 'left'); + $elements[index].setAttribute('style', new_style);}""" + ) + with suppress(Exception): + self.loop.run_until_complete(self.page.evaluate(script)) + self.loop.run_until_complete(self.page.wait(0.1)) + elif ( + self.is_element_present( + 'form [id*="turnstile"] div:not([class])' + ) + or self.is_element_present( + 'form [class*="turnstile"] div:not([class])' + ) + ): + script = ( + """var $elements = document.querySelectorAll( + 'form [id*="turnstile"]'); + var index = 0, length = $elements.length; + for(; index < length; index++){ + $elements[index].setAttribute('align', 'left');} + var $elements = document.querySelectorAll( + 'form [class*="turnstile"]'); + var index = 0, length = $elements.length; + for(; index < length; index++){ + $elements[index].setAttribute('align', 'left');}""" + ) + with suppress(Exception): + self.loop.run_until_complete(self.page.evaluate(script)) + self.loop.run_until_complete(self.page.wait(0.1)) + elif ( + self.is_element_present( + '[style*="text-align: center;"] div:not([class])' + ) + ): + script = ( + """var $elements = document.querySelectorAll( + '[style*="text-align: center;"]'); + var index = 0, length = $elements.length; + for(; index < length; index++){ + the_style = $elements[index].getAttribute('style'); + new_style = the_style.replaceAll('center', 'left'); + $elements[index].setAttribute('style', new_style);}""" + ) + with suppress(Exception): + self.loop.run_until_complete(self.page.evaluate(script)) + self.loop.run_until_complete(self.page.wait(0.1)) + with suppress(Exception): + time.sleep(0.05) + element_rect = self.get_gui_element_rect(selector, timeout=1) + e_x = element_rect["x"] + e_y = element_rect["y"] + x_offset = 25 + y_offset = 32 + if shared_utils.is_windows(): + x_offset = 27 + y_offset = 29 + x = e_x + x_offset + y = e_y + y_offset + sb_config._saved_cf_x_y = (x, y) + time.sleep(0.05) + if hasattr(sb_config, "_cdp_proxy") and sb_config._cdp_proxy: + time.sleep(0.22) # CAPTCHA may load slower with proxy + if use_cdp: + time.sleep(0.03) + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: # Prevent issues with multiple processes + self.bring_active_window_to_front() + time.sleep(0.05) + self.click_with_offset(selector, x_offset, y_offset) + time.sleep(0.05) + else: + self.gui_click_x_y(x, y) + return True + return False + + def __gui_drag_drop(self, x1, y1, x2, y2, timeframe=0.27, uc_lock=False): + self.__install_pyautogui_if_missing() + import pyautogui + pyautogui = self.__get_configured_pyautogui(pyautogui) + screen_width, screen_height = pyautogui.size() + if x1 < 0 or y1 < 0 or x1 > screen_width or y1 > screen_height: + raise Exception( + "PyAutoGUI cannot drag-drop from point (%s, %s)" + " outside screen. (Width: %s, Height: %s)" + % (x1, y1, screen_width, screen_height) + ) + if x2 < 0 or y2 < 0 or x2 > screen_width or y2 > screen_height: + raise Exception( + "PyAutoGUI cannot drag-drop to point (%s, %s)" + " outside screen. (Width: %s, Height: %s)" + % (x2, y2, screen_width, screen_height) + ) + if uc_lock: + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: # Prevent issues with multiple processes + if "--debug" in sys.argv: + print(" <DEBUG> pyautogui.moveTo(%s, %s)" % (x1, y1)) + pyautogui.moveTo(x1, y1, 0.25, pyautogui.easeOutQuad) + self.__add_light_pause() + if "--debug" in sys.argv: + print(" <DEBUG> pyautogui.dragTo(%s, %s)" % (x2, y2)) + pyautogui.dragTo(x2, y2, button="left", duration=timeframe) + else: + # Called from a method where the gui_lock is already active + if "--debug" in sys.argv: + print(" <DEBUG> pyautogui.moveTo(%s, %s)" % (x1, y1)) + pyautogui.moveTo(x1, y1, 0.25, pyautogui.easeOutQuad) + self.__add_light_pause() + if "--debug" in sys.argv: + print(" <DEBUG> pyautogui.dragTo(%s, %s)" % (x2, y2)) + pyautogui.dragTo(x2, y2, button="left", duration=timeframe) + + def gui_drag_drop_points(self, x1, y1, x2, y2, timeframe=0.35): + """Use PyAutoGUI to drag-and-drop from one point to another. + Can simulate click-and-hold when using the same point twice.""" + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: # Prevent issues with multiple processes + self.__install_pyautogui_if_missing() + import pyautogui + pyautogui = self.__get_configured_pyautogui(pyautogui) + width_ratio = 1.0 + if shared_utils.is_windows(): + window_rect = self.get_window_rect() + width = window_rect["width"] + height = window_rect["height"] + win_x = window_rect["x"] + win_y = window_rect["y"] + scr_width = pyautogui.size().width + win_width = self.evaluate("screen.availWidth;") + width_ratio = round(float(scr_width) / float(win_width), 2) + width_ratio += 0.01 + if width_ratio < 0.45 or width_ratio > 2.55: + width_ratio = 1.01 + sb_config._saved_width_ratio = width_ratio + with suppress(Exception): + self.get_window() # If this fails, skip the rest + self.minimize() + self.__add_light_pause() + self.__set_window_rect(win_x, win_y, width, height) + self.__add_light_pause() + x1 = x1 * width_ratio + y1 = y1 * (width_ratio - 0.02) + x2 = x2 * width_ratio + y2 = y2 * (width_ratio - 0.02) + self.bring_active_window_to_front() + self.__gui_drag_drop( + x1, y1, x2, y2, timeframe=timeframe, uc_lock=False + ) + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.wait(0.2)) + + def gui_drag_and_drop(self, drag_selector, drop_selector, timeframe=0.35): + """Use PyAutoGUI to drag-and-drop from one selector to another. + Can simulate click-and-hold when using the same selector twice.""" + self.__slow_mode_pause_if_set() + self.bring_active_window_to_front() + x1, y1 = self.get_gui_element_center(drag_selector) + self.__add_light_pause() + x2, y2 = self.get_gui_element_center(drop_selector) + self.__add_light_pause() + self.gui_drag_drop_points(x1, y1, x2, y2, timeframe=timeframe) + + def gui_click_and_hold(self, selector, timeframe=0.35): + """Use PyAutoGUI to click-and-hold a selector.""" + self.__slow_mode_pause_if_set() + self.bring_active_window_to_front() + x, y = self.get_gui_element_center(selector) + self.__add_light_pause() + self.gui_drag_drop_points(x, y, x, y, timeframe=timeframe) + + def gui_move_to_element(self, selector, timeframe=0.27): + """Same as gui_hover_element()""" + return self.gui_hover_element(selector, timeframe=timeframe) + + def __gui_hover_x_y(self, x, y, timeframe=0.27, uc_lock=False): + self.__install_pyautogui_if_missing() + import pyautogui + pyautogui = self.__get_configured_pyautogui(pyautogui) + screen_width, screen_height = pyautogui.size() + if x < 0 or y < 0 or x > screen_width or y > screen_height: + raise Exception( + "PyAutoGUI cannot hover on point (%s, %s)" + " outside screen. (Width: %s, Height: %s)" + % (x, y, screen_width, screen_height) + ) + if uc_lock: + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: # Prevent issues with multiple processes + if "--debug" in sys.argv: + print(" <DEBUG> pyautogui.moveTo(%s, %s)" % (x, y)) + pyautogui.moveTo(x, y, timeframe, pyautogui.easeOutQuad) + time.sleep(0.056) + else: + # Called from a method where the gui_lock is already active + if "--debug" in sys.argv: + print(" <DEBUG> pyautogui.moveTo(%s, %s)" % (x, y)) + pyautogui.moveTo(x, y, timeframe, pyautogui.easeOutQuad) + time.sleep(0.056) + + def gui_hover_x_y(self, x, y, timeframe=0.27): + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: # Prevent issues with multiple processes + self.__install_pyautogui_if_missing() + import pyautogui + pyautogui = self.__get_configured_pyautogui(pyautogui) + width_ratio = 1.0 + if ( + shared_utils.is_windows() + and ( + not hasattr(sb_config, "_saved_width_ratio") + or not sb_config._saved_width_ratio + ) + ): + window_rect = self.get_window_rect() + width = window_rect["width"] + height = window_rect["height"] + win_x = window_rect["x"] + win_y = window_rect["y"] + if ( + hasattr(sb_config, "_saved_width_ratio") + and sb_config._saved_width_ratio + ): + width_ratio = sb_config._saved_width_ratio + else: + scr_width = pyautogui.size().width + win_width = self.evaluate("screen.availWidth;") + width_ratio = round(float(scr_width) / float(win_width), 2) + width_ratio += 0.01 + if width_ratio < 0.45 or width_ratio > 2.55: + width_ratio = 1.01 + sb_config._saved_width_ratio = width_ratio + with suppress(Exception): + self.__set_window_rect(win_x, win_y, width, height) + self.__add_light_pause() + self.bring_active_window_to_front() + elif ( + shared_utils.is_windows() + and hasattr(sb_config, "_saved_width_ratio") + and sb_config._saved_width_ratio + ): + width_ratio = sb_config._saved_width_ratio + self.bring_active_window_to_front() + if shared_utils.is_windows(): + x = x * width_ratio + y = y * width_ratio + self.__gui_hover_x_y(x, y, timeframe=timeframe, uc_lock=False) + return + self.bring_active_window_to_front() + self.__gui_hover_x_y(x, y, timeframe=timeframe, uc_lock=False) + + def gui_hover_element(self, selector, timeframe=0.27): + try: + self.__verify_pyautogui_has_a_headed_browser() + except Exception: + self.hover_element(selector, timeframe=timeframe) + return + self.__slow_mode_pause_if_set() + element_rect = self.get_gui_element_rect(selector) + width = element_rect["width"] + height = element_rect["height"] + if width > 0 and height > 0: + x, y = self.get_gui_element_center(selector) + self.bring_active_window_to_front() + self.__gui_hover_x_y(x, y, timeframe=timeframe, uc_lock=False) + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.wait(0.1)) + + def hover_element(self, selector, timeframe=0.27): + element = self.select(selector) + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: + self.bring_active_window_to_front() + time.sleep(0.02) + element.mouse_move() + time.sleep(timeframe) + + def hover_and_click(self, hover_selector, click_selector): + if getattr(sb_config, "_cdp_mobile_mode", None): + self.select(click_selector).click() + return + hover_element = self.select(hover_selector) + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: + self.bring_active_window_to_front() + time.sleep(0.02) + hover_element.mouse_move() + time.sleep(0.25) + try: + self.click(click_selector, timeout=0.5) + except Exception: + self.select(click_selector, timeout=2).click() + + def gui_hover_and_click(self, hover_selector, click_selector): + if getattr(sb_config, "_cdp_mobile_mode", None): + self.select(click_selector).click() + return + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: + self.__make_sure_pyautogui_lock_is_writable() + self.bring_active_window_to_front() + self.gui_hover_element(hover_selector) + time.sleep(0.15) + self.gui_hover_element(click_selector) + self.click(click_selector) + + def internalize_links(self): + """All `target="_blank"` links become `target="_self"`. + This prevents those links from opening in a new tab.""" + self.set_attributes('[target="_blank"]', "target", "_self") + + def is_checked(self, selector): + """Return True if checkbox (or radio button) is checked.""" + selector = self.__convert_to_css_if_xpath(selector) + self.find_element(selector, timeout=settings.SMALL_TIMEOUT) + return bool(self.get_element_attribute(selector, "checked")) + + def is_selected(self, selector): + selector = self.__convert_to_css_if_xpath(selector) + return self.is_checked(selector) + + def check_if_unchecked(self, selector): + selector = self.__convert_to_css_if_xpath(selector) + if not self.is_checked(selector): + self.click(selector) + + def select_if_unselected(self, selector): + selector = self.__convert_to_css_if_xpath(selector) + self.check_if_unchecked(selector) + + def uncheck_if_checked(self, selector): + selector = self.__convert_to_css_if_xpath(selector) + if self.is_checked(selector): + self.click(selector) + + def unselect_if_selected(self, selector): + selector = self.__convert_to_css_if_xpath(selector) + self.uncheck_if_checked(selector) + + def is_element_present(self, selector): + try: + self.select(selector, timeout=0.01) + return True + except Exception: + return False + + def is_element_visible(self, selector): + selector = self.__convert_to_css_if_xpath(selector) + element = None + if ":contains(" not in selector: + try: + element = self.select(selector, timeout=0.01) + except Exception: + return False + if not element: + return False + try: + position = element.get_position() + return (position.width != 0 or position.height != 0) + except Exception: + return False + else: + with suppress(Exception): + tag_name = selector.split(":contains(")[0].split(" ")[-1] + text = selector.split(":contains(")[1].split(")")[0][1:-1] + self.loop.run_until_complete( + self.page.select(tag_name, timeout=0.1) + ) + self.loop.run_until_complete(self.page.find(text, timeout=0.1)) + return True + return False + + def is_text_visible(self, text, selector="body"): + selector = self.__convert_to_css_if_xpath(selector) + text = text.strip() + element = None + try: + element = self.find_element(selector, timeout=0.1) + except Exception: + return False + with suppress(Exception): + if text in element.text_all: + return True + return False + + def is_exact_text_visible(self, text, selector="body"): + selector = self.__convert_to_css_if_xpath(selector) + text = text.strip() + element = None + try: + element = self.find_element(selector, timeout=0.1) + except Exception: + return False + with suppress(Exception): + if text == element.text_all.strip(): + return True + return False + + def wait_for_text(self, text, selector="body", timeout=None): + if not timeout: + timeout = settings.SMALL_TIMEOUT + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (timeout * 1000.0) + text = text.strip() + element = None + failure = False + message = "" + try: + element = self.find_element(selector, timeout=timeout) + except Exception: + failure = True + plural = "s" + if timeout == 1: + plural = "" + msg = "\n Element {%s} was not found after %s second%s!" + message = msg % (selector, timeout, plural) + if failure: + raise Exception(message) + for i in range(int(timeout * 10)): + with suppress(Exception): + element = self.find_element(selector, timeout=0.1) + if text in element.text_all: + return True + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + time.sleep(0.1) + message = ( + "Expected text substring {%s} for {%s} was not found!\n" + " (Actual text was {%s})" + % (text, selector, element.text_all) + ) + raise Exception(message) + + def wait_for_text_not_visible(self, text, selector="body", timeout=None): + if not timeout: + timeout = settings.SMALL_TIMEOUT + text = text.strip() + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (timeout * 1000.0) + for i in range(int(timeout * 10)): + if not self.is_text_visible(text, selector): + return True + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + time.sleep(0.1) + plural = "s" + if timeout == 1: + plural = "" + raise Exception( + "Text {%s} in {%s} was still visible after %s second%s!" + % (text, selector, timeout, plural) + ) + + def wait_for_element_visible(self, selector, timeout=None): + if not timeout: + timeout = settings.SMALL_TIMEOUT + failure = False + message = "" + try: + self.select(selector, timeout=timeout) + except Exception: + failure = True + plural = "s" + if timeout == 1: + plural = "" + msg = "\n Element {%s} was not found after %s second%s!" + message = msg % (selector, timeout, plural) + if failure: + raise Exception(message) + for i in range(30): + if self.is_element_visible(selector): + return self.select(selector) + time.sleep(0.1) + raise Exception("Element {%s} was not visible!" % selector) + + def wait_for_element(self, selector, **kwargs): + """Same as wait_for_element_visible()""" + return self.wait_for_element_visible(selector, **kwargs) + + def wait_for_element_not_visible(self, selector, timeout=None): + """Wait for element to not be visible on page. (May still be in DOM)""" + if not timeout: + timeout = settings.SMALL_TIMEOUT + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (timeout * 1000.0) + for i in range(int(timeout * 10)): + if not self.is_element_present(selector): + return True + elif not self.is_element_visible(selector): + return True + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + time.sleep(0.1) + plural = "s" + if timeout == 1: + plural = "" + raise Exception( + "Element {%s} was still visible after %s second%s!" + % (selector, timeout, plural) + ) + + def wait_for_element_absent(self, selector, timeout=None): + """Wait for element to not be present in the DOM.""" + if not timeout: + timeout = settings.SMALL_TIMEOUT + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (timeout * 1000.0) + for i in range(int(timeout * 10)): + if not self.is_element_present(selector): + return True + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + time.sleep(0.1) + plural = "s" + if timeout == 1: + plural = "" + raise Exception( + "Element {%s} was still present after %s second%s!" + % (selector, timeout, plural) + ) + + def wait_for_any_of_elements_visible(self, *args, **kwargs): + """Waits for at least one of the elements to be visible. + Returns the first element that is found. + The input is a list of elements. (Should be CSS selectors) + Optional kwargs include: "timeout" (used by all selectors). + Raises an exception if no elements are visible by the timeout. + Examples: + sb.cdp.wait_for_any_of_elements_visible("h1", "h2", "h3") + OR + sb.cdp.wait_for_any_of_elements_visible(["h1", "h2", "h3"]) """ + selectors = [] + timeout = None + for kwarg in kwargs: + if kwarg == "timeout": + timeout = kwargs["timeout"] + elif kwarg == "by": + pass # Autodetected + elif kwarg == "selector" or kwarg == "selectors": + selector = kwargs[kwarg] + if isinstance(selector, str): + selectors.append(selector) + elif isinstance(selector, list): + selectors_list = selector + for selector in selectors_list: + if isinstance(selector, str): + selectors.append(selector) + else: + raise Exception('Unknown kwarg: "%s"!' % kwarg) + if not timeout: + timeout = settings.SMALL_TIMEOUT + for arg in args: + if isinstance(arg, list): + for selector in arg: + if isinstance(selector, str): + selectors.append(selector) + elif isinstance(arg, str): + selectors.append(arg) + if not selectors: + raise Exception("The selectors list was empty!") + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (timeout * 1000.0) + any_present = False + for i in range(int(timeout * 10)): + for selector in selectors: + if self.is_element_visible(selector): + return self.select(selector) + if self.is_element_present(selector): + any_present = True + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + time.sleep(0.1) + plural = "s" + if timeout == 1: + plural = "" + if not any_present: + # None of the elements exist in the HTML + raise Exception( + "None of the elements {%s} were present after %s second%s!" % ( + str(selectors), + timeout, + plural, + ) + ) + raise Exception( + "None of the elements %s were visible after %s second%s!" % ( + str(selectors), + timeout, + plural, + ) + ) + + def wait_for_any_of_elements_present(self, *args, **kwargs): + """Waits for at least one of the elements to be present. + Visibility not required, but element must be in the DOM. + Returns the first element that is found. + The input is a list of elements. (Should be CSS selectors) + Optional kwargs include: "timeout" (used by all selectors). + Raises an exception if no elements are present by the timeout. + Examples: + self.wait_for_any_of_elements_present("style", "script") + OR + self.wait_for_any_of_elements_present(["style", "script"]) """ + selectors = [] + timeout = None + for kwarg in kwargs: + if kwarg == "timeout": + timeout = kwargs["timeout"] + elif kwarg == "by": + pass # Autodetected + elif kwarg == "selector" or kwarg == "selectors": + selector = kwargs[kwarg] + if isinstance(selector, str): + selectors.append(selector) + elif isinstance(selector, list): + selectors_list = selector + for selector in selectors_list: + if isinstance(selector, str): + selectors.append(selector) + else: + raise Exception('Unknown kwarg: "%s"!' % kwarg) + if not timeout: + timeout = settings.SMALL_TIMEOUT + for arg in args: + if isinstance(arg, list): + for selector in arg: + if isinstance(selector, str): + selectors.append(selector) + elif isinstance(arg, str): + selectors.append(arg) + if not selectors: + raise Exception("The selectors list was empty!") + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (timeout * 1000.0) + for i in range(int(timeout * 10)): + for selector in selectors: + if self.is_element_present(selector): + return self.select(selector) + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + time.sleep(0.1) + plural = "s" + if timeout == 1: + plural = "" + # None of the elements exist in the HTML + raise Exception( + "None of the elements %s were present after %s second%s!" % ( + str(selectors), + timeout, + plural, + ) + ) + + def assert_any_of_elements_visible(self, *args, **kwargs): + """Like wait_for_any_of_elements_visible(), but returns nothing.""" + self.wait_for_any_of_elements_visible(*args, **kwargs) + return True + + def assert_any_of_elements_present(self, *args, **kwargs): + """Like wait_for_any_of_elements_present(), but returns nothing.""" + self.wait_for_any_of_elements_present(*args, **kwargs) + return True + + def assert_element(self, selector, timeout=None): + """Same as assert_element_visible()""" + self.assert_element_visible(selector, timeout=timeout) + return True + + def assert_element_visible(self, selector, timeout=None): + """Same as assert_element()""" + if not timeout: + timeout = settings.SMALL_TIMEOUT + failure = False + message = "" + try: + self.select(selector, timeout=timeout) + except Exception: + failure = True + plural = "s" + if timeout == 1: + plural = "" + msg = "\n Element {%s} was not found after %s second%s!" + message = msg % (selector, timeout, plural) + if failure: + raise Exception(message) + for i in range(30): + if self.is_element_visible(selector): + return True + time.sleep(0.1) + raise Exception("Element {%s} was not visible!" % selector) + + def assert_element_present(self, selector, timeout=None): + """Assert element is present in the DOM. (Visibility NOT required)""" + if not timeout: + timeout = settings.SMALL_TIMEOUT + failure = False + message = "" + try: + self.select(selector, timeout=timeout) + except Exception: + failure = True + plural = "s" + if timeout == 1: + plural = "" + msg = "\n Element {%s} was not found after %s second%s!" + message = msg % (selector, timeout, plural) + if failure: + raise Exception(message) + return True + + def assert_element_absent(self, selector, timeout=None): + """Assert element is not present in the DOM.""" + self.wait_for_element_absent(selector, timeout=timeout) + return True + + def assert_element_not_visible(self, selector, timeout=None): + """Assert element is not visible on page. (May still be in DOM)""" + self.wait_for_element_not_visible(selector, timeout=timeout) + return True + + def assert_element_attribute(self, selector, attribute, value=None): + attributes = self.get_element_attributes(selector) + if attribute not in attributes: + raise Exception( + "Attribute {%s} was not found in element {%s}!" + % (attribute, selector) + ) + if value and attributes[attribute] != value: + raise Exception( + "Expected value {%s} of attribute {%s} " + "was not found in element {%s}! " + "(Actual value was {%s})" + % (value, attribute, selector, attributes[attribute]) + ) + + def assert_title(self, title): + expected = title.strip() + actual = self.get_title().strip() + error = ( + "Expected page title [%s] does not match the actual title [%s]!" + ) + if expected != actual: + time.sleep(2) + actual = self.get_title().strip() + if expected != actual: + raise Exception(error % (expected, actual)) + + def assert_title_contains(self, substring): + expected = substring.strip() + actual = self.get_title().strip() + error = ( + "Expected title substring [%s] does not appear " + "in the actual page title [%s]!" + ) + if expected not in actual: + time.sleep(2) + actual = self.get_title().strip() + if expected not in actual: + raise Exception(error % (expected, actual)) + + def assert_url(self, url): + expected = url.strip() + actual = self.get_current_url().strip() + error = "Expected URL [%s] does not match the actual URL [%s]!" + try: + if expected != actual: + raise Exception(error % (expected, actual)) + except Exception: + time.sleep(2) + actual = self.get_current_url().strip() + if expected != actual: + raise Exception(error % (expected, actual)) + + def assert_url_contains(self, substring): + expected = substring.strip() + actual = self.get_current_url().strip() + error = ( + "Expected URL substring [%s] does not appear " + "in the full URL [%s]!" + ) + if expected not in actual: + time.sleep(2) + actual = self.get_current_url().strip() + if expected not in actual: + raise Exception(error % (expected, actual)) + + def assert_text(self, text, selector="body", timeout=None): + """Same as wait_for_text()""" + self.wait_for_text(text, selector=selector, timeout=timeout) + return True + + def assert_exact_text(self, text, selector="body", timeout=None): + if not timeout: + timeout = settings.SMALL_TIMEOUT + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (timeout * 1000.0) + text = text.strip() + element = None + failure = False + message = "" + try: + element = self.select(selector, timeout=timeout) + except Exception: + failure = True + plural = "s" + if timeout == 1: + plural = "" + msg = "\n Element {%s} was not found after %s second%s!" + message = msg % (selector, timeout, plural) + if failure: + raise Exception(message) + for i in range(int(timeout * 10)): + with suppress(Exception): + element = self.select(selector, timeout=0.1) + if ( + self.is_element_visible(selector) + and text.strip() == element.text_all.strip() + ): + return True + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + time.sleep(0.1) + message = ( + "Expected exact text {%s} for {%s} was not found! " + "(Actual text was {%s})" + % (text, selector, element.text_all) + ) + raise Exception(message) + + def assert_text_not_visible(self, text, selector="body", timeout=None): + """Raises an exception if the text is still visible after timeout.""" + self.wait_for_text_not_visible( + text, selector=selector, timeout=timeout + ) + return True + + def assert_true(self, expression, msg=None): + if not expression: + if not msg: + raise AssertionError("%s is not true." % expression) + else: + raise AssertionError( + "%s is not true. (%s)" % (expression, msg) + ) + + def assert_false(self, expression, msg=None): + if expression: + if not msg: + raise AssertionError("%s is not false." % expression) + else: + raise AssertionError( + "%s is not false. (%s)" % (expression, msg) + ) + + def assert_equal(self, first, second, msg=None): + if first != second: + if not msg: + raise AssertionError( + "%s is not equal to %s." % (first, second) + ) + else: + raise AssertionError( + "%s is not equal to %s. (%s)" % (first, second, msg)) + + def assert_not_equal(self, first, second, msg=None): + if first == second: + if not msg: + raise AssertionError("%s is equal to %s." % (first, second)) + else: + raise AssertionError( + "%s is equal to %s. (%s)" % (first, second, msg) + ) + + def assert_in(self, first, second, msg=None): + if first not in second: + if not msg: + raise AssertionError("%s is not in %s." % (first, second)) + else: + raise AssertionError( + "%s is not in %s. (%s)" % (first, second, msg) + ) + + def assert_not_in(self, first, second, msg=None): + if first in second: + if not msg: + raise AssertionError("%s is in %s." % (first, second)) + else: + raise AssertionError( + "%s is in %s. (%s)" % (first, second, msg) + ) + + def js_scroll_into_view(self, selector): + css_selector = self.__convert_to_css_if_xpath(selector) + css_selector = re.escape(css_selector) # Add "\\" to special chars + css_selector = js_utils.escape_quotes_if_needed(css_selector) + js_code = ( + "document.querySelector('%s')?.scrollIntoView();" + % css_selector + ) + with suppress(Exception): + self.loop.run_until_complete(self.page.evaluate(js_code)) + + def scroll_into_view(self, selector): + self.find_element(selector).scroll_into_view() + self.loop.run_until_complete(self.page.wait(0.05)) + + def scroll_to_y(self, y): + y = int(y) + js_code = "window.scrollTo(0, %s);" % y + with suppress(Exception): + self.loop.run_until_complete(self.page.evaluate(js_code)) + self.loop.run_until_complete(self.page.wait(0.05)) + + def scroll_by_y(self, y): + y = int(y) + js_code = "window.scrollBy(0, %s);" % y + with suppress(Exception): + self.loop.run_until_complete(self.page.evaluate(js_code)) + self.loop.run_until_complete(self.page.wait(0.05)) + + def scroll_to_top(self): + js_code = "window.scrollTo(0, 0);" + with suppress(Exception): + self.loop.run_until_complete(self.page.evaluate(js_code)) + self.loop.run_until_complete(self.page.wait(0.05)) + + def scroll_to_bottom(self): + js_code = "window.scrollTo(0, 10000);" + with suppress(Exception): + self.loop.run_until_complete(self.page.evaluate(js_code)) + self.loop.run_until_complete(self.page.wait(0.05)) + + def scroll_up(self, amount=25): + """Scrolls up as a percentage of the page.""" + try: + self.loop.run_until_complete(self.page.scroll_up(amount)) + except Exception: + amount = self.get_window_size()["height"] * amount / 100 + self.execute_script("window.scrollBy(0, -%s);" % amount) + self.loop.run_until_complete(self.page.wait(0.05)) + + def scroll_down(self, amount=25): + """Scrolls down as a percentage of the page.""" + try: + self.loop.run_until_complete(self.page.scroll_down(amount)) + except Exception: + amount = self.get_window_size()["height"] * amount / 100 + self.execute_script("window.scrollBy(0, %s);" % amount) + self.loop.run_until_complete(self.page.wait(0.05)) + + def save_page_source(self, name, folder=None): + from seleniumbase.core import log_helper + if not name.endswith(".html"): + name = name + ".html" + if folder: + abs_path = os.path.abspath(".") + file_path = os.path.join(abs_path, folder) + if not os.path.exists(file_path): + os.makedirs(file_path) + html_file_path = os.path.join(file_path, name) + else: + html_file_path = name + page_source = self.get_page_source() + last_page = self.get_current_url() + meta_charset = '<meta charset="utf-8">' + rendered_source = "" + if "://" in last_page: + base_href_html = log_helper.get_base_href_html(last_page) + if ' charset="' not in page_source: + rendered_source = "%s\n%s\n%s" % ( + base_href_html, meta_charset, page_source + ) + else: + rendered_source = "%s\n%s" % (base_href_html, page_source) + else: + rendered_source = page_source + html_file = open(html_file_path, mode="w+", encoding="utf-8") + html_file.write(rendered_source) + html_file.close() + + def save_as_html(self, name, folder=None): + """Same as self.save_page_source()""" + self.save_page_source(name, folder=folder) + + def save_screenshot(self, name, folder=None, selector=None): + filename = name + if folder: + filename = os.path.join(folder, name) + if not selector: + self.loop.run_until_complete( + self.page.save_screenshot(filename) + ) + else: + self.select(selector).save_screenshot(filename) + + def print_to_pdf(self, name, folder=None): + filename = name + if folder: + filename = os.path.join(folder, name) + self.loop.run_until_complete(self.page.print_to_pdf(filename)) + + def save_as_pdf(self, *args, **kwargs): + self.print_to_pdf(*args, **kwargs) + + def save_as_pdf_to_logs(self, name=None): + """Saves the page as a PDF to the "latest_logs/" folder. + Naming is automatic: + If NO NAME provided: "_1_PDF.pdf", "_2_PDF.pdf", etc. + If NAME IS provided, then: "_1_name.pdf", "_2_name.pdf", etc.""" + script_logpath = os.path.join(self.log_path, self.__get_script_id()) + self.__create_log_path_as_needed(script_logpath) + if name: + name = str(name) + self.__saved_pdf_count += 1 + if not name or len(name) == 0: + name = "_%s_PDF.pdf" % self.__saved_pdf_count + else: + pre_name = "_%s_" % self.__saved_pdf_count + if len(name) >= 4 and name[-4:].lower() == ".pdf": + name = name[:-4] + if len(name) == 0: + name = "PDF" + name = "%s%s.pdf" % (pre_name, name) + sb_config._has_logs = True + return self.print_to_pdf(name, folder=script_logpath) + + def save_screenshot_to_logs(self, name=None, selector=None): + """Saves a screenshot of the current page to the "latest_logs/" folder. + Naming is automatic: + If NO NAME provided: "_1_screenshot.png", "_2_screenshot.png", etc. + If NAME IS provided, it becomes: "_1_name.png", "_2_name.png", etc. + The screenshot will include the entire page unless a selector is given. + If a provided selector is not found, then takes a full-page screenshot. + The screenshot will be in PNG format.""" + script_logpath = os.path.join(self.log_path, self.__get_script_id()) + self.__create_log_path_as_needed(script_logpath) + if name: + name = str(name) + self.__screenshot_count += 1 + if not name or len(name) == 0: + name = "_%s_screenshot.png" % self.__screenshot_count + else: + pre_name = "_%s_" % self.__screenshot_count + if len(name) >= 4 and name[-4:].lower() == ".png": + name = name[:-4] + if len(name) == 0: + name = "screenshot" + name = "%s%s.png" % (pre_name, name) + if selector: + if self.is_element_present(selector): + return self.save_screenshot( + name, folder=script_logpath, selector=selector + ) + sb_config._has_logs = True + return self.save_screenshot(name, folder=script_logpath) + + def save_page_source_to_logs(self, name=None): + """Saves the page HTML to the "latest_logs/" folder. + Naming is automatic: + If NO NAME provided: "_1_source.html", "_2_source.html", etc. + If NAME IS provided, then: "_1_name.html", "_2_name.html", etc.""" + script_logpath = os.path.join(self.log_path, self.__get_script_id()) + self.__create_log_path_as_needed(script_logpath) + if name: + name = str(name) + self.__page_source_count += 1 + if not name or len(name) == 0: + name = "_%s_source.html" % self.__page_source_count + else: + pre_name = "_%s_" % self.__page_source_count + if len(name) >= 4 and name[-4:].lower() == ".html": + name = name[:-4] + if len(name) == 0: + name = "source" + name = "%s%s.html" % (pre_name, name) + sb_config._has_logs = True + return self.save_page_source(name, folder=script_logpath) + + def save_as_html_to_logs(self, name=None): + """Same as save_page_source_to_logs().""" + self.save_page_source_to_logs(name=name) + + def save_data_to_logs(self, data, file_name=None): + """Saves data to the "latest_logs/" data folder of the current script. + If no file_name, file_name becomes: "data_1.txt", "data_2.txt", etc. + Useful variables for getting the "latest_logs/" or script data folders: + self.log_path OR self.log_abspath (For the top-level folder) + self.data_path OR self.data_abspath (Individual script folders) + If a file_name is given with no extension, adds ".txt" to the end.""" + script_logpath = os.path.join(self.log_path, self.__get_script_id()) + self.__create_log_path_as_needed(script_logpath) + if file_name: + file_name = str(file_name) + if not file_name or len(file_name) == 0: + self.__logs_data_count += 1 + file_name = "data_%s.txt" % self.__logs_data_count + elif "." not in file_name: + file_name = "%s.txt" % file_name + self.__last_data_file = file_name + sb_config._has_logs = True + destination_folder = script_logpath + page_utils._save_data_as(data, destination_folder, file_name) + + def append_data_to_logs(self, data, file_name=None): + """Saves data to the "latest_logs/" folder of the current script. + If no file_name, file_name becomes the last data file used in + save_data_to_logs() or append_data_to_logs(). + If neither method was called before, creates "data_1.txt".""" + script_logpath = os.path.join(self.log_path, self.__get_script_id()) + self.__create_log_path_as_needed(script_logpath) + if file_name: + file_name = str(file_name) + if (not file_name or len(file_name) == 0) and self.__last_data_file: + file_name = self.__last_data_file + elif not file_name or len(file_name) == 0: + if self.__logs_data_count == 0: + self.__logs_data_count += 1 + file_name = "data_%s.txt" % self.__logs_data_count + elif "." not in file_name: + file_name = "%s.txt" % file_name + self.__last_data_file = file_name + sb_config._has_logs = True + destination_folder = script_logpath + page_utils._append_data_to_file(data, destination_folder, file_name) + + def __get_script_id(self): + if hasattr(sb_config, "_cdp_saved_id") and sb_config._cdp_saved_id: + return sb_config._cdp_saved_id + else: + stack_base = traceback.format_stack()[0].split(", in ")[0] + test_base = stack_base.split(os.sep)[-1] + return test_base.split('"')[0] + + def __create_log_path_as_needed(self, script_logpath): + if not os.path.exists(script_logpath): + try: + os.makedirs(script_logpath) + except Exception: + pass + + +class Chrome(CDPMethods): + def __init__(self, url=None, **kwargs): + stack_base = traceback.format_stack()[0].split(", in ")[0] + test_base = stack_base.split(os.sep)[-1] + filename = test_base.split('"')[0] + linename = ".line_" + test_base.split(", line ")[-1] + context_id = filename.split(".")[0] + linename + sb_config._cdp_saved_id = context_id + if not url: + url = "about:blank" + loop = asyncio.new_event_loop() + kwargs["loop"] = loop + driver = cdp_util.start_sync(**kwargs) + page = loop.run_until_complete(driver.get(url)) + wait_timeout = 30.0 + if hasattr(sb_config, "_cdp_proxy") and sb_config._cdp_proxy: + wait_timeout = 45.0 + try: + loop.run_until_complete( + asyncio.wait_for(page.wait(), timeout=wait_timeout) + ) + except asyncio.TimeoutError: + pass + except Exception: + pass + super().__init__(loop, page, driver) diff --git a/seleniumbase/core/sb_driver.py b/seleniumbase/core/sb_driver.py new file mode 100644 index 00000000000..3108b03ed99 --- /dev/null +++ b/seleniumbase/core/sb_driver.py @@ -0,0 +1,411 @@ +"""Add new methods to extend the driver""" +from contextlib import suppress +from selenium.webdriver.remote.webdriver import WebDriver +from selenium.webdriver.remote.webelement import WebElement +from seleniumbase.config import settings +from seleniumbase.fixtures import js_utils +from seleniumbase.fixtures import page_actions +from seleniumbase.fixtures import page_utils +from seleniumbase.fixtures import shared_utils + + +class DriverMethods(WebDriver): + def __init__(self, driver): + self.driver = driver + if hasattr(driver, "session_id"): + self.session_id = driver.session_id + if hasattr(driver, "command_executor"): + self.command_executor = driver.command_executor + + def __is_cdp_swap_needed(self): + """If the driver is disconnected, use a CDP method when available.""" + return shared_utils.is_cdp_swap_needed(self.driver) + + def find_element(self, by=None, value=None): + if not value: + value = by + by = "css selector" + elif not by: + by = "css selector" + else: + value, by = page_utils.swap_selector_and_by_if_reversed(value, by) + return self.driver.default_find_element(by=by, value=value) + + def find_elements(self, by=None, value=None): + if not value: + value = by + by = "css selector" + elif not by: + by = "css selector" + else: + value, by = page_utils.swap_selector_and_by_if_reversed(value, by) + return self.driver.default_find_elements(by=by, value=value) + + def select(self, *args, **kwargs): + if self.__is_cdp_swap_needed(): + return self.driver.cdp.select(*args, **kwargs) + else: + return self.find_element(*args, **kwargs) + + def select_all(self, *args, **kwargs): + if self.__is_cdp_swap_needed(): + return self.driver.cdp.select_all(*args, **kwargs) + else: + return self.find_elements(*args, **kwargs) + + def add_cookie(self, *args, **kwargs): + page_actions._reconnect_if_disconnected(self.driver) + self.driver.default_add_cookie(*args, **kwargs) + + def get_cookie(self, *args, **kwargs): + page_actions._reconnect_if_disconnected(self.driver) + self.driver.default_get_cookie(*args, **kwargs) + + def delete_cookie(self, *args, **kwargs): + page_actions._reconnect_if_disconnected(self.driver) + self.driver.default_delete_cookie(*args, **kwargs) + + def back(self): + if self.__is_cdp_swap_needed(): + self.driver.cdp.go_back() + return + self.driver.default_back() + + def forward(self): + if self.__is_cdp_swap_needed(): + self.driver.cdp.go_forward() + return + self.driver.default_forward() + + def refresh(self, *args, **kwargs): + if self.__is_cdp_swap_needed(): + self.driver.cdp.refresh(*args, **kwargs) + return + self.driver.default_refresh() + + def locator(self, selector, by=None): + if not by: + by = "css selector" + else: + selector, by = page_utils.swap_selector_and_by_if_reversed( + selector, by + ) + with suppress(Exception): + return self.driver.default_find_element(by=by, value=selector) + raise Exception('No such Element: {%s} (by="%s")!' % (selector, by)) + + def get_attribute(self, selector, attribute, by="css selector"): + element = self.locator(selector, by=by) + return element.get_attribute(attribute) + + def get_parent(self, element): + if self.__is_cdp_swap_needed(): + return self.driver.cdp.get_parent(element) + if isinstance(element, str): + element = self.locator(element) + return element.find_element(by="xpath", value="..") + + def get_current_url(self): + if self.__is_cdp_swap_needed(): + current_url = self.driver.cdp.get_current_url() + else: + current_url = self.driver.current_url + return current_url + + def get_page_source(self): + if self.__is_cdp_swap_needed(): + return self.driver.cdp.get_page_source() + return self.driver.page_source + + def get_title(self): + if self.__is_cdp_swap_needed(): + return self.driver.cdp.get_title() + return self.driver.title + + def open_url(self, *args, **kwargs): + page_actions.open_url(self.driver, *args, **kwargs) + + def click(self, *args, **kwargs): + page_actions.click(self.driver, *args, **kwargs) + + def click_link(self, *args, **kwargs): + page_actions.click_link(self.driver, *args, **kwargs) + + def click_if_visible(self, *args, **kwargs): + page_actions.click_if_visible(self.driver, *args, **kwargs) + + def click_active_element(self, *args, **kwargs): + page_actions.click_active_element(self.driver, *args, **kwargs) + + def send_keys(self, *args, **kwargs): + page_actions.send_keys(self.driver, *args, **kwargs) + + def press_keys(self, *args, **kwargs): + page_actions.press_keys(self.driver, *args, **kwargs) + + def update_text(self, *args, **kwargs): + page_actions.update_text(self.driver, *args, **kwargs) + + def submit(self, *args, **kwargs): + page_actions.submit(self.driver, *args, **kwargs) + + def assert_element_visible(self, *args, **kwargs): + page_actions.assert_element_visible(self.driver, *args, **kwargs) + + def assert_element_present(self, *args, **kwargs): + page_actions.assert_element_present(self.driver, *args, **kwargs) + + def assert_element_not_visible(self, *args, **kwargs): + page_actions.assert_element_not_visible(self.driver, *args, **kwargs) + + def assert_text(self, *args, **kwargs): + page_actions.assert_text(self.driver, *args, **kwargs) + + def assert_exact_text(self, *args, **kwargs): + page_actions.assert_exact_text(self.driver, *args, **kwargs) + + def assert_non_empty_text(self, *args, **kwargs): + return page_actions.assert_non_empty_text( + self.driver, *args, **kwargs + ) + + def assert_text_not_visible(self, *args, **kwargs): + return page_actions.assert_text_not_visible( + self.driver, *args, **kwargs + ) + + def wait_for_element(self, *args, **kwargs): + return page_actions.wait_for_element(self.driver, *args, **kwargs) + + def wait_for_element_visible(self, *args, **kwargs): + return page_actions.wait_for_element(self.driver, *args, **kwargs) + + def wait_for_element_present(self, *args, **kwargs): + return page_actions.wait_for_selector(self.driver, *args, **kwargs) + + def wait_for_element_absent(self, *args, **kwargs): + return page_actions.wait_for_element_absent( + self.driver, *args, **kwargs + ) + + def wait_for_element_not_visible(self, *args, **kwargs): + return page_actions.wait_for_element_not_visible( + self.driver, *args, **kwargs + ) + + def wait_for_selector(self, *args, **kwargs): + return page_actions.wait_for_selector(self.driver, *args, **kwargs) + + def wait_for_text(self, *args, **kwargs): + return page_actions.wait_for_text(self.driver, *args, **kwargs) + + def wait_for_exact_text(self, *args, **kwargs): + return page_actions.wait_for_exact_text(self.driver, *args, **kwargs) + + def wait_for_non_empty_text(self, *args, **kwargs): + return page_actions.wait_for_non_empty_text( + self.driver, *args, **kwargs + ) + + def wait_for_text_not_visible(self, *args, **kwargs): + return page_actions.wait_for_text_not_visible( + self.driver, *args, **kwargs + ) + + def wait_for_and_accept_alert(self, *args, **kwargs): + return page_actions.wait_for_and_accept_alert( + self.driver, *args, **kwargs + ) + + def wait_for_and_dismiss_alert(self, *args, **kwargs): + return page_actions.wait_for_and_dismiss_alert( + self.driver, *args, **kwargs + ) + + def is_element_present(self, *args, **kwargs): + return page_actions.is_element_present(self.driver, *args, **kwargs) + + def is_element_visible(self, *args, **kwargs): + return page_actions.is_element_visible(self.driver, *args, **kwargs) + + def is_text_visible(self, *args, **kwargs): + return page_actions.is_text_visible(self.driver, *args, **kwargs) + + def is_exact_text_visible(self, *args, **kwargs): + return page_actions.is_exact_text_visible(self.driver, *args, **kwargs) + + def is_attribute_present(self, *args, **kwargs): + return page_actions.has_attribute(self.driver, *args, **kwargs) + + def is_non_empty_text_visible(self, *args, **kwargs): + return page_actions.is_non_empty_text_visible( + self.driver, *args, **kwargs + ) + + def is_valid_url(self, url): + """Return True if the url is a valid url.""" + return page_utils.is_valid_url(url) + + def is_alert_present(self): + try: + self.driver.switch_to.alert + return True + except Exception: + return False + + def is_online(self): + if self.__is_cdp_swap_needed(): + return self.driver.cdp.evaluate("navigator.onLine;") + return self.driver.execute_script("return navigator.onLine;") + + def is_connected(self): + """ + Return True if WebDriver is connected to the browser. + Note that the stealthy CDP-Driver isn't a WebDriver. + In CDP Mode, the CDP-Driver controls the web browser. + The CDP-Driver can be connected while WebDriver isn't. + """ + if shared_utils.is_windows(): + return ( + not hasattr(self.driver, "_is_connected") + or self.driver._is_connected + ) + try: + self.driver.window_handles + return True + except Exception: + return False + + def is_uc_mode_active(self): + """Return True if the driver is using UC Mode. False otherwise.""" + return ( + hasattr(self.driver, "_is_using_uc") + and self.driver._is_using_uc + ) + + def is_cdp_mode_active(self): + """CDP Mode is a special mode within UC Mode. Activated separately. + Return True if CDP Mode is loaded in the driver. False otherwise.""" + return ( + hasattr(self.driver, "_is_using_cdp") + and self.driver._is_using_cdp + ) + + def js_click(self, *args, **kwargs): + return page_actions.js_click(self.driver, *args, **kwargs) + + def get_text(self, *args, **kwargs): + return page_actions.get_text(self.driver, *args, **kwargs) + + def get_active_element_css(self, *args, **kwargs): + if self.__is_cdp_swap_needed(): + return self.driver.cdp.get_active_element_css() + return js_utils.get_active_element_css(self.driver, *args, **kwargs) + + def get_locale_code(self, *args, **kwargs): + if self.__is_cdp_swap_needed(): + return self.driver.cdp.get_locale_code() + return js_utils.get_locale_code(self.driver, *args, **kwargs) + + def get_screen_rect(self, *args, **kwargs): + if self.__is_cdp_swap_needed(): + return self.driver.cdp.get_screen_rect() + return js_utils.get_screen_rect(self.driver, *args, **kwargs) + + def get_origin(self, *args, **kwargs): + if self.__is_cdp_swap_needed(): + return self.driver.cdp.get_origin() + return js_utils.get_origin(self.driver, *args, **kwargs) + + def get_user_agent(self, *args, **kwargs): + if self.__is_cdp_swap_needed(): + return self.driver.cdp.get_user_agent() + return js_utils.get_user_agent(self.driver, *args, **kwargs) + + def get_cookie_string(self, *args, **kwargs): + if self.__is_cdp_swap_needed(): + return self.driver.cdp.get_cookie_string() + return js_utils.get_cookie_string(self.driver, *args, **kwargs) + + def highlight(self, *args, **kwargs): + if self.__is_cdp_swap_needed(): + selector = None + if "selector" in kwargs: + selector = kwargs["selector"] + else: + selector = args[0] + self.driver.cdp.highlight(selector) + return + if "scroll" in kwargs: + kwargs.pop("scroll") + w_args = kwargs.copy() + if "loops" in w_args: + w_args.pop("loops") + element = page_actions.wait_for_element(self.driver, *args, **w_args) + browser = self.driver.capabilities["browserName"].lower() + js_utils.slow_scroll_to_element(self.driver, element, browser) + if "timeout" in kwargs: + kwargs.pop("timeout") + js_utils.highlight(self.driver, *args, **kwargs) + + def highlight_click(self, *args, **kwargs): + self.highlight(*args, **kwargs) + if "loops" in kwargs: + kwargs.pop("loops") + if "scroll" in kwargs: + kwargs.pop("scroll") + page_actions.click(self.driver, *args, **kwargs) + + def highlight_if_visible( + self, selector, by="css selector", loops=4, scroll=True + ): + if self.is_element_visible(selector, by=by): + self.highlight(selector, by=by, loops=loops, scroll=scroll) + + def switch_to_default_window(self): + self.driver.switch_to.window(self.driver.window_handles[0]) + + def switch_to_newest_window(self): + self.driver.switch_to.window(self.driver.window_handles[-1]) + + def open_new_window(self, switch_to=True): + if switch_to: + try: + self.driver.switch_to.new_window("tab") + except Exception: + self.driver.execute_script("window.open('');") + self.switch_to_newest_window() + else: + self.driver.execute_script("window.open('');") + + def open_new_tab(self, switch_to=True): + self.open_new_window(switch_to=switch_to) + + def switch_to_window(self, *args, **kwargs): + page_actions.switch_to_window(self.driver, *args, **kwargs) + + def switch_to_newest_tab(self): + if self.__is_cdp_swap_needed(): + self.driver.cdp.switch_to_newest_tab() + return + self.switch_to_newest_window() + + def switch_to_tab(self, *args, **kwargs): + self.switch_to_window(*args, **kwargs) + + def switch_to_frame(self, frame="iframe"): + if isinstance(frame, WebElement): + self.driver.switch_to.frame(frame) + else: + iframe = self.locator(frame) + self.driver.switch_to.frame(iframe) + + def reset_window_size(self): + if self.__is_cdp_swap_needed(): + self.driver.cdp.reset_window_size() + return + x = settings.WINDOW_START_X + y = settings.WINDOW_START_Y + width = settings.CHROME_START_WIDTH + height = settings.CHROME_START_HEIGHT + self.driver.set_window_rect(x, y, width, height) diff --git a/seleniumbase/core/session_helper.py b/seleniumbase/core/session_helper.py new file mode 100644 index 00000000000..3894beda18c --- /dev/null +++ b/seleniumbase/core/session_helper.py @@ -0,0 +1,16 @@ +from seleniumbase import config as sb_config + + +def end_reused_class_session_as_needed(): + if ( + getattr(sb_config, "reuse_class_session", None) + and getattr(sb_config, "shared_driver", None) + ): + if ( + hasattr(sb_config.shared_driver, "service") + and sb_config.shared_driver.service.process + ): + try: + sb_config.shared_driver.quit() + except Exception: + sb_config.shared_driver = None diff --git a/seleniumbase/core/settings_parser.py b/seleniumbase/core/settings_parser.py new file mode 100644 index 00000000000..fafed50e58f --- /dev/null +++ b/seleniumbase/core/settings_parser.py @@ -0,0 +1,178 @@ +import re +from seleniumbase.config import settings + + +def set_settings(settings_file): + if not settings_file.endswith(".py"): + raise Exception("\n\n`%s` is not a Python file!\n\n" % settings_file) + + f = open(settings_file, "r") + all_code = f.read() + f.close() + + override_settings = {} + num_settings = 0 + + code_lines = all_code.split("\n") + for line in code_lines: + + # KEY = "VALUE" + data = re.match(r'^\s*([\S]+)\s*=\s*"([\S\s]+)"\s*$', line) + if data: + key = data.group(1) + value = '"' + data.group(2) + '"' + override_settings[key] = value + num_settings += 1 + continue + + # KEY = 'VALUE' + data = re.match(r"^\s*([\S]+)\s*=\s*'([\S\s]+)'\s*$", line) + if data: + key = data.group(1) + value = "'" + data.group(2) + "'" + override_settings[key] = value + num_settings += 1 + continue + + # KEY = VALUE + data = re.match(r"^\s*([\S]+)\s*=\s*([\S]+)\s*$", line) + if data: + key = data.group(1) + value = data.group(2) + override_settings[key] = value + num_settings += 1 + continue + + for key in override_settings.keys(): + value = override_settings[key] + if value.replace(".", "1").isdigit(): + if value.count(".") == 1: + override_settings[key] = float(value) + elif value.count(".") == 0: + override_settings[key] = int(value) + else: + continue + elif value == "True": + override_settings[key] = True + elif value == "False": + override_settings[key] = False + elif len(value) > 1 and value.startswith('"') and value.endswith('"'): + override_settings[key] = value[1:-1] + elif len(value) > 1 and value.startswith("'") and value.endswith("'"): + override_settings[key] = value[1:-1] + else: + continue + + if key == "MINI_TIMEOUT": + settings.MINI_TIMEOUT = override_settings[key] + elif key == "SMALL_TIMEOUT": + settings.SMALL_TIMEOUT = override_settings[key] + elif key == "LARGE_TIMEOUT": + settings.LARGE_TIMEOUT = override_settings[key] + elif key == "EXTREME_TIMEOUT": + settings.EXTREME_TIMEOUT = override_settings[key] + elif key == "PAGE_LOAD_TIMEOUT": + settings.PAGE_LOAD_TIMEOUT = override_settings[key] + elif key == "ARCHIVE_EXISTING_LOGS": + settings.ARCHIVE_EXISTING_LOGS = override_settings[key] + elif key == "ARCHIVE_EXISTING_DOWNLOADS": + settings.ARCHIVE_EXISTING_DOWNLOADS = override_settings[key] + elif key == "SCREENSHOT_WITH_BACKGROUND": + settings.SCREENSHOT_WITH_BACKGROUND = override_settings[key] + elif key == "SCREENSHOT_NAME": + settings.SCREENSHOT_NAME = override_settings[key] + elif key == "BASIC_INFO_NAME": + settings.BASIC_INFO_NAME = override_settings[key] + elif key == "PAGE_SOURCE_NAME": + settings.PAGE_SOURCE_NAME = override_settings[key] + elif key == "LATEST_REPORT_DIR": + settings.LATEST_REPORT_DIR = override_settings[key] + elif key == "REPORT_ARCHIVE_DIR": + settings.REPORT_ARCHIVE_DIR = override_settings[key] + elif key == "HTML_REPORT": + settings.HTML_REPORT = override_settings[key] + elif key == "RESULTS_TABLE": + settings.RESULTS_TABLE = override_settings[key] + elif key == "SWITCH_TO_NEW_TABS_ON_CLICK": + settings.SWITCH_TO_NEW_TABS_ON_CLICK = override_settings[key] + elif key == "WAIT_FOR_RSC_ON_PAGE_LOADS": + settings.WAIT_FOR_RSC_ON_PAGE_LOADS = override_settings[key] + elif key == "WAIT_FOR_RSC_ON_CLICKS": + settings.WAIT_FOR_RSC_ON_CLICKS = override_settings[key] + elif key == "WAIT_FOR_ANGULARJS": + settings.WAIT_FOR_ANGULARJS = override_settings[key] + elif key == "DEFAULT_DEMO_MODE_TIMEOUT": + settings.DEFAULT_DEMO_MODE_TIMEOUT = override_settings[key] + elif key == "HIGHLIGHTS": + settings.HIGHLIGHTS = override_settings[key] + elif key == "DEFAULT_MESSAGE_DURATION": + settings.DEFAULT_MESSAGE_DURATION = override_settings[key] + elif key == "DISABLE_CSP_ON_FIREFOX": + settings.DISABLE_CSP_ON_FIREFOX = override_settings[key] + elif key == "DISABLE_CSP_ON_CHROME": + settings.DISABLE_CSP_ON_CHROME = override_settings[key] + elif key == "RAISE_INVALID_PROXY_STRING_EXCEPTION": + settings.RAISE_INVALID_PROXY_STRING_EXCEPTION = override_settings[ + key + ] + elif key == "WINDOW_START_X": + settings.WINDOW_START_X = override_settings[key] + elif key == "WINDOW_START_Y": + settings.WINDOW_START_Y = override_settings[key] + elif key == "CHROME_START_WIDTH": + settings.CHROME_START_WIDTH = override_settings[key] + elif key == "CHROME_START_HEIGHT": + settings.CHROME_START_HEIGHT = override_settings[key] + elif key == "HEADLESS_START_WIDTH": + settings.HEADLESS_START_WIDTH = override_settings[key] + elif key == "HEADLESS_START_HEIGHT": + settings.HEADLESS_START_HEIGHT = override_settings[key] + elif key == "HIDE_DRIVER_DOWNLOADS": + settings.HIDE_DRIVER_DOWNLOADS = override_settings[key] + elif key == "MASTERQA_DEFAULT_VALIDATION_MESSAGE": + settings.MASTERQA_DEFAULT_VALIDATION_MESSAGE = override_settings[ + key + ] + elif key == "MASTERQA_WAIT_TIME_BEFORE_VERIFY": + settings.MASTERQA_WAIT_TIME_BEFORE_VERIFY = override_settings[key] + elif key == "MASTERQA_START_IN_FULL_SCREEN_MODE": + settings.MASTERQA_START_IN_FULL_SCREEN_MODE = override_settings[ + key + ] + elif key == "MASTERQA_MAX_IDLE_TIME_BEFORE_QUIT": + settings.MASTERQA_MAX_IDLE_TIME_BEFORE_QUIT = override_settings[ + key + ] + elif key == "TOTP_KEY": + settings.TOTP_KEY = override_settings[key] + elif key == "DB_HOST": + settings.DB_HOST = override_settings[key] + elif key == "DB_PORT": + settings.DB_PORT = override_settings[key] + elif key == "DB_USERNAME": + settings.DB_USERNAME = override_settings[key] + elif key == "DB_PASSWORD": + settings.DB_PASSWORD = override_settings[key] + elif key == "DB_SCHEMA": + settings.DB_SCHEMA = override_settings[key] + elif key == "S3_LOG_BUCKET": + settings.S3_LOG_BUCKET = override_settings[key] + elif key == "S3_BUCKET_URL": + settings.S3_BUCKET_URL = override_settings[key] + elif key == "S3_SELENIUM_ACCESS_KEY": + settings.S3_SELENIUM_ACCESS_KEY = override_settings[key] + elif key == "S3_SELENIUM_SECRET_KEY": + settings.S3_SELENIUM_SECRET_KEY = override_settings[key] + elif key == "ENCRYPTION_KEY": + settings.ENCRYPTION_KEY = override_settings[key] + elif key == "OBFUSCATION_START_TOKEN": + settings.OBFUSCATION_START_TOKEN = override_settings[key] + elif key == "OBFUSCATION_END_TOKEN": + settings.OBFUSCATION_END_TOKEN = override_settings[key] + else: + continue + + if num_settings == 0: + raise Exception("Unable to parse the settings file!") + + return override_settings diff --git a/seleniumbase/core/style_sheet.py b/seleniumbase/core/style_sheet.py old mode 100755 new mode 100644 index 3ac19a5a762..ca8d78e314f --- a/seleniumbase/core/style_sheet.py +++ b/seleniumbase/core/style_sheet.py @@ -1,11 +1,24 @@ -title = '''<meta id="OGTitle" property="og:title" content="SeleniumBase"> +import textwrap +from seleniumbase.fixtures import constants + + +class Saved: + # Storing data to prevent extra loading + pass + + +def get_report_style(): + if hasattr(Saved, "report_style"): + return Saved.report_style + # Uses caching to prevent extra method calls + REPORT_FAVICON = constants.Report.get_favicon() + title = """<meta id="OGTitle" property="og:title" content="SeleniumBase"> <title>Test Report ''' % ( - "https://raw.githubusercontent.com/seleniumbase/SeleniumBase" - "/master/seleniumbase/resources/favicon.ico") - -style = title + '''''' - -# Bootstrap Tour Backdrop Style -bt_backdrop_style = ( - ''' - .tour-tour-element { - box-shadow: 0 0 0 99999px rgba(0, 0, 0, 0.20); - pointer-events: none !important; - z-index: 9999; - } - - :not(.tour-tour-element) .orphan.tour-tour { - box-shadow: 0 0 0 99999px rgba(0, 0, 0, 0.20); - } - - .tour-tour { - pointer-events: auto; - z-index: 9999; - } - ''') - -sh_style_test = ( - ''' - var test_tour = new Shepherd.Tour({ - defaults: { - classes: 'shepherd-theme-dark', - scrollTo: true - } - }); - ''') - -# Hopscotch Backdrop Style -hops_backdrop_style = ( - ''' - .hopscotch-bubble-container { - font-size: 110%; - } - ''') - -# Shepherd Backdrop Style -sh_backdrop_style = ( - ''' - body.shepherd-active .shepherd-target.shepherd-enabled { - box-shadow: 0 0 0 99999px rgba(0, 0, 0, 0.20); - pointer-events: none !important; - z-index: 9999; - } - - body.shepherd-active .shepherd-orphan { - box-shadow: 0 0 0 99999px rgba(0, 0, 0, 0.20); - pointer-events: auto; - z-index: 9999; - } - - body.shepherd-active - .shepherd-enabled.shepherd-element-attached-top { - position: relative; - } - - body.shepherd-active - .shepherd-enabled.shepherd-element-attached-bottom { - position: relative; - } - - body.shepherd-active .shepherd-step { - pointer-events: auto; - z-index: 9999; - } - - body.shepherd-active { - pointer-events: none !important; - } - ''') + """ + ) + style = textwrap.dedent(style) + Saved.report_style = style + return style + + +def get_bt_backdrop_style(): + # Bootstrap Tour Backdrop Style + if hasattr(Saved, "bt_backdrop_style"): + return Saved.bt_backdrop_style + bt_backdrop_style = """ + .tour-tour-element { + pointer-events: none !important; + } + :not(.tour-tour-element) .orphan.tour-tour { + box-shadow: 0 0 0 88422px rgba(0, 0, 0, 0.42); + pointer-events: auto !important; + }""" + bt_backdrop_style = textwrap.dedent(bt_backdrop_style) + Saved.bt_backdrop_style = bt_backdrop_style + return bt_backdrop_style + + +def get_dt_backdrop_style(): + # DriverJS Tour Backdrop Style + if hasattr(Saved, "dt_backdrop_style"): + return Saved.dt_backdrop_style + dt_backdrop_style = """ + .driver-fix-stacking { + pointer-events: none !important; + } + #driver-popover-item, .popover-class { + pointer-events: auto !important; + } + button.driver-prev-btn.driver-disabled { + visibility: hidden; + }""" + dt_backdrop_style = textwrap.dedent(dt_backdrop_style) + Saved.dt_backdrop_style = dt_backdrop_style + return dt_backdrop_style + + +def get_messenger_style(): + if hasattr(Saved, "messenger_style"): + return Saved.messenger_style + font_family = '"open-sans",Arial,sans-serif !important' + messenger_style = """ + .messenger-message-inner { + font-family: %s; + font-size: 17px; + } + ul.messenger-theme-flat, ul.messenger-theme-future { + box-shadow: 2px 2px 9px 4px rgba(32, 142, 120, 0.28), + 2px 2px 9px 4px rgba(200, 240, 80, 0.34) !important; + }""" % font_family + messenger_style = textwrap.dedent(messenger_style) + Saved.messenger_style = messenger_style + return messenger_style + + +def get_sh_style_test(): + if hasattr(Saved, "sh_style_test"): + return Saved.sh_style_test + sh_style_test = """ + var test_tour = new Shepherd.Tour({ + defaults: { + classes: 'shepherd-theme-dark', + scrollTo: true + } + });""" + sh_style_test = textwrap.dedent(sh_style_test) + Saved.sh_style_test = sh_style_test + return sh_style_test + + +def get_hops_backdrop_style(): + # Hopscotch Backdrop Style + if hasattr(Saved, "hops_backdrop_style"): + return Saved.hops_backdrop_style + hops_backdrop_style = """ + .hopscotch-bubble-container { + font-size: 110%; + }""" + hops_backdrop_style = textwrap.dedent(hops_backdrop_style) + Saved.hops_backdrop_style = hops_backdrop_style + return hops_backdrop_style + + +def get_introjs_style(): + # IntroJS Style + if hasattr(Saved, "introjs_style"): + return Saved.introjs_style + introjs_style = """ + .introjs-button.introjs-nextbutton, + .introjs-button.introjs-donebutton { + color: #fff !important; + background-color: %s !important; + border: 1px solid %s !important; + text-shadow: none; + box-shadow: none; + } + .introjs-button.introjs-nextbutton:hover, + .introjs-button.introjs-donebutton:hover { + color: #fff !important; + background-color: %s !important; + border: 1px solid %s !important; + } + .introjs-button { + box-sizing: content-box; + text-decoration: none; + } + .introjs-button.introjs-skipbutton { + color: %s; + } + .introjs-tooltip, .introjs-floating { + box-sizing: content-box; + position: absolute; + }""" + introjs_style = textwrap.dedent(introjs_style) + Saved.introjs_style = introjs_style + return introjs_style + + +def get_sh_backdrop_style(): + # Shepherd Backdrop Style + if hasattr(Saved, "sh_backdrop_style"): + return Saved.sh_backdrop_style + sh_backdrop_style = """ + body.shepherd-active .shepherd-target.shepherd-enabled { + box-shadow: 0 0 0 99999px rgba(0, 0, 0, 0.20); + pointer-events: none !important; + z-index: 9999; + } + body.shepherd-active .shepherd-orphan { + box-shadow: 0 0 0 99999px rgba(0, 0, 0, 0.20); + pointer-events: auto; + z-index: 9999; + } + body.shepherd-active + .shepherd-enabled.shepherd-element-attached-top { + position: relative; + } + body.shepherd-active + .shepherd-enabled.shepherd-element-attached-bottom { + position: relative; + } + body.shepherd-active .shepherd-step { + pointer-events: auto; + z-index: 9999; + } + body.shepherd-active { + pointer-events: none !important; + }""" + sh_backdrop_style = textwrap.dedent(sh_backdrop_style) + Saved.sh_backdrop_style = sh_backdrop_style + return sh_backdrop_style + + +def get_pytest_style(): + # pytest html-report Style + if hasattr(Saved, "pytest_style"): + return Saved.pytest_style + pytest_style = """ + body { + font-family: Helvetica, Arial, sans-serif; + font-size: 12px; + min-width: 800px; + color: #999; + } + h1 { + font-size: 24px; + color: black; + } + h2 { + font-size: 16px; + color: black; + } + p { + color: black; + } + a { + color: #999; + } + table { + border-collapse: collapse; + } + #environment td { + padding: 5px; + border: 1px solid #E6E6E6; + } + #environment tr:nth-child(odd) { + background-color: #f6f6f6; + } + span.passed, .passed .col-result { + color: green; + } + span.skipped, span.xfailed, span.rerun, .skipped .col-result, + .xfailed .col-result, .rerun .col-result { + color: orange; + } + span.error, span.failed, span.xpassed, .error .col-result, + .failed .col-result, .xpassed .col-result { + color: red; + } + #results-table { + border: 1px solid #e6e6e6; + color: #999; + font-size: 12px; + width: 100% + } + #results-table th, #results-table td { + padding: 5px; + border: 1px solid #E6E6E6; + text-align: left + } + #results-table th { + font-weight: bold + } + .log:only-child { + height: inherit + } + .log { + background-color: #e6e6e6; + border: 1px solid #e6e6e6; + color: black; + display: block; + font-family: "Courier New", Courier, monospace; + height: 230px; + overflow-y: scroll; + padding: 5px; + white-space: pre-wrap + } + div.image { + border: 1px solid #e6e6e6; + float: right; + height: 240px; + margin-left: 5px; + overflow: hidden; + width: 320px + } + div.image img { + width: 320px + } + .collapsed { + display: none; + } + .expander::after { + content: " (show details)"; + color: #BBB; + font-style: italic; + cursor: pointer; + } + .collapser::after { + content: " (hide details)"; + color: #BBB; + font-style: italic; + cursor: pointer; + } + .sortable { + cursor: pointer; + } + .sort-icon { + font-size: 0px; + float: left; + margin-right: 5px; + margin-top: 5px; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + } + .inactive .sort-icon { + border-top: 8px solid #E6E6E6; + } + .asc.active .sort-icon { + border-bottom: 8px solid #999; + } + .desc.active .sort-icon { + border-top: 8px solid #999; + }""" + pytest_style = textwrap.dedent(pytest_style) + Saved.pytest_style = pytest_style + return pytest_style diff --git a/seleniumbase/core/testcase_manager.py b/seleniumbase/core/testcase_manager.py old mode 100755 new mode 100644 index 0660bd9f3cd..c3ff0c9d1b6 --- a/seleniumbase/core/testcase_manager.py +++ b/seleniumbase/core/testcase_manager.py @@ -2,37 +2,39 @@ class TestcaseManager: - def __init__(self, database_env): self.database_env = database_env def insert_execution_data(self, execution_query_payload): - """ Inserts a test execution row into the database. - Returns the execution guid. - "execution_start_time" is defined by milliseconds since the Epoch. - (See https://currentmillis.com to convert that to a real date.) """ + """Inserts a test execution row into the database. + Returns the execution guid. + "execution_start_time" is defined by milliseconds since the Epoch. + (See https://currentmillis.com to convert that to a real date.)""" query = """INSERT INTO test_execution (guid, execution_start, total_execution_time, username) VALUES (%(guid)s,%(execution_start_time)s, %(total_execution_time)s,%(username)s)""" DatabaseManager(self.database_env).execute_query( - query, - execution_query_payload.get_params()) + query, execution_query_payload.get_params() + ) return execution_query_payload.guid def update_execution_data(self, execution_guid, execution_time): - """ Updates an existing test execution row in the database. """ + """Updates an existing test execution row in the database.""" query = """UPDATE test_execution SET total_execution_time=%(execution_time)s WHERE guid=%(execution_guid)s """ DatabaseManager(self.database_env).execute_query( query, - {"execution_guid": execution_guid, - "execution_time": execution_time}) + { + "execution_guid": execution_guid, + "execution_time": execution_time, + }, + ) def insert_testcase_data(self, testcase_run_payload): - """ Inserts all data for the test in the DB. Returns new row guid. """ + """Inserts all data for the test in the DB. Returns new row guid.""" query = """INSERT INTO test_run_data( guid, browser, state, execution_guid, env, start_time, test_address, runtime, retry_count, message, stack_trace) @@ -49,10 +51,11 @@ def insert_testcase_data(self, testcase_run_payload): %(message)s, %(stack_trace)s) """ DatabaseManager(self.database_env).execute_query( - query, testcase_run_payload.get_params()) + query, testcase_run_payload.get_params() + ) def update_testcase_data(self, testcase_payload): - """ Updates an existing test run in the database. """ + """Updates an existing test run in the database.""" query = """UPDATE test_run_data SET runtime=%(runtime)s, state=%(state)s, @@ -61,14 +64,16 @@ def update_testcase_data(self, testcase_payload): message=%(message)s WHERE guid=%(guid)s """ DatabaseManager(self.database_env).execute_query( - query, testcase_payload.get_params()) + query, testcase_payload.get_params() + ) def update_testcase_log_url(self, testcase_payload): query = """UPDATE test_run_data SET log_url=%(log_url)s WHERE guid=%(guid)s """ DatabaseManager(self.database_env).execute_query( - query, testcase_payload.get_params()) + query, testcase_payload.get_params() + ) class ExecutionQueryPayload: @@ -83,7 +88,7 @@ def get_params(self): "execution_start_time": self.execution_start_time, "total_execution_time": self.total_execution_time, "username": self.username, - "guid": self.guid + "guid": self.guid, } @@ -115,5 +120,5 @@ def get_params(self): "retry_count": self.retry_count, "stack_trace": self.stack_trace, "message": self.message, - "log_url": self.log_url + "log_url": self.log_url, } diff --git a/seleniumbase/core/tour_helper.py b/seleniumbase/core/tour_helper.py old mode 100755 new mode 100644 index e102688d29d..8f6b080cde1 --- a/seleniumbase/core/tour_helper.py +++ b/seleniumbase/core/tour_helper.py @@ -1,40 +1,31 @@ -""" -This module contains methods for running website tours. -These helper methods SHOULD NOT be called directly from tests. -""" +"""This module contains methods for running website tours. +These helper methods SHOULD NOT be called directly from tests.""" import os import re +import textwrap import time -from selenium.webdriver.common.by import By +from seleniumbase import config as sb_config from seleniumbase.config import settings from seleniumbase.core import style_sheet from seleniumbase.fixtures import constants from seleniumbase.fixtures import js_utils from seleniumbase.fixtures import page_actions -EXPORTED_TOURS_FOLDER = "tours_exported" - - -def raise_unable_to_load_jquery_exception(driver): - """ The most-likely reason for jQuery not loading on web pages. """ - raise Exception( - '''Unable to load jQuery on "%s" due to a possible violation ''' - '''of the website's Content Security Policy ''' - '''directive. ''' % driver.current_url) +EXPORTED_TOURS_FOLDER = constants.Tours.EXPORTED_TOURS_FOLDER def activate_bootstrap(driver): - """ Allows you to use Bootstrap Tours with SeleniumBase - http://bootstraptour.com/ + """Allows you to use Bootstrap Tours with SeleniumBase + http://bootstraptour.com/ """ bootstrap_tour_css = constants.BootstrapTour.MIN_CSS bootstrap_tour_js = constants.BootstrapTour.MIN_JS - verify_script = ("""// Verify Bootstrap Tour activated + verify_script = """// Verify Bootstrap Tour activated var tour2 = new Tour({ - });""") + });""" - backdrop_style = style_sheet.bt_backdrop_style + backdrop_style = style_sheet.get_bt_backdrop_style() js_utils.add_css_style(driver, backdrop_style) js_utils.wait_for_ready_state_complete(driver) js_utils.wait_for_angularjs(driver) @@ -51,13 +42,57 @@ def activate_bootstrap(driver): return except Exception: time.sleep(0.15) - raise_unable_to_load_jquery_exception(driver) + js_utils.raise_unable_to_load_jquery_exception(driver) def is_bootstrap_activated(driver): - verify_script = ("""// Verify Bootstrap Tour activated + verify_script = """// Verify Bootstrap Tour activated var tour2 = new Tour({ - });""") + });""" + try: + driver.execute_script(verify_script) + return True + except Exception: + return False + + +def activate_driverjs(driver): + """Allows you to use DriverJS Tours with SeleniumBase + https://kamranahmed.info/driver.js/ + """ + backdrop_style = style_sheet.get_dt_backdrop_style() + driverjs_css = constants.DriverJS.MIN_CSS + driverjs_js = constants.DriverJS.MIN_JS + + verify_script = """// Verify DriverJS activated + var driverjs2 = Driver.name; + """ + + js_utils.wait_for_ready_state_complete(driver) + js_utils.wait_for_angularjs(driver) + js_utils.add_css_style(driver, backdrop_style) + for x in range(4): + js_utils.activate_jquery(driver) + js_utils.add_css_link(driver, driverjs_css) + js_utils.add_js_link(driver, driverjs_js) + time.sleep(0.1) + for x in range(int(settings.MINI_TIMEOUT * 2.0)): + # DriverJS needs a small amount of time to load & activate. + try: + driver.execute_script(verify_script) + js_utils.wait_for_ready_state_complete(driver) + js_utils.wait_for_angularjs(driver) + time.sleep(0.05) + return + except Exception: + time.sleep(0.15) + js_utils.raise_unable_to_load_jquery_exception(driver) + + +def is_driverjs_activated(driver): + verify_script = """// Verify DriverJS activated + var driverjs2 = Driver.name; + """ try: driver.execute_script(verify_script) return True @@ -66,18 +101,17 @@ def is_bootstrap_activated(driver): def activate_hopscotch(driver): - """ Allows you to use Hopscotch Tours with SeleniumBase - http://linkedin.github.io/hopscotch/ + """Allows you to use Hopscotch Tours with SeleniumBase + http://linkedin.github.io/hopscotch/ """ hopscotch_css = constants.Hopscotch.MIN_CSS hopscotch_js = constants.Hopscotch.MIN_JS - backdrop_style = style_sheet.hops_backdrop_style + backdrop_style = style_sheet.get_hops_backdrop_style() - verify_script = ("""// Verify Hopscotch activated + verify_script = """// Verify Hopscotch activated var hops = hopscotch.isActive; - """) + """ - activate_bootstrap(driver) js_utils.wait_for_ready_state_complete(driver) js_utils.wait_for_angularjs(driver) js_utils.add_css_style(driver, backdrop_style) @@ -96,13 +130,13 @@ def activate_hopscotch(driver): return except Exception: time.sleep(0.15) - raise_unable_to_load_jquery_exception(driver) + js_utils.raise_unable_to_load_jquery_exception(driver) def is_hopscotch_activated(driver): - verify_script = ("""// Verify Hopscotch activated + verify_script = """// Verify Hopscotch activated var hops = hopscotch.isActive; - """) + """ try: driver.execute_script(verify_script) return True @@ -111,19 +145,29 @@ def is_hopscotch_activated(driver): def activate_introjs(driver): - """ Allows you to use IntroJS Tours with SeleniumBase - https://introjs.com/ + """Allows you to use IntroJS Tours with SeleniumBase + https://introjs.com/ """ intro_css = constants.IntroJS.MIN_CSS intro_js = constants.IntroJS.MIN_JS - verify_script = ("""// Verify IntroJS activated + theme_color = sb_config.introjs_theme_color + hover_color = sb_config.introjs_hover_color + backdrop_style = style_sheet.get_introjs_style() % ( + theme_color, + hover_color, + hover_color, + hover_color, + theme_color, + ) + + verify_script = """// Verify IntroJS activated var intro2 = introJs(); - """) + """ - activate_bootstrap(driver) js_utils.wait_for_ready_state_complete(driver) js_utils.wait_for_angularjs(driver) + js_utils.add_css_style(driver, backdrop_style) for x in range(4): js_utils.activate_jquery(driver) js_utils.add_css_link(driver, intro_css) @@ -139,13 +183,13 @@ def activate_introjs(driver): return except Exception: time.sleep(0.15) - raise_unable_to_load_jquery_exception(driver) + js_utils.raise_unable_to_load_jquery_exception(driver) def is_introjs_activated(driver): - verify_script = ("""// Verify IntroJS activated + verify_script = """// Verify IntroJS activated var intro2 = introJs(); - """) + """ try: driver.execute_script(verify_script) return True @@ -154,8 +198,8 @@ def is_introjs_activated(driver): def activate_shepherd(driver): - """ Allows you to use Shepherd Tours with SeleniumBase - http://github.hubspot.com/shepherd/docs/welcome/ + """Allows you to use Shepherd Tours with SeleniumBase + https://cdnjs.com/libraries/shepherd/1.8.1 """ shepherd_js = constants.Shepherd.MIN_JS sh_theme_arrows_css = constants.Shepherd.THEME_ARROWS_CSS @@ -166,10 +210,9 @@ def activate_shepherd(driver): sh_theme_sq_dark_css = constants.Shepherd.THEME_SQ_DK_CSS tether_js = constants.Tether.MIN_JS spinner_css = constants.Messenger.SPINNER_CSS - sh_style = style_sheet.sh_style_test - backdrop_style = style_sheet.sh_backdrop_style + sh_style = style_sheet.get_sh_style_test() + backdrop_style = style_sheet.get_sh_backdrop_style() - activate_bootstrap(driver) js_utils.wait_for_ready_state_complete(driver) js_utils.wait_for_angularjs(driver) js_utils.add_css_style(driver, backdrop_style) @@ -199,11 +242,11 @@ def activate_shepherd(driver): return except Exception: time.sleep(0.15) - raise_unable_to_load_jquery_exception(driver) + js_utils.raise_unable_to_load_jquery_exception(driver) def is_shepherd_activated(driver): - sh_style = style_sheet.sh_style_test + sh_style = style_sheet.get_sh_style_test() try: driver.execute_script(sh_style) # Verify Shepherd has loaded return True @@ -212,14 +255,21 @@ def is_shepherd_activated(driver): def play_shepherd_tour(driver, tour_steps, msg_dur, name=None, interval=0): - """ Plays a Shepherd tour on the current website. """ + """Plays a Shepherd tour on the current website.""" instructions = "" for tour_step in tour_steps[name]: instructions += tour_step - instructions += (""" + instructions += """ // Start the tour tour.start(); - $tour = tour;""") + $tour = tour;""" + extra = """ + document.body.addEventListener('keyup', function (event) { + if (event.key === 'PageUp' || event.key === 'ArrowLeft') { + Shepherd.activeTour.back(); } + if (event.key === 'PageDown' || event.key === 'ArrowRight') { + Shepherd.activeTour.next(); } + })""" autoplay = False if interval and interval > 0: autoplay = True @@ -228,26 +278,42 @@ def play_shepherd_tour(driver, tour_steps, msg_dur, name=None, interval=0): interval = 0.5 if not is_shepherd_activated(driver): + instructions += extra activate_shepherd(driver) if len(tour_steps[name]) > 1: try: selector = re.search( r"[\S\s]+{element: '([\S\s]+)', on: [\S\s]+", - tour_steps[name][1]).group(1) - selector = selector.replace('\\', '') + tour_steps[name][1], + ).group(1) + selector = selector.replace("\\", "") page_actions.wait_for_element_present( - driver, selector, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT) + driver, + selector, + by="css selector", + timeout=settings.SMALL_TIMEOUT, + ) except Exception: js_utils.post_messenger_error_message( - driver, "Tour Error: {'%s'} was not found!" % selector, - msg_dur) + driver, "Tour Error: {'%s'} was not found!" % selector, msg_dur + ) raise Exception( "Tour Error: {'%s'} was not found! " "Exiting due to failure on first tour step!" - "" % selector) + "" % selector + ) driver.execute_script(instructions) + try: + page_actions.wait_for_element_visible( + driver, "a.tour-button-right", by="css selector", timeout=1.2 + ) + except Exception: + pass + try: + driver.execute_script('document.activeElement.blur();') + except Exception: + pass tour_on = True if autoplay: start_ms = time.time() * 1000.0 @@ -258,7 +324,8 @@ def play_shepherd_tour(driver, tour_steps, msg_dur, name=None, interval=0): try: time.sleep(0.01) result = driver.execute_script( - "return Shepherd.activeTour.currentStep.isOpen()") + "return Shepherd.activeTour.currentStep.isOpen()" + ) except Exception: tour_on = False result = None @@ -268,10 +335,12 @@ def play_shepherd_tour(driver, tour_steps, msg_dur, name=None, interval=0): try: element = driver.execute_script( "return Shepherd.activeTour.currentStep" - ".options.attachTo.element") + ".options.attachTo.element" + ) shep_text = driver.execute_script( "return Shepherd.activeTour.currentStep" - ".options.text") + ".options.text" + ) except Exception: continue if element != latest_element or shep_text != latest_text: @@ -281,16 +350,19 @@ def play_shepherd_tour(driver, tour_steps, msg_dur, name=None, interval=0): stop_ms = start_ms + (interval * 1000.0) now_ms = time.time() * 1000.0 if now_ms >= stop_ms: - if ((element == latest_element) and ( - shep_text == latest_text)): + if (element == latest_element) and ( + shep_text == latest_text + ): driver.execute_script("Shepherd.activeTour.next()") try: latest_element = driver.execute_script( "return Shepherd.activeTour.currentStep" - ".options.attachTo.element") + ".options.attachTo.element" + ) latest_text = driver.execute_script( "return Shepherd.activeTour.currentStep" - ".options.text") + ".options.text" + ) start_ms = time.time() * 1000.0 stop_ms = start_ms + (interval * 1000.0) except Exception: @@ -301,17 +373,22 @@ def play_shepherd_tour(driver, tour_steps, msg_dur, name=None, interval=0): time.sleep(0.01) selector = driver.execute_script( "return Shepherd.activeTour" - ".currentStep.options.attachTo.element") + ".currentStep.options.attachTo.element" + ) try: js_utils.wait_for_css_query_selector( - driver, selector, timeout=settings.SMALL_TIMEOUT) + driver, selector, timeout=settings.SMALL_TIMEOUT + ) except Exception: remove_script = ( - "jQuery('%s').remove()" % "div.shepherd-content") + "jQuery('%s').remove()" % "div.shepherd-content" + ) driver.execute_script(remove_script) js_utils.post_messenger_error_message( - driver, "Tour Error: {'%s'} was not found!" % selector, - msg_dur) + driver, + "Tour Error: {'%s'} was not found!" % selector, + msg_dur, + ) time.sleep(0.1) driver.execute_script("Shepherd.activeTour.next()") if autoplay: @@ -324,13 +401,13 @@ def play_shepherd_tour(driver, tour_steps, msg_dur, name=None, interval=0): def play_bootstrap_tour( - driver, tour_steps, browser, msg_dur, name=None, interval=0): - """ Plays a Bootstrap tour on the current website. """ + driver, tour_steps, browser, msg_dur, name=None, interval=0 +): + """Plays a Bootstrap tour on the current website.""" instructions = "" for tour_step in tour_steps[name]: instructions += tour_step - instructions += ( - """]); + instructions += """]); // Initialize the tour tour.init(); // Start the tour @@ -338,65 +415,211 @@ def play_bootstrap_tour( // Fix timing issue by restarting tour immediately tour.restart(); // Save for later - $tour = tour;""") - + $tour = tour;""" if interval and interval > 0: if interval < 1: interval = 1 interval = str(float(interval) * 1000.0) instructions = instructions.replace( - 'duration: 0,', 'duration: %s,' % interval) - + "duration: 0,", "duration: %s," % interval + ) if not is_bootstrap_activated(driver): activate_bootstrap(driver) - if len(tour_steps[name]) > 1: try: if "element: " in tour_steps[name][1]: selector = re.search( r"[\S\s]+element: '([\S\s]+)',[\S\s]+title: '", - tour_steps[name][1]).group(1) - selector = selector.replace('\\', '').replace(':first', '') + tour_steps[name][1], + ).group(1) + selector = selector.replace("\\", "").replace(":first", "") page_actions.wait_for_element_present( - driver, selector, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT) + driver, + selector, + by="css selector", + timeout=settings.SMALL_TIMEOUT, + ) else: selector = "html" except Exception: js_utils.post_messenger_error_message( - driver, "Tour Error: {'%s'} was not found!" % selector, - msg_dur) + driver, "Tour Error: {'%s'} was not found!" % selector, msg_dur + ) raise Exception( "Tour Error: {'%s'} was not found! " "Exiting due to failure on first tour step!" - "" % selector) - + "" % selector + ) driver.execute_script(instructions) tour_on = True + try: + page_actions.wait_for_element_visible( + driver, ".tour-tour", by="css selector", timeout=1.2 + ) + except Exception: + pass + try: + driver.execute_script('document.activeElement.blur();') + except Exception: + pass while tour_on: try: time.sleep(0.01) if browser != "firefox": - result = driver.execute_script( - "return $tour.ended()") + result = driver.execute_script("return $tour.ended()") else: page_actions.wait_for_element_present( - driver, ".tour-tour", by=By.CSS_SELECTOR, timeout=0.4) + driver, ".tour-tour", by="css selector", timeout=0.48 + ) result = False except Exception: tour_on = False result = None if result is False: tour_on = True + time.sleep(0.05) else: try: time.sleep(0.01) if browser != "firefox": - result = driver.execute_script( - "return $tour.ended()") + result = driver.execute_script("return $tour.ended()") else: page_actions.wait_for_element_present( - driver, ".tour-tour", by=By.CSS_SELECTOR, timeout=0.4) + driver, ".tour-tour", by="css selector", timeout=0.48 + ) + result = False + if result is False: + time.sleep(0.05) + continue + else: + return + except Exception: + tour_on = False + time.sleep(0.1) + + +def play_driverjs_tour( + driver, tour_steps, browser, msg_dur, name=None, interval=0 +): + """Plays a DriverJS tour on the current website.""" + instructions = "" + for tour_step in tour_steps[name]: + instructions += tour_step + instructions += """] + ); + // Start the tour! + tour.start(); + $tour = tour;""" + extra = """ + document.body.addEventListener('keyup', function (event) { + if (event.key === 'PageUp') { $tour.movePrevious(); } + if (event.key === 'PageDown') { $tour.moveNext(); } + })""" + autoplay = False + if interval and interval > 0: + autoplay = True + interval = float(interval) + if interval < 0.5: + interval = 0.5 + + if not is_driverjs_activated(driver): + instructions += extra + activate_driverjs(driver) + + if len(tour_steps[name]) > 1: + try: + if "element: " in tour_steps[name][1]: + selector = re.search( + r"[\S\s]+element: '([\S\s]+)',[\S\s]+popover: {", + tour_steps[name][1], + ).group(1) + selector = selector.replace("\\", "").replace(":first", "") + page_actions.wait_for_element_present( + driver, + selector, + by="css selector", + timeout=settings.SMALL_TIMEOUT, + ) + else: + selector = "html" + except Exception: + js_utils.post_messenger_error_message( + driver, "Tour Error: {'%s'} was not found!" % selector, msg_dur + ) + raise Exception( + "Tour Error: {'%s'} was not found! " + "Exiting due to failure on first tour step!" + "" % selector + ) + + driver.execute_script(instructions) + driver.execute_script( + 'document.querySelector(".driver-next-btn").focus();' + ) + tour_on = True + if autoplay: + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (interval * 1000.0) + latest_step = 0 + while tour_on: + try: + time.sleep(0.01) + if browser != "firefox": + result = not driver.execute_script("return $tour.isActivated") + else: + page_actions.wait_for_element_visible( + driver, + "#driver-popover-item", + by="css selector", + timeout=1.1, + ) + result = False + except Exception: + tour_on = False + result = None + if result is False: + tour_on = True + driver.execute_script( + 'document.querySelector(".driver-next-btn").focus();' + ) + if autoplay: + try: + current_step = driver.execute_script( + "return $tour.currentStep" + ) + except Exception: + continue + if current_step != latest_step: + latest_step = current_step + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (interval * 1000.0) + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + if current_step == latest_step: + driver.execute_script("$tour.moveNext()") + try: + latest_step = driver.execute_script( + "return $tour.currentStep" + ) + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (interval * 1000.0) + except Exception: + pass + continue + else: + try: + time.sleep(0.01) + if browser != "firefox": + result = not driver.execute_script( + "return $tour.isActivated" + ) + else: + page_actions.wait_for_element_visible( + driver, + "#driver-popover-item", + by="css selector", + timeout=1.1, + ) result = False if result is False: time.sleep(0.1) @@ -409,17 +632,24 @@ def play_bootstrap_tour( def play_hopscotch_tour( - driver, tour_steps, browser, msg_dur, name=None, interval=0): - """ Plays a Hopscotch tour on the current website. """ + driver, tour_steps, browser, msg_dur, name=None, interval=0 +): + """Plays a Hopscotch tour on the current website.""" instructions = "" for tour_step in tour_steps[name]: instructions += tour_step - instructions += ( - """] + instructions += """] }; // Start the tour! hopscotch.startTour(tour); - $tour = hopscotch;""") + $tour = hopscotch;""" + extra = """ + document.body.addEventListener('keyup', function (event) { + if (event.key === 'PageUp' || event.key === 'ArrowLeft') { + $tour.prevStep(); } + if (event.key === 'PageDown' || event.key === 'ArrowRight') { + $tour.nextStep(); } + })""" autoplay = False if interval and interval > 0: autoplay = True @@ -428,6 +658,7 @@ def play_hopscotch_tour( interval = 0.5 if not is_hopscotch_activated(driver): + instructions += extra activate_hopscotch(driver) if len(tour_steps[name]) > 1: @@ -435,23 +666,38 @@ def play_hopscotch_tour( if "target: " in tour_steps[name][1]: selector = re.search( r"[\S\s]+target: '([\S\s]+)',[\S\s]+title: '", - tour_steps[name][1]).group(1) - selector = selector.replace('\\', '').replace(':first', '') + tour_steps[name][1], + ).group(1) + selector = selector.replace("\\", "").replace(":first", "") page_actions.wait_for_element_present( - driver, selector, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT) + driver, + selector, + by="css selector", + timeout=settings.SMALL_TIMEOUT, + ) else: selector = "html" except Exception: js_utils.post_messenger_error_message( - driver, "Tour Error: {'%s'} was not found!" % selector, - msg_dur) + driver, "Tour Error: {'%s'} was not found!" % selector, msg_dur + ) raise Exception( "Tour Error: {'%s'} was not found! " "Exiting due to failure on first tour step!" - "" % selector) + "" % selector + ) driver.execute_script(instructions) + try: + page_actions.wait_for_element_visible( + driver, "button.hopscotch-next", by="css selector", timeout=1.2 + ) + except Exception: + pass + try: + driver.execute_script('document.activeElement.blur();') + except Exception: + pass tour_on = True if autoplay: start_ms = time.time() * 1000.0 @@ -461,12 +707,14 @@ def play_hopscotch_tour( try: time.sleep(0.01) if browser != "firefox": - result = not driver.execute_script( - "return $tour.isActive") + result = not driver.execute_script("return $tour.isActive") else: page_actions.wait_for_element_present( - driver, ".hopscotch-bubble", - by=By.CSS_SELECTOR, timeout=0.4) + driver, + ".hopscotch-bubble", + by="css selector", + timeout=0.4, + ) result = False except Exception: tour_on = False @@ -476,7 +724,8 @@ def play_hopscotch_tour( if autoplay: try: current_step = driver.execute_script( - "return $tour.getCurrStepNum()") + "return $tour.getCurrStepNum()" + ) except Exception: continue if current_step != latest_step: @@ -486,10 +735,11 @@ def play_hopscotch_tour( now_ms = time.time() * 1000.0 if now_ms >= stop_ms: if current_step == latest_step: - driver.execute_script("return $tour.nextStep()") + driver.execute_script("$tour.nextStep()") try: latest_step = driver.execute_script( - "return $tour.getCurrStepNum()") + "return $tour.getCurrStepNum()" + ) start_ms = time.time() * 1000.0 stop_ms = start_ms + (interval * 1000.0) except Exception: @@ -499,12 +749,14 @@ def play_hopscotch_tour( try: time.sleep(0.01) if browser != "firefox": - result = not driver.execute_script( - "return $tour.isActive") + result = not driver.execute_script("return $tour.isActive") else: page_actions.wait_for_element_present( - driver, ".hopscotch-bubble", - by=By.CSS_SELECTOR, timeout=0.4) + driver, + ".hopscotch-bubble", + by="css selector", + timeout=0.4, + ) result = False if result is False: time.sleep(0.1) @@ -517,19 +769,21 @@ def play_hopscotch_tour( def play_introjs_tour( - driver, tour_steps, browser, msg_dur, name=None, interval=0): - """ Plays an IntroJS tour on the current website. """ + driver, tour_steps, browser, msg_dur, name=None, interval=0 +): + """Plays an IntroJS tour on the current website.""" instructions = "" for tour_step in tour_steps[name]: instructions += tour_step - instructions += ( - """] + instructions += """] }); intro.setOption("disableInteraction", true); intro.setOption("overlayOpacity", .29); intro.setOption("scrollToElement", true); intro.setOption("keyboardNavigation", true); - intro.setOption("exitOnEsc", false); + intro.setOption("exitOnEsc", true); + intro.setOption("hidePrev", true); + intro.setOption("nextToDone", true); intro.setOption("exitOnOverlayClick", false); intro.setOption("showStepNumbers", false); intro.setOption("showProgress", false); @@ -538,7 +792,12 @@ def play_introjs_tour( }; // Start the tour startIntro(); - """) + """ + extra = """ + document.body.addEventListener('keyup', function (event) { + if (event.key === 'PageUp') { $tour.previousStep(); } + if (event.key === 'PageDown') { $tour.nextStep(); } + })""" autoplay = False if interval and interval > 0: autoplay = True @@ -547,6 +806,7 @@ def play_introjs_tour( interval = 0.5 if not is_introjs_activated(driver): + instructions += extra activate_introjs(driver) if len(tour_steps[name]) > 1: @@ -554,21 +814,26 @@ def play_introjs_tour( if "element: " in tour_steps[name][1]: selector = re.search( r"[\S\s]+element: '([\S\s]+)',[\S\s]+intro: '", - tour_steps[name][1]).group(1) - selector = selector.replace('\\', '') + tour_steps[name][1], + ).group(1) + selector = selector.replace("\\", "") page_actions.wait_for_element_present( - driver, selector, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT) + driver, + selector, + by="css selector", + timeout=settings.SMALL_TIMEOUT, + ) else: selector = "html" except Exception: js_utils.post_messenger_error_message( - driver, "Tour Error: {'%s'} was not found!" % selector, - msg_dur) + driver, "Tour Error: {'%s'} was not found!" % selector, msg_dur + ) raise Exception( "Tour Error: {'%s'} was not found! " "Exiting due to failure on first tour step!" - "" % selector) + "" % selector + ) driver.execute_script(instructions) tour_on = True if autoplay: @@ -579,12 +844,11 @@ def play_introjs_tour( try: time.sleep(0.01) if browser != "firefox": - result = driver.execute_script( - "return $tour._currentStep") + result = driver.execute_script("return $tour._currentStep") else: page_actions.wait_for_element_present( - driver, ".introjs-tooltip", - by=By.CSS_SELECTOR, timeout=0.4) + driver, ".introjs-tooltip", by="css selector", timeout=0.4 + ) result = True except Exception: tour_on = False @@ -594,7 +858,8 @@ def play_introjs_tour( if autoplay: try: current_step = driver.execute_script( - "return $tour._currentStep") + "return $tour._currentStep" + ) except Exception: continue if current_step != latest_step: @@ -604,10 +869,14 @@ def play_introjs_tour( now_ms = time.time() * 1000.0 if now_ms >= stop_ms: if current_step == latest_step: - driver.execute_script("return $tour.nextStep()") + try: + driver.execute_script("$tour.nextStep()") + except Exception: + driver.execute_script("$tour.exit()") try: latest_step = driver.execute_script( - "return $tour._currentStep") + "return $tour._currentStep" + ) start_ms = time.time() * 1000.0 stop_ms = start_ms + (interval * 1000.0) except Exception: @@ -617,12 +886,14 @@ def play_introjs_tour( try: time.sleep(0.01) if browser != "firefox": - result = driver.execute_script( - "return $tour._currentStep") + result = driver.execute_script("return $tour._currentStep") else: page_actions.wait_for_element_present( - driver, ".introjs-tooltip", - by=By.CSS_SELECTOR, timeout=0.4) + driver, + ".introjs-tooltip", + by="css selector", + timeout=0.4, + ) result = True if result is not None: time.sleep(0.1) @@ -635,15 +906,15 @@ def play_introjs_tour( def export_tour(tour_steps, name=None, filename="my_tour.js", url=None): - """ Exports a tour as a JS file. - It will include necessary resources as well, such as jQuery. - You'll be able to copy the tour directly into the Console of - any web browser to play the tour outside of SeleniumBase runs. """ + """Exports a tour as a JS file. + It will include necessary resources as well, such as jQuery. + You'll be able to copy the tour directly into the Console of + any web browser to play the tour outside of SeleniumBase runs.""" if not name: name = "default" if name not in tour_steps: raise Exception("Tour {%s} does not exist!" % name) - if not filename.endswith('.js'): + if not filename.endswith(".js"): raise Exception('Tour file must end in ".js"!') if not url: url = "data:," @@ -651,6 +922,8 @@ def export_tour(tour_steps, name=None, filename="my_tour.js", url=None): tour_type = None if "Bootstrap" in tour_steps[name][0]: tour_type = "bootstrap" + elif "DriverJS" in tour_steps[name][0]: + tour_type = "driverjs" elif "Hopscotch" in tour_steps[name][0]: tour_type = "hopscotch" elif "IntroJS" in tour_steps[name][0]: @@ -658,59 +931,75 @@ def export_tour(tour_steps, name=None, filename="my_tour.js", url=None): elif "Shepherd" in tour_steps[name][0]: tour_type = "shepherd" else: - raise Exception('Unknown tour type!') + raise Exception("Unknown tour type!") instructions = ( - '''//////// Load Tour Start Page (if not there now) ////////\n\n''' - '''if (window.location.href != "%s") {\n''' - ''' window.location.href="%s";\n''' - '''}\n\n''' - '''//////// Resources ////////\n\n''' - '''function injectCSS(css_link) {''' - '''var head = document.getElementsByTagName("head")[0];''' - '''var link = document.createElement("link");''' - '''link.rel = "stylesheet";''' - '''link.type = "text/css";''' - '''link.href = css_link;''' - '''link.crossorigin = "anonymous";''' - '''head.appendChild(link);''' - '''};\n''' - '''function injectJS(js_link) {''' - '''var head = document.getElementsByTagName("head")[0];''' - '''var script = document.createElement("script");''' - '''script.src = js_link;''' - '''script.defer;''' - '''script.type="text/javascript";''' - '''script.crossorigin = "anonymous";''' - '''script.onload = function() { null };''' - '''head.appendChild(script);''' - '''};\n''' - '''function injectStyle(css) {''' - '''var head = document.getElementsByTagName("head")[0];''' - '''var style = document.createElement("style");''' - '''style.type = "text/css";''' - '''style.appendChild(document.createTextNode(css));''' - '''head.appendChild(style);''' - '''};\n''' % (url, url)) + """//////// Load Tour Start Page (if not there now) ////////\n\n""" + """if (window.location.href != "%s") {\n""" + """ window.location.href="%s";\n""" + """}\n\n""" + """//////// Resources ////////\n\n""" + """function injectCSS(css_link) {""" + """var head = document.getElementsByTagName("head")[0];""" + """var link = document.createElement("link");""" + """link.rel = "stylesheet";""" + """link.type = "text/css";""" + """link.href = css_link;""" + """link.crossorigin = "anonymous";""" + """head.appendChild(link);""" + """};\n""" + """function injectJS(js_link) {""" + """var head = document.getElementsByTagName("head")[0];""" + """var script = document.createElement("script");""" + """script.src = js_link;""" + """script.defer;""" + """script.type="text/javascript";""" + """script.crossorigin = "anonymous";""" + """script.onload = function() { null };""" + """head.appendChild(script);""" + """};\n""" + """function injectStyle(css) {""" + """var head = document.getElementsByTagName("head")[0];""" + """var style = document.createElement("style");""" + """style.type = "text/css";""" + """style.appendChild(document.createTextNode(css));""" + """head.appendChild(style);""" + """};\n""" % (url, url) + ) if tour_type == "bootstrap": jquery_js = constants.JQuery.MIN_JS bootstrap_tour_css = constants.BootstrapTour.MIN_CSS bootstrap_tour_js = constants.BootstrapTour.MIN_JS - backdrop_style = style_sheet.bt_backdrop_style - backdrop_style = backdrop_style.replace('\n', '') + backdrop_style = style_sheet.get_bt_backdrop_style() + backdrop_style = backdrop_style.replace("\n", "") backdrop_style = js_utils.escape_quotes_if_needed(backdrop_style) - instructions += 'injectJS("%s");' % jquery_js - instructions += '\n\n//////// Resources - Load 2 ////////\n\n' + instructions += 'injectJS("%s");\n' % jquery_js + instructions += "\n" + instructions += "function loadResources() { " + instructions += 'if ( typeof jQuery !== "undefined" ) {\n' instructions += 'injectCSS("%s");\n' % bootstrap_tour_css instructions += 'injectStyle("%s");\n' % backdrop_style instructions += 'injectJS("%s");' % bootstrap_tour_js + instructions += '} else { window.setTimeout("loadResources();",100); ' + instructions += "} }\n" + instructions += "loadResources()" + + elif tour_type == "driverjs": + driverjs_css = constants.DriverJS.MIN_CSS + driverjs_js = constants.DriverJS.MIN_JS + backdrop_style = style_sheet.get_dt_backdrop_style() + backdrop_style = backdrop_style.replace("\n", "") + backdrop_style = js_utils.escape_quotes_if_needed(backdrop_style) + instructions += 'injectCSS("%s");\n' % driverjs_css + instructions += 'injectStyle("%s");\n' % backdrop_style + instructions += 'injectJS("%s");' % driverjs_js elif tour_type == "hopscotch": hopscotch_css = constants.Hopscotch.MIN_CSS hopscotch_js = constants.Hopscotch.MIN_JS - backdrop_style = style_sheet.hops_backdrop_style - backdrop_style = backdrop_style.replace('\n', '') + backdrop_style = style_sheet.get_hops_backdrop_style() + backdrop_style = backdrop_style.replace("\n", "") backdrop_style = js_utils.escape_quotes_if_needed(backdrop_style) instructions += 'injectCSS("%s");\n' % hopscotch_css instructions += 'injectStyle("%s");\n' % backdrop_style @@ -719,7 +1008,19 @@ def export_tour(tour_steps, name=None, filename="my_tour.js", url=None): elif tour_type == "introjs": intro_css = constants.IntroJS.MIN_CSS intro_js = constants.IntroJS.MIN_JS + theme_color = sb_config.introjs_theme_color + hover_color = sb_config.introjs_hover_color + backdrop_style = style_sheet.get_introjs_style() % ( + theme_color, + hover_color, + hover_color, + hover_color, + theme_color, + ) + backdrop_style = backdrop_style.replace("\n", "") + backdrop_style = js_utils.escape_quotes_if_needed(backdrop_style) instructions += 'injectCSS("%s");\n' % intro_css + instructions += 'injectStyle("%s");\n' % backdrop_style instructions += 'injectJS("%s");' % intro_js elif tour_type == "shepherd": @@ -733,13 +1034,15 @@ def export_tour(tour_steps, name=None, filename="my_tour.js", url=None): sh_theme_sq_dark_css = constants.Shepherd.THEME_SQ_DK_CSS tether_js = constants.Tether.MIN_JS spinner_css = constants.Messenger.SPINNER_CSS - backdrop_style = style_sheet.sh_backdrop_style - backdrop_style = backdrop_style.replace('\n', '') + backdrop_style = style_sheet.get_sh_backdrop_style() + backdrop_style = backdrop_style.replace("\n", "") backdrop_style = js_utils.escape_quotes_if_needed(backdrop_style) instructions += 'injectCSS("%s");\n' % spinner_css instructions += 'injectJS("%s");\n' % jquery_js - instructions += 'injectJS("%s");' % tether_js - instructions += '\n\n//////// Resources - Load 2 ////////\n\n' + instructions += 'injectJS("%s");\n' % tether_js + instructions += "\n" + instructions += "function loadResources() { " + instructions += 'if ( typeof jQuery !== "undefined" ) {\n' instructions += 'injectCSS("%s");' % sh_theme_arrows_css instructions += 'injectCSS("%s");' % sh_theme_arrows_fix_css instructions += 'injectCSS("%s");' % sh_theme_default_css @@ -747,51 +1050,81 @@ def export_tour(tour_steps, name=None, filename="my_tour.js", url=None): instructions += 'injectCSS("%s");' % sh_theme_sq_css instructions += 'injectCSS("%s");\n' % sh_theme_sq_dark_css instructions += 'injectStyle("%s");\n' % backdrop_style - instructions += 'injectJS("%s");' % shepherd_js + instructions += 'injectJS("%s");\n' % shepherd_js + instructions += '} else { window.setTimeout("loadResources();",100); ' + instructions += "} }\n" + instructions += "loadResources()" + + instructions += "\n\n//////// Tour Code ////////\n\n" + first_instructions = instructions + instructions = " " + if tour_type == "bootstrap": + instructions += "function loadTour() { " + instructions += 'if ( typeof Tour !== "undefined" ) {\n' + elif tour_type == "driverjs": + instructions += "function loadTour() { " + instructions += 'if ( typeof Driver !== "undefined" ) {\n' + elif tour_type == "hopscotch": + instructions += "function loadTour() { " + instructions += 'if ( typeof hopscotch !== "undefined" ) {\n' + elif tour_type == "introjs": + instructions += "function loadTour() { " + instructions += 'if ( typeof introJs !== "undefined" ) {\n' + elif tour_type == "shepherd": + instructions += "function loadTour() { " + instructions += 'if ( typeof Shepherd !== "undefined" ) {\n' - instructions += '\n\n//////// Tour Code ////////\n\n' for tour_step in tour_steps[name]: instructions += tour_step if tour_type == "bootstrap": - instructions += ( - """]); + instructions += """]); // Initialize the tour tour.init(); // Start the tour tour.start(); $tour = tour; - $tour.restart();\n""") + $tour.restart();\n""" + elif tour_type == "driverjs": + instructions += """] + ); + // Start the tour! + tour.start(); + $tour = tour;\n""" elif tour_type == "hopscotch": - instructions += ( - """] + instructions += """] }; // Start the tour! hopscotch.startTour(tour); - $tour = hopscotch;\n""") + $tour = hopscotch;\n""" elif tour_type == "introjs": - instructions += ( - """] + instructions += """] }); intro.setOption("disableInteraction", true); intro.setOption("overlayOpacity", .29); intro.setOption("scrollToElement", true); intro.setOption("keyboardNavigation", true); - intro.setOption("exitOnEsc", false); + intro.setOption("exitOnEsc", true); + intro.setOption("hidePrev", true); + intro.setOption("nextToDone", true); intro.setOption("exitOnOverlayClick", false); intro.setOption("showStepNumbers", false); intro.setOption("showProgress", false); intro.start(); $tour = intro; }; - startIntro();\n""") + startIntro();\n""" elif tour_type == "shepherd": - instructions += ( - """ + instructions += """ tour.start(); - $tour = tour;\n""") + $tour = tour;\n""" else: pass + instructions = textwrap.dedent(instructions) + instructions = first_instructions + instructions + instructions += '\n} else { window.setTimeout("loadTour();",100); } ' + instructions += "}\n" + instructions += "loadTour()\n" exported_tours_folder = EXPORTED_TOURS_FOLDER if exported_tours_folder.endswith("/"): @@ -801,9 +1134,9 @@ def export_tour(tour_steps, name=None, filename="my_tour.js", url=None): os.makedirs(exported_tours_folder) except Exception: pass - import codecs + file_path = exported_tours_folder + "/" + filename - out_file = codecs.open(file_path, "w+") + out_file = open(file_path, mode="w+", encoding="utf-8") out_file.writelines(instructions) out_file.close() - print('\n>>> [%s] was saved!\n' % file_path) + print("\n>>> [%s] was saved!\n" % file_path) diff --git a/seleniumbase/core/visual_helper.py b/seleniumbase/core/visual_helper.py old mode 100755 new mode 100644 index 21cc4ec4d2f..c0133d89d2b --- a/seleniumbase/core/visual_helper.py +++ b/seleniumbase/core/visual_helper.py @@ -1,8 +1,9 @@ import os +from seleniumbase.core import log_helper from seleniumbase.fixtures import constants VISUAL_BASELINE_DIR = constants.VisualBaseline.STORAGE_FOLDER -abs_path = os.path.abspath('.') +abs_path = os.path.abspath(".") visual_baseline_path = os.path.join(abs_path, VISUAL_BASELINE_DIR) @@ -11,9 +12,99 @@ def get_visual_baseline_folder(): def visual_baseline_folder_setup(): - """ Handle Logging """ + """Handle Logging""" if not os.path.exists(visual_baseline_path): try: os.makedirs(visual_baseline_path) except Exception: pass # Should only be reachable during multi-threaded runs + + +def get_sbs_head(): + # Uses caching to prevent extra method calls + SIDE_BY_SIDE_PNG = constants.SideBySide.get_favicon() + head = ( + '' + '' + '' + "Visual Comparison" + "" % (SIDE_BY_SIDE_PNG) + ) + return head + + +def get_sbs_table_row(baseline="baseline.png", diff="baseline_diff.png"): + row = ( + '' + '' + '' + '' + "" + "" % (baseline, diff) + ) + return row + + +def get_sbs_table_html(baseline="baseline.png", diff="baseline_diff.png"): + table_html = ( + '' + '' + "" + '' + '' + "" + ) + row = get_sbs_table_row(baseline, diff) + table_html += row + table_html += "
      Baseline ScreenshotVisual Diff Failure Screenshot
      " + return table_html + + +def get_sbs_gen_by(): + gen_by = ( + '

      Generated by: ' + "SeleniumBase

      " + ) + return gen_by + + +def get_sbs_header_text(): + header_text = "SeleniumBase Visual Comparison" + return header_text + + +def get_sbs_header(): + header_text = get_sbs_header_text() + header = '

      %s

      ' % header_text + return header + + +def get_sbs_footer(): + footer = "
      Last updated: " + timestamp, the_date, the_time = log_helper.get_master_time() + last_updated = "%s at %s" % (the_date, the_time) + footer = footer + "%s" % last_updated + gen_by = get_sbs_gen_by() + footer = footer + gen_by + return footer + + +def get_sbs_html(baseline="baseline.png", diff="baseline_diff.png"): + head = get_sbs_head() + header = get_sbs_header() + table_html = get_sbs_table_html(baseline, diff) + footer = get_sbs_footer() + the_html = ( + '' + + head + + '' + + header + + table_html + + footer + + "" + ) + return the_html diff --git a/seleniumbase/drivers/ReadMe.md b/seleniumbase/drivers/ReadMe.md old mode 100755 new mode 100644 index 7bc16c44c53..01fc6a0661d --- a/seleniumbase/drivers/ReadMe.md +++ b/seleniumbase/drivers/ReadMe.md @@ -1,22 +1,57 @@ -### SeleniumBase web driver storage + -#### Usage: +## SeleniumBase driver storage +To run web automation, you'll need webdrivers for each browser you plan on using. With SeleniumBase, drivers are downloaded automatically as needed into the SeleniumBase `drivers` folder. + +๐ŸŽ›๏ธ You can also download drivers manually with these commands: + +```zsh +seleniumbase get chromedriver +seleniumbase get geckodriver +seleniumbase get edgedriver ``` -seleniumbase install chromedriver -seleniumbase install geckodriver -seleniumbase install edgedriver -seleniumbase install iedriver -seleniumbase install operadriver + +After running the commands above, web drivers will get downloaded into the `seleniumbase/drivers/` folder. SeleniumBase uses those drivers during tests. (The drivers don't come with SeleniumBase by default.) + +If the necessary driver is not found in this location while running tests, SeleniumBase will instead look for the driver on the System PATH. If the necessary driver is not on the System PATH either, SeleniumBase will automatically attempt to download the required driver. + +๐ŸŽ›๏ธ You can also download specific versions of drivers. Examples: + +```zsh +sbase get chromedriver 114 +sbase get chromedriver 114.0.5735.90 +sbase get chromedriver stable +sbase get chromedriver beta +sbase get chromedriver dev +sbase get chromedriver canary +sbase get chromedriver previous # One major version before the stable version +sbase get chromedriver mlatest # Milestone latest version for detected browser +sbase get edgedriver 115.0.1901.183 ``` -After running the commands above, web drivers will get downloaded into this folder. SeleniumBase will then use those drivers during test runs if present. (The drivers don't come with SeleniumBase by default.) +(NOTE: `sbase` is a shortcut for `seleniumbase`) + +-------- + +**Browser Binaries**: + +๐ŸŽ›๏ธ Use the `sbase get` command to download the `Chrome for Testing` and `Chrome-Headless-Shell` browser binaries. Example: + +```zsh +sbase get chromium # (For base `Chromium`) +sbase get cft # (For `Chrome for Testing`) +sbase get chs # (For `Chrome-Headless-Shell`) +``` + +Those commands download those binaries into the `seleniumbase/drivers` folder. (There are subfolders, such as `cft_drivers`, `chs_drivers`, and `chromium_drivers`.) + +To use the base `Chromium` binary in SeleniumBase scripts, add `--use-chromium` on the command-line, or set `use_chromium=True` from within scripts. + +To use the `cft` or `chs` binaries in SeleniumBase scripts, set the `binary_location` to `cft` or `chs`, use `--cft` / `--chs` or set `cft=True` / `chs=True`. + +(Source: https://googlechromelabs.github.io/chrome-for-testing/) -If the necessary driver is not found in this location while running tests, SeleniumBase will instead look for the driver on the System PATH. If the necessary driver is not on the System PATH either, you'll get errors. +-------- -#### Notes: -* chromedriver allows you to run browser tests on Chrome -* geckodriver allows you to run browser tests on Firefox -* edgedriver allows you to run browser tests on MS Edge -* iedriver allows you to run browser tests on MS IE -* operadriver allows you to run browser tests on Opera +[](https://github.com/seleniumbase/SeleniumBase) diff --git a/seleniumbase/drivers/__init__.py b/seleniumbase/drivers/__init__.py old mode 100755 new mode 100644 diff --git a/seleniumbase/drivers/brave_drivers/__init__.py b/seleniumbase/drivers/brave_drivers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seleniumbase/drivers/cft_drivers/__init__.py b/seleniumbase/drivers/cft_drivers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seleniumbase/drivers/chromium_drivers/__init__.py b/seleniumbase/drivers/chromium_drivers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seleniumbase/drivers/chs_drivers/__init__.py b/seleniumbase/drivers/chs_drivers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seleniumbase/drivers/comet_drivers/__init__.py b/seleniumbase/drivers/comet_drivers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seleniumbase/drivers/opera_drivers/__init__.py b/seleniumbase/drivers/opera_drivers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seleniumbase/extensions/ReadMe.md b/seleniumbase/extensions/ReadMe.md old mode 100755 new mode 100644 index 33e25f06867..3b93bc7cfe9 --- a/seleniumbase/extensions/ReadMe.md +++ b/seleniumbase/extensions/ReadMe.md @@ -1,4 +1,13 @@ -### SeleniumBase browser extension storage + -**The List:** -* disable_csp.zip => Disable Chrome's Content-Security-Policy +[](https://github.com/seleniumbase/SeleniumBase) + +

      SeleniumBase browser extension storage

      + +The List: + +* ad_block.zip => This extension blocks certain types of iframe ads from loading. +* disable_csp.zip => This extension disables a website's Content-Security-Policy. +* recorder.zip => Save browser actions to sessionStorage with good CSS selectors. +* sbase_ext.zip => A Chromium extension that does nothing. (Used for testing purposes) +* firefox_addon.xpi => A Firefox add-on that does nothing. (Used for testing purposes) diff --git a/seleniumbase/extensions/__init__.py b/seleniumbase/extensions/__init__.py old mode 100755 new mode 100644 diff --git a/seleniumbase/extensions/ad_block.zip b/seleniumbase/extensions/ad_block.zip new file mode 100644 index 00000000000..ec27557cfb1 Binary files /dev/null and b/seleniumbase/extensions/ad_block.zip differ diff --git a/seleniumbase/extensions/disable_csp.zip b/seleniumbase/extensions/disable_csp.zip index f6e8d0d1f31..9a484e5790d 100644 Binary files a/seleniumbase/extensions/disable_csp.zip and b/seleniumbase/extensions/disable_csp.zip differ diff --git a/seleniumbase/extensions/firefox_addon.xpi b/seleniumbase/extensions/firefox_addon.xpi new file mode 100644 index 00000000000..dca8e2e1231 Binary files /dev/null and b/seleniumbase/extensions/firefox_addon.xpi differ diff --git a/seleniumbase/extensions/recorder.zip b/seleniumbase/extensions/recorder.zip new file mode 100644 index 00000000000..79a4870afae Binary files /dev/null and b/seleniumbase/extensions/recorder.zip differ diff --git a/seleniumbase/extensions/sbase_ext.zip b/seleniumbase/extensions/sbase_ext.zip new file mode 100644 index 00000000000..32ee567f430 Binary files /dev/null and b/seleniumbase/extensions/sbase_ext.zip differ diff --git a/seleniumbase/fixtures/__init__.py b/seleniumbase/fixtures/__init__.py old mode 100755 new mode 100644 diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py old mode 100755 new mode 100644 index 9e0d892736a..667d7c90308 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -1,129 +1,596 @@ -# -*- coding: utf-8 -*- -""" +r"""-----------------------------------------------------------------> +| ______ __ _ ____ | +| / ____/__ / /__ ____ (_)_ ______ ___ / _ \____ ________ | +| \__ \/ _ \/ / _ \/ __ \/ / / / / __ `__ \ / /_) / __ \/ ___/ _ \ | +| ___/ / __/ / __/ / / / / /_/ / / / / / // /_) / (_/ /__ / __/ | +| /____/\___/_/\___/_/ /_/_/\__,_/_/ /_/ /_//_____/\__,_/____/\___/ | +| | +---------------------------------------------------------------------> + The BaseCase class is the main gateway for using The SeleniumBase Framework. -It inherits Python's unittest.TestCase class, and runs with Pytest or Nose. +It inherits Python's unittest.TestCase class and runs with pytest or pynose. All tests using BaseCase automatically launch WebDriver browsers for tests. -Usage: - - from seleniumbase import BaseCase - class MyTestClass(BaseCase): - def test_anything(self): - # Write your code here. Example: - self.open("https://github.com/") - self.update_text("input.header-search-input", "SeleniumBase\n") - self.click('a[href="/seleniumbase/SeleniumBase"]') - self.assert_element("div.repository-content") - .... +Example Test: + +# -------------------------------------------------------------- +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + +class TestSimpleLogin(BaseCase): + def test_simple_login(self): + self.goto("https://seleniumbase.io/simple/login") + self.type("#username", "demo_user") + self.type("#password", "secret_pass") + self.click('a:contains("Sign in")') + self.assert_exact_text("Welcome!", "h1") + self.assert_element("img#image1") + self.highlight("#image1") + self.click_link("Sign out") + self.assert_text("signed out", "#top_message") +# -------------------------------------------------------------- SeleniumBase methods expand and improve on existing WebDriver commands. Improvements include making WebDriver more robust, reliable, and flexible. Page elements are given enough time to load before WebDriver acts on them. -Code becomes greatly simplified and easier to maintain. -""" +Code becomes greatly simplified and easier to maintain.""" -import codecs +import colorama +import fasteners import json import logging import math import os import re +import secrets +import shutil import sys +import textwrap import time import unittest -import uuid -from selenium.common.exceptions import (StaleElementReferenceException, - MoveTargetOutOfBoundsException, - WebDriverException) -from selenium.common import exceptions as selenium_exceptions +import urllib3 +from contextlib import contextmanager, suppress +from selenium.common.exceptions import ( + ElementClickInterceptedException as ECI_Exception, + ElementNotInteractableException as ENI_Exception, + InvalidArgumentException, + MoveTargetOutOfBoundsException, + NoSuchElementException, + NoSuchWindowException, + StaleElementReferenceException as Stale_Exception, + TimeoutException, + WebDriverException, +) +from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys -from selenium.webdriver.support.ui import Select +from selenium.webdriver.remote.remote_connection import LOGGER +from selenium.webdriver.remote.webelement import WebElement from seleniumbase import config as sb_config +from seleniumbase.__version__ import __version__ from seleniumbase.common import decorators +from seleniumbase.common.exceptions import ( + NotConnectedException, + NotUsingChromeException, + NotUsingChromiumException, + ProxyConnectionException, + OutOfScopeException, + VisualException, +) from seleniumbase.config import settings -from seleniumbase.core.testcase_manager import TestcaseDataPayload -from seleniumbase.core.testcase_manager import TestcaseManager +from seleniumbase.core import browser_launcher from seleniumbase.core import download_helper from seleniumbase.core import log_helper -from seleniumbase.core import tour_helper +from seleniumbase.core import session_helper from seleniumbase.core import visual_helper from seleniumbase.fixtures import constants +from seleniumbase.fixtures import css_to_xpath from seleniumbase.fixtures import js_utils from seleniumbase.fixtures import page_actions from seleniumbase.fixtures import page_utils +from seleniumbase.fixtures import shared_utils +from seleniumbase.fixtures import unittest_helper from seleniumbase.fixtures import xpath_to_css -ENI_Exception = selenium_exceptions.ElementNotInteractableException + +__all__ = ["BaseCase"] + +logging.getLogger("requests").setLevel(logging.ERROR) +logging.getLogger("urllib3").setLevel(logging.ERROR) +logging.getLogger("websocket").setLevel(logging.CRITICAL) +urllib3.disable_warnings() +LOGGER.setLevel(logging.WARNING) +is_linux = shared_utils.is_linux() +is_windows = shared_utils.is_windows() +python3_11_or_newer = False +if sys.version_info >= (3, 11): + python3_11_or_newer = True +py311_patch2 = constants.PatchPy311.PATCH class BaseCase(unittest.TestCase): - ''' - A base test case that wraps methods for enhanced usage. - You can also add your own methods here. - ''' + """""" def __init__(self, *args, **kwargs): - super(BaseCase, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) + self.__initialize_variables() + + def __initialize_variables(self): self.driver = None self.environment = None self.env = None # Add a shortened version of self.environment - self.__last_url_of_delayed_assert = "data:," - self.__last_page_load_url = "data:," + self.version_list = shared_utils.make_version_list(__version__) + self.version_tuple = tuple(self.version_list) + self.version_info = self.version_tuple + self.time = time.time + self.__page_sources = [] + self.__extra_actions = [] + self.__js_start_time = 0 + self.__set_c_from_switch = False + self.__frame_switch_layer = 0 # Used by Recorder-Mode + self.__frame_switch_multi = False # Used by Recorder-Mode + self.__last_saved_url = None # Used by Recorder-Mode + self.__uc_frame_layer = 0 + self.__called_setup = False + self.__called_teardown = False + self.__start_time_ms = int(time.time() * 1000.0) + self.__requests_timeout = None + self.__page_source_count = 0 + self.__screenshot_count = 0 + self.__saved_pdf_count = 0 + self.__logs_data_count = 0 + self.__last_data_file = None + self.__level_0_visual_f = False + self.__will_be_skipped = False + self.__passed_then_skipped = False + self.__visual_baseline_copies = [] + self.__last_url_of_deferred_assert = "about:blank" + self.__last_page_load_url = "about:blank" self.__last_page_screenshot = None self.__last_page_screenshot_png = None - self.__delayed_assert_count = 0 - self.__delayed_assert_failures = [] + self.__last_page_url = None + self.__last_page_source = None + self.__skip_reason = None + self.__origins_to_save = [] + self.__actions_to_save = [] + self.__dont_record_open = False + self.__dont_record_js_click = False + self.__new_window_on_rec_open = True + self.__overrided_default_timeouts = False + self.__added_pytest_html_extra = None + self.__deferred_assert_count = 0 + self.__deferred_assert_failures = [] + self.__device_width = None + self.__device_height = None + self.__device_pixel_ratio = None + self.__changed_jqc_theme = False + self.__jqc_default_theme = None + self.__jqc_default_color = None + self.__jqc_default_width = None + self.__saved_id = None # Requires self._* instead of self.__* for external class use + self._language = "English" + self._presentation_slides = {} + self._presentation_transition = {} + self._output_file_saves = True # For Presenter / ChartMaker + self._rec_overrides_switch = True # Recorder-Mode uses set_c vs switch + self._sb_test_identifier = None self._html_report_extra = [] # (Used by pytest_plugin.py) + self._last_page_screenshot = None + self._last_page_url = None + self._final_debug = None self._default_driver = None self._drivers_list = [] + self._drivers_browser_map = {} + self._was_skipped = False + self._chart_data = {} + self._chart_count = 0 + self._chart_label = {} + self._chart_xcount = 0 + self._chart_first_series = {} + self._chart_series_count = {} self._tour_steps = {} + self._xvfb_display = None + self._xvfb_width = None + self._xvfb_height = None - def open(self, url): - self.__last_page_load_url = None - self.driver.get(url) - if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: - self.wait_for_ready_state_complete() - self.__demo_mode_pause_if_active() + @classmethod + def main(self, name, file, *args): + """Run pytest if file was called with "python". + Usage example: + + from seleniumbase import BaseCase + BaseCase.main(__name__, __file__) - def open_url(self, url): - """ In case people are mixing up self.open() with open(), - use this alternative. """ - self.open(url) + class MyTestClass(BaseCase): + def test_example(self): + pass - def click(self, selector, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT): + The run command: + python my_test.py # (Instead of "pytest my_test.py") + + This is useful when sharing code with people who may not be aware + that SeleniumBase tests are run with "pytest" instead of "python". + Now, if they accidentally type "python", the tests will still run. + Eg. "python my_test.py" instead of "pytest my_test.py".""" + if name == "__main__": # Test called with "python" + import subprocess + all_args = [] + for arg in args: + all_args.append(arg) + for arg in sys.argv[1:]: + all_args.append(arg) + # See: https://stackoverflow.com/a/54666289/7058266 + # from pytest import main as pytest_main + # pytest_main([file, "-s", *all_args]) + subprocess.call( + [sys.executable, "-m", "pytest", file, "-s", *all_args] + ) + + def open(self, url, **kwargs): + """Navigates the current browser window to the specified page.""" + self.__check_scope() + if self.__is_cdp_swap_needed(): + self.cdp.open(url, **kwargs) + return + elif ( + getattr(self.driver, "_is_using_uc", None) + # and getattr(self.driver, "_is_using_auth", None) + and not getattr(self.driver, "_is_using_cdp", None) + ): + # Auth in UC Mode requires CDP Mode + # (and now we're always forcing it) + logging.info("open() in UC Mode now always activates CDP Mode.") + self.activate_cdp_mode(url, **kwargs) + return + elif ( + getattr(self.driver, "_is_using_uc", None) + and getattr(self.driver, "_is_using_cdp", None) + ): + self.disconnect() + self.cdp.open(url, **kwargs) + return + self._check_browser() + if self.__needs_minimum_wait(): + time.sleep(0.04) + pre_action_url = None + with suppress(Exception): + pre_action_url = self.driver.current_url + url = str(url).strip() # Remove leading and trailing whitespace + if not self.__looks_like_a_page_url(url): + # url should start with one of the following: + # "http:", "https:", "://", "data:", "file:", + # "about:", "chrome:", or "edge:". + if page_utils.is_valid_url("https://" + url): + url = "https://" + url + else: + raise Exception('Invalid URL: "%s"!' % url) + self.__last_page_load_url = None + js_utils.clear_out_console_logs(self.driver) + if url.startswith("://"): + # Convert URLs such as "://google.com" into "https://google.com" + url = "https" + url + if self.recorder_mode and not self.__dont_record_open: + time_stamp = self.execute_script("return Date.now();") + origin = self.get_origin() + action = ["o_url", origin, url, str(int(time_stamp) - 1)] + self.__extra_actions.append(action) + action = ["_url_", origin, url, time_stamp] + self.__extra_actions.append(action) + if self.recorder_mode and self.__new_window_on_rec_open: + c_url = self.driver.current_url + if ("http:") in c_url or ("https:") in c_url or ("file:") in c_url: + if self.get_domain_url(url) != self.get_domain_url(c_url): + self.open_new_window(switch_to=True) + try: + self.driver.get(url) + except Exception as e: + if not hasattr(e, "msg") and hasattr(self.driver, "default_get"): + try: + self._check_browser() + time.sleep(0.4) + except Exception: + logging.debug("Browser crashed! Will open new browser!") + self.driver = self.get_new_driver() + self.driver.default_get(url) + elif ( + "ERR_CONNECTION_TIMED_OUT" in e.msg + or "ERR_CONNECTION_CLOSED" in e.msg + or "ERR_CONNECTION_RESET" in e.msg + or "ERR_NAME_NOT_RESOLVED" in e.msg + ): + shared_utils.check_if_time_limit_exceeded() + self._check_browser() + time.sleep(0.8) + try: + self.driver.get(url) + except Exception as e2: + if "ERR_CONNECTION_RESET" in e2.msg and self.proxy_string: + message = "ERR_CONNECTION_RESET: " + message += "Invalid proxy and/or Internet unreachable!" + raise ProxyConnectionException(message) + else: + raise + elif ( + "ERR_INTERNET_DISCONNECTED" in e.msg + or "neterror?e=dnsNotFound" in e.msg + or "ERR_PROXY_CONNECTION_FAILED" in e.msg + ): + shared_utils.check_if_time_limit_exceeded() + self._check_browser() + time.sleep(1.05) + try: + self.driver.get(url) + except Exception as e2: + if ( + "ERR_INTERNET_DISCONNECTED" in e2.msg + or "neterror?e=dnsNotFound" in e2.msg + ): + message = "ERR_INTERNET_DISCONNECTED: " + message += "Internet unreachable!" + raise NotConnectedException(message) + elif "ERR_PROXY_CONNECTION_FAILED" in e2.msg: + message = "ERR_PROXY_CONNECTION_FAILED: " + message += "Internet unreachable and/or invalid proxy!" + raise ProxyConnectionException(message) + else: + raise + elif "Timed out receiving message from renderer" in e.msg: + page_load_timeout = self.driver.timeouts.page_load + logging.info( + "The page load timed out after %s seconds! Will retry..." + % page_load_timeout + ) + try: + self.driver.get(url) + except Exception as e: + if "Timed out receiving message from renderer" in e.msg: + raise Exception( + "Retry of page load timed out after %s seconds!" + % page_load_timeout + ) + else: + raise + elif ( + "cannot determine loading status" in e.msg + or "unexpected command response" in e.msg + ): + if self.__needs_minimum_wait(): + time.sleep(0.2) + self.driver.get(url) + else: + pass # Odd issue where the open did happen. Continue. + elif "invalid session id" in e.msg: + logging.debug("Invalid session id. Will open new browser.") + self.driver = self.get_new_driver() + self.driver.get(url) + elif "ERR_TUNNEL_CONNECTION_FAILED" in e.msg and self.proxy_string: + message = "ERR_CONNECTION_RESET: " + message += "Bad proxy and/or Internet unreachable!" + raise ProxyConnectionException(message) + else: + raise + try: + if ( + self.driver.current_url == pre_action_url + and pre_action_url != url + ): + time.sleep(0.1) # Make sure load happens + except Exception: + time.sleep(0.1) # First see if waiting helps + try: + self._check_browser() + if not self.driver.current_url: + raise Exception("No current URL!") + except Exception: + # Spin up a new driver with the URL + self.driver = self.get_new_driver() + self.driver.get(url) + self._check_browser() + if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: + if not self.undetectable: + self.wait_for_ready_state_complete() + if self.__needs_minimum_wait(): + time.sleep(0.08) # Force a minimum wait, even if skipping waits. + if self.undetectable: + self.__uc_frame_layer = 0 + if self.demo_mode: + if self.driver.current_url.startswith(("http", "file", "data")): + if not js_utils.is_jquery_activated(self.driver): + with suppress(Exception): + js_utils.add_js_link( + self.driver, constants.JQuery.MIN_JS + ) + self.__demo_mode_pause_if_active() + + def get(self, url): + """If "url" looks like a page URL, open the URL in the web browser. + Otherwise, return self.get_element(URL_AS_A_SELECTOR) + Examples: + self.get("https://seleniumbase.io") # Navigates to the URL + self.get("input.class") # Finds and returns the WebElement """ + self.__check_scope() + if self.__looks_like_a_page_url(url): + self.open(url) + else: + return self.get_element(url) # url is treated like a selector + + def click( + self, selector, by="css selector", timeout=None, delay=0, scroll=True + ): + self.__check_scope() + self.__skip_if_esc() + if not timeout: + timeout = settings.SMALL_TIMEOUT if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: timeout = self.__get_new_timeout(timeout) - if page_utils.is_xpath_selector(selector): - by = By.XPATH - if page_utils.is_link_text_selector(selector): - selector = page_utils.get_link_text_from_selector(selector) - by = By.LINK_TEXT + original_selector = selector + original_by = by + selector, by = self.__recalculate_selector(selector, by) + if self.__is_cdp_swap_needed(): + self.cdp.click(selector, timeout=timeout, scroll=scroll) + return + if delay and (type(delay) in [int, float]) and delay > 0: + time.sleep(delay) + if page_utils.is_link_text_selector(selector) or by == By.LINK_TEXT: if not self.is_link_text_visible(selector): # Handle a special case of links hidden in dropdowns self.click_link_text(selector, timeout=timeout) return + if ( + page_utils.is_partial_link_text_selector(selector) + or by == By.PARTIAL_LINK_TEXT + ): + if not self.is_partial_link_text_visible(selector): + # Handle a special case of partial links hidden in dropdowns + self.click_partial_link_text(selector, timeout=timeout) + return + if self.__is_shadow_selector(selector): + self.__shadow_click(selector, timeout) + return + if self.__needs_minimum_wait() or self.browser == "safari": + time.sleep(0.05) element = page_actions.wait_for_element_visible( - self.driver, selector, by, timeout=timeout) - self.__demo_mode_highlight_if_active(selector, by) - if not self.demo_mode: - self.__scroll_to_element(element) - pre_action_url = self.driver.current_url + self.driver, + selector, + by, + timeout=timeout, + original_selector=original_selector, + ) + self.__demo_mode_highlight_if_active(original_selector, original_by) + if scroll and not self.demo_mode and not self.slow_mode: + self.__scroll_to_element(element, selector, by) + pre_action_url = None + with suppress(Exception): + pre_action_url = self.driver.current_url + pre_window_count = len(self.driver.window_handles) try: - if self.browser == 'ie' and by == By.LINK_TEXT: - # An issue with clicking Link Text on IE means using jquery + if ( + by == By.LINK_TEXT + and (self.browser == "ie" or self.browser == "safari") + ): self.__jquery_click(selector, by=by) + elif self.browser == "safari": + self.execute_script("arguments[0].click();", element) else: + href = None + new_tab = False + onclick = None + with suppress(Exception): + if self.headless and element.tag_name.lower() == "a": + # Handle a special case of opening a new tab (headless) + href = element.get_attribute("href").strip() + onclick = element.get_attribute("onclick") + target = element.get_attribute("target") + if target == "_blank": + new_tab = True + if new_tab and self.__looks_like_a_page_url(href): + if onclick: + with suppress(Exception): + self.execute_script(onclick) + current_window = self.driver.current_window_handle + self.open_new_window() + with suppress(Exception): + self.open(href) + self.switch_to_window(current_window) + return # Normal click - element.click() - except (StaleElementReferenceException, ENI_Exception): + self.__element_click(element) + except Stale_Exception: self.wait_for_ready_state_complete() - time.sleep(0.05) + time.sleep(0.16) + element = page_actions.wait_for_element_clickable( + self.driver, + selector, + by, + timeout=timeout, + original_selector=original_selector, + ) + with suppress(Exception): + self.__scroll_to_element(element, selector, by) + if self.browser == "safari" and by == By.LINK_TEXT: + self.__jquery_click(selector, by=by) + elif self.browser == "safari": + self.execute_script("arguments[0].click();", element) + else: + self.__element_click(element) + except ENI_Exception as e: + with suppress(Exception): + if ( + "element has zero size" in e.msg + and element.tag_name.lower() == "a" + ): + if "contains(" not in selector: + self.js_click(selector, by=by) + else: + self.jquery_click(selector, by=by) + if self.__needs_minimum_wait(): + time.sleep(0.04) + return + self.wait_for_ready_state_complete() + time.sleep(0.1) element = page_actions.wait_for_element_visible( - self.driver, selector, by, timeout=timeout) - element.click() - except (WebDriverException, MoveTargetOutOfBoundsException): + self.driver, + selector, + by, + timeout=timeout, + original_selector=original_selector, + ) + if not page_actions.is_element_clickable( + self.driver, selector, by + ): + with suppress(Exception): + self.wait_for_element_clickable( + selector, by, timeout=1.8 + ) + element = page_actions.wait_for_element_visible( + self.driver, + selector, + by, + timeout=timeout, + original_selector=original_selector, + ) + href = None + new_tab = False + onclick = None + with suppress(Exception): + if element.tag_name.lower() == "a": + # Handle a special case of opening a new tab (non-headless) + href = element.get_attribute("href").strip() + onclick = element.get_attribute("onclick") + target = element.get_attribute("target") + if target == "_blank": + new_tab = True + if new_tab and self.__looks_like_a_page_url(href): + if onclick: + with suppress(Exception): + self.execute_script(onclick) + current_window = self.driver.current_window_handle + self.open_new_window() + with suppress(Exception): + self.open(href) + self.switch_to_window(current_window) + return + if scroll and not self.demo_mode and not self.slow_mode: + self.__scroll_to_element(element, selector, by) + if self.browser == "firefox" or self.browser == "safari": + if by == By.LINK_TEXT or "contains(" in selector: + self.__jquery_click(selector, by=by) + else: + self.__js_click(selector, by=by) + else: + try: + self.__element_click(element) + except Exception: + self.wait_for_ready_state_complete() + element = page_actions.wait_for_element_visible( + self.driver, + selector, + by, + timeout=timeout, + original_selector=original_selector, + ) + self.__element_click(element) + except MoveTargetOutOfBoundsException: self.wait_for_ready_state_complete() try: self.__js_click(selector, by=by) @@ -132,56 +599,297 @@ def click(self, selector, by=By.CSS_SELECTOR, self.__jquery_click(selector, by=by) except Exception: # One more attempt to click on the element - element = page_actions.wait_for_element_visible( - self.driver, selector, by, timeout=timeout) - element.click() + element = page_actions.wait_for_element_clickable( + self.driver, + selector, + by, + timeout=timeout, + original_selector=original_selector, + ) + self.__element_click(element) + except WebDriverException as e: + if ( + "cannot determine loading status" in e.msg + or "unexpected command response" in e.msg + ): + pass # Odd issue where the click did happen. Continue. + else: + self.wait_for_ready_state_complete() + try: + self.__js_click(selector, by=by) + except Exception: + try: + self.__jquery_click(selector, by=by) + except Exception: + # One more attempt to click on the element + element = page_actions.wait_for_element_visible( + self.driver, + selector, + by, + timeout=timeout, + original_selector=original_selector, + ) + self.__element_click(element) + latest_window_count = len(self.driver.window_handles) + if ( + latest_window_count > pre_window_count + and ( + self.recorder_mode + or ( + settings.SWITCH_TO_NEW_TABS_ON_CLICK + and self.driver.current_url == pre_action_url + ) + ) + ): + self.__switch_to_newest_window_if_not_blank() + elif ( + latest_window_count == pre_window_count - 1 + and latest_window_count > 0 + ): + # If a click closes the active window, + # switch to the last one if it exists. + self.switch_to_window(-1) if settings.WAIT_FOR_RSC_ON_CLICKS: - self.wait_for_ready_state_complete() + if not self.undetectable: + with suppress(Exception): + self.wait_for_ready_state_complete() + if self.__needs_minimum_wait() or self.browser == "safari": + time.sleep(0.05) + else: + time.sleep(0.08) + else: + if not self.undetectable: + # A smaller subset of self.wait_for_ready_state_complete() + with suppress(Exception): + self.wait_for_angularjs(timeout=settings.MINI_TIMEOUT) + if self.__needs_minimum_wait() or self.browser == "safari": + time.sleep(0.045) + try: + if self.driver.current_url != pre_action_url: + self.__ad_block_as_needed() + self.__disable_beforeunload_as_needed() + if self.__needs_minimum_wait(): + time.sleep(0.075) + except Exception: + with suppress(Exception): + self.wait_for_ready_state_complete() + if self.__needs_minimum_wait(): + time.sleep(0.05) + else: + time.sleep(0.085) if self.demo_mode: if self.driver.current_url != pre_action_url: + if not js_utils.is_jquery_activated(self.driver): + js_utils.add_js_link(self.driver, constants.JQuery.MIN_JS) self.__demo_mode_pause_if_active() else: self.__demo_mode_pause_if_active(tiny=True) + elif self.slow_mode: + self.__slow_mode_pause_if_active() + self.__set_esc_skip() + + def slow_click(self, selector, by="css selector", timeout=None): + """Similar to click(), but pauses for a brief moment before clicking. + When used in combination with setting the user-agent, you can often + bypass bot-detection by tricking websites into thinking that you're + not a bot. (Useful on websites that block web automation tools.) + To set the user-agent, use: ``--agent=AGENT``. + Here's an example message from GitHub's bot-blocker: + ``You have triggered an abuse detection mechanism...`` """ + self.__check_scope() + if not timeout: + timeout = settings.SMALL_TIMEOUT + if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + if not self.demo_mode and not self.slow_mode: + self.click(selector, by=by, timeout=timeout, delay=1.05) + elif self.slow_mode: + self.click(selector, by=by, timeout=timeout, delay=0.65) + else: + # Demo Mode already includes a small delay + self.click(selector, by=by, timeout=timeout, delay=0.25) - def double_click(self, selector, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT): - from selenium.webdriver import ActionChains + def double_click(self, selector, by="css selector", timeout=None): + self.__check_scope() + if not timeout: + timeout = settings.SMALL_TIMEOUT if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: timeout = self.__get_new_timeout(timeout) - if page_utils.is_xpath_selector(selector): - by = By.XPATH + original_selector = selector + original_by = by + selector, by = self.__recalculate_selector(selector, by) element = page_actions.wait_for_element_visible( - self.driver, selector, by, timeout=timeout) - self.__demo_mode_highlight_if_active(selector, by) - if not self.demo_mode: - self.__scroll_to_element(element) - pre_action_url = self.driver.current_url + self.driver, + selector, + by, + timeout=timeout, + original_selector=original_selector, + ) + self.__demo_mode_highlight_if_active(original_selector, original_by) + if not self.demo_mode and not self.slow_mode: + self.__scroll_to_element(element, selector, by) + self.wait_for_ready_state_complete() + if self.__needs_minimum_wait(): + time.sleep(0.02) + # Find the element one more time in case scrolling hid it + element = page_actions.wait_for_element_visible( + self.driver, + selector, + by, + timeout=timeout, + original_selector=original_selector, + ) + pre_action_url = None + with suppress(Exception): + pre_action_url = self.driver.current_url try: + if self.browser == "safari": + # Jump to the "except" block where the other script should work + raise Exception("This Exception will be caught.") actions = ActionChains(self.driver) - actions.move_to_element(element) - actions.double_click(element) - actions.perform() - except (StaleElementReferenceException, ENI_Exception): + actions.double_click(element).perform() + except Exception: + css_selector = self.convert_to_css_selector(selector, by=by) + css_selector = re.escape(css_selector) # Add "\\" to special chars + css_selector = self.__escape_quotes_if_needed(css_selector) + double_click_script = ( + """var targetElement1 = document.querySelector('%s'); + var clickEvent1 = document.createEvent('MouseEvents'); + clickEvent1.initEvent('dblclick', true, true); + targetElement1.dispatchEvent(clickEvent1);""" + % css_selector + ) + if ":contains\\(" not in css_selector: + self.execute_script(double_click_script) + else: + double_click_script = ( + """var targetElement1 = arguments[0]; + var clickEvent1 = document.createEvent('MouseEvents'); + clickEvent1.initEvent('dblclick', true, true); + targetElement1.dispatchEvent(clickEvent1);""" + ) + self.execute_script(double_click_script, element) + if settings.WAIT_FOR_RSC_ON_CLICKS: self.wait_for_ready_state_complete() - time.sleep(0.05) - element = page_actions.wait_for_element_visible( - self.driver, selector, by, timeout=timeout) + else: + # A smaller subset of self.wait_for_ready_state_complete() + self.wait_for_angularjs(timeout=settings.MINI_TIMEOUT) + if self.driver.current_url != pre_action_url: + self.__ad_block_as_needed() + self.__disable_beforeunload_as_needed() + if self.demo_mode: + if self.driver.current_url != pre_action_url: + if not js_utils.is_jquery_activated(self.driver): + js_utils.add_js_link(self.driver, constants.JQuery.MIN_JS) + self.__demo_mode_pause_if_active() + else: + self.__demo_mode_pause_if_active(tiny=True) + elif self.slow_mode: + self.__slow_mode_pause_if_active() + elif self.__needs_minimum_wait(): + time.sleep(0.02) + + def context_click(self, selector, by="css selector", timeout=None): + """(A context click is a right-click that opens a context menu.)""" + self.__check_scope() + if not timeout: + timeout = settings.SMALL_TIMEOUT + if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + original_selector = selector + original_by = by + selector, by = self.__recalculate_selector(selector, by) + element = page_actions.wait_for_element_visible( + self.driver, + selector, + by, + timeout=timeout, + original_selector=original_selector, + ) + self.__demo_mode_highlight_if_active(original_selector, original_by) + if not self.demo_mode and not self.slow_mode: + self.__scroll_to_element(element, selector, by) + self.wait_for_ready_state_complete() + if self.__needs_minimum_wait(): + time.sleep(0.02) + # Find the element one more time in case scrolling hid it + element = page_actions.wait_for_element_visible( + self.driver, + selector, + by, + timeout=timeout, + original_selector=original_selector, + ) + pre_action_url = None + with suppress(Exception): + pre_action_url = self.driver.current_url + try: + if self.browser == "safari": + # Jump to the "except" block where the other script should work + raise Exception("This Exception will be caught.") actions = ActionChains(self.driver) - actions.move_to_element(element) - actions.double_click(element) - actions.perform() + actions.context_click(element).perform() + except Exception: + css_selector = self.convert_to_css_selector(selector, by=by) + css_selector = re.escape(css_selector) # Add "\\" to special chars + css_selector = self.__escape_quotes_if_needed(css_selector) + right_click_script = ( + """var targetElement1 = document.querySelector('%s'); + var clickEvent1 = document.createEvent('MouseEvents'); + clickEvent1.initEvent('contextmenu', true, true); + targetElement1.dispatchEvent(clickEvent1);""" + % css_selector + ) + if ":contains\\(" not in css_selector: + self.execute_script(right_click_script) + else: + right_click_script = ( + """var targetElement1 = arguments[0]; + var clickEvent1 = document.createEvent('MouseEvents'); + clickEvent1.initEvent('contextmenu', true, true); + targetElement1.dispatchEvent(clickEvent1);""" + ) + self.execute_script(right_click_script, element) if settings.WAIT_FOR_RSC_ON_CLICKS: self.wait_for_ready_state_complete() + else: + # A smaller subset of self.wait_for_ready_state_complete() + self.wait_for_angularjs(timeout=settings.MINI_TIMEOUT) + if self.driver.current_url != pre_action_url: + self.__ad_block_as_needed() + self.__disable_beforeunload_as_needed() if self.demo_mode: if self.driver.current_url != pre_action_url: + if not js_utils.is_jquery_activated(self.driver): + js_utils.add_js_link(self.driver, constants.JQuery.MIN_JS) self.__demo_mode_pause_if_active() else: self.__demo_mode_pause_if_active(tiny=True) - - def click_chain(self, selectors_list, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT, spacing=0): - """ This method clicks on a list of elements in succession. - 'spacing' is the amount of time to wait between clicks. (sec) """ + elif self.slow_mode: + self.__slow_mode_pause_if_active() + elif self.__needs_minimum_wait(): + time.sleep(0.02) + if self.recorder_mode and self.__current_url_is_recordable(): + if self.get_session_storage_item("pause_recorder") == "no": + if by == By.XPATH: + selector = original_selector + time_stamp = self.execute_script("return Date.now();") + origin = self.get_origin() + action = ["r_clk", selector, origin, time_stamp] + self.__extra_actions.append(action) + + def click_chain( + self, selectors_list, by="css selector", timeout=None, spacing=0 + ): + """This method clicks on a list of elements in succession. + @Params + selectors_list - The list of selectors to click on. + by - The type of selector to search by (Default: "css selector"). + timeout - How long to wait for the selector to be visible. + spacing - The amount of time to wait between clicks (in seconds).""" + self.__check_scope() + if not timeout: + timeout = settings.SMALL_TIMEOUT if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: timeout = self.__get_new_timeout(timeout) for selector in selectors_list: @@ -189,2771 +897,13764 @@ def click_chain(self, selectors_list, by=By.CSS_SELECTOR, if spacing > 0: time.sleep(spacing) - def is_link_text_present(self, link_text): - """ Returns True if the link text appears in the HTML of the page. - The element doesn't need to be visible, - such as elements hidden inside a dropdown selection. """ - soup = self.get_beautiful_soup() - html_links = soup.find_all('a') - for html_link in html_links: - if html_link.text.strip() == link_text.strip(): - return True - return False - - def get_link_attribute(self, link_text, attribute, hard_fail=True): - """ Finds a link by link text and then returns the attribute's value. - If the link text or attribute cannot be found, an exception will - get raised if hard_fail is True (otherwise None is returned). """ - soup = self.get_beautiful_soup() - html_links = soup.find_all('a') - for html_link in html_links: - if html_link.text.strip() == link_text.strip(): - if html_link.has_attr(attribute): - attribute_value = html_link.get(attribute) - return attribute_value - if hard_fail: - raise Exception( - 'Unable to find attribute {%s} from link text {%s}!' - % (attribute, link_text)) - else: - return None - if hard_fail: - raise Exception("Link text {%s} was not found!" % link_text) - else: - return None - - def wait_for_link_text_present(self, link_text, - timeout=settings.SMALL_TIMEOUT): - start_ms = time.time() * 1000.0 - stop_ms = start_ms + (timeout * 1000.0) - for x in range(int(timeout * 5)): - try: - if not self.is_link_text_present(link_text): - raise Exception( - "Link text {%s} was not found!" % link_text) - return - except Exception: - now_ms = time.time() * 1000.0 - if now_ms >= stop_ms: - break - time.sleep(0.2) - raise Exception( - "Link text {%s} was not present after %s seconds!" % ( - link_text, timeout)) - - def click_link_text(self, link_text, timeout=settings.SMALL_TIMEOUT): - """ This method clicks link text on a page """ - # If using phantomjs, might need to extract and open the link directly - if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: + def update_text( + self, selector, text, by="css selector", timeout=None, retry=False + ): + """This method updates an element's text field with new text. + Has multiple parts: + * Waits for the element to be visible. + * Waits for the element to be interactive. + * Clears the text field. + * Types in the new text. + * Hits Enter/Submit (if the text ends in "\n"). + @Params + selector - The selector of the text field. + text - The new text to type into the text field. + by - The type of selector to search by. (Default: "css selector") + timeout - How long to wait for the selector to be visible. + retry - If True, use JS if the Selenium text update fails.""" + self.__check_scope() + if not timeout: + timeout = settings.LARGE_TIMEOUT + if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: timeout = self.__get_new_timeout(timeout) - if self.browser == 'phantomjs': - if self.is_link_text_visible(link_text): - element = self.wait_for_link_text_visible( - link_text, timeout=timeout) - element.click() - return - self.open(self.__get_href_from_link_text(link_text)) + selector, by = self.__recalculate_selector(selector, by) + if self.__is_cdp_swap_needed(): + self.cdp.type(selector, text, timeout=timeout) return - if not self.is_link_text_present(link_text): - self.wait_for_link_text_present(link_text, timeout=timeout) - pre_action_url = self.get_current_url() + if self.__is_shadow_selector(selector): + self.__shadow_type(selector, text, timeout) + return + element = self.wait_for_element_clickable( + selector, by=by, timeout=timeout + ) + self.__demo_mode_highlight_if_active(selector, by) + if not self.demo_mode and not self.slow_mode: + self.__scroll_to_element(element, selector, by) + if self.__needs_minimum_wait(): + time.sleep(0.04) try: - element = self.wait_for_link_text_visible( - link_text, timeout=0.2) - self.__demo_mode_highlight_if_active(link_text, by=By.LINK_TEXT) - try: - element.click() - except (StaleElementReferenceException, ENI_Exception): - self.wait_for_ready_state_complete() - time.sleep(0.05) - element = self.wait_for_link_text_visible( - link_text, timeout=timeout) - element.click() + element.clear() # May need https://stackoverflow.com/a/50691625 + backspaces = Keys.BACK_SPACE * 42 # Is the answer to everything + element.send_keys(backspaces) # In case autocomplete keeps text + except (Stale_Exception, ENI_Exception): + self.wait_for_ready_state_complete() + time.sleep(0.16) + element = self.wait_for_element_clickable( + selector, by=by, timeout=timeout + ) + with suppress(Exception): + element.clear() except Exception: - found_css = False - text_id = self.get_link_attribute(link_text, "id", False) - if text_id: - link_css = '[id="%s"]' % link_text - found_css = True - - if not found_css: - href = self.__get_href_from_link_text(link_text, False) - if href: - if href.startswith('/') or page_utils.is_valid_url(href): - link_css = '[href="%s"]' % href - found_css = True - - if not found_css: - ngclick = self.get_link_attribute(link_text, "ng-click", False) - if ngclick: - link_css = '[ng-click="%s"]' % ngclick - found_css = True - - if not found_css: - onclick = self.get_link_attribute(link_text, "onclick", False) - if onclick: - link_css = '[onclick="%s"]' % onclick - found_css = True - - success = False - if found_css: - if self.is_element_visible(link_css): - self.click(link_css) - success = True + pass # Clearing the text field first might not be necessary + self.__demo_mode_pause_if_active(tiny=True) + pre_action_url = None + if self.demo_mode: + with suppress(Exception): + pre_action_url = self.driver.current_url + text = self.__get_type_checked_text(text) + try: + if not text.endswith("\n"): + element.send_keys(text) + if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: + self.wait_for_ready_state_complete() + else: + element.send_keys(text[:-1]) + if self.slow_mode or self.demo_mode: + self.__demo_mode_pause_if_active(tiny=True) else: - # The link text might be hidden under a dropdown menu - success = self.__click_dropdown_link_text( - link_text, link_css) - - if not success: - element = self.wait_for_link_text_visible( - link_text, timeout=settings.MINI_TIMEOUT) - element.click() - - if settings.WAIT_FOR_RSC_ON_CLICKS: + time.sleep(0.0135) + try: + element.send_keys(Keys.RETURN) + except WebDriverException as e: + if ( + "cannot determine loading status" in e.msg + or "unexpected command response" in e.msg + ): + pass # Odd issue where the click did happen. Continue. + else: + raise + if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: + self.wait_for_ready_state_complete() + if self.__needs_minimum_wait(): + time.sleep(0.04) + except Exception: self.wait_for_ready_state_complete() + time.sleep(0.14) + element = self.wait_for_element_visible( + selector, by=by, timeout=timeout + ) + time.sleep(0.04) + if not text.endswith("\n"): + element.send_keys(text) + else: + element.send_keys(text[:-1]) + if self.slow_mode or self.demo_mode: + self.__demo_mode_pause_if_active(tiny=True) + else: + time.sleep(0.0135) + try: + element.send_keys(Keys.RETURN) + except WebDriverException as e: + if ( + "cannot determine loading status" in e.msg + or "unexpected command response" in e.msg + ): + pass # Odd issue where the click did happen. Continue. + else: + raise + if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: + self.wait_for_ready_state_complete() + if self.__needs_minimum_wait(): + time.sleep(0.03) + if self.undetectable: + time.sleep(0.025) + if ( + retry + and element.get_attribute("value") != text + and not text.endswith("\n") + ): + logging.debug("update_text() is falling back to JavaScript!") + self.set_value(selector, text, by=by) if self.demo_mode: if self.driver.current_url != pre_action_url: + if not js_utils.is_jquery_activated(self.driver): + js_utils.add_js_link(self.driver, constants.JQuery.MIN_JS) self.__demo_mode_pause_if_active() else: self.__demo_mode_pause_if_active(tiny=True) - - def click_link(self, link_text, timeout=settings.SMALL_TIMEOUT): - """ Same as self.click_link_text() """ - self.click_link_text(link_text, timeout=timeout) - - def click_partial_link_text(self, partial_link_text, - timeout=settings.SMALL_TIMEOUT): - """ This method clicks the partial link text on a page. """ - # If using phantomjs, might need to extract and open the link directly - if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: + elif self.slow_mode: + self.__slow_mode_pause_if_active() + + def add_text(self, selector, text, by="css selector", timeout=None): + """The more-reliable version of driver.send_keys() + Similar to update_text(), but won't clear the text field first.""" + self.__check_scope() + if not timeout: + timeout = settings.LARGE_TIMEOUT + if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: timeout = self.__get_new_timeout(timeout) - if self.browser == 'phantomjs': - if self.is_partial_link_text_visible(partial_link_text): - element = self.wait_for_partial_link_text(partial_link_text) - element.click() - return - soup = self.get_beautiful_soup() - html_links = soup.fetch('a') - for html_link in html_links: - if partial_link_text in html_link.text: - for html_attribute in html_link.attrs: - if html_attribute[0] == 'href': - href = html_attribute[1] - if href.startswith('//'): - link = "http:" + href - elif href.startswith('/'): - url = self.driver.current_url - domain_url = self.get_domain_url(url) - link = domain_url + href - else: - link = href - self.open(link) - return - raise Exception( - 'Could not parse link from partial link_text ' - '{%s}' % partial_link_text) - raise Exception( - "Partial link text {%s} was not found!" % partial_link_text) - # Not using phantomjs - element = self.wait_for_partial_link_text( - partial_link_text, timeout=timeout) - self.__demo_mode_highlight_if_active( - partial_link_text, by=By.PARTIAL_LINK_TEXT) - pre_action_url = self.driver.current_url - try: - element.click() - except (StaleElementReferenceException, ENI_Exception): - self.wait_for_ready_state_complete() - time.sleep(0.05) - element = self.wait_for_partial_link_text( - partial_link_text, timeout=timeout) - element.click() - if settings.WAIT_FOR_RSC_ON_CLICKS: + selector, by = self.__recalculate_selector(selector, by) + if self.__is_cdp_swap_needed(): + self.cdp.send_keys(selector, text) + return + if self.__is_shadow_selector(selector): + self.__shadow_type(selector, text, timeout, clear_first=False) + return + if selector == "html" and text in ["\n", Keys.ENTER, Keys.RETURN]: + # This is a shortcut for calling self.click_active_element(). + # Use after "\t" or Keys.TAB to cycle through elements first. + self.click_active_element() + return + element = self.wait_for_element_present( + selector, by=by, timeout=timeout + ) + if ( + selector == "html" and text.count("\t") >= 1 + and text.count("\n") == 1 and text.endswith("\n") + and text.replace("\t", "").replace("\n", "").replace(" ", "") == "" + ): + # Shortcut to send multiple tabs followed by click_active_element() self.wait_for_ready_state_complete() + element.send_keys(Keys.TAB * text.count("\t")) + self.click_active_element() + return + self.__demo_mode_highlight_if_active(selector, by) + if not self.demo_mode and not self.slow_mode: + self.__scroll_to_element(element, selector, by) + pre_action_url = None if self.demo_mode: - if self.driver.current_url != pre_action_url: - self.__demo_mode_pause_if_active() + with suppress(Exception): + pre_action_url = self.driver.current_url + text = self.__get_type_checked_text(text) + try: + if not text.endswith("\n"): + element.send_keys(text) else: - self.__demo_mode_pause_if_active(tiny=True) - - def get_text(self, selector, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT): - if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - if page_utils.is_xpath_selector(selector): - by = By.XPATH - self.wait_for_ready_state_complete() - time.sleep(0.01) - element = page_actions.wait_for_element_visible( - self.driver, selector, by, timeout) - try: - element_text = element.text - except (StaleElementReferenceException, ENI_Exception): + element.send_keys(text[:-1]) + if self.slow_mode or self.demo_mode: + self.__demo_mode_pause_if_active(tiny=True) + else: + time.sleep(0.0135) + element.send_keys(Keys.RETURN) + if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: + self.wait_for_ready_state_complete() + except (Stale_Exception, ENI_Exception): self.wait_for_ready_state_complete() - time.sleep(0.06) - element = page_actions.wait_for_element_visible( - self.driver, selector, by, timeout) - element_text = element.text - return element_text - - def get_attribute(self, selector, attribute, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT): - """ This method uses JavaScript to get the value of an attribute. """ - if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: + time.sleep(0.16) + element = self.wait_for_element_visible( + selector, by=by, timeout=timeout + ) + if not text.endswith("\n"): + element.send_keys(text) + else: + element.send_keys(text[:-1]) + if self.slow_mode or self.demo_mode: + self.__demo_mode_pause_if_active(tiny=True) + else: + time.sleep(0.0135) + element.send_keys(Keys.RETURN) + if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: + self.wait_for_ready_state_complete() + if self.demo_mode: + if self.driver.current_url != pre_action_url: + if not js_utils.is_jquery_activated(self.driver): + js_utils.add_js_link(self.driver, constants.JQuery.MIN_JS) + self.__demo_mode_pause_if_active() + else: + self.__demo_mode_pause_if_active(tiny=True) + elif self.slow_mode: + self.__slow_mode_pause_if_active() + + def type( + self, selector, text, by="css selector", timeout=None, retry=False + ): + """Same as self.update_text() + This method updates an element's text field with new text. + Has multiple parts: + * Waits for the element to be visible. + * Waits for the element to be interactive. + * Clears the text field. + * Types in the new text. + * Hits Enter/Submit (if the text ends in "\n"). + @Params + selector - The selector of the text field. + text - The new text to type into the text field. + by - The type of selector to search by. (Default: "css selector") + timeout - How long to wait for the selector to be visible. + retry - If True, use JS if the Selenium text update fails. + DO NOT confuse self.type() with Python type()! They are different! + """ + self.__check_scope() + if not timeout: + timeout = settings.LARGE_TIMEOUT + if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: timeout = self.__get_new_timeout(timeout) - if page_utils.is_xpath_selector(selector): - by = By.XPATH - if page_utils.is_link_text_selector(selector): - selector = page_utils.get_link_text_from_selector(selector) - by = By.LINK_TEXT + selector, by = self.__recalculate_selector(selector, by) + self.update_text(selector, text, by=by, timeout=timeout, retry=retry) + + def send_keys(self, selector, text, by="css selector", timeout=None): + """Same as self.add_text() + Similar to update_text(), but won't clear the text field first.""" + self.__check_scope() + if not timeout: + timeout = settings.LARGE_TIMEOUT + if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + selector, by = self.__recalculate_selector(selector, by) + self.add_text(selector, text, by=by, timeout=timeout) + + def press_keys(self, selector, text, by="css selector", timeout=None): + """Use send_keys() to press one key at a time.""" + if self.__is_cdp_swap_needed(): + self.cdp.press_keys(selector, text, timeout=timeout) + return self.wait_for_ready_state_complete() - time.sleep(0.01) - element = page_actions.wait_for_element_present( - self.driver, selector, by, timeout) - try: - attribute_value = element.get_attribute(attribute) - except (StaleElementReferenceException, ENI_Exception): - self.wait_for_ready_state_complete() - time.sleep(0.06) - element = page_actions.wait_for_element_present( - self.driver, selector, by, timeout) - attribute_value = element.get_attribute(attribute) - if attribute_value is not None: - return attribute_value - else: - raise Exception("Element {%s} has no attribute {%s}!" % ( - selector, attribute)) + element = self.wait_for_element_present( + selector, by=by, timeout=timeout + ) + if self.demo_mode: + selector, by = self.__recalculate_selector(selector, by) + css_selector = self.convert_to_css_selector(selector, by=by) + self.__demo_mode_highlight_if_active(css_selector, By.CSS_SELECTOR) + if self.recorder_mode and self.__current_url_is_recordable(): + if self.get_session_storage_item("pause_recorder") == "no": + css_selector = self.convert_to_css_selector(selector, by=by) + time_stamp = self.execute_script("return Date.now();") + origin = self.get_origin() + sel_tex = [css_selector, text] + action = ["pkeys", sel_tex, origin, time_stamp] + self.__extra_actions.append(action) + press_enter = False + if text.endswith("\n"): + text = text[:-1] + press_enter = True + for key in text: + element.send_keys(key) + if press_enter: + if self.slow_mode or self.demo_mode: + self.__demo_mode_pause_if_active(tiny=True) + else: + time.sleep(0.0135) + element.send_keys(Keys.RETURN) + if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: + if not self.undetectable: + self.wait_for_ready_state_complete() + else: + time.sleep(0.15) + if self.demo_mode: + if press_enter: + self.__demo_mode_pause_if_active() + else: + self.__demo_mode_pause_if_active(tiny=True) + elif self.slow_mode: + self.__slow_mode_pause_if_active() + elif self.__needs_minimum_wait(): + time.sleep(0.05) + if self.undetectable: + time.sleep(0.02) - def set_attribute(self, selector, attribute, value, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT): - """ This method uses JavaScript to set/update an attribute. """ - if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - if page_utils.is_xpath_selector(selector): - by = By.XPATH - if self.is_element_visible(selector, by=by): - self.scroll_to(selector, by=by, timeout=timeout) - attribute = re.escape(attribute) - attribute = self.__escape_quotes_if_needed(attribute) - value = re.escape(value) - value = self.__escape_quotes_if_needed(value) - css_selector = self.convert_to_css_selector(selector, by=by) - css_selector = re.escape(css_selector) - css_selector = self.__escape_quotes_if_needed(css_selector) - script = ("""document.querySelector('%s').setAttribute('%s','%s');""" - % (css_selector, attribute, value)) - self.execute_script(script) + def submit(self, selector, by="css selector"): + """Alternative to self.driver.find_element_by_*(SELECTOR).submit()""" + self.__check_scope() + selector, by = self.__recalculate_selector(selector, by) + if self.__is_cdp_swap_needed(): + self.cdp.submit(selector) + return + element = self.wait_for_element_clickable( + selector, by=by, timeout=settings.SMALL_TIMEOUT + ) + element.submit() + self.__demo_mode_pause_if_active() - def remove_attribute(self, selector, attribute, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT): - """ This method uses JavaScript to remove an attribute. """ - if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: + def clear(self, selector, by="css selector", timeout=None): + """This method clears an element's text field. + A clear() is already included with most methods that type text, + such as self.type(), self.update_text(), etc. + Does not use Demo Mode highlights, mainly because we expect + that some users will be calling an unnecessary clear() before + calling a method that already includes clear() as part of it. + In case websites trigger an autofill after clearing a field, + add backspaces to make sure autofill doesn't undo the clear. + @Params + selector - The selector of the text field. + by - The type of selector to search by. (Default: "css selector") + timeout - How long to wait for the selector to be visible.""" + self.__check_scope() + if not timeout: + timeout = settings.LARGE_TIMEOUT + if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: timeout = self.__get_new_timeout(timeout) - if page_utils.is_xpath_selector(selector): - by = By.XPATH - if self.is_element_visible(selector, by=by): - self.scroll_to(selector, by=by, timeout=timeout) - attribute = re.escape(attribute) - attribute = self.__escape_quotes_if_needed(attribute) - css_selector = self.convert_to_css_selector(selector, by=by) - css_selector = re.escape(css_selector) - css_selector = self.__escape_quotes_if_needed(css_selector) - script = ("""document.querySelector('%s').removeAttribute('%s');""" - % (css_selector, attribute)) - self.execute_script(script) + selector, by = self.__recalculate_selector(selector, by) + if self.__is_cdp_swap_needed(): + self.cdp.clear_input(selector) + return + if self.__is_shadow_selector(selector): + self.__shadow_clear(selector, timeout) + return + element = self.wait_for_element_visible( + selector, by=by, timeout=timeout + ) + self.scroll_to(selector, by=by, timeout=timeout) + try: + element.clear() + backspaces = Keys.BACK_SPACE * 42 # Autofill Defense + element.send_keys(backspaces) + except (Stale_Exception, ENI_Exception): + self.wait_for_ready_state_complete() + time.sleep(0.16) + element = self.wait_for_element_visible( + selector, by=by, timeout=timeout + ) + element.clear() + with suppress(Exception): + backspaces = Keys.BACK_SPACE * 42 # Autofill Defense + element.send_keys(backspaces) + except Exception: + element.clear() - def get_property_value(self, selector, property, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT): - """ Returns the property value of a page element's computed style. - Example: - opacity = self.get_property_value("html body a", "opacity") - self.assertTrue(float(opacity) > 0, "Element not visible!") """ - if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: + def focus(self, selector, by="css selector", timeout=None): + """Make the current page focus on an interactable element. + If the element is not interactable, only scrolls to it. + The "tab" key is another way of setting the page focus.""" + self.__check_scope() + if not timeout: + timeout = settings.LARGE_TIMEOUT + if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: timeout = self.__get_new_timeout(timeout) - if page_utils.is_xpath_selector(selector): - by = By.XPATH - if page_utils.is_link_text_selector(selector): - selector = page_utils.get_link_text_from_selector(selector) - by = By.LINK_TEXT - self.wait_for_ready_state_complete() - page_actions.wait_for_element_present( - self.driver, selector, by, timeout) + selector, by = self.__recalculate_selector(selector, by) + if self.__is_cdp_swap_needed(): + self.cdp.focus(selector, scroll=True, timeout=timeout) + return + element = self.wait_for_element_present( + selector, by=by, timeout=timeout + ) + if not element.is_displayed(): + css_selector = self.convert_to_css_selector(selector, by=by) + css_selector = re.escape(css_selector) # Add "\\" to special chars + css_selector = self.__escape_quotes_if_needed(css_selector) + script = """document.querySelector('%s').focus();""" % css_selector + self.execute_script(script) + self.__demo_mode_pause_if_active() + return + self.scroll_to(selector, by=by, timeout=timeout) try: - selector = self.convert_to_css_selector(selector, by=by) - except Exception: - # Don't run action if can't convert to CSS_Selector for JavaScript - raise Exception( - "Exception: Could not convert {%s}(by=%s) to CSS_SELECTOR!" % ( - selector, by)) - selector = re.escape(selector) - selector = self.__escape_quotes_if_needed(selector) - script = ("""var $elm = document.querySelector('%s'); - $val = window.getComputedStyle($elm).getPropertyValue('%s'); - return $val;""" - % (selector, property)) - value = self.execute_script(script) - if value is not None: - return value - else: - return "" # Return an empty string if the property doesn't exist + element.send_keys(Keys.NULL) + except (Stale_Exception, ENI_Exception): + self.wait_for_ready_state_complete() + time.sleep(0.12) + element = self.wait_for_element_visible( + selector, by=by, timeout=timeout + ) + try: + element.send_keys(Keys.NULL) + except ENI_Exception: + # Non-interactable element. Skip focus and continue. + pass + self.__demo_mode_pause_if_active() - def refresh_page(self): + def refresh_page(self, *args, **kwargs): + self.__check_scope() self.__last_page_load_url = None + if self.__is_cdp_swap_needed(): + self.cdp.reload(*args, **kwargs) + return + js_utils.clear_out_console_logs(self.driver) self.driver.refresh() self.wait_for_ready_state_complete() - def refresh(self): - """ The shorter version of self.refresh_page() """ - self.refresh_page() + def refresh(self, *args, **kwargs): + """Same as self.refresh_page()""" + self.refresh_page(*args, **kwargs) def get_current_url(self): - return self.driver.current_url + self.__check_scope() + current_url = None + if self.__is_cdp_swap_needed(): + current_url = self.cdp.get_current_url() + else: + current_url = self.driver.current_url + if "%" in current_url: + try: + from urllib.parse import unquote + + current_url = unquote(current_url, errors="strict") + except Exception: + pass + return current_url - def get_page_source(self): + def get_origin(self): + self.__check_scope() + return self.execute_script("return window.location.origin;") + + def get_html(self, *args, **kwargs): + return self.get_page_source(*args, **kwargs) + + def get_page_source(self, *args, **kwargs): + if self.__is_cdp_swap_needed(*args, **kwargs): + return self.cdp.get_page_source() self.wait_for_ready_state_complete() + if self.__needs_minimum_wait: + time.sleep(0.025) return self.driver.page_source def get_page_title(self): + if self.__is_cdp_swap_needed(): + return self.cdp.get_title() self.wait_for_ready_state_complete() + with suppress(Exception): + self.wait_for_element_present( + "title", by="css selector", timeout=settings.MINI_TIMEOUT + ) + time.sleep(0.025) return self.driver.title def get_title(self): - """ The shorter version of self.get_page_title() """ - self.wait_for_ready_state_complete() - return self.driver.title + """The shorter version of self.get_page_title()""" + return self.get_page_title() + + def get_user_agent(self): + if self.__is_cdp_swap_needed(): + return self.cdp.get_user_agent() + return self.execute_script("return navigator.userAgent;") + + def get_locale_code(self): + if self.__is_cdp_swap_needed(): + return self.cdp.get_locale_code() + return self.execute_script( + "return navigator.language || navigator.languages[0];" + ) def go_back(self): + self.__check_scope() + if self.__is_cdp_swap_needed(): + self.cdp.go_back() + return + if getattr(self, "recorder_mode", None): + self.save_recorded_actions() + pre_action_url = None + with suppress(Exception): + pre_action_url = self.driver.current_url self.__last_page_load_url = None self.driver.back() + with suppress(Exception): + if pre_action_url == self.driver.current_url: + self.driver.back() # Again because the page was redirected + if self.recorder_mode: + time_stamp = self.execute_script("return Date.now();") + origin = self.get_origin() + action = ["go_bk", "", origin, time_stamp] + self.__extra_actions.append(action) + if self.browser == "safari": + self.wait_for_ready_state_complete() + time.sleep(0.02) + self.driver.refresh() + time.sleep(0.02) self.wait_for_ready_state_complete() + self.__demo_mode_pause_if_active() def go_forward(self): + self.__check_scope() + if self.__is_cdp_swap_needed(): + self.cdp.go_forward() + return + if getattr(self, "recorder_mode", None): + self.save_recorded_actions() self.__last_page_load_url = None self.driver.forward() + if self.recorder_mode: + time_stamp = self.execute_script("return Date.now();") + origin = self.get_origin() + action = ["go_fw", "", origin, time_stamp] + self.__extra_actions.append(action) self.wait_for_ready_state_complete() + self.__demo_mode_pause_if_active() - def get_image_url(self, selector, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT): - """ Extracts the URL from an image element on the page. """ - if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - return self.get_attribute(selector, - attribute='src', by=by, timeout=timeout) - - def add_text(self, selector, new_value, by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT): - """ The more-reliable version of driver.send_keys() - Similar to update_text(), but won't clear the text field first. """ - if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - if page_utils.is_xpath_selector(selector): - by = By.XPATH - element = self.wait_for_element_visible( - selector, by=by, timeout=timeout) - self.__demo_mode_highlight_if_active(selector, by) - if not self.demo_mode: - self.__scroll_to_element(element) - pre_action_url = self.driver.current_url - try: - if not new_value.endswith('\n'): - element.send_keys(new_value) - else: - new_value = new_value[:-1] - element.send_keys(new_value) - element.send_keys(Keys.RETURN) - if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: - self.wait_for_ready_state_complete() - except (StaleElementReferenceException, ENI_Exception): - self.wait_for_ready_state_complete() - time.sleep(0.06) - element = self.wait_for_element_visible( - selector, by=by, timeout=timeout) - if not new_value.endswith('\n'): - element.send_keys(new_value) - else: - new_value = new_value[:-1] - element.send_keys(new_value) - element.send_keys(Keys.RETURN) - if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: - self.wait_for_ready_state_complete() - except Exception: - exc_message = self.__get_improved_exception_message() - raise Exception(exc_message) - if self.demo_mode: - if self.driver.current_url != pre_action_url: - self.__demo_mode_pause_if_active() + def open_start_page(self): + """Navigates the current browser window to the start_page. + You can set the start_page on the command-line in three ways: + '--start_page=URL', '--start-page=URL', or '--url=URL'. + If the start_page is not set, then "about:blank" will be used.""" + self.__check_scope() + start_page = self.start_page + if isinstance(start_page, str): + start_page = start_page.strip() # Remove extra whitespace + if start_page and len(start_page) >= 4: + if page_utils.is_valid_url(start_page): + self.open(start_page) else: - self.__demo_mode_pause_if_active(tiny=True) + new_start_page = "https://" + start_page + if page_utils.is_valid_url(new_start_page): + self.__dont_record_open = True + self.open(new_start_page) + self.__dont_record_open = False + else: + logging.info('Invalid URL: "%s"!' % start_page) + if self.get_current_url() != "about:blank": + self.open("about:blank") + else: + if self.get_current_url() != "about:blank": + self.open("about:blank") + + def open_if_not_url(self, url): + """Opens the url in the browser if it's not the current url (*). + Parameters tagged on by search engines are ignored for this method. + Eg. If the current url is: + * https://www.bing.com/search?q=SeleniumBase&source=hp + And the url privided by this method call is: + * https://www.bing.com/search?q=SeleniumBase + Then the urls will be considered the same, + and no open() action will be performed. + This method is primarily used by Recorder Mode script generation, + where both clicks and opens are recorded. So if a click() action + leads to an goto() action, then the script generator will attempt + to convert the goto() action into goto_if_not_url() so that the + same page isn't opened again if the user is already on the page.""" + self.__check_scope() + current_url = self.get_current_url() + if current_url != url: + if ( + "?q=" not in current_url + or "&" not in current_url + or current_url.find("?q=") >= current_url.find("&") + or current_url.split("&")[0] != url + ): + self.open(url) + + def is_element_present(self, selector, by="css selector"): + """Returns whether the element exists in the HTML.""" + if self.__is_cdp_swap_needed(): + return self.cdp.is_element_present(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 send_keys(self, selector, new_value, by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT): - """ Same as add_text() -> more reliable, but less name confusion. """ - if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - if page_utils.is_xpath_selector(selector): - by = By.XPATH - self.add_text(selector, new_value, by=by, timeout=timeout) - - def update_text_value(self, selector, new_value, by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT, retry=False): - """ This method updates an element's text value with a new value. - @Params - selector - the selector with the value to update - new_value - the new value for setting the text field - by - the type of selector to search by (Default: CSS) - timeout - how long to wait for the selector to be visible - retry - if True, use JS if the selenium text update fails - """ - if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - if page_utils.is_xpath_selector(selector): - by = By.XPATH - element = self.wait_for_element_visible( - selector, by=by, timeout=timeout) - self.__demo_mode_highlight_if_active(selector, by) - if not self.demo_mode: - self.__scroll_to_element(element) - try: - element.clear() - except (StaleElementReferenceException, ENI_Exception): - self.wait_for_ready_state_complete() - time.sleep(0.06) - element = self.wait_for_element_visible( - selector, by=by, timeout=timeout) - element.clear() - except Exception: - pass # Clearing the text field first isn't critical - self.__demo_mode_pause_if_active(tiny=True) - pre_action_url = self.driver.current_url - try: - if not new_value.endswith('\n'): - element.send_keys(new_value) - if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: - self.wait_for_ready_state_complete() - else: - new_value = new_value[:-1] - element.send_keys(new_value) - element.send_keys(Keys.RETURN) - if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: - self.wait_for_ready_state_complete() - except (StaleElementReferenceException, ENI_Exception): - self.wait_for_ready_state_complete() - time.sleep(0.06) - element = self.wait_for_element_visible( - selector, by=by, timeout=timeout) - element.clear() - if not new_value.endswith('\n'): - element.send_keys(new_value) - else: - new_value = new_value[:-1] - element.send_keys(new_value) - element.send_keys(Keys.RETURN) - if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: - self.wait_for_ready_state_complete() - except Exception: - exc_message = self.__get_improved_exception_message() - raise Exception(exc_message) - if (retry and element.get_attribute('value') != new_value and ( - not new_value.endswith('\n'))): - logging.debug('update_text() is falling back to JavaScript!') - self.set_value(selector, new_value, by=by) - if self.demo_mode: - if self.driver.current_url != pre_action_url: - self.__demo_mode_pause_if_active() - else: - self.__demo_mode_pause_if_active(tiny=True) + def is_element_visible(self, selector, by="css selector"): + """Returns whether the element is visible on the page.""" + if self.__is_cdp_swap_needed(): + return self.cdp.is_element_visible(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 update_text(self, selector, new_value, by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT, retry=False): - """ The shorter version of update_text_value(), which - clears existing text and adds new text into the text field. - We want to keep the old version for backward compatibility. """ - if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - if page_utils.is_xpath_selector(selector): - by = By.XPATH - self.update_text_value(selector, new_value, by=by, - timeout=timeout, retry=retry) + def is_element_clickable(self, selector, 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_clickable(selector) + return page_actions.is_element_clickable(self.driver, selector, by) - def is_element_present(self, selector, by=By.CSS_SELECTOR): - if page_utils.is_xpath_selector(selector): - by = By.XPATH - if page_utils.is_link_text_selector(selector): - selector = page_utils.get_link_text_from_selector(selector) - by = By.LINK_TEXT - return page_actions.is_element_present(self.driver, selector, by) + def is_element_enabled(self, selector, 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="body", by="css selector"): + """Returns whether the text substring is visible in the element.""" + if self.__is_cdp_swap_needed(): + return self.cdp.is_text_visible(text, 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_element_visible(self, selector, by=By.CSS_SELECTOR): - if page_utils.is_xpath_selector(selector): - by = By.XPATH - if page_utils.is_link_text_selector(selector): - selector = page_utils.get_link_text_from_selector(selector) - by = By.LINK_TEXT - return page_actions.is_element_visible(self.driver, selector, by) + def is_exact_text_visible(self, text, selector="body", by="css selector"): + """Returns whether the text is exactly equal to the element text. + (Leading and trailing whitespace is ignored in the verification.)""" + if self.__is_cdp_swap_needed(): + return self.cdp.is_exact_text_visible(text, 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_exact_text_visible(text, selector) + return page_actions.is_exact_text_visible( + self.driver, text, selector, by + ) + + def is_non_empty_text_visible(self, selector="body", by="css selector"): + """Returns whether the element has any non-empty text visible. + Whitespace-only text is considered empty text.""" + 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_non_empty_text_visible(selector) + return page_actions.is_non_empty_text_visible( + self.driver, selector, by=by + ) + + def is_attribute_present( + self, selector, attribute, value=None, by="css selector" + ): + """Returns True if the element attribute/value is found. + If the value is not specified, the attribute only needs to exist.""" + if self.__is_cdp_swap_needed(): + return self.cdp.is_attribute_present( + selector, attribute, value=value + ) + 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 + ) def is_link_text_visible(self, link_text): + """Returns whether there's an exact match for the link text.""" self.wait_for_ready_state_complete() time.sleep(0.01) - return page_actions.is_element_visible(self.driver, link_text, - by=By.LINK_TEXT) + return page_actions.is_element_visible( + self.driver, link_text, by="link text" + ) def is_partial_link_text_visible(self, partial_link_text): + """Returns whether there's a substring match for the link text.""" self.wait_for_ready_state_complete() time.sleep(0.01) - return page_actions.is_element_visible(self.driver, partial_link_text, - by=By.PARTIAL_LINK_TEXT) + return page_actions.is_element_visible( + self.driver, partial_link_text, by="partial link text" + ) - def is_text_visible(self, text, selector="html", by=By.CSS_SELECTOR): + def is_link_text_present(self, link_text): + """Returns True if the link text appears in the HTML of the page. + The element doesn't need to be visible, + such as elements hidden inside a dropdown selection.""" self.wait_for_ready_state_complete() - time.sleep(0.01) - if page_utils.is_xpath_selector(selector): - by = By.XPATH - if page_utils.is_link_text_selector(selector): - selector = page_utils.get_link_text_from_selector(selector) - by = By.LINK_TEXT - return page_actions.is_text_visible(self.driver, text, selector, by) + soup = self.get_beautiful_soup() + html_links = soup.find_all("a") + for html_link in html_links: + if html_link.text.strip() == link_text.strip(): + return True + return False - def find_elements(self, selector, by=By.CSS_SELECTOR, limit=0): - """ Returns a list of matching WebElements. - If "limit" is set and > 0, will only return that many elements. """ + def is_partial_link_text_present(self, link_text): + """Returns True if the partial link appears in the HTML of the page. + The element doesn't need to be visible, + such as elements hidden inside a dropdown selection.""" self.wait_for_ready_state_complete() - if page_utils.is_xpath_selector(selector): - by = By.XPATH - if page_utils.is_link_text_selector(selector): - selector = page_utils.get_link_text_from_selector(selector) - by = By.LINK_TEXT - elements = self.driver.find_elements(by=by, value=selector) - if limit and limit > 0 and len(elements) > limit: - elements = elements[:limit] - return elements + soup = self.get_beautiful_soup() + html_links = soup.find_all("a") + for html_link in html_links: + if link_text.strip() in html_link.text.strip(): + return True + return False - def find_visible_elements(self, selector, by=By.CSS_SELECTOR, limit=0): - """ Returns a list of matching WebElements that are visible. - If "limit" is set and > 0, will only return that many elements. """ + def get_link_attribute(self, link_text, attribute, hard_fail=True): + """Finds a link by link text and then returns the attribute's value. + If the link text or attribute cannot be found, an exception will + get raised if hard_fail is True (otherwise None is returned).""" self.wait_for_ready_state_complete() - if page_utils.is_xpath_selector(selector): - by = By.XPATH - if page_utils.is_link_text_selector(selector): - selector = page_utils.get_link_text_from_selector(selector) - by = By.LINK_TEXT - v_elems = page_actions.find_visible_elements(self.driver, selector, by) - if limit and limit > 0 and len(v_elems) > limit: - v_elems = v_elems[:limit] - return v_elems - - def click_visible_elements(self, selector, by=By.CSS_SELECTOR, limit=0): - """ Finds all matching page elements and clicks visible ones in order. - If a click reloads or opens a new page, the clicking will stop. - Works best for actions such as clicking all checkboxes on a page. - Example: self.click_visible_elements('input[type="checkbox"]') - If "limit" is set and > 0, will only click that many elements. """ - elements = self.find_elements(selector, by=by) - count = 0 - click_count = 0 - for element in elements: - if limit and limit > 0 and click_count >= limit: - return - count += 1 - if count == 1: - self.wait_for_ready_state_complete() - if self.is_element_visible(selector, by=by): - self.click(selector, by=by) - click_count += 1 + soup = self.get_beautiful_soup() + html_links = soup.find_all("a") + for html_link in html_links: + if html_link.text.strip() == link_text.strip(): + if html_link.has_attr(attribute): + attribute_value = html_link.get(attribute) + return attribute_value + if hard_fail: + raise Exception( + "Unable to find attribute {%s} from link text {%s}!" + % (attribute, link_text) + ) + else: + return None + if hard_fail: + raise Exception("Link text {%s} was not found!" % link_text) + else: + return None + + def get_link_text_attribute(self, link_text, attribute, hard_fail=True): + """Same as self.get_link_attribute() + Finds a link by link text and then returns the attribute's value. + If the link text or attribute cannot be found, an exception will + get raised if hard_fail is True (otherwise None is returned).""" + return self.get_link_attribute(link_text, attribute, hard_fail) + + def get_partial_link_text_attribute( + self, link_text, attribute, hard_fail=True + ): + """Finds a link by partial link text and then returns the attribute's + value. If the partial link text or attribute cannot be found, an + exception will get raised if hard_fail is True (otherwise None + is returned).""" + self.wait_for_ready_state_complete() + soup = self.get_beautiful_soup() + html_links = soup.find_all("a") + for html_link in html_links: + if link_text.strip() in html_link.text.strip(): + if html_link.has_attr(attribute): + attribute_value = html_link.get(attribute) + return attribute_value + if hard_fail: + raise Exception( + "Unable to find attribute {%s} from " + "partial link text {%s}!" % (attribute, link_text) + ) + else: + return None + if hard_fail: + msg = "Partial Link text {%s} was not found!" % link_text + page_actions.timeout_exception("LinkTextNotFoundException", msg) + else: + return None + + def click_link_text(self, link_text, timeout=None): + """This method clicks link text on a page.""" + self.__check_scope() + if not timeout: + timeout = settings.SMALL_TIMEOUT + if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + if self.__is_cdp_swap_needed(): + self.cdp.find_element(link_text, timeout=timeout).click() + return + self.__skip_if_esc() + link_text = self.__get_type_checked_text(link_text) + if self.__is_cdp_swap_needed(): + self.cdp.click_link(link_text) + return + if self.browser == "safari": + if self.demo_mode: + self.wait_for_link_text_present(link_text, timeout=timeout) + try: + self.__jquery_slow_scroll_to(link_text, by="link text") + except Exception: + element = self.wait_for_link_text_visible( + link_text, timeout=timeout + ) + self.__slow_scroll_to_element(element) + o_bs = "" # original_box_shadow + loops = settings.HIGHLIGHTS + selector = self.convert_to_css_selector( + link_text, by="link text" + ) + selector = self.__make_css_match_first_element_only(selector) + try: + selector = re.escape(selector) + selector = self.__escape_quotes_if_needed(selector) + self.__highlight_with_jquery(selector, loops, o_bs) + except Exception: + pass # JQuery probably couldn't load. Skip highlighting. + self.__jquery_click(link_text, by="link text") + return + if not self.is_link_text_present(link_text): + self.wait_for_link_text_present(link_text, timeout=timeout) + if not self.demo_mode and not self.slow_mode: + if self.__needs_minimum_wait(): + time.sleep(0.04) + pre_action_url = None + with suppress(Exception): + pre_action_url = self.driver.current_url + pre_window_count = len(self.driver.window_handles) + try: + element = self.wait_for_link_text_visible(link_text, timeout=0.2) + self.__demo_mode_highlight_if_active(link_text, by="link text") + try: + self.__element_click(element) + except (Stale_Exception, ENI_Exception, ECI_Exception): + self.wait_for_ready_state_complete() + time.sleep(0.16) + element = self.wait_for_link_text_visible( + link_text, timeout=timeout + ) + self.__element_click(element) + except Exception: + found_css = False + text_id = self.get_link_attribute(link_text, "id", False) + if text_id: + link_css = '[id="%s"]' % link_text + found_css = True + if not found_css: + href = self.__get_href_from_link_text(link_text, False) + if href: + if href.startswith("/") or page_utils.is_valid_url(href): + link_css = '[href="%s"]' % href + found_css = True + if not found_css: + ngclick = self.get_link_attribute(link_text, "ng-click", False) + if ngclick: + link_css = '[ng-click="%s"]' % ngclick + found_css = True + if not found_css: + onclick = self.get_link_attribute(link_text, "onclick", False) + if onclick: + link_css = '[onclick="%s"]' % onclick + found_css = True + success = False + if found_css: + if self.is_element_visible(link_css): + self.click(link_css) + success = True + else: + # The link text might be hidden under a dropdown menu + success = self.__click_dropdown_link_text( + link_text, link_css + ) + if not success: + element = self.wait_for_link_text_visible( + link_text, timeout=settings.MINI_TIMEOUT + ) + self.__element_click(element) + latest_window_count = len(self.driver.window_handles) + if ( + latest_window_count > pre_window_count + and ( + self.recorder_mode + or ( + settings.SWITCH_TO_NEW_TABS_ON_CLICK + and self.driver.current_url == pre_action_url + ) + ) + ): + self.__switch_to_newest_window_if_not_blank() + elif ( + latest_window_count == pre_window_count - 1 + and latest_window_count > 0 + ): + # If a click closes the active window, + # switch to the last one if it exists. + self.switch_to_window(-1) + if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: + with suppress(Exception): + self.wait_for_ready_state_complete() + if self.demo_mode: + if self.driver.current_url != pre_action_url: + if not js_utils.is_jquery_activated(self.driver): + js_utils.add_js_link(self.driver, constants.JQuery.MIN_JS) + self.__demo_mode_pause_if_active() else: + self.__demo_mode_pause_if_active(tiny=True) + elif self.slow_mode: + self.__slow_mode_pause_if_active() + elif self.__needs_minimum_wait(): + time.sleep(0.04) + + def click_partial_link_text(self, partial_link_text, timeout=None): + """This method clicks the partial link text on a page.""" + self.__check_scope() + if not timeout: + timeout = settings.SMALL_TIMEOUT + if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + partial_link_text = self.__get_type_checked_text(partial_link_text) + if self.__is_cdp_swap_needed(): + self.cdp.find_element(partial_link_text, timeout=timeout).click() + return + if not self.is_partial_link_text_present(partial_link_text): + self.wait_for_partial_link_text_present( + partial_link_text, timeout=timeout + ) + pre_action_url = None + with suppress(Exception): + pre_action_url = self.driver.current_url + pre_window_count = len(self.driver.window_handles) + try: + element = self.wait_for_partial_link_text( + partial_link_text, timeout=0.2 + ) + self.__demo_mode_highlight_if_active( + partial_link_text, by="link text" + ) + try: + self.__element_click(element) + except (Stale_Exception, ENI_Exception, ECI_Exception): self.wait_for_ready_state_complete() - try: - if element.is_displayed(): + time.sleep(0.16) + element = self.wait_for_partial_link_text( + partial_link_text, timeout=timeout + ) + self.__element_click(element) + except Exception: + found_css = False + text_id = self.get_partial_link_text_attribute( + partial_link_text, "id", False + ) + if text_id: + link_css = '[id="%s"]' % partial_link_text + found_css = True + if not found_css: + href = self.__get_href_from_partial_link_text( + partial_link_text, False + ) + if href: + if href.startswith("/") or page_utils.is_valid_url(href): + link_css = '[href="%s"]' % href + found_css = True + if not found_css: + ngclick = self.get_partial_link_text_attribute( + partial_link_text, "ng-click", False + ) + if ngclick: + link_css = '[ng-click="%s"]' % ngclick + found_css = True + if not found_css: + onclick = self.get_partial_link_text_attribute( + partial_link_text, "onclick", False + ) + if onclick: + link_css = '[onclick="%s"]' % onclick + found_css = True + success = False + if found_css: + if self.is_element_visible(link_css): + self.click(link_css) + success = True + else: + # The link text might be hidden under a dropdown menu + success = self.__click_dropdown_partial_link_text( + partial_link_text, link_css + ) + if not success: + element = self.wait_for_partial_link_text( + partial_link_text, timeout=settings.MINI_TIMEOUT + ) + self.__element_click(element) + latest_window_count = len(self.driver.window_handles) + if ( + latest_window_count > pre_window_count + and ( + self.recorder_mode + or ( + settings.SWITCH_TO_NEW_TABS_ON_CLICK + and self.driver.current_url == pre_action_url + ) + ) + ): + self.__switch_to_newest_window_if_not_blank() + elif ( + latest_window_count == pre_window_count - 1 + and latest_window_count > 0 + ): + # If a click closes the active window, + # switch to the last one if it exists. + self.switch_to_window(-1) + if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: + with suppress(Exception): + self.wait_for_ready_state_complete() + if self.demo_mode: + if self.driver.current_url != pre_action_url: + if not js_utils.is_jquery_activated(self.driver): + js_utils.add_js_link(self.driver, constants.JQuery.MIN_JS) + self.__demo_mode_pause_if_active() + else: + self.__demo_mode_pause_if_active(tiny=True) + elif self.slow_mode: + self.__slow_mode_pause_if_active() + + def get_text(self, selector="body", by="css selector", timeout=None): + self.__check_scope() + if not timeout: + timeout = settings.LARGE_TIMEOUT + if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + selector, by = self.__recalculate_selector(selector, by) + if self.__is_cdp_swap_needed(): + return self.cdp.get_text(selector, timeout=timeout) + if self.__is_shadow_selector(selector): + return self.__get_shadow_text(selector, timeout) + self.wait_for_ready_state_complete() + time.sleep(0.01) + element = page_actions.wait_for_element_visible( + self.driver, selector, by, timeout + ) + try: + element_text = element.text + if self.browser == "safari": + if element.tag_name.lower() in ["input", "textarea"]: + element_text = element.get_attribute("value") + else: + element_text = element.get_attribute("innerText") + elif element.tag_name.lower() in ["input", "textarea"]: + element_text = element.get_property("value") + except (Stale_Exception, ENI_Exception, TimeoutException): + self.wait_for_ready_state_complete() + time.sleep(0.14) + element = page_actions.wait_for_element_visible( + self.driver, selector, by, timeout + ) + element_text = element.text + if self.browser == "safari": + if element.tag_name.lower() in ["input", "textarea"]: + element_text = element.get_attribute("value") + else: + element_text = element.get_attribute("innerText") + elif element.tag_name.lower() in ["input", "textarea"]: + element_text = element.get_property("value") + return element_text + + def get_attribute( + self, + selector, + attribute, + by="css selector", + timeout=None, + hard_fail=True, + ): + """This method uses JavaScript to get the value of an attribute. + If the attribute doesn't exist or isn't found, an exception will + get raised if hard_fail is True (otherwise None is returned).""" + self.__check_scope() + if not timeout: + timeout = settings.LARGE_TIMEOUT + if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + selector, by = self.__recalculate_selector(selector, by) + if self.__is_cdp_swap_needed(): + if hard_fail: + return self.cdp.get_element_attribute(selector, attribute) + else: + return self.cdp.get_attribute(selector, attribute) + self.wait_for_ready_state_complete() + time.sleep(0.01) + if self.__is_shadow_selector(selector): + return self.__get_shadow_attribute( + selector, attribute, timeout=timeout + ) + element = page_actions.wait_for_element_present( + self.driver, selector, by, timeout + ) + try: + attribute_value = element.get_attribute(attribute) + except (Stale_Exception, ENI_Exception, TimeoutException): + self.wait_for_ready_state_complete() + time.sleep(0.14) + element = page_actions.wait_for_element_present( + self.driver, selector, by, timeout + ) + attribute_value = element.get_attribute(attribute) + if attribute_value is not None: + return attribute_value + else: + if hard_fail: + raise Exception( + "Element {%s} has no attribute {%s}!" + % (selector, attribute) + ) + else: + return None + + def set_attribute( + self, + selector, + attribute, + value, + by="css selector", + timeout=None, + scroll=False, + ): + """This method uses JavaScript to set/update an attribute. + Only the first matching selector from querySelector() is used.""" + self.__check_scope() + if not timeout: + timeout = settings.LARGE_TIMEOUT + if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + selector, by = self.__recalculate_selector(selector, by) + original_attribute = attribute + original_value = value + if scroll and self.is_element_visible(selector, by=by): + with suppress(Exception): + self.scroll_to(selector, by=by, timeout=timeout) + attribute = re.escape(attribute) + attribute = self.__escape_quotes_if_needed(attribute) + value = re.escape(value) + value = self.__escape_quotes_if_needed(value) + css_selector = self.convert_to_css_selector(selector, by=by) + css_selector = re.escape(css_selector) # Add "\\" to special chars + css_selector = self.__escape_quotes_if_needed(css_selector) + script = ( + """document.querySelector('%s').setAttribute('%s','%s');""" + % (css_selector, attribute, value) + ) + self.execute_script(script) + if self.recorder_mode and self.__current_url_is_recordable(): + if self.get_session_storage_item("pause_recorder") == "no": + time_stamp = self.execute_script("return Date.now();") + origin = self.get_origin() + sele_attr_val = [selector, original_attribute, original_value] + action = ["s_at_", sele_attr_val, origin, time_stamp] + self.__extra_actions.append(action) + + def set_attributes(self, selector, attribute, value, by="css selector"): + """This method uses JavaScript to set/update a common attribute. + All matching selectors from querySelectorAll() are used. + Example => (Make all links on a website redirect to Google): + self.set_attributes("a", "href", "https://google.com")""" + self.__check_scope() + selector, by = self.__recalculate_selector(selector, by) + if self.__is_cdp_swap_needed(): + self.cdp.set_attributes(selector, attribute, value) + return + original_attribute = attribute + original_value = value + attribute = re.escape(attribute) + attribute = self.__escape_quotes_if_needed(attribute) + value = re.escape(value) + value = self.__escape_quotes_if_needed(value) + css_selector = self.convert_to_css_selector(selector, by=by) + css_selector = re.escape(css_selector) # Add "\\" to special chars + css_selector = self.__escape_quotes_if_needed(css_selector) + script = """var $elements = document.querySelectorAll('%s'); + var index = 0, length = $elements.length; + for(; index < length; index++){ + $elements[index].setAttribute('%s','%s');}""" % ( + css_selector, + attribute, + value, + ) + with suppress(Exception): + self.execute_script(script) + if self.recorder_mode and self.__current_url_is_recordable(): + if self.get_session_storage_item("pause_recorder") == "no": + time_stamp = self.execute_script("return Date.now();") + origin = self.get_origin() + sele_attr_val = [selector, original_attribute, original_value] + action = ["s_ats", sele_attr_val, origin, time_stamp] + self.__extra_actions.append(action) + + def set_attribute_all(self, selector, attribute, value, by="css selector"): + """Same as set_attributes(), but using querySelectorAll naming scheme. + This method uses JavaScript to set/update a common attribute. + All matching selectors from querySelectorAll() are used. + Example => (Make all links on a website redirect to Google): + self.set_attribute_all("a", "href", "https://google.com")""" + self.set_attributes(selector, attribute, value, by=by) + + def remove_attribute( + self, selector, attribute, by="css selector", timeout=None + ): + """This method uses JavaScript to remove an attribute. + Only the first matching selector from querySelector() is used.""" + self.__check_scope() + if not timeout: + timeout = settings.LARGE_TIMEOUT + if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + selector, by = self.__recalculate_selector(selector, by) + if self.is_element_visible(selector, by=by): + with suppress(Exception): + self.scroll_to(selector, by=by, timeout=timeout) + attribute = re.escape(attribute) + attribute = self.__escape_quotes_if_needed(attribute) + css_selector = self.convert_to_css_selector(selector, by=by) + css_selector = re.escape(css_selector) # Add "\\" to special chars + css_selector = self.__escape_quotes_if_needed(css_selector) + script = """document.querySelector('%s').removeAttribute('%s');""" % ( + css_selector, + attribute, + ) + self.execute_script(script) + + def remove_attributes(self, selector, attribute, by="css selector"): + """This method uses JavaScript to remove a common attribute. + All matching selectors from querySelectorAll() are used.""" + self.__check_scope() + selector, by = self.__recalculate_selector(selector, by) + attribute = re.escape(attribute) + attribute = self.__escape_quotes_if_needed(attribute) + css_selector = self.convert_to_css_selector(selector, by=by) + css_selector = re.escape(css_selector) # Add "\\" to special chars + css_selector = self.__escape_quotes_if_needed(css_selector) + script = """var $elements = document.querySelectorAll('%s'); + var index = 0, length = $elements.length; + for(; index < length; index++){ + $elements[index].removeAttribute('%s');}""" % ( + css_selector, + attribute, + ) + with suppress(Exception): + self.execute_script(script) + + def internalize_links(self): + """All `target="_blank"` links become `target="_self"`. + This prevents those links from opening in a new tab.""" + if self.__is_cdp_swap_needed(): + self.cdp.internalize_links() + return + self.set_attributes('[target="_blank"]', "target", "_self") + + def get_parent(self, element, by="css selector", timeout=None): + """Returns the parent element. + If element is a string, then finds element first via selector.""" + if self.__is_cdp_swap_needed(): + return self.cdp.get_parent(element) + if isinstance(element, str): + if not timeout: + timeout = settings.LARGE_TIMEOUT + element = self.wait_for_element_present( + element, by=by, timeout=timeout + ) + return element.find_element(by="xpath", value="..") + + def get_property( + self, selector, property, by="css selector", timeout=None + ): + """Returns the property value of an element. + This is not the same as self.get_property_value(), which returns + the value of an element's computed style using a different algorithm. + If no result is found, an empty string (instead of None) is returned. + Example: + html_text = self.get_property(SELECTOR, "textContent") """ + self.__check_scope() + if not timeout: + timeout = settings.LARGE_TIMEOUT + if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + selector, by = self.__recalculate_selector(selector, by) + self.wait_for_ready_state_complete() + time.sleep(0.01) + element = page_actions.wait_for_element_present( + self.driver, selector, by, timeout + ) + try: + property_value = element.get_property(property) + except (Stale_Exception, ENI_Exception, TimeoutException): + self.wait_for_ready_state_complete() + time.sleep(0.14) + element = page_actions.wait_for_element_present( + self.driver, selector, by, timeout + ) + property_value = element.get_property(property) + if not property_value: + return "" + return property_value + + def get_text_content( + self, selector="body", by="css selector", timeout=None + ): + """Returns the text that appears in the HTML for an element. + This is different from "self.get_text(selector, by="css selector")" + because that only returns the visible text on a page for an element, + rather than the HTML text that's being returned from this method.""" + self.__check_scope() + return self.get_property( + selector, property="textContent", by=by, timeout=timeout + ) + + def get_property_value( + self, selector, property, by="css selector", timeout=None + ): + """Returns the property value of a page element's computed style. + Example: + opacity = self.get_property_value("html body a", "opacity") + self.assertTrue(float(opacity) > 0, "Element not visible!") """ + self.__check_scope() + if not timeout: + timeout = settings.LARGE_TIMEOUT + if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + selector, by = self.__recalculate_selector(selector, by) + self.wait_for_ready_state_complete() + element = page_actions.wait_for_element_present( + self.driver, selector, by, timeout + ) + try: + selector = self.convert_to_css_selector(selector, by=by) + except Exception: + # If can't convert to CSS_Selector for JS, use element directly + script = ( + """var $elm = arguments[0]; + $val = window.getComputedStyle($elm).getPropertyValue('%s'); + return $val;""" % property + ) + value = self.execute_script(script, element) + if value is not None: + return value + else: + return "" + selector = re.escape(selector) + selector = self.__escape_quotes_if_needed(selector) + script = """var $elm = document.querySelector('%s'); + $val = window.getComputedStyle($elm).getPropertyValue('%s'); + return $val;""" % (selector, property) + value = self.execute_script(script) + if value is not None: + return value + else: + return "" # Return an empty string if the property doesn't exist + + def get_image_url(self, selector, by="css selector", timeout=None): + """Extracts the URL from an image element on the page.""" + self.__check_scope() + if not timeout: + timeout = settings.LARGE_TIMEOUT + if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + return self.get_attribute( + selector, attribute="src", by=by, timeout=timeout + ) + + def find_elements(self, selector, by="css selector", limit=0): + """Returns a list of matching WebElements. + Elements could be either hidden or visible on the page. + If "limit" is set and > 0, will only return that many elements.""" + selector, by = self.__recalculate_selector(selector, by) + if self.__is_cdp_swap_needed(): + elements = self.cdp.select_all(selector) + if limit and limit > 0 and len(elements) > limit: + elements = elements[:limit] + return elements + self.wait_for_ready_state_complete() + time.sleep(0.05) + elements = self.driver.find_elements(by=by, value=selector) + if limit and limit > 0 and len(elements) > limit: + elements = elements[:limit] + return elements + + def find_visible_elements(self, selector, by="css selector", limit=0): + """Returns a list of matching WebElements that are visible. + If "limit" is set and > 0, will only return that many elements.""" + selector, by = self.__recalculate_selector(selector, by) + if self.__is_cdp_swap_needed(): + elements = self.cdp.find_visible_elements(selector) + if limit and limit > 0 and len(elements) > limit: + elements = elements[:limit] + return elements + self.wait_for_ready_state_complete() + time.sleep(0.05) + return page_actions.find_visible_elements( + self.driver, selector, by, limit + ) + + def click_visible_elements( + self, selector, by="css selector", limit=0, timeout=None, scroll=True + ): + """Finds all matching page elements and clicks visible ones in order. + If a click reloads or opens a new page, the clicking will stop. + If no matching elements appear, an Exception will be raised. + If "limit" is set and > 0, will only click that many elements. + Also clicks elements that become visible from previous clicks. + Works best for actions such as clicking all checkboxes on a page. + Example: self.click_visible_elements('input[type="checkbox"]')""" + self.__check_scope() + if not timeout: + timeout = settings.SMALL_TIMEOUT + if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + selector, by = self.__recalculate_selector(selector, by) + if self.__is_cdp_swap_needed(): + self.cdp.click_visible_elements(selector, limit, scroll=scroll) + return + self.wait_for_ready_state_complete() + if self.__needs_minimum_wait(): + time.sleep(0.12) + if self.undetectable: + time.sleep(0.06) + element = self.wait_for_element_present( + selector, by=by, timeout=timeout + ) + with suppress(Exception): + # If the first element isn't visible, wait a little. + if not element.is_displayed(): + time.sleep(0.16) + if self.undetectable: + time.sleep(0.06) + elements = self.find_elements(selector, by=by) + pre_action_url = None + with suppress(Exception): + pre_action_url = self.driver.current_url + pre_window_count = len(self.driver.window_handles) + click_count = 0 + for element in elements: + if limit and limit > 0 and click_count >= limit: + return + try: + if element.is_displayed(): + if scroll: self.__scroll_to_element(element) + if self.browser == "safari": + self.execute_script("arguments[0].click();", element) + else: element.click() - click_count += 1 - except (StaleElementReferenceException, ENI_Exception): + click_count += 1 self.wait_for_ready_state_complete() - time.sleep(0.05) - try: - if element.is_displayed(): + except ECI_Exception: + continue # (Overlay likely) + except (Stale_Exception, ENI_Exception): + self.wait_for_ready_state_complete() + time.sleep(0.12) + try: + if element.is_displayed(): + if scroll: self.__scroll_to_element(element) + if self.browser == "safari": + self.execute_script( + "arguments[0].click();", element + ) + else: element.click() - click_count += 1 - except (StaleElementReferenceException, ENI_Exception): - return # Probably on new page / Elements are all stale + click_count += 1 + self.wait_for_ready_state_complete() + except (Stale_Exception, ENI_Exception): + latest_window_count = len(self.driver.window_handles) + if ( + latest_window_count > pre_window_count + and ( + self.recorder_mode + or ( + settings.SWITCH_TO_NEW_TABS_ON_CLICK + and self.driver.current_url == pre_action_url + ) + ) + ): + self.__switch_to_newest_window_if_not_blank() + return # Probably on new page / Elements are all stale + latest_window_count = len(self.driver.window_handles) + if ( + latest_window_count > pre_window_count + and ( + self.recorder_mode + or ( + settings.SWITCH_TO_NEW_TABS_ON_CLICK + and self.driver.current_url == pre_action_url + ) + ) + ): + self.__switch_to_newest_window_if_not_blank() + + def click_nth_visible_element( + self, selector, number, by="css selector", timeout=None, scroll=True + ): + """Finds all matching page elements and clicks the nth visible one. + Example: self.click_nth_visible_element('[type="checkbox"]', 5) + (Clicks the 5th visible checkbox on the page.)""" + self.__check_scope() + if not timeout: + timeout = settings.SMALL_TIMEOUT + if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + selector, by = self.__recalculate_selector(selector, by) + if self.__is_cdp_swap_needed(): + self.cdp.click_nth_visible_element(selector, number, scroll=scroll) + return + self.wait_for_ready_state_complete() + self.wait_for_element_present(selector, by=by, timeout=timeout) + elements = self.find_visible_elements(selector, by=by) + if len(elements) < number: + raise Exception( + "Not enough matching {%s} elements of type {%s} to " + "click number %s!" % (selector, by, number) + ) + number = number - 1 + if number < 0: + number = 0 + element = elements[number] + pre_action_url = None + with suppress(Exception): + pre_action_url = self.driver.current_url + pre_window_count = len(self.driver.window_handles) + try: + if scroll: + self.__scroll_to_element(element) + self.__element_click(element) + except (Stale_Exception, ENI_Exception, ECI_Exception): + time.sleep(0.12) + self.wait_for_ready_state_complete() + self.wait_for_element_present(selector, by=by, timeout=timeout) + elements = self.find_visible_elements(selector, by=by) + if len(elements) < number: + raise Exception( + "Not enough matching {%s} elements of type {%s} to " + "click number %s!" % (selector, by, number) + ) + number = number - 1 + if number < 0: + number = 0 + element = elements[number] + self.__element_click(element) + latest_window_count = len(self.driver.window_handles) + if ( + latest_window_count > pre_window_count + and ( + self.recorder_mode + or ( + settings.SWITCH_TO_NEW_TABS_ON_CLICK + and self.driver.current_url == pre_action_url + ) + ) + ): + self.__switch_to_newest_window_if_not_blank() + + def click_if_visible( + self, selector, by="css selector", timeout=0, scroll=True + ): + """If the page selector exists and is visible, clicks on the element. + This method only clicks on the first matching element found. + Use click_visible_elements() to click all matching elements. + If a "timeout" is provided, waits that long for the element + to appear before giving up and returning without a click().""" + if self.__is_cdp_swap_needed(): + self.cdp.click_if_visible(selector, timeout=timeout, scroll=scroll) + return + self.wait_for_ready_state_complete() + if self.is_element_visible(selector, by=by): + self.click(selector, by=by, scroll=scroll) + elif timeout > 0: + with suppress(Exception): + self.wait_for_element_visible( + selector, by=by, timeout=timeout + ) + self.sleep(0.2) + if self.is_element_visible(selector, by=by): + self.click(selector, by=by, scroll=scroll) + + def click_active_element(self): + if self.__is_cdp_swap_needed(): + self.cdp.click_active_element() + return + self.wait_for_ready_state_complete() + pre_action_url = None + with suppress(Exception): + pre_action_url = self.driver.current_url + pre_window_count = len(self.driver.window_handles) + if self.recorder_mode: + selector = js_utils.get_active_element_css(self.driver) + self.click(selector) + return + else: + self.execute_script("document.activeElement.click();") + latest_window_count = len(self.driver.window_handles) + if ( + latest_window_count > pre_window_count + and ( + self.recorder_mode + or ( + settings.SWITCH_TO_NEW_TABS_ON_CLICK + and self.driver.current_url == pre_action_url + ) + ) + ): + self.__switch_to_newest_window_if_not_blank() + if settings.WAIT_FOR_RSC_ON_CLICKS: + self.wait_for_ready_state_complete() + else: + # A smaller subset of self.wait_for_ready_state_complete() + self.wait_for_angularjs(timeout=settings.MINI_TIMEOUT) + if self.driver.current_url != pre_action_url: + self.__ad_block_as_needed() + self.__disable_beforeunload_as_needed() + if self.demo_mode: + if self.driver.current_url != pre_action_url: + if not js_utils.is_jquery_activated(self.driver): + js_utils.add_js_link(self.driver, constants.JQuery.MIN_JS) + self.__demo_mode_pause_if_active() + else: + self.__demo_mode_pause_if_active(tiny=True) + elif self.slow_mode: + self.__slow_mode_pause_if_active() + + def click_with_offset( + self, + selector, + x, + y, + by="css selector", + mark=None, + timeout=None, + center=None, + scroll=True, + ): + """Click an element at an {X,Y}-offset location. + {0,0} is the top-left corner of the element. + If center==True, {0,0} becomes the center of the element. + If mark==True, will draw a dot at location. (Useful for debugging) + In Demo Mode, mark becomes True unless set to False. (Default: None)""" + self.__check_scope() + self.__click_with_offset( + selector, + x, + y, + by=by, + double=False, + mark=mark, + timeout=timeout, + center=center, + scroll=scroll, + ) + + def double_click_with_offset( + self, + selector, + x, + y, + by="css selector", + mark=None, + timeout=None, + center=None, + scroll=True, + ): + """Double click an element at an {X,Y}-offset location. + {0,0} is the top-left corner of the element. + If center==True, {0,0} becomes the center of the element. + If mark==True, will draw a dot at location. (Useful for debugging) + In Demo Mode, mark becomes True unless set to False. (Default: None)""" + self.__check_scope() + self.__click_with_offset( + selector, + x, + y, + by=by, + double=True, + mark=mark, + timeout=timeout, + center=center, + scroll=scroll, + ) + + def is_checked(self, selector, by="css selector", timeout=None): + """Determines if a checkbox or a radio button element is checked. + Returns True if the element is checked. + Returns False if the element is not checked. + If the element is not present on the page, raises an exception. + If the element is not a checkbox or radio, raises an exception.""" + self.__check_scope() + if not timeout: + timeout = settings.SMALL_TIMEOUT + if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + selector, by = self.__recalculate_selector(selector, by) + if self.__is_cdp_swap_needed(): + return self.cdp.is_checked(selector) + kind = self.get_attribute(selector, "type", by=by, timeout=timeout) + if kind != "checkbox" and kind != "radio": + raise Exception("Expecting a checkbox or a radio button element!") + return bool( + self.get_attribute( + selector, "checked", by=by, timeout=timeout, hard_fail=False + ) + ) + + def is_selected(self, selector, by="css selector", timeout=None): + """Same as is_checked()""" + return self.is_checked(selector, by=by, timeout=timeout) + + def check_if_unchecked(self, selector, by="css selector"): + """If a checkbox or radio button is not checked, will check it.""" + self.__check_scope() + selector, by = self.__recalculate_selector(selector, by) + if self.__is_cdp_swap_needed(): + self.cdp.check_if_unchecked(selector) + return + if not self.is_checked(selector, by=by): + if self.is_element_visible(selector, by=by): + self.click(selector, by=by) + else: + element = self.wait_for_element_present(selector, by=by) + opacity = self.execute_script( + 'return arguments[0].style.opacity;', element + ) + # Handle switches that sit on checkboxes with zero opacity: + # Change the opacity a bit to allow the click to succeed. + with suppress(Exception): + self.execute_script( + 'arguments[0].style.opacity="0.001";', element + ) + if self.is_element_visible(selector, by=by): + self.click(selector, by=by) + else: + selector = self.convert_to_css_selector(selector, by=by) + self.__dont_record_js_click = True + self.js_click(selector, by="css selector") + self.__dont_record_js_click = False + with suppress(Exception): + self.execute_script( + 'arguments[0].style.opacity="arguments[1]";', + element, + opacity, + ) + + def select_if_unselected(self, selector, by="css selector"): + """Same as check_if_unchecked()""" + self.check_if_unchecked(selector, by=by) + + def uncheck_if_checked(self, selector, by="css selector"): + """If a checkbox is checked, will uncheck it.""" + self.__check_scope() + selector, by = self.__recalculate_selector(selector, by) + if self.__is_cdp_swap_needed(): + self.cdp.uncheck_if_checked(selector) + return + if self.is_checked(selector, by=by): + if self.is_element_visible(selector, by=by): + self.click(selector, by=by) + else: + element = self.wait_for_element_present(selector, by=by) + opacity = self.execute_script( + 'return arguments[0].style.opacity;', element + ) + # Handle switches that sit on checkboxes with zero opacity: + # Change the opacity a bit to allow the click to succeed. + with suppress(Exception): + self.execute_script( + 'arguments[0].style.opacity="0.001";', element + ) + if self.is_element_visible(selector, by=by): + self.click(selector, by=by) + else: + selector = self.convert_to_css_selector(selector, by=by) + self.__dont_record_js_click = True + self.js_click(selector, by="css selector") + self.__dont_record_js_click = False + with suppress(Exception): + self.execute_script( + 'arguments[0].style.opacity="arguments[1]";', + element, + opacity, + ) + + def unselect_if_selected(self, selector, by="css selector"): + """Same as uncheck_if_checked()""" + self.uncheck_if_checked(selector, by=by) + + def is_element_in_an_iframe(self, selector, by="css selector"): + """Returns True if the selector's element is located in an iframe. + Otherwise returns False.""" + self.__check_scope() + selector, by = self.__recalculate_selector(selector, by) + if self.is_element_present(selector, by=by): + return False + soup = self.get_beautiful_soup() + iframe_list = soup.select("iframe") + for iframe in iframe_list: + iframe_identifier = None + if iframe.has_attr("name") and len(iframe["name"]) > 0: + iframe_identifier = iframe["name"] + elif iframe.has_attr("id") and len(iframe["id"]) > 0: + iframe_identifier = iframe["id"] + elif iframe.has_attr("class") and len(iframe["class"]) > 0: + iframe_class = " ".join(iframe["class"]) + iframe_identifier = '[class="%s"]' % iframe_class + else: + continue + self.switch_to_frame(iframe_identifier) + if self.is_element_present(selector, by=by): + self.switch_to_default_content() + return True + self.switch_to_default_content() + return False + + def switch_to_frame_of_element(self, selector, by="css selector"): + """Set driver control to the iframe containing element (assuming the + element is in a single-nested iframe) and returns the iframe name. + If element is not in an iframe, returns None, and nothing happens. + May not work if multiple iframes are nested within each other.""" + self.wait_for_ready_state_complete() + if self.__needs_minimum_wait(): + time.sleep(0.04) + if self.undetectable: + time.sleep(0.04) + selector, by = self.__recalculate_selector(selector, by) + if self.is_element_present(selector, by=by): + return None + soup = self.get_beautiful_soup() + iframe_list = soup.select("iframe") + for iframe in iframe_list: + iframe_identifier = None + if iframe.has_attr("name") and len(iframe["name"]) > 0: + iframe_identifier = iframe["name"] + elif iframe.has_attr("id") and len(iframe["id"]) > 0: + iframe_identifier = iframe["id"] + elif iframe.has_attr("class") and len(iframe["class"]) > 0: + iframe_class = " ".join(iframe["class"]) + iframe_identifier = '[class="%s"]' % iframe_class + else: + continue + with suppress(Exception): + self.switch_to_frame(iframe_identifier, timeout=1) + if self.__needs_minimum_wait(): + time.sleep(0.02) + if self.is_element_present(selector, by=by): + return iframe_identifier + self.switch_to_default_content() + if self.__needs_minimum_wait(): + time.sleep(0.02) + try: + self.switch_to_frame(selector, timeout=1) + if self.undetectable: + self.__uc_frame_layer += 1 + return selector + except Exception: + if self.is_element_present(selector, by=by): + return "" + raise Exception( + "Could not switch to iframe containing " + "element {%s}!" % selector + ) + + def hover(self, selector, by="css selector", timeout=None): + self.__check_scope() + if not timeout: + timeout = settings.SMALL_TIMEOUT + if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + original_selector = selector + original_by = by + selector, by = self.__recalculate_selector(selector, by) + if self.__is_cdp_swap_needed(): + self.cdp.hover_element(selector) + return + self.wait_for_element_visible( + original_selector, by=original_by, timeout=timeout + ) + self.__demo_mode_highlight_if_active(original_selector, original_by) + self.scroll_to(selector, by=by) + time.sleep(0.05) # Settle down from scrolling before hovering + element = page_actions.hover_on_element(self.driver, selector, by) + if self.recorder_mode and self.__current_url_is_recordable(): + if self.get_session_storage_item("pause_recorder") == "no": + if by == By.XPATH: + selector = original_selector + time_stamp = self.execute_script("return Date.now();") + origin = self.get_origin() + action = ["hover", selector, origin, time_stamp] + self.__extra_actions.append(action) + return element + + def hover_and_click( + self, + hover_selector, + click_selector, + hover_by="css selector", + click_by="css selector", + timeout=None, + js_click=False, + ): + """When you want to hover over an element or dropdown menu, + and then click an element that appears after that.""" + self.__check_scope() + if not timeout: + timeout = settings.SMALL_TIMEOUT + if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + original_selector = hover_selector + original_by = hover_by + hover_selector, hover_by = self.__recalculate_selector( + hover_selector, hover_by + ) + original_click_selector = click_selector + click_selector, click_by = self.__recalculate_selector( + click_selector, click_by + ) + if self.__is_cdp_swap_needed(): + self.cdp.hover_and_click(hover_selector, click_selector) + return + dropdown_element = self.wait_for_element_visible( + original_selector, by=original_by, timeout=timeout + ) + self.__demo_mode_highlight_if_active(original_selector, original_by) + self.scroll_to(hover_selector, by=hover_by) + pre_action_url = None + with suppress(Exception): + pre_action_url = self.driver.current_url + pre_window_count = len(self.driver.window_handles) + if self.recorder_mode and self.__current_url_is_recordable(): + if self.get_session_storage_item("pause_recorder") == "no": + if hover_by == By.XPATH: + hover_selector = original_selector + if click_by == By.XPATH: + click_selector = original_click_selector + time_stamp = self.execute_script("return Date.now();") + origin = self.get_origin() + the_selectors = [hover_selector, click_selector] + action = ["ho_cl", the_selectors, origin, time_stamp] + self.__extra_actions.append(action) + outdated_driver = False + element = None + try: + if self.mobile_emulator: + # On mobile, click to hover the element + dropdown_element.click() + else: + page_actions.hover_element(self.driver, dropdown_element) + except Exception: + outdated_driver = True + element = self.wait_for_element_present( + click_selector, click_by, timeout + ) + if click_by == By.LINK_TEXT: + self.open(self.__get_href_from_link_text(click_selector)) + elif click_by == By.PARTIAL_LINK_TEXT: + self.open( + self.__get_href_from_partial_link_text(click_selector) + ) + else: + self.__dont_record_js_click = True + self.js_click(click_selector, by=click_by) + self.__dont_record_js_click = False + if outdated_driver: + pass # Already did the click workaround + elif self.mobile_emulator: + self.click(click_selector, by=click_by) + elif not outdated_driver: + element = page_actions.hover_and_click( + self.driver, + hover_selector, + click_selector, + hover_by, + click_by, + timeout, + js_click, + ) + latest_window_count = len(self.driver.window_handles) + if ( + latest_window_count > pre_window_count + and ( + self.recorder_mode + or ( + settings.SWITCH_TO_NEW_TABS_ON_CLICK + and self.driver.current_url == pre_action_url + ) + ) + ): + self.__switch_to_newest_window_if_not_blank() + elif self.browser == "safari": + # Release the hover by hovering elsewhere + with suppress(Exception): + page_actions.hover_on_element(self.driver, "body") + if self.demo_mode: + if self.driver.current_url != pre_action_url: + if not js_utils.is_jquery_activated(self.driver): + js_utils.add_js_link(self.driver, constants.JQuery.MIN_JS) + self.__demo_mode_pause_if_active() + else: + self.__demo_mode_pause_if_active(tiny=True) + elif self.slow_mode: + self.__slow_mode_pause_if_active() + return element + + def hover_and_js_click( + self, + hover_selector, + click_selector, + hover_by="css selector", + click_by="css selector", + timeout=None, + ): + self.hover_and_click( + hover_selector=hover_selector, + click_selector=click_selector, + hover_by=hover_by, + click_by=click_by, + timeout=timeout, + js_click=True, + ) + + def hover_and_double_click( + self, + hover_selector, + click_selector, + hover_by="css selector", + click_by="css selector", + timeout=None, + ): + """When you want to hover over an element or dropdown menu, + and then double-click an element that appears after that.""" + self.__check_scope() + if not timeout: + timeout = settings.SMALL_TIMEOUT + if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + original_selector = hover_selector + original_by = hover_by + hover_selector, hover_by = self.__recalculate_selector( + hover_selector, hover_by + ) + hover_selector = self.convert_to_css_selector(hover_selector, hover_by) + hover_by = By.CSS_SELECTOR + click_selector, click_by = self.__recalculate_selector( + click_selector, click_by + ) + dropdown_element = self.wait_for_element_visible( + original_selector, by=original_by, timeout=timeout + ) + self.__demo_mode_highlight_if_active(original_selector, original_by) + self.scroll_to(hover_selector, by=hover_by) + pre_action_url = None + with suppress(Exception): + pre_action_url = self.driver.current_url + pre_window_count = len(self.driver.window_handles) + outdated_driver = False + element = None + try: + page_actions.hover_element(self.driver, dropdown_element) + except Exception: + outdated_driver = True + element = self.wait_for_element_present( + click_selector, click_by, timeout + ) + if click_by == By.LINK_TEXT: + self.open(self.__get_href_from_link_text(click_selector)) + elif click_by == By.PARTIAL_LINK_TEXT: + self.open( + self.__get_href_from_partial_link_text(click_selector) + ) + else: + self.__dont_record_js_click = True + self.js_click(click_selector, click_by) + self.__dont_record_js_click = False + if not outdated_driver: + element = page_actions.hover_element_and_double_click( + self.driver, + dropdown_element, + click_selector, + click_by="css selector", + timeout=timeout, + ) + latest_window_count = len(self.driver.window_handles) + if ( + latest_window_count > pre_window_count + and ( + self.recorder_mode + or ( + settings.SWITCH_TO_NEW_TABS_ON_CLICK + and self.driver.current_url == pre_action_url + ) + ) + ): + self.__switch_to_newest_window_if_not_blank() + if self.demo_mode: + if self.driver.current_url != pre_action_url: + if not js_utils.is_jquery_activated(self.driver): + js_utils.add_js_link(self.driver, constants.JQuery.MIN_JS) + self.__demo_mode_pause_if_active() + else: + self.__demo_mode_pause_if_active(tiny=True) + elif self.slow_mode: + self.__slow_mode_pause_if_active() + return element + + def drag_and_drop( + self, + drag_selector, + drop_selector, + drag_by="css selector", + drop_by="css selector", + timeout=None, + jquery=False, + ): + """Drag-and-drop an element from one selector to another.""" + self.__check_scope() + if not timeout: + timeout = settings.SMALL_TIMEOUT + if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + drag_selector, drag_by = self.__recalculate_selector( + drag_selector, drag_by + ) + drop_selector, drop_by = self.__recalculate_selector( + drop_selector, drop_by + ) + if self.__is_cdp_swap_needed(): + self.cdp.drag_and_drop(drag_selector, drop_selector) + return + drag_element = self.wait_for_element_clickable( + drag_selector, by=drag_by, timeout=timeout + ) + self.__demo_mode_highlight_if_active(drag_selector, drag_by) + drop_element = self.wait_for_element_visible( + drop_selector, by=drop_by, timeout=timeout + ) + self.__demo_mode_highlight_if_active(drop_selector, drop_by) + self.scroll_to(drop_selector, by=drop_by) + drag_selector = self.convert_to_css_selector(drag_selector, drag_by) + drop_selector = self.convert_to_css_selector(drop_selector, drop_by) + if not jquery: + drag_and_drop_script = js_utils.get_js_drag_and_drop_script() + self.execute_script( + drag_and_drop_script, drag_element, drop_element, 0, 0, 1, None + ) + else: + drag_and_drop_script = js_utils.get_drag_and_drop_script() + self.safe_execute_script( + drag_and_drop_script + + ( + "$('%s').simulateDragDrop(" + "{dropTarget: " + "'%s'});" % (drag_selector, drop_selector) + ) + ) + if self.demo_mode: + self.__demo_mode_pause_if_active() + elif self.slow_mode: + self.__slow_mode_pause_if_active() + return drag_element + + def drag_and_drop_with_offset( + self, selector, x, y, by="css selector", timeout=None + ): + """Drag-and-drop an element to an {X,Y}-offset location.""" + self.__check_scope() + if not timeout: + timeout = settings.SMALL_TIMEOUT + if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + selector, by = self.__recalculate_selector(selector, by) + css_selector = self.convert_to_css_selector(selector, by=by) + element = self.wait_for_element_visible(css_selector, timeout=timeout) + self.__demo_mode_highlight_if_active(css_selector, By.CSS_SELECTOR) + css_selector = re.escape(css_selector) # Add "\\" to special chars + css_selector = self.__escape_quotes_if_needed(css_selector) + script = js_utils.get_drag_and_drop_with_offset_script( + css_selector, x, y + ) + self.execute_script(script) + if self.demo_mode: + self.__demo_mode_pause_if_active() + elif self.slow_mode: + self.__slow_mode_pause_if_active() + return element + + def __element_click(self, element): + self.__check_scope() + if ( + not self.undetectable + or self.uc_cdp_events + or self.__uc_frame_layer > 0 + or not hasattr(element, "uc_click") + or element.tag_name.lower() != "a" + ): + element.click() + else: + try: + href = element.get_attribute("href") + target = element.get_attribute("target") + if len(href) > 0 and target != "_blank": + element.uc_click() + else: + element.click() + time.sleep(0.012) + except Exception: + element.click() + + def __select_option( + self, + dropdown_selector, + option, + dropdown_by="css selector", + option_by="text", + timeout=None, + ): + """Selects an HTML option by option text. + @Params + dropdown_selector - the option by option index. + @Params + dropdown_selector - the option by option value. + @Params + dropdown_selector - the option by specification. - Option specifications are by "text", "index", or "value". - Defaults to "text" if option_by is unspecified or unknown. """ - if page_utils.is_xpath_selector(dropdown_selector): - dropdown_by = By.XPATH - element = self.find_element( - dropdown_selector, by=dropdown_by, timeout=timeout) - self.__demo_mode_highlight_if_active(dropdown_selector, dropdown_by) - pre_action_url = self.driver.current_url - try: - if option_by == "index": - Select(element).select_by_index(option) - elif option_by == "value": - Select(element).select_by_value(option) - else: - Select(element).select_by_visible_text(option) - except (StaleElementReferenceException, ENI_Exception): - self.wait_for_ready_state_complete() - time.sleep(0.05) - element = self.find_element( - dropdown_selector, by=dropdown_by, timeout=timeout) - if option_by == "index": - Select(element).select_by_index(option) - elif option_by == "value": - Select(element).select_by_value(option) - else: - Select(element).select_by_visible_text(option) - if settings.WAIT_FOR_RSC_ON_CLICKS: - self.wait_for_ready_state_complete() - if self.demo_mode: - if self.driver.current_url != pre_action_url: - self.__demo_mode_pause_if_active() - else: - self.__demo_mode_pause_if_active(tiny=True) + ############ + + def wait_for_and_accept_alert(self, timeout=None): + self.__check_scope() + if not timeout: + timeout = settings.LARGE_TIMEOUT + if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + alert = page_actions.wait_for_and_accept_alert(self.driver, timeout) + if self.recorder_mode and self.__current_url_is_recordable(): + if self.get_session_storage_item("pause_recorder") == "no": + time_stamp = self.execute_script("return Date.now();") + origin = self.get_origin() + action = ["acc_a", "", origin, time_stamp] + self.__extra_actions.append(action) + return alert + + def wait_for_and_dismiss_alert(self, timeout=None): + self.__check_scope() + if not timeout: + timeout = settings.LARGE_TIMEOUT + if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + alert = page_actions.wait_for_and_dismiss_alert(self.driver, timeout) + if self.recorder_mode and self.__current_url_is_recordable(): + if self.get_session_storage_item("pause_recorder") == "no": + time_stamp = self.execute_script("return Date.now();") + origin = self.get_origin() + action = ["dis_a", "", origin, time_stamp] + self.__extra_actions.append(action) + return alert + + def wait_for_and_switch_to_alert(self, timeout=None): + self.__check_scope() + if not timeout: + timeout = settings.LARGE_TIMEOUT + if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + return page_actions.wait_for_and_switch_to_alert(self.driver, timeout) + + ############ - def select_option_by_text(self, dropdown_selector, option, - dropdown_by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT): - """ Selects an HTML selector - option - the text of the option """ + def accept_alert(self, timeout=None): + """Same as wait_for_and_accept_alert(), but smaller default T_O""" + self.__check_scope() + if not timeout: + timeout = settings.SMALL_TIMEOUT if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: timeout = self.__get_new_timeout(timeout) - self.__select_option(dropdown_selector, option, - dropdown_by=dropdown_by, option_by="text", - timeout=timeout) - - def select_option_by_index(self, dropdown_selector, option, - dropdown_by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT): - """ Selects an HTML selector - option - the index number of the option """ + alert = page_actions.wait_for_and_accept_alert(self.driver, timeout) + if self.recorder_mode and self.__current_url_is_recordable(): + if self.get_session_storage_item("pause_recorder") == "no": + time_stamp = self.execute_script("return Date.now();") + origin = self.get_origin() + action = ["acc_a", "", origin, time_stamp] + self.__extra_actions.append(action) + return alert + + def dismiss_alert(self, timeout=None): + """Same as wait_for_and_dismiss_alert(), but smaller default T_O""" + self.__check_scope() + if not timeout: + timeout = settings.SMALL_TIMEOUT if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: timeout = self.__get_new_timeout(timeout) - self.__select_option(dropdown_selector, option, - dropdown_by=dropdown_by, option_by="index", - timeout=timeout) - - def select_option_by_value(self, dropdown_selector, option, - dropdown_by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT): - """ Selects an HTML selector - option - the value property of the option """ + alert = page_actions.wait_for_and_dismiss_alert(self.driver, timeout) + if self.recorder_mode and self.__current_url_is_recordable(): + if self.get_session_storage_item("pause_recorder") == "no": + time_stamp = self.execute_script("return Date.now();") + origin = self.get_origin() + action = ["dis_a", "", origin, time_stamp] + self.__extra_actions.append(action) + return alert + + def switch_to_alert(self, timeout=None): + """Same as wait_for_and_switch_to_alert(), but smaller default T_O""" + self.__check_scope() + if not timeout: + timeout = settings.SMALL_TIMEOUT if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: timeout = self.__get_new_timeout(timeout) - self.__select_option(dropdown_selector, option, - dropdown_by=dropdown_by, option_by="value", - timeout=timeout) + return page_actions.wait_for_and_switch_to_alert(self.driver, timeout) ############ - def generate_referral(self, start_page, destination_page): - """ This method opens the start_page, creates a referral link there, - and clicks on that link, which goes to the destination_page. - (This generates real traffic for testing analytics software.) """ - if not page_utils.is_valid_url(destination_page): + def quit_extra_driver(self, driver=None): + """Quits the driver only if it's not the default/initial driver. + If a driver is given, quits that, otherwise quits the active driver. + Raises an Exception if quitting the default/initial driver. + Should only be called if a test has already called get_new_driver(). + Afterwards, self.driver points to the default/initial driver + if self.driver was the one being quit. + ---- + If a test never calls get_new_driver(), this method isn't needed. + SeleniumBase automatically quits browsers after tests have ended. + Even if tests do call get_new_driver(), you don't need to use this + method unless you want to quit extra browsers before a test ends. + ---- + Terminology and important details: + * Active driver: The one self.driver is set to. Used within methods. + * Default/initial driver: The one that is spun up when tests start. + Initially, the active driver and the default driver are the same. + The active driver can change when one of these methods is called: + > self.get_new_driver() + > self.switch_to_default_driver() + > self.switch_to_driver() + > self.quit_extra_driver() """ + self.__check_scope() + if not driver: + driver = self.driver + if type(driver).__name__ == "NoneType": + raise Exception("The driver to quit was a NoneType variable!") + elif ( + not hasattr(driver, "get") + or not hasattr(driver, "name") + or not hasattr(driver, "quit") + or not hasattr(driver, "capabilities") + or not hasattr(driver, "window_handles") + ): + raise Exception("The driver to quit does not match a Driver!") + elif self._reuse_session and driver == self._default_driver: raise Exception( - "Exception: destination_page {%s} is not a valid URL!" - % destination_page) - if start_page: - if not page_utils.is_valid_url(start_page): - raise Exception( - "Exception: start_page {%s} is not a valid URL! " - "(Use an empty string or None to start from current page.)" - % start_page) - self.open(start_page) - time.sleep(0.08) - self.wait_for_ready_state_complete() - referral_link = ('''''' - '''''' - '''Magic Link Button''' % destination_page) - self.execute_script( - '''document.body.outerHTML = \"%s\"''' % referral_link) - self.click( - "a.analytics.referral.test", timeout=2) # Clicks generated button - time.sleep(0.15) + "Cannot quit the initial driver in --reuse-session mode!\n" + "This is done automatically after all tests have ended.\n" + "Use this method only if get_new_driver() has been called." + ) + elif ( + driver == self._default_driver + or (driver in self._drivers_list and len(self._drivers_list) == 1) + ): + raise Exception( + "Cannot quit the default/initial driver!\n" + "This is done automatically at the end of each test.\n" + "Use this method only if get_new_driver() has been called." + ) try: - self.click("html") - time.sleep(0.08) + if ( + not is_windows + or self.browser == "ie" + or driver.service.process + ): + driver.quit() + except AttributeError: + pass except Exception: pass + if driver in self._drivers_list: + self._drivers_list.remove(driver) + if driver in self._drivers_browser_map: + del self._drivers_browser_map[driver] + # If the driver to quit was the active driver, switch drivers + if driver == self.driver: + self.switch_to_default_driver() + try: + self._check_browser() + except Exception: + self._default_driver = self._drivers_list[-1] + self.switch_to_default_driver() - def generate_traffic(self, start_page, destination_page, loops=1): - """ Similar to generate_referral(), but can do multiple loops. """ - for loop in range(loops): - self.generate_referral(start_page, destination_page) - time.sleep(0.05) - - def generate_referral_chain(self, pages): - """ Use this method to chain the action of creating button links on - one website page that will take you to the next page. - (When you want to create a referral to a website for traffic - generation without increasing the bounce rate, you'll want to visit - at least one additional page on that site with a button click.) """ - if not type(pages) is tuple and not type(pages) is list: - raise Exception( - "Exception: Expecting a list of website pages for chaining!") - if len(pages) < 2: - raise Exception( - "Exception: At least two website pages required for chaining!") - for page in pages: - # Find out if any of the web pages are invalid before continuing - if not page_utils.is_valid_url(page): - raise Exception( - "Exception: Website page {%s} is not a valid URL!" % page) - for page in pages: - self.generate_referral(None, page) + ############ - def generate_traffic_chain(self, pages, loops=1): - """ Similar to generate_referral_chain(), but for multiple loops. """ - for loop in range(loops): - self.generate_referral_chain(pages) - time.sleep(0.05) + def __assert_eq(self, *args, **kwargs): + """Minified assert_equal() using only the list diff.""" + minified_exception = None + try: + self.assertEqual(*args, **kwargs) + except Exception as e: + str_e = str(e) + minified_exception = "\nAssertionError:\n" + lines = str_e.split("\n") + countdown = 3 + countdown_on = False + first_differing = False + skip_lines = False + for line in lines: + if countdown_on: + if not skip_lines: + minified_exception += line + "\n" + countdown = countdown - 1 + if countdown == 0: + countdown_on = False + skip_lines = False + elif line.startswith("First differing"): + first_differing = True + countdown_on = True + countdown = 3 + minified_exception += line + "\n" + elif line.startswith("First list"): + countdown_on = True + countdown = 3 + if not first_differing: + minified_exception += line + "\n" + else: + skip_lines = True + elif line.startswith("F"): + countdown_on = True + countdown = 3 + minified_exception += line + "\n" + elif line.startswith(("+", "-")): + minified_exception += line + "\n" + elif line.startswith("?"): + minified_exception += line + "\n" + elif line.strip().startswith("*"): + minified_exception += line + "\n" + if minified_exception: + raise VisualException(minified_exception) + + def _process_visual_baseline_logs(self): + if not ( + (python3_11_or_newer and py311_patch2) + or "--pdb" in sys.argv + ): + return + self.__process_visual_baseline_logs() + + def __process_visual_baseline_logs(self): + """Save copies of baseline PNGs in "./latest_logs" during failures. + Also create a side_by_side.html file for visual comparisons.""" + test_logpath = os.path.join(self.log_path, self.__get_test_id()) + for baseline_copy_tuple in self.__visual_baseline_copies: + baseline_path = baseline_copy_tuple[0] + baseline_copy_name = baseline_copy_tuple[1] + b_c_alt_name = baseline_copy_tuple[2] + latest_png_path = baseline_copy_tuple[3] + latest_copy_name = baseline_copy_tuple[4] + l_c_alt_name = baseline_copy_tuple[5] + baseline_copy_path = os.path.join(test_logpath, baseline_copy_name) + b_c_alt_path = os.path.join(test_logpath, b_c_alt_name) + latest_copy_path = os.path.join(test_logpath, latest_copy_name) + l_c_alt_path = os.path.join(test_logpath, l_c_alt_name) + if len(self.__visual_baseline_copies) == 1: + baseline_copy_path = b_c_alt_path + latest_copy_path = l_c_alt_path + if ( + os.path.exists(baseline_path) + and not os.path.exists(baseline_copy_path) + ): + self.__create_log_path_as_needed(test_logpath) + shutil.copy(baseline_path, baseline_copy_path) + if ( + os.path.exists(latest_png_path) + and not os.path.exists(latest_copy_path) + ): + self.__create_log_path_as_needed(test_logpath) + shutil.copy(latest_png_path, latest_copy_path) + if len(self.__visual_baseline_copies) != 1: + return # Skip the rest when deferred visual asserts are used + the_html = visual_helper.get_sbs_html() + file_path = os.path.join(test_logpath, constants.SideBySide.HTML_FILE) + out_file = open(file_path, mode="w+", encoding="utf-8") + out_file.writelines(the_html) + out_file.close() + + def check_window( + self, + name="default", + level=0, + baseline=False, + check_domain=True, + full_diff=False, + ): + """*** Automated Visual Testing with SeleniumBase *** + + The first time a test calls self.check_window() for a unique "name" + parameter provided, it will set a visual baseline, meaning that it + creates a folder, saves the URL to a file, saves the current window + screenshot to a file, and creates the following three files + with the listed data saved: + tags_level1.txt -> HTML tags from the window + tags_level2.txt -> HTML tags + attributes from the window + tags_level3.txt -> HTML tags + attributes/values from the window + + Baseline folders are named based on the test name and the name + parameter passed to self.check_window(). The same test can store + multiple baseline folders. + + If the baseline is being set/reset, the "level" doesn't matter. + + After the first run of self.check_window(), it will compare the + HTML tags of the latest window to the one from the initial run. + Here's how the level system works: + * level=0 -> + DRY RUN ONLY - Will perform comparisons to the baseline (and + print out any differences that are found) but + won't fail the test even if differences exist. + * level=1 -> + HTML tags are compared to tags_level1.txt + * level=2 -> + HTML tags are compared to tags_level1.txt and + HTML tags/attributes are compared to tags_level2.txt + * level=3 -> + HTML tags are compared to tags_level1.txt and + HTML tags + attributes are compared to tags_level2.txt and + HTML tags + attributes/values are compared to tags_level3.txt + As shown, Level-3 is the most strict, Level-1 is the least strict. + If the comparisons from the latest window to the existing baseline + don't match, the current test will fail, except for Level-0 tests. + + You can reset the visual baseline on the command line by using: + --visual_baseline + As long as "--visual_baseline" is used on the command line while + running tests, the self.check_window() method cannot fail because + it will rebuild the visual baseline rather than comparing the html + tags of the latest run to the existing baseline. If there are any + expected layout changes to a website that you're testing, you'll + need to reset the baseline to prevent unnecessary failures. + + self.check_window() will fail with "Page Domain Mismatch Failure" + if the page domain doesn't match the domain of the baseline, + unless "check_domain" is set to False when calling check_window(). + + If you want to use self.check_window() to compare a web page to + a later version of itself from within the same test run, you can + add the parameter "baseline=True" to the first time you call + self.check_window() in a test to use that as the baseline. This + only makes sense if you're calling self.check_window() more than + once with the same name parameter in the same test. + + If "full_diff" is set to False, the error output will only + include the first differing element in the list comparison. + Set "full_diff" to True if you want to see the full output. + + Automated Visual Testing with self.check_window() is not very + effective for websites that have dynamic content that changes + the layout and structure of web pages. For those, you're much + better off using regular SeleniumBase functional testing. + + Example usage: + self.check_window(name="testing", level=0) + self.check_window(name="xkcd_home", level=1) + self.check_window(name="github_page", level=2) + self.check_window(name="wikipedia_page", level=3) """ + self.wait_for_ready_state_complete() + with suppress(Exception): + self.wait_for_element_visible( + "body", timeout=settings.MINI_TIMEOUT + ) + if self.__needs_minimum_wait(): + time.sleep(0.08) + if level == "0": + level = 0 + if level == "1": + level = 1 + if level == "2": + level = 2 + if level == "3": + level = 3 + if level != 0 and level != 1 and level != 2 and level != 3: + raise Exception('Parameter "level" must be set to 0, 1, 2, or 3!') - ############ + if self.demo_mode: + message = ( + "WARNING: Using check_window() from Demo Mode may lead " + "to unexpected results caused by Demo Mode HTML changes." + ) + logging.info(message) - def wait_for_element_present(self, selector, by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT): - """ Waits for an element to appear in the HTML of a page. - The element does not need be visible (it may be hidden). """ - if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - if page_utils.is_xpath_selector(selector): - by = By.XPATH - if page_utils.is_link_text_selector(selector): - selector = page_utils.get_link_text_from_selector(selector) - by = By.LINK_TEXT - return page_actions.wait_for_element_present( - self.driver, selector, by, timeout) - - def assert_element_present(self, selector, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT): - """ Similar to wait_for_element_present(), but returns nothing. - Waits for an element to appear in the HTML of a page. - The element does not need be visible (it may be hidden). - Returns True if successful. Default timeout = SMALL_TIMEOUT. """ - if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - self.wait_for_element_present(selector, by=by, timeout=timeout) - return True + test_id = self.__get_display_id().split("::")[-1] - # For backwards compatibility, earlier method names of the next - # four methods have remained even though they do the same thing, - # with the exception of assert_*, which won't return the element, - # but like the others, will raise an exception if the call fails. + if not name or len(name) < 1: + name = "default" + name = str(name) - def wait_for_element_visible(self, selector, by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT): - """ Waits for an element to appear in the HTML of a page. - The element must be visible (it cannot be hidden). """ - if page_utils.is_xpath_selector(selector): - by = By.XPATH - if page_utils.is_link_text_selector(selector): - selector = page_utils.get_link_text_from_selector(selector) - by = By.LINK_TEXT - return page_actions.wait_for_element_visible( - self.driver, selector, by, timeout) + visual_helper.visual_baseline_folder_setup() + baseline_dir = constants.VisualBaseline.STORAGE_FOLDER + visual_baseline_path = os.path.join(baseline_dir, test_id, name) + page_url_file = os.path.join(visual_baseline_path, "page_url.txt") + baseline_png = "baseline.png" + baseline_png_path = os.path.join(visual_baseline_path, baseline_png) + latest_png = "latest.png" + latest_png_path = os.path.join(visual_baseline_path, latest_png) + level_1_file = os.path.join(visual_baseline_path, "tags_level_1.txt") + level_2_file = os.path.join(visual_baseline_path, "tags_level_2.txt") + level_3_file = os.path.join(visual_baseline_path, "tags_level_3.txt") - def wait_for_element(self, selector, by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT): - """ The shorter version of wait_for_element_visible() """ - if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - return self.wait_for_element_visible(selector, by=by, timeout=timeout) + set_baseline = False + if baseline or self.visual_baseline: + set_baseline = True + if not os.path.exists(visual_baseline_path): + set_baseline = True + try: + os.makedirs(visual_baseline_path) + except Exception: + pass # Only reachable during multi-threaded test runs + if not os.path.exists(page_url_file): + set_baseline = True + if not os.path.exists(baseline_png_path): + set_baseline = True + if not os.path.exists(level_1_file): + set_baseline = True + if not os.path.exists(level_2_file): + set_baseline = True + if not os.path.exists(level_3_file): + set_baseline = True - def find_element(self, selector, by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT): - """ Same as wait_for_element_visible() - returns the element """ - if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - return self.wait_for_element_visible(selector, by=by, timeout=timeout) + page_url = self.get_current_url() + soup = self.get_beautiful_soup() + html_tags = soup.body.find_all() + level_1 = [[tag.name] for tag in html_tags] + level_1 = json.loads(json.dumps(level_1)) # Tuples become lists + level_2 = [[tag.name, sorted(tag.attrs.keys())] for tag in html_tags] + level_2 = json.loads(json.dumps(level_2)) # Tuples become lists + level_3 = [[tag.name, sorted(tag.attrs.items())] for tag in html_tags] + level_3 = json.loads(json.dumps(level_3)) # Tuples become lists - def assert_element(self, selector, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT): - """ Similar to wait_for_element_visible(), but returns nothing. - As above, will raise an exception if nothing can be found. - Returns True if successful. Default timeout = SMALL_TIMEOUT. """ - if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - self.wait_for_element_visible(selector, by=by, timeout=timeout) + if set_baseline: + self.save_screenshot( + baseline_png, visual_baseline_path, selector="body" + ) + out_file = open(page_url_file, mode="w+", encoding="utf-8") + out_file.writelines(page_url) + out_file.close() + out_file = open(level_1_file, mode="w+", encoding="utf-8") + out_file.writelines(json.dumps(level_1)) + out_file.close() + out_file = open(level_2_file, mode="w+", encoding="utf-8") + out_file.writelines(json.dumps(level_2)) + out_file.close() + out_file = open(level_3_file, mode="w+", encoding="utf-8") + out_file.writelines(json.dumps(level_3)) + out_file.close() - if self.demo_mode: - if page_utils.is_xpath_selector(selector): - by = By.XPATH - if page_utils.is_link_text_selector(selector): - selector = page_utils.get_link_text_from_selector(selector) - by = By.LINK_TEXT - messenger_post = "ASSERT %s: %s" % (by, selector) - self.__highlight_with_assert_success(messenger_post, selector, by) - return True + baseline_path = os.path.join(visual_baseline_path, baseline_png) + baseline_copy_name = "baseline_%s.png" % name + b_c_alt_name = "baseline.png" + latest_copy_name = "baseline_diff_%s.png" % name + l_c_alt_name = "baseline_diff.png" + baseline_copy_tuple = ( + baseline_path, baseline_copy_name, b_c_alt_name, + latest_png_path, latest_copy_name, l_c_alt_name, + ) + self.__visual_baseline_copies.append(baseline_copy_tuple) + + is_level_0_failure = False + if not set_baseline: + self.save_screenshot( + latest_png, visual_baseline_path, selector="body" + ) + f = open(page_url_file, "r") + page_url_data = f.read().strip() + f.close() + f = open(level_1_file, "r") + level_1_data = json.loads(f.read()) + f.close() + f = open(level_2_file, "r") + level_2_data = json.loads(f.read()) + f.close() + f = open(level_3_file, "r") + level_3_data = json.loads(f.read()) + f.close() - def assert_element_visible(self, selector, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT): - """ Same as self.assert_element() - As above, will raise an exception if nothing can be found. """ - if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - self.assert_element(selector, by=by, timeout=timeout) - return True + domain_fail = ( + "\n*\nPage Domain Mismatch Failure: " + "Current Page Domain doesn't match the Page Domain of the " + "Baseline! Can't compare two completely different sites! " + "Run with --visual_baseline to reset the baseline!" + ) + level_1_failure = ( + "\n*\n*** Exception: Visual Diff Failure:\n" + "* HTML tags don't match the baseline!" + ) + level_2_failure = ( + "\n*\n*** Exception: Visual Diff Failure:\n" + "* HTML tag attribute names don't match the baseline!" + ) + level_3_failure = ( + "\n*\n*** Exception: Visual Diff Failure:\n" + "* HTML tag attribute values don't match the baseline!" + ) - # For backwards compatibility, earlier method names of the next - # four methods have remained even though they do the same thing, - # with the exception of assert_*, which won't return the element, - # but like the others, will raise an exception if the call fails. + page_domain = self.get_domain_url(page_url) + page_data_domain = self.get_domain_url(page_url_data) + unittest.TestCase.maxDiff = 65536 # 2^16 + if level != 0 and check_domain: + self.assertEqual(page_data_domain, page_domain, domain_fail) + if level == 3: + if not full_diff: + self.__assert_eq(level_3_data, level_3, level_3_failure) + else: + self.assertEqual(level_3_data, level_3, level_3_failure) + if level == 2: + if not full_diff: + self.__assert_eq(level_2_data, level_2, level_2_failure) + else: + self.assertEqual(level_2_data, level_2, level_2_failure) + if level == 1: + if not full_diff: + self.__assert_eq(level_1_data, level_1, level_1_failure) + else: + self.assertEqual(level_1_data, level_1, level_1_failure) + if level == 0: + try: + if check_domain: + self.assertEqual( + page_domain, page_data_domain, domain_fail + ) + try: + if not full_diff: + self.__assert_eq( + level_1_data, level_1, level_1_failure + ) + else: + self.assertEqual( + level_1_data, level_1, level_1_failure + ) + except Exception as e: + print(e) + try: + if not full_diff: + self.__assert_eq( + level_2_data, level_2, level_2_failure + ) + else: + self.assertEqual( + level_2_data, level_2, level_2_failure + ) + except Exception as e: + print(e) + if not full_diff: + self.__assert_eq( + level_3_data, level_3, level_3_failure + ) + else: + self.assertEqual( + level_3_data, level_3, level_3_failure + ) + except Exception as e: + print(e) # Level-0 Dry Run (Only print the differences) + is_level_0_failure = True + unittest.TestCase.maxDiff = None # Reset unittest.TestCase.maxDiff + # Since the check passed, do not save an extra copy of the baseline + del self.__visual_baseline_copies[-1] # .pop() returns the element + if is_level_0_failure: + # Generating the side_by_side.html file for Level-0 failures + test_logpath = os.path.join(self.log_path, self.__get_test_id()) + if ( + not os.path.exists(baseline_path) + or not os.path.exists(latest_png_path) + ): + return + self.__level_0_visual_f = True + if not os.path.exists(test_logpath): + self.__create_log_path_as_needed(test_logpath) + baseline_copy_path = os.path.join(test_logpath, baseline_copy_name) + latest_copy_path = os.path.join(test_logpath, latest_copy_name) + if ( + not os.path.exists(baseline_copy_path) + and not os.path.exists(latest_copy_path) + ): + shutil.copy(baseline_path, baseline_copy_path) + shutil.copy(latest_png_path, latest_copy_path) + the_html = visual_helper.get_sbs_html( + baseline_copy_name, latest_copy_name + ) + alpha_n_d_name = "".join([x if x.isalnum() else "_" for x in name]) + side_by_side_name = "side_by_side_%s.html" % alpha_n_d_name + file_path = os.path.join(test_logpath, side_by_side_name) + out_file = open(file_path, mode="w+", encoding="utf-8") + out_file.writelines(the_html) + out_file.close() - def wait_for_text_visible(self, text, selector="html", by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT): - if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - if page_utils.is_xpath_selector(selector): - by = By.XPATH - if page_utils.is_link_text_selector(selector): - selector = page_utils.get_link_text_from_selector(selector) - by = By.LINK_TEXT - return page_actions.wait_for_text_visible( - self.driver, text, selector, by, timeout) + ############ - def wait_for_exact_text_visible(self, text, selector="html", - by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT): - if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - if page_utils.is_xpath_selector(selector): - by = By.XPATH - if page_utils.is_link_text_selector(selector): - selector = page_utils.get_link_text_from_selector(selector) - by = By.LINK_TEXT - return page_actions.wait_for_exact_text_visible( - self.driver, text, selector, by, timeout) + def __get_new_timeout(self, timeout): + """When using --timeout_multiplier=#.#""" + self.__check_scope() + try: + timeout_multiplier = float(self.timeout_multiplier) + if timeout_multiplier <= 0.5: + timeout_multiplier = 0.5 + timeout = int(math.ceil(timeout_multiplier * timeout)) + return timeout + except Exception: + # Wrong data type for timeout_multiplier (expecting int or float) + return timeout - def wait_for_text(self, text, selector="html", by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT): - """ The shorter version of wait_for_text_visible() """ - if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - return self.wait_for_text_visible( - text, selector, by=by, timeout=timeout) + ############ - def find_text(self, text, selector="html", by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT): - """ Same as wait_for_text_visible() - returns the element """ - if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - return self.wait_for_text_visible( - text, selector, by=by, timeout=timeout) + def __is_cdp_swap_needed(self): + """If the driver is disconnected, use a CDP method when available.""" + cdp_swap_needed = shared_utils.is_cdp_swap_needed(self.driver) + if cdp_swap_needed: + if not self.cdp: + self.cdp = self.driver.cdp + self.undetectable = True + return True + else: + return False - def assert_text_visible(self, text, selector="html", by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT): - """ Same as assert_text() """ - if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - return self.assert_text(text, selector, by=by, timeout=timeout) + ############ - def assert_text(self, text, selector="html", by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT): - """ Similar to wait_for_text_visible() - Raises an exception if the element or the text is not found. - Returns True if successful. Default timeout = SMALL_TIMEOUT. """ - if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - self.wait_for_text_visible(text, selector, by=by, timeout=timeout) + def __check_scope(self): + if hasattr(self, "browser"): # self.browser stores the type of browser + return # All good: setUp() already initialized variables in "self" + else: + message = ( + "\n It looks like you are trying to call a SeleniumBase method" + "\n from outside the scope of your test class's `self` object," + "\n which is initialized by calling BaseCase's setUp() method." + "\n The `self` object is where all test variables are defined." + "\n If you created a custom setUp() method (that overrided the" + "\n the default one), make sure to call super().setUp() in it." + "\n When using page objects, be sure to pass the `self` object" + "\n from your test class into your page object methods so that" + "\n they can call BaseCase class methods with all the required" + "\n variables, which are initialized during the setUp() method" + "\n that runs automatically before all tests called by pytest." + ) + raise OutOfScopeException(message) - if self.demo_mode: - if page_utils.is_xpath_selector(selector): - by = By.XPATH - if page_utils.is_link_text_selector(selector): - selector = page_utils.get_link_text_from_selector(selector) - by = By.LINK_TEXT - messenger_post = ("ASSERT TEXT {%s} in %s: %s" - % (text, by, selector)) - self.__highlight_with_assert_success(messenger_post, selector, by) - return True + ############ - def assert_exact_text(self, text, selector="html", by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT): - """ Similar to assert_text(), but the text must be exact, rather than - exist as a subset of the full text. - (Extra whitespace at the beginning or the end doesn't count.) - Raises an exception if the element or the text is not found. - Returns True if successful. Default timeout = SMALL_TIMEOUT. """ - if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - self.wait_for_exact_text_visible( - text, selector, by=by, timeout=timeout) + def __get_exception_message(self): + """This method extracts the message from an exception if there + was an exception that occurred during the test, assuming + that the exception was in a try/except block and not thrown.""" + exception_info = sys.exc_info()[1] + if hasattr(exception_info, "msg"): + exc_message = exception_info.msg + elif hasattr(exception_info, "message"): + exc_message = exception_info.message + elif hasattr(exception_info, "args") and len(exception_info.args) == 1: + exc_message = exception_info.args[0] + else: + exc_message = sys.exc_info() + return exc_message - if self.demo_mode: - if page_utils.is_xpath_selector(selector): - by = By.XPATH - if page_utils.is_link_text_selector(selector): - selector = page_utils.get_link_text_from_selector(selector) - by = By.LINK_TEXT - messenger_post = ("ASSERT TEXT {%s} in %s: %s" - % (text, by, selector)) - self.__highlight_with_assert_success(messenger_post, selector, by) - return True + def __add_deferred_assert_failure(self, fs=False): + """Add a deferred_assert failure to a list for future processing.""" + self.__check_scope() + current_url = self.driver.current_url + message = self.__get_exception_message() + count = self.__deferred_assert_count + self.__deferred_assert_failures.append( + "DEFERRED ASSERT #%s: (%s) %s\n" % (count, current_url, message) + ) + if fs: + self.save_screenshot_to_logs(name="deferred_#%s" % count) - # For backwards compatibility, earlier method names of the next - # four methods have remained even though they do the same thing, - # with the exception of assert_*, which won't return the element, - # but like the others, will raise an exception if the call fails. + ############ - def wait_for_link_text_visible(self, link_text, - timeout=settings.LARGE_TIMEOUT): - if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: + def deferred_assert_element( + self, selector, by="css selector", timeout=None, fs=False + ): + """A non-terminating assertion for an element visible on the page. + Failures will be saved until the process_deferred_asserts() + method is called from inside a test, likely at the end of it. + If "fs" is set to True, a failure screenshot is saved to the + "latest_logs/" folder for that assertion failure. Otherwise, + only the last page screenshot is taken for all failures when + calling the process_deferred_asserts() method.""" + self.__check_scope() + if not timeout: + timeout = settings.MINI_TIMEOUT + if self.timeout_multiplier and timeout == settings.MINI_TIMEOUT: timeout = self.__get_new_timeout(timeout) - return self.wait_for_element_visible( - link_text, by=By.LINK_TEXT, timeout=timeout) + self.__deferred_assert_count += 1 + with suppress(Exception): + url = self.get_current_url() + if url == self.__last_url_of_deferred_assert: + timeout = 0.6 # Was already on page (full wait not needed) + else: + self.__last_url_of_deferred_assert = url + if self.recorder_mode and self.__current_url_is_recordable(): + if self.get_session_storage_item("pause_recorder") == "no": + time_stamp = self.execute_script("return Date.now();") + origin = self.get_origin() + action = ["da_el", selector, origin, time_stamp] + self.__extra_actions.append(action) + try: + self.wait_for_element_visible(selector, by=by, timeout=timeout) + return True + except Exception: + self.__add_deferred_assert_failure(fs=fs) + return False - def wait_for_link_text(self, link_text, timeout=settings.LARGE_TIMEOUT): - """ The shorter version of wait_for_link_text_visible() """ - if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: + def deferred_assert_element_present( + self, selector, by="css selector", timeout=None, fs=False + ): + """A non-terminating assertion for an element present in the page html. + Failures will be saved until the process_deferred_asserts() + method is called from inside a test, likely at the end of it. + If "fs" is set to True, a failure screenshot is saved to the + "latest_logs/" folder for that assertion failure. Otherwise, + only the last page screenshot is taken for all failures when + calling the process_deferred_asserts() method.""" + self.__check_scope() + if not timeout: + timeout = settings.MINI_TIMEOUT + if self.timeout_multiplier and timeout == settings.MINI_TIMEOUT: timeout = self.__get_new_timeout(timeout) - return self.wait_for_link_text_visible(link_text, timeout=timeout) + self.__deferred_assert_count += 1 + with suppress(Exception): + url = self.get_current_url() + if url == self.__last_url_of_deferred_assert: + timeout = 0.6 # Was already on page (full wait not needed) + else: + self.__last_url_of_deferred_assert = url + if self.recorder_mode and self.__current_url_is_recordable(): + if self.get_session_storage_item("pause_recorder") == "no": + time_stamp = self.execute_script("return Date.now();") + origin = self.get_origin() + action = ["da_ep", selector, origin, time_stamp] + self.__extra_actions.append(action) + try: + self.wait_for_element_present(selector, by=by, timeout=timeout) + return True + except Exception: + self.__add_deferred_assert_failure(fs=fs) + return False - def find_link_text(self, link_text, timeout=settings.LARGE_TIMEOUT): - """ Same as wait_for_link_text_visible() - returns the element """ - if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: + def deferred_assert_text( + self, text, selector="body", by="css selector", timeout=None, fs=False + ): + """A non-terminating assertion for text from an element on a page. + Failures will be saved until the process_deferred_asserts() + method is called from inside a test, likely at the end of it. + If "fs" is set to True, a failure screenshot is saved to the + "latest_logs/" folder for that assertion failure. Otherwise, + only the last page screenshot is taken for all failures when + calling the process_deferred_asserts() method.""" + self.__check_scope() + if not timeout: + timeout = settings.MINI_TIMEOUT + if self.timeout_multiplier and timeout == settings.MINI_TIMEOUT: timeout = self.__get_new_timeout(timeout) - return self.wait_for_link_text_visible(link_text, timeout=timeout) + self.__deferred_assert_count += 1 + with suppress(Exception): + url = self.get_current_url() + if url == self.__last_url_of_deferred_assert: + timeout = 0.6 # Was already on page (full wait not needed) + else: + self.__last_url_of_deferred_assert = url + if self.recorder_mode and self.__current_url_is_recordable(): + if self.get_session_storage_item("pause_recorder") == "no": + time_stamp = self.execute_script("return Date.now();") + origin = self.get_origin() + text_selector = [text, selector] + action = ["da_te", text_selector, origin, time_stamp] + self.__extra_actions.append(action) + try: + self.wait_for_text_visible(text, selector, by=by, timeout=timeout) + return True + except Exception: + self.__add_deferred_assert_failure(fs=fs) + return False - def assert_link_text(self, link_text, timeout=settings.SMALL_TIMEOUT): - """ Similar to wait_for_link_text_visible(), but returns nothing. - As above, will raise an exception if nothing can be found. - Returns True if successful. Default timeout = SMALL_TIMEOUT. """ - if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: + def deferred_assert_exact_text( + self, text, selector="body", by="css selector", timeout=None, fs=False + ): + """A non-terminating assertion for exact text from an element. + Failures will be saved until the process_deferred_asserts() + method is called from inside a test, likely at the end of it. + If "fs" is set to True, a failure screenshot is saved to the + "latest_logs/" folder for that assertion failure. Otherwise, + only the last page screenshot is taken for all failures when + calling the process_deferred_asserts() method.""" + self.__check_scope() + if not timeout: + timeout = settings.MINI_TIMEOUT + if self.timeout_multiplier and timeout == settings.MINI_TIMEOUT: timeout = self.__get_new_timeout(timeout) - self.wait_for_link_text_visible(link_text, timeout=timeout) - if self.demo_mode: - messenger_post = ("ASSERT LINK TEXT {%s}." % link_text) - self.__highlight_with_assert_success( - messenger_post, link_text, by=By.LINK_TEXT) - return True - - # For backwards compatibility, earlier method names of the next - # three methods have remained even though they do the same thing, - # with the exception of assert_*, which won't return the element, - # but like the others, will raise an exception if the call fails. + self.__deferred_assert_count += 1 + with suppress(Exception): + url = self.get_current_url() + if url == self.__last_url_of_deferred_assert: + timeout = 0.6 # Was already on page (full wait not needed) + else: + self.__last_url_of_deferred_assert = url + if self.recorder_mode and self.__current_url_is_recordable(): + if self.get_session_storage_item("pause_recorder") == "no": + time_stamp = self.execute_script("return Date.now();") + origin = self.get_origin() + text_selector = [text, selector] + action = ["da_et", text_selector, origin, time_stamp] + self.__extra_actions.append(action) + try: + self.wait_for_exact_text_visible( + text, selector, by=by, timeout=timeout + ) + return True + except Exception: + self.__add_deferred_assert_failure(fs=fs) + return False - def wait_for_partial_link_text(self, partial_link_text, - timeout=settings.LARGE_TIMEOUT): - if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: + def deferred_assert_non_empty_text( + self, + selector="body", + by="css selector", + timeout=None, + fs=False, + ): + """A non-terminating assertion for non-empty element text. + Failures will be saved until the process_deferred_asserts() + method is called from inside a test, likely at the end of it. + If "fs" is set to True, a failure screenshot is saved to the + "latest_logs/" folder for that assertion failure. Otherwise, + only the last page screenshot is taken for all failures when + calling the process_deferred_asserts() method.""" + self.__check_scope() + if not timeout: + timeout = settings.MINI_TIMEOUT + if self.timeout_multiplier and timeout == settings.MINI_TIMEOUT: timeout = self.__get_new_timeout(timeout) - return self.wait_for_element_visible( - partial_link_text, by=By.PARTIAL_LINK_TEXT, timeout=timeout) + self.__deferred_assert_count += 1 + with suppress(Exception): + url = self.get_current_url() + if url == self.__last_url_of_deferred_assert: + timeout = 0.6 # Was already on page (full wait not needed) + else: + self.__last_url_of_deferred_assert = url + if self.recorder_mode and self.__current_url_is_recordable(): + if self.get_session_storage_item("pause_recorder") == "no": + time_stamp = self.execute_script("return Date.now();") + origin = self.get_origin() + action = ["danet", selector, origin, time_stamp] + self.__extra_actions.append(action) + try: + self.wait_for_non_empty_text_visible( + selector, by=by, timeout=timeout + ) + return True + except Exception: + self.__add_deferred_assert_failure(fs=fs) + return False - def find_partial_link_text(self, partial_link_text, - timeout=settings.LARGE_TIMEOUT): - """ Same as wait_for_partial_link_text() - returns the element """ - if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - return self.wait_for_partial_link_text( - partial_link_text, timeout=timeout) + def deferred_check_window( + self, + name="default", + level=0, + baseline=False, + check_domain=True, + full_diff=False, + fs=False, + ): + """A non-terminating assertion for the check_window() method. + Failures will be saved until the process_deferred_asserts() + method is called from inside a test, likely at the end of it. + If "fs" is set to True, a failure screenshot is saved to the + "latest_logs/" folder for that assertion failure. Otherwise, + only the last page screenshot is taken for all failures when + calling the process_deferred_asserts() method.""" + self.__check_scope() + self.__deferred_assert_count += 1 + try: + self.check_window( + name=name, + level=level, + baseline=baseline, + check_domain=check_domain, + full_diff=full_diff, + ) + return True + except Exception: + self.__add_deferred_assert_failure(fs=fs) + return False - def assert_partial_link_text(self, partial_link_text, - timeout=settings.SMALL_TIMEOUT): - """ Similar to wait_for_partial_link_text(), but returns nothing. - As above, will raise an exception if nothing can be found. - Returns True if successful. Default timeout = SMALL_TIMEOUT. """ - if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - self.wait_for_partial_link_text(partial_link_text, timeout=timeout) - return True + def process_deferred_asserts(self, print_only=False): + """To be used with any test that uses deferred_asserts, which are + non-terminating verifications that only raise exceptions + after this method is called. + This is useful for pages with multiple elements to be checked when + you want to find as many bugs as possible in a single test run + before having all the exceptions get raised simultaneously. + Might be more useful if this method is called after processing all + the deferred asserts on a single html page so that the failure + screenshot matches the location of the deferred asserts. + If "print_only" is set to True, the exception won't get raised.""" + if self.recorder_mode: + time_stamp = self.execute_script("return Date.now();") + origin = self.get_origin() + action = ["pr_da", "", origin, time_stamp] + self.__extra_actions.append(action) + if self.__deferred_assert_failures: + exception_output = "" + exception_output += "\n***** DEFERRED ASSERTION FAILURES:\n" + exception_output += "TEST: %s\n\n" % self.id() + all_failing_checks = self.__deferred_assert_failures + self.__deferred_assert_failures = [] + for tb in all_failing_checks: + exception_output += "%s\n" % tb + if print_only: + print(exception_output) + else: + raise Exception(exception_output.replace("\\n", "\n")) ############ - def wait_for_element_absent(self, selector, by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT): - """ Waits for an element to no longer appear in the HTML of a page. - A hidden element still counts as appearing in the page HTML. - If an element with "hidden" status is acceptable, - use wait_for_element_not_visible() instead. """ - if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - if page_utils.is_xpath_selector(selector): - by = By.XPATH - return page_actions.wait_for_element_absent( - self.driver, selector, by, timeout) + # Alternate naming scheme for the "deferred_assert" methods. + + def delayed_assert_element( + self, selector, by="css selector", timeout=None, fs=False + ): + """Same as self.deferred_assert_element()""" + return self.deferred_assert_element( + selector=selector, by=by, timeout=timeout, fs=fs + ) + + def delayed_assert_element_present( + self, selector, by="css selector", timeout=None, fs=False + ): + """Same as self.deferred_assert_element_present()""" + return self.deferred_assert_element_present( + selector=selector, by=by, timeout=timeout, fs=fs + ) + + def delayed_assert_text( + self, text, selector="body", by="css selector", timeout=None, fs=False + ): + """Same as self.deferred_assert_text()""" + return self.deferred_assert_text( + text=text, selector=selector, by=by, timeout=timeout, fs=fs + ) + + def delayed_assert_exact_text( + self, text, selector="body", by="css selector", timeout=None, fs=False + ): + """Same as self.deferred_assert_exact_text()""" + return self.deferred_assert_exact_text( + text=text, selector=selector, by=by, timeout=timeout, fs=fs + ) + + def delayed_assert_non_empty_text( + self, + selector="body", + by="css selector", + timeout=None, + fs=False, + ): + """Same as self.deferred_assert_non_empty_text()""" + return self.deferred_assert_non_empty_text( + selector=selector, by=by, timeout=timeout, fs=fs + ) + + def delayed_check_window( + self, + name="default", + level=0, + baseline=False, + check_domain=True, + full_diff=False, + fs=False, + ): + """Same as self.deferred_check_window()""" + return self.deferred_check_window( + name=name, + level=level, + baseline=baseline, + check_domain=check_domain, + full_diff=full_diff, + fs=fs, + ) - def assert_element_absent(self, selector, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT): - """ Similar to wait_for_element_absent() - returns nothing. - As above, will raise an exception if the element stays present. - Returns True if successful. Default timeout = SMALL_TIMEOUT. """ - if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - self.wait_for_element_absent(selector, by=by, timeout=timeout) - return True + def process_delayed_asserts(self, print_only=False): + """Same as self.process_deferred_asserts()""" + self.process_deferred_asserts(print_only=print_only) ############ - def wait_for_element_not_visible(self, selector, by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT): - """ Waits for an element to no longer be visible on a page. - The element can be non-existant in the HTML or hidden on the page - to qualify as not visible. """ - if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - if page_utils.is_xpath_selector(selector): - by = By.XPATH - if page_utils.is_link_text_selector(selector): - selector = page_utils.get_link_text_from_selector(selector) - by = By.LINK_TEXT - return page_actions.wait_for_element_not_visible( - self.driver, selector, by, timeout) + def create_presentation( + self, name=None, theme="default", transition="default" + ): + """Creates a Reveal-JS presentation that you can add slides to. + @Params + name - If creating multiple presentations at the same time, + use this to specify the name of the current presentation. + theme - Set a theme with a unique style for the presentation. + Valid themes: "serif" (default), "sky", "white", "black", + "simple", "league", "moon", "night", + "beige", "blood", and "solarized". + transition - Set a transition between slides. + Valid transitions: "none" (default), "slide", "fade", + "zoom", "convex", and "concave".""" + if not name: + name = "default" + if not theme or theme == "default": + theme = "serif" + valid_themes = [ + "serif", + "white", + "black", + "beige", + "simple", + "sky", + "league", + "moon", + "night", + "blood", + "solarized", + ] + theme = theme.lower() + if theme not in valid_themes: + raise Exception( + "Theme {%s} not found! Valid themes: %s" + % (theme, valid_themes) + ) + if not transition or transition == "default": + transition = "none" + valid_transitions = [ + "none", + "slide", + "fade", + "zoom", + "convex", + "concave", + ] + transition = transition.lower() + if transition not in valid_transitions: + raise Exception( + "Transition {%s} not found! Valid transitions: %s" + % (transition, valid_transitions) + ) + reveal_theme_css = None + if theme == "serif": + reveal_theme_css = constants.Reveal.SERIF_MIN_CSS + elif theme == "sky": + reveal_theme_css = constants.Reveal.SKY_MIN_CSS + elif theme == "white": + reveal_theme_css = constants.Reveal.WHITE_MIN_CSS + elif theme == "black": + reveal_theme_css = constants.Reveal.BLACK_MIN_CSS + elif theme == "simple": + reveal_theme_css = constants.Reveal.SIMPLE_MIN_CSS + elif theme == "league": + reveal_theme_css = constants.Reveal.LEAGUE_MIN_CSS + elif theme == "moon": + reveal_theme_css = constants.Reveal.MOON_MIN_CSS + elif theme == "night": + reveal_theme_css = constants.Reveal.NIGHT_MIN_CSS + elif theme == "beige": + reveal_theme_css = constants.Reveal.BEIGE_MIN_CSS + elif theme == "blood": + reveal_theme_css = constants.Reveal.BLOOD_MIN_CSS + elif theme == "solarized": + reveal_theme_css = constants.Reveal.SOLARIZED_MIN_CSS + else: + # Use the default if unable to determine the theme + reveal_theme_css = constants.Reveal.SERIF_MIN_CSS + new_presentation = ( + "\n" + "\n" + '\n' + '\n' + '\n' + '\n' + '\n' + "\n" + "\n\n" + "\n" + "\n" + '
      \n' + '
      \n' + % (constants.Reveal.MIN_CSS, reveal_theme_css) + ) + self._presentation_slides[name] = [] + self._presentation_slides[name].append(new_presentation) + self._presentation_transition[name] = transition + + def add_slide( + self, + content=None, + image=None, + code=None, + iframe=None, + content2=None, + notes=None, + transition=None, + name=None, + ): + """Allows the user to add slides to a presentation. + @Params + content - The HTML content to display on the presentation slide. + image - Attach an image (from a URL link) to the slide. + code - Attach code of any programming language to the slide. + Language-detection will be used to add syntax formatting. + iframe - Attach an iframe (from a URL link) to the slide. + content2 - HTML content to display after adding an image or code. + notes - Additional notes to include with the slide. + ONLY SEEN if show_notes is set for the presentation. + transition - Set a transition between slides. (overrides previous) + Valid transitions: "none" (default), "slide", "fade", + "zoom", "convex", and "concave". + name - If creating multiple presentations at the same time, + use this to select the presentation to add slides to.""" + if not name: + name = "default" + if name not in self._presentation_slides: + # Create a presentation if it doesn't already exist + self.create_presentation(name=name) + if not content: + content = "" + if not content2: + content2 = "" + if not notes: + notes = "" + if not transition: + transition = self._presentation_transition[name] + elif transition == "default": + transition = "none" + valid_transitions = [ + "none", + "slide", + "fade", + "zoom", + "convex", + "concave", + ] + transition = transition.lower() + if transition not in valid_transitions: + raise Exception( + "Transition {%s} not found! Valid transitions: %s" + "" % (transition, valid_transitions) + ) + add_line = "" + if content.startswith("<"): + add_line = "\n" + html = '\n
      %s%s' % ( + transition, + add_line, + content, + ) + if image: + html += '\n
      ' % image + if code: + html += "\n
      " + html += '\n
      \n%s
      ' % code + if iframe: + html += ( + "\n
      " + '\n' % iframe + ) + add_line = "" + if content2.startswith("<"): + add_line = "\n" + if content2: + html += "%s%s" % (add_line, content2) + html += '\n' % notes + html += "\n
      \n" + if "" not in html and "" not in html: + self._presentation_slides[name].append(html) + else: + # Generate multiple slides with and + replacements = False + for num in range(32): + if "" % num in html and "" % num in html: + replacements = True + new_html = html + new_html = new_html.replace("" % num, "") + new_html = new_html.replace("" % num, "") + for num2 in range(32): + if num2 == num: + continue + if "" % num2 not in new_html and num2 >= 2: + break + new_html = new_html.replace("" % num2, "") + new_html = new_html.replace("" % num2, "") + self._presentation_slides[name].append(new_html) + else: + if num >= 2: + break + if not replacements: + # A is missing a closing tag. Do one. + self._presentation_slides[name].append(html) + + def save_presentation( + self, name=None, filename=None, show_notes=False, interval=0 + ): + """Saves a Reveal-JS Presentation to a file for later use. + @Params + name - If creating multiple presentations at the same time, + use this to select the one you wish to use. + filename - The name of the HTML file that you wish to + save the presentation to. (filename must end in ".html") + show_notes - When set to True, the Notes feature becomes enabled, + which allows presenters to see notes next to slides. + interval - The delay time between autoplaying slides. (in seconds) + If set to 0 (default), autoplay is disabled.""" + if not name: + name = "default" + if not filename: + filename = "my_presentation.html" + if name not in self._presentation_slides: + raise Exception("Presentation {%s} does not exist!" % name) + if not filename.endswith(".html"): + raise Exception('Presentation file must end in ".html"!') + if not interval: + interval = 0 + if interval == 0 and self.interval: + interval = float(self.interval) + if not isinstance(interval, (int, float)): + raise Exception('Expecting a numeric value for "interval"!') + if interval < 0: + raise Exception('The "interval" cannot be a negative number!') + interval_ms = float(interval) * 1000.0 + show_notes_str = "false" + if show_notes: + show_notes_str = "true" + the_html = "" + for slide in self._presentation_slides[name]: + the_html += slide + the_html += ( + "\n
      \n" + "
      \n" + '\n' + '\n' + "\n" + "\n" + "\n" + % ( + constants.Reveal.MIN_JS, + constants.PrettifyJS.RUN_PRETTIFY_JS, + show_notes_str, + interval_ms, + ) + ) + + # Remove duplicate ChartMaker library declarations + chart_libs = """ + + + + + """ % ( + constants.HighCharts.HC_JS, + constants.HighCharts.EXPORTING_JS, + constants.HighCharts.EXPORT_DATA_JS, + constants.HighCharts.ACCESSIBILITY_JS, + ) + if the_html.count(chart_libs) > 1: + chart_libs_comment = "" + the_html = the_html.replace(chart_libs, chart_libs_comment) + # Only need to import the HighCharts libraries once + the_html = the_html.replace(chart_libs_comment, chart_libs, 1) + saved_presentations_folder = constants.Presentations.SAVED_FOLDER + if saved_presentations_folder.endswith("/"): + saved_presentations_folder = saved_presentations_folder[:-1] + if not os.path.exists(saved_presentations_folder): + with suppress(Exception): + os.makedirs(saved_presentations_folder) + file_path = os.path.join(saved_presentations_folder, filename) + out_file = open(file_path, mode="w+", encoding="utf-8") + out_file.writelines(the_html) + out_file.close() + if self._output_file_saves: + print("\n>>> [%s] was saved!\n" % file_path) + return file_path + + def begin_presentation( + self, name=None, filename=None, show_notes=False, interval=0 + ): + """Begin a Reveal-JS Presentation in the web browser. + @Params + name - If creating multiple presentations at the same time, + use this to select the one you wish to use. + filename - The name of the HTML file that you wish to + save the presentation to. (filename must end in ".html") + show_notes - When set to True, the Notes feature becomes enabled, + which allows presenters to see notes next to slides. + interval - The delay time between autoplaying slides. (in seconds) + If set to 0 (default), autoplay is disabled.""" + if self.headless or self.headless2 or self.xvfb: + return # Presentations should not run in headless mode. + if not name: + name = "default" + if not filename: + filename = "my_presentation.html" + if name not in self._presentation_slides: + raise Exception("Presentation {%s} does not exist!" % name) + if not filename.endswith(".html"): + raise Exception('Presentation file must end in ".html"!') + if not interval: + interval = 0 + if interval == 0 and self.interval: + interval = float(self.interval) + if not isinstance(interval, (int, float)): + raise Exception('Expecting a numeric value for "interval"!') + if interval < 0: + raise Exception('The "interval" cannot be a negative number!') + end_slide = ( + '\n
      \n' + '

      \n
      \n' + ) + self._presentation_slides[name].append(end_slide) + file_path = self.save_presentation( + name=name, + filename=filename, + show_notes=show_notes, + interval=interval, + ) + self._presentation_slides[name].pop() + self.open_html_file(file_path) + presentation_folder = constants.Presentations.SAVED_FOLDER + with suppress(Exception): + while ( + len(self.driver.window_handles) > 0 + and presentation_folder in self.get_current_url() + ): + time.sleep(0.05) + if self.is_element_visible( + "section.present p.End_Presentation_Now" + ): + break + time.sleep(0.05) - def assert_element_not_visible(self, selector, by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT): - """ Similar to wait_for_element_not_visible() - returns nothing. - As above, will raise an exception if the element stays visible. - Returns True if successful. Default timeout = SMALL_TIMEOUT. """ - if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - self.wait_for_element_not_visible(selector, by=by, timeout=timeout) - return True + ############ + + def create_pie_chart( + self, + chart_name=None, + title=None, + subtitle=None, + data_name=None, + unit=None, + libs=True, + labels=True, + legend=True, + ): + """Creates a JavaScript pie chart using "HighCharts". + @Params + chart_name - If creating multiple charts, + use this to select which one. + title - The title displayed for the chart. + subtitle - The subtitle displayed for the chart. + data_name - The series name. Useful for multi-series charts. + If no data_name, will default to using "Series 1". + unit - The description label given to the chart's y-axis values. + libs - The option to include Chart libraries (JS and CSS files). + Should be set to True (default) for the first time creating + a chart on a web page. If creating multiple charts on the + same web page, you won't need to re-import the libraries + when creating additional charts. + labels - If True, displays labels on the chart for data points. + legend - If True, displays the data point legend on the chart.""" + if not chart_name: + chart_name = "default" + if not data_name: + data_name = "" + style = "pie" + self.__create_highchart( + chart_name=chart_name, + title=title, + subtitle=subtitle, + style=style, + data_name=data_name, + unit=unit, + libs=libs, + labels=labels, + legend=legend, + ) + + def create_bar_chart( + self, + chart_name=None, + title=None, + subtitle=None, + data_name=None, + unit=None, + libs=True, + labels=True, + legend=True, + ): + """Creates a JavaScript bar chart using "HighCharts". + @Params + chart_name - If creating multiple charts, + use this to select which one. + title - The title displayed for the chart. + subtitle - The subtitle displayed for the chart. + data_name - The series name. Useful for multi-series charts. + If no data_name, will default to using "Series 1". + unit - The description label given to the chart's y-axis values. + libs - The option to include Chart libraries (JS and CSS files). + Should be set to True (default) for the first time creating + a chart on a web page. If creating multiple charts on the + same web page, you won't need to re-import the libraries + when creating additional charts. + labels - If True, displays labels on the chart for data points. + legend - If True, displays the data point legend on the chart.""" + if not chart_name: + chart_name = "default" + if not data_name: + data_name = "" + style = "bar" + self.__create_highchart( + chart_name=chart_name, + title=title, + subtitle=subtitle, + style=style, + data_name=data_name, + unit=unit, + libs=libs, + labels=labels, + legend=legend, + ) + + def create_column_chart( + self, + chart_name=None, + title=None, + subtitle=None, + data_name=None, + unit=None, + libs=True, + labels=True, + legend=True, + ): + """Creates a JavaScript column chart using "HighCharts". + @Params + chart_name - If creating multiple charts, + use this to select which one. + title - The title displayed for the chart. + subtitle - The subtitle displayed for the chart. + data_name - The series name. Useful for multi-series charts. + If no data_name, will default to using "Series 1". + unit - The description label given to the chart's y-axis values. + libs - The option to include Chart libraries (JS and CSS files). + Should be set to True (default) for the first time creating + a chart on a web page. If creating multiple charts on the + same web page, you won't need to re-import the libraries + when creating additional charts. + labels - If True, displays labels on the chart for data points. + legend - If True, displays the data point legend on the chart.""" + if not chart_name: + chart_name = "default" + if not data_name: + data_name = "" + style = "column" + self.__create_highchart( + chart_name=chart_name, + title=title, + subtitle=subtitle, + style=style, + data_name=data_name, + unit=unit, + libs=libs, + labels=labels, + legend=legend, + ) + + def create_line_chart( + self, + chart_name=None, + title=None, + subtitle=None, + data_name=None, + unit=None, + zero=False, + libs=True, + labels=True, + legend=True, + ): + """Creates a JavaScript line chart using "HighCharts". + @Params + chart_name - If creating multiple charts, + use this to select which one. + title - The title displayed for the chart. + subtitle - The subtitle displayed for the chart. + data_name - The series name. Useful for multi-series charts. + If no data_name, will default to using "Series 1". + unit - The description label given to the chart's y-axis values. + zero - If True, the y-axis always starts at 0. (Default: False). + libs - The option to include Chart libraries (JS and CSS files). + Should be set to True (default) for the first time creating + a chart on a web page. If creating multiple charts on the + same web page, you won't need to re-import the libraries + when creating additional charts. + labels - If True, displays labels on the chart for data points. + legend - If True, displays the data point legend on the chart.""" + if not chart_name: + chart_name = "default" + if not data_name: + data_name = "" + style = "line" + self.__create_highchart( + chart_name=chart_name, + title=title, + subtitle=subtitle, + style=style, + data_name=data_name, + unit=unit, + zero=zero, + libs=libs, + labels=labels, + legend=legend, + ) + + def create_area_chart( + self, + chart_name=None, + title=None, + subtitle=None, + data_name=None, + unit=None, + zero=False, + libs=True, + labels=True, + legend=True, + ): + """Creates a JavaScript area chart using "HighCharts". + @Params + chart_name - If creating multiple charts, + use this to select which one. + title - The title displayed for the chart. + subtitle - The subtitle displayed for the chart. + data_name - The series name. Useful for multi-series charts. + If no data_name, will default to using "Series 1". + unit - The description label given to the chart's y-axis values. + zero - If True, the y-axis always starts at 0. (Default: False). + libs - The option to include Chart libraries (JS and CSS files). + Should be set to True (default) for the first time creating + a chart on a web page. If creating multiple charts on the + same web page, you won't need to re-import the libraries + when creating additional charts. + labels - If True, displays labels on the chart for data points. + legend - If True, displays the data point legend on the chart.""" + if not chart_name: + chart_name = "default" + if not data_name: + data_name = "" + style = "area" + self.__create_highchart( + chart_name=chart_name, + title=title, + subtitle=subtitle, + style=style, + data_name=data_name, + unit=unit, + zero=zero, + libs=libs, + labels=labels, + legend=legend, + ) + + def __create_highchart( + self, + chart_name=None, + title=None, + subtitle=None, + style=None, + data_name=None, + unit=None, + zero=False, + libs=True, + labels=True, + legend=True, + ): + """Creates a JavaScript chart using the "HighCharts" library.""" + if not chart_name: + chart_name = "default" + if not title: + title = "" + if not subtitle: + subtitle = "" + if not style: + style = "pie" + if not data_name: + data_name = "Series 1" + if not unit: + unit = "Values" + if labels: + labels = "true" + else: + labels = "false" + if legend: + legend = "true" + else: + legend = "false" + title = title.replace("'", "\\'") + subtitle = subtitle.replace("'", "\\'") + unit = unit.replace("'", "\\'") + self._chart_count += 1 + # If chart_libs format is changed, also change: save_presentation() + chart_libs = """ + + + + + """ % ( + constants.HighCharts.HC_JS, + constants.HighCharts.EXPORTING_JS, + constants.HighCharts.EXPORT_DATA_JS, + constants.HighCharts.ACCESSIBILITY_JS, + ) + if not libs: + chart_libs = "" + chart_css = """ + + """ + if not libs: + chart_css = "" + chart_description = "" + chart_figure = """ +
      +
      +

      %s

      +
      + """ % ( + self._chart_count, + chart_description, + ) + min_zero = "" + if zero: + min_zero = "min: 0," + chart_init_1 = """ + + """ + axis = "xAxis: {\n" + axis += " labels: {\n" + axis += " useHTML: true,\n" + axis += " style: {\n" + axis += " fontSize: '14px',\n" + axis += " },\n" + axis += " },\n" + axis += "categories: [" + for label in self._chart_label[chart_name]: + axis += "'%s'," % label + axis += "], crosshair: false}," + the_html = the_html.replace("xAxis: { },", axis) + if not folder: + saved_charts_folder = constants.Charts.SAVED_FOLDER + else: + saved_charts_folder = folder + if saved_charts_folder.endswith("/"): + saved_charts_folder = saved_charts_folder[:-1] + if not os.path.exists(saved_charts_folder): + with suppress(Exception): + os.makedirs(saved_charts_folder) + file_path = os.path.join(saved_charts_folder, filename) + out_file = open(file_path, mode="w+", encoding="utf-8") + out_file.writelines(the_html) + out_file.close() + if self._output_file_saves: + print("\n>>> [%s] was saved!" % file_path) + return file_path + + def display_chart(self, chart_name=None, filename=None, interval=0): + """Displays a SeleniumBase-generated chart in the browser window. + @Params + chart_name - If creating multiple charts at the same time, + use this to select the one you wish to use. + filename - The name of the HTML file that you wish to + save the chart to. (filename must end in ".html") + interval - The delay time for auto-advancing charts. (in seconds) + If set to 0 (default), auto-advancing is disabled.""" + if self.headless or self.headless2 or self.xvfb: + interval = 1 # Race through chart if running in headless mode + if not chart_name: + chart_name = "default" + if not filename: + filename = "my_chart.html" + if not interval: + interval = 0 + if interval == 0 and self.interval: + interval = float(self.interval) + if not isinstance(interval, (int, float)): + raise Exception('Expecting a numeric value for "interval"!') + if interval < 0: + raise Exception('The "interval" cannot be a negative number!') + if chart_name not in self._chart_data: + raise Exception("Chart {%s} does not exist!" % chart_name) + if not filename.endswith(".html"): + raise Exception('Chart file must end in ".html"!') + file_path = self.save_chart(chart_name=chart_name, filename=filename) + self.open_html_file(file_path) + chart_folder = constants.Charts.SAVED_FOLDER + if interval == 0: + with suppress(Exception): + print("\n*** Close the browser window to continue ***") + # Will also continue if manually navigating to a new page + while len(self.driver.window_handles) > 0 and ( + chart_folder in self.get_current_url() + ): + time.sleep(0.05) + else: + with suppress(Exception): + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (interval * 1000.0) + for x in range(int(interval * 10)): + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + if len(self.driver.window_handles) == 0: + break + if chart_folder not in self.get_current_url(): + break + time.sleep(0.1) + + def extract_chart(self, chart_name=None): + """Extracts the HTML from a SeleniumBase-generated chart. + @Params + chart_name - If creating multiple charts at the same time, + use this to select the one you wish to use.""" + if not chart_name: + chart_name = "default" + if chart_name not in self._chart_data: + raise Exception("Chart {%s} does not exist!" % chart_name) + the_html = "" + for chart_data_point in self._chart_data[chart_name]: + the_html += chart_data_point + the_html += """ + ] + }] + }); + + """ + axis = "xAxis: {\n" + axis += " labels: {\n" + axis += " useHTML: true,\n" + axis += " style: {\n" + axis += " fontSize: '14px',\n" + axis += " },\n" + axis += " },\n" + axis += "categories: [" + for label in self._chart_label[chart_name]: + axis += "'%s'," % label + axis += "], crosshair: false}," + the_html = the_html.replace("xAxis: { },", axis) + self._chart_xcount += 1 + the_html = the_html.replace( + "chartcontainer_num_", "chartcontainer_%s_" % self._chart_xcount + ) + return the_html ############ - def wait_for_ready_state_complete(self, timeout=settings.EXTREME_TIMEOUT): - if self.timeout_multiplier and timeout == settings.EXTREME_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - is_ready = js_utils.wait_for_ready_state_complete(self.driver, timeout) - self.wait_for_angularjs(timeout=settings.MINI_TIMEOUT) - if self.js_checking_on: - self.assert_no_js_errors() - if self.ad_block_on: - # If the ad_block feature is enabled, then block ads for new URLs - current_url = self.get_current_url() - if not current_url == self.__last_page_load_url: - time.sleep(0.02) - self.ad_block() - time.sleep(0.01) - if self.is_element_present("iframe"): - time.sleep(0.07) # iframe ads take slightly longer to load - self.ad_block() # Do ad_block on slower-loading iframes - self.__last_page_load_url = current_url - return is_ready + def create_tour(self, name=None, theme=None): + """Creates a guided tour for any website. + The default theme is the IntroJS Library. + @Params + name - If creating multiple tours at the same time, + use this to select the tour you wish to add steps to. + theme - Sets the default theme for the website tour. Available themes: + "Bootstrap", "DriverJS", "Hopscotch", "IntroJS", "Shepherd". + The "Shepherd" library includes variations: "light"/"arrows", + "dark", "default", "square", and "square-dark".""" + if not name: + name = "default" + if theme: + if theme.lower() == "bootstrap": + self.create_bootstrap_tour(name) + elif theme.lower() == "hopscotch": + self.create_hopscotch_tour(name) + elif theme.lower() == "intro": + self.create_introjs_tour(name) + elif theme.lower() == "introjs": + self.create_introjs_tour(name) + elif theme.lower() == "driver": + self.create_driverjs_tour(name) + elif theme.lower() == "driverjs": + self.create_driverjs_tour(name) + elif theme.lower() == "shepherd": + self.create_shepherd_tour(name, theme="light") + elif theme.lower() == "light": + self.create_shepherd_tour(name, theme="light") + elif theme.lower() == "arrows": + self.create_shepherd_tour(name, theme="light") + elif theme.lower() == "dark": + self.create_shepherd_tour(name, theme="dark") + elif theme.lower() == "square": + self.create_shepherd_tour(name, theme="square") + elif theme.lower() == "square-dark": + self.create_shepherd_tour(name, theme="square-dark") + elif theme.lower() == "default": + self.create_shepherd_tour(name, theme="default") + else: + self.create_introjs_tour(name) + else: + self.create_introjs_tour(name) - def wait_for_angularjs(self, timeout=settings.LARGE_TIMEOUT, **kwargs): - js_utils.wait_for_angularjs(self.driver, timeout, **kwargs) + def create_shepherd_tour(self, name=None, theme=None): + """Creates a Shepherd JS website tour. + @Params + name - If creating multiple tours at the same time, + use this to select the tour you wish to add steps to. + theme - Sets the default theme for the tour. + Choose from "light"/"arrows", "dark", "default", "square", + and "square-dark". ("light" is used if None is selected.)""" + shepherd_theme = "shepherd-theme-arrows" + if theme: + if theme.lower() == "default": + shepherd_theme = "shepherd-theme-default" + elif theme.lower() == "dark": + shepherd_theme = "shepherd-theme-dark" + elif theme.lower() == "light": + shepherd_theme = "shepherd-theme-arrows" + elif theme.lower() == "arrows": + shepherd_theme = "shepherd-theme-arrows" + elif theme.lower() == "square": + shepherd_theme = "shepherd-theme-square" + elif theme.lower() == "square-dark": + shepherd_theme = "shepherd-theme-square-dark" + if not name: + name = "default" + new_tour = ( + """ + // Shepherd Tour + var tour = new Shepherd.Tour({ + defaults: { + classes: '%s', + scrollTo: true + } + }); + var allButtons = { + skip: { + text: "Skip", + action: tour.cancel, + classes: 'shepherd-button-secondary tour-button-left' + }, + back: { + text: "Back", + action: tour.back, + classes: 'shepherd-button-secondary' + }, + next: { + text: "Next", + action: tour.next, + classes: 'shepherd-button-primary tour-button-right' + }, + }; + var firstStepButtons = [allButtons.skip, allButtons.next]; + var midTourButtons = [allButtons.back, allButtons.next]; + """ + % shepherd_theme + ) + self._tour_steps[name] = [] + self._tour_steps[name].append(new_tour) - def wait_for_and_accept_alert(self, timeout=settings.LARGE_TIMEOUT): - if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - return page_actions.wait_for_and_accept_alert(self.driver, timeout) + def create_bootstrap_tour(self, name=None): + """Creates a Bootstrap tour for a website. + @Params + name - If creating multiple tours at the same time, + use this to select the tour you wish to add steps to.""" + if not name: + name = "default" + new_tour = """ + // Bootstrap Tour + var tour = new Tour({ + container: 'body', + animation: true, + keyboard: true, + orphan: true, + smartPlacement: true, + autoscroll: true, + backdrop: true, + backdropContainer: 'body', + backdropPadding: 3, + }); + tour.addSteps([ + """ + self._tour_steps[name] = [] + self._tour_steps[name].append(new_tour) - def wait_for_and_dismiss_alert(self, timeout=settings.LARGE_TIMEOUT): - if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - return page_actions.wait_for_and_dismiss_alert(self.driver, timeout) + def create_driverjs_tour(self, name=None): + """Creates a DriverJS tour for a website. + @Params + name - If creating multiple tours at the same time, + use this to select the tour you wish to add steps to.""" + if not name: + name = "default" + new_tour = """ + // DriverJS Tour + var tour = new Driver({ + opacity: 0.24, // Background opacity (0: no popover / overlay) + padding: 6, // Distance of element from around the edges + allowClose: false, // Whether clicking on overlay should close + overlayClickNext: false, // Move to next step on overlay click + doneBtnText: 'Done', // Text that appears on the Done button + closeBtnText: 'Close', // Text appearing on the Close button + nextBtnText: 'Next', // Text that appears on the Next button + prevBtnText: 'Previous', // Text appearing on Previous button + showButtons: true, // This shows control buttons in the footer + keyboardControl: true, // (escape to close, arrow keys to move) + animate: true, // Animate while changing highlighted element + }); + tour.defineSteps([ + """ + self._tour_steps[name] = [] + self._tour_steps[name].append(new_tour) - def wait_for_and_switch_to_alert(self, timeout=settings.LARGE_TIMEOUT): - if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - return page_actions.wait_for_and_switch_to_alert(self.driver, timeout) + def create_hopscotch_tour(self, name=None): + """Creates a Hopscotch tour for a website. + @Params + name - If creating multiple tours at the same time, + use this to select the tour you wish to add steps to.""" + if not name: + name = "default" - def switch_to_frame(self, frame, timeout=settings.SMALL_TIMEOUT): - """ Sets driver control to the specified browser frame. """ - if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - page_actions.switch_to_frame(self.driver, frame, timeout) + new_tour = """ + // Hopscotch Tour + var tour = { + id: "hopscotch_tour", + steps: [ + """ - def switch_to_default_content(self): - """ Brings driver control outside the current iframe. - (If driver control is inside an iframe, the driver control - will be set to one level above the current frame. If the driver - control is not currenly in an iframe, nothing will happen.) """ - self.driver.switch_to.default_content() + self._tour_steps[name] = [] + self._tour_steps[name].append(new_tour) - def open_new_window(self, switch_to=True): - """ Opens a new browser tab/window and switches to it by default. """ - self.driver.execute_script("window.open('');") - time.sleep(0.01) - if switch_to: - self.switch_to_window(len(self.driver.window_handles) - 1) + def create_introjs_tour(self, name=None): + """Creates an IntroJS tour for a website. + @Params + name - If creating multiple tours at the same time, + use this to select the tour you wish to add steps to.""" + if not hasattr(sb_config, "introjs_theme_color"): + sb_config.introjs_theme_color = constants.TourColor.theme_color + if not hasattr(sb_config, "introjs_hover_color"): + sb_config.introjs_hover_color = constants.TourColor.hover_color + if not name: + name = "default" + new_tour = """ + // IntroJS Tour + function startIntro(){ + var intro = introJs(); + intro.setOptions({ + steps: [ + """ + self._tour_steps[name] = [] + self._tour_steps[name].append(new_tour) - def switch_to_window(self, window, timeout=settings.SMALL_TIMEOUT): - if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - page_actions.switch_to_window(self.driver, window, timeout) + def set_introjs_colors(self, theme_color=None, hover_color=None): + """Use this method to set the theme colors for IntroJS tours. + Args must be hex color values that start with a "#" sign. + If a color isn't specified, the color will reset to the default. + The border color of buttons is set to the hover color. + @Params + theme_color - The color of buttons. + hover_color - The color of buttons after hovering over them.""" + if not hasattr(sb_config, "introjs_theme_color"): + sb_config.introjs_theme_color = constants.TourColor.theme_color + if not hasattr(sb_config, "introjs_hover_color"): + sb_config.introjs_hover_color = constants.TourColor.hover_color + if theme_color: + match = re.search(r"^#(?:[0-9a-fA-F]{3}){1,2}$", theme_color) + if not match: + raise Exception( + 'Expecting a hex value color that starts with "#"!' + ) + sb_config.introjs_theme_color = theme_color + else: + sb_config.introjs_theme_color = constants.TourColor.theme_color + if hover_color: + match = re.search(r"^#(?:[0-9a-fA-F]{3}){1,2}$", hover_color) + if not match: + raise Exception( + 'Expecting a hex value color that starts with "#"!' + ) + sb_config.introjs_hover_color = hover_color + else: + sb_config.introjs_hover_color = constants.TourColor.hover_color + + def add_tour_step( + self, + message, + selector=None, + name=None, + title=None, + theme=None, + alignment=None, + duration=None, + ): + """Allows the user to add tour steps for a website. + @Params + message - The message to display. + selector - The CSS Selector of the Element to attach to. + name - If creating multiple tours at the same time, + use this to select the tour you wish to add steps to. + title - Additional header text that appears above the message. + theme - (Shepherd Tours ONLY) The styling of the tour step. + Choose from "light"/"arrows", "dark", "default", "square", + and "square-dark". ("arrows" is used if None is selected.) + alignment - Choose from "top", "bottom", "left", and "right". + ("top" is default, except for Hopscotch and DriverJS). + duration - (Bootstrap Tours ONLY) The amount of time, in seconds, + before automatically advancing to the next tour step.""" + if not selector: + selector = "html" + if page_utils.is_name_selector(selector): + name = page_utils.get_name_from_selector(selector) + selector = '[name="%s"]' % name + if page_utils.is_xpath_selector(selector): + selector = self.convert_to_css_selector(selector, By.XPATH) + selector = self.__escape_quotes_if_needed(selector) + if not name: + name = "default" + if name not in self._tour_steps: + # By default, will create an IntroJS tour if no tours exist + self.create_tour(name=name, theme="introjs") + if not title: + title = "" + title = self.__escape_quotes_if_needed(title) + if message: + message = self.__escape_quotes_if_needed(message) + else: + message = "" + if not alignment or alignment not in [ + "top", + "bottom", + "left", + "right", + ]: + t_name = self._tour_steps[name][0] + if "Hopscotch" not in t_name and "DriverJS" not in t_name: + alignment = "top" + else: + alignment = "bottom" + if "Bootstrap" in self._tour_steps[name][0]: + self.__add_bootstrap_tour_step( + message, + selector=selector, + name=name, + title=title, + alignment=alignment, + duration=duration, + ) + elif "DriverJS" in self._tour_steps[name][0]: + self.__add_driverjs_tour_step( + message, + selector=selector, + name=name, + title=title, + alignment=alignment, + ) + elif "Hopscotch" in self._tour_steps[name][0]: + self.__add_hopscotch_tour_step( + message, + selector=selector, + name=name, + title=title, + alignment=alignment, + ) + elif "IntroJS" in self._tour_steps[name][0]: + self.__add_introjs_tour_step( + message, + selector=selector, + name=name, + title=title, + alignment=alignment, + ) + else: + self.__add_shepherd_tour_step( + message, + selector=selector, + name=name, + title=title, + theme=theme, + alignment=alignment, + ) + + def __add_shepherd_tour_step( + self, + message, + selector=None, + name=None, + title=None, + theme=None, + alignment=None, + ): + """Allows the user to add tour steps for a website. + @Params + message - The message to display. + selector - The CSS Selector of the Element to attach to. + name - If creating multiple tours at the same time, + use this to select the tour you wish to add steps to. + title - Additional header text that appears above the message. + theme - (Shepherd Tours ONLY) The styling of the tour step. + Choose from "light"/"arrows", "dark", "default", "square", + and "square-dark". ("arrows" is used if None is selected.) + alignment - Choose from "top", "bottom", "left", and "right". + ("top" is the default alignment).""" + if theme == "default": + shepherd_theme = "shepherd-theme-default" + elif theme == "dark": + shepherd_theme = "shepherd-theme-dark" + elif theme == "light": + shepherd_theme = "shepherd-theme-arrows" + elif theme == "arrows": + shepherd_theme = "shepherd-theme-arrows" + elif theme == "square": + shepherd_theme = "shepherd-theme-square" + elif theme == "square-dark": + shepherd_theme = "shepherd-theme-square-dark" + else: + shepherd_base_theme = re.search( + r"[\S\s]+classes: '([\S\s]+)',[\S\s]+", + self._tour_steps[name][0], + ).group(1) + shepherd_theme = shepherd_base_theme + shepherd_classes = shepherd_theme + if selector == "html": + shepherd_classes += " shepherd-orphan" + buttons = "firstStepButtons" + if len(self._tour_steps[name]) > 1: + buttons = "midTourButtons" + step = """tour.addStep('%s', { + title: '%s', + classes: '%s', + text: '%s', + attachTo: {element: '%s', on: '%s'}, + buttons: %s, + advanceOn: '.docs-link click' + });""" % ( + name, + title, + shepherd_classes, + message, + selector, + alignment, + buttons, + ) + self._tour_steps[name].append(step) - def switch_to_default_window(self): - self.switch_to_window(0) + def __add_bootstrap_tour_step( + self, + message, + selector=None, + name=None, + title=None, + alignment=None, + duration=None, + ): + """Allows the user to add tour steps for a website. + @Params + message - The message to display. + selector - The CSS Selector of the Element to attach to. + name - If creating multiple tours at the same time, + use this to select the tour you wish to add steps to. + title - Additional header text that appears above the message. + alignment - Choose from "top", "bottom", "left", and "right". + ("top" is the default alignment). + duration - (Bootstrap Tours ONLY) The amount of time, in seconds, + before automatically advancing to the next tour step.""" + if selector != "html": + selector = self.__make_css_match_first_element_only(selector) + element_row = "element: '%s'," % selector + else: + element_row = "" + if not duration: + duration = "0" + else: + duration = str(float(duration) * 1000.0) + bd = "backdrop: true," + if selector == "html": + bd = "backdrop: false," + step = """{ + %s + title: '%s', + content: '%s', + orphan: true, + autoscroll: true, + %s + placement: '%s', + smartPlacement: true, + duration: %s, + },""" % ( + element_row, + title, + message, + bd, + alignment, + duration, + ) + self._tour_steps[name].append(step) - def check_window(self, name="default", level=0, baseline=False): - """ *** Automated Visual Testing with SeleniumBase *** - - The first time a test calls self.check_window() for a unique "name" - parameter provided, it will set a visual baseline, meaning that it - creates a folder, saves the URL to a file, saves the current window - screenshot to a file, and creates the following three files - with the listed data saved: - tags_level1.txt -> HTML tags from the window - tags_level2.txt -> HTML tags + attributes from the window - tags_level3.txt -> HTML tags + attributes/values from the window - - Baseline folders are named based on the test name and the name - parameter passed to self.check_window(). The same test can store - multiple baseline folders. - - If the baseline is being set/reset, the "level" doesn't matter. - - After the first run of self.check_window(), it will compare the - HTML tags of the latest window to the one from the initial run. - Here's how the level system works: - * level=0 -> - DRY RUN ONLY - Will perform a comparison to the baseline, and - print out any differences that are found, but - won't fail the test even if differences exist. - * level=1 -> - HTML tags are compared to tags_level1.txt - * level=2 -> - HTML tags are compared to tags_level1.txt and - HTML tags/attributes are compared to tags_level2.txt - * level=3 -> - HTML tags are compared to tags_level1.txt and - HTML tags + attributes are compared to tags_level2.txt and - HTML tags + attributes/values are compared to tags_level3.txt - As shown, Level-3 is the most strict, Level-1 is the least strict. - If the comparisons from the latest window to the existing baseline - don't match, the current test will fail, except for Level-0 tests. - - You can reset the visual baseline on the command line by using: - --visual_baseline - As long as "--visual_baseline" is used on the command line while - running tests, the self.check_window() method cannot fail because - it will rebuild the visual baseline rather than comparing the html - tags of the latest run to the existing baseline. If there are any - expected layout changes to a website that you're testing, you'll - need to reset the baseline to prevent unnecessary failures. - - self.check_window() will fail with "Page Domain Mismatch Failure" - if the page domain doesn't match the domain of the baseline. - - If you want to use self.check_window() to compare a web page to - a later version of itself from within the same test run, you can - add the parameter "baseline=True" to the first time you call - self.check_window() in a test to use that as the baseline. This - only makes sense if you're calling self.check_window() more than - once with the same name parameter in the same test. - - Automated Visual Testing with self.check_window() is not very - effective for websites that have dynamic content that changes - the layout and structure of web pages. For those, you're much - better off using regular SeleniumBase functional testing. - - Example usage: - self.check_window(name="testing", level=0) - self.check_window(name="xkcd_home", level=1) - self.check_window(name="github_page", level=2) - self.check_window(name="wikipedia_page", level=3) - """ - if level == "0": - level = 0 - if level == "1": - level = 1 - if level == "2": - level = 2 - if level == "3": - level = 3 - if level != 0 and level != 1 and level != 2 and level != 3: - raise Exception('Parameter "level" must be set to 0, 1, 2, or 3!') + def __add_driverjs_tour_step( + self, message, selector=None, name=None, title=None, alignment=None + ): + """Allows the user to add tour steps for a website. + @Params + message - The message to display. + selector - The CSS Selector of the Element to attach to. + name - If creating multiple tours at the same time, + use this to select the tour you wish to add steps to. + title - Additional header text that appears above the message. + alignment - Choose from "top", "bottom", "left", and "right". + ("top" is the default alignment).""" + message = ( + '' + message + "" + ) + title_row = "" + if not title: + title_row = "title: '%s'," % message + message = "" + else: + title_row = "title: '%s'," % title + align_row = "position: '%s'," % alignment + ani_row = "animate: true," + if not selector or selector == "html" or selector == "body": + selector = "body" + ani_row = "animate: false," + align_row = "position: '%s'," % "mid-center" + element_row = "element: '%s'," % selector + desc_row = "description: '%s'," % message + step = """{ + %s + %s + popover: { + className: 'popover-class', + %s + %s + %s + } + },""" % ( + element_row, + ani_row, + title_row, + desc_row, + align_row, + ) + self._tour_steps[name].append(step) - module = self.__class__.__module__ - if '.' in module and len(module.split('.')[-1]) > 1: - module = module.split('.')[-1] - test_id = "%s.%s" % (module, self._testMethodName) - if not name or len(name) < 1: - name = "default" - name = str(name) - visual_helper.visual_baseline_folder_setup() - baseline_dir = constants.VisualBaseline.STORAGE_FOLDER - visual_baseline_path = baseline_dir + "/" + test_id + "/" + name - page_url_file = visual_baseline_path + "/page_url.txt" - screenshot_file = visual_baseline_path + "/screenshot.png" - level_1_file = visual_baseline_path + "/tags_level_1.txt" - level_2_file = visual_baseline_path + "/tags_level_2.txt" - level_3_file = visual_baseline_path + "/tags_level_3.txt" + def __add_hopscotch_tour_step( + self, message, selector=None, name=None, title=None, alignment=None + ): + """Allows the user to add tour steps for a website. + @Params + message - The message to display. + selector - The CSS Selector of the Element to attach to. + name - If creating multiple tours at the same time, + use this to select the tour you wish to add steps to. + title - Additional header text that appears above the message. + alignment - Choose from "top", "bottom", "left", and "right". + ("bottom" is the default alignment).""" + arrow_offset_row = None + if not selector or selector == "html": + selector = "head" + alignment = "bottom" + arrow_offset_row = "arrowOffset: '200'," + else: + arrow_offset_row = "" + step = """{ + target: '%s', + title: '%s', + content: '%s', + %s + showPrevButton: 'true', + scrollDuration: '550', + placement: '%s'}, + """ % ( + selector, + title, + message, + arrow_offset_row, + alignment, + ) + self._tour_steps[name].append(step) - set_baseline = False - if baseline or self.visual_baseline: - set_baseline = True - if not os.path.exists(visual_baseline_path): - set_baseline = True - try: - os.makedirs(visual_baseline_path) - except Exception: - pass # Only reachable during multi-threaded test runs - if not os.path.exists(page_url_file): - set_baseline = True - if not os.path.exists(screenshot_file): - set_baseline = True - if not os.path.exists(level_1_file): - set_baseline = True - if not os.path.exists(level_2_file): - set_baseline = True - if not os.path.exists(level_3_file): - set_baseline = True + def __add_introjs_tour_step( + self, message, selector=None, name=None, title=None, alignment=None + ): + """Allows the user to add tour steps for a website. + @Params + message - The message to display. + selector - The CSS Selector of the Element to attach to. + name - If creating multiple tours at the same time, + use this to select the tour you wish to add steps to. + title - Additional header text that appears above the message. + alignment - Choose from "top", "bottom", "left", and "right". + ("top" is the default alignment).""" + if selector != "html": + element_row = "element: '%s'," % selector + else: + element_row = "" + if title: + message = "
      " + title + "

      " + message + message = '' + message + "" + step = """{%s + intro: '%s', + position: '%s'},""" % ( + element_row, + message, + alignment, + ) + self._tour_steps[name].append(step) - page_url = self.get_current_url() - soup = self.get_beautiful_soup() - html_tags = soup.body.find_all() - level_1 = [[tag.name] for tag in html_tags] - level_1 = json.loads(json.dumps(level_1)) # Tuples become lists - level_2 = [[tag.name, sorted(tag.attrs.keys())] for tag in html_tags] - level_2 = json.loads(json.dumps(level_2)) # Tuples become lists - level_3 = [[tag.name, sorted(tag.attrs.items())] for tag in html_tags] - level_3 = json.loads(json.dumps(level_3)) # Tuples become lists + def play_tour(self, name=None, interval=0): + """Plays a tour on the current website. + @Params + name - If creating multiple tours at the same time, + use this to select the tour you wish to add steps to. + interval - The delay time between autoplaying tour steps. (Seconds) + If set to 0 (default), the tour is fully manual control.""" + from seleniumbase.core import tour_helper + + if self.headless or self.headless2 or self.xvfb: + return # Tours should not run in headless mode. + self.wait_for_ready_state_complete() + if not interval: + interval = 0 + if interval == 0 and self.interval: + interval = float(self.interval) + if not name: + name = "default" + if name not in self._tour_steps: + raise Exception("Tour {%s} does not exist!" % name) + if "Bootstrap" in self._tour_steps[name][0]: + tour_helper.play_bootstrap_tour( + self.driver, + self._tour_steps, + self.browser, + self.message_duration, + name=name, + interval=interval, + ) + elif "DriverJS" in self._tour_steps[name][0]: + tour_helper.play_driverjs_tour( + self.driver, + self._tour_steps, + self.browser, + self.message_duration, + name=name, + interval=interval, + ) + elif "Hopscotch" in self._tour_steps[name][0]: + tour_helper.play_hopscotch_tour( + self.driver, + self._tour_steps, + self.browser, + self.message_duration, + name=name, + interval=interval, + ) + elif "IntroJS" in self._tour_steps[name][0]: + tour_helper.play_introjs_tour( + self.driver, + self._tour_steps, + self.browser, + self.message_duration, + name=name, + interval=interval, + ) + else: + tour_helper.play_shepherd_tour( + self.driver, + self._tour_steps, + self.message_duration, + name=name, + interval=interval, + ) - if set_baseline: - self.save_screenshot("screenshot.png", visual_baseline_path) - out_file = codecs.open(page_url_file, "w+") - out_file.writelines(page_url) - out_file.close() - out_file = codecs.open(level_1_file, "w+") - out_file.writelines(json.dumps(level_1)) - out_file.close() - out_file = codecs.open(level_2_file, "w+") - out_file.writelines(json.dumps(level_2)) - out_file.close() - out_file = codecs.open(level_3_file, "w+") - out_file.writelines(json.dumps(level_3)) - out_file.close() + def start_tour(self, name=None, interval=0): + """Same as self.play_tour()""" + self.play_tour(name=name, interval=interval) - if not set_baseline: - f = open(page_url_file, 'r') - page_url_data = f.read().strip() - f.close() - f = open(level_1_file, 'r') - level_1_data = json.loads(f.read()) - f.close() - f = open(level_2_file, 'r') - level_2_data = json.loads(f.read()) - f.close() - f = open(level_3_file, 'r') - level_3_data = json.loads(f.read()) - f.close() + def export_tour(self, name=None, filename="my_tour.js", url=None): + """Exports a tour as a JS file. + You can call self.export_tour() anywhere where you would + normally use self.play_tour() to play a website tour. + It will include necessary resources as well, such as jQuery. + You'll be able to copy the tour directly into the Console of + any web browser to play the tour outside of SeleniumBase runs. + @Params + name - If creating multiple tours at the same time, + use this to select the tour you wish to add steps to. + filename - The name of the JavaScript file that you wish to + save the tour to. + url - The URL where the tour starts. If not specified, the URL + of the current page will be used.""" + from seleniumbase.core import tour_helper - domain_fail = ( - "Page Domain Mismatch Failure: " - "Current Page Domain doesn't match the Page Domain of the " - "Baseline! Can't compare two completely different sites! " - "Run with --visual_baseline to reset the baseline!") - level_1_failure = ( - "\n\n*** Exception: Visual Diff Failure:\n" - "* HTML tags don't match the baseline!") - level_2_failure = ( - "\n\n*** Exception: Visual Diff Failure:\n" - "* HTML tag attributes don't match the baseline!") - level_3_failure = ( - "\n\n*** Exception: Visual Diff Failure:\n" - "* HTML tag attribute values don't match the baseline!") + if not url: + url = self.get_current_url() + tour_helper.export_tour( + self._tour_steps, name=name, filename=filename, url=url + ) - page_domain = self.get_domain_url(page_url) - page_data_domain = self.get_domain_url(page_url_data) - unittest.TestCase.maxDiff = 1000 - if level == 1 or level == 2 or level == 3: - self.assert_equal(page_domain, page_data_domain, domain_fail) - self.assert_equal(level_1, level_1_data, level_1_failure) - unittest.TestCase.maxDiff = None - if level == 2 or level == 3: - self.assert_equal(level_2, level_2_data, level_2_failure) - if level == 3: - self.assert_equal(level_3, level_3_data, level_3_failure) - if level == 0: - try: - unittest.TestCase.maxDiff = 1000 - self.assert_equal( - page_domain, page_data_domain, domain_fail) - self.assert_equal(level_1, level_1_data, level_1_failure) - unittest.TestCase.maxDiff = None - self.assert_equal(level_2, level_2_data, level_2_failure) - self.assert_equal(level_3, level_3_data, level_3_failure) - except Exception as e: - print(e) # Level-0 Dry Run (Only print the differences) + ############ - def save_screenshot(self, name, folder=None): - """ The screenshot will be in PNG format. """ - return page_actions.save_screenshot(self.driver, name, folder) + def activate_jquery_confirm(self): + """See https://craftpip.github.io/jquery-confirm/ for usage.""" + self.__check_scope() + self._check_browser() + js_utils.activate_jquery_confirm(self.driver) + self.wait_for_ready_state_complete() - def get_new_driver(self, browser=None, headless=None, - servername=None, port=None, proxy=None, agent=None, - switch_to=True, cap_file=None, disable_csp=None): - """ This method spins up an extra browser for tests that require - more than one. The first browser is already provided by tests - that import base_case.BaseCase from seleniumbase. If parameters - aren't specified, the method uses the same as the default driver. - @Params - browser - the browser to use. (Ex: "chrome", "firefox") - headless - the option to run webdriver in headless mode - servername - if using a Selenium Grid, set the host address here - port - if using a Selenium Grid, set the host port here - proxy - if using a proxy server, specify the "host:port" combo here - switch_to - the option to switch to the new driver (default = True) + def set_jqc_theme(self, theme, color=None, width=None): + """Sets the default jquery-confirm theme and width (optional). + Available themes: "bootstrap", "modern", "material", "supervan", + "light", "dark", and "seamless". + Available colors: (This sets the BORDER color, NOT the button color.) + "blue", "default", "green", "red", "purple", "orange", "dark". + Width can be set using percent or pixels. Eg: "36.0%", "450px".""" + if not self.__changed_jqc_theme: + self.__jqc_default_theme = constants.JqueryConfirm.DEFAULT_THEME + self.__jqc_default_color = constants.JqueryConfirm.DEFAULT_COLOR + self.__jqc_default_width = constants.JqueryConfirm.DEFAULT_WIDTH + valid_themes = [ + "bootstrap", + "modern", + "material", + "supervan", + "light", + "dark", + "seamless", + ] + if theme.lower() not in valid_themes: + raise Exception( + "%s is not a valid jquery-confirm theme! " + "Select from %s" % (theme.lower(), valid_themes) + ) + constants.JqueryConfirm.DEFAULT_THEME = theme.lower() + if color: + valid_colors = [ + "blue", + "default", + "green", + "red", + "purple", + "orange", + "dark", + ] + if color.lower() not in valid_colors: + raise Exception( + "%s is not a valid jquery-confirm border color! " + "Select from %s" % (color.lower(), valid_colors) + ) + constants.JqueryConfirm.DEFAULT_COLOR = color.lower() + if width: + if isinstance(width, (int, float)): + # Convert to a string if a number is given + width = str(width) + if width.isnumeric(): + if int(width) <= 0: + raise Exception("Width must be set to a positive number!") + elif int(width) <= 100: + width = str(width) + "%" + else: + width = str(width) + "px" # Use pixels if width is > 100 + if not width.endswith("%") and not width.endswith("px"): + raise Exception( + "jqc width must end with %% for percent or px for pixels!" + ) + value = None + if width.endswith("%"): + value = width[:-1] + if width.endswith("px"): + value = width[:-2] + try: + value = float(value) + except Exception: + raise Exception("%s is not a numeric value!" % value) + if value <= 0: + raise Exception("%s is not a positive number!" % value) + constants.JqueryConfirm.DEFAULT_WIDTH = width + + def reset_jqc_theme(self): + """Resets the jqc theme settings to factory defaults.""" + if self.__changed_jqc_theme: + constants.JqueryConfirm.DEFAULT_THEME = self.__jqc_default_theme + constants.JqueryConfirm.DEFAULT_COLOR = self.__jqc_default_color + constants.JqueryConfirm.DEFAULT_WIDTH = self.__jqc_default_width + self.__changed_jqc_theme = False + + def get_jqc_button_input(self, message, buttons, options=None): """ - if self.browser == "remote" and self.servername == "localhost": - raise Exception('Cannot use "remote" browser driver on localhost!' - ' Did you mean to connect to a remote Grid server' - ' such as BrowserStack or Sauce Labs? In that' - ' case, you must specify the "server" and "port"' - ' parameters on the command line! ' - 'Example: ' - '--server=user:key@hub.browserstack.com --port=80') - browserstack_ref = ( - 'https://browserstack.com/automate/capabilities') - sauce_labs_ref = ( - 'https://wiki.saucelabs.com/display/DOCS/Platform+Configurator#/') - if self.browser == "remote" and not self.cap_file: - raise Exception('Need to specify a desired capabilities file when ' - 'using "--browser=remote". Add "--cap_file=FILE". ' - 'File should be in the Python format used by: ' - '%s OR ' - '%s ' - 'See SeleniumBase/examples/sample_cap_file_BS.py ' - 'and SeleniumBase/examples/sample_cap_file_SL.py' - % (browserstack_ref, sauce_labs_ref)) - if browser is None: - browser = self.browser - browser_name = browser - if headless is None: - headless = self.headless - if servername is None: - servername = self.servername - if port is None: - port = self.port - use_grid = False - if servername != "localhost": - # Use Selenium Grid (Use "127.0.0.1" for localhost Grid) - use_grid = True - proxy_string = proxy - if proxy_string is None: - proxy_string = self.proxy_string - user_agent = agent - if user_agent is None: - user_agent = self.user_agent - if disable_csp is None: - disable_csp = self.disable_csp - if self.demo_mode or self.masterqa_mode: - disable_csp = True - if cap_file is None: - cap_file = self.cap_file - valid_browsers = constants.ValidBrowsers.valid_browsers - if browser_name not in valid_browsers: - raise Exception("Browser: {%s} is not a valid browser option. " - "Valid options = {%s}" % (browser, valid_browsers)) - # Launch a web browser - from seleniumbase.core import browser_launcher - new_driver = browser_launcher.get_driver(browser_name=browser_name, - headless=headless, - use_grid=use_grid, - servername=servername, - port=port, - proxy_string=proxy_string, - user_agent=user_agent, - cap_file=cap_file, - disable_csp=disable_csp) - self._drivers_list.append(new_driver) - if switch_to: - self.driver = new_driver - if self.headless: - # Make sure the invisible browser window is big enough - try: - self.set_window_size(1440, 1080) - self.wait_for_ready_state_complete() - except Exception: - # This shouldn't fail, but in case it does, - # get safely through setUp() so that - # WebDrivers can get closed during tearDown(). - pass + Pop up a jquery-confirm box and return the text of the button clicked. + If running in headless mode, the last button text is returned. + @Params + message: The message to display in the jquery-confirm dialog. + buttons: A list of tuples for text and color. + Example: [("Yes!", "green"), ("No!", "red")] + Available colors: blue, green, red, orange, purple, default, dark. + A simple text string also works: "My Button". (Uses default color.) + options: A list of tuples for options to set. + Example: [("theme", "bootstrap"), ("width", "450px")] + Available theme options: bootstrap, modern, material, supervan, + light, dark, and seamless. + Available colors: (For the BORDER color, NOT the button color.) + "blue", "default", "green", "red", "purple", "orange", "dark". + Example option for changing the border color: ("color", "default") + Width can be set using percent or pixels. Eg: "36.0%", "450px".""" + from seleniumbase.core import jqc_helper + + if message and not isinstance(message, str): + raise Exception('Expecting a string for arg: "message"!') + if not isinstance(buttons, (list, tuple)): + raise Exception('Expecting a list or tuple for arg: "button"!') + if len(buttons) < 1: + raise Exception('List "buttons" requires at least one button!') + new_buttons = [] + for button in buttons: + if isinstance(button, (list, tuple)) and (len(button) == 1): + new_buttons.append(button[0]) + elif isinstance(button, (list, tuple)) and (len(button) > 1): + new_buttons.append((button[0], str(button[1]).lower())) else: - if self.browser == 'chrome' or self.browser == 'opera': - try: - self.driver.set_window_size(1250, 840) - self.wait_for_ready_state_complete() - except Exception: - pass # Keep existing browser resolution - elif self.browser == 'edge': - try: - self.driver.maximize_window() - self.wait_for_ready_state_complete() - except Exception: - pass # Keep existing browser resolution - return new_driver - - def switch_to_driver(self, driver): - """ Sets self.driver to the specified driver. - You may need this if using self.get_new_driver() in your code. """ - self.driver = driver - - def switch_to_default_driver(self): - """ Sets self.driver to the default/original driver. """ - self.driver = self._default_driver + new_buttons.append((str(button), "")) + buttons = new_buttons + if options: + for option in options: + if not isinstance(option, (list, tuple)): + raise Exception('"options" should be a list of tuples!') + if self.headless or self.headless2 or self.xvfb: + return buttons[-1][0] + jqc_helper.jquery_confirm_button_dialog( + self.driver, message, buttons, options + ) + time.sleep(0.02) + jf = "document.querySelector('.jconfirm-box').focus();" + with suppress(Exception): + self.execute_script(jf) + waiting_for_response = True + while waiting_for_response: + time.sleep(0.05) + jqc_open = self.execute_script("return jconfirm.instances.length;") + if str(jqc_open) == "0": + break + time.sleep(0.1) + status = None + try: + status = self.execute_script("return $jqc_status;") + except Exception: + status = self.execute_script("return jconfirm.lastButtonText;") + return status - ############ + def get_jqc_text_input(self, message, button=None, options=None): + """ + Pop up a jquery-confirm box and return the text submitted by the input. + If running in headless mode, the text returned is "" by default. + @Params + message: The message to display in the jquery-confirm dialog. + button: A 2-item list or tuple for text and color. Or just the text. + Example: ["Submit", "blue"] -> (default button if not specified) + Available colors: blue, green, red, orange, purple, default, dark. + A simple text string also works: "My Button". (Uses default color.) + options: A list of tuples for options to set. + Example: [("theme", "bootstrap"), ("width", "450px")] + Available theme options: bootstrap, modern, material, supervan, + light, dark, and seamless. + Available colors: (For the BORDER color, NOT the button color.) + "blue", "default", "green", "red", "purple", "orange", "dark". + Example option for changing the border color: ("color", "default") + Width can be set using percent or pixels. Eg: "36.0%", "450px".""" + from seleniumbase.core import jqc_helper + + if message and not isinstance(message, str): + raise Exception('Expecting a string for arg: "message"!') + if button: + if isinstance(button, (list, tuple)) and (len(button) == 1): + button = (str(button[0]), "") + elif isinstance(button, (list, tuple)) and (len(button) > 1): + valid_colors = [ + "blue", + "default", + "green", + "red", + "purple", + "orange", + "dark", + ] + detected_color = str(button[1]).lower() + if str(button[1]).lower() not in valid_colors: + raise Exception( + "%s is an invalid jquery-confirm button color!\n" + "Select from %s" % (detected_color, valid_colors) + ) + button = (str(button[0]), str(button[1]).lower()) + else: + button = (str(button), "") + else: + button = ("Submit", "blue") + if options: + for option in options: + if not isinstance(option, (list, tuple)): + raise Exception('"options" should be a list of tuples!') + if self.headless or self.headless2 or self.xvfb: + return "" + jqc_helper.jquery_confirm_text_dialog( + self.driver, message, button, options + ) + time.sleep(0.02) + jf = "document.querySelector('.jconfirm-box input.jqc_input').focus();" + with suppress(Exception): + self.execute_script(jf) + waiting_for_response = True + while waiting_for_response: + time.sleep(0.05) + jqc_open = self.execute_script("return jconfirm.instances.length;") + if str(jqc_open) == "0": + break + time.sleep(0.1) + status = None + try: + status = self.execute_script("return $jqc_input;") + except Exception: + status = self.execute_script("return jconfirm.lastInputText;") + return status - def __get_new_timeout(self, timeout): - """ When using --timeout_multiplier=#.# """ + def get_jqc_form_inputs(self, message, buttons, options=None): + """ + Pop up a jquery-confirm box and return the input/button texts as tuple. + If running in headless mode, returns the ("", buttons[-1][0]) tuple. + @Params + message: The message to display in the jquery-confirm dialog. + buttons: A list of tuples for text and color. + Example: [("Yes!", "green"), ("No!", "red")] + Available colors: blue, green, red, orange, purple, default, dark. + A simple text string also works: "My Button". (Uses default color.) + options: A list of tuples for options to set. + Example: [("theme", "bootstrap"), ("width", "450px")] + Available theme options: bootstrap, modern, material, supervan, + light, dark, and seamless. + Available colors: (For the BORDER color, NOT the button color.) + "blue", "default", "green", "red", "purple", "orange", "dark". + Example option for changing the border color: ("color", "default") + Width can be set using percent or pixels. Eg: "36.0%", "450px".""" + from seleniumbase.core import jqc_helper + + if message and not isinstance(message, str): + raise Exception('Expecting a string for arg: "message"!') + if not isinstance(buttons, (list, tuple)): + raise Exception('Expecting a list or tuple for arg: "button"!') + if len(buttons) < 1: + raise Exception('List "buttons" requires at least one button!') + new_buttons = [] + for button in buttons: + if isinstance(button, (list, tuple)) and (len(button) == 1): + new_buttons.append(button[0]) + elif isinstance(button, (list, tuple)) and (len(button) > 1): + new_buttons.append((button[0], str(button[1]).lower())) + else: + new_buttons.append((str(button), "")) + buttons = new_buttons + if options: + for option in options: + if not isinstance(option, (list, tuple)): + raise Exception('"options" should be a list of tuples!') + if self.headless or self.headless2 or self.xvfb: + return ("", buttons[-1][0]) + jqc_helper.jquery_confirm_full_dialog( + self.driver, message, buttons, options + ) + time.sleep(0.02) + jf = "document.querySelector('.jconfirm-box input.jqc_input').focus();" + with suppress(Exception): + self.execute_script(jf) + waiting_for_response = True + while waiting_for_response: + time.sleep(0.05) + jqc_open = self.execute_script("return jconfirm.instances.length;") + if str(jqc_open) == "0": + break + time.sleep(0.1) + text_status = None + button_status = None try: - timeout_multiplier = float(self.timeout_multiplier) - if timeout_multiplier <= 0.5: - timeout_multiplier = 0.5 - timeout = int(math.ceil(timeout_multiplier * timeout)) - return timeout + text_status = self.execute_script("return $jqc_input;") + button_status = self.execute_script("return $jqc_status;") except Exception: - # Wrong data type for timeout_multiplier (expecting int or float) - return timeout + text_status = self.execute_script("return jconfirm.lastInputText;") + button_status = self.execute_script( + "return jconfirm.lastButtonText;" + ) + return (text_status, button_status) ############ - def __get_exception_message(self): - """ This method extracts the message from an exception if there - was an exception that occurred during the test, assuming - that the exception was in a try/except block and not thrown. """ - exception_info = sys.exc_info()[1] - if hasattr(exception_info, 'msg'): - exc_message = exception_info.msg - elif hasattr(exception_info, 'message'): - exc_message = exception_info.message - else: - exc_message = '(Unknown Exception)' - return exc_message + def __are_quotes_escaped(self, string): + return js_utils.are_quotes_escaped(string) - def __get_improved_exception_message(self): - """ - If Chromedriver is out-of-date, make it clear! - Given the high popularity of the following StackOverflow article: - https://stackoverflow.com/questions/49162667/unknown-error- - call-function-result-missing-value-for-selenium-send-keys-even - ... the original error message was not helpful. Tell people directly. - (Only expected when using driver.send_keys() with an old Chromedriver.) - """ - exc_message = self.__get_exception_message() - maybe_using_old_chromedriver = False - if "unknown error: call function result missing" in exc_message: - maybe_using_old_chromedriver = True - if self.browser == 'chrome' and maybe_using_old_chromedriver: - update = ("Your version of ChromeDriver may be out-of-date! " - "Please go to " - "https://sites.google.com/a/chromium.org/chromedriver/ " - "and download the latest version to your system PATH! " - "Or use: ``seleniumbase install chromedriver`` . " - "Original Exception Message: %s" % exc_message) - exc_message = update - return exc_message + def __escape_quotes_if_needed(self, string): + return js_utils.escape_quotes_if_needed(string) - def __add_delayed_assert_failure(self): - """ Add a delayed_assert failure into a list for future processing. """ - current_url = self.driver.current_url - message = self.__get_exception_message() - self.__delayed_assert_failures.append( - "CHECK #%s: (%s)\n %s" % ( - self.__delayed_assert_count, current_url, message)) + def __is_in_frame(self): + return js_utils.is_in_frame(self.driver) + + ############ - def delayed_assert_element(self, selector, by=By.CSS_SELECTOR, - timeout=settings.MINI_TIMEOUT): - """ A non-terminating assertion for an element on a page. - Failures will be saved until the process_delayed_asserts() - method is called from inside a test, likely at the end of it. """ - self.__delayed_assert_count += 1 + def __js_click(self, selector, by="css selector"): + """Clicks an element using pure JS. Does not use jQuery.""" + selector, by = self.__recalculate_selector(selector, by) + css_selector = self.convert_to_css_selector(selector, by=by) + css_selector = re.escape(css_selector) # Add "\\" to special chars + css_selector = self.__escape_quotes_if_needed(css_selector) + is_visible = self.is_element_visible(selector, by=by) + current_url = self.get_current_url() + script = ( + """var simulateClick = function (elem) { + var evt = new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window + }); + var canceled = !elem.dispatchEvent(evt); + }; + var someLink = document.querySelector('%s'); + simulateClick(someLink);""" + % css_selector + ) + if getattr(self, "recorder_mode", None): + self.save_recorded_actions() try: - url = self.get_current_url() - if url == self.__last_url_of_delayed_assert: - timeout = 1 - else: - self.__last_url_of_delayed_assert = url - except Exception: - pass + self.execute_script(script) + except Exception as e: + # If element was visible but no longer, or on a different page now, + # assume that the click actually worked and continue with the test. + if ( + (is_visible and not self.is_element_visible(selector, by=by)) + or current_url != self.get_current_url() + ): + return # The click worked, but threw an Exception. Keep going. + # It appears the first click didn't work. Make another attempt. + self.wait_for_ready_state_complete() + if "Cannot read properties of null" in e.msg: + page_actions.wait_for_element_present( + self.driver, selector, by, timeout=5 + ) + if not page_actions.is_element_clickable( + self.driver, selector, by + ): + with suppress(Exception): + self.wait_for_element_clickable( + selector, by, timeout=1.8 + ) + # If the regular mouse-simulated click fails, do a basic JS click + script = ( + """document.querySelector('%s').click();""" + % css_selector + ) + self.execute_script(script) + + def __js_click_element(self, element): + """Clicks an element using pure JS. Does not use jQuery.""" + is_visible = element.is_displayed() + current_url = self.get_current_url() + script = ( + """var simulateClick = function (elem) { + var evt = new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window + }); + var canceled = !elem.dispatchEvent(evt); + }; + var someLink = arguments[0]; + simulateClick(someLink);""" + ) + if getattr(self, "recorder_mode", None): + self.save_recorded_actions() try: - self.wait_for_element_visible(selector, by=by, timeout=timeout) - return True + self.execute_script(script, element) except Exception: - self.__add_delayed_assert_failure() - return False + # If element was visible but no longer, or on a different page now, + # assume that the click actually worked and continue with the test. + if ( + (is_visible and not element.is_displayed()) + or current_url != self.get_current_url() + ): + return # The click worked, but threw an Exception. Keep going. + # It appears the first click didn't work. Make another attempt. + self.wait_for_ready_state_complete() + # If the regular mouse-simulated click fails, do a basic JS click + script = ("""arguments[0].click();""") + self.execute_script(script, element) - def delayed_assert_text(self, text, selector="html", by=By.CSS_SELECTOR, - timeout=settings.MINI_TIMEOUT): - """ A non-terminating assertion for text from an element on a page. - Failures will be saved until the process_delayed_asserts() - method is called from inside a test, likely at the end of it. """ - self.__delayed_assert_count += 1 + def __js_click_all(self, selector, by="css selector"): + """Clicks all matching elements using pure JS. (No jQuery)""" + selector, by = self.__recalculate_selector(selector, by) + css_selector = self.convert_to_css_selector(selector, by=by) + css_selector = re.escape(css_selector) # Add "\\" to special chars + css_selector = self.__escape_quotes_if_needed(css_selector) + script = ( + """var simulateClick = function (elem) { + var evt = new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window + }); + var canceled = !elem.dispatchEvent(evt); + }; + var $elements = document.querySelectorAll('%s'); + var index = 0, length = $elements.length; + for(; index < length; index++){ + simulateClick($elements[index]);}""" + % css_selector + ) + self.execute_script(script) + + def __click_with_offset( + self, + selector, + x, + y, + by="css selector", + double=False, + mark=None, + timeout=None, + center=None, + scroll=True, + ): + if self.__is_cdp_swap_needed(): + self.cdp.click_with_offset( + selector, x, y, center=center, scroll=scroll + ) + return + self.wait_for_ready_state_complete() + if self.__needs_minimum_wait(): + time.sleep(0.14) + if not timeout: + timeout = settings.SMALL_TIMEOUT + if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + selector, by = self.__recalculate_selector(selector, by) + element = page_actions.wait_for_element_visible( + self.driver, selector, by, timeout + ) + if self.demo_mode: + self.__highlight(selector, by=by, loops=1) + elif self.slow_mode: + if scroll: + self.__slow_scroll_to_element(element) + else: + self.sleep(0.2) + else: + if scroll: + self.__scroll_to_element(element, selector, by) + self.wait_for_ready_state_complete() + if self.__needs_minimum_wait(): + time.sleep(0.03) + if self.demo_mode and mark is None: + mark = True + if mark: + selector = self.convert_to_css_selector(selector, by=by) + selector = re.escape(selector) + selector = self.__escape_quotes_if_needed(selector) + m_x = x + m_y = y + if center: + element_rect = element.rect + left_offset = element_rect["width"] / 2 + top_offset = element_rect["height"] / 2 + m_x = left_offset + (m_x or 0) + m_y = top_offset + (m_y or 0) + px = m_x - 3 + py = m_y - 3 + script = ( + "var canvas = document.querySelector('%s');" + "var ctx = canvas.getContext('2d');" + "ctx.fillStyle = '#F8F808';" + "ctx.fillRect(%s, %s, 7, 7);" + "ctx.fillStyle = '#F80808';" + "ctx.fillRect(%s+1, %s+1, 5, 5);" % (selector, px, py, px, py) + ) + self.execute_script(script) try: - url = self.get_current_url() - if url == self.__last_url_of_delayed_assert: - timeout = 1 - else: - self.__last_url_of_delayed_assert = url + element_location = element.location["y"] + element_location = element_location - constants.Scroll.Y_OFFSET + y + if element_location < 0: + element_location = 0 + scroll_script = "window.scrollTo(0, %s);" % element_location + self.execute_script(scroll_script) + time.sleep(0.12) except Exception: - pass + time.sleep(0.05) + if self.__needs_minimum_wait(): + time.sleep(0.05) try: - self.wait_for_text_visible(text, selector, by=by, timeout=timeout) - return True - except Exception: - self.__add_delayed_assert_failure() - return False - - def process_delayed_asserts(self, print_only=False): - """ To be used with any test that uses delayed_asserts, which are - non-terminating verifications that only raise exceptions - after this method is called. - This is useful for pages with multiple elements to be checked when - you want to find as many bugs as possible in a single test run - before having all the exceptions get raised simultaneously. - Might be more useful if this method is called after processing all - the delayed asserts on a single html page so that the failure - screenshot matches the location of the delayed asserts. - If "print_only" is set to True, the exception won't get raised. """ - if self.__delayed_assert_failures: - exception_output = '' - exception_output += "\n*** DELAYED ASSERTION FAILURES FOR: " - exception_output += "%s\n" % self.id() - all_failing_checks = self.__delayed_assert_failures - self.__delayed_assert_failures = [] - for tb in all_failing_checks: - exception_output += "%s\n" % tb - if print_only: - print(exception_output) + if not center: + element_rect = element.rect + left_offset = element_rect["width"] / 2 + top_offset = element_rect["height"] / 2 + x = -left_offset + (math.ceil(float(x)) or 0) + y = -top_offset + (math.ceil(float(y)) or 0) + action_chains = ActionChains(self.driver) + action_chains.move_to_element_with_offset(element, x, y) + if not double: + action_chains.click().perform() else: - raise Exception(exception_output) - - ############ + action_chains.double_click().perform() + except MoveTargetOutOfBoundsException: + message = ( + "Target coordinates for click are out-of-bounds!\n" + "The offset must stay inside the target element!" + ) + raise Exception(message) + except InvalidArgumentException: + if not self.is_chromium(): + raise + chrome_version = self.driver.capabilities["browserVersion"] + major_chrome_version = chrome_version.split(".")[0] + chrome_dict = self.driver.capabilities["chrome"] + chromedriver_version = chrome_dict["chromedriverVersion"] + chromedriver_version = chromedriver_version.split(" ")[0] + major_chromedriver_version = chromedriver_version.split(".")[0] + if ( + int(major_chromedriver_version) >= 76 + and int(major_chrome_version) >= 76 + ): + raise + install_sb = ( + "seleniumbase get chromedriver %s" % major_chrome_version + ) + if int(major_chromedriver_version) < int(major_chrome_version): + # Upgrading the driver is needed for performing canvas actions + message = ( + "You need to upgrade to a newer\n" + "version of chromedriver to perform canvas actions!\n" + "Reason: github.com/SeleniumHQ/selenium/issues/7000" + "\nYour version of chromedriver is: %s\n" + "And your version of Chrome is: %s\n" + "You can fix this issue by running:\n>>> %s\n" + % (chromedriver_version, chrome_version, install_sb) + ) + raise Exception(message) + else: + raise + if self.demo_mode: + self.__demo_mode_pause_if_active() + elif self.slow_mode: + self.__slow_mode_pause_if_active() - def __js_click(self, selector, by=By.CSS_SELECTOR): - """ Clicks an element using pure JS. Does not use jQuery. """ + def __jquery_slow_scroll_to(self, selector, by="css selector"): selector, by = self.__recalculate_selector(selector, by) - css_selector = self.convert_to_css_selector(selector, by=by) - css_selector = re.escape(css_selector) - css_selector = self.__escape_quotes_if_needed(css_selector) - script = ("""var simulateClick = function (elem) { - var evt = new MouseEvent('click', { - bubbles: true, - cancelable: true, - view: window - }); - var canceled = !elem.dispatchEvent(evt); - }; - var someLink = document.querySelector('%s'); - simulateClick(someLink);""" - % css_selector) - self.execute_script(script) + element = self.wait_for_element_present( + selector, by=by, timeout=settings.SMALL_TIMEOUT + ) + dist = js_utils.get_scroll_distance_to_element(self.driver, element) + time_offset = 0 + try: + if dist and abs(dist) > constants.Values.SSMD: + time_offset = int( + float(abs(dist) - constants.Values.SSMD) / 12.5 + ) + if time_offset > 950: + time_offset = 950 + except Exception: + time_offset = 0 + scroll_time_ms = 550 + time_offset + sleep_time = 0.625 + (float(time_offset) / 1000.0) + selector = self.convert_to_css_selector(selector, by=by) + selector = self.__make_css_match_first_element_only(selector) + scroll_script = ( + """jQuery([document.documentElement, document.body]).animate({""" + """scrollTop: jQuery('%s').offset().top - %s}, %s);""" + % (selector, constants.Scroll.Y_OFFSET, scroll_time_ms) + ) + if js_utils.is_jquery_activated(self.driver): + self.execute_script(scroll_script) + else: + self.__slow_scroll_to_element(element) + time.sleep(sleep_time) - def __jquery_click(self, selector, by=By.CSS_SELECTOR): - """ Clicks an element using jQuery. Different from using pure JS. """ + def __jquery_click(self, selector, by="css selector"): + """Clicks an element using jQuery. Different from using pure JS.""" selector, by = self.__recalculate_selector(selector, by) self.wait_for_element_present( - selector, by=by, timeout=settings.SMALL_TIMEOUT) - if self.is_element_visible(selector, by=by): - self.__demo_mode_highlight_if_active(selector, by) + selector, by=by, timeout=settings.SMALL_TIMEOUT + ) selector = self.convert_to_css_selector(selector, by=by) selector = self.__make_css_match_first_element_only(selector) - click_script = """jQuery('%s')[0].click()""" % selector + click_script = """jQuery('%s')[0].click();""" % selector + if getattr(self, "recorder_mode", None): + self.save_recorded_actions() self.safe_execute_script(click_script) + def __get_major_browser_version(self): + version = self.driver.__dict__["caps"]["browserVersion"] + return version.split(".")[0] + def __get_href_from_link_text(self, link_text, hard_fail=True): href = self.get_link_attribute(link_text, "href", hard_fail) if not href: return None - if href.startswith('//'): + if href.startswith("//"): link = "http:" + href - elif href.startswith('/'): + elif href.startswith("/"): url = self.driver.current_url domain_url = self.get_domain_url(url) link = domain_url + href @@ -2962,149 +14663,1094 @@ def __get_href_from_link_text(self, link_text, hard_fail=True): return link def __click_dropdown_link_text(self, link_text, link_css): - """ When a link may be hidden under a dropdown menu, use this. """ + """When a link may be hidden under a dropdown menu, use this.""" soup = self.get_beautiful_soup() - drop_down_list = soup.select('[class*=dropdown]') - for item in soup.select('[class*=HeaderMenu]'): - drop_down_list.append(item) - for item in soup.select('[class*=menu-item]'): + drop_down_list = [] + for item in soup.select("li[class]"): drop_down_list.append(item) - for item in soup.select('[class*=chevron]'): + csstype = link_css.split("[")[1].split("=")[0] + for item in drop_down_list: + item_text_list = item.text.split("\n") + if link_text in item_text_list and csstype in item.decode(): + dropdown_css = "" + try: + for css_class in item["class"]: + dropdown_css += "." + dropdown_css += css_class + except Exception: + continue + dropdown_css = item.name + dropdown_css + matching_dropdowns = self.find_visible_elements(dropdown_css) + for dropdown in matching_dropdowns: + # The same class names might be used for multiple dropdowns + if dropdown.is_displayed(): + with suppress(Exception): + try: + page_actions.hover_element( + self.driver, + dropdown, + ) + except Exception: + # If hovering fails, driver is likely outdated + # Time to go directly to the hidden link text + self.open( + self.__get_href_from_link_text(link_text) + ) + return True + page_actions.hover_element_and_click( + self.driver, + dropdown, + link_text, + click_by="link text", + timeout=0.12, + ) + return True + return False + + def __get_href_from_partial_link_text(self, link_text, hard_fail=True): + href = self.get_partial_link_text_attribute( + link_text, "href", hard_fail + ) + if not href: + return None + if href.startswith("//"): + link = "http:" + href + elif href.startswith("/"): + url = self.driver.current_url + domain_url = self.get_domain_url(url) + link = domain_url + href + else: + link = href + return link + + def __click_dropdown_partial_link_text(self, link_text, link_css): + """When a partial link may be hidden under a dropdown, use this.""" + soup = self.get_beautiful_soup() + drop_down_list = [] + for item in soup.select("li[class]"): drop_down_list.append(item) - csstype = link_css.split('[')[1].split('=')[0] + csstype = link_css.split("[")[1].split("=")[0] for item in drop_down_list: - if link_text in item.text.split('\n') and csstype in item.decode(): + item_text_list = item.text.split("\n") + if link_text in item_text_list and csstype in item.decode(): dropdown_css = "" - for css_class in item['class']: - dropdown_css += '.' - dropdown_css += css_class + try: + for css_class in item["class"]: + dropdown_css += "." + dropdown_css += css_class + except Exception: + continue dropdown_css = item.name + dropdown_css matching_dropdowns = self.find_visible_elements(dropdown_css) for dropdown in matching_dropdowns: # The same class names might be used for multiple dropdowns - try: - page_actions.hover_element_and_click( - self.driver, dropdown, link_text, - click_by=By.LINK_TEXT, timeout=0.2) - return True - except Exception: - pass + if dropdown.is_displayed(): + with suppress(Exception): + try: + page_actions.hover_element( + self.driver, dropdown + ) + except Exception: + # If hovering fails, driver is likely outdated + # Time to go directly to the hidden link text + self.open( + self.__get_href_from_partial_link_text( + link_text + ) + ) + return True + page_actions.hover_element_and_click( + self.driver, + dropdown, + link_text, + click_by="link text", + timeout=0.12, + ) + return True return False - def __recalculate_selector(self, selector, by): - # Try to determine the type of selector automatically - if page_utils.is_xpath_selector(selector): - by = By.XPATH - if page_utils.is_link_text_selector(selector): - selector = page_utils.get_link_text_from_selector(selector) - by = By.LINK_TEXT - return (selector, by) + def __recalculate_selector(self, selector, by, xp_ok=True): + """Use autodetection to return the correct selector with "by" updated. + If "xp_ok" is False, don't call convert_css_to_xpath(), which is + used to make the ":contains()" selector valid outside of JS calls. + Returns a (selector, by) tuple.""" + return page_utils.recalculate_selector(selector, by, xp_ok=xp_ok) + + def __looks_like_a_page_url(self, url): + return page_utils.looks_like_a_page_url(url) def __make_css_match_first_element_only(self, selector): # Only get the first match return page_utils.make_css_match_first_element_only(selector) + def __switch_to_newest_window_if_not_blank(self): + current_window = self.driver.current_window_handle + try: + self.switch_to_window(-1) + if self.get_current_url() == "about:blank": + self.switch_to_window(current_window) + except Exception: + with suppress(Exception): + self.switch_to_window(current_window) + + def __needs_minimum_wait(self): + if ( + self.page_load_strategy == "none" + and getattr(settings, "SKIP_JS_WAITS", None) + ): + return True + else: + return False + def __demo_mode_pause_if_active(self, tiny=False): if self.demo_mode: + wait_time = settings.DEFAULT_DEMO_MODE_TIMEOUT if self.demo_sleep: wait_time = float(self.demo_sleep) - else: - wait_time = settings.DEFAULT_DEMO_MODE_TIMEOUT if not tiny: time.sleep(wait_time) else: time.sleep(wait_time / 3.4) + elif self.slow_mode: + self.__slow_mode_pause_if_active() + + def __slow_mode_pause_if_active(self): + if self.slow_mode: + wait_time = settings.DEFAULT_DEMO_MODE_TIMEOUT + if self.demo_sleep: + wait_time = float(self.demo_sleep) + time.sleep(wait_time) + + def __demo_mode_scroll_if_active(self, selector, by): + if self.demo_mode: + self.slow_scroll_to(selector, by=by) + + def __demo_mode_highlight_if_active(self, selector, by): + self.__skip_if_esc() + if self.demo_mode: + # Includes self.slow_scroll_to(selector, by=by) by default + self.__highlight(selector, by=by) + elif self.slow_mode: + # Just do the slow scroll part of the highlight() method + time.sleep(0.08) + selector, by = self.__recalculate_selector(selector, by) + element = self.wait_for_element_visible( + selector, by=by, timeout=settings.SMALL_TIMEOUT + ) + try: + if self.browser != "safari": + scroll_distance = js_utils.get_scroll_distance_to_element( + self.driver, element + ) + if abs(scroll_distance) > constants.Values.SSMD: + self.__jquery_slow_scroll_to(selector, by) + else: + self.__slow_scroll_to_element(element) + else: + self.__jquery_slow_scroll_to(selector, by) + except (Stale_Exception, ENI_Exception): + self.wait_for_ready_state_complete() + time.sleep(0.12) + element = self.wait_for_element_visible( + selector, by=by, timeout=settings.SMALL_TIMEOUT + ) + self.__slow_scroll_to_element(element) + time.sleep(0.12) + + def __scroll_to_element(self, element, selector=None, by="css selector"): + success = js_utils.scroll_to_element(self.driver, element) + if not success and selector: + self.wait_for_ready_state_complete() + element = page_actions.wait_for_element_visible( + self.driver, selector, by, timeout=settings.SMALL_TIMEOUT + ) + self.__demo_mode_pause_if_active(tiny=True) + + def __slow_scroll_to_element(self, element): + try: + js_utils.slow_scroll_to_element(self.driver, element) + except Exception: + # Scroll to the element instantly if the slow scroll fails + js_utils.scroll_to_element(self.driver, element) + + def __highlight_with_js(self, selector, loops, o_bs): + if not self.__is_cdp_swap_needed(): + self.wait_for_ready_state_complete() + js_utils.highlight_with_js(self.driver, selector, loops, o_bs) + + def __highlight_element_with_js(self, element, loops, o_bs): + self.wait_for_ready_state_complete() + js_utils.highlight_element_with_js(self.driver, element, loops, o_bs) + + def __highlight_with_jquery(self, selector, loops, o_bs): + if not self.__is_cdp_swap_needed(): + self.wait_for_ready_state_complete() + js_utils.highlight_with_jquery(self.driver, selector, loops, o_bs) + + def __highlight_with_js_2(self, message, selector, o_bs): + duration = self.message_duration + if not duration: + duration = settings.DEFAULT_MESSAGE_DURATION + if ( + (self.headless or self.headless2 or self.xvfb) + and float(duration) > 0.75 + ): + duration = 0.75 + js_utils.highlight_with_js_2( + self.driver, message, selector, o_bs, duration + ) + + def __highlight_element_with_js_2(self, message, element, o_bs): + duration = self.message_duration + if not duration: + duration = settings.DEFAULT_MESSAGE_DURATION + if ( + (self.headless or self.headless2 or self.xvfb) + and float(duration) > 0.75 + ): + duration = 0.75 + js_utils.highlight_element_with_js_2( + self.driver, message, element, o_bs, duration + ) + + def __highlight_with_jquery_2(self, message, selector, o_bs): + duration = self.message_duration + if not duration: + duration = settings.DEFAULT_MESSAGE_DURATION + if ( + (self.headless or self.headless2 or self.xvfb) + and float(duration) > 0.75 + ): + duration = 0.75 + js_utils.highlight_with_jquery_2( + self.driver, message, selector, o_bs, duration + ) + + def __highlight_with_assert_success( + self, message, selector, by="css selector" + ): + selector, by = self.__recalculate_selector(selector, by, xp_ok=False) + element = self.wait_for_element_visible( + selector, by=by, timeout=settings.SMALL_TIMEOUT + ) + try: + if self.browser != "safari": + scroll_distance = js_utils.get_scroll_distance_to_element( + self.driver, element + ) + if abs(scroll_distance) > constants.Values.SSMD: + self.__jquery_slow_scroll_to(selector, by) + else: + self.__slow_scroll_to_element(element) + else: + self.__jquery_slow_scroll_to(selector, by) + except Exception: + self.wait_for_ready_state_complete() + time.sleep(0.12) + element = self.wait_for_element_visible( + selector, by=by, timeout=settings.SMALL_TIMEOUT + ) + self.__slow_scroll_to_element(element) + use_element_directly = False + try: + selector = self.convert_to_css_selector(selector, by=by) + except Exception: + # If can't convert to CSS_Selector for JS, use element directly + use_element_directly = True + + o_bs = "" # original_box_shadow + try: + style = element.get_attribute("style") + except Exception: + self.wait_for_ready_state_complete() + time.sleep(0.12) + element = self.wait_for_element_visible( + selector, by="css selector", timeout=settings.SMALL_TIMEOUT + ) + style = element.get_attribute("style") + if style: + if "box-shadow: " in style: + box_start = style.find("box-shadow: ") + box_end = style.find(";", box_start) + 1 + original_box_shadow = style[box_start:box_end] + o_bs = original_box_shadow + + if use_element_directly: + self.__highlight_element_with_js_2(message, element, o_bs) + elif ":contains" not in selector and ":first" not in selector: + selector = re.escape(selector) + selector = self.__escape_quotes_if_needed(selector) + self.__highlight_with_js_2(message, selector, o_bs) + else: + selector = self.__make_css_match_first_element_only(selector) + selector = re.escape(selector) + selector = self.__escape_quotes_if_needed(selector) + try: + self.__highlight_with_jquery_2(message, selector, o_bs) + except Exception: + pass # JQuery probably couldn't load. Skip highlighting. + time.sleep(0.065) + + def __activate_standard_virtual_display(self): + from sbvirtualdisplay import Display + width = settings.HEADLESS_START_WIDTH + height = settings.HEADLESS_START_HEIGHT + with suppress(Exception): + self._xvfb_display = Display( + visible=0, size=(width, height) + ) + self._xvfb_display.start() + self.headless_active = True + if not self.undetectable: + sb_config._virtual_display = self._xvfb_display + sb_config.headless_active = True + if self._reuse_session and hasattr(sb_config, "_vd_list"): + if isinstance(sb_config._vd_list, list): + sb_config._vd_list.append(self._xvfb_display) + + def __activate_virtual_display(self): + if self.undetectable and not (self.headless or self.headless2): + from sbvirtualdisplay import Display + import Xlib.display + try: + if not self._xvfb_width: + self._xvfb_width = 1366 + if not self._xvfb_height: + self._xvfb_height = 768 + self._xvfb_display = Display( + visible=True, + size=(self._xvfb_width, self._xvfb_height), + backend="xvfb", + use_xauth=True, + ) + if "--debug-display" in sys.argv: + print( + "Starting VDisplay from base_case: (%s, %s)" + % (self._xvfb_width, self._xvfb_height) + ) + self._xvfb_display.start() + sb_config._virtual_display = self._xvfb_display + if "DISPLAY" not in os.environ.keys(): + print( + "\n X11 display failed! Is Xvfb installed? " + "\n Try this: `sudo apt install -y xvfb`" + ) + self.__activate_standard_virtual_display() + else: + self.headless_active = True + if self._reuse_session and hasattr(sb_config, "_vd_list"): + if isinstance(sb_config._vd_list, list): + sb_config._vd_list.append(self._xvfb_display) + except Exception as e: + if hasattr(e, "msg"): + print("\n" + str(e.msg)) + else: + print(e) + print( + "\n X11 display failed! Is Xvfb installed? " + "\n Try this: `sudo apt install -y xvfb`" + ) + self.__activate_standard_virtual_display() + return + pyautogui_is_installed = False + try: + import pyautogui + with suppress(Exception): + use_pyautogui_ver = constants.PyAutoGUI.VER + u_pv = shared_utils.make_version_tuple(use_pyautogui_ver) + pv = shared_utils.make_version_tuple(pyautogui.__version__) + if pv < u_pv: + del pyautogui # To get newer ver + shared_utils.pip_install("pyautogui", version="Latest") + import pyautogui + pyautogui_is_installed = True + except Exception: + message = ( + "PyAutoGUI is required for UC Mode on Linux! " + "Installing now..." + ) + print("\n" + message) + shared_utils.pip_install("pyautogui", version="Latest") + import pyautogui + pyautogui_is_installed = True + if ( + pyautogui_is_installed + and hasattr(pyautogui, "_pyautogui_x11") + ): + try: + pyautogui._pyautogui_x11._display = ( + Xlib.display.Display(os.environ['DISPLAY']) + ) + sb_config._pyautogui_x11_display = ( + pyautogui._pyautogui_x11._display + ) + except Exception as e: + if hasattr(e, "msg"): + print("\n" + str(e.msg)) + else: + print(e) + else: + self.__activate_standard_virtual_display() + + def __activate_virtual_display_as_needed(self): + """This is only needed on Linux. + The "--xvfb" arg is still useful, as it prevents headless mode, + which is the default mode on Linux unless using another arg.""" + if is_linux and (not self.headed or self.xvfb): + pip_find_lock = fasteners.InterProcessLock( + constants.PipInstall.FINDLOCK + ) + try: + with pip_find_lock: + pass + except Exception: + # Since missing permissions, skip the locks + self.__activate_virtual_display() + return + with pip_find_lock: # Prevent issues with multiple processes + with suppress(Exception): + shared_utils.make_writable(constants.PipInstall.FINDLOCK) + self.__activate_virtual_display() + + def __ad_block_as_needed(self): + """This is an internal method for handling ad-blocking. + Use "pytest --ad-block" to enable this during tests. + When not Chromium or in headless mode, use the hack.""" + if self.ad_block_on and (self.headless or not self.is_chromium()): + # (Chromium browsers in headed mode use the extension instead) + current_url = self.get_current_url() + if not current_url == self.__last_page_load_url: + if page_actions.is_element_present( + self.driver, "iframe", By.CSS_SELECTOR + ): + self.ad_block() + self.__last_page_load_url = current_url + + def __disable_beforeunload_as_needed(self): + """Disables beforeunload as needed. Also resets frame_switch state.""" + if getattr(self, "_disable_beforeunload", None): + self.disable_beforeunload() + if self.recorder_mode: + try: + current_url = self.get_current_url + except Exception: + current_url = None + self.__last_saved_url = None + if current_url != self.__last_saved_url: + self.__frame_switch_layer = 0 + self.__frame_switch_multi = False + self.__last_saved_url = current_url + + ############ + + @decorators.deprecated("You should use re.escape() instead.") + def jq_format(self, code): + # DEPRECATED - re.escape() already performs this action. + return js_utils._jq_format(code) + + ############ + + # Shadow DOM / Shadow-root methods + + def __get_shadow_element( + self, selector, timeout=None, must_be_visible=False + ): + self.wait_for_ready_state_complete() + if timeout is None: + timeout = settings.SMALL_TIMEOUT + elif timeout == 0: + timeout = 0.1 # Use for: is_shadow_element_* (* = present/visible) + if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + self.__fail_if_invalid_shadow_selector_usage(selector) + if "::shadow " not in selector: + raise Exception( + 'A Shadow DOM selector must contain at least one "::shadow "!' + ) + selectors = selector.split("::shadow ") + element = self.get_element(selectors[0]) + selector_chain = selectors[0] + is_present = False + for selector_part in selectors[1:]: + shadow_root = None + if self.is_chromium() or self.browser == "firefox": + try: + shadow_root = element.shadow_root + except Exception: + if timeout != 0.1: # Skip wait for special 0.1 (See above) + time.sleep(2) + try: + shadow_root = element.shadow_root + except Exception: + raise Exception( + "Element {%s} has no shadow root!" % selector_chain + ) + else: + try: + shadow_root = self.execute_script( + "return arguments[0].shadowRoot;", element + ) + except Exception: + time.sleep(2) + shadow_root = self.execute_script( + "return arguments[0].shadowRoot;", element + ) + if timeout == 0.1 and not shadow_root: + raise Exception( + "Element {%s} has no shadow root!" % selector_chain + ) + elif not shadow_root: + time.sleep(2) # Wait two seconds for the shadow root to appear + shadow_root = self.execute_script( + "return arguments[0].shadowRoot;", element + ) + if not shadow_root: + raise Exception( + "Element {%s} has no shadow root!" % selector_chain + ) + selector_chain += "::shadow " + selector_chain += selector_part + try: + if ( + (self.is_chromium() or self.browser == "firefox") + and int(self.__get_major_browser_version()) >= 96 + ): + if timeout == 0.1: + element = shadow_root.find_element( + By.CSS_SELECTOR, value=selector_part + ) + else: + found = False + for i in range(int(timeout) * 4): + try: + element = shadow_root.find_element( + By.CSS_SELECTOR, value=selector_part + ) + is_present = True + if ( + selector_part == selectors[-1] + and must_be_visible + ): + if not element.is_displayed(): + raise Exception( + "Shadow Root element not visible!" + ) + found = True + break + except Exception: + time.sleep(0.2) + continue + if not found: + element = shadow_root.find_element( + By.CSS_SELECTOR, value=selector_part + ) + is_present = True + if ( + selector_part == selectors[-1] + and must_be_visible + and not element.is_displayed() + ): + raise Exception( + "Shadow Root element not visible!" + ) + else: + element = page_actions.wait_for_element_present( + shadow_root, + selector_part, + by="css selector", + timeout=timeout, + ) + except Exception: + error = "not present" + the_exception = "NoSuchElementException" + if must_be_visible and is_present: + error = "not visible" + the_exception = "ElementNotVisibleException" + plural = "s" + if timeout == 1: + plural = "" + msg = ( + "Shadow DOM Element {%s} was %s after %s second%s!" + % (selector_chain, error, timeout, plural) + ) + page_actions.timeout_exception(the_exception, msg) + return element + + def __fail_if_invalid_shadow_selector_usage(self, selector): + if selector.strip().endswith("::shadow"): + msg = ( + "A Shadow DOM selector cannot end on a shadow root element!" + " End the selector with an element inside the shadow root!" + ) + raise Exception(msg) + + def __is_shadow_selector(self, selector): + self.__fail_if_invalid_shadow_selector_usage(selector) + if "::shadow " in selector: + return True + return False + + def __shadow_click(self, selector, timeout): + element = self.__get_shadow_element( + selector, timeout=timeout, must_be_visible=True + ) + element.click() + + def __shadow_type(self, selector, text, timeout, clear_first=True): + element = self.__get_shadow_element( + selector, timeout=timeout, must_be_visible=True + ) + if clear_first: + with suppress(Exception): + element.clear() + backspaces = Keys.BACK_SPACE * 42 # Autofill Defense + element.send_keys(backspaces) + text = self.__get_type_checked_text(text) + if not text.endswith("\n"): + element.send_keys(text) + if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: + self.wait_for_ready_state_complete() + else: + element.send_keys(text[:-1]) + element.send_keys(Keys.RETURN) + if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: + self.wait_for_ready_state_complete() + + def __shadow_clear(self, selector, timeout): + element = self.__get_shadow_element( + selector, timeout=timeout, must_be_visible=True + ) + with suppress(Exception): + element.clear() + backspaces = Keys.BACK_SPACE * 42 # Autofill Defense + element.send_keys(backspaces) + + def __get_shadow_text(self, selector, timeout): + element = self.__get_shadow_element( + selector, timeout=timeout, must_be_visible=True + ) + element_text = element.text + if self.browser == "safari": + element_text = element.get_attribute("innerText") + return element_text + + def __get_shadow_attribute(self, selector, attribute, timeout): + element = self.__get_shadow_element(selector, timeout=timeout) + return element.get_attribute(attribute) + + def __wait_for_shadow_text_visible(self, text, selector, timeout): + text = str(text) + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (settings.SMALL_TIMEOUT * 1000.0) + for x in range(int(settings.SMALL_TIMEOUT * 10)): + try: + actual_text = self.__get_shadow_text( + selector, timeout=1 + ).strip() + text = text.strip() + if text not in actual_text: + msg = ( + "Expected text {%s} in element {%s} was not visible!" + % (text, selector) + ) + page_actions.timeout_exception( + "ElementNotVisibleException", msg + ) + return True + except Exception: + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + time.sleep(0.1) + actual_text = self.__get_shadow_text(selector, timeout=1).strip() + text = text.strip() + if text not in actual_text: + msg = "Expected text {%s} in element {%s} was not visible!" % ( + text, + selector, + ) + page_actions.timeout_exception("TextNotVisibleException", msg) + return True + + def __wait_for_exact_shadow_text_visible(self, text, selector, timeout): + text = str(text) + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (settings.SMALL_TIMEOUT * 1000.0) + for x in range(int(settings.SMALL_TIMEOUT * 10)): + try: + actual_text = self.__get_shadow_text( + selector, timeout=1 + ).strip() + text = text.strip() + if text != actual_text: + msg = ( + "Expected exact text {%s} in element {%s} not visible!" + "" % (text, selector) + ) + page_actions.timeout_exception( + "TextNotVisibleException", msg + ) + return True + except Exception: + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + time.sleep(0.1) + actual_text = self.__get_shadow_text(selector, timeout=1).strip() + text = text.strip() + if text != actual_text: + msg = ( + "Expected exact text {%s} in element {%s} was not visible!" + % (text, selector) + ) + page_actions.timeout_exception("TextNotVisibleException", msg) + return True + + def __wait_for_non_empty_shadow_text_visible(self, selector, timeout): + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (settings.SMALL_TIMEOUT * 1000.0) + for x in range(int(settings.SMALL_TIMEOUT * 10)): + try: + actual_text = self.__get_shadow_text(selector, timeout=1) + actual_text = actual_text.strip() + if len(actual_text) == 0: + msg = "Element {%s} has no visible text!" % selector + page_actions.timeout_exception( + "TextNotVisibleException", msg + ) + return True + except Exception: + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + time.sleep(0.1) + actual_text = self.__get_shadow_text(selector, timeout=1) + actual_text = actual_text.strip() + if len(actual_text) == 0: + msg = "Element {%s} has no visible text!" % selector + page_actions.timeout_exception("TextNotVisibleException", msg) + return True + + def __assert_shadow_text_visible(self, text, selector, timeout): + self.__wait_for_shadow_text_visible(text, selector, timeout) + if self.demo_mode: + a_t = "ASSERT TEXT" + i_n = "in" + by = By.CSS_SELECTOR + if self._language != "English": + from seleniumbase.fixtures.words import SD + + a_t = SD.translate_assert_text(self._language) + i_n = SD.translate_in(self._language) + messenger_post = "%s: {%s} %s %s: %s" % ( + a_t, text, i_n, by.upper(), selector + ) + with suppress(Exception): + js_utils.activate_jquery(self.driver) + js_utils.post_messenger_success_message( + self.driver, messenger_post, self.message_duration + ) + + def __assert_exact_shadow_text_visible(self, text, selector, timeout): + self.__wait_for_exact_shadow_text_visible(text, selector, timeout) + if self.demo_mode: + a_t = "ASSERT EXACT TEXT" + i_n = "in" + by = By.CSS_SELECTOR + if self._language != "English": + from seleniumbase.fixtures.words import SD + + a_t = SD.translate_assert_exact_text(self._language) + i_n = SD.translate_in(self._language) + messenger_post = "%s: {%s} %s %s: %s" % ( + a_t, text, i_n, by.upper(), selector + ) + with suppress(Exception): + js_utils.activate_jquery(self.driver) + js_utils.post_messenger_success_message( + self.driver, messenger_post, self.message_duration + ) + + def __assert_non_empty_shadow_text_visible(self, selector, timeout, strip): + self.__wait_for_non_empty_shadow_text_visible(selector, timeout, strip) + if self.demo_mode: + a_t = "ASSERT NON-EMPTY TEXT" + i_n = "in" + by = By.CSS_SELECTOR + if self._language != "English": + from seleniumbase.fixtures.words import SD + + a_t = SD.translate_assert_non_empty_text(self._language) + i_n = SD.translate_in(self._language) + messenger_post = "%s %s %s: %s" % ( + a_t, i_n, by.upper(), selector + ) + with suppress(Exception): + js_utils.activate_jquery(self.driver) + js_utils.post_messenger_success_message( + self.driver, messenger_post, self.message_duration + ) + + def __is_shadow_element_present(self, selector): + try: + element = self.__get_shadow_element(selector, timeout=0.1) + return element is not None + except Exception: + return False + + def __is_shadow_element_visible(self, selector): + try: + element = self.__get_shadow_element(selector, timeout=0.1) + return element.is_displayed() + except Exception: + return False - def __demo_mode_scroll_if_active(self, selector, by): - if self.demo_mode: - self.slow_scroll_to(selector, by=by) + def __is_shadow_element_clickable(self, selector): + try: + element = self.__get_shadow_element(selector, timeout=0.1) + if element.is_displayed() and element.is_enabled(): + return True + return False + except Exception: + return False - def __demo_mode_highlight_if_active(self, selector, by): - if self.demo_mode: - # Includes self.slow_scroll_to(selector, by=by) by default - self.highlight(selector, by=by) + 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 __scroll_to_element(self, element): - js_utils.scroll_to_element(self.driver, element) - self.__demo_mode_pause_if_active(tiny=True) + def __is_shadow_text_visible(self, text, selector): + text = str(text) + try: + element = self.__get_shadow_element(selector, timeout=0.1) + if self.browser == "safari": + return ( + element.is_displayed() + and text in element.get_attribute("innerText") + ) + return element.is_displayed() and text in element.text + except Exception: + return False - def __slow_scroll_to_element(self, element): - js_utils.slow_scroll_to_element(self.driver, element, self.browser) + def __is_shadow_exact_text_visible(self, text, selector): + text = str(text) + try: + element = self.__get_shadow_element(selector, timeout=0.1) + if self.browser == "safari": + return ( + element.is_displayed() + and text in element.get_attribute("innerText") + ) + return ( + element.is_displayed() + and text.strip() == element.text.strip() + ) + except Exception: + return False - def __highlight_with_assert_success( - self, message, selector, by=By.CSS_SELECTOR): - selector, by = self.__recalculate_selector(selector, by) - element = self.find_element( - selector, by=by, timeout=settings.SMALL_TIMEOUT) + def __is_shadow_non_empty_text_visible(self, selector): try: - selector = self.convert_to_css_selector(selector, by=by) + element = self.__get_shadow_element(selector, timeout=0.1) + if self.browser == "safari": + return ( + element.is_displayed() + and len(element.get_attribute("innerText").strip() > 0) + ) + return element.is_displayed() and len(element.text.strip()) > 0 except Exception: - # Don't highlight if can't convert to CSS_SELECTOR - return - self.__slow_scroll_to_element(element) + return False - o_bs = '' # original_box_shadow - style = element.get_attribute('style') - if style: - if 'box-shadow: ' in style: - box_start = style.find('box-shadow: ') - box_end = style.find(';', box_start) + 1 - original_box_shadow = style[box_start:box_end] - o_bs = original_box_shadow + 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 - if ":contains" not in selector and ":first" not in selector: - selector = re.escape(selector) - selector = self.__escape_quotes_if_needed(selector) - self.__highlight_with_js_2(message, selector, o_bs) - else: - selector = self.__make_css_match_first_element_only(selector) - selector = re.escape(selector) - selector = self.__escape_quotes_if_needed(selector) - try: - self.__highlight_with_jquery_2(message, selector, o_bs) - except Exception: - pass # JQuery probably couldn't load. Skip highlighting. - time.sleep(0.065) + def __wait_for_shadow_element_present(self, selector, timeout): + element = self.__get_shadow_element(selector, timeout=timeout) + return element - def __highlight_with_js_2(self, message, selector, o_bs): - js_utils.highlight_with_js_2( - self.driver, message, selector, o_bs, self.message_duration) + def __wait_for_shadow_element_visible(self, selector, timeout): + element = self.__get_shadow_element( + selector, timeout=timeout, must_be_visible=True + ) + return element - def __highlight_with_jquery_2(self, message, selector, o_bs): - js_utils.highlight_with_jquery_2( - self.driver, message, selector, o_bs, self.message_duration) + def __wait_for_shadow_attribute_present( + self, selector, attribute, value=None, timeout=None + ): + element = self.__get_shadow_element(selector, timeout=timeout) + actual_value = element.get_attribute(attribute) + plural = "s" + if timeout == 1: + plural = "" + if value is None: + # The element attribute only needs to exist + if actual_value is not None: + return element + else: + # The element does not have the attribute + message = ( + "Expected attribute {%s} of element {%s} " + "was not present after %s second%s!" + % (attribute, selector, timeout, plural) + ) + page_actions.timeout_exception( + "NoSuchAttributeException", message + ) + else: + if actual_value == value: + return element + else: + message = ( + "Expected value {%s} for attribute {%s} of element " + "{%s} was not present after %s second%s! " + "(Actual value was {%s})" + % ( + value, + attribute, + selector, + timeout, + plural, + actual_value, + ) + ) + page_actions.timeout_exception( + "NoSuchAttributeException", message + ) + + def __assert_shadow_element_present(self, selector): + self.__get_shadow_element(selector) + if self.demo_mode: + a_t = "ASSERT" + by = By.CSS_SELECTOR + if self._language != "English": + from seleniumbase.fixtures.words import SD + + a_t = SD.translate_assert(self._language) + messenger_post = "%s %s: %s" % (a_t, by.upper(), selector) + with suppress(Exception): + js_utils.activate_jquery(self.driver) + js_utils.post_messenger_success_message( + self.driver, messenger_post, self.message_duration + ) + + def __assert_shadow_element_visible(self, selector): + element = self.__get_shadow_element(selector) + if not element.is_displayed(): + msg = "Shadow DOM Element {%s} was not visible!" % selector + page_actions.timeout_exception("NoSuchElementException", msg) + if self.demo_mode: + a_t = "ASSERT" + by = By.CSS_SELECTOR + if self._language != "English": + from seleniumbase.fixtures.words import SD + + a_t = SD.translate_assert(self._language) + messenger_post = "%s %s: %s" % (a_t, by.upper(), selector) + with suppress(Exception): + js_utils.activate_jquery(self.driver) + js_utils.post_messenger_success_message( + self.driver, messenger_post, self.message_duration + ) ############ def setUp(self, masterqa_mode=False): - """ - Be careful if a subclass of BaseCase overrides setUp() - You'll need to add the following line to the subclass setUp() method: - super(SubClassOfBaseCase, self).setUp() - """ - self.masterqa_mode = masterqa_mode + """This method runs before every test begins. + Be careful if a subclass of BaseCase overrides setUp(). + If so, add the following line to the subclass setUp() method: + super().setUp() """ + if not hasattr(self, "_using_sb_fixture") and self.__called_setup: + return # This test already called setUp() + self.__called_setup = True + self.__called_teardown = False self.is_pytest = None + self.log_path = constants.Logs.LATEST + self.masterqa_mode = masterqa_mode try: # This raises an exception if the test is not coming from pytest self.is_pytest = sb_config.is_pytest except Exception: - # Not using pytest (probably nosetests) + # Not using pytest (could be pynose, behave, or raw python) self.is_pytest = False if self.is_pytest: # pytest-specific code - test_id = "%s.%s.%s" % (self.__class__.__module__, - self.__class__.__name__, - self._testMethodName) + test_id = self.__get_test_id() + self.test_id = test_id + self.is_behave = False + if hasattr(self, "_using_sb_fixture"): + self.test_id = sb_config._test_id + if hasattr(sb_config, "_sb_pdb_driver"): + sb_config._sb_pdb_driver = None self.browser = sb_config.browser + self.account = sb_config.account self.data = sb_config.data + self.var1 = sb_config.var1 + self.var2 = sb_config.var2 + self.var3 = sb_config.var3 + variables = sb_config.variables + if variables and isinstance(variables, str) and len(variables) > 0: + import ast + + bad_input = False + if ( + not variables.startswith("{") + or not variables.endswith("}") + ): + bad_input = True + else: + try: + variables = ast.literal_eval(variables) + if not isinstance(variables, dict): + bad_input = True + except Exception: + bad_input = True + if bad_input: + raise Exception( + '\nExpecting a Python dictionary for "variables"!' + "\nEg. --variables=\"{'KEY1':'VALUE', 'KEY2':123}\"" + ) + elif isinstance(variables, dict): + pass # Already processed + else: + variables = {} + sb_config.variables = variables + self.variables = sb_config.variables + self.slow_mode = sb_config.slow_mode self.demo_mode = sb_config.demo_mode self.demo_sleep = sb_config.demo_sleep self.highlights = sb_config.highlights + self.time_limit = sb_config._time_limit + sb_config.time_limit = sb_config._time_limit # Reset between tests self.environment = sb_config.environment self.env = self.environment # Add a shortened version self.with_selenium = sb_config.with_selenium # Should be True self.headless = sb_config.headless + self.headless1 = sb_config.headless1 + if self.headless1: + self.headless = True + self.headless2 = sb_config.headless2 self.headless_active = False + sb_config.headless_active = False + self.headed = sb_config.headed + self.xvfb = sb_config.xvfb + self.xvfb_metrics = sb_config.xvfb_metrics + self.locale_code = sb_config.locale_code + self.interval = sb_config.interval + self.start_page = sb_config.start_page self.log_path = sb_config.log_path self.with_testing_base = sb_config.with_testing_base self.with_basic_test_info = sb_config.with_basic_test_info @@ -3112,18 +15758,94 @@ def setUp(self, masterqa_mode=False): self.with_page_source = sb_config.with_page_source self.with_db_reporting = sb_config.with_db_reporting self.with_s3_logging = sb_config.with_s3_logging + self.protocol = sb_config.protocol self.servername = sb_config.servername self.port = sb_config.port self.proxy_string = sb_config.proxy_string + self.proxy_bypass_list = sb_config.proxy_bypass_list + self.proxy_pac_url = sb_config.proxy_pac_url + self.multi_proxy = sb_config.multi_proxy self.user_agent = sb_config.user_agent + self.mobile_emulator = sb_config.mobile_emulator + self.device_metrics = sb_config.device_metrics self.cap_file = sb_config.cap_file + self.cap_string = sb_config.cap_string + self.settings_file = sb_config.settings_file self.database_env = sb_config.database_env self.message_duration = sb_config.message_duration self.js_checking_on = sb_config.js_checking_on self.ad_block_on = sb_config.ad_block_on + self.host_resolver_rules = sb_config.host_resolver_rules + self.block_images = sb_config.block_images + self.do_not_track = sb_config.do_not_track + self.chromium_arg = sb_config.chromium_arg + self.firefox_arg = sb_config.firefox_arg + self.firefox_pref = sb_config.firefox_pref self.verify_delay = sb_config.verify_delay + self.esc_end = sb_config.esc_end + self.recorder_mode = sb_config.recorder_mode + self.recorder_ext = sb_config.recorder_mode + self.rec_print = sb_config.rec_print + self.rec_behave = sb_config.rec_behave + self.rec_sb_mgr = sb_config.rec_sb_mgr + self.rec_sb_cdp = sb_config.rec_sb_cdp + self.record_sleep = sb_config.record_sleep + if not self.recorder_mode: + if ( + self.record_sleep + or self.rec_print + or self.rec_behave + or self.rec_sb_mgr + or self.rec_sb_cdp + ): + self.recorder_mode = True + self.recorder_ext = True + self.disable_cookies = sb_config.disable_cookies + self.disable_js = sb_config.disable_js self.disable_csp = sb_config.disable_csp + self.disable_ws = sb_config.disable_ws + self.enable_ws = sb_config.enable_ws + if not self.disable_ws: + self.enable_ws = True + self.enable_sync = sb_config.enable_sync + self.use_auto_ext = sb_config.use_auto_ext + self.undetectable = sb_config.undetectable + self.uc_cdp_events = sb_config.uc_cdp_events + self.uc_subprocess = sb_config.uc_subprocess + self.log_cdp_events = sb_config.log_cdp_events + self.no_sandbox = sb_config.no_sandbox + self.disable_gpu = sb_config.disable_gpu + self.incognito = sb_config.incognito + self.guest_mode = sb_config.guest_mode + self.dark_mode = sb_config.dark_mode + self.devtools = sb_config.devtools + self.remote_debug = sb_config.remote_debug + self._multithreaded = sb_config._multithreaded + self._reuse_session = sb_config.reuse_session + self._crumbs = sb_config.crumbs + self._disable_beforeunload = sb_config._disable_beforeunload + self.dashboard = sb_config.dashboard + self._dash_initialized = sb_config._dashboard_initialized + if self.dashboard and self._multithreaded: + self.dash_lock = fasteners.InterProcessLock( + constants.Dashboard.LOCKFILE + ) + self.enable_3d_apis = sb_config.enable_3d_apis + self._swiftshader = sb_config.swiftshader + self.user_data_dir = sb_config.user_data_dir + self.extension_zip = sb_config.extension_zip + self.extension_dir = sb_config.extension_dir + self.disable_features = sb_config.disable_features + self.binary_location = sb_config.binary_location + self.driver_version = sb_config.driver_version + self.page_load_strategy = sb_config.page_load_strategy + self.external_pdf = sb_config.external_pdf + self._final_debug = sb_config.final_debug + self.window_position = sb_config.window_position + self.window_size = sb_config.window_size + self.maximize_option = sb_config.maximize_option self.save_screenshot_after_test = sb_config.save_screenshot + self.no_screenshot_after_test = sb_config.no_screenshot self.visual_baseline = sb_config.visual_baseline self.timeout_multiplier = sb_config.timeout_multiplier self.pytest_html_report = sb_config.pytest_html_report @@ -3132,25 +15854,29 @@ def setUp(self, masterqa_mode=False): self.report_on = True self.use_grid = False if self.servername != "localhost": - # Use Selenium Grid (Use --server=127.0.0.1 for localhost Grid) + # Use Selenium Grid (Use --server="127.0.0.1" for a local Grid) self.use_grid = True if self.with_db_reporting: + import getpass + import uuid from seleniumbase.core.application_manager import ( - ApplicationManager) + ApplicationManager, + ) from seleniumbase.core.testcase_manager import ( - ExecutionQueryPayload) - import getpass + ExecutionQueryPayload, + TestcaseDataPayload, + TestcaseManager, + ) + self.execution_guid = str(uuid.uuid4()) self.testcase_guid = None self.execution_start_time = 0 self.case_start_time = 0 - self.application = None self.testcase_manager = None - self.error_handled = False self.testcase_manager = TestcaseManager(self.database_env) # exec_payload = ExecutionQueryPayload() - exec_payload.execution_start_time = int(time.time() * 1000) + exec_payload.execution_start_time = int(time.time() * 1000.0) self.execution_start_time = exec_payload.execution_start_time exec_payload.guid = self.execution_guid exec_payload.username = getpass.getuser() @@ -3166,70 +15892,505 @@ def setUp(self, masterqa_mode=False): data_payload.browser = "N/A" data_payload.test_address = test_id application = ApplicationManager.generate_application_string( - self._testMethodName) - data_payload.env = application.split('.')[0] - data_payload.start_time = application.split('.')[1] - data_payload.state = constants.State.NOTRUN + self + ) + data_payload.env = application.split(".")[0] + data_payload.start_time = application.split(".")[1] + data_payload.state = constants.State.UNTESTED + self.__skip_reason = None self.testcase_manager.insert_testcase_data(data_payload) - self.case_start_time = int(time.time() * 1000) - if self.headless: - try: - from pyvirtualdisplay import Display - self.display = Display(visible=0, size=(1440, 1080)) - self.display.start() - self.headless_active = True - except Exception: - # pyvirtualdisplay might not be necessary anymore because - # Chrome and Firefox now have built-in headless displays - pass + self.case_start_time = int(time.time() * 1000.0) + elif getattr(self, "is_behave", None): + self.__initialize_variables() + elif getattr(self, "is_nosetest", None): + pass # Setup performed in plugins for pynose + else: + # Pure Python run. (Eg. SB() and Driver() Managers) + pass # Variables initialized in respective plugins - # Launch WebDriver for both Pytest and Nosetests + # Verify SeleniumBase is installed successfully, and used correctly if not hasattr(self, "browser"): - raise Exception("""SeleniumBase plugins did not load! """ - """Please reinstall using:\n""" - """ >>> "python setup.py install" <<< """) - self.driver = self.get_new_driver(browser=self.browser, - headless=self.headless, - servername=self.servername, - port=self.port, - proxy=self.proxy_string, - agent=self.user_agent, - switch_to=True, - cap_file=self.cap_file, - disable_csp=self.disable_csp) - self._default_driver = self.driver - - def __set_last_page_screenshot(self): - """ self.__last_page_screenshot is only for pytest html report logs - self.__last_page_screenshot_png is for all screenshot log files """ - if not self.__last_page_screenshot and ( - not self.__last_page_screenshot_png): + raise Exception( + "SeleniumBase plugins DID NOT load!\n" + 'Either "seleniumbase" is not installed on your system,\n' + 'or you called "setUp()" directly without initialization!\n' + "*** To install SeleniumBase in Develop Mode from a clone:\n" + ' >>> "pip install -e ." (Run in DIR with setup.py)\n' + "*** To install the latest SeleniumBase version from PyPI:\n" + ' >>> "pip install -U seleniumbase" (Run in any DIR)\n' + 'NOTE: "setUp()" can ONLY be called after vars are set.\n' + ' If using "pytest", then "pytest_plugin.py" sets them.\n' + ' The SeleniumBase "SB()" Manager can also set them.\n' + "See SeleniumBase/help_docs/syntax_formats.md for details!" + ) + + if not hasattr(sb_config, "_is_timeout_changed"): + # Should only be reachable from pure Python runs + sb_config._is_timeout_changed = False + sb_config._SMALL_TIMEOUT = settings.SMALL_TIMEOUT + sb_config._LARGE_TIMEOUT = settings.LARGE_TIMEOUT + + if sb_config._is_timeout_changed: + if sb_config._SMALL_TIMEOUT and sb_config._LARGE_TIMEOUT: + settings.SMALL_TIMEOUT = sb_config._SMALL_TIMEOUT + settings.LARGE_TIMEOUT = sb_config._LARGE_TIMEOUT + + if not hasattr(self, "_swiftshader"): + # Not swiftshader: options.add_argument("--disable-gpu") + self._swiftshader = False + + if not hasattr(sb_config, "_recorded_actions"): + # Only filled when Recorder Mode is enabled + sb_config._recorded_actions = {} + sb_config._sb_mgr_recorded_actions = {} + sb_config._sb_cdp_recorded_actions = {} + sb_config._behave_recorded_actions = {} + + if not hasattr(settings, "SWITCH_TO_NEW_TABS_ON_CLICK"): + # If using an older settings file, set the new definitions manually + settings.SWITCH_TO_NEW_TABS_ON_CLICK = True + + # Parse the settings file + if self.settings_file: + from seleniumbase.core import settings_parser + + settings_parser.set_settings(self.settings_file) + + # Set variables that may be useful to developers + self.log_abspath = os.path.abspath(self.log_path) + self.data_path = os.path.join(self.log_path, self.__get_test_id()) + self.data_abspath = os.path.abspath(self.data_path) + + # Add _test_logpath value to sb_config + test_id = self.__get_test_id() + test_logpath = os.path.join(self.log_path, test_id) + sb_config._test_logpath = test_logpath + + # Add _process_dashboard_entry method to sb_config + sb_config._process_dashboard_entry = self._process_dashboard_entry + + # Add _add_pytest_html_extra method to sb_config + sb_config._add_pytest_html_extra = self._add_pytest_html_extra + + # Add _process_visual_baseline_logs method to sb_config + sb_config._process_v_baseline_logs = self._process_visual_baseline_logs + + # Add _log_fail_data method to sb_config + sb_config._log_fail_data = self._log_fail_data + + # Reset the last_page_screenshot variables + sb_config._last_page_screenshot = None + sb_config._last_page_screenshot_png = None + + # Indictate to pytest reports that SeleniumBase is being used + sb_config._sbase_detected = True + sb_config._only_unittest = False + + # Mobile Emulator device metrics: CSS Width, CSS Height, & Pixel-Ratio + if self.device_metrics: + metrics_string = self.device_metrics + metrics_string = metrics_string.replace(" ", "") + metrics_list = metrics_string.split(",") + exception_string = ( + "Invalid input for Mobile Emulator device metrics!\n" + "Expecting a comma-separated string with integer values\n" + "for Width/Height, and an int or float for Pixel-Ratio.\n" + 'Example: --metrics="412,732,3" ' + ) + if len(metrics_list) != 3: + raise Exception(exception_string) try: - element = self.driver.find_element_by_tag_name('body') - if self.is_pytest and self.report_on: - self.__last_page_screenshot_png = ( - self.driver.get_screenshot_as_png()) - self.__last_page_screenshot = element.screenshot_as_base64 + self.__device_width = int(metrics_list[0]) + self.__device_height = int(metrics_list[1]) + self.__device_pixel_ratio = float(metrics_list[2]) + self.mobile_emulator = True + except Exception: + raise Exception(exception_string) + + window_position = self.window_position + if window_position: + if window_position.count(",") != 1: + message = ( + '\n\n window_position expects an "x,y" string!' + '\n (Your input was: "%s")\n' % window_position + ) + raise Exception(message) + window_position = window_position.replace(" ", "") + win_x = None + win_y = None + try: + win_x = int(window_position.split(",")[0]) + win_y = int(window_position.split(",")[1]) + except Exception: + message = ( + '\n\n Expecting integer values for "x,y"!' + '\n (window_position input was: "%s")\n' + % window_position + ) + raise Exception(message) + settings.WINDOW_START_X = win_x + settings.WINDOW_START_Y = win_y + + window_size = self.window_size + if window_size: + if window_size.count(",") != 1: + message = ( + '\n\n window_size expects a "width,height" string!' + '\n (Your input was: "%s")\n' % window_size + ) + raise Exception(message) + window_size = window_size.replace(" ", "") + width = None + height = None + try: + width = int(window_size.split(",")[0]) + height = int(window_size.split(",")[1]) + except Exception: + message = ( + '\n\n Expecting integer values for "width,height"!' + '\n (window_size input was: "%s")\n' % window_size + ) + raise Exception(message) + settings.CHROME_START_WIDTH = width + settings.CHROME_START_HEIGHT = height + settings.HEADLESS_START_WIDTH = width + settings.HEADLESS_START_HEIGHT = height + + if self.xvfb_metrics: + metrics_string = self.xvfb_metrics + metrics_string = metrics_string.replace(" ", "") + metrics_list = metrics_string.split(",")[0:2] + exception_string = ( + "Invalid input for xvfb_metrics!\n" + "Expecting a comma-separated string\n" + "with integer values for Width/Height.\n" + 'Eg. --xvfb-metrics="1920,1080".\n' + "(Minimum: 1024,768) (Default: 1366,768)" + ) + if len(metrics_list) != 2: + raise Exception(exception_string) + try: + self._xvfb_width = int(metrics_list[0]) + self._xvfb_height = int(metrics_list[1]) + # The minimum width,height is: 1024,768 + if self._xvfb_width < 1024: + self._xvfb_width = 1024 + sb_config._xvfb_width = self._xvfb_width + if self._xvfb_height < 768: + self._xvfb_height = 768 + sb_config._xvfb_height = self._xvfb_height + self.xvfb = True + except Exception: + raise Exception(exception_string) + + if self.mobile_emulator and not self.user_agent: + # Use a Pixel user agent by default if not specified + self.user_agent = constants.Mobile.AGENT + + if self.browser in ["firefox", "ie", "safari"]: + # The Recorder Mode browser extension is only for Chrome/Edge. + if self.recorder_mode: + message = ( + "Recorder Mode ONLY supports Chrome and Edge!\n" + '(Your browser choice was: "%s")' % self.browser + ) + raise Exception(message) + + if not getattr(self, "is_nosetest", None): + # Xvfb Virtual Display activation for Linux + self.__activate_virtual_display_as_needed() + + # Dashboard pre-processing: + if self.dashboard: + if self._multithreaded: + with self.dash_lock: + with suppress(Exception): + shared_utils.make_writable( + constants.Dashboard.LOCKFILE + ) + if not self._dash_initialized: + sb_config._dashboard_initialized = True + self._dash_initialized = True + self.__process_dashboard(False, init=True) + else: + if not self._dash_initialized: + sb_config._dashboard_initialized = True + self._dash_initialized = True + self.__process_dashboard(False, init=True) + + # Set the JS start time for Recorder Mode. + # Use this to skip saving recorded actions from previous tests. + if self.recorder_mode: + self.__js_start_time = int(time.time() * 1000.0) + + has_url = False + if self._reuse_session: + if not hasattr(sb_config, "shared_driver"): + sb_config.shared_driver = None + if sb_config.shared_driver: + with suppress(Exception): + self._default_driver = sb_config.shared_driver + self.driver = sb_config.shared_driver + self._drivers_list = [sb_config.shared_driver] + url = self.get_current_url() + if url is not None: + has_url = True + if len(self.driver.window_handles) > 1: + while len(self.driver.window_handles) > 1: + self.switch_to_window(-1) + self.driver.close() + self.switch_to_window(0) + if self._crumbs: + if self.binary_location == "chs": + self.delete_session_storage() + else: + self.wait_for_ready_state_complete() + with suppress(Exception): + self.driver.delete_all_cookies() + if self._reuse_session and sb_config.shared_driver and has_url: + good_start_page = False + if self.recorder_ext: + self.__js_start_time = int(time.time() * 1000.0) + if self.start_page and len(self.start_page) >= 4: + if page_utils.is_valid_url(self.start_page): + good_start_page = True + self.__new_window_on_rec_open = False + self.open(self.start_page) + self.__new_window_on_rec_open = True else: - self.__last_page_screenshot_png = element.screenshot_as_png + new_start_page = "https://" + self.start_page + if page_utils.is_valid_url(new_start_page): + good_start_page = True + self.__dont_record_open = True + self.open(new_start_page) + self.__dont_record_open = False + if self.recorder_ext or (self._crumbs and not good_start_page): + if self.get_current_url() != "about:blank": + self.__new_window_on_rec_open = False + self.open("about:blank") + self.__new_window_on_rec_open = True + if self.recorder_ext: + self.__js_start_time = int(time.time() * 1000.0) + else: + # Launch WebDriver for both pytest and pynose + self.driver = self.get_new_driver( + browser=self.browser, + headless=self.headless, + locale_code=self.locale_code, + protocol=self.protocol, + servername=self.servername, + port=self.port, + proxy=self.proxy_string, + proxy_bypass_list=self.proxy_bypass_list, + proxy_pac_url=self.proxy_pac_url, + multi_proxy=self.multi_proxy, + agent=self.user_agent, + switch_to=True, + cap_file=self.cap_file, + cap_string=self.cap_string, + recorder_ext=self.recorder_ext, + disable_cookies=self.disable_cookies, + disable_js=self.disable_js, + disable_csp=self.disable_csp, + enable_ws=self.enable_ws, + enable_sync=self.enable_sync, + use_auto_ext=self.use_auto_ext, + undetectable=self.undetectable, + uc_cdp_events=self.uc_cdp_events, + uc_subprocess=self.uc_subprocess, + log_cdp_events=self.log_cdp_events, + no_sandbox=self.no_sandbox, + disable_gpu=self.disable_gpu, + headless1=self.headless1, + headless2=self.headless2, + incognito=self.incognito, + guest_mode=self.guest_mode, + dark_mode=self.dark_mode, + devtools=self.devtools, + remote_debug=self.remote_debug, + enable_3d_apis=self.enable_3d_apis, + swiftshader=self._swiftshader, + ad_block_on=self.ad_block_on, + host_resolver_rules=self.host_resolver_rules, + block_images=self.block_images, + do_not_track=self.do_not_track, + chromium_arg=self.chromium_arg, + firefox_arg=self.firefox_arg, + firefox_pref=self.firefox_pref, + user_data_dir=self.user_data_dir, + extension_zip=self.extension_zip, + extension_dir=self.extension_dir, + disable_features=self.disable_features, + binary_location=self.binary_location, + driver_version=self.driver_version, + page_load_strategy=self.page_load_strategy, + external_pdf=self.external_pdf, + is_mobile=self.mobile_emulator, + d_width=self.__device_width, + d_height=self.__device_height, + d_p_r=self.__device_pixel_ratio, + ) + try: + if self.driver.timeouts.implicit_wait > 0: + self.driver.implicitly_wait(0) except Exception: - if not self.__last_page_screenshot: - if self.is_pytest and self.report_on: - try: - self.__last_page_screenshot = ( - self.driver.get_screenshot_as_base64()) - except Exception: - pass - if not self.__last_page_screenshot_png: + with suppress(Exception): + self.driver.implicitly_wait(0) + self._default_driver = self.driver + if self._reuse_session: + sb_config.shared_driver = self.driver + if len(self._drivers_list) == 0: + # The user is overriding self.get_new_driver() + # (Otherwise this code shouldn't be reachable) + self._drivers_list.append(self.driver) + self._drivers_browser_map[self.driver] = self.browser + + if self.browser in ["firefox", "ie", "safari"]: + # Only Chrome and Edge browsers have the mobile emulator. + # Some actions such as hover-clicking are different on mobile. + self.mobile_emulator = False + + # Configure the test time limit (if used). + self.set_time_limit(self.time_limit) + + # Configure the page load timeout + with suppress(Exception): + if hasattr(settings, "PAGE_LOAD_TIMEOUT"): + self.driver.set_page_load_timeout(settings.PAGE_LOAD_TIMEOUT) + else: + self.driver.set_page_load_timeout(120) # Selenium uses 300 + + # Set the start time for the test (in ms). + # Although the pytest clock starts before setUp() begins, + # the time-limit clock starts at the end of the setUp() method. + sb_config.start_time_ms = int(time.time() * 1000.0) + self.__start_time_ms = sb_config.start_time_ms + + def __set_last_page_screenshot(self): + """self.__last_page_screenshot is only for pytest html report logs. + self.__last_page_screenshot_png is for all screenshot log files.""" + SCREENSHOT_SKIPPED = constants.Warnings.SCREENSHOT_SKIPPED + SCREENSHOT_UNDEFINED = constants.Warnings.SCREENSHOT_UNDEFINED + if getattr(self, "no_screenshot_after_test", None): + from seleniumbase.core import encoded_images + + NO_SCREENSHOT = encoded_images.get_no_screenshot_png() + self.__last_page_screenshot = NO_SCREENSHOT + self.__last_page_screenshot_png = SCREENSHOT_SKIPPED + sb_config._last_page_screenshot_png = NO_SCREENSHOT + return + element = None + if ( + not self.__last_page_screenshot + and not self.__last_page_screenshot_png + ): + with suppress(Exception): + try: + element = page_actions.wait_for_element_visible( + self.driver, + "body", + "css selector", + timeout=0.1, + ignore_test_time_limit=True, + ) + except Exception: + element = page_actions.wait_for_element_present( + self.driver, + "body", + "css selector", + timeout=0.1, + ignore_test_time_limit=True, + ) + try: + if getattr(settings, "SCREENSHOT_WITH_BACKGROUND", None): + self.__last_page_screenshot = ( + self.driver.get_screenshot_as_base64() + ) + else: + self.__last_page_screenshot = ( + element.screenshot_as_base64 + ) + except Exception: + with suppress(Exception): + self.__last_page_screenshot = ( + self.driver.get_screenshot_as_base64() + ) + if not self.__last_page_screenshot: + self.__last_page_screenshot = SCREENSHOT_UNDEFINED + self.__last_page_screenshot_png = SCREENSHOT_UNDEFINED + if element: try: self.__last_page_screenshot_png = ( - self.driver.get_screenshot_as_png()) + element.screenshot_as_png + ) except Exception: - pass + with suppress(Exception): + self.__last_page_screenshot_png = ( + self.driver.get_screenshot_as_png() + ) + else: + import base64 + + try: + self.__last_page_screenshot_png = ( + base64.b64decode(self.__last_page_screenshot) + ) + except Exception: + if element: + try: + self.__last_page_screenshot_png = ( + element.screenshot_as_png + ) + except Exception: + with suppress(Exception): + self.__last_page_screenshot_png = ( + self.driver.get_screenshot_as_png() + ) + sb_config._last_page_screenshot_png = self.__last_page_screenshot_png + + def __set_last_page_url(self): + if not self.__last_page_url: + try: + self.__last_page_url = log_helper.get_last_page(self.driver) + except Exception: + self.__last_page_url = None + + def __set_last_page_source(self): + if not self.__last_page_source: + try: + self.__last_page_source = ( + log_helper.get_html_source_with_base_href( + self.driver, self.driver.page_source + ) + ) + except Exception: + self.__last_page_source = ( + constants.Warnings.PAGE_SOURCE_UNDEFINED + ) + sb_config._last_page_source = self.__last_page_source + + def __get_exception_info(self): + exc_message = None + if ( + hasattr(self, "_outcome") + and getattr(self._outcome, "errors", None) + ): + try: + exc_message = self._outcome.errors[-1][1][1] + except Exception: + exc_message = "(Unknown Exception)" + else: + try: + exc_message = sys.last_value + except Exception: + exc_message = "(Unknown Exception)" + return str(exc_message) def __insert_test_result(self, state, err): + from seleniumbase.core.testcase_manager import TestcaseDataPayload + data_payload = TestcaseDataPayload() - data_payload.runtime = int(time.time() * 1000) - self.case_start_time + data_payload.runtime = int(time.time() * 1000.0) - self.case_start_time data_payload.guid = self.testcase_guid data_payload.execution_guid = self.execution_guid data_payload.state = state @@ -3237,247 +16398,1272 @@ def __insert_test_result(self, state, err): import traceback tb_string = traceback.format_exc() if "Message: " in tb_string: - data_payload.message = "Message: " + tb_string.split( - "Message: ")[-1] + data_payload.message = ( + "Message: " + tb_string.split("Message: ")[-1] + ) elif "Exception: " in tb_string: data_payload.message = tb_string.split("Exception: ")[-1] elif "Error: " in tb_string: data_payload.message = tb_string.split("Error: ")[-1] else: - data_payload.message = "Unknown Error: See Stacktrace" + data_payload.message = self.__get_exception_info() + else: + test_id = self.__get_test_id_2() + if ( + self.is_pytest + and test_id in sb_config._results.keys() + and (sb_config._results[test_id] == "Skipped") + ): + if self.__skip_reason: + data_payload.message = "Skipped: " + self.__skip_reason + else: + data_payload.message = "Skipped: (no reason given)" self.testcase_manager.update_testcase_data(data_payload) + def _add_pytest_html_extra(self): + if not ( + (python3_11_or_newer and py311_patch2) + or "--pdb" in sys.argv + ): + return + self.__add_pytest_html_extra() + def __add_pytest_html_extra(self): - try: - if self.with_selenium: - if not self.__last_page_screenshot: - self.__set_last_page_screenshot() - if self.report_on: - extra_url = {} - extra_url['name'] = 'URL' - extra_url['format'] = 'url' - extra_url['content'] = self.get_current_url() - extra_url['mime_type'] = None - extra_url['extension'] = None - extra_image = {} - extra_image['name'] = 'Screenshot' - extra_image['format'] = 'image' - extra_image['content'] = self.__last_page_screenshot - extra_image['mime_type'] = 'image/png' - extra_image['extension'] = 'png' - self._html_report_extra.append(extra_url) - self._html_report_extra.append(extra_image) - except Exception: - pass + if not self.__added_pytest_html_extra: + with suppress(Exception): + if self.with_selenium: + if not self.__last_page_screenshot: + self.__set_last_page_screenshot() + self.__set_last_page_url() + self.__set_last_page_source() + if self.report_on: + extra_url = {} + extra_url["name"] = "URL" + extra_url["format"] = "url" + extra_url["format_type"] = "url" + extra_url["content"] = self.__last_page_url + extra_url["mime_type"] = None + extra_url["extension"] = None + extra_image = {} + extra_image["name"] = "Screenshot" + extra_image["format"] = "image" + extra_image["format_type"] = "image" + extra_image["content"] = self.__last_page_screenshot + extra_image["mime_type"] = "image/png" + extra_image["extension"] = "png" + self.__added_pytest_html_extra = True + if self.__last_page_screenshot != ( + constants.Warnings.SCREENSHOT_UNDEFINED + ): + self._html_report_extra.append(extra_url) + self._html_report_extra.append(extra_image) + + def __delay_driver_quit(self): + delay_driver_quit = False + if ( + getattr(self, "_using_sb_fixture", None) + and "--pdb" in sys.argv + and self.__has_exception() + and len(self._drivers_list) == 1 + and self.driver == self._default_driver + ): + # Special case: Using sb fixture, --pdb, and has error. + # Keep the driver open for debugging and quit it later. + delay_driver_quit = True + return delay_driver_quit def __quit_all_drivers(self): + if self._reuse_session and sb_config.shared_driver: + if len(self._drivers_list) > 0: + if self._drivers_list[0] != sb_config.shared_driver: + if sb_config.shared_driver in self._drivers_list: + self._drivers_list.remove(sb_config.shared_driver) + self._drivers_list.insert(0, sb_config.shared_driver) + self._default_driver = self._drivers_list[0] + self.switch_to_default_driver() + if len(self._drivers_list) > 1: + self._drivers_list = self._drivers_list[1:] + else: + self._drivers_list = [] # Close all open browser windows + delay_driver_quit = self.__delay_driver_quit() self._drivers_list.reverse() # Last In, First Out for driver in self._drivers_list: try: - driver.quit() + if ( + not is_windows + or self.browser == "ie" + or self.servername != "localhost" + or ( + hasattr(driver, "service") + and driver.service.process + ) + ): + if not delay_driver_quit: + driver.quit() + else: + # Save it for later to quit it later. + sb_config._sb_pdb_driver = driver except AttributeError: pass except Exception: pass - self.driver = None - self._drivers_list = [] + if not delay_driver_quit: + self.driver = None + self._default_driver = None + self._drivers_list = [] - def tearDown(self): - """ - Be careful if a subclass of BaseCase overrides setUp() - You'll need to add the following line to the subclass's tearDown(): - super(SubClassOfBaseCase, self).tearDown() - """ + def __has_exception(self): has_exception = False - if sys.version.startswith('3') and hasattr(self, '_outcome'): - if hasattr(self._outcome, 'errors') and self._outcome.errors: - has_exception = True + if hasattr(sys, "last_traceback") and sys.last_traceback is not None: + has_exception = True + elif getattr(self, "is_context_manager", None): + if self.with_testing_base and self._has_failure: + return True + else: + return False + elif hasattr(self, "_outcome") and hasattr(self._outcome, "errors"): + if python3_11_or_newer: + if ( + self._outcome.errors + and self._outcome.errors[-1] + and self._outcome.errors[-1][1] + ): + has_exception = True + else: + if self._outcome.errors: + has_exception = True else: has_exception = sys.exc_info()[1] is not None - if self.__delayed_assert_failures: - print( - "\nWhen using self.delayed_assert_*() methods in your tests, " - "remember to call self.process_delayed_asserts() afterwards. " - "Now calling in tearDown()...\nFailures Detected:") - if not has_exception: - self.process_delayed_asserts() + if self.__will_be_skipped and hasattr(self, "_using_sb_fixture"): + has_exception = False + return has_exception + + def __get_test_id(self): + """The id used in various places such as the test log path.""" + if getattr(self, "is_behave", None): + file_name = sb_config.behave_scenario.filename + file_name = file_name.replace("/", ".").replace("\\", ".") + scenario_name = sb_config.behave_scenario.name + if " -- @" in scenario_name: + scenario_name = scenario_name.split(" # ")[0].rstrip() + scenario_name = re.sub(r"[^\w" + r"_ " + r"]", "", scenario_name) + scenario_name = scenario_name.replace(" ", "_") + test_id = "%s.%s" % (file_name, scenario_name) + return test_id + elif getattr(self, "is_context_manager", None): + if hasattr(self, "_manager_saved_id"): + self.__saved_id = self._manager_saved_id + if self.__saved_id: + return self.__saved_id + filename = self.__class__.__module__.split(".")[-1] + ".py" + methodname = self._testMethodName + context_id = None + if filename == "base_case.py" or methodname == "runTest": + import traceback + stack_base = traceback.format_stack()[0].split(os.sep)[-1] + test_base = stack_base.split(", in ")[0] + if getattr(self, "cm_filename", None): + filename = self.cm_filename + else: + filename = test_base.split('"')[0] + methodname = ".line_" + test_base.split(", line ")[-1] + context_id = filename.split(".")[0] + methodname + self.__saved_id = context_id + return context_id + test_id = "%s.%s.%s" % ( + self.__class__.__module__, + self.__class__.__name__, + self._testMethodName, + ) + if self._sb_test_identifier and len(str(self._sb_test_identifier)) > 6: + test_id = self._sb_test_identifier + elif getattr(self, "_using_sb_fixture", None): + test_id = sb_config._latest_display_id + test_id = test_id.replace(".py::", ".").replace("::", ".") + test_id = test_id.replace("/", ".").replace("\\", ".") + test_id = test_id.replace(" ", "_") + # Linux filename length limit for `open(filename)` = 255 + # 255 - len("latest_logs/") - len("/basic_test_info.txt") = 223 + if len(test_id) <= 223: + return test_id + else: + # 223 - len("__TRUNCATED__") = 210 + # 210 / 2 = 105 + return test_id[:105] + "__TRUNCATED__" + test_id[-105:] + + def __get_test_id_2(self): + """The id for SeleniumBase Dashboard entries.""" + if "PYTEST_CURRENT_TEST" in os.environ: + full_name = os.environ["PYTEST_CURRENT_TEST"] + if "] " in full_name: + return full_name.split("] ")[0] + "]" else: - self.process_delayed_asserts(print_only=True) - self.is_pytest = None + return full_name.split(" ")[0] + if getattr(self, "is_behave", None): + return self.__get_test_id() + test_id = "%s.%s.%s" % ( + self.__class__.__module__.split(".")[-1], + self.__class__.__name__, + self._testMethodName, + ) + if self._sb_test_identifier and len(str(self._sb_test_identifier)) > 6: + test_id = self._sb_test_identifier + if test_id.count(".") > 1: + test_id = ".".join(test_id.split(".")[1:]) + return test_id + + def __get_display_id(self): + """The id for running a test from pytest. (Displayed on Dashboard)""" + if "PYTEST_CURRENT_TEST" in os.environ: + full_name = os.environ["PYTEST_CURRENT_TEST"] + if "] " in full_name: + return full_name.split("] ")[0] + "]" + else: + return full_name.split(" ")[0] + if getattr(self, "is_behave", None): + file_name = sb_config.behave_scenario.filename + line_num = sb_config.behave_line_num + scenario_name = sb_config.behave_scenario.name + if " -- @" in scenario_name: + scenario_name = scenario_name.split(" # ")[0].rstrip() + test_id = "%s:%s => %s" % (file_name, line_num, scenario_name) + return test_id + test_id = "%s.py::%s::%s" % ( + self.__class__.__module__.replace(".", "/"), + self.__class__.__name__, + self._testMethodName, + ) + if self._sb_test_identifier and len(str(self._sb_test_identifier)) > 6: + test_id = self._sb_test_identifier + if hasattr(self, "_using_sb_fixture_class"): + if test_id.count(".") >= 2: + parts = test_id.split(".") + full = parts[-3] + ".py::" + parts[-2] + "::" + parts[-1] + test_id = full + elif hasattr(self, "_using_sb_fixture_no_class"): + if test_id.count(".") >= 1: + parts = test_id.split(".") + full = parts[-2] + ".py::" + parts[-1] + test_id = full + return test_id + + def __get_filename(self): + """The filename of the current SeleniumBase test. (NOT Path)""" + filename = None + if "PYTEST_CURRENT_TEST" in os.environ: + test_id = os.environ["PYTEST_CURRENT_TEST"].split(" ")[0] + filename = test_id.split("::")[0].split("/")[-1] + elif getattr(self, "is_behave", None): + filename = sb_config.behave_scenario.filename + filename = filename.split("/")[-1].split("\\")[-1] + else: + filename = self.__class__.__module__.split(".")[-1] + ".py" + return filename + + def __create_log_path_as_needed(self, test_logpath): + if not os.path.exists(test_logpath): + try: + os.makedirs(test_logpath) + except Exception: + pass # Only reachable during multi-threaded runs + + def _process_dashboard_entry(self, has_exception, init=False): + if self._multithreaded: + self.dash_lock = fasteners.InterProcessLock( + constants.Dashboard.LOCKFILE + ) + with self.dash_lock: + with suppress(Exception): + shared_utils.make_writable(constants.Dashboard.LOCKFILE) + self.__process_dashboard(has_exception, init) + else: + self.__process_dashboard(has_exception, init) + + def __process_dashboard(self, has_exception, init=False): + """SeleniumBase Dashboard Processing""" + if ( + self.is_pytest + and "--pdb" in sys.argv + and has_exception + ): + sb_config._pdb_failure = True + elif ( + self.is_pytest + and getattr(sb_config, "_pdb_failure", None) + and not has_exception + ): + return # Handle case where "pytest --pdb" marks failures as Passed + if self._multithreaded: + existing_res = sb_config._results # For recording "Skipped" tests + abs_path = os.path.abspath(".") + dash_json_loc = constants.Dashboard.DASH_JSON + dash_jsonpath = os.path.join(abs_path, dash_json_loc) + if not init and os.path.exists(dash_jsonpath): + with open(dash_jsonpath, "r") as f: + dash_json = f.read().strip() + dash_data, d_id, dash_rt, tlp, d_stats = json.loads(dash_json) + num_passed, num_failed, num_skipped, num_untested = d_stats + sb_config._results = dash_data + sb_config._display_id = d_id + sb_config._duration = dash_rt # Dashboard Run Time + sb_config._d_t_log_path = tlp # Test Log Path + sb_config.item_count_passed = num_passed + sb_config.item_count_failed = num_failed + sb_config.item_count_skipped = num_skipped + sb_config.item_count_untested = num_untested + if len(sb_config._extra_dash_entries) > 0: + # First take care of existing entries from non-SeleniumBase tests + for test_id in sb_config._extra_dash_entries: + if test_id in sb_config._results.keys(): + if sb_config._results[test_id] == "Skipped": + sb_config.item_count_skipped += 1 + sb_config.item_count_untested -= 1 + elif sb_config._results[test_id] == "Failed": + sb_config.item_count_failed += 1 + sb_config.item_count_untested -= 1 + elif sb_config._results[test_id] == "Passed": + sb_config.item_count_passed += 1 + sb_config.item_count_untested -= 1 + else: # Mark "Skipped" if unknown + sb_config.item_count_skipped += 1 + sb_config.item_count_untested -= 1 + sb_config._extra_dash_entries = [] # Reset the list to empty + # Process new entries + log_dir = self.log_path + ft_id = self.__get_test_id() # Full test id with path to log files + test_id = self.__get_test_id_2() # The test id used by the DashBoard + dud = "seleniumbase/plugins/pytest_plugin.py::BaseClass::base_method" + dud2 = "pytest_plugin.BaseClass.base_method" + if hasattr(self, "_using_sb_fixture") and self.__will_be_skipped: + test_id = sb_config._test_id + if not init: + duration_ms = int(time.time() * 1000.0) - self.__start_time_ms + duration = float(duration_ms) / 1000.0 + duration = "{:.2f}".format(duration) + sb_config._duration[test_id] = duration + if ( + has_exception + or self.save_screenshot_after_test + or self.__screenshot_count > 0 + or self.__logs_data_count > 0 + or self.__level_0_visual_f + or self.__will_be_skipped + ): + sb_config._d_t_log_path[test_id] = os.path.join(log_dir, ft_id) + else: + sb_config._d_t_log_path[test_id] = None + if test_id not in sb_config._display_id.keys(): + sb_config._display_id[test_id] = self.__get_display_id() + if sb_config._display_id[test_id] == dud: + return + if ( + hasattr(self, "_using_sb_fixture") + and test_id not in sb_config._results.keys() + ): + if test_id.count(".") > 1: + alt_test_id = ".".join(test_id.split(".")[1:]) + if alt_test_id in sb_config._results.keys(): + sb_config._results.pop(alt_test_id) + elif test_id.count(".") == 1: + alt_test_id = sb_config._display_id[test_id] + alt_test_id = alt_test_id.replace(".py::", ".") + alt_test_id = alt_test_id.replace("::", ".") + alt_test_id = alt_test_id.replace(" ", "_") + if alt_test_id in sb_config._results.keys(): + sb_config._results.pop(alt_test_id) + if test_id in sb_config._results.keys() and ( + sb_config._results[test_id] == "Skipped" + ): + if self.__passed_then_skipped: + # Multiple calls of setUp() and tearDown() in the same test + sb_config.item_count_passed -= 1 + sb_config.item_count_untested += 1 + self.__passed_then_skipped = False + sb_config._results[test_id] = "Skipped" + sb_config.item_count_skipped += 1 + sb_config.item_count_untested -= 1 + elif ( + self._multithreaded + and test_id in existing_res.keys() + and existing_res[test_id] == "Skipped" + ): + sb_config._results[test_id] = "Skipped" + sb_config.item_count_skipped += 1 + sb_config.item_count_untested -= 1 + elif has_exception: + if test_id not in sb_config._results.keys(): + sb_config._results[test_id] = "Failed" + sb_config.item_count_failed += 1 + sb_config.item_count_untested -= 1 + elif not sb_config._results[test_id] == "Failed": + # tearDown() was called more than once in the test + if sb_config._results[test_id] == "Passed": + # Passed earlier, but last run failed + sb_config._results[test_id] = "Failed" + sb_config.item_count_failed += 1 + sb_config.item_count_passed -= 1 + else: + sb_config._results[test_id] = "Failed" + sb_config.item_count_failed += 1 + sb_config.item_count_untested -= 1 + else: + # pytest-rerunfailures caused a duplicate failure + sb_config._results[test_id] = "Failed" + else: + if ( + test_id in sb_config._results.keys() + and sb_config._results[test_id] == "Failed" + ): + # pytest-rerunfailures reran a test that failed + sb_config._d_t_log_path[test_id] = os.path.join( + log_dir, ft_id + ) + sb_config.item_count_failed -= 1 + sb_config.item_count_untested += 1 + elif ( + test_id in sb_config._results.keys() + and sb_config._results[test_id] == "Passed" + ): + # tearDown() was called more than once in the test + sb_config.item_count_passed -= 1 + sb_config.item_count_untested += 1 + sb_config._results[test_id] = "Passed" + sb_config.item_count_passed += 1 + sb_config.item_count_untested -= 1 + else: + pass # Only initialize the Dashboard on the first processing + num_passed = sb_config.item_count_passed + num_failed = sb_config.item_count_failed + num_skipped = sb_config.item_count_skipped + num_untested = sb_config.item_count_untested + self.create_pie_chart(title=constants.Dashboard.TITLE) + self.add_data_point("Passed", num_passed, color="#84d474") + self.add_data_point("Untested", num_untested, color="#eaeaea") + self.add_data_point("Skipped", num_skipped, color="#efd8b4") + self.add_data_point("Failed", num_failed, color="#f17476") + style = ( + '' % constants.Dashboard.STYLE_CSS + ) + auto_refresh_html = "" + if num_untested > 0: + # Refresh every X seconds when waiting for more test results + auto_refresh_html = constants.Dashboard.META_REFRESH_HTML + else: + # The tests are complete + if sb_config._using_html_report: + # Add the pie chart to the pytest html report + sb_config._saved_dashboard_pie = self.extract_chart() + if self._multithreaded: + abs_path = os.path.abspath(".") + dash_pie = json.dumps(sb_config._saved_dashboard_pie) + dash_pie_loc = constants.Dashboard.DASH_PIE + pie_path = os.path.join(abs_path, dash_pie_loc) + pie_file = open(pie_path, mode="w+", encoding="utf-8") + pie_file.writelines(dash_pie) + pie_file.close() + DASH_PIE_PNG_1 = constants.Dashboard.get_dash_pie_1() + head = ( + '' + '' + '' + "%s" + "Dashboard" + "%s" + % (DASH_PIE_PNG_1, auto_refresh_html, style) + ) + num_total = num_passed + num_failed + num_skipped + num_untested + table_html = ( + "
      " + '' + '' + '' + '' + '' + '' + '' + '' + "
      ' + 'Failed: %s ' + 'Skipped: %s ' + 'Passed: %s ' + 'Untested: %s ' + ' Total: %s
      " + "

      " + '' + '' + '' + '' + '' + "" + % (num_failed, num_skipped, num_passed, num_untested, num_total) + ) + the_failed = [] + the_skipped = [] + the_passed_hl = [] # Passed and has logs + the_passed_nl = [] # Passed and no logs + the_untested = [] + if dud2 in sb_config._results.keys(): + sb_config._results.pop(dud2) + for key in sb_config._results.keys(): + t_res = sb_config._results[key] + t_dur = sb_config._duration[key] + t_d_id = sb_config._display_id[key] + t_l_path = sb_config._d_t_log_path[key] + res_low = t_res.lower() + if sb_config._results[key] == "Failed": + if not sb_config._d_t_log_path[key]: + sb_config._d_t_log_path[key] = os.path.join(log_dir, ft_id) + the_failed.append([res_low, t_res, t_d_id, t_dur, t_l_path]) + elif sb_config._results[key] == "Skipped": + the_skipped.append([res_low, t_res, t_d_id, t_dur, t_l_path]) + elif sb_config._results[key] == "Passed" and t_l_path: + the_passed_hl.append([res_low, t_res, t_d_id, t_dur, t_l_path]) + elif sb_config._results[key] == "Passed" and not t_l_path: + the_passed_nl.append([res_low, t_res, t_d_id, t_dur, t_l_path]) + elif sb_config._results[key] == "Untested": + the_untested.append([res_low, t_res, t_d_id, t_dur, t_l_path]) + for row in the_failed: + row = ( + '' + '' + '' + '" + "" % (row[0], row[1], row[2], row[3], log_dir, row[4]) + ) + table_html += row + for row in the_skipped: + if not row[4]: + row = ( + '' + '' + '' + "" + % (row[0], row[1], row[2], row[3]) + ) + else: + row = ( + '' + '' + '' + '" + "" % (row[0], row[1], row[2], row[3], log_dir, row[4]) + ) + table_html += row + for row in the_passed_hl: + # Passed and has logs + row = ( + '' + '' + '' + '" + "" % (row[0], row[1], row[2], row[3], log_dir, row[4]) + ) + table_html += row + for row in the_passed_nl: + # Passed and no logs + row = ( + '' + '' + '' + "" % (row[0], row[1], row[2], row[3]) + ) + table_html += row + for row in the_untested: + row = ( + '' + '' + "" % (row[0], row[1], row[2], row[3]) + ) + table_html += row + table_html += "
      ResultTestDurationLinks
      %s%s%sLogs / Data' + "
      %s%s%s-
      %s%s%sLogs / Data' + "
      %s%s%sLogs / Data' + "
      %s%s%s-
      %s%s%s-
      " + add_more = "
      Last updated: " + timestamp, the_date, the_time = log_helper.get_master_time() + last_updated = "%s at %s" % (the_date, the_time) + add_more = add_more + "%s" % last_updated + status = "

      Status: Awaiting results..." + status += " (Refresh the page for updates)" + if num_untested == 0: + status = "

      Status: Test Run Complete:" + if num_failed == 0: + if num_passed > 0: + if num_skipped == 0: + status += " Success! (All tests passed)" + else: + status += " Success! (No failing tests)" + else: + status += " All tests were skipped!" + else: + latest_logs_dir = constants.Logs.LATEST + "/" + log_msg = "See latest logs for details" + if num_failed == 1: + status += ( + " 1 test failed! --- " + '(%s)' + "" % (latest_logs_dir, log_msg) + ) + else: + status += ( + " %s tests failed! --- " + '(%s)' + "" % (num_failed, latest_logs_dir, log_msg) + ) + status += "

      " + add_more = add_more + status + gen_by = ( + '

      Generated by: ' + "SeleniumBase

      " + ) + add_more = add_more + gen_by + # Have dashboard auto-refresh on updates when using an http server + refresh_line = ( + '" % constants.Dashboard.LIVE_JS + ) + if num_untested == 0 and sb_config._using_html_report: + sb_config._dash_final_summary = status + add_more = add_more + refresh_line + the_html = ( + '' + + head + + self.extract_chart() + + table_html + + add_more + ) + abs_path = os.path.abspath(".") + file_path = os.path.join(abs_path, "dashboard.html") + out_file = open(file_path, mode="w+", encoding="utf-8") + out_file.writelines(the_html) + out_file.close() + sb_config._dash_html = the_html + if self._multithreaded: + d_stats = (num_passed, num_failed, num_skipped, num_untested) + _results = sb_config._results + _display_id = sb_config._display_id + _rt = sb_config._duration # Run Time (RT) + _tlp = sb_config._d_t_log_path # Test Log Path (TLP) + dash_json = json.dumps((_results, _display_id, _rt, _tlp, d_stats)) + dash_json_loc = constants.Dashboard.DASH_JSON + dash_jsonpath = os.path.join(abs_path, dash_json_loc) + dash_json_file = open(dash_jsonpath, mode="w+", encoding="utf-8") + dash_json_file.writelines(dash_json) + dash_json_file.close() + + def __activate_behave_post_mortem_debug_mode(self): + """Activate Post Mortem Debug Mode for failing tests that use Behave""" + import pdb + + pdb.post_mortem(sb_config.behave_step.exc_traceback) + # Post Mortem Debug Mode ("behave -D pdb") + + def __activate_sb_mgr_post_mortem_debug_mode(self): + """Activate Post Mortem Debug Mode for failing tests that use SB Mgr""" + import pdb + + pdb.post_mortem() + # Post Mortem Debug Mode ("python --pdb") + + def __activate_debug_mode_in_teardown(self): + """Activate Final Trace / Debug Mode""" + import pdb + + pdb.set_trace() + # Final Trace ("--ftrace") + + def has_exception(self): + """(This method should ONLY be used in custom tearDown() methods.) + This method returns True if the test failed or raised an exception. + This is useful for performing additional steps in your tearDown() + method (based on whether or not the test passed or failed). + Example use cases: + * Performing cleanup steps if a test didn't complete. + * Sending test data and/or results to a dashboard service. + """ + return self.__has_exception() + + def save_teardown_screenshot(self): + """(Should ONLY be used at the start of custom tearDown() methods.) + This method takes a screenshot of the active page for FAILING tests + (or when using "--screenshot" / "--save-screenshot" / "--ss"). + That way your tearDown() method can navigate away from the last + page where the test failed, and still get the correct screenshot + before performing tearDown() steps on other pages. If this method + is not included in your custom tearDown() method, a screenshot + will still be taken after calling "super().tearDown()" there. + This method also saves recorded actions when using Recorder Mode. + """ try: - # This raises an exception if the test is not coming from pytest - self.is_pytest = sb_config.is_pytest + self.__check_scope() except Exception: - # Not using pytest (probably nosetests) - self.is_pytest = False - if self.is_pytest: - # pytest-specific code - test_id = "%s.%s.%s" % (self.__class__.__module__, - self.__class__.__name__, - self._testMethodName) + return + if getattr(self, "recorder_mode", None): + # In case tearDown() leaves the origin, save actions first. + self.save_recorded_actions() + if ( + self.__has_exception() + or self.save_screenshot_after_test + or (python3_11_or_newer and py311_patch2) + or "--pdb" in sys.argv + ): + self.__set_last_page_screenshot() + self.__set_last_page_url() + self.__set_last_page_source() + if self.__has_exception() or self.save_screenshot_after_test: + if self.is_pytest: + self.__add_pytest_html_extra() + + def _log_fail_data(self): + if not ( + (python3_11_or_newer and py311_patch2) + or "--pdb" in sys.argv + ): + return + test_id = self.__get_test_id() + test_logpath = os.path.join(self.log_path, test_id) + log_helper.log_test_failure_data( + self, + test_logpath, + self.driver, + self.browser, + self.__last_page_url, + ) + + def _get_browser_version(self): + driver_capabilities = None + if hasattr(self, "driver") and hasattr(self.driver, "capabilities"): + driver_capabilities = self.driver.capabilities + elif hasattr(sb_config, "_browser_version"): + return sb_config._browser_version + else: + return "(Unknown Version)" + if "browserVersion" in driver_capabilities: + browser_version = driver_capabilities["browserVersion"] + else: + browser_version = "(Unknown Version)" + return browser_version + + def _get_driver_name_and_version(self): + if not hasattr(self.driver, "capabilities"): + if hasattr(sb_config, "_driver_name_version"): + return sb_config._driver_name_version + else: + return None + driver = self.driver + if "chrome" in driver.capabilities: + cap_dict = driver.capabilities["chrome"] + return ( + "chromedriver", cap_dict["chromedriverVersion"].split(" ")[0] + ) + elif "msedge" in driver.capabilities: + cap_dict = driver.capabilities["msedge"] + return ( + "msedgedriver", cap_dict["msedgedriverVersion"].split(" ")[0] + ) + elif driver.capabilities["browserName"].lower() == "firefox": + return ( + "geckodriver", driver.capabilities["moz:geckodriverVersion"] + ) + elif driver.capabilities["browserName"].lower() == "safari": + return ("safaridriver", self._get_browser_version()) + elif driver.capabilities["browserName"].lower() == "internet explorer": + return ("iedriver", self._get_browser_version()) + else: + return None + + def _get_num_handles(self): + return len(self.driver.window_handles) + + def _get_rec_shift_esc_script(self): + return ( + """document.onkeydown = function(evt) { + evt = evt || window.event; + var isEscape = false; + if ("key" in evt) { + isEscape = (evt.key === "Escape" || evt.key === "Esc"); + last_key = evt.key; + } else { + isEscape = (evt.keyCode === 27); + last_key = evt.keyCode; + if (last_key === 16) { + last_key = "Shift"; + } + } + if (isEscape && document.sb_last_key === "Shift") { + document.sb_esc_end = "yes"; + } + document.sb_last_key = last_key; + };""" + ) + + def _addSkip(self, result, test_case, reason): + """This method should NOT be called directly from tests.""" + addSkip = getattr(result, 'addSkip', None) + if addSkip is not None: + addSkip(test_case, reason) + else: + import warnings + warnings.warn( + "TestResult has no addSkip method! Skips not reported!", + RuntimeWarning, 2 + ) + result.addSuccess(test_case) + + def _callTestMethod(self, method): + """This method should NOT be called directly from tests.""" + method() + + def run(self, result=None): + """Overwrite the unittest run() method for Python 3.11 or newer. + This method should NOT be called directly from tests.""" + if not python3_11_or_newer: + return super().run(result=result) + if result is None: + result = self.defaultTestResult() + startTestRun = getattr(result, 'startTestRun', None) + stopTestRun = getattr(result, 'stopTestRun', None) + if startTestRun is not None: + startTestRun() + else: + stopTestRun = None + result.startTest(self) + try: + testMethod = getattr(self, self._testMethodName) + if ( + getattr(self.__class__, "__unittest_skip__", False) + or getattr(testMethod, "__unittest_skip__", False) + ): + skip_why = ( + getattr(self.__class__, '__unittest_skip_why__', '') + or getattr(testMethod, '__unittest_skip_why__', '') + ) + self._addSkip(result, self, skip_why) + return result + expecting_failure = ( + getattr(self, "__unittest_expecting_failure__", False) + or getattr(testMethod, "__unittest_expecting_failure__", False) + ) + outcome = unittest_helper._Outcome(result) + try: + self._outcome = outcome + with outcome.testPartExecutor(self): + self._callSetUp() + if outcome.success: + outcome.expecting_failure = expecting_failure + with outcome.testPartExecutor(self, isTest=True): + self._callTestMethod(testMethod) + outcome.expecting_failure = False + with outcome.testPartExecutor(self): + self._callTearDown() + self.doCleanups() + for test, reason in outcome.skipped: + self._addSkip(result, test, reason) + for test, exc_info in outcome.errors: + if exc_info is not None: + if issubclass(exc_info[0], self.failureException): + result.addFailure(test, exc_info) + else: + result.addError(test, exc_info) + if outcome.success: + if expecting_failure: + if outcome.expectedFailure: + self._addExpectedFailure( + result, outcome.expectedFailure + ) + else: + self._addUnexpectedSuccess(result) + else: + result.addSuccess(self) + return result + finally: + outcome.errors.clear() + outcome.expectedFailure = None + self._outcome = None + finally: + result.stopTest(self) + if stopTestRun is not None: + stopTestRun() + + def tearDown(self): + """This method runs after every test completes. + Be careful if a subclass of BaseCase overrides setUp(). + If so, add the following line to the subclass's tearDown() method: + super().tearDown() """ + if not hasattr(self, "_using_sb_fixture") and self.__called_teardown: + # This test already called tearDown() + return + if getattr(self, "recorder_mode", None): + page_actions._reconnect_if_disconnected(self.driver) try: + self.__process_recorded_actions() + except Exception as e: + print("\n (Recorder) Code-generation exception:") + if hasattr(e, "msg"): + print("\n" + str(e.msg)) + else: + print(e) + self.__called_teardown = True + self.__called_setup = False + try: + is_pytest = self.is_pytest # This fails if overriding setUp() + if is_pytest: with_selenium = self.with_selenium - except Exception: - sub_class_name = str( - self.__class__.__bases__[0]).split('.')[-1].split("'")[0] - sub_file_name = str(self.__class__.__bases__[0]).split('.')[-2] - sub_file_name = sub_file_name + ".py" - class_name = str(self.__class__).split('.')[-1].split("'")[0] - file_name = str(self.__class__).split('.')[-2] + ".py" - class_name_used = sub_class_name - file_name_used = sub_file_name - if sub_class_name == "BaseCase": - class_name_used = class_name - file_name_used = file_name - fix_setup = "super(%s, self).setUp()" % class_name_used - fix_teardown = "super(%s, self).tearDown()" % class_name_used - message = ("You're overriding SeleniumBase's BaseCase setUp() " - "method with your own setUp() method, which breaks " - "SeleniumBase. You can fix this by going to your " - "%s class located in your %s file and adding the " - "following line of code AT THE BEGINNING of your " - "setUp() method:\n%s\n\nAlso make sure " - "you have added the following line of code AT THE " - "END of your tearDown() method:\n%s\n" - % (class_name_used, file_name_used, - fix_setup, fix_teardown)) - raise Exception(message) + except Exception: + sub_class_name = ( + str(self.__class__.__bases__[0]).split(".")[-1].split("'")[0] + ) + sub_file_name = str(self.__class__.__bases__[0]).split(".")[-2] + sub_file_name = sub_file_name + ".py" + class_name = str(self.__class__).split(".")[-1].split("'")[0] + file_name = str(self.__class__).split(".")[-2] + ".py" + class_name_used = sub_class_name + file_name_used = sub_file_name + if sub_class_name == "BaseCase": + class_name_used = class_name + file_name_used = file_name + message = ( + "You're overriding SeleniumBase's BaseCase setUp() " + "method with your own setUp() method, which breaks " + "SeleniumBase. You can fix this by going to your " + "%s class located in %s, and adding the " + "following line AT THE BEGINNING of your " + "setUp() method:\nsuper().setUp()\n\n" + "Also make sure the following line is AT THE END " + "of your tearDown() method:\nsuper().tearDown()\n" + % (class_name_used, file_name_used) + ) + raise Exception(message) + # *** Start tearDown() officially *** + if hasattr(self.driver, "_already_quit") and self.driver._already_quit: + logging.warning( + " The driver was already quit in a mode" + " that quits the driver automatically." + ) + page_actions._reconnect_if_disconnected(self.driver) + self.__slow_mode_pause_if_active() + has_exception = self.__has_exception() + sb_config._has_exception = has_exception + sb_config._browser_version = self._get_browser_version() + sb_config._driver_name_version = self._get_driver_name_and_version() + + if self.__overrided_default_timeouts: + # Reset default timeouts in case there are more tests + # These were changed in set_default_timeout() + if sb_config._SMALL_TIMEOUT and sb_config._LARGE_TIMEOUT: + settings.SMALL_TIMEOUT = sb_config._SMALL_TIMEOUT + settings.LARGE_TIMEOUT = sb_config._LARGE_TIMEOUT + sb_config._is_timeout_changed = False + self.__overrided_default_timeouts = False + deferred_exception = None + if self.__deferred_assert_failures: + print( + "\nWhen using self.deferred_assert_*() methods in your tests, " + "remember to call self.process_deferred_asserts() afterwards. " + "Now calling in tearDown()...\nFailures Detected:" + ) + if not has_exception: + try: + self.process_deferred_asserts() + except Exception as e: + deferred_exception = e + else: + self.process_deferred_asserts(print_only=True) + if self.is_pytest: + # pytest-specific code + test_id = self.__get_test_id() if with_selenium: # Save a screenshot if logging is on when an exception occurs if has_exception: self.__add_pytest_html_extra() - if self.with_testing_base and not has_exception and ( - self.save_screenshot_after_test): - test_logpath = self.log_path + "/" + test_id - if not os.path.exists(test_logpath): - try: - os.makedirs(test_logpath) - except Exception: - pass # Only reachable during multi-threaded runs + sb_config._has_exception = True + sb_config._has_logs = True + if ( + self.with_testing_base + and not has_exception + and self.save_screenshot_after_test + ): + test_logpath = os.path.join(self.log_path, test_id) + self.__create_log_path_as_needed(test_logpath) if not self.__last_page_screenshot_png: self.__set_last_page_screenshot() + self.__set_last_page_url() + self.__set_last_page_source() log_helper.log_screenshot( test_logpath, self.driver, - self.__last_page_screenshot_png) + self.__last_page_screenshot_png, + ) self.__add_pytest_html_extra() + sb_config._has_logs = True + elif ( + ( + (python3_11_or_newer and py311_patch2) + or "--pdb" in sys.argv + ) + and not has_exception + ): + # Handle a bug where exceptions aren't seen + self.__set_last_page_screenshot() + self.__set_last_page_url() + self.__set_last_page_source() if self.with_testing_base and has_exception: - test_logpath = self.log_path + "/" + test_id - if not os.path.exists(test_logpath): - try: - os.makedirs(test_logpath) - except Exception: - pass # Only reachable during multi-threaded runs - if ((not self.with_screen_shots) and ( - not self.with_basic_test_info) and ( - not self.with_page_source)): + test_logpath = os.path.join(self.log_path, test_id) + self.__create_log_path_as_needed(test_logpath) + if ( + not self.with_screen_shots + and not self.with_basic_test_info + and not self.with_page_source + ): # Log everything if nothing specified (if testing_base) if not self.__last_page_screenshot_png: self.__set_last_page_screenshot() + self.__set_last_page_url() + self.__set_last_page_source() log_helper.log_screenshot( test_logpath, self.driver, - self.__last_page_screenshot_png) + self.__last_page_screenshot_png, + ) log_helper.log_test_failure_data( - self, test_logpath, self.driver, self.browser) - log_helper.log_page_source(test_logpath, self.driver) + self, + test_logpath, + self.driver, + self.browser, + self.__last_page_url, + ) + log_helper.log_page_source( + test_logpath, self.driver, self.__last_page_source + ) else: if self.with_screen_shots: if not self.__last_page_screenshot_png: self.__set_last_page_screenshot() + self.__set_last_page_url() + self.__set_last_page_source() log_helper.log_screenshot( test_logpath, self.driver, - self.__last_page_screenshot_png) + self.__last_page_screenshot_png, + ) if self.with_basic_test_info: log_helper.log_test_failure_data( - self, test_logpath, self.driver, self.browser) + self, + test_logpath, + self.driver, + self.browser, + self.__last_page_url, + ) if self.with_page_source: log_helper.log_page_source( - test_logpath, self.driver) + test_logpath, + self.driver, + self.__last_page_source, + ) + if self.dashboard: + if self._multithreaded: + with self.dash_lock: + with suppress(Exception): + shared_utils.make_writable( + constants.Dashboard.LOCKFILE + ) + self.__process_dashboard(has_exception) + else: + self.__process_dashboard(has_exception) + if self._final_debug: + self.__activate_debug_mode_in_teardown() # (Pytest) Finally close all open browser windows self.__quit_all_drivers() - if self.headless: - if self.headless_active: - self.display.stop() - self.display = None if self.with_db_reporting: if has_exception: - self.__insert_test_result(constants.State.ERROR, True) + self.__insert_test_result(constants.State.FAILED, True) else: - self.__insert_test_result(constants.State.PASS, False) - runtime = int(time.time() * 1000) - self.execution_start_time + test_id = self.__get_test_id_2() + if test_id in sb_config._results.keys() and ( + sb_config._results[test_id] == "Skipped" + ): + self.__insert_test_result( + constants.State.SKIPPED, False + ) + else: + self.__insert_test_result( + constants.State.PASSED, False + ) + runtime = int(time.time() * 1000.0) - self.execution_start_time self.testcase_manager.update_execution_data( - self.execution_guid, runtime) + self.execution_guid, runtime + ) if self.with_s3_logging and has_exception: - """ If enabled, upload logs to S3 during test exceptions. """ + """If enabled, upload logs to S3 during test exceptions.""" + import uuid from seleniumbase.core.s3_manager import S3LoggingBucket + s3_bucket = S3LoggingBucket() guid = str(uuid.uuid4().hex) - path = "%s/%s" % (self.log_path, test_id) + path = os.path.join(self.log_path, test_id) uploaded_files = [] for logfile in os.listdir(path): - logfile_name = "%s/%s/%s" % (guid, - test_id, - logfile.split(path)[-1]) - s3_bucket.upload_file(logfile_name, - "%s/%s" % (path, logfile)) + logfile_name = "%s/%s/%s" % ( + guid, + test_id, + logfile.split(path)[-1], + ) + s3_bucket.upload_file( + logfile_name, "%s" % os.path.join(path, logfile) + ) uploaded_files.append(logfile_name) s3_bucket.save_uploaded_file_names(uploaded_files) - index_file = s3_bucket.upload_index_file(test_id, guid) - print("\n\n*** Log files uploaded: ***\n%s\n" % index_file) - logging.error( - "\n\n*** Log files uploaded: ***\n%s\n" % index_file) + index_file = s3_bucket.upload_index_file( + test_id, guid, self.data_path, self.save_data_to_logs + ) + print("\n*** Log files uploaded: ***\n%s\n" % index_file) + logging.info( + "\n*** Log files uploaded: ***\n%s\n" % index_file + ) if self.with_db_reporting: + from seleniumbase.core.testcase_manager import ( + TestcaseDataPayload, + TestcaseManager, + ) + self.testcase_manager = TestcaseManager(self.database_env) data_payload = TestcaseDataPayload() data_payload.guid = self.testcase_guid data_payload.logURL = index_file self.testcase_manager.update_testcase_log_url(data_payload) else: - # (Nosetests) + # (Pynose / Behave / Pure Python) + if getattr(self, "is_behave", None): + if sb_config.behave_scenario.status.name == "failed": + has_exception = True + sb_config._has_exception = True + msg = " โŒ Scenario Failed! (Skipping remaining steps:)" + if is_windows: + c1 = colorama.Fore.RED + colorama.Back.LIGHTRED_EX + cr = colorama.Style.RESET_ALL + msg = msg.replace("โŒ", c1 + "><" + cr) + print(msg) + else: + msg = " โœ… Scenario Passed!" + if is_windows: + c2 = colorama.Fore.GREEN + colorama.Back.LIGHTGREEN_EX + cr = colorama.Style.RESET_ALL + msg = msg.replace("โœ…", c2 + "<>" + cr) + print(msg) + if self.dashboard: + self.__process_dashboard(has_exception) if has_exception: - test_id = "%s.%s.%s" % (self.__class__.__module__, - self.__class__.__name__, - self._testMethodName) - test_logpath = "latest_logs/" + test_id - if not os.path.exists(test_logpath): - try: - os.makedirs(test_logpath) - except Exception: - pass # Only reachable during multi-threaded runs + test_id = self.__get_test_id() + test_logpath = os.path.join(self.log_path, test_id) + self.__create_log_path_as_needed(test_logpath) log_helper.log_test_failure_data( - self, test_logpath, self.driver, self.browser) + self, + test_logpath, + self.driver, + self.browser, + self.__last_page_url, + ) if len(self._drivers_list) > 0: if not self.__last_page_screenshot_png: self.__set_last_page_screenshot() + self.__set_last_page_url() + self.__set_last_page_source() log_helper.log_screenshot( test_logpath, self.driver, - self.__last_page_screenshot_png) - log_helper.log_page_source(test_logpath, self.driver) + self.__last_page_screenshot_png, + ) + log_helper.log_page_source( + test_logpath, self.driver, self.__last_page_source + ) elif self.save_screenshot_after_test: - test_id = "%s.%s.%s" % (self.__class__.__module__, - self.__class__.__name__, - self._testMethodName) - test_logpath = "latest_logs/" + test_id - if not os.path.exists(test_logpath): - try: - os.makedirs(test_logpath) - except Exception: - pass # Only reachable during multi-threaded runs + test_id = self.__get_test_id() + test_logpath = os.path.join(self.log_path, test_id) + self.__create_log_path_as_needed(test_logpath) if not self.__last_page_screenshot_png: self.__set_last_page_screenshot() + self.__set_last_page_url() + self.__set_last_page_source() log_helper.log_screenshot( - test_logpath, - self.driver, - self.__last_page_screenshot_png) + test_logpath, self.driver, self.__last_page_screenshot_png + ) if self.report_on: self._last_page_screenshot = self.__last_page_screenshot_png try: self._last_page_url = self.get_current_url() except Exception: self._last_page_url = "(Error: Unknown URL)" - # Finally close all open browser windows + if getattr(self, "is_behave", None) and has_exception: + if getattr(sb_config, "pdb_option", None): + if ( + hasattr(sb_config, "behave_step") + and hasattr(sb_config.behave_step, "exc_traceback") + ): + self.__activate_behave_post_mortem_debug_mode() + if self._final_debug: + self.__activate_debug_mode_in_teardown() + elif getattr(sb_config, "_do_sb_post_mortem", None): + self.__activate_sb_mgr_post_mortem_debug_mode() + elif getattr(sb_config, "_do_sb_final_trace", None): + self.__activate_debug_mode_in_teardown() + # (Pynose / Behave / Pure Python) Close all open browser windows self.__quit_all_drivers() + # Resume tearDown() for all test runners, (Pytest / Pynose / Behave) + if getattr(self, "_xvfb_display", None) and not self._reuse_session: + # Stop the Xvfb virtual display launched from BaseCase + try: + if hasattr(self._xvfb_display, "stop"): + self._xvfb_display.stop() + self._xvfb_display = None + self.headless_active = False + except AttributeError: + pass + except Exception: + pass + if ( + getattr(sb_config, "_virtual_display", None) + and hasattr(sb_config._virtual_display, "stop") + and not getattr(sb_config, "reuse_session", None) + ): + # CDP Mode may launch a 2nd Xvfb virtual display + try: + sb_config._virtual_display.stop() + sb_config._virtual_display = None + sb_config.headless_active = False + except AttributeError: + pass + except Exception: + pass + if self.__visual_baseline_copies: + sb_config._visual_baseline_copies = True + if has_exception: + self.__process_visual_baseline_logs() + if deferred_exception: + # User forgot to call "self.process_deferred_asserts()" in test + raise deferred_exception + + @classmethod + def setUpClass(self): + # Only used when: "--rcs" / "--reuse-class-session" + # Close existing sessions before the class starts. + session_helper.end_reused_class_session_as_needed() + + @classmethod + def tearDownClass(self): + # Only used when: "--rcs" / "--reuse-class-session" + # Close existing sessions after the class finishes. + session_helper.end_reused_class_session_as_needed() + + +r"""-----------------------------------------------------------------> +| ______ __ _ ____ | +| / ____/__ / /__ ____ (_)_ ______ ___ / _ \____ ________ | +| \__ \/ _ \/ / _ \/ __ \/ / / / / __ `__ \ / /_) / __ \/ ___/ _ \ | +| ___/ / __/ / __/ / / / / /_/ / / / / / // /_) / (_/ /__ / __/ | +| /____/\___/_/\___/_/ /_/_/\__,_/_/ /_/ /_//_____/\__,_/____/\___/ | +| | +------------------------------------------------------------------>""" diff --git a/seleniumbase/fixtures/constants.py b/seleniumbase/fixtures/constants.py old mode 100755 new mode 100644 index 0b8b42533da..c45b5b3ab62 --- a/seleniumbase/fixtures/constants.py +++ b/seleniumbase/fixtures/constants.py @@ -1,132 +1,456 @@ -""" -This class containts some frequently-used constants -""" +"""SeleniumBase constants""" +from seleniumbase.core import encoded_images class Environment: # Usage Example => "--env=qa" => Then access value in tests with "self.env" QA = "qa" + RC = "rc" STAGING = "staging" DEVELOP = "develop" PRODUCTION = "production" + PERFORMANCE = "performance" + REPLICA = "replica" + FEDRAMP = "fedramp" + OFFLINE = "offline" + ONLINE = "online" MASTER = "master" + REMOTE = "remote" + LEGACY = "legacy" LOCAL = "local" + ALPHA = "alpha" + BETA = "beta" + DEMO = "demo" + GDPR = "gdpr" + MAIN = "main" TEST = "test" + GOV = "gov" + NEW = "new" + OLD = "old" + UAT = "uat" + + +class ValidEnvs: + valid_envs = [ + "qa", + "rc", + "staging", + "develop", + "production", + "performance", + "replica", + "fedramp", + "offline", + "online", + "master", + "remote", + "legacy", + "local", + "alpha", + "beta", + "demo", + "gdpr", + "main", + "test", + "gov", + "new", + "old", + "uat", + ] + + +class PatchPy311: + # Now that unittest is "patched/fixed" in Python 3.11 and up, + # this second patch might not be needed to fix error-handling. + # Enabling this might slow things slightly to fix some things. + PATCH = False + + +class PageLoadStrategy: + # Usage Example => "--pls=none" + NORMAL = "normal" + EAGER = "eager" + NONE = "none" class Files: + # This is a special downloads folder for files downloaded by tests. + # The "downloaded_files" folder is DELETED when starting new tests. + # Add "--archive-downloads" to save a copy in "archived_files". + # (These folder names should NOT be changed.) DOWNLOADS_FOLDER = "downloaded_files" ARCHIVED_DOWNLOADS_FOLDER = "archived_files" +class Logs: + # This is where log files from the latest run get saved. + # The "latest_logs" folder is DELETED when starting new tests. + # Add "--archive-logs" to save a copy of logs in "archived_logs". + # (These folder names should NOT be changed.) + LATEST = "latest_logs" + SAVED = "archived_logs" + + +class Presentations: + SAVED_FOLDER = "saved_presentations" + + +class Charts: + SAVED_FOLDER = "saved_charts" + + +class Recordings: + SAVED_FOLDER = "recordings" + + +class Dashboard: + TITLE = "SeleniumBaseย Dashboardย ๐Ÿ“Š" + # STYLE_CSS = "https://seleniumbase.io/cdn/css/pytest_style.css" + STYLE_CSS = "assets/pytest_style.css" # Generated before tests + META_REFRESH_HTML = '' + # LIVE_JS = "https://livejs.com/live.js#html" + # LIVE_JS = "https://seleniumbase.io/cdn/js/live.js#html" + LIVE_JS = "assets/live.js#html" # Generated before tests + LOCKFILE = Files.DOWNLOADS_FOLDER + "/dashboard.lock" + DASH_JSON = Files.DOWNLOADS_FOLDER + "/dashboard.json" + DASH_PIE = Files.DOWNLOADS_FOLDER + "/dash_pie.json" + + def get_dash_pie_1(): + if not hasattr(encoded_images, "DASH_PIE_PNG_1"): + encoded_images.DASH_PIE_PNG_1 = encoded_images.get_dash_pie_png1() + return encoded_images.DASH_PIE_PNG_1 + + def get_dash_pie_2(): + if not hasattr(encoded_images, "DASH_PIE_PNG_2"): + encoded_images.DASH_PIE_PNG_2 = encoded_images.get_dash_pie_png2() + return encoded_images.DASH_PIE_PNG_2 + + def get_dash_pie_3(): + if not hasattr(encoded_images, "DASH_PIE_PNG_3"): + encoded_images.DASH_PIE_PNG_3 = encoded_images.get_dash_pie_png3() + return encoded_images.DASH_PIE_PNG_3 + + +class PipInstall: + # FINDLOCK - Checking to see if a package is installed + # (Make sure a package isn't installed multiple times) + FINDLOCK = Files.DOWNLOADS_FOLDER + "/pipfinding.lock" + # LOCKFILE - Locking before performing any pip install + # (Make sure that only one package installs at a time) + LOCKFILE = Files.DOWNLOADS_FOLDER + "/pipinstall.lock" + + +class Report: + def get_favicon(): + if not hasattr(encoded_images, "REPORT_FAVICON"): + encoded_images.REPORT_FAVICON = encoded_images.get_report_favicon() + return encoded_images.REPORT_FAVICON + LOCKFILE = Files.DOWNLOADS_FOLDER + "/htmlreport.lock" + + +class SideBySide: + HTML_FILE = "side_by_side.html" + + def get_favicon(): + if not hasattr(encoded_images, "SIDE_BY_SIDE_PNG"): + encoded_images.SIDE_BY_SIDE_PNG = ( + encoded_images.get_side_by_side_png() + ) + return encoded_images.SIDE_BY_SIDE_PNG + + +class MultiBrowser: + DRIVER_FIXING_LOCK = Files.DOWNLOADS_FOLDER + "/driver_fixing.lock" + DRIVER_REPAIRED = Files.DOWNLOADS_FOLDER + "/driver_fixed.lock" + CERT_FIXING_LOCK = Files.DOWNLOADS_FOLDER + "/cert_fixing.lock" + DOWNLOAD_FILE_LOCK = Files.DOWNLOADS_FOLDER + "/downloading.lock" + FILE_IO_LOCK = Files.DOWNLOADS_FOLDER + "/file_io.lock" + PYAUTOGUILOCK = Files.DOWNLOADS_FOLDER + "/pyautogui.lock" + + +class SavedCookies: + STORAGE_FOLDER = "saved_cookies" + + +class Tours: + EXPORTED_TOURS_FOLDER = "tours_exported" + + class VisualBaseline: STORAGE_FOLDER = "visual_baseline" +class Values: + # Demo Mode has slow scrolling to see where you are on the page better. + # However, a regular slow scroll takes too long to cover big distances. + # If the scroll distance is greater than SSMD, a slow scroll speeds up. + SSMD = 500 # Smooth Scroll Minimum Distance (for advanced slow scroll) + + +class Scroll: + Y_OFFSET = 182 + + +class Warnings: + SCREENSHOT_SKIPPED = "Skipping screenshot!" + SCREENSHOT_UNDEFINED = "Unable to get screenshot!" + PAGE_SOURCE_UNDEFINED = "Unable to get page source!" + INVALID_RUN_COMMAND = "INVALID RUN COMMAND!" + + class JQuery: - VER = "3.4.1" - MIN_JS = "//cdnjs.cloudflare.com/ajax/libs/jquery/%s/jquery.min.js" % VER - # MIN_JS = "//ajax.aspnetcdn.com/ajax/jQuery/jquery-%s.min.js" % VER - # MIN_JS = "//ajax.googleapis.com/ajax/libs/jquery/%s/jquery.min.js" % VER + VER = "3.7.1" + MIN_JS = "https://cdn.jsdelivr.net/npm/jquery@%s/dist/jquery.min.js" % VER class Messenger: + LIB = "https://cdn.jsdelivr.net/npm/messenger-hubspot" VER = "1.5.0" - MIN_CSS = ("//cdnjs.cloudflare.com/ajax/libs/" - "messenger/%s/css/messenger.min.css" % VER) - MIN_JS = ("//cdnjs.cloudflare.com/ajax/libs/" - "messenger/%s/js/messenger.min.js" % VER) - THEME_FLAT_JS = ("//cdnjs.cloudflare.com/ajax/libs/" - "messenger/%s/js/messenger-theme-flat.js" % VER) - THEME_FUTURE_JS = ("//cdnjs.cloudflare.com/ajax/libs/" - "messenger/%s/js/messenger-theme-future.js" % VER) - THEME_FLAT_CSS = ("//cdnjs.cloudflare.com/ajax/libs/" - "messenger/%s/css/messenger-theme-flat.css" % VER) - THEME_FUTURE_CSS = ("//cdnjs.cloudflare.com/ajax/libs/" - "messenger/%s/css/messenger-theme-future.css" % VER) - THEME_BLOCK_CSS = ("//cdnjs.cloudflare.com/ajax/libs/" - "messenger/%s/css/messenger-theme-block.css" % VER) - THEME_AIR_CSS = ("//cdnjs.cloudflare.com/ajax/libs/" - "messenger/%s/css/messenger-theme-air.css" % VER) - THEME_ICE_CSS = ("//cdnjs.cloudflare.com/ajax/libs/" - "messenger/%s/css/messenger-theme-ice.css" % VER) - SPINNER_CSS = ("//cdnjs.cloudflare.com/ajax/libs/" - "messenger/%s/css/messenger-spinner.css" % VER) + THEME = "messenger-theme" + MIN_CSS = "%s@%s/build/css/messenger.min.css" % (LIB, VER) + MIN_JS = "%s@%s/build/js/messenger.min.js" % (LIB, VER) + THEME_FLAT_JS = "%s@%s/build/js/%s-flat.min.js" % (LIB, VER, THEME) + THEME_FUTURE_JS = "%s@%s/build/js/%s-future.min.js" % (LIB, VER, THEME) + THEME_FLAT_CSS = "%s@%s/build/css/%s-flat.min.css" % (LIB, VER, THEME) + THEME_FUTURE_CSS = "%s@%s/build/css/%s-future.min.css" % (LIB, VER, THEME) + THEME_BLOCK_CSS = "%s@%s/build/css/%s-block.min.css" % (LIB, VER, THEME) + THEME_AIR_CSS = "%s@%s/build/css/%s-air.min.css" % (LIB, VER, THEME) + THEME_ICE_CSS = "%s@%s/build/css/%s-ice.min.css" % (LIB, VER, THEME) + SPINNER_CSS = "%s@%s/build/css/messenger-spinner.min.css" % (LIB, VER) class Underscore: - VER = "1.9.1" - MIN_JS = ("//cdnjs.cloudflare.com/ajax/libs/" - "underscore.js/%s/underscore-min.js" % VER) + VER = "1.13.6" + MIN_JS = ( + "https://cdn.jsdelivr.net/npm/underscore@%s/underscore.min.js" % VER + ) class Backbone: - VER = "1.3.3" - MIN_JS = ("//cdnjs.cloudflare.com/ajax/libs/" - "backbone.js/%s/backbone-min.js" % VER) + VER = "1.6.0" + MIN_JS = "https://cdn.jsdelivr.net/npm/backbone@%s/backbone.min.js" % VER + + +class HtmlInspector: + VER = "0.8.2" + MIN_JS = ( + "https://cdnjs.cloudflare.com/ajax/libs/" + "html-inspector/%s/html-inspector.min.js" % VER + ) + + +class PrettifyJS: + RUN_PRETTIFY_JS = ( + "https://cdn.jsdelivr.net/gh/google/" + "code-prettify@master/loader/run_prettify.js" + ) + + +class Reveal: + LIB = "https://cdn.jsdelivr.net/npm/reveal.js" + VER = "3.8.0" + MIN_CSS = "%s@%s/css/reveal.min.css" % (LIB, VER) + SERIF_MIN_CSS = "%s@%s/css/theme/serif.min.css" % (LIB, VER) + WHITE_MIN_CSS = "%s@%s/css/theme/white.min.css" % (LIB, VER) + BLACK_MIN_CSS = "%s@%s/css/theme/black.min.css" % (LIB, VER) + SKY_MIN_CSS = "%s@%s/css/theme/sky.min.css" % (LIB, VER) + MOON_MIN_CSS = "%s@%s/css/theme/moon.min.css" % (LIB, VER) + NIGHT_MIN_CSS = "%s@%s/css/theme/night.min.css" % (LIB, VER) + LEAGUE_MIN_CSS = "%s@%s/css/theme/league.min.css" % (LIB, VER) + BEIGE_MIN_CSS = "%s@%s/css/theme/beige.min.css" % (LIB, VER) + BLOOD_MIN_CSS = "%s@%s/css/theme/blood.min.css" % (LIB, VER) + SIMPLE_MIN_CSS = "%s@%s/css/theme/simple.min.css" % (LIB, VER) + SOLARIZED_MIN_CSS = "%s@%s/css/theme/solarized.min.css" % (LIB, VER) + MIN_JS = "%s@%s/js/reveal.min.js" % (LIB, VER) + + +class HighCharts: + LIB = "https://cdn.jsdelivr.net/npm/highcharts" + VER = "10.3.3" + HC_CSS = "%s@%s/css/highcharts.css" % (LIB, VER) + HC_JS = "%s@%s/highcharts.js" % (LIB, VER) + EXPORTING_JS = "%s@%s/modules/exporting.js" % (LIB, VER) + EXPORT_DATA_JS = "%s@%s/modules/export-data.js" % (LIB, VER) + ACCESSIBILITY_JS = "%s@%s/modules/accessibility.js" % (LIB, VER) class BootstrapTour: - VER = "0.11.0" - MIN_CSS = ("//cdnjs.cloudflare.com/ajax/libs/" - "bootstrap-tour/%s/css/bootstrap-tour-standalone.min.css" % VER) - MIN_JS = ("//cdnjs.cloudflare.com/ajax/libs/" - "bootstrap-tour/%s/js/bootstrap-tour-standalone.min.js" % VER) + LIB = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap-tour" + VER = "0.12.0" + MIN_CSS = "%s/%s/css/bootstrap-tour-standalone.min.css" % (LIB, VER) + # MIN_JS = "%s/%s/js/bootstrap-tour-standalone.min.js" % (LIB, VER) + MIN_JS = "https://seleniumbase.github.io/cdn/js/bootstraptour.min.js" + + +class DriverJS: + LIB = "https://cdn.jsdelivr.net/npm/driver.js" + VER = "0.9.8" + MIN_CSS = "%s@%s/dist/driver.min.css" % (LIB, VER) + MIN_JS = "%s@%s/dist/driver.min.js" % (LIB, VER) class Hopscotch: + LIB = "https://cdn.jsdelivr.net/npm/hopscotch" VER = "0.3.1" - MIN_CSS = ("//cdnjs.cloudflare.com/ajax/libs/" - "hopscotch/%s/css/hopscotch.min.css" % VER) - MIN_JS = ("//cdnjs.cloudflare.com/ajax/libs/" - "hopscotch/%s/js/hopscotch.min.js" % VER) + MIN_CSS = "%s@%s/dist/css/hopscotch.min.css" % (LIB, VER) + MIN_JS = "%s@%s/dist/js/hopscotch.min.js" % (LIB, VER) class IntroJS: - VER = "2.9.3" - MIN_CSS = ("//cdnjs.cloudflare.com/ajax/libs/" - "intro.js/%s/introjs.css" % VER) - MIN_JS = ("//cdnjs.cloudflare.com/ajax/libs/" - "intro.js/%s/intro.min.js" % VER) + VER = "5.1.0" + MIN_CSS = ( + "https://cdn.jsdelivr.net/npm/" + "intro.js@%s/minified/introjs.min.css" % VER + ) + MIN_JS = "https://cdn.jsdelivr.net/npm/intro.js@%s/intro.min.js" % VER + + +class TourColor: + # Used for button colors in IntroJS Tours + # theme_color = "#f26721" # Orange + # hover_color = "#db5409" # Darker Orange + theme_color = "#367be5" # Blue + hover_color = "#245ac0" # Darker Blue class JqueryConfirm: - VER = "3.3.2" - MIN_CSS = ("//cdnjs.cloudflare.com/ajax/libs/" - "jquery-confirm/%s/jquery-confirm.min.css" % VER) - MIN_JS = ("//cdnjs.cloudflare.com/ajax/libs/" - "jquery-confirm/%s/jquery-confirm.min.js" % VER) + LIB = "https://cdn.jsdelivr.net/npm/jquery-confirm" + VER = "3.3.4" + MIN_CSS = "%s@%s/css/jquery-confirm.min.css" % (LIB, VER) + MIN_JS = "%s@%s/js/jquery-confirm.min.js" % (LIB, VER) + DEFAULT_THEME = "bootstrap" + DEFAULT_COLOR = "blue" + DEFAULT_WIDTH = "38%" class Shepherd: + LIB = "https://cdnjs.cloudflare.com/ajax/libs/shepherd" VER = "1.8.1" - MIN_JS = ("//cdnjs.cloudflare.com/ajax/libs/" - "shepherd/%s/js/shepherd.min.js" % VER) - THEME_ARROWS_CSS = ("//cdnjs.cloudflare.com/ajax/libs/" - "shepherd/%s/css/shepherd-theme-arrows.css" % VER) - THEME_ARR_FIX_CSS = ("//cdnjs.cloudflare.com/ajax/libs/" - "shepherd/%s/css/shepherd-theme-arrows-fix.css" % VER) - THEME_DEFAULT_CSS = ("//cdnjs.cloudflare.com/ajax/libs/" - "shepherd/%s/css/shepherd-theme-default.css" % VER) - THEME_DARK_CSS = ("//cdnjs.cloudflare.com/ajax/libs/" - "shepherd/%s/css/shepherd-theme-dark.css" % VER) - THEME_SQ_CSS = ("//cdnjs.cloudflare.com/ajax/libs/" - "shepherd/%s/css/shepherd-theme-square.css" % VER) - THEME_SQ_DK_CSS = ("//cdnjs.cloudflare.com/ajax/libs/" - "shepherd/%s/css/shepherd-theme-square-dark.css" % VER) + MIN_JS = "%s/%s/js/shepherd.min.js" % (LIB, VER) + THEME_ARROWS_CSS = "%s/%s/css/shepherd-theme-arrows.css" % (LIB, VER) + THEME_ARR_FIX_CSS = "%s/%s/css/shepherd-theme-arrows-fix.css" % (LIB, VER) + THEME_DEFAULT_CSS = "%s/%s/css/shepherd-theme-default.css" % (LIB, VER) + THEME_DARK_CSS = "%s/%s/css/shepherd-theme-dark.css" % (LIB, VER) + THEME_SQ_CSS = "%s/%s/css/shepherd-theme-square.css" % (LIB, VER) + THEME_SQ_DK_CSS = "%s/%s/css/shepherd-theme-square-dark.css" % (LIB, VER) class Tether: - VER = "1.4.4" - MIN_JS = ("//cdnjs.cloudflare.com/ajax/libs/" - "tether/%s/js/tether.min.js" % VER) + VER = "1.4.7" + MIN_JS = ( + "https://cdn.jsdelivr.net/npm/tether@%s/dist/js/tether.min.js" % VER + ) + + +class ProxyPy: + # The version installed if proxy.py is not installed + VER = "2.4.3" + + +class PyAutoGUI: + # The version installed if PyAutoGUI is not installed + VER = "0.9.54" + + +class Mobile: + # Default values for mobile settings + WIDTH = 390 + HEIGHT = 715 + RATIO = 3 + AGENT = ( + "Mozilla/5.0 (Linux; Android 13; Pixel 7 XL " + "Build/SP2A.220505.006.A1; wv) " + "AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 " + "Chrome/110.0.5028.105 Mobile Safari/537.36" + ) + + +class UC: + RECONNECT_TIME = 2.4 # Seconds + CDP_MODE_OPEN_WAIT = 0.9 # Seconds + EXTRA_WINDOWS_WAIT = 0.3 # Seconds class ValidBrowsers: - valid_browsers = ( - ["chrome", "edge", "firefox", "ie", - "opera", "phantomjs", "safari", - "android", "iphone", "ipad", "remote"]) + valid_browsers = [ + "chrome", + "edge", + "firefox", + "ie", + "safari", + "remote", + "opera", + "brave", + "comet", + ] + + +class ChromiumSubs: + # Chromium browsers that still use chromedriver + chromium_subs = [ + "opera", + "brave", + "comet", + ] + + +class ValidBinaries: + valid_chrome_binaries_on_linux = [ + "google-chrome", + "google-chrome-stable", + "chrome", + "chromium", + "chromium-browser", + "google-chrome-beta", + "google-chrome-dev", + "google-chrome-unstable", + "chrome-headless-shell", + "brave-browser", + "brave-browser-stable", + "brave", + "opera", + "opera-stable", + "comet", + "comet-browser", + "comet-stable", + "chrome.exe", # WSL (Windows Subsystem for Linux) + "chromium.exe", # WSL (Windows Subsystem for Linux) + ] + valid_edge_binaries_on_linux = [ + "microsoft-edge", + "microsoft-edge-stable", + "microsoft-edge-beta", + "microsoft-edge-dev", + "msedge.exe", # WSL (Windows Subsystem for Linux) + ] + valid_chrome_binaries_on_macos = [ + "Google Chrome", + "Chromium", + "Google Chrome for Testing", + "chrome-headless-shell", + "Google Chrome Beta", + "Google Chrome Dev", + "Brave Browser", + "Brave", + "Opera Browser", + "Opera", + "Comet Browser", + "Comet", + ] + valid_edge_binaries_on_macos = [ + "Microsoft Edge", + ] + valid_chrome_binaries_on_windows = [ + "chrome.exe", + "chromium.exe", + "chrome-headless-shell.exe", + "brave.exe", + "opera.exe", + "comet.exe", + ] + valid_edge_binaries_on_windows = [ + "msedge.exe", + ] class Browser: @@ -134,12 +458,7 @@ class Browser: EDGE = "edge" FIREFOX = "firefox" INTERNET_EXPLORER = "ie" - OPERA = "opera" - PHANTOM_JS = "phantomjs" SAFARI = "safari" - ANDROID = "android" - IPHONE = "iphone" - IPAD = "ipad" REMOTE = "remote" VERSION = { @@ -147,13 +466,8 @@ class Browser: "edge": None, "firefox": None, "ie": None, - "opera": None, - "phantomjs": None, "safari": None, - "android": None, - "iphone": None, - "ipad": None, - "remote": None + "remote": None, } LATEST = { @@ -161,21 +475,21 @@ class Browser: "edge": None, "firefox": None, "ie": None, - "opera": None, - "phantomjs": None, "safari": None, - "android": None, - "iphone": None, - "ipad": None, - "remote": None + "remote": None, } +class Protocol: + HTTP = "http" + HTTPS = "https" + + class State: - NOTRUN = "NotRun" + PASSED = "Passed" + FAILED = "Failed" + SKIPPED = "Skipped" + UNTESTED = "Untested" ERROR = "Error" - FAILURE = "Fail" - PASS = "Pass" - SKIP = "Skip" BLOCKED = "Blocked" DEPRECATED = "Deprecated" diff --git a/seleniumbase/fixtures/css_to_xpath.py b/seleniumbase/fixtures/css_to_xpath.py new file mode 100644 index 00000000000..1b70e27e65a --- /dev/null +++ b/seleniumbase/fixtures/css_to_xpath.py @@ -0,0 +1,57 @@ +"""Convert CSS selectors into XPath selectors""" +from cssselect.xpath import GenericTranslator + + +class ConvertibleToCssTranslator(GenericTranslator): + """An implementation of :py:class:`cssselect.GenericTranslator` with + XPath output that more readily converts back to CSS selectors. + The simplified examples in https://devhints.io/xpath were used as a + reference here.""" + + def css_to_xpath(self, css, prefix="//"): + return super().css_to_xpath(css, prefix) + + def xpath_attrib_equals(self, xpath, name, value): + xpath.add_condition("%s=%s" % (name, self.xpath_literal(value))) + return xpath + + def xpath_attrib_includes(self, xpath, name, value): + from cssselect.xpath import is_non_whitespace + + if is_non_whitespace(value): + xpath.add_condition( + "contains(%s, %s)" % (name, self.xpath_literal(value)) + ) + else: + xpath.add_condition("0") + return xpath + + def xpath_attrib_substringmatch(self, xpath, name, value): + if value: + # Attribute selectors are case sensitive + xpath.add_condition( + "contains(%s, %s)" % (name, self.xpath_literal(value)) + ) + else: + xpath.add_condition("0") + return xpath + + def xpath_class(self, class_selector): + xpath = self.xpath(class_selector.selector) + return self.xpath_attrib_includes( + xpath, "@class", class_selector.class_name + ) + + def xpath_descendant_combinator(self, left, right): + """right is a child, grand-child or further descendant of left""" + return left.join("//", right) + + +def convert_css_to_xpath(css): + """Convert CSS Selectors to XPath Selectors. + Example: + convert_css_to_xpath('button:contains("Next")') + Output => "//button[contains(., 'Next')]" + """ + xpath = ConvertibleToCssTranslator().css_to_xpath(css) + return xpath diff --git a/seleniumbase/fixtures/email_manager.py b/seleniumbase/fixtures/email_manager.py deleted file mode 100755 index 6581775aea6..00000000000 --- a/seleniumbase/fixtures/email_manager.py +++ /dev/null @@ -1,514 +0,0 @@ -""" -EmailManager - a helper class to login, search for, and delete emails. -""" - -import email -import htmlentitydefs -import imaplib -import quopri -import re -import time -import types -from seleniumbase.config import settings - - -class EmailManager: - """ A helper class to interface with an Email account. These imap methods - can search for and fetch messages without needing a browser. - - Example: - - em = EmailManager() - result = em.check_for_recipient( - "[GMAIL.USER]+[SOME CODE OR TIMESTAMP KEY]@gmail.com") - """ - - HTML = "text/html" - PLAIN = "text/plain" - TIMEOUT = 1800 - - def __init__(self, uname=settings.EMAIL_USERNAME, - pwd=settings.EMAIL_PASSWORD, - imap_string=settings.EMAIL_IMAP_STRING, - port=settings.EMAIL_IMAP_PORT): - self.uname = uname - self.pwd = pwd - self.imap_string = imap_string - self.port = port - - def imap_connect(self): - """ - Connect to the IMAP mailbox. - """ - self.mailbox = imaplib.IMAP4_SSL(self.imap_string, self.port) - self.mailbox.login(self.uname, self.pwd) - self.mailbox.select() - - def imap_disconnect(self): - """ - Disconnect from the IMAP mailbox. - """ - self.mailbox.close() - self.mailbox.logout() - - def __imap_search(self, ** criteria_dict): - """ Searches for query in the given IMAP criteria and returns - the message numbers that match as a list of strings. - - Criteria without values (eg DELETED) should be keyword args - with KEY=True, or else not passed. Criteria with values should - be keyword args of the form KEY="VALUE" where KEY is a valid - IMAP key. - - IMAP default is to AND all criteria together. We don't support - other logic quite yet. - - All valid keys: ALL, ANSWERED, BCC , BEFORE , - BODY , CC , DELETED, DRAFT, FLAGGED, FROM - , HEADER (UNTESTED), KEYWORD - , LARGER , NEW, NOT , OLD, ON , - OR (UNTESTED), RECENT, SEEN, - SENTBEFORE , SENTON , SENTSINCE , SINCE , - SMALLER , SUBJECT , TEXT , TO , - UID , UNANSWERED, UNDELETED, UNDRAFT, UNFLAGGED, - UNKEYWORD , UNSEEN. - - For details on keys and their values, see - http://tools.ietf.org/html/rfc3501#section-6.4.4 - - :param criteria_dict: dictionary of search criteria keywords - :raises: EmailException if something in IMAP breaks - :returns: List of message numbers as strings matched by given criteria - """ - self.imap_connect() - - criteria = [] - for key in criteria_dict: - if criteria_dict[key] is True: - criteria.append('(%s)' % key) - else: - criteria.append('(%s "%s")' % (key, criteria_dict[key])) - - # If any of these criteria are not valid IMAP keys, IMAP will tell us. - status, msg_nums = self.mailbox.search('UTF-8', * criteria) - self.imap_disconnect() - - if 0 == len(msg_nums): - msg_nums = [] - - if 'OK' in status: - return self.__parse_imap_search_result(msg_nums) - else: - raise EmailException("IMAP status is " + str(status)) - - def remove_formatting(self, html): - """ - Clean out any whitespace - @Params - html - String of html to remove whitespace from - @Returns - Cleaned string - """ - return ' '.join(html.split()) - - def __parse_imap_search_result(self, result): - """ - This takes the result of imap_search and returns SANE results - @Params - result - result from an imap_search call - @Returns - List of IMAP search results - """ - if isinstance(result, types.ListType): - # Above is same as "type(result) == types.ListType" - if len(result) == 1: - return self.__parse_imap_search_result(result[0]) - else: - return result - elif isinstance(result, types.StringType): - # Above is same as "type(result) == types.StringType" - return result.split() - else: - # Fail silently assuming tests will fail if emails are not found - return [] - - def fetch_html(self, msg_nums): - """ - Given a message number that we found with imap_search, - get the text/html content. - @Params - msg_nums - message number to get html message for - @Returns - HTML content of message matched by message number - """ - if not msg_nums: - raise Exception("Invalid Message Number!") - - return self.__imap_fetch_content_type(msg_nums, self.HTML) - - def fetch_plaintext(self, msg_nums): - """ - Given a message number that we found with imap_search, - get the text/plain content. - @Params - msg_nums - message number to get message for - @Returns - Plaintext content of message matched by message number - """ - if not msg_nums: - raise Exception("Invalid Message Number!") - - return self.__imap_fetch_content_type(msg_nums, self.PLAIN) - - def __imap_fetch_content_type(self, msg_nums, content_type): - """ - Given a message number that we found with imap_search, fetch the - whole source, dump that into an email object, and pick out the part - that matches the content type specified. Return that, if we got - multiple emails, return dict of all the parts. - @Params - msg_nums - message number to search for - content_type - content type of email message to return - @Returns - Specified content type string or dict of all content types of matched - email. - """ - - if not msg_nums: - raise Exception("Invalid Message Number!") - if not content_type: - raise Exception("Need a content type!") - - contents = {} - self.imap_connect() - for num in msg_nums: - status, data = self.mailbox.fetch(num, "(RFC822)") - for response_part in data: - if isinstance(response_part, tuple): - msg = email.message_from_string(response_part[1]) - for part in msg.walk(): - if str(part.get_content_type()) == content_type: - content = str(part.get_payload(decode=True)) - contents[int(num)] = content - self.imap_disconnect() - return contents - - def fetch_html_by_subject(self, email_name): - """ - Get the html of an email, searching by subject. - @Params - email_name - the subject to search for - @Returns - HTML content of the matched email - """ - if not email_name: - raise EmailException("Subject cannot be null") - - results = self.__imap_search(SUBJECT=email_name) - sources = self.fetch_html(results) - - return sources - - def fetch_plaintext_by_subject(self, email_name): - """ - Get the plain text of an email, searching by subject. - @Params - email_name - the subject to search for - @Returns - Plaintext content of the matched email - """ - if not email_name: - raise EmailException("Subject cannot be null") - - results = self.__imap_search(SUBJECT=email_name) - sources = self.fetch_plaintext(results) - - return sources - - def search_for_recipient(self, email, timeout=None, content_type=None): - """ - Get content of emails, sent to a specific email address. - @Params - email - the recipient email address to search for - timeout - seconds to try beore timing out - content_type - type of email string to return - @Returns - Content of the matched email in the given content type - """ - return self.search(timeout=timeout, - content_type=content_type, TO=email) - - def search_for_subject(self, subject, timeout=None, content_type=None): - """ - Get content of emails, sent to a specific email address. - @Params - email - the recipient email address to search for - timeout - seconds to try beore timing out - content_type - type of email string to return - @Returns - Content of the matched email in the given content type - """ - return self.search(timeout=timeout, - content_type=content_type, SUBJECT=subject) - - def search_for_count(self, ** args): - """ - A search that keeps searching up until timeout for a - specific number of matches to a search. If timeout is not - specified we use the default. If count= is not specified we - will fail. Return values are the same as search(), except for count=0, - where we will return an empty list. Use this if you need to wait for a - number of emails other than 1. - - @Params - args - dict of arguments to use in search: - count - number of emails to search for - timeout - seconds to try search before timing out - @Returns - List of message numbers matched by search - """ - if "timeout" not in args.keys(): - timeout = self.TIMEOUT - elif args["timeout"]: - timeout = args["timeout"] - args["timeout"] = timeout / 15 - - if "count" not in args.keys(): - raise EmailException("Count param not defined!") - else: - count = int(args["count"]) - del args["count"] - - results = None - timer = timeout - count = 0 - while count < timer: - try: - results = self.search(** args) - except EmailException: - if count == 0: - return [] - - if results and len(results) == count: - return results - else: - time.sleep(15) - count += 15 - if count >= timer: - raise EmailException("Failed to match criteria %s in %s minutes" % - (args, timeout / 60)) - - def __check_msg_for_headers(self, msg, ** email_headers): - """ - Checks an Email.Message object for the headers in email_headers. - - Following are acceptable header names: ['Delivered-To', - 'Received', 'Return-Path', 'Received-SPF', - 'Authentication-Results', 'DKIM-Signature', - 'DomainKey-Signature', 'From', 'To', 'Message-ID', - 'Subject', 'MIME-Version', 'Content-Type', 'Date', - 'X-Sendgrid-EID', 'Sender']. - - @Params - msg - the Email.message object to check - email_headers - list of headers to check against - @Returns - Boolean whether all the headers were found - """ - all_headers_found = False - email_headers['Delivered-To'] = email_headers['To'] - email_headers.pop('To') - all_headers_found = all(k in msg.keys() for k in email_headers) - - return all_headers_found - - def fetch_message(self, msgnum): - """ - Given a message number, return the Email.Message object. - @Params - msgnum - message number to find - @Returns - Email.Message object for the given message number - """ - self.imap_connect() - status, data = self.mailbox.fetch(msgnum, "(RFC822)") - self.imap_disconnect() - - for response_part in data: - if isinstance(response_part, tuple): - return email.message_from_string(response_part[1]) - - def get_content_type(self, msg, content_type="HTML"): - """ - Given an Email.Message object, gets the content-type payload - as specified by @content_type. This is the actual body of the - email. - @Params - msg - Email.Message object to get message content for - content_type - Type of content to get from the email - @Return - String content of the email in the given type - """ - if "HTML" in content_type.upper(): - content_type = self.HTML - elif "PLAIN" in content_type.upper(): - content_type = self.PLAIN - - for part in msg.walk(): - if str(part.get_content_type()) == content_type: - return str(part.get_payload(decode=True)) - - def search(self, ** args): - """ - Checks email inbox every 15 seconds that match the criteria - up until timeout. - - Search criteria should be keyword args eg - TO="selenium@gmail.com". See __imap_search docstring for list - of valid criteria. If content_type is not defined, will return - a list of msg numbers. - - Options: - - fetch: will return a dict of Message objects, keyed on msgnum, - which can be used to look at headers and other parts of the complete - message. (http://docs.python.org/library/email.message.html) - - timeout: will replace the default module timeout with the - value in SECONDS. - - content_type: should be either "PLAIN" or - "HTML". If defined returns the source of the matched messages - as a dict of msgnum:content. If not defined we return a list - of msg nums. - """ - - if "content_type" not in args.keys(): - content_type = None - elif "HTML" in args["content_type"]: - content_type = self.HTML - del args["content_type"] - elif "PLAIN" in args["content_type"]: - content_type = self.PLAIN - del args["content_type"] - elif args["content_type"]: - content_type = args['content_type'] - del args["content_type"] - - if "timeout" not in args.keys(): - timeout = self.TIMEOUT - elif "timeout" in args: - timeout = args["timeout"] - del args["timeout"] - - fetch = False - if "fetch" in args.keys(): - fetch = True - del args["fetch"] - - results = None - timer = timeout - count = 0 - while count < timer: - results = self.__imap_search(** args) - if len(results) > 0: - if fetch: - msgs = {} - for msgnum in results: - msgs[msgnum] = self.fetch_message(msgnum) - return msgs - elif not content_type: - return results - else: - return self.__imap_fetch_content_type(results, - content_type) - else: - time.sleep(15) - count += 15 - if count >= timer: - raise EmailException( - "Failed to find message for criteria %s in %s minutes" % - (args, timeout / 60)) - - def remove_whitespace(self, html): - """ - Clean whitespace from html - @Params - html - html source to remove whitespace from - @Returns - String html without whitespace - """ - # Does python have a better way to do exactly this? - clean_html = html - for char in ("\r", "\n", "\t"): - clean_html = clean_html.replace(char, "") - return clean_html - - def remove_control_chars(self, html): - """ - Clean control characters from html - @Params - html - html source to remove control characters from - @Returns - String html without control characters - """ - return self.remove_whitespace(html) - - def replace_entities(self, html): - """ - Replace htmlentities with unicode characters - @Params - html - html source to replace entities in - @Returns - String html with entities replaced - """ - def fixup(text): - """replace the htmlentities in some text""" - text = text.group(0) - if text[:2] == "&#": - # character reference - try: - if text[:3] == "&#x": - return chr(int(text[3:-1], 16)) - else: - return chr(int(text[2:-1])) - except ValueError: - pass - else: - # named entity - try: - text = chr(htmlentitydefs.name2codepoint[text[1:-1]]) - except KeyError: - pass - return text # leave as is - return re.sub(r"&#?\w+;", fixup, html) - - def decode_quoted_printable(self, html): - """ - Decoding from Quoted-printable, or QP encoding, that uses ASCII 7bit - chars to encode 8 bit chars, resulting in =3D to represent '='. Python - supports UTF-8 so we decode. Also removes line breaks with '= at the - end.' - @Params - html - html source to decode - @Returns - String decoded HTML source - """ - return self.replace_entities(quopri.decodestring(html)) - - def html_bleach(self, html): - """ - Cleanup and get rid of all extraneous stuff for better comparison - later. Turns formatted into into a single line string. - @Params - html - HTML source to clean up - @Returns - String cleaned up HTML source - """ - return self.decode_quoted_printable(html) - - -class EmailException(Exception): - """Raised when we have an Email-related problem.""" - def __init__(self, value): - self.parameter = value - - def __str__(self): - return repr(self.parameter) diff --git a/seleniumbase/fixtures/errors.py b/seleniumbase/fixtures/errors.py old mode 100755 new mode 100644 index d3ff3c2bc78..c791286dc59 --- a/seleniumbase/fixtures/errors.py +++ b/seleniumbase/fixtures/errors.py @@ -1,20 +1,23 @@ -""" -This module contains test-state related exceptions. +"""SeleniumBase MySQL-related exceptions. + +This feature is DEPRECATED! +Use self.skip() for skipping tests! + Raising one of these in a test will cause the -test-state to be logged appropriately. -""" +test-state to be logged appropriately in the DB +for tests that use the SeleniumBase MySQL option.""" class BlockedTest(Exception): - """Raise this to mark a test as Blocked""" + """Raise this to mark a test as Blocked in the DB.""" pass class SkipTest(Exception): - """Raise this to mark a test as Skipped.""" + """Raise this to mark a test as Skipped in the DB.""" pass class DeprecatedTest(Exception): - """Raise this to mark a test as Deprecated.""" + """Raise this to mark a test as Deprecated in the DB.""" pass diff --git a/seleniumbase/fixtures/js_utils.py b/seleniumbase/fixtures/js_utils.py old mode 100755 new mode 100644 index 8feecfcc60b..7fabb26276a --- a/seleniumbase/fixtures/js_utils.py +++ b/seleniumbase/fixtures/js_utils.py @@ -1,82 +1,152 @@ -""" -This module contains useful Javascript utility methods for base_case.py -These helper methods SHOULD NOT be called directly from tests. -""" +"""This module contains useful Javascript utility methods for BaseCase.""" import re import requests import time +from contextlib import suppress +from selenium.common.exceptions import NoSuchElementException from selenium.common.exceptions import WebDriverException -from seleniumbase.common import decorators +from selenium.webdriver.common.by import By +from seleniumbase import config as sb_config from seleniumbase.config import settings from seleniumbase.fixtures import constants +from seleniumbase.fixtures import css_to_xpath +from seleniumbase.fixtures import shared_utils +from seleniumbase.fixtures import xpath_to_css -def wait_for_ready_state_complete(driver, timeout=settings.EXTREME_TIMEOUT): - """ - The DOM (Document Object Model) has a property called "readyState". +def execute_script(driver, script, *args, **kwargs): + if shared_utils.is_cdp_swap_needed(driver): + return driver.cdp.evaluate(script) + return driver.execute_script(script, *args, **kwargs) + + +def wait_for_ready_state_complete(driver, timeout=settings.LARGE_TIMEOUT): + """The DOM (Document Object Model) has a property called "readyState". When the value of this becomes "complete", page resources are considered - fully loaded (although AJAX and other loads might still be happening). + fully loaded (although AJAX and other loads might still be happening). This method will wait until document.readyState == "complete". - """ - + This may be redundant, as methods already wait for page elements to load. + If the timeout is exceeded, the test will still continue + because readyState == "interactive" may be good enough. + (Previously, tests would fail immediately if exceeding the timeout.)""" + if hasattr(driver, "_swap_driver"): + return + if getattr(settings, "SKIP_JS_WAITS", None): + return start_ms = time.time() * 1000.0 stop_ms = start_ms + (timeout * 1000.0) for x in range(int(timeout * 10)): + if sb_config.time_limit and not sb_config.recorder_mode: + shared_utils.check_if_time_limit_exceeded() try: - ready_state = driver.execute_script("return document.readyState") + ready_state = execute_script(driver, "return document.readyState;") except WebDriverException: # Bug fix for: [Permission denied to access property "document"] time.sleep(0.03) return True - if ready_state == u'complete': - time.sleep(0.01) # Better be sure everything is done loading + if ready_state == "complete": + time.sleep(0.002) return True else: now_ms = time.time() * 1000.0 if now_ms >= stop_ms: break time.sleep(0.1) - raise Exception( - "Page elements never fully loaded after %s seconds!" % timeout) + return False # readyState stayed "interactive" (Not "complete") -def execute_async_script(driver, script, timeout=settings.EXTREME_TIMEOUT): - driver.set_script_timeout(timeout) - return driver.execute_async_script(script) +def execute_async_script(driver, script, timeout=settings.LARGE_TIMEOUT): + if hasattr(driver, "set_script_timeout"): + driver.set_script_timeout(timeout) + if hasattr(driver, "execute_async_script"): + return driver.execute_async_script(script) + else: + return None def wait_for_angularjs(driver, timeout=settings.LARGE_TIMEOUT, **kwargs): - if not settings.WAIT_FOR_ANGULARJS: + if getattr(settings, "SKIP_JS_WAITS", None): + return + with suppress(Exception): + # This closes pop-up alerts + execute_script(driver, "") + if ( + getattr(driver, "_is_using_uc", None) + or not settings.WAIT_FOR_ANGULARJS + ): + wait_for_ready_state_complete(driver) return + if timeout == settings.MINI_TIMEOUT: + timeout = settings.MINI_TIMEOUT / 6.0 + NG_WRAPPER = ( + "%(prefix)s" + "var $elm=document.querySelector(" + "'[data-ng-app],[ng-app],.ng-scope')||document;" + "if(window.angular && angular.getTestability){" + "angular.getTestability($elm).whenStable(%(handler)s)" + "}else{" + "var $inj;try{$inj=angular.element($elm).injector()||" + "angular.injector(['ng'])}catch(ex){" + "$inj=angular.injector(['ng'])};$inj.get=$inj.get||" + "$inj;$inj.get('$browser')." + "notifyWhenNoOutstandingRequests(%(handler)s)}" + "%(suffix)s" + ) + def_pre = "var cb=arguments[arguments.length-1];if(window.angular){" + prefix = kwargs.pop("prefix", def_pre) + handler = kwargs.pop("handler", "function(){cb(true)}") + suffix = kwargs.pop("suffix", "}else{cb(false)}") + script = NG_WRAPPER % { + "prefix": prefix, + "handler": handler, + "suffix": suffix, + } + with suppress(Exception): + execute_async_script(driver, script, timeout=timeout) + + +def convert_to_css_selector(selector, by=By.CSS_SELECTOR): + if by == By.CSS_SELECTOR: + return selector + elif by == By.ID: + return "#%s" % selector + elif by == By.CLASS_NAME: + return ".%s" % selector + elif by == By.NAME: + return '[name="%s"]' % selector + elif by == By.TAG_NAME: + return selector + elif ( + by == By.XPATH + or ( + selector.startswith("/") + or selector.startswith("./") + or selector.startswith("(") + ) + ): + return xpath_to_css.convert_xpath_to_css(selector) + elif by == By.LINK_TEXT: + return 'a:contains("%s")' % selector + elif by == By.PARTIAL_LINK_TEXT: + return 'a:contains("%s")' % selector + else: + raise Exception( + "Exception: Could not convert {%s}(by=%s) to CSS_SELECTOR!" + % (selector, by) + ) + - NG_WRAPPER = '%(prefix)s' \ - 'var $elm=document.querySelector(' \ - '\'[data-ng-app],[ng-app],.ng-scope\')||document;' \ - 'if(window.angular && angular.getTestability){' \ - 'angular.getTestability($elm).whenStable(%(handler)s)' \ - '}else{' \ - 'var $inj;try{$inj=angular.element($elm).injector()||' \ - 'angular.injector([\'ng\'])}catch(ex){' \ - '$inj=angular.injector([\'ng\'])};$inj.get=$inj.get||' \ - '$inj;$inj.get(\'$browser\').' \ - 'notifyWhenNoOutstandingRequests(%(handler)s)}' \ - '%(suffix)s' - def_pre = 'var cb=arguments[arguments.length-1];if(window.angular){' - prefix = kwargs.pop('prefix', def_pre) - handler = kwargs.pop('handler', 'function(){cb(true)}') - suffix = kwargs.pop('suffix', '}else{cb(false)}') - script = NG_WRAPPER % {'prefix': prefix, - 'handler': handler, - 'suffix': suffix} +def is_html_inspector_activated(driver): try: - execute_async_script(driver, script, timeout=timeout) + execute_script(driver, "HTMLInspector;") # Fails if not defined + return True except Exception: - time.sleep(0.05) + return False def is_jquery_activated(driver): try: - driver.execute_script("jQuery('html')") # Fails if jq is not defined + execute_script(driver, "jQuery('html');") # Fails if jq is not defined return True except Exception: return False @@ -84,13 +154,13 @@ def is_jquery_activated(driver): def wait_for_jquery_active(driver, timeout=None): if not timeout: - timeout = int(settings.MINI_TIMEOUT * 10.0) + timeout = 2 else: timeout = int(timeout * 10.0) for x in range(timeout): # jQuery needs a small amount of time to activate. try: - driver.execute_script("jQuery('html')") + execute_script(driver, "jQuery('html');") wait_for_ready_state_complete(driver) wait_for_angularjs(driver) return @@ -98,48 +168,80 @@ def wait_for_jquery_active(driver, timeout=None): time.sleep(0.1) -def activate_jquery(driver): - """ If "jQuery is not defined", use this method to activate it for use. - This happens because jQuery is not always defined on web sites. """ +def raise_unable_to_load_jquery_exception(driver): + has_csp_error = False + csp_violation = "violates the following Content Security Policy directive" + browser_logs = [] try: + browser_logs = driver.get_log("browser") + except (ValueError, WebDriverException): + pass + for entry in browser_logs: + if entry["level"] == "SEVERE": + if csp_violation in entry["message"]: + has_csp_error = True + if has_csp_error: + raise Exception( + """Unable to load jQuery on "%s" due to a violation """ + """of the website's Content Security Policy directive. """ + """To override this policy, add "--disable-csp" on the """ + """command-line when running your tests.""" % driver.current_url + ) + else: + raise Exception( + """Unable to load jQuery on "%s" because this website may be """ + """restricting external JavaScript resources from loading.""" + % driver.current_url + ) + + +def activate_jquery(driver): + # If "jQuery is not defined" on a website, use this method to activate it. + # This method is needed because jQuery is not always defined on web sites. + with suppress(Exception): # Let's first find out if jQuery is already defined. - driver.execute_script("jQuery('html')") + execute_script(driver, "jQuery('html');") # Since that command worked, jQuery is defined. Let's return. return - except Exception: - # jQuery is not currently defined. Let's proceed by defining it. - pass + # jQuery is not defined. It will be loaded in the next part. jquery_js = constants.JQuery.MIN_JS - activate_jquery_script = ( - '''var script = document.createElement('script');''' - '''script.src = "%s";document.getElementsByTagName('head')[0]''' - '''.appendChild(script);''' % jquery_js) - driver.execute_script(activate_jquery_script) - for x in range(int(settings.MINI_TIMEOUT * 10.0)): + add_js_link(driver, jquery_js) + for x in range(36): # jQuery needs a small amount of time to activate. try: - driver.execute_script("jQuery('html')") + execute_script(driver, "jQuery('html');") return + except TypeError as e: + if ( + ( + shared_utils.is_cdp_swap_needed(driver) + or hasattr(driver, "_swap_driver") + ) + and "cannot unpack non-iterable" in str(e) + ): + pass + else: + if x == 18: + add_js_link(driver, jquery_js) + time.sleep(0.1) except Exception: + if x == 18: + add_js_link(driver, jquery_js) time.sleep(0.1) # Since jQuery still isn't activating, give up and raise an exception - raise Exception( - '''Unable to load jQuery on "%s" due to a possible violation ''' - '''of the website's Content Security Policy directive. ''' - '''To override this policy, add "--disable_csp" on the ''' - '''command-line when running your tests.''' % driver.current_url) + raise_unable_to_load_jquery_exception(driver) def are_quotes_escaped(string): - if (string.count("\\'") != string.count("'") or ( - string.count('\\"') != string.count('"'))): + if string.count("\\'") != string.count("'") or ( + string.count('\\"') != string.count('"') + ): return True return False def escape_quotes_if_needed(string): - """ - re.escape() works differently in Python 3.7.0 than earlier versions: + """re.escape() works differently in Python 3.7.0 than earlier versions: Python 3.6.5: >>> import re @@ -161,29 +263,110 @@ def escape_quotes_if_needed(string): return string +def is_in_frame(driver): + # Returns True if the driver has switched to a frame. + # Returns False if the driver was on default content. + if shared_utils.is_cdp_swap_needed(driver): + return False + in_basic_frame = execute_script( + driver, + """ + var frame = window.frameElement; + if (frame) { + return true; + } + else { + return false; + } + """ + ) + location_href = execute_script(driver, """return window.location.href;""") + in_external_frame = False + if driver.current_url != location_href: + in_external_frame = True + if in_basic_frame or in_external_frame: + return True + return False + + def safe_execute_script(driver, script): - """ When executing a script that contains a jQuery command, - it's important that the jQuery library has been loaded first. - This method will load jQuery if it wasn't already loaded. """ + """When executing a script that contains a jQuery command, + it's important that the jQuery library has been loaded first. + This method will load jQuery if it wasn't already loaded.""" try: - driver.execute_script(script) + execute_script(driver, script) + except TypeError as e: + if ( + ( + shared_utils.is_cdp_swap_needed(driver) + or hasattr(driver, "_swap_driver") + ) + and "cannot unpack non-iterable" in str(e) + ): + pass + else: + activate_jquery(driver) # It's a good thing we can define it here + execute_script(driver, script) except Exception: # The likely reason this fails is because: "jQuery is not defined" activate_jquery(driver) # It's a good thing we can define it here - driver.execute_script(script) + execute_script(driver, script) + + +def remove_extra_slashes(selector): + if selector.count('\\"') > 0: + if selector.count('\\"') == selector.count('"'): + selector = selector.replace('\\"', '"') + elif selector.count('\\"') == selector[1:-1].count('"') and ( + "'" not in selector[1:-1] + ): + selector = "'" + selector[1:-1].replace('\\"', '"') + "'" + else: + pass + if selector.count("\\'") > 0: + if selector.count("\\'") == selector.count("'"): + selector = selector.replace("\\'", "'") + elif selector.count("\\'") == selector[1:-1].count("'") and ( + '"' not in selector[1:-1] + ): + selector = '"' + selector[1:-1].replace("\\'", "'") + '"' + else: + pass + return selector + + +def optimize_selector(selector): + if (len(selector) > 2 and selector[0] == "'") and ( + selector[-1] == "'" and '"' not in selector[1:-1] + ): + selector = '"' + selector[1:-1] + '"' + if ( + selector.count('"') == 0 + and selector.count("'") >= 2 + and selector.count("'") % 2 == 0 + and "='" in selector + and "']" in selector + ): + swap_char = "*_SWAP_CHAR_*" + selector = selector.replace("'", swap_char) + selector = selector.replace('"', "'") + selector = selector.replace(swap_char, '"') + return selector def wait_for_css_query_selector( - driver, selector, timeout=settings.SMALL_TIMEOUT): + driver, selector, timeout=settings.LARGE_TIMEOUT +): element = None + selector = escape_quotes_if_needed(selector) + selector = remove_extra_slashes(selector) + selector = optimize_selector(selector) + script = """return document.querySelector('%s');""" % selector start_ms = time.time() * 1000.0 stop_ms = start_ms + (timeout * 1000.0) for x in range(int(timeout * 10)): try: - selector = re.escape(selector) - selector = escape_quotes_if_needed(selector) - element = driver.execute_script( - """return document.querySelector('%s')""" % selector) + element = execute_script(driver, script) if element: return element except Exception: @@ -193,171 +376,402 @@ def wait_for_css_query_selector( if now_ms >= stop_ms: break time.sleep(0.1) + raise NoSuchElementException( + "Element {%s} was not present after %s seconds!" % (selector, timeout) + ) - raise Exception( - "Element {%s} was not present after %s seconds!" % ( - selector, timeout)) + +def is_valid_by(by): + return by in [ + "css selector", "class name", "id", "name", + "link text", "xpath", "tag name", "partial link text", + ] + + +def swap_selector_and_by_if_reversed(selector, by): + if not is_valid_by(by) and is_valid_by(selector): + selector, by = by, selector + return (selector, by) + + +def call_me_later(driver, script, ms): + """Call script after ms passed.""" + call = "function() {%s}" % script + execute_script(driver, "window.setTimeout(%s, %s);" % (call, ms)) + + +def highlight(driver, selector, by="css selector", loops=4): + """For driver.highlight() / driver.page.highlight()""" + swap_selector_and_by_if_reversed(selector, by) + if ":contains(" in selector: + by = "xpath" + selector = css_to_xpath.convert_css_to_xpath(selector) + element = None + try: + element = driver.find_element(by, selector) + except Exception: + time.sleep(1) + element = driver.find_element(by, selector) + o_bs = "" # original_box_shadow + style = element.get_attribute("style") + if style and "box-shadow: " in style: + box_start = style.find("box-shadow: ") + box_end = style.find(";", box_start) + 1 + original_box_shadow = style[box_start:box_end] + o_bs = original_box_shadow + highlight_element_with_js(driver, element, loops=loops, o_bs=o_bs) + + +def highlight_with_js(driver, selector, loops=4, o_bs=""): + with suppress(Exception): + # This closes any pop-up alerts + execute_script(driver, "") + if selector == "html": + selector = "body" + selector_no_spaces = selector.replace(" ", "") + early_exit = False + if '[style=\\"' in selector_no_spaces: + if "box\\-shadow:" in selector: + early_exit = True # Changing the box-shadow changes the selector + elif '[style=\\"' in selector: + selector = selector.replace('[style=\\"', '[style\\*=\\"') + else: + early_exit = True # Changing the box-shadow changes the selector + if early_exit: + return + script = ( + """document.querySelector('%s').style.boxShadow = + '0px 0px 6px 6px rgba(128, 128, 128, 0.5)';""" + % selector + ) + try: + execute_script(driver, script) + except Exception: + return + for n in range(loops): + script = ( + """document.querySelector('%s').style.boxShadow = + '0px 0px 6px 6px rgba(255, 0, 0, 1)';""" + % selector + ) + try: + execute_script(driver, script) + except Exception: + return + time.sleep(0.0181) + script = ( + """document.querySelector('%s').style.boxShadow = + '0px 0px 6px 6px rgba(128, 0, 128, 1)';""" + % selector + ) + try: + execute_script(driver, script) + except Exception: + return + time.sleep(0.0181) + script = ( + """document.querySelector('%s').style.boxShadow = + '0px 0px 6px 6px rgba(0, 0, 255, 1)';""" + % selector + ) + try: + execute_script(driver, script) + except Exception: + return + time.sleep(0.0181) + script = ( + """document.querySelector('%s').style.boxShadow = + '0px 0px 6px 6px rgba(0, 255, 0, 1)';""" + % selector + ) + try: + execute_script(driver, script) + except Exception: + return + time.sleep(0.0181) + script = ( + """document.querySelector('%s').style.boxShadow = + '0px 0px 6px 6px rgba(128, 128, 0, 1)';""" + % selector + ) + try: + execute_script(driver, script) + except Exception: + return + time.sleep(0.0181) + script = ( + """document.querySelector('%s').style.boxShadow = + '0px 0px 6px 6px rgba(128, 0, 128, 1)';""" + % selector + ) + try: + execute_script(driver, script) + except Exception: + return + time.sleep(0.0181) + script = """document.querySelector('%s').style.boxShadow = + '%s';""" % ( + selector, + o_bs, + ) + try: + execute_script(driver, script) + except Exception: + return -def highlight_with_js(driver, selector, loops, o_bs): - script = ("""document.querySelector('%s').style = - 'box-shadow: 0px 0px 6px 6px rgba(128, 128, 128, 0.5)';""" - % selector) - driver.execute_script(script) +def highlight_element_with_js(driver, element, loops=4, o_bs=""): + with suppress(Exception): + # This closes any pop-up alerts + execute_script(driver, "") + script = ( + """arguments[0].style.boxShadow = + '0px 0px 6px 6px rgba(128, 128, 128, 0.5)';""" + ) + try: + execute_script(driver, script, element) + except Exception: + return for n in range(loops): - script = ("""document.querySelector('%s').style = - 'box-shadow: 0px 0px 6px 6px rgba(255, 0, 0, 1)';""" - % selector) - driver.execute_script(script) + script = ( + """arguments[0].style.boxShadow = + '0px 0px 6px 6px rgba(255, 0, 0, 1)';""" + ) + try: + execute_script(driver, script, element) + except Exception: + return time.sleep(0.0181) - script = ("""document.querySelector('%s').style = - 'box-shadow: 0px 0px 6px 6px rgba(128, 0, 128, 1)';""" - % selector) - driver.execute_script(script) + script = ( + """arguments[0].style.boxShadow = + '0px 0px 6px 6px rgba(128, 0, 128, 1)';""" + ) + try: + execute_script(driver, script, element) + except Exception: + return time.sleep(0.0181) - script = ("""document.querySelector('%s').style = - 'box-shadow: 0px 0px 6px 6px rgba(0, 0, 255, 1)';""" - % selector) - driver.execute_script(script) + script = ( + """arguments[0].style.boxShadow = + '0px 0px 6px 6px rgba(0, 0, 255, 1)';""" + ) + try: + execute_script(driver, script, element) + except Exception: + return time.sleep(0.0181) - script = ("""document.querySelector('%s').style = - 'box-shadow: 0px 0px 6px 6px rgba(0, 255, 0, 1)';""" - % selector) - driver.execute_script(script) + script = ( + """arguments[0].style.boxShadow = + '0px 0px 6px 6px rgba(0, 255, 0, 1)';""" + ) + try: + execute_script(driver, script, element) + except Exception: + return time.sleep(0.0181) - script = ("""document.querySelector('%s').style = - 'box-shadow: 0px 0px 6px 6px rgba(128, 128, 0, 1)';""" - % selector) - driver.execute_script(script) + script = ( + """arguments[0].style.boxShadow = + '0px 0px 6px 6px rgba(128, 128, 0, 1)';""" + ) + try: + execute_script(driver, script, element) + except Exception: + return time.sleep(0.0181) - script = ("""document.querySelector('%s').style = - 'box-shadow: 0px 0px 6px 6px rgba(128, 0, 128, 1)';""" - % selector) - driver.execute_script(script) + script = ( + """arguments[0].style.boxShadow = + '0px 0px 6px 6px rgba(128, 0, 128, 1)';""" + ) + try: + execute_script(driver, script, element) + except Exception: + return time.sleep(0.0181) - script = ("""document.querySelector('%s').style = - 'box-shadow: %s';""" - % (selector, o_bs)) - driver.execute_script(script) + script = """arguments[0].style.boxShadow = '%s';""" % (o_bs) + try: + execute_script(driver, script, element) + except Exception: + return -def highlight_with_jquery(driver, selector, loops, o_bs): - script = """jQuery('%s').css('box-shadow', - '0px 0px 6px 6px rgba(128, 128, 128, 0.5)');""" % selector - safe_execute_script(driver, script) +def highlight_with_jquery(driver, selector, loops=4, o_bs=""): + with suppress(Exception): + # This closes any pop-up alerts + execute_script(driver, "") + if selector == "html": + selector = "body" + selector_no_spaces = selector.replace(" ", "") + early_exit = False + if '[style=\\"' in selector_no_spaces: + if "box\\-shadow:" in selector: + early_exit = True # Changing the box-shadow changes the selector + elif '[style=\\"' in selector: + selector = selector.replace('[style=\\"', '[style\\*=\\"') + else: + early_exit = True # Changing the box-shadow changes the selector + if early_exit: + return + script = ( + """jQuery('%s').css('box-shadow', + '0px 0px 6px 6px rgba(128, 128, 128, 0.5)');""" + % selector + ) + with suppress(Exception): + safe_execute_script(driver, script) for n in range(loops): - script = """jQuery('%s').css('box-shadow', - '0px 0px 6px 6px rgba(255, 0, 0, 1)');""" % selector - driver.execute_script(script) + script = ( + """jQuery('%s').css('box-shadow', + '0px 0px 6px 6px rgba(255, 0, 0, 1)');""" + % selector + ) + with suppress(Exception): + execute_script(driver, script) time.sleep(0.0181) - script = """jQuery('%s').css('box-shadow', - '0px 0px 6px 6px rgba(128, 0, 128, 1)');""" % selector - driver.execute_script(script) + script = ( + """jQuery('%s').css('box-shadow', + '0px 0px 6px 6px rgba(128, 0, 128, 1)');""" + % selector + ) + with suppress(Exception): + execute_script(driver, script) time.sleep(0.0181) - script = """jQuery('%s').css('box-shadow', - '0px 0px 6px 6px rgba(0, 0, 255, 1)');""" % selector - driver.execute_script(script) + script = ( + """jQuery('%s').css('box-shadow', + '0px 0px 6px 6px rgba(0, 0, 255, 1)');""" + % selector + ) + with suppress(Exception): + execute_script(driver, script) time.sleep(0.0181) - script = """jQuery('%s').css('box-shadow', - '0px 0px 6px 6px rgba(0, 255, 0, 1)');""" % selector - driver.execute_script(script) + script = ( + """jQuery('%s').css('box-shadow', + '0px 0px 6px 6px rgba(0, 255, 0, 1)');""" + % selector + ) + with suppress(Exception): + execute_script(driver, script) time.sleep(0.0181) - script = """jQuery('%s').css('box-shadow', - '0px 0px 6px 6px rgba(128, 128, 0, 1)');""" % selector - driver.execute_script(script) + script = ( + """jQuery('%s').css('box-shadow', + '0px 0px 6px 6px rgba(128, 128, 0, 1)');""" + % selector + ) + with suppress(Exception): + execute_script(driver, script) time.sleep(0.0181) - script = """jQuery('%s').css('box-shadow', - '0px 0px 6px 6px rgba(128, 0, 128, 1)');""" % selector - driver.execute_script(script) + script = ( + """jQuery('%s').css('box-shadow', + '0px 0px 6px 6px rgba(128, 0, 128, 1)');""" + % selector + ) + with suppress(Exception): + execute_script(driver, script) time.sleep(0.0181) script = """jQuery('%s').css('box-shadow', '%s');""" % (selector, o_bs) - driver.execute_script(script) + with suppress(Exception): + execute_script(driver, script) def add_css_link(driver, css_link): - script_to_add_css = ( - """function injectCSS(css) { - var head = document.getElementsByTagName("head")[0]; - var link = document.createElement("link"); - link.rel = "stylesheet"; - link.type = "text/css"; - link.href = css; - link.crossorigin = "anonymous"; - head.appendChild(link); - } - injectCSS("%s");""") + script_to_add_css = """function injectCSS(css) { + var head_tag=document.getElementsByTagName("head")[0]; + var link_tag=document.createElement("link"); + link_tag.rel="stylesheet"; + link_tag.type="text/css"; + link_tag.href=css; + link_tag.crossorigin="anonymous"; + head_tag.appendChild(link_tag); + } + injectCSS("%s");""" css_link = escape_quotes_if_needed(css_link) - driver.execute_script(script_to_add_css % css_link) + execute_script(driver, script_to_add_css % css_link) def add_js_link(driver, js_link): - script_to_add_js = ( - """function injectJS(link) { - var head = document.getElementsByTagName("head")[0]; - var script = document.createElement("script"); - script.src = link; - script.defer; - script.type="text/javascript"; - script.crossorigin = "anonymous"; - script.onload = function() { null }; - head.appendChild(script); - } - injectJS("%s");""") + script_to_add_js = """function injectJS(link) { + var body_tag=document.getElementsByTagName("body")[0]; + var script_tag=document.createElement("script"); + script_tag.src=link; + script_tag.type="text/javascript"; + script_tag.crossorigin="anonymous"; + script_tag.defer; + script_tag.onload=function() { null }; + body_tag.appendChild(script_tag); + } + injectJS("%s");""" js_link = escape_quotes_if_needed(js_link) - driver.execute_script(script_to_add_js % js_link) + execute_script(driver, script_to_add_js % js_link) def add_css_style(driver, css_style): - add_css_style_script = ( - """function injectStyle(css) { - var head = document.getElementsByTagName("head")[0]; - var style = document.createElement("style"); - style.type = "text/css"; - style.appendChild(document.createTextNode(css)); - head.appendChild(style); - } - injectStyle("%s");""") - css_style = css_style.replace('\n', '') + add_css_style_script = """function injectStyle(css) { + var head_tag=document.getElementsByTagName("head")[0]; + var style_tag=document.createElement("style"); + style_tag.type="text/css"; + style_tag.appendChild(document.createTextNode(css)); + head_tag.appendChild(style_tag); + } + injectStyle("%s");""" + css_style = css_style.replace("\n", "") css_style = escape_quotes_if_needed(css_style) - driver.execute_script(add_css_style_script % css_style) + execute_script(driver, add_css_style_script % css_style) def add_js_code_from_link(driver, js_link): if js_link.startswith("//"): js_link = "http:" + js_link - js_code = requests.get(js_link).text + js_code = requests.get(js_link, timeout=5).text + add_js_code_script = ( + """var body_tag=document.getElementsByTagName('body').item(0);""" + """var script_tag=document.createElement("script");""" + """script_tag.type="text/javascript";""" + """script_tag.onload=function() { null };""" + """script_tag.appendChild(document.createTextNode("%s"));""" + """body_tag.appendChild(script_tag);""" + ) + js_code = js_code.replace("\n", " ") + js_code = escape_quotes_if_needed(js_code) + execute_script(driver, add_js_code_script % js_code) + + +def add_js_code(driver, js_code): add_js_code_script = ( - '''var h = document.getElementsByTagName('head').item(0);''' - '''var s = document.createElement("script");''' - '''s.type = "text/javascript";''' - '''s.onload = function() { null };''' - '''s.appendChild(document.createTextNode("%s"));''' - '''h.appendChild(s);''') - js_code = js_code.replace('\n', '') + """var body_tag=document.getElementsByTagName('body').item(0);""" + """var script_tag=document.createElement("script");""" + """script_tag.type="text/javascript";""" + """script_tag.onload=function() { null };""" + """script_tag.appendChild(document.createTextNode("%s"));""" + """body_tag.appendChild(script_tag);""" + ) + js_code = js_code.replace("\n", " ") js_code = escape_quotes_if_needed(js_code) - driver.execute_script(add_js_code_script % js_code) + execute_script(driver, add_js_code_script % js_code) def add_meta_tag(driver, http_equiv=None, content=None): if http_equiv is None: http_equiv = "Content-Security-Policy" if content is None: - content = ("default-src *; style-src 'self' 'unsafe-inline'; " - "script-src: 'self' 'unsafe-inline' 'unsafe-eval'") - script_to_add_meta = ( - """function injectMeta() { - var meta = document.createElement('meta'); - meta.httpEquiv = "%s"; - meta.content = "%s"; - document.getElementsByTagName('head')[0].appendChild(meta); + content = ( + "default-src *; style-src 'self' 'unsafe-inline'; " + "script-src: 'self' 'unsafe-inline' 'unsafe-eval'" + ) + script_to_add_meta = """function injectMeta() { + var meta_tag=document.createElement('meta'); + meta_tag.httpEquiv="%s"; + meta_tag.content="%s"; + document.getElementsByTagName('head')[0].appendChild(meta_tag); } - injectMeta();""" % (http_equiv, content)) - driver.execute_script(script_to_add_meta) + injectMeta();""" % ( + http_equiv, + content, + ) + execute_script(driver, script_to_add_meta) def is_jquery_confirm_activated(driver): try: - driver.execute_script("jconfirm") # Fails if jq_confirm is not defined + execute_script(driver, "jconfirm;") # Fails if jconfirm is not defined return True except Exception: return False @@ -370,14 +784,17 @@ def activate_jquery_confirm(driver): if not is_jquery_activated(driver): add_js_link(driver, jquery_js) - wait_for_jquery_active(driver, timeout=0.6) + wait_for_jquery_active(driver, timeout=1.2) add_css_link(driver, jq_confirm_css) add_js_link(driver, jq_confirm_js) - for x in range(7): + for x in range(28): # jQuery-Confirm needs a small amount of time to load & activate. + if x == 14: + add_css_link(driver, jq_confirm_css) + add_js_link(driver, jq_confirm_js) try: - driver.execute_script("jconfirm") + execute_script(driver, "jconfirm;") wait_for_ready_state_complete(driver) wait_for_angularjs(driver) return @@ -385,7 +802,37 @@ def activate_jquery_confirm(driver): time.sleep(0.1) +def activate_html_inspector(driver): + jquery_js = constants.JQuery.MIN_JS + html_inspector_js = constants.HtmlInspector.MIN_JS + + if is_html_inspector_activated(driver): + return + if not is_jquery_activated(driver): + add_js_link(driver, jquery_js) + wait_for_jquery_active(driver, timeout=1.2) + wait_for_ready_state_complete(driver) + wait_for_angularjs(driver) + add_js_link(driver, html_inspector_js) + wait_for_ready_state_complete(driver) + wait_for_angularjs(driver) + + for x in range(25): + # HTML-Inspector needs a small amount of time to load & activate. + try: + execute_script(driver, "HTMLInspector;") + wait_for_ready_state_complete(driver) + wait_for_angularjs(driver) + return + except Exception: + time.sleep(0.1) + wait_for_ready_state_complete(driver) + wait_for_angularjs(driver) + + def activate_messenger(driver): + from seleniumbase.core.style_sheet import get_messenger_style + jquery_js = constants.JQuery.MIN_JS messenger_css = constants.Messenger.MIN_CSS messenger_js = constants.Messenger.MIN_JS @@ -398,14 +845,20 @@ def activate_messenger(driver): msgr_theme_ice_css = constants.Messenger.THEME_ICE_CSS spinner_css = constants.Messenger.SPINNER_CSS underscore_js = constants.Underscore.MIN_JS - backbone_js = constants.Backbone.MIN_JS - msg_style = ("Messenger.options = {'maxMessages': 8, " - "extraClasses: 'messenger-fixed " - "messenger-on-bottom messenger-on-right', " - "theme: 'future'}") + msg_style = ( + "Messenger.options = {'maxMessages': 8, " + "extraClasses: 'messenger-fixed " + "messenger-on-bottom messenger-on-right', " + "theme: 'future'}" + ) - add_js_link(driver, jquery_js) + if not is_jquery_activated(driver): + add_js_link(driver, jquery_js) + if hasattr(sb_config, "_cdp_launched") and sb_config._cdp_launched: + time.sleep(0.1) + else: + wait_for_jquery_active(driver, timeout=1.1) add_css_link(driver, messenger_css) add_css_link(driver, msgr_theme_flat_css) add_css_link(driver, msgr_theme_future_css) @@ -413,224 +866,721 @@ def activate_messenger(driver): add_css_link(driver, msgr_theme_air_css) add_css_link(driver, msgr_theme_ice_css) add_js_link(driver, underscore_js) - add_js_link(driver, backbone_js) add_css_link(driver, spinner_css) add_js_link(driver, messenger_js) - add_js_link(driver, msgr_theme_flat_js) - add_js_link(driver, msgr_theme_future_js) + add_css_style(driver, get_messenger_style()) - for x in range(int(settings.MINI_TIMEOUT * 10.0)): + for x in range(10): # Messenger needs a small amount of time to load & activate. try: - driver.execute_script(msg_style) - wait_for_ready_state_complete(driver) - wait_for_angularjs(driver) - return + result = execute_script( + driver, + """ if (typeof Messenger === 'undefined') { return "U"; } """ + ) + if result == "U": + time.sleep(0.022) + continue + else: + break except Exception: - time.sleep(0.1) + time.sleep(0.02) + try: + execute_script(driver, msg_style) + add_js_link(driver, msgr_theme_flat_js) + add_js_link(driver, msgr_theme_future_js) + wait_for_ready_state_complete(driver) + wait_for_angularjs(driver) + return + except Exception: + time.sleep(0.1) -def set_messenger_theme(driver, theme="default", location="default", - max_messages="default"): +def set_messenger_theme( + driver, theme="default", location="default", max_messages="default" +): if theme == "default": theme = "future" if location == "default": location = "bottom_right" + if getattr(sb_config, "mobile_emulator", None): + location = "top_center" if max_messages == "default": max_messages = "8" - valid_themes = ['flat', 'future', 'block', 'air', 'ice'] + valid_themes = ["flat", "future", "block", "air", "ice"] if theme not in valid_themes: raise Exception("Theme: %s is not in %s!" % (theme, valid_themes)) - valid_locations = (['top_left', 'top_center', 'top_right' - 'bottom_left', 'bottom_center', 'bottom_right']) + valid_locations = [ + "top_left", + "top_center", + "top_right", + "bottom_left", + "bottom_center", + "bottom_right", + ] if location not in valid_locations: raise Exception( - "Location: %s is not in %s!" % (location, valid_locations)) + "Location: %s is not in %s!" % (location, valid_locations) + ) - if location == 'top_left': + if location == "top_left": messenger_location = "messenger-on-top messenger-on-left" - elif location == 'top_center': + elif location == "top_center": messenger_location = "messenger-on-top" - elif location == 'top_right': + elif location == "top_right": messenger_location = "messenger-on-top messenger-on-right" - elif location == 'bottom_left': + elif location == "bottom_left": messenger_location = "messenger-on-bottom messenger-on-left" - elif location == 'bottom_center': + elif location == "bottom_center": messenger_location = "messenger-on-bottom" - elif location == 'bottom_right': + elif location == "bottom_right": messenger_location = "messenger-on-bottom messenger-on-right" - msg_style = ("Messenger.options = {'maxMessages': %s, " - "extraClasses: 'messenger-fixed %s', theme: '%s'}" - % (max_messages, messenger_location, theme)) + msg_style = ( + "Messenger.options = {'maxMessages': %s, " + "extraClasses: 'messenger-fixed %s', theme: '%s'}" + % (max_messages, messenger_location, theme) + ) try: - driver.execute_script(msg_style) + time.sleep(0.015) + execute_script(driver, msg_style) except Exception: + time.sleep(0.035) activate_messenger(driver) - driver.execute_script(msg_style) - time.sleep(0.1) + time.sleep(0.175) + with suppress(Exception): + execute_script(driver, msg_style) + time.sleep(0.035) + time.sleep(0.055) -def post_message(driver, message, msg_dur, style="info"): - """ A helper method to post a message on the screen with Messenger. - (Should only be called from post_message() in base_case.py) """ +def post_message(driver, message, msg_dur=None, style="info"): + """A helper method to post a message on the screen with Messenger. + (Should only be called from post_message() in base_case.py)""" if not msg_dur: msg_dur = settings.DEFAULT_MESSAGE_DURATION msg_dur = float(msg_dur) message = re.escape(message) message = escape_quotes_if_needed(message) - messenger_script = ('''Messenger().post({message: "%s", type: "%s", ''' - '''hideAfter: %s, hideOnNavigate: true});''' - % (message, style, msg_dur)) + messenger_script = ( + """Messenger().post({message: "%s", type: "%s", """ + """hideAfter: %s, hideOnNavigate: true});""" + % (message, style, msg_dur) + ) + retry = False try: - driver.execute_script(messenger_script) + execute_script(driver, messenger_script) + except TypeError as e: + if ( + ( + shared_utils.is_cdp_swap_needed(driver) + or hasattr(driver, "_swap_driver") + ) + and "cannot unpack non-iterable" in str(e) + ): + pass + else: + retry = True except Exception: + retry = True + if retry: activate_messenger(driver) set_messenger_theme(driver) try: - driver.execute_script(messenger_script) + execute_script(driver, messenger_script) + except TypeError: + pass except Exception: - time.sleep(0.2) + time.sleep(0.17) activate_messenger(driver) - time.sleep(0.2) + time.sleep(0.17) set_messenger_theme(driver) - time.sleep(0.5) - driver.execute_script(messenger_script) + time.sleep(0.27) + execute_script(driver, messenger_script) -def post_messenger_success_message(driver, message, msg_dur): +def post_messenger_success_message(driver, message, msg_dur=None): if not msg_dur: msg_dur = settings.DEFAULT_MESSAGE_DURATION msg_dur = float(msg_dur) - try: - set_messenger_theme(driver, theme="future", location="bottom_right") - post_message( - driver, message, msg_dur, style="success") + with suppress(Exception): + theme = "future" + location = "bottom_right" + if getattr(sb_config, "mobile_emulator", None): + location = "top_right" + set_messenger_theme(driver, theme=theme, location=location) + post_message(driver, message, msg_dur, style="success") time.sleep(msg_dur + 0.07) - except Exception: - pass -def post_messenger_error_message(driver, message, msg_dur): +def post_messenger_error_message(driver, message, msg_dur=None): if not msg_dur: msg_dur = settings.DEFAULT_MESSAGE_DURATION msg_dur = float(msg_dur) - try: + with suppress(Exception): set_messenger_theme(driver, theme="block", location="top_center") - post_message( - driver, message, msg_dur, style="error") + post_message(driver, message, msg_dur, style="error") time.sleep(msg_dur + 0.07) - except Exception: - pass def highlight_with_js_2(driver, message, selector, o_bs, msg_dur): - script = ("""document.querySelector('%s').style = - 'box-shadow: 0px 0px 6px 6px rgba(128, 128, 128, 0.5)';""" - % selector) - driver.execute_script(script) + with suppress(Exception): + # This closes any pop-up alerts + execute_script(driver, "") + if selector == "html": + selector = "body" + selector_no_spaces = selector.replace(" ", "") + early_exit = False + if '[style=\\"' in selector_no_spaces: + if "box\\-shadow:" in selector: + early_exit = True # Changing the box-shadow changes the selector + elif '[style=\\"' in selector: + selector = selector.replace('[style=\\"', '[style\\*=\\"') + else: + early_exit = True # Changing the box-shadow changes the selector + if early_exit: + with suppress(Exception): + activate_jquery(driver) + post_messenger_success_message(driver, message, msg_dur) + return + script = ( + """document.querySelector('%s').style.boxShadow = + '0px 0px 6px 6px rgba(128, 128, 128, 0.5)';""" + % selector + ) + try: + execute_script(driver, script) + except Exception: + return time.sleep(0.0181) - script = ("""document.querySelector('%s').style = - 'box-shadow: 0px 0px 6px 6px rgba(205, 30, 0, 1)';""" - % selector) - driver.execute_script(script) + script = ( + """document.querySelector('%s').style.boxShadow = + '0px 0px 6px 6px rgba(205, 30, 0, 1)';""" + % selector + ) + try: + execute_script(driver, script) + except Exception: + return time.sleep(0.0181) - script = ("""document.querySelector('%s').style = - 'box-shadow: 0px 0px 6px 6px rgba(128, 0, 128, 1)';""" - % selector) - driver.execute_script(script) + script = ( + """document.querySelector('%s').style.boxShadow = + '0px 0px 6px 6px rgba(128, 0, 128, 1)';""" + % selector + ) + try: + execute_script(driver, script) + except Exception: + return time.sleep(0.0181) - script = ("""document.querySelector('%s').style = - 'box-shadow: 0px 0px 6px 6px rgba(50, 50, 128, 1)';""" - % selector) - driver.execute_script(script) + script = ( + """document.querySelector('%s').style.boxShadow = + '0px 0px 6px 6px rgba(50, 50, 128, 1)';""" + % selector + ) + try: + execute_script(driver, script) + except Exception: + return time.sleep(0.0181) - script = ("""document.querySelector('%s').style = - 'box-shadow: 0px 0px 6px 6px rgba(50, 205, 50, 1)';""" - % selector) - driver.execute_script(script) + script = ( + """document.querySelector('%s').style.boxShadow = + '0px 0px 6px 6px rgba(50, 205, 50, 1)';""" + % selector + ) + try: + execute_script(driver, script) + except Exception: + return time.sleep(0.0181) + with suppress(Exception): + activate_jquery(driver) + post_messenger_success_message(driver, message, msg_dur) + script = """document.querySelector('%s').style.boxShadow = '%s';""" % ( + selector, + o_bs, + ) + try: + execute_script(driver, script) + except Exception: + return - post_messenger_success_message(driver, message, msg_dur) - script = ("""document.querySelector('%s').style = - 'box-shadow: %s';""" % (selector, o_bs)) - driver.execute_script(script) +def highlight_element_with_js_2(driver, message, element, o_bs, msg_dur): + with suppress(Exception): + # This closes any pop-up alerts + execute_script(driver, "") + script = ( + """arguments[0].style.boxShadow = + '0px 0px 6px 6px rgba(128, 128, 128, 0.5)';""" + ) + try: + execute_script(driver, script, element) + except Exception: + return + time.sleep(0.0181) + script = ( + """arguments[0].style.boxShadow = + '0px 0px 6px 6px rgba(205, 30, 0, 1)';""" + ) + try: + execute_script(driver, script, element) + except Exception: + return + time.sleep(0.0181) + script = ( + """arguments[0].style.boxShadow = + '0px 0px 6px 6px rgba(128, 0, 128, 1)';""" + ) + try: + execute_script(driver, script, element) + except Exception: + return + time.sleep(0.0181) + script = ( + """arguments[0].style.boxShadow = + '0px 0px 6px 6px rgba(50, 50, 128, 1)';""" + ) + try: + execute_script(driver, script, element) + except Exception: + return + time.sleep(0.0181) + script = ( + """arguments[0].style.boxShadow = + '0px 0px 6px 6px rgba(50, 205, 50, 1)';""" + ) + try: + execute_script(driver, script, element) + except Exception: + return + time.sleep(0.0181) + with suppress(Exception): + activate_jquery(driver) + post_messenger_success_message(driver, message, msg_dur) + script = """arguments[0].style.boxShadow = '%s';""" % (o_bs) + try: + execute_script(driver, script, element) + except Exception: + return def highlight_with_jquery_2(driver, message, selector, o_bs, msg_dur): - script = """jQuery('%s').css('box-shadow', - '0px 0px 6px 6px rgba(128, 128, 128, 0.5)');""" % selector - safe_execute_script(driver, script) + if selector == "html": + selector = "body" + selector_no_spaces = selector.replace(" ", "") + early_exit = False + if '[style=\\"' in selector_no_spaces: + if "box\\-shadow:" in selector: + early_exit = True # Changing the box-shadow changes the selector + elif '[style=\\"' in selector: + selector = selector.replace('[style=\\"', '[style\\*=\\"') + else: + early_exit = True # Changing the box-shadow changes the selector + if early_exit: + with suppress(Exception): + activate_jquery(driver) + post_messenger_success_message(driver, message, msg_dur) + return + script = ( + """jQuery('%s').css('box-shadow', + '0px 0px 6px 6px rgba(128, 128, 128, 0.5)');""" + % selector + ) + try: + safe_execute_script(driver, script) + except Exception: + return time.sleep(0.0181) - script = """jQuery('%s').css('box-shadow', - '0px 0px 6px 6px rgba(205, 30, 0, 1)');""" % selector - driver.execute_script(script) + script = ( + """jQuery('%s').css('box-shadow', + '0px 0px 6px 6px rgba(205, 30, 0, 1)');""" + % selector + ) + try: + execute_script(driver, script) + except Exception: + return time.sleep(0.0181) - script = """jQuery('%s').css('box-shadow', - '0px 0px 6px 6px rgba(128, 0, 128, 1)');""" % selector - driver.execute_script(script) + script = ( + """jQuery('%s').css('box-shadow', + '0px 0px 6px 6px rgba(128, 0, 128, 1)');""" + % selector + ) + try: + execute_script(driver, script) + except Exception: + return time.sleep(0.0181) - script = """jQuery('%s').css('box-shadow', - '0px 0px 6px 6px rgba(50, 50, 200, 1)');""" % selector - driver.execute_script(script) + script = ( + """jQuery('%s').css('box-shadow', + '0px 0px 6px 6px rgba(50, 50, 200, 1)');""" + % selector + ) + try: + execute_script(driver, script) + except Exception: + return time.sleep(0.0181) - script = """jQuery('%s').css('box-shadow', - '0px 0px 6px 6px rgba(50, 205, 50, 1)');""" % selector - driver.execute_script(script) + script = ( + """jQuery('%s').css('box-shadow', + '0px 0px 6px 6px rgba(50, 205, 50, 1)');""" + % selector + ) + try: + execute_script(driver, script) + except Exception: + return time.sleep(0.0181) - post_messenger_success_message(driver, message, msg_dur) + with suppress(Exception): + activate_jquery(driver) + post_messenger_success_message(driver, message, msg_dur) script = """jQuery('%s').css('box-shadow', '%s');""" % (selector, o_bs) - driver.execute_script(script) + try: + execute_script(driver, script) + except Exception: + return + + +def get_active_element_css(driver): + from seleniumbase.js_code import active_css_js + + if shared_utils.is_cdp_swap_needed(driver): + return driver.cdp.get_active_element_css() + return execute_script(driver, active_css_js.get_active_element_css) + + +def get_locale_code(driver): + script = "return navigator.language || navigator.languages[0];" + return execute_script(driver, script) + + +def get_screen_rect(driver): + return execute_script(driver, "return window.screen;") + + +def get_origin(driver): + return execute_script(driver, "return window.location.origin;") + + +def get_user_agent(driver): + return execute_script(driver, "return navigator.userAgent;") + + +def get_cookie_string(driver): + return execute_script(driver, "return document.cookie;") + + +def get_scroll_distance_to_element(driver, element): + try: + scroll_position = execute_script(driver, "return window.scrollY;") + element_location = None + element_location = element.location["y"] + element_location = element_location - constants.Scroll.Y_OFFSET + if element_location < 0: + element_location = 0 + distance = element_location - scroll_position + return distance + except Exception: + return 0 def scroll_to_element(driver, element): - element_location = element.location['y'] - element_location = element_location - 130 - if element_location < 0: - element_location = 0 - scroll_script = "window.scrollTo(0, %s);" % element_location + if driver.capabilities["browserName"].lower() == "firefox": + return __old_scroll_to_element(driver, element) + try: + driver.execute_script( + "arguments[0].scrollIntoViewIfNeeded(true);", element + ) + return True + except Exception: + return __old_scroll_to_element(driver, element) + + +def __old_scroll_to_element(driver, element): + # Firefox still needs this + # because it doesn't have scrollIntoViewIfNeeded() + element_location_y = None + element_location_x = None + element_width = 0 + screen_width = 0 + try: + element_location_y = element.location["y"] + except Exception: + return False + try: + element_location_x = element.location["x"] + except Exception: + element_location_x = 0 + try: + element_width = element.size["width"] + except Exception: + element_width = 0 + try: + screen_width = driver.get_window_size()["width"] + except Exception: + screen_width = execute_script(driver, "return window.innerWidth;") + element_location_y = element_location_y - constants.Scroll.Y_OFFSET + if element_location_y < 0: + element_location_y = 0 + element_location_x_fix = element_location_x - 400 + if element_location_x_fix < 0: + element_location_x_fix = 0 + if element_location_x + element_width <= screen_width: + element_location_x_fix = 0 + scroll_script = "window.scrollTo(%s, %s);" % ( + element_location_x_fix, element_location_y + ) # The old jQuery scroll_script required by=By.CSS_SELECTOR # scroll_script = "jQuery('%s')[0].scrollIntoView()" % selector + # This other scroll_script does not centralize the element + # execute_script(driver, "arguments[0].scrollIntoView();", element) try: - driver.execute_script(scroll_script) - except WebDriverException: - pass # Older versions of Firefox experienced issues here + execute_script(driver, scroll_script) + return True + except Exception: + return False -def slow_scroll_to_element(driver, element, browser): - if browser == 'ie': +def slow_scroll_to_element(driver, element, *args, **kwargs): + if driver.capabilities["browserName"] == "internet explorer": # IE breaks on slow-scrolling. Do a fast scroll instead. scroll_to_element(driver, element) return - scroll_position = driver.execute_script("return window.scrollY;") - element_location = element.location['y'] - element_location = element_location - 130 - if element_location < 0: - element_location = 0 - distance = element_location - scroll_position + scroll_position = execute_script(driver, "return window.scrollY;") + element_location_y = None + try: + if shared_utils.is_cdp_swap_needed(driver): + element_location_y = element.get_position().y + else: + element_location_y = element.location["y"] + except Exception: + if shared_utils.is_cdp_swap_needed(driver): + element.scroll_into_view() + else: + element.location_once_scrolled_into_view + return + try: + if shared_utils.is_cdp_swap_needed(driver): + element_location_x = element.get_position().x + else: + element_location_x = element.location["x"] + except Exception: + element_location_x = 0 + try: + if shared_utils.is_cdp_swap_needed(driver): + element_width = element.get_position().width + else: + element_width = element.size["width"] + except Exception: + element_width = 0 + try: + if shared_utils.is_cdp_swap_needed(driver): + screen_width = driver.cdp.get_window_size()["width"] + else: + screen_width = driver.get_window_size()["width"] + except Exception: + screen_width = execute_script(driver, "return window.innerWidth;") + element_location_y = element_location_y - constants.Scroll.Y_OFFSET + if element_location_y < 0: + element_location_y = 0 + element_location_x_fix = element_location_x - 400 + if element_location_x_fix < 0: + element_location_x_fix = 0 + if element_location_x + element_width <= screen_width: + element_location_x_fix = 0 + if shared_utils.is_cdp_swap_needed(driver): + distance = element_location_y + else: + distance = element_location_y - scroll_position if distance != 0: total_steps = int(abs(distance) / 50.0) + 2.0 step_value = float(distance) / total_steps new_position = scroll_position for y in range(int(total_steps)): - time.sleep(0.0114) + time.sleep(0.011) new_position += step_value scroll_script = "window.scrollTo(0, %s);" % new_position - driver.execute_script(scroll_script) + execute_script(driver, scroll_script) time.sleep(0.01) - scroll_script = "window.scrollTo(0, %s);" % element_location - driver.execute_script(scroll_script) + scroll_script = "window.scrollTo(%s, %s);" % ( + element_location_x_fix, element_location_y + ) + if not shared_utils.is_cdp_swap_needed(driver): + execute_script(driver, scroll_script) time.sleep(0.01) if distance > 430 or distance < -300: # Add small recovery time for long-distance slow-scrolling time.sleep(0.162) + else: + time.sleep(0.045) + + +def get_drag_and_drop_script(): + # This script uses jQuery to perform a Drag-and-Drop action. + # (Requires the Drag-selector and the Drop-selector to work) + script = r"""(function( $ ) { + $.fn.simulateDragDrop = function(options) { + return this.each(function() { + new $.simulateDragDrop(this, options); + }); + }; + $.simulateDragDrop = function(elem, options) { + this.options = options; + this.simulateEvent(elem, options); + }; + $.extend($.simulateDragDrop.prototype, { + simulateEvent: function(elem, options) { + /*Simulating drag start*/ + var type = 'dragstart'; + var event = this.createEvent(type); + this.dispatchEvent(elem, type, event); + + /*Simulating drop*/ + type = 'drop'; + var dropEvent = this.createEvent(type, {}); + dropEvent.dataTransfer = event.dataTransfer; + this.dispatchEvent( + $(options.dropTarget)[0], type, dropEvent); + + /*Simulating drag end*/ + type = 'dragend'; + var dragEndEvent = this.createEvent(type, {}); + dragEndEvent.dataTransfer = event.dataTransfer; + this.dispatchEvent(elem, type, dragEndEvent); + }, + createEvent: function(type) { + var event = document.createEvent("CustomEvent"); + event.initCustomEvent(type, true, true, null); + event.dataTransfer = { + data: { + }, + setData: function(type, val){ + this.data[type] = val; + }, + getData: function(type){ + return this.data[type]; + } + }; + return event; + }, + dispatchEvent: function(elem, type, event) { + if(elem.dispatchEvent) { + elem.dispatchEvent(event); + }else if( elem.fireEvent ) { + elem.fireEvent("on"+type, event); + } + } + }); + })(jQuery);""" + return script + + +def get_js_drag_and_drop_script(): + # HTML5 Drag-and-Drop script (Requires extra parameters to work) + # param1 (WebElement): Source element to drag + # param2 (WebElement): Target element for the drop (Optional) + # param3 (int): Optional - Drop offset x relative to the target + # param4 (int): Optional - Drop offset y relative to the target + # param4 (int): Optional - Delay in milliseconds (default = 1ms) + # param5 (string): Optional - Key pressed (ALT or CTRL or SHIFT) + script = """var t=arguments,e=t[0],n=t[1],i=t[2]||0,o=t[3]||0,r=t[4]||1, + a=t[5]||'',s='alt'===a||'\ue00a'===a,l='ctrl'===a||'\ue009'===a, + c='shift'===a||'\ue008'===a,u=e.ownerDocument, + f=e.getBoundingClientRect(),g=n?n.getBoundingClientRect():f, + p=f.left+f.width/2,d=f.top+f.height/2,h=g.left+(i||g.width/2), + m=g.top+(o||g.height/2),v=u.elementFromPoint(p,d), + y=u.elementFromPoint(h,m);if(!v||!y){ + var E=new Error('source or target element is not interactable'); + throw E.code=15,E}var _={constructor:DataTransfer,effectAllowed:null, + dropEffect:null,types:[],files:Object.setPrototypeOf([],null), + _items:Object.setPrototypeOf([],{add:function(t,e){ + this[this.length]={_data:''+t,kind:'string', + type:e,getAsFile:function(){},getAsString:function(t){t(this._data)}}, + _.types.push(e)},remove:function(t){ + Array.prototype.splice.call(this,65535&t,1),_.types.splice(65535&t,1)}, + clear:function(t,e){this.length=0,_.types.length=0}}), + setData:function(t,e){this.clearData(t),this._items.add(e,t)}, + getData:function(t){for(var e=this._items.length; + e--&&this._items[e].type!==t;);return e>=0?this._items[e]._data:null}, + clearData:function(t){for(var e=this._items.length; + e--&&this._items[e].type!==t;);this._items.remove(e)}, + setDragImage:function(t){}};function w(t,e,n,i){ + for(var o=0;o> 1), + y: rect.top + (rect.height >> 1)}; + var dropPt = {x: dragPt.x + offsetX, y: dragPt.y + offsetY}; + var target = document.elementFromPoint(dropPt.x, dropPt.y); + var dataTransfer = { + dropEffect: '', + effectAllowed: 'all', + files: [], + items: {}, + types: [], + setData: function (format, data) { + this.items[format] = data; + this.types.push(format); + }, + getData: function (format) { + return this.items[format]; + }, + clearData: function (format) { } + }; + var emit = function (event, target, pt) { + var evt = document.createEvent('MouseEvent'); + evt.initMouseEvent(event, true, true, window, 0, 0, 0, pt.x, pt.y, + false, false, false, false, 0, null); + evt.dataTransfer = dataTransfer; + target.dispatchEvent(evt); + }; + emit('mousedown', source, dragPt); + emit('mousemove', source, dragPt); + emit('mousemove', source, dropPt); + emit('mouseup', source, dropPt);""" + script = script_a + script_b + return script + + +def clear_out_console_logs(driver): + with suppress(Exception): + # Clear out the current page log before navigating to a new page + # (To make sure that assert_no_js_errors() uses current results) + driver.get_log("browser") -@decorators.deprecated("Use re.escape() instead, which does what you want!") def _jq_format(code): """ DEPRECATED - Use re.escape() instead, which performs the intended action. @@ -639,8 +1589,8 @@ def _jq_format(code): If you just want to escape quotes, there's escape_quotes_if_needed(). This is similar to "json.dumps(value)", but with one less layer of quotes. """ - code = code.replace('\\', '\\\\').replace('\t', '\\t').replace('\n', '\\n') - code = code.replace('\"', '\\\"').replace('\'', '\\\'') - code = code.replace('\v', '\\v').replace('\a', '\\a').replace('\f', '\\f') - code = code.replace('\b', '\\b').replace(r'\u', '\\u').replace('\r', '\\r') + code = code.replace("\\", "\\\\").replace("\t", "\\t").replace("\n", "\\n") + code = code.replace('"', '\\"').replace("'", "\\'") + code = code.replace("\v", "\\v").replace("\a", "\\a").replace("\f", "\\f") + code = code.replace("\b", "\\b").replace(r"\u", "\\u").replace("\r", "\\r") return code diff --git a/seleniumbase/fixtures/page_actions.py b/seleniumbase/fixtures/page_actions.py old mode 100755 new mode 100644 index ef88230d5a5..49744e8aa3f --- a/seleniumbase/fixtures/page_actions.py +++ b/seleniumbase/fixtures/page_actions.py @@ -1,49 +1,56 @@ -""" -This module contains a set of methods that can be used for page loads and -for waiting for elements to appear on a page. +"""This module contains useful methods for waiting on elements to load. -These methods improve on and expand existing WebDriver commands. +These methods improve and expand on existing WebDriver commands. Improvements include making WebDriver commands more robust and more reliable by giving page elements enough time to load before taking action on them. -The default option for searching for elements is by CSS Selector. -This can be changed by overriding the "By" parameter. +The default option for searching for elements is by "css selector". +This can be changed by overriding the "By" parameter from this import: +> from selenium.webdriver.common.by import By Options are: -By.CSS_SELECTOR -By.CLASS_NAME -By.ID -By.NAME -By.LINK_TEXT -By.XPATH -By.TAG_NAME -By.PARTIAL_LINK_TEXT +By.CSS_SELECTOR # "css selector" +By.CLASS_NAME # "class name" +By.ID # "id" +By.NAME # "name" +By.LINK_TEXT # "link text" +By.XPATH # "xpath" +By.TAG_NAME # "tag name" +By.PARTIAL_LINK_TEXT # "partial link text" """ - -import codecs import os -import sys import time -import traceback -from selenium.webdriver.common.by import By +from contextlib import suppress +from filelock import FileLock +from selenium.common.exceptions import ElementNotInteractableException +from selenium.common.exceptions import ElementNotVisibleException +from selenium.common.exceptions import NoAlertPresentException +from selenium.common.exceptions import NoSuchAttributeException +from selenium.common.exceptions import NoSuchElementException +from selenium.common.exceptions import NoSuchWindowException +from selenium.common.exceptions import StaleElementReferenceException from selenium.webdriver.common.action_chains import ActionChains -from selenium.webdriver.remote.errorhandler import ElementNotVisibleException -from selenium.webdriver.remote.errorhandler import NoSuchElementException -from selenium.webdriver.remote.errorhandler import NoAlertPresentException -from selenium.webdriver.remote.errorhandler import NoSuchFrameException -from selenium.webdriver.remote.errorhandler import NoSuchWindowException +from selenium.webdriver.common.keys import Keys +from seleniumbase.common.exceptions import LinkTextNotFoundException +from seleniumbase.common.exceptions import TextNotVisibleException from seleniumbase.config import settings +from seleniumbase.fixtures import constants +from seleniumbase.fixtures import page_utils +from seleniumbase.fixtures import shared_utils -def is_element_present(driver, selector, by=By.CSS_SELECTOR): +def is_element_present(driver, selector, by="css selector"): """ Returns whether the specified element selector is present on the page. @Params driver - the webdriver object (required) - selector - the locator that is used (required) - by - the method to search for the locator (Default: By.CSS_SELECTOR) + selector - the locator for identifying the page element (required) + by - the type of selector being used (Default: "css selector") @Returns Boolean (is element present) """ + if __is_cdp_swap_needed(driver): + return driver.cdp.is_element_present(selector) + selector, by = page_utils.swap_selector_and_by_if_reversed(selector, by) try: driver.find_element(by=by, value=selector) return True @@ -51,16 +58,19 @@ def is_element_present(driver, selector, by=By.CSS_SELECTOR): return False -def is_element_visible(driver, selector, by=By.CSS_SELECTOR): +def is_element_visible(driver, selector, by="css selector"): """ Returns whether the specified element selector is visible on the page. @Params driver - the webdriver object (required) - selector - the locator that is used (required) - by - the method to search for the locator (Default: By.CSS_SELECTOR) + selector - the locator for identifying the page element (required) + by - the type of selector being used (Default: "css selector") @Returns Boolean (is element visible) """ + if __is_cdp_swap_needed(driver): + return driver.cdp.is_element_visible(selector) + selector, by = page_utils.swap_selector_and_by_if_reversed(selector, by) try: element = driver.find_element(by=by, value=selector) return element.is_displayed() @@ -68,40 +78,207 @@ def is_element_visible(driver, selector, by=By.CSS_SELECTOR): return False -def is_text_visible(driver, text, selector, by=By.CSS_SELECTOR): +def is_element_clickable(driver, selector, by="css selector"): """ - Returns whether the specified text is visible in the specified selector. + Returns whether the specified element selector is clickable. @Params driver - the webdriver object (required) - text - the text string to search for - selector - the locator that is used (required) - by - the method to search for the locator (Default: By.CSS_SELECTOR) + selector - the locator for identifying the page element (required) + by - the type of selector being used (Default: "css selector") + @Returns + Boolean (is element clickable) + """ + _reconnect_if_disconnected(driver) + try: + element = driver.find_element(by=by, value=selector) + if element.is_displayed() and element.is_enabled(): + return True + return False + except Exception: + return False + + +def is_element_enabled(driver, selector, by="css selector"): + """ + Returns whether the specified element selector is enabled on the page. + @Params + driver - the webdriver object (required) + selector - the locator for identifying the page element (required) + by - the type of selector being used (Default: "css selector") + @Returns + Boolean (is element enabled) + """ + _reconnect_if_disconnected(driver) + try: + element = driver.find_element(by=by, value=selector) + return element.is_enabled() + except Exception: + return False + + +def is_text_visible(driver, text, selector="html", by="css selector"): + """ + Returns whether the text substring is visible in the given selector. + @Params + driver - the webdriver object (required) + text - the text string to search for (required) + selector - the locator for identifying the page element + by - the type of selector being used (Default: "css selector") + @Returns + Boolean (is text visible) + """ + _reconnect_if_disconnected(driver) + selector, by = page_utils.swap_selector_and_by_if_reversed(selector, by) + text = str(text) + try: + element = driver.find_element(by=by, value=selector) + element_text = element.text + if shared_utils.is_safari(driver): + if element.tag_name.lower() in ["input", "textarea"]: + element_text = element.get_attribute("value") + else: + element_text = element.get_attribute("innerText") + elif element.tag_name.lower() in ["input", "textarea"]: + element_text = element.get_property("value") + return element.is_displayed() and text in element_text + except Exception: + return False + + +def is_exact_text_visible(driver, text, selector, by="css selector"): + """ + Returns whether the exact text is visible in the given selector. + (Ignores leading and trailing whitespace) + @Params + driver - the webdriver object (required) + text - the text string to search for (required) + selector - the locator for identifying the page element (required) + by - the type of selector being used (Default: "css selector") @Returns Boolean (is text visible) """ + _reconnect_if_disconnected(driver) + selector, by = page_utils.swap_selector_and_by_if_reversed(selector, by) + text = str(text) try: element = driver.find_element(by=by, value=selector) - return element.is_displayed() and text in element.text + element_text = element.text + if shared_utils.is_safari(driver): + if element.tag_name.lower() in ["input", "textarea"]: + element_text = element.get_attribute("value") + else: + element_text = element.get_attribute("innerText") + elif element.tag_name.lower() in ["input", "textarea"]: + element_text = element.get_property("value") + return ( + element.is_displayed() + and text.strip() == element_text.strip() + ) except Exception: return False -def hover_on_element(driver, selector, by=By.CSS_SELECTOR): +def is_attribute_present( + driver, selector, attribute, value=None, by="css selector" +): + """ + Returns whether the specified attribute is present in the given selector. + @Params + driver - the webdriver object (required) + selector - the locator for identifying the page element (required) + attribute - the attribute that is expected for the element (required) + value - the attribute value that is expected (Default: None) + by - the type of selector being used (Default: "css selector") + @Returns + Boolean (is attribute present) + """ + if __is_cdp_swap_needed(driver): + return driver.cdp.is_attribute_present( + selector, attribute, value=value + ) + _reconnect_if_disconnected(driver) + try: + element = driver.find_element(by=by, value=selector) + 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 is_non_empty_text_visible(driver, selector, by="css selector"): + """ + Returns whether the element has any text visible for the given selector. + @Params + driver - the webdriver object (required) + selector - the locator for identifying the page element (required) + by - the type of selector being used (Default: "css selector") + @Returns + Boolean (is any text visible in the element with the selector) + """ + _reconnect_if_disconnected(driver) + try: + element = driver.find_element(by=by, value=selector) + element_text = element.text + if shared_utils.is_safari(driver): + if element.tag_name.lower() in ["input", "textarea"]: + element_text = element.get_attribute("value") + else: + element_text = element.get_attribute("innerText") + elif element.tag_name.lower() in ["input", "textarea"]: + element_text = element.get_property("value") + element_text = element_text.strip() + return element.is_displayed() and len(element_text) > 0 + except Exception: + return False + + +def hover_on_element(driver, selector, by="css selector"): """ Fires the hover event for the specified element by the given selector. @Params driver - the webdriver object (required) - selector - the locator (css selector) that is used (required) - by - the method to search for the locator (Default: By.CSS_SELECTOR) + selector - the locator for identifying the page element (required) + by - the type of selector being used (Default: "css selector") """ + _reconnect_if_disconnected(driver) element = driver.find_element(by=by, value=selector) hover = ActionChains(driver).move_to_element(element) hover.perform() + return element + + +def hover_element(driver, element): + """ + Similar to hover_on_element(), but uses found element, not a selector. + """ + _reconnect_if_disconnected(driver) + hover = ActionChains(driver).move_to_element(element) + hover.perform() + return element -def hover_and_click(driver, hover_selector, click_selector, - hover_by=By.CSS_SELECTOR, click_by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT): +def timeout_exception(exception, message): + exc, msg = shared_utils.format_exc(exception, message) + raise exc(msg) + + +def hover_and_click( + driver, + hover_selector, + click_selector, + hover_by="css selector", + click_by="css selector", + timeout=settings.SMALL_TIMEOUT, + js_click=False, +): """ Fires the hover event for a specified element by a given selector, then clicks on another element specified. Useful for dropdown hover based menus. @@ -109,42 +286,58 @@ def hover_and_click(driver, hover_selector, click_selector, driver - the webdriver object (required) hover_selector - the css selector to hover over (required) click_selector - the css selector to click on (required) - hover_by - the method to search by (Default: By.CSS_SELECTOR) - click_by - the method to search by (Default: By.CSS_SELECTOR) + hover_by - the hover selector type to search by (Default: "css selector") + click_by - the click selector type to search by (Default: "css selector") timeout - number of seconds to wait for click element to appear after hover + js_click - the option to use js_click() instead of click() on the last part """ + _reconnect_if_disconnected(driver) start_ms = time.time() * 1000.0 stop_ms = start_ms + (timeout * 1000.0) element = driver.find_element(by=hover_by, value=hover_selector) hover = ActionChains(driver).move_to_element(element) - hover.perform() for x in range(int(timeout * 10)): try: + hover.perform() element = driver.find_element(by=click_by, value=click_selector) - element.click() + if js_click: + driver.execute_script("arguments[0].click();", element) + else: + element.click() return element except Exception: now_ms = time.time() * 1000.0 if now_ms >= stop_ms: break time.sleep(0.1) - raise NoSuchElementException( - "Element {%s} was not present after %s seconds!" % - (click_selector, timeout)) + plural = "s" + if timeout == 1: + plural = "" + message = "Element {%s} was not present after %s second%s!" % ( + click_selector, + timeout, + plural, + ) + timeout_exception(NoSuchElementException, message) -def hover_element_and_click(driver, element, click_selector, - click_by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT): +def hover_element_and_click( + driver, + element, + click_selector, + click_by="css selector", + timeout=settings.SMALL_TIMEOUT, +): """ Similar to hover_and_click(), but assumes top element is already found. """ + _reconnect_if_disconnected(driver) start_ms = time.time() * 1000.0 stop_ms = start_ms + (timeout * 1000.0) hover = ActionChains(driver).move_to_element(element) - hover.perform() for x in range(int(timeout * 10)): try: + hover.perform() element = driver.find_element(by=click_by, value=click_selector) element.click() return element @@ -153,31 +346,83 @@ def hover_element_and_click(driver, element, click_selector, if now_ms >= stop_ms: break time.sleep(0.1) - raise NoSuchElementException( - "Element {%s} was not present after %s seconds!" % - (click_selector, timeout)) + plural = "s" + if timeout == 1: + plural = "" + message = "Element {%s} was not present after %s second%s!" % ( + click_selector, + timeout, + plural, + ) + timeout_exception(NoSuchElementException, message) + + +def hover_element_and_double_click( + driver, + element, + click_selector, + click_by="css selector", + timeout=settings.SMALL_TIMEOUT, +): + _reconnect_if_disconnected(driver) + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (timeout * 1000.0) + hover = ActionChains(driver).move_to_element(element) + for x in range(int(timeout * 10)): + try: + hover.perform() + element_2 = driver.find_element(by=click_by, value=click_selector) + actions = ActionChains(driver) + actions.move_to_element(element_2) + actions.double_click(element_2) + actions.perform() + return element_2 + except Exception: + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + time.sleep(0.1) + plural = "s" + if timeout == 1: + plural = "" + message = "Element {%s} was not present after %s second%s!" % ( + click_selector, + timeout, + plural, + ) + timeout_exception(NoSuchElementException, message) -def wait_for_element_present(driver, selector, by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT): +def wait_for_element_present( + driver, + selector, + by="css selector", + timeout=settings.LARGE_TIMEOUT, + original_selector=None, + ignore_test_time_limit=False, +): """ Searches for the specified element by the given selector. Returns the - element object if the element is present on the page. The element can be - invisible. Raises an exception if the element does not appear in the - specified timeout. + element object if it exists in the HTML. (The element can be invisible.) + Raises NoSuchElementException if the element does not exist in the HTML + within the specified timeout. @Params driver - the webdriver object - selector - the locator that is used (required) - by - the method to search for the locator (Default: By.CSS_SELECTOR) + selector - the locator for identifying the page element (required) + by - the type of selector being used (Default: "css selector") timeout - the time to wait for elements in seconds + original_selector - handle pre-converted ":contains(TEXT)" selector + ignore_test_time_limit - ignore test time limit (NOT related to timeout) @Returns A web element object """ - + _reconnect_if_disconnected(driver) element = None start_ms = time.time() * 1000.0 stop_ms = start_ms + (timeout * 1000.0) for x in range(int(timeout * 10)): + if not ignore_test_time_limit: + shared_utils.check_if_time_limit_exceeded() try: element = driver.find_element(by=by, value=selector) return element @@ -186,35 +431,62 @@ def wait_for_element_present(driver, selector, by=By.CSS_SELECTOR, if now_ms >= stop_ms: break time.sleep(0.1) + plural = "s" + if timeout == 1: + plural = "" if not element: - raise NoSuchElementException( - "Element {%s} was not present after %s seconds!" % ( - selector, timeout)) + if ( + original_selector + and ":contains(" in original_selector + and "contains(." in selector + ): + selector = original_selector + message = "Element {%s} was not present after %s second%s!" % ( + selector, + timeout, + plural, + ) + timeout_exception(NoSuchElementException, message) + else: + return element -def wait_for_element_visible(driver, selector, by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT): +def wait_for_element_visible( + driver, + selector, + by="css selector", + timeout=settings.LARGE_TIMEOUT, + original_selector=None, + ignore_test_time_limit=False, +): """ Searches for the specified element by the given selector. Returns the element object if the element is present and visible on the page. - Raises an exception if the element does not appear in the - specified timeout. + Raises NoSuchElementException if the element does not exist in the HTML + within the specified timeout. + Raises ElementNotVisibleException if the element exists in the HTML, + but is not visible (eg. opacity is "0") within the specified timeout. @Params driver - the webdriver object (required) - selector - the locator that is used (required) - by - the method to search for the locator (Default: By.CSS_SELECTOR) + selector - the locator for identifying the page element (required) + by - the type of selector being used (Default: "css selector") timeout - the time to wait for elements in seconds - + original_selector - handle pre-converted ":contains(TEXT)" selector + ignore_test_time_limit - ignore test time limit (NOT related to timeout) @Returns A web element object """ - + _reconnect_if_disconnected(driver) element = None + is_present = False start_ms = time.time() * 1000.0 stop_ms = start_ms + (timeout * 1000.0) for x in range(int(timeout * 10)): + if not ignore_test_time_limit: + shared_utils.check_if_time_limit_exceeded() try: element = driver.find_element(by=by, value=selector) + is_present = True if element.is_displayed(): return element else: @@ -228,44 +500,114 @@ def wait_for_element_visible(driver, selector, by=By.CSS_SELECTOR, plural = "s" if timeout == 1: plural = "" - if not element and by != By.LINK_TEXT: - raise ElementNotVisibleException( - "Element {%s} was not visible after %s second%s!" % ( - selector, timeout, plural)) - if not element and by == By.LINK_TEXT: - raise ElementNotVisibleException( - "Link text {%s} was not visible after %s second%s!" % ( - selector, timeout, plural)) + if not element and by != "link text": + if ( + original_selector + and ":contains(" in original_selector + and "contains(." in selector + ): + selector = original_selector + if not is_present: + # The element does not exist in the HTML + message = "Element {%s} was not present after %s second%s!" % ( + selector, + timeout, + plural, + ) + timeout_exception(NoSuchElementException, message) + # The element exists in the HTML, but is not visible + message = "Element {%s} was not visible after %s second%s!" % ( + selector, + timeout, + plural, + ) + timeout_exception(ElementNotVisibleException, message) + elif not element and by == "link text": + message = "Link text {%s} was not found after %s second%s!" % ( + selector, + timeout, + plural, + ) + timeout_exception(LinkTextNotFoundException, message) + else: + return element -def wait_for_text_visible(driver, text, selector, by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT): +def wait_for_text_visible( + driver, + text, + selector, + by="css selector", + timeout=settings.LARGE_TIMEOUT, +): """ Searches for the specified element by the given selector. Returns the element object if the text is present in the element and visible - on the page. Raises an exception if the text or element do not appear - in the specified timeout. + on the page. + Raises NoSuchElementException if the element does not exist in the HTML + within the specified timeout. + Raises ElementNotVisibleException if the element exists in the HTML, + but the text is not visible within the specified timeout. @Params driver - the webdriver object (required) text - the text that is being searched for in the element (required) - selector - the locator that is used (required) - by - the method to search for the locator (Default: By.CSS_SELECTOR) + selector - the locator for identifying the page element (required) + by - the type of selector being used (Default: "css selector") timeout - the time to wait for elements in seconds + browser - used to handle a special edge case when using Safari @Returns A web element object that contains the text searched for """ - + _reconnect_if_disconnected(driver) element = None + is_present = False + full_text = None + text = str(text) start_ms = time.time() * 1000.0 stop_ms = start_ms + (timeout * 1000.0) for x in range(int(timeout * 10)): + shared_utils.check_if_time_limit_exceeded() + full_text = None try: element = driver.find_element(by=by, value=selector) - if element.is_displayed() and text in element.text: - return element + is_present = True + if ( + element.tag_name.lower() in ["input", "textarea"] + and not shared_utils.is_safari(driver) + ): + if ( + element.is_displayed() + and text in element.get_property("value") + ): + return element + else: + if element.is_displayed(): + full_text = element.get_property("value").strip() + element = None + raise Exception() + elif shared_utils.is_safari(driver): + text_attr = "innerText" + if element.tag_name.lower() in ["input", "textarea"]: + text_attr = "value" + if ( + element.is_displayed() + and text in element.get_attribute(text_attr) + ): + return element + else: + if element.is_displayed(): + full_text = element.get_attribute(text_attr) + full_text = full_text.strip() + element = None + raise Exception() else: - element = None - raise Exception() + if element.is_displayed() and text in element.text: + return element + else: + if element.is_displayed(): + full_text = element.text.strip() + element = None + raise Exception() except Exception: now_ms = time.time() * 1000.0 if now_ms >= stop_ms: @@ -275,40 +617,346 @@ def wait_for_text_visible(driver, text, selector, by=By.CSS_SELECTOR, if timeout == 1: plural = "" if not element: - raise ElementNotVisibleException( - "Expected text {%s} for {%s} was not visible after %s second%s!" % - (text, selector, timeout, plural)) + if not is_present: + # The element does not exist in the HTML + message = "Element {%s} was not present after %s second%s!" % ( + selector, + timeout, + plural, + ) + timeout_exception(NoSuchElementException, message) + # The element exists in the HTML, but the text is not visible + message = None + if not full_text or len(str(full_text.replace("\n", ""))) > 320: + message = ( + "Expected text substring {%s} for {%s} was not visible " + "after %s second%s!" % (text, selector, timeout, plural) + ) + else: + full_text = full_text.replace("\n", "\\n ") + message = ( + "Expected text substring {%s} for {%s} was not visible " + "after %s second%s!\n (Actual string found was {%s})" + % (text, selector, timeout, plural, full_text) + ) + timeout_exception(TextNotVisibleException, message) + else: + return element -def wait_for_exact_text_visible(driver, text, selector, by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT): +def wait_for_exact_text_visible( + driver, + text, + selector, + by="css selector", + timeout=settings.LARGE_TIMEOUT, +): """ Searches for the specified element by the given selector. Returns the element object if the text matches exactly with the text in the element, and the text is visible. - Raises an exception if the text or element do not appear - in the specified timeout. + Raises NoSuchElementException if the element does not exist in the HTML + within the specified timeout. + Raises ElementNotVisibleException if the element exists in the HTML, + but the exact text is not visible within the specified timeout. @Params driver - the webdriver object (required) text - the exact text that is expected for the element (required) - selector - the locator that is used (required) - by - the method to search for the locator (Default: By.CSS_SELECTOR) + selector - the locator for identifying the page element (required) + by - the type of selector being used (Default: "css selector") timeout - the time to wait for elements in seconds + browser - used to handle a special edge case when using Safari @Returns A web element object that contains the text searched for """ + _reconnect_if_disconnected(driver) + element = None + is_present = False + actual_text = None + text = str(text) + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (timeout * 1000.0) + for x in range(int(timeout * 10)): + shared_utils.check_if_time_limit_exceeded() + actual_text = None + try: + element = driver.find_element(by=by, value=selector) + is_present = True + if element.tag_name.lower() in ["input", "textarea"]: + if ( + element.is_displayed() + and text.strip() == element.get_property("value").strip() + ): + return element + else: + if element.is_displayed(): + actual_text = element.get_property("value").strip() + element = None + raise Exception() + elif shared_utils.is_safari(driver): + text_attr = "innerText" + if element.tag_name.lower() in ["input", "textarea"]: + text_attr = "value" + if element.is_displayed() and ( + text.strip() == element.get_attribute(text_attr).strip() + ): + return element + else: + if element.is_displayed(): + actual_text = element.get_attribute(text_attr) + actual_text = actual_text.strip() + element = None + raise Exception() + else: + if ( + element.is_displayed() + and text.strip() == element.text.strip() + ): + return element + else: + if element.is_displayed(): + actual_text = element.text.strip() + element = None + raise Exception() + except Exception: + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + time.sleep(0.1) + plural = "s" + if timeout == 1: + plural = "" + if not element: + if not is_present: + # The element does not exist in the HTML + message = "Element {%s} was not present after %s second%s!" % ( + selector, + timeout, + plural, + ) + timeout_exception(NoSuchElementException, message) + # The element exists in the HTML, but the exact text is not visible + message = None + if not actual_text or len(str(actual_text)) > 120: + message = ( + "Expected exact text {%s} for {%s} was not visible " + "after %s second%s!" % (text, selector, timeout, plural) + ) + else: + actual_text = actual_text.replace("\n", "\\n") + message = ( + "Expected exact text {%s} for {%s} was not visible " + "after %s second%s!\n (Actual text was {%s})" + % (text, selector, timeout, plural, actual_text) + ) + timeout_exception(TextNotVisibleException, message) + else: + return element + +def wait_for_any_of_elements_visible( + driver, + selectors, + timeout=settings.LARGE_TIMEOUT, + original_selectors=[], + ignore_test_time_limit=False, +): + """ + Waits for at least one of the elements in the selector list to be visible. + Returns the first element that is found. + Raises NoSuchElementException if none of the elements exist in the HTML + within the specified timeout. + Raises ElementNotVisibleException if the element exists in the HTML, + but is not visible (eg. opacity is "0") within the specified timeout. + @Params + driver - the webdriver object (required) + selectors - the list of selectors for identifying page elements (required) + timeout - the time to wait for elements in seconds + original_selectors - handle pre-converted ":contains(TEXT)" selectors + ignore_test_time_limit - ignore test time limit (NOT related to timeout) + @Returns + A web element object + """ + if not isinstance(selectors, (list, tuple)): + raise Exception("`selectors` must be a list or tuple!") + if not selectors: + raise Exception("`selectors` cannot be an empty list!") + _reconnect_if_disconnected(driver) element = None + any_present = False start_ms = time.time() * 1000.0 stop_ms = start_ms + (timeout * 1000.0) for x in range(int(timeout * 10)): + if not ignore_test_time_limit: + shared_utils.check_if_time_limit_exceeded() + try: + for selector in selectors: + by = "css selector" + if page_utils.is_xpath_selector(selector): + by = "xpath" + try: + element = driver.find_element(by=by, value=selector) + any_present = True + if element.is_displayed(): + return element + element = None + except Exception: + pass + raise Exception("Nothing found yet!") + except Exception: + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + time.sleep(0.1) + plural = "s" + if timeout == 1: + plural = "" + if original_selectors: + selectors = original_selectors + if not element: + if not any_present: + # None of the elements exist in the HTML + message = ( + "None of the elements {%s} were present after %s second%s!" % ( + str(selectors), + timeout, + plural, + ) + ) + timeout_exception(NoSuchElementException, message) + # At least one element exists in the HTML, but none are visible + message = "None of the elements %s were visible after %s second%s!" % ( + str(selectors), + timeout, + plural, + ) + timeout_exception(ElementNotVisibleException, message) + else: + return element + + +def wait_for_any_of_elements_present( + driver, + selectors, + timeout=settings.LARGE_TIMEOUT, + original_selectors=[], + ignore_test_time_limit=False, +): + """ + Waits for at least one of the elements in the selector list to be present. + Visibility not required. (Eg. hidden in the HTML) + Returns the first element that is found. + Raises NoSuchElementException if none of the elements exist in the HTML + within the specified timeout. + @Params + driver - the webdriver object (required) + selectors - the list of selectors for identifying page elements (required) + timeout - the time to wait for elements in seconds + original_selectors - handle pre-converted ":contains(TEXT)" selectors + ignore_test_time_limit - ignore test time limit (NOT related to timeout) + @Returns + A web element object + """ + if not isinstance(selectors, (list, tuple)): + raise Exception("`selectors` must be a list or tuple!") + if not selectors: + raise Exception("`selectors` cannot be an empty list!") + _reconnect_if_disconnected(driver) + element = None + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (timeout * 1000.0) + for x in range(int(timeout * 10)): + if not ignore_test_time_limit: + shared_utils.check_if_time_limit_exceeded() + try: + for selector in selectors: + by = "css selector" + if page_utils.is_xpath_selector(selector): + by = "xpath" + try: + element = driver.find_element(by=by, value=selector) + return element + except Exception: + pass + raise Exception("Nothing found yet!") + except Exception: + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + time.sleep(0.1) + plural = "s" + if timeout == 1: + plural = "" + if original_selectors: + selectors = original_selectors + if not element: + # None of the elements exist in the HTML + message = ( + "None of the elements %s were present after %s second%s!" % ( + str(selectors), + timeout, + plural, + ) + ) + timeout_exception(NoSuchElementException, message) + else: + return element + + +def wait_for_attribute( + driver, + selector, + attribute, + value=None, + by="css selector", + timeout=settings.LARGE_TIMEOUT, +): + """ + Searches for the specified element attribute by the given selector. + Returns the element object if the expected attribute is present + and the expected attribute value is present (if specified). + Raises NoSuchElementException if the element does not exist in the HTML + within the specified timeout. + Raises NoSuchAttributeException if the element exists in the HTML, + but the expected attribute/value is not present within the timeout. + @Params + driver - the webdriver object (required) + selector - the locator for identifying the page element (required) + attribute - the attribute that is expected for the element (required) + value - the attribute value that is expected (Default: None) + by - the type of selector being used (Default: "css selector") + timeout - the time to wait for the element attribute in seconds + @Returns + A web element object that contains the expected attribute/value + """ + _reconnect_if_disconnected(driver) + element = None + element_present = False + attribute_present = False + found_value = None + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (timeout * 1000.0) + for x in range(int(timeout * 10)): + shared_utils.check_if_time_limit_exceeded() try: element = driver.find_element(by=by, value=selector) - if element.is_displayed() and text.strip() == element.text.strip(): - return element + element_present = True + attribute_present = False + found_value = element.get_attribute(attribute) + if found_value is not None: + attribute_present = True else: element = None raise Exception() + + if value is not None: + if found_value == value: + return element + else: + element = None + raise Exception() + else: + return element except Exception: now_ms = time.time() * 1000.0 if now_ms >= stop_ms: @@ -318,27 +966,164 @@ def wait_for_exact_text_visible(driver, text, selector, by=By.CSS_SELECTOR, if timeout == 1: plural = "" if not element: - raise ElementNotVisibleException( - "Expected exact text {%s} for {%s} was not visible " - "after %s second%s!" % (text, selector, timeout, plural)) + if not element_present: + # The element does not exist in the HTML + message = "Element {%s} was not present after %s second%s!" % ( + selector, + timeout, + plural, + ) + timeout_exception(NoSuchElementException, message) + if not attribute_present: + # The element does not have the attribute + message = ( + "Expected attribute {%s} of element {%s} was not present " + "after %s second%s!" % (attribute, selector, timeout, plural) + ) + timeout_exception(NoSuchAttributeException, message) + # The element attribute exists, but the expected value does not match + message = ( + "Expected value {%s} for attribute {%s} of element {%s} was not " + "present after %s second%s! (The actual value was {%s})" + % (value, attribute, selector, timeout, plural, found_value) + ) + timeout_exception(NoSuchAttributeException, message) + else: + return element -def wait_for_element_absent(driver, selector, by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT): +def wait_for_element_clickable( + driver, + selector, + by="css selector", + timeout=settings.LARGE_TIMEOUT, + original_selector=None, +): + """ + Searches for the specified element by the given selector. Returns the + element object if the element is present, visible, & clickable on the page. + Raises NoSuchElementException if the element does not exist in the HTML + within the specified timeout. + Raises ElementNotVisibleException if the element exists in the HTML, + but is not visible (eg. opacity is "0") within the specified timeout. + Raises ElementNotInteractableException if the element is not clickable. + @Params + driver - the webdriver object (required) + selector - the locator for identifying the page element (required) + by - the type of selector being used (Default: "css selector") + timeout - the time to wait for elements in seconds + original_selector - handle pre-converted ":contains(TEXT)" selector + @Returns + A web element object + """ + _reconnect_if_disconnected(driver) + element = None + is_present = False + is_visible = False + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (timeout * 1000.0) + for x in range(int(timeout * 10)): + shared_utils.check_if_time_limit_exceeded() + try: + element = driver.find_element(by=by, value=selector) + is_present = True + if element.is_displayed(): + is_visible = True + if element.is_enabled(): + return element + else: + element = None + raise Exception() + else: + element = None + raise Exception() + except Exception: + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + time.sleep(0.1) + plural = "s" + if timeout == 1: + plural = "" + if not element and by != "link text": + if ( + original_selector + and ":contains(" in original_selector + and "contains(." in selector + ): + selector = original_selector + if not is_present: + # The element does not exist in the HTML + message = "Element {%s} was not present after %s second%s!" % ( + selector, + timeout, + plural, + ) + timeout_exception(NoSuchElementException, message) + if not is_visible: + # The element exists in the HTML, but is not visible + message = "Element {%s} was not visible after %s second%s!" % ( + selector, + timeout, + plural, + ) + timeout_exception(ElementNotVisibleException, message) + # The element is visible in the HTML, but is not clickable + message = "Element {%s} was not clickable after %s second%s!" % ( + selector, + timeout, + plural, + ) + timeout_exception(ElementNotInteractableException, message) + elif not element and by == "link text" and not is_visible: + message = "Link text {%s} was not found after %s second%s!" % ( + selector, + timeout, + plural, + ) + timeout_exception(LinkTextNotFoundException, message) + elif not element and by == "link text" and is_visible: + message = "Link text {%s} was not clickable after %s second%s!" % ( + selector, + timeout, + plural, + ) + timeout_exception(ElementNotInteractableException, message) + else: + return element + + +def wait_for_element_absent( + driver, + selector, + by="css selector", + timeout=settings.LARGE_TIMEOUT, + original_selector=None, +): """ Searches for the specified element by the given selector. Raises an exception if the element is still present after the specified timeout. @Params driver - the webdriver object - selector - the locator that is used (required) - by - the method to search for the locator (Default: By.CSS_SELECTOR) + selector - the locator for identifying the page element (required) + by - the type of selector being used (Default: "css selector") timeout - the time to wait for elements in seconds + original_selector - handle pre-converted ":contains(TEXT)" selector """ - + if __is_cdp_swap_needed(driver): + if page_utils.is_valid_by(by): + original_selector = selector + elif page_utils.is_valid_by(selector): + original_selector = by + selector, by = page_utils.recalculate_selector(original_selector, by) + driver.cdp.wait_for_element_absent(selector) + return True + _reconnect_if_disconnected(driver) start_ms = time.time() * 1000.0 stop_ms = start_ms + (timeout * 1000.0) for x in range(int(timeout * 10)): + shared_utils.check_if_time_limit_exceeded() try: driver.find_element(by=by, value=selector) now_ms = time.time() * 1000.0 @@ -350,26 +1135,51 @@ def wait_for_element_absent(driver, selector, by=By.CSS_SELECTOR, plural = "s" if timeout == 1: plural = "" - raise Exception("Element {%s} was still present after %s second%s!" % - (selector, timeout, plural)) + if ( + original_selector + and ":contains(" in original_selector + and "contains(." in selector + ): + selector = original_selector + message = "Element {%s} was still present after %s second%s!" % ( + selector, + timeout, + plural, + ) + timeout_exception(Exception, message) -def wait_for_element_not_visible(driver, selector, by=By.CSS_SELECTOR, - timeout=settings.LARGE_TIMEOUT): +def wait_for_element_not_visible( + driver, + selector, + by="css selector", + timeout=settings.LARGE_TIMEOUT, + original_selector=None, +): """ Searches for the specified element by the given selector. Raises an exception if the element is still visible after the specified timeout. @Params driver - the webdriver object (required) - selector - the locator that is used (required) - by - the method to search for the locator (Default: By.CSS_SELECTOR) + selector - the locator for identifying the page element (required) + by - the type of selector being used (Default: "css selector") timeout - the time to wait for the element in seconds + original_selector - handle pre-converted ":contains(TEXT)" selector """ - + if __is_cdp_swap_needed(driver): + if page_utils.is_valid_by(by): + original_selector = selector + elif page_utils.is_valid_by(selector): + original_selector = by + selector, by = page_utils.recalculate_selector(original_selector, by) + driver.cdp.wait_for_element_not_visible(selector) + return True + _reconnect_if_disconnected(driver) start_ms = time.time() * 1000.0 stop_ms = start_ms + (timeout * 1000.0) for x in range(int(timeout * 10)): + shared_utils.check_if_time_limit_exceeded() try: element = driver.find_element(by=by, value=selector) if element.is_displayed(): @@ -384,86 +1194,345 @@ def wait_for_element_not_visible(driver, selector, by=By.CSS_SELECTOR, plural = "s" if timeout == 1: plural = "" - raise Exception( - "Element {%s} was still visible after %s second%s!" % ( - selector, timeout, plural)) + if ( + original_selector + and ":contains(" in original_selector + and "contains(." in selector + ): + selector = original_selector + message = "Element {%s} was still visible after %s second%s!" % ( + selector, + timeout, + plural, + ) + timeout_exception(Exception, message) + + +def wait_for_text_not_visible( + driver, + text, + selector="html", + by="css selector", + timeout=settings.LARGE_TIMEOUT, +): + """ + Searches for the text in the element of the given selector on the page. + Returns True if the text is not visible on the page within the timeout. + Raises an exception if the text is still present after the timeout. + @Params + driver - the webdriver object (required) + text - the text that is being searched for in the element (required) + selector - the locator for identifying the page element (required) + by - the type of selector being used (Default: "css selector") + timeout - the time to wait for elements in seconds + @Returns + A web element object that contains the text searched for + """ + _reconnect_if_disconnected(driver) + text = str(text) + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (timeout * 1000.0) + for x in range(int(timeout * 10)): + shared_utils.check_if_time_limit_exceeded() + if not is_text_visible(driver, text, selector, by=by): + return True + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + time.sleep(0.1) + plural = "s" + if timeout == 1: + plural = "" + message = "Text {%s} in {%s} was still visible after %s second%s!" % ( + text, + selector, + timeout, + plural, + ) + timeout_exception(Exception, message) -def find_visible_elements(driver, selector, by=By.CSS_SELECTOR): +def wait_for_exact_text_not_visible( + driver, + text, + selector="html", + by="css selector", + timeout=settings.LARGE_TIMEOUT, +): + """ + Searches for the text in the element of the given selector on the page. + Returns True if the element is missing the exact text within the timeout. + Raises an exception if the exact text is still present after the timeout. + @Params + driver - the webdriver object (required) + text - the text that is being searched for in the element (required) + selector - the locator for identifying the page element (required) + by - the type of selector being used (Default: "css selector") + timeout - the time to wait for elements in seconds + @Returns + A web element object that contains the text searched for + """ + _reconnect_if_disconnected(driver) + text = str(text) + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (timeout * 1000.0) + for x in range(int(timeout * 10)): + shared_utils.check_if_time_limit_exceeded() + if not is_exact_text_visible(driver, text, selector, by=by): + return True + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + time.sleep(0.1) + plural = "s" + if timeout == 1: + plural = "" + message = ( + "Exact text {%s} for {%s} was still visible after %s second%s!" % ( + text, + selector, + timeout, + plural, + ) + ) + timeout_exception(Exception, message) + + +def wait_for_non_empty_text_visible( + driver, selector, by="css selector", timeout=settings.LARGE_TIMEOUT, +): + """ + Searches for any text in the element of the given selector. + Returns the element if it has visible text within the timeout. + Raises an exception if the element has no text within the timeout. + Whitespace-only text is considered empty text. + @Params + driver - the webdriver object (required) + text - the text that is being searched for in the element (required) + selector - the locator for identifying the page element (required) + by - the type of selector being used (Default: "css selector") + timeout - the time to wait for elements in seconds + @Returns + The web element object that has text + """ + _reconnect_if_disconnected(driver) + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (timeout * 1000.0) + element = None + visible = None + for x in range(int(timeout * 10)): + shared_utils.check_if_time_limit_exceeded() + try: + element = None + visible = False + element = driver.find_element(by=by, value=selector) + if element.is_displayed(): + visible = True + element_text = element.text + if shared_utils.is_safari(driver): + if element.tag_name.lower() in ["input", "textarea"]: + element_text = element.get_attribute("value") + else: + element_text = element.get_attribute("innerText") + elif element.tag_name.lower() in ["input", "textarea"]: + element_text = element.get_property("value") + element_text = element_text.strip() + if element.is_displayed() and len(element_text) > 0: + return element + except Exception: + element = None + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + time.sleep(0.1) + plural = "s" + if timeout == 1: + plural = "" + if not element: + # The element does not exist in the HTML + message = "Element {%s} was not present after %s second%s!" % ( + selector, + timeout, + plural, + ) + timeout_exception(NoSuchElementException, message) + elif not visible: + # The element exists in the HTML, but is not visible + message = "Element {%s} was not visible after %s second%s!" % ( + selector, + timeout, + plural, + ) + timeout_exception(ElementNotVisibleException, message) + else: + # The element exists in the HTML, but has no visible text + message = ( + "Element {%s} has no visible text after %s second%s!" + "" % (selector, timeout, plural) + ) + timeout_exception(TextNotVisibleException, message) + + +def wait_for_attribute_not_present( + driver, + selector, + attribute, + value=None, + by="css selector", + timeout=settings.LARGE_TIMEOUT, +): + """ + Searches for the specified element attribute by the given selector. + Returns True if the attribute isn't present on the page within the timeout. + Also returns True if the element is not present within the timeout. + Raises an exception if the attribute is still present after the timeout. + @Params + driver - the webdriver object (required) + selector - the locator for identifying the page element (required) + attribute - the element attribute (required) + value - the attribute value (Default: None) + by - the type of selector being used (Default: "css selector") + timeout - the time to wait for the element attribute in seconds + """ + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (timeout * 1000.0) + for x in range(int(timeout * 10)): + shared_utils.check_if_time_limit_exceeded() + if not is_attribute_present( + driver, selector, attribute, value=value, by=by + ): + return True + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + time.sleep(0.1) + plural = "s" + if timeout == 1: + plural = "" + message = ( + "Attribute {%s} of element {%s} was still present after %s second%s!" + "" % (attribute, selector, timeout, plural) + ) + if value: + message = ( + "Value {%s} for attribute {%s} of element {%s} was still present " + "after %s second%s!" + "" % (value, attribute, selector, timeout, plural) + ) + timeout_exception(Exception, message) + + +def find_visible_elements(driver, selector, by="css selector", limit=0): """ Finds all WebElements that match a selector and are visible. - Similar to webdriver.find_elements. + Similar to webdriver.find_elements(). + If "limit" is set and > 0, will only return that many elements. @Params driver - the webdriver object (required) - selector - the locator that is used to search the DOM (required) - by - the method to search for the locator (Default: By.CSS_SELECTOR) + selector - the locator for identifying the page element (required) + by - the type of selector being used (Default: "css selector") + limit - the maximum number of elements to return if > 0. """ + _reconnect_if_disconnected(driver) elements = driver.find_elements(by=by, value=selector) - return [element for element in elements if element.is_displayed()] + if limit and limit > 0 and len(elements) > limit: + elements = elements[:limit] + try: + v_elems = [element for element in elements if element.is_displayed()] + return v_elems + except (StaleElementReferenceException, ElementNotInteractableException): + time.sleep(0.1) + elements = driver.find_elements(by=by, value=selector) + extra_elements = [] + if limit and limit > 0 and len(elements) > limit: + elements = elements[:limit] + extra_elements = elements[limit:] + v_elems = [] + for element in elements: + if element.is_displayed(): + v_elems.append(element) + if extra_elements and limit and len(v_elems) < limit: + for element in extra_elements: + if element.is_displayed(): + v_elems.append(element) + if len(v_elems) >= limit: + break + return v_elems -def save_screenshot(driver, name, folder=None): +def save_screenshot( + driver, name, folder=None, selector=None, by="css selector" +): """ - Saves a screenshot to the current directory (or to a subfolder if provided) + Saves a screenshot of the current page. + If no folder is specified, uses the folder where pytest was called. + The screenshot will include the entire page unless a selector is given. + If a provided selector is not found, then takes a full-page screenshot. If the folder provided doesn't exist, it will get created. - The screenshot will be in PNG format. + The screenshot will be in PNG format: (*.png) """ - if "." not in name: + _reconnect_if_disconnected(driver) + if not name.endswith(".png"): name = name + ".png" if folder: - abs_path = os.path.abspath('.') - file_path = abs_path + "/%s" % folder + abs_path = os.path.abspath(".") + file_path = os.path.join(abs_path, folder) if not os.path.exists(file_path): os.makedirs(file_path) - screenshot_path = "%s/%s" % (file_path, name) + screenshot_path = os.path.join(file_path, name) else: screenshot_path = name - try: - element = driver.find_element_by_tag_name('body') - element_png = element.screenshot_as_png - with open(screenshot_path, "wb") as file: - file.write(element_png) - except Exception: + if selector: + try: + element = driver.find_element(by=by, value=selector) + element_png = element.screenshot_as_png + with open(screenshot_path, "wb") as file: + file.write(element_png) + except Exception: + if driver: + driver.get_screenshot_as_file(screenshot_path) + else: + pass + else: if driver: driver.get_screenshot_as_file(screenshot_path) else: pass -def _get_last_page(driver): - try: - last_page = driver.current_url - except Exception: - last_page = '[WARNING! Browser Not Open!]' - if len(last_page) < 5: - last_page = '[WARNING! Browser Not Open!]' - return last_page - - -def save_test_failure_data(driver, name, browser_type, folder=None): +def save_page_source(driver, name, folder=None): """ - Saves failure data to the current directory (or to a subfolder if provided) - If the folder provided doesn't exist, it will get created. + Saves the page HTML to the current directory (or given subfolder). + If the folder specified doesn't exist, it will get created. + @Params + name - The file name to save the current page's HTML to. + folder - The folder to save the file to. (Default = current folder) """ + from seleniumbase.core import log_helper + + if not __is_cdp_swap_needed(driver): + _reconnect_if_disconnected(driver) # If disconnected without CDP + if not name.endswith(".html"): + name = name + ".html" if folder: - abs_path = os.path.abspath('.') - file_path = abs_path + "/%s" % folder + abs_path = os.path.abspath(".") + file_path = os.path.join(abs_path, folder) if not os.path.exists(file_path): os.makedirs(file_path) - failure_data_file_path = "%s/%s" % (file_path, name) + html_file_path = os.path.join(file_path, name) else: - failure_data_file_path = name - failure_data_file = codecs.open(failure_data_file_path, "w+", "utf-8") - last_page = _get_last_page(driver) - data_to_save = [] - data_to_save.append("Last_Page: %s" % last_page) - data_to_save.append("Browser: %s " % browser_type) - data_to_save.append("Traceback: " + ''.join( - traceback.format_exception(sys.exc_info()[0], - sys.exc_info()[1], - sys.exc_info()[2]))) - failure_data_file.writelines("\r\n".join(data_to_save)) - failure_data_file.close() + html_file_path = name + page_source = None + if __is_cdp_swap_needed(driver): + page_source = driver.cdp.get_page_source() + else: + page_source = driver.page_source + rendered_source = log_helper.get_html_source_with_base_href( + driver, page_source + ) + html_file = open(html_file_path, mode="w+", encoding="utf-8") + html_file.write(rendered_source) + html_file.close() def wait_for_and_accept_alert(driver, timeout=settings.LARGE_TIMEOUT): @@ -473,6 +1542,7 @@ def wait_for_and_accept_alert(driver, timeout=settings.LARGE_TIMEOUT): driver - the webdriver object (required) timeout - the time to wait for the alert in seconds """ + _reconnect_if_disconnected(driver) alert = wait_for_and_switch_to_alert(driver, timeout) alert_text = alert.text alert.accept() @@ -486,6 +1556,7 @@ def wait_for_and_dismiss_alert(driver, timeout=settings.LARGE_TIMEOUT): driver - the webdriver object (required) timeout - the time to wait for the alert in seconds """ + _reconnect_if_disconnected(driver) alert = wait_for_and_switch_to_alert(driver, timeout) alert_text = alert.text alert.dismiss() @@ -501,10 +1572,11 @@ def wait_for_and_switch_to_alert(driver, timeout=settings.LARGE_TIMEOUT): driver - the webdriver object (required) timeout - the time to wait for the alert in seconds """ - + _reconnect_if_disconnected(driver) start_ms = time.time() * 1000.0 stop_ms = start_ms + (timeout * 1000.0) for x in range(int(timeout * 10)): + shared_utils.check_if_time_limit_exceeded() try: alert = driver.switch_to.alert # Raises exception if no alert present @@ -515,34 +1587,80 @@ def wait_for_and_switch_to_alert(driver, timeout=settings.LARGE_TIMEOUT): if now_ms >= stop_ms: break time.sleep(0.1) - raise Exception("Alert was not present after %s seconds!" % timeout) + message = "Alert was not present after %s seconds!" % timeout + timeout_exception(Exception, message) -def switch_to_frame(driver, frame, timeout=settings.SMALL_TIMEOUT): +def switch_to_frame( + driver, frame, timeout=settings.SMALL_TIMEOUT, invisible=False +): """ - Wait for an iframe to appear, and switch to it. This should be usable - as a drop-in replacement for driver.switch_to.frame(). + Wait for an iframe to appear, and switch to it. This should be + usable as a drop-in replacement for driver.switch_to.frame(). @Params driver - the webdriver object (required) - frame - the frame element, name, or index + frame - the frame element, name, id, index, or selector timeout - the time to wait for the alert in seconds + invisible - if True, the iframe can be invisible """ - + _reconnect_if_disconnected(driver) start_ms = time.time() * 1000.0 stop_ms = start_ms + (timeout * 1000.0) for x in range(int(timeout * 10)): + shared_utils.check_if_time_limit_exceeded() try: driver.switch_to.frame(frame) return True - except NoSuchFrameException: + except Exception: + if isinstance(frame, str): + by = None + if page_utils.is_xpath_selector(frame): + by = "xpath" + else: + by = "css selector" + if ( + is_element_visible(driver, frame, by=by) + or (invisible and is_element_present(driver, frame, by=by)) + ): + with suppress(Exception): + element = driver.find_element(by=by, value=frame) + driver.switch_to.frame(element) + return True now_ms = time.time() * 1000.0 if now_ms >= stop_ms: break time.sleep(0.1) - raise Exception("Frame was not present after %s seconds!" % timeout) + plural = "s" + if timeout == 1: + plural = "" + presence = "visible" + if invisible: + presence = "present" + message = "Frame {%s} was not %s after %s second%s!" % ( + frame, + presence, + timeout, + plural, + ) + timeout_exception(Exception, message) + + +def __switch_to_window(driver, window_handle, uc_lock=True): + if getattr(driver, "_is_using_uc", None) and uc_lock: + gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK) + with gui_lock: + driver.switch_to.window(window_handle) + else: + driver.switch_to.window(window_handle) + return True -def switch_to_window(driver, window, timeout=settings.SMALL_TIMEOUT): +def switch_to_window( + driver, + window, + timeout=settings.SMALL_TIMEOUT, + uc_lock=True, +): """ Wait for a window to appear, and switch to it. This should be usable as a drop-in replacement for driver.switch_to.window(). @@ -550,31 +1668,494 @@ def switch_to_window(driver, window, timeout=settings.SMALL_TIMEOUT): driver - the webdriver object (required) window - the window index or window handle timeout - the time to wait for the window in seconds + uc_lock - if UC Mode and True, switch_to_window() uses thread-locking """ - + _reconnect_if_disconnected(driver) + if window == -1: + window = len(driver.window_handles) - 1 start_ms = time.time() * 1000.0 stop_ms = start_ms + (timeout * 1000.0) if isinstance(window, int): + if shared_utils.is_safari(driver): + # Reversed window_handles on Safari + window = len(driver.window_handles) - 1 - window + if window < 0: + window = 0 for x in range(int(timeout * 10)): + shared_utils.check_if_time_limit_exceeded() try: window_handle = driver.window_handles[window] - driver.switch_to.window(window_handle) + __switch_to_window(driver, window_handle, uc_lock=uc_lock) return True except IndexError: now_ms = time.time() * 1000.0 if now_ms >= stop_ms: break time.sleep(0.1) - raise Exception("Window was not present after %s seconds!" % timeout) + plural = "s" + if timeout == 1: + plural = "" + message = "Window {%s} was not present after %s second%s!" % ( + window, + timeout, + plural, + ) + timeout_exception(Exception, message) else: window_handle = window for x in range(int(timeout * 10)): + shared_utils.check_if_time_limit_exceeded() try: - driver.switch_to.window(window_handle) + __switch_to_window(driver, window_handle, uc_lock=uc_lock) return True except NoSuchWindowException: now_ms = time.time() * 1000.0 if now_ms >= stop_ms: break time.sleep(0.1) - raise Exception("Window was not present after %s seconds!" % timeout) + plural = "s" + if timeout == 1: + plural = "" + message = "Window {%s} was not present after %s second%s!" % ( + window, + timeout, + plural, + ) + timeout_exception(Exception, message) + + +############ + +# Special methods for use with UC Mode + +def _reconnect_if_disconnected(driver): + if ( + getattr(driver, "_is_using_uc", None) + and hasattr(driver, "is_connected") + and not driver.is_connected() + ): + with suppress(Exception): + driver.connect() + + +def __is_cdp_swap_needed(driver): + """If the driver is disconnected, use a CDP method when available.""" + return shared_utils.is_cdp_swap_needed(driver) + + +############ + +# Support methods for direct use from driver + +def open_url(driver, url): + if __is_cdp_swap_needed(driver): + driver.cdp.open(url) + return + elif ( + getattr(driver, "_is_using_uc", None) + # and getattr(driver, "_is_using_auth", None) + and not getattr(driver, "_is_using_cdp", None) + ): + # Auth in UC Mode requires CDP Mode + # (and now we're always forcing it) + driver.uc_activate_cdp_mode(url) + return + elif ( + getattr(driver, "_is_using_uc", None) + and getattr(driver, "_is_using_cdp", None) + ): + driver.disconnect() + driver.cdp.open(url) + return + url = str(url).strip() # Remove leading and trailing whitespace + if not page_utils.looks_like_a_page_url(url): + if page_utils.is_valid_url("https://" + url): + url = "https://" + url + driver.get(url) + + +def click(driver, selector, by="css selector", timeout=settings.SMALL_TIMEOUT): + selector, by = page_utils.recalculate_selector(selector, by) + if __is_cdp_swap_needed(driver): + driver.cdp.click(selector) + return + element = wait_for_element_clickable( + driver, selector, by=by, timeout=timeout + ) + element.click() + + +def click_link(driver, link_text, timeout=settings.SMALL_TIMEOUT): + if __is_cdp_swap_needed(driver): + driver.cdp.click_link(link_text) + return + element = wait_for_element_clickable( + driver, link_text, by="link text", timeout=timeout + ) + element.click() + + +def click_if_visible( + driver, selector, by="css selector", timeout=0 +): + selector, by = page_utils.recalculate_selector(selector, by) + if __is_cdp_swap_needed(driver): + driver.cdp.click_if_visible(selector) + return + if is_element_visible(driver, selector, by=by): + click(driver, selector, by=by, timeout=1) + elif timeout > 0: + with suppress(Exception): + wait_for_element_visible( + driver, selector, by=by, timeout=timeout + ) + if is_element_visible(driver, selector, by=by): + click(driver, selector, by=by, timeout=1) + + +def click_active_element(driver): + if __is_cdp_swap_needed(driver): + driver.cdp.click_active_element() + return + driver.execute_script("document.activeElement.click();") + + +def js_click( + driver, selector, by="css selector", timeout=settings.SMALL_TIMEOUT +): + selector, by = page_utils.recalculate_selector(selector, by) + if __is_cdp_swap_needed(driver): + driver.cdp.click(selector) + return + element = wait_for_element_present( + driver, selector, by=by, timeout=timeout + ) + if not element.is_displayed() or not element.is_enabled(): + time.sleep(0.2) # If not clickable, wait a bit longer before clicking + element = wait_for_element_present(driver, selector, by=by, timeout=1) + script = ( + """var simulateClick = function (elem) { + var evt = new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window + }); + var canceled = !elem.dispatchEvent(evt); + }; + var someLink = arguments[0]; + simulateClick(someLink);""" + ) + driver.execute_script(script, element) + + +def send_keys( + driver, selector, text, by="css selector", timeout=settings.LARGE_TIMEOUT +): + selector, by = page_utils.recalculate_selector(selector, by) + if __is_cdp_swap_needed(driver): + driver.cdp.send_keys(selector, text) + return + element = wait_for_element_present( + driver, selector, by=by, timeout=timeout + ) + if not text.endswith("\n"): + element.send_keys(text) + else: + element.send_keys(text[:-1]) + element.submit() + + +def press_keys( + driver, selector, text, by="css selector", timeout=settings.LARGE_TIMEOUT +): + selector, by = page_utils.recalculate_selector(selector, by) + if __is_cdp_swap_needed(driver): + driver.cdp.press_keys(selector, text) + return + element = wait_for_element_present( + driver, selector, by=by, timeout=timeout + ) + if not text.endswith("\n"): + for key in text: + element.send_keys(key) + else: + for key in text[:-1]: + element.send_keys(key) + element.send_keys(Keys.RETURN) + + +def update_text( + driver, selector, text, by="css selector", timeout=settings.LARGE_TIMEOUT +): + selector, by = page_utils.recalculate_selector(selector, by) + if __is_cdp_swap_needed(driver): + driver.cdp.type(selector, text) + return + element = wait_for_element_clickable( + driver, selector, by=by, timeout=timeout + ) + element.clear() + if not text.endswith("\n"): + element.send_keys(text) + else: + element.send_keys(text[:-1]) + element.submit() + + +def submit(driver, selector, by="css selector"): + selector, by = page_utils.recalculate_selector(selector, by) + if __is_cdp_swap_needed(driver): + driver.cdp.send_keys(selector, "\r\n") + return + element = wait_for_element_clickable( + driver, selector, by=by, timeout=settings.SMALL_TIMEOUT + ) + element.submit() + + +def has_attribute( + driver, selector, attribute, value=None, by="css selector" +): + selector, by = page_utils.recalculate_selector(selector, by) + return is_attribute_present( + driver, selector, attribute, value=value, by=by + ) + + +def assert_element_visible( + driver, selector, by="css selector", timeout=settings.SMALL_TIMEOUT +): + original_selector = None + if page_utils.is_valid_by(by): + original_selector = selector + elif page_utils.is_valid_by(selector): + original_selector = by + selector, by = page_utils.recalculate_selector(selector, by) + if __is_cdp_swap_needed(driver): + driver.cdp.assert_element(selector) + return True + wait_for_element_visible( + driver, + selector, + by=by, + timeout=timeout, + original_selector=original_selector, + ) + + +def assert_element_present( + driver, selector, by="css selector", timeout=settings.SMALL_TIMEOUT +): + original_selector = None + if page_utils.is_valid_by(by): + original_selector = selector + elif page_utils.is_valid_by(selector): + original_selector = by + selector, by = page_utils.recalculate_selector(selector, by) + if __is_cdp_swap_needed(driver): + driver.cdp.assert_element_present(selector) + return True + wait_for_element_present( + driver, + selector, + by=by, + timeout=timeout, + original_selector=original_selector, + ) + + +def assert_element_not_visible( + driver, selector, by="css selector", timeout=settings.SMALL_TIMEOUT +): + original_selector = None + if page_utils.is_valid_by(by): + original_selector = selector + elif page_utils.is_valid_by(selector): + original_selector = by + selector, by = page_utils.recalculate_selector(selector, by) + wait_for_element_not_visible( + driver, + selector, + by=by, + timeout=timeout, + original_selector=original_selector, + ) + + +def assert_text( + driver, + text, + selector="html", + by="css selector", + timeout=settings.SMALL_TIMEOUT, +): + selector, by = page_utils.recalculate_selector(selector, by) + if __is_cdp_swap_needed(driver): + driver.cdp.assert_text(text, selector) + return True + wait_for_text_visible( + driver, text.strip(), selector, by=by, timeout=timeout + ) + + +def assert_exact_text( + driver, + text, + selector="html", + by="css selector", + timeout=settings.SMALL_TIMEOUT, +): + selector, by = page_utils.recalculate_selector(selector, by) + if __is_cdp_swap_needed(driver): + driver.cdp.assert_exact_text(text, selector) + return True + wait_for_exact_text_visible( + driver, text.strip(), selector, by=by, timeout=timeout + ) + + +def assert_non_empty_text( + driver, + selector, + by="css selector", + timeout=settings.SMALL_TIMEOUT, +): + selector, by = page_utils.recalculate_selector(selector, by) + wait_for_non_empty_text_visible( + driver, selector, by=by, timeout=timeout + ) + + +def assert_text_not_visible( + driver, + text, + selector="html", + by="css selector", + timeout=settings.SMALL_TIMEOUT, +): + selector, by = page_utils.recalculate_selector(selector, by) + wait_for_text_not_visible( + driver, text.strip(), selector, by=by, timeout=timeout + ) + + +def wait_for_element( + driver, + selector, + by="css selector", + timeout=settings.LARGE_TIMEOUT, +): + original_selector = None + if page_utils.is_valid_by(by): + original_selector = selector + elif page_utils.is_valid_by(selector): + original_selector = by + selector, by = page_utils.recalculate_selector(selector, by) + if __is_cdp_swap_needed(driver): + return driver.cdp.select(selector) + return wait_for_element_visible( + driver=driver, + selector=selector, + by=by, + timeout=timeout, + original_selector=original_selector, + ) + + +def wait_for_selector( + driver, + selector, + by="css selector", + timeout=settings.LARGE_TIMEOUT, +): + original_selector = None + if page_utils.is_valid_by(by): + original_selector = selector + elif page_utils.is_valid_by(selector): + original_selector = by + selector, by = page_utils.recalculate_selector(selector, by) + if __is_cdp_swap_needed(driver): + return driver.cdp.select(selector) + return wait_for_element_present( + driver=driver, + selector=selector, + by=by, + timeout=timeout, + original_selector=original_selector, + ) + + +def wait_for_text( + driver, + text, + selector, + by="css selector", + timeout=settings.LARGE_TIMEOUT, +): + selector, by = page_utils.recalculate_selector(selector, by) + if __is_cdp_swap_needed(driver): + return driver.cdp.find_element(selector) + return wait_for_text_visible( + driver=driver, + text=text, + selector=selector, + by=by, + timeout=timeout, + ) + + +def wait_for_exact_text( + driver, + text, + selector, + by="css selector", + timeout=settings.LARGE_TIMEOUT, +): + selector, by = page_utils.recalculate_selector(selector, by) + return wait_for_exact_text_visible( + driver=driver, + text=text, + selector=selector, + by=by, + timeout=timeout, + ) + + +def wait_for_non_empty_text( + driver, + selector, + by="css selector", + timeout=settings.LARGE_TIMEOUT, +): + selector, by = page_utils.recalculate_selector(selector, by) + return wait_for_non_empty_text_visible( + driver=driver, + selector=selector, + by=by, + timeout=timeout, + ) + + +def get_text( + driver, + selector="body", + by="css selector", + timeout=settings.LARGE_TIMEOUT +): + if __is_cdp_swap_needed(driver): + return driver.cdp.get_text(selector) + element = wait_for_element( + driver=driver, + selector=selector, + by=by, + timeout=timeout, + ) + element_text = element.text + if shared_utils.is_safari(driver): + if element.tag_name.lower() in ["input", "textarea"]: + element_text = element.get_attribute("value") + else: + element_text = element.get_attribute("innerText") + elif element.tag_name.lower() in ["input", "textarea"]: + element_text = element.get_property("value") + return element_text diff --git a/seleniumbase/fixtures/page_utils.py b/seleniumbase/fixtures/page_utils.py old mode 100755 new mode 100644 index 257ef666485..55fc005c8af --- a/seleniumbase/fixtures/page_utils.py +++ b/seleniumbase/fixtures/page_utils.py @@ -1,9 +1,11 @@ -""" -This module contains useful utility methods. -""" -import codecs +"""This module contains useful utility methods""" +import fasteners +import os import re import requests +from selenium.webdriver.common.by import By +from seleniumbase.fixtures import constants +from seleniumbase.fixtures import css_to_xpath def get_domain_url(url): @@ -13,97 +15,204 @@ def get_domain_url(url): Into this: https://blog.xkcd.com """ - if "http://" not in url and "https://" not in url: + if not url.startswith(("http://", "https://")): return url - url_header = url.split('://')[0] - simple_url = url.split('://')[1] - base_url = simple_url.split('/')[0] - domain_url = url_header + '://' + base_url + url_header = url.split("://")[0] + simple_url = url.split("://")[1] + base_url = simple_url.split("/")[0] + domain_url = url_header + "://" + base_url return domain_url +def is_valid_by(by): + return by in [ + "css selector", "class name", "id", "name", + "link text", "xpath", "tag name", "partial link text", + ] + + +def swap_selector_and_by_if_reversed(selector, by): + if not is_valid_by(by) and is_valid_by(selector): + selector, by = by, selector + return (selector, by) + + def is_xpath_selector(selector): - """ - A basic method to determine if a selector is an xpath selector. - """ - if (selector.startswith('/') or selector.startswith('./') or ( - selector.startswith('('))): - return True - return False + """Determine if a selector is an xpath selector.""" + return selector.startswith(("/", "./", "(")) def is_link_text_selector(selector): - """ - A basic method to determine if a selector is a link text selector. - """ - if (selector.startswith('link=') or selector.startswith('link_text=')): - return True - return False + """Determine if a selector is a link text selector.""" + return selector.startswith(("link=", "link_text=", "text=")) + + +def is_partial_link_text_selector(selector): + """Determine if a selector is a partial link text selector.""" + return selector.startswith(( + "partial_link=", "partial_link_text=", "partial_text=", + "p_link=", "p_link_text=", "p_text=" + )) + + +def is_name_selector(selector): + """Determine if a selector is a name selector.""" + return selector.startswith(("name=", "&")) + + +def recalculate_selector(selector, by, xp_ok=True): + """Use autodetection to return the correct selector with "by" updated. + If "xp_ok" is False, don't call convert_css_to_xpath(), which is + used to make the ":contains()" selector valid outside of JS calls. + Returns a (selector, by) tuple.""" + _type = type(selector) + if _type is not str: + msg = "Expecting a selector of type: \"\" (string)!" + raise Exception('Invalid selector type: "%s"\n%s' % (_type, msg)) + _by_type = type(by) + if _by_type is not str: + msg = "Expecting a `by` of type: \"\" (string)!" + raise Exception('Invalid `by` type: "%s"\n%s' % (_by_type, msg)) + if not is_valid_by(by) and is_valid_by(selector): + selector, by = swap_selector_and_by_if_reversed(selector, by) + if is_xpath_selector(selector): + by = By.XPATH + if is_link_text_selector(selector): + selector = get_link_text_from_selector(selector) + by = By.LINK_TEXT + if is_partial_link_text_selector(selector): + selector = get_partial_link_text_from_selector(selector) + by = By.PARTIAL_LINK_TEXT + if is_name_selector(selector): + name = get_name_from_selector(selector) + selector = '[name="%s"]' % name + by = By.CSS_SELECTOR + if xp_ok and ":contains(" in selector and by == By.CSS_SELECTOR: + selector = css_to_xpath.convert_css_to_xpath(selector) + by = By.XPATH + if by == "": + by = By.CSS_SELECTOR + if not is_valid_by(by): + valid_by_options = [ + "css selector", "link text", "partial link text", + "name", "xpath", "id", "tag name", "class name", + ] + msg = "Choose a `by` from: %s." % valid_by_options + raise Exception('Invalid `by`: "%s"\n%s' % (by, msg)) + return (selector, by) + + +def looks_like_a_page_url(url): + """Returns True if the url parameter looks like a URL. This method + is slightly more lenient than page_utils.is_valid_url(url) due to + possible typos when calling self.get(url), which will try to + navigate to the page if a URL is detected, but will instead call + self.get_element(URL_AS_A_SELECTOR) if the input is not a URL.""" + return url.startswith(( + "http:", "https:", "://", "about:", "blob:", "chrome:", "opera:", + "data:", "edge:", "file:", "view-source:", "chrome-search:", + "chrome-extension:", "chrome-untrusted:", "isolated-app:", + "chrome-devtools:", "devtools:", "brave:", "comet:" + )) def get_link_text_from_selector(selector): - """ - A basic method to get the link text from a link text selector. - """ - if selector.startswith('link='): - return selector.split('link=')[1] - elif selector.startswith('link_text='): - return selector.split('link_text=')[1] + """Get the link text from a link text selector.""" + if selector.startswith("link="): + return selector[len("link="):] + elif selector.startswith("link_text="): + return selector[len("link_text="):] + elif selector.startswith("text="): + return selector[len("text="):] + return selector + + +def get_partial_link_text_from_selector(selector): + """Get the partial link text from a partial link selector.""" + if selector.startswith("partial_link="): + return selector[len("partial_link="):] + elif selector.startswith("partial_link_text="): + return selector[len("partial_link_text="):] + elif selector.startswith("partial_text="): + return selector[len("partial_text="):] + elif selector.startswith("p_link="): + return selector[len("p_link="):] + elif selector.startswith("p_link_text="): + return selector[len("p_link_text="):] + elif selector.startswith("p_text="): + return selector[len("p_text="):] + return selector + + +def get_name_from_selector(selector): + """Get the name from a name selector.""" + if selector.startswith("name="): + return selector[len("name="):] + if selector.startswith("&"): + return selector[len("&"):] return selector def is_valid_url(url): regex = re.compile( - r'^(?:http)s?://' # http:// or https:// - r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+' - r'(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... - r'localhost|' # localhost... - r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip - r'(?::\d+)?' # optional port - r'(?:/?|[/?]\S+)$', re.IGNORECASE) - if regex.match(url) or url == 'about:blank' or url == 'data:,': + r"^(?:http)s?://" # http:// or https:// + r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+" + r"(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" # domain... + r"localhost|" # localhost... + r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # ...or ip + r"(?::\d+)?" # optional port + r"(?:/?|[/?]\S+)$", + re.IGNORECASE, + ) + if ( + regex.match(url) + or url.startswith(( + "about:", "blob:", "chrome:", "data:", "edge:", "file:" + )) + ): return True else: return False def _get_unique_links(page_url, soup): - """ - Returns all unique links. + """Returns all unique links. Includes: "a"->"href", "img"->"src", "link"->"href", and "script"->"src" links. """ - if "http://" not in page_url and "https://" not in page_url: + if not page_url.startswith("http://") and not page_url.startswith( + "https://" + ): return [] - prefix = 'http:' - if page_url.startswith('https:'): - prefix = 'https:' - simple_url = page_url.split('://')[1] - base_url = simple_url.split('/')[0] + prefix = "http:" + if page_url.startswith("https:"): + prefix = "https:" + simple_url = page_url.split("://")[1] + base_url = simple_url.split("/")[0] full_base_url = prefix + "//" + base_url raw_links = [] raw_unique_links = [] # Get "href" from all "a" tags - links = soup.find_all('a') + links = soup.find_all("a") for link in links: - raw_links.append(link.get('href')) + raw_links.append(link.get("href")) # Get "src" from all "img" tags - img_links = soup.find_all('img') + img_links = soup.find_all("img") for img_link in img_links: - raw_links.append(img_link.get('src')) + raw_links.append(img_link.get("src")) # Get "href" from all "link" tags - links = soup.find_all('link') + links = soup.find_all("link") for link in links: - raw_links.append(link.get('href')) + raw_links.append(link.get("href")) # Get "src" from all "script" tags - img_links = soup.find_all('script') + img_links = soup.find_all("script") for img_link in img_links: - raw_links.append(img_link.get('src')) + raw_links.append(img_link.get("src")) for link in raw_links: if link not in raw_unique_links: @@ -112,33 +221,60 @@ def _get_unique_links(page_url, soup): unique_links = [] for link in raw_unique_links: if link and len(link) > 1: - if link.startswith('//'): + if link.startswith("//"): link = prefix + link - elif link.startswith('/'): + elif link.startswith("/"): link = full_base_url + link - elif link.startswith('./'): - link = full_base_url + link[1:] - elif link.startswith('#'): + elif link == "./": + link = page_url + elif link.startswith("./"): + f_b_url = full_base_url + if len(simple_url.split("/")) > 1: + f_b_url = full_base_url + "/" + simple_url.split("/")[1] + link = f_b_url + link[1:] + elif link.startswith("../"): + if page_url.endswith("/"): + link = page_url + link + else: + link = page_url + "/" + link + elif link.startswith("#"): link = full_base_url + link - elif '//' not in link: - link = full_base_url + "/" + link + elif "//" not in link: + f_b_url = full_base_url + if len(simple_url.split("/")) > 1: + f_b_url = full_base_url + "/" + simple_url.split("/")[1] + link = f_b_url + "/" + link + elif link.startswith('"') and link.endswith('"') and len(link) > 4: + link = link[1:-1] else: pass unique_links.append(link) - return unique_links + links = unique_links + links = list(set(links)) # Make sure all duplicates were removed + links = sorted(links) # Sort all the links alphabetically + return links -def _get_link_status_code(link, allow_redirects=False, timeout=5): - """ Get the status code of a link. - If the timeout is exceeded, will return a 404. - For a list of available status codes, see: - https://en.wikipedia.org/wiki/List_of_HTTP_status_codes - """ +def _get_link_status_code( + link, + allow_redirects=False, + timeout=5, + verify=False, +): + """Get the status code of a link. + If the timeout is exceeded, will return a 404. + If "verify" is False, will ignore certificate errors. + For a list of available status codes, see: + https://en.wikipedia.org/wiki/List_of_HTTP_status_codes """ status_code = None try: - response = requests.get( - link, allow_redirects=allow_redirects, timeout=timeout) + response = requests.head( + link, + allow_redirects=allow_redirects, + timeout=timeout, + verify=verify, + ) status_code = response.status_code except Exception: status_code = 404 @@ -146,37 +282,85 @@ def _get_link_status_code(link, allow_redirects=False, timeout=5): def _print_unique_links_with_status_codes(page_url, soup): - """ Finds all unique links in the html of the page source - and then prints out those links with their status codes. - Format: ["link" -> "status_code"] (per line) - Page links include those obtained from: - "a"->"href", "img"->"src", "link"->"href", and "script"->"src". - """ + """Finds all unique links in the html of the page source + and then prints out those links with their status codes. + Format: ["link" -> "status_code"] (per line) + Page links include those obtained from: + "a"->"href", "img"->"src", "link"->"href", and "script"->"src". """ links = _get_unique_links(page_url, soup) for link in links: status_code = _get_link_status_code(link) print(link, " -> ", status_code) -def _download_file_to(file_url, destination_folder, new_file_name=None): +def _download_file_to( + file_url, destination_folder, new_file_name=None, headers=None +): if new_file_name: file_name = new_file_name else: - file_name = file_url.split('/')[-1] - r = requests.get(file_url) - with open(destination_folder + '/' + file_name, "wb") as code: - code.write(r.content) + file_name = file_url.split("/")[-1] + r = requests.get(file_url, headers=headers, timeout=5) + file_path = os.path.join(destination_folder, file_name) + download_file_lock = fasteners.InterProcessLock( + constants.MultiBrowser.DOWNLOAD_FILE_LOCK + ) + with download_file_lock: + with open(file_path, mode="wb") as code: + code.write(r.content) def _save_data_as(data, destination_folder, file_name): - out_file = codecs.open(destination_folder + '/' + file_name, "w+") - out_file.writelines(data) - out_file.close() + file_io_lock = fasteners.InterProcessLock( + constants.MultiBrowser.FILE_IO_LOCK + ) + with file_io_lock: + out_file = open( + os.path.join(destination_folder, file_name), + mode="w+", + encoding="utf-8", + ) + out_file.writelines(data) + out_file.close() + + +def _append_data_to_file(data, destination_folder, file_name): + file_io_lock = fasteners.InterProcessLock( + constants.MultiBrowser.FILE_IO_LOCK + ) + with file_io_lock: + existing_data = "" + if os.path.exists(os.path.join(destination_folder, file_name)): + with open( + os.path.join(destination_folder, file_name), mode="r" + ) as f: + existing_data = f.read() + if not existing_data.split("\n")[-1] == "": + existing_data += "\n" + out_file = open( + os.path.join(destination_folder, file_name), + mode="w+", + encoding="utf-8", + ) + out_file.writelines("%s%s" % (existing_data, data)) + out_file.close() + + +def _get_file_data(folder, file_name): + file_io_lock = fasteners.InterProcessLock( + constants.MultiBrowser.FILE_IO_LOCK + ) + with file_io_lock: + if not os.path.exists(os.path.join(folder, file_name)): + raise Exception("File not found!") + with open(os.path.join(folder, file_name), mode="r") as f: + data = f.read() + return data def make_css_match_first_element_only(selector): # Only get the first match - last_syllable = selector.split(' ')[-1] - if ':' not in last_syllable and ':contains' not in selector: - selector += ':first' + last_syllable = selector.split(" ")[-1] + if ":first" not in last_syllable: + selector += ":first" return selector diff --git a/seleniumbase/fixtures/shared_utils.py b/seleniumbase/fixtures/shared_utils.py new file mode 100644 index 00000000000..9cf42887338 --- /dev/null +++ b/seleniumbase/fixtures/shared_utils.py @@ -0,0 +1,317 @@ +"""Shared utility methods""" +import colorama +import os +import pathlib +import platform +import sys +import time +from contextlib import suppress +from seleniumbase import config as sb_config +from seleniumbase.config import settings +from seleniumbase.fixtures import constants + + +def pip_install(package, version=None): + import fasteners + import subprocess + + pip_install_lock = fasteners.InterProcessLock( + constants.PipInstall.LOCKFILE + ) + upgrade_to_latest = False + if ( + version + and ("U" in str(version).upper() or "L" in str(version).upper()) + ): + # Upgrade to Latest when specified with "U" or "L" + upgrade_to_latest = True + with pip_install_lock: + if not version: + subprocess.check_call( + [sys.executable, "-m", "pip", "install", package] + ) + elif not upgrade_to_latest: + package_and_version = package + "==" + str(version) + subprocess.check_call( + [sys.executable, "-m", "pip", "install", package_and_version] + ) + else: + subprocess.check_call( + [sys.executable, "-m", "pip", "install", "-U", package] + ) + + +def make_version_list(version_str): + return [int(i) for i in version_str.split(".") if i.isdigit()] + + +def make_version_tuple(version_str): + return tuple(make_version_list(version_str)) + + +def get_mfa_code(totp_key=None): + """Returns a time-based one-time password based on the + Google Authenticator algorithm for multi-factor authentication. + If the "totp_key" is not specified, this method defaults + to using the one provided in [seleniumbase/config/settings.py]. + Google Authenticator codes expire & change at 30-sec intervals. + If the fetched password expires in the next 1.2 seconds, waits + for a new one before returning it (may take up to 1.2 seconds). + See https://pyotp.readthedocs.io/en/latest/ for details.""" + import pyotp + + if not totp_key: + totp_key = settings.TOTP_KEY + epoch_interval = time.time() / 30.0 + cycle_lifespan = float(epoch_interval) - int(epoch_interval) + if float(cycle_lifespan) > 0.96: + # Password expires in the next 1.2 seconds. Wait for a new one. + for i in range(30): + time.sleep(0.04) + epoch_interval = time.time() / 30.0 + cycle_lifespan = float(epoch_interval) - int(epoch_interval) + if not float(cycle_lifespan) > 0.96: + # The new password cycle has begun + break + totp = pyotp.TOTP(totp_key) + return str(totp.now()) + + +def is_arm_linux(): + """Returns True if machine is ARM Linux. + This will be useful once Google adds + support for ARM Linux ChromeDriver. + (Raspberry Pi uses ARM architecture.)""" + return ( + platform.system() == "Linux" + and platform.machine() == "aarch64" + ) + + +def is_arm_mac(): + """Returns True if machine is ARM Mac. + (Eg. M1 / M2 Macs use ARM processors)""" + return ( + "darwin" in sys.platform + and ( + "arm" in platform.processor().lower() + or "arm64" in platform.version().lower() + ) + ) + + +def is_mac(): + return "darwin" in sys.platform + + +def is_linux(): + return "linux" in sys.platform + + +def is_windows(): + return "win32" in sys.platform + + +def is_safari(driver): + return driver.capabilities["browserName"].lower() == "safari" + + +def get_terminal_width(): + width = 80 # default + try: + width = os.get_terminal_size().columns + except Exception: + try: + import shutil + + width = shutil.get_terminal_size((80, 20)).columns + except Exception: + pass + return width + + +def fix_colorama_if_windows(): + if is_windows(): + colorama.just_fix_windows_console() + + +def fix_url_as_needed(url): + if not url: + url = "data:," + elif url.startswith("//"): + url = "https:" + url + elif ":" not in url: + url = "https://" + url + return url + + +def reconnect_if_disconnected(driver): + if ( + getattr(driver, "_is_using_uc", None) + and hasattr(driver, "is_connected") + and not driver.is_connected() + ): + with suppress(Exception): + driver.connect() + + +def is_cdp_swap_needed(driver): + """ + When someone is using CDP Mode with a disconnected webdriver, + but they forget to reconnect before calling a webdriver method, + this method is used to substitute the webdriver method for a + CDP Mode method instead, which keeps CDP Stealth Mode enabled. + For other webdriver methods, SeleniumBase will reconnect first. + """ + return ( + hasattr(driver, "is_cdp_mode_active") + and driver.is_cdp_mode_active() + and hasattr(driver, "is_connected") + and not driver.is_connected() + ) + + +def is_chrome_130_or_newer(self, binary_location=None): + from seleniumbase.core import detect_b_ver + + """Due to changes in Chrome-130, UC Mode freezes at start-up + unless the user-data-dir already exists and is populated.""" + with suppress(Exception): + if not binary_location: + ver = detect_b_ver.get_browser_version_from_os("google-chrome") + else: + ver = detect_b_ver.get_browser_version_from_binary( + binary_location + ) + if ver and len(ver) > 3 and int(ver.split(".")[0]) >= 130: + return True + return False + + +def make_dir_files_writable(dir_path): + # Make all files in the given directory writable. + for file_path in pathlib.Path(dir_path).glob("*"): + if file_path.is_file(): + mode = os.stat(file_path).st_mode + mode |= (mode & 0o444) >> 1 # copy R bits to W + with suppress(Exception): + os.chmod(file_path, mode) + + +def make_writable(file_path): + # Set permissions to: "If you can read it, you can write it." + mode = os.stat(file_path).st_mode + mode |= (mode & 0o444) >> 1 # copy R bits to W + os.chmod(file_path, mode) + + +def make_executable(file_path): + # Set permissions to: "If you can read it, you can execute it." + mode = os.stat(file_path).st_mode + mode |= (mode & 0o444) >> 2 # copy R bits to X + os.chmod(file_path, mode) + + +def format_exc(exception, message): + """Formats an exception message to make the output cleaner.""" + from selenium.common.exceptions import ElementNotVisibleException + from selenium.common.exceptions import NoAlertPresentException + from selenium.common.exceptions import NoSuchAttributeException + from selenium.common.exceptions import NoSuchElementException + from selenium.common.exceptions import NoSuchFrameException + from selenium.common.exceptions import NoSuchWindowException + from seleniumbase.common.exceptions import LinkTextNotFoundException + from seleniumbase.common.exceptions import NoSuchFileException + from seleniumbase.common.exceptions import NoSuchOptionException + from seleniumbase.common.exceptions import TextNotVisibleException + from seleniumbase.common import exceptions + + if exception is Exception: + exc = Exception + return exc, message + elif exception is ElementNotVisibleException: + exc = exceptions.ElementNotVisibleException + elif exception == "ElementNotVisibleException": + exc = exceptions.ElementNotVisibleException + elif exception is LinkTextNotFoundException: + exc = exceptions.LinkTextNotFoundException + elif exception == "LinkTextNotFoundException": + exc = exceptions.LinkTextNotFoundException + elif exception is NoSuchElementException: + exc = exceptions.NoSuchElementException + elif exception == "NoSuchElementException": + exc = exceptions.NoSuchElementException + elif exception is TextNotVisibleException: + exc = exceptions.TextNotVisibleException + elif exception == "TextNotVisibleException": + exc = exceptions.TextNotVisibleException + elif exception is NoAlertPresentException: + exc = exceptions.NoAlertPresentException + elif exception == "NoAlertPresentException": + exc = exceptions.NoAlertPresentException + elif exception is NoSuchAttributeException: + exc = exceptions.NoSuchAttributeException + elif exception == "NoSuchAttributeException": + exc = exceptions.NoSuchAttributeException + elif exception is NoSuchFrameException: + exc = exceptions.NoSuchFrameException + elif exception == "NoSuchFrameException": + exc = exceptions.NoSuchFrameException + elif exception is NoSuchWindowException: + exc = exceptions.NoSuchWindowException + elif exception == "NoSuchWindowException": + exc = exceptions.NoSuchWindowException + elif exception is NoSuchFileException: + exc = exceptions.NoSuchFileException + elif exception == "NoSuchFileException": + exc = exceptions.NoSuchFileException + elif exception is NoSuchOptionException: + exc = exceptions.NoSuchOptionException + elif exception == "NoSuchOptionException": + exc = exceptions.NoSuchOptionException + elif isinstance(exception, str): + exc = Exception + message = "%s: %s" % (exception, message) + return exc, message + else: + exc = Exception + return exc, message + message = _format_message(message) + try: + exc.message = message + except Exception: + pass + return exc, message + + +def _format_message(message): + message = "\n " + message + return message + + +def __time_limit_exceeded(message): + from seleniumbase.common.exceptions import TimeLimitExceededException + + raise TimeLimitExceededException(message) + + +def check_if_time_limit_exceeded(): + if ( + getattr(sb_config, "time_limit", None) + and not sb_config.recorder_mode + ): + time_limit = sb_config.time_limit + now_ms = int(time.time() * 1000) + if now_ms > sb_config.start_time_ms + sb_config.time_limit_ms: + display_time_limit = time_limit + plural = "s" + if float(int(time_limit)) == float(time_limit): + display_time_limit = int(time_limit) + if display_time_limit == 1: + plural = "" + message = ( + "This test has exceeded the time limit of %s second%s!" + % (display_time_limit, plural) + ) + message = _format_message(message) + __time_limit_exceeded(message) diff --git a/seleniumbase/fixtures/unittest_helper.py b/seleniumbase/fixtures/unittest_helper.py new file mode 100644 index 00000000000..e672a56d3ec --- /dev/null +++ b/seleniumbase/fixtures/unittest_helper.py @@ -0,0 +1,41 @@ +import sys +from contextlib import contextmanager +from unittest.case import _ShouldStop, SkipTest + + +class _Outcome(object): + def __init__(self, result=None): + self.expecting_failure = False + self.result = result + self.result_supports_subtests = hasattr(result, "addSubTest") + self.success = True + self.skipped = [] + self.expectedFailure = None + self.errors = [] + + @contextmanager + def testPartExecutor(self, test_case, isTest=False): + old_success = self.success + self.success = True + try: + yield + except KeyboardInterrupt: + raise + except SkipTest as e: + self.success = False + self.skipped.append((test_case, str(e))) + except _ShouldStop: + pass + except Exception: + exc_info = sys.exc_info() + if self.expecting_failure: + self.expectedFailure = exc_info + else: + self.success = False + self.errors.append((test_case, exc_info)) + exc_info = None + else: + if self.result_supports_subtests and self.success: + self.errors.append((test_case, None)) + finally: + self.success = self.success and old_success diff --git a/seleniumbase/fixtures/words.py b/seleniumbase/fixtures/words.py new file mode 100644 index 00000000000..f46e6f82ab8 --- /dev/null +++ b/seleniumbase/fixtures/words.py @@ -0,0 +1,185 @@ +"""Small Dictionary for Demo Mode translations""" + + +class SD: + def translate_in(language): + words = {} + words["English"] = "in" + words["Chinese"] = "ๅœจ" + words["Dutch"] = "in" + words["French"] = "dans" + words["Italian"] = "nel" + words["Japanese"] = "ใซ" + words["Korean"] = "์—" + words["Portuguese"] = "no" + words["Russian"] = "ะฒ" + words["Spanish"] = "en" + return words[language] + + def translate_assert(language): + words = {} + words["English"] = "ASSERT" + words["Chinese"] = "ๆ–ญ่จ€" + words["Dutch"] = "CONTROLEREN" + words["French"] = "Vร‰RIFIER" + words["Italian"] = "VERIFICARE" + words["Japanese"] = "ๆคœ่จผ" + words["Korean"] = "ํ™•์ธ" + words["Portuguese"] = "VERIFICAR" + words["Russian"] = "ะŸะžะ”ะขะ’ะ•ะ ะ”ะ˜ะขะฌ" + words["Spanish"] = "VERIFICAR" + return words[language] + + def translate_assert_text(language): + words = {} + words["English"] = "ASSERT TEXT" + words["Chinese"] = "ๆ–ญ่จ€ๆ–‡ๆœฌ" + words["Dutch"] = "CONTROLEREN TEKST" + words["French"] = "Vร‰RIFIER TEXTE" + words["Italian"] = "VERIFICARE TESTO" + words["Japanese"] = "ใƒ†ใ‚ญใ‚นใƒˆใ‚’็ขบ่ชใ™ใ‚‹" + words["Korean"] = "ํ…์ŠคํŠธ ํ™•์ธํ•˜๋Š”" + words["Portuguese"] = "VERIFICAR TEXTO" + words["Russian"] = "ะŸะžะ”ะขะ’ะ•ะ ะ”ะ˜ะขะฌ ะขะ•ะšะกะข" + words["Spanish"] = "VERIFICAR TEXTO" + return words[language] + + def translate_assert_exact_text(language): + words = {} + words["English"] = "ASSERT EXACT TEXT" + words["Chinese"] = "็กฎๅˆ‡ๆ–ญ่จ€ๆ–‡ๆœฌ" + words["Dutch"] = "CONTROLEREN EXACTE TEKST" + words["French"] = "Vร‰RIFIER EXACTEMENT TEXTE" + words["Italian"] = "VERIFICARE TESTO ESATTO" + words["Japanese"] = "ๆญฃ็ขบใชใƒ†ใ‚ญใ‚นใƒˆใ‚’็ขบ่ชใ™ใ‚‹" + words["Korean"] = "์ •ํ™•ํ•œ ํ…์ŠคํŠธ๋ฅผ ํ™•์ธํ•˜๋Š”" + words["Portuguese"] = "VERIFICAR TEXTO EXATO" + words["Russian"] = "ะŸะžะ”ะขะ’ะ•ะ ะ”ะ˜ะขะฌ ะขะ•ะšะกะข ะขะžะงะะž" + words["Spanish"] = "VERIFICAR TEXTO EXACTO" + return words[language] + + def translate_assert_link_text(language): + words = {} + words["English"] = "ASSERT LINK TEXT" + words["Chinese"] = "ๆ–ญ่จ€้“พๆŽฅๆ–‡ๆœฌ" + words["Dutch"] = "CONTROLEREN LINKTEKST" + words["French"] = "Vร‰RIFIER TEXTE DU LIEN" + words["Italian"] = "VERIFICARE TESTO DEL COLLEGAMENTO" + words["Japanese"] = "ใƒชใƒณใ‚ฏใƒ†ใ‚ญใ‚นใƒˆใ‚’็ขบ่ชใ™ใ‚‹" + words["Korean"] = "๋งํฌ ํ…์ŠคํŠธ ํ™•์ธ" + words["Portuguese"] = "VERIFICAR TEXTO DO LINK" + words["Russian"] = "ะŸะžะ”ะขะ’ะ•ะ ะ”ะ˜ะขะฌ ะกะกะซะ›ะšะฃ" + words["Spanish"] = "VERIFICAR TEXTO DEL ENLACE" + return words[language] + + def translate_assert_non_empty_text(language): + words = {} + words["English"] = "ASSERT NON-EMPTY TEXT" + words["Chinese"] = "ๆ–ญ่จ€้ž็ฉบๆ–‡ๆœฌ" + words["Dutch"] = "CONTROLEREN NIET-LEGE TEKST" + words["French"] = "Vร‰RIFIER TEXTE NON VIDE" + words["Italian"] = "VERIFICARE TESTO NON VUOTO" + words["Japanese"] = "็ฉบใงใฏใชใ„ใƒ†ใ‚ญใ‚นใƒˆใ‚’็ขบ่ชใ™ใ‚‹" + words["Korean"] = "๋น„์–ด ์žˆ์ง€ ์•Š์€ ํ…์ŠคํŠธ ํ™•์ธํ•˜๋Š”" + words["Portuguese"] = "VERIFICAR TEXTO NรƒO VAZIO" + words["Russian"] = "ะŸะžะ”ะขะ’ะ•ะ ะ”ะ˜ะขะฌ ะะ•ะŸะฃะกะขะžะ™ ะขะ•ะšะกะข" + words["Spanish"] = "VERIFICAR TEXTO NO VACรO" + return words[language] + + def translate_assert_attribute(language): + words = {} + words["English"] = "ASSERT ATTRIBUTE" + words["Chinese"] = "ๆ–ญ่จ€ๅฑžๆ€ง" + words["Dutch"] = "CONTROLEREN ATTRIBUUT" + words["French"] = "Vร‰RIFIER ATTRIBUT" + words["Italian"] = "VERIFICARE ATTRIBUTO" + words["Japanese"] = "ๅฑžๆ€งใ‚’็ขบ่ชใ™ใ‚‹" + words["Korean"] = "ํŠน์„ฑ ํ™•์ธ" + words["Portuguese"] = "VERIFICAR ATRIBUTO" + words["Russian"] = "ะŸะžะ”ะขะ’ะ•ะ ะ”ะ˜ะขะฌ ะะขะ ะ˜ะ‘ะฃะข" + words["Spanish"] = "VERIFICAR ATRIBUTO" + return words[language] + + def translate_assert_title(language): + words = {} + words["English"] = "ASSERT TITLE" + words["Chinese"] = "ๆ–ญ่จ€ๆ ‡้ข˜" + words["Dutch"] = "CONTROLEREN TITEL" + words["French"] = "Vร‰RIFIER TITRE" + words["Italian"] = "VERIFICARE TITOLO" + words["Japanese"] = "ใ‚ฟใ‚คใƒˆใƒซใ‚’็ขบ่ช" + words["Korean"] = "์ œ๋ชฉ ํ™•์ธ" + words["Portuguese"] = "VERIFICAR TรTULO" + words["Russian"] = "ะŸะžะ”ะขะ’ะ•ะ ะ”ะ˜ะขะฌ ะะะ—ะ’ะะะ˜ะ•" + words["Spanish"] = "VERIFICAR TรTULO" + return words[language] + + def translate_assert_title_contains(language): + words = {} + words["English"] = "ASSERT TITLE CONTAINS" + words["Chinese"] = "ๆ–ญ่จ€ๆ ‡้ข˜ๅŒ…ๅซ" + words["Dutch"] = "CONTROLEREN TITEL BEVAT" + words["French"] = "Vร‰RIFIER TITRE CONTIENT" + words["Italian"] = "VERIFICARE TITOLO CONTIENE" + words["Japanese"] = "ใ‚ฟใ‚คใƒˆใƒซ้ƒจๅˆ†ๆ–‡ๅญ—ๅˆ—ใ‚’็ขบ่ชใ™ใ‚‹" + words["Korean"] = "์ œ๋ชฉ ๋ถ€๋ถ„ ํ™•์ธ" + words["Portuguese"] = "VERIFICAR TรTULO CONTร‰M" + words["Russian"] = "ะŸะžะ”ะขะ’ะ•ะ ะ”ะ˜ะขะฌ ะะะ—ะ’ะะะ˜ะ• ะกะžะ”ะ•ะ ะ–ะ˜ะข" + words["Spanish"] = "VERIFICAR TรTULO CONTIENE" + return words[language] + + def translate_assert_url(language): + words = {} + words["English"] = "ASSERT URL" + words["Chinese"] = "ๆ–ญ่จ€ URL" + words["Dutch"] = "CONTROLEREN URL" + words["French"] = "Vร‰RIFIER URL" + words["Italian"] = "VERIFICARE URL" + words["Japanese"] = "URL ใ‚’็ขบ่ชใ™ใ‚‹" + words["Korean"] = "URL ํ™•์ธ" + words["Portuguese"] = "VERIFICAR URL" + words["Russian"] = "ะŸะžะ”ะขะ’ะ•ะ ะ”ะ˜ะขะฌ URL" + words["Spanish"] = "VERIFICAR URL" + return words[language] + + def translate_assert_url_contains(language): + words = {} + words["English"] = "ASSERT URL CONTAINS" + words["Chinese"] = "ๆ–ญ่จ€ URL ๅŒ…ๅซ" + words["Dutch"] = "CONTROLEREN URL BEVAT" + words["French"] = "Vร‰RIFIER URL CONTIENT" + words["Italian"] = "VERIFICARE URL CONTIENE" + words["Japanese"] = "URL ใ‚’็ขบ่ชใ™ใ‚‹" + words["Korean"] = "URL ํ™•์ธ" + words["Portuguese"] = "VERIFICAR URL CONTร‰M" + words["Russian"] = "ะŸะžะ”ะขะ’ะ•ะ ะ”ะ˜ะขะฌ URL ะกะžะ”ะ•ะ ะ–ะ˜ะข" + words["Spanish"] = "VERIFICAR URL CONTIENE" + return words[language] + + def translate_assert_no_404_errors(language): + words = {} + words["English"] = "ASSERT NO 404 ERRORS" + words["Chinese"] = "ๆฃ€ๆŸฅๆ–ญๅผ€็š„้“พๆŽฅ" + words["Dutch"] = "CONTROLEREN OP 404 FOUTEN" + words["French"] = "AFFIRMEZ PAS D'ERREURS 404" + words["Italian"] = "CONTROLLA ERRORI 404" + words["Japanese"] = "ใƒชใƒณใ‚ฏๅˆ‡ใ‚Œใ‚’็ขบ่ชใ™ใ‚‹" + words["Korean"] = "๋Š์–ด์ง„ ๋งํฌ ํ™•์ธ" + words["Portuguese"] = "VERIFICAR SE Hร ERROS 404" + words["Russian"] = "ะŸะ ะžะ’ะ•ะ ะ˜ะขะฌ ะžะจะ˜ะ‘ะšะ˜ 404" + words["Spanish"] = "VERIFICAR SI HAY ERRORES 404" + return words[language] + + def translate_assert_no_js_errors(language): + words = {} + words["English"] = "ASSERT NO JS ERRORS" + words["Chinese"] = "ๆฃ€ๆŸฅJS้”™่ฏฏ" + words["Dutch"] = "CONTROLEREN OP JS FOUTEN" + words["French"] = "AFFIRMEZ PAS D'ERREURS JS" + words["Italian"] = "CONTROLLA ERRORI JS" + words["Japanese"] = "JSใ‚จใƒฉใƒผใ‚’็ขบ่ชใ™ใ‚‹" + words["Korean"] = "JS ์˜ค๋ฅ˜ ํ™•์ธ" + words["Portuguese"] = "VERIFICAR SE Hร ERROS JS" + words["Russian"] = "ะŸะ ะžะ’ะ•ะ ะ˜ะขะฌ ะžะจะ˜ะ‘ะšะ˜ JS" + words["Spanish"] = "VERIFICAR SI HAY ERRORES JS" + return words[language] diff --git a/seleniumbase/fixtures/xpath_to_css.py b/seleniumbase/fixtures/xpath_to_css.py old mode 100755 new mode 100644 index ce58deabbd7..618adfdb157 --- a/seleniumbase/fixtures/xpath_to_css.py +++ b/seleniumbase/fixtures/xpath_to_css.py @@ -1,13 +1,10 @@ -""" -Convert XPath selectors into CSS selectors -""" - +"""Convert XPath selectors into CSS selectors""" import re _sub_regexes = { - "tag": r"([a-zA-Z][a-zA-Z0-9]{0,10}|\*)", + "tag": r"([a-zA-Z][-a-zA-Z0-9]{0,40}|\*)", "attribute": r"[.a-zA-Z_:][-\w:.]*(\(\))?)", - "value": r"\s*[\w/:][-/\w\s,:;.]*" + "value": r"\s*[\w/:][-/\w\s,:;.\S]*", } _validation_re = ( @@ -23,7 +20,7 @@ r"(?Pcontains\((?P@?%(attribute)s,\s*[\"\']" r"(?P%(value)s)[\"\']\))" r")\])?" - r"(\[(?P\d)\])?" + r"(\[(?P\d+)\])?" r")" r")" % _sub_regexes ) @@ -46,16 +43,18 @@ def _handle_brackets_in_strings(xpath): for chunk_num in range(len_chunks): if chunk_num % 2 != 0: chunks[chunk_num] = chunks[chunk_num].replace( - '[', '_STR_L_bracket_') + "[", "_STR_L_bracket_" + ) chunks[chunk_num] = chunks[chunk_num].replace( - ']', '_STR_R_bracket_') + "]", "_STR_R_bracket_" + ) new_xpath += chunks[chunk_num] if chunk_num != len_chunks - 1: new_xpath += '"' return new_xpath -def _filter_xpath_grouping(xpath): +def _filter_xpath_grouping(xpath, original): """ This method removes the outer parentheses for xpath grouping. The xpath converter will break otherwise. @@ -67,55 +66,69 @@ def _filter_xpath_grouping(xpath): xpath = xpath[1:] # Next remove the last closed parentheses - index = xpath.rfind(')') + index = xpath.rfind(")") + index_p1 = index + 1 # Make "flake8" and "black" agree if index == -1: - raise XpathException("Invalid or unsupported Xpath: %s" % xpath) - xpath = xpath[:index] + xpath[index + 1:] + raise XpathException( + "\nInvalid or unsupported XPath:\n%s\n" + "(Unable to convert XPath Selector to CSS Selector)" + "" % original + ) + xpath = xpath[:index] + xpath[index_p1:] return xpath -def _get_raw_css_from_xpath(xpath): +def _get_raw_css_from_xpath(xpath, original): css = "" + attr = "" position = 0 while position < len(xpath): node = prog.match(xpath[position:]) if node is None: - raise XpathException("Invalid or unsupported Xpath: %s" % xpath) + raise XpathException( + "\nInvalid or unsupported XPath:\n%s\n" + "(Unable to convert XPath Selector to CSS Selector)" + "" % original + ) match = node.groupdict() if position != 0: - nav = " " if match['nav'] == "//" else " > " + nav = " " if match["nav"] == "//" else " > " else: nav = "" - tag = "" if match['tag'] == "*" else match['tag'] or "" - - if match['idvalue']: - attr = "#%s" % match['idvalue'].replace(" ", "#") - elif match['matched']: - if match['mattr'] == "@id": - attr = "#%s" % match['mvalue'].replace(" ", "#") - elif match['mattr'] == "@class": - attr = ".%s" % match['mvalue'].replace(" ", ".") - elif match['mattr'] in ["text()", "."]: - attr = ":contains(^%s$)" % match['mvalue'] - elif match['mattr']: - if match["mvalue"].find(" ") != -1: - match["mvalue"] = "\"%s\"" % match["mvalue"] - attr = "[%s=%s]" % (match['mattr'].replace("@", ""), - match['mvalue']) - elif match['contained']: - if match['cattr'].startswith("@"): - attr = "[%s*=%s]" % (match['cattr'].replace("@", ""), - match['cvalue']) - elif match['cattr'] == "text()": - attr = ":contains(%s)" % match['cvalue'] + tag = "" if match["tag"] == "*" else match["tag"] or "" + + if match["idvalue"]: + attr = "#%s" % match["idvalue"].replace(" ", "#") + elif match["matched"]: + if match["mattr"] == "@id": + attr = "#%s" % match["mvalue"].replace(" ", "#") + elif match["mattr"] == "@class": + attr = ".%s" % match["mvalue"].replace(" ", ".") + elif match["mattr"] in ["text()", "."]: + attr = ":contains('%s')" % match["mvalue"] + elif match["mattr"]: + attr = '[%s="%s"]' % ( + match["mattr"].replace("@", ""), + match["mvalue"], + ) + elif match["contained"]: + if match["cattr"].startswith("@"): + attr = '[%s*="%s"]' % ( + match["cattr"].replace("@", ""), + match["cvalue"], + ) + elif match["cattr"] == "text()": + attr = ':contains("%s")' % match["cvalue"] + elif match["cattr"] == ".": + attr = ':contains("%s")' % match["cvalue"] else: attr = "" - if match['nth']: - nth = ":nth-of-type(%s)" % match['nth'] + if match["nth"]: + nth = ":nth-of-type(%s)" % match["nth"] else: nth = "" @@ -128,27 +141,143 @@ def _get_raw_css_from_xpath(xpath): def convert_xpath_to_css(xpath): + original = xpath + xpath = xpath.replace(" = '", "='") + + # **** Start of handling special xpath edge cases instantly **** + + # Handle a special edge case that converts to: 'tag.class:contains("TEXT")' + c3 = "@class and contains(concat(' ', normalize-space(@class), ' '), ' " + if c3 in xpath and xpath.count(c3) == 1 and xpath.count("[@") == 1: + p2 = " ') and (contains(., '" + if ( + xpath.count(p2) == 1 + and xpath.endswith("'))]") + and xpath.count("//") == 1 + and xpath.count(" ') and (") == 1 + ): + s_contains = xpath.split(p2)[1].split("'))]")[0] + s_tag = xpath.split("//")[1].split("[@class")[0] + s_class = xpath.split(c3)[1].split(" ') and (")[0] + return '%s.%s:contains("%s")' % (s_tag, s_class, s_contains) + + # Find instance of: //tag[@attribute='value' and (contains(., 'TEXT'))] + data = re.match( + r"""^\s*//(\S+)\[@(\S+)='(\S+)'\s+and\s+""" + r"""\(contains\(\.,\s'(\S+)'\)\)\]""", + xpath, + ) + if data: + s_tag = data.group(1) + s_atr = data.group(2) + s_val = data.group(3) + s_contains = data.group(4) + return '%s[%s="%s"]:contains("%s")' % (s_tag, s_atr, s_val, s_contains) + + # Find instance of: //tag[@attribute1='value1' and (@attribute2='value2')] + data = re.match( + r"""^\s*//(\S+)\[@(\S+)='(\S+)'\s+and\s+""" + r"""\(@(\S+)='(\S+)'\)\]""", + xpath, + ) + if data: + s_tag = data.group(1) + s_atr1 = data.group(2) + s_val1 = data.group(3) + s_atr2 = data.group(4) + s_val2 = data.group(5) + return '%s[%s="%s"][%s="%s"]' % (s_tag, s_atr1, s_val1, s_atr2, s_val2) + + # Find instance of: //tag[@attribute1='value1' or @attribute2='value2'] + # This tracks single/double quotes and maps them to comma-separated CSS. + data = re.match( + r"^\s*//([\w\-*]*)\s*\[\s*@?([\w\-:]+)\s*=\s*[\"']([^\"']*)[\"']\s+" + r"or\s+@?([\w\-:]+)\s*=\s*[\"']([^\"']*)[\"']\s*\]", + xpath, + ) + if data: + s_tag = data.group(1) + if s_tag == "*": + s_tag = "" + s_atr1 = data.group(2) + s_val1 = data.group(3) + s_atr2 = data.group(4) + s_val2 = data.group(5) + return '%s[%s="%s"], %s[%s="%s"]' % ( + s_tag, s_atr1, s_val1, s_tag, s_atr2, s_val2 + ) + + # **** End of handling special xpath edge cases instantly **** + if xpath[0] != '"' and xpath[-1] != '"' and xpath.count('"') % 2 == 0: xpath = _handle_brackets_in_strings(xpath) + xpath = xpath.replace("descendant-or-self::*/", "descORself/") + if len(xpath) > 3: + xpath = xpath[0:3] + xpath[3:].replace("//", "/descORself/") + + if " and contains(@" in xpath and xpath.count(" and contains(@") == 1: + spot1 = xpath.find(" and contains(@") + spot1 = spot1 + len(" and contains(@") + spot2 = xpath.find(",", spot1) + attr = xpath[spot1:spot2] + swap = " and contains(@%s, " % attr + if swap in xpath: + swap_spot = xpath.find(swap) + close_paren = xpath.find("]", swap_spot) - 1 + close_paren_p1 = close_paren + 1 # Make "flake8" and "black" agree + if close_paren > 1: + xpath = xpath[:close_paren] + xpath[close_paren_p1:] + xpath = xpath.replace(swap, "_STAR_=") - if xpath.startswith('('): - xpath = _filter_xpath_grouping(xpath) + if xpath.startswith("("): + xpath = _filter_xpath_grouping(xpath, original) - css = _get_raw_css_from_xpath(xpath) + css = "" + if "/descORself/" in xpath and ("@id" in xpath or "@class" in xpath): + css_sections = [] + xpath_sections = xpath.split("/descORself/") + for xpath_section in xpath_sections: + if not xpath_section.startswith("//"): + xpath_section = "//" + xpath_section + css_sections.append( + _get_raw_css_from_xpath(xpath_section, original) + ) + css = "/descORself/".join(css_sections) + else: + css = _get_raw_css_from_xpath(xpath, original) - attribute_defs = re.findall(r'(\[\w+\=\S+\])', css) + attribute_defs = re.findall(r"(\[\w+\=\S+\])", css) for attr_def in attribute_defs: - if (attr_def.count('[') == 1 and attr_def.count(']') == 1 and ( - attr_def.count('=') == 1) and attr_def.count('"') == 0 and ( - attr_def.count("'") == 0)) and attr_def.count(' ') == 0: + if ( + attr_def.count("[") == 1 + and attr_def.count("]") == 1 + and attr_def.count("=") == 1 + and attr_def.count('"') == 0 + and attr_def.count("'") == 0 + and attr_def.count(" ") == 0 + ): # Now safe to manipulate - q1 = attr_def.find('=') + 1 - q2 = attr_def.find(']') + q1 = attr_def.find("=") + 1 + q2 = attr_def.find("]") new_attr_def = attr_def[:q1] + "'" + attr_def[q1:q2] + "']" css = css.replace(attr_def, new_attr_def) # Replace the string-brackets with escaped ones - css = css.replace('_STR_L_bracket_', '\\[') - css = css.replace('_STR_R_bracket_', '\\]') + css = css.replace("_STR_L_bracket_", "\\[") + css = css.replace("_STR_R_bracket_", "\\]") + + # Handle a lot of edge cases with conversion + css = css.replace(" > descORself > ", " ") + css = css.replace(" descORself > ", " ") + css = css.replace("/descORself/*", " ") + css = css.replace("/descORself/", " ") + css = css.replace("descORself > ", "") + css = css.replace("descORself/", " ") + css = css.replace("descORself", " ") + css = css.replace("_STAR_=", "*=") + css = css.replace("]/", "] ") + css = css.replace("] *[", "] > [") + css = css.replace("'", '"') + css = css.replace("[@", "[") return css diff --git a/seleniumbase/js_code/__init__.py b/seleniumbase/js_code/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seleniumbase/js_code/active_css_js.py b/seleniumbase/js_code/active_css_js.py new file mode 100644 index 00000000000..996a00b3cec --- /dev/null +++ b/seleniumbase/js_code/active_css_js.py @@ -0,0 +1,268 @@ +############################################################################### +# active_css_js - Return the Best CSS Selector of the Currently Active Element. +############################################################################### + +get_active_element_css = r""" +var cssPathById = function(el) { + if (!(el instanceof Element)) return; + var path = []; + while (el != null && el.nodeType === Node.ELEMENT_NODE) { + var selector = el.nodeName.toLowerCase(); + if (el.id) { + elid = el.id; + if (/\s/.test(elid) || elid.includes(',') || elid.includes('.') || + elid.includes('(') || elid.includes(':') || hasDigit(elid[0])) + return cssPathByAttribute(el, 'id'); + selector += '#' + elid; + path.unshift(selector); + break; + } else { + var sib = el, nth = 1; + while (sib = sib.previousElementSibling) { + if (sib.nodeName.toLowerCase() == selector) + nth++; + } + if (nth != 1) + selector += ':nth-of-type('+nth+')'; + } + path.unshift(selector); + el = el.parentNode; + } + return path.join(' > '); +}; +var cssPathByAttribute = function(el, attr) { + if (!(el instanceof Element)) return; + var path = []; + while (el !== null && el.nodeType === Node.ELEMENT_NODE) { + var selector = el.nodeName.toLowerCase(); + if (el.hasAttribute(attr) && + el.getAttribute(attr).length > 0 && + !el.getAttribute(attr).includes('\n')) + { + the_attr = el.getAttribute(attr); + the_attr = the_attr.replaceAll('"', '\\"'); + the_attr = the_attr.replaceAll("'", "\\'"); + selector += '[' + attr + '="' + the_attr + '"]'; + path.unshift(selector); + break; + } else { + var sib = el, nth = 1; + while (sib = sib.previousElementSibling) { + if (sib.nodeName.toLowerCase() == selector) + nth++; + } + if (nth != 1) + selector += ':nth-of-type('+nth+')'; + } + path.unshift(selector); + el = el.parentNode; + } + return path.join(' > '); +}; +var cssPathByClass = function(el) { + if (!(el instanceof Element)) return; + var path = []; + while (el !== null && el.nodeType === Node.ELEMENT_NODE) { + var selector = el.nodeName.toLowerCase(); + if (el.hasAttribute('class') && + el.getAttribute('class').length > 0 && + !el.getAttribute('class').includes(' ') && + (el.getAttribute('class').includes('-')) && + document.querySelectorAll( + selector + '.' + el.getAttribute('class')).length == 1) { + selector += '.' + el.getAttribute('class'); + path.unshift(selector); + break; + } else { + var sib = el, nth = 1; + while (sib = sib.previousElementSibling) { + if (sib.nodeName.toLowerCase() == selector) nth++; + } + if (nth != 1) + selector += ':nth-of-type('+nth+')'; + } + path.unshift(selector); + el = el.parentNode; + } + return path.join(' > '); +}; +var ssOccurrences = function(string, subString, allowOverlapping) { + if (subString.length <= 0) + return (string.length + 1); + var n = 0; + var pos = 0; + var step = allowOverlapping ? 1 : subString.length; + while (true) { + pos = string.indexOf(subString, pos); + if (pos >= 0) { ++n; pos += step; } + else break; + } + return n; +}; +function hasDigit(str) { + return /\d/.test(str); +}; +function isGen(str) { + return /[_-]\d/.test(str) || /\d[a-z]/.test(str); +}; +function tagName(el) { + return el.tagName.toLowerCase(); +}; +function turnIntoParentAsNeeded(el) { + if (tagName(el) == 'span' || tagName(el) == 'i') { + if (tagName(el.parentElement) == 'button') { + el = el.parentElement; + } + else if (tagName(el.parentElement.parentElement) == 'button') { + el = el.parentElement.parentElement; + } + } + return el; +} +var getBestSelector = function(el) { + if (!(el instanceof Element)) return; + el = turnIntoParentAsNeeded(el); + sel_by_id = cssPathById(el); + if (!sel_by_id.includes(' > ') && !isGen(sel_by_id)) return sel_by_id; + child_count_by_id = ssOccurrences(sel_by_id, ' > '); + selector_by_class = cssPathByClass(el); + tag_name = tagName(el); + non_id_attributes = []; + non_id_attributes.push('name'); + non_id_attributes.push('data-qa'); + non_id_attributes.push('data-tid'); + non_id_attributes.push('data-el'); + non_id_attributes.push('data-se'); + non_id_attributes.push('data-name'); + non_id_attributes.push('data-auto'); + non_id_attributes.push('data-text'); + non_id_attributes.push('data-test'); + non_id_attributes.push('data-testid'); + non_id_attributes.push('data-test-id'); + non_id_attributes.push('data-test-selector'); + non_id_attributes.push('data-nav'); + non_id_attributes.push('data-sb'); + non_id_attributes.push('data-cy'); + non_id_attributes.push('data-action'); + non_id_attributes.push('data-target'); + non_id_attributes.push('data-tooltip'); + non_id_attributes.push('alt'); + non_id_attributes.push('title'); + non_id_attributes.push('heading'); + non_id_attributes.push('translate'); + non_id_attributes.push('aria-label'); + non_id_attributes.push('aria-describedby'); + non_id_attributes.push('rel'); + non_id_attributes.push('ng-model'); + non_id_attributes.push('ng-href'); + non_id_attributes.push('href'); + non_id_attributes.push('label'); + non_id_attributes.push('data-content'); + non_id_attributes.push('data-tip'); + non_id_attributes.push('data-for'); + non_id_attributes.push('class'); + non_id_attributes.push('for'); + non_id_attributes.push('placeholder'); + non_id_attributes.push('value'); + non_id_attributes.push('ng-click'); + non_id_attributes.push('ng-if'); + non_id_attributes.push('src'); + selector_by_attr = []; + all_by_attr = []; + num_by_attr = []; + child_count_by_attr = []; + for (var i = 0; i < non_id_attributes.length; i++) { + n_i_attr = non_id_attributes[i]; + selector_by_attr[i] = null; + if (n_i_attr == 'class') selector_by_attr[i] = selector_by_class; + else selector_by_attr[i] = cssPathByAttribute(el, n_i_attr); + all_by_attr[i] = document.querySelectorAll(selector_by_attr[i]); + num_by_attr[i] = all_by_attr[i].length; + if (!selector_by_attr[i].includes(' > ') && + ((num_by_attr[i] == 1) || (el == all_by_attr[i][0]))) + { + if (n_i_attr.startsWith('aria') || n_i_attr == 'for') + if (hasDigit(selector_by_attr[i])) continue; + return selector_by_attr[i]; + } + child_count_by_attr[i] = ssOccurrences(selector_by_attr[i], ' > '); + } + basic_tags = []; + basic_tags.push('h1'); + basic_tags.push('h2'); + basic_tags.push('h3'); + basic_tags.push('canvas'); + basic_tags.push('center'); + basic_tags.push('input'); + basic_tags.push('textarea'); + for (var i = 0; i < basic_tags.length; i++) { + d_qsa = document.querySelectorAll(basic_tags[i]); + if (tag_name == basic_tags[i] && d_qsa.length == 1 && el == d_qsa[0]) + return basic_tags[i]; + } + contains_tags = []; + contains_tags.push('a'); + contains_tags.push('b'); + contains_tags.push('h1'); + contains_tags.push('h2'); + contains_tags.push('h3'); + contains_tags.push('h4'); + contains_tags.push('h5'); + contains_tags.push('code'); + contains_tags.push('mark'); + contains_tags.push('button'); + contains_tags.push('label'); + contains_tags.push('legend'); + contains_tags.push('li'); + contains_tags.push('td'); + contains_tags.push('th'); + contains_tags.push('i'); + contains_tags.push('small'); + contains_tags.push('strong'); + contains_tags.push('summary'); + contains_tags.push('span'); + all_by_tag = []; + text_content = ''; + if (el.textContent) + text_content = el.textContent.trim(); + for (var i = 0; i < contains_tags.length; i++) { + if (tag_name == contains_tags[i] && + text_content.length >= 2 && text_content.length <= 64) + { + t_count = 0; + all_by_tag[i] = document.querySelectorAll(contains_tags[i]); + for (var j = 0; j < all_by_tag[i].length; j++) { + if (all_by_tag[i][j].textContent.includes(text_content)) + t_count += 1; + } + if (t_count === 1 && !text_content.includes('\n')) { + text_content = text_content.replaceAll("'", "\\'"); + text_content = text_content.replaceAll('"', '\\"'); + return tag_name += ':contains("'+text_content+'")'; + } + } + } + best_selector = sel_by_id; + lowest_child_count = child_count_by_id; + child_count_by_class = ssOccurrences(selector_by_class, ' > '); + if (child_count_by_class < lowest_child_count) { + best_selector = selector_by_class; + lowest_child_count = child_count_by_class; + } + for (var i = 0; i < non_id_attributes.length; i++) { + if (child_count_by_attr[i] < lowest_child_count && + ((num_by_attr[i] == 1) || (el == all_by_attr[i][0]))) + { + best_selector = selector_by_attr[i]; + lowest_child_count = child_count_by_attr[i]; + } + } + best_selector = best_selector.replaceAll('html > body', 'body'); + selector = best_selector.replaceAll(' > ', ' '); + selector = selector.replaceAll(' div ', ' '); + if (document.querySelector(selector) == el) + best_selector = selector; + return best_selector; +}; +return getBestSelector(document.activeElement); +""" diff --git a/seleniumbase/js_code/live_js.py b/seleniumbase/js_code/live_js.py new file mode 100644 index 00000000000..01865b0b6bb --- /dev/null +++ b/seleniumbase/js_code/live_js.py @@ -0,0 +1,181 @@ +############################################################################### +# live_js - Reload web pages when changes are detected on the server side. +############################################################################### + +live_js = r""" +(function () { + var headers = { + "Etag": 1, "Last-Modified": 1, "Content-Length": 1, "Content-Type": 1 }, + resources = {}, + pendingRequests = {}, + currentLinkElements = {}, + oldLinkElements = {}, + interval = 1000, + loaded = false, + active = { "html": 1, "css": 1, "js": 1 }; + var Live = { + heartbeat: function () { + if (document.body) { + if (!loaded) Live.loadresources(); + Live.checkForChanges(); + } + setTimeout(Live.heartbeat, interval); + }, + loadresources: function () { + function isLocal(url) { + var loc = document.location, + reg = new RegExp("^\\.|^\/(?!\/)|^[\\w]((?!://).)*$|" + + loc.protocol + "//" + loc.host); + return url.match(reg); + } + var scripts = document.getElementsByTagName("script"), + links = document.getElementsByTagName("link"), + uris = []; + for (var i = 0; i < scripts.length; i++) { + var script = scripts[i], src = script.getAttribute("src"); + if (src && isLocal(src)) + uris.push(src); + if (src && src.match(/\blive.js#/)) { + for (var type in active) + active[type] = src.match("[#,|]" + type) != null + if (src.match("notify")) + alert("Live.js is loaded."); + } + } + if (!active.js) uris = []; + if (active.html) uris.push(document.location.href); + for (var i = 0; i < links.length && active.css; i++) { + var link = links[i], rel = link.getAttribute("rel" + ),href = link.getAttribute("href", 2); + if (href && rel && rel.match(new RegExp("stylesheet", "i") + ) && isLocal(href)) { + uris.push(href); + currentLinkElements[href] = link; + } + } + for (var i = 0; i < uris.length; i++) { + var url = uris[i]; + Live.getHead(url, function (url, info) { + resources[url] = info; + }); + } + var head = document.getElementsByTagName("head")[0], + style = document.createElement("style"), + rule = "transition: all .3s ease-out;" + css = [".livejs-loading * { ", + rule, " -webkit-", rule, "-moz-", rule, "-o-", rule, "}"].join(''); + style.setAttribute("type", "text/css"); + head.appendChild(style); + style.styleSheet ? style.styleSheet.cssText = css : + style.appendChild(document.createTextNode(css)); + loaded = true; +}, +checkForChanges: function () { + for (var url in resources) { + if (pendingRequests[url]) + continue; + Live.getHead(url, function (url, newInfo) { + var oldInfo = resources[url], + hasChanged = false; + resources[url] = newInfo; + for (var header in oldInfo) { + var oldValue = oldInfo[header], + newValue = newInfo[header], + contentType = newInfo["Content-Type"]; + switch (header.toLowerCase()) { + case "etag": + if (!newValue) break; + default: + hasChanged = oldValue != newValue; + break; + } + if (hasChanged) { + Live.refreshResource(url, contentType); + break; + } + } + }); + } +}, +refreshResource: function (url, type) { + switch (type.toLowerCase()) { + case "text/css": + var link = currentLinkElements[url], + html = document.body.parentNode, + head = link.parentNode, + next = link.nextSibling, + newLink = document.createElement("link"); + html.className = html.className.replace(/\s*livejs\-loading/gi, '' + ) + ' livejs-loading'; + newLink.setAttribute("type", "text/css"); + newLink.setAttribute("rel", "stylesheet"); + newLink.setAttribute("href", url + "?now=" + new Date() * 1); + next ? head.insertBefore(newLink, next) : head.appendChild( + newLink); + currentLinkElements[url] = newLink; + oldLinkElements[url] = link; + Live.removeoldLinkElements(); + break; + case "text/html": + if (url != document.location.href) + return; + case "text/javascript": + case "application/javascript": + case "application/x-javascript": + document.location.reload(); + } +}, +removeoldLinkElements: function () { + var pending = 0; + for (var url in oldLinkElements) { + try { + var link = currentLinkElements[url], + oldLink = oldLinkElements[url], + html = document.body.parentNode, + sheet = link.sheet || link.styleSheet, + rules = sheet.rules || sheet.cssRules; + if (rules.length >= 0) { + oldLink.parentNode.removeChild(oldLink); + delete oldLinkElements[url]; + setTimeout(function () { + html.className = html.className.replace( + /\s*livejs\-loading/gi, ''); + }, 100); + } + } catch (e) { + pending++; + } + if (pending) setTimeout(Live.removeoldLinkElements, 50); + } +}, +getHead: function (url, callback) { + pendingRequests[url] = true; + var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : + new ActiveXObject("Microsoft.XmlHttp"); + xhr.open("HEAD", url, true); + xhr.onreadystatechange = function () { + delete pendingRequests[url]; + if (xhr.readyState == 4 && xhr.status != 304) { + xhr.getAllResponseHeaders(); + var info = {}; + for (var h in headers) { + var value = xhr.getResponseHeader(h); + if (h.toLowerCase() == "etag" && value) + value = value.replace(/^W\//, ''); + if (h.toLowerCase() == "content-type" && value) + value = value.replace(/^(.*?);.*?$/i, "$1"); + info[h] = value; + } + callback(url, info); + } + } + xhr.send(); +} +}; +if (document.location.protocol != "file:") { + if (!window.liveJsLoaded) + Live.heartbeat(); + window.liveJsLoaded = true; +} +})(); +""" diff --git a/seleniumbase/js_code/recorder_js.py b/seleniumbase/js_code/recorder_js.py new file mode 100644 index 00000000000..a127e98abb7 --- /dev/null +++ b/seleniumbase/js_code/recorder_js.py @@ -0,0 +1,800 @@ +############################################################################### +# recorder_js - Save browser actions to sessionStorage with good CSS selectors. +############################################################################### + +recorder_js = r""" +var cssPathById = function(el) { + if (!(el instanceof Element)) return; + var path = []; + while (el != null && el.nodeType === Node.ELEMENT_NODE) { + var selector = el.nodeName.toLowerCase(); + if (el.id) { + elid = el.id; + if (/\s/.test(elid) || elid.includes(',') || elid.includes('.') || + elid.includes('(') || elid.includes(':') || hasDigit(elid[0])) + return cssPathByAttribute(el, 'id'); + selector += '#' + elid; + path.unshift(selector); + break; + } else { + var sib = el, nth = 1; + while (sib = sib.previousElementSibling) { + if (sib.nodeName.toLowerCase() == selector) + nth++; + } + if (nth != 1) + selector += ':nth-of-type('+nth+')'; + } + path.unshift(selector); + el = el.parentNode; + } + return path.join(' > '); +}; +var cssPathByAttribute = function(el, attr) { + if (!(el instanceof Element)) return; + var path = []; + while (el !== null && el.nodeType === Node.ELEMENT_NODE) { + var selector = el.nodeName.toLowerCase(); + if (el.hasAttribute(attr) && + el.getAttribute(attr).length > 0 && + !el.getAttribute(attr).includes('\n')) + { + the_attr = el.getAttribute(attr); + the_attr = the_attr.replaceAll('"', '\\"'); + the_attr = the_attr.replaceAll("'", "\\'"); + selector += '[' + attr + '="' + the_attr + '"]'; + path.unshift(selector); + break; + } else { + var sib = el, nth = 1; + while (sib = sib.previousElementSibling) { + if (sib.nodeName.toLowerCase() == selector) + nth++; + } + if (nth != 1) + selector += ':nth-of-type('+nth+')'; + } + path.unshift(selector); + el = el.parentNode; + } + return path.join(' > '); +}; +var cssPathByClass = function(el) { + if (!(el instanceof Element)) return; + var path = []; + while (el !== null && el.nodeType === Node.ELEMENT_NODE) { + var selector = el.nodeName.toLowerCase(); + if (el.hasAttribute('class') && + el.getAttribute('class').length > 0 && + !el.getAttribute('class').includes(' ') && + (el.getAttribute('class').includes('-')) && + document.querySelectorAll( + selector + '.' + el.getAttribute('class')).length == 1) { + selector += '.' + el.getAttribute('class'); + path.unshift(selector); + break; + } else { + var sib = el, nth = 1; + while (sib = sib.previousElementSibling) { + if (sib.nodeName.toLowerCase() == selector) nth++; + } + if (nth != 1) + selector += ':nth-of-type('+nth+')'; + } + path.unshift(selector); + el = el.parentNode; + } + return path.join(' > '); +}; +var ssOccurrences = function(string, subString, allowOverlapping) { + if (subString.length <= 0) + return (string.length + 1); + var n = 0; + var pos = 0; + var step = allowOverlapping ? 1 : subString.length; + while (true) { + pos = string.indexOf(subString, pos); + if (pos >= 0) { ++n; pos += step; } + else break; + } + return n; +}; +function hasDigit(str) { + return /\d/.test(str); +}; +function isGen(str) { + return /[_-]\d/.test(str) || /\d[a-z]/.test(str); +}; +function tagName(el) { + return el.tagName.toLowerCase(); +}; +function turnIntoParentAsNeeded(el) { + if (tagName(el) == 'span' || tagName(el) == 'i') { + if (tagName(el.parentElement) == 'button') { + el = el.parentElement; + } + else if (tagName(el.parentElement.parentElement) == 'button') { + el = el.parentElement.parentElement; + } + } + return el; +} +var getBestSelector = function(el) { + if (!(el instanceof Element)) return; + el = turnIntoParentAsNeeded(el); + sel_by_id = cssPathById(el); + if (!sel_by_id.includes(' > ') && !isGen(sel_by_id)) return sel_by_id; + child_count_by_id = ssOccurrences(sel_by_id, ' > '); + selector_by_class = cssPathByClass(el); + tag_name = tagName(el); + non_id_attributes = []; + non_id_attributes.push('name'); + non_id_attributes.push('data-qa'); + non_id_attributes.push('data-tid'); + non_id_attributes.push('data-el'); + non_id_attributes.push('data-se'); + non_id_attributes.push('data-name'); + non_id_attributes.push('data-auto'); + non_id_attributes.push('data-text'); + non_id_attributes.push('data-test'); + non_id_attributes.push('data-testid'); + non_id_attributes.push('data-test-id'); + non_id_attributes.push('data-test-selector'); + non_id_attributes.push('data-nav'); + non_id_attributes.push('data-sb'); + non_id_attributes.push('data-cy'); + non_id_attributes.push('data-action'); + non_id_attributes.push('data-target'); + non_id_attributes.push('data-tooltip'); + non_id_attributes.push('alt'); + non_id_attributes.push('title'); + non_id_attributes.push('heading'); + non_id_attributes.push('translate'); + non_id_attributes.push('aria-label'); + non_id_attributes.push('aria-describedby'); + non_id_attributes.push('rel'); + non_id_attributes.push('ng-model'); + non_id_attributes.push('ng-href'); + non_id_attributes.push('href'); + non_id_attributes.push('label'); + non_id_attributes.push('data-content'); + non_id_attributes.push('data-tip'); + non_id_attributes.push('data-for'); + non_id_attributes.push('class'); + non_id_attributes.push('for'); + non_id_attributes.push('placeholder'); + non_id_attributes.push('value'); + non_id_attributes.push('ng-click'); + non_id_attributes.push('ng-if'); + non_id_attributes.push('src'); + selector_by_attr = []; + all_by_attr = []; + num_by_attr = []; + child_count_by_attr = []; + for (var i = 0; i < non_id_attributes.length; i++) { + n_i_attr = non_id_attributes[i]; + selector_by_attr[i] = null; + if (n_i_attr == 'class') selector_by_attr[i] = selector_by_class; + else selector_by_attr[i] = cssPathByAttribute(el, n_i_attr); + all_by_attr[i] = document.querySelectorAll(selector_by_attr[i]); + num_by_attr[i] = all_by_attr[i].length; + if (!selector_by_attr[i].includes(' > ') && + ((num_by_attr[i] == 1) || (el == all_by_attr[i][0]))) + { + if (n_i_attr.startsWith('aria') || n_i_attr == 'for') + if (hasDigit(selector_by_attr[i])) continue; + return selector_by_attr[i]; + } + child_count_by_attr[i] = ssOccurrences(selector_by_attr[i], ' > '); + } + basic_tags = []; + basic_tags.push('h1'); + basic_tags.push('h2'); + basic_tags.push('h3'); + basic_tags.push('canvas'); + basic_tags.push('center'); + basic_tags.push('input'); + basic_tags.push('textarea'); + for (var i = 0; i < basic_tags.length; i++) { + d_qsa = document.querySelectorAll(basic_tags[i]); + if (tag_name == basic_tags[i] && d_qsa.length == 1 && el == d_qsa[0]) + return basic_tags[i]; + } + contains_tags = []; + contains_tags.push('a'); + contains_tags.push('b'); + contains_tags.push('h1'); + contains_tags.push('h2'); + contains_tags.push('h3'); + contains_tags.push('h4'); + contains_tags.push('h5'); + contains_tags.push('code'); + contains_tags.push('mark'); + contains_tags.push('button'); + contains_tags.push('label'); + contains_tags.push('legend'); + contains_tags.push('li'); + contains_tags.push('td'); + contains_tags.push('th'); + contains_tags.push('i'); + contains_tags.push('small'); + contains_tags.push('strong'); + contains_tags.push('summary'); + contains_tags.push('span'); + all_by_tag = []; + text_content = ''; + if (el.textContent) + text_content = el.textContent.trim(); + for (var i = 0; i < contains_tags.length; i++) { + if (tag_name == contains_tags[i] && + text_content.length >= 2 && text_content.length <= 64) + { + t_count = 0; + all_by_tag[i] = document.querySelectorAll(contains_tags[i]); + for (var j = 0; j < all_by_tag[i].length; j++) { + if (all_by_tag[i][j].textContent.includes(text_content)) + t_count += 1; + } + if (t_count === 1 && !text_content.includes('\n')) { + text_content = text_content.replaceAll("'", "\\'"); + text_content = text_content.replaceAll('"', '\\"'); + return tag_name += ':contains("'+text_content+'")'; + } + } + } + best_selector = sel_by_id; + lowest_child_count = child_count_by_id; + child_count_by_class = ssOccurrences(selector_by_class, ' > '); + if (child_count_by_class < lowest_child_count) { + best_selector = selector_by_class; + lowest_child_count = child_count_by_class; + } + for (var i = 0; i < non_id_attributes.length; i++) { + if (child_count_by_attr[i] < lowest_child_count && + ((num_by_attr[i] == 1) || (el == all_by_attr[i][0]))) + { + best_selector = selector_by_attr[i]; + lowest_child_count = child_count_by_attr[i]; + } + } + best_selector = best_selector.replaceAll('html > body', 'body'); + selector = best_selector.replaceAll(' > ', ' '); + selector = selector.replaceAll(' div ', ' '); + if (document.querySelector(selector) == el) + best_selector = selector; + return best_selector; +}; + +function useHref(tag_name, el) { + return (tag_name === 'a' && el.hasAttribute('href') && + el.getAttribute('href').length > 0 && el.origin != 'null'); +}; +function saveRecordedActions() { + json_rec_act = JSON.stringify(document.recorded_actions); + sessionStorage.setItem('recorded_actions', json_rec_act); +}; +function new_tab_on_new_origin() { + var AllAnchorTags = document.getElementsByTagName('a'); + for (var i = 0; i < AllAnchorTags.length; i++) { + if (!AllAnchorTags[i].sbset) { + AllAnchorTags[i].sbset = true; + AllAnchorTags[i].addEventListener('click', function (event) { + rec_mode = sessionStorage.getItem('recorder_mode'); + if (rec_mode !== '2' && rec_mode !== '3') { + if (this.origin && + this.origin != 'null' && + this.origin != document.location.origin && + this.hasAttribute('href')) + { + event.preventDefault(); + window.open(this.href, '_blank').focus(); + } + } else { event.preventDefault(); event.stopPropagation(); } + }, false); + } + } +}; +new_tab_on_new_origin(); +var AllInputTags = document.getElementsByTagName('input'); +var AllButtonTags = document.getElementsByTagName('button'); +var All_IB_Tags = []; +All_IB_Tags.push(...AllInputTags, ...AllButtonTags); +for (var i = 0; i < All_IB_Tags.length; i++) { + All_IB_Tags[i].addEventListener('click', function (event) { + rec_mode = sessionStorage.getItem('recorder_mode'); + if (rec_mode === '2' || rec_mode === '3') + { event.preventDefault(); event.stopPropagation(); } + }, false); +} +var SearchInputs = document.querySelectorAll('input[type="search"]'); +for (var i = 0; i < SearchInputs.length; i++) { + SearchInputs[i].addEventListener('change', function (event) { + new_tab_on_new_origin(); + }, false); +} +var AwayForms = document.querySelectorAll('form[action^="//"]'); +for (var i = 0; i < AwayForms.length; i++) { + AwayForms[i].target = '_blank'; +} +var reset_recorder_state = function() { + document.recorded_actions = []; + sessionStorage.setItem('pause_recorder', 'no'); + sessionStorage.setItem('recorder_mode', '1'); + sessionStorage.setItem('recorder_title', document.title); + const d_now = Date.now(); + document.recorder_last_mouseup = d_now; + w_orig = window.location.origin; + w_href = window.location.href; + if (sessionStorage.getItem('recorder_activated') === 'yes') { + ss_ra = JSON.parse(sessionStorage.getItem('recorded_actions')); + document.recorded_actions = ss_ra; + document.recorded_actions.push(['_url_', w_orig, w_href, d_now]); + } + else { + sessionStorage.setItem('recorder_activated', 'yes'); + document.recorded_actions.push(['begin', w_orig, w_href, d_now]); + } + saveRecordedActions(); + return; +}; +reset_recorder_state(); +var reset_if_recorder_undefined = function() { + if (typeof document.recorded_actions === 'undefined') + reset_recorder_state(); +}; +var set_border = function(color) { + document.querySelector('body').style.border = '5px solid ' + color; + document.querySelector('body').style.borderRadius = '10px'; +}; + +document.body.addEventListener('mouseover', function (event) { + reset_if_recorder_undefined(); + if (sessionStorage.getItem('pause_recorder') === 'yes') return; + const el = event.target; + const selector = getBestSelector(el); + if (!selector.startsWith('body') && !selector.includes(' div')) { + document.title = selector; + } +}); +document.body.addEventListener('mouseout', function (event) { + reset_if_recorder_undefined(); + if (sessionStorage.getItem('pause_recorder') === 'yes') return; + document.title = sessionStorage.getItem('recorder_title'); +}); +window.addEventListener('blur', () => { + setTimeout(() => { + reset_if_recorder_undefined(); + rec_mode = sessionStorage.getItem('recorder_mode'); + if (rec_mode === '2' || rec_mode === '3') return; + const el = document.activeElement; + const d_now = Date.now(); + const d_now2 = d_now + 1; + const doc_t = document.title; + skip_open = false; + if (tagName(el) === 'iframe' && + doc_t.startsWith('iframe') && + Date.now() - document.recorder_last_mouseup > 32) + { + const selector = getBestSelector(el); + const el_cw = el.contentWindow; + origin = window.location.origin; + if (el.hasAttribute('src') && el.getAttribute('src').length > 0) { + if (el.src.startsWith('data:')) return; + skip_open = true; window.open(el.src, "_blank"); + } + else document.body.innerHTML = el_cw.document.body.innerHTML; + window.focus(); + document.recorded_actions.push(['sw_fr', selector, origin, d_now]); + if (skip_open) + document.recorded_actions.push(['sk_fo', '', origin, d_now2]); + saveRecordedActions(); + } + }); +}, { once: false }); +document.body.addEventListener('click', function (event) { + // do nothing +}); +document.body.addEventListener('dblclick', function (event) { + reset_if_recorder_undefined(); + if (sessionStorage.getItem('pause_recorder') === 'yes') return; + document.recorded_actions.push(['dbclk', '', '', Date.now()+1]); + saveRecordedActions(); +}); +document.body.addEventListener('submit', function (event) { + reset_if_recorder_undefined(); + if (sessionStorage.getItem('pause_recorder') === 'yes') return; + const d_now = Date.now(); + ra_len = document.recorded_actions.length; + if (ra_len > 0 && + document.recorded_actions[ra_len-1][0] === 'input' && + !document.recorded_actions[ra_len-1][2].endsWith('\n')) + { + selector = document.recorded_actions[ra_len-1][1]; + text = document.recorded_actions[ra_len-1][2] + '\n'; + document.recorded_actions.pop(); + document.recorded_actions.push(['input', selector, text, d_now]); + saveRecordedActions(); + } +}); +document.body.addEventListener('formdata', function (event) { + reset_if_recorder_undefined(); + if (sessionStorage.getItem('pause_recorder') === 'yes') return; + const d_now = Date.now(); + ra_len = document.recorded_actions.length; + if (ra_len > 0 && + document.recorded_actions[ra_len-1][0] === 'input' && + !document.recorded_actions[ra_len-1][2].endsWith('\n')) + { + selector = document.recorded_actions[ra_len-1][1]; + text = document.querySelector(selector).value + '\n'; + document.recorded_actions.pop(); + document.recorded_actions.push(['input', selector, text, d_now]); + saveRecordedActions(); + } +}); +document.body.addEventListener('dragstart', function (event) { + reset_if_recorder_undefined(); + if (sessionStorage.getItem('pause_recorder') === 'yes') return; + const d_now = Date.now(); + const el = event.target; + const selector = getBestSelector(el); + ra_len = document.recorded_actions.length; + rec_mode = sessionStorage.getItem('recorder_mode'); + if (rec_mode === '2' || rec_mode === '3') return; + if (ra_len > 0 && + document.recorded_actions[ra_len-1][0] === 'mo_dn' && + document.recorded_actions[ra_len-1][1] === selector) + { + document.recorded_actions.pop(); + } + if (el.draggable === true) { + document.recorded_actions.push(['drags', selector, '', d_now]); + } + saveRecordedActions(); +}); +document.body.addEventListener('dragend', function (event) { + reset_if_recorder_undefined(); + if (sessionStorage.getItem('pause_recorder') === 'yes') return; + ra_len = document.recorded_actions.length; + if (ra_len > 0 && document.recorded_actions[ra_len-1][0] === 'drags') + { + document.recorded_actions.pop(); + saveRecordedActions(); + } +}); +document.body.addEventListener('drop', function (event) { + reset_if_recorder_undefined(); + if (sessionStorage.getItem('pause_recorder') === 'yes') return; + const d_now = Date.now(); + const el = event.target; + const selector = getBestSelector(el); + ra_len = document.recorded_actions.length; + if (ra_len > 0 && document.recorded_actions[ra_len-1][0] === 'drags') + { + drg_s = document.recorded_actions[ra_len-1][1]; + document.recorded_actions.pop(); + document.recorded_actions.push(['ddrop', drg_s, selector, d_now]); + saveRecordedActions(); + } +}); +document.body.addEventListener('change', function (event) { + reset_if_recorder_undefined(); + if (sessionStorage.getItem('pause_recorder') === 'yes') return; + const d_now = Date.now(); + const el = event.target; + const selector = getBestSelector(el); + ra_len = document.recorded_actions.length; + tag_name = tagName(el); + e_type = el.type; + if (tag_name === 'select') + { + el_computed = document.querySelector(selector); + optxt = el_computed.options[el_computed.selectedIndex].text; + document.recorded_actions.push(['s_opt', selector, optxt, d_now]); + } + else if (tag_name === 'input' && e_type === 'range') + { + if (ra_len > 0 && document.recorded_actions[ra_len-1][1] === selector) + { + document.recorded_actions.pop(); + ra_len = document.recorded_actions.length; + } + if (ra_len > 0 && document.recorded_actions[ra_len-1][1] === selector) + { + document.recorded_actions.pop(); + ra_len = document.recorded_actions.length; + } + value = el.value; + document.recorded_actions.push(['set_v', selector, value, d_now]); + } + else if (tag_name === 'input' && e_type === 'file') { + if (ra_len > 0 && document.recorded_actions[ra_len-1][1] === selector) + { + document.recorded_actions.pop(); + ra_len = document.recorded_actions.length; + } + value = el.value; + document.recorded_actions.push(['cho_f', selector, value, d_now]); + } + else if (ra_len > 0 && + document.recorded_actions[ra_len-1][1] === selector && + tag_name === 'input' && e_type === 'checkbox') + { + document.recorded_actions.pop(); + ra_len = document.recorded_actions.length; + if (ra_len > 0 && document.recorded_actions[ra_len-1][1] === selector) + document.recorded_actions.pop(); + } + if (tag_name === 'input' && e_type === 'checkbox' && el.checked) + document.recorded_actions.push(['c_box', selector, 'yes', d_now]); + else if (tag_name === 'input' && e_type === 'checkbox' && !el.checked) + document.recorded_actions.push(['c_box', selector, 'no', d_now]); + saveRecordedActions(); +}); +document.body.addEventListener('mousedown', function (event) { + reset_if_recorder_undefined(); + new_tab_on_new_origin(); + if (sessionStorage.getItem('pause_recorder') === 'yes') return; + const d_now = Date.now(); + el = event.target; + const selector = getBestSelector(el); + ra_len = document.recorded_actions.length; + rec_mode = sessionStorage.getItem('recorder_mode'); + tag_name = tagName(el); + if (rec_mode === '2' || rec_mode === '3') + { + el = turnIntoParentAsNeeded(el); + text = el.innerText; + t_con = el.textContent; + origin = window.location.origin; + sel_has_contains = selector.includes(':contains('); + if (!text) { text = ''; } + text = text.trim(); + if (el.tagName.toLowerCase() == "input") + text = el.value.trim(); + if (!t_con) { t_con = ''; } + if (rec_mode === '2' || ( + rec_mode === '3' && sel_has_contains && text === t_con.trim())) + { + document.recorded_actions.push(['as_el', selector, origin, d_now]); + saveRecordedActions(); + return; + } + else if (rec_mode === '3') { + action = 'as_et'; + var match = /\r|\n/.exec(text); + if (match) { + lines = text.split(/\r\n|\r|\n/g); + text = ''; + for (var i = 0; i < lines.length; i++) { + if (lines[i].length > 0) { + action = 'as_te'; text = lines[i]; break; + } + } + } + tex_sel = [text, selector]; + document.recorded_actions.push([action, tex_sel, origin, d_now]); + saveRecordedActions(); + return; + } + } + if (ra_len > 0 && document.recorded_actions[ra_len-1][0] === 'mo_dn') + document.recorded_actions.pop(); + if (tag_name === 'select') { + // do nothing ('change' action) + } + else + document.recorded_actions.push(['mo_dn', selector, '', d_now]); + saveRecordedActions(); +}); +document.body.addEventListener('mouseup', function (event) { + reset_if_recorder_undefined(); + if (sessionStorage.getItem('pause_recorder') === 'yes') return; + const d_now = Date.now(); + document.recorder_last_mouseup = d_now; + const el = event.target; + selector = getBestSelector(el); + ra_len = document.recorded_actions.length; + tag_name = tagName(el); + parent_el = el.parentElement; + parent_tag_name = tagName(parent_el); + grand_el = ""; + grand_tag_name = ""; + origin = ""; + rec_mode = sessionStorage.getItem('recorder_mode'); + if (rec_mode === '2' || rec_mode === '3') return; + if (parent_el.parentElement != null) { + grand_el = parent_el.parentElement; grand_tag_name = tagName(grand_el); + } + if (ra_len > 0 && + document.recorded_actions[ra_len-1][1] === selector && + (document.recorded_actions[ra_len-1][0] === 'mo_dn' || + tag_name === 'a' || parent_tag_name === 'a') && tag_name !== 'select') + { + href = ''; + if (useHref(tag_name, el)) + { + href = el.href; origin = el.origin; + } + else if (useHref(parent_tag_name, parent_el)) + { + href = parent_el.href; origin = parent_el.origin; + } + else if (useHref(grand_tag_name, grand_el)) + { + href = grand_el.href; origin = grand_el.origin; + } + document.recorded_actions.pop(); + child_count = ssOccurrences(selector, ' > '); + if ((tag_name === "a" && !el.hasAttribute('onclick') && + child_count > 0 && href.length > 0) || + (parent_tag_name === "a" && href.length > 0 && + child_count > 1 && !parent_el.hasAttribute('onclick')) || + (grand_tag_name === "a" && href.length > 0 && + child_count > 2 && !grand_el.hasAttribute('onclick'))) + { + w_orig = window.location.origin; + if (origin === w_orig) + document.recorded_actions.push(['_url_', origin, href, d_now]); + else + document.recorded_actions.push(['begin', origin, href, d_now]); + } + else + document.recorded_actions.push(['click', selector, href, d_now]); + // hover+click + if (el.parentElement.classList.contains('dropdown-content') && + el.parentElement.parentElement.classList.contains('dropdown')) + { + ch_s = selector; + pa_el = el.parentElement.parentElement; + pa_s = getBestSelector(pa_el); + if (pa_el.childElementCount >= 2 && + !pa_el.firstElementChild.classList.contains('dropdown-content')) + { + pa_el = pa_el.firstElementChild; + pa_s = getBestSelector(pa_el); + } + document.recorded_actions.pop(); + document.recorded_actions.push(['h_clk', pa_s, ch_s, d_now]); + } + else if (tag_name === 'canvas') + { + rect = el.getBoundingClientRect(); + p_x = event.clientX - rect.left; + p_y = event.clientY - rect.top; + c_offset = [selector, p_x, p_y]; + document.recorded_actions.pop(); + document.recorded_actions.push(['canva', c_offset, href, d_now]); + } + } + else if (ra_len > 0 && + document.recorded_actions[ra_len-1][0] === 'mo_dn' && + document.recorded_actions[ra_len-1][1] === selector && + tag_name === 'select') + { document.recorded_actions.pop(); } + else if (ra_len > 0 && + document.recorded_actions[ra_len-1][0] === 'mo_dn') + { + // accidental drag&drop + document.recorded_actions.pop(); + } + saveRecordedActions(); +}); +document.body.addEventListener('contextmenu', function (event) { + reset_if_recorder_undefined(); + if (sessionStorage.getItem('pause_recorder') === 'yes') return; + const el = event.target; + const selector = getBestSelector(el); + ra_len = document.recorded_actions.length; + if (ra_len > 0 && + document.recorded_actions[ra_len-1][0] === 'mo_dn' && + document.recorded_actions[ra_len-1][1] === selector) + { + document.recorded_actions.pop(); + saveRecordedActions(); + } +}); +document.body.addEventListener('keydown', function (event) { + reset_if_recorder_undefined(); + if (sessionStorage.getItem('pause_recorder') === 'yes') return; + if (document.recorded_actions.length == 0) return; + const el = event.target; + const selector = getBestSelector(el); + const d_now = Date.now(); + const tag_name = tagName(el); + const l_key = event.key.toLowerCase(); + if (l_key === 'enter' && (tag_name === 'button' || tag_name === 'a')) + { + href = ''; + if (useHref(tag_name, el)) + href = el.href; + document.recorded_actions.push(['click', selector, href, d_now]); + saveRecordedActions(); + } +}); +document.body.addEventListener('keyup', function (event) { + reset_if_recorder_undefined(); + // pause+resume controls + pause_rec = sessionStorage.getItem('pause_recorder'); + rec_mode = sessionStorage.getItem('recorder_mode'); + l_key = event.key.toLowerCase(); + no_border = 'none'; + if (l_key === 'escape' && pause_rec === 'no' && rec_mode === '1') { + sessionStorage.setItem('pause_recorder', 'yes'); + pause_rec = 'yes'; + console.log('SeleniumBase Recorder paused'); + document.querySelector('body').style.border = no_border; + document.title = sessionStorage.getItem('recorder_title'); + } + else if ((event.key === '`' || event.key === '~') && pause_rec === 'yes') { + sessionStorage.setItem('pause_recorder', 'no'); + pause_rec = 'no'; + console.log('SeleniumBase Recorder resumed'); + set_border('#F43344'); + } + else if (event.key === '^' && pause_rec === 'no') { + sessionStorage.setItem('recorder_mode', '2'); + set_border('#EF5BE9'); + } + else if (event.key === '&' && pause_rec === 'no') { + sessionStorage.setItem('recorder_mode', '3'); + set_border('#30C6C6'); + } + else if (pause_rec === 'no' && l_key !== 'shift' && l_key !== 'backspace') + { + sessionStorage.setItem('recorder_mode', '1'); + set_border('#F43344'); + } + // after switching modes + if (sessionStorage.getItem('pause_recorder') === 'yes') return; + const d_now = Date.now(); + const el = event.target; + const selector = getBestSelector(el); + skip_input = false; + if ((tagName(el) === 'input' && + el.type !== 'checkbox' && + el.type !== 'range') || + tagName(el) === 'textarea') + { + ra_len = document.recorded_actions.length; + if (ra_len > 0 && l_key === 'enter' && + document.recorded_actions[ra_len-1][0] === 'input' && + document.recorded_actions[ra_len-1][1] === selector && + !document.recorded_actions[ra_len-1][2].endsWith('\n')) + { + s_text = document.recorded_actions[ra_len-1][2] + '\n'; + document.recorded_actions.pop(); + document.recorded_actions.push(['input', selector, s_text, d_now]); + document.recorded_actions.push(['submi', selector, s_text, d_now]); + skip_input = true; + } + else if (ra_len > 0 && + document.recorded_actions[ra_len-1][0] === 'click' && + document.recorded_actions[ra_len-1][1] === selector) + { + document.recorded_actions.pop(); + } + else if (ra_len > 0 && + document.recorded_actions[ra_len-1][0] === 'input' && + document.recorded_actions[ra_len-1][1] === selector && + !document.recorded_actions[ra_len-1][2].endsWith('\n') && + l_key !== 'tab') + { + document.recorded_actions.pop(); + } + else if (ra_len > 0 && + document.recorded_actions[ra_len-1][0] === 'input' && + document.recorded_actions[ra_len-1][1] === selector && + document.recorded_actions[ra_len-1][2].endsWith('\n')) + { + skip_input = true; + } + if (!skip_input && !el.hasAttribute('readonly') && l_key !== 'tab') { + document.recorded_actions.push( + ['input', selector, el.value, d_now]); + } + } + saveRecordedActions(); +}); +set_border('#F43344'); +""" diff --git a/seleniumbase/masterqa/ReadMe.md b/seleniumbase/masterqa/ReadMe.md old mode 100755 new mode 100644 index a1a679a9174..9db88a2286f --- a/seleniumbase/masterqa/ReadMe.md +++ b/seleniumbase/masterqa/ReadMe.md @@ -1,46 +1,55 @@ -![](http://cdn2.hubspot.net/hubfs/100006/images/masterqa_logo-11.png "MasterQA") + -### MasterQA combines SeleniumBase automation with manual verification steps to bridge the gap between manual QA and automated QA. +![](https://seleniumbase.github.io/cdn/img/masterqa_logo.png "MasterQA") -![](https://cdn2.hubspot.net/hubfs/100006/images/masterqa6.gif "MasterQA") +

      MasterQA combines automation with manual verification steps.

      -Here's some example code from [basic_masterqa_test.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/basic_masterqa_test.py): +![](https://seleniumbase.github.io/cdn/gif/masterqa6.gif "MasterQA") + +Here's code from [basic_masterqa_test_0.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/master_qa/basic_masterqa_test_0.py): ```python -self.open("https://xkcd.com/1700/") -self.verify("Do you see a webcomic?") -self.highlight_click('link=Blag') -self.verify('Do you see a blog archive?') -self.highlight_update_text("input#s", "Dragons\n") -self.verify('Do you see "dragons" in the search results?') +from seleniumbase import MasterQA +MasterQA.main(__name__, __file__) + +class MasterQATests(MasterQA): + def test_masterqa(self): + self.goto("https://xkcd.com/1700/") + self.verify("Do you see a webcomic?") + self.goto("https://seleniumbase.io/demo_page") + self.highlight('table') + self.verify("Do you see elements in a table?") + self.goto("https://seleniumbase.io/devices/") + self.highlight("div.mockup-wrapper") + self.verify("Do you see 4 computer devices?") ``` -After the web browser performs various automated actions, a pop-up window will ask the user questions for each verification command. +After each automation checkpoint, a pop-up window will ask the user questions for each verification command. + +When the test run completes, as seen from [this longer example](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/master_qa/masterqa_test_1.py), you'll reach the results page that appears after answering all the verification questions. (Failed verifications generate links to screenshots and log files.) -At the end of a full test run, as seen from [this longer example](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/masterqa_test.py), you'll see a results page that appears after responding to all the verification questions. (Failed verifications generate links to screenshots and log files.) +![](https://seleniumbase.github.io/cdn/img/mqa_hybrid.png "MasterQA") -![](http://cdn2.hubspot.net/hubfs/100006/images/hybrid_screen.png "MasterQA") +You may have noticed the `Incomplete Test Runs` row on the results page. If the value for that is not zero, it means that one of the automated steps failed. This could happen if you tell your script to perform an action on an element that doesn't exist. Now that we're mixing automation with manual QA, it's good to tell apart the failures from each. The results_table CSV file contains a spreadsheet with the details of each failure (if any) for both manual and automated steps. -You may have noticed the ``Incomplete Test Runs`` row on the results page. If the value for that is not zero, it means that one of the automated steps failed. This could happen if you tell your script to perform an action on an element that doesn't exist. Now that we're mixing automation with manual QA, it's good to tell apart the failures from each. The results_table CSV file contains a spreadsheet with the details of each failure (if any) for both manual and automated steps. +**How to run the example tests from scratch:** -#### How to run the example tests from scratch: -```bash +```zsh git clone https://github.com/seleniumbase/SeleniumBase.git cd SeleniumBase -pip install -r requirements.txt --upgrade -python setup.py install +pip install . cd examples/master_qa -pytest basic_masterqa_test.py -pytest masterqa_test.py +pytest basic_masterqa_test_0.py +pytest masterqa_test_1.py ``` At the end of your test run, you'll receive a report with results, screenshots, and log files. Close the Results Page window when you're done. -### Check out [masterqa_test.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/masterqa_test.py) to learn how to write your own MasterQA tests: +**Check out [masterqa_test_1.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/master_qa/masterqa_test_1.py) to learn how to write your own MasterQA tests:** -You'll notice that tests are written based on [SeleniumBase](http://seleniumbase.com), with the key difference of using a different import: ``from seleniumbase import MasterQA`` rather than ``from seleniumbase import BaseCase``. Now the test class will import ``MasterQA`` instead of ``BaseCase``. +You'll notice that tests are written the same way as regular [SeleniumBase](https://github.com/seleniumbase/SeleniumBase) tests, with the key difference being a different import: `from seleniumbase import MasterQA` rather than `from seleniumbase import BaseCase`. Now your Python test class will import `MasterQA` instead of `BaseCase`. -To add a manual verification step, use ``self.verify()`` in the code after each part of the script that needs manual verification. If you want to include a custom question, add text inside that call (in quotes). Example: +To add a manual verification step, use `self.verify()` in the code after each part of your test that needs a manual verification step. If you want to include a custom question, add text inside that call (in quotes). Example: ```python self.verify() @@ -48,6 +57,6 @@ self.verify() self.verify("Can you find the moon?") ``` ---- +-------- -MasterQA is powered by [SeleniumBase](http://seleniumbase.com), the most advanced open-source automation framework on the [Planet](https://en.wikipedia.org/wiki/Earth). +MasterQA is powered by [SeleniumBase](https://github.com/seleniumbase/SeleniumBase). diff --git a/seleniumbase/masterqa/__init__.py b/seleniumbase/masterqa/__init__.py old mode 100755 new mode 100644 diff --git a/seleniumbase/masterqa/master_qa.py b/seleniumbase/masterqa/master_qa.py old mode 100755 new mode 100644 index bb879328c08..5240d3a28d9 --- a/seleniumbase/masterqa/master_qa.py +++ b/seleniumbase/masterqa/master_qa.py @@ -1,70 +1,134 @@ +"""Manually verify pages quickly while assisted by automation.""" import os import shutil import sys import time +from selenium.common.exceptions import NoAlertPresentException from selenium.common.exceptions import WebDriverException -from selenium.webdriver.remote.errorhandler import NoAlertPresentException from seleniumbase import BaseCase -from seleniumbase.core.style_sheet import style +from seleniumbase.core.style_sheet import get_report_style from seleniumbase.config import settings from seleniumbase.fixtures import js_utils -LATEST_REPORT_DIR = settings.LATEST_REPORT_DIR -ARCHIVE_DIR = settings.REPORT_ARCHIVE_DIR -RESULTS_PAGE = settings.HTML_REPORT -BAD_PAGE_LOG = settings.RESULTS_TABLE -DEFAULT_VALIDATION_MESSAGE = settings.MASTERQA_DEFAULT_VALIDATION_MESSAGE -WAIT_TIME_BEFORE_VERIFY = settings.MASTERQA_WAIT_TIME_BEFORE_VERIFY -START_IN_FULL_SCREEN_MODE = settings.MASTERQA_START_IN_FULL_SCREEN_MODE -MAX_IDLE_TIME_BEFORE_QUIT = settings.MASTERQA_MAX_IDLE_TIME_BEFORE_QUIT -# This tool allows testers to quickly verify pages while assisted by automation +python3_11_or_newer = False +if sys.version_info >= (3, 11): + python3_11_or_newer = True -class __MasterQATestCase__(BaseCase): +class MasterQA(BaseCase): + def setUp(self): + self.check_count = 0 + self.auto_close_results_page = False + super().setUp(masterqa_mode=True) + self.LATEST_REPORT_DIR = settings.LATEST_REPORT_DIR + self.ARCHIVE_DIR = settings.REPORT_ARCHIVE_DIR + self.RESULTS_PAGE = settings.HTML_REPORT + self.BAD_PAGE_LOG = settings.RESULTS_TABLE + self.DEFAULT_VALIDATION_TITLE = "Manual Check" + self.DEFAULT_VALIDATION_MESSAGE = ( + settings.MASTERQA_DEFAULT_VALIDATION_MESSAGE + ) + self.WAIT_TIME_BEFORE_VERIFY = ( + settings.MASTERQA_WAIT_TIME_BEFORE_VERIFY + ) + self.START_IN_FULL_SCREEN_MODE = ( + settings.MASTERQA_START_IN_FULL_SCREEN_MODE + ) + self.MAX_IDLE_TIME_BEFORE_QUIT = ( + settings.MASTERQA_MAX_IDLE_TIME_BEFORE_QUIT + ) + self.__manual_check_setup() + if self.headless: + self.auto_close_results_page = True + if self.START_IN_FULL_SCREEN_MODE: + self.maximize_window() + + def verify(self, *args): + warn_msg = "\nWARNING: MasterQA skips manual checks in headless mode!" + self.check_count += 1 + if self.headless: + if self.check_count == 1: + print(warn_msg) + return + # This is where the magic happens + self.__manual_page_check(*args) + + def auto_close_results(self): + """If this method is called, the results page will automatically close + at the end of the test run, rather than waiting on the user to close + the results page manually. + """ + self.auto_close_results_page = True - def get_timestamp(self): + def tearDown(self): + if self.headless and self.check_count > 0: + print( + "WARNING: %s manual checks were skipped! (MasterQA)" + % self.check_count + ) + if self.__has_exception(): + self.__add_failure(sys.exc_info()[1]) + self.__process_manual_check_results(self.auto_close_results_page) + super().tearDown() + + #################### + + def __get_timestamp(self): return str(int(time.time() * 1000)) - def manual_check_setup(self): + def __manual_check_setup(self): self.manual_check_count = 0 self.manual_check_successes = 0 self.incomplete_runs = 0 self.page_results_list = [] - self.clear_out_old_logs(archive_past_runs=False) + self.__clear_out_old_logs(archive_past_runs=False) - def clear_out_old_logs(self, archive_past_runs=True, get_log_folder=False): - abs_path = os.path.abspath('.') - file_path = abs_path + "/%s" % LATEST_REPORT_DIR + def __clear_out_old_logs( + self, archive_past_runs=True, get_log_folder=False + ): + abs_path = os.path.abspath(".") + file_path = os.path.join(abs_path, self.LATEST_REPORT_DIR) if not os.path.exists(file_path): os.makedirs(file_path) if archive_past_runs: archive_timestamp = int(time.time()) - if not os.path.exists("%s/../%s/" % (file_path, ARCHIVE_DIR)): - os.makedirs("%s/../%s/" % (file_path, ARCHIVE_DIR)) - archive_dir = "%s/../%s/log_%s" % ( - file_path, ARCHIVE_DIR, archive_timestamp) + archive_dir_root = os.path.join(file_path, "..", self.ARCHIVE_DIR) + if not os.path.exists(archive_dir_root): + os.makedirs(archive_dir_root) + archive_dir = os.path.join( + archive_dir_root, "log_%s" % archive_timestamp + ) shutil.move(file_path, archive_dir) os.makedirs(file_path) if get_log_folder: return archive_dir else: + latest_report_local = os.path.join(".", self.LATEST_REPORT_DIR) # Just delete bad pages to make room for the latest run. - filelist = [f for f in os.listdir( - "./%s" % LATEST_REPORT_DIR) if f.startswith("failed_") or ( - f == RESULTS_PAGE) or (f.startswith("automation_failure")) or ( - f == BAD_PAGE_LOG)] + filelist = [ + f + for f in os.listdir(latest_report_local) + if (f.startswith("failed_")) + or (f == self.RESULTS_PAGE) + or (f.startswith("automation_failure")) + or (f == self.BAD_PAGE_LOG) + ] for f in filelist: - os.remove("%s/%s" % (file_path, f)) + os.remove(os.path.join(file_path, f)) - def jq_confirm_dialog(self, question): + def __jq_confirm_dialog(self, question): count = self.manual_check_count + 1 - title_content = ('
      Manual Check #%s:' - '

      %s' - '' % (count, question)) + title = self.DEFAULT_VALIDATION_TITLE + title_content = ( + '
      %s #%s:' + '

      %s' + "" % (title, count, question) + ) title_content = js_utils.escape_quotes_if_needed(title_content) - jqcd = ("""jconfirm({ + jqcd = ( + """jconfirm({ boxWidth: '32.5%%', useBootstrap: false, containerFluid: false, @@ -79,27 +143,33 @@ def jq_confirm_dialog(self, question): title: '%s', content: '', buttons: { - fail_button: { - btnClass: 'btn-red', - text: 'NO / FAIL', - action: function(){ - $jqc_status = "Failure!" - } - }, pass_button: { btnClass: 'btn-green', text: 'YES / PASS', + keys: ['y', 'p', '1'], + action: function(){ + $jqc_status = "Success!"; + jconfirm.lastButtonText = "Success!"; + } + }, + fail_button: { + btnClass: 'btn-red', + text: 'NO / FAIL', + keys: ['n', 'f', '2'], action: function(){ - $jqc_status = "Success!" + $jqc_status = "Failure!"; + jconfirm.lastButtonText = "Failure!"; } } } - });""" % title_content) + });""" + % title_content + ) self.execute_script(jqcd) - def manual_page_check(self, *args): + def __manual_page_check(self, *args): if not args: - instructions = DEFAULT_VALIDATION_MESSAGE # self.verify() + instructions = self.DEFAULT_VALIDATION_MESSAGE # self.verify() else: instructions = str(args[0]) if len(args) > 1: @@ -111,6 +181,12 @@ def manual_page_check(self, *args): elif instructions and "?" in instructions: question = instructions + wait_time_before_verify = self.WAIT_TIME_BEFORE_VERIFY + if self.verify_delay: + wait_time_before_verify = float(self.verify_delay) + # Allow a moment to see the full page before the dialog box pops up + time.sleep(wait_time_before_verify) + use_jqc = False self.wait_for_ready_state_complete() if js_utils.is_jquery_confirm_activated(self.driver): @@ -120,26 +196,22 @@ def manual_page_check(self, *args): get_jqc = None try: get_jqc = self.execute_script("return jconfirm") - get_jqc = get_jqc["instances"] + if get_jqc is None: + raise Exception("jconfirm did not load") use_jqc = True except Exception: use_jqc = False if use_jqc: - wait_time_before_verify = WAIT_TIME_BEFORE_VERIFY - if self.verify_delay: - wait_time_before_verify = float(self.verify_delay) - # Allow a moment to see the full page before the dialog box pops up - time.sleep(wait_time_before_verify) - # Use the jquery_confirm library for manual page checks - self.jq_confirm_dialog(question) + self.__jq_confirm_dialog(question) time.sleep(0.02) waiting_for_response = True while waiting_for_response: time.sleep(0.05) jqc_open = self.execute_script( - "return jconfirm.instances.length") + "return jconfirm.instances.length" + ) if str(jqc_open) == "0": break time.sleep(0.1) @@ -147,36 +219,39 @@ def manual_page_check(self, *args): try: status = self.execute_script("return $jqc_status") except Exception: - status = "Failure!" - pre_status = self.execute_script( - "return jconfirm.lastClicked.hasClass('btn-green')") - if pre_status: - status = "Success!" + status = self.execute_script("return jconfirm.lastButtonText") else: # Fallback to plain js confirm dialogs if can't load jquery_confirm - if self.browser == 'ie': + if self.browser == "ie": text = self.execute_script( - '''if(confirm("%s")){return "Success!"} - else{return "Failure!"}''' % question) - elif self.browser == 'chrome': - self.execute_script('''if(confirm("%s")) + """if(confirm("%s")){return "Success!"} + else{return "Failure!"}""" + % question + ) + elif self.browser == "chrome": + self.execute_script( + """if(confirm("%s")) {window.master_qa_result="Success!"} - else{window.master_qa_result="Failure!"}''' % question) + else{window.master_qa_result="Failure!"}""" + % question + ) time.sleep(0.05) - self.wait_for_special_alert_absent() - text = self.execute_script('return window.master_qa_result') + self.__wait_for_special_alert_absent() + text = self.execute_script("return window.master_qa_result") else: try: self.execute_script( - '''if(confirm("%s")) + """if(confirm("%s")) {window.master_qa_result="Success!"} - else{window.master_qa_result="Failure!"}''' % question) + else{window.master_qa_result="Failure!"}""" + % question + ) except WebDriverException: # Fix for https://github.com/mozilla/geckodriver/issues/431 pass time.sleep(0.05) - self.wait_for_special_alert_absent() - text = self.execute_script('return window.master_qa_result') + self.__wait_for_special_alert_absent() + text = self.execute_script("return window.master_qa_result") status = text self.manual_check_count += 1 @@ -187,32 +262,39 @@ def manual_page_check(self, *args): if "Success!" in str(status): self.manual_check_successes += 1 self.page_results_list.append( - '"%s","%s","%s","%s","%s","%s","%s","%s"' % ( + '"%s","%s","%s","%s","%s","%s","%s","%s"' + % ( self.manual_check_count, "Success", "-", current_url, self.browser, - self.get_timestamp()[:-3], + self.__get_timestamp()[:-3], instructions, - "*")) + "*", + ) + ) return 1 else: bad_page_name = "failed_check_%s.png" % self.manual_check_count - self.save_screenshot(bad_page_name, folder=LATEST_REPORT_DIR) + self.save_screenshot(bad_page_name, folder=self.LATEST_REPORT_DIR) self.page_results_list.append( - '"%s","%s","%s","%s","%s","%s","%s","%s"' % ( + '"%s","%s","%s","%s","%s","%s","%s","%s"' + % ( self.manual_check_count, "FAILED!", bad_page_name, current_url, self.browser, - self.get_timestamp()[:-3], + self.__get_timestamp()[:-3], instructions, - "*")) + "*", + ) + ) return 0 - def wait_for_special_alert_absent(self, timeout=MAX_IDLE_TIME_BEFORE_QUIT): + def __wait_for_special_alert_absent(self): + timeout = self.MAX_IDLE_TIME_BEFORE_QUIT for x in range(int(timeout * 20)): try: alert = self.driver.switch_to.alert @@ -224,31 +306,55 @@ def wait_for_special_alert_absent(self, timeout=MAX_IDLE_TIME_BEFORE_QUIT): return self.driver.quit() raise Exception( - "%s seconds passed without human action! Stopping..." % timeout) + "%s seconds passed without human action! Stopping..." % timeout + ) + + def __has_exception(self): + has_exception = False + if hasattr(sys, "last_traceback") and sys.last_traceback is not None: + has_exception = True + elif hasattr(self, "_outcome"): + if hasattr(self._outcome, "errors"): + if python3_11_or_newer: + if ( + self._outcome.errors + and self._outcome.errors[-1] + and self._outcome.errors[-1][1] + ): + has_exception = True + else: + if self._outcome.errors: + has_exception = True + else: + has_exception = sys.exc_info()[1] is not None + return has_exception - def add_failure(self, exception=None): + def __add_failure(self, exception=None): exc_info = None if exception: - if hasattr(exception, 'msg'): + if hasattr(exception, "msg"): exc_info = exception.msg - elif hasattr(exception, 'message'): + elif hasattr(exception, "message"): exc_info = exception.message else: - exc_info = '(Unknown Exception)' + exc_info = "(Unknown Exception)" self.incomplete_runs += 1 error_page = "automation_failure_%s.png" % self.incomplete_runs - self.save_screenshot(error_page, folder=LATEST_REPORT_DIR) + self.save_screenshot(error_page, folder=self.LATEST_REPORT_DIR) self.page_results_list.append( - '"%s","%s","%s","%s","%s","%s","%s","%s"' % ( + '"%s","%s","%s","%s","%s","%s","%s","%s"' + % ( "ERR", "ERROR!", error_page, self.driver.current_url, self.browser, - self.get_timestamp()[:-3], + self.__get_timestamp()[:-3], "-", - exc_info)) + exc_info, + ) + ) try: # Return to the original window if another was opened self.driver.switch_to_window(self.driver.window_handles[1]) @@ -257,37 +363,39 @@ def add_failure(self, exception=None): except Exception: pass - def add_bad_page_log_file(self): - abs_path = os.path.abspath('.') - file_path = abs_path + "/%s" % LATEST_REPORT_DIR - log_file = "%s/%s" % (file_path, BAD_PAGE_LOG) - f = open(log_file, 'w') - h_p1 = '''"Num","Result","Screenshot","URL","Browser","Epoch Time",''' - h_p2 = '''"Verification Instructions","Additional Info"\n''' + def __add_bad_page_log_file(self): + abs_path = os.path.abspath(".") + file_path = os.path.join(abs_path, self.LATEST_REPORT_DIR) + log_file = os.path.join(file_path, self.BAD_PAGE_LOG) + f = open(log_file, "w") + h_p1 = """"Num","Result","Screenshot","URL","Browser","Epoch Time",""" + h_p2 = """"Verification Instructions","Additional Info"\n""" page_header = h_p1 + h_p2 f.write(page_header) for line in self.page_results_list: f.write("%s\n" % line) f.close() - def add_results_page(self, html): - abs_path = os.path.abspath('.') - file_path = abs_path + "/%s" % LATEST_REPORT_DIR - results_file_name = RESULTS_PAGE - results_file = "%s/%s" % (file_path, results_file_name) - f = open(results_file, 'w') + def __add_results_page(self, html): + abs_path = os.path.abspath(".") + file_path = os.path.join(abs_path, self.LATEST_REPORT_DIR) + results_file_name = self.RESULTS_PAGE + results_file = os.path.join(file_path, results_file_name) + f = open(results_file, "w") f.write(html) f.close() return results_file - def process_manual_check_results(self, auto_close_results_page=False): + def __process_manual_check_results(self, auto_close_results_page=False): perfection = True failures_count = self.manual_check_count - self.manual_check_successes - print("\n\n*** Test Result: ***") + if not self.headless: + print("") + print("\n*** MasterQA Manual Test Results: ***") if self.manual_check_successes == self.manual_check_count: pass else: - print("WARNING: There were page issues detected!") + print("WARNING: Not all tests passed manual inspection!") perfection = False if self.incomplete_runs > 0: @@ -301,13 +409,13 @@ def process_manual_check_results(self, auto_close_results_page=False): print("WARNING: No manual checks were performed!") else: pass - self.add_bad_page_log_file() # Includes successful results + self.__add_bad_page_log_file() # Includes successful results - log_string = self.clear_out_old_logs(get_log_folder=True) - log_folder = log_string.split('/')[-1] - abs_path = os.path.abspath('.') - file_path = abs_path + "/%s" % ARCHIVE_DIR - log_path = "%s/%s" % (file_path, log_folder) + log_string = self.__clear_out_old_logs(get_log_folder=True) + log_folder = log_string.split(os.sep)[-1] + abs_path = os.path.abspath(".") + file_path = os.path.join(abs_path, self.ARCHIVE_DIR) + log_path = os.path.join(file_path, log_folder) web_log_path = "file://%s" % log_path tf_color = "#11BB11" @@ -318,7 +426,7 @@ def process_manual_check_results(self, auto_close_results_page=False): if self.incomplete_runs > 0: ir_color = "#EE3A3A" - summary_table = '''
      + summary_table = """
      @@ -326,56 +434,75 @@ def process_manual_check_results(self, auto_close_results_page=False): -
      TESTING SUMMARY         
      CHECKS FAILED: %s
      TOTAL VERIFICATIONS: %s
      INCOMPLETE TEST RUNS: %s
      ''' % (self.manual_check_successes, - tf_color, - failures_count, - self.manual_check_count, - ir_color, - self.incomplete_runs) - - summary_table = '''

      - %s

      ''' % summary_table - - log_link_shown = '../%s%s/' % ( - ARCHIVE_DIR, web_log_path.split(ARCHIVE_DIR)[1]) - csv_link = '%s/%s' % (web_log_path, BAD_PAGE_LOG) - csv_link_shown = '%s' % BAD_PAGE_LOG - log_table = '''

      +
      """ % ( + self.manual_check_successes, + tf_color, + failures_count, + self.manual_check_count, + ir_color, + self.incomplete_runs, + ) + + summary_table = ( + """

      + %s

      """ + % summary_table + ) + + log_link_shown = os.path.join( + "..", "%s%s" % ( + self.ARCHIVE_DIR, web_log_path.split(self.ARCHIVE_DIR)[1] + ) + ) + csv_link = os.path.join(web_log_path, self.BAD_PAGE_LOG) + csv_link_shown = "%s" % self.BAD_PAGE_LOG + log_table = """

      -
      LOG FILES LINK:  %s
      RESULTS TABLE:  %s

      ''' % ( - web_log_path, log_link_shown, csv_link, csv_link_shown) - - failure_table = '

      ' +

      """ % ( + web_log_path, + log_link_shown, + csv_link, + csv_link_shown, + ) + + failure_table = "

      " any_screenshots = False for line in self.page_results_list: - line = line.split(',') + line = line.split(",") if line[1] == '"FAILED!"' or line[1] == '"ERROR!"': if not any_screenshots: any_screenshots = True - failure_table += ''' + failure_table += """ - ''' + """ display_url = line[3] if len(display_url) > 60: - display_url = display_url[0:58] + '...' - line = '%s' % ( - "file://" + log_path + '/' + line[2], line[2]) + ''' + display_url = display_url[0:58] + "..." + line = ( + '%s' + % ("file://" + log_path + "/" + line[2], line[2]) + + """     \n' % line - failure_table += '
      SCREENSHOT FILE     LOCATION OF FAILURE
      - ''' + '%s' % (line[3], display_url) - line = line.replace('"', '') - failure_table += '
      %s
      ' - table_view = '%s%s%s' % ( - summary_table, log_table, failure_table) - report_html = '%s%s' % ( - style, table_view) - results_file = self.add_results_page(report_html) - archived_results_file = log_path + '/' + RESULTS_PAGE - shutil.copyfile(results_file, archived_results_file) - print( - "\n*** The results html page is located at: ***\n" + results_file) + """ + + '%s' % (line[3], display_url) + ) + line = line.replace('"', "") + failure_table += "%s\n" % line + failure_table += "" + table_view = "%s%s%s" % (summary_table, log_table, failure_table) + report_html = "%s%s" % ( + get_report_style(), + table_view, + ) + results_file = self.__add_results_page(report_html) + archived_results_file = os.path.join(log_path, self.RESULTS_PAGE) + shutil.copyfile(results_file, os.path.realpath(archived_results_file)) + if self.manual_check_count > 0: + print( + "\n*** The manual test report is located at:\n" + results_file + ) self.open("file://%s" % archived_results_file) if auto_close_results_page: # Long enough to notice the results before closing the page @@ -383,43 +510,8 @@ def process_manual_check_results(self, auto_close_results_page=False): else: # The user can decide when to close the results page print("\n*** Close the html report window to continue ***") - while len(self.driver.window_handles): - time.sleep(0.1) - - -class MasterQA(__MasterQATestCase__): - - def setUp(self): - self.check_count = 0 - self.auto_close_results_page = False - super(__MasterQATestCase__, self).setUp(masterqa_mode=True) - self.manual_check_setup() - if self.headless: - self.auto_close_results_page = True - if START_IN_FULL_SCREEN_MODE: - self.maximize_window() - - def verify(self, *args): - warn_msg = "\nWARNING: MasterQA skips manual checks in headless mode!" - self.check_count += 1 - if self.headless: - if self.check_count == 1: - print(warn_msg) - return - # This is where the magic happens - self.manual_page_check(*args) - - def auto_close_results(self): - ''' If this method is called, the results page will automatically close - at the end of the test run, rather than waiting on the user to close - the results page manually. - ''' - self.auto_close_results_page = True - - def tearDown(self): - if self.headless and self.check_count > 0: - print("WARNING: %s manual checks were skipped!" % self.check_count) - if sys.exc_info()[1]: - self.add_failure(sys.exc_info()[1]) - self.process_manual_check_results(self.auto_close_results_page) - super(__MasterQATestCase__, self).tearDown() + try: + while len(self.driver.window_handles): + time.sleep(0.1) + except Exception: + pass diff --git a/seleniumbase/plugins/__init__.py b/seleniumbase/plugins/__init__.py old mode 100755 new mode 100644 diff --git a/seleniumbase/plugins/base_plugin.py b/seleniumbase/plugins/base_plugin.py old mode 100755 new mode 100644 index 200b403b182..0a1bd5b2251 --- a/seleniumbase/plugins/base_plugin.py +++ b/seleniumbase/plugins/base_plugin.py @@ -1,85 +1,203 @@ -""" -This is the Nose plugin for saving logs and setting a test environment. -Vars include "env" and "log_path". -You can have tests behave differently based on the environment. -You can access the values of these variables from the tests. -""" - -import os +"""Base Plugin for SeleniumBase tests that run with pynose / nosetests""" +import ast import sys import time +from contextlib import suppress from nose.plugins import Plugin -from nose.exc import SkipTest +from seleniumbase import config as sb_config +from seleniumbase.config import settings +from seleniumbase.core import download_helper from seleniumbase.core import log_helper from seleniumbase.core import report_helper -from seleniumbase.fixtures import constants, errors +from seleniumbase.fixtures import constants + +python3_11_or_newer = False +if sys.version_info >= (3, 11): + python3_11_or_newer = True +py311_patch2 = constants.PatchPy311.PATCH = True class Base(Plugin): - """ - The base_plugin includes the following command-line options for nosetests: - --env=ENV (Set a test environment. Use "self.env" to access env in tests.) - --data=DATA (Extra data to pass to tests. Use "self.data" in tests.) - --log_path=LOG_PATH (The directory where log files get saved to.) - --archive_logs (Archive old log files instead of deleting them.) - --report (The option to create a fancy report after tests complete.) - --show_report If self.report is turned on, then the report will + """This plugin adds the following command-line options to pynose: + --env=ENV (Set the test env. Access with "self.env" in tests.) + --account=STR (Set account. Access with "self.account" in tests.) + --data=STRING (Extra test data. Access with "self.data" in tests.) + --var1=STRING (Extra test data. Access with "self.var1" in tests.) + --var2=STRING (Extra test data. Access with "self.var2" in tests.) + --var3=STRING (Extra test data. Access with "self.var3" in tests.) + --variables=DICT (Extra test data. Access with "self.variables".) + --settings-file=FILE (Override default SeleniumBase settings.) + --ftrace | --final-trace (Enter Debug Mode after any test ends.) + --archive-logs (Archive old log files instead of deleting them.) + --archive-downloads (Archive old downloads instead of deleting.) + --report (Create a fancy nosetests report after tests complete.) + --show-report If self.report is turned on, then the report will display immediately after tests complete their run. Only use this when running tests locally, as this will pause the test run until the report window is closed. """ - name = 'testing_base' # Usage: --with-testing_base (Enabled by default) + name = "testing_base" # Usage: --with-testing_base (Enabled by default) def options(self, parser, env): - super(Base, self).options(parser, env=env) - parser.add_option( - '--env', action='store', - dest='environment', + super().options(parser, env=env) + parser.addoption = parser.add_option # Reuse name from pytest parser + parser.addoption( + "--env", + action="store", + dest="environment", choices=( constants.Environment.QA, + constants.Environment.RC, constants.Environment.STAGING, constants.Environment.DEVELOP, constants.Environment.PRODUCTION, + constants.Environment.PERFORMANCE, + constants.Environment.REPLICA, + constants.Environment.FEDRAMP, + constants.Environment.OFFLINE, + constants.Environment.ONLINE, constants.Environment.MASTER, + constants.Environment.REMOTE, + constants.Environment.LEGACY, constants.Environment.LOCAL, - constants.Environment.TEST), + constants.Environment.ALPHA, + constants.Environment.BETA, + constants.Environment.DEMO, + constants.Environment.GDPR, + constants.Environment.MAIN, + constants.Environment.TEST, + constants.Environment.GOV, + constants.Environment.NEW, + constants.Environment.OLD, + constants.Environment.UAT, + ), default=constants.Environment.TEST, - help="The environment to run the tests in.") - parser.add_option( - '--data', dest='data', + help="""This option sets a test env from a list of choices. + Access using "self.env" or "self.environment".""", + ) + parser.addoption( + "--account", + dest="account", + default=None, + help="""This option sets a test account string. + In tests, use "self.account" to get the value.""", + ) + parser.addoption( + "--data", + dest="data", + default=None, + help="Extra data to pass to tests from the command line.", + ) + parser.addoption( + "--var1", + dest="var1", + default=None, + help="Extra data to pass to tests from the command line.", + ) + parser.addoption( + "--var2", + dest="var2", + default=None, + help="Extra data to pass to tests from the command line.", + ) + parser.addoption( + "--var3", + dest="var3", + default=None, + help="Extra data to pass to tests from the command line.", + ) + parser.addoption( + "--variables", + dest="variables", + default=None, + help="""A var dict to pass to tests from the command line. + Example usage: + ---------------------------------------------- + Option: --variables='{"special":123}' + Access: self.variables["special"] + ---------------------------------------------- + Option: --variables='{"color":"red","num":42}' + Access: self.variables["color"] + Access: self.variables["num"] + ----------------------------------------------""", + ) + parser.addoption( + "--settings_file", + "--settings-file", + "--settings", + action="store", + dest="settings_file", default=None, - help='Extra data to pass from the command line.') - parser.add_option( - '--log_path', dest='log_path', - default='latest_logs/', - help='Where the log files are saved.') - parser.add_option( - '--archive_logs', action="store_true", - dest='archive_logs', + help="""The file that stores key/value pairs for overriding + values in the SeleniumBase settings.py file.""", + ) + parser.addoption( + "--final-debug", + "--final-trace", + "--fdebug", + "--ftrace", + action="store_true", + dest="final_debug", default=False, - help="Archive old log files instead of deleting them.") - parser.add_option( - '--report', action="store_true", dest='report', + help="""Enter Debug Mode at the end of each test. + To enter Debug Mode only on failures, use "--pdb". + If using both "--final-debug" and "--pdb" together, + then Debug Mode will activate twice on failures.""", + ) + parser.addoption( + "--log_path", + "--log-path", + dest="log_path", + default=constants.Logs.LATEST + "/", + help="""(DEPRECATED) - This field is NOT EDITABLE anymore. + Log files are saved to the "latest_logs/" folder.""", + ) + parser.addoption( + "--archive_logs", + "--archive-logs", + action="store_true", + dest="archive_logs", default=False, - help='Create a fancy report at the end of the test suite.') - parser.add_option( - '--show_report', action="store_true", - dest='show_report', + help="Archive old log files instead of deleting them.", + ) + parser.addoption( + "--archive_downloads", + "--archive-downloads", + action="store_true", + dest="archive_downloads", default=False, - help="If true when using report, will display it after tests run.") + help="Archive old downloads instead of deleting them.", + ) + parser.addoption( + "--report", + action="store_true", + dest="report", + default=False, + help="Create a fancy report at the end of the test suite.", + ) + parser.addoption( + "--show_report", + "--show-report", + action="store_true", + dest="show_report", + default=False, + help="If true when using report, will display it after tests run.", + ) found_processes_arg = False for arg in sys.argv: - if "--processes=" in arg: + if "--processes=" in arg or "--processes" in arg: found_processes_arg = True if found_processes_arg: print("* WARNING: Don't use multi-threading with nosetests! *") - parser.add_option( - '--processes', dest='processes', + parser.addoption( + "--processes", + dest="processes", default=0, - help="WARNING: Don't use multi-threading with nosetests!") + help="WARNING: Don't use multi-threading with nosetests!", + ) def configure(self, options, conf): - super(Base, self).configure(options, conf) + super().configure(options, conf) self.enabled = True # Used if test class inherits BaseCase self.options = options self.report_on = options.report @@ -90,111 +208,169 @@ def configure(self, options, conf): self.duration = float(0) self.page_results_list = [] self.test_count = 0 - self.import_error = False - log_path = options.log_path + log_path = constants.Logs.LATEST + "/" archive_logs = options.archive_logs log_helper.log_folder_setup(log_path, archive_logs) + download_helper.reset_downloads_folder() + sb_config.is_nosetest = True if self.report_on: report_helper.clear_out_old_report_logs(archive_past_runs=False) def beforeTest(self, test): - test_logpath = self.options.log_path + "/" + test.id() - if not os.path.exists(test_logpath): - os.makedirs(test_logpath) + sb_config._context_of_runner = False # Context Manager Compatibility + variables = self.options.variables + if variables and isinstance(variables, str) and len(variables) > 0: + bad_input = False + if not variables.startswith("{") or not variables.endswith("}"): + bad_input = True + else: + try: + variables = ast.literal_eval(variables) + if not isinstance(variables, dict): + bad_input = True + except Exception: + bad_input = True + if bad_input: + raise Exception( + '\nExpecting a Python dictionary for "variables"!' + "\nEg. --variables=\"{'KEY1':'VALUE', 'KEY2':123}\"" + ) + else: + variables = {} + test.test.test_id = test.id() + test.test.is_nosetest = True test.test.environment = self.options.environment + sb_config.environment = self.options.environment test.test.env = self.options.environment # Add a shortened version + test.test.account = self.options.account + sb_config.account = self.options.account test.test.data = self.options.data + sb_config.data = self.options.data + test.test.var1 = self.options.var1 + sb_config.var1 = self.options.var1 + test.test.var2 = self.options.var2 + sb_config.var2 = self.options.var2 + test.test.var3 = self.options.var3 + sb_config.var3 = self.options.var3 + test.test.variables = variables # Already verified is a dictionary + sb_config.variables = variables + test.test.settings_file = self.options.settings_file + sb_config.settings_file = self.options.settings_file + test.test._final_debug = self.options.final_debug + test.test.log_path = self.options.log_path + if self.options.archive_downloads: + settings.ARCHIVE_EXISTING_DOWNLOADS = True test.test.args = self.options test.test.report_on = self.report_on self.test_count += 1 self.start_time = float(time.time()) def finalize(self, result): + log_helper.archive_logs_if_set( + self.options.log_path, self.options.archive_logs + ) + log_helper.clear_empty_logs() if self.report_on: - if not self.import_error: - report_helper.add_bad_page_log_file(self.page_results_list) - report_log_path = report_helper.archive_new_report_logs() - report_helper.build_report( - report_log_path, - self.page_results_list, - self.successes, self.failures, - self.options.browser, - self.show_report) - - def __log_all_options_if_none_specified(self, test): - """ - When testing_base is specified, but none of the log options to save are - specified (basic_test_info, screen_shots, page_source), then save them - all by default. Otherwise, save only selected ones from their plugins. - """ - if ((not self.options.enable_plugin_basic_test_info) and ( - not self.options.enable_plugin_screen_shots) and ( - not self.options.enable_plugin_page_source)): - test_logpath = self.options.log_path + "/" + test.id() - log_helper.log_screenshot(test_logpath, test.driver) - log_helper.log_test_failure_data( - test, test_logpath, test.driver, test.browser) - log_helper.log_page_source(test_logpath, test.driver) + report_helper.add_bad_page_log_file(self.page_results_list) + report_log_path = report_helper.archive_new_report_logs() + report_helper.build_report( + report_log_path, + self.page_results_list, + self.successes, + self.failures, + self.options.browser, + self.show_report, + ) def addSuccess(self, test, capt): if self.report_on: self.duration = str( - "%0.3fs" % (float(time.time()) - float(self.start_time))) + "%.2fs" % (float(time.time()) - float(self.start_time)) + ) self.successes.append(test.id()) self.page_results_list.append( report_helper.process_successes( - test, self.test_count, self.duration)) + test, self.test_count, self.duration + ) + ) - def add_fails_or_errors(self, test): + def add_fails_or_errors(self, test, err): if self.report_on: self.duration = str( - "%0.3fs" % (float(time.time()) - float(self.start_time))) - if test.id() == 'nose.failure.Failure.runTest': - print(">>> ERROR: Could not locate tests to run!") - print(">>> The Test Report WILL NOT be generated!") - self.import_error = True + "%.2fs" % (float(time.time()) - float(self.start_time)) + ) + if test.id() == "nose.failure.Failure.runTest": return self.failures.append(test.id()) - br = self.options.browser self.page_results_list.append( report_helper.process_failures( - test, self.test_count, br, self.duration)) + test, self.test_count, self.duration + ) + ) + if python3_11_or_newer and py311_patch2: + # Handle a bug on Python 3.11 where exceptions aren't seen + sb_config._browser_version = None + with suppress(Exception): + test._BaseCase__set_last_page_screenshot() + test._BaseCase__set_last_page_url() + test._BaseCase__set_last_page_source() + sb_config._browser_version = test._get_browser_version() + test._log_fail_data() + sb_config._excinfo_tb = err + log_path = None + source = None + if hasattr(sb_config, "_test_logpath"): + log_path = sb_config._test_logpath + if hasattr(sb_config, "_last_page_source"): + source = sb_config._last_page_source + if log_path and source: + log_helper.log_page_source(log_path, None, source) + last_page_screenshot_png = None + if hasattr(sb_config, "_last_page_screenshot_png"): + last_page_screenshot_png = sb_config._last_page_screenshot_png + if log_path and last_page_screenshot_png: + log_helper.log_screenshot( + log_path, None, last_page_screenshot_png + ) def addFailure(self, test, err, capt=None, tbinfo=None): - # self.__log_all_options_if_none_specified(test) - self.add_fails_or_errors(test) + self.add_fails_or_errors(test, err) def addError(self, test, err, capt=None): - """ - Since Skip, Blocked, and Deprecated are all technically errors, but not - error states, we want to make sure that they don't show up in - the nose output as errors. - """ - if (err[0] == errors.BlockedTest or ( - err[0] == errors.SkipTest) or ( - err[0] == errors.DeprecatedTest)): - print(err[1].__str__().split('''-------------------- >> ''' - '''begin captured logging''' - ''' << --------------------''', 1)[0]) + """Since Skip, Blocked, and Deprecated are all technically errors, + but not error states, we want to make sure that they + don't show up in the nose output as errors.""" + from seleniumbase.fixtures import errors + + if ( + err[0] == errors.BlockedTest + or (err[0] == errors.SkipTest) + or (err[0] == errors.DeprecatedTest) + ): + print( + err[1] + .__str__() + .split( + """-------------------- >> """ + """begin captured logging""" + """ << --------------------""", + 1, + )[0] + ) else: - # self.__log_all_options_if_none_specified(test) pass - self.add_fails_or_errors(test) + self.add_fails_or_errors(test, err) def handleError(self, test, err, capt=None): - """ - If the database plugin is not present, we have to handle capturing - "errors" that shouldn't be reported as such in base. - """ + """After each test error, record testcase run information. + "Error" also encompasses any states other than Pass or Fail.""" + from nose.exc import SkipTest + from seleniumbase.fixtures import errors + if not hasattr(test.test, "testcase_guid"): if err[0] == errors.BlockedTest: raise SkipTest(err[1]) - return True - elif err[0] == errors.DeprecatedTest: raise SkipTest(err[1]) - return True - elif err[0] == errors.SkipTest: raise SkipTest(err[1]) - return True diff --git a/seleniumbase/plugins/basic_test_info.py b/seleniumbase/plugins/basic_test_info.py old mode 100755 new mode 100644 index 569a4f25b0d..d46e8160f1c --- a/seleniumbase/plugins/basic_test_info.py +++ b/seleniumbase/plugins/basic_test_info.py @@ -1,36 +1,21 @@ -""" -The plugin for saving basic test info to the logs for Selenium tests. -The created file will be saved in the default logs folder (in .../logs) -Data to be saved includes: -* Last page url -* Browser -* Server -* Error -* Traceback -""" - +"""Test Info Plugin for SeleniumBase tests that run with pynose / nosetests""" import os -import codecs +import time import traceback from nose.plugins import Plugin from seleniumbase.config import settings class BasicTestInfo(Plugin): - """ - This plugin will capture basic info when a test fails or - raises an error. It will store that basic test info in - the default logs or in the file specified by the user. - """ + """This plugin captures basic info when a test fails or raises an error.""" name = "basic_test_info" # Usage: --with-basic_test_info - logfile_name = settings.BASIC_INFO_NAME def options(self, parser, env): - super(BasicTestInfo, self).options(parser, env=env) + super().options(parser, env=env) def configure(self, options, conf): - super(BasicTestInfo, self).configure(options, conf) + super().configure(options, conf) if not self.enabled: return self.options = options @@ -40,7 +25,7 @@ def addError(self, test, err, capt=None): if not os.path.exists(test_logpath): os.makedirs(test_logpath) file_name = "%s/%s" % (test_logpath, self.logfile_name) - basic_info_file = codecs.open(file_name, "w+", "utf-8") + basic_info_file = open(file_name, mode="w+", encoding="utf-8") self.__log_test_error_data(basic_info_file, test, err, "Error") basic_info_file.close() @@ -49,16 +34,18 @@ def addFailure(self, test, err, capt=None, tbinfo=None): if not os.path.exists(test_logpath): os.makedirs(test_logpath) file_name = "%s/%s" % (test_logpath, self.logfile_name) - basic_info_file = codecs.open(file_name, "w+", "utf-8") + basic_info_file = open(file_name, mode="w+", encoding="utf-8") self.__log_test_error_data(basic_info_file, test, err, "Error") basic_info_file.close() def __log_test_error_data(self, log_file, test, err, type): data_to_save = [] - data_to_save.append("Last_Page: %s" % test.driver.current_url) - data_to_save.append("Browser: %s " % self.options.browser) + data_to_save.append("Last Page: %s" % test.driver.current_url) + data_to_save.append(" Browser: %s" % self.options.browser) + data_to_save.append("Timestamp: %s" % int(time.time())) data_to_save.append("Server: %s " % self.options.servername) data_to_save.append("%s: %s" % (type, err[0])) - data_to_save.append("Traceback: " + ''.join( - traceback.format_exception(*err))) + data_to_save.append( + "Traceback: " + "".join(traceback.format_exception(*err)) + ) log_file.writelines("\r\n".join(data_to_save)) diff --git a/seleniumbase/plugins/db_reporting_plugin.py b/seleniumbase/plugins/db_reporting_plugin.py old mode 100755 new mode 100644 index 5278edfd313..45b47b32bcf --- a/seleniumbase/plugins/db_reporting_plugin.py +++ b/seleniumbase/plugins/db_reporting_plugin.py @@ -1,26 +1,13 @@ -""" -This plugin is for recording test results in the Testcase Database. -""" - -import getpass +"""DB Reporting Plugin for SeleniumBase tests that use pynose / nosetests""" import time import uuid -from optparse import SUPPRESS_HELP from nose.plugins import Plugin -from nose.exc import SkipTest -from seleniumbase.core.application_manager import ApplicationManager -from seleniumbase.core.testcase_manager import ExecutionQueryPayload -from seleniumbase.core.testcase_manager import TestcaseDataPayload -from seleniumbase.core.testcase_manager import TestcaseManager from seleniumbase.fixtures import constants -from seleniumbase.fixtures import errors class DBReporting(Plugin): - """ - This plugin records test results in the Testcase Database. - """ - name = 'db_reporting' # Usage: --with-db_reporting + """This plugin records test results in the Testcase Database.""" + name = "db_reporting" # Usage: --with-db_reporting def __init__(self): Plugin.__init__(self) @@ -28,27 +15,60 @@ def __init__(self): self.testcase_guid = None self.execution_start_time = 0 self.case_start_time = 0 - self.application = None self.testcase_manager = None - self.error_handled = False + self._result_set = False + self._test = None def options(self, parser, env): - super(DBReporting, self).options(parser, env=env) - parser.add_option('--database_environment', action='store', - dest='database_env', - choices=('production', 'qa', 'staging', 'develop', - 'test', 'local', 'master'), - default='test', - help=SUPPRESS_HELP) + super().options(parser, env=env) + parser.add_option( + "--database_env", + "--database-env", + action="store", + dest="database_env", + choices=( + constants.Environment.QA, + constants.Environment.RC, + constants.Environment.STAGING, + constants.Environment.DEVELOP, + constants.Environment.PRODUCTION, + constants.Environment.PERFORMANCE, + constants.Environment.REPLICA, + constants.Environment.FEDRAMP, + constants.Environment.OFFLINE, + constants.Environment.ONLINE, + constants.Environment.MASTER, + constants.Environment.REMOTE, + constants.Environment.LEGACY, + constants.Environment.LOCAL, + constants.Environment.ALPHA, + constants.Environment.BETA, + constants.Environment.DEMO, + constants.Environment.GDPR, + constants.Environment.MAIN, + constants.Environment.TEST, + constants.Environment.GOV, + constants.Environment.NEW, + constants.Environment.OLD, + constants.Environment.UAT, + ), + default=constants.Environment.TEST, + help="The database environment to run the tests in.", + ) def configure(self, options, conf): - super(DBReporting, self).configure(options, conf) + from seleniumbase.core.testcase_manager import TestcaseManager + + super().configure(options, conf) self.options = options self.testcase_manager = TestcaseManager(self.options.database_env) def begin(self): - """ At the start of the run, we want to record the test - execution information in the database. """ + """At the start of the run, we want to record the test + execution information in the database.""" + import getpass + from seleniumbase.core.testcase_manager import ExecutionQueryPayload + exec_payload = ExecutionQueryPayload() exec_payload.execution_start_time = int(time.time() * 1000) self.execution_start_time = exec_payload.execution_start_time @@ -57,7 +77,10 @@ def begin(self): self.testcase_manager.insert_execution_data(exec_payload) def startTest(self, test): - """ At the start of the test, set the testcase details. """ + """At the start of the test, set testcase details.""" + from seleniumbase.core.application_manager import ApplicationManager + from seleniumbase.core.testcase_manager import TestcaseDataPayload + data_payload = TestcaseDataPayload() self.testcase_guid = str(uuid.uuid4()) data_payload.guid = self.testcase_guid @@ -68,72 +91,91 @@ def startTest(self, test): data_payload.browser = "N/A" data_payload.test_address = test.id() application = ApplicationManager.generate_application_string(test) - data_payload.env = application.split('.')[0] - data_payload.start_time = application.split('.')[1] - data_payload.state = constants.State.NOTRUN + data_payload.env = application.split(".")[0] + data_payload.start_time = application.split(".")[1] + data_payload.state = constants.State.UNTESTED self.testcase_manager.insert_testcase_data(data_payload) self.case_start_time = int(time.time() * 1000) # Make the testcase guid available to other plugins test.testcase_guid = self.testcase_guid + self._test = test + self._test._nose_skip_reason = None def finalize(self, result): - """ At the end of the run, we want to - update the DB row with the execution time. """ + """At the end of the test run, we want to + update the DB row with the total execution time.""" runtime = int(time.time() * 1000) - self.execution_start_time - self.testcase_manager.update_execution_data(self.execution_guid, - runtime) + self.testcase_manager.update_execution_data( + self.execution_guid, runtime + ) + + def afterTest(self, test): + if not self._result_set: + err = None + try: + err = self._test._nose_skip_reason + if err: + err = "Skipped: " + str(err) + err = (err, err) + except Exception: + pass + if not err: + err = "Skipped: (no reason given)" + err = (err, err) + self.__insert_test_result(constants.State.SKIPPED, self._test, err) def addSuccess(self, test, capt): - """ - After test completion, we want to record testcase run information. - """ - self.__insert_test_result(constants.State.PASS, test) + """After each test success, record testcase run information.""" + self.__insert_test_result(constants.State.PASSED, test) + self._result_set = True + + def addFailure(self, test, err, capt=None, tbinfo=None): + """After each test failure, record testcase run information.""" + self.__insert_test_result(constants.State.FAILED, test, err) + self._result_set = True def addError(self, test, err, capt=None): - """ - After a test error, we want to record testcase run information. - """ - self.__insert_test_result(constants.State.ERROR, test, err) + """After each test error, record testcase run information. + (Test errors should be treated the same as test failures.)""" + self.__insert_test_result(constants.State.FAILED, test, err) + self._result_set = True def handleError(self, test, err, capt=None): - """ - After a test error, we want to record testcase run information. - "Error" also encompasses any states other than Pass or Fail, so we - check for those first. - """ + """After each test error, record testcase run information. + "Error" also encompasses any states other than Pass or Fail.""" + from nose.exc import SkipTest + from seleniumbase.fixtures import errors + if err[0] == errors.BlockedTest: self.__insert_test_result(constants.State.BLOCKED, test, err) - self.error_handled = True + self._result_set = True raise SkipTest(err[1]) - return True - elif err[0] == errors.DeprecatedTest: self.__insert_test_result(constants.State.DEPRECATED, test, err) - self.error_handled = True + self._result_set = True raise SkipTest(err[1]) - return True - elif err[0] == errors.SkipTest: - self.__insert_test_result(constants.State.SKIP, test, err) - self.error_handled = True + self.__insert_test_result(constants.State.SKIPPED, test, err) + self._result_set = True raise SkipTest(err[1]) - return True - - def addFailure(self, test, err, capt=None, tbinfo=None): - """ - After a test failure, we want to record testcase run information. - """ - self.__insert_test_result(constants.State.FAILURE, test, err) def __insert_test_result(self, state, test, err=None): + from seleniumbase.core.testcase_manager import TestcaseDataPayload + data_payload = TestcaseDataPayload() data_payload.runtime = int(time.time() * 1000) - self.case_start_time data_payload.guid = self.testcase_guid data_payload.execution_guid = self.execution_guid data_payload.state = state if err is not None: - data_payload.message = err[1].__str__().split( - '''-------------------- >> ''' - '''begin captured logging''' - ''' << --------------------''', 1)[0] + data_payload.message = ( + err[1] + .__str__() + .split( + """-------------------- >> """ + """begin captured logging""" + """ << --------------------""", + 1, + )[0] + ) self.testcase_manager.update_testcase_data(data_payload) diff --git a/seleniumbase/plugins/driver_manager.py b/seleniumbase/plugins/driver_manager.py new file mode 100644 index 00000000000..4be5af844af --- /dev/null +++ b/seleniumbase/plugins/driver_manager.py @@ -0,0 +1,998 @@ +""" +The SeleniumBase Driver as a Python Context Manager or a returnable object. +########################################################################### + +The SeleniumBase Driver as a context manager: +Usage --> ``with DriverContext() as driver:`` + +Example --> + +```python +from seleniumbase import DriverContext + +with DriverContext(uc=True) as driver: + driver.get("https://google.com/ncr") +``` + +# (The browser exits automatically after the "with" block ends.) + +########################################################################### +# Above: The driver as a context manager. (Used with a "with" statement.) # +# ----------------------------------------------------------------------- # +# Below: The driver as a returnable object. (Used with "return" command.) # +########################################################################### + +The SeleniumBase Driver as a returnable object: +Usage --> ``driver = Driver()`` + +Example --> + +```python +from seleniumbase import Driver + +driver = Driver(uc=True) +driver.get("https://google.com/ncr") +``` + +########################################################################### +""" +import os +import sys +from seleniumbase.core import sb_driver + + +class DriverContext(): + def __init__(self, *args, **kwargs): + self.driver = Driver(*args, **kwargs) + + def __enter__(self): + return self.driver + + def __exit__(self, exc_type, exc_val, exc_tb): + try: + if ( + hasattr(self, "driver") + and hasattr(self.driver, "quit") + and ( + "win32" not in sys.platform + or self.driver.service.process + ) + ): + self.driver.quit() + except Exception: + pass + return False + + +def Driver( + browser=None, # Choose from "chrome", "edge", "firefox", or "safari". + headless=None, # Use the default headless mode for Chromium and Firefox. + headless1=None, # Use Chromium's old headless mode. (Fast, but limited) + headless2=None, # Use Chromium's new headless mode. (Has more features) + headed=None, # Run tests in headed/GUI mode on Linux, where not default. + locale_code=None, # Set the Language Locale Code for the web browser. + protocol=None, # The Selenium Grid protocol: "http" or "https". + servername=None, # The Selenium Grid server/IP used for tests. + port=None, # The Selenium Grid port used by the test server. + proxy=None, # Use proxy. Format: "SERVER:PORT" or "USER:PASS@SERVER:PORT". + proxy_bypass_list=None, # Skip proxy when using the listed domains. + proxy_pac_url=None, # Use PAC file. (Format: URL or USERNAME:PASSWORD@URL) + multi_proxy=None, # Allow multiple proxies with auth when multi-threaded. + agent=None, # Modify the web browser's User-Agent string. + cap_file=None, # The desired capabilities to use with a Selenium Grid. + cap_string=None, # The desired capabilities to use with a Selenium Grid. + recorder_ext=None, # Enables the SeleniumBase Recorder Chromium extension. + disable_cookies=None, # Disable Cookies on websites. (Pages might break!) + disable_js=None, # Disable JavaScript on websites. (Pages might break!) + disable_csp=None, # Disable the Content Security Policy of websites. + enable_ws=None, # Enable Web Security on Chromium-based browsers. + disable_ws=None, # Reverse of "enable_ws". (None and False are different) + enable_sync=None, # Enable "Chrome Sync" on websites. + use_auto_ext=None, # Use Chrome's automation extension. + undetectable=None, # Use undetected-chromedriver to evade bot-detection. + uc_cdp_events=None, # Capture CDP events in undetected-chromedriver mode. + uc_subprocess=None, # Use undetected-chromedriver as a subprocess. + log_cdp_events=None, # Capture {"performance": "ALL", "browser": "ALL"} + no_sandbox=None, # (DEPRECATED) - "--no-sandbox" is always used now. + disable_gpu=None, # (DEPRECATED) - GPU is disabled if not "swiftshader". + incognito=None, # Enable Chromium's Incognito mode. + guest_mode=None, # Enable Chromium's Guest mode. + dark_mode=None, # Enable Chromium's Dark mode. + devtools=None, # Open Chromium's DevTools when the browser opens. + remote_debug=None, # Enable Chrome's Debugger on "http://localhost:9222". + enable_3d_apis=None, # Enable WebGL and 3D APIs. + swiftshader=None, # Chrome: --use-gl=angle / --use-angle=swiftshader-webgl + ad_block_on=None, # Block some types of display ads from loading. + host_resolver_rules=None, # Set host-resolver-rules, comma-separated. + block_images=None, # Block images from loading during tests. + do_not_track=None, # Tell websites that you don't want to be tracked. + chromium_arg=None, # "ARG=N,ARG2" (Set Chromium args, ","-separated.) + firefox_arg=None, # "ARG=N,ARG2" (Set Firefox args, comma-separated.) + firefox_pref=None, # SET (Set Firefox PREFERENCE:VALUE set, ","-separated) + user_data_dir=None, # Set the Chrome user data directory to use. + extension_zip=None, # Load a Chrome Extension .zip|.crx, comma-separated. + extension_dir=None, # Load a Chrome Extension directory, comma-separated. + disable_features=None, # "F1,F2" (Disable Chrome features, ","-separated.) + binary_location=None, # Set path of the Chromium browser binary to use. + driver_version=None, # Set the chromedriver or uc_driver version to use. + page_load_strategy=None, # Set Chrome PLS to "normal", "eager", or "none". + external_pdf=None, # Set Chrome "plugins.always_open_pdf_externally":True. + window_position=None, # Set the browser's starting window position: "X,Y" + window_size=None, # Set the browser's starting window size: "Width,Height" + is_mobile=None, # Use the mobile device emulator while running tests. + mobile=None, # Shortcut / Duplicate of "is_mobile". + d_width=None, # Set device width + d_height=None, # Set device height + d_p_r=None, # Set device pixel ratio + position=None, # Shortcut / Duplicate of "window_position". + size=None, # Shortcut / Duplicate of "window_size". + uc=None, # Shortcut / Duplicate of "undetectable". + undetected=None, # Shortcut / Duplicate of "undetectable". + uc_cdp=None, # Shortcut / Duplicate of "uc_cdp_events". + uc_sub=None, # Shortcut / Duplicate of "uc_subprocess". + locale=None, # Shortcut / Duplicate of "locale_code". + log_cdp=None, # Shortcut / Duplicate of "log_cdp_events". + ad_block=None, # Shortcut / Duplicate of "ad_block_on". + server=None, # Shortcut / Duplicate of "servername". + guest=None, # Shortcut / Duplicate of "guest_mode". + pls=None, # Shortcut / Duplicate of "page_load_strategy". + cft=None, # Use "Chrome for Testing" + chs=None, # Use "Chrome-Headless-Shell" + use_chromium=None, # Use base "Chromium" +) -> sb_driver.DriverMethods: + """ + * SeleniumBase Driver as a Python Context Manager or a returnable object. * + + Example 1: (context manager format) + ----------------------------------- + .. code-block:: python + from seleniumbase import DriverContext + + with DriverContext() as driver: + driver.get("https://google.com/ncr") + + Example 2: (as a Python returnable) + ----------------------------------- + .. code-block:: python + from seleniumbase import Driver + + driver = Driver() + driver.get("https://google.com/ncr") + + Optional Parameters: + -------------------- + browser (str): Choose from "chrome", "edge", "firefox", or "safari". + headless (bool): Use the default headless mode for Chromium and Firefox. + headless1 (bool): Use Chromium's old headless mode. (Fast, but limited) + headless2 (bool): Use Chromium's new headless mode. (Has more features) + headed (bool): Run tests in headed/GUI mode on Linux, where not default. + locale_code (str): Set the Language Locale Code for the web browser. + protocol (str): The Selenium Grid protocol: "http" or "https". + servername (str): The Selenium Grid server/IP used for tests. + port (int): The Selenium Grid port used by the test server. + proxy (str): Use proxy. Format: "SERVER:PORT" or "USER:PASS@SERVER:PORT". + proxy_bypass_list (str): Skip proxy when using the listed domains. + proxy_pac_url (str): Use PAC file. (Format: URL or USERNAME:PASSWORD@URL) + multi_proxy (bool): Allow multiple proxies with auth when multi-threaded. + agent (str): Modify the web browser's User-Agent string. + cap_file (str): The desired capabilities to use with a Selenium Grid. + cap_string (str): The desired capabilities to use with a Selenium Grid. + recorder_ext (bool): Enables the SeleniumBase Recorder Chromium extension. + disable_cookies (bool): Disable Cookies on websites. (Pages might break!) + disable_js (bool): Disable JavaScript on websites. (Pages might break!) + disable_csp (bool): Disable the Content Security Policy of websites. + enable_ws (bool): Enable Web Security on Chromium-based browsers. + disable_ws (bool): Reverse of "enable_ws". (None and False are different) + enable_sync (bool): Enable "Chrome Sync" on websites. + use_auto_ext (bool): Use Chrome's automation extension. + undetectable (bool): Use undetected-chromedriver to evade bot-detection. + uc_cdp_events (bool): Capture CDP events in undetected-chromedriver mode. + uc_subprocess (bool): Use undetected-chromedriver as a subprocess. + log_cdp_events (bool): Capture {"performance": "ALL", "browser": "ALL"} + no_sandbox (bool): (DEPRECATED) - "--no-sandbox" is always used now. + disable_gpu (bool): (DEPRECATED) - GPU is disabled if not "swiftshader". + incognito (bool): Enable Chromium's Incognito mode. + guest_mode (bool): Enable Chromium's Guest mode. + dark_mode (bool): Enable Chromium's Dark mode. + devtools (bool): Open Chromium's DevTools when the browser opens. + remote_debug (bool): Enable Chrome's Debugger on "http://localhost:9222". + enable_3d_apis (bool): Enable WebGL and 3D APIs. + swiftshader (bool): Chrome: --use-gl=angle / --use-angle=swiftshader-webgl + ad_block_on (bool): Block some types of display ads from loading. + host_resolver_rules (str): Set host-resolver-rules, comma-separated. + block_images (bool): Block images from loading during tests. + do_not_track (bool): Tell websites that you don't want to be tracked. + chromium_arg (str): "ARG=N,ARG2" (Set Chromium args, ","-separated.) + firefox_arg (str): "ARG=N,ARG2" (Set Firefox args, comma-separated.) + firefox_pref (str): SET (Set Firefox PREFERENCE:VALUE set, ","-separated) + user_data_dir (str): Set the Chrome user data directory to use. + extension_zip (str): Load a Chrome Extension .zip|.crx, comma-separated. + extension_dir (str): Load a Chrome Extension directory, comma-separated. + disable_features (str): "F1,F2" (Disable Chrome features, ","-separated.) + binary_location (str): Set path of the Chromium browser binary to use. + driver_version (str): Set the chromedriver or uc_driver version to use. + page_load_strategy (str): Set Chrome PLS to "normal", "eager", or "none". + external_pdf (bool): Set Chrome "plugins.always_open_pdf_externally":True + window_position (x,y): Set the browser's starting window position: "X,Y" + window_size (w,h): Set the browser's starting window size: "Width,Height" + is_mobile (bool): Use the mobile device emulator while running tests. + mobile (bool): Shortcut / Duplicate of "is_mobile". + d_width (int): Set device width + d_height (int): Set device height + d_p_r (float): Set device pixel ratio + position (x,y): Shortcut / Duplicate of "window_position". + size (w,h): Shortcut / Duplicate of "window_size". + uc (bool): Shortcut / Duplicate of "undetectable". + undetected (bool): Shortcut / Duplicate of "undetectable". + uc_cdp (bool): Shortcut / Duplicate of "uc_cdp_events". + uc_sub (bool): Shortcut / Duplicate of "uc_subprocess". + locale (str): Shortcut / Duplicate of "locale_code". + log_cdp (bool): Shortcut / Duplicate of "log_cdp_events". + ad_block (bool): Shortcut / Duplicate of "ad_block_on". + server (str): Shortcut / Duplicate of "servername". + guest (bool): Shortcut / Duplicate of "guest_mode". + pls (str): Shortcut / Duplicate of "page_load_strategy". + """ + from seleniumbase import config as sb_config + from seleniumbase.config import settings + from seleniumbase.core import browser_launcher + from seleniumbase.core import detect_b_ver + from seleniumbase.fixtures import constants + from seleniumbase.fixtures import shared_utils + + sys_argv = sys.argv + arg_join = " ".join(sys_argv) + existing_runner = False + collect_only = ("--co" in sys_argv or "--collect-only" in sys_argv) + all_scripts = (hasattr(sb_config, "all_scripts") and sb_config.all_scripts) + if ( + (hasattr(sb_config, "is_behave") and sb_config.is_behave) + or (hasattr(sb_config, "is_pytest") and sb_config.is_pytest) + or (hasattr(sb_config, "is_nosetest") and sb_config.is_nosetest) + ): + existing_runner = True + if ( + existing_runner + and not hasattr(sb_config, "_context_of_runner") + ): + if hasattr(sb_config, "is_pytest") and sb_config.is_pytest: + import pytest + msg = "Skipping `Driver()` script. (Use `python`, not `pytest`)" + if not collect_only and not all_scripts: + print("\n *** %s" % msg) + if collect_only or not all_scripts: + pytest.skip(allow_module_level=True) + elif hasattr(sb_config, "is_nosetest") and sb_config.is_nosetest: + raise Exception( + "\n A Driver() script was triggered by nosetest collection!" + '\n (Prevent that by using: ``if __name__ == "__main__":``)' + ) + elif existing_runner: + sb_config._context_of_runner = True + sb_config._browser_shortcut = None + sb_config._cdp_browser = None + sb_config._cdp_bin_loc = None + browser_changes = 0 + browser_set = None + browser_text = None + browser_list = [] + # Check if binary-location in options + bin_loc_in_options = False + if ( + binary_location + and len(str(binary_location)) > 5 + and os.path.exists(str(binary_location)) + ): + bin_loc_in_options = True + else: + for arg in sys_argv: + if arg in ["--binary-location", "--binary_location", "--bl"]: + bin_loc_in_options = True + if ( + browser + and browser in constants.ChromiumSubs.chromium_subs + and not bin_loc_in_options + ): + bin_loc = detect_b_ver.get_binary_location(browser) + if bin_loc and os.path.exists(bin_loc): + if browser in bin_loc.lower().split("/")[-1].split("\\")[-1]: + sb_config._cdp_browser = browser + sb_config._cdp_bin_loc = bin_loc + binary_location = bin_loc + bin_loc_in_options = True + # As a shortcut, you can use "--edge" instead of "--browser=edge", etc, + # but you can only specify one default browser for tests. (Default: chrome) + if "--browser=chrome" in sys_argv or "--browser chrome" in sys_argv: + browser_changes += 1 + browser_set = "chrome" + browser_list.append("--browser=chrome") + if "--browser=edge" in sys_argv or "--browser edge" in sys_argv: + browser_changes += 1 + browser_set = "edge" + browser_list.append("--browser=edge") + if "--browser=firefox" in sys_argv or "--browser firefox" in sys_argv: + browser_changes += 1 + browser_set = "firefox" + browser_list.append("--browser=firefox") + if "--browser=safari" in sys_argv or "--browser safari" in sys_argv: + browser_changes += 1 + browser_set = "safari" + browser_list.append("--browser=safari") + if "--browser=ie" in sys_argv or "--browser ie" in sys_argv: + browser_changes += 1 + browser_set = "ie" + browser_list.append("--browser=ie") + if "--browser=remote" in sys_argv or "--browser remote" in sys_argv: + browser_changes += 1 + browser_set = "remote" + browser_list.append("--browser=remote") + if "--browser=opera" in sys_argv or "--browser opera" in sys_argv: + if not bin_loc_in_options: + bin_loc = detect_b_ver.get_binary_location("opera") + if os.path.exists(bin_loc): + browser_changes += 1 + browser_set = "opera" + sb_config._browser_shortcut = "opera" + sb_config._cdp_browser = "opera" + sb_config._cdp_bin_loc = bin_loc + browser_list.append("--browser=opera") + if "--browser=brave" in sys_argv or "--browser brave" in sys_argv: + if not bin_loc_in_options: + bin_loc = detect_b_ver.get_binary_location("brave") + if os.path.exists(bin_loc): + browser_changes += 1 + browser_set = "brave" + sb_config._browser_shortcut = "brave" + sb_config._cdp_browser = "brave" + sb_config._cdp_bin_loc = bin_loc + browser_list.append("--browser=brave") + if "--browser=comet" in sys_argv or "--browser comet" in sys_argv: + if not bin_loc_in_options: + bin_loc = detect_b_ver.get_binary_location("comet") + if os.path.exists(bin_loc): + browser_changes += 1 + browser_set = "comet" + sb_config._browser_shortcut = "comet" + sb_config._cdp_browser = "comet" + sb_config._cdp_bin_loc = bin_loc + browser_list.append("--browser=comet") + browser_text = browser_set + if "--chrome" in sys_argv and not browser_set == "chrome": + browser_changes += 1 + browser_text = "chrome" + browser_list.append("--chrome") + if "--edge" in sys_argv and not browser_set == "edge": + browser_changes += 1 + browser_text = "edge" + browser_list.append("--edge") + if "--firefox" in sys_argv and not browser_set == "firefox": + browser_changes += 1 + browser_text = "firefox" + browser_list.append("--firefox") + if "--ie" in sys_argv and not browser_set == "ie": + browser_changes += 1 + browser_text = "ie" + browser_list.append("--ie") + if "--safari" in sys_argv and not browser_set == "safari": + browser_changes += 1 + browser_text = "safari" + browser_list.append("--safari") + if "--opera" in sys_argv and not browser_set == "opera": + if not bin_loc_in_options: + bin_loc = detect_b_ver.get_binary_location("opera") + if os.path.exists(bin_loc): + browser_changes += 1 + browser_text = "opera" + sb_config._browser_shortcut = "opera" + sb_config._cdp_browser = "opera" + sb_config._cdp_bin_loc = bin_loc + browser_list.append("--opera") + if "--brave" in sys_argv and not browser_set == "brave": + if not bin_loc_in_options: + bin_loc = detect_b_ver.get_binary_location("brave") + if os.path.exists(bin_loc): + browser_changes += 1 + browser_text = "brave" + sb_config._browser_shortcut = "brave" + sb_config._cdp_browser = "brave" + sb_config._cdp_bin_loc = bin_loc + browser_list.append("--brave") + if "--comet" in sys_argv and not browser_set == "comet": + if not bin_loc_in_options: + bin_loc = detect_b_ver.get_binary_location("comet") + if os.path.exists(bin_loc): + browser_changes += 1 + browser_text = "comet" + sb_config._browser_shortcut = "comet" + sb_config._cdp_browser = "comet" + sb_config._cdp_bin_loc = bin_loc + browser_list.append("--comet") + if browser_changes > 1: + message = "\n\n TOO MANY browser types were entered!" + message += "\n There were %s found:\n > %s" % ( + browser_changes, + ", ".join(browser_list), + ) + message += "\n ONLY ONE default browser is allowed!" + message += "\n Select a single browser & try again!\n" + if not browser: + raise Exception(message) + if browser is None: + if browser_text: + browser = browser_text + else: + browser = "chrome" + else: + browser = browser.lower() + valid_browsers = constants.ValidBrowsers.valid_browsers + if browser not in valid_browsers: + raise Exception( + "Browser: {%s} is not a valid browser option. " + "Valid options = {%s}" % (browser, valid_browsers) + ) + if sb_config._browser_shortcut: + browser = sb_config._browser_shortcut + if headless is None: + if "--headless" in sys_argv: + headless = True + else: + headless = False + if headless1 is None: + if "--headless1" in sys_argv: + headless1 = True + else: + headless1 = False + if headless1: + headless = True + if headless2 is None: + if "--headless2" in sys_argv: + headless2 = True + else: + headless2 = False + if protocol is None: + protocol = "http" # For the Selenium Grid only! + if server is not None and servername is None: + servername = server + if servername is None: + servername = "localhost" # For the Selenium Grid only! + use_grid = False + if servername != "localhost": + # Use Selenium Grid (Use "127.0.0.1" for localhost Grid) + use_grid = True + if port is None: + port = "4444" # For the Selenium Grid only! + if incognito is None: + if "--incognito" in sys_argv: + incognito = True + else: + incognito = False + if guest is not None and guest_mode is None: + guest_mode = guest + if guest_mode is None: + if "--guest" in sys_argv: + guest_mode = True + else: + guest_mode = False + if dark_mode is None: + if "--dark" in sys_argv: + dark_mode = True + else: + dark_mode = False + if devtools is None: + if "--devtools" in sys_argv: + devtools = True + else: + devtools = False + if mobile is not None and is_mobile is None: + is_mobile = mobile + if is_mobile is None: + if "--mobile" in sys_argv: + is_mobile = True + else: + is_mobile = False + test_id = "direct_driver" + proxy_string = proxy + if proxy_string is None and "--proxy" in arg_join: + if "--proxy=" in arg_join: + proxy_string = arg_join.split("--proxy=")[1].split(" ")[0] + elif "--proxy " in arg_join: + proxy_string = arg_join.split("--proxy ")[1].split(" ")[0] + if proxy_string: + if proxy_string.startswith('"') and proxy_string.endswith('"'): + proxy_string = proxy_string[1:-1] + elif proxy_string.startswith("'") and proxy_string.endswith("'"): + proxy_string = proxy_string[1:-1] + c_a = chromium_arg + if c_a is None and "--chromium-arg" in arg_join: + count = 0 + for arg in sys_argv: + if arg.startswith("--chromium-arg="): + c_a = arg.split("--chromium-arg=")[1] + break + elif arg == "--chromium-arg" and len(sys_argv) > count + 1: + c_a = sys_argv[count + 1] + if c_a.startswith("-"): + c_a = None + break + count += 1 + chromium_arg = c_a + d_f = disable_features + if d_f is None and "--disable-features" in arg_join: + count = 0 + for arg in sys_argv: + if arg.startswith("--disable-features="): + d_f = arg.split("--disable-features=")[1] + break + elif arg == "--disable-features" and len(sys_argv) > count + 1: + d_f = sys_argv[count + 1] + if d_f.startswith("-"): + d_f = None + break + count += 1 + disable_features = d_f + if window_position is None and position is not None: + window_position = position + w_p = window_position + if w_p is None and "--window-position" in arg_join: + count = 0 + for arg in sys_argv: + if arg.startswith("--window-position="): + w_p = arg.split("--window-position=")[1] + break + elif arg == "--window-position" and len(sys_argv) > count + 1: + w_p = sys_argv[count + 1] + if w_p.startswith("-"): + w_p = None + break + count += 1 + window_position = w_p + if window_position: + if window_position.count(",") != 1: + message = ( + '\n\n window_position expects an "x,y" string!' + '\n (Your input was: "%s")\n' % window_position + ) + raise Exception(message) + window_position = window_position.replace(" ", "") + win_x = None + win_y = None + try: + win_x = int(window_position.split(",")[0]) + win_y = int(window_position.split(",")[1]) + except Exception: + message = ( + '\n\n Expecting integer values for "x,y"!' + '\n (window_position input was: "%s")\n' + % window_position + ) + raise Exception(message) + settings.WINDOW_START_X = win_x + settings.WINDOW_START_Y = win_y + if window_size is None and size is not None: + window_size = size + w_s = window_size + if w_s is None and "--window-size" in arg_join: + count = 0 + for arg in sys_argv: + if arg.startswith("--window-size="): + w_s = arg.split("--window-size=")[1] + break + elif arg == "--window-size" and len(sys_argv) > count + 1: + w_s = sys_argv[count + 1] + if w_s.startswith("-"): + w_s = None + break + count += 1 + window_size = w_s + if window_size: + if window_size.count(",") != 1: + message = ( + '\n\n window_size expects a "width,height" string!' + '\n (Your input was: "%s")\n' % window_size + ) + raise Exception(message) + window_size = window_size.replace(" ", "") + width = None + height = None + try: + width = int(window_size.split(",")[0]) + height = int(window_size.split(",")[1]) + except Exception: + message = ( + '\n\n Expecting integer values for "width,height"!' + '\n (window_size input was: "%s")\n' % window_size + ) + raise Exception(message) + settings.CHROME_START_WIDTH = width + settings.CHROME_START_HEIGHT = height + settings.HEADLESS_START_WIDTH = width + settings.HEADLESS_START_HEIGHT = height + if agent is None and "--agent" in arg_join: + count = 0 + for arg in sys_argv: + if arg.startswith("--agent="): + agent = arg.split("--agent=")[1] + break + elif arg == "--agent" and len(sys_argv) > count + 1: + agent = sys_argv[count + 1] + if agent.startswith("-"): + agent = None + break + count += 1 + user_agent = agent + found_bl = None + if hasattr(sb_config, "_cdp_bin_loc") and sb_config._cdp_bin_loc: + binary_location = sb_config._cdp_bin_loc + if binary_location is None and "--binary-location" in arg_join: + count = 0 + for arg in sys_argv: + if arg.startswith("--binary-location="): + found_bl = arg.split("--binary-location=")[1] + break + elif arg == "--binary-location" and len(sys_argv) > count + 1: + found_bl = sys_argv[count + 1] + if found_bl.startswith("-"): + found_bl = None + break + count += 1 + if found_bl: + binary_location = found_bl + if binary_location is None and "--bl=" in arg_join: + for arg in sys_argv: + if arg.startswith("--bl="): + binary_location = arg.split("--bl=")[1] + break + if use_chromium and not binary_location: + binary_location = "_chromium_" + elif cft and not binary_location: + binary_location = "cft" + elif chs and not binary_location: + binary_location = "chs" + if ( + ("--use-chromium" in sys_argv and not binary_location) + or ("--chromium" in sys_argv and not binary_location) + ): + binary_location = "_chromium_" + elif "--cft" in sys_argv and not binary_location: + binary_location = "cft" + elif "--chs" in sys_argv and not binary_location: + binary_location = "chs" + if ( + binary_location + and binary_location.lower() == "chs" + and browser == "chrome" + ): + headless = True + headless1 = False + headless2 = False + recorder_mode = False + if recorder_ext: + recorder_mode = True + if ( + "--recorder" in sys_argv + or "--record" in sys_argv + or "--rec" in sys_argv + ): + recorder_mode = True + recorder_ext = True + if ( + undetectable + or undetected + or uc + or uc_cdp_events + or uc_cdp + or uc_subprocess + or uc_sub + ): + undetectable = True + if undetectable or undetected or uc: + uc_subprocess = True # Use UC as a subprocess by default. + elif ( + "--undetectable" in sys_argv + or "--undetected" in sys_argv + or "--uc" in sys_argv + or "--uc-cdp-events" in sys_argv + or "--uc_cdp_events" in sys_argv + or "--uc-cdp" in sys_argv + or "--uc-subprocess" in sys_argv + or "--uc_subprocess" in sys_argv + or "--uc-sub" in sys_argv + ): + undetectable = True + if uc_subprocess is None and uc_sub is None: + uc_subprocess = True # Use UC as a subprocess by default. + else: + undetectable = False + if uc_subprocess or uc_sub: + uc_subprocess = True + elif ( + "--uc-subprocess" in sys_argv + or "--uc_subprocess" in sys_argv + or "--uc-sub" in sys_argv + ): + uc_subprocess = True + else: + uc_subprocess = False + if uc_cdp_events or uc_cdp: + undetectable = True + uc_cdp_events = True + elif ( + "--uc-cdp-events" in sys_argv + or "--uc_cdp_events" in sys_argv + or "--uc-cdp" in sys_argv + or "--uc_cdp" in sys_argv + ): + undetectable = True + uc_cdp_events = True + else: + uc_cdp_events = False + if ( + undetectable + and browser not in ["chrome", "opera", "brave", "comet"] + ): + message = ( + '\n Undetected-Chromedriver Mode ONLY supports Chromium browsers!' + '\n ("uc=True" / "undetectable=True" / "--uc")' + '\n (Your browser choice was: "%s".)' + '\n (Will use "%s" without UC Mode.)\n' % (browser, browser) + ) + print(message) + if headed is None: + # Override the default headless mode on Linux if set. + if "--gui" in sys_argv or "--headed" in sys_argv: + headed = True + else: + headed = False + if ( + shared_utils.is_linux() + and not headed + and not headless + and not headless2 + and ( + not undetectable + or "DISPLAY" not in os.environ.keys() + or not os.environ["DISPLAY"] + ) + ): + headless = True + if recorder_mode and headless: + headless = False + headless1 = False + headless2 = True + if headless2 and browser == "firefox": + headless2 = False # Only for Chromium browsers + headless = True # Firefox has regular headless + elif browser not in ["chrome", "edge", "opera", "brave", "comet"]: + headless2 = False # Only for Chromium browsers + if disable_csp is None: + if ( + "--disable-csp" in sys_argv + or "--no-csp" in sys_argv + or "--dcsp" in sys_argv + ): + disable_csp = True + else: + disable_csp = False + if ( + (enable_ws is None and disable_ws is None) + and ( + "--disable-web-security" in sys_argv + or "--disable-ws" in sys_argv + or "--dws" in sys_argv + ) + ): + enable_ws = False + elif ( + (enable_ws is None and disable_ws is None) + or (disable_ws is not None and not disable_ws) + or (enable_ws is not None and enable_ws) + ): + enable_ws = True + else: + enable_ws = False + if log_cdp_events is None and log_cdp is None: + if ( + "--log-cdp-events" in sys_argv + or "--log_cdp_events" in sys_argv + or "--log-cdp" in sys_argv + or "--log_cdp" in sys_argv + ): + log_cdp_events = True + else: + log_cdp_events = False + elif log_cdp_events or log_cdp: + log_cdp_events = True + else: + log_cdp_events = False + if use_auto_ext is None: + if "--use-auto-ext" in sys_argv: + use_auto_ext = True + else: + use_auto_ext = False + if disable_cookies is None: + if "--disable-cookies" in sys_argv: + disable_cookies = True + else: + disable_cookies = False + if disable_js is None: + if "--disable-js" in sys_argv: + disable_js = True + else: + disable_js = False + if pls is not None and page_load_strategy is None: + page_load_strategy = pls + if not page_load_strategy and "--pls=" in arg_join: + if "--pls=none" in sys_argv or '--pls="none"' in sys_argv: + page_load_strategy = "none" + elif "--pls=eager" in sys_argv or '--pls="eager"' in sys_argv: + page_load_strategy = "eager" + elif "--pls=normal" in sys_argv or '--pls="normal"' in sys_argv: + page_load_strategy = "normal" + if page_load_strategy is not None: + if page_load_strategy.lower() not in ["normal", "eager", "none"]: + raise Exception( + 'page_load_strategy must be "normal", "eager", or "none"!' + ) + page_load_strategy = page_load_strategy.lower() + elif "--pls=normal" in sys_argv or '--pls="normal"' in sys_argv: + page_load_strategy = "normal" + elif "--pls=eager" in sys_argv or '--pls="eager"' in sys_argv: + page_load_strategy = "eager" + elif "--pls=none" in sys_argv or '--pls="none"' in sys_argv: + page_load_strategy = "none" + if block_images is None: + if "--block-images" in sys_argv or "--block_images" in sys_argv: + block_images = True + else: + block_images = False + if do_not_track is None: + if "--do-not-track" in sys_argv or "--do_not_track" in sys_argv: + do_not_track = True + else: + do_not_track = False + if external_pdf is None: + if "--external-pdf" in sys_argv or "--external_pdf" in sys_argv: + external_pdf = True + else: + external_pdf = False + if remote_debug is None: + if "--remote-debug" in sys_argv or "--remote_debug" in sys_argv: + remote_debug = True + else: + remote_debug = False + if enable_3d_apis is None: + if "--enable-3d-apis" in sys_argv or "--enable_3d_apis" in sys_argv: + enable_3d_apis = True + else: + enable_3d_apis = False + if swiftshader is None: + if "--swiftshader" in sys_argv: + swiftshader = True + else: + swiftshader = False + if locale is not None and locale_code is None: + locale_code = locale + if locale_code is None: + if '--locale="' in arg_join: + locale_code = ( + arg_join.split('--locale="')[1].split('"')[0] + ) + elif '--locale=' in arg_join: + locale_code = ( + arg_join.split('--locale=')[1].split(' ')[0] + ) + elif '--locale-code="' in arg_join: + locale_code = ( + arg_join.split('--locale-code="')[1].split('"')[0] + ) + elif '--locale-code=' in arg_join: + locale_code = ( + arg_join.split('--locale-code=')[1].split(' ')[0] + ) + if ad_block is not None and ad_block_on is None: + ad_block_on = ad_block + if ad_block_on is None: + if "--ad-block" in sys_argv or "--ad_block" in sys_argv: + ad_block_on = True + else: + ad_block_on = False + if host_resolver_rules is None: + if '--host-resolver-rules="' in arg_join: + host_resolver_rules = ( + arg_join.split('--host-resolver-rules="')[1].split('"')[0] + ) + elif '--host_resolver_rules="' in arg_join: + host_resolver_rules = ( + arg_join.split("--host_resolver_rules=")[1].split('"')[0] + ) + if driver_version is None and "--driver-version" in arg_join: + count = 0 + for arg in sys_argv: + if arg.startswith("--driver-version="): + driver_version = arg.split("--driver-version=")[1] + break + elif arg == "--driver-version" and len(sys_argv) > count + 1: + driver_version = sys_argv[count + 1] + if driver_version.startswith("-"): + driver_version = None + break + count += 1 + if driver_version is None and "--driver_version" in arg_join: + count = 0 + for arg in sys_argv: + if arg.startswith("--driver_version="): + driver_version = arg.split("--driver_version=")[1] + break + elif arg == "--driver_version" and len(sys_argv) > count + 1: + driver_version = sys_argv[count + 1] + if driver_version.startswith("-"): + driver_version = None + break + count += 1 + if browser in constants.ChromiumSubs.chromium_subs: + if not binary_location: + browser = "chrome" # Still uses chromedriver + sb_config._browser_shortcut = browser + browser_name = browser + + # Launch a web browser + driver = browser_launcher.get_driver( + browser_name=browser_name, + headless=headless, + locale_code=locale_code, + use_grid=use_grid, + protocol=protocol, + servername=servername, + port=port, + proxy_string=proxy_string, + proxy_bypass_list=proxy_bypass_list, + proxy_pac_url=proxy_pac_url, + multi_proxy=multi_proxy, + user_agent=user_agent, + cap_file=cap_file, + cap_string=cap_string, + recorder_ext=recorder_ext, + disable_cookies=disable_cookies, + disable_js=disable_js, + disable_csp=disable_csp, + enable_ws=enable_ws, + enable_sync=enable_sync, + use_auto_ext=use_auto_ext, + undetectable=undetectable, + uc_cdp_events=uc_cdp_events, + uc_subprocess=uc_subprocess, + log_cdp_events=log_cdp_events, + no_sandbox=no_sandbox, + disable_gpu=disable_gpu, + headless1=headless1, + headless2=headless2, + incognito=incognito, + guest_mode=guest_mode, + dark_mode=dark_mode, + devtools=devtools, + remote_debug=remote_debug, + enable_3d_apis=enable_3d_apis, + swiftshader=swiftshader, + ad_block_on=ad_block_on, + host_resolver_rules=host_resolver_rules, + block_images=block_images, + do_not_track=do_not_track, + chromium_arg=chromium_arg, + firefox_arg=firefox_arg, + firefox_pref=firefox_pref, + user_data_dir=user_data_dir, + extension_zip=extension_zip, + extension_dir=extension_dir, + disable_features=disable_features, + binary_location=binary_location, + driver_version=driver_version, + page_load_strategy=page_load_strategy, + external_pdf=external_pdf, + test_id=test_id, + mobile_emulator=is_mobile, + device_width=d_width, + device_height=d_height, + device_pixel_ratio=d_p_r, + browser=browser_name, + ) + return driver diff --git a/seleniumbase/plugins/page_source.py b/seleniumbase/plugins/page_source.py old mode 100755 new mode 100644 index 292eb59a593..6254f9b842e --- a/seleniumbase/plugins/page_source.py +++ b/seleniumbase/plugins/page_source.py @@ -1,28 +1,20 @@ -""" -The plugin for capturing and storing the page source on errors and failures. -""" - +"""PageSource Plugin for SeleniumBase tests that run with pynose / nosetests""" import os -import codecs from nose.plugins import Plugin from seleniumbase.config import settings from seleniumbase.core import log_helper class PageSource(Plugin): - """ - This plugin will capture the page source when a test fails - or raises an error. It will store the page source in the - logs file specified, along with default test information. - """ + """Capture the page source after a test fails.""" name = "page_source" # Usage: --with-page_source logfile_name = settings.PAGE_SOURCE_NAME def options(self, parser, env): - super(PageSource, self).options(parser, env=env) + super().options(parser, env=env) def configure(self, options, conf): - super(PageSource, self).configure(options, conf) + super().configure(options, conf) if not self.enabled: return self.options = options @@ -31,15 +23,15 @@ def addError(self, test, err, capt=None): try: page_source = test.driver.page_source except Exception: - # Since we can't get the page source from here, skip saving it return test_logpath = self.options.log_path + "/" + test.id() if not os.path.exists(test_logpath): os.makedirs(test_logpath) - html_file_name = "%s/%s" % (test_logpath, self.logfile_name) - html_file = codecs.open(html_file_name, "w+", "utf-8") + html_file_name = os.path.join(test_logpath, self.logfile_name) + html_file = open(html_file_name, mode="w+", encoding="utf-8") rendered_source = log_helper.get_html_source_with_base_href( - test.driver, page_source) + test.driver, page_source + ) html_file.write(rendered_source) html_file.close() @@ -47,14 +39,14 @@ def addFailure(self, test, err, capt=None, tbinfo=None): try: page_source = test.driver.page_source except Exception: - # Since we can't get the page source from here, skip saving it return test_logpath = self.options.log_path + "/" + test.id() if not os.path.exists(test_logpath): os.makedirs(test_logpath) - html_file_name = "%s/%s" % (test_logpath, self.logfile_name) - html_file = codecs.open(html_file_name, "w+", "utf-8") + html_file_name = os.path.join(test_logpath, self.logfile_name) + html_file = open(html_file_name, mode="w+", encoding="utf-8") rendered_source = log_helper.get_html_source_with_base_href( - test.driver, page_source) + test.driver, page_source + ) html_file.write(rendered_source) html_file.close() diff --git a/seleniumbase/plugins/pytest_plugin.py b/seleniumbase/plugins/pytest_plugin.py index 89dded2b7f9..ed46083f94b 100644 --- a/seleniumbase/plugins/pytest_plugin.py +++ b/seleniumbase/plugins/pytest_plugin.py @@ -1,276 +1,2884 @@ -# -*- coding: utf-8 -*- -""" This is the pytest configuration file """ - -import optparse +"""This is the pytest configuration file for setting test options.""" +import colorama +import os import pytest +import sys +import time +from contextlib import suppress from seleniumbase import config as sb_config +from seleniumbase.config import settings +from seleniumbase.core import detect_b_ver from seleniumbase.core import log_helper -from seleniumbase.core import proxy_helper from seleniumbase.fixtures import constants +from seleniumbase.fixtures import shared_utils + +is_windows = shared_utils.is_windows() +python3_11_or_newer = False +if sys.version_info >= (3, 11): + python3_11_or_newer = True +py311_patch2 = constants.PatchPy311.PATCH +sys_argv = sys.argv +full_time = None +pytest_plugins = ["pytester"] # Adds the "testdir" fixture def pytest_addoption(parser): - parser = parser.getgroup('SeleniumBase', - 'SeleniumBase specific configuration options') - parser.addoption('--browser', action="store", - dest='browser', - type=str.lower, - choices=constants.ValidBrowsers.valid_browsers, - default=constants.Browser.GOOGLE_CHROME, - help="""Specifies the web browser to use. Default: Chrome. - If you want to use Firefox, explicitly indicate that. - Example: (--browser=firefox)""") - parser.addoption('--with-selenium', action="store_true", - dest='with_selenium', - default=True, - help="Use if tests need to be run with a web browser.") - parser.addoption('--env', action='store', - dest='environment', - type=str.lower, - choices=( - constants.Environment.QA, - constants.Environment.STAGING, - constants.Environment.DEVELOP, - constants.Environment.PRODUCTION, - constants.Environment.MASTER, - constants.Environment.LOCAL, - constants.Environment.TEST - ), - default=constants.Environment.TEST, - help="The environment to run the tests in.") - parser.addoption('--data', dest='data', - default=None, - help='Extra data to pass from the command line.') - parser.addoption('--cap_file', dest='cap_file', - default=None, - help="""The file that stores browser desired capabilities - for BrowserStack or Sauce Labs web drivers.""") - parser.addoption('--with-testing_base', action="store_true", - dest='with_testing_base', - default=True, - help="""Use to save logs and screenshots when tests fail. - The following options are now active by default - with --with-testing_base (which is on by default): - --with-screen_shots , - --with-basic_test_info , - --with-page_source - """) - parser.addoption('--log_path', dest='log_path', - default='latest_logs/', - help='Where the log files are saved.') - parser.addoption('--archive_logs', action="store_true", - dest='archive_logs', - default=False, - help="Archive old log files instead of deleting them.") - parser.addoption('--with-db_reporting', action="store_true", - dest='with_db_reporting', - default=False, - help="Use to record test data in the MySQL database.") - parser.addoption('--database_env', action='store', - dest='database_env', - choices=( - 'production', 'qa', 'staging', 'develop', - 'test', 'local', 'master' - ), - default='test', - help=optparse.SUPPRESS_HELP) - parser.addoption('--with-s3_logging', action="store_true", - dest='with_s3_logging', - default=False, - help="Use to save test log files in Amazon S3.") - parser.addoption('--with-screen_shots', action="store_true", - dest='with_screen_shots', - default=False, - help="""Use to save screenshots on test failure. - (Automatically on when using --with-testing_base)""") - parser.addoption('--with-basic_test_info', action="store_true", - dest='with_basic_test_info', - default=False, - help="""Use to save basic test info on test failure. - (Automatically on when using --with-testing_base)""") - parser.addoption('--with-page_source', action="store_true", - dest='with_page_source', - default=False, - help="""Use to save page source on test failure. - (Automatically on when using --with-testing_base)""") - parser.addoption('--server', action='store', - dest='servername', - default='localhost', - help="""Designates the Selenium Grid server to use. - Default: localhost.""") - parser.addoption('--port', action='store', - dest='port', - default='4444', - help="""Designates the Selenium Grid port to use. - Default: 4444.""") - parser.addoption('--proxy', action='store', - dest='proxy_string', - default=None, - help="""Designates the proxy server:port to use. - Format: servername:port. OR - username:password@servername:port OR - A dict key from proxy_list.PROXY_LIST - Default: None.""") - parser.addoption('--agent', action='store', - dest='user_agent', - default=None, - help="""Designates the User-Agent for the browser to use. - Format: A string. - Default: None.""") - parser.addoption('--headless', action="store_true", - dest='headless', - default=False, - help="""Using this makes Webdriver run headlessly, - which is required on headless machines.""") - parser.addoption('--is_pytest', action="store_true", - dest='is_pytest', - default=True, - help="""This is used by the BaseCase class to tell apart - pytest runs from nosetest runs. (Automatic)""") - parser.addoption('--demo_mode', action="store_true", - dest='demo_mode', - default=False, - help="""Using this slows down the automation so that - you can see what it's actually doing.""") - parser.addoption('--demo_sleep', action='store', dest='demo_sleep', - default=None, - help="""Setting this overrides the Demo Mode sleep - time that happens after browser actions.""") - parser.addoption('--highlights', action='store', dest='highlights', - default=None, - help="""Setting this overrides the default number of - highlight animation loops to have per call.""") - parser.addoption('--message_duration', action="store", - dest='message_duration', - default=None, - help="""Setting this overrides the default time that - messenger notifications remain visible when reaching - assert statements during Demo Mode.""") - parser.addoption('--check_js', action="store_true", - dest='js_checking_on', - default=False, - help="""The option to check for JavaScript errors after - every page load.""") - parser.addoption('--ad_block', action="store_true", - dest='ad_block_on', - default=False, - help="""Using this makes WebDriver block display ads - that are defined in ad_block_list.AD_BLOCK_LIST.""") - parser.addoption('--verify_delay', action='store', dest='verify_delay', - default=None, - help="""Setting this overrides the default wait time - before each MasterQA verification pop-up.""") - parser.addoption('--disable_csp', action="store_true", - dest='disable_csp', - default=False, - help="""Using this disables the Content Security Policy of - websites, which may interfere with some features of - SeleniumBase, such as loading custom JavaScript - libraries for various testing actions. - Setting this to True (--disable_csp) overrides the - value set in seleniumbase/config/settings.py""") - parser.addoption('--save_screenshot', action='store_true', - dest='save_screenshot', - default=False, - help="""Take a screenshot on last page after the last step - of the test. (Added to the "latest_logs" folder.)""") - parser.addoption('--visual_baseline', action='store_true', - dest='visual_baseline', - default=False, - help="""Setting this resets the visual baseline for - Automated Visual Testing with SeleniumBase. - When a test calls self.check_window(), it will - rebuild its files in the visual_baseline folder.""") - parser.addoption('--timeout_multiplier', action='store', - dest='timeout_multiplier', - default=None, - help="""Setting this overrides the default timeout - by the multiplier when waiting for page elements. - Unused when tests overide the default value.""") + """This plugin adds the following command-line options to pytest: + --browser=BROWSER (The web browser to use. Default: "chrome".) + --chrome (Shortcut for "--browser=chrome". Default.) + --edge (Shortcut for "--browser=edge".) + --firefox (Shortcut for "--browser=firefox".) + --safari (Shortcut for "--browser=safari".) + --opera (Shortcut for "--browser=opera".) + --brave (Shortcut for "--browser=brave".) + --comet (Shortcut for "--browser=comet".) + --chromium (Shortcut for using base `Chromium`) + --cft (Shortcut for using `Chrome for Testing`) + --chs (Shortcut for using `Chrome-Headless-Shell`) + --settings-file=FILE (Override default SeleniumBase settings.) + --env=ENV (Set the test env. Access with "self.env" in tests.) + --account=STR (Set account. Access with "self.account" in tests.) + --data=STRING (Extra test data. Access with "self.data" in tests.) + --var1=STRING (Extra test data. Access with "self.var1" in tests.) + --var2=STRING (Extra test data. Access with "self.var2" in tests.) + --var3=STRING (Extra test data. Access with "self.var3" in tests.) + --variables=DICT (Extra test data. Access with "self.variables".) + --user-data-dir=DIR (Set the Chrome user data directory to use.) + --protocol=PROTOCOL (The Selenium Grid protocol: http|https.) + --server=SERVER (The Selenium Grid server/IP used for tests.) + --port=PORT (The Selenium Grid port used by the test server.) + --cap-file=FILE (The web browser's desired capabilities to use.) + --cap-string=STRING (The web browser's desired capabilities to use.) + --proxy=SERVER:PORT (Connect to a proxy server:port as tests are running) + --proxy=USERNAME:PASSWORD@SERVER:PORT (Use an authenticated proxy server) + --proxy-bypass-list=STRING (";"-separated hosts to bypass, Eg "*.foo.com") + --proxy-pac-url=URL (Connect to a proxy server using a PAC_URL.pac file.) + --proxy-pac-url=USERNAME:PASSWORD@URL (Authenticated proxy with PAC URL.) + --proxy-driver (If a driver download is needed, will use: --proxy=PROXY.) + --multi-proxy (Allow multiple authenticated proxies when multi-threaded.) + --agent=STRING (Modify the web browser's User-Agent string.) + --mobile (Use the mobile device emulator while running tests.) + --metrics=STRING (Set mobile metrics: "CSSWidth,CSSHeight,PixelRatio".) + --chromium-arg="ARG=N,ARG2" (Set Chromium args, ","-separated, no spaces.) + --firefox-arg="ARG=N,ARG2" (Set Firefox args, comma-separated, no spaces.) + --firefox-pref=SET (Set a Firefox preference:value set, comma-separated.) + --extension-zip=ZIP (Load a Chrome Extension .zip|.crx, comma-separated.) + --extension-dir=DIR (Load a Chrome Extension directory, comma-separated.) + --disable-features="F1,F2" (Disable features, comma-separated, no spaces.) + --binary-location=PATH (Set path of the Chromium browser binary to use.) + --driver-version=VER (Set the chromedriver or uc_driver version to use.) + --sjw (Skip JS Waits for readyState to be "complete" or Angular to load.) + --wfa (Wait for AngularJS to be done loading after specific web actions.) + --pls=PLS (Set pageLoadStrategy on Chrome: "normal", "eager", or "none".) + --headless (The default headless mode. Linux uses this mode by default.) + --headless1 (Use Chrome's old headless mode. Fast, but has limitations.) + --headless2 (Use Chrome's new headless mode, which supports extensions.) + --headed (Run tests in headed/GUI mode on Linux OS, where not default.) + --xvfb (Run tests using the Xvfb virtual display server on Linux OS.) + --xvfb-metrics=STRING (Set Xvfb display size on Linux: "Width,Height".) + --locale=LOCALE_CODE (Set the Language Locale Code for the web browser.) + --interval=SECONDS (The autoplay interval for presentations & tour steps) + --start-page=URL (The starting URL for the web browser when tests begin.) + --archive-logs (Archive existing log files instead of deleting them.) + --archive-downloads (Archive old downloads instead of deleting them.) + --time-limit=SECONDS (Safely fail any test that exceeds the time limit.) + --slow (Slow down the automation. Faster than using Demo Mode.) + --demo (Slow down and visually see test actions as they occur.) + --demo-sleep=SECONDS (Set the wait time after Slow & Demo Mode actions.) + --highlights=NUM (Number of highlight animations for Demo Mode actions.) + --message-duration=SECONDS (The time length for Messenger alerts.) + --check-js (Check for JavaScript errors after page loads.) + --ad-block (Block some types of display ads from loading.) + --host-resolver-rules=RULES (Set host-resolver-rules, comma-separated.) + --block-images (Block images from loading during tests.) + --do-not-track (Indicate to websites that you don't want to be tracked.) + --verify-delay=SECONDS (The delay before MasterQA verification checks.) + --ee / --esc-end (Lets the user end the current test via the ESC key.) + --recorder (Enables the Recorder for turning browser actions into code.) + --rec-sb-mgr (A Recorder Mode that generates SB() context manager code.) + --rec-sb-cdp (A Recorder Mode that generates Pure CDP Mode sb_cdp code.) + --rec-behave (Same as Recorder Mode, but also generates behave-gherkin.) + --rec-sleep (If the Recorder is enabled, also records self.sleep calls.) + --rec-print (If the Recorder is enabled, prints output after tests end.) + --disable-cookies (Disable Cookies on websites. Pages might break!) + --disable-js (Disable JavaScript on websites. Pages might break!) + --disable-csp (Disable the Content Security Policy of websites.) + --disable-ws (Disable Web Security on Chromium-based browsers.) + --enable-ws (Enable Web Security on Chromium-based browsers.) + --enable-sync (Enable "Chrome Sync" on websites.) + --uc | --undetected (Use undetected-chromedriver to evade bot-detection.) + --uc-cdp-events (Capture CDP events when running in "--undetected" mode.) + --log-cdp ("goog:loggingPrefs", {"performance": "ALL", "browser": "ALL"}) + --remote-debug (Sync to Chrome Remote Debugger chrome://inspect/#devices) + --ftrace | --final-trace (Debug Mode after each test. Don't use with CI!) + --dashboard (Enable the SeleniumBase Dashboard. Saved at: dashboard.html) + --dash-title=STRING (Set the title shown for the generated dashboard.) + --enable-3d-apis (Enables WebGL and 3D APIs.) + --swiftshader (Chrome "--use-gl=angle" / "--use-angle=swiftshader-webgl") + --incognito (Enable Chrome's Incognito mode.) + --guest (Enable Chrome's Guest mode.) + --dark (Enable Chrome's Dark mode.) + --devtools (Open Chrome's DevTools when the browser opens.) + --rs | --reuse-session (Reuse browser session for all tests.) + --rcs | --reuse-class-session (Reuse session for tests in class.) + --crumbs (Delete all cookies between tests reusing a session.) + --disable-beforeunload (Disable the "beforeunload" event on Chrome.) + --window-position=X,Y (Set the browser's starting window position.) + --window-size=WIDTH,HEIGHT (Set the browser's starting window size.) + --maximize (Start tests with the browser window maximized.) + --screenshot (Save a screenshot at the end of each test.) + --no-screenshot (No screenshots saved unless tests directly ask it.) + --visual-baseline (Set the visual baseline for Visual/Layout tests.) + --external-pdf (Set Chromium "plugins.always_open_pdf_externally":True.) + --timeout-multiplier=MULTIPLIER (Multiplies the default timeout values.) + --list-fail-page (After each failing test, list the URL of the failure.) + """ + c1 = "" + c2 = "" + c3 = "" + cr = "" + if "linux" not in sys.platform: + # This will be seen when typing "pytest --help" on the command line. + c1 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.MAGENTA + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + s_str = "SeleniumBase" + s_str = s_str.replace("SeleniumBase", c1 + "Selenium" + c2 + "Base" + cr) + s_str = s_str + cr + " " + c3 + "command-line options for pytest" + cr + parser = parser.getgroup("SeleniumBase", s_str) + parser.addoption( + "--browser", + action="store", + dest="browser", + type=str.lower, + choices=constants.ValidBrowsers.valid_browsers, + default=constants.Browser.GOOGLE_CHROME, + help="""Specifies the web browser to use. Default: Chrome. + Examples: (--browser=edge OR --browser=firefox)""", + ) + parser.addoption( + "--chrome", + action="store_true", + dest="use_chrome", + default=False, + help="""Shortcut for --browser=chrome (Default)""", + ) + parser.addoption( + "--edge", + action="store_true", + dest="use_edge", + default=False, + help="""Shortcut for --browser=edge""", + ) + parser.addoption( + "--firefox", + action="store_true", + dest="use_firefox", + default=False, + help="""Shortcut for --browser=firefox""", + ) + parser.addoption( + "--ie", + action="store_true", + dest="use_ie", + default=False, + help="""Shortcut for --browser=ie""", + ) + parser.addoption( + "--safari", + action="store_true", + dest="use_safari", + default=False, + help="""Shortcut for --browser=safari""", + ) + parser.addoption( + "--opera", + action="store_true", + dest="use_opera", + default=False, + help="""Shortcut for --browser=opera""", + ) + parser.addoption( + "--brave", + action="store_true", + dest="use_brave", + default=False, + help="""Shortcut for --browser=brave""", + ) + parser.addoption( + "--comet", + action="store_true", + dest="use_comet", + default=False, + help="""Shortcut for --browser=comet""", + ) + parser.addoption( + "--use-chromium", + "--chromium", + action="store_true", + dest="use_chromium", + default=False, + help="""Shortcut for using base `Chromium`""", + ) + parser.addoption( + "--cft", + action="store_true", + dest="use_cft", + default=False, + help="""Shortcut for using `Chrome for Testing`""", + ) + parser.addoption( + "--chs", + action="store_true", + dest="use_chs", + default=False, + help="""Shortcut for using `Chrome-Headless-Shell`""", + ) + parser.addoption( + "--with-selenium", + action="store_true", + dest="with_selenium", + default=True, + help="""(DEPRECATED) Start tests with an open web browser. + (This is ALWAYS True now when importing BaseCase)""", + ) + parser.addoption( + "--env", + action="store", + dest="environment", + type=str.lower, + choices=( + constants.Environment.QA, + constants.Environment.RC, + constants.Environment.STAGING, + constants.Environment.DEVELOP, + constants.Environment.PRODUCTION, + constants.Environment.PERFORMANCE, + constants.Environment.REPLICA, + constants.Environment.FEDRAMP, + constants.Environment.OFFLINE, + constants.Environment.ONLINE, + constants.Environment.MASTER, + constants.Environment.REMOTE, + constants.Environment.LEGACY, + constants.Environment.LOCAL, + constants.Environment.ALPHA, + constants.Environment.BETA, + constants.Environment.DEMO, + constants.Environment.GDPR, + constants.Environment.MAIN, + constants.Environment.TEST, + constants.Environment.GOV, + constants.Environment.NEW, + constants.Environment.OLD, + constants.Environment.UAT, + ), + default=constants.Environment.TEST, + help="""This option sets a test env from a list of choices. + Access using "self.env" or "self.environment".""", + ) + parser.addoption( + "--account", + dest="account", + default=None, + help="""This option sets a test account string. + In tests, use "self.account" to get the value.""", + ) + parser.addoption( + "--data", + dest="data", + default=None, + help="Extra data to pass to tests from the command line.", + ) + parser.addoption( + "--var1", + dest="var1", + default=None, + help="Extra data to pass to tests from the command line.", + ) + parser.addoption( + "--var2", + dest="var2", + default=None, + help="Extra data to pass to tests from the command line.", + ) + parser.addoption( + "--var3", + dest="var3", + default=None, + help="Extra data to pass to tests from the command line.", + ) + parser.addoption( + "--variables", + dest="variables", + default=None, + help="""A var dict to pass to tests from the command line. + Example usage: + ---------------------------------------------- + Option: --variables='{"special":123}' + Access: self.variables["special"] # (123) + ---------------------------------------------- + Option: --variables='{"color":"red","num":42}' + Access: self.variables["color"] # ("red") + Access: self.variables["num"] # (42) + ----------------------------------------------""", + ) + parser.addoption( + "--cap_file", + "--cap-file", + dest="cap_file", + default=None, + help="""The file that stores browser desired capabilities + for BrowserStack, Sauce Labs, or other grids.""", + ) + parser.addoption( + "--cap_string", + "--cap-string", + dest="cap_string", + default=None, + help="""The string that stores browser desired capabilities + for BrowserStack, Sauce Labs, or other grids. + Enclose cap-string in single quotes. + Enclose parameter keys in double quotes. + Example: --cap-string='{"name":"test1","v":"42"}'""", + ) + parser.addoption( + "--settings_file", + "--settings-file", + "--settings", + action="store", + dest="settings_file", + default=None, + help="""The file that stores key/value pairs for + overriding values in the + seleniumbase/config/settings.py file.""", + ) + parser.addoption( + "--user_data_dir", + "--user-data-dir", + dest="user_data_dir", + default=None, + help="""The Chrome User Data Directory to use. (Profile) + If the directory doesn't exist, it'll be created.""", + ) + parser.addoption( + "--with-testing_base", + "--with-testing-base", + action="store_true", + dest="with_testing_base", + default=True, + help="""(DEPRECATED) - This option is always enabled now. + Use for saving logs & screenshots when tests fail. + The following options are now active by default + with --with-testing_base (which is always on now): + --with-screen_shots , + --with-basic_test_info , + --with-page_source + """, + ) + parser.addoption( + "--log_path", + "--log-path", + dest="log_path", + default=constants.Logs.LATEST + "/", + help="""(DEPRECATED) - This value is NOT EDITABLE anymore. + Log files are saved to the "latest_logs/" folder.""", + ) + parser.addoption( + "--archive_logs", + "--archive-logs", + action="store_true", + dest="archive_logs", + default=False, + help="Archive old log files instead of deleting them.", + ) + parser.addoption( + "--archive_downloads", + "--archive-downloads", + action="store_true", + dest="archive_downloads", + default=False, + help="Archive old downloads instead of deleting them.", + ) + parser.addoption( + "--sjw", + "--skip_js_waits", + "--skip-js-waits", + action="store_true", + dest="skip_js_waits", + default=False, + help="""Skip all calls to wait_for_ready_state_complete() + and wait_for_angularjs(), which are part of many + SeleniumBase methods for improving reliability.""", + ) + parser.addoption( + "--wfa", + "--wait_for_angularjs", + "--wait-for-angularjs", + action="store_true", + dest="wait_for_angularjs", + default=False, + help="""Add waiting for AngularJS. (The default setting + was changed to no longer wait for AngularJS to + finish loading as an extra JavaScript call.)""", + ) + parser.addoption( + "--with-db_reporting", + "--with-db-reporting", + action="store_true", + dest="with_db_reporting", + default=False, + help="Use to record test data in the MySQL database.", + ) + parser.addoption( + "--database_env", + "--database-env", + action="store", + dest="database_env", + choices=( + constants.Environment.QA, + constants.Environment.RC, + constants.Environment.STAGING, + constants.Environment.DEVELOP, + constants.Environment.PRODUCTION, + constants.Environment.PERFORMANCE, + constants.Environment.REPLICA, + constants.Environment.FEDRAMP, + constants.Environment.OFFLINE, + constants.Environment.ONLINE, + constants.Environment.MASTER, + constants.Environment.REMOTE, + constants.Environment.LEGACY, + constants.Environment.LOCAL, + constants.Environment.ALPHA, + constants.Environment.BETA, + constants.Environment.DEMO, + constants.Environment.GDPR, + constants.Environment.MAIN, + constants.Environment.TEST, + constants.Environment.GOV, + constants.Environment.NEW, + constants.Environment.OLD, + constants.Environment.UAT, + ), + default=constants.Environment.TEST, + help="The database environment to run the tests in.", + ) + parser.addoption( + "--with-s3_logging", + "--with-s3-logging", + action="store_true", + dest="with_s3_logging", + default=False, + help="Use to save test log files in Amazon S3.", + ) + parser.addoption( + "--with-screen_shots", + "--with-screen-shots", + action="store_true", + dest="with_screen_shots", + default=False, + help="""(DEPRECATED) - Screenshots are always saved now. + This option saves screenshots during test failures. + Screenshots are saved in the "latest_logs/" folder. + (Automatically on when using --with-testing_base)""", + ) + parser.addoption( + "--with-basic_test_info", + "--with-basic-test-info", + action="store_true", + dest="with_basic_test_info", + default=False, + help="""(DEPRECATED) - Info files are always saved now. + This option saves basic test info on test failures. + These files are saved in the "latest_logs/" folder. + (Automatically on when using --with-testing_base)""", + ) + parser.addoption( + "--with-page_source", + "--with-page-source", + action="store_true", + dest="with_page_source", + default=False, + help="""(DEPRECATED) - Page source is saved by default. + This option saves page source files on test failures. + (Automatically on when using --with-testing_base)""", + ) + parser.addoption( + "--protocol", + action="store", + dest="protocol", + choices=( + constants.Protocol.HTTP, + constants.Protocol.HTTPS, + ), + default=constants.Protocol.HTTP, + help="""Designates the Selenium Grid protocol to use. + Default: http.""", + ) + parser.addoption( + "--server", + action="store", + dest="servername", + default="localhost", + help="""Designates the Selenium Grid server to use. + Use "127.0.0.1" to connect to a localhost Grid. + If unset or set to "localhost", Grid isn't used. + Default: "localhost".""", + ) + parser.addoption( + "--port", + action="store", + dest="port", + default="4444", + help="""Designates the Selenium Grid port to use. + Default: 4444. (If 443, protocol becomes "https")""", + ) + parser.addoption( + "--proxy", + "--proxy-server", + "--proxy-string", + action="store", + dest="proxy_string", + default=None, + help="""Designates the proxy server:port to use. + Format: servername:port. OR + username:password@servername:port OR + A dict key from proxy_list.PROXY_LIST + Default: None.""", + ) + parser.addoption( + "--proxy-bypass-list", + "--proxy_bypass_list", + action="store", + dest="proxy_bypass_list", + default=None, + help="""Designates the hosts, domains, and/or IP addresses + to bypass when using a proxy server with "--proxy". + Format: A ";"-separated string. + Example usage: + pytest + --proxy="username:password@servername:port" + --proxy-bypass-list="*.foo.com;github.com" + pytest + --proxy="servername:port" + --proxy-bypass-list="127.0.0.1:8080" + Default: None.""", + ) + parser.addoption( + "--proxy-pac-url", + "--pac-url", + action="store", + dest="proxy_pac_url", + default=None, + help="""Designates the proxy PAC URL to use. + Format: A URL string OR + A username:password@URL string + Default: None.""", + ) + parser.addoption( + "--proxy-driver", + "--proxy_driver", + action="store_true", + dest="proxy_driver", + default=False, + help="""If a driver download is needed for tests, + uses proxy settings set via --proxy=PROXY.""", + ) + parser.addoption( + "--multi-proxy", + "--multi_proxy", + action="store_true", + dest="multi_proxy", + default=False, + help="""If you need to run multi-threaded tests with + multiple proxies that require authentication, + set this to allow multiple configurations.""", + ) + parser.addoption( + "--agent", + "--user-agent", + "--user_agent", + action="store", + dest="user_agent", + default=None, + help="""Designates the User-Agent for the browser to use. + Format: A string. + Default: None.""", + ) + parser.addoption( + "--mobile", + "--mobile-emulator", + "--mobile_emulator", + action="store_true", + dest="mobile_emulator", + default=False, + help="""If this option is enabled, the mobile emulator + will be used while running tests.""", + ) + parser.addoption( + "--metrics", + "--device-metrics", + "--device_metrics", + action="store", + dest="device_metrics", + default=None, + help="""Designates the three device metrics of the mobile + emulator: CSS Width, CSS Height, and Pixel-Ratio. + Format: A comma-separated string with the 3 values. + Examples: "375,734,5" or "412,732,3" or "390,715,3" + Default: None. (Will use default values if None)""", + ) + parser.addoption( + "--chromium_arg", + "--chromium-arg", + action="store", + dest="chromium_arg", + default=None, + help="""Add a Chromium argument for Chrome/Edge browsers. + Format: A comma-separated list of Chromium args. + If an arg doesn't start with "--", that will be + added to the beginning of the arg automatically. + Default: None.""", + ) + parser.addoption( + "--firefox_arg", + "--firefox-arg", + action="store", + dest="firefox_arg", + default=None, + help="""Add a Firefox argument for Firefox browser runs. + Format: A comma-separated list of Firefox args. + If an arg doesn't start with "--", that will be + added to the beginning of the arg automatically. + Default: None.""", + ) + parser.addoption( + "--firefox_pref", + "--firefox-pref", + action="store", + dest="firefox_pref", + default=None, + help="""Set a Firefox preference:value combination. + Format: A comma-separated list of pref:value items. + Example usage: + --firefox-pref="browser.formfill.enable:True" + --firefox-pref="pdfjs.disabled:False" + --firefox-pref="abc.def.xyz:42,hello.world:text" + Boolean and integer values to the right of the ":" + will be automatically converted into proper format. + If there's no ":" in the string, then True is used. + Default: None.""", + ) + parser.addoption( + "--extension_zip", + "--extension-zip", + "--crx", + action="store", + dest="extension_zip", + default=None, + help="""Designates the Chrome Extension ZIP file to load. + Format: A comma-separated list of .zip or .crx files + containing the Chrome extensions to load. + Default: None.""", + ) + parser.addoption( + "--extension_dir", + "--extension-dir", + action="store", + dest="extension_dir", + default=None, + help="""Designates the Chrome Extension folder to load. + Format: A directory containing the Chrome extension. + (Can also be a comma-separated list of directories.) + Default: None.""", + ) + parser.addoption( + "--disable_features", + "--disable-features", + action="store", + dest="disable_features", + default=None, + help="""Disable Chromium features from Chrome/Edge browsers. + Format: A comma-separated list of Chromium features. + Default: None.""", + ) + parser.addoption( + "--binary_location", + "--binary-location", + "--bl", + action="store", + dest="binary_location", + default=None, + help="""Sets the path of the Chromium browser binary to use. + Uses the default location if not os.path.exists(PATH)""", + ) + parser.addoption( + "--driver_version", + "--driver-version", + action="store", + dest="driver_version", + default=None, + help="""Setting this overrides the default driver version, + which is set to match the detected browser version. + Major version only. Example: "--driver-version=114" + (Only chromedriver and uc_driver are affected.)""", + ) + parser.addoption( + "--pls", + "--page_load_strategy", + "--page-load-strategy", + action="store", + dest="page_load_strategy", + type=str.lower, + choices=( + constants.PageLoadStrategy.NORMAL, + constants.PageLoadStrategy.EAGER, + constants.PageLoadStrategy.NONE, + ), + default=None, + help="""This option sets Chrome's pageLoadStrategy. + List of choices: "normal", "eager", "none".""", + ) + parser.addoption( + "--headless", + action="store_true", + dest="headless", + default=False, + help="""Using this option activates headless mode, + which is required on headless machines + UNLESS using a virtual display with Xvfb. + Default: False on Mac/Windows. True on Linux.""", + ) + parser.addoption( + "--headless1", + action="store_true", + dest="headless1", + default=False, + help="""This option activates the old headless mode, + which is faster, but has limitations. + (May be phased out by Chrome in the future.)""", + ) + parser.addoption( + "--headless2", + action="store_true", + dest="headless2", + default=False, + help="""This option activates the new headless mode, + which supports Chromium extensions, and more, + but is slower than the standard headless mode.""", + ) + parser.addoption( + "--headed", + "--gui", + action="store_true", + dest="headed", + default=False, + help="""Using this makes Webdriver run web browsers with + a GUI when running tests on Linux machines. + (The default setting on Linux is headless.) + (The default setting on Mac or Windows is headed.)""", + ) + parser.addoption( + "--xvfb", + action="store_true", + dest="xvfb", + default=False, + help="""Using this makes tests run headlessly using Xvfb + instead of the browser's built-in headless mode. + When using "--xvfb", the "--headless" option + will no longer be enabled by default on Linux. + Default: False. (Linux-ONLY!)""", + ) + parser.addoption( + "--xvfb-metrics", + "--xvfb_metrics", + action="store", + dest="xvfb_metrics", + default=None, + help="""Customize the Xvfb metrics (Width,Height) on Linux. + Format: A comma-separated string with the 2 values. + Examples: "1920,1080" or "1366,768" or "1024,768". + Default: None. (None: "1366,768". Min: "1024,768".)""", + ) + parser.addoption( + "--locale_code", + "--locale-code", + "--locale", + action="store", + dest="locale_code", + default=None, + help="""Designates the Locale Code for the web browser. + A Locale is a specific version of a spoken Language. + The Locale alters visible text on supported websites. + See: https://seleniumbase.io/help_docs/locale_codes/ + Default: None. (The web browser's default mode.)""", + ) + parser.addoption( + "--interval", + action="store", + dest="interval", + default=None, + help="""This globally overrides the default interval, + (in seconds), of features that include autoplay + functionality, such as tours and presentations. + Overrides from methods take priority over this. + (Headless Mode skips tours and presentations.)""", + ) + parser.addoption( + "--start_page", + "--start-page", + "--url", + action="store", + dest="start_page", + default=None, + help="""Designates the starting URL for the web browser + when each test begins. + Default: None.""", + ) + parser.addoption( + "--is_pytest", + "--is-pytest", + action="store_true", + dest="is_pytest", + default=True, + help="""This is used by the BaseCase class to tell apart + pytest runs from nosetest runs. (Automatic)""", + ) + parser.addoption( + "--all-scripts", + "--all_scripts", + action="store_true", + dest="all_scripts", + default=False, + help="""Use this to run `SB()`, `DriverContext()` and + `Driver()` scripts that are discovered during + the pytest collection phase.""", + ) + parser.addoption( + "--time_limit", + "--time-limit", + "--timelimit", + action="store", + dest="time_limit", + default=None, + help="""Use this to set a time limit per test, in seconds. + If a test runs beyond the limit, it fails.""", + ) + parser.addoption( + "--slow_mode", + "--slow-mode", + "--slowmo", + "--slow", + action="store_true", + dest="slow_mode", + default=False, + help="""Using this slows down the automation.""", + ) + parser.addoption( + "--demo_mode", + "--demo-mode", + "--demo", + action="store_true", + dest="demo_mode", + default=False, + help="""Using this slows down the automation and lets you + visually see what the tests are actually doing.""", + ) + parser.addoption( + "--demo_sleep", + "--demo-sleep", + action="store", + dest="demo_sleep", + default=None, + help="""Setting this overrides the Demo Mode sleep + time that happens after browser actions.""", + ) + parser.addoption( + "--highlights", + action="store", + dest="highlights", + default=None, + help="""Setting this overrides the default number of + highlight animation loops to have per call.""", + ) + parser.addoption( + "--message_duration", + "--message-duration", + action="store", + dest="message_duration", + default=None, + help="""Setting this overrides the default time that + messenger notifications remain visible when + reaching assert statements during Demo Mode.""", + ) + parser.addoption( + "--check_js", + "--check-js", + action="store_true", + dest="js_checking_on", + default=False, + help="""The option to check for JavaScript errors after + every page load.""", + ) + parser.addoption( + "--adblock", + "--ad_block", + "--ad-block", + "--block_ads", + "--block-ads", + action="store_true", + dest="ad_block_on", + default=False, + help="""Using this makes WebDriver block display ads + that are defined in ad_block_list.AD_BLOCK_LIST.""", + ) + parser.addoption( + "--host_resolver_rules", + "--host-resolver-rules", + action="store", + dest="host_resolver_rules", + default=None, + help="""Use this option to set "host-resolver-rules". + This lets you re-map traffic from any domain. + Eg. "MAP www.google-analytics.com 0.0.0.0". + Eg. "MAP * ~NOTFOUND , EXCLUDE myproxy". + Eg. "MAP * 0.0.0.0 , EXCLUDE 127.0.0.1". + Eg. "MAP *.google.com myproxy". + Find more examples on these pages: + (https://www.electronjs.org/docs/ + latest/api/command-line-switches) + (https://www.chromium.org/developers/ + design-documents/network-stack/socks-proxy/) + Use comma-separation for multiple host rules.""", + ) + parser.addoption( + "--block_images", + "--block-images", + action="store_true", + dest="block_images", + default=False, + help="""Using this makes WebDriver block images from + loading on web pages during tests.""", + ) + parser.addoption( + "--do_not_track", + "--do-not-track", + action="store_true", + dest="do_not_track", + default=False, + help="""Indicate to websites that you don't want to be + tracked. The browser will send an extra HTTP + header each time it requests a web page. + https://support.google.com/chrome/answer/2790761""", + ) + parser.addoption( + "--verify_delay", + "--verify-delay", + action="store", + dest="verify_delay", + default=None, + help="""Setting this overrides the default wait time + before each MasterQA verification pop-up.""", + ) + parser.addoption( + "--esc-end", + "--esc_end", + "--ee", + action="store_true", + dest="esc_end", + default=False, + help="""End the current test early via the ESC key. + The test will be marked as skipped.""", + ) + parser.addoption( + "--recorder", + "--record", + "--rec", + "--codegen", + action="store_true", + dest="recorder_mode", + default=False, + help="""Using this enables the SeleniumBase Recorder, + which records browser actions for converting + into SeleniumBase scripts.""", + ) + parser.addoption( + "--rec-sb-mgr", + action="store_true", + dest="rec_sb_mgr", + default=False, + help="""Not only enables the SeleniumBase Recorder, + but also saves recorded actions into the + context manager format.""", + ) + parser.addoption( + "--rec-sb-cdp", + action="store_true", + dest="rec_sb_cdp", + default=False, + help="""Not only enables the SeleniumBase Recorder, + but also saves recorded actions into the + Pure CDP Mode sb_cdp sync format.""", + ) + parser.addoption( + "--rec-behave", + "--rec-gherkin", + action="store_true", + dest="rec_behave", + default=False, + help="""Not only enables the SeleniumBase Recorder, + but also saves recorded actions into the + behave-gerkin format, which includes a + feature file, an imported steps file, + and the environment.py file.""", + ) + parser.addoption( + "--rec-sleep", + "--record-sleep", + action="store_true", + dest="record_sleep", + default=False, + help="""If Recorder Mode is enabled, + records sleep(seconds) calls.""", + ) + parser.addoption( + "--rec-print", + action="store_true", + dest="rec_print", + default=False, + help="""If Recorder Mode is enabled, + prints output after tests end.""", + ) + parser.addoption( + "--disable_js", + "--disable-js", + action="store_true", + dest="disable_js", + default=False, + help="""The option to disable JavaScript on web pages. + Warning: Most web pages will stop working!""", + ) + parser.addoption( + "--disable_cookies", + "--disable-cookies", + action="store_true", + dest="disable_cookies", + default=False, + help="""The option to disable Cookies on web pages. + Warning: Several pages may stop working!""", + ) + parser.addoption( + "--disable_csp", + "--disable-csp", + "--no_csp", + "--no-csp", + "--dcsp", + action="store_true", + dest="disable_csp", + default=False, + help="""Using this disables the Content Security Policy of + websites, which may interfere with some features of + SeleniumBase, such as loading custom JavaScript + libraries for various testing actions. + Setting this to True (--disable-csp) overrides the + value set in seleniumbase/config/settings.py""", + ) + parser.addoption( + "--disable_ws", + "--disable-ws", + "--dws", + "--disable-web-security", + action="store_true", + dest="disable_ws", + default=False, + help="""Using this disables the "Web Security" feature of + Chrome and Chromium-based browsers such as Edge.""", + ) + parser.addoption( + "--enable_ws", + "--enable-ws", + "--enable-web-security", + action="store_true", + dest="enable_ws", + default=False, + help="""Using this enables the "Web Security" feature of + Chrome and Chromium-based browsers such as Edge.""", + ) + parser.addoption( + "--enable_sync", + "--enable-sync", + action="store_true", + dest="enable_sync", + default=False, + help="""Using this enables the "Chrome Sync" feature.""", + ) + parser.addoption( + "--use_auto_ext", + "--use-auto-ext", + "--auto-ext", + action="store_true", + dest="use_auto_ext", + default=False, + help="""(DEPRECATED) - Enable the automation extension. + It's not required, but some commands & advanced + features may need it.""", + ) + parser.addoption( + "--undetected", + "--undetectable", + "--uc", # undetected-chromedriver + action="store_true", + dest="undetectable", + default=False, + help="""Using this option makes chromedriver undetectable + to websites that use anti-bot services to block + automation tools from navigating them freely.""", + ) + parser.addoption( + "--uc_cdp_events", + "--uc-cdp-events", + "--uc-cdp", # For capturing CDP events during UC Mode + action="store_true", + dest="uc_cdp_events", + default=None, + help="""Captures CDP events during Undetectable Mode runs. + Then you can add a listener to perform actions on + received data, such as printing it to the console: + from pprint import pformat + self.driver.add_cdp_listener( + "*", lambda data: print(pformat(data)) + ) + self.goto(url)""", + ) + parser.addoption( + "--uc_subprocess", + "--uc-subprocess", + "--uc-sub", # undetected-chromedriver subprocess mode + action="store_true", + dest="uc_subprocess", + default=None, + help="""(DEPRECATED) - (UC Mode always uses this now.) + Use undetectable-chromedriver as a subprocess, + which can help avoid issues that might result.""", + ) + parser.addoption( + "--no_sandbox", + "--no-sandbox", + action="store_true", + dest="no_sandbox", + default=False, + help="""(DEPRECATED) - "--no-sandbox" is always used now. + Using this enables the "No Sandbox" feature. + (This setting is now always enabled by default.)""", + ) + parser.addoption( + "--disable_gpu", + "--disable-gpu", + action="store_true", + dest="disable_gpu", + default=False, + help="""(DEPRECATED) - GPU is disabled if no swiftshader. + Using this enables the "Disable GPU" feature. + (GPU is disabled by default if swiftshader off.)""", + ) + parser.addoption( + "--log_cdp", + "--log-cdp", + "--log_cdp_events", + "--log-cdp-events", + action="store_true", + dest="log_cdp_events", + default=None, + help="""Capture CDP events. Then you can print them. + Eg. print(driver.get_log("performance"))""", + ) + parser.addoption( + "--remote_debug", + "--remote-debug", + "--remote-debugger", + "--remote_debugger", + action="store_true", + dest="remote_debug", + default=False, + help="""This syncs the browser to Chromium's remote debugger. + To access the remote debugging interface, go to: + chrome://inspect/#devices while tests are running. + The previous URL was at: http://localhost:9222/ + Info: chromedevtools.github.io/devtools-protocol/""", + ) + parser.addoption( + "--final-debug", + "--final-trace", + "--fdebug", + "--ftrace", + action="store_true", + dest="final_debug", + default=False, + help="""Enter Debug Mode at the end of each test. + To enter Debug Mode only on failures, use "--pdb". + If using both "--final-debug" and "--pdb" together, + then Debug Mode will activate twice on failures.""", + ) + parser.addoption( + "--dashboard", + action="store_true", + dest="dashboard", + default=False, + help="""Using this enables the SeleniumBase Dashboard. + To access the SeleniumBase Dashboard interface, + open the dashboard.html file located in the same + folder that the pytest command was run from.""", + ) + parser.addoption( + "--dash_title", + "--dash-title", + dest="dash_title", + default=None, + help="Set the title shown for the generated dashboard.", + ) + parser.addoption( + "--enable_3d_apis", + "--enable-3d-apis", + action="store_true", + dest="enable_3d_apis", + default=False, + help="""Using this enables WebGL and 3D APIs.""", + ) + parser.addoption( + "--swiftshader", + action="store_true", + dest="swiftshader", + default=False, + help="""Using this enables the "--use-gl=swiftshader" + feature when running tests on Chrome.""", + ) + parser.addoption( + "--incognito", + "--incognito_mode", + "--incognito-mode", + action="store_true", + dest="incognito", + default=False, + help="""Using this enables Chrome's Incognito mode.""", + ) + parser.addoption( + "--guest", + "--guest_mode", + "--guest-mode", + action="store_true", + dest="guest_mode", + default=False, + help="""Using this enables Chrome's Guest mode.""", + ) + parser.addoption( + "--dark", + "--dark_mode", + "--dark-mode", + action="store_true", + dest="dark_mode", + default=False, + help="""Using this enables Chrome's Dark mode.""", + ) + parser.addoption( + "--devtools", + "--open_devtools", + "--open-devtools", + action="store_true", + dest="devtools", + default=False, + help="""Using this opens Chrome's DevTools.""", + ) + parser.addoption( + "--rs", + "--reuse_session", + "--reuse-session", + action="store_true", + dest="reuse_session", + default=False, + help="""The option to reuse the selenium browser window + session for all tests.""", + ) + parser.addoption( + "--rcs", + "--reuse_class_session", + "--reuse-class-session", + action="store_true", + dest="reuse_class_session", + default=False, + help="""The option to reuse the selenium browser window + session for all tests within the same class.""", + ) + parser.addoption( + "--crumbs", + action="store_true", + dest="crumbs", + default=False, + help="""The option to delete all cookies between tests + that reuse the same browser session. This option + is only useful if using "--reuse-session"/"--rs" + or "--reuse-class-session"/"--rcs" because tests + use a new clean browser if not reusing sessions.""", + ) + parser.addoption( + "--disable-beforeunload", + "--disable_beforeunload", + action="store_true", + dest="_disable_beforeunload", + default=False, + help="""The option to disable the "beforeunload" event + on Chromium browsers (Chrome or Edge). + This is already the default Firefox option.""", + ) + parser.addoption( + "--window-position", + "--window_position", + action="store", + dest="window_position", + default=None, + help="""The option to set the starting window x,y position + Format: A comma-separated string with the 2 values. + Example: "55,66" + Default: None. (Will use default values if None)""", + ) + parser.addoption( + "--window-size", + "--window_size", + action="store", + dest="window_size", + default=None, + help="""The option to set the default window "width,height". + Format: A comma-separated string with the 2 values. + Example: "1200,800" + Default: None. (Will use default values if None)""", + ) + parser.addoption( + "--maximize_window", + "--maximize-window", + "--maximize", + "--fullscreen", + action="store_true", + dest="maximize_option", + default=False, + help="""The option to start with a maximized browser window. + (Overrides the "window-size" option if used.)""", + ) + parser.addoption( + "--screenshot", + "--save_screenshot", + "--save-screenshot", + "--ss", + action="store_true", + dest="save_screenshot", + default=False, + help="""Save a screenshot at the end of every test. + By default, this is only done for failures. + Will be saved in the "latest_logs/" folder.""", + ) + parser.addoption( + "--no-screenshot", + "--no_screenshot", + "--ns", + action="store_true", + dest="no_screenshot", + default=False, + help="""No screenshots saved unless tests directly ask it. + This changes default behavior where screenshots are + saved for test failures and pytest-html reports.""", + ) + parser.addoption( + "--visual_baseline", + "--visual-baseline", + action="store_true", + dest="visual_baseline", + default=False, + help="""Setting this resets the visual baseline for + Automated Visual Testing with SeleniumBase. + When a test calls self.check_window(), it will + rebuild its files in the visual_baseline folder.""", + ) + parser.addoption( + "--external_pdf", + "--external-pdf", + action="store_true", + dest="external_pdf", + default=False, + help="""This option sets the following on Chromium: + "plugins.always_open_pdf_externally": True, + which causes opened PDF URLs to download immediately, + instead of being displayed in the browser window.""", + ) + parser.addoption( + "--timeout_multiplier", + "--timeout-multiplier", + action="store", + dest="timeout_multiplier", + default=None, + help="""Setting this overrides the default timeout + by the multiplier when waiting for page elements. + Unused when tests override the default value.""", + ) + parser.addoption( + "--lfp", + "--list-fail-page", + "--list-fail-pages", + action="store_true", + dest="fail_page", + default=False, + help="""(For debugging) After each failing test, list the URL + where the failure occurred. + Useful when you don't have access to the latest_logs/ + folder, such as when running tests in GitHub Actions.""", + ) + + arg_join = " ".join(sys_argv) + sb_config._browser_shortcut = None + sb_config._cdp_browser = None + sb_config._cdp_bin_loc = None + sb_config._vd_list = [] + # Check if binary-location in options + bin_loc_in_options = False + for arg in sys_argv: + if arg in ["--binary-location", "--binary_location", "--bl"]: + bin_loc_in_options = True + # SeleniumBase does not support pytest-timeout due to hanging browsers. + for arg in sys_argv: + if "--timeout=" in arg: + raise Exception( + "\n Don't use --timeout=s from pytest-timeout! " + "\n It's not thread-safe for WebDriver processes! " + "\n Use --time-limit=s from SeleniumBase instead!\n" + ) + + # Dashboard Mode does not support tests using forked subprocesses. + if "--forked" in sys_argv and "--dashboard" in sys_argv: + raise Exception( + "\n Dashboard Mode does NOT support forked subprocesses!" + '\n (*** DO NOT combine "--forked" with "--dashboard"! ***)\n' + ) + + # Reuse-Session Mode does not support tests using forked subprocesses. + if "--forked" in sys_argv and ( + "--rs" in sys_argv or "--reuse-session" in sys_argv + ): + raise Exception( + "\n Reuse-Session Mode does NOT support forked subprocesses!" + '\n (DO NOT combine "--forked" with "--rs"/"--reuse-session"!)\n' + ) + + # Recorder Mode does not support multi-threaded / multi-process runs. + if ( + "--recorder" in sys_argv + or "--record" in sys_argv + or "--rec" in sys_argv + ): + if ("-n" in sys_argv) or (" -n=" in arg_join) or ("-c" in sys_argv): + raise Exception( + "\n Recorder Mode does NOT support multi-process mode (-n)!" + '\n (DO NOT combine "--recorder" with "-n NUM_PROCESSES"!)\n' + ) + + using_recorder = False + if ( + "--recorder" in sys_argv + or "--record" in sys_argv + or "--rec" in sys_argv + ): + using_recorder = True + + # As a shortcut, you can use "--edge" instead of "--browser=edge", etc, + # but you can only specify one default browser for tests. (Default: chrome) + browser_changes = 0 + browser_set = None + browser_text = None + browser_list = [] + if "--browser=chrome" in sys_argv or "--browser chrome" in sys_argv: + browser_changes += 1 + browser_set = "chrome" + browser_list.append("--browser=chrome") + if "--browser=edge" in sys_argv or "--browser edge" in sys_argv: + browser_changes += 1 + browser_set = "edge" + browser_list.append("--browser=edge") + if "--browser=firefox" in sys_argv or "--browser firefox" in sys_argv: + browser_changes += 1 + browser_set = "firefox" + browser_list.append("--browser=firefox") + if "--browser=safari" in sys_argv or "--browser safari" in sys_argv: + browser_changes += 1 + browser_set = "safari" + browser_list.append("--browser=safari") + if "--browser=ie" in sys_argv or "--browser ie" in sys_argv: + browser_changes += 1 + browser_set = "ie" + browser_list.append("--browser=ie") + if "--browser=remote" in sys_argv or "--browser remote" in sys_argv: + browser_changes += 1 + browser_set = "remote" + browser_list.append("--browser=remote") + if "--browser=opera" in sys_argv or "--browser opera" in sys_argv: + if not bin_loc_in_options: + bin_loc = detect_b_ver.get_binary_location("opera") + if os.path.exists(bin_loc): + browser_changes += 1 + browser_set = "opera" + sb_config._browser_shortcut = "opera" + sb_config._cdp_browser = "opera" + sb_config._cdp_bin_loc = bin_loc + browser_list.append("--browser=opera") + if "--browser=brave" in sys_argv or "--browser brave" in sys_argv: + if not bin_loc_in_options: + bin_loc = detect_b_ver.get_binary_location("brave") + if os.path.exists(bin_loc): + browser_changes += 1 + browser_set = "brave" + sb_config._browser_shortcut = "brave" + sb_config._cdp_browser = "brave" + sb_config._cdp_bin_loc = bin_loc + browser_list.append("--browser=brave") + if "--browser=comet" in sys_argv or "--browser comet" in sys_argv: + if not bin_loc_in_options: + bin_loc = detect_b_ver.get_binary_location("comet") + if os.path.exists(bin_loc): + browser_changes += 1 + browser_set = "comet" + sb_config._browser_shortcut = "comet" + sb_config._cdp_browser = "comet" + sb_config._cdp_bin_loc = bin_loc + browser_list.append("--browser=comet") + browser_text = browser_set + if "--chrome" in sys_argv and not browser_set == "chrome": + browser_changes += 1 + browser_text = "chrome" + sb_config._browser_shortcut = "chrome" + browser_list.append("--chrome") + if "--edge" in sys_argv and not browser_set == "edge": + browser_changes += 1 + browser_text = "edge" + sb_config._browser_shortcut = "edge" + browser_list.append("--edge") + if "--firefox" in sys_argv and not browser_set == "firefox": + browser_changes += 1 + browser_text = "firefox" + sb_config._browser_shortcut = "firefox" + browser_list.append("--firefox") + if "--ie" in sys_argv and not browser_set == "ie": + browser_changes += 1 + browser_text = "ie" + sb_config._browser_shortcut = "ie" + browser_list.append("--ie") + if "--safari" in sys_argv and not browser_set == "safari": + browser_changes += 1 + browser_text = "safari" + sb_config._browser_shortcut = "safari" + browser_list.append("--safari") + if "--opera" in sys_argv and not browser_set == "opera": + if not bin_loc_in_options: + bin_loc = detect_b_ver.get_binary_location("opera") + if os.path.exists(bin_loc): + browser_changes += 1 + browser_text = "opera" + sb_config._browser_shortcut = "opera" + sb_config._cdp_browser = "opera" + sb_config._cdp_bin_loc = bin_loc + browser_list.append("--opera") + if "--brave" in sys_argv and not browser_set == "brave": + if not bin_loc_in_options: + bin_loc = detect_b_ver.get_binary_location("brave") + if os.path.exists(bin_loc): + browser_changes += 1 + browser_text = "brave" + sb_config._browser_shortcut = "brave" + sb_config._cdp_browser = "brave" + sb_config._cdp_bin_loc = bin_loc + browser_list.append("--brave") + if "--comet" in sys_argv and not browser_set == "comet": + if not bin_loc_in_options: + bin_loc = detect_b_ver.get_binary_location("comet") + if os.path.exists(bin_loc): + browser_changes += 1 + browser_text = "comet" + sb_config._browser_shortcut = "comet" + sb_config._cdp_browser = "comet" + sb_config._cdp_bin_loc = bin_loc + browser_list.append("--comet") + if browser_changes > 1: + message = "\n TOO MANY browser types were entered!" + message += "\n There were %s found:\n > %s" % ( + browser_changes, + ", ".join(browser_list), + ) + message += "\n ONLY ONE default browser is allowed!" + message += "\n Select a single browser & try again!\n" + raise Exception(message) + if ( + using_recorder + and browser_changes == 1 + and browser_text not in [ + "chrome", "edge", "opera", "brave", "comet", "chromium" + ] + ): + message = ( + "\n Recorder Mode ONLY supports Chromium browsers!" + '\n (Your browser choice was: "%s")\n' % browser_list[0] + ) + raise Exception(message) + undetectable = False + if ( + "--undetected" in sys_argv + or "--undetectable" in sys_argv + or "--uc" in sys_argv + or "--uc-cdp-events" in sys_argv + or "--uc_cdp_events" in sys_argv + or "--uc-cdp" in sys_argv + or "--uc-subprocess" in sys_argv + or "--uc_subprocess" in sys_argv + or "--uc-sub" in sys_argv + ): + undetectable = True + if ( + browser_changes == 1 + and browser_text not in [ + "chrome", "opera", "brave", "comet", "chromium" + ] + and undetectable + ): + message = ( + '\n Undetected-Chromedriver Mode ONLY supports Chrome!' + '\n ("--uc" / "--undetected" / "--undetectable")' + '\n (Your browser choice was: "%s")\n' % browser_list[0] + ) + raise Exception(message) def pytest_configure(config): - """ This runs after command line options have been parsed """ + """This runs after command-line options have been parsed.""" + global _pytest_config + _pytest_config = config + sb_config.item_count = 0 + sb_config.item_count_passed = 0 + sb_config.item_count_failed = 0 + sb_config.item_count_skipped = 0 + sb_config.item_count_untested = 0 sb_config.is_pytest = True - sb_config.browser = config.getoption('browser') - sb_config.data = config.getoption('data') - sb_config.environment = config.getoption('environment') - sb_config.with_selenium = config.getoption('with_selenium') - sb_config.user_agent = config.getoption('user_agent') - sb_config.headless = config.getoption('headless') - sb_config.with_testing_base = config.getoption('with_testing_base') - sb_config.with_db_reporting = config.getoption('with_db_reporting') - sb_config.with_s3_logging = config.getoption('with_s3_logging') - sb_config.with_screen_shots = config.getoption('with_screen_shots') - sb_config.with_basic_test_info = config.getoption('with_basic_test_info') - sb_config.with_page_source = config.getoption('with_page_source') - sb_config.servername = config.getoption('servername') - sb_config.port = config.getoption('port') - sb_config.proxy_string = config.getoption('proxy_string') - sb_config.cap_file = config.getoption('cap_file') - sb_config.database_env = config.getoption('database_env') - sb_config.log_path = config.getoption('log_path') - sb_config.archive_logs = config.getoption('archive_logs') - sb_config.demo_mode = config.getoption('demo_mode') - sb_config.demo_sleep = config.getoption('demo_sleep') - sb_config.highlights = config.getoption('highlights') - sb_config.message_duration = config.getoption('message_duration') - sb_config.js_checking_on = config.getoption('js_checking_on') - sb_config.ad_block_on = config.getoption('ad_block_on') - sb_config.verify_delay = config.getoption('verify_delay') - sb_config.disable_csp = config.getoption('disable_csp') - sb_config.save_screenshot = config.getoption('save_screenshot') - sb_config.visual_baseline = config.getoption('visual_baseline') - sb_config.timeout_multiplier = config.getoption('timeout_multiplier') + sb_config.is_behave = False + sb_config.is_nosetest = False + sb_config.is_context_manager = False + sb_config.pytest_config = config + sb_config.browser = config.getoption("browser") + if sb_config._browser_shortcut: + sb_config.browser = sb_config._browser_shortcut + elif sys_argv == ["-c"]: # Multithreading messes with args + if config.getoption("use_opera"): + bin_loc = detect_b_ver.get_binary_location("opera") + if bin_loc and os.path.exists(bin_loc): + sb_config.browser = "opera" + elif config.getoption("use_brave"): + bin_loc = detect_b_ver.get_binary_location("brave") + if bin_loc and os.path.exists(bin_loc): + sb_config.browser = "brave" + elif config.getoption("use_comet"): + bin_loc = detect_b_ver.get_binary_location("comet") + if bin_loc and os.path.exists(bin_loc): + sb_config.browser = "comet" + sb_config.account = config.getoption("account") + sb_config.data = config.getoption("data") + sb_config.var1 = config.getoption("var1") + sb_config.var2 = config.getoption("var2") + sb_config.var3 = config.getoption("var3") + sb_config.variables = config.getoption("variables") + sb_config.environment = config.getoption("environment") + sb_config.with_selenium = config.getoption("with_selenium") + sb_config.user_agent = config.getoption("user_agent") + sb_config.mobile_emulator = config.getoption("mobile_emulator") + sb_config.device_metrics = config.getoption("device_metrics") + sb_config.headless = config.getoption("headless") + sb_config.headless1 = config.getoption("headless1") + if sb_config.headless1: + sb_config.headless = True + sb_config.headless2 = config.getoption("headless2") + if sb_config.headless2 and sb_config.browser == "firefox": + sb_config.headless2 = False # Only for Chromium browsers + sb_config.headless = True # Firefox has regular headless + elif ( + sb_config.browser not in [ + "chrome", "edge", "opera", "brave", "comet", "chromium" + ] + ): + sb_config.headless2 = False # Only for Chromium browsers + sb_config.headed = config.getoption("headed") + sb_config.xvfb = config.getoption("xvfb") + sb_config.xvfb_metrics = config.getoption("xvfb_metrics") + sb_config.locale_code = config.getoption("locale_code") + sb_config.interval = config.getoption("interval") + sb_config.start_page = config.getoption("start_page") + sb_config.chromium_arg = config.getoption("chromium_arg") + sb_config.firefox_arg = config.getoption("firefox_arg") + sb_config.firefox_pref = config.getoption("firefox_pref") + sb_config.extension_zip = config.getoption("extension_zip") + sb_config.extension_dir = config.getoption("extension_dir") + sb_config.disable_features = config.getoption("disable_features") + sb_config.binary_location = config.getoption("binary_location") + if getattr(sb_config, "_cdp_bin_loc", None): + sb_config.binary_location = sb_config._cdp_bin_loc + elif not sb_config.binary_location: + if ( + config.getoption("use_opera") + or sb_config._browser_shortcut == "opera" + ): + bin_loc = detect_b_ver.get_binary_location("opera") + if bin_loc and os.path.exists(bin_loc): + sb_config.binary_location = bin_loc + elif ( + config.getoption("use_brave") + or sb_config._browser_shortcut == "brave" + ): + bin_loc = detect_b_ver.get_binary_location("brave") + if bin_loc and os.path.exists(bin_loc): + sb_config.binary_location = bin_loc + elif ( + config.getoption("use_comet") + or sb_config._browser_shortcut == "comet" + ): + bin_loc = detect_b_ver.get_binary_location("comet") + if bin_loc and os.path.exists(bin_loc): + sb_config.binary_location = bin_loc + if config.getoption("use_chromium") and not sb_config.binary_location: + sb_config.binary_location = "_chromium_" + elif config.getoption("use_cft") and not sb_config.binary_location: + sb_config.binary_location = "cft" + elif config.getoption("use_chs") and not sb_config.binary_location: + sb_config.binary_location = "chs" + if ( + sb_config.binary_location + and sb_config.binary_location.lower() == "chs" + and sb_config.browser == "chrome" + ): + sb_config.headless = True + sb_config.headless1 = False + sb_config.headless2 = False + if sb_config.browser in constants.ChromiumSubs.chromium_subs: + if not sb_config.binary_location: + sb_config.browser = "chrome" # Still uses chromedriver + sb_config._browser_shortcut = sb_config.browser + sb_config.driver_version = config.getoption("driver_version") + sb_config.page_load_strategy = config.getoption("page_load_strategy") + sb_config.with_testing_base = config.getoption("with_testing_base") + sb_config.with_db_reporting = config.getoption("with_db_reporting") + sb_config.with_s3_logging = config.getoption("with_s3_logging") + sb_config.with_screen_shots = config.getoption("with_screen_shots") + sb_config.with_basic_test_info = config.getoption("with_basic_test_info") + sb_config.with_page_source = config.getoption("with_page_source") + sb_config.protocol = config.getoption("protocol") + sb_config.servername = config.getoption("servername") + sb_config.port = config.getoption("port") + if sb_config.servername != "localhost": + # Using Selenium Grid + # (Set --server="127.0.0.1" for localhost Grid) + if str(sb_config.port) == "443": + sb_config.protocol = "https" + sb_config.proxy_string = config.getoption("proxy_string") + sb_config.proxy_bypass_list = config.getoption("proxy_bypass_list") + sb_config.proxy_pac_url = config.getoption("proxy_pac_url") + sb_config.proxy_driver = config.getoption("proxy_driver") + sb_config.multi_proxy = config.getoption("multi_proxy") + sb_config.cap_file = config.getoption("cap_file") + sb_config.cap_string = config.getoption("cap_string") + sb_config.settings_file = config.getoption("settings_file") + sb_config.user_data_dir = config.getoption("user_data_dir") + sb_config.database_env = config.getoption("database_env") + sb_config.log_path = constants.Logs.LATEST + "/" + sb_config.archive_logs = config.getoption("archive_logs") + if config.getoption("archive_downloads"): + settings.ARCHIVE_EXISTING_DOWNLOADS = True + if config.getoption("skip_js_waits"): + settings.SKIP_JS_WAITS = True + if config.getoption("wait_for_angularjs"): + settings.WAIT_FOR_ANGULARJS = True + sb_config.all_scripts = config.getoption("all_scripts") + sb_config._time_limit = config.getoption("time_limit") + sb_config.time_limit = config.getoption("time_limit") + sb_config.slow_mode = config.getoption("slow_mode") + sb_config.demo_mode = config.getoption("demo_mode") + sb_config.demo_sleep = config.getoption("demo_sleep") + sb_config.highlights = config.getoption("highlights") + sb_config.message_duration = config.getoption("message_duration") + sb_config.js_checking_on = config.getoption("js_checking_on") + sb_config.ad_block_on = config.getoption("ad_block_on") + sb_config.host_resolver_rules = config.getoption("host_resolver_rules") + sb_config.block_images = config.getoption("block_images") + sb_config.do_not_track = config.getoption("do_not_track") + sb_config.verify_delay = config.getoption("verify_delay") + sb_config.esc_end = config.getoption("esc_end") + sb_config.recorder_mode = config.getoption("recorder_mode") + sb_config.recorder_ext = config.getoption("recorder_mode") # Again + sb_config.rec_sb_mgr = config.getoption("rec_sb_mgr") + sb_config.rec_sb_cdp = config.getoption("rec_sb_cdp") + sb_config.rec_behave = config.getoption("rec_behave") + sb_config.rec_print = config.getoption("rec_print") + sb_config.record_sleep = config.getoption("record_sleep") + if not sb_config.recorder_mode: + if ( + sb_config.record_sleep + or sb_config.rec_print + or sb_config.rec_behave + or sb_config.rec_sb_mgr + or sb_config.rec_sb_cdp + ): + sb_config.recorder_mode = True + sb_config.recorder_ext = True + sb_config.disable_cookies = config.getoption("disable_cookies") + sb_config.disable_js = config.getoption("disable_js") + sb_config.disable_csp = config.getoption("disable_csp") + sb_config.disable_ws = config.getoption("disable_ws") + sb_config.enable_ws = config.getoption("enable_ws") + if not sb_config.disable_ws: + sb_config.enable_ws = True + sb_config.enable_sync = config.getoption("enable_sync") + sb_config.use_auto_ext = config.getoption("use_auto_ext") + sb_config.undetectable = config.getoption("undetectable") + sb_config.uc_cdp_events = config.getoption("uc_cdp_events") + if sb_config.uc_cdp_events and not sb_config.undetectable: + sb_config.undetectable = True + sb_config.uc_subprocess = config.getoption("uc_subprocess") + if sb_config.uc_subprocess and not sb_config.undetectable: + sb_config.undetectable = True + sb_config.no_sandbox = config.getoption("no_sandbox") + sb_config.disable_gpu = config.getoption("disable_gpu") + sb_config.log_cdp_events = config.getoption("log_cdp_events") + sb_config.remote_debug = config.getoption("remote_debug") + sb_config.final_debug = config.getoption("final_debug") + sb_config.dashboard = config.getoption("dashboard") + sb_config.dash_title = config.getoption("dash_title") + sb_config.enable_3d_apis = config.getoption("enable_3d_apis") + sb_config.swiftshader = config.getoption("swiftshader") + sb_config.incognito = config.getoption("incognito") + sb_config.guest_mode = config.getoption("guest_mode") + sb_config.dark_mode = config.getoption("dark_mode") + sb_config.devtools = config.getoption("devtools") + sb_config.reuse_session = config.getoption("reuse_session") + sb_config.reuse_class_session = config.getoption("reuse_class_session") + if sb_config.reuse_class_session: + sb_config.reuse_session = True + sb_config.shared_driver = None # The default driver for session reuse + sb_config.crumbs = config.getoption("crumbs") + sb_config._disable_beforeunload = config.getoption("_disable_beforeunload") + sb_config.window_position = config.getoption("window_position") + sb_config.window_size = config.getoption("window_size") + sb_config.maximize_option = config.getoption("maximize_option") + sb_config.save_screenshot = config.getoption("save_screenshot") + sb_config.no_screenshot = config.getoption("no_screenshot") + sb_config.visual_baseline = config.getoption("visual_baseline") + sb_config.external_pdf = config.getoption("external_pdf") + sb_config.timeout_multiplier = config.getoption("timeout_multiplier") + sb_config.list_fp = config.getoption("fail_page") + sb_config._is_timeout_changed = False + sb_config._has_logs = False + sb_config._fail_page = None + sb_config._SMALL_TIMEOUT = settings.SMALL_TIMEOUT + sb_config._LARGE_TIMEOUT = settings.LARGE_TIMEOUT sb_config.pytest_html_report = config.getoption("htmlpath") # --html=FILE + sb_config._sb_class = None # (Used with the sb fixture for "--rcs") + sb_config._sb_node = {} # sb node dictionary (Used with the sb fixture) + # Dashboard-specific variables + sb_config._results = {} # SBase Dashboard test results + sb_config._duration = {} # SBase Dashboard test duration + sb_config._display_id = {} # SBase Dashboard display ID + sb_config._d_t_log_path = {} # SBase Dashboard test log path + sb_config._dash_html = None # SBase Dashboard HTML copy + sb_config._test_id = None # SBase Dashboard test id + sb_config._latest_display_id = None # The latest SBase display id + sb_config._dashboard_initialized = False # Becomes True after init + sb_config._has_exception = False # This becomes True if any test fails + sb_config._multithreaded = False # This becomes True if multithreading + sb_config._only_unittest = True # If any test uses BaseCase, becomes False + sb_config._sbase_detected = False # Becomes True during SeleniumBase tests + sb_config._extra_dash_entries = [] # Dashboard entries for non-SBase tests + sb_config._using_html_report = False # Becomes True when using html report + sb_config._dash_is_html_report = False # Dashboard becomes the html report + sb_config._saved_dashboard_pie = None # Copy of pie chart for html report + sb_config._dash_final_summary = None # Dash status to add to html report + sb_config._html_report_name = None # The name of the pytest html report + sb_config._html_report_copy = None # The copy of the pytest html report + + arg_join = " ".join(sys_argv) + if ( + "-n" in sys_argv + or " -n=" in arg_join + or " -n" in arg_join + or "-c" in sys_argv + or "-n=" in config.getini("addopts") + or "-n " in config.getini("addopts") + or "-n" in config.getini("addopts") + ): + sb_config._multithreaded = True + if ( + "--html" in sys_argv + or " --html=" in arg_join + or "--html=" in config.getini("addopts") + or "--html " in config.getini("addopts") + ): + sb_config._using_html_report = True + sb_config._html_report_name = config.getoption("htmlpath") + if sb_config.dashboard: + if sb_config._html_report_name == "dashboard.html": + sb_config._dash_is_html_report = True + sb_config._html_report_copy = "last_report.html" + + # Recorder Mode does not support multi-threaded / multi-process runs. + if sb_config.recorder_mode and sb_config._multithreaded: + # At this point, the user likely put a "-n NUM" in the pytest.ini file. + # Since raising an exception in pytest_configure raises INTERNALERROR, + # print a message here instead and cancel Recorder Mode. + print( + "\n Recorder Mode does NOT support multi-process mode (-n)!" + '\n (DO NOT combine "--recorder" with "-n NUM_PROCESSES"!)' + "\n (The Recorder WILL BE DISABLED during this run!)\n" + ) + sb_config.recorder_mode = False + sb_config.recorder_ext = False + + if sb_config.xvfb and "linux" not in sys.platform: + # The Xvfb virtual display server is for Linux OS Only! + sb_config.xvfb = False + if ( + "linux" in sys.platform + and not sb_config.headed + and not sb_config.headless + and not sb_config.headless2 + and not sb_config.xvfb + ): + if not sb_config.undetectable: + print( + "(Linux uses --headless by default. " + "To override, use --headed / --gui. " + "For Xvfb mode instead, use --xvfb. " + "Or you can hide this info by using " + "--headless / --headless2 / --uc.)" + ) + sb_config.headless = True + else: + sb_config.xvfb = True + + # Recorder Mode can still optimize scripts in --headless2 mode. + if sb_config.recorder_mode and sb_config.headless: + sb_config.headless = False + sb_config.headless1 = False + sb_config.headless2 = True + + if not sb_config.headless and not sb_config.headless2: + sb_config.headed = True + + if config.getoption("use_chrome"): + sb_config.browser = "chrome" + elif config.getoption("use_edge"): + sb_config.browser = "edge" + elif config.getoption("use_firefox"): + sb_config.browser = "firefox" + elif config.getoption("use_ie"): + sb_config.browser = "ie" + elif config.getoption("use_safari"): + sb_config.browser = "safari" + else: + pass # Use the browser specified by "--browser=BROWSER" + + if sb_config.browser == "safari" and sb_config.headless: + sb_config.headless = False # Safari doesn't support headless mode + sb_config.headless1 = False - if sb_config.with_testing_base: - log_helper.log_folder_setup(sb_config.log_path, sb_config.archive_logs) - proxy_helper.remove_proxy_zip_if_present() + if sb_config.dash_title: + constants.Dashboard.TITLE = sb_config.dash_title.replace("_", " ") + if sb_config.save_screenshot and sb_config.no_screenshot: + sb_config.save_screenshot = False # "no_screenshot" has priority -def pytest_unconfigure(): - """ This runs after all tests have completed with pytest. """ - proxy_helper.remove_proxy_zip_if_present() + if ( + "-v" in sys_argv and not sb_config._multithreaded + or ( + hasattr(config, "invocation_params") + and "-v" in config.invocation_params.args + and ( + "-n=1" in config.invocation_params.args + or "-n1" in config.invocation_params.args + or "-n 1" in " ".join(config.invocation_params.args) + ) + ) + ): + sb_config.list_fp = True # List the fail pages in console output + if ( + sb_config._multithreaded + and "--co" not in sys_argv + and "--collect-only" not in sys_argv + ): + from seleniumbase.core import download_helper + from seleniumbase.core import proxy_helper -def pytest_runtest_setup(): - """ This runs before every test with pytest """ + log_helper.log_folder_setup( + constants.Logs.LATEST + "/", sb_config.archive_logs + ) + download_helper.reset_downloads_folder() + proxy_helper.remove_proxy_zip_if_present() + + +def pytest_sessionstart(session): pass +def _get_test_ids_(the_item): + test_id = the_item.nodeid + if not test_id: + test_id = "unidentified_TestCase" + display_id = test_id + r""" + # Now using the nodeid for both the test_id and display_id. + # (This only impacts tests using The Dashboard.) + # If there are any issues, we'll revert back to the old code. + test_id = the_item.nodeid.split("/")[-1].replace(" ", "_") + if "[" in test_id: + test_id_intro = test_id.split("[")[0] + parameter = test_id.split("[")[1] + parameter = re.sub(re.compile(r"\W"), "", parameter) + test_id = test_id_intro + "__" + parameter + display_id = test_id + test_id = test_id.replace("/", ".").replace("\\", ".") + test_id = test_id.replace("::", ".").replace(".py", "") + """ + return test_id, display_id + + +def _create_dashboard_assets_(): + from seleniumbase.js_code.live_js import live_js + from seleniumbase.core.style_sheet import get_pytest_style + + abs_path = os.path.abspath(".") + assets_folder = os.path.join(abs_path, "assets") + if not os.path.exists(assets_folder): + with suppress(Exception): + os.makedirs(assets_folder, exist_ok=True) + pytest_style_css = os.path.join(assets_folder, "pytest_style.css") + add_pytest_style_css = True + if os.path.exists(pytest_style_css): + existing_pytest_style = None + with open(pytest_style_css, mode="r") as f: + existing_pytest_style = f.read() + if existing_pytest_style == get_pytest_style(): + add_pytest_style_css = False + if add_pytest_style_css: + out_file = open(pytest_style_css, mode="w+", encoding="utf-8") + out_file.writelines(get_pytest_style()) + out_file.close() + live_js_file = os.path.join(assets_folder, "live.js") + add_live_js_file = True + if os.path.exists(live_js_file): + existing_live_js = None + with open(live_js_file, mode="r") as f: + existing_live_js = f.read() + if existing_live_js == live_js: + add_live_js_file = False + if add_live_js_file: + out_file = open(live_js_file, mode="w+", encoding="utf-8") + out_file.writelines(live_js) + out_file.close() + + +def pytest_itemcollected(item): + if "--co" in sys_argv or "--collect-only" in sys_argv: + return + sb_config.item_count += 1 + if sb_config.dashboard: + test_id, display_id = _get_test_ids_(item) + sb_config._results[test_id] = "Untested" + sb_config._duration[test_id] = "-" + sb_config._display_id[test_id] = display_id + sb_config._d_t_log_path[test_id] = None + + +def pytest_deselected(items): + if "--co" in sys_argv or "--collect-only" in sys_argv: + return + if sb_config.dashboard: + sb_config.item_count -= len(items) + for item in items: + test_id, display_id = _get_test_ids_(item) + if test_id in sb_config._results.keys(): + sb_config._results.pop(test_id) + + +def pytest_collection_finish(session): + """This runs after item collection is finalized. + https://docs.pytest.org/en/stable/reference.html """ + sb_config._context_of_runner = False # Context Manager Compatibility + if "--co" in sys_argv or "--collect-only" in sys_argv: + return + if len(session.items) > 0 and not sb_config._multithreaded: + from seleniumbase.core import download_helper + from seleniumbase.core import proxy_helper + + log_helper.log_folder_setup( + constants.Logs.LATEST + "/", sb_config.archive_logs + ) + download_helper.reset_downloads_folder() + proxy_helper.remove_proxy_zip_if_present() + if sb_config.dashboard and len(session.items) > 0: + _create_dashboard_assets_() + # Print the Dashboard path if at least one test runs. + sb_config.item_count_untested = sb_config.item_count + dash_path = os.path.join(os.getcwd(), "dashboard.html") + dash_url = "file://" + dash_path.replace("\\", "/") + star_len = len("Dashboard: ") + len(dash_url) + with suppress(Exception): + terminal_size = os.get_terminal_size().columns + if terminal_size > 30 and star_len > terminal_size: + star_len = terminal_size + stars = "*" * star_len + c1 = "" + cr = "" + if "linux" not in sys.platform: + c1 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX + cr = colorama.Style.RESET_ALL + if sb_config._multithreaded: + if ( + hasattr(session.config, "workerinput") + and session.config.workerinput["workerid"] == "gw0" + ): + sys.stderr.write( + "\nDashboard: %s%s%s\n%s\n" % (c1, dash_url, cr, stars) + ) + else: + print("Dashboard: %s%s%s\n%s" % (c1, dash_url, cr, stars)) + htmlpath = session.config.getoption("--html", default=None) + if htmlpath: + with suppress(Exception): + if getattr(sb_config, "_multithreaded", None): + from filelock import FileLock + dash_lock = FileLock(constants.Report.LOCKFILE) + with dash_lock: + _fix_html_report(htmlpath) + else: + _fix_html_report(htmlpath) + + +def pytest_runtest_setup(item): + """This runs before every test with pytest.""" + if "--co" in sys_argv or "--collect-only" in sys_argv: + return + if sb_config.dashboard: + sb_config._sbase_detected = False + sb_config._pdb_failure = False + sb_config._fail_page = None + test_id, display_id = _get_test_ids_(item) + sb_config._test_id = test_id + sb_config._latest_display_id = display_id + + +def pytest_runtest_logreport(report): + with suppress(Exception): + if _pytest_config: # Global variable + htmlpath = _pytest_config.getoption("--html", default=None) + if htmlpath: + # The HTML report option is enabled + if report.when == "teardown": + if getattr(sb_config, "_multithreaded", None): + # Multi-threaded tests + from filelock import FileLock + dash_lock = FileLock(constants.Report.LOCKFILE) + with dash_lock: + _fix_html_report(htmlpath) + else: + # Single-threaded tests + _fix_html_report(htmlpath) + + +def _fix_html_report(report_path): + # (Only if there's a pytest-html report present) + with suppress(Exception): + abs_path = os.path.abspath(".") + html_report_path = os.path.join(abs_path, report_path) + if not os.path.exists(html_report_path): + return + the_html_r = None + with open(html_report_path, mode="r", encoding="utf-8") as f: + the_html_r = f.read() + if "page to ge the" not in the_html_r: + return # The typo was already fixed + # Collapsible rows are trickier to select for copy/paste + the_html_r = the_html_r.replace( + "findAll('.collapsible", "//findAll('.collapsible" + ) + # Fix typo in pytest-html 4.0.2 + # (Later versions have a worse bug that prevents live updates) + the_html_r = the_html_r.replace("page to ge the", "page to get the") + with open(html_report_path, mode="w", encoding="utf-8") as f: + f.write(the_html_r) # Update the HTML report + + def pytest_runtest_teardown(item): - """ This runs after every test with pytest """ + """This runs after every test with pytest.""" + if "--co" in sys_argv or "--collect-only" in sys_argv: + return + with suppress(Exception): + if hasattr(item, "_testcase") or hasattr(sb_config, "_sb_pdb_driver"): + if hasattr(item, "_testcase"): + self = item._testcase + with suppress(Exception): + if ( + getattr(self, "driver", None) + and "--pdb" not in sys_argv + ): + if not (is_windows or self.driver.service.process): + self.driver.quit() + elif getattr(sb_config, "_sb_pdb_driver", None): + with suppress(Exception): + if ( + not is_windows + or sb_config._sb_pdb_driver.service.process + ): + sb_config._sb_pdb_driver.quit() + sb_config._sb_pdb_driver = None + with suppress(Exception): + if ( + getattr(self, "_xvfb_display", None) + and hasattr(self._xvfb_display, "stop") + and not getattr(sb_config, "reuse_session", None) + ): + self.headless_active = False + sb_config.headless_active = False + self._xvfb_display.stop() + self._xvfb_display = None + if ( + getattr(sb_config, "_virtual_display", None) + and hasattr(sb_config._virtual_display, "stop") + and not getattr(sb_config, "reuse_session", None) + ): + sb_config._virtual_display.stop() + sb_config._virtual_display = None + if ( + ( + sb_config._has_exception + or (python3_11_or_newer and py311_patch2) + or "--pdb" in sys_argv + ) + and sb_config.list_fp + and sb_config._fail_page + ): + if ( + "-s" in sys_argv + or "--capture=no" in sys_argv + or "--capture=tee-sys" in sys_argv + or ( + hasattr(sb_config.pytest_config, "invocation_params") + and ( + "-s" in sb_config.pytest_config.invocation_params.args + or "--capture=no" in ( + sb_config.pytest_config.invocation_params.args + ) + or "--capture=tee-sys" in ( + sb_config.pytest_config.invocation_params.args + ) + ) + ) + ): + print("\n=> Fail Page: %s" % sb_config._fail_page) + else: + sys.stdout.write("\n=> Fail Page: %s\n" % sb_config._fail_page) + + +def pytest_html_duration_format(duration): + return "%.2f" % duration + - # Make sure webdriver has exited properly and any headless display +def pytest_sessionfinish(session): + pass + + +def pytest_terminal_summary(terminalreporter): + if "--co" in sys_argv or "--collect-only" in sys_argv: + return + if ( + not hasattr(terminalreporter, "stats") + or not hasattr(terminalreporter.stats, "keys") + ): + return + if len(terminalreporter.stats.keys()) == 0: + return + if not sb_config._multithreaded and not sb_config._sbase_detected: + return + latest_logs_dir = os.path.join(os.getcwd(), constants.Logs.LATEST) + os.sep + if ( + "failed" in terminalreporter.stats.keys() + and os.path.exists(latest_logs_dir) + and os.listdir(latest_logs_dir) + ): + sb_config._has_exception = True + if sb_config._multithreaded: + if os.path.exists(latest_logs_dir) and os.listdir(latest_logs_dir): + sb_config._has_exception = True + if sb_config.dashboard: + abs_path = os.path.abspath(".") + dash_lock = constants.Dashboard.LOCKFILE + dash_lock_path = os.path.join(abs_path, dash_lock) + if os.path.exists(dash_lock_path): + sb_config._only_unittest = False + if sb_config._has_exception and ( + sb_config.dashboard and not sb_config._only_unittest + ): + # Print link a second time because the first one may be off-screen + dashboard_file = os.path.join(os.getcwd(), "dashboard.html") + dashboard_url = "file://" + dashboard_file.replace("\\", "/") + terminalreporter.write_sep("-", "Dashboard = %s" % dashboard_url) + if ( + sb_config._has_exception + or sb_config.save_screenshot + or sb_config._has_logs + ): + # Log files are generated during test failures and Screenshot Mode + terminalreporter.write_sep( + "-", "Latest Logs dir: %s" % latest_logs_dir + ) + + +def _perform_pytest_unconfigure_(config): + from seleniumbase.core import proxy_helper + + reporter = config.pluginmanager.get_plugin("terminalreporter") + start_time = None + start_time = reporter._session_start.time # (pytest >= 8.4.0) + duration = time.time() - start_time + if not getattr(sb_config, "multi_proxy", None): + proxy_helper.remove_proxy_zip_if_present() + if getattr(sb_config, "reuse_session", None): + # Close the shared browser session + if sb_config.shared_driver: + try: + if ( + not is_windows + or sb_config.browser == "ie" + or sb_config.shared_driver.service.process + ): + sb_config.shared_driver.quit() + except AttributeError: + pass + except Exception: + pass + sb_config.shared_driver = None + with suppress(Exception): + if ( + getattr(sb_config, "_virtual_display", None) + and hasattr(sb_config._virtual_display, "stop") + ): + sb_config._virtual_display.stop() + sb_config._virtual_display = None + sb_config.headless_active = False + if getattr(sb_config, "_vd_list", None): + if isinstance(sb_config._vd_list, list): + for display in sb_config._vd_list: + if display: + with suppress(Exception): + display.stop() + if hasattr(sb_config, "log_path") and sb_config.item_count > 0: + log_helper.archive_logs_if_set( + constants.Logs.LATEST + "/", sb_config.archive_logs + ) + if os.path.exists("./assets/"): # Used by pytest-html reports + with suppress(Exception): + shared_utils.make_dir_files_writable("./assets/") + log_helper.clear_empty_logs() + # Dashboard post-processing: Disable time-based refresh and stamp complete + if not getattr(sb_config, "dashboard", None): + html_report_path = None + the_html_r = None + abs_path = os.path.abspath(".") + if sb_config._html_report_name: + html_report_path = os.path.join( + abs_path, sb_config._html_report_name + ) + if sb_config._html_report_copy: + html_report_path_copy = os.path.join( + abs_path, sb_config._html_report_copy + ) + if ( + sb_config._using_html_report + and html_report_path + and os.path.exists(html_report_path) + ): + with open(html_report_path, mode="r", encoding="utf-8") as f: + the_html_r = f.read() + assets_chunk = "if (assets.length === 1) {" + remove_media = "container.classList.remove('media-container')" + rm_n_left = '
      <
      ' + rm_n_right = '
      >
      ' + the_html_r = the_html_r.replace( + assets_chunk, + "%s %s" % (assets_chunk, remove_media), + ) + the_html_r = the_html_r.replace(rm_n_left, "") + the_html_r = the_html_r.replace(rm_n_right, "") + the_html_r = the_html_r.replace("
        $", "$") + the_html_r = the_html_r.replace("}
          ", "}") + the_html_r = the_html_r.replace( + "
        • ${val}
        • ", "${val},  " + ) + the_html_r = the_html_r.replace( + "
          ${value}
          ", "${value}' + 'SeleniumBase' + ) + the_html_r = the_html_r.replace( + ph_link, "%s and %s" % (sb_link, ph_link) + ) + the_html_r = the_html_r.replace( + "findAll('.collapsible", "//findAll('.collapsible" + ) + the_html_r = the_html_r.replace( + "mediaName.innerText", "//mediaName.innerText" + ) + the_html_r = the_html_r.replace( + "counter.innerText", "//counter.innerText" + ) + run_count = '

          ' + run_c_loc = the_html_r.find(run_count) + rc_loc = the_html_r.find(" took ", run_c_loc) + end_rc_loc = the_html_r.find(".

          ", rc_loc) + run_time = "%.2f" % duration + new_time = " ran in %s seconds" % run_time + the_html_r = ( + the_html_r[:rc_loc] + new_time + the_html_r[end_rc_loc:] + ) + with open(html_report_path, mode="w", encoding="utf-8") as f: + f.write(the_html_r) # Finalize the HTML report + with suppress(Exception): + shared_utils.make_writable(html_report_path) + with open(html_report_path_copy, mode="w", encoding="utf-8") as f: + f.write(the_html_r) # Finalize the HTML report copy + with suppress(Exception): + shared_utils.make_writable(html_report_path_copy) + assets_style = "./assets/style.css" + if os.path.exists(assets_style): + html_style = None + with open(assets_style, mode="r", encoding="utf-8") as f: + html_style = f.read() + if html_style: + html_style = html_style.replace("top: -50px;", "top: 2px;") + html_style = html_style.replace("+ 50px)", "+ 40px)") + html_style = html_style.replace("ht: 240px;", "ht: 228px;") + html_style = html_style.replace( + "- 80px);", "- 80px);\n margin-bottom: -42px;" + ) + html_style = html_style.replace(".collapsible", ".oldc") + html_style = html_style.replace(" (hide details)", "") + html_style = html_style.replace(" (show details)", "") + with open(assets_style, mode="w", encoding="utf-8") as f: + f.write(html_style) + with suppress(Exception): + shared_utils.make_writable(assets_style) + # Done with "pytest_unconfigure" unless using the Dashboard + return + stamp = "" + if sb_config._dash_is_html_report: + # (If the Dashboard URL is the same as the HTML Report URL:) + # Have the html report refresh back to a dashboard on update + stamp += ( + '\n" % constants.Dashboard.LIVE_JS + ) + stamp += "\n" + find_it = constants.Dashboard.META_REFRESH_HTML + swap_with = "" # Stop refreshing the page after the run is done + find_it_2 = "Awaiting results... (Refresh the page for updates)" + swap_with_2 = ( + "Test Run ENDED: Some results UNREPORTED due to skipped tearDown()" + ) + find_it_3 = 'Untested' + swap_with_3 = 'Unreported' + # These use caching to prevent extra method calls + DASH_PIE_PNG_1 = constants.Dashboard.get_dash_pie_1() + DASH_PIE_PNG_2 = constants.Dashboard.get_dash_pie_2() + DASH_PIE_PNG_3 = constants.Dashboard.get_dash_pie_3() + find_it_4 = 'href="%s"' % DASH_PIE_PNG_1 + swap_with_4 = 'href="%s"' % DASH_PIE_PNG_2 try: - self = item._testcase - try: - if hasattr(self, 'driver') and self.driver: - self.driver.quit() - except Exception: - pass - try: - if hasattr(self, 'headless') and self.headless: - if self.headless_active: - if hasattr(self, 'display') and self.display: - self.display.stop() - except Exception: - pass + abs_path = os.path.abspath(".") + dashboard_path = os.path.join(abs_path, "dashboard.html") + # Part 1: Finalizing the dashboard / integrating html report + if os.path.exists(dashboard_path): + the_html_d = None + with open(dashboard_path, mode="r", encoding="utf-8") as f: + the_html_d = f.read() + if sb_config._multithreaded and "-c" in sys_argv: + # Threads have "-c" in sys.argv, except for the last + raise Exception('Break out of "try" block.') + if sb_config._multithreaded: + dash_pie_loc = constants.Dashboard.DASH_PIE + pie_path = os.path.join(abs_path, dash_pie_loc) + if os.path.exists(pie_path): + import json + + with open(pie_path, mode="r") as f: + dash_pie = f.read().strip() + sb_config._saved_dashboard_pie = json.loads(dash_pie) + # If the test run doesn't complete by itself, stop refresh + the_html_d = the_html_d.replace(find_it, swap_with) + the_html_d = the_html_d.replace(find_it_2, swap_with_2) + the_html_d = the_html_d.replace(find_it_3, swap_with_3) + the_html_d = the_html_d.replace(find_it_4, swap_with_4) + the_html_d += stamp + if sb_config._dash_is_html_report and ( + sb_config._saved_dashboard_pie + ): + the_html_d = the_html_d.replace( + "

          dashboard.html

          ", + sb_config._saved_dashboard_pie, + ) + the_html_d = the_html_d.replace( + "", + '' % DASH_PIE_PNG_3, + ) + the_html_d = the_html_d.replace("", '') + the_html_d = the_html_d.replace( + "", + '' + '', + ) + if sb_config._dash_final_summary: + the_html_d += sb_config._dash_final_summary + time.sleep(0.1) # Add time for "livejs" to detect changes + with open(dashboard_path, mode="w", encoding="utf-8") as f: + f.write(the_html_d) # Finalize the dashboard + time.sleep(0.1) # Add time for "livejs" to detect changes + the_html_d = the_html_d.replace( + "", "" + ) + with open(dashboard_path, mode="w", encoding="utf-8") as f: + f.write(the_html_d) # Finalize the dashboard + with suppress(Exception): + shared_utils.make_writable(dashboard_path) + assets_style = "./assets/style.css" + if os.path.exists(assets_style): + html_style = None + with open(assets_style, mode="r", encoding="utf-8") as f: + html_style = f.read() + if html_style: + html_style = html_style.replace("top: -50px;", "top: 2px;") + html_style = html_style.replace("+ 50px)", "+ 40px)") + html_style = html_style.replace("ht: 240px;", "ht: 228px;") + html_style = html_style.replace( + "- 80px);", "- 80px);\n margin-bottom: -42px;" + ) + html_style = html_style.replace(".collapsible", ".oldc") + html_style = html_style.replace(" (hide details)", "") + html_style = html_style.replace(" (show details)", "") + with open(assets_style, mode="w", encoding="utf-8") as f: + f.write(html_style) + with suppress(Exception): + shared_utils.make_writable(assets_style) + # Part 2: Appending a pytest html report with dashboard data + html_report_path = None + if sb_config._html_report_name: + html_report_path = os.path.join( + abs_path, sb_config._html_report_name + ) + if sb_config._html_report_copy: + html_report_path_copy = os.path.join( + abs_path, sb_config._html_report_copy + ) + if ( + sb_config._using_html_report + and html_report_path + and os.path.exists(html_report_path) + and not sb_config._dash_is_html_report + ): + # Add the dashboard pie to the pytest html report + the_html_r = None + with open(html_report_path, mode="r", encoding="utf-8") as f: + the_html_r = f.read() + if sb_config._saved_dashboard_pie: + h_r_name = sb_config._html_report_name + if "/" in h_r_name and h_r_name.endswith(".html"): + h_r_name = h_r_name.split("/")[-1] + elif "\\" in h_r_name and h_r_name.endswith(".html"): + h_r_name = h_r_name.split("\\")[-1] + the_html_r = the_html_r.replace( + '

          %s

          ' % h_r_name, + sb_config._saved_dashboard_pie, + ) + the_html_r = the_html_r.replace( + "", + '' % DASH_PIE_PNG_3, + ) + if sb_config._dash_final_summary: + the_html_r += sb_config._dash_final_summary + assets_chunk = "if (assets.length === 1) {" + remove_media = "container.classList.remove('media-container')" + rm_n_left = '
          <
          ' + rm_n_right = '
          >
          ' + the_html_r = the_html_r.replace( + assets_chunk, + "%s %s" % (assets_chunk, remove_media), + ) + the_html_r = the_html_r.replace(rm_n_left, "") + the_html_r = the_html_r.replace(rm_n_right, "") + the_html_r = the_html_r.replace("
            $", "$") + the_html_r = the_html_r.replace("}
              ", "}") + the_html_r = the_html_r.replace( + "
            • ${val}
            • ", "${val},  " + ) + the_html_r = the_html_r.replace( + "
              ${value}
              ", "${value}' + 'SeleniumBase' + ) + the_html_r = the_html_r.replace( + ph_link, "%s and %s" % (sb_link, ph_link) + ) + the_html_r = the_html_r.replace( + "findAll('.collapsible", "//findAll('.collapsible" + ) + the_html_r = the_html_r.replace( + "mediaName.innerText", "//mediaName.innerText" + ) + the_html_r = the_html_r.replace( + "counter.innerText", "//counter.innerText" + ) + run_count = '

              ' + run_c_loc = the_html_r.find(run_count) + rc_loc = the_html_r.find(" took ", run_c_loc) + end_rc_loc = the_html_r.find(".

              ", rc_loc) + run_time = "%.2f" % duration + new_time = " ran in %s seconds" % run_time + the_html_r = ( + the_html_r[:rc_loc] + new_time + the_html_r[end_rc_loc:] + ) + with open(html_report_path, mode="w", encoding="utf-8") as f: + f.write(the_html_r) # Finalize the HTML report + with suppress(Exception): + shared_utils.make_writable(html_report_path) + with open( + html_report_path_copy, mode="w", encoding="utf-8" + ) as f: + f.write(the_html_r) # Finalize the HTML report copy + with suppress(Exception): + shared_utils.make_writable(html_report_path_copy) + except KeyboardInterrupt: + pass except Exception: pass +def pytest_unconfigure(config): + """This runs after all tests have completed with pytest.""" + if "--co" in sys_argv or "--collect-only" in sys_argv: + return + reporter = config.pluginmanager.get_plugin("terminalreporter") + if ( + not hasattr(reporter, "_session_start") + or ( + hasattr(reporter, "_session_start") + and not hasattr(reporter._session_start, "time") + ) + ): + return + if getattr(sb_config, "_multithreaded", None): + import fasteners + + dash_lock = fasteners.InterProcessLock(constants.Dashboard.LOCKFILE) + if getattr(sb_config, "dashboard", None): + # Multi-threaded tests with the Dashboard + abs_path = os.path.abspath(".") + dash_lock_file = constants.Dashboard.LOCKFILE + dash_lock_path = os.path.join(abs_path, dash_lock_file) + if os.path.exists(dash_lock_path): + sb_config._only_unittest = False + dashboard_path = os.path.join(abs_path, "dashboard.html") + with dash_lock: + if ( + sb_config._dash_html + and config.getoption("htmlpath") == "dashboard.html" + ): + # Dash is HTML Report (Multithreaded) + sb_config._dash_is_html_report = True + with open( + dashboard_path, mode="w", encoding="utf-8" + ) as f: + f.write(sb_config._dash_html) + # Dashboard Multithreaded + _perform_pytest_unconfigure_(config) + return + else: + # Dash Lock is missing + _perform_pytest_unconfigure_(config) + return + with dash_lock: + # Multi-threaded tests + _perform_pytest_unconfigure_(config) + return + else: + # Single-threaded tests + _perform_pytest_unconfigure_(config) + return + + +@pytest.fixture() +def sb(request): + """SeleniumBase as a pytest fixture. + Usage example: "def test_one(sb):" + You may need to use this for tests that use other pytest fixtures.""" + from seleniumbase import BaseCase + from seleniumbase.core import session_helper + + class BaseClass(BaseCase): + def setUp(self): + super().setUp() + + def tearDown(self): + self.save_teardown_screenshot() + super().tearDown() + + def base_method(self): + pass + + if request.cls: + if sb_config.reuse_class_session: + the_class = str(request.cls).split(".")[-1].split("'")[0] + if the_class != sb_config._sb_class: + session_helper.end_reused_class_session_as_needed() + sb_config._sb_class = the_class + request.cls.sb = BaseClass("base_method") + request.cls.sb.setUp() + request.cls.sb._needs_tearDown = True + request.cls.sb._using_sb_fixture = True + request.cls.sb._using_sb_fixture_class = True + sb_config._sb_node[request.node.nodeid] = request.cls.sb + yield request.cls.sb + if request.cls.sb._needs_tearDown: + request.cls.sb.tearDown() + request.cls.sb._needs_tearDown = False + else: + sb = BaseClass("base_method") + sb.setUp() + sb._needs_tearDown = True + sb._using_sb_fixture = True + sb._using_sb_fixture_no_class = True + sb_config._sb_node[request.node.nodeid] = sb + yield sb + if sb._needs_tearDown: + sb.tearDown() + sb._needs_tearDown = False + + @pytest.mark.hookwrapper def pytest_runtest_makereport(item, call): - pytest_html = item.config.pluginmanager.getplugin('html') + if "--co" in sys_argv or "--collect-only" in sys_argv: + return + pytest_html = item.config.pluginmanager.getplugin("html") outcome = yield report = outcome.get_result() - if pytest_html and report.when == 'call': - try: - extra_report = item._testcase._html_report_extra - extra = getattr(report, 'extra', []) - if extra_report[1]["content"]: - report.extra = extra + extra_report - except Exception: - pass + if sb_config._multithreaded: + sb_config._using_html_report = True # For Dashboard use + if ( + pytest_html + and report.when == "call" + and hasattr(sb_config, "dashboard") + ): + if sb_config.dashboard and not sb_config._sbase_detected: + test_id, display_id = _get_test_ids_(item) + r_outcome = report.outcome + if len(r_outcome) > 1: + r_outcome = r_outcome[0].upper() + r_outcome[1:] + sb_config._results[test_id] = r_outcome + sb_config._duration[test_id] = "*****" + sb_config._display_id[test_id] = display_id + sb_config._d_t_log_path[test_id] = "" + if test_id not in sb_config._extra_dash_entries: + sb_config._extra_dash_entries.append(test_id) + elif ( + sb_config._sbase_detected + and ((python3_11_or_newer and py311_patch2) or "--pdb" in sys_argv) + and (report.outcome == "failed" or "AssertionError" in str(call)) + and not sb_config._has_exception + ): + # Handle a bug on Python 3.11 where exceptions aren't seen + log_path = "" + sb_config._has_logs = True + if hasattr(sb_config, "_test_logpath"): + log_path = sb_config._test_logpath + if sb_config.dashboard: + sb_config._process_dashboard_entry(True) + if hasattr(sb_config, "_add_pytest_html_extra"): + sb_config._add_pytest_html_extra() + if hasattr(sb_config, "_visual_baseline_copies"): + sb_config._process_v_baseline_logs() + sb_config._excinfo_value = call.excinfo.value + sb_config._excinfo_tb = call.excinfo.tb + if "pytest_plugin.BaseClass.base_method" not in log_path: + source = None + if hasattr(sb_config, "_last_page_source"): + source = sb_config._last_page_source + if log_path and source: + log_helper.log_page_source(log_path, None, source) + last_page_screenshot_png = None + if hasattr(sb_config, "_last_page_screenshot_png"): + last_page_screenshot_png = ( + sb_config._last_page_screenshot_png + ) + if log_path and last_page_screenshot_png: + log_helper.log_screenshot( + log_path, None, last_page_screenshot_png + ) + if log_path: + sb_config._log_fail_data() + with suppress(Exception): + extra_report = None + if hasattr(item, "_testcase"): + extra_report = item._testcase._html_report_extra + elif hasattr(item.instance, "sb") or ( + item.nodeid in sb_config._sb_node + ): + if not hasattr(item.instance, "sb"): + sb_node = sb_config._sb_node[item.nodeid] + else: + sb_node = item.instance.sb + test_id = item.nodeid + if not test_id: + test_id = "unidentified_TestCase" + r""" + # Now using the nodeid for both the test_id and display_id. + # (This only impacts tests using The Dashboard.) + # If there are any issues, we'll revert back to the old code. + test_id = test_id.split("/")[-1].replace(" ", "_") + if "[" in test_id: + test_id_intro = test_id.split("[")[0] + parameter = test_id.split("[")[1] + parameter = re.sub(re.compile(r"\W"), "", parameter) + test_id = test_id_intro + "__" + parameter + test_id = test_id.replace("/", ".").replace("\\", ".") + test_id = test_id.replace("::", ".").replace(".py", "") + """ + sb_node._sb_test_identifier = test_id + if sb_node._needs_tearDown: + sb_node.tearDown() + sb_node._needs_tearDown = False + extra_report = sb_node._html_report_extra + else: + return + extra = getattr(report, "extra", []) + if len(extra_report) > 1 and extra_report[1]["content"]: + report.extras = extra + extra_report + if sb_config._dash_is_html_report: + # If the Dashboard URL is the same as the HTML Report URL, + # have the html report refresh back to a dashboard on update. + refresh_updates = ( + '" % constants.Dashboard.LIVE_JS + ) + report.extras.append(pytest_html.extras.html(refresh_updates)) diff --git a/seleniumbase/plugins/s3_logging_plugin.py b/seleniumbase/plugins/s3_logging_plugin.py old mode 100755 new mode 100644 index 28d991c6063..f83ac359f22 --- a/seleniumbase/plugins/s3_logging_plugin.py +++ b/seleniumbase/plugins/s3_logging_plugin.py @@ -1,49 +1,58 @@ -""" -The S3 Logging Plugin to upload all logs to the S3 bucket specifed. -""" - +"""S3 Logging Plugin for SeleniumBase tests that run with pynose / nosetests""" import uuid -import logging import os -from seleniumbase.core.s3_manager import S3LoggingBucket from nose.plugins import Plugin class S3Logging(Plugin): - """ - The plugin for uploading test logs to the S3 bucket specified. - """ - name = 's3_logging' # Usage: --with-s3_logging + """The plugin for uploading test logs to the S3 bucket specified.""" + name = "s3_logging" # Usage: --with-s3-logging def configure(self, options, conf): - """ Get the options. """ - super(S3Logging, self).configure(options, conf) + """Get the options.""" + super().configure(options, conf) self.options = options + self.test_id = None + + def save_data_to_logs(self, data, file_name): + from seleniumbase.fixtures import page_utils + + test_logpath = os.path.join(self.options.log_path, self.test_id) + file_name = str(file_name) + destination_folder = test_logpath + page_utils._save_data_as(data, destination_folder, file_name) def afterTest(self, test): - """ After each testcase, upload logs to the S3 bucket. """ + """Upload logs to the S3 bucket after tests complete.""" + from seleniumbase.core.s3_manager import S3LoggingBucket + + self.test_id = test.test.id() s3_bucket = S3LoggingBucket() guid = str(uuid.uuid4().hex) - path = "%s/%s" % (self.options.log_path, - test.test.id()) + path = os.path.join(self.options.log_path, self.test_id) uploaded_files = [] for logfile in os.listdir(path): - logfile_name = "%s/%s/%s" % (guid, - test.test.id(), - logfile.split(path)[-1]) - s3_bucket.upload_file(logfile_name, - "%s/%s" % (path, logfile)) + logfile_name = "%s/%s/%s" % ( + guid, + self.test_id, + logfile.split(path)[-1], + ) + s3_bucket.upload_file(logfile_name, os.path.join(path, logfile)) uploaded_files.append(logfile_name) s3_bucket.save_uploaded_file_names(uploaded_files) - index_file = s3_bucket.upload_index_file(test.id(), guid) - print("\n\n*** Log files uploaded: ***\n%s\n" % index_file) - logging.error("\n\n*** Log files uploaded: ***\n%s\n" % index_file) + index_file = s3_bucket.upload_index_file( + test.id(), guid, path, self.save_data_to_logs + ) + print("\n*** Log files uploaded: ***\n%s\n" % index_file) - # If the database plugin is running, attach a link - # to the logs index database row + # If the SB database plugin is also being used, + # attach a link to the logs index database row. if hasattr(test.test, "testcase_guid"): - from seleniumbase.core.testcase_manager \ - import TestcaseDataPayload, TestcaseManager + from seleniumbase.core.testcase_manager import ( + TestcaseDataPayload, + TestcaseManager, + ) + self.testcase_manager = TestcaseManager(self.options.database_env) data_payload = TestcaseDataPayload() data_payload.guid = test.test.testcase_guid diff --git a/seleniumbase/plugins/sb_manager.py b/seleniumbase/plugins/sb_manager.py new file mode 100644 index 00000000000..5efe51d8e93 --- /dev/null +++ b/seleniumbase/plugins/sb_manager.py @@ -0,0 +1,1508 @@ +""" +SeleniumBase as a Python Context Manager. +######################################### + +The SeleniumBase SB Context Manager: +Usage --> ``with SB() as sb:`` + +Example --> + +```python +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + url = "https://google.com/ncr" + sb.activate_cdp_mode(url) + sb.click_if_visible('button:contains("Accept all")') + sb.type('[name="q"]', "SeleniumBase GitHub page") + sb.click('[value="Google Search"]') + sb.sleep(2) + print(sb.get_page_title()) +``` + +# (The browser exits automatically after the "with" block ends.) + +######################################### +""" +from contextlib import contextmanager, suppress +from typing import Any, Generator +from seleniumbase import BaseCase + + +@contextmanager # Usage: -> ``with SB() as sb:`` +def SB( + test=None, # Test Mode: Output, Logging, Continue on failure unless "rtf". + rtf=None, # Shortcut / Duplicate of "raise_test_failure". + raise_test_failure=None, # If "test" mode, raise Exception on 1st failure. + browser=None, # Choose from "chrome", "edge", "firefox", or "safari". + headless=None, # Use the default headless mode for Chromium and Firefox. + headless1=None, # Use Chromium's old headless mode. (Fast, but limited) + headless2=None, # Use Chromium's new headless mode. (Has more features) + locale_code=None, # Set the Language Locale Code for the web browser. + protocol=None, # The Selenium Grid protocol: "http" or "https". + servername=None, # The Selenium Grid server/IP used for tests. + port=None, # The Selenium Grid port used by the test server. + proxy=None, # Use proxy. Format: "SERVER:PORT" or "USER:PASS@SERVER:PORT". + proxy_bypass_list=None, # Skip proxy when using the listed domains. + proxy_pac_url=None, # Use PAC file. (Format: URL or USERNAME:PASSWORD@URL) + multi_proxy=None, # Allow multiple proxies with auth when multi-threaded. + agent=None, # Modify the web browser's User-Agent string. + cap_file=None, # The desired capabilities to use with a Selenium Grid. + cap_string=None, # The desired capabilities to use with a Selenium Grid. + recorder_ext=None, # Enables the SeleniumBase Recorder Chromium extension. + disable_cookies=None, # Disable Cookies on websites. (Pages might break!) + disable_js=None, # Disable JavaScript on websites. (Pages might break!) + disable_csp=None, # Disable the Content Security Policy of websites. + enable_ws=None, # Enable Web Security on Chromium-based browsers. + enable_sync=None, # Enable "Chrome Sync" on websites. + use_auto_ext=None, # Use Chrome's automation extension. + undetectable=None, # Use undetected-chromedriver to evade bot-detection. + uc_cdp_events=None, # Capture CDP events in undetected-chromedriver mode. + uc_subprocess=None, # Use undetected-chromedriver as a subprocess. + log_cdp_events=None, # Capture {"performance": "ALL", "browser": "ALL"} + incognito=None, # Enable Chromium's Incognito mode. + guest_mode=None, # Enable Chromium's Guest mode. + dark_mode=None, # Enable Chromium's Dark mode. + devtools=None, # Open Chromium's DevTools when the browser opens. + remote_debug=None, # Enable Chrome's Debugger on "http://localhost:9222". + enable_3d_apis=None, # Enable WebGL and 3D APIs. + swiftshader=None, # Chrome: --use-gl=angle / --use-angle=swiftshader-webgl + ad_block_on=None, # Block some types of display ads from loading. + host_resolver_rules=None, # Set host-resolver-rules, comma-separated. + block_images=None, # Block images from loading during tests. + do_not_track=None, # Tell websites that you don't want to be tracked. + chromium_arg=None, # "ARG=N,ARG2" (Set Chromium args, ","-separated.) + firefox_arg=None, # "ARG=N,ARG2" (Set Firefox args, comma-separated.) + firefox_pref=None, # SET (Set Firefox PREFERENCE:VALUE set, ","-separated) + user_data_dir=None, # Set the Chrome user data directory to use. + extension_zip=None, # Load a Chrome Extension .zip|.crx, comma-separated. + extension_dir=None, # Load a Chrome Extension directory, comma-separated. + disable_features=None, # "F1,F2" (Disable Chrome features, ","-separated.) + binary_location=None, # Set path of the Chromium browser binary to use. + driver_version=None, # Set the chromedriver or uc_driver version to use. + skip_js_waits=None, # Skip JS Waits (readyState=="complete" and Angular). + wait_for_angularjs=None, # Wait for AngularJS to load after some actions. + external_pdf=None, # Set Chrome "plugins.always_open_pdf_externally":True. + window_position=None, # Set the browser's starting window position: "X,Y" + window_size=None, # Set the browser's starting window size: "Width,Height" + is_mobile=None, # Use the mobile device emulator while running tests. + mobile=None, # Shortcut / Duplicate of "is_mobile". + device_metrics=None, # Set mobile metrics: "CSSWidth,CSSHeight,PixelRatio" + xvfb=None, # Run tests using the Xvfb virtual display server on Linux OS. + xvfb_metrics=None, # Set Xvfb display size on Linux: "Width,Height". + start_page=None, # The starting URL for the web browser when tests begin. + rec_print=None, # If Recorder is enabled, prints output after tests end. + rec_sb_mgr=None, # Recorder Mode that generates SB() context manager code. + rec_sb_cdp=None, # Recorder Mode that generates Pure CDP Mode sb_cdp code. + rec_behave=None, # Like Recorder Mode, but also generates behave-gherkin. + record_sleep=None, # If Recorder enabled, also records self.sleep calls. + data=None, # Extra test data. Access with "self.data" in tests. + var1=None, # Extra test data. Access with "self.var1" in tests. + var2=None, # Extra test data. Access with "self.var2" in tests. + var3=None, # Extra test data. Access with "self.var3" in tests. + variables=None, # DICT (Extra test data. Access with "self.variables") + account=None, # Set account. Access with "self.account" in tests. + environment=None, # Set the test env. Access with "self.env" in tests. + headed=None, # Run tests in headed/GUI mode on Linux, where not default. + maximize=None, # Start tests with the browser window maximized. + disable_ws=None, # Reverse of "enable_ws". (None and False are different) + disable_beforeunload=None, # Disable the "beforeunload" event on Chromium. + settings_file=None, # A file for overriding default SeleniumBase settings. + position=None, # Shortcut / Duplicate of "window_position". + size=None, # Shortcut / Duplicate of "window_size". + uc=None, # Shortcut / Duplicate of "undetectable". + undetected=None, # Shortcut / Duplicate of "undetectable". + uc_cdp=None, # Shortcut / Duplicate of "uc_cdp_events". + uc_sub=None, # Shortcut / Duplicate of "uc_subprocess". + locale=None, # Shortcut / Duplicate of "locale_code". + log_cdp=None, # Shortcut / Duplicate of "log_cdp_events". + ad_block=None, # Shortcut / Duplicate of "ad_block_on". + server=None, # Shortcut / Duplicate of "servername". + guest=None, # Shortcut / Duplicate of "guest_mode". + pls=None, # Shortcut / Duplicate of "page_load_strategy". + sjw=None, # Shortcut / Duplicate of "skip_js_waits". + wfa=None, # Shortcut / Duplicate of "wait_for_angularjs". + cft=None, # Use "Chrome for Testing" + chs=None, # Use "Chrome-Headless-Shell" + use_chromium=None, # Use base "Chromium" + save_screenshot=None, # Save a screenshot at the end of each test. + no_screenshot=None, # No screenshots saved unless tests directly ask it. + page_load_strategy=None, # Set Chrome PLS to "normal", "eager", or "none". + timeout_multiplier=None, # Multiplies the default timeout values. + js_checking_on=None, # Check for JavaScript errors after page loads. + slow=None, # Slow down the automation. Faster than using Demo Mode. + demo=None, # Slow down and visually see test actions as they occur. + demo_sleep=None, # SECONDS (Set wait time after Slow & Demo Mode actions.) + message_duration=None, # SECONDS (The time length for Messenger alerts.) + highlights=None, # Number of highlight animations for Demo Mode actions. + interval=None, # SECONDS (Autoplay interval for SB Slides & Tour steps.) + time_limit=None, # SECONDS (Safely fail tests that exceed the time limit.) +) -> Generator[BaseCase, Any, None]: + """ + * SeleniumBase as a Python Context Manager * + + Example: + -------- + .. code-block:: python + from seleniumbase import SB + + with SB(uc=True, test=True) as sb: + url = "https://google.com/ncr" + sb.activate_cdp_mode(url) + sb.click_if_visible('button:contains("Accept all")') + sb.type('[name="q"]', "SeleniumBase GitHub page") + sb.click('[value="Google Search"]') + sb.sleep(2) + print(sb.get_page_title()) + + Optional Parameters: + -------------------- + test (bool): Test Mode: Output, Logging, Continue on failure unless "rtf". + rtf (bool): Shortcut / Duplicate of "raise_test_failure". + raise_test_failure (bool): If "test" mode, raise Exception on 1st failure. + browser (str): Choose from "chrome", "edge", "firefox", or "safari". + headless (bool): Use the default headless mode for Chromium and Firefox. + headless1 (bool): Use Chromium's old headless mode. (Fast, but limited) + headless2 (bool): Use Chromium's new headless mode. (Has more features) + locale_code (str): Set the Language Locale Code for the web browser. + protocol (str): The Selenium Grid protocol: "http" or "https". + servername (str): The Selenium Grid server/IP used for tests. + port (int): The Selenium Grid port used by the test server. + proxy (str): Use proxy. Format: "SERVER:PORT" or "USER:PASS@SERVER:PORT". + proxy_bypass_list (str): Skip proxy when using the listed domains. + proxy_pac_url (str): Use PAC file. (Format: URL or USERNAME:PASSWORD@URL) + multi_proxy (bool): # Allow multiple proxies with auth when multithreaded. + agent (str): Modify the web browser's User-Agent string. + cap_file (str): The desired capabilities to use with a Selenium Grid. + cap_string (str): The desired capabilities to use with a Selenium Grid. + recorder_ext (bool): Enables the SeleniumBase Recorder Chromium extension. + disable_cookies (bool): Disable Cookies on websites. (Pages might break!) + disable_js (bool): Disable JavaScript on websites. (Pages might break!) + disable_csp (bool): Disable the Content Security Policy of websites. + enable_ws (bool): Enable Web Security on Chromium-based browsers. + enable_sync (bool): Enable "Chrome Sync" on websites. + use_auto_ext (bool): Use Chrome's automation extension. + undetectable (bool): Use undetected-chromedriver to evade bot-detection. + uc_cdp_events (bool): Capture CDP events in undetected-chromedriver mode. + uc_subprocess (bool): Use undetected-chromedriver as a subprocess. + log_cdp_events (bool): Capture {"performance": "ALL", "browser": "ALL"} + incognito (bool): Enable Chromium's Incognito mode. + guest_mode (bool): Enable Chromium's Guest mode. + dark_mode (bool): Enable Chromium's Dark mode. + devtools (bool): Open Chromium's DevTools when the browser opens. + remote_debug (bool): Enable Chrome's Debugger on "http://localhost:9222". + enable_3d_apis (bool): Enable WebGL and 3D APIs. + swiftshader (bool): Chrome: --use-gl=angle / --use-angle=swiftshader-webgl + ad_block_on (bool): Block some types of display ads from loading. + host_resolver_rules (str): Set host-resolver-rules, comma-separated. + block_images (bool): Block images from loading during tests. + do_not_track (bool): Tell websites that you don't want to be tracked. + chromium_arg (str): "ARG=N,ARG2" (Set Chromium args, ","-separated.) + firefox_arg (str): "ARG=N,ARG2" (Set Firefox args, comma-separated.) + firefox_pref (str): SET (Set Firefox PREFERENCE:VALUE set, ","-separated) + user_data_dir (str): Set the Chrome user data directory to use. + extension_zip (str): Load a Chrome Extension .zip|.crx, comma-separated. + extension_dir (str): Load a Chrome Extension directory, comma-separated. + disable_features (str): "F1,F2" (Disable Chrome features, ","-separated.) + binary_location (str): Set path of the Chromium browser binary to use. + driver_version (str): Set the chromedriver or uc_driver version to use. + skip_js_waits (bool): Skip JS Waits (readyState=="complete" and Angular). + wait_for_angularjs (bool): Wait for AngularJS to load after some actions. + external_pdf (bool): Set Chrome "plugins.always_open_pdf_externally":True. + window_position (x,y): Set the browser's starting window position: "X,Y" + window_size (w,h): Set the browser's starting window size: "Width,Height" + is_mobile (bool): Use the mobile device emulator while running tests. + mobile (bool): Shortcut / Duplicate of "is_mobile". + device_metrics (w,h,pr): Mobile metrics: "CSSWidth,CSSHeight,PixelRatio" + xvfb (bool): Run tests using the Xvfb virtual display server on Linux OS. + xvfb_metrics (w,h): Set Xvfb display size on Linux: "Width,Height". + start_page (str): The starting URL for the web browser when tests begin. + rec_print (bool): If Recorder is enabled, prints output after tests end. + rec_sb_mgr (bool): Recorder Mode that generates SB() context manager code. + rec_sb_cdp (bool): Recorder Mode that generates Pure CDP Mode sb_cdp code. + rec_behave (bool): Like Recorder Mode, but also generates behave-gherkin. + record_sleep (bool): If Recorder enabled, also records self.sleep calls. + data (str): Extra test data. Access with "self.data" in tests. + var1 (str): Extra test data. Access with "self.var1" in tests. + var2 (str): Extra test data. Access with "self.var2" in tests. + var3 (str): Extra test data. Access with "self.var3" in tests. + variables (dict): Extra test data. Access with "self.variables". + account (str): Set account. Access with "self.account" in tests. + environment (str): Set the test env. Access with "self.env" in tests. + headed (bool): Run tests in headed/GUI mode on Linux, where not default. + maximize (bool): Start tests with the browser window maximized. + disable_ws (bool): Reverse of "enable_ws". (None and False are different) + disable_beforeunload (bool): Disable the "beforeunload" event on Chromium. + settings_file (str): A file for overriding default SeleniumBase settings. + position (x,y): Shortcut / Duplicate of "window_position". + size (w,h): Shortcut / Duplicate of "window_size". + uc (bool): Shortcut / Duplicate of "undetectable". + undetected (bool): Shortcut / Duplicate of "undetectable". + uc_cdp (bool): Shortcut / Duplicate of "uc_cdp_events". + uc_sub (bool): Shortcut / Duplicate of "uc_subprocess". + locale (str): Shortcut / Duplicate of "locale_code". + log_cdp (bool): Shortcut / Duplicate of "log_cdp_events". + ad_block (bool): Shortcut / Duplicate of "ad_block_on". + server (str): Shortcut / Duplicate of "servername". + guest (bool): Shortcut / Duplicate of "guest_mode". + pls (str): Shortcut / Duplicate of "page_load_strategy". + sjw (bool): Shortcut / Duplicate of "skip_js_waits". + wfa (bool): Shortcut / Duplicate of "wait_for_angularjs". + save_screenshot (bool): Save a screenshot at the end of each test. + no_screenshot (bool): No screenshots saved unless tests directly ask it. + page_load_strategy (str): Set Chrome PLS to "normal", "eager", or "none". + timeout_multiplier (float): Multiplies the default timeout values. + js_checking_on (bool): Check for JavaScript errors after page loads. + slow (bool): Slow down the automation. Faster than using Demo Mode. + demo (bool): Slow down and visually see test actions as they occur. + demo_sleep (float): SECONDS (Set wait time after Slow & Demo Mode actions) + message_duration (float): SECONDS (The time length for Messenger alerts.) + highlights (int): Number of highlight animations for Demo Mode actions. + interval (float): SECONDS (Autoplay interval for SB Slides & Tour steps.) + time_limit (float): SECONDS (Safely fail tests that exceed the time limit) + """ + import colorama + import gc + import os + import sys + import time + import traceback + from seleniumbase import config as sb_config + from seleniumbase.config import settings + from seleniumbase.core import detect_b_ver + from seleniumbase.fixtures import constants + from seleniumbase.fixtures import shared_utils + + sb_config_backup = sb_config + sb_config._do_sb_post_mortem = False + sys_argv = sys.argv + arg_join = " ".join(sys_argv) + archive_logs = False + existing_runner = False + collect_only = ("--co" in sys_argv or "--collect-only" in sys_argv) + all_scripts = (hasattr(sb_config, "all_scripts") and sb_config.all_scripts) + do_log_folder_setup = False # The first "test=True" run does it + inner_test = False + if ( + (hasattr(sb_config, "is_behave") and sb_config.is_behave) + or (hasattr(sb_config, "is_pytest") and sb_config.is_pytest) + or (hasattr(sb_config, "is_nosetest") and sb_config.is_nosetest) + ): + existing_runner = True + if test: + inner_test = True + test = False # Already using a test runner. Skip extra test steps. + elif test is None and "--test" in sys_argv: + test = True + if existing_runner and not hasattr(sb_config, "_context_of_runner"): + if hasattr(sb_config, "is_pytest") and sb_config.is_pytest: + import pytest + msg = "Skipping `SB()` script. (Use `python`, not `pytest`)" + if not collect_only and not all_scripts: + print("\n *** %s" % msg) + if collect_only or not all_scripts: + pytest.skip(allow_module_level=True) + elif hasattr(sb_config, "is_nosetest") and sb_config.is_nosetest: + raise Exception( + "\n SB Manager script was triggered by nosetest collection!" + '\n (Prevent that by using: ``if __name__ == "__main__":``)' + ) + elif existing_runner: + sb_config._context_of_runner = True + if ( + not existing_runner + and not hasattr(sb_config, "_has_older_context") + and test + ): + # This is the first "test" from context manager scripts run. + sb_config._has_older_context = True + do_log_folder_setup = True + else: + if test: + pass # Not the first "test" of context manager scripts. + else: + pass # Not in "test" mode. (No special output/logging.) + with_testing_base = False + if test: + with_testing_base = True + if ( + raise_test_failure + or rtf + or "--raise-test-failure" in sys_argv + or "--raise_test_failure" in sys_argv + or "--rtf" in sys_argv + or "-x" in sys_argv # Carry-over from "pytest" + or "--exitfirst" in sys_argv # Carry-over from "pytest" + ): + raise_test_failure = True # Exit on first error or failed test. + else: + raise_test_failure = False + sb_config._browser_shortcut = None + sb_config._cdp_browser = None + sb_config._cdp_bin_loc = None + browser_changes = 0 + browser_set = None + browser_text = None + browser_list = [] + # Check if binary-location in options + bin_loc_in_options = False + if ( + binary_location + and len(str(binary_location)) > 5 + and os.path.exists(str(binary_location)) + ): + bin_loc_in_options = True + else: + for arg in sys_argv: + if arg in ["--binary-location", "--binary_location", "--bl"]: + bin_loc_in_options = True + if ( + browser + and browser in constants.ChromiumSubs.chromium_subs + and not bin_loc_in_options + ): + bin_loc = detect_b_ver.get_binary_location(browser) + if bin_loc and os.path.exists(bin_loc): + if browser in bin_loc.lower().split("/")[-1].split("\\")[-1]: + sb_config._cdp_browser = browser + sb_config._cdp_bin_loc = bin_loc + binary_location = bin_loc + bin_loc_in_options = True + # As a shortcut, you can use "--edge" instead of "--browser=edge", etc, + # but you can only specify one default browser for tests. (Default: chrome) + if "--browser=chrome" in sys_argv or "--browser chrome" in sys_argv: + browser_changes += 1 + browser_set = "chrome" + browser_list.append("--browser=chrome") + if "--browser=edge" in sys_argv or "--browser edge" in sys_argv: + browser_changes += 1 + browser_set = "edge" + browser_list.append("--browser=edge") + if "--browser=firefox" in sys_argv or "--browser firefox" in sys_argv: + browser_changes += 1 + browser_set = "firefox" + browser_list.append("--browser=firefox") + if "--browser=safari" in sys_argv or "--browser safari" in sys_argv: + browser_changes += 1 + browser_set = "safari" + browser_list.append("--browser=safari") + if "--browser=ie" in sys_argv or "--browser ie" in sys_argv: + browser_changes += 1 + browser_set = "ie" + browser_list.append("--browser=ie") + if "--browser=remote" in sys_argv or "--browser remote" in sys_argv: + browser_changes += 1 + browser_set = "remote" + browser_list.append("--browser=remote") + if "--browser=opera" in sys_argv or "--browser opera" in sys_argv: + if not bin_loc_in_options: + bin_loc = detect_b_ver.get_binary_location("opera") + if os.path.exists(bin_loc): + browser_changes += 1 + browser_set = "opera" + sb_config._browser_shortcut = "opera" + sb_config._cdp_browser = "opera" + sb_config._cdp_bin_loc = bin_loc + browser_list.append("--browser=opera") + if "--browser=brave" in sys_argv or "--browser brave" in sys_argv: + if not bin_loc_in_options: + bin_loc = detect_b_ver.get_binary_location("brave") + if os.path.exists(bin_loc): + browser_changes += 1 + browser_set = "brave" + sb_config._browser_shortcut = "brave" + sb_config._cdp_browser = "brave" + sb_config._cdp_bin_loc = bin_loc + browser_list.append("--browser=brave") + if "--browser=comet" in sys_argv or "--browser comet" in sys_argv: + if not bin_loc_in_options: + bin_loc = detect_b_ver.get_binary_location("comet") + if os.path.exists(bin_loc): + browser_changes += 1 + browser_set = "comet" + sb_config._browser_shortcut = "comet" + sb_config._cdp_browser = "comet" + sb_config._cdp_bin_loc = bin_loc + browser_list.append("--browser=comet") + browser_text = browser_set + if "--chrome" in sys_argv and not browser_set == "chrome": + browser_changes += 1 + browser_text = "chrome" + sb_config._browser_shortcut = "chrome" + browser_list.append("--chrome") + if "--edge" in sys_argv and not browser_set == "edge": + browser_changes += 1 + browser_text = "edge" + sb_config._browser_shortcut = "edge" + browser_list.append("--edge") + if "--firefox" in sys_argv and not browser_set == "firefox": + browser_changes += 1 + browser_text = "firefox" + sb_config._browser_shortcut = "firefox" + browser_list.append("--firefox") + if "--ie" in sys_argv and not browser_set == "ie": + browser_changes += 1 + browser_text = "ie" + sb_config._browser_shortcut = "ie" + browser_list.append("--ie") + if "--safari" in sys_argv and not browser_set == "safari": + browser_changes += 1 + browser_text = "safari" + sb_config._browser_shortcut = "safari" + browser_list.append("--safari") + if "--opera" in sys_argv and not browser_set == "opera": + if not bin_loc_in_options: + bin_loc = detect_b_ver.get_binary_location("opera") + if os.path.exists(bin_loc): + browser_changes += 1 + browser_text = "opera" + sb_config._browser_shortcut = "opera" + sb_config._cdp_browser = "opera" + sb_config._cdp_bin_loc = bin_loc + browser_list.append("--opera") + if "--brave" in sys_argv and not browser_set == "brave": + if not bin_loc_in_options: + bin_loc = detect_b_ver.get_binary_location("brave") + if os.path.exists(bin_loc): + browser_changes += 1 + browser_text = "brave" + sb_config._browser_shortcut = "brave" + sb_config._cdp_browser = "brave" + sb_config._cdp_bin_loc = bin_loc + browser_list.append("--brave") + if "--comet" in sys_argv and not browser_set == "comet": + if not bin_loc_in_options: + bin_loc = detect_b_ver.get_binary_location("comet") + if os.path.exists(bin_loc): + browser_changes += 1 + browser_text = "comet" + sb_config._browser_shortcut = "comet" + sb_config._cdp_browser = "comet" + sb_config._cdp_bin_loc = bin_loc + browser_list.append("--comet") + if browser_changes > 1: + message = "\n\n TOO MANY browser types were entered!" + message += "\n There were %s found:\n > %s" % ( + browser_changes, + ", ".join(browser_list), + ) + message += "\n ONLY ONE default browser is allowed!" + message += "\n Select a single browser & try again!\n" + if not browser: + raise Exception(message) + if browser is None: + if browser_text: + browser = browser_text + else: + browser = "chrome" + else: + browser = browser.lower() + valid_browsers = constants.ValidBrowsers.valid_browsers + if browser not in valid_browsers: + raise Exception( + "Browser: {%s} is not a valid browser option. " + "Valid options = {%s}" % (browser, valid_browsers) + ) + if headless is None: + if "--headless" in sys_argv: + headless = True + else: + headless = False + if headless1 is None: + if "--headless1" in sys_argv: + headless1 = True + else: + headless1 = False + if headless1: + headless = True + if headless2 is None: + if "--headless2" in sys_argv: + headless2 = True + else: + headless2 = False + if protocol is None: + protocol = "http" # For the Selenium Grid only! + if server is not None and servername is None: + servername = server + if servername is None: + servername = "localhost" # For the Selenium Grid only! + if port is None: + port = "4444" # For the Selenium Grid only! + if not environment: + environment = "test" + if incognito is None: + if "--incognito" in sys_argv: + incognito = True + else: + incognito = False + if guest is not None and guest_mode is None: + guest_mode = guest + if guest_mode is None: + if "--guest" in sys_argv: + guest_mode = True + else: + guest_mode = False + if dark_mode is None: + if "--dark" in sys_argv: + dark_mode = True + else: + dark_mode = False + if devtools is None: + if "--devtools" in sys_argv: + devtools = True + else: + devtools = False + if mobile is not None and is_mobile is None: + is_mobile = mobile + if is_mobile is None: + if "--mobile" in sys_argv: + is_mobile = True + else: + is_mobile = False + if is_mobile: + sb_config.mobile_emulator = True + proxy_string = proxy + if proxy_string is None and "--proxy" in arg_join: + if "--proxy=" in arg_join: + proxy_string = arg_join.split("--proxy=")[1].split(" ")[0] + elif "--proxy " in arg_join: + proxy_string = arg_join.split("--proxy ")[1].split(" ")[0] + if proxy_string: + if proxy_string.startswith('"') and proxy_string.endswith('"'): + proxy_string = proxy_string[1:-1] + elif proxy_string.startswith("'") and proxy_string.endswith("'"): + proxy_string = proxy_string[1:-1] + c_a = chromium_arg + if c_a is None and "--chromium-arg" in arg_join: + count = 0 + for arg in sys_argv: + if arg.startswith("--chromium-arg="): + c_a = arg.split("--chromium-arg=")[1] + break + elif arg == "--chromium-arg" and len(sys_argv) > count + 1: + c_a = sys_argv[count + 1] + if c_a.startswith("-"): + c_a = None + break + count += 1 + chromium_arg = c_a + d_f = disable_features + if d_f is None and "--disable-features" in arg_join: + count = 0 + for arg in sys_argv: + if arg.startswith("--disable-features="): + d_f = arg.split("--disable-features=")[1] + break + elif arg == "--disable-features" and len(sys_argv) > count + 1: + d_f = sys_argv[count + 1] + if d_f.startswith("-"): + d_f = None + break + count += 1 + disable_features = d_f + if window_position is None and position is not None: + window_position = position + w_p = window_position + if ( + w_p is None + and ("--window-position" in arg_join or "--position" in arg_join) + ): + count = 0 + for arg in sys_argv: + if arg.startswith("--window-position="): + w_p = arg.split("--window-position=")[1] + break + elif arg == "--window-position" and len(sys_argv) > count + 1: + w_p = sys_argv[count + 1] + if w_p.startswith("-"): + w_p = None + break + count += 1 + window_position = w_p + if window_size is None and size is not None: + window_size = size + w_s = window_size + if w_s is None and "--window-size" in arg_join: + count = 0 + for arg in sys_argv: + if arg.startswith("--window-size="): + w_s = arg.split("--window-size=")[1] + break + elif arg == "--window-size" and len(sys_argv) > count + 1: + w_s = sys_argv[count + 1] + if w_s.startswith("-"): + w_s = None + break + count += 1 + window_size = w_s + x_m = xvfb_metrics + if x_m is None and "--xvfb-metrics" in arg_join: + count = 0 + for arg in sys_argv: + if arg.startswith("--xvfb-metrics="): + x_m = arg.split("--xvfb-metrics=")[1] + break + elif arg == "--xvfb-metrics" and len(sys_argv) > count + 1: + x_m = sys_argv[count + 1] + if x_m.startswith("-"): + x_m = None + break + count += 1 + xvfb_metrics = x_m + if agent is None and "--agent" in arg_join: + count = 0 + for arg in sys_argv: + if arg.startswith("--agent="): + agent = arg.split("--agent=")[1] + break + elif arg == "--agent" and len(sys_argv) > count + 1: + agent = sys_argv[count + 1] + if agent.startswith("-"): + agent = None + break + count += 1 + user_agent = agent + found_bl = None + if binary_location is None and "--binary-location" in arg_join: + count = 0 + for arg in sys_argv: + if arg.startswith("--binary-location="): + found_bl = arg.split("--binary-location=")[1] + break + elif arg == "--binary-location" and len(sys_argv) > count + 1: + found_bl = sys_argv[count + 1] + if found_bl.startswith("-"): + found_bl = None + break + count += 1 + if found_bl: + binary_location = found_bl + if binary_location is None and "--bl=" in arg_join: + for arg in sys_argv: + if arg.startswith("--bl="): + binary_location = arg.split("--bl=")[1] + break + if use_chromium and not binary_location: + binary_location = "_chromium_" + elif cft and not binary_location: + binary_location = "cft" + elif chs and not binary_location: + binary_location = "chs" + if ( + ("--use-chromium" in sys_argv and not binary_location) + or ("--chromium" in sys_argv and not binary_location) + ): + binary_location = "_chromium_" + elif "--cft" in sys_argv and not binary_location: + binary_location = "cft" + elif "--chs" in sys_argv and not binary_location: + binary_location = "chs" + if ( + binary_location + and binary_location.lower() == "chs" + and browser == "chrome" + ): + headless = True + headless1 = False + headless2 = False + recorder_mode = False + if recorder_ext: + recorder_mode = True + if ( + "--recorder" in sys_argv + or "--record" in sys_argv + or "--rec" in sys_argv + ): + recorder_mode = True + recorder_ext = True + if rec_print is None: + if "--rec-print" in sys_argv: + rec_print = True + else: + rec_print = False + if rec_sb_mgr is None: + if "--rec-sb-mgr" in sys_argv: + rec_sb_mgr = True + else: + rec_sb_mgr = False + if rec_sb_cdp is None: + if "--rec-sb-cdp" in sys_argv: + rec_sb_cdp = True + else: + rec_sb_cdp = False + if rec_behave is None: + if "--rec-behave" in sys_argv: + rec_behave = True + else: + rec_behave = False + if record_sleep is None: + if "--rec-sleep" in sys_argv or "--record-sleep" in sys_argv: + record_sleep = True + else: + record_sleep = False + if xvfb is None: + if "--xvfb" in sys_argv: + xvfb = True + else: + xvfb = False + if not shared_utils.is_linux(): + # The Xvfb virtual display server is for Linux OS Only! + xvfb = False + if ( + undetectable + or undetected + or uc + or uc_cdp_events + or uc_cdp + or uc_subprocess + or uc_sub + ): + undetectable = True + if undetectable or undetected or uc: + uc_subprocess = True # Use UC as a subprocess by default. + elif ( + "--undetectable" in sys_argv + or "--undetected" in sys_argv + or "--uc" in sys_argv + or "--uc-cdp-events" in sys_argv + or "--uc_cdp_events" in sys_argv + or "--uc-cdp" in sys_argv + or "--uc-subprocess" in sys_argv + or "--uc_subprocess" in sys_argv + or "--uc-sub" in sys_argv + ): + undetectable = True + if uc_subprocess is None and uc_sub is None: + uc_subprocess = True # Use UC as a subprocess by default. + else: + undetectable = False + if uc_subprocess or uc_sub: + uc_subprocess = True + elif ( + "--uc-subprocess" in sys_argv + or "--uc_subprocess" in sys_argv + or "--uc-sub" in sys_argv + ): + uc_subprocess = True + else: + uc_subprocess = False + if uc_cdp_events or uc_cdp: + undetectable = True + uc_cdp_events = True + elif ( + "--uc-cdp-events" in sys_argv + or "--uc_cdp_events" in sys_argv + or "--uc-cdp" in sys_argv + or "--uc_cdp" in sys_argv + ): + undetectable = True + uc_cdp_events = True + else: + uc_cdp_events = False + if ( + undetectable + and browser not in ["chrome", "opera", "brave", "comet"] + ): + message = ( + '\n Undetected-Chromedriver Mode ONLY supports Chromium browsers!' + '\n ("uc=True" / "undetectable=True" / "--uc")' + '\n (Your browser choice was: "%s".)' + '\n (Will use "%s" without UC Mode.)\n' % (browser, browser) + ) + print(message) + if headed is None: + # Override the default headless mode on Linux if set. + if "--gui" in sys_argv or "--headed" in sys_argv: + headed = True + else: + headed = False + if ( + shared_utils.is_linux() + and not headed + and not headless + and not headless2 + and not xvfb + ): + if not undetectable: + headless = True + else: + xvfb = True + if headless2 and browser == "firefox": + headless2 = False # Only for Chromium browsers + headless = True # Firefox has regular headless + elif browser not in ["chrome", "edge", "opera", "brave", "comet"]: + headless2 = False # Only for Chromium browsers + if not headless and not headless2: + headed = True + if not recorder_mode: + if ( + record_sleep + or rec_print + or rec_behave + or rec_sb_mgr + or rec_sb_cdp + ): + recorder_mode = True + recorder_ext = True + if recorder_mode and headless: + headless = False + headless1 = False + headless2 = True + sb_config.proxy_driver = False + if "--proxy-driver" in sys_argv or "--proxy_driver" in sys_argv: + sb_config.proxy_driver = True + if variables and isinstance(variables, str) and len(variables) > 0: + import ast + bad_input = False + if ( + not variables.startswith("{") + or not variables.endswith("}") + ): + bad_input = True + else: + try: + variables = ast.literal_eval(variables) + if not isinstance(variables, dict): + bad_input = True + except Exception: + bad_input = True + if bad_input: + raise Exception( + '\nExpecting a Python dictionary for "variables"!' + "\nEg. --variables=\"{'KEY1':'VALUE', 'KEY2':123}\"" + ) + elif not isinstance(variables, dict): + variables = {} + if disable_csp is None: + if ( + "--disable-csp" in sys_argv + or "--no-csp" in sys_argv + or "--dcsp" in sys_argv + ): + disable_csp = True + else: + disable_csp = False + if ( + (enable_ws is None and disable_ws is None) + and ( + "--disable-web-security" in sys_argv + or "--disable-ws" in sys_argv + or "--dws" in sys_argv + ) + ): + enable_ws = False + disable_ws = True + elif ( + (enable_ws is None and disable_ws is None) + or (disable_ws is not None and not disable_ws) + or (enable_ws is not None and enable_ws) + ): + enable_ws = True + disable_ws = False + else: + enable_ws = False + disable_ws = True + if log_cdp_events is None and log_cdp is None: + if ( + "--log-cdp-events" in sys_argv + or "--log_cdp_events" in sys_argv + or "--log-cdp" in sys_argv + or "--log_cdp" in sys_argv + ): + log_cdp_events = True + else: + log_cdp_events = False + elif log_cdp_events or log_cdp: + log_cdp_events = True + else: + log_cdp_events = False + if use_auto_ext is None: + if "--use-auto-ext" in sys_argv: + use_auto_ext = True + else: + use_auto_ext = False + if disable_cookies is None: + if "--disable-cookies" in sys_argv: + disable_cookies = True + else: + disable_cookies = False + if disable_js is None: + if "--disable-js" in sys_argv: + disable_js = True + else: + disable_js = False + maximize_option = False + if maximize or "--maximize" in sys_argv: + maximize_option = True + _disable_beforeunload = False + if disable_beforeunload: + _disable_beforeunload = True + if pls is not None and page_load_strategy is None: + page_load_strategy = pls + if not page_load_strategy and "--pls=" in arg_join: + if "--pls=none" in sys_argv or '--pls="none"' in sys_argv: + page_load_strategy = "none" + elif "--pls=eager" in sys_argv or '--pls="eager"' in sys_argv: + page_load_strategy = "eager" + elif "--pls=normal" in sys_argv or '--pls="normal"' in sys_argv: + page_load_strategy = "normal" + if page_load_strategy is not None: + if page_load_strategy.lower() not in ["normal", "eager", "none"]: + raise Exception( + 'page_load_strategy must be "normal", "eager", or "none"!' + ) + page_load_strategy = page_load_strategy.lower() + elif "--pls=normal" in sys_argv or '--pls="normal"' in sys_argv: + page_load_strategy = "normal" + elif "--pls=eager" in sys_argv or '--pls="eager"' in sys_argv: + page_load_strategy = "eager" + elif "--pls=none" in sys_argv or '--pls="none"' in sys_argv: + page_load_strategy = "none" + if sjw is not None and skip_js_waits is None: + skip_js_waits = sjw + if skip_js_waits is None: + if ( + "--sjw" in sys_argv + or "--skip_js_waits" in sys_argv + or "--skip-js-waits" in sys_argv + ): + settings.SKIP_JS_WAITS = True + elif skip_js_waits: + settings.SKIP_JS_WAITS = skip_js_waits + if wfa is not None and wait_for_angularjs is None: + wait_for_angularjs = wfa + if wait_for_angularjs is None: + if ( + "--wfa" in sys_argv + or "--wait_for_angularjs" in sys_argv + or "--wait-for-angularjs" in sys_argv + ): + settings.WAIT_FOR_ANGULARJS = True + elif wait_for_angularjs: + settings.WAIT_FOR_ANGULARJS = wait_for_angularjs + if save_screenshot is None: + if ( + "--screenshot" in sys_argv + or "--save-screenshot" in sys_argv + or "--ss" in sys_argv + ): + save_screenshot = True + else: + save_screenshot = False + if no_screenshot is None: + if "--no-screenshot" in sys_argv or "--ns" in sys_argv: + no_screenshot = True + else: + no_screenshot = False + if save_screenshot and no_screenshot: + save_screenshot = False # "no_screenshot" has priority + if browser == "safari" and headless: + headless = False # Safari doesn't support headless mode + headless1 = False + if js_checking_on is None: + if "--check-js" in sys_argv: + js_checking_on = True + else: + js_checking_on = False + slow_mode = False + if slow: + slow_mode = True + elif "--slow" in sys_argv: + slow_mode = True + demo_mode = False + if demo: + demo_mode = True + elif "--demo" in sys_argv: + demo_mode = True + if block_images is None: + if "--block-images" in sys_argv or "--block_images" in sys_argv: + block_images = True + else: + block_images = False + if do_not_track is None: + if "--do-not-track" in sys_argv or "--do_not_track" in sys_argv: + do_not_track = True + else: + do_not_track = False + if external_pdf is None: + if "--external-pdf" in sys_argv or "--external_pdf" in sys_argv: + external_pdf = True + else: + external_pdf = False + if remote_debug is None: + if "--remote-debug" in sys_argv or "--remote_debug" in sys_argv: + remote_debug = True + else: + remote_debug = False + if enable_3d_apis is None: + if "--enable-3d-apis" in sys_argv or "--enable_3d_apis" in sys_argv: + enable_3d_apis = True + else: + enable_3d_apis = False + if swiftshader is None: + if "--swiftshader" in sys_argv: + swiftshader = True + else: + swiftshader = False + if locale is not None and locale_code is None: + locale_code = locale + if locale_code is None: + if '--locale="' in arg_join: + locale_code = ( + arg_join.split('--locale="')[1].split('"')[0] + ) + elif '--locale=' in arg_join: + locale_code = ( + arg_join.split('--locale=')[1].split(' ')[0] + ) + elif '--locale-code="' in arg_join: + locale_code = ( + arg_join.split('--locale-code="')[1].split('"')[0] + ) + elif '--locale-code=' in arg_join: + locale_code = ( + arg_join.split('--locale-code=')[1].split(' ')[0] + ) + if ad_block is not None and ad_block_on is None: + ad_block_on = ad_block + if ad_block_on is None: + if "--ad-block" in sys_argv or "--ad_block" in sys_argv: + ad_block_on = True + else: + ad_block_on = False + if host_resolver_rules is None: + if '--host-resolver-rules="' in arg_join: + host_resolver_rules = ( + arg_join.split('--host-resolver-rules="')[1].split('"')[0] + ) + elif '--host_resolver_rules="' in arg_join: + host_resolver_rules = ( + arg_join.split("--host_resolver_rules=")[1].split('"')[0] + ) + if driver_version is None and "--driver-version" in arg_join: + count = 0 + for arg in sys_argv: + if arg.startswith("--driver-version="): + driver_version = arg.split("--driver-version=")[1] + break + elif arg == "--driver-version" and len(sys_argv) > count + 1: + driver_version = sys_argv[count + 1] + if driver_version.startswith("-"): + driver_version = None + break + count += 1 + if driver_version is None and "--driver_version" in arg_join: + count = 0 + for arg in sys_argv: + if arg.startswith("--driver_version="): + driver_version = arg.split("--driver_version=")[1] + break + elif arg == "--driver_version" and len(sys_argv) > count + 1: + driver_version = sys_argv[count + 1] + if driver_version.startswith("-"): + driver_version = None + break + count += 1 + if highlights is not None: + try: + highlights = int(highlights) + except Exception: + raise Exception('"highlights" must be an integer!') + if interval is not None: + try: + interval = float(interval) + except Exception: + raise Exception('"interval" must be numeric!') + if time_limit is not None: + try: + time_limit = float(time_limit) + except Exception: + raise Exception('"time_limit" must be numeric!') + + sb_config.with_testing_base = with_testing_base + sb_config.browser = browser + if sb_config._browser_shortcut: + sb_config.browser = sb_config._browser_shortcut + if not hasattr(sb_config, "is_behave"): + sb_config.is_behave = False + if not hasattr(sb_config, "is_pytest"): + sb_config.is_pytest = False + if not hasattr(sb_config, "is_nosetest"): + sb_config.is_nosetest = False + sb_config.is_context_manager = True + sb_config.headless = headless + sb_config.headless1 = headless1 + sb_config.headless2 = headless2 + sb_config.headed = headed + sb_config.xvfb = xvfb + sb_config.xvfb_metrics = xvfb_metrics + sb_config.start_page = start_page + sb_config.locale_code = locale_code + sb_config.protocol = protocol + sb_config.servername = servername + sb_config.port = port + sb_config.data = data + sb_config.var1 = var1 + sb_config.var2 = var2 + sb_config.var3 = var3 + sb_config.variables = variables + sb_config.account = account + sb_config.environment = environment + sb_config.env = environment + sb_config.user_agent = user_agent + sb_config.incognito = incognito + sb_config.guest_mode = guest_mode + sb_config.dark_mode = dark_mode + sb_config.devtools = devtools + sb_config.mobile_emulator = is_mobile + sb_config.device_metrics = device_metrics + sb_config.extension_zip = extension_zip + sb_config.extension_dir = extension_dir + sb_config.database_env = "test" + sb_config.log_path = constants.Logs.LATEST + sb_config.archive_logs = archive_logs + sb_config.disable_csp = disable_csp + sb_config.disable_ws = disable_ws + sb_config.enable_ws = enable_ws + sb_config.enable_sync = enable_sync + sb_config.use_auto_ext = use_auto_ext + sb_config.undetectable = undetectable + sb_config.uc_cdp_events = uc_cdp_events + sb_config.uc_subprocess = uc_subprocess + sb_config.log_cdp_events = log_cdp_events + sb_config.no_sandbox = None + sb_config.disable_gpu = None + sb_config.disable_cookies = disable_cookies + sb_config.disable_js = disable_js + sb_config._multithreaded = False + sb_config.reuse_session = False + sb_config.crumbs = False + sb_config.final_debug = False + sb_config.visual_baseline = False + sb_config.window_position = window_position + sb_config.window_size = window_size + sb_config.maximize_option = maximize_option + sb_config._disable_beforeunload = _disable_beforeunload + sb_config.save_screenshot = save_screenshot + sb_config.no_screenshot = no_screenshot + sb_config.binary_location = binary_location + if hasattr(sb_config, "_cdp_bin_loc") and sb_config._cdp_bin_loc: + sb_config.binary_location = sb_config._cdp_bin_loc + sb_config.driver_version = driver_version + sb_config.page_load_strategy = page_load_strategy + sb_config.timeout_multiplier = timeout_multiplier + sb_config.pytest_html_report = None + sb_config.with_db_reporting = False + sb_config.with_s3_logging = False + sb_config.js_checking_on = js_checking_on + sb_config.recorder_mode = recorder_mode + sb_config.recorder_ext = recorder_ext + sb_config.record_sleep = record_sleep + sb_config.rec_sb_mgr = rec_sb_mgr + sb_config.rec_sb_cdp = rec_sb_cdp + sb_config.rec_behave = rec_behave + sb_config.rec_print = rec_print + sb_config.report_on = False + sb_config.slow_mode = slow_mode + sb_config.demo_mode = demo_mode + sb_config._time_limit = time_limit + sb_config.demo_sleep = demo_sleep + sb_config.dashboard = False + sb_config._dashboard_initialized = False + sb_config.message_duration = message_duration + sb_config.host_resolver_rules = host_resolver_rules + sb_config.block_images = block_images + sb_config.do_not_track = do_not_track + sb_config.external_pdf = external_pdf + sb_config.remote_debug = remote_debug + sb_config.settings_file = settings_file + sb_config.user_data_dir = user_data_dir + sb_config.chromium_arg = chromium_arg + sb_config.firefox_arg = firefox_arg + sb_config.firefox_pref = firefox_pref + sb_config.disable_features = disable_features + sb_config.proxy_string = proxy_string + sb_config.proxy_bypass_list = proxy_bypass_list + sb_config.proxy_pac_url = proxy_pac_url + sb_config.multi_proxy = multi_proxy + sb_config.enable_3d_apis = enable_3d_apis + sb_config.swiftshader = swiftshader + sb_config.ad_block_on = ad_block_on + sb_config.highlights = highlights + sb_config.interval = interval + sb_config.cap_file = cap_file + sb_config.cap_string = cap_string + if sb_config.browser in constants.ChromiumSubs.chromium_subs: + if not sb_config.binary_location: + sb_config.browser = "chrome" # Still uses chromedriver + sb_config._browser_shortcut = sb_config.browser + + sb = BaseCase() + sb.with_testing_base = sb_config.with_testing_base + sb.browser = sb_config.browser + sb.is_behave = False + sb.is_pytest = False + sb.is_nosetest = False + sb.is_context_manager = sb_config.is_context_manager + sb.headless = sb_config.headless + sb.headless1 = sb_config.headless1 + sb.headless2 = sb_config.headless2 + sb.headed = sb_config.headed + sb.xvfb = sb_config.xvfb + sb.xvfb_metrics = sb_config.xvfb_metrics + sb.start_page = sb_config.start_page + sb.locale_code = sb_config.locale_code + sb.protocol = sb_config.protocol + sb.servername = sb_config.servername + sb.port = sb_config.port + sb.data = sb_config.data + sb.var1 = sb_config.var1 + sb.var2 = sb_config.var2 + sb.var3 = sb_config.var3 + sb.variables = sb_config.variables + sb.account = sb_config.account + sb.environment = sb_config.environment + sb.env = sb_config.env + sb.user_agent = sb_config.user_agent + sb.incognito = sb_config.incognito + sb.guest_mode = sb_config.guest_mode + sb.dark_mode = sb_config.dark_mode + sb.devtools = sb_config.devtools + sb.binary_location = sb_config.binary_location + sb.driver_version = sb_config.driver_version + sb.mobile_emulator = sb_config.mobile_emulator + sb.device_metrics = sb_config.device_metrics + sb.extension_zip = sb_config.extension_zip + sb.extension_dir = sb_config.extension_dir + sb.database_env = sb_config.database_env + sb.log_path = sb_config.log_path + sb.archive_logs = sb_config.archive_logs + sb.disable_csp = sb_config.disable_csp + sb.disable_ws = sb_config.disable_ws + sb.enable_ws = sb_config.enable_ws + sb.enable_sync = sb_config.enable_sync + sb.use_auto_ext = sb_config.use_auto_ext + sb.undetectable = sb_config.undetectable + sb.uc_cdp_events = sb_config.uc_cdp_events + sb.uc_subprocess = sb_config.uc_subprocess + sb.log_cdp_events = sb_config.log_cdp_events + sb.no_sandbox = sb_config.no_sandbox + sb.disable_gpu = sb_config.disable_gpu + sb.disable_cookies = sb_config.disable_cookies + sb.disable_js = sb_config.disable_js + sb._multithreaded = sb_config._multithreaded + sb._reuse_session = sb_config.reuse_session + sb._crumbs = sb_config.crumbs + sb._final_debug = sb_config.final_debug + sb.visual_baseline = sb_config.visual_baseline + sb.window_position = sb_config.window_position + sb.window_size = sb_config.window_size + sb.maximize_option = sb_config.maximize_option + sb._disable_beforeunload = sb_config._disable_beforeunload + sb.save_screenshot_after_test = sb_config.save_screenshot + sb.no_screenshot_after_test = sb_config.no_screenshot + sb.page_load_strategy = sb_config.page_load_strategy + sb.timeout_multiplier = sb_config.timeout_multiplier + sb.pytest_html_report = sb_config.pytest_html_report + sb.with_db_reporting = sb_config.with_db_reporting + sb.with_s3_logging = sb_config.with_s3_logging + sb.js_checking_on = sb_config.js_checking_on + sb.recorder_mode = sb_config.recorder_mode + sb.recorder_ext = sb_config.recorder_ext + sb.record_sleep = sb_config.record_sleep + sb.rec_sb_mgr = sb_config.rec_sb_mgr + sb.rec_sb_cdp = sb_config.rec_sb_cdp + sb.rec_behave = sb_config.rec_behave + sb.rec_print = sb_config.rec_print + sb.report_on = sb_config.report_on + sb.slow_mode = sb_config.slow_mode + sb.demo_mode = sb_config.demo_mode + sb.time_limit = sb_config._time_limit + sb.demo_sleep = sb_config.demo_sleep + sb.dashboard = sb_config.dashboard + sb._dash_initialized = sb_config._dashboard_initialized + sb.message_duration = sb_config.message_duration + sb.host_resolver_rules = sb_config.host_resolver_rules + sb.block_images = sb_config.block_images + sb.do_not_track = sb_config.do_not_track + sb.external_pdf = sb_config.external_pdf + sb.remote_debug = sb_config.remote_debug + sb.settings_file = sb_config.settings_file + sb.user_data_dir = sb_config.user_data_dir + sb.chromium_arg = sb_config.chromium_arg + sb.firefox_arg = sb_config.firefox_arg + sb.firefox_pref = sb_config.firefox_pref + sb.disable_features = sb_config.disable_features + sb.proxy_string = sb_config.proxy_string + sb.proxy_bypass_list = sb_config.proxy_bypass_list + sb.proxy_pac_url = sb_config.proxy_pac_url + sb.multi_proxy = sb_config.multi_proxy + sb.enable_3d_apis = sb_config.enable_3d_apis + sb._swiftshader = sb_config.swiftshader + sb.ad_block_on = sb_config.ad_block_on + sb.highlights = sb_config.highlights + sb.interval = sb_config.interval + sb.cap_file = sb_config.cap_file + sb.cap_string = sb_config.cap_string + sb._has_failure = False # This may change + + with suppress(Exception): + stack_base = traceback.format_stack()[0].split("with SB(")[0] + stack_base = stack_base.split(os.sep)[-1] + test_base = stack_base.split(", in ")[0] + filename = test_base.split('"')[0] + methodname = ".line_" + test_base.split(", line ")[-1] + context_id = filename.split(".")[0] + methodname + sb._manager_saved_id = context_id + + if hasattr(sb_config, "headless_active"): + sb.headless_active = sb_config.headless_active + else: + sb.headless_active = False + test_name = None + terminal_width = shared_utils.get_terminal_width() + if test: + c1 = colorama.Fore.GREEN + b1 = colorama.Style.BRIGHT + cr = colorama.Style.RESET_ALL + stack_base = traceback.format_stack()[0].split("with SB(")[0] + stack_base = stack_base.split(os.sep)[-1] + test_name = stack_base.split(", in ")[0].replace('", line ', ":") + test_name += ":SB" + start_text = "=== {%s} starts ===" % test_name + remaining_spaces = terminal_width - len(start_text) + left_space = "" + right_space = "" + if remaining_spaces > 0: + left_spaces = int(remaining_spaces / 2) + left_space = left_spaces * "=" + right_spaces = remaining_spaces - left_spaces + right_space = right_spaces * "=" + if not test_name.startswith("runpy.py:"): + print("%s%s%s%s%s" % (b1, left_space, start_text, right_space, cr)) + if do_log_folder_setup: + from seleniumbase.core import log_helper + from seleniumbase.core import download_helper + from seleniumbase.core import proxy_helper + + log_helper.log_folder_setup(constants.Logs.LATEST + os.sep) + log_helper.clear_empty_logs() + download_helper.reset_downloads_folder() + if not sb_config.multi_proxy: + proxy_helper.remove_proxy_zip_if_present() + start_time = time.time() + sb.setUp() + test_passed = True # This can change later + teardown_exception = None + if "--trace" in sys_argv: + breakpoint() # Debug Mode + # Type "s" and press [Enter] to step into "yield sb". + try: + yield sb + except Exception as e: + sb._has_failure = True + exception = e + test_passed = False + if (test or inner_test) and not test_name: + print(e) + return + elif not test_name: + raise + else: + the_traceback = traceback.format_exc().strip() + try: + p2 = the_traceback.split(', in ')[1].split('", line ')[0] + filename = p2.split(os.sep)[-1] + sb.cm_filename = filename + except Exception: + sb.cm_filename = None + # Tests will raise an exception if raise_test_failure is True + finally: + if sb._has_failure and "--pdb" in sys_argv: + sb_config._do_sb_post_mortem = True + elif ( + "--final-debug" in sys_argv + or "--final-trace" in sys_argv + or "--fdebug" in sys_argv + or "--ftrace" in sys_argv + ): + sb_config._do_sb_final_trace = True + try: + sb.tearDown() + except Exception as t_e: + teardown_exception = t_e + print(traceback.format_exc().strip()) + if test and not test_passed: + print("********** ERROR: The test AND the tearDown() FAILED!") + if ( + hasattr(sb_config, "_virtual_display") + and sb_config._virtual_display + and hasattr(sb_config._virtual_display, "stop") + ): + try: + sb_config._virtual_display.stop() + sb_config._virtual_display = None + sb_config.headless_active = False + except AttributeError: + pass + except Exception: + pass + end_time = time.time() + run_time = end_time - start_time + sb_config = sb_config_backup + if test: + sb_config._has_older_context = True + if test_name: + result = "passed" + if test and not test_passed: + result = "failed" + c1 = colorama.Fore.RED + end_text = ( + "=== {%s} %s in %.2fs ===" + % (test_name, result, run_time) + ) + remaining_spaces = terminal_width - len(end_text) + end_text = ( + "=== %s%s{%s} %s%s%s in %.2fs ===" + % (b1, c1, test_name, result, cr, c1, run_time) + ) + left_space = "" + right_space = "" + if remaining_spaces > 0: + left_spaces = int(remaining_spaces / 2) + left_space = left_spaces * "=" + right_spaces = remaining_spaces - left_spaces + right_space = right_spaces * "=" + if test and not test_passed: + print(the_traceback) + if not test_name.startswith("runpy.py:"): + print( + "%s%s%s%s%s" + % (c1, left_space, end_text, right_space, cr) + ) + if undetectable and hasattr(sb, "_drivers_browser_map"): + import asyncio + for driver in sb._drivers_browser_map.keys(): + if ( + hasattr(driver, "cdp") + and driver.cdp + and hasattr(driver.cdp, "loop") + ): + asyncio.set_event_loop(driver.cdp.loop) + tasks = [] + for tab in driver.cdp.get_tabs(): + tasks.append(tab.aclose()) + tasks.append(driver.cdp.driver.connection.aclose()) + driver.cdp.loop.run_until_complete(asyncio.gather(*tasks)) + driver.cdp.loop.close() + gc.collect() + if test and test_name and not test_passed and raise_test_failure: + raise exception + elif ( + teardown_exception + and ( + not test + or (test_passed and raise_test_failure) + ) + ): + raise teardown_exception diff --git a/seleniumbase/plugins/screen_shots.py b/seleniumbase/plugins/screen_shots.py old mode 100755 new mode 100644 index ef8152679dd..8768fd57333 --- a/seleniumbase/plugins/screen_shots.py +++ b/seleniumbase/plugins/screen_shots.py @@ -1,27 +1,19 @@ -""" -Contains the screenshot plugin for the selenium tests. -""" - +"""Screenshot Plugin for SeleniumBase tests that run with pynose / nosetests""" import os from nose.plugins import Plugin from seleniumbase.config import settings class ScreenShots(Plugin): - """ - This plugin will take a screenshot when either a test fails - or raises an error. It will store that screenshot either in - the default logs file or in another file of the user's specification. - """ - + """This plugin takes a screenshot when a test fails.""" name = "screen_shots" logfile_name = settings.SCREENSHOT_NAME def options(self, parser, env): - super(ScreenShots, self).options(parser, env=env) + super().options(parser, env=env) def configure(self, options, conf): - super(ScreenShots, self).configure(options, conf) + super().configure(options, conf) if not self.enabled: return self.options = options diff --git a/seleniumbase/plugins/selenium_plugin.py b/seleniumbase/plugins/selenium_plugin.py old mode 100755 new mode 100644 index 743c62756c0..1aae789d78d --- a/seleniumbase/plugins/selenium_plugin.py +++ b/seleniumbase/plugins/selenium_plugin.py @@ -1,216 +1,1537 @@ -""" -This plugin gives the power of Selenium to nosetests -by providing a WebDriver object for the tests to use. -""" - +"""Selenium Plugin for SeleniumBase tests that run with pynose / nosetests""" +import os +import sys +from contextlib import suppress from nose.plugins import Plugin -from pyvirtualdisplay import Display +from seleniumbase import config as sb_config +from seleniumbase.config import settings +from seleniumbase.core import detect_b_ver from seleniumbase.core import proxy_helper from seleniumbase.fixtures import constants +from seleniumbase.fixtures import shared_utils class SeleniumBrowser(Plugin): + """This plugin adds the following command-line options to pynose: + --browser=BROWSER (The web browser to use. Default: "chrome".) + --chrome (Shortcut for "--browser=chrome". Default.) + --edge (Shortcut for "--browser=edge".) + --firefox (Shortcut for "--browser=firefox".) + --safari (Shortcut for "--browser=safari".) + --opera (Shortcut for "--browser=opera".) + --brave (Shortcut for "--browser=brave".) + --comet (Shortcut for "--browser=comet".) + --chromium (Shortcut for using base `Chromium`) + --cft (Shortcut for using `Chrome for Testing`) + --chs (Shortcut for using `Chrome-Headless-Shell`) + --user-data-dir=DIR (Set the Chrome user data directory to use.) + --protocol=PROTOCOL (The Selenium Grid protocol: http|https.) + --server=SERVER (The Selenium Grid server/IP used for tests.) + --port=PORT (The Selenium Grid port used by the test server.) + --cap-file=FILE (The web browser's desired capabilities to use.) + --cap-string=STRING (The web browser's desired capabilities to use.) + --proxy=SERVER:PORT (Connect to a proxy server:port as tests are running) + --proxy=USERNAME:PASSWORD@SERVER:PORT (Use an authenticated proxy server) + --proxy-bypass-list=STRING (";"-separated hosts to bypass, Eg "*.foo.com") + --proxy-pac-url=URL (Connect to a proxy server using a PAC_URL.pac file.) + --proxy-pac-url=USERNAME:PASSWORD@URL (Authenticated proxy with PAC URL.) + --proxy-driver (If a driver download is needed, will use: --proxy=PROXY.) + --multi-proxy (Allow multiple authenticated proxies when multi-threaded.) + --agent=STRING (Modify the web browser's User-Agent string.) + --mobile (Use the mobile device emulator while running tests.) + --metrics=STRING (Set mobile metrics: "CSSWidth,CSSHeight,PixelRatio".) + --chromium-arg="ARG=N,ARG2" (Set Chromium args, ","-separated, no spaces.) + --firefox-arg="ARG=N,ARG2" (Set Firefox args, comma-separated, no spaces.) + --firefox-pref=SET (Set a Firefox preference:value set, comma-separated.) + --extension-zip=ZIP (Load a Chrome Extension .zip|.crx, comma-separated.) + --extension-dir=DIR (Load a Chrome Extension directory, comma-separated.) + --disable-features="F1,F2" (Disable features, comma-separated, no spaces.) + --binary-location=PATH (Set path of the Chromium browser binary to use.) + --driver-version=VER (Set the chromedriver or uc_driver version to use.) + --sjw (Skip JS Waits for readyState to be "complete" or Angular to load.) + --wfa (Wait for AngularJS to be done loading after specific web actions.) + --pls=PLS (Set pageLoadStrategy on Chrome: "normal", "eager", or "none".) + --headless (The default headless mode. Linux uses this mode by default.) + --headless1 (Use Chrome's old headless mode. Fast, but has limitations.) + --headless2 (Use Chrome's new headless mode, which supports extensions.) + --headed (Run tests in headed/GUI mode on Linux OS, where not default.) + --xvfb (Run tests using the Xvfb virtual display server on Linux OS.) + --xvfb-metrics=STRING (Set Xvfb display size on Linux: "Width,Height".) + --locale=LOCALE_CODE (Set the Language Locale Code for the web browser.) + --interval=SECONDS (The autoplay interval for presentations & tour steps) + --start-page=URL (The starting URL for the web browser when tests begin.) + --time-limit=SECONDS (Safely fail any test that exceeds the time limit.) + --slow (Slow down the automation. Faster than using Demo Mode.) + --demo (Slow down and visually see test actions as they occur.) + --demo-sleep=SECONDS (Set the wait time after Slow & Demo Mode actions.) + --highlights=NUM (Number of highlight animations for Demo Mode actions.) + --message-duration=SECONDS (The time length for Messenger alerts.) + --check-js (Check for JavaScript errors after page loads.) + --ad-block (Block some types of display ads from loading.) + --host-resolver-rules=RULES (Set host-resolver-rules, comma-separated.) + --block-images (Block images from loading during tests.) + --do-not-track (Indicate to websites that you don't want to be tracked.) + --verify-delay=SECONDS (The delay before MasterQA verification checks.) + --ee / --esc-end (Lets the user end the current test via the ESC key.) + --recorder (Enables the Recorder for turning browser actions into code.) + --rec-sb-mgr (A Recorder Mode that generates SB() context manager code.) + --rec-sb-cdp (A Recorder Mode that generates Pure CDP Mode sb_cdp code.) + --rec-behave (Same as Recorder Mode, but also generates behave-gherkin.) + --rec-sleep (If the Recorder is enabled, also records self.sleep calls.) + --rec-print (If the Recorder is enabled, prints output after tests end.) + --disable-cookies (Disable Cookies on websites. Pages might break!) + --disable-js (Disable JavaScript on websites. Pages might break!) + --disable-csp (Disable the Content Security Policy of websites.) + --disable-ws (Disable Web Security on Chromium-based browsers.) + --enable-ws (Enable Web Security on Chromium-based browsers.) + --enable-sync (Enable "Chrome Sync" on websites.) + --uc | --undetected (Use undetected-chromedriver to evade bot-detection.) + --uc-cdp-events (Capture CDP events when running in "--undetected" mode.) + --log-cdp ("goog:loggingPrefs", {"performance": "ALL", "browser": "ALL"}) + --remote-debug (Sync to Chrome Remote Debugger chrome://inspect/#devices) + --enable-3d-apis (Enables WebGL and 3D APIs.) + --swiftshader (Chrome "--use-gl=angle" / "--use-angle=swiftshader-webgl") + --incognito (Enable Chrome's Incognito mode.) + --guest (Enable Chrome's Guest mode.) + --dark (Enable Chrome's Dark mode.) + --devtools (Open Chrome's DevTools when the browser opens.) + --disable-beforeunload (Disable the "beforeunload" event on Chrome.) + --window-position=X,Y (Set the browser's starting window position.) + --window-size=WIDTH,HEIGHT (Set the browser's starting window size.) + --maximize (Start tests with the browser window maximized.) + --screenshot (Save a screenshot at the end of each test.) + --visual-baseline (Set the visual baseline for Visual/Layout tests.) + --external-pdf (Set Chromium "plugins.always_open_pdf_externally": True.) + --timeout-multiplier=MULTIPLIER (Multiplies the default timeout values.) """ - The plugin for Selenium tests. Takes in key arguments and then - creates a WebDriver object. All arguments are passed to the tests. - - The following command line options are available to the tests: - self.options.browser -- the browser to use (--browser) - self.options.server -- the server used by the test (--server) - self.options.port -- the port used by the test (--port) - self.options.proxy -- designates the proxy server:port to use. (--proxy) - self.options.agent -- designates the User Agent for the browser. (--agent) - self.options.headless -- the option to run headlessly (--headless) - self.options.demo_mode -- the option to slow down Selenium (--demo_mode) - self.options.demo_sleep -- Selenium action delay in DemoMode (--demo_sleep) - self.options.highlights -- # of highlight animations shown (--highlights) - self.options.message_duration -- Messenger alert time (--message_duration) - self.options.js_checking_on -- option to check for js errors (--check_js) - self.options.ad_block -- the option to block some display ads (--ad_block) - self.options.verify_delay -- delay before MasterQA checks (--verify_delay) - self.options.disable_csp -- disable Content Security Policy (--disable_csp) - self.options.save_screenshot -- save screen after test (--save_screenshot) - self.options.visual_baseline -- set the visual baseline (--visual_baseline) - self.options.timeout_multiplier -- increase defaults (--timeout_multiplier) - """ - name = 'selenium' # Usage: --with-selenium + name = "selenium" # Usage: --with-selenium def options(self, parser, env): - super(SeleniumBrowser, self).options(parser, env=env) - - parser.add_option( - '--browser', action='store', - dest='browser', + super().options(parser, env=env) + parser.addoption = parser.add_option # Reuse name from pytest parser + parser.addoption( + "--browser", + action="store", + dest="browser", choices=constants.ValidBrowsers.valid_browsers, default=constants.Browser.GOOGLE_CHROME, help="""Specifies the web browser to use. Default: Chrome. - If you want to use Firefox, explicitly indicate that. - Example: (--browser=firefox)""") - parser.add_option( - '--browser_version', action='store', - dest='browser_version', - default="latest", - help="""The browser version to use. Explicitly select - a version number or use "latest".""") - parser.add_option( - '--cap_file', action='store', - dest='cap_file', + Examples: (--browser=edge OR --browser=firefox)""", + ) + parser.addoption( + "--chrome", + action="store_true", + dest="use_chrome", + default=False, + help="""Shortcut for --browser=chrome (Default)""", + ) + parser.addoption( + "--edge", + action="store_true", + dest="use_edge", + default=False, + help="""Shortcut for --browser=edge""", + ) + parser.addoption( + "--firefox", + action="store_true", + dest="use_firefox", + default=False, + help="""Shortcut for --browser=firefox""", + ) + parser.addoption( + "--ie", + action="store_true", + dest="use_ie", + default=False, + help="""Shortcut for --browser=ie""", + ) + parser.addoption( + "--safari", + action="store_true", + dest="use_safari", + default=False, + help="""Shortcut for --browser=safari""", + ) + parser.addoption( + "--opera", + action="store_true", + dest="use_opera", + default=False, + help="""Shortcut for --browser=opera""", + ) + parser.addoption( + "--brave", + action="store_true", + dest="use_brave", + default=False, + help="""Shortcut for --browser=brave""", + ) + parser.addoption( + "--comet", + action="store_true", + dest="use_comet", + default=False, + help="""Shortcut for --browser=comet""", + ) + parser.addoption( + "--use-chromium", + "--chromium", + action="store_true", + dest="use_chromium", + default=False, + help="""Shortcut for using base `Chromium`""", + ) + parser.addoption( + "--cft", + action="store_true", + dest="use_cft", + default=False, + help="""Shortcut for using `Chrome for Testing`""", + ) + parser.addoption( + "--chs", + action="store_true", + dest="use_chs", + default=False, + help="""Shortcut for using `Chrome-Headless-Shell`""", + ) + parser.addoption( + "--cap_file", + "--cap-file", + action="store", + dest="cap_file", default=None, help="""The file that stores browser desired capabilities - for BrowserStack or Sauce Labs web drivers.""") - parser.add_option( - '--server', action='store', dest='servername', - default='localhost', + for BrowserStack, Sauce Labs, or other grids.""", + ) + parser.addoption( + "--cap_string", + "--cap-string", + dest="cap_string", + default=None, + help="""The string that stores browser desired capabilities + for BrowserStack, Sauce Labs, or other grids. + Enclose cap-string in single quotes. + Enclose parameter keys in double quotes. + Example: --cap-string='{"name":"test1","v":"42"}'""", + ) + parser.addoption( + "--user_data_dir", + "--user-data-dir", + action="store", + dest="user_data_dir", + default=None, + help="""The Chrome User Data Directory to use. (Chrome Profile) + If the directory doesn't exist, it'll be created.""", + ) + parser.addoption( + "--sjw", + "--skip_js_waits", + "--skip-js-waits", + action="store_true", + dest="skip_js_waits", + default=False, + help="""Skip all calls to wait_for_ready_state_complete() + and wait_for_angularjs(), which are part of many + SeleniumBase methods for improving reliability.""", + ) + parser.addoption( + "--wfa", + "--wait_for_angularjs", + "--wait-for-angularjs", + action="store_true", + dest="wait_for_angularjs", + default=False, + help="""Add waiting for AngularJS. (The default setting + was changed to no longer wait for AngularJS to + finish loading as an extra JavaScript call.)""", + ) + parser.addoption( + "--protocol", + action="store", + dest="protocol", + choices=( + constants.Protocol.HTTP, + constants.Protocol.HTTPS, + ), + default=constants.Protocol.HTTP, + help="""Designates the Selenium Grid protocol to use. + Default: http.""", + ) + parser.addoption( + "--server", + action="store", + dest="servername", + default="localhost", help="""Designates the Selenium Grid server to use. - Default: localhost.""") - parser.add_option( - '--port', action='store', dest='port', - default='4444', + Use "127.0.0.1" to connect to a localhost Grid. + If unset or set to "localhost", Grid isn't used. + Default: "localhost".""", + ) + parser.addoption( + "--port", + action="store", + dest="port", + default="4444", help="""Designates the Selenium Grid port to use. - Default: 4444.""") - parser.add_option( - '--proxy', action='store', - dest='proxy_string', + Default: 4444. (If 443, protocol becomes "https")""", + ) + parser.addoption( + "--proxy", + "--proxy-server", + "--proxy-string", + action="store", + dest="proxy_string", default=None, help="""Designates the proxy server:port to use. Format: servername:port. OR username:password@servername:port OR A dict key from proxy_list.PROXY_LIST - Default: None.""") - parser.add_option( - '--agent', action='store', - dest='user_agent', + Default: None.""", + ) + parser.addoption( + "--proxy-bypass-list", + "--proxy_bypass_list", + action="store", + dest="proxy_bypass_list", + default=None, + help="""Designates the hosts, domains, and/or IP addresses + to bypass when using a proxy server with "--proxy". + Format: A ";"-separated string. + Example usage: + pytest + --proxy="username:password@servername:port" + --proxy-bypass-list="*.foo.com;github.com" + pytest + --proxy="servername:port" + --proxy-bypass-list="127.0.0.1:8080" + Default: None.""", + ) + parser.addoption( + "--proxy-pac-url", + "--pac-url", + action="store", + dest="proxy_pac_url", + default=None, + help="""Designates the proxy PAC URL to use. + Format: A URL string OR + A username:password@URL string + Default: None.""", + ) + parser.addoption( + "--proxy-driver", + "--proxy_driver", + action="store_true", + dest="proxy_driver", + default=False, + help="""If a driver download is needed for tests, + uses proxy settings set via --proxy=PROXY.""", + ) + parser.addoption( + "--multi-proxy", + "--multi_proxy", + action="store_true", + dest="multi_proxy", + default=False, + help="""If you need to run multi-threaded tests with + multiple proxies that require authentication, + set this to allow multiple configurations.""", + ) + parser.addoption( + "--agent", + "--user-agent", + "--user_agent", + action="store", + dest="user_agent", default=None, help="""Designates the User-Agent for the browser to use. Format: A string. - Default: None.""") - parser.add_option( - '--headless', action="store_true", - dest='headless', - default=False, - help="""Using this makes Webdriver run headlessly, - which is required on headless machines.""") - parser.add_option( - '--demo_mode', action="store_true", - dest='demo_mode', - default=False, - help="""Using this slows down the automation so that - you can see what it's actually doing.""") - parser.add_option( - '--demo_sleep', action='store', dest='demo_sleep', + Default: None.""", + ) + parser.addoption( + "--mobile", + "--mobile-emulator", + "--mobile_emulator", + action="store_true", + dest="mobile_emulator", + default=False, + help="""If this option is enabled, the mobile emulator + will be used while running tests.""", + ) + parser.addoption( + "--metrics", + "--device-metrics", + "--device_metrics", + action="store", + dest="device_metrics", + default=None, + help="""Designates the three device metrics of the mobile + emulator: CSS Width, CSS Height, and Pixel-Ratio. + Format: A comma-separated string with the 3 values. + Examples: "375,734,5" or "412,732,3" or "390,715,3" + Default: None. (Will use default values if None)""", + ) + parser.addoption( + "--chromium_arg", + "--chromium-arg", + action="store", + dest="chromium_arg", + default=None, + help="""Add a Chromium argument for Chrome/Edge browsers. + Format: A comma-separated list of Chromium args. + If an arg doesn't start with "--", that will be + added to the beginning of the arg automatically. + Default: None.""", + ) + parser.addoption( + "--firefox_arg", + "--firefox-arg", + action="store", + dest="firefox_arg", + default=None, + help="""Add a Firefox argument for Firefox browser runs. + Format: A comma-separated list of Firefox args. + If an arg doesn't start with "--", that will be + added to the beginning of the arg automatically. + Default: None.""", + ) + parser.addoption( + "--firefox_pref", + "--firefox-pref", + action="store", + dest="firefox_pref", + default=None, + help="""Set a Firefox preference:value combination. + Format: A comma-separated list of pref:value items. + Example usage: + --firefox-pref="browser.formfill.enable:True" + --firefox-pref="pdfjs.disabled:False" + --firefox-pref="abc.def.xyz:42,hello.world:text" + Boolean and integer values to the right of the ":" + will be automatically converted into proper format. + If there's no ":" in the string, then True is used. + Default: None.""", + ) + parser.addoption( + "--extension_zip", + "--extension-zip", + "--crx", + action="store", + dest="extension_zip", + default=None, + help="""Designates the Chrome Extension ZIP file to load. + Format: A comma-separated list of .zip or .crx files + containing the Chrome extensions to load. + Default: None.""", + ) + parser.addoption( + "--extension_dir", + "--extension-dir", + action="store", + dest="extension_dir", + default=None, + help="""Designates the Chrome Extension folder to load. + Format: A directory containing the Chrome extension. + (Can also be a comma-separated list of directories.) + Default: None.""", + ) + parser.addoption( + "--disable_features", + "--disable-features", + action="store", + dest="disable_features", + default=None, + help="""Disable Chromium features from Chrome/Edge browsers. + Format: A comma-separated list of Chromium features. + Default: None.""", + ) + parser.addoption( + "--binary_location", + "--binary-location", + "--bl", + action="store", + dest="binary_location", + default=None, + help="""Sets the path of the Chromium browser binary to use. + Uses the default location if not os.path.exists(PATH)""", + ) + parser.addoption( + "--driver_version", + "--driver-version", + action="store", + dest="driver_version", + default=None, + help="""Setting this overrides the default driver version, + which is set to match the detected browser version. + Major version only. Example: "--driver-version=114" + (Only chromedriver and uc_driver are affected.)""", + ) + parser.addoption( + "--pls", + "--page_load_strategy", + "--page-load-strategy", + action="store", + dest="page_load_strategy", + choices=( + constants.PageLoadStrategy.NORMAL, + constants.PageLoadStrategy.EAGER, + constants.PageLoadStrategy.NONE, + ), + default=None, + help="""This option sets Chrome's pageLoadStrategy. + List of choices: "normal", "eager", "none".""", + ) + parser.addoption( + "--headless", + action="store_true", + dest="headless", + default=False, + help="""Using this option activates headless mode, + which is required on headless machines + UNLESS using a virtual display with Xvfb. + Default: False on Mac/Windows. True on Linux.""", + ) + parser.addoption( + "--headless1", + action="store_true", + dest="headless1", + default=False, + help="""This option activates the old headless mode, + which is faster, but has limitations. + (May be phased out by Chrome in the future.)""", + ) + parser.addoption( + "--headless2", + action="store_true", + dest="headless2", + default=False, + help="""This option activates the new headless mode, + which supports Chromium extensions, and more, + but is slower than the standard headless mode.""", + ) + parser.addoption( + "--headed", + "--gui", + action="store_true", + dest="headed", + default=False, + help="""Using this makes Webdriver run web browsers with + a GUI when running tests on Linux machines. + (The default setting on Linux is headless.) + (The default setting on Mac or Windows is headed.)""", + ) + parser.addoption( + "--xvfb", + action="store_true", + dest="xvfb", + default=False, + help="""Using this makes tests run headlessly using Xvfb + instead of the browser's built-in headless mode. + When using "--xvfb", the "--headless" option + will no longer be enabled by default on Linux. + Default: False. (Linux-ONLY!)""", + ) + parser.addoption( + "--xvfb-metrics", + "--xvfb_metrics", + action="store", + dest="xvfb_metrics", + default=None, + help="""Customize the Xvfb metrics (Width,Height) on Linux. + Format: A comma-separated string with the 2 values. + Examples: "1920,1080" or "1366,768" or "1024,768". + Default: None. (None: "1366,768". Min: "1024,768".)""", + ) + parser.addoption( + "--locale_code", + "--locale-code", + "--locale", + action="store", + dest="locale_code", + default=None, + help="""Designates the Locale Code for the web browser. + A Locale is a specific version of a spoken Language. + The Locale alters visible text on supported websites. + See: https://seleniumbase.io/help_docs/locale_codes/ + Default: None. (The web browser's default mode.)""", + ) + parser.addoption( + "--interval", + action="store", + dest="interval", + default=None, + help="""This globally overrides the default interval, + (in seconds), of features that include autoplay + functionality, such as tours and presentations. + Overrides from methods take priority over this. + (Headless Mode skips tours and presentations.)""", + ) + parser.addoption( + "--start_page", + "--start-page", + "--url", + action="store", + dest="start_page", + default=None, + help="""Designates the starting URL for the web browser + when each test begins. + Default: None.""", + ) + parser.addoption( + "--time_limit", + "--time-limit", + "--timelimit", + action="store", + dest="time_limit", + default=None, + help="""Use this to set a time limit per test, in seconds. + If a test runs beyond the limit, it fails.""", + ) + parser.addoption( + "--slow_mode", + "--slow-mode", + "--slowmo", + "--slow", + action="store_true", + dest="slow_mode", + default=False, + help="""Using this slows down the automation.""", + ) + parser.addoption( + "--demo_mode", + "--demo-mode", + "--demo", + action="store_true", + dest="demo_mode", + default=False, + help="""Using this slows down the automation and lets you + visually see what the tests are actually doing.""", + ) + parser.addoption( + "--demo_sleep", + "--demo-sleep", + action="store", + dest="demo_sleep", default=None, help="""Setting this overrides the Demo Mode sleep - time that happens after browser actions.""") - parser.add_option( - '--highlights', action='store', - dest='highlights', default=None, + time that happens after browser actions.""", + ) + parser.addoption( + "--highlights", + action="store", + dest="highlights", + default=None, help="""Setting this overrides the default number of - highlight animation loops to have per call.""") - parser.add_option( - '--message_duration', action="store", - dest='message_duration', default=None, + highlight animation loops to have per call.""", + ) + parser.addoption( + "--message_duration", + "--message-duration", + action="store", + dest="message_duration", + default=None, help="""Setting this overrides the default time that - messenger notifications remain visible when reaching - assert statements during Demo Mode.""") - parser.add_option( - '--check_js', action="store_true", - dest='js_checking_on', + messenger notifications remain visible when + reaching assert statements during Demo Mode.""", + ) + parser.addoption( + "--check_js", + "--check-js", + action="store_true", + dest="js_checking_on", default=False, help="""The option to check for JavaScript errors after - every page load.""") - parser.add_option( - '--ad_block', action="store_true", - dest='ad_block_on', + every page load.""", + ) + parser.addoption( + "--adblock", + "--ad_block", + "--ad-block", + "--block_ads", + "--block-ads", + action="store_true", + dest="ad_block_on", default=False, help="""Using this makes WebDriver block display ads - that are defined in ad_block_list.AD_BLOCK_LIST.""") - parser.add_option( - '--verify_delay', action='store', - dest='verify_delay', default=None, + that are defined in ad_block_list.AD_BLOCK_LIST.""", + ) + parser.addoption( + "--host_resolver_rules", + "--host-resolver-rules", + action="store", + dest="host_resolver_rules", + default=None, + help="""Use this option to set "host-resolver-rules". + This lets you re-map traffic from any domain. + Eg. "MAP www.google-analytics.com 0.0.0.0". + Eg. "MAP * ~NOTFOUND , EXCLUDE myproxy". + Eg. "MAP * 0.0.0.0 , EXCLUDE 127.0.0.1". + Eg. "MAP *.google.com myproxy". + Find more examples on these pages: + (https://www.electronjs.org/docs/ + latest/api/command-line-switches) + (https://www.chromium.org/developers/ + design-documents/network-stack/socks-proxy/) + Use comma-separation for multiple host rules.""", + ) + parser.addoption( + "--block_images", + "--block-images", + action="store_true", + dest="block_images", + default=False, + help="""Using this makes WebDriver block images from + loading on web pages during tests.""", + ) + parser.addoption( + "--do_not_track", + "--do-not-track", + action="store_true", + dest="do_not_track", + default=False, + help="""Indicate to websites that you don't want to be + tracked. The browser will send an extra HTTP + header each time it requests a web page. + https://support.google.com/chrome/answer/2790761""", + ) + parser.addoption( + "--verify_delay", + "--verify-delay", + action="store", + dest="verify_delay", + default=None, help="""Setting this overrides the default wait time - before each MasterQA verification pop-up.""") - parser.add_option( - '--disable_csp', action="store_true", - dest='disable_csp', + before each MasterQA verification pop-up.""", + ) + parser.addoption( + "--esc-end", + "--esc_end", + "--ee", + action="store_true", + dest="esc_end", + default=False, + help="""End the current test early via the ESC key. + The test will be marked as skipped.""", + ) + parser.addoption( + "--recorder", + "--record", + "--rec", + "--codegen", + action="store_true", + dest="recorder_mode", + default=False, + help="""Using this enables the SeleniumBase Recorder, + which records browser actions for converting + into SeleniumBase scripts.""", + ) + parser.addoption( + "--rec-sb-mgr", + action="store_true", + dest="rec_sb_mgr", + default=False, + help="""Not only enables the SeleniumBase Recorder, + but also saves recorded actions into the + context manager format.""", + ) + parser.addoption( + "--rec-sb-cdp", + action="store_true", + dest="rec_sb_cdp", + default=False, + help="""Not only enables the SeleniumBase Recorder, + but also saves recorded actions into the + Pure CDP Mode sb_cdp sync format.""", + ) + parser.addoption( + "--rec-behave", + "--rec-gherkin", + action="store_true", + dest="rec_behave", + default=False, + help="""Not only enables the SeleniumBase Recorder, + but also saves recorded actions into the + behave-gerkin format, which includes a + feature file, an imported steps file, + and the environment.py file.""", + ) + parser.addoption( + "--rec-sleep", + "--record-sleep", + action="store_true", + dest="record_sleep", + default=False, + help="""If Recorder Mode is enabled, + records sleep(seconds) calls.""", + ) + parser.addoption( + "--rec-print", + action="store_true", + dest="rec_print", + default=False, + help="""If Recorder Mode is enabled, + prints output after tests end.""", + ) + parser.addoption( + "--disable_js", + "--disable-js", + action="store_true", + dest="disable_js", + default=False, + help="""The option to disable JavaScript on web pages. + Warning: Most web pages will stop working!""", + ) + parser.addoption( + "--disable_cookies", + "--disable-cookies", + action="store_true", + dest="disable_cookies", + default=False, + help="""The option to disable Cookies on web pages. + Warning: Several pages may stop working!""", + ) + parser.addoption( + "--disable_csp", + "--disable-csp", + "--no_csp", + "--no-csp", + "--dcsp", + action="store_true", + dest="disable_csp", default=False, help="""Using this disables the Content Security Policy of websites, which may interfere with some features of SeleniumBase, such as loading custom JavaScript libraries for various testing actions. - Setting this to True (--disable_csp) overrides the - value set in seleniumbase/config/settings.py""") - parser.add_option( - '--save_screenshot', action="store_true", - dest='save_screenshot', - default=False, - help="""Take a screenshot on last page after the last step - of the test. (Added to the "latest_logs" folder.)""") - parser.add_option( - '--visual_baseline', action='store_true', - dest='visual_baseline', + Setting this to True (--disable-csp) overrides the + value set in seleniumbase/config/settings.py""", + ) + parser.addoption( + "--disable_ws", + "--disable-ws", + "--dws", + "--disable-web-security", + action="store_true", + dest="disable_ws", + default=False, + help="""Using this disables the "Web Security" feature of + Chrome and Chromium-based browsers such as Edge.""", + ) + parser.addoption( + "--enable_ws", + "--enable-ws", + "--enable-web-security", + action="store_true", + dest="enable_ws", + default=False, + help="""Using this enables the "Web Security" feature of + Chrome and Chromium-based browsers such as Edge.""", + ) + parser.addoption( + "--enable_sync", + "--enable-sync", + action="store_true", + dest="enable_sync", + default=False, + help="""Using this enables the "Chrome Sync" feature.""", + ) + parser.addoption( + "--use_auto_ext", + "--use-auto-ext", + "--auto-ext", + action="store_true", + dest="use_auto_ext", + default=False, + help="""(DEPRECATED) - Enable the automation extension. + It's not required, but some commands & advanced + features may need it.""", + ) + parser.addoption( + "--undetected", + "--undetectable", + "--uc", # undetected-chromedriver + action="store_true", + dest="undetectable", + default=False, + help="""Using this option makes chromedriver undetectable + to websites that use anti-bot services to block + automation tools from navigating them freely.""", + ) + parser.addoption( + "--uc_cdp_events", + "--uc-cdp-events", + "--uc-cdp", # For capturing CDP events during UC Mode + action="store_true", + dest="uc_cdp_events", + default=None, + help="""Captures CDP events during Undetectable Mode runs. + Then you can add a listener to perform actions on + received data, such as printing it to the console: + from pprint import pformat + self.driver.add_cdp_listener( + "*", lambda data: print(pformat(data)) + ) + self.goto(url)""", + ) + parser.addoption( + "--uc_subprocess", + "--uc-subprocess", + "--uc-sub", # undetected-chromedriver subprocess mode + action="store_true", + dest="uc_subprocess", + default=None, + help="""(DEPRECATED) - (UC Mode always uses this now.) + Use undetectable-chromedriver as a subprocess, + which can help avoid issues that might result.""", + ) + parser.addoption( + "--no_sandbox", + "--no-sandbox", + action="store_true", + dest="no_sandbox", + default=False, + help="""(DEPRECATED) - "--no-sandbox" is always used now. + Using this enables the "No Sandbox" feature. + (This setting is now always enabled by default.)""", + ) + parser.addoption( + "--disable_gpu", + "--disable-gpu", + action="store_true", + dest="disable_gpu", + default=False, + help="""(DEPRECATED) - GPU is disabled if no swiftshader. + Using this enables the "Disable GPU" feature. + (GPU is disabled by default if swiftshader off.)""", + ) + parser.addoption( + "--log_cdp", + "--log-cdp", + "--log_cdp_events", + "--log-cdp-events", + action="store_true", + dest="log_cdp_events", + default=None, + help="""Capture CDP events. Then you can print them. + Eg. print(driver.get_log("performance"))""", + ) + parser.addoption( + "--remote_debug", + "--remote-debug", + "--remote-debugger", + "--remote_debugger", + action="store_true", + dest="remote_debug", + default=False, + help="""This syncs the browser to Chromium's remote debugger. + To access the remote debugging interface, go to: + chrome://inspect/#devices while tests are running. + The previous URL was at: http://localhost:9222/ + Info: chromedevtools.github.io/devtools-protocol/""", + ) + parser.addoption( + "--enable_3d_apis", + "--enable-3d-apis", + action="store_true", + dest="enable_3d_apis", + default=False, + help="""Using this enables WebGL and 3D APIs.""", + ) + parser.addoption( + "--swiftshader", + action="store_true", + dest="swiftshader", + default=False, + help="""Using this enables the "--use-gl=swiftshader" + feature when running tests on Chrome.""", + ) + parser.addoption( + "--incognito", + "--incognito_mode", + "--incognito-mode", + action="store_true", + dest="incognito", + default=False, + help="""Using this enables Chrome's Incognito mode.""", + ) + parser.addoption( + "--guest", + "--guest_mode", + "--guest-mode", + action="store_true", + dest="guest_mode", + default=False, + help="""Using this enables Chrome's Guest mode.""", + ) + parser.addoption( + "--dark", + "--dark_mode", + "--dark-mode", + action="store_true", + dest="dark_mode", + default=False, + help="""Using this enables Chrome's Dark mode.""", + ) + parser.addoption( + "--devtools", + "--open_devtools", + "--open-devtools", + action="store_true", + dest="devtools", + default=False, + help="""Using this opens Chrome's DevTools.""", + ) + parser.addoption( + "--disable-beforeunload", + "--disable_beforeunload", + action="store_true", + dest="_disable_beforeunload", + default=False, + help="""The option to disable the "beforeunload" event + on Chromium browsers (Chrome or Edge). + This is already the default Firefox option.""", + ) + parser.addoption( + "--window-position", + "--window_position", + action="store", + dest="window_position", + default=None, + help="""The option to set the starting window x,y position. + Format: A comma-separated string with the 2 values. + Example: "55,66" + Default: None. (Will use default values if None)""", + ) + parser.addoption( + "--window-size", + "--window_size", + action="store", + dest="window_size", + default=None, + help="""The option to set the default window "width,height". + Format: A comma-separated string with the 2 values. + Example: "1200,800" + Default: None. (Will use default values if None)""", + ) + parser.addoption( + "--maximize_window", + "--maximize-window", + "--maximize", + "--fullscreen", + action="store_true", + dest="maximize_option", + default=False, + help="""The option to start with a maximized browser window. + (Overrides the "window-size" option if used.)""", + ) + parser.addoption( + "--screenshot", + "--save_screenshot", + "--save-screenshot", + "--ss", + action="store_true", + dest="save_screenshot", + default=False, + help="""Save a screenshot at the end of every test. + By default, this is only done for failures. + Will be saved in the "latest_logs/" folder.""", + ) + parser.addoption( + "--no-screenshot", + "--no_screenshot", + "--ns", + action="store_true", + dest="no_screenshot", + default=False, + help="""No screenshots saved unless tests directly ask it. + This changes default behavior where screenshots are + saved for test failures and pytest-html reports.""", + ) + parser.addoption( + "--visual_baseline", + "--visual-baseline", + action="store_true", + dest="visual_baseline", default=False, help="""Setting this resets the visual baseline for Automated Visual Testing with SeleniumBase. When a test calls self.check_window(), it will - rebuild its files in the visual_baseline folder.""") - parser.add_option( - '--timeout_multiplier', action='store', - dest='timeout_multiplier', + rebuild its files in the visual_baseline folder.""", + ) + parser.addoption( + "--external_pdf", + "--external-pdf", + action="store_true", + dest="external_pdf", + default=False, + help="""This option sets the following on Chromium: + "plugins.always_open_pdf_externally": True, + which causes opened PDF URLs to download immediately, + instead of being displayed in the browser window.""", + ) + parser.addoption( + "--timeout_multiplier", + "--timeout-multiplier", + action="store", + dest="timeout_multiplier", default=None, help="""Setting this overrides the default timeout by the multiplier when waiting for page elements. - Unused when tests overide the default value.""") + Unused when tests override the default value.""", + ) def configure(self, options, conf): - super(SeleniumBrowser, self).configure(options, conf) + super().configure(options, conf) self.enabled = True # Used if test class inherits BaseCase self.options = options self.headless_active = False # Default setting + sb_config.headless_active = False + sb_config.is_nosetest = True proxy_helper.remove_proxy_zip_if_present() def beforeTest(self, test): + browser = self.options.browser + test.test.browser = browser + test.test.headless = None + test.test.headless1 = None + test.test.headless2 = None + # As a shortcut, you can use "--edge" instead of "--browser=edge", etc, + # but you can only specify one default browser. (Default: chrome) + sb_config._browser_shortcut = None + sys_argv = sys.argv + browser_changes = 0 + browser_set = None + browser_text = None + browser_list = [] + # Check if binary-location in options + bin_loc_in_options = False + for arg in sys_argv: + if arg in ["--binary-location", "--binary_location", "--bl"]: + bin_loc_in_options = True + if "--browser=chrome" in sys_argv or "--browser chrome" in sys_argv: + browser_changes += 1 + browser_set = "chrome" + browser_list.append("--browser=chrome") + if "--browser=edge" in sys_argv or "--browser edge" in sys_argv: + browser_changes += 1 + browser_set = "edge" + browser_list.append("--browser=edge") + if "--browser=firefox" in sys_argv or "--browser firefox" in sys_argv: + browser_changes += 1 + browser_set = "firefox" + browser_list.append("--browser=firefox") + if "--browser=safari" in sys_argv or "--browser safari" in sys_argv: + browser_changes += 1 + browser_set = "safari" + browser_list.append("--browser=safari") + if "--browser=ie" in sys_argv or "--browser ie" in sys_argv: + browser_changes += 1 + browser_set = "ie" + browser_list.append("--browser=ie") + if "--browser=remote" in sys_argv or "--browser remote" in sys_argv: + browser_changes += 1 + browser_set = "remote" + browser_list.append("--browser=remote") + if "--browser=opera" in sys_argv or "--browser opera" in sys_argv: + if not bin_loc_in_options: + bin_loc = detect_b_ver.get_binary_location("opera") + if os.path.exists(bin_loc): + browser_changes += 1 + browser_set = "opera" + sb_config._browser_shortcut = "opera" + sb_config._cdp_browser = "opera" + sb_config._cdp_bin_loc = bin_loc + browser_list.append("--browser=opera") + if "--browser=brave" in sys_argv or "--browser brave" in sys_argv: + if not bin_loc_in_options: + bin_loc = detect_b_ver.get_binary_location("brave") + if os.path.exists(bin_loc): + browser_changes += 1 + browser_set = "brave" + sb_config._browser_shortcut = "brave" + sb_config._cdp_browser = "brave" + sb_config._cdp_bin_loc = bin_loc + browser_list.append("--browser=brave") + if "--browser=comet" in sys_argv or "--browser comet" in sys_argv: + if not bin_loc_in_options: + bin_loc = detect_b_ver.get_binary_location("comet") + if os.path.exists(bin_loc): + browser_changes += 1 + browser_set = "comet" + sb_config._browser_shortcut = "comet" + sb_config._cdp_browser = "comet" + sb_config._cdp_bin_loc = bin_loc + browser_list.append("--browser=comet") + browser_text = browser_set + if "--chrome" in sys_argv and not browser_set == "chrome": + browser_changes += 1 + browser_text = "chrome" + sb_config._browser_shortcut = "chrome" + browser_list.append("--chrome") + if "--edge" in sys_argv and not browser_set == "edge": + browser_changes += 1 + browser_text = "edge" + sb_config._browser_shortcut = "edge" + browser_list.append("--edge") + if "--firefox" in sys_argv and not browser_set == "firefox": + browser_changes += 1 + browser_text = "firefox" + sb_config._browser_shortcut = "firefox" + browser_list.append("--firefox") + if "--ie" in sys_argv and not browser_set == "ie": + browser_changes += 1 + browser_text = "ie" + sb_config._browser_shortcut = "ie" + browser_list.append("--ie") + if "--safari" in sys_argv and not browser_set == "safari": + browser_changes += 1 + browser_text = "safari" + sb_config._browser_shortcut = "safari" + browser_list.append("--safari") + if "--opera" in sys_argv and not browser_set == "opera": + if not bin_loc_in_options: + bin_loc = detect_b_ver.get_binary_location("opera") + if os.path.exists(bin_loc): + browser_changes += 1 + browser_text = "opera" + sb_config._browser_shortcut = "opera" + sb_config._cdp_browser = "opera" + sb_config._cdp_bin_loc = bin_loc + browser_list.append("--opera") + if "--brave" in sys_argv and not browser_set == "brave": + if not bin_loc_in_options: + bin_loc = detect_b_ver.get_binary_location("brave") + if os.path.exists(bin_loc): + browser_changes += 1 + browser_text = "brave" + sb_config._browser_shortcut = "brave" + sb_config._cdp_browser = "brave" + sb_config._cdp_bin_loc = bin_loc + browser_list.append("--brave") + if "--comet" in sys_argv and not browser_set == "comet": + if not bin_loc_in_options: + bin_loc = detect_b_ver.get_binary_location("comet") + if os.path.exists(bin_loc): + browser_changes += 1 + browser_text = "comet" + sb_config._browser_shortcut = "comet" + sb_config._cdp_browser = "comet" + sb_config._cdp_bin_loc = bin_loc + browser_list.append("--comet") + if browser_changes > 1: + message = "\n\n TOO MANY browser types were entered!" + message += "\n There were %s found:\n > %s" % ( + browser_changes, + ", ".join(browser_list), + ) + message += "\n ONLY ONE default browser is allowed!" + message += "\n Select a single browser & try again!\n" + raise Exception(message) + if browser_text: + browser = browser_text + if self.options.recorder_mode and browser not in [ + "chrome", "edge", "opera", "brave", "comet", "chromium" + ]: + message = ( + "\n\n Recorder Mode ONLY supports Chromium browsers!" + '\n (Your browser choice was: "%s")\n' % browser + ) + raise Exception(message) + window_position = self.options.window_position + if window_position: + if window_position.count(",") != 1: + message = ( + '\n\n window_position expects an "x,y" string!' + '\n (Your input was: "%s")\n' % window_position + ) + raise Exception(message) + window_position = window_position.replace(" ", "") + win_x = None + win_y = None + try: + win_x = int(window_position.split(",")[0]) + win_y = int(window_position.split(",")[1]) + except Exception: + message = ( + '\n\n Expecting integer values for "x,y"!' + '\n (window_position input was: "%s")\n' + % window_position + ) + raise Exception(message) + settings.WINDOW_START_X = win_x + settings.WINDOW_START_Y = win_y + window_size = self.options.window_size + if window_size: + if window_size.count(",") != 1: + message = ( + '\n\n window_size expects a "width,height" string!' + '\n (Your input was: "%s")\n' % window_size + ) + raise Exception(message) + window_size = window_size.replace(" ", "") + width = None + height = None + try: + width = int(window_size.split(",")[0]) + height = int(window_size.split(",")[1]) + except Exception: + message = ( + '\n\n Expecting integer values for "width,height"!' + '\n (window_size input was: "%s")\n' % window_size + ) + raise Exception(message) + settings.CHROME_START_WIDTH = width + settings.CHROME_START_HEIGHT = height + settings.HEADLESS_START_WIDTH = width + settings.HEADLESS_START_HEIGHT = height + test.test.is_nosetest = True + test.test.is_behave = False + test.test.is_pytest = False + test.test.is_context_manager = False + sb_config.is_nosetest = True + sb_config.is_behave = False + sb_config.is_pytest = False + sb_config.is_context_manager = False test.test.browser = self.options.browser + if sb_config._browser_shortcut: + self.options.browser = sb_config._browser_shortcut + test.test.browser = sb_config._browser_shortcut test.test.cap_file = self.options.cap_file + test.test.cap_string = self.options.cap_string test.test.headless = self.options.headless + test.test.headless1 = self.options.headless1 + if test.test.headless1: + test.test.headless = True + test.test.headless2 = self.options.headless2 + if test.test.headless and test.test.browser == "safari": + test.test.headless = False # Safari doesn't use headless + test.test.headless1 = False + if test.test.headless2 and test.test.browser == "firefox": + test.test.headless2 = False # Only for Chromium browsers + test.test.headless = True # Firefox has regular headless + self.options.headless2 = False + self.options.headless = True + elif test.test.browser not in ["chrome", "edge"]: + test.test.headless2 = False # Only for Chromium browsers + self.options.headless2 = False + test.test.headed = self.options.headed + test.test.xvfb = self.options.xvfb + test.test.xvfb_metrics = self.options.xvfb_metrics + test.test.locale_code = self.options.locale_code + test.test.interval = self.options.interval + test.test.start_page = self.options.start_page + if self.options.skip_js_waits: + settings.SKIP_JS_WAITS = True + if self.options.wait_for_angularjs: + settings.WAIT_FOR_ANGULARJS = True + test.test.protocol = self.options.protocol test.test.servername = self.options.servername test.test.port = self.options.port + test.test.user_data_dir = self.options.user_data_dir + test.test.extension_zip = self.options.extension_zip + test.test.extension_dir = self.options.extension_dir + test.test.disable_features = self.options.disable_features + test.test.binary_location = self.options.binary_location + if getattr(sb_config, "_cdp_bin_loc", None): + test.test.binary_location = sb_config._cdp_bin_loc + if self.options.use_chromium and not test.test.binary_location: + test.test.binary_location = "_chromium_" + elif self.options.use_cft and not test.test.binary_location: + test.test.binary_location = "cft" + elif self.options.use_chs and not test.test.binary_location: + test.test.binary_location = "chs" + sb_config.binary_location = test.test.binary_location + if ( + test.test.binary_location + and test.test.binary_location.lower() == "chs" + and test.test.browser == "chrome" + ): + test.test.headless = True + test.test.headless1 = False + test.test.headless2 = False + if test.test.browser in constants.ChromiumSubs.chromium_subs: + if not sb_config.binary_location: + test.test.browser = "chrome" # Still uses chromedriver + sb_config._browser_shortcut = test.test.browser + test.test.driver_version = self.options.driver_version + test.test.page_load_strategy = self.options.page_load_strategy + test.test.chromium_arg = self.options.chromium_arg + test.test.firefox_arg = self.options.firefox_arg + test.test.firefox_pref = self.options.firefox_pref test.test.proxy_string = self.options.proxy_string + test.test.proxy_bypass_list = self.options.proxy_bypass_list + test.test.proxy_pac_url = self.options.proxy_pac_url + test.test.multi_proxy = self.options.multi_proxy test.test.user_agent = self.options.user_agent + test.test.mobile_emulator = self.options.mobile_emulator + test.test.device_metrics = self.options.device_metrics + test.test.time_limit = self.options.time_limit + test.test.slow_mode = self.options.slow_mode test.test.demo_mode = self.options.demo_mode test.test.demo_sleep = self.options.demo_sleep test.test.highlights = self.options.highlights test.test.message_duration = self.options.message_duration test.test.js_checking_on = self.options.js_checking_on test.test.ad_block_on = self.options.ad_block_on + test.test.host_resolver_rules = self.options.host_resolver_rules + test.test.block_images = self.options.block_images + test.test.do_not_track = self.options.do_not_track test.test.verify_delay = self.options.verify_delay # MasterQA + test.test.esc_end = self.options.esc_end + test.test.recorder_mode = self.options.recorder_mode + test.test.recorder_ext = self.options.recorder_mode # Again + test.test.rec_sb_mgr = self.options.rec_sb_mgr + test.test.rec_sb_cdp = self.options.rec_sb_cdp + test.test.rec_behave = self.options.rec_behave + test.test.rec_print = self.options.rec_print + test.test.record_sleep = self.options.record_sleep + if not self.options.recorder_mode: + if ( + self.options.record_sleep + or self.options.rec_print + or self.options.rec_behave + or self.options.rec_sb_mgr + or self.options.rec_sb_cdp + ): + test.test.recorder_mode = True + test.test.recorder_ext = True + test.test.disable_cookies = self.options.disable_cookies + test.test.disable_js = self.options.disable_js test.test.disable_csp = self.options.disable_csp + test.test.disable_ws = self.options.disable_ws + test.test.enable_ws = self.options.enable_ws + if not self.options.disable_ws: + test.test.enable_ws = True + test.test.enable_sync = self.options.enable_sync + test.test.use_auto_ext = self.options.use_auto_ext + test.test.undetectable = self.options.undetectable + test.test.uc_cdp_events = self.options.uc_cdp_events + test.test.log_cdp_events = self.options.log_cdp_events + if test.test.uc_cdp_events and not test.test.undetectable: + test.test.undetectable = True + test.test.uc_subprocess = self.options.uc_subprocess + if test.test.uc_subprocess and not test.test.undetectable: + test.test.undetectable = True + test.test.no_sandbox = self.options.no_sandbox + test.test.disable_gpu = self.options.disable_gpu + test.test.remote_debug = self.options.remote_debug + test.test.enable_3d_apis = self.options.enable_3d_apis + test.test._swiftshader = self.options.swiftshader + test.test.incognito = self.options.incognito + test.test.guest_mode = self.options.guest_mode + test.test.dark_mode = self.options.dark_mode + test.test.devtools = self.options.devtools + test.test._disable_beforeunload = self.options._disable_beforeunload + test.test.window_position = self.options.window_position + test.test.window_size = self.options.window_size + test.test.maximize_option = self.options.maximize_option + if self.options.save_screenshot and self.options.no_screenshot: + self.options.save_screenshot = False # no_screenshot has priority test.test.save_screenshot_after_test = self.options.save_screenshot + test.test.no_screenshot_after_test = self.options.no_screenshot test.test.visual_baseline = self.options.visual_baseline + test.test.external_pdf = self.options.external_pdf test.test.timeout_multiplier = self.options.timeout_multiplier - test.test.use_grid = False + test.test.dashboard = False + test.test._multithreaded = False + test.test._reuse_session = False + sb_config.recorder_mode = test.test.recorder_mode + sb_config.no_screenshot = test.test.no_screenshot_after_test if test.test.servername != "localhost": - # Use Selenium Grid (Use --server=127.0.0.1 for localhost Grid) - test.test.use_grid = True - if self.options.headless: - self.display = Display(visible=0, size=(1920, 1200)) - self.display.start() - self.headless_active = True + # Using Selenium Grid + # (Set --server="127.0.0.1" for localhost Grid) + if str(self.options.port) == "443": + test.test.protocol = "https" + if ( + shared_utils.is_linux() + and not self.options.headed + and not self.options.headless + and not self.options.headless2 + and not self.options.xvfb + ): + if not self.options.undetectable: + print( + "(Linux uses --headless by default. " + "To override, use --headed / --gui. " + "For Xvfb mode instead, use --xvfb. " + "Or you can hide this info by using " + "--headless / --headless2 / --uc.)" + ) + self.options.headless = True + test.test.headless = True + else: + self.options.xvfb = True + test.test.xvfb = True + # Recorder Mode can still optimize scripts in --headless2 mode. + if self.options.recorder_mode and self.options.headless: + self.options.headless = False + self.options.headless1 = False + self.options.headless2 = True + test.test.headless = False + test.test.headless1 = False + test.test.headless2 = True + if not self.options.headless and not self.options.headless2: + self.options.headed = True + test.test.headed = True + sb_config._virtual_display = None + sb_config.headless_active = False + self.headless_active = False + if ( + shared_utils.is_linux() + and (not self.options.headed or self.options.xvfb) + ): + width = settings.HEADLESS_START_WIDTH + height = settings.HEADLESS_START_HEIGHT + with suppress(Exception): + from sbvirtualdisplay import Display + + self._xvfb_display = Display(visible=0, size=(width, height)) + self._xvfb_display.start() + sb_config._virtual_display = self._xvfb_display + self.headless_active = True + sb_config.headless_active = True + sb_config._is_timeout_changed = False + sb_config._SMALL_TIMEOUT = settings.SMALL_TIMEOUT + sb_config._LARGE_TIMEOUT = settings.LARGE_TIMEOUT + sb_config._context_of_runner = False # Context Manager Compatibility + sb_config.mobile_emulator = self.options.mobile_emulator + sb_config.proxy_driver = self.options.proxy_driver + sb_config.multi_proxy = self.options.multi_proxy # The driver will be received later self.driver = None test.test.driver = self.driver def finalize(self, result): - """ This runs after all tests have completed with nosetests. """ - proxy_helper.remove_proxy_zip_if_present() + """This runs after all tests have completed with nosetests.""" + if not getattr(sb_config, "multi_proxy", None): + proxy_helper.remove_proxy_zip_if_present() def afterTest(self, test): try: # If the browser window is still open, close it now. - self.driver.quit() + if ( + not shared_utils.is_windows() + or test.test.browser == "ie" + or self.driver.service.process + ): + self.driver.quit() except AttributeError: pass except Exception: pass - if self.options.headless: - if self.headless_active: - self.display.stop() + with suppress(Exception): + if ( + getattr(self, "_xvfb_display", None) + and hasattr(self._xvfb_display, "stop") + ): + self.headless_active = False + sb_config.headless_active = False + self._xvfb_display.stop() + self._xvfb_display = None + if ( + getattr(sb_config, "_virtual_display", None) + and hasattr(sb_config._virtual_display, "stop") + ): + sb_config._virtual_display.stop() + sb_config._virtual_display = None diff --git a/seleniumbase/resources/ReadMe.md b/seleniumbase/resources/ReadMe.md old mode 100755 new mode 100644 index adb9fa76be0..3cf1ae20413 --- a/seleniumbase/resources/ReadMe.md +++ b/seleniumbase/resources/ReadMe.md @@ -1,19 +1,31 @@ -## Resources Help + -SeleniumBase uses some Javascript libraries for optional advanced features such as website tours, messenger, highlighting elements on a page, and other jQuery actions. In some cases, you may want to host these Javascript and CSS files from your own websites. For simplicity and convenience, these resources have been downloaded into the "resources" folder. If you decide to use your local versions, you may need to update the corresponding URLs in [base_case.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/fixtures/base_case.py). +## [](https://github.com/seleniumbase/SeleniumBase/) Resource Files -Here are some of the resource files you'll find here: +SeleniumBase uses JavaScript libraries for bonus features such as the Website Tour Maker, Presentation Maker, Chart Maker, Demo Mode, HTML Inspector, and more. In general, SeleniumBase retrieves these resources via CDN link. **favicon.ico** - This file is used by [style_sheet.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/core/style_sheet.py) for the favicon icon. Currently, SeleniumBase uses the version at [https://raw.githubusercontent.com/seleniumbase/SeleniumBase/master/seleniumbase/resources/favicon.ico](https://raw.githubusercontent.com/seleniumbase/SeleniumBase/master/seleniumbase/resources/favicon.ico). -**messenger/** - Files in this folder are used for creating Javascript notifications during test runs in Demo Mode. +-------- -**jquery_confirm/** - Files in this folder are used for creating Javascript confirmation prompts during test runs when using MasterQA. +The remaining resources have been moved into [github.com/seleniumbase/resource-files](https://github.com/seleniumbase/resource-files) in order to reduce the size of SeleniumBase: -**shepherd/** - Files in this folder are used for creating website tours using the Shepherd Javascript library. (This is the default tour library.) +**messenger/** - Files in this folder are used for creating JavaScript notifications during test runs in Demo Mode. -**bootstrap_tour/** - Files in this folder are used for creating website tours using the Bootstrap Tour Javascript library. +**jquery_confirm/** - Files in this folder are used for creating JavaScript confirmation prompts during test runs when using MasterQA. -**introjs/** - Files in this folder are used for creating website tours using the IntroJS Javascript library. +**html_inspector/** - Files in this folder are used for the HTML Inspector, which validates website pages. -**hopscotch/** - Files in this folder are used for creating website tours using the Hopscotch Javascript library. +**reveal/** - Files in this folder are used for the HTML Presentation Maker. + +**prettify/** - Files in this folder are used to assist the HTML Presentation Maker. + +**shepherd/** - Files in this folder are used for creating website tours using the Shepherd JavaScript library. + +**bootstrap_tour/** - Files in this folder are used for creating website tours using the Bootstrap Tour JavaScript library. + +**introjs/** - Files in this folder are used for creating website tours using the IntroJS JavaScript library. + +**driverjs/** - Files in this folder are used for creating website tours using the DriverJS JavaScript library. + +**hopscotch/** - Files in this folder are used for creating website tours using the Hopscotch JavaScript library. diff --git a/seleniumbase/resources/__init__.py b/seleniumbase/resources/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seleniumbase/resources/bootstrap_tour/bootstrap-tour-standalone.min.css b/seleniumbase/resources/bootstrap_tour/bootstrap-tour-standalone.min.css deleted file mode 100644 index 68614533527..00000000000 --- a/seleniumbase/resources/bootstrap_tour/bootstrap-tour-standalone.min.css +++ /dev/null @@ -1,26 +0,0 @@ -/* ======================================================================== - * bootstrap-tour - v0.10.3 - * http://bootstraptour.com - * ======================================================================== - * Copyright 2012-2015 Ulrich Sossou - * - * ======================================================================== - * Licensed under the MIT License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * 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. - * ======================================================================== - */ - -/*! - * Bootstrap v3.1.0 (http://getbootstrap.com) - * Copyright 2011-2014 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;vertical-align:middle;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn.active.focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus,.btn.focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:focus,.btn-default.focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active:hover,.btn-default.active:hover,.open>.dropdown-toggle.btn-default:hover,.btn-default:active:focus,.btn-default.active:focus,.open>.dropdown-toggle.btn-default:focus,.btn-default:active.focus,.btn-default.active.focus,.open>.dropdown-toggle.btn-default.focus{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled.focus,.btn-default[disabled].focus,fieldset[disabled] .btn-default.focus{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary:focus,.btn-primary.focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:focus,.btn-success.focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active:hover,.btn-success.active:hover,.open>.dropdown-toggle.btn-success:hover,.btn-success:active:focus,.btn-success.active:focus,.open>.dropdown-toggle.btn-success:focus,.btn-success:active.focus,.btn-success.active.focus,.open>.dropdown-toggle.btn-success.focus{color:#fff;background-color:#398439;border-color:#255625}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled.focus,.btn-success[disabled].focus,fieldset[disabled] .btn-success.focus{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:focus,.btn-info.focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active:hover,.btn-info.active:hover,.open>.dropdown-toggle.btn-info:hover,.btn-info:active:focus,.btn-info.active:focus,.open>.dropdown-toggle.btn-info:focus,.btn-info:active.focus,.btn-info.active.focus,.open>.dropdown-toggle.btn-info.focus{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled.focus,.btn-info[disabled].focus,fieldset[disabled] .btn-info.focus{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:focus,.btn-warning.focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active:hover,.btn-warning.active:hover,.open>.dropdown-toggle.btn-warning:hover,.btn-warning:active:focus,.btn-warning.active:focus,.open>.dropdown-toggle.btn-warning:focus,.btn-warning:active.focus,.btn-warning.active.focus,.open>.dropdown-toggle.btn-warning.focus{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled.focus,.btn-warning[disabled].focus,fieldset[disabled] .btn-warning.focus{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:focus,.btn-danger.focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active:hover,.btn-danger.active:hover,.open>.dropdown-toggle.btn-danger:hover,.btn-danger:active:focus,.btn-danger.active:focus,.open>.dropdown-toggle.btn-danger:focus,.btn-danger:active.focus,.btn-danger.active.focus,.open>.dropdown-toggle.btn-danger.focus{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled.focus,.btn-danger[disabled].focus,fieldset[disabled] .btn-danger.focus{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#337ab7;font-weight:400;border-radius:0}.btn-link,.btn-link:active,.btn-link.active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#777;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=submit].btn-block,input[type=reset].btn-block,input[type=button].btn-block{width:100%}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-top-left-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-top-left-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=radio],[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;letter-spacing:normal;line-break:auto;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;word-wrap:normal;font-size:14px;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{border-width:10px;content:""}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:rgba(0,0,0,.25);bottom:-11px}.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#fff}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999;border-right-color:rgba(0,0,0,.25)}.popover.right>.arrow:after{content:" ";left:1px;bottom:-10px;border-left-width:0;border-right-color:#fff}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25);top:-11px}.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{content:" ";right:1px;border-right-width:0;border-left-color:#fff;bottom:-10px}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height,visibility;transition-property:height,visibility;-webkit-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;transition-timing-function:ease}.tour-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1100;background-color:#000;opacity:.8;filter:alpha(opacity=80)}.tour-step-backdrop{position:relative;z-index:1101}.tour-step-backdrop>td{position:relative;z-index:1101}.tour-step-background{position:absolute!important;z-index:1100;background:inherit;border-radius:6px}.popover[class*=tour-]{z-index:1102}.popover[class*=tour-] .popover-navigation{padding:9px 14px;overflow:hidden}.popover[class*=tour-] .popover-navigation [data-role=end]{float:right}.popover[class*=tour-] .popover-navigation [data-role=prev],.popover[class*=tour-] .popover-navigation [data-role=next],.popover[class*=tour-] .popover-navigation [data-role=end]{cursor:pointer}.popover[class*=tour-] .popover-navigation [data-role=prev].disabled,.popover[class*=tour-] .popover-navigation [data-role=next].disabled,.popover[class*=tour-] .popover-navigation [data-role=end].disabled{cursor:default}.popover[class*=tour-].orphan{position:fixed;margin-top:0}.popover[class*=tour-].orphan .arrow{display:none} \ No newline at end of file diff --git a/seleniumbase/resources/bootstrap_tour/bootstrap-tour-standalone.min.js b/seleniumbase/resources/bootstrap_tour/bootstrap-tour-standalone.min.js deleted file mode 100644 index 4827f08ad5c..00000000000 --- a/seleniumbase/resources/bootstrap_tour/bootstrap-tour-standalone.min.js +++ /dev/null @@ -1,22 +0,0 @@ -/* ======================================================================== - * bootstrap-tour - v0.10.3 - * http://bootstraptour.com - * ======================================================================== - * Copyright 2012-2015 Ulrich Sossou - * - * ======================================================================== - * Licensed under the MIT License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * 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. - * ======================================================================== - */ - -+function(t){"use strict";function e(){var t=document.createElement("bootstrap"),e={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var o in e)if(void 0!==t.style[o])return{end:e[o]};return!1}t.fn.emulateTransitionEnd=function(e){var o=!1,n=this;t(this).one("bsTransitionEnd",function(){o=!0});var i=function(){o||t(n).trigger(t.support.transition.end)};return setTimeout(i,e),this},t(function(){t.support.transition=e(),t.support.transition&&(t.event.special.bsTransitionEnd={bindType:t.support.transition.end,delegateType:t.support.transition.end,handle:function(e){return t(e.target).is(this)?e.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(t){"use strict";function e(e){return this.each(function(){var n=t(this),i=n.data("bs.tooltip"),r="object"==typeof e&&e;(i||!/destroy|hide/.test(e))&&(i||n.data("bs.tooltip",i=new o(this,r)),"string"==typeof e&&i[e]())})}var o=function(t,e){this.type=null,this.options=null,this.enabled=null,this.timeout=null,this.hoverState=null,this.$element=null,this.inState=null,this.init("tooltip",t,e)};o.VERSION="3.3.7",o.TRANSITION_DURATION=150,o.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},o.prototype.init=function(e,o,n){if(this.enabled=!0,this.type=e,this.$element=t(o),this.options=this.getOptions(n),this.$viewport=this.options.viewport&&t(t.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var i=this.options.trigger.split(" "),r=i.length;r--;){var s=i[r];if("click"==s)this.$element.on("click."+this.type,this.options.selector,t.proxy(this.toggle,this));else if("manual"!=s){var a="hover"==s?"mouseenter":"focusin",p="hover"==s?"mouseleave":"focusout";this.$element.on(a+"."+this.type,this.options.selector,t.proxy(this.enter,this)),this.$element.on(p+"."+this.type,this.options.selector,t.proxy(this.leave,this))}}this.options.selector?this._options=t.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},o.prototype.getDefaults=function(){return o.DEFAULTS},o.prototype.getOptions=function(e){return e=t.extend({},this.getDefaults(),this.$element.data(),e),e.delay&&"number"==typeof e.delay&&(e.delay={show:e.delay,hide:e.delay}),e},o.prototype.getDelegateOptions=function(){var e={},o=this.getDefaults();return this._options&&t.each(this._options,function(t,n){o[t]!=n&&(e[t]=n)}),e},o.prototype.enter=function(e){var o=e instanceof this.constructor?e:t(e.currentTarget).data("bs."+this.type);return o||(o=new this.constructor(e.currentTarget,this.getDelegateOptions()),t(e.currentTarget).data("bs."+this.type,o)),e instanceof t.Event&&(o.inState["focusin"==e.type?"focus":"hover"]=!0),o.tip().hasClass("in")||"in"==o.hoverState?(o.hoverState="in",void 0):(clearTimeout(o.timeout),o.hoverState="in",o.options.delay&&o.options.delay.show?(o.timeout=setTimeout(function(){"in"==o.hoverState&&o.show()},o.options.delay.show),void 0):o.show())},o.prototype.isInStateTrue=function(){for(var t in this.inState)if(this.inState[t])return!0;return!1},o.prototype.leave=function(e){var o=e instanceof this.constructor?e:t(e.currentTarget).data("bs."+this.type);return o||(o=new this.constructor(e.currentTarget,this.getDelegateOptions()),t(e.currentTarget).data("bs."+this.type,o)),e instanceof t.Event&&(o.inState["focusout"==e.type?"focus":"hover"]=!1),o.isInStateTrue()?void 0:(clearTimeout(o.timeout),o.hoverState="out",o.options.delay&&o.options.delay.hide?(o.timeout=setTimeout(function(){"out"==o.hoverState&&o.hide()},o.options.delay.hide),void 0):o.hide())},o.prototype.show=function(){var e=t.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(e);var n=t.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(e.isDefaultPrevented()||!n)return;var i=this,r=this.tip(),s=this.getUID(this.type);this.setContent(),r.attr("id",s),this.$element.attr("aria-describedby",s),this.options.animation&&r.addClass("fade");var a="function"==typeof this.options.placement?this.options.placement.call(this,r[0],this.$element[0]):this.options.placement,p=/\s?auto?\s?/i,h=p.test(a);h&&(a=a.replace(p,"")||"top"),r.detach().css({top:0,left:0,display:"block"}).addClass(a).data("bs."+this.type,this),this.options.container?r.appendTo(this.options.container):r.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var l=this.getPosition(),u=r[0].offsetWidth,c=r[0].offsetHeight;if(h){var d=a,f=this.getPosition(this.$viewport);a="bottom"==a&&l.bottom+c>f.bottom?"top":"top"==a&&l.top-cf.width?"left":"left"==a&&l.left-us.top+s.height&&(i.top=s.top+s.height-p)}else{var h=e.left-r,l=e.left+r+o;hs.right&&(i.left=s.left+s.width-l)}return i},o.prototype.getTitle=function(){var t,e=this.$element,o=this.options;return t=e.attr("data-original-title")||("function"==typeof o.title?o.title.call(e[0]):o.title)},o.prototype.getUID=function(t){do t+=~~(1e6*Math.random());while(document.getElementById(t));return t},o.prototype.tip=function(){if(!this.$tip&&(this.$tip=t(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},o.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},o.prototype.enable=function(){this.enabled=!0},o.prototype.disable=function(){this.enabled=!1},o.prototype.toggleEnabled=function(){this.enabled=!this.enabled},o.prototype.toggle=function(e){var o=this;e&&(o=t(e.currentTarget).data("bs."+this.type),o||(o=new this.constructor(e.currentTarget,this.getDelegateOptions()),t(e.currentTarget).data("bs."+this.type,o))),e?(o.inState.click=!o.inState.click,o.isInStateTrue()?o.enter(o):o.leave(o)):o.tip().hasClass("in")?o.leave(o):o.enter(o)},o.prototype.destroy=function(){var t=this;clearTimeout(this.timeout),this.hide(function(){t.$element.off("."+t.type).removeData("bs."+t.type),t.$tip&&t.$tip.detach(),t.$tip=null,t.$arrow=null,t.$viewport=null,t.$element=null})};var n=t.fn.tooltip;t.fn.tooltip=e,t.fn.tooltip.Constructor=o,t.fn.tooltip.noConflict=function(){return t.fn.tooltip=n,this}}(jQuery),+function(t){"use strict";function e(e){return this.each(function(){var n=t(this),i=n.data("bs.popover"),r="object"==typeof e&&e;(i||!/destroy|hide/.test(e))&&(i||n.data("bs.popover",i=new o(this,r)),"string"==typeof e&&i[e]())})}var o=function(t,e){this.init("popover",t,e)};if(!t.fn.tooltip)throw new Error("Popover requires tooltip.js");o.VERSION="3.3.7",o.DEFAULTS=t.extend({},t.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),o.prototype=t.extend({},t.fn.tooltip.Constructor.prototype),o.prototype.constructor=o,o.prototype.getDefaults=function(){return o.DEFAULTS},o.prototype.setContent=function(){var t=this.tip(),e=this.getTitle(),o=this.getContent();t.find(".popover-title")[this.options.html?"html":"text"](e),t.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof o?"html":"append":"text"](o),t.removeClass("fade top bottom left right in"),t.find(".popover-title").html()||t.find(".popover-title").hide()},o.prototype.hasContent=function(){return this.getTitle()||this.getContent()},o.prototype.getContent=function(){var t=this.$element,e=this.options;return t.attr("data-content")||("function"==typeof e.content?e.content.call(t[0]):e.content)},o.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var n=t.fn.popover;t.fn.popover=e,t.fn.popover.Constructor=o,t.fn.popover.noConflict=function(){return t.fn.popover=n,this}}(jQuery),function(t,e){return"function"==typeof define&&define.amd?define(["jquery"],function(o){return t.Tour=e(o)}):"object"==typeof exports?module.exports=e(require("jQuery")):t.Tour=e(t.jQuery)}(window,function(t){var e,o;return o=window.document,e=function(){function e(e){var o;try{o=window.localStorage}catch(n){o=!1}this._options=t.extend({name:"tour",steps:[],container:"body",autoscroll:!0,keyboard:!0,storage:o,debug:!1,backdrop:!1,backdropContainer:"body",backdropPadding:0,redirect:!0,orphan:!1,duration:!1,delay:!1,basePath:"",template:'',afterSetState:function(){},afterGetState:function(){},afterRemoveState:function(){},onStart:function(){},onEnd:function(){},onShow:function(){},onShown:function(){},onHide:function(){},onHidden:function(){},onNext:function(){},onPrev:function(){},onPause:function(){},onResume:function(){},onRedirectError:function(){}},e),this._force=!1,this._inited=!1,this._current=null,this.backdrop={overlay:null,$element:null,$background:null,backgroundShown:!1,overlayElementShown:!1}}return e.prototype.addSteps=function(t){var e,o,n;for(o=0,n=t.length;n>o;o++)e=t[o],this.addStep(e);return this},e.prototype.addStep=function(t){return this._options.steps.push(t),this},e.prototype.getStep=function(e){return null!=this._options.steps[e]?t.extend({id:"step-"+e,path:"",host:"",placement:"right",title:"",content:"

              ",next:e===this._options.steps.length-1?-1:e+1,prev:e-1,animation:!0,container:this._options.container,autoscroll:this._options.autoscroll,backdrop:this._options.backdrop,backdropContainer:this._options.backdropContainer,backdropPadding:this._options.backdropPadding,redirect:this._options.redirect,reflexElement:this._options.steps[e].element,backdropElement:this._options.steps[e].element,orphan:this._options.orphan,duration:this._options.duration,delay:this._options.delay,template:this._options.template,onShow:this._options.onShow,onShown:this._options.onShown,onHide:this._options.onHide,onHidden:this._options.onHidden,onNext:this._options.onNext,onPrev:this._options.onPrev,onPause:this._options.onPause,onResume:this._options.onResume,onRedirectError:this._options.onRedirectError},this._options.steps[e]):void 0},e.prototype.init=function(t){return this._force=t,this.ended()?(this._debug("Tour ended, init prevented."),this):(this.setCurrentStep(),this._initMouseNavigation(),this._initKeyboardNavigation(),this._onResize(function(t){return function(){return t.showStep(t._current)}}(this)),null!==this._current&&this.showStep(this._current),this._inited=!0,this)},e.prototype.start=function(t){var e;return null==t&&(t=!1),this._inited||this.init(t),null===this._current&&(e=this._makePromise(null!=this._options.onStart?this._options.onStart(this):void 0),this._callOnPromiseDone(e,this.showStep,0)),this},e.prototype.next=function(){var t;return t=this.hideStep(this._current,this._current+1),this._callOnPromiseDone(t,this._showNextStep)},e.prototype.prev=function(){var t;return t=this.hideStep(this._current,this._current-1),this._callOnPromiseDone(t,this._showPrevStep)},e.prototype.goTo=function(t){var e;return e=this.hideStep(this._current,t),this._callOnPromiseDone(e,this.showStep,t)},e.prototype.end=function(){var e,n;return e=function(e){return function(){return t(o).off("click.tour-"+e._options.name),t(o).off("keyup.tour-"+e._options.name),t(window).off("resize.tour-"+e._options.name),e._setState("end","yes"),e._inited=!1,e._force=!1,e._clearTimer(),null!=e._options.onEnd?e._options.onEnd(e):void 0}}(this),n=this.hideStep(this._current),this._callOnPromiseDone(n,e)},e.prototype.ended=function(){return!this._force&&!!this._getState("end")},e.prototype.restart=function(){return this._removeState("current_step"),this._removeState("end"),this._removeState("redirect_to"),this.start()},e.prototype.pause=function(){var t;return t=this.getStep(this._current),t&&t.duration?(this._paused=!0,this._duration-=(new Date).getTime()-this._start,window.clearTimeout(this._timer),this._debug("Paused/Stopped step "+(this._current+1)+" timer ("+this._duration+" remaining)."),null!=t.onPause?t.onPause(this,this._duration):void 0):this},e.prototype.resume=function(){var t;return t=this.getStep(this._current),t&&t.duration?(this._paused=!1,this._start=(new Date).getTime(),this._duration=this._duration||t.duration,this._timer=window.setTimeout(function(t){return function(){return t._isLast()?t.next():t.end()}}(this),this._duration),this._debug("Started step "+(this._current+1)+" timer with duration "+this._duration),null!=t.onResume&&this._duration!==t.duration?t.onResume(this,this._duration):void 0):this},e.prototype.hideStep=function(e,o){var n,i,r,s;return(s=this.getStep(e))?(this._clearTimer(),r=this._makePromise(null!=s.onHide?s.onHide(this,e):void 0),i=function(n){return function(){var i,r;return i=t(s.element),i.data("bs.popover")||i.data("popover")||(i=t("body")),i.popover("destroy").removeClass("tour-"+n._options.name+"-element tour-"+n._options.name+"-"+e+"-element").removeData("bs.popover").focus(),s.reflex&&t(s.reflexElement).removeClass("tour-step-element-reflex").off(""+n._reflexEvent(s.reflex)+".tour-"+n._options.name),s.backdrop&&(r=null!=o&&n.getStep(o),r&&r.backdrop&&r.backdropElement===s.backdropElement||n._hideBackdrop()),null!=s.onHidden?s.onHidden(n):void 0}}(this),n=s.delay.hide||s.delay,"[object Number]"==={}.toString.call(n)&&n>0?(this._debug("Wait "+n+" milliseconds to hide the step "+(this._current+1)),window.setTimeout(function(t){return function(){return t._callOnPromiseDone(r,i)}}(this),n)):this._callOnPromiseDone(r,i),r):void 0},e.prototype.showStep=function(t){var e,n,i,r,s,a;return this.ended()?(this._debug("Tour ended, showStep prevented."),this):(a=this.getStep(t),a&&(s=t0?(this._debug("Wait "+i+" milliseconds to show the step "+(this._current+1)),window.setTimeout(function(t){return function(){return t._callOnPromiseDone(n,r)}}(this),i)):this._callOnPromiseDone(n,r),n):void 0)},e.prototype.getCurrentStep=function(){return this._current},e.prototype.setCurrentStep=function(t){return null!=t?(this._current=t,this._setState("current_step",t)):(this._current=this._getState("current_step"),this._current=null===this._current?null:parseInt(this._current,10)),this},e.prototype.redraw=function(){return this._showOverlayElement(this.getStep(this.getCurrentStep()).element,!0)},e.prototype._setState=function(t,e){var o,n;if(this._options.storage){n=""+this._options.name+"_"+t;try{this._options.storage.setItem(n,e)}catch(i){o=i,o.code===DOMException.QUOTA_EXCEEDED_ERR&&this._debug("LocalStorage quota exceeded. State storage failed.")}return this._options.afterSetState(n,e)}return null==this._state&&(this._state={}),this._state[t]=e},e.prototype._removeState=function(t){var e;return this._options.storage?(e=""+this._options.name+"_"+t,this._options.storage.removeItem(e),this._options.afterRemoveState(e)):null!=this._state?delete this._state[t]:void 0},e.prototype._getState=function(t){var e,o;return this._options.storage?(e=""+this._options.name+"_"+t,o=this._options.storage.getItem(e)):null!=this._state&&(o=this._state[t]),(void 0===o||"null"===o)&&(o=null),this._options.afterGetState(t,o),o},e.prototype._showNextStep=function(){var t,e,o;return o=this.getStep(this._current),e=function(t){return function(){return t.showStep(o.next)}}(this),t=this._makePromise(null!=o.onNext?o.onNext(this):void 0),this._callOnPromiseDone(t,e)},e.prototype._showPrevStep=function(){var t,e,o;return o=this.getStep(this._current),e=function(t){return function(){return t.showStep(o.prev)}}(this),t=this._makePromise(null!=o.onPrev?o.onPrev(this):void 0),this._callOnPromiseDone(t,e)},e.prototype._debug=function(t){return this._options.debug?window.console.log("Bootstrap Tour '"+this._options.name+"' | "+t):void 0},e.prototype._isRedirect=function(t,e,o){var n;return null!=t&&""!==t&&("[object RegExp]"==={}.toString.call(t)&&!t.test(o.origin)||"[object String]"==={}.toString.call(t)&&this._isHostDifferent(t,o))?!0:(n=[o.pathname,o.search,o.hash].join(""),null!=e&&""!==e&&("[object RegExp]"==={}.toString.call(e)&&!e.test(n)||"[object String]"==={}.toString.call(e)&&this._isPathDifferent(e,n)))},e.prototype._isHostDifferent=function(t,e){switch({}.toString.call(t)){case"[object RegExp]":return!t.test(e.origin);case"[object String]":return this._getProtocol(t)!==this._getProtocol(e.href)||this._getHost(t)!==this._getHost(e.href);default:return!0}},e.prototype._isPathDifferent=function(t,e){return this._getPath(t)!==this._getPath(e)||!this._equal(this._getQuery(t),this._getQuery(e))||!this._equal(this._getHash(t),this._getHash(e))},e.prototype._isJustPathHashDifferent=function(t,e,o){var n;return null!=t&&""!==t&&this._isHostDifferent(t,o)?!1:(n=[o.pathname,o.search,o.hash].join(""),"[object String]"==={}.toString.call(e)?this._getPath(e)===this._getPath(n)&&this._equal(this._getQuery(e),this._getQuery(n))&&!this._equal(this._getHash(e),this._getHash(n)):!1)},e.prototype._redirect=function(e,n,i){var r;return t.isFunction(e.redirect)?e.redirect.call(this,i):(r="[object String]"==={}.toString.call(e.host)?""+e.host+i:i,this._debug("Redirect to "+r),this._getState("redirect_to")!==""+n?(this._setState("redirect_to",""+n),o.location.href=r):(this._debug("Error redirection loop to "+i),this._removeState("redirect_to"),null!=e.onRedirectError?e.onRedirectError(this):void 0))},e.prototype._isOrphan=function(e){return null==e.element||!t(e.element).length||t(e.element).is(":hidden")&&"http://www.w3.org/2000/svg"!==t(e.element)[0].namespaceURI},e.prototype._isLast=function(){return this._current").parent().html()},e.prototype._reflexEvent=function(t){return"[object Boolean]"==={}.toString.call(t)?"click":t},e.prototype._focus=function(t,e,o){var n,i;return i=o?"end":"next",n=t.find("[data-role='"+i+"']"),e.on("shown.bs.popover",function(){return n.focus()})},e.prototype._reposition=function(e,n){var i,r,s,a,p,h,l;if(a=e[0].offsetWidth,r=e[0].offsetHeight,l=e.offset(),p=l.left,h=l.top,i=t(o).outerHeight()-l.top-e.outerHeight(),0>i&&(l.top=l.top+i),s=t("html").outerWidth()-l.left-e.outerWidth(),0>s&&(l.left=l.left+s),l.top<0&&(l.top=0),l.left<0&&(l.left=0),e.offset(l),"bottom"===n.placement||"top"===n.placement){if(p!==l.left)return this._replaceArrow(e,2*(l.left-p),a,"left")}else if(h!==l.top)return this._replaceArrow(e,2*(l.top-h),r,"top")},e.prototype._center=function(e){return e.css("top",t(window).outerHeight()/2-e.outerHeight()/2)},e.prototype._replaceArrow=function(t,e,o,n){return t.find(".arrow").css(n,e?50*(1-e/o)+"%":"")},e.prototype._scrollIntoView=function(e,o){var n,i,r,s,a,p,h;if(n=t(e.element),!n.length)return o();switch(i=t(window),a=n.offset().top,s=n.outerHeight(),h=i.height(),p=0,e.placement){case"top":p=Math.max(0,a-h/2);break;case"left":case"right":p=Math.max(0,a+s/2-h/2);break;case"bottom":p=Math.max(0,a+s-h/2)}return this._debug("Scroll into view. ScrollTop: "+p+". Element offset: "+a+". Window height: "+h+"."),r=0,t("body, html").stop(!0,!0).animate({scrollTop:Math.ceil(p)},function(t){return function(){return 2===++r?(o(),t._debug("Scroll into view.\nAnimation end element offset: "+n.offset().top+".\nWindow height: "+i.height()+".")):void 0}}(this))},e.prototype._onResize=function(e,o){return t(window).on("resize.tour-"+this._options.name,function(){return clearTimeout(o),o=setTimeout(e,100)})},e.prototype._initMouseNavigation=function(){var e;return e=this,t(o).off("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='prev']").off("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='next']").off("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='end']").off("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='pause-resume']").on("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='next']",function(t){return function(e){return e.preventDefault(),t.next()}}(this)).on("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='prev']",function(t){return function(e){return e.preventDefault(),t._current>0?t.prev():void 0}}(this)).on("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='end']",function(t){return function(e){return e.preventDefault(),t.end()}}(this)).on("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='pause-resume']",function(o){var n;return o.preventDefault(),n=t(this),n.text(e._paused?n.data("pause-text"):n.data("resume-text")),e._paused?e.resume():e.pause()})},e.prototype._initKeyboardNavigation=function(){return this._options.keyboard?t(o).on("keyup.tour-"+this._options.name,function(t){return function(e){if(e.which)switch(e.which){case 39:return e.preventDefault(),t._isLast()?t.next():t.end();case 37:if(e.preventDefault(),t._current>0)return t.prev()}}}(this)):void 0},e.prototype._makePromise=function(e){return e&&t.isFunction(e.then)?e:null},e.prototype._callOnPromiseDone=function(t,e,o){return t?t.then(function(t){return function(){return e.call(t,o)}}(this)):e.call(this,o)},e.prototype._showBackdrop=function(e){return this.backdrop.backgroundShown?void 0:(this.backdrop=t("
              ",{"class":"tour-backdrop"}),this.backdrop.backgroundShown=!0,t(e.backdropContainer).append(this.backdrop))},e.prototype._hideBackdrop=function(){return this._hideOverlayElement(),this._hideBackground()},e.prototype._hideBackground=function(){return this.backdrop&&this.backdrop.remove?(this.backdrop.remove(),this.backdrop.overlay=null,this.backdrop.backgroundShown=!1):void 0},e.prototype._showOverlayElement=function(e,o){var n,i,r;return i=t(e.element),n=t(e.backdropElement),!i||0===i.length||this.backdrop.overlayElementShown&&!o?void 0:(this.backdrop.overlayElementShown||(this.backdrop.$element=n.addClass("tour-step-backdrop"),this.backdrop.$background=t("
              ",{"class":"tour-step-background"}),this.backdrop.$background.appendTo(e.backdropContainer),this.backdrop.overlayElementShown=!0),r={width:n.innerWidth(),height:n.innerHeight(),offset:n.offset()},e.backdropPadding&&(r=this._applyBackdropPadding(e.backdropPadding,r)),this.backdrop.$background.width(r.width).height(r.height).offset(r.offset))},e.prototype._hideOverlayElement=function(){return this.backdrop.overlayElementShown?(this.backdrop.$element.removeClass("tour-step-backdrop"),this.backdrop.$background.remove(),this.backdrop.$element=null,this.backdrop.$background=null,this.backdrop.overlayElementShown=!1):void 0},e.prototype._applyBackdropPadding=function(t,e){return"object"==typeof t?(null==t.top&&(t.top=0),null==t.right&&(t.right=0),null==t.bottom&&(t.bottom=0),null==t.left&&(t.left=0),e.offset.top=e.offset.top-t.top,e.offset.left=e.offset.left-t.left,e.width=e.width+t.left+t.right,e.height=e.height+t.top+t.bottom):(e.offset.top=e.offset.top-t,e.offset.left=e.offset.left-t,e.width=e.width+2*t,e.height=e.height+2*t),e},e.prototype._clearTimer=function(){return window.clearTimeout(this._timer),this._timer=null,this._duration=null},e.prototype._getProtocol=function(t){return t=t.split("://"),t.length>1?t[0]:"http"},e.prototype._getHost=function(t){return t=t.split("//"),t=t.length>1?t[1]:t[0],t.split("/")[0]},e.prototype._getPath=function(t){return t.replace(/\/?$/,"").split("?")[0].split("#")[0]},e.prototype._getQuery=function(t){return this._getParams(t,"?")},e.prototype._getHash=function(t){return this._getParams(t,"#")},e.prototype._getParams=function(t,e){var o,n,i,r,s;if(n=t.split(e),1===n.length)return{};for(n=n[1].split("&"),i={},r=0,s=n.length;s>r;r++)o=n[r],o=o.split("="),i[o[0]]=o[1]||"";return i},e.prototype._equal=function(t,e){var o,n,i,r,s,a;if("[object Object]"==={}.toString.call(t)&&"[object Object]"==={}.toString.call(e)){if(n=Object.keys(t),i=Object.keys(e),n.length!==i.length)return!1;for(o in t)if(r=t[o],!this._equal(e[o],r))return!1;return!0}if("[object Array]"==={}.toString.call(t)&&"[object Array]"==={}.toString.call(e)){if(t.length!==e.length)return!1;for(o=s=0,a=t.length;a>s;o=++s)if(r=t[o],!this._equal(r,e[o]))return!1;return!0}return t===e},e}()}); \ No newline at end of file diff --git a/seleniumbase/resources/favicon.ico b/seleniumbase/resources/favicon.ico index 1507bc732ac..6a99a7ed411 100644 Binary files a/seleniumbase/resources/favicon.ico and b/seleniumbase/resources/favicon.ico differ diff --git a/seleniumbase/resources/hopscotch/hopscotch.min.css b/seleniumbase/resources/hopscotch/hopscotch.min.css deleted file mode 100644 index b5295f247a7..00000000000 --- a/seleniumbase/resources/hopscotch/hopscotch.min.css +++ /dev/null @@ -1,17 +0,0 @@ -/**! hopscotch - v0.3.1 -* -* Copyright 2017 LinkedIn Corp. All rights reserved. -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* 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. -*/ -.animated{-webkit-animation-fill-mode:both;-moz-animation-fill-mode:both;-ms-animation-fill-mode:both;-o-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:1s;-moz-animation-duration:1s;-ms-animation-duration:1s;-o-animation-duration:1s;animation-duration:1s}@-webkit-keyframes fadeInUp{0%{opacity:0;-webkit-transform:translateY(20px)}100%{opacity:1;-webkit-transform:translateY(0)}}@-moz-keyframes fadeInUp{0%{opacity:0;-moz-transform:translateY(20px)}100%{opacity:1;-moz-transform:translateY(0)}}@-o-keyframes fadeInUp{0%{opacity:0;-o-transform:translateY(20px)}100%{opacity:1;-o-transform:translateY(0)}}@keyframes fadeInUp{0%{opacity:0;transform:translateY(20px)}100%{opacity:1;transform:translateY(0)}}.fade-in-up{-webkit-animation-name:fadeInUp;-moz-animation-name:fadeInUp;-o-animation-name:fadeInUp;animation-name:fadeInUp}@-webkit-keyframes fadeInDown{0%{opacity:0;-webkit-transform:translateY(-20px)}100%{opacity:1;-webkit-transform:translateY(0)}}@-moz-keyframes fadeInDown{0%{opacity:0;-moz-transform:translateY(-20px)}100%{opacity:1;-moz-transform:translateY(0)}}@-o-keyframes fadeInDown{0%{opacity:0;-ms-transform:translateY(-20px)}100%{opacity:1;-ms-transform:translateY(0)}}@keyframes fadeInDown{0%{opacity:0;transform:translateY(-20px)}100%{opacity:1;transform:translateY(0)}}.fade-in-down{-webkit-animation-name:fadeInDown;-moz-animation-name:fadeInDown;-o-animation-name:fadeInDown;animation-name:fadeInDown}@-webkit-keyframes fadeInRight{0%{opacity:0;-webkit-transform:translateX(-20px)}100%{opacity:1;-webkit-transform:translateX(0)}}@-moz-keyframes fadeInRight{0%{opacity:0;-moz-transform:translateX(-20px)}100%{opacity:1;-moz-transform:translateX(0)}}@-o-keyframes fadeInRight{0%{opacity:0;-o-transform:translateX(-20px)}100%{opacity:1;-o-transform:translateX(0)}}@keyframes fadeInRight{0%{opacity:0;transform:translateX(-20px)}100%{opacity:1;transform:translateX(0)}}.fade-in-right{-webkit-animation-name:fadeInRight;-moz-animation-name:fadeInRight;-o-animation-name:fadeInRight;animation-name:fadeInRight}@-webkit-keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translateX(20px)}100%{opacity:1;-webkit-transform:translateX(0)}}@-moz-keyframes fadeInLeft{0%{opacity:0;-moz-transform:translateX(20px)}100%{opacity:1;-moz-transform:translateX(0)}}@-o-keyframes fadeInLeft{0%{opacity:0;-o-transform:translateX(20px)}100%{opacity:1;-o-transform:translateX(0)}}@keyframes fadeInLeft{0%{opacity:0;transform:translateX(20px)}100%{opacity:1;transform:translateX(0)}}.fade-in-left{-webkit-animation-name:fadeInLeft;-moz-animation-name:fadeInLeft;-o-animation-name:fadeInLeft;animation-name:fadeInLeft}div.hopscotch-bubble .hopscotch-nav-button{font-weight:700;border-width:1px;border-style:solid;cursor:pointer;margin:0;overflow:visible;text-decoration:none!important;width:auto;padding:0 10px;height:26px;line-height:24px;font-size:12px;*zoom:1;white-space:nowrap;display:-moz-inline-stack;display:inline-block;*vertical-align:auto;zoom:1;*display:inline;vertical-align:middle;-moz-border-radius:3px;-ms-border-radius:3px;-o-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}div.hopscotch-bubble .hopscotch-nav-button:hover{*zoom:1;-webkit-box-shadow:0 1px 3px rgba(0,0,0,.25);-moz-box-shadow:0 1px 3px rgba(0,0,0,.25);box-shadow:0 1px 3px rgba(0,0,0,.25)}div.hopscotch-bubble .hopscotch-nav-button:active{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.25) inset;-moz-box-shadow:0 1px 2px rgba(0,0,0,.25) inset;box-shadow:0 1px 2px rgba(0,0,0,.25) inset}div.hopscotch-bubble .hopscotch-nav-button.next{border-color:#1b5480;color:#fff;margin:0 0 0 10px;text-shadow:0 1px 1px rgba(0,0,0,.35);background-color:#287bbc;filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#287bbc', endColorstr='#23639a');background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(0%,#287bbc),color-stop(100%,#23639a));background-image:-webkit-linear-gradient(to bottom,#287bbc 0,#23639a 100%);background-image:-moz-linear-gradient(to bottom,#287bbc 0,#23639a 100%);background-image:-o-linear-gradient(to bottom,#287bbc 0,#23639a 100%);background-image:linear-gradient(to bottom,#287bbc 0,#23639a 100%)}div.hopscotch-bubble .hopscotch-nav-button.next:hover{background-color:#2672ae;filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#2672ae', endColorstr='#1e4f7e');background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(0%,#2672ae),color-stop(100%,#1e4f7e));background-image:-webkit-linear-gradient(to bottom,#2672ae 0,#1e4f7e 100%);background-image:-moz-linear-gradient(to bottom,#2672ae 0,#1e4f7e 100%);background-image:-o-linear-gradient(to bottom,#2672ae 0,#1e4f7e 100%);background-image:linear-gradient(to bottom,#2672ae 0,#1e4f7e 100%)}div.hopscotch-bubble .hopscotch-nav-button.prev{border-color:#a7a7a7;color:#444;text-shadow:0 1px 1px rgba(255,255,255,.75);background-color:#f2f2f2;filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#f2f2f2', endColorstr='#e9e9e9');background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(0%,#f2f2f2),color-stop(100%,#e9e9e9));background-image:-webkit-linear-gradient(to bottom,#f2f2f2 0,#e9e9e9 100%);background-image:-moz-linear-gradient(to bottom,#f2f2f2 0,#e9e9e9 100%);background-image:-o-linear-gradient(to bottom,#f2f2f2 0,#e9e9e9 100%);background-image:linear-gradient(to bottom,#f2f2f2 0,#e9e9e9 100%)}div.hopscotch-bubble .hopscotch-nav-button.prev:hover{background-color:#e8e8e8;filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#FFE8E8E8', endColorstr='#FFA9A9A9');background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(0%,#e8e8e8),color-stop(13%,#e3e3e3),color-stop(32%,#d7d7d7),color-stop(71%,#b9b9b9),color-stop(100%,#a9a9a9));background-image:-webkit-linear-gradient(to bottom,#e8e8e8 0,#e3e3e3 13%,#d7d7d7 32%,#b9b9b9 71%,#a9a9a9 100%);background-image:-moz-linear-gradient(to bottom,#e8e8e8 0,#e3e3e3 13%,#d7d7d7 32%,#b9b9b9 71%,#a9a9a9 100%);background-image:-o-linear-gradient(to bottom,#e8e8e8 0,#e3e3e3 13%,#d7d7d7 32%,#b9b9b9 71%,#a9a9a9 100%);background-image:linear-gradient(to bottom,#e8e8e8 0,#e3e3e3 13%,#d7d7d7 32%,#b9b9b9 71%,#a9a9a9 100%)}div.hopscotch-bubble{background-color:#fff;border:5px solid #000;border:5px solid rgba(0,0,0,.5);color:#333;font-family:Helvetica,Arial;font-size:13px;position:absolute;z-index:999999;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-moz-background-clip:padding;-webkit-background-clip:padding;background-clip:padding-box}div.hopscotch-bubble *{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}div.hopscotch-bubble.animate{-moz-transition-property:top,left;-moz-transition-duration:1s;-moz-transition-timing-function:ease-in-out;-ms-transition-property:top,left;-ms-transition-duration:1s;-ms-transition-timing-function:ease-in-out;-o-transition-property:top,left;-o-transition-duration:1s;-o-transition-timing-function:ease-in-out;-webkit-transition-property:top,left;-webkit-transition-duration:1s;-webkit-transition-timing-function:ease-in-out;transition-property:top,left;transition-duration:1s;transition-timing-function:ease-in-out}div.hopscotch-bubble.invisible{opacity:0}div.hopscotch-bubble.hide,div.hopscotch-bubble .hide,div.hopscotch-bubble .hide-all{display:none}div.hopscotch-bubble h3{color:#000;font-family:Helvetica,Arial;font-size:16px;font-weight:700;line-height:19px;margin:-1px 15px 0 0;padding:0}div.hopscotch-bubble .hopscotch-bubble-container{padding:15px;position:relative;text-align:left;-webkit-font-smoothing:antialiased}div.hopscotch-bubble .hopscotch-content{font-family:Helvetica,Arial;font-weight:400;line-height:17px;margin:-5px 0 11px;padding-top:8px}div.hopscotch-bubble .hopscotch-bubble-content{margin:0 0 0 40px}div.hopscotch-bubble.no-number .hopscotch-bubble-content{margin:0}div.hopscotch-bubble .hopscotch-bubble-close{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:0;color:#000;background:transparent url(../img/sprite-green.png) -192px -92px no-repeat;display:block;padding:8px;position:absolute;text-decoration:none;text-indent:-9999px;width:8px;height:8px;top:0;right:0}div.hopscotch-bubble .hopscotch-bubble-close.hide,div.hopscotch-bubble .hopscotch-bubble-close.hide-all{display:none}div.hopscotch-bubble .hopscotch-bubble-number{background:transparent url(../img/sprite-green.png) 0 0 no-repeat;color:#fff;display:block;float:left;font-size:17px;font-weight:700;line-height:31px;padding:0 10px 0 0;text-align:center;width:30px;height:30px}div.hopscotch-bubble .hopscotch-bubble-arrow-container{position:absolute;width:34px;height:34px}div.hopscotch-bubble .hopscotch-bubble-arrow-container .hopscotch-bubble-arrow,div.hopscotch-bubble .hopscotch-bubble-arrow-container .hopscotch-bubble-arrow-border{width:0;height:0}div.hopscotch-bubble .hopscotch-bubble-arrow-container.up{top:-22px;left:10px}div.hopscotch-bubble .hopscotch-bubble-arrow-container.up .hopscotch-bubble-arrow{border-bottom:17px solid #fff;border-left:17px solid transparent;border-right:17px solid transparent;position:relative;top:-10px}div.hopscotch-bubble .hopscotch-bubble-arrow-container.up .hopscotch-bubble-arrow-border{border-bottom:17px solid #000;border-bottom:17px solid rgba(0,0,0,.5);border-left:17px solid transparent;border-right:17px solid transparent}div.hopscotch-bubble .hopscotch-bubble-arrow-container.down{bottom:-39px;left:10px}div.hopscotch-bubble .hopscotch-bubble-arrow-container.down .hopscotch-bubble-arrow{border-top:17px solid #fff;border-left:17px solid transparent;border-right:17px solid transparent;position:relative;top:-24px}div.hopscotch-bubble .hopscotch-bubble-arrow-container.down .hopscotch-bubble-arrow-border{border-top:17px solid #000;border-top:17px solid rgba(0,0,0,.5);border-left:17px solid transparent;border-right:17px solid transparent}div.hopscotch-bubble .hopscotch-bubble-arrow-container.left{top:10px;left:-22px}div.hopscotch-bubble .hopscotch-bubble-arrow-container.left .hopscotch-bubble-arrow{border-bottom:17px solid transparent;border-right:17px solid #fff;border-top:17px solid transparent;position:relative;left:7px;top:-34px}div.hopscotch-bubble .hopscotch-bubble-arrow-container.left .hopscotch-bubble-arrow-border{border-right:17px solid #000;border-right:17px solid rgba(0,0,0,.5);border-bottom:17px solid transparent;border-top:17px solid transparent}div.hopscotch-bubble .hopscotch-bubble-arrow-container.right{top:10px;right:-39px}div.hopscotch-bubble .hopscotch-bubble-arrow-container.right .hopscotch-bubble-arrow{border-bottom:17px solid transparent;border-left:17px solid #fff;border-top:17px solid transparent;position:relative;left:-7px;top:-34px}div.hopscotch-bubble .hopscotch-bubble-arrow-container.right .hopscotch-bubble-arrow-border{border-left:17px solid #000;border-left:17px solid rgba(0,0,0,.5);border-bottom:17px solid transparent;border-top:17px solid transparent}div.hopscotch-bubble .hopscotch-actions{margin:10px 0 0;text-align:right} \ No newline at end of file diff --git a/seleniumbase/resources/hopscotch/hopscotch.min.js b/seleniumbase/resources/hopscotch/hopscotch.min.js deleted file mode 100644 index 97e99a02f08..00000000000 --- a/seleniumbase/resources/hopscotch/hopscotch.min.js +++ /dev/null @@ -1,17 +0,0 @@ -/**! hopscotch - v0.3.1 -* -* Copyright 2017 LinkedIn Corp. All rights reserved. -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* 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. -*/ -!function(a,b){"object"==typeof exports&&"undefined"!=typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):a.hopscotch=b()}(this,function(){"use strict";var a,b,c,d,e,f,g,h,i,j,k,l,m,n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(a){return typeof a}:function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a},o="bubble_default",p=window.Sizzle||null,q="undefined",r=!1,s=("undefined"==typeof jQuery?"undefined":n(jQuery))!==q,t=!1,u=!1,v=/^[a-zA-Z]+[a-zA-Z0-9_-]*$/,w={left:"right",right:"left"};try{n(window.sessionStorage)!==q&&(t=!0,sessionStorage.setItem("hopscotch.test.storage","ok"),sessionStorage.removeItem("hopscotch.test.storage"),u=!0)}catch(x){}l={smoothScroll:!0,scrollDuration:1e3,scrollTopMargin:200,showCloseButton:!0,showPrevButton:!1,showNextButton:!0,bubbleWidth:280,bubblePadding:15,arrowWidth:20,skipIfNoElement:!0,isRtl:!1,cookieName:"hopscotch.tour.state"},Array.isArray||(Array.isArray=function(a){return"[object Array]"===Object.prototype.toString.call(a)}),k=function(){r&&m.startTour()},h={addClass:function(a,b){var c,d,e,f;if(a.className){for(d=b.split(/\s+/),c=" "+a.className+" ",e=0,f=d.length;f>e;++e)c.indexOf(" "+d[e]+" ")<0&&(c+=d[e]+" ");a.className=c.replace(/^\s+|\s+$/g,"")}else a.className=b},removeClass:function(a,b){var c,d,e,f;for(d=b.split(/\s+/),c=" "+a.className+" ",e=0,f=d.length;f>e;++e)c=c.replace(" "+d[e]+" "," ");a.className=c.replace(/^\s+|\s+$/g,"")},hasClass:function(a,b){var c;return a.className?(c=" "+a.className+" ",-1!==c.indexOf(" "+b+" ")):!1},getPixelValue:function(a){var b="undefined"==typeof a?"undefined":n(a);return"number"===b?a:"string"===b?parseInt(a,10):0},valOrDefault:function(a,b){return("undefined"==typeof a?"undefined":n(a))!==q?a:b},invokeCallbackArrayHelper:function(a){var b;return Array.isArray(a)&&(b=j[a[0]],"function"==typeof b)?b.apply(this,a.slice(1)):void 0},invokeCallbackArray:function(a){var b,c;if(Array.isArray(a)){if("string"==typeof a[0])return h.invokeCallbackArrayHelper(a);for(b=0,c=a.length;c>b;++b)h.invokeCallback(a[b])}},invokeCallback:function(a){return"function"==typeof a?a():"string"==typeof a&&j[a]?j[a]():h.invokeCallbackArray(a)},invokeEventCallbacks:function(a,b){var c,d,e=i[a];if(b)return this.invokeCallback(b);for(c=0,d=e.length;d>c;++c)this.invokeCallback(e[c].cb)},getScrollTop:function(){var a;return a=n(window.pageYOffset)!==q?window.pageYOffset:document.documentElement.scrollTop},getScrollLeft:function(){var a;return a=n(window.pageXOffset)!==q?window.pageXOffset:document.documentElement.scrollLeft},getWindowHeight:function(){return window.innerHeight||document.documentElement.clientHeight},addEvtListener:function(a,b,c){return a?a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent("on"+b,c):void 0},removeEvtListener:function(a,b,c){return a?a.removeEventListener?a.removeEventListener(b,c,!1):a.detachEvent("on"+b,c):void 0},documentIsReady:function(){return"complete"===document.readyState},evtPreventDefault:function(a){a.preventDefault?a.preventDefault():event&&(event.returnValue=!1)},extend:function(a,b){var c;for(c in b)b.hasOwnProperty(c)&&(a[c]=b[c])},getStepTargetHelper:function(a){var b=document.getElementById(a);if(b)return b;if(s)return b=jQuery(a),b.length?b[0]:null;if(p)return b=new p(a),b.length?b[0]:null;if(document.querySelector)try{return document.querySelector(a)}catch(c){}return/^#[a-zA-Z][\w-_:.]*$/.test(a)?document.getElementById(a.substring(1)):null},getStepTarget:function(a){var b;if(!a||!a.target)return null;if("string"==typeof a.target)return h.getStepTargetHelper(a.target);if(Array.isArray(a.target)){var c,d;for(c=0,d=a.target.length;d>c;c++)if("string"==typeof a.target[c]&&(b=h.getStepTargetHelper(a.target[c])))return b;return null}return a.target},getI18NString:function(a){return e[a]||d[a]},setState:function(a,b,c){var d,e="";if(t&&u)try{sessionStorage.setItem(a,b)}catch(f){u=!1,this.setState(a,b,c)}else t&&sessionStorage.removeItem(a),c&&(d=new Date,d.setTime(d.getTime()+24*c*60*60*1e3),e="; expires="+d.toGMTString()),document.cookie=a+"="+b+e+"; path=/"},getState:function(a){var b,c,d,e=a+"=",f=document.cookie.split(";");if(t&&(d=sessionStorage.getItem(a)))return d;for(b=0;b0,showNext:h.valOrDefault(a.showNextButton,this.opt.showNextButton),showCTA:h.valOrDefault(a.showCTAButton&&a.ctaLabel,!1),ctaLabel:a.ctaLabel,showClose:h.valOrDefault(this.opt.showCloseButton,!0)},step:{num:b,isLast:h.valOrDefault(n,!1),title:a.title||"",content:a.content||"",isRtl:a.isRtl,placement:a.placement,padding:h.valOrDefault(a.padding,this.opt.bubblePadding),width:h.getPixelValue(a.width)||this.opt.bubbleWidth,customData:a.customData||{}},tour:{isTour:this.opt.isTourBubble,numSteps:j,unsafe:h.valOrDefault(g,!1),customData:e||{}}},"function"==typeof d)r.innerHTML=d(q);else if("string"==typeof d){if(!m.templates||"function"!=typeof m.templates[d])throw new Error('Bubble rendering failed - template "'+d+'" is not a function.');r.innerHTML=m.templates[d](q)}else if(f)r.innerHTML=f(q);else{if(!m.templates||"function"!=typeof m.templates[o])throw new Error('Bubble rendering failed - template "'+o+'" is not a function.');r.innerHTML=m.templates[o](q)}var s,t=r.children,u=t.length;for(p=0;u>p;p++)s=t[p],h.hasClass(s,"hopscotch-arrow")&&(this.arrowEl=s);return r.style.zIndex="number"==typeof a.zindex?a.zindex:"",this._setArrow(a.placement),this.hide(!1),this.setPosition(a),c&&c(!a.fixedElement),this},_getStepNum:function(a){var b,c,d=0,e=m.getSkippedStepsIndexes(),f=e.length;for(c=0;f>c;c++)b=e[c],a>b&&d++;return a-d},_getStepI18nNum:function(a){var b=h.getI18NString("stepNums");return b&&au||u>=t.steps.length?null:t.steps[u]},G=function(){z.nextStep()},H=function(a){var b,c,d,e,f,g,i=C(),j=i.element,k=h.getPixelValue(j.style.top),l=k+h.getPixelValue(j.offsetHeight),m=h.getStepTarget(F()),o=m.getBoundingClientRect(),p=o.top+h.getScrollTop(),r=o.bottom+h.getScrollTop(),t=p>k?k:p,u=l>r?l:r,v=h.getScrollTop(),w=v+h.getWindowHeight(),x=t-E("scrollTopMargin");t>=v&&(t<=v+E("scrollTopMargin")||w>=u)?a&&a():E("smoothScroll")?("undefined"==typeof YAHOO?"undefined":n(YAHOO))!==q&&n(YAHOO.env)!==q&&n(YAHOO.env.ua)!==q&&n(YAHOO.util)!==q&&n(YAHOO.util.Scroll)!==q?(b=YAHOO.env.ua.webkit?document.body:document.documentElement,d=YAHOO.util.Easing?YAHOO.util.Easing.easeOut:void 0,c=new YAHOO.util.Scroll(b,{scroll:{to:[0,x]}},E("scrollDuration")/1e3,d),c.onComplete.subscribe(a),c.animate()):s?jQuery("body, html").animate({scrollTop:x},E("scrollDuration"),a):(0>x&&(x=0),e=v>t?-1:1,f=Math.abs(v-x)/(E("scrollDuration")/10),(g=function(){var b=h.getScrollTop(),c=b+e*f;return e>0&&c>=x||0>e&&x>=c?(c=x,a&&a(),void window.scrollTo(0,c)):(window.scrollTo(0,c),h.getScrollTop()===b?void(a&&a()):void setTimeout(g,10))})()):(window.scrollTo(0,x),a&&a())},I=function P(a,b){var c,d,e;u+a>=0&&u+a0?d.multipage:u>0&&t.steps[u-1].multipage,f=function(c){var f;if(-1===c)return this.endTour(!0);if(a&&(f=b>0?h.invokeEventCallbacks("next",d.onNext):h.invokeEventCallbacks("prev",d.onPrev)),c===u){if(e)return void N();f=h.valOrDefault(f,!0),f?this.showStep(c):this.endTour(!1)}},!e&&E("skipIfNoElement"))I(b,function(a){f.call(i,a)});else if(u+b>=0&&u+b2&&(B=d[2].split(",")),x=parseInt(x,10)),this},L=function(a,b,c){var d,e;if(u=a||0,A=b||{},d=F(),e=h.getStepTarget(d))return void c(u);if(!e){if(h.invokeEventCallbacks("error"),A[u]=!0,E("skipIfNoElement"))return void I(1,c);u=-1,c(u)}},M=function(a){function b(){d.show(),h.invokeEventCallbacks("show",c.onShow)}var c=t.steps[a],d=C(),e=h.getStepTarget(c);u!==a&&F().nextOnTargetClick&&h.removeEvtListener(h.getStepTarget(F()),"click",G),u=a,d.hide(!1),d.render(c,a,function(a){a?H(b):b(),c.nextOnTargetClick&&h.addEvtListener(e,"click",G)}),N()},N=function(){var a=t.id+":"+u,b=m.getSkippedStepsIndexes();b&&b.length>0&&(a+=":"+b.join(",")),h.setState(E("cookieName"),a,1)},O=function(a){a&&this.configure(a)};this.getCalloutManager=function(){return("undefined"==typeof k?"undefined":n(k))===q&&(k=new c),k},this.startTour=function(a,b){var c,d,e={},f=this;if(!t){if(!a)throw new Error("Tour data is required for startTour.");if(!a.id||!v.test(a.id))throw new Error("Tour ID is using an invalid format. Use alphanumeric, underscores, and/or hyphens only. First character must be a letter.");t=a,K.call(this,a)}if(("undefined"==typeof b?"undefined":n(b))!==q){if(b>=t.steps.length)throw new Error("Specified step number out of bounds.");d=b}if(!h.documentIsReady())return r=!0,this;if("undefined"==typeof d&&t.id===w&&("undefined"==typeof x?"undefined":n(x))!==q){if(d=x,B.length>0)for(var g=0,i=B.length;i>g;g++)e[B[g]]=!0}else d||(d=0);return L(d,e,function(a){var b=-1!==a&&h.getStepTarget(t.steps[a]);return b?(h.invokeEventCallbacks("start"),c=C(),c.hide(!1),f.isActive=!0,void(h.getStepTarget(F())?f.showStep(a):(h.invokeEventCallbacks("error"),E("skipIfNoElement")&&f.nextStep(!1)))):void f.endTour(!1,!1)}),this},this.showStep=function(a){var b=t.steps[a],c=u;return h.getStepTarget(b)?(b.delay?setTimeout(function(){M(a)},b.delay):M(a),this):(u=a,h.invokeEventCallbacks("error"),void(u=c))},this.prevStep=function(a){return J.call(this,a,-1),this},this.nextStep=function(a){return J.call(this,a,1),this},this.endTour=function(a,b){var c,d=C();return a=h.valOrDefault(a,!0),b=h.valOrDefault(b,!0),t&&(c=F(),c&&c.nextOnTargetClick&&h.removeEvtListener(h.getStepTarget(c),"click",G)),u=0,x=void 0,d.hide(),a&&h.clearState(E("cookieName")),this.isActive&&(this.isActive=!1,t&&b&&h.invokeEventCallbacks("end")),this.removeCallbacks(null,!0),this.resetDefaultOptions(),D(),t=null,this},this.getCurrTour=function(){return t},this.getCurrTarget=function(){return h.getStepTarget(F())},this.getCurrStepNum=function(){return u},this.getSkippedStepsIndexes=function(){var a,b=[];for(a in A)b.push(a);return b},this.refreshBubblePosition=function(){var a=F();return a&&C().setPosition(a),this.getCalloutManager().refreshCalloutPositions(),this},this.listen=function(a,b,c){return a&&i[a].push({cb:b,fromTour:c}),this},this.unlisten=function(a,b){var c,d,e=i[a];for(c=0,d=e.length;d>c;++c)e[c].cb===b&&e.splice(c,1);return this},this.removeCallbacks=function(a,b){var c,d,e,f;for(f in i)if(!a||a===f)if(b)for(c=i[f],d=0,e=c.length;e>d;++d)c[d].fromTour&&(c.splice(d--,1),--e);else i[f]=[];return this},this.registerHelper=function(a,b){"string"==typeof a&&"function"==typeof b&&(j[a]=b)},this.unregisterHelper=function(a){j[a]=null},this.invokeHelper=function(a){var b,c,d=[];for(b=1,c=arguments.length;c>b;++b)d.push(arguments[b]);j[a]&&j[a].call(null,d)},this.setCookieName=function(a){return p.cookieName=a,this},this.resetDefaultOptions=function(){return p={},this},this.resetDefaultI18N=function(){return e={},this},this.getState=function(){return h.getState(E("cookieName"))},y=function(a,b){var c,d,f,g,i=["next","prev","start","end","show","error","close"];for(p||this.resetDefaultOptions(),h.extend(p,a),a&&h.extend(e,a.i18n),f=0,g=i.length;g>f;++f)d="on"+i[f].charAt(0).toUpperCase()+i[f].substring(1),a[d]&&this.listen(i[f],a[d],b);return c=C(!0),this},this.configure=function(a){return y.call(this,a,!1)},this.setRenderer=function(a){var b="undefined"==typeof a?"undefined":n(a);return"string"===b?(o=a,f=void 0):"function"===b&&(f=a),this},this.setEscaper=function(a){return"function"==typeof a&&(g=a),this},O.call(this,a)},m=new a,function(){var a={};a.escape=function(a){return g?g(a):null==a?"":(""+a).replace(new RegExp("[&<>\"']","g"),function(a){return"&"==a?"&":"<"==a?"<":">"==a?">":'"'==a?""":"'"==a?"'":void 0})},this.templates=this.templates||{},this.templates.bubble_default=function(b){function c(b,c){return c?a.escape(b):b}var d,e="";a.escape,Array.prototype.join;e+="\n";var f=b.i18n,g=b.buttons,h=b.step,i=b.tour;return e+='\n
              \n ',i.isTour&&(e+=''+(null==(d=f.stepNum)?"":d)+""),e+='\n
              \n ',""!==h.title&&(e+='

              '+(null==(d=c(h.title,i.unsafe))?"":d)+"

              "),e+="\n ",""!==h.content&&(e+='
              '+(null==(d=c(h.content,i.unsafe))?"":d)+"
              "),e+='\n
              \n
              \n ',g.showPrev&&(e+='"),e+="\n ",g.showCTA&&(e+='"),e+="\n ",g.showNext&&(e+='"),e+="\n
              \n ",g.showClose&&(e+='"),e+='\n
              \n
              \n
              \n
              \n
              \n'}}.call(m);var y=m;return y}); \ No newline at end of file diff --git a/seleniumbase/resources/introjs/intro.min.js b/seleniumbase/resources/introjs/intro.min.js deleted file mode 100644 index ec140f409fa..00000000000 --- a/seleniumbase/resources/introjs/intro.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t(),module.exports.introJs=function(){return console.warn('Deprecated: please use require("intro.js") directly, instead of the introJs method of the function'),t().apply(this,arguments)};else if("function"==typeof define&&define.amd)define([],t);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).introJs=t()}}(function(){function n(t){this._targetElement=t,this._introItems=[],this._options={nextLabel:"Next →",prevLabel:"← Back",skipLabel:"Skip",doneLabel:"Done",hidePrev:!1,hideNext:!1,tooltipPosition:"bottom",tooltipClass:"",highlightClass:"",exitOnEsc:!0,exitOnOverlayClick:!0,showStepNumbers:!0,keyboardNavigation:!0,showButtons:!0,showBullets:!0,showProgress:!1,scrollToElement:!0,scrollTo:"element",scrollPadding:30,overlayOpacity:.8,positionPrecedence:["bottom","top","right","left"],disableInteraction:!1,helperElementPadding:10,hintPosition:"top-middle",hintButtonLabel:"Got it",hintAnimation:!0,buttonClass:"introjs-button"}}function e(t,i){var e=t.querySelectorAll("*[data-intro]"),n=[];if(this._options.steps)B(this._options.steps,function(t){var e=h(t);if(e.step=n.length+1,"string"==typeof e.element&&(e.element=document.querySelector(e.element)),void 0===e.element||null===e.element){var i=document.querySelector(".introjsFloatingElement");null===i&&((i=document.createElement("div")).className="introjsFloatingElement",document.body.appendChild(i)),e.element=i,e.position="floating"}e.scrollTo=e.scrollTo||this._options.scrollTo,void 0===e.disableInteraction&&(e.disableInteraction=this._options.disableInteraction),null!==e.element&&n.push(e)}.bind(this));else{var o;if(e.length<1)return!1;B(e,function(t){if((!i||t.getAttribute("data-intro-group")===i)&&"none"!==t.style.display){var e=parseInt(t.getAttribute("data-step"),10);o=void 0!==t.getAttribute("data-disable-interaction")?!!t.getAttribute("data-disable-interaction"):this._options.disableInteraction,0o.height&&m(n,"bottom");r.top-s<0&&m(n,"top");r.right+l>o.width&&m(n,"right");r.left-l<0&&m(n,"left");var c=(h=i||"",u=h.indexOf("-"),-1!==u?h.substr(u):"");var h,u;i&&(i=i.split("-")[0]);n.length&&(a="auto"!==i&&-1a.height?(i.className="introjs-arrow left-bottom",e.style.top="-"+(l.height-r.height-20)+"px"):i.className="introjs-arrow left";break;case"left":o||!0!==this._options.showStepNumbers||(e.style.top="15px"),r.top+l.height>a.height?(e.style.top="-"+(l.height-r.height-20)+"px",i.className="introjs-arrow right-bottom"):i.className="introjs-arrow right",e.style.right=r.width+20+"px";break;case"floating":i.style.display="none",e.style.left="50%",e.style.top="50%",e.style.marginLeft="-"+l.width/2+"px",e.style.marginTop="-"+l.height/2+"px",null!=n&&(n.style.left="-"+(l.width/2+18)+"px",n.style.top="-"+(l.height/2+18)+"px");break;case"bottom-right-aligned":i.className="introjs-arrow top-right",f(r,u=0,l,e),e.style.top=r.height+20+"px";break;case"bottom-middle-aligned":i.className="introjs-arrow top-middle",d=r.width/2-l.width/2,o&&(d+=5),f(r,d,l,e)&&(e.style.right=null,p(r,d,l,a,e)),e.style.top=r.height+20+"px";break;default:i.className="introjs-arrow top",p(r,0,l,a,e),e.style.top=r.height+20+"px"}}function p(t,e,i,n,o){return t.left+e+i.width>n.width?(o.style.left=n.width-i.width-t.left+"px",!1):(o.style.left=e+"px",!0)}function f(t,e,i,n){return t.left+t.width-e-i.width<0?(n.style.left=-t.left+"px",!1):(n.style.right=e+"px",!0)}function m(t,e){-1 a.active").className="",r.querySelector('.introjs-bullets li > a[data-stepnumber="'+o.step+'"]').className="active"),r.querySelector(".introjs-progress .introjs-progressbar").style.cssText="width:"+z.call(s)+"%;",r.querySelector(".introjs-progress .introjs-progressbar").setAttribute("aria-valuenow",z.call(s)),d.style.opacity=1,c&&(c.style.opacity=1),null!=i&&/introjs-donebutton/gi.test(i.className)?i.focus():null!=t&&t.focus(),P.call(s,o.scrollTo,o,h)},350)}else{var f=document.createElement("div"),m=document.createElement("div"),b=document.createElement("div"),g=document.createElement("div"),y=document.createElement("div"),v=document.createElement("div"),_=document.createElement("div"),w=document.createElement("div");f.className=a,m.className="introjs-tooltipReferenceLayer",(n=R(o.element))!==document.body&&V(n,o.element),T.call(s,f),T.call(s,m),this._targetElement.appendChild(f),this._targetElement.appendChild(m),b.className="introjs-arrow",y.className="introjs-tooltiptext",y.innerHTML=o.intro,!(v.className="introjs-bullets")===this._options.showBullets&&(v.style.display="none");var C=document.createElement("ul");C.setAttribute("role","tablist");var j=function(){s.goToStep(this.getAttribute("data-stepnumber"))};B(this._introItems,function(t,e){var i=document.createElement("li"),n=document.createElement("a");i.setAttribute("role","presentation"),n.setAttribute("role","tab"),n.onclick=j,e===o.step-1&&(n.className="active"),I(n),n.innerHTML=" ",n.setAttribute("data-stepnumber",t.step),i.appendChild(n),C.appendChild(i)}),v.appendChild(C),!(_.className="introjs-progress")===this._options.showProgress&&(_.style.display="none");var k=document.createElement("div");k.className="introjs-progressbar",k.setAttribute("role","progress"),k.setAttribute("aria-valuemin",0),k.setAttribute("aria-valuemax",100),k.setAttribute("aria-valuenow",z.call(this)),k.style.cssText="width:"+z.call(this)+"%;",_.appendChild(k),!(w.className="introjs-tooltipbuttons")===this._options.showButtons&&(w.style.display="none"),g.className="introjs-tooltip",g.appendChild(y),g.appendChild(v),g.appendChild(_);var x=document.createElement("span");!0===this._options.showStepNumbers&&(x.className="introjs-helperNumberLayer",x.innerHTML=o.step,m.appendChild(x)),g.appendChild(b),m.appendChild(g),(t=document.createElement("a")).onclick=function(){s._introItems.length-1!==s._currentStep&&E.call(s)},I(t),t.innerHTML=this._options.nextLabel,(e=document.createElement("a")).onclick=function(){0!==s._currentStep&&N.call(s)},I(e),e.innerHTML=this._options.prevLabel,(i=document.createElement("a")).className=this._options.buttonClass+" introjs-skipbutton ",I(i),i.innerHTML=this._options.skipLabel,i.onclick=function(){s._introItems.length-1===s._currentStep&&"function"==typeof s._introCompleteCallback&&s._introCompleteCallback.call(s),s._introItems.length-1!==s._currentStep&&"function"==typeof s._introExitCallback&&s._introExitCallback.call(s),"function"==typeof s._introSkipCallback&&s._introSkipCallback.call(s),A.call(s,s._targetElement)},w.appendChild(i),1l?window.scrollBy(0,n.top-(l/2-n.height/2)-this._options.scrollPadding):window.scrollBy(0,n.top-(l/2-n.height/2)+this._options.scrollPadding)}}function q(){B(document.querySelectorAll(".introjs-showElement"),function(t){O(t,/introjs-[a-zA-Z]+/g)})}function B(t,e,i){if(t)for(var n=0,o=t.length;ntd,tr.introjs-showElement>th{z-index:9999999!important}.introjs-disableInteraction{z-index:99999999!important;position:absolute;background-color:#fff;opacity:0}.introjs-relativePosition,tr.introjs-showElement>td,tr.introjs-showElement>th{position:relative}.introjs-helperLayer{box-sizing:content-box;position:absolute;z-index:9999998;background-color:#fff;background-color:rgba(255,255,255,.9);border:1px solid #777;border:1px solid rgba(0,0,0,.5);border-radius:4px;box-shadow:0 2px 15px rgba(0,0,0,.4);-webkit-transition:all .3s ease-out;-moz-transition:all .3s ease-out;-ms-transition:all .3s ease-out;-o-transition:all .3s ease-out;transition:all .3s ease-out}.introjs-tooltipReferenceLayer{box-sizing:content-box;position:absolute;visibility:hidden;z-index:100000000;background-color:transparent;-webkit-transition:all .3s ease-out;-moz-transition:all .3s ease-out;-ms-transition:all .3s ease-out;-o-transition:all .3s ease-out;transition:all .3s ease-out}.introjs-helperLayer *,.introjs-helperLayer :after,.introjs-helperLayer :before{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;-ms-box-sizing:content-box;-o-box-sizing:content-box;box-sizing:content-box}.introjs-helperNumberLayer{box-sizing:content-box;position:absolute;visibility:visible;top:-16px;left:-16px;z-index:9999999999!important;padding:2px;font-family:Arial,verdana,tahoma;font-size:13px;font-weight:700;color:#fff;text-align:center;text-shadow:1px 1px 1px rgba(0,0,0,.3);background:#ff3019;background:-webkit-linear-gradient(top,#ff3019 0,#cf0404 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#ff3019),color-stop(100%,#cf0404));background:-moz-linear-gradient(top,#ff3019 0,#cf0404 100%);background:-ms-linear-gradient(top,#ff3019 0,#cf0404 100%);background:-o-linear-gradient(top,#ff3019 0,#cf0404 100%);background:linear-gradient(to bottom,#ff3019 0,#cf0404 100%);width:20px;height:20px;line-height:20px;border:3px solid #fff;border-radius:50%;box-shadow:0 2px 5px rgba(0,0,0,.4)}.introjs-arrow{border:5px solid transparent;content:'';position:absolute}.introjs-arrow.top{top:-10px;border-bottom-color:#fff}.introjs-arrow.top-right{top:-10px;right:10px;border-bottom-color:#fff}.introjs-arrow.top-middle{top:-10px;left:50%;margin-left:-5px;border-bottom-color:#fff}.introjs-arrow.right{right:-10px;top:10px;border-left-color:#fff}.introjs-arrow.right-bottom{bottom:10px;right:-10px;border-left-color:#fff}.introjs-arrow.bottom{bottom:-10px;border-top-color:#fff}.introjs-arrow.bottom-right{bottom:-10px;right:10px;border-top-color:#fff}.introjs-arrow.bottom-middle{bottom:-10px;left:50%;margin-left:-5px;border-top-color:#fff}.introjs-arrow.left{left:-10px;top:10px;border-right-color:#fff}.introjs-arrow.left-bottom{left:-10px;bottom:10px;border-right-color:#fff}.introjs-tooltip{box-sizing:content-box;position:absolute;visibility:visible;padding:10px;background-color:#fff;min-width:200px;max-width:300px;border-radius:3px;box-shadow:0 1px 10px rgba(0,0,0,.4);-webkit-transition:opacity .1s ease-out;-moz-transition:opacity .1s ease-out;-ms-transition:opacity .1s ease-out;-o-transition:opacity .1s ease-out;transition:opacity .1s ease-out}.introjs-tooltipbuttons{text-align:right;white-space:nowrap}.introjs-button{box-sizing:content-box;position:relative;overflow:visible;display:inline-block;padding:.3em .8em;border:1px solid #d4d4d4;margin:0;text-decoration:none;text-shadow:1px 1px 0 #fff;font:11px/normal sans-serif;color:#333;white-space:nowrap;cursor:pointer;outline:0;background-color:#ececec;background-image:-webkit-gradient(linear,0 0,0 100%,from(#f4f4f4),to(#ececec));background-image:-moz-linear-gradient(#f4f4f4,#ececec);background-image:-o-linear-gradient(#f4f4f4,#ececec);background-image:linear-gradient(#f4f4f4,#ececec);-webkit-background-clip:padding;-moz-background-clip:padding;-o-background-clip:padding-box;-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em;zoom:1;margin-top:10px}.introjs-button:hover{border-color:#bcbcbc;text-decoration:none;box-shadow:0 1px 1px #e3e3e3}.introjs-button:active,.introjs-button:focus{background-image:-webkit-gradient(linear,0 0,0 100%,from(#ececec),to(#f4f4f4));background-image:-moz-linear-gradient(#ececec,#f4f4f4);background-image:-o-linear-gradient(#ececec,#f4f4f4);background-image:linear-gradient(#ececec,#f4f4f4)}.introjs-button::-moz-focus-inner{padding:0;border:0}.introjs-skipbutton{box-sizing:content-box;margin-right:5px;color:#7a7a7a}.introjs-prevbutton{-webkit-border-radius:.2em 0 0 .2em;-moz-border-radius:.2em 0 0 .2em;border-radius:.2em 0 0 .2em;border-right:none}.introjs-prevbutton.introjs-fullbutton{border:1px solid #d4d4d4;-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em}.introjs-nextbutton{-webkit-border-radius:0 .2em .2em 0;-moz-border-radius:0 .2em .2em 0;border-radius:0 .2em .2em 0}.introjs-nextbutton.introjs-fullbutton{-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em}.introjs-disabled,.introjs-disabled:focus,.introjs-disabled:hover{color:#9a9a9a;border-color:#d4d4d4;box-shadow:none;cursor:default;background-color:#f4f4f4;background-image:none;text-decoration:none}.introjs-hidden{display:none}.introjs-bullets{text-align:center}.introjs-bullets ul{box-sizing:content-box;clear:both;margin:15px auto 0;padding:0;display:inline-block}.introjs-bullets ul li{box-sizing:content-box;list-style:none;float:left;margin:0 2px}.introjs-bullets ul li a{box-sizing:content-box;display:block;width:6px;height:6px;background:#ccc;border-radius:10px;-moz-border-radius:10px;-webkit-border-radius:10px;text-decoration:none;cursor:pointer}.introjs-bullets ul li a:hover{background:#999}.introjs-bullets ul li a.active{background:#999}.introjs-progress{box-sizing:content-box;overflow:hidden;height:10px;margin:10px 0 5px 0;border-radius:4px;background-color:#ecf0f1}.introjs-progressbar{box-sizing:content-box;float:left;width:0%;height:100%;font-size:10px;line-height:10px;text-align:center;background-color:#08c}.introjsFloatingElement{position:absolute;height:0;width:0;left:50%;top:50%}.introjs-fixedTooltip{position:fixed}.introjs-hint{box-sizing:content-box;position:absolute;background:0 0;width:20px;height:15px;cursor:pointer}.introjs-hint:focus{border:0;outline:0}.introjs-hidehint{display:none}.introjs-fixedhint{position:fixed}.introjs-hint:hover>.introjs-hint-pulse{border:5px solid rgba(60,60,60,.57)}.introjs-hint-pulse{box-sizing:content-box;width:10px;height:10px;border:5px solid rgba(60,60,60,.27);-webkit-border-radius:30px;-moz-border-radius:30px;border-radius:30px;background-color:rgba(136,136,136,.24);z-index:10;position:absolute;-webkit-transition:all .2s ease-out;-moz-transition:all .2s ease-out;-ms-transition:all .2s ease-out;-o-transition:all .2s ease-out;transition:all .2s ease-out}.introjs-hint-no-anim .introjs-hint-dot{-webkit-animation:none;-moz-animation:none;animation:none}.introjs-hint-dot{box-sizing:content-box;border:10px solid rgba(146,146,146,.36);background:0 0;-webkit-border-radius:60px;-moz-border-radius:60px;border-radius:60px;height:50px;width:50px;-webkit-animation:introjspulse 3s ease-out;-moz-animation:introjspulse 3s ease-out;animation:introjspulse 3s ease-out;-webkit-animation-iteration-count:infinite;-moz-animation-iteration-count:infinite;animation-iteration-count:infinite;position:absolute;top:-25px;left:-25px;z-index:1;opacity:0}@-webkit-keyframes introjspulse{0%{-webkit-transform:scale(0);opacity:0}25%{-webkit-transform:scale(0);opacity:.1}50%{-webkit-transform:scale(.1);opacity:.3}75%{-webkit-transform:scale(.5);opacity:.5}100%{-webkit-transform:scale(1);opacity:0}}@-moz-keyframes introjspulse{0%{-moz-transform:scale(0);opacity:0}25%{-moz-transform:scale(0);opacity:.1}50%{-moz-transform:scale(.1);opacity:.3}75%{-moz-transform:scale(.5);opacity:.5}100%{-moz-transform:scale(1);opacity:0}}@keyframes introjspulse{0%{transform:scale(0);opacity:0}25%{transform:scale(0);opacity:.1}50%{transform:scale(.1);opacity:.3}75%{transform:scale(.5);opacity:.5}100%{transform:scale(1);opacity:0}} diff --git a/seleniumbase/resources/jquery_confirm/jquery-confirm.min.css b/seleniumbase/resources/jquery_confirm/jquery-confirm.min.css deleted file mode 100644 index 400f0b8d257..00000000000 --- a/seleniumbase/resources/jquery_confirm/jquery-confirm.min.css +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * jquery-confirm v3.3.2 (http://craftpip.github.io/jquery-confirm/) - * Author: boniface pereira - * Website: www.craftpip.com - * Contact: hey@craftpip.com - * - * Copyright 2013-2017 jquery-confirm - * Licensed under MIT (https://github.com/craftpip/jquery-confirm/blob/master/LICENSE) - */@-webkit-keyframes jconfirm-spin{from{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes jconfirm-spin{from{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}body[class*=jconfirm-no-scroll-]{overflow:hidden!important}.jconfirm{position:fixed;top:0;left:0;right:0;bottom:0;z-index:99999999;font-family:inherit;overflow:hidden}.jconfirm .jconfirm-bg{position:fixed;top:0;left:0;right:0;bottom:0;-webkit-transition:opacity .4s;transition:opacity .4s}.jconfirm .jconfirm-bg.jconfirm-bg-h{opacity:0!important}.jconfirm .jconfirm-scrollpane{-webkit-perspective:500px;perspective:500px;-webkit-perspective-origin:center;perspective-origin:center;display:table;width:100%;height:100%}.jconfirm .jconfirm-row{display:table-row;width:100%}.jconfirm .jconfirm-cell{display:table-cell;vertical-align:middle}.jconfirm .jconfirm-holder{max-height:100%;padding:50px 0}.jconfirm .jconfirm-box-container{-webkit-transition:-webkit-transform;transition:-webkit-transform;transition:transform;transition:transform,-webkit-transform}.jconfirm .jconfirm-box-container.jconfirm-no-transition{-webkit-transition:none!important;transition:none!important}.jconfirm .jconfirm-box{background:white;border-radius:4px;position:relative;outline:0;padding:15px 15px 0;overflow:hidden;margin-left:auto;margin-right:auto}@-webkit-keyframes type-blue{1%,100%{border-color:#3498db}50%{border-color:#5faee3}}@keyframes type-blue{1%,100%{border-color:#3498db}50%{border-color:#5faee3}}@-webkit-keyframes type-green{1%,100%{border-color:#2ecc71}50%{border-color:#54d98c}}@keyframes type-green{1%,100%{border-color:#2ecc71}50%{border-color:#54d98c}}@-webkit-keyframes type-red{1%,100%{border-color:#e74c3c}50%{border-color:#ed7669}}@keyframes type-red{1%,100%{border-color:#e74c3c}50%{border-color:#ed7669}}@-webkit-keyframes type-orange{1%,100%{border-color:#f1c40f}50%{border-color:#f4d03f}}@keyframes type-orange{1%,100%{border-color:#f1c40f}50%{border-color:#f4d03f}}@-webkit-keyframes type-purple{1%,100%{border-color:#9b59b6}50%{border-color:#b07cc6}}@keyframes type-purple{1%,100%{border-color:#9b59b6}50%{border-color:#b07cc6}}@-webkit-keyframes type-dark{1%,100%{border-color:#34495e}50%{border-color:#46627f}}@keyframes type-dark{1%,100%{border-color:#34495e}50%{border-color:#46627f}}.jconfirm .jconfirm-box.jconfirm-type-animated{-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.jconfirm .jconfirm-box.jconfirm-type-blue{border-top:solid 7px #3498db;-webkit-animation-name:type-blue;animation-name:type-blue}.jconfirm .jconfirm-box.jconfirm-type-green{border-top:solid 7px #2ecc71;-webkit-animation-name:type-green;animation-name:type-green}.jconfirm .jconfirm-box.jconfirm-type-red{border-top:solid 7px #e74c3c;-webkit-animation-name:type-red;animation-name:type-red}.jconfirm .jconfirm-box.jconfirm-type-orange{border-top:solid 7px #f1c40f;-webkit-animation-name:type-orange;animation-name:type-orange}.jconfirm .jconfirm-box.jconfirm-type-purple{border-top:solid 7px #9b59b6;-webkit-animation-name:type-purple;animation-name:type-purple}.jconfirm .jconfirm-box.jconfirm-type-dark{border-top:solid 7px #34495e;-webkit-animation-name:type-dark;animation-name:type-dark}.jconfirm .jconfirm-box.loading{height:120px}.jconfirm .jconfirm-box.loading:before{content:'';position:absolute;left:0;background:white;right:0;top:0;bottom:0;border-radius:10px;z-index:1}.jconfirm .jconfirm-box.loading:after{opacity:.6;content:'';height:30px;width:30px;border:solid 3px transparent;position:absolute;left:50%;margin-left:-15px;border-radius:50%;-webkit-animation:jconfirm-spin 1s infinite linear;animation:jconfirm-spin 1s infinite linear;border-bottom-color:dodgerblue;top:50%;margin-top:-15px;z-index:2}.jconfirm .jconfirm-box div.jconfirm-closeIcon{height:20px;width:20px;position:absolute;top:10px;right:10px;cursor:pointer;opacity:.6;text-align:center;font-size:27px!important;line-height:14px!important;display:none;z-index:1}.jconfirm .jconfirm-box div.jconfirm-closeIcon:empty{display:none}.jconfirm .jconfirm-box div.jconfirm-closeIcon .fa{font-size:16px}.jconfirm .jconfirm-box div.jconfirm-closeIcon .glyphicon{font-size:16px}.jconfirm .jconfirm-box div.jconfirm-closeIcon .zmdi{font-size:16px}.jconfirm .jconfirm-box div.jconfirm-closeIcon:hover{opacity:1}.jconfirm .jconfirm-box div.jconfirm-title-c{display:block;font-size:22px;line-height:20px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default;padding-bottom:15px}.jconfirm .jconfirm-box div.jconfirm-title-c.jconfirm-hand{cursor:move}.jconfirm .jconfirm-box div.jconfirm-title-c .jconfirm-icon-c{font-size:inherit;display:inline-block;vertical-align:middle}.jconfirm .jconfirm-box div.jconfirm-title-c .jconfirm-icon-c i{vertical-align:middle}.jconfirm .jconfirm-box div.jconfirm-title-c .jconfirm-icon-c:empty{display:none}.jconfirm .jconfirm-box div.jconfirm-title-c .jconfirm-title{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;font-size:inherit;font-family:inherit;display:inline-block;vertical-align:middle}.jconfirm .jconfirm-box div.jconfirm-title-c .jconfirm-title:empty{display:none}.jconfirm .jconfirm-box div.jconfirm-content-pane{margin-bottom:15px;height:auto;-webkit-transition:height .4s ease-in;transition:height .4s ease-in;display:inline-block;width:100%;position:relative;overflow-x:hidden;overflow-y:auto}.jconfirm .jconfirm-box div.jconfirm-content-pane.no-scroll{overflow-y:hidden}.jconfirm .jconfirm-box div.jconfirm-content-pane::-webkit-scrollbar{width:3px}.jconfirm .jconfirm-box div.jconfirm-content-pane::-webkit-scrollbar-track{background:rgba(0,0,0,0.1)}.jconfirm .jconfirm-box div.jconfirm-content-pane::-webkit-scrollbar-thumb{background:#666;border-radius:3px}.jconfirm .jconfirm-box div.jconfirm-content-pane .jconfirm-content{overflow:auto}.jconfirm .jconfirm-box div.jconfirm-content-pane .jconfirm-content img{max-width:100%;height:auto}.jconfirm .jconfirm-box div.jconfirm-content-pane .jconfirm-content:empty{display:none}.jconfirm .jconfirm-box .jconfirm-buttons{padding-bottom:11px}.jconfirm .jconfirm-box .jconfirm-buttons>button{margin-bottom:4px;margin-left:2px;margin-right:2px}.jconfirm .jconfirm-box .jconfirm-buttons button{display:inline-block;padding:6px 12px;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border-radius:4px;min-height:1em;-webkit-transition:opacity .1s ease,background-color .1s ease,color .1s ease,background .1s ease,-webkit-box-shadow .1s ease;transition:opacity .1s ease,background-color .1s ease,color .1s ease,background .1s ease,-webkit-box-shadow .1s ease;transition:opacity .1s ease,background-color .1s ease,color .1s ease,box-shadow .1s ease,background .1s ease;transition:opacity .1s ease,background-color .1s ease,color .1s ease,box-shadow .1s ease,background .1s ease,-webkit-box-shadow .1s ease;-webkit-tap-highlight-color:transparent;border:0;background-image:none}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-blue{background-color:#3498db;color:#FFF;text-shadow:none;-webkit-transition:background .2s;transition:background .2s}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-blue:hover{background-color:#2980b9;color:#FFF}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-green{background-color:#2ecc71;color:#FFF;text-shadow:none;-webkit-transition:background .2s;transition:background .2s}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-green:hover{background-color:#27ae60;color:#FFF}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-red{background-color:#e74c3c;color:#FFF;text-shadow:none;-webkit-transition:background .2s;transition:background .2s}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-red:hover{background-color:#c0392b;color:#FFF}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-orange{background-color:#f1c40f;color:#FFF;text-shadow:none;-webkit-transition:background .2s;transition:background .2s}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-orange:hover{background-color:#f39c12;color:#FFF}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-default{background-color:#ecf0f1;color:#000;text-shadow:none;-webkit-transition:background .2s;transition:background .2s}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-default:hover{background-color:#bdc3c7;color:#000}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-purple{background-color:#9b59b6;color:#FFF;text-shadow:none;-webkit-transition:background .2s;transition:background .2s}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-purple:hover{background-color:#8e44ad;color:#FFF}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-dark{background-color:#34495e;color:#FFF;text-shadow:none;-webkit-transition:background .2s;transition:background .2s}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-dark:hover{background-color:#2c3e50;color:#FFF}.jconfirm .jconfirm-box.jconfirm-type-red .jconfirm-title-c .jconfirm-icon-c{color:#e74c3c!important}.jconfirm .jconfirm-box.jconfirm-type-blue .jconfirm-title-c .jconfirm-icon-c{color:#3498db!important}.jconfirm .jconfirm-box.jconfirm-type-green .jconfirm-title-c .jconfirm-icon-c{color:#2ecc71!important}.jconfirm .jconfirm-box.jconfirm-type-purple .jconfirm-title-c .jconfirm-icon-c{color:#9b59b6!important}.jconfirm .jconfirm-box.jconfirm-type-orange .jconfirm-title-c .jconfirm-icon-c{color:#f1c40f!important}.jconfirm .jconfirm-box.jconfirm-type-dark .jconfirm-title-c .jconfirm-icon-c{color:#34495e!important}.jconfirm .jconfirm-clear{clear:both}.jconfirm.jconfirm-rtl{direction:rtl}.jconfirm.jconfirm-rtl div.jconfirm-closeIcon{left:5px;right:auto}.jconfirm.jconfirm-white .jconfirm-bg,.jconfirm.jconfirm-light .jconfirm-bg{background-color:#444;opacity:.2}.jconfirm.jconfirm-white .jconfirm-box,.jconfirm.jconfirm-light .jconfirm-box{-webkit-box-shadow:0 2px 6px rgba(0,0,0,0.2);box-shadow:0 2px 6px rgba(0,0,0,0.2);border-radius:5px}.jconfirm.jconfirm-white .jconfirm-box .jconfirm-title-c .jconfirm-icon-c,.jconfirm.jconfirm-light .jconfirm-box .jconfirm-title-c .jconfirm-icon-c{margin-right:8px;margin-left:0}.jconfirm.jconfirm-white .jconfirm-box .jconfirm-buttons,.jconfirm.jconfirm-light .jconfirm-box .jconfirm-buttons{float:right}.jconfirm.jconfirm-white .jconfirm-box .jconfirm-buttons button,.jconfirm.jconfirm-light .jconfirm-box .jconfirm-buttons button{text-transform:uppercase;font-size:14px;font-weight:bold;text-shadow:none}.jconfirm.jconfirm-white .jconfirm-box .jconfirm-buttons button.btn-default,.jconfirm.jconfirm-light .jconfirm-box .jconfirm-buttons button.btn-default{-webkit-box-shadow:none;box-shadow:none;color:#333}.jconfirm.jconfirm-white .jconfirm-box .jconfirm-buttons button.btn-default:hover,.jconfirm.jconfirm-light .jconfirm-box .jconfirm-buttons button.btn-default:hover{background:#ddd}.jconfirm.jconfirm-white.jconfirm-rtl .jconfirm-title-c .jconfirm-icon-c,.jconfirm.jconfirm-light.jconfirm-rtl .jconfirm-title-c .jconfirm-icon-c{margin-left:8px;margin-right:0}.jconfirm.jconfirm-black .jconfirm-bg,.jconfirm.jconfirm-dark .jconfirm-bg{background-color:darkslategray;opacity:.4}.jconfirm.jconfirm-black .jconfirm-box,.jconfirm.jconfirm-dark .jconfirm-box{-webkit-box-shadow:0 2px 6px rgba(0,0,0,0.2);box-shadow:0 2px 6px rgba(0,0,0,0.2);background:#444;border-radius:5px;color:white}.jconfirm.jconfirm-black .jconfirm-box .jconfirm-title-c .jconfirm-icon-c,.jconfirm.jconfirm-dark .jconfirm-box .jconfirm-title-c .jconfirm-icon-c{margin-right:8px;margin-left:0}.jconfirm.jconfirm-black .jconfirm-box .jconfirm-buttons,.jconfirm.jconfirm-dark .jconfirm-box .jconfirm-buttons{float:right}.jconfirm.jconfirm-black .jconfirm-box .jconfirm-buttons button,.jconfirm.jconfirm-dark .jconfirm-box .jconfirm-buttons button{border:0;background-image:none;text-transform:uppercase;font-size:14px;font-weight:bold;text-shadow:none;-webkit-transition:background .1s;transition:background .1s;color:white}.jconfirm.jconfirm-black .jconfirm-box .jconfirm-buttons button.btn-default,.jconfirm.jconfirm-dark .jconfirm-box .jconfirm-buttons button.btn-default{-webkit-box-shadow:none;box-shadow:none;color:#fff;background:0}.jconfirm.jconfirm-black .jconfirm-box .jconfirm-buttons button.btn-default:hover,.jconfirm.jconfirm-dark .jconfirm-box .jconfirm-buttons button.btn-default:hover{background:#666}.jconfirm.jconfirm-black.jconfirm-rtl .jconfirm-title-c .jconfirm-icon-c,.jconfirm.jconfirm-dark.jconfirm-rtl .jconfirm-title-c .jconfirm-icon-c{margin-left:8px;margin-right:0}.jconfirm .jconfirm-box.hilight.jconfirm-hilight-shake{-webkit-animation:shake .82s cubic-bezier(0.36,0.07,0.19,0.97) both;animation:shake .82s cubic-bezier(0.36,0.07,0.19,0.97) both;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.jconfirm .jconfirm-box.hilight.jconfirm-hilight-glow{-webkit-animation:glow .82s cubic-bezier(0.36,0.07,0.19,0.97) both;animation:glow .82s cubic-bezier(0.36,0.07,0.19,0.97) both;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}@-webkit-keyframes shake{10%,90%{-webkit-transform:translate3d(-2px,0,0);transform:translate3d(-2px,0,0)}20%,80%{-webkit-transform:translate3d(4px,0,0);transform:translate3d(4px,0,0)}30%,50%,70%{-webkit-transform:translate3d(-8px,0,0);transform:translate3d(-8px,0,0)}40%,60%{-webkit-transform:translate3d(8px,0,0);transform:translate3d(8px,0,0)}}@keyframes shake{10%,90%{-webkit-transform:translate3d(-2px,0,0);transform:translate3d(-2px,0,0)}20%,80%{-webkit-transform:translate3d(4px,0,0);transform:translate3d(4px,0,0)}30%,50%,70%{-webkit-transform:translate3d(-8px,0,0);transform:translate3d(-8px,0,0)}40%,60%{-webkit-transform:translate3d(8px,0,0);transform:translate3d(8px,0,0)}}@-webkit-keyframes glow{0%,100%{-webkit-box-shadow:0 0 0 red;box-shadow:0 0 0 red}50%{-webkit-box-shadow:0 0 30px red;box-shadow:0 0 30px red}}@keyframes glow{0%,100%{-webkit-box-shadow:0 0 0 red;box-shadow:0 0 0 red}50%{-webkit-box-shadow:0 0 30px red;box-shadow:0 0 30px red}}.jconfirm{-webkit-perspective:400px;perspective:400px}.jconfirm .jconfirm-box{opacity:1;-webkit-transition-property:all;transition-property:all}.jconfirm .jconfirm-box.jconfirm-animation-top,.jconfirm .jconfirm-box.jconfirm-animation-left,.jconfirm .jconfirm-box.jconfirm-animation-right,.jconfirm .jconfirm-box.jconfirm-animation-bottom,.jconfirm .jconfirm-box.jconfirm-animation-opacity,.jconfirm .jconfirm-box.jconfirm-animation-zoom,.jconfirm .jconfirm-box.jconfirm-animation-scale,.jconfirm .jconfirm-box.jconfirm-animation-none,.jconfirm .jconfirm-box.jconfirm-animation-rotate,.jconfirm .jconfirm-box.jconfirm-animation-rotatex,.jconfirm .jconfirm-box.jconfirm-animation-rotatey,.jconfirm .jconfirm-box.jconfirm-animation-scaley,.jconfirm .jconfirm-box.jconfirm-animation-scalex{opacity:0}.jconfirm .jconfirm-box.jconfirm-animation-rotate{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.jconfirm .jconfirm-box.jconfirm-animation-rotatex{-webkit-transform:rotateX(90deg);transform:rotateX(90deg);-webkit-transform-origin:center;transform-origin:center}.jconfirm .jconfirm-box.jconfirm-animation-rotatexr{-webkit-transform:rotateX(-90deg);transform:rotateX(-90deg);-webkit-transform-origin:center;transform-origin:center}.jconfirm .jconfirm-box.jconfirm-animation-rotatey{-webkit-transform:rotatey(90deg);transform:rotatey(90deg);-webkit-transform-origin:center;transform-origin:center}.jconfirm .jconfirm-box.jconfirm-animation-rotateyr{-webkit-transform:rotatey(-90deg);transform:rotatey(-90deg);-webkit-transform-origin:center;transform-origin:center}.jconfirm .jconfirm-box.jconfirm-animation-scaley{-webkit-transform:scaley(1.5);transform:scaley(1.5);-webkit-transform-origin:center;transform-origin:center}.jconfirm .jconfirm-box.jconfirm-animation-scalex{-webkit-transform:scalex(1.5);transform:scalex(1.5);-webkit-transform-origin:center;transform-origin:center}.jconfirm .jconfirm-box.jconfirm-animation-top{-webkit-transform:translate(0px,-100px);transform:translate(0px,-100px)}.jconfirm .jconfirm-box.jconfirm-animation-left{-webkit-transform:translate(-100px,0px);transform:translate(-100px,0px)}.jconfirm .jconfirm-box.jconfirm-animation-right{-webkit-transform:translate(100px,0px);transform:translate(100px,0px)}.jconfirm .jconfirm-box.jconfirm-animation-bottom{-webkit-transform:translate(0px,100px);transform:translate(0px,100px)}.jconfirm .jconfirm-box.jconfirm-animation-zoom{-webkit-transform:scale(1.2);transform:scale(1.2)}.jconfirm .jconfirm-box.jconfirm-animation-scale{-webkit-transform:scale(0.5);transform:scale(0.5)}.jconfirm .jconfirm-box.jconfirm-animation-none{visibility:hidden}.jconfirm.jconfirm-supervan .jconfirm-bg{background-color:rgba(54,70,93,0.95)}.jconfirm.jconfirm-supervan .jconfirm-box{background-color:transparent}.jconfirm.jconfirm-supervan .jconfirm-box.jconfirm-type-blue{border:0}.jconfirm.jconfirm-supervan .jconfirm-box.jconfirm-type-green{border:0}.jconfirm.jconfirm-supervan .jconfirm-box.jconfirm-type-red{border:0}.jconfirm.jconfirm-supervan .jconfirm-box.jconfirm-type-orange{border:0}.jconfirm.jconfirm-supervan .jconfirm-box.jconfirm-type-purple{border:0}.jconfirm.jconfirm-supervan .jconfirm-box.jconfirm-type-dark{border:0}.jconfirm.jconfirm-supervan .jconfirm-box div.jconfirm-closeIcon{color:white}.jconfirm.jconfirm-supervan .jconfirm-box div.jconfirm-title-c{text-align:center;color:white;font-size:28px;font-weight:normal}.jconfirm.jconfirm-supervan .jconfirm-box div.jconfirm-title-c>*{padding-bottom:25px}.jconfirm.jconfirm-supervan .jconfirm-box div.jconfirm-title-c .jconfirm-icon-c{margin-right:8px;margin-left:0}.jconfirm.jconfirm-supervan .jconfirm-box div.jconfirm-content-pane{margin-bottom:25px}.jconfirm.jconfirm-supervan .jconfirm-box div.jconfirm-content{text-align:center;color:white}.jconfirm.jconfirm-supervan .jconfirm-box .jconfirm-buttons{text-align:center}.jconfirm.jconfirm-supervan .jconfirm-box .jconfirm-buttons button{font-size:16px;border-radius:2px;background:#303f53;text-shadow:none;border:0;color:white;padding:10px;min-width:100px}.jconfirm.jconfirm-supervan.jconfirm-rtl .jconfirm-box div.jconfirm-title-c .jconfirm-icon-c{margin-left:8px;margin-right:0}.jconfirm.jconfirm-material .jconfirm-bg{background-color:rgba(0,0,0,0.67)}.jconfirm.jconfirm-material .jconfirm-box{background-color:white;-webkit-box-shadow:0 7px 8px -4px rgba(0,0,0,0.2),0 13px 19px 2px rgba(0,0,0,0.14),0 5px 24px 4px rgba(0,0,0,0.12);box-shadow:0 7px 8px -4px rgba(0,0,0,0.2),0 13px 19px 2px rgba(0,0,0,0.14),0 5px 24px 4px rgba(0,0,0,0.12);padding:30px 25px 10px 25px}.jconfirm.jconfirm-material .jconfirm-box .jconfirm-title-c .jconfirm-icon-c{margin-right:8px;margin-left:0}.jconfirm.jconfirm-material .jconfirm-box div.jconfirm-closeIcon{color:rgba(0,0,0,0.87)}.jconfirm.jconfirm-material .jconfirm-box div.jconfirm-title-c{color:rgba(0,0,0,0.87);font-size:22px;font-weight:bold}.jconfirm.jconfirm-material .jconfirm-box div.jconfirm-content{color:rgba(0,0,0,0.87)}.jconfirm.jconfirm-material .jconfirm-box .jconfirm-buttons{text-align:right}.jconfirm.jconfirm-material .jconfirm-box .jconfirm-buttons button{text-transform:uppercase;font-weight:500}.jconfirm.jconfirm-material.jconfirm-rtl .jconfirm-title-c .jconfirm-icon-c{margin-left:8px;margin-right:0}.jconfirm.jconfirm-bootstrap .jconfirm-bg{background-color:rgba(0,0,0,0.21)}.jconfirm.jconfirm-bootstrap .jconfirm-box{background-color:white;-webkit-box-shadow:0 3px 8px 0 rgba(0,0,0,0.2);box-shadow:0 3px 8px 0 rgba(0,0,0,0.2);border:solid 1px rgba(0,0,0,0.4);padding:15px 0 0}.jconfirm.jconfirm-bootstrap .jconfirm-box .jconfirm-title-c .jconfirm-icon-c{margin-right:8px;margin-left:0}.jconfirm.jconfirm-bootstrap .jconfirm-box div.jconfirm-closeIcon{color:rgba(0,0,0,0.87)}.jconfirm.jconfirm-bootstrap .jconfirm-box div.jconfirm-title-c{color:rgba(0,0,0,0.87);font-size:22px;font-weight:bold;padding-left:15px;padding-right:15px}.jconfirm.jconfirm-bootstrap .jconfirm-box div.jconfirm-content{color:rgba(0,0,0,0.87);padding:0 15px}.jconfirm.jconfirm-bootstrap .jconfirm-box .jconfirm-buttons{text-align:right;padding:10px;margin:-5px 0 0;border-top:solid 1px #ddd;overflow:hidden;border-radius:0 0 4px 4px}.jconfirm.jconfirm-bootstrap .jconfirm-box .jconfirm-buttons button{font-weight:500}.jconfirm.jconfirm-bootstrap.jconfirm-rtl .jconfirm-title-c .jconfirm-icon-c{margin-left:8px;margin-right:0}.jconfirm.jconfirm-modern .jconfirm-bg{background-color:slategray;opacity:.6}.jconfirm.jconfirm-modern .jconfirm-box{background-color:white;-webkit-box-shadow:0 7px 8px -4px rgba(0,0,0,0.2),0 13px 19px 2px rgba(0,0,0,0.14),0 5px 24px 4px rgba(0,0,0,0.12);box-shadow:0 7px 8px -4px rgba(0,0,0,0.2),0 13px 19px 2px rgba(0,0,0,0.14),0 5px 24px 4px rgba(0,0,0,0.12);padding:30px 30px 15px}.jconfirm.jconfirm-modern .jconfirm-box div.jconfirm-closeIcon{color:rgba(0,0,0,0.87);top:15px;right:15px}.jconfirm.jconfirm-modern .jconfirm-box div.jconfirm-title-c{color:rgba(0,0,0,0.87);font-size:24px;font-weight:bold;text-align:center;margin-bottom:10px}.jconfirm.jconfirm-modern .jconfirm-box div.jconfirm-title-c .jconfirm-icon-c{-webkit-transition:-webkit-transform .5s;transition:-webkit-transform .5s;transition:transform .5s;transition:transform .5s,-webkit-transform .5s;-webkit-transform:scale(0);transform:scale(0);display:block;margin-right:0;margin-left:0;margin-bottom:10px;font-size:69px;color:#aaa}.jconfirm.jconfirm-modern .jconfirm-box div.jconfirm-content{text-align:center;font-size:15px;color:#777;margin-bottom:25px}.jconfirm.jconfirm-modern .jconfirm-box .jconfirm-buttons{text-align:center}.jconfirm.jconfirm-modern .jconfirm-box .jconfirm-buttons button{font-weight:bold;text-transform:uppercase;-webkit-transition:background .1s;transition:background .1s;padding:10px 20px}.jconfirm.jconfirm-modern .jconfirm-box .jconfirm-buttons button+button{margin-left:4px}.jconfirm.jconfirm-modern.jconfirm-open .jconfirm-box .jconfirm-title-c .jconfirm-icon-c{-webkit-transform:scale(1);transform:scale(1)} \ No newline at end of file diff --git a/seleniumbase/resources/jquery_confirm/jquery-confirm.min.js b/seleniumbase/resources/jquery_confirm/jquery-confirm.min.js deleted file mode 100644 index a2498bba815..00000000000 --- a/seleniumbase/resources/jquery_confirm/jquery-confirm.min.js +++ /dev/null @@ -1,10 +0,0 @@ -/*! - * jquery-confirm v3.3.2 (http://craftpip.github.io/jquery-confirm/) - * Author: Boniface Pereira - * Website: www.craftpip.com - * Contact: hey@craftpip.com - * - * Copyright 2013-2017 jquery-confirm - * Licensed under MIT (https://github.com/craftpip/jquery-confirm/blob/master/LICENSE) - */ -if(typeof jQuery==="undefined"){throw new Error("jquery-confirm requires jQuery");}var jconfirm,Jconfirm;(function($,window){$.fn.confirm=function(options,option2){if(typeof options==="undefined"){options={};}if(typeof options==="string"){options={content:options,title:(option2)?option2:false};}$(this).each(function(){var $this=$(this);if($this.attr("jc-attached")){console.warn("jConfirm has already been attached to this element ",$this[0]);return;}$this.on("click",function(e){e.preventDefault();var jcOption=$.extend({},options);if($this.attr("data-title")){jcOption.title=$this.attr("data-title");}if($this.attr("data-content")){jcOption.content=$this.attr("data-content");}if(typeof jcOption.buttons=="undefined"){jcOption.buttons={};}jcOption["$target"]=$this;if($this.attr("href")&&Object.keys(jcOption.buttons).length==0){var buttons=$.extend(true,{},jconfirm.pluginDefaults.defaultButtons,(jconfirm.defaults||{}).defaultButtons||{});var firstBtn=Object.keys(buttons)[0];jcOption.buttons=buttons;jcOption.buttons[firstBtn].action=function(){location.href=$this.attr("href");};}jcOption.closeIcon=false;var instance=$.confirm(jcOption);});$this.attr("jc-attached",true);});return $(this);};$.confirm=function(options,option2){if(typeof options==="undefined"){options={};}if(typeof options==="string"){options={content:options,title:(option2)?option2:false};}var putDefaultButtons=!(options.buttons==false);if(typeof options.buttons!="object"){options.buttons={};}if(Object.keys(options.buttons).length==0&&putDefaultButtons){var buttons=$.extend(true,{},jconfirm.pluginDefaults.defaultButtons,(jconfirm.defaults||{}).defaultButtons||{});options.buttons=buttons;}return jconfirm(options);};$.alert=function(options,option2){if(typeof options==="undefined"){options={};}if(typeof options==="string"){options={content:options,title:(option2)?option2:false};}var putDefaultButtons=!(options.buttons==false);if(typeof options.buttons!="object"){options.buttons={};}if(Object.keys(options.buttons).length==0&&putDefaultButtons){var buttons=$.extend(true,{},jconfirm.pluginDefaults.defaultButtons,(jconfirm.defaults||{}).defaultButtons||{});var firstBtn=Object.keys(buttons)[0];options.buttons[firstBtn]=buttons[firstBtn];}return jconfirm(options);};$.dialog=function(options,option2){if(typeof options==="undefined"){options={};}if(typeof options==="string"){options={content:options,title:(option2)?option2:false,closeIcon:function(){}};}options.buttons={};if(typeof options.closeIcon=="undefined"){options.closeIcon=function(){};}options.confirmKeys=[13];return jconfirm(options);};jconfirm=function(options){if(typeof options==="undefined"){options={};}var pluginOptions=$.extend(true,{},jconfirm.pluginDefaults);if(jconfirm.defaults){pluginOptions=$.extend(true,pluginOptions,jconfirm.defaults);}pluginOptions=$.extend(true,{},pluginOptions,options);var instance=new Jconfirm(pluginOptions);jconfirm.instances.push(instance);return instance;};Jconfirm=function(options){$.extend(this,options);this._init();};Jconfirm.prototype={_init:function(){var that=this;if(!jconfirm.instances.length){jconfirm.lastFocused=$("body").find(":focus");}this._id=Math.round(Math.random()*99999);this.contentParsed=$(document.createElement("div"));if(!this.lazyOpen){setTimeout(function(){that.open();},0);}},_buildHTML:function(){var that=this;this._parseAnimation(this.animation,"o");this._parseAnimation(this.closeAnimation,"c");this._parseBgDismissAnimation(this.backgroundDismissAnimation);this._parseColumnClass(this.columnClass);this._parseTheme(this.theme);this._parseType(this.type);var template=$(this.template);template.find(".jconfirm-box").addClass(this.animationParsed).addClass(this.backgroundDismissAnimationParsed).addClass(this.typeParsed);if(this.typeAnimated){template.find(".jconfirm-box").addClass("jconfirm-type-animated");}if(this.useBootstrap){template.find(".jc-bs3-row").addClass(this.bootstrapClasses.row);template.find(".jc-bs3-row").addClass("justify-content-md-center justify-content-sm-center justify-content-xs-center justify-content-lg-center");template.find(".jconfirm-box-container").addClass(this.columnClassParsed);if(this.containerFluid){template.find(".jc-bs3-container").addClass(this.bootstrapClasses.containerFluid);}else{template.find(".jc-bs3-container").addClass(this.bootstrapClasses.container);}}else{template.find(".jconfirm-box").css("width",this.boxWidth);}if(this.titleClass){template.find(".jconfirm-title-c").addClass(this.titleClass);}template.addClass(this.themeParsed);var ariaLabel="jconfirm-box"+this._id;template.find(".jconfirm-box").attr("aria-labelledby",ariaLabel).attr("tabindex",-1);template.find(".jconfirm-content").attr("id",ariaLabel);if(this.bgOpacity!==null){template.find(".jconfirm-bg").css("opacity",this.bgOpacity);}if(this.rtl){template.addClass("jconfirm-rtl");}this.$el=template.appendTo(this.container);this.$jconfirmBoxContainer=this.$el.find(".jconfirm-box-container");this.$jconfirmBox=this.$body=this.$el.find(".jconfirm-box");this.$jconfirmBg=this.$el.find(".jconfirm-bg");this.$title=this.$el.find(".jconfirm-title");this.$titleContainer=this.$el.find(".jconfirm-title-c");this.$content=this.$el.find("div.jconfirm-content");this.$contentPane=this.$el.find(".jconfirm-content-pane");this.$icon=this.$el.find(".jconfirm-icon-c");this.$closeIcon=this.$el.find(".jconfirm-closeIcon");this.$holder=this.$el.find(".jconfirm-holder");this.$btnc=this.$el.find(".jconfirm-buttons");this.$scrollPane=this.$el.find(".jconfirm-scrollpane");that.setStartingPoint();this._contentReady=$.Deferred();this._modalReady=$.Deferred();this.$holder.css({"padding-top":this.offsetTop,"padding-bottom":this.offsetBottom,});this.setTitle();this.setIcon();this._setButtons();this._parseContent();this.initDraggable();if(this.isAjax){this.showLoading(false);}$.when(this._contentReady,this._modalReady).then(function(){if(that.isAjaxLoading){setTimeout(function(){that.isAjaxLoading=false;that.setContent();that.setTitle();that.setIcon();setTimeout(function(){that.hideLoading(false);that._updateContentMaxHeight();},100);if(typeof that.onContentReady==="function"){that.onContentReady();}},50);}else{that._updateContentMaxHeight();that.setTitle();that.setIcon();if(typeof that.onContentReady==="function"){that.onContentReady();}}if(that.autoClose){that._startCountDown();}});this._watchContent();if(this.animation==="none"){this.animationSpeed=1;this.animationBounce=1;}this.$body.css(this._getCSS(this.animationSpeed,this.animationBounce));this.$contentPane.css(this._getCSS(this.animationSpeed,1));this.$jconfirmBg.css(this._getCSS(this.animationSpeed,1));this.$jconfirmBoxContainer.css(this._getCSS(this.animationSpeed,1));},_typePrefix:"jconfirm-type-",typeParsed:"",_parseType:function(type){this.typeParsed=this._typePrefix+type;},setType:function(type){var oldClass=this.typeParsed;this._parseType(type);this.$jconfirmBox.removeClass(oldClass).addClass(this.typeParsed);},themeParsed:"",_themePrefix:"jconfirm-",setTheme:function(theme){var previous=this.theme;this.theme=theme||this.theme;this._parseTheme(this.theme);if(previous){this.$el.removeClass(previous);}this.$el.addClass(this.themeParsed);this.theme=theme;},_parseTheme:function(theme){var that=this;theme=theme.split(",");$.each(theme,function(k,a){if(a.indexOf(that._themePrefix)===-1){theme[k]=that._themePrefix+$.trim(a);}});this.themeParsed=theme.join(" ").toLowerCase();},backgroundDismissAnimationParsed:"",_bgDismissPrefix:"jconfirm-hilight-",_parseBgDismissAnimation:function(bgDismissAnimation){var animation=bgDismissAnimation.split(",");var that=this;$.each(animation,function(k,a){if(a.indexOf(that._bgDismissPrefix)===-1){animation[k]=that._bgDismissPrefix+$.trim(a);}});this.backgroundDismissAnimationParsed=animation.join(" ").toLowerCase();},animationParsed:"",closeAnimationParsed:"",_animationPrefix:"jconfirm-animation-",setAnimation:function(animation){this.animation=animation||this.animation;this._parseAnimation(this.animation,"o");},_parseAnimation:function(animation,which){which=which||"o";var animations=animation.split(",");var that=this;$.each(animations,function(k,a){if(a.indexOf(that._animationPrefix)===-1){animations[k]=that._animationPrefix+$.trim(a);}});var a_string=animations.join(" ").toLowerCase();if(which==="o"){this.animationParsed=a_string;}else{this.closeAnimationParsed=a_string;}return a_string;},setCloseAnimation:function(closeAnimation){this.closeAnimation=closeAnimation||this.closeAnimation;this._parseAnimation(this.closeAnimation,"c");},setAnimationSpeed:function(speed){this.animationSpeed=speed||this.animationSpeed;},columnClassParsed:"",setColumnClass:function(colClass){if(!this.useBootstrap){console.warn("cannot set columnClass, useBootstrap is set to false");return;}this.columnClass=colClass||this.columnClass;this._parseColumnClass(this.columnClass);this.$jconfirmBoxContainer.addClass(this.columnClassParsed);},_updateContentMaxHeight:function(){var height=$(window).height()-(this.$jconfirmBox.outerHeight()-this.$contentPane.outerHeight())-(this.offsetTop+this.offsetBottom);this.$contentPane.css({"max-height":height+"px"});},setBoxWidth:function(width){if(this.useBootstrap){console.warn("cannot set boxWidth, useBootstrap is set to true");return;}this.boxWidth=width;this.$jconfirmBox.css("width",width);},_parseColumnClass:function(colClass){colClass=colClass.toLowerCase();var p;switch(colClass){case"xl":case"xlarge":p="col-md-12";break;case"l":case"large":p="col-md-8 col-md-offset-2";break;case"m":case"medium":p="col-md-6 col-md-offset-3";break;case"s":case"small":p="col-md-4 col-md-offset-4";break;case"xs":case"xsmall":p="col-md-2 col-md-offset-5";break;default:p=colClass;}this.columnClassParsed=p;},initDraggable:function(){var that=this;var $t=this.$titleContainer;this.resetDrag();if(this.draggable){$t.on("mousedown",function(e){$t.addClass("jconfirm-hand");that.mouseX=e.clientX;that.mouseY=e.clientY;that.isDrag=true;});$(window).on("mousemove."+this._id,function(e){if(that.isDrag){that.movingX=e.clientX-that.mouseX+that.initialX;that.movingY=e.clientY-that.mouseY+that.initialY;that.setDrag();}});$(window).on("mouseup."+this._id,function(){$t.removeClass("jconfirm-hand");if(that.isDrag){that.isDrag=false;that.initialX=that.movingX;that.initialY=that.movingY;}});}},resetDrag:function(){this.isDrag=false;this.initialX=0;this.initialY=0;this.movingX=0;this.movingY=0;this.mouseX=0;this.mouseY=0;this.$jconfirmBoxContainer.css("transform","translate("+0+"px, "+0+"px)");},setDrag:function(){if(!this.draggable){return;}this.alignMiddle=false;var boxWidth=this.$jconfirmBox.outerWidth();var boxHeight=this.$jconfirmBox.outerHeight();var windowWidth=$(window).width();var windowHeight=$(window).height();var that=this;var dragUpdate=1;if(that.movingX%dragUpdate===0||that.movingY%dragUpdate===0){if(that.dragWindowBorder){var leftDistance=(windowWidth/2)-boxWidth/2;var topDistance=(windowHeight/2)-boxHeight/2;topDistance-=that.dragWindowGap;leftDistance-=that.dragWindowGap;if(leftDistance+that.movingX<0){that.movingX=-leftDistance;}else{if(leftDistance-that.movingX<0){that.movingX=leftDistance;}}if(topDistance+that.movingY<0){that.movingY=-topDistance;}else{if(topDistance-that.movingY<0){that.movingY=topDistance;}}}that.$jconfirmBoxContainer.css("transform","translate("+that.movingX+"px, "+that.movingY+"px)");}},_scrollTop:function(){if(typeof pageYOffset!=="undefined"){return pageYOffset;}else{var B=document.body;var D=document.documentElement;D=(D.clientHeight)?D:B;return D.scrollTop;}},_watchContent:function(){var that=this;if(this._timer){clearInterval(this._timer);}var prevContentHeight=0;this._timer=setInterval(function(){if(that.smoothContent){var contentHeight=that.$content.outerHeight()||0;if(contentHeight!==prevContentHeight){that.$contentPane.css({height:contentHeight}).scrollTop(0);prevContentHeight=contentHeight;}var wh=$(window).height();var total=that.offsetTop+that.offsetBottom+that.$jconfirmBox.height()-that.$contentPane.height()+that.$content.height();if(total').html(that.buttons[key].text).addClass(that.buttons[key].btnClass).prop("disabled",that.buttons[key].isDisabled).css("display",that.buttons[key].isHidden?"none":"").click(function(e){e.preventDefault();var res=that.buttons[key].action.apply(that,[that.buttons[key]]);that.onAction.apply(that,[key,that.buttons[key]]);that._stopCountDown();if(typeof res==="undefined"||res){that.close();}});that.buttons[key].el=button_element;that.buttons[key].setText=function(text){button_element.html(text);};that.buttons[key].addClass=function(className){button_element.addClass(className);};that.buttons[key].removeClass=function(className){button_element.removeClass(className);};that.buttons[key].disable=function(){that.buttons[key].isDisabled=true;button_element.prop("disabled",true);};that.buttons[key].enable=function(){that.buttons[key].isDisabled=false;button_element.prop("disabled",false);};that.buttons[key].show=function(){that.buttons[key].isHidden=false;button_element.css("display","");};that.buttons[key].hide=function(){that.buttons[key].isHidden=true;button_element.css("display","none");};that["$_"+key]=that["$$"+key]=button_element;that.$btnc.append(button_element);});if(total_buttons===0){this.$btnc.hide();}if(this.closeIcon===null&&total_buttons===0){this.closeIcon=true;}if(this.closeIcon){if(this.closeIconClass){var closeHtml='';this.$closeIcon.html(closeHtml);}this.$closeIcon.click(function(e){e.preventDefault();var buttonName=false;var shouldClose=false;var str;if(typeof that.closeIcon=="function"){str=that.closeIcon();}else{str=that.closeIcon;}if(typeof str=="string"&&typeof that.buttons[str]!="undefined"){buttonName=str;shouldClose=false;}else{if(typeof str=="undefined"||!!(str)==true){shouldClose=true;}else{shouldClose=false;}}if(buttonName){var btnResponse=that.buttons[buttonName].action.apply(that);shouldClose=(typeof btnResponse=="undefined")||!!(btnResponse);}if(shouldClose){that.close();}});this.$closeIcon.show();}else{this.$closeIcon.hide();}},setTitle:function(string,force){force=force||false;if(typeof string!=="undefined"){if(typeof string=="string"){this.title=string;}else{if(typeof string=="function"){if(typeof string.promise=="function"){console.error("Promise was returned from title function, this is not supported.");}var response=string();if(typeof response=="string"){this.title=response;}else{this.title=false;}}else{this.title=false;}}}if(this.isAjaxLoading&&!force){return;}this.$title.html(this.title||"");this.updateTitleContainer();},setIcon:function(iconClass,force){force=force||false;if(typeof iconClass!=="undefined"){if(typeof iconClass=="string"){this.icon=iconClass;}else{if(typeof iconClass==="function"){var response=iconClass();if(typeof response=="string"){this.icon=response;}else{this.icon=false;}}else{this.icon=false;}}}if(this.isAjaxLoading&&!force){return;}this.$icon.html(this.icon?'':"");this.updateTitleContainer();},updateTitleContainer:function(){if(!this.title&&!this.icon){this.$titleContainer.hide();}else{this.$titleContainer.show();}},setContentPrepend:function(content,force){if(!content){return;}this.contentParsed.prepend(content);},setContentAppend:function(content){if(!content){return;}this.contentParsed.append(content);},setContent:function(content,force){force=!!force;var that=this;if(content){this.contentParsed.html("").append(content);}if(this.isAjaxLoading&&!force){return;}this.$content.html("");this.$content.append(this.contentParsed);setTimeout(function(){that.$body.find("input[autofocus]:visible:first").focus();},100);},loadingSpinner:false,showLoading:function(disableButtons){this.loadingSpinner=true;this.$jconfirmBox.addClass("loading");if(disableButtons){this.$btnc.find("button").prop("disabled",true);}},hideLoading:function(enableButtons){this.loadingSpinner=false;this.$jconfirmBox.removeClass("loading");if(enableButtons){this.$btnc.find("button").prop("disabled",false);}},ajaxResponse:false,contentParsed:"",isAjax:false,isAjaxLoading:false,_parseContent:function(){var that=this;var e=" ";if(typeof this.content=="function"){var res=this.content.apply(this);if(typeof res=="string"){this.content=res;}else{if(typeof res=="object"&&typeof res.always=="function"){this.isAjax=true;this.isAjaxLoading=true;res.always(function(data,status,xhr){that.ajaxResponse={data:data,status:status,xhr:xhr};that._contentReady.resolve(data,status,xhr);if(typeof that.contentLoaded=="function"){that.contentLoaded(data,status,xhr);}});this.content=e;}else{this.content=e;}}}if(typeof this.content=="string"&&this.content.substr(0,4).toLowerCase()==="url:"){this.isAjax=true;this.isAjaxLoading=true;var u=this.content.substring(4,this.content.length);$.get(u).done(function(html){that.contentParsed.html(html);}).always(function(data,status,xhr){that.ajaxResponse={data:data,status:status,xhr:xhr};that._contentReady.resolve(data,status,xhr);if(typeof that.contentLoaded=="function"){that.contentLoaded(data,status,xhr);}});}if(!this.content){this.content=e;}if(!this.isAjax){this.contentParsed.html(this.content);this.setContent();that._contentReady.resolve();}},_stopCountDown:function(){clearInterval(this.autoCloseInterval);if(this.$cd){this.$cd.remove();}},_startCountDown:function(){var that=this;var opt=this.autoClose.split("|");if(opt.length!==2){console.error("Invalid option for autoClose. example 'close|10000'");return false;}var button_key=opt[0];var time=parseInt(opt[1]);if(typeof this.buttons[button_key]==="undefined"){console.error("Invalid button key '"+button_key+"' for autoClose");return false;}var seconds=Math.ceil(time/1000);this.$cd=$(' ('+seconds+")").appendTo(this["$_"+button_key]);this.autoCloseInterval=setInterval(function(){that.$cd.html(" ("+(seconds-=1)+") ");if(seconds<=0){that["$$"+button_key].trigger("click");that._stopCountDown();}},1000);},_getKey:function(key){switch(key){case 192:return"tilde";case 13:return"enter";case 16:return"shift";case 9:return"tab";case 20:return"capslock";case 17:return"ctrl";case 91:return"win";case 18:return"alt";case 27:return"esc";case 32:return"space";}var initial=String.fromCharCode(key);if(/^[A-z0-9]+$/.test(initial)){return initial.toLowerCase();}else{return false;}},reactOnKey:function(e){var that=this;var a=$(".jconfirm");if(a.eq(a.length-1)[0]!==this.$el[0]){return false;}var key=e.which;if(this.$content.find(":input").is(":focus")&&/13|32/.test(key)){return false;}var keyChar=this._getKey(key);if(keyChar==="esc"&&this.escapeKey){if(this.escapeKey===true){this.$scrollPane.trigger("click");}else{if(typeof this.escapeKey==="string"||typeof this.escapeKey==="function"){var buttonKey;if(typeof this.escapeKey==="function"){buttonKey=this.escapeKey();}else{buttonKey=this.escapeKey;}if(buttonKey){if(typeof this.buttons[buttonKey]==="undefined"){console.warn("Invalid escapeKey, no buttons found with key "+buttonKey);}else{this["$_"+buttonKey].trigger("click");}}}}}$.each(this.buttons,function(key,button){if(button.keys.indexOf(keyChar)!=-1){that["$_"+key].trigger("click");}});},setDialogCenter:function(){console.info("setDialogCenter is deprecated, dialogs are centered with CSS3 tables");},_unwatchContent:function(){clearInterval(this._timer);},close:function(onClosePayload){var that=this;if(typeof this.onClose==="function"){this.onClose(onClosePayload);}this._unwatchContent();$(window).unbind("resize."+this._id);$(window).unbind("keyup."+this._id);$(window).unbind("jcKeyDown."+this._id);if(this.draggable){$(window).unbind("mousemove."+this._id);$(window).unbind("mouseup."+this._id);this.$titleContainer.unbind("mousedown");}that.$el.removeClass(that.loadedClass);$("body").removeClass("jconfirm-no-scroll-"+that._id);that.$jconfirmBoxContainer.removeClass("jconfirm-no-transition");setTimeout(function(){that.$body.addClass(that.closeAnimationParsed);that.$jconfirmBg.addClass("jconfirm-bg-h");var closeTimer=(that.closeAnimation==="none")?1:that.animationSpeed;setTimeout(function(){that.$el.remove();var l=jconfirm.instances;var i=jconfirm.instances.length-1;for(i;i>=0;i--){if(jconfirm.instances[i]._id===that._id){jconfirm.instances.splice(i,1);}}if(!jconfirm.instances.length){if(that.scrollToPreviousElement&&jconfirm.lastFocused&&jconfirm.lastFocused.length&&$.contains(document,jconfirm.lastFocused[0])){var $lf=jconfirm.lastFocused;if(that.scrollToPreviousElementAnimate){var st=$(window).scrollTop();var ot=jconfirm.lastFocused.offset().top;var wh=$(window).height();if(!(ot>st&&ot<(st+wh))){var scrollTo=(ot-Math.round((wh/3)));$("html, body").animate({scrollTop:scrollTo},that.animationSpeed,"swing",function(){$lf.focus();});}else{$lf.focus();}}else{$lf.focus();}jconfirm.lastFocused=false;}}if(typeof that.onDestroy==="function"){that.onDestroy();}},closeTimer*0.4);},50);return true;},open:function(){if(this.isOpen()){return false;}this._buildHTML();this._bindEvents();this._open();return true;},setStartingPoint:function(){var el=false;if(this.animateFromElement!==true&&this.animateFromElement){el=this.animateFromElement;jconfirm.lastClicked=false;}else{if(jconfirm.lastClicked&&this.animateFromElement===true){el=jconfirm.lastClicked;jconfirm.lastClicked=false;}else{return false;}}if(!el){return false;}var offset=el.offset();var iTop=el.outerHeight()/2;var iLeft=el.outerWidth()/2;iTop-=this.$jconfirmBox.outerHeight()/2;iLeft-=this.$jconfirmBox.outerWidth()/2;var sourceTop=offset.top+iTop;sourceTop=sourceTop-this._scrollTop();var sourceLeft=offset.left+iLeft;var wh=$(window).height()/2;var ww=$(window).width()/2;var targetH=wh-this.$jconfirmBox.outerHeight()/2;var targetW=ww-this.$jconfirmBox.outerWidth()/2;sourceTop-=targetH;sourceLeft-=targetW;if(Math.abs(sourceTop)>wh||Math.abs(sourceLeft)>ww){return false;}this.$jconfirmBoxContainer.css("transform","translate("+sourceLeft+"px, "+sourceTop+"px)");},_open:function(){var that=this;if(typeof that.onOpenBefore==="function"){that.onOpenBefore();}this.$body.removeClass(this.animationParsed);this.$jconfirmBg.removeClass("jconfirm-bg-h");this.$body.focus();that.$jconfirmBoxContainer.css("transform","translate("+0+"px, "+0+"px)");setTimeout(function(){that.$body.css(that._getCSS(that.animationSpeed,1));that.$body.css({"transition-property":that.$body.css("transition-property")+", margin"});that.$jconfirmBoxContainer.addClass("jconfirm-no-transition");that._modalReady.resolve();if(typeof that.onOpen==="function"){that.onOpen();}that.$el.addClass(that.loadedClass);},this.animationSpeed);},loadedClass:"jconfirm-open",isClosed:function(){return !this.$el||this.$el.css("display")==="";},isOpen:function(){return !this.isClosed();},toggle:function(){if(!this.isOpen()){this.open();}else{this.close();}}};jconfirm.instances=[];jconfirm.lastFocused=false;jconfirm.pluginDefaults={template:'
              ',title:"Hello",titleClass:"",type:"default",typeAnimated:true,draggable:true,dragWindowGap:15,dragWindowBorder:true,animateFromElement:true,alignMiddle:true,smoothContent:true,content:"Are you sure to continue?",buttons:{},defaultButtons:{ok:{action:function(){}},close:{action:function(){}}},contentLoaded:function(){},icon:"",lazyOpen:false,bgOpacity:null,theme:"light",animation:"scale",closeAnimation:"scale",animationSpeed:400,animationBounce:1,escapeKey:true,rtl:false,container:"body",containerFluid:false,backgroundDismiss:false,backgroundDismissAnimation:"shake",autoClose:false,closeIcon:null,closeIconClass:false,watchInterval:100,columnClass:"col-md-4 col-md-offset-4 col-sm-6 col-sm-offset-3 col-xs-10 col-xs-offset-1",boxWidth:"50%",scrollToPreviousElement:true,scrollToPreviousElementAnimate:true,useBootstrap:true,offsetTop:40,offsetBottom:40,bootstrapClasses:{container:"container",containerFluid:"container-fluid",row:"row"},onContentReady:function(){},onOpenBefore:function(){},onOpen:function(){},onClose:function(){},onDestroy:function(){},onAction:function(){}};var keyDown=false;$(window).on("keydown",function(e){if(!keyDown){var $target=$(e.target);var pass=false;if($target.closest(".jconfirm-box").length){pass=true;}if(pass){$(window).trigger("jcKeyDown");}keyDown=true;}});$(window).on("keyup",function(){keyDown=false;});jconfirm.lastClicked=false;$(document).on("mousedown","button, a",function(){jconfirm.lastClicked=$(this);});})(jQuery,window); \ No newline at end of file diff --git a/seleniumbase/resources/messenger/messenger-spinner.css b/seleniumbase/resources/messenger/messenger-spinner.css deleted file mode 100644 index 313349fb309..00000000000 --- a/seleniumbase/resources/messenger/messenger-spinner.css +++ /dev/null @@ -1,165 +0,0 @@ -@-webkit-keyframes ui-spinner-rotate-right { - 0% { - -webkit-transform: rotate(0deg); - } - 25% { - -webkit-transform: rotate(180deg); - } - 50% { - -webkit-transform: rotate(180deg); - } - 75% { - -webkit-transform: rotate(360deg); - } - 100% { - -webkit-transform: rotate(360deg); - } -} -@-webkit-keyframes ui-spinner-rotate-left { - 0% { - -webkit-transform: rotate(0deg); - } - 25% { - -webkit-transform: rotate(0deg); - } - 50% { - -webkit-transform: rotate(180deg); - } - 75% { - -webkit-transform: rotate(180deg); - } - 100% { - -webkit-transform: rotate(360deg); - } -} -@-moz-keyframes ui-spinner-rotate-right { - 0% { - -moz-transform: rotate(0deg); - } - 25% { - -moz-transform: rotate(180deg); - } - 50% { - -moz-transform: rotate(180deg); - } - 75% { - -moz-transform: rotate(360deg); - } - 100% { - -moz-transform: rotate(360deg); - } -} -@-moz-keyframes ui-spinner-rotate-left { - 0% { - -moz-transform: rotate(0deg); - } - 25% { - -moz-transform: rotate(0deg); - } - 50% { - -moz-transform: rotate(180deg); - } - 75% { - -moz-transform: rotate(180deg); - } - 100% { - -moz-transform: rotate(360deg); - } -} -@keyframes ui-spinner-rotate-right { - 0% { - transform: rotate(0deg); - } - 25% { - transform: rotate(180deg); - } - 50% { - transform: rotate(180deg); - } - 75% { - transform: rotate(360deg); - } - 100% { - transform: rotate(360deg); - } -} -@keyframes ui-spinner-rotate-left { - 0% { - transform: rotate(0deg); - } - 25% { - transform: rotate(0deg); - } - 50% { - transform: rotate(180deg); - } - 75% { - transform: rotate(180deg); - } - 100% { - transform: rotate(360deg); - } -} -/* line 118, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner { - position: relative; - border-radius: 100%; -} -/* line 122, ../../src/sass/messenger-spinner.scss */ -ul.messenger.messenger-spinner-active .messenger-spinner .messenger-spinner { - display: block; -} -/* line 126, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side { - width: 50%; - height: 100%; - overflow: hidden; - position: absolute; -} -/* line 132, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side .messenger-spinner-fill { - border-radius: 999px; - position: absolute; - width: 100%; - height: 100%; - -moz-animation-iteration-count: infinite; - -webkit-animation-iteration-count: infinite; - animation-iteration-count: infinite; - -moz-animation-timing-function: linear; - -webkit-animation-timing-function: linear; - animation-timing-function: linear; -} -/* line 142, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side-left { - left: 0; -} -/* line 145, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side-left .messenger-spinner-fill { - left: 100%; - border-top-left-radius: 0; - border-bottom-left-radius: 0; - -moz-animation-name: ui-spinner-rotate-left; - -webkit-animation-name: ui-spinner-rotate-left; - animation-name: ui-spinner-rotate-left; - -moz-transform-origin: 0 50% 50%; - -ms-transform-origin: 0 50% 50%; - -webkit-transform-origin: 0 50% 50%; - transform-origin: 0 50% 50%; -} -/* line 154, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side-right { - left: 50%; -} -/* line 157, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side-right .messenger-spinner-fill { - left: -100%; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - -moz-animation-name: ui-spinner-rotate-right; - -webkit-animation-name: ui-spinner-rotate-right; - animation-name: ui-spinner-rotate-right; - -moz-transform-origin: 100% 50% 50%; - -ms-transform-origin: 100% 50% 50%; - -webkit-transform-origin: 100% 50% 50%; - transform-origin: 100% 50% 50%; -} diff --git a/seleniumbase/resources/messenger/messenger-theme-air.css b/seleniumbase/resources/messenger/messenger-theme-air.css deleted file mode 100644 index 0420e4b203f..00000000000 --- a/seleniumbase/resources/messenger/messenger-theme-air.css +++ /dev/null @@ -1,359 +0,0 @@ -@import url("//fonts.googleapis.com/css?family=Raleway:400"); -@-webkit-keyframes ui-spinner-rotate-right { - 0% { - -webkit-transform: rotate(0deg); - } - 25% { - -webkit-transform: rotate(180deg); - } - 50% { - -webkit-transform: rotate(180deg); - } - 75% { - -webkit-transform: rotate(360deg); - } - 100% { - -webkit-transform: rotate(360deg); - } -} -@-webkit-keyframes ui-spinner-rotate-left { - 0% { - -webkit-transform: rotate(0deg); - } - 25% { - -webkit-transform: rotate(0deg); - } - 50% { - -webkit-transform: rotate(180deg); - } - 75% { - -webkit-transform: rotate(180deg); - } - 100% { - -webkit-transform: rotate(360deg); - } -} -@-moz-keyframes ui-spinner-rotate-right { - 0% { - -moz-transform: rotate(0deg); - } - 25% { - -moz-transform: rotate(180deg); - } - 50% { - -moz-transform: rotate(180deg); - } - 75% { - -moz-transform: rotate(360deg); - } - 100% { - -moz-transform: rotate(360deg); - } -} -@-moz-keyframes ui-spinner-rotate-left { - 0% { - -moz-transform: rotate(0deg); - } - 25% { - -moz-transform: rotate(0deg); - } - 50% { - -moz-transform: rotate(180deg); - } - 75% { - -moz-transform: rotate(180deg); - } - 100% { - -moz-transform: rotate(360deg); - } -} -@keyframes ui-spinner-rotate-right { - 0% { - transform: rotate(0deg); - } - 25% { - transform: rotate(180deg); - } - 50% { - transform: rotate(180deg); - } - 75% { - transform: rotate(360deg); - } - 100% { - transform: rotate(360deg); - } -} -@keyframes ui-spinner-rotate-left { - 0% { - transform: rotate(0deg); - } - 25% { - transform: rotate(0deg); - } - 50% { - transform: rotate(180deg); - } - 75% { - transform: rotate(180deg); - } - 100% { - transform: rotate(360deg); - } -} -/* line 118, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner { - position: relative; - border-radius: 100%; -} -/* line 122, ../../src/sass/messenger-spinner.scss */ -ul.messenger.messenger-spinner-active .messenger-spinner .messenger-spinner { - display: block; -} -/* line 126, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side { - width: 50%; - height: 100%; - overflow: hidden; - position: absolute; -} -/* line 132, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side .messenger-spinner-fill { - border-radius: 999px; - position: absolute; - width: 100%; - height: 100%; - -moz-animation-iteration-count: infinite; - -webkit-animation-iteration-count: infinite; - animation-iteration-count: infinite; - -moz-animation-timing-function: linear; - -webkit-animation-timing-function: linear; - animation-timing-function: linear; -} -/* line 142, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side-left { - left: 0; -} -/* line 145, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side-left .messenger-spinner-fill { - left: 100%; - border-top-left-radius: 0; - border-bottom-left-radius: 0; - -moz-animation-name: ui-spinner-rotate-left; - -webkit-animation-name: ui-spinner-rotate-left; - animation-name: ui-spinner-rotate-left; - -moz-transform-origin: 0 50% 50%; - -ms-transform-origin: 0 50% 50%; - -webkit-transform-origin: 0 50% 50%; - transform-origin: 0 50% 50%; -} -/* line 154, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side-right { - left: 50%; -} -/* line 157, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side-right .messenger-spinner-fill { - left: -100%; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - -moz-animation-name: ui-spinner-rotate-right; - -webkit-animation-name: ui-spinner-rotate-right; - animation-name: ui-spinner-rotate-right; - -moz-transform-origin: 100% 50% 50%; - -ms-transform-origin: 100% 50% 50%; - -webkit-transform-origin: 100% 50% 50%; - transform-origin: 100% 50% 50%; -} - -/* line 16, ../../src/sass/messenger-theme-air.sass */ -ul.messenger-theme-air { - -moz-user-select: none; - -webkit-user-select: none; - -o-user-select: none; - user-select: none; - font-family: "Raleway", sans-serif; -} -/* line 20, ../../src/sass/messenger-theme-air.sass */ -ul.messenger-theme-air .messenger-message { - -moz-transition: background-color 0.4s; - -o-transition: background-color 0.4s; - -webkit-transition: background-color 0.4s; - transition: background-color 0.4s; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; - border-radius: 5px; - -moz-box-shadow: inset 0 0 0 1px #fff, inset 0 2px #fff, 0 0 0 1px rgba(0, 0, 0, 0.1), 0 1px rgba(0, 0, 0, 0.2); - -webkit-box-shadow: inset 0 0 0 1px #fff, inset 0 2px #fff, 0 0 0 1px rgba(0, 0, 0, 0.1), 0 1px rgba(0, 0, 0, 0.2); - box-shadow: inset 0 0 0 1px #fff, inset 0 2px #fff, 0 0 0 1px rgba(0, 0, 0, 0.1), 0 1px rgba(0, 0, 0, 0.2); - border: 0px; - background-color: rgba(255, 255, 255, 0.8); - position: relative; - margin-bottom: 1em; - font-size: 13px; - color: #666; - font-weight: 500; - padding: 10px 30px 11px 46px; -} -/* line 33, ../../src/sass/messenger-theme-air.sass */ -ul.messenger-theme-air .messenger-message:hover { - background-color: white; -} -/* line 36, ../../src/sass/messenger-theme-air.sass */ -ul.messenger-theme-air .messenger-message .messenger-close { - position: absolute; - top: 0px; - right: 0px; - color: #888; - opacity: 1; - font-weight: bold; - display: block; - font-size: 20px; - line-height: 20px; - padding: 8px 10px 7px 7px; - cursor: pointer; - background: transparent; - border: 0; - -webkit-appearance: none; -} -/* line 52, ../../src/sass/messenger-theme-air.sass */ -ul.messenger-theme-air .messenger-message .messenger-close:hover { - color: #444; -} -/* line 55, ../../src/sass/messenger-theme-air.sass */ -ul.messenger-theme-air .messenger-message .messenger-close:active { - color: #222; -} -/* line 58, ../../src/sass/messenger-theme-air.sass */ -ul.messenger-theme-air .messenger-message .messenger-actions { - float: none; - margin-top: 10px; -} -/* line 62, ../../src/sass/messenger-theme-air.sass */ -ul.messenger-theme-air .messenger-message .messenger-actions a { - -moz-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.1), inset 0px 1px rgba(255, 255, 255, 0.05); - -webkit-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.1), inset 0px 1px rgba(255, 255, 255, 0.05); - box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.1), inset 0px 1px rgba(255, 255, 255, 0.05); - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - border-radius: 4px; - text-decoration: none; - display: inline-block; - padding: 10px; - color: #888; - margin-right: 10px; - padding: 3px 10px 5px; - text-transform: capitalize; -} -/* line 73, ../../src/sass/messenger-theme-air.sass */ -ul.messenger-theme-air .messenger-message .messenger-actions a:hover { - -moz-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.1), inset 0px 1px rgba(255, 255, 255, 0.15); - -webkit-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.1), inset 0px 1px rgba(255, 255, 255, 0.15); - box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.1), inset 0px 1px rgba(255, 255, 255, 0.15); - color: #444; -} -/* line 77, ../../src/sass/messenger-theme-air.sass */ -ul.messenger-theme-air .messenger-message .messenger-actions a:active { - -moz-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.18), inset 0px 1px rgba(0, 0, 0, 0.05); - -webkit-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.18), inset 0px 1px rgba(0, 0, 0, 0.05); - box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.18), inset 0px 1px rgba(0, 0, 0, 0.05); - background: rgba(0, 0, 0, 0.04); - color: #444; -} -/* line 82, ../../src/sass/messenger-theme-air.sass */ -ul.messenger-theme-air .messenger-message .messenger-actions .messenger-phrase { - display: none; -} -/* line 85, ../../src/sass/messenger-theme-air.sass */ -ul.messenger-theme-air .messenger-message .messenger-message-inner:before { - -moz-box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.3); - -webkit-box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.3); - box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.3); - -moz-border-radius: 50%; - -webkit-border-radius: 50%; - border-radius: 50%; - position: absolute; - left: 17px; - display: block; - content: " "; - top: 50%; - margin-top: -8px; - height: 13px; - width: 13px; - z-index: 20; -} -/* line 99, ../../src/sass/messenger-theme-air.sass */ -ul.messenger-theme-air .messenger-message.alert-success .messenger-message-inner:before { - background-color: #5fca4a; -} -/* line 34, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-air .messenger-message.alert-error.messenger-retry-soon .messenger-spinner { - width: 24px; - height: 24px; - background: transparent; -} -/* line 39, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-air .messenger-message.alert-error.messenger-retry-soon .messenger-spinner .messenger-spinner-side .messenger-spinner-fill { - background: #dd6a45; - -moz-animation-duration: 20s; - -webkit-animation-duration: 20s; - animation-duration: 20s; - opacity: 1; -} -/* line 47, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-air .messenger-message.alert-error.messenger-retry-soon .messenger-spinner:after { - content: ""; - background: #fff; - position: absolute; - width: 19px; - height: 19px; - border-radius: 50%; - top: 2px; - left: 2px; - display: block; -} -/* line 34, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-air .messenger-message.alert-error.messenger-retry-later .messenger-spinner { - width: 24px; - height: 24px; - background: transparent; -} -/* line 39, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-air .messenger-message.alert-error.messenger-retry-later .messenger-spinner .messenger-spinner-side .messenger-spinner-fill { - background: #dd6a45; - -moz-animation-duration: 600s; - -webkit-animation-duration: 600s; - animation-duration: 600s; - opacity: 1; -} -/* line 47, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-air .messenger-message.alert-error.messenger-retry-later .messenger-spinner:after { - content: ""; - background: #fff; - position: absolute; - width: 19px; - height: 19px; - border-radius: 50%; - top: 2px; - left: 2px; - display: block; -} -/* line 109, ../../src/sass/messenger-theme-air.sass */ -ul.messenger-theme-air .messenger-message.alert-error .messenger-message-inner:before { - background-color: #dd6a45; -} -/* line 113, ../../src/sass/messenger-theme-air.sass */ -ul.messenger-theme-air .messenger-message.alert-info .messenger-message-inner:before { - background-color: #61c4b8; -} -/* line 116, ../../src/sass/messenger-theme-air.sass */ -ul.messenger-theme-air .messenger-spinner { - display: block; - position: absolute; - left: 12px; - top: 50%; - margin-top: -13px; - z-index: 999; - height: 24px; - width: 24px; - z-index: 10; -} diff --git a/seleniumbase/resources/messenger/messenger-theme-block.css b/seleniumbase/resources/messenger/messenger-theme-block.css deleted file mode 100644 index b154e6da9ae..00000000000 --- a/seleniumbase/resources/messenger/messenger-theme-block.css +++ /dev/null @@ -1,96 +0,0 @@ -/* line 4, ../../src/sass/messenger-theme-block.sass */ -ul.messenger.messenger-theme-block.messenger-fixed { - width: 100%; - top: 0; - left: 0; - margin-left: 0px; -} -/* line 10, ../../src/sass/messenger-theme-block.sass */ -ul.messenger.messenger-theme-block.messenger-fixed.messenger-on-bottom { - top: auto; - bottom: 0; -} -/* line 14, ../../src/sass/messenger-theme-block.sass */ -ul.messenger.messenger-theme-block.messenger-fixed.messenger-on-top { - top: 0px; - bottom: auto; -} -/* line 18, ../../src/sass/messenger-theme-block.sass */ -ul.messenger.messenger-theme-block.messenger-fixed.messenger-on-top, ul.messenger.messenger-theme-block.messenger-fixed.messenger-on-bottom { - left: 0px; - right: 0px; -} -/* line 22, ../../src/sass/messenger-theme-block.sass */ -ul.messenger.messenger-theme-block.messenger-fixed.messenger-on-top.messenger-on-right, ul.messenger.messenger-theme-block.messenger-fixed.messenger-on-top.messenger-on-left, ul.messenger.messenger-theme-block.messenger-fixed.messenger-on-bottom.messenger-on-right, ul.messenger.messenger-theme-block.messenger-fixed.messenger-on-bottom.messenger-on-left { - width: 350px; -} -/* line 25, ../../src/sass/messenger-theme-block.sass */ -ul.messenger.messenger-theme-block.messenger-fixed.messenger-on-top.messenger-on-left, ul.messenger.messenger-theme-block.messenger-fixed.messenger-on-bottom.messenger-on-left { - right: auto; -} -/* line 28, ../../src/sass/messenger-theme-block.sass */ -ul.messenger.messenger-theme-block.messenger-fixed.messenger-on-top.messenger-on-right, ul.messenger.messenger-theme-block.messenger-fixed.messenger-on-bottom.messenger-on-right { - left: auto; -} -/* line 31, ../../src/sass/messenger-theme-block.sass */ -ul.messenger.messenger-theme-block.messenger-fixed .messenger-message-slot { - max-width: none; -} -/* line 34, ../../src/sass/messenger-theme-block.sass */ -ul.messenger.messenger-theme-block.messenger-fixed .messenger-message { - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - border-width: 1px; - border-style: solid; - text-align: center; - padding: 1em; -} -/* line 41, ../../src/sass/messenger-theme-block.sass */ -ul.messenger.messenger-theme-block.messenger-fixed .messenger-message.alert-warning { - color: #c09853; - background-color: #fcf8e3; - border-color: #fbeed5; -} -/* line 46, ../../src/sass/messenger-theme-block.sass */ -ul.messenger.messenger-theme-block.messenger-fixed .messenger-message.alert-error, ul.messenger.messenger-theme-block.messenger-fixed .messenger-message.alert-danger { - color: #b94a48; - background-color: #f2dede; - border-color: #eed3d7; -} -/* line 51, ../../src/sass/messenger-theme-block.sass */ -ul.messenger.messenger-theme-block.messenger-fixed .messenger-message.alert-success { - color: #468847; - background-color: #dff0d8; - border-color: #d6e9c6; -} -/* line 56, ../../src/sass/messenger-theme-block.sass */ -ul.messenger.messenger-theme-block.messenger-fixed .messenger-message.alert-info { - color: #3a87ad; - background-color: #d9edf7; - border-color: #bce8f1; -} -/* line 61, ../../src/sass/messenger-theme-block.sass */ -ul.messenger.messenger-theme-block.messenger-fixed .messenger-message .messenger-close { - filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=50); - opacity: 0.5; - padding: 0; - cursor: pointer; - color: inherit; - background: transparent; - border: 0; - -webkit-appearance: none; - float: right; - position: relative; - top: -13px; - left: 11px; - font-size: 22px; -} -/* line 75, ../../src/sass/messenger-theme-block.sass */ -ul.messenger.messenger-theme-block.messenger-fixed .messenger-message .messenger-close:hover { - filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=80); - opacity: 0.8; -} -/* line 78, ../../src/sass/messenger-theme-block.sass */ -ul.messenger.messenger-theme-block.messenger-fixed .messenger-message .messenger-close:active { - filter: progid:DXImageTransform.Microsoft.Alpha(enabled=false); - opacity: 1; -} diff --git a/seleniumbase/resources/messenger/messenger-theme-flat.js b/seleniumbase/resources/messenger/messenger-theme-flat.js deleted file mode 100644 index 794980c7dbc..00000000000 --- a/seleniumbase/resources/messenger/messenger-theme-flat.js +++ /dev/null @@ -1,33 +0,0 @@ -(function() { - var $, FlatMessage, spinner_template, - __hasProp = {}.hasOwnProperty, - __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; - - $ = jQuery; - - spinner_template = '
              \n \n \n \n \n \n \n
              '; - - FlatMessage = (function(_super) { - - __extends(FlatMessage, _super); - - function FlatMessage() { - return FlatMessage.__super__.constructor.apply(this, arguments); - } - - FlatMessage.prototype.template = function(opts) { - var $message; - $message = FlatMessage.__super__.template.apply(this, arguments); - $message.append($(spinner_template)); - return $message; - }; - - return FlatMessage; - - })(window.Messenger.Message); - - window.Messenger.themes.flat = { - Message: FlatMessage - }; - -}).call(this); diff --git a/seleniumbase/resources/messenger/messenger-theme-future.css b/seleniumbase/resources/messenger/messenger-theme-future.css deleted file mode 100644 index c1c07fbc4cf..00000000000 --- a/seleniumbase/resources/messenger/messenger-theme-future.css +++ /dev/null @@ -1,412 +0,0 @@ -@-webkit-keyframes ui-spinner-rotate-right { - 0% { - -webkit-transform: rotate(0deg); - } - 25% { - -webkit-transform: rotate(180deg); - } - 50% { - -webkit-transform: rotate(180deg); - } - 75% { - -webkit-transform: rotate(360deg); - } - 100% { - -webkit-transform: rotate(360deg); - } -} -@-webkit-keyframes ui-spinner-rotate-left { - 0% { - -webkit-transform: rotate(0deg); - } - 25% { - -webkit-transform: rotate(0deg); - } - 50% { - -webkit-transform: rotate(180deg); - } - 75% { - -webkit-transform: rotate(180deg); - } - 100% { - -webkit-transform: rotate(360deg); - } -} -@-moz-keyframes ui-spinner-rotate-right { - 0% { - -moz-transform: rotate(0deg); - } - 25% { - -moz-transform: rotate(180deg); - } - 50% { - -moz-transform: rotate(180deg); - } - 75% { - -moz-transform: rotate(360deg); - } - 100% { - -moz-transform: rotate(360deg); - } -} -@-moz-keyframes ui-spinner-rotate-left { - 0% { - -moz-transform: rotate(0deg); - } - 25% { - -moz-transform: rotate(0deg); - } - 50% { - -moz-transform: rotate(180deg); - } - 75% { - -moz-transform: rotate(180deg); - } - 100% { - -moz-transform: rotate(360deg); - } -} -@keyframes ui-spinner-rotate-right { - 0% { - transform: rotate(0deg); - } - 25% { - transform: rotate(180deg); - } - 50% { - transform: rotate(180deg); - } - 75% { - transform: rotate(360deg); - } - 100% { - transform: rotate(360deg); - } -} -@keyframes ui-spinner-rotate-left { - 0% { - transform: rotate(0deg); - } - 25% { - transform: rotate(0deg); - } - 50% { - transform: rotate(180deg); - } - 75% { - transform: rotate(180deg); - } - 100% { - transform: rotate(360deg); - } -} -/* line 118, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner { - position: relative; - border-radius: 100%; -} -/* line 122, ../../src/sass/messenger-spinner.scss */ -ul.messenger.messenger-spinner-active .messenger-spinner .messenger-spinner { - display: block; -} -/* line 126, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side { - width: 50%; - height: 100%; - overflow: hidden; - position: absolute; -} -/* line 132, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side .messenger-spinner-fill { - border-radius: 999px; - position: absolute; - width: 100%; - height: 100%; - -moz-animation-iteration-count: infinite; - -webkit-animation-iteration-count: infinite; - animation-iteration-count: infinite; - -moz-animation-timing-function: linear; - -webkit-animation-timing-function: linear; - animation-timing-function: linear; -} -/* line 142, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side-left { - left: 0; -} -/* line 145, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side-left .messenger-spinner-fill { - left: 100%; - border-top-left-radius: 0; - border-bottom-left-radius: 0; - -moz-animation-name: ui-spinner-rotate-left; - -webkit-animation-name: ui-spinner-rotate-left; - animation-name: ui-spinner-rotate-left; - -moz-transform-origin: 0 50% 50%; - -ms-transform-origin: 0 50% 50%; - -webkit-transform-origin: 0 50% 50%; - transform-origin: 0 50% 50%; -} -/* line 154, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side-right { - left: 50%; -} -/* line 157, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side-right .messenger-spinner-fill { - left: -100%; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - -moz-animation-name: ui-spinner-rotate-right; - -webkit-animation-name: ui-spinner-rotate-right; - animation-name: ui-spinner-rotate-right; - -moz-transform-origin: 100% 50% 50%; - -ms-transform-origin: 100% 50% 50%; - -webkit-transform-origin: 100% 50% 50%; - transform-origin: 100% 50% 50%; -} - -/* line 15, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future { - -moz-box-shadow: inset 0px 1px rgba(255, 255, 255, 0.24), 0px 1px 5px rgba(0, 0, 0, 0.6); - -webkit-box-shadow: inset 0px 1px rgba(255, 255, 255, 0.24), 0px 1px 5px rgba(0, 0, 0, 0.6); - box-shadow: inset 0px 1px rgba(255, 255, 255, 0.24), 0px 1px 5px rgba(0, 0, 0, 0.6); - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - border-radius: 4px; - -moz-user-select: none; - -webkit-user-select: none; - -o-user-select: none; - user-select: none; - background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzVjNWI1YiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzM1MzUzNSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); - background-size: 100%; - background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #5c5b5b), color-stop(100%, #353535)); - background-image: -moz-linear-gradient(#5c5b5b, #353535); - background-image: -webkit-linear-gradient(#5c5b5b, #353535); - background-image: linear-gradient(#5c5b5b, #353535); - background-color: #5c5b5b; - border: 1px solid rgba(0, 0, 0, 0.5); -} -/* line 23, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message { - -moz-box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 0px -1px rgba(0, 0, 0, 0.23), inset 48px 0px 0px rgba(0, 0, 0, 0.3), inset 46px 0px 0px rgba(255, 255, 255, 0.07); - -webkit-box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 0px -1px rgba(0, 0, 0, 0.23), inset 48px 0px 0px rgba(0, 0, 0, 0.3), inset 46px 0px 0px rgba(255, 255, 255, 0.07); - box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 0px -1px rgba(0, 0, 0, 0.23), inset 48px 0px 0px rgba(0, 0, 0, 0.3), inset 46px 0px 0px rgba(255, 255, 255, 0.07); - -moz-border-radius: 0px; - -webkit-border-radius: 0px; - border-radius: 0px; - position: relative; - border: 0px; - margin-bottom: 0px; - font-size: 13px; - background: transparent; - color: #f0f0f0; - text-shadow: 0px 1px #111; - font-weight: 500; - padding: 10px 30px 13px 65px; -} -/* line 36, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message a { - color: #5599ff; -} -/* line 39, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message .messenger-close { - position: absolute; - top: 0px; - right: 0px; - color: #888; - text-shadow: 0px 1px #000; - opacity: 1; - font-weight: bold; - display: block; - font-size: 20px; - line-height: 20px; - padding: 8px 10px 7px 7px; - cursor: pointer; - background: transparent; - border: 0; - -webkit-appearance: none; -} -/* line 56, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message .messenger-close:hover { - color: #bbb; -} -/* line 59, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message .messenger-close:active { - color: #777; -} -/* line 62, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message .messenger-actions { - float: none; - margin-top: 10px; -} -/* line 66, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message .messenger-actions a { - -moz-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.2), inset 0px 1px rgba(255, 255, 255, 0.1); - -webkit-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.2), inset 0px 1px rgba(255, 255, 255, 0.1); - box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.2), inset 0px 1px rgba(255, 255, 255, 0.1); - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - border-radius: 4px; - text-decoration: none; - display: inline-block; - padding: 10px; - color: #aaa; - text-shadow: 0px 1px #222; - margin-right: 10px; - padding: 3px 10px 5px; - text-transform: capitalize; -} -/* line 78, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message .messenger-actions a:hover { - -moz-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.2), inset 0px 1px rgba(255, 255, 255, 0.2); - -webkit-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.2), inset 0px 1px rgba(255, 255, 255, 0.2); - box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.2), inset 0px 1px rgba(255, 255, 255, 0.2); - color: #f0f0f0; -} -/* line 82, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message .messenger-actions a:active { - -moz-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.28), inset 0px 1px rgba(0, 0, 0, 0.1); - -webkit-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.28), inset 0px 1px rgba(0, 0, 0, 0.1); - box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.28), inset 0px 1px rgba(0, 0, 0, 0.1); - background: rgba(0, 0, 0, 0.04); - color: #aaa; -} -/* line 87, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message .messenger-actions .messenger-phrase { - display: none; -} -/* line 90, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message .messenger-message-inner:before { - -moz-box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.6), 0px 0px 0px 1px rgba(0, 0, 0, 0.2); - -webkit-box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.6), 0px 0px 0px 1px rgba(0, 0, 0, 0.2); - box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.6), 0px 0px 0px 1px rgba(0, 0, 0, 0.2); - -moz-border-radius: 50%; - -webkit-border-radius: 50%; - border-radius: 50%; - position: absolute; - left: 17px; - display: block; - content: " "; - top: 50%; - margin-top: -8px; - height: 13px; - width: 13px; - z-index: 20; -} -/* line 103, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message.alert-success .messenger-message-inner:before { - background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzVmY2E0YSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzA5OGQzOCIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); - background-size: 100%; - background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #5fca4a), color-stop(100%, #098d38)); - background-image: -moz-linear-gradient(top, #5fca4a, #098d38); - background-image: -webkit-linear-gradient(top, #5fca4a, #098d38); - background-image: linear-gradient(to bottom, #5fca4a, #098d38); - background-color: #5fca4a; -} -/* line 107, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message.alert-info .messenger-message-inner:before { - background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzYxYzRiOCIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzE5OTJhMyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); - background-size: 100%; - background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #61c4b8), color-stop(100%, #1992a3)); - background-image: -moz-linear-gradient(top, #61c4b8, #1992a3); - background-image: -webkit-linear-gradient(top, #61c4b8, #1992a3); - background-image: linear-gradient(to bottom, #61c4b8, #1992a3); - background-color: #61c4b8; -} -/* line 113, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message.alert-error .messenger-message-inner:before { - background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2RkNmE0NSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzkxMzYxYSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); - background-size: 100%; - background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #dd6a45), color-stop(100%, #91361a)); - background-image: -moz-linear-gradient(top, #dd6a45, #91361a); - background-image: -webkit-linear-gradient(top, #dd6a45, #91361a); - background-image: linear-gradient(to bottom, #dd6a45, #91361a); - background-color: #dd6a45; -} -/* line 34, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-future .messenger-message.alert-error.messenger-retry-soon .messenger-spinner { - width: 32px; - height: 32px; - background: transparent; -} -/* line 39, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-future .messenger-message.alert-error.messenger-retry-soon .messenger-spinner .messenger-spinner-side .messenger-spinner-fill { - background: #dd6a45; - -moz-animation-duration: 20s; - -webkit-animation-duration: 20s; - animation-duration: 20s; - opacity: 1; -} -/* line 47, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-future .messenger-message.alert-error.messenger-retry-soon .messenger-spinner:after { - content: ""; - background: #333; - position: absolute; - width: 26px; - height: 26px; - border-radius: 50%; - top: 3px; - left: 3px; - display: block; -} -/* line 34, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-future .messenger-message.alert-error.messenger-retry-later .messenger-spinner { - width: 32px; - height: 32px; - background: transparent; -} -/* line 39, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-future .messenger-message.alert-error.messenger-retry-later .messenger-spinner .messenger-spinner-side .messenger-spinner-fill { - background: #dd6a45; - -moz-animation-duration: 600s; - -webkit-animation-duration: 600s; - animation-duration: 600s; - opacity: 1; -} -/* line 47, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-future .messenger-message.alert-error.messenger-retry-later .messenger-spinner:after { - content: ""; - background: #333; - position: absolute; - width: 26px; - height: 26px; - border-radius: 50%; - top: 3px; - left: 3px; - display: block; -} -/* line 125, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message-slot.messenger-last .messenger-message { - -moz-border-radius: 4px 4px 0px 0px; - -webkit-border-radius: 4px; - border-radius: 4px 4px 0px 0px; -} -/* line 128, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message-slot.messenger-first .messenger-message { - -moz-border-radius: 0px 0px 4px 4px; - -webkit-border-radius: 0px; - border-radius: 0px 0px 4px 4px; - -moz-box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 48px 0px 0px rgba(0, 0, 0, 0.3), inset 46px 0px 0px rgba(255, 255, 255, 0.07); - -webkit-box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 48px 0px 0px rgba(0, 0, 0, 0.3), inset 46px 0px 0px rgba(255, 255, 255, 0.07); - box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 48px 0px 0px rgba(0, 0, 0, 0.3), inset 46px 0px 0px rgba(255, 255, 255, 0.07); -} -/* line 132, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-message-slot.messenger-first.messenger-last .messenger-message { - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - border-radius: 4px; - -moz-box-shadow: inset 48px 0px 0px rgba(0, 0, 0, 0.3), inset 46px 0px 0px rgba(255, 255, 255, 0.07); - -webkit-box-shadow: inset 48px 0px 0px rgba(0, 0, 0, 0.3), inset 46px 0px 0px rgba(255, 255, 255, 0.07); - box-shadow: inset 48px 0px 0px rgba(0, 0, 0, 0.3), inset 46px 0px 0px rgba(255, 255, 255, 0.07); -} -/* line 136, ../../src/sass/messenger-theme-future.sass */ -ul.messenger-theme-future .messenger-spinner { - display: block; - position: absolute; - left: 7px; - top: 50%; - margin-top: -18px; - z-index: 999; - height: 32px; - width: 32px; - z-index: 10; -} diff --git a/seleniumbase/resources/messenger/messenger-theme-future.js b/seleniumbase/resources/messenger/messenger-theme-future.js deleted file mode 100644 index 41f4ea40be1..00000000000 --- a/seleniumbase/resources/messenger/messenger-theme-future.js +++ /dev/null @@ -1,33 +0,0 @@ -(function() { - var $, FutureMessage, spinner_template, - __hasProp = {}.hasOwnProperty, - __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; - - $ = jQuery; - - spinner_template = '
              \n \n \n \n \n \n \n
              '; - - FutureMessage = (function(_super) { - - __extends(FutureMessage, _super); - - function FutureMessage() { - return FutureMessage.__super__.constructor.apply(this, arguments); - } - - FutureMessage.prototype.template = function(opts) { - var $message; - $message = FutureMessage.__super__.template.apply(this, arguments); - $message.append($(spinner_template)); - return $message; - }; - - return FutureMessage; - - })(window.Messenger.Message); - - window.Messenger.themes.future = { - Message: FutureMessage - }; - -}).call(this); diff --git a/seleniumbase/resources/messenger/messenger-theme-ice.css b/seleniumbase/resources/messenger/messenger-theme-ice.css deleted file mode 100644 index bc5951d90c9..00000000000 --- a/seleniumbase/resources/messenger/messenger-theme-ice.css +++ /dev/null @@ -1,110 +0,0 @@ -@import url("//fonts.googleapis.com/css?family=Raleway:400"); -/* line 12, ../../src/sass/messenger-theme-ice.sass */ -ul.messenger-theme-ice { - -moz-user-select: none; - -webkit-user-select: none; - -o-user-select: none; - user-select: none; - font-family: "Raleway", sans-serif; -} -/* line 16, ../../src/sass/messenger-theme-ice.sass */ -ul.messenger-theme-ice .messenger-message { - -moz-border-radius: 5px; - -webkit-border-radius: 5px; - border-radius: 5px; - -moz-box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.14), 0 4px #aaa, 0 5px rgba(0, 0, 0, 0.05); - -webkit-box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.14), 0 4px #aaa, 0 5px rgba(0, 0, 0, 0.05); - box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.14), 0 4px #aaa, 0 5px rgba(0, 0, 0, 0.05); - border: 0px; - background-color: #f6f6f6; - position: relative; - margin-bottom: 1.5em; - font-size: 13px; - color: #666; - font-weight: 500; - padding: 12px 22px; -} -/* line 28, ../../src/sass/messenger-theme-ice.sass */ -ul.messenger-theme-ice .messenger-message .messenger-close { - position: absolute; - top: 0px; - right: 0px; - color: #888; - opacity: 1; - font-weight: bold; - display: block; - font-size: 20px; - line-height: 20px; - padding: 8px 10px 7px 7px; - cursor: pointer; - background: transparent; - border: 0; - -webkit-appearance: none; -} -/* line 44, ../../src/sass/messenger-theme-ice.sass */ -ul.messenger-theme-ice .messenger-message .messenger-close:hover { - color: #444; -} -/* line 47, ../../src/sass/messenger-theme-ice.sass */ -ul.messenger-theme-ice .messenger-message .messenger-close:active { - color: #222; -} -/* line 50, ../../src/sass/messenger-theme-ice.sass */ -ul.messenger-theme-ice .messenger-message .messenger-actions { - float: none; - margin-top: 10px; -} -/* line 54, ../../src/sass/messenger-theme-ice.sass */ -ul.messenger-theme-ice .messenger-message .messenger-actions a { - -moz-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.1), inset 0px 1px rgba(255, 255, 255, 0.05); - -webkit-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.1), inset 0px 1px rgba(255, 255, 255, 0.05); - box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.1), inset 0px 1px rgba(255, 255, 255, 0.05); - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - border-radius: 4px; - position: relative; - text-decoration: none; - display: inline-block; - padding: 10px; - color: #888; - margin-right: 10px; - padding: 3px 10px 5px; - text-transform: capitalize; -} -/* line 66, ../../src/sass/messenger-theme-ice.sass */ -ul.messenger-theme-ice .messenger-message .messenger-actions a:hover, ul.messenger-theme-ice .messenger-message .messenger-actions a:active { - -moz-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.1), inset 0px 1px rgba(255, 255, 255, 0.15), 0 2px #aaa; - -webkit-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.1), inset 0px 1px rgba(255, 255, 255, 0.15), 0 2px #aaa; - box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.1), inset 0px 1px rgba(255, 255, 255, 0.15), 0 2px #aaa; - color: #444; -} -/* line 70, ../../src/sass/messenger-theme-ice.sass */ -ul.messenger-theme-ice .messenger-message .messenger-actions a:active { - -moz-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.1), inset 0px 1px rgba(255, 255, 255, 0.15), 0 1px #aaa; - -webkit-box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.1), inset 0px 1px rgba(255, 255, 255, 0.15), 0 1px #aaa; - box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.1), inset 0px 1px rgba(255, 255, 255, 0.15), 0 1px #aaa; - top: 1px; -} -/* line 74, ../../src/sass/messenger-theme-ice.sass */ -ul.messenger-theme-ice .messenger-message .messenger-actions .messenger-phrase { - display: none; -} -/* line 77, ../../src/sass/messenger-theme-ice.sass */ -ul.messenger-theme-ice .messenger-message .messenger-message-inner:before { - display: block; - z-index: 20; - font-weight: bold; - margin-bottom: 2px; -} -/* line 84, ../../src/sass/messenger-theme-ice.sass */ -ul.messenger-theme-ice .messenger-message.alert-success .messenger-message-inner:before { - content: "Success"; -} -/* line 88, ../../src/sass/messenger-theme-ice.sass */ -ul.messenger-theme-ice .messenger-message.alert-error .messenger-message-inner:before { - content: "Error"; -} -/* line 92, ../../src/sass/messenger-theme-ice.sass */ -ul.messenger-theme-ice .messenger-message.alert-info .messenger-message-inner:before { - content: "Information"; -} diff --git a/seleniumbase/resources/messenger/messenger.min.css b/seleniumbase/resources/messenger/messenger.min.css deleted file mode 100644 index 9303066ed35..00000000000 --- a/seleniumbase/resources/messenger/messenger.min.css +++ /dev/null @@ -1 +0,0 @@ -ul.messenger{margin:0;padding:0}ul.messenger>li{list-style:none;margin:0;padding:0}ul.messenger.messenger-empty{display:none}ul.messenger .messenger-message{overflow:hidden;*zoom:1}ul.messenger .messenger-message.messenger-hidden{display:none}ul.messenger .messenger-message .messenger-phrase,ul.messenger .messenger-message .messenger-actions a{padding-right:5px}ul.messenger .messenger-message .messenger-actions{float:right}ul.messenger .messenger-message .messenger-actions a{cursor:pointer;text-decoration:underline}ul.messenger .messenger-message ul,ul.messenger .messenger-message ol{margin:10px 18px 0}ul.messenger.messenger-fixed{position:fixed;z-index:10000}ul.messenger.messenger-fixed .messenger-message{min-width:0;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}ul.messenger.messenger-fixed .message .messenger-actions{float:left}ul.messenger.messenger-fixed.messenger-on-top{top:20px}ul.messenger.messenger-fixed.messenger-on-bottom{bottom:20px}ul.messenger.messenger-fixed.messenger-on-top,ul.messenger.messenger-fixed.messenger-on-bottom{left:50%;width:800px;margin-left:-400px}@media(max-width:960px){ul.messenger.messenger-fixed.messenger-on-top,ul.messenger.messenger-fixed.messenger-on-bottom{left:10%;width:80%;margin-left:0}}ul.messenger.messenger-fixed.messenger-on-top.messenger-on-right,ul.messenger.messenger-fixed.messenger-on-bottom.messenger-on-right{right:20px;left:auto}ul.messenger.messenger-fixed.messenger-on-top.messenger-on-left,ul.messenger.messenger-fixed.messenger-on-bottom.messenger-on-left{left:20px;margin-left:0}ul.messenger.messenger-fixed.messenger-on-right,ul.messenger.messenger-fixed.messenger-on-left{width:350px}ul.messenger.messenger-fixed.messenger-on-right .messenger-actions,ul.messenger.messenger-fixed.messenger-on-left .messenger-actions{float:left}ul.messenger .messenger-spinner{display:none}ul.messenger .messenger-clickable{cursor:pointer} \ No newline at end of file diff --git a/seleniumbase/resources/messenger/messenger.min.js b/seleniumbase/resources/messenger/messenger.min.js deleted file mode 100644 index 484e8a3cbc4..00000000000 --- a/seleniumbase/resources/messenger/messenger.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! messenger 1.5.0 */ -(function(){var e,t=window.Messenger;e=window.Messenger=function(){return e._call.apply(this,arguments)},window.Messenger.noConflict=function(){return window.Messenger=t,e}})(),window.Messenger._=function(){if(window._)return window._;var e=Array.prototype,t=Object.prototype,n=Function.prototype,s=(e.push,e.slice),r=(e.concat,t.toString);t.hasOwnProperty;var o=e.forEach,i=(e.map,e.reduce,e.reduceRight,e.filter),a=(e.every,e.some,e.indexOf,e.lastIndexOf,Array.isArray,Object.keys),l=n.bind,u={},c={},h=u.each=u.forEach=function(e,t,n){if(null!=e)if(o&&e.forEach===o)e.forEach(t,n);else if(e.length===+e.length){for(var s=0,r=e.length;r>s;s++)if(t.call(n,e[s],s,e)===c)return}else for(var i in e)if(u.has(e,i)&&t.call(n,e[i],i,e)===c)return};u.result=function(e,t){if(null==e)return null;var n=e[t];return u.isFunction(n)?n.call(e):n},u.once=function(e){var t,n=!1;return function(){return n?t:(n=!0,t=e.apply(this,arguments),e=null,t)}};var p=0;return u.uniqueId=function(e){var t=++p+"";return e?e+t:t},u.filter=u.select=function(e,t,n){var s=[];return null==e?s:i&&e.filter===i?e.filter(t,n):(h(e,function(e,r,o){t.call(n,e,r,o)&&(s[s.length]=e)}),s)},h(["Arguments","Function","String","Number","Date","RegExp"],function(e){u["is"+e]=function(t){return r.call(t)=="[object "+e+"]"}}),u.defaults=function(e){return h(s.call(arguments,1),function(t){if(t)for(var n in t)null==e[n]&&(e[n]=t[n])}),e},u.extend=function(e){return h(s.call(arguments,1),function(t){if(t)for(var n in t)e[n]=t[n]}),e},u.keys=a||function(e){if(e!==Object(e))throw new TypeError("Invalid object");var t=[];for(var n in e)u.has(e,n)&&(t[t.length]=n);return t},u.bind=function(e,t){if(e.bind===l&&l)return l.apply(e,s.call(arguments,1));var n=s.call(arguments,2);return function(){return e.apply(t,n.concat(s.call(arguments)))}},u.isObject=function(e){return e===Object(e)},u}(),window.Messenger.Events=function(){if(window.Backbone&&Backbone.Events)return Backbone.Events;var e=function(){var e=/\s+/,t=function(t,n,s,r){if(!s)return!0;if("object"==typeof s)for(var o in s)t[n].apply(t,[o,s[o]].concat(r));else{if(!e.test(s))return!0;for(var i=s.split(e),a=0,l=i.length;l>a;a++)t[n].apply(t,[i[a]].concat(r))}},n=function(e,t){var n,s=-1,r=e.length;switch(t.length){case 0:for(;r>++s;)(n=e[s]).callback.call(n.ctx);return;case 1:for(;r>++s;)(n=e[s]).callback.call(n.ctx,t[0]);return;case 2:for(;r>++s;)(n=e[s]).callback.call(n.ctx,t[0],t[1]);return;case 3:for(;r>++s;)(n=e[s]).callback.call(n.ctx,t[0],t[1],t[2]);return;default:for(;r>++s;)(n=e[s]).callback.apply(n.ctx,t)}},s={on:function(e,n,s){if(!t(this,"on",e,[n,s])||!n)return this;this._events||(this._events={});var r=this._events[e]||(this._events[e]=[]);return r.push({callback:n,context:s,ctx:s||this}),this},once:function(e,n,s){if(!t(this,"once",e,[n,s])||!n)return this;var r=this,o=_.once(function(){r.off(e,o),n.apply(this,arguments)});return o._callback=n,this.on(e,o,s),this},off:function(e,n,s){var r,o,i,a,l,u,c,h;if(!this._events||!t(this,"off",e,[n,s]))return this;if(!e&&!n&&!s)return this._events={},this;for(a=e?[e]:_.keys(this._events),l=0,u=a.length;u>l;l++)if(e=a[l],r=this._events[e]){if(i=[],n||s)for(c=0,h=r.length;h>c;c++)o=r[c],(n&&n!==o.callback&&n!==o.callback._callback||s&&s!==o.context)&&i.push(o);this._events[e]=i}return this},trigger:function(e){if(!this._events)return this;var s=Array.prototype.slice.call(arguments,1);if(!t(this,"trigger",e,s))return this;var r=this._events[e],o=this._events.all;return r&&n(r,s),o&&n(o,arguments),this},listenTo:function(e,t,n){var s=this._listeners||(this._listeners={}),r=e._listenerId||(e._listenerId=_.uniqueId("l"));return s[r]=e,e.on(t,"object"==typeof t?this:n,this),this},stopListening:function(e,t,n){var s=this._listeners;if(s){if(e)e.off(t,"object"==typeof t?this:n,this),t||n||delete s[e._listenerId];else{"object"==typeof t&&(n=this);for(var r in s)s[r].off(t,n,this);this._listeners={}}return this}}};return s.bind=s.on,s.unbind=s.off,s};return e()}(),function(){var e,t,n,s,r,o,i,a,l,u,c,h={}.hasOwnProperty,p=function(e,t){function n(){this.constructor=e}for(var s in t)h.call(t,s)&&(e[s]=t[s]);return n.prototype=t.prototype,e.prototype=new n,e.__super__=t.prototype,e},d=[].slice,f=[].indexOf||function(e){for(var t=0,n=this.length;n>t;t++)if(t in this&&this[t]===e)return t;return-1};e=jQuery,o=null!=(l=window._)?l:window.Messenger._,s=null!=(u="undefined"!=typeof Backbone&&null!==Backbone?Backbone.Events:void 0)?u:window.Messenger.Events,n=function(){function t(t){e.extend(this,s),o.isObject(t)&&(t.el&&this.setElement(t.el),this.model=t.model),this.initialize.apply(this,arguments)}return t.prototype.setElement=function(t){return this.$el=e(t),this.el=this.$el[0]},t.prototype.delegateEvents=function(e){var t,n,s,r,i,a,l;if(e||(e=o.result(this,"events"))){this.undelegateEvents(),t=/^(\S+)\s*(.*)$/,l=[];for(s in e){if(i=e[s],o.isFunction(i)||(i=this[e[s]]),!i)throw Error('Method "'+e[s]+'" does not exist');r=s.match(t),n=r[1],a=r[2],i=o.bind(i,this),n+=".delegateEvents"+this.cid,""===a?l.push(this.jqon(n,i)):l.push(this.jqon(n,a,i))}return l}},t.prototype.jqon=function(e,t,n){var s;return null!=this.$el.on?(s=this.$el).on.apply(s,arguments):(null==n&&(n=t,t=void 0),null!=t?this.$el.delegate(t,e,n):this.$el.bind(e,n))},t.prototype.jqoff=function(e){var t;return null!=this.$el.off?(t=this.$el).off.apply(t,arguments):(this.$el.undelegate(),this.$el.unbind(e))},t.prototype.undelegateEvents=function(){return this.jqoff(".delegateEvents"+this.cid)},t.prototype.remove=function(){return this.undelegateEvents(),this.$el.remove()},t}(),i=function(t){function n(){return n.__super__.constructor.apply(this,arguments)}return p(n,t),n.prototype.defaults={hideAfter:10,scroll:!0,closeButtonText:"×",escapeText:!1},n.prototype.initialize=function(t){return null==t&&(t={}),this.shown=!1,this.rendered=!1,this.messenger=t.messenger,this.options=e.extend({},this.options,t,this.defaults)},n.prototype.show=function(){var e;return this.rendered||this.render(),this.$message.removeClass("messenger-hidden"),e=this.shown,this.shown=!0,e?void 0:this.trigger("show")},n.prototype.hide=function(){var e;if(this.rendered)return this.$message.addClass("messenger-hidden"),e=this.shown,this.shown=!1,e?this.trigger("hide"):void 0},n.prototype.cancel=function(){return this.hide()},n.prototype.update=function(t){var n,s=this;return o.isString(t)&&(t={message:t}),e.extend(this.options,t),this.lastUpdate=new Date,this.rendered=!1,this.events=null!=(n=this.options.events)?n:{},this.render(),this.actionsToEvents(),this.delegateEvents(),this.checkClickable(),this.options.hideAfter?(this.$message.addClass("messenger-will-hide-after"),null!=this._hideTimeout&&clearTimeout(this._hideTimeout),this._hideTimeout=setTimeout(function(){return s.hide()},1e3*this.options.hideAfter)):this.$message.removeClass("messenger-will-hide-after"),this.options.hideOnNavigate?(this.$message.addClass("messenger-will-hide-on-navigate"),null!=("undefined"!=typeof Backbone&&null!==Backbone?Backbone.history:void 0)&&Backbone.history.on("route",function(){return s.hide()})):this.$message.removeClass("messenger-will-hide-on-navigate"),this.trigger("update",this)},n.prototype.scrollTo=function(){return this.options.scroll?e.scrollTo(this.$el,{duration:400,offset:{left:0,top:-20}}):void 0},n.prototype.timeSinceUpdate=function(){return this.lastUpdate?new Date-this.lastUpdate:null},n.prototype.actionsToEvents=function(){var e,t,n,s,r=this;n=this.options.actions,s=[];for(t in n)e=n[t],s.push(this.events['click [data-action="'+t+'"] a']=function(e){return function(n){return n.preventDefault(),n.stopPropagation(),r.trigger("action:"+t,e,n),e.action.call(r,n,r)}}(e));return s},n.prototype.checkClickable=function(){var e,t,n,s;n=this.events,s=[];for(t in n)e=n[t],"click"===t?s.push(this.$message.addClass("messenger-clickable")):s.push(void 0);return s},n.prototype.undelegateEvents=function(){var e;return n.__super__.undelegateEvents.apply(this,arguments),null!=(e=this.$message)?e.removeClass("messenger-clickable"):void 0},n.prototype.parseActions=function(){var t,n,s,r,o,i;n=[],o=this.options.actions;for(r in o)t=o[r],s=e.extend({},t),s.name=r,null==(i=s.label)&&(s.label=r),n.push(s);return n},n.prototype.template=function(t){var n,s,r,o,i,a,l,u,c,h,p=this;for(i=e("
              "),t.showCloseButton&&(r=e('