mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-15 04:27:20 +03:00
150 lines
4.7 KiB
TypeScript
150 lines
4.7 KiB
TypeScript
import { afterEach, beforeEach, expect, it, jest } from "bun:test"
|
|
import { createRoot, createSignal } from "solid-js"
|
|
import { createSidebarPeek } from "../src/shell/sidebar-peek"
|
|
|
|
const cleanups: (() => void)[] = []
|
|
|
|
// The dwell, grace, watch and slide-out fallbacks are timers; fake time keeps them exact on a loaded runner.
|
|
beforeEach(() => jest.useFakeTimers())
|
|
|
|
afterEach(() => {
|
|
for (const cleanup of cleanups.splice(0)) cleanup()
|
|
document.body.innerHTML = ""
|
|
jest.useRealTimers()
|
|
})
|
|
|
|
function mount() {
|
|
const row = document.createElement("div")
|
|
const aside = document.createElement("aside")
|
|
const panel = document.createElement("div")
|
|
const content = document.createElement("main")
|
|
aside.append(panel)
|
|
row.append(aside, content)
|
|
document.body.append(row)
|
|
const [open, setOpen] = createSignal(false)
|
|
const [enabled, setEnabled] = createSignal(true)
|
|
const painted: (number | undefined)[] = []
|
|
const docked: (() => void)[] = []
|
|
|
|
const peek = createRoot((dispose) => {
|
|
cleanups.push(dispose)
|
|
|
|
return createSidebarPeek({
|
|
available: () => !open(),
|
|
open,
|
|
enabled,
|
|
aside: () => aside,
|
|
panel: () => panel,
|
|
paint: (shown) => painted.push(shown),
|
|
whenDocked: (run) => docked.push(run),
|
|
})
|
|
})
|
|
|
|
return { peek, content, setOpen, setEnabled, painted, docked }
|
|
}
|
|
|
|
// Rests the pointer on the window's start edge, below the header band.
|
|
function restOnEdge(target: Element, clientX = 2, buttons = 0) {
|
|
target.dispatchEvent(
|
|
new PointerEvent("pointermove", { bubbles: true, clientX, clientY: 300, buttons, pointerType: "mouse" }),
|
|
)
|
|
}
|
|
|
|
// Past the edge's 40ms dwell, and short of the 150ms watch that would start closing a peek the pointer isn't in.
|
|
const afterDwell = () => jest.advanceTimersByTime(60)
|
|
|
|
it("peeks once the pointer rests on the window's start edge", () => {
|
|
const view = mount()
|
|
restOnEdge(view.content)
|
|
afterDwell()
|
|
expect(view.peek.shown()).toBe(true)
|
|
})
|
|
|
|
it("peeks from the edge zone 16px past the 8px gutter, and not beyond it", () => {
|
|
const inside = mount()
|
|
restOnEdge(inside.content, 24)
|
|
afterDwell()
|
|
expect(inside.peek.shown()).toBe(true)
|
|
|
|
const beyond = mount()
|
|
restOnEdge(beyond.content, 25)
|
|
afterDwell()
|
|
expect(beyond.peek.shown()).toBe(false)
|
|
})
|
|
|
|
it("doesn't peek while a mouse button is held, as when a text selection drags into the edge zone", () => {
|
|
const view = mount()
|
|
restOnEdge(view.content, 12, 1)
|
|
afterDwell()
|
|
expect(view.peek.shown()).toBe(false)
|
|
})
|
|
|
|
it("a press, a drag, a dialog, a resize or a window blur clears the pending peek", () => {
|
|
const interruptions = [
|
|
() => document.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, pointerType: "mouse" })),
|
|
() => document.dispatchEvent(new Event("dragstart", { bubbles: true })),
|
|
() => window.dispatchEvent(new Event("resize")),
|
|
() => window.dispatchEvent(new Event("blur")),
|
|
() => {
|
|
const dialog = document.createElement("div")
|
|
dialog.setAttribute("role", "dialog")
|
|
document.body.append(dialog)
|
|
},
|
|
]
|
|
|
|
for (const interrupt of interruptions) {
|
|
const view = mount()
|
|
restOnEdge(view.content)
|
|
interrupt()
|
|
afterDwell()
|
|
expect(view.peek.shown()).toBe(false)
|
|
|
|
for (const cleanup of cleanups.splice(0)) cleanup()
|
|
document.body.innerHTML = ""
|
|
}
|
|
})
|
|
|
|
it("a press released outside the window no longer holds the peek open", () => {
|
|
const view = mount()
|
|
restOnEdge(view.content)
|
|
afterDwell()
|
|
expect(view.peek.shown()).toBe(true)
|
|
|
|
// Pressed, then out of the window, where no pointerup reaches the page.
|
|
document.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, buttons: 1, pointerType: "mouse" }))
|
|
document.documentElement.dispatchEvent(new PointerEvent("pointerleave", { pointerType: "mouse" }))
|
|
// The 100ms grace, then the slide out, which settles by its 800ms fallback when no frame lands it first.
|
|
jest.advanceTimersByTime(1_000)
|
|
expect(view.peek.shown()).toBe(false)
|
|
|
|
restOnEdge(view.content)
|
|
afterDwell()
|
|
expect(view.peek.shown()).toBe(true)
|
|
})
|
|
|
|
it("a dock from a peek ends the hand-off once the docked sidebar lands", async () => {
|
|
const view = mount()
|
|
restOnEdge(view.content)
|
|
afterDwell()
|
|
view.setOpen(true)
|
|
expect(view.peek.handedOff()).toBe(true)
|
|
expect(view.peek.shown()).toBe(true)
|
|
|
|
await Promise.resolve()
|
|
view.docked.forEach((run) => run())
|
|
expect(view.peek.handedOff()).toBe(false)
|
|
expect(view.peek.shown()).toBe(false)
|
|
expect(view.painted.at(-1)).toBeUndefined()
|
|
})
|
|
|
|
it("ends a peek at once when the vertical sidebar goes away", () => {
|
|
const view = mount()
|
|
restOnEdge(view.content)
|
|
afterDwell()
|
|
expect(view.peek.shown()).toBe(true)
|
|
|
|
view.setEnabled(false)
|
|
expect(view.peek.shown()).toBe(false)
|
|
expect(view.painted.at(-1)).toBeUndefined()
|
|
})
|