test(app): migrate more e2e suites to isolated backend (#20505)
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user