Skip to content

Commit

Permalink
ryan/feat(ux: create input loading spinner) (#399)
Browse files Browse the repository at this point in the history
https://github.com/user-attachments/assets/0577e7e5-846f-4b11-b139-f9ac67ad5109

I created the `LoadingSpinner` component which is designed to be used in
buttons when sending data to the database. It has a
`min-width/max-width` and `min-height/max-height` equal to tailwind's
`w-7/w-9` and `h-7/h-9` so it fits within the smallest and largest size
variants of the `Button` component.

- [X] When a user clicks a submit button, the text should be replaced by
a spinner
- [X] When a user clicks a submit button, the button should be disabled
from being clicked again

Closes #91
  • Loading branch information
shashilo authored Sep 20, 2024
2 parents 2356f5c + f910e60 commit 36b4ab6
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 96 deletions.
85 changes: 64 additions & 21 deletions app/(main)/login/page.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import Login from './page';
import React, { useState as useStateMock } from 'react';

const getUser = jest.fn();
const mockLogin = jest.fn();
const mockPush = jest.fn();
const getUser = jest.fn();
const setIsLoading = jest.fn();

jest.mock('react', () => ({
...jest.requireActual('react'),
useState: jest.fn(),
}));

let continueButton: HTMLElement,
emailInput: HTMLInputElement,
Expand Down Expand Up @@ -33,44 +40,47 @@ jest.mock('../../../context/AuthContextProvider', () => ({
}));

describe('Login', () => {
const setIsLoading = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
jest
.spyOn(React, 'useState')
.mockImplementation(() => [false, setIsLoading]);

render(<Login />);

continueButton = screen.getByTestId('continue-button');
emailInput = screen.getByTestId('email');
passwordInput = screen.getByTestId('password');
continueButton = screen.getByTestId('continue-button');
});
test('should render the login page', () => {
it('should render the login page', () => {
expect(continueButton).toBeInTheDocument();
expect(emailInput).toBeInTheDocument();
expect(passwordInput).toBeInTheDocument();
expect(continueButton).toBeInTheDocument();
});

test('should update email state when input value changes', () => {
fireEvent.change(emailInput, { target: { value: '[email protected]' } });
expect(emailInput).toHaveValue('[email protected]');
});
it('should update email and password fields and submit form', async () => {
const form = screen.getByTestId('login-form');

test('should update password state when input value changes', () => {
fireEvent.change(passwordInput, { target: { value: 'password123' } });
expect(passwordInput).toHaveValue('password123');
});
await act(async () => {
fireEvent.change(emailInput, { target: { value: '[email protected]' } });
fireEvent.change(passwordInput, { target: { value: 'password123' } });
});

test('should call loginAccount function with email and password when continue button is clicked', async () => {
fireEvent.change(emailInput, { target: { value: '[email protected]' } });
fireEvent.change(passwordInput, { target: { value: 'password123' } });
fireEvent.click(continueButton);
await act(async () => {
fireEvent.submit(form);
});

await waitFor(() => {
expect(mockLogin).toHaveBeenCalledTimes(1);
expect(mockLogin).toHaveBeenCalledWith({
email: '[email protected]',
password: 'password123',
});
});
});

test('redirects to /weeklyPicks when the button is clicked', () => {
it('redirects to /weeklyPicks when the button is clicked', () => {
mockUseAuthContext.isSignedIn = true;

render(<Login />);
Expand All @@ -79,11 +89,44 @@ describe('Login', () => {
mockUseAuthContext.isSignedIn = false;
});

test('redirects to /league/all when user navigates to /login', async () => {
it('redirects to /league/all when user navigates to /login', async () => {
mockUseAuthContext.isSignedIn = true;

act(() => {
render(<Login />);
});

await waitFor(() => {
expect(mockPush).toHaveBeenCalledWith('/league/all');
});

mockUseAuthContext.isSignedIn = false;
});
});

describe('Login loading spinner', () => {
it('should show the loading spinner', async () => {
(useStateMock as jest.Mock).mockImplementation((init: boolean) => [
true,
setIsLoading,
]);

render(<Login />);

expect(mockPush).toHaveBeenCalledWith('/league/all');
await waitFor(() => {
expect(screen.queryByTestId('loading-spinner')).toBeInTheDocument();
});
});
it('should not show the loading spinner', async () => {
(useStateMock as jest.Mock).mockImplementation((init: boolean) => [
false,
setIsLoading,
]);

render(<Login />);

await waitFor(() => {
expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument();
});
});
});
35 changes: 25 additions & 10 deletions app/(main)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ import {
Form,
FormControl,
FormField,
FormItem,
FormItem,
FormMessage,
} from '../../../components/Form/Form';
import { Input } from '@/components/Input/Input';
import { useAuthContext } from '@/context/AuthContextProvider';
import { useRouter } from 'next/navigation';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import LinkCustom from '@/components/LinkCustom/LinkCustom';
import LoadingSpinner from '@/components/LoadingSpinner/LoadingSpinner';
import Logo from '@/components/Logo/Logo';
import logo from '@/public/assets/logo-colored-outline.svg';
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';

/**
* The schema for the login form.
Expand Down Expand Up @@ -82,10 +82,18 @@ const Login = (): React.JSX.Element => {
* Handles the form submission.
* @param {LoginUserSchemaType} data - The data from the form.
*/
const onSubmit: SubmitHandler<LoginUserSchemaType> = async (data) => {
setIsLoading(true);
await login(data);
setIsLoading(false);
const onSubmit: SubmitHandler<LoginUserSchemaType> = async (
data: LoginUserSchemaType,
): Promise<void> => {
try {
setIsLoading(true);
await login(data);
} catch (error) {
console.error('Login error:', error);
throw new Error('An error occurred while logging in');
} finally {
setIsLoading(false);
}
};

return (
Expand Down Expand Up @@ -115,6 +123,7 @@ const Login = (): React.JSX.Element => {
<form
id="input-container"
className="grid gap-3"
data-testid="login-form"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
Expand All @@ -130,7 +139,7 @@ const Login = (): React.JSX.Element => {
{...field}
/>
</FormControl>
{form.formState.errors.email && (
{form.formState.errors?.email && (
<FormMessage>
{form.formState.errors.email.message}
</FormMessage>
Expand All @@ -151,17 +160,23 @@ const Login = (): React.JSX.Element => {
{...field}
/>
</FormControl>
{form.formState.errors.password && (
{form.formState.errors?.password && (
<FormMessage>
{form.formState.errors.password.message}
{form.formState.errors?.password.message}
</FormMessage>
)}
</FormItem>
)}
/>
<Button
data-testid="continue-button"
label={isLoading ? <LoadingSpinner /> : 'Continue'}
label={
isLoading ? (
<LoadingSpinner data-testid="loading-spinner" />
) : (
'Continue'
)
}
type="submit"
disabled={!email || !password || isLoading}
/>
Expand Down
Loading

0 comments on commit 36b4ab6

Please sign in to comment.