nicer logging of returned output
This commit is contained in:
+112
-96
@@ -61,6 +61,7 @@ const EXTENSION_NOT_CONNECTED_ERROR = `The Playwriter Chrome extension is not co
|
|||||||
const NO_TABS_ENABLED_ERROR = `No browser tabs have Playwriter enabled. Click the extension icon on at least one tab to enable it.`
|
const NO_TABS_ENABLED_ERROR = `No browser tabs have Playwriter enabled. Click the extension icon on at least one tab to enable it.`
|
||||||
|
|
||||||
const MAX_LOGS_PER_PAGE = 5000
|
const MAX_LOGS_PER_PAGE = 5000
|
||||||
|
const DEFAULT_PLAYWRIGHT_TIMEOUT = 10_000
|
||||||
|
|
||||||
const ALLOWED_MODULES = new Set([
|
const ALLOWED_MODULES = new Set([
|
||||||
'path', 'node:path',
|
'path', 'node:path',
|
||||||
@@ -113,22 +114,28 @@ function isRegExp(value: any): value is RegExp {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPromise(value: any): value is Promise<unknown> {
|
||||||
|
return typeof value === 'object' && value !== null && typeof value.then === 'function'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export class PlaywrightExecutor {
|
export class PlaywrightExecutor {
|
||||||
private isConnected = false
|
private isConnected = false
|
||||||
private page: Page | null = null
|
private page: Page | null = null
|
||||||
private browser: Browser | null = null
|
private browser: Browser | null = null
|
||||||
private context: BrowserContext | null = null
|
private context: BrowserContext | null = null
|
||||||
|
|
||||||
private userState: Record<string, any> = {}
|
private userState: Record<string, any> = {}
|
||||||
private browserLogs: Map<string, string[]> = new Map()
|
private browserLogs: Map<string, string[]> = new Map()
|
||||||
private lastSnapshots: WeakMap<Page, string> = new WeakMap()
|
private lastSnapshots: WeakMap<Page, string> = new WeakMap()
|
||||||
private cdpSessionCache: WeakMap<Page, CDPSession> = new WeakMap()
|
private cdpSessionCache: WeakMap<Page, CDPSession> = new WeakMap()
|
||||||
private scopedFs: ScopedFS
|
private scopedFs: ScopedFS
|
||||||
private sandboxedRequire: NodeRequire
|
private sandboxedRequire: NodeRequire
|
||||||
|
|
||||||
private cdpConfig: CdpConfig
|
private cdpConfig: CdpConfig
|
||||||
private logger: ExecutorLogger
|
private logger: ExecutorLogger
|
||||||
|
|
||||||
constructor(options: ExecutorOptions) {
|
constructor(options: ExecutorOptions) {
|
||||||
this.cdpConfig = options.cdpConfig
|
this.cdpConfig = options.cdpConfig
|
||||||
this.logger = options.logger || { log: console.log, error: console.error }
|
this.logger = options.logger || { log: console.log, error: console.error }
|
||||||
@@ -136,7 +143,7 @@ export class PlaywrightExecutor {
|
|||||||
this.scopedFs = new ScopedFS(options.cwd ? [options.cwd, '/tmp', os.tmpdir()] : undefined)
|
this.scopedFs = new ScopedFS(options.cwd ? [options.cwd, '/tmp', os.tmpdir()] : undefined)
|
||||||
this.sandboxedRequire = this.createSandboxedRequire(require)
|
this.sandboxedRequire = this.createSandboxedRequire(require)
|
||||||
}
|
}
|
||||||
|
|
||||||
private createSandboxedRequire(originalRequire: NodeRequire): NodeRequire {
|
private createSandboxedRequire(originalRequire: NodeRequire): NodeRequire {
|
||||||
const scopedFs = this.scopedFs
|
const scopedFs = this.scopedFs
|
||||||
const sandboxedRequire = ((id: string) => {
|
const sandboxedRequire = ((id: string) => {
|
||||||
@@ -153,15 +160,15 @@ export class PlaywrightExecutor {
|
|||||||
}
|
}
|
||||||
return originalRequire(id)
|
return originalRequire(id)
|
||||||
}) as NodeRequire
|
}) as NodeRequire
|
||||||
|
|
||||||
sandboxedRequire.resolve = originalRequire.resolve
|
sandboxedRequire.resolve = originalRequire.resolve
|
||||||
sandboxedRequire.cache = originalRequire.cache
|
sandboxedRequire.cache = originalRequire.cache
|
||||||
sandboxedRequire.extensions = originalRequire.extensions
|
sandboxedRequire.extensions = originalRequire.extensions
|
||||||
sandboxedRequire.main = originalRequire.main
|
sandboxedRequire.main = originalRequire.main
|
||||||
|
|
||||||
return sandboxedRequire
|
return sandboxedRequire
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setDeviceScaleFactorForMacOS(context: BrowserContext): Promise<void> {
|
private async setDeviceScaleFactorForMacOS(context: BrowserContext): Promise<void> {
|
||||||
if (os.platform() !== 'darwin') {
|
if (os.platform() !== 'darwin') {
|
||||||
return
|
return
|
||||||
@@ -172,7 +179,7 @@ export class PlaywrightExecutor {
|
|||||||
}
|
}
|
||||||
options.deviceScaleFactor = 2
|
options.deviceScaleFactor = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
private async preserveSystemColorScheme(context: BrowserContext): Promise<void> {
|
private async preserveSystemColorScheme(context: BrowserContext): Promise<void> {
|
||||||
const options = (context as any)._options
|
const options = (context as any)._options
|
||||||
if (!options) {
|
if (!options) {
|
||||||
@@ -182,38 +189,38 @@ export class PlaywrightExecutor {
|
|||||||
options.reducedMotion = 'no-override'
|
options.reducedMotion = 'no-override'
|
||||||
options.forcedColors = 'no-override'
|
options.forcedColors = 'no-override'
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearUserState() {
|
private clearUserState() {
|
||||||
Object.keys(this.userState).forEach((key) => delete this.userState[key])
|
Object.keys(this.userState).forEach((key) => delete this.userState[key])
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearConnectionState() {
|
private clearConnectionState() {
|
||||||
this.isConnected = false
|
this.isConnected = false
|
||||||
this.browser = null
|
this.browser = null
|
||||||
this.page = null
|
this.page = null
|
||||||
this.context = null
|
this.context = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupPageConsoleListener(page: Page) {
|
private setupPageConsoleListener(page: Page) {
|
||||||
const targetId = (page as any)._guid as string | undefined
|
const targetId = (page as any)._guid as string | undefined
|
||||||
if (!targetId) {
|
if (!targetId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.browserLogs.has(targetId)) {
|
if (!this.browserLogs.has(targetId)) {
|
||||||
this.browserLogs.set(targetId, [])
|
this.browserLogs.set(targetId, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
page.on('framenavigated', (frame) => {
|
page.on('framenavigated', (frame) => {
|
||||||
if (frame === page.mainFrame()) {
|
if (frame === page.mainFrame()) {
|
||||||
this.browserLogs.set(targetId, [])
|
this.browserLogs.set(targetId, [])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
page.on('close', () => {
|
page.on('close', () => {
|
||||||
this.browserLogs.delete(targetId)
|
this.browserLogs.delete(targetId)
|
||||||
})
|
})
|
||||||
|
|
||||||
page.on('console', (msg) => {
|
page.on('console', (msg) => {
|
||||||
try {
|
try {
|
||||||
const logEntry = `[${msg.type()}] ${msg.text()}`
|
const logEntry = `[${msg.type()}] ${msg.text()}`
|
||||||
@@ -230,7 +237,7 @@ export class PlaywrightExecutor {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async checkExtensionStatus(): Promise<{ connected: boolean; activeTargets: number }> {
|
private async checkExtensionStatus(): Promise<{ connected: boolean; activeTargets: number }> {
|
||||||
const { host = '127.0.0.1', port = 19988 } = this.cdpConfig
|
const { host = '127.0.0.1', port = 19988 } = this.cdpConfig
|
||||||
try {
|
try {
|
||||||
@@ -245,58 +252,60 @@ export class PlaywrightExecutor {
|
|||||||
return { connected: false, activeTargets: 0 }
|
return { connected: false, activeTargets: 0 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ensureConnection(): Promise<{ browser: Browser; page: Page }> {
|
private async ensureConnection(): Promise<{ browser: Browser; page: Page }> {
|
||||||
if (this.isConnected && this.browser && this.page) {
|
if (this.isConnected && this.browser && this.page) {
|
||||||
return { browser: this.browser, page: this.page }
|
return { browser: this.browser, page: this.page }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check extension status first to provide better error messages
|
// Check extension status first to provide better error messages
|
||||||
const extensionStatus = await this.checkExtensionStatus()
|
const extensionStatus = await this.checkExtensionStatus()
|
||||||
if (!extensionStatus.connected) {
|
if (!extensionStatus.connected) {
|
||||||
throw new Error(EXTENSION_NOT_CONNECTED_ERROR)
|
throw new Error(EXTENSION_NOT_CONNECTED_ERROR)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a fresh unique URL for each Playwright connection
|
// Generate a fresh unique URL for each Playwright connection
|
||||||
const cdpUrl = getCdpUrl(this.cdpConfig)
|
const cdpUrl = getCdpUrl(this.cdpConfig)
|
||||||
const browser = await chromium.connectOverCDP(cdpUrl)
|
const browser = await chromium.connectOverCDP(cdpUrl)
|
||||||
|
|
||||||
browser.on('disconnected', () => {
|
browser.on('disconnected', () => {
|
||||||
this.logger.log('Browser disconnected, clearing connection state')
|
this.logger.log('Browser disconnected, clearing connection state')
|
||||||
this.clearConnectionState()
|
this.clearConnectionState()
|
||||||
})
|
})
|
||||||
|
|
||||||
const contexts = browser.contexts()
|
const contexts = browser.contexts()
|
||||||
const context = contexts.length > 0 ? contexts[0] : await browser.newContext()
|
const context = contexts.length > 0 ? contexts[0] : await browser.newContext()
|
||||||
|
|
||||||
context.on('page', (page) => {
|
context.on('page', (page) => {
|
||||||
this.setupPageConsoleListener(page)
|
this.setupPageConsoleListener(page)
|
||||||
})
|
})
|
||||||
|
|
||||||
const pages = context.pages()
|
const pages = context.pages()
|
||||||
if (pages.length === 0) {
|
if (pages.length === 0) {
|
||||||
throw new Error(NO_TABS_ENABLED_ERROR)
|
throw new Error(NO_TABS_ENABLED_ERROR)
|
||||||
}
|
}
|
||||||
const page = pages[0]
|
const page = pages[0]
|
||||||
|
|
||||||
|
context.setDefaultTimeout(DEFAULT_PLAYWRIGHT_TIMEOUT)
|
||||||
|
|
||||||
context.pages().forEach((p) => this.setupPageConsoleListener(p))
|
context.pages().forEach((p) => this.setupPageConsoleListener(p))
|
||||||
|
|
||||||
await this.preserveSystemColorScheme(context)
|
await this.preserveSystemColorScheme(context)
|
||||||
await this.setDeviceScaleFactorForMacOS(context)
|
await this.setDeviceScaleFactorForMacOS(context)
|
||||||
|
|
||||||
this.browser = browser
|
this.browser = browser
|
||||||
this.page = page
|
this.page = page
|
||||||
this.context = context
|
this.context = context
|
||||||
this.isConnected = true
|
this.isConnected = true
|
||||||
|
|
||||||
return { browser, page }
|
return { browser, page }
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getCurrentPage(timeout = 10000): Promise<Page> {
|
private async getCurrentPage(timeout = 10000): Promise<Page> {
|
||||||
if (this.page && !this.page.isClosed()) {
|
if (this.page && !this.page.isClosed()) {
|
||||||
return this.page
|
return this.page
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.browser) {
|
if (this.browser) {
|
||||||
const contexts = this.browser.contexts()
|
const contexts = this.browser.contexts()
|
||||||
if (contexts.length > 0) {
|
if (contexts.length > 0) {
|
||||||
@@ -309,10 +318,10 @@ export class PlaywrightExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(NO_TABS_ENABLED_ERROR)
|
throw new Error(NO_TABS_ENABLED_ERROR)
|
||||||
}
|
}
|
||||||
|
|
||||||
async reset(): Promise<{ page: Page; context: BrowserContext }> {
|
async reset(): Promise<{ page: Page; context: BrowserContext }> {
|
||||||
if (this.browser) {
|
if (this.browser) {
|
||||||
try {
|
try {
|
||||||
@@ -321,54 +330,56 @@ export class PlaywrightExecutor {
|
|||||||
this.logger.error('Error closing browser:', e)
|
this.logger.error('Error closing browser:', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.clearConnectionState()
|
this.clearConnectionState()
|
||||||
this.clearUserState()
|
this.clearUserState()
|
||||||
|
|
||||||
// Check extension status first to provide better error messages
|
// Check extension status first to provide better error messages
|
||||||
const extensionStatus = await this.checkExtensionStatus()
|
const extensionStatus = await this.checkExtensionStatus()
|
||||||
if (!extensionStatus.connected) {
|
if (!extensionStatus.connected) {
|
||||||
throw new Error(EXTENSION_NOT_CONNECTED_ERROR)
|
throw new Error(EXTENSION_NOT_CONNECTED_ERROR)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a fresh unique URL for each Playwright connection
|
// Generate a fresh unique URL for each Playwright connection
|
||||||
const cdpUrl = getCdpUrl(this.cdpConfig)
|
const cdpUrl = getCdpUrl(this.cdpConfig)
|
||||||
const browser = await chromium.connectOverCDP(cdpUrl)
|
const browser = await chromium.connectOverCDP(cdpUrl)
|
||||||
|
|
||||||
browser.on('disconnected', () => {
|
browser.on('disconnected', () => {
|
||||||
this.logger.log('Browser disconnected, clearing connection state')
|
this.logger.log('Browser disconnected, clearing connection state')
|
||||||
this.clearConnectionState()
|
this.clearConnectionState()
|
||||||
})
|
})
|
||||||
|
|
||||||
const contexts = browser.contexts()
|
const contexts = browser.contexts()
|
||||||
const context = contexts.length > 0 ? contexts[0] : await browser.newContext()
|
const context = contexts.length > 0 ? contexts[0] : await browser.newContext()
|
||||||
|
|
||||||
context.on('page', (page) => {
|
context.on('page', (page) => {
|
||||||
this.setupPageConsoleListener(page)
|
this.setupPageConsoleListener(page)
|
||||||
})
|
})
|
||||||
|
|
||||||
const pages = context.pages()
|
const pages = context.pages()
|
||||||
if (pages.length === 0) {
|
if (pages.length === 0) {
|
||||||
throw new Error(NO_TABS_ENABLED_ERROR)
|
throw new Error(NO_TABS_ENABLED_ERROR)
|
||||||
}
|
}
|
||||||
const page = pages[0]
|
const page = pages[0]
|
||||||
|
|
||||||
|
context.setDefaultTimeout(DEFAULT_PLAYWRIGHT_TIMEOUT)
|
||||||
|
|
||||||
context.pages().forEach((p) => this.setupPageConsoleListener(p))
|
context.pages().forEach((p) => this.setupPageConsoleListener(p))
|
||||||
|
|
||||||
await this.preserveSystemColorScheme(context)
|
await this.preserveSystemColorScheme(context)
|
||||||
await this.setDeviceScaleFactorForMacOS(context)
|
await this.setDeviceScaleFactorForMacOS(context)
|
||||||
|
|
||||||
this.browser = browser
|
this.browser = browser
|
||||||
this.page = page
|
this.page = page
|
||||||
this.context = context
|
this.context = context
|
||||||
this.isConnected = true
|
this.isConnected = true
|
||||||
|
|
||||||
return { page, context }
|
return { page, context }
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(code: string, timeout = 10000): Promise<ExecuteResult> {
|
async execute(code: string, timeout = 10000): Promise<ExecuteResult> {
|
||||||
const consoleLogs: Array<{ method: string; args: any[] }> = []
|
const consoleLogs: Array<{ method: string; args: any[] }> = []
|
||||||
|
|
||||||
const formatConsoleLogs = (logs: Array<{ method: string; args: any[] }>, prefix = 'Console output') => {
|
const formatConsoleLogs = (logs: Array<{ method: string; args: any[] }>, prefix = 'Console output') => {
|
||||||
if (logs.length === 0) {
|
if (logs.length === 0) {
|
||||||
return ''
|
return ''
|
||||||
@@ -384,14 +395,14 @@ export class PlaywrightExecutor {
|
|||||||
})
|
})
|
||||||
return text + '\n'
|
return text + '\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.ensureConnection()
|
await this.ensureConnection()
|
||||||
const page = await this.getCurrentPage(timeout)
|
const page = await this.getCurrentPage(timeout)
|
||||||
const context = this.context || page.context()
|
const context = this.context || page.context()
|
||||||
|
|
||||||
this.logger.log('Executing code:', code)
|
this.logger.log('Executing code:', code)
|
||||||
|
|
||||||
const customConsole = {
|
const customConsole = {
|
||||||
log: (...args: any[]) => { consoleLogs.push({ method: 'log', args }) },
|
log: (...args: any[]) => { consoleLogs.push({ method: 'log', args }) },
|
||||||
info: (...args: any[]) => { consoleLogs.push({ method: 'info', args }) },
|
info: (...args: any[]) => { consoleLogs.push({ method: 'info', args }) },
|
||||||
@@ -399,7 +410,7 @@ export class PlaywrightExecutor {
|
|||||||
error: (...args: any[]) => { consoleLogs.push({ method: 'error', args }) },
|
error: (...args: any[]) => { consoleLogs.push({ method: 'error', args }) },
|
||||||
debug: (...args: any[]) => { consoleLogs.push({ method: 'debug', args }) },
|
debug: (...args: any[]) => { consoleLogs.push({ method: 'debug', args }) },
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessibilitySnapshot = async (options: {
|
const accessibilitySnapshot = async (options: {
|
||||||
page: Page
|
page: Page
|
||||||
search?: string | RegExp
|
search?: string | RegExp
|
||||||
@@ -414,7 +425,7 @@ export class PlaywrightExecutor {
|
|||||||
const sanitizedStr = rawStr.toWellFormed?.() ?? rawStr
|
const sanitizedStr = rawStr.toWellFormed?.() ?? rawStr
|
||||||
// Apply format transformation
|
// Apply format transformation
|
||||||
const snapshotStr = formatSnapshot(sanitizedStr, format, this.logger)
|
const snapshotStr = formatSnapshot(sanitizedStr, format, this.logger)
|
||||||
|
|
||||||
if (showDiffSinceLastCall) {
|
if (showDiffSinceLastCall) {
|
||||||
const previousSnapshot = this.lastSnapshots.get(targetPage)
|
const previousSnapshot = this.lastSnapshots.get(targetPage)
|
||||||
if (!previousSnapshot) {
|
if (!previousSnapshot) {
|
||||||
@@ -427,13 +438,13 @@ export class PlaywrightExecutor {
|
|||||||
}
|
}
|
||||||
return patch
|
return patch
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastSnapshots.set(targetPage, snapshotStr)
|
this.lastSnapshots.set(targetPage, snapshotStr)
|
||||||
|
|
||||||
if (!search) {
|
if (!search) {
|
||||||
return snapshotStr
|
return snapshotStr
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = snapshotStr.split('\n')
|
const lines = snapshotStr.split('\n')
|
||||||
const matchIndices: number[] = []
|
const matchIndices: number[] = []
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
@@ -444,11 +455,11 @@ export class PlaywrightExecutor {
|
|||||||
if (matchIndices.length >= 10) break
|
if (matchIndices.length >= 10) break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchIndices.length === 0) {
|
if (matchIndices.length === 0) {
|
||||||
return 'No matches found'
|
return 'No matches found'
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONTEXT_LINES = 5
|
const CONTEXT_LINES = 5
|
||||||
const includedLines = new Set<number>()
|
const includedLines = new Set<number>()
|
||||||
for (const idx of matchIndices) {
|
for (const idx of matchIndices) {
|
||||||
@@ -458,7 +469,7 @@ export class PlaywrightExecutor {
|
|||||||
includedLines.add(i)
|
includedLines.add(i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortedIndices = [...includedLines].sort((a, b) => a - b)
|
const sortedIndices = [...includedLines].sort((a, b) => a - b)
|
||||||
const result: string[] = []
|
const result: string[] = []
|
||||||
for (let i = 0; i < sortedIndices.length; i++) {
|
for (let i = 0; i < sortedIndices.length; i++) {
|
||||||
@@ -472,7 +483,7 @@ export class PlaywrightExecutor {
|
|||||||
}
|
}
|
||||||
throw new Error('accessibilitySnapshot is not available on this page')
|
throw new Error('accessibilitySnapshot is not available on this page')
|
||||||
}
|
}
|
||||||
|
|
||||||
const getLocatorStringForElement = async (element: any) => {
|
const getLocatorStringForElement = async (element: any) => {
|
||||||
if (!element || typeof element.evaluate !== 'function') {
|
if (!element || typeof element.evaluate !== 'function') {
|
||||||
throw new Error('getLocatorStringForElement: argument must be a Playwright Locator or ElementHandle')
|
throw new Error('getLocatorStringForElement: argument must be a Playwright Locator or ElementHandle')
|
||||||
@@ -492,17 +503,17 @@ export class PlaywrightExecutor {
|
|||||||
return toLocator(result.selector, 'javascript')
|
return toLocator(result.selector, 'javascript')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPageTargetId = async (p: Page): Promise<string> => {
|
const getPageTargetId = async (p: Page): Promise<string> => {
|
||||||
const guid = (p as any)._guid
|
const guid = (p as any)._guid
|
||||||
if (guid) return guid
|
if (guid) return guid
|
||||||
throw new Error('Could not get page identifier: _guid not available')
|
throw new Error('Could not get page identifier: _guid not available')
|
||||||
}
|
}
|
||||||
|
|
||||||
const getLatestLogs = async (options?: { page?: Page; count?: number; search?: string | RegExp }) => {
|
const getLatestLogs = async (options?: { page?: Page; count?: number; search?: string | RegExp }) => {
|
||||||
const { page: filterPage, count, search } = options || {}
|
const { page: filterPage, count, search } = options || {}
|
||||||
let allLogs: string[] = []
|
let allLogs: string[] = []
|
||||||
|
|
||||||
if (filterPage) {
|
if (filterPage) {
|
||||||
const targetId = await getPageTargetId(filterPage)
|
const targetId = await getPageTargetId(filterPage)
|
||||||
const pageLogs = this.browserLogs.get(targetId) || []
|
const pageLogs = this.browserLogs.get(targetId) || []
|
||||||
@@ -512,7 +523,7 @@ export class PlaywrightExecutor {
|
|||||||
allLogs.push(...pageLogs)
|
allLogs.push(...pageLogs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
const matchIndices: number[] = []
|
const matchIndices: number[] = []
|
||||||
for (let i = 0; i < allLogs.length; i++) {
|
for (let i = 0; i < allLogs.length; i++) {
|
||||||
@@ -520,7 +531,7 @@ export class PlaywrightExecutor {
|
|||||||
const isMatch = typeof search === 'string' ? log.includes(search) : isRegExp(search) && search.test(log)
|
const isMatch = typeof search === 'string' ? log.includes(search) : isRegExp(search) && search.test(log)
|
||||||
if (isMatch) matchIndices.push(i)
|
if (isMatch) matchIndices.push(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONTEXT_LINES = 5
|
const CONTEXT_LINES = 5
|
||||||
const includedIndices = new Set<number>()
|
const includedIndices = new Set<number>()
|
||||||
for (const idx of matchIndices) {
|
for (const idx of matchIndices) {
|
||||||
@@ -530,7 +541,7 @@ export class PlaywrightExecutor {
|
|||||||
includedIndices.add(i)
|
includedIndices.add(i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortedIndices = [...includedIndices].sort((a, b) => a - b)
|
const sortedIndices = [...includedIndices].sort((a, b) => a - b)
|
||||||
const result: string[] = []
|
const result: string[] = []
|
||||||
for (let i = 0; i < sortedIndices.length; i++) {
|
for (let i = 0; i < sortedIndices.length; i++) {
|
||||||
@@ -542,14 +553,14 @@ export class PlaywrightExecutor {
|
|||||||
}
|
}
|
||||||
allLogs = result
|
allLogs = result
|
||||||
}
|
}
|
||||||
|
|
||||||
return count !== undefined ? allLogs.slice(-count) : allLogs
|
return count !== undefined ? allLogs.slice(-count) : allLogs
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearAllLogs = () => {
|
const clearAllLogs = () => {
|
||||||
this.browserLogs.clear()
|
this.browserLogs.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCDPSession = async (options: { page: Page }) => {
|
const getCDPSession = async (options: { page: Page }) => {
|
||||||
const cached = this.cdpSessionCache.get(options.page)
|
const cached = this.cdpSessionCache.get(options.page)
|
||||||
if (cached) return cached
|
if (cached) return cached
|
||||||
@@ -559,22 +570,22 @@ export class PlaywrightExecutor {
|
|||||||
this.cdpSessionCache.set(options.page, session)
|
this.cdpSessionCache.set(options.page, session)
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
const createDebugger = (options: { cdp: ICDPSession }) => new Debugger(options)
|
const createDebugger = (options: { cdp: ICDPSession }) => new Debugger(options)
|
||||||
const createEditor = (options: { cdp: ICDPSession }) => new Editor(options)
|
const createEditor = (options: { cdp: ICDPSession }) => new Editor(options)
|
||||||
|
|
||||||
const getStylesForLocatorFn = async (options: { locator: any }) => {
|
const getStylesForLocatorFn = async (options: { locator: any }) => {
|
||||||
const cdp = await getCDPSession({ page: options.locator.page() })
|
const cdp = await getCDPSession({ page: options.locator.page() })
|
||||||
return getStylesForLocator({ locator: options.locator, cdp })
|
return getStylesForLocator({ locator: options.locator, cdp })
|
||||||
}
|
}
|
||||||
|
|
||||||
const getReactSourceFn = async (options: { locator: any }) => {
|
const getReactSourceFn = async (options: { locator: any }) => {
|
||||||
const cdp = await getCDPSession({ page: options.locator.page() })
|
const cdp = await getCDPSession({ page: options.locator.page() })
|
||||||
return getReactSource({ locator: options.locator, cdp })
|
return getReactSource({ locator: options.locator, cdp })
|
||||||
}
|
}
|
||||||
|
|
||||||
const screenshotCollector: ScreenshotResult[] = []
|
const screenshotCollector: ScreenshotResult[] = []
|
||||||
|
|
||||||
const screenshotWithAccessibilityLabelsFn = async (options: { page: Page; interactiveOnly?: boolean }) => {
|
const screenshotWithAccessibilityLabelsFn = async (options: { page: Page; interactiveOnly?: boolean }) => {
|
||||||
return screenshotWithAccessibilityLabels({
|
return screenshotWithAccessibilityLabels({
|
||||||
...options,
|
...options,
|
||||||
@@ -585,9 +596,9 @@ export class PlaywrightExecutor {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const self = this
|
const self = this
|
||||||
|
|
||||||
let vmContextObj: any = {
|
let vmContextObj: any = {
|
||||||
page,
|
page,
|
||||||
context,
|
context,
|
||||||
@@ -616,54 +627,59 @@ export class PlaywrightExecutor {
|
|||||||
import: (specifier: string) => import(specifier),
|
import: (specifier: string) => import(specifier),
|
||||||
...usefulGlobals,
|
...usefulGlobals,
|
||||||
}
|
}
|
||||||
|
|
||||||
const vmContext = vm.createContext(vmContextObj)
|
const vmContext = vm.createContext(vmContextObj)
|
||||||
const wrappedCode = `(async () => { ${code} })()`
|
const wrappedCode = `(async () => { ${code} })()`
|
||||||
|
const hasExplicitReturn = /\breturn\b/.test(code)
|
||||||
|
|
||||||
const result = await Promise.race([
|
const result = await Promise.race([
|
||||||
vm.runInContext(wrappedCode, vmContext, { timeout, displayErrors: true }),
|
vm.runInContext(wrappedCode, vmContext, { timeout, displayErrors: true }),
|
||||||
new Promise((_, reject) => setTimeout(() => reject(new CodeExecutionTimeoutError(timeout)), timeout)),
|
new Promise((_, reject) => setTimeout(() => reject(new CodeExecutionTimeoutError(timeout)), timeout)),
|
||||||
])
|
])
|
||||||
|
|
||||||
let responseText = formatConsoleLogs(consoleLogs)
|
let responseText = formatConsoleLogs(consoleLogs)
|
||||||
|
|
||||||
if (result !== undefined) {
|
// Only show return value if user explicitly used return
|
||||||
responseText += 'Return value:\n'
|
if (hasExplicitReturn) {
|
||||||
if (typeof result === 'string') {
|
const resolvedResult = isPromise(result) ? await result : result
|
||||||
responseText += result
|
if (resolvedResult !== undefined) {
|
||||||
} else {
|
const formatted = util.inspect(resolvedResult, { depth: 4, colors: false, maxArrayLength: 100, breakLength: 80 })
|
||||||
responseText += JSON.stringify(result, null, 2)
|
if (formatted.trim()) {
|
||||||
|
responseText += `[return value] ${formatted}\n`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (consoleLogs.length === 0) {
|
|
||||||
responseText += 'Code executed successfully (no output)'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!responseText.trim()) {
|
||||||
|
responseText = 'Code executed successfully (no output)'
|
||||||
|
}
|
||||||
|
|
||||||
for (const screenshot of screenshotCollector) {
|
for (const screenshot of screenshotCollector) {
|
||||||
responseText += `\nScreenshot saved to: ${screenshot.path}\n`
|
responseText += `\nScreenshot saved to: ${screenshot.path}\n`
|
||||||
responseText += `Labels shown: ${screenshot.labelCount}\n\n`
|
responseText += `Labels shown: ${screenshot.labelCount}\n\n`
|
||||||
responseText += `Accessibility snapshot:\n${screenshot.snapshot}\n`
|
responseText += `Accessibility snapshot:\n${screenshot.snapshot}\n`
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_LENGTH = 6000
|
const MAX_LENGTH = 6000
|
||||||
let finalText = responseText.trim()
|
let finalText = responseText.trim()
|
||||||
if (finalText.length > MAX_LENGTH) {
|
if (finalText.length > MAX_LENGTH) {
|
||||||
finalText = finalText.slice(0, MAX_LENGTH) +
|
finalText = finalText.slice(0, MAX_LENGTH) +
|
||||||
`\n\n[Truncated to ${MAX_LENGTH} characters. Better manage your logs or paginate them to read the full logs]`
|
`\n\n[Truncated to ${MAX_LENGTH} characters. Better manage your logs or paginate them to read the full logs]`
|
||||||
}
|
}
|
||||||
|
|
||||||
const images = screenshotCollector.map((s) => ({ data: s.base64, mimeType: s.mimeType }))
|
const images = screenshotCollector.map((s) => ({ data: s.base64, mimeType: s.mimeType }))
|
||||||
|
|
||||||
return { text: finalText, images, isError: false }
|
return { text: finalText, images, isError: false }
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const errorStack = error.stack || error.message
|
const errorStack = error.stack || error.message
|
||||||
const isTimeoutError = error instanceof CodeExecutionTimeoutError || error.name === 'TimeoutError'
|
const isTimeoutError = error instanceof CodeExecutionTimeoutError || error.name === 'TimeoutError'
|
||||||
|
|
||||||
this.logger.error('Error in execute:', errorStack)
|
this.logger.error('Error in execute:', errorStack)
|
||||||
|
|
||||||
const logsText = formatConsoleLogs(consoleLogs, 'Console output (before error)')
|
const logsText = formatConsoleLogs(consoleLogs, 'Console output (before error)')
|
||||||
const resetHint = isTimeoutError ? '' :
|
const resetHint = isTimeoutError ? '' :
|
||||||
'\n\n[HINT: If this is an internal Playwright error, page/browser closed, or connection issue, call reset to reconnect.]'
|
'\n\n[HINT: If this is an internal Playwright error, page/browser closed, or connection issue, call reset to reconnect.]'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: `${logsText}\nError executing code: ${error.message}\n${errorStack}${resetHint}`,
|
text: `${logsText}\nError executing code: ${error.message}\n${errorStack}${resetHint}`,
|
||||||
images: [],
|
images: [],
|
||||||
@@ -671,7 +687,7 @@ export class PlaywrightExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get info about current connection state */
|
/** Get info about current connection state */
|
||||||
getStatus(): { connected: boolean; pageUrl: string | null; pagesCount: number } {
|
getStatus(): { connected: boolean; pageUrl: string | null; pagesCount: number } {
|
||||||
return {
|
return {
|
||||||
@@ -680,7 +696,7 @@ export class PlaywrightExecutor {
|
|||||||
pagesCount: this.context?.pages().length || 0,
|
pagesCount: this.context?.pages().length || 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get keys of user-defined state */
|
/** Get keys of user-defined state */
|
||||||
getStateKeys(): string[] {
|
getStateKeys(): string[] {
|
||||||
return Object.keys(this.userState)
|
return Object.keys(this.userState)
|
||||||
@@ -694,12 +710,12 @@ export class ExecutorManager {
|
|||||||
private executors = new Map<string, PlaywrightExecutor>()
|
private executors = new Map<string, PlaywrightExecutor>()
|
||||||
private cdpConfig: CdpConfig
|
private cdpConfig: CdpConfig
|
||||||
private logger: ExecutorLogger
|
private logger: ExecutorLogger
|
||||||
|
|
||||||
constructor(options: { cdpConfig: CdpConfig; logger?: ExecutorLogger }) {
|
constructor(options: { cdpConfig: CdpConfig; logger?: ExecutorLogger }) {
|
||||||
this.cdpConfig = options.cdpConfig
|
this.cdpConfig = options.cdpConfig
|
||||||
this.logger = options.logger || { log: console.log, error: console.error }
|
this.logger = options.logger || { log: console.log, error: console.error }
|
||||||
}
|
}
|
||||||
|
|
||||||
getExecutor(sessionId: string, cwd?: string): PlaywrightExecutor {
|
getExecutor(sessionId: string, cwd?: string): PlaywrightExecutor {
|
||||||
let executor = this.executors.get(sessionId)
|
let executor = this.executors.get(sessionId)
|
||||||
if (!executor) {
|
if (!executor) {
|
||||||
@@ -712,11 +728,11 @@ export class ExecutorManager {
|
|||||||
}
|
}
|
||||||
return executor
|
return executor
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteExecutor(sessionId: string): boolean {
|
deleteExecutor(sessionId: string): boolean {
|
||||||
return this.executors.delete(sessionId)
|
return this.executors.delete(sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
listSessions(): Array<{ id: string; stateKeys: string[] }> {
|
listSessions(): Array<{ id: string; stateKeys: string[] }> {
|
||||||
return [...this.executors.entries()].map(([id, executor]) => {
|
return [...this.executors.entries()].map(([id, executor]) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user