aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-19 22:28:36 -0700
committerAdam Soltys <[email protected]>2026-09-19 22:28:36 -0700
commit1b142c92ce869a6ebe97825931ef3553255ba58e (patch)
tree4a16e864ad7a827678c5bab888b8eb1764eb4657 /tools
parent74152d4ae33e1ea065769d261c615885c387590a (diff)
downloadcoinosv3-1b142c92ce869a6ebe97825931ef3553255ba58e.tar.gz
coinosv3-1b142c92ce869a6ebe97825931ef3553255ba58e.zip
Zaps: tap again while one flies, hold the bolt to change the amount.
A pending zap no longer refuses the next tap: the optimistic tally keeps one entry per zap and matches each report and receipt to the oldest it can describe, so two default-amount zaps stack, settle and hand over to their receipts one at a time. Instant post zaps run off the send screen's zap slot, so a second tap can't evict the one still resolving, and send action ids are unique within a millisecond. Holding ⚡ (500ms) opens the amount screen in an edit mode — Save alone, or Save and zap this note — from the action bar and the chat chips alike; the press's timer lives on the element so a background repaint mid-press can't turn a lifted finger into a hold. Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to 'tools')
-rw-r--r--tools/zap-feedback-test.js45
1 files changed, 32 insertions, 13 deletions
diff --git a/tools/zap-feedback-test.js b/tools/zap-feedback-test.js
index 2248913..bda3e23 100644
--- a/tools/zap-feedback-test.js
+++ b/tools/zap-feedback-test.js
@@ -42,7 +42,7 @@ function receipt(receiptId, pk, sats) {
}
receipt('base', author, 100); render();
window.test = {
- get calls() { return calls; }, get toasts() { return toasts; }, ui, render,
+ get calls() { return calls; }, get toasts() { return toasts; }, get amount() { return amount; }, ui, render,
settle: (ok) => feature.zapSettled(id, ok, amount),
receipt: () => receipt('ours', me, amount),
unavailable: () => mode = 'unavailable',
@@ -119,18 +119,20 @@ try {
check('the same tap plays the supplied MP3 clip', await page.evaluate(() => {
const samples = zapAudio.buffer?.getChannelData(0);
return zapAudio.starts === 1 && zapAudio.context.state === 'running'
- && zapAudio.buffer.duration > 1.8 && zapAudio.buffer.duration < 2
+ && zapAudio.buffer.duration > 1.2 && zapAudio.buffer.duration < 1.6
&& samples.some(v => Math.abs(v) > .05)
&& samples.every(v => Number.isFinite(v));
}));
- await page.evaluate(() => {
- window.effect = document.querySelector('.zap-fx'); test.render();
- document.querySelector('.note-zap').click();
- });
+ await page.evaluate(() => { window.effect = document.querySelector('.zap-fx'); test.render(); });
+ s = await state();
+ check('rerenders preserve the effect and charge nothing', s.effects === 1 && s.calls === 1 && await page.evaluate(() => effect.isConnected));
+ check('rerenders do not repeat the sound', await page.evaluate(() => zapAudio.starts === 1));
+ check('rerenders preserve the same post tremble', await page.evaluate(() => document.querySelector('article').getAnimations()[0] === tremble));
+ // A zap still in the air is no reason to refuse the next one.
+ await page.click('.note-zap');
s = await state();
- check('rerenders preserve effect; double tap does not charge twice', s.effects === 1 && s.calls === 1 && await page.evaluate(() => effect.isConnected));
- check('rerenders and duplicate taps do not repeat the sound', await page.evaluate(() => zapAudio.starts === 1));
- check('rerenders and duplicate taps preserve the same post tremble', await page.evaluate(() => document.querySelector('article').getAnimations()[0] === tremble));
+ check('a second tap while the first flies stacks another zap', s.effects === 2 && s.calls === 2 && s.tally === '142' && s.flying);
+ check('the second tap has its own sound', await page.evaluate(() => zapAudio.starts === 2));
// Freeze animations at their impact frame for an inspectable mobile preview.
await page.evaluate(() => document.querySelectorAll('.zap-fx *').forEach((e) => e.getAnimations().forEach((a) => { a.pause(); a.currentTime = 230; })));
const bounds = await page.$eval('.zap-fx-amount', (e) => { const r = e.getBoundingClientRect(); return { left: r.left, right: r.right }; });
@@ -138,8 +140,11 @@ try {
await page.screenshot({ path: '/tmp/coinos-zap-feedback.png' });
await page.evaluate(() => test.settle(false)); await pause(100);
s = await state();
- check('failure restores the previous total without another animation', s.tally === '100' && !s.flying && s.effects === 1);
- check('failure does not play another sound', await page.evaluate(() => zapAudio.starts === 1));
+ check('one failure takes one zap off; the other keeps flying', s.tally === '121' && s.flying && s.effects === 2);
+ await page.evaluate(() => test.settle(false)); await pause(100);
+ s = await state();
+ check('failure restores the previous total without another animation', s.tally === '100' && !s.flying && s.effects === 2);
+ check('failure does not play another sound', await page.evaluate(() => zapAudio.starts === 2));
await pause(1750);
check('effect cleans up', (await state()).effects === 0);
check('the post returns to rest without residual transforms', await page.$eval('article', e => !e.getAnimations().length && getComputedStyle(e).transform === 'none'));
@@ -150,7 +155,8 @@ try {
await pause(1750);
await page.evaluate(() => test.settle(true)); await pause(100);
check('late confirmation never replays the effect', (await state()).effects === 0);
- check('confirmation never replays the sound', await page.evaluate(() => zapAudio.starts === 2));
+ check('confirmation never replays the sound', await page.evaluate(() => zapAudio.starts === 3));
+ check('a repeated confirmation does not count the zap again', (await state()).tally === '121');
await page.evaluate(() => test.unavailable());
await page.click('.note-zap'); await pause(100);
check('unavailable payments roll back in place with an error toast', (await state()).tally === '121' && await page.evaluate(() => test.toasts.length === 1 && test.ui.chatOpen));
@@ -169,12 +175,25 @@ try {
await page.click('.note-zap'); await pause(100);
check('Ark payment begins with the optimistic amount', (await state()).tally === '142');
await page.evaluate(() => test.finishArk(false)); await pause(100);
- check('Ark failure rolls back and toasts without opening Send', (await state()).tally === '121' && await page.evaluate(() => test.ui.chatOpen && test.ui.arkZap === null && test.toasts.at(-1).includes('Ark payment failed')));
+ check('Ark failure rolls back and toasts without opening Send', (await state()).tally === '121' && await page.evaluate(() => test.ui.chatOpen && !test.ui.arkZap && test.toasts.at(-1).includes('Ark payment failed')));
await pause(1750);
await page.click('.note-zap'); await pause(100);
const toastCount = await page.evaluate(() => test.toasts.length);
await page.evaluate(() => test.finishArk(true)); await pause(100);
check('Ark success settles silently without a second effect', (await state()).tally === '142' && !(await state()).flying && (await state()).effects === 1 && await page.evaluate((n) => test.toasts.length === n, toastCount));
+ // Holding the bolt opens the amount screen instead of paying.
+ const calls = await page.evaluate(() => test.calls);
+ const box = await (await page.$('.note-zap')).boundingBox();
+ await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
+ await page.mouse.down(); await pause(650); await page.mouse.up(); await pause(100);
+ check('holding the bolt opens the amount screen without paying', await page.evaluate((n) => test.calls === n && document.querySelector('#setup input')?.value === '21', calls));
+ await page.evaluate(() => { const i = document.querySelector('#setup input'); i.value = '50'; i.dispatchEvent(new Event('input')); });
+ await page.click('#setup .btn-primary'); await pause(100);
+ check('saving from the held screen changes the amount and pays nothing', await page.evaluate((n) => test.calls === n && !document.querySelector('#setup input') && test.amount === 50, calls));
+ await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
+ await page.mouse.down(); await pause(200); await page.mouse.up(); await pause(100);
+ check('a short press is still a tap', await page.evaluate((n) => test.calls === n + 1 && !document.querySelector('#setup input'), calls));
+ await page.evaluate(() => test.finishArk(true)); await pause(100);
const silentPage = await browser.newPage();
silentPage.on('pageerror', e => errors.push(e.message));
await silentPage.evaluateOnNewDocument(() => {