# TestFly Part 13: Actionability Engine — Auto-Wait and Verified Click/Fill/Clear Assertions

URL: https://hakangul.lovable.app/blog/testfly-part-13-actionability-engine-auto-wait-verified-actions
Author: Hakan Gül — Team Lead Senior Test Automation Engineer
Published: 2026-10-12
Updated: 2026-10-12
Category: test-automation
Tags: TestFly, Selenium, Java, SDET, Testing

> TestFly 1.1.0 adds a Playwright-style actionability engine: every click, hover, type and clear waits for the element to be visible, enabled, stable and uncovered before acting, and the new assertThat(x).click()/.fill()/.clear() verify the action actually reached and changed the page.

## English

**TL;DR:** TestFly 1.1.0 adds an actionability engine that every `click`, `hover`, `type`, `clear` and `append` call runs through before touching the DOM: visible, enabled where relevant, geometrically stable, not covered by another element, and scrolled into view if needed. On top of it, `assertThat(x).click()` / `.fill(text)` / `.clear()` turn the action itself into an assertion — they verify the click event actually reached the element and that typed text stayed in the field. A failure throws `NotActionableException` naming the element and the exact blocker.

---

## How Does TestFly Decide an Element Is Ready for an Action?

Every interaction is tagged with an `ActionKind`, which decides which checks apply:

```java
public enum ActionKind {
    CLICK(true, true),   // enabled, and not covered by another element
    HOVER(false, true),  // not covered; a disabled element can still be hovered
    EDIT(true, false);   // enabled; being covered doesn't matter for keyboard input
}
```

Every kind requires the element to be visible and stable regardless of its flags. `WaitEngine.waitForActionable(element, kind)` polls until all the required checks pass, re-scrolling the element into view if it drifts offscreen between polls:

```java
public static WebElement waitForActionable(WebElement element, ActionKind kind) {
    TestFlyConfig.Actionability cfg = actionabilityConfig();
    if (!cfg.isEnabled()) {
        return kind == ActionKind.CLICK ? waitForClickable(element) : waitForVisible(element);
    }
    // visible, enabled (if required), stable bounding box, not covered (if required)...
}
```

A stale element bubbles up as `StaleElementReferenceException` so the caller (the `Locator` chain) can re-resolve it from scratch instead of failing on a detached node.

## What Exactly Gets Checked, and in What Order?

| Check | Applies to | Detail |
| --- | --- | --- |
| Visible | all kinds | element has a non-empty bounding box and isn't `display:none`/`visibility:hidden` |
| Enabled | `CLICK`, `EDIT` | skipped for `HOVER` — a disabled button can still be hovered |
| Stable | all kinds | bounding box unchanged between two consecutive polls (default poll interval: 100ms) |
| Not covered | `CLICK`, `HOVER` | nothing else receives a hit-test at the element's centre; skipped for `EDIT` |
| Auto-scroll | all kinds | scrolls the element to the viewport centre if it's offscreen when checked |

The `stabilityIntervalMs` config (default 100ms) is both the polling interval and the minimum time an element's bounding box must stay put — the same anti-animation idea Playwright uses, adapted to Selenium's `WebElement`.

## What Happens When an Element Never Becomes Actionable?

```java
public class NotActionableException extends TimeoutException {
    public NotActionableException(String message, Throwable cause) {
        super(message, cause);
    }
}
```

It extends `TimeoutException`, so any existing `catch (TimeoutException e)` handler keeps working. The message names the element and the specific blocker — for example which overlay is covering the centre point, or that the bounding box kept moving. Nothing silently clicks the wrong spot or types into a half-animated field.

## How Do I Configure or Bypass Actionability Checks?

```yaml
# testfly.yml
actionability:
  enabled: true           # false restores the pre-1.1.0 waits
  stabilityIntervalMs: 100
  hitTargetCheck: true
  autoScroll: true
```

```java
public static final class Actionability {
    private boolean enabled = true;
    private int stabilityIntervalMs = 100;
    private boolean hitTargetCheck = true;
    private boolean autoScroll = true;
}
```

Setting `actionability.enabled: false` falls back to the legacy waits: `waitForClickable` for clicks, `waitForVisible` for everything else — useful during a migration, or if a page genuinely needs the old behaviour.

For a single locator, `Locator.force()` skips just the stability and not-covered checks (Playwright's `force: true`), while still requiring the element to be visible, and enabled where the action needs it:

```java
find("#banner-close").force().click();
```

`BasePage.doubleClick`, `rightClick`, `selectBy*` and `BaseConditions.click`/`type` all route through the same `waitForActionable` call, so the behaviour is consistent across the whole API surface.

## How Does `assertThat(x).click()` Verify a Click Actually Landed?

A plain `Locator.click()` performs the actionability wait and clicks — same as before. `assertThat(x).click()` goes one step further and turns the action into an assertion:

```java
assertThat(loginButton).click();
```

1. Resolves the element the same way `Locator.click()` does, including self-healing, and waits until it's visible and enabled.
2. Scrolls it to the centre of the viewport and moves the mouse onto it with `Actions`, like a real user.
3. Waits until nothing else — a cookie banner, a modal, a sticky header — covers its centre point.
4. Clicks it, then asserts that the `click` event actually reached the element or one of its descendants.

On success it logs a passed `Clicked: ...` step. On failure it's a regular `AssertionError` (or a recorded soft failure in soft mode) naming which step failed. If the click navigates away or opens an alert before the event listener can read back the result, the click still counts as delivered — it doesn't fail just because the page moved on. `Locator.click()` itself is unchanged; this is an additive, stricter sibling on `LocatorAssert`.

## How Do `assertThat(x).fill(text)` and `.clear()` Verify the Field Actually Changed?

```java
assertThat(userNameField).fill("admin");
assertThat(passwordField).fill(password);
assertThat(searchField).clear();
```

Both share one path (`fillAndVerify`):

1. Resolve the element like `Locator.fill(String)` would (waiting, with self-healing), and wait until it's visible and enabled.
2. Wait until the field is not `readonly`, then scroll it to the viewport centre.
3. Clear it and type the text exactly as `Locator.type(String)` does.
4. Wait until the field's value **equals** the text and stays that way — catching cases where `maxlength`, an input mask, or framework state (React, Vue…) silently changed or reset what was typed.

`clear()` is the same path with an empty string. For `type="password"` fields, the value itself is never logged or put in a failure message — only its length is. Fields that reformat input as you type (phone masks, for example) will fail this stricter assertion; use `Locator.fill(text)` followed by `assertThat(x).hasValue(formattedValue)` for those instead.

## FAQ

### Does turning on actionability checks slow every test down?

Each action waits roughly one extra polling interval (100ms by default) compared to the pre-1.1.0 waits, since the engine needs at least two consecutive stable polls to confirm the bounding box stopped moving. Checks fail open when JavaScript is unavailable, so there's no hard dependency on a JS-capable session.

### Does `assertThat(x).click()` replace `Locator.click()`?

No. `Locator.click()` is unchanged and still just performs the actionability wait and clicks. `assertThat(x).click()` is an additive, verified sibling that also asserts the click event reached the element, logs a `Clicked: ...` step, and supports `as(...)`, `within(...)` and soft mode.

### Can I combine `not()` with the verified `click()`/`fill()`/`clear()`?

No — these three methods call `rejectNegation(...)` and throw if `not()` was chained before them. They assert that an action happened and verifiably changed the page; there's no well-defined "negated click."

### What's the difference between `force()` and `actionability.enabled: false`?

`force()` is per-locator and only skips the stability and not-covered checks — the element must still be visible, and enabled where the action requires it. `actionability.enabled: false` is global and reverts every action in the suite to the pre-1.1.0 waits (`waitForClickable`/`waitForVisible`), dropping all the new checks at once.

## Türkçe

**Özet:** TestFly 1.1.0, her `click`, `hover`, `type`, `clear` ve `append` çağrısının DOM'a dokunmadan önce geçtiği bir actionability (eyleme hazırlık) motoru ekliyor: görünür, gerektiğinde etkin (enabled), geometrik olarak sabit, başka bir eleman tarafından kapatılmamış ve gerekirse görünüme kaydırılmış. Bunun üzerine `assertThat(x).click()` / `.fill(text)` / `.clear()`, eylemin kendisini bir assertion'a dönüştürüyor — click olayının gerçekten elemente ulaştığını ve yazılan metnin alanda kaldığını doğruluyor. Bir hata `NotActionableException` fırlatır ve elementi ile tam engelleyici nedeni adlandırır.

---

## TestFly Bir Elementin Eyleme Hazır Olduğuna Nasıl Karar Veriyor?

Her etkileşim, hangi kontrollerin uygulanacağına karar veren bir `ActionKind` ile etiketlenir:

```java
public enum ActionKind {
    CLICK(true, true),   // etkin olmalı, ve başka bir eleman tarafından kapatılmamalı
    HOVER(false, true),  // kapatılmamalı; devre dışı bir eleman yine de hover edilebilir
    EDIT(true, false);   // etkin olmalı; kapatılmış olması klavye girişi için önemli değil
}
```

Bayraklarından bağımsız olarak her tür, elementin görünür ve sabit olmasını gerektirir. `WaitEngine.waitForActionable(element, kind)`, gereken tüm kontroller geçene kadar polling yapar ve element pollinglar arasında ekran dışına kayarsa yeniden görünüme kaydırır:

```java
public static WebElement waitForActionable(WebElement element, ActionKind kind) {
    TestFlyConfig.Actionability cfg = actionabilityConfig();
    if (!cfg.isEnabled()) {
        return kind == ActionKind.CLICK ? waitForClickable(element) : waitForVisible(element);
    }
    // görünür, etkin (gerekiyorsa), sabit bounding box, kapatılmamış (gerekiyorsa)...
}
```

Stale (koptu) bir eleman `StaleElementReferenceException` olarak yukarı taşınır, böylece çağıran taraf (`Locator` zinciri) kopmuş bir düğümde hata vermek yerine elementi sıfırdan yeniden çözebilir.

## Tam Olarak Ne Kontrol Ediliyor, Hangi Sırayla?

| Kontrol | Uygulandığı tür | Detay |
| --- | --- | --- |
| Görünür | tüm türler | element boş olmayan bir bounding box'a sahip ve `display:none`/`visibility:hidden` değil |
| Etkin | `CLICK`, `EDIT` | `HOVER` için atlanır — devre dışı bir buton yine de hover edilebilir |
| Sabit | tüm türler | ardışık iki pollingde bounding box değişmemiş (varsayılan polling aralığı: 100ms) |
| Kapatılmamış | `CLICK`, `HOVER` | elementin merkezinde başka hiçbir şey hit-test'i almıyor; `EDIT` için atlanır |
| Otomatik kaydırma | tüm türler | kontrol sırasında ekran dışındaysa elementi viewport merkezine kaydırır |

`stabilityIntervalMs` ayarı (varsayılan 100ms) hem polling aralığıdır hem de bir elementin bounding box'ının yerinde kalması gereken minimum süredir — Playwright'ın kullandığı aynı anti-animasyon fikrinin, Selenium'un `WebElement`'ine uyarlanmış hali.

## Bir Element Hiç Eyleme Hazır Olmazsa Ne Olur?

```java
public class NotActionableException extends TimeoutException {
    public NotActionableException(String message, Throwable cause) {
        super(message, cause);
    }
}
```

`TimeoutException`'dan türetilir, böylece mevcut `catch (TimeoutException e)` yakalayıcıları çalışmaya devam eder. Mesaj, elementi ve tam engelleyici nedeni adlandırır — örneğin merkez noktayı hangi overlay'in kapattığını, ya da bounding box'ın sürekli hareket ettiğini. Hiçbir şey sessizce yanlış noktaya tıklamaz veya yarı animasyonlu bir alana yazmaz.

## Actionability Kontrollerini Nasıl Yapılandırır veya Atlarım?

```yaml
# testfly.yml
actionability:
  enabled: true           # false, 1.1.0 öncesi bekleme davranışını geri getirir
  stabilityIntervalMs: 100
  hitTargetCheck: true
  autoScroll: true
```

```java
public static final class Actionability {
    private boolean enabled = true;
    private int stabilityIntervalMs = 100;
    private boolean hitTargetCheck = true;
    private boolean autoScroll = true;
}
```

`actionability.enabled: false` ayarı, eski bekleme davranışına döner: click'ler için `waitForClickable`, diğer her şey için `waitForVisible` — bir geçiş sürecinde ya da bir sayfanın gerçekten eski davranışa ihtiyaç duyduğu durumlarda kullanışlıdır.

Tek bir locator için `Locator.force()`, yalnızca sabitlik ve kapatılmamışlık kontrollerini atlar (Playwright'ın `force: true`'su), elementin yine de görünür olmasını ve eylemin gerektirdiği yerde etkin olmasını zorunlu tutar:

```java
find("#banner-close").force().click();
```

`BasePage.doubleClick`, `rightClick`, `selectBy*` ve `BaseConditions.click`/`type`, hepsi aynı `waitForActionable` çağrısından geçer, böylece davranış tüm API yüzeyinde tutarlıdır.

## `assertThat(x).click()` Bir Tıklamanın Gerçekten Ulaştığını Nasıl Doğruluyor?

Düz bir `Locator.click()`, actionability beklemesini yapar ve tıklar — öncekiyle aynı. `assertThat(x).click()` bir adım daha ileri gider ve eylemi bir assertion'a dönüştürür:

```java
assertThat(loginButton).click();
```

1. Elementi `Locator.click()`'in yaptığı gibi çözer (self-healing dahil) ve görünür ve etkin olana kadar bekler.
2. Onu viewport'un merkezine kaydırır ve `Actions` ile gerçek bir kullanıcı gibi fareyi üzerine taşır.
3. Merkez noktasını başka hiçbir şeyin — çerez banner'ı, modal, yapışkan header — kapatmadığı ana kadar bekler.
4. Tıklar, ardından `click` olayının gerçekten elemente ya da onun bir alt elementine ulaştığını assert eder.

Başarı durumunda geçen bir `Clicked: ...` adımı loglanır. Başarısızlıkta, hangi adımın başarısız olduğunu adlandıran normal bir `AssertionError` (ya da soft modda kaydedilmiş bir soft failure) döner. Eğer tıklama, olay dinleyicisi sonucu okuyabilmeden önce sayfadan uzaklaşır ya da bir alert açarsa, tıklama yine de ulaşmış sayılır — sayfa ilerledi diye başarısız olmaz. `Locator.click()`'in kendisi değişmedi; bu, `LocatorAssert` üzerinde eklenen, daha katı bir kardeş metottur.

## `assertThat(x).fill(text)` ve `.clear()` Alanın Gerçekten Değiştiğini Nasıl Doğruluyor?

```java
assertThat(userNameField).fill("admin");
assertThat(passwordField).fill(password);
assertThat(searchField).clear();
```

İkisi de tek bir yolu (`fillAndVerify`) paylaşır:

1. Elementi `Locator.fill(String)`'in yapacağı gibi çözer (bekleme ve self-healing ile) ve görünür ve etkin olana kadar bekler.
2. Alanın `readonly` olmadığı ana kadar bekler, ardından viewport merkezine kaydırır.
3. Alanı temizler ve metni tam olarak `Locator.type(String)`'in yaptığı gibi yazar.
4. Alanın değerinin metne **tam olarak eşit** olup öyle kaldığı ana kadar bekler — `maxlength`, bir input mask ya da framework state'inin (React, Vue…) yazılan değeri sessizce değiştirdiği ya da sıfırladığı durumları yakalar.

`clear()`, boş bir string ile aynı yoldur. `type="password"` alanları için değerin kendisi hiçbir zaman loglanmaz ya da hata mesajına yazılmaz — yalnızca uzunluğu yazılır. Yazarken girişi yeniden biçimlendiren alanlar (örneğin telefon maskeleri) bu daha katı assertion'da başarısız olur; bunlar için `Locator.fill(text)`'i ardından `assertThat(x).hasValue(formatlanmışDeğer)` ile kullanın.

## SSS

### Actionability kontrollerini açmak her testi yavaşlatır mı?

Her eylem, 1.1.0 öncesi bekleyişlere kıyasla yaklaşık bir ekstra polling aralığı (varsayılan 100ms) bekler, çünkü motor bounding box'ın hareketi durdurduğunu doğrulamak için en az iki ardışık sabit polling'e ihtiyaç duyar. JavaScript kullanılamadığında kontroller fail-open olur, yani JS destekli bir oturuma zorunlu bir bağımlılık yoktur.

### `assertThat(x).click()`, `Locator.click()`'in yerini mi alıyor?

Hayır. `Locator.click()` değişmedi ve hâlâ sadece actionability beklemesini yapıp tıklıyor. `assertThat(x).click()`, click olayının elemente ulaştığını da assert eden, geçen bir `Clicked: ...` adımı loglayan ve `as(...)`, `within(...)` ve soft modu destekleyen, eklenmiş ve doğrulanmış bir kardeş metottur.

### `not()`'u doğrulanmış `click()`/`fill()`/`clear()` ile birleştirebilir miyim?

Hayır — bu üç metot `rejectNegation(...)` çağırır ve `not()` kendilerinden önce zincirlenmişse hata fırlatır. Bunlar bir eylemin gerçekleştiğini ve sayfayı doğrulanabilir şekilde değiştirdiğini assert ediyor; iyi tanımlanmış bir "negatif tıklama" yok.

### `force()` ile `actionability.enabled: false` arasındaki fark nedir?

`force()` locator başınadır ve yalnızca sabitlik ve kapatılmamışlık kontrollerini atlar — element yine de görünür olmalı, eylemin gerektirdiği yerde etkin olmalıdır. `actionability.enabled: false` globaldir ve suite'teki her eylemi 1.1.0 öncesi bekleyişlere (`waitForClickable`/`waitForVisible`) geri döndürür, yeni kontrollerin tümünü bir kerede kaldırır.
