'use strict'; // Tab switching document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', () => { const tab = btn.dataset.tab; document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); btn.classList.add('active'); document.getElementById(`tab-${tab}`).classList.add('active'); if (tab === 'history') loadHistory(); }); }); // Sync range <-> number inputs function syncRangeNumber(rangeId, numId) { const range = document.getElementById(rangeId); const num = document.getElementById(numId); range.addEventListener('input', () => num.value = range.value); num.addEventListener('input', () => range.value = num.value); } syncRangeNumber('vus', 'vus-num'); syncRangeNumber('duration', 'duration-num'); // Show/hide request body for non-GET methods const methodSel = document.getElementById('httpMethod'); const bodyGroup = document.getElementById('body-group'); methodSel.addEventListener('change', () => { const m = methodSel.value; bodyGroup.style.display = (m !== 'GET' && m !== 'HEAD') ? '' : 'none'; }); // ---- Run Test ---- const form = document.getElementById('test-form'); const startBtn = document.getElementById('start-btn'); const resultPanel = document.getElementById('result-panel'); const outputLog = document.getElementById('output-log'); const resultStatus= document.getElementById('result-status'); const summaryPanel= document.getElementById('summary-panel'); const summaryCards= document.getElementById('summary-cards'); form.addEventListener('submit', async (e) => { e.preventDefault(); const url = document.getElementById('url').value.trim(); const vus = parseInt(document.getElementById('vus-num').value, 10); const duration = parseInt(document.getElementById('duration-num').value, 10); const rpsLimit = parseInt(document.getElementById('rpsLimit').value, 10) || 0; const httpMethod = document.getElementById('httpMethod').value; const requestBody = document.getElementById('requestBody').value.trim(); const headers = document.getElementById('headers').value.trim() || '{}'; // Validate headers JSON try { JSON.parse(headers); } catch { alert('Custom Headers must be valid JSON (or leave empty).'); return; } startBtn.disabled = true; startBtn.textContent = '⏳ Running…'; outputLog.textContent = ''; summaryPanel.style.display = 'none'; resultPanel.style.display = ''; resultStatus.textContent = 'running'; resultStatus.className = 'badge running'; let res; try { res = await fetch('/api/tests', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url, vus, duration, rpsLimit, httpMethod, requestBody, headers }), }); } catch (err) { alert('Failed to start test: ' + err.message); resetBtn(); return; } if (!res.ok) { const err = await res.json().catch(() => ({ error: res.statusText })); alert('Error: ' + err.error); resetBtn(); return; } const { id } = await res.json(); streamTest(id); }); function streamTest(id) { const es = new EventSource(`/api/tests/${id}/stream`); es.onmessage = (e) => { const data = JSON.parse(e.data); if (data.chunk) { outputLog.textContent += data.chunk; outputLog.scrollTop = outputLog.scrollHeight; } if (data.done) { es.close(); resultStatus.textContent = data.status; resultStatus.className = `badge ${data.status}`; if (data.summary) renderSummary(data.summary); resetBtn(); } if (data.error) { es.close(); outputLog.textContent += '\n[error] ' + data.error; resetBtn(); } }; es.onerror = () => { es.close(); resetBtn(); }; } function resetBtn() { startBtn.disabled = false; startBtn.textContent = '▶ Run Test'; } function renderSummary(summary) { if (!summary || Object.keys(summary).length === 0) return; summaryPanel.style.display = ''; summaryCards.innerHTML = ''; const cards = []; if (summary.http_reqs) { cards.push({ label: 'Total Requests', value: summary.http_reqs[0], cls: 'info' }); cards.push({ label: 'Req/s', value: summary.http_reqs[1], cls: 'info' }); } if (summary.http_req_duration) { cards.push({ label: 'Avg Duration', value: summary.http_req_duration[0], cls: 'info' }); cards.push({ label: 'p(95) Duration', value: summary.http_req_duration[5], cls: 'info' }); cards.push({ label: 'Max Duration', value: summary.http_req_duration[3], cls: 'info' }); } if (summary.http_req_failed) { const pct = parseFloat(summary.http_req_failed[0]); cards.push({ label: 'Failure Rate', value: summary.http_req_failed[0], cls: pct > 5 ? 'bad' : 'good' }); } if (summary.iterations) { cards.push({ label: 'Iterations', value: summary.iterations[0], cls: 'info' }); } for (const c of cards) { const div = document.createElement('div'); div.className = 'summary-card'; div.innerHTML = `
Failed to load history.
'; return; } if (!tests.length) { container.innerHTML = 'No tests yet.
'; return; } container.innerHTML = ''; for (const t of tests) { const item = document.createElement('div'); item.className = 'history-item'; item.dataset.id = t.id; const date = new Date(t.created_at).toLocaleString(); const dur = t.finished_at ? `${Math.round((t.finished_at - t.created_at) / 1000)}s` : `${t.duration}s`; let summaryLine = ''; if (t.summary) { const s = t.summary; const parts = []; if (s.http_reqs) parts.push(`${s.http_reqs[0]} reqs @ ${s.http_reqs[1]}`); if (s.http_req_duration) parts.push(`avg ${s.http_req_duration[0]}`); if (s.http_req_failed) parts.push(`${s.http_req_failed[0]} failed`); summaryLine = parts.join(' · '); } item.innerHTML = `${escHtml(test.output || '(no output)')}
`;
// Add style to table cells inline
detail.querySelectorAll('td').forEach((td, i) => {
td.style.padding = '.3rem .5rem';
td.style.borderBottom = '1px solid var(--border)';
if (i % 2 === 0) td.style.color = 'var(--muted)';
});
itemEl.insertAdjacentElement('afterend', detail);
}
document.getElementById('refresh-history').addEventListener('click', loadHistory);
function escHtml(s) {
return String(s)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
}