aboutsummaryrefslogtreecommitdiffstats
path: root/tools/youtube-tap-test.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-11-10 07:21:18 -0700
committerAdam Soltys <[email protected]>2026-11-10 07:21:18 -0700
commitf6970a3be5eb1019675debe47b0eedd77892a933 (patch)
tree0df8d58507c68a7e51fa88edd700e72fbcf0354f /tools/youtube-tap-test.js
parentd6b9298e2dfde43542a516b3b0cac4770592a794 (diff)
downloadcoinosv3-f6970a3be5eb1019675debe47b0eedd77892a933.tar.gz
coinosv3-f6970a3be5eb1019675debe47b0eedd77892a933.zip
A YouTube clip under a mouse takes its own clicks, and YouTube's full-screen button leaves our full screen. "The fullscreen icon on this youtube video worked to make the video fullscreen but not toggle it off … clicking the central pause button didn't pause, it fullscreened. it should just pause" (Adam, 2026-11-10). YouTube's newer player puts its pause button in the middle and its full-screen button above the bottom bar, both under the layer that takes a tap on the picture to full screen: so either press took the box full screen, and once there (the layer aside) the same button asked for full screen again from inside the frame, which shows nothing and leaves no way out. Now the layer is off while a mouse is over the box (by-mouse, set from the pointer's type; a finger gets it back), so the player's buttons do what they say, and a mouse click on the still plays it where it is. A touch's tap still goes full screen with sound; there, when the frame becomes the full-screen element in place of its box (the page sees that much), it's read as "out" and both steps are left. youtube-tap-test covers the touch runs with a finger, the button leaving full screen, and a mouse
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Diffstat (limited to 'tools/youtube-tap-test.js')
-rw-r--r--tools/youtube-tap-test.js86
1 files changed, 81 insertions, 5 deletions
diff --git a/tools/youtube-tap-test.js b/tools/youtube-tap-test.js
index e08058b..bf2832f 100644
--- a/tools/youtube-tap-test.js
+++ b/tools/youtube-tap-test.js
@@ -3,6 +3,8 @@
// sound, full screen. The player's own taps never reach the page, so a layer
// over the picture takes them; YouTube's control bar along the bottom is
// left to YouTube, and full screen the layer steps aside.
+// Full screen, YouTube's own full-screen button leaves it. Under a mouse
+// there is no layer: the player takes its own clicks (pause, full screen).
// Run: bun tools/youtube-tap-test.js
import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
@@ -26,7 +28,7 @@ const server = Bun.serve({ port: 0, fetch: (req) => {
return new Response(html, { headers: { 'content-type': 'text/html' } });
} });
// YouTube, stood in for: the player page notes every command it is sent
-const PLAYER = '<!doctype html><html><body style="margin:0;background:#222;height:100vh"><script>window.__cmds=[];addEventListener("message",(e)=>{try{const m=JSON.parse(e.data);if(m.event==="command")window.__cmds.push(m.func)}catch{}});</script></body></html>';
+const PLAYER = '<!doctype html><html><body style="margin:0;background:#222;height:100vh"><script>window.__cmds=[];addEventListener("message",(e)=>{try{const m=JSON.parse(e.data);if(m.event==="command")window.__cmds.push(m.func)}catch{}});window.__clicks=0;addEventListener("click",()=>{window.__clicks++;if(!window.__fsButton)return;if(document.fullscreenElement)document.exitFullscreen();else document.documentElement.requestFullscreen().catch(()=>{})});</script></body></html>';
const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox', '--autoplay-policy=no-user-gesture-required'] });
async function run(saveData) {
@@ -55,7 +57,8 @@ async function run(saveData) {
const st = () => page.evaluate(() => { const b = document.querySelector('.yt-embed'); const f = b && b.querySelector('iframe'); const sh = b && b.querySelector('.yt-shield'); return { fs: !!b && document.fullscreenElement === b, iframe: !!f, muted: !!f && /mute=1/.test(f.src), shield: !!sh && getComputedStyle(sh).display !== 'none', thread: !!document.querySelector('.thread-card') }; });
const cmds = async () => { const f = page.frames().find((x) => x.url().startsWith('https://www.youtube-nocookie.com/')); return f ? f.evaluate(() => window.__cmds.slice()) : null; };
try {
- await page.setViewport({ width: 390, height: 844 });
+ // a phone: a finger, no hover (a mouse is run apart, below)
+ await page.setViewport({ width: 390, height: 844, hasTouch: true, isMobile: true });
await page.goto(server.url.href, { waitUntil: 'domcontentloaded' });
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
@@ -85,13 +88,13 @@ async function run(saveData) {
await sleep(500);
// YouTube's control bar along the bottom is YouTube's
let b = await box();
- await page.mouse.click(b.x + b.w * 0.3, b.y + b.h - 20); await sleep(500);
+ await page.touchscreen.tap(b.x + b.w * 0.3, b.y + b.h - 20); await sleep(500);
check('a tap on YouTube\'s control bar stays YouTube\'s', !(await st()).fs && !(await st()).thread, JSON.stringify(await st()));
} else {
check('with Data Saver, the still and a play button', !s.iframe && await page.evaluate(() => !!document.querySelector('.yt-embed .yt-play')), JSON.stringify(s));
}
const b = await box();
- await page.mouse.click(b.x + b.w * 0.3, b.y + b.h * 0.35);
+ await page.touchscreen.tap(b.x + b.w * 0.3, b.y + b.h * 0.35);
check('one tap on the picture: full screen', await waitFor(() => document.fullscreenElement === document.querySelector('.yt-embed'), null, 3000), JSON.stringify(await st()));
s = await st();
check('…the player there', s.iframe);
@@ -102,9 +105,82 @@ async function run(saveData) {
await page.evaluate(() => document.exitFullscreen()); await sleep(500);
s = await st();
check('out of full screen, the layer is back', s.shield && !s.fs, JSON.stringify(s));
+ // YouTube's own full-screen button, pressed while we hold the screen:
+ // the frame asks for full screen from inside — that press means "out"
+ const b2 = await box();
+ await page.touchscreen.tap(b2.x + b2.w * 0.3, b2.y + b2.h * 0.35);
+ await waitFor(() => document.fullscreenElement === document.querySelector('.yt-embed'), null, 3000);
+ await page.frames().find((x) => x.url().startsWith('https://www.youtube-nocookie.com/')).evaluate(() => { window.__fsButton = true; });
+ await page.touchscreen.tap(200, 400); // inside the frame now
+ check('full screen, the player\'s own full-screen button leaves it', await waitFor(() => !document.fullscreenElement, null, 3000), await page.evaluate(() => String(document.fullscreenElement && document.fullscreenElement.tagName)));
check('no page errors', errs.length === 0, errs.slice(0, 2).join(' | '));
} finally { await ctx.close(); }
}
-try { await run(false); await run(true); } finally { await browser.close(); server.stop(true); }
+// A mouse reaches the player's own buttons, so its clicks are the player's:
+// the middle one pauses, the corner one goes full screen and back.
+async function runMouse() {
+ console.log('\n[a mouse: the player takes its own clicks]');
+ const ctx = await browser.createBrowserContext();
+ const page = await ctx.newPage();
+ await page.setRequestInterception(true);
+ page.on('request', (r) => {
+ const u = r.url();
+ if (u.startsWith('https://www.youtube-nocookie.com/embed/')) return r.respond({ status: 200, contentType: 'text/html', body: PLAYER });
+ if (u.startsWith('https://i.ytimg.com/')) return r.respond({ status: 200, contentType: 'image/png', body: png });
+ return r.continue();
+ });
+ await page.evaluateOnNewDocument(() => {
+ const Real = window.WebSocket;
+ window.WebSocket = function (...a) { const ws = new Real(...a); const send = ws.send.bind(ws); ws.send = (d) => { try { const m = JSON.parse(d); if (m[0] === 'REQ') { setTimeout(() => ws.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(['EOSE', m[1]]) })), 0); return; } if (m[0] === 'EVENT') return; } catch {} return send(d); }; return ws; };
+ window.WebSocket.prototype = Real.prototype; Object.assign(window.WebSocket, Real);
+ });
+ const errs = [];
+ page.on('pageerror', (e) => errs.push(String(e)));
+ const click = (t) => page.evaluate((x) => { const e = [...document.querySelectorAll('button')].find((n) => n.textContent.trim().toLowerCase().includes(x)); if (e) { e.click(); return true; } return false; }, t);
+ const waitText = async (x, ms = 25000) => { for (let i = 0; i < ms / 250; i++) { if ((await page.evaluate(() => document.body.innerText)).toLowerCase().includes(x)) return true; await sleep(250); } return false; };
+ const waitFor = async (fn, arg, ms = 10000) => { for (let i = 0; i < ms / 100; i++) { if (await page.evaluate(fn, arg)) return true; await sleep(100); } return false; };
+ const box = () => page.evaluate(() => { const b = document.querySelector('.notes-feed .yt-embed').getBoundingClientRect(); return { x: b.x, y: b.y, w: b.width, h: b.height }; });
+ const fsTag = () => page.evaluate(() => (document.fullscreenElement ? document.fullscreenElement.tagName : ''));
+ const player = () => page.frames().find((x) => x.url().startsWith('https://www.youtube-nocookie.com/'));
+ try {
+ await page.setViewport({ width: 1280, height: 900 });
+ await page.goto(server.url.href, { waitUntil: 'domcontentloaded' });
+ await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
+ await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
+ await click('create a new wallet'); await sleep(600);
+ await seedWords(page);
+ const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
+ await skipSeedCheck(page);
+ await waitText('receive', 20000);
+ const base = cacheKeyFor(mn + '\n');
+ await page.evaluate(([k, pk, ns]) => {
+ localStorage.setItem(k + ':follows', JSON.stringify({ tags: [['p', pk]], c: '', at: Math.floor(Date.now() / 1000) }));
+ localStorage.setItem(k + ':feedNotes', JSON.stringify(ns));
+ localStorage.setItem(k + ':profiles', JSON.stringify({ [pk]: { name: 'Tuber', t: Date.now() } }));
+ localStorage.setItem('btc-wallet-feed:' + k, 'following');
+ }, [base, AUTHOR, POSTS]);
+ await page.reload({ waitUntil: 'domcontentloaded' });
+ await waitText('receive', 20000);
+ await page.evaluate(() => { const n = document.querySelector('.app-nav-button[aria-label="Feed"]') || [...document.querySelectorAll('a,button')].find((x) => /^feed$/i.test((x.getAttribute('aria-label') || x.textContent || '').trim())); if (n) n.click(); });
+ await page.waitForSelector('.notes-feed .yt-embed', { timeout: 20000 });
+ await page.evaluate(() => document.querySelector('.notes-feed .yt-embed').scrollIntoView({ block: 'center' }));
+ await waitFor(() => !!document.querySelector('.yt-embed iframe'), null, 6000);
+ await sleep(800);
+ const b = await box();
+ await page.mouse.move(b.x + b.w * 0.4, b.y + b.h * 0.4); await sleep(200);
+ check('under a mouse, no layer over the player', await page.evaluate(() => { const sh = document.querySelector('.yt-embed .yt-shield'); return !sh || getComputedStyle(sh).display === 'none'; }));
+ await player().evaluate(() => { window.__fsButton = true; }); // from here every click is the corner button
+ await page.mouse.click(b.x + b.w * 0.5, b.y + b.h * 0.5); await sleep(600);
+ check('a click in the middle reaches the player', (await player().evaluate(() => window.__clicks)) === 1);
+ check('…and the page does not take the box full screen', (await fsTag()) !== 'DIV', await fsTag());
+ check('…nor open the thread', !(await page.evaluate(() => !!document.querySelector('.thread-card'))));
+ // (the stand-in goes full screen on a click, as YouTube's corner button does)
+ check('the player\'s own full screen is left to it', (await fsTag()) === 'IFRAME', await fsTag());
+ await page.mouse.click(400, 300); await sleep(600);
+ check('…and its button brings it back', (await fsTag()) === '', await fsTag());
+ check('no page errors', errs.length === 0, errs.slice(0, 2).join(' | '));
+ } finally { await ctx.close(); }
+}
+try { await run(false); await run(true); await runMouse(); } finally { await browser.close(); server.stop(true); }
console.log(ok ? '\n✅ a YouTube clip: one tap, full screen, with sound' : '\n❌ failed');
process.exit(ok ? 0 : 1);