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
|
// Exercise the production carousel handlers and DOM morph with native browser
// scrolling; wallet/network data is replaced by two fixed account faces.
import assert from 'node:assert/strict';
import puppeteer from './puppeteer.js';
const source = await Bun.file('src/app.js').text();
const dom = source.slice(source.indexOf('function h('), source.indexOf('const root = document.getElementById'));
const carousel = source.slice(source.indexOf(' if (carouselMode) {'), source.indexOf(" const dtRaw = animWindow('acct'"));
const end = source.slice(source.indexOf('function endCarouselDrag()'), source.indexOf('function renderCrash('));
const tab = source.slice(source.indexOf(" const TAB_ORDER = ['history', 'receive', 'send'];"), source.indexOf(" return h(\n 'div',", source.indexOf(" const TAB_ORDER = ['history', 'receive', 'send'];")));
const animation = source.slice(source.indexOf('const _uiSeen ='), source.indexOf('// A balance that counts'));
const js = `${dom}
${animation}
let _carDragging = false, _renderDeferred = false, _accDir = null;
const ui = { account: 'spending', tab: 'history' }, ACCOUNT_KEY = 'test-account';
const accountSel = () => ui.account;
const root = document.getElementById('app');
window.changes = [];
function balanceCard() {
const carouselMode = true;
const sel = accountSel(), faceFor = (v) => h('div', { style: 'height:110px' }, v), cardExtras = () => [];
${carousel}
}
${end}
function tabContent() { return h('div', { style: 'height:350px' }, ui.account + ':' + ui.tab); }
function render() {
if (_carDragging) { _renderDeferred = true; return; }
${tab}
const next = h('div', {}, balanceCard(), pane);
if (root.firstChild) morph(root.firstChild, next); else root.append(next);
if (changes.at(-1) !== ui.account) changes.push(ui.account);
}
window.state = () => ({ account: ui.account, changes: [...changes], frozen: !!document.querySelector('.bal-carousel')._skipMorph });
render();`;
const css = await Bun.file('src/style.css').text();
const html = `<!doctype html><style>${css}\n#app { width:400px; margin:40px auto; }</style><div id="app"></div><script>${js}</script>`;
const server = Bun.serve({ port: 0, fetch: () => new Response(html, { headers: { 'Content-Type': 'text/html' } }) });
const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox'] });
try {
const page = await browser.newPage();
const errors = [];
page.on('pageerror', (e) => errors.push(e.message));
await page.goto(server.url.href);
await page.waitForSelector('.bal-carousel');
await Bun.sleep(400);
for (const injectStaleEnd of [false, true]) {
for (const direction of [-1, 1]) {
await page.evaluate(() => { changes.length = 0; changes.push(ui.account); });
const box = await page.$eval('.bal-carousel', (e) => { const r = e.getBoundingClientRect(); return { x: r.x + r.width/2, y: r.y + 70 }; });
await page.mouse.move(box.x, box.y); await page.mouse.down();
await page.mouse.move(box.x + direction * 30, box.y);
await page.mouse.move(box.x + direction * 75, box.y);
if (injectStaleEnd) {
// A queued scrollend from the final direct scrollLeft write arrives
// after release, before the first smooth-scroll animation frame.
await page.evaluate(() => window.addEventListener('pointerup', () => {
document.querySelector('.bal-carousel').dispatchEvent(new Event('scrollend'));
}, { once: true }));
}
await page.mouse.up();
await Bun.sleep(950);
const state = await page.evaluate(() => window.state());
console.log({ injectStaleEnd, direction, ...state });
assert.deepEqual(state.changes, direction < 0 ? ['spending', 'savings'] : ['savings', 'spending'], 'one content transition per flick');
assert.equal(state.frozen, false);
}
}
// A small slow pull stays on the same card and still releases the freeze.
await page.evaluate(() => {
changes.length = 0; changes.push(ui.account);
const el = document.querySelector('.bal-carousel');
el.dispatchEvent(new PointerEvent('pointerdown', { pointerType: 'mouse', button: 0, clientX: 300, bubbles: true }));
window.dispatchEvent(new PointerEvent('pointermove', { clientX: 290 }));
window.dispatchEvent(new PointerEvent('pointerup'));
});
await Bun.sleep(800);
assert.deepEqual((await page.evaluate(() => window.state())).changes, ['spending']);
assert.equal((await page.evaluate(() => window.state())).frozen, false);
// Simulate a browser dropping the requested smooth scroll entirely: the
// fallback must finish the intended switch, not unfreeze at the old card.
await page.evaluate(() => {
const el = document.querySelector('.bal-carousel');
el.scrollTo = function (opts) { if (opts.behavior !== 'smooth') Element.prototype.scrollTo.call(this, opts); };
el.dispatchEvent(new PointerEvent('pointerdown', { pointerType: 'mouse', button: 0, clientX: 300, bubbles: true }));
window.dispatchEvent(new PointerEvent('pointermove', { clientX: 220 }));
window.dispatchEvent(new PointerEvent('pointerup'));
});
await Bun.sleep(850);
assert.deepEqual((await page.evaluate(() => window.state())).changes, ['spending', 'savings']);
assert.equal((await page.evaluate(() => window.state())).frozen, false);
await page.evaluate(() => { delete document.querySelector('.bal-carousel').scrollTo; });
// Grab again before a glide finishes, and hold beyond its old fallback.
const box = await page.$eval('.bal-carousel', (el) => { const r = el.getBoundingClientRect(); return { x: r.x + 150, y: r.y + 70 }; });
await page.mouse.move(box.x, box.y); await page.mouse.down();
await page.mouse.move(box.x + 80, box.y); await page.mouse.up();
await page.mouse.down(); await page.mouse.move(box.x + 60, box.y);
await Bun.sleep(800);
assert.equal((await page.evaluate(() => window.state())).frozen, true, 'old fallback cannot unfreeze a new drag');
await page.mouse.up(); await Bun.sleep(850);
assert.equal((await page.evaluate(() => window.state())).frozen, false);
assert.deepEqual(errors, []);
console.log('✓ short pull, missing scrollend/glide fallback and re-grab hold');
console.log('✓ fast flicks in both directions switch content once, including a stale drag scrollend');
} finally { await browser.close(); server.stop(true); }
|