diff options
Diffstat (limited to 'tools/safe-boot-test.js')
| -rw-r--r-- | tools/safe-boot-test.js | 77 |
1 files changed, 77 insertions, 0 deletions
diff --git a/tools/safe-boot-test.js b/tools/safe-boot-test.js new file mode 100644 index 0000000..a23c067 --- /dev/null +++ b/tools/safe-boot-test.js @@ -0,0 +1,77 @@ +// The crash-loop guard: the inline BOOT_GUARD counts page starts, the app +// clears the count once it has lived or leaves cleanly, and a third start +// in a row opens in safe mode — a notice naming the last boot phase, no +// automatic Ark connection, and "Open normally" back to a plain boot. +// Run: bun tools/safe-boot-test.js +import assert from 'node:assert/strict'; +import puppeteer from './puppeteer.js'; +import { generateMnemonic } from '@scure/bip39'; +import { wordlist } from '@scure/bip39/wordlists/english'; +import { BOOT_GUARD } from '../build.js'; + +const bundle = await Bun.build({ entrypoints: ['src/app.js'], target: 'browser', minify: true, + plugins: [{ name: 'safe-boot-test', setup(build) { + build.onLoad({ filter: /src\/app\.js$/ }, async ({ path }) => ({ loader: 'js', contents: await Bun.file(path).text() + ` +window.sbApp = { ui, wallet, render, enterWallet, get ready() { return !_bootDeciding; } }; +` })); + build.onLoad({ filter: /src\/features\/ark\.js$/ }, async ({ path }) => ({ loader: 'js', contents: + (await Bun.file(path).text()).replace(" return {\n id: 'ark',", ` + window.sbArk = { get ark() { return ark; }, get connecting() { return !!arkConnectPromise; }, get lastAutoInit() { return lastAutoInit; } }; + return {\n id: 'ark',`) })); + build.onLoad({ filter: /src\/video-compress\.js$/ }, () => ({ loader: 'js', contents: 'export async function compressVideo(f) { return f; }' })); + } }] }); +assert(bundle.success, bundle.logs.join('\n')); +const html = `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>${await Bun.file('src/style.css').text()}</style><div id="app"></div>${BOOT_GUARD}<script type="module">${await bundle.outputs[0].text()}</script>`; +const server = Bun.serve({ port: 0, fetch: (req) => new URL(req.url).pathname === '/' + ? new Response(html, { headers: { 'Content-Type': 'text/html' } }) : new Response(null, { status: 404 }) }); +const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox'] }); +let ok = true; +const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; }; +try { + const page = await browser.newPage(), errors = []; + page.on('pageerror', (e) => errors.push(e.message)); + await page.setViewport({ width: 390, height: 844 }); + await page.setRequestInterception(true); + page.on('request', (r) => r.url().startsWith(server.url.origin) || r.url().startsWith('data:') ? r.continue() : r.abort()); + await page.evaluateOnNewDocument(() => { + if (!localStorage.getItem('sb-seeded')) { + localStorage.setItem('sb-seeded', '1'); + localStorage.setItem('btc-wallet-network', 'regtest'); + localStorage.setItem('btc-wallet-ark-provider:regtest', 'local'); + // two starts already, neither cleared: the last one died merging the worker's state + localStorage.setItem('coinos-boot', '2'); + localStorage.setItem('coinos-boot-phase', 'ark-merge'); + } + }); + await page.goto(server.url.href, { waitUntil: 'domcontentloaded' }); + await page.waitForFunction(() => window.sbApp?.ready); + const guard = await page.evaluate(() => ({ safe: window.__safeBoot, phase: window.__bootPhase, count: localStorage.getItem('coinos-boot') })); + check('the third start in a row opens in safe mode, knowing the phase the last one reached', guard.safe === 3 && guard.phase === 'ark-merge' && guard.count === '3', JSON.stringify(guard)); + await page.evaluate(async (mn) => { + await sbApp.enterWallet(mn, '', { fresh: true, generated: true }); + sbApp.ui.onb = null; localStorage.removeItem('btc-wallet-onb-step'); + sbApp.ui.tab = 'wallet'; sbApp.render(); + }, generateMnemonic(wordlist)); + await new Promise((r) => setTimeout(r, 1500)); + const notice = await page.evaluate(() => { const n = [...document.querySelectorAll('.notice')].find((x) => /safe mode/i.test(x.textContent)); return n ? n.textContent : ''; }); + check('the wallet home says so and names the phase', /safe mode/i.test(notice) && /ark-merge/.test(notice) && /2 times/.test(notice), notice.slice(0, 120)); + const arkState = await page.evaluate(() => ({ ark: !!sbArk.ark, connecting: sbArk.connecting, auto: sbArk.lastAutoInit })); + check('nothing dialled the Ark server on its own', !arkState.ark && !arkState.connecting && !arkState.auto, JSON.stringify(arkState)); + // a clean leave clears the count + await page.evaluate(() => dispatchEvent(new Event('pagehide'))); + check('leaving the page cleanly clears the count', (await page.evaluate(() => localStorage.getItem('coinos-boot'))) === null); + // "Open normally": the count is cleared and the page reloads into a plain boot + await page.evaluate(() => localStorage.setItem('coinos-boot', '3')); + await Promise.all([ + page.waitForNavigation({ waitUntil: 'domcontentloaded' }), + page.evaluate(() => { const b = [...document.querySelectorAll('.notice button')].find((x) => /open normally/i.test(x.textContent)); b.click(); }), + ]); + await page.waitForFunction(() => window.sbApp?.ready); + const after = await page.evaluate(() => ({ safe: window.__safeBoot, count: localStorage.getItem('coinos-boot'), notice: [...document.querySelectorAll('.notice')].some((x) => /safe mode/i.test(x.textContent)) })); + check('"Open normally" reloads into a plain boot (one start counted, no notice)', after.safe === undefined && after.count === '1' && !after.notice, JSON.stringify(after)); + await new Promise((r) => setTimeout(r, 8600)); + check('a boot that lives eight seconds clears its count', (await page.evaluate(() => localStorage.getItem('coinos-boot'))) === null); + check('no page errors', errors.length === 0, errors.join(' | ').slice(0, 200)); +} finally { await browser.close(); server.stop(true); } +console.log(ok ? '\n✅ safe mode after a crash loop' : '\n❌ failed'); +process.exit(ok ? 0 : 1); |
