Files
superpowers/skills/agentic-end-to-end-testing/driving-web-browser.md

3.8 KiB

Driving a Web UI (Browser)

Use a Chrome/CDP browser tool. After authenticated navigation, drive the page through eval against the app's own JS entry points rather than synthesizing clicks where possible — it's more robust to layout change than clicking coordinates or brittle selectors.

Authenticated navigation

If the app's login flow is a token-bearing redirect (e.g. a URL like /auth?token=<TOKEN>&next=<path>), navigate straight to that URL and then wait for an element you expect to exist once the session is live:

navigate http://<host>/auth?token=<TOKEN>&next=<path>
await_element [data-some-marker]

Use the literal token value, not the path to the file that contains it. Passing the path instead of the token itself typically renders as an "invalid token" page rather than an obvious stack trace — if you see that error, check which one you passed.

Optimistic-vs-settled assertions

For any "did the optimistic UI update happen before the request resolved?" scenario, fire the action but don't await it, take a synchronous DOM snapshot (the pending placeholder is there now), then await and snapshot again:

(async () => {
  const before = {
    pendingCount: document.querySelectorAll(".optimistic-pending").length,
  };
  // Fire — capture the promise but don't await yet.
  const promise = window.App.doAction(id, payload).catch(e => e);
  // Synchronous: the pending placeholder is in the DOM RIGHT NOW.
  const sync = {
    pendingCount: document.querySelectorAll(".optimistic-pending").length,
    pendingText: document.querySelector(".optimistic-pending")?.textContent,
  };
  await promise;
  await new Promise(r => setTimeout(r, 200));  // let the DOM settle
  const after = {
    pendingCount: document.querySelectorAll(".optimistic-pending").length,
    failedCount: document.querySelectorAll(".optimistic-failed").length,
    reason: document.querySelector(".optimistic-failed-reason")?.textContent,
  };
  return JSON.stringify({ before, sync, after }, null, 2);
})()

Without the no-await capture you can't tell "rendered then reconciled" from "never rendered" — both look identical in the post-await snapshot alone.

Return a plain string from eval

Join your findings into a string (e.g. JSON.stringify(..., null, 2) or \n-joined lines) before returning from eval. Some bridges stringify a returned object as [object Object], silently discarding everything you wanted to inspect.

Probing internal state when the DOM is ambiguous

Inspect the app's singleton via window.<App>?.state (or whatever it exposes) when the DOM alone can't tell you what happened:

JSON.stringify({
  state: window.App?.state,          // idle | processing | …
  hydrated: window.App?.hydrated,
  pendingType: typeof window.App?.pending,
  windowKeys: Object.keys(window).filter(k => k.toLowerCase().includes("app")),
})

The windowKeys scan is useful when you don't already know the singleton's name — grep the result for something plausible. If a hydration/connection flag is false when you expect true, or a registry that should be an object comes back "undefined", that's usually the real bug, not a DOM timing issue.

Prefer labels over selectors

When a step needs a concrete locator, prefer a label the user actually sees (button text, aria-label, visible heading) over a brittle structural selector like #nav > li:nth-child(3). A layout shuffle breaks the selector; it rarely changes the label.

When console capture is unreliable

If the browser tool's console-log capture is flaky or stubbed, route debug output through eval instead: push entries to a window.__DEBUG_LOG array from the page, then read it back with a follow-up eval call. This sidesteps the capture path entirely and gives you an ordinary string to inspect.