mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Review findings from the first round: - ui-settings renders its full-viewport panel as a fixed-position DESCENDANT of the sidebar column, so `pointerleave` never fires when the pointer moves onto it and the bars stayed drawn after it closed. Leaving is now decided against the column's box from a document-level pointermove that exists only while the bars are drawn; the element's own leave stays for the pointer that leaves the window. - The rebind gate judges the rule rather than each declaration, so a pair that mixes `transparent` with an l2 hover no longer passes, and the elevation half compares whole values instead of token shape. - Hiding no longer exempts a sheet from the elevated-surface rebind check. - The e2e polls the reveal before reading a colour for the golden, and pins that a pointerless scroll draws no thumb.
158 lines
6.5 KiB
TypeScript
158 lines
6.5 KiB
TypeScript
// @vitest-environment jsdom
|
|
/**
|
|
* Pointer-revealed scrollbars, the shell's half: which class state the column
|
|
* carries as the pointer crosses it. The stylesheet rule that state drives is
|
|
* asserted in scrollbar-quiet-styles.spec.ts (node environment — a jsdom spec
|
|
* has no file: module URL to read the sheet through).
|
|
*/
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { act, cleanup, fireEvent, render } from '@testing-library/react'
|
|
import type { SidebarRootComponentProps, SidebarSectionOwnerProps } from '../src/client/contract/slots.ts'
|
|
import { SidebarRoot } from '../src/client/SidebarRoot.tsx'
|
|
import { en } from '../src/client/locales.ts'
|
|
|
|
/** Pinned column box; the shell compares pointer coordinates against it. */
|
|
const COLUMN_WIDTH = 280
|
|
const COLUMN_HEIGHT = 600
|
|
|
|
const t: SidebarRootComponentProps['t'] = key => (en as Record<string, string>)[key] ?? key
|
|
/** The shell never reads the global hooks; the props share carries them regardless. */
|
|
const neverHook = (() => { throw new Error('shell must not read global hooks') }) as never
|
|
|
|
afterEach(() => {
|
|
cleanup()
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
/**
|
|
* Render the shell and expose its column element.
|
|
* @returns the column element and whether it currently carries the quiet state.
|
|
*/
|
|
function mountColumn(): { column: HTMLElement; quiet: () => boolean } {
|
|
const view = render(
|
|
<SidebarRoot
|
|
collapsed={false} width={300}
|
|
useSessions={neverHook} useWorkspaces={neverHook}
|
|
startSession={vi.fn()} toggleSidebar={vi.fn()} t={t}
|
|
renderSlot={((_key: string, owner: SidebarSectionOwnerProps) =>
|
|
<div data-testid="region" data-wide={owner.wide} />) as SidebarRootComponentProps['renderSlot']}
|
|
/>,
|
|
)
|
|
const column = view.container.firstElementChild
|
|
if (!(column instanceof HTMLElement)) throw new Error('sidebar column not rendered')
|
|
// jsdom lays nothing out, and the leave decision is geometric: pin the box
|
|
// the shell reads so a coordinate can be inside or outside it.
|
|
Object.defineProperty(column, 'getBoundingClientRect', {
|
|
value: () => ({
|
|
left: 0, top: 0, right: COLUMN_WIDTH, bottom: COLUMN_HEIGHT,
|
|
x: 0, y: 0, width: COLUMN_WIDTH, height: COLUMN_HEIGHT, toJSON: () => ({}),
|
|
}),
|
|
})
|
|
// CSS-module locals are hashed in this bench, so the state is read as a
|
|
// substring of the class list rather than as an exact local name.
|
|
return { column, quiet: () => [...column.classList].some(name => name.includes('quietBars')) }
|
|
}
|
|
|
|
/**
|
|
* Cross the pointer into or out of the column. React synthesizes
|
|
* `pointerenter`/`pointerleave` from `pointerover`/`pointerout`, so the raw
|
|
* enter and leave events it does not listen to would assert nothing.
|
|
* @param column - the sidebar column element.
|
|
* @param direction - `in` to enter the column, `out` to leave it.
|
|
*/
|
|
function movePointer(column: HTMLElement, direction: 'in' | 'out'): void {
|
|
const outside = document.body
|
|
if (direction === 'in') fireEvent.pointerOver(column, { relatedTarget: outside })
|
|
else fireEvent.pointerOut(column, { relatedTarget: outside })
|
|
}
|
|
|
|
/**
|
|
* Move the pointer over the document, as a pointer crossing a fixed overlay
|
|
* that is a DOM descendant of the column does.
|
|
* @param x - client x coordinate.
|
|
* @param y - client y coordinate.
|
|
*/
|
|
function movePointerOverDocument(x: number, y: number): void {
|
|
fireEvent.pointerMove(document, { clientX: x, clientY: y })
|
|
}
|
|
|
|
describe('SidebarRoot pointer-revealed scrollbars', () => {
|
|
it('draws them only while the pointer is inside, and lingers on the way out', () => {
|
|
vi.useFakeTimers()
|
|
const { column, quiet } = mountColumn()
|
|
// At rest — the pointer has never been over the column — the bars are off.
|
|
expect(quiet()).toBe(true)
|
|
movePointer(column, 'in')
|
|
expect(quiet()).toBe(false)
|
|
movePointer(column, 'out')
|
|
// The linger: still drawn just before the window closes, gone just after.
|
|
act(() => { vi.advanceTimersByTime(1999) })
|
|
expect(quiet()).toBe(false)
|
|
act(() => { vi.advanceTimersByTime(1) })
|
|
expect(quiet()).toBe(true)
|
|
})
|
|
|
|
it('cancels a pending hide when the pointer comes back', () => {
|
|
vi.useFakeTimers()
|
|
const { column, quiet } = mountColumn()
|
|
movePointer(column, 'in')
|
|
movePointer(column, 'out')
|
|
act(() => { vi.advanceTimersByTime(1000) })
|
|
movePointer(column, 'in')
|
|
// The first leave's timer would fire here; a cancelled one leaves the bars
|
|
// drawn, which is what keeps a pointer skirting the edge from blinking them.
|
|
act(() => { vi.advanceTimersByTime(5000) })
|
|
expect(quiet()).toBe(false)
|
|
})
|
|
|
|
it('hides when the pointer moves outside the column box without leaving its subtree', () => {
|
|
// ui-settings renders its full-viewport panel as a fixed-position
|
|
// DESCENDANT of the column, so DOM containment reports the pointer as
|
|
// still inside while it is visually somewhere else entirely.
|
|
vi.useFakeTimers()
|
|
const { column, quiet } = mountColumn()
|
|
movePointer(column, 'in')
|
|
expect(quiet()).toBe(false)
|
|
movePointerOverDocument(COLUMN_WIDTH + 400, 300)
|
|
act(() => { vi.advanceTimersByTime(2000) })
|
|
expect(quiet()).toBe(true)
|
|
})
|
|
|
|
it('does not restart the window when the pointer keeps moving outside', () => {
|
|
vi.useFakeTimers()
|
|
const { column, quiet } = mountColumn()
|
|
movePointer(column, 'in')
|
|
movePointer(column, 'out')
|
|
act(() => { vi.advanceTimersByTime(1500) })
|
|
// A pending hide is left alone rather than re-armed: otherwise a pointer
|
|
// resting outside the column would keep pushing the bars' disappearance
|
|
// out, one move at a time.
|
|
movePointerOverDocument(COLUMN_WIDTH + 400, 300)
|
|
act(() => { vi.advanceTimersByTime(600) })
|
|
expect(quiet()).toBe(true)
|
|
})
|
|
|
|
it('keeps them drawn while the pointer moves inside the column box', () => {
|
|
vi.useFakeTimers()
|
|
const { column, quiet } = mountColumn()
|
|
movePointer(column, 'in')
|
|
movePointer(column, 'out')
|
|
// A move landing back inside the box cancels the pending hide, the same
|
|
// way re-entering the element does.
|
|
movePointerOverDocument(COLUMN_WIDTH - 10, 300)
|
|
act(() => { vi.advanceTimersByTime(5000) })
|
|
expect(quiet()).toBe(false)
|
|
})
|
|
|
|
it('drops the pending hide when the column unmounts', () => {
|
|
vi.useFakeTimers()
|
|
const { column } = mountColumn()
|
|
movePointer(column, 'in')
|
|
movePointer(column, 'out')
|
|
cleanup()
|
|
// A timer surviving the unmount would call setState on a dead component.
|
|
expect(() => { vi.advanceTimersByTime(5000) }).not.toThrow()
|
|
expect(vi.getTimerCount()).toBe(0)
|
|
})
|
|
})
|