← back to Dear Bubbe Nextjs
archived/tests/mobile-test-suite.js
689 lines
/**
* Dear Bubbe Mobile Experience Test Suite
* Tests all mobile functionality including voice chat, onboarding, sharing, and UI/UX
*/
const puppeteer = require('puppeteer');
const fs = require('fs').promises;
// Test configuration
const TEST_URL = 'http://45.61.58.125:3011';
const MOBILE_DEVICES = [
{ name: 'iPhone 12', viewport: { width: 390, height: 844 }, userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1' },
{ name: 'Samsung Galaxy S21', viewport: { width: 360, height: 800 }, userAgent: 'Mozilla/5.0 (Linux; Android 11; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36' },
{ name: 'iPhone SE', viewport: { width: 375, height: 667 }, userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1' }
];
// Test results storage
const testResults = {
timestamp: new Date().toISOString(),
tests: [],
summary: { passed: 0, failed: 0, warnings: 0 }
};
// Utility function to log test results
function logTest(category, test, status, details = '') {
const result = { category, test, status, details, timestamp: new Date().toISOString() };
testResults.tests.push(result);
if (status === 'PASS') {
testResults.summary.passed++;
console.log(`✅ ${category} - ${test}: PASSED`);
} else if (status === 'FAIL') {
testResults.summary.failed++;
console.error(`❌ ${category} - ${test}: FAILED - ${details}`);
} else if (status === 'WARNING') {
testResults.summary.warnings++;
console.warn(`⚠️ ${category} - ${test}: WARNING - ${details}`);
}
if (details) console.log(` Details: ${details}`);
}
// Test 1: Voice Chat Flow
async function testVoiceChatFlow(page, device, browser) {
console.log(`\n📱 Testing Voice Chat Flow on ${device.name}...`);
try {
// Check if voice chat auto-starts
await new Promise(r => setTimeout(r, 3000)); // Give time for auto-start
// Check for Bubbe's initial greeting
const bubbeMessages = await page.$$eval('.bg-blue-50', elements =>
elements.map(el => el.textContent)
);
if (bubbeMessages.length > 0) {
logTest('Voice Chat', `Auto-start on ${device.name}`, 'PASS', 'Bubbe started talking automatically');
// Check if the greeting includes invasive questions
const hasInvasiveQuestion = bubbeMessages.some(msg =>
msg.includes('?') && (
msg.includes('married') ||
msg.includes('job') ||
msg.includes('weight') ||
msg.includes('money')
)
);
if (hasInvasiveQuestion) {
logTest('Voice Chat', `Invasive questions on ${device.name}`, 'PASS', 'Bubbe asks appropriate invasive questions');
} else {
logTest('Voice Chat', `Invasive questions on ${device.name}`, 'WARNING', 'No invasive questions detected in greeting');
}
} else {
logTest('Voice Chat', `Auto-start on ${device.name}`, 'WARNING', 'No automatic Bubbe greeting detected');
}
// Check for TALK button visibility
const talkButton = await page.$x('//button[contains(text(), "TALK")');
const isTalkButtonVisible = talkButton ? await talkButton.isVisible() : false;
if (!isTalkButtonVisible && bubbeMessages.length > 0) {
logTest('Voice Chat', `No TALK button after response on ${device.name}`, 'PASS', 'Auto-listening continues correctly');
} else if (isTalkButtonVisible) {
logTest('Voice Chat', `No TALK button after response on ${device.name}`, 'FAIL', 'TALK button appeared when it shouldn\'t');
}
// Test microphone permissions request
const context = browser.defaultBrowserContext();
await context.overridePermissions(TEST_URL, ['microphone']);
logTest('Voice Chat', `Microphone permissions on ${device.name}`, 'PASS', 'Permissions handled correctly');
// Check for continuous conversation indicators
const listeningIndicator = await page.$('.animate-pulse');
if (listeningIndicator) {
logTest('Voice Chat', `Listening indicator on ${device.name}`, 'PASS', 'Visual feedback for listening state');
}
} catch (error) {
logTest('Voice Chat', `General flow on ${device.name}`, 'FAIL', error.message);
}
}
// Test 2: User Onboarding
async function testUserOnboarding(page, device) {
console.log(`\n📝 Testing User Onboarding on ${device.name}...`);
try {
// Check for profile/signup prompt
const profileButton = await page.$x('//button[contains(text(), "Profile")');
const signupPrompt = await page.$('[class*="signup"]');
if (profileButton || signupPrompt) {
logTest('Onboarding', `Profile access on ${device.name}`, 'PASS', 'Profile/signup options available');
// Test profile form if available
if (profileButton) {
await profileButton.click();
await new Promise(r => setTimeout(r,(1000);
// Check for form fields
const nameInput = await page.$('input[placeholder*="name" i]');
const relationshipDropdown = await page.$('select');
const locationInput = await page.$('input[placeholder*="location" i]');
if (nameInput && relationshipDropdown && locationInput) {
logTest('Onboarding', `Form fields on ${device.name}`, 'PASS', 'All required fields present');
// Test relationship dropdown doesn't cut off Done button
const doneButton = await page.$x('//button[contains(text(), "Done")');
if (doneButton) {
const isVisible = await doneButton.isIntersectingViewport();
if (isVisible) {
logTest('Onboarding', `Done button visibility on ${device.name}`, 'PASS', 'Done button not cut off');
} else {
logTest('Onboarding', `Done button visibility on ${device.name}`, 'FAIL', 'Done button may be cut off by dropdown');
}
}
// Test Enter key navigation on mobile keyboard
await nameInput.type('Test User');
await page.keyboard.press('Enter');
const focusedElement = await page.evaluate(() => document.activeElement?.tagName);
logTest('Onboarding', `Enter key navigation on ${device.name}`, 'PASS', `Focus moved to: ${focusedElement}`);
// Test location field with dynamic Bubbe comments
await locationInput.type('New York');
await new Promise(r => setTimeout(r,(500);
const bubbeComment = await page.$('.text-blue-600');
if (bubbeComment) {
const comment = await bubbeComment.textContent();
logTest('Onboarding', `Dynamic location comments on ${device.name}`, 'PASS', `Bubbe says: "${comment}"`);
}
} else {
logTest('Onboarding', `Form fields on ${device.name}`, 'FAIL', 'Missing required form fields');
}
}
} else {
logTest('Onboarding', `Profile access on ${device.name}`, 'WARNING', 'No profile/signup options visible');
}
} catch (error) {
logTest('Onboarding', `General flow on ${device.name}`, 'FAIL', error.message);
}
}
// Test 3: Share Functionality
async function testShareFunctionality(page, device) {
console.log(`\n🔗 Testing Share Functionality on ${device.name}...`);
try {
// Look for share button
const shareButton = await page.$('button[aria-label*="share" i]');
const shareIcon = await page.$('svg[class*="share"]');
if (shareButton || shareIcon) {
logTest('Share', `Share button present on ${device.name}`, 'PASS', 'Share option available');
const clickableElement = shareButton || (await shareIcon.$('..'));
if (clickableElement) {
await clickableElement.click();
await new Promise(r => setTimeout(r,(1000);
// Check for share modal
const shareModal = await page.$('[role="dialog"]');
if (shareModal) {
logTest('Share', `Share modal opens on ${device.name}`, 'PASS', 'Modal displayed correctly');
// Test platform buttons
const platforms = ['WhatsApp', 'Instagram', 'TikTok', 'X', 'Facebook', 'SMS', 'Email'];
const foundPlatforms = [];
for (const platform of platforms) {
const button = await page.$(`button:has-text("${platform}")`);
if (button) {
foundPlatforms.push(platform);
// Check if button is clickable
const isClickable = await button.isEnabled();
if (!isClickable) {
logTest('Share', `${platform} button on ${device.name}`, 'WARNING', 'Button present but not clickable');
}
}
}
if (foundPlatforms.length >= 5) {
logTest('Share', `Platform buttons on ${device.name}`, 'PASS', `Found: ${foundPlatforms.join(', ')}`);
} else {
logTest('Share', `Platform buttons on ${device.name}`, 'WARNING', `Only found ${foundPlatforms.length} platforms`);
}
// Test copy link functionality
const copyButton = await page.$x('//button[contains(text(), "Copy")');
if (copyButton) {
await copyButton.click();
await new Promise(r => setTimeout(r,(500);
const copiedText = await page.$('text="Copied!"');
if (copiedText) {
logTest('Share', `Copy link on ${device.name}`, 'PASS', 'Copy functionality works');
} else {
logTest('Share', `Copy link on ${device.name}`, 'WARNING', 'Copy confirmation not shown');
}
}
// Close modal
const closeButton = await page.$('button[aria-label*="close" i]');
if (closeButton) {
await closeButton.click();
}
} else {
logTest('Share', `Share modal opens on ${device.name}`, 'FAIL', 'Modal did not open');
}
}
} else {
logTest('Share', `Share button present on ${device.name}`, 'FAIL', 'No share button found');
}
} catch (error) {
logTest('Share', `General functionality on ${device.name}`, 'FAIL', error.message);
}
}
// Test 4: Response Quality
async function testResponseQuality(page, device) {
console.log(`\n💬 Testing Response Quality on ${device.name}...`);
try {
// Send a test message
const input = await page.$('textarea, input[type="text"]');
if (input) {
await input.type('Tell me about the weather');
await page.keyboard.press('Enter');
// Wait for response
await new Promise(r => setTimeout(r,(3000);
// Get the latest Bubbe response
const responses = await page.$$eval('.bg-blue-50', elements =>
elements.map(el => el.textContent)
);
const latestResponse = responses[responses.length - 1];
if (latestResponse) {
// Check if response is complete (ends with punctuation)
const endsWithPunctuation = /[.!?]$/.test(latestResponse.trim());
if (endsWithPunctuation) {
logTest('Response Quality', `Complete sentences on ${device.name}`, 'PASS', 'Response ends properly');
} else {
logTest('Response Quality', `Complete sentences on ${device.name}`, 'FAIL', 'Response appears cut off');
}
// Check if response ends with a question
const endsWithQuestion = latestResponse.trim().endsWith('?');
if (endsWithQuestion) {
logTest('Response Quality', `Ends with question on ${device.name}`, 'PASS', 'Conversational flow maintained');
} else {
logTest('Response Quality', `Ends with question on ${device.name}`, 'WARNING', 'No question at end');
}
// Check response length (should be under 320 chars for mobile)
if (latestResponse.length <= 320) {
logTest('Response Quality', `Mobile-optimized length on ${device.name}`, 'PASS', `Length: ${latestResponse.length} chars`);
} else {
logTest('Response Quality', `Mobile-optimized length on ${device.name}`, 'WARNING', `Too long: ${latestResponse.length} chars`);
}
// Check for Yiddish terms
const yiddishTerms = ['oy', 'vey', 'meshuga', 'schmuck', 'putz', 'bubbeleh', 'feh'];
const hasYiddish = yiddishTerms.some(term =>
latestResponse.toLowerCase().includes(term)
);
if (hasYiddish) {
logTest('Response Quality', `Yiddish terms on ${device.name}`, 'PASS', 'Authentic Bubbe personality');
} else {
logTest('Response Quality', `Yiddish terms on ${device.name}`, 'WARNING', 'No Yiddish detected');
}
// Check for invasive questions
const invasivePatterns = [
'married', 'single', 'job', 'money', 'weight',
'kids', 'pregnant', 'dating', 'salary', 'rent'
];
const hasInvasive = invasivePatterns.some(pattern =>
latestResponse.toLowerCase().includes(pattern)
);
if (hasInvasive) {
logTest('Response Quality', `Invasive questions on ${device.name}`, 'PASS', 'True to character');
}
} else {
logTest('Response Quality', `Response generation on ${device.name}`, 'FAIL', 'No response received');
}
} else {
logTest('Response Quality', `Input field on ${device.name}`, 'FAIL', 'Cannot find input field');
}
} catch (error) {
logTest('Response Quality', `General quality on ${device.name}`, 'FAIL', error.message);
}
}
// Test 5: General Mobile UI/UX
async function testGeneralMobileUX(page, device) {
console.log(`\n📱 Testing General Mobile UI/UX on ${device.name}...`);
try {
// Check viewport meta tag
const viewportMeta = await page.$eval('meta[name="viewport"]', el => el.content);
if (viewportMeta.includes('width=device-width')) {
logTest('Mobile UI', `Viewport meta on ${device.name}`, 'PASS', viewportMeta);
} else {
logTest('Mobile UI', `Viewport meta on ${device.name}`, 'FAIL', 'Missing proper viewport meta');
}
// Check for hydration errors
const consoleErrors = [];
page.on('console', msg => {
if (msg.type() === 'error') {
consoleErrors.push(msg.text());
}
});
await page.reload();
await new Promise(r => setTimeout(r,(3000);
const hydrationErrors = consoleErrors.filter(err =>
err.includes('hydration') || err.includes('Hydration')
);
if (hydrationErrors.length === 0) {
logTest('Mobile UI', `No hydration errors on ${device.name}`, 'PASS', 'Clean console');
} else {
logTest('Mobile UI', `No hydration errors on ${device.name}`, 'FAIL', `Found ${hydrationErrors.length} errors`);
}
// Test landscape orientation
await page.setViewport({
width: device.viewport.height,
height: device.viewport.width
});
await new Promise(r => setTimeout(r,(1000);
const landscapeLayout = await page.$('.container');
if (landscapeLayout) {
logTest('Mobile UI', `Landscape orientation on ${device.name}`, 'PASS', 'Layout adapts to landscape');
}
// Return to portrait
await page.setViewport(device.viewport);
// Check for scroll functionality
await page.evaluate(() => {
window.scrollTo(0, document.body.scrollHeight);
});
const scrolled = await page.evaluate(() => window.pageYOffset > 0);
if (scrolled) {
logTest('Mobile UI', `Scroll functionality on ${device.name}`, 'PASS', 'Page scrolls properly');
}
// Test nudging system (wait for 30 seconds - abbreviated for testing)
console.log(' Waiting 5 seconds to test nudge system...');
await new Promise(r => setTimeout(r,(5000);
const nudgeMessage = await page.$('text=/still there|hello|anyone|waiting/i');
if (nudgeMessage) {
logTest('Mobile UI', `Nudging system on ${device.name}`, 'PASS', 'Nudge messages appear');
} else {
logTest('Mobile UI', `Nudging system on ${device.name}`, 'WARNING', 'No nudge detected in 5 seconds');
}
// Check mobile-specific styles
const fontSize = await page.$eval('input, textarea', el =>
window.getComputedStyle(el).fontSize
);
if (fontSize === '16px') {
logTest('Mobile UI', `Input font size on ${device.name}`, 'PASS', 'Prevents iOS zoom');
} else {
logTest('Mobile UI', `Input font size on ${device.name}`, 'WARNING', `Font size is ${fontSize}`);
}
// Check for touch-friendly button sizes
const buttons = await page.$$('button');
let tooSmall = 0;
for (const button of buttons.slice(0, 5)) { // Check first 5 buttons
const box = await button.boundingBox();
if (box && (box.width < 44 || box.height < 44)) {
tooSmall++;
}
}
if (tooSmall === 0) {
logTest('Mobile UI', `Touch targets on ${device.name}`, 'PASS', 'All buttons are touch-friendly');
} else {
logTest('Mobile UI', `Touch targets on ${device.name}`, 'WARNING', `${tooSmall} buttons may be too small`);
}
} catch (error) {
logTest('Mobile UI', `General UX on ${device.name}`, 'FAIL', error.message);
}
}
// Test 6: Known Issues Verification
async function testKnownIssuesFixes(page, device) {
console.log(`\n🔧 Testing Known Issues Fixes on ${device.name}...`);
try {
// Send multiple messages to test response completeness
const input = await page.$('textarea, input[type="text"]');
if (input) {
// Test 1: Response completeness
await input.type('What do you think about my life choices?');
await page.keyboard.press('Enter');
await new Promise(r => setTimeout(r,(3000);
const responses = await page.$$eval('.bg-blue-50', elements =>
elements.map(el => el.textContent)
);
const lastResponse = responses[responses.length - 1];
if (lastResponse && !lastResponse.endsWith('...')) {
logTest('Known Issues', `Response not cut off on ${device.name}`, 'PASS', 'Complete response');
} else {
logTest('Known Issues', `Response not cut off on ${device.name}`, 'FAIL', 'Response may be truncated');
}
// Test 2: TALK button doesn't appear after Bubbe speaks
await new Promise(r => setTimeout(r,(2000);
const talkButton = await page.$x('//button[contains(text(), "TALK")');
if (!talkButton || !(await talkButton.isVisible())) {
logTest('Known Issues', `No TALK button after response on ${device.name}`, 'PASS', 'Continuous conversation works');
} else {
logTest('Known Issues', `No TALK button after response on ${device.name}`, 'FAIL', 'TALK button incorrectly shown');
}
// Test 3: Conversation continues automatically
const listeningState = await page.$('.animate-pulse, [aria-label*="listening"]');
if (listeningState) {
logTest('Known Issues', `Auto-continue conversation on ${device.name}`, 'PASS', 'Listening continues');
} else {
logTest('Known Issues', `Auto-continue conversation on ${device.name}`, 'WARNING', 'May not be auto-listening');
}
// Test 4: Profile saves correctly
const profileButton = await page.$x('//button[contains(text(), "Profile")');
if (profileButton) {
await profileButton.click();
await new Promise(r => setTimeout(r,(1000);
const nameInput = await page.$('input[placeholder*="name" i]');
if (nameInput) {
await nameInput.fill('');
await nameInput.type('Test User Mobile');
const saveButton = await page.$x('//button[contains(text(), "Save"), button:has-text("Done")');
if (saveButton) {
await saveButton.click();
await new Promise(r => setTimeout(r,(1000);
// Reopen profile to check if saved
await profileButton.click();
await new Promise(r => setTimeout(r,(1000);
const savedValue = await nameInput.inputValue();
if (savedValue === 'Test User Mobile') {
logTest('Known Issues', `Profile saves on ${device.name}`, 'PASS', 'Data persists correctly');
} else {
logTest('Known Issues', `Profile saves on ${device.name}`, 'FAIL', 'Data not saved properly');
}
}
}
}
}
} catch (error) {
logTest('Known Issues', `Fix verification on ${device.name}`, 'FAIL', error.message);
}
}
// Main test runner
async function runMobileTests() {
console.log('🚀 Starting Dear Bubbe Mobile Experience Tests');
console.log('=' .repeat(60));
console.log(`URL: ${TEST_URL}`);
console.log(`Time: ${new Date().toLocaleString()}`);
console.log('=' .repeat(60));
for (const device of MOBILE_DEVICES) {
console.log(`\n${'='.repeat(60)}`);
console.log(`📱 Testing on ${device.name}`);
console.log(`Viewport: ${device.viewport.width}x${device.viewport.height}`);
console.log('=' .repeat(60));
const browser = await puppeteer.launch({
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-web-security',
'--disable-features=site-per-process',
'--use-fake-ui-for-media-stream',
'--use-fake-device-for-media-stream'
]
});
try {
const page = await browser.newPage();
// Set mobile viewport and user agent
await page.setViewport(device.viewport);
await page.setUserAgent(device.userAgent);
// Enable console logging
page.on('console', msg => {
if (msg.type() === 'error') {
console.log(` [Console Error]: ${msg.text()}`);
}
});
// Navigate to the app
await page.goto(TEST_URL, { waitUntil: 'networkidle2', timeout: 30000 });
// Run all test suites
await testVoiceChatFlow(page, device, browser);
await testUserOnboarding(page, device);
await testShareFunctionality(page, device);
await testResponseQuality(page, device);
await testGeneralMobileUX(page, device);
await testKnownIssuesFixes(page, device);
// Take screenshot for review
const screenshotName = `mobile-test-${device.name.replace(/\s+/g, '-')}-${Date.now()}.png`;
await page.screenshot({
path: `/root/Projects/dear-bubbe-nextjs/test-screenshots/${screenshotName}`,
fullPage: true
});
console.log(` 📸 Screenshot saved: ${screenshotName}`);
} catch (error) {
console.error(`❌ Fatal error testing ${device.name}: ${error.message}`);
logTest('System', `Device testing on ${device.name}`, 'FAIL', error.message);
} finally {
await browser.close();
}
}
// Generate test report
console.log('\n' + '='.repeat(60));
console.log('📊 TEST RESULTS SUMMARY');
console.log('='.repeat(60));
console.log(`✅ Passed: ${testResults.summary.passed}`);
console.log(`❌ Failed: ${testResults.summary.failed}`);
console.log(`⚠️ Warnings: ${testResults.summary.warnings}`);
console.log('='.repeat(60));
// Save detailed report
const reportPath = `/root/Projects/dear-bubbe-nextjs/mobile-test-report-${Date.now()}.json`;
await fs.writeFile(reportPath, JSON.stringify(testResults, null, 2));
console.log(`\n📄 Detailed report saved to: ${reportPath}`);
// Generate HTML report
const htmlReport = generateHTMLReport(testResults);
const htmlPath = `/root/Projects/dear-bubbe-nextjs/mobile-test-report-${Date.now()}.html`;
await fs.writeFile(htmlPath, htmlReport);
console.log(`📄 HTML report saved to: ${htmlPath}`);
// Return exit code based on failures
return testResults.summary.failed > 0 ? 1 : 0;
}
// Generate HTML report
function generateHTMLReport(results) {
const html = `
<!DOCTYPE html>
<html>
<head>
<title>Dear Bubbe Mobile Test Report</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 20px; background: #f5f5f5; }
.header { background: #2563eb; color: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
.summary { display: flex; gap: 20px; margin-bottom: 20px; }
.summary-card { background: white; padding: 15px; border-radius: 8px; flex: 1; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.passed { color: #10b981; font-size: 24px; font-weight: bold; }
.failed { color: #ef4444; font-size: 24px; font-weight: bold; }
.warning { color: #f59e0b; font-size: 24px; font-weight: bold; }
.test-category { background: white; margin-bottom: 15px; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.category-header { background: #f3f4f6; padding: 10px 15px; font-weight: bold; }
.test-item { padding: 10px 15px; border-bottom: 1px solid #e5e7eb; display: flex; align-items: center; }
.test-item:last-child { border-bottom: none; }
.test-name { flex: 1; }
.test-status { padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: bold; }
.status-pass { background: #d1fae5; color: #065f46; }
.status-fail { background: #fee2e2; color: #991b1b; }
.status-warning { background: #fed7aa; color: #92400e; }
.test-details { color: #6b7280; font-size: 12px; margin-top: 4px; }
</style>
</head>
<body>
<div class="header">
<h1>Dear Bubbe Mobile Test Report</h1>
<p>Generated: ${results.timestamp}</p>
</div>
<div class="summary">
<div class="summary-card">
<div class="passed">${results.summary.passed}</div>
<div>Tests Passed</div>
</div>
<div class="summary-card">
<div class="failed">${results.summary.failed}</div>
<div>Tests Failed</div>
</div>
<div class="summary-card">
<div class="warning">${results.summary.warnings}</div>
<div>Warnings</div>
</div>
</div>
${Object.entries(groupTestsByCategory(results.tests)).map(([category, tests]) => `
<div class="test-category">
<div class="category-header">${category}</div>
${tests.map(test => `
<div class="test-item">
<div class="test-name">
${test.test}
${test.details ? `<div class="test-details">${test.details}</div>` : ''}
</div>
<div class="test-status status-${test.status.toLowerCase()}">${test.status}</div>
</div>
`).join('')}
</div>
`).join('')}
</body>
</html>`;
return html;
}
// Group tests by category for report
function groupTestsByCategory(tests) {
const grouped = {};
tests.forEach(test => {
if (!grouped[test.category]) {
grouped[test.category] = [];
}
grouped[test.category].push(test);
});
return grouped;
}
// Run the tests
runMobileTests()
.then(exitCode => {
console.log('\n✅ Mobile testing completed!');
process.exit(exitCode);
})
.catch(error => {
console.error('❌ Test suite failed:', error);
process.exit(1);
});