@autifyhq/muon
Version:
Muon - AI-Powered Playwright Test Coding Agent with Advanced Test Fixing Capabilities
1,031 lines (1,030 loc) • 62.5 kB
JavaScript
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',