import type { OpenCodeEvent } from "@opencode/client/promise" import { expect, test } from "@playwright/test" import { captureConsoleWarnings, openCommandPalette, openPaletteWorkspace, paletteSession, } from "../utils/command-palette" import { REMOTE_SERVER, SERVER } from "../utils/app" import { mockRemoteServer, mockWorkspace } from "../utils/workspace" test.use({ serviceWorkers: "block", permissions: ["clipboard-read", "clipboard-write"] }) test("copies the session ID while file and session searches are still pending", async ({ page }) => { const warnings = captureConsoleWarnings(page) const { dialog, input } = await openCommandPalette(page) const release = Promise.withResolvers() await page.route(/\/api\/(session\?|fs\/find\?)/, async (route) => { await release.promise await route.fallback() }) await input.pressSequentially("copy session") const copy = dialog.getByRole("option", { name: "Copy Session ID", exact: true }) await expect(copy).toHaveAttribute("aria-selected", "true") await input.press("Enter") await expect(dialog).toHaveCount(0) await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe(paletteSession.id) await expect(page.locator('[data-testid^="toast-v2-"] [data-slot="icon-svg"]')).toBeVisible() expect(warnings).toEqual([]) release.resolve() }) test("home commands do not wait for session search", async ({ page }) => { const { dialog, input } = await openCommandPalette(page, true) const release = Promise.withResolvers() await page.route("**/api/session?*", async (route) => { await release.promise await route.fallback() }) await input.fill("open settings") await expect(dialog.getByRole("option")).toHaveCount(1) await expect(dialog.getByRole("option", { name: /^Open settings/ })).toHaveAttribute("aria-selected", "true") await input.press("Enter") await expect(page).toHaveURL("/settings") await expect(page.getByTestId("settings-screen").getByRole("tab", { name: "Preferences", exact: true })).toBeVisible() release.resolve() }) test("finds a setting and opens Settings with its row in view and highlighted", async ({ page }) => { await openPaletteWorkspace(page) await page.keyboard.press("Control+K") const dialog = page.getByRole("dialog") const input = dialog.getByRole("combobox") await expect(input).toBeFocused() await input.fill("theme") await expect(dialog.getByRole("option", { name: /^Theme/ })).toHaveAttribute("aria-selected", "true") // Settings list after commands, and the keyboard walks rows in their visual order. const searched = Promise.all([ page.waitForResponse((response) => new URL(response.url()).pathname === "/api/session"), page.waitForResponse((response) => new URL(response.url()).pathname === "/api/fs/find"), ]) await input.fill("model") await searched const titles = dialog.locator('[data-slot="search-list-header"]') await expect(titles.first()).toHaveText("Commands") await expect(titles.last()).toHaveText("Settings") const options = dialog.getByRole("option") for (const index of Array.from({ length: await options.count() }, (_, index) => index)) { await expect(options.nth(index)).toHaveAttribute("aria-selected", "true") await input.press("ArrowDown") } await input.fill("theme") await expect(dialog.getByRole("option", { name: /^Theme/ })).toHaveAttribute("aria-selected", "true") await input.press("Enter") await expect(page).toHaveURL("/settings?tab=appearance") const row = page.getByTestId("settings-screen").locator('[data-search-target="row"]') await expect(row).toContainText("Theme") await expect(row).toBeInViewport() }) test("opens from the Settings page and finds another setting", async ({ page }) => { await openPaletteWorkspace(page, true) await page.keyboard.press("ControlOrMeta+,") await expect(page).toHaveURL("/settings") const screen = page.getByTestId("settings-screen") await expect(screen.getByRole("tab", { name: "Preferences", exact: true })).toBeVisible() await page.keyboard.press("ControlOrMeta+K") const dialog = page.getByRole("dialog") const input = dialog.getByRole("combobox") await expect(input).toBeFocused() await input.fill("theme") await expect(dialog.getByRole("option", { name: /^Theme/ })).toHaveAttribute("aria-selected", "true") await input.press("Enter") await expect(dialog).toHaveCount(0) await expect(page).toHaveURL("/settings?tab=appearance") const row = screen.locator('[data-search-target="row"]') await expect(row).toContainText("Theme") await expect(row).toBeInViewport() }) test("lists recent sessions before a query is entered", async ({ page }) => { const { dialog } = await openCommandPalette(page, true) const session = dialog.getByRole("option", { name: /Palette fixture session/ }) await expect(dialog.getByText("Recent sessions", { exact: true })).toBeVisible() await expect(session).toHaveAttribute("aria-selected", "false") await session.click() await expect(dialog).toHaveCount(0) await expect(page.getByRole("heading", { name: paletteSession.title, exact: true })).toBeVisible() }) test("titlebar search lists recent sessions and opens the typed match", async ({ page }) => { await openPaletteWorkspace(page, true) await page.getByRole("banner").getByRole("button", { name: "Search sessions", exact: true }).click() const dialog = page.getByRole("dialog") const input = dialog.getByRole("combobox") await expect(input).toBeFocused() await expect(input).toHaveAttribute("placeholder", "Search sessions") await expect(dialog.getByText("Recent sessions", { exact: true })).toBeVisible() await expect(dialog.getByRole("option", { name: /Palette fixture session/ })).toHaveAttribute("aria-selected", "true") // Session search lists sessions only, never commands. await input.fill("settings") await expect(dialog.getByText("No results found", { exact: true })).toBeVisible() await input.fill("Palette fixture") await expect(dialog.getByRole("option")).toHaveCount(1) await input.press("Enter") await expect(dialog).toHaveCount(0) await expect(page.getByRole("heading", { name: paletteSession.title, exact: true })).toBeVisible() }) test("titlebar search asks for one bounded result set after a pause in typing", async ({ page }) => { await openPaletteWorkspace(page, true) const searches: URL[] = [] page.on("request", (request) => { const url = new URL(request.url()) if (request.method() !== "GET" || url.pathname !== "/api/session") return if (!url.searchParams.get("search")) return searches.push(url) }) await page.getByRole("banner").getByRole("button", { name: "Search sessions", exact: true }).click() const dialog = page.getByRole("dialog") const input = dialog.getByRole("combobox") await expect(dialog.getByRole("option", { name: /Palette fixture session/ })).toHaveAttribute("aria-selected", "true") const searched = page.waitForRequest((request) => { const url = new URL(request.url()) return ( request.method() === "GET" && url.pathname === "/api/session" && url.searchParams.get("search") === "Palette fixture" ) }) // A burst of input in one task is one pause: every keystroke must supersede the last, and only the pause searches. await input.evaluate((element) => { // SAFETY: the dialog's combobox is SearchList's own text input, which is an HTMLInputElement. const field = element as HTMLInputElement const value = "Palette fixture" for (let index = 1; index <= value.length; index++) { field.value = value.slice(0, index) field.dispatchEvent(new InputEvent("input", { bubbles: true })) } }) const request = new URL((await searched).url()) await expect(dialog.getByRole("option", { name: /Palette fixture session/ })).toBeVisible() expect(searches).toHaveLength(1) expect(request.searchParams.get("limit")).toBe("50") expect(request.searchParams.get("parentID")).toBe("null") expect(request.searchParams.get("archived")).toBe("false") expect(request.searchParams.get("cursor")).toBeNull() }) test("titlebar search shows one server's matches while another server's search fails", async ({ page }) => { await mockRemoteServer(page, { name: "Remote" }) const escape = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") // Only typed searches fail, so the remote server still lists its recent sessions. await page.route(new RegExp(`^${escape(REMOTE_SERVER)}/api/session\\?.*search=[^&]`), (route) => route.request().method() === "GET" ? route.fulfill({ status: 500, json: { message: "Search failed" }, headers: { "access-control-allow-origin": "*" }, }) : route.fallback(), ) await openPaletteWorkspace(page, true) // The server matches more than titles, so only its answer can list this session. Registered after the local mock so // this route answers first. await page.route(new RegExp(`^${escape(SERVER)}/api/session\\?.*search=[^&]`), (route) => route.fulfill({ json: { data: [ { ...paletteSession, id: "ses_palette_found", location: { directory: paletteSession.directory }, title: "Matched on the server", }, ], }, }), ) await page.getByRole("banner").getByRole("button", { name: "Search sessions", exact: true }).click() const dialog = page.getByRole("dialog") await dialog.getByRole("combobox").fill("needle") await expect(dialog.getByRole("option")).toHaveText([/Matched on the server/]) await expect(dialog.getByText("Request failed", { exact: true })).toHaveCount(0) }) test("a working session keeps its project avatar in Search sessions and shows its activity ring in place of its age", async ({ page, }) => { const working = { id: "ses_palette_working", title: "Working session" } const idle = { id: "ses_palette_idle", title: "Idle session" } await mockWorkspace(page, { name: "PaletteActivity", sessions: [working, idle], sessionStatus: { [working.id]: { type: "running" } }, }) await page.goto("/") await page.getByRole("banner").getByRole("button", { name: "Search sessions", exact: true }).click() const dialog = page.getByRole("dialog") const workingRow = dialog.getByRole("option", { name: /Working session/ }) const idleRow = dialog.getByRole("option", { name: /Idle session/ }) await expect(workingRow.locator('[data-component="activity-ring"]')).toBeVisible() await expect(workingRow.locator('[data-component="project-avatar-v2"]')).toBeVisible() await expect(workingRow.locator(".command-palette-meta")).toHaveText("Working") await expect(idleRow.locator('[data-component="activity-ring"]')).toHaveCount(0) await expect(idleRow.locator('[data-component="project-avatar-v2"]')).toBeVisible() await expect(idleRow.locator(".command-palette-meta")).not.toHaveText("Working") }) test("Search commands lists only commands and runs the chosen one", async ({ page }) => { const { dialog, input } = await openCommandPalette(page, true) await input.fill("search commands") await dialog.getByRole("option", { name: /^Search commands/ }).click() const search = page.getByRole("dialog").getByRole("combobox") await expect(search).toHaveAttribute("placeholder", "Search commands") await expect(search).toBeFocused() await expect(page.getByRole("dialog").getByRole("option", { name: /^Open settings/ })).toBeVisible() await search.fill("Palette fixture") await expect(page.getByRole("dialog").getByText("No results found", { exact: true })).toBeVisible() await search.fill("open settings") await expect(page.getByRole("dialog").getByRole("option")).toHaveCount(1) await search.press("Enter") await expect(page).toHaveURL("/settings") }) test("Search projects opens the full location picker from a session, draft, and Home", async ({ page }) => { const editor = page.locator('[data-component="composer-editor"]') const picker = page.getByRole("combobox", { name: "Search projects", exact: true }) await openPaletteWorkspace(page) const shortcut = (await page.evaluate(() => navigator.platform.includes("Mac"))) ? "Meta+Shift+O" : "Control+Shift+O" await page.keyboard.press(shortcut) await expect(page).toHaveURL(/\/new-session\?draftId=.+$/) await expect(picker).toBeFocused() const projects = page.getByRole("dialog").getByRole("option") await expect(projects.filter({ hasText: "Add project" })).toBeVisible() await picker.fill("command-palette") await expect(projects).toHaveCount(2) await expect(projects.getByRole("button", { name: "Worktrees", exact: true })).toBeVisible() await picker.press("ArrowRight") await expect(page.getByRole("option", { name: /^New worktree/ })).toBeVisible() await page.getByRole("combobox").press("Escape") await picker.press("Escape") await expect(picker).toHaveCount(0) await expect(editor).toBeFocused() await page.keyboard.press(shortcut) await expect(picker).toBeFocused() await expect(projects.filter({ hasText: "Add project" })).toBeVisible() await picker.press("Escape") await page.goto("/") await expect( page.getByRole("region", { name: "Recent sessions" }).getByRole("button", { name: /Palette fixture session/ }), ).toBeEnabled() await page.keyboard.press(shortcut) await expect(page).toHaveURL(/\/new-session\?draftId=.+$/) await expect(picker).toBeFocused() await expect(projects.filter({ hasText: "Add project" })).toBeVisible() }) declare global { interface Window { __paletteOrders?: string[][] } } test("titlebar search keeps recent order stable across agent steps", async ({ page }) => { const busy = { ...paletteSession, time: { ...paletteSession.time } } const fresh = { ...paletteSession, id: "ses_command_palette_fresh", title: "Fresh fixture session", time: { created: 1700000001000, updated: 1700000001000 }, } const mock = await openPaletteWorkspace(page, true, [busy, fresh]) // A step bumps the busy session's time on the server, but only a finished run reorders recent sessions. busy.time.updated = 1700000002000 // SAFETY: a step in a session whose transcript is not open; only its session ID and time matter here. await mock.push([ { id: "evt_busy_step", created: busy.time.updated, type: "session.step.started", durable: { aggregateID: busy.id, seq: 1, version: 1 }, location: { directory: busy.directory }, data: { sessionID: busy.id, assistantMessageID: "msg_busy_step", agent: "build", model: { providerID: "test", id: "test" }, started: busy.time.updated, }, } as OpenCodeEvent, ]) const home = page.getByRole("region", { name: "Recent sessions" }).getByRole("button", { name: /fixture session/ }) await expect(home).toHaveText([/Fresh fixture session/, /Palette fixture session/]) await page.evaluate(() => { const seen: string[][] = [] window.__paletteOrders = seen new MutationObserver(() => { const order = [...document.querySelectorAll('[role="dialog"] [role="option"] .command-palette-title')].map( (title) => title.textContent ?? "", ) if (order.length > 0 && order.join() !== seen.at(-1)?.join()) seen.push(order) }).observe(document.body, { childList: true, subtree: true, characterData: true }) }) const dialog = page.getByRole("dialog") const openAndClose = async () => { const listed = page.waitForResponse((response) => new URL(response.url()).pathname === "/api/session") await page.getByRole("banner").getByRole("button", { name: "Search sessions", exact: true }).click() await listed await expect(dialog.getByRole("option")).toHaveText([/Fresh fixture session/, /Palette fixture session/]) await dialog.getByRole("combobox").press("Escape") await expect(dialog).toHaveCount(0) } await openAndClose() await openAndClose() expect(await page.evaluate(() => window.__paletteOrders)).toEqual([[fresh.title, paletteSession.title]]) }) const olderSession = { ...paletteSession, id: "ses_command_palette_older", title: "Older fixture session", time: { created: 1699999999000, updated: 1699999999000 }, } test("titlebar search drops a recent session deleted while it is open", async ({ page }) => { const mock = await openPaletteWorkspace(page, true, [paletteSession, olderSession]) await page.getByRole("banner").getByRole("button", { name: "Search sessions", exact: true }).click() const options = page.getByRole("dialog").getByRole("option") await expect(options).toHaveText([/Palette fixture session/, /Older fixture session/]) await mock.push([ { id: "evt_older_deleted", created: 1700000003000, type: "session.deleted", durable: { aggregateID: olderSession.id, seq: 1, version: 2 }, location: { directory: olderSession.directory }, data: { sessionID: olderSession.id }, }, ]) await expect(options).toHaveText([/Palette fixture session/]) }) test("reopens from its shortcut as soon as it closes", async ({ page }) => { const { dialog, input } = await openCommandPalette(page) await input.press("Escape") await expect(dialog).toHaveCount(0) // A closed dialog stops blocking shortcuts when it leaves the screen, not when it is disposed afterwards. await page.keyboard.press("ControlOrMeta+Shift+P") await expect(dialog.getByRole("combobox")).toBeFocused() }) test("appends search results without resetting the selected command", async ({ page }) => { const { dialog, input } = await openCommandPalette(page) const files = Promise.withResolvers() const sessions = Promise.withResolvers() await page.route("**/api/fs/find?*", async (route) => { await files.promise await route.fulfill({ json: { data: [{ path: "copy.txt", type: "file" }] } }) }) await page.route("**/api/session?*", async (route) => { await sessions.promise // Like the server, only a matching title answers, so the session leaves the results for "copy session". const search = new URL(route.request().url()).searchParams.get("search") ?? "" await route.fulfill({ json: { data: "copy fixture".includes(search.toLowerCase()) ? [{ ...paletteSession, location: { directory: paletteSession.directory }, title: "Copy fixture" }] : [], }, }) }) await input.fill("copy") const project = dialog.getByRole("option", { name: "Copy Project ID", exact: true }) await expect(project).toBeVisible() // Select a non-first command with the keyboard before remote results arrive. await input.press("ArrowDown") await expect(project).toHaveAttribute("aria-selected", "true") files.resolve() await expect(dialog.getByRole("option", { name: "/ copy.txt", exact: true })).toBeVisible() await expect(project).toHaveAttribute("aria-selected", "true") // File results are usable even while sessions are still pending. sessions.resolve() await expect(dialog.getByRole("option", { name: /Copy fixture/ })).toBeVisible() await expect(project).toHaveAttribute("aria-selected", "true") await input.fill("copy session") await expect(dialog.getByRole("option", { name: "Copy Session ID", exact: true })).toHaveAttribute( "aria-selected", "true", ) await expect(dialog.getByRole("option", { name: "Copy Project ID", exact: true })).toHaveCount(0) }) test("lists commands above matching sessions and jumps between sections with Alt and the arrows", async ({ page }) => { const { dialog, input } = await openCommandPalette(page) await page.route("**/api/session?*", (route) => route.fulfill({ json: { data: [ { ...paletteSession, id: "ses_palette_copy", location: { directory: paletteSession.directory }, title: "Copy fixture", }, ], }, }), ) await input.fill("copy") const session = dialog.getByRole("option", { name: /Copy fixture/ }) await expect(session).toBeVisible() const commands = dialog.getByRole("group", { name: "Commands", exact: true }).getByRole("option") await expect(commands.first()).toHaveAttribute("aria-selected", "true") await expect(dialog.locator('[data-slot="search-list-header"]').first()).toHaveText("Commands") await input.press("Alt+ArrowDown") await expect(session).toHaveAttribute("aria-selected", "true") await input.press("Alt+ArrowUp") await expect(commands.first()).toHaveAttribute("aria-selected", "true") await input.press("ArrowDown") await input.press("Alt+ArrowUp") await expect(commands.first()).toHaveAttribute("aria-selected", "true") }) test("selects the first session when it arrives above automatically selected files", async ({ page }) => { const { dialog, input } = await openCommandPalette(page) const sessions = Promise.withResolvers() await page.route("**/api/fs/find?*", (route) => route.fulfill({ json: { data: [{ path: "README.md", type: "file" }] } }), ) await page.route("**/api/session?*", async (route) => { await sessions.promise await route.fulfill({ json: { data: Array.from({ length: 18 }, (_, index) => ({ ...paletteSession, id: index === 0 ? paletteSession.id : `ses_palette_readme_${index}`, location: { directory: paletteSession.directory }, title: `README work ${index + 1}`, })), }, }) }) await input.fill("README") await expect(dialog.getByRole("option", { name: "/ README.md", exact: true })).toHaveAttribute( "aria-selected", "true", ) sessions.resolve() const first = dialog.getByRole("option", { name: /README work 1 command-palette/ }) const second = dialog.getByRole("option", { name: /README work 2 command-palette/ }) await expect(first).toHaveAttribute("aria-selected", "true") await expect(first).toBeInViewport() await input.press("ArrowDown") await expect(second).toHaveAttribute("aria-selected", "true") await expect(second).toBeInViewport() await input.press("ArrowUp") await expect(first).toHaveAttribute("aria-selected", "true") await input.press("Enter") await expect(dialog).toHaveCount(0) await expect(page.getByRole("heading", { name: paletteSession.title, exact: true })).toBeVisible() }) test("keeps a keyboard-selected file when session results arrive later", async ({ page }) => { const warnings = captureConsoleWarnings(page) const { dialog, input } = await openCommandPalette(page) const sessions = Promise.withResolvers() await page.route("**/api/fs/find?*", (route) => route.fulfill({ json: { data: [ { path: "README.txt", type: "file" }, { path: "README.md", type: "file" }, ], }, }), ) await page.route("**/api/session?*", async (route) => { await sessions.promise await route.fulfill({ json: { data: [{ ...paletteSession, location: { directory: paletteSession.directory }, title: "README work" }], }, }) }) await input.fill("README") const file = dialog.getByRole("option", { name: "/ README.md", exact: true }) await expect(dialog.getByRole("option", { name: "/ README.txt", exact: true })).toHaveAttribute( "aria-selected", "true", ) await input.press("ArrowDown") await expect(file).toHaveAttribute("aria-selected", "true") sessions.resolve() await expect(dialog.getByRole("option", { name: /README work/ })).toBeVisible() await expect(file).toHaveAttribute("aria-selected", "true") await input.press("Enter") await expect(dialog).toHaveCount(0) const tab = page.getByRole("tab", { name: "README.md", exact: true }) await expect(tab).toBeVisible() await expect(page.getByRole("heading", { name: paletteSession.title, exact: true })).toBeVisible() await page .getByRole("complementary", { name: "Review and files" }) .getByRole("button", { name: "Close tab", exact: true }) .click() await expect(tab).toHaveCount(0) await expect(page.getByRole("heading", { name: paletteSession.title, exact: true })).toBeVisible() // The duplicate command ID warning is DEV-only, so only dev-server runs can catch duplicate tab commands here. expect(warnings).toEqual([]) }) test("navigation replaces commands without retaining disposed owners", async ({ page }) => { const warnings = captureConsoleWarnings(page) const palette = await openCommandPalette(page, true) await palette.input.press("Escape") await expect(palette.dialog).toHaveCount(0) await page .getByRole("region", { name: "Recent sessions" }) .getByRole("button", { name: /Palette fixture session/ }) .click() const editor = page.locator('[data-component="composer-editor"]') await expect(editor).toBeEditable() await page.keyboard.press("ControlOrMeta+t") await expect(page).toHaveURL(/\/new-session\?/) await expect(editor).toBeEditable() await editor.blur() await page.keyboard.press("Control+l") await expect(editor).toBeFocused() await page.keyboard.press("ControlOrMeta+Shift+P") const dialog = page.getByRole("dialog") await expect(dialog.getByRole("combobox")).toBeFocused() await expect(dialog.getByRole("combobox")).toHaveAttribute("placeholder", "Search files, commands, and sessions") await dialog.getByRole("combobox").fill("copy session") await expect(dialog.getByRole("option", { name: "Copy Session ID", exact: true })).toHaveCount(0) await dialog.getByRole("combobox").press("Escape") await expect(dialog).toHaveCount(0) const tabs = page.locator("[data-titlebar-tab-link]") await tabs.filter({ hasText: paletteSession.title }).click() await expect(page.getByRole("heading", { name: paletteSession.title, exact: true })).toBeVisible() for (const count of [3, 4]) { await page.getByRole("button", { name: "New session", exact: true }).click() await expect(tabs).toHaveCount(count) await expect(editor).toBeEditable() } await page.setViewportSize({ width: 600, height: 800 }) await page.locator('[data-slot="mobile-tabs-trigger"]').click() await expect(page.locator('[data-slot="mobile-tabs-drawer"] [data-titlebar-tab-link]')).toHaveCount(4) await page.setViewportSize({ width: 1280, height: 800 }) await expect(page.locator('[data-slot="titlebar-tabs"] [data-titlebar-tab-link]')).toHaveCount(4) const active = new URL(page.url()) const closing = page.locator(`[data-titlebar-tab-link][href="${active.pathname}${active.search}"]`) await expect(closing).toHaveCount(1) await page.keyboard.press("ControlOrMeta+w") await expect(tabs).toHaveCount(3) await expect(closing).toHaveCount(0) await expect(tabs.filter({ hasText: paletteSession.title })).toHaveCount(1) await tabs.filter({ hasText: paletteSession.title }).click() await expect(page.getByRole("heading", { name: paletteSession.title, exact: true })).toBeVisible() await page.keyboard.press("ControlOrMeta+Shift+P") await expect(dialog.getByRole("combobox")).toBeFocused() await dialog.getByRole("combobox").fill("copy session") await expect(dialog.getByRole("option", { name: "Copy Session ID", exact: true })).toHaveAttribute( "aria-selected", "true", ) expect(warnings).toEqual([]) }) test("keeps matching session and file results steady across keystrokes while searches are pending", async ({ page, }) => { await openPaletteWorkspace(page) const mac = await page.evaluate(() => /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)) await page.keyboard.press(mac ? "Meta+Shift+P" : "Control+Shift+P") const dialog = page.getByRole("dialog") const input = dialog.getByRole("combobox") await expect(input).toBeFocused() const sessionOption = dialog.getByRole("option", { name: /Palette fixture session/ }) const matchingFile = dialog.getByRole("option", { name: "packages/desktop/ palette.ts", exact: true }) const nonMatchingFile = dialog.getByRole("option", { name: "src/app/ other.ts", exact: true }) await expect(sessionOption).toBeVisible() const pendingFiles = Promise.withResolvers() const pendingSessions = Promise.withResolvers() await page.route("**/api/fs/find?*", async (route) => { const query = new URL(route.request().url()).searchParams.get("query") ?? "" if (query !== "p") await pendingFiles.promise await route.fulfill({ json: { data: query === "p" ? [ { path: "packages/desktop/palette.ts", type: "file" }, { path: "src/app/other.ts", type: "file" }, ] : [{ path: "packages/desktop/palette.ts", type: "file" }], }, }) }) await page.route("**/api/session?*", async (route) => { await pendingSessions.promise await route.fulfill({ json: { data: [{ ...paletteSession, location: { directory: paletteSession.directory } }], }, }) }) await input.press("p") await expect(matchingFile).toBeVisible() await expect(nonMatchingFile).toBeVisible() await expect(sessionOption).toBeVisible() const probe = await page.evaluateHandle(() => { const results = document.querySelector('[role="dialog"] [data-slot="search-list-scroll"]')! const state = { emptyFrames: 0, initialSessionEl: Array.from(results.querySelectorAll('[role="option"]')).find((el) => el.textContent?.includes("Palette fixture session"), ) ?? null, initialFileEl: Array.from(results.querySelectorAll('[role="option"]')).find((el) => el.textContent?.includes("palette.ts")) ?? null, } const observer = new MutationObserver(() => { if ( results.querySelectorAll('[role="option"]').length === 0 || results.querySelector('[data-slot="search-list-empty"]') ) { state.emptyFrames++ } }) observer.observe(results, { childList: true, subtree: true }) return { finish() { observer.disconnect() const currentSessionEl = Array.from(results.querySelectorAll('[role="option"]')).find((el) => el.textContent?.includes("Palette fixture session"), ) const currentFileEl = Array.from(results.querySelectorAll('[role="option"]')).find((el) => el.textContent?.includes("palette.ts"), ) return { emptyFrames: state.emptyFrames, sameSessionNode: Boolean(state.initialSessionEl && state.initialSessionEl === currentSessionEl), sameFileNode: Boolean(state.initialFileEl && state.initialFileEl === currentFileEl), } }, } }) for (const key of "alette") { await input.press(key) await expect(sessionOption).toBeVisible() await expect(matchingFile).toBeVisible() await expect(nonMatchingFile).toHaveCount(0) } await expect(sessionOption).toHaveAttribute("aria-selected", "true") pendingFiles.resolve() pendingSessions.resolve() await expect(sessionOption).toBeVisible() await expect(matchingFile).toBeVisible() const stability = await probe.evaluate((tracker) => tracker.finish()) expect(stability).toEqual({ emptyFrames: 0, sameSessionNode: true, sameFileNode: true, }) }) test("titlebar session search shrinks with results while its field stays anchored through scrolling", async ({ page, }) => { await openPaletteWorkspace(page, true, [ { ...paletteSession, time: { ...paletteSession.time } }, ...Array.from({ length: 23 }, (_, index) => ({ ...paletteSession, id: `ses_compact_palette_${index}`, title: index === 0 ? "Compact keeper needle" : `Compact keeper result ${index}`, time: { created: 1700000000000 - index - 1, updated: 1700000000000 - index - 1 }, })), ]) await page.getByRole("banner").getByRole("button", { name: "Search sessions", exact: true }).click() const dialog = page.getByRole("dialog") const input = dialog.getByRole("combobox") const options = dialog.getByRole("option") const container = page.locator('[data-component="dialog-v2"] [data-slot="dialog-container"]') const geometry = () => container.evaluate((element) => ({ height: element.getBoundingClientRect().height, inputTop: element.querySelector('[role="combobox"]')!.getBoundingClientRect().top, })) await expect(input).toBeFocused() await expect(options).toHaveCount(10) // Hover waits for the field's opening animation to settle before taking the reference position. await input.hover() const initial = await geometry() expect(initial.height).toBeGreaterThan(200) await input.fill("Compact keeper needle") await expect(options).toHaveText([/Compact keeper needle/]) await expect.poll(async () => (await geometry()).height).toBeLessThan(initial.height - 100) await expect.poll(async () => (await geometry()).inputTop).toBeCloseTo(initial.inputTop, 0) await input.clear() await expect(options).toHaveCount(10) await expect.poll(geometry).toEqual({ height: expect.closeTo(initial.height, 0), inputTop: expect.closeTo(initial.inputTop, 0), }) // A broader query exposes more than the recent-session limit and must scroll below the field. await input.fill("Compact keeper") await expect(options).toHaveCount(23) const scroll = dialog.locator('[data-slot="search-list-scroll"]') await expect.poll(() => scroll.evaluate((element) => element.scrollHeight - element.clientHeight)).toBeGreaterThan(0) await expect.poll(async () => (await geometry()).height).toBeLessThanOrEqual(480) const last = dialog.getByRole("option", { name: /Compact keeper result 22/ }) await expect(last).not.toBeInViewport() await scroll.hover() await page.mouse.wheel(0, 2000) await expect(last).toBeInViewport() await expect.poll(() => scroll.evaluate((element) => element.scrollTop)).toBeGreaterThan(0) await expect.poll(async () => (await geometry()).inputTop).toBeCloseTo(initial.inputTop, 0) await expect(input).toBeFocused() })