Merge fix/boot-silent-no-autostart: boot silent, no un-gestured auto-start

This commit is contained in:
BenStullsBets
2026-06-30 06:32:12 -07:00
3 changed files with 43 additions and 31 deletions
+6 -5
View File
@@ -83,13 +83,14 @@ test("audio is a 0-10 level dial", async ({ page }) => {
expect(ctl.max).toBe("10");
});
test("the experience auto-starts once loaded: video on + audio at 3/10", async ({ page }) => {
test("boots silent (video off, audio 0); turning video on couples audio to 3/10", async ({ page }) => {
await boot(page);
// No gesture needed — autoStart() flips Video on and lifts audio to a gentle level
// the moment the preload finishes.
await expect.poll(() => page.evaluate(() => (document.querySelector("#visual") as HTMLInputElement).checked)).toBe(true);
// Boots SILENT — no un-gestured auto-start, so the browser autoplay block never bites.
expect(await page.evaluate(() => (document.querySelector("#visual") as HTMLInputElement).checked)).toBe(false);
expect(await page.evaluate(() => (document.querySelector("#audio") as HTMLInputElement).value)).toBe("0");
// Turning Video on (a click gesture) lifts the audio dial to a gentle 3/10.
await enableVideo(page);
await expect.poll(() => page.evaluate(() => (document.querySelector("#audio") as HTMLInputElement).value)).toBe("3");
// The level label reflects it.
expect(await page.evaluate(() => document.querySelector("#audio-level-val")?.textContent)).toBe("3");
});
+6 -14
View File
@@ -1260,8 +1260,12 @@ async function main() {
$("stage").addEventListener("wheel", onWheel, { passive: false });
update(); // render the initial state (both toggles off → black, silent)
await preloadAllMedia(); // download all media, updating the loading bar
hideLoading(); // experience is ready to run smoothly
autoStart(); // once loaded: video on + gentle audio (3/10), no gesture needed
hideLoading(); // experience is ready — boots SILENT (video off, audio 0)
// No un-gestured auto-start: a browser autoplay policy blocks an audio play() made
// outside a user gesture (muted video survives, sound does not), so an auto-start
// would show video but stay silent. Instead the experience waits for the operator to
// turn Video on — that click IS the gesture, and its handler (above) lifts audio to
// 3/10 and starts the soundtrack in-gesture, so sound reliably comes up with video.
}
// Populate the language dropdown from the registry and wire live switching.
@@ -1297,16 +1301,4 @@ function setLanguage(lang) {
if (lastAffect) renderAffect(lastAffect.strength, lastAffect.intensity, lastAffect.right);
}
// Auto-start the experience the moment media is ready: bring Video on and the audio
// level to 3/10 (the same gentle default the first manual Video-on couples in). The
// audio <audio> elements ideally unlock inside a user gesture (Safari); this fires
// outside one, so a browser with a strict autoplay policy may BLOCK the first audio
// play() until any interaction — the on-screen audio status readout will show it.
function autoStart() {
$("visual").checked = true;
videoEverOn = true;
if (audioLevel() === 0) { $("audio").value = "3"; updateAudioLevelLabel(); }
applyAudio();
update();
}
main();
+31 -12
View File
@@ -2,12 +2,15 @@
cleanly when Playwright or its browser binary is absent.
NOTE: headless engines relax BOTH autoplay and GPU compositing, so these assert
the WIRING (auto-start on load, dialplay/volume, video-off blanking) real
Safari/iOS autoplay and real-GPU compositing still need a device by-ear/eye check.
the WIRING (boot-silent, video-on couples audio, dialplay/volume, video-off
blanking) real Safari/iOS autoplay and real-GPU compositing still need a device
by-ear/eye check.
The experience AUTO-STARTS once the preload finishes: Video turns on and the audio
level dial rises to 3/10 with no user gesture (app.js autoStart()). Audio is a 010
range dial (not a checkbox); set it by writing `value` + firing an `input` event.
The experience BOOTS SILENT video off, audio dial at 0 so nothing plays until a
real user gesture. Turning Video ON (a click) couples the audio dial up to 3/10 and
starts the soundtrack IN that gesture, sidestepping the browser autoplay block that
swallowed an un-gestured auto-start. Audio is a 010 range dial (not a checkbox);
set it by writing `value` + firing an `input` event.
Install: pip install -e '.[e2e]' && python -m playwright install chromium
"""
@@ -63,7 +66,7 @@ def page(app_url):
pg = browser.new_page()
pg.goto(app_url)
# wait out the "Loading Universe…" splash (it overlays + blocks clicks); the
# auto-start fires as it dismisses.
# experience boots silent (video off, audio 0) as it dismisses.
pg.wait_for_selector("#loading", state="detached", timeout=60000)
yield pg
browser.close()
@@ -84,10 +87,23 @@ def _set_audio(page, level):
)
def test_app_auto_starts_video_and_audio(page):
# Once the preload finishes the experience auto-starts: Video on + audio at 3/10,
# video showing (black hidden), a scale soundtrack playing — no tap-to-start wall.
assert page.is_checked("#visual") is True
def test_app_boots_silent_video_off(page):
# The experience boots SILENT: Video off, audio dial at 0, screen black, both
# crossfade elements paused — nothing plays until a real user gesture.
assert page.is_checked("#visual") is False
assert page.evaluate("document.getElementById('audio').value") == "0"
assert page.evaluate("document.getElementById('audio-level-val').textContent") == "0"
page.wait_for_selector("#black:not(.hidden)") # screen blanked
assert page.evaluate(
"(() => { const a = document.getElementById('aud'), b = document.getElementById('aud-b');"
" return a.paused && b.paused; })()"
) is True
def test_video_on_couples_audio_to_three_and_plays(page):
# Turning Video on (a click gesture) lifts the audio dial to 3/10 and starts the
# soundtrack IN that gesture: video shows (black hidden), a scale soundtrack plays.
_toggle_visual(page) # off -> on
page.wait_for_function("document.getElementById('audio').value === '3'")
page.wait_for_function("document.getElementById('audio-level-val').textContent === '3'")
page.wait_for_selector("#black.hidden", state="attached") # video showing
@@ -113,8 +129,11 @@ def test_audio_dial_zero_silences(page):
assert page.evaluate("document.getElementById('audio').value") == "0"
def test_video_off_blanks_after_autostart(page):
# Video is auto-on; turning it off blanks the screen and hides the GPU layers.
def test_video_off_blanks(page):
# Turn Video on (boot is off), then off again: that blanks the screen and hides
# the GPU layers.
_toggle_visual(page) # off -> on
page.wait_for_selector("#black.hidden", state="attached")
_toggle_visual(page) # on -> off
page.wait_for_selector("#black:not(.hidden)")
page.wait_for_function(