feat(sim): sky scale + auto-start experience + drop reef_hawaiian
Expanded-pools sky-scale work plus two experience tweaks: - New `sky` altitude (airplane-window / low-flight layer) between orbit and coast; RING_ORDER cosmos·orbit·sky·coast·reef·abyss. New clip pools across all scales, re-baked the full adjacent-pair morph set (all-intra, LFS), and removed the now-stale cosmos↔abyss transitions the old ring order produced. - Auto-start: once the preload finishes the experience turns Video on and raises the audio dial to 3/10 with no user gesture (app.js autoStart()). - Dropped reef_hawaiian (text overlay — violates the text-free pool rule); reef pool 7→6, manifest transitions 586→558. Tests: rewrote tests/test_e2e_audio.py for the dial UI + auto-start; updated the manifest morph-count assertions and the altitude-scrub target for the new ring order. 302 pytest + 11 Playwright e2e + 9 node unit all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,7 @@ def test_transitions_are_per_clip_pair_both_directions():
|
||||
expected = _expected_pairs()
|
||||
got = {(t["from"], t["to"], t["file"]) for t in trans}
|
||||
assert got == expected
|
||||
assert len(trans) == len(expected) == 154
|
||||
assert len(trans) == len(expected) == 558
|
||||
assert all(t["model"] == "placeholder-zoom" for t in trans)
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ def test_generate_media_bakes_every_member_pair(monkeypatch, tmp_path):
|
||||
rev = {f"{h}__{l}.rev.mp4" for H, L in b._adjacent_edges()
|
||||
for h in b.POOLS[H] for l in b.POOLS[L]}
|
||||
assert fwd <= outs and rev <= outs
|
||||
assert len(fwd) + len(rev) == 154
|
||||
assert len(fwd) + len(rev) == 558
|
||||
|
||||
|
||||
def test_transition_cmd_is_all_intra():
|
||||
|
||||
+50
-48
@@ -1,9 +1,13 @@
|
||||
"""Playwright E2E for the audio + Video/Audio toggles (audio spec §9). Skips
|
||||
"""Playwright E2E for the audio dial + Video toggle (audio spec §9). Skips
|
||||
cleanly when Playwright or its browser binary is absent.
|
||||
|
||||
NOTE: headless engines relax BOTH autoplay and GPU compositing, so these assert
|
||||
the WIRING (toggle→play, the first-video-on→audio coupling, video-off blanking) —
|
||||
real Safari/iOS autoplay and real-GPU compositing still need a device by-ear/eye check.
|
||||
the WIRING (auto-start on load, dial→play/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 0–10
|
||||
range dial (not a checkbox); set it by writing `value` + firing an `input` event.
|
||||
|
||||
Install: pip install -e '.[e2e]' && python -m playwright install chromium
|
||||
"""
|
||||
@@ -58,75 +62,73 @@ def page(app_url):
|
||||
pytest.skip(f"chromium not available: {exc}")
|
||||
pg = browser.new_page()
|
||||
pg.goto(app_url)
|
||||
# wait out the "Loading Universe…" splash (it overlays + blocks clicks)
|
||||
# wait out the "Loading Universe…" splash (it overlays + blocks clicks); the
|
||||
# auto-start fires as it dismisses.
|
||||
pg.wait_for_selector("#loading", state="detached", timeout=60000)
|
||||
yield pg
|
||||
browser.close()
|
||||
|
||||
|
||||
def _toggle(page, which):
|
||||
"""Click the visible toggle track for #visual / #audio (the input is hidden)."""
|
||||
page.click(f"label[for='{which}'] .dev-switch-track")
|
||||
def _toggle_visual(page):
|
||||
"""Click the visible Video toggle track (the checkbox input is hidden)."""
|
||||
page.click("label[for='visual'] .dev-switch-track")
|
||||
|
||||
|
||||
def test_app_starts_blanked_and_silent(page):
|
||||
# both toggles default off → black screen, no audio (no tap-to-start wall)
|
||||
assert page.is_checked("#visual") is False
|
||||
assert page.is_checked("#audio") is False
|
||||
page.wait_for_selector("#black:not(.hidden)")
|
||||
assert page.evaluate("document.getElementById('aud').paused") is True
|
||||
def _set_audio(page, level):
|
||||
"""Drive the 0–10 audio dial: set value + fire `input` (the engine reconciles
|
||||
playback synchronously in that gesture)."""
|
||||
page.evaluate(
|
||||
"(lvl) => { const a = document.getElementById('audio');"
|
||||
" a.value = String(lvl); a.dispatchEvent(new Event('input', {bubbles:true})); }",
|
||||
level,
|
||||
)
|
||||
|
||||
|
||||
def test_audio_toggle_plays_the_current_scale_soundtrack(page):
|
||||
_toggle(page, "audio")
|
||||
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
|
||||
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
|
||||
page.wait_for_function(
|
||||
"(() => { const a = document.getElementById('aud');"
|
||||
" return /\\/media\\/audio\\/.+\\.mp3/.test(a.src) && !a.paused; })()"
|
||||
)
|
||||
|
||||
|
||||
def test_first_video_on_also_enables_audio(page):
|
||||
_toggle(page, "visual") # first video-on couples audio
|
||||
page.wait_for_function("document.getElementById('audio').checked === true")
|
||||
def test_audio_dial_sets_level_and_label(page):
|
||||
_set_audio(page, 7)
|
||||
page.wait_for_function("document.getElementById('audio').value === '7'")
|
||||
page.wait_for_function("document.getElementById('audio-level-val').textContent === '7'")
|
||||
|
||||
|
||||
def test_audio_dial_zero_silences(page):
|
||||
# Level 0 fades both crossfade elements to silence, then pauses them.
|
||||
_set_audio(page, 0)
|
||||
page.wait_for_function(
|
||||
"(() => { const a = document.getElementById('aud'); return !!a.src && !a.paused; })()"
|
||||
"(() => { const a = document.getElementById('aud'), b = document.getElementById('aud-b');"
|
||||
" return a.paused && b.paused; })()"
|
||||
)
|
||||
page.wait_for_selector("#black.hidden", state="attached") # video showing (black hidden=display:none)
|
||||
assert page.evaluate("document.getElementById('audio').value") == "0"
|
||||
|
||||
|
||||
def test_audio_before_video_stays_audio_only(page):
|
||||
_toggle(page, "audio") # audio on first
|
||||
page.wait_for_function("!document.getElementById('aud').paused")
|
||||
assert page.is_checked("#visual") is False # video untouched
|
||||
page.wait_for_selector("#black:not(.hidden)") # still blanked
|
||||
|
||||
|
||||
def test_audio_then_video_leaves_both_on(page):
|
||||
_toggle(page, "audio") # audio first
|
||||
page.wait_for_function("!document.getElementById('aud').paused")
|
||||
_toggle(page, "visual") # then video
|
||||
page.wait_for_selector("#black.hidden", state="attached") # video showing
|
||||
assert page.is_checked("#audio") is True
|
||||
assert page.evaluate("!document.getElementById('aud').paused") is True # audio still plays
|
||||
def test_video_off_blanks_after_autostart(page):
|
||||
# Video is auto-on; turning it off blanks the screen and hides the GPU layers.
|
||||
_toggle_visual(page) # on -> off
|
||||
page.wait_for_selector("#black:not(.hidden)")
|
||||
page.wait_for_function(
|
||||
"['vid','paint'].every(id => getComputedStyle(document.getElementById(id)).opacity === '0')"
|
||||
)
|
||||
|
||||
|
||||
def test_soundtrack_fallback_when_ring_lacks_audio(page):
|
||||
# simulate a stale server whose /api/ring omits the per-scale `audio` field;
|
||||
# the client falls back to the scale-id soundtrack map so audio still plays
|
||||
# Client resilience: even if /api/ring omits the per-scale `audio` field, the
|
||||
# scale-id fallback map keeps a soundtrack playing. Drop the field, re-apply the
|
||||
# dial, and confirm the cosmos soundtrack still resolves + plays.
|
||||
page.evaluate("ring.scales.forEach(s => { delete s.audio; })")
|
||||
_toggle(page, "audio")
|
||||
_set_audio(page, 5)
|
||||
page.wait_for_function(
|
||||
"(() => { const a = document.getElementById('aud');"
|
||||
" return a.src.includes('/media/audio/cosmos/') && !a.paused; })()"
|
||||
)
|
||||
|
||||
|
||||
def test_video_off_blanks_after_being_on(page):
|
||||
_toggle(page, "visual") # on
|
||||
page.wait_for_selector("#black.hidden", state="attached")
|
||||
_toggle(page, "visual") # off
|
||||
page.wait_for_selector("#black:not(.hidden)")
|
||||
# the GPU-composited video layers are hidden too (settle the opacity transition)
|
||||
page.wait_for_function(
|
||||
"['vid','paint'].every(id => getComputedStyle(document.getElementById(id)).opacity === '0')"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user