DEV Community

Cover image for 🧩Runtime Snapshots #8 — From Bug Reports to Automated Regression: A QA Pipeline
Alechko
Alechko

Posted on

🧩Runtime Snapshots #8 — From Bug Reports to Automated Regression: A QA Pipeline

TL;DR: Stop writing "button doesn't work" in bug reports. Start capturing the actual DOM state. This post shows how to integrate runtime snapshots into QA workflows — from manual bug reports to fully automated Playwright pipelines.


The Bug Report That Wastes Everyone's Time

Every QA engineer has written this:

Bug: Submit button not clickable
URL: /checkout
Steps: Fill form → click Submit → nothing happens
Expected: Form submits
Actual: Button appears active but doesn't respond
Enter fullscreen mode Exit fullscreen mode

Developer response: "Works on my machine."

Two hours of back-and-forth later, someone finally discovers: the button has pointer-events: none from a CSS conflict that only triggers after async validation.

The problem isn't QA's description. It's that LLMs and developers can't see what QA saw.


Level 1: Bug Reports With Context

The simplest improvement: capture DOM state alongside the bug.

Instead of describing what you see, capture what's actually there:

{
  "metadata": { "url": "https://app.example.com/checkout" },
  "nodes": {
    "#submit-btn": {
      "tag": "BUTTON",
      "text": "Submit",
      "attrs": { "disabled": "true" }
    }
  },
  "details": {
    "#submit-btn": {
      "styles": { "pointer-events": "none", "opacity": "0.5" },
      "hints": { "interactive": false, "focusable": false }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now the developer sees immediately:

  • disabled="true" — button is programmatically disabled
  • pointer-events: none — CSS blocks clicks
  • interactive: false — computed accessibility state

No reproduction needed. The state is captured.

Bug Report Template

## Bug: Submit button not clickable

**URL:** https://app.example.com/checkout
**Browser:** Firefox 120

**DOM Context (SiFR):**
[paste JSON here]

**Analysis:** Button has disabled=true and pointer-events: none.
Likely cause: async validation hasn't completed.
Enter fullscreen mode Exit fullscreen mode

Time saved per bug: 30 minutes to 2 hours.


Level 2: AI-Assisted Debugging

With structured DOM context, LLMs stop guessing.

The Prompt Pattern

Here's the DOM context of a problematic element in SiFR format.

Analyze and tell me:
1. Why is this element not interactive?
2. What CSS/JS changes would fix it?
3. Are there accessibility violations?

[SiFR JSON]
Enter fullscreen mode Exit fullscreen mode

What Changes

Without context:

"The button might be disabled. Check if there's a disabled attribute or CSS pointer-events. Also verify JavaScript event handlers..."

With SiFR context:

"The button is disabled because:

  1. disabled="true" attribute is set
  2. pointer-events: none in computed styles
  3. Parent form has aria-busy="true"

Fix: The async validation sets aria-busy but never clears it on success. Add form.removeAttribute('aria-busy') after validation resolves."

QA Prompt Library

Task Prompt
Debug interactivity "Why is this element not responding to clicks?"
Layout issues "Why is this element positioned incorrectly? Analyze the layout data."
Accessibility audit "Check this element for WCAG 2.1 AA violations."
Selector generation "Generate a stable CSS selector for this element that won't break with minor DOM changes."
Regression detection "Compare these two snapshots and list all functional differences."

Level 3: Playwright Integration

Here's where it gets interesting. Runtime snapshots + test automation = regression detection that actually works.

Capturing SiFR in Playwright

async function captureSiFR(page, selector = 'body', preset = 'normal') {
  return await page.evaluate(({ selector, preset }) => {
    return new Promise((resolve, reject) => {
      const requestId = Date.now().toString();
      const timeout = setTimeout(() => reject(new Error('Capture timeout')), 5000);

      document.addEventListener('e2llm-capture-response', (e) => {
        if (e.detail.requestId === requestId) {
          clearTimeout(timeout);
          resolve(e.detail.data);
        }
      }, { once: true });

      document.dispatchEvent(new CustomEvent('e2llm-capture-request', {
        detail: { requestId, selector, options: { preset } }
      }));
    });
  }, { selector, preset });
}
Enter fullscreen mode Exit fullscreen mode

Use Case 1: Selector Generation

Flaky selectors kill test suites. SiFR captures what makes elements unique:

async function getStableSelectors(page, url) {
  await page.goto(url);
  const sifr = await captureSiFR(page);

  // Extract selectors for high-salience interactive elements
  return Object.entries(sifr.nodes)
    .filter(([_, node]) => node.salience === 'HIGH')
    .filter(([id]) => sifr.details[id]?.hints?.interactive)
    .map(([id, node]) => ({
      selector: id,
      tag: node.tag,
      text: node.text?.slice(0, 50),
      dataOid: node.oid
    }));
}

// Result:
// [
//   { selector: "#checkout-submit", tag: "BUTTON", text: "Complete Purchase" },
//   { selector: "[data-oid='nav-cart']", tag: "A", text: "Cart (3)" }
// ]
Enter fullscreen mode Exit fullscreen mode

Use Case 2: DOM Regression Testing

Catch structural changes before they hit production:

async function detectRegressions(page, url, baseline) {
  await page.goto(url);
  const current = await captureSiFR(page);

  const diff = { added: [], removed: [], changed: [] };

  // Compare interactive elements
  const baseButtons = new Set(baseline.summary?.interactive?.buttons || []);
  const currButtons = new Set(current.summary?.interactive?.buttons || []);

  for (const btn of currButtons) {
    if (!baseButtons.has(btn)) {
      diff.added.push({ type: 'button', selector: btn });
    }
  }

  for (const btn of baseButtons) {
    if (!currButtons.has(btn)) {
      diff.removed.push({ type: 'button', selector: btn });
    }
  }

  // Compare patterns (lists, grids)
  const basePatterns = baseline.summary?.patterns || {};
  const currPatterns = current.summary?.patterns || {};

  for (const [id, pattern] of Object.entries(currPatterns)) {
    const base = basePatterns[id];
    if (base && pattern.count !== base.count) {
      diff.changed.push({
        type: 'pattern_count',
        id,
        was: base.count,
        now: pattern.count
      });
    }
  }

  return diff;
}
Enter fullscreen mode Exit fullscreen mode

Use Case 3: Layout Regression

Detect when elements shift unexpectedly:

async function checkLayoutRegression(page, url, baselineDetails, threshold = 5) {
  await page.goto(url);
  const sifr = await captureSiFR(page, 'body', 'visual');
  const issues = [];

  for (const [id, baseline] of Object.entries(baselineDetails)) {
    const current = sifr.details[id];
    if (!current?.layout) continue;

    const dx = Math.abs(current.layout.x - baseline.layout.x);
    const dy = Math.abs(current.layout.y - baseline.layout.y);
    const dw = Math.abs(current.layout.w - baseline.layout.w);
    const dh = Math.abs(current.layout.h - baseline.layout.h);

    if (dx > threshold || dy > threshold) {
      issues.push({
        element: id,
        issue: 'position_shift',
        delta: { x: dx, y: dy }
      });
    }

    if (dw > threshold || dh > threshold) {
      issues.push({
        element: id,
        issue: 'size_change', 
        delta: { width: dw, height: dh }
      });
    }
  }

  return issues;
}
Enter fullscreen mode Exit fullscreen mode

Use Case 4: Accessibility Scanning

Automated a11y checks from runtime state:

async function scanAccessibility(page, url) {
  await page.goto(url);
  const sifr = await captureSiFR(page);
  const violations = [];

  for (const [id, node] of Object.entries(sifr.nodes)) {
    const details = sifr.details[id];

    // Interactive element without accessible name
    if (details?.hints?.interactive && !node.text && !node.attrs?.['aria-label']) {
      violations.push({
        element: id,
        rule: 'missing-accessible-name',
        severity: 'serious'
      });
    }

    // Image without alt text
    if (node.tag === 'IMG' && !node.attrs?.alt) {
      violations.push({
        element: id,
        rule: 'img-alt',
        severity: 'critical'
      });
    }

    // Form input without label
    if (['INPUT', 'SELECT', 'TEXTAREA'].includes(node.tag)) {
      const hasLabel = sifr.relations?.some(r => 
        r.to === id && r.type === 'labels'
      );
      if (!hasLabel && !node.attrs?.['aria-label'] && !node.attrs?.['aria-labelledby']) {
        violations.push({
          element: id,
          rule: 'label-missing',
          severity: 'serious'
        });
      }
    }
  }

  return violations;
}
Enter fullscreen mode Exit fullscreen mode

The Pipeline

Put it together:

PR opened
    ↓
Playwright runs against staging
    ↓
SiFR captures key pages
    ↓
Compare against baseline snapshots
    ↓
Report: "3 buttons removed, 2 layout shifts detected, 1 new a11y violation"
    ↓
Block merge or flag for review
Enter fullscreen mode Exit fullscreen mode

No pixel diffing. No screenshot comparison. Just semantic changes that actually matter.


What This Enables

For manual QA:

  • Bug reports that developers can act on immediately
  • No more "works on my machine"
  • AI assistance that's actually useful

For automation:

  • Selectors that don't break every sprint
  • Regression detection beyond visual diffing
  • Accessibility scanning from runtime state

For everyone:

  • Faster debugging cycles
  • Fewer production issues
  • Less time wasted on reproduction

Try It

The format is implemented in Element to LLM — a free browser extension:

For Playwright integration, you'll need to load the extension in your test browser. The event-based API (e2llm-capture-request / e2llm-capture-response) works in any automation context.


Series Index

  1. Runtime Snapshots #1: Taking a "fine" signup form and making it work
  2. Runtime Snapshots #2: a11y starts with runtime context
  3. 🧩 Runtime Snapshots #3 — QA That Speaks JSON
  4. 🧩 Runtime Snapshots #4 — The Invisible JSON: when hidden state breaks your LLM
  5. 🧩 Runtime Snapshots #5 — The Real Thing: How We Actually Use It
  6. 🧩 Runtime Snapshots #6 — The Hidden Reason Your LLM UI Agents Cost Too Much
  7. Runtime Snapshots #7 — Inside SiFR: The Schema That Makes LLMs See Web UIs

Using SiFR in your QA pipeline? I'd love to hear what's working and what's missing. Drop a comment or open an issue.

Top comments (0)