From f48e2e56c95a7b96648176f1f783929dc4f33b37 Mon Sep 17 00:00:00 2001 From: Filip <34747899+neriousy@users.noreply.github.com> Date: Fri, 30 Jan 2026 19:04:02 +0100 Subject: test(app): change language test (#11295) --- packages/app/e2e/app/home.spec.ts | 21 ++++++ packages/app/e2e/app/navigation.spec.ts | 9 +++ packages/app/e2e/app/palette.spec.ts | 15 ++++ packages/app/e2e/app/server-default.spec.ts | 67 +++++++++++++++++ packages/app/e2e/app/session.spec.ts | 21 ++++++ packages/app/e2e/app/titlebar-history.spec.ts | 52 +++++++++++++ packages/app/e2e/context.spec.ts | 45 ----------- packages/app/e2e/file-open.spec.ts | 23 ------ packages/app/e2e/file-tree.spec.ts | 37 ---------- packages/app/e2e/file-viewer.spec.ts | 35 --------- packages/app/e2e/files/file-open.spec.ts | 23 ++++++ packages/app/e2e/files/file-tree.spec.ts | 37 ++++++++++ packages/app/e2e/files/file-viewer.spec.ts | 35 +++++++++ packages/app/e2e/home.spec.ts | 21 ------ packages/app/e2e/model-picker.spec.ts | 43 ----------- packages/app/e2e/models-visibility.spec.ts | 86 ---------------------- packages/app/e2e/models/model-picker.spec.ts | 43 +++++++++++ packages/app/e2e/models/models-visibility.spec.ts | 86 ++++++++++++++++++++++ packages/app/e2e/navigation.spec.ts | 9 --- packages/app/e2e/palette.spec.ts | 15 ---- packages/app/e2e/prompt-mention.spec.ts | 26 ------- packages/app/e2e/prompt-slash-open.spec.ts | 22 ------ packages/app/e2e/prompt.spec.ts | 62 ---------------- packages/app/e2e/prompt/context.spec.ts | 45 +++++++++++ packages/app/e2e/prompt/prompt-mention.spec.ts | 26 +++++++ packages/app/e2e/prompt/prompt-slash-open.spec.ts | 22 ++++++ packages/app/e2e/prompt/prompt.spec.ts | 62 ++++++++++++++++ packages/app/e2e/server-default.spec.ts | 67 ----------------- packages/app/e2e/session.spec.ts | 21 ------ packages/app/e2e/settings-providers.spec.ts | 56 -------------- packages/app/e2e/settings.spec.ts | 44 ----------- .../app/e2e/settings/settings-language.spec.ts | 39 ++++++++++ .../app/e2e/settings/settings-providers.spec.ts | 56 ++++++++++++++ packages/app/e2e/settings/settings.spec.ts | 44 +++++++++++ packages/app/e2e/sidebar-session-links.spec.ts | 61 --------------- packages/app/e2e/sidebar.spec.ts | 21 ------ .../app/e2e/sidebar/sidebar-session-links.spec.ts | 35 +++++++++ packages/app/e2e/sidebar/sidebar.spec.ts | 21 ++++++ packages/app/e2e/terminal-init.spec.ts | 25 ------- packages/app/e2e/terminal.spec.ts | 16 ---- packages/app/e2e/terminal/terminal-init.spec.ts | 25 +++++++ packages/app/e2e/terminal/terminal.spec.ts | 16 ++++ packages/app/e2e/titlebar-history.spec.ts | 52 ------------- packages/app/e2e/utils.ts | 2 + packages/app/src/components/settings-general.tsx | 1 + 45 files changed, 803 insertions(+), 787 deletions(-) create mode 100644 packages/app/e2e/app/home.spec.ts create mode 100644 packages/app/e2e/app/navigation.spec.ts create mode 100644 packages/app/e2e/app/palette.spec.ts create mode 100644 packages/app/e2e/app/server-default.spec.ts create mode 100644 packages/app/e2e/app/session.spec.ts create mode 100644 packages/app/e2e/app/titlebar-history.spec.ts delete mode 100644 packages/app/e2e/context.spec.ts delete mode 100644 packages/app/e2e/file-open.spec.ts delete mode 100644 packages/app/e2e/file-tree.spec.ts delete mode 100644 packages/app/e2e/file-viewer.spec.ts create mode 100644 packages/app/e2e/files/file-open.spec.ts create mode 100644 packages/app/e2e/files/file-tree.spec.ts create mode 100644 packages/app/e2e/files/file-viewer.spec.ts delete mode 100644 packages/app/e2e/home.spec.ts delete mode 100644 packages/app/e2e/model-picker.spec.ts delete mode 100644 packages/app/e2e/models-visibility.spec.ts create mode 100644 packages/app/e2e/models/model-picker.spec.ts create mode 100644 packages/app/e2e/models/models-visibility.spec.ts delete mode 100644 packages/app/e2e/navigation.spec.ts delete mode 100644 packages/app/e2e/palette.spec.ts delete mode 100644 packages/app/e2e/prompt-mention.spec.ts delete mode 100644 packages/app/e2e/prompt-slash-open.spec.ts delete mode 100644 packages/app/e2e/prompt.spec.ts create mode 100644 packages/app/e2e/prompt/context.spec.ts create mode 100644 packages/app/e2e/prompt/prompt-mention.spec.ts create mode 100644 packages/app/e2e/prompt/prompt-slash-open.spec.ts create mode 100644 packages/app/e2e/prompt/prompt.spec.ts delete mode 100644 packages/app/e2e/server-default.spec.ts delete mode 100644 packages/app/e2e/session.spec.ts delete mode 100644 packages/app/e2e/settings-providers.spec.ts delete mode 100644 packages/app/e2e/settings.spec.ts create mode 100644 packages/app/e2e/settings/settings-language.spec.ts create mode 100644 packages/app/e2e/settings/settings-providers.spec.ts create mode 100644 packages/app/e2e/settings/settings.spec.ts delete mode 100644 packages/app/e2e/sidebar-session-links.spec.ts delete mode 100644 packages/app/e2e/sidebar.spec.ts create mode 100644 packages/app/e2e/sidebar/sidebar-session-links.spec.ts create mode 100644 packages/app/e2e/sidebar/sidebar.spec.ts delete mode 100644 packages/app/e2e/terminal-init.spec.ts delete mode 100644 packages/app/e2e/terminal.spec.ts create mode 100644 packages/app/e2e/terminal/terminal-init.spec.ts create mode 100644 packages/app/e2e/terminal/terminal.spec.ts delete mode 100644 packages/app/e2e/titlebar-history.spec.ts diff --git a/packages/app/e2e/app/home.spec.ts b/packages/app/e2e/app/home.spec.ts new file mode 100644 index 000000000..f21dc40ec --- /dev/null +++ b/packages/app/e2e/app/home.spec.ts @@ -0,0 +1,21 @@ +import { test, expect } from "../fixtures" +import { serverName } from "../utils" + +test("home renders and shows core entrypoints", async ({ page }) => { + await page.goto("/") + + await expect(page.getByRole("button", { name: "Open project" }).first()).toBeVisible() + await expect(page.getByRole("button", { name: serverName })).toBeVisible() +}) + +test("server picker dialog opens from home", async ({ page }) => { + await page.goto("/") + + const trigger = page.getByRole("button", { name: serverName }) + await expect(trigger).toBeVisible() + await trigger.click() + + const dialog = page.getByRole("dialog") + await expect(dialog).toBeVisible() + await expect(dialog.getByRole("textbox").first()).toBeVisible() +}) diff --git a/packages/app/e2e/app/navigation.spec.ts b/packages/app/e2e/app/navigation.spec.ts new file mode 100644 index 000000000..0812ea018 --- /dev/null +++ b/packages/app/e2e/app/navigation.spec.ts @@ -0,0 +1,9 @@ +import { test, expect } from "../fixtures" +import { dirPath, promptSelector } from "../utils" + +test("project route redirects to /session", async ({ page, directory, slug }) => { + await page.goto(dirPath(directory)) + + await expect(page).toHaveURL(new RegExp(`/${slug}/session`)) + await expect(page.locator(promptSelector)).toBeVisible() +}) diff --git a/packages/app/e2e/app/palette.spec.ts b/packages/app/e2e/app/palette.spec.ts new file mode 100644 index 000000000..264b463bb --- /dev/null +++ b/packages/app/e2e/app/palette.spec.ts @@ -0,0 +1,15 @@ +import { test, expect } from "../fixtures" +import { modKey } from "../utils" + +test("search palette opens and closes", async ({ page, gotoSession }) => { + await gotoSession() + + await page.keyboard.press(`${modKey}+P`) + + const dialog = page.getByRole("dialog") + await expect(dialog).toBeVisible() + await expect(dialog.getByRole("textbox").first()).toBeVisible() + + await page.keyboard.press("Escape") + await expect(dialog).toHaveCount(0) +}) diff --git a/packages/app/e2e/app/server-default.spec.ts b/packages/app/e2e/app/server-default.spec.ts new file mode 100644 index 000000000..6f44ded1a --- /dev/null +++ b/packages/app/e2e/app/server-default.spec.ts @@ -0,0 +1,67 @@ +import { test, expect } from "../fixtures" +import { serverName, serverUrl } from "../utils" + +const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl" + +test("can set a default server on web", async ({ page, gotoSession }) => { + await page.addInitScript((key: string) => { + try { + localStorage.removeItem(key) + } catch { + return + } + }, DEFAULT_SERVER_URL_KEY) + + await gotoSession() + + const status = page.getByRole("button", { name: "Status" }) + await expect(status).toBeVisible() + const popover = page.locator('[data-component="popover-content"]').filter({ hasText: "Manage servers" }) + + const ensurePopoverOpen = async () => { + if (await popover.isVisible()) return + await status.click() + await expect(popover).toBeVisible() + } + + await ensurePopoverOpen() + await popover.getByRole("button", { name: "Manage servers" }).click() + + const dialog = page.getByRole("dialog") + await expect(dialog).toBeVisible() + + const row = dialog.locator('[data-slot="list-item"]').filter({ hasText: serverName }).first() + await expect(row).toBeVisible() + + const menu = row.locator('[data-component="icon-button"]').last() + await menu.click() + await page.getByRole("menuitem", { name: "Set as default" }).click() + + await expect.poll(() => page.evaluate((key) => localStorage.getItem(key), DEFAULT_SERVER_URL_KEY)).toBe(serverUrl) + await expect(row.getByText("Default", { exact: true })).toBeVisible() + + await page.keyboard.press("Escape") + const closed = await dialog + .waitFor({ state: "detached", timeout: 1500 }) + .then(() => true) + .catch(() => false) + + if (!closed) { + await page.keyboard.press("Escape") + const closedSecond = await dialog + .waitFor({ state: "detached", timeout: 1500 }) + .then(() => true) + .catch(() => false) + + if (!closedSecond) { + await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } }) + await expect(dialog).toHaveCount(0) + } + } + + await ensurePopoverOpen() + + const serverRow = popover.locator("button").filter({ hasText: serverName }).first() + await expect(serverRow).toBeVisible() + await expect(serverRow.getByText("Default", { exact: true })).toBeVisible() +}) diff --git a/packages/app/e2e/app/session.spec.ts b/packages/app/e2e/app/session.spec.ts new file mode 100644 index 000000000..8d605f0c3 --- /dev/null +++ b/packages/app/e2e/app/session.spec.ts @@ -0,0 +1,21 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../utils" + +test("can open an existing session and type into the prompt", async ({ page, sdk, gotoSession }) => { + const title = `e2e smoke ${Date.now()}` + const created = await sdk.session.create({ title }).then((r) => r.data) + + if (!created?.id) throw new Error("Session create did not return an id") + const sessionID = created.id + + try { + await gotoSession(sessionID) + + const prompt = page.locator(promptSelector) + await prompt.click() + await page.keyboard.type("hello from e2e") + await expect(prompt).toContainText("hello from e2e") + } finally { + await sdk.session.delete({ sessionID }).catch(() => undefined) + } +}) diff --git a/packages/app/e2e/app/titlebar-history.spec.ts b/packages/app/e2e/app/titlebar-history.spec.ts new file mode 100644 index 000000000..649e5e0dc --- /dev/null +++ b/packages/app/e2e/app/titlebar-history.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from "../fixtures" +import { modKey, promptSelector } from "../utils" + +test("titlebar back/forward navigates between sessions", async ({ page, slug, sdk, gotoSession }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + + const stamp = Date.now() + const one = await sdk.session.create({ title: `e2e titlebar history 1 ${stamp}` }).then((r) => r.data) + const two = await sdk.session.create({ title: `e2e titlebar history 2 ${stamp}` }).then((r) => r.data) + + if (!one?.id) throw new Error("Session create did not return an id") + if (!two?.id) throw new Error("Session create did not return an id") + + try { + await gotoSession(one.id) + + const main = page.locator("main") + const collapsed = ((await main.getAttribute("class")) ?? "").includes("xl:border-l") + if (collapsed) { + await page.keyboard.press(`${modKey}+B`) + await expect(main).not.toHaveClass(/xl:border-l/) + } + + const link = page.locator(`[data-session-id="${two.id}"] a`).first() + await expect(link).toBeVisible() + await link.scrollIntoViewIfNeeded() + await link.click() + + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`)) + await expect(page.locator(promptSelector)).toBeVisible() + + const back = page.getByRole("button", { name: "Back" }) + const forward = page.getByRole("button", { name: "Forward" }) + + await expect(back).toBeVisible() + await expect(back).toBeEnabled() + await back.click() + + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${one.id}(?:\\?|#|$)`)) + await expect(page.locator(promptSelector)).toBeVisible() + + await expect(forward).toBeVisible() + await expect(forward).toBeEnabled() + await forward.click() + + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`)) + await expect(page.locator(promptSelector)).toBeVisible() + } finally { + await sdk.session.delete({ sessionID: one.id }).catch(() => undefined) + await sdk.session.delete({ sessionID: two.id }).catch(() => undefined) + } +}) diff --git a/packages/app/e2e/context.spec.ts b/packages/app/e2e/context.spec.ts deleted file mode 100644 index beabd2eb7..000000000 --- a/packages/app/e2e/context.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { test, expect } from "./fixtures" -import { promptSelector } from "./utils" - -test("context panel can be opened from the prompt", async ({ page, sdk, gotoSession }) => { - const title = `e2e smoke context ${Date.now()}` - const created = await sdk.session.create({ title }).then((r) => r.data) - - if (!created?.id) throw new Error("Session create did not return an id") - const sessionID = created.id - - try { - await sdk.session.promptAsync({ - sessionID, - noReply: true, - parts: [ - { - type: "text", - text: "seed context", - }, - ], - }) - - await expect - .poll(async () => { - const messages = await sdk.session.messages({ sessionID, limit: 1 }).then((r) => r.data ?? []) - return messages.length - }) - .toBeGreaterThan(0) - - await gotoSession(sessionID) - - const contextButton = page - .locator('[data-component="button"]') - .filter({ has: page.locator('[data-component="progress-circle"]').first() }) - .first() - - await expect(contextButton).toBeVisible() - await contextButton.click() - - const tabs = page.locator('[data-component="tabs"][data-variant="normal"]') - await expect(tabs.getByRole("tab", { name: "Context" })).toBeVisible() - } finally { - await sdk.session.delete({ sessionID }).catch(() => undefined) - } -}) diff --git a/packages/app/e2e/file-open.spec.ts b/packages/app/e2e/file-open.spec.ts deleted file mode 100644 index fb7104b6b..000000000 --- a/packages/app/e2e/file-open.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { test, expect } from "./fixtures" -import { modKey } from "./utils" - -test("can open a file tab from the search palette", async ({ page, gotoSession }) => { - await gotoSession() - - await page.keyboard.press(`${modKey}+P`) - - const dialog = page.getByRole("dialog") - await expect(dialog).toBeVisible() - - const input = dialog.getByRole("textbox").first() - await input.fill("package.json") - - const fileItem = dialog.locator('[data-slot="list-item"][data-key^="file:"]').first() - await expect(fileItem).toBeVisible() - await fileItem.click() - - await expect(dialog).toHaveCount(0) - - const tabs = page.locator('[data-component="tabs"][data-variant="normal"]') - await expect(tabs.locator('[data-slot="tabs-trigger"]').first()).toBeVisible() -}) diff --git a/packages/app/e2e/file-tree.spec.ts b/packages/app/e2e/file-tree.spec.ts deleted file mode 100644 index 0b04eb246..000000000 --- a/packages/app/e2e/file-tree.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { test, expect } from "./fixtures" - -test.skip("file tree can expand folders and open a file", async ({ page, gotoSession }) => { - await gotoSession() - - const toggle = page.getByRole("button", { name: "Toggle file tree" }) - const treeTabs = page.locator('[data-component="tabs"][data-variant="pill"][data-scope="filetree"]') - - if ((await toggle.getAttribute("aria-expanded")) !== "true") await toggle.click() - await expect(treeTabs).toBeVisible() - - await treeTabs.locator('[data-slot="tabs-trigger"]').nth(1).click() - - const node = (name: string) => treeTabs.getByRole("button", { name, exact: true }) - - await expect(node("packages")).toBeVisible() - await node("packages").click() - - await expect(node("app")).toBeVisible() - await node("app").click() - - await expect(node("src")).toBeVisible() - await node("src").click() - - await expect(node("components")).toBeVisible() - await node("components").click() - - await expect(node("file-tree.tsx")).toBeVisible() - await node("file-tree.tsx").click() - - const tab = page.getByRole("tab", { name: "file-tree.tsx" }) - await expect(tab).toBeVisible() - await tab.click() - - const code = page.locator('[data-component="code"]').first() - await expect(code.getByText("export default function FileTree")).toBeVisible() -}) diff --git a/packages/app/e2e/file-viewer.spec.ts b/packages/app/e2e/file-viewer.spec.ts deleted file mode 100644 index 1e0f8a6f2..000000000 --- a/packages/app/e2e/file-viewer.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { test, expect } from "./fixtures" -import { modKey } from "./utils" - -test("smoke file viewer renders real file content", async ({ page, gotoSession }) => { - await gotoSession() - - const sep = process.platform === "win32" ? "\\" : "/" - const file = ["packages", "app", "package.json"].join(sep) - - await page.keyboard.press(`${modKey}+P`) - - const dialog = page.getByRole("dialog") - await expect(dialog).toBeVisible() - - const input = dialog.getByRole("textbox").first() - await input.fill(file) - - const fileItem = dialog - .locator( - '[data-slot="list-item"][data-key^="file:"][data-key*="packages"][data-key*="app"][data-key$="package.json"]', - ) - .first() - await expect(fileItem).toBeVisible() - await fileItem.click() - - await expect(dialog).toHaveCount(0) - - const tab = page.getByRole("tab", { name: "package.json" }) - await expect(tab).toBeVisible() - await tab.click() - - const code = page.locator('[data-component="code"]').first() - await expect(code).toBeVisible() - await expect(code.getByText("@opencode-ai/app")).toBeVisible() -}) diff --git a/packages/app/e2e/files/file-open.spec.ts b/packages/app/e2e/files/file-open.spec.ts new file mode 100644 index 000000000..e384f0b0d --- /dev/null +++ b/packages/app/e2e/files/file-open.spec.ts @@ -0,0 +1,23 @@ +import { test, expect } from "../fixtures" +import { modKey } from "../utils" + +test("can open a file tab from the search palette", async ({ page, gotoSession }) => { + await gotoSession() + + await page.keyboard.press(`${modKey}+P`) + + const dialog = page.getByRole("dialog") + await expect(dialog).toBeVisible() + + const input = dialog.getByRole("textbox").first() + await input.fill("package.json") + + const fileItem = dialog.locator('[data-slot="list-item"][data-key^="file:"]').first() + await expect(fileItem).toBeVisible() + await fileItem.click() + + await expect(dialog).toHaveCount(0) + + const tabs = page.locator('[data-component="tabs"][data-variant="normal"]') + await expect(tabs.locator('[data-slot="tabs-trigger"]').first()).toBeVisible() +}) diff --git a/packages/app/e2e/files/file-tree.spec.ts b/packages/app/e2e/files/file-tree.spec.ts new file mode 100644 index 000000000..844da1b32 --- /dev/null +++ b/packages/app/e2e/files/file-tree.spec.ts @@ -0,0 +1,37 @@ +import { test, expect } from "../fixtures" + +test.skip("file tree can expand folders and open a file", async ({ page, gotoSession }) => { + await gotoSession() + + const toggle = page.getByRole("button", { name: "Toggle file tree" }) + const treeTabs = page.locator('[data-component="tabs"][data-variant="pill"][data-scope="filetree"]') + + if ((await toggle.getAttribute("aria-expanded")) !== "true") await toggle.click() + await expect(treeTabs).toBeVisible() + + await treeTabs.locator('[data-slot="tabs-trigger"]').nth(1).click() + + const node = (name: string) => treeTabs.getByRole("button", { name, exact: true }) + + await expect(node("packages")).toBeVisible() + await node("packages").click() + + await expect(node("app")).toBeVisible() + await node("app").click() + + await expect(node("src")).toBeVisible() + await node("src").click() + + await expect(node("components")).toBeVisible() + await node("components").click() + + await expect(node("file-tree.tsx")).toBeVisible() + await node("file-tree.tsx").click() + + const tab = page.getByRole("tab", { name: "file-tree.tsx" }) + await expect(tab).toBeVisible() + await tab.click() + + const code = page.locator('[data-component="code"]').first() + await expect(code.getByText("export default function FileTree")).toBeVisible() +}) diff --git a/packages/app/e2e/files/file-viewer.spec.ts b/packages/app/e2e/files/file-viewer.spec.ts new file mode 100644 index 000000000..bed6d1d36 --- /dev/null +++ b/packages/app/e2e/files/file-viewer.spec.ts @@ -0,0 +1,35 @@ +import { test, expect } from "../fixtures" +import { modKey } from "../utils" + +test("smoke file viewer renders real file content", async ({ page, gotoSession }) => { + await gotoSession() + + const sep = process.platform === "win32" ? "\\" : "/" + const file = ["packages", "app", "package.json"].join(sep) + + await page.keyboard.press(`${modKey}+P`) + + const dialog = page.getByRole("dialog") + await expect(dialog).toBeVisible() + + const input = dialog.getByRole("textbox").first() + await input.fill(file) + + const fileItem = dialog + .locator( + '[data-slot="list-item"][data-key^="file:"][data-key*="packages"][data-key*="app"][data-key$="package.json"]', + ) + .first() + await expect(fileItem).toBeVisible() + await fileItem.click() + + await expect(dialog).toHaveCount(0) + + const tab = page.getByRole("tab", { name: "package.json" }) + await expect(tab).toBeVisible() + await tab.click() + + const code = page.locator('[data-component="code"]').first() + await expect(code).toBeVisible() + await expect(code.getByText("@opencode-ai/app")).toBeVisible() +}) diff --git a/packages/app/e2e/home.spec.ts b/packages/app/e2e/home.spec.ts deleted file mode 100644 index c6fb0e3b0..000000000 --- a/packages/app/e2e/home.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { test, expect } from "./fixtures" -import { serverName } from "./utils" - -test("home renders and shows core entrypoints", async ({ page }) => { - await page.goto("/") - - await expect(page.getByRole("button", { name: "Open project" }).first()).toBeVisible() - await expect(page.getByRole("button", { name: serverName })).toBeVisible() -}) - -test("server picker dialog opens from home", async ({ page }) => { - await page.goto("/") - - const trigger = page.getByRole("button", { name: serverName }) - await expect(trigger).toBeVisible() - await trigger.click() - - const dialog = page.getByRole("dialog") - await expect(dialog).toBeVisible() - await expect(dialog.getByRole("textbox").first()).toBeVisible() -}) diff --git a/packages/app/e2e/model-picker.spec.ts b/packages/app/e2e/model-picker.spec.ts deleted file mode 100644 index 9e64b3dfb..000000000 --- a/packages/app/e2e/model-picker.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { test, expect } from "./fixtures" -import { promptSelector } from "./utils" - -test("smoke model selection updates prompt footer", async ({ page, gotoSession }) => { - await gotoSession() - - await page.locator(promptSelector).click() - await page.keyboard.type("/model") - - const command = page.locator('[data-slash-id="model.choose"]') - await expect(command).toBeVisible() - await command.hover() - - await page.keyboard.press("Enter") - - const dialog = page.getByRole("dialog") - await expect(dialog).toBeVisible() - - const input = dialog.getByRole("textbox").first() - - const selected = dialog.locator('[data-slot="list-item"][data-selected="true"]').first() - await expect(selected).toBeVisible() - - const other = dialog.locator('[data-slot="list-item"]:not([data-selected="true"])').first() - const target = (await other.count()) > 0 ? other : selected - - const key = await target.getAttribute("data-key") - if (!key) throw new Error("Failed to resolve model key from list item") - - const name = (await target.locator("span").first().innerText()).trim() - const model = key.split(":").slice(1).join(":") - - await input.fill(model) - - const item = dialog.locator(`[data-slot="list-item"][data-key="${key}"]`) - await expect(item).toBeVisible() - await item.click() - - await expect(dialog).toHaveCount(0) - - const form = page.locator(promptSelector).locator("xpath=ancestor::form[1]") - await expect(form.locator('[data-component="button"]').filter({ hasText: name }).first()).toBeVisible() -}) diff --git a/packages/app/e2e/models-visibility.spec.ts b/packages/app/e2e/models-visibility.spec.ts deleted file mode 100644 index 680ba96a3..000000000 --- a/packages/app/e2e/models-visibility.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { test, expect } from "./fixtures" -import { modKey, promptSelector } from "./utils" - -test("hiding a model removes it from the model picker", async ({ page, gotoSession }) => { - await gotoSession() - - await page.locator(promptSelector).click() - await page.keyboard.type("/model") - - const command = page.locator('[data-slash-id="model.choose"]') - await expect(command).toBeVisible() - await command.hover() - await page.keyboard.press("Enter") - - const picker = page.getByRole("dialog") - await expect(picker).toBeVisible() - - const target = picker.locator('[data-slot="list-item"]').first() - await expect(target).toBeVisible() - - const key = await target.getAttribute("data-key") - if (!key) throw new Error("Failed to resolve model key from list item") - - const name = (await target.locator("span").first().innerText()).trim() - if (!name) throw new Error("Failed to resolve model name from list item") - - await page.keyboard.press("Escape") - await expect(picker).toHaveCount(0) - - const settings = page.getByRole("dialog") - - await page.keyboard.press(`${modKey}+Comma`).catch(() => undefined) - const opened = await settings - .waitFor({ state: "visible", timeout: 3000 }) - .then(() => true) - .catch(() => false) - - if (!opened) { - await page.getByRole("button", { name: "Settings" }).first().click() - await expect(settings).toBeVisible() - } - - await settings.getByRole("tab", { name: "Models" }).click() - const search = settings.getByPlaceholder("Search models") - await expect(search).toBeVisible() - await search.fill(name) - - const toggle = settings.locator('[data-component="switch"]').filter({ hasText: name }).first() - const input = toggle.locator('[data-slot="switch-input"]') - await expect(toggle).toBeVisible() - await expect(input).toHaveAttribute("aria-checked", "true") - await toggle.locator('[data-slot="switch-control"]').click() - await expect(input).toHaveAttribute("aria-checked", "false") - - await page.keyboard.press("Escape") - const closed = await settings - .waitFor({ state: "detached", timeout: 1500 }) - .then(() => true) - .catch(() => false) - if (!closed) { - await page.keyboard.press("Escape") - const closedSecond = await settings - .waitFor({ state: "detached", timeout: 1500 }) - .then(() => true) - .catch(() => false) - if (!closedSecond) { - await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } }) - await expect(settings).toHaveCount(0) - } - } - - await page.locator(promptSelector).click() - await page.keyboard.type("/model") - await expect(command).toBeVisible() - await command.hover() - await page.keyboard.press("Enter") - - const pickerAgain = page.getByRole("dialog") - await expect(pickerAgain).toBeVisible() - await expect(pickerAgain.locator('[data-slot="list-item"]').first()).toBeVisible() - - await expect(pickerAgain.locator(`[data-slot="list-item"][data-key="${key}"]`)).toHaveCount(0) - - await page.keyboard.press("Escape") - await expect(pickerAgain).toHaveCount(0) -}) diff --git a/packages/app/e2e/models/model-picker.spec.ts b/packages/app/e2e/models/model-picker.spec.ts new file mode 100644 index 000000000..a0c70aabe --- /dev/null +++ b/packages/app/e2e/models/model-picker.spec.ts @@ -0,0 +1,43 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../utils" + +test("smoke model selection updates prompt footer", async ({ page, gotoSession }) => { + await gotoSession() + + await page.locator(promptSelector).click() + await page.keyboard.type("/model") + + const command = page.locator('[data-slash-id="model.choose"]') + await expect(command).toBeVisible() + await command.hover() + + await page.keyboard.press("Enter") + + const dialog = page.getByRole("dialog") + await expect(dialog).toBeVisible() + + const input = dialog.getByRole("textbox").first() + + const selected = dialog.locator('[data-slot="list-item"][data-selected="true"]').first() + await expect(selected).toBeVisible() + + const other = dialog.locator('[data-slot="list-item"]:not([data-selected="true"])').first() + const target = (await other.count()) > 0 ? other : selected + + const key = await target.getAttribute("data-key") + if (!key) throw new Error("Failed to resolve model key from list item") + + const name = (await target.locator("span").first().innerText()).trim() + const model = key.split(":").slice(1).join(":") + + await input.fill(model) + + const item = dialog.locator(`[data-slot="list-item"][data-key="${key}"]`) + await expect(item).toBeVisible() + await item.click() + + await expect(dialog).toHaveCount(0) + + const form = page.locator(promptSelector).locator("xpath=ancestor::form[1]") + await expect(form.locator('[data-component="button"]').filter({ hasText: name }).first()).toBeVisible() +}) diff --git a/packages/app/e2e/models/models-visibility.spec.ts b/packages/app/e2e/models/models-visibility.spec.ts new file mode 100644 index 000000000..0db7580c2 --- /dev/null +++ b/packages/app/e2e/models/models-visibility.spec.ts @@ -0,0 +1,86 @@ +import { test, expect } from "../fixtures" +import { modKey, promptSelector } from "../utils" + +test("hiding a model removes it from the model picker", async ({ page, gotoSession }) => { + await gotoSession() + + await page.locator(promptSelector).click() + await page.keyboard.type("/model") + + const command = page.locator('[data-slash-id="model.choose"]') + await expect(command).toBeVisible() + await command.hover() + await page.keyboard.press("Enter") + + const picker = page.getByRole("dialog") + await expect(picker).toBeVisible() + + const target = picker.locator('[data-slot="list-item"]').first() + await expect(target).toBeVisible() + + const key = await target.getAttribute("data-key") + if (!key) throw new Error("Failed to resolve model key from list item") + + const name = (await target.locator("span").first().innerText()).trim() + if (!name) throw new Error("Failed to resolve model name from list item") + + await page.keyboard.press("Escape") + await expect(picker).toHaveCount(0) + + const settings = page.getByRole("dialog") + + await page.keyboard.press(`${modKey}+Comma`).catch(() => undefined) + const opened = await settings + .waitFor({ state: "visible", timeout: 3000 }) + .then(() => true) + .catch(() => false) + + if (!opened) { + await page.getByRole("button", { name: "Settings" }).first().click() + await expect(settings).toBeVisible() + } + + await settings.getByRole("tab", { name: "Models" }).click() + const search = settings.getByPlaceholder("Search models") + await expect(search).toBeVisible() + await search.fill(name) + + const toggle = settings.locator('[data-component="switch"]').filter({ hasText: name }).first() + const input = toggle.locator('[data-slot="switch-input"]') + await expect(toggle).toBeVisible() + await expect(input).toHaveAttribute("aria-checked", "true") + await toggle.locator('[data-slot="switch-control"]').click() + await expect(input).toHaveAttribute("aria-checked", "false") + + await page.keyboard.press("Escape") + const closed = await settings + .waitFor({ state: "detached", timeout: 1500 }) + .then(() => true) + .catch(() => false) + if (!closed) { + await page.keyboard.press("Escape") + const closedSecond = await settings + .waitFor({ state: "detached", timeout: 1500 }) + .then(() => true) + .catch(() => false) + if (!closedSecond) { + await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } }) + await expect(settings).toHaveCount(0) + } + } + + await page.locator(promptSelector).click() + await page.keyboard.type("/model") + await expect(command).toBeVisible() + await command.hover() + await page.keyboard.press("Enter") + + const pickerAgain = page.getByRole("dialog") + await expect(pickerAgain).toBeVisible() + await expect(pickerAgain.locator('[data-slot="list-item"]').first()).toBeVisible() + + await expect(pickerAgain.locator(`[data-slot="list-item"][data-key="${key}"]`)).toHaveCount(0) + + await page.keyboard.press("Escape") + await expect(pickerAgain).toHaveCount(0) +}) diff --git a/packages/app/e2e/navigation.spec.ts b/packages/app/e2e/navigation.spec.ts deleted file mode 100644 index 76923af6e..000000000 --- a/packages/app/e2e/navigation.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { test, expect } from "./fixtures" -import { dirPath, promptSelector } from "./utils" - -test("project route redirects to /session", async ({ page, directory, slug }) => { - await page.goto(dirPath(directory)) - - await expect(page).toHaveURL(new RegExp(`/${slug}/session`)) - await expect(page.locator(promptSelector)).toBeVisible() -}) diff --git a/packages/app/e2e/palette.spec.ts b/packages/app/e2e/palette.spec.ts deleted file mode 100644 index 617c55ac1..000000000 --- a/packages/app/e2e/palette.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { test, expect } from "./fixtures" -import { modKey } from "./utils" - -test("search palette opens and closes", async ({ page, gotoSession }) => { - await gotoSession() - - await page.keyboard.press(`${modKey}+P`) - - const dialog = page.getByRole("dialog") - await expect(dialog).toBeVisible() - await expect(dialog.getByRole("textbox").first()).toBeVisible() - - await page.keyboard.press("Escape") - await expect(dialog).toHaveCount(0) -}) diff --git a/packages/app/e2e/prompt-mention.spec.ts b/packages/app/e2e/prompt-mention.spec.ts deleted file mode 100644 index 113b8465f..000000000 --- a/packages/app/e2e/prompt-mention.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { test, expect } from "./fixtures" -import { promptSelector } from "./utils" - -test("smoke @mention inserts file pill token", async ({ page, gotoSession }) => { - await gotoSession() - - await page.locator(promptSelector).click() - const sep = process.platform === "win32" ? "\\" : "/" - const file = ["packages", "app", "package.json"].join(sep) - const filePattern = /packages[\\/]+app[\\/]+\s*package\.json/ - - await page.keyboard.type(`@${file}`) - - const suggestion = page.getByRole("button", { name: filePattern }).first() - await expect(suggestion).toBeVisible() - await suggestion.hover() - - await page.keyboard.press("Tab") - - const pill = page.locator(`${promptSelector} [data-type="file"]`).first() - await expect(pill).toBeVisible() - await expect(pill).toHaveAttribute("data-path", filePattern) - - await page.keyboard.type(" ok") - await expect(page.locator(promptSelector)).toContainText("ok") -}) diff --git a/packages/app/e2e/prompt-slash-open.spec.ts b/packages/app/e2e/prompt-slash-open.spec.ts deleted file mode 100644 index 3c29d405c..000000000 --- a/packages/app/e2e/prompt-slash-open.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { test, expect } from "./fixtures" -import { promptSelector } from "./utils" - -test("smoke /open opens file picker dialog", async ({ page, gotoSession }) => { - await gotoSession() - - await page.locator(promptSelector).click() - await page.keyboard.type("/open") - - const command = page.locator('[data-slash-id="file.open"]') - await expect(command).toBeVisible() - await command.hover() - - await page.keyboard.press("Enter") - - const dialog = page.getByRole("dialog") - await expect(dialog).toBeVisible() - await expect(dialog.getByRole("textbox").first()).toBeVisible() - - await page.keyboard.press("Escape") - await expect(dialog).toHaveCount(0) -}) diff --git a/packages/app/e2e/prompt.spec.ts b/packages/app/e2e/prompt.spec.ts deleted file mode 100644 index 3e5892ce8..000000000 --- a/packages/app/e2e/prompt.spec.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { test, expect } from "./fixtures" -import { promptSelector } from "./utils" - -function sessionIDFromUrl(url: string) { - const match = /\/session\/([^/?#]+)/.exec(url) - return match?.[1] -} - -test("can send a prompt and receive a reply", async ({ page, sdk, gotoSession }) => { - test.setTimeout(120_000) - - const pageErrors: string[] = [] - const onPageError = (err: Error) => { - pageErrors.push(err.message) - } - page.on("pageerror", onPageError) - - await gotoSession() - - const token = `E2E_OK_${Date.now()}` - - const prompt = page.locator(promptSelector) - await prompt.click() - await page.keyboard.type(`Reply with exactly: ${token}`) - await page.keyboard.press("Enter") - - await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 }) - - const sessionID = (() => { - const id = sessionIDFromUrl(page.url()) - if (!id) throw new Error(`Failed to parse session id from url: ${page.url()}`) - return id - })() - - try { - await expect - .poll( - async () => { - const messages = await sdk.session.messages({ sessionID, limit: 50 }).then((r) => r.data ?? []) - return messages - .filter((m) => m.info.role === "assistant") - .flatMap((m) => m.parts) - .filter((p) => p.type === "text") - .map((p) => p.text) - .join("\n") - }, - { timeout: 90_000 }, - ) - - .toContain(token) - - const reply = page.locator('[data-slot="session-turn-summary-section"]').filter({ hasText: token }).first() - await expect(reply).toBeVisible({ timeout: 90_000 }) - } finally { - page.off("pageerror", onPageError) - await sdk.session.delete({ sessionID }).catch(() => undefined) - } - - if (pageErrors.length > 0) { - throw new Error(`Page error(s):\n${pageErrors.join("\n")}`) - } -}) diff --git a/packages/app/e2e/prompt/context.spec.ts b/packages/app/e2e/prompt/context.spec.ts new file mode 100644 index 000000000..f0f3f073a --- /dev/null +++ b/packages/app/e2e/prompt/context.spec.ts @@ -0,0 +1,45 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../utils" + +test("context panel can be opened from the prompt", async ({ page, sdk, gotoSession }) => { + const title = `e2e smoke context ${Date.now()}` + const created = await sdk.session.create({ title }).then((r) => r.data) + + if (!created?.id) throw new Error("Session create did not return an id") + const sessionID = created.id + + try { + await sdk.session.promptAsync({ + sessionID, + noReply: true, + parts: [ + { + type: "text", + text: "seed context", + }, + ], + }) + + await expect + .poll(async () => { + const messages = await sdk.session.messages({ sessionID, limit: 1 }).then((r) => r.data ?? []) + return messages.length + }) + .toBeGreaterThan(0) + + await gotoSession(sessionID) + + const contextButton = page + .locator('[data-component="button"]') + .filter({ has: page.locator('[data-component="progress-circle"]').first() }) + .first() + + await expect(contextButton).toBeVisible() + await contextButton.click() + + const tabs = page.locator('[data-component="tabs"][data-variant="normal"]') + await expect(tabs.getByRole("tab", { name: "Context" })).toBeVisible() + } finally { + await sdk.session.delete({ sessionID }).catch(() => undefined) + } +}) diff --git a/packages/app/e2e/prompt/prompt-mention.spec.ts b/packages/app/e2e/prompt/prompt-mention.spec.ts new file mode 100644 index 000000000..85acb4c28 --- /dev/null +++ b/packages/app/e2e/prompt/prompt-mention.spec.ts @@ -0,0 +1,26 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../utils" + +test("smoke @mention inserts file pill token", async ({ page, gotoSession }) => { + await gotoSession() + + await page.locator(promptSelector).click() + const sep = process.platform === "win32" ? "\\" : "/" + const file = ["packages", "app", "package.json"].join(sep) + const filePattern = /packages[\\/]+app[\\/]+\s*package\.json/ + + await page.keyboard.type(`@${file}`) + + const suggestion = page.getByRole("button", { name: filePattern }).first() + await expect(suggestion).toBeVisible() + await suggestion.hover() + + await page.keyboard.press("Tab") + + const pill = page.locator(`${promptSelector} [data-type="file"]`).first() + await expect(pill).toBeVisible() + await expect(pill).toHaveAttribute("data-path", filePattern) + + await page.keyboard.type(" ok") + await expect(page.locator(promptSelector)).toContainText("ok") +}) diff --git a/packages/app/e2e/prompt/prompt-slash-open.spec.ts b/packages/app/e2e/prompt/prompt-slash-open.spec.ts new file mode 100644 index 000000000..3e769e330 --- /dev/null +++ b/packages/app/e2e/prompt/prompt-slash-open.spec.ts @@ -0,0 +1,22 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../utils" + +test("smoke /open opens file picker dialog", async ({ page, gotoSession }) => { + await gotoSession() + + await page.locator(promptSelector).click() + await page.keyboard.type("/open") + + const command = page.locator('[data-slash-id="file.open"]') + await expect(command).toBeVisible() + await command.hover() + + await page.keyboard.press("Enter") + + const dialog = page.getByRole("dialog") + await expect(dialog).toBeVisible() + await expect(dialog.getByRole("textbox").first()).toBeVisible() + + await page.keyboard.press("Escape") + await expect(dialog).toHaveCount(0) +}) diff --git a/packages/app/e2e/prompt/prompt.spec.ts b/packages/app/e2e/prompt/prompt.spec.ts new file mode 100644 index 000000000..b58e5e296 --- /dev/null +++ b/packages/app/e2e/prompt/prompt.spec.ts @@ -0,0 +1,62 @@ +import { test, expect } from "../fixtures" +import { promptSelector } from "../utils" + +function sessionIDFromUrl(url: string) { + const match = /\/session\/([^/?#]+)/.exec(url) + return match?.[1] +} + +test("can send a prompt and receive a reply", async ({ page, sdk, gotoSession }) => { + test.setTimeout(120_000) + + const pageErrors: string[] = [] + const onPageError = (err: Error) => { + pageErrors.push(err.message) + } + page.on("pageerror", onPageError) + + await gotoSession() + + const token = `E2E_OK_${Date.now()}` + + const prompt = page.locator(promptSelector) + await prompt.click() + await page.keyboard.type(`Reply with exactly: ${token}`) + await page.keyboard.press("Enter") + + await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 }) + + const sessionID = (() => { + const id = sessionIDFromUrl(page.url()) + if (!id) throw new Error(`Failed to parse session id from url: ${page.url()}`) + return id + })() + + try { + await expect + .poll( + async () => { + const messages = await sdk.session.messages({ sessionID, limit: 50 }).then((r) => r.data ?? []) + return messages + .filter((m) => m.info.role === "assistant") + .flatMap((m) => m.parts) + .filter((p) => p.type === "text") + .map((p) => p.text) + .join("\n") + }, + { timeout: 90_000 }, + ) + + .toContain(token) + + const reply = page.locator('[data-slot="session-turn-summary-section"]').filter({ hasText: token }).first() + await expect(reply).toBeVisible({ timeout: 90_000 }) + } finally { + page.off("pageerror", onPageError) + await sdk.session.delete({ sessionID }).catch(() => undefined) + } + + if (pageErrors.length > 0) { + throw new Error(`Page error(s):\n${pageErrors.join("\n")}`) + } +}) diff --git a/packages/app/e2e/server-default.spec.ts b/packages/app/e2e/server-default.spec.ts deleted file mode 100644 index b6b16f0bc..000000000 --- a/packages/app/e2e/server-default.spec.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { test, expect } from "./fixtures" -import { serverName, serverUrl } from "./utils" - -const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl" - -test("can set a default server on web", async ({ page, gotoSession }) => { - await page.addInitScript((key: string) => { - try { - localStorage.removeItem(key) - } catch { - return - } - }, DEFAULT_SERVER_URL_KEY) - - await gotoSession() - - const status = page.getByRole("button", { name: "Status" }) - await expect(status).toBeVisible() - const popover = page.locator('[data-component="popover-content"]').filter({ hasText: "Manage servers" }) - - const ensurePopoverOpen = async () => { - if (await popover.isVisible()) return - await status.click() - await expect(popover).toBeVisible() - } - - await ensurePopoverOpen() - await popover.getByRole("button", { name: "Manage servers" }).click() - - const dialog = page.getByRole("dialog") - await expect(dialog).toBeVisible() - - const row = dialog.locator('[data-slot="list-item"]').filter({ hasText: serverName }).first() - await expect(row).toBeVisible() - - const menu = row.locator('[data-component="icon-button"]').last() - await menu.click() - await page.getByRole("menuitem", { name: "Set as default" }).click() - - await expect.poll(() => page.evaluate((key) => localStorage.getItem(key), DEFAULT_SERVER_URL_KEY)).toBe(serverUrl) - await expect(row.getByText("Default", { exact: true })).toBeVisible() - - await page.keyboard.press("Escape") - const closed = await dialog - .waitFor({ state: "detached", timeout: 1500 }) - .then(() => true) - .catch(() => false) - - if (!closed) { - await page.keyboard.press("Escape") - const closedSecond = await dialog - .waitFor({ state: "detached", timeout: 1500 }) - .then(() => true) - .catch(() => false) - - if (!closedSecond) { - await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } }) - await expect(dialog).toHaveCount(0) - } - } - - await ensurePopoverOpen() - - const serverRow = popover.locator("button").filter({ hasText: serverName }).first() - await expect(serverRow).toBeVisible() - await expect(serverRow.getByText("Default", { exact: true })).toBeVisible() -}) diff --git a/packages/app/e2e/session.spec.ts b/packages/app/e2e/session.spec.ts deleted file mode 100644 index 19e25a421..000000000 --- a/packages/app/e2e/session.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { test, expect } from "./fixtures" -import { promptSelector } from "./utils" - -test("can open an existing session and type into the prompt", async ({ page, sdk, gotoSession }) => { - const title = `e2e smoke ${Date.now()}` - const created = await sdk.session.create({ title }).then((r) => r.data) - - if (!created?.id) throw new Error("Session create did not return an id") - const sessionID = created.id - - try { - await gotoSession(sessionID) - - const prompt = page.locator(promptSelector) - await prompt.click() - await page.keyboard.type("hello from e2e") - await expect(prompt).toContainText("hello from e2e") - } finally { - await sdk.session.delete({ sessionID }).catch(() => undefined) - } -}) diff --git a/packages/app/e2e/settings-providers.spec.ts b/packages/app/e2e/settings-providers.spec.ts deleted file mode 100644 index 326a9fad1..000000000 --- a/packages/app/e2e/settings-providers.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { test, expect } from "./fixtures" -import { modKey, promptSelector } from "./utils" - -test("smoke providers settings opens provider selector", async ({ page, gotoSession }) => { - await gotoSession() - - const dialog = page.getByRole("dialog") - - await page.keyboard.press(`${modKey}+Comma`).catch(() => undefined) - - const opened = await dialog - .waitFor({ state: "visible", timeout: 3000 }) - .then(() => true) - .catch(() => false) - - if (!opened) { - await page.getByRole("button", { name: "Settings" }).first().click() - await expect(dialog).toBeVisible() - } - - await dialog.getByRole("tab", { name: "Providers" }).click() - await expect(dialog.getByText("Connected providers", { exact: true })).toBeVisible() - await expect(dialog.getByText("Popular providers", { exact: true })).toBeVisible() - - await dialog.getByRole("button", { name: "Show more providers" }).click() - - const providerDialog = page.getByRole("dialog").filter({ has: page.getByPlaceholder("Search providers") }) - - await expect(providerDialog).toBeVisible() - await expect(providerDialog.getByPlaceholder("Search providers")).toBeVisible() - await expect(providerDialog.locator('[data-slot="list-item"]').first()).toBeVisible() - - await page.keyboard.press("Escape") - await expect(providerDialog).toHaveCount(0) - await expect(page.locator(promptSelector)).toBeVisible() - - const stillOpen = await dialog.isVisible().catch(() => false) - if (!stillOpen) return - - await page.keyboard.press("Escape") - const closed = await dialog - .waitFor({ state: "detached", timeout: 1500 }) - .then(() => true) - .catch(() => false) - if (closed) return - - await page.keyboard.press("Escape") - const closedSecond = await dialog - .waitFor({ state: "detached", timeout: 1500 }) - .then(() => true) - .catch(() => false) - if (closedSecond) return - - await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } }) - await expect(dialog).toHaveCount(0) -}) diff --git a/packages/app/e2e/settings.spec.ts b/packages/app/e2e/settings.spec.ts deleted file mode 100644 index 09dc942cc..000000000 --- a/packages/app/e2e/settings.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { test, expect } from "./fixtures" -import { modKey } from "./utils" - -test("smoke settings dialog opens, switches tabs, closes", async ({ page, gotoSession }) => { - await gotoSession() - - const dialog = page.getByRole("dialog") - - await page.keyboard.press(`${modKey}+Comma`).catch(() => undefined) - - const opened = await dialog - .waitFor({ state: "visible", timeout: 3000 }) - .then(() => true) - .catch(() => false) - - if (!opened) { - await page.getByRole("button", { name: "Settings" }).first().click() - await expect(dialog).toBeVisible() - } - - await dialog.getByRole("tab", { name: "Shortcuts" }).click() - await expect(dialog.getByRole("button", { name: "Reset to defaults" })).toBeVisible() - await expect(dialog.getByPlaceholder("Search shortcuts")).toBeVisible() - - await page.keyboard.press("Escape") - - const closed = await dialog - .waitFor({ state: "detached", timeout: 1500 }) - .then(() => true) - .catch(() => false) - - if (closed) return - - await page.keyboard.press("Escape") - const closedSecond = await dialog - .waitFor({ state: "detached", timeout: 1500 }) - .then(() => true) - .catch(() => false) - - if (closedSecond) return - - await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } }) - await expect(dialog).toHaveCount(0) -}) diff --git a/packages/app/e2e/settings/settings-language.spec.ts b/packages/app/e2e/settings/settings-language.spec.ts new file mode 100644 index 000000000..b2ef70bf8 --- /dev/null +++ b/packages/app/e2e/settings/settings-language.spec.ts @@ -0,0 +1,39 @@ +import { test, expect } from "../fixtures" +import { modKey, settingsLanguageSelectSelector } from "../utils" + +test("smoke changing language updates settings labels", async ({ page, gotoSession }) => { + await page.addInitScript(() => { + localStorage.setItem("opencode.global.dat:language", JSON.stringify({ locale: "en" })) + }) + + await gotoSession() + + const dialog = page.getByRole("dialog") + + await page.keyboard.press(`${modKey}+Comma`).catch(() => undefined) + + const opened = await dialog + .waitFor({ state: "visible", timeout: 3000 }) + .then(() => true) + .catch(() => false) + + if (!opened) { + await page.getByRole("button", { name: "Settings" }).first().click() + await expect(dialog).toBeVisible() + } + + const heading = dialog.getByRole("heading", { level: 2 }) + await expect(heading).toHaveText("General") + + const select = dialog.locator(settingsLanguageSelectSelector) + await expect(select).toBeVisible() + await select.locator('[data-slot="select-select-trigger"]').click() + + await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Deutsch" }).click() + + await expect(heading).toHaveText("Allgemein") + + await select.locator('[data-slot="select-select-trigger"]').click() + await page.locator('[data-slot="select-select-item"]').filter({ hasText: "English" }).click() + await expect(heading).toHaveText("General") +}) diff --git a/packages/app/e2e/settings/settings-providers.spec.ts b/packages/app/e2e/settings/settings-providers.spec.ts new file mode 100644 index 000000000..5b9325c2a --- /dev/null +++ b/packages/app/e2e/settings/settings-providers.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from "../fixtures" +import { modKey, promptSelector } from "../utils" + +test("smoke providers settings opens provider selector", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = page.getByRole("dialog") + + await page.keyboard.press(`${modKey}+Comma`).catch(() => undefined) + + const opened = await dialog + .waitFor({ state: "visible", timeout: 3000 }) + .then(() => true) + .catch(() => false) + + if (!opened) { + await page.getByRole("button", { name: "Settings" }).first().click() + await expect(dialog).toBeVisible() + } + + await dialog.getByRole("tab", { name: "Providers" }).click() + await expect(dialog.getByText("Connected providers", { exact: true })).toBeVisible() + await expect(dialog.getByText("Popular providers", { exact: true })).toBeVisible() + + await dialog.getByRole("button", { name: "Show more providers" }).click() + + const providerDialog = page.getByRole("dialog").filter({ has: page.getByPlaceholder("Search providers") }) + + await expect(providerDialog).toBeVisible() + await expect(providerDialog.getByPlaceholder("Search providers")).toBeVisible() + await expect(providerDialog.locator('[data-slot="list-item"]').first()).toBeVisible() + + await page.keyboard.press("Escape") + await expect(providerDialog).toHaveCount(0) + await expect(page.locator(promptSelector)).toBeVisible() + + const stillOpen = await dialog.isVisible().catch(() => false) + if (!stillOpen) return + + await page.keyboard.press("Escape") + const closed = await dialog + .waitFor({ state: "detached", timeout: 1500 }) + .then(() => true) + .catch(() => false) + if (closed) return + + await page.keyboard.press("Escape") + const closedSecond = await dialog + .waitFor({ state: "detached", timeout: 1500 }) + .then(() => true) + .catch(() => false) + if (closedSecond) return + + await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } }) + await expect(dialog).toHaveCount(0) +}) diff --git a/packages/app/e2e/settings/settings.spec.ts b/packages/app/e2e/settings/settings.spec.ts new file mode 100644 index 000000000..293a4ba9a --- /dev/null +++ b/packages/app/e2e/settings/settings.spec.ts @@ -0,0 +1,44 @@ +import { test, expect } from "../fixtures" +import { modKey } from "../utils" + +test("smoke settings dialog opens, switches tabs, closes", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = page.getByRole("dialog") + + await page.keyboard.press(`${modKey}+Comma`).catch(() => undefined) + + const opened = await dialog + .waitFor({ state: "visible", timeout: 3000 }) + .then(() => true) + .catch(() => false) + + if (!opened) { + await page.getByRole("button", { name: "Settings" }).first().click() + await expect(dialog).toBeVisible() + } + + await dialog.getByRole("tab", { name: "Shortcuts" }).click() + await expect(dialog.getByRole("button", { name: "Reset to defaults" })).toBeVisible() + await expect(dialog.getByPlaceholder("Search shortcuts")).toBeVisible() + + await page.keyboard.press("Escape") + + const closed = await dialog + .waitFor({ state: "detached", timeout: 1500 }) + .then(() => true) + .catch(() => false) + + if (closed) return + + await page.keyboard.press("Escape") + const closedSecond = await dialog + .waitFor({ state: "detached", timeout: 1500 }) + .then(() => true) + .catch(() => false) + + if (closedSecond) return + + await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } }) + await expect(dialog).toHaveCount(0) +}) diff --git a/packages/app/e2e/sidebar-session-links.spec.ts b/packages/app/e2e/sidebar-session-links.spec.ts deleted file mode 100644 index fab64736e..000000000 --- a/packages/app/e2e/sidebar-session-links.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { test, expect } from "./fixtures" -import { modKey, promptSelector } from "./utils" - -type Locator = { - first: () => Locator - getAttribute: (name: string) => Promise - scrollIntoViewIfNeeded: () => Promise - click: () => Promise -} - -type Page = { - locator: (selector: string) => Locator - keyboard: { - press: (key: string) => Promise - } -} - -type Fixtures = { - page: Page - slug: string - sdk: { - session: { - create: (input: { title: string }) => Promise<{ data?: { id?: string } }> - delete: (input: { sessionID: string }) => Promise - } - } - gotoSession: (sessionID?: string) => Promise -} - -test("sidebar session links navigate to the selected session", async ({ page, slug, sdk, gotoSession }: Fixtures) => { - const stamp = Date.now() - - const one = await sdk.session.create({ title: `e2e sidebar nav 1 ${stamp}` }).then((r) => r.data) - const two = await sdk.session.create({ title: `e2e sidebar nav 2 ${stamp}` }).then((r) => r.data) - - if (!one?.id) throw new Error("Session create did not return an id") - if (!two?.id) throw new Error("Session create did not return an id") - - try { - await gotoSession(one.id) - - const main = page.locator("main") - const collapsed = ((await main.getAttribute("class")) ?? "").includes("xl:border-l") - if (collapsed) { - await page.keyboard.press(`${modKey}+B`) - await expect(main).not.toHaveClass(/xl:border-l/) - } - - const target = page.locator(`[data-session-id="${two.id}"] a`).first() - await expect(target).toBeVisible() - await target.scrollIntoViewIfNeeded() - await target.click() - - await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`)) - await expect(page.locator(promptSelector)).toBeVisible() - await expect(page.locator(`[data-session-id="${two.id}"] a`).first()).toHaveClass(/\bactive\b/) - } finally { - await sdk.session.delete({ sessionID: one.id }).catch(() => undefined) - await sdk.session.delete({ sessionID: two.id }).catch(() => undefined) - } -}) diff --git a/packages/app/e2e/sidebar.spec.ts b/packages/app/e2e/sidebar.spec.ts deleted file mode 100644 index 925590f51..000000000 --- a/packages/app/e2e/sidebar.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { test, expect } from "./fixtures" -import { modKey } from "./utils" - -test("sidebar can be collapsed and expanded", async ({ page, gotoSession }) => { - await gotoSession() - - const main = page.locator("main") - const closedClass = /xl:border-l/ - const isClosed = await main.evaluate((node) => node.className.includes("xl:border-l")) - - if (isClosed) { - await page.keyboard.press(`${modKey}+B`) - await expect(main).not.toHaveClass(closedClass) - } - - await page.keyboard.press(`${modKey}+B`) - await expect(main).toHaveClass(closedClass) - - await page.keyboard.press(`${modKey}+B`) - await expect(main).not.toHaveClass(closedClass) -}) diff --git a/packages/app/e2e/sidebar/sidebar-session-links.spec.ts b/packages/app/e2e/sidebar/sidebar-session-links.spec.ts new file mode 100644 index 000000000..8c3f69547 --- /dev/null +++ b/packages/app/e2e/sidebar/sidebar-session-links.spec.ts @@ -0,0 +1,35 @@ +import { test, expect } from "../fixtures" +import { modKey, promptSelector } from "../utils" + +test("sidebar session links navigate to the selected session", async ({ page, slug, sdk, gotoSession }) => { + const stamp = Date.now() + + const one = await sdk.session.create({ title: `e2e sidebar nav 1 ${stamp}` }).then((r) => r.data) + const two = await sdk.session.create({ title: `e2e sidebar nav 2 ${stamp}` }).then((r) => r.data) + + if (!one?.id) throw new Error("Session create did not return an id") + if (!two?.id) throw new Error("Session create did not return an id") + + try { + await gotoSession(one.id) + + const main = page.locator("main") + const collapsed = ((await main.getAttribute("class")) ?? "").includes("xl:border-l") + if (collapsed) { + await page.keyboard.press(`${modKey}+B`) + await expect(main).not.toHaveClass(/xl:border-l/) + } + + const target = page.locator(`[data-session-id="${two.id}"] a`).first() + await expect(target).toBeVisible() + await target.scrollIntoViewIfNeeded() + await target.click() + + await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`)) + await expect(page.locator(promptSelector)).toBeVisible() + await expect(page.locator(`[data-session-id="${two.id}"] a`).first()).toHaveClass(/\bactive\b/) + } finally { + await sdk.session.delete({ sessionID: one.id }).catch(() => undefined) + await sdk.session.delete({ sessionID: two.id }).catch(() => undefined) + } +}) diff --git a/packages/app/e2e/sidebar/sidebar.spec.ts b/packages/app/e2e/sidebar/sidebar.spec.ts new file mode 100644 index 000000000..ba58b1008 --- /dev/null +++ b/packages/app/e2e/sidebar/sidebar.spec.ts @@ -0,0 +1,21 @@ +import { test, expect } from "../fixtures" +import { modKey } from "../utils" + +test("sidebar can be collapsed and expanded", async ({ page, gotoSession }) => { + await gotoSession() + + const main = page.locator("main") + const closedClass = /xl:border-l/ + const isClosed = await main.evaluate((node) => node.className.includes("xl:border-l")) + + if (isClosed) { + await page.keyboard.press(`${modKey}+B`) + await expect(main).not.toHaveClass(closedClass) + } + + await page.keyboard.press(`${modKey}+B`) + await expect(main).toHaveClass(closedClass) + + await page.keyboard.press(`${modKey}+B`) + await expect(main).not.toHaveClass(closedClass) +}) diff --git a/packages/app/e2e/terminal-init.spec.ts b/packages/app/e2e/terminal-init.spec.ts deleted file mode 100644 index cfde2d019..000000000 --- a/packages/app/e2e/terminal-init.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { test, expect } from "./fixtures" -import { promptSelector, terminalSelector, terminalToggleKey } from "./utils" - -test("smoke terminal mounts and can create a second tab", async ({ page, gotoSession }) => { - await gotoSession() - - const terminals = page.locator(terminalSelector) - const opened = await terminals.first().isVisible() - - if (!opened) { - await page.keyboard.press(terminalToggleKey) - } - - await expect(terminals.first()).toBeVisible() - await expect(terminals.first().locator("textarea")).toHaveCount(1) - await expect(terminals).toHaveCount(1) - - // Ghostty captures a lot of keybinds when focused; move focus back - // to the app shell before triggering `terminal.new`. - await page.locator(promptSelector).click() - await page.keyboard.press("Control+Alt+T") - - await expect(terminals).toHaveCount(2) - await expect(terminals.nth(1).locator("textarea")).toHaveCount(1) -}) diff --git a/packages/app/e2e/terminal.spec.ts b/packages/app/e2e/terminal.spec.ts deleted file mode 100644 index fc558b632..000000000 --- a/packages/app/e2e/terminal.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { test, expect } from "./fixtures" -import { terminalSelector, terminalToggleKey } from "./utils" - -test("terminal panel can be toggled", async ({ page, gotoSession }) => { - await gotoSession() - - const terminal = page.locator(terminalSelector) - const initiallyOpen = await terminal.isVisible() - if (initiallyOpen) { - await page.keyboard.press(terminalToggleKey) - await expect(terminal).toHaveCount(0) - } - - await page.keyboard.press(terminalToggleKey) - await expect(terminal).toBeVisible() -}) diff --git a/packages/app/e2e/terminal/terminal-init.spec.ts b/packages/app/e2e/terminal/terminal-init.spec.ts new file mode 100644 index 000000000..6faa73a75 --- /dev/null +++ b/packages/app/e2e/terminal/terminal-init.spec.ts @@ -0,0 +1,25 @@ +import { test, expect } from "../fixtures" +import { promptSelector, terminalSelector, terminalToggleKey } from "../utils" + +test("smoke terminal mounts and can create a second tab", async ({ page, gotoSession }) => { + await gotoSession() + + const terminals = page.locator(terminalSelector) + const opened = await terminals.first().isVisible() + + if (!opened) { + await page.keyboard.press(terminalToggleKey) + } + + await expect(terminals.first()).toBeVisible() + await expect(terminals.first().locator("textarea")).toHaveCount(1) + await expect(terminals).toHaveCount(1) + + // Ghostty captures a lot of keybinds when focused; move focus back + // to the app shell before triggering `terminal.new`. + await page.locator(promptSelector).click() + await page.keyboard.press("Control+Alt+T") + + await expect(terminals).toHaveCount(2) + await expect(terminals.nth(1).locator("textarea")).toHaveCount(1) +}) diff --git a/packages/app/e2e/terminal/terminal.spec.ts b/packages/app/e2e/terminal/terminal.spec.ts new file mode 100644 index 000000000..aaf5c2d75 --- /dev/null +++ b/packages/app/e2e/terminal/terminal.spec.ts @@ -0,0 +1,16 @@ +import { test, expect } from "../fixtures" +import { terminalSelector, terminalToggleKey } from "../utils" + +test("terminal panel can be toggled", async ({ page, gotoSession }) => { + await gotoSession() + + const terminal = page.locator(terminalSelector) + const initiallyOpen = await terminal.isVisible() + if (initiallyOpen) { + await page.keyboard.press(terminalToggleKey) + await expect(terminal).toHaveCount(0) + } + + await page.keyboard.press(terminalToggleKey) + await expect(terminal).toBeVisible() +}) diff --git a/packages/app/e2e/titlebar-history.spec.ts b/packages/app/e2e/titlebar-history.spec.ts deleted file mode 100644 index d4aa605e6..000000000 --- a/packages/app/e2e/titlebar-history.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { test, expect } from "./fixtures" -import { modKey, promptSelector } from "./utils" - -test("titlebar back/forward navigates between sessions", async ({ page, slug, sdk, gotoSession }) => { - await page.setViewportSize({ width: 1400, height: 800 }) - - const stamp = Date.now() - const one = await sdk.session.create({ title: `e2e titlebar history 1 ${stamp}` }).then((r) => r.data) - const two = await sdk.session.create({ title: `e2e titlebar history 2 ${stamp}` }).then((r) => r.data) - - if (!one?.id) throw new Error("Session create did not return an id") - if (!two?.id) throw new Error("Session create did not return an id") - - try { - await gotoSession(one.id) - - const main = page.locator("main") - const collapsed = ((await main.getAttribute("class")) ?? "").includes("xl:border-l") - if (collapsed) { - await page.keyboard.press(`${modKey}+B`) - await expect(main).not.toHaveClass(/xl:border-l/) - } - - const link = page.locator(`[data-session-id="${two.id}"] a`).first() - await expect(link).toBeVisible() - await link.scrollIntoViewIfNeeded() - await link.click() - - await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`)) - await expect(page.locator(promptSelector)).toBeVisible() - - const back = page.getByRole("button", { name: "Back" }) - const forward = page.getByRole("button", { name: "Forward" }) - - await expect(back).toBeVisible() - await expect(back).toBeEnabled() - await back.click() - - await expect(page).toHaveURL(new RegExp(`/${slug}/session/${one.id}(?:\\?|#|$)`)) - await expect(page.locator(promptSelector)).toBeVisible() - - await expect(forward).toBeVisible() - await expect(forward).toBeEnabled() - await forward.click() - - await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`)) - await expect(page.locator(promptSelector)).toBeVisible() - } finally { - await sdk.session.delete({ sessionID: one.id }).catch(() => undefined) - await sdk.session.delete({ sessionID: two.id }).catch(() => undefined) - } -}) diff --git a/packages/app/e2e/utils.ts b/packages/app/e2e/utils.ts index 3de488bd9..3dec12592 100644 --- a/packages/app/e2e/utils.ts +++ b/packages/app/e2e/utils.ts @@ -14,6 +14,8 @@ export const promptSelector = '[data-component="prompt-input"]' export const terminalSelector = '[data-component="terminal"]' export const modelVariantCycleSelector = '[data-action="model-variant-cycle"]' +export const settingsLanguageSelectSelector = '[data-action="settings-language"]' + export function createSdk(directory?: string) { return createOpencodeClient({ baseUrl: serverUrl, directory, throwOnError: true }) } diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index 054fa6ef4..b2fb43536 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -154,6 +154,7 @@ export const SettingsGeneral: Component = () => { description={language.t("settings.general.row.language.description")} >