From b7db4c35f6304d6158d1372ca6d24b6e6426da7c Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Thu, 22 Oct 2026 12:40:18 -0700 Subject: [PATCH] fix(inspect): reset a released preview video in the next task On WebKit (GStreamer) a video replaced while its pipeline was still starting blocked the page: pause, src removal and load() in one task never returned, so the next chooser selection hung (CI page workflows webkit shard 2/2, "URLs and media end with their owning session"). The src is still dropped before the URL is revoked; only the load() reset moves to the next task. A fresh-page regression replaces the first video as soon as it shows, fifteen times. --- TESTING.md | 6 ++++-- tests/browser/workspace-inspect.spec.mjs | 22 ++++++++++++++++++++++ web/js/workspace/inspect.mjs | 8 ++++++-- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/TESTING.md b/TESTING.md index 5e32b35..c4ed58c 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1296,8 +1296,10 @@ mapped differences"): `image/png,image/jpeg,image/webp` plus `video/*,.mp4,.mov,.m4v,.webm,.mkv,.avi,.wmv`. A video previews in the same `#local-preview` slot as `video.local-file-preview` with native `controls`, `playsInline`, `preload="metadata"` and `muted`, never autoplays - inline, and is paused, unloaded (`removeAttribute('src')`, `load()`) and removed on - replacement or dispose. + inline, and is paused, has its `src` removed and is removed from the page on replacement + or dispose, before its URL is revoked; its `load()` reset runs in the next task, because + WebKit (GStreamer) blocked the page when a still-starting video was reset in the same + task (`replacing a video that just started loading never blocks the page`). - **D63** (status line): one `p.small.spaced-16` in `#local-preview`, for states the concept never shows: "Drop one file at a time.", "Folders cannot be inspected. Choose one image or video file.", "This file type cannot be previewed or inspected here. diff --git a/tests/browser/workspace-inspect.spec.mjs b/tests/browser/workspace-inspect.spec.mjs index 98e6c3d..f429946 100644 --- a/tests/browser/workspace-inspect.spec.mjs +++ b/tests/browser/workspace-inspect.spec.mjs @@ -391,6 +391,28 @@ test('URLs and media end with their owning session', async ({ page }) => { expect(state.revoked.filter(url => url === state.created[7])).toHaveLength(1); }); +test('replacing a video that just started loading never blocks the page', async ({ context, browserName }) => { + test.setTimeout(120000); + // WebKit (GStreamer) blocked the page when a video's pipeline was reset with load() in the same task that + // removed its src, while it was still starting: the next chooser selection never completed. Each fresh page + // replaces its first video as soon as it shows. + for (let attempt = 0; attempt < 15; attempt++) { + const page = await context.newPage(); + try { + if (attempt === 0) await startCoverage(page, browserName); + await openInspect(page); + await choose(page, localFixture('core-tags.webm', 'video/webm')); + await expect(preview(page).locator('video.local-file-preview')).toBeVisible(); + await chooser(page).setInputFiles(parityPng('f02.png'), { timeout: 8000 }); + await expect(preview(page).locator('img.local-file-preview')).toBeVisible(); + await expect(preview(page).locator('video')).toHaveCount(0); + if (attempt === 0) await saveCoverage(page, browserName); + } finally { + await page.close(); + } + } +}); + test('replacing a file mid-read never shows or sends the old record', async ({ page, context }) => { const host = await bridgeHost(context); try { diff --git a/web/js/workspace/inspect.mjs b/web/js/workspace/inspect.mjs index 1bcc059..29bad24 100644 --- a/web/js/workspace/inspect.mjs +++ b/web/js/workspace/inspect.mjs @@ -37,10 +37,14 @@ function element(tag, className, text) { return node; } -/** Stops a preview from holding its blob: URL: media is paused and unloaded before the session revokes the URL. */ +/** + * Stops a preview from holding its blob: URL: media is paused and its src dropped before the session revokes the URL. + * The media resets with load() in the next task: WebKit (GStreamer) blocked the page when a starting video was reset in + * the same task that removed its src. + */ function releaseMedia(node) { if (!node) return; - if (node instanceof HTMLMediaElement) { node.pause(); node.removeAttribute('src'); node.load(); } + if (node instanceof HTMLMediaElement) { node.pause(); node.removeAttribute('src'); setTimeout(() => node.load(), 0); } else node.removeAttribute('src'); node.remove(); }