UNPKG

@autifyhq/muon

Version:

Muon - AI-Powered Playwright Test Coding Agent with Advanced Test Fixing Capabilities

1,031 lines (1,030 loc) 62.5 kB
import { execSync } from 'node:child_process'; import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import chalk from 'chalk'; import { chromium } from 'playwright'; export class CodingTools { constructor(projectPath, logger) { this.browser = null; this.context = null; this.page = null; this.pages = []; this.activePage = null; this.isBrowserInitialized = false; this.serverUrl = null; this.currentTestSteps = []; this.projectPath = projectPath; this.logger = logger; } logTool(name, input, output) { if (this.logger) { this.logger.logToolCall(name, input, output); } } // Browser Management Methods async launchBrowser(headless = true) { if (this.browser) { await this.closeBrowser(); } this.browser = await chromium.launch({ headless, args: ['--disable-web-security', '--disable-features=VizDisplayCompositor'], }); this.context = await this.browser.newContext({ viewport: { width: 1280, height: 720 }, userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', }); this.page = await this.context.newPage(); this.activePage = this.page; this.pages = [this.page]; this.isBrowserInitialized = true; this.logTool('launchBrowser', { headless }, 'Browser launched successfully'); } async closeBrowser() { try { if (this.pages.length > 0) { for (const page of this.pages) { await page.close(); } } if (this.context) { await this.context.close(); this.context = null; } if (this.browser) { await this.browser.close(); this.browser = null; } this.page = null; this.activePage = null; this.pages = []; this.isBrowserInitialized = false; // NOTE: Keeping currentTestSteps intact - only clear via clearRecordedSteps() this.logTool('closeBrowser', {}, 'Browser closed successfully'); } catch (error) { console.error('Browser cleanup failed:', error); } } getBrowserPage() { return this.page; } isBrowserOpen() { return this.browser !== null && this.page !== null; } validateBrowserInitialized() { if (!this.isBrowserInitialized || !this.activePage) { throw new Error('Browser not initialized. Please launch browser first.'); } } async resolveFrame(iframeSelectors = []) { let frame = this.activePage.mainFrame(); for (const selector of iframeSelectors) { const frameElement = await frame.$(selector); if (!frameElement) { throw new Error(`Iframe not found: ${selector}`); } const contentFrame = await frameElement.contentFrame(); if (!contentFrame) { throw new Error(`Cannot access iframe content: ${selector}`); } frame = contentFrame; } return frame; } recordTestStep(stepName, action, selector, value) { const step = { id: Date.now(), timestamp: new Date().toISOString(), name: stepName, action: action, ...(selector && { selector }), ...(value !== undefined && { value }), ...(this.activePage && { url: this.activePage.url() }), }; this.currentTestSteps.push(step); // Debug log to track step recording console.log(`📝 Step recorded: ${stepName} (${action}) - Total steps: ${this.currentTestSteps.length}`); return step; } getTools() { return [ { name: 'readFile', description: 'Read the contents of a file with optional pagination', parameters: { type: 'object', properties: { filePath: { type: 'string', description: 'Path to the file to read', }, skip: { type: 'number', description: 'Number of characters to skip', }, limit: { type: 'number', description: 'Maximum number of characters to read', }, }, required: ['filePath'], }, func: async ({ filePath, skip = 0, limit = 4 * 1024, }) => { try { const absolutePath = resolve(this.projectPath, filePath); const content = await readFile(absolutePath, 'utf-8'); const totalSize = content.length; const startPos = Math.min(skip, totalSize); const endPos = Math.min(startPos + limit, totalSize); const paginatedContent = content.substring(startPos, endPos); const lines = content.split('\n'); const paginatedLines = paginatedContent.split('\n'); const result = { content: paginatedContent, totalSize, showing: paginatedContent.length, skipped: skip, totalLines: lines.length, showingLines: paginatedLines.length, filePath, hasMore: endPos < totalSize, }; this.logTool('readFile', { filePath, skip, limit }, `Read ${paginatedContent.length}/${totalSize} characters from ${filePath}`); if (result.hasMore) { return `File content of ${filePath} (${skip}-${endPos}/${totalSize} chars):\n\`\`\`\n${paginatedContent}\n\`\`\`\n\n**Note**: File has more content. Use skip=${endPos} to continue reading.`; } else { return `File content of ${filePath}:\n\`\`\`\n${paginatedContent}\n\`\`\``; } } catch (error) { const errorMsg = `Error reading file ${filePath}: ${error instanceof Error ? error.message : String(error)}`; this.logTool('readFile', { filePath }, errorMsg); return errorMsg; } }, }, { name: 'writeFile', description: 'Write content to a file', parameters: { type: 'object', properties: { filePath: { type: 'string', description: 'Path to the file to write', }, content: { type: 'string', description: 'Content to write to the file', }, }, required: ['filePath', 'content'], }, func: async ({ filePath, content }) => { try { const absolutePath = resolve(this.projectPath, filePath); await mkdir(dirname(absolutePath), { recursive: true }); await writeFile(absolutePath, content, 'utf-8'); const result = `Successfully wrote ${content.length} characters to ${filePath}`; this.logTool('writeFile', { filePath, content: `${content.substring(0, 100)}...` }, result); return result; } catch (error) { const errorMsg = `Error writing file ${filePath}: ${error instanceof Error ? error.message : String(error)}`; this.logTool('writeFile', { filePath, content: `${content.substring(0, 100)}...` }, errorMsg); return errorMsg; } }, }, { name: 'listDirectory', description: 'List the contents of a directory', parameters: { type: 'object', properties: { dirPath: { type: 'string', description: 'Path to the directory to list', }, }, required: ['dirPath'], }, func: async ({ dirPath }) => { try { const absolutePath = resolve(this.projectPath, dirPath); const items = await readdir(absolutePath, { withFileTypes: true }); const result = items .map((item) => `${item.isDirectory() ? '📁' : '📄'} ${item.name}`) .join('\n'); this.logTool('listDirectory', { dirPath }, `Listed ${items.length} items in ${dirPath}`); return `Contents of ${dirPath}:\n${result}`; } catch (error) { const errorMsg = `Error listing directory ${dirPath}: ${error instanceof Error ? error.message : String(error)}`; this.logTool('listDirectory', { dirPath }, errorMsg); return errorMsg; } }, }, { name: 'executeCommand', description: 'Execute a shell command in the project directory with timeout support', parameters: { type: 'object', properties: { command: { type: 'string', description: 'Command to execute' }, timeout: { type: 'number', description: 'Timeout in seconds (default: 60, use 0 for no timeout)', }, }, required: ['command'], }, func: async ({ command, timeout = 60 }) => { try { const timeoutMs = timeout > 0 ? timeout * 1000 : undefined; const output = execSync(command, { cwd: this.projectPath, encoding: 'utf-8', maxBuffer: 1024 * 1024, timeout: timeoutMs, }); const result = `Command executed successfully:\n\`\`\`\n${output}\n\`\`\``; this.logTool('executeCommand', { command, timeout }, `Executed: ${command} (${output.length} chars output)`); return result; } catch (error) { let errorMsg = JSON.stringify(error); if (error.code === 'ETIMEDOUT') { errorMsg = `Command timed out after ${timeout} seconds: ${command}\nTip: Use background=true for long-running commands or increase timeout.`; } else if (error.signal === 'SIGTERM') { errorMsg = `Command was terminated: ${command}`; } this.logTool('executeCommand', { command }, errorMsg); return errorMsg; } }, }, { name: 'searchFileContent', description: 'Search for text content within files using regex patterns', parameters: { type: 'object', properties: { text: { type: 'string', description: 'Text or regex pattern to search for', }, filePattern: { type: 'string', description: 'File pattern to limit search (e.g., "*.test.ts")', }, skip: { type: 'number', description: 'Number of results to skip', }, limit: { type: 'number', description: 'Maximum number of results to return', }, }, required: ['text'], }, func: async ({ text, filePattern, skip = 0, limit = 100, }) => { try { // Escape special regex characters in text for literal search const escapedText = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const searchRegex = new RegExp(escapedText, 'gi'); const filePatternRegex = filePattern ? new RegExp(filePattern .replace(/[.+?^${}()|[\]\\*]/g, '\\$&') // Escape regex chars including * .replace(/\\\*/g, '.*'), // Convert escaped * to .* 'i') : null; const allMatches = []; const searchInFile = async (filePath) => { try { const content = await readFile(filePath, 'utf-8'); const lines = content.split('\n'); lines.forEach((line, index) => { if (searchRegex.test(line)) { allMatches.push({ file: filePath.replace(`${this.projectPath}/`, ''), line: index + 1, content: line.trim(), match: line.match(searchRegex)?.[0] || text, }); } }); } catch (_error) { // Skip files that can't be read } }; const searchDirectory = async (dir) => { const items = await readdir(dir, { withFileTypes: true }); for (const item of items) { const itemPath = resolve(dir, item.name); if (item.isFile()) { if (filePatternRegex && !filePatternRegex.test(item.name)) { continue; } await searchInFile(itemPath); } else if (item.isDirectory() && !item.name.startsWith('.')) { await searchDirectory(itemPath); } } }; await searchDirectory(this.projectPath); const paginatedMatches = allMatches.slice(skip, skip + limit); const resultMessage = `Found ${allMatches.length} matches for "${text}"`; this.logTool('searchFileContent', { text, filePattern }, resultMessage); let output = `${resultMessage}\n\n`; if (paginatedMatches.length > 0) { output += paginatedMatches .map((match) => `${match.file}:${match.line}: ${match.content}`) .join('\n'); if (allMatches.length > skip + limit) { output += `\n\n**Note**: Showing ${skip + 1}-${skip + limit} of ${allMatches.length} results. Use skip=${skip + limit} to see more.`; } } else { output += 'No matches found.'; } return output; } catch (error) { const errorMsg = `Error searching file content: ${error instanceof Error ? error.message : String(error)}`; this.logTool('searchFileContent', { text }, errorMsg); return errorMsg; } }, }, // Browser Automation Tools { name: 'initializeBrowser', description: 'Initialize browser for test simulation and exploration', parameters: { type: 'object', properties: { serverUrl: { type: 'string', description: 'Base URL of the application to test', }, stepName: { type: 'string', description: 'Name for this test step', }, }, }, func: async ({ stepName }) => { try { if (this.isBrowserInitialized) { await this.closeBrowser(); } await this.launchBrowser(false); const result = `Browser initialized successfully`; this.logTool('initializeBrowser', { stepName }, result); return { message: result }; } catch (error) { const errorMsg = `Error initializing browser: ${error instanceof Error ? error.message : String(error)}`; this.logTool('initializeBrowser', { stepName }, errorMsg); return { error: errorMsg }; } }, }, { name: 'getPageDOM', description: 'Get complete page DOM including main frame and all iframes with shadow DOM support', parameters: { type: 'object', properties: { includeIframes: { type: 'boolean', description: 'Include iframe content (default: true)', }, }, }, func: async ({ includeIframes = true }) => { try { this.validateBrowserInitialized(); const result = await this.getCompletePageDOM(this.activePage, includeIframes); this.logTool('getPageDOM', { includeIframes }, `Retrieved page DOM with ${result.stats?.totalElements || 0} elements`); return result; } catch (error) { const errorMsg = `Error getting page DOM: ${error instanceof Error ? error.message : String(error)}`; this.logTool('getPageDOM', { includeIframes }, errorMsg); return { success: false, error: errorMsg }; } }, }, { name: 'getMainFrameDOM', description: 'Get DOM of main frame only (excludes iframes)', parameters: { type: 'object', properties: {}, }, func: async () => { try { this.validateBrowserInitialized(); const dom = await this.extractFrameDOM(this.activePage.mainFrame()); const result = { success: true, value: { url: this.activePage.url(), title: await this.activePage.title(), dom, }, }; this.logTool('getMainFrameDOM', {}, `Retrieved main frame DOM`); return result; } catch (error) { const errorMsg = `Error getting main frame DOM: ${error instanceof Error ? error.message : String(error)}`; this.logTool('getMainFrameDOM', {}, errorMsg); return { success: false, error: errorMsg }; } }, }, { name: 'getIframeDOM', description: 'Get DOM of a specific iframe by name or src', parameters: { type: 'object', properties: { identifier: { type: 'string', description: 'Iframe name, src URL, or index (0-based)', }, }, required: ['identifier'], }, func: async ({ identifier }) => { try { this.validateBrowserInitialized(); let frame = null; // Try by name frame = this.activePage.frame(identifier) || null; // Try by index if (!frame && !Number.isNaN(parseInt(identifier))) { const frames = this.activePage.frames(); const index = parseInt(identifier); if (index >= 0 && index < frames.length) { frame = frames[index] || null; } } if (!frame) { const errorMsg = `Iframe not found: ${identifier}`; this.logTool('getIframeDOM', { identifier }, errorMsg); return { success: false, error: errorMsg }; } const dom = await this.extractFrameDOM(frame); const result = { success: true, value: { identifier, url: frame.url(), title: await frame.title(), dom, }, }; this.logTool('getIframeDOM', { identifier }, `Retrieved iframe DOM for ${identifier}`); return result; } catch (error) { const errorMsg = `Error getting iframe DOM: ${error instanceof Error ? error.message : String(error)}`; this.logTool('getIframeDOM', { identifier }, errorMsg); return { success: false, error: errorMsg }; } }, }, { name: 'listIframes', description: 'List all iframes on the current page', parameters: { type: 'object', properties: {}, }, func: async () => { try { this.validateBrowserInitialized(); const iframes = await this.getIframeList(this.activePage); const frameDetails = await Promise.all(iframes.map(async (iframeInfo, index) => { try { let frame = null; if (iframeInfo.name) { frame = this.activePage.frame(iframeInfo.name); } if (!frame && iframeInfo.src) { frame = this.activePage.frame(iframeInfo.src); } if (frame) { return { index, name: iframeInfo.name, src: iframeInfo.src, id: iframeInfo.id, url: frame.url(), title: await frame.title(), accessible: true, }; } else { return { index, name: iframeInfo.name, src: iframeInfo.src, id: iframeInfo.id, url: iframeInfo.src || 'unknown', title: 'unknown', accessible: false, }; } } catch (error) { return { index, name: iframeInfo.name, src: iframeInfo.src, id: iframeInfo.id, url: 'error', title: 'error', accessible: false, error: error instanceof Error ? error.message : String(error), }; } })); const result = { success: true, value: frameDetails, }; this.logTool('listIframes', {}, `Found ${frameDetails.length} iframes`); return result; } catch (error) { const errorMsg = `Error listing iframes: ${error instanceof Error ? error.message : String(error)}`; this.logTool('listIframes', {}, errorMsg); return { success: false, error: errorMsg }; } }, }, { name: 'navigate', description: 'Navigate to a URL', parameters: { type: 'object', properties: { url: { type: 'string', description: 'URL to navigate to (can be relative or absolute)', }, stepName: { type: 'string', description: 'Name for this test step', }, }, required: ['url'], }, func: async ({ url, stepName }) => { try { this.validateBrowserInitialized(); let targetUrl = url; if (!url.startsWith('http')) { targetUrl = new URL(url, this.serverUrl || 'http://localhost:3000').href; } await this.activePage.goto(targetUrl, { waitUntil: 'domcontentloaded', }); this.recordTestStep(stepName || 'Navigate', 'NAVIGATE', undefined, targetUrl); const result = `Navigated to ${targetUrl}`; this.logTool('navigate', { url: targetUrl }, result); return { url: targetUrl, message: result }; } catch (error) { const errorMsg = `Error navigating to URL: ${error instanceof Error ? error.message : String(error)}`; this.logTool('navigate', { url }, errorMsg); return { error: errorMsg }; } }, }, { name: 'clickElement', description: 'Click on an element using CSS selector', parameters: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector for the element to click', }, stepName: { type: 'string', description: 'Name for this test step', }, iframeSelectors: { type: 'array', items: { type: 'string' }, description: 'Array of iframe selectors to navigate through', }, }, required: ['selector'], }, func: async ({ selector, stepName, iframeSelectors = [], }) => { try { this.validateBrowserInitialized(); const frame = await this.resolveFrame(iframeSelectors); await frame.click(selector); this.recordTestStep(stepName || 'Click Element', 'CLICK', selector); const result = `Clicked element: ${selector}`; this.logTool('clickElement', { selector }, result); return { selector, message: result }; } catch (error) { const errorMsg = `Error clicking element: ${error instanceof Error ? error.message : String(error)}`; this.logTool('clickElement', { selector }, errorMsg); return { error: errorMsg }; } }, }, { name: 'inputText', description: 'Input text into an element', parameters: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector for the input element', }, text: { type: 'string', description: 'Text to input into the element', }, stepName: { type: 'string', description: 'Name for this test step', }, iframeSelectors: { type: 'array', items: { type: 'string' }, description: 'Array of iframe selectors to navigate through', }, }, required: ['selector', 'text'], }, func: async ({ selector, text, stepName, iframeSelectors = [], }) => { try { this.validateBrowserInitialized(); const frame = await this.resolveFrame(iframeSelectors); await frame.fill(selector, text); this.recordTestStep(stepName || 'Input Text', 'INPUT_TEXT', selector, text); const result = `Entered text in element: ${selector}`; this.logTool('inputText', { selector, text }, result); return { selector, text, message: result }; } catch (error) { const errorMsg = `Error inputting text: ${error instanceof Error ? error.message : String(error)}`; this.logTool('inputText', { selector, text }, errorMsg); return { error: errorMsg }; } }, }, { name: 'selectOption', description: 'Select an option from a dropdown', parameters: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector for the select element', }, value: { type: 'string', description: 'Value or text of the option to select', }, stepName: { type: 'string', description: 'Name for this test step', }, iframeSelectors: { type: 'array', items: { type: 'string' }, description: 'Array of iframe selectors to navigate through', }, }, required: ['selector', 'value'], }, func: async ({ selector, value, stepName, iframeSelectors = [], }) => { try { this.validateBrowserInitialized(); const frame = await this.resolveFrame(iframeSelectors); await frame.selectOption(selector, value); this.recordTestStep(stepName || 'Select Option', 'SELECT_OPTION', selector, value); const result = `Selected option in element: ${selector}`; this.logTool('selectOption', { selector, value }, result); return { selector, value, message: result }; } catch (error) { const errorMsg = `Error selecting option: ${error instanceof Error ? error.message : String(error)}`; this.logTool('selectOption', { selector, value }, errorMsg); return { error: errorMsg }; } }, }, { name: 'checkCheckbox', description: 'Check or uncheck a checkbox', parameters: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector for the checkbox element', }, checked: { type: 'boolean', description: 'Whether to check (true) or uncheck (false) the checkbox', }, stepName: { type: 'string', description: 'Name for this test step', }, iframeSelectors: { type: 'array', items: { type: 'string' }, description: 'Array of iframe selectors to navigate through', }, }, required: ['selector', 'checked'], }, func: async ({ selector, checked, stepName, iframeSelectors = [], }) => { try { this.validateBrowserInitialized(); const frame = await this.resolveFrame(iframeSelectors); await frame.setChecked(selector, checked); this.recordTestStep(stepName || 'Toggle Checkbox', 'CHECK_CHECKBOX', selector, checked); const result = `${checked ? 'Checked' : 'Unchecked'} checkbox: ${selector}`; this.logTool('checkCheckbox', { selector, checked }, result); return { selector, checked, message: result }; } catch (error) { const errorMsg = `Error toggling checkbox: ${error instanceof Error ? error.message : String(error)}`; this.logTool('checkCheckbox', { selector, checked }, errorMsg); return { error: errorMsg }; } }, }, { name: 'assertText', description: 'Assert that an element contains specific text', parameters: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector for the element to check', }, text: { type: 'string', description: 'Expected text content' }, matchType: { type: 'string', enum: ['exact', 'contains', 'not_contains', 'regex', 'begins_with'], description: 'How to match the text', }, stepName: { type: 'string', description: 'Name for this test step', }, iframeSelectors: { type: 'array', items: { type: 'string' }, description: 'Array of iframe selectors to navigate through', }, useInnerText: { type: 'boolean', description: 'Whether to use innerText (true) or textContent (false)', }, }, required: ['selector', 'text'], }, func: async ({ selector, text, matchType = 'contains', stepName, iframeSelectors = [], useInnerText = true, }) => { try { this.validateBrowserInitialized(); const frame = await this.resolveFrame(iframeSelectors); const element = await frame.$(selector); if (!element) { throw new Error(`Element not found: ${selector}`); } const actualText = useInnerText ? await element.innerText() : await element.textContent(); let passed = false; switch (matchType) { case 'exact': passed = actualText === text; break; case 'contains': passed = actualText.includes(text); break; case 'not_contains': passed = !actualText.includes(text); break; case 'regex': passed = new RegExp(text).test(actualText); break; case 'begins_with': passed = actualText.startsWith(text); break; default: throw new Error(`Unknown match type: ${matchType}`); } this.recordTestStep(stepName || 'Verify Text', 'ASSERT_TEXT', selector, text); const result = passed ? `Text assertion passed: ${selector}` : `Text assertion failed: Expected "${text}" but got "${actualText}"`; this.logTool('assertText', { selector, text, matchType }, result); return { selector, expectedText: text, actualText, matchType, passed, message: result, }; } catch (error) { const errorMsg = `Error asserting text: ${error instanceof Error ? error.message : String(error)}`; this.logTool('assertText', { selector, text }, errorMsg); return { error: errorMsg }; } }, }, { name: 'assertUrl', description: 'Assert that the current URL matches expectations', parameters: { type: 'object', properties: { value: { type: 'string', description: 'Expected URL or URL pattern', }, matchType: { type: 'string', enum: ['exact', 'contains', 'not_contains', 'regex', 'begins_with'], description: 'How to match the URL', }, stepName: { type: 'string', description: 'Name for this test step', }, }, required: ['value'], }, func: async ({ value, matchType = 'contains', stepName, }) => { try { this.validateBrowserInitialized(); const currentUrl = this.activePage.url(); let passed = false; switch (matchType) { case 'exact': passed = currentUrl === value; break; case 'contains': passed = currentUrl.includes(value); break; case 'not_contains': passed = !currentUrl.includes(value); break; case 'regex': passed = new RegExp(value).test(currentUrl); break; case 'begins_with': passed = currentUrl.startsWith(value); break; default: throw new Error(`Unknown match type: ${matchType}`); } this.recordTestStep(stepName || 'Verify URL', 'ASSERT_URL', undefined, value); const result = passed ? `URL assertion passed` : `URL assertion failed: Expected "${value}" but got "${currentUrl}"`; this.logTool('assertUrl', { value, matchType }, result); return { expectedUrl: value, actualUrl: currentUrl, matchType, passed, message: result, }; } catch (error) { const errorMsg = `Error asserting URL: ${error instanceof Error ? error.message : String(error)}`; this.logTool('assertUrl', { value }, errorMsg); return { error: errorMsg }; } }, }, { name: 'sleep', description: 'Wait for a specified number of seconds', parameters: { type: 'object', properties: { seconds: { type: 'number', description: 'Number of seconds to wait', }, stepName: { type: 'string', description: 'Name for this test step', }, }, required: ['seconds'], }, func: async ({ seconds, stepName }) => { try { if (seconds <= 0) { throw new Error('Seconds must be a positive number'); } await new Promise((resolve) => setTimeout(resolve, seconds * 1000)); this.recordTestStep(stepName || 'Pause', 'SLEEP', undefined, seconds); const result = `Waited for ${seconds} seconds`; this.logTool('sleep', { seconds }, result); return { seconds, message: result }; } catch (error) { const errorMsg = `Error during sleep: ${error instanceof Error ? error.message : String(error)}`; this.logTool('sleep', { seconds }, errorMsg); return { error: errorMsg }; } }, }, { name: 'getBrowserStatus', description: 'Get current browser status and recorded test steps', parameters: { type: 'object', properties: {}, }, func: async () => { try { const status = { isInitialized: this.isBrowserInitialized, serverUrl: this.serverUrl, pagesCount: this.pages.length, currentUrl: this.activePage ? this.activePage.url() : null, stepsRecorded: this.currentTestSteps.length, steps: this.currentTestSteps, }; const result = `Browser status retrieved`; this.logTool('getBrowserStatus', {}, result); return { status, message: result }; } catch (error) { const errorMsg = `Error getting browser status: ${error instanceof Error ? error.message : String(error)}`; this.logTool('getBrowserStatus', {}, errorMsg); return { error: errorMsg }; } }, }, { name: 'clearRecordedSteps', description: 'Clear all recorded test steps', parameters: { type: 'object', properties: {}, }, func: async () => { try { const clearedCount = this.currentTestSteps.length; if (clearedCount > 0) { console.log(`🗑️ Clearing ${clearedCount} recorded steps:`); this.currentTestSteps.forEach((step, index) => { console.log(` ${index + 1}. ${step.name} (${step.action})`); }); } this.currentTestSteps = []; const result = `Cleared ${clearedCount} recorded test steps`; this.logTool('clearRecordedSteps', {}, result); return { clearedCount, message: result }; } catch (error) { const errorMsg = `Error clearing recorded steps: ${error instanceof Error ? error.message : String(error)}`; this.logTool('clearRecordedSteps', {}, errorMsg); return { error: errorMsg }; } }, }, { name: 'closeBrowser', description: 'Close the Playwright browser session', parameters: { type: 'object',