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

469 lines
19 KiB
JavaScript

#!/usr/bin/env node
/**
* Mobile UI Diagnostic Screenshot Tool
*
* Purpose: Generate annotated screenshots for manual review (LOCAL ONLY - not for CI)
*
* Features:
* - Takes screenshots at multiple mobile viewports
* - Annotates problem areas visually
* - Opens sheet menu and captures it
* - Generates HTML report
*
* Usage: node test_mobile_diagnostic.js
*/
const puppeteer = require('puppeteer');
const AuthHelper = require('../auth_helper');
const path = require('path');
const fs = require('fs').promises;
// Skip in CI
if (process.env.CI) {
console.log('⏭️ Skipping diagnostic tool in CI environment');
process.exit(0);
}
// Test devices
const DEVICES = {
'iPhone_SE': {
width: 375,
height: 667,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true
},
'iPhone_14_Pro': {
width: 430,
height: 932,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true
},
'iPad_Mini': {
width: 768,
height: 1024,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true
}
};
// All pages to test
const PAGES = [
{ path: '/', name: 'Research', requiresAuth: true },
{ path: '/history/', name: 'History', requiresAuth: true },
{ path: '/news/', name: 'News', requiresAuth: true },
{ path: '/news/subscriptions', name: 'Subscriptions', requiresAuth: true },
{ path: '/settings/', name: 'Settings', requiresAuth: true },
{ path: '/metrics/', name: 'Metrics', requiresAuth: true },
{ path: '/metrics/context-overflow', name: 'Metrics_Context', requiresAuth: true },
{ path: '/metrics/costs', name: 'Metrics_Costs', requiresAuth: true },
{ path: '/benchmark/', name: 'Benchmark', requiresAuth: true },
{ path: '/settings/collections', name: 'Collections', requiresAuth: true },
{ path: '/library/', name: 'Library', requiresAuth: true },
{ path: '/auth/login', name: 'Login', requiresAuth: false },
{ path: '/auth/register', name: 'Register', requiresAuth: false },
];
class MobileDiagnosticTool {
constructor() {
this.baseUrl = process.env.TEST_BASE_URL || 'http://127.0.0.1:5000';
this.timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
this.outputDir = path.join(__dirname, 'diagnostic-screenshots', this.timestamp);
this.results = [];
}
async run() {
console.log('🔍 Mobile UI Diagnostic Tool');
console.log('=' .repeat(60));
console.log(`📁 Output: ${this.outputDir}`);
console.log('=' .repeat(60));
await fs.mkdir(this.outputDir, { recursive: true });
let browser;
try {
browser = await puppeteer.launch({
headless: process.env.HEADLESS !== 'false',
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
for (const [deviceName, viewport] of Object.entries(DEVICES)) {
await this.testDevice(browser, deviceName, viewport);
}
await this.generateReport();
console.log('\n✅ Diagnostic complete!');
console.log(`📁 View report: file://${path.join(this.outputDir, 'report.html')}`);
} finally {
if (browser) await browser.close();
}
}
async testDevice(browser, deviceName, viewport) {
console.log(`\n📱 Testing ${deviceName} (${viewport.width}x${viewport.height})`);
console.log('─'.repeat(50));
const deviceDir = path.join(this.outputDir, deviceName);
await fs.mkdir(deviceDir, { recursive: true });
const page = await browser.newPage();
await page.setViewport(viewport);
// Authenticate
const authHelper = new AuthHelper(page, this.baseUrl);
try {
await authHelper.ensureAuthenticated();
console.log(' ✅ Authenticated');
} catch (error) {
console.log(' ⚠️ Authentication failed:', error.message);
}
for (const pageInfo of PAGES) {
await this.testPage(page, deviceName, deviceDir, pageInfo);
}
// Test sheet menu
await this.testSheetMenu(page, deviceName, deviceDir);
await page.close();
}
async testPage(page, deviceName, deviceDir, pageInfo) {
console.log(` 📄 ${pageInfo.name}...`);
try {
await page.goto(this.baseUrl + pageInfo.path, {
waitUntil: 'domcontentloaded',
timeout: 30000
});
// Wait for mobile nav to initialize
await new Promise(resolve => setTimeout(resolve, 1000));
// Run diagnostic checks
const diagnostics = await page.evaluate(() => {
const issues = [];
// Check for horizontal scroll
if (document.body.scrollWidth > window.innerWidth) {
issues.push({
type: 'horizontal-scroll',
severity: 'error',
message: `Horizontal scroll detected: ${document.body.scrollWidth}px > ${window.innerWidth}px`
});
}
// Check touch targets
const smallTargets = [];
document.querySelectorAll('button, a, input, select, textarea').forEach(el => {
const rect = el.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
if (rect.width < 44 || rect.height < 44) {
smallTargets.push({
tag: el.tagName,
text: el.textContent?.slice(0, 20) || el.placeholder || '',
size: `${Math.round(rect.width)}x${Math.round(rect.height)}`,
rect: { top: rect.top, left: rect.left, width: rect.width, height: rect.height }
});
}
}
});
if (smallTargets.length > 0) {
issues.push({
type: 'small-touch-targets',
severity: 'warning',
message: `${smallTargets.length} elements smaller than 44x44px`,
details: smallTargets.slice(0, 5)
});
}
// Check small text
const smallText = [];
document.querySelectorAll('p, span, label, h1, h2, h3, h4, h5, h6, a, button').forEach(el => {
const style = window.getComputedStyle(el);
const fontSize = parseFloat(style.fontSize);
if (fontSize > 0 && fontSize < 12 && el.textContent?.trim()) {
smallText.push({
tag: el.tagName,
text: el.textContent.slice(0, 30),
fontSize: Math.round(fontSize)
});
}
});
if (smallText.length > 0) {
issues.push({
type: 'small-text',
severity: 'warning',
message: `${smallText.length} elements with font size < 12px`,
details: smallText.slice(0, 5)
});
}
// Check mobile nav
const mobileNav = document.querySelector('.ldr-mobile-bottom-nav');
const mobileNavVisible = mobileNav &&
window.getComputedStyle(mobileNav).display !== 'none' &&
mobileNav.classList.contains('visible');
// Check if content is hidden behind nav
const mainContent = document.querySelector('.ldr-main-content, .ldr-page.active, main');
let contentHiddenBehindNav = false;
if (mainContent && mobileNav) {
const contentRect = mainContent.getBoundingClientRect();
const navRect = mobileNav.getBoundingClientRect();
if (contentRect.bottom > navRect.top) {
contentHiddenBehindNav = true;
issues.push({
type: 'content-behind-nav',
severity: 'error',
message: 'Content may be hidden behind mobile navigation'
});
}
}
// Check sidebar
const sidebar = document.querySelector('.ldr-sidebar');
const sidebarVisible = sidebar &&
window.getComputedStyle(sidebar).display !== 'none';
if (sidebarVisible && window.innerWidth <= 768) {
issues.push({
type: 'sidebar-visible-on-mobile',
severity: 'error',
message: 'Desktop sidebar is visible on mobile viewport'
});
}
return {
url: window.location.href,
title: document.title,
viewport: { width: window.innerWidth, height: window.innerHeight },
mobileNav: {
exists: !!mobileNav,
visible: mobileNavVisible
},
sidebar: {
exists: !!sidebar,
visible: sidebarVisible
},
issues
};
});
// Annotate issues on page
if (diagnostics.issues.length > 0) {
await this.annotateIssues(page, diagnostics.issues);
}
// Take screenshot
const screenshotPath = path.join(deviceDir, `${pageInfo.name}.png`);
await page.screenshot({ path: screenshotPath, fullPage: false });
// Store results
this.results.push({
device: deviceName,
page: pageInfo.name,
path: pageInfo.path,
screenshot: path.relative(this.outputDir, screenshotPath),
diagnostics
});
const issueCount = diagnostics.issues.length;
const status = issueCount === 0 ? '✅' : issueCount > 2 ? '❌' : '⚠️';
console.log(` ${status} ${issueCount} issues | Nav: ${diagnostics.mobileNav.visible ? '✅' : '❌'}`);
} catch (error) {
console.log(` ❌ Error: ${error.message}`);
this.results.push({
device: deviceName,
page: pageInfo.name,
path: pageInfo.path,
error: error.message
});
}
}
async annotateIssues(page, issues) {
await page.evaluate((issueList) => {
// Remove previous annotations
document.querySelectorAll('.diagnostic-annotation').forEach(el => el.remove());
issueList.forEach(issue => {
if (issue.type === 'small-touch-targets' && issue.details) {
issue.details.forEach(target => {
const annotation = document.createElement('div');
annotation.className = 'diagnostic-annotation';
annotation.style.cssText = `
position: fixed;
top: ${target.rect.top}px;
left: ${target.rect.left}px;
width: ${target.rect.width}px;
height: ${target.rect.height}px;
border: 2px dashed orange;
background: rgba(255, 165, 0, 0.2);
pointer-events: none;
z-index: 99999;
`;
document.body.appendChild(annotation);
});
}
});
}, issues);
}
async testSheetMenu(page, deviceName, deviceDir) {
console.log(' 📋 Testing sheet menu...');
try {
// Navigate to a page with mobile nav
await page.goto(this.baseUrl + '/news/', {
waitUntil: 'domcontentloaded',
timeout: 30000
});
await new Promise(resolve => setTimeout(resolve, 1000));
// Click More button to open sheet
const moreBtn = await page.$('.ldr-mobile-nav-tab[data-tab-id="more"]');
if (moreBtn) {
await moreBtn.click();
await new Promise(resolve => setTimeout(resolve, 500));
// Check if sheet opened
const sheetOpen = await page.evaluate(() => {
const sheet = document.querySelector('.ldr-mobile-sheet-menu');
return sheet && sheet.classList.contains('active');
});
if (sheetOpen) {
// Check if Settings section is visible (the one that was getting cut off)
const settingsVisible = await page.evaluate(() => {
const sheet = document.querySelector('.ldr-mobile-sheet-content');
const settingsSection = Array.from(sheet?.querySelectorAll('.ldr-mobile-sheet-title') || [])
.find(el => el.textContent.includes('Settings'));
if (!settingsSection) return { found: false };
const rect = settingsSection.getBoundingClientRect();
const sheetRect = sheet.getBoundingClientRect();
return {
found: true,
visible: rect.top < sheetRect.bottom,
position: { top: rect.top, sheetBottom: sheetRect.bottom }
};
});
// Take screenshot of open sheet
const screenshotPath = path.join(deviceDir, 'SheetMenu_Open.png');
await page.screenshot({ path: screenshotPath, fullPage: false });
this.results.push({
device: deviceName,
page: 'SheetMenu',
path: 'N/A',
screenshot: path.relative(this.outputDir, screenshotPath),
diagnostics: {
sheetMenu: {
opened: true,
settingsVisible: settingsVisible.visible,
settingsDetails: settingsVisible
}
}
});
const status = settingsVisible.visible ? '✅' : '❌';
console.log(` ${status} Settings section ${settingsVisible.visible ? 'visible' : 'CUT OFF!'}`);
// Close sheet
const overlay = await page.$('.ldr-mobile-sheet-overlay');
if (overlay) await overlay.click();
} else {
console.log(' ⚠️ Sheet menu did not open');
}
} else {
console.log(' ⚠️ More button not found');
}
} catch (error) {
console.log(` ❌ Error testing sheet menu: ${error.message}`);
}
}
async generateReport() {
const reportPath = path.join(this.outputDir, 'report.html');
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 UI Diagnostic Report - ${this.timestamp}</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #1a1a2e; color: #eee; }
h1 { color: #6e4ff6; }
h2 { color: #40bfff; border-bottom: 1px solid #333; padding-bottom: 10px; }
.device-section { margin: 30px 0; }
.page-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; }
.page-card { background: #252540; border-radius: 12px; overflow: hidden; }
.page-card img { width: 100%; height: auto; border-bottom: 1px solid #333; }
.page-info { padding: 15px; }
.page-name { font-weight: bold; font-size: 16px; margin-bottom: 10px; }
.issue { padding: 8px 12px; margin: 5px 0; border-radius: 6px; font-size: 13px; }
.issue.error { background: rgba(250, 92, 124, 0.2); border-left: 3px solid #fa5c7c; }
.issue.warning { background: rgba(249, 188, 11, 0.2); border-left: 3px solid #f9bc0b; }
.status { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 12px; }
.status.ok { background: #0acf97; color: #000; }
.status.error { background: #fa5c7c; }
.status.warning { background: #f9bc0b; color: #000; }
.nav-status { margin-top: 10px; font-size: 13px; }
.summary { background: #252540; padding: 20px; border-radius: 12px; margin-bottom: 30px; }
</style>
</head>
<body>
<h1>📱 Mobile UI Diagnostic Report</h1>
<p>Generated: ${new Date().toISOString()}</p>
<div class="summary">
<h2>Summary</h2>
<p>Total pages tested: ${this.results.length}</p>
<p>Pages with errors: ${this.results.filter(r => r.diagnostics?.issues?.some(i => i.severity === 'error')).length}</p>
<p>Pages with warnings: ${this.results.filter(r => r.diagnostics?.issues?.some(i => i.severity === 'warning')).length}</p>
</div>
${Object.entries(DEVICES).map(([deviceName]) => `
<div class="device-section">
<h2>📱 ${deviceName}</h2>
<div class="page-grid">
${this.results.filter(r => r.device === deviceName).map(result => `
<div class="page-card">
${result.screenshot ? `<img src="${result.screenshot}" alt="${result.page}">` : ''}
<div class="page-info">
<div class="page-name">${result.page}</div>
${result.error ? `<div class="issue error">Error: ${result.error}</div>` : ''}
${result.diagnostics?.issues?.map(issue => `
<div class="issue ${issue.severity}">${issue.message}</div>
`).join('') || ''}
<div class="nav-status">
Mobile Nav: ${result.diagnostics?.mobileNav?.visible ? '✅ Visible' : '❌ Hidden/Missing'}
</div>
</div>
</div>
`).join('')}
</div>
</div>
`).join('')}
</body>
</html>`;
await fs.writeFile(reportPath, html);
}
}
// Run
const tool = new MobileDiagnosticTool();
tool.run().catch(error => {
console.error('❌ Diagnostic tool failed:', error);
process.exit(1);
});