playwright-sniff
Version:
Monitoring library for Playwright that measures action times, catches showstoppers and generates reports
355 lines • 14.5 kB
JavaScript
"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