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
381 lines
14 KiB
JavaScript
381 lines
14 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Mobile Fixes Verification Test
|
|
*
|
|
* Purpose: Verify mobile CSS fixes work correctly on small phone viewports
|
|
* Tests: News, Results, Library, and main research page
|
|
* Viewports: 360px (small Android), 375px (iPhone SE)
|
|
*
|
|
* Usage: node test_mobile_fixes_verification.js
|
|
*/
|
|
|
|
const puppeteer = require('puppeteer');
|
|
const path = require('path');
|
|
const fs = require('fs').promises;
|
|
|
|
// Test devices - focus on SMALL phones
|
|
const DEVICES = {
|
|
'Small_Android_360': {
|
|
width: 360,
|
|
height: 640,
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
'iPhone_SE_375': {
|
|
width: 375,
|
|
height: 667,
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
'iPhone_14_430': {
|
|
width: 430,
|
|
height: 932,
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
}
|
|
};
|
|
|
|
// Pages to test (focus on fixed pages)
|
|
const PAGES = [
|
|
{ path: '/', name: 'Research_Main' },
|
|
{ path: '/news/', name: 'News' },
|
|
{ path: '/library/', name: 'Library' },
|
|
{ path: '/history/', name: 'History' },
|
|
{ path: '/settings/', name: 'Settings' },
|
|
];
|
|
|
|
class MobileFixesTest {
|
|
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, 'mobile-fixes-screenshots', this.timestamp);
|
|
this.results = [];
|
|
this.passed = 0;
|
|
this.failed = 0;
|
|
}
|
|
|
|
async run() {
|
|
console.log('📱 Mobile Fixes Verification Test');
|
|
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' + '='.repeat(60));
|
|
console.log(`✅ Tests passed: ${this.passed}`);
|
|
console.log(`❌ Tests failed: ${this.failed}`);
|
|
console.log(`📁 Screenshots: ${this.outputDir}`);
|
|
console.log('='.repeat(60));
|
|
|
|
// Exit with error code if any failures
|
|
process.exit(this.failed > 0 ? 1 : 0);
|
|
|
|
} 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);
|
|
|
|
// Try to authenticate
|
|
try {
|
|
await this.authenticate(page);
|
|
console.log(' ✅ Authenticated');
|
|
} catch (error) {
|
|
console.log(' ⚠️ Auth failed, testing public pages:', error.message);
|
|
}
|
|
|
|
for (const pageInfo of PAGES) {
|
|
await this.testPage(page, deviceName, deviceDir, pageInfo, viewport);
|
|
}
|
|
|
|
await page.close();
|
|
}
|
|
|
|
async authenticate(page) {
|
|
// Go to login page
|
|
await page.goto(`${this.baseUrl}/auth/login`, { waitUntil: 'networkidle2', timeout: 30000 });
|
|
|
|
// Check if already logged in
|
|
const url = page.url();
|
|
if (!url.includes('/auth/login')) {
|
|
return; // Already authenticated
|
|
}
|
|
|
|
// Try to login with test credentials
|
|
const username = process.env.TEST_USERNAME || 'test@example.com';
|
|
const password = process.env.TEST_PASSWORD || 'testpassword';
|
|
|
|
await page.type('input[name="email"], input[name="username"], #email, #username', username);
|
|
await page.type('input[name="password"], #password', password);
|
|
await page.click('button[type="submit"]');
|
|
await page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 10000 }).catch(() => {});
|
|
}
|
|
|
|
async testPage(page, deviceName, deviceDir, pageInfo, viewport) {
|
|
console.log(` 📄 ${pageInfo.name}...`);
|
|
|
|
const result = {
|
|
device: deviceName,
|
|
page: pageInfo.name,
|
|
viewport,
|
|
issues: [],
|
|
screenshot: null
|
|
};
|
|
|
|
try {
|
|
await page.goto(this.baseUrl + pageInfo.path, {
|
|
waitUntil: 'networkidle2',
|
|
timeout: 30000
|
|
});
|
|
|
|
// Wait for mobile nav and page to settle
|
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
|
|
// Run diagnostic checks
|
|
const diagnostics = await page.evaluate(() => {
|
|
const issues = [];
|
|
|
|
// Check for horizontal scroll (most critical mobile issue)
|
|
const hasHorizontalScroll = document.documentElement.scrollWidth > window.innerWidth;
|
|
if (hasHorizontalScroll) {
|
|
issues.push({
|
|
type: 'HORIZONTAL_SCROLL',
|
|
severity: 'HIGH',
|
|
message: `Page has horizontal scroll: ${document.documentElement.scrollWidth}px > ${window.innerWidth}px`
|
|
});
|
|
|
|
// Find elements causing overflow
|
|
const overflowingElements = [];
|
|
document.querySelectorAll('*').forEach(el => {
|
|
const rect = el.getBoundingClientRect();
|
|
if (rect.right > window.innerWidth + 5) {
|
|
overflowingElements.push({
|
|
tag: el.tagName,
|
|
class: el.className,
|
|
width: rect.width,
|
|
overflow: rect.right - window.innerWidth
|
|
});
|
|
}
|
|
});
|
|
|
|
if (overflowingElements.length > 0) {
|
|
issues.push({
|
|
type: 'OVERFLOW_ELEMENTS',
|
|
severity: 'HIGH',
|
|
message: `${overflowingElements.length} elements overflow viewport`,
|
|
details: overflowingElements.slice(0, 5)
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check sidebar visibility (should be hidden on mobile)
|
|
const sidebar = document.querySelector('.ldr-sidebar');
|
|
if (sidebar) {
|
|
const sidebarStyle = window.getComputedStyle(sidebar);
|
|
if (sidebarStyle.display !== 'none' && sidebarStyle.visibility !== 'hidden') {
|
|
issues.push({
|
|
type: 'SIDEBAR_VISIBLE',
|
|
severity: 'MEDIUM',
|
|
message: 'Desktop sidebar is visible on mobile'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check mobile nav visibility
|
|
const mobileNav = document.querySelector('.ldr-mobile-bottom-nav');
|
|
if (mobileNav) {
|
|
const navStyle = window.getComputedStyle(mobileNav);
|
|
if (navStyle.display === 'none') {
|
|
issues.push({
|
|
type: 'MOBILE_NAV_HIDDEN',
|
|
severity: 'MEDIUM',
|
|
message: 'Mobile navigation is hidden'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check touch targets (44px minimum)
|
|
const smallButtons = [];
|
|
document.querySelectorAll('button, a.btn, [role="button"]').forEach(btn => {
|
|
const rect = btn.getBoundingClientRect();
|
|
if (rect.width > 0 && rect.height > 0 && (rect.width < 40 || rect.height < 40)) {
|
|
smallButtons.push({
|
|
text: btn.textContent.trim().substring(0, 30),
|
|
size: `${Math.round(rect.width)}x${Math.round(rect.height)}`
|
|
});
|
|
}
|
|
});
|
|
|
|
if (smallButtons.length > 3) {
|
|
issues.push({
|
|
type: 'SMALL_TOUCH_TARGETS',
|
|
severity: 'LOW',
|
|
message: `${smallButtons.length} buttons below 44px touch target`,
|
|
details: smallButtons.slice(0, 5)
|
|
});
|
|
}
|
|
|
|
// Check text readability
|
|
const tooSmallText = [];
|
|
document.querySelectorAll('p, span, label, button, a').forEach(el => {
|
|
const style = window.getComputedStyle(el);
|
|
const fontSize = parseFloat(style.fontSize);
|
|
if (fontSize > 0 && fontSize < 12 && el.textContent.trim()) {
|
|
tooSmallText.push({
|
|
tag: el.tagName,
|
|
text: el.textContent.trim().substring(0, 20),
|
|
size: fontSize
|
|
});
|
|
}
|
|
});
|
|
|
|
if (tooSmallText.length > 5) {
|
|
issues.push({
|
|
type: 'SMALL_TEXT',
|
|
severity: 'LOW',
|
|
message: `${tooSmallText.length} elements with text < 12px`,
|
|
details: tooSmallText.slice(0, 5)
|
|
});
|
|
}
|
|
|
|
return {
|
|
issues,
|
|
pageWidth: document.documentElement.scrollWidth,
|
|
viewportWidth: window.innerWidth
|
|
};
|
|
});
|
|
|
|
result.issues = diagnostics.issues;
|
|
result.pageWidth = diagnostics.pageWidth;
|
|
result.viewportWidth = diagnostics.viewportWidth;
|
|
|
|
// Take screenshot
|
|
const screenshotPath = path.join(deviceDir, `${pageInfo.name}.png`);
|
|
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
result.screenshot = screenshotPath;
|
|
|
|
// Report results
|
|
const hasHighIssues = result.issues.some(i => i.severity === 'HIGH');
|
|
if (hasHighIssues) {
|
|
console.log(` ❌ FAILED - ${result.issues.filter(i => i.severity === 'HIGH').length} critical issues`);
|
|
result.issues.filter(i => i.severity === 'HIGH').forEach(i => {
|
|
console.log(` • ${i.message}`);
|
|
});
|
|
this.failed++;
|
|
} else {
|
|
const mediumIssues = result.issues.filter(i => i.severity === 'MEDIUM').length;
|
|
const lowIssues = result.issues.filter(i => i.severity === 'LOW').length;
|
|
if (mediumIssues > 0 || lowIssues > 0) {
|
|
console.log(` ⚠️ PASSED with warnings (${mediumIssues} medium, ${lowIssues} low)`);
|
|
} else {
|
|
console.log(` ✅ PASSED`);
|
|
}
|
|
this.passed++;
|
|
}
|
|
|
|
} catch (error) {
|
|
console.log(` ❌ ERROR: ${error.message}`);
|
|
result.issues.push({
|
|
type: 'ERROR',
|
|
severity: 'HIGH',
|
|
message: error.message
|
|
});
|
|
this.failed++;
|
|
}
|
|
|
|
this.results.push(result);
|
|
}
|
|
|
|
async generateReport() {
|
|
// Generate JSON report
|
|
const jsonPath = path.join(this.outputDir, 'report.json');
|
|
await fs.writeFile(jsonPath, JSON.stringify(this.results, null, 2));
|
|
|
|
// Generate HTML report
|
|
const html = `<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Mobile Fixes Verification Report</title>
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 20px; background: #1a1a2e; color: #eee; }
|
|
h1 { color: #4ecca3; }
|
|
.device { margin: 20px 0; padding: 20px; background: #16213e; border-radius: 8px; }
|
|
.device h2 { color: #4ecca3; margin-top: 0; }
|
|
.page { margin: 10px 0; padding: 15px; background: #0f3460; border-radius: 4px; }
|
|
.page img { max-width: 300px; border: 1px solid #4ecca3; margin-top: 10px; }
|
|
.passed { border-left: 4px solid #4ecca3; }
|
|
.failed { border-left: 4px solid #e94560; }
|
|
.issue { padding: 5px 10px; margin: 5px 0; border-radius: 3px; }
|
|
.issue.HIGH { background: #e94560; }
|
|
.issue.MEDIUM { background: #ff9a00; color: #000; }
|
|
.issue.LOW { background: #4ecca3; color: #000; }
|
|
.summary { padding: 20px; background: #16213e; border-radius: 8px; margin-bottom: 20px; }
|
|
.summary .passed { color: #4ecca3; font-size: 24px; }
|
|
.summary .failed { color: #e94560; font-size: 24px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>📱 Mobile Fixes Verification Report</h1>
|
|
<p>Generated: ${new Date().toISOString()}</p>
|
|
|
|
<div class="summary">
|
|
<span class="passed">✅ Passed: ${this.passed}</span> |
|
|
<span class="failed">❌ Failed: ${this.failed}</span>
|
|
</div>
|
|
|
|
${Object.keys(DEVICES).map(device => `
|
|
<div class="device">
|
|
<h2>📱 ${device} (${DEVICES[device].width}x${DEVICES[device].height})</h2>
|
|
${this.results.filter(r => r.device === device).map(r => `
|
|
<div class="page ${r.issues.some(i => i.severity === 'HIGH') ? 'failed' : 'passed'}">
|
|
<strong>${r.page}</strong>
|
|
${r.issues.length > 0 ? `
|
|
<div class="issues">
|
|
${r.issues.map(i => `<div class="issue ${i.severity}">${i.severity}: ${i.message}</div>`).join('')}
|
|
</div>
|
|
` : '<div style="color:#4ecca3">No issues</div>'}
|
|
${r.screenshot ? `<br><img src="${r.device}/${r.page}.png" alt="${r.page}">` : ''}
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
`).join('')}
|
|
</body>
|
|
</html>`;
|
|
|
|
const htmlPath = path.join(this.outputDir, 'report.html');
|
|
await fs.writeFile(htmlPath, html);
|
|
}
|
|
}
|
|
|
|
// Run the test
|
|
const test = new MobileFixesTest();
|
|
test.run().catch(error => {
|
|
console.error('Test failed:', error);
|
|
process.exit(1);
|
|
});
|