summaryrefslogtreecommitdiffhomepage
path: root/packages/app/e2e
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-12 09:49:14 -0600
committerGitHub <[email protected]>2026-02-12 09:49:14 -0600
commitff4414bb152acfddb5c0eb073c38bedc1df4ae14 (patch)
tree78381c67d21ef6f089647f6b19e7aa2976840dbc /packages/app/e2e
parent56ad2db02055955f926fda0e4a89055b22ead6f9 (diff)
downloadopencode-ff4414bb152acfddb5c0eb073c38bedc1df4ae14.tar.gz
opencode-ff4414bb152acfddb5c0eb073c38bedc1df4ae14.zip
chore: refactor packages/app files (#13236)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Frank <[email protected]>
Diffstat (limited to 'packages/app/e2e')
-rw-r--r--packages/app/e2e/files/file-open.spec.ts19
-rw-r--r--packages/app/e2e/files/file-viewer.spec.ts39
-rw-r--r--packages/app/e2e/projects/workspace-new-session.spec.ts10
-rw-r--r--packages/app/e2e/projects/workspaces.spec.ts86
-rw-r--r--packages/app/e2e/prompt/context.spec.ts101
-rw-r--r--packages/app/e2e/prompt/prompt.spec.ts3
-rw-r--r--packages/app/e2e/session/session-undo-redo.spec.ts32
-rw-r--r--packages/app/e2e/session/session.spec.ts45
8 files changed, 238 insertions, 97 deletions
diff --git a/packages/app/e2e/files/file-open.spec.ts b/packages/app/e2e/files/file-open.spec.ts
index 3c636d748..abb28242d 100644
--- a/packages/app/e2e/files/file-open.spec.ts
+++ b/packages/app/e2e/files/file-open.spec.ts
@@ -1,15 +1,28 @@
import { test, expect } from "../fixtures"
-import { openPalette, clickListItem } from "../actions"
+import { promptSelector } from "../selectors"
test("can open a file tab from the search palette", async ({ page, gotoSession }) => {
await gotoSession()
- const dialog = await openPalette(page)
+ await page.locator(promptSelector).click()
+ await page.keyboard.type("/open")
+
+ const command = page.locator('[data-slash-id="file.open"]').first()
+ await expect(command).toBeVisible()
+ await page.keyboard.press("Enter")
+
+ const dialog = page
+ .getByRole("dialog")
+ .filter({ has: page.getByPlaceholder(/search files/i) })
+ .first()
+ await expect(dialog).toBeVisible()
const input = dialog.getByRole("textbox").first()
await input.fill("package.json")
- await clickListItem(dialog, { keyStartsWith: "file:" })
+ const item = dialog.locator('[data-slot="list-item"][data-key^="file:"]').first()
+ await expect(item).toBeVisible({ timeout: 30_000 })
+ await item.click()
await expect(dialog).toHaveCount(0)
diff --git a/packages/app/e2e/files/file-viewer.spec.ts b/packages/app/e2e/files/file-viewer.spec.ts
index 528384497..b968acc13 100644
--- a/packages/app/e2e/files/file-viewer.spec.ts
+++ b/packages/app/e2e/files/file-viewer.spec.ts
@@ -1,18 +1,41 @@
import { test, expect } from "../fixtures"
-import { openPalette, clickListItem } from "../actions"
+import { promptSelector } from "../selectors"
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.locator(promptSelector).click()
+ await page.keyboard.type("/open")
- const dialog = await openPalette(page)
+ const command = page.locator('[data-slash-id="file.open"]').first()
+ await expect(command).toBeVisible()
+ await page.keyboard.press("Enter")
- const input = dialog.getByRole("textbox").first()
- await input.fill(file)
+ const dialog = page
+ .getByRole("dialog")
+ .filter({ has: page.getByPlaceholder(/search files/i) })
+ .first()
+ await expect(dialog).toBeVisible()
- await clickListItem(dialog, { text: /packages.*app.*package.json/ })
+ const input = dialog.getByRole("textbox").first()
+ await input.fill("package.json")
+
+ const items = dialog.locator('[data-slot="list-item"][data-key^="file:"]')
+ let index = -1
+ await expect
+ .poll(
+ async () => {
+ const keys = await items.evaluateAll((nodes) => nodes.map((node) => node.getAttribute("data-key") ?? ""))
+ index = keys.findIndex((key) => /packages[\\/]+app[\\/]+package\.json$/i.test(key.replace(/^file:/, "")))
+ return index >= 0
+ },
+ { timeout: 30_000 },
+ )
+ .toBe(true)
+
+ const item = items.nth(index)
+ await expect(item).toBeVisible()
+ await item.click()
await expect(dialog).toHaveCount(0)
@@ -22,5 +45,5 @@ test("smoke file viewer renders real file content", async ({ page, gotoSession }
const code = page.locator('[data-component="code"]').first()
await expect(code).toBeVisible()
- await expect(code.getByText("@opencode-ai/app")).toBeVisible()
+ await expect(code.getByText(/"name"\s*:\s*"@opencode-ai\/app"/)).toBeVisible()
})
diff --git a/packages/app/e2e/projects/workspace-new-session.spec.ts b/packages/app/e2e/projects/workspace-new-session.spec.ts
index 5af314caf..f33972cc3 100644
--- a/packages/app/e2e/projects/workspace-new-session.spec.ts
+++ b/packages/app/e2e/projects/workspace-new-session.spec.ts
@@ -69,15 +69,19 @@ async function createSessionFromWorkspace(page: Page, slug: string, text: string
const prompt = page.locator(promptSelector)
await expect(prompt).toBeVisible()
+ await expect(prompt).toBeEditable()
await prompt.click()
- await page.keyboard.type(text)
- await page.keyboard.press("Enter")
+ await expect(prompt).toBeFocused()
+ await prompt.fill(text)
+ await expect.poll(async () => ((await prompt.textContent()) ?? "").trim()).toContain(text)
+ await prompt.press("Enter")
await expect.poll(() => slugFromUrl(page.url())).toBe(slug)
- await expect(page).toHaveURL(new RegExp(`/${slug}/session/[^/?#]+`), { timeout: 30_000 })
+ await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 30_000 }).not.toBe("")
const sessionID = sessionIDFromUrl(page.url())
if (!sessionID) throw new Error(`Failed to parse session id from url: ${page.url()}`)
+ await expect(page).toHaveURL(new RegExp(`/${slug}/session/${sessionID}(?:[/?#]|$)`))
return sessionID
}
diff --git a/packages/app/e2e/projects/workspaces.spec.ts b/packages/app/e2e/projects/workspaces.spec.ts
index 071c398b2..386739526 100644
--- a/packages/app/e2e/projects/workspaces.spec.ts
+++ b/packages/app/e2e/projects/workspaces.spec.ts
@@ -11,18 +11,12 @@ import {
cleanupTestProject,
clickMenuItem,
confirmDialog,
- openProjectMenu,
openSidebar,
openWorkspaceMenu,
setWorkspacesEnabled,
} from "../actions"
-import {
- inlineInputSelector,
- projectSwitchSelector,
- projectWorkspacesToggleSelector,
- workspaceItemSelector,
-} from "../selectors"
-import { dirSlug } from "../utils"
+import { dropdownMenuContentSelector, inlineInputSelector, workspaceItemSelector } from "../selectors"
+import { createSdk, dirSlug } from "../utils"
function slugFromUrl(url: string) {
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
@@ -143,26 +137,35 @@ test("non-git projects keep workspace mode disabled", async ({ page, withProject
await fs.writeFile(path.join(nonGit, "README.md"), "# e2e nongit\n")
try {
- await withProject(
- async () => {
- await openSidebar(page)
+ await withProject(async () => {
+ await page.goto(`/${nonGitSlug}/session`)
+
+ await expect.poll(() => slugFromUrl(page.url()), { timeout: 30_000 }).not.toBe("")
- const nonGitButton = page.locator(projectSwitchSelector(nonGitSlug)).first()
- await expect(nonGitButton).toBeVisible()
- await nonGitButton.click()
- await expect(page).toHaveURL(new RegExp(`/${nonGitSlug}/session`))
+ const activeDir = base64Decode(slugFromUrl(page.url()))
+ expect(path.basename(activeDir)).toContain("opencode-e2e-project-nongit-")
- const menu = await openProjectMenu(page, nonGitSlug)
- const toggle = menu.locator(projectWorkspacesToggleSelector(nonGitSlug)).first()
+ await openSidebar(page)
+ await expect(page.getByRole("button", { name: "New workspace" })).toHaveCount(0)
- await expect(toggle).toBeVisible()
- await expect(toggle).toBeDisabled()
+ const trigger = page.locator('[data-action="project-menu"]').first()
+ const hasMenu = await trigger
+ .isVisible()
+ .then((x) => x)
+ .catch(() => false)
+ if (!hasMenu) return
- await expect(menu.getByRole("menuitem", { name: "New workspace" })).toHaveCount(0)
- await expect(page.getByRole("button", { name: "New workspace" })).toHaveCount(0)
- },
- { extra: [nonGit] },
- )
+ await trigger.click({ force: true })
+
+ const menu = page.locator(dropdownMenuContentSelector).first()
+ await expect(menu).toBeVisible()
+
+ const toggle = menu.locator('[data-action="project-workspaces-toggle"]').first()
+
+ await expect(toggle).toBeVisible()
+ await expect(toggle).toBeDisabled()
+ await expect(menu.getByRole("menuitem", { name: "New workspace" })).toHaveCount(0)
+ })
} finally {
await cleanupTestProject(nonGit)
}
@@ -256,14 +259,45 @@ test("can delete a workspace", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })
await withProject(async (project) => {
- const { rootSlug, slug } = await setupWorkspaceTest(page, project)
+ const sdk = createSdk(project.directory)
+ const { rootSlug, slug, directory } = await setupWorkspaceTest(page, project)
+
+ await expect
+ .poll(
+ async () => {
+ const worktrees = await sdk.worktree
+ .list()
+ .then((r) => r.data ?? [])
+ .catch(() => [] as string[])
+ return worktrees.includes(directory)
+ },
+ { timeout: 30_000 },
+ )
+ .toBe(true)
const menu = await openWorkspaceMenu(page, slug)
await clickMenuItem(menu, /^Delete$/i, { force: true })
await confirmDialog(page, /^Delete workspace$/i)
await expect(page).toHaveURL(new RegExp(`/${rootSlug}/session`))
- await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0)
+
+ await expect
+ .poll(
+ async () => {
+ const worktrees = await sdk.worktree
+ .list()
+ .then((r) => r.data ?? [])
+ .catch(() => [] as string[])
+ return worktrees.includes(directory)
+ },
+ { timeout: 60_000 },
+ )
+ .toBe(false)
+
+ await project.gotoSession()
+
+ await openSidebar(page)
+ await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0, { timeout: 60_000 })
await expect(page.locator(workspaceItemSelector(rootSlug)).first()).toBeVisible()
})
})
diff --git a/packages/app/e2e/prompt/context.spec.ts b/packages/app/e2e/prompt/context.spec.ts
index 80aa9ea33..366191fd7 100644
--- a/packages/app/e2e/prompt/context.spec.ts
+++ b/packages/app/e2e/prompt/context.spec.ts
@@ -1,40 +1,95 @@
import { test, expect } from "../fixtures"
+import type { Page } from "@playwright/test"
import { promptSelector } from "../selectors"
import { withSession } from "../actions"
+function contextButton(page: Page) {
+ return page
+ .locator('[data-component="button"]')
+ .filter({ has: page.locator('[data-component="progress-circle"]').first() })
+ .first()
+}
+
+async function seedContextSession(input: { sessionID: string; sdk: Parameters<typeof withSession>[0] }) {
+ await input.sdk.session.promptAsync({
+ sessionID: input.sessionID,
+ noReply: true,
+ parts: [
+ {
+ type: "text",
+ text: "seed context",
+ },
+ ],
+ })
+
+ await expect
+ .poll(async () => {
+ const messages = await input.sdk.session
+ .messages({ sessionID: input.sessionID, limit: 1 })
+ .then((r) => r.data ?? [])
+ return messages.length
+ })
+ .toBeGreaterThan(0)
+}
+
test("context panel can be opened from the prompt", async ({ page, sdk, gotoSession }) => {
const title = `e2e smoke context ${Date.now()}`
await withSession(sdk, title, async (session) => {
- await sdk.session.promptAsync({
- sessionID: session.id,
- noReply: true,
- parts: [
- {
- type: "text",
- text: "seed context",
- },
- ],
- })
+ await seedContextSession({ sessionID: session.id, sdk })
- await expect
- .poll(async () => {
- const messages = await sdk.session.messages({ sessionID: session.id, limit: 1 }).then((r) => r.data ?? [])
- return messages.length
- })
- .toBeGreaterThan(0)
+ await gotoSession(session.id)
+
+ const trigger = contextButton(page)
+ await expect(trigger).toBeVisible()
+ await trigger.click()
+
+ const tabs = page.locator('[data-component="tabs"][data-variant="normal"]')
+ await expect(tabs.getByRole("tab", { name: "Context" })).toBeVisible()
+ })
+})
+test("context panel can be closed from the context tab close action", async ({ page, sdk, gotoSession }) => {
+ await withSession(sdk, `e2e context toggle ${Date.now()}`, async (session) => {
+ await seedContextSession({ sessionID: session.id, sdk })
await gotoSession(session.id)
- const contextButton = page
- .locator('[data-component="button"]')
- .filter({ has: page.locator('[data-component="progress-circle"]').first() })
- .first()
+ await page.locator(promptSelector).click()
- await expect(contextButton).toBeVisible()
- await contextButton.click()
+ const trigger = contextButton(page)
+ await expect(trigger).toBeVisible()
+ await trigger.click()
const tabs = page.locator('[data-component="tabs"][data-variant="normal"]')
- await expect(tabs.getByRole("tab", { name: "Context" })).toBeVisible()
+ const context = tabs.getByRole("tab", { name: "Context" })
+ await expect(context).toBeVisible()
+
+ await page.getByRole("button", { name: "Close tab" }).first().click()
+ await expect(context).toHaveCount(0)
+ })
+})
+
+test("context panel can open file picker from context actions", async ({ page, sdk, gotoSession }) => {
+ await withSession(sdk, `e2e context tabs ${Date.now()}`, async (session) => {
+ await seedContextSession({ sessionID: session.id, sdk })
+ await gotoSession(session.id)
+
+ await page.locator(promptSelector).click()
+
+ const trigger = contextButton(page)
+ await expect(trigger).toBeVisible()
+ await trigger.click()
+
+ await expect(page.getByRole("tab", { name: "Context" })).toBeVisible()
+ await page.getByRole("button", { name: "Open file" }).first().click()
+
+ const dialog = page
+ .getByRole("dialog")
+ .filter({ has: page.getByPlaceholder(/search files/i) })
+ .first()
+ await expect(dialog).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
index 07d242c63..ff9f5daf0 100644
--- a/packages/app/e2e/prompt/prompt.spec.ts
+++ b/packages/app/e2e/prompt/prompt.spec.ts
@@ -44,9 +44,6 @@ test("can send a prompt and receive a reply", async ({ page, sdk, gotoSession })
)
.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)
diff --git a/packages/app/e2e/session/session-undo-redo.spec.ts b/packages/app/e2e/session/session-undo-redo.spec.ts
index 2a250dd86..c6ea2aea0 100644
--- a/packages/app/e2e/session/session-undo-redo.spec.ts
+++ b/packages/app/e2e/session/session-undo-redo.spec.ts
@@ -10,21 +10,26 @@ async function seedConversation(input: {
sessionID: string
token: string
}) {
+ const messages = async () =>
+ await input.sdk.session.messages({ sessionID: input.sessionID, limit: 100 }).then((r) => r.data ?? [])
+ const seeded = await messages()
+ const userIDs = new Set(seeded.filter((m) => m.info.role === "user").map((m) => m.info.id))
+
const prompt = input.page.locator(promptSelector)
await expect(prompt).toBeVisible()
- await prompt.click()
- await input.page.keyboard.type(`Reply with exactly: ${input.token}`)
- await input.page.keyboard.press("Enter")
+ await input.sdk.session.promptAsync({
+ sessionID: input.sessionID,
+ noReply: true,
+ parts: [{ type: "text", text: input.token }],
+ })
let userMessageID: string | undefined
await expect
.poll(
async () => {
- const messages = await input.sdk.session
- .messages({ sessionID: input.sessionID, limit: 50 })
- .then((r) => r.data ?? [])
- const users = messages.filter(
+ const users = (await messages()).filter(
(m) =>
+ !userIDs.has(m.info.id) &&
m.info.role === "user" &&
m.parts.filter((p) => p.type === "text").some((p) => p.text.includes(input.token)),
)
@@ -33,21 +38,14 @@ async function seedConversation(input: {
const user = users[users.length - 1]
if (!user) return false
userMessageID = user.info.id
-
- const assistantText = messages
- .filter((m) => m.info.role === "assistant")
- .flatMap((m) => m.parts)
- .filter((p) => p.type === "text")
- .map((p) => p.text)
- .join("\n")
-
- return assistantText.includes(input.token)
+ return true
},
- { timeout: 90_000 },
+ { timeout: 90_000, intervals: [250, 500, 1_000] },
)
.toBe(true)
if (!userMessageID) throw new Error("Expected a user message id")
+ await expect(input.page.locator(`[data-message-id="${userMessageID}"]`).first()).toBeVisible({ timeout: 30_000 })
return { prompt, userMessageID }
}
diff --git a/packages/app/e2e/session/session.spec.ts b/packages/app/e2e/session/session.spec.ts
index 4610fb331..93eaee5cb 100644
--- a/packages/app/e2e/session/session.spec.ts
+++ b/packages/app/e2e/session/session.spec.ts
@@ -34,21 +34,34 @@ async function seedMessage(sdk: Sdk, sessionID: string) {
test("session can be renamed via header menu", async ({ page, sdk, gotoSession }) => {
const stamp = Date.now()
const originalTitle = `e2e rename test ${stamp}`
- const newTitle = `e2e renamed ${stamp}`
+ const renamedTitle = `e2e renamed ${stamp}`
await withSession(sdk, originalTitle, async (session) => {
await seedMessage(sdk, session.id)
await gotoSession(session.id)
+ await expect(page.getByRole("heading", { level: 1 }).first()).toHaveText(originalTitle)
const menu = await openSessionMoreMenu(page, session.id)
await clickMenuItem(menu, /rename/i)
const input = page.locator(".session-scroller").locator(inlineInputSelector).first()
await expect(input).toBeVisible()
- await input.fill(newTitle)
+ await expect(input).toBeFocused()
+ await input.fill(renamedTitle)
+ await expect(input).toHaveValue(renamedTitle)
await input.press("Enter")
- await expect(page.getByRole("heading", { level: 1 }).first()).toContainText(newTitle)
+ await expect
+ .poll(
+ async () => {
+ const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data)
+ return data?.title
+ },
+ { timeout: 30_000 },
+ )
+ .toBe(renamedTitle)
+
+ await expect(page.getByRole("heading", { level: 1 }).first()).toHaveText(renamedTitle)
})
})
@@ -116,8 +129,14 @@ test("session can be shared and unshared via header button", async ({ page, sdk,
await seedMessage(sdk, session.id)
await gotoSession(session.id)
- const { rightSection, popoverBody } = await openSharePopover(page)
- await popoverBody.getByRole("button", { name: "Publish" }).first().click()
+ const shared = await openSharePopover(page)
+ const publish = shared.popoverBody.getByRole("button", { name: "Publish" }).first()
+ await expect(publish).toBeVisible({ timeout: 30_000 })
+ await publish.click()
+
+ await expect(shared.popoverBody.getByRole("button", { name: "Unpublish" }).first()).toBeVisible({
+ timeout: 30_000,
+ })
await expect
.poll(
@@ -129,14 +148,14 @@ test("session can be shared and unshared via header button", async ({ page, sdk,
)
.not.toBeUndefined()
- const copyButton = rightSection.locator('button[aria-label="Copy link"]').first()
- await expect(copyButton).toBeVisible({ timeout: 30_000 })
-
- const sharedPopover = await openSharePopover(page)
- const unpublish = sharedPopover.popoverBody.getByRole("button", { name: "Unpublish" }).first()
+ const unpublish = shared.popoverBody.getByRole("button", { name: "Unpublish" }).first()
await expect(unpublish).toBeVisible({ timeout: 30_000 })
await unpublish.click()
+ await expect(shared.popoverBody.getByRole("button", { name: "Publish" }).first()).toBeVisible({
+ timeout: 30_000,
+ })
+
await expect
.poll(
async () => {
@@ -147,10 +166,8 @@ test("session can be shared and unshared via header button", async ({ page, sdk,
)
.toBeUndefined()
- await expect(copyButton).not.toBeVisible({ timeout: 30_000 })
-
- const unsharedPopover = await openSharePopover(page)
- await expect(unsharedPopover.popoverBody.getByRole("button", { name: "Publish" }).first()).toBeVisible({
+ const unshared = await openSharePopover(page)
+ await expect(unshared.popoverBody.getByRole("button", { name: "Publish" }).first()).toBeVisible({
timeout: 30_000,
})
})