mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-09 02:08:19 +01:00
fix(browse): touch points wait for a rendered frame, so a tap right after goto lands where it was aimed (#3956)
* fix(browse): a touch right after navigation waits for a rendered frame, so it lands where aimed (GSTA-262) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(browse): the frame wait before a touch is capped at 1 s, and touch cancel waits for the frame that delivers it (GSTA-279) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
7c0e396d1a
commit
4b9d74faf9
3 files changed
+32
No files matched your search
@@ -28,7 +28,26 @@ function usage(problem: string, fix: string): Error {
|
||||
return new Error(`Usage: ${problem}\nfix: ${fix}`);
|
||||
}
|
||||
|
||||
const FRAME_WAIT_MS = 1000;
|
||||
|
||||
/**
|
||||
* Chromium maps touch points with the page scale of the last rendered frame; a just-navigated document maps them at the 980px default until it renders.
|
||||
* The wait runs page JS, so a page that replaces requestAnimationFrame, or navigates mid-wait, only costs FRAME_WAIT_MS, never the gesture queue.
|
||||
*/
|
||||
async function renderedFrame(session: TabSession): Promise<void> {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
const frame = session.getPage().evaluate(() => document.visibilityState === 'hidden'
|
||||
? undefined
|
||||
: new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))));
|
||||
await Promise.race([
|
||||
frame.catch(() => {}),
|
||||
new Promise<void>((resolve) => { timer = setTimeout(resolve, FRAME_WAIT_MS); }),
|
||||
]);
|
||||
clearTimeout(timer);
|
||||
}
|
||||
|
||||
async function dispatch(session: TabSession, type: 'touchStart' | 'touchMove' | 'touchEnd', points: Points): Promise<void> {
|
||||
if (type === 'touchStart') await renderedFrame(session);
|
||||
const cdp = await tabCdpSession(session.getPage());
|
||||
session.setFingerCancel(() => {
|
||||
cdp.send('Input.dispatchTouchEvent', { type: 'touchCancel', touchPoints: [] }).catch(() => {});
|
||||
@@ -153,6 +172,7 @@ async function touch(session: TabSession, args: string[]): Promise<string> {
|
||||
if (held.size === 0) return 'No fingers held; nothing to cancel';
|
||||
const cdp = await tabCdpSession(session.getPage());
|
||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchCancel', touchPoints: [] });
|
||||
await renderedFrame(session);
|
||||
held.clear();
|
||||
return 'Cancelled all held fingers';
|
||||
}
|
||||
|
||||
@@ -250,6 +250,15 @@ describe('touch input', () => {
|
||||
expect(await page().evaluate(() => (window as any).__log.at(-1).type)).toBe('touchcancel');
|
||||
});
|
||||
|
||||
test('a page that replaces requestAnimationFrame still gets its tap, and the next gesture runs', async () => {
|
||||
await freshTouchTab();
|
||||
await page().evaluate(() => { window.requestAnimationFrame = () => 0; });
|
||||
const within = (p: Promise<unknown>) => Promise.race([p.then(() => 'done'), new Promise((resolve) => setTimeout(() => resolve('hung'), 10_000))]);
|
||||
expect(await within(run('tap', ['#fire']))).toBe('done');
|
||||
expect(await within(run('tap', ['#fire']))).toBe('done');
|
||||
expect(await data('#fire', 'shots')).toBe('2');
|
||||
});
|
||||
|
||||
test('touch cancel with no finger held is a no-op, not a protocol error', async () => {
|
||||
await freshTouchTab();
|
||||
expect(await run('touch', ['cancel'])).toContain('nothing to cancel');
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
### Fixed
|
||||
|
||||
- **A tap or touch right after a page loads now lands where you aimed (GSTA-262).** `$B goto <url> && $B tap 240 440` on a phone-emulated tab could hit a different element. `$B goto` returns once the HTML is parsed, but before the page has drawn a frame with its mobile viewport, and Chromium places touch points using the last drawn frame. So the touch was scaled by 0.401 (393/980, the desktop default width) and landed at (96, 176), on the zoom area instead of the fire button. Every `tap`, `swipe`, `pinch` and `touch down` now waits for the page to draw a frame before the first touch (two animation frames; skipped for a hidden tab, and capped at one second, so a page that replaces `requestAnimationFrame` or navigates mid-wait can't stall the gesture). `$B touch cancel` also waits for a drawn frame before it returns, because Chromium delivers the touchcancel event with the next frame, so a check right after it could still see the old touch. This also fixes the flaky `touch input > held fingers are cancelled on navigation and by touch cancel` test (22 failures on 2026-11-03 and 12 more the next day, all in free shard 4). Under CPU load, a loop of navigate-then-touch missed 45 of 60 times before the fix and 0 of 60 after; the whole test file failed 1 of 20 runs before and 0 of 20 after. A touch-then-cancel loop under the same load saw the stale touch 2 of 60 times before the cancel wait and 0 of 300 after.
|
||||
Reference in new issue
Block a user