aboutsummaryrefslogtreecommitdiffstats
path: root/node_modules/jade/lib/nodes
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2022-10-13 18:05:49 -0700
committerAdam Soltys <[email protected]>2022-10-13 18:05:49 -0700
commit81e8a6ac1e61cf812252f08f624f8ccf7b7cb8a5 (patch)
tree4b0dd6d726f218ca979d5abc729c73956e828957 /node_modules/jade/lib/nodes
parent974c0fa9556c294415e5a14d2ba35c2f630b2db7 (diff)
downloadcoinos-server-81e8a6ac1e61cf812252f08f624f8ccf7b7cb8a5.tar.gz
coinos-server-81e8a6ac1e61cf812252f08f624f8ccf7b7cb8a5.zip
tickets
Diffstat (limited to 'node_modules/jade/lib/nodes')
0 files changed, 0 insertions, 0 deletions
duleRepaint holds while a finger is down or the page scrolled in the last 200 ms (at most 1.5 s), and the profile flush likewise (at most 5 s, forced on an account switch) and sizes the blob by summing rows instead of stringifying it. Tests: the create flow's heads-up / passphrase / check steps from earlier today broke every test that made a wallet by waiting for the words and clicking "Skip verification"; tools/puppeteer.js exports seedWords() and skipSeedCheck() and the 96 tests use them. room-faces and avatar-profile fail the same way on 8858591 (row.hover, scrollIntoView), before today's changes' href='/coinosv3.git/commit/tools/pager-close-test.js?id=e905e5be5b2a0385e89de5ab3c0d95a917307314'>e905e5b ^
928da70 ^
e905e5b ^
928da70 ^








































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
139
140
141
142







                                                                             
                                                                     












































































                                                                                                                                                                                                                                                                                                                                                                                                    

                                                                                                                                         









                                                                                                                     
                        
                                                                                                      
                            








































                                                                                                                                                                                                  
// Closing the pager lands on the feed under it, whatever the browser's
// history says. The × was Back, and the entry Back reached isn't always the
// feed (the browser's older entries outlive a reload of the app in the
// background; entries merge at boot): it took the reader to the wallet, then
// to the chat list. Now landing anywhere but the page under the pager puts
// the reader on that page, and that entry becomes it. And the ordinary case
// still steps Back, so the phone's Back doesn't reopen the pager.
// Run: bun tools/pager-close-test.js
import puppeteer, { seedWords, skipSeedCheck } from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
import { existsSync } from 'node:fs';
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 CLIP = '/tmp/coinos-tail-clip.webm';
if (!existsSync(CLIP)) Bun.spawnSync(['ffmpeg', '-y', '-loglevel', 'error', '-f', 'lavfi', '-i', 'testsrc=size=180x320:rate=10', '-t', '20', '-c:v', 'libvpx', '-b:v', '100k', CLIP]);

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 (/^\/c\d\.webm$/.test(path)) return new Response(Bun.file(CLIP), { headers: { 'content-type': 'video/webm' } });
  if (path.startsWith('/punks') || path === '/verify-worker.js') return new Response(Bun.file('dist' + path));
  return new Response(html, { headers: { 'content-type': 'text/html' } });
} });
const U = (n) => server.url.origin + '/' + n + '.webm';
// what the phone has: two clips among words; what only the relays have (slowly): two older clips
const cached = [
  post('clip zero ' + U('c0'), 100), post('words between ' + 'lorem ipsum. '.repeat(6), 200), post('clip one ' + U('c1'), 300),
];
const older = [post('clip two ' + U('c2'), 5000), post('more words ' + 'lorem ipsum. '.repeat(6), 5100), post('clip three ' + U('c3'), 5200)];

const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox', '--autoplay-policy=no-user-gesture-required'] });
const page = await browser.newPage();
// The relays: the newest posts at once; anything asked for by age (until) comes 3 s later
await page.evaluateOnNewDocument(([fresh, old]) => {
  const all = [...fresh, ...old];
  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('#'));
  window.__olderAsks = 0;
  class FakeWS {
    constructor(url) { this.url = String(url); this.readyState = 0; setTimeout(() => { this.readyState = 1; this.onopen && this.onopen({}); }, 5); }
    send(raw) {
      const m = JSON.parse(raw);
      const reply = (x, ms = 5) => setTimeout(() => this.readyState === 1 && this.onmessage && this.onmessage({ data: JSON.stringify(x) }), ms);
      if (m[0] === 'REQ') {
        const filters = m.slice(2).filter((f) => !f.cache);
        const slow = filters.some((f) => f.until);
        if (slow) window.__olderAsks++;
        const pool = slow ? all : fresh;
        const ms = slow ? 3000 : 5;
        for (const f of filters) {
          const hits = pool.filter((e) => match(f, e)).sort((a, b) => b.created_at - a.created_at);
          for (const e of f.limit ? hits.slice(0, f.limit) : hits) reply(['EVENT', m[1], e], ms);
        }
        reply(['EOSE', m[1]], ms + 5);
      } else if (m[0] === 'EVENT') reply(['OK', m[1].id, true, '']);
      else if (m[0] === 'COUNT') reply(['COUNT', m[1], { count: 0 }]);
    }
    close() { this.readyState = 3; this.onclose && this.onclose({ code: 1000 }); }
    addEventListener(t, fn) { this['on' + t] = fn; }
    removeEventListener() {}
  }
  FakeWS.CONNECTING = 0; FakeWS.OPEN = 1; FakeWS.CLOSING = 2; FakeWS.CLOSED = 3;
  window.WebSocket = FakeWS;
}, [cached, older]);
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; };
// what the pager rests on: a clip's name, or 'tail'
const AT = () => { const sc = document.querySelector('.mp-scroll'); if (!sc) return null; const s = [...sc.querySelectorAll('.mp-slide, .mp-tail')].find((x) => { const r = x.getBoundingClientRect(); return Math.abs(r.top) < 4 && Math.abs(r.left) < 4; }); return !s ? 'between' : s.classList.contains('mp-tail') ? 'tail' : s.getAttribute('data-k').split('/').pop().replace('.webm', ''); };
const at = () => page.evaluate(AT);
const resting = (name, ms = 5000) => waitFor(([f, n]) => eval(f)() === n, ['(' + AT + ')', name], ms);
const playing = (name) => waitFor((n) => { const s = [...document.querySelectorAll('.mp-slide')].find((x) => (x.getAttribute('data-k') || '').endsWith('/' + n + '.webm')); const v = s && s.querySelector('video.mp-media'); return v && !v.paused && v.currentTime > 0; }, name, 6000);
const tail = () => page.evaluate(() => { const t = document.querySelector('.mp-tail'); return t ? { text: t.textContent.trim(), done: t.classList.contains('done'), last: t === t.parentElement.lastElementChild } : null; });

const st = () => page.evaluate(() => ({ pager: !!document.querySelector('.media-pager'), feed: !!document.querySelector('.notes-feed'), chats: !!document.querySelector('.chat-list, .msg-list'), wallet: !document.querySelector('.notes-feed') && /receive/i.test(document.body.innerText), i: history.state && history.state.i }));
const openFirst = async () => {
  const r = await page.evaluate(() => { const row = [...document.querySelectorAll('.notes-feed [data-key]')].find((n) => n.textContent.includes('clip zero')); row.querySelector('video.note-video').scrollIntoView({ block: 'center' }); const b = row.querySelector('video.note-video').getBoundingClientRect(); return { x: b.x + b.width * 0.2, y: b.y + b.height * 0.3 }; });
  await sleep(400);
  // a mouse click on a playing clip pauses it; a paused one opens
  await page.evaluate(([x, y]) => { const v = document.elementFromPoint(x, y); if (v && v.tagName === 'VIDEO') v.pause(); }, [r.x, r.y]);
  await page.mouse.click(r.x, r.y);
  return waitFor(() => !!document.querySelector('.media-pager.videos'), null, 4000);
};
const close = async () => { await page.evaluate(() => document.querySelector('.mp-x').click()); await sleep(1200); };
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 seedWords(page);
  const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
  await skipSeedCheck(page);
  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: 'Clip Poster', t: Date.now() } }));
    localStorage.setItem('btc-wallet-feed:' + k, 'following');
  }, [base, AUTHOR, cached]);
  await page.reload({ waitUntil: 'domcontentloaded' });
  await waitText('receive', 20000);
  await sleep(800);
  await page.evaluate(() => document.querySelector('.app-nav-button[aria-label="Messages"]').click());
  await sleep(800);
  const chatsNav = await page.evaluate(() => history.state);
  await page.evaluate(() => document.querySelector('.app-nav-button[aria-label="Feed"]').click());
  await page.waitForSelector('.notes-feed video.note-video', { timeout: 20000 });
  await sleep(1200);

  console.log('\n[the ordinary case: × steps Back, onto the feed]');
  const before = await page.evaluate(() => history.state.i);
  check('the pager opens', await openFirst());
  await page.keyboard.press('ArrowDown'); await sleep(800);
  await close();
  let s = await st();
  check('× lands on the feed', !s.pager && s.feed, JSON.stringify(s));
  check('…by stepping Back (the pager\'s entry gone, not one more on top)', s.i === before, s.i + ' vs ' + before);

  console.log('\n[the entry under the pager isn\'t the feed (it says: the chat list)]');
  await page.evaluate((nav) => history.replaceState({ ...history.state, nav: nav.nav }, ''), chatsNav);
  check('the pager opens', await openFirst());
  await page.keyboard.press('ArrowDown'); await sleep(800);
  await close();
  s = await st();
  check('× still lands on the feed, not the chat list', !s.pager && s.feed, JSON.stringify(s));
  const fixed = await page.evaluate(() => history.state && history.state.nav && history.state.nav.msgView);
  check('…and that entry is the feed now', fixed === 'feed', String(fixed));
  check('the phone\'s Back from there leaves the feed (no pager reopening)', await (async () => { await page.evaluate(() => history.back()); await sleep(1000); return !(await st()).pager; })());
  check('no page errors', errs.length === 0, errs.slice(0, 2).join(' | '));
} finally { await browser.close(); server.stop(true); }
console.log(ok ? '\n✅ closing the pager lands on the feed under it' : '\n❌ failed');
process.exit(ok ? 0 : 1);