From 969236b3f78132960cc16aa43b0c46539de34c1a Mon Sep 17 00:00:00 2001 From: BenStullsBets Date: Thu, 2 Jul 2026 16:00:59 -0700 Subject: [PATCH] feat(audio): shareable ?audio= deep-link presets the sound mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ?audio=none|soundtrack|music (and ?vol=0..10) presets the Audio source/volume on load, so a link opens straight into a chosen sound mode — e.g. share …/?audio=music with someone who'd prefer music, …/?audio=soundtrack for the ambience. Invalid values ignored (default Soundtrack stands). +5 e2e (url-params.spec.ts). Co-Authored-By: Claude Opus 4.8 --- simulator/e2e/tests/url-params.spec.ts | 47 ++++++++++++++++++++++++++ simulator/static/app.js | 23 +++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 simulator/e2e/tests/url-params.spec.ts diff --git a/simulator/e2e/tests/url-params.spec.ts b/simulator/e2e/tests/url-params.spec.ts new file mode 100644 index 0000000..d42fd36 --- /dev/null +++ b/simulator/e2e/tests/url-params.spec.ts @@ -0,0 +1,47 @@ +import { test, expect, Page } from "@playwright/test"; + +// Shareable deep-link: ?audio=none|soundtrack|music presets the Audio source on load +// (and ?vol=0..10 the master volume), so a link opens straight into a chosen sound mode. + +async function ready(page: Page) { + await page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 }); +} +async function launch(page: Page) { + await page.locator("#welcome-launch").click(); + await page.waitForSelector("#welcome", { state: "detached", timeout: 10_000 }); +} +const srcVal = (page: Page) => page.locator("#audio-source").inputValue(); + +test("?audio=music preselects the Music source and survives launch", async ({ page }) => { + await page.goto("/?audio=music"); + await ready(page); + expect(await srcVal(page)).toBe("music"); // set at init, before launch + await launch(page); + expect(await srcVal(page)).toBe("music"); +}); + +test("?audio=soundtrack preselects Soundtrack", async ({ page }) => { + await page.goto("/?audio=soundtrack"); + await ready(page); + expect(await srcVal(page)).toBe("soundtrack"); +}); + +test("?audio=none preselects None and disables Volume", async ({ page }) => { + await page.goto("/?audio=none"); + await ready(page); + expect(await srcVal(page)).toBe("none"); + await expect(page.locator("#audio")).toBeDisabled(); +}); + +test("an invalid ?audio value is ignored (stays default Soundtrack)", async ({ page }) => { + await page.goto("/?audio=banana"); + await ready(page); + expect(await srcVal(page)).toBe("soundtrack"); +}); + +test("?vol sets the master volume for the shared link", async ({ page }) => { + await page.goto("/?audio=music&vol=7"); + await ready(page); + await launch(page); + expect(await page.locator("#audio").inputValue()).toBe("7"); +}); diff --git a/simulator/static/app.js b/simulator/static/app.js index 4902f1c..e949f6d 100644 --- a/simulator/static/app.js +++ b/simulator/static/app.js @@ -1410,6 +1410,27 @@ function syncVolumeDisabled() { if (slider) slider.disabled = audioSource() === "none"; } +// Shareable deep-link: ?audio=none|soundtrack|music presets the Audio source on load +// (and ?vol=0..10 the master volume), so a link opens straight into a chosen sound mode +// — e.g. share …/?audio=music with someone who'd prefer music over the soundtrack. The +// source is set on the (welcome-hidden) panel dropdown; the launch gesture then applies +// it. Invalid values are ignored (the default Soundtrack stands). +function applyUrlAudioParams() { + const p = new URLSearchParams(location.search); + const src = (p.get("audio") || "").toLowerCase(); + if (src === "none" || src === "soundtrack" || src === "music") { + const el = $("audio-source"); if (el) el.value = src; + } + const vol = p.get("vol"); + if (vol !== null && vol !== "" && Number.isFinite(+vol)) { + const v = String(Math.max(0, Math.min(10, Math.round(+vol)))); + // Seed BOTH the welcome mirror (carried into the panel on launch) and the panel + // slider, so the shared level survives entry. + const w = $("welcome-audio"); if (w) w.value = v; + const a = $("audio"); if (a) a.value = v; + } +} + // The element currently carrying the most signal — for the diagnostic readout, // which used to assume a single `aud`. With two crossfading elements either may be // the audible one, so report on whichever is louder (ties → aud). @@ -1682,7 +1703,9 @@ async function main() { // Audio source dropdown: reconcile SYNCHRONOUSLY in this change gesture so // switching to Soundtrack/Music unlocks + plays on Safari; None greys Volume. $("audio-source").addEventListener("change", () => { syncVolumeDisabled(); applyAudio(); debounced(); }); + applyUrlAudioParams(); // shareable ?audio=/?vol= deep-link presets syncVolumeDisabled(); + updateWelcomeAudioLabel(); // Video toggle (panel): the FIRST time video turns on without having entered via // Launch, begin the experience too — couple audio in. (Normally Launch entered // first, so videoEverOn is already true and this is a no-op beyond re-render.)