diff options
| author | Adam Soltys <[email protected]> | 2026-10-11 22:47:47 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-11 22:47:47 -0700 |
| commit | 6eda01863920ae45d56a65563a7f956a429fced8 (patch) | |
| tree | 5e01151020f152cb6131672ea1983fa26412b972 | |
| parent | 880abf73720acc13e07242568d56c14bca247c5e (diff) | |
| download | coinosv3-6eda01863920ae45d56a65563a7f956a429fced8.tar.gz coinosv3-6eda01863920ae45d56a65563a7f956a429fced8.zip | |
Feed: a picture opens in the media pager, with the feed's other pictures a swipe away. Tapping a picture in a feed row used to open it alone (the lightbox); it now opens the media grid's pager on that picture, over the feed's pictures in the feed's own order — every picture of a post before the next post's (a repost's inner post, the feed's moderation) — so a swipe up or down, or any arrow key (up/down as before, now left/right too), steps to the next or previous one. A tap on the picture that opened it still closes it, as a full-screen picture always has; once the reader has moved on, the first tap shows the post's details and actions, and the next (or Escape, or the ×) closes the pager with the feed landed on that post (glideToPost, once the pop has brought the feed back). The pager's pictures pinch to zoom (attachZoom, now exported, without its wheel — the wheel is the pager's; at 1× one finger is the pager's swipe). Pictures in threads, profiles and chats still open alone. tools/feed-picture-pager-test.js; image-tap-test and lightbox-scroll-test accept the pager as the full-screen picture.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
| -rw-r--r-- | src/app.js | 7 | ||||
| -rw-r--r-- | src/features/messages.js | 87 | ||||
| -rw-r--r-- | src/style.css | 2 | ||||
| -rw-r--r-- | tools/feed-picture-pager-test.js | 131 | ||||
| -rw-r--r-- | tools/image-tap-test.js | 4 | ||||
| -rw-r--r-- | tools/lightbox-scroll-test.js | 4 |
6 files changed, 220 insertions, 15 deletions
@@ -620,7 +620,7 @@ function imageViewer() { // off, so the picture does its own. Only the first-mounted nodes keep these // listeners: later renders morph into them, and the img is left out of the // morph so its transform survives a background render. -function attachZoom(box, img) { +function attachZoom(box, img, { wheel = true } = {}) { img._skipMorph = true; const MAX = 5, pts = new Map(); let s = 1, x = 0, y = 0, pinch = null, pan = null, moved = false; @@ -634,6 +634,8 @@ function attachZoom(box, img) { img.style.transition = animate ? 'transform .2s ease' : 'none'; img.style.transform = s === 1 && !x && !y ? '' : `translate(${x}px, ${y}px) scale(${s})`; img.style.cursor = s > 1 ? 'grab' : ''; + // zoomed, the finger pans the picture; at 1× the touch is the page's (a pager's swipe) + img.style.touchAction = s > 1 ? 'none' : ''; }; // scale to ns keeping the picture point under (cx, cy) where it is const zoomAt = (ns, cx, cy) => { @@ -693,7 +695,7 @@ function attachZoom(box, img) { if (e.target.closest('.lightbox-x')) return; if (moved) { moved = false; e.stopImmediatePropagation(); e.preventDefault(); return; } }, true); - box.addEventListener('wheel', (e) => { + if (wheel) box.addEventListener('wheel', (e) => { e.preventDefault(); zoomAt(s * Math.exp(-e.deltaY * 0.002), e.clientX, e.clientY); clamp(); apply(false); @@ -6243,6 +6245,7 @@ const ctx = { parseAmount: parseField, worthLine, openImage: (src) => { ui.lightbox = src; render(); }, + attachZoom: (box, img, opts) => attachZoom(box, img, opts), // What woke this run, if anything did, and how to say we're finished. // Both read through a closure: ctx is built before these are defined, and // a bare `wakeDone` shorthand captured the binding while it was still diff --git a/src/features/messages.js b/src/features/messages.js index 7c81d27..b1da365 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -7965,7 +7965,7 @@ export function messagesFeature(ctx) { if (mediaFailed.has(url)) return h('a', { href: url, target: '_blank', rel: 'noopener noreferrer' }, label || url); return h('img', { src: url, class: 'note-img note-img-late clickable', loading: 'lazy', decoding: 'async', alt: label || '', - onClick: (e) => { if (!onPicture(e)) return; e.stopPropagation(); ctx.openImage && ctx.openImage(url); }, + onClick: (e) => { if (!onPicture(e)) return; e.stopPropagation(); openPicture(url, e); }, onError: (e) => { e.target.style.display = 'none'; }, }); } @@ -7978,7 +7978,7 @@ export function messagesFeature(ctx) { decoding: known && known.thumb ? 'sync' : 'async', width: size?.width, height: size?.height, style: size ? 'height:auto;aspect-ratio:' + size.width + '/' + size.height + ';--ar:' + (size.width / size.height).toFixed(4) : undefined, - onClick: (e) => { if (!onPicture(e)) return; e.stopPropagation(); ctx.openImage && ctx.openImage(url); }, + onClick: (e) => { if (!onPicture(e)) return; e.stopPropagation(); openPicture(url, e); }, // a picture we were TOLD was a picture and which won't load leaves // nothing behind — the alt text is already in the sentence above it onError: (e) => { if (!size) e.target.style.display = 'none'; }, @@ -11597,6 +11597,37 @@ export function messagesFeature(ctx) { } return out; } + // The feed's pictures in reading order — every one of a post before the + // next post's — for the pager opened from a picture in the feed: the + // feed's own list (its order, its moderation), not the media index. + function feedImageItems(c) { + const out = []; + const list = c.winList ? c.winList() : c.notes; + for (const row of list) { + const ev = row.kind === REPOST ? repostInner(row) || row : row; + if (modOn('sensitive') && sensitiveWhy(ev) !== null && !isMe(ev.pubkey)) continue; + for (const url of noteMediaUrls(ev.content)) { + if (url.startsWith('https://i.ytimg.com/')) continue; + out.push({ ev, url, row }); + } + } + return out; + } + // A picture tapped in the feed opens in the media pager, on that picture, + // with the feed's other pictures a swipe (or an arrow) away, the way the + // media grid's are. On any other page — a thread, a profile, a chat — the + // picture opens alone, as before. + function openPicture(url, e) { + try { + const inFeed = ui.chatOpen && ui.msgView === 'feed' && !ui.profilePk && !ui.noteThread && feed && !feed.stopped && feed.winList; + const rowId = e && e.target && e.target.closest ? e.target.closest('.notes-feed > [data-key]')?.getAttribute('data-key') : null; + if (inFeed && rowId) { + const it = feedImageItems(feed).find((x) => x.row.id === rowId && x.url === url); + if (it) { openMediaPager('images', mediaKey(it), { fromFeed: true }); return; } + } + } catch {} + ctx.openImage && ctx.openImage(url); + } // The words of the post, without the links to the media it carries. function mediaCaption(ev) { let s = String(ev.content || ''); @@ -11722,11 +11753,35 @@ export function messagesFeature(ctx) { } } - function openMediaPager(kind, start) { - ui.mediaPager = { kind, start, feed: curFeedId }; + // `fromFeed`: opened from a picture in the feed (its pictures, in the + // feed's order), rather than from the media grid + function openMediaPager(kind, start, { fromFeed = false } = {}) { + ui.mediaPager = { kind, start, feed: curFeedId, fromFeed }; render(); } const closeMediaPager = () => ctx.goBack(() => { ui.mediaPager = null; }); + // still on the picture that opened the pager (nothing swiped to yet) + const pagerOnStart = () => !pager || !pager.cur || pager.cur.getAttribute('data-k') === pager.start; + // Dismissing a pager opened from the feed after moving through it lands + // the feed on the post of the picture last looked at; otherwise (the + // media grid's pager, or nothing moved) it simply closes. + function dismissPager() { + if (pager && pager.fromFeed && !pagerOnStart() && pager.cur) closePagerTo(pager.cur._rowId); + else closeMediaPager(); + } + function closePagerTo(rowId) { + const c = feedNow(); + closeMediaPager(); + // the pop that closes it lands on a later turn: the glide waits for the feed + let tries = 0; + const go = () => { + if (ui.mediaPager || !document.querySelector('.notes-feed')) { if (++tries < 90) requestAnimationFrame(go); return; } + const list = c.winList ? c.winList() : c.notes; + const i = list.findIndex((e) => e.id === rowId); + if (i >= 0) glideToPost(c, list, i); + }; + requestAnimationFrame(go); + } // Leaving the pager for another page (a profile, sign-in, the zap amount): // only the feed's own render syncs the pager, and those pages don't paint // the feed — so the overlay goes now, not on the next feed render. @@ -11800,6 +11855,9 @@ export function messagesFeature(ctx) { setTimeout(() => pagerCounts(slide), 50); }) }, h('span', { class: 'i', html: I_ZAP }), h('span', { class: 'n' }))))); slide._ev = ev; + slide._rowId = it.row ? it.row.id : ev.id; // the feed row to land on (a repost's wrapper, not its inner post) + // pinch to zoom a picture (drag pans it zoomed; the wheel is the pager's) + if (!video && ctx.attachZoom) ctx.attachZoom(slide, media, { wheel: false }); if (video) { guardPicture(media, url); media.muted = !pagerLoud; @@ -11911,16 +11969,26 @@ export function messagesFeature(ctx) { const el = h('div', { class: 'media-pager' + (video ? ' videos' : '') + (pagerLoud ? '' : ' quiet'), role: 'dialog', 'aria-modal': 'true' }, scroller, h('div', { class: 'mp-top' }, - h('button', { class: 'mp-x', type: 'button', 'aria-label': t('close'), onClick: (e) => { e.stopPropagation(); closeMediaPager(); } }, '×'), + h('button', { class: 'mp-x', type: 'button', 'aria-label': t('close'), onClick: (e) => { e.stopPropagation(); dismissPager(); } }, '×'), video ? h('button', { class: 'mp-sound', type: 'button', onClick: (e) => { e.stopPropagation(); pagerSound(!pagerLoud); } }, h('span', { class: 'on' }, '\u{1F50A}'), h('span', { class: 'off' }, '\u{1F507} ' + t('videoUnmute'))) : null)); // a tap shows (or hides) who posted it and what they said el.addEventListener('click', (e) => { if (e.target.closest('button, a, .mp-caption')) return; + if (pager && pager.fromFeed) { + // Opened from a post in the feed: a tap on the picture that opened + // it closes it, as a full-screen picture always has. Once the reader + // has moved on, the first tap shows the post's details, and the next + // closes the pager onto that post in the feed. + if (pagerOnStart()) { closeMediaPager(); return; } + if (!el.classList.contains('show')) { el.classList.add('show'); pagerCounts(pager.cur); return; } + dismissPager(); + return; + } el.classList.toggle('show'); if (el.classList.contains('show')) pagerCounts(pager && pager.cur); }); - pager = { el, scroller, kind: want.kind, keys: new Set(), cur: null, feed: want.feed }; + pager = { el, scroller, kind: want.kind, keys: new Set(), cur: null, feed: want.feed, fromFeed: !!want.fromFeed, start: want.start }; pager.near = new IntersectionObserver((entries) => { for (const e of entries) { if (!e.isIntersecting) continue; @@ -11953,7 +12021,7 @@ export function messagesFeature(ctx) { function growPager() { if (!pager) return; const c = feedNow(); - for (const it of mediaItems(c, pager.kind)) { + for (const it of (pager.fromFeed ? feedImageItems(c) : mediaItems(c, pager.kind))) { const k = mediaKey(it); if (pager.keys.has(k)) continue; pager.keys.add(k); @@ -11966,8 +12034,9 @@ export function messagesFeature(ctx) { if (typeof window !== 'undefined') { window.addEventListener('keydown', (e) => { if (!pager || e.defaultPrevented) return; - if (e.key === 'Escape') { e.preventDefault(); closeMediaPager(); return; } - const step = e.key === 'ArrowDown' || e.key === 'j' || e.key === 'PageDown' ? 1 : e.key === 'ArrowUp' || e.key === 'k' || e.key === 'PageUp' ? -1 : 0; + if (e.key === 'Escape') { e.preventDefault(); dismissPager(); return; } + // the arrows, any of the four: next (down, right) or previous (up, left) + const step = ['ArrowDown', 'ArrowRight', 'j', 'PageDown'].includes(e.key) ? 1 : ['ArrowUp', 'ArrowLeft', 'k', 'PageUp'].includes(e.key) ? -1 : 0; if (step) { e.preventDefault(); pager.scroller.scrollBy({ top: step * pager.scroller.clientHeight, behavior: 'smooth' }); return; } if (e.key === ' ' && pager.cur) { const v = pager.cur.querySelector('video.mp-media'); diff --git a/src/style.css b/src/style.css index 5ce0211..0a1562d 100644 --- a/src/style.css +++ b/src/style.css @@ -1480,6 +1480,8 @@ body::after { display: flex; align-items: center; justify-content: center; overflow: hidden; } .mp-media { max-width: 100%; max-height: 100%; width: 100%; height: 100%; object-fit: contain; display: block; user-select: none; -webkit-user-select: none; } +/* a picture pinches to zoom (attachZoom); one finger at 1× is the pager's swipe */ +.mp-slide img.mp-media { touch-action: pan-y; transform-origin: center center; will-change: transform; } /* nothing but the picture until a tap */ .mp-meta, .mp-top { opacity: 0; pointer-events: none; transition: opacity .18s; } .media-pager.show .mp-meta, .media-pager.show .mp-top { opacity: 1; pointer-events: auto; } diff --git a/tools/feed-picture-pager-test.js b/tools/feed-picture-pager-test.js new file mode 100644 index 0000000..6c45756 --- /dev/null +++ b/tools/feed-picture-pager-test.js @@ -0,0 +1,131 @@ +// A picture tapped in the feed opens in the media pager, on that picture: +// swipes and arrows (all four) step through the feed's pictures, every one +// of a post before the next post's. A tap on the picture that opened it +// closes it, as before; once the reader has moved on, a tap shows the post's +// details and the next tap (or Escape) closes the pager onto that post. +// Run: bun tools/feed-picture-pager-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; +const PICS = { a1: png('400x300'), a2: png('300x400'), b: png('320x320'), d: png('360x240') }; + +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: (req) => { + const path = new URL(req.url).pathname; + if (path.startsWith('/punks')) return new Response(Bun.file('dist' + path)); + const m = /^\/(a1|a2|b|d)\.png$/.exec(path); + if (m) return new Response(PICS[m[1]], { headers: { 'content-type': 'image/png' } }); + return new Response(html, { headers: { 'content-type': 'text/html' } }); +} }); +const U = (n) => server.url.origin + '/' + n + '.png'; +const seeded = [ + post('two pictures ' + U('a1') + ' ' + U('a2'), 100), + post('one picture ' + U('b'), 200), + post('only words here, nothing to look at', 300), + post('a last picture ' + U('d'), 400), + ...Array.from({ length: 12 }, (_, 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(); +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; }; +// the slide the pager rests on, by its picture +const curPic = () => page.evaluate(() => { + const sc = document.querySelector('.mp-scroll'); if (!sc) return null; + const s = [...sc.children].find((x) => Math.abs(x.offsetTop - sc.scrollTop) < 4); + return s ? s.getAttribute('data-k').split('|')[1].split('/').pop() : 'between'; +}); +const resting = async (name) => waitFor((n) => { const sc = document.querySelector('.mp-scroll'); if (!sc) return false; const s = [...sc.children].find((x) => Math.abs(x.offsetTop - sc.scrollTop) < 4); return !!s && s.getAttribute('data-k').endsWith('/' + n); }, name, 4000); +const openPic = async (word) => { + await page.evaluate((w) => { const r = [...document.querySelectorAll('.notes-feed [data-key]')].find((n) => n.textContent.includes(w)); r.scrollIntoView({ block: 'center' }); }, word); + await sleep(400); + const at = await page.evaluate((w) => { const r = [...document.querySelectorAll('.notes-feed [data-key]')].find((n) => n.textContent.includes(w)); const i = r.querySelector('img.note-img').getBoundingClientRect(); return { x: i.x + i.width / 2, y: i.y + i.height / 2, y0: window.scrollY }; }, word); + await page.mouse.click(at.x, at.y); + await waitFor(() => !!document.querySelector('.media-pager'), null, 4000); + return at.y0; +}; +const tapPager = async () => { const at = await page.evaluate(() => { const sc = document.querySelector('.mp-scroll'); const s = [...sc.children].find((x) => Math.abs(x.offsetTop - sc.scrollTop) < 4) || sc.firstChild; const r = s.querySelector('img.mp-media').getBoundingClientRect(); return { x: r.x + r.width / 2, y: r.y + r.height / 2 }; }); await page.mouse.click(at.x, at.y); }; +const rowTop = (word) => page.evaluate((w) => { const r = [...document.querySelectorAll('.notes-feed [data-key]')].find((n) => n.textContent.includes(w)); return r ? Math.round(r.getBoundingClientRect().top) : null; }, word); + +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 pictures are in the feed', await waitFor(() => document.querySelectorAll('.notes-feed img.note-img').length >= 4, null, 20000)); + + console.log('\n[a tap on the picture that opened it closes it]'); + const y0 = await openPic('two pictures'); + check('the picture opens in the pager, on itself', await resting('a1.png'), await curPic()); + check('no details until a tap', await page.evaluate(() => !document.querySelector('.media-pager').classList.contains('show'))); + await tapPager(); await sleep(600); + check('one tap closes it', await waitFor(() => !document.querySelector('.media-pager'), null, 3000)); + check('the feed is where it was', Math.abs((await page.evaluate(() => window.scrollY)) - y0) <= 2); + + console.log('\n[arrows walk the pictures, a post\'s own first]'); + await openPic('two pictures'); + await page.keyboard.press('ArrowDown'); + check('down: the post\'s second picture', await resting('a2.png'), await curPic()); + await page.keyboard.press('ArrowDown'); + check('down again: the next post\'s picture', await resting('b.png'), await curPic()); + await page.keyboard.press('ArrowRight'); + check('right: the next picture, past a post with none', await resting('d.png'), await curPic()); + await page.keyboard.press('ArrowLeft'); + check('left: back one', await resting('b.png'), await curPic()); + await page.keyboard.press('ArrowUp'); + check('up: back one more', await resting('a2.png'), await curPic()); + check('no history entry per step', await page.evaluate(() => history.length) < 50); + + console.log('\n[moved on: a tap shows the post, the next closes onto it]'); + await page.keyboard.press('ArrowDown'); await resting('b.png'); + await tapPager(); await sleep(400); + check('the first tap shows the details', await page.evaluate(() => document.querySelector('.media-pager')?.classList.contains('show'))); + check('…with the author and the words', await page.evaluate(() => { const sc = document.querySelector('.mp-scroll'); const s = [...sc.children].find((x) => Math.abs(x.offsetTop - sc.scrollTop) < 4); return /Picture Poster/.test(s.textContent) && /one picture/.test(s.textContent); })); + await tapPager(); + check('the second tap closes it', await waitFor(() => !document.querySelector('.media-pager') && !!document.querySelector('.notes-feed'), null, 3000)); + await waitFor((w) => { const r = [...document.querySelectorAll('.notes-feed [data-key]')].find((n) => n.textContent.includes(w)); return r && r.getBoundingClientRect().top < 120 && r.getBoundingClientRect().top > -10; }, 'one picture', 3000); + check('…and the feed sits on that post', (await rowTop('one picture')) !== null && (await rowTop('one picture')) < 120 && (await rowTop('one picture')) > -10, 'row top ' + (await rowTop('one picture'))); + + console.log('\n[a swipe, then Escape]'); + await page.evaluate(() => window.scrollTo(0, 0)); await sleep(300); + await openPic('two pictures'); + await page.evaluate(() => { const sc = document.querySelector('.mp-scroll'); sc.scrollTo({ top: sc.clientHeight }); }); // what a swipe up does + check('the swipe lands on the next picture', await resting('a2.png'), await curPic()); + await page.keyboard.press('ArrowDown'); await resting('b.png'); + await page.keyboard.press('ArrowDown'); await resting('d.png'); + await page.keyboard.press('Escape'); + check('Escape closes it', await waitFor(() => !document.querySelector('.media-pager') && !!document.querySelector('.notes-feed'), null, 3000)); + await waitFor((w) => { const r = [...document.querySelectorAll('.notes-feed [data-key]')].find((n) => n.textContent.includes(w)); return r && r.getBoundingClientRect().top < 120 && r.getBoundingClientRect().top > -10; }, 'a last picture', 3000); + check('…onto the last picture\'s post', (await rowTop('a last picture')) !== null && (await rowTop('a last picture')) < 120 && (await rowTop('a last picture')) > -10, 'row top ' + (await rowTop('a last picture'))); + check('no page errors', errs.length === 0, errs.slice(0, 2).join(' | ')); +} finally { await browser.close(); server.stop(true); } +console.log(ok ? '\n✅ feed pictures page like the media grid\'s' : '\n❌ failed'); +process.exit(ok ? 0 : 1); diff --git a/tools/image-tap-test.js b/tools/image-tap-test.js index 2bf840b..d3bcb7e 100644 --- a/tools/image-tap-test.js +++ b/tools/image-tap-test.js @@ -56,7 +56,7 @@ try { const beside = process.env.DBG && await page.evaluate(([x, y]) => { const e = document.elementFromPoint(x, y); const chain = []; for (let n = e; n && chain.length < 5; n = n.parentElement) chain.push(n.tagName + '.' + (n.className || '').toString().split(' ').join('.') + (n.onclick ? '[click]' : '')); return chain; }, [r.x + 8, r.y + r.h / 2]); await page.mouse.click(r.x + 8, r.y + r.h / 2); await sleep(1200); - const band = await page.evaluate(() => ({ thread: !!document.querySelector('.thread-page'), lightbox: !!document.querySelector('.lightbox') })); + const band = await page.evaluate(() => ({ thread: !!document.querySelector('.thread-page'), lightbox: !!document.querySelector('.lightbox, .media-pager') })); check('a tap on the blank band opens the post', band.thread && !band.lightbox, JSON.stringify(band)); await page.goBack(); await sleep(1200); @@ -64,7 +64,7 @@ try { const r2 = await page.evaluate(() => { const b = document.querySelector('.notes-feed img.note-img').getBoundingClientRect(); return { x: b.left, y: b.top, w: b.width, h: b.height }; }); await page.mouse.click(r2.x + r2.w / 2, r2.y + r2.h / 2); await sleep(800); - const pic = await page.evaluate(() => ({ thread: !!document.querySelector('.thread-page'), lightbox: !!document.querySelector('.lightbox') })); + const pic = await page.evaluate(() => ({ thread: !!document.querySelector('.thread-page'), lightbox: !!document.querySelector('.lightbox, .media-pager') })); check('a tap on the picture opens the picture', pic.lightbox && !pic.thread, JSON.stringify(pic)); } finally { await browser.close(); server.stop(true); } console.log(ok ? '\n✅ bands open the post, the picture opens the picture' : '\n❌ failed'); diff --git a/tools/lightbox-scroll-test.js b/tools/lightbox-scroll-test.js index 1eb019f..be03e9b 100644 --- a/tools/lightbox-scroll-test.js +++ b/tools/lightbox-scroll-test.js @@ -82,10 +82,10 @@ try { console.log('\n[a picture, opened and closed]'); const y2 = await showPost('picture post 17'); await sleep(400); check('the reader is somewhere else now', Math.abs(y2 - y1) > 400, `${y1} → ${y2}`); - check('a picture opens full screen', await clickPicture() && await waitFor(() => !!document.querySelector('.lightbox'), null, 4000)); + check('a picture opens full screen', await clickPicture() && await waitFor(() => !!document.querySelector('.lightbox, .media-pager'), null, 4000)); const under = await scrollY(); await page.keyboard.press('Escape'); await sleep(900); - check('the picture closed', await page.evaluate(() => !document.querySelector('.lightbox'))); + check('the picture closed', await page.evaluate(() => !document.querySelector('.lightbox, .media-pager'))); const y3 = await scrollY(); check('the feed is where it was, not jumped to the stale place or the top', Math.abs(y3 - y2) <= 2, `before ${y2}, under ${under}, after ${y3}, stale ${y1}`); |
