aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/profile-feed-test.js9
-rw-r--r--tools/profile-replies-test.js93
2 files changed, 100 insertions, 2 deletions
diff --git a/tools/profile-feed-test.js b/tools/profile-feed-test.js
index f2ca9e6..87dfc80 100644
--- a/tools/profile-feed-test.js
+++ b/tools/profile-feed-test.js
@@ -49,8 +49,13 @@ try {
await page.evaluate(() => document.querySelector('.chat-avatar.clickable')?.click());
check('profile screen opens', await waitText('back', 10000));
// the feed: either notes arrive or the explicit empty state shows
- const fed = await waitText('Latest posts', 15000);
- const empty = !fed && (await bodyText()).includes('No recent posts');
+ // (the Posts / Replies tabs come first; their rows once the relays answer)
+ let fed = false, empty = false;
+ for (let i = 0; i < 60 && !fed && !empty; i++) {
+ fed = await page.evaluate(() => document.querySelectorAll('.notes-feed [data-zap-post]').length > 0);
+ empty = !fed && /No recent posts/.test(await bodyText());
+ if (!fed && !empty) await sleep(250);
+ }
check('note feed resolves', fed || empty, fed ? 'notes shown' : 'no posts for this pk');
if (fed) {
// the ⚡ in every note's action bar, live when the wallet can pay
diff --git a/tools/profile-replies-test.js b/tools/profile-replies-test.js
new file mode 100644
index 0000000..b50999f
--- /dev/null
+++ b/tools/profile-replies-test.js
@@ -0,0 +1,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);