LoginPage.test.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. /**
  2. * Tests for the LoginPage component.
  3. */
  4. import { describe, it, expect, beforeEach } from 'vitest';
  5. import { screen, waitFor } from '@testing-library/react';
  6. import userEvent from '@testing-library/user-event';
  7. import { render } from '../utils';
  8. import { LoginPage } from '../../pages/LoginPage';
  9. import { http, HttpResponse } from 'msw';
  10. import { server } from '../mocks/server';
  11. describe('LoginPage', () => {
  12. beforeEach(() => {
  13. server.use(
  14. http.get('/api/v1/auth/status', () => {
  15. return HttpResponse.json({ auth_enabled: true, requires_setup: false });
  16. })
  17. );
  18. });
  19. describe('rendering', () => {
  20. it('renders the login form', async () => {
  21. render(<LoginPage />);
  22. await waitFor(() => {
  23. expect(screen.getByRole('heading', { name: /Bambuddy Login/i })).toBeInTheDocument();
  24. });
  25. expect(screen.getByLabelText(/Username/i)).toBeInTheDocument();
  26. expect(screen.getByLabelText(/Password/i)).toBeInTheDocument();
  27. expect(screen.getByRole('button', { name: /Sign in/i })).toBeInTheDocument();
  28. });
  29. it('renders the sign in description', async () => {
  30. render(<LoginPage />);
  31. await waitFor(() => {
  32. expect(screen.getByText(/Sign in to your account/i)).toBeInTheDocument();
  33. });
  34. });
  35. });
  36. describe('form validation', () => {
  37. it('shows error when submitting empty form', async () => {
  38. const user = userEvent.setup();
  39. render(<LoginPage />);
  40. await waitFor(() => {
  41. expect(screen.getByRole('button', { name: /Sign in/i })).toBeInTheDocument();
  42. });
  43. await user.click(screen.getByRole('button', { name: /Sign in/i }));
  44. // The form has required fields, so HTML5 validation should prevent submission
  45. // or the component shows a toast
  46. });
  47. it('allows entering username and password', async () => {
  48. const user = userEvent.setup();
  49. render(<LoginPage />);
  50. await waitFor(() => {
  51. expect(screen.getByLabelText(/Username/i)).toBeInTheDocument();
  52. });
  53. await user.type(screen.getByLabelText(/Username/i), 'testuser');
  54. await user.type(screen.getByLabelText(/Password/i), 'testpassword');
  55. expect(screen.getByLabelText(/Username/i)).toHaveValue('testuser');
  56. expect(screen.getByLabelText(/Password/i)).toHaveValue('testpassword');
  57. });
  58. });
  59. describe('login flow', () => {
  60. it('submits login request with credentials', async () => {
  61. const user = userEvent.setup();
  62. let loginCalled = false;
  63. server.use(
  64. http.post('/api/v1/auth/login', async ({ request }) => {
  65. loginCalled = true;
  66. const body = await request.json() as { username: string; password: string };
  67. if (body.username === 'validuser' && body.password === 'validpass') {
  68. return HttpResponse.json({
  69. access_token: 'test-token',
  70. token_type: 'bearer',
  71. user: {
  72. id: 1,
  73. username: 'validuser',
  74. role: 'admin',
  75. is_active: true,
  76. created_at: new Date().toISOString(),
  77. },
  78. });
  79. }
  80. return HttpResponse.json(
  81. { detail: 'Incorrect username or password' },
  82. { status: 401 }
  83. );
  84. })
  85. );
  86. render(<LoginPage />);
  87. await waitFor(() => {
  88. expect(screen.getByLabelText(/Username/i)).toBeInTheDocument();
  89. });
  90. await user.type(screen.getByLabelText(/Username/i), 'validuser');
  91. await user.type(screen.getByLabelText(/Password/i), 'validpass');
  92. await user.click(screen.getByRole('button', { name: /Sign in/i }));
  93. // Verify the login endpoint was called
  94. await waitFor(() => {
  95. expect(loginCalled).toBe(true);
  96. });
  97. });
  98. it('shows loading state during login', async () => {
  99. const user = userEvent.setup();
  100. let resolveLogin: () => void;
  101. const loginPromise = new Promise<void>(resolve => { resolveLogin = resolve; });
  102. // Slow login endpoint that we control
  103. server.use(
  104. http.post('/api/v1/auth/login', async () => {
  105. await loginPromise;
  106. return HttpResponse.json({
  107. access_token: 'test-token',
  108. token_type: 'bearer',
  109. user: {
  110. id: 1,
  111. username: 'testuser',
  112. role: 'admin',
  113. is_active: true,
  114. created_at: new Date().toISOString(),
  115. },
  116. });
  117. })
  118. );
  119. render(<LoginPage />);
  120. await waitFor(() => {
  121. expect(screen.getByLabelText(/Username/i)).toBeInTheDocument();
  122. });
  123. await user.type(screen.getByLabelText(/Username/i), 'testuser');
  124. await user.type(screen.getByLabelText(/Password/i), 'testpass');
  125. await user.click(screen.getByRole('button', { name: /Sign in/i }));
  126. // Check for loading state - button text should change to "Logging in..."
  127. await waitFor(() => {
  128. expect(screen.getByRole('button', { name: /Logging in/i })).toBeInTheDocument();
  129. });
  130. // Release the login request
  131. resolveLogin!();
  132. });
  133. });
  134. });