diff options
| author | Adam <[email protected]> | 2026-02-12 09:49:14 -0600 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-02-12 09:49:14 -0600 |
| commit | ff4414bb152acfddb5c0eb073c38bedc1df4ae14 (patch) | |
| tree | 78381c67d21ef6f089647f6b19e7aa2976840dbc /packages/app/e2e | |
| parent | 56ad2db02055955f926fda0e4a89055b22ead6f9 (diff) | |
| download | opencode-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.ts | 19 | ||||
| -rw-r--r-- | packages/app/e2e/files/file-viewer.spec.ts | 39 | ||||
| -rw-r--r-- | packages/app/e2e/projects/workspace-new-session.spec.ts | 10 | ||||
| -rw-r--r-- | packages/app/e2e/projects/workspaces.spec.ts | 86 | ||||
| -rw-r--r-- | packages/app/e2e/prompt/context.spec.ts | 101 | ||||
| -rw-r--r-- | packages/app/e2e/prompt/prompt.spec.ts | 3 | ||||
| -rw-r--r-- | packages/app/e2e/session/session-undo-redo.spec.ts | 32 | ||||
| -rw-r--r-- | packages/app/e2e/session/session.spec.ts | 45 |
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, }) }) |
