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
387 lines
15 KiB
JavaScript
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;
|