Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/app/e2e/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
terminalSelector,
workspaceItemSelector,
workspaceMenuTriggerSelector,
workspacePinToggleSelector,
} from "./selectors"

export async function defocus(page: Page) {
Expand Down Expand Up @@ -853,3 +854,16 @@ export async function openWorkspaceMenu(page: Page, workspaceSlug: string) {
await expect(menu).toBeVisible()
return menu
}

export async function setWorkspacePinned(page: Page, workspaceSlug: string, enabled: boolean) {
const menu = await openWorkspaceMenu(page, workspaceSlug)
const toggle = menu.locator(workspacePinToggleSelector(workspaceSlug)).first()
await expect(toggle).toBeVisible()
const name = await toggle.textContent()
const pinned = (name ?? "").toLowerCase().includes("unpin")
if (pinned === enabled) {
await page.keyboard.press("Escape")
return
}
await toggle.click({ force: true })
}
276 changes: 275 additions & 1 deletion packages/app/e2e/projects/workspaces.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,41 @@ import { test, expect } from "../fixtures"

test.describe.configure({ mode: "serial" })
import {
createTestProject,
cleanupTestProject,
clickMenuItem,
confirmDialog,
openSidebar,
openWorkspaceMenu,
setWorkspacePinned,
setWorkspacesEnabled,
slugFromUrl,
waitSlug,
} from "../actions"
import { dropdownMenuContentSelector, inlineInputSelector, workspaceItemSelector } from "../selectors"
import {
dropdownMenuContentSelector,
inlineInputSelector,
projectSwitchSelector,
workspaceDividerSelector,
workspaceItemSelector,
} from "../selectors"
import { createSdk, dirSlug } from "../utils"

async function ensureWorkspacesEnabled(page: Page, slug: string) {
for (const _ of [0, 1, 2]) {
await openSidebar(page)
await setWorkspacesEnabled(page, slug, true)
const visible = await page
.getByRole("button", { name: "New workspace" })
.first()
.isVisible()
.then((x) => x)
.catch(() => false)
if (visible) return
}
await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible({ timeout: 60_000 })
}

async function setupWorkspaceTest(page: Page, project: { slug: string }) {
const rootSlug = project.slug
await openSidebar(page)
Expand Down Expand Up @@ -279,6 +302,257 @@ test("can delete a workspace", async ({ page, withProject }) => {
})
})

test("can pin and unpin a workspace with persistence", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })
await withProject(async ({ slug: rootSlug }) => {
await ensureWorkspacesEnabled(page, rootSlug)

const workspaces = [] as string[]
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)

workspaces.push(slugFromUrl(page.url()))
await openSidebar(page)
}

const a = workspaces[0]
const b = workspaces[1]
if (!a || !b) throw new Error("Expected two created workspaces")

const key = (slug: string) => {
return base64Decode(slug)
.replace(/[\\/]+/g, "/")
.replace(/\/+$/, "")
.toLowerCase()
}

const aKey = key(a)
const bKey = key(b)
const rootKey = key(rootSlug)

const list = 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)
})
const seen = new Set<string>()
return slugs
.filter((slug) => {
const slugKey = key(slug)
if (seen.has(slugKey)) return false
seen.add(slugKey)
return true
})
.filter((slug) => {
const slugKey = key(slug)
return slugKey === aKey || slugKey === bKey
})
}

const listAll = 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)
})
const seen = new Set<string>()
return slugs
.filter((slug) => {
const slugKey = key(slug)
if (seen.has(slugKey)) return false
seen.add(slugKey)
return true
})
.filter((slug) => {
const slugKey = key(slug)
return slugKey === rootKey || slugKey === aKey || slugKey === bKey
})
}

const find = async (target: string) => {
const slugs = await listAll()
return slugs.find((slug) => key(slug) === target)
}

await expect.poll(async () => (await list()).length).toBe(2)
const before = await list()
const aSlug = await find(aKey)
if (!aSlug) throw new Error("Missing first workspace slug")

await setWorkspacePinned(page, aSlug, true)
await expect.poll(async () => (await list()).map((slug) => key(slug))).toEqual([aKey, bKey])

await setWorkspacePinned(page, rootSlug, false)
await expect.poll(async () => key((await listAll())[0] ?? "")).toBe(aKey)

await setWorkspacePinned(page, rootSlug, true)
await expect.poll(async () => key((await listAll())[0] ?? "")).toBe(rootKey)

await page.reload()
await openSidebar(page)
await expect.poll(async () => (await list()).map((slug) => key(slug))).toEqual([aKey, bKey])

const pinnedSlug = await find(aKey)
if (!pinnedSlug) throw new Error("Missing pinned workspace slug")
await setWorkspacePinned(page, pinnedSlug, false)
await expect.poll(async () => await list()).toEqual(before)
})
})

test("workspace pinning is isolated per project", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })

const other = await createTestProject()
const otherSlug = dirSlug(other)
const dirs = [] as string[]
const key = (slug: string) =>
base64Decode(slug)
.replace(/[\\/]+/g, "/")
.replace(/\/+$/, "")
.toLowerCase()

try {
await withProject(
async ({ slug }) => {
await ensureWorkspacesEnabled(page, slug)

await page.getByRole("button", { name: "New workspace" }).first().click()
await expect
.poll(
() => {
const next = slugFromUrl(page.url())
if (!next) return ""
if (next === slug) return ""
return next
},
{ timeout: 45_000 },
)
.not.toBe("")

const pinnedSlug = slugFromUrl(page.url())
dirs.push(base64Decode(pinnedSlug))
const pinnedKey = key(pinnedSlug)

await openSidebar(page)
await setWorkspacePinned(page, pinnedSlug, true)

const pinnedMenu = await openWorkspaceMenu(page, pinnedSlug)
await expect(
pinnedMenu
.getByRole("menuitem")
.filter({ hasText: /^Unpin$/i })
.first(),
).toBeVisible()
await page.keyboard.press("Escape")

const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
await expect(otherButton).toBeVisible()
await otherButton.click()
await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))

await ensureWorkspacesEnabled(page, otherSlug)

await page.getByRole("button", { name: "New workspace" }).first().click()
await expect
.poll(
() => {
const next = slugFromUrl(page.url())
if (!next) return ""
if (next === otherSlug) return ""
return next
},
{ timeout: 45_000 },
)
.not.toBe("")

const otherWorkspace = slugFromUrl(page.url())
dirs.push(base64Decode(otherWorkspace))

await openSidebar(page)
const otherMenu = await openWorkspaceMenu(page, otherWorkspace)
await expect(otherMenu.getByRole("menuitem").filter({ hasText: /^Pin$/i }).first()).toBeVisible()
await page.keyboard.press("Escape")

const rootButton = page.locator(projectSwitchSelector(slug)).first()
await expect(rootButton).toBeVisible()
await rootButton.click()

await openSidebar(page)
const slugs = await page
.locator('[data-component="sidebar-nav-desktop"] [data-component="workspace-item"]')
.evaluateAll((els) => {
return els.map((el) => el.getAttribute("data-workspace") ?? "").filter((x) => x.length > 0)
})
const rootSlug = slugs.find((slug) => key(slug) === pinnedKey)
if (!rootSlug) throw new Error("Could not find pinned workspace in original project")

const rootMenu = await openWorkspaceMenu(page, rootSlug)
await expect(
rootMenu
.getByRole("menuitem")
.filter({ hasText: /^Unpin$/i })
.first(),
).toBeVisible()
},
{ extra: [other] },
)
} finally {
await Promise.all(dirs.map((dir) => cleanupTestProject(dir)))
await cleanupTestProject(other)
}
})

test("workspace divider is shown only with mixed pin state", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })

await withProject(async ({ slug: rootSlug }) => {
await ensureWorkspacesEnabled(page, rootSlug)

const workspaces = [] as string[]
try {
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)

workspaces.push(slugFromUrl(page.url()))
await openSidebar(page)
}

const a = workspaces[0]
const b = workspaces[1]
if (!a || !b) throw new Error("Expected two created workspaces")

await setWorkspacePinned(page, rootSlug, false)
await setWorkspacePinned(page, a, true)
await setWorkspacePinned(page, b, false)
await expect.poll(async () => await page.locator(workspaceDividerSelector).count()).toBeGreaterThan(0)

await setWorkspacePinned(page, a, false)
await expect.poll(async () => await page.locator(workspaceDividerSelector).count()).toBe(0)
} finally {
await Promise.all(workspaces.map((slug) => cleanupTestProject(base64Decode(slug))))
}
})
})

test("can reorder workspaces by drag and drop", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })
await withProject(async ({ slug: rootSlug }) => {
Expand Down
5 changes: 5 additions & 0 deletions packages/app/e2e/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,14 @@ export const sessionItemSelector = (sessionID: string) => `${sidebarNavSelector}
export const workspaceItemSelector = (slug: string) =>
`${sidebarNavSelector} [data-component="workspace-item"][data-workspace="${slug}"]`

export const workspaceDividerSelector = `${sidebarNavSelector} [data-component="workspace-item"][data-workspace-divider="true"]`

export const workspaceMenuTriggerSelector = (slug: string) =>
`${sidebarNavSelector} [data-action="workspace-menu"][data-workspace="${slug}"]`

export const workspacePinToggleSelector = (slug: string) =>
`[data-action="workspace-pin-toggle"][data-workspace="${slug}"]`

export const workspaceNewSessionSelector = (slug: string) =>
`${sidebarNavSelector} [data-action="workspace-new-session"][data-workspace="${slug}"]`

Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,8 @@ export const dict = {
"session.delete.button": "Delete session",

"workspace.new": "New workspace",
"workspace.pin": "Pin",
"workspace.unpin": "Unpin",
"workspace.type.local": "local",
"workspace.type.sandbox": "sandbox",
"workspace.create.failed.title": "Failed to create workspace",
Expand Down
Loading
Loading