# TestFly Part 14: Step Traces — Per-Step ARIA/DOM Snapshots for Visual Debugging

URL: https://hakangul.lovable.app/blog/testfly-part-14-step-traces-aria-dom-snapshots-debugging
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, Debugging

> TestFly 1.1.0 adds per-step snapshots to the trace: every step records the accessibility tree (or the pruned DOM) as it looked when it started, a failed test also captures the page after its last step with a before/after diff, and Cucumber scenarios now get traces too.

## English

# TestFly Part 14: Step Traces — Per-Step ARIA/DOM Snapshots for Visual Debugging

**TL;DR:** TestFly 1.1.0 adds per-step snapshots to the trace: every step now records the
accessibility tree (or, with `dom`/`both`, the pruned page HTML) as it looked when the step
started, and a failed test also records the page after its last step with a before/after diff.
New config keys `tracing.snapshots`, `tracing.snapshotMaxKb` and `tracing.snapshotOnFailureAfter`
control what's captured, `StepRecord` carries the new fields, and `TraceRecorder.saveIfEnabled`
writes the trace — now from TestNG, JUnit 5, *and* Cucumber, with the HTML report linking to it
from every test.

## What Is a Step Trace, and Why Snapshot Every Step?

A trace is one self-contained HTML file — `target/traces/{ClassName}/{testMethod}-trace.html` —
that replays a test's step timeline: step names, offsets, screenshots, the final failure
screenshot, the error and stack trace, and the video if recording is on. You open it in any
browser; it needs no server.

Screenshots tell you what the page *looked* like. They don't tell you what was actually in the
DOM — whether a button was disabled, whether an element had `aria-hidden`, whether the markup you
expected was even there. That's the gap per-step snapshots close: alongside each step's
screenshot, the trace now embeds a text snapshot of the page exactly as `StepSnapshotCapture`
saw it the moment the step began.

```yaml
# testfly.yml
tracing:
  enabled: true
```

With tracing on, every step gets a snapshot by default — no extra code in the test.

## What Does Each Step's Snapshot Contain?

Capture is driven by `StepSnapshotCapture.captureForStep()`, called once per step. It checks
`tracing.enabled`, reads the `tracing.snapshots` mode, and only touches the browser if a driver
session is actually alive — API and database tests, or steps logged before a browser exists,
simply get no snapshot:

```java
// StepSnapshotCapture.java (simplified)
public static Snapshots captureForStep() {
    TestFlyConfig.Tracing tracing = tracing();
    if (tracing == null || !tracing.isEnabled()) {
        return Snapshots.NONE;
    }
    String mode = tracing.getSnapshots();
    boolean aria = mode.equals("aria") || mode.equals("both");
    boolean dom  = mode.equals("dom")  || mode.equals("both");
    WebDriver driver = liveDriver();
    if (driver == null) {
        return Snapshots.NONE;
    }
    int maxKb = tracing.getSnapshotMaxKb();
    return new Snapshots(aria ? captureAria(driver, maxKb) : null,
                          dom  ? captureDom(driver, maxKb)  : null);
}
```

`StepRecord` — the object that already held a step's name, offset, status and screenshot — grew
two new fields plus one that's filled in later, for a failed test's final state:

```java
// StepRecord.java
public StepRecord(String name, long offsetMs, String status, String screenshotBase64,
                   String ariaSnapshot, String domSnapshot) { ... }

public String getAriaSnapshot()      { return ariaSnapshot; }
public String getDomSnapshot()       { return domSnapshot; }
public String getAfterAriaSnapshot() { return afterAriaSnapshot; } // set only on failure
```

`aria` captures the same YAML text `ariaSnapshot()` prints — small, readable, unaffected by CSS
class churn. `dom` captures the page HTML, pruned of scripts, styles and decoration by
`DomPruner`; it's bigger but shows markup the accessibility tree leaves out, like a CSS class
you're debugging.

## How Do You Configure What Gets Captured?

```yaml
# testfly.yml
tracing:
  enabled: true
  snapshots: both             # none | aria | dom | both — default: aria
  snapshotMaxKb: 256           # longest snapshot kept per step, in KB — default: 256
  snapshotOnFailureAfter: true # also snapshot the page after the last step of a failed test
```

`TestFlyConfig` validates `tracing.snapshots` eagerly — anything other than `none`, `aria`, `dom`
or `both` throws `IllegalArgumentException` at config-load time, not mid-run. A snapshot longer
than `snapshotMaxKb` is cut and ends with `… truncated (N more characters)`, so one verbose page
never bloats the whole trace file.

## What Does the Trace HTML Actually Show You?

Click a step in the timeline and the screenshot panel opens with tabs: `ARIA (before step)`,
`DOM (before step)`, and — only on the last step of a failed test — `ARIA (after failure)` and a
computed `Diff` tab. The diff is a plain line-based set difference between the before and after
YAML, rendered client-side in the trace's inlined JavaScript (`lineDiff()` in `TraceRecorder`):
lines only in the "before" snapshot are marked `-`, lines only in "after" are marked `+`. No
external diff library, no network call — the trace is one file you can email or drop in Slack.

Step blocks in the timeline also get a small 🌳 marker when they carry a snapshot, next to the
📷 marker for a screenshot, so you can see at a glance which steps have something to click into.

## Does This Slow Down or Risk Failing My Tests?

Capturing a snapshot is one script call per step — a few milliseconds on a typical page — and it
only runs when `tracing.enabled` is on. `StepSnapshotCapture` wraps every capture in a
`try/catch` that returns `null` on failure rather than propagating: a page that can't be read, a
closed driver, or a flaky script call never fails the test, it just leaves that step without a
snapshot. `TraceRecorder.saveIfEnabled` itself is wrapped the same way:

```java
// TraceRecorder.java
@TestFlyApi(since = "1.1.0")
public static void saveIfEnabled(String testId, String testName, boolean isPassing) {
    try {
        TestFlyConfig.Tracing tracing = TestFlyContext.getConfig().getTracing();
        if (tracing == null || !tracing.isEnabled()) return;
        if (isPassing && !tracing.isCaptureOnPass()) return;
        save(testId, testName);
    } catch (Exception ignored) {
    }
}
```

## Does This Work With Cucumber?

Yes — as of 1.1.0, `CucumberHooks` calls the same trace machinery TestNG and JUnit 5 use, so
Gherkin scenarios get a trace file too, snapshots included, with no change to your feature files
or step definitions. The HTML test report also links directly to each test's trace from its
detail panel, so you go from a red row in the report to the exact step and snapshot that shows
what went wrong, in one click.

## FAQ

### Do I need to add code to my tests to get per-step snapshots?

No. With `tracing.enabled: true`, every step captures a snapshot automatically according to
`tracing.snapshots` (default `aria`). There's no per-step API to call.

### What happens if a test has no browser, like an API test?

Nothing is captured. `StepSnapshotCapture` checks for a live `WebDriver` session before taking a
snapshot and returns no snapshot if none exists — it never creates or recreates a driver just to
capture one.

### Can a snapshot capture failure cause my test to fail?

No. Every capture path is wrapped so an exception is swallowed and logged at `FINE` level, never
rethrown. A snapshot is best-effort; it never affects the pass/fail verdict.

### Is the diff tab a real structural diff of the accessibility tree?

No — it's a line-based set difference between the "before" and "after" ARIA YAML text, computed
in the trace's own inline JavaScript. It's enough to spot what appeared or disappeared, not a
semantic tree diff.

## Türkçe

# TestFly Part 14: Adım İzleri — Görsel Hata Ayıklama İçin Adım Başına ARIA/DOM Snapshot'ları

**Özet:** TestFly 1.1.0, trace'e adım başına snapshot ekliyor: artık her adım, başladığı andaki
erişilebilirlik ağacını (ya da `dom`/`both` ile sayfanın budanmış HTML'ini) kaydediyor, başarısız
bir test ise son adımından sonraki sayfayı da kaydedip öncesiyle bir diff gösteriyor. Yeni config
anahtarları `tracing.snapshots`, `tracing.snapshotMaxKb` ve `tracing.snapshotOnFailureAfter` neyin
yakalanacağını belirliyor, `StepRecord` yeni alanları taşıyor, `TraceRecorder.saveIfEnabled` trace'i
yazıyor — artık yalnızca TestNG ve JUnit 5'ten değil, Cucumber'dan da; HTML rapor da her testten
trace'e doğrudan bağlantı veriyor.

## Adım İzi Nedir, Her Adımı Neden Snapshot'lıyoruz?

Bir trace, bir testin adım zaman çizelgesini yeniden oynatan tek bir bağımsız HTML dosyasıdır —
`target/traces/{SınıfAdı}/{testMetodu}-trace.html`. Adım isimleri, zaman damgaları, ekran görüntüleri,
hatanın anındaki son ekran görüntüsü, hata mesajı ve stack trace, kayıt açıksa video — hepsi burada.
Tek dosyayı herhangi bir tarayıcıda açarsın; sunucuya ihtiyaç yok.

Ekran görüntüleri sayfanın *nasıl göründüğünü* gösterir. DOM'da gerçekte ne olduğunu göstermez —
bir buton devre dışı mıydı, bir elementte `aria-hidden` var mıydı, beklediğin markup orada mıydı.
Adım başına snapshot'ların kapattığı boşluk tam da bu: her adımın ekran görüntüsünün yanında, trace
artık `StepSnapshotCapture`'ın adım başladığı anda gördüğü sayfanın metin snapshot'ını da gömüyor.

```yaml
# testfly.yml
tracing:
  enabled: true
```

Tracing açıkken, her adım varsayılan olarak bir snapshot alır — teste ek kod gerekmez.

## Her Adımın Snapshot'ı Ne İçeriyor?

Yakalama, adım başına bir kez çağrılan `StepSnapshotCapture.captureForStep()` tarafından yönetiliyor.
`tracing.enabled`'i kontrol ediyor, `tracing.snapshots` modunu okuyor ve yalnızca gerçekten canlı bir
driver oturumu varsa tarayıcıya dokunuyor — API ve veritabanı testleri, ya da tarayıcı oluşmadan önce
loglanan adımlar, hiç snapshot almıyor:

```java
// StepSnapshotCapture.java (sadeleştirilmiş)
public static Snapshots captureForStep() {
    TestFlyConfig.Tracing tracing = tracing();
    if (tracing == null || !tracing.isEnabled()) {
        return Snapshots.NONE;
    }
    String mode = tracing.getSnapshots();
    boolean aria = mode.equals("aria") || mode.equals("both");
    boolean dom  = mode.equals("dom")  || mode.equals("both");
    WebDriver driver = liveDriver();
    if (driver == null) {
        return Snapshots.NONE;
    }
    int maxKb = tracing.getSnapshotMaxKb();
    return new Snapshots(aria ? captureAria(driver, maxKb) : null,
                          dom  ? captureDom(driver, maxKb)  : null);
}
```

Zaten bir adımın adını, zaman damgasını, durumunu ve ekran görüntüsünü tutan `StepRecord`, iki yeni
alan artı bir tane daha aldı; o da yalnızca başarısız bir test için sonradan doldurulan son durum:

```java
// StepRecord.java
public StepRecord(String name, long offsetMs, String status, String screenshotBase64,
                   String ariaSnapshot, String domSnapshot) { ... }

public String getAriaSnapshot()      { return ariaSnapshot; }
public String getDomSnapshot()       { return domSnapshot; }
public String getAfterAriaSnapshot() { return afterAriaSnapshot; } // yalnızca başarısızlıkta
```

`aria`, `ariaSnapshot()`'ın yazdırdığı aynı YAML metnini yakalıyor — küçük, okunabilir, CSS sınıfı
değişikliklerinden etkilenmiyor. `dom`, `DomPruner` tarafından script'lerden, stillerden ve
dekorasyondan budanmış sayfa HTML'ini yakalıyor; daha büyük ama erişilebilirlik ağacının göstermediği
markup'ı gösteriyor — örneğin hata ayıkladığın bir CSS sınıfını.

## Neyin Yakalanacağını Nasıl Yapılandırırsın?

```yaml
# testfly.yml
tracing:
  enabled: true
  snapshots: both             # none | aria | dom | both — varsayılan: aria
  snapshotMaxKb: 256           # adım başına tutulan en uzun snapshot, KB — varsayılan: 256
  snapshotOnFailureAfter: true # başarısız testin son adımından sonraki sayfayı da yakala
```

`TestFlyConfig`, `tracing.snapshots`'ı erken doğrular — `none`, `aria`, `dom` veya `both` dışında
bir değer, çalışma sırasında değil config yüklenirken `IllegalArgumentException` fırlatır.
`snapshotMaxKb`'den uzun bir snapshot kesilir ve `… truncated (N more characters)` ile biter, böylece
tek bir ayrıntılı sayfa tüm trace dosyasını şişirmez.

## Trace HTML'i Gerçekte Ne Gösteriyor?

Zaman çizelgesinde bir adıma tıkla, ekran görüntüsü paneli sekmelerle açılır: `ARIA (before step)`,
`DOM (before step)`, ve — yalnızca başarısız bir testin son adımında — `ARIA (after failure)` ve
hesaplanmış bir `Diff` sekmesi. Diff, öncesi ve sonrası YAML'ları arasında satır bazlı bir küme farkı;
trace'in kendi gömülü JavaScript'inde (`TraceRecorder`'daki `lineDiff()`) istemci tarafında hesaplanır:
yalnızca "öncesi"nde olan satırlar `-` ile, yalnızca "sonrası"nda olanlar `+` ile işaretlenir. Harici
bir diff kütüphanesi yok, ağ çağrısı yok — trace, e-postayla gönderebileceğin ya da Slack'e
bırakabileceğin tek bir dosya olarak kalıyor.

Zaman çizelgesindeki adım blokları da snapshot taşıdıklarında küçük bir 🌳 işareti alır, ekran
görüntüsü için 📷 işaretinin yanında — hangi adımların tıklanacak bir şeye sahip olduğunu bir bakışta
görürsün.

## Bu Testlerimi Yavaşlatır ya da Başarısız Olma Riski Taşır Mı?

Bir snapshot almak adım başına bir script çağrısı — tipik bir sayfada birkaç milisaniye — ve yalnızca
`tracing.enabled` açıkken çalışır. `StepSnapshotCapture`, her yakalamayı başarısızlıkta hata fırlatmak
yerine `null` döndüren bir `try/catch`'e sarıyor: okunamayan bir sayfa, kapanmış bir driver ya da
kararsız bir script çağrısı testi asla başarısız kılmaz, yalnızca o adımı snapshot'sız bırakır.
`TraceRecorder.saveIfEnabled`'ın kendisi de aynı şekilde sarmalanmış:

```java
// TraceRecorder.java
@TestFlyApi(since = "1.1.0")
public static void saveIfEnabled(String testId, String testName, boolean isPassing) {
    try {
        TestFlyConfig.Tracing tracing = TestFlyContext.getConfig().getTracing();
        if (tracing == null || !tracing.isEnabled()) return;
        if (isPassing && !tracing.isCaptureOnPass()) return;
        save(testId, testName);
    } catch (Exception ignored) {
    }
}
```

## Bu Cucumber İle Çalışıyor Mu?

Evet — 1.1.0 itibarıyla `CucumberHooks`, TestNG ve JUnit 5'in kullandığı aynı trace mekanizmasını
çağırıyor, bu yüzden Gherkin senaryoları da snapshot'lar dahil bir trace dosyası alıyor; feature
dosyalarında ya da step definition'larda hiçbir değişiklik gerekmiyor. HTML test raporu da artık her
testin detay panelinden doğrudan kendi trace'ine bağlantı veriyor, yani raporda kırmızı bir satırdan,
neyin yanlış gittiğini gösteren tam adıma ve snapshot'a tek tıkla gidiyorsun.

## SSS

### Adım başına snapshot almak için testlerime kod eklemem gerekiyor mu?

Hayır. `tracing.enabled: true` ile, her adım `tracing.snapshots`'a göre (varsayılan `aria`) otomatik
olarak bir snapshot alır. Adım başına çağrılacak bir API yok.

### Tarayıcısı olmayan bir testte, örneğin bir API testinde ne olur?

Hiçbir şey yakalanmaz. `StepSnapshotCapture`, snapshot almadan önce canlı bir `WebDriver` oturumu
olup olmadığını kontrol eder ve yoksa snapshot almadan geçer — yalnızca bir tane yakalamak için asla
bir driver oluşturmaz ya da yeniden oluşturmaz.

### Bir snapshot yakalama hatası testimi başarısız kılabilir mi?

Hayır. Her yakalama yolu, bir istisnanın yutulup `FINE` seviyesinde loglandığı, asla yeniden
fırlatılmadığı şekilde sarmalanmıştır. Bir snapshot en iyi çaba ilkesiyle alınır; geçti/kaldı
sonucunu hiçbir zaman etkilemez.

### Diff sekmesi erişilebilirlik ağacının gerçek bir yapısal farkı mı?

Hayır — "öncesi" ve "sonrası" ARIA YAML metni arasında, trace'in kendi gömülü JavaScript'inde
hesaplanan satır bazlı bir küme farkıdır. Neyin ortaya çıktığını ya da kaybolduğunu görmek için
yeterlidir, anlamsal bir ağaç farkı değildir.
