Skip to content

Commit d8f2d64

Browse files
Apply PR #11386: test(app): session actions
2 parents 124ec61 + c71a015 commit d8f2d64

18 files changed

+313
-106
lines changed

packages/app/e2e/actions.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ import os from "node:os"
44
import path from "node:path"
55
import { execSync } from "node:child_process"
66
import { modKey, serverUrl } from "./utils"
7+
import {
8+
sessionItemSelector,
9+
dropdownMenuTriggerSelector,
10+
dropdownMenuContentSelector,
11+
titlebarRightSelector,
12+
popoverBodySelector,
13+
listItemSelector,
14+
listItemKeySelector,
15+
listItemKeyStartsWithSelector,
16+
} from "./selectors"
17+
import type { createSdk } from "./utils"
718

819
export async function defocus(page: Page) {
920
await page.mouse.click(5, 5)
@@ -158,3 +169,103 @@ export function sessionIDFromUrl(url: string) {
158169
const match = /\/session\/([^/?#]+)/.exec(url)
159170
return match?.[1]
160171
}
172+
173+
export async function hoverSessionItem(page: Page, sessionID: string) {
174+
const sessionEl = page.locator(sessionItemSelector(sessionID)).first()
175+
await expect(sessionEl).toBeVisible()
176+
await sessionEl.hover()
177+
return sessionEl
178+
}
179+
180+
export async function openSessionMoreMenu(page: Page, sessionID: string) {
181+
const sessionEl = await hoverSessionItem(page, sessionID)
182+
183+
const menuTrigger = sessionEl.locator(dropdownMenuTriggerSelector).first()
184+
await expect(menuTrigger).toBeVisible()
185+
await menuTrigger.click()
186+
187+
const menu = page.locator(dropdownMenuContentSelector).first()
188+
await expect(menu).toBeVisible()
189+
return menu
190+
}
191+
192+
export async function clickMenuItem(menu: Locator, itemName: string | RegExp, options?: { force?: boolean }) {
193+
const item = menu.getByRole("menuitem").filter({ hasText: itemName }).first()
194+
await expect(item).toBeVisible()
195+
await item.click({ force: options?.force })
196+
}
197+
198+
export async function confirmDialog(page: Page, buttonName: string | RegExp) {
199+
const dialog = page.getByRole("dialog").first()
200+
await expect(dialog).toBeVisible()
201+
202+
const button = dialog.getByRole("button").filter({ hasText: buttonName }).first()
203+
await expect(button).toBeVisible()
204+
await button.click()
205+
}
206+
207+
export async function openSharePopover(page: Page) {
208+
const rightSection = page.locator(titlebarRightSelector)
209+
const shareButton = rightSection.getByRole("button", { name: "Share" }).first()
210+
await expect(shareButton).toBeVisible()
211+
212+
const popoverBody = page
213+
.locator(popoverBodySelector)
214+
.filter({ has: page.getByRole("button", { name: /^(Publish|Unpublish)$/ }) })
215+
.first()
216+
217+
const opened = await popoverBody
218+
.isVisible()
219+
.then((x) => x)
220+
.catch(() => false)
221+
222+
if (!opened) {
223+
await shareButton.click()
224+
await expect(popoverBody).toBeVisible()
225+
}
226+
return { rightSection, popoverBody }
227+
}
228+
229+
export async function clickPopoverButton(page: Page, buttonName: string | RegExp) {
230+
const button = page.getByRole("button").filter({ hasText: buttonName }).first()
231+
await expect(button).toBeVisible()
232+
await button.click()
233+
}
234+
235+
export async function clickListItem(
236+
container: Locator | Page,
237+
filter: string | RegExp | { key?: string; text?: string | RegExp; keyStartsWith?: string },
238+
): Promise<Locator> {
239+
let item: Locator
240+
241+
if (typeof filter === "string" || filter instanceof RegExp) {
242+
item = container.locator(listItemSelector).filter({ hasText: filter }).first()
243+
} else if (filter.keyStartsWith) {
244+
item = container.locator(listItemKeyStartsWithSelector(filter.keyStartsWith)).first()
245+
} else if (filter.key) {
246+
item = container.locator(listItemKeySelector(filter.key)).first()
247+
} else if (filter.text) {
248+
item = container.locator(listItemSelector).filter({ hasText: filter.text }).first()
249+
} else {
250+
throw new Error("Invalid filter provided to clickListItem")
251+
}
252+
253+
await expect(item).toBeVisible()
254+
await item.click()
255+
return item
256+
}
257+
258+
export async function withSession<T>(
259+
sdk: ReturnType<typeof createSdk>,
260+
title: string,
261+
callback: (session: { id: string; title: string }) => Promise<T>,
262+
): Promise<T> {
263+
const session = await sdk.session.create({ title }).then((r) => r.data)
264+
if (!session?.id) throw new Error("Session create did not return an id")
265+
266+
try {
267+
return await callback(session)
268+
} finally {
269+
await sdk.session.delete({ sessionID: session.id }).catch(() => undefined)
270+
}
271+
}

packages/app/e2e/app/server-default.spec.ts

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { test, expect } from "../fixtures"
22
import { serverName, serverUrl } from "../utils"
3+
import { clickListItem, closeDialog, clickMenuItem } from "../actions"
34

45
const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl"
56

@@ -33,31 +34,18 @@ test("can set a default server on web", async ({ page, gotoSession }) => {
3334
const row = dialog.locator('[data-slot="list-item"]').filter({ hasText: serverName }).first()
3435
await expect(row).toBeVisible()
3536

36-
const menu = row.locator('[data-component="icon-button"]').last()
37-
await menu.click()
38-
await page.getByRole("menuitem", { name: "Set as default" }).click()
37+
const menuTrigger = row.locator('[data-slot="dropdown-menu-trigger"]').first()
38+
await expect(menuTrigger).toBeVisible()
39+
await menuTrigger.click({ force: true })
40+
41+
const menu = page.locator('[data-component="dropdown-menu-content"]').first()
42+
await expect(menu).toBeVisible()
43+
await clickMenuItem(menu, /set as default/i)
3944

4045
await expect.poll(() => page.evaluate((key) => localStorage.getItem(key), DEFAULT_SERVER_URL_KEY)).toBe(serverUrl)
4146
await expect(row.getByText("Default", { exact: true })).toBeVisible()
4247

43-
await page.keyboard.press("Escape")
44-
const closed = await dialog
45-
.waitFor({ state: "detached", timeout: 1500 })
46-
.then(() => true)
47-
.catch(() => false)
48-
49-
if (!closed) {
50-
await page.keyboard.press("Escape")
51-
const closedSecond = await dialog
52-
.waitFor({ state: "detached", timeout: 1500 })
53-
.then(() => true)
54-
.catch(() => false)
55-
56-
if (!closedSecond) {
57-
await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } })
58-
await expect(dialog).toHaveCount(0)
59-
}
60-
}
48+
await closeDialog(page, dialog)
6149

6250
await ensurePopoverOpen()
6351

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,16 @@
11
import { test, expect } from "../fixtures"
22
import { promptSelector } from "../selectors"
3+
import { withSession } from "../actions"
34

45
test("can open an existing session and type into the prompt", async ({ page, sdk, gotoSession }) => {
56
const title = `e2e smoke ${Date.now()}`
6-
const created = await sdk.session.create({ title }).then((r) => r.data)
77

8-
if (!created?.id) throw new Error("Session create did not return an id")
9-
const sessionID = created.id
10-
11-
try {
12-
await gotoSession(sessionID)
8+
await withSession(sdk, title, async (session) => {
9+
await gotoSession(session.id)
1310

1411
const prompt = page.locator(promptSelector)
1512
await prompt.click()
1613
await page.keyboard.type("hello from e2e")
1714
await expect(prompt).toContainText("hello from e2e")
18-
} finally {
19-
await sdk.session.delete({ sessionID }).catch(() => undefined)
20-
}
15+
})
2116
})
Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,42 @@
11
import { test, expect } from "../fixtures"
2-
import { openSidebar } from "../actions"
2+
import { openSidebar, withSession } from "../actions"
33
import { promptSelector } from "../selectors"
44

55
test("titlebar back/forward navigates between sessions", async ({ page, slug, sdk, gotoSession }) => {
66
await page.setViewportSize({ width: 1400, height: 800 })
77

88
const stamp = Date.now()
9-
const one = await sdk.session.create({ title: `e2e titlebar history 1 ${stamp}` }).then((r) => r.data)
10-
const two = await sdk.session.create({ title: `e2e titlebar history 2 ${stamp}` }).then((r) => r.data)
119

12-
if (!one?.id) throw new Error("Session create did not return an id")
13-
if (!two?.id) throw new Error("Session create did not return an id")
10+
await withSession(sdk, `e2e titlebar history 1 ${stamp}`, async (one) => {
11+
await withSession(sdk, `e2e titlebar history 2 ${stamp}`, async (two) => {
12+
await gotoSession(one.id)
1413

15-
try {
16-
await gotoSession(one.id)
14+
await openSidebar(page)
1715

18-
await openSidebar(page)
16+
const link = page.locator(`[data-session-id="${two.id}"] a`).first()
17+
await expect(link).toBeVisible()
18+
await link.scrollIntoViewIfNeeded()
19+
await link.click()
1920

20-
const link = page.locator(`[data-session-id="${two.id}"] a`).first()
21-
await expect(link).toBeVisible()
22-
await link.scrollIntoViewIfNeeded()
23-
await link.click()
21+
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
22+
await expect(page.locator(promptSelector)).toBeVisible()
2423

25-
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
26-
await expect(page.locator(promptSelector)).toBeVisible()
24+
const back = page.getByRole("button", { name: "Back" })
25+
const forward = page.getByRole("button", { name: "Forward" })
2726

28-
const back = page.getByRole("button", { name: "Back" })
29-
const forward = page.getByRole("button", { name: "Forward" })
27+
await expect(back).toBeVisible()
28+
await expect(back).toBeEnabled()
29+
await back.click()
3030

31-
await expect(back).toBeVisible()
32-
await expect(back).toBeEnabled()
33-
await back.click()
31+
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${one.id}(?:\\?|#|$)`))
32+
await expect(page.locator(promptSelector)).toBeVisible()
3433

35-
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${one.id}(?:\\?|#|$)`))
36-
await expect(page.locator(promptSelector)).toBeVisible()
34+
await expect(forward).toBeVisible()
35+
await expect(forward).toBeEnabled()
36+
await forward.click()
3737

38-
await expect(forward).toBeVisible()
39-
await expect(forward).toBeEnabled()
40-
await forward.click()
41-
42-
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
43-
await expect(page.locator(promptSelector)).toBeVisible()
44-
} finally {
45-
await sdk.session.delete({ sessionID: one.id }).catch(() => undefined)
46-
await sdk.session.delete({ sessionID: two.id }).catch(() => undefined)
47-
}
38+
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
39+
await expect(page.locator(promptSelector)).toBeVisible()
40+
})
41+
})
4842
})

packages/app/e2e/files/file-open.spec.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { test, expect } from "../fixtures"
2-
import { openPalette } from "../actions"
2+
import { openPalette, clickListItem } from "../actions"
33

44
test("can open a file tab from the search palette", async ({ page, gotoSession }) => {
55
await gotoSession()
@@ -9,9 +9,7 @@ test("can open a file tab from the search palette", async ({ page, gotoSession }
99
const input = dialog.getByRole("textbox").first()
1010
await input.fill("package.json")
1111

12-
const fileItem = dialog.locator('[data-slot="list-item"][data-key^="file:"]').first()
13-
await expect(fileItem).toBeVisible()
14-
await fileItem.click()
12+
await clickListItem(dialog, { keyStartsWith: "file:" })
1513

1614
await expect(dialog).toHaveCount(0)
1715

packages/app/e2e/files/file-viewer.spec.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { test, expect } from "../fixtures"
2-
import { openPalette } from "../actions"
2+
import { openPalette, clickListItem } from "../actions"
33

44
test("smoke file viewer renders real file content", async ({ page, gotoSession }) => {
55
await gotoSession()
@@ -12,13 +12,7 @@ test("smoke file viewer renders real file content", async ({ page, gotoSession }
1212
const input = dialog.getByRole("textbox").first()
1313
await input.fill(file)
1414

15-
const fileItem = dialog
16-
.locator(
17-
'[data-slot="list-item"][data-key^="file:"][data-key*="packages"][data-key*="app"][data-key$="package.json"]',
18-
)
19-
.first()
20-
await expect(fileItem).toBeVisible()
21-
await fileItem.click()
15+
await clickListItem(dialog, { text: /packages.*app.*package.json/ })
2216

2317
await expect(dialog).toHaveCount(0)
2418

packages/app/e2e/models/model-picker.spec.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { test, expect } from "../fixtures"
22
import { promptSelector } from "../selectors"
3+
import { clickListItem } from "../actions"
34

45
test("smoke model selection updates prompt footer", async ({ page, gotoSession }) => {
56
await gotoSession()
@@ -32,9 +33,7 @@ test("smoke model selection updates prompt footer", async ({ page, gotoSession }
3233

3334
await input.fill(model)
3435

35-
const item = dialog.locator(`[data-slot="list-item"][data-key="${key}"]`)
36-
await expect(item).toBeVisible()
37-
await item.click()
36+
await clickListItem(dialog, { key })
3837

3938
await expect(dialog).toHaveCount(0)
4039

packages/app/e2e/models/models-visibility.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { test, expect } from "../fixtures"
22
import { promptSelector } from "../selectors"
3-
import { closeDialog, openSettings } from "../actions"
3+
import { closeDialog, openSettings, clickListItem } from "../actions"
44

55
test("hiding a model removes it from the model picker", async ({ page, gotoSession }) => {
66
await gotoSession()

packages/app/e2e/projects/project-edit.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ test("dialog edit project updates name and startup script", async ({ page, gotoS
1414
await expect(trigger).toBeVisible()
1515
await trigger.click({ force: true })
1616

17-
await page.getByRole("menuitem", { name: "Edit" }).click()
17+
const menu = page.locator('[data-component="dropdown-menu-content"]').first()
18+
await expect(menu).toBeVisible()
19+
20+
const editItem = menu.getByRole("menuitem", { name: "Edit" }).first()
21+
await expect(editItem).toBeVisible()
22+
await editItem.click({ force: true })
1823

1924
const dialog = page.getByRole("dialog")
2025
await expect(dialog).toBeVisible()

packages/app/e2e/projects/projects-close.spec.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { test, expect } from "../fixtures"
2-
import { createTestProject, seedProjects, cleanupTestProject, openSidebar } from "../actions"
2+
import { createTestProject, seedProjects, cleanupTestProject, openSidebar, clickMenuItem } from "../actions"
33
import { projectCloseHoverSelector, projectCloseMenuSelector, projectSwitchSelector } from "../selectors"
44
import { dirSlug } from "../utils"
55

@@ -33,7 +33,7 @@ test("can close a project via project header more options menu", async ({ page,
3333
await page.setViewportSize({ width: 1400, height: 800 })
3434

3535
const other = await createTestProject()
36-
const otherName = other.split("/").pop()
36+
const otherName = other.split("/").pop() ?? other
3737
const otherSlug = dirSlug(other)
3838
await seedProjects(page, { directory, extra: [other] })
3939

@@ -59,17 +59,10 @@ test("can close a project via project header more options menu", async ({ page,
5959
await trigger.focus()
6060
await page.keyboard.press("Enter")
6161

62-
const close = page
63-
.locator(projectCloseMenuSelector(otherSlug))
64-
.or(page.getByRole("menuitem", { name: "Close" }))
65-
.or(
66-
page
67-
.locator('[data-component="dropdown-menu-content"] [data-slot="dropdown-menu-item"]')
68-
.filter({ hasText: "Close" }),
69-
)
70-
.first()
71-
await expect(close).toBeVisible({ timeout: 10_000 })
72-
await close.click({ force: true })
62+
const menu = page.locator('[data-component="dropdown-menu-content"]').first()
63+
await expect(menu).toBeVisible({ timeout: 10_000 })
64+
65+
await clickMenuItem(menu, /^Close$/i, { force: true })
7366
await expect(otherButton).toHaveCount(0)
7467
} finally {
7568
await cleanupTestProject(other)

0 commit comments

Comments
 (0)