aboutsummaryrefslogtreecommitdiffstats
path: root/tools/feed-boot-chrome-test.js
blob: 91e5188e9bfcc9ff82084f478402615ea9931794 (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
// The feed opened by URL (/feed) paints as the restoring wallet's from its
// first frame. It painted as a visitor's — no post button, chips or nav —
// until the restore landed a beat later (the vault's scrypt, the wallet's
// state, every feature's init: ~0.5 s on a laptop, more on a phone), and
// the button "took a while to appear" (Adam, 2026-10-31). Boot now tells
// a wallet is about to open (a session list in the tab, a vault the last
// unlock opened without a password, watch-only accounts) before the work,
// and the page paints for it. A visitor with no wallet still gets none.
// Run: bun tools/feed-boot-chrome-test.js
import puppeteer from './puppeteer.js';
import { buildHtml } from '../build.js';
import { encryptVault } from '../src/wallet.js';
import { generateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';

let fails = 0;
const check = (name, ok, detail = '') => { console.log(` ${ok ? '✓' : '✗'} ${name}${detail ? ' — ' + String(detail).slice(0, 200) : ''}`); if (!ok) fails++; };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

const html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 5331, fetch: (req) => {
  const p = new URL(req.url).pathname;
  if (p === '/verify-worker.js' || p.startsWith('/punks')) return new Response(Bun.file('dist' + p));
  return new Response(html, { headers: { 'content-type': 'text/html' } });
} });
const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
const mnemonic = generateMnemonic(wordlist);
const account = { id: 'boot-test', label: 'Boot', type: 'full', mnemonic, passphrase: '', network: 'regtest', persisted: true };
const vault = encryptVault([{ label: account.label, mnemonic, passphrase: '', network: 'regtest' }], '');

// Every mutation batch gets a number; the first batch each piece of the page
// showed up in says whether it came with the page or after it.
const watch = () => {
  window.__seen = {}; window.__batch = 0;
  const SEL = { page: '.feed-page', fab: '.feed-post-fab', chips: '.feed-chips', nav: '.app-bottom-nav', side: '.wallet-side' };
  new MutationObserver(() => {
    window.__batch++;
    for (const [k, s] of Object.entries(SEL)) if (!(k in window.__seen) && document.querySelector(s)) { window.__seen[k] = { batch: window.__batch, at: Math.round(performance.now()) }; if (k === 'fab') window.__fab0 = document.querySelector(s); }
  }).observe(document, { childList: true, subtree: true });
};
async function load(name, seed) {
  const ctx = await browser.createBrowserContext(); // its own storage: each scenario starts clean
  const page = await ctx.newPage();
  const errors = [];
  page.on('pageerror', (e) => errors.push(e.message));
  await page.setViewport({ width: 390, height: 844 });
  await page.evaluateOnNewDocument(watch);
  await page.evaluateOnNewDocument(seed, { account, vault });
  await page.goto('http://localhost:5331/feed', { waitUntil: 'domcontentloaded' });
  await sleep(3500);
  const r = await page.evaluate(() => ({
    seen: window.__seen, errors: [],
    fabSame: !!window.__fab0 && document.querySelector('.feed-post-fab') === window.__fab0,
    chips: [...document.querySelectorAll('.feed-chip')].map((c) => c.textContent.trim() + (c.classList.contains('on') ? '*' : '')),
    fab: !!document.querySelector('.feed-post-fab'), nav: !!document.querySelector('.app-bottom-nav'),
    walletOpen: !!document.querySelector('.app-nav-profile, .chat-avatar'),
    blankNote: localStorage.getItem('btc-wallet-vault-blank'),
    text: document.body.innerText.replace(/\s+/g, ' ').slice(0, 120),
  }));
  r.errors = errors;
  console.log(`[${name}] ${JSON.stringify(r.seen)} chips=${r.chips.join('|')}`);
  await ctx.close();
  return r;
}
// with the page, or (the nav rides the boot shell too) before it
const withPage = (r, k) => r.seen[k] && r.seen.page && r.seen[k].batch <= r.seen.page.batch;

try {
  // 1. A reload: the tab's session list says which wallet is coming.
  let r = await load('session', ({ account }) => {
    localStorage.setItem('btc-wallet-network', 'regtest');
    sessionStorage.setItem('btc-wallet-accounts', JSON.stringify({ accounts: [account], activeId: account.id }));
  });
  check('session: the post button paints with the feed page', withPage(r, 'fab'), JSON.stringify(r.seen));
  check('session: the chips paint with the page', withPage(r, 'chips'));
  check('session: the nav paints with the page', withPage(r, 'nav'));
  check('session: the wallet then opens behind it', r.fab && r.nav && r.walletOpen, r.text);
  check('session: the button is the same node through the restore (no blink)', r.fabSame);
  check('session: Popular stood for the visitor copy, then the wallet\'s own chips', r.chips.length >= 2 && r.chips.filter((c) => /Popular/.test(c)).length === 1, r.chips.join('|'));
  check('session: no page errors', r.errors.length === 0, r.errors.join(' | '));

  // 2. A cold start: a vault the last unlock opened without a password.
  r = await load('vault', ({ vault }) => {
    localStorage.setItem('btc-wallet-network', 'regtest');
    localStorage.setItem('btc-wallet-vault', JSON.stringify(vault));
    localStorage.setItem('btc-wallet-vault-blank', '1');
  });
  check('vault: the post button paints with the feed page', withPage(r, 'fab'), JSON.stringify(r.seen));
  check('vault: the nav paints with the page', withPage(r, 'nav'));
  check('vault: the wallet opens behind it', r.fab && r.walletOpen, r.text);
  check('vault: the same node through the scrypt restore', r.fabSame);
  check('vault: no page errors', r.errors.length === 0, r.errors.join(' | '));

  // 3. The same vault before any unlock left its note: boot can't tell, so
  //    the page paints as a visitor's and grows the button when the wallet
  //    opens — and the unlock leaves the note for next time.
  r = await load('vault, no note', ({ vault }) => {
    localStorage.setItem('btc-wallet-network', 'regtest');
    localStorage.setItem('btc-wallet-vault', JSON.stringify(vault));
  });
  check('unnoted vault: the button still comes, after the page', r.fab && r.seen.fab && r.seen.page && r.seen.fab.batch > r.seen.page.batch, JSON.stringify(r.seen));
  check('unnoted vault: the unlock leaves the note', r.blankNote === '1');

  // 4. A visitor: no wallet on this device, nothing to paint for.
  r = await load('visitor', () => { localStorage.setItem('btc-wallet-network', 'regtest'); });
  check('visitor: the feed paints, without a post button', r.seen.page && !r.fab && !r.seen.fab, JSON.stringify(r.seen));
  check('visitor: no page errors', r.errors.length === 0, r.errors.join(' | '));
} finally {
  await browser.close();
  server.stop(true);
}
console.log(fails ? `\n❌ ${fails} failing` : '\n✅ the feed paints for the wallet that is coming');
process.exit(fails ? 1 : 0);