diff --git a/packages/app/e2e/browser/provider-settings-refresh.spec.ts b/packages/app/e2e/browser/provider-settings-refresh.spec.ts index 961b518435..6c9739e894 100644 --- a/packages/app/e2e/browser/provider-settings-refresh.spec.ts +++ b/packages/app/e2e/browser/provider-settings-refresh.spec.ts @@ -1,5 +1,7 @@ +import path from "node:path"; import type { Locator } from "@playwright/test"; import { expect, test, type Page } from "../support/fixtures"; +import { clickNewChat, gotoWorkspace } from "../support/helpers/launcher"; import { expectComposerVisible } from "../support/helpers/composer"; import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; @@ -214,3 +216,66 @@ test.describe("provider settings overlay stack", () => { } }); }); + +async function openTimedOutProvider(page: Page, workspaceId: string, width: number) { + await gotoWorkspace(page, workspaceId); + await clickNewChat(page); + await expectComposerVisible(page); + await page.setViewportSize({ width, height: 900 }); + await page.getByRole("button", { name: /Select model/ }).click(); + if (width < 600) { + await page.getByTestId("agent-controls-model").click(); + } else { + await page.getByRole("button", { name: "Back", exact: true }).click(); + } + await page.getByTestId("model-provider-picker-timeout").click(); + await expect(page.getByText(/Timed out refreshing Pi timeout/)).toBeVisible(); +} + +async function expectErrorHasSideGutters(page: Page) { + const error = page.getByText(/Timed out refreshing Pi timeout/); + const bounds = await error.evaluate((element) => { + const text = element.getBoundingClientRect(); + const state = element.parentElement!.getBoundingClientRect(); + return { left: text.left - state.left, right: state.right - text.right }; + }); + expect(bounds.left, "error text stays inside the empty state's left edge").toBeGreaterThan(0); + expect(bounds.right, "error text stays inside the empty state's right edge").toBeGreaterThan(0); + await expect(page.getByRole("button", { name: "Retry", exact: true })).toBeVisible(); +} + +test.use({ + e2eDaemonConfig: { + version: 1, + agents: { + catalogRefreshTimeoutMs: 2000, + providers: { + "picker-timeout": { + extends: "pi", + label: "Pi timeout", + enabled: true, + command: [ + process.execPath, + path.resolve("e2e/fixtures/fake-pi-rpc.mjs"), + "--stall-model-discovery", + ], + }, + }, + }, + }, +}); + +test.describe("model discovery timeout", () => { + for (const width of [1200, 390]) { + test(`keeps a wrapped discovery error away from both picker edges at ${width}px`, async ({ + page, + withWorkspace, + }, testInfo) => { + await page.setViewportSize({ width: 1200, height: 900 }); + const workspace = await withWorkspace({ prefix: "model-error-spacing-" }); + await openTimedOutProvider(page, workspace.workspaceId, width); + await page.screenshot({ path: testInfo.outputPath("model-error.png") }); + await expectErrorHasSideGutters(page); + }); + } +}); diff --git a/packages/app/e2e/fixtures/fake-pi-rpc.mjs b/packages/app/e2e/fixtures/fake-pi-rpc.mjs index b6d156ddfc..372e29c44b 100644 --- a/packages/app/e2e/fixtures/fake-pi-rpc.mjs +++ b/packages/app/e2e/fixtures/fake-pi-rpc.mjs @@ -6,6 +6,7 @@ for await (const line of lines) { const command = JSON.parse(line); let data = {}; if (command.type === "get_available_models") { + if (process.argv.includes("--stall-model-discovery")) continue; data = { models: [ { diff --git a/packages/app/src/components/model-browser.tsx b/packages/app/src/components/model-browser.tsx index 17924b233d..2ca6b6a9c2 100644 --- a/packages/app/src/components/model-browser.tsx +++ b/packages/app/src/components/model-browser.tsx @@ -1692,7 +1692,7 @@ const styles = StyleSheet.create((theme) => ({ opacity: 0, }, emptyState: { - paddingVertical: theme.spacing[4], + padding: theme.spacing[4], alignItems: "center", gap: theme.spacing[2], },