Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ 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.1.0] - 2026-10-24

### Changed
- Set the default and strict network idle thresholds to zero so all tracked XHR/fetch requests finish before interactions.
- Upgrade note: apps with constant polling or analytics can set `network_idle_threshold=2` to restore the previous allowance of up to two pending requests.
- Clicking a disabled element now waits for it to become enabled and raises a timeout if it stays disabled, identifying the element and configured duration.
- A stale retry refuses replaced elements with different text or a different fingerprint rather than clicking them.

### Fixed
- Re-find elements with the same locator after the page settles, so list redraws during stabilization do not return an outdated element.
- Retry a locator-backed wrapped click once after a stale element reference, settling before the retry lookup and waiting through a temporarily empty list.
- Capture tag, visible text, identifying attributes, locator-match index, and parent text in the fingerprint; refuse reordered rows even when their buttons have identical labels.
- Read fingerprint fields in one browser script call within the existing deadline, cap element and trimmed parent text at 200 characters, and handle a redraw during the read.
- Report that an element is no longer on the page when it permanently disappears before a click retry. Preserve Selenium's original not-found diagnostics for unsuccessful lookups.
- Share one timeout deadline across each `click()` or `find_element()` call's stabilization, polling, and retry waits.
- Add local-browser and unit regressions for list redraws, reordered targets, transient empty lists, disabled overlays, retry limits, and total timeout budgets.

## [1.0.5] - 2026-10-18

### Changed
Expand Down
43 changes: 39 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ from waitless import stabilize, StabilizationConfig
config = StabilizationConfig(
timeout=10, # Max wait time (seconds)
mutation_rate_threshold=50, # mutations/sec considered stable (allows animations)
network_idle_threshold=2, # Max pending requests (allows background traffic)
network_idle_threshold=0, # Wait for all tracked XHR/fetch requests
animation_detection=True, # Track CSS animations (non-blocking in normal mode)
strictness='normal', # 'strict' | 'normal' | 'relaxed'
debug_mode=True # Enable logging
Expand Down Expand Up @@ -191,7 +191,17 @@ Sample output:

### Network Threshold Warning

The default `network_idle_threshold=2` allows some background traffic.
The default and `StabilizationConfig.strict()` use `network_idle_threshold=0`,
so all tracked XHR/fetch requests must finish before the network is considered
idle. This prevents a delayed response from leaving a loading overlay in place
when an interaction starts. Set `network_idle_threshold=2` to allow up to two
requests to remain pending for background analytics or polling.

When upgrading to 1.1.0, apps with constant polling or analytics can set
`network_idle_threshold=2` to restore the previous allowance.
Two other behavior changes apply: clicking a disabled element now waits and
raises a timeout if it stays disabled, and a stale retry refuses a replaced
element with different text or a different fingerprint rather than clicking it.

Many apps have background traffic that never stops:
- Analytics calls
Expand All @@ -207,7 +217,32 @@ config = StabilizationConfig(network_idle_threshold=3)
### Wrapped Elements

The stabilized driver returns wrapped elements that auto-wait before `click()`,
`send_keys()`, `submit()`, and `clear()`. They behave like WebElements but:
`send_keys()`, `submit()`, and `clear()`.

`click()` also waits for `is_enabled()` to become true. If the element stays
disabled, its timeout message includes the tag, visible text, and configured
duration. Each `click()` and `find_element()` call shares one timeout budget
across its stabilization, polling, and retry waits.
If an element permanently disappears before a click retry, the error says it
is no longer on the page instead of reporting that it was not enabled.

Elements returned by `find_element()` retain their locator and a fingerprint
captured while the element is live: tag name, visible text, the `id`, `name`,
`data-testid`, and `aria-label` attributes when present, its index among all
matches of the locator, and its parent's trimmed text. Element and parent text
are capped at 200 characters in the browser. Fingerprint fields are read in one
script call within the existing timeout budget, using the locator's matches
to determine the index. A stale click can retry
once: Waitless waits for the page to settle, looks up the candidate using the
same locator, and checks its fingerprint before clicking. If the fingerprint
differs, Waitless raises `StaleElementReferenceException` explaining that it
refused to click a different element. Identical replacements can be retried;
elements indistinguishable by these fields cannot be distinguished by this check.

Elements returned by `find_elements()` or `execute_script()` (including
`execute_async_script()`) do not retain a locator and do not retry stale clicks.
Their clicks still wait for stability and enabled state. They behave like
WebElements but:

- `isinstance(element, WebElement)` returns `False`
- Use `.unwrap()` to get the original element if needed
Expand Down Expand Up @@ -299,7 +334,7 @@ It isn't tested against SeleniumBase in this repository. `stabilize()` accepts a
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.
Waitless polls every 50 ms (`poll_interval`) while waiting for stability. The DOM must have been quiet for 0.1 s (`dom_settle_time`), and the other configured signals must also be stable. An already settled page can pass the check immediately; Waitless does not add a fixed pause to every interaction. New activity extends the wait within the call's timeout budget (10 s by default). `find_element()` checks stability before and after locating an element, and `click()` also waits for enabled state. WebDriver commands themselves, including any Selenium implicit waits, may extend wall-clock time beyond that budget. 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).
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "waitless"
version = "1.0.5"
version = "1.1.0"
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"
Expand Down
Loading
Loading