add test for stagehand. Target.attachToTarget handling and better DCP types
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Generated
+1913
-17
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,4 @@
|
|||||||
packages:
|
packages:
|
||||||
- ./*
|
- ./*
|
||||||
|
|
||||||
|
preferWorkspacePackages: true
|
||||||
|
|||||||
Reference in New Issue
Block a user