- Fix collect_tests to use JSON report parsing (was returning 0 tests) - Add Test Collection panel to testing dashboard showing total tests, unit/integration/performance breakdown, and file count - Reorganize sidebar: create Platform Health section with Testing Hub, Code Quality, and Background Tasks - Keep Developer Tools for Components and Icons only - Platform Monitoring now contains Import Jobs and Application Logs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
420 lines
23 KiB
HTML
420 lines
23 KiB
HTML
{# app/templates/admin/testing-dashboard.html #}
|
|
{% extends "admin/base.html" %}
|
|
{% from 'shared/macros/alerts.html' import loading_state, error_state, alert_dynamic %}
|
|
{% from 'shared/macros/headers.html' import page_header_flex, refresh_button, action_button %}
|
|
|
|
{% block title %}Testing Dashboard{% endblock %}
|
|
|
|
{% block alpine_data %}testingDashboard(){% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script src="/static/admin/js/testing-dashboard.js"></script>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
{% call page_header_flex(title='Testing Dashboard', subtitle='pytest results and test coverage') %}
|
|
{{ refresh_button(variant='secondary') }}
|
|
{{ action_button('Run Tests', 'Running...', 'running', 'runTests()', icon='play') }}
|
|
{% endcall %}
|
|
|
|
{{ loading_state('Loading test results...') }}
|
|
|
|
{{ error_state('Error loading test results') }}
|
|
|
|
{{ alert_dynamic(type='success', message_var='successMessage', show_condition='successMessage') }}
|
|
|
|
<!-- Running Indicator -->
|
|
<div x-show="running" x-cloak class="mb-6">
|
|
<div class="p-4 bg-purple-100 dark:bg-purple-900 rounded-lg shadow-xs">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center">
|
|
<span x-html="$icon('spinner', 'h-5 w-5 text-purple-600 dark:text-purple-400 mr-3')"></span>
|
|
<div>
|
|
<p class="font-semibold text-purple-800 dark:text-purple-200">Running tests...</p>
|
|
<p class="text-sm text-purple-600 dark:text-purple-300">Tests are executing in the background. You can leave this page and come back.</p>
|
|
</div>
|
|
</div>
|
|
<div class="text-right">
|
|
<p class="text-2xl font-bold text-purple-700 dark:text-purple-300" x-text="formatDuration(elapsedTime)">0s</p>
|
|
<p class="text-xs text-purple-600 dark:text-purple-400">elapsed</p>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3">
|
|
<div class="w-full bg-purple-200 dark:bg-purple-800 rounded-full h-1.5 overflow-hidden">
|
|
<div class="bg-purple-600 dark:bg-purple-400 h-1.5 rounded-full animate-pulse" style="width: 100%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Dashboard Content -->
|
|
<div x-show="!loading && !error">
|
|
<!-- Stats Cards Row 1 - Main Metrics -->
|
|
<div class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
|
|
<!-- Card: Total Tests -->
|
|
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-500">
|
|
<span x-html="$icon('beaker', 'w-5 h-5')"></span>
|
|
</div>
|
|
<div>
|
|
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
Total Tests
|
|
</p>
|
|
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.total_tests">
|
|
0
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Card: Passed -->
|
|
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<div class="p-3 mr-4 text-green-500 bg-green-100 rounded-full dark:text-green-100 dark:bg-green-500">
|
|
<span x-html="$icon('check-circle', 'w-5 h-5')"></span>
|
|
</div>
|
|
<div>
|
|
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
Passed
|
|
</p>
|
|
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.passed">
|
|
0
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Card: Failed -->
|
|
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<div class="p-3 mr-4 text-red-500 bg-red-100 rounded-full dark:text-red-100 dark:bg-red-500">
|
|
<span x-html="$icon('x-circle', 'w-5 h-5')"></span>
|
|
</div>
|
|
<div>
|
|
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
Failed
|
|
</p>
|
|
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.failed + stats.errors">
|
|
0
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Card: Pass Rate -->
|
|
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<div class="p-3 mr-4 rounded-full"
|
|
:class="{
|
|
'text-green-500 bg-green-100 dark:text-green-100 dark:bg-green-500': stats.pass_rate >= 90,
|
|
'text-yellow-500 bg-yellow-100 dark:text-yellow-100 dark:bg-yellow-500': stats.pass_rate >= 70 && stats.pass_rate < 90,
|
|
'text-red-500 bg-red-100 dark:text-red-100 dark:bg-red-500': stats.pass_rate < 70
|
|
}">
|
|
<span x-html="$icon('chart-bar', 'w-5 h-5')"></span>
|
|
</div>
|
|
<div>
|
|
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
Pass Rate
|
|
</p>
|
|
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.pass_rate + '%'">
|
|
0%
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats Cards Row 2 - Secondary Metrics -->
|
|
<div class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
|
|
<!-- Skipped -->
|
|
<div class="p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<p class="text-sm font-medium text-gray-600 dark:text-gray-400 mb-1">Skipped</p>
|
|
<p class="text-2xl font-semibold text-yellow-600 dark:text-yellow-400" x-text="stats.skipped">0</p>
|
|
</div>
|
|
|
|
<!-- Duration -->
|
|
<div class="p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<p class="text-sm font-medium text-gray-600 dark:text-gray-400 mb-1">Duration</p>
|
|
<p class="text-2xl font-semibold text-gray-700 dark:text-gray-200" x-text="formatDuration(stats.duration_seconds)">0s</p>
|
|
</div>
|
|
|
|
<!-- Coverage -->
|
|
<div class="p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<p class="text-sm font-medium text-gray-600 dark:text-gray-400 mb-1">Coverage</p>
|
|
<p class="text-2xl font-semibold text-gray-700 dark:text-gray-200" x-text="stats.coverage_percent ? stats.coverage_percent + '%' : 'N/A'">N/A</p>
|
|
</div>
|
|
|
|
<!-- Last Run Status -->
|
|
<div class="p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<p class="text-sm font-medium text-gray-600 dark:text-gray-400 mb-1">Status</p>
|
|
<p class="text-2xl font-semibold"
|
|
:class="{
|
|
'text-green-600 dark:text-green-400': stats.last_run_status === 'passed',
|
|
'text-red-600 dark:text-red-400': stats.last_run_status === 'failed',
|
|
'text-yellow-600 dark:text-yellow-400': stats.last_run_status === 'running',
|
|
'text-gray-600 dark:text-gray-400': !stats.last_run_status
|
|
}"
|
|
x-text="stats.last_run_status ? stats.last_run_status.toUpperCase() : 'NO RUNS'">
|
|
NO RUNS
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Test Collection Stats -->
|
|
<div class="mb-8 p-6 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h4 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
|
|
Test Collection
|
|
</h4>
|
|
<span x-show="stats.last_collected" class="text-xs text-gray-500 dark:text-gray-400">
|
|
Last collected: <span x-text="stats.last_collected ? new Date(stats.last_collected).toLocaleString() : 'Never'"></span>
|
|
</span>
|
|
</div>
|
|
<template x-if="stats.collected_tests > 0">
|
|
<div class="grid gap-4 md:grid-cols-5">
|
|
<!-- Total Collected -->
|
|
<div class="text-center p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
<p class="text-2xl font-bold text-gray-700 dark:text-gray-200" x-text="stats.collected_tests">0</p>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">Total Tests</p>
|
|
</div>
|
|
<!-- Unit Tests -->
|
|
<div class="text-center p-3 bg-blue-50 dark:bg-blue-900/30 rounded-lg">
|
|
<p class="text-2xl font-bold text-blue-600 dark:text-blue-400" x-text="stats.unit_tests">0</p>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">Unit</p>
|
|
</div>
|
|
<!-- Integration Tests -->
|
|
<div class="text-center p-3 bg-purple-50 dark:bg-purple-900/30 rounded-lg">
|
|
<p class="text-2xl font-bold text-purple-600 dark:text-purple-400" x-text="stats.integration_tests">0</p>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">Integration</p>
|
|
</div>
|
|
<!-- Performance Tests -->
|
|
<div class="text-center p-3 bg-orange-50 dark:bg-orange-900/30 rounded-lg">
|
|
<p class="text-2xl font-bold text-orange-600 dark:text-orange-400" x-text="stats.performance_tests">0</p>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">Performance</p>
|
|
</div>
|
|
<!-- Test Files -->
|
|
<div class="text-center p-3 bg-green-50 dark:bg-green-900/30 rounded-lg">
|
|
<p class="text-2xl font-bold text-green-600 dark:text-green-400" x-text="stats.total_test_files">0</p>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">Files</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template x-if="stats.collected_tests === 0">
|
|
<div class="text-center py-4 text-gray-500 dark:text-gray-400">
|
|
<span x-html="$icon('collection', 'w-8 h-8 mx-auto mb-2')"></span>
|
|
<p class="text-sm">No collection data. Click "Collect Tests" to discover available tests.</p>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Trend Chart and Tests by Category -->
|
|
<div class="grid gap-6 mb-8 md:grid-cols-2">
|
|
<!-- Trend Chart -->
|
|
<div class="p-6 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<h4 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
|
Pass Rate Trend (Last 10 Runs)
|
|
</h4>
|
|
<div class="h-64 flex items-center justify-center text-gray-500 dark:text-gray-400">
|
|
<template x-if="stats.trend && stats.trend.length > 0">
|
|
<div class="w-full">
|
|
<template x-for="(run, idx) in stats.trend" :key="idx">
|
|
<div class="mb-2">
|
|
<div class="flex justify-between text-sm mb-1">
|
|
<span x-text="new Date(run.timestamp).toLocaleDateString()"></span>
|
|
<span>
|
|
<span x-text="run.passed"></span>/<span x-text="run.total"></span>
|
|
(<span x-text="run.pass_rate"></span>%)
|
|
</span>
|
|
</div>
|
|
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
|
<div class="h-2 rounded-full transition-all duration-300"
|
|
:class="{
|
|
'bg-green-500': run.pass_rate >= 90,
|
|
'bg-yellow-500': run.pass_rate >= 70 && run.pass_rate < 90,
|
|
'bg-red-500': run.pass_rate < 70
|
|
}"
|
|
:style="'width: ' + run.pass_rate + '%'">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
<template x-if="!stats.trend || stats.trend.length === 0">
|
|
<div class="text-center">
|
|
<span x-html="$icon('beaker', 'w-12 h-12 mx-auto mb-2 text-gray-400')"></span>
|
|
<p>No test runs yet</p>
|
|
<p class="text-sm">Run tests to see trend data</p>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tests by Category -->
|
|
<div class="p-6 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<h4 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
|
Tests by Category
|
|
</h4>
|
|
<div class="space-y-3">
|
|
<template x-if="stats.by_category && Object.keys(stats.by_category).length > 0">
|
|
<template x-for="[category, data] in Object.entries(stats.by_category)" :key="category">
|
|
<div>
|
|
<div class="flex justify-between text-sm mb-1">
|
|
<span class="text-gray-700 dark:text-gray-300" x-text="category"></span>
|
|
<span class="font-semibold">
|
|
<span class="text-green-600" x-text="data.passed"></span>
|
|
/
|
|
<span x-text="data.total"></span>
|
|
</span>
|
|
</div>
|
|
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
|
<div class="bg-green-500 h-2 rounded-full"
|
|
:style="'width: ' + (data.total > 0 ? (data.passed / data.total * 100) : 0) + '%'">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
<template x-if="!stats.by_category || Object.keys(stats.by_category).length === 0">
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">No category data available</p>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Top Failing Tests -->
|
|
<div class="mb-8">
|
|
<div class="p-6 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<h4 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
|
Top Failing Tests
|
|
</h4>
|
|
<template x-if="stats.top_failing && stats.top_failing.length > 0">
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full whitespace-nowrap">
|
|
<thead>
|
|
<tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
|
|
<th class="px-4 py-3">Test Name</th>
|
|
<th class="px-4 py-3">File</th>
|
|
<th class="px-4 py-3 text-right">Failures</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
|
<template x-for="test in stats.top_failing" :key="test.test_name + test.test_file">
|
|
<tr class="text-gray-700 dark:text-gray-400">
|
|
<td class="px-4 py-3">
|
|
<span class="font-semibold" x-text="test.test_name"></span>
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400 truncate max-w-xs" x-text="test.test_file"></td>
|
|
<td class="px-4 py-3 text-right">
|
|
<span class="px-2 py-1 text-xs font-semibold text-red-700 bg-red-100 rounded-full dark:bg-red-700 dark:text-red-100"
|
|
x-text="test.failure_count">
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</template>
|
|
<template x-if="!stats.top_failing || stats.top_failing.length === 0">
|
|
<div class="text-center py-8 text-gray-500 dark:text-gray-400">
|
|
<span x-html="$icon('check-circle', 'w-12 h-12 mx-auto mb-2 text-green-500')"></span>
|
|
<p class="font-medium">No failing tests!</p>
|
|
<p class="text-sm">All tests are passing</p>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Actions -->
|
|
<div class="mb-8">
|
|
<div class="p-6 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<h4 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
|
Quick Actions
|
|
</h4>
|
|
<div class="flex flex-wrap gap-3">
|
|
<button @click="runTests('tests/unit')"
|
|
:disabled="running"
|
|
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed">
|
|
<span x-html="$icon('beaker', 'w-4 h-4 mr-2')"></span>
|
|
Run Unit Tests
|
|
</button>
|
|
<button @click="runTests('tests/integration')"
|
|
:disabled="running"
|
|
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-blue-600 border border-transparent rounded-lg hover:bg-blue-700 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed">
|
|
<span x-html="$icon('server', 'w-4 h-4 mr-2')"></span>
|
|
Run Integration Tests
|
|
</button>
|
|
<button @click="collectTests()"
|
|
:disabled="collecting"
|
|
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 dark:text-gray-300 transition-colors duration-150 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed">
|
|
<span x-html="$icon('collection', 'w-4 h-4 mr-2')"></span>
|
|
<span x-text="collecting ? 'Collecting...' : 'Collect Tests'"></span>
|
|
</button>
|
|
<a href="/admin/testing-hub"
|
|
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 dark:text-gray-300 transition-colors duration-150 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none">
|
|
<span x-html="$icon('clipboard-list', 'w-4 h-4 mr-2')"></span>
|
|
Manual Testing
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Runs -->
|
|
<div class="mb-8" x-show="runs.length > 0">
|
|
<div class="p-6 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<h4 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
|
Recent Test Runs
|
|
</h4>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full whitespace-nowrap">
|
|
<thead>
|
|
<tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
|
|
<th class="px-4 py-3">Time</th>
|
|
<th class="px-4 py-3">Path</th>
|
|
<th class="px-4 py-3 text-center">Total</th>
|
|
<th class="px-4 py-3 text-center">Passed</th>
|
|
<th class="px-4 py-3 text-center">Failed</th>
|
|
<th class="px-4 py-3 text-center">Pass Rate</th>
|
|
<th class="px-4 py-3">Duration</th>
|
|
<th class="px-4 py-3">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
|
<template x-for="run in runs" :key="run.id">
|
|
<tr class="text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700">
|
|
<td class="px-4 py-3 text-sm" x-text="new Date(run.timestamp).toLocaleString()"></td>
|
|
<td class="px-4 py-3 text-sm" x-text="run.test_path || 'tests'"></td>
|
|
<td class="px-4 py-3 text-center" x-text="run.total_tests"></td>
|
|
<td class="px-4 py-3 text-center text-green-600" x-text="run.passed"></td>
|
|
<td class="px-4 py-3 text-center text-red-600" x-text="run.failed + run.errors"></td>
|
|
<td class="px-4 py-3 text-center">
|
|
<span class="px-2 py-1 text-xs font-semibold rounded-full"
|
|
:class="{
|
|
'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100': run.pass_rate >= 90,
|
|
'text-yellow-700 bg-yellow-100 dark:bg-yellow-700 dark:text-yellow-100': run.pass_rate >= 70 && run.pass_rate < 90,
|
|
'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100': run.pass_rate < 70
|
|
}"
|
|
x-text="run.pass_rate.toFixed(1) + '%'">
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-sm" x-text="formatDuration(run.duration_seconds)"></td>
|
|
<td class="px-4 py-3">
|
|
<span class="px-2 py-1 text-xs font-semibold rounded-full"
|
|
:class="{
|
|
'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100': run.status === 'passed',
|
|
'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100': run.status === 'failed',
|
|
'text-yellow-700 bg-yellow-100 dark:bg-yellow-700 dark:text-yellow-100': run.status === 'running',
|
|
'text-gray-700 bg-gray-100 dark:bg-gray-700 dark:text-gray-100': run.status === 'error'
|
|
}"
|
|
x-text="run.status">
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Last Run Info -->
|
|
<div x-show="stats.last_run" class="text-sm text-gray-600 dark:text-gray-400 text-center">
|
|
Last run: <span x-text="stats.last_run ? new Date(stats.last_run).toLocaleString() : 'Never'"></span>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|