diff --git a/index.html b/index.html
index d4cb768..b133958 100644
--- a/index.html
+++ b/index.html
@@ -38,11 +38,65 @@
+
+
+
+
+
+
+
+
+
+
-
This is the base template. Add your project content here.
-
diff --git a/lib/runner.js b/lib/runner.js
index 0034609..e3500ab 100644
--- a/lib/runner.js
+++ b/lib/runner.js
@@ -20,7 +20,11 @@ async function runTest(url, options = {}) {
}
// Launch Chrome
- const chrome = await chromeLauncher.launch({ chromeFlags: ['--headless'] });
+ const chromePath = require('puppeteer').executablePath();
+ const chrome = await chromeLauncher.launch({
+ chromePath,
+ chromeFlags: ['--headless', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage']
+ });
// Lighthouse Config
const port = chrome.port;
diff --git a/script.js b/script.js
index 81eaaa1..bb001a2 100644
--- a/script.js
+++ b/script.js
@@ -1,5 +1,179 @@
// ============================================================================
-// Git Version Badge - Auto-update from server
+// State & Config
+// ============================================================================
+let currentDevice = 'mobile';
+
+// ============================================================================
+// UI Functions
+// ============================================================================
+
+function setDevice(device) {
+ currentDevice = device;
+ document.querySelectorAll('.toggle-option').forEach(el => {
+ el.classList.toggle('active', el.dataset.value === device);
+ });
+}
+
+function setLoading(isLoading) {
+ const btn = document.getElementById('run-btn');
+ const spinner = document.getElementById('loading-spinner');
+ const btnText = btn.querySelector('span');
+
+ if (isLoading) {
+ btn.disabled = true;
+ spinner.style.display = 'block';
+ btnText.textContent = 'Running Test...';
+ } else {
+ btn.disabled = false;
+ spinner.style.display = 'none';
+ btnText.textContent = 'Run Test';
+ }
+}
+
+// ============================================================================
+// API Handlers
+// ============================================================================
+
+async function runTest() {
+ const urlInput = document.getElementById('test-url');
+ const url = urlInput.value.trim();
+ const errorMsg = document.getElementById('error-msg');
+ const resultsArea = document.getElementById('results-area');
+
+ if (!url) {
+ showError('Please enter a valid URL');
+ return;
+ }
+
+ try {
+ new URL(url);
+ } catch {
+ showError('Invalid URL format (include http:// or https://)');
+ return;
+ }
+
+ // Reset UI
+ errorMsg.style.display = 'none';
+ resultsArea.classList.remove('visible');
+ setLoading(true);
+
+ try {
+ const response = await fetch('/api/run-test', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ url: url,
+ isMobile: currentDevice === 'mobile'
+ })
+ });
+
+ if (!response.ok) throw new Error('Test failed to start');
+
+ const data = await response.json();
+ displayResults(data);
+ loadHistory(); // Refresh history
+
+ } catch (error) {
+ console.error(error);
+ showError('Test execution failed. Check console for details.');
+ } finally {
+ setLoading(false);
+ }
+}
+
+function displayResults(data) {
+ const resultsArea = document.getElementById('results-area');
+
+ // Update Metrics
+ updateMetric('score-perf', Math.round(data.scores.performance), true);
+ updateMetric('metric-lcp', Math.round(data.metrics.lcp));
+ updateMetric('metric-cls', data.metrics.cls.toFixed(3));
+ updateMetric('metric-tbt', Math.round(data.metrics.tbt));
+
+ // Remove existing actions if any
+ const existingActions = resultsArea.querySelector('.report-actions');
+ if (existingActions) existingActions.remove();
+
+ // Add Report Button
+ const actionsDiv = document.createElement('div');
+ actionsDiv.className = 'report-actions';
+ actionsDiv.innerHTML = `
+
+ 📄 View Full Report
+
+ `;
+ resultsArea.appendChild(actionsDiv);
+
+ resultsArea.classList.add('visible');
+
+ // Scroll to results
+ resultsArea.scrollIntoView({ behavior: 'smooth' });
+}
+
+function updateMetric(id, value, isScore = false) {
+ const el = document.getElementById(id);
+ el.textContent = value;
+
+ if (isScore) {
+ el.className = 'metric-value'; // Reset
+ if (value >= 90) el.classList.add('score-good');
+ else if (value >= 50) el.classList.add('score-average');
+ else el.classList.add('score-poor');
+ }
+}
+
+function showError(msg) {
+ const el = document.getElementById('error-msg');
+ el.textContent = msg;
+ el.style.display = 'block';
+}
+
+async function loadHistory() {
+ try {
+ const response = await fetch('/api/history');
+ const history = await response.json();
+
+ const container = document.getElementById('history-list');
+ container.innerHTML = '
Recent Tests
';
+
+ if (history.length === 0) {
+ container.innerHTML += 'No tests run yet.
';
+ return;
+ }
+
+ history.slice(0, 10).forEach(test => {
+ const date = new Date(test.timestamp).toLocaleString();
+ const perfScore = Math.round(test.scores.performance);
+ const colorClass = perfScore >= 90 ? 'score-good' : (perfScore >= 50 ? 'score-average' : 'score-poor');
+
+ const html = `
+
+
+
${test.url}
+
+ ${date} • ${test.isMobile ? '📱 Mobile' : '💻 Desktop'}
+
+
+
+
+ `;
+ container.innerHTML += html;
+ });
+
+ } catch (error) {
+ console.error('Failed to load history', error);
+ }
+}
+
+// ============================================================================
+// Git Version Badge
// ============================================================================
async function updateVersionBadge() {
@@ -16,7 +190,6 @@ async function updateVersionBadge() {
const commitAgeEl = document.getElementById('commit-age');
if (data.error || !data.commitId) {
- // Fallback - try to get from git locally or show placeholder
commitIdEl.textContent = 'local';
commitAgeEl.textContent = 'dev mode';
commitIdEl.style.color = 'var(--color-text-muted)';
@@ -35,8 +208,11 @@ async function updateVersionBadge() {
}
}
-// Update version badge on page load
-document.addEventListener('DOMContentLoaded', updateVersionBadge);
-
-// Optional: Auto-refresh every 5 minutes to show latest version
-setInterval(updateVersionBadge, 5 * 60 * 1000);
+// Initialization
+document.addEventListener('DOMContentLoaded', () => {
+ updateVersionBadge();
+ loadHistory();
+
+ // Auto-refresh Git badge
+ setInterval(updateVersionBadge, 5 * 60 * 1000);
+});
diff --git a/styles.css b/styles.css
index 133190d..1e9f529 100644
--- a/styles.css
+++ b/styles.css
@@ -384,3 +384,217 @@ body::before {
font-size: 2rem;
}
}
+/* ===================================
+ TEST LAUNCHER COMPONENT
+ =================================== */
+.launcher-form {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-lg);
+}
+
+.form-group {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-xs);
+}
+
+.form-label {
+ font-size: 0.875rem;
+ font-weight: 500;
+ color: var(--color-text-secondary);
+}
+
+.form-input {
+ background: rgba(10, 14, 26, 0.6);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ padding: var(--spacing-md);
+ color: var(--color-text-primary);
+ font-family: var(--font-primary);
+ font-size: 1rem;
+ transition: all var(--transition-fast);
+}
+
+.form-input:focus {
+ outline: none;
+ border-color: var(--color-accent-primary);
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
+}
+
+.form-input::placeholder {
+ color: var(--color-text-muted);
+}
+
+.form-row {
+ display: flex;
+ gap: var(--spacing-lg);
+ flex-wrap: wrap;
+}
+
+.toggle-group {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-md);
+ background: rgba(10, 14, 26, 0.4);
+ padding: var(--spacing-xs);
+ border-radius: var(--radius-lg);
+ border: 1px solid var(--color-border);
+}
+
+.toggle-option {
+ padding: var(--spacing-xs) var(--spacing-md);
+ border-radius: var(--radius-md);
+ cursor: pointer;
+ font-size: 0.875rem;
+ font-weight: 500;
+ color: var(--color-text-tertiary);
+ transition: all var(--transition-fast);
+}
+
+.toggle-option.active {
+ background: var(--color-bg-tertiary);
+ color: var(--color-text-primary);
+ box-shadow: var(--shadow-sm);
+}
+
+.btn-primary {
+ background: var(--gradient-primary);
+ color: white;
+ border: none;
+ padding: var(--spacing-md) var(--spacing-xl);
+ border-radius: var(--radius-md);
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all var(--transition-base);
+ box-shadow: var(--shadow-glow);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--spacing-sm);
+}
+
+.btn-primary:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 20px rgba(99, 102, 241, 0.6);
+}
+
+.btn-primary:disabled {
+ opacity: 0.7;
+ cursor: not-allowed;
+ transform: none;
+}
+
+/* Loading State */
+.loading-spinner {
+ width: 1.5rem;
+ height: 1.5rem;
+ border: 3px solid rgba(255, 255, 255, 0.3);
+ border-top-color: white;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+/* Results Section */
+.results-container {
+ margin-top: var(--spacing-2xl);
+ display: none; /* Hidden by default */
+}
+
+.results-container.visible {
+ display: block;
+ animation: fadeInDown 0.6s ease-out;
+}
+
+.metrics-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: var(--spacing-lg);
+ margin-top: var(--spacing-lg);
+}
+
+.metric-card {
+ background: rgba(255, 255, 255, 0.05);
+ padding: var(--spacing-lg);
+ border-radius: var(--radius-lg);
+ border: 1px solid var(--color-border);
+ text-align: center;
+}
+
+.metric-value {
+ font-size: 2.5rem;
+ font-weight: 800;
+ margin-bottom: var(--spacing-xs);
+ background: var(--gradient-primary);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
+
+.metric-label {
+ color: var(--color-text-secondary);
+ font-size: 0.875rem;
+}
+
+.score-good { color: var(--color-accent-success); -webkit-text-fill-color: var(--color-accent-success); }
+.score-average { color: var(--color-accent-warning); -webkit-text-fill-color: var(--color-accent-warning); }
+.score-poor { color: var(--color-accent-danger); -webkit-text-fill-color: var(--color-accent-danger); }
+
+/* History List */
+.history-list {
+ margin-top: var(--spacing-xl);
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-sm);
+}
+
+.history-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: var(--spacing-md);
+ background: rgba(255, 255, 255, 0.03);
+ border-radius: var(--radius-md);
+ border: 1px solid transparent;
+ transition: all var(--transition-fast);
+}
+
+.history-item:hover {
+ background: rgba(255, 255, 255, 0.05);
+ border-color: var(--color-border);
+}
+
+.btn-secondary {
+ background: rgba(255, 255, 255, 0.1);
+ color: var(--color-text-primary);
+ border: 1px solid var(--color-border);
+ padding: var(--spacing-sm) var(--spacing-lg);
+ border-radius: var(--radius-md);
+ font-size: 0.875rem;
+ font-weight: 500;
+ cursor: pointer;
+ text-decoration: none;
+ transition: all var(--transition-base);
+ display: inline-flex;
+ align-items: center;
+ gap: var(--spacing-sm);
+ margin-left: var(--spacing-md);
+}
+
+.btn-secondary:hover {
+ background: rgba(255, 255, 255, 0.2);
+ border-color: var(--color-text-secondary);
+ color: white;
+}
+
+.report-actions {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-top: var(--spacing-lg);
+ gap: var(--spacing-md);
+}