refactor(e2e): faster tests (#12021)

This commit is contained in:
Filip
2026-02-04 00:45:49 +01:00
committed by GitHub
parent b5a4671c64
commit acac05f22e
6 changed files with 259 additions and 282 deletions

View File

@@ -1,5 +1,5 @@
import { test as base, expect } from "@playwright/test" import { test as base, expect, type Page } from "@playwright/test"
import { seedProjects } from "./actions" import { cleanupTestProject, createTestProject, seedProjects } from "./actions"
import { promptSelector } from "./selectors" import { promptSelector } from "./selectors"
import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils" import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils"
@@ -8,6 +8,14 @@ export const settingsKey = "settings.v3"
type TestFixtures = { type TestFixtures = {
sdk: ReturnType<typeof createSdk> sdk: ReturnType<typeof createSdk>
gotoSession: (sessionID?: string) => Promise<void> gotoSession: (sessionID?: string) => Promise<void>
withProject: <T>(
callback: (project: {
directory: string
slug: string
gotoSession: (sessionID?: string) => Promise<void>
}) => Promise<T>,
options?: { extra?: string[] },
) => Promise<T>
} }
type WorkerFixtures = { type WorkerFixtures = {
@@ -33,7 +41,37 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
await use(createSdk(directory)) await use(createSdk(directory))
}, },
gotoSession: async ({ page, directory }, use) => { gotoSession: async ({ page, directory }, use) => {
await seedProjects(page, { directory }) await seedStorage(page, { directory })
const gotoSession = async (sessionID?: string) => {
await page.goto(sessionPath(directory, sessionID))
await expect(page.locator(promptSelector)).toBeVisible()
}
await use(gotoSession)
},
withProject: async ({ page }, use) => {
await use(async (callback, options) => {
const directory = await createTestProject()
const slug = dirSlug(directory)
await seedStorage(page, { directory, extra: options?.extra })
const gotoSession = async (sessionID?: string) => {
await page.goto(sessionPath(directory, sessionID))
await expect(page.locator(promptSelector)).toBeVisible()
}
try {
await gotoSession()
return await callback({ directory, slug, gotoSession })
} finally {
await cleanupTestProject(directory)
}
})
},
})
async function seedStorage(page: Page, input: { directory: string; extra?: string[] }) {
await seedProjects(page, input)
await page.addInitScript(() => { await page.addInitScript(() => {
localStorage.setItem( localStorage.setItem(
"opencode.global.dat:model", "opencode.global.dat:model",
@@ -44,13 +82,6 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
}), }),
) )
}) })
const gotoSession = async (sessionID?: string) => {
await page.goto(sessionPath(directory, sessionID))
await expect(page.locator(promptSelector)).toBeVisible()
} }
await use(gotoSession)
},
})
export { expect } export { expect }

View File

@@ -1,10 +1,10 @@
import { test, expect } from "../fixtures" import { test, expect } from "../fixtures"
import { openSidebar } from "../actions" import { openSidebar } from "../actions"
test("dialog edit project updates name and startup script", async ({ page, gotoSession }) => { test("dialog edit project updates name and startup script", async ({ page, withProject }) => {
await gotoSession()
await page.setViewportSize({ width: 1400, height: 800 }) await page.setViewportSize({ width: 1400, height: 800 })
await withProject(async () => {
await openSidebar(page) await openSidebar(page)
const open = async () => { const open = async () => {
@@ -50,3 +50,4 @@ test("dialog edit project updates name and startup script", async ({ page, gotoS
await reopened.getByRole("button", { name: "Cancel" }).click() await reopened.getByRole("button", { name: "Cancel" }).click()
await expect(reopened).toHaveCount(0) await expect(reopened).toHaveCount(0)
}) })
})

View File

@@ -1,18 +1,17 @@
import { test, expect } from "../fixtures" import { test, expect } from "../fixtures"
import { createTestProject, seedProjects, cleanupTestProject, openSidebar, clickMenuItem } from "../actions" import { createTestProject, cleanupTestProject, openSidebar, clickMenuItem } from "../actions"
import { projectCloseHoverSelector, projectCloseMenuSelector, projectSwitchSelector } from "../selectors" import { projectCloseHoverSelector, projectCloseMenuSelector, projectSwitchSelector } from "../selectors"
import { dirSlug } from "../utils" import { dirSlug } from "../utils"
test("can close a project via hover card close button", async ({ page, directory, gotoSession }) => { test("can close a project via hover card close button", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 }) await page.setViewportSize({ width: 1400, height: 800 })
const other = await createTestProject() const other = await createTestProject()
const otherSlug = dirSlug(other) const otherSlug = dirSlug(other)
await seedProjects(page, { directory, extra: [other] })
try { try {
await gotoSession() await withProject(
async () => {
await openSidebar(page) await openSidebar(page)
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first() const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
@@ -24,22 +23,24 @@ test("can close a project via hover card close button", async ({ page, directory
await close.click() await close.click()
await expect(otherButton).toHaveCount(0) await expect(otherButton).toHaveCount(0)
},
{ extra: [other] },
)
} finally { } finally {
await cleanupTestProject(other) await cleanupTestProject(other)
} }
}) })
test("can close a project via project header more options menu", async ({ page, directory, gotoSession }) => { test("can close a project via project header more options menu", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 }) await page.setViewportSize({ width: 1400, height: 800 })
const other = await createTestProject() const other = await createTestProject()
const otherName = other.split("/").pop() ?? other const otherName = other.split("/").pop() ?? other
const otherSlug = dirSlug(other) const otherSlug = dirSlug(other)
await seedProjects(page, { directory, extra: [other] })
try { try {
await gotoSession() await withProject(
async () => {
await openSidebar(page) await openSidebar(page)
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first() const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
@@ -64,6 +65,9 @@ test("can close a project via project header more options menu", async ({ page,
await clickMenuItem(menu, /^Close$/i, { force: true }) await clickMenuItem(menu, /^Close$/i, { force: true })
await expect(otherButton).toHaveCount(0) await expect(otherButton).toHaveCount(0)
},
{ extra: [other] },
)
} finally { } finally {
await cleanupTestProject(other) await cleanupTestProject(other)
} }

View File

@@ -1,19 +1,17 @@
import { test, expect } from "../fixtures" import { test, expect } from "../fixtures"
import { defocus, createTestProject, seedProjects, cleanupTestProject } from "../actions" import { defocus, createTestProject, cleanupTestProject } from "../actions"
import { projectSwitchSelector } from "../selectors" import { projectSwitchSelector } from "../selectors"
import { dirSlug } from "../utils" import { dirSlug } from "../utils"
test("can switch between projects from sidebar", async ({ page, directory, gotoSession }) => { test("can switch between projects from sidebar", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 }) await page.setViewportSize({ width: 1400, height: 800 })
const other = await createTestProject() const other = await createTestProject()
const otherSlug = dirSlug(other) const otherSlug = dirSlug(other)
await seedProjects(page, { directory, extra: [other] })
try { try {
await gotoSession() await withProject(
async ({ directory }) => {
await defocus(page) await defocus(page)
const currentSlug = dirSlug(directory) const currentSlug = dirSlug(directory)
@@ -28,6 +26,9 @@ test("can switch between projects from sidebar", async ({ page, directory, gotoS
await currentButton.click() await currentButton.click()
await expect(page).toHaveURL(new RegExp(`/${currentSlug}/session`)) await expect(page).toHaveURL(new RegExp(`/${currentSlug}/session`))
},
{ extra: [other] },
)
} finally { } finally {
await cleanupTestProject(other) await cleanupTestProject(other)
} }

View File

@@ -10,33 +10,20 @@ import {
cleanupTestProject, cleanupTestProject,
clickMenuItem, clickMenuItem,
confirmDialog, confirmDialog,
createTestProject,
openSidebar, openSidebar,
openWorkspaceMenu, openWorkspaceMenu,
seedProjects,
setWorkspacesEnabled, setWorkspacesEnabled,
} from "../actions" } from "../actions"
import { inlineInputSelector, projectSwitchSelector, workspaceItemSelector } from "../selectors" import { inlineInputSelector, workspaceItemSelector } from "../selectors"
import { dirSlug } from "../utils"
function slugFromUrl(url: string) { function slugFromUrl(url: string) {
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? "" return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
} }
async function setupWorkspaceTest(page: Page, directory: string, gotoSession: () => Promise<void>) { async function setupWorkspaceTest(page: Page, project: { slug: string }) {
const project = await createTestProject() const rootSlug = project.slug
const rootSlug = dirSlug(project)
await seedProjects(page, { directory, extra: [project] })
await gotoSession()
await openSidebar(page) await openSidebar(page)
const target = page.locator(projectSwitchSelector(rootSlug)).first()
await expect(target).toBeVisible()
await target.click()
await expect(page).toHaveURL(new RegExp(`/${rootSlug}/session`))
await openSidebar(page)
await setWorkspacesEnabled(page, rootSlug, true) await setWorkspacesEnabled(page, rootSlug, true)
await page.getByRole("button", { name: "New workspace" }).first().click() await page.getByRole("button", { name: "New workspace" }).first().click()
@@ -70,25 +57,13 @@ async function setupWorkspaceTest(page: Page, directory: string, gotoSession: ()
) )
.toBe(true) .toBe(true)
return { project, rootSlug, slug, directory: dir } return { rootSlug, slug, directory: dir }
} }
test("can enable and disable workspaces from project menu", async ({ page, directory, gotoSession }) => { test("can enable and disable workspaces from project menu", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 }) await page.setViewportSize({ width: 1400, height: 800 })
const project = await createTestProject() await withProject(async ({ slug }) => {
const slug = dirSlug(project)
await seedProjects(page, { directory, extra: [project] })
try {
await gotoSession()
await openSidebar(page)
const target = page.locator(projectSwitchSelector(slug)).first()
await expect(target).toBeVisible()
await target.click()
await expect(page).toHaveURL(new RegExp(`/${slug}/session`))
await openSidebar(page) await openSidebar(page)
await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible() await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible()
@@ -101,27 +76,13 @@ test("can enable and disable workspaces from project menu", async ({ page, direc
await setWorkspacesEnabled(page, slug, false) await setWorkspacesEnabled(page, slug, false)
await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible() await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible()
await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0) await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0)
} finally { })
await cleanupTestProject(project)
}
}) })
test("can create a workspace", async ({ page, directory, gotoSession }) => { test("can create a workspace", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 }) await page.setViewportSize({ width: 1400, height: 800 })
const project = await createTestProject() await withProject(async ({ slug }) => {
const slug = dirSlug(project)
await seedProjects(page, { directory, extra: [project] })
try {
await gotoSession()
await openSidebar(page)
const target = page.locator(projectSwitchSelector(slug)).first()
await expect(target).toBeVisible()
await target.click()
await expect(page).toHaveURL(new RegExp(`/${slug}/session`))
await openSidebar(page) await openSidebar(page)
await setWorkspacesEnabled(page, slug, true) await setWorkspacesEnabled(page, slug, true)
@@ -162,17 +123,15 @@ test("can create a workspace", async ({ page, directory, gotoSession }) => {
await expect(page.locator(workspaceItemSelector(workspaceSlug)).first()).toBeVisible() await expect(page.locator(workspaceItemSelector(workspaceSlug)).first()).toBeVisible()
await cleanupTestProject(workspaceDir) await cleanupTestProject(workspaceDir)
} finally { })
await cleanupTestProject(project)
}
}) })
test("can rename a workspace", async ({ page, directory, gotoSession }) => { test("can rename a workspace", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 }) await page.setViewportSize({ width: 1400, height: 800 })
const { project, slug } = await setupWorkspaceTest(page, directory, gotoSession) await withProject(async (project) => {
const { slug } = await setupWorkspaceTest(page, project)
try {
const rename = `e2e workspace ${Date.now()}` const rename = `e2e workspace ${Date.now()}`
const menu = await openWorkspaceMenu(page, slug) const menu = await openWorkspaceMenu(page, slug)
await clickMenuItem(menu, /^Rename$/i, { force: true }) await clickMenuItem(menu, /^Rename$/i, { force: true })
@@ -186,17 +145,15 @@ test("can rename a workspace", async ({ page, directory, gotoSession }) => {
await input.fill(rename) await input.fill(rename)
await input.press("Enter") await input.press("Enter")
await expect(item).toContainText(rename) await expect(item).toContainText(rename)
} finally { })
await cleanupTestProject(project)
}
}) })
test("can reset a workspace", async ({ page, directory, sdk, gotoSession }) => { test("can reset a workspace", async ({ page, sdk, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 }) await page.setViewportSize({ width: 1400, height: 800 })
const { project, slug, directory: createdDir } = await setupWorkspaceTest(page, directory, gotoSession) await withProject(async (project) => {
const { slug, directory: createdDir } = await setupWorkspaceTest(page, project)
try {
const readme = path.join(createdDir, "README.md") const readme = path.join(createdDir, "README.md")
const extra = path.join(createdDir, `e2e_reset_${Date.now()}.txt`) const extra = path.join(createdDir, `e2e_reset_${Date.now()}.txt`)
const original = await fs.readFile(readme, "utf8") const original = await fs.readFile(readme, "utf8")
@@ -250,17 +207,15 @@ test("can reset a workspace", async ({ page, directory, sdk, gotoSession }) => {
.catch(() => false) .catch(() => false)
}) })
.toBe(false) .toBe(false)
} finally { })
await cleanupTestProject(project)
}
}) })
test("can delete a workspace", async ({ page, directory, gotoSession }) => { test("can delete a workspace", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 }) await page.setViewportSize({ width: 1400, height: 800 })
const { project, rootSlug, slug } = await setupWorkspaceTest(page, directory, gotoSession) await withProject(async (project) => {
const { rootSlug, slug } = await setupWorkspaceTest(page, project)
try {
const menu = await openWorkspaceMenu(page, slug) const menu = await openWorkspaceMenu(page, slug)
await clickMenuItem(menu, /^Delete$/i, { force: true }) await clickMenuItem(menu, /^Delete$/i, { force: true })
await confirmDialog(page, /^Delete workspace$/i) await confirmDialog(page, /^Delete workspace$/i)
@@ -268,18 +223,12 @@ test("can delete a workspace", async ({ page, directory, gotoSession }) => {
await expect(page).toHaveURL(new RegExp(`/${rootSlug}/session`)) await expect(page).toHaveURL(new RegExp(`/${rootSlug}/session`))
await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0) await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0)
await expect(page.locator(workspaceItemSelector(rootSlug)).first()).toBeVisible() await expect(page.locator(workspaceItemSelector(rootSlug)).first()).toBeVisible()
} finally { })
await cleanupTestProject(project)
}
}) })
test("can reorder workspaces by drag and drop", async ({ page, directory, gotoSession }) => { test("can reorder workspaces by drag and drop", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 }) await page.setViewportSize({ width: 1400, height: 800 })
await withProject(async ({ slug: rootSlug }) => {
const project = await createTestProject()
const rootSlug = dirSlug(project)
await seedProjects(page, { directory, extra: [project] })
const workspaces = [] as { directory: string; slug: string }[] const workspaces = [] as { directory: string; slug: string }[]
const listSlugs = async () => { const listSlugs = async () => {
@@ -325,15 +274,8 @@ test("can reorder workspaces by drag and drop", async ({ page, directory, gotoSe
} }
try { try {
await gotoSession()
await openSidebar(page) await openSidebar(page)
const target = page.locator(projectSwitchSelector(rootSlug)).first()
await expect(target).toBeVisible()
await target.click()
await expect(page).toHaveURL(new RegExp(`/${rootSlug}/session`))
await openSidebar(page)
await setWorkspacesEnabled(page, rootSlug, true) await setWorkspacesEnabled(page, rootSlug, true)
for (const _ of [0, 1]) { for (const _ of [0, 1]) {
@@ -386,6 +328,6 @@ test("can reorder workspaces by drag and drop", async ({ page, directory, gotoSe
await expect.poll(async () => await list()).toEqual([from, to]) await expect.poll(async () => await list()).toEqual([from, to])
} finally { } finally {
await Promise.all(workspaces.map((w) => cleanupTestProject(w.directory))) await Promise.all(workspaces.map((w) => cleanupTestProject(w.directory)))
await cleanupTestProject(project)
} }
}) })
})

View File

@@ -6,7 +6,6 @@ const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "localhost"
const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096" const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
const command = `bun run dev -- --host 0.0.0.0 --port ${port}` const command = `bun run dev -- --host 0.0.0.0 --port ${port}`
const reuse = !process.env.CI const reuse = !process.env.CI
const win = process.platform === "win32"
export default defineConfig({ export default defineConfig({
testDir: "./e2e", testDir: "./e2e",
@@ -15,8 +14,7 @@ export default defineConfig({
expect: { expect: {
timeout: 10_000, timeout: 10_000,
}, },
fullyParallel: !win, fullyParallel: true,
workers: win ? 1 : undefined,
forbidOnly: !!process.env.CI, forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0, retries: process.env.CI ? 2 : 0,
reporter: [["html", { outputFolder: "e2e/playwright-report", open: "never" }], ["line"]], reporter: [["html", { outputFolder: "e2e/playwright-report", open: "never" }], ["line"]],