diff options
| author | Adam Soltys <[email protected]> | 2026-10-17 05:50:08 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-17 05:50:08 -0700 |
| commit | bfcdc0beb9b0016aa0c58043c993eaad73825fe9 (patch) | |
| tree | a170c3494a999ca63ba5d92b6486475f72ea14dd /tools | |
| parent | 5615d645875f80300ea813de266b292f02ccd93f (diff) | |
| download | coinosv3-bfcdc0beb9b0016aa0c58043c993eaad73825fe9.tar.gz coinosv3-bfcdc0beb9b0016aa0c58043c993eaad73825fe9.zip | |
The picture pager works like the video pager: pictures join once decoded, the screen's way round with the shape toggle, and it flies (and counts) once ten are ready; flights end without the crawl, and the feed beneath doesn't repaint mid-flight.HEADmaster
Adam, 2026-10-17: "image pager should work similar. we need the tall only chip there too. i'm noticing some stutter/choppiness when flying images. especially towards the end of the fly as it's snapping to the one it's landing on." Measured on his phone (Pixel, 60 Hz) over synthesized touch flings in the live image pager: frames of 33–183 ms mid-flight, some with no script running (pictures fetched and decoded as the flight came near them: the near observer attached every post within 250% of the screen, a few of them originals — GIFs, hosts the server copies can't reach); the feed under the pager re-rendering (renderThreadStable, 40–55 ms each) during flights; and the flight's ease-out cube spending its last ~400 ms under 3 px a frame, the landing picture stepping a pixel at a time.
- A picture post waits out of the carousel until its first picture is decoded (img.decode(), the copy falling back to the original as before). pagerWarmPics keeps four decoding at a time, up to sixteen ready ahead of the reader and three behind; a post whose copy and original both fail stays out. The picture's size, learned from the decode, places it for the shape rule.
- The shape rule and its toggle ("Tall only" / "Wide only" / "All shapes", one setting with the videos) are in the picture pager's top bar too.
- Flying needs ten posts ready, for pictures as for clips; the counter is hidden until then while posts are still warming (a pager with fewer than ten, all of them ready, counts them). The counter fade on settling applies to every pager (the frame loop's motion flag now runs for pictures too).
- A flight eases out on a square instead of a cube: no long crawl at the end, and it sets off nearer the finger's speed.
- scheduleRepaint holds off while the pager is moving.
- pagerLoadAhead fills to the posts found (waiting ones too), not just the ready ones, so a gated pager doesn't crawl page after page.
Tests: new tools/picture-pager-ready-test.js (a picture held back by the server waits out and a step passes over it; it joins above the one on screen without moving it; the wide one is out; no counter while one is warming, 3 / 6 once all are in; the toggle lets the wide one in and out and is as tall as the close button). pager-gone-picture now checks the dead picture is passed over. Picture tests not about shape turn all shapes on; pager-swipe-fly has twelve posts (ten ready before it flies) and waits out the longer flights; pager-loading's fixture gains pagerFound; pager-ready-gate: four clips all ready show their count. All pager tests pass except feed-pager-ahead and feed-video-pager, which fail the same checks as on a clean checkout.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/feed-picture-pager-test.js | 1 | ||||
| -rw-r--r-- | tools/pager-gone-picture-test.js | 19 | ||||
| -rw-r--r-- | tools/pager-loading-test.js | 2 | ||||
| -rw-r--r-- | tools/pager-ready-gate-test.js | 2 | ||||
| -rw-r--r-- | tools/pager-swipe-fly-test.js | 8 | ||||
| -rw-r--r-- | tools/picture-pager-ready-test.js | 102 | ||||
| -rw-r--r-- | tools/profile-pager-test.js | 1 | ||||
| -rw-r--r-- | tools/quote-pager-test.js | 1 |
8 files changed, 119 insertions, 17 deletions
diff --git a/tools/feed-picture-pager-test.js b/tools/feed-picture-pager-test.js index ff69626..4cbe68c 100644 --- a/tools/feed-picture-pager-test.js +++ b/tools/feed-picture-pager-test.js @@ -36,6 +36,7 @@ const seeded = [ 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); diff --git a/tools/pager-gone-picture-test.js b/tools/pager-gone-picture-test.js index 683493b..34c89b6 100644 --- a/tools/pager-gone-picture-test.js +++ b/tools/pager-gone-picture-test.js @@ -1,8 +1,7 @@ // A picture whose host is down (its server answers 502, so neither a copy -// nor the original can load) shows a plain "couldn't be loaded" slide with -// the host's name, not the browser's broken-picture glyph in a corner -// (Adam's screenshot, 2026-10-16: isolabellart's relay down). The slides -// around it are untouched. Run: bun tools/pager-gone-picture-test.js +// nor the original can load) never joins the pager's carousel: a picture +// joins once decoded (2026-10-17), and a step passes over it to the next. +// (Its slide is marked gone, with the host's name, as before — out of sight.) Run: bun tools/pager-gone-picture-test.js import puppeteer from './puppeteer.js'; import { cacheKeyFor } from '../src/wallet.js'; import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure'; @@ -30,6 +29,7 @@ const seeded = [post('first big ' + U('big1'), 100), post('second big ' + U('big ...Array.from({ length: 10 }, (_, i) => post('filler ' + i + ' ' + 'lorem ipsum dolor sit amet. '.repeat(5), 500 + i * 60))]; 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); @@ -63,15 +63,10 @@ try { await page.mouse.click(at.x, at.y); check('the picture opens in the pager', await waitFor(() => !!document.querySelector('.media-pager .mp-slide img.mp-media'), null, 4000)); check('the picture that loads is sharpened at rest', await waitFor(() => { const c = document.querySelector('.mp-slide.mp-cur img.mp-media'); return c && c.getAttribute('src').endsWith('/big1.png'); }, null, 5000)); + check('the dead picture\'s post never joins the carousel: both its copy and its original were tried', await waitFor(() => { const s = [...document.querySelectorAll('.mp-slide')].find((x) => x.getAttribute('data-k').endsWith('/big2.png')); const g = s && s.closest('.mp-group'); return !!g && g.classList.contains('mp-wait') && s.classList.contains('mp-gone') && s.querySelector('img.mp-media').getAttribute('src').endsWith('/big2.png'); }, null, 8000)); await page.keyboard.press('ArrowDown'); - check('down: the dead picture\'s slide', await waitFor(() => { const c = document.querySelector('.mp-slide.mp-cur'); return c && c.getAttribute('data-k').endsWith('/big2.png'); }, null, 4000)); - check('it says the picture couldn\'t be loaded, naming the host', await waitFor(() => { const c = document.querySelector('.mp-slide.mp-cur'); return c.classList.contains('mp-gone') && /couldn’t be loaded/.test(c.textContent) && /localhost/.test(c.querySelector('.mp-missing')?.textContent || ''); }, null, 6000), await page.evaluate(() => document.querySelector('.mp-slide.mp-cur').outerHTML.slice(0, 300))); - check('…and the broken <img> is hidden', await page.evaluate(() => getComputedStyle(document.querySelector('.mp-slide.mp-cur img.mp-media')).display === 'none')); - check('the original was tried after the copy failed', await page.evaluate(() => document.querySelector('.mp-slide.mp-cur img.mp-media').getAttribute('src').endsWith('/big2.png'))); - await page.keyboard.press('ArrowDown'); - check('the next slide is fine', await waitFor(() => { const c = document.querySelector('.mp-slide.mp-cur'); return c && c.getAttribute('data-k').endsWith('/big3.png') && !c.classList.contains('mp-gone'); }, null, 4000) && await waitFor(() => document.querySelector('.mp-slide.mp-cur img.mp-media').getAttribute('src').endsWith('/big3.png'), null, 5000)); - check('its caption and controls are still there on the dead one', await page.evaluate(() => { const g = [...document.querySelectorAll('.mp-slide')].find((x) => x.getAttribute('data-k').endsWith('/big2.png')); return !!g.querySelector('.mp-like') && /second big/.test(g.textContent); })); + check('down: past it, to the next picture', await waitFor(() => { const c = document.querySelector('.mp-slide.mp-cur'); return c && c.getAttribute('data-k').endsWith('/big3.png') && !c.classList.contains('mp-gone'); }, null, 4000) && await waitFor(() => document.querySelector('.mp-slide.mp-cur img.mp-media').getAttribute('src').endsWith('/big3.png'), null, 5000), await page.evaluate(() => document.querySelector('.mp-slide.mp-cur')?.getAttribute('data-k'))); 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 dead picture says so' : '\n❌ failed'); +console.log(ok ? '\n✅ a dead picture is passed over' : '\n❌ failed'); process.exit(ok ? 0 : 1); diff --git a/tools/pager-loading-test.js b/tools/pager-loading-test.js index 76e2764..42a5344 100644 --- a/tools/pager-loading-test.js +++ b/tools/pager-loading-test.js @@ -21,7 +21,7 @@ function fixture(kind = 'images', grid = false) { }; const src = { c: cache, more, done: () => false }; const ctx = vm.createContext({ - pager: p, pagerGroups: () => groups.slice(0, count), + pager: p, pagerGroups: () => groups.slice(0, count), pagerFound: () => groups.slice(0, count), pagerSource: () => grid ? null : src, feedNow: () => cache, mediaIndex: () => index, feedDef: () => ({}), crawlMedia: (c, k) => { assert.equal(c, cache); assert.equal(k, kind); return more(); }, growPager: () => {}, Date: { now: () => clock }, diff --git a/tools/pager-ready-gate-test.js b/tools/pager-ready-gate-test.js index 3a4f2be..dd6982d 100644 --- a/tools/pager-ready-gate-test.js +++ b/tools/pager-ready-gate-test.js @@ -135,7 +135,7 @@ try { for (let i = 1; i <= 3; i++) { while (performance.now() < t0 + 80 * i / 3); fire('touchmove', 700 - 600 * i / 3); } fire('touchend', 100, true); }); - check('with ' + ready + ' clips ready, no counter shows', await page.evaluate(() => getComputedStyle(document.querySelector('.mp-position')).visibility === 'hidden')); + check('with ' + ready + ' clips ready and none still warming, the counter shows (that is all there is)', await page.evaluate(() => getComputedStyle(document.querySelector('.mp-position')).visibility === 'visible')); check('with ' + ready + ' clips ready, a hard flick from tall2 goes one post, to tall3', ready < 10 && await waitFor(() => { const s = document.querySelector('.mp-slide.mp-cur'); return !!s && /tall3/.test(s.getAttribute('data-k')); }, null, 4000), String(await curClip())); await page.keyboard.press('ArrowUp'); await waitFor(() => /tall2/.test(document.querySelector('.mp-slide.mp-cur')?.getAttribute('data-k') || ''), null, 4000); diff --git a/tools/pager-swipe-fly-test.js b/tools/pager-swipe-fly-test.js index 418b8e4..6f62007 100644 --- a/tools/pager-swipe-fly-test.js +++ b/tools/pager-swipe-fly-test.js @@ -11,7 +11,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 +26,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 +34,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 @@ -100,7 +101,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(); diff --git a/tools/picture-pager-ready-test.js b/tools/picture-pager-ready-test.js new file mode 100644 index 0000000..7cfaa45 --- /dev/null +++ b/tools/picture-pager-ready-test.js @@ -0,0 +1,102 @@ +// The picture pager works like the video pager (Adam, 2026-10-17: "image +// pager should work similar. we need the tall only chip there too"): a +// picture post joins the carousel once its picture is decoded, so a step +// passes over one still on its way; the shapes the wrong way round for +// the screen stay out, with the "Tall only" / "All shapes" toggle in the +// top bar; and with fewer than ten ready and more still warming, no +// counter shows and a flick goes one post. Run: bun tools/picture-pager-ready-test.js +import puppeteer from './puppeteer.js'; +import { cacheKeyFor } from '../src/wallet.js'; +import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure'; +import { buildHtml } from '../build.js'; +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; +// t1..t4 tall, w1 wide, sq square; slow (tall) is held back until released +const PICS = { t1: png('300x400'), t2: png('300x420'), slow: png('300x440'), t3: png('300x460'), w1: png('480x270'), sq: png('320x320'), t4: png('300x480') }; +let release; const held = new Promise((r) => (release = r)); +const SK = generateSecretKey(), AUTHOR = getPublicKey(SK); +const now = Math.floor(Date.now() / 1000); +const post = (content, ago) => finalizeEvent({ kind: 1, created_at: now - ago, tags: [], content }, SK); +const html = await buildHtml({ minify: true, pwa: false }); +const server = Bun.serve({ port: 0, fetch: async (req) => { + const path = new URL(req.url).pathname; + if (path.startsWith('/punks')) return new Response(Bun.file('dist' + path)); + const m = /^\/(\w+)\.png$/.exec(path); + if (m && PICS[m[1]]) { if (m[1] === 'slow') await held; return new Response(PICS[m[1]], { headers: { 'content-type': 'image/png', 'cache-control': 'no-store' } }); } + return new Response(html, { headers: { 'content-type': 'text/html' } }); +} }); +const U = (n) => server.url.origin + '/' + n + '.png'; +const ORDER = ['t1', 'slow', 't2', 'w1', 'sq', 't3', 't4']; +const seeded = ORDER.map((n, i) => post('picture ' + n + ' ' + U(n), 100 + i * 100)); + +const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] }); +const page = await browser.newPage(); +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 name = (k) => k.split('|')[1].split('/').pop().replace('.png', ''); +const carousel = () => page.evaluate(() => [...document.querySelectorAll('.mp-scroll > .mp-group:not(.mp-wait):not(.mp-off)')].map((g) => g.querySelector('.mp-slide').getAttribute('data-k').split('/').pop().replace('.png', '')).join(' ')); +const cur = () => page.evaluate(() => { const s = document.querySelector('.mp-slide.mp-cur'); return s ? s.getAttribute('data-k').split('/').pop().replace('.png', '') : null; }); +const resting = (n) => waitFor((n) => { const s = [...document.querySelectorAll('.mp-slide')].find((x) => { const r = x.getBoundingClientRect(); return Math.abs(r.top) < 2 && Math.abs(r.left) < 2; }); return !!s && s.getAttribute('data-k').endsWith('/' + n + '.png'); }, n, 5000); + +try { + await page.setViewport({ width: 390, height: 844 }); + 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 page.waitForSelector('.words .w .t'); + const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' '); + await click('skip verification'); + 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: 'Picture Poster', t: Date.now() } })); + localStorage.setItem('btc-wallet-feed:' + k, 'following'); + }, [base, AUTHOR, seeded]); + await page.reload({ waitUntil: 'domcontentloaded' }); + await waitText('receive', 20000); + await page.evaluate(() => document.querySelector('.app-nav-button[aria-label="Feed"]').click()); + check('the posts are in the feed', await waitFor(() => document.querySelectorAll('.notes-feed [data-key]').length >= 7, null, 20000)); + await waitFor(() => !!document.querySelector('.notes-feed img.note-img'), null, 10000); + await page.evaluate(() => { const r = [...document.querySelectorAll('.notes-feed [data-key]')].find((n) => n.textContent.includes('picture t1')); r.scrollIntoView({ block: 'center' }); }); + await sleep(400); + const at = await page.evaluate(() => { const r = [...document.querySelectorAll('.notes-feed [data-key]')].find((n) => n.textContent.includes('picture t1')); const i = r.querySelector('img.note-img').getBoundingClientRect(); return { x: i.x + i.width / 2, y: i.y + i.height / 2 }; }); + await page.mouse.click(at.x, at.y); + check('the first picture opens in the pager', await waitFor(() => !!document.querySelector('.media-pager .mp-slide'), null, 4000) && await resting('t1'), await cur()); + + console.log('\n[a picture joins once decoded]'); + check('the pictures decoded join: t2, t3, t4 and the square', await waitFor(() => document.querySelectorAll('.mp-scroll > .mp-group:not(.mp-wait):not(.mp-off)').length >= 5, null, 8000), await carousel()); + check('the one still on its way waits out of the carousel', !(await carousel()).includes('slow'), await carousel()); + check('the wide one is out for its shape', !(await carousel()).includes('w1'), await carousel()); + check('with fewer than ten ready and one still warming, no counter shows', await page.evaluate(() => getComputedStyle(document.querySelector('.mp-position')).visibility === 'hidden')); + await page.keyboard.press('ArrowDown'); + check('down from t1 passes over it, to t2', await resting('t2'), await cur()); + release(); + check('once it has come, it joins, above the one on screen', await waitFor(() => /t1 slow t2/.test([...document.querySelectorAll('.mp-scroll > .mp-group:not(.mp-wait):not(.mp-off)')].map((g) => g.querySelector('.mp-slide').getAttribute('data-k').split('/').pop().replace('.png', '')).join(' ')), null, 8000), await carousel()); + check('…without moving it', await resting('t2'), await cur()); + check('everything in, the counter shows: 3 / 6', await waitFor(() => getComputedStyle(document.querySelector('.mp-position')).visibility === 'visible' && document.querySelector('.mp-position').textContent === '3 / 6', null, 3000), await page.evaluate(() => document.querySelector('.mp-position').textContent)); + await page.keyboard.press('ArrowUp'); + check('up lands on it', await resting('slow'), await cur()); + + console.log('\n[the shape toggle in the picture pager]'); + const label = await page.evaluate(() => document.querySelector('.media-pager .mp-fit')?.textContent); + check('the top bar has the toggle, saying tall only', label === 'Tall only', String(label)); + check('…as tall as the close button', await page.evaluate(() => document.querySelector('.mp-fit').offsetHeight === document.querySelector('.mp-x').offsetHeight)); + await page.evaluate(() => document.querySelector('.mp-fit').click()); + check('tapped: the wide picture joins', await waitFor(() => [...document.querySelectorAll('.mp-scroll > .mp-group:not(.mp-wait):not(.mp-off)')].some((g) => /w1\.png$/.test(g.querySelector('.mp-slide').getAttribute('data-k'))), null, 6000), await carousel()); + check('…and the toggle says all shapes', (await page.evaluate(() => document.querySelector('.mp-fit').textContent)) === 'All shapes'); + check('…the picture on screen stayed put', await resting('slow'), await cur()); + await page.evaluate(() => document.querySelector('.mp-fit').click()); + check('tapped again: the wide one is out', await waitFor(() => !document.querySelector('.mp-scroll > .mp-group:not(.mp-wait):not(.mp-off) .mp-slide[data-k$="/w1.png"]'), null, 3000), await carousel()); + check('no page errors', errs.length === 0, errs.slice(0, 2).join(' | ')); +} catch (e) { console.error(e); ok = false; } finally { release(); await browser.close(); server.stop(true); } +console.log(ok ? '\n✅ the picture pager: decoded pictures only, the screen\'s way round, with the toggle' : '\n❌ failed'); +process.exit(ok ? 0 : 1); diff --git a/tools/profile-pager-test.js b/tools/profile-pager-test.js index 29341e7..aac681a 100644 --- a/tools/profile-pager-test.js +++ b/tools/profile-pager-test.js @@ -36,6 +36,7 @@ const seeded = [ 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); diff --git a/tools/quote-pager-test.js b/tools/quote-pager-test.js index 5d9e536..7f8b76c 100644 --- a/tools/quote-pager-test.js +++ b/tools/quote-pager-test.js @@ -39,6 +39,7 @@ const ALL = [...FEED, quotedPost]; 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) await page.evaluateOnNewDocument((evs) => { const match = (f, e) => (!f.kinds || f.kinds.includes(e.kind)) && (!f.authors || f.authors.includes(e.pubkey)) && (!f.ids || f.ids.includes(e.id)) && (!f.until || e.created_at <= f.until) && (!f.since || e.created_at >= f.since) && !Object.keys(f).some((k) => k.startsWith('#')); |
