- Self-contained HTML dashboard with MS Office 365 design - pytest plugin captures inputs, outputs, and errors per test - Unified orchestrator runs pytest + torture tests together - Test files persisted in reports/test_files/ with relative links - GitHub Actions workflow with PR comments and job summaries - Makefile with convenient commands (test, view-dashboard, etc.) - Works offline with embedded JSON data (no CORS issues)
964 lines
35 KiB
HTML
964 lines
35 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>MCP Office Tools - Test Dashboard</title>
|
|
<style>
|
|
/* Microsoft Office Color Palette */
|
|
:root {
|
|
/* Office App Colors */
|
|
--word-blue: #2B579A;
|
|
--excel-green: #217346;
|
|
--powerpoint-orange: #D24726;
|
|
--outlook-blue: #0078D4;
|
|
|
|
/* Fluent Design Colors */
|
|
--primary-blue: #0078D4;
|
|
--success-green: #107C10;
|
|
--warning-orange: #FF8C00;
|
|
--error-red: #D83B01;
|
|
--neutral-gray: #605E5C;
|
|
--light-gray: #F3F2F1;
|
|
--lighter-gray: #FAF9F8;
|
|
--border-gray: #E1DFDD;
|
|
|
|
/* Status Colors */
|
|
--pass-green: #107C10;
|
|
--fail-red: #D83B01;
|
|
--skip-yellow: #FFB900;
|
|
|
|
/* Backgrounds */
|
|
--bg-primary: #FFFFFF;
|
|
--bg-secondary: #FAF9F8;
|
|
--bg-tertiary: #F3F2F1;
|
|
|
|
/* Text */
|
|
--text-primary: #201F1E;
|
|
--text-secondary: #605E5C;
|
|
--text-light: #8A8886;
|
|
}
|
|
|
|
/* Reset and Base Styles */
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', sans-serif;
|
|
background: var(--bg-secondary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
}
|
|
|
|
/* Header */
|
|
.header {
|
|
background: linear-gradient(135deg, var(--primary-blue) 0%, var(--word-blue) 100%);
|
|
color: white;
|
|
padding: 2rem 2rem 3rem;
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.header-content {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 2rem;
|
|
font-weight: 600;
|
|
margin-bottom: 0.5rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.office-icons {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.office-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 4px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 700;
|
|
font-size: 16px;
|
|
color: white;
|
|
}
|
|
|
|
.icon-word { background: var(--word-blue); }
|
|
.icon-excel { background: var(--excel-green); }
|
|
.icon-powerpoint { background: var(--powerpoint-orange); }
|
|
|
|
.header-meta {
|
|
opacity: 0.9;
|
|
font-size: 0.9rem;
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
/* Main Container */
|
|
.container {
|
|
max-width: 1400px;
|
|
margin: -2rem auto 2rem;
|
|
padding: 0 2rem;
|
|
}
|
|
|
|
/* Summary Cards */
|
|
.summary-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
gap: 1.5rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.summary-card {
|
|
background: var(--bg-primary);
|
|
border-radius: 8px;
|
|
padding: 1.5rem;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
border: 1px solid var(--border-gray);
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
}
|
|
|
|
.summary-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.card-title {
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.card-value {
|
|
font-size: 2.5rem;
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
}
|
|
|
|
.card-subtitle {
|
|
font-size: 0.875rem;
|
|
color: var(--text-light);
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.status-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.25rem 0.75rem;
|
|
border-radius: 12px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.badge-pass { background: rgba(16, 124, 16, 0.1); color: var(--pass-green); }
|
|
.badge-fail { background: rgba(216, 59, 1, 0.1); color: var(--fail-red); }
|
|
.badge-skip { background: rgba(255, 185, 0, 0.1); color: var(--skip-yellow); }
|
|
|
|
/* Controls */
|
|
.controls {
|
|
background: var(--bg-primary);
|
|
border-radius: 8px;
|
|
padding: 1.5rem;
|
|
margin-bottom: 1.5rem;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
border: 1px solid var(--border-gray);
|
|
display: flex;
|
|
gap: 1rem;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
}
|
|
|
|
.search-box {
|
|
flex: 1;
|
|
min-width: 300px;
|
|
position: relative;
|
|
}
|
|
|
|
.search-box input {
|
|
width: 100%;
|
|
padding: 0.75rem 1rem 0.75rem 2.5rem;
|
|
border: 2px solid var(--border-gray);
|
|
border-radius: 4px;
|
|
font-size: 0.875rem;
|
|
font-family: inherit;
|
|
transition: border-color 0.2s;
|
|
}
|
|
|
|
.search-box input:focus {
|
|
outline: none;
|
|
border-color: var(--primary-blue);
|
|
}
|
|
|
|
.search-icon {
|
|
position: absolute;
|
|
left: 0.875rem;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: var(--text-light);
|
|
}
|
|
|
|
.filter-group {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.filter-btn {
|
|
padding: 0.5rem 1rem;
|
|
border: 2px solid var(--border-gray);
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
border-radius: 4px;
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.filter-btn:hover {
|
|
border-color: var(--primary-blue);
|
|
background: var(--lighter-gray);
|
|
}
|
|
|
|
.filter-btn.active {
|
|
background: var(--primary-blue);
|
|
color: white;
|
|
border-color: var(--primary-blue);
|
|
}
|
|
|
|
.filter-btn.word.active { background: var(--word-blue); border-color: var(--word-blue); }
|
|
.filter-btn.excel.active { background: var(--excel-green); border-color: var(--excel-green); }
|
|
.filter-btn.powerpoint.active { background: var(--powerpoint-orange); border-color: var(--powerpoint-orange); }
|
|
|
|
/* Test Results */
|
|
.test-results {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.test-item {
|
|
background: var(--bg-primary);
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
border: 1px solid var(--border-gray);
|
|
overflow: hidden;
|
|
transition: box-shadow 0.2s;
|
|
}
|
|
|
|
.test-item:hover {
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|
}
|
|
|
|
.test-header {
|
|
padding: 1.25rem 1.5rem;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.test-header:hover {
|
|
background: var(--lighter-gray);
|
|
}
|
|
|
|
.test-status-icon {
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 700;
|
|
font-size: 14px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.status-pass {
|
|
background: var(--pass-green);
|
|
color: white;
|
|
}
|
|
|
|
.status-fail {
|
|
background: var(--fail-red);
|
|
color: white;
|
|
}
|
|
|
|
.status-skip {
|
|
background: var(--skip-yellow);
|
|
color: white;
|
|
}
|
|
|
|
.test-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.test-name {
|
|
font-weight: 600;
|
|
font-size: 1rem;
|
|
color: var(--text-primary);
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.test-meta {
|
|
font-size: 0.875rem;
|
|
color: var(--text-light);
|
|
display: flex;
|
|
gap: 1rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.test-category-badge {
|
|
display: inline-block;
|
|
padding: 0.25rem 0.75rem;
|
|
border-radius: 4px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
color: white;
|
|
}
|
|
|
|
.category-word { background: var(--word-blue); }
|
|
.category-excel { background: var(--excel-green); }
|
|
.category-powerpoint { background: var(--powerpoint-orange); }
|
|
.category-universal { background: var(--outlook-blue); }
|
|
.category-server { background: var(--neutral-gray); }
|
|
.category-other { background: var(--text-light); }
|
|
|
|
.test-duration {
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.expand-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 4px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--lighter-gray);
|
|
color: var(--text-secondary);
|
|
transition: transform 0.2s, background 0.2s;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.test-header:hover .expand-icon {
|
|
background: var(--light-gray);
|
|
}
|
|
|
|
.test-item.expanded .expand-icon {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
.test-details {
|
|
display: none;
|
|
border-top: 1px solid var(--border-gray);
|
|
background: var(--bg-secondary);
|
|
}
|
|
|
|
.test-item.expanded .test-details {
|
|
display: block;
|
|
}
|
|
|
|
.details-section {
|
|
padding: 1.5rem;
|
|
border-bottom: 1px solid var(--border-gray);
|
|
}
|
|
|
|
.details-section:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.section-title {
|
|
font-weight: 600;
|
|
font-size: 0.875rem;
|
|
color: var(--text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.code-block {
|
|
background: var(--text-primary);
|
|
color: #D4D4D4;
|
|
padding: 1rem;
|
|
border-radius: 4px;
|
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
|
font-size: 0.875rem;
|
|
line-height: 1.5;
|
|
overflow-x: auto;
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
.error-block {
|
|
background: rgba(216, 59, 1, 0.05);
|
|
border-left: 4px solid var(--error-red);
|
|
padding: 1rem;
|
|
border-radius: 4px;
|
|
color: var(--error-red);
|
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
|
font-size: 0.875rem;
|
|
line-height: 1.5;
|
|
overflow-x: auto;
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
.inputs-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
gap: 1rem;
|
|
}
|
|
|
|
.input-item {
|
|
background: var(--bg-primary);
|
|
padding: 1rem;
|
|
border-radius: 4px;
|
|
border: 1px solid var(--border-gray);
|
|
}
|
|
|
|
.input-label {
|
|
font-weight: 600;
|
|
font-size: 0.75rem;
|
|
color: var(--text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.input-value {
|
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
|
font-size: 0.875rem;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.file-link {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
color: var(--primary-blue);
|
|
text-decoration: none;
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 4px;
|
|
background: rgba(43, 87, 154, 0.1);
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.file-link:hover {
|
|
background: rgba(43, 87, 154, 0.2);
|
|
text-decoration: underline;
|
|
}
|
|
|
|
/* Empty State */
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 4rem 2rem;
|
|
color: var(--text-light);
|
|
}
|
|
|
|
.empty-state-icon {
|
|
font-size: 4rem;
|
|
margin-bottom: 1rem;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
/* Footer */
|
|
.footer {
|
|
text-align: center;
|
|
padding: 2rem;
|
|
color: var(--text-light);
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
/* Progress Bar */
|
|
.progress-bar {
|
|
width: 100%;
|
|
height: 8px;
|
|
background: var(--light-gray);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, var(--success-green) 0%, var(--excel-green) 100%);
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 768px) {
|
|
.container {
|
|
padding: 0 1rem;
|
|
}
|
|
|
|
.header {
|
|
padding: 1.5rem 1rem 2rem;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.summary-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.controls {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.search-box {
|
|
min-width: 100%;
|
|
}
|
|
}
|
|
|
|
/* Utility Classes */
|
|
.hidden {
|
|
display: none !important;
|
|
}
|
|
|
|
.text-muted {
|
|
color: var(--text-light);
|
|
}
|
|
|
|
.text-success {
|
|
color: var(--pass-green);
|
|
}
|
|
|
|
.text-error {
|
|
color: var(--fail-red);
|
|
}
|
|
|
|
.text-warning {
|
|
color: var(--skip-yellow);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Header -->
|
|
<header class="header">
|
|
<div class="header-content">
|
|
<h1>
|
|
<div class="office-icons">
|
|
<div class="office-icon icon-word">W</div>
|
|
<div class="office-icon icon-excel">X</div>
|
|
<div class="office-icon icon-powerpoint">P</div>
|
|
</div>
|
|
MCP Office Tools - Test Dashboard
|
|
</h1>
|
|
<div class="header-meta">
|
|
<span id="test-timestamp">Loading...</span>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Main Container -->
|
|
<div class="container">
|
|
<!-- Summary Cards -->
|
|
<div class="summary-grid">
|
|
<div class="summary-card">
|
|
<div class="card-header">
|
|
<div class="card-title">Total Tests</div>
|
|
</div>
|
|
<div class="card-value" id="total-tests">0</div>
|
|
<div class="card-subtitle">Test cases executed</div>
|
|
</div>
|
|
|
|
<div class="summary-card">
|
|
<div class="card-header">
|
|
<div class="card-title">Passed</div>
|
|
<span class="status-badge badge-pass">
|
|
<span>✓</span>
|
|
</span>
|
|
</div>
|
|
<div class="card-value text-success" id="passed-tests">0</div>
|
|
<div class="card-subtitle">
|
|
<span id="pass-rate">0%</span> pass rate
|
|
</div>
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" id="pass-progress" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="summary-card">
|
|
<div class="card-header">
|
|
<div class="card-title">Failed</div>
|
|
<span class="status-badge badge-fail">
|
|
<span>✗</span>
|
|
</span>
|
|
</div>
|
|
<div class="card-value text-error" id="failed-tests">0</div>
|
|
<div class="card-subtitle">Tests with errors</div>
|
|
</div>
|
|
|
|
<div class="summary-card">
|
|
<div class="card-header">
|
|
<div class="card-title">Duration</div>
|
|
</div>
|
|
<div class="card-value" id="total-duration" style="font-size: 2rem;">0s</div>
|
|
<div class="card-subtitle">Total execution time</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Controls -->
|
|
<div class="controls">
|
|
<div class="search-box">
|
|
<span class="search-icon">🔍</span>
|
|
<input
|
|
type="text"
|
|
id="search-input"
|
|
placeholder="Search tests by name, module, or category..."
|
|
autocomplete="off"
|
|
>
|
|
</div>
|
|
<div class="filter-group">
|
|
<button class="filter-btn active" data-filter="all">All</button>
|
|
<button class="filter-btn word" data-filter="Word">Word</button>
|
|
<button class="filter-btn excel" data-filter="Excel">Excel</button>
|
|
<button class="filter-btn powerpoint" data-filter="PowerPoint">PowerPoint</button>
|
|
<button class="filter-btn" data-filter="Universal">Universal</button>
|
|
<button class="filter-btn" data-filter="Server">Server</button>
|
|
</div>
|
|
<div class="filter-group">
|
|
<button class="filter-btn" data-status="passed">Passed</button>
|
|
<button class="filter-btn" data-status="failed">Failed</button>
|
|
<button class="filter-btn" data-status="skipped">Skipped</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Test Results -->
|
|
<div id="test-results" class="test-results">
|
|
<!-- Tests will be dynamically inserted here -->
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div id="empty-state" class="empty-state hidden">
|
|
<div class="empty-state-icon">📭</div>
|
|
<h2>No Test Results Found</h2>
|
|
<p>Run tests with: <code>pytest --dashboard-output=reports/test_results.json</code></p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<footer class="footer">
|
|
<p>MCP Office Tools Test Dashboard | Generated with ❤️ using pytest</p>
|
|
</footer>
|
|
|
|
<script>
|
|
// Dashboard Application
|
|
class TestDashboard {
|
|
constructor() {
|
|
this.data = null;
|
|
this.filteredTests = [];
|
|
this.activeFilters = {
|
|
category: 'all',
|
|
status: null,
|
|
search: ''
|
|
};
|
|
|
|
this.init();
|
|
}
|
|
|
|
async init() {
|
|
await this.loadData();
|
|
this.setupEventListeners();
|
|
this.render();
|
|
}
|
|
|
|
async loadData() {
|
|
try {
|
|
// Try embedded data first (works with file:// URLs)
|
|
const embeddedScript = document.getElementById('test-results-data');
|
|
if (embeddedScript) {
|
|
this.data = JSON.parse(embeddedScript.textContent);
|
|
this.filteredTests = this.data.tests;
|
|
return;
|
|
}
|
|
// Fallback to fetch (works with http:// URLs)
|
|
const response = await fetch('test_results.json');
|
|
this.data = await response.json();
|
|
this.filteredTests = this.data.tests;
|
|
} catch (error) {
|
|
console.error('Failed to load test results:', error);
|
|
document.getElementById('empty-state').classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
setupEventListeners() {
|
|
// Search
|
|
document.getElementById('search-input').addEventListener('input', (e) => {
|
|
this.activeFilters.search = e.target.value.toLowerCase();
|
|
this.applyFilters();
|
|
});
|
|
|
|
// Category filters
|
|
document.querySelectorAll('[data-filter]').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
document.querySelectorAll('[data-filter]').forEach(b => b.classList.remove('active'));
|
|
e.target.classList.add('active');
|
|
this.activeFilters.category = e.target.dataset.filter;
|
|
this.applyFilters();
|
|
});
|
|
});
|
|
|
|
// Status filters
|
|
document.querySelectorAll('[data-status]').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
if (e.target.classList.contains('active')) {
|
|
e.target.classList.remove('active');
|
|
this.activeFilters.status = null;
|
|
} else {
|
|
document.querySelectorAll('[data-status]').forEach(b => b.classList.remove('active'));
|
|
e.target.classList.add('active');
|
|
this.activeFilters.status = e.target.dataset.status;
|
|
}
|
|
this.applyFilters();
|
|
});
|
|
});
|
|
}
|
|
|
|
applyFilters() {
|
|
this.filteredTests = this.data.tests.filter(test => {
|
|
// Category filter
|
|
if (this.activeFilters.category !== 'all' && test.category !== this.activeFilters.category) {
|
|
return false;
|
|
}
|
|
|
|
// Status filter
|
|
if (this.activeFilters.status && test.outcome !== this.activeFilters.status) {
|
|
return false;
|
|
}
|
|
|
|
// Search filter
|
|
if (this.activeFilters.search) {
|
|
const searchStr = this.activeFilters.search;
|
|
const matchName = test.name.toLowerCase().includes(searchStr);
|
|
const matchModule = test.module.toLowerCase().includes(searchStr);
|
|
const matchCategory = test.category.toLowerCase().includes(searchStr);
|
|
|
|
if (!matchName && !matchModule && !matchCategory) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
this.renderTests();
|
|
}
|
|
|
|
render() {
|
|
if (!this.data) return;
|
|
|
|
this.renderSummary();
|
|
this.renderTests();
|
|
}
|
|
|
|
renderSummary() {
|
|
const { metadata, summary } = this.data;
|
|
|
|
// Timestamp
|
|
const timestamp = new Date(metadata.start_time).toLocaleString();
|
|
document.getElementById('test-timestamp').textContent = `Run on ${timestamp}`;
|
|
|
|
// Summary cards
|
|
document.getElementById('total-tests').textContent = summary.total;
|
|
document.getElementById('passed-tests').textContent = summary.passed;
|
|
document.getElementById('failed-tests').textContent = summary.failed;
|
|
document.getElementById('pass-rate').textContent = `${summary.pass_rate.toFixed(1)}%`;
|
|
document.getElementById('pass-progress').style.width = `${summary.pass_rate}%`;
|
|
|
|
// Duration
|
|
const duration = metadata.duration.toFixed(2);
|
|
document.getElementById('total-duration').textContent = `${duration}s`;
|
|
}
|
|
|
|
renderTests() {
|
|
const container = document.getElementById('test-results');
|
|
const emptyState = document.getElementById('empty-state');
|
|
|
|
if (this.filteredTests.length === 0) {
|
|
container.innerHTML = '';
|
|
emptyState.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
emptyState.classList.add('hidden');
|
|
|
|
container.innerHTML = this.filteredTests.map(test => this.createTestItem(test)).join('');
|
|
|
|
// Add click handlers for expand/collapse
|
|
container.querySelectorAll('.test-header').forEach(header => {
|
|
header.addEventListener('click', () => {
|
|
header.parentElement.classList.toggle('expanded');
|
|
});
|
|
});
|
|
}
|
|
|
|
createTestItem(test) {
|
|
const statusIcon = this.getStatusIcon(test.outcome);
|
|
const categoryClass = `category-${test.category.toLowerCase()}`;
|
|
const duration = (test.duration * 1000).toFixed(0); // ms
|
|
|
|
return `
|
|
<div class="test-item" data-test-id="${test.nodeid}">
|
|
<div class="test-header">
|
|
<div class="test-status-icon status-${test.outcome}">
|
|
${statusIcon}
|
|
</div>
|
|
<div class="test-info">
|
|
<div class="test-name">${this.escapeHtml(test.name)}</div>
|
|
<div class="test-meta">
|
|
<span class="test-category-badge ${categoryClass}">${test.category}</span>
|
|
<span>${test.module}</span>
|
|
<span class="test-duration">${duration}ms</span>
|
|
</div>
|
|
</div>
|
|
<div class="expand-icon">▼</div>
|
|
</div>
|
|
<div class="test-details">
|
|
${this.createTestDetails(test)}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
createTestDetails(test) {
|
|
let html = '';
|
|
|
|
// Inputs
|
|
if (test.inputs && Object.keys(test.inputs).length > 0) {
|
|
html += `
|
|
<div class="details-section">
|
|
<div class="section-title">Test Inputs</div>
|
|
<div class="inputs-grid">
|
|
${Object.entries(test.inputs).map(([key, value]) => `
|
|
<div class="input-item">
|
|
<div class="input-label">${this.escapeHtml(key)}</div>
|
|
<div class="input-value">${this.formatInputValue(key, value)}</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Outputs
|
|
if (test.outputs) {
|
|
html += `
|
|
<div class="details-section">
|
|
<div class="section-title">Test Outputs</div>
|
|
<div class="code-block">${this.escapeHtml(JSON.stringify(test.outputs, null, 2))}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Error
|
|
if (test.error) {
|
|
html += `
|
|
<div class="details-section">
|
|
<div class="section-title">Error Details</div>
|
|
<div class="error-block">${this.escapeHtml(test.error)}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Traceback
|
|
if (test.traceback) {
|
|
html += `
|
|
<div class="details-section">
|
|
<div class="section-title">Traceback</div>
|
|
<div class="error-block">${this.escapeHtml(test.traceback)}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Full path
|
|
html += `
|
|
<div class="details-section">
|
|
<div class="section-title">Test Path</div>
|
|
<div class="code-block">${this.escapeHtml(test.nodeid)}</div>
|
|
</div>
|
|
`;
|
|
|
|
return html;
|
|
}
|
|
|
|
getStatusIcon(outcome) {
|
|
switch (outcome) {
|
|
case 'passed': return '✓';
|
|
case 'failed': return '✗';
|
|
case 'skipped': return '⊘';
|
|
default: return '?';
|
|
}
|
|
}
|
|
|
|
formatInputValue(key, value) {
|
|
const strValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
// Detect file paths - relative (test_files/...) or absolute
|
|
const isRelativePath = strValue.startsWith('test_files/');
|
|
const isAbsolutePath = /^["']?(\/[^"']+|[A-Z]:\\[^"']+)["']?$/i.test(strValue);
|
|
const isFilePath = isRelativePath || isAbsolutePath || key.toLowerCase().includes('file') || key.toLowerCase().includes('path');
|
|
|
|
if (isFilePath && (isRelativePath || isAbsolutePath)) {
|
|
// Extract the actual path (remove quotes if present)
|
|
const cleanPath = strValue.replace(/^["']|["']$/g, '');
|
|
const fileName = cleanPath.split('/').pop() || cleanPath.split('\\').pop();
|
|
const fileExt = fileName.split('.').pop()?.toLowerCase() || '';
|
|
// Choose icon based on file type
|
|
let icon = '📄';
|
|
if (['xlsx', 'xls', 'csv'].includes(fileExt)) icon = '📊';
|
|
else if (['docx', 'doc'].includes(fileExt)) icon = '📝';
|
|
else if (['pptx', 'ppt'].includes(fileExt)) icon = '📽️';
|
|
|
|
// Use relative path for relative files, file:// for absolute paths
|
|
const href = isRelativePath ? this.escapeHtml(cleanPath) : `file://${this.escapeHtml(cleanPath)}`;
|
|
const downloadAttr = isRelativePath ? 'download' : '';
|
|
return `<a href="${href}" class="file-link" title="Download ${this.escapeHtml(fileName)}" ${downloadAttr} target="_blank">${icon} ${this.escapeHtml(fileName)}</a>`;
|
|
}
|
|
return this.escapeHtml(strValue);
|
|
}
|
|
|
|
escapeHtml(text) {
|
|
if (text === null || text === undefined) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = String(text);
|
|
return div.innerHTML;
|
|
}
|
|
}
|
|
|
|
// Initialize dashboard when DOM is ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', () => new TestDashboard());
|
|
} else {
|
|
new TestDashboard();
|
|
}
|
|
</script>
|
|
<script type="application/json" id="test-results-data">{"metadata": {"start_time": "2026-01-11T00:23:10.209539", "end_time": "2026-01-11T00:23:12.295169", "duration": 1.052842140197754, "exit_status": 0, "pytest_version": "9.0.2", "test_types": ["pytest", "torture_test"]}, "summary": {"total": 6, "passed": 5, "failed": 0, "skipped": 1, "pass_rate": 83.33333333333334}, "categories": {"Excel": {"total": 4, "passed": 3, "failed": 0, "skipped": 1}, "Word": {"total": 2, "passed": 2, "failed": 0, "skipped": 0}}, "tests": [{"name": "Excel Data Analysis", "nodeid": "torture_test.py::test_excel_data_analysis", "category": "Excel", "outcome": "passed", "duration": 0.1404409408569336, "timestamp": "2026-01-11T00:23:12.271793", "module": "torture_test", "class": null, "function": "test_excel_data_analysis", "inputs": {"file": "test_files/test_data.xlsx"}, "outputs": {"sheets_analyzed": ["Test Data"]}, "error": null, "traceback": null}, {"name": "Excel Formula Extraction", "nodeid": "torture_test.py::test_excel_formula_extraction", "category": "Excel", "outcome": "passed", "duration": 0.0031723976135253906, "timestamp": "2026-01-11T00:23:12.274971", "module": "torture_test", "class": null, "function": "test_excel_formula_extraction", "inputs": {"file": "test_files/test_data.xlsx"}, "outputs": {"total_formulas": 8}, "error": null, "traceback": null}, {"name": "Excel Chart Data Generation", "nodeid": "torture_test.py::test_excel_chart_generation", "category": "Excel", "outcome": "passed", "duration": 0.003323078155517578, "timestamp": "2026-01-11T00:23:12.278299", "module": "torture_test", "class": null, "function": "test_excel_chart_generation", "inputs": {"file": "test_files/test_data.xlsx", "x_column": "Category", "y_columns": ["Value"]}, "outputs": {"chart_libraries": 2}, "error": null, "traceback": null}, {"name": "Word Structure Analysis", "nodeid": "torture_test.py::test_word_structure_analysis", "category": "Word", "outcome": "passed", "duration": 0.010413646697998047, "timestamp": "2026-01-11T00:23:12.288718", "module": "torture_test", "class": null, "function": "test_word_structure_analysis", "inputs": {"file": "test_files/test_document.docx"}, "outputs": {"total_headings": 0}, "error": null, "traceback": null}, {"name": "Word Table Extraction", "nodeid": "torture_test.py::test_word_table_extraction", "category": "Word", "outcome": "passed", "duration": 0.006224393844604492, "timestamp": "2026-01-11T00:23:12.294948", "module": "torture_test", "class": null, "function": "test_word_table_extraction", "inputs": {"file": "test_files/test_document.docx"}, "outputs": {"total_tables": 0}, "error": null, "traceback": null}, {"name": "Real Excel File Analysis (FORScan)", "nodeid": "torture_test.py::test_real_excel_analysis", "category": "Excel", "outcome": "skipped", "duration": 0, "timestamp": "2026-01-11T00:23:12.294963", "module": "torture_test", "class": null, "function": "test_real_excel_analysis", "inputs": {"file": "/home/rpm/FORScan Lite spreadsheets v1.1/FORScan Lite spreadsheet - PIDs.xlsx"}, "outputs": null, "error": "File not found: /home/rpm/FORScan Lite spreadsheets v1.1/FORScan Lite spreadsheet - PIDs.xlsx", "traceback": null}]}</script>
|
|
</body>
|
|
</html>
|