learningcircuit--local-deep-research
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
691 行
30 KiB
JavaScript
可执行文件
691 行
30 KiB
JavaScript
可执行文件
#!/usr/bin/env node
|
||
/**
|
||
* Comprehensive Mobile Test - All Pages
|
||
* Tests ALL pages in the LDR application across multiple mobile viewports
|
||
*
|
||
* Features:
|
||
* - Tests 3 mobile viewports: 360px (small Android), 375px (iPhone SE), 430px (iPhone 14)
|
||
* - Tests ALL pages including auth, settings subpages, metrics subpages
|
||
* - Checks for horizontal overflow, mobile nav, sidebar, touch targets, text readability
|
||
* - Takes screenshots of each page at each viewport
|
||
* - Generates comprehensive summary report
|
||
*/
|
||
|
||
const puppeteer = require('puppeteer');
|
||
const AuthHelper = require('../auth_helper');
|
||
const { getPuppeteerLaunchOptions } = require('../puppeteer_config');
|
||
const path = require('path');
|
||
const fs = require('fs').promises;
|
||
|
||
// Test configuration
|
||
const CONFIG = {
|
||
baseUrl: process.env.TEST_BASE_URL || 'http://127.0.0.1:5000',
|
||
timeout: 30000,
|
||
screenshotDir: path.join(__dirname, 'screenshots'),
|
||
};
|
||
|
||
// Mobile viewports to test
|
||
const VIEWPORTS = {
|
||
'Android_360': { width: 360, height: 640, isMobile: true, hasTouch: true, label: 'Small Android (360px)' },
|
||
'iPhone_SE': { width: 375, height: 667, isMobile: true, hasTouch: true, label: 'iPhone SE (375px)' },
|
||
'iPhone_14': { width: 430, height: 932, isMobile: true, hasTouch: true, label: 'iPhone 14 (430px)' },
|
||
};
|
||
|
||
// ALL pages to test
|
||
const PAGES = [
|
||
// Main pages
|
||
{ path: '/', name: 'Research', requiresAuth: true, category: 'Main' },
|
||
{ path: '/history/', name: 'History', requiresAuth: true, category: 'Main' },
|
||
{ path: '/news/', name: 'News', requiresAuth: true, category: 'Main' },
|
||
{ path: '/news/subscriptions', name: 'News-Subscriptions', requiresAuth: true, category: 'News' },
|
||
|
||
// Settings pages
|
||
{ path: '/settings/', name: 'Settings', requiresAuth: true, category: 'Settings' },
|
||
{ path: '/settings/collections', name: 'Settings-Collections', requiresAuth: true, category: 'Settings' },
|
||
{ path: '/settings/embeddings', name: 'Settings-Embeddings', requiresAuth: true, category: 'Settings' },
|
||
|
||
// Metrics pages
|
||
{ path: '/metrics/', name: 'Metrics', requiresAuth: true, category: 'Metrics' },
|
||
{ path: '/metrics/costs', name: 'Metrics-Costs', requiresAuth: true, category: 'Metrics' },
|
||
{ path: '/metrics/context-overflow', name: 'Metrics-ContextOverflow', requiresAuth: true, category: 'Metrics' },
|
||
{ path: '/metrics/star-reviews', name: 'Metrics-StarReviews', requiresAuth: true, category: 'Metrics' },
|
||
|
||
// Other pages
|
||
{ path: '/benchmark/', name: 'Benchmark', requiresAuth: true, category: 'Other' },
|
||
{ path: '/library/', name: 'Library', requiresAuth: true, category: 'Other' },
|
||
{ path: '/library/download-manager', name: 'Library-DownloadManager', requiresAuth: true, category: 'Other' },
|
||
|
||
// Auth pages
|
||
{ path: '/auth/login', name: 'Login', requiresAuth: false, category: 'Auth' },
|
||
{ path: '/auth/register', name: 'Register', requiresAuth: false, category: 'Auth' },
|
||
];
|
||
|
||
class ComprehensiveMobileTest {
|
||
constructor() {
|
||
this.results = {
|
||
startTime: new Date(),
|
||
viewportResults: {},
|
||
pageResults: {},
|
||
issues: [],
|
||
screenshots: [],
|
||
summary: {
|
||
total: 0,
|
||
passed: 0,
|
||
failed: 0,
|
||
warnings: 0,
|
||
pagesTestedPerViewport: {},
|
||
criticalIssues: [],
|
||
viewportStats: {}
|
||
}
|
||
};
|
||
}
|
||
|
||
async run() {
|
||
console.log('🚀 Comprehensive Mobile Test - All Pages');
|
||
console.log('='.repeat(70));
|
||
console.log(`Testing ${Object.keys(VIEWPORTS).length} viewports × ${PAGES.length} pages = ${Object.keys(VIEWPORTS).length * PAGES.length} tests`);
|
||
console.log('='.repeat(70));
|
||
|
||
let browser;
|
||
try {
|
||
// Ensure screenshot directory exists
|
||
await fs.mkdir(CONFIG.screenshotDir, { recursive: true });
|
||
|
||
browser = await puppeteer.launch(getPuppeteerLaunchOptions());
|
||
|
||
for (const [deviceName, viewport] of Object.entries(VIEWPORTS)) {
|
||
await this.testDevice(browser, deviceName, viewport);
|
||
}
|
||
|
||
await this.generateReport();
|
||
|
||
// Exit with error code if there are failed tests
|
||
process.exit(this.results.summary.failed > 0 ? 1 : 0);
|
||
|
||
} catch (error) {
|
||
console.error('❌ Test suite failed:', error);
|
||
process.exit(1);
|
||
} finally {
|
||
if (browser) await browser.close();
|
||
}
|
||
}
|
||
|
||
async testDevice(browser, deviceName, viewport) {
|
||
console.log(`\n${'='.repeat(70)}`);
|
||
console.log(`📱 Testing ${viewport.label}`);
|
||
console.log(`${'='.repeat(70)}`);
|
||
|
||
this.results.viewportResults[deviceName] = {
|
||
viewport: viewport.label,
|
||
tests: [],
|
||
passed: 0,
|
||
failed: 0,
|
||
warnings: 0
|
||
};
|
||
|
||
const page = await browser.newPage();
|
||
await page.setViewport(viewport);
|
||
|
||
// Authenticate for auth-required pages
|
||
const authHelper = new AuthHelper(page, CONFIG.baseUrl);
|
||
let authenticated = false;
|
||
try {
|
||
await authHelper.ensureAuthenticated();
|
||
authenticated = true;
|
||
console.log('✅ Authentication successful\n');
|
||
} catch {
|
||
console.log('⚠️ Authentication failed, testing unauthenticated pages only\n');
|
||
}
|
||
|
||
// Test each page
|
||
for (const pageInfo of PAGES) {
|
||
if (pageInfo.requiresAuth && !authenticated) {
|
||
console.log(` ⏭️ Skipping ${pageInfo.name} (requires auth)`);
|
||
continue;
|
||
}
|
||
await this.testPage(page, deviceName, viewport, pageInfo);
|
||
}
|
||
|
||
await page.close();
|
||
|
||
// Display viewport summary
|
||
const vpResult = this.results.viewportResults[deviceName];
|
||
console.log(`\n${'─'.repeat(70)}`);
|
||
console.log(`${viewport.label} Summary: ${vpResult.passed}/${vpResult.tests.length} passed, ${vpResult.failed} failed, ${vpResult.warnings} warnings`);
|
||
console.log(`${'─'.repeat(70)}`);
|
||
}
|
||
|
||
async testPage(page, deviceName, viewport, pageInfo) {
|
||
const testName = `${deviceName}/${pageInfo.name}`;
|
||
const startTime = Date.now();
|
||
|
||
try {
|
||
// Navigate to page
|
||
await page.goto(CONFIG.baseUrl + pageInfo.path, {
|
||
waitUntil: 'networkidle2',
|
||
timeout: CONFIG.timeout
|
||
});
|
||
|
||
// Wait for page to settle
|
||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||
|
||
// Take screenshot
|
||
const screenshotName = `${deviceName}_${pageInfo.name}.png`;
|
||
const screenshotPath = path.join(CONFIG.screenshotDir, screenshotName);
|
||
await page.screenshot({
|
||
path: screenshotPath,
|
||
fullPage: true
|
||
});
|
||
this.results.screenshots.push({
|
||
viewport: deviceName,
|
||
page: pageInfo.name,
|
||
path: screenshotPath
|
||
});
|
||
|
||
// Run comprehensive checks
|
||
const checks = await page.evaluate(() => {
|
||
const results = {
|
||
horizontalOverflow: false,
|
||
overflowAmount: 0,
|
||
mobileNavExists: false,
|
||
mobileNavVisible: false,
|
||
sidebarHidden: true,
|
||
smallTouchTargets: 0,
|
||
touchTargetDetails: [],
|
||
textReadability: {
|
||
tooSmall: 0,
|
||
elements: []
|
||
},
|
||
viewportWidth: window.innerWidth,
|
||
scrollWidth: document.body.scrollWidth,
|
||
errors: [],
|
||
warnings: []
|
||
};
|
||
|
||
// 1. Check horizontal overflow (CRITICAL)
|
||
const threshold = 5; // Allow 5px tolerance
|
||
if (document.body.scrollWidth > window.innerWidth + threshold) {
|
||
results.horizontalOverflow = true;
|
||
results.overflowAmount = document.body.scrollWidth - window.innerWidth;
|
||
results.errors.push(`Horizontal overflow: ${results.overflowAmount}px beyond viewport`);
|
||
}
|
||
|
||
// 2. Check mobile navigation visibility
|
||
const mobileNav = document.querySelector('.ldr-mobile-bottom-nav');
|
||
results.mobileNavExists = !!mobileNav;
|
||
if (mobileNav) {
|
||
const style = window.getComputedStyle(mobileNav);
|
||
results.mobileNavVisible = style.display !== 'none' &&
|
||
mobileNav.classList.contains('visible');
|
||
}
|
||
|
||
// 3. Check sidebar hidden on mobile
|
||
const sidebar = document.querySelector('.ldr-sidebar');
|
||
if (sidebar) {
|
||
const style = window.getComputedStyle(sidebar);
|
||
results.sidebarHidden = style.display === 'none' ||
|
||
style.visibility === 'hidden' ||
|
||
parseFloat(style.opacity) === 0;
|
||
|
||
if (!results.sidebarHidden && window.innerWidth <= 768) {
|
||
results.errors.push('Desktop sidebar visible on mobile viewport');
|
||
}
|
||
}
|
||
|
||
// 4. Check touch target sizes (minimum 44x44px recommended)
|
||
const interactiveSelectors = [
|
||
'button:not([disabled])',
|
||
'a:not([disabled])',
|
||
'input[type="button"]:not([disabled])',
|
||
'input[type="submit"]:not([disabled])',
|
||
'.ldr-mobile-nav-tab',
|
||
'[role="button"]'
|
||
];
|
||
|
||
document.querySelectorAll(interactiveSelectors.join(', ')).forEach(el => {
|
||
const rect = el.getBoundingClientRect();
|
||
// Only check visible elements
|
||
if (rect.width > 0 && rect.height > 0) {
|
||
const minDimension = Math.min(rect.width, rect.height);
|
||
if (minDimension < 44) {
|
||
results.smallTouchTargets++;
|
||
if (results.touchTargetDetails.length < 5) { // Limit details to first 5
|
||
const identifier = el.id || el.className || el.tagName;
|
||
results.touchTargetDetails.push({
|
||
element: identifier.substring(0, 50),
|
||
size: `${Math.round(rect.width)}×${Math.round(rect.height)}px`
|
||
});
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// 5. Check text readability (minimum 16px for body text)
|
||
const textElements = document.querySelectorAll('p, li, span, div, td, th, label');
|
||
textElements.forEach(el => {
|
||
const style = window.getComputedStyle(el);
|
||
const fontSize = parseFloat(style.fontSize);
|
||
const hasText = el.textContent.trim().length > 20; // Only check substantial text
|
||
|
||
if (hasText && fontSize < 14) {
|
||
results.textReadability.tooSmall++;
|
||
if (results.textReadability.elements.length < 3) {
|
||
results.textReadability.elements.push({
|
||
size: `${fontSize}px`,
|
||
text: el.textContent.substring(0, 30) + '...'
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
// Add warnings
|
||
if (results.smallTouchTargets > 5) {
|
||
results.warnings.push(`${results.smallTouchTargets} touch targets < 44px`);
|
||
}
|
||
if (results.textReadability.tooSmall > 0) {
|
||
results.warnings.push(`${results.textReadability.tooSmall} text elements < 14px`);
|
||
}
|
||
|
||
return results;
|
||
});
|
||
|
||
// Analyze results
|
||
const testResult = {
|
||
name: testName,
|
||
page: pageInfo.name,
|
||
path: pageInfo.path,
|
||
viewport: deviceName,
|
||
status: 'passed',
|
||
issues: [],
|
||
warnings: [],
|
||
checks,
|
||
duration: (Date.now() - startTime) / 1000,
|
||
screenshot: screenshotName
|
||
};
|
||
|
||
// Critical issues (fail test)
|
||
if (checks.horizontalOverflow) {
|
||
testResult.status = 'failed';
|
||
testResult.issues.push(`❌ CRITICAL: Horizontal overflow (${checks.overflowAmount}px)`);
|
||
this.results.issues.push({
|
||
severity: 'critical',
|
||
page: pageInfo.name,
|
||
viewport: deviceName,
|
||
issue: `Horizontal overflow: ${checks.overflowAmount}px`
|
||
});
|
||
}
|
||
|
||
// Check mobile nav visibility (only for authenticated pages, not auth pages)
|
||
if (pageInfo.requiresAuth && !pageInfo.path.includes('/auth/')) {
|
||
const currentUrl = page.url();
|
||
// Only require mobile nav if we're actually on the page (not redirected to login)
|
||
if (!currentUrl.includes('/auth/') && !checks.mobileNavVisible) {
|
||
testResult.status = 'failed';
|
||
testResult.issues.push('❌ CRITICAL: Mobile navigation not visible');
|
||
this.results.issues.push({
|
||
severity: 'critical',
|
||
page: pageInfo.name,
|
||
viewport: deviceName,
|
||
issue: 'Mobile navigation not visible'
|
||
});
|
||
}
|
||
}
|
||
|
||
if (!checks.sidebarHidden) {
|
||
testResult.status = 'failed';
|
||
testResult.issues.push('❌ CRITICAL: Desktop sidebar visible on mobile');
|
||
this.results.issues.push({
|
||
severity: 'critical',
|
||
page: pageInfo.name,
|
||
viewport: deviceName,
|
||
issue: 'Desktop sidebar visible'
|
||
});
|
||
}
|
||
|
||
// Warnings (don't fail test)
|
||
if (checks.smallTouchTargets > 10) {
|
||
testResult.warnings.push(`⚠️ ${checks.smallTouchTargets} small touch targets (< 44px)`);
|
||
if (checks.touchTargetDetails.length > 0) {
|
||
testResult.warnings.push(` Examples: ${checks.touchTargetDetails.map(t => `${t.element} ${t.size}`).join(', ')}`);
|
||
}
|
||
}
|
||
|
||
if (checks.textReadability.tooSmall > 5) {
|
||
testResult.warnings.push(`⚠️ ${checks.textReadability.tooSmall} text elements too small (< 14px)`);
|
||
}
|
||
|
||
// Update counters
|
||
this.results.summary.total++;
|
||
if (testResult.status === 'passed') {
|
||
this.results.summary.passed++;
|
||
this.results.viewportResults[deviceName].passed++;
|
||
} else {
|
||
this.results.summary.failed++;
|
||
this.results.viewportResults[deviceName].failed++;
|
||
}
|
||
|
||
if (testResult.warnings.length > 0) {
|
||
this.results.summary.warnings++;
|
||
this.results.viewportResults[deviceName].warnings++;
|
||
}
|
||
|
||
// Store result
|
||
this.results.viewportResults[deviceName].tests.push(testResult);
|
||
|
||
if (!this.results.pageResults[pageInfo.name]) {
|
||
this.results.pageResults[pageInfo.name] = [];
|
||
}
|
||
this.results.pageResults[pageInfo.name].push(testResult);
|
||
|
||
// Display result
|
||
const status = testResult.status === 'passed' ? '✅' : '❌';
|
||
const category = pageInfo.category ? `[${pageInfo.category}]` : '';
|
||
console.log(` ${status} ${category} ${pageInfo.name.padEnd(30)} | ${checks.viewportWidth}×${checks.scrollWidth}px`);
|
||
|
||
if (testResult.issues.length > 0) {
|
||
testResult.issues.forEach(issue => console.log(` ${issue}`));
|
||
}
|
||
if (testResult.warnings.length > 0) {
|
||
testResult.warnings.forEach(warning => console.log(` ${warning}`));
|
||
}
|
||
|
||
} catch (error) {
|
||
this.results.summary.total++;
|
||
this.results.summary.failed++;
|
||
this.results.viewportResults[deviceName].failed++;
|
||
|
||
const errorResult = {
|
||
name: testName,
|
||
page: pageInfo.name,
|
||
path: pageInfo.path,
|
||
viewport: deviceName,
|
||
status: 'failed',
|
||
issues: [`❌ ERROR: ${error.message}`],
|
||
warnings: [],
|
||
duration: (Date.now() - startTime) / 1000
|
||
};
|
||
|
||
this.results.viewportResults[deviceName].tests.push(errorResult);
|
||
console.log(` ❌ ${pageInfo.name} - ERROR: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
async generateReport() {
|
||
this.results.endTime = new Date();
|
||
this.results.duration = (this.results.endTime - this.results.startTime) / 1000;
|
||
|
||
console.log('\n' + '='.repeat(70));
|
||
console.log('📊 COMPREHENSIVE TEST REPORT');
|
||
console.log('='.repeat(70));
|
||
|
||
// Overall summary
|
||
console.log('\n📈 Overall Summary:');
|
||
console.log(` Total Tests: ${this.results.summary.total}`);
|
||
console.log(` ✅ Passed: ${this.results.summary.passed}`);
|
||
console.log(` ❌ Failed: ${this.results.summary.failed}`);
|
||
console.log(` ⚠️ Warnings: ${this.results.summary.warnings}`);
|
||
console.log(` ⏱️ Duration: ${this.results.duration.toFixed(2)}s`);
|
||
console.log(` 📸 Screenshots: ${this.results.screenshots.length}`);
|
||
|
||
// Viewport breakdown
|
||
console.log('\n📱 Viewport Breakdown:');
|
||
for (const [deviceName, vpResult] of Object.entries(this.results.viewportResults)) {
|
||
const passRate = vpResult.tests.length > 0
|
||
? ((vpResult.passed / vpResult.tests.length) * 100).toFixed(1)
|
||
: 0;
|
||
console.log(` ${VIEWPORTS[deviceName].label}:`);
|
||
console.log(` ${vpResult.passed}/${vpResult.tests.length} passed (${passRate}%) | ${vpResult.failed} failed | ${vpResult.warnings} warnings`);
|
||
}
|
||
|
||
// Critical issues
|
||
if (this.results.summary.failed > 0) {
|
||
console.log('\n❌ CRITICAL ISSUES:');
|
||
const criticalIssues = this.results.issues.filter(i => i.severity === 'critical');
|
||
|
||
// Group by issue type
|
||
const issuesByType = {};
|
||
criticalIssues.forEach(issue => {
|
||
const type = issue.issue.split(':')[0];
|
||
if (!issuesByType[type]) {
|
||
issuesByType[type] = [];
|
||
}
|
||
issuesByType[type].push(issue);
|
||
});
|
||
|
||
for (const [type, issues] of Object.entries(issuesByType)) {
|
||
console.log(`\n ${type}:`);
|
||
issues.forEach(issue => {
|
||
console.log(` - ${issue.page} @ ${issue.viewport}: ${issue.issue}`);
|
||
});
|
||
}
|
||
}
|
||
|
||
// Failed pages
|
||
if (this.results.summary.failed > 0) {
|
||
console.log('\n🚨 Failed Pages:');
|
||
for (const [pageName, results] of Object.entries(this.results.pageResults)) {
|
||
const failed = results.filter(r => r.status === 'failed');
|
||
if (failed.length > 0) {
|
||
console.log(` ${pageName}:`);
|
||
failed.forEach(r => {
|
||
console.log(` - ${VIEWPORTS[r.viewport].label}: ${r.issues.join(', ')}`);
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// Page category summary
|
||
console.log('\n📑 Page Category Summary:');
|
||
const categorySummary = {};
|
||
for (const page of PAGES) {
|
||
if (!categorySummary[page.category]) {
|
||
categorySummary[page.category] = { total: 0, passed: 0, failed: 0 };
|
||
}
|
||
const pageResults = this.results.pageResults[page.name] || [];
|
||
pageResults.forEach(r => {
|
||
categorySummary[page.category].total++;
|
||
if (r.status === 'passed') {
|
||
categorySummary[page.category].passed++;
|
||
} else {
|
||
categorySummary[page.category].failed++;
|
||
}
|
||
});
|
||
}
|
||
|
||
for (const [category, stats] of Object.entries(categorySummary)) {
|
||
const passRate = stats.total > 0 ? ((stats.passed / stats.total) * 100).toFixed(1) : 0;
|
||
console.log(` ${category}: ${stats.passed}/${stats.total} passed (${passRate}%)`);
|
||
}
|
||
|
||
// Screenshot info
|
||
console.log(`\n📸 Screenshots saved to: ${CONFIG.screenshotDir}/`);
|
||
console.log(` Total screenshots: ${this.results.screenshots.length}`);
|
||
|
||
// Save detailed JSON report
|
||
const reportPath = path.join(__dirname, 'mobile-test-report.json');
|
||
await fs.writeFile(reportPath, JSON.stringify(this.results, null, 2));
|
||
console.log(`\n💾 Detailed report saved to: ${reportPath}`);
|
||
|
||
// Generate HTML report
|
||
await this.generateHtmlReport();
|
||
|
||
console.log('\n' + '='.repeat(70));
|
||
if (this.results.summary.failed === 0) {
|
||
console.log('🎉 ALL TESTS PASSED!');
|
||
} else {
|
||
console.log(`⚠️ ${this.results.summary.failed} TEST(S) FAILED`);
|
||
}
|
||
console.log('='.repeat(70) + '\n');
|
||
}
|
||
|
||
async generateHtmlReport() {
|
||
const html = `<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Mobile Test Report - ${new Date().toLocaleDateString()}</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 20px; background: #f5f5f5; }
|
||
.container { max-width: 1400px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
||
h1 { color: #333; margin-bottom: 20px; }
|
||
h2 { color: #555; margin: 30px 0 15px; padding-bottom: 10px; border-bottom: 2px solid #eee; }
|
||
h3 { color: #666; margin: 20px 0 10px; }
|
||
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin: 20px 0; }
|
||
.stat-card { padding: 20px; border-radius: 6px; background: #f8f9fa; }
|
||
.stat-card.passed { border-left: 4px solid #28a745; }
|
||
.stat-card.failed { border-left: 4px solid #dc3545; }
|
||
.stat-card.warning { border-left: 4px solid #ffc107; }
|
||
.stat-label { font-size: 14px; color: #666; margin-bottom: 5px; }
|
||
.stat-value { font-size: 32px; font-weight: bold; color: #333; }
|
||
.viewport-results { margin: 20px 0; }
|
||
.viewport { margin-bottom: 30px; padding: 20px; background: #f8f9fa; border-radius: 6px; }
|
||
.test-grid { display: grid; gap: 15px; margin-top: 15px; }
|
||
.test-item { padding: 15px; background: white; border-radius: 4px; border-left: 4px solid #28a745; }
|
||
.test-item.failed { border-left-color: #dc3545; }
|
||
.test-item.warning { border-left-color: #ffc107; }
|
||
.test-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
|
||
.test-name { font-weight: 600; color: #333; }
|
||
.test-status { padding: 4px 12px; border-radius: 4px; font-size: 12px; font-weight: 600; }
|
||
.test-status.passed { background: #d4edda; color: #155724; }
|
||
.test-status.failed { background: #f8d7da; color: #721c24; }
|
||
.test-details { font-size: 14px; color: #666; margin-top: 8px; }
|
||
.issue { color: #dc3545; margin-top: 5px; }
|
||
.warning { color: #ffc107; margin-top: 5px; }
|
||
.screenshot-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; margin: 20px 0; }
|
||
.screenshot { border: 1px solid #ddd; border-radius: 4px; padding: 10px; background: white; }
|
||
.screenshot img { width: 100%; border-radius: 4px; }
|
||
.screenshot-label { text-align: center; margin-top: 8px; font-size: 12px; color: #666; }
|
||
.category-summary { display: grid; gap: 10px; }
|
||
.category-item { padding: 12px; background: #f8f9fa; border-radius: 4px; display: flex; justify-content: space-between; align-items: center; }
|
||
.progress-bar { width: 200px; height: 20px; background: #e9ecef; border-radius: 10px; overflow: hidden; }
|
||
.progress-fill { height: 100%; background: #28a745; transition: width 0.3s; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>📱 Mobile Test Report</h1>
|
||
<p style="color: #666; margin-bottom: 20px;">Generated on ${new Date().toLocaleString()}</p>
|
||
|
||
<h2>📈 Overall Summary</h2>
|
||
<div class="summary">
|
||
<div class="stat-card">
|
||
<div class="stat-label">Total Tests</div>
|
||
<div class="stat-value">${this.results.summary.total}</div>
|
||
</div>
|
||
<div class="stat-card passed">
|
||
<div class="stat-label">Passed</div>
|
||
<div class="stat-value">${this.results.summary.passed}</div>
|
||
</div>
|
||
<div class="stat-card failed">
|
||
<div class="stat-label">Failed</div>
|
||
<div class="stat-value">${this.results.summary.failed}</div>
|
||
</div>
|
||
<div class="stat-card warning">
|
||
<div class="stat-label">Warnings</div>
|
||
<div class="stat-value">${this.results.summary.warnings}</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Duration</div>
|
||
<div class="stat-value">${this.results.duration.toFixed(1)}s</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Screenshots</div>
|
||
<div class="stat-value">${this.results.screenshots.length}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<h2>📱 Viewport Results</h2>
|
||
<div class="viewport-results">
|
||
${Object.entries(this.results.viewportResults).map(([deviceName, vpResult]) => `
|
||
<div class="viewport">
|
||
<h3>${VIEWPORTS[deviceName].label} (${VIEWPORTS[deviceName].width}px)</h3>
|
||
<p style="margin: 10px 0;">
|
||
<strong>${vpResult.passed}/${vpResult.tests.length}</strong> tests passed
|
||
(${vpResult.tests.length > 0 ? ((vpResult.passed / vpResult.tests.length) * 100).toFixed(1) : 0}%)
|
||
</p>
|
||
<div class="test-grid">
|
||
${vpResult.tests.map(test => `
|
||
<div class="test-item ${test.status} ${test.warnings.length > 0 ? 'warning' : ''}">
|
||
<div class="test-header">
|
||
<span class="test-name">${test.page}</span>
|
||
<span class="test-status ${test.status}">${test.status.toUpperCase()}</span>
|
||
</div>
|
||
<div class="test-details">
|
||
<div>Path: ${test.path}</div>
|
||
<div>Duration: ${test.duration.toFixed(2)}s</div>
|
||
${test.screenshot ? `<div>Screenshot: ${test.screenshot}</div>` : ''}
|
||
${test.issues.length > 0 ? test.issues.map(issue => `<div class="issue">${issue}</div>`).join('') : ''}
|
||
${test.warnings.length > 0 ? test.warnings.map(warning => `<div class="warning">${warning}</div>`).join('') : ''}
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
|
||
<h2>📑 Page Category Summary</h2>
|
||
<div class="category-summary">
|
||
${(() => {
|
||
const categorySummary = {};
|
||
for (const page of PAGES) {
|
||
if (!categorySummary[page.category]) {
|
||
categorySummary[page.category] = { total: 0, passed: 0, failed: 0 };
|
||
}
|
||
const pageResults = this.results.pageResults[page.name] || [];
|
||
pageResults.forEach(r => {
|
||
categorySummary[page.category].total++;
|
||
if (r.status === 'passed') {
|
||
categorySummary[page.category].passed++;
|
||
} else {
|
||
categorySummary[page.category].failed++;
|
||
}
|
||
});
|
||
}
|
||
return Object.entries(categorySummary).map(([category, stats]) => {
|
||
const passRate = stats.total > 0 ? ((stats.passed / stats.total) * 100) : 0;
|
||
return `
|
||
<div class="category-item">
|
||
<div>
|
||
<strong>${category}</strong>
|
||
<div style="font-size: 14px; color: #666;">${stats.passed}/${stats.total} passed</div>
|
||
</div>
|
||
<div class="progress-bar">
|
||
<div class="progress-fill" style="width: ${passRate}%"></div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
})()}
|
||
</div>
|
||
|
||
${this.results.summary.failed > 0 ? `
|
||
<h2>❌ Critical Issues</h2>
|
||
<div style="background: #f8d7da; padding: 15px; border-radius: 6px; border-left: 4px solid #dc3545;">
|
||
${this.results.issues.map(issue => `
|
||
<div style="margin: 8px 0;">
|
||
<strong>${issue.page}</strong> @ ${issue.viewport}: ${issue.issue}
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
` : ''}
|
||
|
||
<h2>📸 Screenshots</h2>
|
||
<p style="color: #666; margin-bottom: 15px;">All screenshots saved to: ${CONFIG.screenshotDir}/</p>
|
||
<p style="color: #666;">Total screenshots: ${this.results.screenshots.length}</p>
|
||
</div>
|
||
</body>
|
||
</html>`;
|
||
|
||
const htmlPath = path.join(__dirname, 'mobile-test-report.html');
|
||
await fs.writeFile(htmlPath, html);
|
||
console.log(`📄 HTML report saved to: ${htmlPath}`);
|
||
}
|
||
}
|
||
|
||
// Run
|
||
const test = new ComprehensiveMobileTest();
|
||
test.run();
|