mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 16:34:22 +00:00
523 lines
22 KiB
TypeScript
523 lines
22 KiB
TypeScript
import { expect, test, type Page } from "@playwright/test"
|
|
import {
|
|
REMOTE_SERVER,
|
|
SERVER,
|
|
T0,
|
|
expectPath,
|
|
holdRoute,
|
|
project,
|
|
seed,
|
|
session,
|
|
sessionHref,
|
|
type TabSeed,
|
|
} from "../utils/app"
|
|
import { mockServers, type MockServerConfig } from "../utils/mock-server"
|
|
import { expectSessionTitle } from "../utils/waits"
|
|
|
|
const directoryA = "C:/server-a"
|
|
|
|
const directoryB = "/home/server-b"
|
|
|
|
const sessionA = session({ id: "ses_server_a", directory: directoryA, title: "Server A session" })
|
|
|
|
const childA = session({
|
|
id: "ses_server_a_child",
|
|
directory: directoryA,
|
|
title: "Server A child",
|
|
parentID: sessionA.id,
|
|
})
|
|
|
|
const sessionB = session({ id: "ses_server_b", directory: directoryB, title: "Server B session" })
|
|
|
|
type Reply = { origin: string; sessionID: string; permissionID: string; body: unknown }
|
|
|
|
test.use({ serviceWorkers: "block" })
|
|
|
|
function pending(id: string, sessionID: string) {
|
|
return { id, sessionID, action: "shell", resources: ["git status"], metadata: {}, save: [] }
|
|
}
|
|
|
|
async function setup(
|
|
page: Page,
|
|
input: { tabs: TabSeed[]; a?: Partial<MockServerConfig>; b?: Partial<MockServerConfig> },
|
|
) {
|
|
const replies: Reply[] = []
|
|
const lists: URL[] = []
|
|
const sessionGets: string[] = []
|
|
page.on("request", (request) => {
|
|
const url = new URL(request.url())
|
|
|
|
if (url.pathname === "/api/permission/request") lists.push(url)
|
|
})
|
|
|
|
const config = (origin: string, name: string, directory: string, sessions: ReturnType<typeof session>[]) => {
|
|
const id = name.toLowerCase().replace(" ", "-")
|
|
|
|
return {
|
|
directory,
|
|
project: project({ id: `proj_${id}`, directory }),
|
|
provider: {
|
|
all: [{ id, name: `${name} Provider`, models: { [id]: { id, name: `${name} Model` } } }],
|
|
connected: [id],
|
|
default: { providerID: id, modelID: id },
|
|
},
|
|
sessions,
|
|
pageMessages: () => ({ items: [] }),
|
|
sessionStatus: Object.fromEntries(sessions.map((item) => [item.id, { type: "running" }])),
|
|
strictDirectory: true,
|
|
onPermissionReply: (reply: Omit<Reply, "origin">) => replies.push({ origin, ...reply }),
|
|
}
|
|
}
|
|
|
|
const servers = await mockServers(page, {
|
|
[SERVER]: {
|
|
...config(SERVER, "Server A", directoryA, [sessionA, childA]),
|
|
eventRetry: 20,
|
|
onSession: (id) => sessionGets.push(id),
|
|
...input.a,
|
|
},
|
|
[REMOTE_SERVER]: { ...config(REMOTE_SERVER, "Server B", directoryB, [sessionB]), ...input.b },
|
|
})
|
|
|
|
await seed(page, { servers: [REMOTE_SERVER], tabs: input.tabs })
|
|
|
|
const listed = (origin: string, directory: string) =>
|
|
expect
|
|
.poll(() =>
|
|
lists.some((url) => url.origin === origin && url.searchParams.get("location[directory]") === directory),
|
|
)
|
|
.toBe(true)
|
|
|
|
return {
|
|
replies,
|
|
sessionGets,
|
|
transport: servers[SERVER]!.transport,
|
|
listed,
|
|
enableAutoAccept: () => enableAutoAccept(page),
|
|
}
|
|
}
|
|
|
|
async function enableAutoAccept(page: Page) {
|
|
await page.keyboard.press("Control+,")
|
|
const autoAccept = page.getByTestId("settings-screen").locator('[data-action="settings-auto-accept-permissions"]')
|
|
await autoAccept.locator('[data-slot="switch-control"]').click()
|
|
await expect(autoAccept.getByRole("switch")).toBeChecked()
|
|
}
|
|
|
|
async function chooseProjectAutoAccept(page: Page, project: string, current: string, choice: "On" | "Off") {
|
|
const settings = page.getByTestId("settings-screen")
|
|
await settings.getByRole("tab", { name: "Projects", exact: true }).click()
|
|
await settings.getByRole("button", { name: project, exact: true }).click()
|
|
const autoAccept = settings.locator('[data-action="settings-project-auto-accept-permissions"]')
|
|
await expect(autoAccept).toHaveText(current)
|
|
await autoAccept.click()
|
|
await page.getByRole("option", { name: choice, exact: true }).click()
|
|
await expect(autoAccept).toHaveText(choice)
|
|
await settings.getByRole("button", { name: "Back to projects", exact: true }).click()
|
|
}
|
|
|
|
const reply = (sessionID: string, permissionID: string) => ({
|
|
origin: SERVER,
|
|
sessionID,
|
|
permissionID,
|
|
body: { decision: "once" },
|
|
})
|
|
|
|
test("settings opened from a remote session sweep every server and keep the remote scope", async ({ page }) => {
|
|
// Server A has no tab and is never visited: its pending request proves one toggle sweeps every connected server.
|
|
const view = await setup(page, { tabs: [], a: { permissions: [pending("permission-pending-a", sessionA.id)] } })
|
|
await page.goto(sessionHref(sessionB.id, REMOTE_SERVER))
|
|
await expectSessionTitle(page, sessionB.title)
|
|
|
|
await view.enableAutoAccept()
|
|
const settings = page.getByTestId("settings-screen")
|
|
await expect(page).toHaveURL("/settings")
|
|
await expect(page.locator('[data-titlebar-tab][data-active="true"]')).toHaveCount(0)
|
|
await expect(page.getByRole("dialog")).toHaveCount(0)
|
|
await view.listed(REMOTE_SERVER, directoryB)
|
|
await expect.poll(() => view.replies).toEqual([reply(sessionA.id, "permission-pending-a")])
|
|
|
|
// Per-server pages start on the server of the tab settings opened from.
|
|
await settings.getByRole("tab", { name: "Models", exact: true }).click()
|
|
await expect(settings.getByRole("button", { name: "Server 127.0.0.1:4097", exact: true })).toBeVisible()
|
|
await expect(settings.getByRole("switch", { name: "Server B Model" })).toBeEnabled()
|
|
await expect(settings.getByRole("switch", { name: "Server A Model" })).toHaveCount(0)
|
|
await settings.getByRole("button", { name: "Back to app" }).click()
|
|
await expect(settings).toBeHidden()
|
|
await expectPath(page, sessionHref(sessionB.id, REMOTE_SERVER))
|
|
await expect(page.locator('[data-titlebar-tab][data-active="true"]')).toContainText(sessionB.title)
|
|
await page.keyboard.press("Control+]")
|
|
await expect(page).toHaveURL("/settings?tab=models")
|
|
await expect(settings.getByRole("tab", { name: "Models", exact: true })).toHaveAttribute("aria-selected", "true")
|
|
await page.keyboard.press("Escape")
|
|
await expectPath(page, sessionHref(sessionB.id, REMOTE_SERVER))
|
|
await expectSessionTitle(page, sessionB.title)
|
|
})
|
|
|
|
test("a remote session whose server signed the app out asks to sign in again, then opens", async ({ page }) => {
|
|
// The saved password is stale; the server's pairing link signs the app in again at the same address.
|
|
await setup(page, {
|
|
tabs: [],
|
|
b: { password: "session-token", pairing: { code: "one-time-code", token: "session-token" } },
|
|
})
|
|
await seed(page, {
|
|
storage: {
|
|
"opencode.global.dat:server": {
|
|
list: [{ type: "http", http: { url: REMOTE_SERVER, password: "old-password" } }],
|
|
},
|
|
},
|
|
})
|
|
await page.goto(sessionHref(sessionB.id, REMOTE_SERVER))
|
|
|
|
const signedOut = page.getByText("Signed out of this server", { exact: true })
|
|
await expect(signedOut).toBeVisible()
|
|
await page.getByRole("button", { name: "Sign in again", exact: true }).click()
|
|
const editor = page.getByRole("dialog", { name: "Edit server" })
|
|
|
|
// An empty address explains itself instead of closing the dialog.
|
|
await editor.getByRole("button", { name: "Use password", exact: true }).click()
|
|
await editor.getByLabel("Server address", { exact: true }).fill("")
|
|
await editor.getByRole("button", { name: "Save", exact: true }).click()
|
|
await expect(editor.getByRole("alert")).toHaveText("Enter a valid HTTP or HTTPS server address.")
|
|
await editor.getByRole("button", { name: "Use pairing link", exact: true }).click()
|
|
|
|
await editor.getByLabel("Pairing link", { exact: true }).fill(`${REMOTE_SERVER}/auth/connect/one-time-code`)
|
|
await editor.getByRole("button", { name: "Save", exact: true }).click()
|
|
|
|
await expect(editor).toHaveCount(0)
|
|
await expectSessionTitle(page, sessionB.title)
|
|
await expect(signedOut).toHaveCount(0)
|
|
})
|
|
|
|
// The session route waits this long for an uncached session before it calls the server unreachable.
|
|
const RESOLUTION_TIMEOUT_MS = 30_000
|
|
|
|
// How often the app checks each server's health.
|
|
const SERVER_HEALTH_POLL_MS = 10_000
|
|
|
|
test("a session its server never answers for says the server is unreachable, then opens", async ({ page }) => {
|
|
const late = session({ id: "ses_server_a_late", directory: directoryA, title: "Late session" })
|
|
const sessions = [sessionA]
|
|
await setup(page, { tabs: [], a: { sessions } })
|
|
const metadata = await holdRoute(page, new RegExp(`/api/session/${late.id}(\\?|$)`), { method: "GET" })
|
|
await page.clock.install()
|
|
await page.goto(sessionHref(late.id))
|
|
await metadata.arrived
|
|
const unreachable = page.getByText("Can't reach this server", { exact: true })
|
|
await expect(unreachable).toHaveCount(0)
|
|
|
|
await page.clock.fastForward(RESOLUTION_TIMEOUT_MS)
|
|
await expect(unreachable).toBeVisible()
|
|
await expect(page.getByRole("button", { name: "Close Tab", exact: true })).toBeVisible()
|
|
|
|
// The request that hung still fills the cache when it answers.
|
|
sessions.push(late)
|
|
metadata.release()
|
|
await expectSessionTitle(page, late.title)
|
|
await expect(unreachable).toHaveCount(0)
|
|
})
|
|
|
|
// The inbox shows what it has once this long passes, even while a server still has not answered.
|
|
const INBOX_SETTLE_TIMEOUT_MS = 10_000
|
|
|
|
test("one server that never answers the inbox index does not hide the other server's sessions", async ({ page }) => {
|
|
await setup(page, { tabs: [] })
|
|
await seed(page, { settings: { appearance: { tabLayout: "inbox" } } })
|
|
|
|
const index = await holdRoute(page, new RegExp(`^${REMOTE_SERVER}/api/session\\?(.*&)?order=desc`), {
|
|
method: "GET",
|
|
})
|
|
|
|
await page.clock.install()
|
|
await page.goto("/")
|
|
await index.arrived
|
|
|
|
const inbox = page.locator('[data-slot="sidebar-inbox"]')
|
|
const row = inbox.locator('[data-slot="sidebar-inbox-item"]').filter({ hasText: sessionA.title })
|
|
await expect(inbox).toHaveAttribute("aria-busy", "true")
|
|
await expect(row).toHaveCount(0)
|
|
|
|
await page.clock.fastForward(INBOX_SETTLE_TIMEOUT_MS)
|
|
await expect(inbox).toHaveAttribute("aria-busy", "false")
|
|
await expect(row).toBeVisible()
|
|
})
|
|
|
|
test("an open session stays visible while its server stops answering", async ({ page }) => {
|
|
const late = session({ id: "ses_server_a_late", directory: directoryA, title: "Late session" })
|
|
await setup(page, { tabs: [sessionA.id, late.id], a: { sessions: [sessionA] } })
|
|
await holdRoute(page, new RegExp(`/api/session/${late.id}(\\?|$)`), { method: "GET" })
|
|
await page.clock.install()
|
|
await page.goto(sessionHref(sessionA.id))
|
|
await expectSessionTitle(page, sessionA.title)
|
|
|
|
await page.route(`${SERVER}/api/info`, (route) => route.fulfill({ status: 503, body: "" }))
|
|
await page.clock.fastForward(SERVER_HEALTH_POLL_MS)
|
|
const unreachable = page.getByText("Can't reach this server", { exact: true })
|
|
|
|
// An uncached session on the same server proves the app now considers the server down.
|
|
await page.locator(`[data-titlebar-tab-slot]:has(a[href="${sessionHref(late.id)}"])`).click()
|
|
await expect(unreachable).toBeVisible()
|
|
|
|
await page.locator(`[data-titlebar-tab-slot]:has(a[href="${sessionHref(sessionA.id)}"])`).click()
|
|
await expectSessionTitle(page, sessionA.title)
|
|
await expect(unreachable).toHaveCount(0)
|
|
})
|
|
|
|
test("auto-accept responds for an unfocused server session and its child", async ({ page }) => {
|
|
const view = await setup(page, { tabs: [sessionA.id, { session: sessionB.id, server: REMOTE_SERVER }] })
|
|
await page.goto(sessionHref(sessionA.id))
|
|
await expectSessionTitle(page, sessionA.title)
|
|
await view.enableAutoAccept()
|
|
await view.listed(SERVER, directoryA)
|
|
await page.keyboard.press("Escape")
|
|
|
|
await page.locator(`[data-titlebar-tab-slot]:has(a[href="${sessionHref(sessionB.id, REMOTE_SERVER)}"])`).click()
|
|
await expectSessionTitle(page, sessionB.title)
|
|
await view.transport.waitForConnection()
|
|
|
|
for (const [index, item] of [sessionA, childA].entries()) {
|
|
await view.transport.send({
|
|
id: `evt_permission_background_${index}`,
|
|
created: 1700000001000 + index,
|
|
type: "permission.asked",
|
|
location: { directory: directoryA },
|
|
data: { ...pending(`permission-background-${index}`, item.id) },
|
|
})
|
|
}
|
|
|
|
await expect
|
|
.poll(() => view.replies)
|
|
.toEqual([reply(sessionA.id, "permission-background-0"), reply(childA.id, "permission-background-1")])
|
|
})
|
|
|
|
test("auto-accept sweeps again after a reconnect and resyncs active sessions", async ({ page }) => {
|
|
const queued: ReturnType<typeof pending>[] = []
|
|
const failures = { next: 0 }
|
|
|
|
const view = await setup(page, {
|
|
tabs: [sessionA.id],
|
|
a: {
|
|
permissions: () => queued,
|
|
permissionListFailures: () => failures.next-- > 0,
|
|
},
|
|
})
|
|
|
|
await page.goto(sessionHref(sessionA.id))
|
|
await expectSessionTitle(page, sessionA.title)
|
|
const first = await view.transport.waitForConnection()
|
|
await view.enableAutoAccept()
|
|
await view.listed(SERVER, directoryA)
|
|
await page.keyboard.press("Escape")
|
|
|
|
// Asked while the client is disconnected, so only a reconnect sweep finds it. The first listing after the
|
|
// reconnect fails, so only the bounded sweep retry can deliver the reply.
|
|
queued.push(pending("permission-offline-a", sessionA.id))
|
|
failures.next = 1
|
|
const before = view.sessionGets.length
|
|
await view.transport.disconnect()
|
|
await view.transport.waitForConnection({ after: first.id })
|
|
|
|
await expect.poll(() => view.replies).toEqual([reply(sessionA.id, "permission-offline-a")])
|
|
expect(view.sessionGets.slice(before)).toContain(sessionA.id)
|
|
})
|
|
|
|
test("auto-accept approves a request discovered by opening a session", async ({ page }) => {
|
|
// Served only by the per-session permission list, never by a location sweep or an event.
|
|
const view = await setup(page, {
|
|
tabs: [sessionA.id],
|
|
a: { sessionPermissions: { [sessionA.id]: [pending("permission-synced-a", sessionA.id)] } },
|
|
})
|
|
|
|
await page.goto(sessionHref(sessionA.id))
|
|
await expectSessionTitle(page, sessionA.title)
|
|
await view.enableAutoAccept()
|
|
await expect.poll(() => view.replies).toEqual([reply(sessionA.id, "permission-synced-a")])
|
|
})
|
|
|
|
test("a project's auto-accept choice overrides the default in both directions", async ({ page }) => {
|
|
const api = session({ id: "ses_project_api", directory: "/work/api", title: "API session", projectID: "proj_api" })
|
|
const web = session({ id: "ses_project_web", directory: "/work/web", title: "Web session", projectID: "proj_web" })
|
|
const requests = { [api.id]: [pending("permission-api", api.id)], [web.id]: [pending("permission-web", web.id)] }
|
|
const replies: string[] = []
|
|
await mockServers(page, {
|
|
[SERVER]: {
|
|
directory: "/work/api",
|
|
project: project({ id: "proj_api", directory: "/work/api" }),
|
|
projects: [
|
|
project({ id: "proj_api", directory: "/work/api" }),
|
|
project({ id: "proj_web", directory: "/work/web" }),
|
|
],
|
|
provider: { all: [], connected: [], default: {} },
|
|
sessions: [api, web],
|
|
pageMessages: () => ({ items: [] }),
|
|
sessionStatus: { [api.id]: { type: "running" }, [web.id]: { type: "running" } },
|
|
permissions: () => Object.values(requests).flat(),
|
|
sessionPermissions: requests,
|
|
onPermissionReply: (reply) => replies.push(reply.permissionID),
|
|
},
|
|
})
|
|
await seed(page, { tabs: [api.id, web.id], usedProjects: { local: ["proj_api", "proj_web"] } })
|
|
const dock = page.locator('[data-component="dock-prompt"][data-kind="permission"]')
|
|
const settings = page.getByTestId("settings-screen")
|
|
|
|
await page.goto(sessionHref(web.id))
|
|
await expectSessionTitle(page, web.title)
|
|
await expect(dock.getByText("git status")).toBeVisible()
|
|
|
|
// On for api while the default is off; off for web before the default turns on.
|
|
await page.keyboard.press("Control+,")
|
|
await chooseProjectAutoAccept(page, "api", "Use default (Off)", "On")
|
|
await expect.poll(() => replies).toEqual(["permission-api"])
|
|
await chooseProjectAutoAccept(page, "web", "Use default (Off)", "Off")
|
|
await settings.getByRole("tab", { name: "Preferences", exact: true }).click()
|
|
const autoAccept = settings.locator('[data-action="settings-auto-accept-permissions"]')
|
|
await autoAccept.locator('[data-slot="switch-control"]').click()
|
|
await expect(autoAccept.getByRole("switch")).toBeChecked()
|
|
await settings.getByRole("button", { name: "Back to app" }).click()
|
|
|
|
await expectSessionTitle(page, web.title)
|
|
await expect(dock.getByText("git status")).toBeVisible()
|
|
expect(replies).toEqual(["permission-api"])
|
|
|
|
// Back on the default, web follows it.
|
|
await page.keyboard.press("Control+,")
|
|
await settings.getByRole("tab", { name: "Projects", exact: true }).click()
|
|
await settings.getByRole("button", { name: "web", exact: true }).click()
|
|
await settings.locator('[data-action="settings-project-auto-accept-permissions"]').click()
|
|
await page.getByRole("option", { name: "Use default (On)" }).click()
|
|
await expect.poll(() => replies).toEqual(["permission-api", "permission-web"])
|
|
})
|
|
|
|
test("customized vertical sidebar merges a project across servers and narrows it with the view menu", async ({
|
|
page,
|
|
}) => {
|
|
// The same repo on two machines shares its project ID, so both servers' sessions share one group.
|
|
const shared = (directory: string) => project({ id: "proj_shared", directory, name: "shared" })
|
|
|
|
const local = session({ id: "ses_local_open", directory: directoryA, title: "Local open", projectID: "proj_shared" })
|
|
|
|
const recent = session({
|
|
id: "ses_local_recent",
|
|
directory: directoryA,
|
|
title: "Local recent",
|
|
projectID: "proj_shared",
|
|
created: T0 - 60_000,
|
|
})
|
|
|
|
const remote = session({
|
|
id: "ses_remote_open",
|
|
directory: directoryB,
|
|
title: "Remote open",
|
|
projectID: "proj_shared",
|
|
})
|
|
|
|
const config = (directory: string, sessions: ReturnType<typeof session>[]) => ({
|
|
directory,
|
|
project: shared(directory),
|
|
provider: { all: [], connected: [], default: {} },
|
|
sessions,
|
|
pageMessages: () => ({ items: [] }),
|
|
})
|
|
|
|
await mockServers(page, {
|
|
[SERVER]: config(directoryA, [local, recent]),
|
|
[REMOTE_SERVER]: config(directoryB, [remote]),
|
|
})
|
|
await seed(page, {
|
|
servers: [REMOTE_SERVER],
|
|
tabs: [local.id, { session: remote.id, server: REMOTE_SERVER }],
|
|
settings: { appearance: { tabLayout: "vertical" }, general: { sidebarOrganize: "customize" } },
|
|
})
|
|
await page.goto(sessionHref(local.id))
|
|
await expectSessionTitle(page, local.title)
|
|
|
|
const sidebar = page.locator('[data-slot="vertical-tabs"]')
|
|
|
|
const groups = sidebar.locator("[data-sidebar-group]")
|
|
|
|
const titles = sidebar.locator("[data-titlebar-tab-slot] [data-titlebar-tab-title]")
|
|
|
|
await expect(groups).toHaveText([/shared$/])
|
|
await expect(titles).toHaveText([local.title, remote.title])
|
|
// Only the tab from the other machine names its server.
|
|
await expect(sidebar.locator('[data-slot="tab-label"]')).toHaveCount(1)
|
|
await expect(sidebar.getByRole("button", { name: recent.title })).toBeVisible()
|
|
|
|
// Collapsing the group that holds the current session keeps only that session below it.
|
|
const header = groups.first().locator("button[aria-expanded]")
|
|
|
|
await header.click()
|
|
await expect(header).toHaveAttribute("aria-expanded", "false")
|
|
// Only a collapsed group counts its open tabs.
|
|
await expect(groups).toHaveText([/shared\s*2$/])
|
|
await expect(titles).toHaveText([local.title])
|
|
await expect(sidebar.getByRole("button", { name: recent.title })).toHaveCount(0)
|
|
await header.click()
|
|
await expect(titles).toHaveText([local.title, remote.title])
|
|
|
|
const menu = page.getByRole("menu")
|
|
|
|
await page.getByRole("button", { name: "View options" }).click()
|
|
|
|
// Moving between submenu triggers swaps the open submenu instead of stacking them.
|
|
for (const [name, item] of [
|
|
[/^Group by/, "Server → Project"],
|
|
[/^Sort by/, "Attention"],
|
|
] as const) {
|
|
const box = await menu.getByRole("menuitem", { name }).boundingBox()
|
|
|
|
if (!box) throw new Error(`no ${name} trigger`)
|
|
await page.mouse.move(box.x + 40, box.y + box.height / 2, { steps: 12 })
|
|
await expect(menu.last()).toContainText(item)
|
|
}
|
|
|
|
await expect(menu).toHaveCount(2)
|
|
await menu.getByRole("menuitem", { name: /^Group by/ }).click()
|
|
await page.getByRole("menuitemradio", { name: "Server → Project" }).click()
|
|
await page.keyboard.press("Escape")
|
|
await expect(groups).toHaveCount(4)
|
|
await expect(sidebar.locator('[data-slot="tab-label"]')).toHaveCount(0)
|
|
|
|
// A server filter hides the other server's rows, but never the active tab.
|
|
await page.getByRole("button", { name: "View options" }).click()
|
|
await page.getByRole("menuitem", { name: /^Server/ }).click()
|
|
await page.getByRole("menuitemcheckbox").last().click()
|
|
await page.keyboard.press("Escape")
|
|
await page.keyboard.press("Escape")
|
|
await expect(titles).toHaveText([local.title, remote.title])
|
|
await expect(sidebar.getByRole("button", { name: recent.title })).toHaveCount(0)
|
|
await page.getByRole("button", { name: "View options" }).click()
|
|
await menu.getByRole("menuitem", { name: "Reset filters" }).click()
|
|
await expect(sidebar.getByRole("button", { name: recent.title })).toBeVisible()
|
|
})
|
|
|
|
test("deleting a session's tab in the customized sidebar removes it from the list too", async ({ page }) => {
|
|
const first = session({ id: "ses_delete_first", directory: directoryA, title: "Delete me" })
|
|
|
|
const second = session({ id: "ses_delete_second", directory: directoryA, title: "Keep me" })
|
|
|
|
await mockServers(page, {
|
|
[SERVER]: {
|
|
directory: directoryA,
|
|
project: project({ id: "proj_delete", directory: directoryA }),
|
|
provider: { all: [], connected: [], default: {} },
|
|
sessions: [first, second],
|
|
pageMessages: () => ({ items: [] }),
|
|
},
|
|
})
|
|
await seed(page, {
|
|
tabs: [first.id, second.id],
|
|
settings: { appearance: { tabLayout: "vertical" }, general: { sidebarOrganize: "customize" } },
|
|
})
|
|
await page.goto(sessionHref(first.id))
|
|
await expectSessionTitle(page, first.title)
|
|
|
|
const sidebar = page.locator('[data-slot="vertical-tabs"]')
|
|
|
|
await sidebar.locator("[data-titlebar-tab-title]", { hasText: first.title }).click({ button: "right" })
|
|
await page.getByRole("menuitem", { name: "Delete…", exact: true }).click()
|
|
await page.getByRole("dialog").getByRole("button", { name: "Delete session", exact: true }).click()
|
|
await expectSessionTitle(page, second.title)
|
|
// The session the app had loaded must not come back as a recent row, which would open a missing session.
|
|
await expect(sidebar.getByText(first.title)).toHaveCount(0)
|
|
})
|