donobu
Version:
Create browser automations with an LLM agent and replay them as Playwright scripts.
227 lines • 9.98 kB
JavaScript
;
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