tab-navigate-mousedown.spec.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. import { expect, test, type Page, type Route } from "@playwright/test"
  2. import { base64Encode } from "@opencode-ai/core/util/encode"
  3. const server = "http://127.0.0.1:4096"
  4. const sessionA = session("ses_tab_a", "Tab A session")
  5. const sessionB = session("ses_tab_b", "Tab B session")
  6. test("pressing mouse down on a tab navigates before mouse up", async ({ page }) => {
  7. await mockServer(page)
  8. await page.addInitScript(
  9. ({ server, sessionA, sessionB }) => {
  10. localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
  11. localStorage.setItem(
  12. "opencode.window.browser.dat:tabs",
  13. JSON.stringify([
  14. { type: "session", server, sessionId: sessionA },
  15. { type: "session", server, sessionId: sessionB },
  16. ]),
  17. )
  18. },
  19. { server, sessionA: sessionA.id, sessionB: sessionB.id },
  20. )
  21. const hrefA = `/server/${base64Encode(server)}/session/${sessionA.id}`
  22. const hrefB = `/server/${base64Encode(server)}/session/${sessionB.id}`
  23. await page.goto(hrefA)
  24. await expect(page.getByText(sessionA.title).first()).toBeVisible()
  25. const linkB = page.locator(`a[data-titlebar-tab-link][href="${hrefB}"]`)
  26. await expect(linkB).toBeVisible()
  27. const box = await linkB.boundingBox()
  28. if (!box) throw new Error("tab link has no bounding box")
  29. await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2)
  30. await page.mouse.down()
  31. // Navigation must happen on mousedown, before the button is released.
  32. await expect(page).toHaveURL(new RegExp(`${hrefB.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`))
  33. await page.mouse.up()
  34. await expect(page).toHaveURL(new RegExp(`${hrefB.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`))
  35. })
  36. function session(id: string, title: string) {
  37. return {
  38. id,
  39. slug: id,
  40. projectID: "project-tabs",
  41. directory: "C:/tab-project",
  42. title,
  43. version: "dev",
  44. time: { created: 1, updated: 1 },
  45. }
  46. }
  47. async function mockServer(page: Page) {
  48. const sessions = [sessionA, sessionB]
  49. await page.route("**/*", async (route) => {
  50. const url = new URL(route.request().url())
  51. if (url.origin !== server) return route.fallback()
  52. if (url.pathname === "/global/event" || url.pathname === "/event") return sse(route)
  53. if (url.pathname === "/global/health") return json(route, { healthy: true })
  54. if (url.pathname === "/session") return json(route, sessions)
  55. const byId = sessions.find((item) => url.pathname === `/session/${item.id}`)
  56. if (byId) return json(route, byId)
  57. if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404)
  58. if (/^\/session\/[^/]+\/message$/.test(url.pathname)) return json(route, [])
  59. if (/^\/session\/[^/]+\/(children|diff)$/.test(url.pathname)) return json(route, [])
  60. if (["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/diff"].includes(url.pathname))
  61. return json(route, [])
  62. if (["/global/config", "/config", "/provider/auth", "/mcp", "/session/status"].includes(url.pathname))
  63. return json(route, {})
  64. if (url.pathname === "/provider")
  65. return json(route, { all: [], connected: [], default: { providerID: "", modelID: "" } })
  66. if (url.pathname === "/agent") return json(route, [{ name: "build", mode: "primary" }])
  67. if (url.pathname === "/project" || url.pathname === "/project/current") {
  68. const project = {
  69. id: sessionA.projectID,
  70. worktree: sessionA.directory,
  71. vcs: "git",
  72. time: { created: 1, updated: 1 },
  73. sandboxes: [],
  74. }
  75. return json(route, url.pathname === "/project" ? [project] : project)
  76. }
  77. if (url.pathname === "/path")
  78. return json(route, {
  79. state: sessionA.directory,
  80. config: sessionA.directory,
  81. worktree: sessionA.directory,
  82. directory: sessionA.directory,
  83. home: sessionA.directory,
  84. })
  85. if (url.pathname === "/vcs") return json(route, { branch: "main", default_branch: "main" })
  86. return json(route, {})
  87. })
  88. }
  89. function json(route: Route, body: unknown, status = 200) {
  90. return route.fulfill({
  91. status,
  92. contentType: "application/json",
  93. headers: { "access-control-allow-origin": "*" },
  94. body: JSON.stringify(body),
  95. })
  96. }
  97. function sse(route: Route) {
  98. return route.fulfill({ status: 200, contentType: "text/event-stream", body: ": ok\n\n" })
  99. }