OCaml HTML5 parser/serialiser based on Python's JustHTML
at main 21 kB view raw
1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>HTML5rw Regression Test Suite</title> 7 <style> 8 :root { 9 --bg-primary: #1a1a2e; 10 --bg-secondary: #16213e; 11 --bg-tertiary: #0f3460; 12 --text-primary: #eee; 13 --text-secondary: #aaa; 14 --text-muted: #666; 15 --accent: #e94560; 16 --accent-light: #ff6b8a; 17 --success: #4ade80; 18 --success-dim: rgba(74, 222, 128, 0.2); 19 --failure: #f87171; 20 --failure-dim: rgba(248, 113, 113, 0.2); 21 --warning: #fbbf24; 22 --info: #60a5fa; 23 --border: #333; 24 --code-bg: #0d1117; 25 } 26 27 * { box-sizing: border-box; margin: 0; padding: 0; } 28 29 body { 30 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 31 background: var(--bg-primary); 32 color: var(--text-primary); 33 line-height: 1.6; 34 padding: 20px; 35 } 36 37 .container { max-width: 1400px; margin: 0 auto; } 38 39 .hero { 40 background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%); 41 padding: 30px 40px; 42 border-radius: 12px; 43 margin-bottom: 30px; 44 border: 1px solid var(--border); 45 } 46 47 .hero h1 { 48 font-size: 2rem; 49 margin-bottom: 10px; 50 color: var(--accent); 51 } 52 53 .hero p { color: var(--text-secondary); margin-bottom: 15px; } 54 55 .controls { 56 display: flex; 57 gap: 12px; 58 flex-wrap: wrap; 59 align-items: center; 60 } 61 62 button { 63 padding: 12px 24px; 64 border: none; 65 border-radius: 8px; 66 background: var(--accent); 67 color: white; 68 cursor: pointer; 69 font-size: 14px; 70 font-weight: 600; 71 transition: all 0.2s; 72 } 73 74 button:hover { background: var(--accent-light); transform: translateY(-1px); } 75 button:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } 76 button.secondary { background: var(--bg-tertiary); border: 1px solid var(--border); } 77 button.secondary:hover { background: var(--bg-secondary); } 78 79 select { 80 padding: 12px 16px; 81 border: 1px solid var(--border); 82 border-radius: 8px; 83 background: var(--bg-secondary); 84 color: var(--text-primary); 85 font-size: 14px; 86 } 87 88 .summary-grid { 89 display: grid; 90 grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); 91 gap: 20px; 92 margin-bottom: 30px; 93 } 94 95 .summary-card { 96 background: var(--bg-secondary); 97 border-radius: 12px; 98 padding: 20px; 99 border: 1px solid var(--border); 100 text-align: center; 101 } 102 103 .summary-card h3 { 104 font-size: 0.8rem; 105 text-transform: uppercase; 106 letter-spacing: 1px; 107 color: var(--text-secondary); 108 margin-bottom: 8px; 109 } 110 111 .summary-card .value { 112 font-size: 2rem; 113 font-weight: 700; 114 } 115 116 .summary-card .value.success { color: var(--success); } 117 .summary-card .value.failure { color: var(--failure); } 118 .summary-card .value.neutral { color: var(--text-primary); } 119 120 .progress-container { 121 background: var(--bg-secondary); 122 border-radius: 12px; 123 padding: 20px; 124 margin-bottom: 30px; 125 border: 1px solid var(--border); 126 } 127 128 .progress-bar { 129 height: 24px; 130 background: var(--failure-dim); 131 border-radius: 12px; 132 overflow: hidden; 133 margin-top: 10px; 134 } 135 136 .progress-fill { 137 height: 100%; 138 background: var(--success); 139 border-radius: 12px; 140 transition: width 0.3s ease; 141 display: flex; 142 align-items: center; 143 justify-content: center; 144 font-size: 12px; 145 font-weight: 600; 146 } 147 148 .status-text { 149 font-size: 14px; 150 color: var(--text-secondary); 151 margin-bottom: 8px; 152 } 153 154 .results-section { 155 background: var(--bg-secondary); 156 border-radius: 12px; 157 margin-bottom: 20px; 158 border: 1px solid var(--border); 159 overflow: hidden; 160 } 161 162 .results-header { 163 padding: 16px 20px; 164 background: var(--bg-tertiary); 165 cursor: pointer; 166 display: flex; 167 justify-content: space-between; 168 align-items: center; 169 } 170 171 .results-header:hover { background: #1a4a7a; } 172 173 .results-header h2 { 174 font-size: 1rem; 175 display: flex; 176 align-items: center; 177 gap: 10px; 178 } 179 180 .results-header .toggle { color: var(--text-secondary); transition: transform 0.2s; } 181 .results-header.collapsed .toggle { transform: rotate(-90deg); } 182 183 .results-stats { 184 display: flex; 185 gap: 15px; 186 font-size: 14px; 187 } 188 189 .results-stats .passed { color: var(--success); } 190 .results-stats .failed { color: var(--failure); } 191 192 .results-content { padding: 15px; } 193 .results-content.hidden { display: none; } 194 195 .test-item { 196 margin: 6px 0; 197 border: 1px solid var(--border); 198 border-radius: 6px; 199 overflow: hidden; 200 } 201 202 .test-header { 203 padding: 10px 14px; 204 cursor: pointer; 205 display: flex; 206 justify-content: space-between; 207 align-items: center; 208 background: var(--bg-primary); 209 font-size: 13px; 210 } 211 212 .test-header:hover { background: rgba(255,255,255,0.03); } 213 214 .test-header .status { 215 width: 8px; 216 height: 8px; 217 border-radius: 50%; 218 margin-right: 10px; 219 flex-shrink: 0; 220 } 221 222 .test-header .status.passed { background: var(--success); } 223 .test-header .status.failed { background: var(--failure); } 224 225 .test-header .test-info { flex: 1; display: flex; align-items: center; min-width: 0; } 226 .test-header .test-num { font-weight: 600; margin-right: 10px; color: var(--text-muted); } 227 .test-header .test-desc { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } 228 .test-header .expand-icon { color: var(--text-muted); font-size: 0.75rem; } 229 230 .test-details { 231 padding: 15px; 232 background: var(--code-bg); 233 border-top: 1px solid var(--border); 234 display: none; 235 font-size: 13px; 236 } 237 238 .test-details.visible { display: block; } 239 240 .detail-section { margin-bottom: 15px; } 241 .detail-section:last-child { margin-bottom: 0; } 242 243 .detail-section h4 { 244 font-size: 11px; 245 text-transform: uppercase; 246 letter-spacing: 1px; 247 color: var(--text-muted); 248 margin-bottom: 8px; 249 } 250 251 .detail-section pre { 252 background: var(--bg-secondary); 253 padding: 12px; 254 border-radius: 6px; 255 overflow-x: auto; 256 font-family: 'Monaco', 'Menlo', monospace; 257 font-size: 12px; 258 white-space: pre-wrap; 259 word-break: break-word; 260 max-height: 300px; 261 overflow-y: auto; 262 border: 1px solid var(--border); 263 } 264 265 .detail-row { 266 display: grid; 267 grid-template-columns: 1fr 1fr; 268 gap: 15px; 269 } 270 271 .filter-controls { 272 display: flex; 273 gap: 10px; 274 margin-bottom: 20px; 275 flex-wrap: wrap; 276 } 277 278 .filter-controls input[type="search"] { 279 padding: 10px 14px; 280 border: 1px solid var(--border); 281 border-radius: 8px; 282 background: var(--bg-secondary); 283 color: var(--text-primary); 284 font-size: 14px; 285 width: 250px; 286 } 287 288 .log-output { 289 background: var(--code-bg); 290 border: 1px solid var(--border); 291 border-radius: 8px; 292 padding: 15px; 293 font-family: 'Monaco', 'Menlo', monospace; 294 font-size: 12px; 295 max-height: 200px; 296 overflow-y: auto; 297 white-space: pre-wrap; 298 margin-bottom: 20px; 299 } 300 301 @media (max-width: 768px) { 302 .detail-row { grid-template-columns: 1fr; } 303 .summary-grid { grid-template-columns: 1fr 1fr; } 304 } 305 </style> 306</head> 307<body> 308 <div class="container"> 309 <div class="hero"> 310 <h1>HTML5rw Regression Test Suite</h1> 311 <p> 312 Browser-based regression testing for the HTML5rw OCaml parser. 313 Tests are loaded from the html5lib-tests conformance suite. 314 </p> 315 <div class="controls"> 316 <button id="run-all" onclick="runAllTests()">Run All Tests</button> 317 <button id="run-tree" class="secondary" onclick="runTreeTests()">Tree Construction Only</button> 318 <button id="run-encoding" class="secondary" onclick="runEncodingTests()">Encoding Only</button> 319 <select id="mode-select"> 320 <option value="js">JavaScript (js_of_ocaml)</option> 321 <option value="wasm">WebAssembly (wasm_of_ocaml)</option> 322 </select> 323 </div> 324 </div> 325 326 <div class="summary-grid" id="summary" style="display: none;"> 327 <div class="summary-card"> 328 <h3>Total Tests</h3> 329 <div class="value neutral" id="total-count">0</div> 330 </div> 331 <div class="summary-card"> 332 <h3>Passed</h3> 333 <div class="value success" id="passed-count">0</div> 334 </div> 335 <div class="summary-card"> 336 <h3>Failed</h3> 337 <div class="value failure" id="failed-count">0</div> 338 </div> 339 <div class="summary-card"> 340 <h3>Pass Rate</h3> 341 <div class="value" id="pass-rate">0%</div> 342 </div> 343 </div> 344 345 <div class="progress-container" id="progress-container"> 346 <div class="status-text" id="status-text">Ready to run tests. Click a button above to start.</div> 347 <div class="progress-bar"> 348 <div class="progress-fill" id="progress-fill" style="width: 0%"></div> 349 </div> 350 </div> 351 352 <div class="log-output" id="log-output">Waiting for tests to start...</div> 353 354 <div class="filter-controls" id="filter-controls" style="display: none;"> 355 <input type="search" id="search" placeholder="Search tests..."> 356 <select id="filter"> 357 <option value="all">All Tests</option> 358 <option value="passed">Passed Only</option> 359 <option value="failed">Failed Only</option> 360 </select> 361 <button class="secondary" onclick="expandAll()">Expand All</button> 362 <button class="secondary" onclick="collapseAll()">Collapse All</button> 363 </div> 364 365 <div id="results-container"></div> 366 </div> 367 368 <script> 369 // Test file lists 370 const TREE_CONSTRUCTION_FILES = [ 371 "adoption01.dat", "adoption02.dat", "blocks.dat", "comments01.dat", 372 "doctype01.dat", "domjs-unsafe.dat", "entities01.dat", "entities02.dat", 373 "foreign-fragment.dat", "html5test-com.dat", "inbody01.dat", "isindex.dat", 374 "main-element.dat", "math.dat", "menuitem-element.dat", "namespace-sensitivity.dat", 375 "noscript01.dat", "pending-spec-changes-plain-text-unsafe.dat", 376 "pending-spec-changes.dat", "plain-text-unsafe.dat", "quirks01.dat", "ruby.dat", 377 "scriptdata01.dat", "search-element.dat", "svg.dat", "tables01.dat", 378 "template.dat", "tests_innerHTML_1.dat", "tests1.dat", "tests10.dat", 379 "tests11.dat", "tests12.dat", "tests14.dat", "tests15.dat", "tests16.dat", 380 "tests17.dat", "tests18.dat", "tests19.dat", "tests2.dat", "tests20.dat", 381 "tests21.dat", "tests22.dat", "tests23.dat", "tests24.dat", "tests25.dat", 382 "tests26.dat", "tests3.dat", "tests4.dat", "tests5.dat", "tests6.dat", 383 "tests7.dat", "tests8.dat", "tests9.dat", "tricky01.dat", "webkit01.dat", 384 "webkit02.dat" 385 ]; 386 387 const ENCODING_FILES = [ 388 "test-yahoo-jp.dat", "tests1.dat", "tests2.dat" 389 ]; 390 391 let testRunner = null; 392 let isRunning = false; 393 394 function log(msg) { 395 const output = document.getElementById('log-output'); 396 output.textContent += msg + '\n'; 397 output.scrollTop = output.scrollHeight; 398 } 399 400 function clearLog() { 401 document.getElementById('log-output').textContent = ''; 402 } 403 404 function updateProgress(current, total, msg) { 405 const pct = total > 0 ? (current / total * 100) : 0; 406 document.getElementById('progress-fill').style.width = pct + '%'; 407 document.getElementById('status-text').textContent = msg || `Running: ${current}/${total}`; 408 } 409 410 function updateSummary(passed, failed) { 411 const total = passed + failed; 412 const rate = total > 0 ? (passed / total * 100).toFixed(1) : 0; 413 document.getElementById('total-count').textContent = total; 414 document.getElementById('passed-count').textContent = passed; 415 document.getElementById('failed-count').textContent = failed; 416 document.getElementById('pass-rate').textContent = rate + '%'; 417 document.getElementById('pass-rate').className = 'value ' + (rate >= 99 ? 'success' : rate >= 90 ? 'neutral' : 'failure'); 418 document.getElementById('summary').style.display = 'grid'; 419 } 420 421 async function loadTestRunner() { 422 const mode = document.getElementById('mode-select').value; 423 const scriptName = mode === 'wasm' ? 'htmlrw-tests.wasm.js' : 'htmlrw-tests.js'; 424 425 if (typeof html5rwTests !== 'undefined') { 426 return true; 427 } 428 429 log(`Loading ${scriptName}...`); 430 try { 431 await new Promise((resolve, reject) => { 432 const script = document.createElement('script'); 433 script.src = `_build/default/lib/js/${scriptName}`; 434 script.onload = resolve; 435 script.onerror = () => reject(new Error(`Failed to load ${scriptName}`)); 436 document.head.appendChild(script); 437 }); 438 439 // Wait for initialization 440 await new Promise(resolve => setTimeout(resolve, 100)); 441 442 if (typeof html5rwTests === 'undefined') { 443 throw new Error('Test runner not initialized'); 444 } 445 446 log(`Test runner loaded (version ${html5rwTests.version})`); 447 return true; 448 } catch (e) { 449 log(`ERROR: ${e.message}`); 450 log('Make sure to run: opam exec -- dune build lib/js/htmlrw-tests.js'); 451 return false; 452 } 453 } 454 455 async function fetchTestFile(type, filename) { 456 const basePath = type === 'tree-construction' 457 ? 'html5lib-tests/tree-construction/' 458 : 'html5lib-tests/encoding/'; 459 const url = basePath + filename; 460 const response = await fetch(url); 461 if (!response.ok) { 462 throw new Error(`Failed to fetch ${url}: ${response.status}`); 463 } 464 return await response.text(); 465 } 466 467 function renderFileResult(result) { 468 const section = document.createElement('div'); 469 section.className = 'results-section'; 470 section.dataset.filename = result.filename; 471 472 const collapsed = result.failedCount === 0 ? 'collapsed' : ''; 473 const hidden = result.failedCount === 0 ? 'hidden' : ''; 474 475 section.innerHTML = ` 476 <div class="results-header ${collapsed}"> 477 <h2><span class="toggle">▼</span> ${escapeHtml(result.filename)}</h2> 478 <div class="results-stats"> 479 <span class="passed">✓ ${result.passedCount}</span> 480 <span class="failed">✗ ${result.failedCount}</span> 481 </div> 482 </div> 483 <div class="results-content ${hidden}"> 484 ${result.tests.map(renderTestResult).join('')} 485 </div> 486 `; 487 488 // Add toggle handler 489 section.querySelector('.results-header').addEventListener('click', function() { 490 this.classList.toggle('collapsed'); 491 this.nextElementSibling.classList.toggle('hidden'); 492 }); 493 494 // Add test detail handlers 495 section.querySelectorAll('.test-header').forEach(header => { 496 header.addEventListener('click', function(e) { 497 e.stopPropagation(); 498 const details = this.nextElementSibling; 499 details.classList.toggle('visible'); 500 const icon = this.querySelector('.expand-icon'); 501 icon.textContent = details.classList.contains('visible') ? '▲' : '▼'; 502 }); 503 }); 504 505 return section; 506 } 507 508 function renderTestResult(test) { 509 const statusClass = test.success ? 'passed' : 'failed'; 510 return ` 511 <div class="test-item" data-passed="${test.success}"> 512 <div class="test-header"> 513 <div class="test-info"> 514 <span class="status ${statusClass}"></span> 515 <span class="test-num">#${test.testNum}</span> 516 <span class="test-desc">${escapeHtml(test.description)}</span> 517 </div> 518 <span class="expand-icon">▼</span> 519 </div> 520 <div class="test-details"> 521 <div class="detail-section"> 522 <h4>Input</h4> 523 <pre>${escapeHtml(test.input)}</pre> 524 </div> 525 <div class="detail-row"> 526 <div class="detail-section"> 527 <h4>Expected</h4> 528 <pre>${escapeHtml(test.expected)}</pre> 529 </div> 530 <div class="detail-section"> 531 <h4>Actual</h4> 532 <pre>${escapeHtml(test.actual)}</pre> 533 </div> 534 </div> 535 </div> 536 </div> 537 `; 538 } 539 540 function escapeHtml(str) { 541 const div = document.createElement('div'); 542 div.textContent = str; 543 return div.innerHTML; 544 } 545 546 async function runTests(testType, files, basePath) { 547 if (isRunning) return; 548 isRunning = true; 549 550 clearLog(); 551 document.getElementById('results-container').innerHTML = ''; 552 document.getElementById('filter-controls').style.display = 'none'; 553 554 const buttons = document.querySelectorAll('button'); 555 buttons.forEach(b => b.disabled = true); 556 557 try { 558 if (!await loadTestRunner()) { 559 return; 560 } 561 562 log(`Starting ${testType} tests...`); 563 let totalPassed = 0; 564 let totalFailed = 0; 565 const allResults = []; 566 567 for (let i = 0; i < files.length; i++) { 568 const filename = files[i]; 569 updateProgress(i, files.length, `Loading ${filename}...`); 570 571 try { 572 const content = await fetchTestFile(basePath, filename); 573 log(`Running ${filename}...`); 574 575 let result; 576 if (basePath === 'tree-construction') { 577 result = html5rwTests.runTreeConstructionTest(filename, content); 578 } else { 579 result = html5rwTests.runEncodingTest(filename, content); 580 } 581 582 totalPassed += result.passedCount; 583 totalFailed += result.failedCount; 584 allResults.push(result); 585 586 log(` ${filename}: ${result.passedCount} passed, ${result.failedCount} failed`); 587 588 // Render result immediately 589 const section = renderFileResult(result); 590 document.getElementById('results-container').appendChild(section); 591 592 } catch (e) { 593 log(` ERROR loading ${filename}: ${e.message}`); 594 } 595 596 updateSummary(totalPassed, totalFailed); 597 updateProgress(i + 1, files.length); 598 } 599 600 updateProgress(files.length, files.length, `Complete: ${totalPassed} passed, ${totalFailed} failed`); 601 log(`\n=== SUMMARY ===`); 602 log(`Total: ${totalPassed + totalFailed} tests`); 603 log(`Passed: ${totalPassed}`); 604 log(`Failed: ${totalFailed}`); 605 log(`Pass rate: ${((totalPassed / (totalPassed + totalFailed)) * 100).toFixed(2)}%`); 606 607 document.getElementById('filter-controls').style.display = 'flex'; 608 setupFilters(); 609 610 } finally { 611 isRunning = false; 612 buttons.forEach(b => b.disabled = false); 613 } 614 } 615 616 function runAllTests() { 617 // Run both tree and encoding tests 618 runTests('all', TREE_CONSTRUCTION_FILES.concat(ENCODING_FILES.map(f => 'encoding/' + f)), 'tree-construction'); 619 } 620 621 async function runTreeTests() { 622 await runTests('tree-construction', TREE_CONSTRUCTION_FILES, 'tree-construction'); 623 } 624 625 async function runEncodingTests() { 626 await runTests('encoding', ENCODING_FILES, 'encoding'); 627 } 628 629 function setupFilters() { 630 const search = document.getElementById('search'); 631 const filter = document.getElementById('filter'); 632 633 search.addEventListener('input', applyFilters); 634 filter.addEventListener('change', applyFilters); 635 } 636 637 function applyFilters() { 638 const query = document.getElementById('search').value.toLowerCase(); 639 const filterValue = document.getElementById('filter').value; 640 641 document.querySelectorAll('.test-item').forEach(item => { 642 const text = item.textContent.toLowerCase(); 643 const passed = item.dataset.passed === 'true'; 644 let visible = true; 645 646 if (query && !text.includes(query)) visible = false; 647 if (filterValue === 'passed' && !passed) visible = false; 648 if (filterValue === 'failed' && passed) visible = false; 649 650 item.style.display = visible ? '' : 'none'; 651 }); 652 } 653 654 function expandAll() { 655 document.querySelectorAll('.results-header.collapsed').forEach(h => h.click()); 656 } 657 658 function collapseAll() { 659 document.querySelectorAll('.results-header:not(.collapsed)').forEach(h => h.click()); 660 } 661 662 // Quick test on load 663 window.addEventListener('load', function() { 664 log('Ready. Select a test mode and click Run to begin.'); 665 }); 666 </script> 667</body> 668</html>