Files
apes-sidebar/tests/tab-bar-wheel.spec.tsx
T
Menghuan1918 f17a1b30ac feat(ui): 自由窗口——tab 拖出侧边栏成为可移动/缩放/置顶的浮动窗口 (v0.16.0)
- 拖出:标签栏 tab 拖到主会话列(HTML5 DnD 检测 + 虚线提示浮层)或右键菜单
  「移动到自由窗口」(右/底面板共用 TabBar),tab 从 pane 移动进 SidebarState.floats
- 窗口(FreeWindow,渲染于 [data-dsh-panel-host] z-42):头部 pointer 拖动
  (rAF 直写 + 松手提交)、拖到 pane 高亮停靠(center 合并)、SE 角缩放、
  点击置顶、头部右键(回到侧边栏/关闭)、X = closeTab 走正常生命周期
- 服务语义:openTab dedupe/id 聚焦浮动 tab = 置顶窗口(不展开面板);
  closeTab/activateTab 兼顾 floats;agent 终端 reconcile 覆盖浮窗;
  features 单调新增 'floatWindows',版本 0.16.0
- 持久化:floats 随会话 localStorage,sanitizeState 宽容校验 + 几何钳制,
  uid 播种覆盖 float 前缀;i18n 三键 19 语言同步
- 关键修复(e2e 发现):portal 覆盖层(头部 Menu)的合成事件沿 React 树
  冒泡回拖拽表面,劫持菜单行点击——拖拽起点加 DOM 同源守卫(jsdom 不复现)
- e2e-mount.sh:缺省 tarball 改取 mtime 最新(旧字典序选择会把冒烟挂到
  过期产物);新增 tests/e2e/float-window.e2e.ts 双用例
2026-08-23 23:44:18 +08:00

154 lines
4.8 KiB
TypeScript

/**
* Tab-strip wheel-scroll tests: a plain mouse wheel over the tab row emits
* deltaY, which a horizontal scrollport (`overflow-x: auto`) never consumes —
* the handler in TabBar translates it into horizontal scrolling. It must
* consume the event ONLY when the strip actually overflows (else the page
* scrolls normally), must leave modifier-key gestures to the browser (shift
* = native horizontal scroll, ctrl/cmd = zoom), and must scale deltaMode
* lines/pages like the browser would.
*/
// @vitest-environment jsdom
import { afterEach, describe, expect, it } from 'vitest'
import { createElement } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { act } from 'react-dom/test-utils'
// The act() environment flag (React 18.2 reads it before flushing effects).
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
import { TabBar } from '../src/client/TabBar.tsx'
import type { SidebarTab } from '../src/client/state.ts'
function mountBar(overflow: boolean): { list: HTMLElement; unmount: () => void } {
const container = document.createElement('div')
document.body.append(container)
const tabs: SidebarTab[] = [
{ id: 't1', type: 'explorer', title: 'Explorer' },
{ id: 't2', type: 'git', title: 'Git' },
]
const root: Root = createRoot(container)
act(() => {
root.render(createElement(TabBar, {
paneId: 'pane:1',
tabs,
active: 't1',
onActivate: () => {},
onClose: () => {},
onNewTab: () => {},
newTabOptions: [],
onFloatTab: () => {},
onDropTab: () => {},
}))
})
const list = container.querySelector('[class*="tabList"]') as HTMLElement
expect(list).not.toBeNull()
// jsdom has no layout: stub the scrollport geometry to simulate overflow.
Object.defineProperty(list, 'scrollWidth', { value: overflow ? 600 : 200, configurable: true })
Object.defineProperty(list, 'clientWidth', { value: 200, configurable: true })
return {
list,
unmount: () => {
act(() => { root.unmount() })
container.remove()
},
}
}
/** Dispatch a wheel event on the strip and return whether it was consumed. */
function wheel(list: HTMLElement, init: WheelEventInit): WheelEvent {
const event = new WheelEvent('wheel', { cancelable: true, bubbles: true, ...init })
list.dispatchEvent(event)
return event
}
afterEach(() => {
document.body.innerHTML = ''
})
describe('TabBar wheel → horizontal scroll', () => {
it('translates a vertical wheel delta into scrollLeft when the strip overflows', () => {
const { list, unmount } = mountBar(true)
try {
list.scrollLeft = 0
const event = wheel(list, { deltaY: 120 })
expect(event.defaultPrevented).toBe(true)
expect(list.scrollLeft).toBe(120)
} finally {
unmount()
}
})
it('scans left for a negative delta', () => {
const { list, unmount } = mountBar(true)
try {
list.scrollLeft = 500
const event = wheel(list, { deltaY: -80 })
expect(event.defaultPrevented).toBe(true)
expect(list.scrollLeft).toBe(420)
} finally {
unmount()
}
})
it('does not consume the event when the strip does not overflow', () => {
const { list, unmount } = mountBar(false)
try {
list.scrollLeft = 0
const event = wheel(list, { deltaY: 120 })
expect(event.defaultPrevented).toBe(false)
expect(list.scrollLeft).toBe(0)
} finally {
unmount()
}
})
it('leaves modifier-key gestures to the browser', () => {
const { list, unmount } = mountBar(true)
try {
list.scrollLeft = 0
const ctrl = wheel(list, { deltaY: 120, ctrlKey: true })
const shift = wheel(list, { deltaY: 120, shiftKey: true })
const meta = wheel(list, { deltaY: 120, metaKey: true })
expect(ctrl.defaultPrevented).toBe(false)
expect(shift.defaultPrevented).toBe(false)
expect(meta.defaultPrevented).toBe(false)
expect(list.scrollLeft).toBe(0)
} finally {
unmount()
}
})
it('scales line-mode deltas by 16px per line', () => {
const { list, unmount } = mountBar(true)
try {
list.scrollLeft = 0
const event = wheel(list, { deltaY: 3, deltaMode: 1 })
expect(event.defaultPrevented).toBe(true)
expect(list.scrollLeft).toBe(48)
} finally {
unmount()
}
})
it('adds a horizontal trackpad delta to the scroll', () => {
const { list, unmount } = mountBar(true)
try {
list.scrollLeft = 0
const event = wheel(list, { deltaX: 40 })
expect(event.defaultPrevented).toBe(true)
expect(list.scrollLeft).toBe(40)
} finally {
unmount()
}
})
it('stops scrolling after unmount', () => {
const { list, unmount } = mountBar(true)
list.scrollLeft = 0
unmount()
const event = wheel(list, { deltaY: 120 })
expect(event.defaultPrevented).toBe(false)
expect(list.scrollLeft).toBe(0)
})
})