add test for stagehand. Target.attachToTarget handling and better DCP types

This commit is contained in:
Tommy D. Rossi
2025-12-08 15:13:51 +01:00
parent e5630d76a3
commit d20cea7c49
8 changed files with 2096 additions and 78 deletions
+1
View File
@@ -26,6 +26,7 @@
"author": "Tommaso De Rossi, morse <beats.by.morse@gmail.com>", "author": "Tommaso De Rossi, morse <beats.by.morse@gmail.com>",
"license": "", "license": "",
"devDependencies": { "devDependencies": {
"@browserbasehq/stagehand": "^3.0.5",
"@mizchi/selector-generator": "1.50.0-next", "@mizchi/selector-generator": "1.50.0-next",
"@types/chrome": "^0.0.315", "@types/chrome": "^0.0.315",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
+28 -3
View File
@@ -1,26 +1,51 @@
import type { Protocol } from 'devtools-protocol'; import type { Protocol } from 'devtools-protocol';
import type { ProtocolMapping } from 'devtools-protocol/types/protocol-mapping.js'; import type { ProtocolMapping } from 'devtools-protocol/types/protocol-mapping.js';
export type CDPCommand<T extends keyof ProtocolMapping.Commands = keyof ProtocolMapping.Commands> = { export type CDPCommandFor<T extends keyof ProtocolMapping.Commands> = {
id: number; id: number;
sessionId?: string; sessionId?: string;
method: T; method: T;
params?: ProtocolMapping.Commands[T]['paramsType'][0]; params?: ProtocolMapping.Commands[T]['paramsType'][0];
}; };
export type CDPResponse<T extends keyof ProtocolMapping.Commands = keyof ProtocolMapping.Commands> = { export type CDPCommand = {
[K in keyof ProtocolMapping.Commands]: CDPCommandFor<K>;
}[keyof ProtocolMapping.Commands];
export type CDPResponseFor<T extends keyof ProtocolMapping.Commands> = {
id: number; id: number;
sessionId?: string; sessionId?: string;
result?: ProtocolMapping.Commands[T]['returnType']; result?: ProtocolMapping.Commands[T]['returnType'];
error?: { code?: number; message: string }; error?: { code?: number; message: string };
}; };
export type CDPEvent<T extends keyof ProtocolMapping.Events = keyof ProtocolMapping.Events> = { export type CDPResponse = {
[K in keyof ProtocolMapping.Commands]: CDPResponseFor<K>;
}[keyof ProtocolMapping.Commands];
export type CDPEventFor<T extends keyof ProtocolMapping.Events> = {
method: T; method: T;
sessionId?: string; sessionId?: string;
params?: ProtocolMapping.Events[T][0]; params?: ProtocolMapping.Events[T][0];
}; };
export type CDPEvent = {
[K in keyof ProtocolMapping.Events]: CDPEventFor<K>;
}[keyof ProtocolMapping.Events];
export type CDPResponseBase = {
id: number;
sessionId?: string;
result?: unknown;
error?: { code?: number; message: string };
};
export type CDPEventBase = {
method: string;
sessionId?: string;
params?: unknown;
};
export type CDPMessage = CDPCommand | CDPResponse | CDPEvent; export type CDPMessage = CDPCommand | CDPResponse | CDPEvent;
export { Protocol, ProtocolMapping }; export { Protocol, ProtocolMapping };
+72 -32
View File
@@ -3,7 +3,7 @@ import { serve } from '@hono/node-server'
import { createNodeWebSocket } from '@hono/node-ws' import { createNodeWebSocket } from '@hono/node-ws'
import type { WSContext } from 'hono/ws' import type { WSContext } from 'hono/ws'
import type { Protocol } from '../cdp-types.js' import type { Protocol } from '../cdp-types.js'
import type { CDPCommand, CDPResponse, CDPEvent } from '../cdp-types.js' import type { CDPCommand, CDPResponseBase, CDPEventBase, CDPEventFor } from '../cdp-types.js'
import type { ExtensionMessage, ExtensionEventMessage } from './protocol.js' import type { ExtensionMessage, ExtensionEventMessage } from './protocol.js'
import chalk from 'chalk' import chalk from 'chalk'
@@ -20,7 +20,8 @@ type PlaywrightClient = {
ws: WSContext ws: WSContext
} }
export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.0.0.1', logger = console }: { port?: number; host?: string; logger?: { log(...args: any[]): void; error(...args: any[]): void } } = {}) {
export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.0.0.1', logger }: { port?: number; host?: string; logger?: { log(...args: any[]): void; error(...args: any[]): void } } = {}) {
const connectedTargets = new Map<string, ConnectedTarget>() const connectedTargets = new Map<string, ConnectedTarget>()
const playwrightClients = new Map<string, PlaywrightClient>() const playwrightClients = new Map<string, PlaywrightClient>()
@@ -88,14 +89,14 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
if (direction === 'from-playwright') { if (direction === 'from-playwright') {
const clientLabel = clientId ? chalk.blue(`[${clientId}]`) : '' const clientLabel = clientId ? chalk.blue(`[${clientId}]`) : ''
logger.log(chalk.cyan('← Playwright'), clientLabel + ':', method + detailsStr) logger?.log(chalk.cyan('← Playwright'), clientLabel + ':', method + detailsStr)
} else if (direction === 'from-extension') { } else if (direction === 'from-extension') {
logger.log(chalk.yellow('← Extension:'), method + detailsStr) logger?.log(chalk.yellow('← Extension:'), method + detailsStr)
} else if (direction === 'to-playwright') { } else if (direction === 'to-playwright') {
const color = source === 'server' ? chalk.magenta : chalk.green const color = source === 'server' ? chalk.magenta : chalk.green
const sourceLabel = source === 'server' ? chalk.gray(' (server-generated)') : '' const sourceLabel = source === 'server' ? chalk.gray(' (server-generated)') : ''
const clientLabel = clientId ? chalk.blue(`[${clientId}]`) : chalk.blue('[ALL]') const clientLabel = clientId ? chalk.blue(`[${clientId}]`) : chalk.blue('[ALL]')
logger.log(color('→ Playwright'), clientLabel + ':', method + detailsStr + sourceLabel) logger?.log(color('→ Playwright'), clientLabel + ':', method + detailsStr + sourceLabel)
} }
} }
@@ -104,7 +105,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
clientId, clientId,
source = 'extension' source = 'extension'
}: { }: {
message: CDPResponse | CDPEvent message: CDPResponseBase | CDPEventBase
clientId?: string clientId?: string
source?: 'extension' | 'server' source?: 'extension' | 'server'
}) { }) {
@@ -189,6 +190,25 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
return {} return {}
} }
case 'Target.setDiscoverTargets': {
return {}
}
case 'Target.attachToTarget': {
const targetId = params?.targetId
if (!targetId) {
throw new Error('targetId is required for Target.attachToTarget')
}
for (const target of connectedTargets.values()) {
if (target.targetId === targetId) {
return { sessionId: target.sessionId } satisfies Protocol.Target.AttachToTargetResponse
}
}
throw new Error(`Target ${targetId} not found in connected targets`)
}
case 'Target.getTargetInfo': { case 'Target.getTargetInfo': {
const targetId = params?.targetId const targetId = params?.targetId
@@ -251,7 +271,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
app.post('/mcp-log', async (c) => { app.post('/mcp-log', async (c) => {
try { try {
const { level, args } = await c.req.json() const { level, args } = await c.req.json()
const logFn = (logger as any)[level] || logger.log const logFn = (logger as any)[level] || logger?.log
const prefix = chalk.red(`[MCP] [${level.toUpperCase()}]`) const prefix = chalk.red(`[MCP] [${level.toUpperCase()}]`)
logFn(prefix, ...args) logFn(prefix, ...args)
return c.json({ ok: true }) return c.json({ ok: true })
@@ -266,13 +286,13 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
return { return {
onOpen(_event, ws) { onOpen(_event, ws) {
if (playwrightClients.has(clientId)) { if (playwrightClients.has(clientId)) {
logger.log(chalk.red(`Rejecting duplicate client ID: ${clientId}`)) logger?.log(chalk.red(`Rejecting duplicate client ID: ${clientId}`))
ws.close(1000, 'Client ID already connected') ws.close(1000, 'Client ID already connected')
return return
} }
playwrightClients.set(clientId, { id: clientId, ws }) playwrightClients.set(clientId, { id: clientId, ws })
logger.log(chalk.green(`Playwright client connected: ${clientId} (${playwrightClients.size} total)`)) logger?.log(chalk.green(`Playwright client connected: ${clientId} (${playwrightClients.size} total)`))
}, },
async onMessage(event, ws) { async onMessage(event, ws) {
@@ -321,8 +341,8 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
}, },
waitingForDebugger: false waitingForDebugger: false
} }
} satisfies CDPEvent } satisfies CDPEventFor<'Target.attachedToTarget'>
logger.log(chalk.magenta('[Server] Target.attachedToTarget full payload:'), JSON.stringify(attachedPayload)) logger?.log(chalk.magenta('[Server] Target.attachedToTarget full payload:'), JSON.stringify(attachedPayload))
sendToPlaywright({ sendToPlaywright({
message: attachedPayload, message: attachedPayload,
clientId, clientId,
@@ -331,12 +351,32 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
} }
} }
if (method === 'Target.setDiscoverTargets' && (params)?.discover) {
for (const target of connectedTargets.values()) {
const targetCreatedPayload = {
method: 'Target.targetCreated',
params: {
targetInfo: {
...target.targetInfo,
attached: true
}
}
} satisfies CDPEventFor<'Target.targetCreated'>
logger?.log(chalk.magenta('[Server] Target.targetCreated full payload:'), JSON.stringify(targetCreatedPayload))
sendToPlaywright({
message: targetCreatedPayload,
clientId,
source: 'server'
})
}
}
sendToPlaywright({ sendToPlaywright({
message: { id, sessionId, result }, message: { id, sessionId, result },
clientId clientId
}) })
} catch (e) { } catch (e) {
logger.error('Error handling CDP command:', method, params, e) logger?.error('Error handling CDP command:', method, params, e)
sendToPlaywright({ sendToPlaywright({
message: { message: {
id, id,
@@ -350,11 +390,11 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
onClose() { onClose() {
playwrightClients.delete(clientId) playwrightClients.delete(clientId)
logger.log(chalk.yellow(`Playwright client disconnected: ${clientId} (${playwrightClients.size} remaining)`)) logger?.log(chalk.yellow(`Playwright client disconnected: ${clientId} (${playwrightClients.size} remaining)`))
}, },
onError(event) { onError(event) {
logger.error(`Playwright WebSocket error [${clientId}]:`, event) logger?.error(`Playwright WebSocket error [${clientId}]:`, event)
} }
} }
})) }))
@@ -363,9 +403,9 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
return { return {
onOpen(_event, ws) { onOpen(_event, ws) {
if (extensionWs) { if (extensionWs) {
logger.log(chalk.yellow('Closing existing extension connection to replace with new one')) logger?.log(chalk.yellow('Closing existing extension connection to replace with new one'))
extensionWs.close(4001, 'Extension Replaced') extensionWs.close(4001, 'Extension Replaced')
// Clear state from the old connection to prevent leaks // Clear state from the old connection to prevent leaks
connectedTargets.clear() connectedTargets.clear()
for (const pending of extensionPendingRequests.values()) { for (const pending of extensionPendingRequests.values()) {
@@ -380,7 +420,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
} }
extensionWs = ws extensionWs = ws
logger.log('Extension connected with clean state') logger?.log('Extension connected with clean state')
}, },
async onMessage(event, ws) { async onMessage(event, ws) {
@@ -396,7 +436,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
if ('id' in message) { if ('id' in message) {
const pending = extensionPendingRequests.get(message.id) const pending = extensionPendingRequests.get(message.id)
if (!pending) { if (!pending) {
logger.log('Unexpected response with id:', message.id) logger?.log('Unexpected response with id:', message.id)
return return
} }
@@ -409,7 +449,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
} }
} else if (message.method === 'log') { } else if (message.method === 'log') {
const { level, args } = message.params const { level, args } = message.params
const logFn = (logger as any)[level] || logger.log const logFn = (logger as any)[level] || logger?.log
const prefix = chalk.yellow(`[Extension] [${level.toUpperCase()}]`) const prefix = chalk.yellow(`[Extension] [${level.toUpperCase()}]`)
logFn(prefix, ...args) logFn(prefix, ...args)
} else { } else {
@@ -431,7 +471,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
if (method === 'Target.attachedToTarget') { if (method === 'Target.attachedToTarget') {
const targetParams = params as Protocol.Target.AttachedToTargetEvent const targetParams = params as Protocol.Target.AttachedToTargetEvent
logger.log(chalk.yellow('[Extension] Target.attachedToTarget full payload:'), JSON.stringify({ method, params: targetParams, sessionId })) logger?.log(chalk.yellow('[Extension] Target.attachedToTarget full payload:'), JSON.stringify({ method, params: targetParams, sessionId }))
// Check if we already sent this target to clients (e.g., from Target.setAutoAttach response) // Check if we already sent this target to clients (e.g., from Target.setAutoAttach response)
const alreadyConnected = connectedTargets.has(targetParams.sessionId) const alreadyConnected = connectedTargets.has(targetParams.sessionId)
@@ -449,7 +489,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
message: { message: {
method: 'Target.attachedToTarget', method: 'Target.attachedToTarget',
params: targetParams params: targetParams
} as CDPEvent, } as CDPEventBase,
source: 'extension' source: 'extension'
}) })
} }
@@ -461,7 +501,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
message: { message: {
method: 'Target.detachedFromTarget', method: 'Target.detachedFromTarget',
params: detachParams params: detachParams
} as CDPEvent, } as CDPEventBase,
source: 'extension' source: 'extension'
}) })
} else if (method === 'Target.targetInfoChanged') { } else if (method === 'Target.targetInfoChanged') {
@@ -477,7 +517,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
message: { message: {
method: 'Target.targetInfoChanged', method: 'Target.targetInfoChanged',
params: infoParams params: infoParams
} as CDPEvent, } as CDPEventBase,
source: 'extension' source: 'extension'
}) })
} else { } else {
@@ -486,7 +526,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
sessionId, sessionId,
method, method,
params params
} as CDPEvent, } as CDPEventBase,
source: 'extension' source: 'extension'
}) })
} }
@@ -494,12 +534,12 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
}, },
onClose(event, ws) { onClose(event, ws) {
logger.log('Extension disconnected') logger?.log('Extension disconnected')
// If this is an old connection closing after we've already established a new one, // If this is an old connection closing after we've already established a new one,
// don't clear the global state // don't clear the global state
if (extensionWs && extensionWs !== ws) { if (extensionWs && extensionWs !== ws) {
logger.log('Old extension connection closed, keeping new one active') logger?.log('Old extension connection closed, keeping new one active')
return return
} }
@@ -518,7 +558,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
}, },
onError(event) { onError(event) {
logger.error('Extension WebSocket error:', event) logger?.error('Extension WebSocket error:', event)
} }
} }
})) }))
@@ -530,11 +570,11 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
const cdpEndpoint = `${wsHost}/cdp` const cdpEndpoint = `${wsHost}/cdp`
const extensionEndpoint = `${wsHost}/extension` const extensionEndpoint = `${wsHost}/extension`
logger.log('CDP relay server started') logger?.log('CDP relay server started')
logger.log('Host:', host) logger?.log('Host:', host)
logger.log('Port:', port) logger?.log('Port:', port)
logger.log('Extension endpoint:', extensionEndpoint) logger?.log('Extension endpoint:', extensionEndpoint)
logger.log('CDP endpoint:', cdpEndpoint) logger?.log('CDP endpoint:', cdpEndpoint)
return { return {
close() { close() {
+3 -3
View File
@@ -1,4 +1,4 @@
import { CDPCommand, CDPEvent, ProtocolMapping } from '../cdp-types.js' import { CDPEventFor, ProtocolMapping } from '../cdp-types.js'
export const VERSION = 1 export const VERSION = 1
@@ -32,9 +32,9 @@ export type ExtensionEventMessage =
[K in keyof ProtocolMapping.Events]: { [K in keyof ProtocolMapping.Events]: {
method: 'forwardCDPEvent' method: 'forwardCDPEvent'
params: { params: {
method: CDPEvent<K>['method'] method: CDPEventFor<K>['method']
sessionId?: string sessionId?: string
params?: CDPEvent<K>['params'] params?: CDPEventFor<K>['params']
} }
} }
}[keyof ProtocolMapping.Events] }[keyof ProtocolMapping.Events]
+75 -21
View File
@@ -610,8 +610,8 @@ describe('MCP Server Tests', () => {
expect(results).toMatchInlineSnapshot(` expect(results).toMatchInlineSnapshot(`
[ [
{ {
"title": "", "title": "🎄 Twelve Days of Shell 🎄",
"url": "about:blank", "url": "https://12days.cmdchallenge.com/",
}, },
{ {
"title": "Example Domain", "title": "Example Domain",
@@ -696,8 +696,8 @@ describe('MCP Server Tests', () => {
expect(results).toMatchInlineSnapshot(` expect(results).toMatchInlineSnapshot(`
[ [
{ {
"title": "", "title": "🎄 Twelve Days of Shell 🎄",
"url": "about:blank", "url": "https://12days.cmdchallenge.com/",
}, },
{ {
"title": "Example Domain", "title": "Example Domain",
@@ -762,18 +762,18 @@ describe('MCP Server Tests', () => {
const pages = await browserContext.pages() const pages = await browserContext.pages()
expect(pages.length).toBeGreaterThan(0) expect(pages.length).toBeGreaterThan(0)
const page = pages[0] const page = pages[0]
await page.goto('https://example.com/disconnect-test') await page.goto('https://example.com/disconnect-test')
await page.waitForLoadState('networkidle') await page.waitForLoadState('networkidle')
await page.bringToFront() await page.bringToFront()
// Enable extension on this page // Enable extension on this page
const initialEnable = await serviceWorker.evaluate(async () => { const initialEnable = await serviceWorker.evaluate(async () => {
return await globalThis.toggleExtensionForActiveTab() return await globalThis.toggleExtensionForActiveTab()
}) })
console.log('Initial enable result:', initialEnable) console.log('Initial enable result:', initialEnable)
expect(initialEnable.isConnected).toBe(true) expect(initialEnable.isConnected).toBe(true)
// Wait for extension to fully connect // Wait for extension to fully connect
await new Promise(resolve => setTimeout(resolve, 500)) await new Promise(resolve => setTimeout(resolve, 500))
@@ -790,7 +790,7 @@ describe('MCP Server Tests', () => {
`, `,
}, },
}) })
const beforeOutput = (beforeDisconnect as any).content[0].text const beforeOutput = (beforeDisconnect as any).content[0].text
expect(beforeOutput).toContain('foundTestPage') expect(beforeOutput).toContain('foundTestPage')
console.log('Before disconnect:', beforeOutput) console.log('Before disconnect:', beforeOutput)
@@ -800,7 +800,7 @@ describe('MCP Server Tests', () => {
await serviceWorker.evaluate(async () => { await serviceWorker.evaluate(async () => {
await globalThis.disconnectEverything() await globalThis.disconnectEverything()
}) })
// Wait for disconnect to complete // Wait for disconnect to complete
await new Promise(resolve => setTimeout(resolve, 500)) await new Promise(resolve => setTimeout(resolve, 500))
@@ -815,7 +815,7 @@ describe('MCP Server Tests', () => {
`, `,
}, },
}) })
const afterDisconnectOutput = (afterDisconnect as any).content[0].text const afterDisconnectOutput = (afterDisconnect as any).content[0].text
console.log('After disconnect:', afterDisconnectOutput) console.log('After disconnect:', afterDisconnectOutput)
expect(afterDisconnectOutput).toContain('Pages after disconnect: 0') expect(afterDisconnectOutput).toContain('Pages after disconnect: 0')
@@ -829,10 +829,10 @@ describe('MCP Server Tests', () => {
console.log('toggleExtensionForActiveTab result:', result) console.log('toggleExtensionForActiveTab result:', result)
return result return result
}) })
console.log('Reconnect result:', reconnectResult) console.log('Reconnect result:', reconnectResult)
expect(reconnectResult.isConnected).toBe(true) expect(reconnectResult.isConnected).toBe(true)
// Wait for extension to fully reconnect and relay server to be ready // Wait for extension to fully reconnect and relay server to be ready
console.log('Waiting for reconnection to stabilize...') console.log('Waiting for reconnection to stabilize...')
await new Promise(resolve => setTimeout(resolve, 1000)) await new Promise(resolve => setTimeout(resolve, 1000))
@@ -863,25 +863,25 @@ describe('MCP Server Tests', () => {
console.log('Checking pages after reconnect...'); console.log('Checking pages after reconnect...');
const pages = context.pages(); const pages = context.pages();
console.log('Pages after reconnect:', pages.length); console.log('Pages after reconnect:', pages.length);
if (pages.length === 0) { if (pages.length === 0) {
console.log('No pages found!'); console.log('No pages found!');
return { pagesCount: 0, foundTestPage: false }; return { pagesCount: 0, foundTestPage: false };
} }
const testPage = pages.find(p => p.url().includes('disconnect-test')); const testPage = pages.find(p => p.url().includes('disconnect-test'));
console.log('Found test page after reconnect:', !!testPage); console.log('Found test page after reconnect:', !!testPage);
if (testPage) { if (testPage) {
console.log('Test page URL:', testPage.url()); console.log('Test page URL:', testPage.url());
return { pagesCount: pages.length, foundTestPage: true, url: testPage.url() }; return { pagesCount: pages.length, foundTestPage: true, url: testPage.url() };
} }
return { pagesCount: pages.length, foundTestPage: false }; return { pagesCount: pages.length, foundTestPage: false };
`, `,
}, },
}) })
const afterReconnectOutput = (afterReconnect as any).content[0].text const afterReconnectOutput = (afterReconnect as any).content[0].text
console.log('After reconnect:', afterReconnectOutput) console.log('After reconnect:', afterReconnectOutput)
expect(afterReconnectOutput).toContain('foundTestPage') expect(afterReconnectOutput).toContain('foundTestPage')
@@ -900,11 +900,11 @@ describe('MCP Server Tests', () => {
// Clear any existing logs from previous tests // Clear any existing logs from previous tests
clearAllLogs(); clearAllLogs();
console.log('Cleared all existing logs'); console.log('Cleared all existing logs');
// Verify connection is working // Verify connection is working
const pages = context.pages(); const pages = context.pages();
console.log('Current pages count:', pages.length); console.log('Current pages count:', pages.length);
return { success: true, pagesCount: pages.length }; return { success: true, pagesCount: pages.length };
`, `,
}, },
@@ -996,7 +996,7 @@ describe('MCP Server Tests', () => {
`, `,
}, },
}) })
const beforeReloadOutput = (beforeReloadResult as any).content[0].text const beforeReloadOutput = (beforeReloadResult as any).content[0].text
expect(beforeReloadOutput).toContain('[log] Before reload 99999') expect(beforeReloadOutput).toContain('[log] Before reload 99999')
@@ -1025,7 +1025,7 @@ describe('MCP Server Tests', () => {
`, `,
}, },
}) })
const afterReloadOutput = (afterReloadResult as any).content[0].text const afterReloadOutput = (afterReloadResult as any).content[0].text
expect(afterReloadOutput).toContain('[log] After reload 88888') expect(afterReloadOutput).toContain('[log] After reload 88888')
expect(afterReloadOutput).not.toContain('[log] Before reload 99999') expect(afterReloadOutput).not.toContain('[log] Before reload 99999')
@@ -1357,6 +1357,60 @@ describe('MCP Server Tests', () => {
await page.close() await page.close()
}, 60000) }, 60000)
it('should work with stagehand', async () => {
if (!browserContext) throw new Error('Browser not initialized')
const serviceWorker = await getExtensionServiceWorker(browserContext)
await serviceWorker.evaluate(async () => {
await globalThis.disconnectEverything()
})
await new Promise(r => setTimeout(r, 500))
const targetUrl = 'https://example.com/'
const enableResult = await serviceWorker.evaluate(async (url) => {
const tab = await chrome.tabs.create({ url, active: true })
await new Promise(r => setTimeout(r, 1000))
return await globalThis.toggleExtensionForActiveTab()
}, targetUrl)
console.log('Extension enabled:', enableResult)
expect(enableResult.isConnected).toBe(true)
await new Promise(r => setTimeout(r, 1000))
const { Stagehand } = await import('@browserbasehq/stagehand')
const stagehand = new Stagehand({
env: 'LOCAL',
verbose: 1,
disablePino: true,
localBrowserLaunchOptions: {
cdpUrl: getCdpUrl(),
},
})
console.log('Initializing Stagehand...')
await stagehand.init()
console.log('Stagehand initialized')
const context = stagehand.context
console.log('Stagehand context:', context)
expect(context).toBeDefined()
const pages = context.pages()
console.log('Stagehand pages:', pages.length, pages.map(p => p.url()))
const stagehandPage = pages.find(p => p.url().includes('example.com'))
expect(stagehandPage).toBeDefined()
const url = stagehandPage!.url()
console.log('Stagehand page URL:', url)
expect(url).toContain('example.com')
await stagehand.close()
}, 60000)
}) })
@@ -28,10 +28,10 @@ Return value:
- generic [ref=e21]: - generic [ref=e21]:
- generic: ⌘ - generic: ⌘
- generic: K - generic: K
- link "100.8k" [ref=e22] [cursor=pointer]: - link "100.5k" [ref=e22] [cursor=pointer]:
- /url: https://github.com/shadcn-ui/ui - /url: https://github.com/shadcn-ui/ui
- img - img
- generic [ref=e23]: 100.8k - generic [ref=e23]: 100.5k
- button "Toggle theme" [ref=e24]: - button "Toggle theme" [ref=e24]:
- img - img
- generic [ref=e25]: Toggle theme - generic [ref=e25]: Toggle theme
+1913 -17
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -1,2 +1,4 @@
packages: packages:
- ./* - ./*
preferWorkspacePackages: true