test(app): share workspace slug wait helper across e2e specs (#16446)
This commit is contained in:
@@ -72,6 +72,9 @@ test("test description", async ({ page, sdk, gotoSession }) => {
|
|||||||
- `openSidebar(page)` / `closeSidebar(page)` - Toggle sidebar
|
- `openSidebar(page)` / `closeSidebar(page)` - Toggle sidebar
|
||||||
- `withSession(sdk, title, callback)` - Create temp session
|
- `withSession(sdk, title, callback)` - Create temp session
|
||||||
- `withProject(...)` - Create temp project/workspace
|
- `withProject(...)` - Create temp project/workspace
|
||||||
|
- `sessionIDFromUrl(url)` - Read session ID from URL
|
||||||
|
- `slugFromUrl(url)` - Read workspace slug from URL
|
||||||
|
- `waitSlug(page, skip?)` - Wait for resolved workspace slug
|
||||||
- `trackSession(sessionID, directory?)` - Register session for fixture cleanup
|
- `trackSession(sessionID, directory?)` - Register session for fixture cleanup
|
||||||
- `trackDirectory(directory)` - Register directory for fixture cleanup
|
- `trackDirectory(directory)` - Register directory for fixture cleanup
|
||||||
- `clickListItem(container, filter)` - Click list item by key/text
|
- `clickListItem(container, filter)` - Click list item by key/text
|
||||||
@@ -169,9 +172,10 @@ await page.keyboard.press(`${modKey}+Comma`) // Open settings
|
|||||||
1. Choose appropriate folder or create new one
|
1. Choose appropriate folder or create new one
|
||||||
2. Import from `../fixtures`
|
2. Import from `../fixtures`
|
||||||
3. Use helper functions from `../actions` and `../selectors`
|
3. Use helper functions from `../actions` and `../selectors`
|
||||||
4. Clean up any created resources
|
4. When validating routing, use shared helpers from `../actions`. Workspace URL slugs can be canonicalized on Windows, so assert against canonical or resolved workspace slugs.
|
||||||
5. Use specific selectors (avoid CSS classes)
|
5. Clean up any created resources
|
||||||
6. Test one feature per test file
|
6. Use specific selectors (avoid CSS classes)
|
||||||
|
7. Test one feature per test file
|
||||||
|
|
||||||
## Local Development
|
## Local Development
|
||||||
|
|
||||||
|
|||||||
@@ -199,6 +199,33 @@ export async function cleanupTestProject(directory: string) {
|
|||||||
await fs.rm(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }).catch(() => undefined)
|
await fs.rm(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }).catch(() => undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function slugFromUrl(url: string) {
|
||||||
|
return /\/([^/]+)\/session(?:[/?#]|$)/.exec(url)?.[1] ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitSlug(page: Page, skip: string[] = []) {
|
||||||
|
let prev = ""
|
||||||
|
let next = ""
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
() => {
|
||||||
|
const slug = slugFromUrl(page.url())
|
||||||
|
if (!slug) return ""
|
||||||
|
if (skip.includes(slug)) return ""
|
||||||
|
if (slug !== prev) {
|
||||||
|
prev = slug
|
||||||
|
next = ""
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
next = slug
|
||||||
|
return slug
|
||||||
|
},
|
||||||
|
{ timeout: 45_000 },
|
||||||
|
)
|
||||||
|
.not.toBe("")
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
export function sessionIDFromUrl(url: string) {
|
export function sessionIDFromUrl(url: string) {
|
||||||
const match = /\/session\/([^/?#]+)/.exec(url)
|
const match = /\/session\/([^/?#]+)/.exec(url)
|
||||||
return match?.[1]
|
return match?.[1]
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { base64Decode } from "@opencode-ai/util/encode"
|
import { base64Decode } from "@opencode-ai/util/encode"
|
||||||
import type { Page } from "@playwright/test"
|
import type { Page } from "@playwright/test"
|
||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { defocus, createTestProject, cleanupTestProject, openSidebar, sessionIDFromUrl } from "../actions"
|
import { defocus, createTestProject, cleanupTestProject, openSidebar, sessionIDFromUrl, waitSlug } from "../actions"
|
||||||
import { projectSwitchSelector, promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
|
import { projectSwitchSelector, promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
|
||||||
import { dirSlug } from "../utils"
|
import { dirSlug, resolveDirectory } from "../utils"
|
||||||
|
|
||||||
function slugFromUrl(url: string) {
|
|
||||||
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
|
|
||||||
}
|
|
||||||
|
|
||||||
async function workspaces(page: Page, directory: string, enabled: boolean) {
|
async function workspaces(page: Page, directory: string, enabled: boolean) {
|
||||||
await page.evaluate(
|
await page.evaluate(
|
||||||
@@ -76,7 +72,6 @@ test("switching back to a project opens the latest workspace session", async ({
|
|||||||
|
|
||||||
const other = await createTestProject()
|
const other = await createTestProject()
|
||||||
const otherSlug = dirSlug(other)
|
const otherSlug = dirSlug(other)
|
||||||
let workspaceDir: string | undefined
|
|
||||||
try {
|
try {
|
||||||
await withProject(
|
await withProject(
|
||||||
async ({ directory, slug, trackSession, trackDirectory }) => {
|
async ({ directory, slug, trackSession, trackDirectory }) => {
|
||||||
@@ -89,33 +84,27 @@ test("switching back to a project opens the latest workspace session", async ({
|
|||||||
|
|
||||||
await page.getByRole("button", { name: "New workspace" }).first().click()
|
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||||
|
|
||||||
await expect
|
const raw = await waitSlug(page, [slug])
|
||||||
.poll(
|
const dir = base64Decode(raw)
|
||||||
() => {
|
if (!dir) throw new Error(`Failed to decode workspace slug: ${raw}`)
|
||||||
const next = slugFromUrl(page.url())
|
const space = await resolveDirectory(dir)
|
||||||
if (!next) return ""
|
const next = dirSlug(space)
|
||||||
if (next === slug) return ""
|
trackDirectory(space)
|
||||||
return next
|
|
||||||
},
|
|
||||||
{ timeout: 45_000 },
|
|
||||||
)
|
|
||||||
.not.toBe("")
|
|
||||||
|
|
||||||
const workspaceSlug = slugFromUrl(page.url())
|
|
||||||
workspaceDir = base64Decode(workspaceSlug)
|
|
||||||
if (!workspaceDir) throw new Error(`Failed to decode workspace slug: ${workspaceSlug}`)
|
|
||||||
trackDirectory(workspaceDir)
|
|
||||||
await openSidebar(page)
|
await openSidebar(page)
|
||||||
|
|
||||||
const workspace = page.locator(workspaceItemSelector(workspaceSlug)).first()
|
const item = page.locator(`${workspaceItemSelector(next)}, ${workspaceItemSelector(raw)}`).first()
|
||||||
await expect(workspace).toBeVisible()
|
await expect(item).toBeVisible()
|
||||||
await workspace.hover()
|
await item.hover()
|
||||||
|
|
||||||
const newSession = page.locator(workspaceNewSessionSelector(workspaceSlug)).first()
|
const btn = page.locator(`${workspaceNewSessionSelector(next)}, ${workspaceNewSessionSelector(raw)}`).first()
|
||||||
await expect(newSession).toBeVisible()
|
await expect(btn).toBeVisible()
|
||||||
await newSession.click({ force: true })
|
await btn.click({ force: true })
|
||||||
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session(?:[/?#]|$)`))
|
// A new workspace can be discovered via a transient slug before the route and sidebar
|
||||||
|
// settle to the canonical workspace path on Windows, so interact with either and assert
|
||||||
|
// against the resolved workspace slug.
|
||||||
|
await waitSlug(page)
|
||||||
|
await expect(page).toHaveURL(new RegExp(`/${next}/session(?:[/?#]|$)`))
|
||||||
|
|
||||||
// Create a session by sending a prompt
|
// Create a session by sending a prompt
|
||||||
const prompt = page.locator(promptSelector)
|
const prompt = page.locator(promptSelector)
|
||||||
@@ -128,9 +117,9 @@ test("switching back to a project opens the latest workspace session", async ({
|
|||||||
|
|
||||||
const created = sessionIDFromUrl(page.url())
|
const created = sessionIDFromUrl(page.url())
|
||||||
if (!created) throw new Error(`Failed to get session ID from url: ${page.url()}`)
|
if (!created) throw new Error(`Failed to get session ID from url: ${page.url()}`)
|
||||||
trackSession(created, workspaceDir)
|
trackSession(created, space)
|
||||||
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session/${created}(?:[/?#]|$)`))
|
await expect(page).toHaveURL(new RegExp(`/${next}/session/${created}(?:[/?#]|$)`))
|
||||||
|
|
||||||
await openSidebar(page)
|
await openSidebar(page)
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +1,10 @@
|
|||||||
import { base64Decode } from "@opencode-ai/util/encode"
|
import { base64Decode } from "@opencode-ai/util/encode"
|
||||||
import type { Page } from "@playwright/test"
|
import type { Page } from "@playwright/test"
|
||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { openSidebar, sessionIDFromUrl, setWorkspacesEnabled } from "../actions"
|
import { openSidebar, sessionIDFromUrl, setWorkspacesEnabled, slugFromUrl, waitSlug } from "../actions"
|
||||||
import { promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
|
import { promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
|
||||||
import { createSdk } from "../utils"
|
import { createSdk } from "../utils"
|
||||||
|
|
||||||
function slugFromUrl(url: string) {
|
|
||||||
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitSlug(page: Page, skip: string[] = []) {
|
|
||||||
let prev = ""
|
|
||||||
await expect
|
|
||||||
.poll(
|
|
||||||
() => {
|
|
||||||
const slug = slugFromUrl(page.url())
|
|
||||||
if (!slug) return ""
|
|
||||||
if (skip.includes(slug)) return ""
|
|
||||||
if (slug !== prev) {
|
|
||||||
prev = slug
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return slug
|
|
||||||
},
|
|
||||||
{ timeout: 45_000 },
|
|
||||||
)
|
|
||||||
.not.toBe("")
|
|
||||||
return slugFromUrl(page.url())
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitWorkspaceReady(page: Page, slug: string) {
|
async function waitWorkspaceReady(page: Page, slug: string) {
|
||||||
await openSidebar(page)
|
await openSidebar(page)
|
||||||
await expect
|
await expect
|
||||||
|
|||||||
@@ -14,34 +14,12 @@ import {
|
|||||||
openSidebar,
|
openSidebar,
|
||||||
openWorkspaceMenu,
|
openWorkspaceMenu,
|
||||||
setWorkspacesEnabled,
|
setWorkspacesEnabled,
|
||||||
|
slugFromUrl,
|
||||||
|
waitSlug,
|
||||||
} from "../actions"
|
} from "../actions"
|
||||||
import { dropdownMenuContentSelector, inlineInputSelector, workspaceItemSelector } from "../selectors"
|
import { dropdownMenuContentSelector, inlineInputSelector, workspaceItemSelector } from "../selectors"
|
||||||
import { createSdk, dirSlug } from "../utils"
|
import { createSdk, dirSlug } from "../utils"
|
||||||
|
|
||||||
function slugFromUrl(url: string) {
|
|
||||||
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitSlug(page: Page, skip: string[] = []) {
|
|
||||||
let prev = ""
|
|
||||||
await expect
|
|
||||||
.poll(
|
|
||||||
() => {
|
|
||||||
const slug = slugFromUrl(page.url())
|
|
||||||
if (!slug) return ""
|
|
||||||
if (skip.includes(slug)) return ""
|
|
||||||
if (slug !== prev) {
|
|
||||||
prev = slug
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return slug
|
|
||||||
},
|
|
||||||
{ timeout: 45_000 },
|
|
||||||
)
|
|
||||||
.not.toBe("")
|
|
||||||
return slugFromUrl(page.url())
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setupWorkspaceTest(page: Page, project: { slug: string }) {
|
async function setupWorkspaceTest(page: Page, project: { slug: string }) {
|
||||||
const rootSlug = project.slug
|
const rootSlug = project.slug
|
||||||
await openSidebar(page)
|
await openSidebar(page)
|
||||||
@@ -353,17 +331,7 @@ test("can reorder workspaces by drag and drop", async ({ page, withProject }) =>
|
|||||||
for (const _ of [0, 1]) {
|
for (const _ of [0, 1]) {
|
||||||
const prev = slugFromUrl(page.url())
|
const prev = slugFromUrl(page.url())
|
||||||
await page.getByRole("button", { name: "New workspace" }).first().click()
|
await page.getByRole("button", { name: "New workspace" }).first().click()
|
||||||
await expect
|
const slug = await waitSlug(page, [rootSlug, prev])
|
||||||
.poll(
|
|
||||||
() => {
|
|
||||||
const slug = slugFromUrl(page.url())
|
|
||||||
return slug.length > 0 && slug !== rootSlug && slug !== prev
|
|
||||||
},
|
|
||||||
{ timeout: 45_000 },
|
|
||||||
)
|
|
||||||
.toBe(true)
|
|
||||||
|
|
||||||
const slug = slugFromUrl(page.url())
|
|
||||||
const dir = base64Decode(slug)
|
const dir = base64Decode(slug)
|
||||||
workspaces.push({ slug, directory: dir })
|
workspaces.push({ slug, directory: dir })
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user