Skip to content

Commit fa68fc1

Browse files
committed
feat(app): keep narrow desktop windows in the desktop layout
1 parent ce44769 commit fa68fc1

50 files changed

Lines changed: 952 additions & 608 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎packages/app/component-tests/composer.spec.ts‎

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -402,14 +402,8 @@ for (const direction of ["ltr", "rtl"]) {
402402
Math.abs((direction === "ltr" ? action.x - last.x - last.width : last.x - action.x - action.width) - 12),
403403
).toBeLessThan(1)
404404
await page.keyboard.press("Enter")
405-
// Below the mobile breakpoint the variant picker opens in a bottom drawer instead of a menu.
406-
await expect(
407-
width < 768
408-
? page
409-
.getByRole("dialog", { name: "Choose model variant", exact: true })
410-
.getByRole("button", { name: "high", exact: true })
411-
: page.getByRole("menuitemradio", { name: "high", exact: true }),
412-
).toBeVisible()
405+
// A narrow window with a mouse keeps the menu; only a touch screen gets the bottom drawer.
406+
await expect(page.getByRole("menuitemradio", { name: "high", exact: true })).toBeVisible()
413407
await page.keyboard.press("Escape")
414408
await expect(variant).toBeFocused()
415409
await expect(submit).toBeInViewport()
@@ -708,7 +702,7 @@ story("keeps default thinking accessible by keyboard without composer hover", as
708702
})
709703

710704
story.describe("mobile variant drawer", () => {
711-
story.use({ viewport: { width: 360, height: 720 } })
705+
story.use({ hasTouch: true, isMobile: true, viewport: { width: 360, height: 720 } })
712706

713707
// Guards patches/@corvu%2Fdrawer@0.2.4.patch. The drawer starts its opening transition two frames after it opens.
714708
// Dismissing it before then used to slide it open anyway, and dismissing it in that frame left it closing off screen

‎packages/app/component-tests/timeline-virtualizer.spec.ts‎

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,35 @@ story.beforeEach(async ({ mount }) => {
77
await expect(component.getByRole("textbox", { name: "Prompt", exact: true })).toBeVisible()
88
})
99

10-
story("spaces the first mobile message without changing desktop spacing", async ({ page }) => {
11-
await page.setViewportSize({ width: 390, height: 844 })
12-
await page.evaluate(async (fixture) => {
13-
const { mountTimelineVirtualizer } = await import(fixture)
14-
mountTimelineVirtualizer({ count: 1, rowHeight: 60, immediate: true })
15-
}, fixture)
16-
const root = page.getByTestId("timeline-virtualizer-fixture")
17-
await root.getByRole("button", { name: "Complete Markdown", exact: true }).click()
18-
const content = root.locator("[data-timeline-virtual-content]")
19-
await expect(content).toHaveCSS("visibility", "visible")
10+
story.describe("touch", () => {
11+
story.use({ hasTouch: true, isMobile: true })
2012

21-
const gap = () =>
22-
root.locator('[data-timeline-key="user-message:message-0"]').evaluate((element) => {
23-
const viewport = element.closest("[data-scrollable]")!
13+
story("spaces the first mobile message without changing desktop spacing", async ({ page }) => {
14+
await page.setViewportSize({ width: 390, height: 844 })
15+
await page.evaluate(async (fixture) => {
16+
const { mountTimelineVirtualizer } = await import(fixture)
17+
mountTimelineVirtualizer({ count: 1, rowHeight: 60, immediate: true })
18+
}, fixture)
19+
const root = page.getByTestId("timeline-virtualizer-fixture")
20+
await root.getByRole("button", { name: "Complete Markdown", exact: true }).click()
21+
const content = root.locator("[data-timeline-virtual-content]")
22+
await expect(content).toHaveCSS("visibility", "visible")
2423

25-
return element.getBoundingClientRect().top - viewport.getBoundingClientRect().top
26-
})
24+
const gap = () =>
25+
root.locator('[data-timeline-key="user-message:message-0"]').evaluate((element) => {
26+
const viewport = element.closest("[data-scrollable]")!
2727

28-
await expect.poll(gap).toBe(16)
29-
await root.evaluate((element) => element.setAttribute("dir", "rtl"))
30-
await expect.poll(gap).toBe(16)
31-
await page.setViewportSize({ width: 1280, height: 900 })
32-
await expect.poll(gap).toBe(0)
33-
await page.setViewportSize({ width: 390, height: 844 })
34-
await expect.poll(gap).toBe(16)
28+
return element.getBoundingClientRect().top - viewport.getBoundingClientRect().top
29+
})
30+
31+
await expect.poll(gap).toBe(16)
32+
await root.evaluate((element) => element.setAttribute("dir", "rtl"))
33+
await expect.poll(gap).toBe(16)
34+
await page.setViewportSize({ width: 1280, height: 900 })
35+
await expect.poll(gap).toBe(0)
36+
await page.setViewportSize({ width: 390, height: 844 })
37+
await expect.poll(gap).toBe(16)
38+
})
3539
})
3640

3741
story("bounds the cheap suffix and reveals only ready measured rows", async ({ page }) => {

‎packages/app/e2e/regression/command-palette.spec.ts‎

Lines changed: 59 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -296,63 +296,67 @@ test("keeps a keyboard-selected file when session results arrive later", async (
296296
expect(warnings).toEqual([])
297297
})
298298

299-
test("navigation replaces commands without retaining disposed owners", async ({ page }) => {
300-
const warnings = captureConsoleWarnings(page)
301-
const palette = await openCommandPalette(page, true)
302-
await palette.input.press("Escape")
303-
await expect(palette.dialog).toHaveCount(0)
304-
await page
305-
.getByRole("region", { name: "Recent sessions" })
306-
.getByRole("button", { name: /Palette fixture session/ })
307-
.click()
308-
const editor = page.locator('[data-component="composer-editor"]')
309-
await expect(editor).toBeEditable()
310-
await page.keyboard.press("ControlOrMeta+t")
311-
await expect(page).toHaveURL(/\/new-session\?/)
312-
await expect(editor).toBeEditable()
313-
await editor.blur()
314-
await page.keyboard.press("Control+l")
315-
await expect(editor).toBeFocused()
316-
await page.keyboard.press("ControlOrMeta+Shift+P")
317-
const dialog = page.getByRole("dialog")
318-
await expect(dialog.getByRole("combobox")).toBeFocused()
319-
await expect(dialog.getByRole("combobox")).toHaveAttribute("placeholder", "Search files, commands, and sessions")
320-
await dialog.getByRole("combobox").fill("copy session")
321-
await expect(dialog.getByRole("option", { name: "Copy Session ID", exact: true })).toHaveCount(0)
322-
await dialog.getByRole("combobox").press("Escape")
323-
await expect(dialog).toHaveCount(0)
324-
const tabs = page.locator("[data-titlebar-tab-link]")
325-
await tabs.filter({ hasText: paletteSession.title }).click()
326-
await expect(page.getByRole("heading", { name: paletteSession.title, exact: true })).toBeVisible()
327-
328-
for (const count of [3, 4]) {
329-
await page.getByRole("button", { name: "New session", exact: true }).click()
330-
await expect(tabs).toHaveCount(count)
299+
test.describe("touch", () => {
300+
test.use({ hasTouch: true, isMobile: true })
301+
302+
test("navigation replaces commands without retaining disposed owners", async ({ page }) => {
303+
const warnings = captureConsoleWarnings(page)
304+
const palette = await openCommandPalette(page, true)
305+
await palette.input.press("Escape")
306+
await expect(palette.dialog).toHaveCount(0)
307+
await page
308+
.getByRole("region", { name: "Recent sessions" })
309+
.getByRole("button", { name: /Palette fixture session/ })
310+
.click()
311+
const editor = page.locator('[data-component="composer-editor"]')
331312
await expect(editor).toBeEditable()
332-
}
313+
await page.keyboard.press("ControlOrMeta+t")
314+
await expect(page).toHaveURL(/\/new-session\?/)
315+
await expect(editor).toBeEditable()
316+
await editor.blur()
317+
await page.keyboard.press("Control+l")
318+
await expect(editor).toBeFocused()
319+
await page.keyboard.press("ControlOrMeta+Shift+P")
320+
const dialog = page.getByRole("dialog")
321+
await expect(dialog.getByRole("combobox")).toBeFocused()
322+
await expect(dialog.getByRole("combobox")).toHaveAttribute("placeholder", "Search files, commands, and sessions")
323+
await dialog.getByRole("combobox").fill("copy session")
324+
await expect(dialog.getByRole("option", { name: "Copy Session ID", exact: true })).toHaveCount(0)
325+
await dialog.getByRole("combobox").press("Escape")
326+
await expect(dialog).toHaveCount(0)
327+
const tabs = page.locator("[data-titlebar-tab-link]")
328+
await tabs.filter({ hasText: paletteSession.title }).click()
329+
await expect(page.getByRole("heading", { name: paletteSession.title, exact: true })).toBeVisible()
330+
331+
for (const count of [3, 4]) {
332+
await page.getByRole("button", { name: "New session", exact: true }).click()
333+
await expect(tabs).toHaveCount(count)
334+
await expect(editor).toBeEditable()
335+
}
333336

334-
await page.setViewportSize({ width: 600, height: 800 })
335-
await page.locator('[data-slot="mobile-tabs-trigger"]').click()
336-
await expect(page.locator('[data-slot="mobile-tabs-drawer"] [data-titlebar-tab-link]')).toHaveCount(4)
337-
await page.setViewportSize({ width: 1280, height: 800 })
338-
await expect(page.locator('[data-slot="titlebar-tabs"] [data-titlebar-tab-link]')).toHaveCount(4)
339-
const active = new URL(page.url())
340-
const closing = page.locator(`[data-titlebar-tab-link][href="${active.pathname}${active.search}"]`)
341-
await expect(closing).toHaveCount(1)
342-
await page.keyboard.press("ControlOrMeta+w")
343-
await expect(tabs).toHaveCount(3)
344-
await expect(closing).toHaveCount(0)
345-
await expect(tabs.filter({ hasText: paletteSession.title })).toHaveCount(1)
346-
await tabs.filter({ hasText: paletteSession.title }).click()
347-
await expect(page.getByRole("heading", { name: paletteSession.title, exact: true })).toBeVisible()
348-
await page.keyboard.press("ControlOrMeta+Shift+P")
349-
await expect(dialog.getByRole("combobox")).toBeFocused()
350-
await dialog.getByRole("combobox").fill("copy session")
351-
await expect(dialog.getByRole("option", { name: "Copy Session ID", exact: true })).toHaveAttribute(
352-
"aria-selected",
353-
"true",
354-
)
355-
expect(warnings).toEqual([])
337+
await page.setViewportSize({ width: 600, height: 800 })
338+
await page.locator('[data-slot="mobile-tabs-trigger"]').click()
339+
await expect(page.locator('[data-slot="mobile-tabs-drawer"] [data-titlebar-tab-link]')).toHaveCount(4)
340+
await page.setViewportSize({ width: 1280, height: 800 })
341+
await expect(page.locator('[data-slot="titlebar-tabs"] [data-titlebar-tab-link]')).toHaveCount(4)
342+
const active = new URL(page.url())
343+
const closing = page.locator(`[data-titlebar-tab-link][href="${active.pathname}${active.search}"]`)
344+
await expect(closing).toHaveCount(1)
345+
await page.keyboard.press("ControlOrMeta+w")
346+
await expect(tabs).toHaveCount(3)
347+
await expect(closing).toHaveCount(0)
348+
await expect(tabs.filter({ hasText: paletteSession.title })).toHaveCount(1)
349+
await tabs.filter({ hasText: paletteSession.title }).click()
350+
await expect(page.getByRole("heading", { name: paletteSession.title, exact: true })).toBeVisible()
351+
await page.keyboard.press("ControlOrMeta+Shift+P")
352+
await expect(dialog.getByRole("combobox")).toBeFocused()
353+
await dialog.getByRole("combobox").fill("copy session")
354+
await expect(dialog.getByRole("option", { name: "Copy Session ID", exact: true })).toHaveAttribute(
355+
"aria-selected",
356+
"true",
357+
)
358+
expect(warnings).toEqual([])
359+
})
356360
})
357361

358362
test("keeps matching session and file results steady across keystrokes while searches are pending", async ({

‎packages/app/e2e/regression/mobile-composer-submit-focus.spec.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { sessionHref } from "../utils/app"
44
import { openWithDirection } from "../utils/direction"
55
import { mockOpenCodeServer } from "../utils/mock-server"
66

7+
// The mobile layout is for touch screens; a narrow window with a mouse keeps the desktop layout.
8+
test.use({ hasTouch: true, isMobile: true })
9+
710
for (const { width, direction } of [
811
{ width: 390, direction: "ltr" },
912
{ width: 390, direction: "rtl" },

‎packages/app/e2e/regression/mobile-extension-drawers.spec.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { fixture, mockStressTimeline } from "../utils/session-fixture"
33
import { sessionHref } from "../utils/app"
44
import { openWithDirection } from "../utils/direction"
55

6+
// The mobile layout is for touch screens; a narrow window with a mouse keeps the desktop layout.
7+
test.use({ hasTouch: true, isMobile: true })
8+
69
for (const scheme of ["light", "dark"] as const) {
710
for (const direction of ["ltr", "rtl"] as const) {
811
test(`mobile extensions reuse the session drawer in ${scheme} ${direction}`, async ({ page }, testInfo) => {

‎packages/app/e2e/regression/mobile-extensions.spec.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { sessionHref } from "../utils/app"
33
import { fixture, mockStressTimeline } from "../utils/session-fixture"
44
import { fileNode } from "../utils/workspace"
55

6-
test.use({ viewport: { width: 390, height: 844 } })
6+
test.use({ viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true })
77

88
async function openFiles(page: Page) {
99
await page

0 commit comments

Comments
 (0)