* Removed the old SpeedTestApiRoutes and RandomData classes to streamline the architecture. * Introduced SpeedTestController to handle API requests and integrate with the new service layer. * Added RandomDataGenerator for generating random data, improving separation of concerns. * Created interfaces for speed test operations, enhancing type safety and maintainability. * Updated InMemorySpeedTestRepository to manage sessions with improved timeout handling.
365 lines
12 KiB
HTML
365 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Speed Test</title>
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: #f5f5f5;
|
|
}
|
|
|
|
.container {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 30px;
|
|
box-shadow: 0 2px 20px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
h1 {
|
|
text-align: center;
|
|
color: #333;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.controls {
|
|
display: flex;
|
|
gap: 15px;
|
|
margin-bottom: 30px;
|
|
align-items: center;
|
|
}
|
|
|
|
.size-selector {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
select, button {
|
|
padding: 10px 15px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 6px;
|
|
font-size: 16px;
|
|
}
|
|
|
|
button {
|
|
background: #007bff;
|
|
color: white;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
button:hover:not(:disabled) {
|
|
background: #0056b3;
|
|
}
|
|
|
|
button:disabled {
|
|
background: #ccc;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.progress {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.progress-bar {
|
|
width: 100%;
|
|
height: 20px;
|
|
background: #e9ecef;
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #28a745, #20c997);
|
|
width: 0%;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.progress-text {
|
|
text-align: center;
|
|
font-weight: bold;
|
|
color: #666;
|
|
}
|
|
|
|
.results {
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.result-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: 10px 0;
|
|
border-bottom: 1px solid #dee2e6;
|
|
}
|
|
|
|
.result-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.result-label {
|
|
font-weight: 500;
|
|
}
|
|
|
|
.result-value {
|
|
font-weight: bold;
|
|
color: #007bff;
|
|
}
|
|
|
|
.status {
|
|
text-align: center;
|
|
padding: 10px;
|
|
margin: 10px 0;
|
|
border-radius: 6px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.status.idle {
|
|
background: #e9ecef;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.status.downloading {
|
|
background: #d1ecf1;
|
|
color: #0c5460;
|
|
}
|
|
|
|
.status.uploading {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
}
|
|
|
|
.status.completed {
|
|
background: #d1ecf1;
|
|
color: #0c5460;
|
|
}
|
|
|
|
.status.error {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
}
|
|
|
|
.hidden {
|
|
display: none;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>🌐 Network Speed Test</h1>
|
|
|
|
<div class="controls">
|
|
<div class="size-selector">
|
|
<label for="testSize">Test Size:</label>
|
|
<select id="testSize">
|
|
<option value="1048576">1 MB</option>
|
|
<option value="5242880">5 MB</option>
|
|
<option value="10485760" selected>10 MB</option>
|
|
<option value="26214400">25 MB</option>
|
|
<option value="52428800">50 MB</option>
|
|
<option value="104857600">100 MB</option>
|
|
<option value="262144000">250 MB</option>
|
|
<option value="524288000">500 MB</option>
|
|
<option value="1048576000">1 GB</option>
|
|
</select>
|
|
</div>
|
|
<button id="startTest" onclick="startSpeedTest()">Start Speed Test</button>
|
|
</div>
|
|
|
|
<div class="status idle" id="status">Ready to start speed test</div>
|
|
|
|
<div class="progress hidden" id="progressContainer">
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" id="progressFill"></div>
|
|
</div>
|
|
<div class="progress-text" id="progressText">0%</div>
|
|
</div>
|
|
|
|
<div class="results hidden" id="results">
|
|
<h3>Test Results</h3>
|
|
<div class="result-item">
|
|
<span class="result-label">Download Speed:</span>
|
|
<span class="result-value" id="downloadSpeed">-</span>
|
|
</div>
|
|
<div class="result-item">
|
|
<span class="result-label">Upload Speed:</span>
|
|
<span class="result-value" id="uploadSpeed">-</span>
|
|
</div>
|
|
<div class="result-item">
|
|
<span class="result-label">Total Time:</span>
|
|
<span class="result-value" id="totalTime">-</span>
|
|
</div>
|
|
<div class="result-item">
|
|
<span class="result-label">Data Size:</span>
|
|
<span class="result-value" id="dataSize">-</span>
|
|
</div>
|
|
<div class="result-item">
|
|
<span class="result-label">Data Integrity:</span>
|
|
<span class="result-value" id="dataIntegrity">-</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let downloadedData = null;
|
|
let requestId = null;
|
|
const baseUrl = window.location.origin; // Same server as frontend
|
|
|
|
async function startSpeedTest() {
|
|
const testSize = document.getElementById('testSize').value;
|
|
const startButton = document.getElementById('startTest');
|
|
|
|
// Reset UI
|
|
updateStatus('idle', 'Starting speed test...');
|
|
document.getElementById('progressContainer').classList.add('hidden');
|
|
document.getElementById('results').classList.add('hidden');
|
|
startButton.disabled = true;
|
|
startButton.textContent = 'Testing...';
|
|
|
|
try {
|
|
// Phase 1: Download
|
|
await performDownload(testSize);
|
|
|
|
// Phase 2: Upload
|
|
await performUpload();
|
|
|
|
// Show results
|
|
document.getElementById('results').classList.remove('hidden');
|
|
updateStatus('completed', 'Speed test completed successfully!');
|
|
|
|
} catch (error) {
|
|
console.error('Speed test failed:', error);
|
|
updateStatus('error', `Speed test failed: ${error.message}`);
|
|
} finally {
|
|
startButton.disabled = false;
|
|
startButton.textContent = 'Start Speed Test';
|
|
}
|
|
}
|
|
|
|
async function performDownload(size) {
|
|
updateStatus('downloading', 'Downloading test data...');
|
|
updateProgress(0);
|
|
|
|
document.getElementById('progressContainer').classList.remove('hidden');
|
|
|
|
const startTime = Date.now();
|
|
const response = await fetch(`${baseUrl}/data?size=${size}`);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Download failed: ${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
// Get request ID from headers
|
|
requestId = response.headers.get('X-Request-ID');
|
|
if (!requestId) {
|
|
throw new Error('No request ID received from server');
|
|
}
|
|
|
|
// Read the response as array buffer
|
|
const contentLength = parseInt(response.headers.get('Content-Length') || '0');
|
|
const reader = response.body.getReader();
|
|
const chunks = [];
|
|
let receivedLength = 0;
|
|
|
|
while (true) {
|
|
const {done, value} = await reader.read();
|
|
if (done) break;
|
|
|
|
chunks.push(value);
|
|
receivedLength += value.length;
|
|
|
|
// Update progress
|
|
const progress = (receivedLength / contentLength) * 100;
|
|
updateProgress(progress);
|
|
}
|
|
|
|
// Combine chunks
|
|
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
|
const combined = new Uint8Array(totalLength);
|
|
let offset = 0;
|
|
for (const chunk of chunks) {
|
|
combined.set(chunk, offset);
|
|
offset += chunk.length;
|
|
}
|
|
|
|
downloadedData = combined;
|
|
const endTime = Date.now();
|
|
const downloadTime = (endTime - startTime) / 1000; // seconds
|
|
const downloadSpeedMBps = (totalLength / 1024 / 1024) / downloadTime;
|
|
const downloadSpeedMbps = downloadSpeedMBps * 8;
|
|
|
|
document.getElementById('downloadSpeed').textContent = `${downloadSpeedMbps.toFixed(2)} Mbps`;
|
|
document.getElementById('dataSize').textContent = `${(totalLength / 1024 / 1024).toFixed(2)} MB`;
|
|
|
|
console.log(`Download completed: ${downloadSpeedMbps.toFixed(2)} Mbps`);
|
|
}
|
|
|
|
async function performUpload() {
|
|
updateStatus('uploading', 'Uploading test data...');
|
|
updateProgress(0);
|
|
|
|
const startTime = Date.now();
|
|
|
|
const response = await fetch(`${baseUrl}/data?requestId=${requestId}`, {
|
|
method: 'POST',
|
|
body: downloadedData,
|
|
headers: {
|
|
'Content-Type': 'application/octet-stream'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
const endTime = Date.now();
|
|
const uploadTime = (endTime - startTime) / 1000; // seconds
|
|
const uploadSpeedMBps = (downloadedData.length / 1024 / 1024) / uploadTime;
|
|
const uploadSpeedMbps = uploadSpeedMBps * 8;
|
|
|
|
// Update results
|
|
document.getElementById('uploadSpeed').textContent = `${uploadSpeedMbps.toFixed(2)} Mbps`;
|
|
document.getElementById('totalTime').textContent = `${((endTime - startTime) / 1000).toFixed(2)} seconds`;
|
|
document.getElementById('dataIntegrity').textContent = result.valid ? '✅ Valid' : '❌ Invalid';
|
|
|
|
updateProgress(100);
|
|
|
|
console.log(`Upload completed: ${uploadSpeedMbps.toFixed(2)} Mbps`);
|
|
console.log('Validation result:', result);
|
|
}
|
|
|
|
function updateStatus(statusClass, message) {
|
|
const statusEl = document.getElementById('status');
|
|
statusEl.className = `status ${statusClass}`;
|
|
statusEl.textContent = message;
|
|
}
|
|
|
|
function updateProgress(percent) {
|
|
const progressFill = document.getElementById('progressFill');
|
|
const progressText = document.getElementById('progressText');
|
|
|
|
progressFill.style.width = `${percent}%`;
|
|
progressText.textContent = `${percent.toFixed(1)}%`;
|
|
}
|
|
|
|
// Server URL (same origin since frontend is served from same server)
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
console.log('Speed test frontend loaded. Server URL:', baseUrl);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|