Răsfoiți Sursa

refactor(e2e): faster tests (#12021)

Filip 2 săptămâni în urmă
părinte
comite
acac05f22e

+ 44 - 13
packages/app/e2e/fixtures.ts

@@ -1,5 +1,5 @@
-import { test as base, expect } from "@playwright/test"
-import { seedProjects } from "./actions"
+import { test as base, expect, type Page } from "@playwright/test"
+import { cleanupTestProject, createTestProject, seedProjects } from "./actions"
 import { promptSelector } from "./selectors"
 import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils"
 
@@ -8,6 +8,14 @@ export const settingsKey = "settings.v3"
 type TestFixtures = {
   sdk: ReturnType<typeof createSdk>
   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 = {
@@ -33,17 +41,7 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
     await use(createSdk(directory))
   },
   gotoSession: async ({ page, directory }, use) => {
-    await seedProjects(page, { directory })
-    await page.addInitScript(() => {
-      localStorage.setItem(
-        "opencode.global.dat:model",
-        JSON.stringify({
-          recent: [{ providerID: "opencode", modelID: "big-pickle" }],
-          user: [],
-          variant: {},
-        }),
-      )
-    })
+    await seedStorage(page, { directory })
 
     const gotoSession = async (sessionID?: string) => {
       await page.goto(sessionPath(directory, sessionID))
@@ -51,6 +49,39 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
     }
     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(() => {
+    localStorage.setItem(
+      "opencode.global.dat:model",
+      JSON.stringify({
+        recent: [{ providerID: "opencode", modelID: "big-pickle" }],
+        user: [],
+        variant: {},
+      }),
+    )
+  })
+}
+
 export { expect }

+ 37 - 36
packages/app/e2e/projects/project-edit.spec.ts

@@ -1,52 +1,53 @@
 import { test, expect } from "../fixtures"
 import { openSidebar } from "../actions"
 
-test("dialog edit project updates name and startup script", async ({ page, gotoSession }) => {
-  await gotoSession()
+test("dialog edit project updates name and startup script", async ({ page, withProject }) => {
   await page.setViewportSize({ width: 1400, height: 800 })
 
-  await openSidebar(page)
+  await withProject(async () => {
+    await openSidebar(page)
 
-  const open = async () => {
-    const header = page.locator(".group\\/project").first()
-    await header.hover()
-    const trigger = header.getByRole("button", { name: "More options" }).first()
-    await expect(trigger).toBeVisible()
-    await trigger.click({ force: true })
-
-    const menu = page.locator('[data-component="dropdown-menu-content"]').first()
-    await expect(menu).toBeVisible()
+    const open = async () => {
+      const header = page.locator(".group\\/project").first()
+      await header.hover()
+      const trigger = header.getByRole("button", { name: "More options" }).first()
+      await expect(trigger).toBeVisible()
+      await trigger.click({ force: true })
 
-    const editItem = menu.getByRole("menuitem", { name: "Edit" }).first()
-    await expect(editItem).toBeVisible()
-    await editItem.click({ force: true })
+      const menu = page.locator('[data-component="dropdown-menu-content"]').first()
+      await expect(menu).toBeVisible()
 
-    const dialog = page.getByRole("dialog")
-    await expect(dialog).toBeVisible()
-    await expect(dialog.getByRole("heading", { level: 2 })).toHaveText("Edit project")
-    return dialog
-  }
+      const editItem = menu.getByRole("menuitem", { name: "Edit" }).first()
+      await expect(editItem).toBeVisible()
+      await editItem.click({ force: true })
 
-  const name = `e2e project ${Date.now()}`
-  const startup = `echo e2e_${Date.now()}`
+      const dialog = page.getByRole("dialog")
+      await expect(dialog).toBeVisible()
+      await expect(dialog.getByRole("heading", { level: 2 })).toHaveText("Edit project")
+      return dialog
+    }
 
-  const dialog = await open()
+    const name = `e2e project ${Date.now()}`
+    const startup = `echo e2e_${Date.now()}`
 
-  const nameInput = dialog.getByLabel("Name")
-  await nameInput.fill(name)
+    const dialog = await open()
 
-  const startupInput = dialog.getByLabel("Workspace startup script")
-  await startupInput.fill(startup)
+    const nameInput = dialog.getByLabel("Name")
+    await nameInput.fill(name)
 
-  await dialog.getByRole("button", { name: "Save" }).click()
-  await expect(dialog).toHaveCount(0)
+    const startupInput = dialog.getByLabel("Workspace startup script")
+    await startupInput.fill(startup)
 
-  const header = page.locator(".group\\/project").first()
-  await expect(header).toContainText(name)
+    await dialog.getByRole("button", { name: "Save" }).click()
+    await expect(dialog).toHaveCount(0)
 
-  const reopened = await open()
-  await expect(reopened.getByLabel("Name")).toHaveValue(name)
-  await expect(reopened.getByLabel("Workspace startup script")).toHaveValue(startup)
-  await reopened.getByRole("button", { name: "Cancel" }).click()
-  await expect(reopened).toHaveCount(0)
+    const header = page.locator(".group\\/project").first()
+    await expect(header).toContainText(name)
+
+    const reopened = await open()
+    await expect(reopened.getByLabel("Name")).toHaveValue(name)
+    await expect(reopened.getByLabel("Workspace startup script")).toHaveValue(startup)
+    await reopened.getByRole("button", { name: "Cancel" }).click()
+    await expect(reopened).toHaveCount(0)
+  })
 })

+ 48 - 44
packages/app/e2e/projects/projects-close.spec.ts

@@ -1,69 +1,73 @@
 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 { 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 })
 
   const other = await createTestProject()
   const otherSlug = dirSlug(other)
-  await seedProjects(page, { directory, extra: [other] })
 
   try {
-    await gotoSession()
-
-    await openSidebar(page)
-
-    const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
-    await expect(otherButton).toBeVisible()
-    await otherButton.hover()
-
-    const close = page.locator(projectCloseHoverSelector(otherSlug)).first()
-    await expect(close).toBeVisible()
-    await close.click()
-
-    await expect(otherButton).toHaveCount(0)
+    await withProject(
+      async () => {
+        await openSidebar(page)
+
+        const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
+        await expect(otherButton).toBeVisible()
+        await otherButton.hover()
+
+        const close = page.locator(projectCloseHoverSelector(otherSlug)).first()
+        await expect(close).toBeVisible()
+        await close.click()
+
+        await expect(otherButton).toHaveCount(0)
+      },
+      { extra: [other] },
+    )
   } finally {
     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 })
 
   const other = await createTestProject()
   const otherName = other.split("/").pop() ?? other
   const otherSlug = dirSlug(other)
-  await seedProjects(page, { directory, extra: [other] })
 
   try {
-    await gotoSession()
-
-    await openSidebar(page)
-
-    const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
-    await expect(otherButton).toBeVisible()
-    await otherButton.click()
-
-    await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))
-
-    const header = page
-      .locator(".group\\/project")
-      .filter({ has: page.locator(`[data-action="project-menu"][data-project="${otherSlug}"]`) })
-      .first()
-    await expect(header).toContainText(otherName)
-
-    const trigger = header.locator(`[data-action="project-menu"][data-project="${otherSlug}"]`).first()
-    await expect(trigger).toHaveCount(1)
-    await trigger.focus()
-    await page.keyboard.press("Enter")
-
-    const menu = page.locator('[data-component="dropdown-menu-content"]').first()
-    await expect(menu).toBeVisible({ timeout: 10_000 })
-
-    await clickMenuItem(menu, /^Close$/i, { force: true })
-    await expect(otherButton).toHaveCount(0)
+    await withProject(
+      async () => {
+        await openSidebar(page)
+
+        const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
+        await expect(otherButton).toBeVisible()
+        await otherButton.click()
+
+        await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))
+
+        const header = page
+          .locator(".group\\/project")
+          .filter({ has: page.locator(`[data-action="project-menu"][data-project="${otherSlug}"]`) })
+          .first()
+        await expect(header).toContainText(otherName)
+
+        const trigger = header.locator(`[data-action="project-menu"][data-project="${otherSlug}"]`).first()
+        await expect(trigger).toHaveCount(1)
+        await trigger.focus()
+        await page.keyboard.press("Enter")
+
+        const menu = page.locator('[data-component="dropdown-menu-content"]').first()
+        await expect(menu).toBeVisible({ timeout: 10_000 })
+
+        await clickMenuItem(menu, /^Close$/i, { force: true })
+        await expect(otherButton).toHaveCount(0)
+      },
+      { extra: [other] },
+    )
   } finally {
     await cleanupTestProject(other)
   }

+ 17 - 16
packages/app/e2e/projects/projects-switch.spec.ts

@@ -1,33 +1,34 @@
 import { test, expect } from "../fixtures"
-import { defocus, createTestProject, seedProjects, cleanupTestProject } from "../actions"
+import { defocus, createTestProject, cleanupTestProject } from "../actions"
 import { projectSwitchSelector } from "../selectors"
 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 })
 
   const other = await createTestProject()
   const otherSlug = dirSlug(other)
 
-  await seedProjects(page, { directory, extra: [other] })
-
   try {
-    await gotoSession()
-
-    await defocus(page)
+    await withProject(
+      async ({ directory }) => {
+        await defocus(page)
 
-    const currentSlug = dirSlug(directory)
-    const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
-    await expect(otherButton).toBeVisible()
-    await otherButton.click()
+        const currentSlug = dirSlug(directory)
+        const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
+        await expect(otherButton).toBeVisible()
+        await otherButton.click()
 
-    await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))
+        await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))
 
-    const currentButton = page.locator(projectSwitchSelector(currentSlug)).first()
-    await expect(currentButton).toBeVisible()
-    await currentButton.click()
+        const currentButton = page.locator(projectSwitchSelector(currentSlug)).first()
+        await expect(currentButton).toBeVisible()
+        await currentButton.click()
 
-    await expect(page).toHaveURL(new RegExp(`/${currentSlug}/session`))
+        await expect(page).toHaveURL(new RegExp(`/${currentSlug}/session`))
+      },
+      { extra: [other] },
+    )
   } finally {
     await cleanupTestProject(other)
   }

+ 106 - 164
packages/app/e2e/projects/workspaces.spec.ts

@@ -10,33 +10,20 @@ import {
   cleanupTestProject,
   clickMenuItem,
   confirmDialog,
-  createTestProject,
   openSidebar,
   openWorkspaceMenu,
-  seedProjects,
   setWorkspacesEnabled,
 } from "../actions"
-import { inlineInputSelector, projectSwitchSelector, workspaceItemSelector } from "../selectors"
-import { dirSlug } from "../utils"
+import { inlineInputSelector, workspaceItemSelector } from "../selectors"
 
 function slugFromUrl(url: string) {
   return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
 }
 
-async function setupWorkspaceTest(page: Page, directory: string, gotoSession: () => Promise<void>) {
-  const project = await createTestProject()
-  const rootSlug = dirSlug(project)
-  await seedProjects(page, { directory, extra: [project] })
-
-  await gotoSession()
+async function setupWorkspaceTest(page: Page, project: { slug: string }) {
+  const rootSlug = project.slug
   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 page.getByRole("button", { name: "New workspace" }).first().click()
@@ -70,25 +57,13 @@ async function setupWorkspaceTest(page: Page, directory: string, gotoSession: ()
     )
     .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 })
 
-  const project = await createTestProject()
-  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 withProject(async ({ slug }) => {
     await openSidebar(page)
 
     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 expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible()
     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 })
 
-  const project = await createTestProject()
-  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 withProject(async ({ slug }) => {
     await openSidebar(page)
     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 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 })
 
-  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 menu = await openWorkspaceMenu(page, slug)
     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.press("Enter")
     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 })
 
-  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 extra = path.join(createdDir, `e2e_reset_${Date.now()}.txt`)
     const original = await fs.readFile(readme, "utf8")
@@ -250,17 +207,15 @@ test("can reset a workspace", async ({ page, directory, sdk, gotoSession }) => {
           .catch(() => 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 })
 
-  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)
     await clickMenuItem(menu, /^Delete$/i, { force: true })
     await confirmDialog(page, /^Delete workspace$/i)
@@ -268,124 +223,111 @@ test("can delete a workspace", async ({ page, directory, gotoSession }) => {
     await expect(page).toHaveURL(new RegExp(`/${rootSlug}/session`))
     await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0)
     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 withProject(async ({ slug: rootSlug }) => {
+    const workspaces = [] as { directory: string; slug: string }[]
 
-  const project = await createTestProject()
-  const rootSlug = dirSlug(project)
-  await seedProjects(page, { directory, extra: [project] })
+    const listSlugs = async () => {
+      const nodes = page.locator('[data-component="sidebar-nav-desktop"] [data-component="workspace-item"]')
+      const slugs = await nodes.evaluateAll((els) => {
+        return els.map((el) => el.getAttribute("data-workspace") ?? "").filter((x) => x.length > 0)
+      })
+      return slugs
+    }
 
-  const workspaces = [] as { directory: string; slug: string }[]
+    const waitReady = async (slug: string) => {
+      await expect
+        .poll(
+          async () => {
+            const item = page.locator(workspaceItemSelector(slug)).first()
+            try {
+              await item.hover({ timeout: 500 })
+              return true
+            } catch {
+              return false
+            }
+          },
+          { timeout: 60_000 },
+        )
+        .toBe(true)
+    }
 
-  const listSlugs = async () => {
-    const nodes = page.locator('[data-component="sidebar-nav-desktop"] [data-component="workspace-item"]')
-    const slugs = await nodes.evaluateAll((els) => {
-      return els.map((el) => el.getAttribute("data-workspace") ?? "").filter((x) => x.length > 0)
-    })
-    return slugs
-  }
+    const drag = async (from: string, to: string) => {
+      const src = page.locator(workspaceItemSelector(from)).first()
+      const dst = page.locator(workspaceItemSelector(to)).first()
 
-  const waitReady = async (slug: string) => {
-    await expect
-      .poll(
-        async () => {
-          const item = page.locator(workspaceItemSelector(slug)).first()
-          try {
-            await item.hover({ timeout: 500 })
-            return true
-          } catch {
-            return false
-          }
-        },
-        { timeout: 60_000 },
-      )
-      .toBe(true)
-  }
+      await src.scrollIntoViewIfNeeded()
+      await dst.scrollIntoViewIfNeeded()
 
-  const drag = async (from: string, to: string) => {
-    const src = page.locator(workspaceItemSelector(from)).first()
-    const dst = page.locator(workspaceItemSelector(to)).first()
+      const a = await src.boundingBox()
+      const b = await dst.boundingBox()
+      if (!a || !b) throw new Error("Failed to resolve workspace drag bounds")
 
-    await src.scrollIntoViewIfNeeded()
-    await dst.scrollIntoViewIfNeeded()
+      await page.mouse.move(a.x + a.width / 2, a.y + a.height / 2)
+      await page.mouse.down()
+      await page.mouse.move(b.x + b.width / 2, b.y + b.height / 2, { steps: 12 })
+      await page.mouse.up()
+    }
 
-    const a = await src.boundingBox()
-    const b = await dst.boundingBox()
-    if (!a || !b) throw new Error("Failed to resolve workspace drag bounds")
+    try {
+      await openSidebar(page)
 
-    await page.mouse.move(a.x + a.width / 2, a.y + a.height / 2)
-    await page.mouse.down()
-    await page.mouse.move(b.x + b.width / 2, b.y + b.height / 2, { steps: 12 })
-    await page.mouse.up()
-  }
+      await setWorkspacesEnabled(page, rootSlug, true)
+
+      for (const _ of [0, 1]) {
+        const prev = slugFromUrl(page.url())
+        await page.getByRole("button", { name: "New workspace" }).first().click()
+        await expect
+          .poll(
+            () => {
+              const slug = slugFromUrl(page.url())
+              return slug.length > 0 && slug !== rootSlug && slug !== prev
+            },
+            { timeout: 45_000 },
+          )
+          .toBe(true)
 
-  try {
-    await gotoSession()
-    await openSidebar(page)
+        const slug = slugFromUrl(page.url())
+        const dir = base64Decode(slug)
+        workspaces.push({ slug, directory: dir })
 
-    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 openSidebar(page)
-    await setWorkspacesEnabled(page, rootSlug, true)
+      if (workspaces.length !== 2) throw new Error("Expected two created workspaces")
 
-    for (const _ of [0, 1]) {
-      const prev = slugFromUrl(page.url())
-      await page.getByRole("button", { name: "New workspace" }).first().click()
-      await expect
-        .poll(
-          () => {
-            const slug = slugFromUrl(page.url())
-            return slug.length > 0 && slug !== rootSlug && slug !== prev
-          },
-          { timeout: 45_000 },
-        )
-        .toBe(true)
+      const a = workspaces[0].slug
+      const b = workspaces[1].slug
 
-      const slug = slugFromUrl(page.url())
-      const dir = base64Decode(slug)
-      workspaces.push({ slug, directory: dir })
+      await waitReady(a)
+      await waitReady(b)
 
-      await openSidebar(page)
-    }
+      const list = async () => {
+        const slugs = await listSlugs()
+        return slugs.filter((s) => s !== rootSlug && (s === a || s === b)).slice(0, 2)
+      }
 
-    if (workspaces.length !== 2) throw new Error("Expected two created workspaces")
+      await expect
+        .poll(async () => {
+          const slugs = await list()
+          return slugs.length === 2
+        })
+        .toBe(true)
 
-    const a = workspaces[0].slug
-    const b = workspaces[1].slug
+      const before = await list()
+      const from = before[1]
+      const to = before[0]
+      if (!from || !to) throw new Error("Failed to resolve initial workspace order")
 
-    await waitReady(a)
-    await waitReady(b)
+      await drag(from, to)
 
-    const list = async () => {
-      const slugs = await listSlugs()
-      return slugs.filter((s) => s !== rootSlug && (s === a || s === b)).slice(0, 2)
+      await expect.poll(async () => await list()).toEqual([from, to])
+    } finally {
+      await Promise.all(workspaces.map((w) => cleanupTestProject(w.directory)))
     }
-
-    await expect
-      .poll(async () => {
-        const slugs = await list()
-        return slugs.length === 2
-      })
-      .toBe(true)
-
-    const before = await list()
-    const from = before[1]
-    const to = before[0]
-    if (!from || !to) throw new Error("Failed to resolve initial workspace order")
-
-    await drag(from, to)
-
-    await expect.poll(async () => await list()).toEqual([from, to])
-  } finally {
-    await Promise.all(workspaces.map((w) => cleanupTestProject(w.directory)))
-    await cleanupTestProject(project)
-  }
+  })
 })

+ 1 - 3
packages/app/playwright.config.ts

@@ -6,7 +6,6 @@ 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
-const win = process.platform === "win32"
 
 export default defineConfig({
   testDir: "./e2e",
@@ -15,8 +14,7 @@ export default defineConfig({
   expect: {
     timeout: 10_000,
   },
-  fullyParallel: !win,
-  workers: win ? 1 : undefined,
+  fullyParallel: true,
   forbidOnly: !!process.env.CI,
   retries: process.env.CI ? 2 : 0,
   reporter: [["html", { outputFolder: "e2e/playwright-report", open: "never" }], ["line"]],