|
|
@@ -0,0 +1,160 @@
|
|
|
+import { expect, type Locator, type Page } from "@playwright/test"
|
|
|
+import fs from "node:fs/promises"
|
|
|
+import os from "node:os"
|
|
|
+import path from "node:path"
|
|
|
+import { execSync } from "node:child_process"
|
|
|
+import { modKey, serverUrl } from "./utils"
|
|
|
+
|
|
|
+export async function defocus(page: Page) {
|
|
|
+ await page.mouse.click(5, 5)
|
|
|
+}
|
|
|
+
|
|
|
+export async function openPalette(page: Page) {
|
|
|
+ await defocus(page)
|
|
|
+ await page.keyboard.press(`${modKey}+P`)
|
|
|
+
|
|
|
+ const dialog = page.getByRole("dialog")
|
|
|
+ await expect(dialog).toBeVisible()
|
|
|
+ await expect(dialog.getByRole("textbox").first()).toBeVisible()
|
|
|
+ return dialog
|
|
|
+}
|
|
|
+
|
|
|
+export async function closeDialog(page: Page, dialog: Locator) {
|
|
|
+ await page.keyboard.press("Escape")
|
|
|
+ const closed = await dialog
|
|
|
+ .waitFor({ state: "detached", timeout: 1500 })
|
|
|
+ .then(() => true)
|
|
|
+ .catch(() => false)
|
|
|
+
|
|
|
+ if (closed) return
|
|
|
+
|
|
|
+ await page.keyboard.press("Escape")
|
|
|
+ const closedSecond = await dialog
|
|
|
+ .waitFor({ state: "detached", timeout: 1500 })
|
|
|
+ .then(() => true)
|
|
|
+ .catch(() => false)
|
|
|
+
|
|
|
+ if (closedSecond) return
|
|
|
+
|
|
|
+ await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } })
|
|
|
+ await expect(dialog).toHaveCount(0)
|
|
|
+}
|
|
|
+
|
|
|
+export async function isSidebarClosed(page: Page) {
|
|
|
+ const main = page.locator("main")
|
|
|
+ const classes = (await main.getAttribute("class")) ?? ""
|
|
|
+ return classes.includes("xl:border-l")
|
|
|
+}
|
|
|
+
|
|
|
+export async function toggleSidebar(page: Page) {
|
|
|
+ await defocus(page)
|
|
|
+ await page.keyboard.press(`${modKey}+B`)
|
|
|
+}
|
|
|
+
|
|
|
+export async function openSidebar(page: Page) {
|
|
|
+ if (!(await isSidebarClosed(page))) return
|
|
|
+ await toggleSidebar(page)
|
|
|
+ await expect(page.locator("main")).not.toHaveClass(/xl:border-l/)
|
|
|
+}
|
|
|
+
|
|
|
+export async function closeSidebar(page: Page) {
|
|
|
+ if (await isSidebarClosed(page)) return
|
|
|
+ await toggleSidebar(page)
|
|
|
+ await expect(page.locator("main")).toHaveClass(/xl:border-l/)
|
|
|
+}
|
|
|
+
|
|
|
+export async function openSettings(page: Page) {
|
|
|
+ await defocus(page)
|
|
|
+
|
|
|
+ const dialog = page.getByRole("dialog")
|
|
|
+ await page.keyboard.press(`${modKey}+Comma`).catch(() => undefined)
|
|
|
+
|
|
|
+ const opened = await dialog
|
|
|
+ .waitFor({ state: "visible", timeout: 3000 })
|
|
|
+ .then(() => true)
|
|
|
+ .catch(() => false)
|
|
|
+
|
|
|
+ if (opened) return dialog
|
|
|
+
|
|
|
+ await page.getByRole("button", { name: "Settings" }).first().click()
|
|
|
+ await expect(dialog).toBeVisible()
|
|
|
+ return dialog
|
|
|
+}
|
|
|
+
|
|
|
+export async function seedProjects(page: Page, input: { directory: string; extra?: string[] }) {
|
|
|
+ await page.addInitScript(
|
|
|
+ (args: { directory: string; serverUrl: string; extra: string[] }) => {
|
|
|
+ const key = "opencode.global.dat:server"
|
|
|
+ const raw = localStorage.getItem(key)
|
|
|
+ const parsed = (() => {
|
|
|
+ if (!raw) return undefined
|
|
|
+ try {
|
|
|
+ return JSON.parse(raw) as unknown
|
|
|
+ } catch {
|
|
|
+ return undefined
|
|
|
+ }
|
|
|
+ })()
|
|
|
+
|
|
|
+ const store = parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : {}
|
|
|
+ const list = Array.isArray(store.list) ? store.list : []
|
|
|
+ const lastProject = store.lastProject && typeof store.lastProject === "object" ? store.lastProject : {}
|
|
|
+ const projects = store.projects && typeof store.projects === "object" ? store.projects : {}
|
|
|
+ const nextProjects = { ...(projects as Record<string, unknown>) }
|
|
|
+
|
|
|
+ const add = (origin: string, directory: string) => {
|
|
|
+ const current = nextProjects[origin]
|
|
|
+ const items = Array.isArray(current) ? current : []
|
|
|
+ const existing = items.filter(
|
|
|
+ (p): p is { worktree: string; expanded?: boolean } =>
|
|
|
+ !!p &&
|
|
|
+ typeof p === "object" &&
|
|
|
+ "worktree" in p &&
|
|
|
+ typeof (p as { worktree?: unknown }).worktree === "string",
|
|
|
+ )
|
|
|
+
|
|
|
+ if (existing.some((p) => p.worktree === directory)) return
|
|
|
+ nextProjects[origin] = [{ worktree: directory, expanded: true }, ...existing]
|
|
|
+ }
|
|
|
+
|
|
|
+ const directories = [args.directory, ...args.extra]
|
|
|
+ for (const directory of directories) {
|
|
|
+ add("local", directory)
|
|
|
+ add(args.serverUrl, directory)
|
|
|
+ }
|
|
|
+
|
|
|
+ localStorage.setItem(
|
|
|
+ key,
|
|
|
+ JSON.stringify({
|
|
|
+ list,
|
|
|
+ projects: nextProjects,
|
|
|
+ lastProject,
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ },
|
|
|
+ { directory: input.directory, serverUrl, extra: input.extra ?? [] },
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+export async function createTestProject() {
|
|
|
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-project-"))
|
|
|
+
|
|
|
+ await fs.writeFile(path.join(root, "README.md"), "# e2e\n")
|
|
|
+
|
|
|
+ execSync("git init", { cwd: root, stdio: "ignore" })
|
|
|
+ execSync("git add -A", { cwd: root, stdio: "ignore" })
|
|
|
+ execSync('git -c user.name="e2e" -c user.email="[email protected]" commit -m "init" --allow-empty', {
|
|
|
+ cwd: root,
|
|
|
+ stdio: "ignore",
|
|
|
+ })
|
|
|
+
|
|
|
+ return root
|
|
|
+}
|
|
|
+
|
|
|
+export async function cleanupTestProject(directory: string) {
|
|
|
+ await fs.rm(directory, { recursive: true, force: true }).catch(() => undefined)
|
|
|
+}
|
|
|
+
|
|
|
+export function sessionIDFromUrl(url: string) {
|
|
|
+ const match = /\/session\/([^/?#]+)/.exec(url)
|
|
|
+ return match?.[1]
|
|
|
+}
|