remote-tab-busy.spec.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import { expect, test, type Page, type Route } from "@playwright/test"
  2. import { base64Encode } from "@opencode-ai/core/util/encode"
  3. import { currentSession } from "../utils/mock-server"
  4. const serverA = "http://127.0.0.1:4096"
  5. const serverB = "http://127.0.0.1:4097"
  6. const sessionA = session("ses_server_a", "C:/server-a", "Server A session")
  7. const sessionB = session("ses_server_b", "/home/server-b", "Server B session")
  8. test("tab busy indicator reflects the tab server's own session status", async ({ page }) => {
  9. await mockServers(page)
  10. await page.addInitScript(
  11. ({ serverA, serverB, sessionA, sessionB }) => {
  12. localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
  13. localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] }))
  14. localStorage.setItem(
  15. "opencode.window.browser.dat:tabs",
  16. JSON.stringify([
  17. { type: "session", server: serverA, sessionId: sessionA },
  18. { type: "session", server: serverB, sessionId: sessionB },
  19. ]),
  20. )
  21. },
  22. { serverA, serverB, sessionA: sessionA.id, sessionB: sessionB.id },
  23. )
  24. const hrefA = `/server/${base64Encode(serverA)}/session/${sessionA.id}`
  25. const hrefB = `/server/${base64Encode(serverB)}/session/${sessionB.id}`
  26. await page.goto(hrefA)
  27. await expect(page.getByText(sessionA.title).first()).toBeVisible()
  28. // Session B is busy on server B while server A stays the active server, so the
  29. // busy indicator must come from the tab server's status, not the active server's.
  30. const tabB = page.locator(`[data-titlebar-tab-slot]:has(a[href="${hrefB}"])`)
  31. await expect(tabB.locator('[data-component="session-progress-indicator-v2"]')).toBeVisible()
  32. const tabA = page.locator(`[data-titlebar-tab-slot]:has(a[href="${hrefA}"])`)
  33. await expect(tabA.locator("[data-titlebar-tab-title]")).toHaveText(sessionA.title)
  34. await expect(tabA.locator('[data-component="session-progress-indicator-v2"]')).toHaveCount(0)
  35. })
  36. function session(id: string, directory: string, title: string) {
  37. return {
  38. id,
  39. slug: id,
  40. projectID: `project-${id}`,
  41. directory,
  42. title,
  43. version: "dev",
  44. time: { created: 1, updated: 1 },
  45. }
  46. }
  47. async function mockServers(page: Page) {
  48. await page.route("**/*", async (route) => {
  49. const url = new URL(route.request().url())
  50. if (url.origin !== serverA && url.origin !== serverB) return route.fallback()
  51. const current = url.origin === serverA ? sessionA : sessionB
  52. const directory = url.searchParams.get("directory")
  53. if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500)
  54. if (url.pathname === "/global/event" || url.pathname === "/event" || url.pathname === "/api/event")
  55. return sse(route, url.pathname === "/api/event")
  56. if (url.pathname === "/global/health") return json(route, {}, 404)
  57. if (url.pathname === "/api/health") return json(route, { pid: 1 })
  58. if (url.pathname === "/api/session/active")
  59. return json(route, { data: url.origin === serverB ? { [sessionB.id]: { type: "running" } } : {} })
  60. if (url.pathname === "/api/session") return json(route, { data: [currentSession(current)], cursor: {} })
  61. if (url.pathname === `/api/session/${current.id}`) return json(route, { data: currentSession(current) })
  62. if (url.pathname === `/api/session/${current.id}/message`) return json(route, { data: [], cursor: {} })
  63. if (url.pathname === `/session/${current.id}`) return json(route, current)
  64. if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404)
  65. if (url.pathname === `/session/${current.id}/message`) return json(route, [])
  66. if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(url.pathname)) return json(route, [])
  67. if (["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/diff"].includes(url.pathname))
  68. return json(route, [])
  69. if (["/global/config", "/config", "/provider/auth", "/mcp"].includes(url.pathname)) return json(route, {})
  70. if (url.pathname === "/provider")
  71. return json(route, { all: [], connected: [], default: { providerID: "", modelID: "" } })
  72. if (url.pathname === "/agent") return json(route, [{ name: "build", mode: "primary" }])
  73. if (url.pathname === "/project" || url.pathname === "/project/current") {
  74. const project = {
  75. id: current.projectID,
  76. worktree: current.directory,
  77. vcs: "git",
  78. time: { created: 1, updated: 1 },
  79. sandboxes: [],
  80. }
  81. return json(route, url.pathname === "/project" ? [project] : project)
  82. }
  83. if (url.pathname === "/path")
  84. return json(route, {
  85. state: current.directory,
  86. config: current.directory,
  87. worktree: current.directory,
  88. directory: current.directory,
  89. home: current.directory,
  90. })
  91. if (url.pathname === "/api/path")
  92. return json(route, {
  93. state: current.directory,
  94. config: current.directory,
  95. worktree: current.directory,
  96. directory: current.directory,
  97. home: current.directory,
  98. })
  99. if (url.pathname === "/vcs") return json(route, { branch: "main", default_branch: "main" })
  100. if (url.pathname === "/api/vcs")
  101. return json(route, {
  102. location: { directory: current.directory },
  103. data: { branch: "main", defaultBranch: "main" },
  104. })
  105. return json(route, {})
  106. })
  107. }
  108. function json(route: Route, body: unknown, status = 200) {
  109. return route.fulfill({
  110. status,
  111. contentType: "application/json",
  112. headers: { "access-control-allow-origin": "*" },
  113. body: JSON.stringify(body),
  114. })
  115. }
  116. function sse(route: Route, current: boolean) {
  117. return route.fulfill({
  118. status: 200,
  119. contentType: "text/event-stream",
  120. body: current ? 'data: {"id":"evt_connected","type":"server.connected","data":{}}\n\n' : ": ok\n\n",
  121. })
  122. }