(* HTML Test Report Generator - Standalone HTML reports for test results *) type test_result = { test_num : int; description : string; input : string; expected : string; actual : string; success : bool; details : (string * string) list; (* Additional key-value pairs *) raw_test_data : string option; (* Original test file content for context *) } type file_result = { filename : string; test_type : string; passed_count : int; failed_count : int; tests : test_result list; } type match_quality_stats = { exact_matches : int; code_matches : int; message_matches : int; substring_matches : int; severity_mismatches : int; no_matches : int; not_applicable : int; } type test_type_stats = { isvalid_passed : int; isvalid_total : int; novalid_passed : int; novalid_total : int; haswarn_passed : int; haswarn_total : int; } type report = { title : string; test_type : string; description : string; files : file_result list; total_passed : int; total_failed : int; match_quality : match_quality_stats option; test_type_breakdown : test_type_stats option; strictness_mode : string option; run_timestamp : string option; } let html_escape s = let buf = Buffer.create (String.length s * 2) in String.iter (fun c -> match c with | '&' -> Buffer.add_string buf "&" | '<' -> Buffer.add_string buf "<" | '>' -> Buffer.add_string buf ">" | '"' -> Buffer.add_string buf """ | '\'' -> Buffer.add_string buf "'" | c -> Buffer.add_char buf c ) s; Buffer.contents buf let truncate_string ?(max_len=10000) s = if String.length s <= max_len then s else String.sub s 0 max_len ^ "\n... (truncated at " ^ string_of_int max_len ^ " chars)" let css = {| :root { --bg-primary: #1a1a2e; --bg-secondary: #16213e; --bg-tertiary: #0f3460; --text-primary: #eee; --text-secondary: #aaa; --text-muted: #666; --accent: #e94560; --accent-light: #ff6b8a; --success: #4ade80; --success-dim: rgba(74, 222, 128, 0.2); --failure: #f87171; --failure-dim: rgba(248, 113, 113, 0.2); --warning: #fbbf24; --info: #60a5fa; --border: #333; --code-bg: #0d1117; } * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg-primary); color: var(--text-primary); line-height: 1.6; } .container { max-width: 1600px; margin: 0 auto; padding: 20px; } /* Hero Header */ .hero { background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%); padding: 40px; border-radius: 12px; margin-bottom: 30px; border: 1px solid var(--border); } .hero h1 { font-size: 2rem; margin-bottom: 15px; color: var(--accent); display: flex; align-items: center; gap: 15px; } .hero h1::before { content: "๐งช"; font-size: 1.5rem; } .hero-description { color: var(--text-secondary); max-width: 900px; margin-bottom: 20px; font-size: 1.05rem; } .hero-meta { display: flex; gap: 20px; flex-wrap: wrap; font-size: 0.9rem; color: var(--text-muted); } .hero-meta span { display: flex; align-items: center; gap: 6px; } /* Summary Cards */ .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; } .summary-card { background: var(--bg-secondary); border-radius: 12px; padding: 24px; border: 1px solid var(--border); text-align: center; } .summary-card.large { grid-column: span 2; } .summary-card h3 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 1px; color: var(--text-secondary); margin-bottom: 10px; } .summary-card .value { font-size: 2.5rem; font-weight: 700; line-height: 1.2; } .summary-card .value.success { color: var(--success); } .summary-card .value.failure { color: var(--failure); } .summary-card .value.neutral { color: var(--text-primary); } .summary-card .subtext { font-size: 0.85rem; color: var(--text-muted); margin-top: 5px; } /* Progress Bar */ .progress-bar { height: 12px; background: var(--failure-dim); border-radius: 6px; overflow: hidden; margin-top: 15px; } .progress-fill { height: 100%; background: var(--success); border-radius: 6px; transition: width 0.5s ease; } /* Stats Breakdown */ .stats-section { background: var(--bg-secondary); border-radius: 12px; padding: 24px; margin-bottom: 30px; border: 1px solid var(--border); } .stats-section h2 { font-size: 1.2rem; margin-bottom: 20px; color: var(--accent-light); display: flex; align-items: center; gap: 10px; } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px; } .stat-item { background: var(--bg-primary); padding: 16px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; } .stat-item .label { font-size: 0.9rem; color: var(--text-secondary); } .stat-item .count { font-size: 1.4rem; font-weight: 600; } .stat-item.success .count { color: var(--success); } .stat-item.failure .count { color: var(--failure); } .stat-item.warning .count { color: var(--warning); } .stat-item.info .count { color: var(--info); } /* Controls */ .controls { display: flex; gap: 12px; margin-bottom: 25px; flex-wrap: wrap; align-items: center; } input[type="search"], select { padding: 10px 14px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-secondary); color: var(--text-primary); font-size: 14px; } input[type="search"] { width: 300px; } input[type="search"]:focus, select:focus { outline: none; border-color: var(--accent); } button { padding: 10px 18px; border: none; border-radius: 8px; background: var(--accent); color: white; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; } button:hover { background: var(--accent-light); } button.secondary { background: var(--bg-tertiary); border: 1px solid var(--border); } button.secondary:hover { background: var(--bg-secondary); } /* Sidebar */ .layout { display: grid; grid-template-columns: 280px 1fr; gap: 30px; } .sidebar { position: sticky; top: 20px; height: fit-content; max-height: calc(100vh - 40px); overflow-y: auto; background: var(--bg-secondary); border-radius: 12px; padding: 16px; border: 1px solid var(--border); } .sidebar h3 { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; color: var(--text-muted); margin-bottom: 12px; padding: 0 8px; } .sidebar-item { padding: 10px 12px; border-radius: 8px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; font-size: 14px; transition: all 0.2s; } .sidebar-item:hover { background: var(--bg-tertiary); } .sidebar-item.active { background: var(--accent); } .sidebar-item .name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 160px; } .sidebar-item .badge { font-size: 11px; padding: 3px 8px; border-radius: 12px; background: var(--bg-primary); font-weight: 600; } .sidebar-item .badge.all-passed { color: var(--success); } .sidebar-item .badge.has-failed { color: var(--failure); } /* File Sections */ .file-section { margin-bottom: 24px; background: var(--bg-secondary); border-radius: 12px; overflow: hidden; border: 1px solid var(--border); } .file-header { padding: 18px 24px; background: var(--bg-tertiary); cursor: pointer; display: flex; justify-content: space-between; align-items: center; transition: background 0.2s; } .file-header:hover { background: #1a4a7a; } .file-header h2 { font-size: 1.1rem; display: flex; align-items: center; gap: 12px; } .file-header .toggle { font-size: 1rem; transition: transform 0.3s; color: var(--text-secondary); } .file-header.collapsed .toggle { transform: rotate(-90deg); } .file-stats { display: flex; gap: 20px; font-size: 14px; } .file-stats .passed { color: var(--success); font-weight: 500; } .file-stats .failed { color: var(--failure); font-weight: 500; } .tests-container { padding: 12px; } .tests-container.hidden { display: none; } /* Test Items */ .test-item { margin: 8px 0; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; transition: border-color 0.2s; } .test-item:hover { border-color: var(--text-muted); } .test-header { padding: 12px 16px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; background: var(--bg-primary); transition: background 0.2s; } .test-header:hover { background: rgba(255,255,255,0.03); } .test-header .status { width: 10px; height: 10px; border-radius: 50%; margin-right: 12px; flex-shrink: 0; } .test-header .status.passed { background: var(--success); } .test-header .status.failed { background: var(--failure); } .test-header .test-info { flex: 1; display: flex; align-items: center; min-width: 0; } .test-header .test-num { font-weight: 600; margin-right: 12px; color: var(--text-muted); font-size: 0.9rem; } .test-header .test-desc { font-size: 14px; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .test-header .expand-icon { color: var(--text-muted); font-size: 0.8rem; } /* Test Details */ .test-details { padding: 20px; background: var(--code-bg); border-top: 1px solid var(--border); display: none; } .test-details.visible { display: block; } .detail-section { margin-bottom: 20px; } .detail-section:last-child { margin-bottom: 0; } .detail-section h4 { font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: var(--text-muted); margin-bottom: 10px; display: flex; align-items: center; gap: 8px; } .detail-section pre { background: var(--bg-secondary); padding: 16px; border-radius: 8px; overflow-x: auto; font-family: 'Monaco', 'Menlo', 'Consolas', monospace; font-size: 13px; white-space: pre-wrap; word-break: break-word; max-height: 400px; overflow-y: auto; line-height: 1.5; border: 1px solid var(--border); } .detail-row { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } .comparison-label { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 600; margin-left: 8px; } .comparison-label.match { background: var(--success-dim); color: var(--success); } .comparison-label.mismatch { background: var(--failure-dim); color: var(--failure); } /* Explanation Section */ .explanation { background: var(--bg-secondary); border-radius: 12px; padding: 24px; margin-bottom: 30px; border: 1px solid var(--border); } .explanation h2 { font-size: 1.2rem; margin-bottom: 16px; color: var(--accent-light); } .explanation p { color: var(--text-secondary); margin-bottom: 12px; } .explanation ul { list-style: none; padding-left: 0; } .explanation li { padding: 8px 0; padding-left: 24px; position: relative; color: var(--text-secondary); } .explanation li::before { content: "โ"; position: absolute; left: 0; color: var(--accent); } .explanation code { background: var(--bg-primary); padding: 2px 6px; border-radius: 4px; font-family: monospace; font-size: 0.9em; color: var(--accent-light); } @media (max-width: 1000px) { .layout { grid-template-columns: 1fr; } .sidebar { display: none; } .detail-row { grid-template-columns: 1fr; } .summary-card.large { grid-column: span 1; } } |} let js = {| document.addEventListener('DOMContentLoaded', function() { // File section toggling document.querySelectorAll('.file-header').forEach(header => { header.addEventListener('click', function() { this.classList.toggle('collapsed'); const container = this.nextElementSibling; container.classList.toggle('hidden'); }); }); // Test details toggling document.querySelectorAll('.test-header').forEach(header => { header.addEventListener('click', function(e) { e.stopPropagation(); const details = this.nextElementSibling; details.classList.toggle('visible'); const icon = this.querySelector('.expand-icon'); if (icon) icon.textContent = details.classList.contains('visible') ? 'โฒ' : 'โผ'; }); }); // Sidebar navigation document.querySelectorAll('.sidebar-item').forEach(item => { item.addEventListener('click', function() { const fileId = this.dataset.file; const section = document.getElementById(fileId); if (section) { section.scrollIntoView({ behavior: 'smooth', block: 'start' }); const header = section.querySelector('.file-header'); if (header && header.classList.contains('collapsed')) { header.click(); } } document.querySelectorAll('.sidebar-item').forEach(i => i.classList.remove('active')); this.classList.add('active'); }); }); // Search functionality const searchInput = document.getElementById('search'); if (searchInput) { searchInput.addEventListener('input', function() { const query = this.value.toLowerCase(); document.querySelectorAll('.test-item').forEach(item => { const text = item.textContent.toLowerCase(); item.style.display = text.includes(query) ? '' : 'none'; }); // Update counts document.querySelectorAll('.file-section').forEach(section => { const visible = section.querySelectorAll('.test-item:not([style*="display: none"])').length; const total = section.querySelectorAll('.test-item').length; if (visible === 0 && query) { section.style.display = 'none'; } else { section.style.display = ''; } }); }); } // Filter functionality const filterSelect = document.getElementById('filter'); if (filterSelect) { filterSelect.addEventListener('change', function() { const filter = this.value; document.querySelectorAll('.test-item').forEach(item => { const passed = item.querySelector('.status.passed') !== null; if (filter === 'all') { item.style.display = ''; } else if (filter === 'passed') { item.style.display = passed ? '' : 'none'; } else if (filter === 'failed') { item.style.display = passed ? 'none' : ''; } }); }); } // Expand/Collapse all document.getElementById('expand-all')?.addEventListener('click', function() { document.querySelectorAll('.file-header.collapsed').forEach(h => h.click()); }); document.getElementById('collapse-all')?.addEventListener('click', function() { document.querySelectorAll('.file-header:not(.collapsed)').forEach(h => h.click()); }); // Show only failed document.getElementById('show-failed')?.addEventListener('click', function() { document.getElementById('filter').value = 'failed'; document.getElementById('filter').dispatchEvent(new Event('change')); }); }); |} let generate_test_html test = let status_class = if test.success then "passed" else "failed" in let desc_escaped = html_escape test.description in let input_escaped = html_escape (truncate_string test.input) in let expected_escaped = html_escape (truncate_string test.expected) in let actual_escaped = html_escape (truncate_string test.actual) in let details_html = String.concat "" (List.map (fun (key, value) -> Printf.sprintf {|
%s
%s
%s
%s
%s
%s
This report shows the results of running the %s test suite against the HTML5 validator implementation.
Tests are organized by category and classified by their expected outcome:
-isvalid.html โ Valid HTML that should produce no errors or warnings-novalid.html โ Invalid HTML that should produce at least one error-haswarn.html โ HTML that should produce at least one warningClick on any test to expand its details and see the input HTML, expected output, and actual validator messages.