diff options
| author | Dax Raad <[email protected]> | 2026-04-14 23:10:07 -0400 |
|---|---|---|
| committer | Dax Raad <[email protected]> | 2026-04-14 23:10:25 -0400 |
| commit | 627159acac04409d7697a6739e2c572c2a010943 (patch) | |
| tree | 5f87465ea69f41aff0cd96ae5411fe438da480b3 /packages/app/e2e/session | |
| parent | f44aa02e2677b2b89a1a9f517c0ff8990383deaa (diff) | |
| download | opencode-627159acac04409d7697a6739e2c572c2a010943.tar.gz opencode-627159acac04409d7697a6739e2c572c2a010943.zip | |
delete all e2e tests (#22501)
Cherry-picked from ea463e604cdd2a3e83e1c286e39b789455f0d413
Diffstat (limited to 'packages/app/e2e/session')
| -rw-r--r-- | packages/app/e2e/session/session-child-navigation.spec.ts | 64 | ||||
| -rw-r--r-- | packages/app/e2e/session/session-composer-dock.spec.ts | 655 | ||||
| -rw-r--r-- | packages/app/e2e/session/session-model-persistence.spec.ts | 362 | ||||
| -rw-r--r-- | packages/app/e2e/session/session-review.spec.ts | 440 | ||||
| -rw-r--r-- | packages/app/e2e/session/session-undo-redo.spec.ts | 233 | ||||
| -rw-r--r-- | packages/app/e2e/session/session.spec.ts | 182 |
6 files changed, 0 insertions, 1936 deletions
diff --git a/packages/app/e2e/session/session-child-navigation.spec.ts b/packages/app/e2e/session/session-child-navigation.spec.ts deleted file mode 100644 index c9fad1af8..000000000 --- a/packages/app/e2e/session/session-child-navigation.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { seedSessionTask, withSession } from "../actions" -import { test, expect } from "../fixtures" -import { inputMatch } from "../prompt/mock" - -test("task tool child-session link does not trigger stale show errors", async ({ page, llm, project }) => { - test.setTimeout(120_000) - - const errs: string[] = [] - const onError = (err: Error) => { - errs.push(err.message) - } - page.on("pageerror", onError) - - try { - await project.open() - await withSession(project.sdk, `e2e child nav ${Date.now()}`, async (session) => { - const taskInput = { - description: "Open child session", - prompt: "Search the repository for AssistantParts and then reply with exactly CHILD_OK.", - subagent_type: "general", - } - await llm.toolMatch(inputMatch(taskInput), "task", taskInput) - const child = await seedSessionTask(project.sdk, { - sessionID: session.id, - description: taskInput.description, - prompt: taskInput.prompt, - }) - project.trackSession(child.sessionID) - - await project.gotoSession(session.id) - - const header = page.locator("[data-session-title]") - await expect(header.getByRole("button", { name: "More options" })).toBeVisible({ timeout: 30_000 }) - - const card = page - .locator('[data-component="task-tool-card"]') - .filter({ hasText: /open child session/i }) - .first() - await expect(card).toBeVisible({ timeout: 30_000 }) - await card.click() - - await expect(page).toHaveURL(new RegExp(`/session/${child.sessionID}(?:[/?#]|$)`), { timeout: 30_000 }) - await expect(header.locator('[data-slot="session-title-parent"]')).toHaveText(session.title) - await expect(header.locator('[data-slot="session-title-child"]')).toHaveText(taskInput.description) - await expect(header.locator('[data-slot="session-title-separator"]')).toHaveText("/") - await expect - .poll( - () => - header.locator('[data-slot="session-title-separator"]').evaluate((el) => ({ - left: getComputedStyle(el).paddingLeft, - right: getComputedStyle(el).paddingRight, - })), - { timeout: 30_000 }, - ) - .toEqual({ left: "8px", right: "8px" }) - await expect(header.getByRole("button", { name: "More options" })).toHaveCount(0) - await expect(page.getByText("Subagent sessions cannot be prompted.")).toBeVisible({ timeout: 30_000 }) - await expect(page.getByRole("button", { name: "Back to main session." })).toBeVisible({ timeout: 30_000 }) - await expect.poll(() => errs, { timeout: 5_000 }).toEqual([]) - }) - } finally { - page.off("pageerror", onError) - } -}) diff --git a/packages/app/e2e/session/session-composer-dock.spec.ts b/packages/app/e2e/session/session-composer-dock.spec.ts deleted file mode 100644 index ecacea83d..000000000 --- a/packages/app/e2e/session/session-composer-dock.spec.ts +++ /dev/null @@ -1,655 +0,0 @@ -import { test, expect } from "../fixtures" -import { - composerEvent, - type ComposerDriverState, - type ComposerProbeState, - type ComposerWindow, -} from "../../src/testing/session-composer" -import { cleanupSession, clearSessionDockSeed, closeDialog, openSettings, seedSessionQuestion } from "../actions" -import { - permissionDockSelector, - promptSelector, - questionDockSelector, - sessionComposerDockSelector, - sessionTodoToggleButtonSelector, -} from "../selectors" -import { modKey } from "../utils" -import { inputMatch } from "../prompt/mock" - -type Sdk = Parameters<typeof clearSessionDockSeed>[0] -type PermissionRule = { permission: string; pattern: string; action: "allow" | "deny" | "ask" } - -async function withDockSession<T>( - sdk: Sdk, - title: string, - fn: (session: { id: string; title: string }) => Promise<T>, - opts?: { permission?: PermissionRule[]; trackSession?: (sessionID: string) => void }, -) { - const session = await sdk.session - .create(opts?.permission ? { title, permission: opts.permission } : { title }) - .then((r) => r.data) - if (!session?.id) throw new Error("Session create did not return an id") - opts?.trackSession?.(session.id) - try { - return await fn(session) - } finally { - await cleanupSession({ sdk, sessionID: session.id }) - } -} - -const defaultQuestions = [ - { - header: "Need input", - question: "Pick one option", - options: [ - { label: "Continue", description: "Continue now" }, - { label: "Stop", description: "Stop here" }, - ], - }, -] - -test.setTimeout(120_000) - -async function withDockSeed<T>(sdk: Sdk, sessionID: string, fn: () => Promise<T>) { - try { - return await fn() - } finally { - await clearSessionDockSeed(sdk, sessionID).catch(() => undefined) - } -} - -async function clearPermissionDock(page: any, label: RegExp) { - const dock = page.locator(permissionDockSelector) - await expect(dock).toBeVisible() - await dock.getByRole("button", { name: label }).click() -} - -async function setAutoAccept(page: any, enabled: boolean) { - const dialog = await openSettings(page) - const toggle = dialog.locator('[data-action="settings-auto-accept-permissions"]').first() - const input = toggle.locator('[data-slot="switch-input"]').first() - await expect(toggle).toBeVisible() - const checked = (await input.getAttribute("aria-checked")) === "true" - if (checked !== enabled) await toggle.locator('[data-slot="switch-control"]').click() - await expect(input).toHaveAttribute("aria-checked", enabled ? "true" : "false") - await closeDialog(page, dialog) -} - -async function expectQuestionBlocked(page: any) { - await expect(page.locator(questionDockSelector)).toBeVisible() - await expect(page.locator(promptSelector)).toHaveCount(0) -} - -async function expectQuestionOpen(page: any) { - await expect(page.locator(questionDockSelector)).toHaveCount(0) - await expect(page.locator(promptSelector)).toBeVisible() -} - -async function expectPermissionBlocked(page: any) { - await expect(page.locator(permissionDockSelector)).toBeVisible() - await expect(page.locator(promptSelector)).toHaveCount(0) -} - -async function expectPermissionOpen(page: any) { - await expect(page.locator(permissionDockSelector)).toHaveCount(0) - await expect(page.locator(promptSelector)).toBeVisible() -} - -async function todoDock(page: any, sessionID: string) { - await page.addInitScript(() => { - const win = window as ComposerWindow - win.__opencode_e2e = { - ...win.__opencode_e2e, - composer: { - enabled: true, - sessions: {}, - }, - } - }) - - const write = async (driver: ComposerDriverState | undefined) => { - await page.evaluate( - (input: { event: string; sessionID: string; driver: ComposerDriverState | undefined }) => { - const win = window as ComposerWindow - const composer = win.__opencode_e2e?.composer - if (!composer?.enabled) throw new Error("Composer e2e driver is not enabled") - composer.sessions ??= {} - const prev = composer.sessions[input.sessionID] ?? {} - if (!input.driver) { - if (!prev.probe) { - delete composer.sessions[input.sessionID] - } else { - composer.sessions[input.sessionID] = { probe: prev.probe } - } - } else { - composer.sessions[input.sessionID] = { - ...prev, - driver: input.driver, - } - } - window.dispatchEvent(new CustomEvent(input.event, { detail: { sessionID: input.sessionID } })) - }, - { event: composerEvent, sessionID, driver }, - ) - } - - const read = () => - page.evaluate((sessionID: string) => { - const win = window as ComposerWindow - return win.__opencode_e2e?.composer?.sessions?.[sessionID]?.probe ?? null - }, sessionID) as Promise<ComposerProbeState | null> - - const api = { - async clear() { - await write(undefined) - return api - }, - async open(todos: NonNullable<ComposerDriverState["todos"]>) { - await write({ live: true, todos }) - return api - }, - async finish(todos: NonNullable<ComposerDriverState["todos"]>) { - await write({ live: false, todos }) - return api - }, - async expectOpen(states: ComposerProbeState["states"]) { - await expect.poll(read, { timeout: 10_000 }).toMatchObject({ - mounted: true, - collapsed: false, - hidden: false, - count: states.length, - states, - }) - return api - }, - async expectCollapsed(states: ComposerProbeState["states"]) { - await expect.poll(read, { timeout: 10_000 }).toMatchObject({ - mounted: true, - collapsed: true, - hidden: true, - count: states.length, - states, - }) - return api - }, - async expectClosed() { - await expect.poll(read, { timeout: 10_000 }).toMatchObject({ mounted: false }) - return api - }, - async collapse() { - await page.locator(sessionTodoToggleButtonSelector).click() - return api - }, - async expand() { - await page.locator(sessionTodoToggleButtonSelector).click() - return api - }, - } - - return api -} - -async function withMockPermission<T>( - page: any, - request: { - id: string - sessionID: string - permission: string - patterns: string[] - metadata?: Record<string, unknown> - always?: string[] - }, - opts: { child?: any } | undefined, - fn: (state: { resolved: () => Promise<void> }) => Promise<T>, -) { - const listUrl = /\/permission(?:\?.*)?$/ - const replyUrls = [/\/session\/[^/]+\/permissions\/[^/?]+(?:\?.*)?$/, /\/permission\/[^/]+\/reply(?:\?.*)?$/] - let pending = [ - { - ...request, - always: request.always ?? ["*"], - metadata: request.metadata ?? {}, - }, - ] - - const list = async (route: any) => { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify(pending), - }) - } - - const reply = async (route: any) => { - const url = new URL(route.request().url()) - const parts = url.pathname.split("/").filter(Boolean) - const id = parts.at(-1) === "reply" ? parts.at(-2) : parts.at(-1) - pending = pending.filter((item) => item.id !== id) - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify(true), - }) - } - - await page.route(listUrl, list) - for (const item of replyUrls) { - await page.route(item, reply) - } - - const sessionList = opts?.child - ? async (route: any) => { - const res = await route.fetch() - const json = await res.json() - const list = Array.isArray(json) ? json : Array.isArray(json?.data) ? json.data : undefined - if (Array.isArray(list) && !list.some((item) => item?.id === opts.child?.id)) list.push(opts.child) - await route.fulfill({ - response: res, - body: JSON.stringify(json), - }) - } - : undefined - - if (sessionList) await page.route("**/session?*", sessionList) - - const state = { - async resolved() { - await expect.poll(() => pending.length, { timeout: 10_000 }).toBe(0) - }, - } - - try { - return await fn(state) - } finally { - await page.unroute(listUrl, list) - for (const item of replyUrls) { - await page.unroute(item, reply) - } - if (sessionList) await page.unroute("**/session?*", sessionList) - } -} - -test("default dock shows prompt input", async ({ page, project }) => { - await project.open() - 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(promptSelector)).toBeVisible() - await expect(page.locator('[data-action="prompt-permissions"]')).toHaveCount(0) - await expect(page.locator(questionDockSelector)).toHaveCount(0) - await expect(page.locator(permissionDockSelector)).toHaveCount(0) - - await page.locator(promptSelector).click() - await expect(page.locator(promptSelector)).toBeFocused() - }, - { trackSession: project.trackSession }, - ) -}) - -test("auto-accept toggle works before first submit", async ({ page, project }) => { - await project.open() - - await setAutoAccept(page, true) - await setAutoAccept(page, false) -}) - -test("blocked question flow unblocks after submit", async ({ page, llm, project }) => { - await project.open() - await withDockSession( - project.sdk, - "e2e composer dock question", - async (session) => { - await withDockSeed(project.sdk, session.id, async () => { - await project.gotoSession(session.id) - - await llm.toolMatch(inputMatch({ questions: defaultQuestions }), "question", { questions: defaultQuestions }) - await seedSessionQuestion(project.sdk, { - sessionID: session.id, - questions: defaultQuestions, - }) - - const dock = page.locator(questionDockSelector) - await expectQuestionBlocked(page) - - await dock.locator('[data-slot="question-option"]').first().click() - await dock.getByRole("button", { name: /submit/i }).click() - - await expectQuestionOpen(page) - }) - }, - { trackSession: project.trackSession }, - ) -}) - -test("blocked question flow supports keyboard shortcuts", async ({ page, llm, project }) => { - await project.open() - await withDockSession( - project.sdk, - "e2e composer dock question keyboard", - async (session) => { - await withDockSeed(project.sdk, session.id, async () => { - await project.gotoSession(session.id) - - await llm.toolMatch(inputMatch({ questions: defaultQuestions }), "question", { questions: defaultQuestions }) - await seedSessionQuestion(project.sdk, { - sessionID: session.id, - questions: defaultQuestions, - }) - - const dock = page.locator(questionDockSelector) - const first = dock.locator('[data-slot="question-option"]').first() - const second = dock.locator('[data-slot="question-option"]').nth(1) - - await expectQuestionBlocked(page) - await expect(first).toBeFocused() - - await page.keyboard.press("ArrowDown") - await expect(second).toBeFocused() - - await page.keyboard.press("Space") - await page.keyboard.press(`${modKey}+Enter`) - await expectQuestionOpen(page) - }) - }, - { trackSession: project.trackSession }, - ) -}) - -test("blocked question flow supports escape dismiss", async ({ page, llm, project }) => { - await project.open() - await withDockSession( - project.sdk, - "e2e composer dock question escape", - async (session) => { - await withDockSeed(project.sdk, session.id, async () => { - await project.gotoSession(session.id) - - await llm.toolMatch(inputMatch({ questions: defaultQuestions }), "question", { questions: defaultQuestions }) - await seedSessionQuestion(project.sdk, { - sessionID: session.id, - questions: defaultQuestions, - }) - - const dock = page.locator(questionDockSelector) - const first = dock.locator('[data-slot="question-option"]').first() - - await expectQuestionBlocked(page) - await expect(first).toBeFocused() - - await page.keyboard.press("Escape") - await expectQuestionOpen(page) - }) - }, - { trackSession: project.trackSession }, - ) -}) - -test("blocked permission flow supports allow once", async ({ page, project }) => { - await project.open() - await withDockSession( - project.sdk, - "e2e composer dock permission once", - async (session) => { - await project.gotoSession(session.id) - await setAutoAccept(page, false) - await withMockPermission( - page, - { - id: "per_e2e_once", - sessionID: session.id, - permission: "bash", - patterns: ["/tmp/opencode-e2e-perm-once"], - metadata: { description: "Need permission for command" }, - }, - undefined, - async (state) => { - await page.goto(page.url()) - await expectPermissionBlocked(page) - - await clearPermissionDock(page, /allow once/i) - await state.resolved() - await page.goto(page.url()) - await expectPermissionOpen(page) - }, - ) - }, - { trackSession: project.trackSession }, - ) -}) - -test("blocked permission flow supports reject", async ({ page, project }) => { - await project.open() - await withDockSession( - project.sdk, - "e2e composer dock permission reject", - async (session) => { - await project.gotoSession(session.id) - await setAutoAccept(page, false) - await withMockPermission( - page, - { - id: "per_e2e_reject", - sessionID: session.id, - permission: "bash", - patterns: ["/tmp/opencode-e2e-perm-reject"], - }, - undefined, - async (state) => { - await page.goto(page.url()) - await expectPermissionBlocked(page) - - await clearPermissionDock(page, /deny/i) - await state.resolved() - await page.goto(page.url()) - await expectPermissionOpen(page) - }, - ) - }, - { trackSession: project.trackSession }, - ) -}) - -test("blocked permission flow supports allow always", async ({ page, project }) => { - await project.open() - await withDockSession( - project.sdk, - "e2e composer dock permission always", - async (session) => { - await project.gotoSession(session.id) - await setAutoAccept(page, false) - await withMockPermission( - page, - { - id: "per_e2e_always", - sessionID: session.id, - permission: "bash", - patterns: ["/tmp/opencode-e2e-perm-always"], - metadata: { description: "Need permission for command" }, - }, - undefined, - async (state) => { - await page.goto(page.url()) - await expectPermissionBlocked(page) - - await clearPermissionDock(page, /allow always/i) - await state.resolved() - await page.goto(page.url()) - await expectPermissionOpen(page) - }, - ) - }, - { trackSession: project.trackSession }, - ) -}) - -test("child session question request blocks parent dock and unblocks after submit", async ({ page, llm, project }) => { - const questions = [ - { - header: "Child input", - question: "Pick one child option", - options: [ - { label: "Continue", description: "Continue child" }, - { label: "Stop", description: "Stop child" }, - ], - }, - ] - await project.open() - await withDockSession( - project.sdk, - "e2e composer dock child question parent", - async (session) => { - await project.gotoSession(session.id) - - const child = await project.sdk.session - .create({ - title: "e2e composer dock child question", - parentID: session.id, - }) - .then((r) => r.data) - if (!child?.id) throw new Error("Child session create did not return an id") - project.trackSession(child.id) - - try { - await withDockSeed(project.sdk, child.id, async () => { - await llm.toolMatch(inputMatch({ questions }), "question", { questions }) - await seedSessionQuestion(project.sdk, { - sessionID: child.id, - questions, - }) - - const dock = page.locator(questionDockSelector) - await expectQuestionBlocked(page) - - await dock.locator('[data-slot="question-option"]').first().click() - await dock.getByRole("button", { name: /submit/i }).click() - - await expectQuestionOpen(page) - }) - } finally { - await cleanupSession({ sdk: project.sdk, sessionID: child.id }) - } - }, - { trackSession: project.trackSession }, - ) -}) - -test("child session permission request blocks parent dock and supports allow once", async ({ page, project }) => { - await project.open() - await withDockSession( - project.sdk, - "e2e composer dock child permission parent", - async (session) => { - await project.gotoSession(session.id) - await setAutoAccept(page, false) - - const child = await project.sdk.session - .create({ - title: "e2e composer dock child permission", - parentID: session.id, - }) - .then((r) => r.data) - if (!child?.id) throw new Error("Child session create did not return an id") - project.trackSession(child.id) - - try { - await withMockPermission( - page, - { - id: "per_e2e_child", - sessionID: child.id, - permission: "bash", - patterns: ["/tmp/opencode-e2e-perm-child"], - metadata: { description: "Need child permission" }, - }, - { child }, - async (state) => { - await page.goto(page.url()) - await expectPermissionBlocked(page) - - await clearPermissionDock(page, /allow once/i) - await state.resolved() - await page.goto(page.url()) - - await expectPermissionOpen(page) - }, - ) - } finally { - await cleanupSession({ sdk: project.sdk, sessionID: child.id }) - } - }, - { trackSession: project.trackSession }, - ) -}) - -test("todo dock transitions and collapse behavior", async ({ page, project }) => { - await project.open() - await withDockSession( - project.sdk, - "e2e composer dock todo", - async (session) => { - const dock = await todoDock(page, session.id) - await project.gotoSession(session.id) - await expect(page.locator(sessionComposerDockSelector)).toBeVisible() - - try { - await dock.open([ - { content: "first task", status: "pending", priority: "high" }, - { content: "second task", status: "in_progress", priority: "medium" }, - ]) - await dock.expectOpen(["pending", "in_progress"]) - - await dock.collapse() - await dock.expectCollapsed(["pending", "in_progress"]) - - await dock.expand() - await dock.expectOpen(["pending", "in_progress"]) - - await dock.finish([ - { content: "first task", status: "completed", priority: "high" }, - { content: "second task", status: "cancelled", priority: "medium" }, - ]) - await dock.expectClosed() - } finally { - await dock.clear() - } - }, - { trackSession: project.trackSession }, - ) -}) - -test("keyboard focus stays off prompt while blocked", async ({ page, llm, project }) => { - const questions = [ - { - header: "Need input", - question: "Pick one option", - options: [{ label: "Continue", description: "Continue now" }], - }, - ] - await project.open() - await withDockSession( - project.sdk, - "e2e composer dock keyboard", - async (session) => { - await withDockSeed(project.sdk, session.id, async () => { - await project.gotoSession(session.id) - - await llm.toolMatch(inputMatch({ questions }), "question", { questions }) - await seedSessionQuestion(project.sdk, { - sessionID: session.id, - questions, - }) - - await expectQuestionBlocked(page) - - await page.locator("main").click({ position: { x: 5, y: 5 } }) - await page.keyboard.type("abc") - await expect(page.locator(promptSelector)).toHaveCount(0) - }) - }, - { trackSession: project.trackSession }, - ) -}) diff --git a/packages/app/e2e/session/session-model-persistence.spec.ts b/packages/app/e2e/session/session-model-persistence.spec.ts deleted file mode 100644 index c107cc518..000000000 --- a/packages/app/e2e/session/session-model-persistence.spec.ts +++ /dev/null @@ -1,362 +0,0 @@ -import type { Locator, Page } from "@playwright/test" -import { test, expect } from "../fixtures" -import { openSidebar, resolveSlug, setWorkspacesEnabled, waitSession, waitSlug } from "../actions" -import { - promptAgentSelector, - promptModelSelector, - promptVariantSelector, - workspaceItemSelector, - workspaceNewSessionSelector, -} from "../selectors" -import { createSdk, sessionPath } from "../utils" - -type Footer = { - agent: string - model: string - variant: string -} - -type Probe = { - dir?: string - sessionID?: string - agent?: string - model?: { providerID: string; modelID: string; name?: string } - variant?: string | null - pick?: { - agent?: string - model?: { providerID: string; modelID: string } - variant?: string | null - } - variants?: string[] - models?: Array<{ providerID: string; modelID: string; name: string }> - agents?: Array<{ name: string }> -} - -const escape = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") - -const text = async (locator: Locator) => ((await locator.textContent()) ?? "").trim() - -const modelKey = (state: Probe | null) => (state?.model ? `${state.model.providerID}:${state.model.modelID}` : null) - -async function probe(page: Page): Promise<Probe | null> { - return page.evaluate(() => { - const win = window as Window & { - __opencode_e2e?: { - model?: { - current?: Probe - } - } - } - return win.__opencode_e2e?.model?.current ?? null - }) -} - -async function currentModel(page: Page) { - await expect.poll(() => probe(page).then(modelKey), { timeout: 30_000 }).not.toBe(null) - const value = await probe(page).then(modelKey) - if (!value) throw new Error("Failed to resolve current model key") - return value -} - -async function waitControl(page: Page, key: "setAgent" | "setModel" | "setVariant") { - await expect - .poll( - () => - page.evaluate((key) => { - const win = window as Window & { - __opencode_e2e?: { - model?: { - controls?: Record<string, unknown> - } - } - } - return !!win.__opencode_e2e?.model?.controls?.[key] - }, key), - { timeout: 30_000 }, - ) - .toBe(true) -} - -async function pickAgent(page: Page, value: string) { - await waitControl(page, "setAgent") - await page.evaluate((value) => { - const win = window as Window & { - __opencode_e2e?: { - model?: { - controls?: { - setAgent?: (value: string | undefined) => void - } - } - } - } - const fn = win.__opencode_e2e?.model?.controls?.setAgent - if (!fn) throw new Error("Model e2e agent control is not enabled") - fn(value) - }, value) -} - -async function pickModel(page: Page, value: { providerID: string; modelID: string }) { - await waitControl(page, "setModel") - await page.evaluate((value) => { - const win = window as Window & { - __opencode_e2e?: { - model?: { - controls?: { - setModel?: (value: { providerID: string; modelID: string } | undefined) => void - } - } - } - } - const fn = win.__opencode_e2e?.model?.controls?.setModel - if (!fn) throw new Error("Model e2e model control is not enabled") - fn(value) - }, value) -} - -async function pickVariant(page: Page, value: string) { - await waitControl(page, "setVariant") - await page.evaluate((value) => { - const win = window as Window & { - __opencode_e2e?: { - model?: { - controls?: { - setVariant?: (value: string | undefined) => void - } - } - } - } - const fn = win.__opencode_e2e?.model?.controls?.setVariant - if (!fn) throw new Error("Model e2e variant control is not enabled") - fn(value) - }, value) -} - -async function read(page: Page): Promise<Footer> { - return { - agent: await text(page.locator(`${promptAgentSelector} [data-slot="select-select-trigger-value"]`).first()), - model: await text(page.locator(`${promptModelSelector} [data-action="prompt-model"] span`).first()), - variant: await text(page.locator(`${promptVariantSelector} [data-slot="select-select-trigger-value"]`).first()), - } -} - -async function waitFooter(page: Page, expected: Partial<Footer>) { - let hit: Footer | null = null - await expect - .poll( - async () => { - const state = await read(page) - const ok = Object.entries(expected).every(([key, value]) => state[key as keyof Footer] === value) - if (ok) hit = state - return ok - }, - { timeout: 30_000 }, - ) - .toBe(true) - if (!hit) throw new Error("Failed to resolve prompt footer state") - return hit -} - -async function waitModel(page: Page, value: string) { - await expect.poll(() => probe(page).then(modelKey), { timeout: 30_000 }).toBe(value) -} - -async function choose(page: Page, root: string, value: string) { - const select = page.locator(root) - await expect(select).toBeVisible() - await pickAgent(page, value) -} - -async function variantCount(page: Page) { - return (await probe(page))?.variants?.length ?? 0 -} - -async function agents(page: Page) { - return ((await probe(page))?.agents ?? []).map((item) => item.name).filter(Boolean) -} - -async function ensureVariant(page: Page, directory: string): Promise<Footer> { - const current = await read(page) - if ((await variantCount(page)) >= 2) return current - - const cfg = await createSdk(directory) - .config.get() - .then((x) => x.data) - const visible = new Set(await agents(page)) - const entry = Object.entries(cfg?.agent ?? {}).find((item) => { - const value = item[1] - return !!value && typeof value === "object" && "variant" in value && "model" in value && visible.has(item[0]) - }) - const name = entry?.[0] - test.skip(!name, "no agent with alternate variants available") - if (!name) return current - - await choose(page, promptAgentSelector, name) - await expect.poll(() => variantCount(page), { timeout: 30_000 }).toBeGreaterThanOrEqual(2) - return waitFooter(page, { agent: name }) -} - -async function chooseDifferentVariant(page: Page): Promise<Footer> { - const current = await read(page) - const next = (await probe(page))?.variants?.find((item) => item !== current.variant) - if (!next) throw new Error("Current model has no alternate variant to select") - - await pickVariant(page, next) - return waitFooter(page, { agent: current.agent, model: current.model, variant: next }) -} - -async function chooseOtherModel(page: Page, skip: string[] = []): Promise<Footer> { - const current = await currentModel(page) - const next = (await probe(page))?.models?.find((item) => { - const key = `${item.providerID}:${item.modelID}` - return key !== current && !skip.includes(key) - }) - if (!next) throw new Error("Failed to choose a different model") - await pickModel(page, { providerID: next.providerID, modelID: next.modelID }) - await expect.poll(async () => (await read(page)).model, { timeout: 30_000 }).toBe(next.name) - return read(page) -} - -async function goto(page: Page, directory: string, sessionID?: string) { - await page.goto(sessionPath(directory, sessionID)) - await waitSession(page, { directory, sessionID }) -} - -async function submit(project: Parameters<typeof test>[0]["project"], value: string) { - return project.prompt(value) -} - -async function createWorkspace(page: Page, root: string, seen: string[]) { - await openSidebar(page) - await page.getByRole("button", { name: "New workspace" }).first().click() - - const next = await resolveSlug(await waitSlug(page, [root, ...seen])) - await waitSession(page, { directory: next.directory }) - return next -} - -async function waitWorkspace(page: Page, slug: string) { - await openSidebar(page) - await expect - .poll( - async () => { - const item = page.locator(workspaceItemSelector(slug)).first() - try { - await item.hover({ timeout: 500 }) - return true - } catch { - return false - } - }, - { timeout: 60_000 }, - ) - .toBe(true) -} - -async function newWorkspaceSession(page: Page, slug: string) { - await waitWorkspace(page, slug) - const item = page.locator(workspaceItemSelector(slug)).first() - await item.hover() - - const button = page.locator(workspaceNewSessionSelector(slug)).first() - await expect(button).toBeVisible() - await button.click({ force: true }) - - const next = await resolveSlug(await waitSlug(page)) - return waitSession(page, { directory: next.directory }).then((item) => item.directory) -} - -test("session model restore per session without leaking into new sessions", async ({ page, project }) => { - await page.setViewportSize({ width: 1440, height: 900 }) - - await project.open() - await project.gotoSession() - - const firstState = await chooseOtherModel(page) - const firstKey = await currentModel(page) - const first = await submit(project, `session variant ${Date.now()}`) - - await page.reload() - await waitSession(page, { directory: project.directory, sessionID: first }) - await waitFooter(page, firstState) - - await project.gotoSession() - const fresh = await read(page) - expect(fresh.model).not.toBe(firstState.model) - - const secondState = await chooseOtherModel(page, [firstKey]) - const second = await submit(project, `session model ${Date.now()}`) - - await goto(page, project.directory, first) - await waitFooter(page, firstState) - - await goto(page, project.directory, second) - await waitFooter(page, secondState) - - await project.gotoSession() - await page.reload() - await waitSession(page, { directory: project.directory }) - await waitFooter(page, fresh) -}) - -test("session model restore across workspaces", async ({ page, project }) => { - await page.setViewportSize({ width: 1440, height: 900 }) - - await project.open() - const root = project.directory - await project.gotoSession() - - const firstState = await chooseOtherModel(page) - const firstKey = await currentModel(page) - const first = await submit(project, `root session ${Date.now()}`) - - await openSidebar(page) - await setWorkspacesEnabled(page, project.slug, true) - - const one = await createWorkspace(page, project.slug, []) - const oneDir = await newWorkspaceSession(page, one.slug) - project.trackDirectory(oneDir) - - const secondState = await chooseOtherModel(page, [firstKey]) - const secondKey = await currentModel(page) - const second = await submit(project, `workspace one ${Date.now()}`) - - const two = await createWorkspace(page, project.slug, [one.slug]) - const twoDir = await newWorkspaceSession(page, two.slug) - project.trackDirectory(twoDir) - - const thirdState = await chooseOtherModel(page, [firstKey, secondKey]) - const third = await submit(project, `workspace two ${Date.now()}`) - - await goto(page, root, first) - await waitFooter(page, firstState) - - await goto(page, oneDir, second) - await waitFooter(page, secondState) - - await goto(page, twoDir, third) - await waitFooter(page, thirdState) - - await goto(page, root, first) - await waitFooter(page, firstState) -}) - -test("variant preserved when switching agent modes", async ({ page, project }) => { - await page.setViewportSize({ width: 1440, height: 900 }) - - await project.open() - await project.gotoSession() - - await ensureVariant(page, project.directory) - const updated = await chooseDifferentVariant(page) - - const available = await agents(page) - const other = available.find((name) => name !== updated.agent) - test.skip(!other, "only one agent available") - if (!other) return - - await choose(page, promptAgentSelector, other) - await waitFooter(page, { agent: other, variant: updated.variant }) - - await choose(page, promptAgentSelector, updated.agent) - await waitFooter(page, { agent: updated.agent, variant: updated.variant }) -}) diff --git a/packages/app/e2e/session/session-review.spec.ts b/packages/app/e2e/session/session-review.spec.ts deleted file mode 100644 index c0a98cb2e..000000000 --- a/packages/app/e2e/session/session-review.spec.ts +++ /dev/null @@ -1,440 +0,0 @@ -import { waitSessionIdle, withSession } from "../actions" -import { test, expect } from "../fixtures" -import { bodyText } from "../prompt/mock" - -const count = 14 - -function body(mark: string) { - return [ - `title ${mark}`, - `mark ${mark}`, - ...Array.from({ length: 32 }, (_, i) => `line ${String(i + 1).padStart(2, "0")} ${mark}`), - ] -} - -function files(tag: string) { - return Array.from({ length: count }, (_, i) => { - const id = String(i).padStart(2, "0") - return { - file: `review-scroll-${id}.txt`, - mark: `${tag}-${id}`, - } - }) -} - -function seed(list: ReturnType<typeof files>) { - const out = ["*** Begin Patch"] - - for (const item of list) { - out.push(`*** Add File: ${item.file}`) - for (const line of body(item.mark)) out.push(`+${line}`) - } - - out.push("*** End Patch") - return out.join("\n") -} - -function edit(file: string, prev: string, next: string) { - return ["*** Begin Patch", `*** Update File: ${file}`, "@@", `-mark ${prev}`, `+mark ${next}`, "*** End Patch"].join( - "\n", - ) -} - -async function patchWithMock( - llm: Parameters<typeof test>[0]["llm"], - sdk: Parameters<typeof withSession>[0], - sessionID: string, - patchText: string, -) { - const callsBefore = await llm.calls() - await llm.toolMatch( - (hit) => bodyText(hit).includes("Your only valid response is one apply_patch tool call."), - "apply_patch", - { patchText }, - ) - await sdk.session.prompt({ - sessionID, - agent: "build", - system: [ - "You are seeding deterministic e2e UI state.", - "Your only valid response is one apply_patch tool call.", - `Use this JSON input: ${JSON.stringify({ patchText })}`, - "Do not call any other tools.", - "Do not output plain text.", - ].join("\n"), - parts: [{ type: "text", text: "Apply the provided patch exactly once." }], - }) - - await expect.poll(() => llm.calls().then((c) => c > callsBefore), { timeout: 30_000 }).toBe(true) - await expect - .poll( - async () => { - const diff = await sdk.session.diff({ sessionID }).then((res) => res.data ?? []) - return diff.length - }, - { timeout: 120_000 }, - ) - .toBeGreaterThan(0) -} - -async function show(page: Parameters<typeof test>[0]["page"]) { - const btn = page.getByRole("button", { name: "Toggle review" }).first() - await expect(btn).toBeVisible() - if ((await btn.getAttribute("aria-expanded")) !== "true") await btn.click() - await expect(btn).toHaveAttribute("aria-expanded", "true") -} - -async function expand(page: Parameters<typeof test>[0]["page"]) { - const close = page.getByRole("button", { name: /^Collapse all$/i }).first() - const open = await close - .isVisible() - .then((value) => value) - .catch(() => false) - - const btn = page.getByRole("button", { name: /^Expand all$/i }).first() - if (open) { - await close.click() - await expect(btn).toBeVisible() - } - - await expect(btn).toBeVisible() - await btn.click() - await expect(close).toBeVisible() -} - -async function waitMark(page: Parameters<typeof test>[0]["page"], file: string, mark: string) { - await page.waitForFunction( - ({ file, mark }) => { - const view = document.querySelector('[data-slot="session-review-scroll"] .scroll-view__viewport') - if (!(view instanceof HTMLElement)) return false - - const head = Array.from(view.querySelectorAll("h3")).find( - (node) => node instanceof HTMLElement && node.textContent?.includes(file), - ) - if (!(head instanceof HTMLElement)) return false - - return Array.from(head.parentElement?.querySelectorAll("diffs-container") ?? []).some((host) => { - if (!(host instanceof HTMLElement)) return false - const root = host.shadowRoot - return root?.textContent?.includes(`mark ${mark}`) ?? false - }) - }, - { file, mark }, - { timeout: 60_000 }, - ) -} - -async function spot(page: Parameters<typeof test>[0]["page"], file: string) { - return page.evaluate((file) => { - const view = document.querySelector('[data-slot="session-review-scroll"] .scroll-view__viewport') - if (!(view instanceof HTMLElement)) return null - - const row = Array.from(view.querySelectorAll("h3")).find( - (node) => node instanceof HTMLElement && node.textContent?.includes(file), - ) - if (!(row instanceof HTMLElement)) return null - - const a = row.getBoundingClientRect() - const b = view.getBoundingClientRect() - return { - top: a.top - b.top, - y: view.scrollTop, - } - }, file) -} - -async function comment(page: Parameters<typeof test>[0]["page"], file: string, note: string) { - const row = page.locator(`[data-file="${file}"]`).first() - await expect(row).toBeVisible() - - const line = row.locator('diffs-container [data-line="2"]').first() - await expect(line).toBeVisible() - await line.hover() - - const add = row.getByRole("button", { name: /^Comment$/ }).first() - await expect(add).toBeVisible() - await add.click() - - const area = row.locator('[data-slot="line-comment-textarea"]').first() - await expect(area).toBeVisible() - await area.fill(note) - - const submit = row.locator('[data-slot="line-comment-action"][data-variant="primary"]').first() - await expect(submit).toBeEnabled() - await submit.click() - - await expect(row.locator('[data-slot="line-comment-content"]').filter({ hasText: note }).first()).toBeVisible() - await expect(row.locator('[data-slot="line-comment-tools"]').first()).toBeVisible() -} - -async function overflow(page: Parameters<typeof test>[0]["page"], file: string) { - const row = page.locator(`[data-file="${file}"]`).first() - const view = page.locator('[data-slot="session-review-scroll"] .scroll-view__viewport').first() - const pop = row.locator('[data-slot="line-comment-popover"][data-inline-body]').first() - const tools = row.locator('[data-slot="line-comment-tools"]').first() - - const [width, viewBox, popBox, toolsBox] = await Promise.all([ - view.evaluate((el) => el.scrollWidth - el.clientWidth), - view.boundingBox(), - pop.boundingBox(), - tools.boundingBox(), - ]) - - if (!viewBox || !popBox || !toolsBox) return null - - return { - width, - pop: popBox.x + popBox.width - (viewBox.x + viewBox.width), - tools: toolsBox.x + toolsBox.width - (viewBox.x + viewBox.width), - } -} - -async function openReviewFile(page: Parameters<typeof test>[0]["page"], file: string) { - const row = page.locator(`[data-file="${file}"]`).first() - await expect(row).toBeVisible() - await row.hover() - - const open = row.getByRole("button", { name: /^Open file$/i }).first() - await expect(open).toBeVisible() - await open.click() - - const tab = page.getByRole("tab", { name: file }).first() - await expect(tab).toBeVisible() - await tab.click() - - const viewer = page.locator('[data-component="file"][data-mode="text"]').first() - await expect(viewer).toBeVisible() - return viewer -} - -async function fileComment(page: Parameters<typeof test>[0]["page"], note: string) { - const viewer = page.locator('[data-component="file"][data-mode="text"]').first() - await expect(viewer).toBeVisible() - - const line = viewer.locator('diffs-container [data-line="2"]').first() - await expect(line).toBeVisible() - await line.hover() - - const add = viewer.getByRole("button", { name: /^Comment$/ }).first() - await expect(add).toBeVisible() - await add.click() - - const area = viewer.locator('[data-slot="line-comment-textarea"]').first() - await expect(area).toBeVisible() - await area.fill(note) - - const submit = viewer.locator('[data-slot="line-comment-action"][data-variant="primary"]').first() - await expect(submit).toBeEnabled() - await submit.click() - - await expect(viewer.locator('[data-slot="line-comment-content"]').filter({ hasText: note }).first()).toBeVisible() - await expect(viewer.locator('[data-slot="line-comment-tools"]').first()).toBeVisible() -} - -async function fileOverflow(page: Parameters<typeof test>[0]["page"]) { - const viewer = page.locator('[data-component="file"][data-mode="text"]').first() - const view = page.locator('[role="tabpanel"] .scroll-view__viewport').first() - const pop = viewer.locator('[data-slot="line-comment-popover"][data-inline-body]').first() - const tools = viewer.locator('[data-slot="line-comment-tools"]').first() - - const [width, viewBox, popBox, toolsBox] = await Promise.all([ - view.evaluate((el) => el.scrollWidth - el.clientWidth), - view.boundingBox(), - pop.boundingBox(), - tools.boundingBox(), - ]) - - if (!viewBox || !popBox || !toolsBox) return null - - return { - width, - pop: popBox.x + popBox.width - (viewBox.x + viewBox.width), - tools: toolsBox.x + toolsBox.width - (viewBox.x + viewBox.width), - } -} - -test("review applies inline comment clicks without horizontal overflow", async ({ page, llm, project }) => { - test.setTimeout(180_000) - - const tag = `review-comment-${Date.now()}` - const file = `review-comment-${tag}.txt` - const note = `comment ${tag}` - - await page.setViewportSize({ width: 1280, height: 900 }) - - await project.open() - await withSession(project.sdk, `e2e review comment ${tag}`, async (session) => { - project.trackSession(session.id) - await patchWithMock(llm, project.sdk, session.id, seed([{ file, mark: tag }])) - - await expect - .poll( - async () => { - const diff = await project.sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? []) - return diff.length - }, - { timeout: 60_000 }, - ) - .toBe(1) - - await project.gotoSession(session.id) - await show(page) - - const tab = page.getByRole("tab", { name: /Review/i }).first() - await expect(tab).toBeVisible() - await tab.click() - - await expand(page) - await waitMark(page, file, tag) - await comment(page, file, note) - - await expect - .poll(async () => (await overflow(page, file))?.width ?? Number.POSITIVE_INFINITY, { timeout: 10_000 }) - .toBeLessThanOrEqual(1) - await expect - .poll(async () => (await overflow(page, file))?.pop ?? Number.POSITIVE_INFINITY, { timeout: 10_000 }) - .toBeLessThanOrEqual(1) - await expect - .poll(async () => (await overflow(page, file))?.tools ?? Number.POSITIVE_INFINITY, { timeout: 10_000 }) - .toBeLessThanOrEqual(1) - }) -}) - -test("review file comments submit on click without clipping actions", async ({ page, llm, project }) => { - test.setTimeout(180_000) - - const tag = `review-file-comment-${Date.now()}` - const file = `review-file-comment-${tag}.txt` - const note = `comment ${tag}` - - await page.setViewportSize({ width: 1280, height: 900 }) - - await project.open() - await withSession(project.sdk, `e2e review file comment ${tag}`, async (session) => { - project.trackSession(session.id) - await patchWithMock(llm, project.sdk, session.id, seed([{ file, mark: tag }])) - - await expect - .poll( - async () => { - const diff = await project.sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? []) - return diff.length - }, - { timeout: 60_000 }, - ) - .toBe(1) - - await project.gotoSession(session.id) - await show(page) - - const tab = page.getByRole("tab", { name: /Review/i }).first() - await expect(tab).toBeVisible() - await tab.click() - - await expand(page) - await waitMark(page, file, tag) - await openReviewFile(page, file) - await fileComment(page, note) - - await expect - .poll(async () => (await fileOverflow(page))?.width ?? Number.POSITIVE_INFINITY, { timeout: 10_000 }) - .toBeLessThanOrEqual(1) - await expect - .poll(async () => (await fileOverflow(page))?.pop ?? Number.POSITIVE_INFINITY, { timeout: 10_000 }) - .toBeLessThanOrEqual(1) - await expect - .poll(async () => (await fileOverflow(page))?.tools ?? Number.POSITIVE_INFINITY, { timeout: 10_000 }) - .toBeLessThanOrEqual(1) - }) -}) - -test.fixme("review keeps scroll position after a live diff update", async ({ page, llm, project }) => { - test.setTimeout(180_000) - - const tag = `review-${Date.now()}` - const list = files(tag) - const hit = list[list.length - 4]! - const next = `${tag}-live` - - await page.setViewportSize({ width: 1600, height: 1000 }) - - await project.open() - await withSession(project.sdk, `e2e review ${tag}`, async (session) => { - project.trackSession(session.id) - await patchWithMock(llm, project.sdk, session.id, seed(list)) - - await expect - .poll( - async () => { - const info = await project.sdk.session.get({ sessionID: session.id }).then((res) => res.data) - return info?.summary?.files ?? 0 - }, - { timeout: 60_000 }, - ) - .toBe(list.length) - - await expect - .poll( - async () => { - const diff = await project.sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? []) - return diff.length - }, - { timeout: 60_000 }, - ) - .toBe(list.length) - - await project.gotoSession(session.id) - await show(page) - - const tab = page.getByRole("tab", { name: /Review/i }).first() - await expect(tab).toBeVisible() - await tab.click() - - const view = page.locator('[data-slot="session-review-scroll"] .scroll-view__viewport').first() - await expect(view).toBeVisible() - const heads = page.getByRole("heading", { level: 3 }).filter({ hasText: /^review-scroll-/ }) - await expect(heads).toHaveCount(list.length, { timeout: 60_000 }) - - await expand(page) - await waitMark(page, hit.file, hit.mark) - - const row = page - .getByRole("heading", { - level: 3, - name: new RegExp(hit.file.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")), - }) - .first() - await expect(row).toBeVisible() - await row.evaluate((el) => el.scrollIntoView({ block: "center" })) - - await expect.poll(async () => (await spot(page, hit.file))?.y ?? 0).toBeGreaterThan(200) - const prev = await spot(page, hit.file) - if (!prev) throw new Error(`missing review row for ${hit.file}`) - - await patchWithMock(llm, project.sdk, session.id, edit(hit.file, hit.mark, next)) - - await expect - .poll( - async () => { - const diff = await project.sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? []) - const item = diff.find((item) => item.file === hit.file) - return typeof item?.after === "string" ? item.after : "" - }, - { timeout: 60_000 }, - ) - .toContain(`mark ${next}`) - - await waitMark(page, hit.file, next) - - await expect - .poll( - async () => { - const next = await spot(page, hit.file) - if (!next) return Number.POSITIVE_INFINITY - return Math.max(Math.abs(next.top - prev.top), Math.abs(next.y - prev.y)) - }, - { timeout: 60_000 }, - ) - .toBeLessThanOrEqual(32) - }) -}) diff --git a/packages/app/e2e/session/session-undo-redo.spec.ts b/packages/app/e2e/session/session-undo-redo.spec.ts deleted file mode 100644 index 709a45b4c..000000000 --- a/packages/app/e2e/session/session-undo-redo.spec.ts +++ /dev/null @@ -1,233 +0,0 @@ -import type { Page } from "@playwright/test" -import { test, expect } from "../fixtures" -import { withSession } from "../actions" -import { createSdk, modKey } from "../utils" -import { promptSelector } from "../selectors" - -async function seedConversation(input: { - page: Page - sdk: ReturnType<typeof createSdk> - sessionID: string - token: string -}) { - const messages = async () => - await input.sdk.session.messages({ sessionID: input.sessionID, limit: 100 }).then((r) => r.data ?? []) - const seeded = await messages() - const userIDs = new Set(seeded.filter((m) => m.info.role === "user").map((m) => m.info.id)) - - const prompt = input.page.locator(promptSelector) - await expect(prompt).toBeVisible() - await input.sdk.session.promptAsync({ - sessionID: input.sessionID, - noReply: true, - parts: [{ type: "text", text: input.token }], - }) - - let userMessageID: string | undefined - await expect - .poll( - async () => { - const users = (await messages()).filter( - (m) => - !userIDs.has(m.info.id) && - m.info.role === "user" && - m.parts.filter((p) => p.type === "text").some((p) => p.text.includes(input.token)), - ) - if (users.length === 0) return false - - const user = users[users.length - 1] - if (!user) return false - userMessageID = user.info.id - return true - }, - { timeout: 90_000, intervals: [250, 500, 1_000] }, - ) - .toBe(true) - - if (!userMessageID) throw new Error("Expected a user message id") - await expect(input.page.locator(`[data-message-id="${userMessageID}"]`)).toHaveCount(1, { timeout: 30_000 }) - return { prompt, userMessageID } -} - -test("slash undo sets revert and restores prior prompt", async ({ page, project }) => { - test.setTimeout(120_000) - - const token = `undo_${Date.now()}` - - await project.open() - const sdk = project.sdk - - await withSession(sdk, `e2e undo ${Date.now()}`, async (session) => { - project.trackSession(session.id) - await project.gotoSession(session.id) - - const seeded = await seedConversation({ page, sdk, sessionID: session.id, token }) - - await seeded.prompt.click() - await page.keyboard.type("/undo") - - const undo = page.locator('[data-slash-id="session.undo"]').first() - await expect(undo).toBeVisible() - await page.keyboard.press("Enter") - - await expect - .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), { - timeout: 30_000, - }) - .toBe(seeded.userMessageID) - - await expect(seeded.prompt).toContainText(token) - await expect(page.locator(`[data-message-id="${seeded.userMessageID}"]`)).toHaveCount(0) - }) -}) - -test("slash redo clears revert and restores latest state", async ({ page, project }) => { - test.setTimeout(120_000) - - const token = `redo_${Date.now()}` - - await project.open() - const sdk = project.sdk - - await withSession(sdk, `e2e redo ${Date.now()}`, async (session) => { - project.trackSession(session.id) - await project.gotoSession(session.id) - - const seeded = await seedConversation({ page, sdk, sessionID: session.id, token }) - - await seeded.prompt.click() - await page.keyboard.type("/undo") - - const undo = page.locator('[data-slash-id="session.undo"]').first() - await expect(undo).toBeVisible() - await page.keyboard.press("Enter") - - await expect - .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), { - timeout: 30_000, - }) - .toBe(seeded.userMessageID) - - await seeded.prompt.click() - await page.keyboard.press(`${modKey}+A`) - await page.keyboard.press("Backspace") - await page.keyboard.type("/redo") - - const redo = page.locator('[data-slash-id="session.redo"]').first() - await expect(redo).toBeVisible() - await page.keyboard.press("Enter") - - await expect - .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), { - timeout: 30_000, - }) - .toBeUndefined() - - await expect(seeded.prompt).not.toContainText(token) - await expect(page.locator(`[data-message-id="${seeded.userMessageID}"]`)).toHaveCount(1) - }) -}) - -test("slash undo/redo traverses multi-step revert stack", async ({ page, project }) => { - test.setTimeout(120_000) - - const firstToken = `undo_redo_first_${Date.now()}` - const secondToken = `undo_redo_second_${Date.now()}` - - await project.open() - const sdk = project.sdk - - await withSession(sdk, `e2e undo redo stack ${Date.now()}`, async (session) => { - project.trackSession(session.id) - await project.gotoSession(session.id) - - const first = await seedConversation({ - page, - sdk, - sessionID: session.id, - token: firstToken, - }) - const second = await seedConversation({ - page, - sdk, - sessionID: session.id, - token: secondToken, - }) - - expect(first.userMessageID).not.toBe(second.userMessageID) - - const firstMessage = page.locator(`[data-message-id="${first.userMessageID}"]`) - const secondMessage = page.locator(`[data-message-id="${second.userMessageID}"]`) - - await expect(firstMessage).toHaveCount(1) - await expect(secondMessage).toHaveCount(1) - - await second.prompt.click() - await page.keyboard.press(`${modKey}+A`) - await page.keyboard.press("Backspace") - await page.keyboard.type("/undo") - - const undo = page.locator('[data-slash-id="session.undo"]').first() - await expect(undo).toBeVisible() - await page.keyboard.press("Enter") - - await expect - .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), { - timeout: 30_000, - }) - .toBe(second.userMessageID) - - await expect(firstMessage).toHaveCount(1) - await expect(secondMessage).toHaveCount(0) - - await second.prompt.click() - await page.keyboard.press(`${modKey}+A`) - await page.keyboard.press("Backspace") - await page.keyboard.type("/undo") - await expect(undo).toBeVisible() - await page.keyboard.press("Enter") - - await expect - .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), { - timeout: 30_000, - }) - .toBe(first.userMessageID) - - await expect(firstMessage).toHaveCount(0) - await expect(secondMessage).toHaveCount(0) - - await second.prompt.click() - await page.keyboard.press(`${modKey}+A`) - await page.keyboard.press("Backspace") - await page.keyboard.type("/redo") - - const redo = page.locator('[data-slash-id="session.redo"]').first() - await expect(redo).toBeVisible() - await page.keyboard.press("Enter") - - await expect - .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), { - timeout: 30_000, - }) - .toBe(second.userMessageID) - - await expect(firstMessage).toHaveCount(1) - await expect(secondMessage).toHaveCount(0) - - await second.prompt.click() - await page.keyboard.press(`${modKey}+A`) - await page.keyboard.press("Backspace") - await page.keyboard.type("/redo") - await expect(redo).toBeVisible() - await page.keyboard.press("Enter") - - await expect - .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), { - timeout: 30_000, - }) - .toBeUndefined() - - await expect(firstMessage).toHaveCount(1) - await expect(secondMessage).toHaveCount(1) - }) -}) diff --git a/packages/app/e2e/session/session.spec.ts b/packages/app/e2e/session/session.spec.ts deleted file mode 100644 index 1b5fb1b60..000000000 --- a/packages/app/e2e/session/session.spec.ts +++ /dev/null @@ -1,182 +0,0 @@ -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" - -type Sdk = Parameters<typeof withSession>[0] - -async function seedMessage(sdk: Sdk, sessionID: string) { - await sdk.session.promptAsync({ - sessionID, - noReply: true, - parts: [{ type: "text", text: "e2e seed" }], - }) - - await expect - .poll( - async () => { - const messages = await sdk.session.messages({ sessionID, limit: 1 }).then((r) => r.data ?? []) - return messages.length - }, - { timeout: 30_000 }, - ) - .toBeGreaterThan(0) -} - -test("session can be renamed via header menu", async ({ page, project }) => { - const stamp = Date.now() - const originalTitle = `e2e rename test ${stamp}` - const renamedTitle = `e2e renamed ${stamp}` - - await project.open() - await withSession(project.sdk, originalTitle, async (session) => { - project.trackSession(session.id) - await seedMessage(project.sdk, session.id) - await project.gotoSession(session.id) - await expect(page.getByRole("heading", { level: 1 }).first()).toHaveText(originalTitle) - - const menu = await openSessionMoreMenu(page, session.id) - await clickMenuItem(menu, /rename/i) - - const input = page.locator(".scroll-view__viewport").locator(inlineInputSelector).first() - await expect(input).toBeVisible() - await expect(input).toBeFocused() - await input.fill(renamedTitle) - await expect(input).toHaveValue(renamedTitle) - await input.press("Enter") - - await expect - .poll( - async () => { - const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data) - return data?.title - }, - { timeout: 30_000 }, - ) - .toBe(renamedTitle) - - await expect(page.getByRole("heading", { level: 1 }).first()).toHaveText(renamedTitle) - }) -}) - -test("session can be archived via header menu", async ({ page, project }) => { - const stamp = Date.now() - const title = `e2e archive test ${stamp}` - - await project.open() - await withSession(project.sdk, title, async (session) => { - project.trackSession(session.id) - await seedMessage(project.sdk, session.id) - await project.gotoSession(session.id) - const menu = await openSessionMoreMenu(page, session.id) - await clickMenuItem(menu, /archive/i) - - await expect - .poll( - async () => { - const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data) - return data?.time?.archived - }, - { timeout: 30_000 }, - ) - .not.toBeUndefined() - - await openSidebar(page) - await expect(page.locator(sessionItemSelector(session.id))).toHaveCount(0) - }) -}) - -test("session can be deleted via header menu", async ({ page, project }) => { - const stamp = Date.now() - const title = `e2e delete test ${stamp}` - - await project.open() - await withSession(project.sdk, title, async (session) => { - project.trackSession(session.id) - await seedMessage(project.sdk, session.id) - await project.gotoSession(session.id) - const menu = await openSessionMoreMenu(page, session.id) - await clickMenuItem(menu, /delete/i) - await confirmDialog(page, /delete/i) - - await expect - .poll( - async () => { - const data = await project.sdk.session - .get({ sessionID: session.id }) - .then((r) => r.data) - .catch(() => undefined) - return data?.id - }, - { timeout: 30_000 }, - ) - .toBeUndefined() - - await openSidebar(page) - await expect(page.locator(sessionItemSelector(session.id))).toHaveCount(0) - }) -}) - -test("session can be shared and unshared via header button", async ({ page, project }) => { - test.skip(shareDisabled, "Share is disabled in this environment (OPENCODE_DISABLE_SHARE).") - - const stamp = Date.now() - const title = `e2e share test ${stamp}` - - await project.open() - await withSession(project.sdk, title, async (session) => { - project.trackSession(session.id) - await project.gotoSession(session.id) - await project.prompt(`share seed ${stamp}`) - - const shared = await openSharePopover(page) - const publish = shared.popoverBody.getByRole("button", { name: "Publish" }).first() - await expect(publish).toBeVisible({ timeout: 30_000 }) - await publish.click() - - await expect(shared.popoverBody.getByRole("button", { name: "Unpublish" }).first()).toBeVisible({ - timeout: 30_000, - }) - - await expect - .poll( - async () => { - const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data) - return data?.share?.url || undefined - }, - { timeout: 30_000 }, - ) - .not.toBeUndefined() - - const unpublish = shared.popoverBody.getByRole("button", { name: "Unpublish" }).first() - await expect(unpublish).toBeVisible({ timeout: 30_000 }) - await unpublish.click() - - await expect(shared.popoverBody.getByRole("button", { name: "Publish" }).first()).toBeVisible({ - timeout: 30_000, - }) - - await expect - .poll( - async () => { - const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data) - return data?.share?.url || undefined - }, - { timeout: 30_000 }, - ) - .toBeUndefined() - - const unshared = await openSharePopover(page) - await expect(unshared.popoverBody.getByRole("button", { name: "Publish" }).first()).toBeVisible({ - timeout: 30_000, - }) - }) -}) |
