UNPKG

playwright-sniff

Version:

Monitoring library for Playwright that measures action times, catches showstoppers and generates reports

355 lines 14.5 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PlaywrightSniff = void 0; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const types_1 = require("./types"); const utils_1 = require("./utils"); const html_report_1 = require("./html-report"); /** * PlaywrightSniff - A monitoring tool for Playwright actions */ class PlaywrightSniff { /** * Creates a new PlaywrightSniff instance */ constructor(config) { this.failures = []; this.showStoppers = []; this.requestStartTimes = {}; this.requestDurations = []; this.detailedRequestDurations = []; this.timings = []; this.isMonitoring = false; this.reportData = []; this.page = config.page; this.options = Object.assign(Object.assign({}, PlaywrightSniff.DEFAULT_OPTIONS), config.options); this.logger = this.options.logger; // Create screenshot directory if needed if (this.options.captureScreenshots) { if (!fs_1.default.existsSync(this.options.screenshotDir)) { fs_1.default.mkdirSync(this.options.screenshotDir, { recursive: true }); } } } /** * Start monitoring page actions */ start() { return __awaiter(this, void 0, void 0, function* () { if (this.isMonitoring) { this.logger('Monitoring already started', types_1.LogLevel.WARN); return; } this.failures = []; this.showStoppers = []; this.requestStartTimes = {}; this.requestDurations = []; this.detailedRequestDurations = []; this.timings = []; this.isMonitoring = true; this.reportData = []; this.logger(`Started monitoring Playwright actions for ${this.testName}`, types_1.LogLevel.INFO); // Set up listeners yield this.setupSniffingListeners(); }); } /** * Stop monitoring page actions */ stop() { if (!this.isMonitoring) { this.logger('Monitoring not started', types_1.LogLevel.WARN); return; } this.saveReport(); this.generateHTMLReport(); this.isMonitoring = false; this.logger(`Stopped monitoring Playwright actions for ${this.testName}`, types_1.LogLevel.INFO); if (this.hasShowStoppers()) { throw new Error('Test failed due to showstoppers'); } } /** * Measure the execution time of an action * @param action Function to execute and measure * @param label Label to identify the action */ measureAction(action, label) { return __awaiter(this, void 0, void 0, function* () { if (!this.isMonitoring) { this.logger('Monitoring not started', types_1.LogLevel.WARN); return action(); } const start = Date.now(); try { yield action(); const duration = Date.now() - start; const isSlow = duration > this.options.slowThreshold; this.timings.push({ label, duration, slow: isSlow, failed: false }); if (isSlow) { this.logger(`Slow action detected: ${label} took ${duration}ms (threshold: ${this.options.slowThreshold}ms)`, types_1.LogLevel.WARN); } } catch (error) { const duration = Date.now() - start; const errorMessage = (0, utils_1.cleanErrorMessage)(error); this.timings.push({ label, duration: 0, slow: false, failed: true }); this.showStoppers.push({ label, criticalError: errorMessage }); this.logger(`Showstopper detected during "${label}": ${errorMessage}`, types_1.LogLevel.ERROR); if (this.options.captureScreenshots) { yield this.captureErrorScreenshot(label); } // Consider whether re-throwing is needed based on your use case // throw error; } }); } /** * Add a custom failure */ addFailure(error, type = 'custom', metadata = {}) { if (!this.isMonitoring) { this.logger('Monitoring not started', types_1.LogLevel.WARN); return; } this.failures.push(Object.assign({ error, type }, metadata)); this.logger(`Failure added: ${error}`, types_1.LogLevel.WARN); } /** * Add a custom showstopper */ addShowStopper(label, criticalError) { return __awaiter(this, void 0, void 0, function* () { if (!this.isMonitoring) { this.logger('Monitoring not started', types_1.LogLevel.WARN); return; } const showStopper = { label, criticalError }; // Capture screenshot if enabled if (this.options.captureScreenshots) { const screenshotPath = yield this.captureErrorScreenshot(label); if (screenshotPath) { showStopper.screenshot = screenshotPath; } } this.showStoppers.push(showStopper); this.logger(`Showstopper added: ${label} - ${criticalError}`, types_1.LogLevel.ERROR); }); } /** * Get the current sniffing results */ getResults() { const timingsPassed = this.timings.filter(t => !t.failed); const avgLoadTime = (0, utils_1.calculateAverage)(timingsPassed.map(t => t.duration)); const avgRequestTime = (0, utils_1.calculateAverage)(this.requestDurations); const slowRequests = this.detailedRequestDurations .filter(r => r.duration > this.options.slowThreshold) .sort((a, b) => b.duration - a.duration); return { reportData: [{ timestamp: new Date().toLocaleString(), passed: this.showStoppers.length === 0, showStoppers: this.showStoppers, slowThreshold: this.options.slowThreshold, pageLoadSteps: this.timings, avgLoadTime, avgRequestTime, slowRequests, failures: this.failures, testName: this.testName, }] }; } /** * Generate and save a report */ saveReport(outputFile) { var _a; const results = this.getResults(); const filePath = outputFile || this.options.outputFile; let existingData = { reportData: [] }; const currentPpid = (_a = process.ppid) === null || _a === void 0 ? void 0 : _a.toString(); let shouldClear = false; if (fs_1.default.existsSync(filePath)) { try { existingData = JSON.parse(fs_1.default.readFileSync(filePath, 'utf8')); if (!existingData.reportData || !Array.isArray(existingData.reportData)) { existingData = { reportData: [] }; } if (existingData.testRunnerPid && existingData.testRunnerPid !== currentPpid) { shouldClear = true; existingData = { reportData: [] }; } } catch (error) { this.logger(`Error reading existing report: ${error}`, types_1.LogLevel.ERROR); existingData = { reportData: [] }; } } existingData.testRunnerPid = currentPpid; existingData.reportData.push(results.reportData[0]); const action = fs_1.default.existsSync(filePath) && !shouldClear ? 'updated' : 'created'; fs_1.default.writeFileSync(filePath, JSON.stringify(existingData, null, 2)); this.logger(`Report ${action} at ${filePath}`, types_1.LogLevel.INFO); return filePath; } /** * Generate HTML Report */ generateHTMLReport(outputHTML) { let reportData; const filePath = outputHTML || this.options.outputHTML; const jsonFilePath = this.options.outputFile; try { const rawData = fs_1.default.readFileSync(jsonFilePath, 'utf-8'); reportData = JSON.parse(rawData); } catch (error) { this.logger(`Error reading existing json report: ${error}`, types_1.LogLevel.ERROR); process.exit(1); } const html = (0, html_report_1.generateReportHTML)(reportData); const action = fs_1.default.existsSync(filePath) ? 'updated' : 'created'; fs_1.default.writeFileSync(filePath, html); this.logger(`HTML Report ${action} at ${filePath}`, types_1.LogLevel.INFO); } /** * Check if there are any showstoppers */ hasShowStoppers() { return this.showStoppers.length > 0; } /** * Get list of showstoppers */ getShowStoppers() { return this.showStoppers; } setTestName(name) { this.testName = name; } /** * Setup all listeners for sniffing */ setupSniffingListeners() { return __awaiter(this, void 0, void 0, function* () { this.page.on('console', msg => { if (msg.type() === 'error') { this.failures.push({ error: msg.text(), type: 'console' }); } }); this.page.on('requestfailed', (request) => __awaiter(this, void 0, void 0, function* () { var _a; const response = yield request.response(); const status = response === null || response === void 0 ? void 0 : response.status(); const url = request.url(); const errorText = ((_a = request.failure()) === null || _a === void 0 ? void 0 : _a.errorText) || 'Unknown error'; this.failures.push({ error: `${errorText}`, requestUrl: url, requestStatus: status || null, requestMethod: request.method(), type: 'request', }); })); this.page.on('request', request => { this.requestStartTimes[request.url()] = Date.now(); }); this.page.on('requestfinished', response => { const url = response.url(); const method = response.method(); if (this.requestStartTimes[url]) { const duration = Date.now() - this.requestStartTimes[url]; this.requestDurations.push(duration); this.detailedRequestDurations.push({ url, duration, method }); } }); this.page.on('response', (response) => __awaiter(this, void 0, void 0, function* () { const status = response.status(); if (status >= 500 && status < 600) { const url = response.url(); const method = response.request().method(); let body = '[body unreadable]'; try { body = yield response.text(); } catch (e) { /* ignore */ } yield this.addShowStopper(`${method} - ${url}`, `Status: ${status} - Body: ${body.substring(0, 100)}...`); } })); }); } /** * Setup handler for alerts on the page */ setupAlertHandler(locator) { try { this.page.addLocatorHandler(locator, () => __awaiter(this, void 0, void 0, function* () { const alertMessage = yield locator.allTextContents(); yield this.addShowStopper('Unexpected alert', alertMessage ? alertMessage.join(', ') : ''); })); } catch (e) { this.logger('Could not set up alert handler', types_1.LogLevel.WARN); } } /** * Capture a screenshot for an error */ captureErrorScreenshot(label) { return __awaiter(this, void 0, void 0, function* () { try { if (!fs_1.default.existsSync(this.options.screenshotDir)) { fs_1.default.mkdirSync(this.options.screenshotDir, { recursive: true }); } const safeName = label.replace(/[^a-z0-9]/gi, '_').toLowerCase(); const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\./g, '-'); const fileName = `error_${safeName}_${timestamp}.png`; const filePath = path_1.default.join(this.options.screenshotDir, fileName); yield this.page.screenshot({ path: filePath }); return filePath; } catch (e) { this.logger(`Failed to capture error screenshot: ${e}`, types_1.LogLevel.ERROR); return undefined; } }); } } exports.PlaywrightSniff = PlaywrightSniff; /** * Default options for monitoring */ PlaywrightSniff.DEFAULT_OPTIONS = { slowThreshold: 2000, captureScreenshots: true, screenshotDir: './screenshots', outputFile: 'sniffing-results.json', outputHTML: 'sniffing-report.html', logger: utils_1.defaultLogger }; //# sourceMappingURL=monitor.js.map