Files
gui/apps/kitchen-sink/tests/SheetScrollableDrag.animated.test.tsx
zeekayandHanzo Dev 69c6e860d7 transplant onto tamagui v2.6.0, brand as a re-runnable transform
This repo could never sync. There was NO merge-base with tamagui — 372 commits
here against 15,791 there, unrelated histories — because it began as a
copy-paste rebrand with a flattened layout. Every "sync" since was a hand
re-application that drifted, which is why the tree sat on 7.3.1 while upstream
shipped fixes we then rediscovered by hand.

This commit's PARENT IS TAMAGUI. A future sync is `git merge tama/main` then
`node scripts/rebrand.mjs` — a real merge, because the history is now shared.

174 packages: 166 upstream, our 8 restored (chrome, shell, telemetry, recipes,
get, font-geist-{sans,mono}, @hanzo/booker), bento-get gained. 170/170 build.

Our patches to files UPSTREAM ALSO OWNS are carried, which the package-level
delta missed and which was where the value actually was:
  - useMedia's server snapshot (`initState ?? getMedia()`). Upstream still
    returns undefined there, so every Tamagui Dialog kills a prerender — the
    exact crash hanzo.app spent a day on.
  - data-slot on web host nodes, absent upstream entirely.
Both applied ONTO upstream's newer files (it rewrote useMedia +150/-63 and
createComponent +160/-55), marked `PATCH (ours, carried across the transplant)`.

Seven traps are encoded in scripts/rebrand.mjs, each found by a gate:
skip itself and lockfiles; special-case the double-branded names in BOTH scoped
and bare form; map path strings, not just paths; drop a level for the flattened
group; check disk rather than a stale index.

Rust build artifacts are NOT committed. An earlier draft of this commit swept
apps/desktop/src-tauri/target/debug in via `git add -A` — 3,859 files including
a 272MB .a, over GitHub's 100MB cap, so the push could never have landed. They
are gitignored now.

Co-Authored-By: Hanzo Dev <[email protected]>
2026-07-30 18:25:06 -07:00

1431 lines
49 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Playwright E2E Test for Sheet + ScrollView gesture coordination
*
* Ported from Detox tests to verify web implementation matches native quality.
*
* Tests the smooth handoff behavior between sheet dragging and scrollview scrolling:
* 1. At top snap point + swipe up → scroll content naturally
* 2. At top snap point + swipe down → drag sheet down (NOT scroll)
* 3. Scrolled down + drag down → scroll to top THEN hand off to sheet drag
* 4. Dragging up + hit sheet top → continue into scrolling
*/
import { expect, test, type CDPSession, type Page } from '@playwright/test'
import { setupPage } from './test-utils'
// mobile viewport with touch support for realistic sheet testing
test.use({
viewport: { width: 390, height: 844 },
hasTouch: true,
isMobile: true,
})
test.beforeEach(async ({ page }) => {
// capture console logs for debugging
page.on('console', (msg) => {
if (
msg.text().includes('[Sheet') ||
msg.text().includes('[ScrollView') ||
msg.text().includes('[gesture')
) {
console.log(`BROWSER: ${msg.text()}`)
}
})
await setupPage(page, { name: 'SheetScrollableDrag', type: 'useCase' })
})
/**
* perform a drag gesture using touch events via touchscreen API
* this properly triggers touch events which our gesture handlers listen for
*
* use mode: 'touch' for scrollview/content areas (synthetic touch events)
* use mode: 'trusted-touch' for handle/frame areas (real mobile touch path)
*/
async function dragSheet(
page: Page,
startX: number,
startY: number,
deltaY: number,
options: { steps?: number; stepDelay?: number; mode?: 'touch' | 'trusted-touch' } = {}
) {
const { steps = 20, stepDelay = 16, mode = 'touch' } = options
if (mode === 'trusted-touch') {
const cdp = await page.context().newCDPSession(page)
let currentY = startY
await cdp.send('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints: [{ x: startX, y: currentY }],
})
for (let i = 1; i <= steps; i++) {
currentY = startY + (deltaY * i) / steps
await cdp.send('Input.dispatchTouchEvent', {
type: 'touchMove',
touchPoints: [{ x: startX, y: currentY }],
})
await page.waitForTimeout(stepDelay)
}
await cdp.send('Input.dispatchTouchEvent', {
type: 'touchEnd',
touchPoints: [],
})
await cdp.detach()
return
}
// touch mode for ScrollView areas
const endY = startY + deltaY
// touchscreen doesn't have a direct swipe, so we use evaluate to dispatch touch events
await page.evaluate(
({ startX, startY, endY, steps, stepDelay }) => {
return new Promise<void>((resolve) => {
const target = document.elementFromPoint(startX, startY)
if (!target) {
resolve()
return
}
const dispatchTouch = (type: string, x: number, y: number) => {
const touch = new Touch({
identifier: 0,
target,
clientX: x,
clientY: y,
pageX: x,
pageY: y,
})
const touchEvent = new TouchEvent(type, {
bubbles: true,
cancelable: true,
touches: type === 'touchend' ? [] : [touch],
targetTouches: type === 'touchend' ? [] : [touch],
changedTouches: [touch],
})
target.dispatchEvent(touchEvent)
}
// touchstart
dispatchTouch('touchstart', startX, startY)
let step = 0
const interval = setInterval(() => {
step++
const progress = step / steps
const currentY = startY + (endY - startY) * progress
dispatchTouch('touchmove', startX, currentY)
if (step >= steps) {
clearInterval(interval)
dispatchTouch('touchend', startX, endY)
resolve()
}
}, stepDelay)
})
},
{ startX, startY, endY, steps, stepDelay }
)
}
async function readScrollableDragState(page: Page) {
return page.evaluate(() => {
const frame = document.querySelector<HTMLElement>(
'[data-testid="sheet-scrollable-drag-frame"]'
)
const scrollview = document.querySelector<HTMLElement>(
'[data-testid="sheet-scrollable-drag-scrollview"]'
)
const unexpectedClose = document.querySelector<HTMLElement>(
'[data-testid="sheet-scrollable-drag-unexpected-close"]'
)
return {
frameTop: frame?.getBoundingClientRect().top ?? 0,
scrollTop: scrollview?.scrollTop ?? 0,
unexpectedClose: unexpectedClose?.textContent ?? '',
}
})
}
async function trustedTouchMove(
page: Page,
cdp: CDPSession,
point: { x: number; y: number },
deltaY: number,
steps: number
) {
for (let i = 0; i < steps; i++) {
point.y += deltaY / steps
await cdp.send('Input.dispatchTouchEvent', {
type: 'touchMove',
touchPoints: [{ x: point.x, y: point.y }],
})
await page.waitForTimeout(16)
}
return readScrollableDragState(page)
}
test.describe('SheetScrollableDrag - RNGH Web Equivalent', () => {
test('should open sheet at position 0', async ({ page }) => {
await page.getByTestId('sheet-scrollable-drag-trigger').click()
await page.waitForTimeout(600)
const frame = page.getByTestId('sheet-scrollable-drag-frame')
await expect(frame).toBeVisible({ timeout: 5000 })
// should be at position 0 (top snap)
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 0'
)
await expect(page.getByTestId('sheet-scrollable-drag-scroll-y')).toContainText(
'ScrollView Y: 0'
)
})
test('Case 1: drag DOWN at scrollY=0 should drag sheet, NOT scroll', async ({
page,
}) => {
// open sheet
await page.getByTestId('sheet-scrollable-drag-trigger').click()
await page.waitForTimeout(600)
const frame = page.getByTestId('sheet-scrollable-drag-frame')
await expect(frame).toBeVisible({ timeout: 5000 })
// verify starting state
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 0'
)
await expect(page.getByTestId('sheet-scrollable-drag-scroll-y')).toContainText(
'ScrollView Y: 0'
)
// get the scrollview for dragging
const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
const scrollviewBox = await scrollview.boundingBox()
expect(scrollviewBox).toBeTruthy()
// drag DOWN on scrollview - should drag sheet, NOT scroll
await dragSheet(
page,
scrollviewBox!.x + scrollviewBox!.width / 2,
scrollviewBox!.y + scrollviewBox!.height / 3,
200,
{ steps: 25, stepDelay: 16 }
)
await page.waitForTimeout(600)
// EXPECTED: sheet moved to position 1, scroll stayed at 0
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 1'
)
// scroll should still be at 0 (or very close to it)
const scrollY = await page.getByTestId('sheet-scrollable-drag-scroll-y').textContent()
const scrollVal = parseInt(scrollY?.replace('ScrollView Y: ', '') || '0', 10)
expect(scrollVal).toBeLessThanOrEqual(5) // allow small tolerance
})
test('STRESS: multiple handoffs starting from scroll', async ({ page }) => {
// stress test: scroll → drag → scroll → drag (4x)
// each handoff should work cleanly without jitter
if (process.env.CI) test.slow() // triple timeout for CI runners
await page.getByTestId('sheet-scrollable-drag-trigger').click()
await page.waitForTimeout(800)
const frame = page.getByTestId('sheet-scrollable-drag-frame')
await expect(frame).toBeVisible({ timeout: 5000 })
const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
const scrollviewBox = await scrollview.boundingBox()
expect(scrollviewBox).toBeTruthy()
const cx = scrollviewBox!.x + scrollviewBox!.width / 2
const cy = scrollviewBox!.y + scrollviewBox!.height / 2
for (let i = 0; i < 4; i++) {
// 1. scroll down (drag up gesture)
await dragSheet(page, cx, cy, -150, { steps: 20, stepDelay: 12 })
await page.waitForTimeout(400)
// verify scroll happened, sheet still at 0
const scrollY1 = await page
.getByTestId('sheet-scrollable-drag-scroll-y')
.textContent()
const scrollVal1 = parseInt(scrollY1?.replace('ScrollView Y: ', '') || '0', 10)
expect(scrollVal1).toBeGreaterThan(20)
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 0'
)
// 2. drag down past scroll=0 to pull sheet
await dragSheet(page, cx, cy, 300, { steps: 30, stepDelay: 12 })
await page.waitForTimeout(400)
// verify sheet moved, scroll at 0
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 1'
)
await expect(page.getByTestId('sheet-scrollable-drag-scroll-y')).toContainText(
'ScrollView Y: 0'
)
// 3. drag up to bring sheet back (no scroll should happen here)
await dragSheet(page, cx, cy, -200, { steps: 25, stepDelay: 12 })
await page.waitForTimeout(400)
// verify sheet back at 0, scroll still 0
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 0'
)
const scrollY2 = await page
.getByTestId('sheet-scrollable-drag-scroll-y')
.textContent()
const scrollVal2 = parseInt(scrollY2?.replace('ScrollView Y: ', '') || '0', 10)
expect(scrollVal2).toBeLessThanOrEqual(10)
}
})
test('STRESS: multiple handoffs starting from drag', async ({ page }) => {
// stress test: drag → scroll → drag → scroll (4x)
// start by dragging sheet down first
await page.getByTestId('sheet-scrollable-drag-trigger').click()
await page.waitForTimeout(800)
const frame = page.getByTestId('sheet-scrollable-drag-frame')
await expect(frame).toBeVisible({ timeout: 5000 })
const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
const scrollviewBox = await scrollview.boundingBox()
expect(scrollviewBox).toBeTruthy()
const cx = scrollviewBox!.x + scrollviewBox!.width / 2
const cy = scrollviewBox!.y + scrollviewBox!.height / 2
for (let i = 0; i < 4; i++) {
// 1. drag down to pull sheet (scrollY=0, so pan takes over)
// use touch mode on scrollview - this should work via our touch handler
await dragSheet(page, cx, cy, 200, { steps: 25, stepDelay: 12 })
await page.waitForTimeout(400)
// verify sheet moved, scroll still 0
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 1'
)
await expect(page.getByTestId('sheet-scrollable-drag-scroll-y')).toContainText(
'ScrollView Y: 0'
)
// wait for animation to settle
await page.waitForTimeout(400)
// 2. drag up to bring sheet back - this should work via touch handler
// sheet not at top + dragging up = pan owns
await dragSheet(page, cx, cy, -250, { steps: 30, stepDelay: 12 })
await page.waitForTimeout(400)
// verify sheet at 0
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 0'
)
// scroll should still be 0 (no scroll during drag up when sheet not at top)
await expect(page.getByTestId('sheet-scrollable-drag-scroll-y')).toContainText(
'ScrollView Y: 0'
)
}
})
test('CRITICAL: single gesture - scroll, pull sheet, reverse to push sheet up - no scroll during push', async ({
page,
}) => {
// ONE CONTINUOUS GESTURE with direction changes:
// 1. drag up to scroll
// 2. reverse to drag down (scroll back to 0, then pull sheet)
// 3. reverse again to drag up (push sheet back up)
// BUG: step 3 was scrolling when it should only move sheet
await page.getByTestId('sheet-scrollable-drag-trigger').click()
await page.waitForTimeout(600)
const frame = page.getByTestId('sheet-scrollable-drag-frame')
await expect(frame).toBeVisible({ timeout: 5000 })
const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
const scrollviewBox = await scrollview.boundingBox()
expect(scrollviewBox).toBeTruthy()
const cx = scrollviewBox!.x + scrollviewBox!.width / 2
const startY = scrollviewBox!.y + scrollviewBox!.height / 2
// single continuous gesture with direction changes
await page.evaluate(
({ cx, startY }) => {
return new Promise<void>((resolve) => {
const target = document.elementFromPoint(cx, startY)
if (!target) {
resolve()
return
}
const dispatchTouch = (type: string, x: number, y: number) => {
const touch = new Touch({
identifier: 0,
target,
clientX: x,
clientY: y,
pageX: x,
pageY: y,
})
const touchEvent = new TouchEvent(type, {
bubbles: true,
cancelable: true,
touches: type === 'touchend' ? [] : [touch],
targetTouches: type === 'touchend' ? [] : [touch],
changedTouches: [touch],
})
target.dispatchEvent(touchEvent)
}
// touchstart
dispatchTouch('touchstart', cx, startY)
let step = 0
const moves = [
// cycle 1
// phase 1: drag up to scroll (20 steps, -5px each = -100px)
...Array(20).fill(-5),
// phase 2: drag down to scroll back and pull sheet (40 steps, +8px each = +320px)
...Array(40).fill(8),
// phase 3: drag up to push sheet back (30 steps, -6px each = -180px)
// THIS SHOULD NOT SCROLL
...Array(30).fill(-6),
// cycle 2 - repeat to catch bugs that only show on 2nd+ cycle
...Array(20).fill(-5),
...Array(40).fill(8),
...Array(30).fill(-6),
// cycle 3
...Array(20).fill(-5),
...Array(40).fill(8),
...Array(30).fill(-6),
// cycle 4
...Array(20).fill(-5),
...Array(40).fill(8),
...Array(30).fill(-6),
]
let currentY = startY
const interval = setInterval(() => {
if (step >= moves.length) {
clearInterval(interval)
dispatchTouch('touchend', cx, currentY)
resolve()
return
}
currentY += moves[step]
dispatchTouch('touchmove', cx, currentY)
step++
}, 16)
})
},
{ cx, startY }
)
await page.waitForTimeout(400)
// check maxScrollY - this captures if scroll happened at ANY point during "push up" phases
// if scroll happened during push up, maxScrollY will be > what it should be
const maxScrollY = await page
.getByTestId('sheet-scrollable-drag-max-scroll-y')
.textContent()
const maxScrollVal = parseInt(maxScrollY?.replace('Max scroll Y: ', '') || '0', 10)
console.log('maxScrollVal:', maxScrollVal)
// the key assertion: maxScrollY should be around 100 (from the initial scroll phase)
// if it's significantly higher, scroll happened during "push sheet up" phases
// each scroll phase does ~100px, so 4 cycles should still be ~100 max if working correctly
// if broken, we'd see 200+ because scroll happens during push-up phases too
expect(maxScrollVal).toBeLessThanOrEqual(150)
})
test('CRITICAL: trusted touch can drag to scroll to drag to scroll without lifting', async ({
page,
browserName,
}) => {
test.skip(browserName !== 'chromium', 'trusted touch dispatch uses Chromium CDP')
await page.getByTestId('sheet-scrollable-drag-trigger').click()
await page.waitForTimeout(600)
const frame = page.getByTestId('sheet-scrollable-drag-frame')
await expect(frame).toBeVisible({ timeout: 5000 })
const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
const scrollviewBox = await scrollview.boundingBox()
expect(scrollviewBox).toBeTruthy()
const point = {
x: scrollviewBox!.x + scrollviewBox!.width / 2,
y: scrollviewBox!.y + scrollviewBox!.height / 2,
}
const start = await readScrollableDragState(page)
const cdp = await page.context().newCDPSession(page)
await cdp.send('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints: [{ x: point.x, y: point.y }],
})
const afterDragDown = await trustedTouchMove(page, cdp, point, 180, 24)
const afterScrollUp = await trustedTouchMove(page, cdp, point, -420, 56)
const afterDragDownAgain = await trustedTouchMove(page, cdp, point, 380, 52)
const afterScrollUpAgain = await trustedTouchMove(page, cdp, point, -360, 48)
await cdp.send('Input.dispatchTouchEvent', {
type: 'touchEnd',
touchPoints: [],
})
await cdp.detach()
await page.waitForTimeout(400)
expect(afterDragDown.frameTop).toBeGreaterThan(start.frameTop + 100)
expect(afterDragDown.scrollTop).toBeLessThanOrEqual(5)
expect(afterScrollUp.frameTop).toBeLessThanOrEqual(start.frameTop + 20)
expect(afterScrollUp.scrollTop).toBeGreaterThan(100)
expect(afterDragDownAgain.frameTop).toBeGreaterThan(start.frameTop + 60)
expect(afterDragDownAgain.scrollTop).toBeLessThanOrEqual(5)
expect(afterScrollUpAgain.frameTop).toBeLessThanOrEqual(start.frameTop + 20)
expect(afterScrollUpAgain.scrollTop).toBeGreaterThan(80)
const unexpectedClose = await page
.getByTestId('sheet-scrollable-drag-unexpected-close')
.textContent()
expect(unexpectedClose).toContain('no')
})
test('CRITICAL: trusted touch can scroll to drag to scroll to reverse-scroll without lifting', async ({
page,
browserName,
}) => {
test.skip(browserName !== 'chromium', 'trusted touch dispatch uses Chromium CDP')
await page.getByTestId('sheet-scrollable-drag-trigger').click()
await page.waitForTimeout(600)
const frame = page.getByTestId('sheet-scrollable-drag-frame')
await expect(frame).toBeVisible({ timeout: 5000 })
const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
const scrollviewBox = await scrollview.boundingBox()
expect(scrollviewBox).toBeTruthy()
const point = {
x: scrollviewBox!.x + scrollviewBox!.width / 2,
y: scrollviewBox!.y + scrollviewBox!.height / 2,
}
const start = await readScrollableDragState(page)
const cdp = await page.context().newCDPSession(page)
await cdp.send('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints: [{ x: point.x, y: point.y }],
})
const afterInitialScroll = await trustedTouchMove(page, cdp, point, -300, 40)
const afterDragDown = await trustedTouchMove(page, cdp, point, 460, 60)
const afterScrollDownAgain = await trustedTouchMove(page, cdp, point, -420, 56)
const afterReverseScroll = await trustedTouchMove(page, cdp, point, 160, 24)
await cdp.send('Input.dispatchTouchEvent', {
type: 'touchEnd',
touchPoints: [],
})
await cdp.detach()
await page.waitForTimeout(400)
expect(afterInitialScroll.frameTop).toBeLessThanOrEqual(start.frameTop + 20)
expect(afterInitialScroll.scrollTop).toBeGreaterThan(120)
expect(afterDragDown.frameTop).toBeGreaterThan(start.frameTop + 100)
expect(afterDragDown.scrollTop).toBeLessThanOrEqual(5)
expect(afterScrollDownAgain.frameTop).toBeLessThanOrEqual(start.frameTop + 30)
expect(afterScrollDownAgain.scrollTop).toBeGreaterThan(100)
expect(afterReverseScroll.frameTop).toBeLessThanOrEqual(start.frameTop + 30)
expect(afterReverseScroll.scrollTop).toBeLessThan(afterScrollDownAgain.scrollTop - 80)
expect(afterReverseScroll.scrollTop).toBeGreaterThan(10)
const unexpectedClose = await page
.getByTestId('sheet-scrollable-drag-unexpected-close')
.textContent()
expect(unexpectedClose).toContain('no')
})
test('CRITICAL: from pos 1, single gesture drag up past top then reverse down - no mid-gesture snap', async ({
page,
}) => {
// BUG: sheet at position 1, one continuous gesture:
// 1. drag up to move sheet to top (pan owns, sheet moves up)
// 2. continue dragging up past top (pan→scroll handoff)
// 3. reverse to drag down (scroll→pan handoff)
//
// the bug: during the pan→scroll handoff in step 2, snapToPosition(0) is
// called MID-GESTURE. this triggers setPosition(0) → useEffect →
// setScrollEnabled → animateTo which fights with the active gesture,
// causing snap, broken state, and "Maximum update depth exceeded".
//
// snapToPosition should NEVER be called during a continuous gesture.
// position changes should only happen on release (touchend).
await page.getByTestId('sheet-scrollable-drag-trigger').click()
await page.waitForTimeout(600)
const frame = page.getByTestId('sheet-scrollable-drag-frame')
await expect(frame).toBeVisible({ timeout: 5000 })
// get sheet to position 1 via handle drag
const handle = page.getByTestId('sheet-scrollable-drag-handle')
let handleBox = await handle.boundingBox()
expect(handleBox).toBeTruthy()
await dragSheet(
page,
handleBox!.x + handleBox!.width / 2,
handleBox!.y + handleBox!.height / 2,
200,
{ steps: 25, stepDelay: 16, mode: 'trusted-touch' }
)
await page.waitForTimeout(600)
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 1'
)
await page.waitForTimeout(400)
const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
const scrollviewBox = await scrollview.boundingBox()
expect(scrollviewBox).toBeTruthy()
const cx = scrollviewBox!.x + scrollviewBox!.width / 2
const startY = scrollviewBox!.y + scrollviewBox!.height / 2
// listen for errors
const errors: string[] = []
page.on('pageerror', (err) => errors.push(err.message))
// set up a MutationObserver to track position changes DURING the gesture.
// the position text element updates when setPosition is called.
// any position change between touchstart and touchend is a mid-gesture snap (the bug).
await page.evaluate(() => {
const posEl = document.querySelector(
'[data-testid="sheet-scrollable-drag-position"]'
)
if (!posEl) return
;(window as any).__midGestureSnaps = []
;(window as any).__gestureActive = false
// track touch state
document.addEventListener(
'touchstart',
() => {
;(window as any).__gestureActive = true
},
{ capture: true }
)
document.addEventListener(
'touchend',
() => {
;(window as any).__gestureActive = false
},
{ capture: true }
)
// observe position text changes
const observer = new MutationObserver(() => {
if ((window as any).__gestureActive) {
const text = posEl.textContent || ''
;(window as any).__midGestureSnaps.push(text)
}
})
observer.observe(posEl, { childList: true, characterData: true, subtree: true })
})
// single continuous gesture: drag up past top, then reverse down
await page.evaluate(
({ cx, startY }) => {
return new Promise<void>((resolve) => {
const target = document.elementFromPoint(cx, startY)
if (!target) {
resolve()
return
}
const dispatchTouch = (type: string, x: number, y: number) => {
const touch = new Touch({
identifier: 0,
target,
clientX: x,
clientY: y,
pageX: x,
pageY: y,
})
target.dispatchEvent(
new TouchEvent(type, {
bubbles: true,
cancelable: true,
touches: type === 'touchend' ? [] : [touch],
targetTouches: type === 'touchend' ? [] : [touch],
changedTouches: [touch],
})
)
}
dispatchTouch('touchstart', cx, startY)
let step = 0
const moves = [
// phase 1: drag up - moves sheet from pos 1 to top (30 steps, -8px = -240px)
...Array(30).fill(-8),
// phase 2: continue up past top - should start scrolling (20 steps, -5px = -100px)
// THIS is where the pan→scroll handoff happens and snapToPosition(0) is called
...Array(20).fill(-5),
// phase 3: reverse down - scroll back then pull sheet (40 steps, +8px = +320px)
...Array(40).fill(8),
]
let currentY = startY
const interval = setInterval(() => {
if (step >= moves.length) {
clearInterval(interval)
dispatchTouch('touchend', cx, currentY)
resolve()
return
}
currentY += moves[step]
dispatchTouch('touchmove', cx, currentY)
step++
}, 16)
})
},
{ cx, startY }
)
await page.waitForTimeout(600)
// no Maximum update depth exceeded
expect(errors.filter((e) => e.includes('Maximum update depth'))).toHaveLength(0)
// CRITICAL: no position changes should happen during the gesture
// snapToPosition should only be called on release, not mid-gesture
const midGestureSnaps = await page.evaluate(
() => (window as any).__midGestureSnaps || []
)
console.log('mid-gesture snaps:', midGestureSnaps)
expect(midGestureSnaps).toHaveLength(0)
})
test('CRITICAL: from pos 1, drag up past top then back down - should snap to pos 1 not dismiss', async ({
page,
}) => {
// BUG: from pos 1, single continuous gesture:
// 1. drag up (pan moves sheet to top)
// 2. continue up past top (pan→scroll handoff, scroll owns)
// 3. reverse down past scroll=0 (scroll→pan handoff, pan owns)
// 4. release just below where pos 1 would be
//
// expected: sheet snaps to position 1
// actual bug: if touchend fires while owner='scroll', release() is never
// called. the sheet's animated position is stranded and it dismisses.
await page.getByTestId('sheet-scrollable-drag-trigger').click()
await page.waitForTimeout(600)
const frame = page.getByTestId('sheet-scrollable-drag-frame')
await expect(frame).toBeVisible({ timeout: 5000 })
// get sheet to position 1
const handle = page.getByTestId('sheet-scrollable-drag-handle')
let handleBox = await handle.boundingBox()
expect(handleBox).toBeTruthy()
await dragSheet(
page,
handleBox!.x + handleBox!.width / 2,
handleBox!.y + handleBox!.height / 2,
200,
{ steps: 25, stepDelay: 16, mode: 'trusted-touch' }
)
await page.waitForTimeout(600)
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 1'
)
await page.waitForTimeout(400)
const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
const scrollviewBox = await scrollview.boundingBox()
expect(scrollviewBox).toBeTruthy()
const cx = scrollviewBox!.x + scrollviewBox!.width / 2
const startY = scrollviewBox!.y + scrollviewBox!.height / 2
// single continuous gesture: drag up past top, scroll a bit, reverse down to ~pos 1
await page.evaluate(
({ cx, startY }) => {
return new Promise<void>((resolve) => {
const target = document.elementFromPoint(cx, startY)
if (!target) {
resolve()
return
}
const dispatchTouch = (type: string, x: number, y: number) => {
const touch = new Touch({
identifier: 0,
target,
clientX: x,
clientY: y,
pageX: x,
pageY: y,
})
target.dispatchEvent(
new TouchEvent(type, {
bubbles: true,
cancelable: true,
touches: type === 'touchend' ? [] : [touch],
targetTouches: type === 'touchend' ? [] : [touch],
changedTouches: [touch],
})
)
}
dispatchTouch('touchstart', cx, startY)
let step = 0
const moves = [
// phase 1: drag up - moves sheet from pos 1 to top
// pos 1 → pos 0 is ~300px, so need ~40 steps × -8px = -320px
...Array(40).fill(-8),
// phase 2: continue up past top - should start scrolling (20 steps, -5px = -100px)
...Array(20).fill(-5),
// phase 3: reverse down - scroll back to 0, then pull sheet down (50 steps, +8px = +400px)
// this should end near where pos 1 is
...Array(50).fill(8),
]
let currentY = startY
const interval = setInterval(() => {
if (step >= moves.length) {
clearInterval(interval)
dispatchTouch('touchend', cx, currentY)
resolve()
return
}
currentY += moves[step]
dispatchTouch('touchmove', cx, currentY)
step++
}, 16)
})
},
{ cx, startY }
)
await page.waitForTimeout(800)
// the bug: stale startY in release() causes it to compute a position way
// below the screen. with dismissOnSnapToBottom, this triggers setOpen(false)
// which dismisses the sheet unexpectedly.
const unexpectedClose = await page
.getByTestId('sheet-scrollable-drag-unexpected-close')
.textContent()
console.log('unexpected close:', unexpectedClose)
// the sheet should NOT have been dismissed during this gesture
expect(unexpectedClose).toContain('no')
})
test('Case 1b: scroll, drag down, drag UP - scroll should NOT happen during drag up', async ({
page,
}) => {
// This tests: scroll down, drag sheet down, then drag UP
// During drag UP (bringing sheet back), scroll should NOT change
await page.getByTestId('sheet-scrollable-drag-trigger').click()
await page.waitForTimeout(600)
const frame = page.getByTestId('sheet-scrollable-drag-frame')
await expect(frame).toBeVisible({ timeout: 5000 })
const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
const scrollviewBox = await scrollview.boundingBox()
expect(scrollviewBox).toBeTruthy()
// 1. scroll down
await page.mouse.move(
scrollviewBox!.x + scrollviewBox!.width / 2,
scrollviewBox!.y + scrollviewBox!.height / 2
)
await page.mouse.wheel(0, 150)
await page.waitForTimeout(300)
// 2. drag DOWN to pull sheet (this scrolls back to 0, then drags sheet)
await dragSheet(
page,
scrollviewBox!.x + scrollviewBox!.width / 2,
scrollviewBox!.y + scrollviewBox!.height / 3,
300,
{ steps: 30, stepDelay: 16 }
)
await page.waitForTimeout(400)
// sheet should be at position 1
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 1'
)
// scroll should be 0
await expect(page.getByTestId('sheet-scrollable-drag-scroll-y')).toContainText(
'ScrollView Y: 0'
)
// 3. NOW drag UP - this should bring sheet back to position 0
// scroll should NOT change during this drag
await dragSheet(
page,
scrollviewBox!.x + scrollviewBox!.width / 2,
scrollviewBox!.y + scrollviewBox!.height / 2,
-250,
{ steps: 30, stepDelay: 16 }
)
await page.waitForTimeout(300)
// sheet should be at position 0
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 0'
)
// CRITICAL: scroll should still be 0 - no scrolling during drag up
const scrollAfter = await page
.getByTestId('sheet-scrollable-drag-scroll-y')
.textContent()
const scrollVal = parseInt(scrollAfter?.replace('ScrollView Y: ', '') || '0', 10)
expect(scrollVal).toBeLessThanOrEqual(5) // allow tiny tolerance
})
test('Case 1c: scroll then drag DOWN - scroll should NOT change during drag', async ({
page,
}) => {
// This tests the basic jitter scenario:
// 1. Scroll content down (scrollY > 0)
// 2. Drag down to pull sheet
// 3. During the drag, scrollY should NOT change (no jitter)
await page.getByTestId('sheet-scrollable-drag-trigger').click()
await page.waitForTimeout(600)
const frame = page.getByTestId('sheet-scrollable-drag-frame')
await expect(frame).toBeVisible({ timeout: 5000 })
const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
const scrollviewBox = await scrollview.boundingBox()
expect(scrollviewBox).toBeTruthy()
// first scroll down with wheel
await page.mouse.move(
scrollviewBox!.x + scrollviewBox!.width / 2,
scrollviewBox!.y + scrollviewBox!.height / 2
)
await page.mouse.wheel(0, 150)
await page.waitForTimeout(300)
// verify we scrolled
const scrollBefore = await page
.getByTestId('sheet-scrollable-drag-scroll-y')
.textContent()
const scrollValBefore = parseInt(
scrollBefore?.replace('ScrollView Y: ', '') || '0',
10
)
expect(scrollValBefore).toBeGreaterThan(30)
// now do a long drag DOWN - should first scroll back to 0, then drag sheet
// but DURING the sheet drag portion, scroll should stay at 0
await dragSheet(
page,
scrollviewBox!.x + scrollviewBox!.width / 2,
scrollviewBox!.y + scrollviewBox!.height / 3,
400, // long drag to ensure we hit both scroll-back and sheet-drag phases
{ steps: 40, stepDelay: 16 }
)
await page.waitForTimeout(100)
// check final scroll position - should be 0 (scrolled back) not negative or jumping around
const scrollAfter = await page
.getByTestId('sheet-scrollable-drag-scroll-y')
.textContent()
const scrollValAfter = parseInt(scrollAfter?.replace('ScrollView Y: ', '') || '0', 10)
// scroll should be at 0 (not negative, not jumping around)
expect(scrollValAfter).toBe(0)
// sheet should have moved to position 1
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 1'
)
})
test('Case 2: at top snap, drag UP should scroll content', async ({ page }) => {
// open sheet
await page.getByTestId('sheet-scrollable-drag-trigger').click()
await page.waitForTimeout(600)
const frame = page.getByTestId('sheet-scrollable-drag-frame')
await expect(frame).toBeVisible({ timeout: 5000 })
// verify starting state - at top, scroll at 0
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 0'
)
// get the scrollview
const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
const scrollviewBox = await scrollview.boundingBox()
expect(scrollviewBox).toBeTruthy()
// use wheel event to scroll content (more reliable than drag for scroll)
await page.mouse.move(
scrollviewBox!.x + scrollviewBox!.width / 2,
scrollviewBox!.y + scrollviewBox!.height / 2
)
await page.mouse.wheel(0, 200)
await page.waitForTimeout(400)
// EXPECTED: sheet stays at position 0, content scrolled
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 0'
)
// scroll should have increased
const scrollY = await page.getByTestId('sheet-scrollable-drag-scroll-y').textContent()
const scrollVal = parseInt(scrollY?.replace('ScrollView Y: ', '') || '0', 10)
expect(scrollVal).toBeGreaterThan(50)
})
test('Case 3: drag sheet up from position 1 should NOT scroll simultaneously', async ({
page,
}) => {
// open sheet
await page.getByTestId('sheet-scrollable-drag-trigger').click()
await page.waitForTimeout(600)
const frame = page.getByTestId('sheet-scrollable-drag-frame')
await expect(frame).toBeVisible({ timeout: 5000 })
// get handle for dragging
const handle = page.getByTestId('sheet-scrollable-drag-handle')
let handleBox = await handle.boundingBox()
expect(handleBox).toBeTruthy()
// first drag down to position 1 using the mobile touch path
await dragSheet(
page,
handleBox!.x + handleBox!.width / 2,
handleBox!.y + handleBox!.height / 2,
200,
{ steps: 25, stepDelay: 16, mode: 'trusted-touch' }
)
await page.waitForTimeout(600)
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 1'
)
// verify scroll is still at 0 after first drag
await expect(page.getByTestId('sheet-scrollable-drag-scroll-y')).toContainText(
'ScrollView Y: 0'
)
// wait for animation to settle
await page.waitForTimeout(400)
// get new handle position
handleBox = await handle.boundingBox()
expect(handleBox).toBeTruthy()
// now drag UP back to position 0
await dragSheet(
page,
handleBox!.x + handleBox!.width / 2,
handleBox!.y + handleBox!.height / 2,
-250,
{ steps: 30, stepDelay: 16 }
)
await page.waitForTimeout(600)
// EXPECTED: sheet back at position 0, scroll should still be 0 (no simultaneous scroll)
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 0'
)
})
test('Case 4: scroll down, then drag down should scroll back first', async ({
page,
}) => {
// open sheet
await page.getByTestId('sheet-scrollable-drag-trigger').click()
await page.waitForTimeout(600)
const frame = page.getByTestId('sheet-scrollable-drag-frame')
await expect(frame).toBeVisible({ timeout: 5000 })
// get the scrollview
const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
const scrollviewBox = await scrollview.boundingBox()
expect(scrollviewBox).toBeTruthy()
// first scroll down (wheel up to scroll content down)
await page.mouse.move(
scrollviewBox!.x + scrollviewBox!.width / 2,
scrollviewBox!.y + scrollviewBox!.height / 2
)
await page.mouse.wheel(0, 200)
await page.waitForTimeout(400)
// verify we scrolled
const scrollBefore = await page
.getByTestId('sheet-scrollable-drag-scroll-y')
.textContent()
const scrollValBefore = parseInt(
scrollBefore?.replace('ScrollView Y: ', '') || '0',
10
)
expect(scrollValBefore).toBeGreaterThan(50)
// now drag DOWN - should scroll back first before dragging sheet
await dragSheet(
page,
scrollviewBox!.x + scrollviewBox!.width / 2,
scrollviewBox!.y + scrollviewBox!.height / 3,
300,
{ steps: 30, stepDelay: 16 }
)
await page.waitForTimeout(600)
// either scroll absorbed the swipe (scroll at 0 or close) OR sheet moved
const scrollAfter = await page
.getByTestId('sheet-scrollable-drag-scroll-y')
.textContent()
const scrollValAfter = parseInt(scrollAfter?.replace('ScrollView Y: ', '') || '0', 10)
const posAfter = await page
.getByTestId('sheet-scrollable-drag-position')
.textContent()
const posValAfter = parseInt(posAfter?.replace('Sheet position: ', '') || '0', 10)
// success if either: scroll went back to 0, or sheet moved to position 1
expect(scrollValAfter < scrollValBefore || posValAfter === 1).toBeTruthy()
})
test('Case 5: HANDOFF - scroll to 0 then drag sheet in one gesture', async ({
page,
}) => {
// open sheet
await page.getByTestId('sheet-scrollable-drag-trigger').click()
await page.waitForTimeout(600)
const frame = page.getByTestId('sheet-scrollable-drag-frame')
await expect(frame).toBeVisible({ timeout: 5000 })
// get the scrollview
const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
const scrollviewBox = await scrollview.boundingBox()
expect(scrollviewBox).toBeTruthy()
// first scroll down a bit
await page.mouse.move(
scrollviewBox!.x + scrollviewBox!.width / 2,
scrollviewBox!.y + scrollviewBox!.height / 2
)
await page.mouse.wheel(0, 100)
await page.waitForTimeout(300)
// verify we scrolled
const scrollBefore = await page
.getByTestId('sheet-scrollable-drag-scroll-y')
.textContent()
const scrollValBefore = parseInt(
scrollBefore?.replace('ScrollView Y: ', '') || '0',
10
)
expect(scrollValBefore).toBeGreaterThan(0)
// long drag down - should scroll to 0 then drag sheet
await dragSheet(
page,
scrollviewBox!.x + scrollviewBox!.width / 2,
scrollviewBox!.y + scrollviewBox!.height / 4,
350,
{ steps: 40, stepDelay: 16 }
)
await page.waitForTimeout(600)
// EXPECTED: scroll should be at 0, sheet should have moved to position 1
await expect(page.getByTestId('sheet-scrollable-drag-scroll-y')).toContainText(
'ScrollView Y: 0'
)
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 1'
)
})
test('Case 6: multiple direction changes without getting stuck', async ({ page }) => {
// open sheet
await page.getByTestId('sheet-scrollable-drag-trigger').click()
await page.waitForTimeout(600)
const frame = page.getByTestId('sheet-scrollable-drag-frame')
await expect(frame).toBeVisible({ timeout: 5000 })
// verify starting state
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 0'
)
const handle = page.getByTestId('sheet-scrollable-drag-handle')
let handleBox = await handle.boundingBox()
expect(handleBox).toBeTruthy()
// 1. drag down to position 1
await dragSheet(
page,
handleBox!.x + handleBox!.width / 2,
handleBox!.y + handleBox!.height / 2,
200,
{ steps: 25, stepDelay: 16 }
)
await page.waitForTimeout(600)
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 1'
)
// 2. drag up back to position 0
handleBox = await handle.boundingBox()
expect(handleBox).toBeTruthy()
await dragSheet(
page,
handleBox!.x + handleBox!.width / 2,
handleBox!.y + handleBox!.height / 2,
-250,
{ steps: 30, stepDelay: 16 }
)
await page.waitForTimeout(600)
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 0'
)
// 3. now scroll up (content should scroll since at top snap)
const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
const scrollviewBox = await scrollview.boundingBox()
expect(scrollviewBox).toBeTruthy()
await page.mouse.move(
scrollviewBox!.x + scrollviewBox!.width / 2,
scrollviewBox!.y + scrollviewBox!.height / 2
)
await page.mouse.wheel(0, 150)
await page.waitForTimeout(400)
const scrollAfter = await page
.getByTestId('sheet-scrollable-drag-scroll-y')
.textContent()
const scrollVal = parseInt(scrollAfter?.replace('ScrollView Y: ', '') || '0', 10)
expect(scrollVal).toBeGreaterThan(30)
// 4. drag down - should scroll back first, then drag sheet
await dragSheet(
page,
scrollviewBox!.x + scrollviewBox!.width / 2,
scrollviewBox!.y + scrollviewBox!.height / 3,
350,
{ steps: 40, stepDelay: 16 }
)
await page.waitForTimeout(600)
// should have scrolled to 0
await expect(page.getByTestId('sheet-scrollable-drag-scroll-y')).toContainText(
'ScrollView Y: 0'
)
})
test('Case 7: drag UP from position 1 - scroll locked during drag, final position 0', async ({
page,
}) => {
// open sheet
await page.getByTestId('sheet-scrollable-drag-trigger').click()
await page.waitForTimeout(600)
const frame = page.getByTestId('sheet-scrollable-drag-frame')
await expect(frame).toBeVisible({ timeout: 5000 })
const handle = page.getByTestId('sheet-scrollable-drag-handle')
let handleBox = await handle.boundingBox()
expect(handleBox).toBeTruthy()
// first drag down to position 1 using the mobile touch path
await dragSheet(
page,
handleBox!.x + handleBox!.width / 2,
handleBox!.y + handleBox!.height / 2,
200,
{ steps: 25, stepDelay: 16, mode: 'trusted-touch' }
)
await page.waitForTimeout(600)
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 1'
)
// verify scroll is at 0
await expect(page.getByTestId('sheet-scrollable-drag-scroll-y')).toContainText(
'ScrollView Y: 0'
)
await page.waitForTimeout(400)
// drag UP via scrollview area - sheet moves to position 0
const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
const scrollviewBox = await scrollview.boundingBox()
expect(scrollviewBox).toBeTruthy()
await dragSheet(
page,
scrollviewBox!.x + scrollviewBox!.width / 2,
scrollviewBox!.y + scrollviewBox!.height / 2,
-250,
{ steps: 30, stepDelay: 16 }
)
await page.waitForTimeout(600)
// sheet should be at position 0
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 0'
)
// scroll should still be 0 (locked during drag)
const scrollY = await page.getByTestId('sheet-scrollable-drag-scroll-y').textContent()
const scrollVal = parseInt(scrollY?.replace('ScrollView Y: ', '') || '0', 10)
expect(scrollVal).toBeLessThanOrEqual(5)
})
test('Case 8: rubber band at top - dragging up keeps sheet at top', async ({
page,
}) => {
// open sheet
await page.getByTestId('sheet-scrollable-drag-trigger').click()
await page.waitForTimeout(600)
const frame = page.getByTestId('sheet-scrollable-drag-frame')
await expect(frame).toBeVisible({ timeout: 5000 })
// verify at position 0 (top snap)
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 0'
)
await expect(page.getByTestId('sheet-scrollable-drag-scroll-y')).toContainText(
'ScrollView Y: 0'
)
// drag UP on handle - should NOT scroll content, should show resistance
const handle = page.getByTestId('sheet-scrollable-drag-handle')
const handleBox = await handle.boundingBox()
expect(handleBox).toBeTruthy()
// use the mobile touch path for the handle
await dragSheet(
page,
handleBox!.x + handleBox!.width / 2,
handleBox!.y + handleBox!.height / 2,
-150,
{ steps: 20, stepDelay: 16, mode: 'trusted-touch' }
)
await page.waitForTimeout(600)
// sheet should still be at position 0 after rubber band release
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 0'
)
// frame should still be visible (sheet didn't disappear)
await expect(frame).toBeVisible()
// scroll should NOT have happened during drag
const maxScrollY = await page
.getByTestId('sheet-scrollable-drag-max-scroll-y')
.textContent()
const maxScrollVal = parseInt(maxScrollY?.replace('Max scroll Y: ', '') || '0', 10)
expect(maxScrollVal).toBeLessThanOrEqual(5)
})
test('Case 9: HANDOFF UP - drag UP from position 1, continue into scroll', async ({
page,
}) => {
// open sheet
await page.getByTestId('sheet-scrollable-drag-trigger').click()
await page.waitForTimeout(600)
const frame = page.getByTestId('sheet-scrollable-drag-frame')
await expect(frame).toBeVisible({ timeout: 5000 })
const handle = page.getByTestId('sheet-scrollable-drag-handle')
let handleBox = await handle.boundingBox()
expect(handleBox).toBeTruthy()
// first drag down to position 1 using the mobile touch path
await dragSheet(
page,
handleBox!.x + handleBox!.width / 2,
handleBox!.y + handleBox!.height / 2,
200,
{ steps: 25, stepDelay: 16, mode: 'trusted-touch' }
)
await page.waitForTimeout(600)
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 1'
)
await expect(page.getByTestId('sheet-scrollable-drag-scroll-y')).toContainText(
'ScrollView Y: 0'
)
await page.waitForTimeout(400)
// CRITICAL: one LONG drag UP - should:
// 1. Move sheet from position 1 to position 0
// 2. Continue into scrolling without lifting finger
const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
const scrollviewBox = await scrollview.boundingBox()
expect(scrollviewBox).toBeTruthy()
await dragSheet(
page,
scrollviewBox!.x + scrollviewBox!.width / 2,
scrollviewBox!.y + scrollviewBox!.height / 2,
-400,
{ steps: 50, stepDelay: 12 } // longer, faster drag for handoff
)
await page.waitForTimeout(300)
// sheet should be at position 0
await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
'Sheet position: 0'
)
// AND scroll should have happened (handoff successful)
// check maxScrollY instead of current scrollY because scroll may bounce back
const maxScrollY = await page
.getByTestId('sheet-scrollable-drag-max-scroll-y')
.textContent()
const maxScrollVal = parseInt(maxScrollY?.replace('Max scroll Y: ', '') || '0', 10)
expect(maxScrollVal).toBeGreaterThan(30) // handoff occurred
})
})