mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-24 06:33:01 -04:00
fix(app): stabilize pane transitions (#44484)
This commit is contained in:
@@ -131,8 +131,11 @@ test("animates review and terminal panels while caching hidden terminal content"
|
||||
await resetTerminalBottomMotion(page)
|
||||
await resetTerminalAnchorGaps(page)
|
||||
await resetPanelGaps(page)
|
||||
const reviewContent = page.locator('[data-component="session-review-v2"]')
|
||||
await reviewContent.evaluate((element) => element.setAttribute("data-cache-probe", "original"))
|
||||
await reviewToggle.click()
|
||||
await expect(page.locator("#review-panel")).toHaveCount(0)
|
||||
await expect(page.locator("#review-panel")).toBeHidden()
|
||||
await expect(reviewContent).toHaveAttribute("data-cache-probe", "original")
|
||||
await expect(panel).toBeVisible()
|
||||
await expectHeightMotions(page, "session-side-region", 2)
|
||||
await expectHeightMotions(page, "session-side-terminal-region", 2)
|
||||
@@ -142,6 +145,7 @@ test("animates review and terminal panels while caching hidden terminal content"
|
||||
await expectPanelGapHeld(page)
|
||||
await reviewToggle.click()
|
||||
await expect(page.locator("#review-panel")).toBeVisible()
|
||||
await expect(reviewContent).toHaveAttribute("data-cache-probe", "original")
|
||||
await expectHeightMotions(page, "session-side-region", 3)
|
||||
await expectHeightMotions(page, "session-side-terminal-region", 3)
|
||||
|
||||
@@ -161,14 +165,18 @@ test("animates review and terminal panels while caching hidden terminal content"
|
||||
await reviewToggle.click()
|
||||
await expect(page.locator("#review-panel")).toHaveCount(0)
|
||||
await expectWidthMotions(page, 2)
|
||||
await expectSideSlideSettled(page, 2)
|
||||
await expectHiddenSideAligned(page)
|
||||
|
||||
await resetHeightMotions(page)
|
||||
await resetHorizontalScrolls(page)
|
||||
await page.keyboard.press("Control+Backquote")
|
||||
await expect(panel).toHaveAttribute("aria-hidden", "false")
|
||||
await expect(page.locator('[data-component="terminal"]')).toBeVisible()
|
||||
await expectWidthMotions(page, 3)
|
||||
await expectSideMotionSettled(page)
|
||||
await expectSideSlideSettled(page, 3)
|
||||
await expectNoHeightMotion(page)
|
||||
await expectNoHorizontalScroll(page)
|
||||
|
||||
await page.keyboard.press("Control+Backquote")
|
||||
await expect(panel).toBeHidden()
|
||||
@@ -209,6 +217,8 @@ test("animates review and terminal panels while caching hidden terminal content"
|
||||
|
||||
type MotionProbe = {
|
||||
widths: number
|
||||
widthEnds: number
|
||||
horizontalScrolls: number[]
|
||||
reviewWidths: number[]
|
||||
paintGaps: { review: number; terminalSurface: number }[]
|
||||
terminalContentSizes: { width: number; height: number }[]
|
||||
@@ -225,6 +235,8 @@ async function installMotionProbe(page: Page) {
|
||||
await page.evaluate(() => {
|
||||
const probe: MotionProbe = {
|
||||
widths: 0,
|
||||
widthEnds: 0,
|
||||
horizontalScrolls: [],
|
||||
reviewWidths: [],
|
||||
paintGaps: [],
|
||||
terminalContentSizes: [],
|
||||
@@ -299,10 +311,16 @@ async function installMotionProbe(page: Page) {
|
||||
probe.heights.push(slot)
|
||||
}
|
||||
})
|
||||
document.addEventListener("transitionend", (event) => {
|
||||
if (!(event.target instanceof Element)) return
|
||||
if (event.propertyName === "width" && event.target.getAttribute("data-slot") === "session-chat-panel")
|
||||
probe.widthEnds++
|
||||
})
|
||||
document.addEventListener("animationstart", (event) => {
|
||||
if (!(event.target instanceof Element) || event.target.getAttribute("data-component") !== "terminal-panel") return
|
||||
probe.animations.push(event.animationName)
|
||||
})
|
||||
window.addEventListener("scroll", () => probe.horizontalScrolls.push(window.scrollX))
|
||||
;(window as Window & { __panelMotion?: MotionProbe }).__panelMotion = probe
|
||||
})
|
||||
}
|
||||
@@ -320,11 +338,10 @@ async function resetHeightMotions(page: Page) {
|
||||
})
|
||||
}
|
||||
|
||||
async function expectSideMotionSettled(page: Page) {
|
||||
const side = page.locator('[data-slot="session-side-panel-presence"]')
|
||||
async function expectSideSlideSettled(page: Page, count: number) {
|
||||
await expect
|
||||
.poll(() => side.evaluate((element) => element.getAnimations().every((item) => item.playState === "finished")))
|
||||
.toBe(true)
|
||||
.poll(() => page.evaluate(() => (window as Window & { __panelMotion?: MotionProbe }).__panelMotion?.widthEnds ?? 0))
|
||||
.toBeGreaterThanOrEqual(count)
|
||||
}
|
||||
|
||||
async function expectNoHeightMotion(page: Page) {
|
||||
@@ -334,6 +351,40 @@ async function expectNoHeightMotion(page: Page) {
|
||||
expect(heights).toEqual([])
|
||||
}
|
||||
|
||||
async function expectHiddenSideAligned(page: Page) {
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() => {
|
||||
const chat = document.querySelector<HTMLElement>('[data-slot="session-chat-panel"]')
|
||||
const side = document.querySelector<HTMLElement>('[data-slot="session-side-panel-presence"]')
|
||||
if (!chat?.parentElement || !side) return Number.POSITIVE_INFINITY
|
||||
const row = chat.parentElement.getBoundingClientRect()
|
||||
const hidden = side.getBoundingClientRect()
|
||||
return Math.max(
|
||||
Math.abs(row.top - hidden.top),
|
||||
Math.abs(row.right - hidden.right),
|
||||
Math.abs(row.bottom - hidden.bottom),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.toBeLessThanOrEqual(1)
|
||||
}
|
||||
|
||||
async function resetHorizontalScrolls(page: Page) {
|
||||
await page.evaluate(() => {
|
||||
const probe = (window as Window & { __panelMotion?: MotionProbe }).__panelMotion
|
||||
if (probe) probe.horizontalScrolls = []
|
||||
})
|
||||
}
|
||||
|
||||
async function expectNoHorizontalScroll(page: Page) {
|
||||
const scrolls = await page.evaluate(
|
||||
() => (window as Window & { __panelMotion?: MotionProbe }).__panelMotion?.horizontalScrolls ?? [],
|
||||
)
|
||||
expect(Math.max(0, ...scrolls)).toBe(0)
|
||||
expect(await page.evaluate(() => window.scrollX)).toBe(0)
|
||||
}
|
||||
|
||||
async function expectReviewWidthStable(page: Page) {
|
||||
const side = page.locator('[data-slot="session-side-panel-presence"]')
|
||||
await expect
|
||||
|
||||
@@ -101,11 +101,11 @@ export const focusTerminalById = (id: string) => {
|
||||
|
||||
const textarea = terminal.querySelector("textarea")
|
||||
if (textarea instanceof HTMLTextAreaElement) {
|
||||
textarea.focus()
|
||||
textarea.focus({ preventScroll: true })
|
||||
return true
|
||||
}
|
||||
|
||||
terminal.focus()
|
||||
terminal.focus({ preventScroll: true })
|
||||
terminal.dispatchEvent(
|
||||
typeof PointerEvent === "function"
|
||||
? new PointerEvent("pointerdown", { bubbles: true, cancelable: true })
|
||||
|
||||
@@ -165,7 +165,7 @@ export function SessionScreen(props: { session: SessionModel }) {
|
||||
<>
|
||||
<SessionHeader />
|
||||
<div class="flex-1 min-h-0 flex flex-col gap-2 p-2">
|
||||
<div ref={screen.panel.ref} class="flex-1 min-h-0 flex flex-col md:flex-row gap-2">
|
||||
<div ref={screen.panel.ref} class="relative flex-1 min-h-0 flex flex-col md:flex-row gap-2">
|
||||
<div
|
||||
classList={{
|
||||
"@container relative z-10 shrink-0 flex flex-col min-h-0 h-full flex-1 md:flex-none transition-[width]": true,
|
||||
@@ -241,6 +241,7 @@ export function SessionScreen(props: { session: SessionModel }) {
|
||||
if (event.currentTarget !== event.target) return
|
||||
if (event.animationName !== "side-region-presence-out") return
|
||||
if (screen.side.region.open()) return
|
||||
if (sideTerminalVisible()) return
|
||||
setStore("sideRegionPresent", false)
|
||||
setStore("sideReviewPresent", false)
|
||||
}}
|
||||
|
||||
@@ -347,9 +347,9 @@ export const Terminal = (props: TerminalProps) => {
|
||||
const focusTerminal = () => {
|
||||
const t = term
|
||||
if (!t) return
|
||||
t.focus()
|
||||
t.textarea?.focus()
|
||||
setTimeout(() => t.textarea?.focus(), 0)
|
||||
const focus = () => (t.textarea ? t.textarea.focus({ preventScroll: true }) : t.focus())
|
||||
focus()
|
||||
setTimeout(focus, 0)
|
||||
}
|
||||
const handlePointerDown = () => {
|
||||
const activeElement = document.activeElement
|
||||
|
||||
Reference in New Issue
Block a user