Skip to content

e2e: show a URL bar in browser session recordings#1216

Merged
RhysSullivan merged 1 commit into
mainfrom
recording-url-bar
Jun 29, 2026
Merged

e2e: show a URL bar in browser session recordings#1216
RhysSullivan merged 1 commit into
mainfrom
recording-url-bar

Conversation

@RhysSullivan

Copy link
Copy Markdown
Owner

What

Browser e2e recordings are chromeless (Playwright records the page viewport only), so a shared session.mp4 gives no hint of which page each moment was on. The runs viewer reconstructs a URL bar from the nav timeline, but the raw video that actually gets passed around had none.

This injects a synthetic browser URL bar at the top of every top-level page, so it shows up in the recording and the step/failure screenshots. It's wired into the shared browser.session path, so every browser scenario gets it with no opt-in.

How it stays invisible to scenarios

  • Rendered inside a closed shadow root, so Playwright locators and the accessibility tree can't see it (verified: getByText on the bar's URL returns 0 matches).
  • pointer-events: none, so it never intercepts a click.
  • Updates on full navigations and SPA pushState/replaceState/popstate, plus a 250ms catch-all (covers redirect-back flows like checkout).
  • Pure DOM, no dependency on an ffmpeg drawtext build.
  • Skipped under E2E_DESK (headed browser already shows real chrome). The desktop-VM targets film the real app window via a different path and are unaffected.

Verification

  • tsc, oxlint, oxfmt clean.
  • A real cloud browser run (connect-panel) passed unchanged and produced the bar in its screenshots and session.mp4.
  • A standalone check confirmed the URL updates across an SPA pushState with no reload, and that locators cannot see the bar.

Follow-up (not in this PR)

The runs viewer still draws its own synthetic URL bar above the video, so in-viewer playback now shows the URL twice (the standalone mp4 is correct). Worth trimming the viewer's .urlbar text since the video now carries it. Happy to do that here or separately.

Playwright records the page viewport only, so the recording is chromeless and
a shared session.mp4 gives no hint of which page each moment was on. The runs
viewer reconstructs a URL bar from the nav timeline, but the raw video that
people actually pass around had none.

Inject a synthetic URL bar at the top of every top-level page so it shows up
in the video and the step screenshots, fed by location.href and updated across
SPA route changes. It renders inside a closed shadow root (invisible to
Playwright locators and the accessibility tree) and is pointer-events none, so
it never perturbs a scenario. Skipped under E2E_DESK, where the headed browser
already shows real chrome.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
executor-marketing c6b20b8 Commit Preview URL

Branch Preview URL
Jun 29 2026, 07:35 PM

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
executor-cloud c6b20b8 Jun 29 2026, 07:36 PM

@RhysSullivan RhysSullivan merged commit 6748810 into main Jun 29, 2026
14 checks passed
@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Cloudflare preview

Torn down — the PR is closed.

@greptile-apps

greptile-apps Bot commented Jun 29, 2026

Copy link
Copy Markdown

Greptile Summary

This PR injects a synthetic browser URL bar into Playwright's headless recordings so that shared session.mp4 files and per-step screenshots include the current page URL without requiring any ffmpeg drawtext build.

  • A new recording-url-bar.ts module renders a 32 px fixed overlay inside a closed shadow root (invisible to Playwright locators, pointer-events:none) and keeps the URL current across full navigations, SPA pushState/replaceState, popstate, and a 250 ms catch-all interval.
  • browser.ts wires it in with a single await installRecordingUrlBar(context) call after tracing starts; the install is a no-op under E2E_DESK where real browser chrome is already visible.

Confidence Score: 4/5

Safe to merge — the overlay is fully isolated from the test surface (closed shadow root, pointer-events:none) and the only finding is a cosmetic flex-layout detail that affects whether long URLs show an ellipsis or a hard clip.

The integration is narrow and well-guarded: closed shadow root, iframe skip, double-inject guard, and E2E_DESK bypass all check out. The one cosmetic gap is the missing flex:1;min-width:0 on the URL span, which means long URLs are clipped silently rather than truncated with an ellipsis. That does not affect test correctness or the recording usefulness.

No files require special attention beyond the url span styling in recording-url-bar.ts.

Important Files Changed

Filename Overview
e2e/src/recording-url-bar.ts New file: injects a closed-shadow-root URL bar into every top-level page via Playwright addInitScript; patches history API and listens to popstate/hashchange for SPA updates; guarded against iframes and double-injection. Minor cosmetic issue with flex constraints on the URL span.
e2e/src/surfaces/browser.ts One-liner integration: calls installRecordingUrlBar(context) after tracing.start() and before cookie injection; correct placement in session lifecycle with no side-effects.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant BT as browser.ts (Node)
    participant PW as Playwright Context
    participant Page as Browser Page (DOM)
    participant Bar as URL Bar (Shadow DOM)

    BT->>PW: chromium.launch() + browser.newContext()
    BT->>PW: installRecordingUrlBar(context)
    PW->>Page: addInitScript(injectUrlBar) [registered for all future docs]

    Note over Page: On each new top-level document load
    Page->>Page: "injectUrlBar() runs (window.top === window.self check)"
    Page->>Bar: install() creates host div + closed shadow root
    Bar->>Bar: render() reads location.href, strips protocol
    Bar->>Page: root.appendChild(host)

    Note over Bar: Ongoing URL tracking
    Page->>Bar: pushState / replaceState (patched) to render()
    Page->>Bar: popstate / hashchange events to render()
    Bar->>Bar: setInterval 250ms re-read href + re-attach if detached

    Note over BT: Session teardown
    BT->>PW: context.close() flushes recording
    BT->>BT: ffmpeg to session.mp4 with URL bar baked in
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant BT as browser.ts (Node)
    participant PW as Playwright Context
    participant Page as Browser Page (DOM)
    participant Bar as URL Bar (Shadow DOM)

    BT->>PW: chromium.launch() + browser.newContext()
    BT->>PW: installRecordingUrlBar(context)
    PW->>Page: addInitScript(injectUrlBar) [registered for all future docs]

    Note over Page: On each new top-level document load
    Page->>Page: "injectUrlBar() runs (window.top === window.self check)"
    Page->>Bar: install() creates host div + closed shadow root
    Bar->>Bar: render() reads location.href, strips protocol
    Bar->>Page: root.appendChild(host)

    Note over Bar: Ongoing URL tracking
    Page->>Bar: pushState / replaceState (patched) to render()
    Page->>Bar: popstate / hashchange events to render()
    Bar->>Bar: setInterval 250ms re-read href + re-attach if detached

    Note over BT: Session teardown
    BT->>PW: context.close() flushes recording
    BT->>BT: ffmpeg to session.mp4 with URL bar baked in
Loading

Reviews (1): Last reviewed commit: "e2e: show a URL bar in browser session r..." | Re-trigger Greptile

Comment on lines +64 to +65
const url = document.createElement("span");
url.style.cssText = "overflow:hidden;text-overflow:ellipsis";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The url span is missing flex:1;min-width:0, which are required for text-overflow:ellipsis to fire on a flex item. Without min-width:0, a flex item's minimum size defaults to min-content (the full un-broken text width), so the item never shrinks below the URL's length. The bar's own overflow:hidden still clips the text, but clipping is silent — you get a hard cut instead of the trailing that signals truncation. Any URL longer than the available space after the traffic lights and lock icon will be silently clipped rather than ellipsis-terminated.

Suggested change
const url = document.createElement("span");
url.style.cssText = "overflow:hidden;text-overflow:ellipsis";
const url = document.createElement("span");
url.style.cssText = "flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis";

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant