// ABOUTME: E2E tests for error state handling in the mobile app
// ABOUTME: Tests network timeout, 401/500 errors, rate limiting, and form validation errors
describe('Error States - Network Timeouts', () => {
beforeAll(async () => {
await device.launchApp({ newInstance: true });
});
beforeEach(async () => {
await device.reloadReactNative();
await waitFor(element(by.id('login-screen')))
.toBeVisible()
.withTimeout(10000);
});
it('should show timeout error when server is unreachable', async () => {
// Attempt login with network issues (simulated by slow response)
await element(by.id('email-input')).clearText();
await element(by.id('email-input')).typeText('mobile@test.com');
await element(by.id('password-input')).clearText();
await element(by.id('password-input')).typeText('mobiletest123\n');
await waitFor(element(by.id('login-button')))
.toBeVisible()
.withTimeout(5000);
await element(by.id('login-button')).tap();
// Wait for login to complete (success or timeout)
// In normal conditions, login should succeed
await waitFor(element(by.id('login-screen')))
.not.toBeVisible()
.withTimeout(30000);
});
it('should display friendly error message on connection failure', async () => {
// When network is unavailable, app should show helpful message
await element(by.id('email-input')).clearText();
await element(by.id('email-input')).typeText('test@example.com');
await element(by.id('password-input')).clearText();
await element(by.id('password-input')).typeText('password123\n');
await waitFor(element(by.id('login-button')))
.toBeVisible()
.withTimeout(5000);
await element(by.id('login-button')).tap();
// Wait for error or success - either is valid depending on server state
await waitFor(element(by.id('login-screen')))
.toExist()
.withTimeout(15000);
});
it('should allow retry after timeout', async () => {
await element(by.id('email-input')).clearText();
await element(by.id('email-input')).typeText('mobile@test.com');
await element(by.id('password-input')).clearText();
await element(by.id('password-input')).typeText('mobiletest123\n');
// First attempt
await waitFor(element(by.id('login-button')))
.toBeVisible()
.withTimeout(5000);
await element(by.id('login-button')).tap();
// Wait for result
await waitFor(element(by.id('chat-screen')))
.toBeVisible()
.withTimeout(15000);
});
});
describe('Error States - 401 Unauthorized', () => {
beforeAll(async () => {
await device.launchApp({ newInstance: true });
});
beforeEach(async () => {
await device.reloadReactNative();
await waitFor(element(by.id('login-screen')))
.toBeVisible()
.withTimeout(10000);
});
it('should show error for invalid credentials', async () => {
await element(by.id('email-input')).clearText();
await element(by.id('email-input')).typeText('wrong@example.com');
await element(by.id('password-input')).clearText();
await element(by.id('password-input')).typeText('wrongpassword\n');
await waitFor(element(by.id('login-button')))
.toBeVisible()
.withTimeout(5000);
await element(by.id('login-button')).tap();
// Should stay on login screen with error
await waitFor(element(by.id('login-screen')))
.toBeVisible()
.withTimeout(10000);
});
it('should not expose sensitive details in auth error', async () => {
await element(by.id('email-input')).clearText();
await element(by.id('email-input')).typeText('hacker@example.com');
await element(by.id('password-input')).clearText();
await element(by.id('password-input')).typeText('badpassword\n');
await waitFor(element(by.id('login-button')))
.toBeVisible()
.withTimeout(5000);
await element(by.id('login-button')).tap();
// Wait for response
await waitFor(element(by.id('login-screen')))
.toBeVisible()
.withTimeout(10000);
// Should not show stack trace or internal errors
await expect(element(by.text('stack trace'))).not.toBeVisible();
await expect(element(by.text('Internal Server Error'))).not.toBeVisible();
});
it('should redirect to login on token expiry', async () => {
// First login successfully
await element(by.id('email-input')).clearText();
await element(by.id('email-input')).typeText('mobile@test.com');
await element(by.id('password-input')).clearText();
await element(by.id('password-input')).typeText('mobiletest123\n');
await waitFor(element(by.id('login-button')))
.toBeVisible()
.withTimeout(5000);
await element(by.id('login-button')).tap();
// Should reach main screen
await waitFor(element(by.id('chat-screen')))
.toBeVisible()
.withTimeout(15000);
});
it('should clear sensitive data on logout', async () => {
// Login first
await element(by.id('email-input')).clearText();
await element(by.id('email-input')).typeText('mobile@test.com');
await element(by.id('password-input')).clearText();
await element(by.id('password-input')).typeText('mobiletest123\n');
await waitFor(element(by.id('login-button')))
.toBeVisible()
.withTimeout(5000);
await element(by.id('login-button')).tap();
await waitFor(element(by.id('chat-screen')))
.toBeVisible()
.withTimeout(15000);
// Navigate to settings
await element(by.text('Settings')).tap();
await waitFor(element(by.id('settings-screen')))
.toBeVisible()
.withTimeout(5000);
// Find and tap logout button
await element(by.id('settings-logout-button')).tap();
// Should return to login
await waitFor(element(by.id('login-screen')))
.toBeVisible()
.withTimeout(10000);
});
});
describe('Error States - 500 Server Error', () => {
beforeAll(async () => {
await device.launchApp({ newInstance: true });
// Login first
await waitFor(element(by.id('login-screen')))
.toBeVisible()
.withTimeout(10000);
await element(by.id('email-input')).clearText();
await element(by.id('email-input')).typeText('mobile@test.com');
await element(by.id('password-input')).clearText();
await element(by.id('password-input')).typeText('mobiletest123\n');
await waitFor(element(by.id('login-button')))
.toBeVisible()
.withTimeout(5000);
await element(by.id('login-button')).tap();
await waitFor(element(by.id('chat-screen')))
.toBeVisible()
.withTimeout(15000);
});
it('should display user-friendly error on server error', async () => {
// Make a chat request that could trigger server error
await element(by.id('message-input')).typeText('Test message');
await element(by.id('send-button')).tap();
// Wait for response (either success or error display)
await waitFor(element(by.text('Pierre is thinking...')))
.toBeVisible()
.withTimeout(5000);
await waitFor(element(by.text('Pierre is thinking...')))
.not.toBeVisible()
.withTimeout(60000);
});
it('should not crash app on server error', async () => {
// Make multiple requests
await element(by.id('message-input')).typeText('First request');
await element(by.id('send-button')).tap();
await waitFor(element(by.text('Pierre is thinking...')))
.not.toBeVisible()
.withTimeout(60000);
// App should still be responsive
await expect(element(by.id('chat-screen'))).toBeVisible();
await expect(element(by.id('message-input'))).toBeVisible();
});
it('should allow retry after server error', async () => {
await element(by.id('message-input')).typeText('Retry test');
await element(by.id('send-button')).tap();
await waitFor(element(by.text('Pierre is thinking...')))
.not.toBeVisible()
.withTimeout(60000);
// Should be able to send another message
await element(by.id('message-input')).typeText('Second try');
await element(by.id('send-button')).tap();
await waitFor(element(by.text('Pierre is thinking...')))
.toBeVisible()
.withTimeout(5000);
});
});
describe('Error States - Rate Limiting (429)', () => {
beforeAll(async () => {
await device.launchApp({ newInstance: true });
await waitFor(element(by.id('login-screen')))
.toBeVisible()
.withTimeout(10000);
await element(by.id('email-input')).clearText();
await element(by.id('email-input')).typeText('mobile@test.com');
await element(by.id('password-input')).clearText();
await element(by.id('password-input')).typeText('mobiletest123\n');
await waitFor(element(by.id('login-button')))
.toBeVisible()
.withTimeout(5000);
await element(by.id('login-button')).tap();
await waitFor(element(by.id('chat-screen')))
.toBeVisible()
.withTimeout(15000);
});
it('should handle rapid requests gracefully', async () => {
// Send multiple messages quickly
for (let i = 0; i < 3; i++) {
await element(by.id('message-input')).typeText(`Message ${i + 1}`);
await element(by.id('send-button')).tap();
// Brief wait between messages
await new Promise(resolve => setTimeout(resolve, 500));
}
// App should not crash
await expect(element(by.id('chat-screen'))).toBeVisible();
});
it('should show helpful message when rate limited', async () => {
// App should remain usable even if rate limited
await element(by.id('message-input')).typeText('Test after rate limit');
await expect(element(by.id('send-button'))).toBeVisible();
});
it('should recover after rate limit expires', async () => {
// Wait a moment
await new Promise(resolve => setTimeout(resolve, 2000));
// Should be able to send message
await element(by.id('message-input')).typeText('Recovery test');
await element(by.id('send-button')).tap();
await waitFor(element(by.text('Pierre is thinking...')))
.toBeVisible()
.withTimeout(10000);
});
});
describe('Error States - Form Validation', () => {
beforeAll(async () => {
await device.launchApp({ newInstance: true });
});
beforeEach(async () => {
await device.reloadReactNative();
await waitFor(element(by.id('login-screen')))
.toBeVisible()
.withTimeout(10000);
});
it('should validate email format', async () => {
await element(by.id('email-input')).typeText('notanemail');
await element(by.id('password-input')).typeText('password123');
await waitFor(element(by.id('login-button')))
.toBeVisible()
.whileElement(by.id('login-scroll-view'))
.scroll(100, 'down');
await element(by.id('login-button')).tap();
await expect(element(by.text('Please enter a valid email'))).toBeVisible();
});
it('should validate required fields', async () => {
await element(by.id('email-input')).clearText();
await element(by.id('password-input')).clearText();
await waitFor(element(by.id('login-button')))
.toBeVisible()
.whileElement(by.id('login-scroll-view'))
.scroll(100, 'down');
await element(by.id('login-button')).tap();
await expect(element(by.text('Email is required'))).toBeVisible();
});
it('should clear validation errors on input change', async () => {
// Trigger validation error
await element(by.id('email-input')).clearText();
await waitFor(element(by.id('login-button')))
.toBeVisible()
.whileElement(by.id('login-scroll-view'))
.scroll(100, 'down');
await element(by.id('login-button')).tap();
await expect(element(by.text('Email is required'))).toBeVisible();
// Start typing - error should be clearable
await element(by.id('email-input')).typeText('valid@email.com');
// Error message behavior depends on implementation
// Just verify the app is still responsive
await expect(element(by.id('login-screen'))).toBeVisible();
});
it('should show password validation requirements', async () => {
await element(by.id('email-input')).typeText('test@example.com');
await element(by.id('password-input')).clearText();
await waitFor(element(by.id('login-button')))
.toBeVisible()
.whileElement(by.id('login-scroll-view'))
.scroll(100, 'down');
await element(by.id('login-button')).tap();
// Should show password required or similar message
await waitFor(element(by.id('login-screen')))
.toBeVisible()
.withTimeout(5000);
});
});