Files
apes-sidebar/tests/tab-bar-middle-click.spec.tsx
T
Menghuan1918 223b2545d9 refactor(shell): retire the plugin's own right panel and free windows
DSH 0.1.5 owns the right column (its native Sidebar hosts every tab type
this plugin registers), so the plugin's own right panel, its width drag /
new-session seed prefs, and the free-window feature are gone. The bottom
workbench is the only plugin-owned surface left, and its expand/collapse
control is registered into DSH's session-header utilities list slot (the
header's corner seat belongs to the native sidebar's own control).

- state: one `bottomSplits` tree; `makeDefaultState()` takes no arguments;
  `toggleBottomPanel` / `openTabInBottomPane` are the only landing
  reducers; `setBottomHeight` and `sanitizeState` share the
  `CONVERSATION_MIN` cap (a persisted tall height no longer snaps on the
  first drag).
- service: every landing in the workbench (creation or focus) expands it;
  `SIDEBAR_FEATURES` drops `'floatWindows'`; the open/close/activate tab
  pools cover the single tree.
- shell: `Sidebar.tsx` renders only the bottom workbench; the toggle lives
  in `sidebar/bottom-toggle.tsx`; layout-push pushes height only (no width,
  no `--dsh-sidebar-width`).
- prefs: `openByDefault` / `defaultWidthPercent` / `changesDiffFloat`
  removed from the schema, defaults, client parser, settings page, and all
  20 dictionaries; the retired keys in stored documents are ignored.
- tests: free-window specs deleted, panel/float assertions dropped,
  remaining suites adapted (124 files / 1287 tests); the mount lane asserts
  the header toggle and sweeps the native sidebar's guide page.
- docs: README / README_EN / guide §0,§11 / AGENTS test map.
2026-09-09 15:18:06 +08:00

193 lines
6.0 KiB
TypeScript

/**
* Tab-strip middle-click close tests. The press target is recorded on middle
* mousedown (preventDefaulted to disarm Chrome's middle-click autoscroll)
* and the close settles on the first middle mouseup OVER that same tab —
* release-position semantics matching VS Code (microsoft/vscode#101028) and
* user expectations for Chrome tabs (crbug/40679924): pressing on a tab and
* releasing elsewhere cancels. The browser dispatches auxclick to the common
* ancestor of the press/release targets when they differ (or suppresses it
* entirely), so settling on the recorded press target keeps release
* semantics without depending on auxclick delivery. Left-button paths
* (activate / drag) and wheel scrolling are untouched.
*/
// @vitest-environment jsdom
import { afterEach, describe, expect, it, vi } 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).
import { setupReactAct } from './test-utils.ts'
setupReactAct()
import { TabBar } from '../src/client/TabBar.tsx'
import type { SidebarTab } from '../src/client/state.ts'
function mountBar(): {
tab0: HTMLElement
tab1: HTMLElement
onClose: ReturnType<typeof vi.fn>
onActivate: ReturnType<typeof vi.fn>
unmount: () => void
} {
const container = document.createElement('div')
document.body.append(container)
const onClose = vi.fn()
const onActivate = vi.fn()
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: [],
onDropTab: () => {},
}))
})
const tabEls = [...container.querySelectorAll('[class*="tabList"] > [class*="tab"]')] as HTMLElement[]
expect(tabEls.length).toBe(2)
// noUncheckedIndexedAccess: the length assertion above is the guard, but
// the compiler still sees `HTMLElement | undefined` through indexing.
const tab0 = tabEls[0]!
const tab1 = tabEls[1]!
return {
tab0,
tab1,
onClose,
onActivate,
unmount: () => {
act(() => { root.unmount() })
container.remove()
},
}
}
/** Dispatch a native mouse event; returns the event (to read defaultPrevented). */
function mouse(target: EventTarget, type: string, button: number): MouseEvent {
const event = new MouseEvent(type, { bubbles: true, cancelable: true, button })
target.dispatchEvent(event)
return event
}
afterEach(() => {
document.body.innerHTML = ''
vi.restoreAllMocks()
})
describe('TabBar middle-click close', () => {
it('closes when the middle mouseup lands on the same tab', () => {
const { tab0, onClose, unmount } = mountBar()
try {
mouse(tab0, 'mousedown', 1)
mouse(tab0, 'mouseup', 1)
expect(onClose).toHaveBeenCalledWith('t1')
} finally {
unmount()
}
})
it('preventDefaults the middle mousedown (disarms browser autoscroll)', () => {
const { tab0, unmount } = mountBar()
try {
const event = mouse(tab0, 'mousedown', 1)
expect(event.defaultPrevented).toBe(true)
} finally {
unmount()
}
})
it('a release elsewhere (drag-away) cancels the close — one-shot', () => {
const { tab0, onClose, unmount } = mountBar()
try {
// Press on tab 0, release on unrelated element: no close.
mouse(tab0, 'mousedown', 1)
mouse(document.body, 'mouseup', 1)
expect(onClose).not.toHaveBeenCalled()
// A later middle mouseup over the tab (no new press) must NOT close.
mouse(tab0, 'mouseup', 1)
expect(onClose).not.toHaveBeenCalled()
} finally {
unmount()
}
})
it('a release over a DIFFERENT tab does not close', () => {
const { tab0, tab1, onClose, unmount } = mountBar()
try {
mouse(tab0, 'mousedown', 1)
mouse(tab1, 'mouseup', 1)
expect(onClose).not.toHaveBeenCalled()
} finally {
unmount()
}
})
it('middle mousedown on the close button closes (release over the tab)', () => {
const { tab0, onClose, unmount } = mountBar()
try {
const button = tab0.querySelector('button') as HTMLButtonElement
expect(button).not.toBeNull()
mouse(button, 'mousedown', 1)
mouse(button, 'mouseup', 1)
expect(onClose).toHaveBeenCalledWith('t1')
} finally {
unmount()
}
})
it('left mousedown does not close; left click activates', () => {
const { tab0, onClose, onActivate, unmount } = mountBar()
try {
mouse(tab0, 'mousedown', 0)
mouse(tab0, 'mouseup', 0)
expect(onClose).not.toHaveBeenCalled()
mouse(tab0, 'click', 0)
expect(onActivate).toHaveBeenCalledWith('t1')
} finally {
unmount()
}
})
it('a middle auxclick alone (no mousedown) does not close', () => {
// Pins the contract: the close settles on the mouseup of the recorded
// press, not on auxclick delivery — the browser redirects auxclick to
// the common ancestor (or suppresses it) when press/release targets
// differ, which was the failure mode.
const { tab0, onClose, unmount } = mountBar()
try {
mouse(tab0, 'auxclick', 1)
expect(onClose).not.toHaveBeenCalled()
} finally {
unmount()
}
})
it('a left mouseup after a middle press does not consume the press', () => {
const { tab0, onClose, unmount } = mountBar()
try {
mouse(tab0, 'mousedown', 1)
mouse(window, 'mouseup', 0)
expect(onClose).not.toHaveBeenCalled()
mouse(tab0, 'mouseup', 1)
expect(onClose).toHaveBeenCalledWith('t1')
} finally {
unmount()
}
})
it('stops closing after unmount', () => {
const { tab0, onClose, unmount } = mountBar()
mouse(tab0, 'mousedown', 1)
unmount()
mouse(tab0, 'mouseup', 1)
expect(onClose).not.toHaveBeenCalled()
})
})