Skip to content

Commit 6f1d260

Browse files
markijbemaAlorse
authored andcommitted
Close the local browser when used as fallback for remote (RooCodeInc#4823)
1 parent b3d3956 commit 6f1d260

File tree

3 files changed

+245
-5
lines changed

3 files changed

+245
-5
lines changed

.changeset/curly-states-see.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"roo-cline": patch
3+
---
4+
5+
Close the browsertool properly when a remote browser is configured but a fallback local one is used

src/services/browser/BrowserSession.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export class BrowserSession {
2121
private page?: Page
2222
private currentMousePosition?: string
2323
private lastConnectionAttempt?: number
24+
private isUsingRemoteBrowser: boolean = false
2425

2526
constructor(context: vscode.ExtensionContext) {
2627
this.context = context
@@ -70,6 +71,7 @@ export class BrowserSession {
7071
defaultViewport: this.getViewport(),
7172
// headless: false,
7273
})
74+
this.isUsingRemoteBrowser = false
7375
}
7476

7577
/**
@@ -86,6 +88,7 @@ export class BrowserSession {
8688
console.log(`Connected to remote browser at ${chromeHostUrl}`)
8789
this.context.globalState.update("cachedChromeHostUrl", chromeHostUrl)
8890
this.lastConnectionAttempt = Date.now()
91+
this.isUsingRemoteBrowser = true
8992

9093
return true
9194
} catch (error) {
@@ -192,15 +195,12 @@ export class BrowserSession {
192195
if (this.browser || this.page) {
193196
console.log("closing browser...")
194197

195-
const remoteBrowserEnabled = this.context.globalState.get("remoteBrowserEnabled") as boolean | undefined
196-
if (remoteBrowserEnabled && this.browser) {
198+
if (this.isUsingRemoteBrowser && this.browser) {
197199
await this.browser.disconnect().catch(() => {})
198200
} else {
199201
await this.browser?.close().catch(() => {})
200-
this.resetBrowserState()
201202
}
202-
203-
// this.resetBrowserState()
203+
this.resetBrowserState()
204204
}
205205
return {}
206206
}
@@ -212,6 +212,7 @@ export class BrowserSession {
212212
this.browser = undefined
213213
this.page = undefined
214214
this.currentMousePosition = undefined
215+
this.isUsingRemoteBrowser = false
215216
}
216217

217218
async doAction(action: (page: Page) => Promise<void>): Promise<BrowserActionResult> {
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
// npx vitest services/browser/__tests__/BrowserSession.spec.ts
2+
3+
import { describe, it, expect, vi, beforeEach } from "vitest"
4+
import { BrowserSession } from "../BrowserSession"
5+
import { discoverChromeHostUrl, tryChromeHostUrl } from "../browserDiscovery"
6+
import { fileExistsAtPath } from "../../../utils/fs"
7+
8+
// Mock dependencies
9+
vi.mock("vscode", () => ({
10+
ExtensionContext: vi.fn(),
11+
Uri: {
12+
file: vi.fn((path) => ({ fsPath: path })),
13+
},
14+
}))
15+
16+
// Mock puppeteer-core
17+
vi.mock("puppeteer-core", () => {
18+
const mockBrowser = {
19+
newPage: vi.fn().mockResolvedValue({
20+
goto: vi.fn().mockResolvedValue(undefined),
21+
on: vi.fn(),
22+
off: vi.fn(),
23+
screenshot: vi.fn().mockResolvedValue("mockScreenshotBase64"),
24+
url: vi.fn().mockReturnValue("https://example.com"),
25+
}),
26+
pages: vi.fn().mockResolvedValue([]),
27+
close: vi.fn().mockResolvedValue(undefined),
28+
disconnect: vi.fn().mockResolvedValue(undefined),
29+
}
30+
31+
return {
32+
Browser: vi.fn(),
33+
Page: vi.fn(),
34+
TimeoutError: class TimeoutError extends Error {},
35+
launch: vi.fn().mockResolvedValue(mockBrowser),
36+
connect: vi.fn().mockResolvedValue(mockBrowser),
37+
}
38+
})
39+
40+
// Mock PCR
41+
vi.mock("puppeteer-chromium-resolver", () => {
42+
return {
43+
default: vi.fn().mockResolvedValue({
44+
puppeteer: {
45+
launch: vi.fn().mockImplementation(async () => {
46+
const { launch } = await import("puppeteer-core")
47+
return launch()
48+
}),
49+
},
50+
executablePath: "/mock/path/to/chromium",
51+
}),
52+
}
53+
})
54+
55+
// Mock fs
56+
vi.mock("fs/promises", () => ({
57+
mkdir: vi.fn().mockResolvedValue(undefined),
58+
readFile: vi.fn(),
59+
writeFile: vi.fn(),
60+
access: vi.fn(),
61+
}))
62+
63+
// Mock fileExistsAtPath
64+
vi.mock("../../../utils/fs", () => ({
65+
fileExistsAtPath: vi.fn().mockResolvedValue(false),
66+
}))
67+
68+
// Mock browser discovery functions
69+
vi.mock("../browserDiscovery", () => ({
70+
discoverChromeHostUrl: vi.fn().mockResolvedValue(null),
71+
tryChromeHostUrl: vi.fn().mockResolvedValue(false),
72+
}))
73+
74+
// Mock delay
75+
vi.mock("delay", () => ({
76+
default: vi.fn().mockResolvedValue(undefined),
77+
}))
78+
79+
// Mock p-wait-for
80+
vi.mock("p-wait-for", () => ({
81+
default: vi.fn().mockResolvedValue(undefined),
82+
}))
83+
84+
describe("BrowserSession", () => {
85+
let browserSession: BrowserSession
86+
let mockContext: any
87+
88+
beforeEach(() => {
89+
vi.clearAllMocks()
90+
91+
// Set up mock context
92+
mockContext = {
93+
globalState: {
94+
get: vi.fn(),
95+
update: vi.fn(),
96+
},
97+
globalStorageUri: {
98+
fsPath: "/mock/global/storage/path",
99+
},
100+
extensionUri: {
101+
fsPath: "/mock/extension/path",
102+
},
103+
}
104+
105+
// Create browser session
106+
browserSession = new BrowserSession(mockContext)
107+
})
108+
109+
describe("Remote browser disabled", () => {
110+
it("should launch a local browser when remote browser is disabled", async () => {
111+
// Mock context to indicate remote browser is disabled
112+
mockContext.globalState.get.mockImplementation((key: string) => {
113+
if (key === "remoteBrowserEnabled") return false
114+
return undefined
115+
})
116+
117+
await browserSession.launchBrowser()
118+
119+
const puppeteerCore = await import("puppeteer-core")
120+
121+
// Verify that a local browser was launched
122+
expect(puppeteerCore.launch).toHaveBeenCalled()
123+
124+
// Verify that remote browser connection was not attempted
125+
expect(discoverChromeHostUrl).not.toHaveBeenCalled()
126+
expect(tryChromeHostUrl).not.toHaveBeenCalled()
127+
128+
expect((browserSession as any).isUsingRemoteBrowser).toBe(false)
129+
})
130+
})
131+
132+
describe("Remote browser successfully connects", () => {
133+
it("should connect to a remote browser when enabled and connection succeeds", async () => {
134+
// Mock context to indicate remote browser is enabled
135+
mockContext.globalState.get.mockImplementation((key: string) => {
136+
if (key === "remoteBrowserEnabled") return true
137+
if (key === "remoteBrowserHost") return "http://remote-browser:9222"
138+
return undefined
139+
})
140+
141+
// Mock successful remote browser connection
142+
vi.mocked(tryChromeHostUrl).mockResolvedValue(true)
143+
144+
await browserSession.launchBrowser()
145+
146+
const puppeteerCore = await import("puppeteer-core")
147+
148+
// Verify that connect was called
149+
expect(puppeteerCore.connect).toHaveBeenCalled()
150+
151+
// Verify that local browser was not launched
152+
expect(puppeteerCore.launch).not.toHaveBeenCalled()
153+
154+
expect((browserSession as any).isUsingRemoteBrowser).toBe(true)
155+
})
156+
})
157+
158+
describe("Remote browser enabled but falls back to local", () => {
159+
it("should fall back to local browser when remote connection fails", async () => {
160+
// Mock context to indicate remote browser is enabled
161+
mockContext.globalState.get.mockImplementation((key: string) => {
162+
if (key === "remoteBrowserEnabled") return true
163+
if (key === "remoteBrowserHost") return "http://remote-browser:9222"
164+
return undefined
165+
})
166+
167+
// Mock failed remote browser connection
168+
vi.mocked(tryChromeHostUrl).mockResolvedValue(false)
169+
vi.mocked(discoverChromeHostUrl).mockResolvedValue(null)
170+
171+
await browserSession.launchBrowser()
172+
173+
// Import puppeteer-core to check if launch was called
174+
const puppeteerCore = await import("puppeteer-core")
175+
176+
// Verify that local browser was launched as fallback
177+
expect(puppeteerCore.launch).toHaveBeenCalled()
178+
179+
// Verify that isUsingRemoteBrowser is false
180+
expect((browserSession as any).isUsingRemoteBrowser).toBe(false)
181+
})
182+
})
183+
184+
describe("closeBrowser", () => {
185+
it("should close a local browser properly", async () => {
186+
const puppeteerCore = await import("puppeteer-core")
187+
188+
// Create a mock browser directly
189+
const mockBrowser = {
190+
newPage: vi.fn().mockResolvedValue({}),
191+
pages: vi.fn().mockResolvedValue([]),
192+
close: vi.fn().mockResolvedValue(undefined),
193+
disconnect: vi.fn().mockResolvedValue(undefined),
194+
}
195+
196+
// Set browser and page on the session
197+
;(browserSession as any).browser = mockBrowser
198+
;(browserSession as any).page = {}
199+
;(browserSession as any).isUsingRemoteBrowser = false
200+
201+
await browserSession.closeBrowser()
202+
203+
// Verify that browser.close was called
204+
expect(mockBrowser.close).toHaveBeenCalled()
205+
expect(mockBrowser.disconnect).not.toHaveBeenCalled()
206+
207+
// Verify that browser state was reset
208+
expect((browserSession as any).browser).toBeUndefined()
209+
expect((browserSession as any).page).toBeUndefined()
210+
expect((browserSession as any).isUsingRemoteBrowser).toBe(false)
211+
})
212+
213+
it("should disconnect from a remote browser properly", async () => {
214+
// Create a mock browser directly
215+
const mockBrowser = {
216+
newPage: vi.fn().mockResolvedValue({}),
217+
pages: vi.fn().mockResolvedValue([]),
218+
close: vi.fn().mockResolvedValue(undefined),
219+
disconnect: vi.fn().mockResolvedValue(undefined),
220+
}
221+
222+
// Set browser and page on the session
223+
;(browserSession as any).browser = mockBrowser
224+
;(browserSession as any).page = {}
225+
;(browserSession as any).isUsingRemoteBrowser = true
226+
227+
await browserSession.closeBrowser()
228+
229+
// Verify that browser.disconnect was called
230+
expect(mockBrowser.disconnect).toHaveBeenCalled()
231+
expect(mockBrowser.close).not.toHaveBeenCalled()
232+
})
233+
})
234+
})

0 commit comments

Comments
 (0)