diff --git a/tests/e2e/artificial-opencode-scroll-scenario.ts b/tests/e2e/artificial-opencode-scroll-scenario.ts index f98e89e5994..ad1e3576228 100644 --- a/tests/e2e/artificial-opencode-scroll-scenario.ts +++ b/tests/e2e/artificial-opencode-scroll-scenario.ts @@ -82,12 +82,17 @@ export async function measureActiveTerminalWheelScroll(page: Page): Promise('.xterm-screen') - if (!screen) { - throw new Error('Active terminal screen is unavailable') + // Why: Linux headless can miss wheel input over xterm's text layer while + // output is flooding; the viewport is the scrollable surface users affect. + const wheelTarget = + pane.container.querySelector('.xterm-viewport') ?? + pane.container.querySelector('.xterm') ?? + pane.container.querySelector('.xterm-screen') + if (!wheelTarget) { + throw new Error('Active terminal wheel target is unavailable') } const buffer = pane.terminal.buffer.active - const rect = screen.getBoundingClientRect() + const rect = wheelTarget.getBoundingClientRect() return { baseY: buffer.baseY, beforeViewportY: buffer.viewportY, @@ -119,6 +124,16 @@ export async function measureActiveTerminalWheelScroll(page: Page): Promise= target.beforeViewportY) { + await dispatchActiveTerminalWheelEvent(page) + } while (performance.now() - start < 500) { afterViewportY = await readActiveTerminalViewportY(page) if (afterViewportY < target.beforeViewportY) { @@ -164,6 +179,45 @@ export function annotateScrollMeasurement( }) } +async function dispatchActiveTerminalWheelEvent(page: Page): Promise { + await page.evaluate(() => { + const store = window.__store + const state = store?.getState() + const worktreeId = state?.activeWorktreeId + const tabId = + state?.activeTabType === 'terminal' + ? state.activeTabId + : worktreeId + ? (state?.activeTabIdByWorktree?.[worktreeId] ?? null) + : null + const manager = tabId ? window.__paneManagers?.get(tabId) : null + const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null + if (!pane) { + throw new Error('Active terminal pane is unavailable') + } + // Why: CI can drop CDP wheel input while the active textarea is focused; + // dispatching on xterm's own surfaces still exercises its user scroll path. + const wheelTargets = [ + pane.container.querySelector('.xterm'), + pane.container.querySelector('.xterm-viewport'), + pane.container.querySelector('.xterm-screen') + ].filter((target): target is HTMLElement => Boolean(target)) + if (wheelTargets.length === 0) { + throw new Error('Active terminal wheel target is unavailable') + } + for (const wheelTarget of wheelTargets) { + wheelTarget.dispatchEvent( + new WheelEvent('wheel', { + bubbles: true, + cancelable: true, + deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaY: -1200 + }) + ) + } + }) +} + async function readActiveTerminalViewportY(page: Page): Promise { return page.evaluate(() => { const store = window.__store