mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 19:46:16 +03:00
test(app): update e2e tests for the session details menu (#57560)
This commit is contained in:
committed by
GitHub
Unverified
parent
4933547d40
commit
fe594884c6
@@ -405,7 +405,7 @@ for (const state of [
|
||||
for (const loaded of state.recovery ? [false, true] : [true]) {
|
||||
if (state.recovery && loaded) {
|
||||
messages.resolve()
|
||||
await expect(header.getByRole("button", { name: "More options", exact: true })).toBeVisible()
|
||||
await expect(header.getByRole("button", { name: "Session details", exact: true })).toBeVisible()
|
||||
}
|
||||
|
||||
await expect(trigger.locator('[data-slot="session-location-project"]')).toHaveText(name)
|
||||
|
||||
@@ -16,7 +16,8 @@ type Surface = "popover" | "dialog"
|
||||
|
||||
// A session in a worktree of the project. The harness answers the MCP list for each location directory and stores
|
||||
// connect/disconnect results per directory; `requests` records every MCP request and the directory it targeted.
|
||||
// The popover belongs to the summary extension; the dialog (Ctrl+;) is the app's own MCP toggle.
|
||||
// The popover is the session details menu's MCP submenu, which shows state but does not toggle; the dialog (Ctrl+;) is
|
||||
// the app's own MCP toggle.
|
||||
async function open(
|
||||
page: Page,
|
||||
input: { surface: Surface } & Pick<MockServerConfig, "mcp" | "onMcpAction" | "integrations" | "onIntegrationOAuth">,
|
||||
@@ -45,12 +46,22 @@ async function open(
|
||||
await page.goto(sessionHref(sessionID))
|
||||
await expectSessionTitle(page, title)
|
||||
await expect(page.getByRole("textbox", { name: "Prompt", exact: true })).toBeEditable()
|
||||
const panel = page.getByRole("dialog", { name: input.surface === "popover" ? "MCP" : "MCPs", exact: true })
|
||||
|
||||
const panel =
|
||||
input.surface === "popover"
|
||||
? page.getByRole("menu", { name: "MCP", exact: true })
|
||||
: page.getByRole("dialog", { name: "MCPs", exact: true })
|
||||
|
||||
const show = async () => {
|
||||
if (input.surface === "dialog") return page.keyboard.press("Control+;")
|
||||
await page.getByRole("button", { name: "Session details", exact: true }).click()
|
||||
await page.getByRole("button", { name: "MCP", exact: true }).click()
|
||||
const item = page.getByRole("menuitem", { name: "MCP", exact: true })
|
||||
await item.click()
|
||||
await expect(panel).toBeVisible()
|
||||
// Straight across into the submenu, so the pointer never crosses the items below MCP.
|
||||
const from = (await item.boundingBox())!
|
||||
const to = (await panel.boundingBox())!
|
||||
await page.mouse.move(to.x + to.width / 2, from.y + from.height / 2, { steps: 5 })
|
||||
}
|
||||
|
||||
await show()
|
||||
@@ -66,7 +77,7 @@ for (const shared of [true, false]) {
|
||||
const actions: Parameters<NonNullable<MockServerConfig["onMcpAction"]>>[0][] = []
|
||||
|
||||
const view = await open(page, {
|
||||
surface: "popover",
|
||||
surface: "dialog",
|
||||
mcp: (target) =>
|
||||
!shared && target !== workspace ? [] : [{ name: "figma-desktop", status: { status: "disabled" } }],
|
||||
onMcpAction: (action) => void actions.push(action),
|
||||
@@ -85,7 +96,7 @@ for (const shared of [true, false]) {
|
||||
expect(view.requests.every((request) => request.directory === workspace)).toBe(true)
|
||||
|
||||
view.requests.length = 0
|
||||
await view.panel.locator('[data-slot="truncated-text-label"]').filter({ hasText: "figma-desktop" }).click()
|
||||
await view.panel.locator('[data-slot="switch-control"]').click()
|
||||
await expect(view.toggle).not.toBeChecked()
|
||||
await expect(view.toggle).toBeEnabled()
|
||||
expect(actions).toEqual([
|
||||
@@ -100,7 +111,37 @@ for (const shared of [true, false]) {
|
||||
})
|
||||
}
|
||||
|
||||
for (const surface of ["popover", "dialog"] as const) {
|
||||
test("the MCP popover shows a failed server and reconnects it from its details", async ({ page }) => {
|
||||
const error = "Streamable HTTP error: Error POSTing to endpoint: 404 Not Found"
|
||||
|
||||
const view = await open(page, {
|
||||
surface: "popover",
|
||||
mcp: (target) => [
|
||||
{ name: "figma-desktop", status: target !== workspace ? { status: "connected" } : { status: "failed", error } },
|
||||
],
|
||||
})
|
||||
|
||||
const row = view.panel.locator(".session-mcp-row").filter({ hasText: "figma-desktop" })
|
||||
await expect(view.toggle).toHaveCount(0)
|
||||
await expect(row.getByText("Failed", { exact: true })).toBeVisible()
|
||||
const failed = view.panel.getByRole("button", { name: "Details for figma-desktop", exact: true })
|
||||
await expect(failed).toHaveText("Details")
|
||||
view.requests.length = 0
|
||||
await failed.click()
|
||||
const details = page.getByRole("dialog", { name: "figma-desktop", exact: true })
|
||||
await expect(details).toHaveAccessibleDescription("Connection failed")
|
||||
await expect(details.getByText(error, { exact: true })).toBeVisible()
|
||||
await details.getByRole("button", { name: "Try again", exact: true }).click()
|
||||
await expect(details).toBeHidden()
|
||||
expect(view.requests.filter((request) => request.path.endsWith("/connect"))).toEqual([
|
||||
{ path: "/api/experimental/mcp/figma-desktop/connect", directory: workspace },
|
||||
])
|
||||
|
||||
if (!(await view.panel.isVisible())) await view.show()
|
||||
await expect(row.getByText("Connected", { exact: true })).toBeVisible()
|
||||
})
|
||||
|
||||
for (const surface of ["dialog"] as const) {
|
||||
test(`shows connection failures from the MCP ${surface} and allows reconnecting`, async ({ page }) => {
|
||||
const error = "Streamable HTTP error: Error POSTing to endpoint: 404 Not Found"
|
||||
const state = { fail: true }
|
||||
@@ -175,13 +216,6 @@ for (const surface of ["popover", "dialog"] as const) {
|
||||
|
||||
state.fail = false
|
||||
await details.getByRole("button", { name: "Try again", exact: true }).click()
|
||||
|
||||
if (surface === "popover") {
|
||||
await expect(details).toBeHidden()
|
||||
|
||||
if (!(await view.panel.isVisible())) await view.show()
|
||||
}
|
||||
|
||||
await expect(view.toggle).toBeChecked()
|
||||
await expect(view.toggle).toBeEnabled()
|
||||
})
|
||||
@@ -243,7 +277,15 @@ for (const surface of ["popover", "dialog"] as const) {
|
||||
])
|
||||
|
||||
if (!(await view.panel.isVisible())) await view.show()
|
||||
await expect(view.toggle).toBeChecked()
|
||||
|
||||
if (surface === "dialog") await expect(view.toggle).toBeChecked()
|
||||
else
|
||||
await expect(
|
||||
view.panel
|
||||
.locator(".session-mcp-row")
|
||||
.filter({ hasText: "figma-desktop" })
|
||||
.getByText("Connected", { exact: true }),
|
||||
).toBeVisible()
|
||||
await expect(view.panel.getByRole("button", { name: "Sign in to figma-desktop", exact: true })).toHaveCount(0)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ for (const scheme of ["light", "dark"] as const) {
|
||||
}
|
||||
}
|
||||
|
||||
test("mobile extension drawers keep configured lists, retry, refresh, and MCP toggles working", async ({ page }) => {
|
||||
test("mobile extension drawers keep configured lists, retry, refresh, and MCP status working", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
await mockStressTimeline(page)
|
||||
const state = { fail: true, extra: false, enabled: true }
|
||||
@@ -182,7 +182,8 @@ test("mobile extension drawers keep configured lists, retry, refresh, and MCP to
|
||||
|
||||
await details.getByRole("button", { name: "MCP", exact: true }).click()
|
||||
const mcp = page.getByRole("dialog", { name: "MCP", exact: true })
|
||||
await expect(mcp.getByRole("switch")).toHaveCount(30)
|
||||
await expect(mcp.locator(".session-mcp-row")).toHaveCount(30)
|
||||
await expect(mcp.getByRole("switch")).toHaveCount(0)
|
||||
await expect
|
||||
.poll(() =>
|
||||
mcp
|
||||
@@ -191,18 +192,9 @@ test("mobile extension drawers keep configured lists, retry, refresh, and MCP to
|
||||
)
|
||||
.toBe(true)
|
||||
await expect(mcp).toBeInViewport({ ratio: 1 })
|
||||
const disconnect = page.waitForRequest((request) => new URL(request.url()).pathname.endsWith("/disconnect"))
|
||||
await mcp.getByRole("switch", { name: "mcp-00", exact: true }).focus()
|
||||
await mcp.getByRole("switch", { name: "mcp-00", exact: true }).press("Space")
|
||||
await disconnect
|
||||
await expect(mcp.getByRole("switch", { name: "mcp-00", exact: true })).not.toBeChecked()
|
||||
|
||||
const last = mcp
|
||||
.locator(".session-mcp-row")
|
||||
.filter({ has: page.getByRole("switch", { name: "mcp-29", exact: true }) })
|
||||
|
||||
const last = mcp.locator(".session-mcp-row").filter({ hasText: "mcp-29" })
|
||||
await last.scrollIntoViewIfNeeded()
|
||||
await expect(last).toBeInViewport()
|
||||
await mcp.getByRole("switch", { name: "mcp-29", exact: true }).focus()
|
||||
await expect(mcp.getByRole("switch", { name: "mcp-29", exact: true })).toBeFocused()
|
||||
await expect(mcp.getByRole("button", { name: "Manage MCP servers", exact: true })).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -124,7 +124,7 @@ for (const position of ["top", "bottom"] as const) {
|
||||
await expect(details.locator('[data-slot="mobile-panel-header"]')).toHaveCount(0)
|
||||
await expect(details.getByRole("button", { name: "Close", exact: true })).toHaveCount(0)
|
||||
await expect(details.getByRole("heading", { name: "Session details", exact: true })).toHaveCSS("width", "1px")
|
||||
await expect(details.getByText(fixture.project.name, { exact: true })).toBeVisible()
|
||||
await expect(details.getByRole("button", { name: "Local repository", exact: true })).toBeVisible()
|
||||
await expect(details.getByRole("button", { name: "No changes", exact: true })).toBeVisible()
|
||||
await expect(details.getByRole("button", { name: "MCP", exact: true })).toBeVisible()
|
||||
await page.locator('[data-slot="mobile-drawer-overlay"]').click({ position: { x: 10, y: 10 } })
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import { expect, test, type Locator, type Page } from "@playwright/test"
|
||||
import { SERVER, holdRoute, seed, sessionHref } from "../utils/app"
|
||||
import { openWithDirection } from "../utils/direction"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
@@ -89,10 +89,12 @@ test("a session in a worktree subfolder names its worktree and lists cached work
|
||||
const list = await holdRoute(page, (url) => url.pathname === "/api/worktree", { method: "GET" })
|
||||
await trigger.click()
|
||||
await location.click()
|
||||
await enter(page, location, page.getByRole("menu").filter({ hasText: "Move session to" }))
|
||||
await list.arrived
|
||||
await page.getByRole("menuitem", { name: "Worktree", exact: true }).click()
|
||||
await expect(page.getByRole("menuitem", { name: "other", exact: true })).toBeVisible()
|
||||
await expect(page.getByRole("menuitem", { name: "feature", exact: true })).toHaveCount(0)
|
||||
const existing = page.getByRole("menu").filter({ has: page.getByRole("menuitem", { name: "other", exact: true }) })
|
||||
await expect(existing.getByRole("menuitem", { name: "other", exact: true })).toBeVisible()
|
||||
await expect(existing.getByRole("menuitem", { name: "feature", exact: true })).toHaveCount(0)
|
||||
list.release()
|
||||
})
|
||||
|
||||
@@ -312,7 +314,12 @@ test("summary catalogs load, refresh while cached, and tell errors from empty",
|
||||
await page.getByRole("button", { name: "Session details", exact: true }).click()
|
||||
const summary = page.getByRole("menu", { name: "Session details", exact: true })
|
||||
const menu = (name: string) => page.getByRole("menu", { name, exact: true })
|
||||
const open = (name: string) => summary.getByRole("menuitem", { name, exact: true }).click()
|
||||
|
||||
const open = async (name: string) => {
|
||||
const item = summary.getByRole("menuitem", { name, exact: true })
|
||||
await item.click()
|
||||
await enter(page, item, menu(name))
|
||||
}
|
||||
|
||||
await prefetch.arrived
|
||||
await expect(summary.getByRole("menuitem", { name: "Local repository", exact: true })).toBeVisible()
|
||||
@@ -393,8 +400,10 @@ test("MCP rows show each server's state and the menu opens the MCP servers dialo
|
||||
})
|
||||
await page.goto(sessionHref(fixture.targetID))
|
||||
await page.getByRole("button", { name: "Session details", exact: true }).click()
|
||||
await page.getByRole("menuitem", { name: "MCP", exact: true }).click()
|
||||
const item = page.getByRole("menuitem", { name: "MCP", exact: true })
|
||||
await item.click()
|
||||
const submenu = page.getByRole("menu", { name: "MCP", exact: true })
|
||||
await enter(page, item, submenu)
|
||||
|
||||
const row = (name: string) =>
|
||||
submenu.locator(".session-mcp-row").filter({ has: page.getByText(name, { exact: true }) })
|
||||
@@ -417,53 +426,6 @@ test("MCP rows show each server's state and the menu opens the MCP servers dialo
|
||||
await expect(submenu).toBeHidden()
|
||||
})
|
||||
|
||||
test("MCP authentication starts before a slow resource catalog finishes", async ({ page, context }) => {
|
||||
const state = { connected: false }
|
||||
const attempts: string[] = []
|
||||
await context.route("https://auth.example.test/**", (route) => route.fulfill({ body: "Sign in" }))
|
||||
await mockStressTimeline(page, {
|
||||
mcp: [{ name: "linear", integrationID: "linear-oauth", status: { status: "needs_auth" } }],
|
||||
onMcpAction: () => {
|
||||
state.connected = true
|
||||
|
||||
return { status: "needs_auth" }
|
||||
},
|
||||
integrations: [{ id: "linear-oauth", name: "Linear", methods: [{ id: "oauth", type: "oauth" }], connections: [] }],
|
||||
onIntegrationOAuth: () => ({ url: "https://auth.example.test/authorize" }),
|
||||
})
|
||||
page.on("request", (request) => {
|
||||
if (request.method() === "POST" && new URL(request.url()).pathname.startsWith("/api/integration/"))
|
||||
attempts.push(request.url())
|
||||
})
|
||||
|
||||
// Only the resource catalog refresh after connecting is slow.
|
||||
const resources = await holdRoute(page, (url) => state.connected && url.pathname === "/api/mcp/resource", {
|
||||
method: "GET",
|
||||
})
|
||||
|
||||
await page.goto(sessionHref(fixture.targetID))
|
||||
await page.getByRole("button", { name: "Session details", exact: true }).click()
|
||||
await page.getByRole("menuitem", { name: "MCP", exact: true }).click()
|
||||
const submenu = page.getByRole("menu", { name: "MCP", exact: true })
|
||||
const signIn = submenu.getByRole("button", { name: "Sign in to linear", exact: true })
|
||||
await expect(signIn).toBeEnabled()
|
||||
|
||||
try {
|
||||
const popup = page.waitForEvent("popup")
|
||||
await signIn.click()
|
||||
await expect(await popup).toHaveURL("https://auth.example.test/authorize")
|
||||
await resources.arrived
|
||||
// The sign-in link waits for the catalog before it is ready again.
|
||||
await expect(signIn).toBeDisabled()
|
||||
} finally {
|
||||
resources.release()
|
||||
}
|
||||
|
||||
await expect(signIn).toBeEnabled()
|
||||
expect(attempts).toHaveLength(1)
|
||||
expect(new URL(attempts[0]).searchParams.get("location[directory]")).toBe(fixture.directory)
|
||||
})
|
||||
|
||||
test("multiple desktop connections show the session's server name", async ({ page }) => {
|
||||
await mockStressTimeline(page)
|
||||
await page.route("http://secondary.test/**", (route) =>
|
||||
@@ -523,8 +485,10 @@ test.describe("remote configuration", () => {
|
||||
{ name: "LSP", path: `${home}/lsp.json` },
|
||||
].entries()) {
|
||||
await trigger.click()
|
||||
await summary.getByRole("menuitem", { name: service.name, exact: true }).click()
|
||||
const item = summary.getByRole("menuitem", { name: service.name, exact: true })
|
||||
await item.click()
|
||||
const menu = page.getByRole("menu", { name: service.name, exact: true })
|
||||
await enter(page, item, menu)
|
||||
const copy = menu.getByRole("button", { name: "Copy configuration file path", exact: true })
|
||||
await expect(tooltip).toHaveCount(0)
|
||||
await copy.hover()
|
||||
@@ -552,7 +516,9 @@ test.describe("remote configuration", () => {
|
||||
entries.length = 0
|
||||
await page.evaluate(() => navigator.clipboard.writeText("original clipboard"))
|
||||
await trigger.click()
|
||||
await summary.getByRole("menuitem", { name: "Skills", exact: true }).click()
|
||||
const skills = summary.getByRole("menuitem", { name: "Skills", exact: true })
|
||||
await skills.click()
|
||||
await enter(page, skills, page.getByRole("menu", { name: "Skills", exact: true }))
|
||||
|
||||
const copy = page
|
||||
.getByRole("menu", { name: "Skills", exact: true })
|
||||
@@ -592,6 +558,14 @@ test("long MCP lists stay in the viewport in a real RTL locale", async ({ page }
|
||||
await expect(menu).toBeHidden()
|
||||
})
|
||||
|
||||
// Moves the pointer straight across from a submenu's trigger into the open submenu, so it never crosses sibling items.
|
||||
async function enter(page: Page, trigger: Locator, menu: Locator) {
|
||||
await expect(menu).toBeVisible()
|
||||
const from = (await trigger.boundingBox())!
|
||||
const to = (await menu.boundingBox())!
|
||||
await page.mouse.move(to.x + to.width / 2, from.y + from.height / 2, { steps: 5 })
|
||||
}
|
||||
|
||||
// Solid reports reactive work created without an owner; the warning appears in dev builds only.
|
||||
function ownerWarnings(page: Page) {
|
||||
const warnings: string[] = []
|
||||
|
||||
Reference in New Issue
Block a user