review-image-flash.spec.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import { expect, test, type Page } from "@playwright/test"
  2. import { base64Encode } from "@opencode-ai/core/util/encode"
  3. import { mockOpenCodeServer } from "../utils/mock-server"
  4. import { expectAppVisible, expectSessionTitle } from "../utils/waits"
  5. const directory = "C:/OpenCode/ReviewImageFlashRegression"
  6. const sessionID = "ses_review_image_flash_regression"
  7. const title = "Review image flash regression"
  8. const imageFile = "assets/preview.png"
  9. test("clicking an image file in the v2 review pane does not blank the panel", async ({ page }) => {
  10. await openReview(page)
  11. await installReviewFlashProbe(page)
  12. await page.getByRole("button", { name: /preview\.png/ }).click()
  13. await waitForReviewFlashProbe(page, 400)
  14. const trace = await collectReviewFlashProbe(page)
  15. const bad = trace.samples.filter((sample) => sample.blank || sample.blackCenter)
  16. expect(trace.samples.length).toBeGreaterThan(0)
  17. expect(
  18. bad,
  19. JSON.stringify({ bad: bad.slice(0, 8), first: trace.samples.slice(0, 8), last: trace.samples.slice(-4) }, null, 2),
  20. ).toEqual([])
  21. })
  22. async function openReview(page: Page) {
  23. await page.setViewportSize({ width: 960, height: 900 })
  24. await page.addInitScript(() => {
  25. localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
  26. })
  27. await mockOpenCodeServer(page, {
  28. directory,
  29. project: {
  30. id: "proj_review_image_flash_regression",
  31. worktree: directory,
  32. vcs: "git",
  33. name: "review-image-flash-regression",
  34. time: { created: 1700000000000, updated: 1700000000000 },
  35. sandboxes: [],
  36. },
  37. provider: { all: [], connected: [], default: {} },
  38. sessions: [
  39. {
  40. id: sessionID,
  41. slug: "review-image-flash-regression",
  42. projectID: "proj_review_image_flash_regression",
  43. directory,
  44. title,
  45. version: "dev",
  46. time: { created: 1700000000000, updated: 1700000000000 },
  47. },
  48. ],
  49. vcsDiff: [
  50. {
  51. file: "src/example.ts",
  52. additions: 1,
  53. deletions: 1,
  54. status: "modified",
  55. patch:
  56. "diff --git a/src/example.ts b/src/example.ts\n--- a/src/example.ts\n+++ b/src/example.ts\n@@ -1 +1 @@\n-export const value = 'before'\n+export const value = 'after'\n",
  57. },
  58. {
  59. file: imageFile,
  60. patch: "",
  61. additions: 1,
  62. deletions: 0,
  63. status: "added",
  64. },
  65. ],
  66. fileContent: async (path) => {
  67. if (path !== imageFile) return undefined
  68. await new Promise((resolve) => setTimeout(resolve, 250))
  69. return {
  70. type: "binary",
  71. content: "iVBORw0KGgo=",
  72. encoding: "base64",
  73. mimeType: "image/png",
  74. }
  75. },
  76. fileList: (path) => {
  77. if (!path) {
  78. return [
  79. { name: "assets", path: "assets", absolute: `${directory}/assets`, type: "directory", ignored: false },
  80. { name: "src", path: "src", absolute: `${directory}/src`, type: "directory", ignored: false },
  81. ]
  82. }
  83. if (path === "assets") {
  84. return [
  85. {
  86. name: "preview.png",
  87. path: imageFile,
  88. absolute: `${directory}/${imageFile}`,
  89. type: "file",
  90. ignored: false,
  91. },
  92. ]
  93. }
  94. if (path === "src") {
  95. return [
  96. {
  97. name: "example.ts",
  98. path: "src/example.ts",
  99. absolute: `${directory}/src/example.ts`,
  100. type: "file",
  101. ignored: false,
  102. },
  103. ]
  104. }
  105. return []
  106. },
  107. pageMessages: () => ({
  108. items: [
  109. {
  110. info: {
  111. id: "msg_review_image_flash_regression",
  112. sessionID,
  113. role: "user",
  114. time: { created: 1700000000000 },
  115. summary: { diffs: [] },
  116. agent: "build",
  117. model: { providerID: "opencode", modelID: "test" },
  118. },
  119. parts: [
  120. {
  121. id: "prt_review_image_flash_regression",
  122. sessionID,
  123. messageID: "msg_review_image_flash_regression",
  124. type: "text",
  125. text: "Review this change.",
  126. },
  127. ],
  128. },
  129. ],
  130. }),
  131. })
  132. await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
  133. await expectSessionTitle(page, title)
  134. await page.getByRole("button", { name: "Toggle review" }).click()
  135. await expectAppVisible(page.locator('#review-panel [data-component="session-review-v2"]'))
  136. await expectAppVisible(page.getByRole("button", { name: /preview\.png/ }))
  137. }
  138. async function installReviewFlashProbe(page: Page) {
  139. await page.evaluate(() => {
  140. const samples: Array<{
  141. observedAtMs: number
  142. blank: boolean
  143. blackCenter: boolean
  144. text: string
  145. background: string
  146. }> = []
  147. const startedAt = performance.now()
  148. const sample = () => {
  149. const panel = document.querySelector<HTMLElement>('#review-panel [data-component="session-review-v2"]')
  150. const rect = panel?.getBoundingClientRect()
  151. const center = rect
  152. ? document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2)
  153. : undefined
  154. const background = center instanceof Element ? getComputedStyle(center).backgroundColor : ""
  155. samples.push({
  156. observedAtMs: performance.now() - startedAt,
  157. blank: !panel || panel.textContent?.trim().length === 0,
  158. blackCenter: background === "rgb(0, 0, 0)",
  159. text: panel?.textContent?.trim().slice(0, 80) ?? "",
  160. background,
  161. })
  162. if (performance.now() - startedAt < 500) requestAnimationFrame(sample)
  163. }
  164. document.addEventListener(
  165. "click",
  166. (event) => {
  167. const target = event.target instanceof Element ? event.target : undefined
  168. if (!target?.closest('[data-slot="file-tree-v2-row"]')) return
  169. requestAnimationFrame(sample)
  170. },
  171. { capture: true, once: true },
  172. )
  173. ;(window as Window & { __reviewImageFlash?: { samples: typeof samples; startedAt: number } }).__reviewImageFlash = {
  174. samples,
  175. startedAt,
  176. }
  177. })
  178. }
  179. async function waitForReviewFlashProbe(page: Page, durationMs: number) {
  180. await page.waitForFunction((durationMs) => {
  181. const state = (window as Window & { __reviewImageFlash?: { samples: unknown[]; startedAt: number } })
  182. .__reviewImageFlash
  183. return !!state && state.samples.length > 0 && performance.now() - state.startedAt >= durationMs
  184. }, durationMs)
  185. }
  186. async function collectReviewFlashProbe(page: Page) {
  187. return page.evaluate(() => {
  188. return (window as Window & { __reviewImageFlash?: { samples: unknown[]; startedAt: number } }).__reviewImageFlash!
  189. }) as Promise<{
  190. startedAt: number
  191. samples: Array<{ observedAtMs: number; blank: boolean; blackCenter: boolean; text: string; background: string }>
  192. }>
  193. }