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
469 lines
19 KiB
JavaScript
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);
|
|
});
|