From 5c5dfa5766849aee06de7992677f9c8e31b5f854 Mon Sep 17 00:00:00 2001 From: Thales <> Date: Sat, 29 Aug 2026 16:08:09 +0100 Subject: [PATCH] Cover the streaming render path, which was never broken I said in #493 that the streaming path was reasoned about rather than measured, because looking for its canvases found none: document.querySelector("#multitrack-container").querySelectorAll("canvas") // 0 after eight seconds, with the overlay cleared, canplay fired and no failed requests. That was convincing and wrong. WaveSurfer renders into shadow roots and querySelectorAll does not cross a shadow boundary. Walking the roots finds nine canvases, correctly sized, and a zoom re-renders them: 910px each at 1x, and at 5x a 4550px lane cut into 4000 + 550, backing store matching CSS width in every case. So there was nothing to fix, and the gap was mine. The path is covered now, and the assertion is the one that matters on it: WaveSurfer bars are configured in pixels, so they keep their width across a zoom only if it re-renders rather than letting a fixed-size canvas stretch. Backing width equal to CSS width is what distinguishes the two, and a stretched canvas fails it. The shadow-root walk lives in a helper with the trap written down next to it, because the next person to look for a canvas here will otherwise reach the same false conclusion. Closes #494 --- tests/e2e/zoom.spec.mjs | 69 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/e2e/zoom.spec.mjs b/tests/e2e/zoom.spec.mjs index a58b77ba..b6b56432 100644 --- a/tests/e2e/zoom.spec.mjs +++ b/tests/e2e/zoom.spec.mjs @@ -65,6 +65,27 @@ const loopBounds = (page) => }; }); +// Every canvas WaveSurfer has drawn, reached through the shadow roots it renders +// into. `#multitrack-container.querySelectorAll("canvas")` returns nothing -- +// not because the streaming path draws nothing, but because querySelectorAll +// does not cross a shadow boundary. That blind spot is the reason this path +// looked broken and went uncovered. +const waveSurferCanvases = (page) => + page.evaluate(() => { + const out = []; + const walk = (root) => { + for (const el of root.querySelectorAll("*")) { + if (!el.shadowRoot) continue; + for (const c of el.shadowRoot.querySelectorAll("canvas")) { + out.push({ backing: c.width, css: Math.round(c.getBoundingClientRect().width) }); + } + walk(el.shadowRoot); + } + }; + walk(document.querySelector("#multitrack-container")); + return out; + }); + // Deliberately wide: the bar-count maths only has room to prove itself when the // panel can hold a few hundred bars. test.use({ viewport: { width: 1600, height: 900 } }); @@ -270,6 +291,54 @@ test.describe("waveform zoom", () => { expect(panned.scrollLeft).not.toBe(zoomed.scrollLeft); }); + // The SVG overview is only the visible waveform when the Web Audio engine owns + // playback. On the streaming path WaveSurfer draws the lanes into canvases + // instead and gets its own zoom call, so the "bars keep their width" promise + // rests on a completely different mechanism there: its bars are configured in + // pixels (barWidth 3, barGap 2), which holds only if it re-renders rather than + // letting a fixed-size canvas stretch. + test("the streaming path re-renders its canvases instead of stretching them", async ({ page }) => { + await page.addInitScript(() => window.localStorage.setItem("stemdeck.audioEngine", "0")); + await openStudio(page, { tauri: true }); + await page.waitForFunction( + () => { + let n = 0; + const walk = (root) => { + for (const el of root.querySelectorAll("*")) { + if (!el.shadowRoot) continue; + n += el.shadowRoot.querySelectorAll("canvas").length; + walk(el.shadowRoot); + } + }; + walk(document.querySelector("#multitrack-container")); + return n > 0; + }, + null, + { timeout: 20000 }, + ); + + const before = await zoomState(page); + const canvasesBefore = await waveSurferCanvases(page); + expect(canvasesBefore.length).toBeGreaterThan(0); + + for (let i = 0; i < 40; i++) await wheel(page, -240); + await page.waitForTimeout(1200); + + const after = await zoomState(page); + const canvasesAfter = await waveSurferCanvases(page); + expect(after.content).toBeGreaterThan(before.content * 4); + + // A canvas whose backing store matches its CSS width was drawn at that + // width. A stretched one keeps its original pixel width while its box grows, + // which is the failure the SVG path had to be fixed for. + for (const c of canvasesAfter) expect(c.backing).toBe(c.css); + // And something actually got wider, rather than the lanes being re-cut into + // more canvases of the same size. WaveSurfer chunks at 4000px, so the widest + // is the one to look at. + const widest = (list) => Math.max(...list.map((c) => c.backing)); + expect(widest(canvasesAfter)).toBeGreaterThan(widest(canvasesBefore) * 2); + }); + test("opening another track returns to the fitted view", async ({ page }) => { await openStudio(page, { tauri: true }); for (let i = 0; i < 10; i++) await wheel(page, -240);