fixtures.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. import { test as base, expect, type Page } from "@playwright/test"
  2. import type { E2EWindow } from "../src/testing/terminal"
  3. import {
  4. healthPhase,
  5. cleanupSession,
  6. cleanupTestProject,
  7. createTestProject,
  8. setHealthPhase,
  9. seedProjects,
  10. sessionIDFromUrl,
  11. waitSlug,
  12. waitSession,
  13. } from "./actions"
  14. import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils"
  15. export const settingsKey = "settings.v3"
  16. const seedModel = (() => {
  17. const [providerID = "opencode", modelID = "big-pickle"] = (
  18. process.env.OPENCODE_E2E_MODEL ?? "opencode/big-pickle"
  19. ).split("/")
  20. return {
  21. providerID: providerID || "opencode",
  22. modelID: modelID || "big-pickle",
  23. }
  24. })()
  25. type TestFixtures = {
  26. sdk: ReturnType<typeof createSdk>
  27. gotoSession: (sessionID?: string) => Promise<void>
  28. withProject: <T>(
  29. callback: (project: {
  30. directory: string
  31. slug: string
  32. gotoSession: (sessionID?: string) => Promise<void>
  33. trackSession: (sessionID: string, directory?: string) => void
  34. trackDirectory: (directory: string) => void
  35. }) => Promise<T>,
  36. options?: { extra?: string[] },
  37. ) => Promise<T>
  38. }
  39. type WorkerFixtures = {
  40. directory: string
  41. slug: string
  42. }
  43. export const test = base.extend<TestFixtures, WorkerFixtures>({
  44. page: async ({ page }, use) => {
  45. let boundary: string | undefined
  46. setHealthPhase(page, "test")
  47. const consoleHandler = (msg: { text(): string }) => {
  48. const text = msg.text()
  49. if (!text.includes("[e2e:error-boundary]")) return
  50. if (healthPhase(page) === "cleanup") {
  51. console.warn(`[e2e:error-boundary][cleanup-warning]\n${text}`)
  52. return
  53. }
  54. boundary ||= text
  55. console.log(text)
  56. }
  57. const pageErrorHandler = (err: Error) => {
  58. console.log(`[e2e:pageerror] ${err.stack || err.message}`)
  59. }
  60. page.on("console", consoleHandler)
  61. page.on("pageerror", pageErrorHandler)
  62. await use(page)
  63. page.off("console", consoleHandler)
  64. page.off("pageerror", pageErrorHandler)
  65. if (boundary) throw new Error(boundary)
  66. },
  67. directory: [
  68. async ({}, use) => {
  69. const directory = await getWorktree()
  70. await use(directory)
  71. },
  72. { scope: "worker" },
  73. ],
  74. slug: [
  75. async ({ directory }, use) => {
  76. await use(dirSlug(directory))
  77. },
  78. { scope: "worker" },
  79. ],
  80. sdk: async ({ directory }, use) => {
  81. await use(createSdk(directory))
  82. },
  83. gotoSession: async ({ page, directory }, use) => {
  84. await seedStorage(page, { directory })
  85. const gotoSession = async (sessionID?: string) => {
  86. await page.goto(sessionPath(directory, sessionID))
  87. await waitSession(page, { directory, sessionID })
  88. }
  89. await use(gotoSession)
  90. },
  91. withProject: async ({ page }, use) => {
  92. await use(async (callback, options) => {
  93. const root = await createTestProject()
  94. const sessions = new Map<string, string>()
  95. const dirs = new Set<string>()
  96. await seedStorage(page, { directory: root, extra: options?.extra })
  97. const gotoSession = async (sessionID?: string) => {
  98. await page.goto(sessionPath(root, sessionID))
  99. await waitSession(page, { directory: root, sessionID })
  100. const current = sessionIDFromUrl(page.url())
  101. if (current) trackSession(current)
  102. }
  103. const trackSession = (sessionID: string, directory?: string) => {
  104. sessions.set(sessionID, directory ?? root)
  105. }
  106. const trackDirectory = (directory: string) => {
  107. if (directory !== root) dirs.add(directory)
  108. }
  109. try {
  110. await gotoSession()
  111. const slug = await waitSlug(page)
  112. return await callback({ directory: root, slug, gotoSession, trackSession, trackDirectory })
  113. } finally {
  114. setHealthPhase(page, "cleanup")
  115. await Promise.allSettled(
  116. Array.from(sessions, ([sessionID, directory]) => cleanupSession({ sessionID, directory })),
  117. )
  118. await Promise.allSettled(Array.from(dirs, (directory) => cleanupTestProject(directory)))
  119. await cleanupTestProject(root)
  120. setHealthPhase(page, "test")
  121. }
  122. })
  123. },
  124. })
  125. async function seedStorage(page: Page, input: { directory: string; extra?: string[] }) {
  126. await seedProjects(page, input)
  127. await page.addInitScript((model: { providerID: string; modelID: string }) => {
  128. const win = window as E2EWindow
  129. win.__opencode_e2e = {
  130. ...win.__opencode_e2e,
  131. model: {
  132. enabled: true,
  133. },
  134. prompt: {
  135. enabled: true,
  136. },
  137. terminal: {
  138. enabled: true,
  139. terminals: {},
  140. },
  141. }
  142. localStorage.setItem(
  143. "opencode.global.dat:model",
  144. JSON.stringify({
  145. recent: [model],
  146. user: [],
  147. variant: {},
  148. }),
  149. )
  150. }, seedModel)
  151. }
  152. export { expect }