diff --git a/CHANGELOG.md b/CHANGELOG.md index e616f03..fb24597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.5] - Unreleased + +### Changed +- Documentation and packaging metadata only; no behavior changes (version strings bumped). +- README: problem-first opening, PyPI/Python/license badges, "When to use Waitless (and when not to)" comparison, FAQ and troubleshooting, and links to the blog posts and project page. +- PyPI metadata: clearer summary, more search keywords and classifiers, project page as Homepage, Blog URL. + ## [1.0.4] - 2026-09-16 ### Fixed diff --git a/README.md b/README.md index 3ccfcce..90829b8 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,15 @@ # Waitless +[![PyPI version](https://img.shields.io/pypi/v/waitless.svg)](https://pypi.org/project/waitless/) +[![Python versions](https://img.shields.io/pypi/pyversions/waitless.svg)](https://pypi.org/project/waitless/) +[![License: MIT](https://img.shields.io/pypi/l/waitless.svg)](https://github.com/godhiraj-code/waitless/blob/main/LICENSE) [![CI](https://github.com/godhiraj-code/waitless/actions/workflows/ci.yml/badge.svg)](https://github.com/godhiraj-code/waitless/actions/workflows/ci.yml) -**Automatic UI stabilization for Selenium** - -Reduce explicit waits and sleeps by automatically evaluating multiple UI stability signals. +**Stop flaky Selenium tests caused by clicking before the page is ready.** Waitless wraps your WebDriver so `get`, `find_element` and `click` wait for the DOM and network to settle (and, in strict mode, for animations and layout too): no `time.sleep()`, less `WebDriverWait` boilerplate, and fewer `StaleElementReferenceException`, `ElementClickInterceptedException` and `NoSuchElementException` failures caused by timing races. +**Automatic UI stabilization for Selenium.** Reduce explicit waits and sleeps by automatically evaluating multiple UI stability signals. +**Learn more:** [Waitless v1: the end of flaky tests](https://www.dhirajdas.dev/blog/waitless-v1-end-of-flaky-tests) · [Eliminating flaky tests with Waitless](https://www.dhirajdas.dev/blog/waitless-eliminate-flaky-tests) · [Project page](https://www.dhirajdas.dev/project/waitless) ## Installation @@ -49,6 +52,7 @@ Automation tests fail because interactions happen while the UI is still changing | Approach | Problem | |----------|---------| | `time.sleep(2)` | Too slow, still fails sometimes | +| `driver.implicitly_wait(10)` | Only retries element lookup until the element exists; it does not wait for pending requests, animations or DOM churn | | `WebDriverWait` | Requires an explicit condition at each synchronization point | | Retries | Masks the real problem, adds flakiness | @@ -67,6 +71,28 @@ Waitless evaluates page-level stability signals: Before supported interactions, Waitless polls until the enabled mandatory signals meet their configured thresholds. +## When to use Waitless (and when not to) + +**Good fit:** + +- Selenium 4 test suites in Python, on single-page apps (React, Angular, Vue) where clicks land while the page is still re-rendering +- Pages with AJAX/fetch calls, lazy-loaded content or long-polling that make tests pass locally and fail in CI +- Suites full of `time.sleep()` calls or copy-pasted `WebDriverWait` boilerplate that you want to remove + +**Not a good fit:** + +- **Playwright.** Playwright already auto-waits for elements to be actionable, and Waitless only supports Selenium. +- **Specific business conditions.** If a test must wait for "the order status says Shipped", keep a `WebDriverWait` with an explicit condition for that. Waitless waits for the page to be *stable*, not for your app to reach a particular state. The two work well together. +- **Async Selenium clients or Service Worker traffic.** See [Current Limitations](#current-limitations). + +| | `time.sleep` | `implicitly_wait` | `WebDriverWait` | Waitless | +|---|---|---|---|---| +| Waits for element presence | No (fixed delay) | Yes | Yes (per condition) | Yes (`find_element` retries until timeout) | +| Waits for pending XHR/fetch | No | No | Only if you write the condition | Yes | +| Waits for DOM mutations to settle | No | No | Only if you write the condition | Yes | +| Waits for animations/layout | No | No | Only if you write the condition | Strict mode | +| Code needed per interaction | One sleep | None | One condition | None after `stabilize(driver)` | + ## Configuration ```python @@ -244,7 +270,39 @@ timeout after the page-stability wait. - **Sync only** - No async/await support yet - **No Service Workers** - SW network requests not intercepted -See [CHANGELOG.md](CHANGELOG.md) for version history. +See [CHANGELOG.md](https://github.com/godhiraj-code/waitless/blob/main/CHANGELOG.md) for version history. + +## FAQ and Troubleshooting + +**Does it work with pytest?** +Yes. Waitless has no runner dependency; wrap the driver in your fixture: + +```python +import pytest +from selenium import webdriver +from waitless import stabilize + +@pytest.fixture +def driver(): + d = stabilize(webdriver.Chrome()) + yield d + d.quit() +``` + +**Does it work with Shadow DOM?** +Yes, for open shadow roots. The MutationObserver also watches open shadow roots, and elements and shadow roots returned from searches are wrapped so they auto-wait too. Closed shadow roots are not visible to page JavaScript, so they are not observed. + +**Does it work with SeleniumBase or other wrappers?** +It isn't tested against SeleniumBase in this repository. `stabilize()` accepts any WebDriver-like object that has `execute_script`, `find_element` and `current_url`, so you can wrap the underlying Selenium driver. If a framework needs a real `WebDriver` instance, pass it `driver.unwrapped`. + +**My app has WebSockets, analytics beacons or long polling and never goes idle. I get `StabilizationTimeout`.** +Run the diagnostics (see [Diagnostics](#diagnostics)) to see which signal is blocking. WebSocket/SSE tracking is off by default, so it doesn't block unless you enable it. For background XHR/fetch traffic, raise `network_idle_threshold` or use `StabilizationConfig.relaxed()` (DOM only). For infinite spinners, set `animation_detection=False`. + +**How much slower does it make my tests?** +Waitless polls every 50 ms (`poll_interval`) and returns as soon as the enabled signals are stable, after a 0.1 s DOM quiet period (`dom_settle_time`). On a settled page that's a short pause per interaction; on a busy page it waits as long as the page is busy, up to `timeout` (10 s by default). Measure it in your environment with `python benchmarks/overhead_test.py` from a repository checkout. + +**`isinstance(element, WebElement)` is `False`.** +That's expected: the stabilized driver returns wrapped elements. Call `.unwrap()` on an element, or use `driver.unwrapped` for the original driver. See [Wrapped Elements](#wrapped-elements). ## API Reference @@ -268,4 +326,4 @@ See [CHANGELOG.md](CHANGELOG.md) for version history. ## License -MIT +MIT. See [LICENSE](https://github.com/godhiraj-code/waitless/blob/main/LICENSE). diff --git a/pyproject.toml b/pyproject.toml index 9f1b02b..fc8ac60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [build-system] -requires = ["setuptools>=61.0", "wheel"] +requires = ["setuptools>=77.0", "wheel"] build-backend = "setuptools.build_meta" [project] name = "waitless" -version = "1.0.4" -description = "Reduce explicit waits in Selenium by evaluating UI stability signals" +version = "1.0.5" +description = "Auto-wait for Selenium in Python: fix flaky tests by waiting for DOM, network, animation and layout stability instead of time.sleep." readme = "README.md" license = "MIT" requires-python = ">=3.9" @@ -19,7 +19,16 @@ keywords = [ "ui-testing", "wait", "stability", - "flaky-tests" + "flaky-tests", + "selenium-python", + "webdriver", + "auto-wait", + "explicit-wait", + "time-sleep", + "pytest", + "test-automation", + "StaleElementReferenceException", + "ElementClickInterceptedException" ] classifiers = [ "Development Status :: 4 - Beta", @@ -32,19 +41,23 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3 :: Only", "Topic :: Software Development :: Testing", + "Topic :: Software Development :: Testing :: Acceptance", "Topic :: Software Development :: Quality Assurance", + "Topic :: Internet :: WWW/HTTP :: Browsers", ] dependencies = [ "selenium>=4.0", ] [project.urls] -Homepage = "https://www.dhirajdas.dev" +Homepage = "https://www.dhirajdas.dev/project/waitless" Documentation = "https://github.com/godhiraj-code/waitless#readme" -Repository = "https://github.com/godhiraj-code/waitless.git" +Repository = "https://github.com/godhiraj-code/waitless" Issues = "https://github.com/godhiraj-code/waitless/issues" Changelog = "https://github.com/godhiraj-code/waitless/blob/main/CHANGELOG.md" +Blog = "https://www.dhirajdas.dev/blog/waitless-v1-end-of-flaky-tests" [project.scripts] waitless = "waitless.__main__:main" diff --git a/waitless/__init__.py b/waitless/__init__.py index 6f73665..63ee115 100644 --- a/waitless/__init__.py +++ b/waitless/__init__.py @@ -36,7 +36,7 @@ driver = unstabilize(driver) # Back to original behavior """ -__version__ = '1.0.4' +__version__ = '1.0.5' __author__ = 'Dhiraj Das' # Public API diff --git a/waitless/instrumentation.py b/waitless/instrumentation.py index 08ff01c..f237320 100644 --- a/waitless/instrumentation.py +++ b/waitless/instrumentation.py @@ -12,7 +12,7 @@ window.__waitless__ = { _initialized: true, - _version: '1.0.4', + _version: '1.0.5', // State tracking pendingRequests: 0,