Files
aiturk-hermes-ide/apps/desktop/src/store/composer-popout.test.ts
T

131 lines
5.0 KiB
TypeScript

import { beforeEach, describe, expect, it } from 'vitest'
import {
$composerPopoutZones,
clampPopoutPosition,
getComposerPopoutZone,
POPOUT_WIDTH_REM,
type PopoutBounds,
pruneComposerPopoutZones,
setComposerPopoutPosition,
setComposerPoppedOut
} from './composer-popout'
// jsdom's window is 1024x768; every expectation below is relative to that.
const VW = 1024
const VH = 768
const BOX = { height: 56, width: 320 }
// A split layout: chat on the right half, under a 40px header.
const RIGHT_HALF: PopoutBounds = { bottom: VH, left: VW / 2, right: VW, top: 40 }
// The same drag intent, viewed from the left half.
const LEFT_HALF: PopoutBounds = { bottom: VH, left: 0, right: VW / 2, top: 40 }
const LEFT_ZONE = 'g-left'
const RIGHT_ZONE = 'g-right'
describe('clampPopoutPosition', () => {
it('keeps the whole box inside its surface, not just its corner', () => {
const { bottom, right } = clampPopoutPosition({ bottom: 24, right: 24 }, BOX, RIGHT_HALF)
// Insets are viewport-relative, so convert back to an absolute rect.
const boxRight = VW - right
const boxLeft = boxRight - BOX.width
const boxBottom = VH - bottom
const boxTop = boxBottom - BOX.height
expect(boxLeft).toBeGreaterThanOrEqual(RIGHT_HALF.left)
expect(boxRight).toBeLessThanOrEqual(RIGHT_HALF.right)
expect(boxTop).toBeGreaterThanOrEqual(RIGHT_HALF.top)
expect(boxBottom).toBeLessThanOrEqual(RIGHT_HALF.bottom)
})
it('pulls an out-of-bounds intent back into the surface', () => {
// Dragged far left of this surface (a position that belongs to the other
// half of the split) — it must land inside, not off the edge.
const { right } = clampPopoutPosition({ bottom: 24, right: 900 }, BOX, RIGHT_HALF)
expect(VW - right - BOX.width).toBeGreaterThanOrEqual(RIGHT_HALF.left)
})
it('is pure — the same intent yields a different placement per surface', () => {
const intent = { bottom: 24, right: 24 }
const onRight = clampPopoutPosition(intent, BOX, RIGHT_HALF)
const onLeft = clampPopoutPosition(intent, BOX, LEFT_HALF)
// Right half honors the intent (it fits); left half has to push the box
// inward. Two surfaces, two placements, one unchanged intent — this is what
// lets keep-alive tabs share a zone's position without overwriting it.
expect(onRight).not.toEqual(onLeft)
expect(clampPopoutPosition(intent, BOX, RIGHT_HALF)).toEqual(onRight)
})
it('falls back to the full window when the surface has no measured area', () => {
const { bottom, right } = clampPopoutPosition({ bottom: 24, right: 24 }, BOX, undefined)
expect(bottom).toBe(24)
expect(right).toBe(24)
})
it('assumes the compact float width when the box is unmeasured', () => {
// Pre-layout (peel-off, restore) there is no rect yet; the fallback must
// still leave the box grabbable rather than clamping it to a zero-width slot.
const { right } = clampPopoutPosition({ bottom: 24, right: 5000 }, undefined, RIGHT_HALF)
expect(VW - right - POPOUT_WIDTH_REM * 16).toBeGreaterThanOrEqual(RIGHT_HALF.left)
})
})
describe('pop-out state is scoped to a layout zone', () => {
beforeEach(() => {
$composerPopoutZones.set({})
})
it('floats one zone without touching its neighbor', () => {
setComposerPoppedOut(LEFT_ZONE, true)
// Tabs in the left zone float; the right split stays docked. This is the
// whole point of keying by zone: popping out on the left must not fling a
// composer out of every other pane.
expect(getComposerPopoutZone(LEFT_ZONE).poppedOut).toBe(true)
expect(getComposerPopoutZone(RIGHT_ZONE).poppedOut).toBe(false)
})
it('keeps each zone position independent', () => {
setComposerPopoutPosition(LEFT_ZONE, { bottom: 100, right: 100 })
setComposerPopoutPosition(RIGHT_ZONE, { bottom: 24, right: 24 })
expect(getComposerPopoutZone(LEFT_ZONE).position).toEqual({ bottom: 100, right: 100 })
expect(getComposerPopoutZone(RIGHT_ZONE).position).toEqual({ bottom: 24, right: 24 })
})
it('reports the default for a zone nobody has touched', () => {
// Every tab in a fresh zone reads the same default, so a new split starts
// docked at the default corner rather than inheriting a neighbor's drag.
expect(getComposerPopoutZone('g-unseen')).toEqual({ poppedOut: false, position: { bottom: 24, right: 24 } })
})
it('does not let one zone float seed the zones split after it', () => {
setComposerPoppedOut(LEFT_ZONE, true)
setComposerPopoutPosition(LEFT_ZONE, { bottom: 200, right: 200 })
// A zone created later is its own thing — it must not inherit the float
// state of whichever zone happened to be touched first.
expect(getComposerPopoutZone('g-split-later')).toEqual({
poppedOut: false,
position: { bottom: 24, right: 24 }
})
})
it('drops state for zones that no longer exist', () => {
setComposerPoppedOut(LEFT_ZONE, true)
setComposerPoppedOut(RIGHT_ZONE, true)
pruneComposerPopoutZones([LEFT_ZONE])
expect(Object.keys($composerPopoutZones.get())).toEqual([LEFT_ZONE])
})
})