aboutsummaryrefslogtreecommitdiffstats
path: root/tools/profile-replies-test.js
blob: b50999f43c770b5ef084b892346c38f072ea356b (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
// A profile's Posts / Replies tabs: Posts (the default) is what they posted,
// Replies their answers in other people's threads. A tab with few of its
// kind among the pages fetched so far asks for older pages by itself.
// Run: bun tools/profile-replies-test.js
import puppeteer from './puppeteer.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { npubEncode } from 'nostr-tools/nip19';
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 key = () => { const sk = generateSecretKey(); return { sk, pk: getPublicKey(sk) }; };
const AUTHOR = key(), OTHER = key();
const now = Math.floor(Date.now() / 1000);
const theirs = finalizeEvent({ kind: 1, created_at: now - 99999, tags: [], content: 'a post by someone else' }, OTHER.sk);
const evs = [];
// newest first: 25 replies with 3 posts among them, then 10 older posts
let t = now - 60;
for (let i = 0; i < 28; i++) {
  const post = i % 9 === 4; // three posts in the first page
  evs.push(finalizeEvent(post
    ? { kind: 1, created_at: t, tags: [], content: 'recent post number ' + i }
    : { kind: 1, created_at: t, tags: [['e', theirs.id, '', 'root'], ['p', OTHER.pk]], content: 'my reply number ' + i }, AUTHOR.sk));
  t -= 60;
}
for (let i = 0; i < 10; i++) { evs.push(finalizeEvent({ kind: 1, created_at: t, tags: [], content: 'older post number ' + i }, AUTHOR.sk)); t -= 60; }
const PROFILE = finalizeEvent({ kind: 0, created_at: now - 10, tags: [], content: JSON.stringify({ name: 'Replier' }) }, AUTHOR.sk);

const html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 0, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) });
const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
await page.evaluateOnNewDocument(([pk, evs, prof]) => {
  window.__pages = 0;
  const Real = window.WebSocket;
  window.WebSocket = function (...a) {
    const ws = new Real(...a);
    const send = ws.send.bind(ws);
    ws.send = (data) => {
      try {
        const m = JSON.parse(data);
        if (m[0] === 'REQ') {
          let out = [];
          for (const f of m.slice(2)) {
            if (!(f.authors || []).includes(pk)) continue;
            if ((f.kinds || []).includes(0)) out.push(prof);
            if ((f.kinds || []).includes(1) && !f['#e'] && !f['#p']) {
              const page = evs.filter((e) => !f.until || e.created_at <= f.until).slice(0, f.limit || 30);
              if (f.until) window.__pages++;
              out.push(...page);
            }
          }
          setTimeout(() => {
            for (const e of out) ws.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(['EVENT', m[1], e]) }));
            ws.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(['EOSE', m[1]]) }));
          }, 0);
          return;
        }
        if (m[0] === 'EVENT') return;
      } catch {}
      return send(data);
    };
    return ws;
  };
  window.WebSocket.prototype = Real.prototype; Object.assign(window.WebSocket, Real);
}, [AUTHOR.pk, evs, PROFILE]);
const errs = [];
page.on('pageerror', (e) => errs.push(String(e)));
const rows = () => page.evaluate(() => [...document.querySelectorAll('.notes-feed [data-zap-post]')].map((r) => r.innerText));
const waitFor = async (fn, ms = 15000) => { for (let i = 0; i < ms / 250; i++) { if (await page.evaluate(fn)) return true; await sleep(250); } return false; };

try {
  await page.setViewport({ width: 390, height: 844 });
  await page.goto(server.url.href + npubEncode(AUTHOR.pk), { waitUntil: 'domcontentloaded' });
  check('the profile opens on its Posts tab', await waitFor(() => document.querySelector('.prof-tabs button.active')?.dataset.tab === 'posts', 20000));
  check('older pages are fetched until Posts has something to show', await waitFor(() => [...document.querySelectorAll('.notes-feed [data-zap-post]')].length >= 12, 15000),
    (await rows()).length + ' rows, ' + (await page.evaluate(() => window.__pages)) + ' older page(s)');
  let r = await rows();
  check('Posts holds only their posts', r.length > 0 && r.every((x) => /post number/.test(x) && !/my reply/.test(x)), r.length + ' rows');
  check('...the three recent ones and the older ones', r.filter((x) => /recent post/.test(x)).length === 3 && r.some((x) => /older post/.test(x)));
  await page.evaluate(() => document.querySelector('.prof-tabs button[data-tab="replies"]').click());
  await sleep(600);
  r = await rows();
  check('Replies holds only their replies', r.length >= 12 && r.every((x) => /my reply/.test(x)), r.length + ' rows');
  check('...each marked as a reply', r.every((x) => /reply/i.test(x.split('\n').slice(0, 3).join(' '))));
  await page.evaluate(() => document.querySelector('.prof-tabs button[data-tab="posts"]').click());
  await sleep(400);
  if (process.env.SHOT) { await page.evaluate(() => document.querySelector('.prof-tabs').scrollIntoView({ block: 'start' })); await page.evaluate(() => window.scrollBy(0, -80)); await sleep(300); await page.screenshot({ path: process.env.SHOT }); }
  check('back to Posts', (await rows()).every((x) => /post number/.test(x)));
  check('no page errors', errs.length === 0, errs.slice(0, 2).join(' | '));
} finally { await browser.close(); server.stop(true); }
console.log(ok ? '\n✅ profiles split posts and replies' : '\n❌ failed');
process.exit(ok ? 0 : 1);