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
601 lines
22 KiB
JavaScript
601 lines
22 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Mobile Navigation CI/CD Test Suite
|
|
* Programmatic tests with assertions for automated testing
|
|
*/
|
|
|
|
const puppeteer = require('puppeteer');
|
|
const path = require('path');
|
|
const fs = require('fs').promises;
|
|
const { getPuppeteerLaunchOptions } = require('../puppeteer_config');
|
|
|
|
// Test configuration
|
|
const TEST_CONFIG = {
|
|
baseUrl: process.env.TEST_BASE_URL || 'http://127.0.0.1:5000',
|
|
headless: process.env.HEADLESS !== 'false',
|
|
timeout: 30000,
|
|
screenshotOnFailure: process.env.CI ? false : (process.env.SCREENSHOT_ON_FAILURE !== 'false'), // Disable screenshots in CI
|
|
outputFormat: process.env.OUTPUT_FORMAT || 'json', // json, junit, console
|
|
};
|
|
|
|
// Test devices
|
|
const DEVICES = {
|
|
'iPhone_14_Pro_Max': {
|
|
width: 430,
|
|
height: 932,
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true,
|
|
},
|
|
'iPhone_SE': {
|
|
width: 375,
|
|
height: 667,
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
'iPad_Mini': {
|
|
width: 768,
|
|
height: 1024,
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
'Desktop': {
|
|
width: 1920,
|
|
height: 1080,
|
|
deviceScaleFactor: 1,
|
|
isMobile: false,
|
|
hasTouch: false
|
|
}
|
|
};
|
|
|
|
// Test results storage
|
|
class TestResults {
|
|
constructor() {
|
|
this.startTime = new Date();
|
|
this.tests = [];
|
|
this.summary = {
|
|
total: 0,
|
|
passed: 0,
|
|
failed: 0,
|
|
skipped: 0
|
|
};
|
|
}
|
|
|
|
addTest(test) {
|
|
this.tests.push(test);
|
|
this.summary.total++;
|
|
this.summary[test.status]++;
|
|
}
|
|
|
|
getJUnit() {
|
|
const duration = (new Date() - this.startTime) / 1000;
|
|
let xml = `<?xml version="1.0" encoding="UTF-8"?>\n`;
|
|
xml += `<testsuites name="Mobile Navigation Tests" tests="${this.summary.total}" failures="${this.summary.failed}" time="${duration}">\n`;
|
|
xml += ` <testsuite name="Mobile Navigation" tests="${this.summary.total}" failures="${this.summary.failed}" time="${duration}">\n`;
|
|
|
|
for (const test of this.tests) {
|
|
xml += ` <testcase classname="${test.suite}" name="${test.name}" time="${test.duration || 0}">\n`;
|
|
if (test.status === 'failed') {
|
|
xml += ` <failure message="${this.escapeXml(test.error || 'Test failed')}">\n`;
|
|
xml += ` ${this.escapeXml(test.details || '')}\n`;
|
|
xml += ` </failure>\n`;
|
|
}
|
|
xml += ` </testcase>\n`;
|
|
}
|
|
|
|
xml += ` </testsuite>\n`;
|
|
xml += `</testsuites>`;
|
|
return xml;
|
|
}
|
|
|
|
escapeXml(str) {
|
|
return str.replace(/[<>&'"]/g, (c) => {
|
|
switch (c) {
|
|
case '<': return '<';
|
|
case '>': return '>';
|
|
case '&': return '&';
|
|
case '\'': return ''';
|
|
case '"': return '"';
|
|
default: return c;
|
|
}
|
|
});
|
|
}
|
|
|
|
getJSON() {
|
|
return {
|
|
startTime: this.startTime,
|
|
endTime: new Date(),
|
|
duration: (new Date() - this.startTime) / 1000,
|
|
summary: this.summary,
|
|
tests: this.tests
|
|
};
|
|
}
|
|
}
|
|
|
|
// Test assertions
|
|
class MobileNavAssertions {
|
|
static async assertMobileNavExists(page, device) {
|
|
// Wait for mobile navigation to be dynamically created
|
|
try {
|
|
await page.waitForSelector('.ldr-mobile-bottom-nav', {
|
|
timeout: 5000,
|
|
visible: false // Element may exist but not be visible initially
|
|
});
|
|
} catch {
|
|
throw new Error(`Mobile navigation not found on ${device} after 5s wait`);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static async assertMobileNavVisible(page, device, shouldBeVisible) {
|
|
const isVisible = await page.evaluate(() => {
|
|
const nav = document.querySelector('.ldr-mobile-bottom-nav');
|
|
if (!nav) return false;
|
|
const style = window.getComputedStyle(nav);
|
|
return style.display !== 'none' && nav.classList.contains('visible');
|
|
});
|
|
|
|
if (isVisible !== shouldBeVisible) {
|
|
throw new Error(`Mobile nav visibility mismatch on ${device}. Expected: ${shouldBeVisible}, Got: ${isVisible}`);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static async assertSidebarHidden(page, device, shouldBeHidden) {
|
|
const sidebarInfo = await page.evaluate(() => {
|
|
const sidebar = document.querySelector('.ldr-sidebar, aside.ldr-sidebar');
|
|
if (!sidebar) return { exists: false, isHidden: true };
|
|
|
|
const style = window.getComputedStyle(sidebar);
|
|
const rect = sidebar.getBoundingClientRect();
|
|
|
|
return {
|
|
exists: true,
|
|
isHidden: style.display === 'none' ||
|
|
style.visibility === 'hidden' ||
|
|
style.transform.includes('translateX(-100%)') ||
|
|
style.transform.includes('translateX(-') ||
|
|
rect.left < -100,
|
|
display: style.display,
|
|
visibility: style.visibility,
|
|
transform: style.transform,
|
|
position: `${rect.left},${rect.top}`
|
|
};
|
|
});
|
|
|
|
if (!sidebarInfo.exists) {
|
|
console.log(` ✓ Sidebar doesn't exist in DOM (good for mobile)`);
|
|
return true;
|
|
}
|
|
|
|
if (sidebarInfo.isHidden !== shouldBeHidden) {
|
|
const details = `Display: ${sidebarInfo.display}, Visibility: ${sidebarInfo.visibility}, Transform: ${sidebarInfo.transform}, Position: ${sidebarInfo.position}`;
|
|
throw new Error(`Sidebar visibility mismatch on ${device}. Expected hidden: ${shouldBeHidden}, Got: ${!sidebarInfo.isHidden}\nDetails: ${details}`);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static async assertTabCount(page, expectedCount) {
|
|
// Wait for tabs to be created
|
|
await page.waitForSelector('.ldr-mobile-nav-tab', {
|
|
timeout: 5000
|
|
});
|
|
|
|
const tabCount = await page.evaluate(() => {
|
|
const tabs = document.querySelectorAll('.ldr-mobile-nav-tab');
|
|
return tabs.length;
|
|
});
|
|
|
|
if (tabCount !== expectedCount) {
|
|
throw new Error(`Tab count mismatch. Expected: ${expectedCount}, Got: ${tabCount}`);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static async assertActiveTab(page, expectedTab) {
|
|
const activeTab = await page.evaluate(() => {
|
|
const active = document.querySelector('.ldr-mobile-nav-tab.active');
|
|
return active ? active.dataset.tabId : null;
|
|
});
|
|
|
|
if (activeTab !== expectedTab) {
|
|
throw new Error(`Active tab mismatch. Expected: ${expectedTab}, Got: ${activeTab}`);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static async assertSheetMenuToggle(page) {
|
|
// Skip this test in CI - sheet menu animations are flaky in headless
|
|
if (process.env.CI) {
|
|
console.log(' ⏭️ Skipping sheet menu test in CI (animation timing issues)');
|
|
return true;
|
|
}
|
|
|
|
// Click More button
|
|
await page.click('.ldr-mobile-nav-tab[data-tab-id="more"]');
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
// Check if sheet is open
|
|
const sheetOpen = await page.evaluate(() => {
|
|
const sheet = document.querySelector('.ldr-mobile-sheet-menu');
|
|
return sheet && sheet.classList.contains('active');
|
|
});
|
|
|
|
if (!sheetOpen) {
|
|
throw new Error('Sheet menu failed to open');
|
|
}
|
|
|
|
// Close sheet
|
|
await page.click('.ldr-mobile-sheet-overlay');
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
const sheetClosed = await page.evaluate(() => {
|
|
const sheet = document.querySelector('.ldr-mobile-sheet-menu');
|
|
return sheet && !sheet.classList.contains('active');
|
|
});
|
|
|
|
if (!sheetClosed) {
|
|
throw new Error('Sheet menu failed to close');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static async assertNoOverlap(page) {
|
|
const hasOverlap = await page.evaluate(() => {
|
|
const searchInput = document.querySelector('input[type="text"], input[type="search"], textarea');
|
|
const sidebar = document.querySelector('.sidebar');
|
|
|
|
if (!searchInput || !sidebar) return false;
|
|
|
|
const searchRect = searchInput.getBoundingClientRect();
|
|
const sidebarRect = sidebar.getBoundingClientRect();
|
|
|
|
// Check if sidebar overlaps search input
|
|
return !(searchRect.right < sidebarRect.left ||
|
|
searchRect.left > sidebarRect.right ||
|
|
searchRect.bottom < sidebarRect.top ||
|
|
searchRect.top > sidebarRect.bottom);
|
|
});
|
|
|
|
if (hasOverlap) {
|
|
throw new Error('Sidebar overlaps with input elements (Issue #667)');
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Main test runner
|
|
class MobileNavTestRunner {
|
|
constructor() {
|
|
this.results = new TestResults();
|
|
this.browser = null;
|
|
}
|
|
|
|
async setup() {
|
|
// Check server availability
|
|
try {
|
|
const response = await fetch(TEST_CONFIG.baseUrl);
|
|
if (!response.ok) {
|
|
throw new Error(`Server not responding at ${TEST_CONFIG.baseUrl}`);
|
|
}
|
|
} catch (error) {
|
|
console.error(`❌ Server check failed: ${error.message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
this.browser = await puppeteer.launch(getPuppeteerLaunchOptions({
|
|
headless: TEST_CONFIG.headless
|
|
}));
|
|
}
|
|
|
|
async teardown() {
|
|
if (this.browser) {
|
|
await this.browser.close();
|
|
}
|
|
}
|
|
|
|
async runTest(testName, testFn, context = {}) {
|
|
const startTime = Date.now();
|
|
const test = {
|
|
name: testName,
|
|
suite: context.suite || 'MobileNavigation',
|
|
device: context.device,
|
|
status: 'passed',
|
|
error: null,
|
|
details: null,
|
|
duration: 0
|
|
};
|
|
|
|
try {
|
|
await Promise.race([
|
|
testFn(),
|
|
new Promise((_, reject) => setTimeout(() => reject(new Error(`Test "${testName}" timed out after 30s`)), 30000))
|
|
]);
|
|
if (process.env.VERBOSE) {
|
|
console.log(` ✅ ${testName}`);
|
|
}
|
|
} catch (error) {
|
|
test.status = 'failed';
|
|
test.error = error.message;
|
|
test.details = error.stack;
|
|
|
|
if (TEST_CONFIG.screenshotOnFailure && context.page) {
|
|
try {
|
|
const screenshotPath = `./test-failures/${context.device}_${testName.replace(/\s+/g, '_')}.png`;
|
|
await fs.mkdir('./test-failures', { recursive: true });
|
|
|
|
// Add visual debugging based on test type
|
|
if (testName.includes('sidebar') || testName.includes('overlap')) {
|
|
await context.page.evaluate(() => {
|
|
// Highlight sidebar in red if it exists
|
|
const sidebar = document.querySelector('.ldr-sidebar, aside.ldr-sidebar');
|
|
if (sidebar) {
|
|
sidebar.style.outline = '3px solid red';
|
|
sidebar.style.outlineOffset = '2px';
|
|
}
|
|
|
|
// Highlight mobile nav in green
|
|
const mobileNav = document.querySelector('.ldr-mobile-bottom-nav');
|
|
if (mobileNav) {
|
|
mobileNav.style.outline = '3px solid lime';
|
|
}
|
|
|
|
// Highlight any overlapping elements
|
|
const inputs = document.querySelectorAll('input, textarea, select');
|
|
inputs.forEach(el => {
|
|
el.style.outline = '2px dashed blue';
|
|
});
|
|
});
|
|
}
|
|
|
|
if (testName.includes('tab') || testName.includes('navigation')) {
|
|
await context.page.evaluate(() => {
|
|
// Highlight active tab
|
|
const activeTab = document.querySelector('.ldr-mobile-nav-tab.active');
|
|
if (activeTab) {
|
|
activeTab.style.backgroundColor = 'yellow';
|
|
activeTab.style.color = 'black';
|
|
}
|
|
|
|
// Number all tabs
|
|
const tabs = document.querySelectorAll('.ldr-mobile-nav-tab');
|
|
tabs.forEach((tab, i) => {
|
|
const badge = document.createElement('span');
|
|
badge.textContent = `${i + 1}`;
|
|
badge.style.cssText = 'position:absolute;top:0;right:0;background:red;color:white;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:12px;';
|
|
tab.style.position = 'relative';
|
|
tab.appendChild(badge);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Add timeout to screenshot to prevent hanging
|
|
await context.page.screenshot({
|
|
path: screenshotPath,
|
|
fullPage: true,
|
|
timeout: 5000 // 5 second timeout for screenshot
|
|
});
|
|
test.screenshot = screenshotPath;
|
|
console.log(` 📸 Screenshot saved: ${screenshotPath}`);
|
|
} catch (screenshotError) {
|
|
console.error(`Warning: Failed to capture screenshot: ${screenshotError.message}`);
|
|
// Don't fail the test because of screenshot failure
|
|
}
|
|
}
|
|
|
|
console.error(` ❌ ${testName}: ${error.message}`);
|
|
} finally {
|
|
test.duration = (Date.now() - startTime) / 1000;
|
|
this.results.addTest(test);
|
|
}
|
|
}
|
|
|
|
async testDevice(deviceName, viewport) {
|
|
console.log(`\nTesting ${deviceName}...`);
|
|
const page = await this.browser.newPage();
|
|
await page.setViewport(viewport);
|
|
|
|
const context = { device: deviceName, page, suite: `MobileNav.${deviceName}` };
|
|
|
|
try {
|
|
// First authenticate to test on authenticated pages
|
|
const AuthHelper = require('../auth_helper');
|
|
const authHelper = new AuthHelper(page, TEST_CONFIG.baseUrl);
|
|
await authHelper.ensureAuthenticatedWithTimeout();
|
|
|
|
// Navigate to home page after authentication
|
|
await page.goto(TEST_CONFIG.baseUrl, {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: TEST_CONFIG.timeout
|
|
});
|
|
|
|
// Ensure mobile navigation script has loaded
|
|
await page.waitForFunction(() => window.mobileNav !== undefined, {
|
|
timeout: 5000
|
|
});
|
|
|
|
// Test 1: Mobile nav visibility based on viewport
|
|
const shouldShowMobileNav = viewport.width <= 767;
|
|
await this.runTest(
|
|
`Mobile nav visibility (${viewport.width}px)`,
|
|
async () => {
|
|
if (shouldShowMobileNav) {
|
|
await MobileNavAssertions.assertMobileNavExists(page, deviceName);
|
|
await MobileNavAssertions.assertMobileNavVisible(page, deviceName, true);
|
|
} else {
|
|
await MobileNavAssertions.assertMobileNavVisible(page, deviceName, false);
|
|
}
|
|
},
|
|
context
|
|
);
|
|
|
|
// Test 2: Sidebar hidden on mobile
|
|
await this.runTest(
|
|
'Sidebar hidden on mobile',
|
|
async () => {
|
|
// Skip iPad Mini sidebar test in CI - rendering inconsistencies
|
|
if (process.env.CI && deviceName === 'iPad_Mini') {
|
|
console.log(' ⏭️ Skipping iPad Mini sidebar test in CI (rendering inconsistencies)');
|
|
return;
|
|
}
|
|
const shouldHideSidebar = viewport.width <= 767;
|
|
await MobileNavAssertions.assertSidebarHidden(page, deviceName, shouldHideSidebar);
|
|
},
|
|
context
|
|
);
|
|
|
|
// Test 3: No overlap with input elements (Issue #667)
|
|
await this.runTest(
|
|
'No sidebar overlap with inputs',
|
|
async () => {
|
|
await MobileNavAssertions.assertNoOverlap(page);
|
|
},
|
|
context
|
|
);
|
|
|
|
// Test 3b: Also test login page for overlap (Issue #667)
|
|
await this.runTest(
|
|
'No sidebar overlap on login page',
|
|
async () => {
|
|
// Navigate to login page
|
|
await page.goto(`${TEST_CONFIG.baseUrl}/auth/login`, {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: TEST_CONFIG.timeout
|
|
});
|
|
await MobileNavAssertions.assertNoOverlap(page);
|
|
// Navigate back to home
|
|
await page.goto(TEST_CONFIG.baseUrl, {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: TEST_CONFIG.timeout
|
|
});
|
|
},
|
|
context
|
|
);
|
|
|
|
// Mobile-specific tests
|
|
if (shouldShowMobileNav) {
|
|
// Test 4: Correct number of tabs
|
|
await this.runTest(
|
|
'Correct tab count',
|
|
async () => {
|
|
await MobileNavAssertions.assertTabCount(page, 5);
|
|
},
|
|
context
|
|
);
|
|
|
|
// Test 5: Sheet menu toggle
|
|
await this.runTest(
|
|
'Sheet menu toggle',
|
|
async () => {
|
|
await MobileNavAssertions.assertSheetMenuToggle(page);
|
|
},
|
|
context
|
|
);
|
|
|
|
// Test 6: Navigation between pages
|
|
await this.runTest(
|
|
'Tab navigation',
|
|
async () => {
|
|
// Skip navigation test in CI - timing issues with navigation
|
|
if (process.env.CI) {
|
|
console.log(' ⏭️ Skipping tab navigation test in CI (navigation timing issues)');
|
|
return;
|
|
}
|
|
|
|
// Click History tab
|
|
const [_response] = await Promise.all([
|
|
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 10000 }),
|
|
page.click('.ldr-mobile-nav-tab[data-tab-id="history"]')
|
|
]);
|
|
|
|
const url = page.url();
|
|
if (!url.includes('/history')) {
|
|
throw new Error(`Navigation failed. Expected /history, got ${url}`);
|
|
}
|
|
|
|
// Check active tab
|
|
await MobileNavAssertions.assertActiveTab(page, 'history');
|
|
},
|
|
context
|
|
);
|
|
}
|
|
|
|
} finally {
|
|
await page.close();
|
|
}
|
|
}
|
|
|
|
async run() {
|
|
console.log('🚀 Mobile Navigation CI/CD Test Suite');
|
|
console.log('=====================================');
|
|
console.log(`Base URL: ${TEST_CONFIG.baseUrl}`);
|
|
console.log(`Output Format: ${TEST_CONFIG.outputFormat}`);
|
|
console.log('');
|
|
|
|
await this.setup();
|
|
|
|
try {
|
|
// Test each device
|
|
for (const [deviceName, viewport] of Object.entries(DEVICES)) {
|
|
await this.testDevice(deviceName, viewport);
|
|
}
|
|
} finally {
|
|
await this.teardown();
|
|
}
|
|
|
|
// Output results
|
|
await this.outputResults();
|
|
|
|
// Exit with appropriate code
|
|
process.exit(this.results.summary.failed > 0 ? 1 : 0);
|
|
}
|
|
|
|
async outputResults() {
|
|
console.log('\n=====================================');
|
|
console.log('TEST RESULTS');
|
|
console.log('=====================================');
|
|
console.log(`Total: ${this.results.summary.total}`);
|
|
console.log(`Passed: ${this.results.summary.passed}`);
|
|
console.log(`Failed: ${this.results.summary.failed}`);
|
|
console.log(`Duration: ${((new Date() - this.results.startTime) / 1000).toFixed(2)}s`);
|
|
|
|
// Save results based on output format
|
|
if (TEST_CONFIG.outputFormat === 'junit') {
|
|
const junitPath = './test-results.xml';
|
|
await fs.writeFile(junitPath, this.results.getJUnit());
|
|
console.log(`\nJUnit results saved to: ${junitPath}`);
|
|
} else if (TEST_CONFIG.outputFormat === 'json') {
|
|
const jsonPath = './test-results.json';
|
|
await fs.writeFile(jsonPath, JSON.stringify(this.results.getJSON(), null, 2));
|
|
console.log(`\nJSON results saved to: ${jsonPath}`);
|
|
}
|
|
|
|
// Always output to console if there are failures
|
|
if (this.results.summary.failed > 0) {
|
|
console.log('\n❌ FAILED TESTS:');
|
|
for (const test of this.results.tests) {
|
|
if (test.status === 'failed') {
|
|
console.log(` - ${test.suite}.${test.name}: ${test.error}`);
|
|
if (test.screenshot) {
|
|
console.log(` Screenshot: ${test.screenshot}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run tests if executed directly
|
|
if (require.main === module) {
|
|
const runner = new MobileNavTestRunner();
|
|
runner.run().catch(error => {
|
|
console.error('Test runner failed:', error);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
module.exports = { MobileNavTestRunner, MobileNavAssertions, TestResults };
|