aboutsummaryrefslogtreecommitdiffstats
path: root/tools/feed-picture-pager-test.js
blob: 32625c99d4ba552ffd091c3ad51d37e521d06473 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// 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.querySelectorAll('.mp-slide')].find((x) => { const r = x.getBoundingClientRect(); return Math.abs(r.top) < 4 && Math.abs(r.left) < 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.querySelectorAll('.mp-slide')].find((x) => { const r = x.getBoundingClientRect(); return Math.abs(r.top) < 4 && Math.abs(r.left) < 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.querySelectorAll('.mp-slide')].find((x) => { const r = x.getBoundingClientRect(); return Math.abs(r.top) < 4 && Math.abs(r.left) < 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[left and right: along a post\'s pictures; up and down: post to post]');
  await openPic('two pictures');
  check('a post with two pictures shows two dots, the first lit', await page.evaluate(() => { const d = document.querySelector('.mp-group .mp-dots'); return d && d.children.length === 2 && d.children[0].classList.contains('on'); }));
  await page.keyboard.press('ArrowRight');
  check('right: the post\'s second picture', await resting('a2.png'), await curPic());
  check('…its dot lit', await page.evaluate(() => { const d = document.querySelector('.mp-group .mp-dots'); return d.children[1].classList.contains('on') && !d.children[0].classList.contains('on'); }));
  await page.keyboard.press('ArrowRight'); await sleep(600);
  check('right at the post\'s last: stays (left and right stay in the post)', (await curPic()) === 'a2.png', await curPic());
  await page.keyboard.press('ArrowDown');
  check('down: the next post\'s picture', await resting('b.png'), await curPic());
  await page.keyboard.press('ArrowDown');
  check('down again: the next, past a post with none', await resting('d.png'), await curPic());
  await page.keyboard.press('ArrowUp');
  check('up: back one post', await resting('b.png'), await curPic());
  await page.keyboard.press('ArrowUp');
  check('up again: the first post, on the picture it was left at', await resting('a2.png'), await curPic());
  await page.keyboard.press('ArrowLeft');
  check('left: its first picture', await resting('a1.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.querySelectorAll('.mp-slide')].find((x) => { const r = x.getBoundingClientRect(); return Math.abs(r.top) < 4 && Math.abs(r.left) < 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 r = document.querySelector('.mp-row'); r.scrollTo({ left: r.clientWidth }); }); // what a swipe to the left does
  check('a sideways swipe lands on the post\'s next picture', await resting('a2.png'), await curPic());
  await page.evaluate(() => { const sc = document.querySelector('.mp-scroll'); sc.scrollTo({ top: sc.clientHeight }); }); // what a swipe up does
  check('a swipe up lands on the next post\'s', await resting('b.png'), await curPic());
  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);