Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions packages/app/e2e/files/file-open.spec.ts
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
39 changes: 31 additions & 8 deletions packages/app/e2e/files/file-viewer.spec.ts
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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()
})
10 changes: 7 additions & 3 deletions packages/app/e2e/projects/workspace-new-session.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
86 changes: 60 additions & 26 deletions packages/app/e2e/projects/workspaces.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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] ?? ""
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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()
})
})
Expand Down
101 changes: 78 additions & 23 deletions packages/app/e2e/prompt/context.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
3 changes: 0 additions & 3 deletions packages/app/e2e/prompt/prompt.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading