[
  {
    "path": ".flake8",
    "content": "[flake8]\nextend-ignore = E203, C901\nexclude =\n    .git,\n    __pycache__,\n    docs,\n    old,\n    dist\nmax-line-length = 160\n"
  },
  {
    "path": ".gitbook.yaml",
    "content": "root: ./docs\n\n​structure:\n    readme: README.md\n    summary: SUMMARY.md​\n"
  },
  {
    "path": ".github/workflows/publish-pylenium.yml",
    "content": "name: Upload Pylenium Package to pypi\n\non:\n  release:\n    types: [created]\n  workflow_dispatch:\n    inputs:\n      version: # keeping this for eventaul autobump feature\n        description: \"The version to publish to pypi\"\n        required: false\n        default: \"develop\"\n\njobs:\n  pypi-publish:\n    name: upload release to PyPI\n    runs-on: ubuntu-latest\n    permissions:\n      # IMPORTANT: this permission is mandatory for trusted publishing\n      id-token: write\n    steps:\n      - uses: actions/checkout@v2\n\n      - name: Set up Python\n        uses: actions/setup-python@v2\n        with:\n          python-version: \"3.x\"\n\n      - name: Build distributions\n        run: |\n          pip install poetry\n          poetry build\n\n      - name: Publish package distributions to PyPI\n        uses: pypa/gh-action-pypi-publish@release/v1\n"
  },
  {
    "path": ".github/workflows/test-pylenium.yml",
    "content": "# This workflow will install Python dependencies, run tests and lint with a single version of Python\n# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions\n\nname: Build and Test Pylenium with Selenoid\n\non:\n  push:\n    branches: [main]\n    paths-ignore:\n      - \"docs/**\"\n  pull_request:\n    branches: [main]\n    paths-ignore:\n      - \"docs/**\"\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n\n      - name: Set up Python 3.9\n        uses: actions/setup-python@v2\n        with:\n          python-version: 3.9\n\n      - name: Install dependencies\n        run: |\n          pip install poetry\n          # install dependencies from pyproject.toml\n          poetry install\n\n      - name: Lint with flake8\n        run: |\n          # Using .flake8 file\n          poetry run poe lint\n\n      - name: Run Unit Tests\n        run: |\n          poetry run pytest tests/unit\n\n      - name: Start Selenoid Server\n        # https://github.com/marketplace/actions/start-selenoid-server?version=v2\n        uses: Xotabu4/selenoid-github-action@v2\n        with:\n          selenoid-start-arguments: |\n            --args \"-timeout 250s\"\n\n      - name: Run UI Tests\n        run: |\n          poetry run pytest tests/ui --remote_url \"http://localhost:4444/wd/hub\"\n"
  },
  {
    "path": ".gitignore",
    "content": "# General\n.idea\ntest_results\nallure-report\ntest_env\ntests/data\ndata\n\n# LambdaTest\n.hyperexecute\nlogs\n.updatedhyperexecute.yaml\nhyperexecute-code*\nhyperexecuteupdate\n\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\nPipfile.lock\n\n.pypyrc\n\na11y.json\n"
  },
  {
    "path": ".gitpod.Dockerfile",
    "content": "FROM gitpod/workspace-full-vnc\n\nUSER root\n\n# RUN apt-get update -qqy && apt-get install -y wget curl gnupg2\n\n# So we can install browsers and browser drivers later\nRUN wget https://packages.microsoft.com/config/ubuntu/21.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb \\\n    && dpkg -i packages-microsoft-prod.deb && rm packages-microsoft-prod.deb\nRUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \\\n    echo \"deb http://dl.google.com/linux/chrome/deb/ stable main\" >> /etc/apt/sources.list.d/google-chrome.list\nRUN mkdir -p /home/gitpod/selenium /var/run/supervisor /var/log/supervisor && \\\n    chmod -R 777 /var/run/supervisor /var/log/supervisor\n\nENV DEBIAN_FRONTEND=noninteractive\n\n# Browsers\nRUN apt-get update -qqy && \\\n    apt-get -qy install google-chrome-stable firefox && \\\n    rm -rf /var/lib/apt/lists/* /var/cache/apt/*\n\n# Browser Drivers\nRUN CHROME_MAJOR_VERSION=$(google-chrome --version | sed -E \"s/.* ([0-9]+)(\\.[0-9]+){3}.*/\\1/\") \\\n    && CHROME_DRIVER_VERSION=$(wget --no-verbose -O - \"https://chromedriver.storage.googleapis.com/LATEST_RELEASE_${CHROME_MAJOR_VERSION}\") \\\n    && echo \"Using ChromeDriver version: \"$CHROME_DRIVER_VERSION \\\n    && wget --no-verbose -O /tmp/chromedriver_linux64.zip https://chromedriver.storage.googleapis.com/$CHROME_DRIVER_VERSION/chromedriver_linux64.zip \\\n    && rm -rf /home/gitpod/selenium/chromedriver \\\n    && unzip /tmp/chromedriver_linux64.zip -d /home/gitpod/selenium \\\n    && rm /tmp/chromedriver_linux64.zip \\\n    && mv /home/gitpod/selenium/chromedriver /home/gitpod/selenium/chromedriver-$CHROME_DRIVER_VERSION \\\n    && chmod 755 /home/gitpod/selenium/chromedriver-$CHROME_DRIVER_VERSION \\\n    && sudo ln -fs /home/gitpod/selenium/chromedriver-$CHROME_DRIVER_VERSION /usr/bin/chromedriver\nRUN GK_VERSION=\"0.31.0\" \\\n    && echo \"Using GeckoDriver version: \"$GK_VERSION \\\n    && wget --no-verbose -O /tmp/geckodriver.tar.gz https://github.com/mozilla/geckodriver/releases/download/v$GK_VERSION/geckodriver-v$GK_VERSION-linux64.tar.gz \\\n    && rm -rf /home/gitpod/selenium/geckodriver \\\n    && tar -C /home/gitpod/selenium -zxf /tmp/geckodriver.tar.gz \\\n    && rm /tmp/geckodriver.tar.gz \\\n    && mv /home/gitpod/selenium/geckodriver /home/gitpod/selenium/geckodriver-$GK_VERSION \\\n    && chmod 755 /home/gitpod/selenium/geckodriver-$GK_VERSION \\\n    && ln -fs /home/gitpod/selenium/geckodriver-$GK_VERSION /usr/bin/geckodriver\n\n# To run browser tests\nENV DISPLAY :99.0\nENV DISPLAY_NUM 99\nENV SCREEN_WIDTH 1360\nENV SCREEN_HEIGHT 1020\nENV SCREEN_DEPTH 24\nENV SCREEN_DPI 96\nENV VNC_PORT 5900\nENV NO_VNC_PORT 7900\n"
  },
  {
    "path": ".gitpod.yml",
    "content": "vscode:\n  extensions:\n    - ms-python.python\n    - bungcip.better-toml\n    - PKief.material-icon-theme\n\nimage:\n  file: .gitpod.Dockerfile\n\ntasks:\n  - name: \"Setup\"\n    command: |\n      git config --global pull.rebase false\n      pip install poetry\n      poetry install\nports:\n  - port: 5900\n    onOpen: ignore\n  - port: 6080\n    onOpen: open-browser\n  - port: 10000\n    onOpen: ignore\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"python.testing.pytestArgs\": [\"tests\"],\n  \"python.defaultInterpreterPath\": \".venv/bin/python\",\n  \"python.testing.unittestEnabled\": false,\n  \"python.testing.pytestEnabled\": true,\n  \"files.exclude\": {\n    \"**/.git\": true,\n    \"**/.svn\": true,\n    \"**/.hg\": true,\n    \"**/CVS\": true,\n    \"**/.DS_Store\": true,\n    \"**/Thumbs.db\": true,\n    \"**/*.crswap\": true,\n    \"**/*.pytest_cache\": true,\n    \"**/*__pycache__\": true\n  },\n  \"python.linting.enabled\": true,\n  \"flake8.args\": [\"--rcfile=.flake8\"],\n  \"python.formatting.provider\": \"none\",\n  \"workbench.iconTheme\": \"material-icon-theme\",\n  \"[python]\": {\n    \"editor.defaultFormatter\": \"ms-python.black-formatter\"\n  }\n}\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment include:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [atom@github.com](mailto:atom@github.com). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at [contributor-covenant.org/version/2/0/code_of_conduct/][version]\n\n[homepage]: https://contributor-covenant.org\n[version]: https://www.contributor-covenant.org/version/2/0/code_of_conduct/\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Pylenium\n\n:+1::tada: First off, thanks for taking the time to contribute! :tada::+1:\n\n\nThe following is a set of guidelines for contributing to Pylenium on GitHub. These are mostly guidelines, not rules.\nUse your best judgment, and feel free to propose changes to this document in a pull request.\n\n#### Table of Contents\n\n* [Code of Conduct](./CODE_OF_CONDUCT.md)\n* [What should I know before getting started?](#what-should-i-know-before-getting-started)\n* [How can I Contribute?](#how-can-i-contribute)\n    * [Setup the Project](#setup-the-project-to-start)\n    * [Report a Bug](#report-a-bug)\n        * [Before submitting a Bug](#before-submitting-a-bug-report)\n        * [How do I submit a (good) Bug](#how-do-i-submit-a-good-bug-report)\n    * [Suggesting Enhancements](#suggesting-enhancements)\n        * [Before submitting an Enhancement](#before-submitting-an-enhancement-suggestion)\n        * [How do I submit a (good) Enhancement?](#how-do-i-submit-a-good-enhancement-suggestion)\n    * [Your first Code Contribution](#your-first-code-contribution)\n* [Pull Requests (PR)](#pull-requests)\n    * [PR Templates](#pr-templates)\n        * [Requirements for contributing a Bugfix](#requirements-for-contributing-a-bug-fix)\n            * [Bug Template](#bug-template)\n        * [Requirements for contributing Documentation](#requirements-for-contributing-documentation)\n            * [Documentation Template](#documentation-template)\n        * [Requirements for contributing adding, changing, or removing a Feature](#requirements-for-adding-changing-or-removing-a-feature)\n            * [Enhancement Template](#enhancement-template)\n    * [Style Guides](#style-guides)\n        * [Git Commit Messages](#git-commit-messages)\n        * [Python Styleguide](#python-styleguide)\n\n## Code of Conduct\n\nThis project and everyone participating in it is governed by the [Pylenium Code of Conduct](CODE_OF_CONDUCT.md).\nBy participating, you are expected to uphold this code. Please report unacceptable behavior to Carlos Kidman (@ElSnoMan).\n\n## What should I know before getting started?\n\nPylenium is a wrapper of Selenium, not Cypress.\nHowever, like Cypress, we want Pylenium to be an all-in-one solution that comes with everything you need and expect from a robust Test Automation Solution.\n\nMost features should be \"plug and play\" for the user to make it easy for them, but we also need to keep it extendable\nso they can customize or swap components, like a reporting tool, if they want to. However, some components cannot be changed.\n\nFor example:\n\n* Selenium (for obvious reasons)\n* pytest as the Testing Framework\n\n## How can I Contribute?\n\n### Setup the Project to Start\n\nThis [Guide in our Documentation][clone and setup]\ngives you the steps to clone and setup the project.\n\nFrom there, open the [Issues Tab][Issues Tab] and check out the [Pull Requests Guide](#pull-requests)!\n\n### Report a Bug\n\nThis section guides you through submitting a bug report for Pylenium. Following these guidelines helps maintainers\nand the community understand your report :pencil:, reproduce the behavior :computer: :computer:, and find related reports :mag_right:.\n\nWhen you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report).\nFollowing this and providing the information it asks for helps us resolve issues faster.\n\n> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one.\n\n#### Before Submitting A Bug Report\n\n* **Perform a cursory search** to see if the problem has already been reported.\nIf it has **and the issue is still open**, add a comment to the existing issue instead of opening a new one.\n\n#### How Do I Submit A (Good) Bug Report?\n\nBugs are tracked as [GitHub issues](https://guides.github.com/features/issues/).\nCreate an issue on this repository and explain the problem and include additional details to help maintainers reproduce the problem:\n\n* **Use a clear and descriptive title** for the issue to identify the problem.\n* **Describe the exact steps which reproduce the problem** in as many details as possible. For example, start by explaining how you started Pylenium or ran a test(s), e.g. which command exactly you used in the terminal. When listing steps, **don't just say what you did, but explain how you did it**. For example, running tests via the IDE's UI behaves differently than the Terminal.\n* **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).\n* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior.\n* **Explain which behavior you expected to see instead and why.**\n* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. If you use the keyboard while following the steps, **record the GIF with the [Keybinding Resolver](https://github.com/atom/keybinding-resolver) shown**. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux.\n* **If you're reporting that Pylenium crashed, quits, or doesn't run tests**, include a stack trace and the Project Structure in the issue in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines), a [file attachment](https://help.github.com/articles/file-attachments-on-issues-and-pull-requests/), or put it in a [gist](https://gist.github.com/) and provide link to that gist.\n\nProvide more context by answering these questions:\n\n* **Can you reproduce the problem in the Terminal?** Sometimes Test issues are IDE-specific and not necessarily Selenium or Pytest.\n* **Did the problem start happening recently** (e.g. after updating to a new version of Pylenium) or was this always a problem?\n* If the problem started happening recently, **can you reproduce the problem in an older version of Pylenium?** What's the most recent version in which the problem doesn't happen?\n* **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens.\n\nInclude details about your configuration and environment:\n\n* **Which version of Pylenium are you using?** You can get the exact version by running `pylenium --version` in your terminal.\n* **What's the name and version of the OS you're using**?\n* **Are you running Tests in locally vs the Cloud?** If so, which tools or software are you using and which operating systems and versions are used for the host and the guest?\n* **Have you checked the Browser, Drivers, or Selenium about WebDriver issues?** Remember, Pylenium is a wrapper of Selenium, so something like Drag and Drop not working may be a WebDriver or Browser issue.\n\n### Suggesting Enhancements\n\nThis section guides you through submitting an enhancement suggestion for Pylenium, including completely new features and minor improvements to existing functionality or documentation.\nFollowing these guidelines helps maintainers and the community understand your suggestion :pencil: and find related suggestions :mag_right:.\n\nBefore creating enhancement suggestions, please check [this list](#before-submitting-an-enhancement-suggestion) as you might find out that you don't need to create one.\nWhen you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-a-good-enhancement-suggestion)\nand the steps that you imagine you would take if the feature you're requesting existed.\n\n#### Before Submitting An Enhancement Suggestion\n\n* **Check the [Pylenium Docs](https://elsnoman.gitbook.io/pylenium/)** for tips and commands — you might discover that the enhancement is already available. Most importantly, check if you're using the latest version of Pylenium.\n* **Perform a cursory search in the Issues Tab** to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.\n\n#### How Do I Submit A (Good) Enhancement Suggestion?\n\nEnhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). Create an issue on this repository and provide the following information:\n\n* **Use a clear and descriptive title** for the issue to identify the suggestion.\n* **Provide a step-by-step description of the suggested enhancement** in as many details as possible.\n* **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).\n* **Describe the current behavior** and **explain which behavior you expected to see instead** and why.\n* **Include screenshots and animated GIFs** which help you demonstrate the steps or point out the part of Pylenium which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux.\n* **Explain why this enhancement would be useful** to most Pylenium users and isn't something that can or should be implemented on your own or on another package, like a pytest plugin.\n* **List some other automation tools or applications where this enhancement exists.**\n* **Specify which version of Pylenium you're using.** You can get the exact version by running `pylenium --version` in your terminal.\n* **Specify the name and version of the OS you're using.**\n\n### Your First Code Contribution\n\nUnsure where to begin contributing to Pylenium? You can start by looking through `beginner` and `help-wanted` issues:\n\n* Beginner issues - Labeled with `beginner`, these issues should only require a few lines of code, and a test or two.\n* Help wanted issues - Labeled with `help wanted`, these issues should be a bit more involved than `beginner` issues.\n\n## Pull Requests\n\nThe process described here has several goals:\n\n- Maintain Pylenium's quality or improve it\n- Fix problems that are important to users\n- Engage the community in working toward the best possible Pylenium\n- Enable a sustainable system for Pylenium's maintainers to review contributions\n\nPlease follow these steps to have your contribution considered by the maintainers:\n\n1. Follow all instructions in [the template](#pr-templates)\n2. Follow the [style guides](#style-guides)\n3. After you submit your pull request, verify that all [status checks](https://help.github.com/articles/about-status-checks/) are passing <details><summary>What if the status checks are failing?</summary>If a status check is failing, and you believe that the failure is unrelated to your change, please leave a comment on the pull request explaining why you believe the failure is unrelated. A maintainer will re-run the status check for you. If we conclude that the failure was a false positive, then we will open an issue to track that problem with our status check suite.</details>\n\nWhile the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted.\n\n## PR Templates\n\n### Requirements for Contributing a Bug Fix\n\n* Fill out the template below. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion.\n* The pull request must only fix an existing bug. To contribute other changes, you must use a different template.\n* The pull request must update the test suite to demonstrate the changed functionality.\n* After you create the pull request, all status checks must be pass before a maintainer reviews your contribution.\n\n#### Bug Template\n\n1. Identify the Bug\n\n    > Link to the issue describing the bug that you're fixing.\n\n    If there is not yet an issue for your bug, please open a new issue and then link to that issue in your pull request.\n    Note: In some cases, one person's \"bug\" is another person's \"feature.\" If the pull request does not address an existing issue with the \"bug\" label, the maintainers have the final say on whether the current behavior is a bug.\n\n2. Description of the Change\n\n    > We must be able to understand the design of your change from this description.\n\n    If we can't get a good idea of what the code will be doing from the description here, the pull request may be closed at the maintainers' discretion.\n    Keep in mind that the maintainer reviewing this PR may not be familiar with or have worked with the code here recently, so please walk us through the concepts.\n\n3. Alternate Designs\n\n    > Explain what other alternates were considered and why the proposed version was selected\n\n4. Possible Drawbacks\n\n    > What are the possible side-effects or negative impacts of the code change?\n\n5. Verification Process\n\n    > What process did you follow to verify that the change has not introduced any regressions?\n    \n    Describe the actions you performed (including buttons you clicked, text you typed, commands you ran, etc.),\n    and describe the results you observed.\n\n6. Release Notes\n\n    > Please describe the changes in a single line that explains this improvement in\n    terms that a user can understand. This text will be used in Pylenium's release notes.\n\n    If this change is not user-facing or notable enough to be included in release notes you may use the strings \"Not applicable\" or \"N/A\" here.\n\n    Examples:\n\n    - The GitHub package now allows you to add co-authors to commits.\n    - Fixed an issue where multiple cursors did not work in a file with a single line.\n    - Increased the performance of searching and replacing across a whole project.\n\n### Requirements for Contributing Documentation\n\n* Fill out the template below. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion.\n* The pull request must only contribute documentation (for example, markdown files or API docs). To contribute other changes, you must use a different template.\n\n#### Documentation Template\n\n1. Description of the Change\n\n    > We must be able to understand the purpose of your change from this description.\n    \n    If we can't get a good idea of the benefits of the change from the description here, the pull request may be closed at the maintainers' discretion.\n\n2. Release Notes\n\n     > Please describe the changes in a single line that explains this improvement in\n     terms that a user can understand.  This text will be used in Pylenium's release notes.\n\n    If this change is not user-facing or notable enough to be included in release notes\n    you may use the strings \"Not applicable\" or \"N/A\" here.\n\n    Examples:\n\n    - The GitHub package now allows you to add co-authors to commits.\n    - Fixed an issue where multiple cursors did not work in a file with a single line.\n    - Increased the performance of searching and replacing across a whole project.\n\n### Requirements for Adding, Changing, or Removing a Feature\n\n* Fill out the template below. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion.\n* The pull request must contribute a change that has been endorsed by the maintainer team. See details in the template below.\n* The pull request must update the test suite to exercise the updated functionality.\n* After you create the pull request, all status checks must be pass before a maintainer reviews your contribution.\n\n#### Enhancement Template\n\n1. Issue or RFC Endorsed by Pylenium's Maintainers\n\n    > Link to the issue or RFC that your change relates to.\n\n    To contribute an enhancement that isn't endorsed, please follow our guide for Suggesting Enhancements.\n\n    To contribute other changes, you must use a different template.\n\n2. Description of the Change\n\n    > We must be able to understand the design of your change from this description.\n\n    If we can't get a good idea of what the code will be doing from the description here, the pull request may be closed at the maintainers' discretion.\n    Keep in mind that the maintainer reviewing this PR may not be familiar with or have worked with the code here recently, so please walk us through the concepts.\n\n3. Alternate Designs\n\n    > Explain what other alternates were considered and why the proposed version was selected.\n\n4. Possible Drawbacks\n\n    > What are the possible side-effects or negative impacts of the code change?\n\n5. Verification Process\n\n    > What process did you follow to verify that your change has the desired effects?\n\n    - How did you verify that all new functionality works as expected?\n    - How did you verify that all changed functionality works as expected?\n    - How did you verify that the change has not introduced any regressions?\n\n    Describe the actions you performed (including buttons you clicked, text you typed, commands you ran, etc.), and describe the results you observed.\n\n6. Release Notes\n\n    > Please describe the changes in a single line that explains this improvement in\n    terms that a user can understand. This text will be used in Atom's release notes.\n\n    If this change is not user-facing or notable enough to be included in release notes\n    you may use the strings \"Not applicable\" or \"N/A\" here.\n\n    Examples:\n\n    - The GitHub package now allows you to add co-authors to commits.\n    - Fixed an issue where multiple cursors did not work in a file with a single line.\n    - Increased the performance of searching and replacing across a whole project.\n\n\n### Style Guides\n\n#### Git Commit Messages\n\n* Use the present tense (\"Add feature\" not \"Added feature\")\n* Use the imperative mood (\"Move cursor to...\" not \"Moves cursor to...\")\n* Limit the first line to 72 characters or less\n* Reference issues and pull requests liberally after the first line\n\nConsider starting the commit message with an applicable emoji:\n\n* 🎨 :art: when improving the format/structure of the code\n* 🐎 :racehorse: when improving performance\n* 🚱 :non-potable_water: when plugging memory leaks\n* 📝 :memo: when writing docs\n* 🐧 :penguin: when fixing something on Linux\n* 🍎 :apple: when fixing something on macOS\n* 🏁 :checkered_flag: when fixing something on Windows\n* 🐛 :bug: when fixing a bug\n* 🔥 :fire: when removing code or files\n* 💚 :green_heart: when fixing the CI build\n* ✅ :white_check_mark: when adding tests\n* 🔒 :lock: when dealing with security\n* ⬆ :arrow_up: when upgrading dependencies\n* ⬇ :arrow_down: when downgrading dependencies\n* 👕 :shirt: when removing linter warnings\n\n#### Python Styleguide\n\n`pep8` is the styleguide and `flake8` is the linter in CI.\n\n* Indents are 4 spaces (turn your tabs to spaces!)\n* Type Hint as much as possible, but it should make sense\n* Docstrings are _valuable_ because Pylenium is a library. Help the user stay in code rather than looking up docs all the time\n* Docstrings follow the Google format\n\n```python\ndef first() -> Element:\n    \"\"\" Get the first element from this list. \"\"\"\n    return _elements[0]\n\n# --- over ---\n\ndef first():\n    return _elements[0]\n```\n\n[clone and setup]: https://elsnoman.gitbook.io/pylenium/contribute/clone-and-setup-the-project\n[Issues Tab]: https://github.com/ElSnoMan/pyleniumio/issues"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Carlos Kidman\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Pylenium: Easy Python Web Test Automation\n\n- [The Mission](#the-mission-is-simple)\n    - [Test Example](#test-example)\n    - [Purpose](#purpose)\n- [Quickstart](#quick-start)\n    - [1. Install](#1-install-pyleniumio)\n    - [2. Initialize](#2-initialize-pylenium)\n    - [3. Write a Test](#3-write-a-test)\n    - [4. Run the Test](#4-run-the-test)\n- [Contribute](#contribute)\n\n\n## The mission is simple\n\n> Bring the best of Selenium, Cypress and Python into one package.\n\nThis means:\n\n* Automatic waiting and synchronization\n* Quick setup to start writing tests\n* Easy to use and clean syntax for amazing readability and maintainability\n* Automatic driver installation so you don't need to manage drivers\n* Leverage the awesome Python language\n* and more!\n\n### Test Example\n\nAlthough Pylenium is a thin wrapper of Selenium, let's use this simple scenario to show the difference between using `Selenium` and `Pylenium`:\n\n1. **Visit** the QA at the Point website: [https://qap.dev](https://qap.dev/)\n2. **Hover** the About link to reveal a menu\n3. **Click** the Leadership link in that menu\n4. **Assert** Carlos Kidman is on the Leadership page\n\n```python\ndef test_carlos_is_on_leadership(py):\n    py.visit('https://qap.dev')\n    py.get('a[href=\"/about\"]').hover()\n    py.get('a[href=\"/leadership\"][class^=\"Header-nav\"]').click()\n    assert py.contains('Carlos Kidman')\n```\n\n```python\nfrom selenium import webdriver\nfrom selenium.webdriver.common.action_chains import ActionChains\nfrom selenium.webdriver.common.by import By\nfrom selenium.webdriver.support import expected_conditions as EC\nfrom selenium.webdriver.support.wait import WebDriverWait\n\n# define your setup and teardown fixture\n@pytest.fixture\ndef driver():\n    driver = webdriver.Chrome()\n    yield driver\n    driver.quit()\n\n\ndef test_carlos_is_on_leadership_page_with_selenium(driver):\n    wait = WebDriverWait(driver, timeout=10)\n    driver.get('https://qap.dev')\n\n    # hover About link\n    about_link = driver.find_element(By.CSS_SELECTOR, \"a[href='/about']\")\n    actions = ActionChains(driver)\n    actions.move_to_element(about_link).perform()\n\n    # click Leadership link in About menu\n    wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, \"a[href='/leadership'][class^='Header-nav']\"))).click()\n\n    # check if 'Carlos Kidman' is on the page\n    assert wait.until(lambda _: driver.find_element(By.XPATH, \"//*[contains(text(), 'Carlos Kidman')]\"))\n```\n\n### Purpose\n\nI teach courses and do trainings for both **Selenium** and **Cypress**, but Selenium, out of the box, _feels_ clunky. When you start at a new place, you almost always need to \"setup\" the framework from scratch all over again. Instead of getting right to creating meaningful tests, you end up spending most of your time building a custom framework, maintaining it, and having to teach others to use it.\n\nAlso, many people blame Selenium for bad or flaky tests. This usually tells me that they have yet to experience someone that truly knows how to make Selenium amazing! This also tells me that they are not aware of the usual root causes that make Test Automation fail:\n\n* Poor programming skills, test design and practices\n* Flaky applications\n* Complex frameworks\n\nWhat if we tried to get the best from both worlds and combine it with an amazing language?\n\n**Selenium** has done an amazing job of providing W3C bindings to many languages and makes scaling a breeze.\n\n**Cypress** has done an amazing job of making the testing experience more enjoyable - especially for beginners.\n\n**Pylenium** looks to bring more Cypress-like bindings and techniques to Selenium \\(like automatic waits\\) and still leverage Selenium's power along with the ease-of-use and power of **Python**.\n\n## Quick Start\n\nThe [Official Pylenium Docs](https://elsnoman.gitbook.io/pylenium) are the best place to start, but you can quickly get going with the following steps:\n\n### 1. Install **pyleniumio**\n\n```python\npip install pyleniumio\n\n---or---\n\npipenv install pyleniumio\n\n---or---\n\npoetry add pyleniumio\n```\n\n### 2. Initialize Pylenium\n\n```bash\n# execute at your Project Root\npylenium init\n```\n\nThis creates three files:\n\n* `conftest.py` - This has the fixtures needed for Pylenium\n* `pylenium.json` - This is the config file for Pylenium\n* `pytest.ini` - This is the config file for pytest\n\nBy default, Pylenium uses the Chrome browser. You have to install Chrome or update the `pylenium.json` file to use the browser of your choice.\n\n### 3. Write a test\n\nCreate a directory called `tests` and then a test file called `test_google.py`\n\nDefine a new test called `test_google_search`\n\n```python\ndef test_google_search(py)\n```\n\nPylenium uses **pytest** as the Test Framework. You only need to pass in `py`to the function!\n\nNow we can use **Pylenium Commands** to interact with the browser.\n\n```python\ndef test_google_search(py):\n    py.visit('https://google.com')\n    py.get(\"[name='q']\").type('puppies')\n    py.get(\"[name='btnK']\").submit()\n    assert py.should().contain_title('puppies')\n```\n\n### 4. Run the Test\n\nThis will depend on your IDE, but you can always run tests from the CLI:\n\n```bash\npython -m pytest tests/test_google.py\n```\n\nYou're all set! You should see the browser open and complete the commands we had in the test :\\)\n\n## Contribute\n\nPylenium uses [Gitpod](https://gitpod.io/) to make it easy to work on it from the _desktop or browser_ without having to worry about the setup like having the correct Python version installed after cloning the repo.\n\n> 💡 With a single click, you can open the repo in your browser, make your changes, then submit a pull request!\n\n0. If you're new to Gitpod, check out their [Getting Started](https://www.gitpod.io/docs/introduction/getting-started) docs to see how to use it\n1. Visit [Pylenium's repo](https://github.com/ElSnoMan/pyleniumio) and click the `Gitpod` button to open the repo in a VS Code browser window\n2. Wait for Gitpod to set up the project. You'll see things get setup from the various `.gitpod*` files at the Project Root\n3. Once complete, create a new branch and start making your changes\n4. When ready, submit a Pull Request!\n5. Reviewers will see your CI pipeline and even be able to open your Gitpod instance if needed - making collaboration much easier\n6. Gitpod instances are ephemeral, so you can create, share, and delete them as needed\n\n> 🧪 By default, UI tests executed in Gitpod are headless. If you'd like to see UI tests run, open port `6080` from the bottom right corner of VS Code.\n\nFor more details and other ways to contribute to Pylenium, visit the [CONTRIBUTING.md](/CONTRIBUTING.md) doc 👀\n"
  },
  {
    "path": "conftest.py",
    "content": "\"\"\"\nThe `conftest.py` and `pylenium.json` files generated by Pylenium should stay at your Workspace Root (aka Project Root).\n\nconftest.py\n    Although this file is editable, you should only change its contents if you know what you are doing.\n    Instead, you can create your own conftest.py file in the folder where you store your tests.\n\npylenium.json\n    You can change the values, but DO NOT touch the keys or you will break the schema.\n\npy\n    The main fixture you need from this is `py`. This is the instance of Pylenium for each test.\n    Just pass py into your test and you're ready to go!\n\nExamples:\n    def test_go_to_google(py):\n        py.visit('https://google.com')\n        assert 'Google' in py.title()\n\"\"\"\n\nimport copy\nimport json\nimport logging\nimport shutil\nfrom pathlib import Path\n\nimport allure\nimport pytest\nimport requests\nfrom faker import Faker\n\nfrom pylenium.a11y import PyleniumAxe\nfrom pylenium.config import PyleniumConfig, TestCase\nfrom pylenium.driver import Pylenium\n\n\n@pytest.fixture(scope=\"function\")\ndef fake() -> Faker:\n    \"\"\"A basic instance of Faker to make test data.\"\"\"\n    return Faker()\n\n\n@pytest.fixture(scope=\"function\")\ndef api():\n    \"\"\"A basic instance of Requests to make HTTP API calls.\"\"\"\n    return requests\n\n\n@pytest.fixture(scope=\"session\", autouse=True)\ndef project_root() -> Path:\n    \"\"\"The Project (or Workspace) root as a filepath.\n\n    * This conftest.py file should be in the Project Root if not already.\n    \"\"\"\n    return Path(__file__).absolute().parent\n\n\n@pytest.fixture(scope=\"session\", autouse=True)\ndef test_results_dir(project_root: Path, request) -> Path:\n    \"\"\"Creates the `/test_results` directory to store the results of the Test Run.\n\n    Returns:\n        The `/test_results` directory as a filepath (str).\n    \"\"\"\n    session = request.node\n    test_results_dir = project_root.joinpath(\"test_results\")\n\n    if test_results_dir.exists():\n        # delete /test_results from previous Test Run\n        shutil.rmtree(test_results_dir, ignore_errors=True)\n\n    try:\n        # race condition can occur between checking file existence and\n        # creating the file when using pytest with multiple workers\n        test_results_dir.mkdir(parents=True, exist_ok=True)\n    except FileExistsError:\n        pass\n\n    for test in session.items:\n        try:\n            # make the test_result directory for each test\n            test_results_dir.joinpath(test.name).mkdir(parents=True, exist_ok=True)\n        except FileExistsError:\n            pass\n\n    return test_results_dir\n\n\n@pytest.fixture(scope=\"session\")\ndef _load_pylenium_json(project_root, request) -> PyleniumConfig:\n    \"\"\"Load the default pylenium.json file or the given pylenium.json config file (if specified).\n\n    * Pylenium looks for these files from the Project Root!\n\n    I may have multiple pylenium.json files with different presets. For example:\n    - stage-pylenium.json\n    - dev-testing.json\n    - firefox-pylenium.json\n\n    Examples\n    --------\n    $ pytest\n    >>> Loads the default file: PROJECT_ROOT/pylenium.json\n\n    $ pytest pylenium_json=dev-pylenium.json\n    >>> Loads the config file: PROJECT_ROOT/dev-pylenium.json\n\n    $ pytest pylenium_json=\"configs/stage-pylenium.json\"\n    >>> Loads the config file: PROJECT_ROOT/configs/stage-pylenium.json\n    \"\"\"\n    custom_config_filepath = request.config.getoption(\"pylenium_json\")\n    config_filepath = project_root.joinpath(custom_config_filepath or \"pylenium.json\")\n\n    try:\n        with config_filepath.open() as file:\n            _json = json.load(file)\n        config = PyleniumConfig(**_json)\n    except FileNotFoundError:\n        logging.warning(f\"The config_filepath was not found, so PyleniumConfig will load with default values. File not found: {config_filepath.absolute()}\")\n        config = PyleniumConfig()\n\n    return config\n\n\n@pytest.fixture(scope=\"session\")\ndef _override_pylenium_config_values(_load_pylenium_json: PyleniumConfig, request) -> PyleniumConfig:\n    \"\"\"Override any PyleniumConfig values after loading the initial pylenium.json config file.\n\n    After a pylenium.json config file is loaded and converted to a PyleniumConfig object,\n    then any CLI arguments override their respective key/values.\n    \"\"\"\n    config = _load_pylenium_json\n    # Driver Settings\n    cli_remote_url = request.config.getoption(\"--remote_url\")\n    if cli_remote_url:\n        config.driver.remote_url = cli_remote_url\n\n    cli_browser_options = request.config.getoption(\"--options\")\n    if cli_browser_options:\n        config.driver.options = [option.strip() for option in cli_browser_options.split(\",\")]\n\n    cli_browser = request.config.getoption(\"--browser\")\n    if cli_browser:\n        config.driver.browser = cli_browser\n\n    cli_local_path = request.config.getoption(\"--local_path\")\n    if cli_local_path:\n        config.driver.local_path = cli_local_path\n\n    cli_capabilities = request.config.getoption(\"--caps\")\n    if cli_capabilities:\n        # --caps must be in '{\"name\": \"value\", \"boolean\": true}' format\n        # with double quotes around each key. booleans are lowercase.\n        config.driver.capabilities = json.loads(cli_capabilities)\n\n    cli_page_wait_time = request.config.getoption(\"--page_load_wait_time\")\n    if cli_page_wait_time and cli_page_wait_time.isdigit():\n        config.driver.page_load_wait_time = int(cli_page_wait_time)\n\n    # Logging Settings\n    cli_screenshots_on = request.config.getoption(\"--screenshots_on\")\n    if cli_screenshots_on:\n        shots_on = cli_screenshots_on.lower() == \"true\"\n        config.logging.screenshots_on = shots_on\n\n    cli_extensions = request.config.getoption(\"--extensions\")\n    if cli_extensions:\n        config.driver.extension_paths = [ext.strip() for ext in cli_extensions.split(\",\")]\n\n    cli_log_level = request.config.getoption(\"--pylog_level\")\n    if cli_log_level:\n        level = cli_log_level.upper()\n        config.logging.pylog_level = level if level in [\"DEBUG\", \"COMMAND\", \"INFO\", \"USER\", \"WARNING\", \"ERROR\", \"CRITICAL\"] else \"INFO\"\n\n    return config\n\n\n@pytest.fixture(scope=\"function\")\ndef py_config(_override_pylenium_config_values) -> PyleniumConfig:\n    \"\"\"Get a fresh copy of the PyleniumConfig for each test\n\n    See _load_pylenium_json and _override_pylenium_config_values for how the initial configuration is read.\n    \"\"\"\n    return copy.deepcopy(_override_pylenium_config_values)\n\n\n@pytest.fixture(scope=\"class\")\ndef pyc_config(_override_pylenium_config_values) -> PyleniumConfig:\n    \"\"\"Get a fresh copy of the PyleniumConfig for each test class\"\"\"\n    return copy.deepcopy(_override_pylenium_config_values)\n\n\n@pytest.fixture(scope=\"session\")\ndef pys_config(_override_pylenium_config_values) -> PyleniumConfig:\n    \"\"\"Get a fresh copy of the PyleniumConfig for each test session\"\"\"\n    return copy.deepcopy(_override_pylenium_config_values)\n\n\n@pytest.fixture(scope=\"function\")\ndef test_case(test_results_dir: Path, request) -> TestCase:\n    \"\"\"Manages data pertaining to the currently running Test Function or Case.\n\n        * Creates the test-specific logger.\n\n    Args:\n        test_results_dir: The ./test_results directory this Test Run (aka Session) is writing to\n\n    Returns:\n        An instance of TestCase.\n    \"\"\"\n    test_name = request.node.name\n    test_result_path = test_results_dir.joinpath(test_name)\n    return TestCase(name=test_name, file_path=test_result_path)\n\n\n@pytest.fixture(scope=\"function\")\ndef py(test_case: TestCase, py_config: PyleniumConfig, request):\n    \"\"\"Initialize a Pylenium driver for each test.\n\n    Pass in this `py` fixture into the test function.\n\n    Examples:\n        def test_go_to_google(py):\n            py.visit('https://google.com')\n            assert 'Google' in py.title()\n    \"\"\"\n    py = Pylenium(py_config)\n    yield py\n    try:\n        if request.node.report.failed:\n            # if the test failed, execute code in this block\n            if py_config.logging.screenshots_on:\n                screenshot = py.screenshot(str(test_case.file_path.joinpath(\"test_failed.png\")))\n                allure.attach(screenshot, \"test_failed.png\", allure.attachment_type.PNG)\n\n        elif request.node.report.passed:\n            # if the test passed, execute code in this block\n            pass\n        else:\n            # if the test has another result (ie skipped, inconclusive), execute code in this block\n            pass\n    except Exception:\n        logging.error(\"Failed to take screenshot on test failure.\")\n    py.quit()\n\n\n@pytest.fixture(scope=\"class\")\ndef pyc(pyc_config: PyleniumConfig, request):\n    \"\"\"Initialize a Pylenium driver for an entire test class.\"\"\"\n    py = Pylenium(pyc_config)\n    yield py\n    try:\n        if request.node.report.failed:\n            # if the test failed, execute code in this block\n            if pyc_config.logging.screenshots_on:\n                allure.attach(py.webdriver.get_screenshot_as_png(), \"test_failed.png\", allure.attachment_type.PNG)\n        elif request.node.report.passed:\n            # if the test passed, execute code in this block\n            pass\n        else:\n            # if the test has another result (ie skipped, inconclusive), execute code in this block\n            pass\n    except Exception:\n        logging.error(\"Failed to take screenshot on test failure.\")\n    py.quit()\n\n\n@pytest.fixture(scope=\"session\")\ndef pys(pys_config: PyleniumConfig, request):\n    \"\"\"Initialize a Pylenium driver for an entire test session.\"\"\"\n    py = Pylenium(pys_config)\n    yield py\n    try:\n        if request.node.report.failed:\n            # if the test failed, execute code in this block\n            if pys_config.logging.screenshots_on:\n                allure.attach(py.webdriver.get_screenshot_as_png(), \"test_failed.png\", allure.attachment_type.PNG)\n        elif request.node.report.passed:\n            # if the test passed, execute code in this block\n            pass\n        else:\n            # if the test has another result (ie skipped, inconclusive), execute code in this block\n            pass\n    except Exception:\n        logging.error(\"Failed to take screenshot on test failure.\")\n    py.quit()\n\n\n@pytest.fixture(scope=\"function\")\ndef axe(py) -> PyleniumAxe:\n    \"\"\"The aXe A11y audit tool as a fixture.\"\"\"\n    return PyleniumAxe(py.webdriver)\n\n\n@pytest.hookimpl(tryfirst=True, hookwrapper=True)\ndef pytest_runtest_makereport(item, call):\n    \"\"\"Yield each test's outcome so we can handle it in other fixtures.\"\"\"\n    outcome = yield\n    report = outcome.get_result()\n    if report.when == \"call\":\n        setattr(item, \"report\", report)\n    return report\n\n\ndef pytest_addoption(parser):\n    parser.addoption(\"--browser\", action=\"store\", default=\"\", help=\"The lowercase browser name: chrome | firefox\")\n    parser.addoption(\"--local_path\", action=\"store\", default=\"\", help=\"The filepath to the local driver\")\n    parser.addoption(\"--remote_url\", action=\"store\", default=\"\", help=\"Grid URL to connect tests to.\")\n    parser.addoption(\"--screenshots_on\", action=\"store\", default=\"\", help=\"Should screenshots be saved? true | false\")\n    parser.addoption(\n        \"--pylenium_json\",\n        action=\"store\",\n        default=\"\",\n        help=\"The filepath of the pylenium.json file to use (ie dev-pylenium.json)\",\n    )\n    parser.addoption(\n        \"--pylog_level\", action=\"store\", default=\"INFO\", help=\"Set the logging level: 'DEBUG' | 'COMMAND' | 'INFO' | 'USER' | 'WARNING' | 'ERROR' | 'CRITICAL'\"\n    )\n    parser.addoption(\n        \"--options\",\n        action=\"store\",\n        default=\"\",\n        help='Comma-separated list of Browser Options. Ex. \"headless, incognito\"',\n    )\n    parser.addoption(\n        \"--caps\",\n        action=\"store\",\n        default=\"\",\n        help='List of key-value pairs. Ex. \\'{\"name\": \"value\", \"boolean\": true}\\'',\n    )\n    parser.addoption(\n        \"--page_load_wait_time\",\n        action=\"store\",\n        default=\"\",\n        help=\"The amount of time to wait for a page load before raising an error. Default is 0.\",\n    )\n    parser.addoption(\"--extensions\", action=\"store\", default=\"\", help='Comma-separated list of extension paths. Ex. \"*.crx, *.crx\"')\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "# To execute this docker-compose yml file use `docker-compose -f docker-compose-v3.yml up`\n# Add the `-d` flag at the end for detached execution\n# To stop the execution, hit Ctrl+C, and then `docker-compose -f docker-compose-v3.yml down`\nversion: \"3\"\nservices:\n  chrome:\n    image: selenium/node-chrome:4.1.3-20220327\n    shm_size: 2gb\n    depends_on:\n      - selenium-hub\n    environment:\n      - SE_EVENT_BUS_HOST=selenium-hub\n      - SE_EVENT_BUS_PUBLISH_PORT=4442\n      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443\n\n  edge:\n    image: selenium/node-edge:4.1.3-20220327\n    shm_size: 2gb\n    depends_on:\n      - selenium-hub\n    environment:\n      - SE_EVENT_BUS_HOST=selenium-hub\n      - SE_EVENT_BUS_PUBLISH_PORT=4442\n      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443\n\n  firefox:\n    image: selenium/node-firefox:4.1.3-20220327\n    shm_size: 2gb\n    depends_on:\n      - selenium-hub\n    environment:\n      - SE_EVENT_BUS_HOST=selenium-hub\n      - SE_EVENT_BUS_PUBLISH_PORT=4442\n      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443\n\n  selenium-hub:\n    image: selenium/hub:4.1.3-20220327\n    container_name: selenium-hub\n    ports:\n      - \"4442:4442\"\n      - \"4443:4443\"\n      - \"4444:4444\"\n"
  },
  {
    "path": "docs/README.md",
    "content": "---\ndescription: Web Test Automation made easy\n---\n\n# Welcome to the Pylenium.io Docs\n\n[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg)](https://github.com/ElSnoMan/pyleniumio/tree/0bd684d227127daf2eccd2f284b849d4a91e3cb5/docs/code_of_conduct.md)\n\n## Welcome to the Pylenium.io Docs\n\n### The mission is simple\n\n> Bring the best of Selenium, Cypress and Python into one package.\n\nThis means:\n\n* Automatic waiting and synchronization\n* Quick setup to start writing tests\n* Easy to use and clean syntax for amazing readability and maintainability\n* Automatic driver installation so you don't need to manage drivers\n* Leverage the awesome Python language\n* and more!\n\n#### Test Example\n\nLet's use this simple scenario to show the difference between using `Selenium` and `Pylenium`:\n\n1. **Visit** the QA at the Point website: [https://qap.dev](https://qap.dev/)\n2. **Hover** the About link to reveal a menu\n3. **Click** the Leadership link in that menu\n4. **Assert** Carlos Kidman is on the Leadership page\n\n{% code title=\"Using Pylenium\" %}\n```python\ndef test_carlos_is_on_leadership(py):\n    py.visit('https://qap.dev')\n    py.get('a[href=\"/about\"]').hover()\n    py.get('a[href=\"/leadership\"][class^=\"Header-nav\"]').click()\n    assert py.contains('Carlos Kidman')\n```\n{% endcode %}\n\n{% code title=\"The same test using Selenium\" %}\n```python\nfrom selenium import webdriver\nfrom selenium.webdriver.common.action_chains import ActionChains\nfrom selenium.webdriver.common.by import By\nfrom selenium.webdriver.support import expected_conditions as EC\nfrom selenium.webdriver.support.wait import WebDriverWait\n\n# define your setup and teardown fixture\n@pytest.fixture\ndef driver():\n    driver = webdriver.Chrome()\n    yield driver\n    driver.quit()\n\n\ndef test_carlos_is_on_leadership_page_with_selenium(driver):\n    wait = WebDriverWait(driver, timeout=10)\n    driver.get('https://qap.dev')\n\n    # hover About link\n    about_link = driver.find_element(By.CSS_SELECTOR, \"a[href='/about']\")\n    actions = ActionChains(driver)\n    actions.move_to_element(about_link).perform()\n\n    # click Leadership link in About menu\n    wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, \"a[href='/leadership'][class^='Header-nav']\"))).click()\n\n    # check if 'Carlos Kidman' is on the page\n    assert wait.until(lambda _: driver.find_element(By.XPATH, \"//*[contains(text(), 'Carlos Kidman')]\"))\n```\n{% endcode %}\n\n#### Purpose\n\nI teach courses and do trainings for both **Selenium** and **Cypress**, but Selenium, out of the box, _feels_ clunky. When you start at a new place, you almost always need to \"setup\" the framework from scratch all over again. Instead of getting right to creating meaningful tests, you end up spending most of your time building a custom framework, maintaining it, and having to teach others to use it.\n\nAlso, many people blame Selenium for bad or flaky tests. This usually tells me that they have yet to experience someone that truly knows how to make Selenium amazing! This also tells me that they are not aware of the usual root causes that make Test Automation fail:\n\n* Poor programming skills, test design and practices\n* Flaky applications\n* Complex frameworks\n\nWhat if we tried to get the best from both worlds and combine it with an amazing language?\n\n**Selenium** has done an amazing job of providing W3C bindings to many languages and makes scaling a breeze.\n\n**Cypress** has done an amazing job of making the testing experience more enjoyable - especially for beginners.\n\n**Pylenium** looks to bring more Cypress-like bindings and techniques to Selenium \\(like automatic waits\\) and still leverage Selenium's power along with the ease-of-use and power of **Python**.\n\n### Quick Start\n\n{% hint style=\"success\" %}\nIf you are new to Selenium or Python, do the [Getting Started steps 1-4](getting-started/virtual-environments.md)\n{% endhint %}\n\nYou can also watch the Getting Started video with Pylenium's creator, Carlos Kidman!\n\n{% embed url=\"https://www.youtube.com/watch?v=li1nc4SUojo\" caption=\"Getting Started with v1.7.7+\" %}\n\n{% hint style=\"success\" %}\nYou don't need to worry about installing any driver binaries like `chromedriver`. **Pylenium** does this all for you automatically :\\)\n{% endhint %}\n\n#### 1. Install **pyleniumio**\n\n{% code title=\"Terminal $\" %}\n```python\npip install pyleniumio\n\n---or---\n\npipenv install pyleniumio\n```\n{% endcode %}\n\n#### 2. Initialize Pylenium\n\n{% code title=\"Terminal $ \" %}\n```text\npylenium init\n```\n{% endcode %}\n\n{% hint style=\"success\" %}\nExecute this command at your Project Root\n{% endhint %}\n\nThis creates three files:\n\n* `conftest.py` - This has the fixtures needed for Pylenium.\n* `pylenium.json` - This is the [configuration ](https://github.com/ElSnoMan/pyleniumio/blob/master/docs/configuration/pylenium.json.md)file for Pylenium.\n* `pytest.ini` - This is the configuration file for pytest and is used to connect to [ReportPortal](https://github.com/ElSnoMan/pyleniumio/blob/master/docs/configuration/report-portal.md)\n\nBy default, Pylenium uses Chrome browser. You have to install Chrome or update the `pylenium.json` file to use the browser of your choice.\n\n#### 3. Write a test\n\nCreate a directory called `tests` and then a test file called `test_google.py`\n\nDefine a new test called `test_google_search`\n\n{% code title=\"test\\_google.py\" %}\n```python\ndef test_google_search(py)\n```\n{% endcode %}\n\n{% hint style=\"info\" %}\nPylenium uses **pytest** as the Test Framework. You only need to pass in `py`to the function!\n{% endhint %}\n\nNow we can use **Pylenium Commands** to interact with the browser.\n\n{% code title=\"test\\_google.py\" %}\n```python\ndef test_google_search(py):\n    py.visit('https://google.com')\n    py.get(\"[name='q']\").type('puppies')\n    py.get(\"[name='btnK']\").submit()\n    assert py.should().contain_title('puppies')\n```\n{% endcode %}\n\n#### 4. Run the Test\n\nThis will depend on your IDE, but you can always run tests from the CLI:\n\n{% code title=\"Terminal $ \\(venv\\)\" %}\n```bash\npython -m pytest tests/test_google.py\n```\n{% endcode %}\n\nYou're all set! You should see the browser open and complete the commands we had in the test :\\)\n"
  },
  {
    "path": "docs/SUMMARY.md",
    "content": "# Table of contents\n\n* [Welcome to the Pylenium.io Docs](README.md)\n* [Changelog](changelog.md)\n\n## Getting Started\n\n* [1. Virtual Environments](getting-started/virtual-environments.md)\n* [2. Setup pytest](getting-started/setup-pytest.md)\n* [3. Project Structure with pytest](getting-started/project-structure-with-pytest.md)\n* [4. Writing Tests with Pylenium](getting-started/writing-tests-with-pylenium.md)\n\n## Guides\n\n* [Run Tests in Parallel](guides/run-tests-in-parallel.md)\n* [Run Tests in Containers](guides/run-tests-in-containers.md)\n\n## CLI\n\n* [Pylenium CLI](cli/pylenium-cli.md)\n* [Report Portal](cli/report-portal.md)\n\n## Configuration\n\n* [pylenium.json](configuration/pylenium.json.md)\n* [Driver](configuration/driver.md)\n* [Logging](configuration/logging.md)\n* [Report Portal](configuration/report-portal.md)\n\n## CONTRIBUTE\n\n* [Clone and Setup the Project](contribute/clone-and-setup-the-project.md)\n\n## Fixtures\n\n* [axe](fixtures/axe.md)\n* [fake](fixtures/fake.md)\n* [api \\(aka requests\\)](fixtures/requests.md)\n\n## Pylenium Commands\n\n* [Commands](pylenium-commands/commands.md)\n* [contains](pylenium-commands/contains.md)\n* [delete\\_all\\_cookies](pylenium-commands/delete_all_cookies.md)\n* [delete\\_cookie](pylenium-commands/delete_cookie.md)\n* [execute\\_script](pylenium-commands/execute_script.md)\n* [fake](pylenium-commands/fake.md)\n* [find](pylenium-commands/find.md)\n* [findx](pylenium-commands/find_xpath.md)\n* [get](pylenium-commands/get.md)\n* [get\\_cookies](pylenium-commands/get_cookies.md)\n* [get\\_cookie](pylenium-commands/get_cookie.md)\n* [getx](pylenium-commands/get_xpath.md)\n* [go](pylenium-commands/go.md)\n* [maximize\\_window](pylenium-commands/maximize_window.md)\n* [quit](pylenium-commands/quit.md)\n* [request](pylenium-commands/request.md)\n* [screenshot](pylenium-commands/screenshot.md)\n* [scroll\\_to](pylenium-commands/scroll_to.md)\n* [set\\_cookie](pylenium-commands/set_cookie.md)\n* [should](pylenium-commands/should.md)\n* [switch\\_to.default\\_content](pylenium-commands/switch_to.default_content.md)\n* [switch\\_to.frame](pylenium-commands/switch_to.frame.md)\n* [switch\\_to.parent\\_frame](pylenium-commands/switch_to.parent_frame.md)\n* [switch\\_to.window](pylenium-commands/switch_to.window.md)\n* [title](pylenium-commands/title.md)\n* [url](pylenium-commands/url.md)\n* [wait](pylenium-commands/wait.md)\n* [webdriver](pylenium-commands/webdriver.md)\n* [window\\_handles](pylenium-commands/window_handles.md)\n* [window\\_size](pylenium-commands/window_size.md)\n* [viewport](pylenium-commands/viewport.md)\n* [visit](pylenium-commands/visit.md)\n* [\\(deprecated\\) xpath](pylenium-commands/xpath.md)\n\n## Element Commands\n\n* [check](element-commands/check.md)\n* [children](element-commands/children.md)\n* [click](element-commands/click.md)\n* [css\\_value](element-commands/css_value.md)\n* [deselect](element-commands/deselect.md)\n* [double\\_click](element-commands/double_click.md)\n* [drag\\_to](element-commands/drag_to.md)\n* [drag\\_to\\_element](element-commands/drag_to_element.md)\n* [first](element-commands/first.md)\n* [get\\_attribute](element-commands/get_attribute.md)\n* [get\\_property](element-commands/get_property.md)\n* [hover](element-commands/hover.md)\n* [is\\_checked](element-commands/is_checked.md)\n* [is\\_displayed](element-commands/is_displayed.md)\n* [is\\_enabled](element-commands/is_enabled.md)\n* [is\\_empty](element-commands/is_empty.md)\n* [is\\_selected](element-commands/is_selected.md)\n* [last](element-commands/last.md)\n* [length](element-commands/length.md)\n* [open\\_shadow\\_dom](element-commands/open_shadow_dom.md)\n* [parent](element-commands/parent.md)\n* [right\\_click](element-commands/right_click.md)\n* [screenshot](element-commands/screenshot.md)\n* [scroll\\_into\\_view](element-commands/scroll_into_view.md)\n* [select](element-commands/select.md)\n* [select\\_many](element-commands/select_many.md)\n* [should](element-commands/should.md)\n* [siblings](element-commands/siblings.md)\n* [submit](element-commands/submit.md)\n* [tag\\_name](element-commands/tag_name.md)\n* [text](element-commands/text.md)\n* [type](element-commands/type.md)\n* [uncheck](element-commands/uncheck.md)\n* [webelement](element-commands/webelement.md)\n\n## Misc\n\n* [Install chromedriver](misc/install-chromedriver.md)\n\n"
  },
  {
    "path": "docs/changelog.md",
    "content": "---\ndescription: Summary of notable changes and fixes.\n---\n\n# Changelog\n\n## 1.12.3 - 2021-14-04\n\n### Overview\n\n`.select()` and `.select_many()` worked pretty well as expected. However, they wouldn't fail as expected! Because we were trying to combine all the \"select strategies\" in a single function, it made it harder to test and debug _and_ hid some exceptions... like when an `<option>` didn't exist... whoops!\n\nNow, each strategy has been pulled out into its own method and positive and negative tests have been added to make sure it works 😉\n\n* `.select_by_index(index: int)`\n* `.select_by_text(text: str)`\n* `.select_by_value(value)`\n\n### Changes\n\n* New `.select_by_*()` methods exist on the `Element` object. Just remember that the dropdowns have to be a `<select>` element!\n* `.select()` and `.select_many()` still exist, but show a `DEPRECATED` warning. We will be removing them in a future release\n* Our docs have a new home! We are still using GitBook, but we now have the official `docs.pylenium.io` domain! Check it out 😄\n\n## 1.12.2 - 2021-28-01\n\n### Overview\n\nMicrosoft's Edge Browser can be used on Macs now, but we hadn't tested it locally on a Mac before. One of our amazing users did and they found a bug! This should now be fixed 😄\n\n### Fixes\n\nRemoved `options` from `webdriver_factory.build_edge()` since it isn't needed and was causing the `MicrosoftEdgeDriver` to raise an error.\n\n### Contributors\n\nFor anyone looking to contribute, we have changed using `pipenv` as our package manager to `poetry`.\n\n* You can find out more about `poetry` by visiting their website: [https://python-poetry.org](https://python-poetry.org)\n* You can see how to setup your machine for Python Development with `poetry` with my [Video on YouTube](https://youtu.be/547Jr26duHQ)\n\n## 1.11.0 - 2020-10-30\n\n### Overview\n\nPylenium can now do Accessibility \\(a11y\\) Testing and Audits using aXe! Easily generate JSON reports to share and export or write assertions against it directly in your tests.\n\n### Added\n\n#### aXe integration\n\nThere are two ways to start using aXe in your tests:\n\n* `PyleniumAxe` class from `pylenium.a11y` module\n* `axe` fixture \\(recommended\\)\n\n```python\ndef test_axe_fixture(py, axe):\n    py.visit('https://qap.dev')\n    # save the axe report as a file\n    report = axe.run(name='a11y_audit.json')\n    # and/or use the report directly in the test(s)\n    assert len(report.violations) == 0\n```\n\nIn the above example, we are using Pylenium to navigate to the website and then `axe` to run the audit, generate the report, and check that we have zero violations!\n\n#### iframes\n\nThe main change here is the ability to drag and drop within iframes. Pylenium uses jQuery to perform this action, but we need to inject jQuery if the page doesn't already have it. However, in V1 of our jQuery implementation, it would only inject into the main document and not within each iframe. This is now fixed!\n\n* Pylenium's jQuery V2 now comes in its own module and injects into all iframes of the page\n* `py.switch_to` now comes with a `py.switch_to.frame_by_element()` which is useful when the iframe does not have an `id` or `name` attribute\n\n```python\niframe = py.get('iframe')\npy.switch_to.frame_by_element(iframe)\n```\n\n## 1.10.0 - 2020-10-7\n\n### Overview\n\nPylenium can now gather Web Performance metrics right from the browser and into your hands! Through Pylenium's Performance API, you can leverage the different Performance and Timing objects that each browser tracks and generates. Also, we have created some custom data points, like `Time to Interactive`, `Page Load Time`, and `Time to First Contentful Paint`!\n\n### Added\n\n* Performance API\n\n```python\n# Access the Performance API right from py\nperf = py.performance.get()\n```\n\n* WindowPerformance Object\n\nThe main abstraction that holds all of these metrics and data points.\n\n```python\n# get the TTI\ntti = py.performance.get().time_to_interactive()\n```\n\n* Stopwatch Decorator\n\nThe `performance.py` module includes a Stopwatch Decorator that you add to any function. This will log how long it takes for that function to complete!\n\n```python\n# 1. How long does it take to add an item to the cart?\n@stopwatch\ndef add_item_to_cart(py):\n    py.get('#add-item').click()\n    py.get('#added-notification').should().be_visible()\n\n# 2. How long does it take to edit an item's available stock via the API and see it change in the UI?\n@stopwatch\ndef update_available_stock(py, item, quantity):\n    payload = {'item': item, 'qty': quantity}\n    api.items.update(payload)\n    py.get(f'#available-stock-{item}').should().have_text(quantity)\n```\n\n* CONTRIBUTING.md\n* CODE\\_OF\\_CONDUCT.md\n\n#### Linked Issues\n\n[Gather Web Performance data and log how long actions take](https://app.gitkraken.com/glo/view/card/c08c640de7754a2f9cd68034ffbd93a4)\n\n## 1.9.7 - 2020-09-28\n\n### Added\n\n* Github Actions CI to Pylenium repo\n* Pylenium CLI: `pylenium version` is now `pylenium --version`\n\nIf you want to import the `Pylenium` class into a Module, you used to do this:\n\n```python\nfrom pylenium import Pylenium\n```\n\nThis has now been changed \\(for CI reasons\\) to:\n\n```python\nfrom pylenium.driver import Pylenium\n```\n\nSo make sure you update this import statement if you were using the previous version!\n\n### Fixes\n\n* pytest version 6.1.0 \\(released in September 2020\\) causes issues within Pylenium's conftest.py. Changed Pipfile to use previous version until a solution is found\n* `is_checked()` now works for Radio Buttons and Checkboxes\n\n## 1.9.0 - 2020-06-24\n\n> Changes were made to the `conftest.py` file, so make sure to run `pylenium init -c` after upgrading to `1.9.0` to overwrite it with the latest. Not doing this will likely result in `ModuleNotFoundErrors`\n\n### Report Portal \\(RP\\)\n\nRP is now natively supported by Pylenium! If you are not already familiar with Report Portal, I highly suggest you check it out. It gives you robust reporting and categorizing of your test runs and results and is backed with machine learning! [https://reportportal.io](https://reportportal.io)\n\nWe had very basic logging and reporting, but we wanted to provide a better and more robust reporting solution. After a lot of research, we landed on RP. They are not only free and Open Source, but they also have a great community, Slack group, and YouTube channel with different demos and presentations to help you take your reporting to the next level. This level of modern support was crucial in our decision and we hope you enjoy it!\n\n### **Added**\n\n* `pylenium init` now also creates a default `pytest.ini` file at your Project Root. This contains values to easily connect with RP.\n* `pylenium portal` [CLI Commands](cli/report-portal.md) to quickly setup your RP instance\n\n```bash\n# 1. Download the docker-compose file used to spin up RP\npylenium portal download\n\n# 2. Configure your machine and this docker-compose.yml based on your OS and needs\n#     by going to https://reportportal.io/docs/Deploy-with-Docker\n```\n\n```bash\n# 3. Spin up the RP instance\npylenium portal up\n```\n\nThat's it! You'll get helpful hints as you execute each command so you know where to go and how to login. Happy reporting!\n\n### **Fixes**\n\n* `get_xpath` and `find_xpath` functions were not behaving as expected. This has been fixed, but we have also renamed them to\n  * `getx()`\n  * `findx()`\n* `AttributeError` was raised if there were more than one `pytest_runtest_makereport` fixtures in the project.\n* Logging now uses the built-in `logging` python package, but screenshots are still saved to the `test_results` directory.\n\n## 1.8.2 - 2020-05-21\n\n### Changed\n\n* **`Element.should().not_exist()` --&gt; `Element.should().disappear()`**\n\nIf the intent is to check that the element is not on the page, then use:\n\n```python\npy.should().not_find()\n```\n\nIf the intent is to wait until an existing element does not exist anymore or \"disappear\", then you used to have to do\n\n```python\npy.get().should().not_exist()\n```\n\nHowever, this is clearly confusing because the way `should not exist` reads would suggest that both options are the same. This has now been changed to more clearly reflect that intent.\n\n```python\npy.get().should().disappear()\n```\n\n* **`Element.should().have_attr()` doesn't require the `value` argument**\n\nThere is a scenario when all you want to check on an element is that an attribute exists or not.\n\nExample:\n\n```python\npy.wait().until(lambda _: toggle.get_attribute('aria-checked'))\n```\n\nUnfortunately, `ElementShould.have_attr()` requires an attribute name AND a value. Trying to use it in this scenario is difficult to use or straight up doesn't work.\n\n```python\n# doesn't work\npy.get(TOGGLE).should().have_attr('aria-checked', True)\n\n# doesn't work either\npy.get(TOGGLE).should().have_attr('aria-checked', '')\n\n# this is just confusing...\npy.get(TOGGLE).should().not_have_attr('aria-checked', None)\n\n# this may work if it sees the custom `aria-checked` attribute as \"checked\"\npy.get(TOGGLE).should().be_checked()\n```\n\n### **Solution**\n\nMake the existing expectations not require the `value` argument.\n\n* `should().have_attr(name, value=None)`\n* `should().not_have_attr(name, value=None)`\n\n### Fixed\n\n* `drag_and_drop.js` was not included in the pylenium installation. Now it is!\n* Some typos\n\n## 1.8.0 - 2020-05-11\n\n### Added\n\nThis is a bigger change that sets us up for things we want to do with better reporting and BDD functionality. There may be some breaking changes depending how you wrote your tests.\n\nFor example, the property of `Element.text` is now a function `Element.text()` and `.find()` no longer has an `at_least_one` parameter.\n\nMake sure you run your tests after upgrading to catch errors like `str is not invocable`. They should be easy to fix.\n\n### **PyleniumShould**\n\nThe use case of checking that an element is NOT on the page or DOM was much more common than anticipated. I have changed how the `.find()` and `.find_xpath()` functions behave to help with this, but there are now three easy to use \"should\" commands as well.\n\n* **`.not_find()`**\n* **`.not_find_xpath()`**\n* **`.not_contain()`**\n\n```python\n# example usage\npy.should().not_find('#hidden-element')\n```\n\n### **Driver**\n\nHaving these as properties was actually messing people up as they used Pylenium. Because almost all of the commands are functions, it was common that someone would try `py.url()` or `py.title()` only to see the test fail saying that `str is not invocable`. Changing these to functions feels more natural.\n\n* `.url` property changed to `.url()` function\n* `.title` property changed to `.title()` function\n\n### **XPaths**\n\nRemoved the `.xpath()` function from _Pylenium_ and _Element_ and replaced with `get` and `find` options. The `.xpath()` function _could_ return an empty list, a single element, or a list of 2 or more elements. The flexibility was pretty \"clever\", but it was not intuitive to work with. Separating it into two distinct functions to match the CSS versions of `get()` and `.find()` made more sense.\n\n* `.get_xpath()`\n* `find_xpath()`\n\n```python\n# single element with xpath\npy.get_xpath('//input[@name=\"q\"]')\n\n# list of elements with xpath\npy.find_xpath('//li')\n```\n\n### **Find Elements**\n\nThe `.find()` and `.find_xpath()` functions on the _Pylenium_ and _Element_ objects will now return an empty list if none are found rather than throwing an exception. Dealing with an empty list is easier and cleaner than having to handle an exception.\n\nHowever, this is not the case If the timeout is set to `0` \\(zero\\). The next section goes into more detail.\n\n### **Immediate Poll with timeout=0**\n\nThere are times when you don't want to use an awesome wait and a timeout of 1 second isn't good enough. For all of the _Find Element_ commands, you can now set the timeout to `0` \\(zero\\) to poll the DOM immediately as if you were using Selenium with no wait.\n\n> This will still return an `Element` or `Elements` object if found, but no _wait_ is used.\n\nLet's take a look at the `.get()` signature:\n\n```python\ndef get(self, css: str, timeout: int = None) -> Element\n```\n\n* If `timeout=None` \\(default\\), the function will use the default `wait_time` in `pylenium.json` which is 10 seconds.\n\n```python\n# use `wait_time` in pylenium.json\npy.get('#button').click()\n```\n\n* If `timeout > 0`, override the default wait\\_time.\n\n```python\n# shorten it to 3 seconds instead\npy.get('#button', timeout=3).click()\n\n---or---\n\n# give it even more time\npy.get('#button', timeout=30).click()\n```\n\n* If timeout=0, poll the DOM immediately without any waiting.\n\n```python\n# no waiting, immediately poll the DOM\npy.get('#button', timeout=0).click()\n```\n\n### **Element and Elements**\n\nChanged some properties to functions for the same reasons as the props in Driver.\n\n* `Elements.length` property changed to `Elements.length()` function\n* `Element.tag_name` property changed to `Element.tag_name()` function\n* `Element.text` property changed to `Element.text()` function\n\n### **ElementsShould**\n\n_Pylenium_ and _Element_ have their own Should classes for expectations. Most of our assertions and checks are done against them, but there were enough use cases against the length of the Elements that I wanted to include them to make it easier. Now when you have a list of elements \\(Elements\\), you can use `.should()`:\n\n* `be_empty()`\n* `not_be_empty()`\n* `have_length()`\n* `be_greater_than()`\n* `be_less_than()`\n\n## 1.7.7 - 2020-05-08\n\n### Added\n\nPylenium CLI\n\n### Details\n\nAfter a fresh install of pyleniumio, you now need to initialize pylenium using the Pylenium CLI:\n\n```bash\n$ pylenium init\n```\n\nYou can also see the available options using the `--help` argument.\n\n```bash\n$ pylenium init --help\n```\n\nThis will create the `conftest.py` file needed by Pylenium as well as the default `pylenium.json` config file.\n\n{% hint style=\"info\" %}\nRun this command at the Project Root so Pylenium is globally accessible by all files/tests in your project.\n{% endhint %}\n\n### Purpose\n\nOriginally, Pylenium would copy a conftest.py file and overwrite any existing conftest.py files the user had at the Project Root. This was a necessary side effect with how `setup.py` was working. With `pylenium init`, you now have the option to create or overwrite these files rather than needing to start from scratch.\n\n`pylenium init` also creates a default `pylenium.json` so the user knows what config values they can change globally. This makes for a much easier experience for users.\n\nThis also removes the requirement of the user being in the context of a virtual environment. Although this is still 100% recommended, `pylenium init` can be executed in or out of the venv.\n\n## 1.6.2 - 2020-05-07\n\n### Added\n\n* `options.add_extension()`\n* `Element.open_shadow_dom()`\n\n### Details\n\n**Add Extension**\n\nYou can now easily add extensions to your browser sessions by either using the `--extensions` CLI argument and passing in a list of file paths, or you can also do this in the `pylenium.json`\n\n```javascript\n{\n    \"driver\": {\n        \"extension_paths\": [\"path.crx\", \"other-path.crx\"]\n    }\n}\n```\n\n**Shadow DOM**\n\nShadow DOMs are a bit tricky, but you can now find elements within them by using the `Element.open_shadow_dom()` command. Check out this example using `chrome://extensions`:\n\n```python\ndef test_loading_extension_to_browser(py):\n    py.visit('chrome://extensions/')\n    shadow1 = py.get('extensions-manager').open_shadow_dom()\n    shadow2 = shadow1.get('extensions-item-list').open_shadow_dom()\n    extension_shadow_dom = shadow2.find('extensions-item')[1].open_shadow_dom()\n    assert extension_shadow_dom.get('#name-and-version').should().contain_text('Get CRX')\n```\n\n## 1.6.1 - 2020-05-01\n\n### Added\n\n* `drag_to( css )`\n* `drag_to_element( to_element )`\n\n### Details\n\n`Element.drag_to( css )` will drag the current element to the element with the given CSS.\n\n```python\npy.get('#draggable-box').drag_to('#drop-here')\n```\n\n`Element.drag_to_element( to_element )` will drag the current element to the given element.\n\n```python\nto_element = py.get('#drop-here')\npy.get('#draggable-box').drag_to(to_element)\n```\n\n```python\n# one more example\nfrom_element = py.get('#draggable-box')\nto_element = py.get('#drop-here')\n\nfrom_element.drag_to_element(to_element)\n```\n\n## 1.6.0 - 2020-04-28\n\n### Added\n\n* Page Load Wait Time\n* Test Case Name into Capabilities for frameworks like Selenoid\n* Add Experimental Options via `pylenium.json`\n\n### Details\n\n**Page Load Wait Time**\n\nBy default, the Page Load timeout is `0` just like Selenium. However, there were cases where users wanted to control this globally or as needed per test case. You can now do this a few different ways:\n\n```bash\n# set it globally in CLI\n--page_load_wait_time 10\n```\n\n```javascript\n// set it globally in pylenium.json\n{\n    \"page_load_wait_time\": 10\n}\n```\n\n```python\n# override the global page_load_wait_time just for the current test\npy.set_page_load_timeout(10)\n```\n\n**Test Case Name into Capabilities**\n\nThis was primarily for other frameworks like Selenoid and Zalenium that used this name to label the tests in their runners. For example, in Selenoid, you can filter tests by name. Before this change, the tests were given an unhelpful, generic name instead of the proper test name. That's fixed now :\\)\n\n**Add Experimental Options**\n\nFor users that want to use some of the experimental options for browsers, you can now do this within `pylenium.json`. This is a list of dictionaries \\(key-value pairs\\) that you want to include globally.\n\n```javascript\n{\n    \"experimental_options\": [\n        {\"useAutomationExtension\": false},\n        {\"otherName\": \"value\"}\n    ]\n}\n```\n\n## 1.5.4 - 2020-04-27\n\n### Added\n\n* `WebDriverFactory().build_capabilities()`\n* capabilities is a single dictionary instead of a list of dictionaries\n\nOriginally I wasn't going to add capabilities because it was going to be deprecated in Selenium 4. However, it seems enough people need it \\(including my very own Workfront\\) and even with Selenium 4, there will be cases where they are needed.\n\nAlso, with the refactor it became very clear that a single dictionary of capabilities was much better than a list of them. This change has been reflected in `pylenium.json` as well as in the CLI args.\n\n{% code title=\"pylenium.json\" %}\n```python\n{\n    \"driver\": {\n        \"capabilities\": {\n            \"enableVNC\": true,\n            \"enableVideo\": false,\n            \"name\": \"value\"\n        }\n    }\n}\n```\n{% endcode %}\n\n{% code title=\"Terminal\" %}\n```python\n--caps = '{\"name\": \"value\", \"boolean\": true}'\n```\n{% endcode %}\n\n## 1.5.2 - 2020-04-24\n\n### Added\n\n* `EdgeChromiumDriver`\n* `Customize DesiredCapabilities`\n\n### Details\n\n* `EdgeChromiumDriver`\n\n{% code title=\"pylenium.json\" %}\n```python\n{\n    \"driver\": {\n        \"browser\": \"edge\"\n    }\n}\n```\n{% endcode %}\n\n{% code title=\"Terminal\" %}\n```python\n--browser=\"chrome\"\n```\n{% endcode %}\n\n* `Customize DesiredCapabilities`\n\n```javascript\n// pylenium.json\n\n{\n  \"driver\": {\n    \"capabilities\": [\n        {\"name\": \"value\"}\n    ]\n  }\n}\n```\n\n{% code title=\"Terminal\" %}\n```python\n--caps [{\"name1\": \"value1\"}, {\"name2\": \"value2\"}]\n```\n{% endcode %}\n\n## 1.4.1 - 2020-04-17\n\n### Added\n\n* `webdriver_manager`\n* `configure pylenium.json`\n\n### Details\n\n* `webdriver_manager`\n\nThis is the biggest change made in this release. Pylenium now uses `webdriver_manager` to install the necessary driver binaries to the user's machine automatically!\n\n> This means that the user does NOT need to worry about installing them anymore!\n\nOf course, they will still need the actual browser installed, but that's much easier than installing the driver binaries and adding them to the PATH.\n\nThis is a great step in making UI Automation with Pylenium a pleasant experience for everyone :\\)\n\n* `pylenium.json defaults`\n\nPrior to this release, we would install a `pylenium.json` file at the Project Root alongside our conftest.py. The issue is that this JSON file was meant to be an easy way to control Pylenium's settings \\(which it was\\), but is overriden every time they would update to a new version of Pylenium...\n\nThis also caused issues in CI/CD pipelines because they could not rely on this file to configure Pylenium since installing it fresh in the pipeline would give you a fresh pylenium.json...\n\nThis is now taken care of! We are using our BaseModel classes to use defaults that can be overriden two different ways:\n\n1. They can still override them using the CLI options. For example:  `pytest tests --browser='opera'`\n2. They can create a `pylenium.json` at the Project Root \\(same dir as conftest.py\\) with the values they want to override. They can also include any other key/value pairs in the `custom` object:\n\n```javascript\n// pylenium.json\n\n{\n    driver: {\n        \"wait_time\": 5\n    }\n    custom = {\n        \"foo\": \"bar\"\n    }\n}\n```\n\n```python\n# use it in code\npy.config.custom.get('foo')  # => yields \"bar\"\n\n---or---\n\npy.config.custom['foo']  # => yields \"bar\"\n```\n\n## 1.3.0 - 2020-04-05\n\n### Fixed\n\n* Updated the tests in the examples directory\n* Fixed an issue when using `PyleniumWait`\n\n### Added\n\n* `py.should()` - A collection of expectations for the current driver \\( [\\#15](https://github.com/ElSnoMan/pyleniumio/issues/15) \\)\n* `Element.should()` - A collection of expectations for the current element \\( [\\#36](https://github.com/ElSnoMan/pyleniumio/issues/36) \\)\n* `Element.get_property()`\n* `Element.is_enabled()`\n* `Elements.is_empty()`\n\n## 1.2.10 - 2020-03-24\n\n### Added\n\n* `Element.select(value)` - value can now be the index of the option\n\n## 1.2.9 - 2020-03-23\n\n### Added\n\n* `py.scroll_to(x, y)` - Scroll x and y pixels on the page\n* `Element.scroll_into_view()` - Scroll the element into the viewport\n* `Element.right_click()` - Right click on the element\n\n## 1.2.8 - 2020-03-21\n\n### Fixed\n\n* `DesiredCapabilities` error if user had an old version of chromedriver\n* `py.switch_to.frame()` wasn't switching to frame properly\n\n### Added\n\n* **`PyleniumWait`**\n* **Pylenium Commands &gt; wait** -  doc with Usage examples\n\n### Changed\n\n* Custom timeouts to some commands, like `.get()`, to override global wait\\_time in pylenium.json\n\n## 1.2.7 - 2020-03-17\n\n### Added\n\n* Official release of V1 to the Autobots class\n* The core functionality of Pylenium\n\n"
  },
  {
    "path": "docs/cli/pylenium-cli.md",
    "content": "---\ndescription: 'The CLI comes with commands to initialize and create Pylenium files, and more.'\n---\n\n# Pylenium CLI\n\n## pylenium init\n\nInitializes Pylenium into the current directory. This creates Pylenium's required files:\n\n* `conftest.py`\n* `pylenium.json`\n* `pytest.ini`\n\n{% code title=\"Terminal $\" %}\n```bash\npylenium init\n```\n{% endcode %}\n\n{% hint style=\"success\" %}\nMake sure to run this command at the Project Root \\(aka Workspace\\)\n{% endhint %}\n\n{% hint style=\"info\" %}\nBy default, this will not overwrite Pylenium files if they already exist.\n{% endhint %}\n\n## Overwrite conftest.py file\n\nYou can overwrite an existing **conftest.py** file with the latest version by using the `-c` flag.\n\n{% code title=\"Terminal $\" %}\n```bash\npylenium init -c\n```\n{% endcode %}\n\n## Overwrite pylenium.json file\n\nYou can overwrite an existing **pylenium.json** file with the latest defaults by using the `-p` flag.\n\n{% code title=\"Terminal $\" %}\n```bash\npylenium init -p\n```\n{% endcode %}\n\n## Overwrite pytest.ini file\n\nYou can overwrite an existing **pytest.ini** file with the latest defaults by using the `-i` flag.\\\n\n{% code title=\"Terminal $\" %}\n```bash\npylenium init -i\n```\n{% endcode %}\n\n"
  },
  {
    "path": "docs/cli/report-portal.md",
    "content": "---\ndescription: CLI commands to easily setup and teardown your RP instance.\n---\n\n# Report Portal\n\n{% hint style=\"success\" %}\nMake sure to execute these commands in your Project Root.\n{% endhint %}\n\n## portal download\n\nDownload the default `docker-compose.yml` file needed to spin up the RP instance.\n\n{% hint style=\"info\" %}\nThis will place the file at your Project Root as `docker-compose.report-portal.yml`\n{% endhint %}\n\n{% code title=\"Terminal $\" %}\n```text\npylenium portal download\n```\n{% endcode %}\n\n## portal up\n\nSpin up the RP instance using the downloaded YAML file.\n\n{% code title=\"Terminal $\" %}\n```text\npylenium portal up\n```\n{% endcode %}\n\nYou can then open [http://localhost:8080](http://localhost:8080) to see your newly created instance!\n\n## portal down\n\nTeardown the RP instance.\n\n{% hint style=\"warning\" %}\nThis may or may not work depending on your Terminal on Windows.\n{% endhint %}\n\n{% code title=\"Terminal $\" %}\n```text\npylenium portal down\n```\n{% endcode %}\n\n"
  },
  {
    "path": "docs/configuration/driver.md",
    "content": "---\ndescription: Configure the driver via the pylenium.json or the CLI.\n---\n\n# Driver\n\n## The Driver Settings\n\nSupported Drivers:\n\n* **Chrome**\n* **Firefox**\n* **IE**\n* **Opera**\n* **Edge \\(Chromium\\)**\n\n Let's take a look at the Driver Settings in `pylenium.json`\n\n{% code title=\"pylenium.json\" %}\n```javascript\n\"driver\": {\n    \"browser\": \"chrome\",\n    \"remote_url\": \"\",\n    \"wait_time\": 10,\n    \"page_load_wait_time\": 0,\n    \"options\": [],\n    \"experimental_options\": null,\n    \"capabilities\": {},\n    \"extension_paths\": [],\n    \"webdriver_kwargs\": {},\n    \"version\": \"latest\"\n  }\n```\n{% endcode %}\n\nLet's break each one of these down so you know what they are for and how you can configure them.\n\n### browser\n\n{% hint style=\"info\" %}\nDefault is **`\"chrome\"`**\n{% endhint %}\n\nThis is the browser name - `\"chrome\"` or `\"firefox\"` or `\"ie\"` or `\"opera\"` or `\"edge\"`\n\n{% code title=\"pylenium.json\" %}\n```javascript\n\"driver\": {\n    \"browser\": \"firefox\"\n}\n```\n{% endcode %}\n\n{% code title=\"Terminal\" %}\n```bash\npython -m pytest tests --browser=firefox\n```\n{% endcode %}\n\n### remote\\_url\n\n{% hint style=\"info\" %}\nDefault is empty or**`\"\"`**\n{% endhint %}\n\nThis is used to connect to things like **Selenium Grid.**\n\n{% hint style=\"success\" %}\nCheck out [Run Tests in Containers](../guides/run-tests-in-containers.md) for an example of how to do this locally with **Docker**\n{% endhint %}\n\n{% code title=\"pylenium.json\" %}\n```javascript\n\"driver\": {\n    \"remote_url\": \"http://localhost:4444/wd/hub\"\n}\n```\n{% endcode %}\n\n{% code title=\"Terminal\" %}\n```bash\npython -m pytest tests --remote_url=\"http://localhost:4444/wd/hub\"\n```\n{% endcode %}\n\n### wait\\_time\n\n{% hint style=\"info\" %}\nDefault is **`10`**\n{% endhint %}\n\nThe global number of seconds for actions to wait for.\n\n{% code title=\"pylenium.json\" %}\n```javascript\n\"driver\": {\n    \"wait_time\": 7\n}\n```\n{% endcode %}\n\n{% code title=\"Terminal\" %}\n```bash\nYou cannot set this from the command line\n```\n{% endcode %}\n\n### page\\_load\\_wait\\_time\n\n{% hint style=\"info\" %}\nDefault is 0\n{% endhint %}\n\nThe amount of time to wait for the page to load before raising an error.\n\n```bash\n# set it globally in CLI\n--page_load_wait_time 10\n```\n\n```javascript\n// set it globally in pylenium.json\n{\n    \"driver\": {\n        \"page_load_wait_time\": 10\n    }\n}\n```\n\n```python\n# override the global page_load_wait_time just for the current test\npy.set_page_load_timeout(10)\n```\n\n### options\n\n{% hint style=\"info\" %}\nDefault is empty or **`[]`**\n{% endhint %}\n\nA list of browser options to include when instantiating Pylenium.\n\n{% code title=\"pylenium.json\" %}\n```javascript\n\"driver\": {\n    \"options\": [\"headless\", \"incognito\"]\n}\n```\n{% endcode %}\n\n{% code title=\"Terminal\" %}\n```bash\npython -m pytest tests --options=\"headless, incognito\"\n```\n{% endcode %}\n\n### experimental\\_options\n\n{% hint style=\"info\" %}\nDefault is `null` or `None`\n{% endhint %}\n\nA list of experimental options to include in the driver. These can only be added using `pylenium.json`\n\n```javascript\n{\n    \"experimental_options\": [\n        {\"useAutomationExtension\": false},\n        {\"otherName\": \"value\"}\n    ]\n}\n```\n\n### capabilities\n\n{% hint style=\"info\" %}\nDefault is empty or `{}`\n{% endhint %}\n\nA dictionary of the desired capabilities to include when instantiating Pylenium.\n\n{% code title=\"pylenium.json\" %}\n```python\n{\n    \"driver\": {\n        \"capabilities\": {\n            \"enableVNC\": true,\n            \"enableVideo\": false,\n            \"name\": \"value\"\n        }\n    }\n}\n```\n{% endcode %}\n\n{% code title=\"Terminal\" %}\n```python\n--caps = '{\"name\": \"value\", \"boolean\": true}'\n```\n{% endcode %}\n\n### extension\\_paths\n\nThe list of extensions to be included when instantiating Pylenium.\n\n{% hint style=\"info\" %}\nDefault is empty or `[]`\n{% endhint %}\n\n```javascript\n{\n    \"driver\": {\n        \"extension_paths\": [\"path_to_crx.crx\", \"other-path.crx\"]\n    }\n}\n```\n\n### webdriver_kwargs\n\nArbitrary keyword arguments to pass to the Selenium webdriver.\n\n{% hint style=\"info\" %}\nDefault is empty or `{}`\n{% endhint %}\n\n```javascript\n{\n    \"driver\": {\n        \"webdriver_kwargs\": {\"service_log_path\": \"webdriver.log\"}\n    }\n}\n```\n\n### version\n\n{% hint style=\"info\" %}\nDefault is **`\"latest\"`**\n{% endhint %}\n\nThe browser version to use.\n\n{% code title=\"pylenium.json\" %}\n```javascript\n\"driver\": {\n    \"version\": \"latest\"\n}\n```\n{% endcode %}\n\n{% code title=\"Terminal\" %}\n```bash\nYou cannot set the browser version this way\n```\n{% endcode %}\n\n"
  },
  {
    "path": "docs/configuration/logging.md",
    "content": "---\ndescription: Configure logging via the pylenium.json or the CLI.\n---\n\n# Logging\n\n## Levels\n\nPylenium has 3 logging levels:\n\n* `'off'`\n* `'info' (default)`\n* `'debug'`\n\nWhen you start a Test Run, Pylenium will create a **test\\_results** directory at your Project Root.\n\n{% hint style=\"info\" %}\nYou should put **test\\_results** in your .`gitignore`\n{% endhint %}\n\nBy default, each test using the `py` fixture will have its own subdirectory with a `test_log.txt` and, if the test fails, a screenshot.\n\nYou can easily control the logging and screenshots by setting the values in `pylenium.json` or passing in command-line arguments.\n\n### off\n\nLike the name suggests, this will turn off all `INFO` and higher entries, which is almost everything.\n\n{% hint style=\"info\" %}\nYou will still get a `test_log.txt` file, but it will only have basic info of the test like chromedriver version.\n{% endhint %}\n\n{% code title=\"pylenium.json\" %}\n```text\n\"pylog_level\": \"off\"\n```\n{% endcode %}\n\n{% code title=\"Terminal\" %}\n```text\npython -m pytest tests --pylog_level=off\n```\n{% endcode %}\n\n### info\n\nThis will include basic info as well as the `INFO` , `ACTION`, `STEP` entries.\n\n{% hint style=\"success\" %}\nThis is the **default** configuration, so you don't need to explicitly set these.\n{% endhint %}\n\n{% code title=\"pylenium.json\" %}\n```text\n\"pylog_level\": \"info\"\n```\n{% endcode %}\n\n{% code title=\"Terminal\" %}\n```text\npython -m pytest tests --pylog_level=info\n```\n{% endcode %}\n\n### debug\n\nThis will include everything in **info** as well as the `DEBUG` , `WARNING`, `ERROR` and `FATAL` entries.\n\n{% hint style=\"info\" %}\nYou can use `py.log.debug(message)` to log debugging entries!\n{% endhint %}\n\n{% code title=\"pylenium.json\" %}\n```text\n\"pylog_level\": \"debug\"\n```\n{% endcode %}\n\n{% code title=\"Terminal\" %}\n```text\npython -m pytest tests --pylog_level=debug\n```\n{% endcode %}\n\n## Screenshots\n\nBy default, if a test fails, Pylenium will take a screenshot and include it in the respective `test_results` test directory.\n\nThis can easily be turned **off.**\n\n{% code title=\"pylenium.json\" %}\n```text\n\"screenshots_on\": False\n```\n{% endcode %}\n\n{% code title=\"Terminal\" %}\n```text\npython -m pytest tests --screenshots_on=false\n```\n{% endcode %}\n\n"
  },
  {
    "path": "docs/configuration/pylenium.json.md",
    "content": "---\ndescription: The configuration file for Pylenium\n---\n\n# pylenium.json\n\n## Configure with a JSON File\n\nIf you don't want to use Pylenium's defaults but you don't want to configure it via the CLI, you can create a **`pylenium.json`** file at the Project Root \\(same directory as our `conftest.py` file\\) and do it with a JSON instead.\n\nHere are all of the current settings \\(and their defaults\\) you can configure right now:\n\n```javascript\n{\n  \"driver\": {\n    \"browser\": \"chrome\",\n    \"remote_url\": \"\",\n    \"wait_time\": 10,\n    \"page_load_wait_time\": 0,\n    \"options\": [],\n    \"experimental_options\": null,\n    \"capabilities\": {},\n    \"extension_paths\": [],\n    \"webdriver_kwargs\": {},\n    \"version\": \"latest\"\n  },\n\n  \"logging\": {\n    \"screenshots_on\": true,\n    \"pylog_level\": \"info\"\n  },\n\n  \"viewport\": {\n    \"maximize\": true,\n    \"width\": 1440,\n    \"height\": 900,\n    \"orientation\": \"portrait\"\n  },\n\n  \"custom\": {}\n}\n```\n\n## Change a single value\n\n{% hint style=\"info\" %}\nYou only need to change the values you care about.\n{% endhint %}\n\nIf I only wanted to change the browser to be `\"firefox\"`, then only include that:\n\n```bash\n{\n  \"driver\": {\n    \"browser\": \"firefox\"\n  }\n}\n```\n\n## Adding custom values\n\n{% hint style=\"info\" %}\nYou can add any objects within the **`custom`** object to be used by **`py.config`**\n{% endhint %}\n\nAdding your own key/value pairs is easy:\n\n```bash\n{\n  \"custom\": {\n    \"env_url\": \"https://staging.our-app.com\"\n  }\n}\n```\n\nNow you can use it like any other dictionary in Python:\n\n```python\npy.config.custom.get('env_url')\n\n---or---\n\npy.config.custom['env_url']\n```\n\n### Complex custom objects\n\nMore complex or nested objects are easy to add as well:\n\n```bash\n{\n  \"custom\": {\n    \"environment\": {\n      \"url\": \"https://staging.our-app.com\",\n      \"username\": \"foo\",\n      \"password\": \"bar\",\n      \"clusters\": [ \"cl01\", \"cl03\", \"cl05\" ]\n    }\n  }\n}\n```\n\nIt's still just a Python dictionary, so you can easily access them:\n\n```python\n# Get the entire environment object\npy.config.custom.get('environment')\n\n# Get only the url\npy.config.custom['environment']['url']\n\n# Get the first item in the list of clusters\npy.config.custom['environment']['clusters'][0]\n```\n"
  },
  {
    "path": "docs/configuration/report-portal.md",
    "content": "---\ndescription: Connect your tests to a RP instance.\n---\n\n# Report Portal\n\n{% hint style=\"info\" %}\nThis doc assumes you are already familiar with the [Report Portal CLI commands](../cli/report-portal.md)\n{% endhint %}\n\n## Configure with pytest.ini\n\nOnce you've spun up your instance of Report Portal \\(RP\\), there are only a few values you need to change in the `pytest.ini` file created when you ran `pylenium init`.\n\nHere is the default pytest.ini file:\n\n```graphql\n[pytest]\n;ReportPortal `pytest-reportportal` plugin\n;ReportPortal (required)\nrp_endpoint = http://localhost:8080\nrp_uuid = [UUID from USER PROFILE section of ReportPortal]\nrp_launch = EXAMPLE_TEST_RUN_NAME\nrp_project = default_personal\n\n;For more info, including other pytest.ini options, visit: https://github.com/reportportal/agent-python-pytest\n;ReportPortal (optional)\nrp_ignore_errors = True\nrp_hierarchy_dirs = True\nrp_hierarchy_module = False\nrp_hierarchy_class = False\n```\n\n{% hint style=\"info\" %}\nThe only value that _needs ****_be changed is the `rp_uuid` which is the **ACCESS TOKEN** to connect\n{% endhint %}\n\n### Launch and Project\n\n* `rp_launch` is the name of the **Test Run** and is the grouping of tests that will be reported to RP under the **Launches** tab.\n* `rp_project` is the name of the **Project** that you would like the Launch to report to.\n\nBy default, there is a `superadmin_personal` and a `default_personal` project when you first spin up your instance of RP. You can switch between them or create more on the **Administrate** page under the User Menu.\n\n{% hint style=\"info\" %}\nAny of these variables _can_ be overridden via the CLI in case you need them defined at runtime.\n{% endhint %}\n\n### Get the ACCESS TOKEN\n\nOnce you have logged in to your instance of RP, go to the **Profile** page under the User Menu in the top right corner. This will have the ACCESS TOKEN for you to copy.\n\nPaste this into the `rp_uuid` value in `pytest.ini` and save. That's it!\n\n## Run the Tests\n\nAll that's left is to run the tests like usual, but now include the `--reportportal` flag.\n\n{% code title=\"Example\" %}\n```bash\npytest tests/test_checkout.py --reportportal\n```\n{% endcode %}\n\nRefresh the Launches tab and you should now see your Test Run!\n\n{% hint style=\"success\" %}\nThere's a lot you can do, so make sure to visit their docs for more: [https://reportportal.io/docs](https://reportportal.io/docs)\n{% endhint %}\n\n"
  },
  {
    "path": "docs/contribute/clone-and-setup-the-project.md",
    "content": "---\ndescription: The first step in contributing to Pylenium.io\n---\n\n# Clone and Setup the Project\n\n## 1. Fork the Project\n\nThis GitHub Guide is simple and concise! It goes over:\n\n* How to Fork\n* Clone and add to your Fork\n* Submitting a Pull Request \\(PR\\)\n\n{% embed url=\"https://guides.github.com/activities/forking/\" caption=\"\" %}\n\n## 2. Virtual Environment\n\nOnce cloned, open your IDE of choice and create a Virtual Environment.\n\n{% hint style=\"info\" %}\nFollow the Pylenium guide for venvs and pytest here: [Virtual Environments](../getting-started/virtual-environments.md)\n{% endhint %}\n\n## 3. Package Management with Pipenv\n\nPipenv is the package manager that is used for Pylenium. Please take a moment to read this guide to understand how to use it and the Pipfiles.\n\n{% embed url=\"https://realpython.com/pipenv-guide/\" caption=\"\" %}\n\n### Install pipenv globally\n\n{% tabs %}\n{% tab title=\"Mac \\| Linux\" %}\n```text\npip3 install pipenv\n```\n{% endtab %}\n\n{% tab title=\"Windows\" %}\n```bash\npip install pipenv\n```\n{% endtab %}\n{% endtabs %}\n\n### Install the packages from Pipfile\n\nNow open a Terminal in the pyleniumio project to install the packages.\n\n{% code title=\"Terminal $\" %}\n```bash\npipenv sync\n```\n{% endcode %}\n\n{% hint style=\"warning\" %}\nYou must be using **Python 3.8.1** or greater!\n{% endhint %}\n\n## 4. Follow our Contributing Guide\n\nYou're all set! Head on over to our [CONTRIBUTING GUIDE](https://github.com/ElSnoMan/pyleniumio/tree/d887dd0028538e9416fe3fe284a75ab30a2dc744/CONTRIBUTING.md) for more information on:\n\n* Code of Conduct\n* Templates for Bugs, Enhancements, etc.\n* Guidelines for Pull Requests and suggestions\n* and more!\n\nThank you for taking the time to contribute! We're excited to have you!\n\n"
  },
  {
    "path": "docs/element-commands/check.md",
    "content": "---\ndescription: The command to select checkboxes or radio buttons.\n---\n\n# check\n\n## Syntax\n\n```python\nElement.check()\nElement.check(allow_selected)\n\n---or---\n\nElements.check()\nElements.check(allow_selected)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\n# check a radio button\npy.get('[type=\"radio\"]').check()\n\n---or---\n\n# check all boxes on the page\npy.find('[type=\"checkbox\"]').check()\n\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'get' yields an Element that is not a checkbox or radio button\npy.get('a').check()\n```\n{% endcode %}\n\n## Arguments\n\n* `allow_selected=False (bool)` - If **True,** do not raise an error if the box or radio button to check is _already_ selected.\n\n{% hint style=\"info\" %}\nDefault is **False** because why would you want to select a box that's already selected?\n{% endhint %}\n\n## Yields\n\n* **\\(Element or Elements\\)** The current instance so you can chain commands\n\n## Raises\n\n* **ValueError** if the element is selected already. Set `allow_selected` to **True** to ignore this.\n* **ValueError** if the element is not a checkbox or radio button\n\n"
  },
  {
    "path": "docs/element-commands/children.md",
    "content": "---\ndescription: The command to get the children of the element.\n---\n\n# children\n\n## Syntax\n\n```python\nElement.children()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get('a').children()\n\n---or--- # store in a variable\n\nelements = py.get('ul').children()\n\n---or--- # chain another command\n\nchild = py.get('ul').children().first()\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(Elements\\)** A list of Elements. The list is empty if no children are found.\n\n"
  },
  {
    "path": "docs/element-commands/click.md",
    "content": "---\ndescription: The command to click the element.\n---\n\n# click\n\n## Syntax\n\n```python\nElement.click()\nElement.click(force=True)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get('a').click()\n\n---or--- # chain a Pylenium command\n\npy.get('a').click().wait.until(lambda _: py.title == 'New Page')\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'click' yields Pylenium, not Element\npy.get('a').click().text\n```\n{% endcode %}\n\n## Arguments\n\n* **`force=False (bool)`** - If **True**, a JavascriptExecutor command is sent instead of Selenium's native `.click()`\n\n## Yields\n\n* **\\(Pylenium\\)** The current instance of Pylenium so you can chain commands.\n\n{% hint style=\"info\" %}\nYou can continue the chain if `.click()` opens a new view or navigates to a different page\n{% endhint %}\n\n"
  },
  {
    "path": "docs/element-commands/css_value.md",
    "content": "---\ndescription: Get the CSS Value of the element given the property name.\n---\n\n# css\\_value\n\n## Syntax\n\n```python\nElement.css_value(property_name)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get('a').css_value('background-color')\n\n---or--- # chain a Pylenium command\n\npy.find('a').first().get('span').css_value('color')\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'Pylenium' yields Pylenium, not Element\npy.css_value('color')\n\n---or---\n\n# Errors, 1 is not a valid property name\npy.get('#button').css_value(1)\n```\n{% endcode %}\n\n## Arguments\n\n* **`property_name (str)`** - The name of the CSS Property\n\n## Yields\n\n* **\\(Any\\)** Typically strings, but this depends on the CSS Property\n\n"
  },
  {
    "path": "docs/element-commands/deselect.md",
    "content": "---\ndescription: The command to deselect an <option> within a multi <select> element.\n---\n\n# deselect\n\n## Syntax\n\n```python\nElement.deselect(value)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get('select').deselect('option-2')\n\n---or--- # chain a Pylenium command\n\npy.get('select').deselect('locked').get('#start-edit').click()\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, can only perform 'deselect' on <select> elements\npy.get('ul > li').deselect('option-2')\n```\n{% endcode %}\n\n## Arguments\n\n* `value (str)` - The text or value of the option to deselect.\n\n## Yields\n\n* **\\(Pylenium\\)** The current instance of Pylenium so you can chain commands.\n\n"
  },
  {
    "path": "docs/element-commands/double_click.md",
    "content": "---\ndescription: The command to double click the element.\n---\n\n# double\\_click\n\n## Syntax\n\n```python\nElement.double_click()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get('a').double_click()\n\n---or--- # chain a Pylenium command\n\npy.get('a').double_click().switch_to.window(index=1)\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'double_click' yields Pylenium, not Element\npy.get('a').double_click().text\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(Pylenium\\)** The current instance of Pylenium so you can chain commands\n\n"
  },
  {
    "path": "docs/element-commands/drag_to.md",
    "content": "---\ndescription: >-\n  The command to drag the current element to another element given its CSS\n  selector.\n---\n\n# drag\\_to\n\n## Syntax\n\n```python\nElement.drag_to(css)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get('#drag-this').drag_to('#drop-here')\n\n---or---\n\nfrom_element = py.get('#drag-this')\nfrom_element.drag_to('#drop-here')\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'drag_to' takes a CSS selector string\n\nto_element = py.get('#drop-here')\npy.get('#drag-this').drag_to(to_element)\n\n# Use the .drag_to_element() command instead\n```\n{% endcode %}\n\n## Arguments\n\n* `css(str)` - The CSS selector of the element to drag to.\n\n## Yields\n\n* **\\(Element\\)** The current element that was dragged.\n\n"
  },
  {
    "path": "docs/element-commands/drag_to_element.md",
    "content": "---\ndescription: The command to drag the current element to the given element.\n---\n\n# drag\\_to\\_element\n\n## Syntax\n\n```python\nElement.drag_to_element(to_element)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\nelement = py.get('#drop-here')\npy.get('#drag-this').drag_to_element(element)\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'drag_to_element' takes an Element\npy.get('#drag-this').drag_to_element('#drop-here')\n\n# Use the .drag_to() command instead\n\n---or---\n\n# Errors, `drag_to_element` and `drag_to` are not part of the Pylenium object\nelement = py.get('#drop-here')\npy.drag_to_element(element)\n```\n{% endcode %}\n\n## Arguments\n\n* `to_element(Element)` - The destination element to drag to.\n\n## Yields\n\n* **\\(Element\\)** The current element that was dragged.\n\n"
  },
  {
    "path": "docs/element-commands/first.md",
    "content": "---\ndescription: The command to get the first Element in a list of Elements.\n---\n\n# first\n\n## Syntax\n\n```python\nElements.first()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.find('li').first()\n\n---or--- # store in a variable\n\nelement = py.xpath('//a').first()\n\n---or--- # chain an Element command\n\npy.get('ul > li').children().first().click()\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'get' yields Element, not Elements\npy.get('ul > li').first()\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(Element\\)** The first Element in the list of Elements\n\n## Raises\n\n* **IndexError** if Elements is empty\n\n"
  },
  {
    "path": "docs/element-commands/get_attribute.md",
    "content": "---\ndescription: The command to get the attribute's value with the given name.\n---\n\n# get\\_attribute\n\n## Syntax\n\n```python\nElement.get_attribute(attribute)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get('a').get_attribute('href')\n\n---or--- # store in a variable\n\nhref = py.get('a').get_attribute('href')\n```\n{% endcode %}\n\n## Arguments\n\n* `attribute (str)` = The name of the attribute to find in the Element\n\n## Yields\n\n* If the value is `'true'` or `'false\"`, then this returns a bool of **True** or **False**\n* If the name does not exist, return **None**\n* All other values are returned as strings\n\n"
  },
  {
    "path": "docs/element-commands/get_property.md",
    "content": "---\ndescription: The command to get the specified property's value of the element.\n---\n\n# get\\_property\n\n## Syntax\n\n```python\nElement.get_property(property)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get('.nav-link').get_property('innerHTML')\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'py' cannot call this directly\npy.get_property('className')\n```\n{% endcode %}\n\n## Arguments\n\n* `property (str)`:  The name of the property.\n\n## Yields\n\n* The value returned by the property, but this is usually a string.\n\n"
  },
  {
    "path": "docs/element-commands/hover.md",
    "content": "---\ndescription: The command to hover the element.\n---\n\n# hover\n\n## Syntax\n\n```python\nElement.hover()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get('.menu').hover()\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'hover' yields Pylenium, not Element\npy.get('.menu').hover().click()\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(Pylenium\\)** The current instance of Pylenium so you can chain commands\n\n## Examples\n\nOriginally, `.hover()` returned the current Element, but most hovering scenarios revealed items that were _not_ descendants of the hovered element.\n\n```python\npy.get('.menu').hover().contains('About').click()\n```\n\n"
  },
  {
    "path": "docs/element-commands/is_checked.md",
    "content": "---\ndescription: The command to check if this element is checked.\n---\n\n# is\\_checked\n\n## Syntax\n\n```python\nElement.is_checked()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get('#checkbox').is_checked()\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(bool\\)** True if the element is checked, else False\n\n"
  },
  {
    "path": "docs/element-commands/is_displayed.md",
    "content": "---\ndescription: The command to check if this element is displayed.\n---\n\n# is\\_displayed\n\n## Syntax\n\n```python\nElement.is_displayed()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get('#button').is_displayed()\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(bool\\)** True if the element is displayed, else False\n\n{% hint style=\"info\" %}\n\"displayed\" means that the element is in the DOM and has a size greater than zero such that it is visible to the user.\n{% endhint %}\n\n"
  },
  {
    "path": "docs/element-commands/is_empty.md",
    "content": "---\ndescription: The command to check if the list of elements is empty.\n---\n\n# is\\_empty\n\n## Syntax\n\n```python\nElements.is_empty()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.find('a.hidden-link').is_empty()\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'Element' is not a list\npy.get('a').is_empty()\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(bool\\)** True if the length is zero, else False\n\n"
  },
  {
    "path": "docs/element-commands/is_enabled.md",
    "content": "---\ndescription: The command to check if the element is enabled.\n---\n\n# is\\_enabled\n\n## Syntax\n\n```python\nElement.is_enabled()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get('#button').is_enabled()\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(bool\\)** True if the element is enabled, else False\n\n"
  },
  {
    "path": "docs/element-commands/is_selected.md",
    "content": "---\ndescription: The command that checks if the element is selected.\n---\n\n# is\\_selected\n\n## Syntax\n\n```python\nElement.is_selected()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get('.option').is_selected()\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(bool\\)** True if the element is selected, else False\n\n"
  },
  {
    "path": "docs/element-commands/last.md",
    "content": "---\ndescription: The command to get the last Element in a list of Elements.\n---\n\n# last\n\n## Syntax\n\n```python\nElements.last()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.find('li').last()\n\n---or--- # store in variable\n\nlast = py.xpath('//a').last()\n\n---or--- # chain an Element command\n\npy.get('ul > li').children().last().click()\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'get' yields Element, not Elements\npy.get('ul > li').last()\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(Element\\)** The last Element in a list of Elements\n\n## Raises\n\n* **IndexError** if Elements is empty\n\n"
  },
  {
    "path": "docs/element-commands/length.md",
    "content": "---\ndescription: The command to get the length of Elements.\n---\n\n# length\n\n## Syntax\n\n```python\nElements.length()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\nassert py.find('li').length() == 5\n\n---or---\n\nassert py.find('li').should().have_length(5)\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'get' yields Element, not Elements\nassert py.get('li').length() == 1\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(int\\)** The length of the Elements\n\n"
  },
  {
    "path": "docs/element-commands/open_shadow_dom.md",
    "content": "---\ndescription: The command to open/expand a Shadow DOM element.\n---\n\n# open\\_shadow\\_dom\n\n## Syntax\n\n```python\nElement.open_shadow_dom()\n```\n\n## Usage\n\nShadow DOMs are a bit tricky because, like iframes, you need to \"switch\" to its context to find elements or objects within it. Check out this example using `chrome://extensions`:\n\n```python\ndef test_loading_extension_to_browser(py):\n    py.visit('chrome://extensions/')\n    shadow1 = py.get('extensions-manager').open_shadow_dom()\n    shadow2 = shadow1.get('extensions-item-list').open_shadow_dom()\n    extension_shadow_dom = shadow2.find('extensions-item')[1].open_shadow_dom()\n    assert extension_shadow_dom.get('#name-and-version').should().contain_text('Get CRX')\n```\n\n## Yields\n\n* `The Shadow Root (Element)`. With this element you can search for things within the Shadow context.\n\n"
  },
  {
    "path": "docs/element-commands/parent.md",
    "content": "---\ndescription: The command to get the parent of the element.\n---\n\n# parent\n\n## Syntax\n\n```python\nElement.parent()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get('li').parent()\n\n---or--- # store in a variable\n\nelement = py.get('li').parent()\n\n---or--- # chain an Element command\n\npy.get('li').parent().click()\n\n---or--- # even go up the DOM tree\n\ngrand_parent = py.get('li').parent().parent()\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(Element\\)** The parent Element\n\n"
  },
  {
    "path": "docs/element-commands/right_click.md",
    "content": "---\ndescription: The command to right-click the element.\n---\n\n# right\\_click\n\n## Syntax\n\n```text\nElement.right_click()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get('#context-menu').right_click()\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# 'py' does not have this command\npy.right_click()\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(Pylenium\\)** so you can chain another command\n\n"
  },
  {
    "path": "docs/element-commands/screenshot.md",
    "content": "---\ndescription: The command to take a screenshot of the element.\n---\n\n# screenshot\n\n## Syntax\n\n```python\nElement.screenshot(filename)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get('a[href=\"/about\"]').screenshot('elements/about-link.png')\n\n---or--- # chain an Element command\n\npy.get('#save-button').screenshot('save-.png').click()\n```\n{% endcode %}\n\n## Arguments\n\n* `filename (str)` - The file path including the file name and extension like `.png`\n\n## Yields\n\n* **\\(Element\\)** The current instance of Element so you can chain commands\n\n"
  },
  {
    "path": "docs/element-commands/scroll_into_view.md",
    "content": "---\ndescription: The command to scroll this element into the viewport\n---\n\n# scroll\\_into\\_view\n\n## Syntax\n\n```text\nElement.scroll_into_view()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get('#footer-link').scroll_into_view()\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# 'py' does not have this command\npy.scroll_into_view()\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(Element\\)** so you can chain another command\n\n"
  },
  {
    "path": "docs/element-commands/select.md",
    "content": "---\ndescription: The command to select an <option> within a <select> element.\n---\n\n# select\n\n## Syntax\n\n```python\nElement.select(value)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get('select').select('option-2')\n\n---or--- # chain an Element command\n\npy.get('#dropdown').select('option-1').select('option-2')\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, can only perform 'select' on a <select> element\npy.get('ul > li').select('option-2')\n```\n{% endcode %}\n\n## Arguments\n\n* `value (str)` - The **text** or **value** or **index** of the option to select.\n\n## Yields\n\n* **\\(Element\\)** The current instance of Element so you can chain commands.\n\n## Examples\n\n{% code title=\"select by index\" %}\n```python\n# select the second option in the list\npy.get('select').select(1)\n```\n{% endcode %}\n\n{% code title=\"select by value or visible text\" %}\n```python\n# select the option with the text or value\npy.get('select').select('option2')\n```\n{% endcode %}\n\n"
  },
  {
    "path": "docs/element-commands/select_many.md",
    "content": "---\ndescription: The command to select multiple <option>s in a multi <select> element.\n---\n\n# select\\_many\n\n## Syntax\n\n```python\nElement.select_many(values)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get('select').select_many(['option-1', 2])\n\n---or--- # chain a Pylenium command\n\npy.get('select').select_many(['opt-1', 'opt-2']).screenshot('new_view.png')\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, can only perform 'select_many' on a multi <select> element\npy.get('ul > li').select_many(['option-1', 2])\n```\n{% endcode %}\n\n## Arguments\n\n* `values (list)` - The list of texts or values of the &lt;option&gt;s you want to select.\n\n## Yields\n\n* **\\(Pylenium\\)** The current instance of Pylenium so you can chain commands.\n\n"
  },
  {
    "path": "docs/element-commands/should.md",
    "content": "---\ndescription: A collection of expectations for the current Element or Elements.\n---\n\n# should\n\n## Element Expectations \\(ElementShould\\)\n\n### Positive Conditions\n\n* `.be_checked()`\n* `.be_clickable()`\n* `.be_disabled()`\n* `.be_enabled()`\n* `.be_focused()`\n* `.be_hidden()`\n* `.be_selected()`\n* `.be_visible()`\n* `.contain_text()`\n* `.disappear()`\n* `.have_attr()`\n* `.have_class()`\n* `.have_prop()`\n* `.have_text()`\n* `.have_value()`\n\n### Negative Conditions\n\n* `.not_be_focused()`\n* `.not_have_attr()`\n* `.not_have_text()`\n* `.not_have_value()`\n\n## Elements Expectations \\(ElementsShould\\)\n\n### Positive Conditions\n\n* `.be_empty()`\n* `.have_length()`\n* `.be_greater_than()`\n* `.be_less_than()`\n\n### Negative Conditions\n\n* `.not_be_empty()`\n\n## Syntax\n\n```python\n# use the default wait_time\nElement.should().<expectation>\nElements.should().<expectation>\n\n---or---\n\n# customize the wait_time for this expectation\nElement.should(timeout).<expectation>\nElements.should(timeout).<expectation>\n\n---or---\n\n# ignore exceptions that you expect to \"get in the way\"\nElement.should(ignored_exceptions).<expectation>\nElements.should(ignored_exceptions).<expectation>\n\n---or---\n\n# customize both fully\nElement.should(timeout, ignored_exceptions).<expectation>\nElements.should(timeout, ignored_exceptions).<expectation>\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get('#about-link').should().have_text('About')\n\n---or---\n\npy.find('li > a').should().have_length(10)\n```\n{% endcode %}\n\n## Arguments\n\n### No args\n\n* `.be_checked()`\n* `.be_clickable()`\n* `.be_disabled()`\n* `.be_enabled()`\n* `.be_focused()`\n* `.be_hidden()`\n* `.be_selected()`\n* `.be_visible()`\n* `.disappear()`\n* `.not_be_focused()`\n* `.be_empty()`\n* `.not_be_empty()`\n\n### Have args\n\n* `.contain_text(text)`\n* `.have_attr(attribute, value=None)`\n* `.have_class(class_name)`\n* `.have_prop(property)`\n* `.have_text(text, case_sensitive=True)`\n* `.have_value(value)`\n* `.not_have_attr(attribute)`\n* `.not_have_text(text)`\n* `.not_have_value(value)`\n* `.have_length(int)`\n* `.be_greater_than(int)`\n* `.be_less_than(int)`\n\n## Yields\n\n* **\\(Element\\)** If the assertion passes, then the current Element is returned, else an **AssertionError** is raised if the condition is not met within the specified timeout.\n\n"
  },
  {
    "path": "docs/element-commands/siblings.md",
    "content": "---\ndescription: The command to get the siblings of the element.\n---\n\n# siblings\n\n## Syntax\n\n```python\nElement.siblings()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get('a').siblings()\n\n---or--- # store in a variable\n\nelements = py.get('.nav-link').siblings()\n\n---or--- # chain an Elements command\n\nsibling = py.get('ul').siblings().first()\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(Elements\\)** A list of Elements. The list is empty if no siblings are found.\n\n"
  },
  {
    "path": "docs/element-commands/submit.md",
    "content": "---\ndescription: The command to submit a form.\n---\n\n# submit\n\n## Syntax\n\n```python\nElement.submit()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get('form').submit()\n\n---or---\n\npy.get('input[type=\"submit\"]').submit()\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'submit' may have no effect on certain elements\npy.get('a').submit()\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(Pylenium\\)** The current instance of Pylenium so you can chain commands.\n\n{% hint style=\"info\" %}\nYou can continue the chain if `.submit()` opens a new view or navigates to a different page\n{% endhint %}\n\n"
  },
  {
    "path": "docs/element-commands/tag_name.md",
    "content": "---\ndescription: The command that gets the current Element's tag name.\n---\n\n# tag\\_name\n\n## Syntax\n\n```python\nElement.tag_name()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\nassert py.get('.btn').tag_name() == 'button'\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'tag_name' is not a property\npy.get('a').tag_name\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(str\\)** The tag name of the current Element\n\n"
  },
  {
    "path": "docs/element-commands/text.md",
    "content": "---\ndescription: The command to get the text of the current Element.\n---\n\n# text\n\n## Syntax\n\n```python\nElement.text()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\nassert py.get('.nav.link').text() == 'About'\n\n---or---\n\nassert py.get('.nav.link').should().have_text('About')\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'text' is not a property\npy.get('a').text\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(str\\)** The text of the current Element\n\n"
  },
  {
    "path": "docs/element-commands/type.md",
    "content": "---\ndescription: 'The command to type keys into a field, input or text box.'\n---\n\n# type\n\n## Syntax\n\n```python\nElement.type(*args)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get('#username').type('my-username')\n\n---or--- # combine with other keys and strings\n\npy.get('#search').type('puppies', py.Keys.ENTER)\n\n---or--- # chain an Element command\n\npy.get('#email').type('foo@example.com').get_attribute('value')\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'type' may have no effect on other types of elements\npy.get('a').type('foo')\n```\n{% endcode %}\n\n## Arguments\n\n* `*args (Any)` - A comma-separated list of arguments to type\n\n{% hint style=\"success\" %}\nIt's best to use **strings** and the **Keys** class\n{% endhint %}\n\n## Yields\n\n* **\\(Element\\)** The current Element so you can chain commands\n\n"
  },
  {
    "path": "docs/element-commands/uncheck.md",
    "content": "---\ndescription: The command to deselect checkboxes and radio buttons.\n---\n\n# uncheck\n\n## Syntax\n\n```python\nElement.uncheck()\nElement.uncheck(allow_selected)\n\n---or---\n\nElements.uncheck()\nElements.uncheck(allow_selected)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\n# uncheck a radio button\npy.get('[type=\"radio\"]').uncheck()\n\n---or---\n\n# uncheck all boxes on the page\npy.find('[type=\"checkbox\"]').uncheck()\n\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'get' yields an Element that is not a checkbox or radio button\npy.get('a').uncheck()\n```\n{% endcode %}\n\n## Arguments\n\n* `allow_deselected=False (bool)` - If **True,** do not raise an error if the box or radio button to uncheck is _already_ deselected.\n\n{% hint style=\"info\" %}\nDefault is **False** because why would you want to deselect a box that's not selected?\n{% endhint %}\n\n## Yields\n\n* **\\(Element or Elements\\)** The current instance so you can chain commands\n\n## Raises\n\n* **ValueError** if the element is not selected already. Set `allow_deselected` to **True** to ignore this.\n* **ValueError** if the element is not a checkbox or radio button\n\n"
  },
  {
    "path": "docs/element-commands/webelement.md",
    "content": "---\ndescription: >-\n  The property that is the current instance of Selenium's WebElement that\n  Element is wrapping.\n---\n\n# webelement\n\n## Syntax\n\n```python\nElement.webelement\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get('a').webelement\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'webelement' is not a function\npy.get('a').webelement()\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(WebElement\\)** The current instance of Selenium's **WebElement** that is wrapped by **Element**\n\n## Examples\n\nMost scenarios won't need this, but it's provided just in case. The biggest reasons to use `.webelement`\n\n* access functionality that may not exist in Pylenium\n* functionality that requires you pass in a WebElement\n\n```python\n# Using the expected_conditions as EC\nelement = py.get('.loading-spinner')\npy.wait.until(EC.staleness_of(element.webelement))\n```\n\n"
  },
  {
    "path": "docs/examples/test_sample.py",
    "content": "\"\"\" Examples will be added to this directory and its files.\n\nHowever, the best source for info and details is in the\nofficial documentation here:\n\nhttps://elsnoman.gitbook.io/pylenium\n\nYou can also contact the author, @CarlosKidman\non Twitter or LinkedIn.\n\"\"\"\n\n# You can mix Selenium into some Pylenium commands\nfrom selenium.webdriver.common.by import By\nfrom selenium.webdriver.support import expected_conditions as ec\n\n\n# pass in the `py` fixture into your test function\n# this _is_ Pylenium!\ndef test_pylenium_basics(py):\n    # Use Cypress-like commands like `.visit()`\n    py.visit(\"https://google.com\")\n    # `.get()` uses CSS to locate a single element\n    py.get('[name=\"q\"]').type(\"puppies\", py.Keys.ENTER)\n    # `assert` followed by a boolean expression\n    assert \"puppies\" in py.title\n\n\ndef test_access_selenium(py):\n    py.visit(\"https://google.com\")\n    # access the wrapped WebDriver with `py.webdriver`\n    search_field = py.webdriver.find_element(By.CSS_SELECTOR, \"[name='q']\")\n    # access the wrapped WebElement with `Element.webelement`\n    assert py.get('[name\"q\"]').webelement.is_enabled()\n    # you can store elements and objects to be used later since\n    # we don't rely on Promises or chaining in Python\n    search_field.send_keys(\"puppies\", py.Keys.ENTER)\n    assert \"puppies\" in py.title\n\n\ndef test_chaining_commands(py):\n    py.visit(\"https://google.com\").get('[name=\"q\"]').type(\"puppies\", py.Keys.ENTER)\n    assert \"puppies\" in py.title\n\n\ndef test_waiting(py):\n    py.visit(\"https://google.com\")\n    # wait using expected conditions\n    # default `.wait()` uses WebDriverWait which returns Selenium's WebElement objects\n    py.wait().until(ec.visibility_of_element_located((By.CSS_SELECTOR, '[name=\"q\"]'))).send_keys(\"puppies\")\n    # use_py=True to use a PyleniumWait which returns Pylenium's Element and Elements objects\n    py.wait(use_py=True).until(lambda _: py.get('[name=\"q\"]')).type(py.Keys.ENTER)\n    # wait using lambda function\n    assert py.wait().until(lambda x: \"puppies\" in x.title)\n"
  },
  {
    "path": "docs/fixtures/axe.md",
    "content": "---\ndescription: Accessibility (A11y) Testing with aXe\n---\n\n# axe\n\n## Usage\n\nThe `axe` fixture is the recommended way to run A11y audits since it's so easy and straightforward.\n\n```python\ndef test_axe_fixture(py, axe):\n    py.visit('https://qap.dev')\n    # save the axe report as a file\n    report = axe.run(name='a11y_audit.json')\n    # and/or use the report directly in the test(s)\n    assert len(report.violations) == 0\n```\n\n{% hint style=\"success\" %}\n You can generate the report as a JSON and/or use the`AxeReport` object directly.\n{% endhint %}\n\n{% hint style=\"warning\" %}\nRunning an audit will generate a JSON report _only_ if a `name` is given.\n{% endhint %}\n\n{% code title=\"function signature\" %}\n```python\ndef run(name: str = None, context: Dict = None, options: Dict = None) -> AxeReport\n```\n{% endcode %}\n\n## Arguments\n\n* `name: str` The file path \\(including name and `.json` extension\\) of the report to save as a JSON\n* `context: Dict` The dictionary of page part\\(s\\), by CSS Selectors, to include or exclude in the audit\n* `options: Dict` The dictionary of aXe options to include in the audit\n\n{% hint style=\"info\" %}\nVisit the official aXe documentation for more information about the `context` and `options` arguments.\n\n[https://github.com/dequelabs/axe-core/blob/master/doc/API.md\\#parameters-axerun](https://github.com/dequelabs/axe-core/blob/master/doc/API.md#parameters-axerun)\n{% endhint %}\n\n## Yields\n\n* `AxeReport` object that represents the audit report in code\n\nIf you include the `name` argument, then that report is also created at the specified file path.\n\n{% hint style=\"danger\" %}\nIf any of the directories in the path do not exist, then a `FileNotFound` error is raised.\n{% endhint %}\n\n"
  },
  {
    "path": "docs/fixtures/fake.md",
    "content": "---\ndescription: A basic instance of Faker to generate test data.\n---\n\n# fake\n\n## What is Faker?\n\nPut simply, Faker is a library that generates fake data for you.\n\n{% hint style=\"info\" %}\nCheck out their docs when you have some time: [https://faker.readthedocs.io](https://faker.readthedocs.io/en/stable/index.html)\n{% endhint %}\n\n## Three Ways to Use it\n\n* `py.fake` - A basic Faker instance for UI tests\n* `fake fixture` - A fixture of Faker for any tests\n* `Create your own` - Some users may need advanced functionality like **Locales** and **Providers**\n\n{% hint style=\"info\" %}\nThis doc will go over the first two. Check out their docs for more advanced usage.\n{% endhint %}\n\n## Syntax\n\n```python\n# Faker instance for UI tests\npy.fake\n\n---or---\n\n# fake fixture to be used in any tests\ndef test_(fake)\n```\n\n## Usage\n\n{% code title=\"py.fake\" %}\n```python\ndef test_new_user_flow(py):\n    py.visit('https://some-page.com')\n    py.get('#email').type(py.fake.email())\n    py.get('#password').type(py.fake.password())\n    py.contains('Login').click()\n    assert py.contains('Success!')\n```\n{% endcode %}\n\n{% code title=\"fake fixture\" %}\n```python\ndef test_fake_cc_expire(fake):\n    fake.credit_card_expire(start='now', end='+10y', date_format='%m/%y')\n    # '07/27'\n    \n\ndef test_fake_address(fake):\n    fake.address()\n    # '00232 Isabel Creek\\nReynoldsport, CA 05875'\n```\n{% endcode %}\n\n## What can I fake?\n\nTo see a full list of all the default providers that come out of the box, go to the link below:\n\n{% embed url=\"https://faker.readthedocs.io/en/stable/providers.html\" %}\n\n## FAQs\n\nWhen I type `py.fake.`, I'm not seeing `address()` or anything else in your examples. What gives?\n\n* Because of the way Faker works with their Providers, you don't get IntelliSense. This is good and bad. Bad because you don't see all the options that are available, but good because you can create your own, custom Providers to generate almost everything you'd need for your applications and systems. Just type `py.fake.address()` and it will work!\n\nWhich of the three approaches should I use?\n\n* It's entirely up to you and your needs and style. This is completely valid:\n\n```python\ndef test_a_page(py, fake):\n    py.visit('https://page.').get('#email').type(fake.email())\n```\n\n* If you need more advanced power, you can always create your own instance of Faker:\n\n```python\nfrom faker import Faker\n\nfake = Faker()\n```\n\n"
  },
  {
    "path": "docs/fixtures/requests.md",
    "content": "---\ndescription: A library for working with HTTP Clients and APIs.\n---\n\n# api \\(aka requests\\)\n\n## What is requests?\n\n**Requests** is an elegant and simple HTTP library for Python, built for human beings.\n\n{% embed url=\"https://2.python-requests.org/en/master/\" %}\n\n## Three Ways to Use it\n\n* `py.request` - A basic **requests** instance for UI tests \\(V1\\)\n* `api fixture` - A fixture of **requests** for any tests\n* `import requests` - Simply use the import statement to bring it into any file!\n\n## Syntax\n\n```python\n# for UI tests\npy.request\n\n---or--- # use the fixture\n\ndef test_(api)\n\n---or--- # just import it\n\nimport requests\n```\n\n## Usage\n\n{% code title=\"py.request\" %}\n```python\nBASE_URL = 'https://statsroyale.com'\n\n\ndef test_py_request(py):\n    py.visit(BASE_URL)\n    response = py.request.get(f'{BASE_URL}/api/cards')\n    assert response.ok\n    assert response.json()[0]['name'] == 'Royal Ghost'\n```\n{% endcode %}\n\n{% code title=\"api fixture\" %}\n```python\ndef test_api_fixture(api):\n    response = api.request.get(f'{BASE_URL}/api/cards')\n```\n{% endcode %}\n\n{% code title=\"import\" %}\n```python\nimport requests\n\nresponse = requests.get(f'{BASE_URL}/api/cards')\n```\n{% endcode %}\n\n## CRUD\n\nRequests provides everything you need out of the box, but these are probably the actions you want :\\)\n\n### GET\n\n```python\nrequests.get()\n```\n\n### POST\n\n```python\nrequests.post()\n```\n\n### DELETE\n\n```python\nrequests.delete()\n```\n\n### PATCH\n\n```python\nrequests.patch()\n```\n\n### PUT\n\n```python\nrequests.put()\n```\n\n"
  },
  {
    "path": "docs/getting-started/project-structure-with-pytest.md",
    "content": "---\ndescription: pytest uses specific naming conventions and project structure\n---\n\n# 3. Project Structure with pytest\n\n## Pylenium Files\n\nYou should have created these in the previous step, but they are required for **Pylenium** to do its magic.\n\n* `conftest.py`\n* `pylenium.json`\n* `pytest.ini`\n\n{% hint style=\"success\" %}\nMake sure these are at the Project Root \\(aka Workspace\\)\n{% endhint %}\n\n## conftest.py\n\npytest uses special functions called **Fixtures** to control the **Setup** and **Teardown** of tests and runs.\n\n{% hint style=\"danger\" %}\nIf you put any other **custom** functions or fixtures in this **conftest.py**, they will be _overridden_ when you upgrade Pylenium\n{% endhint %}\n\n### Fixture Example\n\n```python\nimport pytest\n\n@pytest.fixture\ndef user():\n    new_user = user_service.create()\n    yield new_user\n    user_service.delete(new_user)\n```\n\n* `@pytest.fixture` - this decorator indicates that this function has a Setup and Teardown \n* `def user():` - define the function like normal. `user` will be the name of the fixture to be used in tests\n* Everything _before_ the `yield` is executed before each test\n* `yield new_user` - returns `new_user` and gives control back to the test. The rest of the function is not executed yet\n* Everything _after_ the `yield` is executed after each test\n\n### Use the Fixture\n\n{% code title=\"test\\_\\*.py file\" %}\n```bash\ndef test_my_website(py, login, user):\n    py.visit('https://qap.dev')\n    login.with(user)\n    ...\n```\n{% endcode %}\n\nWhen this test is ran:\n\n1. test - The test looks at its parameter list and calls the `py` fixture\n2. fixture - `user` yields the newly created user\n3. test - line 2 is executed by navigating to `https://qap.dev` and then logging in with the new user\n4. fixture - test is complete \\(doesn't matter if it passes or fails\\) and `user_service.delete_user()` is executed\n\n### Folder Structure\n\nThe `conftest.py` file is used to _store_ fixtures and make them available to any tests in their **Scope**.\n\n{% hint style=\"info\" %}\n**Scope** refers to the file's siblings and their descendants.\n{% endhint %}\n\nTake a look at the following Project Structure\n\n* Project\n  * conftest.py  \\# 1\n  * pylenium.json\n  * api\\_tests\n    * conftest.py  \\# 2 \n    * test\\_rest\\_api.py\n  * ui\\_tests\n    * conftest.py  \\# 3\n    * test\\_google.py\n\n`test_google.py` would have access to fixtures in `conftest.py #1` and `conftest.py #3`\n\n`test_rest_api.py` would have access to fixtures in `conftest.py #1` and `conftest.py #2`\n\n## Test Naming Conventions\n\nBy now it might be obvious that pytest has specific naming conventions by default.\n\n### Folders and Files\n\n* You may want to store your tests in a `/tests` directory \\(optional\\)\n* You may want to make files easily identified as test files, so use `test_*.py` \\(optional\\)\n\n{% hint style=\"info\" %}\nThese techniques help you and the Test Runner discover/find and execute your tests more easily, but they are not required. Do what works best for you and your team.\n{% endhint %}\n\n**pytest** can run tests based off of directories or files, so you can group tests into **Suites** this way.\n\n* Project\n  * tests\n    * ui\n      * test\\_login.py\n      * test\\_checkout.py\n    * api\n      * test\\_payment.py\n      * test\\_user.py\n    * unit\n\n```bash\n# run all tests\n$ python -m pytest tests\n\n# run tests in ui directory\n$ python -m pytest tests/ui\n\n# run only the payment api tests\n$ python -m pytest tests/api/test_payment.py\n```\n\n### Classes\n\nYou _can_ group tests into Suites using Classes.\n\n{% hint style=\"danger\" %}\nThis is not the recommended approach for beginners\n{% endhint %}\n\n```python\ndef TestCheckout:\n\n    def test_with_visa(self, py):\n        # test code\n    \n    def test_with_mastercard(self, py):\n        # test code\n```\n\n* The class must start with the word `Test`\n* Test functions must have `self` as their first parameter \\(since they are in a class\\)\n\n{% hint style=\"info\" %}\nYou can have as many Test Classes and Test Functions as you want in a file\n{% endhint %}\n\n### Test Functions\n\nTests do NOT need to be in a Test Class. They can exist by themselves in a file and makes the tests and overall file look much cleaner.\n\n{% hint style=\"success\" %}\nRECOMMEND this approach for working with Pylenium for beginners and everyone else really\n{% endhint %}\n\n{% code title=\"test\\_checkout.py\" %}\n```python\ndef test_with_visa(py):\n    # test_code\n    \ndef test_with_mastercard(py):\n    # test_code\n```\n{% endcode %}\n\n* Test names must start with `test_`, but can have anything else after that\n\n{% hint style=\"danger\" %}\nTests should not _share_ **data** or **state**.\n{% endhint %}\n\n{% hint style=\"success\" %}\nTests should be **modular**, **deterministic** and **meaningful**\n{% endhint %}\n\nPylenium is architected in a way that makes test design easy and intuitive, but also gives you a lot of things for free. **The framework is already designed to be scaled with containerized solutions like Docker and Kubernetes.**\n\n"
  },
  {
    "path": "docs/getting-started/setup-pytest.md",
    "content": "---\ndescription: >-\n  pytest is a modern and powerful Test Framework and we want to get intellisense\n  and autocomplete\n---\n\n# 2. Setup pytest\n\n## 1. Install pyleniumio\n\nInstall **Pylenium** into your [Virtual Environment](virtual-environments.md) if you haven't already:\n\n{% tabs %}\n{% tab title=\"pip\" %}\n{% code title=\"Terminal $ \\(venv\\)\" %}\n```bash\npip install pyleniumio\n```\n{% endcode %}\n{% endtab %}\n\n{% tab title=\"pipenv\" %}\n{% code title=\"Terminal $ \\(venv\\)\" %}\n```text\npipenv install pyleniumio\n```\n{% endcode %}\n{% endtab %}\n{% endtabs %}\n\n## 2. Initialize Pylenium\n\n{% code title=\"Terminal $ \\(venv\\)\" %}\n```text\npylenium init\n```\n{% endcode %}\n\n{% hint style=\"success\" %}\nExecute this command at your Project Root\n{% endhint %}\n\nThis creates three files:\n\n* `conftest.py` - This has the fixtures needed for Pylenium.\n* `pylenium.json` - This is the [configuration ](../configuration/pylenium.json.md)file for Pylenium.\n* `pytest.ini` - This is the configuration file for pytest and is used to connect to [ReportPortal](../cli/report-portal.md)\n\nBy default, pylenium uses Chrome browser. You have to install Chrome or update the `pylenium.json` to use the browser of your choice.\n\n## 3. Select pytest as the Test Framework\n\nTo get the most out of your IDE, you need to configure it to use **pytest** as the Test Framework. This will give you:\n\n* Intellisense\n* Autocomplete\n* Run/Debug Test functionality with breakpoints\n* more depending on IDE\n\n{% tabs %}\n{% tab title=\"PyCharm\" %}\n{% code title=\"\\(RECOMMENDED IDE\\)\" %}\n```text\nOpen Preferences (or Settings)\nOpen Tools > Python Integrated Tools\nSelect pytest in the \"Default test runner\" dropdown\n```\n{% endcode %}\n{% endtab %}\n\n{% tab title=\"VS Code\" %}\n```text\nOpen Command Palette (CMD + SHIFT + P or CTRL + SHIFT + P)\nSearch for \"Python: Configure Tests\"\nSelect pytest\n\n# VS Code doesn't fully support pytest so you won't get things like IntelliSense\n```\n{% endtab %}\n{% endtabs %}\n\n{% hint style=\"info\" %}\nVisit the pytest docs for more info on how to use it: [https://docs.pytest.org/](https://docs.pytest.org/)\n{% endhint %}\n\n"
  },
  {
    "path": "docs/getting-started/virtual-environments.md",
    "content": "---\ndescription: 'This is the first, critical piece to modern software development with Python.'\n---\n\n# 1. Virtual Environments\n\n## A Virtual Environment is required\n\n**PyCharm** creates a venv by default when you create a new Project.\n\n{% hint style=\"success\" %}\nYou can skip this step if you already have a Virtual Environment in your Project\n{% endhint %}\n\n## What is a Virtual Environment? \n\nWithout Virtual Environments \\(**venv**\\), everything you install would be global to your machine. Every project you have would be sharing the same packages and dependencies which could cause clashes or unwanted side effects.\n\nLuckily, **venvs** are easy to setup. Open a Terminal in the context of your Project Directory.\n\n{% hint style=\"warning\" %}\nWe assume you already have **python3** installed on your machine\n{% endhint %}\n\n{% tabs %}\n{% tab title=\"Mac\" %}\n```bash\n$ python3 --version\n# should print 3.x.x\n\n$ python3 -m venv \"venv\"\n```\n{% endtab %}\n\n{% tab title=\"Windows\" %}\n```bash\n$ python --version\n# should print 3.x.x\n \n$ python -m venv \"venv\"\n```\n{% endtab %}\n{% endtabs %}\n\n{% hint style=\"info\" %}\nDownload Python if you haven't already [https://www.python.org/downloads/](https://www.python.org/downloads/)\n{% endhint %}\n\nDepending on your IDE, it _should_ automatically detect that a Virtual Environment has been created and ask if it should use it. Accept :\\)\n\nOtherwise, you can manually configure your IDE to use the Virtual Environment.\n\n{% tabs %}\n{% tab title=\"VS Code\" %}\n```text\nInstall the Python extension\nOpen the Command Palette (CMD + SHIFT + P or CTRL + SHIFT + P)\nSearch for \"Python: Select Interpreter\"\nSelect the venv for your Project\n```\n{% endtab %}\n\n{% tab title=\"PyCharm\" %}\n```\nOpen Preferences (or Settings)\nOpen Project > Project Interpreter\nSelect the venv for your Project in the Project Interpreter dropdown\nClick APPLY, then OK\n```\n{% endtab %}\n{% endtabs %}\n\nKill all Terminal sessions, then reopen a Terminal. It should now open and activate the Virtual Environment automatically. This is indicated by the **\\(venv\\)** prefix as seen in the example below:\n\n{% code title=\"New Terminal\" %}\n```bash\n$ (venv) python --version\n# should print 3.x.x for Mac or Windows.\n# Mac users don't need to use python3 or pip3 anymore!\n\n```\n{% endcode %}\n\n{% hint style=\"info\" %}\nReal Python goes more in-depth on their website: [Virtual Environments](https://realpython.com/python-virtual-environments-a-primer/)\n{% endhint %}\n\n"
  },
  {
    "path": "docs/getting-started/writing-tests-with-pylenium.md",
    "content": "---\ndescription: Easy as py\n---\n\n# 4. Writing Tests with Pylenium\n\n## Create a Test File\n\nA **Test File** is just that - a file with tests in it. Depending on the type of project you're working on, you may want these to live right next to your code files or in a separate `/tests` directory.\n\n{% hint style=\"info\" %}\nIn these docs we will assume you are writing your tests in a `/tests` directory of your project.\n{% endhint %}\n\nCreate a Test File called `test_qap_dev.py`\n\n* Test Files do not need to start with `test_`\n* This is a naming convention used to make it easier to distinguish between code and tests\n\nYou should now have a Project Structure that looks like this:\n\n* Project\n  * tests\n    * test\\_qap\\_dev.py\n  * conftest.py\n  * venv\n\n## Write the Test\n\nWe are going to make a test that does the following:\n\n1. _Visits_ the **QA at the Point** website: [https://qap.dev](https://qap.dev)\n2. _Hovers_ the **About** link to reveal a menu\n3. _Click_ the **Leadership** link in that menu\n4. _Assert_ **Carlos Kidman** is on the Leadership page\n\n{% code title=\"test\\_qap\\_dev.py\" %}\n```python\ndef test_carlos_is_on_leadership(py):\n    py.\n```\n{% endcode %}\n\nWhen you type `py.`, you should see an **auto-complete** or **IntelliSense** menu appear with the list of [Pylenium Commands](../pylenium-commands/commands.md) like `.visit()` and `.get()`\n\n{% hint style=\"warning\" %}\n Your IDE may not do this or you may be missing an Extension or Plugin.\n\nPyCharm has [pytest support](setup-pytest.md) out-of-the-box.\n{% endhint %}\n\nLet's move on with the steps.\n\n* Visit [https://qap.dev](https://qap.dev)\n\n{% code title=\"test\\_qap\\_dev.py\" %}\n```bash\ndef test_carlos_is_on_leadership(py):\n    py.visit('https://qap.dev')\n```\n{% endcode %}\n\n* Hover the **About** link to reveal a menu\n\n{% code title=\"test\\_qap\\_dev.py\" %}\n```python\ndef test_carlos_is_on_leadership(py):\n    py.visit('https://qap.dev')\n    py.get('a[href=\"/about\"]').hover()\n```\n{% endcode %}\n\n{% hint style=\"info\" %}\nWhen you get an Element from locator methods:\n\n* `.get()  | .getx()`\n* `.find() | .findx()`\n* `.contains()`\n\nyou can perform actions against the element like:\n\n* `.click()`\n* `.type()`\n* `.hover()`\n* `and more!`\n{% endhint %}\n\n{% hint style=\"success\" %}\nMake sure to check out the many commands available in Pylenium\n{% endhint %}\n\n* Click the **Leadership** link in the menu\n\n{% code title=\"test\\_qap\\_dev.py\" %}\n```python\ndef test_carlos_is_on_leadership(py):\n    py.visit('https://qap.dev')\n    py.get('a[href=\"/about\"]').hover()\n    py.get('a[href=\"/leadership\"][class^=\"Header-nav\"]').click()\n```\n{% endcode %}\n\n* Assert **Carlos Kidman** is on the Leadership page\n\n{% code title=\"test\\_qap\\_dev.py\" %}\n```python\ndef test_carlos_is_on_leadership(py):\n    py.visit('https://qap.dev')\n    py.get('a[href=\"/about\"]').hover()\n    py.get('a[href=\"/leadership\"][class^=\"Header-nav\"]').click()\n    assert py.contains('Carlos Kidman')\n```\n{% endcode %}\n\n## Run the Test\n\nIf you're using PyCharm, there should be a green **Play** button next to the test definition. Click it and select either **Run** to execute normally or **Debug** to use breakpoints in Debug Mode.\n\nOtherwise, use the method your IDE provides. You can always use the CLI as well:\n\n{% code title=\"Terminal $ \\(venv\\)\" %}\n```bash\npython -m pytest tests/test_qap_dev.py\n```\n{% endcode %}\n\n### Look at the Difference\n\nHere is the same test but written with Selenium out of the box:\n\n```bash\n# define your setup and teardown fixture\n@pytest.fixture\ndef driver():\n    driver = webdriver.Chrome()\n    yield driver\n    driver.quit()\n\n\ndef test_carlos_is_on_leadership_page_with_selenium(driver):\n    driver.get('https://qap.dev')\n    \n    # hover About link\n    about_link = driver.find_element(By.CSS_SELECTOR, \"a[href='/about']\")\n    actions = ActionChains(driver)\n    actions.move_to_element(about_link).perform()\n    \n    # click Leadership link in About menu\n    driver.find_element(By.CSS_SELECTOR, \"a[href='/leadership'][class^='Header-nav']\").click()\n    \n    # check if 'Carlos Kidman' is on the page\n    assert driver.find_element(By.XPATH, \"//*[contains(text(), 'Carlos Kidman')]\")\n```\n\n## Another Test Example\n\nLet's write another test that searches for `Pylenium` and makes sure the results page contains that term in the title.\n\n1. Navigate to Google.com\n2. Type `Pylenium` into the search field\n3. Submit the search\n4. Assert the results page contains the title\n\n```python\ndef test_google_search(py):\n    py.visit('https://google.com')\n    py.get('[name=\"q\"]').type('Pylenium')\n    py.get('[name=\"btnK\"]').submit()\n    assert py.should().contain_title('Pylenium')\n```\n\nYou've already seen different Element commands like `.visit()`, `.type()` and `.submit()`,  but there is also a _Should_ object for:\n\n* [Element](../element-commands/should.md)\n* [Elements](../element-commands/should.md)\n* [Pylenium](../pylenium-commands/should.md)\n\nIn the example above, `py.should()` uses an Explicit Wait to wait until the \"driver\" detects that the current page's title contains `\"Pylenium\"`. \n\n* If the title contains `\"Pylenium\"` within the specified timeout, then it returns `True` and passes the assertion\n* If the title does not meet the expectation within the specified timeout, then it returns `False` and fails the assertion\n\n{% hint style=\"success\" %}\nYou can leverage these _Should_ expectations to easily wait for conditions or write assertions!\n{% endhint %}\n\n"
  },
  {
    "path": "docs/guides/run-tests-in-containers.md",
    "content": "# Run Tests in Containers\n\n## Configure the Test Run\n\nRegardless of the scaling option you go with \\(Selenoid, Zalenium, Docker vs Kubernetes, etc.\\), you will need to connect your tests to a **Remote URL.**\n\nYou can do this two ways:\n\n* Update **remote\\_url** in ****`pylenium.json`\n* Pass in the argument when running the tests in the CLI\n\n### Run Tests in CLI\n\n{% hint style=\"info\" %}\nThis is the most common option since it is what you will use in your pipelines and CI\n{% endhint %}\n\n{% code title=\"Terminal $ \\(venv\\) \\# example\" %}\n```bash\npython -m pytest tests/ui -n 2 --remote_url=\"http://localhost:4444/wd/hub\"\n```\n{% endcode %}\n\n### Update pylenium.json\n\n{% hint style=\"info\" %}\nThis option is great for local development and debugging\n{% endhint %}\n\n{% code title=\"pylenium.json\" %}\n```bash\n\"remote_url\": \"http://localhost:4444/wd/hub\"\n```\n{% endcode %}\n\n### Config Layers\n\n* Layer 1 - `pylenium.json` is deserialized into **PyleniumConfig**\n* Layer 2 - If there are any CLI args, they will override their respective values in **PyleniumConfig**\n\n## Docker Example\n\nWith **Docker** installed, you can easily spin up a **Selenium Grid** with the `docker-compose` command.\n\n### docker-compose.yml\n\nYou will need a `docker-compose.yml` file and then open a Terminal in the same directory as this file.\n\n{% code title=\"docker-compose.yml\" %}\n```yaml\nversion: \"3\"\nservices:\n\n  selenium-hub:\n    image: selenium/hub\n    ports:\n      - \"4444:4444\"\n    environment:\n        GRID_MAX_SESSION: 16\n        GRID_BROWSER_TIMEOUT: 300\n        GRID_TIMEOUT: 300\n\n  chrome:\n    image: selenium/node-chrome\n    depends_on:\n      - selenium-hub\n    environment:\n      HUB_PORT_4444_TCP_ADDR: selenium-hub\n      HUB_PORT_4444_TCP_PORT: 4444\n      NODE_MAX_SESSION: 2\n      NODE_MAX_INSTANCES: 2\n\n  firefox:\n    image: selenium/node-firefox\n    depends_on:\n      - selenium-hub\n    environment:\n      HUB_PORT_4444_TCP_ADDR: selenium-hub\n      HUB_PORT_4444_TCP_PORT: 4444\n      NODE_MAX_SESSION: 4\n      NODE_MAX_INSTANCES: 4\n```\n{% endcode %}\n\nThis configuration will spin up a **Hub** node \\(load balancer\\), a **Chrome** node with 2 available drivers and a **Firefox** node with 4 available drivers.\n\n### Spin up the Grid\n\nWith a single command you will have all of this created for you:\n\n{% code title=\"Terminal $\" %}\n```bash\ndocker-compose up -d\n```\n{% endcode %}\n\n{% hint style=\"info\" %}\nOnce complete, you can visually see these Grid by going to [http://localhost:4444/grid/console](http://localhost:4444/grid/console)\n{% endhint %}\n\nNow **Configure the Test Run** \\(steps at top of this doc\\) to target the Hub which will balance the tests across its Nodes.\n\n### Scale Nodes\n\nWith the YAML file example above, it will create 1 chrome Node with 2 available drivers by default. You can easily scale this to the number you need.\n\n{% code title=\"Terminal $\" %}\n```bash\ndocker-compose up -d --scale chrome=5\n```\n{% endcode %}\n\nThis will spin up the Grid with 5 chrome Nodes!\n\n### Tear Down the Grid\n\nWhen you're done using the Grid, a single command will tear it completely down.\n\n{% code title=\"Terminal $\" %}\n```bash\ndocker-compose down\n```\n{% endcode %}\n\n"
  },
  {
    "path": "docs/guides/run-tests-in-parallel.md",
    "content": "# Run Tests in Parallel\n\n## Simple CLI\n\nPylenium comes with **pytest** and the **pyest-xdist** plugin to run tests concurrently. All you need to do is use the `-n [NUMBER]` option when running the tests in the CLI.\n\n{% code title=\"Terminal $ \\(venv\\) \\# run 2 tests concurrently\" %}\n```bash\npython -m pytest tests -n 2\n```\n{% endcode %}\n\n{% hint style=\"success\" %}\n Pylenium is already designed to scale in parallel with or without containers\n{% endhint %}\n\n## Configure the IDE\n\nMost IDEs will allow you to configure your Test File or Test Run with additional arguments.\n\nFor example, in PyCharm, you can:\n\n* Open **Run** in the Top Menu\n* Select **Edit Configurations**\n* Then add `-n 2` to the **Additional Arguments** field\n\n{% hint style=\"info\" %}\nThat allows you to Run and Debug tests while still having 2 run at a time\n{% endhint %}\n\n\n\n"
  },
  {
    "path": "docs/misc/install-chromedriver.md",
    "content": "---\ndescription: This is NO LONGER NEEDED in version 1.4.1+\n---\n\n# Install chromedriver\n\n{% hint style=\"danger\" %}\nPylenium installs these for you automatically! YOU DO NOT NEED TO DO THIS!\n{% endhint %}\n\n## Options\n\nThere are many ways to install the driver executables needed to work with Selenium. This document will go over some of them, but use the one that works for you:\n\n* **`Chocolatey`** - Package Manager for Windows\n* **`Homebrew`** - Package Manager for Mac\n* **`webdriver-manager`** - A great tool to manage drivers for any OS\n* **`Manual Installation`** - worst-case scenario, you can always install them manually\n\n{% hint style=\"info\" %}\nIn a future release, we will take care of this step for you :\\)\n{% endhint %}\n\n## The driver must be on your PATH\n\nPylenium will only look for drivers that exist on your PATH. There are multiple ways to do this, so this doc will break down the most common.\n\n{% hint style=\"success\" %}\nYou can skip this step if you already have the drivers on your PATH\n{% endhint %}\n\n## Chocolatey\n\nIf you are on Windows, you can use the **Chocolatey** package manager by going to their installation page:\n\n{% embed url=\"https://chocolatey.org/docs/installation\" %}\n\n{% hint style=\"warning\" %}\nFollow the instructions for your Terminal of choice: **Command Prompt** or **Powershell**\n{% endhint %}\n\n### Install chromedriver\n\n1. Go to the installation page and copy the Command for your Terminal\n2. Open your Terminal in **Administrative Mode**\n3. Paste the command and execute it\n4. Close the Terminal and re-open it\n5. Install chromedriver\n\n{% code title=\"Terminal\" %}\n```bash\nchoco --version\n# 0.10.15\n\nchoco install chromedriver\n# accept any prompts and choco will print where it was installed\n\nchromedriver --version\n# ChromeDriver 80.0.3987.106\n```\n{% endcode %}\n\n### Add to PATH\n\nCopy the file path where **choco** installed the chromedriver and set it in your PATH. The following link shows how you can manually add a directory to your PATH.\n\n{% embed url=\"https://www.howtogeek.com/118594/how-to-edit-your-system-path-for-easy-command-line-access/\" %}\n\n{% hint style=\"warning\" %}\nAfter editing your PATH, restart your IDE and Terminals for the new PATH to register\n{% endhint %}\n\n## Homebrew\n\nIf you are on MacOS, you can the use the **Homebrew** package manager by going to their installation page:\n\n{% embed url=\"https://brew.sh/\" %}\n\n### Install chromedriver\n\n1. Run the script and follow the prompts\n2. Install chromedriver\n\n{% code title=\"Terminal\" %}\n```bash\nbrew cask install chromedriver\n```\n{% endcode %}\n\n### Add to PATH\n\n{% hint style=\"success\" %}\nbrew automatically install it to your `/usr/local/bin` directory which is _already_ on your PATH\n{% endhint %}\n\n## webdriver-manager\n\nThis is a **Node** module that installs and manages different drivers. You will need [Node.js](https://nodejs.org/en/download/) installed to use this, but it works for both **Windows** and **MacOS**, making it a crowd favorite.\n\n{% embed url=\"https://www.npmjs.com/package/webdriver-manager\" %}\n\n### Install chromedriver\n\n```bash\nwebdriver-manager update --output_dir=\"file-path\"\n```\n\n### Add to PATH\n\nThis doesn't automatically add it to your path, so take note where the **chromedriver** was saved so you can manually add it.\n\n## Manual Installation\n\nWorst-case scenario, you can always install the drivers manually.\n\n### Install chromedriver\n\n1. Click on the link below to go to Chrome's downloads\n2. Click on the version that you want\n3. Download the chromedriver\n\n{% embed url=\"https://sites.google.com/a/chromium.org/chromedriver/downloads\" %}\n\n### Add to PATH\n\nYou can either copy and paste the chromedriver to a directory that is **already** on the PATH, or you can add a folder to the PATH as well. Here is a helpful doc:\n\n{% embed url=\"https://zwbetz.com/download-chromedriver-binary-and-add-to-your-path-for-automated-functional-testing/\" %}\n\n{% hint style=\"warning\" %}\nAfter editing your PATH, restart your IDE and Terminals for the new PATH to register\n{% endhint %}\n\n"
  },
  {
    "path": "docs/pylenium-commands/commands.md",
    "content": "---\ndescription: Pylenium offers many commands and features out of the box.\n---\n\n# Commands\n\n## py\n\nThis is the main object in Pylenium. This is basically the **bot** you're controlling in your tests.\n\n{% code title=\"example\" %}\n```python\npy.visit('https://qap.dev')\n```\n{% endcode %}\n\n{% hint style=\"info\" %}\n This is used to interact with the browser and find elements\n{% endhint %}\n\n## element\\(s\\)\n\nThese commands allow you to interact and perform actions against an Element or Elements.\n\n{% code title=\"You can chain commands\" %}\n```python\npy.get('ul').find('li').first().click()\n```\n{% endcode %}\n\n{% code title=\"or you can store them in variables\" %}\n```bash\n# click the first element with id=button\nelement = py.get('#button')\nelement.click()\n```\n{% endcode %}\n\n{% code title=\"Mix and match variables and chains\" %}\n```python\n# print the href value of all links on the page\nelements = py.find('a')\nfor el in elements:\n    print(el.get_attribute('href'))\n```\n{% endcode %}\n\n{% code title=\"Use what is best for you :\\)\" %}\n```python\n# check all checkboxes\npy.find('input.checkbox').check()\n```\n{% endcode %}\n\n"
  },
  {
    "path": "docs/pylenium-commands/contains.md",
    "content": "---\ndescription: The command to get the DOM element containing the given text.\n---\n\n# contains\n\n## Syntax\n\n```python\npy.contains(text)\npy.contains(text, timeout)\n\n---or---\n\nElement.contains(text)\nElement.contains(timeout)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\n# Yield Element in .nav containing 'About'\npy.get('.nav').contains('About')\n\n---or---\n\n# Yield first Element in document containing 'Hello'\npy.contains('Hello')\n\n---or--- # store in variable\n\nelement = py.contains('About')\n\n---or--- # chain an Element command\n\npy.contains('About').click()\n\n---or--- # control the timeout in any of the above usages\n\npy.contains('Deck Builder', timeout=5).click()\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'title' does not yield Element\npy.title.contains('QAP')\n\n---or---\n\n# Errors, 'get_cookies' does not yield Element\npy.get_cookies().contains('Cooke Monster')\n```\n{% endcode %}\n\n## Arguments\n\n* `text (str)` - The text to look for\n* `timeout=0 (int)` - The amount of seconds for this command to succeed.\n  * `None` will use the default **wait\\_time** in `pylenium.json`\n  * Zero \\(`0`\\) will poll the DOM immediately with no wait\n  * Greater than zero will override the default **wait\\_time**\n\n{% hint style=\"info\" %}\nIt does not need to be an _exact_ match\n{% endhint %}\n\n## Yields\n\n* **\\(Element\\)** The first element that is found, even if multiple elements match the query\n\n"
  },
  {
    "path": "docs/pylenium-commands/delete_all_cookies.md",
    "content": "---\ndescription: The command to delete all cookies in the current browser session.\n---\n\n# delete\\_all\\_cookies\n\n## Syntax\n\n```python\npy.delete_all_cookies()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.delete_all_cookies()\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'delete_all_cookies' yields None\npy.delete_all_cookies().get()\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* None\n\n"
  },
  {
    "path": "docs/pylenium-commands/delete_cookie.md",
    "content": "---\ndescription: The command to delete a cookie with the given name.\n---\n\n# delete\\_cookie\n\n## Syntax\n\n```python\npy.delete_cookie(name)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.delete_cookie('foo')\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'delete_cookie' yields None\npy.delete_cookie('foo').get()\n```\n{% endcode %}\n\n## Arguments\n\n* `name (str)` - The name of the cookie\n\n## Yields\n\n* None\n\n"
  },
  {
    "path": "docs/pylenium-commands/execute_script.md",
    "content": "---\ndescription: The command to execute javascript into the browser.\n---\n\n# execute\\_script\n\n## Syntax\n\n```python\npy.execute_script(javascript)\npy.execute_script(javascript, *args)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\n# Yields the value of document.title\npy.execute_script('return document.title;')\n\n---or---\n\n# Yields the .innerText of the element with the id of 'foo'\npy.execute_script('return document.getElementById(arguments[0]).innerText', 'foo')\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'execute_script' yields a WebElement, not a Pylenium Element\npy.execute_script('return document.getElementById(arguments[0])').get()\n```\n{% endcode %}\n\n## Arguments\n\n* `javascript (str)` - The javascript to execute\n* `*args (Any)` - A comma-separated list of arguments to pass into the javascript string\n\n{% hint style=\"info\" %}\nYou can access the **\\*args** in the javascript by using `arguments[0]`, `arguments[1]`, etc.\n{% endhint %}\n\n## Yields\n\n* **\\(Any\\)** This will return whatever is in the `return statement` of your javascript.\n\n{% hint style=\"info\" %}\nIf you do not include a **return**, then `.execute_script()` will return **None**\n{% endhint %}\n\n## Examples\n\n```python\n# You can pass in complex objects\nul_element = py.get('ul')\npy.execute_script('return arguments[0].children;', ul_element.webelement)\n# We use the .webelement property to send Selenium's WebElement\n# that is understood by the browser\n```\n\n```python\n# You can create complex javascript strings\nget_siblings_script = '''\n    elem = document.getElementById(arguments[0]);\n    var siblings = [];\n    var sibling = elem.parentNode.firstChild;\n\n    while (sibling) {\n        if (sibling.nodeType === 1 && sibling !== elem) {\n            siblings.push(sibling);\n        }\n        sibling = sibling.nextSibling\n    }\n    return siblings;\n    '''\nsiblings = self.py.execute_script(get_siblings_script, 'foo')\n```\n\n"
  },
  {
    "path": "docs/pylenium-commands/fake.md",
    "content": "---\ndescription: A basic instance of Faker to generate test data.\n---\n\n# fake\n\n## Syntax\n\n```text\npy.fake\n```\n\n{% hint style=\"success\" %}\nThis is a **command** and a **fixture**. More details in his doc: [**Fixtures &gt; fake**](../fixtures/fake.md)\\*\\*\\*\\*\n{% endhint %}\n\n\n\n"
  },
  {
    "path": "docs/pylenium-commands/find.md",
    "content": "---\ndescription: The command to get DOM elements that match the CSS selector.\n---\n\n# find\n\n## Syntax\n\n```python\npy.find(css)\npy.find(css, timeout)\n\n---or---\n\nElement.find(css)\nElement.find(css, timeout)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\n# Yield Elements in .nav with tag name of a\npy.get('.nav').find('a')\n\n---or---\n\n# Yield all Elements in document with id of 'button'\npy.find('#button')\n\n---or--- # store in variable\n\nelements = py.find('li')\n\n---or--- # chain an Elements command\n\nelement = py.find('ul > li').first()\n\n---or--- # control the timeout in any of the above usages\n\npy.find('li', timeout=5).last()\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'title' does not yield Element\npy.title.find('QAP')\n\n---or---\n\n# Errors, 'get_cookie' does not yield Element\npy.get_cookie().find('Cooke Monster')\n```\n{% endcode %}\n\n## Arguments\n\n* `css (str)` - The CSS selector to use\n* `timeout=0 (int)` - The amount of seconds for this command to succeed.\n  * `None` will use the default **wait\\_time** in `pylenium.json`\n  * Zero \\(`0`\\) will poll the DOM immediately with no wait\n  * Greater than zero will override the default **wait\\_time**\n\n## Yields\n\n* **\\(Elements\\)** A list of elements that match the query.\n\n## Examples\n\n```python\n# if you expect the elements not to be present\nassert py.find('ul > li').should().be_empty()\n\n# otherwise, just use the default\nelements = py.find('ul > li')\n```\n\n"
  },
  {
    "path": "docs/pylenium-commands/find_xpath.md",
    "content": "---\ndescription: The command to find all the elements that match the XPath selector.\n---\n\n# findx\n\n## Syntax\n\n```python\npy.findx(xpath)\npy.findx(xpath, timeout)\n\n---or---\n\nElement.findx(xpath)\nElement.findx(xpath, timeout)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\n# Yield all Elements in .nav with tag name of a\npy.get('.nav').findx('//a')\n\n---or---\n\n# Yield all Elements in document with id of 'button'\npy.findx('//*[@id=\"button\"]')\n\n---or--- # store in variable\n\nelements = py.findx('//*[@id=\"button\"]')\n\n---or--- # chain an Element(s) command\n\n# if one element is found, still returns a list of 1: [Element]\npy.findx('//*[@id=\"button\"]').first().click()\n\n---or--- # control the timeout in any of the above usages\n\npy.findx('//a[@href=\"/about\"]', timeout=5).length()\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'title' does not yield Element\npy.title.findx('//a')\n\n---or---\n\n# Errors, 'get_cookie' does not yield Element\npy.get_cookie().findx('//[text()=\"foo\" and @class=\"bar\"]')\n```\n{% endcode %}\n\n## Arguments\n\n* `xpath (str)` - The XPATH selector to use\n* `timeout=None (int)` - The amount of seconds for this command to succeed.\n  * `None` will use the default **wait\\_time** in `pylenium.json`\n  * Zero \\(`0`\\) will poll the DOM immediately with no wait\n  * Greater than zero will override the default **wait\\_time**\n\n## Yields\n\n* **\\(Elements\\)** The list of elements found.\n  * If none are found, returns an empty list\n  * If one or more are found, return the list normally\n\n## Examples\n\n```python\n# there should be 3 `a` elements\npy.findx('//a').should().have_length(3)\n```\n\n"
  },
  {
    "path": "docs/pylenium-commands/get.md",
    "content": "---\ndescription: The command to get the DOM element that matches the CSS selector.\n---\n\n# get\n\n## Syntax\n\n```python\npy.get(css)\npy.get(css, timeout)\n\n---or---\n\nElement.get(css)\nElement.get(css, timeout)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\n# Yield Element in .nav with tag name of a\npy.get('.nav').get('a')\n\n---or---\n\n# Yield first Element in document with id of 'button'\npy.get('#button')\n\n\n---or--- # store in variable\n\nelement = py.get('#login')\n\n---or--- # chain an Element command\n\npy.get('#save-button').click()\n\n---or--- # control the timeout in any of the above usages\n\npy.get('a[href=\"/about\"]', timeout=5).click()\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'title' does not yield Element\npy.title.get('QAP')\n\n---or---\n\n# Errors, 'get_cookie' does not yield Element\npy.get_cookie().get('Cooke Monster')\n```\n{% endcode %}\n\n## Arguments\n\n* `css (str)` - The CSS selector to use\n* `timeout=0 (int)` - The amount of seconds for this command to succeed.\n  * `None` will use the default **wait\\_time** in `pylenium.json`\n  * Zero \\(`0`\\) will poll the DOM immediately with no wait\n  * Greater than zero will override the default **wait\\_time**\n\n## Yields\n\n* **\\(Element\\)** The first element that is found, even if multiple elements match the query.\n\n"
  },
  {
    "path": "docs/pylenium-commands/get_cookie.md",
    "content": "---\ndescription: The command to get the cookie with the given name.\n---\n\n# get\\_cookie\n\n## Syntax\n\n```python\npy.get_cookie(name)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get_cookie('foo')\n\n---or--- # \"key\" into the dictionary\n\nval = py.get_cookie('foo')['value']\n\n---or--- # use the .get() function in dict\n\nval = py.get_cookie('foo').get('value')\n```\n{% endcode %}\n\n## Arguments\n\n* `name (str)` - The name of the cookie\n\n## Yields\n\n* **\\(dict\\)** The cookie as a dictionary. Cookie objects have the following properties:\n  * `name`\n  * `value`\n  * `path`\n  * `domain`\n  * `httpOnly`\n  * `secure`\n  * `expiry`\n\n{% hint style=\"info\" %}\nReturns **None** if the cookie does not exist\n{% endhint %}\n\n"
  },
  {
    "path": "docs/pylenium-commands/get_cookies.md",
    "content": "---\ndescription: The command to get all cookies in the current browser session.\n---\n\n# get\\_cookies\n\n## Syntax\n\n```python\npy.get_cookies()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.get_cookies()\n\n---or--- # store in variable\n\ncookies = py.get_cookies()\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **List\\[dict\\]** A list of cookie objects. Each cookie object has the following properties:\n  * `name`\n  * `value`\n  * `path`\n  * `domain`\n  * `httpOnly`\n  * `secure`\n  * `expiry`\n\n## Examples\n\n```python\npy.set_cookie({'name': 'foo', 'value': 'bar'})\n\ncookie = py.get_cookies()[0]\n\nprint(cookie['name'])      # 'foo'\nprint(cookie.get('value')) # 'bar'\n```\n\n```python\npy.set_cookie({'name': 'foo', 'value': 'bar'})\npy.set_cookie({'name': 'yes', 'value', 'please'})\n\nfor cookie in py.get_cookies():\n    print(cookie['name'])\n    print(cookie.get('value'))\n```\n\n"
  },
  {
    "path": "docs/pylenium-commands/get_xpath.md",
    "content": "---\ndescription: The command to get a single element using an XPath selector.\n---\n\n# getx\n\n## Syntax\n\n```python\npy.getx(xpath)\npy.getx(xpath, timeout)\n\n---or---\n\nElement.getx(xpath)\nElement.getx(xpath, timeout)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\n# Yield the first Element in .nav with tag name of a\npy.get('.nav').getx('//a')\n\n---or---\n\n# Yield the first Element in document with id of 'button'\npy.getx('//*[@id=\"button\"]')\n\n---or--- # store in variable\n\nelement = py.getx('//*[@id=\"button\"]')\n\n---or--- # chain an Element(s) command\n\n# chain an action\npy.getx('//*[@id=\"button\"]').click()\n\n---or--- # control the timeout in any of the above usages\n\npy.getx('//a[@href=\"/about\"]', timeout=5).click()\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'title' does not yield Element\npy.title.getx('//a')\n\n---or---\n\n# Errors, 'get_cookie' does not yield Element\npy.get_cookie().getx('//[text()=\"foo\" and @class=\"bar\"]')\n```\n{% endcode %}\n\n## Arguments\n\n* `xpath (str)` - The XPATH selector to use\n* `timeout=None (int)` - The amount of seconds for this command to succeed.\n  * `None` will use the default **wait\\_time** in `pylenium.json`\n  * Zero \\(`0`\\) will poll the DOM immediately with no wait\n  * Greater than zero will override the default **wait\\_time**\n\n## Yields\n\n* **\\(Element\\)** The first element found, even if multiple elements match the query.\n\n## Examples\n\n```python\n# the button should be displayed\npy.getx('//*[@id=\"button\"]').should().be_visible()\n```\n\n"
  },
  {
    "path": "docs/pylenium-commands/go.md",
    "content": "---\ndescription: Navigate forward or back in the browser's history.\n---\n\n# go\n\n## Syntax\n\n```python\npy.go(direction)\npy.go(direction, number)\n```\n\n## Usage\n\n* Go forward one page\n\n```python\npy.go('forward')\n```\n\n* Go back two pages\n\n```python\npy.go('back', 2)\n```\n\n## Arguments\n\n* `direction (str)` - forward or back\n* `number=1 (int)` - go back or forward N pages in history\n\n{% hint style=\"warning\" %}\n**`number`** must be a positive integer.\n{% endhint %}\n\n## Yields\n\n* **\\(Pylenium\\)** The current instance of Pylenium so you can chain commands.\n\n"
  },
  {
    "path": "docs/pylenium-commands/maximize_window.md",
    "content": "---\ndescription: The command the maximize the current window.\n---\n\n# maximize\\_window\n\n## Syntax\n\n```python\npy.maximize_window()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\n# by default, Pylenium will maximaze the window for you, but just in case...\npy.maximize_window().visit('https://qap.dev')\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(Pylenium\\)** The current instance of Pylenium so you can chain commands\n\n"
  },
  {
    "path": "docs/pylenium-commands/quit.md",
    "content": "---\ndescription: The command to quit the driver and close all associated windows.\n---\n\n# quit\n\n## Syntax\n\n```python\npy.quit()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.quit()\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'quit' terminates the current browser session\npy.quit().get()\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* None\n\n"
  },
  {
    "path": "docs/pylenium-commands/request.md",
    "content": "---\ndescription: >-\n  Requests is an elegant and simple HTTP library for Python, built for human\n  beings.\n---\n\n# request\n\n## Syntax\n\n```text\npy.request\n```\n\n{% hint style=\"success\" %}\nThis is a **command** and a **fixture**. More details in his doc: [**Fixtures &gt; api**](../fixtures/requests.md)\\*\\*\\*\\*\n{% endhint %}\n\n"
  },
  {
    "path": "docs/pylenium-commands/screenshot.md",
    "content": "---\ndescription: The command to take a screenshot of the current window.\n---\n\n# screenshot\n\n## Syntax\n\n```python\npy.screenshot(filename)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\n# saves the screenshot to the current working directory\npy.screenshot('ss.png')\n\n---or---\n\n# saves the screenshot using the filepath\npy.screenshot('../images/ss.png')\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, include the file extension like '.png'\npy.screenshot('ss')\n\n---or---\n\n# Errors, .screenshot() yields None\npy.screenshot('ss.png').get('a')\n```\n{% endcode %}\n\n## Arguments\n\n* `filename (str)` - The filename including the **path** to the directory you want to save it in\n\n{% hint style=\"info\" %}\nMake sure to include the file extension like **.png**\n{% endhint %}\n\n## Yields\n\n* None\n\n"
  },
  {
    "path": "docs/pylenium-commands/scroll_to.md",
    "content": "---\ndescription: The command to scroll to the given location.\n---\n\n# scroll\\_to\n\n## Syntax\n\n```text\npy.scroll_to(x, y)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\n# scroll down 500px\npy.scroll_to(0, 500)\n```\n{% endcode %}\n\n## Arguments\n\n* **`x (int)`**: The number of pixels to scroll horizontally\n* **`y (int)`**: The number of pixels to scroll vertically\n\n## Yields\n\n* **\\(Pylenium\\)** so you can chain another command\n\n"
  },
  {
    "path": "docs/pylenium-commands/set_cookie.md",
    "content": "---\ndescription: The command to set a cookie into the current browser session.\n---\n\n# set\\_cookie\n\n## Syntax\n\n```python\npy.set_cookie(cookie)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.set_cookie({'name' : 'foo', 'value' : 'bar'})\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'set_cookie' accepts a single argument that is a dict\npy.set_cookie('foo', 'bar')\n\n---or---\n\n# Errors, 'set_cookie' yields None\npy.set_cookie({'name' : 'foo', 'value' : 'bar'}).get('foo')\n```\n{% endcode %}\n\n## Arguments\n\n* `cookie (dict)` - A dictionary with required keys: `\"name\"` and `\"value\"`\n\n{% hint style=\"info\" %}\nOptional keys: `\"path\"`, `\"domain\"`, `\"secure\"`, `\"expiry\"`\n{% endhint %}\n\n## Yields\n\n* None\n\n"
  },
  {
    "path": "docs/pylenium-commands/should.md",
    "content": "---\ndescription: A collection of expectations for the current driver.\n---\n\n# should\n\n## Expectations\n\n* `.contain_title()`\n* `.contain_url()`\n* `.have_title()`\n* `.have_url()`\n* `.not_find()`\n* `.not_find_xpath()`\n* `.not_contain()`\n\n## Syntax\n\n```python\n# use the default wait_time\npy.should().<expectation>\n\n---or---\n\n# customize the wait_time for this expectation\npy.should(timeout).<expectation>\n\n---or---\n\n# ignore exceptions that you expect to \"get in the way\"\npy.should(ignored_exceptions).<expectation>\n\n---or---\n\n# customize both fully\npy.should(timeout, ignored_exceptions).<expectation>\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.visit('https://qap.dev').should().have_title('QA at the Point')\n```\n{% endcode %}\n\n## Arguments\n\n* `.contain_title(string)` - The substring for the title to contain\n* `.contain_url(string)` - The substring for the URL to contain\n* `.have_title(title)` - The case-sensitive title to match\n* `.have_url(url)` - The case-sensitive url to match\n* `.not_find(css)` - The CSS selector\n* `.not_find_xpath(xpath)` - The XPATH selector\n* `.not_contain(text)` - The text to contain\n\n## Yields\n\n* **\\(Pylenium\\)** If the assertion passes, then the current instance of Pylenium is returned, else an **AssertionError** is raised if the condition is not met within the specified timeout.\n* **\\(Bool\\)** for the Find Element expectations.\n\n"
  },
  {
    "path": "docs/pylenium-commands/switch_to.default_content.md",
    "content": "---\ndescription: >-\n  The command to switch the driver's context to the default (or starting)\n  content.\n---\n\n# switch\\_to.default\\_content\n\n## Syntax\n\n```python\npy.switch_to.default_content()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.switch_to.default_content()\n\n---or--- # chain a Pylenium command in the new context\n\npy.switch_to.default_content().get('.link')\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(Pylenium\\)** The current instance of Pylenium so you can chain commands\n\n{% hint style=\"info\" %}\nIf the driver is already in the default context, nothing changes\n{% endhint %}\n\n"
  },
  {
    "path": "docs/pylenium-commands/switch_to.frame.md",
    "content": "---\ndescription: The command to switch the driver's context to the frame given its name or id.\n---\n\n# switch\\_to.frame\n\n## Syntax\n\n```python\npy.switch_to.frame(name_or_id)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\n# switch to an iframe with name of 'main-content'\npy.switch_to.frame('main-content')\n\n---or--- # chain a Pylenium command\n\npy.switch_to.frame('main-content').contains('Add New').click()\n```\n{% endcode %}\n\n## Arguments\n\n* `name_or_id (str)` - The **name** or **id** attribute value of the `<frame>` element\n\n## Yields\n\n* **\\(Pylenium\\)** The current instance of Pylenium so you can chain commands\n\n## Examples\n\n```python\n<div>\n    <frame id='foo'>\n        <a href='/different-page' id='bar'>Link in iframe</a>\n    </frame>\n</div>\n```\n\nIf we wanted to click the link above, we would need to:\n\n1. switch the driver's context to the iframe\n2. then perform the click\n\nThis is a piece of cake with Pylenium:\n\n```python\npy.switch_to.frame('foo').get('#bar').click()\n```\n\n"
  },
  {
    "path": "docs/pylenium-commands/switch_to.parent_frame.md",
    "content": "---\ndescription: >-\n  The command to switch the driver's context to the parent frame of the current\n  frame.\n---\n\n# switch\\_to.parent\\_frame\n\n## Syntax\n\n```python\npy.switch_to.parent_frame()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\n# switch to a frame with name of 'iframe'\npy.switch_to.frame('iframe')\n\n# switch back to the main website\npy.switch_to.parent_frame()\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(Pylenium\\)** The current instance of Pylenium so you can chain commands\n\n## Examples\n\n```python\n<div>\n    <frame id='foo'>\n        <button>Button in iframe</button>\n    </frame>\n    <button id='bar'>Button in main html (aka default content)</button>\n</div>\n```\n\n```python\n# switch to the iframe to click the 'Button in iframe'\npy.switch_to.frame('foo').contains('Button in iframe').click()\n\n# switch back to the main html to click the 'bar' button\npy.switch_to.parent_frame().get('#bar').click()\n```\n\n"
  },
  {
    "path": "docs/pylenium-commands/switch_to.window.md",
    "content": "---\ndescription: >-\n  The command to switch the driver's context to the specified Window or Browser\n  Tab.\n---\n\n# switch\\_to.window\n\n## Syntax\n\n```python\npy.switch_to.window(name_or_handle)\npy.switch_to.window(index)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\n# switch to a Window by handle\nwindows = py.window_handles\npy.switch_to.window(name_or_handle=windows[1])\n```\n{% endcode %}\n\n{% code title=\"correct usage\" %}\n```python\n# switch to a newly opened Browser Tab by index\npy.switch_to.window(index=1)\n```\n{% endcode %}\n\n## Arguments\n\n* `name_or_handle='' (str)`- The **name** or **window handle** of the Window to switch to\n* `index=0 (int)` - The index position of the Window Handle\n\n{% hint style=\"info\" %}\n**index=0** will switch to the default content\n{% endhint %}\n\n## Yields\n\n* **\\(Pylenium\\)** The current instance of Pylenium so you can chain commands\n\n"
  },
  {
    "path": "docs/pylenium-commands/title.md",
    "content": "---\ndescription: The command to get the current page's title.\n---\n\n# title\n\n## Syntax\n\n```python\npy.title()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.title()\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\npy.title\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(str\\)** The `document.title` property of the current page\n\n## Examples\n\n```python\nassert py.title() == 'QA at the Point'\n```\n\n\n\n"
  },
  {
    "path": "docs/pylenium-commands/url.md",
    "content": "---\ndescription: The command to get the current page's URL.\n---\n\n# url\n\n## Syntax\n\n```text\npy.url()\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```text\npy.url()\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```text\npy.url\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(str\\)** The current page's URL\n\n## Examples\n\n```python\nassert py.url().endswith('/checkout')\n```\n\n"
  },
  {
    "path": "docs/pylenium-commands/viewport.md",
    "content": "---\ndescription: The command to control the size and orientation of the current browser window.\n---\n\n# viewport\n\n## Syntax\n\n```python\npy.viewport(width, height)\npy.viewport(width, height, orientation)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.viewport(1280, 800) # macbook-13 size\n```\n{% endcode %}\n\n## Arguments\n\n* `width` - The width in pixels\n* `height` - The height in pixels\n* `orientation='portrait' (str)` - Pass `'landscape'` to reverse the width and height\n\n## Yields\n\n* **\\(Pylenium\\)** The current instance of Pylenium so you can change commands\n\n## Examples\n\n```python\npy.viewport(1280, 800) # macbook-13 size\npy.viewport(1440, 900) # macbook-15 size\npy.viewport(375, 667, orientation='landscape')  # iPhone X size\n```\n\n"
  },
  {
    "path": "docs/pylenium-commands/visit.md",
    "content": "---\ndescription: The command to navigate to URLs.\n---\n\n# visit\n\n## Syntax\n\n```python\npy.visit(url)\n```\n\n## Usage\n\n```bash\npy.visit('https://qap.dev')\n```\n\n## Arguments\n\n* `url (str)` - the URL to visit\n\n{% hint style=\"info\" %}\nMake sure to include the protocol **http** or **https**\n{% endhint %}\n\n## Yields\n\n* **\\(Pylenium\\)** The current instance of **Pylenium** so you can _chain_ another command\n\n## Examples\n\n```bash\n# navigate to a URL\npy.visit('https://qap.dev')\n```\n\n```bash\n# navigate to a URL and click on About link\npy.visit('https://qap.dev').contains('About').click()\n```\n\n"
  },
  {
    "path": "docs/pylenium-commands/wait.md",
    "content": "---\ndescription: The command to execute a method or function as a condition to wait for.\n---\n\n# wait\n\nThere are two types of Wait objects:\n\n- **WebDriverWait \\(default\\)**\n  - returns `WebElement` and `List[WebElement]`\n- **PyleniumWait**\n  - returns `Element` and `Elements`\n  - has a built-in `.sleep()` method\n\n`wait.until(condition)` is the most common use of Wait and allows you to wait until the condition returns a _non-False_ value.\n\nHowever, both Waits require the condition to use a WebDriver. In the example below, we can pass in a **lambda** \\(aka anonymous function\\) where `x` _is_ the **WebDriver**.\n\n```python\n# .is_displayed() returns a bool, so the return value is True\npy.wait().until(lambda x: x.find_element(By.ID, 'foo').is_displayed())\n```\n\n```python\n# the WebElement is returned once the element is found in the DOM\npy.wait().until(lambda x: x.find_element(By.ID, 'foo'))\n```\n\n```python\n# because use_py=True, this will now return Element instead\n# also, this will wait up to 5 seconds instead of the default in pylenium.json\npy.wait(5, use_py=True).until(lambda x: x.find_element(By.ID, 'foo'))\n```\n\n## Syntax\n\n```python\n# all 3 parameters are Optional with defaults\npy.wait(timeout=0, use_pylenium=False, ignored_exceptions: list = None)\n```\n\n## Usage\n\nThe usages are almost identical between the Wait objects, but you need to identify why you need to use a Wait in the first place. Pylenium does a lot of waiting for you automatically, but not for everything.\n\n{% hint style=\"info\" %}\nRemember, the biggest difference is what is returned: **`WebElement`** vs **`Element`**\n{% endhint %}\n\n{% hint style=\"success\" %}\nGood framework and test design includes waiting for the right things.\n{% endhint %}\n\n### WebDriverWait\n\nThis is the default Wait object. This will return WebElement, so you won't have Pylenium's Element commands like `.hover()` - that is what PyleniumWait is for.\n\n- Using the defaults\n\n{% code title=\"defaults\" %}\n\n```python\n# uses WebDriverWait and returns WebElement once '#save' is found\npy.wait().until(lambda x: x.find_element(By.ID, 'save')).click()\n```\n\n{% endcode %}\n\n- Using custom **`timeout`**\n\n{% code title=\"WebDriverWait with custom timeout\" %}\n\n```python\n# uses WebDriverWait but overrides the default wait_time used in pylenium.json\npy.wait(5).until(lambda x: x.find_element(By.ID, 'login-button').is_enabled())\n```\n\n{% endcode %}\n\n- Using **`ignored_exceptions`**\n\nBy default, the only exception that is ignored is the `NoSuchElementException`. You can change this by adding a list of Exceptions that you want your condition to ignore.\n\n{% code title=\"WebDriverWait with ignored\\_exceptions\" %}\n\n```python\n# ignore exceptions every time the condition is executed\n# also, this will return True because\n    # x.title == 'QA at the Point'\n# is a boolean expression\nexceptions = [NoSuchElementException, WebDriverException]\npy.wait(ignored_exceptions=exceptions).until(lambda x: x.title == 'Pylenium.io')\n```\n\n{% endcode %}\n\n- Combine arguments\n\n```python\nexceptions = [NoSuchElementException, WebDriverException]\npy.wait(7, ignored_exceptions=exceptions).until(lambda x: x.execute_script('js'))\n```\n\n### PyleniumWait\n\nIf you want to return Pylenium objects like `Element` and `Elements`, then set `use_py=True.` Otherwise, it works the same way as WebDriverWait.\n\n{% code title=\"PyleniumWait with default timeout\" %}\n\n```python\npy.wait(use_py=True).until(lambda x: x.find_element(By.ID, 'menu')).hover()\n```\n\n{% endcode %}\n\n{% code title=\"PyleniumWait with custom timeout\" %}\n\n```python\npy.wait(5, use_py=True).until(lambda x: x.find_element(By.ID, 'menu')).hover()\n```\n\n{% endcode %}\n\n- PyleniumWait also includes a **`.sleep()`** command\n\n```python\n# time.sleep() for 3 seconds\npy.wait(use_py=True).sleep(3)\n```\n\n### Expected Conditions\n\nExpected Conditions are a list of pre-built conditions that you can use in your Waits and can be used in either WebDriverWait or PyleniumWait to replace the lambda functions in the examples above.\n\n```python\nfrom selenium.webdriver.support import expected_conditions as ec\n\npy.wait().until(ec.title_is('Pylenium.io'))\n```\n\n{% hint style=\"info\" %}\n**`title_is()`** is just one of the many pre-built conditions in this module. Give it a try!\n{% endhint %}\n\n## Yields\n\n- Whatever the non-False value of the condition is\n\n## Raises\n\n- **`TimeoutException`** if the condition is not met within the timeout time\n- Depending on the condition, it would raise other Exceptions. If you know which ones are expected, you can include them in the **`ignored_exceptions`** as an argument.\n"
  },
  {
    "path": "docs/pylenium-commands/webdriver.md",
    "content": "---\ndescription: The property to get the current instance of Selenium's WebDriver.\n---\n\n# webdriver\n\n## Syntax\n\n```text\npy.webdriver\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\npy.webdriver\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\npy.webdriver()\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **\\(WebDriver\\)** The current instance of WebDriver that Pylenium is wrapping\n\n## Examples\n\nMost scenarios won't need this, but it's provided just in case. The biggest reasons to use `py.webdriver`\n\n* access functionality that may not exist in Pylenium\n* functionality that requires you pass in a WebDriver\n\n```python\n# get WebDriver's current Capabilities\ncaps = py.webdriver.capabilities\n```\n\n```python\n# function requires a WebDriver\nactions = ActionChains(py.webdriver)\n```\n\n\n\n"
  },
  {
    "path": "docs/pylenium-commands/window_handles.md",
    "content": "---\ndescription: >-\n  This property gets a list of all the window handles in the current browser\n  session.\n---\n\n# window\\_handles\n\n## Syntax\n\n```python\npy.window_handles\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\n# this property is mainly used to switch to windows or tabs\n\n# assert that there are two windows - the main website and a new tab\nwindows = py.window_handles\nassert len(windows) == 2\n\n# then switch to the new tab\npy.switch_to.window(name_or_handle=windows[1])\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **List\\[str\\]** A list of all the window handles in the current browser session.\n\n"
  },
  {
    "path": "docs/pylenium-commands/window_size.md",
    "content": "---\ndescription: This property get the size of the current window.\n---\n\n# window\\_size\n\n## Syntax\n\n```python\npy.window_size\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\nsize = py.window_size\n\n# print the width\nprint(size['width'])\n\n# print the height\nprint(size['height']\n```\n{% endcode %}\n\n## Arguments\n\n* None\n\n## Yields\n\n* **Dict\\[str, int\\]** The current window's size as a dictionary\n\n"
  },
  {
    "path": "docs/pylenium-commands/xpath.md",
    "content": "---\ndescription: The command to get DOM elements that match the XPath selector.\n---\n\n# \\(deprecated\\) xpath\n\n{% hint style=\"danger\" %}\nThis is no longer valid in version 1.8.0+\n{% endhint %}\n\n## Syntax\n\n```python\npy.xpath(xpath)\npy.xpath(xpath, at_least_one)\npy.xpath(xpath, timeout)\npy.xpath(xpath, at_least_one, timeout)\n\n---or---\n\nElement.xpath(xpath)\nElement.xpath(xpath, at_least_one)\nElement.xpath(xpath, timeout)\nElement.xpath(xpath, at_least_one, timeout)\n```\n\n## Usage\n\n{% code title=\"correct usage\" %}\n```python\n# Yield Elements in .nav with tag name of a\npy.get('.nav').xpath('//a')\n\n---or---\n\n# Yield Elements in document with id of 'button'\npy.xpath('//*[@id=\"button\"]')\n\n---or--- # store in variable\n\nelement = py.xpath('//*[@id=\"button\"]')\n\n---or--- # chain an Element(s) command\n\n# if one element is found\npy.xpath('//*[@id=\"button\"]').click()\n\n# if more elements are found\npy.xpath('//*[@id=\"button\"]').first().click()\n\n---or--- # control the timeout in any of the above usages\n\npy.xpath('//a[@href=\"/about\"]', timeout=5).click()\n```\n{% endcode %}\n\n{% code title=\"incorrect usage\" %}\n```python\n# Errors, 'title' does not yield Element\npy.title.xpath('//a')\n\n---or---\n\n# Errors, 'get_cookie' does not yield Element\npy.get_cookie().xpath('//[text()=\"foo\" and @class=\"bar\"]')\n```\n{% endcode %}\n\n## Arguments\n\n* `xpath (str)` - The XPATH selector to use\n* `at_least_one=True (bool)` - **True** if you want to wait for at least one element to be found\n* `timeout=0 (int)` - The amount of seconds for this command to succeed.\n  * This overrides the **default** **wait\\_time** in `pylenium.json()`\n\n## Yields\n\n* **\\(Elements\\)** A list of the found elements. If only one is found, return **Element** instead.\n\n## Examples\n\n```python\n# if you expect the elements not to be present\nelements = py.xpath('//a', at_least_one=False)\n\n# otherwise, just use the default\nelements = py.xpath('//a')\n```\n\n"
  },
  {
    "path": "poetry.toml",
    "content": "[virtualenvs]\nin-project = true\n"
  },
  {
    "path": "pylenium/__init__.py",
    "content": ""
  },
  {
    "path": "pylenium/a11y.py",
    "content": "from typing import Any, Dict, List, Optional\nfrom axe_selenium_python.axe import Axe\nfrom pydantic.main import BaseModel\nfrom pydantic import Field\nfrom selenium.webdriver.remote.webdriver import WebDriver\n\n\nclass AxeRelatedNode(BaseModel):\n    html: str\n    target: List[str]\n\n\nclass AxeSubNode(BaseModel):\n    data: Optional[Any]\n    id: str\n    impact: Optional[str]\n    message: str\n    related_nodes: List[AxeRelatedNode] = Field(alias=\"relatedNodes\")\n\n\nclass AxeNode(BaseModel):\n    all: List[AxeSubNode]\n    any: List[AxeSubNode]\n    html: str\n    impact: Optional[str]\n    none: List[AxeSubNode]\n    target: List[str]\n\n\nclass AxeAudit(BaseModel):\n    description: str\n    help: str\n    help_url: str = Field(alias=\"helpUrl\")\n    id: str\n    impact: Optional[str]\n    nodes: List\n    tags: List[str]\n\n\nclass AxeNodeViolation(AxeNode):\n    failure_summary: str = Field(alias=\"failureSummary\")\n\n\nclass AxeAuditViolation(AxeAudit):\n    nodes: List[AxeNodeViolation]\n\n\nclass AxeReport(BaseModel):\n    \"\"\"The aXe Audit Report in a user-friendly object.\"\"\"\n\n    inapplicable: List[AxeAudit]\n    incomplete: List[AxeAudit]\n    passes: List[AxeAudit]\n    violations: List[AxeAuditViolation]\n    timestamp: str\n    url: str\n\n\nclass PyleniumAxe:\n    \"\"\"The Pylenium abstraction of the axe-selenium-python package.\"\"\"\n\n    def __init__(self, webdriver: WebDriver):\n        self.webdriver = webdriver\n\n    def run(\n        self, name: Optional[str] = None, context: Optional[Dict] = None, options: Optional[Dict] = None\n    ) -> AxeReport:\n        \"\"\"Run the aXe audit and return an AxeReport object with the results.\n\n        For more info on the `context` and `options` parameters, visit the aXe official docs:\n        https://github.com/dequelabs/axe-core/blob/master/doc/API.md#parameters-axerun\n\n        Args:\n            name: The file path and name of the report to save as a JSON. If not provided, the file is not saved.\n            context: The dictionary of page part(s), by CSS Selectors, to include or exclude in the audit.\n            options: The dictionary of aXe options to include in the audit.\n\n        Examples:\n        ```\n            # Save the report to export or share\n            PyleniumAxe(py.webriver).run(name='a11y_report.json')\n\n            # Use the AxeReport for a hard assertion\n            report = PyleniumAxe(py.webdriver).run(name='ally_report.json')\n            violation_count = len(report.violations)\n            assert violation_count == 0, f'{violation_count} violation(s) found!'\n        ```\n\n        Raises:\n            FileNotFoundError if the given `name` is within a directory that doesn't exist.\n                * Include the file extension (e.g., axe_audit.json)\n                * If the file does not exist, it will be created\n                * However, if a directory does not exist, the file is not created and the above Error is raised\n        \"\"\"\n        axe = Axe(self.webdriver)\n        axe.inject()\n        results = axe.run(context=context, options=options)\n        if name:\n            axe.write_results(results, name)\n        return AxeReport(**results)\n"
  },
  {
    "path": "pylenium/cdp.py",
    "content": "\"\"\" Chrome DevTools Protocol (CDP) introduced in Selenium 4.\n\nResources:\n    - https://www.selenium.dev/documentation/webdriver/bidirectional/chrome_devtools/\n    - https://chromedevtools.github.io/devtools-protocol/\n\n* Currently only supports the Chrome Browser, although some chromium browsers may work as well.\n\"\"\"\n\nfrom typing import Dict\n\n\nclass CDP:\n    \"\"\"Chrome DevTools Protocol.\"\"\"\n\n    def __init__(self, webdriver):\n        self._webdriver = webdriver\n\n    def execute_command(self, cmd: str, cmd_args: Dict) -> Dict:\n        \"\"\"Execute Chrome Devtools Protocol command and get returned result.\n\n        The command and command args should follow chrome devtools protocol domains/commands, refer to link\n        https://chromedevtools.github.io/devtools-protocol/\n\n        Args:\n            cmd: The command name\n            cmd_args: The command args. Pass an empty dict {} if there is no command args\n\n        Examples:\n        ```\n            py.cdp.execute_command('Network.getResponseBody', {'requestId': requestId})\n        ```\n\n        Returns:\n            A dict of results or an empty dict {} if there is no result to return.\n            For example, to getResponseBody:\n            {'base64Encoded': False, 'body': 'response body string'}\n        \"\"\"\n        return self._webdriver.execute_cdp_cmd(cmd, cmd_args)\n\n    def get_performance_metrics(self) -> Dict:\n        \"\"\"Get performance metrics from Chrome DevTools - similar to the Performance tab in Chrome.\n\n        Examples:\n        ```\n            metrics = py.cdp.get_performance_metrics()\n            >>> metrics\n\n            {'metrics': [\n                {'name': 'Timestamp', 'value': 425608.80694},\n                {'name': 'AudioHandlers', 'value': 0},\n                {'name': 'ThreadTime', 'value': 0.002074},\n                ...\n                ]\n            }\n        ```\n\n        Returns:\n            A dict of performance metrics including 'ScriptDuration', 'ThreadTime', 'ProcessTime', and 'DomContentLoaded'.\n        \"\"\"\n        # The commented out code below should have been executed prior to this function call.\n        # self._webdriver.execute_cdp_cmd(\"Performance.enable\", {})\n        return self._webdriver.execute_cdp_cmd(\"Performance.getMetrics\", {})\n"
  },
  {
    "path": "pylenium/config.py",
    "content": "from pathlib import Path\nfrom typing import List, Dict, Optional\n\nfrom pydantic import BaseModel\n\n# PYLENIUM CONFIG #\n###################\n\n\nclass DriverConfig(BaseModel):\n    browser: str = \"chrome\"\n    remote_url: str = \"\"\n    wait_time: int = 10\n    page_load_wait_time: int = 0\n    options: List[str] = []\n    capabilities: Dict = {}\n    experimental_options: Optional[List[Dict]] = None\n    extension_paths: Optional[List[str]] = None\n    webdriver_kwargs: Optional[Dict] = None\n    local_path: str = \"\"\n\n\nclass LoggingConfig(BaseModel):\n    pylog_level: str = \"INFO\"\n    screenshots_on: bool = True\n\n\nclass ViewportConfig(BaseModel):\n    maximize: bool = True\n    width: int = 1440\n    height: int = 900\n    orientation: str = \"portrait\"\n\n\nclass PyleniumConfig(BaseModel):\n    driver: DriverConfig = DriverConfig()\n    logging: LoggingConfig = LoggingConfig()\n    viewport: ViewportConfig = ViewportConfig()\n    custom: dict = {}\n\n\n# MODELS #\n##########\n\n\nclass TestCase(BaseModel):\n    name: str\n    file_path: Path\n\n    class ConfigDict:\n        arbitrary_types_allowed = True\n"
  },
  {
    "path": "pylenium/driver.py",
    "content": "from logging import Logger\nfrom typing import Dict, List, Set, Union\n\nfrom faker import Faker\nfrom selenium.common.exceptions import TimeoutException, WebDriverException\nfrom selenium.webdriver.common.by import By\nfrom selenium.webdriver.common.keys import Keys\nfrom selenium.webdriver.remote.webdriver import WebDriver\nfrom selenium.webdriver.support import expected_conditions as ec\nfrom selenium.webdriver.support.wait import WebDriverWait\n\nfrom pylenium import webdriver_factory\nfrom pylenium.a11y import PyleniumAxe\nfrom pylenium.cdp import CDP\nfrom pylenium.config import PyleniumConfig\nfrom pylenium.element import Element, Elements\nfrom pylenium.log import logger as log\nfrom pylenium.performance import Performance\nfrom pylenium.switch_to import SwitchTo\nfrom pylenium.wait import PyleniumWait\n\n\nclass PyleniumShould:\n    \"\"\"A collection of conditions (aka expectations) for the Pylenium Driver including the browser, window, and more.\n\n    Examples:\n    ```\n        py.should().have_title(\"QA at the Point\")\n    ```\n    \"\"\"\n\n    def __init__(self, py: \"Pylenium\", timeout: int, ignored_exceptions: list = None):\n        self._py = py\n        self._wait: PyleniumWait = self._py.wait(timeout=timeout, use_py=True, ignored_exceptions=ignored_exceptions)\n\n    def have_title(self, title: str) -> \"Pylenium\":\n        \"\"\"An expectation that the current title matches the given title.\n\n        Args:\n            title: The title to match.\n\n        Returns:\n            The current instance of Pylenium.\n\n        Raises:\n            `AssertionError` if the condition is not met within the timeout.\n        \"\"\"\n        log.command(\"Pylenium.should().have_title(): `%s`\", title)\n        try:\n            value = self._wait.until(ec.title_is(title))\n        except TimeoutException:\n            value = False\n        if value:\n            return self._py\n        raise AssertionError(f\"Expected Title: `{title}` - Actual Title: `{self._py.title()}`\")\n\n    def contain_title(self, string: str) -> \"Pylenium\":\n        \"\"\"An expectation that the current title contains the given string.\n\n        Args:\n            string: The case-sensitive string for the title to contain.\n\n        Returns:\n            The current instance of Pylenium.\n\n        Raises:\n            `AssertionError` if the condition is not met within the timeout.\n        \"\"\"\n        log.command(\"Pylenium.should().contain_title(): `%s`\", string)\n        try:\n            value = self._wait.until(ec.title_contains(string))\n        except TimeoutException:\n            value = False\n        if value:\n            return self._py\n        raise AssertionError(f\"Expected `{string}` to be in `{self._py.title()}`\")\n\n    def have_url(self, url: str) -> \"Pylenium\":\n        \"\"\"An expectation that the current URL matches the given url.\n\n        Args:\n            url: The url to match.\n\n        Returns:\n            The current instance of Pylenium.\n\n        Raises:\n            `AssertionError` if the condition is not met within the timeout.\n        \"\"\"\n        log.command(\"Pylenium.should().have_url(): `%s`\", url)\n        try:\n            value = self._wait.until(ec.url_to_be(url))\n        except TimeoutException:\n            value = False\n        if value:\n            return self._py\n        raise AssertionError(f\"Expected URL: `{url}` - Actual URL: `{self._py.url()}`\")\n\n    def contain_url(self, string: str) -> \"Pylenium\":\n        \"\"\"An expectation that the current URL contains the given string.\n\n        Args:\n            string: The case-sensitive string for the url to contain.\n\n        Returns:\n            The current instance of Pylenium.\n\n        Raises:\n            `AssertionError` if the condition is not met within the timeout.\n        \"\"\"\n        log.command(\"Pylenium.should().contain_url(): `%s`\", string)\n        try:\n            value = self._wait.until(ec.url_contains(string))\n        except TimeoutException:\n            value = False\n        if value:\n            return self._py\n        raise AssertionError(f\"Expected `{string}` to be in `{self._py.url()}`\")\n\n    def not_find(self, css: str) -> bool:\n        \"\"\"An expectation that there are no elements with the given CSS in the DOM.\n\n        Args:\n            css: The CSS selector.\n\n        Returns:\n            True if no elements are found.\n\n        Raises:\n            `AssertionError` if the condition is not met within the timeout.\n        \"\"\"\n        log.command(\"Pylenium.should().not_find() elements with CSS: `%s`\", css)\n        try:\n            self._wait.until_not(lambda x: x.find_element(By.CSS_SELECTOR, css))\n            return True\n        except TimeoutException:\n            raise AssertionError(f\"Found element with css: `{css}`\")\n\n    def not_findx(self, xpath: str) -> bool:\n        \"\"\"An expectation that there are no elements with the given XPATH in the DOM.\n\n        Args:\n            xpath: The XPATH selector.\n\n        Returns:\n            True if no elements are found.\n\n        Raises:\n            `AssertionError` if the condition is not met within the timeout.\n        \"\"\"\n        log.command(\"Pylenium.should().not_findx() elements with XPATH: `%s`\", xpath)\n        try:\n            self._wait.until_not(lambda x: x.find_element(By.XPATH, xpath))\n            return True\n        except TimeoutException:\n            raise AssertionError(f\"Found element with xpath: `{xpath}`\")\n\n    def not_contain(self, text: str) -> bool:\n        \"\"\"An expectation that there are no elements with the given text in the DOM.\n\n        Args:\n            text: The text to contain.\n\n        Returns:\n            True if no elements are found.\n\n        Raises:\n            `AssertionError` if the condition is not met within the timeout.\n        \"\"\"\n        log.command(\"Pylenium.should().not_contain() any elements with the text: `%s`\", text)\n        try:\n            self._wait.until_not(lambda x: x.find_element(By.XPATH, f'//*[contains(text(), \"{text}\")]'))\n            return True\n        except TimeoutException:\n            raise AssertionError(f\"Found element containing text: `{text}`\")\n\n\nclass Pylenium:\n    \"\"\"The Pylenium API.\"\"\"\n\n    def __init__(self, config: PyleniumConfig):\n        self.config = config\n        log.setLevel(self.config.logging.pylog_level)\n        self.fake = Faker()\n        self.Keys = Keys\n        self._webdriver = None\n        self._wait = None\n\n    def init_webdriver(self):\n        \"\"\"Initialize WebDriver using the Pylenium Config.\"\"\"\n        self._webdriver = webdriver_factory.build_from_config(self.config)\n        caps = self._webdriver.capabilities\n        try:\n            log.debug(\n                \"Capabilities: browserName: %s, browserVersion: %s, platformName: %s, session_id: %s\",\n                caps[\"browserName\"],\n                caps[\"browserVersion\"],\n                caps[\"platformName\"],\n                self._webdriver.session_id,\n            )\n        except Exception:\n            log.warning(\"webdriver.capabilities did not have a key that Pylenium was expecting. \" \"Is your driver executable the right version?\")\n\n        # Default instance of PyleniumWait\n        self._wait = PyleniumWait(self, self._webdriver, self.config.driver.wait_time, ignored_exceptions=None)\n\n        # Initial Browser Setup\n        if self.config.driver.page_load_wait_time:\n            self.set_page_load_timeout(self.config.driver.page_load_wait_time)\n\n        if self.config.viewport.maximize:\n            self.maximize_window()\n        else:\n            self.viewport(self.config.viewport.width, self.config.viewport.height, self.config.viewport.orientation)\n        return self._webdriver\n\n    @property\n    def webdriver(self) -> WebDriver:\n        \"\"\"The current instance of Selenium's `WebDriver` API.\"\"\"\n        return self.init_webdriver() if self._webdriver is None else self._webdriver\n\n    @property\n    def log(self) -> Logger:\n        \"\"\"A simple and convenient logger to use.\n\n        This is Pylenium's logger which can be useful, but it's recommended that you use your own.\n        However, sometimes you need something simple and this logger may be enough.\n\n        Levels:\n            - CRITICAL = 50\n            - ERROR    = 40\n            - WARNING  = 30\n            - USER     = 25 * (for YOU!)\n            - INFO     = 20\n            - COMMAND  = 15 * (default, used by Pylenium)\n            - DEBUG    = 10\n\n        Examples:\n        ```\n            # You can use the familiar debug, info, warning, error, and critical levels\n            py.log.info(\"INFO MESSAGE\")\n\n            # Or use the custom levels (see Levels above)\n            py.log.this(\"Hello, %s\", \"world\")    # a message at the USER level\n            py.log.command(\"Don't use this one\") # a message at the COMMAND level\n        ```\n\n        Returns:\n            Pylenium's instance of a Logger class\n        \"\"\"\n        return log\n\n    # region Sub APIs\n\n    def should(self, timeout: int = 0, ignored_exceptions: list = None) -> PyleniumShould:\n        \"\"\"PyleniumdShould API: A collection of expectations for the current Pylenium Driver.\n\n        Examples:\n        ```\n            py.should().contain_title(\"QA at the Point\")\n            py.should().have_url(\"https://qap.dev\")\n        ```\n        \"\"\"\n        if timeout:\n            wait_time = timeout\n        else:\n            wait_time = self.config.driver.wait_time\n        return PyleniumShould(self, wait_time, ignored_exceptions)\n\n    @property\n    def axe(self) -> PyleniumAxe:\n        \"\"\"PyleniumAxe API: Accessibility (a11y) Auditing and Reporting.\n\n        Examples:\n        ```\n            # Write automated tests against the report aXe returns!\n            def test_zero_violations(py: Pylenium):\n                py.visit(\"https://fake.website.com\")\n                report = py.axe.run()\n                violation_count = len(report.violations)\n                assert violation_count == 0, f\"{violation_count} violation(s) found!\"\n        ```\n        \"\"\"\n        return PyleniumAxe(self.webdriver)\n\n    @property\n    def performance(self) -> Performance:\n        \"\"\"Performance API: Pylenium's custom way of capturing web performnace metrics.\n\n        Examples:\n        ```\n            # Store the entire WebPerformance object and log it\n            perf = py.performance.get()\n            log.info(perf.dict())\n\n            # Get a single data point from WebPerformance\n            tti = py.performance.get().time_to_interactive()\n        ```\n        \"\"\"\n        return Performance(self.webdriver)\n\n    @property\n    def cdp(self) -> CDP:\n        \"\"\"Chrome DevTools Protocol API.\n\n        Examples:\n        ```\n            metrics = py.cdp.get_performance_metrics()\n            >>> metrics\n\n            {'metrics': [\n                {'name': 'Timestamp', 'value': 425608.80694},\n                {'name': 'AudioHandlers', 'value': 0},\n                {'name': 'ThreadTime', 'value': 0.002074},\n                ...\n                ]\n            }\n        ```\n        \"\"\"\n        return CDP(self.webdriver)\n\n    # endregion\n\n    # region METHODS\n\n    def title(self) -> str:\n        \"\"\"Get the current page's title.\"\"\"\n        log.command(\"py.title() - Get the current page title\")\n        return self.webdriver.title\n\n    def url(self) -> str:\n        \"\"\"Get the current page's URL.\"\"\"\n        log.command(\"py.url() - Get the current page URL\")\n        return self.webdriver.current_url\n\n    # endregion\n\n    # region NAVIGATION\n\n    def visit(self, url: str) -> \"Pylenium\":\n        \"\"\"Navigate to the given URL.\n\n        Returns:\n            The current instance of Pylenium\n        \"\"\"\n        log.command(\"py.visit() - Visit URL: `%s`\", url)\n        self.webdriver.get(url)\n        return self\n\n    def go(self, direction: str, number: int = 1) -> \"Pylenium\":\n        \"\"\"Navigate forward or back.\n\n        Args:\n            direction: `\"forward\"` or `\"back\"`\n            number: default is 1, will go back or forward one page in history.\n\n        Examples:\n        ```\n            py.go(\"back\", 2) # will go back 2 pages in history.\n            py.go(\"forward\") # will go forward 1 page in history.\n        ```\n\n        Returns:\n            The current instance of Pylenium\n        \"\"\"\n        log.command(\"py.go() - Go %s %s in browser history\", direction, number)\n        if direction == \"back\":\n            self.webdriver.execute_script(\"window.history.go(arguments[0])\", number * -1)\n        elif direction == \"forward\":\n            self.webdriver.execute_script(\"window.history.go(arguments[0])\", number)\n        else:\n            raise ValueError(f\"direction was invalid. Must be `forward` or `back` but was {direction}\")\n        return self\n\n    def reload(self) -> \"Pylenium\":\n        \"\"\"Reload (aka refresh) the current window.\n\n        Returns:\n            The current instance of Pylenium\n        \"\"\"\n        log.command(\"py.reload() - Reload (refresh) the current page\")\n        self.webdriver.refresh()\n        return self\n\n    # endregion\n\n    # region FIND ELEMENTS\n\n    def contains(self, text: str, timeout: int = None) -> Element:\n        \"\"\"Get the DOM element containing the given text.\n\n        * If `timeout=None` (default), use the default wait_time.\n        * If `timeout > 0`, override the default wait_time.\n        * If `timeout=0`, poll the DOM immediately without any waiting.\n\n        Args:\n            text: The text for the element to contain.\n            timeout: The number of seconds to wait for this to succeed. Overrides the default wait_time.\n\n        Returns:\n            The first element that is found, even if multiple elements match the query.\n        \"\"\"\n        log.command(\"py.contains() - Get the element containing the text: `%s`\", text)\n        locator = (By.XPATH, f'//*[contains(text(), \"{text}\")]')\n\n        if timeout == 0:\n            element = self.webdriver.find_element(*locator)\n        else:\n            element = self.wait(timeout).until(lambda x: x.find_element(*locator), f\"Could not find element with the text `{text}`\")\n        return Element(self, element, locator)\n\n    def get(self, css: str, timeout: int = None) -> Element:\n        \"\"\"Get the DOM element that matches the CSS selector.\n\n        * If `timeout=None` (default), use the default wait_time.\n        * If `timeout > 0`, override the default wait_time.\n        * If `timeout=0`, poll the DOM immediately without any waiting.\n\n        Args:\n            css: The selector to use.\n            timeout: The number of seconds to wait for this to succeed. Overrides the default wait_time.\n\n        Returns:\n            The first element that is found, even if multiple elements match the query.\n        \"\"\"\n        log.command(\"py.get() - Find the element with CSS: `%s`\", css)\n        by = By.CSS_SELECTOR\n\n        if timeout == 0:\n            element = self.webdriver.find_element(by, css)\n        else:\n            element = self.wait(timeout).until(lambda x: x.find_element(by, css), f\"Could not find element with the CSS `{css}`\")\n        return Element(self, element, locator=(by, css))\n\n    def find(self, css: str, timeout: int = None) -> Elements:\n        \"\"\"Finds all DOM elements that match the CSS selector.\n\n        * If `timeout=None (default)`, use the default wait_time.\n        * If `timeout > 0`, override the default wait_time.\n        * If `timeout=0`, poll the DOM immediately without any waiting.\n\n        Args:\n            css: The selector to use.\n            timeout: The number of seconds to wait for this to succeed. Overrides the default wait_time.\n\n        Returns:\n            A list of the found elements.\n        \"\"\"\n        by = By.CSS_SELECTOR\n        log.command(\"py.find() - Find elements with CSS: `%s`\", css)\n\n        try:\n            if timeout == 0:\n                elements = self.webdriver.find_elements(by, css)\n            else:\n                elements = self.wait(timeout).until(lambda x: x.find_elements(by, css), f\"Could not find any elements with the CSS `{css}`\")\n        except TimeoutException:\n            elements = []\n        return Elements(self, elements, locator=(by, css))\n\n    def getx(self, xpath: str, timeout: int = None) -> Element:\n        \"\"\"Finds the DOM element that match the XPATH selector.\n\n        * If `timeout=None` (default), use the default wait_time.\n        * If `timeout > 0`, override the default wait_time.\n        * If `timeout=0`, poll the DOM immediately without any waiting.\n\n        Args:\n            xpath: The selector to use.\n            timeout: The number of seconds to wait for this to succeed. Overrides the default wait_time.\n\n        Returns:\n            The first element that is found, even if multiple elements match the query.\n        \"\"\"\n        by = By.XPATH\n        log.command(\"py.getx() - Find the element with xpath: `%s`\", xpath)\n\n        if timeout == 0:\n            element = self.webdriver.find_element(by, xpath)\n        else:\n            element = self.wait(timeout).until(lambda x: x.find_element(by, xpath), f\"Could not find an element with xpath: `{xpath}`\")\n        return Element(self, element, locator=(by, xpath))\n\n    def findx(self, xpath: str, timeout: int = None) -> Elements:\n        \"\"\"Finds the DOM elements that match the XPATH selector.\n\n        * If `timeout=None` (default), use the default wait_time.\n        * If `timeout > 0`, override the default wait_time.\n        * If `timeout=0`, poll the DOM immediately without any waiting.\n\n        Args:\n            xpath: The selector to use.\n            timeout: The number of seconds to wait for this to succeed. Overrides the default wait_time.\n\n        Returns:\n            A list of the found elements.\n        \"\"\"\n        by = By.XPATH\n        log.command(\"py.findx() - Find elements with xpath: `%s`\", xpath)\n\n        try:\n            if timeout == 0:\n                elements = self.webdriver.find_elements(by, xpath)\n            else:\n                elements = self.wait(timeout).until(lambda x: x.find_elements(by, xpath), f\"Could not find an element with xpath: `{xpath}`\")\n        except TimeoutException:\n            elements = []\n        return Elements(self, elements, locator=(by, xpath))\n\n    # endregion\n\n    # region UTILITIES\n\n    def wait(self, timeout: int = None, use_py: bool = False, ignored_exceptions: List = None) -> Union[WebDriverWait, PyleniumWait]:\n        \"\"\"The Wait object with the given timeout in seconds.\n\n        If `timeout=None` or `timeout=0`,\n        return the default instance of wait, else return a new instance of WebDriverWait or PyleniumWait.\n\n        Args:\n            timeout: The number of seconds to wait for the condition.\n            use_py: True for a PyleniumWait.\n            ignored_exceptions: List of exceptions for the condition to ignore.\n\n        Default Ignored Exceptions:\n            * `NoSuchElementException`\n\n        Examples:\n        ```\n            # Use the default wait_time in pylenium.json\n\n            py.wait().until(lambda x: x.find_element(By.ID, \"foo\").get_attribute(\"style\") == \"display: block;\")\n\n            # Use a different timeout to control how long to wait for\n\n            py.wait(5).until(lambda x: x.find_element(By.ID, \"foo\").is_displayed())\n            py.wait(15, [NoSuchElementException, WebDriverException]).until(lambda x: x.find_element(By.ID, \"foo\"))\n        ```\n        \"\"\"\n        if timeout:  # if not None and greater than 0\n            return self._wait.build(timeout, use_py, ignored_exceptions)\n        return self._wait.build(self.config.driver.wait_time, use_py, ignored_exceptions)\n\n    # endregion\n\n    # region COOKIES\n\n    def delete_cookie(self, name: str) -> None:\n        \"\"\"Deletes the cookie with the given name.\n\n        Examples:\n        ```\n            py.delete_cookie(\"cookie_name\")\n        ```\n        \"\"\"\n        log.command(\"py.delete_cookie() - Delete cookie named: `%s`\", name)\n        self.webdriver.delete_cookie(name)\n\n    def delete_all_cookies(self) -> None:\n        \"\"\"Delete all cookies in the current session.\"\"\"\n        log.command(\"py.delete_all_cookies() - Delete all cookies\")\n        self.webdriver.delete_all_cookies()\n\n    def get_cookie(self, name) -> Dict:\n        \"\"\"Get the cookie with the given name.\n\n        Returns:\n            The cookie if found, else None.\n\n        Examples:\n        ```\n            cookie = py.get_cookie(\"cookie_name\")\n            assert cookie[\"name\"] == name\n        ```\n        \"\"\"\n        log.command(\"py.get_cookie() - Get cookie with name: `%s`\", name)\n        return self.webdriver.get_cookie(name)\n\n    def get_all_cookies(self) -> Set[Dict]:\n        \"\"\"Get all cookies.\n\n        Returns:\n            A set of cookies\n\n        Examples:\n        ```\n            cookies = py.get_all_cookies()\n            assert len(cookies) > 0\n        ```\n        \"\"\"\n        log.command(\"py.get_cookies() - Get all cookies\")\n        return self.webdriver.get_cookies()\n\n    def set_cookie(self, cookie: Dict):\n        \"\"\"Adds a cookie to your current session.\n\n        Args:\n            cookie: A dictionary object, with required keys: \"name\" and \"value\";\n\n                * optional keys: \"path\", \"domain\", \"secure\", \"expiry\"\n\n        Examples:\n        ```\n            py.set_cookie({'name' : 'foo', 'value' : 'bar'})`\n            py.set_cookie({'name' : 'foo', 'value' : 'bar', 'path' : '/'})\n            py.set_cookie({'name' : 'foo', 'value' : 'bar', 'path' : '/', 'secure':True})\n        ```\n        \"\"\"\n        log.command(\"py.set_cookie() - Set a cookie with name=`%s` and value=`%s`\", cookie[\"name\"], cookie[\"value\"])\n        self.webdriver.add_cookie(cookie)\n\n    # endregion\n\n    # region BROWSER\n\n    def execute_script(self, javascript: str, *args):\n        \"\"\"Executes javascript in the current window or frame.\n\n        Args:\n            javascript: The script string to execute.\n            args: Any arguments to be used in the script.\n\n        Returns:\n            The value returned by the script.\n\n        Examples:\n        ```\n            title = py.execute_script(\"return document.title;\")\n            webelement = py.execute_script(\"return document.getElementById(arguments[0]);\", element_id)\n        ```\n        \"\"\"\n        log.command(\"py.execute_script() - Execute javascript into the Browser\")\n        return self.webdriver.execute_script(javascript, *args)\n\n    def execute_async_script(self, javascript: str, *args):\n        \"\"\"Executes javascript asynchronously in the current window or frame.\n\n        Args:\n            javascript: The script string to execute.\n            args: Any arguments to be used in the script.\n\n        Returns:\n            The value returned by the script.\n\n        Examples:\n        ```\n            py.execute_async_script(\"return document.title;\")\n        ```\n        \"\"\"\n        log.command(\"py.execute_async_script() - Execute javascript asynchronously into the Browser\")\n        return self.webdriver.execute_async_script(javascript, *args)\n\n    def quit(self):\n        \"\"\"Quits the driver.\n\n        Closes any and every window/tab associated with the current session.\n        \"\"\"\n        log.command(\"py.quit() - Quit Pylenium and close all windows from the browser session\")\n        self.webdriver.quit()\n\n    def screenshot(self, filename: str) -> str:\n        \"\"\"Take a screenshot of the current Window.\n\n        Args:\n            filename: the filepath including the filename and extension (like `.png`)\n\n        Examples:\n        ```\n            py.screenshot(\"screenshots/home_page.png\")\n        ```\n        \"\"\"\n        log.command(\"py.screenshot() - Save screenshot to: `%s`}\", filename)\n        self.webdriver.save_screenshot(filename)\n        return filename\n\n    def scroll_to(self, x, y) -> \"Pylenium\":\n        \"\"\"Scroll to a location on the page.\n\n        Args:\n            x: The number of pixels to scroll horizontally.\n            y: The number of pixels to scroll vertically.\n\n        Examples:\n        ```\n            # Scroll down 500 px\n            py.scroll_to(0, 500)\n        ```\n        \"\"\"\n        log.command(\"py.scroll_to() - Scroll to (%s, %s)\", x, y)\n        js = \"window.scrollTo(arguments[0], arguments[1]);\"\n        self.webdriver.execute_script(js, x, y)\n        return self\n\n    @property\n    def switch_to(self) -> SwitchTo:\n        \"\"\"Switch between contexts like Windows, Tabs, and iFrames.\n\n        Examples:\n        ```\n            py.switch_to.new_tab()\n            py.switch_to.frame(\"iframe-id\")\n        ```\n        \"\"\"\n        return SwitchTo(self)\n\n    def maximize_window(self) -> \"Pylenium\":\n        \"\"\"Maximizes the current Window.\"\"\"\n        try:\n            self.webdriver.maximize_window()\n        except WebDriverException as e:\n            log.error(f\"Unable to maximize window: {e.msg}\")\n        return self\n\n    def set_page_load_timeout(self, timeout: int) -> \"Pylenium\":\n        \"\"\"Set the amount of time to wait for a page load to complete before throwing an error.\n\n        Args:\n            timeout: The time to wait for.\n        \"\"\"\n        self.webdriver.set_page_load_timeout(timeout)\n        return self\n\n    def viewport(self, width: int, height: int, orientation: str = \"portrait\") -> \"Pylenium\":\n        \"\"\"Control the size and orientation of the current context's browser window.\n\n        Args:\n            width: The width in pixels\n            height: The height in pixels\n            orientation: default is 'portrait'. Pass 'landscape' to reverse the width/height.\n\n        Examples:\n        ```\n            py.viewport(1280, 800) # macbook-13 size\n            py.viewport(1440, 900) # macbook-15 size\n            py.viewport(375, 667)  # iPhone X size\n        ```\n        \"\"\"\n        log.command(\"py.viewport() - Set viewport to width=%s, height=%s, orientation=%s\", width, height, orientation)\n        if orientation == \"portrait\":\n            self.webdriver.set_window_size(width, height)\n        elif orientation == \"landscape\":\n            self.webdriver.set_window_size(height, width)\n        else:\n            raise ValueError(\"Orientation must be `portrait` or `landscape`.\")\n        return self\n\n    @property\n    def window_handles(self) -> List[str]:\n        \"\"\"Returns the handles of all Windows in the current session.\n\n        * This property is mainly used to switch between windows or tabs\n\n        Examples:\n        ```\n            # assert that there are two windows - the main website and a new tab\n            windows = py.window_handles\n            assert len(windows) == 2\n\n            # then switch to the new tab\n            py.switch_to.window(name_or_handle=windows[1])\n        ```\n        \"\"\"\n        return self.webdriver.window_handles\n\n    @property\n    def window_size(self) -> Dict:\n        \"\"\"Gets the width and height of the current Window.\n\n        Examples:\n        ```\n            size = py.window_size\n            print(size[\"width\"])\n            print(size[\"height\"])\n        ```\n        \"\"\"\n        return self.webdriver.get_window_size()\n\n    # endregion\n"
  },
  {
    "path": "pylenium/element.py",
    "content": "import time\nfrom typing import List, Optional, Tuple\n\nfrom selenium.common.exceptions import NoSuchElementException, TimeoutException, WebDriverException\nfrom selenium.webdriver import ActionChains\nfrom selenium.webdriver.common.by import By\nfrom selenium.webdriver.remote.webdriver import WebElement\nfrom selenium.webdriver.support import expected_conditions as ec\nfrom selenium.webdriver.support.select import Select\n\nfrom pylenium import jquery\nfrom pylenium.log import logger as log\n\n\nclass ElementWait:\n    def __init__(self, webelement, timeout: int, ignored_exceptions: list = None):\n        self._webelement = webelement\n        self._timeout = 10 if timeout == 0 else timeout\n        if ignored_exceptions:\n            self._ignored_exceptions = ignored_exceptions\n        else:\n            self._ignored_exceptions = NoSuchElementException\n\n    def until(self, method, message=\"\"):\n        screen = None\n        stacktrace = None\n\n        end_time = time.time() + self._timeout\n        while True:\n            try:\n                value = method(self._webelement)\n                if value:\n                    return value\n            except self._ignored_exceptions as exc:\n                screen = getattr(exc, \"screen\", None)\n                stacktrace = getattr(exc, \"stacktrace\", None)\n            time.sleep(0.5)\n            if time.time() > end_time:\n                break\n        raise TimeoutException(message, screen, stacktrace)\n\n\nclass ElementsShould:\n    \"\"\"ElementsShould API: Commands (aka Expectations) for the current list of Elements.\"\"\"\n\n    def __init__(self, py, elements: \"Elements\", timeout: int, ignored_exceptions: list = None):\n        self._py = py\n        self._elements = elements\n        self._wait = py.wait(timeout=timeout, use_py=True, ignored_exceptions=ignored_exceptions)\n\n    # region POSITIVE EXPECTATIONS\n\n    def be_empty(self) -> bool:\n        \"\"\"An expectation that the list has no elements.\n\n        Returns:\n            True if empty, else False.\n\n        Raises:\n            `AssertionError` if the condition is not met in the specified amount of time.\n        \"\"\"\n        log.command(\"Elements.should().be_empty()\")\n        try:\n            if self._elements.is_empty():\n                return True\n            locator = self._elements.locator\n            value = self._wait.until(lambda drvr: len(drvr.find_elements(*locator)) == 0)\n        except TimeoutException:\n            value = False\n        if value:\n            return True\n        raise AssertionError(\"List of elements was not empty\")\n\n    def be_greater_than(self, length: int) -> bool:\n        \"\"\"An expectation that the number of elements in the list is greater than the given length.\n\n        Args:\n            length: The length that the list should be greater than.\n\n        Raises:\n            `AssertionError` if the condition is not met in the specified amount of time.\n        \"\"\"\n        log.command(\"Elements.should().be_greater_than(): %s\", length)\n        try:\n            if self._elements.length() > length:\n                return True\n            locator = self._elements.locator\n            value = self._wait.until(lambda drvr: len(drvr.find_elements(*locator)) > length)\n        except TimeoutException:\n            value = False\n        if value:\n            return True\n        raise AssertionError(f\"Length of elements was not greater than {length}\")\n\n    def be_less_than(self, length: int) -> bool:\n        \"\"\"An expectation that the number of elements in the list is less than the given length.\n\n        Args:\n            length: The length that the list should be less than.\n\n        Raises:\n            `AssertionError` if the condition is not met in the specified amount of time.\n        \"\"\"\n        log.command(\"Elements.should().be_less_than(): %s\", length)\n        try:\n            if self._elements.length() < length:\n                return True\n            locator = self._elements.locator\n            value = self._wait.until(lambda drvr: len(drvr.find_elements(*locator)) < length)\n        except TimeoutException:\n            value = False\n        if value:\n            return True\n        raise AssertionError(f\"Length of elements was not less than {length}\")\n\n    def have_length(self, length: int) -> bool:\n        \"\"\"An expectation that the number of elements in the list is equal to the given length.\n\n        Args:\n            length: The length that the list should be equal to.\n\n        Raises:\n            `AssertionError` if the condition is not met in the specified amount of time.\n        \"\"\"\n        log.command(\"Elements.should().have_length(): %s\", length)\n        try:\n            if self._elements.length() == length:\n                return True\n            locator = self._elements.locator\n            value = self._wait.until(lambda drvr: len(drvr.find_elements(*locator)) == length)\n        except TimeoutException:\n            value = False\n        if value:\n            return True\n        raise AssertionError(f\"Length of elements was not equal to {length}\")\n\n    # endregion\n\n    # region NEGATIVE EXPECTATIONS\n\n    def not_be_empty(self) -> \"Elements\":\n        \"\"\"An expectation that the list has at least one element.\n\n        Returns:\n            The list of elements if not empty.\n\n        Raises:\n            `AssertionError` if the condition is not met in the specified amount of time.\n        \"\"\"\n        log.command(\"Elements.should().not_be_empty()\")\n        try:\n            if not self._elements.is_empty():\n                return self._elements\n            locator = self._elements.locator\n            value = self._wait.until(lambda drvr: drvr.find_elements(*locator))\n        except TimeoutException:\n            value = False\n        if value:\n            return Elements(self._py, value, self._elements.locator)\n        raise AssertionError(\"List of elements was empty\")\n\n    # endregion\n\n\nclass ElementShould:\n    \"\"\"ElementShould API: Commands (aka Expectations) for the current Element.\"\"\"\n\n    def __init__(self, py, element: \"Element\", timeout: int, ignored_exceptions: list = None):\n        self._py = py\n        self._element = element\n        self._wait = ElementWait(element.webelement, timeout, ignored_exceptions)\n\n    # region POSITIVE EXPECTATIONS\n\n    def be_clickable(self) -> \"Element\":\n        \"\"\"An expectation that the element is displayed and enabled so you can click it.\n\n        Returns:\n            The current element.\n\n        Raises:\n            `AssertionError` if the condition is not met in the specified amount of time.\n        \"\"\"\n        log.command(\"Element.should().be_clickable()\")\n        try:\n            value = self._wait.until(lambda e: e.is_displayed() and e.is_enabled())\n        except TimeoutException:\n            value = False\n        if value:\n            return self._element\n        raise AssertionError(\"Element was not clickable\")\n\n    def be_checked(self) -> \"Element\":\n        \"\"\"An expectation that the element is checked.\n\n        Returns:\n            The current element.\n\n        Raises:\n            `AssertionError` if the condition is not met in the specified amount of time.\n        \"\"\"\n        log.command(\"Element.should().be_checked()\")\n        try:\n            value = self._wait.until(lambda e: self._element.is_checked())\n        except TimeoutException:\n            value = False\n        if value:\n            return self._element\n        raise AssertionError(\"Element was not checked\")\n\n    def be_disabled(self) -> \"Element\":\n        \"\"\"An expectation that the element is disabled.\n\n        Returns:\n            The current element.\n\n        Raises:\n            `AssertionError` if the condition is not met in the specified amount of time.\n        \"\"\"\n        log.command(\"Element.should().be_disabled()\")\n        try:\n            value = self._wait.until(lambda e: not e.is_enabled())\n        except TimeoutException:\n            value = False\n        if value:\n            return self._element\n        raise AssertionError(\"Element was not disabled\")\n\n    def be_enabled(self) -> \"Element\":\n        \"\"\"An expectation that the element is enabled.\n\n        Returns:\n            The current element.\n\n        Raises:\n            `AssertionError` if the condition is not met in the specified amount of time.\n        \"\"\"\n        log.command(\"Element.should().be_enabled()\")\n        try:\n            value = self._wait.until(lambda e: e.is_enabled())\n        except TimeoutException:\n            value = False\n        if value:\n            return self._element\n        raise AssertionError(\"Element was not enabled\")\n\n    def be_focused(self) -> \"Element\":\n        \"\"\"An expectation that the element is focused.\n\n        Returns:\n            The current element.\n\n        Raises:\n            `AssertionError` if the condition is not met in the specified amount of time.\n        \"\"\"\n        log.command(\"Element.should().be_focused()\")\n        try:\n            value = self._wait.until(lambda e: e == self._py.webdriver.switch_to.active_element)\n        except TimeoutException:\n            value = False\n\n        if value:\n            return self._element\n        raise AssertionError(\"Element was not focused\")\n\n    def be_hidden(self) -> \"Element\":\n        \"\"\"An expectation that the element is not displayed but still in the DOM (aka hidden).\n\n        Returns:\n            The current element.\n\n        Raises:\n            `AssertionError` if the condition is not met in the specified amount of time.\n        \"\"\"\n        log.command(\"Element.should().be_hidden()\")\n        try:\n            value = self._wait.until(lambda e: e and not e.is_displayed())\n        except TimeoutException:\n            value = False\n\n        if value:\n            return self._element\n        raise AssertionError(\"Element was not hidden\")\n\n    def be_selected(self) -> \"Element\":\n        \"\"\"An expectation that the element is selected.\n\n        Returns:\n            The current element.\n\n        Raises:\n            `AssertionError` if the condition is not met in the specified amount of time.\n        \"\"\"\n        log.command(\"Element.should().be_selected()\")\n        try:\n            value = self._wait.until(lambda e: e.is_selected())\n        except TimeoutException:\n            value = False\n\n        if value:\n            return self._element\n        raise AssertionError(\"Element was not selected\")\n\n    def be_visible(self) -> \"Element\":\n        \"\"\"An expectation that the element is displayed.\n\n        Returns:\n            The current element.\n\n        Raises:\n            `AssertionError` if the condition is not met in the specified amount of time.\n        \"\"\"\n        log.command(\"Element.should().be_visible()\")\n        try:\n            value = self._wait.until(lambda e: e and e.is_displayed())\n        except TimeoutException:\n            value = False\n\n        if value:\n            return self._element\n        raise AssertionError(\"Element was not visible\")\n\n    def have_attr(self, attr: str, value: Optional[str] = None) -> \"Element\":\n        \"\"\"An expectation that the element has the given attribute with the given value.\n\n        Args:\n            attr: The name of the attribute.\n            value (optional): The value of the attribute.\n\n        Returns:\n            The current element.\n\n        Raises:\n            `AssertionError` if the condition is not met in the specified amount of time.\n        \"\"\"\n        log.command(\"Element.should().have_attr() `%s`\", attr)\n        try:\n            if value is None:\n                val = self._wait.until(lambda e: e.get_attribute(attr))\n            else:\n                val = self._wait.until(lambda e: e.get_attribute(attr) == value)\n        except TimeoutException:\n            val = False\n\n        if val:\n            return self._element\n        if value is None:\n            raise AssertionError(f\"Element did not have attribute: `{attr}`\")\n        raise AssertionError(\n            f\"Expected Attribute Value: `{value}` \"\n            f'- Actual Attribute Value: `{self._element.get_attribute(\"value\")}`'\n        )\n\n    def have_class(self, class_name: str) -> \"Element\":\n        \"\"\"An expectation that the element has the given className.\n\n        Args:\n            class_name: The `.className` of the element\n\n        Returns:\n            The current element.\n\n        Raises:\n            `AssertionError` if the condition is not met in the specified amount of time.\n        \"\"\"\n        log.command(\"Element.should().have_class() `%s`\", class_name)\n        try:\n            val = self._wait.until(lambda e: e.get_attribute(\"class\") == class_name)\n        except TimeoutException:\n            val = False\n\n        if val:\n            return self._element\n        raise AssertionError(\n            f\"Expected className: `{class_name}` \" f'- Actual className: `{self._element.get_attribute(\"class\")}`'\n        )\n\n    def have_prop(self, prop: str, value: str) -> \"Element\":\n        \"\"\"An expectation that the element has the given property with the given value.\n\n        Args:\n            prop: The name of the property.\n            value: The value of the property.\n\n        Returns:\n            The current element.\n\n        Raises:\n            `AssertionError` if the condition is not met in the specified amount of time.\n        \"\"\"\n        log.command(\"Element.should().have_prop() `%s` with value of `%s`\", prop, value)\n        try:\n            val = self._wait.until(lambda e: e.get_property(prop) == value)\n        except TimeoutException:\n            val = False\n\n        if val:\n            return self._element\n        raise AssertionError(\n            f\"Expected Property value: `{value}` \" f\"- Actual Property value: `{self._element.get_property(prop)}`\"\n        )\n\n    def have_text(self, text, case_sensitive=True) -> \"Element\":\n        \"\"\"An expectation that the element has the given text.\n\n        Args:\n            text: The exact text to match.\n            case_sensitive: False if you want to ignore casing and leading/trailing spaces.\n\n        Returns:\n            The current element.\n\n        Raises:\n            `AssertionError` if the condition is not met in the specified amount of time.\n        \"\"\"\n        log.command(\"Element.should().have_text() `%s`\", text)\n        try:\n            if case_sensitive:\n                value = self._wait.until(lambda e: e.text == text)\n            else:\n                value = self._wait.until(lambda e: e.text.strip().lower() == text.lower())\n        except TimeoutException:\n            value = False\n\n        if value:\n            return self._element\n        raise AssertionError(f\"Expected text: `{text}` - Actual text: `{self._element.text()}`\")\n\n    def contain_text(self, text, case_sensitive=True) -> \"Element\":\n        \"\"\"An expectation that the element contains the given text.\n\n        Args:\n            text: The text that the element should contain.\n            case_sensitive: False if you want to ignore casing and leading/trailing spaces.\n\n        Returns:\n            The current element.\n\n        Raises:\n            `AssertionError` if the condition is not met in the specified amount of time.\n        \"\"\"\n        log.command(\"Element.should().contain_text() `%s`\", text)\n        try:\n            if case_sensitive:\n                value = self._wait.until(lambda e: text in e.text)\n            else:\n                value = self._wait.until(lambda e: text.lower() in e.text.strip().lower())\n        except TimeoutException:\n            value = False\n\n        if value:\n            return self._element\n        raise AssertionError(f\"Expected `{text}` to be in `{self._element.text()}`\")\n\n    def have_value(self, value) -> \"Element\":\n        \"\"\"An expectation that the element has the given value.\n\n        Args:\n            value: The exact value to match. Pass `None` if you expect the element not to have the value attribute.\n\n        Returns:\n            The current element.\n\n        Raises:\n            `AssertionError` if the condition is not met in the specified amount of time.\n\n        Examples:\n            * An element with no `value` attribute will yield `None`\n            * An element with a `value` attribute with no value will yield an empty string `\"\"`\n            * An element with a `value` attribute with a value will yield the value\n        \"\"\"\n        log.command(\"Element.should().have_value() `%s`\", value)\n        try:\n            val = self._wait.until(lambda e: e.get_attribute(\"value\") == value)\n        except TimeoutException:\n            val = False\n\n        if val:\n            return self._element\n        raise AssertionError(f'Expected value: `{value}` - Actual value: `{self._element.get_attribute(\"value\")}`')\n\n    # endregion\n\n    # region NEGATIVE EXPECTATIONS\n\n    def not_be_focused(self) -> \"Element\":\n        \"\"\"An expectation that the element is not focused.\n\n        Returns:\n            The current element.\n\n        Raises:\n            `AssertionError` if the condition is not met in the specified amount of time.\n        \"\"\"\n        log.command(\"Element.should().not_be_focused()\")\n        try:\n            value = self._wait.until(lambda e: e != self._py.webdriver.switch_to.active_element)\n        except TimeoutException:\n            value = False\n\n        if value:\n            return self._element\n        raise AssertionError(\"Element had focus\")\n\n    def disappear(self):\n        \"\"\"An expectation that the element eventually disappears from the DOM.\n\n        Returns:\n            The current instance of Pylenium.\n\n        Raises:\n            `AssertionError` if the condition is not met in the specified amount of time.\n\n        Examples:\n            # wait for a loading spinner to appear and then disappear once the load is complete\n            py.get(#spinner).should().disappear()\n        \"\"\"\n        log.command(\"Element.should().disappear()\")\n        try:\n            value = self._wait.until(ec.invisibility_of_element(self._element.webelement))\n        except TimeoutException:\n            value = False\n        if value:\n            return self._py\n        raise AssertionError(\"Element was still visible or still in the DOM\")\n\n    def not_have_attr(self, attr: str, value: Optional[str] = None) -> \"Element\":\n        \"\"\"An expectation that the element does not have the given attribute with the given value.\n\n        Either the attribute does not exist on the element or the value does not match the given value.\n\n        Args:\n            attr: The name of the attribute.\n            value (optional): The value of the attribute.\n\n        Returns:\n            The current element.\n\n        Raises:\n            `AssertionError` if the condition is not met in the specified amount of time.\n        \"\"\"\n        log.command(\"Element.should().not_have_attr() `%s`\", attr)\n        try:\n            if value is None:\n                val = self._wait.until(lambda e: not e.get_attribute(attr))\n            else:\n                val = self._wait.until(lambda e: e.get_attribute(attr) is None or e.get_attribute(attr) != value)\n        except TimeoutException:\n            val = False\n\n        if val:\n            return self._element\n        if value is None:\n            raise AssertionError(f\"Element had the attribute: `{attr}`\")\n        raise AssertionError(f\"Element still had attribute `{attr}` with the value of `{value}`\")\n\n    def not_have_value(self, value) -> \"Element\":\n        \"\"\"An expectation that the element does not have the given value.\n\n        Args:\n            value: The exact value not to match. Pass `None` if you expect the element to have the value attribute.\n\n        Returns:\n            The current element.\n\n        Raises:\n            `AssertionError` if the condition is not met in the specified amount of time.\n\n        Examples:\n            * An element with no `value` attribute will yield `None`\n            * An element with a `value` attribute with no value will yield an empty string `\"\"`\n            * An element with a `value` attribute with a value will yield the value\n        \"\"\"\n        log.command(\"Element.should().not_have_value() `%s`\", value)\n        try:\n            val = self._wait.until(lambda e: e.get_attribute(\"value\") != value)\n        except TimeoutException:\n            val = False\n\n        if val:\n            return self._element\n        raise AssertionError(f\"Element had value matching ``{value}``\")\n\n    def not_have_text(self, text, case_sensitive=True) -> \"Element\":\n        \"\"\"An expectation that the element does not have the given text.\n\n        Args:\n            text: The exact text to match.\n            case_sensitive: False if you want to ignore casing and leading/trailing spaces.\n\n        Returns:\n            The current element.\n\n        Raises:\n            `AssertionError` if the condition is not met in the specified amount of time.\n        \"\"\"\n        log.command(\"Element.should().not_have_text() `%s`\", text)\n        try:\n            if case_sensitive:\n                value = self._wait.until(lambda e: e.text != text)\n            else:\n                value = self._wait.until(lambda e: e.text.strip().lower() != text.lower())\n        except TimeoutException:\n            value = False\n\n        if value:\n            return self._element\n        raise AssertionError(f\"Element had the text matching ``{text}``\")\n\n    # endregion\n\n\nclass Elements(List[\"Element\"]):\n    \"\"\"Elements API: Represents a list of DOM webelements and includes commands to work with them.\"\"\"\n\n    def __init__(self, py, web_elements, locator: Optional[Tuple]):\n        self._list = [Element(py, element, None) for element in web_elements]\n        self._py = py\n        self.locator = locator\n        super().__init__(self._list)\n\n    def should(self, timeout: int = 0, ignored_exceptions: list = None) -> ElementsShould:\n        \"\"\"A collection of expectations for this list of elements.\n\n        Examples:\n        ```\n            py.find(\"option\").should().not_be_empty()\n        ```\n        \"\"\"\n        if timeout:\n            wait_time = timeout\n        else:\n            wait_time = self._py.config.driver.wait_time\n        return ElementsShould(self._py, self, wait_time, ignored_exceptions)\n\n    # region METHODS\n\n    def length(self) -> int:\n        \"\"\"The number of elements in the list.\"\"\"\n        return len(self._list)\n\n    def first(self) -> \"Element\":\n        \"\"\"Gets the first element in the list.\n\n        Raises:\n            `IndexError` if the list is empty.\n        \"\"\"\n        if self.length() > 0:\n            return self._list[0]\n        raise IndexError(\"Cannot get first() from an empty list\")\n\n    def last(self) -> \"Element\":\n        \"\"\"Gets the last element in the list.\n\n        Raises:\n            `IndexError` if the list is empty.\n        \"\"\"\n        if self.length() > 0:\n            return self._list[-1]\n        raise IndexError(\"Cannot get last() from an empty list\")\n\n    # endregion\n\n    # region CONDITIONS\n\n    def is_empty(self) -> bool:\n        \"\"\"Checks if there are zero elements in the list.\"\"\"\n        return self.length() == 0\n\n    def are_checked(self) -> bool:\n        \"\"\"Check that all checkbox or radio buttons in this list are selected.\"\"\"\n        for element in self._list:\n            if not element.is_checked():\n                return False\n        # every element is checked\n        return True\n\n    # endregion\n\n    # region ACTIONS\n\n    def check(self, allow_selected=False) -> \"Elements\":\n        \"\"\"Check all checkboxes or radio buttons in this list.\n\n        Args:\n            allow_selected: Do not raise error if any elements are already selected.\n\n        Raises:\n            `ValueError` if any elements are already selected.\n            `ValueError` if any elements are not checkboxes or radio buttons.\n        \"\"\"\n        log.command(\"Elements.check() - Check all checkboxes or radio buttons in this list\")\n        for element in self._list:\n            element.check(allow_selected)\n        return self\n\n    def uncheck(self, allow_deselected=False) -> \"Elements\":\n        \"\"\"Check all checkboxes or radio buttons in this list.\n\n        Args:\n            allow_deselected: Do not raise error if any elements are already deselected.\n\n        Raises\n            `ValueError` if any elements are already selected.\n            `ValueError` if any elements are not checkboxes or radio buttons.\n        \"\"\"\n        log.command(\"Elements.uncheck() - Uncheck all checkboxes or radio buttons in this list\")\n        for element in self._list:\n            element.uncheck(allow_deselected)\n        return self\n\n    # endregion\n\n\nclass Element:\n    \"\"\"Element API: Represents a single DOM webelement and includes the commands to work with it.\"\"\"\n\n    def __init__(self, py, web_element: WebElement, locator: Optional[Tuple]):\n        self._py = py\n        self._webelement = (web_element,)\n        self.locator = locator\n\n    @property\n    def webelement(self) -> WebElement:\n        \"\"\"The current instance of the Selenium's `WebElement` API.\"\"\"\n        if isinstance(self._webelement, Tuple):\n            return self._webelement[0]\n        return self._webelement\n\n    def should(self, timeout: int = 0, ignored_exceptions: list = None) -> ElementShould:\n        \"\"\"A collection of expectations for this element.\n\n        Examples:\n        ```\n            py.get(\"#foo\").should().be_clickable()\n        ```\n        \"\"\"\n        if timeout:\n            wait_time = timeout\n        else:\n            wait_time = self._py.config.driver.wait_time\n        return ElementShould(self._py, self, wait_time, ignored_exceptions)\n\n    # region METHODS\n\n    def css_value(self, property_name: str):\n        \"\"\"EXPERIMENTAL: Gets the CSS Value of this element given the property's name.\"\"\"\n        log.command(\"Element.css_value() - Get a CSS Value given the property: `%s`\", property_name)\n        try:\n            return self.webelement.value_of_css_property(property_name)\n        except WebDriverException:\n            log.warning(\"Property Name: `%s` is invalid or not found\", property_name)\n            return None\n\n    def tag_name(self) -> str:\n        \"\"\"Gets the tag name of this element.\"\"\"\n        log.command(\"Element.tag_name() - Get the tag name of this element\")\n        return self.webelement.tag_name\n\n    def text(self) -> str:\n        \"\"\"Gets the InnerText of this element.\"\"\"\n        log.command(\"Element.text() - Get the text of this element\")\n        return self.webelement.text\n\n    def get_attribute(self, attribute: str):\n        \"\"\"Gets the given attribute's value.\n\n            * If the value is 'true' or 'false', then this returns a Boolean\n            * If the name does not exist, then return None\n            * All other values are returned as strings\n\n        Args:\n            attribute: The name of the element's attribute.\n\n        Returns:\n            The value of the attribute. If the attribute does not exist, returns None\n        \"\"\"\n        log.command(\"Element.get_attribute() - Get the `%s` attribute value from this element\", attribute)\n        value = self.webelement.get_attribute(attribute)\n        if value == \"true\":\n            return True\n        if value == \"false\":\n            return False\n        return value\n\n    def get_property(self, prop: str):\n        \"\"\"Gets the property's value.\n\n        Args:\n            prop: The name of the element's property.\n\n        Returns:\n            The value of the property.\n        \"\"\"\n        log.command(\"Element.get_property() - Get the `%s` property value of this element\", prop)\n        return self.webelement.get_property(prop)\n\n    # endregion\n\n    # region CONDITIONS\n\n    def is_checked(self) -> bool:\n        \"\"\"Check that this checkbox or radio button is selected.\n\n        Raises:\n            `ValueError` if element is not a checkbox or radio button\n        \"\"\"\n        type_ = self.webelement.get_attribute(\"type\")\n        if type_ != \"checkbox\" and type_ != \"radio\":\n            raise ValueError(\"Element is not a checkbox or radio button\")\n\n        log.command(\"Element.is_checked() - Check if this checkbox or radio button element is selected\")\n        return self._py.webdriver.execute_script(\"return arguments[0].checked;\", self.webelement)\n\n    def is_displayed(self) -> bool:\n        \"\"\"Check that this element is displayed.\n\n        Raises:\n            `NoSuchElementException` if the element is not in the DOM\n\n        Returns:\n            True if element is visible to the user, else False\n        \"\"\"\n        log.command(\"Element.is_displayed() - Check if this element is displayed\")\n        return self.webelement.is_displayed()\n\n    def is_enabled(self) -> bool:\n        \"\"\"Check that this element is enabled.\n\n        Returns:\n            True if the element is enabled, else False\n        \"\"\"\n        log.command(\"Element.is_enabled() - Check if this element is enabled\")\n        return self.webelement.is_enabled()\n\n    def is_selected(self) -> bool:\n        \"\"\"Check that this element is selected.\n\n        Returns:\n            True if the element is selected, else False\n        \"\"\"\n        log.command(\"Element.is_selected() - Check if this element is selected\")\n        return self.webelement.is_selected()\n\n    # endregion\n\n    # region ACTIONS\n\n    def check(self, allow_selected=False) -> \"Element\":\n        \"\"\"Check this checkbox or radio button.\n\n        Args:\n            allow_selected: Do not raise error if element is already selected.\n\n        Raises:\n            - `ValueError` if element is already selected.\n            - `ValueError` if element is not a checkbox or radio button\n\n        Returns:\n            The current element\n        \"\"\"\n        log.command(\"Element.check() - Select this checkbox or radio button\")\n        type_ = self.webelement.get_attribute(\"type\")\n        if type_ == \"checkbox\" or type_ == \"radio\":\n            checked = self._py.webdriver.execute_script(\"return arguments[0].checked;\", self.webelement)\n            if not checked:\n                self.webelement.click()\n                return self\n            if allow_selected:\n                return self\n            raise ValueError(f\"{type_} is already selected\")\n        raise ValueError(\"Element is not a checkbox or radio button\")\n\n    def uncheck(self, allow_deselected=False) -> \"Element\":\n        \"\"\"Uncheck this checkbox or radio button.\n\n        Args:\n            allow_deselected: Do not raise error if element is already deselected.\n\n        Raises:\n            - `ValueError` if element is already deselected.\n            - `ValueError` if element is not a checkbox or radio button\n\n        Returns:\n            The current element\n        \"\"\"\n        log.command(\"Element.uncheck() - Deselect this checkbox or radio button\")\n        type_ = self.webelement.get_attribute(\"type\")\n        if type_ == \"checkbox\" or type_ == \"radio\":\n            checked = self._py.webdriver.execute_script(\"return arguments[0].checked;\", self.webelement)\n            if checked:\n                self.webelement.click()\n                return self\n            if allow_deselected:\n                return self\n            raise ValueError(f\"{type_} is already deselected\")\n        raise ValueError(\"Element is not a checkbox or radio button\")\n\n    def clear(self) -> \"Element\":\n        \"\"\"Clears the text of the input or textarea element.\n\n            * Only works on elements that can accept text entry.\n\n        Returns:\n            The current element\n        \"\"\"\n        log.command(\"Element.clear() - Clear the input of this element.\")\n        self.webelement.clear()\n        return self\n\n    def click(self, force=False):\n        \"\"\"Clicks the element.\n\n        Args:\n            force: If True, a JavascriptExecutor command is sent instead of Selenium's native `.click()`.\n\n        Returns:\n            The current instance of Pylenium\n        \"\"\"\n        log.command(\"Element.click() - Click this element\")\n        if force:\n            self._py.webdriver.execute_script(\"arguments[0].click()\", self.webelement)\n        else:\n            self.webelement.click()\n        return self._py\n\n    def deselect(self, value):\n        \"\"\"Deselects all `<option>` within a multi `<select>` element that match the given value.\n\n        Args:\n            value: The value or text content of the `<option>` elements to be deselected.\n\n        Raises:\n            - `UnexpectedTagNameException` if this element is not a `<select>`\n            - `NoSuchElementException` if a matching `<option>` element is not found.\n\n        Returns:\n            The current element\n        \"\"\"\n        log.command(\"Element.deselect() - Deselect options from a dropdown element\")\n        select = Select(self.webelement)\n        try:\n            select.deselect_by_visible_text(value)\n        except NoSuchElementException:\n            select.deselect_by_value(value)\n        return self\n\n    def double_click(self):\n        \"\"\"Double clicks the element.\n\n        Returns:\n            The current instance of Pylenium\n        \"\"\"\n        log.command(\"Element.double_click() - Double click this element\")\n        ActionChains(self._py.webdriver).double_click(self.webelement).perform()\n        return self._py\n\n    def drag_to(self, css: str) -> \"Element\":\n        \"\"\"Drag the current element to another element given its CSS selector.\n\n        Args:\n            css: The CSS selector of the element to drag to.\n\n        Returns:\n            The current element\n\n        Examples:\n        ```\n            py.get(\"#draggable-card\").drag_to(\"#done-column\")\n        ```\n        \"\"\"\n        log.command(\"Element.drag_to() - Drag this element to another element by CSS: `%s`\", css)\n        to_element = self._py.get(css).webelement\n        jquery.drag_and_drop(self._py.webdriver, self.webelement, to_element)\n        return self\n\n    def drag_to_element(self, to_element: \"Element\") -> \"Element\":\n        \"\"\"Drag the current element to the given element.\n\n        Args:\n            to_element: The Element to drag to.\n\n        Returns:\n            The current element\n\n        Examples:\n        ```\n            column = py.get(\"#done-column\")\n            py.get(\"#draggable-card\").drag_to_element(column)\n        ```\n        \"\"\"\n        log.command(\"Element.drag_to_element() - Drag this element to another element\")\n        jquery.drag_and_drop(self._py.webdriver, self.webelement, to_element.webelement)\n        return self\n\n    def focus(self) -> \"Element\":\n        \"\"\"Put focus on the element.\n\n        Returns:\n            The current element\n        \"\"\"\n\n        log.command(\"Element.focus() - Focus this element\")\n        self._py.execute_script(\"arguments[0].focus();\", self.webelement)\n        return self\n\n    def hover(self):\n        \"\"\"Hovers the element.\n\n        Returns:\n            The current instance of Pylenium\n        \"\"\"\n        log.command(\"Element.hover() - Hovers this element\")\n        ActionChains(self._py.webdriver).move_to_element(self.webelement).perform()\n        return self._py\n\n    def right_click(self):\n        \"\"\"Right clicks the element.\n\n        Returns:\n            The current instance of Pylenium\n        \"\"\"\n        log.command(\"Element.right_click() - Right click this element\")\n        ActionChains(self._py.webdriver).context_click(self.webelement).perform()\n        return self._py\n\n    def select_by_index(self, index: int) -> \"Element\":\n        \"\"\"Select an `<option>` element within a `<select>` dropdown given its index.\n\n        This is not done by counting the options, but by examining their index attributes.\n\n        Args:\n            index: The index position of the `<option>` to be selected.\n\n        Raises:\n            - `UnexpectedTagNameException` if the dropdown is not a `<select>` element.\n            - `NoSuchElementException` if the `<option>` with the given index doesn't exist.\n\n        Returns:\n            The current element\n        \"\"\"\n        log.command(\"Element.select_by_index() - Select an <option> element in the dropdown by index: %s\", index)\n        dropdown = Select(self.webelement)\n        dropdown.select_by_index(index)\n        return self\n\n    def select_by_text(self, text: str) -> \"Element\":\n        \"\"\"Selects all `<option>` elements within a `<select>` dropdown given the option's text.\n\n        Args:\n            text: The text within the `<option>` to be selected.\n\n        Raises:\n            - `UnexpectedTagNameException` if the dropdown is not a `<select>` element.\n            - `NoSuchElementException` if the `<option>` with the given text doesn't exist.\n\n        Returns:\n            The current element\n        \"\"\"\n        log.command(\n            \"Element.select_by_text() - Select one or more <option> elements in the dropdown by text: `%s`\", text\n        )\n        dropdown = Select(self.webelement)\n        dropdown.select_by_visible_text(text)\n        return self\n\n    def select_by_value(self, value) -> \"Element\":\n        \"\"\"Selects all `<option>` elements within a `<select>` dropdown given the option's value.\n\n        Args:\n            value: The value within the `<option>` to be selected.\n\n        Raises:\n            - `UnexpectedTagNameException` if the dropdown is not a `<select>` element.\n            - `NoSuchElementException` if the `<option>` with the given value doesn't exist.\n\n        Returns:\n            The current element\n        \"\"\"\n        log.command(\n            \"Element.select_by_value() - Select one or more <option> elements in this dropdown by value: `%s`\", value\n        )\n        dropdown = Select(self.webelement)\n        dropdown.select_by_value(value)\n        return self\n\n    def submit(self):\n        \"\"\"Submits the form.\n\n            * Meant for `<form>` and `<input>` elements. May not have an effect on other types.\n\n        Returns:\n            The current instance of Pylenium\n        \"\"\"\n        log.command(\"Element.submit() - Submit the form\")\n        self.webelement.submit()\n        return self._py\n\n    def type(self, *args) -> \"Element\":\n        \"\"\"Simulate a user typing keys into the input.\n\n        Returns:\n            The current element\n        \"\"\"\n        log.command(\"Element.type() - Type keys into this element\")\n        self.webelement.send_keys(args)\n        return self\n\n    def upload(self, filepath: str) -> \"Element\":\n        \"\"\"A convenience method to upload a file to the element.\n\n        You can already do this by using the `Element.type()` command,\n        but using `Element.upload()` is more intuitive and easier to read.\n\n        ```\n        # Selenium .send_keys()\n        driver.find_element(By.ID(\"select-file\")).send_keys(\"path/to/file.png\")\n\n        # Pylenium .type()\n        py.get(\"#select-file\").type(\"path/to/file.png\")\n        ```\n\n        Args:\n            filepath: The absolute path, including the filename and extension, of the file to upload.\n\n        Returns:\n            The current element\n\n        Examples:\n            A 'normal' flow for uploading a file is:\n        ```\n            1. Get the 'Select File' element to select the file to upload\n            2. Click on an 'Upload Button' to complete the upload\n\n            py.get(\"#select-file\").upload(\"path/to/file.png\")\n            py.get(\"#upload-button\").click()\n        ```\n        \"\"\"\n        log.command(\"Element.upload() - Select a file to upload: `%s`\", filepath)\n        self.webelement.send_keys(filepath)\n        return self\n\n    # endregion\n\n    # region FIND ELEMENTS\n\n    def contains(self, text: str, timeout: int = None) -> \"Element\":\n        \"\"\"Gets the DOM element containing the `text`.\n\n        Args:\n            text: The text for the element to contain\n            timeout: The number of seconds to find the element. Overrides default wait_time.\n\n        Returns:\n            The first element that is found, even if multiple elements match the query.\n        \"\"\"\n        log.command(\"Element.contains() - Get the element that contains text: `%s`\", text)\n        locator = (By.XPATH, f'.//*[contains(text(), \"{text}\")]')\n\n        if timeout == 0:\n            element = self.webelement.find_element(*locator)\n        else:\n            element = self._py.wait(timeout).until(\n                lambda _: self.webelement.find_element(*locator), f\"Could not find element with the text: `{text}`\"\n            )\n        return Element(self._py, element, locator)\n\n    def get(self, css: str, timeout: int = None) -> \"Element\":\n        \"\"\"Gets the DOM element that matches the `css` selector in this element's context.\n\n        Args:\n            css: The selector\n            timeout: The number of seconds to find the element. Overrides default wait_time.\n\n        Returns:\n            The first element that is found, even if multiple elements match the query.\n        \"\"\"\n        log.command(\"Element.get() - Get the element with CSS: `%s`\", css)\n        by = By.CSS_SELECTOR\n\n        if timeout == 0:\n            element = self.webelement.find_element(by, css)\n        else:\n            element = self._py.wait(timeout).until(\n                lambda _: self.webelement.find_element(by, css), f\"Could not find element with the CSS: `{css}`\"\n            )\n        return Element(self._py, element, locator=(by, css))\n\n    def find(self, css: str, timeout: int = None) -> Elements:\n        \"\"\"Finds all DOM elements that match the `css` selector in this element's context.\n\n        Args:\n            css: The selector\n            timeout: The number of seconds to find at least one element. Overrides default wait_time.\n\n        Returns:\n            A list of the found elements. If none are found, an empty list is returned.\n        \"\"\"\n        log.command(\"Element.find() - Find elements with CSS: `%s`\", css)\n        by = By.CSS_SELECTOR\n\n        try:\n            if timeout == 0:\n                elements = self.webelement.find_elements(by, css)\n            else:\n                elements = self._py.wait(timeout).until(\n                    lambda _: self.webelement.find_elements(by, css), f\"Could not find any elements with CSS: `{css}`\"\n                )\n        except TimeoutException:\n            elements = []\n        return Elements(self._py, elements, locator=(by, css))\n\n    def getx(self, xpath: str, timeout: int = None) -> \"Element\":\n        \"\"\"Finds the DOM element that matches the `xpath` selector.\n\n        Args:\n            xpath: The selector to use.\n            timeout: The number of seconds to find the element. Overrides default wait_time.\n\n        Returns:\n            The first element found, even if multiple elements match the query.\n        \"\"\"\n        log.command(\"Element.getx() - Get the element with xpath: `%s`\", xpath)\n        by = By.XPATH\n\n        if timeout == 0:\n            elements = self.webelement.find_element(by, xpath)\n        else:\n            elements = self._py.wait(timeout).until(\n                lambda _: self.webelement.find_element(by, xpath),\n                f\"Could not find any elements with the xpath: `{xpath}`\",\n            )\n        return Element(self._py, elements, locator=(by, xpath))\n\n    def findx(self, xpath: str, timeout: int = None) -> \"Elements\":\n        \"\"\"Finds the DOM elements that matches the `xpath` selector.\n\n        Args:\n            xpath: The selector to use.\n            timeout: The number of seconds to find at least one element. Overrides default wait_time.\n\n        Returns:\n            A list of the found elements. If none are found, an empty list is returned.\n        \"\"\"\n        log.command(\"Element.findx() - Find the elements with xpath: `%s`\", xpath)\n        by = By.XPATH\n\n        try:\n            if timeout == 0:\n                elements = self.webelement.find_elements(by, xpath)\n            else:\n                elements = self._py.wait(timeout).until(\n                    lambda _: self.webelement.find_elements(by, xpath),\n                    f\"Could not find any elements with the xpath: `{xpath}`\",\n                )\n        except TimeoutException:\n            elements = []\n        return Elements(self._py, elements, locator=(by, xpath))\n\n    # endregion\n\n    # region FAMILY\n\n    def children(self) -> Elements:\n        \"\"\"Gets the Child elements.\"\"\"\n        log.command(\"Element.children() - Get the children of this element\")\n        elements = self._py.webdriver.execute_script(\"return arguments[0].children;\", self.webelement)\n        return Elements(self._py, elements, None)\n\n    def parent(self) -> \"Element\":\n        \"\"\"Gets the Parent element.\"\"\"\n        log.command(\"Element.parent() - Get the parent of this element\")\n        js = \"\"\"\n        elem = arguments[0];\n        return elem.parentNode;\n        \"\"\"\n        element = self._py.webdriver.execute_script(js, self.webelement)\n        return Element(self._py, element, None)\n\n    def siblings(self) -> Elements:\n        \"\"\"Gets the Sibling elements.\"\"\"\n        log.command(\"Element.siblings() - Get the siblings of this element\")\n        js = \"\"\"\n        elem = arguments[0];\n        var siblings = [];\n        var sibling = elem.parentNode.firstChild;\n\n        while (sibling) {\n            if (sibling.nodeType === 1 && sibling !== elem) {\n                siblings.push(sibling);\n            }\n            sibling = sibling.nextSibling\n        }\n        return siblings;\n        \"\"\"\n        elements = self._py.webdriver.execute_script(js, self.webelement)\n        return Elements(self._py, elements, None)\n\n    # endregion\n\n    # region UTILITIES\n\n    def screenshot(self, filename) -> \"Element\":\n        \"\"\"Take a screenshot of the current element.\n\n        Args:\n            filename: the filepath including the filename and extension (like `.png`)\n\n        Examples:\n        ```\n            py.get('#save-button').screenshot('elements/save-button.png')\n        ```\n        \"\"\"\n        log.command(\"Element.screenshot() - Take a screenshot of this element and save to: `%s`\", filename)\n        self.webelement.screenshot(filename)\n        return self\n\n    def scroll_into_view(self) -> \"Element\":\n        \"\"\"Scroll this element into view.\n\n        This usually means the middle of the element will appear at the bottom of the viewport.\n        \"\"\"\n        log.command(\"Element.scroll_into_view() - Scroll this element into view\")\n        self._py.webdriver.execute_script(\"arguments[0].scrollIntoView(true);\", self.webelement)\n        return self\n\n    def open_shadow_dom(self) -> \"Element\":\n        \"\"\"Open a Shadow DOM and return the Shadow Root element.\n\n        Examples:\n        ```\n            # Click a button within nested Shadow DOMs\n            shadow1 = py.get(\"extensions-manager\").open_shadow_dom()\n            shadow2 = shadow1.get(\"extensions-list\").open_shadow_dom()\n            shadow2.get(\"button\").click()\n        ```\n\n        References:\n            https://www.seleniumeasy.com/selenium-tutorials/accessing-shadow-dom-elements-with-webdriver\n        \"\"\"\n        log.command(\"Element.open_shadow_dom() - Open a Shadow DOM and return the Root element\")\n        shadow_element = self._py.execute_script(\"return arguments[0].shadowRoot\", self.webelement)\n        return Element(self._py, shadow_element, locator=None)\n\n    def highlight(self, effect_time=1, color=\"red\", border=5) -> \"Element\":\n        \"\"\"Highlights (blinks) the element.\"\"\"\n\n        def apply_style(s):\n            self._py.webdriver.execute_script(\"arguments[0].setAttribute('style', arguments[1]);\", self.webelement, s)\n\n        original_style = self.webelement.get_attribute(\"style\")\n        apply_style(f\"border: {border}px solid {color};\")\n        time.sleep(effect_time)\n        apply_style(original_style)\n\n        return self\n\n    # endregion\n"
  },
  {
    "path": "pylenium/jquery.py",
    "content": "from selenium.webdriver.remote.webdriver import WebDriver, By\nfrom selenium.webdriver.remote.webelement import WebElement\nfrom selenium.webdriver.support.wait import WebDriverWait\nfrom selenium.common.exceptions import StaleElementReferenceException\nfrom pylenium import utils\n\n\ndef inject(driver: WebDriver, version=\"3.5.1\", timeout=10):\n    \"\"\"Inject the given jQuery version to the current context and any iframes within it.\n\n    Args:\n        driver: The instance of WebDriver to attach to.\n        version: The jQuery version. (Default is 3.5.1)\n        timeout: The max number of seconds to wait for jQuery to be loaded. (Default is 10)\n    \"\"\"\n    jquery_url = f\"https://code.jquery.com/jquery-{version}.min.js\"\n    load_jquery = utils.read_script_from_file(\"load_jquery.js\")\n    driver.execute_async_script(load_jquery, jquery_url, None)\n    WebDriverWait(driver, timeout).until(\n        lambda drvr: drvr.execute_script('return typeof(jQuery) !== \"undefined\";'),\n        message='jQuery was \"undefined\" which means it did not load within timeout.',\n    )\n    iframes = driver.find_elements(By.TAG_NAME, \"iframe\")\n    for iframe in iframes:\n        try:\n            driver.execute_async_script(load_jquery, jquery_url, iframe)\n        except StaleElementReferenceException:\n            pass\n\n\ndef exists(driver: WebDriver) -> str:\n    \"\"\"Checks if jQuery exists in the current context.\n\n    Returns:\n        The version if found, else returns an empty string\n    \"\"\"\n    version = driver.execute_script(\"return jQuery().jquery;\")\n    return version if version is not None else \"\"\n\n\ndef drag_and_drop(driver: WebDriver, drag_element: WebElement, drop_element: WebElement, version=\"3.5.1\", timeout=10):\n    \"\"\"Simulate Drag and Drop using jQuery.\n\n    Args:\n        driver: The driver that will simulate the drag and drop.\n        drag_element: The element to be dragged.\n        drop_element: The element to drop to.\n        version: The jQuery version to use.\n        timeout: The max of number of seconds to wait for jQuery to be loaded.\n    \"\"\"\n    inject(driver, version)\n    dnd_js = utils.read_script_from_file(\"drag_and_drop.js\")\n    driver.execute_script(\n        dnd_js + \"jQuery(arguments[0]).simulateDragDrop({ dropTarget: arguments[1] });\", drag_element, drop_element\n    )\n"
  },
  {
    "path": "pylenium/log.py",
    "content": "\"\"\"Custom Logging for Pylenium\n\nLog Levels:\n    CRITICAL = 50,\n    ERROR    = 40,\n    WARNING  = 30,\n    USER     = 25, *\n    INFO     = 20, (default)\n    COMMAND  = 15,\n    DEBUG    = 10\n\"\"\"\n\nimport logging\nfrom rich.logging import RichHandler\n\n\nCOMMAND_LOG_LEVEL = 15\nCOMMAND_LOG_LEVEL_NAME = \"COMMAND\"\nUSER_LOG_LEVEL = 25\nUSER_LOG_LEVEL_NAME = \"USER\"\n\n\n# Add the levels\nlogging.addLevelName(COMMAND_LOG_LEVEL, COMMAND_LOG_LEVEL_NAME)\nlogging.COMMAND = COMMAND_LOG_LEVEL\nlogging.addLevelName(USER_LOG_LEVEL, USER_LOG_LEVEL_NAME)\nlogging.USER = USER_LOG_LEVEL_NAME\n\n# Create logger\nlogger = logging.getLogger(\"PYL\")\nlogger.setLevel(logging.INFO)\n\n# Configure logger\n# DATE_FORMAT = \"%Y/%m/%d %H:%M:%S\"\nlogger.addHandler(RichHandler(rich_tracebacks=True, markup=True))\n\n\ndef command(self, message: str, *args, **kwargs) -> None:\n    \"\"\"Log a command message.\n\n    Args:\n        message: The message to log (the string must be in \"%s\" format. No f-strings)\n\n    Examples:\n        def visit(self, url):\n            log.command(\"py.visit() - Visit URL: %s\", url)\n            ...\n    \"\"\"\n    # Yes, logger takes its '*args' as 'args'.\n    if self.isEnabledFor(COMMAND_LOG_LEVEL):\n        self._log(COMMAND_LOG_LEVEL, message, args, **kwargs)\n\n\ndef this(self, message: str, *args, **kwargs) -> None:\n    \"\"\"Log a message at the USER log level (one above the INFO log level).\n\n    * This is a convenient, custom log level for Pylenium users to use if needed\n\n    Args:\n        message: the message to log (the string must be in \"%s\" format. No f-strings)\n\n    Examples:\n        def add_to_cart(self, item: str, quantity: int):\n            log.this(\"Add items to cart: item=%s - qty=%s\", item, quantity)\n            ...\n    \"\"\"\n    # Yes, logger takes its '*args' as 'args'.\n    if self.isEnabledFor(USER_LOG_LEVEL):\n        self._log(USER_LOG_LEVEL, message, args, **kwargs)\n\n\n# Register the new loging functions\nlogging.Logger.command = command\nlogging.Logger.this = this\n"
  },
  {
    "path": "pylenium/performance.py",
    "content": "import time\nfrom typing import List, Union\n\nfrom pydantic import BaseModel, Field\nfrom selenium.common.exceptions import TimeoutException\nfrom selenium.webdriver.support.wait import WebDriverWait\n\nfrom pylenium.log import logger as log\n\n\ndef stopwatch(func):\n    \"\"\"Stopwatch Decorator.\n\n    Use this decorator on any function to measure how long it took\n    for the function to complete. This is in seconds, but may have fractions of a second\n    if the system clock provides more precision.\n\n    Notes:\n        This is _logged_, not printed to the Terminal\n\n    Examples:\n    ```\n        # 1. How long does it take to add an item to the cart?\n        @stopwatch\n        def add_item_to_cart(py):\n            py.get('#add-item').click()\n            py.get('#added-notification').should().be_visible()\n\n        # 2. How long does it take to edit an item's available stock via the API and see it change in the UI?\n        @stopwatch\n        def update_available_stock(py, item, quantity):\n            payload = {'item': item, 'qty': quantity}\n            api.items.update(payload)\n            py.get(f'#available-stock-{item}').should().have_text(quantity)\n    ```\n    \"\"\"\n\n    def wrapper(*args, **kwargs):\n        start_time = time.time()\n        func(*args, **kwargs)\n        stop_time = time.time()\n        func_name = func.__name__\n        log.debug(\"STOPWATCH - %s took %s seconds\", func_name, stop_time - start_time)\n\n    return wrapper\n\n\nclass Performance:\n    \"\"\"Pylenium's Performance API.\"\"\"\n\n    def __init__(self, webdriver):\n        self._webdriver = webdriver\n\n    def _wait(self, timeout=10):\n        return WebDriverWait(self._webdriver, timeout=timeout)\n\n    def get(self):\n        \"\"\"The main method used to generate a WebPerformance object from the current web page.\n\n        Notes:\n            Calling this method too soon may yield NoneTypes because the browser hasn't generated them yet.\n\n        Examples:\n        ```\n            # Store the entire WebPerformance object and log it\n            perf = py.performance.get()\n            log.info(perf.dict())\n\n            # Get a single data point from WebPerformance\n            tti = py.performance.get().time_to_interactive()\n        ```\n        \"\"\"\n        return WebPerformance(\n            time_origin=self.get_time_origin(),\n            navigation_timing=self.get_navigation_timing(),\n            paint_timing=self.get_paint_timing(),\n            resources=self.get_resources(),\n        )\n\n    def get_time_origin(self) -> float:\n        \"\"\"Returns the timeOrigin precision value.\n\n        This is the high resolution timestamp of the start time of the performance measurement.\n        \"\"\"\n        js = \"return window.performance.timeOrigin;\"\n        time_origin = self._wait().until(lambda driver: driver.execute_script(js), \"Time Origin not generated yet\")\n        return time_origin\n\n    def get_navigation_timing(self):\n        \"\"\"Return the PerformanceNavigationTiming object as a Python object.\"\"\"\n        js = 'return window.performance.getEntriesByType(\"navigation\")[0];'\n        navigation = self._wait().until(lambda driver: driver.execute_script(js), \"NavigationTiming not generated yet\")\n        return NavigationTiming(**navigation)\n\n    def get_paint_timing(self):\n        \"\"\"Return the PerformancePaintTiming object as a Python object.\"\"\"\n        js = 'return window.performance.getEntriesByName(\"first-contentful-paint\")[0];'\n        paint = self._wait().until(lambda driver: driver.execute_script(js), \"PaintTiming not generated yet\")\n        return PaintTiming(**paint)\n\n    def get_resources(self):\n        \"\"\"Return a list of PerformanceResourceTiming objects as Python objects.\"\"\"\n        js = 'return window.performance.getEntriesByType(\"resource\");'\n        try:\n            resources = self._wait().until(\n                lambda driver: driver.execute_script(js), message=\"Resources not generated yet or there are none\"\n            )\n            return [ResourceTiming(**resource) for resource in resources]\n        except TimeoutException:\n            return None  # because there were no Resources captured for the current web page\n\n\nclass NavigationTiming(BaseModel):\n    \"\"\"The PerformanceNavigationTiming Representation.\n\n    Metrics regarding the browser's document navigation events\n\n    References:\n        https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming\n    \"\"\"\n\n    connect_end: float = Field(alias=\"connectEnd\")\n    connect_start: float = Field(alias=\"connectStart\")\n    decoded_body_size: Union[int, float] = Field(alias=\"decodedBodySize\")\n    dom_complete: float = Field(alias=\"domComplete\")\n    dom_content_loaded_event_end: float = Field(alias=\"domContentLoadedEventEnd\")\n    dom_content_loaded_event_start: float = Field(alias=\"domContentLoadedEventStart\")\n    time_to_interactive: float = Field(alias=\"domInteractive\")\n    domain_lookup_end: float = Field(alias=\"domainLookupEnd\")\n    domain_lookup_start: float = Field(alias=\"domainLookupStart\")\n    duration: float\n    encoded_body_size: Union[int, float] = Field(alias=\"encodedBodySize\")\n    entry_type: str = Field(alias=\"entryType\")\n    fetch_start: float = Field(alias=\"fetchStart\")\n    initiator_type: str = Field(alias=\"initiatorType\")\n    load_event_end: float = Field(alias=\"loadEventEnd\")\n    load_event_start: float = Field(alias=\"loadEventStart\")\n    name: str\n    next_hop_protocol: str = Field(alias=\"nextHopProtocol\")\n    redirect_count: int = Field(alias=\"redirectCount\")\n    redirect_end: int = Field(alias=\"redirectEnd\")\n    redirect_start: int = Field(alias=\"redirectStart\")\n    request_start: float = Field(alias=\"requestStart\")\n    response_end: float = Field(alias=\"responseEnd\")\n    response_start: float = Field(alias=\"responseStart\")\n    secure_connection_start: float = Field(alias=\"secureConnectionStart\")\n    server_timing: List = Field(alias=\"serverTiming\")\n    start_time: int = Field(alias=\"startTime\")\n    transfer_size: Union[int, float] = Field(alias=\"transferSize\")\n    type: str\n    unload_event_end: int = Field(alias=\"unloadEventEnd\")\n    unload_event_start: int = Field(alias=\"unloadEventStart\")\n    worker_start: Union[int, float] = Field(alias=\"workerStart\")\n\n\nclass PaintTiming(BaseModel):\n    \"\"\"The PerformancePaintTiming Representation.\n\n    Provides timing information about \"paint\" (also called \"render\") operations during web page construction.\n\n    References:\n        https://developer.mozilla.org/en-US/docs/Web/API/PerformancePaintTiming\n    \"\"\"\n\n    duration: float\n    entry_type: str = Field(alias=\"entryType\", default=\"paint\")\n    name: str = Field(default=\"first-contentful-paint\")\n    start_time: float = Field(alias=\"startTime\")\n\n\nclass ResourceTiming(BaseModel):\n    \"\"\"The PerformanceResourceTiming Representation.\n\n    Detailed network timing data regarding the loading of an application's resources.\n    An application can use the timing metrics to determine, for example, the length of time it takes\n    to fetch a specific resource, such as an XMLHttpRequest, <SVG>, image, or script.\n\n    References:\n        https://developer.mozilla.org/en-US/docs/web/api/performanceresourcetiming\n    \"\"\"\n\n    connect_end: float = Field(alias=\"connectEnd\")\n    connect_start: float = Field(alias=\"connectStart\")\n    decoded_body_size: int = Field(alias=\"decodedBodySize\")\n    domain_lookup_end: float = Field(alias=\"domainLookupEnd\")\n    domain_lookup_start: float = Field(alias=\"domainLookupStart\")\n    duration: float\n    encoded_body_size: int = Field(alias=\"encodedBodySize\")\n    entry_type: str = Field(alias=\"entryType\", default=\"resource\")\n    fetch_start: float = Field(alias=\"fetchStart\")\n    initiator_type: str = Field(alias=\"initiatorType\")\n    name: str\n    next_hop_protocol: str = Field(alias=\"nextHopProtocol\")\n    redirect_end: float = Field(alias=\"redirectEnd\")\n    redirect_start: float = Field(alias=\"redirectStart\")\n    request_start: float = Field(alias=\"requestStart\")\n    response_end: float = Field(alias=\"responseEnd\")\n    response_start: float = Field(alias=\"responseStart\")\n    secure_connection_start: float = Field(alias=\"secureConnectionStart\")\n    server_timing: List = Field(alias=\"serverTiming\")\n    start_time: float = Field(alias=\"startTime\")\n    transfer_size: int = Field(alias=\"transferSize\")\n    worker_start: float = Field(alias=\"workerStart\")\n\n\nclass WebPerformance(BaseModel):\n    \"\"\"Pylenium's WebPerformance Object.\n\n    This is built using multiple W3C Performance Timing objects to provide\n    custom data points like:\n\n    * Page Load Time\n    * Time to First Contentful Paint\n    * Time to Interactive (TTI)\n    * and more!\n    \"\"\"\n\n    time_origin: float  # High resolution timestamp of the start time of the Performance measurement\n    navigation_timing: NavigationTiming\n    paint_timing: PaintTiming\n    resources: List[ResourceTiming]\n\n    def page_load_time(self) -> float:\n        \"\"\"The time it takes for the page to load as experienced by the user.\"\"\"\n        return self.navigation_timing.load_event_end - self.navigation_timing.start_time\n\n    def time_to_first_byte(self) -> float:\n        \"\"\"The time it takes before the first byte of response is received from the server.\"\"\"\n        return self.navigation_timing.response_start\n\n    def time_to_first_contentful_paint(self) -> float:\n        \"\"\"The time it takes for the majority of content to be fully rendered and consumable by the user.\"\"\"\n        return self.paint_timing.start_time\n\n    def time_to_interactive(self) -> float:\n        \"\"\"The time it takes for the layout to be stabilized and the page is responsive.\"\"\"\n        return self.navigation_timing.dom_complete\n\n    def number_of_requests(self) -> int:\n        \"\"\"The number of requests sent from start of navigation until end of page load.\"\"\"\n        return len(self.resources)\n\n    def time_to_dom_content_loaded(self) -> float:\n        return self.navigation_timing.dom_content_loaded_event_end\n\n    def page_weight(self) -> float:\n        \"\"\"The amount of bytes transferred for the page to be loaded.\"\"\"\n        resource_transfer_size = sum([r.transfer_size for r in self.resources])\n        return self.navigation_timing.transfer_size + resource_transfer_size\n\n    def connection_time(self) -> float:\n        \"\"\"The time taken to connect to the server.\"\"\"\n        return self.navigation_timing.connect_end - self.navigation_timing.connect_start\n\n    def request_time(self) -> float:\n        \"\"\"The time taken to send a request to the server and receive the response.\"\"\"\n        return self.navigation_timing.response_end - self.navigation_timing.response_start\n\n    def fetch_time(self) -> float:\n        \"\"\"The time to complete the document fetch (including accessing any caches, etc.).\"\"\"\n        return self.navigation_timing.response_end - self.navigation_timing.fetch_start\n"
  },
  {
    "path": "pylenium/scripts/__init__.py",
    "content": ""
  },
  {
    "path": "pylenium/scripts/allure_reporting.py",
    "content": "\"\"\" Allure reporting integration \"\"\"\nimport platform\nfrom typing import List\n\nimport typer\n\nfrom pylenium.scripts.cli_utils import parse_response, run_process\n\n\ndef _install(commands: List[str]):\n    \"\"\"Command to install allure via the CLI\"\"\"\n    typer.secho(\"\\n🛑 It's recommended that you run the above command(s) yourself to see all the output 🛑\", fg=typer.colors.BRIGHT_YELLOW)\n    answer = typer.prompt(\"\\nWould you like to proceed? (y/n)\", default=\"n\")\n    try:\n        if answer == \"y\":\n            response = run_process(commands)\n            _, err = parse_response(response)\n            if response.returncode != 0:\n                typer.secho(f\"😢 Unable to install allure. {err}\", fg=typer.colors.BRIGHT_RED)\n                typer.secho(\"Visit allure's docs for more options: https://docs.qameta.io/allure/#_get_started\", fg=typer.colors.BRIGHT_CYAN)\n                return\n            typer.secho(\"✅ allure installed. Try running `pylenium allure check` to verify the installation.\", fg=typer.colors.BRIGHT_GREEN)\n            return\n\n        if answer == \"n\" or answer is not None:\n            typer.secho(\"❌ Command aborted\")\n            return\n    except FileNotFoundError:\n        typer.secho(\"One of the commands was not found...\", fg=typer.colors.BRIGHT_RED)\n        typer.secho(\"Visit their docs for more info: https://docs.qameta.io/allure/#_get_started\", fg=typer.colors.BRIGHT_CYAN)\n\n\ndef install_for_linux():\n    \"\"\"Install allure on a Debian-based Linux machine.\"\"\"\n    typer.secho(\"This command only works for Debian-based Linux and uses sudo:\\n\", fg=typer.colors.BRIGHT_YELLOW)\n    typer.secho(\"    sudo apt-add-repository ppa:qameta/allure\")\n    typer.secho(\"    sudo apt-get update\")\n    typer.secho(\"    sudo apt-get install allure\")\n    _install(\n        [\n            \"sudo\",\n            \"apt-add-repository\",\n            \"ppa:qameta/allure\",\n            \"-y\",\n            \"sudo\",\n            \"apt-get\",\n            \"update\",\n            \"-y\",\n            \"sudo\",\n            \"apt-get\",\n            \"install\",\n            \"allure\",\n            \"-y\",\n        ]\n    )\n\n\ndef install_for_mac():\n    typer.secho(\"This command uses homebrew to do the installation:\\n\", fg=typer.colors.BRIGHT_YELLOW)\n    typer.secho(\"    brew install allure\")\n    _install([\"brew\", \"install\", \"allure\"])\n\n\ndef install_for_windows():\n    typer.secho(\"This command uses scoop to do the installation:\\n\", fg=typer.colors.BRIGHT_YELLOW)\n    typer.secho(\"    scoop install allure\")\n    _install([\"scoop\", \"install\", \"allure\"])\n\n\napp = typer.Typer()\n\n\n@app.command()\ndef check():\n    \"\"\"Check if the allure CLI is installed on the current machine.\"\"\"\n    typer.secho(\"\\n$ allure --version\")\n    err_message = \"\\n❌ allure is not installed or not added to the PATH. Visit https://docs.qameta.io/allure/#_get_started\"\n    try:\n        response = run_process([\"allure\", \"--version\"])\n        out, err = parse_response(response)\n        if response.returncode != 0:\n            typer.secho(err_message, fg=typer.colors.BRIGHT_RED)\n            typer.secho(err, fg=typer.colors.BRIGHT_RED)\n            return\n        typer.secho(f\"\\n✅ allure is installed with version: {out}\", fg=typer.colors.BRIGHT_GREEN)\n    except FileNotFoundError:\n        typer.secho(err_message)\n\n\n@app.command()\ndef install():\n    \"\"\"Install the allure CLI to the current machine.\"\"\"\n    typer.secho(\n        \"\\n💡 For more installation options and details, please visit allure's docs: https://docs.qameta.io/allure/#_get_started\", fg=typer.colors.BRIGHT_CYAN\n    )\n    operating_system = platform.system()\n    if operating_system.upper() == \"LINUX\":\n        install_for_linux()\n        return\n\n    if operating_system.upper() == \"DARWIN\":\n        install_for_mac()\n        return\n\n    if operating_system.upper() == \"WINDOWS\":\n        install_for_windows()\n        return\n\n\n@app.command()\ndef serve(folder: str = typer.Option(\"allure-report\", \"--folder\", \"-f\", help=\"The folder path to the allure report\")):\n    \"\"\"Start the allure server and serve the allure report given its folder path.\"\"\"\n    typer.secho(f\"\\n$ allure serve {folder}\")\n    typer.secho(\"Press <Ctrl+C> to exit\", fg=typer.colors.BRIGHT_CYAN)\n    try:\n        response = run_process([\"allure\", \"serve\", folder])\n        _, err = parse_response(response)\n        if response.returncode != 0:\n            typer.secho(f\"\\n❌ Unable to serve allure report. Check that the folder path is valid. {err}\", fg=typer.colors.BRIGHT_RED)\n    except FileNotFoundError:\n        typer.secho(\"\\n❌ allure is not installed or not added to the PATH. Visit https://docs.qameta.io/allure/#_get_started\", fg=typer.colors.BRIGHT_RED)\n\n\nif __name__ == \"__main__\":\n    app()\n"
  },
  {
    "path": "pylenium/scripts/cli.py",
    "content": "\"\"\" The Pylenium CLI using Typer.\n\nFor more information, visit their official docs: https://typer.tiangolo.com/\n\"\"\"\n\nimport os\nimport shutil\n\nimport typer\n\nfrom pylenium.scripts import allure_reporting as allure_\n\napp = typer.Typer()\napp.add_typer(allure_.app, name=\"allure\", help=\"Allure Reporting Commands\")\n\n\ndef _copy(file, to_dir, message) -> str:\n    \"\"\"Copies a file to the given directory.\n\n    If the file exists, it is overwritten.\n\n    Returns:\n        The absolute path of the newly copied file.\n    \"\"\"\n    newly_created_path = shutil.copy(src=file, dst=to_dir)\n    typer.secho(f\"✅ {message} {newly_created_path}\", fg=typer.colors.BRIGHT_GREEN)\n    return newly_created_path\n\n\n@app.command()\ndef init(\n    overwrite_conftest: bool = typer.Option(False, \"--conftest\", \"-c\", help=\"Overwrite conftest.py\"),\n    overwrite_pylenium_json: bool = typer.Option(False, \"--pylenium_json\", \"-p\", help=\"Overwrite pylenium.json\"),\n    overwrite_pytest_ini: bool = typer.Option(False, \"--pytest_ini\", \"-i\", help=\"Overwrite pytest.ini\"),\n):\n    \"\"\"Initializes Pylenium into the current directory.\n\n    By default, this creates (if they do not exist): conftest.py, pylenium.json, pytest.ini\n\n    If you want to overwrite these existing files, use the available Options.\n    \"\"\"\n    scripts_path = os.path.dirname(os.path.abspath(__file__))\n    user_cwd = os.getcwd()\n\n    conftest = f\"{scripts_path}/conftest.py\"\n    pylenium_json = f\"{scripts_path}/pylenium.json\"\n    pytest_ini = f\"{scripts_path}/pytest.ini\"\n\n    # initialize conftest.py\n    if os.path.exists(f\"{user_cwd}/conftest.py\"):\n        if overwrite_conftest:\n            _copy(file=conftest, to_dir=user_cwd, message=\"conftest.py was overwritten at:\")\n        else:\n            typer.secho(\"conftest.py already exists at this location\", fg=typer.colors.BRIGHT_YELLOW)\n            typer.secho(\"💡 Use -c flag if you want to replace it with the latest\\n\", fg=typer.colors.BRIGHT_CYAN)\n    else:\n        _copy(file=conftest, to_dir=user_cwd, message=\"conftest.py was created at:\")\n\n    # initialize pylenium.json\n    if os.path.exists(f\"{user_cwd}/pylenium.json\"):\n        if overwrite_pylenium_json:\n            _copy(file=pylenium_json, to_dir=user_cwd, message=\"pylenium.json was overwritten at:\")\n        else:\n            typer.secho(\"pylenium.json already exists at this location\", fg=typer.colors.BRIGHT_YELLOW)\n            typer.secho(\"💡 Use -p flag if you want to replace it with the latest defaults\\n\", fg=typer.colors.BRIGHT_CYAN)\n    else:\n        _copy(file=pylenium_json, to_dir=user_cwd, message=\"pylenium.json was created at:\")\n\n    # initialize pytest.ini\n    if os.path.exists(f\"{user_cwd}/pytest.ini\"):\n        if overwrite_pytest_ini:\n            _copy(file=pytest_ini, to_dir=user_cwd, message=\"pytest.ini was overwritten at:\")\n        else:\n            typer.secho(\"pytest.ini already exists at this location\", fg=typer.colors.BRIGHT_YELLOW)\n            typer.secho(\"💡 Use -i flag if you want to replace it with the latest defaults\\n\", fg=typer.colors.BRIGHT_CYAN)\n    else:\n        _copy(file=pytest_ini, to_dir=user_cwd, message=\"pytest.ini was created at:\")\n\n\nif __name__ == \"__main__\":\n    app()\n"
  },
  {
    "path": "pylenium/scripts/cli_utils.py",
    "content": "import subprocess\nfrom typing import List, Tuple, Union\n\n\ndef run_process(tokenized_command: Union[List[str], str], shell=False) -> subprocess.CompletedProcess:\n    \"\"\" Run a subprocess given the tokenized command.\n\n    Args:\n        tokenized_command: Tokenized list of strings that make up the command to run.\n        shell: If the arg is a single string, change this to True\n\n    Returns:\n        `CompletedProcess` with - args, returncode, stderr, and stdout\n\n    Raises:\n        Does not raise a `CalledProcessError`, so make sure to check the returncode for a non-zero value.\n    \"\"\"\n    response = subprocess.run(args=tokenized_command, stderr=subprocess.PIPE, stdout=subprocess.PIPE, shell=shell)\n    return response\n\n\ndef parse_response(response: subprocess.CompletedProcess) -> Tuple[str, str]:\n    output = str(response.stdout, \"utf-8\")\n    error = str(response.stderr, \"utf-8\")\n    return output, error\n"
  },
  {
    "path": "pylenium/scripts/conftest.py",
    "content": "\"\"\"\nThe `conftest.py` and `pylenium.json` files generated by Pylenium should stay at your Workspace Root (aka Project Root).\n\nconftest.py\n    Although this file is editable, you should only change its contents if you know what you are doing.\n    Instead, you can create your own conftest.py file in the folder where you store your tests.\n\npylenium.json\n    You can change the values, but DO NOT touch the keys or you will break the schema.\n\npy\n    The main fixture you need from this is `py`. This is the instance of Pylenium for each test.\n    Just pass py into your test and you're ready to go!\n\nExamples:\n    def test_go_to_google(py):\n        py.visit('https://google.com')\n        assert 'Google' in py.title()\n\"\"\"\n\nimport copy\nimport json\nimport logging\nimport shutil\nfrom pathlib import Path\n\nimport allure\nimport pytest\nimport requests\nfrom faker import Faker\n\nfrom pylenium.a11y import PyleniumAxe\nfrom pylenium.config import PyleniumConfig, TestCase\nfrom pylenium.driver import Pylenium\n\n\n@pytest.fixture(scope=\"function\")\ndef fake() -> Faker:\n    \"\"\"A basic instance of Faker to make test data.\"\"\"\n    return Faker()\n\n\n@pytest.fixture(scope=\"function\")\ndef api():\n    \"\"\"A basic instance of Requests to make HTTP API calls.\"\"\"\n    return requests\n\n\n@pytest.fixture(scope=\"session\", autouse=True)\ndef project_root() -> Path:\n    \"\"\"The Project (or Workspace) root as a filepath.\n\n    * This conftest.py file should be in the Project Root if not already.\n    \"\"\"\n    return Path(__file__).absolute().parent\n\n\n@pytest.fixture(scope=\"session\", autouse=True)\ndef test_results_dir(project_root: Path, request) -> Path:\n    \"\"\"Creates the `/test_results` directory to store the results of the Test Run.\n\n    Returns:\n        The `/test_results` directory as a filepath (str).\n    \"\"\"\n    session = request.node\n    test_results_dir = project_root.joinpath(\"test_results\")\n\n    if test_results_dir.exists():\n        # delete /test_results from previous Test Run\n        shutil.rmtree(test_results_dir, ignore_errors=True)\n\n    try:\n        # race condition can occur between checking file existence and\n        # creating the file when using pytest with multiple workers\n        test_results_dir.mkdir(parents=True, exist_ok=True)\n    except FileExistsError:\n        pass\n\n    for test in session.items:\n        try:\n            # make the test_result directory for each test\n            test_results_dir.joinpath(test.name).mkdir(parents=True, exist_ok=True)\n        except FileExistsError:\n            pass\n\n    return test_results_dir\n\n\n@pytest.fixture(scope=\"session\")\ndef _load_pylenium_json(project_root, request) -> PyleniumConfig:\n    \"\"\"Load the default pylenium.json file or the given pylenium.json config file (if specified).\n\n    * Pylenium looks for these files from the Project Root!\n\n    I may have multiple pylenium.json files with different presets. For example:\n    - stage-pylenium.json\n    - dev-testing.json\n    - firefox-pylenium.json\n\n    Examples\n    --------\n    $ pytest\n    >>> Loads the default file: PROJECT_ROOT/pylenium.json\n\n    $ pytest pylenium_json=dev-pylenium.json\n    >>> Loads the config file: PROJECT_ROOT/dev-pylenium.json\n\n    $ pytest pylenium_json=\"configs/stage-pylenium.json\"\n    >>> Loads the config file: PROJECT_ROOT/configs/stage-pylenium.json\n    \"\"\"\n    custom_config_filepath = request.config.getoption(\"pylenium_json\")\n    config_filepath = project_root.joinpath(custom_config_filepath or \"pylenium.json\")\n\n    try:\n        with config_filepath.open() as file:\n            _json = json.load(file)\n        config = PyleniumConfig(**_json)\n    except FileNotFoundError:\n        logging.warning(f\"The config_filepath was not found, so PyleniumConfig will load with default values. File not found: {config_filepath.absolute()}\")\n        config = PyleniumConfig()\n\n    return config\n\n\n@pytest.fixture(scope=\"session\")\ndef _override_pylenium_config_values(_load_pylenium_json: PyleniumConfig, request) -> PyleniumConfig:\n    \"\"\"Override any PyleniumConfig values after loading the initial pylenium.json config file.\n\n    After a pylenium.json config file is loaded and converted to a PyleniumConfig object,\n    then any CLI arguments override their respective key/values.\n    \"\"\"\n    config = _load_pylenium_json\n    # Driver Settings\n    cli_remote_url = request.config.getoption(\"--remote_url\")\n    if cli_remote_url:\n        config.driver.remote_url = cli_remote_url\n\n    cli_browser_options = request.config.getoption(\"--options\")\n    if cli_browser_options:\n        config.driver.options = [option.strip() for option in cli_browser_options.split(\",\")]\n\n    cli_browser = request.config.getoption(\"--browser\")\n    if cli_browser:\n        config.driver.browser = cli_browser\n\n    cli_local_path = request.config.getoption(\"--local_path\")\n    if cli_local_path:\n        config.driver.local_path = cli_local_path\n\n    cli_capabilities = request.config.getoption(\"--caps\")\n    if cli_capabilities:\n        # --caps must be in '{\"name\": \"value\", \"boolean\": true}' format\n        # with double quotes around each key. booleans are lowercase.\n        config.driver.capabilities = json.loads(cli_capabilities)\n\n    cli_page_wait_time = request.config.getoption(\"--page_load_wait_time\")\n    if cli_page_wait_time and cli_page_wait_time.isdigit():\n        config.driver.page_load_wait_time = int(cli_page_wait_time)\n\n    # Logging Settings\n    cli_screenshots_on = request.config.getoption(\"--screenshots_on\")\n    if cli_screenshots_on:\n        shots_on = cli_screenshots_on.lower() == \"true\"\n        config.logging.screenshots_on = shots_on\n\n    cli_extensions = request.config.getoption(\"--extensions\")\n    if cli_extensions:\n        config.driver.extension_paths = [ext.strip() for ext in cli_extensions.split(\",\")]\n\n    cli_log_level = request.config.getoption(\"--pylog_level\")\n    if cli_log_level:\n        level = cli_log_level.upper()\n        config.logging.pylog_level = level if level in [\"DEBUG\", \"COMMAND\", \"INFO\", \"USER\", \"WARNING\", \"ERROR\", \"CRITICAL\"] else \"INFO\"\n\n    return config\n\n\n@pytest.fixture(scope=\"function\")\ndef py_config(_override_pylenium_config_values) -> PyleniumConfig:\n    \"\"\"Get a fresh copy of the PyleniumConfig for each test\n\n    See _load_pylenium_json and _override_pylenium_config_values for how the initial configuration is read.\n    \"\"\"\n    return copy.deepcopy(_override_pylenium_config_values)\n\n\n@pytest.fixture(scope=\"class\")\ndef pyc_config(_override_pylenium_config_values) -> PyleniumConfig:\n    \"\"\"Get a fresh copy of the PyleniumConfig for each test class\"\"\"\n    return copy.deepcopy(_override_pylenium_config_values)\n\n\n@pytest.fixture(scope=\"session\")\ndef pys_config(_override_pylenium_config_values) -> PyleniumConfig:\n    \"\"\"Get a fresh copy of the PyleniumConfig for each test session\"\"\"\n    return copy.deepcopy(_override_pylenium_config_values)\n\n\n@pytest.fixture(scope=\"function\")\ndef test_case(test_results_dir: Path, request) -> TestCase:\n    \"\"\"Manages data pertaining to the currently running Test Function or Case.\n\n        * Creates the test-specific logger.\n\n    Args:\n        test_results_dir: The ./test_results directory this Test Run (aka Session) is writing to\n\n    Returns:\n        An instance of TestCase.\n    \"\"\"\n    test_name = request.node.name\n    test_result_path = test_results_dir.joinpath(test_name)\n    return TestCase(name=test_name, file_path=test_result_path)\n\n\n@pytest.fixture(scope=\"function\")\ndef py(test_case: TestCase, py_config: PyleniumConfig, request):\n    \"\"\"Initialize a Pylenium driver for each test.\n\n    Pass in this `py` fixture into the test function.\n\n    Examples:\n        def test_go_to_google(py):\n            py.visit('https://google.com')\n            assert 'Google' in py.title()\n    \"\"\"\n    py = Pylenium(py_config)\n    yield py\n    try:\n        if request.node.report.failed:\n            # if the test failed, execute code in this block\n            if py_config.logging.screenshots_on:\n                screenshot = py.screenshot(str(test_case.file_path.joinpath(\"test_failed.png\")))\n                allure.attach(screenshot, \"test_failed.png\", allure.attachment_type.PNG)\n\n        elif request.node.report.passed:\n            # if the test passed, execute code in this block\n            pass\n        else:\n            # if the test has another result (ie skipped, inconclusive), execute code in this block\n            pass\n    except Exception:\n        logging.error(\"Failed to take screenshot on test failure.\")\n    py.quit()\n\n\n@pytest.fixture(scope=\"class\")\ndef pyc(pyc_config: PyleniumConfig, request):\n    \"\"\"Initialize a Pylenium driver for an entire test class.\"\"\"\n    py = Pylenium(pyc_config)\n    yield py\n    try:\n        if request.node.report.failed:\n            # if the test failed, execute code in this block\n            if pyc_config.logging.screenshots_on:\n                allure.attach(py.webdriver.get_screenshot_as_png(), \"test_failed.png\", allure.attachment_type.PNG)\n        elif request.node.report.passed:\n            # if the test passed, execute code in this block\n            pass\n        else:\n            # if the test has another result (ie skipped, inconclusive), execute code in this block\n            pass\n    except Exception:\n        logging.error(\"Failed to take screenshot on test failure.\")\n    py.quit()\n\n\n@pytest.fixture(scope=\"session\")\ndef pys(pys_config: PyleniumConfig, request):\n    \"\"\"Initialize a Pylenium driver for an entire test session.\"\"\"\n    py = Pylenium(pys_config)\n    yield py\n    try:\n        if request.node.report.failed:\n            # if the test failed, execute code in this block\n            if pys_config.logging.screenshots_on:\n                allure.attach(py.webdriver.get_screenshot_as_png(), \"test_failed.png\", allure.attachment_type.PNG)\n        elif request.node.report.passed:\n            # if the test passed, execute code in this block\n            pass\n        else:\n            # if the test has another result (ie skipped, inconclusive), execute code in this block\n            pass\n    except Exception:\n        logging.error(\"Failed to take screenshot on test failure.\")\n    py.quit()\n\n\n@pytest.fixture(scope=\"function\")\ndef axe(py) -> PyleniumAxe:\n    \"\"\"The aXe A11y audit tool as a fixture.\"\"\"\n    return PyleniumAxe(py.webdriver)\n\n\n@pytest.hookimpl(tryfirst=True, hookwrapper=True)\ndef pytest_runtest_makereport(item, call):\n    \"\"\"Yield each test's outcome so we can handle it in other fixtures.\"\"\"\n    outcome = yield\n    report = outcome.get_result()\n    if report.when == \"call\":\n        setattr(item, \"report\", report)\n    return report\n\n\ndef pytest_addoption(parser):\n    parser.addoption(\"--browser\", action=\"store\", default=\"\", help=\"The lowercase browser name: chrome | firefox\")\n    parser.addoption(\"--local_path\", action=\"store\", default=\"\", help=\"The filepath to the local driver\")\n    parser.addoption(\"--remote_url\", action=\"store\", default=\"\", help=\"Grid URL to connect tests to.\")\n    parser.addoption(\"--screenshots_on\", action=\"store\", default=\"\", help=\"Should screenshots be saved? true | false\")\n    parser.addoption(\n        \"--pylenium_json\",\n        action=\"store\",\n        default=\"\",\n        help=\"The filepath of the pylenium.json file to use (ie dev-pylenium.json)\",\n    )\n    parser.addoption(\n        \"--pylog_level\", action=\"store\", default=\"INFO\", help=\"Set the logging level: 'DEBUG' | 'COMMAND' | 'INFO' | 'USER' | 'WARNING' | 'ERROR' | 'CRITICAL'\"\n    )\n    parser.addoption(\n        \"--options\",\n        action=\"store\",\n        default=\"\",\n        help='Comma-separated list of Browser Options. Ex. \"headless, incognito\"',\n    )\n    parser.addoption(\n        \"--caps\",\n        action=\"store\",\n        default=\"\",\n        help='List of key-value pairs. Ex. \\'{\"name\": \"value\", \"boolean\": true}\\'',\n    )\n    parser.addoption(\n        \"--page_load_wait_time\",\n        action=\"store\",\n        default=\"\",\n        help=\"The amount of time to wait for a page load before raising an error. Default is 0.\",\n    )\n    parser.addoption(\"--extensions\", action=\"store\", default=\"\", help='Comma-separated list of extension paths. Ex. \"*.crx, *.crx\"')\n"
  },
  {
    "path": "pylenium/scripts/drag_and_drop.js",
    "content": "(function( $ ) {\n        $.fn.simulateDragDrop = function(options) {\n                return this.each(function() {\n                        new $.simulateDragDrop(this, options);\n                });\n        };\n        $.simulateDragDrop = function(elem, options) {\n                this.options = options;\n                this.simulateEvent(elem, options);\n        };\n        $.extend($.simulateDragDrop.prototype, {\n                simulateEvent: function(elem, options) {\n                        /*Simulating drag start*/\n                        var type = 'dragstart';\n                        var event = this.createEvent(type);\n                        this.dispatchEvent(elem, type, event);\n\n                        /*Simulating drop*/\n                        type = 'drop';\n                        var dropEvent = this.createEvent(type, {});\n                        dropEvent.dataTransfer = event.dataTransfer;\n                        this.dispatchEvent($(options.dropTarget)[0], type, dropEvent);\n\n                        /*Simulating drag end*/\n                        type = 'dragend';\n                        var dragEndEvent = this.createEvent(type, {});\n                        dragEndEvent.dataTransfer = event.dataTransfer;\n                        this.dispatchEvent(elem, type, dragEndEvent);\n                },\n                createEvent: function(type) {\n                        var event = document.createEvent(\"CustomEvent\");\n                        event.initCustomEvent(type, true, true, null);\n                        event.dataTransfer = {\n                                data: {\n                                },\n                                setData: function(type, val){\n                                        this.data[type] = val;\n                                },\n                                getData: function(type){\n                                        return this.data[type];\n                                }\n                        };\n                        return event;\n                },\n                dispatchEvent: function(elem, type, event) {\n                        if(elem.dispatchEvent) {\n                                elem.dispatchEvent(event);\n                        }else if( elem.fireEvent ) {\n                                elem.fireEvent(\"on\"+type, event);\n                        }\n                }\n        });\n})(jQuery);"
  },
  {
    "path": "pylenium/scripts/load_jquery.js",
    "content": "(function(jqueryUrl, iframe, callback) {\n    if (typeof jqueryUrl != 'string') {\n        jqueryUrl = 'https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js';\n    }\n\n    function insertJquery(doc) {\n        if (typeof jQuery == 'undefined') {\n            let script = doc.createElement('script');\n            let head = doc.getElementsByTagName('head')[0];\n            let done = false;\n            script.onload = script.onreadystatechange = (function() {\n                if (!done && (!this.readyState || this.readyState === 'loaded'\n                        || this.readyState === 'complete')) {\n                    done = true;\n                    script.onload = script.onreadystatechange = null;\n                    head.removeChild(script);\n                    callback();\n                }\n            });\n            script.src = jqueryUrl;\n            head.appendChild(script);\n        }\n        else {\n            callback();\n        }\n    }\n\n    if (iframe != null) {\n        let context = iframe.contentDocument;\n        insertJquery(context);\n    }\n    else {\n        insertJquery(document)\n    }\n})(arguments[0], arguments[1], arguments[arguments.length - 1]);\n"
  },
  {
    "path": "pylenium/scripts/pylenium.json",
    "content": "{\n  \"driver\": {\n    \"browser\": \"chrome\",\n    \"remote_url\": \"\",\n    \"wait_time\": 10,\n    \"page_load_wait_time\": 0,\n    \"options\": [],\n    \"capabilities\": {},\n    \"experimental_options\": null,\n    \"extension_paths\": [],\n    \"webdriver_kwargs\": {},\n    \"local_path\": \"\"\n  },\n  \"logging\": {\n    \"screenshots_on\": true\n  },\n  \"viewport\": {\n    \"maximize\": true,\n    \"width\": 1440,\n    \"height\": 900,\n    \"orientation\": \"portrait\"\n  },\n\n  \"customer\": {}\n}\n"
  },
  {
    "path": "pylenium/scripts/pytest.ini",
    "content": "[pytest]\n; Configuring pytest\n; More info: https://docs.pytest.org/en/6.2.x/customize.html\n\n;Logging\n; DATE FORMAT EXAMPLE: %Y-%m-%d %H:%M:%S\nlog_cli_format = %(asctime)s %(levelname)-8s %(name)-8s %(message)s\nlog_cli_level = COMMAND\nlog_cli_date_format = %H:%M:%S\n"
  },
  {
    "path": "pylenium/switch_to.py",
    "content": "from selenium.common.exceptions import NoSuchFrameException\nfrom selenium.webdriver.support import expected_conditions as ec\n\nfrom pylenium.element import Element\nfrom pylenium.log import logger as log\n\n\nclass FrameIsAvailable:\n    \"\"\"Expected Condition since the current one from Selenium doesn't work for strings, only tuples.\"\"\"\n\n    def __init__(self, frame_name_or_id):\n        self.frame_name_or_id = frame_name_or_id\n\n    def __call__(self, driver):\n        try:\n            driver.switch_to.frame(self.frame_name_or_id)\n            return True\n        except NoSuchFrameException:\n            return False\n\n\nclass SwitchTo:\n    def __init__(self, pylenium):\n        self._py = pylenium\n\n    def frame(self, name_or_id: str, timeout: int = 0):\n        \"\"\"Switch the driver's context to a frame given the name or id of the element.\n\n        Args:\n            name_or_id: The frame's `id` or `name` attribute value\n            timeout: The number of seconds to wait for the frame to be switched to.\n\n        Examples:\n        ```\n            # Switch to an iframe\n            py.switch_to.frame(\"main-frame\")\n        ```\n\n        Returns:\n            The current instance of Pylenium\n        \"\"\"\n        log.debug(\"py.switch_to.frame() - Switch to frame using name or id: `%s`\", name_or_id)\n        self._py.wait(timeout).until(FrameIsAvailable(name_or_id))\n        return self._py\n\n    def frame_by_element(self, element: Element, timeout: int = 0):\n        \"\"\"Switch the driver's context to the given frame element.\n\n        Args:\n            element (Element): The frame element to switch to\n            timeout: The number of seconds to wait for the frame to be switched to.\n\n        Examples:\n        ```\n            iframe = py.get(\"iframe\")\n            py.switch_to.frame_by_element(iframe)\n        ```\n\n        Returns:\n            The current instance of Pylenium\n        \"\"\"\n        log.command(\"py.switch_to.frame_by_element() - Switch to frame using an Element\")\n        self._py.wait(timeout).until(ec.frame_to_be_available_and_switch_to_it(element.locator))\n        return self._py\n\n    def parent_frame(self):\n        \"\"\"Switch the driver's context to the parent frame.\n\n        * If the parent frame is the current context, nothing happens.\n\n        Returns:\n            The current instance of Pylenium\n        \"\"\"\n        log.command(\"py.switch_to.parent_frame() - Switch to the parent frame\")\n        self._py.webdriver.switch_to.parent_frame()\n        return self._py\n\n    def default_content(self):\n        \"\"\"Switch the driver's context to the default content.\n\n        * If the default_content is the current context, nothing happens.\n\n        Returns:\n            The current instance of Pylenium\n        \"\"\"\n        log.command(\"py.switch_to.default_content() - Switch to default content of this browser session\")\n        self._py.webdriver.switch_to.default_content()\n        return self._py\n\n    def new_window(self):\n        \"\"\"Open a new Browser Window and switch the driver's context (aka focus) to it.\n\n        Returns:\n            The current instance of Pylenium with the new window focused\n        \"\"\"\n        log.command(\"py.new_window() - Open a new browser window\")\n        self._py.webdriver.switch_to.new_window(\"window\")\n        return self._py\n\n    def new_tab(self):\n        \"\"\"Open a new Browser Tab and switch the driver's context (aka focus) to it.\n\n        Returns:\n            The current instance of Pylenium with the new tab focused\n        \"\"\"\n        log.command(\"py.new_tab() - Open a new browser tab\")\n        self._py.webdriver.switch_to.new_window(\"tab\")\n        return self._py\n\n    def window(self, name_or_handle=\"\", index=0):\n        \"\"\"Switch the driver's context (aka focus) to the existing Browser Window or Browser Tab.\n\n        Args:\n            name_or_handle: The name or window handle of the Window or Tab to switch to.\n            index: The index position of the Window Handle.\n\n        * `index=0` would be the main, default content.\n\n        Examples:\n        ```\n            # Switch to a Window by handle\n            windows = py.window_handles\n            py.switch_to.window(name_or_handle=windows[1])\n\n            # Switch to a newly opened Browser Tab by index\n            py.switch_to.window(index=1)\n        ```\n\n        Returns:\n            The current instance of Pylenium with the new window or tab focused\n        \"\"\"\n        if index:\n            handle = self._py.webdriver.window_handles[index]\n            log.command(\"py.switch_to.window() - Switch to a Tab or Window by index: %s\", index)\n            self._py.webdriver.switch_to.window(handle)\n            return self._py\n        if name_or_handle:\n            log.command(\"py.switch_to.window() - Switch to Tab or Window by name or handle: `%s`\", name_or_handle)\n            self._py.webdriver.switch_to.window(name_or_handle)\n            return self._py\n        # context unchanged\n        return self._py\n"
  },
  {
    "path": "pylenium/utils.py",
    "content": "import pathlib\n\n\ndef read_script_from_file(file_name) -> str:\n    \"\"\" Get the script string from a file in the scripts directory.\n\n    Args:\n        file_name: The file name with extension to read from.\n\n    Examples:\n        script = read_script_from_file('drag_and_drop.js')\n    \"\"\"\n    path = str(pathlib.Path(__file__).parent.absolute())\n    with open(path + f'/scripts/{file_name}', 'r', encoding='utf-8') as file:\n        script = file.read()\n    return script\n"
  },
  {
    "path": "pylenium/wait.py",
    "content": "import time\nfrom typing import Tuple, Optional, Union\n\nfrom selenium.webdriver.remote.webelement import WebElement\nfrom selenium.webdriver.support.wait import WebDriverWait\n\nfrom pylenium.element import Element, Elements\n\n\nclass PyleniumWait:\n    \"\"\"The Pylenium version of Wait that returns Element and Elements objects.\"\"\"\n\n    def __init__(self, py, webdriver, timeout, ignored_exceptions: Optional[Tuple] = None):\n        self._py = py\n        self._webdriver = webdriver\n        self._wait = WebDriverWait(driver=webdriver, timeout=timeout, ignored_exceptions=ignored_exceptions)\n\n    def sleep(self, seconds: int):\n        \"\"\"The test will sleep for the given number of seconds.\n\n        Args:\n            seconds: The number of seconds to sleep for.\n\n        This is the same as using time.sleep()\n        \"\"\"\n        time.sleep(seconds)\n\n    def until(self, method, message=\"\"):\n        \"\"\"Wait until the method returns a non-False value.\n\n        * The method uses Selenium WebDriver\n\n        Args:\n            method: The condition to wait for\n            message: The message to show if the condition is not met in the time frame.\n\n        Returns:\n            If the value is a webelement, return a Pylenium Element object\n            If the value is a list of WebElement, return a Pylenium Elements object\n            Else return the non-False value\n\n        Examples:\n            # return an Element\n            py.wait().until(lambda x: x.find_element(By.ID, 'foo'), 'element \"foo\" was not found')\n            # return Elements\n            py.wait().until(lambda x: x.find_elements(By.XPATH, '//a'))\n            # return True\n            py.wait(5).until(lambda x: x.title  == 'QA at the Point')\n        \"\"\"\n        value = self._wait.until(method, message)\n        if isinstance(value, WebElement):\n            return Element(self._py, value, None)\n        if isinstance(value, list):\n            try:\n                return Elements(self._py, value, None)\n            except Exception:\n                pass  # not a list of WebElement\n        return value\n\n    def until_not(self, method, message=\"\"):\n        \"\"\"Wait until the method returns a False value.\n\n        * The method uses Selenium WebDriver\n\n        Args:\n            method: The condition to wait for\n            message: The message to show if the condition is not met in the time frame.\n\n        Returns:\n            If the value is a webelement, return a Pylenium Element object\n            If the value is a list of WebElement, return a Pylenium Elements object\n            Else return the non-False value\n\n        Examples:\n            # wait until the title is not 'Home Page'\n            py.wait().until_not(lambda x: x.title == 'Home Page' )\n        \"\"\"\n        value = self._wait.until_not(method, message)\n        if isinstance(value, WebElement):\n            return Element(self._py, value, None)\n        if isinstance(value, list):\n            try:\n                return Elements(self._py, value, None)\n            except Exception:\n                pass  # not a list of WebElement\n        return value\n\n    def build(\n        self, timeout: int, use_py=False, ignored_exceptions: list = None\n    ) -> Union[WebDriverWait, \"PyleniumWait\"]:\n        \"\"\"Builds a WebDriverWait or PyleniumWait.\n\n        Args:\n            timeout: The number of seconds to wait for the condition to be True\n            use_py: True if you want a PyleniumWait. False for a default WebDriverWait\n            ignored_exceptions: List of exceptions to ignore in the Wait\n\n        Returns:\n            New instance of WebDriverWait if use_pylenium=False, else returns PyleniumWait\n        \"\"\"\n        if use_py:\n            return PyleniumWait(self._py, self._webdriver, timeout, ignored_exceptions)\n        return WebDriverWait(self._webdriver, timeout, ignored_exceptions=ignored_exceptions)\n"
  },
  {
    "path": "pylenium/webdriver_factory.py",
    "content": "\"\"\" Factory to build WebDrivers, leveraging Selenium Manager.\n\nThe Pylenium class asks for a PyleniumConfig object to build a WebDriver,\nso the `build_from_config` method is the \"main\" method in this module.\n\"\"\"\n\nfrom typing import Dict, List, Optional\n\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.service import Service as ChromeService\nfrom selenium.webdriver.edge.options import Options as EdgeOptions\nfrom selenium.webdriver.edge.service import Service as EdgeService\nfrom selenium.webdriver.firefox.service import Service as FirefoxService\nfrom selenium.webdriver.safari.service import Service as SafariService\nfrom selenium.webdriver.remote.webdriver import WebDriver\n\nfrom pylenium.config import PyleniumConfig\n\n\nclass Browser:\n    \"\"\"ENUM of supported browsers.\"\"\"\n\n    CHROME = \"chrome\"\n    EDGE = \"edge\"\n    SAFARI = \"safari\"\n    FIREFOX = \"firefox\"\n    IE = \"ie\"\n\n\ndef build_capabilities(browser: str, capabilities: Optional[Dict]) -> Dict:\n    \"\"\"Build the capabilities dictionary for the given browser.\n\n    Some WebDrivers pass in capabilities directly, but others (ie Chrome) require it be added via the Options object.\n\n    Args:\n        browser: The name of the browser.\n        capabilities: The dict of capabilities to include. If None, default caps are used.\n\n    Usage:\n        caps = build_capabilities(\"chrome\", {\"enableVNC\": True, \"enableVideo\": False})\n    \"\"\"\n    caps = {}\n\n    if browser == Browser.CHROME:\n        caps.update(webdriver.DesiredCapabilities.CHROME.copy())\n    elif browser == Browser.EDGE:\n        caps.update(webdriver.DesiredCapabilities.EDGE.copy())\n    elif browser == Browser.SAFARI:\n        caps.update(webdriver.DesiredCapabilities.SAFARI.copy())\n    elif browser == Browser.FIREFOX:\n        caps.update(webdriver.DesiredCapabilities.FIREFOX.copy())\n    elif browser == Browser.IE:\n        caps.update(webdriver.DesiredCapabilities.INTERNETEXPLORER.copy())\n    else:\n        raise ValueError(f\"{browser} is not supported. Cannot build capabilities.\")\n\n    if capabilities:\n        caps.update(capabilities)\n\n    return caps\n\n\ndef build_options(\n    browser: str,\n    browser_options: Optional[List[str]],\n    experimental_options: Optional[List[Dict]],\n    extension_paths: Optional[List[str]],\n):\n    \"\"\"Build the Options object for the given browser.\n\n    Args:\n        browser: The name of the browser.\n        browser_options: The list of options/arguments to include.\n        experimental_options: The list of experimental options to include.\n        extension_paths: The list of extension filepaths to add to the browser session.\n\n    Usage:\n        options = build_options(\"chrome\", [\"headless\", \"incognito\"], [{\"useAutomationExtension\", False}])\n    \"\"\"\n    browser = browser.lower()\n    if browser == Browser.CHROME:\n        options = webdriver.ChromeOptions()\n    elif browser == Browser.EDGE:\n        options = EdgeOptions()\n    elif browser == Browser.SAFARI:\n        options = webdriver.SafariOptions()\n    elif browser == Browser.FIREFOX:\n        options = webdriver.FirefoxOptions()\n    elif browser == Browser.IE:\n        options = webdriver.IeOptions()\n    else:\n        raise ValueError(f\"{browser} is not supported. Cannot build options.\")\n\n    for option in browser_options:\n        if option.startswith(\"--\"):\n            options.add_argument(option)\n        else:\n            options.add_argument(f\"--{option}\")\n\n    if experimental_options:\n        for exp_option in experimental_options:\n            ((name, value),) = exp_option.items()\n            options.add_experimental_option(name, value)\n\n    if extension_paths:\n        for path in extension_paths:\n            options.add_extension(path)\n\n    return options\n\n\ndef build_from_config(config: PyleniumConfig) -> WebDriver:\n    \"\"\"The \"main\" method for building a WebDriver using PyleniumConfig.\n\n    Args:\n        config: PyleniumConfig is built using pylenium.json and CLI args.\n\n    Usage:\n        driver = webdriver_factory.build_from_config(config)\n\n    Returns:\n        An instance of WebDriver.\n    \"\"\"\n    browser = config.driver.browser.lower()\n    remote_url = config.driver.remote_url\n    _config = {\n        \"options\": config.driver.options,\n        \"capabilities\": config.driver.capabilities,\n        \"experimental_options\": config.driver.experimental_options,\n        \"extension_paths\": config.driver.extension_paths,\n        \"webdriver_kwargs\": config.driver.webdriver_kwargs,\n    }\n\n    if remote_url:\n        # version is passed in as {\"browserVersion\": version} in capabilities\n        return build_remote(browser, remote_url, **_config)\n\n    # Set fields for the rest of the non-remote drivers\n    _config[\"local_path\"] = config.driver.local_path\n\n    # Build the WebDriver based on the browser\n    if browser == Browser.CHROME:\n        return build_chrome(**_config)\n    if browser == Browser.EDGE:\n        return build_edge(**_config)\n    if browser == Browser.SAFARI:\n        return build_safari(**_config)\n    if browser == Browser.FIREFOX:\n        return build_firefox(**_config)\n    if browser == Browser.IE:\n        return build_ie(**_config)\n    raise ValueError(f\"{config.driver.browser} is not supported. Cannot build WebDriver from config.\")\n\n\ndef build_chrome(\n    options: Optional[List[str]],\n    capabilities: Optional[Dict],\n    experimental_options: Optional[List[Dict]],\n    extension_paths: Optional[List[str]],\n    local_path: Optional[str],\n    webdriver_kwargs: Optional[Dict],\n):\n    \"\"\"Build a Chrome WebDriver.\n\n    Args:\n        options: The list of options/arguments to include.\n        capabilities: The dict of capabilities to include.\n        experimental_options: The list of experimental options to include.\n        extension_paths: The list of extension filepaths to add to the browser.\n        local_path: The path to the driver binary (only to bypass WebDriverManager)\n        webdriver_kwargs: additional keyword arguments to pass.\n\n    Usage:\n        driver = webdriver_factory.build_chrome(\"latest\", [\"headless\", \"incognito\"])\n    \"\"\"\n    browser_options = build_options(Browser.CHROME, options, experimental_options, extension_paths)\n    caps = build_capabilities(Browser.CHROME, capabilities)\n    for cap in caps:\n        browser_options.set_capability(cap, caps[cap])\n\n    driver = webdriver.Chrome(\n        options=browser_options,\n        service=ChromeService(local_path or None),\n        **(webdriver_kwargs or {}),\n    )\n\n    # enable Performance Metrics from Chrome Dev Tools\n    driver.execute_cdp_cmd(\"Performance.enable\", {})\n    return driver\n\n\ndef build_edge(\n    options: Optional[List[str]],\n    capabilities: Optional[Dict],\n    experimental_options: Optional[List[Dict]],\n    extension_paths: Optional[List[str]],\n    local_path: Optional[str],\n    webdriver_kwargs: Optional[Dict],\n) -> WebDriver:\n    \"\"\"Build an Edge WebDriver.\n\n    Args:\n        options: The list of options/arguments to include.\n        capabilities: The dict of capabilities to include.\n        experimental_options: The list of experimental options to include.\n        extension_paths: The list of extensions to add to the browser.\n        local_path: The path to the driver binary.\n        webdriver_kwargs: additional keyword arguments to pass.\n\n    Usage:\n        driver = webdriver_factory.build_edge(\"latest\", [\"headless\", \"incognito\"])\n    \"\"\"\n    caps = build_capabilities(Browser.EDGE, capabilities)\n    browser_options = build_options(Browser.EDGE, options, experimental_options, extension_paths)\n    for cap in caps:\n        browser_options.set_capability(cap, caps[cap])\n\n    return webdriver.Edge(\n        service=EdgeService(local_path or None),\n        options=browser_options,\n        **(webdriver_kwargs or {}),\n    )\n\n\ndef build_safari(\n    options: Optional[List[str]],\n    capabilities: Optional[Dict],\n    experimental_options: Optional[List[Dict]],\n    extension_paths: Optional[List[str]],\n    local_path: Optional[str],\n    webdriver_kwargs: Optional[Dict],\n) -> WebDriver:\n    \"\"\"Build a Safari WebDriver.\n\n    Args:\n        options: The list of options/arguments to include.\n        capabilities: The dict of capabilities to include.\n        experimental_options: The list of experimental options to include.\n        extension_paths: The list of extensions to add to the browser.\n        local_path: The path to the driver binary.\n        webdriver_kwargs: additional keyword arguments to pass.\n\n    Usage:\n        driver = webdriver_factory.build_safari(\"latest\", [\"headless\"])\n    \"\"\"\n    browser_options = build_options(Browser.SAFARI, options, experimental_options, extension_paths)\n    caps = build_capabilities(Browser.SAFARI, capabilities)\n    for cap in caps:\n        browser_options.set_capability(cap, caps[cap])\n\n    return webdriver.Safari(\n        options=options,\n        service=SafariService(local_path or None),\n        **(webdriver_kwargs or {}),\n    )\n\n\ndef build_firefox(\n    options: Optional[List[str]],\n    capabilities: Optional[Dict],\n    experimental_options: Optional[List[Dict]],\n    extension_paths: Optional[List[str]],\n    local_path: Optional[str],\n    webdriver_kwargs: Optional[Dict],\n):\n    \"\"\"Build a Firefox WebDriver.\n\n    Args:\n        options: The list of options/arguments to include.\n        capabilities: The dict of capabilities to include.\n        experimental_options: The list of experimental options to include.\n        extension_paths: The list of extensions to add to the browser.\n        local_path: The path to the driver binary.\n        webdriver_kwargs: additional keyword arguments to pass.\n\n    Usage:\n        driver = webdriver_factory.build_firefox(\"latest\", [\"headless\", \"incognito\"])\n    \"\"\"\n    caps = build_capabilities(Browser.FIREFOX, capabilities)\n    browser_options = build_options(Browser.FIREFOX, options, experimental_options, extension_paths)\n    for cap in caps:\n        browser_options.set_capability(cap, caps[cap])\n\n    return webdriver.Firefox(\n        options=browser_options,\n        service=FirefoxService(local_path or None),\n        **(webdriver_kwargs or {}),\n    )\n\n\ndef build_ie(\n    options: Optional[List[str]],\n    capabilities: Optional[Dict],\n    experimental_options: Optional[List[Dict]],\n    extension_paths: Optional[List[str]],\n    local_path: Optional[str],\n    webdriver_kwargs: Optional[Dict],\n) -> WebDriver:\n    \"\"\"Build an Internet Explorer WebDriver.\n\n    Args:\n        options: The list of options/arguments to include.\n        capabilities: The dict of capabilities.\n        experimental_options: The list of experimental options to include.\n        extension_paths: The list of extensions to add to the browser.\n        local_path: The path to the driver binary.\n        webdriver_kwargs: additional keyword arguments to pass.\n\n    Usage:\n        driver = webdriver_factory.build_ie(\"latest\", [\"headless\"])\n    \"\"\"\n    caps = build_capabilities(Browser.IE, capabilities)\n    browser_options = build_options(Browser.IE, options, experimental_options, extension_paths)\n    return webdriver.Ie(\n        executable_path=local_path or None,\n        options=browser_options,\n        capabilities=caps,\n        **(webdriver_kwargs or {}),\n    )\n\n\ndef build_remote(\n    browser: str,\n    remote_url: str,\n    options: Optional[List[str]],\n    capabilities: Optional[Dict],\n    experimental_options: Optional[List[Dict]],\n    extension_paths: Optional[List[str]],\n    webdriver_kwargs: Optional[Dict],\n):\n    \"\"\"Build a RemoteDriver connected to a Grid.\n\n    Args:\n        browser: Name of the browser to connect to.\n        remote_url: The URL to connect to the Grid.\n        browser_options: The list of options/arguments to include.\n        capabilities: The dict of capabilities to include.\n        experimental_options: The list of experimental options to include.\n        extension_paths: The list of extensions to add to the browser.\n        webdriver_kwargs: additional keyword arguments to pass.\n\n    Usage:\n        driver = webdriver_factory.build_remote(\"chrome\", \"http://localhost:4444/wd/hub\", [\"headless\"])\n\n    Returns:\n        The instance of WebDriver once the connection is successful\n    \"\"\"\n    caps = build_capabilities(browser, capabilities)\n    browser_options = build_options(browser, options, experimental_options, extension_paths)\n    for cap in caps:\n        browser_options.set_capability(cap, caps[cap])\n\n    return webdriver.Remote(\n        command_executor=remote_url,\n        options=browser_options,\n        **(webdriver_kwargs or {}),\n    )\n"
  },
  {
    "path": "pylenium.json",
    "content": "{\n  \"driver\": {\n    \"browser\": \"chrome\",\n    \"remote_url\": \"\",\n    \"wait_time\": 10,\n    \"page_load_wait_time\": 0,\n    \"options\": [],\n    \"capabilities\": {},\n    \"experimental_options\": null,\n    \"extension_paths\": [],\n    \"webdriver_kwargs\": {},\n    \"local_path\": \"\"\n  },\n  \"logging\": {\n    \"screenshots_on\": true\n  },\n  \"viewport\": {\n    \"maximize\": true,\n    \"width\": 1440,\n    \"height\": 900,\n    \"orientation\": \"portrait\"\n  },\n\n  \"custom\": {}\n}\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[tool.poetry]\nname = \"pyleniumio\"\nversion = \"1.21.0\"\ndescription = \"The best of Selenium and Cypress in a single Python Package\"\nauthors = [\"Carlos <carlos@qap.dev>\"]\nlicense = \"MIT\"\nreadme = \"README.md\"\npackages = [{ include = \"pylenium\" }]\n\n[tool.poetry.scripts]\npylenium = \"pylenium.scripts.cli:app\"\n\n[tool.poetry.dependencies]\npython = \">=3.8.1\"\nrequests = \"^2.31.0\"\npytest-xdist = \"^3.5.0\"\naxe-selenium-python = \"^2.1.6\"\nallure-pytest = \"^2.13.2\"\ntyper = { version = \"^0.9.0\", extras = [\"all\"] }\nselenium = \"^4.17.2\"\npydantic = \"^2.6.1\"\nfaker = \"^23.1.0\"\npytest = \"^8.0.0\"\n\n[tool.poetry.dev-dependencies]\nblack = \"^24.1.1\"\npytest-cov = \"4.1.0\"\nflake8 = \"^7.0.0\"\npoethepoet = \"^0.24.4\"\n\n[tool.black]\nline-length = 160\n\n[tool.poe.tasks]\nlint = { \"cmd\" = \"flake8 pylenium tests\", \"help\" = \"Run Flake8 Linter\" }\ntest-unit = { \"cmd\" = \"pytest tests/unit --cov=. --cov-report term-missing -n 4\", \"help\" = \"Run Unit Tests and get Code Coverage Report\" }\ntest-ui = { \"cmd\" = \"pytest tests/ui --cov=. --cov-report term-missing -n 2\", \"help\" = \"Run UI Tests\" }\n\n[build-system]\nrequires = [\"poetry-core>=1.0.0\"]\nbuild-backend = \"poetry.core.masonry.api\"\n"
  },
  {
    "path": "pytest.ini",
    "content": "[pytest]\n; Configuring pytest\n; More info: https://docs.pytest.org/en/6.2.x/customize.html\ntestpaths =\n    tests\n\n;Logging\n; DATE FORMAT EXAMPLE: %Y-%m-%d %H:%M:%S\nlog_cli_format = %(asctime)s %(levelname)-8s %(name)-8s %(message)s\nlog_cli_level = COMMAND\nlog_cli_date_format = %H:%M:%S\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/performance/test_cdp_performance.py",
    "content": "\"\"\" Chrome DevTools Protocol - Performance Tab \"\"\"\nfrom pylenium.driver import Pylenium\n\n\ndef test_capture_performance_metrics(py: Pylenium):\n    py.visit(\"https://qap.dev\")\n    metrics = py.cdp.get_performance_metrics()\n    assert metrics[\"metrics\"]\n    assert metrics[\"metrics\"][0][\"name\"] == \"Timestamp\"\n    assert metrics[\"metrics\"][0][\"value\"] > 0\n"
  },
  {
    "path": "tests/performance/test_performance.py",
    "content": "import pytest\n\nfrom pylenium.driver import Pylenium\n\n\n@pytest.fixture\ndef qap_dev(py) -> Pylenium:\n    py.visit('https://qap.dev')\n    py.getx('//*[contains(@class, \"Footer\") and text()=\"Present at QAP\"]').should().be_visible()\n    return py\n\n\ndef test_custom_perf_metrics(qap_dev):\n    \"\"\" Check Pylenium's native web performance metrics.\n\n    These data points are not part of W3C's Performance APIs and are our own calculations.\n    \"\"\"\n    perf = qap_dev.performance.get()\n    assert perf.page_load_time()\n    assert perf.time_to_first_byte()\n    assert perf.time_to_first_contentful_paint()\n    assert perf.time_to_interactive()\n    assert perf.number_of_requests()\n    assert perf.time_to_dom_content_loaded()\n    assert perf.page_weight()\n    assert perf.connection_time()\n    assert perf.request_time()\n    assert perf.fetch_time()\n\n\ndef test_get_timing_objects(qap_dev):\n    \"\"\" Check retrievals and schemas. \"\"\"\n    assert qap_dev.performance.get_navigation_timing()\n    assert qap_dev.performance.get_paint_timing()\n    assert qap_dev.performance.get_resources()\n    assert qap_dev.performance.get_time_origin()\n"
  },
  {
    "path": "tests/test_flows.py",
    "content": "import pytest\nfrom pylenium.driver import Pylenium\n\n\n@pytest.fixture(scope=\"session\")\ndef sauce(pys: Pylenium) -> Pylenium:\n    \"\"\"Login to saucedemo.com as standard user.\"\"\"\n    pys.visit(\"https://www.saucedemo.com/\")\n    pys.get(\"#user-name\").type(\"standard_user\")\n    pys.get(\"#password\").type(\"secret_sauce\")\n    pys.get(\"#login-button\").click()\n    yield pys\n    pys.get(\"#react-burger-menu-btn\").click()\n    pys.get(\"#logout_sidebar_link\").should().be_visible().click()\n\n\nclass TestSauceDemo:\n    def test_add_to_cart_css(self, sauce: Pylenium):\n        \"\"\"Add an item to the cart. The number badge on the cart icon should increment as expected.\"\"\"\n        sauce.get(\"[id*='add-to-cart']\").click()\n        assert sauce.get(\"a.shopping_cart_link\").should().have_text(\"1\")\n\n    def test_add_to_cart_xpath(self, sauce: Pylenium):\n        \"\"\"Add 6 different items to the cart. There should be 6 items in the cart.\"\"\"\n        for button in sauce.findx(\"//*[contains(@id, 'add-to-cart')]\"):\n            button.click()\n        sauce.getx(\"//a[@class='shopping_cart_link']\").click()\n        assert sauce.findx(\"//*[@class='cart_item']\").should().have_length(6)\n"
  },
  {
    "path": "tests/ui/test_element.py",
    "content": "import pytest\nfrom pylenium.driver import Pylenium\n\n\nTHE_INTERNET = \"https://the-internet.herokuapp.com\"\nDEMO_QA = \"https://demoqa.com\"\n\n\ndef test_element_with_no_siblings(py: Pylenium):\n    py.visit(f\"{THE_INTERNET}/dropdown\")\n    elements = py.get(\"#page-footer > div\").siblings()\n    assert elements.should().be_empty()\n\n\ndef test_element_parent_and_siblings(py: Pylenium):\n    py.visit(f\"{DEMO_QA}/menu\")\n    parent = py.contains(\"Main Item 1\").parent()\n    assert parent.tag_name() == \"li\"\n    assert parent.siblings().should().have_length(2)\n\n\ndef test_element_text(py: Pylenium):\n    py.visit(f\"{DEMO_QA}/text-box\")\n    assert py.get(\"#userName-label\").should().have_text(\"Full Name\")\n\n\ndef test_find_in_element_context(py: Pylenium):\n    py.visit(f\"{DEMO_QA}/menu\")\n    menu_2 = py.contains(\"Main Item 2\")\n    items = menu_2.parent().find(\"li\")\n    assert items.should().have_length(5)\n\n\ndef test_children(py: Pylenium):\n    py.visit(f\"{THE_INTERNET}/dropdown\")\n    options = py.get(\"#dropdown\").children()\n    assert options.should().have_length(3)\n\n\ndef test_forced_click(py: Pylenium):\n    py.visit(f\"{DEMO_QA}/checkbox\")\n    # without forcing, this raises ElementNotInteractableException\n    py.get(\"[type='checkbox']\").click(force=True)\n    assert py.get(\"#result\").should().contain_text(\"You have selected\")\n\n\ndef test_element_should_be_clickable(py: Pylenium):\n    py.visit(f\"{DEMO_QA}/buttons\")\n    assert py.contains(\"Click Me\").should().be_clickable()\n\n\ndef test_element_should_not_be_clickable(py: Pylenium):\n    py.visit(f\"{DEMO_QA}/checkbox\")\n    with pytest.raises(AssertionError):\n        py.get('[type=\"checkbox\"]').should(timeout=3).be_clickable()\n\n\ndef test_element_should_be_visible(py: Pylenium):\n    py.visit(f\"{DEMO_QA}/buttons\")\n    assert py.contains(\"Click Me\").should().be_visible()\n\n\ndef test_element_should_be_hidden(py: Pylenium):\n    py.visit(f\"{THE_INTERNET}/hovers\")\n    assert py.get(\"[href='/users/1']\").should().be_hidden()\n\n\ndef test_element_focus(py: Pylenium):\n    py.visit(f\"{THE_INTERNET}/forgot_password\")\n    field = py.get(\"#email\")\n    assert field.should().not_be_focused()\n    field.click()\n    assert field.should().be_focused()\n\n\ndef test_elements_should_be_empty(py: Pylenium):\n    py.visit(\"https://google.com\")\n    assert py.find(\"select\", timeout=3).should().be_empty()\n    assert py.findx(\"//select\", timeout=0).should().be_empty()\n\n\ndef test_elements_should_not_be_empty(py: Pylenium):\n    py.visit(f\"{THE_INTERNET}/add_remove_elements/\")\n    py.contains(\"Add Element\").double_click()\n    assert py.find(\".added-manually\").should().not_be_empty()\n\n\ndef test_elements_should_have_length(py: Pylenium):\n    py.visit(f\"{THE_INTERNET}/add_remove_elements/\")\n    py.contains(\"Add Element\").click()\n    py.contains(\"Add Element\").click()\n    assert py.find(\".added-manually\").should().have_length(2)\n\n\ndef test_elements_should_be_greater_than(py: Pylenium):\n    py.visit(f\"{THE_INTERNET}/add_remove_elements/\")\n    py.contains(\"Add Element\").click()\n    py.contains(\"Add Element\").click()\n    assert py.find(\".added-manually\").should().be_greater_than(1)\n\n\ndef test_elements_should_be_less_than(py: Pylenium):\n    py.visit(f\"{THE_INTERNET}/add_remove_elements/\")\n    py.contains(\"Add Element\").click()\n    py.contains(\"Add Element\").click()\n    assert py.find(\".added-manually\").should().be_less_than(3)\n\n\ndef test_element_attribute(py: Pylenium):\n    search_field = '[name=\"q\"]'\n    py.visit(\"https://google.com\")\n    assert py.get(search_field).get_attribute(\"title\") == \"Search\"\n    assert py.get(search_field).should().have_attr(\"title\", \"Search\")\n\n\ndef test_element_property(py: Pylenium):\n    search_field = '[name=\"q\"]'\n    py.visit(\"https://google.com\")\n    assert py.get(search_field).get_property(\"maxLength\") == 2048\n    assert py.get(search_field).should().have_prop(\"maxLength\", 2048)\n\n\ndef test_element_should_disappear(py: Pylenium):\n    py.visit(f\"{THE_INTERNET}/dynamic_loading/1\")\n    py.get(\"#start > button\").click()\n    assert py.get(\"#loading\").should().disappear()\n    assert py.get(\"#finish\").should().have_text(\"Hello World!\")\n\n\ndef test_element_has_attribute(py: Pylenium):\n    py.visit(f\"{THE_INTERNET}/checkboxes\")\n    py.find('[type=\"checkbox\"]')[1].should().have_attr(\"checked\")\n\n\ndef test_element_does_not_have_attribute(py: Pylenium):\n    py.visit(f\"{THE_INTERNET}/checkboxes\")\n    py.get('[type=\"checkbox\"]').should().not_have_attr(\"checked\")\n\n\ndef test_element_has_attribute_with_value(py: Pylenium):\n    py.visit(f\"{THE_INTERNET}/checkboxes\")\n    py.get('[type=\"checkbox\"]').should().have_attr(\"type\", \"checkbox\")\n\n\ndef test_element_does_not_have_attribute_with_value(py: Pylenium):\n    py.visit(f\"{THE_INTERNET}/checkboxes\")\n    py.should().contain_title(\"The Internet\")\n    py.get('[type=\"checkbox\"]').should().not_have_attr(\"type\", \"box\")\n\n\ndef test_getx_nested_element(py: Pylenium):\n    py.visit(f\"{DEMO_QA}/automation-practice-form\")\n    container = py.getx('//*[@id=\"subjectsContainer\"]')\n    element = container.getx(\".//input\")\n    element_id = element.get_attribute(\"id\")\n    assert element_id == \"subjectsInput\"\n\n\ndef test_findx_nested_element(py: Pylenium):\n    py.visit(f\"{DEMO_QA}/automation-practice-form\")\n    container = py.getx('//*[@id=\"hobbiesWrapper\"]')\n    elements = container.findx(\".//input\")\n    assert len(elements) == 3\n    for element in elements:\n        assert element.get_attribute(\"type\") == \"checkbox\"\n\n\ndef test_focus(py: Pylenium):\n    py.visit(f\"{DEMO_QA}/automation-practice-form\")\n    element = py.getx('//*[@id=\"firstName\"]').focus()\n    active_elem = py.webdriver.switch_to.active_element\n    assert active_elem == element.webelement\n"
  },
  {
    "path": "tests/ui/test_element_actions.py",
    "content": "import pytest\nfrom selenium.common.exceptions import NoSuchElementException\nfrom pylenium import jquery\nfrom pylenium.driver import Pylenium\nfrom pylenium.element import Element\n\n\nTHE_INTERNET = \"https://the-internet.herokuapp.com\"\n\n\n@pytest.fixture\ndef dropdown(py: Pylenium) -> Element:\n    py.visit(f\"{THE_INTERNET}/dropdown\")\n    return py.get(\"#dropdown\")\n\n\ndef test_check_single_box(py):\n    py.visit(f\"{THE_INTERNET}/checkboxes\")\n    assert py.get('[type=\"checkbox\"]').check().should().be_checked()\n    assert py.get('[type=\"checkbox\"]').uncheck().is_checked() is False\n\n\ndef test_check_many_boxes(py):\n    py.visit(f\"{THE_INTERNET}/checkboxes\")\n    assert py.find('[type=\"checkbox\"]').check(allow_selected=True).are_checked()\n\n\ndef test_select_by_index_fails_if_option_not_available(dropdown: Element):\n    with pytest.raises(NoSuchElementException):\n        dropdown.select_by_index(3)\n\n\ndef test_select_by_text_fails_if_option_not_available(dropdown: Element):\n    with pytest.raises(NoSuchElementException):\n        dropdown.select_by_value(\"Option 3\")\n\n\ndef test_select_by_value_fails_if_option_not_available(dropdown: Element):\n    with pytest.raises(NoSuchElementException):\n        dropdown.select_by_value(\"3\")\n\n\ndef test_select_by_index(dropdown: Element):\n    dropdown.select_by_index(1)\n    assert dropdown.getx(\"./option[2]\").should().be_selected()\n\n\ndef test_select_by_text(dropdown: Element):\n    dropdown.select_by_text(\"Option 1\")\n    assert dropdown.getx(\"./option[2]\").should().be_selected()\n\n\ndef test_select_by_value(dropdown: Element):\n    dropdown.select_by_value(\"2\")\n    assert dropdown.getx(\"./option[3]\").should().be_selected()\n\n\ndef test_drag_to_with_selector(py):\n    py.visit(f\"{THE_INTERNET}/drag_and_drop\")\n    py.get(\"#column-a\").drag_to(\"#column-b\")\n    assert py.get(\"#column-b > header\").should().have_text(\"A\")\n\n\ndef test_drag_to_with_element(py):\n    py.visit(f\"{THE_INTERNET}/drag_and_drop\")\n    column_b = py.get(\"#column-b\")\n    py.get(\"#column-a\").drag_to_element(column_b)\n    assert column_b.get(\"header\").should().have_text(\"A\")\n\n\ndef test_jquery(py):\n    py.visit(\"https://amazon.com\")\n    jquery.inject(py.webdriver, version=\"3.5.1\")\n    assert py.execute_script(\"return jQuery.expando;\") is not None\n    assert py.execute_script(\"return $.expando;\") is not None\n    assert jquery.exists(py.webdriver) == \"3.5.1\"\n\n\ndef test_hover(py):\n    py.visit(f\"{THE_INTERNET}/hovers\")\n    assert py.get(\".figure\").hover().contains(\"View profile\").should().be_visible()\n\n\ndef test_radio_buttons(py):\n    py.visit(\"http://test.rubywatir.com/radios.php\")\n    radio = py.get(\"#radioId\")\n    assert radio.check().should().be_checked()\n\n    py.get('[value=\"Radio1\"]').check()\n    assert not radio.is_checked()\n\n\ndef test_checkbox_buttons(py):\n    py.visit(\"http://test.rubywatir.com/checkboxes.php\")\n    checkbox = py.get(\"input[name=sports][value=soccer]\")\n    assert checkbox.should().be_checked()\n\n    checkbox.uncheck()\n    assert not checkbox.is_checked()\n\n\ndef test_upload_file(py: Pylenium, project_root):\n    py.visit(f\"{THE_INTERNET}/upload\")\n    py.get(\"#file-upload\").upload(f\"{project_root}/LICENSE\")\n    py.get(\"#file-submit\").click()\n    assert py.contains(\"File Uploaded!\")\n"
  },
  {
    "path": "tests/ui/test_pydriver.py",
    "content": "import os\nimport pytest\nfrom selenium.webdriver.common.by import By\nfrom pylenium.a11y import PyleniumAxe\nfrom pylenium.driver import Pylenium\n\n\nTHE_INTERNET = \"https://the-internet.herokuapp.com\"\nTEST_PAGES = \"https://testpages.eviltester.com\"\n\n\ndef test_jit_webdriver(py: Pylenium):\n    assert py._webdriver is None\n    assert py.webdriver is not None\n    assert py._webdriver is not None\n\n\n@pytest.mark.skipif(os.environ.get(\"LT_USERNAME\") is not None, reason=\"Doesn't work well with LambdaTest\")\ndef test_browser_options(py: Pylenium):\n    py.config.driver.options = [\"--headless\"]\n    py.visit(\"https://google.com\")\n    assert py.should().contain_title(\"Google\")\n\n\ndef test_execute_script(py: Pylenium):\n    py.visit(\"https://google.com\")\n    webelement = py.get(\"[name='q']\").webelement\n    assert py.execute_script(\"return arguments[0].parentNode;\", webelement)\n\n\ndef test_new_window_and_tab(py: Pylenium):\n    py.switch_to.new_window()\n    assert len(py.window_handles) == 2\n\n    py.switch_to.new_tab()\n    assert len(py.window_handles) == 3\n\n\ndef test_cookies(py: Pylenium):\n    cookie_to_set = {\"name\": \"foo\", \"value\": \"bar\"}\n    cookie_to_test = {\n        \"domain\": \"www.google.com\",\n        \"httpOnly\": False,\n        \"name\": \"foo\",\n        \"path\": \"/\",\n        \"secure\": True,\n        \"value\": \"bar\",\n        \"sameSite\": \"Lax\",\n    }\n    py.visit(\"https://google.com\")\n\n    py.set_cookie(cookie_to_set)\n    assert py.get_cookie(\"foo\") == cookie_to_test\n\n    py.delete_cookie(\"foo\")\n    assert py.get_cookie(\"foo\") is None\n\n\ndef test_viewport(py: Pylenium):\n    py.visit(\"https://google.com\")\n    py.viewport(1280, 800)\n    assert {\"width\": 1280, \"height\": 800} == py.window_size\n\n\ndef test_hover_and_click_to_page_transition(py: Pylenium):\n    py.visit(\"https://qap.dev\")\n    py.get('a[href=\"/about\"]').hover().get('a[href=\"/leadership\"][class*=Header]').click()\n    assert py.contains(\"Carlos Kidman\").should().contain_text(\"Carlos Kidman\")\n\n\ndef test_pylenium_wait_until(py: Pylenium):\n    py.visit(\"https://qap.dev\")\n    py.wait(use_py=True).sleep(2)\n    element = py.wait(5, use_py=True).until(lambda x: x.find_element(By.CSS_SELECTOR, '[href=\"/about\"]'))\n    assert element.tag_name() == \"a\"\n    assert element.hover()\n\n\ndef test_webdriver_wait_until(py: Pylenium):\n    py.visit(\"https://qap.dev\")\n    element = py.wait(5).until(lambda x: x.find_element(By.CSS_SELECTOR, '[href=\"/about\"]'))\n    assert element.tag_name == \"a\"\n\n\ndef test_switch_to_frame_by_element_then_back(py: Pylenium):\n    py.visit(f\"{TEST_PAGES}/styled/iframes-test.html\")\n    iframe = py.get(\"iframe#theheaderhtml\")\n    py.switch_to.frame_by_element(iframe)\n    assert py.get(\"h1\").should().contain_text(\"Nested Page Example\")\n\n    py.switch_to.default_content()\n    assert py.get(\"h1\").should().contain_text(\"iFrames Example\")\n\n    py.switch_to.frame_by_element(iframe)\n    assert py.get(\"h1\").should().contain_text(\"Nested Page Example\")\n\n    py.switch_to.parent_frame()\n    assert py.get(\"h2\").should().contain_text(\"iFrame Example List\")\n\n\ndef test_have_url(py: Pylenium):\n    py.visit(\"https://qap.dev\")\n    py.should().have_url(\"https://www.qap.dev/\")\n\n\n@pytest.mark.skip(reason=\"Unstable test as of Selenium 4\")\ndef test_loading_extension_to_browser(py: Pylenium, project_root):\n    py.config.driver.extension_paths.append(f\"{project_root}/tests/ui/Get CRX.crx\")\n    py.visit(\"chrome://extensions/\")\n    shadow1 = py.get(\"extensions-manager\").open_shadow_dom()\n    shadow2 = shadow1.get(\"extensions-item-list\").open_shadow_dom()\n    ext_shadow_dom = shadow2.find(\"extensions-item\").first().open_shadow_dom()\n    assert ext_shadow_dom.get(\"#name-and-version\").should().contain_text(\"Get CRX\")\n\n\ndef test_should_not_find(py: Pylenium):\n    py.visit(\"https://google.com\")\n    assert py.should().not_find(\"select\")\n    assert py.should().not_findx(\"//select\")\n    assert py.should().not_contain(\"foobar\")\n\n\ndef test_axe_run(py: Pylenium):\n    py.visit(\"https://qap.dev\")\n    axe = PyleniumAxe(py.webdriver)\n    report = axe.run(name=\"a11y.json\")\n    number_of_violations = len(report.violations)\n    assert number_of_violations < 10, f\"{number_of_violations} violation(s) found\"\n\n\ndef test_axe_fixture(axe):\n    axe.webdriver.get(\"https://qap.dev\")\n    file_name = \"a11y.json\"\n    axe.run(name=file_name)\n    assert os.path.exists(file_name)\n"
  },
  {
    "path": "tests/unit/test_config.py",
    "content": "def test_py_config_defaults(py_config):\n    # driver settings\n    assert py_config.driver.browser == \"chrome\"\n    assert py_config.driver.remote_url == \"\"\n    assert py_config.driver.wait_time == 10\n    assert py_config.driver.page_load_wait_time == 0\n    assert py_config.driver.options == []\n    assert py_config.driver.capabilities == {}\n    assert py_config.driver.experimental_options is None\n    assert py_config.driver.webdriver_kwargs == {}\n\n    # logging settings\n    assert py_config.logging.screenshots_on is True\n    assert py_config.logging.pylog_level == \"INFO\"\n\n    # viewport settings\n    assert py_config.viewport.maximize is True\n    assert py_config.viewport.width == 1440\n    assert py_config.viewport.height == 900\n    assert py_config.viewport.orientation == \"portrait\"\n\n    # custom settings\n    assert py_config.custom is not None\n"
  },
  {
    "path": "tests/unit/test_faker.py",
    "content": "\"\"\" Tests to see what could be faked.\n\nFaker docs: https://faker.readthedocs.io/en/stable/providers.html\n\n* You don't get intellisense\n* Use the list of Providers in the above link to know what you can do\n* You can use localizations\n* You can create your own, custom Providers\n\"\"\"\n\n\ndef test_fake_name(fake):\n    assert fake.name()\n\n\ndef test_fake_first_name(fake):\n    assert fake.first_name()\n\n\ndef test_fake_last_name(fake):\n    assert fake.last_name()\n\n\ndef test_fake_email(fake):\n    assert fake.email()\n\n\ndef test_fake_address(fake):\n    assert fake.address()\n\n\ndef test_fake_text(fake):\n    assert fake.text()\n\n\ndef test_fake_ssn(fake):\n    assert fake.ssn()\n\n\ndef test_fake_locale(fake):\n    assert fake.locale()\n\n\ndef test_fake_country(fake):\n    assert fake.country()\n\n\ndef test_fake_postal_code(fake):\n    assert fake.postalcode()\n"
  },
  {
    "path": "tests/unit/test_requests.py",
    "content": "def test_api_fixture(api):\n    response = api.get('https://google.com')\n    assert response.ok\n"
  }
]