test(app): update e2e tests for the session details menu (#57560)

This commit is contained in:
James Long
2026-10-20 19:47:45 -04:00
committed by GitHub
Unverified
parent 4933547d40
commit fe594884c6
5 changed files with 91 additions and 83 deletions
+1 -1
View File
@@ -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[] = []