# Python Testing Guide

This guide helps contributors write tests in the Selenium Python codebase.

## Test Framework

* Tests use [pytest](https://pytest.org).
* Test HTML pages live in `common/src/web/`.
* `pages` fixture loads test pages via `pages.load("pageName.html")`.
* Assertions use standard pytest `assert` statements.

```python
import pytest
from selenium.webdriver.common.by import By

def test_element_is_displayed(driver, pages):
    pages.load("javascriptPage.html")

    element = driver.find_element(By.ID, "displayed")
    assert element.is_displayed() is True

@pytest.mark.xfail_safari(reason="Safari doesn't support this")
def test_something_safari_fails(driver, pages):
    # Expected to fail on Safari
    pass
```

## Running Tests

Bazel creates test targets for each browser. Tests run in parallel by default.

```shell
bazel test //py/...  # All tests
bazel test //py:unit  # Unit tests (no browser)
bazel test //py:test-chrome  # Chrome browser tests
bazel test //py:test-firefox  # Firefox browser tests
bazel test //py:test-chrome-common  # Common (cross-browser) tests with Chrome

# A single test file with Chrome (target = test/<path>/<file>-<browser>[-variant]).
# Discover exact names with: bazel query //py:all | grep window_tests
bazel test //py:test/selenium/webdriver/common/window_tests-chrome

# With BiDi protocol
bazel test //py:test-chrome-bidi

# Test filters
bazel test //py/... --test_tag_filters=chrome

# Additional arguments
bazel test //py/... --flaky_test_attempts=3
bazel test //py/... --test_output=all
bazel test //py/... --test_output=streamed  # Live output for debugging
bazel test //py:test-chrome --headless

# Run a specific test in a test file
bazel test //py:test/selenium/webdriver/common/bidi_browsing_context_tests-chrome-bidi \
  --test_arg=-k \
  --test_arg=test_get_tree_with_child \

# View all targets
bazel query //py/...
```

## Running Tests Without Bazel (using pytest)

You can run tests directly with pytest after setting up the development environment.

### Setup

First, install the required dependencies:

```shell
pip install -r py/requirements_lock.txt
```

Then build the generated files and copy them into your local source tree:

```shell
./go py:local_dev
```

### Running with pytest

```shell
# Run all tests in a directory
pytest py/test/selenium/webdriver/chrome/ --driver chrome

# Run a specific test file
pytest py/test/selenium/webdriver/common/window_tests.py

# Run a specific test function
pytest py/test/selenium/webdriver/common/window_tests.py::test_should_get_the_size_of_the_current_window

# With pytest options
pytest py/test/selenium/webdriver/chrome/ --driver chrome --headless -v
```
> **Note:**
> For running BiDi tests, use the `--bidi` flag.

## Skipping Tests

Skips use pytest markers; each accepts optional `reason` and `run` parameters
(`run=False` skips the test entirely instead of expecting a failure).

| Marker | When to Use |
|--------|-------------|
| `@pytest.mark.xfail_chrome` | Test expected to fail on Chrome |
| `@pytest.mark.xfail_firefox` | Test expected to fail on Firefox |
| `@pytest.mark.xfail_safari` | Test expected to fail on Safari |
| `@pytest.mark.xfail_edge` | Test expected to fail on Edge |
| `@pytest.mark.xfail_ie` | Test expected to fail on IE |
| `@pytest.mark.xfail_remote` | Test expected to fail with Remote WebDriver |

```python
@pytest.mark.xfail_chrome(reason="Not implemented yet")
@pytest.mark.xfail_firefox(reason="https://bugzilla.mozilla.org/123")
def test_something(driver, pages):
    pass

@pytest.mark.xfail_safari(run=False)  # Skip entirely instead of xfail
def test_skip_safari(driver, pages):
    pass
```

## Driver Lifecycle

| Marker | When to Use |
|--------|-------------|
| `@pytest.mark.no_driver_after_test` | Teardown driver after test |
| `@pytest.mark.needs_fresh_driver` | Restart driver for test isolation |

## Fixtures

We make use of
[pytest fixtures](https://docs.pytest.org/en/stable/reference/fixtures.html)
to simplify test setup/teardown. There are several
[built-in pytest fixtures](https://docs.pytest.org/en/stable/reference/fixtures.html),
and many of our own internal fixtures. If a fixture is specific to a module, you will
find it defined within the test file that uses it. If it is shared among several
modules, you will find the main fixtures in `conftest.py`:

| Fixture | Description |
|---------|-------------|
| `driver` | WebDriver instance, auto-parametrized by browser |
| `pages` | Load test pages: `pages.load("page.html")` or `pages.url("page.html")` |
| `webserver` | Test HTTP server reference |
| `clean_driver` | Fresh driver without parametrization |
| `clean_options` | Fresh browser options instance |

## Test Organization

```
py/test/
├── unit/                    # Unit tests (no browser)
│   └── selenium/webdriver/
└── selenium/webdriver/      # Integration tests
    ├── common/              # Cross-browser tests
    ├── chrome/
    ├── firefox/
    ├── safari/
    └── remote/
```

Test files end in `_tests.py` (e.g., `visibility_tests.py`).

## Build Files

* Adding tests shouldn't require Bazel changes—files matching `*_tests.py` are picked up automatically.
* Make sure the test file is in a directory covered by existing `py_test_suite` targets.
