diff options
| author | Adam Soltys <[email protected]> | 2026-10-07 09:59:28 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-07 09:59:28 -0700 |
| commit | 6f3c5ac7f2accc2349c7bef9e5b06e9990fb194c (patch) | |
| tree | 3895966e0db599c305880f036c3ff8bbfe346973 | |
| parent | 95b72a73545cd2a3700b0e0866eee54e59d3b50e (diff) | |
| download | coinosv3-6f3c5ac7f2accc2349c7bef9e5b06e9990fb194c.tar.gz coinosv3-6f3c5ac7f2accc2349c7bef9e5b06e9990fb194c.zip | |
ark ui-test and comments-test: no more timing races
ark ui-test: wait for the selected balance card to stick before opening
Receive (the carousel lands on a card's history, and a glide could settle
back on Savings), wait for the first mailbox read before paying, paste
addresses whole (typed a key at a time, a re-render mid-typing broke the
checksum), and poll for alice's balance and the history rows. 8/8 runs.
comments-test: the article and comments it fetched are handed to the app
on every relay socket that asks, so it no longer depends on the app
reaching the same public relays.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | tools/ark/ui-test.js | 63 | ||||
| -rw-r--r-- | tools/comments-test.js | 23 |
2 files changed, 76 insertions, 10 deletions
diff --git a/tools/ark/ui-test.js b/tools/ark/ui-test.js index 4460cd3..e4e2e5a 100644 --- a/tools/ark/ui-test.js +++ b/tools/ark/ui-test.js @@ -89,6 +89,28 @@ const waitFace = async (label, want, ms = 30000) => { } return false; }; +// An address arrives the way a paste does — whole. Typed a key at a time, +// a re-render mid-typing can drop or reorder characters (the app then +// rightly refuses a bad checksum). +const pasteDest = (text) => page.evaluate((text) => { + const el = document.querySelector('textarea.send-dest'); + el.focus(); + el.value = text; + el.dispatchEvent(new Event('input', { bubbles: true })); +}, text); +// Select a balance card and open its Receive tab. The carousel glides to the +// card and landing on one opens its history — and a glide can settle back +// on the other card — so keep asking until the account sticks, then open +// Receive (the tab a receive's celebration takes over). +const openReceive = async (account) => { + for (let i = 0; i < 20; i++) { + await page.evaluate((a) => uiApp.ctx.setAccount(a), account); + await sleep(750); + if (await page.evaluate((a) => uiApp.ctx.getAccount() === a, account)) break; + } + await sleep(750); + await page.evaluate(() => { uiApp.ui.tab = 'receive'; uiApp.render(); }); +}; const tabPane = () => page.evaluate(() => document.querySelector('.tab-pane')?.innerText || ''); try { @@ -110,14 +132,32 @@ try { await snap('spending'); console.log('\n[2] receive over Ark'); - await page.evaluate(() => uiApp.ctx.setAccount('spending')); + await openReceive('spending'); let arkAddr = ''; for (let i = 0; i < 120 && !arkAddr; i++) { arkAddr = ((await tabPane()).match(/tark1[a-z0-9]+/) || [''])[0]; if (!arkAddr) await sleep(250); } - check('ark address on the Receive tab', !!arkAddr, arkAddr.slice(0, 24) + '…'); + check('ark address on the Receive tab', !!arkAddr, arkAddr ? arkAddr.slice(0, 24) + '…' + : 'tab ' + (await page.evaluate(() => uiApp.ui.tab)) + ': ' + (await tabPane()).slice(0, 200).replace(/\n/g, ' | ')); if (!arkAddr) { await snap('no-ark-address'); throw new Error('ark never connected — is captaind up?'); } + // A new wallet's first mailbox read is its baseline: whatever it finds is + // history and never celebrated. Pay only once that read is behind us, or + // the payment can land inside it and the celebration check is a coin toss. + const baselineDone = () => page.evaluate(() => new Promise((res) => { + const req = indexedDB.open('coinos-ark'); + req.onerror = () => res(false); + req.onsuccess = () => { + try { + const all = req.result.transaction('states').objectStore('states').getAll(); + all.onsuccess = () => res(all.result.some((v) => v && Array.isArray(v.movements) && !v.baselinePending)); + all.onerror = () => res(false); + } catch { res(false); } + }; + })); + let baselined = false; + for (let i = 0; i < 120 && !baselined; i++) { baselined = await baselineDone(); if (!baselined) await sleep(250); } + check('the first mailbox read (the baseline) is done', baselined); sh(`${ALICE} -q send ${arkAddr} "25000 sat"`); check('spending shows 25,000', await waitFace('Spending', 25000), String(await faceSat('Spending'))); check('payment celebration shown', await waitText('Payment received', 10000)); @@ -130,7 +170,7 @@ try { await sleep(500); const aliceAddr = sh(`${ALICE} -q address`); const before = aliceBalance(); - await page.type('textarea.send-dest', aliceAddr); + await pasteDest(aliceAddr); await sleep(800); check('no fee controls for an ark dest', /no mining fee/i.test(await tabPane()) && !/fee rate|coin control/i.test(await tabPane())); await page.type('.tab-pane input[placeholder="0"]', '7000'); @@ -142,16 +182,23 @@ try { await clickText('.tab-pane button.btn-primary', 'Send'); check('sent view', await waitText('Sent!', 15000)); await snap('sent'); - check('alice +7000', aliceBalance() === before + 7000, String(aliceBalance() - before)); + // alice's CLI wallet sees the coin once it has read its mailbox + let aliceGot = 0; + for (let i = 0; i < 30 && aliceGot !== 7000; i++) { aliceGot = aliceBalance() - before; if (aliceGot !== 7000) await sleep(500); } + check('alice +7000', aliceGot === 7000, String(aliceGot)); check('spending shows 18,000', await waitFace('Spending', 18000), String(await faceSat('Spending'))); await clickText('.card', 'Sent!'); // tap anywhere to proceed - await sleep(400); - const hist = await bodyText(); + let hist = ''; + for (let i = 0; i < 40; i++) { + hist = await bodyText(); + if (/received[\s\S]*\+25,000/i.test(hist) && /-7,000/.test(hist)) break; + await sleep(250); + } check('history shows the receive', /received[\s\S]*\+25,000/i.test(hist)); check('history shows the send', /-7,000/.test(hist)); console.log('\n[4] Savings -> Spending (board)'); - await page.evaluate(() => uiApp.ctx.setAccount('savings')); + await openReceive('savings'); let btcAddr = ''; for (let i = 0; i < 40 && !btcAddr; i++) { btcAddr = ((await tabPane()).match(/bcrt1[a-z0-9]+/) || [''])[0]; @@ -165,7 +212,7 @@ try { // sending Savings to your own Spending address opens the move panel instead await clickText('button', 'Send'); await sleep(500); - await page.type('textarea.send-dest', arkAddr); + await pasteDest(arkAddr); check('own Spending address opens the move panel', await waitText('Moving from Savings instead', 10000)); await page.type('.balance-move input', '30000'); await sleep(300); diff --git a/tools/comments-test.js b/tools/comments-test.js index 01d47d5..6801f44 100644 --- a/tools/comments-test.js +++ b/tools/comments-test.js @@ -25,12 +25,31 @@ const html = await buildHtml({ minify: true, pwa: false }); const server = Bun.serve({ port: 5283, 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(() => { +// The article and its comments, fetched above, are also handed to the app +// on every relay socket that asks for them: the app reaching the same public +// relays as this script (one of them can be refusing this IP) is not what's +// under test. +await page.evaluateOnNewDocument((known) => { window.__sent = []; window.__all = []; + const matches = (f, e) => { + if (f.ids && !f.ids.includes(e.id)) return false; + if (f.kinds && !f.kinds.includes(e.kind)) return false; + if (f.authors && !f.authors.includes(e.pubkey)) return false; + for (const [k, vals] of Object.entries(f)) { + if (k[0] !== '#' || !Array.isArray(vals)) continue; + if (!e.tags.some((t) => t[0] === k.slice(1) && vals.includes(t[1]))) return false; + } + return !!(f.ids || Object.keys(f).some((k) => k[0] === '#')); + }; const send = WebSocket.prototype.send; WebSocket.prototype.send = function (d) { window.__all.push(this.url + ' ' + String(d).slice(0, 60)); + if (typeof d === 'string' && d.startsWith('["REQ"')) { + const [, sub, ...filters] = JSON.parse(d); + const hits = known.filter((e) => filters.some((f) => matches(f, e))); + if (hits.length) setTimeout(() => { for (const e of hits) this.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(['EVENT', sub, e]) })); }, 30); + } if (typeof d === 'string' && d.startsWith('["EVENT"')) { // every event: nothing leaves the test const ev = JSON.parse(d)[1]; window.__sent.push(ev); @@ -40,7 +59,7 @@ await page.evaluateOnNewDocument(() => { } return send.call(this, d); }; -}); +}, [root, ...comments]); 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, ms = 15000) => { for (let i = 0; i < ms / 200; i++) { if (await page.evaluate(fn)) return true; await sleep(200); } return false; }; |
