diff --git a/frontend/app.js b/frontend/app.js index d80c689..a2cc18c 100755 --- a/frontend/app.js +++ b/frontend/app.js @@ -2420,8 +2420,44 @@ document.querySelectorAll('.chip').forEach((c) => { els.repeatBtn.addEventListener('click', toggleRepeat); els.fsBtn.addEventListener('click', () => { const stage = els.video.parentElement; - if (document.fullscreenElement) document.exitFullscreen(); - else stage.requestFullscreen?.(); + const video = els.video; + // iOS Safari — including an installed PWA running in standalone mode — + // does not implement the standard Fullscreen API for arbitrary elements. + // stage.requestFullscreen is simply undefined there, so the button did + // nothing (this is the iPhone-in-portrait-PWA bug report). WebKit instead + // exposes a video-only, non-standard fullscreen API that *does* work in + // standalone mode: HTMLVideoElement.webkitEnterFullscreen/ExitFullscreen. + // Try the standard API first everywhere else, then fall back to the + // WebKit video API before giving up. + if (document.fullscreenElement || video.webkitDisplayingFullscreen) { + try { + if (document.exitFullscreen) document.exitFullscreen()?.catch(() => {}); + else if (video.webkitExitFullscreen) video.webkitExitFullscreen(); + } catch { /* e.g. InvalidStateError — nothing more we can do */ } + return; + } + if (stage.requestFullscreen) { + // requestFullscreen() can both throw synchronously (e.g. + // InvalidStateError when preconditions like active user-gesture + // transient activation aren't met) and return a promise that rejects + // asynchronously. Guard against both instead of letting either surface + // as an uncaught error. + try { + stage.requestFullscreen()?.catch(() => {}); + } catch { /* no-op — fullscreen simply won't engage this time */ } + } else if (video.webkitEnterFullscreen) { + if (Player.mode === 'audio') { + toast('Fullscreen isn’t available in audio-only mode'); + return; + } + try { + // Throws InvalidStateError if the element has no loaded media (e.g. + // no video source yet) — nothing to show fullscreen in that case. + video.webkitEnterFullscreen(); + } catch { /* no-op — no media loaded to go fullscreen with */ } + } else { + toast('Fullscreen isn’t supported on this device'); + } }); els.seek.addEventListener('input', () => { diff --git a/tests/fullscreen-ios.smoke.spec.js b/tests/fullscreen-ios.smoke.spec.js new file mode 100644 index 0000000..822463b --- /dev/null +++ b/tests/fullscreen-ios.smoke.spec.js @@ -0,0 +1,98 @@ +/** + * Smoke test for the iOS/PWA fullscreen-button fix (Task #52). + * + * Bug: on an iPhone running the installed PWA in portrait (standalone + * display-mode), tapping the fullscreen button did nothing. Root cause: iOS + * Safari does not implement the standard Fullscreen API + * (Element.requestFullscreen) for arbitrary elements — even in a standalone + * PWA — so `stage.requestFullscreen?.()` silently no-ops. WebKit instead + * exposes a video-only, non-standard fallback, + * HTMLVideoElement.webkitEnterFullscreen/webkitExitFullscreen, which does + * work in standalone mode. + * + * Playwright/Chromium implements the standard Fullscreen API, so to exercise + * the WebKit fallback branch we delete `requestFullscreen` from the stage + * element (simulating iOS Safari) and stub `webkitEnterFullscreen` / + * `webkitExitFullscreen` on the