-
Notifications
You must be signed in to change notification settings - Fork 153
e2e: show a URL bar in browser session recordings #1216
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+112
−0
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| // A synthetic browser URL bar, baked into the session recording itself. | ||
| // | ||
| // Playwright records the page viewport only (the recording is chromeless), so | ||
| // a shared `session.mp4` gives no hint of which URL each moment was on. The | ||
| // runs viewer reconstructs a URL bar from the nav timeline, but the raw video | ||
| // (the thing people actually pass around) had none. This injects a thin 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 must not perturb the scenario: it renders inside a CLOSED shadow root | ||
| // (invisible to Playwright locators and the accessibility tree) and is | ||
| // `pointer-events: none` (never intercepts a click). The styling mirrors the | ||
| // viewer's synthetic chrome (traffic lights, #161b22 bar) so the in-viewer and | ||
| // standalone-video looks agree. | ||
| import type { BrowserContext } from "playwright"; | ||
|
|
||
| /** Runs in the page before any app script, on every top-level document. */ | ||
| function injectUrlBar(): void { | ||
| // Top frame only (iframes should not each grow their own bar). | ||
| if (window.top !== window.self) return; | ||
| const flagged = window as Window & { __e2eUrlBar?: boolean }; | ||
| if (flagged.__e2eUrlBar) return; | ||
| flagged.__e2eUrlBar = true; | ||
|
|
||
| const BAR_H = 32; | ||
|
|
||
| const install = (): void => { | ||
| const root = document.documentElement; | ||
| if (!root) return; | ||
|
|
||
| const host = document.createElement("div"); | ||
| host.style.cssText = `position:fixed;top:0;left:0;width:100%;height:${BAR_H}px;z-index:2147483647;pointer-events:none`; | ||
| const shadow = host.attachShadow({ mode: "closed" }); | ||
|
|
||
| const bar = document.createElement("div"); | ||
| bar.style.cssText = [ | ||
| "display:flex", | ||
| "align-items:center", | ||
| "gap:8px", | ||
| `height:${BAR_H}px`, | ||
| "box-sizing:border-box", | ||
| "padding:0 12px", | ||
| "background:#161b22", | ||
| "border-bottom:1px solid #21262d", | ||
| "font:13px/1 ui-monospace,SFMono-Regular,Menlo,monospace", | ||
| "color:#c9d1d9", | ||
| "white-space:nowrap", | ||
| "overflow:hidden", | ||
| ].join(";"); | ||
|
|
||
| const dot = (color: string): HTMLElement => { | ||
| const d = document.createElement("span"); | ||
| d.style.cssText = `width:11px;height:11px;border-radius:50%;flex:none;background:${color}`; | ||
| return d; | ||
| }; | ||
| const lights = document.createElement("span"); | ||
| lights.style.cssText = "display:inline-flex;gap:6px;margin-right:4px"; | ||
| lights.append(dot("#ff5f57"), dot("#febc2e"), dot("#28c840")); | ||
|
|
||
| const lock = document.createElement("span"); | ||
| lock.textContent = "⌁"; // the viewer's URL-bar glyph | ||
| lock.style.cssText = "color:#8b949e;flex:none"; | ||
|
|
||
| const url = document.createElement("span"); | ||
| url.style.cssText = "overflow:hidden;text-overflow:ellipsis"; | ||
|
|
||
| bar.append(lights, lock, url); | ||
| shadow.append(bar); | ||
| root.appendChild(host); | ||
|
|
||
| const render = (): void => { | ||
| const next = location.href.replace(/^https?:\/\//, "") || "about:blank"; | ||
| if (url.textContent !== next) url.textContent = next; | ||
| }; | ||
| render(); | ||
|
|
||
| // SPA route changes don't reload the document, so re-read the URL on every | ||
| // history transition; the interval also re-attaches the bar if a framework | ||
| // re-render detached it, and is the catch-all for navigations we can't hook. | ||
| window.setInterval(() => { | ||
| if (!root.contains(host)) root.appendChild(host); | ||
| render(); | ||
| }, 250); | ||
| for (const ev of ["popstate", "hashchange"] as const) window.addEventListener(ev, render); | ||
| for (const name of ["pushState", "replaceState"] as const) { | ||
| const orig = history[name] as (...args: unknown[]) => unknown; | ||
| if (typeof orig === "function") { | ||
| history[name] = function (this: History, ...args: unknown[]) { | ||
| const result = orig.apply(this, args); | ||
| render(); | ||
| return result; | ||
| } as History[typeof name]; | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| if (document.documentElement) install(); | ||
| else window.addEventListener("DOMContentLoaded", install, { once: true }); | ||
| } | ||
|
|
||
| /** | ||
| * Install the recording URL bar on a Playwright context. No-op on the desk | ||
| * (E2E_DESK), where the browser is headed and already shows real chrome. | ||
| */ | ||
| export const installRecordingUrlBar = async (context: BrowserContext): Promise<void> => { | ||
| if (process.env.E2E_DESK === "1") return; | ||
| await context.addInitScript(injectUrlBar); | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
urlspan is missingflex:1;min-width:0, which are required fortext-overflow:ellipsisto fire on a flex item. Withoutmin-width:0, a flex item's minimum size defaults tomin-content(the full un-broken text width), so the item never shrinks below the URL's length. The bar's ownoverflow:hiddenstill 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.