test(app): migrate more e2e suites to isolated backend (#20505)

This commit is contained in:
Kit Langton
2026-04-01 13:15:42 -04:00
committed by GitHub
parent d1e0a4640c
commit c559af51ce
8 changed files with 607 additions and 537 deletions
+2 -1
View File
@@ -1,7 +1,7 @@
import { test, expect } from "../fixtures" import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors" import { promptSelector } from "../selectors"
import { assistantText, sessionIDFromUrl, withSession } from "../actions" import { assistantText, sessionIDFromUrl, withSession } from "../actions"
import { openaiModel, promptMatch, withMockOpenAI } from "./mock" import { openaiModel, promptMatch, titleMatch, withMockOpenAI } from "./mock"
const text = (value: string | null) => (value ?? "").replace(/\u200B/g, "").trim() const text = (value: string | null) => (value ?? "").replace(/\u200B/g, "").trim()
@@ -24,6 +24,7 @@ test("prompt succeeds when sync message endpoint is unreachable", async ({
llmUrl: llm.url, llmUrl: llm.url,
fn: async () => { fn: async () => {
const token = `E2E_ASYNC_${Date.now()}` const token = `E2E_ASYNC_${Date.now()}`
await llm.textMatch(titleMatch, "E2E Title")
await llm.textMatch(promptMatch(token), token) await llm.textMatch(promptMatch(token), token)
await withBackendProject( await withBackendProject(
+63 -30
View File
@@ -1,8 +1,9 @@
import type { ToolPart } from "@opencode-ai/sdk/v2/client" import type { ToolPart } from "@opencode-ai/sdk/v2/client"
import type { Page } from "@playwright/test" import type { Page } from "@playwright/test"
import { test, expect } from "../fixtures" import { test, expect } from "../fixtures"
import { withSession } from "../actions" import { assistantText, sessionIDFromUrl } from "../actions"
import { promptSelector } from "../selectors" import { promptSelector } from "../selectors"
import { openaiModel, promptMatch, titleMatch, withMockOpenAI } from "./mock"
const text = (value: string | null) => (value ?? "").replace(/\u200B/g, "").trim() const text = (value: string | null) => (value ?? "").replace(/\u200B/g, "").trim()
@@ -43,20 +44,13 @@ async function wait(page: Page, value: string) {
await expect.poll(async () => text(await page.locator(promptSelector).textContent())).toBe(value) await expect.poll(async () => text(await page.locator(promptSelector).textContent())).toBe(value)
} }
async function reply(sdk: Parameters<typeof withSession>[0], sessionID: string, token: string) { async function reply(
sdk: { session: { messages: Parameters<typeof assistantText>[0]["session"] } },
sessionID: string,
token: string,
) {
await expect await expect
.poll( .poll(() => assistantText(sdk as Parameters<typeof assistantText>[0], sessionID), { timeout: 90_000 })
async () => {
const messages = await sdk.session.messages({ sessionID, limit: 50 }).then((r) => r.data ?? [])
return messages
.filter((item) => item.info.role === "assistant")
.flatMap((item) => item.parts)
.filter((item) => item.type === "text")
.map((item) => item.text)
.join("\n")
},
{ timeout: 90_000 },
)
.toContain(token) .toContain(token)
} }
@@ -79,36 +73,52 @@ async function shell(sdk: Parameters<typeof withSession>[0], sessionID: string,
.toContain(token) .toContain(token)
} }
test("prompt history restores unsent draft with arrow navigation", async ({ page, sdk, gotoSession }) => { test("prompt history restores unsent draft with arrow navigation", async ({
page,
llm,
backend,
withBackendProject,
}) => {
test.setTimeout(120_000) test.setTimeout(120_000)
await withSession(sdk, `e2e prompt history ${Date.now()}`, async (session) => { await withMockOpenAI({
await gotoSession(session.id) serverUrl: backend.url,
llmUrl: llm.url,
const prompt = page.locator(promptSelector) fn: async () => {
const firstToken = `E2E_HISTORY_ONE_${Date.now()}` const firstToken = `E2E_HISTORY_ONE_${Date.now()}`
const secondToken = `E2E_HISTORY_TWO_${Date.now()}` const secondToken = `E2E_HISTORY_TWO_${Date.now()}`
const first = `Reply with exactly: ${firstToken}` const first = `Reply with exactly: ${firstToken}`
const second = `Reply with exactly: ${secondToken}` const second = `Reply with exactly: ${secondToken}`
const draft = `draft ${Date.now()}` const draft = `draft ${Date.now()}`
await llm.textMatch(titleMatch, "E2E Title")
await llm.textMatch(promptMatch(firstToken), firstToken)
await llm.textMatch(promptMatch(secondToken), secondToken)
await withBackendProject(
async (project) => {
const prompt = page.locator(promptSelector)
await prompt.click() await prompt.click()
await page.keyboard.type(first) await page.keyboard.type(first)
await page.keyboard.press("Enter") await page.keyboard.press("Enter")
await wait(page, "") await wait(page, "")
await reply(sdk, session.id, firstToken)
await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 })
const sessionID = sessionIDFromUrl(page.url())!
project.trackSession(sessionID)
await reply(project.sdk, sessionID, firstToken)
await prompt.click() await prompt.click()
await page.keyboard.type(second) await page.keyboard.type(second)
await page.keyboard.press("Enter") await page.keyboard.press("Enter")
await wait(page, "") await wait(page, "")
await reply(sdk, session.id, secondToken) await reply(project.sdk, sessionID, secondToken)
await prompt.click() await prompt.click()
await page.keyboard.type(draft) await page.keyboard.type(draft)
await wait(page, draft) await wait(page, draft)
// Clear the draft before navigating history (ArrowUp only works when prompt is empty)
await prompt.fill("") await prompt.fill("")
await wait(page, "") await wait(page, "")
@@ -123,16 +133,22 @@ test("prompt history restores unsent draft with arrow navigation", async ({ page
await page.keyboard.press("ArrowDown") await page.keyboard.press("ArrowDown")
await wait(page, "") await wait(page, "")
},
{
model: openaiModel,
},
)
},
}) })
}) })
test("shell history stays separate from normal prompt history", async ({ page, sdk, gotoSession }) => { test("shell history stays separate from normal prompt history", async ({ page, llm, backend, withBackendProject }) => {
test.setTimeout(120_000) test.setTimeout(120_000)
await withSession(sdk, `e2e shell history ${Date.now()}`, async (session) => { await withMockOpenAI({
await gotoSession(session.id) serverUrl: backend.url,
llmUrl: llm.url,
const prompt = page.locator(promptSelector) fn: async () => {
const firstToken = `E2E_SHELL_ONE_${Date.now()}` const firstToken = `E2E_SHELL_ONE_${Date.now()}`
const secondToken = `E2E_SHELL_TWO_${Date.now()}` const secondToken = `E2E_SHELL_TWO_${Date.now()}`
const normalToken = `E2E_NORMAL_${Date.now()}` const normalToken = `E2E_NORMAL_${Date.now()}`
@@ -140,19 +156,30 @@ test("shell history stays separate from normal prompt history", async ({ page, s
const second = `echo ${secondToken}` const second = `echo ${secondToken}`
const normal = `Reply with exactly: ${normalToken}` const normal = `Reply with exactly: ${normalToken}`
await llm.textMatch(titleMatch, "E2E Title")
await llm.textMatch(promptMatch(normalToken), normalToken)
await withBackendProject(
async (project) => {
const prompt = page.locator(promptSelector)
await prompt.click() await prompt.click()
await page.keyboard.type("!") await page.keyboard.type("!")
await page.keyboard.type(first) await page.keyboard.type(first)
await page.keyboard.press("Enter") await page.keyboard.press("Enter")
await wait(page, "") await wait(page, "")
await shell(sdk, session.id, first, firstToken)
await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 })
const sessionID = sessionIDFromUrl(page.url())!
project.trackSession(sessionID)
await shell(project.sdk, sessionID, first, firstToken)
await prompt.click() await prompt.click()
await page.keyboard.type("!") await page.keyboard.type("!")
await page.keyboard.type(second) await page.keyboard.type(second)
await page.keyboard.press("Enter") await page.keyboard.press("Enter")
await wait(page, "") await wait(page, "")
await shell(sdk, session.id, second, secondToken) await shell(project.sdk, sessionID, second, secondToken)
await prompt.click() await prompt.click()
await page.keyboard.type("!") await page.keyboard.type("!")
@@ -175,10 +202,16 @@ test("shell history stays separate from normal prompt history", async ({ page, s
await page.keyboard.type(normal) await page.keyboard.type(normal)
await page.keyboard.press("Enter") await page.keyboard.press("Enter")
await wait(page, "") await wait(page, "")
await reply(sdk, session.id, normalToken) await reply(project.sdk, sessionID, normalToken)
await prompt.click() await prompt.click()
await page.keyboard.press("ArrowUp") await page.keyboard.press("ArrowUp")
await wait(page, normal) await wait(page, normal)
},
{
model: openaiModel,
},
)
},
}) })
}) })
+2 -2
View File
@@ -10,10 +10,10 @@ const isBash = (part: unknown): part is ToolPart => {
return "state" in part return "state" in part
} }
test("shell mode runs a command in the project directory", async ({ page, withProject }) => { test("shell mode runs a command in the project directory", async ({ page, withBackendProject }) => {
test.setTimeout(120_000) test.setTimeout(120_000)
await withProject(async ({ directory, gotoSession, trackSession, sdk }) => { await withBackendProject(async ({ directory, gotoSession, trackSession, sdk }) => {
const prompt = page.locator(promptSelector) const prompt = page.locator(promptSelector)
const cmd = process.platform === "win32" ? "dir" : "command ls" const cmd = process.platform === "win32" ? "dir" : "command ls"
@@ -22,14 +22,15 @@ async function seed(sdk: Parameters<typeof withSession>[0], sessionID: string) {
.toBeGreaterThan(0) .toBeGreaterThan(0)
} }
test("/share and /unshare update session share state", async ({ page, sdk, gotoSession }) => { test("/share and /unshare update session share state", async ({ page, withBackendProject }) => {
test.skip(shareDisabled, "Share is disabled in this environment (OPENCODE_DISABLE_SHARE).") test.skip(shareDisabled, "Share is disabled in this environment (OPENCODE_DISABLE_SHARE).")
await withSession(sdk, `e2e slash share ${Date.now()}`, async (session) => { await withBackendProject(async (project) => {
await withSession(project.sdk, `e2e slash share ${Date.now()}`, async (session) => {
const prompt = page.locator(promptSelector) const prompt = page.locator(promptSelector)
await seed(sdk, session.id) await seed(project.sdk, session.id)
await gotoSession(session.id) await project.gotoSession(session.id)
await prompt.click() await prompt.click()
await page.keyboard.type("/share") await page.keyboard.type("/share")
@@ -39,7 +40,7 @@ test("/share and /unshare update session share state", async ({ page, sdk, gotoS
await expect await expect
.poll( .poll(
async () => { async () => {
const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data) const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data)
return data?.share?.url || undefined return data?.share?.url || undefined
}, },
{ timeout: 30_000 }, { timeout: 30_000 },
@@ -54,7 +55,7 @@ test("/share and /unshare update session share state", async ({ page, sdk, gotoS
await expect await expect
.poll( .poll(
async () => { async () => {
const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data) const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data)
return data?.share?.url || undefined return data?.share?.url || undefined
}, },
{ timeout: 30_000 }, { timeout: 30_000 },
@@ -62,3 +63,4 @@ test("/share and /unshare update session share state", async ({ page, sdk, gotoS
.toBeUndefined() .toBeUndefined()
}) })
}) })
})
@@ -1,7 +1,7 @@
import { seedSessionTask, withSession } from "../actions" import { seedSessionTask, withSession } from "../actions"
import { test, expect } from "../fixtures" import { test, expect } from "../fixtures"
test("task tool child-session link does not trigger stale show errors", async ({ page, sdk, gotoSession }) => { test("task tool child-session link does not trigger stale show errors", async ({ page, withBackendProject }) => {
test.setTimeout(120_000) test.setTimeout(120_000)
const errs: string[] = [] const errs: string[] = []
@@ -10,12 +10,15 @@ test("task tool child-session link does not trigger stale show errors", async ({
} }
page.on("pageerror", onError) page.on("pageerror", onError)
await withBackendProject(async ({ gotoSession, trackSession, sdk }) => {
await withSession(sdk, `e2e child nav ${Date.now()}`, async (session) => { await withSession(sdk, `e2e child nav ${Date.now()}`, async (session) => {
trackSession(session.id)
const child = await seedSessionTask(sdk, { const child = await seedSessionTask(sdk, {
sessionID: session.id, sessionID: session.id,
description: "Open child session", description: "Open child session",
prompt: "Search the repository for AssistantParts and then reply with exactly CHILD_OK.", prompt: "Search the repository for AssistantParts and then reply with exactly CHILD_OK.",
}) })
trackSession(child.sessionID)
try { try {
await gotoSession(session.id) await gotoSession(session.id)
@@ -35,3 +38,4 @@ test("task tool child-session link does not trigger stale show errors", async ({
} }
}) })
}) })
})
@@ -256,9 +256,10 @@ async function withMockPermission<T>(
} }
} }
test("default dock shows prompt input", async ({ page, sdk, gotoSession }) => { test("default dock shows prompt input", async ({ page, withBackendProject }) => {
await withDockSession(sdk, "e2e composer dock default", async (session) => { await withBackendProject(async (project) => {
await gotoSession(session.id) await withDockSession(project.sdk, "e2e composer dock default", async (session) => {
await project.gotoSession(session.id)
await expect(page.locator(sessionComposerDockSelector)).toBeVisible() await expect(page.locator(sessionComposerDockSelector)).toBeVisible()
await expect(page.locator(promptSelector)).toBeVisible() await expect(page.locator(promptSelector)).toBeVisible()
@@ -269,8 +270,10 @@ test("default dock shows prompt input", async ({ page, sdk, gotoSession }) => {
await expect(page.locator(promptSelector)).toBeFocused() await expect(page.locator(promptSelector)).toBeFocused()
}) })
}) })
})
test("auto-accept toggle works before first submit", async ({ page, gotoSession }) => { test("auto-accept toggle works before first submit", async ({ page, withBackendProject }) => {
await withBackendProject(async ({ gotoSession }) => {
await gotoSession() await gotoSession()
const button = page.locator('[data-action="prompt-permissions"]').first() const button = page.locator('[data-action="prompt-permissions"]').first()
@@ -280,13 +283,15 @@ test("auto-accept toggle works before first submit", async ({ page, gotoSession
await setAutoAccept(page, true) await setAutoAccept(page, true)
await setAutoAccept(page, false) await setAutoAccept(page, false)
}) })
})
test("blocked question flow unblocks after submit", async ({ page, sdk, gotoSession }) => { test("blocked question flow unblocks after submit", async ({ page, withBackendProject }) => {
await withDockSession(sdk, "e2e composer dock question", async (session) => { await withBackendProject(async (project) => {
await withDockSeed(sdk, session.id, async () => { await withDockSession(project.sdk, "e2e composer dock question", async (session) => {
await gotoSession(session.id) await withDockSeed(project.sdk, session.id, async () => {
await project.gotoSession(session.id)
await seedSessionQuestion(sdk, { await seedSessionQuestion(project.sdk, {
sessionID: session.id, sessionID: session.id,
questions: [ questions: [
{ {
@@ -310,13 +315,15 @@ test("blocked question flow unblocks after submit", async ({ page, sdk, gotoSess
}) })
}) })
}) })
})
test("blocked question flow supports keyboard shortcuts", async ({ page, sdk, gotoSession }) => { test("blocked question flow supports keyboard shortcuts", async ({ page, withBackendProject }) => {
await withDockSession(sdk, "e2e composer dock question keyboard", async (session) => { await withBackendProject(async (project) => {
await withDockSeed(sdk, session.id, async () => { await withDockSession(project.sdk, "e2e composer dock question keyboard", async (session) => {
await gotoSession(session.id) await withDockSeed(project.sdk, session.id, async () => {
await project.gotoSession(session.id)
await seedSessionQuestion(sdk, { await seedSessionQuestion(project.sdk, {
sessionID: session.id, sessionID: session.id,
questions: [ questions: [
{ {
@@ -346,13 +353,15 @@ test("blocked question flow supports keyboard shortcuts", async ({ page, sdk, go
}) })
}) })
}) })
})
test("blocked question flow supports escape dismiss", async ({ page, sdk, gotoSession }) => { test("blocked question flow supports escape dismiss", async ({ page, withBackendProject }) => {
await withDockSession(sdk, "e2e composer dock question escape", async (session) => { await withBackendProject(async (project) => {
await withDockSeed(sdk, session.id, async () => { await withDockSession(project.sdk, "e2e composer dock question escape", async (session) => {
await gotoSession(session.id) await withDockSeed(project.sdk, session.id, async () => {
await project.gotoSession(session.id)
await seedSessionQuestion(sdk, { await seedSessionQuestion(project.sdk, {
sessionID: session.id, sessionID: session.id,
questions: [ questions: [
{ {
@@ -377,10 +386,12 @@ test("blocked question flow supports escape dismiss", async ({ page, sdk, gotoSe
}) })
}) })
}) })
})
test("blocked permission flow supports allow once", async ({ page, sdk, gotoSession }) => { test("blocked permission flow supports allow once", async ({ page, withBackendProject }) => {
await withDockSession(sdk, "e2e composer dock permission once", async (session) => { await withBackendProject(async (project) => {
await gotoSession(session.id) await withDockSession(project.sdk, "e2e composer dock permission once", async (session) => {
await project.gotoSession(session.id)
await setAutoAccept(page, false) await setAutoAccept(page, false)
await withMockPermission( await withMockPermission(
page, page,
@@ -404,10 +415,12 @@ test("blocked permission flow supports allow once", async ({ page, sdk, gotoSess
) )
}) })
}) })
})
test("blocked permission flow supports reject", async ({ page, sdk, gotoSession }) => { test("blocked permission flow supports reject", async ({ page, withBackendProject }) => {
await withDockSession(sdk, "e2e composer dock permission reject", async (session) => { await withBackendProject(async (project) => {
await gotoSession(session.id) await withDockSession(project.sdk, "e2e composer dock permission reject", async (session) => {
await project.gotoSession(session.id)
await setAutoAccept(page, false) await setAutoAccept(page, false)
await withMockPermission( await withMockPermission(
page, page,
@@ -430,10 +443,12 @@ test("blocked permission flow supports reject", async ({ page, sdk, gotoSession
) )
}) })
}) })
})
test("blocked permission flow supports allow always", async ({ page, sdk, gotoSession }) => { test("blocked permission flow supports allow always", async ({ page, withBackendProject }) => {
await withDockSession(sdk, "e2e composer dock permission always", async (session) => { await withBackendProject(async (project) => {
await gotoSession(session.id) await withDockSession(project.sdk, "e2e composer dock permission always", async (session) => {
await project.gotoSession(session.id)
await setAutoAccept(page, false) await setAutoAccept(page, false)
await withMockPermission( await withMockPermission(
page, page,
@@ -457,16 +472,17 @@ test("blocked permission flow supports allow always", async ({ page, sdk, gotoSe
) )
}) })
}) })
})
test("child session question request blocks parent dock and unblocks after submit", async ({ test("child session question request blocks parent dock and unblocks after submit", async ({
page, page,
sdk, withBackendProject,
gotoSession,
}) => { }) => {
await withDockSession(sdk, "e2e composer dock child question parent", async (session) => { await withBackendProject(async (project) => {
await gotoSession(session.id) await withDockSession(project.sdk, "e2e composer dock child question parent", async (session) => {
await project.gotoSession(session.id)
const child = await sdk.session const child = await project.sdk.session
.create({ .create({
title: "e2e composer dock child question", title: "e2e composer dock child question",
parentID: session.id, parentID: session.id,
@@ -475,8 +491,8 @@ test("child session question request blocks parent dock and unblocks after submi
if (!child?.id) throw new Error("Child session create did not return an id") if (!child?.id) throw new Error("Child session create did not return an id")
try { try {
await withDockSeed(sdk, child.id, async () => { await withDockSeed(project.sdk, child.id, async () => {
await seedSessionQuestion(sdk, { await seedSessionQuestion(project.sdk, {
sessionID: child.id, sessionID: child.id,
questions: [ questions: [
{ {
@@ -499,21 +515,22 @@ test("child session question request blocks parent dock and unblocks after submi
await expectQuestionOpen(page) await expectQuestionOpen(page)
}) })
} finally { } finally {
await cleanupSession({ sdk, sessionID: child.id }) await cleanupSession({ sdk: project.sdk, sessionID: child.id })
} }
}) })
}) })
})
test("child session permission request blocks parent dock and supports allow once", async ({ test("child session permission request blocks parent dock and supports allow once", async ({
page, page,
sdk, withBackendProject,
gotoSession,
}) => { }) => {
await withDockSession(sdk, "e2e composer dock child permission parent", async (session) => { await withBackendProject(async (project) => {
await gotoSession(session.id) await withDockSession(project.sdk, "e2e composer dock child permission parent", async (session) => {
await project.gotoSession(session.id)
await setAutoAccept(page, false) await setAutoAccept(page, false)
const child = await sdk.session const child = await project.sdk.session
.create({ .create({
title: "e2e composer dock child permission", title: "e2e composer dock child permission",
parentID: session.id, parentID: session.id,
@@ -544,15 +561,17 @@ test("child session permission request blocks parent dock and supports allow onc
}, },
) )
} finally { } finally {
await cleanupSession({ sdk, sessionID: child.id }) await cleanupSession({ sdk: project.sdk, sessionID: child.id })
} }
}) })
}) })
})
test("todo dock transitions and collapse behavior", async ({ page, sdk, gotoSession }) => { test("todo dock transitions and collapse behavior", async ({ page, withBackendProject }) => {
await withDockSession(sdk, "e2e composer dock todo", async (session) => { await withBackendProject(async (project) => {
await withDockSession(project.sdk, "e2e composer dock todo", async (session) => {
const dock = await todoDock(page, session.id) const dock = await todoDock(page, session.id)
await gotoSession(session.id) await project.gotoSession(session.id)
await expect(page.locator(sessionComposerDockSelector)).toBeVisible() await expect(page.locator(sessionComposerDockSelector)).toBeVisible()
try { try {
@@ -578,13 +597,15 @@ test("todo dock transitions and collapse behavior", async ({ page, sdk, gotoSess
} }
}) })
}) })
})
test("keyboard focus stays off prompt while blocked", async ({ page, sdk, gotoSession }) => { test("keyboard focus stays off prompt while blocked", async ({ page, withBackendProject }) => {
await withDockSession(sdk, "e2e composer dock keyboard", async (session) => { await withBackendProject(async (project) => {
await withDockSeed(sdk, session.id, async () => { await withDockSession(project.sdk, "e2e composer dock keyboard", async (session) => {
await gotoSession(session.id) await withDockSeed(project.sdk, session.id, async () => {
await project.gotoSession(session.id)
await seedSessionQuestion(sdk, { await seedSessionQuestion(project.sdk, {
sessionID: session.id, sessionID: session.id,
questions: [ questions: [
{ {
@@ -603,3 +624,4 @@ test("keyboard focus stays off prompt while blocked", async ({ page, sdk, gotoSe
}) })
}) })
}) })
})
@@ -49,13 +49,13 @@ async function seedConversation(input: {
return { prompt, userMessageID } return { prompt, userMessageID }
} }
test("slash undo sets revert and restores prior prompt", async ({ page, withProject }) => { test("slash undo sets revert and restores prior prompt", async ({ page, withBackendProject }) => {
test.setTimeout(120_000) test.setTimeout(120_000)
const token = `undo_${Date.now()}` const token = `undo_${Date.now()}`
await withProject(async (project) => { await withBackendProject(async (project) => {
const sdk = createSdk(project.directory) const sdk = project.sdk
await withSession(sdk, `e2e undo ${Date.now()}`, async (session) => { await withSession(sdk, `e2e undo ${Date.now()}`, async (session) => {
await project.gotoSession(session.id) await project.gotoSession(session.id)
@@ -81,13 +81,13 @@ test("slash undo sets revert and restores prior prompt", async ({ page, withProj
}) })
}) })
test("slash redo clears revert and restores latest state", async ({ page, withProject }) => { test("slash redo clears revert and restores latest state", async ({ page, withBackendProject }) => {
test.setTimeout(120_000) test.setTimeout(120_000)
const token = `redo_${Date.now()}` const token = `redo_${Date.now()}`
await withProject(async (project) => { await withBackendProject(async (project) => {
const sdk = createSdk(project.directory) const sdk = project.sdk
await withSession(sdk, `e2e redo ${Date.now()}`, async (session) => { await withSession(sdk, `e2e redo ${Date.now()}`, async (session) => {
await project.gotoSession(session.id) await project.gotoSession(session.id)
@@ -128,14 +128,14 @@ test("slash redo clears revert and restores latest state", async ({ page, withPr
}) })
}) })
test("slash undo/redo traverses multi-step revert stack", async ({ page, withProject }) => { test("slash undo/redo traverses multi-step revert stack", async ({ page, withBackendProject }) => {
test.setTimeout(120_000) test.setTimeout(120_000)
const firstToken = `undo_redo_first_${Date.now()}` const firstToken = `undo_redo_first_${Date.now()}`
const secondToken = `undo_redo_second_${Date.now()}` const secondToken = `undo_redo_second_${Date.now()}`
await withProject(async (project) => { await withBackendProject(async (project) => {
const sdk = createSdk(project.directory) const sdk = project.sdk
await withSession(sdk, `e2e undo redo stack ${Date.now()}`, async (session) => { await withSession(sdk, `e2e undo redo stack ${Date.now()}`, async (session) => {
await project.gotoSession(session.id) await project.gotoSession(session.id)
+29 -21
View File
@@ -31,14 +31,15 @@ async function seedMessage(sdk: Sdk, sessionID: string) {
.toBeGreaterThan(0) .toBeGreaterThan(0)
} }
test("session can be renamed via header menu", async ({ page, sdk, gotoSession }) => { test("session can be renamed via header menu", async ({ page, withBackendProject }) => {
const stamp = Date.now() const stamp = Date.now()
const originalTitle = `e2e rename test ${stamp}` const originalTitle = `e2e rename test ${stamp}`
const renamedTitle = `e2e renamed ${stamp}` const renamedTitle = `e2e renamed ${stamp}`
await withSession(sdk, originalTitle, async (session) => { await withBackendProject(async (project) => {
await seedMessage(sdk, session.id) await withSession(project.sdk, originalTitle, async (session) => {
await gotoSession(session.id) await seedMessage(project.sdk, session.id)
await project.gotoSession(session.id)
await expect(page.getByRole("heading", { level: 1 }).first()).toHaveText(originalTitle) await expect(page.getByRole("heading", { level: 1 }).first()).toHaveText(originalTitle)
const menu = await openSessionMoreMenu(page, session.id) const menu = await openSessionMoreMenu(page, session.id)
@@ -54,7 +55,7 @@ test("session can be renamed via header menu", async ({ page, sdk, gotoSession }
await expect await expect
.poll( .poll(
async () => { async () => {
const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data) const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data)
return data?.title return data?.title
}, },
{ timeout: 30_000 }, { timeout: 30_000 },
@@ -64,21 +65,23 @@ test("session can be renamed via header menu", async ({ page, sdk, gotoSession }
await expect(page.getByRole("heading", { level: 1 }).first()).toHaveText(renamedTitle) await expect(page.getByRole("heading", { level: 1 }).first()).toHaveText(renamedTitle)
}) })
}) })
})
test("session can be archived via header menu", async ({ page, sdk, gotoSession }) => { test("session can be archived via header menu", async ({ page, withBackendProject }) => {
const stamp = Date.now() const stamp = Date.now()
const title = `e2e archive test ${stamp}` const title = `e2e archive test ${stamp}`
await withSession(sdk, title, async (session) => { await withBackendProject(async (project) => {
await seedMessage(sdk, session.id) await withSession(project.sdk, title, async (session) => {
await gotoSession(session.id) await seedMessage(project.sdk, session.id)
await project.gotoSession(session.id)
const menu = await openSessionMoreMenu(page, session.id) const menu = await openSessionMoreMenu(page, session.id)
await clickMenuItem(menu, /archive/i) await clickMenuItem(menu, /archive/i)
await expect await expect
.poll( .poll(
async () => { async () => {
const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data) const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data)
return data?.time?.archived return data?.time?.archived
}, },
{ timeout: 30_000 }, { timeout: 30_000 },
@@ -89,14 +92,16 @@ test("session can be archived via header menu", async ({ page, sdk, gotoSession
await expect(page.locator(sessionItemSelector(session.id))).toHaveCount(0) await expect(page.locator(sessionItemSelector(session.id))).toHaveCount(0)
}) })
}) })
})
test("session can be deleted via header menu", async ({ page, sdk, gotoSession }) => { test("session can be deleted via header menu", async ({ page, withBackendProject }) => {
const stamp = Date.now() const stamp = Date.now()
const title = `e2e delete test ${stamp}` const title = `e2e delete test ${stamp}`
await withSession(sdk, title, async (session) => { await withBackendProject(async (project) => {
await seedMessage(sdk, session.id) await withSession(project.sdk, title, async (session) => {
await gotoSession(session.id) await seedMessage(project.sdk, session.id)
await project.gotoSession(session.id)
const menu = await openSessionMoreMenu(page, session.id) const menu = await openSessionMoreMenu(page, session.id)
await clickMenuItem(menu, /delete/i) await clickMenuItem(menu, /delete/i)
await confirmDialog(page, /delete/i) await confirmDialog(page, /delete/i)
@@ -104,7 +109,7 @@ test("session can be deleted via header menu", async ({ page, sdk, gotoSession }
await expect await expect
.poll( .poll(
async () => { async () => {
const data = await sdk.session const data = await project.sdk.session
.get({ sessionID: session.id }) .get({ sessionID: session.id })
.then((r) => r.data) .then((r) => r.data)
.catch(() => undefined) .catch(() => undefined)
@@ -118,16 +123,18 @@ test("session can be deleted via header menu", async ({ page, sdk, gotoSession }
await expect(page.locator(sessionItemSelector(session.id))).toHaveCount(0) await expect(page.locator(sessionItemSelector(session.id))).toHaveCount(0)
}) })
}) })
})
test("session can be shared and unshared via header button", async ({ page, sdk, gotoSession }) => { test("session can be shared and unshared via header button", async ({ page, withBackendProject }) => {
test.skip(shareDisabled, "Share is disabled in this environment (OPENCODE_DISABLE_SHARE).") test.skip(shareDisabled, "Share is disabled in this environment (OPENCODE_DISABLE_SHARE).")
const stamp = Date.now() const stamp = Date.now()
const title = `e2e share test ${stamp}` const title = `e2e share test ${stamp}`
await withSession(sdk, title, async (session) => { await withBackendProject(async (project) => {
await seedMessage(sdk, session.id) await withSession(project.sdk, title, async (session) => {
await gotoSession(session.id) await seedMessage(project.sdk, session.id)
await project.gotoSession(session.id)
const shared = await openSharePopover(page) const shared = await openSharePopover(page)
const publish = shared.popoverBody.getByRole("button", { name: "Publish" }).first() const publish = shared.popoverBody.getByRole("button", { name: "Publish" }).first()
@@ -141,7 +148,7 @@ test("session can be shared and unshared via header button", async ({ page, sdk,
await expect await expect
.poll( .poll(
async () => { async () => {
const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data) const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data)
return data?.share?.url || undefined return data?.share?.url || undefined
}, },
{ timeout: 30_000 }, { timeout: 30_000 },
@@ -159,7 +166,7 @@ test("session can be shared and unshared via header button", async ({ page, sdk,
await expect await expect
.poll( .poll(
async () => { async () => {
const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data) const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data)
return data?.share?.url || undefined return data?.share?.url || undefined
}, },
{ timeout: 30_000 }, { timeout: 30_000 },
@@ -172,3 +179,4 @@ test("session can be shared and unshared via header button", async ({ page, sdk,
}) })
}) })
}) })
})