test(app): fix e2e
parent
1466b43c5c
commit
95e9407e63
|
|
@ -31,18 +31,20 @@ Your app is ready to be deployed!
|
||||||
|
|
||||||
## E2E Testing
|
## 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
|
```bash
|
||||||
bun add -D @playwright/test
|
|
||||||
bunx playwright install
|
bunx playwright install
|
||||||
bun run test:e2e
|
bun run test:e2e:local
|
||||||
|
bun run test:e2e:local -- --grep "settings"
|
||||||
```
|
```
|
||||||
|
|
||||||
Environment options:
|
Environment options:
|
||||||
|
|
||||||
- `PLAYWRIGHT_BASE_URL` (default: `http://localhost:3000`)
|
- `PLAYWRIGHT_SERVER_HOST` / `PLAYWRIGHT_SERVER_PORT` (backend address, default: `localhost:4096`)
|
||||||
- `PLAYWRIGHT_PORT` (default: `3000`)
|
- `PLAYWRIGHT_PORT` (Vite dev server port, default: `3000`)
|
||||||
|
- `PLAYWRIGHT_BASE_URL` (override base URL, default: `http://localhost:<PLAYWRIGHT_PORT>`)
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,24 +37,19 @@ test("can send a prompt and receive a reply", async ({ page, sdk, gotoSession })
|
||||||
.poll(
|
.poll(
|
||||||
async () => {
|
async () => {
|
||||||
const messages = await sdk.session.messages({ sessionID, limit: 50 }).then((r) => r.data ?? [])
|
const messages = await sdk.session.messages({ sessionID, limit: 50 }).then((r) => r.data ?? [])
|
||||||
const assistant = messages
|
return messages
|
||||||
.slice()
|
.filter((m) => m.info.role === "assistant")
|
||||||
.reverse()
|
.flatMap((m) => m.parts)
|
||||||
.find((m) => m.info.role === "assistant")
|
.filter((p) => p.type === "text")
|
||||||
|
.map((p) => p.text)
|
||||||
return (
|
.join("\n")
|
||||||
assistant?.parts
|
|
||||||
.filter((p) => p.type === "text")
|
|
||||||
.map((p) => p.text)
|
|
||||||
.join("\n") ?? ""
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{ timeout: 90_000 },
|
{ timeout: 90_000 },
|
||||||
)
|
)
|
||||||
|
|
||||||
.toContain(token)
|
.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 })
|
await expect(reply).toBeVisible({ timeout: 90_000 })
|
||||||
} finally {
|
} finally {
|
||||||
page.off("pageerror", onPageError)
|
page.off("pageerror", onPageError)
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
"test": "playwright test",
|
"test": "playwright test",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:local": "bun script/e2e-local.ts",
|
||||||
"test:e2e:ui": "playwright test --ui",
|
"test:e2e:ui": "playwright test --ui",
|
||||||
"test:e2e:report": "playwright show-report e2e/playwright-report"
|
"test:e2e:report": "playwright show-report e2e/playwright-report"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue