summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorFilip <[email protected]>2026-01-31 13:42:47 +0100
committerGitHub <[email protected]>2026-01-31 06:42:47 -0600
commit511c7abacaebb5e007139395078b6a848e5e9ea5 (patch)
treef135c6621283aa6bcd66a1a3e5831202c7ba42eb
parentf834915d3fbcf05137fc61fc51b9fa07b815e82c (diff)
downloadopencode-511c7abacaebb5e007139395078b6a848e5e9ea5.tar.gz
opencode-511c7abacaebb5e007139395078b6a848e5e9ea5.zip
test(app): session actions (#11386)
-rw-r--r--packages/app/e2e/actions.ts111
-rw-r--r--packages/app/e2e/app/server-default.spec.ts30
-rw-r--r--packages/app/e2e/app/session.spec.ts13
-rw-r--r--packages/app/e2e/app/titlebar-history.spec.ts56
-rw-r--r--packages/app/e2e/files/file-open.spec.ts6
-rw-r--r--packages/app/e2e/files/file-viewer.spec.ts10
-rw-r--r--packages/app/e2e/models/model-picker.spec.ts5
-rw-r--r--packages/app/e2e/models/models-visibility.spec.ts2
-rw-r--r--packages/app/e2e/projects/project-edit.spec.ts7
-rw-r--r--packages/app/e2e/projects/projects-close.spec.ts19
-rw-r--r--packages/app/e2e/prompt/context.spec.ts17
-rw-r--r--packages/app/e2e/prompt/prompt.spec.ts2
-rw-r--r--packages/app/e2e/selectors.ts18
-rw-r--r--packages/app/e2e/session/session.spec.ts115
-rw-r--r--packages/app/e2e/settings/settings-providers.spec.ts2
-rw-r--r--packages/app/e2e/sidebar/sidebar-session-links.spec.ts2
-rw-r--r--packages/app/script/e2e-local.ts2
-rw-r--r--turbo.json2
18 files changed, 313 insertions, 106 deletions
diff --git a/packages/app/e2e/actions.ts b/packages/app/e2e/actions.ts
index 3da16d317..7308adc7e 100644
--- a/packages/app/e2e/actions.ts
+++ b/packages/app/e2e/actions.ts
@@ -4,6 +4,17 @@ import os from "node:os"
import path from "node:path"
import { execSync } from "node:child_process"
import { modKey, serverUrl } from "./utils"
+import {
+ sessionItemSelector,
+ dropdownMenuTriggerSelector,
+ dropdownMenuContentSelector,
+ titlebarRightSelector,
+ popoverBodySelector,
+ listItemSelector,
+ listItemKeySelector,
+ listItemKeyStartsWithSelector,
+} from "./selectors"
+import type { createSdk } from "./utils"
export async function defocus(page: Page) {
await page.mouse.click(5, 5)
@@ -158,3 +169,103 @@ export function sessionIDFromUrl(url: string) {
const match = /\/session\/([^/?#]+)/.exec(url)
return match?.[1]
}
+
+export async function hoverSessionItem(page: Page, sessionID: string) {
+ const sessionEl = page.locator(sessionItemSelector(sessionID)).first()
+ await expect(sessionEl).toBeVisible()
+ await sessionEl.hover()
+ return sessionEl
+}
+
+export async function openSessionMoreMenu(page: Page, sessionID: string) {
+ const sessionEl = await hoverSessionItem(page, sessionID)
+
+ const menuTrigger = sessionEl.locator(dropdownMenuTriggerSelector).first()
+ await expect(menuTrigger).toBeVisible()
+ await menuTrigger.click()
+
+ const menu = page.locator(dropdownMenuContentSelector).first()
+ await expect(menu).toBeVisible()
+ return menu
+}
+
+export async function clickMenuItem(menu: Locator, itemName: string | RegExp, options?: { force?: boolean }) {
+ const item = menu.getByRole("menuitem").filter({ hasText: itemName }).first()
+ await expect(item).toBeVisible()
+ await item.click({ force: options?.force })
+}
+
+export async function confirmDialog(page: Page, buttonName: string | RegExp) {
+ const dialog = page.getByRole("dialog").first()
+ await expect(dialog).toBeVisible()
+
+ const button = dialog.getByRole("button").filter({ hasText: buttonName }).first()
+ await expect(button).toBeVisible()
+ await button.click()
+}
+
+export async function openSharePopover(page: Page) {
+ const rightSection = page.locator(titlebarRightSelector)
+ const shareButton = rightSection.getByRole("button", { name: "Share" }).first()
+ await expect(shareButton).toBeVisible()
+
+ const popoverBody = page
+ .locator(popoverBodySelector)
+ .filter({ has: page.getByRole("button", { name: /^(Publish|Unpublish)$/ }) })
+ .first()
+
+ const opened = await popoverBody
+ .isVisible()
+ .then((x) => x)
+ .catch(() => false)
+
+ if (!opened) {
+ await shareButton.click()
+ await expect(popoverBody).toBeVisible()
+ }
+ return { rightSection, popoverBody }
+}
+
+export async function clickPopoverButton(page: Page, buttonName: string | RegExp) {
+ const button = page.getByRole("button").filter({ hasText: buttonName }).first()
+ await expect(button).toBeVisible()
+ await button.click()
+}
+
+export async function clickListItem(
+ container: Locator | Page,
+ filter: string | RegExp | { key?: string; text?: string | RegExp; keyStartsWith?: string },
+): Promise<Locator> {
+ let item: Locator
+
+ if (typeof filter === "string" || filter instanceof RegExp) {
+ item = container.locator(listItemSelector).filter({ hasText: filter }).first()
+ } else if (filter.keyStartsWith) {
+ item = container.locator(listItemKeyStartsWithSelector(filter.keyStartsWith)).first()
+ } else if (filter.key) {
+ item = container.locator(listItemKeySelector(filter.key)).first()
+ } else if (filter.text) {
+ item = container.locator(listItemSelector).filter({ hasText: filter.text }).first()
+ } else {
+ throw new Error("Invalid filter provided to clickListItem")
+ }
+
+ await expect(item).toBeVisible()
+ await item.click()
+ return item
+}
+
+export async function withSession<T>(
+ sdk: ReturnType<typeof createSdk>,
+ title: string,
+ callback: (session: { id: string; title: string }) => Promise<T>,
+): Promise<T> {
+ const session = await sdk.session.create({ title }).then((r) => r.data)
+ if (!session?.id) throw new Error("Session create did not return an id")
+
+ try {
+ return await callback(session)
+ } finally {
+ await sdk.session.delete({ sessionID: session.id }).catch(() => undefined)
+ }
+}
diff --git a/packages/app/e2e/app/server-default.spec.ts b/packages/app/e2e/app/server-default.spec.ts
index 6f44ded1a..adbc83473 100644
--- a/packages/app/e2e/app/server-default.spec.ts
+++ b/packages/app/e2e/app/server-default.spec.ts
@@ -1,5 +1,6 @@
import { test, expect } from "../fixtures"
import { serverName, serverUrl } from "../utils"
+import { clickListItem, closeDialog, clickMenuItem } from "../actions"
const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl"
@@ -33,31 +34,18 @@ test("can set a default server on web", async ({ page, gotoSession }) => {
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()
+ const menuTrigger = row.locator('[data-slot="dropdown-menu-trigger"]').first()
+ await expect(menuTrigger).toBeVisible()
+ await menuTrigger.click({ force: true })
+
+ const menu = page.locator('[data-component="dropdown-menu-content"]').first()
+ await expect(menu).toBeVisible()
+ await clickMenuItem(menu, /set as default/i)
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 closeDialog(page, dialog)
await ensurePopoverOpen()
diff --git a/packages/app/e2e/app/session.spec.ts b/packages/app/e2e/app/session.spec.ts
index d35af7ef7..c7fdfdc54 100644
--- a/packages/app/e2e/app/session.spec.ts
+++ b/packages/app/e2e/app/session.spec.ts
@@ -1,21 +1,16 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
+import { withSession } from "../actions"
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)
+ await withSession(sdk, title, async (session) => {
+ await gotoSession(session.id)
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
index c7ff6566c..ec65dca0b 100644
--- a/packages/app/e2e/app/titlebar-history.spec.ts
+++ b/packages/app/e2e/app/titlebar-history.spec.ts
@@ -1,48 +1,42 @@
import { test, expect } from "../fixtures"
-import { openSidebar } from "../actions"
+import { openSidebar, withSession } from "../actions"
import { promptSelector } from "../selectors"
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")
+ await withSession(sdk, `e2e titlebar history 1 ${stamp}`, async (one) => {
+ await withSession(sdk, `e2e titlebar history 2 ${stamp}`, async (two) => {
+ await gotoSession(one.id)
- try {
- await gotoSession(one.id)
+ await openSidebar(page)
- await openSidebar(page)
+ const link = page.locator(`[data-session-id="${two.id}"] a`).first()
+ await expect(link).toBeVisible()
+ await link.scrollIntoViewIfNeeded()
+ await link.click()
- 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()
- 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" })
- 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(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(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(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)
- }
+ await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
+ await expect(page.locator(promptSelector)).toBeVisible()
+ })
+ })
})
diff --git a/packages/app/e2e/files/file-open.spec.ts b/packages/app/e2e/files/file-open.spec.ts
index dea35d25b..3c636d748 100644
--- a/packages/app/e2e/files/file-open.spec.ts
+++ b/packages/app/e2e/files/file-open.spec.ts
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
-import { openPalette } from "../actions"
+import { openPalette, clickListItem } from "../actions"
test("can open a file tab from the search palette", async ({ page, gotoSession }) => {
await gotoSession()
@@ -9,9 +9,7 @@ test("can open a file tab from the search palette", async ({ page, gotoSession }
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 clickListItem(dialog, { keyStartsWith: "file:" })
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 3dc0dead2..528384497 100644
--- a/packages/app/e2e/files/file-viewer.spec.ts
+++ b/packages/app/e2e/files/file-viewer.spec.ts
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
-import { openPalette } from "../actions"
+import { openPalette, clickListItem } from "../actions"
test("smoke file viewer renders real file content", async ({ page, gotoSession }) => {
await gotoSession()
@@ -12,13 +12,7 @@ test("smoke file viewer renders real file content", async ({ page, gotoSession }
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 clickListItem(dialog, { text: /packages.*app.*package.json/ })
await expect(dialog).toHaveCount(0)
diff --git a/packages/app/e2e/models/model-picker.spec.ts b/packages/app/e2e/models/model-picker.spec.ts
index df95e04d2..01e72464c 100644
--- a/packages/app/e2e/models/model-picker.spec.ts
+++ b/packages/app/e2e/models/model-picker.spec.ts
@@ -1,5 +1,6 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
+import { clickListItem } from "../actions"
test("smoke model selection updates prompt footer", async ({ page, gotoSession }) => {
await gotoSession()
@@ -32,9 +33,7 @@ test("smoke model selection updates prompt footer", async ({ page, gotoSession }
await input.fill(model)
- const item = dialog.locator(`[data-slot="list-item"][data-key="${key}"]`)
- await expect(item).toBeVisible()
- await item.click()
+ await clickListItem(dialog, { key })
await expect(dialog).toHaveCount(0)
diff --git a/packages/app/e2e/models/models-visibility.spec.ts b/packages/app/e2e/models/models-visibility.spec.ts
index 36f14596d..c69911179 100644
--- a/packages/app/e2e/models/models-visibility.spec.ts
+++ b/packages/app/e2e/models/models-visibility.spec.ts
@@ -1,6 +1,6 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
-import { closeDialog, openSettings } from "../actions"
+import { closeDialog, openSettings, clickListItem } from "../actions"
test("hiding a model removes it from the model picker", async ({ page, gotoSession }) => {
await gotoSession()
diff --git a/packages/app/e2e/projects/project-edit.spec.ts b/packages/app/e2e/projects/project-edit.spec.ts
index 22d053f3d..772c25951 100644
--- a/packages/app/e2e/projects/project-edit.spec.ts
+++ b/packages/app/e2e/projects/project-edit.spec.ts
@@ -14,7 +14,12 @@ test("dialog edit project updates name and startup script", async ({ page, gotoS
await expect(trigger).toBeVisible()
await trigger.click({ force: true })
- await page.getByRole("menuitem", { name: "Edit" }).click()
+ const menu = page.locator('[data-component="dropdown-menu-content"]').first()
+ await expect(menu).toBeVisible()
+
+ const editItem = menu.getByRole("menuitem", { name: "Edit" }).first()
+ await expect(editItem).toBeVisible()
+ await editItem.click({ force: true })
const dialog = page.getByRole("dialog")
await expect(dialog).toBeVisible()
diff --git a/packages/app/e2e/projects/projects-close.spec.ts b/packages/app/e2e/projects/projects-close.spec.ts
index c3618740d..bd323b90c 100644
--- a/packages/app/e2e/projects/projects-close.spec.ts
+++ b/packages/app/e2e/projects/projects-close.spec.ts
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
-import { createTestProject, seedProjects, cleanupTestProject, openSidebar } from "../actions"
+import { createTestProject, seedProjects, cleanupTestProject, openSidebar, clickMenuItem } from "../actions"
import { projectCloseHoverSelector, projectCloseMenuSelector, projectSwitchSelector } from "../selectors"
import { dirSlug } from "../utils"
@@ -33,7 +33,7 @@ test("can close a project via project header more options menu", async ({ page,
await page.setViewportSize({ width: 1400, height: 800 })
const other = await createTestProject()
- const otherName = other.split("/").pop()
+ const otherName = other.split("/").pop() ?? other
const otherSlug = dirSlug(other)
await seedProjects(page, { directory, extra: [other] })
@@ -59,17 +59,10 @@ test("can close a project via project header more options menu", async ({ page,
await trigger.focus()
await page.keyboard.press("Enter")
- const close = page
- .locator(projectCloseMenuSelector(otherSlug))
- .or(page.getByRole("menuitem", { name: "Close" }))
- .or(
- page
- .locator('[data-component="dropdown-menu-content"] [data-slot="dropdown-menu-item"]')
- .filter({ hasText: "Close" }),
- )
- .first()
- await expect(close).toBeVisible({ timeout: 10_000 })
- await close.click({ force: true })
+ const menu = page.locator('[data-component="dropdown-menu-content"]').first()
+ await expect(menu).toBeVisible({ timeout: 10_000 })
+
+ await clickMenuItem(menu, /^Close$/i, { force: true })
await expect(otherButton).toHaveCount(0)
} finally {
await cleanupTestProject(other)
diff --git a/packages/app/e2e/prompt/context.spec.ts b/packages/app/e2e/prompt/context.spec.ts
index 9e8f998f2..80aa9ea33 100644
--- a/packages/app/e2e/prompt/context.spec.ts
+++ b/packages/app/e2e/prompt/context.spec.ts
@@ -1,16 +1,13 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
+import { withSession } from "../actions"
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 withSession(sdk, title, async (session) => {
await sdk.session.promptAsync({
- sessionID,
+ sessionID: session.id,
noReply: true,
parts: [
{
@@ -22,12 +19,12 @@ test("context panel can be opened from the prompt", async ({ page, sdk, gotoSess
await expect
.poll(async () => {
- const messages = await sdk.session.messages({ sessionID, limit: 1 }).then((r) => r.data ?? [])
+ const messages = await sdk.session.messages({ sessionID: session.id, limit: 1 }).then((r) => r.data ?? [])
return messages.length
})
.toBeGreaterThan(0)
- await gotoSession(sessionID)
+ await gotoSession(session.id)
const contextButton = page
.locator('[data-component="button"]')
@@ -39,7 +36,5 @@ test("context panel can be opened from the prompt", async ({ page, sdk, gotoSess
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.spec.ts b/packages/app/e2e/prompt/prompt.spec.ts
index 33f8d7ebc..07d242c63 100644
--- a/packages/app/e2e/prompt/prompt.spec.ts
+++ b/packages/app/e2e/prompt/prompt.spec.ts
@@ -1,6 +1,6 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
-import { sessionIDFromUrl } from "../actions"
+import { sessionIDFromUrl, withSession } from "../actions"
test("can send a prompt and receive a reply", async ({ page, sdk, gotoSession }) => {
test.setTimeout(120_000)
diff --git a/packages/app/e2e/selectors.ts b/packages/app/e2e/selectors.ts
index 9179a6fd5..8beade573 100644
--- a/packages/app/e2e/selectors.ts
+++ b/packages/app/e2e/selectors.ts
@@ -15,3 +15,21 @@ export const projectMenuTriggerSelector = (slug: string) =>
`${sidebarNavSelector} [data-action="project-menu"][data-project="${slug}"]`
export const projectCloseMenuSelector = (slug: string) => `[data-action="project-close-menu"][data-project="${slug}"]`
+
+export const titlebarRightSelector = "#opencode-titlebar-right"
+
+export const popoverBodySelector = '[data-slot="popover-body"]'
+
+export const dropdownMenuTriggerSelector = '[data-slot="dropdown-menu-trigger"]'
+
+export const dropdownMenuContentSelector = '[data-component="dropdown-menu-content"]'
+
+export const inlineInputSelector = '[data-component="inline-input"]'
+
+export const sessionItemSelector = (sessionID: string) => `${sidebarNavSelector} [data-session-id="${sessionID}"]`
+
+export const listItemSelector = '[data-slot="list-item"]'
+
+export const listItemKeyStartsWithSelector = (prefix: string) => `${listItemSelector}[data-key^="${prefix}"]`
+
+export const listItemKeySelector = (key: string) => `${listItemSelector}[data-key="${key}"]`
diff --git a/packages/app/e2e/session/session.spec.ts b/packages/app/e2e/session/session.spec.ts
new file mode 100644
index 000000000..05984bbee
--- /dev/null
+++ b/packages/app/e2e/session/session.spec.ts
@@ -0,0 +1,115 @@
+import { test, expect } from "../fixtures"
+import {
+ openSidebar,
+ openSessionMoreMenu,
+ clickMenuItem,
+ confirmDialog,
+ openSharePopover,
+ withSession,
+} from "../actions"
+import { sessionItemSelector, inlineInputSelector } from "../selectors"
+
+const shareDisabled = process.env.OPENCODE_DISABLE_SHARE === "true" || process.env.OPENCODE_DISABLE_SHARE === "1"
+
+test("sidebar session can be renamed", async ({ page, sdk, gotoSession }) => {
+ const stamp = Date.now()
+ const originalTitle = `e2e rename test ${stamp}`
+ const newTitle = `e2e renamed ${stamp}`
+
+ await withSession(sdk, originalTitle, async (session) => {
+ await gotoSession(session.id)
+ await openSidebar(page)
+
+ const menu = await openSessionMoreMenu(page, session.id)
+ await clickMenuItem(menu, /rename/i)
+
+ const input = page.locator(sessionItemSelector(session.id)).locator(inlineInputSelector).first()
+ await expect(input).toBeVisible()
+ await input.fill(newTitle)
+ await input.press("Enter")
+
+ await expect(page.locator(sessionItemSelector(session.id)).locator("a").first()).toContainText(newTitle)
+ })
+})
+
+test("sidebar session can be archived", async ({ page, sdk, gotoSession }) => {
+ const stamp = Date.now()
+ const title = `e2e archive test ${stamp}`
+
+ await withSession(sdk, title, async (session) => {
+ await gotoSession(session.id)
+ await openSidebar(page)
+
+ const sessionEl = page.locator(sessionItemSelector(session.id))
+ const menu = await openSessionMoreMenu(page, session.id)
+ await clickMenuItem(menu, /archive/i)
+
+ await expect(sessionEl).not.toBeVisible()
+ })
+})
+
+test("sidebar session can be deleted", async ({ page, sdk, gotoSession }) => {
+ const stamp = Date.now()
+ const title = `e2e delete test ${stamp}`
+
+ await withSession(sdk, title, async (session) => {
+ await gotoSession(session.id)
+ await openSidebar(page)
+
+ const sessionEl = page.locator(sessionItemSelector(session.id))
+ const menu = await openSessionMoreMenu(page, session.id)
+ await clickMenuItem(menu, /delete/i)
+ await confirmDialog(page, /delete/i)
+
+ await expect(sessionEl).not.toBeVisible()
+ })
+})
+
+test("session can be shared and unshared via header button", async ({ page, sdk, gotoSession }) => {
+ test.skip(shareDisabled, "Share is disabled in this environment (OPENCODE_DISABLE_SHARE).")
+
+ const stamp = Date.now()
+ const title = `e2e share test ${stamp}`
+
+ await withSession(sdk, title, async (session) => {
+ await gotoSession(session.id)
+
+ const { rightSection, popoverBody } = await openSharePopover(page)
+ await popoverBody.getByRole("button", { name: "Publish" }).first().click()
+
+ await expect
+ .poll(
+ async () => {
+ const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data)
+ return data?.share?.url || undefined
+ },
+ { timeout: 30_000 },
+ )
+ .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()
+ await expect(unpublish).toBeVisible({ timeout: 30_000 })
+ await unpublish.click()
+
+ await expect
+ .poll(
+ async () => {
+ const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data)
+ return data?.share?.url || undefined
+ },
+ { timeout: 30_000 },
+ )
+ .toBeUndefined()
+
+ await expect(copyButton).not.toBeVisible({ timeout: 30_000 })
+
+ const unsharedPopover = await openSharePopover(page)
+ await expect(unsharedPopover.popoverBody.getByRole("button", { name: "Publish" }).first()).toBeVisible({
+ timeout: 30_000,
+ })
+ })
+})
diff --git a/packages/app/e2e/settings/settings-providers.spec.ts b/packages/app/e2e/settings/settings-providers.spec.ts
index 4b3b178cc..2bd2616bc 100644
--- a/packages/app/e2e/settings/settings-providers.spec.ts
+++ b/packages/app/e2e/settings/settings-providers.spec.ts
@@ -1,6 +1,6 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
-import { closeDialog, openSettings } from "../actions"
+import { closeDialog, openSettings, clickListItem } from "../actions"
test("smoke providers settings opens provider selector", async ({ page, gotoSession }) => {
await gotoSession()
diff --git a/packages/app/e2e/sidebar/sidebar-session-links.spec.ts b/packages/app/e2e/sidebar/sidebar-session-links.spec.ts
index 1c0f4fa71..cda2278a9 100644
--- a/packages/app/e2e/sidebar/sidebar-session-links.spec.ts
+++ b/packages/app/e2e/sidebar/sidebar-session-links.spec.ts
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
-import { openSidebar } from "../actions"
+import { openSidebar, withSession } from "../actions"
import { promptSelector } from "../selectors"
test("sidebar session links navigate to the selected session", async ({ page, slug, sdk, gotoSession }) => {
diff --git a/packages/app/script/e2e-local.ts b/packages/app/script/e2e-local.ts
index 2c7be2ad9..df2107f76 100644
--- a/packages/app/script/e2e-local.ts
+++ b/packages/app/script/e2e-local.ts
@@ -58,7 +58,7 @@ const sandbox = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-"))
const serverEnv = {
...process.env,
- OPENCODE_DISABLE_SHARE: "true",
+ OPENCODE_DISABLE_SHARE: process.env.OPENCODE_DISABLE_SHARE ?? "true",
OPENCODE_DISABLE_LSP_DOWNLOAD: "true",
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true",
diff --git a/turbo.json b/turbo.json
index 5de1b8d75..f06ddb0e8 100644
--- a/turbo.json
+++ b/turbo.json
@@ -1,5 +1,7 @@
{
"$schema": "https://turborepo.com/schema.json",
+ "globalEnv": ["CI", "OPENCODE_DISABLE_SHARE"],
+ "globalPassThroughEnv": ["CI", "OPENCODE_DISABLE_SHARE"],
"tasks": {
"typecheck": {},
"build": {