file-browser-sidebar-tab-switch.spec.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import { base64Encode } from "@opencode-ai/core/util/encode"
  2. import { expect, test, type Page } from "@playwright/test"
  3. import { mockOpenCodeServer } from "../utils/mock-server"
  4. import { expectSessionTitle } from "../utils/waits"
  5. const directory = "C:/OpenCode/FileBrowserSidebar"
  6. const projectID = "proj_file_browser_sidebar"
  7. const sessionID = "ses_file_browser_sidebar"
  8. const title = "File browser sidebar"
  9. const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
  10. const files = Array.from({ length: 80 }, (_, index) => `file-${String(index).padStart(2, "0")}.ts`)
  11. // Marks the file-browser sidebar DOM node so a remount (fresh node) is detectable.
  12. const PROBE = "original"
  13. test.use({ viewport: { width: 1440, height: 900 } })
  14. // The file-browser sidebar must stay mounted across preview/pinned file-tab
  15. // switches. Remounting resets scroll and filter state.
  16. test("keeps the file-browser sidebar mounted when switching file tabs", async ({ page }) => {
  17. await setup(page)
  18. await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
  19. await expectSessionTitle(page, title)
  20. const panel = page.locator("#review-panel")
  21. await panel.getByRole("button", { name: "Open file" }).click()
  22. await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "")
  23. const sidebar = panel.locator('[data-component="session-review-v2-sidebar-root"]')
  24. await expect(sidebar).toBeVisible()
  25. await expect(panel.getByRole("button", { name: "file-00.ts" })).toBeVisible()
  26. await panel.getByRole("button", { name: "file-00.ts" }).click()
  27. await expect(panel.getByRole("tab", { name: "file-00.ts" })).toHaveAttribute("data-selected", "")
  28. await expect(panel.getByText("contents:file-00.ts", { exact: true })).toBeVisible()
  29. const viewport = panel.locator('[data-slot="session-review-v2-sidebar-tree"] .scroll-view__viewport')
  30. await viewport.hover()
  31. await page.mouse.wheel(0, 100_000)
  32. await expect
  33. .poll(() => viewport.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop))
  34. .toBeLessThanOrEqual(1)
  35. const scrolled = await viewport.evaluate((element) => element.scrollTop)
  36. expect(scrolled).toBeGreaterThan(0)
  37. await writeProbe(page)
  38. await panel.getByRole("button", { name: "file-79.ts" }).click()
  39. await expect(panel.getByRole("tab", { name: "file-79.ts" })).toHaveAttribute("data-selected", "")
  40. await expect(panel.getByText("contents:file-79.ts", { exact: true })).toBeVisible()
  41. expect(await readProbe(page)).toBe(PROBE)
  42. await expect.poll(() => viewport.evaluate((element) => element.scrollTop)).toBe(scrolled)
  43. await panel.getByRole("button", { name: "file-78.ts" }).dblclick()
  44. await expect(panel.getByRole("tab", { name: "file-78.ts" })).toHaveAttribute("data-selected", "")
  45. await panel.getByRole("button", { name: "file-79.ts" }).click()
  46. await expect(panel.getByRole("tab", { name: "file-79.ts" })).toHaveAttribute("data-selected", "")
  47. await panel.getByRole("tab", { name: "file-78.ts" }).click()
  48. await expect(panel.getByRole("tab", { name: "file-78.ts" })).toHaveAttribute("data-selected", "")
  49. expect(await readProbe(page)).toBe(PROBE)
  50. await expect.poll(() => viewport.evaluate((element) => element.scrollTop)).toBe(scrolled)
  51. })
  52. type Probed = HTMLElement & { __e2eProbe?: string }
  53. async function writeProbe(page: Page) {
  54. await page.locator('#review-panel [data-component="session-review-v2-sidebar-root"]').evaluate((el, probe) => {
  55. ;(el as Probed).__e2eProbe = probe
  56. }, PROBE)
  57. }
  58. async function readProbe(page: Page) {
  59. return page
  60. .locator('#review-panel [data-component="session-review-v2-sidebar-root"]')
  61. .evaluate((el) => (el as Probed).__e2eProbe)
  62. }
  63. async function setup(page: Page) {
  64. await mockOpenCodeServer(page, {
  65. directory,
  66. project: {
  67. id: projectID,
  68. worktree: directory,
  69. vcs: "git",
  70. name: "file-browser-sidebar",
  71. time: { created: 1700000000000, updated: 1700000000000 },
  72. sandboxes: [],
  73. },
  74. provider: {
  75. all: [
  76. {
  77. id: "opencode",
  78. name: "OpenCode",
  79. models: { test: { id: "test", name: "Test", limit: { context: 200_000 } } },
  80. },
  81. ],
  82. connected: ["opencode"],
  83. default: { providerID: "opencode", modelID: "test" },
  84. },
  85. sessions: [
  86. {
  87. id: sessionID,
  88. slug: sessionID,
  89. projectID,
  90. directory,
  91. title,
  92. version: "dev",
  93. time: { created: 1700000000000, updated: 1700000000000 },
  94. },
  95. ],
  96. vcsDiff: [],
  97. fileList: (path) => {
  98. if (path) return []
  99. return files.map((name) => ({
  100. name,
  101. path: name,
  102. absolute: `${directory}/${name}`,
  103. type: "file" as const,
  104. ignored: false,
  105. }))
  106. },
  107. fileContent: (path) => ({ type: "text", content: `contents:${path}` }),
  108. pageMessages: () => ({ items: [] }),
  109. })
  110. await page.addInitScript(
  111. ({ directory, server, sessionID }) => {
  112. localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
  113. localStorage.setItem(
  114. "opencode.global.dat:server",
  115. JSON.stringify({
  116. projects: { local: [{ worktree: directory, expanded: true }] },
  117. lastProject: { local: directory },
  118. }),
  119. )
  120. localStorage.setItem(
  121. "opencode.global.dat:layout",
  122. JSON.stringify({ review: { diffStyle: "split", panelOpened: true } }),
  123. )
  124. localStorage.setItem(
  125. "opencode.global.dat:review-panel-v2",
  126. JSON.stringify({ sidebarOpened: true, sidebarWidth: 240, expandMode: "collapse" }),
  127. )
  128. localStorage.setItem(
  129. "opencode.window.browser.dat:tabs",
  130. JSON.stringify([{ type: "session", server, sessionId: sessionID }]),
  131. )
  132. },
  133. { directory, server, sessionID },
  134. )
  135. }