Files
opencode/packages/app/test-browser/sidebar-peek.test.ts

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()
})