aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-02 09:26:56 -0700
committerAdam Soltys <[email protected]>2026-10-02 09:26:56 -0700
commitfe569ffa701e3f9b54330def31193217eb470ab9 (patch)
treea13f33180c53346f49369f4bd52d03bfa38005e6 /tools
parent65aa6c93153b28554127627dde2883a0685eeaff (diff)
downloadcoinosv3-fe569ffa701e3f9b54330def31193217eb470ab9.tar.gz
coinosv3-fe569ffa701e3f9b54330def31193217eb470ab9.zip
Balance card gear replaces Accounts; Spending's coins live behind it
- The "Accounts" pill on each balance face is now a gear. It opens that face's settings: Spending leads with its coins (renewal, the co-operative move to Savings, unilateral exit), then Security (lock, change password), the recovery phrase, auto-logout; Savings adds its public key. A wallet with no nostr identity also gets sign-in and log-out there, since it has no profile page to hold them. - The exit cost is no longer said under the balance; the Spending settings state it, always, not only when coin chains run deep. - The lone Manage link under the balance is gone (the gear is the way in); warning notices that are about the coins link to the same page. - The Accounts list, Add wallet and Delete all are no longer reached from the wallet (Delete all stays behind profile > Log out > forget). - Tests follow the gear: padlock-ask, lock-wallet and account-access pass again; cooperative-exit-ui clicks the Spending gear. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
Diffstat (limited to 'tools')
-rw-r--r--tools/account-access-test.js25
-rw-r--r--tools/ark/cooperative-exit-ui-test.js27
-rw-r--r--tools/lock-wallet-test.js16
-rw-r--r--tools/padlock-ask-test.js12
4 files changed, 54 insertions, 26 deletions
diff --git a/tools/account-access-test.js b/tools/account-access-test.js
index d2685de..4a83fd0 100644
--- a/tools/account-access-test.js
+++ b/tools/account-access-test.js
@@ -31,9 +31,9 @@ try {
await page.reload();
await page.waitForSelector('.header-avatar');
check('fresh local account defaults to mainnet', await page.evaluate(() => JSON.parse(localStorage.getItem('btc-wallet-watch'))[0].network === 'mainnet'));
- check('anonymous avatar has an accessible Accounts label', await page.$eval('.header-avatar', (e) => e.getAttribute('aria-label') === 'Accounts'));
+ check('anonymous avatar has an accessible label', await page.$eval('.header-avatar', (e) => e.getAttribute('aria-label') === 'Account settings'));
await page.click('.header-avatar');
- check('avatar opens Accounts with sign-in and logout', await page.evaluate(() => ['Accounts', 'Sign into another account', 'Log out'].every((s) => document.body.innerText.includes(s))));
+ check('avatar opens the wallet settings with sign-in and logout', await page.evaluate(() => ['Security', 'Sign into another account', 'Log out'].every((s) => document.body.innerText.includes(s))));
const saved = await page.evaluate(() => localStorage.getItem('btc-wallet-watch'));
await click('Sign into another account');
check('sign-in opens on Create new, not the seed import box', await page.evaluate(() =>
@@ -57,28 +57,29 @@ try {
}, { key: cacheKeyFor(xpub) });
await page.reload();
await page.waitForSelector('[aria-label="Messages"]');
- await click('Accounts');
- const onAccounts = () => page.evaluate(() => [...document.querySelectorAll('h3')].some(e => e.textContent === 'Accounts'));
+ // the balance card's gear: a page like any other under the header
+ await page.click('.balance-gear');
+ const onSettings = () => page.evaluate(() => history.state.nav.screen === 'accountSettings' && [...document.querySelectorAll('h3')].some(e => e.textContent === 'Security'));
for (const label of ['Search', 'Messages', 'Your profile', 'Settings']) {
- check(label + ' starts on Accounts', await onAccounts());
+ check(label + ' starts on the wallet settings', await onSettings());
await page.click('[aria-label="' + label + '"]');
- check(label + ' opens from Accounts', await page.evaluate(label => {
+ check(label + ' opens from the wallet settings', await page.evaluate(label => {
const nav = history.state.nav;
- return nav.screen === 'wallet' && ![...document.querySelectorAll('h3')].some(e => e.textContent === 'Accounts')
+ return nav.screen === 'wallet'
&& (label === 'Search' ? !!document.querySelector('.user-search-input')
: label === 'Messages' ? nav.chatOpen && nav.msgView === 'home'
- : label === 'Your profile' ? !!document.querySelector('.npub-box') && nav.profilePk === 'a'.repeat(64)
+ : label === 'Your profile' ? nav.profilePk === 'a'.repeat(64)
: nav.tab === 'settings');
}, label));
await page.goBack();
- await page.waitForFunction(() => [...document.querySelectorAll('h3')].some(e => e.textContent === 'Accounts'));
- check('Back returns from ' + label + ' to Accounts', await onAccounts());
+ await page.waitForFunction(() => history.state.nav.screen === 'accountSettings');
+ check('Back returns from ' + label + ' to the wallet settings', await onSettings());
}
await page.click('[aria-label="Lock wallet"]');
- check('Lock opens its password prompt from Accounts', await page.evaluate(() => !!document.querySelector('input[type="password"]')));
+ check('Lock opens its password prompt from the wallet settings', await page.evaluate(() => !!document.querySelector('input[type="password"]')));
await click('Not now');
await page.click('.brand');
- check('the logo returns to the wallet', await page.evaluate(() => history.state.nav.screen === 'wallet' && ![...document.querySelectorAll('h3')].some(e => e.textContent === 'Accounts')));
+ check('the logo returns to the wallet', await page.evaluate(() => history.state.nav.screen === 'wallet' && !!document.querySelector('.balance-gear')));
assert.deepEqual(errors, []);
check('no browser errors', true);
} finally { await browser.close(); server.stop(true); }
diff --git a/tools/ark/cooperative-exit-ui-test.js b/tools/ark/cooperative-exit-ui-test.js
index 1af394e..831ed3b 100644
--- a/tools/ark/cooperative-exit-ui-test.js
+++ b/tools/ark/cooperative-exit-ui-test.js
@@ -8,10 +8,11 @@ import { wordlist } from '@scure/bip39/wordlists/english';
const bundle = await Bun.build({ entrypoints: ['src/app.js'], target: 'browser', minify: true,
plugins: [{ name: 'cooperative-exit-test', setup(build) {
build.onLoad({ filter: /src\/app\.js$/ }, async ({ path }) => ({ loader: 'js', contents: await Bun.file(path).text() + `
-window.coopApp = { ui, wallet, ctx, render, enterWallet, get ready() { return !_bootDeciding; } };
+window.coopApp = { ui, wallet, ctx, render, enterWallet, get ready() { return !_bootDeciding; },
+ setSpending: () => { const a = activeAccount(); if (a) a.kind = 'spending'; } };
` }));
build.onLoad({ filter: /src\/features\/ark\.js$/ }, async ({ path }) => ({ loader: 'js', contents:
- (await Bun.file(path).text()).replace(" return {\n id: 'ark',", `
+ (await Bun.file(path).text()).replace(' arkReady() { return arkAvailable(); },', ' arkReady() { return window.forceArkReady || arkAvailable(); },').replace(" return {\n id: 'ark',", `
window.coopFeature = { setManager: (mgr) => { ark = mgr; }, offboard: doArkOffboard };
return {\n id: 'ark',`) }));
build.onLoad({ filter: /src\/video-compress\.js$/ }, () => ({ loader: 'js', contents: 'export async function compressVideo(f) { return f; }' }));
@@ -70,6 +71,28 @@ try {
const send = () => page.evaluate(() => [...document.querySelectorAll('.card')]
.find((c) => c.querySelector('h3')?.textContent === 'Co-operative exit').querySelector('.btn-primary').click());
const edit = (selector, value) => page.$eval(selector, (e, value) => { e.value = value; e.dispatchEvent(new Event('input', { bubbles: true })); }, value);
+ // The balance card's gear is the way in: Spending's settings lead with
+ // its coins and carry what a unilateral exit costs; the home screen no
+ // longer says it under the balance.
+ await page.evaluate(() => { window.forceArkReady = true; coopApp.wallet.saveFeatureState('arkDepth', { show: true, exitFee: 4321, key: 'x' }); coopApp.setSpending(); coopApp.render(); });
+ const homeText = await page.evaluate(() => document.body.innerText);
+ assert(!/Unilateral exit costs/.test(homeText), 'the exit cost is not on the home screen');
+ assert(!/Accounts/.test(homeText), 'no Accounts button on the home screen');
+ await page.screenshot({ path: '/tmp/gear-home.png' });
+ const gears = await page.$$eval('.balance-gear', (es) => es.length);
+ assert(gears >= 1, 'the balance card wears a gear');
+ const faces = await page.$$eval('.balance-face', (fs) => fs.map((f) => f.innerText.split('\n')[0]));
+ assert(faces.some((f) => /spending/i.test(f)), 'a Spending face: ' + JSON.stringify(faces));
+ await page.evaluate(() => [...document.querySelectorAll('.balance-face')].find((f) => /spending/i.test(f.innerText.split('\n')[0])).querySelector('.balance-gear').click());
+ await page.waitForSelector('.coin-table');
+ const st = await page.evaluate(() => ({ nav: coopApp.ui.screen, text: document.body.innerText }));
+ assert.equal(st.nav, 'accountSettings');
+ assert.match(st.text, /Unilateral exit costs about 4,?321 sats/, 'the exit cost lives on the settings page');
+ assert.match(st.text, /Security/); assert.match(st.text, /Lock wallet/); assert.match(st.text, /Recovery phrase/);
+ await page.screenshot({ path: '/tmp/gear-settings.png', fullPage: true });
+ console.log('✓ The Spending gear opens its settings: coins, the exit cost (gone from home), lock and recovery phrase');
+ await page.evaluate(() => { window.forceArkReady = false; coopApp.wallet.saveFeatureState('arkDepth', null); coopApp.ui.screen = 'wallet'; coopApp.render(); });
+
await manage();
assert.deepEqual(await page.$$eval('.card h4', (es) => es.map((e) => e.textContent)), ['Renewal', 'Co-operative exit', 'Unilateral exit']);
await coopButton();
diff --git a/tools/lock-wallet-test.js b/tools/lock-wallet-test.js
index 13d2b91..107ccd0 100644
--- a/tools/lock-wallet-test.js
+++ b/tools/lock-wallet-test.js
@@ -82,8 +82,8 @@ try {
await sleep(1000);
console.log('\n[the padlock still asks — locking needs a password]');
- // the padlock lives on the Accounts page (the balance card's switcher opens it)
- await page.evaluate(() => document.querySelector('.balance-switch')?.click());
+ // the padlock lives on the wallet settings page (the balance card's gear)
+ await page.evaluate(() => document.querySelector('.balance-gear')?.click());
await sleep(600);
await page.evaluate(() => { const b = [...document.querySelectorAll('button')].find((n) => n.title === 'Lock wallet'); if (b) b.click(); });
await sleep(600);
@@ -91,7 +91,7 @@ try {
check('padlock asks even after "Not now"', /protect this device/i.test(txt), txt.slice(0, 120).replace(/\n+/g, ' | '));
await click('button', 'Not now');
await sleep(600);
- check('and declining lands back on the Accounts page', /add account/i.test(await body()));
+ check('and declining lands back on the wallet settings', /security/i.test(await body()));
console.log('\n[now with a password]');
await click('button', 'Change password');
@@ -139,13 +139,17 @@ try {
check('backing out deletes nothing', (await vaultSize()) > 0);
console.log('\n[delete all is the destructive one]');
- await page.evaluate(() => document.querySelector('.balance-switch')?.click());
+ // the same road as above, this time all the way: profile → Log out →
+ // forget all data → the warning → Delete all
+ await page.evaluate(() => document.querySelector('.header-avatar')?.click());
await sleep(800);
- await click('button', 'Delete all');
+ await click('button', 'Log out');
+ await sleep(500);
+ await click('button', 'forget all data');
await sleep(600);
txt = await body();
check('it warns first', /delete all/i.test(txt) && /(delete|remove|erase|wipe|lose)/i.test(txt), txt.slice(0, 160).replace(/\n+/g, ' | '));
- await page.evaluate(() => { const b = [...document.querySelectorAll('.btn-primary')].find((e) => /delete all/i.test(e.textContent)); if (b) b.click(); });
+ await page.evaluate(() => { const b = [...document.querySelectorAll('.confirm-pop button')].find((e) => /delete all/i.test(e.textContent)); if (b) b.click(); });
await sleep(1200);
txt = await body();
check('everything is gone', (await vaultSize()) === 0);
diff --git a/tools/padlock-ask-test.js b/tools/padlock-ask-test.js
index f1fa3cf..5570b8f 100644
--- a/tools/padlock-ask-test.js
+++ b/tools/padlock-ask-test.js
@@ -35,25 +35,25 @@ try {
await click('button', 'Open wallet');
await waitText('receive', 20000);
- // open the Accounts page (the balance card's switcher) — the padlock lives there
- await page.evaluate(() => document.querySelector('.balance-switch')?.click());
+ // open the balance card's settings (its gear) — the padlock lives there
+ await page.evaluate(() => document.querySelector('.balance-gear')?.click());
await sleep(900);
- check('Accounts page is open', /add account/i.test(await bodyText()), (await bodyText()).slice(0, 60).replace(/\n/g, ' | '));
+ check('the wallet settings page is open', /security/i.test(await bodyText()), (await bodyText()).slice(0, 60).replace(/\n/g, ' | '));
// tap the padlock — the ask must appear without navigating anywhere
const tapped = await page.evaluate(() => {
const b = [...document.querySelectorAll('button')].find((e) => /lock/i.test(e.getAttribute('aria-label') || ''));
if (!b) return false; b.click(); return true;
});
- check('padlock is reachable from the Accounts page', tapped);
+ check('padlock is reachable from the wallet settings', tapped);
await sleep(500);
const text = await bodyText();
check('the ask appears on the tap', /protect this device/i.test(text), text.split('\n').slice(0, 3).join(' | '));
- // declining lands back on the Accounts page, not somewhere else
+ // declining lands back on the settings page, not somewhere else
await click('button', 'not now');
await sleep(600);
- check('declining lands back on the Accounts page', /add account/i.test(await bodyText()), (await bodyText()).slice(0, 60).replace(/\n/g, ' | '));
+ check('declining lands back on the wallet settings', /security/i.test(await bodyText()), (await bodyText()).slice(0, 60).replace(/\n/g, ' | '));
} finally {
await browser.close(); server.stop(true);
}