108 lines · 4.3 KB · TypeScript
RawDownload
// 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);};