Quellcode durchsuchen

test(app): fix e2e

Adam vor 4 Wochen
Ursprung
Commit
95e9407e63

+ 7 - 5
packages/app/README.md

@@ -31,18 +31,20 @@ Your app is ready to be deployed!
 
 ## E2E Testing
 
-The Playwright runner expects the app already running at `http://localhost:3000`.
+Playwright starts the Vite dev server automatically via `webServer`, and UI tests need an opencode backend (defaults to `localhost:4096`).
+Use the local runner to create a temp sandbox, seed data, and run the tests.
 
 ```bash
-bun add -D @playwright/test
 bunx playwright install
-bun run test:e2e
+bun run test:e2e:local
+bun run test:e2e:local -- --grep "settings"
 ```
 
 Environment options:
 
-- `PLAYWRIGHT_BASE_URL` (default: `http://localhost:3000`)
-- `PLAYWRIGHT_PORT` (default: `3000`)
+- `PLAYWRIGHT_SERVER_HOST` / `PLAYWRIGHT_SERVER_PORT` (backend address, default: `localhost:4096`)
+- `PLAYWRIGHT_PORT` (Vite dev server port, default: `3000`)
+- `PLAYWRIGHT_BASE_URL` (override base URL, default: `http://localhost:<PLAYWRIGHT_PORT>`)
 
 ## Deployment
 

+ 7 - 12
packages/app/e2e/prompt.spec.ts

@@ -37,24 +37,19 @@ test("can send a prompt and receive a reply", async ({ page, sdk, gotoSession })
       .poll(
         async () => {
           const messages = await sdk.session.messages({ sessionID, limit: 50 }).then((r) => r.data ?? [])
-          const assistant = messages
-            .slice()
-            .reverse()
-            .find((m) => m.info.role === "assistant")
-
-          return (
-            assistant?.parts
-              .filter((p) => p.type === "text")
-              .map((p) => p.text)
-              .join("\n") ?? ""
-          )
+          return messages
+            .filter((m) => m.info.role === "assistant")
+            .flatMap((m) => m.parts)
+            .filter((p) => p.type === "text")
+            .map((p) => p.text)
+            .join("\n")
         },
         { timeout: 90_000 },
       )
 
       .toContain(token)
 
-    const reply = page.locator('[data-component="text-part"]').filter({ hasText: token }).first()
+    const reply = page.locator('[data-slot="session-turn-summary-section"]').filter({ hasText: token }).first()
     await expect(reply).toBeVisible({ timeout: 90_000 })
   } finally {
     page.off("pageerror", onPageError)

+ 1 - 0
packages/app/package.json

@@ -15,6 +15,7 @@
     "serve": "vite preview",
     "test": "playwright test",
     "test:e2e": "playwright test",
+    "test:e2e:local": "bun script/e2e-local.ts",
     "test:e2e:ui": "playwright test --ui",
     "test:e2e:report": "playwright show-report e2e/playwright-report"
   },

+ 130 - 0
packages/app/script/e2e-local.ts

@@ -0,0 +1,130 @@
+import fs from "node:fs/promises"
+import net from "node:net"
+import os from "node:os"
+import path from "node:path"
+
+async function freePort() {
+  return await new Promise<number>((resolve, reject) => {
+    const server = net.createServer()
+    server.once("error", reject)
+    server.listen(0, () => {
+      const address = server.address()
+      if (!address || typeof address === "string") {
+        server.close(() => reject(new Error("Failed to acquire a free port")))
+        return
+      }
+      server.close((err) => {
+        if (err) {
+          reject(err)
+          return
+        }
+        resolve(address.port)
+      })
+    })
+  })
+}
+
+async function waitForHealth(url: string) {
+  const timeout = Date.now() + 60_000
+  while (Date.now() < timeout) {
+    const ok = await fetch(url)
+      .then((r) => r.ok)
+      .catch(() => false)
+    if (ok) return
+    await new Promise((r) => setTimeout(r, 250))
+  }
+  throw new Error(`Timed out waiting for server health: ${url}`)
+}
+
+const appDir = process.cwd()
+const repoDir = path.resolve(appDir, "../..")
+const opencodeDir = path.join(repoDir, "packages", "opencode")
+const modelsJson = path.join(opencodeDir, "test", "tool", "fixtures", "models-api.json")
+
+const extraArgs = (() => {
+  const args = process.argv.slice(2)
+  if (args[0] === "--") return args.slice(1)
+  return args
+})()
+
+const [serverPort, webPort] = await Promise.all([freePort(), freePort()])
+
+const sandbox = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-"))
+
+const serverEnv = {
+  ...process.env,
+  MODELS_DEV_API_JSON: modelsJson,
+  OPENCODE_DISABLE_MODELS_FETCH: "true",
+  OPENCODE_DISABLE_SHARE: "true",
+  OPENCODE_DISABLE_LSP_DOWNLOAD: "true",
+  OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
+  OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true",
+  OPENCODE_TEST_HOME: path.join(sandbox, "home"),
+  XDG_DATA_HOME: path.join(sandbox, "share"),
+  XDG_CACHE_HOME: path.join(sandbox, "cache"),
+  XDG_CONFIG_HOME: path.join(sandbox, "config"),
+  XDG_STATE_HOME: path.join(sandbox, "state"),
+  OPENCODE_E2E_PROJECT_DIR: repoDir,
+  OPENCODE_E2E_SESSION_TITLE: "E2E Session",
+  OPENCODE_E2E_MESSAGE: "Seeded for UI e2e",
+  OPENCODE_E2E_MODEL: "opencode/gpt-5-nano",
+  OPENCODE_CLIENT: "app",
+} satisfies Record<string, string>
+
+const runnerEnv = {
+  ...process.env,
+  PLAYWRIGHT_SERVER_HOST: "localhost",
+  PLAYWRIGHT_SERVER_PORT: String(serverPort),
+  VITE_OPENCODE_SERVER_HOST: "localhost",
+  VITE_OPENCODE_SERVER_PORT: String(serverPort),
+  PLAYWRIGHT_PORT: String(webPort),
+} satisfies Record<string, string>
+
+const seed = Bun.spawn(["bun", "script/seed-e2e.ts"], {
+  cwd: opencodeDir,
+  env: serverEnv,
+  stdout: "inherit",
+  stderr: "inherit",
+})
+
+const seedExit = await seed.exited
+if (seedExit !== 0) {
+  process.exit(seedExit)
+}
+
+const server = Bun.spawn(
+  [
+    "bun",
+    "dev",
+    "--",
+    "--print-logs",
+    "--log-level",
+    "WARN",
+    "serve",
+    "--port",
+    String(serverPort),
+    "--hostname",
+    "127.0.0.1",
+  ],
+  {
+    cwd: opencodeDir,
+    env: serverEnv,
+    stdout: "inherit",
+    stderr: "inherit",
+  },
+)
+
+try {
+  await waitForHealth(`http://localhost:${serverPort}/global/health`)
+
+  const runner = Bun.spawn(["bun", "test:e2e", ...extraArgs], {
+    cwd: appDir,
+    env: runnerEnv,
+    stdout: "inherit",
+    stderr: "inherit",
+  })
+
+  process.exitCode = await runner.exited
+} finally {
+  server.kill()
+}