aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/video-unmute-test.js51
1 files changed, 51 insertions, 0 deletions
diff --git a/tools/video-unmute-test.js b/tools/video-unmute-test.js
new file mode 100644
index 0000000..16cb56f
--- /dev/null
+++ b/tools/video-unmute-test.js
@@ -0,0 +1,51 @@
+// Repaint before autoplay used to copy a handler closing over a detached video.
+// Run: bun tools/video-unmute-test.js
+// Phone: adb reverse tcp:5302 tcp:5302; BROWSER_URL=http://localhost:9223 bun tools/video-unmute-test.js
+import assert from 'node:assert/strict';
+import puppeteer from 'puppeteer-core';
+const app = await Bun.file('src/app.js').text();
+const messages = await Bun.file('src/features/messages.js').text();
+const dom = app.slice(app.indexOf('function h(tag,'), app.indexOf("const root = document.getElementById('app');"));
+const video = messages.slice(messages.indexOf(' const autoplayOk ='), messages.indexOf(' // The still, with a play button'));
+const clip = '/tmp/coinos-unmute-test.webm';
+const ffmpeg = Bun.spawnSync(['ffmpeg', '-y', '-loglevel', 'error', '-f', 'lavfi', '-i', 'color=size=160x90:rate=10', '-f', 'lavfi', '-i', 'sine=frequency=440', '-t', '10', '-c:v', 'libvpx', '-c:a', 'libvorbis', clip]);
+assert.equal(ffmpeg.exitCode, 0, 'generate a video with audio');
+const server = Bun.serve({ port: 5302, fetch(req) {
+ if (new URL(req.url).pathname === '/clip.webm') return new Response(Bun.file(clip), { headers: { 'content-type': 'video/webm' } });
+ return new Response(`<style>video{width:280px}.note-video-box{position:relative}.vid-sound{position:absolute;top:8px;left:8px}</style><div id="app"></div><script>
+ // Hold autoplay until after repaint to make the race deterministic.
+ window.IntersectionObserver = undefined;
+ const t = key => key;
+ ${dom}
+ ${video}
+ const url = '/clip.webm';
+ const live = videoNode(url);
+ document.getElementById('app').append(live);
+ const originalButton = live.querySelector('button');
+ const fresh = videoNode(url);
+ morph(live, fresh);
+ window.test = { live, fresh, originalButton, rebuild() {
+ const next = videoNode(url); live.replaceWith(next); return next.querySelector('video').muted;
+ } };
+ </script>`, { headers: { 'content-type': 'text/html' } });
+} });
+const remote = !!process.env.BROWSER_URL;
+const browser = remote
+ ? await puppeteer.connect({ browserURL: process.env.BROWSER_URL })
+ : await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox'] });
+const page = await browser.newPage();
+try {
+ if (!remote) await page.setViewport({ width: 390, height: 844, isMobile: true, hasTouch: true });
+ await page.goto('http://localhost:5302/');
+ await page.waitForFunction(() => test.live.querySelector('video').readyState >= 2);
+ assert.equal(await page.evaluate(() => test.originalButton === test.live.querySelector('button') && !test.fresh.isConnected && test.live.querySelector('video').muted), true, 'repaint retains the live muted player and button');
+ await page.tap('.vid-sound');
+ await page.waitForFunction(() => !test.live.querySelector('video').paused);
+ assert.deepEqual(await page.evaluate(() => ({ muted: test.live.querySelector('video').muted, button: !!test.live.querySelector('button'), detachedMuted: test.fresh.querySelector('video').muted, frozen: test.live._skipMorph })), { muted: false, button: false, detachedMuted: true, frozen: true });
+ assert.equal(await page.evaluate(() => test.rebuild()), false, 'remounted clip remembers sound');
+ console.log('✓ Touch after repaint unmutes the live player, starts playback, removes the button, and survives remount');
+} finally {
+ await page.close();
+ if (remote) await browser.disconnect(); else await browser.close();
+ server.stop(true);
+}