aboutsummaryrefslogtreecommitdiffstats
path: root/tools/pager-swipe-fly-test.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/pager-swipe-fly-test.js')
-rw-r--r--tools/pager-swipe-fly-test.js97
1 files changed, 77 insertions, 20 deletions
diff --git a/tools/pager-swipe-fly-test.js b/tools/pager-swipe-fly-test.js
index aec8fdd..657ea75 100644
--- a/tools/pager-swipe-fly-test.js
+++ b/tools/pager-swipe-fly-test.js
@@ -1,8 +1,10 @@
// On a phone, a fast flick up or down in the media pager flies past several
// posts and lands snapped on one, the way a spin of the wheel does; a
// gentler swipe is one post, as before; a fling at the end lands on the
-// last post rather than wrapping past it; a finger put down mid-flight
-// catches it. Run: bun tools/pager-swipe-fly-test.js
+// last post and goes no further (no going round to the first); a tap
+// mid-flight stops it on the post most on screen, and a swipe back from
+// there goes to the one before.
+// Run: bun tools/pager-swipe-fly-test.js
import puppeteer from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
@@ -11,7 +13,7 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let ok = true;
const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; };
const png = (size) => Bun.spawnSync(['ffmpeg', '-loglevel', 'error', '-f', 'lavfi', '-i', 'testsrc=size=' + size, '-frames:v', '1', '-f', 'image2', '-c:v', 'png', 'pipe:1']).stdout;
-const N = 9, PIC = png('360x360');
+const N = 12, PIC = png('360x360');
const SK = generateSecretKey(), AUTHOR = getPublicKey(SK);
const now = Math.floor(Date.now() / 1000);
const html = await buildHtml({ minify: true, pwa: false });
@@ -26,6 +28,7 @@ const seeded = Array.from({ length: N }, (_, i) => finalizeEvent({ kind: 1, crea
const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
+await page.evaluateOnNewDocument(() => { try { localStorage.setItem('coinos-clips-fit', 'all'); } catch {} }); // (every shape: this test isn't about the screen's way round)
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);
@@ -33,7 +36,7 @@ const waitText = async (x, ms = 25000) => { for (let i = 0; i < ms / 250; i++) {
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; };
// the post the pager rests square on (its number), or 'between'
const at = () => page.evaluate(() => { const s = [...document.querySelectorAll('.mp-slide')].find((x) => { const r = x.getBoundingClientRect(); return Math.abs(r.top) < 1 && Math.abs(r.left) < 1; }); return s ? Number(/p(\d+)\.png/.exec(s.getAttribute('data-k'))[1]) : 'between'; });
-const settled = async () => { let last = null; for (let i = 0; i < 40; i++) { const t = await page.evaluate(() => document.querySelector('.mp-scroll').scrollTop); if (t === last) { await sleep(100); if (await page.evaluate(() => document.querySelector('.mp-scroll').scrollTop) === t) return; } last = t; await sleep(50); } };
+const settled = async () => { let last = null; for (let i = 0; i < 100; i++) { /* (a long flight takes up to 4 s) */ const t = await page.evaluate(() => document.querySelector('.mp-scroll').scrollTop); if (t === last) { await sleep(100); if (await page.evaluate(() => document.querySelector('.mp-scroll').scrollTop) === t) return; } last = t; await sleep(50); } };
// a swipe up the screen: px of travel over about `ms` milliseconds. A
// quick one is dispatched inside the page, as fast as the handlers take
// the moves (about 30–50 ms each here, headless): the browser's own touch
@@ -58,6 +61,23 @@ const swipe = async (px, ms, steps = 6) => {
fire('touchend', 700 - px, true);
}, [px, ms, steps]);
};
+// A swipe whose moves are made on schedule (each event stamped as the finger
+// moved) but handled late, the main thread busy: held back and delivered in
+// a burst at the end (burst), or the last move and the lift after a stall.
+const queued = (px, ms, steps, mode) => page.evaluate(([px, ms, steps, mode]) => {
+ const el = document.querySelector('.mp-scroll'), target = document.elementFromPoint(195, 700) || el;
+ const touch = (y) => new Touch({ identifier: 1, target, clientX: 195, clientY: y, pageX: 195, pageY: y });
+ const make = (type, y, up) => new TouchEvent(type, { bubbles: true, cancelable: true, touches: up ? [] : [touch(y)], changedTouches: [touch(y)], targetTouches: up ? [] : [touch(y)] });
+ const t0 = performance.now(), evs = [make('touchstart', 700)];
+ for (let i = 1; i <= steps; i++) { while (performance.now() < t0 + ms * i / steps); evs.push(make('touchmove', 700 - px * i / steps)); }
+ evs.push(make('touchend', 700 - px, true));
+ if (mode === 'burst') { for (const e of evs) target.dispatchEvent(e); return; }
+ // stall: all but the last move handled as they come, then 300 ms busy
+ const tail = evs.splice(-2);
+ for (const e of evs) target.dispatchEvent(e);
+ const t1 = performance.now(); while (performance.now() < t1 + 300);
+ for (const e of tail) target.dispatchEvent(e);
+}, [px, ms, steps, mode]);
try {
await page.setViewport({ width: 390, height: 844, hasTouch: true, isMobile: true });
await page.goto(server.url.href, { waitUntil: 'domcontentloaded' });
@@ -83,7 +103,8 @@ try {
const pic = await page.evaluate(() => { const r = [...document.querySelectorAll('.notes-feed [data-key]')].find((n) => n.textContent.includes('picture post 1 ')); const i = r.querySelector('img.note-img').getBoundingClientRect(); return { x: i.x + i.width / 2, y: i.y + i.height / 2 }; });
await page.touchscreen.tap(pic.x, pic.y);
check('the first picture opens in the pager', await waitFor(() => !!document.querySelector('.media-pager'), null, 4000) && (await (async () => { await settled(); return at(); })()) === 1, String(await at()));
- check('…with all the posts loaded', await waitFor((n) => document.querySelectorAll('.mp-group').length >= n, N, 8000));
+ check('…with all the posts loaded', await waitFor((n) => document.querySelectorAll('.mp-group').length >= n, N, 15000));
+ check('…and decoded, in the carousel (ten ready before it flies)', await waitFor((n) => document.querySelectorAll('.mp-scroll > .mp-group:not(.mp-wait):not(.mp-off)').length >= n, N, 10000), String(await page.evaluate(() => document.querySelectorAll('.mp-scroll > .mp-group:not(.mp-wait):not(.mp-off)').length)));
console.log('\n[a gentle swipe: one post]');
await swipe(320, 130); await settled();
@@ -91,37 +112,73 @@ try {
console.log('\n[a fast flick: several posts, landing snapped]');
const trace = async (sw) => { await page.evaluate(() => { const sc = document.querySelector('.mp-scroll'); window.__tr = []; const t0 = performance.now(); const rec = () => { window.__tr.push(sc.scrollTop); if (performance.now() - t0 < 1500) requestAnimationFrame(rec); }; requestAnimationFrame(rec); }); await sw(); await sleep(1600); return page.evaluate(() => window.__tr); };
+ await page.evaluate(() => { const el = document.querySelector('.mp-position'); window.__counts = [el.textContent]; new MutationObserver(() => { if (window.__counts.at(-1) !== el.textContent) window.__counts.push(el.textContent); }).observe(el, { childList: true, characterData: true, subtree: true }); });
const tr = await trace(() => swipe(600, 80, 3)); await settled(); // three big moves: 3–6 px/ms as their handling takes, a long coast's worth (past the nine loaded, maybe)
const landed = await at();
check('a 600px flick in a few frames flies several posts on', landed !== 'between' && landed >= 5, String(landed));
check('…landing square on a post', landed !== 'between');
check('…in a glide, never back', tr.every((x, i) => !i || x >= tr[i - 1] - 0.5), tr.slice(0, 12).map((x) => Math.round(x)).join(' '));
+ check('the counter ran through the posts on the way, not jumping on landing', (await page.evaluate(() => window.__counts)).length >= 4, (await page.evaluate(() => window.__counts)).join(' → '));
const seen = await page.evaluate(() => document.querySelector('.mp-position').textContent);
check('the counter says where it landed', seen === landed + ' / ' + N, seen);
- console.log('\n[a flick near the end coasts round to the start]');
+ console.log('\n[the ends are ends]');
while ((await at()) !== 7) { await page.keyboard.press((await at()) < 7 ? 'ArrowDown' : 'ArrowUp'); await settled(); }
const before = await at();
await swipe(600, 80, 3); await settled();
- check('from post ' + before + ' (a flight\'s worth would be past the end), a flick comes round to the first post', before >= 7 && (await at()) === 1, String(await at()));
- await swipe(-600, 80, 3); await settled();
- check('…and a flick back up from the first comes round to the last', (await at()) === N, String(await at()));
+ check('from post ' + before + ', a flick whose flight would be past the end lands on the last post', before === 7 && (await at()) === N, String(await at()));
await swipe(600, 80, 3); await settled();
- check('…a flick down from the last, to the first again', (await at()) === 1, String(await at()));
+ check('…a flick on from the last stays there (no going round to the first)', (await at()) === N, String(await at()));
+ await page.keyboard.press('ArrowDown'); await settled();
+ check('…and so does ↓', (await at()) === N, String(await at()));
+ while ((await at()) !== 1) { await page.keyboard.press('ArrowUp'); await settled(); }
+ await swipe(-600, 80, 3); await settled();
+ check('a flick back up from the first stays on the first', (await at()) === 1, String(await at()));
await swipe(320, 130); await settled();
check('a gentle swipe from the first: the second, as ever', (await at()) === 2, String(await at()));
- console.log('\n[a finger down mid-flight catches it]');
+ console.log('\n[a busy main thread: the speed is the finger\'s, not the handling\'s]');
+ // (the dispatch loop runs all the events in one go: built on schedule, each stamped as it was made)
+ while ((await at()) !== 1) { await page.keyboard.press('ArrowUp'); await settled(); }
+ await queued(200, 200, 8, 'burst'); await settled();
+ check('from post 1, a gentle 200px swipe over 200ms, its moves handled in one burst: one post on', (await at()) === 2, String(await at()));
+ await queued(600, 80, 4, 'stall'); await settled();
+ check('a fast flick whose last move was handled 300ms late still flies', typeof (await at()) === 'number' && (await at()) >= 5, String(await at()));
+ while ((await at()) !== 1) { await page.keyboard.press('ArrowUp'); await settled(); }
+
+ console.log('\n[a tap mid-flight stops it on the post most on screen]');
+ // the post most on screen as the finger comes down (its number), then the touch, in one go
+ const down = () => page.evaluate(() => {
+ const sc = document.querySelector('.mp-scroll'), y = sc.scrollTop;
+ const g = [...sc.querySelectorAll(':scope > .mp-group:not(.mp-wait):not(.mp-off)')].reduce((b, x) => (Math.abs(x.offsetTop - y) < Math.abs(b.offsetTop - y) ? x : b));
+ const target = window.__tt = document.elementFromPoint(195, 500), t = new Touch({ identifier: 2, target, clientX: 195, clientY: 500, pageX: 195, pageY: 500 });
+ target.dispatchEvent(new TouchEvent('touchstart', { bubbles: true, cancelable: true, touches: [t], changedTouches: [t], targetTouches: [t] }));
+ return Number(/p(\d+)\.png/.exec(g.querySelector('.mp-slide').getAttribute('data-k'))[1]);
+ });
+ // the same finger moving (to y), or lifted there
+ const finger = (y, up) => page.evaluate(([y, up]) => {
+ const target = window.__tt, t = new Touch({ identifier: 2, target, clientX: 195, clientY: y, pageX: 195, pageY: y });
+ target.dispatchEvent(new TouchEvent(up ? 'touchend' : 'touchmove', { bubbles: true, cancelable: true, touches: up ? [] : [t], changedTouches: [t], targetTouches: up ? [] : [t] }));
+ }, [y, up]);
+ await swipe(600, 80, 3);
+ await sleep(150);
+ const flying = await page.evaluate(() => document.querySelector('.mp-scroll').scrollTop);
+ const most = await down();
+ await sleep(60); await finger(500, true);
+ await sleep(250);
+ check('a tap mid-flight: on the post most on screen in a moment', flying > 0 && (await at()) === most, most + ' wanted, ' + (await at()));
+ await settled();
+ check('…and stays there', (await at()) === most, String(await at()));
+ check('…the tap only stopped it (no details brought up)', !(await page.evaluate(() => document.querySelector('.media-pager').classList.contains('show'))));
+
+ console.log('\n[a swipe back mid-flight: the post before]');
+ while ((await at()) !== 1) { await page.keyboard.press('ArrowUp'); await settled(); }
await swipe(600, 80, 3);
- await sleep(120);
- const mid = await page.evaluate(() => document.querySelector('.mp-scroll').scrollTop);
- await page.touchscreen.touchStart(195, 500);
- await page.touchscreen.touchMove(195, 480); await sleep(30);
- await page.touchscreen.touchMove(195, 470); await sleep(200);
- const held = await page.evaluate(() => document.querySelector('.mp-scroll').scrollTop);
- await page.touchscreen.touchEnd(); await settled();
- check('the flight stops under the finger', mid > 0 && Math.abs(held - mid) < 844, mid + ' → ' + held);
- check('…and lands square on a post when it lifts', (await at()) !== 'between', String(await at()));
+ await sleep(150);
+ const most2 = await down();
+ for (const y of [520, 560, 640, 760]) { await finger(y); await sleep(20); }
+ await finger(760, true); await settled();
+ check('caught on post ' + most2 + ' and swiped back: post ' + (most2 - 1), most2 > 1 && (await at()) === most2 - 1, String(await at()));
check('no page errors', errs.length === 0, errs.slice(0, 2).join(' | '));
} catch (e) { console.error(e); ok = false; } finally { await browser.close(); server.stop(true); }
console.log(ok ? '\n✅ a fast flick flies through the pager' : '\n❌ failed');