mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:41:09 +02:00
first implementation
This commit is contained in:
parent
98efa6f6e8
commit
74dc6892ab
61 changed files with 30899 additions and 4934 deletions
308
docs/test-examples/mobile/ExampleComponent.test.tsx
Normal file
308
docs/test-examples/mobile/ExampleComponent.test.tsx
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
/**
|
||||
* Example React Native Component Test
|
||||
*
|
||||
* This demonstrates best practices for testing React Native components:
|
||||
* - Render testing
|
||||
* - User interaction testing
|
||||
* - State changes
|
||||
* - Props validation
|
||||
* - Accessibility testing
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, fireEvent, waitFor, screen } from '@testing-library/react-native';
|
||||
import { ExampleComponent } from '../ExampleComponent';
|
||||
|
||||
describe('ExampleComponent', () => {
|
||||
// Mock data
|
||||
const mockOnPress = jest.fn();
|
||||
const mockOnLongPress = jest.fn();
|
||||
const defaultProps = {
|
||||
title: 'Test Title',
|
||||
description: 'Test Description',
|
||||
onPress: mockOnPress,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render with required props', () => {
|
||||
const { getByText } = render(<ExampleComponent {...defaultProps} />);
|
||||
|
||||
expect(getByText('Test Title')).toBeTruthy();
|
||||
expect(getByText('Test Description')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with testID for automation', () => {
|
||||
const { getByTestId } = render(<ExampleComponent {...defaultProps} testID="example-component" />);
|
||||
|
||||
expect(getByTestId('example-component')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render loading state', () => {
|
||||
const { getByTestId, queryByText } = render(<ExampleComponent {...defaultProps} loading />);
|
||||
|
||||
expect(getByTestId('loading-indicator')).toBeTruthy();
|
||||
expect(queryByText('Test Title')).toBeNull(); // Content hidden when loading
|
||||
});
|
||||
|
||||
it('should render error state', () => {
|
||||
const errorMessage = 'Something went wrong';
|
||||
const { getByText } = render(<ExampleComponent {...defaultProps} error={errorMessage} />);
|
||||
|
||||
expect(getByText(errorMessage)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render optional icon when provided', () => {
|
||||
const { getByTestId } = render(<ExampleComponent {...defaultProps} icon="star" />);
|
||||
|
||||
expect(getByTestId('icon-star')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not render description when not provided', () => {
|
||||
const { queryByText } = render(<ExampleComponent title="Title Only" onPress={mockOnPress} />);
|
||||
|
||||
expect(queryByText('Test Description')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call onPress when pressed', () => {
|
||||
const { getByText } = render(<ExampleComponent {...defaultProps} />);
|
||||
|
||||
fireEvent.press(getByText('Test Title'));
|
||||
|
||||
expect(mockOnPress).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call onLongPress when long pressed', () => {
|
||||
const { getByText } = render(<ExampleComponent {...defaultProps} onLongPress={mockOnLongPress} />);
|
||||
|
||||
fireEvent(getByText('Test Title'), 'onLongPress');
|
||||
|
||||
expect(mockOnLongPress).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not call onPress when disabled', () => {
|
||||
const { getByText } = render(<ExampleComponent {...defaultProps} disabled />);
|
||||
|
||||
fireEvent.press(getByText('Test Title'));
|
||||
|
||||
expect(mockOnPress).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not call onPress when loading', () => {
|
||||
const { getByTestId } = render(
|
||||
<ExampleComponent {...defaultProps} loading testID="example-component" />
|
||||
);
|
||||
|
||||
fireEvent.press(getByTestId('example-component'));
|
||||
|
||||
expect(mockOnPress).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show feedback on press (opacity change)', async () => {
|
||||
const { getByText } = render(<ExampleComponent {...defaultProps} />);
|
||||
const touchable = getByText('Test Title').parent;
|
||||
|
||||
fireEvent(touchable, 'onPressIn');
|
||||
await waitFor(() => {
|
||||
expect(touchable.props.style).toMatchObject({
|
||||
opacity: 0.6, // Active opacity
|
||||
});
|
||||
});
|
||||
|
||||
fireEvent(touchable, 'onPressOut');
|
||||
await waitFor(() => {
|
||||
expect(touchable.props.style).toMatchObject({
|
||||
opacity: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('State Management', () => {
|
||||
it('should toggle favorite state on icon press', async () => {
|
||||
const { getByTestId, rerender } = render(<ExampleComponent {...defaultProps} favoritable />);
|
||||
|
||||
const favoriteIcon = getByTestId('favorite-icon');
|
||||
expect(favoriteIcon.props.name).toBe('heart-outline'); // Initial state
|
||||
|
||||
fireEvent.press(favoriteIcon);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(favoriteIcon.props.name).toBe('heart'); // Toggled state
|
||||
});
|
||||
});
|
||||
|
||||
it('should maintain expanded state across re-renders', async () => {
|
||||
const { getByTestId, rerender } = render(<ExampleComponent {...defaultProps} expandable />);
|
||||
|
||||
const expandButton = getByTestId('expand-button');
|
||||
fireEvent.press(expandButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('expanded-content')).toBeTruthy();
|
||||
});
|
||||
|
||||
// Re-render with updated props
|
||||
rerender(<ExampleComponent {...defaultProps} description="Updated Description" expandable />);
|
||||
|
||||
// Expanded state should persist
|
||||
expect(getByTestId('expanded-content')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Props Validation', () => {
|
||||
it('should handle empty title gracefully', () => {
|
||||
const { queryByText } = render(<ExampleComponent title="" onPress={mockOnPress} />);
|
||||
|
||||
expect(queryByText('')).toBeNull();
|
||||
});
|
||||
|
||||
it('should truncate long titles', () => {
|
||||
const longTitle = 'This is a very long title that should be truncated at some point';
|
||||
const { getByText } = render(<ExampleComponent title={longTitle} onPress={mockOnPress} />);
|
||||
|
||||
const titleElement = getByText(/This is a very long/);
|
||||
expect(titleElement.props.numberOfLines).toBe(1);
|
||||
expect(titleElement.props.ellipsizeMode).toBe('tail');
|
||||
});
|
||||
|
||||
it('should apply custom styles', () => {
|
||||
const customStyle = { backgroundColor: 'red', padding: 20 };
|
||||
const { getByTestId } = render(
|
||||
<ExampleComponent {...defaultProps} style={customStyle} testID="example-component" />
|
||||
);
|
||||
|
||||
const component = getByTestId('example-component');
|
||||
expect(component.props.style).toMatchObject(customStyle);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have accessible label', () => {
|
||||
const { getByLabelText } = render(<ExampleComponent {...defaultProps} />);
|
||||
|
||||
expect(getByLabelText('Test Title')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have accessible role', () => {
|
||||
const { getByRole } = render(<ExampleComponent {...defaultProps} />);
|
||||
|
||||
expect(getByRole('button')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have accessible hint', () => {
|
||||
const { getByA11yHint } = render(
|
||||
<ExampleComponent {...defaultProps} accessibilityHint="Double tap to open details" />
|
||||
);
|
||||
|
||||
expect(getByA11yHint('Double tap to open details')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should be disabled for screen readers when disabled', () => {
|
||||
const { getByTestId } = render(
|
||||
<ExampleComponent {...defaultProps} disabled testID="example-component" />
|
||||
);
|
||||
|
||||
const component = getByTestId('example-component');
|
||||
expect(component.props.accessibilityState).toMatchObject({
|
||||
disabled: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle rapid taps (debouncing)', async () => {
|
||||
jest.useFakeTimers();
|
||||
const { getByText } = render(<ExampleComponent {...defaultProps} />);
|
||||
|
||||
const button = getByText('Test Title');
|
||||
|
||||
// Rapid taps
|
||||
fireEvent.press(button);
|
||||
fireEvent.press(button);
|
||||
fireEvent.press(button);
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
// Should only call once due to debouncing
|
||||
expect(mockOnPress).toHaveBeenCalledTimes(1);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should handle null children gracefully', () => {
|
||||
const { container } = render(<ExampleComponent {...defaultProps}>{null}</ExampleComponent>);
|
||||
|
||||
expect(container).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should handle undefined props gracefully', () => {
|
||||
const { getByText } = render(<ExampleComponent title="Test" onPress={mockOnPress} description={undefined} />);
|
||||
|
||||
expect(getByText('Test')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance', () => {
|
||||
it('should not re-render unnecessarily', () => {
|
||||
const renderSpy = jest.fn();
|
||||
const ComponentWithSpy = (props) => {
|
||||
renderSpy();
|
||||
return <ExampleComponent {...props} />;
|
||||
};
|
||||
|
||||
const { rerender } = render(<ComponentWithSpy {...defaultProps} />);
|
||||
|
||||
expect(renderSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Re-render with same props
|
||||
rerender(<ComponentWithSpy {...defaultProps} />);
|
||||
|
||||
// Should use memo and not re-render
|
||||
expect(renderSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should only re-render when relevant props change', () => {
|
||||
const renderSpy = jest.fn();
|
||||
const ComponentWithSpy = (props) => {
|
||||
renderSpy();
|
||||
return <ExampleComponent {...props} />;
|
||||
};
|
||||
|
||||
const { rerender } = render(<ComponentWithSpy {...defaultProps} />);
|
||||
|
||||
expect(renderSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Re-render with different title
|
||||
rerender(<ComponentWithSpy {...defaultProps} title="New Title" />);
|
||||
|
||||
// Should re-render
|
||||
expect(renderSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Snapshot Testing', () => {
|
||||
it('should match snapshot for default state', () => {
|
||||
const { toJSON } = render(<ExampleComponent {...defaultProps} />);
|
||||
|
||||
expect(toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot for loading state', () => {
|
||||
const { toJSON } = render(<ExampleComponent {...defaultProps} loading />);
|
||||
|
||||
expect(toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot for error state', () => {
|
||||
const { toJSON } = render(<ExampleComponent {...defaultProps} error="Error message" />);
|
||||
|
||||
expect(toJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
342
docs/test-examples/mobile/authService.test.ts
Normal file
342
docs/test-examples/mobile/authService.test.ts
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
/**
|
||||
* Example React Native Service Test
|
||||
*
|
||||
* This demonstrates best practices for testing services:
|
||||
* - Mock fetch/API calls
|
||||
* - Test async operations
|
||||
* - Test error handling
|
||||
* - Test storage operations
|
||||
* - Use MSW for API mocking (optional)
|
||||
*/
|
||||
|
||||
import { authService } from '../authService';
|
||||
import { tokenManager } from '../tokenManager';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('expo-secure-store');
|
||||
jest.mock('../tokenManager');
|
||||
|
||||
// Mock data
|
||||
const mockTokens = {
|
||||
appToken: 'mock-app-token-12345',
|
||||
refreshToken: 'mock-refresh-token-12345',
|
||||
manaToken: 'mock-mana-token-12345',
|
||||
};
|
||||
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
email: 'test@example.com',
|
||||
};
|
||||
|
||||
describe('authService', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
global.fetch = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('signIn', () => {
|
||||
it('should sign in successfully with valid credentials', async () => {
|
||||
// Arrange
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
success: true,
|
||||
...mockTokens,
|
||||
user: mockUser,
|
||||
}),
|
||||
};
|
||||
|
||||
(global.fetch as jest.Mock).mockResolvedValue(mockResponse);
|
||||
(SecureStore.setItemAsync as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
// Act
|
||||
const result = await authService.signIn('test@example.com', 'password123');
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.user).toEqual(mockUser);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/auth/signin'),
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: expect.stringContaining('test@example.com'),
|
||||
})
|
||||
);
|
||||
|
||||
// Verify tokens were stored
|
||||
expect(SecureStore.setItemAsync).toHaveBeenCalledWith('@auth/appToken', mockTokens.appToken);
|
||||
expect(SecureStore.setItemAsync).toHaveBeenCalledWith('@auth/refreshToken', mockTokens.refreshToken);
|
||||
});
|
||||
|
||||
it('should handle invalid credentials error', async () => {
|
||||
// Arrange
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
status: 401,
|
||||
json: async () => ({
|
||||
success: false,
|
||||
error: 'INVALID_CREDENTIALS',
|
||||
}),
|
||||
};
|
||||
|
||||
(global.fetch as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
// Act
|
||||
const result = await authService.signIn('test@example.com', 'wrongpassword');
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('INVALID_CREDENTIALS');
|
||||
expect(SecureStore.setItemAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle network errors', async () => {
|
||||
// Arrange
|
||||
(global.fetch as jest.Mock).mockRejectedValue(new Error('Network request failed'));
|
||||
|
||||
// Act
|
||||
const result = await authService.signIn('test@example.com', 'password123');
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Network');
|
||||
});
|
||||
|
||||
it('should handle storage errors', async () => {
|
||||
// Arrange
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
success: true,
|
||||
...mockTokens,
|
||||
user: mockUser,
|
||||
}),
|
||||
};
|
||||
|
||||
(global.fetch as jest.Mock).mockResolvedValue(mockResponse);
|
||||
(SecureStore.setItemAsync as jest.Mock).mockRejectedValue(new Error('Storage unavailable'));
|
||||
|
||||
// Act
|
||||
const result = await authService.signIn('test@example.com', 'password123');
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Storage');
|
||||
});
|
||||
|
||||
it('should validate email format', async () => {
|
||||
// Act
|
||||
const result = await authService.signIn('invalid-email', 'password123');
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('email');
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should validate password is not empty', async () => {
|
||||
// Act
|
||||
const result = await authService.signIn('test@example.com', '');
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('password');
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle timeout errors', async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
// Arrange
|
||||
(global.fetch as jest.Mock).mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(() => resolve({ ok: true, json: async () => ({}) }), 60000);
|
||||
})
|
||||
);
|
||||
|
||||
// Act
|
||||
const resultPromise = authService.signIn('test@example.com', 'password123');
|
||||
|
||||
jest.advanceTimersByTime(30000); // Advance 30s (timeout threshold)
|
||||
const result = await resultPromise;
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('timeout');
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe('signOut', () => {
|
||||
it('should sign out successfully', async () => {
|
||||
// Arrange
|
||||
(SecureStore.deleteItemAsync as jest.Mock).mockResolvedValue(undefined);
|
||||
(tokenManager.clearTokens as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
// Act
|
||||
const result = await authService.signOut();
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(true);
|
||||
expect(SecureStore.deleteItemAsync).toHaveBeenCalledWith('@auth/appToken');
|
||||
expect(SecureStore.deleteItemAsync).toHaveBeenCalledWith('@auth/refreshToken');
|
||||
expect(tokenManager.clearTokens).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle storage errors during sign out', async () => {
|
||||
// Arrange
|
||||
(SecureStore.deleteItemAsync as jest.Mock).mockRejectedValue(new Error('Storage error'));
|
||||
|
||||
// Act
|
||||
const result = await authService.signOut();
|
||||
|
||||
// Assert
|
||||
// Should succeed even if storage fails (user intent matters)
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshToken', () => {
|
||||
it('should refresh token successfully', async () => {
|
||||
// Arrange
|
||||
const oldRefreshToken = 'old-refresh-token';
|
||||
const newTokens = {
|
||||
appToken: 'new-app-token',
|
||||
refreshToken: 'new-refresh-token',
|
||||
};
|
||||
|
||||
(SecureStore.getItemAsync as jest.Mock).mockResolvedValue(oldRefreshToken);
|
||||
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
success: true,
|
||||
...newTokens,
|
||||
}),
|
||||
};
|
||||
|
||||
(global.fetch as jest.Mock).mockResolvedValue(mockResponse);
|
||||
(SecureStore.setItemAsync as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
// Act
|
||||
const result = await authService.refreshToken();
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.appToken).toBe(newTokens.appToken);
|
||||
expect(SecureStore.setItemAsync).toHaveBeenCalledWith('@auth/appToken', newTokens.appToken);
|
||||
});
|
||||
|
||||
it('should handle missing refresh token', async () => {
|
||||
// Arrange
|
||||
(SecureStore.getItemAsync as jest.Mock).mockResolvedValue(null);
|
||||
|
||||
// Act
|
||||
const result = await authService.refreshToken();
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('No refresh token');
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle expired refresh token', async () => {
|
||||
// Arrange
|
||||
(SecureStore.getItemAsync as jest.Mock).mockResolvedValue('expired-refresh-token');
|
||||
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
status: 401,
|
||||
json: async () => ({
|
||||
success: false,
|
||||
error: 'REFRESH_TOKEN_EXPIRED',
|
||||
}),
|
||||
};
|
||||
|
||||
(global.fetch as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
// Act
|
||||
const result = await authService.refreshToken();
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('REFRESH_TOKEN_EXPIRED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkAuthStatus', () => {
|
||||
it('should return true when valid token exists', async () => {
|
||||
// Arrange
|
||||
(tokenManager.getValidToken as jest.Mock).mockResolvedValue('valid-token');
|
||||
|
||||
// Act
|
||||
const result = await authService.checkAuthStatus();
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when no token exists', async () => {
|
||||
// Arrange
|
||||
(tokenManager.getValidToken as jest.Mock).mockResolvedValue(null);
|
||||
|
||||
// Act
|
||||
const result = await authService.checkAuthStatus();
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should refresh expired token automatically', async () => {
|
||||
// Arrange
|
||||
(tokenManager.getValidToken as jest.Mock)
|
||||
.mockResolvedValueOnce(null) // First call: no valid token
|
||||
.mockResolvedValueOnce('new-valid-token'); // After refresh
|
||||
|
||||
(authService.refreshToken as jest.Mock) = jest.fn().mockResolvedValue({
|
||||
success: true,
|
||||
appToken: 'new-valid-token',
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await authService.checkAuthStatus();
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
expect(authService.refreshToken).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration with TokenManager', () => {
|
||||
it('should notify TokenManager of new tokens', async () => {
|
||||
// Arrange
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
success: true,
|
||||
...mockTokens,
|
||||
user: mockUser,
|
||||
}),
|
||||
};
|
||||
|
||||
(global.fetch as jest.Mock).mockResolvedValue(mockResponse);
|
||||
(SecureStore.setItemAsync as jest.Mock).mockResolvedValue(undefined);
|
||||
(tokenManager.setTokens as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
// Act
|
||||
await authService.signIn('test@example.com', 'password123');
|
||||
|
||||
// Assert
|
||||
expect(tokenManager.setTokens).toHaveBeenCalledWith(mockTokens);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue