Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

778 lines
30 KiB
JavaScript

/**
* UI Test Suite Runner
*
* Runs all UI tests in sequence and provides a summary report.
* This script executes each test individually and tracks pass/fail status.
*
* Prerequisites: Web server running on http://127.0.0.1:5000
*
* Usage:
* node tests/ui_tests/run_all_tests.js # run all tests
* node tests/ui_tests/run_all_tests.js --shard=auth-login # run one shard
*
* Valid shards: auth-login, auth-register, auth-pages, research-workflow,
* research-form, research-metrics, settings-core, settings-pages,
* library, history-news, mobile, api-crud, error-benchmark, accessibility,
* chat-core, chat-lifecycle, link-analytics
*/
// Keep in sync with `strategy.matrix.shard` in .github/workflows/docker-tests.yml.
// A mismatch would cause silent test misrouting.
//
// Shard design (17 shards, ~4 tests each):
// Each shard runs in its own Docker container with a dedicated server.
// Keeping shards small prevents cascade failures when one test stresses
// the server (e.g., encrypted DB creation in auth-register).
const VALID_SHARDS = [
'auth-login', // login/auth flow tests
'auth-register', // registration (isolated — heavy SQLCipher DB creation)
'auth-pages', // page browsing, navigation, comprehensive auth
'research-workflow', // core research lifecycle
'research-form', // research form interactions + results
'research-metrics', // metrics charts, dashboard, progress
'settings-core', // settings page, errors, save, interactions
'settings-pages', // settings tabs, star reviews
'library', // collections, documents
'history-news', // history page, news subscriptions
'mobile', // mobile navigation, interactions, UI functionality
'api-crud', // API endpoints, CRUD operations, rate limiting
'error-benchmark', // error handling/recovery, benchmark, context overflow
'accessibility', // keyboard navigation & ARIA
'chat-core', // chat-mode v2 input + a11y + chips + nav
'chat-lifecycle', // chat-mode v2 session lifecycle + export + persistence
'link-analytics', // /metrics/links full-page render + XSS runtime check
];
const { spawn } = require('child_process');
const http = require('http');
const path = require('path');
/** Format a Date as HH:MM:SS for log timestamps */
function ts(date = new Date()) {
return date.toISOString().slice(11, 19);
}
/**
* Wait for the server to be responsive before starting the next test.
* Prevents cascade failures when a previous test stressed the server
* (e.g., registration creating encrypted databases).
*/
async function waitForServer(maxWaitMs = 60000) {
const startTime = Date.now();
let delay = 1000;
let wasDown = false;
while (Date.now() - startTime < maxWaitMs) {
try {
const ok = await new Promise((resolve) => {
const req = http.get('http://127.0.0.1:5000/api/v1/health', { timeout: 5000 }, (res) => {
resolve(res.statusCode >= 200 && res.statusCode < 400);
res.resume();
});
req.on('error', () => resolve(false));
req.on('timeout', () => { req.destroy(); resolve(false); });
});
if (ok) {
if (wasDown) console.log('Server recovered after being unresponsive');
return true;
}
wasDown = true;
} catch {
wasDown = true;
}
// Capture into const so the closure isn't flagged as no-loop-func.
const sleepFor = delay;
await new Promise(r => setTimeout(r, sleepFor));
delay = Math.min(delay * 2, 8000);
}
console.log(`Server did not respond within ${maxWaitMs/1000}s — subsequent tests may fail`);
return false;
}
const tests = [
// =====================================================================
// Shard: auth-login (2 tests)
// =====================================================================
{
name: 'Authentication Flow Test',
file: 'test_auth_flow.js',
shard: 'auth-login',
description: 'Tests registration, login, and logout functionality'
},
{
name: 'Login Validation Test',
file: 'test_login_validation.js',
shard: 'auth-login',
description: 'Tests login form validation'
},
// =====================================================================
// Shard: auth-register (2 tests)
// Register Full Flow is isolated because it creates an encrypted
// SQLCipher database (CPU-intensive key derivation + 58 tables +
// 500+ settings) which can block the server for 2+ minutes.
// =====================================================================
{
name: 'Register Validation Test',
file: 'test_register_validation.js',
shard: 'auth-register',
description: 'Tests registration form validation without auth'
},
{
name: 'Register Full Flow Test',
file: 'test_register_full_flow.js',
shard: 'auth-register',
description: 'Tests complete registration flow (CPU-heavy SQLCipher DB creation)'
},
// =====================================================================
// Shard: auth-pages (3 tests)
// =====================================================================
{
name: 'All Pages Browser Test',
file: 'test_pages_browser.js',
shard: 'auth-pages',
description: 'Tests all main pages for basic functionality'
},
{
name: 'Full Navigation Test',
file: 'test_full_navigation.js',
shard: 'auth-pages',
description: 'Tests full app navigation flow'
},
{
name: 'Auth Comprehensive CI Tests',
file: 'test_auth_comprehensive_ci.js',
shard: 'auth-pages',
description: 'Tests password strength, form validation, remember me, sessions'
},
// =====================================================================
// Shard: research-workflow (3 tests)
// =====================================================================
{
name: 'Research Workflow Test',
file: 'test_research_workflow.js',
shard: 'research-workflow',
description: 'Tests the complete research lifecycle from submission to results'
},
{
name: 'Research Workflow CI Tests',
file: 'test_research_workflow_ci.js',
shard: 'research-workflow',
description: 'Tests research form, progress page, results, exports'
},
{
name: 'Follow-up Research CI Tests',
file: 'test_followup_research_ci.js',
shard: 'research-workflow',
description: 'Tests follow-up research flow'
},
// =====================================================================
// Shard: research-form (3 tests)
// =====================================================================
{
name: 'Research Form CI Tests',
file: 'test_research_form_ci.js',
shard: 'research-form',
description: 'Tests advanced options, mode toggle, dropdowns, validation'
},
{
name: 'Research Results Test',
file: 'test_research_results.js',
shard: 'research-form',
description: 'Tests error handling for non-existent research and history page structure'
},
{
name: 'Results & Exports CI Tests',
file: 'test_results_exports_ci.js',
shard: 'research-form',
description: 'Tests star ratings, export buttons, download functionality'
},
// =====================================================================
// Shard: research-metrics (3 tests)
// =====================================================================
{
name: 'Metrics Charts Test',
file: 'test_metrics_charts.js',
shard: 'research-metrics',
description: 'Tests Chart.js rendering for token and search charts'
},
{
name: 'Metrics Dashboard CI Tests',
file: 'test_metrics_dashboard_ci.js',
shard: 'research-metrics',
description: 'Tests metrics dashboard, cost analytics, star reviews, links'
},
{
name: 'Realtime Progress CI Tests',
file: 'test_realtime_progress_ci.js',
shard: 'research-metrics',
description: 'Tests progress page and real-time elements'
},
// =====================================================================
// Shard: link-analytics (1 test — release-pipeline only)
// =====================================================================
{
name: 'Link Analytics Full Page Test',
file: 'test_link_analytics_full.js',
shard: 'link-analytics',
description: 'Verifies /metrics/links renders, Recent Researches header shows numeric total, no console errors, no script element leaks'
},
// =====================================================================
// Shard: settings-core (4 tests)
// =====================================================================
{
name: 'Settings Page Test',
file: 'test_settings_page.js',
shard: 'settings-core',
description: 'Tests settings page loading and API integration'
},
{
name: 'Settings Error Detection Test',
file: 'test_settings_errors.js',
shard: 'settings-core',
description: 'Tests error handling when changing settings'
},
{
name: 'Settings Save Test',
file: 'test_settings_save.js',
shard: 'settings-core',
description: 'Tests settings save workflow and validation'
},
{
name: 'Settings Interactions CI Tests',
file: 'test_settings_interactions_ci.js',
shard: 'settings-core',
description: 'Tests tabs, search, toggles, save, raw config'
},
// =====================================================================
// Shard: settings-pages (3 tests)
// =====================================================================
{
name: 'Settings Pages CI Tests',
file: 'test_settings_pages_ci.js',
shard: 'settings-pages',
description: 'Tests settings tabs, navigation, provider/engine settings'
},
{
name: 'Star Reviews Test',
file: 'test_star_reviews.js',
shard: 'settings-pages',
description: 'Tests star reviews analytics page and visualizations'
},
{
name: 'Journal Quality CI Tests',
file: 'test_journal_quality_ci.js',
shard: 'settings-pages',
description: 'Tests journal quality dashboard: tabs, threshold slider settings round-trip, data-status APIs'
},
// =====================================================================
// Shard: library (5 tests)
// =====================================================================
{
name: 'Library Collections CI Tests',
file: 'test_library_collections_ci.js',
shard: 'library',
description: 'Tests library page, collections, document details'
},
{
name: 'Library Documents CI Tests',
file: 'test_library_documents_ci.js',
shard: 'library',
description: 'Tests filters, views, PDF/text viewers, bulk actions'
},
{
name: 'Library Collections Page Test',
file: 'library/test_collections_page.js',
shard: 'library',
description: 'Tests library collections page'
},
{
name: 'Download Manager CI Tests',
file: 'test_download_manager_ci.js',
shard: 'library',
description: 'Tests download manager: stats, filters, selection cycle, collections API'
},
{
name: 'Zotero Integration CI Tests',
file: 'test_zotero_integration_ci.js',
shard: 'library',
description: 'Tests Zotero page structure, config/status APIs, API-key non-leak contract, unconfigured negative paths'
},
// =====================================================================
// Shard: history-news (3 tests)
// =====================================================================
{
name: 'History Page CI Tests',
file: 'test_history_page_ci.js',
shard: 'history-news',
description: 'Tests history table, actions, search/filter'
},
{
name: 'History Page Test',
file: 'test_history_page.js',
shard: 'history-news',
description: 'Tests history page functionality'
},
{
name: 'News Subscriptions CI Tests',
file: 'test_news_subscriptions_ci.js',
shard: 'history-news',
description: 'Tests news feeds, subscription CRUD, form validation'
},
// =====================================================================
// Shard: mobile (4 tests)
// =====================================================================
{
name: 'Mobile Interactions CI Tests',
file: 'test_mobile_interactions_ci.js',
shard: 'mobile',
description: 'Tests mobile modals, navigation, forms'
},
{
name: 'Mobile Navigation CI Test',
file: 'mobile/test_mobile_navigation_ci.js',
shard: 'mobile',
description: 'Tests mobile navigation patterns'
},
{
name: 'UI Functionality CI Tests',
file: 'mobile/test_ui_functionality_ci.js',
shard: 'mobile',
description: 'Tests forms, dropdowns, modals, navigation, buttons'
},
{
name: 'Loading & Feedback CI Tests',
file: 'test_loading_feedback_ci.js',
shard: 'mobile',
description: 'Tests spinners, toasts, progress bars, hover states'
},
// =====================================================================
// Shard: api-crud (3 tests)
// =====================================================================
{
name: 'API Endpoints CI Tests',
file: 'test_api_endpoints_ci.js',
shard: 'api-crud',
description: 'Tests all major API endpoints'
},
{
name: 'CRUD Operations CI Tests',
file: 'test_crud_operations_ci.js',
shard: 'api-crud',
description: 'Tests collections, subscriptions, documents CRUD'
},
{
name: 'Rate Limiting Functionality Test',
file: 'test_rate_limiting_settings.js',
shard: 'api-crud',
description: 'Tests rate limiting works on auth endpoints and static files are exempt'
},
// =====================================================================
// Shard: error-benchmark (4 tests)
// =====================================================================
{
name: 'Error Recovery Test',
file: 'test_error_recovery.js',
shard: 'error-benchmark',
description: 'Tests how the UI handles various error conditions gracefully'
},
{
name: 'Error Handling CI Tests',
file: 'test_error_handling_ci.js',
shard: 'error-benchmark',
description: 'Tests 404, 401, 429, validation errors'
},
{
name: 'Benchmark CI Tests',
file: 'test_benchmark_ci.js',
shard: 'error-benchmark',
description: 'Tests benchmark dashboard and results pages'
},
{
name: 'Context Overflow CI Tests',
file: 'test_context_overflow_ci.js',
shard: 'error-benchmark',
description: 'Tests context overflow analytics page'
},
// =====================================================================
// Shard: accessibility (1 test)
// =====================================================================
{
name: 'Keyboard & Accessibility CI Tests',
file: 'test_keyboard_accessibility_ci.js',
shard: 'accessibility',
description: 'Tests keyboard navigation, shortcuts, ARIA, focus management'
},
// =====================================================================
// Shard: chat-core (7 tests)
// chat-mode v2 — input, a11y, security, navigation. These tests do
// not require an LLM backend; they exercise the chat page's
// client-side behavior + the chat HTTP routes.
// =====================================================================
{
name: 'Chat ARIA Live Region Test',
file: 'chat/test_chat_aria_live.js',
shard: 'chat-core',
description: 'Tests role=log + aria-live on .ldr-chat-messages'
},
{
name: 'Chat Keyboard & Input Test',
file: 'chat/test_chat_keyboard_and_input.js',
shard: 'chat-core',
description: 'Tests Enter-to-send, Shift+Enter newline, textarea state',
// Re-enabled: the "CDP input not delivered" failure (#4430) was
// Chrome's password-leak-detection dialog after login, now disabled
// via the seeded profile in chrome_profile.js.
},
{
name: 'Chat CSRF Required Test',
file: 'chat/test_chat_csrf_required.js',
shard: 'chat-core',
description: 'Tests that state-mutating chat endpoints reject missing CSRF tokens'
},
{
name: 'Chat Suggestion Chips Test',
file: 'chat/test_chat_suggestion_chips.js',
shard: 'chat-core',
description: 'Tests suggestion-chip click dispatches a chat message'
},
{
name: 'Chat New Chat Button Test',
file: 'chat/test_chat_new_chat_button.js',
shard: 'chat-core',
description: 'Tests "New Chat" button starts a fresh session'
},
{
name: 'Chat URL ?q= Param Test',
file: 'chat/test_chat_url_q_param.js',
shard: 'chat-core',
description: 'Tests /chat?q=... pre-fills the input'
},
{
name: 'Chat Page Navigation Test',
file: 'chat/test_chat_page_navigation.js',
shard: 'chat-core',
description: 'Tests sidebar navigation to /chat works'
},
// =====================================================================
// Shard: chat-lifecycle (6 tests)
// chat-mode v2 — session lifecycle (edit/archive/export), error
// surfacing, reload persistence. LLM-dependent tests live in the
// skipCI section below.
// =====================================================================
{
name: 'Chat Archived Session Rejects Send Test',
file: 'chat/test_chat_archived_session_rejects.js',
shard: 'chat-lifecycle',
description: 'Tests an archived session rejects POST /api/chat/sessions/<id>/messages with 409'
},
{
name: 'Chat Edit Title Test',
file: 'chat/test_chat_edit_title.js',
shard: 'chat-lifecycle',
description: 'Tests in-place rename of a chat session via PATCH'
},
{
name: 'Chat Export Markdown Test',
file: 'chat/test_chat_export_markdown.js',
shard: 'chat-lifecycle',
description: 'Tests export-to-markdown endpoint + UI flow'
},
{
name: 'Chat Reload Persistence Test',
file: 'chat/test_chat_reload_persistence.js',
shard: 'chat-lifecycle',
description: 'Tests messages persist across a page reload'
},
{
name: 'Chat Session Management Test',
file: 'chat/test_chat_session_management.js',
shard: 'chat-lifecycle',
description: 'Tests list/archive/reactivate/delete via the chat UI'
},
{
name: 'Chat Error States Test',
file: 'chat/test_chat_error_states.js',
shard: 'chat-lifecycle',
description: 'Tests 404/429/network-down error rendering'
},
// =====================================================================
// Skipped tests (skipCI: true) — still need shard assignments for
// local runs. Shard names can be anything valid since they never run
// in CI; assigned to the closest active shard.
// =====================================================================
{
name: 'Research Submit Test',
file: 'test_research_submit.js',
shard: 'research-form',
description: 'Tests research submission',
skipCI: true, // Requires LLM backend
},
{
name: 'Export Functionality Test',
file: 'test_export_functionality.js',
shard: 'research-form',
description: 'Tests export features',
skipCI: true, // Auth hangs with "Navigating frame was detached" in Docker
},
{
name: 'Concurrent Limit Test',
file: 'test_concurrent_limit.js',
shard: 'research-workflow',
description: 'Tests concurrent research limits',
skipCI: true, // Requires LLM backend — always fails without model server
},
{
name: 'News Feed CI Tests',
file: 'test_news_feed_ci.js',
shard: 'history-news',
description: 'Tests feed, filters, templates, subscription management',
skipCI: true, // Intermittent 60s navigation timeouts; core coverage in test_news_subscriptions_ci.js
},
{
name: 'Settings Validation Test',
file: 'test_settings_validation.js',
shard: 'settings-core',
description: 'Tests settings input validation',
skipCI: true, // Same frame-detachment issue as Export test in Docker
},
{
name: 'Research Form Validation Test',
file: 'test_research_form_validation.js',
shard: 'research-form',
description: 'Tests research form field validation',
skipCI: true, // Redundant with test_research_form_ci.js; auth frame-detachment in Docker
},
{
name: 'Form Validation ARIA Tests',
file: 'test_form_validation_aria_ci.js',
shard: 'accessibility',
description: 'Tests inline form validation with ARIA support',
skipCI: true, // Auth frame-detachment in Docker causes intermittent 120s timeout
},
{
name: 'Research Simple Test',
file: 'test_research_simple.js',
shard: 'research-workflow',
description: 'Tests basic research flow',
skipCI: true, // Requires LLM backend to complete research submission
},
{
name: 'Research Form Test',
file: 'test_research_form.js',
shard: 'research-form',
description: 'Tests research form interactions',
skipCI: true, // Diagnostic test — requires LLM for form submission
},
{
name: 'Research API Test',
file: 'test_research_api.js',
shard: 'research-workflow',
description: 'Tests research API endpoints via UI',
skipCI: true, // Diagnostic test — requires functioning LLM API
},
{
name: 'Queue Simple Test',
file: 'test_queue_simple.js',
shard: 'research-workflow',
description: 'Tests research queue functionality',
skipCI: true, // Requires LLM backend — always fails without model server
},
{
name: 'Chat Message Flow E2E Test',
file: 'chat/test_chat_message_flow.js',
shard: 'chat-lifecycle',
description: 'End-to-end: send message, watch research streaming, assert assistant response',
skipCI: true, // Requires LDR_TEST_LLM_URL + LDR_TEST_LLM_MODEL backend
},
{
name: 'Chat report_content Refactor Test',
file: 'chat/test_chat_report_content_refactor.js',
shard: 'chat-lifecycle',
description: 'Verifies report_content shape change: chat shows answer-only, /results assembles full',
skipCI: true, // Requires LDR_TEST_LLM_URL + LDR_TEST_LLM_MODEL backend
},
];
async function runTest(test) {
return new Promise((resolve) => {
const startTime = Date.now();
console.log(`\n[${ts()}] Running: ${test.name}`);
const testProcess = spawn('node', [test.file], {
cwd: path.join(__dirname),
stdio: 'inherit',
env: { ...process.env, NODE_OPTIONS: '--max-old-space-size=4096' }
});
// Add timeout for individual tests
// 300 seconds in CI to handle slow registration/auth tests.
// In Docker, new user registration creates encrypted SQLCipher databases
// with key derivation + 58 tables + 500+ settings, which can take 60-120s.
// Subsequent tests may also be slow while the server recovers.
// 60 seconds locally for faster feedback.
const isCI = !!process.env.CI;
const timeoutMs = isCI ? 300000 : 60000;
const timeout = setTimeout(() => {
const elapsed = Math.round((Date.now() - startTime) / 1000);
console.log(`\n⏱️ Test timeout: ${test.name} exceeded ${timeoutMs/1000} seconds (${elapsed}s elapsed)`);
console.log(`🔪 Sending SIGTERM to PID ${testProcess.pid}...`);
testProcess.kill('SIGTERM');
setTimeout(() => {
if (!testProcess.killed) {
console.log(`🔫 Process still alive, sending SIGKILL to PID ${testProcess.pid}...`);
testProcess.kill('SIGKILL');
}
}, 5000);
}, timeoutMs);
testProcess.on('close', (code) => {
clearTimeout(timeout);
const elapsed = Math.round((Date.now() - startTime) / 1000);
const success = code === 0;
console.log(`[${ts()}] ${success ? '✅' : '❌'} ${test.name}: ${success ? 'PASSED' : 'FAILED'} (${elapsed}s)`);
// Grep-friendly line for post-run duration analysis (used to rebalance shards).
console.log(`TIMING: ${test.name}: ${elapsed}`);
if (code !== 0 && code !== null) {
console.log(` Exit code: ${code}`);
}
resolve({
name: test.name,
success,
code,
duration: elapsed
});
});
testProcess.on('error', (error) => {
clearTimeout(timeout);
const elapsed = Math.round((Date.now() - startTime) / 1000);
console.log(`[${ts()}] ❌ ${test.name}: ERROR - ${error.message} (${elapsed}s)`);
resolve({
name: test.name,
success: false,
error: error.message,
duration: elapsed
});
});
});
}
function parseShardArg() {
const shardArg = process.argv.find(arg => arg.startsWith('--shard='));
return shardArg ? shardArg.split('=')[1] : null;
}
function validateBeforeRun(requestedShard, isCI) {
// Every test must declare a shard so filtering can't silently skip it.
const untagged = tests.filter(t => !t.shard);
if (untagged.length > 0) {
console.error('FATAL: the following tests are missing a `shard:` property:');
untagged.forEach(t => console.error(` - ${t.name} (${t.file})`));
console.error(`Valid shards: ${VALID_SHARDS.join(', ')}`);
process.exit(1);
}
if (requestedShard && !VALID_SHARDS.includes(requestedShard)) {
console.error(`FATAL: unknown shard "${requestedShard}".`);
console.error(`Valid shards: ${VALID_SHARDS.join(', ')}`);
process.exit(1);
}
// Matrix misconfiguration guard: if CI forgot to pass --shard, every
// matrix cell would otherwise run the full suite. Fail loud.
if (isCI && !requestedShard) {
console.error('FATAL: CI=true but no --shard flag provided.');
console.error('Matrix is misconfigured — each cell must pass --shard=<name>.');
console.error(`Valid shards: ${VALID_SHARDS.join(', ')}`);
process.exit(1);
}
}
async function runAllTests() {
const suiteStart = new Date();
const isCI = !!process.env.CI;
const requestedShard = parseShardArg();
validateBeforeRun(requestedShard, isCI);
const shardLabel = requestedShard ? ` [shard: ${requestedShard}]` : '';
console.log(`[${ts(suiteStart)}] Starting UI Test Suite${shardLabel}\n`);
const results = [];
for (const test of tests) {
// skipCI takes priority: a test marked skipCI stays skipped in CI even
// if its shard matches. Keeps existing skip semantics intact.
if (test.skipCI && isCI) {
// Only log the skip when this shard would have run the test.
if (!requestedShard || test.shard === requestedShard) {
console.log(`\n[${ts()}] ⏭️ Skipping: ${test.name} (not supported in CI Docker)`);
results.push({ name: test.name, success: true, duration: 0, skipped: true });
}
continue;
}
// Shard filter: silently drop tests that don't belong to the requested shard.
if (requestedShard && test.shard !== requestedShard) {
continue;
}
// Ensure server is responsive before starting each test.
// Prevents cascade failures when a previous test stressed the server.
await waitForServer();
const result = await runTest(test);
results.push(result);
}
// Print summary
const suiteEnd = new Date();
const wallTime = Math.round((suiteEnd - suiteStart) / 1000);
console.log(`\n[${ts(suiteEnd)}] TEST SUMMARY`);
const passed = results.filter(r => r.success).length;
const failed = results.filter(r => !r.success).length;
const totalDuration = results.reduce((sum, r) => sum + (r.duration || 0), 0);
// Sort by duration descending so slowest tests are easy to spot
const sorted = [...results].sort((a, b) => (b.duration || 0) - (a.duration || 0));
sorted.forEach(result => {
const status = result.success ? '✅ PASS' : '❌ FAIL';
const duration = result.duration ? ` (${result.duration}s)` : '';
console.log(`${status} ${result.name}${duration}`);
if (result.error) {
console.log(` Error: ${result.error}`);
}
});
const shardSuffix = requestedShard ? ` [shard: ${requestedShard}]` : '';
const rate = results.length === 0 ? 0 : Math.round((passed / results.length) * 100);
console.log(`Total: ${results.length} | Passed: ${passed} | Failed: ${failed} | Duration: ${totalDuration}s | Wall: ${wallTime}s | Rate: ${rate}%${shardSuffix}`);
if (failed === 0) {
console.log('All tests passed!');
} else {
console.log(`${failed} test(s) failed.`);
}
process.exit(failed > 0 ? 1 : 0);
}
runAllTests().catch(error => {
console.error('💥 Test runner error:', error);
process.exit(1);
});