add editor. use different port in tests
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
declare const process: { env: { PLAYWRITER_PORT: string } }
|
||||
|
||||
import { createStore } from 'zustand/vanilla'
|
||||
import type { ExtensionState, ConnectionState, TabState, TabInfo } from './types'
|
||||
import type { CDPEvent, Protocol } from 'playwriter/src/cdp-types'
|
||||
import type { ExtensionCommandMessage, ExtensionResponseMessage } from 'playwriter/src/extension/protocol'
|
||||
|
||||
const RELAY_URL = 'ws://localhost:19988/extension'
|
||||
const RELAY_PORT = process.env.PLAYWRITER_PORT
|
||||
const RELAY_URL = `ws://localhost:${RELAY_PORT}/extension`
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
@@ -462,7 +465,7 @@ function startReplacedRetryLoop(): void {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const response = await fetch('http://localhost:19988/extension/status')
|
||||
const response = await fetch(`http://localhost:${RELAY_PORT}/extension/status`)
|
||||
const data = await response.json()
|
||||
if (!data.connected) {
|
||||
store.setState({ connectionState: 'disconnected', errorText: undefined })
|
||||
@@ -518,12 +521,12 @@ async function ensureConnection(): Promise<void> {
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug('Waiting for server at http://localhost:19988...')
|
||||
logger.debug(`Waiting for server at http://localhost:${RELAY_PORT}...`)
|
||||
store.setState({ connectionState: 'reconnecting' })
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
await fetch('http://localhost:19988', { method: 'HEAD' })
|
||||
await fetch(`http://localhost:${RELAY_PORT}`, { method: 'HEAD' })
|
||||
logger.debug('Server is available')
|
||||
break
|
||||
} catch {
|
||||
|
||||
@@ -7,7 +7,9 @@ import fs from 'node:fs';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const defineEnv: Record<string, string> = {};
|
||||
const defineEnv: Record<string, string> = {
|
||||
'process.env.PLAYWRITER_PORT': JSON.stringify(process.env.PLAYWRITER_PORT || '19988'),
|
||||
};
|
||||
if (process.env.TESTING) {
|
||||
defineEnv['import.meta.env.TESTING'] = 'true';
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { Page } from 'playwright-core'
|
||||
import type { CDPSession } from './cdp-session.js'
|
||||
import type { Debugger } from './debugger.js'
|
||||
import type { Editor } from './editor.js'
|
||||
|
||||
export declare const page: Page
|
||||
export declare const getCDPSession: (options: { page: Page }) => Promise<CDPSession>
|
||||
export declare const createDebugger: (options: { cdp: CDPSession }) => Debugger
|
||||
export declare const createEditor: (options: { cdp: CDPSession }) => Editor
|
||||
export declare const console: { log: (...args: unknown[]) => void }
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { page, getCDPSession, createEditor, console } from './debugger-examples-types.js'
|
||||
|
||||
// Example: List available scripts
|
||||
async function listScripts() {
|
||||
const cdp = await getCDPSession({ page })
|
||||
const editor = createEditor({ cdp })
|
||||
await editor.enable()
|
||||
|
||||
const scripts = editor.list({ search: 'app' })
|
||||
console.log(scripts)
|
||||
}
|
||||
|
||||
// Example: Read a script with line numbers
|
||||
async function readScript() {
|
||||
const cdp = await getCDPSession({ page })
|
||||
const editor = createEditor({ cdp })
|
||||
await editor.enable()
|
||||
|
||||
const { content, totalLines } = await editor.read({
|
||||
url: 'https://example.com/app.js',
|
||||
})
|
||||
console.log('Total lines:', totalLines)
|
||||
console.log(content)
|
||||
|
||||
const { content: partial } = await editor.read({
|
||||
url: 'https://example.com/app.js',
|
||||
offset: 100,
|
||||
limit: 50,
|
||||
})
|
||||
console.log(partial)
|
||||
}
|
||||
|
||||
// Example: Edit a script (exact string replacement)
|
||||
async function editScript() {
|
||||
const cdp = await getCDPSession({ page })
|
||||
const editor = createEditor({ cdp })
|
||||
await editor.enable()
|
||||
|
||||
await editor.edit({
|
||||
url: 'https://example.com/app.js',
|
||||
oldString: 'const DEBUG = false',
|
||||
newString: 'const DEBUG = true',
|
||||
})
|
||||
|
||||
const dryRunResult = await editor.edit({
|
||||
url: 'https://example.com/app.js',
|
||||
oldString: 'old code',
|
||||
newString: 'new code',
|
||||
dryRun: true,
|
||||
})
|
||||
console.log('Dry run result:', dryRunResult)
|
||||
}
|
||||
|
||||
// Example: Search across all scripts
|
||||
async function searchScripts() {
|
||||
const cdp = await getCDPSession({ page })
|
||||
const editor = createEditor({ cdp })
|
||||
await editor.enable()
|
||||
|
||||
const matches = await editor.grep({ regex: /console\.log/ })
|
||||
console.log(matches)
|
||||
|
||||
const todoMatches = await editor.grep({
|
||||
regex: /TODO|FIXME/i,
|
||||
include: 'app',
|
||||
})
|
||||
console.log(todoMatches)
|
||||
}
|
||||
|
||||
// Example: Write entire script content
|
||||
async function writeScript() {
|
||||
const cdp = await getCDPSession({ page })
|
||||
const editor = createEditor({ cdp })
|
||||
await editor.enable()
|
||||
|
||||
const { content } = await editor.read({ url: 'https://example.com/app.js' })
|
||||
const newContent = content.replace(/console\.log/g, 'console.debug')
|
||||
|
||||
await editor.write({
|
||||
url: 'https://example.com/app.js',
|
||||
content: newContent,
|
||||
})
|
||||
}
|
||||
|
||||
// Example: Edit an inline script (scripts without URL get inline://{id} URLs)
|
||||
async function editInlineScript() {
|
||||
const cdp = await getCDPSession({ page })
|
||||
const editor = createEditor({ cdp })
|
||||
await editor.enable()
|
||||
|
||||
const matches = await editor.grep({ regex: /myFunction/ })
|
||||
if (matches.length > 0) {
|
||||
const { url } = matches[0]
|
||||
console.log('Found in:', url)
|
||||
|
||||
await editor.edit({
|
||||
url,
|
||||
oldString: 'return false',
|
||||
newString: 'return true',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export { listScripts, readScript, editScript, searchScripts, writeScript, editInlineScript }
|
||||
@@ -0,0 +1,341 @@
|
||||
import type { CDPSession } from './cdp-session.js'
|
||||
|
||||
export interface ScriptInfo {
|
||||
url: string
|
||||
scriptId: string
|
||||
}
|
||||
|
||||
export interface ReadResult {
|
||||
content: string
|
||||
totalLines: number
|
||||
startLine: number
|
||||
endLine: number
|
||||
}
|
||||
|
||||
export interface SearchMatch {
|
||||
url: string
|
||||
lineNumber: number
|
||||
lineContent: string
|
||||
}
|
||||
|
||||
export interface EditResult {
|
||||
success: boolean
|
||||
stackChanged?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* A class for viewing and editing web page scripts via Chrome DevTools Protocol.
|
||||
* Provides a Claude Code-like interface: list, read, edit, grep.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const cdp = await getCDPSession({ page })
|
||||
* const editor = new Editor({ cdp })
|
||||
* await editor.enable()
|
||||
*
|
||||
* // List available scripts
|
||||
* const scripts = editor.list({ search: 'app' })
|
||||
*
|
||||
* // Read a script
|
||||
* const { content } = await editor.read({ url: 'https://example.com/app.js' })
|
||||
*
|
||||
* // Edit a script
|
||||
* await editor.edit({
|
||||
* url: 'https://example.com/app.js',
|
||||
* oldString: 'console.log("old")',
|
||||
* newString: 'console.log("new")'
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export class Editor {
|
||||
private cdp: CDPSession
|
||||
private enabled = false
|
||||
private scripts = new Map<string, ScriptInfo>()
|
||||
private scriptsByUrl = new Map<string, ScriptInfo>()
|
||||
private sourceCache = new Map<string, string>()
|
||||
|
||||
constructor({ cdp }: { cdp: CDPSession }) {
|
||||
this.cdp = cdp
|
||||
this.setupEventListeners()
|
||||
}
|
||||
|
||||
private setupEventListeners() {
|
||||
this.cdp.on('Debugger.scriptParsed', (params) => {
|
||||
if (!params.url.startsWith('chrome') && !params.url.startsWith('devtools')) {
|
||||
const url = params.url || `inline://${params.scriptId}`
|
||||
const info: ScriptInfo = {
|
||||
url,
|
||||
scriptId: params.scriptId,
|
||||
}
|
||||
this.scripts.set(params.scriptId, info)
|
||||
this.scriptsByUrl.set(url, info)
|
||||
this.sourceCache.delete(params.scriptId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the editor. Must be called before other methods.
|
||||
* Scripts are collected from Debugger.scriptParsed events.
|
||||
* Reload the page after enabling to capture all scripts.
|
||||
*/
|
||||
async enable(): Promise<void> {
|
||||
if (this.enabled) {
|
||||
return
|
||||
}
|
||||
await this.cdp.send('Debugger.enable')
|
||||
this.enabled = true
|
||||
}
|
||||
|
||||
private getScriptByUrl(url: string): ScriptInfo {
|
||||
const script = this.scriptsByUrl.get(url)
|
||||
if (!script) {
|
||||
const available = Array.from(this.scriptsByUrl.keys()).slice(0, 5)
|
||||
throw new Error(`Script not found: ${url}\nAvailable: ${available.join(', ')}${this.scriptsByUrl.size > 5 ? '...' : ''}`)
|
||||
}
|
||||
return script
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists available scripts. Use search to filter by URL substring.
|
||||
*
|
||||
* @param options - Options
|
||||
* @param options.search - Optional substring to filter URLs (case-insensitive)
|
||||
* @returns Array of scripts with url and scriptId
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // List all scripts
|
||||
* const scripts = editor.list()
|
||||
*
|
||||
* // Search for specific scripts
|
||||
* const appScripts = editor.list({ search: 'app' })
|
||||
* const reactScripts = editor.list({ search: 'react' })
|
||||
* ```
|
||||
*/
|
||||
list({ search }: { search?: string } = {}): ScriptInfo[] {
|
||||
const scripts = Array.from(this.scripts.values())
|
||||
const filtered = search ? scripts.filter((s) => s.url.toLowerCase().includes(search.toLowerCase())) : scripts
|
||||
return filtered
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a script's source code by URL.
|
||||
* Returns line-numbered content like Claude Code's Read tool.
|
||||
* For inline scripts, use the `inline://` URL from list() or grep().
|
||||
*
|
||||
* @param options - Options
|
||||
* @param options.url - Script URL (inline scripts have `inline://{id}` URLs)
|
||||
* @param options.offset - Line number to start from (0-based, default 0)
|
||||
* @param options.limit - Number of lines to return (default 2000)
|
||||
* @returns Content with line numbers, total lines, and range info
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Read by URL
|
||||
* const { content, totalLines } = await editor.read({
|
||||
* url: 'https://example.com/app.js'
|
||||
* })
|
||||
*
|
||||
* // Read inline script (URL from grep result)
|
||||
* const { content } = await editor.read({ url: 'inline://42' })
|
||||
*
|
||||
* // Read lines 100-200
|
||||
* const { content } = await editor.read({
|
||||
* url: 'https://example.com/app.js',
|
||||
* offset: 100,
|
||||
* limit: 100
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
async read({ url, offset = 0, limit = 2000 }: { url: string; offset?: number; limit?: number }): Promise<ReadResult> {
|
||||
await this.enable()
|
||||
const script = this.getScriptByUrl(url)
|
||||
|
||||
let source = this.sourceCache.get(script.scriptId)
|
||||
if (!source) {
|
||||
const response = await this.cdp.send('Debugger.getScriptSource', { scriptId: script.scriptId })
|
||||
source = response.scriptSource
|
||||
this.sourceCache.set(script.scriptId, source)
|
||||
}
|
||||
|
||||
const lines = source.split('\n')
|
||||
const totalLines = lines.length
|
||||
const startLine = Math.min(offset, totalLines)
|
||||
const endLine = Math.min(offset + limit, totalLines)
|
||||
const selectedLines = lines.slice(startLine, endLine)
|
||||
|
||||
const content = selectedLines.map((line, i) => `${String(startLine + i + 1).padStart(5)}| ${line}`).join('\n')
|
||||
|
||||
return {
|
||||
content,
|
||||
totalLines,
|
||||
startLine: startLine + 1,
|
||||
endLine,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits a script by replacing oldString with newString.
|
||||
* Like Claude Code's Edit tool - performs exact string replacement.
|
||||
*
|
||||
* @param options - Options
|
||||
* @param options.url - Script URL (inline scripts have `inline://{id}` URLs)
|
||||
* @param options.oldString - Exact string to find and replace
|
||||
* @param options.newString - Replacement string
|
||||
* @param options.dryRun - If true, validate without applying (default false)
|
||||
* @returns Result with success status
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Replace a string
|
||||
* await editor.edit({
|
||||
* url: 'https://example.com/app.js',
|
||||
* oldString: 'const DEBUG = false',
|
||||
* newString: 'const DEBUG = true'
|
||||
* })
|
||||
*
|
||||
* // Edit inline script
|
||||
* await editor.edit({
|
||||
* url: 'inline://42',
|
||||
* oldString: 'old code',
|
||||
* newString: 'new code'
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
async edit({
|
||||
url,
|
||||
oldString,
|
||||
newString,
|
||||
dryRun = false,
|
||||
}: {
|
||||
url: string
|
||||
oldString: string
|
||||
newString: string
|
||||
dryRun?: boolean
|
||||
}): Promise<EditResult> {
|
||||
await this.enable()
|
||||
const script = this.getScriptByUrl(url)
|
||||
|
||||
let source = this.sourceCache.get(script.scriptId)
|
||||
if (!source) {
|
||||
const response = await this.cdp.send('Debugger.getScriptSource', { scriptId: script.scriptId })
|
||||
source = response.scriptSource
|
||||
this.sourceCache.set(script.scriptId, source)
|
||||
}
|
||||
|
||||
const matchCount = source.split(oldString).length - 1
|
||||
if (matchCount === 0) {
|
||||
throw new Error(`oldString not found in ${url}`)
|
||||
}
|
||||
if (matchCount > 1) {
|
||||
throw new Error(`oldString found ${matchCount} times in ${url}. Provide more context to make it unique.`)
|
||||
}
|
||||
|
||||
const newSource = source.replace(oldString, newString)
|
||||
|
||||
const response = await this.cdp.send('Debugger.setScriptSource', {
|
||||
scriptId: script.scriptId,
|
||||
scriptSource: newSource,
|
||||
dryRun,
|
||||
})
|
||||
|
||||
if (!dryRun) {
|
||||
this.sourceCache.set(script.scriptId, newSource)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
stackChanged: response.stackChanged,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for a regex across all scripts.
|
||||
* Like Claude Code's Grep tool - returns matching lines with context.
|
||||
*
|
||||
* @param options - Options
|
||||
* @param options.regex - Regular expression to search for
|
||||
* @param options.include - Optional URL substring to filter which scripts to search
|
||||
* @returns Array of matches with url, line number, and line content
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Search all scripts for "fetchUser"
|
||||
* const matches = await editor.grep({ regex: /fetchUser/ })
|
||||
*
|
||||
* // Search only in app scripts
|
||||
* const matches = await editor.grep({
|
||||
* regex: /TODO/i,
|
||||
* include: 'app'
|
||||
* })
|
||||
*
|
||||
* // Regex search for console methods
|
||||
* const matches = await editor.grep({
|
||||
* regex: /console\.(log|error|warn)/
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
async grep({ regex, include }: { regex: RegExp; include?: string }): Promise<SearchMatch[]> {
|
||||
await this.enable()
|
||||
|
||||
const matches: SearchMatch[] = []
|
||||
const scripts = include ? this.list({ search: include }) : this.list()
|
||||
|
||||
for (const script of scripts) {
|
||||
let source = this.sourceCache.get(script.scriptId)
|
||||
if (!source) {
|
||||
try {
|
||||
const response = await this.cdp.send('Debugger.getScriptSource', { scriptId: script.scriptId })
|
||||
source = response.scriptSource
|
||||
this.sourceCache.set(script.scriptId, source)
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const lines = source.split('\n')
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (regex.test(lines[i])) {
|
||||
matches.push({
|
||||
url: script.url,
|
||||
lineNumber: i + 1,
|
||||
lineContent: lines[i].trim().slice(0, 200),
|
||||
})
|
||||
regex.lastIndex = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes entire content to a script, replacing all existing code.
|
||||
* Use with caution - prefer edit() for targeted changes.
|
||||
*
|
||||
* @param options - Options
|
||||
* @param options.url - Script URL (inline scripts have `inline://{id}` URLs)
|
||||
* @param options.content - New script content
|
||||
* @param options.dryRun - If true, validate without applying (default false)
|
||||
*/
|
||||
async write({ url, content, dryRun = false }: { url: string; content: string; dryRun?: boolean }): Promise<EditResult> {
|
||||
await this.enable()
|
||||
const script = this.getScriptByUrl(url)
|
||||
|
||||
const response = await this.cdp.send('Debugger.setScriptSource', {
|
||||
scriptId: script.scriptId,
|
||||
scriptSource: content,
|
||||
dryRun,
|
||||
})
|
||||
|
||||
if (!dryRun) {
|
||||
this.sourceCache.set(script.scriptId, content)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
stackChanged: response.stackChanged,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,23 +9,28 @@ const __filename = url.fileURLToPath(import.meta.url)
|
||||
|
||||
export interface CreateTransportOptions {
|
||||
clientName?: string
|
||||
port?: number
|
||||
}
|
||||
|
||||
export async function createTransport(args: string[]): Promise<{
|
||||
export async function createTransport({ args = [], port }: { args?: string[]; port?: number } = {}): Promise<{
|
||||
transport: Transport
|
||||
stderr: Stream | null
|
||||
}> {
|
||||
const env: Record<string, string> = {
|
||||
...process.env as Record<string, string>,
|
||||
DEBUG: 'playwriter:mcp:test',
|
||||
DEBUG_COLORS: '0',
|
||||
DEBUG_HIDE_DATE: '1',
|
||||
}
|
||||
if (port) {
|
||||
env.PLAYWRITER_PORT = String(port)
|
||||
}
|
||||
const transport = new StdioClientTransport({
|
||||
command: 'pnpm',
|
||||
args: ['vite-node', path.join(path.dirname(__filename), 'mcp.ts'), ...args],
|
||||
cwd: path.join(path.dirname(__filename), '..'),
|
||||
stderr: 'pipe',
|
||||
env: {
|
||||
...process.env,
|
||||
DEBUG: 'playwriter:mcp:test',
|
||||
DEBUG_COLORS: '0',
|
||||
DEBUG_HIDE_DATE: '1',
|
||||
},
|
||||
env,
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -44,7 +49,7 @@ export async function createMCPClient(options?: CreateTransportOptions): Promise
|
||||
version: '1.0.0',
|
||||
})
|
||||
|
||||
const { transport, stderr } = await createTransport([])
|
||||
const { transport, stderr } = await createTransport({ port: options?.port })
|
||||
|
||||
let stderrBuffer = ''
|
||||
stderr?.on('data', (data) => {
|
||||
|
||||
+116
-46
@@ -12,6 +12,7 @@ import type { Protocol } from 'devtools-protocol'
|
||||
import { imageSize } from 'image-size'
|
||||
import { getCDPSessionForPage } from './cdp-session.js'
|
||||
import { Debugger } from './debugger.js'
|
||||
import { Editor } from './editor.js'
|
||||
import { startPlayWriterCDPRelayServer, type RelayServer } from './extension/cdp-relay.js'
|
||||
import { createFileLogger } from './create-logger.js'
|
||||
import type { CDPCommand } from './cdp-types.js'
|
||||
@@ -20,6 +21,7 @@ import { killPortProcess } from 'kill-port-process'
|
||||
declare const window: any
|
||||
declare const document: any
|
||||
|
||||
const TEST_PORT = 19987
|
||||
|
||||
const execAsync = promisify(exec)
|
||||
|
||||
@@ -67,15 +69,15 @@ interface TestContext {
|
||||
}
|
||||
|
||||
async function setupTestContext({ tempDirPrefix }: { tempDirPrefix: string }): Promise<TestContext> {
|
||||
await killProcessOnPort(19988)
|
||||
await killProcessOnPort(TEST_PORT)
|
||||
|
||||
console.log('Building extension...')
|
||||
await execAsync('TESTING=1 pnpm build', { cwd: '../extension' })
|
||||
await execAsync(`TESTING=1 PLAYWRITER_PORT=${TEST_PORT} pnpm build`, { cwd: '../extension' })
|
||||
console.log('Extension built')
|
||||
|
||||
const localLogPath = path.join(process.cwd(), 'relay-server.log')
|
||||
const logger = createFileLogger({ logFilePath: localLogPath })
|
||||
const relayServer = await startPlayWriterCDPRelayServer({ port: 19988, logger })
|
||||
const relayServer = await startPlayWriterCDPRelayServer({ port: TEST_PORT, logger })
|
||||
|
||||
const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), tempDirPrefix))
|
||||
const extensionPath = path.resolve('../extension/dist')
|
||||
@@ -136,7 +138,7 @@ describe('MCP Server Tests', () => {
|
||||
beforeAll(async () => {
|
||||
testCtx = await setupTestContext({ tempDirPrefix: 'pw-test-' })
|
||||
|
||||
const result = await createMCPClient()
|
||||
const result = await createMCPClient({ port: TEST_PORT })
|
||||
client = result.client
|
||||
cleanup = result.cleanup
|
||||
}, 600000)
|
||||
@@ -165,7 +167,7 @@ describe('MCP Server Tests', () => {
|
||||
})
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
const cdpPage = browser.contexts()[0].pages().find(p => {
|
||||
return p.url().startsWith('about:')
|
||||
})
|
||||
@@ -382,7 +384,7 @@ describe('MCP Server Tests', () => {
|
||||
|
||||
// 3. Verify we can connect via direct CDP and see the page
|
||||
|
||||
let directBrowser = await chromium.connectOverCDP(getCdpUrl())
|
||||
let directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
let contexts = directBrowser.contexts()
|
||||
let pages = contexts[0].pages()
|
||||
|
||||
@@ -408,7 +410,7 @@ describe('MCP Server Tests', () => {
|
||||
// connecting to relay will succeed, but listing pages should NOT show our page
|
||||
|
||||
// Connect to relay again
|
||||
directBrowser = await chromium.connectOverCDP(getCdpUrl())
|
||||
directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
contexts = directBrowser.contexts()
|
||||
pages = contexts[0].pages()
|
||||
|
||||
@@ -426,7 +428,7 @@ describe('MCP Server Tests', () => {
|
||||
|
||||
// 7. Verify page is back
|
||||
|
||||
directBrowser = await chromium.connectOverCDP(getCdpUrl())
|
||||
directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
// Wait a bit for targets to populate
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
@@ -454,7 +456,7 @@ describe('MCP Server Tests', () => {
|
||||
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
||||
|
||||
// Connect once
|
||||
const directBrowser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
// Wait a bit for connection and initial target discovery
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
@@ -536,7 +538,7 @@ describe('MCP Server Tests', () => {
|
||||
})
|
||||
|
||||
// 3. Connect via CDP
|
||||
const cdpUrl = getCdpUrl()
|
||||
const cdpUrl = getCdpUrl({ port: TEST_PORT })
|
||||
const directBrowser = await chromium.connectOverCDP(cdpUrl)
|
||||
const connectedPage = directBrowser.contexts()[0].pages().find(p => p.url() === initialUrl)
|
||||
expect(connectedPage).toBeDefined()
|
||||
@@ -617,7 +619,7 @@ describe('MCP Server Tests', () => {
|
||||
expect(targetIds.idA).not.toBe(targetIds.idB)
|
||||
|
||||
// Verify independent connections
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
|
||||
const pages = browser.contexts()[0].pages()
|
||||
|
||||
@@ -673,7 +675,7 @@ describe('MCP Server Tests', () => {
|
||||
})
|
||||
|
||||
// 3. Verify via CDP that the correct URL is shown
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
// Wait for sync
|
||||
await new Promise(r => setTimeout(r, 1000))
|
||||
|
||||
@@ -1178,7 +1180,7 @@ describe('MCP Server Tests', () => {
|
||||
|
||||
await new Promise(r => setTimeout(r, 2000))
|
||||
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
const pages = browser.contexts()[0].pages()
|
||||
const xPage = pages.find(p => p.url().includes('x.com'))
|
||||
|
||||
@@ -1204,7 +1206,7 @@ describe('MCP Server Tests', () => {
|
||||
})
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
const pages = browser.contexts()[0].pages()
|
||||
const testPage = pages.find(p => p.url().includes('repeated-test'))
|
||||
|
||||
@@ -1244,7 +1246,7 @@ describe('MCP Server Tests', () => {
|
||||
`,
|
||||
},
|
||||
}),
|
||||
chromium.connectOverCDP(getCdpUrl())
|
||||
chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
])
|
||||
|
||||
const mcpOutput = (mcpResult as any).content[0].text
|
||||
@@ -1274,7 +1276,7 @@ describe('MCP Server Tests', () => {
|
||||
await new Promise(r => setTimeout(r, 3000))
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
const pages = browser.contexts()[0].pages()
|
||||
const ytPage = pages.find(p => p.url().includes('youtube.com'))
|
||||
|
||||
@@ -1310,7 +1312,7 @@ describe('MCP Server Tests', () => {
|
||||
}
|
||||
testCtx!.relayServer.on('cdp:command', commandHandler)
|
||||
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
|
||||
|
||||
expect(cdpPage).toBeDefined()
|
||||
@@ -1439,7 +1441,7 @@ describe('MCP Server Tests', () => {
|
||||
}
|
||||
testCtx!.relayServer.on('cdp:command', commandHandler)
|
||||
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
let cdpPage
|
||||
for (const p of browser.contexts()[0].pages()) {
|
||||
const html = await p.content()
|
||||
@@ -1546,11 +1548,11 @@ describe('MCP Server Tests', () => {
|
||||
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
|
||||
expect(cdpPage).toBeDefined()
|
||||
|
||||
const wsUrl = getCdpUrl()
|
||||
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
||||
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
||||
|
||||
const layoutMetrics = await cdpSession.send('Page.getLayoutMetrics')
|
||||
@@ -1626,11 +1628,11 @@ describe('MCP Server Tests', () => {
|
||||
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
|
||||
expect(cdpPage).toBeDefined()
|
||||
|
||||
const wsUrl = getCdpUrl()
|
||||
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
||||
const client = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
||||
|
||||
const layoutMetrics = await client.send('Page.getLayoutMetrics')
|
||||
@@ -1671,7 +1673,7 @@ describe('MCP Server Tests', () => {
|
||||
verbose: 1,
|
||||
disablePino: true,
|
||||
localBrowserLaunchOptions: {
|
||||
cdpUrl: getCdpUrl(),
|
||||
cdpUrl: getCdpUrl({ port: TEST_PORT }),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1796,11 +1798,11 @@ describe('CDP Session Tests', () => {
|
||||
})
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
|
||||
expect(cdpPage).toBeDefined()
|
||||
|
||||
const wsUrl = getCdpUrl()
|
||||
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
||||
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
||||
const dbg = new Debugger({ cdp: cdpSession })
|
||||
|
||||
@@ -1866,11 +1868,11 @@ describe('CDP Session Tests', () => {
|
||||
})
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('news.ycombinator.com'))
|
||||
expect(cdpPage).toBeDefined()
|
||||
|
||||
const wsUrl = getCdpUrl()
|
||||
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
||||
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
||||
const dbg = new Debugger({ cdp: cdpSession })
|
||||
|
||||
@@ -1916,7 +1918,7 @@ describe('CDP Session Tests', () => {
|
||||
})
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
let cdpPage
|
||||
for (const p of browser.contexts()[0].pages()) {
|
||||
const html = await p.content()
|
||||
@@ -1927,7 +1929,7 @@ describe('CDP Session Tests', () => {
|
||||
}
|
||||
expect(cdpPage).toBeDefined()
|
||||
|
||||
const wsUrl = getCdpUrl()
|
||||
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
||||
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
||||
const dbg = new Debugger({ cdp: cdpSession })
|
||||
|
||||
@@ -1965,11 +1967,11 @@ describe('CDP Session Tests', () => {
|
||||
})
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
|
||||
expect(cdpPage).toBeDefined()
|
||||
|
||||
const wsUrl = getCdpUrl()
|
||||
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
||||
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
||||
const dbg = new Debugger({ cdp: cdpSession })
|
||||
|
||||
@@ -2038,11 +2040,11 @@ describe('CDP Session Tests', () => {
|
||||
})
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
|
||||
expect(cdpPage).toBeDefined()
|
||||
|
||||
const wsUrl = getCdpUrl()
|
||||
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
||||
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
||||
await cdpSession.send('Profiler.enable')
|
||||
await cdpSession.send('Profiler.start')
|
||||
@@ -2093,11 +2095,11 @@ describe('CDP Session Tests', () => {
|
||||
})
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
|
||||
expect(cdpPage).toBeDefined()
|
||||
|
||||
const wsUrl = getCdpUrl()
|
||||
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
||||
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
||||
|
||||
const initialTargets = await cdpSession.send('Target.getTargets')
|
||||
@@ -2144,11 +2146,11 @@ describe('CDP Session Tests', () => {
|
||||
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
|
||||
expect(cdpPage).toBeDefined()
|
||||
|
||||
const wsUrl = getCdpUrl()
|
||||
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
||||
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
||||
|
||||
const { targetInfos } = await cdpSession.send('Target.getTargets')
|
||||
@@ -2196,11 +2198,11 @@ describe('CDP Session Tests', () => {
|
||||
await page.goto('https://news.ycombinator.com/', { waitUntil: 'networkidle' })
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('news.ycombinator.com'))
|
||||
expect(cdpPage).toBeDefined()
|
||||
|
||||
const wsUrl = getCdpUrl()
|
||||
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
||||
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
||||
|
||||
const evalResult = await cdpSession.send('Runtime.evaluate', {
|
||||
@@ -2228,11 +2230,11 @@ describe('CDP Session Tests', () => {
|
||||
})
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
|
||||
expect(cdpPage).toBeDefined()
|
||||
|
||||
const wsUrl = getCdpUrl()
|
||||
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
||||
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
||||
|
||||
const initialEvalResult = await cdpSession.send('Runtime.evaluate', {
|
||||
@@ -2280,11 +2282,11 @@ describe('CDP Session Tests', () => {
|
||||
})
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
|
||||
expect(cdpPage).toBeDefined()
|
||||
|
||||
const wsUrl = getCdpUrl()
|
||||
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
||||
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
||||
const dbg = new Debugger({ cdp: cdpSession })
|
||||
|
||||
@@ -2356,7 +2358,7 @@ describe('CDP Session Tests', () => {
|
||||
})
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
let cdpPage
|
||||
for (const p of browser.contexts()[0].pages()) {
|
||||
const html = await p.content()
|
||||
@@ -2367,7 +2369,7 @@ describe('CDP Session Tests', () => {
|
||||
}
|
||||
expect(cdpPage).toBeDefined()
|
||||
|
||||
const wsUrl = getCdpUrl()
|
||||
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
||||
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
||||
const dbg = new Debugger({ cdp: cdpSession })
|
||||
|
||||
@@ -2420,7 +2422,7 @@ describe('CDP Session Tests', () => {
|
||||
})
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
|
||||
expect(cdpPage).toBeDefined()
|
||||
|
||||
@@ -2447,4 +2449,72 @@ describe('CDP Session Tests', () => {
|
||||
await browser.close()
|
||||
await page.close()
|
||||
}, 60000)
|
||||
|
||||
it('should use Editor class to list, read, and edit scripts', async () => {
|
||||
const browserContext = getBrowserContext()
|
||||
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
||||
|
||||
const page = await browserContext.newPage()
|
||||
await page.goto('https://example.com/')
|
||||
await page.bringToFront()
|
||||
|
||||
await serviceWorker.evaluate(async () => {
|
||||
await globalThis.toggleExtensionForActiveTab()
|
||||
})
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
||||
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
|
||||
expect(cdpPage).toBeDefined()
|
||||
|
||||
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
||||
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
||||
const editor = new Editor({ cdp: cdpSession })
|
||||
|
||||
await editor.enable()
|
||||
|
||||
await cdpPage!.addScriptTag({
|
||||
content: `
|
||||
function greetUser(name) {
|
||||
console.log('Hello, ' + name);
|
||||
return 'Hello, ' + name;
|
||||
}
|
||||
`,
|
||||
})
|
||||
await new Promise(r => setTimeout(r, 300))
|
||||
|
||||
const scripts = editor.list()
|
||||
expect(scripts.length).toBeGreaterThan(0)
|
||||
|
||||
const matches = await editor.grep({ regex: /greetUser/ })
|
||||
expect(matches.length).toBeGreaterThan(0)
|
||||
|
||||
const match = matches[0]
|
||||
const { content, totalLines } = await editor.read({ url: match.url })
|
||||
expect(content).toContain('greetUser')
|
||||
expect(totalLines).toBeGreaterThan(0)
|
||||
|
||||
await editor.edit({
|
||||
url: match.url,
|
||||
oldString: "console.log('Hello, ' + name);",
|
||||
newString: "console.log('Hello, ' + name); console.log('EDITOR_TEST_MARKER');",
|
||||
})
|
||||
|
||||
const consoleLogs: string[] = []
|
||||
cdpPage!.on('console', msg => {
|
||||
consoleLogs.push(msg.text())
|
||||
})
|
||||
|
||||
await cdpPage!.evaluate(() => {
|
||||
(window as any).greetUser('World')
|
||||
})
|
||||
await new Promise(r => setTimeout(r, 200))
|
||||
|
||||
expect(consoleLogs).toContain('Hello, World')
|
||||
expect(consoleLogs).toContain('EDITOR_TEST_MARKER')
|
||||
|
||||
cdpSession.close()
|
||||
await browser.close()
|
||||
await page.close()
|
||||
}, 60000)
|
||||
})
|
||||
|
||||
+46
-1
@@ -16,6 +16,7 @@ import { killPortProcess } from 'kill-port-process'
|
||||
import { waitForPageLoad, WaitForPageLoadOptions, WaitForPageLoadResult } from './wait-for-page-load.js'
|
||||
import { getCDPSessionForPage, CDPSession } from './cdp-session.js'
|
||||
import { Debugger } from './debugger.js'
|
||||
import { Editor } from './editor.js'
|
||||
|
||||
class CodeExecutionTimeoutError extends Error {
|
||||
constructor(timeout: number) {
|
||||
@@ -74,6 +75,7 @@ interface VMContext {
|
||||
waitForPageLoad: (options: WaitForPageLoadOptions) => Promise<WaitForPageLoadResult>
|
||||
getCDPSession: (options: { page: Page }) => Promise<CDPSession>
|
||||
createDebugger: (options: { cdp: CDPSession }) => Debugger
|
||||
createEditor: (options: { cdp: CDPSession }) => Editor
|
||||
require: NodeRequire
|
||||
import: (specifier: string) => Promise<any>
|
||||
}
|
||||
@@ -101,7 +103,7 @@ const lastSnapshots: WeakMap<Page, string> = new WeakMap()
|
||||
// Cache CDP sessions per page
|
||||
const cdpSessionCache: WeakMap<Page, CDPSession> = new WeakMap()
|
||||
|
||||
const RELAY_PORT = 19988
|
||||
const RELAY_PORT = Number(process.env.PLAYWRITER_PORT) || 19988
|
||||
const NO_TABS_ERROR = `No browser tabs are connected. Please install and enable the Playwriter extension on at least one tab: https://chromewebstore.google.com/detail/playwriter-mcp/jfeammnjpkecdekppnclgkkffahnhfhe`
|
||||
|
||||
async function setDeviceScaleFactorForMacOS(context: BrowserContext): Promise<void> {
|
||||
@@ -426,6 +428,43 @@ server.resource('debugger-api', 'playwriter://debugger-api', { mimeType: 'text/p
|
||||
}
|
||||
})
|
||||
|
||||
server.resource('editor-api', 'playwriter://editor-api', { mimeType: 'text/plain' }, async () => {
|
||||
const packageJsonPath = require.resolve('playwriter/package.json')
|
||||
const distDir = path.join(path.dirname(packageJsonPath), 'dist')
|
||||
|
||||
const editorTypes = fs
|
||||
.readFileSync(path.join(distDir, 'editor.d.ts'), 'utf-8')
|
||||
.replace(/\/\/# sourceMappingURL=.*$/gm, '')
|
||||
.trim()
|
||||
const editorExamples = fs.readFileSync(path.join(distDir, 'editor-examples.ts'), 'utf-8')
|
||||
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: 'playwriter://editor-api',
|
||||
text: dedent`
|
||||
# Editor API Reference
|
||||
|
||||
The Editor class provides a Claude Code-like interface for viewing and editing web page scripts at runtime.
|
||||
|
||||
## Types
|
||||
|
||||
\`\`\`ts
|
||||
${editorTypes}
|
||||
\`\`\`
|
||||
|
||||
## Examples
|
||||
|
||||
\`\`\`ts
|
||||
${editorExamples}
|
||||
\`\`\`
|
||||
`,
|
||||
mimeType: 'text/plain',
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
server.tool(
|
||||
'execute',
|
||||
promptContent,
|
||||
@@ -625,6 +664,10 @@ server.tool(
|
||||
return new Debugger(options)
|
||||
}
|
||||
|
||||
const createEditor = (options: { cdp: CDPSession }) => {
|
||||
return new Editor(options)
|
||||
}
|
||||
|
||||
let vmContextObj: VMContextWithGlobals = {
|
||||
page,
|
||||
context,
|
||||
@@ -637,6 +680,7 @@ server.tool(
|
||||
waitForPageLoad,
|
||||
getCDPSession,
|
||||
createDebugger,
|
||||
createEditor,
|
||||
resetPlaywright: async () => {
|
||||
const { page: newPage, context: newContext } = await resetConnection()
|
||||
|
||||
@@ -652,6 +696,7 @@ server.tool(
|
||||
waitForPageLoad,
|
||||
getCDPSession,
|
||||
createDebugger,
|
||||
createEditor,
|
||||
resetPlaywright: vmContextObj.resetPlaywright,
|
||||
require,
|
||||
// TODO --experimental-vm-modules is needed to make import work in vm
|
||||
|
||||
@@ -97,14 +97,38 @@ you have access to some functions in addition to playwright methods:
|
||||
- Example: `const cdp = await getCDPSession({ page }); const metrics = await cdp.send('Page.getLayoutMetrics');`
|
||||
- `createDebugger({ cdp })`: creates a Debugger instance for setting breakpoints, stepping, and inspecting variables. Works with browser JS or Node.js (--inspect).
|
||||
- `cdp`: a CDPSession from `getCDPSession`
|
||||
- Methods: `enable()`, `setBreakpoint({ file, line })`, `deleteBreakpoint({ breakpointId })`, `listBreakpoints()`, `listScripts({ search? })`, `evaluate({ expression })`, `inspectVariables({ scope? })`, `getLocation()`, `stepOver()`, `stepInto()`, `stepOut()`, `resume()`, `isPaused()`
|
||||
- Methods: `enable()`, `setBreakpoint({ file, line, condition? })`, `deleteBreakpoint({ breakpointId })`, `listBreakpoints()`, `listScripts({ search? })`, `evaluate({ expression })`, `inspectLocalVariables()`, `getLocation()`, `stepOver()`, `stepInto()`, `stepOut()`, `resume()`, `isPaused()`, `setXHRBreakpoint({ url })`, `removeXHRBreakpoint({ url })`, `listXHRBreakpoints()`, `setBlackboxPatterns({ patterns })`, `addBlackboxPattern({ pattern })`, `removeBlackboxPattern({ pattern })`, `listBlackboxPatterns()`
|
||||
- Example:
|
||||
```js
|
||||
const cdp = await getCDPSession({ page }); const dbg = createDebugger({ cdp }); await dbg.enable();
|
||||
console.log(dbg.listScripts({ search: 'app' }));
|
||||
await dbg.setBreakpoint({ file: 'https://example.com/app.js', line: 42 });
|
||||
// conditional breakpoint - only pause when userId is 123
|
||||
await dbg.setBreakpoint({ file: 'app.js', line: 50, condition: 'userId === 123' });
|
||||
// XHR breakpoint - pause when fetch/XHR URL contains '/api'
|
||||
await dbg.setXHRBreakpoint({ url: '/api' });
|
||||
// blackbox framework code when stepping
|
||||
await dbg.setBlackboxPatterns({ patterns: ['node_modules/'] });
|
||||
// user triggers the code, then:
|
||||
if (dbg.isPaused()) { console.log(await dbg.getLocation()); console.log(await dbg.inspectVariables()); await dbg.resume(); }
|
||||
if (dbg.isPaused()) { console.log(await dbg.getLocation()); console.log(await dbg.inspectLocalVariables()); await dbg.resume(); }
|
||||
```
|
||||
- `createEditor({ cdp })`: creates an Editor instance for viewing and live-editing page scripts. Provides a Claude Code-like interface.
|
||||
- `cdp`: a CDPSession from `getCDPSession`
|
||||
- Methods: `enable()`, `list({ search? })`, `read({ url, offset?, limit? })`, `edit({ url, oldString, newString, dryRun? })`, `grep({ regex, include? })`, `write({ url, content, dryRun? })`
|
||||
- Inline scripts (no URL) get synthetic URLs like `inline://42` - use grep() to find them by content
|
||||
- Example:
|
||||
```js
|
||||
const cdp = await getCDPSession({ page }); const editor = createEditor({ cdp }); await editor.enable();
|
||||
// list available scripts (inline scripts have inline:// URLs)
|
||||
console.log(editor.list({ search: 'app' }));
|
||||
// read a script with line numbers (like Claude Code Read tool)
|
||||
const { content, totalLines } = await editor.read({ url: 'https://example.com/app.js', offset: 0, limit: 50 });
|
||||
console.log(content);
|
||||
// edit a script (like Claude Code Edit tool) - exact string replacement
|
||||
await editor.edit({ url: 'https://example.com/app.js', oldString: 'DEBUG = false', newString: 'DEBUG = true' });
|
||||
// search across all scripts (like Grep) - useful for finding inline scripts
|
||||
const matches = await editor.grep({ regex: /myFunction/ });
|
||||
if (matches.length > 0) { await editor.edit({ url: matches[0].url, oldString: 'return false', newString: 'return true' }); }
|
||||
```
|
||||
|
||||
example:
|
||||
|
||||
Reference in New Issue
Block a user