Skip to content

Commit 8a221c7

Browse files
committed
Self-host Playwright's trace viewer next to the runs
trace.playwright.dev is HTTPS, so when the runs viewer is reached over plain HTTP (tailscale IP) the browser blocks its fetch of trace.zip as mixed content and the trace never loads. playwright-core ships the trace viewer as a static PWA — rebuild-viewer.ts now copies it to runs/trace-viewer and the run page links there same-origin, which also keeps traces viewable offline.
1 parent 7ee458a commit 8a221c7

3 files changed

Lines changed: 30 additions & 52 deletions

File tree

e2e/scripts/rebuild-viewer.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
// test: refresh runs/manifest.json + vite-build the SPA into runs/.
33
// Usage: bun e2e/scripts/rebuild-viewer.ts
44
import { execFileSync } from "node:child_process";
5-
import { rmSync } from "node:fs";
6-
import { join } from "node:path";
5+
import { cpSync, rmSync } from "node:fs";
6+
import { createRequire } from "node:module";
7+
import { dirname, join } from "node:path";
78
import { fileURLToPath } from "node:url";
89

910
import { buildManifest } from "../src/viewer/manifest";
@@ -17,4 +18,18 @@ execFileSync("bunx", ["vite", "build", "--config", "viewer/vite.config.ts"], {
1718
cwd: e2eDir,
1819
stdio: "inherit",
1920
});
21+
22+
// Self-host Playwright's trace viewer (a static PWA shipped inside
23+
// playwright-core) next to the runs. trace.playwright.dev is HTTPS and
24+
// browsers refuse to let it fetch a trace.zip from a plain-HTTP server
25+
// (mixed content) — which is exactly how this viewer is reached over
26+
// tailscale. Same-origin, the restriction disappears.
27+
// playwright-core isn't a direct dependency — resolve it through
28+
// playwright (which is), then walk from its entry to the package root.
29+
const require = createRequire(import.meta.url);
30+
const playwrightCoreEntry = createRequire(require.resolve("playwright")).resolve("playwright-core");
31+
const traceViewerSrc = join(dirname(playwrightCoreEntry), "lib/vite/traceViewer");
32+
rmSync(join(runsDir, "trace-viewer"), { recursive: true, force: true });
33+
cpSync(traceViewerSrc, join(runsDir, "trace-viewer"), { recursive: true });
34+
2035
console.log(`viewer rebuilt at ${runsDir}`);

e2e/src/timeline.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,7 @@ const write = (runDir: string, timeline: Timeline) =>
3232
writeFileSync(fileFor(runDir), JSON.stringify(timeline, null, 1));
3333

3434
/** Record that `window`'s recording clock starts now. */
35-
export const markRecordingStart = (
36-
runDir: string,
37-
window: TimelineWindow,
38-
): void => {
35+
export const markRecordingStart = (runDir: string, window: TimelineWindow): void => {
3936
const timeline = read(runDir);
4037
write(runDir, {
4138
...timeline,

e2e/viewer/src/SessionPlayer.tsx

Lines changed: 12 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -104,20 +104,10 @@ export const SessionPlayer = ({
104104
}
105105
return { window: entry.window, from: toSession(entry.at), to };
106106
});
107-
}, [
108-
timeline,
109-
sessionStart,
110-
terminalAnchor,
111-
browserAnchor,
112-
castDuration,
113-
videoDuration,
114-
]);
107+
}, [timeline, sessionStart, terminalAnchor, browserAnchor, castDuration, videoDuration]);
115108

116109
const duration = acts.at(-1)?.to ?? 0;
117-
const ready =
118-
castDuration !== null &&
119-
videoDuration !== null &&
120-
Number.isFinite(duration);
110+
const ready = castDuration !== null && videoDuration !== null && Number.isFinite(duration);
121111

122112
const actAt = (time: number): number => {
123113
for (let i = acts.length - 1; i >= 0; i -= 1) {
@@ -133,9 +123,7 @@ export const SessionPlayer = ({
133123
const mediaTime = (window: Act["window"], time: number): number =>
134124
Math.max(
135125
0,
136-
(sessionStart +
137-
time * 1000 -
138-
(window === "terminal" ? terminalAnchor : browserAnchor)) /
126+
(sessionStart + time * 1000 - (window === "terminal" ? terminalAnchor : browserAnchor)) /
139127
1000,
140128
);
141129

@@ -178,22 +166,15 @@ export const SessionPlayer = ({
178166
if (act.window === "browser") {
179167
cast?.pause();
180168
if (video) {
181-
const target = Math.min(
182-
mediaTime("browser", time),
183-
(videoDuration ?? Infinity) - 0.05,
184-
);
185-
if (Math.abs(video.currentTime - target) > 0.25)
186-
video.currentTime = target;
169+
const target = Math.min(mediaTime("browser", time), (videoDuration ?? Infinity) - 0.05);
170+
if (Math.abs(video.currentTime - target) > 0.25) video.currentTime = target;
187171
if (play) await video.play().catch(() => {});
188172
else video.pause();
189173
}
190174
} else {
191175
videoRef.current?.pause();
192176
if (cast) {
193-
const target = Math.min(
194-
mediaTime("terminal", time),
195-
(castDuration ?? Infinity) - 0.05,
196-
);
177+
const target = Math.min(mediaTime("terminal", time), (castDuration ?? Infinity) - 0.05);
197178
const current = cast.getCurrentTime();
198179
const now = typeof current === "number" ? current : await current;
199180
if (Math.abs(now - target) > 0.25) await cast.seek(target);
@@ -220,9 +201,7 @@ export const SessionPlayer = ({
220201
const current = castPlayer.current?.getCurrentTime() ?? 0;
221202
media = typeof current === "number" ? current : await current;
222203
}
223-
const wall =
224-
(act.window === "terminal" ? terminalAnchor : browserAnchor) +
225-
media * 1000;
204+
const wall = (act.window === "terminal" ? terminalAnchor : browserAnchor) + media * 1000;
226205
let next = Math.max(tRef.current, (wall - sessionStart) / 1000);
227206
if (next >= act.to - 0.05) {
228207
if (index === acts.length - 1) {
@@ -263,9 +242,7 @@ export const SessionPlayer = ({
263242
// Live URL: the last main-frame navigation at-or-before "now" (wall).
264243
const wallNow = sessionStart + t * 1000;
265244
const currentUrl =
266-
[...(timeline.nav ?? [])]
267-
.reverse()
268-
.find((entry) => entry.at <= wallNow + 250)?.url ?? "";
245+
[...(timeline.nav ?? [])].reverse().find((entry) => entry.at <= wallNow + 250)?.url ?? "";
269246

270247
// Trace markers in session time (only the ones inside the session).
271248
const traceMarks = useMemo(
@@ -346,9 +323,7 @@ export const SessionPlayer = ({
346323
muted
347324
playsInline
348325
preload="auto"
349-
onLoadedMetadata={(event) =>
350-
setVideoDuration(event.currentTarget.duration)
351-
}
326+
onLoadedMetadata={(event) => setVideoDuration(event.currentTarget.duration)}
352327
/>
353328
</div>
354329
</div>
@@ -361,9 +336,7 @@ export const SessionPlayer = ({
361336
className="scrub"
362337
onClick={(event) => {
363338
const rect = event.currentTarget.getBoundingClientRect();
364-
void seekTo(
365-
((event.clientX - rect.left) / rect.width) * duration,
366-
);
339+
void seekTo(((event.clientX - rect.left) / rect.width) * duration);
367340
}}
368341
>
369342
{ready &&
@@ -387,12 +360,7 @@ export const SessionPlayer = ({
387360
title={`${mark.url.replace(/^https?:\/\/[^/]+/, "")} → trace ${mark.id.slice(0, 8)}`}
388361
/>
389362
))}
390-
{ready && (
391-
<span
392-
className="head"
393-
style={{ left: `${(t / duration) * 100}%` }}
394-
/>
395-
)}
363+
{ready && <span className="head" style={{ left: `${(t / duration) * 100}%` }} />}
396364
</div>
397365
<span className="clock">
398366
{fmt(t)} / {ready ? fmt(duration) : "…"}
@@ -432,9 +400,7 @@ export const SessionPlayer = ({
432400
</span>
433401
<span
434402
className={`trace-ms${slow ? " slow" : ""}${
435-
mark.status !== undefined && mark.status >= 400
436-
? " err"
437-
: ""
403+
mark.status !== undefined && mark.status >= 400 ? " err" : ""
438404
}`}
439405
>
440406
{mark.ms !== undefined ? `${mark.ms}ms` : "·"}

0 commit comments

Comments
 (0)