diff options
| author | Kit Langton <[email protected]> | 2026-04-02 14:17:28 -0400 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-04-02 18:17:28 +0000 |
| commit | c3ef69c8664ab84464d720f75eb9bf35289c1847 (patch) | |
| tree | d7b1aa78355c39472804bdfbbd9db87722d0587b /packages/app/e2e/session | |
| parent | 363891126c9b2a909db0e724eb6377bd3d12b38b (diff) | |
| download | opencode-c3ef69c8664ab84464d720f75eb9bf35289c1847.tar.gz opencode-c3ef69c8664ab84464d720f75eb9bf35289c1847.zip | |
test(app): add a golden path for mocked e2e prompts (#20593)
Diffstat (limited to 'packages/app/e2e/session')
| -rw-r--r-- | packages/app/e2e/session/session-child-navigation.spec.ts | 51 | ||||
| -rw-r--r-- | packages/app/e2e/session/session-composer-dock.spec.ts | 670 | ||||
| -rw-r--r-- | packages/app/e2e/session/session-model-persistence.spec.ts | 178 | ||||
| -rw-r--r-- | packages/app/e2e/session/session-review.spec.ts | 331 | ||||
| -rw-r--r-- | packages/app/e2e/session/session-undo-redo.spec.ts | 285 | ||||
| -rw-r--r-- | packages/app/e2e/session/session.spec.ts | 248 |
6 files changed, 846 insertions, 917 deletions
diff --git a/packages/app/e2e/session/session-child-navigation.spec.ts b/packages/app/e2e/session/session-child-navigation.spec.ts index 1ab4746e4..34a1a9e2e 100644 --- a/packages/app/e2e/session/session-child-navigation.spec.ts +++ b/packages/app/e2e/session/session-child-navigation.spec.ts @@ -3,7 +3,7 @@ import { test, expect } from "../fixtures" import { inputMatch } from "../prompt/mock" import { promptSelector } from "../selectors" -test("task tool child-session link does not trigger stale show errors", async ({ page, llm, withMockProject }) => { +test("task tool child-session link does not trigger stale show errors", async ({ page, llm, project }) => { test.setTimeout(120_000) const errs: string[] = [] @@ -13,34 +13,33 @@ test("task tool child-session link does not trigger stale show errors", async ({ page.on("pageerror", onError) try { - await withMockProject(async ({ gotoSession, trackSession, sdk }) => { - await withSession(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(sdk, { - sessionID: session.id, - description: taskInput.description, - prompt: taskInput.prompt, - }) - trackSession(child.sessionID) + 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 gotoSession(session.id) + await project.gotoSession(session.id) - const link = page - .locator("a.subagent-link") - .filter({ hasText: /open child session/i }) - .first() - await expect(link).toBeVisible({ timeout: 30_000 }) - await link.click() + const link = page + .locator("a.subagent-link") + .filter({ hasText: /open child session/i }) + .first() + await expect(link).toBeVisible({ timeout: 30_000 }) + await link.click() - await expect(page).toHaveURL(new RegExp(`/session/${child.sessionID}(?:[/?#]|$)`), { timeout: 30_000 }) - await expect(page.locator(promptSelector)).toBeVisible({ timeout: 30_000 }) - await expect.poll(() => errs, { timeout: 5_000 }).toEqual([]) - }) + await expect(page).toHaveURL(new RegExp(`/session/${child.sessionID}(?:[/?#]|$)`), { timeout: 30_000 }) + await expect(page.locator(promptSelector)).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 index bf0cc35b7..8eeac5b1a 100644 --- a/packages/app/e2e/session/session-composer-dock.spec.ts +++ b/packages/app/e2e/session/session-composer-dock.spec.ts @@ -242,9 +242,7 @@ async function withMockPermission<T>( 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({ - status: res.status(), - headers: res.headers(), - contentType: "application/json", + response: res, body: JSON.stringify(json), }) } @@ -269,240 +267,227 @@ async function withMockPermission<T>( } } -test("default dock shows prompt input", async ({ page, withBackendProject }) => { - await withBackendProject(async (project) => { - 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(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("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(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, withBackendProject }) => { - await withBackendProject(async ({ gotoSession }) => { - await gotoSession() +test("auto-accept toggle works before first submit", async ({ page, project }) => { + await project.open() - const button = page.locator('[data-action="prompt-permissions"]').first() - await expect(button).toBeVisible() - await expect(button).toHaveAttribute("aria-pressed", "false") + const button = page.locator('[data-action="prompt-permissions"]').first() + await expect(button).toBeVisible() + await expect(button).toHaveAttribute("aria-pressed", "false") - await setAutoAccept(page, true) - await setAutoAccept(page, false) - }) + await setAutoAccept(page, true) + await setAutoAccept(page, false) }) -test("blocked question flow unblocks after submit", async ({ page, llm, withMockProject }) => { - await withMockProject(async (project) => { - await withDockSession( - project.sdk, - "e2e composer dock question", - async (session) => { - await withDockSeed(project.sdk, session.id, async () => { - await project.gotoSession(session.id) +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, - }) + 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) + 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 dock.locator('[data-slot="question-option"]').first().click() + await dock.getByRole("button", { name: /submit/i }).click() - await expectQuestionOpen(page) - }) - }, - { trackSession: project.trackSession }, - ) - }) + await expectQuestionOpen(page) + }) + }, + { trackSession: project.trackSession }, + ) }) -test("blocked question flow supports keyboard shortcuts", async ({ page, llm, withMockProject }) => { - await withMockProject(async (project) => { - await withDockSession( - project.sdk, - "e2e composer dock question keyboard", - async (session) => { - await withDockSeed(project.sdk, session.id, async () => { - await project.gotoSession(session.id) +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, - }) + 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) + 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 expectQuestionBlocked(page) + await expect(first).toBeFocused() - await page.keyboard.press("ArrowDown") - await expect(second).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 }, - ) - }) + 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, withMockProject }) => { - await withMockProject(async (project) => { - await withDockSession( - project.sdk, - "e2e composer dock question escape", - async (session) => { - await withDockSeed(project.sdk, session.id, async () => { - await project.gotoSession(session.id) +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, - }) + 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 dock = page.locator(questionDockSelector) + const first = dock.locator('[data-slot="question-option"]').first() - await expectQuestionBlocked(page) - await expect(first).toBeFocused() + await expectQuestionBlocked(page) + await expect(first).toBeFocused() - await page.keyboard.press("Escape") - await expectQuestionOpen(page) - }) - }, - { trackSession: project.trackSession }, - ) - }) + await page.keyboard.press("Escape") + await expectQuestionOpen(page) + }) + }, + { trackSession: project.trackSession }, + ) }) -test("blocked permission flow supports allow once", async ({ page, withBackendProject }) => { - await withBackendProject(async (project) => { - 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 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, withBackendProject }) => { - await withBackendProject(async (project) => { - 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 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, withBackendProject }) => { - await withBackendProject(async (project) => { - 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("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, - withMockProject, -}) => { +test("child session question request blocks parent dock and unblocks after submit", async ({ page, llm, project }) => { const questions = [ { header: "Child input", @@ -513,137 +498,131 @@ test("child session question request blocks parent dock and unblocks after submi ], }, ] - await withMockProject(async (project) => { - await withDockSession( - project.sdk, - "e2e composer dock child question parent", - async (session) => { - await project.gotoSession(session.id) + 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) - const child = await project.sdk.session - .create({ - title: "e2e composer dock child question", - parentID: session.id, + try { + await withDockSeed(project.sdk, child.id, async () => { + await llm.toolMatch(inputMatch({ questions }), "question", { questions }) + await seedSessionQuestion(project.sdk, { + sessionID: child.id, + questions, }) - .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) + 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 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 }, - ) - }) + 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, - withBackendProject, -}) => { - await withBackendProject(async (project) => { - await withDockSession( - project.sdk, - "e2e composer dock child permission parent", - async (session) => { - await project.gotoSession(session.id) - await setAutoAccept(page, false) +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) - 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 }, - ) - }) + 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, withBackendProject }) => { - await withBackendProject(async (project) => { - 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("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, withMockProject }) => { +test("keyboard focus stays off prompt while blocked", async ({ page, llm, project }) => { const questions = [ { header: "Need input", @@ -651,28 +630,27 @@ test("keyboard focus stays off prompt while blocked", async ({ page, llm, withMo options: [{ label: "Continue", description: "Continue now" }], }, ] - await withMockProject(async (project) => { - await withDockSession( - project.sdk, - "e2e composer dock keyboard", - async (session) => { - await withDockSeed(project.sdk, session.id, async () => { - await project.gotoSession(session.id) + 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 llm.toolMatch(inputMatch({ questions }), "question", { questions }) + await seedSessionQuestion(project.sdk, { + sessionID: session.id, + questions, + }) - await expectQuestionBlocked(page) + 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 }, - ) - }) + 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 index 66bc451bc..c107cc518 100644 --- a/packages/app/e2e/session/session-model-persistence.spec.ts +++ b/packages/app/e2e/session/session-model-persistence.spec.ts @@ -1,15 +1,6 @@ import type { Locator, Page } from "@playwright/test" import { test, expect } from "../fixtures" -import { - openSidebar, - resolveSlug, - sessionIDFromUrl, - setWorkspacesEnabled, - waitSession, - waitSessionIdle, - waitSlug, - withNoReplyPrompt, -} from "../actions" +import { openSidebar, resolveSlug, setWorkspacesEnabled, waitSession, waitSlug } from "../actions" import { promptAgentSelector, promptModelSelector, @@ -230,35 +221,8 @@ async function goto(page: Page, directory: string, sessionID?: string) { await waitSession(page, { directory, sessionID }) } -async function submit(page: Page, value: string) { - const prompt = page.locator('[data-component="prompt-input"]') - await expect(prompt).toBeVisible() - - await withNoReplyPrompt(page, async () => { - await prompt.click() - await prompt.fill(value) - await prompt.press("Enter") - }) - - await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 30_000 }).not.toBe("") - const id = sessionIDFromUrl(page.url()) - if (!id) throw new Error(`Failed to resolve session id from ${page.url()}`) - return id -} - -async function waitUser(directory: string, sessionID: string) { - const sdk = createSdk(directory) - await expect - .poll( - async () => { - const items = await sdk.session.messages({ sessionID, limit: 20 }).then((x) => x.data ?? []) - return items.some((item) => item.info.role === "user") - }, - { timeout: 30_000 }, - ) - .toBe(true) - await sdk.session.abort({ sessionID }).catch(() => undefined) - await waitSessionIdle(sdk, sessionID, 30_000).catch(() => undefined) +async function submit(project: Parameters<typeof test>[0]["project"], value: string) { + return project.prompt(value) } async function createWorkspace(page: Page, root: string, seen: string[]) { @@ -301,108 +265,98 @@ async function newWorkspaceSession(page: Page, slug: string) { return waitSession(page, { directory: next.directory }).then((item) => item.directory) } -test("session model restore per session without leaking into new sessions", async ({ page, withProject }) => { +test("session model restore per session without leaking into new sessions", async ({ page, project }) => { await page.setViewportSize({ width: 1440, height: 900 }) - await withProject(async ({ directory, gotoSession, trackSession }) => { - await gotoSession() + await project.open() + await project.gotoSession() - const firstState = await chooseOtherModel(page) - const firstKey = await currentModel(page) - const first = await submit(page, `session variant ${Date.now()}`) - trackSession(first) - await waitUser(directory, first) + 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, sessionID: first }) - await waitFooter(page, firstState) + await page.reload() + await waitSession(page, { directory: project.directory, sessionID: first }) + await waitFooter(page, firstState) - await gotoSession() - const fresh = await read(page) - expect(fresh.model).not.toBe(firstState.model) + 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(page, `session model ${Date.now()}`) - trackSession(second) - await waitUser(directory, second) + const secondState = await chooseOtherModel(page, [firstKey]) + const second = await submit(project, `session model ${Date.now()}`) - await goto(page, directory, first) - await waitFooter(page, firstState) + await goto(page, project.directory, first) + await waitFooter(page, firstState) - await goto(page, directory, second) - await waitFooter(page, secondState) + await goto(page, project.directory, second) + await waitFooter(page, secondState) - await gotoSession() - await waitFooter(page, fresh) - }) + await project.gotoSession() + await page.reload() + await waitSession(page, { directory: project.directory }) + await waitFooter(page, fresh) }) -test("session model restore across workspaces", async ({ page, withProject }) => { +test("session model restore across workspaces", async ({ page, project }) => { await page.setViewportSize({ width: 1440, height: 900 }) - await withProject(async ({ directory: root, slug, gotoSession, trackDirectory, trackSession }) => { - await gotoSession() + await project.open() + const root = project.directory + await project.gotoSession() - const firstState = await chooseOtherModel(page) - const firstKey = await currentModel(page) - const first = await submit(page, `root session ${Date.now()}`) - trackSession(first, root) - await waitUser(root, first) + 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, slug, true) + await openSidebar(page) + await setWorkspacesEnabled(page, project.slug, true) - const one = await createWorkspace(page, slug, []) - const oneDir = await newWorkspaceSession(page, one.slug) - trackDirectory(oneDir) + 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(page, `workspace one ${Date.now()}`) - trackSession(second, oneDir) - await waitUser(oneDir, second) + 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, slug, [one.slug]) - const twoDir = await newWorkspaceSession(page, two.slug) - trackDirectory(twoDir) + 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(page, `workspace two ${Date.now()}`) - trackSession(third, twoDir) - await waitUser(twoDir, third) + 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, root, first) + await waitFooter(page, firstState) - await goto(page, oneDir, second) - await waitFooter(page, secondState) + await goto(page, oneDir, second) + await waitFooter(page, secondState) - await goto(page, twoDir, third) - await waitFooter(page, thirdState) + await goto(page, twoDir, third) + await waitFooter(page, thirdState) - await goto(page, root, first) - await waitFooter(page, firstState) - }) + await goto(page, root, first) + await waitFooter(page, firstState) }) -test("variant preserved when switching agent modes", async ({ page, withProject }) => { +test("variant preserved when switching agent modes", async ({ page, project }) => { await page.setViewportSize({ width: 1440, height: 900 }) - await withProject(async ({ directory, gotoSession }) => { - await gotoSession() + await project.open() + await project.gotoSession() - await ensureVariant(page, directory) - const updated = await chooseDifferentVariant(page) + 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 + 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, other) + await waitFooter(page, { agent: other, variant: updated.variant }) - await choose(page, promptAgentSelector, updated.agent) - await waitFooter(page, { agent: updated.agent, 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 index c7529112f..c0a98cb2e 100644 --- a/packages/app/e2e/session/session-review.spec.ts +++ b/packages/app/e2e/session/session-review.spec.ts @@ -1,6 +1,6 @@ import { waitSessionIdle, withSession } from "../actions" import { test, expect } from "../fixtures" -import { inputMatch } from "../prompt/mock" +import { bodyText } from "../prompt/mock" const count = 14 @@ -47,8 +47,12 @@ async function patchWithMock( patchText: string, ) { const callsBefore = await llm.calls() - await llm.toolMatch(inputMatch({ patchText }), "apply_patch", { patchText }) - await sdk.session.promptAsync({ + 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: [ @@ -61,12 +65,16 @@ async function patchWithMock( parts: [{ type: "text", text: "Apply the provided patch exactly once." }], }) - // Wait for the agent loop to actually start before checking idle. - // promptAsync is fire-and-forget — without this, waitSessionIdle can - // return immediately because the session status is still undefined. await expect.poll(() => llm.calls().then((c) => c > callsBefore), { timeout: 30_000 }).toBe(true) - - await waitSessionIdle(sdk, sessionID, 120_000) + 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"]) { @@ -245,7 +253,7 @@ async function fileOverflow(page: Parameters<typeof test>[0]["page"]) { } } -test("review applies inline comment clicks without horizontal overflow", async ({ page, llm, withMockProject }) => { +test("review applies inline comment clicks without horizontal overflow", async ({ page, llm, project }) => { test.setTimeout(180_000) const tag = `review-comment-${Date.now()}` @@ -254,46 +262,45 @@ test("review applies inline comment clicks without horizontal overflow", async ( await page.setViewportSize({ width: 1280, height: 900 }) - await withMockProject(async (project) => { - 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) - }) + 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, withMockProject }) => { +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()}` @@ -302,47 +309,46 @@ test("review file comments submit on click without clipping actions", async ({ p await page.setViewportSize({ width: 1280, height: 900 }) - await withMockProject(async (project) => { - 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) - }) + 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, withMockProject }) => { +test.fixme("review keeps scroll position after a live diff update", async ({ page, llm, project }) => { test.setTimeout(180_000) const tag = `review-${Date.now()}` @@ -352,84 +358,83 @@ test.fixme("review keeps scroll position after a live diff update", async ({ pag await page.setViewportSize({ width: 1600, height: 1000 }) - await withMockProject(async (project) => { - 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) - }) + 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 index a63bd9e3b..709a45b4c 100644 --- a/packages/app/e2e/session/session-undo-redo.spec.ts +++ b/packages/app/e2e/session/session-undo-redo.spec.ts @@ -49,188 +49,185 @@ async function seedConversation(input: { return { prompt, userMessageID } } -test("slash undo sets revert and restores prior prompt", async ({ page, withBackendProject }) => { +test("slash undo sets revert and restores prior prompt", async ({ page, project }) => { test.setTimeout(120_000) const token = `undo_${Date.now()}` - await withBackendProject(async (project) => { - const sdk = project.sdk + 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) + 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 }) + const seeded = await seedConversation({ page, sdk, sessionID: session.id, token }) - await seeded.prompt.click() - await page.keyboard.type("/undo") + 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") + 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 + .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) - }) + 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, withBackendProject }) => { +test("slash redo clears revert and restores latest state", async ({ page, project }) => { test.setTimeout(120_000) const token = `redo_${Date.now()}` - await withBackendProject(async (project) => { - const sdk = project.sdk + 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) + 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 }) + const seeded = await seedConversation({ page, sdk, sessionID: session.id, token }) - await seeded.prompt.click() - await page.keyboard.type("/undo") + 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") + 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 + .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") + 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") + 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 + .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) - }) + 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, withBackendProject }) => { +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 withBackendProject(async (project) => { - const sdk = project.sdk + 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") - await withSession(sdk, `e2e undo redo stack ${Date.now()}`, async (session) => { - project.trackSession(session.id) - await project.gotoSession(session.id) + const undo = page.locator('[data-slash-id="session.undo"]').first() + await expect(undo).toBeVisible() + await page.keyboard.press("Enter") - const first = await seedConversation({ - page, - sdk, - sessionID: session.id, - token: firstToken, + await expect + .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), { + timeout: 30_000, }) - const second = await seedConversation({ - page, - sdk, - sessionID: session.id, - token: secondToken, + .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) - 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) - }) + 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 index 6c885460c..1b5fb1b60 100644 --- a/packages/app/e2e/session/session.spec.ts +++ b/packages/app/e2e/session/session.spec.ts @@ -31,156 +31,152 @@ async function seedMessage(sdk: Sdk, sessionID: string) { .toBeGreaterThan(0) } -test("session can be renamed via header menu", async ({ page, withBackendProject }) => { +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 withBackendProject(async (project) => { - 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) - }) + 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, withBackendProject }) => { +test("session can be archived via header menu", async ({ page, project }) => { const stamp = Date.now() const title = `e2e archive test ${stamp}` - await withBackendProject(async (project) => { - 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) - }) + 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, withBackendProject }) => { +test("session can be deleted via header menu", async ({ page, project }) => { const stamp = Date.now() const title = `e2e delete test ${stamp}` - await withBackendProject(async (project) => { - 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) - }) + 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, withBackendProject }) => { +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 withBackendProject(async (project) => { - await withSession(project.sdk, title, async (session) => { - project.trackSession(session.id) - await seedMessage(project.sdk, session.id) - await project.gotoSession(session.id) - - 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, - }) + 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, }) }) }) |
