Skip to content

Commit c559af5

Browse files
authored
test(app): migrate more e2e suites to isolated backend (#20505)
1 parent d1e0a46 commit c559af5

8 files changed

Lines changed: 630 additions & 560 deletions

packages/app/e2e/prompt/prompt-async.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { test, expect } from "../fixtures"
22
import { promptSelector } from "../selectors"
33
import { assistantText, sessionIDFromUrl, withSession } from "../actions"
4-
import { openaiModel, promptMatch, withMockOpenAI } from "./mock"
4+
import { openaiModel, promptMatch, titleMatch, withMockOpenAI } from "./mock"
55

66
const text = (value: string | null) => (value ?? "").replace(/\u200B/g, "").trim()
77

@@ -24,6 +24,7 @@ test("prompt succeeds when sync message endpoint is unreachable", async ({
2424
llmUrl: llm.url,
2525
fn: async () => {
2626
const token = `E2E_ASYNC_${Date.now()}`
27+
await llm.textMatch(titleMatch, "E2E Title")
2728
await llm.textMatch(promptMatch(token), token)
2829

2930
await withBackendProject(
Lines changed: 141 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import type { ToolPart } from "@opencode-ai/sdk/v2/client"
22
import type { Page } from "@playwright/test"
33
import { test, expect } from "../fixtures"
4-
import { withSession } from "../actions"
4+
import { assistantText, sessionIDFromUrl } from "../actions"
55
import { promptSelector } from "../selectors"
6+
import { openaiModel, promptMatch, titleMatch, withMockOpenAI } from "./mock"
67

78
const text = (value: string | null) => (value ?? "").replace(/\u200B/g, "").trim()
89

@@ -43,20 +44,13 @@ async function wait(page: Page, value: string) {
4344
await expect.poll(async () => text(await page.locator(promptSelector).textContent())).toBe(value)
4445
}
4546

46-
async function reply(sdk: Parameters<typeof withSession>[0], sessionID: string, token: string) {
47+
async function reply(
48+
sdk: { session: { messages: Parameters<typeof assistantText>[0]["session"] } },
49+
sessionID: string,
50+
token: string,
51+
) {
4752
await expect
48-
.poll(
49-
async () => {
50-
const messages = await sdk.session.messages({ sessionID, limit: 50 }).then((r) => r.data ?? [])
51-
return messages
52-
.filter((item) => item.info.role === "assistant")
53-
.flatMap((item) => item.parts)
54-
.filter((item) => item.type === "text")
55-
.map((item) => item.text)
56-
.join("\n")
57-
},
58-
{ timeout: 90_000 },
59-
)
53+
.poll(() => assistantText(sdk as Parameters<typeof assistantText>[0], sessionID), { timeout: 90_000 })
6054
.toContain(token)
6155
}
6256

@@ -79,106 +73,145 @@ async function shell(sdk: Parameters<typeof withSession>[0], sessionID: string,
7973
.toContain(token)
8074
}
8175

82-
test("prompt history restores unsent draft with arrow navigation", async ({ page, sdk, gotoSession }) => {
76+
test("prompt history restores unsent draft with arrow navigation", async ({
77+
page,
78+
llm,
79+
backend,
80+
withBackendProject,
81+
}) => {
8382
test.setTimeout(120_000)
8483

85-
await withSession(sdk, `e2e prompt history ${Date.now()}`, async (session) => {
86-
await gotoSession(session.id)
87-
88-
const prompt = page.locator(promptSelector)
89-
const firstToken = `E2E_HISTORY_ONE_${Date.now()}`
90-
const secondToken = `E2E_HISTORY_TWO_${Date.now()}`
91-
const first = `Reply with exactly: ${firstToken}`
92-
const second = `Reply with exactly: ${secondToken}`
93-
const draft = `draft ${Date.now()}`
94-
95-
await prompt.click()
96-
await page.keyboard.type(first)
97-
await page.keyboard.press("Enter")
98-
await wait(page, "")
99-
await reply(sdk, session.id, firstToken)
100-
101-
await prompt.click()
102-
await page.keyboard.type(second)
103-
await page.keyboard.press("Enter")
104-
await wait(page, "")
105-
await reply(sdk, session.id, secondToken)
106-
107-
await prompt.click()
108-
await page.keyboard.type(draft)
109-
await wait(page, draft)
110-
111-
// Clear the draft before navigating history (ArrowUp only works when prompt is empty)
112-
await prompt.fill("")
113-
await wait(page, "")
114-
115-
await page.keyboard.press("ArrowUp")
116-
await wait(page, second)
117-
118-
await page.keyboard.press("ArrowUp")
119-
await wait(page, first)
120-
121-
await page.keyboard.press("ArrowDown")
122-
await wait(page, second)
123-
124-
await page.keyboard.press("ArrowDown")
125-
await wait(page, "")
84+
await withMockOpenAI({
85+
serverUrl: backend.url,
86+
llmUrl: llm.url,
87+
fn: async () => {
88+
const firstToken = `E2E_HISTORY_ONE_${Date.now()}`
89+
const secondToken = `E2E_HISTORY_TWO_${Date.now()}`
90+
const first = `Reply with exactly: ${firstToken}`
91+
const second = `Reply with exactly: ${secondToken}`
92+
const draft = `draft ${Date.now()}`
93+
94+
await llm.textMatch(titleMatch, "E2E Title")
95+
await llm.textMatch(promptMatch(firstToken), firstToken)
96+
await llm.textMatch(promptMatch(secondToken), secondToken)
97+
98+
await withBackendProject(
99+
async (project) => {
100+
const prompt = page.locator(promptSelector)
101+
102+
await prompt.click()
103+
await page.keyboard.type(first)
104+
await page.keyboard.press("Enter")
105+
await wait(page, "")
106+
107+
await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 })
108+
const sessionID = sessionIDFromUrl(page.url())!
109+
project.trackSession(sessionID)
110+
await reply(project.sdk, sessionID, firstToken)
111+
112+
await prompt.click()
113+
await page.keyboard.type(second)
114+
await page.keyboard.press("Enter")
115+
await wait(page, "")
116+
await reply(project.sdk, sessionID, secondToken)
117+
118+
await prompt.click()
119+
await page.keyboard.type(draft)
120+
await wait(page, draft)
121+
122+
await prompt.fill("")
123+
await wait(page, "")
124+
125+
await page.keyboard.press("ArrowUp")
126+
await wait(page, second)
127+
128+
await page.keyboard.press("ArrowUp")
129+
await wait(page, first)
130+
131+
await page.keyboard.press("ArrowDown")
132+
await wait(page, second)
133+
134+
await page.keyboard.press("ArrowDown")
135+
await wait(page, "")
136+
},
137+
{
138+
model: openaiModel,
139+
},
140+
)
141+
},
126142
})
127143
})
128144

129-
test("shell history stays separate from normal prompt history", async ({ page, sdk, gotoSession }) => {
145+
test("shell history stays separate from normal prompt history", async ({ page, llm, backend, withBackendProject }) => {
130146
test.setTimeout(120_000)
131147

132-
await withSession(sdk, `e2e shell history ${Date.now()}`, async (session) => {
133-
await gotoSession(session.id)
134-
135-
const prompt = page.locator(promptSelector)
136-
const firstToken = `E2E_SHELL_ONE_${Date.now()}`
137-
const secondToken = `E2E_SHELL_TWO_${Date.now()}`
138-
const normalToken = `E2E_NORMAL_${Date.now()}`
139-
const first = `echo ${firstToken}`
140-
const second = `echo ${secondToken}`
141-
const normal = `Reply with exactly: ${normalToken}`
142-
143-
await prompt.click()
144-
await page.keyboard.type("!")
145-
await page.keyboard.type(first)
146-
await page.keyboard.press("Enter")
147-
await wait(page, "")
148-
await shell(sdk, session.id, first, firstToken)
149-
150-
await prompt.click()
151-
await page.keyboard.type("!")
152-
await page.keyboard.type(second)
153-
await page.keyboard.press("Enter")
154-
await wait(page, "")
155-
await shell(sdk, session.id, second, secondToken)
156-
157-
await prompt.click()
158-
await page.keyboard.type("!")
159-
await page.keyboard.press("ArrowUp")
160-
await wait(page, second)
161-
162-
await page.keyboard.press("ArrowUp")
163-
await wait(page, first)
164-
165-
await page.keyboard.press("ArrowDown")
166-
await wait(page, second)
167-
168-
await page.keyboard.press("ArrowDown")
169-
await wait(page, "")
170-
171-
await page.keyboard.press("Escape")
172-
await wait(page, "")
173-
174-
await prompt.click()
175-
await page.keyboard.type(normal)
176-
await page.keyboard.press("Enter")
177-
await wait(page, "")
178-
await reply(sdk, session.id, normalToken)
179-
180-
await prompt.click()
181-
await page.keyboard.press("ArrowUp")
182-
await wait(page, normal)
148+
await withMockOpenAI({
149+
serverUrl: backend.url,
150+
llmUrl: llm.url,
151+
fn: async () => {
152+
const firstToken = `E2E_SHELL_ONE_${Date.now()}`
153+
const secondToken = `E2E_SHELL_TWO_${Date.now()}`
154+
const normalToken = `E2E_NORMAL_${Date.now()}`
155+
const first = `echo ${firstToken}`
156+
const second = `echo ${secondToken}`
157+
const normal = `Reply with exactly: ${normalToken}`
158+
159+
await llm.textMatch(titleMatch, "E2E Title")
160+
await llm.textMatch(promptMatch(normalToken), normalToken)
161+
162+
await withBackendProject(
163+
async (project) => {
164+
const prompt = page.locator(promptSelector)
165+
166+
await prompt.click()
167+
await page.keyboard.type("!")
168+
await page.keyboard.type(first)
169+
await page.keyboard.press("Enter")
170+
await wait(page, "")
171+
172+
await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 })
173+
const sessionID = sessionIDFromUrl(page.url())!
174+
project.trackSession(sessionID)
175+
await shell(project.sdk, sessionID, first, firstToken)
176+
177+
await prompt.click()
178+
await page.keyboard.type("!")
179+
await page.keyboard.type(second)
180+
await page.keyboard.press("Enter")
181+
await wait(page, "")
182+
await shell(project.sdk, sessionID, second, secondToken)
183+
184+
await prompt.click()
185+
await page.keyboard.type("!")
186+
await page.keyboard.press("ArrowUp")
187+
await wait(page, second)
188+
189+
await page.keyboard.press("ArrowUp")
190+
await wait(page, first)
191+
192+
await page.keyboard.press("ArrowDown")
193+
await wait(page, second)
194+
195+
await page.keyboard.press("ArrowDown")
196+
await wait(page, "")
197+
198+
await page.keyboard.press("Escape")
199+
await wait(page, "")
200+
201+
await prompt.click()
202+
await page.keyboard.type(normal)
203+
await page.keyboard.press("Enter")
204+
await wait(page, "")
205+
await reply(project.sdk, sessionID, normalToken)
206+
207+
await prompt.click()
208+
await page.keyboard.press("ArrowUp")
209+
await wait(page, normal)
210+
},
211+
{
212+
model: openaiModel,
213+
},
214+
)
215+
},
183216
})
184217
})

packages/app/e2e/prompt/prompt-shell.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ const isBash = (part: unknown): part is ToolPart => {
1010
return "state" in part
1111
}
1212

13-
test("shell mode runs a command in the project directory", async ({ page, withProject }) => {
13+
test("shell mode runs a command in the project directory", async ({ page, withBackendProject }) => {
1414
test.setTimeout(120_000)
1515

16-
await withProject(async ({ directory, gotoSession, trackSession, sdk }) => {
16+
await withBackendProject(async ({ directory, gotoSession, trackSession, sdk }) => {
1717
const prompt = page.locator(promptSelector)
1818
const cmd = process.platform === "win32" ? "dir" : "command ls"
1919

packages/app/e2e/prompt/prompt-slash-share.spec.ts

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,43 +22,45 @@ async function seed(sdk: Parameters<typeof withSession>[0], sessionID: string) {
2222
.toBeGreaterThan(0)
2323
}
2424

25-
test("/share and /unshare update session share state", async ({ page, sdk, gotoSession }) => {
25+
test("/share and /unshare update session share state", async ({ page, withBackendProject }) => {
2626
test.skip(shareDisabled, "Share is disabled in this environment (OPENCODE_DISABLE_SHARE).")
2727

28-
await withSession(sdk, `e2e slash share ${Date.now()}`, async (session) => {
29-
const prompt = page.locator(promptSelector)
28+
await withBackendProject(async (project) => {
29+
await withSession(project.sdk, `e2e slash share ${Date.now()}`, async (session) => {
30+
const prompt = page.locator(promptSelector)
3031

31-
await seed(sdk, session.id)
32-
await gotoSession(session.id)
32+
await seed(project.sdk, session.id)
33+
await project.gotoSession(session.id)
3334

34-
await prompt.click()
35-
await page.keyboard.type("/share")
36-
await expect(page.locator('[data-slash-id="session.share"]').first()).toBeVisible()
37-
await page.keyboard.press("Enter")
35+
await prompt.click()
36+
await page.keyboard.type("/share")
37+
await expect(page.locator('[data-slash-id="session.share"]').first()).toBeVisible()
38+
await page.keyboard.press("Enter")
3839

39-
await expect
40-
.poll(
41-
async () => {
42-
const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data)
43-
return data?.share?.url || undefined
44-
},
45-
{ timeout: 30_000 },
46-
)
47-
.not.toBeUndefined()
40+
await expect
41+
.poll(
42+
async () => {
43+
const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data)
44+
return data?.share?.url || undefined
45+
},
46+
{ timeout: 30_000 },
47+
)
48+
.not.toBeUndefined()
4849

49-
await prompt.click()
50-
await page.keyboard.type("/unshare")
51-
await expect(page.locator('[data-slash-id="session.unshare"]').first()).toBeVisible()
52-
await page.keyboard.press("Enter")
50+
await prompt.click()
51+
await page.keyboard.type("/unshare")
52+
await expect(page.locator('[data-slash-id="session.unshare"]').first()).toBeVisible()
53+
await page.keyboard.press("Enter")
5354

54-
await expect
55-
.poll(
56-
async () => {
57-
const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data)
58-
return data?.share?.url || undefined
59-
},
60-
{ timeout: 30_000 },
61-
)
62-
.toBeUndefined()
55+
await expect
56+
.poll(
57+
async () => {
58+
const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data)
59+
return data?.share?.url || undefined
60+
},
61+
{ timeout: 30_000 },
62+
)
63+
.toBeUndefined()
64+
})
6365
})
6466
})

0 commit comments

Comments
 (0)