@playwright-mocks/reporters
Version:
Single Playwright reporter for mock interceptor with configurable logging and HTML report generation utility
252 lines âĸ 11.2 kB
JavaScript
export class TestReportGenerator {
// Generates the clickable row for the test in the main table
generateTestTableRow(test, testIndex) {
return `
<tr class="test-row" onclick="window.location.href='test-${testIndex}.html'">
<td>${this.escapeHtml(test.title)}</td>
<td class="${test.status}"><span class="status-icon">${this.statusIcon(test.status)}</span>${test.status}</td>
<td>${test.duration}ms</td>
<td>${this.escapeHtml(test.file)}</td>
</tr>
`;
}
// Generates the full HTML for a single test page
generateTestPage(test, testIndex, cssPath) {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test: ${this.escapeHtml(test.title)}</title>
<link rel="stylesheet" href="${cssPath}">
<script>
function toggleBlock(id) {
var el = document.getElementById(id);
if (el) el.classList.toggle('open');
}
window.onload = function() {
var err = document.getElementById('block-error');
if (err) err.classList.add('open');
}
</script>
</head>
<body>
<div class="container">
<div class="header">
<h1>Test Details</h1>
<div class="test-header-details">
<span class="status-icon icon-${test.status}">${this.statusIcon(test.status)}</span>
<span class="${test.status}" style="font-size:1.1rem; font-weight:600;">${test.status}</span>
<span style="margin-left:24px; color:#b0b0b0;">${this.escapeHtml(test.title)}</span>
</div>
<div class="test-header-meta">
<span class="meta-item"><span class="block-icon">â°</span> ${test.duration}ms</span>
<span class="meta-item"><span class="block-icon">đ</span> ${this.escapeHtml(test.file)}</span>
</div>
</div>
<div class="content">
${this.generateTestBlocks(test)}
<div style="margin-top:32px;"><a href="index.html">â Back to Report</a></div>
</div>
</div>
</body>
</html>`;
}
// Generates all blocks for the test page
generateTestBlocks(test) {
return [
this.errorBlock(test),
this.stepsBlock(test),
this.networkRequestsBlock(test),
this.attachmentsBlock(test),
this.mocksBlock(test),
this.metaBlock(test)
].filter(Boolean).join('\n');
}
// Generates test blocks for modal view (simplified version)
generateTestBlocksForModal(test) {
return [
this.errorBlock(test),
this.attachmentsBlock(test),
this.mocksBlock(test),
this.stepsBlock(test),
this.networkRequestsBlock(test),
this.metaBlock(test)
].filter(Boolean).join('\n');
}
// Block generator
block(title, content, id, icon, open) {
return `<div class="test-block${open ? ' open' : ''}" id="block-${id}">
<div class="test-block-title" onclick="toggleBlock('block-${id}')">
<span class="block-icon">${this.blockIcon(icon)}</span>${title}
<span class="block-toggle">âļ</span>
</div>
<div class="test-block-content">${content}</div>
</div>`;
}
// Status block
statusBlock(test) {
return `<span class="status-icon icon-${test.status}">${this.statusIcon(test.status)}</span> <span class="${test.status}">${test.status}</span>`;
}
// Attachments block
attachmentsBlock(test) {
if (!test.attachments || test.attachments.length === 0)
return '';
return this.block('Attachments', `
<ul class="attachment-list">
${test.attachments.map(att => `<li class="attachment-item"><span class='icon-attachment'>đ</span> ${this.escapeHtml(att.name || 'Attachment')} (${this.escapeHtml(att.contentType || '')})</li>`).join('')}
</ul>
`, 'attachments', 'attachment', false);
}
// Error block (open by default if exists)
errorBlock(test) {
// If error info is available in attachments, show it
const errorAttachment = test.attachments?.find(att => att.name?.toLowerCase().includes('error'));
if (test.status === 'failed' || test.status === 'timedOut' || errorAttachment) {
const errorMsg = errorAttachment ? this.escapeHtml(errorAttachment.body?.toString() || '') : 'Test failed or timed out.';
return this.block('Error', `<div class="error-block"><span class='icon-error'>â</span> ${errorMsg}</div>`, 'error', 'error', true);
}
return '';
}
// Mocks block (if any attachment is a mock)
mocksBlock(test) {
const mocks = (test.attachments || []).filter(att => att.name?.toLowerCase().includes('mock'));
if (mocks.length === 0)
return '';
return this.block('Mocks', `
<ul class="attachment-list">
${mocks.map(att => `<li class="attachment-item"><span class='icon-mock'>đ§Š</span> ${this.escapeHtml(att.name || 'Mock')} (${this.escapeHtml(att.contentType || '')})</li>`).join('')}
</ul>
`, 'mocks', 'mock', false);
}
// Metadata block
metaBlock(test) {
return this.block('Metadata', `
<table class="meta-table">
<tr><th>Test ID</th><td>${this.escapeHtml(test.id)}</td></tr>
<tr><th>File</th><td>${this.escapeHtml(test.file)}</td></tr>
<tr><th>Status</th><td>${this.escapeHtml(test.status)}</td></tr>
<tr><th>Duration</th><td>${test.duration}ms</td></tr>
</table>
`, 'meta', 'meta', false);
}
// Steps block (if steps attachment exists)
stepsBlock(test) {
const stepsAttachment = (test.attachments || []).find(att => att.name && att.name.toLowerCase().includes('steps') && att.contentType === 'application/json');
if (!stepsAttachment)
return '';
let stepsData;
try {
stepsData = typeof stepsAttachment.body === 'string' ? JSON.parse(stepsAttachment.body) : stepsAttachment.body;
}
catch {
return this.block('Steps', '<div class="error-block">Failed to parse steps data</div>', 'steps', 'info-circle', false);
}
return this.block('Steps', this.renderStepsTree(stepsData), 'steps', 'info-circle', false);
}
// Recursive rendering of steps tree
renderStepsTree(steps) {
if (!steps)
return '';
if (Array.isArray(steps)) {
return `<ul class="steps-list">${steps.map(s => this.renderStepItem(s)).join('')}</ul>`;
}
return this.renderStepItem(steps);
}
renderStepItem(step) {
const hasChildren = Array.isArray(step.steps) && step.steps.length > 0;
const statusClass = step.status ? `step-status-${step.status}` : '';
const errorBlock = step.error ? `<div class="step-error">â ${this.escapeHtml(step.error.message || step.error)}</div>` : '';
const duration = step.duration != null ? `<span class="step-duration">â° ${step.duration}ms</span>` : '';
return `<li class="step-item ${statusClass}">
<div class="step-header">
<span class="step-title">${this.escapeHtml(step.title || step.name || 'Step')}</span>
<span class="step-status">${this.statusIcon(step.status)}</span>
${duration}
</div>
${errorBlock}
${hasChildren ? `<ul class="steps-list">${step.steps.map((s) => this.renderStepItem(s)).join('')}</ul>` : ''}
</li>`;
}
// Network Requests block (if network attachments exist)
networkRequestsBlock(test) {
const networkAttachments = (test.attachments || []).filter(att => att.name && (att.name.toLowerCase().includes('request') ||
att.name.toLowerCase().includes('response') ||
att.name === 'Response Data') && att.contentType === 'application/json');
if (networkAttachments.length === 0)
return '';
const requests = this.parseNetworkRequests(networkAttachments);
if (requests.length === 0)
return '';
return this.block('Network Requests', this.renderNetworkRequests(requests), 'network', 'network', false);
}
// Parse network requests from attachments
parseNetworkRequests(attachments) {
const requests = [];
attachments.forEach(att => {
try {
const data = typeof att.body === 'string' ? JSON.parse(att.body) : att.body;
if (data.request || data.response) {
// Try to match the RequestResponseData structure
const req = data.request || data.response || data;
requests.push(req);
}
}
catch { }
});
return requests;
}
// Render network requests list
renderNetworkRequests(requests) {
return `<ul class="network-requests-list">
${requests.map(req => this.renderNetworkRequest(req)).join('')}
</ul>`;
}
renderNetworkRequest(req) {
const statusClass = req.status >= 400 ? 'network-error' : req.status >= 300 ? 'network-redirect' : 'network-success';
const statusText = req.status ? `${req.status}` : 'Pending';
const duration = req.duration ? `<span class="network-duration">â° ${req.duration}ms</span>` : '';
const errorBlock = req.error ? `<div class="network-error-details">â ${this.escapeHtml(typeof req.error === 'string' ? req.error : req.error.message)}</div>` : '';
return `<li class="network-request-item ${statusClass}">
<div class="network-request-header">
<span class="network-method">${this.escapeHtml(req.method || 'GET')}</span>
<span class="network-url">${this.escapeHtml(req.url || 'Unknown URL')}</span>
<span class="network-status">${statusText}</span>
${duration}
</div>
${errorBlock}
</li>`;
}
escapeHtml(text) {
if (!text)
return '';
return String(text)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
statusIcon(status) {
switch (status) {
case 'passed': return 'âī¸';
case 'failed': return 'â';
case 'skipped': return 'âī¸';
case 'timedOut': return 'â°';
default: return 'âšī¸';
}
}
blockIcon(icon) {
switch (icon) {
case 'error': return 'â';
case 'attachment': return 'đ';
case 'mock': return 'đ§Š';
case 'meta': return 'âšī¸';
case 'clock': return 'â°';
case 'file': return 'đ';
case 'info-circle': return 'đĩ';
default: return 'đĻ';
}
}
}
//# sourceMappingURL=test-report.generator.js.map