UNPKG

@playwright-mocks/reporters

Version:

Single Playwright reporter for mock interceptor with configurable logging and HTML report generation utility

252 lines â€ĸ 11.2 kB
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, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&#39;'); } 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