# TestFly Part 12: Playwright-Style Assertions in Selenium — not(), ARIA Snapshots & Strict Mode

URL: https://hakangul.lovable.app/blog/testfly-part-12-playwright-style-assertions-aria-snapshots-strict-mode
Author: Hakan Gül — Team Lead Senior Test Automation Engineer
Published: 2026-10-12
Updated: 2026-10-12
Category: test-automation
Tags: TestFly, Selenium, Playwright, Accessibility, ARIA, Java, SDET

> TestFly 1.1.0 brings Playwright for Java's assertion ergonomics to Selenium: a chainable not() negation, deterministic hasRole()/hasAccessibleName() checks, YAML-based matchesAriaSnapshot(), and opt-in strict mode that fails loudly instead of silently clicking the wrong element.

## English

**TL;DR:** TestFly 1.1.0 closes the gap with Playwright for Java's assertion API without leaving Selenium: `LocatorAssert.not()` negates the next check, `hasRole()` / `hasAccessibleName()` / `hasAccessibleDescription()` read the real accessibility tree instead of guessing from DOM attributes, `matchesAriaSnapshot()` compares a subtree against a Playwright-style YAML template, and opt-in `locators.strict` turns an ambiguous locator into a loud failure instead of a silent click on the wrong element.

---

## Why Do Selenium Assertions Need a not() Method?

Negative assertions in classic Selenium/TestNG code usually mean one of two bad options: wrap the positive assertion in a `try/catch` and invert the result, or write a separate polling loop that waits for a condition to stay false. Both bury the real intent of the test in boilerplate.

TestFly's `LocatorAssert` now carries the same `not()` Playwright for Java has — it negates **only the assertion directly after it** in the chain:

```java
import io.testfly.test.BaseTest;
import org.testng.annotations.Test;

import static io.testfly.assertion.Assertions.assertThat;

public class CheckoutErrorTest extends BaseTest {

    @Test
    public void testErrorBannerClearsAfterRetry() {
        open("/checkout");
        find("#pay-btn").click();

        assertThat(find("#error-banner")).hasText("Payment declined");

        find("#retry-btn").click();

        // Only the assertion right after not() is negated
        assertThat(find("#error-banner")).not().isVisible();
        assertThat(find("#spinner")).not().isAttached();
    }
}
```

As in Playwright, most negated assertions also pass when no matching element exists — `not().isVisible()` is satisfied whether the banner is hidden or gone from the DOM entirely. If the test needs the element to exist and just be in a different state, assert presence first: `assertThat(status).isVisible().not().hasText("Error")`. `not()` cannot be combined with the verified actions `click()`, `fill()` or `clear()`.

## What Are Deterministic Accessibility Assertions?

TestFly already shipped `hasAriaLabel`-style attribute checks. 1.1.0 adds assertions that read the **computed** accessibility tree the same way a screen reader would, through one bundled probe script — no axe-core injection, no LLM call:

```java
@Test
public void testSubmitButtonIsAccessible() {
    open("/signup");

    assertThat(find("#submit"))
        .hasRole(Role.BUTTON)
        .hasAccessibleName("Create account");

    assertThat(find("#email"))
        .hasAccessibleDescription(Pattern.compile("(?i)required"));
}
```

`hasRole(Role | String)` checks the explicit `role` attribute or the implicit role of the HTML tag. `hasAccessibleName` and `hasAccessibleDescription` accept either an exact `String` (whitespace collapsed, same matching `Locator.withName(...)` uses) or a `Pattern` for a partial/case-insensitive match. Only the 36 standard ARIA `Role` values and visible text are reported; decorative images and hidden content are skipped, and Shadow DOM is not entered.

## How Do ARIA Snapshots Work?

`matchesAriaSnapshot()` is TestFly's answer to Playwright's `toMatchAriaSnapshot()`: it captures the accessibility subtree under a locator and compares it against a YAML template — a **partial, ordered** constraint, not a full serialization diff.

```java
@Test
public void testNavigationLandmarkStructure() {
    open("/dashboard");

    assertThat(By.tagName("nav")).matchesAriaSnapshot("""
        - link "Home"
        - link "Docs"
        - button "Account" [expanded=false]
        """);
}
```

Matching rules, in the style of Playwright:

| Rule | Behavior |
|---|---|
| Role | must match exactly |
| Quoted name | case-insensitive substring match |
| `/regex/` name | matched with `find()` |
| `[attribute]` / `[level=2]` | every template attribute must be present on the page; extra page attributes are ignored |

A malformed template fails immediately with `IllegalArgumentException`, before waiting on the timeout. On failure, the assertion error includes both the template and the captured snapshot, so the diff is visible without re-running anything. To write a template in the first place, print what TestFly currently sees:

```java
System.out.println(find("nav").ariaSnapshot());
// or, for the whole page:
System.out.println(ariaSnapshot());
```

and paste the printed YAML into the test, trimmed to only the nodes that matter.

## How Does Strict Mode Prevent Silent Multi-Element Bugs?

By default, when a locator resolves to several visible elements, TestFly — like classic Selenium — silently clicks or reads the first one. That is exactly how a renamed CSS class or an A/B-tested duplicate banner turns into a test that passes against the wrong element for weeks before anyone notices.

Opt in per project with:

```yaml
# testfly.yml
locators:
  strict: true
```

With `locators.strict: true`, an action on a locator matching more than one **visible** element — and not narrowed with `first()`, `nth()` or `last()` — now throws immediately:

```
Strict mode violation: css=".card .btn-buy" resolved to 3 visible elements;
use first()/nth()/last() or narrow the locator. Candidates: <button.btn-buy#card-1>, <button.btn-buy#card-2>, <button.btn-buy#card-3>
```

`isVisible()` and `isEnabled()` surface the same violation instead of silently checking the first match; `count()`, `elements()` and `assertThat(...)` (for counting/enumerating) are unaffected, since ambiguity is the expected case there. Narrow the locator explicitly once strict mode flags it:

```java
find(".card .btn-buy").nth(1).click();          // second card, explicit
find(".card").filter(withText("Pro Plan"))
             .find(".btn-buy").click();          // narrowed by content instead
```

Hidden duplicates — a closed mobile nav sitting next to the desktop one, a hidden modal clone — are filtered out before the check runs, so responsive layouts don't trigger false strict-mode violations.

## FAQ

### Does `not()` replace the existing Selenium assertion methods?
No. `not()` is a modifier on `LocatorAssert` — it negates whichever assertion comes right after it in the same chain. All existing positive assertions are unchanged.

### Do the accessibility assertions require axe-core or an external library?
No. `hasRole`, `hasAccessibleName`, `hasAccessibleDescription` and `matchesAriaSnapshot` use one bundled probe script that reads the browser's computed accessibility tree directly — no axe-core injection and no LLM call.

### Will enabling `locators.strict` break existing tests?
It can, by design — that is the point. Any locator that was silently matching more than one visible element before now fails explicitly. The fix is the same one Playwright recommends: narrow the locator with `first()`, `nth()`, `last()`, or a more specific selector.

### Does strict mode affect `count()` or `elements()`?
No. Those methods exist specifically to work with multiple matches, so strict mode does not apply to them — only to actions (`click`, `fill`, …) and the boolean checks `isVisible()`/`isEnabled()`.

## Türkçe

**Özet:** TestFly 1.1.0, Selenium'dan ayrılmadan Playwright for Java'nın assertion API'sine olan farkı kapatıyor: `LocatorAssert.not()` zincirdeki bir sonraki kontrolü negatife çeviriyor, `hasRole()` / `hasAccessibleName()` / `hasAccessibleDescription()` DOM attribute'larından tahmin etmek yerine gerçek erişilebilirlik ağacını okuyor, `matchesAriaSnapshot()` bir alt ağacı Playwright tarzı bir YAML şablonuyla karşılaştırıyor ve opsiyonel `locators.strict`, belirsiz bir locator'ı sessizce yanlış elemente tıklamak yerine yüksek sesle hataya çeviriyor.

---

## Selenium Assertion'ları Neden not() Metoduna İhtiyaç Duyuyor?

Klasik Selenium/TestNG kodunda negatif assertion'lar genelde iki kötü seçenekten birine dönüşür: pozitif assertion'ı `try/catch` içine alıp sonucu ters çevirmek, ya da bir koşulun yanlış kalmasını bekleyen ayrı bir polling döngüsü yazmak. İkisi de testin asıl niyetini boilerplate'in içine gömer.

TestFly'ın `LocatorAssert`'i artık Playwright for Java'daki aynı `not()`'a sahip — zincirde **yalnızca kendisinden hemen sonra gelen** assertion'ı negatife çeviriyor:

```java
import io.testfly.test.BaseTest;
import org.testng.annotations.Test;

import static io.testfly.assertion.Assertions.assertThat;

public class CheckoutErrorTest extends BaseTest {

    @Test
    public void testErrorBannerClearsAfterRetry() {
        open("/checkout");
        find("#pay-btn").click();

        assertThat(find("#error-banner")).hasText("Payment declined");

        find("#retry-btn").click();

        // Yalnızca not()'tan hemen sonraki assertion negatife döner
        assertThat(find("#error-banner")).not().isVisible();
        assertThat(find("#spinner")).not().isAttached();
    }
}
```

Playwright'ta olduğu gibi, negatife çevrilmiş assertion'ların çoğu eşleşen bir eleman hiç olmadığında da geçer — `not().isVisible()`, banner gizli olsa da DOM'dan tamamen kaldırılmış olsa da sağlanır. Eğer test elementin var olmasını ama farklı bir durumda olmasını istiyorsa, önce varlığı doğrula: `assertThat(status).isVisible().not().hasText("Error")`. `not()`, doğrulanmış `click()`, `fill()` veya `clear()` aksiyonlarıyla birlikte kullanılamaz.

## Deterministik Erişilebilirlik Assertion'ları Nedir?

TestFly zaten `hasAriaLabel` tarzı attribute kontrolleri sunuyordu. 1.1.0, bir ekran okuyucunun göreceği şekilde **hesaplanmış** erişilebilirlik ağacını okuyan assertion'lar ekliyor; tek bir gömülü probe script üzerinden — axe-core enjeksiyonu yok, LLM çağrısı yok:

```java
@Test
public void testSubmitButtonIsAccessible() {
    open("/signup");

    assertThat(find("#submit"))
        .hasRole(Role.BUTTON)
        .hasAccessibleName("Create account");

    assertThat(find("#email"))
        .hasAccessibleDescription(Pattern.compile("(?i)required"));
}
```

`hasRole(Role | String)`, açık `role` attribute'unu ya da HTML etiketinin örtük rolünü kontrol eder. `hasAccessibleName` ve `hasAccessibleDescription`, ya tam bir `String` (boşluklar sadeleştirilmiş, `Locator.withName(...)`'ın kullandığı eşleşmeyle aynı) ya da kısmi/büyük-küçük harf duyarsız eşleşme için bir `Pattern` kabul eder. Yalnızca 36 standart ARIA `Role` değeri ve görünür metin raporlanır; dekoratif görseller ve gizli içerik atlanır, Shadow DOM'a girilmez.

## ARIA Snapshot'lar Nasıl Çalışır?

`matchesAriaSnapshot()`, TestFly'ın Playwright'ın `toMatchAriaSnapshot()`'ına cevabı: bir locator'ın altındaki erişilebilirlik alt ağacını yakalar ve bunu bir YAML şablonuyla karşılaştırır — tam bir serileştirme diff'i değil, **kısmi ve sıralı** bir kısıttır.

```java
@Test
public void testNavigationLandmarkStructure() {
    open("/dashboard");

    assertThat(By.tagName("nav")).matchesAriaSnapshot("""
        - link "Home"
        - link "Docs"
        - button "Account" [expanded=false]
        """);
}
```

Playwright tarzı eşleşme kuralları:

| Kural | Davranış |
|---|---|
| Rol | tam olarak eşleşmeli |
| Tırnaklı isim | büyük/küçük harf duyarsız alt dize eşleşmesi |
| `/regex/` isim | `find()` ile eşleştirilir |
| `[attribute]` / `[level=2]` | şablondaki her attribute sayfada bulunmalı; sayfadaki fazla attribute'lar yok sayılır |

Bozuk bir şablon, timeout'u beklemeden hemen `IllegalArgumentException` ile hata verir. Başarısızlık durumunda assertion hatası hem şablonu hem de yakalanan snapshot'ı içerir, böylece hiçbir şeyi yeniden çalıştırmadan fark görülebilir. Baştan bir şablon yazmak için, TestFly'ın o an ne gördüğünü yazdır:

```java
System.out.println(find("nav").ariaSnapshot());
// ya da tüm sayfa için:
System.out.println(ariaSnapshot());
```

ve yazdırılan YAML'ı, yalnızca önemli düğümlere indirgeyerek teste yapıştır.

## Strict Mode Sessiz Çoklu-Eleman Hatalarını Nasıl Önler?

Varsayılan olarak, bir locator birden fazla görünür elemente eşleştiğinde, TestFly — klasik Selenium gibi — sessizce ilkini tıklar ya da okur. Yeniden adlandırılmış bir CSS sınıfının ya da A/B testi yapılan bir duplicate banner'ın, kimse fark etmeden haftalarca yanlış elemente karşı geçen bir teste dönüşmesinin tam sebebi budur.

Proje başına açılır:

```yaml
# testfly.yml
locators:
  strict: true
```

`locators.strict: true` ile, birden fazla **görünür** elemente eşleşen ve `first()`, `nth()` veya `last()` ile daraltılmamış bir locator üzerindeki aksiyon artık hemen hata fırlatır:

```
Strict mode violation: css=".card .btn-buy" resolved to 3 visible elements;
use first()/nth()/last() or narrow the locator. Candidates: <button.btn-buy#card-1>, <button.btn-buy#card-2>, <button.btn-buy#card-3>
```

`isVisible()` ve `isEnabled()`, ilk eşleşmeyi sessizce kontrol etmek yerine aynı ihlali yüzeye çıkarır; `count()`, `elements()` ve `assertThat(...)` (sayma/numaralandırma için) etkilenmez, çünkü belirsizlik orada beklenen durumdur. Strict mode bir ihlali işaretlediğinde locator'ı açıkça daralt:

```java
find(".card .btn-buy").nth(1).click();          // ikinci kart, açıkça belirtilmiş
find(".card").filter(withText("Pro Plan"))
             .find(".btn-buy").click();          // içeriğe göre daraltılmış
```

Gizli duplicate'ler — masaüstü navigasyonun yanında duran kapalı bir mobil nav, gizli bir modal klonu — kontrol çalışmadan önce filtrelenir, böylece responsive layout'lar yanlış strict-mode ihlallerine yol açmaz.

## SSS

### not(), mevcut Selenium assertion metodlarının yerini mi alıyor?
Hayır. `not()`, `LocatorAssert` üzerinde bir değiştiricidir — aynı zincirde kendisinden hemen sonra gelen assertion'ı negatife çevirir. Mevcut tüm pozitif assertion'lar değişmeden kalır.

### Erişilebilirlik assertion'ları axe-core ya da harici bir kütüphane gerektiriyor mu?
Hayır. `hasRole`, `hasAccessibleName`, `hasAccessibleDescription` ve `matchesAriaSnapshot`, tarayıcının hesaplanmış erişilebilirlik ağacını doğrudan okuyan tek bir gömülü probe script kullanır — axe-core enjeksiyonu ve LLM çağrısı yoktur.

### locators.strict'i açmak mevcut testleri bozar mı?
Bozabilir, bu tasarım gereği — asıl amaç da bu. Daha önce sessizce birden fazla görünür elemente eşleşen her locator artık açıkça hata verir. Çözüm, Playwright'ın da önerdiği aynı çözüm: locator'ı `first()`, `nth()`, `last()` ya da daha spesifik bir seçiciyle daraltmak.

### Strict mode, count() veya elements()'i etkiler mi?
Hayır. Bu metodlar özellikle birden fazla eşleşmeyle çalışmak için var, bu yüzden strict mode onlara uygulanmaz — yalnızca aksiyonlara (`click`, `fill`, …) ve boolean kontrollere (`isVisible()`/`isEnabled()`) uygulanır.
