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

387 lines
15 KiB
JavaScript

#!/usr/bin/env node
/**
* Comprehensive Mobile UI Test
* Tests all UI elements for mobile compatibility, not just navigation
*/
const puppeteer = require('puppeteer');
const AuthHelper = require('../auth_helper');
const fs = require('fs').promises;
const path = require('path');
class MobileUITester {
constructor() {
this.baseUrl = process.env.TEST_BASE_URL || 'http://127.0.0.1:5000';
this.results = {
passed: [],
failed: [],
warnings: []
};
}
async test() {
let browser;
try {
browser = await puppeteer.launch({
headless: process.env.HEADLESS !== 'false',
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const devices = [
{ name: 'iPhone_SE', width: 375, height: 667 },
{ name: 'iPhone_14_Pro', width: 430, height: 932 },
{ name: 'iPad_Mini', width: 768, height: 1024 }
];
for (const device of devices) {
await this.testDevice(browser, device);
}
this.printResults();
// Exit with error if any tests failed
process.exit(this.results.failed.length > 0 ? 1 : 0);
} catch (error) {
console.error('Test failed:', error);
process.exit(1);
} finally {
if (browser) await browser.close();
}
}
async testDevice(browser, device) {
console.log(`\n📱 Testing ${device.name} (${device.width}x${device.height})`);
console.log('═'.repeat(50));
const page = await browser.newPage();
await page.setViewport({
width: device.width,
height: device.height,
isMobile: true,
hasTouch: true
});
// Authenticate
const authHelper = new AuthHelper(page, this.baseUrl);
await authHelper.ensureAuthenticated();
// Test pages
const pages = [
{ path: '/', name: 'Research', tests: ['topBar', 'inputFields', 'buttons', 'modals'] },
{ path: '/history/', name: 'History', tests: ['topBar', 'searchBar', 'list'] },
{ path: '/news/', name: 'News', tests: ['topBar', 'filters', 'cards'] },
{ path: '/settings/', name: 'Settings', tests: ['topBar', 'tabs', 'forms', 'scrolling'] },
{ path: '/metrics/', name: 'Metrics', tests: ['topBar', 'charts', 'tables'] },
{ path: '/benchmark/', name: 'Benchmark', tests: ['topBar', 'forms', 'buttons'] }
];
for (const pageInfo of pages) {
console.log(`\n 📄 ${pageInfo.name}`);
await page.goto(this.baseUrl + pageInfo.path, {
waitUntil: 'domcontentloaded',
timeout: 30000
});
// Run UI tests
const results = await page.evaluate(() => {
const tests = {};
// 1. Check duplicate navigation bars
tests.duplicateNavBars = {
oldNav: !!document.querySelector('.mobile-tab-bar'),
newNav: !!document.querySelector('.ldr-mobile-bottom-nav'),
hasDuplicates: false
};
tests.duplicateNavBars.hasDuplicates =
tests.duplicateNavBars.oldNav && tests.duplicateNavBars.newNav;
// 2. Check top bar elements
const topBar = document.querySelector('.top-bar');
if (topBar) {
const topBarRect = topBar.getBoundingClientRect();
const userInfo = document.querySelector('.user-info');
const logoutBtn = document.querySelector('.logout-btn');
tests.topBar = {
exists: true,
overflowing: topBarRect.width < topBar.scrollWidth,
userInfoVisible: userInfo ?
window.getComputedStyle(userInfo).display !== 'none' : false,
logoutVisible: logoutBtn ?
window.getComputedStyle(logoutBtn).display !== 'none' : false,
height: topBarRect.height,
isClipped: false
};
// Check if elements are clipped
if (userInfo) {
const userRect = userInfo.getBoundingClientRect();
tests.topBar.isClipped = userRect.right > window.innerWidth;
}
} else {
tests.topBar = { exists: false };
}
// 3. Check input fields accessibility
const inputs = document.querySelectorAll('input, textarea, select');
tests.inputs = {
total: inputs.length,
accessible: 0,
overlapped: [],
tooSmall: []
};
inputs.forEach(input => {
const rect = input.getBoundingClientRect();
const style = window.getComputedStyle(input);
// Check if visible and accessible
if (rect.width > 0 && rect.height > 0 &&
style.display !== 'none' && style.visibility !== 'hidden') {
tests.inputs.accessible++;
// Check if too small for touch (minimum 44x44px for iOS)
if (rect.height < 44) {
tests.inputs.tooSmall.push({
type: input.tagName,
height: rect.height,
id: input.id || input.name
});
}
}
// Check for overlaps with navigation
const navBar = document.querySelector('.ldr-mobile-bottom-nav, .mobile-tab-bar');
if (navBar) {
const navRect = navBar.getBoundingClientRect();
if (rect.bottom > navRect.top && rect.top < navRect.bottom) {
tests.inputs.overlapped.push({
type: input.tagName,
id: input.id || input.name
});
}
}
});
// 4. Check buttons and touch targets
const buttons = document.querySelectorAll('button, .btn, a.btn');
tests.buttons = {
total: buttons.length,
tooSmall: [],
overlapped: []
};
buttons.forEach(button => {
const rect = button.getBoundingClientRect();
if (rect.height < 44 || rect.width < 44) {
tests.buttons.tooSmall.push({
text: button.textContent.trim().substring(0, 20),
size: `${rect.width}x${rect.height}`
});
}
});
// 5. Check horizontal scrolling (should not exist)
tests.horizontalScroll = {
bodyWidth: document.body.scrollWidth,
windowWidth: window.innerWidth,
hasScroll: document.body.scrollWidth > window.innerWidth
};
// 6. Check content padding for nav bars
const mainContent = document.querySelector('.main-content, .page, main');
if (mainContent) {
const contentRect = mainContent.getBoundingClientRect();
const navBar = document.querySelector('.ldr-mobile-bottom-nav');
tests.contentPadding = {
hasBottomPadding: false,
bottomSpace: 0
};
if (navBar) {
const navRect = navBar.getBoundingClientRect();
tests.contentPadding.bottomSpace = window.innerHeight - contentRect.bottom;
tests.contentPadding.hasBottomPadding =
tests.contentPadding.bottomSpace >= navRect.height;
}
}
// 7. Check tab navigation (Settings page)
const tabs = document.querySelectorAll('.nav-tabs, .tab-navigation, [role="tablist"]');
if (tabs.length > 0) {
const tabContainer = tabs[0];
const tabRect = tabContainer.getBoundingClientRect();
tests.tabs = {
exists: true,
overflowing: tabContainer.scrollWidth > tabRect.width,
scrollable: window.getComputedStyle(tabContainer).overflowX === 'auto' ||
window.getComputedStyle(tabContainer).overflowX === 'scroll'
};
}
// 8. Check modals and overlays
const modals = document.querySelectorAll('.modal, .sheet-menu, .overlay');
tests.modals = {
visible: [],
zIndexIssues: []
};
modals.forEach(modal => {
const style = window.getComputedStyle(modal);
if (style.display !== 'none' && style.visibility !== 'hidden') {
tests.modals.visible.push(modal.className);
// Check z-index
const zIndex = parseInt(style.zIndex, 10) || 0;
if (zIndex < 1000) {
tests.modals.zIndexIssues.push({
class: modal.className,
zIndex
});
}
}
});
// 9. Check text readability
const textElements = document.querySelectorAll('p, span, label, h1, h2, h3, h4, h5, h6');
tests.text = {
tooSmall: [],
truncated: []
};
textElements.forEach(elem => {
const style = window.getComputedStyle(elem);
const fontSize = parseFloat(style.fontSize);
if (fontSize < 12) {
tests.text.tooSmall.push({
tag: elem.tagName,
fontSize,
text: elem.textContent.substring(0, 30)
});
}
// Check for text overflow
if (elem.scrollWidth > elem.clientWidth) {
tests.text.truncated.push({
tag: elem.tagName,
text: elem.textContent.substring(0, 30)
});
}
});
return tests;
});
// Analyze results
this.analyzeResults(device.name, pageInfo.name, results);
// Take screenshot for review
const screenshotDir = './mobile-ui-tests';
await fs.mkdir(screenshotDir, { recursive: true });
const screenshotPath = path.join(screenshotDir, `${device.name}_${pageInfo.name}.png`);
await page.screenshot({ path: screenshotPath, fullPage: false });
}
await page.close();
}
analyzeResults(device, pageName, results) {
const context = `${device} - ${pageName}`;
// Check for critical issues
if (results.duplicateNavBars?.hasDuplicates) {
this.results.failed.push(`${context}: Duplicate navigation bars (both old and new)`);
}
if (results.topBar?.isClipped) {
this.results.failed.push(`${context}: Top bar elements are clipped/cut off`);
}
if (results.inputs?.overlapped?.length > 0) {
this.results.failed.push(`${context}: ${results.inputs.overlapped.length} input(s) overlapped by navigation`);
}
if (results.horizontalScroll?.hasScroll) {
this.results.failed.push(`${context}: Horizontal scroll detected (${results.horizontalScroll.bodyWidth}px > ${results.horizontalScroll.windowWidth}px)`);
}
if (results.contentPadding && !results.contentPadding.hasBottomPadding) {
this.results.warnings.push(`${context}: Content may be hidden behind navigation bar`);
}
if (results.tabs?.overflowing && !results.tabs?.scrollable) {
this.results.failed.push(`${context}: Tab navigation overflowing and not scrollable`);
}
// Check for warnings
if (results.inputs?.tooSmall?.length > 0) {
this.results.warnings.push(`${context}: ${results.inputs.tooSmall.length} input(s) below 44px touch target`);
}
if (results.buttons?.tooSmall?.length > 0) {
this.results.warnings.push(`${context}: ${results.buttons.tooSmall.length} button(s) below 44px touch target`);
}
if (results.text?.tooSmall?.length > 0) {
this.results.warnings.push(`${context}: ${results.text.tooSmall.length} text element(s) below 12px`);
}
if (results.text?.truncated?.length > 0) {
this.results.warnings.push(`${context}: ${results.text.truncated.length} text element(s) truncated`);
}
// Log success if no issues
if (!this.hasIssues(results)) {
this.results.passed.push(`${context}: All UI elements properly displayed`);
}
}
hasIssues(results) {
return results.duplicateNavBars?.hasDuplicates ||
results.topBar?.isClipped ||
results.inputs?.overlapped?.length > 0 ||
results.horizontalScroll?.hasScroll ||
(results.tabs?.overflowing && !results.tabs?.scrollable);
}
printResults() {
console.log('\n' + '═'.repeat(60));
console.log('📊 MOBILE UI TEST RESULTS');
console.log('═'.repeat(60));
if (this.results.passed.length > 0) {
console.log('\n✅ PASSED:');
this.results.passed.forEach(p => console.log(` ${p}`));
}
if (this.results.failed.length > 0) {
console.log('\n❌ FAILED:');
this.results.failed.forEach(f => console.log(` ${f}`));
}
if (this.results.warnings.length > 0) {
console.log('\n⚠️ WARNINGS:');
this.results.warnings.forEach(w => console.log(` ${w}`));
}
console.log('\n' + '═'.repeat(60));
console.log(`Total: ${this.results.passed.length} passed, ${this.results.failed.length} failed, ${this.results.warnings.length} warnings`);
if (this.results.failed.length === 0) {
console.log('✅ All critical UI tests passed!');
} else {
console.log('❌ Critical UI issues found - see failures above');
}
}
}
// Run tests
if (require.main === module) {
const tester = new MobileUITester();
tester.test();
}
module.exports = MobileUITester;