summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-01-17 20:52:21 -0600
committerAdam <[email protected]>2026-01-19 09:03:52 -0600
commit03d7467ea268f2f0f8d99f48ea1522741014b4bf (patch)
tree5df719dfef2b7d67fad86405a99cd2375174c41b /packages
parent23e9c02a7fd80063dd49e3b9cbd2a0c6519034bc (diff)
downloadopencode-03d7467ea268f2f0f8d99f48ea1522741014b4bf.tar.gz
opencode-03d7467ea268f2f0f8d99f48ea1522741014b4bf.zip
test(app): initial e2e test setup
Diffstat (limited to 'packages')
-rw-r--r--packages/app/.gitignore2
-rw-r--r--packages/app/README.md15
-rw-r--r--packages/app/e2e/home.spec.ts6
-rw-r--r--packages/app/package.json7
-rw-r--r--packages/app/playwright.config.ts43
-rw-r--r--packages/opencode/script/seed-e2e.ts50
-rw-r--r--packages/opencode/src/share/share-next.ts6
-rw-r--r--packages/opencode/src/share/share.ts5
8 files changed, 133 insertions, 1 deletions
diff --git a/packages/app/.gitignore b/packages/app/.gitignore
index 4a20d55a7..d699efb38 100644
--- a/packages/app/.gitignore
+++ b/packages/app/.gitignore
@@ -1 +1,3 @@
src/assets/theme.css
+e2e/test-results
+e2e/playwright-report
diff --git a/packages/app/README.md b/packages/app/README.md
index bd10e6c8d..42a688150 100644
--- a/packages/app/README.md
+++ b/packages/app/README.md
@@ -29,6 +29,21 @@ It correctly bundles Solid in production mode and optimizes the build for the be
The build is minified and the filenames include the hashes.<br>
Your app is ready to be deployed!
+## E2E Testing
+
+The Playwright runner expects the app already running at `http://localhost:3000`.
+
+```bash
+bun add -D @playwright/test
+bunx playwright install
+bun run test:e2e
+```
+
+Environment options:
+
+- `PLAYWRIGHT_BASE_URL` (default: `http://localhost:3000`)
+- `PLAYWRIGHT_PORT` (default: `3000`)
+
## Deployment
You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.)
diff --git a/packages/app/e2e/home.spec.ts b/packages/app/e2e/home.spec.ts
new file mode 100644
index 000000000..ff57923d5
--- /dev/null
+++ b/packages/app/e2e/home.spec.ts
@@ -0,0 +1,6 @@
+import { test, expect } from "@playwright/test"
+
+test("home shows recent projects header", async ({ page }) => {
+ await page.goto("/")
+ await expect(page.getByText("Recent projects")).toBeVisible()
+})
diff --git a/packages/app/package.json b/packages/app/package.json
index 38d9a25f5..2a754c967 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -12,11 +12,16 @@
"start": "vite",
"dev": "vite",
"build": "vite build",
- "serve": "vite preview"
+ "serve": "vite preview",
+ "test": "playwright test",
+ "test:e2e": "playwright test",
+ "test:e2e:ui": "playwright test --ui",
+ "test:e2e:report": "playwright show-report e2e/playwright-report"
},
"license": "MIT",
"devDependencies": {
"@happy-dom/global-registrator": "20.0.11",
+ "@playwright/test": "1.57.0",
"@tailwindcss/vite": "catalog:",
"@tsconfig/bun": "1.0.9",
"@types/bun": "catalog:",
diff --git a/packages/app/playwright.config.ts b/packages/app/playwright.config.ts
new file mode 100644
index 000000000..10819e69f
--- /dev/null
+++ b/packages/app/playwright.config.ts
@@ -0,0 +1,43 @@
+import { defineConfig, devices } from "@playwright/test"
+
+const port = Number(process.env.PLAYWRIGHT_PORT ?? 3000)
+const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? `http://localhost:${port}`
+const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "localhost"
+const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
+const command = `bun run dev -- --host 0.0.0.0 --port ${port}`
+const reuse = !process.env.CI
+
+export default defineConfig({
+ testDir: "./e2e",
+ outputDir: "./e2e/test-results",
+ timeout: 60_000,
+ expect: {
+ timeout: 10_000,
+ },
+ fullyParallel: true,
+ forbidOnly: !!process.env.CI,
+ retries: process.env.CI ? 2 : 0,
+ reporter: [["html", { outputFolder: "e2e/playwright-report", open: "never" }], ["line"]],
+ webServer: {
+ command,
+ url: baseURL,
+ reuseExistingServer: reuse,
+ timeout: 120_000,
+ env: {
+ VITE_OPENCODE_SERVER_HOST: serverHost,
+ VITE_OPENCODE_SERVER_PORT: serverPort,
+ },
+ },
+ use: {
+ baseURL,
+ trace: "on-first-retry",
+ screenshot: "only-on-failure",
+ video: "retain-on-failure",
+ },
+ projects: [
+ {
+ name: "chromium",
+ use: { ...devices["Desktop Chrome"] },
+ },
+ ],
+})
diff --git a/packages/opencode/script/seed-e2e.ts b/packages/opencode/script/seed-e2e.ts
new file mode 100644
index 000000000..ba2155cb6
--- /dev/null
+++ b/packages/opencode/script/seed-e2e.ts
@@ -0,0 +1,50 @@
+const dir = process.env.OPENCODE_E2E_PROJECT_DIR ?? process.cwd()
+const title = process.env.OPENCODE_E2E_SESSION_TITLE ?? "E2E Session"
+const text = process.env.OPENCODE_E2E_MESSAGE ?? "Seeded for UI e2e"
+const model = process.env.OPENCODE_E2E_MODEL ?? "opencode/gpt-5-nano"
+const parts = model.split("/")
+const providerID = parts[0] ?? "opencode"
+const modelID = parts[1] ?? "gpt-5-nano"
+const now = Date.now()
+
+const seed = async () => {
+ const { Instance } = await import("../src/project/instance")
+ const { InstanceBootstrap } = await import("../src/project/bootstrap")
+ const { Session } = await import("../src/session")
+ const { Identifier } = await import("../src/id/id")
+ const { Project } = await import("../src/project/project")
+
+ await Instance.provide({
+ directory: dir,
+ init: InstanceBootstrap,
+ fn: async () => {
+ const session = await Session.create({ title })
+ const messageID = Identifier.descending("message")
+ const partID = Identifier.descending("part")
+ const message = {
+ id: messageID,
+ sessionID: session.id,
+ role: "user" as const,
+ time: { created: now },
+ agent: "build",
+ model: {
+ providerID,
+ modelID,
+ },
+ }
+ const part = {
+ id: partID,
+ sessionID: session.id,
+ messageID,
+ type: "text" as const,
+ text,
+ time: { start: now },
+ }
+ await Session.updateMessage(message)
+ await Session.updatePart(part)
+ await Project.update({ projectID: Instance.project.id, name: "E2E Project" })
+ },
+ })
+}
+
+await seed()
diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts
index 95271f8c8..dddce95cb 100644
--- a/packages/opencode/src/share/share-next.ts
+++ b/packages/opencode/src/share/share-next.ts
@@ -15,7 +15,10 @@ export namespace ShareNext {
return Config.get().then((x) => x.enterprise?.url ?? "https://opncd.ai")
}
+ const disabled = process.env["OPENCODE_DISABLE_SHARE"] === "true" || process.env["OPENCODE_DISABLE_SHARE"] === "1"
+
export async function init() {
+ if (disabled) return
Bus.subscribe(Session.Event.Updated, async (evt) => {
await sync(evt.properties.info.id, [
{
@@ -63,6 +66,7 @@ export namespace ShareNext {
}
export async function create(sessionID: string) {
+ if (disabled) return { id: "", url: "", secret: "" }
log.info("creating share", { sessionID })
const result = await fetch(`${await url()}/api/share`, {
method: "POST",
@@ -110,6 +114,7 @@ export namespace ShareNext {
const queue = new Map<string, { timeout: NodeJS.Timeout; data: Map<string, Data> }>()
async function sync(sessionID: string, data: Data[]) {
+ if (disabled) return
const existing = queue.get(sessionID)
if (existing) {
for (const item of data) {
@@ -145,6 +150,7 @@ export namespace ShareNext {
}
export async function remove(sessionID: string) {
+ if (disabled) return
log.info("removing share", { sessionID })
const share = await get(sessionID)
if (!share) return
diff --git a/packages/opencode/src/share/share.ts b/packages/opencode/src/share/share.ts
index 1006b23d5..f7bf4b3fa 100644
--- a/packages/opencode/src/share/share.ts
+++ b/packages/opencode/src/share/share.ts
@@ -11,6 +11,7 @@ export namespace Share {
const pending = new Map<string, any>()
export async function sync(key: string, content: any) {
+ if (disabled) return
const [root, ...splits] = key.split("/")
if (root !== "session") return
const [sub, sessionID] = splits
@@ -69,7 +70,10 @@ export namespace Share {
process.env["OPENCODE_API"] ??
(Installation.isPreview() || Installation.isLocal() ? "https://api.dev.opencode.ai" : "https://api.opencode.ai")
+ const disabled = process.env["OPENCODE_DISABLE_SHARE"] === "true" || process.env["OPENCODE_DISABLE_SHARE"] === "1"
+
export async function create(sessionID: string) {
+ if (disabled) return { url: "", secret: "" }
return fetch(`${URL}/share_create`, {
method: "POST",
body: JSON.stringify({ sessionID: sessionID }),
@@ -79,6 +83,7 @@ export namespace Share {
}
export async function remove(sessionID: string, secret: string) {
+ if (disabled) return {}
return fetch(`${URL}/share_delete`, {
method: "POST",
body: JSON.stringify({ sessionID, secret }),