- Implement Next.js middleware for route protection - Create Zustand auth store for web (similar to mobile) - Add comprehensive tests for middleware and auth store - Protect authenticated routes (/dashboard, /profile) - Redirect unauthenticated users to /login - Redirect authenticated users from auth routes to /dashboard - Handle session expiration with 401 callback - Set access token cookie for middleware - All tests passing (105 tests total)
514 lines
17 KiB
TypeScript
514 lines
17 KiB
TypeScript
/**
|
|
* Setup WiFi Screen Tests
|
|
* Tests WiFi network selection, batch sensor setup, and error handling
|
|
*/
|
|
|
|
import React from 'react';
|
|
import { render, fireEvent, waitFor } from '@testing-library/react-native';
|
|
import { useLocalSearchParams, router } from 'expo-router';
|
|
import SetupWiFiScreen from '../[id]/setup-wifi';
|
|
import { useBLE } from '@/contexts/BLEContext';
|
|
import { api } from '@/services/api';
|
|
import * as wifiPasswordStore from '@/services/wifiPasswordStore';
|
|
|
|
// Mock Alert
|
|
const mockAlert = jest.fn();
|
|
jest.mock('react-native/Libraries/Alert/Alert', () => ({
|
|
alert: mockAlert,
|
|
}));
|
|
|
|
// Mock dependencies
|
|
jest.mock('expo-router', () => ({
|
|
useLocalSearchParams: jest.fn(),
|
|
router: {
|
|
push: jest.fn(),
|
|
back: jest.fn(),
|
|
replace: jest.fn(),
|
|
},
|
|
}));
|
|
|
|
jest.mock('@/contexts/BLEContext', () => ({
|
|
useBLE: jest.fn(),
|
|
}));
|
|
|
|
jest.mock('@/services/api', () => ({
|
|
api: {
|
|
attachDeviceToBeneficiary: jest.fn(),
|
|
},
|
|
}));
|
|
|
|
jest.mock('@/services/wifiPasswordStore', () => ({
|
|
getAllWiFiPasswords: jest.fn(),
|
|
saveWiFiPassword: jest.fn(),
|
|
migrateFromAsyncStorage: jest.fn(),
|
|
}));
|
|
|
|
jest.mock('@/services/analytics', () => ({
|
|
analytics: {
|
|
trackSensorSetupStep: jest.fn(),
|
|
trackSensorSetupComplete: jest.fn(),
|
|
trackSensorSetupRetry: jest.fn(),
|
|
trackSensorSetupSkip: jest.fn(),
|
|
trackSensorSetupCancelled: jest.fn(),
|
|
},
|
|
}));
|
|
|
|
jest.mock('expo-device', () => ({
|
|
isDevice: false, // Simulate running in simulator
|
|
}));
|
|
|
|
describe('SetupWiFiScreen', () => {
|
|
const mockDevices = [
|
|
{ id: 'device-1', name: 'WP_497_81a14c', mac: '81A14C', wellId: 497 },
|
|
{ id: 'device-2', name: 'WP_498_82b25d', mac: '82B25D', wellId: 498 },
|
|
];
|
|
|
|
const mockWiFiNetworks = [
|
|
{ ssid: 'HomeNetwork', rssi: -45 },
|
|
{ ssid: 'GuestNetwork', rssi: -65 },
|
|
{ ssid: 'WeakSignal', rssi: -80 },
|
|
];
|
|
|
|
const mockConnectDevice = jest.fn();
|
|
const mockDisconnectDevice = jest.fn();
|
|
const mockGetWiFiList = jest.fn();
|
|
const mockSetWiFi = jest.fn();
|
|
const mockRebootDevice = jest.fn();
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
|
|
// Mock route params with serialized devices
|
|
(useLocalSearchParams as jest.Mock).mockReturnValue({
|
|
id: '1',
|
|
devices: JSON.stringify(mockDevices),
|
|
});
|
|
|
|
// Mock BLE context
|
|
(useBLE as jest.Mock).mockReturnValue({
|
|
connectDevice: mockConnectDevice,
|
|
disconnectDevice: mockDisconnectDevice,
|
|
getWiFiList: mockGetWiFiList,
|
|
setWiFi: mockSetWiFi,
|
|
rebootDevice: mockRebootDevice,
|
|
});
|
|
|
|
// Mock successful BLE operations
|
|
mockConnectDevice.mockResolvedValue(true);
|
|
mockGetWiFiList.mockResolvedValue(mockWiFiNetworks);
|
|
mockSetWiFi.mockResolvedValue(true);
|
|
mockRebootDevice.mockResolvedValue(true);
|
|
|
|
// Mock API
|
|
(api.attachDeviceToBeneficiary as jest.Mock).mockResolvedValue({ ok: true });
|
|
|
|
// Mock WiFi password store
|
|
(wifiPasswordStore.getAllWiFiPasswords as jest.Mock).mockResolvedValue({});
|
|
(wifiPasswordStore.saveWiFiPassword as jest.Mock).mockResolvedValue(undefined);
|
|
(wifiPasswordStore.migrateFromAsyncStorage as jest.Mock).mockResolvedValue(undefined);
|
|
});
|
|
|
|
describe('WiFi Network Loading', () => {
|
|
it('should display loading state while scanning for networks', async () => {
|
|
mockConnectDevice.mockImplementation(() => new Promise(resolve => setTimeout(() => resolve(true), 100)));
|
|
mockGetWiFiList.mockImplementation(() => new Promise(resolve => setTimeout(() => resolve(mockWiFiNetworks), 100)));
|
|
|
|
const { getByText } = render(<SetupWiFiScreen />);
|
|
|
|
expect(getByText('Scanning for WiFi networks...')).toBeTruthy();
|
|
});
|
|
|
|
it('should display WiFi networks after loading', async () => {
|
|
const { getByText, queryByText } = render(<SetupWiFiScreen />);
|
|
|
|
await waitFor(() => {
|
|
expect(queryByText('Scanning for WiFi networks...')).toBeNull();
|
|
});
|
|
|
|
expect(getByText('HomeNetwork')).toBeTruthy();
|
|
expect(getByText('GuestNetwork')).toBeTruthy();
|
|
expect(getByText('WeakSignal')).toBeTruthy();
|
|
});
|
|
|
|
it('should display network count in header', async () => {
|
|
const { getByText, queryByText } = render(<SetupWiFiScreen />);
|
|
|
|
await waitFor(() => {
|
|
expect(queryByText('Scanning for WiFi networks...')).toBeNull();
|
|
});
|
|
|
|
expect(getByText(/Available Networks \(3\)/)).toBeTruthy();
|
|
});
|
|
|
|
it('should display signal strength for each network', async () => {
|
|
const { getByText, queryByText } = render(<SetupWiFiScreen />);
|
|
|
|
await waitFor(() => {
|
|
expect(queryByText('Scanning for WiFi networks...')).toBeNull();
|
|
});
|
|
|
|
// Check for signal labels
|
|
expect(getByText(/Excellent/)).toBeTruthy(); // -45 dBm
|
|
expect(getByText(/Good/)).toBeTruthy(); // -65 dBm
|
|
expect(getByText(/Weak/)).toBeTruthy(); // -80 dBm
|
|
});
|
|
});
|
|
|
|
describe('Network Selection', () => {
|
|
it('should show password input when network is selected', async () => {
|
|
const { getByText, queryByText, getByPlaceholderText } = render(<SetupWiFiScreen />);
|
|
|
|
await waitFor(() => {
|
|
expect(queryByText('Scanning for WiFi networks...')).toBeNull();
|
|
});
|
|
|
|
fireEvent.press(getByText('HomeNetwork'));
|
|
|
|
expect(getByText('WiFi Password')).toBeTruthy();
|
|
expect(getByPlaceholderText('Enter password')).toBeTruthy();
|
|
});
|
|
|
|
it('should auto-fill saved password for known network', async () => {
|
|
(wifiPasswordStore.getAllWiFiPasswords as jest.Mock).mockResolvedValue({
|
|
'HomeNetwork': 'savedPassword123',
|
|
});
|
|
|
|
const { getByText, queryByText, getByDisplayValue } = render(<SetupWiFiScreen />);
|
|
|
|
await waitFor(() => {
|
|
expect(queryByText('Scanning for WiFi networks...')).toBeNull();
|
|
});
|
|
|
|
fireEvent.press(getByText('HomeNetwork'));
|
|
|
|
await waitFor(() => {
|
|
expect(getByDisplayValue('savedPassword123')).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it('should show saved password icon for networks with stored passwords', async () => {
|
|
(wifiPasswordStore.getAllWiFiPasswords as jest.Mock).mockResolvedValue({
|
|
'HomeNetwork': 'savedPassword123',
|
|
});
|
|
|
|
const { getByText, queryByText } = render(<SetupWiFiScreen />);
|
|
|
|
await waitFor(() => {
|
|
expect(queryByText('Scanning for WiFi networks...')).toBeNull();
|
|
});
|
|
|
|
// The key icon should be visible for HomeNetwork
|
|
// This would require checking for the Ionicons component
|
|
});
|
|
|
|
it('should toggle password visibility', async () => {
|
|
const { getByText, queryByText, getByPlaceholderText } = render(<SetupWiFiScreen />);
|
|
|
|
await waitFor(() => {
|
|
expect(queryByText('Scanning for WiFi networks...')).toBeNull();
|
|
});
|
|
|
|
fireEvent.press(getByText('HomeNetwork'));
|
|
|
|
const passwordInput = getByPlaceholderText('Enter password');
|
|
fireEvent.changeText(passwordInput, 'mypassword');
|
|
|
|
// Password should be hidden by default (secureTextEntry)
|
|
expect(passwordInput.props.secureTextEntry).toBe(true);
|
|
|
|
// Find and press the eye icon button to toggle visibility
|
|
// This would need the toggle button to have a testID
|
|
});
|
|
});
|
|
|
|
describe('Device Info Display', () => {
|
|
it('should display single device info when one sensor selected', async () => {
|
|
(useLocalSearchParams as jest.Mock).mockReturnValue({
|
|
id: '1',
|
|
devices: JSON.stringify([mockDevices[0]]),
|
|
});
|
|
|
|
const { getByText, queryByText } = render(<SetupWiFiScreen />);
|
|
|
|
await waitFor(() => {
|
|
expect(queryByText('Scanning for WiFi networks...')).toBeNull();
|
|
});
|
|
|
|
expect(getByText('WP_497_81a14c')).toBeTruthy();
|
|
expect(getByText('Well ID: 497')).toBeTruthy();
|
|
});
|
|
|
|
it('should display multiple device summary when multiple sensors selected', async () => {
|
|
const { getByText, queryByText } = render(<SetupWiFiScreen />);
|
|
|
|
await waitFor(() => {
|
|
expect(queryByText('Scanning for WiFi networks...')).toBeNull();
|
|
});
|
|
|
|
expect(getByText('2 Sensors Selected')).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe('Validation', () => {
|
|
it('should disable connect button when no password entered', async () => {
|
|
const { getByText, queryByText } = render(<SetupWiFiScreen />);
|
|
|
|
await waitFor(() => {
|
|
expect(queryByText('Scanning for WiFi networks...')).toBeNull();
|
|
});
|
|
|
|
fireEvent.press(getByText('HomeNetwork'));
|
|
|
|
const connectButton = getByText(/Connect/);
|
|
expect(connectButton.parent?.props.disabled || connectButton.props.disabled).toBeTruthy();
|
|
});
|
|
|
|
it('should enable connect button when password is entered', async () => {
|
|
const { getByText, queryByText, getByPlaceholderText } = render(<SetupWiFiScreen />);
|
|
|
|
await waitFor(() => {
|
|
expect(queryByText('Scanning for WiFi networks...')).toBeNull();
|
|
});
|
|
|
|
fireEvent.press(getByText('HomeNetwork'));
|
|
fireEvent.changeText(getByPlaceholderText('Enter password'), 'validpassword');
|
|
|
|
// Connect button should now be enabled
|
|
});
|
|
|
|
it('should show validation error for short password', async () => {
|
|
const { getByText, queryByText, getByPlaceholderText } = render(<SetupWiFiScreen />);
|
|
|
|
await waitFor(() => {
|
|
expect(queryByText('Scanning for WiFi networks...')).toBeNull();
|
|
});
|
|
|
|
fireEvent.press(getByText('HomeNetwork'));
|
|
fireEvent.changeText(getByPlaceholderText('Enter password'), 'short');
|
|
|
|
// Try to connect with short password
|
|
fireEvent.press(getByText(/Connect/));
|
|
|
|
await waitFor(() => {
|
|
expect(mockAlert).toHaveBeenCalledWith(
|
|
'Invalid WiFi Credentials',
|
|
expect.stringContaining('8-63 characters')
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Batch Setup Process', () => {
|
|
it('should show progress screen during batch setup', async () => {
|
|
const { getByText, queryByText, getByPlaceholderText } = render(<SetupWiFiScreen />);
|
|
|
|
await waitFor(() => {
|
|
expect(queryByText('Scanning for WiFi networks...')).toBeNull();
|
|
});
|
|
|
|
fireEvent.press(getByText('HomeNetwork'));
|
|
fireEvent.changeText(getByPlaceholderText('Enter password'), 'validpassword123');
|
|
fireEvent.press(getByText(/Connect All/));
|
|
|
|
await waitFor(() => {
|
|
expect(getByText('Setting Up Sensors')).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it('should show step progress for each sensor', async () => {
|
|
const { getByText, queryByText, getByPlaceholderText } = render(<SetupWiFiScreen />);
|
|
|
|
await waitFor(() => {
|
|
expect(queryByText('Scanning for WiFi networks...')).toBeNull();
|
|
});
|
|
|
|
fireEvent.press(getByText('HomeNetwork'));
|
|
fireEvent.changeText(getByPlaceholderText('Enter password'), 'validpassword123');
|
|
fireEvent.press(getByText(/Connect All/));
|
|
|
|
await waitFor(() => {
|
|
// Should show connecting steps
|
|
expect(queryByText(/Connected|Connecting/)).toBeTruthy();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Error Handling', () => {
|
|
it('should show error when connection fails', async () => {
|
|
mockConnectDevice.mockRejectedValue(new Error('Connection failed'));
|
|
|
|
const { getByText, queryByText } = render(<SetupWiFiScreen />);
|
|
|
|
await waitFor(() => {
|
|
// Should show error alert
|
|
expect(mockAlert).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
it('should show retry and skip options on sensor failure', async () => {
|
|
mockConnectDevice.mockResolvedValueOnce(true);
|
|
mockSetWiFi.mockRejectedValueOnce(new Error('WiFi configuration failed'));
|
|
|
|
const { getByText, queryByText, getByPlaceholderText } = render(<SetupWiFiScreen />);
|
|
|
|
await waitFor(() => {
|
|
expect(queryByText('Scanning for WiFi networks...')).toBeNull();
|
|
});
|
|
|
|
fireEvent.press(getByText('HomeNetwork'));
|
|
fireEvent.changeText(getByPlaceholderText('Enter password'), 'validpassword123');
|
|
fireEvent.press(getByText(/Connect All/));
|
|
|
|
// Should show error with retry/skip options
|
|
await waitFor(() => {
|
|
expect(queryByText(/Retry|Skip/)).toBeTruthy();
|
|
}, { timeout: 10000 });
|
|
});
|
|
|
|
it('should handle API attachment error', async () => {
|
|
(api.attachDeviceToBeneficiary as jest.Mock).mockResolvedValue({
|
|
ok: false,
|
|
error: { message: 'Device already attached to another beneficiary' },
|
|
});
|
|
|
|
// Setup and trigger batch process
|
|
// Verify error is shown
|
|
});
|
|
});
|
|
|
|
describe('Results Screen', () => {
|
|
it('should show results after all sensors processed', async () => {
|
|
const { getByText, queryByText, getByPlaceholderText } = render(<SetupWiFiScreen />);
|
|
|
|
await waitFor(() => {
|
|
expect(queryByText('Scanning for WiFi networks...')).toBeNull();
|
|
});
|
|
|
|
fireEvent.press(getByText('HomeNetwork'));
|
|
fireEvent.changeText(getByPlaceholderText('Enter password'), 'validpassword123');
|
|
fireEvent.press(getByText(/Connect All/));
|
|
|
|
// Wait for setup to complete
|
|
await waitFor(() => {
|
|
expect(queryByText('Setup Complete') || queryByText('Done')).toBeTruthy();
|
|
}, { timeout: 15000 });
|
|
});
|
|
|
|
it('should show success count on results screen', async () => {
|
|
// After successful setup, should show how many succeeded
|
|
});
|
|
|
|
it('should navigate to equipment screen when Done is pressed', async () => {
|
|
// Complete setup and press Done
|
|
// Verify router.replace is called with equipment route
|
|
});
|
|
});
|
|
|
|
describe('Navigation', () => {
|
|
it('should go back and disconnect when back button pressed', async () => {
|
|
const { getByText, queryByText } = render(<SetupWiFiScreen />);
|
|
|
|
await waitFor(() => {
|
|
expect(queryByText('Scanning for WiFi networks...')).toBeNull();
|
|
});
|
|
|
|
// Press back button (arrow)
|
|
// Verify disconnectDevice is called for each device
|
|
// Verify router.back is called
|
|
});
|
|
|
|
it('should show cancel confirmation during active setup', async () => {
|
|
const { getByText, queryByText, getByPlaceholderText } = render(<SetupWiFiScreen />);
|
|
|
|
await waitFor(() => {
|
|
expect(queryByText('Scanning for WiFi networks...')).toBeNull();
|
|
});
|
|
|
|
fireEvent.press(getByText('HomeNetwork'));
|
|
fireEvent.changeText(getByPlaceholderText('Enter password'), 'validpassword123');
|
|
fireEvent.press(getByText(/Connect All/));
|
|
|
|
// During setup, trying to cancel should show confirmation
|
|
});
|
|
});
|
|
|
|
describe('Empty Network List', () => {
|
|
it('should show empty state when no networks found', async () => {
|
|
mockGetWiFiList.mockResolvedValue([]);
|
|
|
|
const { getByText, queryByText } = render(<SetupWiFiScreen />);
|
|
|
|
await waitFor(() => {
|
|
expect(queryByText('Scanning for WiFi networks...')).toBeNull();
|
|
});
|
|
|
|
expect(getByText('No WiFi networks found')).toBeTruthy();
|
|
expect(getByText('Try Again')).toBeTruthy();
|
|
});
|
|
|
|
it('should refresh networks when Try Again is pressed', async () => {
|
|
mockGetWiFiList.mockResolvedValueOnce([]).mockResolvedValueOnce(mockWiFiNetworks);
|
|
|
|
const { getByText, queryByText } = render(<SetupWiFiScreen />);
|
|
|
|
await waitFor(() => {
|
|
expect(getByText('No WiFi networks found')).toBeTruthy();
|
|
});
|
|
|
|
fireEvent.press(getByText('Try Again'));
|
|
|
|
await waitFor(() => {
|
|
expect(getByText('HomeNetwork')).toBeTruthy();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('WiFi Password Store Integration', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
|
|
(useLocalSearchParams as jest.Mock).mockReturnValue({
|
|
id: '1',
|
|
devices: JSON.stringify([{ id: 'device-1', name: 'WP_497', mac: '81A14C', wellId: 497 }]),
|
|
});
|
|
|
|
(useBLE as jest.Mock).mockReturnValue({
|
|
connectDevice: jest.fn().mockResolvedValue(true),
|
|
disconnectDevice: jest.fn(),
|
|
getWiFiList: jest.fn().mockResolvedValue([{ ssid: 'TestNetwork', rssi: -50 }]),
|
|
setWiFi: jest.fn().mockResolvedValue(true),
|
|
rebootDevice: jest.fn().mockResolvedValue(true),
|
|
});
|
|
|
|
(api.attachDeviceToBeneficiary as jest.Mock).mockResolvedValue({ ok: true });
|
|
(wifiPasswordStore.migrateFromAsyncStorage as jest.Mock).mockResolvedValue(undefined);
|
|
});
|
|
|
|
it('should save password after successful setup', async () => {
|
|
(wifiPasswordStore.getAllWiFiPasswords as jest.Mock).mockResolvedValue({});
|
|
|
|
const { getByText, queryByText, getByPlaceholderText } = render(<SetupWiFiScreen />);
|
|
|
|
await waitFor(() => {
|
|
expect(queryByText('Scanning for WiFi networks...')).toBeNull();
|
|
});
|
|
|
|
fireEvent.press(getByText('TestNetwork'));
|
|
fireEvent.changeText(getByPlaceholderText('Enter password'), 'newpassword123');
|
|
fireEvent.press(getByText(/Connect/));
|
|
|
|
await waitFor(() => {
|
|
expect(wifiPasswordStore.saveWiFiPassword).toHaveBeenCalledWith('TestNetwork', 'newpassword123');
|
|
});
|
|
});
|
|
|
|
it('should migrate passwords from AsyncStorage on mount', async () => {
|
|
render(<SetupWiFiScreen />);
|
|
|
|
await waitFor(() => {
|
|
expect(wifiPasswordStore.migrateFromAsyncStorage).toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|