diff --git a/src/main/resources/report/report.css b/src/main/resources/report/report.css index a4d17f2..a53fccb 100644 --- a/src/main/resources/report/report.css +++ b/src/main/resources/report/report.css @@ -424,3 +424,225 @@ .main-content { padding: 14px 8px; } .search-wrap input { width: 140px; } } + +/* ── Print stylesheet ── */ +@media print { + @page { + size: auto; + margin: 12mm; + } + + *, *::before, *::after { + box-shadow: none !important; + text-shadow: none !important; + } + + :root, [data-theme="dark"] { + --bg: #ffffff !important; + --surface: #ffffff !important; + --surface2: #fafafa !important; + --text: #212121 !important; + --text-secondary: #616161 !important; + --text-primary: #212121 !important; + --muted: #757575 !important; + --border: #e0e0e0 !important; + --primary: #303f9f !important; + --primary-light: #f0f2f5 !important; + --hover: #f0f2f5 !important; + --good-bg: #e8f5e9 !important; + --warn-bg: #fff3e0 !important; + --bad-bg: #ffebee !important; + --code-bg: #f5f5f5 !important; + --code-text: #212121 !important; + } + + html, body { + width: 100% !important; + margin: 0 !important; + padding: 0 !important; + background: #fff !important; + color: #212121 !important; + font-size: 10pt; + line-height: 1.4; + transition: none !important; + -webkit-print-color-adjust: exact !important; + print-color-adjust: exact !important; + } + + /* Hide unnecessary interface elements */ + .app-bar, + .sidebar, + .filter-bar, + .filter-btn, + .search-wrap, + .dark-toggle, + .copy-failure-btn, + .tag-clear-btn, + .active-tag-pill, + .failures-shortcut, + .lightbox, + .footer { + display: none !important; + } + + .layout { + display: block !important; + width: 100% !important; + min-height: 0 !important; + margin: 0 !important; + padding: 0 !important; + } + + .main-content { + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + margin: 0 !important; + padding: 0 !important; + overflow: visible !important; + } + + /* Print all relevant report sections, not just the currently active tab */ + .tab-pane { + display: block !important; + visibility: visible !important; + opacity: 1 !important; + page-break-inside: auto; + break-inside: auto; + } + + #tab-tests, + #tab-failures { + break-before: page; + page-break-before: always; + margin-top: 24px; + } + + .tab-heading { + color: #212121 !important; + font-size: 16pt; + font-weight: 700; + margin-bottom: 12px; + page-break-after: avoid; + break-after: avoid; + } + + .card { + background: #ffffff !important; + color: #212121 !important; + box-shadow: none !important; + border: 1px solid #dcdcdc !important; + break-inside: avoid; + page-break-inside: avoid; + margin-bottom: 16px; + } + + .card-header, + .card-body, + .card-body-centered, + .detail-panel, + .ai-analysis-body { + background: #ffffff !important; + color: #212121 !important; + } + + .card-header { + border-bottom: 1px solid #e0e0e0 !important; + page-break-after: avoid; + break-after: avoid; + } + + .table-card, + .table-wrap, + .gantt-wrap { + overflow: visible !important; + } + + table { + width: 100% !important; + border-collapse: collapse !important; + page-break-inside: auto; + break-inside: auto; + } + + thead { + display: table-header-group !important; + } + + tr { + break-inside: avoid; + page-break-inside: avoid; + } + + th, td { + color: #212121 !important; + border-color: #e0e0e0 !important; + overflow-wrap: anywhere; + word-break: break-word; + } + + .group-header { + background: #f0f2f5 !important; + break-after: avoid; + page-break-after: avoid; + } + + .group-header td { + color: #212121 !important; + border-bottom: 2px solid #303f9f !important; + } + + .chart-card, + .chart-card canvas, + .gantt-row, + .failure-group, + .step-item, + .detail-row { + break-inside: avoid; + page-break-inside: avoid; + } + + /* Expand stack traces completely without scrolling or clipping */ + .stack-trace { + max-height: none !important; + overflow: visible !important; + white-space: pre-wrap !important; + overflow-wrap: anywhere !important; + word-break: break-all !important; + background: #f5f5f5 !important; + color: #212121 !important; + border: 1px solid #ddd !important; + } + + .error-msg, + .failure-group-msg { + color: #b71c1c !important; + } + + .stat-value, + .stat-label, + .meta-label, + .meta-value, + .run-summary, + .failure-why, + .failure-group-tests, + .group-count { + color: #212121 !important; + } + + .card:hover { + transform: none !important; + } + + a { + color: #212121 !important; + text-decoration: none !important; + } + + img, .screenshot-thumb { + max-width: 100% !important; + height: auto !important; + page-break-inside: avoid; + break-inside: avoid; + } +} diff --git a/src/main/resources/report/report.js b/src/main/resources/report/report.js index f6bcd85..0c8b568 100644 --- a/src/main/resources/report/report.js +++ b/src/main/resources/report/report.js @@ -275,3 +275,180 @@ function copyFailure(btn, event) { } catch (e) {} })(); + +// ── Print handling ── +let printState = null; + +// Prepare the complete report for printing. +window.addEventListener('beforeprint', function () { + // 1. Save interactive state before modifying anything + printState = { + // Tab state + activeTabPaneId: (function() { + const activePane = document.querySelector('.tab-pane.active'); + return activePane ? activePane.id : 'tab-dashboard'; + })(), + activeNavItemIndex: (function() { + const items = Array.from(document.querySelectorAll('.nav-item')); + return items.findIndex(item => item.classList.contains('active')); + })(), + // Filter and search state + activeFilter: activeFilter, + activeTagFilter: activeTagFilter, + searchValue: (function() { + const searchInput = document.getElementById('searchInput'); + return searchInput ? searchInput.value : ''; + })(), + activeFilterBtnIndex: (function() { + const btns = Array.from(document.querySelectorAll('.filter-btn')); + return btns.findIndex(b => b.classList.contains('active')); + })(), + activeTagPillDisplay: (function() { + const pill = document.getElementById('activeTagPill'); + return pill ? pill.style.display : 'none'; + })(), + // Group row and icon states + groupMembers: Array.from(document.querySelectorAll('.group-member')).map(row => ({ + el: row, + display: row.style.display + })), + groupHeaders: Array.from(document.querySelectorAll('.group-header')).map(row => ({ + el: row, + display: row.style.display + })), + groupIcons: Array.from(document.querySelectorAll('.group-icon')).map(icon => ({ + el: icon, + isClosed: icon.classList.contains('closed') + })), + // Detail row and icon states + detailRows: Array.from(document.querySelectorAll('.detail-row')).map(row => ({ + el: row, + display: row.style.display + })), + expandIcons: Array.from(document.querySelectorAll('.expand-icon')).map(icon => ({ + el: icon, + isOpen: icon.classList.contains('open') + })) + }; + + // 2. Expand all test groups so collapsed groups do not exclude results + document.querySelectorAll('.group-member:not(.detail-row)').forEach(function (row) { + row.style.display = ''; + }); + + document.querySelectorAll('.group-header').forEach(function (row) { + row.style.display = ''; + }); + + document.querySelectorAll('.group-icon').forEach(function (icon) { + icon.classList.remove('closed'); + }); + + // 3. Expand failed-test detail rows before printing so error messages and stack traces are included + document.querySelectorAll('#testTable tbody tr.group-member[data-status="FAILED"], #failureTable tbody tr.group-member').forEach(function (failedRow) { + const next = failedRow.nextElementSibling; + if (next && next.classList.contains('detail-row')) { + next.style.display = ''; + } + const icon = failedRow.querySelector('.expand-icon'); + if (icon) { + icon.classList.add('open'); + } + }); + + // 4. Ensure filters/search do not exclude tests from the printed report + document.querySelectorAll('.filter-btn').forEach(function (button) { + button.classList.remove('active'); + }); + + const allFilter = document.querySelector('.filter-btn[onclick*="ALL"]'); + if (allFilter) { + allFilter.classList.add('active'); + } + + const searchInput = document.getElementById('searchInput'); + if (searchInput) { + searchInput.value = ''; + } + + const activeTagPill = document.getElementById('activeTagPill'); + if (activeTagPill) { + activeTagPill.style.display = 'none'; + } +}); + +// Restore original interactive state after printing. +window.addEventListener('afterprint', function () { + if (!printState) return; + + // 1. Restore tabs + document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active')); + document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); + + const originalTabPane = document.getElementById(printState.activeTabPaneId); + if (originalTabPane) { + originalTabPane.classList.add('active'); + } + + const navItems = document.querySelectorAll('.nav-item'); + if (printState.activeNavItemIndex >= 0 && navItems[printState.activeNavItemIndex]) { + navItems[printState.activeNavItemIndex].classList.add('active'); + } + + // 2. Restore filters and search query + activeFilter = printState.activeFilter; + activeTagFilter = printState.activeTagFilter; + + document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); + const filterBtns = document.querySelectorAll('.filter-btn'); + if (printState.activeFilterBtnIndex >= 0 && filterBtns[printState.activeFilterBtnIndex]) { + filterBtns[printState.activeFilterBtnIndex].classList.add('active'); + } + + const searchInput = document.getElementById('searchInput'); + if (searchInput) { + searchInput.value = printState.searchValue; + } + + const activeTagPill = document.getElementById('activeTagPill'); + if (activeTagPill) { + activeTagPill.style.display = printState.activeTagPillDisplay; + } + + // 3. Restore group rows and icons + printState.groupMembers.forEach(item => { + item.el.style.display = item.display; + }); + + printState.groupHeaders.forEach(item => { + item.el.style.display = item.display; + }); + + printState.groupIcons.forEach(item => { + if (item.isClosed) { + item.el.classList.add('closed'); + } else { + item.el.classList.remove('closed'); + } + }); + + // 4. Restore detail rows and icons + printState.detailRows.forEach(item => { + item.el.style.display = item.display; + }); + + printState.expandIcons.forEach(item => { + if (item.isOpen) { + item.el.classList.add('open'); + } else { + item.el.classList.remove('open'); + } + }); + + // 5. Re-apply search/filters if there were any active + if (printState.activeFilter !== 'ALL' || printState.activeTagFilter || printState.searchValue) { + applySearch(); + } + + printState = null; +}); \ No newline at end of file