UNPKG

donobu

Version:

Create browser automations with an LLM agent and replay them as Playwright scripts.

227 lines 9.98 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TestFileUpdater = void 0; const fs = __importStar(require("fs")); const typescript_1 = __importDefault(require("typescript")); const Logger_1 = require("../../../utils/Logger"); /** * A class that handles the updating of test files based on test failures. */ class TestFileUpdater { /** * Updates a specific test case in a test file with new test code. * * @param testFilePath The path to the test file to update * @param testName The name of the test case to update * @param newTestCode The new test code to replace the old test with * @param options Options for updating the file * @returns True if the update was successful, false otherwise */ static async updateTestCase(testFilePath, testName, newTestCode) { try { // Verify the test file exists if (!fs.existsSync(testFilePath)) { Logger_1.appLogger.error(`Test file does not exist: ${testFilePath}`); return false; } // Read the original test file const originalTestCode = await fs.promises.readFile(testFilePath, 'utf8'); // Parse the original and new test code const sourceFile = typescript_1.default.createSourceFile(testFilePath, originalTestCode, typescript_1.default.ScriptTarget.Latest, true); const newTestSourceFile = typescript_1.default.createSourceFile('new-test.ts', newTestCode, typescript_1.default.ScriptTarget.Latest, true); // Extract the new test case const extractedNewTest = this.extractTestCase(newTestSourceFile, newTestCode, testName); if (!extractedNewTest) { Logger_1.appLogger.warn(`Could not extract the new test case "${testName}" from the provided code`); await fs.promises.writeFile(testFilePath, newTestCode, 'utf8'); Logger_1.appLogger.info(`Replaced entire test file with new code`); return true; } // Find and replace the old test case const updatedCode = this.findAndReplaceTestCase(sourceFile, originalTestCode, testName, extractedNewTest); if (updatedCode === originalTestCode) { Logger_1.appLogger.warn(`Could not find the test case "${testName}" in the original file`); await fs.promises.writeFile(testFilePath, newTestCode, 'utf8'); Logger_1.appLogger.info(`Replaced entire test file with new code`); return true; } // Write the updated code back to the test file await fs.promises.writeFile(testFilePath, updatedCode, 'utf8'); Logger_1.appLogger.info(`Successfully updated test case "${testName}" in ${testFilePath}`); return true; } catch (error) { Logger_1.appLogger.error(`Error updating test file ${testFilePath}:`, error); return false; } } /** * Extracts a specific test case from source code. * * @param sourceFile The TypeScript source file * @param sourceCode The original source code string * @param testName The name of the test to extract * @returns The extracted test code or null if not found */ static extractTestCase(sourceFile, sourceCode, testName) { let extractedTest = null; typescript_1.default.forEachChild(sourceFile, (node) => { if (extractedTest) { return; } // Already found if (this.isTestNode(node)) { const title = this.getTestTitle(node); if (title && this.testTitleMatches(title, testName)) { extractedTest = sourceCode.substring(node.pos, node.end); } } }); return extractedTest; } /** * Finds a specific test case in the original code and replaces it with new code. * * @param sourceFile The TypeScript source file * @param sourceCode The original source code string * @param testName The name of the test to replace * @param newTestCode The new test code to insert * @returns The updated source code */ static findAndReplaceTestCase(sourceFile, sourceCode, testName, newTestCode) { let updatedCode = sourceCode; typescript_1.default.forEachChild(sourceFile, (node) => { if (updatedCode !== sourceCode) { return; } // Already replaced if (this.isTestNode(node)) { const title = this.getTestTitle(node); if (title && this.testTitleMatches(title, testName)) { // Replace the test updatedCode = sourceCode.substring(0, node.pos) + newTestCode + sourceCode.substring(node.end); } } }); return updatedCode; } /** * Checks if a node is a test definition (test or it function call). */ static isTestNode(node) { return (typescript_1.default.isExpressionStatement(node) && typescript_1.default.isCallExpression(node.expression) && typescript_1.default.isIdentifier(node.expression.expression) && (node.expression.expression.text === 'test' || node.expression.expression.text === 'it')); } /** * Gets the title string from a test node. */ static getTestTitle(node) { if (!this.isTestNode(node)) { return null; } const callExpr = node .expression; const args = callExpr.arguments; if (args.length >= 1 && typescript_1.default.isStringLiteral(args[0])) { return args[0].text; } return null; } /** * Checks if a test title matches the target test name. */ static testTitleMatches(title, testName) { return (title === testName || testName.includes(title) || title.includes(testName)); } /** * Updates a test file based on a Playwright JSON reporter output. * * @param jsonReportPath Path to the Playwright JSON report * @param testFilePath Path to the test file to update (if not specified, will be extracted from the report) * @param newTestCode The new test code * @returns True if the update was successful, false otherwise */ static async updateFromPlaywrightReport(jsonReportPath, newTestCode, testFilePath) { try { // Read and parse the Playwright report const reportContent = await fs.promises.readFile(jsonReportPath, 'utf8'); const testResults = JSON.parse(reportContent); // Extract failed test information const failedTests = []; for (const suite of testResults.suites || []) { const file = suite.file; for (const spec of suite.specs || []) { if (!spec.ok) { for (const test of spec.tests || []) { if (!test.ok) { failedTests.push({ file, title: `${spec.title} ${test.title}`.trim(), }); } } } } } if (failedTests.length === 0) { Logger_1.appLogger.warn('No failed tests found in the report'); return false; } // If test file path is not specified, use the first failed test's file const targetTestFilePath = testFilePath || failedTests[0].file; const targetTestName = failedTests[0].title; if (!targetTestFilePath) { Logger_1.appLogger.error('Could not determine test file path from report'); return false; } // Update the test file return await this.updateTestCase(targetTestFilePath, targetTestName, newTestCode); } catch (error) { Logger_1.appLogger.error(`Error updating from Playwright report:`, error); return false; } } } exports.TestFileUpdater = TestFileUpdater; //# sourceMappingURL=TestFileUpdater.js.map