refactor: replace usages of custom toaster with react-hot-toast

This commit is contained in:
Nicolas Meienberger 2023-04-08 18:34:30 +02:00
parent d2fda696d7
commit 9fa8452e24
16 changed files with 70 additions and 179 deletions

View file

@ -1,9 +1,8 @@
import React from 'react';
import { fireEvent, render, renderHook, screen, waitFor } from '../../../../../../tests/test-utils';
import { fireEvent, render, screen, waitFor } from '../../../../../../tests/test-utils';
import { createAppEntity } from '../../../../mocks/fixtures/app.fixtures';
import { getTRPCMock, getTRPCMockError } from '../../../../mocks/getTrpcMock';
import { server } from '../../../../mocks/server';
import { useToastStore } from '../../../../state/toastStore';
import { AppDetailsContainer } from './AppDetailsContainer';
describe('Test: AppDetailsContainer', () => {
@ -112,7 +111,6 @@ describe('Test: AppDetailsContainer', () => {
// Arrange
const app = createAppEntity({ overrides: { status: 'missing' } });
server.use(getTRPCMock({ path: ['app', 'installApp'], type: 'mutation', response: app }));
const { result } = renderHook(() => useToastStore());
render(<AppDetailsContainer app={app} />);
const openModalButton = screen.getByRole('button', { name: 'Install' });
fireEvent.click(openModalButton);
@ -122,15 +120,12 @@ describe('Test: AppDetailsContainer', () => {
fireEvent.click(installButton);
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0].status).toEqual('success');
expect(result.current.toasts[0].title).toEqual('App installed successfully');
expect(screen.getByText('App installed successfully')).toBeInTheDocument();
});
});
it('should display a toast error when install mutation fails', async () => {
// Arrange
const { result } = renderHook(() => useToastStore());
server.use(
getTRPCMockError({
path: ['app', 'installApp'],
@ -149,9 +144,7 @@ describe('Test: AppDetailsContainer', () => {
fireEvent.click(installButton);
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0].description).toEqual('my big error');
expect(result.current.toasts[0].status).toEqual('error');
expect(screen.getByText('Failed to install app: my big error')).toBeInTheDocument();
});
});
});
@ -161,7 +154,6 @@ describe('Test: AppDetailsContainer', () => {
// Arrange
const app = createAppEntity({ overrides: { version: 2, latestVersion: 3 } });
server.use(getTRPCMock({ path: ['app', 'updateApp'], type: 'mutation', response: app }));
const { result } = renderHook(() => useToastStore());
render(<AppDetailsContainer app={app} />);
const openModalButton = screen.getByRole('button', { name: 'Update' });
fireEvent.click(openModalButton);
@ -171,15 +163,12 @@ describe('Test: AppDetailsContainer', () => {
modalUpdateButton.click();
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0].status).toEqual('success');
expect(result.current.toasts[0].title).toEqual('App updated successfully');
expect(screen.getByText('App updated successfully')).toBeInTheDocument();
});
});
it('should display a toast error when update mutation fails', async () => {
// Arrange
const { result } = renderHook(() => useToastStore());
server.use(getTRPCMockError({ path: ['app', 'updateApp'], type: 'mutation', message: 'my big error' }));
const app = createAppEntity({ overrides: { version: 2, latestVersion: 3 }, overridesInfo: { tipi_version: 3 } });
render(<AppDetailsContainer app={app} />);
@ -192,9 +181,7 @@ describe('Test: AppDetailsContainer', () => {
// Assert
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0].description).toEqual('my big error');
expect(result.current.toasts[0].status).toEqual('error');
expect(screen.getByText('Failed to update app: my big error')).toBeInTheDocument();
});
});
});
@ -204,7 +191,6 @@ describe('Test: AppDetailsContainer', () => {
// Arrange
const app = createAppEntity({ status: 'stopped' });
server.use(getTRPCMock({ path: ['app', 'uninstallApp'], type: 'mutation', response: { id: app.id, config: {}, status: 'missing' } }));
const { result } = renderHook(() => useToastStore());
render(<AppDetailsContainer app={app} />);
const openModalButton = screen.getByRole('button', { name: 'Remove' });
fireEvent.click(openModalButton);
@ -215,15 +201,12 @@ describe('Test: AppDetailsContainer', () => {
// Assert
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0].status).toEqual('success');
expect(result.current.toasts[0].title).toEqual('App uninstalled successfully');
expect(screen.getByText('App uninstalled successfully')).toBeInTheDocument();
});
});
it('should display a toast error when uninstall mutation fails', async () => {
// Arrange
const { result } = renderHook(() => useToastStore());
server.use(getTRPCMockError({ path: ['app', 'uninstallApp'], type: 'mutation', message: 'my big error' }));
const app = createAppEntity({ status: 'stopped' });
render(<AppDetailsContainer app={app} />);
@ -236,9 +219,7 @@ describe('Test: AppDetailsContainer', () => {
// Assert
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0].description).toEqual('my big error');
expect(result.current.toasts[0].status).toEqual('error');
expect(screen.getByText('Failed to uninstall app: my big error')).toBeInTheDocument();
});
});
});
@ -248,7 +229,6 @@ describe('Test: AppDetailsContainer', () => {
// Arrange
const app = createAppEntity({ status: 'stopped' });
server.use(getTRPCMock({ path: ['app', 'startApp'], type: 'mutation', response: app }));
const { result } = renderHook(() => useToastStore());
render(<AppDetailsContainer app={app} />);
// Act
@ -257,15 +237,12 @@ describe('Test: AppDetailsContainer', () => {
// Assert
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0].status).toEqual('success');
expect(result.current.toasts[0].title).toEqual('App started successfully');
expect(screen.getByText('App started successfully')).toBeInTheDocument();
});
});
it('should display a toast error when start mutation fails', async () => {
// Arrange
const { result } = renderHook(() => useToastStore());
server.use(getTRPCMockError({ path: ['app', 'startApp'], type: 'mutation', message: 'my big error' }));
const app = createAppEntity({ status: 'stopped' });
render(<AppDetailsContainer app={app} />);
@ -276,9 +253,7 @@ describe('Test: AppDetailsContainer', () => {
// Assert
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0].description).toEqual('my big error');
expect(result.current.toasts[0].status).toEqual('error');
expect(screen.getByText('Failed to start app: my big error')).toBeInTheDocument();
});
});
});
@ -288,7 +263,6 @@ describe('Test: AppDetailsContainer', () => {
// Arrange
const app = createAppEntity({ status: 'running' });
server.use(getTRPCMock({ path: ['app', 'stopApp'], type: 'mutation', response: app }));
const { result } = renderHook(() => useToastStore());
render(<AppDetailsContainer app={app} />);
const openModalButton = screen.getByRole('button', { name: 'Stop' });
fireEvent.click(openModalButton);
@ -299,15 +273,12 @@ describe('Test: AppDetailsContainer', () => {
// Assert
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0].status).toEqual('success');
expect(result.current.toasts[0].title).toEqual('App stopped successfully');
expect(screen.getByText('App stopped successfully')).toBeInTheDocument();
});
});
it('should display a toast error when stop mutation fails', async () => {
// Arrange
const { result } = renderHook(() => useToastStore());
server.use(getTRPCMockError({ path: ['app', 'stopApp'], type: 'mutation', message: 'my big error' }));
const app = createAppEntity({ status: 'running' });
render(<AppDetailsContainer app={app} />);
@ -320,9 +291,7 @@ describe('Test: AppDetailsContainer', () => {
// Assert
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0].description).toEqual('my big error');
expect(result.current.toasts[0].status).toEqual('error');
expect(screen.getByText('Failed to stop app: my big error')).toBeInTheDocument();
});
});
});
@ -332,7 +301,6 @@ describe('Test: AppDetailsContainer', () => {
// Arrange
const app = createAppEntity({ status: 'running', overridesInfo: { exposable: true } });
server.use(getTRPCMock({ path: ['app', 'updateAppConfig'], type: 'mutation', response: app }));
const { result } = renderHook(() => useToastStore());
render(<AppDetailsContainer app={app} />);
const openModalButton = screen.getByRole('button', { name: 'Settings' });
fireEvent.click(openModalButton);
@ -343,15 +311,12 @@ describe('Test: AppDetailsContainer', () => {
// Assert
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0].status).toEqual('success');
expect(result.current.toasts[0].title).toEqual('App config updated successfully. Restart the app to apply the changes');
expect(screen.getByText('App config updated successfully. Restart the app to apply the changes')).toBeInTheDocument();
});
});
it('should display a toast error when update config mutation fails', async () => {
// Arrange
const { result } = renderHook(() => useToastStore());
server.use(getTRPCMockError({ path: ['app', 'updateAppConfig'], type: 'mutation', message: 'my big error' }));
const app = createAppEntity({ status: 'running', overridesInfo: { exposable: true } });
render(<AppDetailsContainer app={app} />);
@ -364,9 +329,7 @@ describe('Test: AppDetailsContainer', () => {
// Assert
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0].description).toEqual('my big error');
expect(result.current.toasts[0].status).toEqual('error');
expect(screen.getByText('Failed to update app config: my big error')).toBeInTheDocument();
});
});
});

View file

@ -1,6 +1,6 @@
import React from 'react';
import { toast } from 'react-hot-toast';
import { useDisclosure } from '../../../../hooks/useDisclosure';
import { useToastStore } from '../../../../state/toastStore';
import { AppLogo } from '../../../../components/AppLogo/AppLogo';
import { AppStatus } from '../../../../components/AppStatus';
import { AppActions } from '../../components/AppActions';
@ -20,7 +20,6 @@ interface IProps {
}
export const AppDetailsContainer: React.FC<IProps> = ({ app }) => {
const { addToast } = useToastStore();
const installDisclosure = useDisclosure();
const uninstallDisclosure = useDisclosure();
const stopDisclosure = useDisclosure();
@ -41,11 +40,11 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app }) => {
},
onSuccess: () => {
invalidate();
addToast({ title: 'App installed successfully', status: 'success' });
toast.success('App installed successfully');
},
onError: (e) => {
invalidate();
addToast({ title: 'Install error', description: e.message, status: 'error' });
toast.error(`Failed to install app: ${e.message}`);
},
});
@ -56,9 +55,9 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app }) => {
},
onSuccess: () => {
invalidate();
addToast({ title: 'App uninstalled successfully', status: 'success' });
toast.success('App uninstalled successfully');
},
onError: (e) => addToast({ title: 'Uninstall error', description: e.message, status: 'error' }),
onError: (e) => toast.error(`Failed to uninstall app: ${e.message}`),
});
const stop = trpc.app.stopApp.useMutation({
@ -68,9 +67,9 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app }) => {
},
onSuccess: () => {
invalidate();
addToast({ title: 'App stopped successfully', status: 'success' });
toast.success('App stopped successfully');
},
onError: (e) => addToast({ title: 'Stop error', description: e.message, status: 'error' }),
onError: (e) => toast.error(`Failed to stop app: ${e.message}`),
});
const update = trpc.app.updateApp.useMutation({
@ -80,9 +79,9 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app }) => {
},
onSuccess: () => {
invalidate();
addToast({ title: 'App updated successfully', status: 'success' });
toast.success('App updated successfully');
},
onError: (e) => addToast({ title: 'Update error', description: e.message, status: 'error' }),
onError: (e) => toast.error(`Failed to update app: ${e.message}`),
});
const start = trpc.app.startApp.useMutation({
@ -91,18 +90,18 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app }) => {
},
onSuccess: () => {
invalidate();
addToast({ title: 'App started successfully', status: 'success' });
toast.success('App started successfully');
},
onError: (e) => addToast({ title: 'Start error', description: e.message, status: 'error' }),
onError: (e) => toast.error(`Failed to start app: ${e.message}`),
});
const updateConfig = trpc.app.updateAppConfig.useMutation({
onMutate: () => updateSettingsDisclosure.close(),
onSuccess: () => {
invalidate();
addToast({ title: 'App config updated successfully. Restart the app to apply the changes', status: 'success' });
toast.success('App config updated successfully. Restart the app to apply the changes');
},
onError: (e) => addToast({ title: 'Update error', description: e.message, status: 'error' }),
onError: (e) => toast.error(`Failed to update app config: ${e.message}`),
});
const updateAvailable = Number(app.version || 0) < Number(app?.latestVersion || 0);

View file

@ -1,9 +1,8 @@
import { faker } from '@faker-js/faker';
import React from 'react';
import { fireEvent, render, renderHook, screen, waitFor } from '../../../../../../tests/test-utils';
import { fireEvent, render, screen, waitFor } from '../../../../../../tests/test-utils';
import { getTRPCMock, getTRPCMockError } from '../../../../mocks/getTrpcMock';
import { server } from '../../../../mocks/server';
import { useToastStore } from '../../../../state/toastStore';
import { LoginContainer } from './LoginContainer';
describe('Test: LoginContainer', () => {
@ -64,7 +63,6 @@ describe('Test: LoginContainer', () => {
it('should show error message if login fails', async () => {
// Arrange
const { result } = renderHook(() => useToastStore());
server.use(getTRPCMockError({ path: ['auth', 'login'], type: 'mutation', status: 500, message: 'my big error' }));
render(<LoginContainer />);
@ -79,9 +77,7 @@ describe('Test: LoginContainer', () => {
// Assert
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0].description).toEqual('my big error');
expect(result.current.toasts[0].status).toEqual('error');
expect(screen.getByText(/my big error/)).toBeInTheDocument();
});
const token = localStorage.getItem('token');
expect(token).toBeNull();
@ -118,7 +114,6 @@ describe('Test: LoginContainer', () => {
it('should show error message if totp code is invalid', async () => {
// arrange
const { result } = renderHook(() => useToastStore());
const email = faker.internet.email();
const password = faker.internet.password();
const totpSessionId = faker.datatype.uuid();
@ -150,9 +145,7 @@ describe('Test: LoginContainer', () => {
// assert
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0].description).toEqual('Invalid totp code');
expect(result.current.toasts[0].status).toEqual('error');
expect(screen.getByText(/Invalid totp code/)).toBeInTheDocument();
});
});

View file

@ -1,6 +1,6 @@
import { useRouter } from 'next/router';
import React, { useState } from 'react';
import { useToastStore } from '../../../../state/toastStore';
import { toast } from 'react-hot-toast';
import { trpc } from '../../../../utils/trpc';
import { AuthFormLayout } from '../../components/AuthFormLayout';
import { LoginForm } from '../../components/LoginForm';
@ -11,12 +11,11 @@ type FormValues = { email: string; password: string };
export const LoginContainer: React.FC = () => {
const [totpSessionId, setTotpSessionId] = useState<string | null>(null);
const router = useRouter();
const { addToast } = useToastStore();
const utils = trpc.useContext();
const login = trpc.auth.login.useMutation({
onError: (e) => {
localStorage.removeItem('token');
addToast({ title: 'Login error', description: e.message, status: 'error' });
toast.error(`Login failed: ${e.message}`);
},
onSuccess: (data) => {
if (data.totpSessionId) {
@ -31,7 +30,7 @@ export const LoginContainer: React.FC = () => {
const verifyTotp = trpc.auth.verifyTotp.useMutation({
onError: (e) => {
addToast({ title: 'Error', description: e.message, status: 'error' });
toast.error(`Verification failed: ${e.message}`);
},
onSuccess: (data) => {
localStorage.setItem('token', data.token);

View file

@ -1,9 +1,8 @@
import { faker } from '@faker-js/faker';
import React from 'react';
import { fireEvent, render, renderHook, screen, waitFor } from '../../../../../../tests/test-utils';
import { fireEvent, render, screen, waitFor } from '../../../../../../tests/test-utils';
import { getTRPCMock, getTRPCMockError } from '../../../../mocks/getTrpcMock';
import { server } from '../../../../mocks/server';
import { useToastStore } from '../../../../state/toastStore';
import { RegisterContainer } from './RegisterContainer';
describe('Test: RegisterContainer', () => {
@ -42,7 +41,6 @@ describe('Test: RegisterContainer', () => {
const email = faker.internet.email();
const password = faker.internet.password();
const { result } = renderHook(() => useToastStore());
server.use(getTRPCMockError({ path: ['auth', 'register'], type: 'mutation', status: 500, message: 'my big error' }));
render(<RegisterContainer />);
@ -59,9 +57,7 @@ describe('Test: RegisterContainer', () => {
// Assert
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0].description).toEqual('my big error');
expect(result.current.toasts[0].status).toEqual('error');
expect(screen.getByText('Registration failed: my big error')).toBeInTheDocument();
});
});
});

View file

@ -1,6 +1,6 @@
import { useRouter } from 'next/router';
import React from 'react';
import { useToastStore } from '../../../../state/toastStore';
import { toast } from 'react-hot-toast';
import { trpc } from '../../../../utils/trpc';
import { AuthFormLayout } from '../../components/AuthFormLayout';
import { RegisterForm } from '../../components/RegisterForm';
@ -8,13 +8,12 @@ import { RegisterForm } from '../../components/RegisterForm';
type FormValues = { email: string; password: string };
export const RegisterContainer: React.FC = () => {
const { addToast } = useToastStore();
const router = useRouter();
const utils = trpc.useContext();
const register = trpc.auth.register.useMutation({
onError: (e) => {
localStorage.removeItem('token');
addToast({ title: 'Register error', description: e.message, status: 'error' });
toast.error(`Registration failed: ${e.message}`);
},
onSuccess: (data) => {
localStorage.setItem('token', data.token);

View file

@ -1,8 +1,7 @@
import React from 'react';
import { fireEvent, render, screen, waitFor, renderHook } from '../../../../../../tests/test-utils';
import { fireEvent, render, screen, waitFor } from '../../../../../../tests/test-utils';
import { getTRPCMock, getTRPCMockError } from '../../../../mocks/getTrpcMock';
import { server } from '../../../../mocks/server';
import { useToastStore } from '../../../../state/toastStore';
import { ResetPasswordContainer } from './ResetPasswordContainer';
const pushFn = jest.fn();
@ -55,7 +54,6 @@ describe('ResetPasswordContainer', () => {
it('should show error toast if reset password mutation fails', async () => {
// Arrange
const { result, unmount } = renderHook(() => useToastStore());
render(<ResetPasswordContainer isRequested />);
const resetPasswordForm = screen.getByRole('button', { name: 'Reset password' });
fireEvent.click(resetPasswordForm);
@ -74,15 +72,12 @@ describe('ResetPasswordContainer', () => {
// Assert
await waitFor(() => {
expect(result.current.toasts[0].description).toBe(error.message);
expect(screen.getByText(/Something went wrong/)).toBeInTheDocument();
});
unmount();
});
it('should call the cancel request mutation when cancel button is clicked', async () => {
// Arrange
const { result, unmount } = renderHook(() => useToastStore());
render(<ResetPasswordContainer isRequested />);
server.use(getTRPCMock({ path: ['auth', 'cancelPasswordChangeRequest'], type: 'mutation', response: true }));
@ -93,10 +88,8 @@ describe('ResetPasswordContainer', () => {
// Assert
await waitFor(() => {
expect(result.current.toasts[0].title).toBe('Password change request cancelled');
expect(screen.getByText('Password change request cancelled')).toBeInTheDocument();
});
unmount();
});
it('should redirect to login page when Back to login button is clicked', async () => {

View file

@ -1,7 +1,7 @@
import { useRouter } from 'next/router';
import React from 'react';
import { toast } from 'react-hot-toast';
import { Button } from '../../../../components/ui/Button';
import { useToastStore } from '../../../../state/toastStore';
import { trpc } from '../../../../utils/trpc';
import { AuthFormLayout } from '../../components/AuthFormLayout';
import { ResetPasswordForm } from '../../components/ResetPasswordForm';
@ -13,7 +13,6 @@ type Props = {
type FormValues = { password: string };
export const ResetPasswordContainer: React.FC<Props> = ({ isRequested }) => {
const { addToast } = useToastStore();
const router = useRouter();
const utils = trpc.useContext();
const resetPassword = trpc.auth.changeOperatorPassword.useMutation({
@ -21,13 +20,13 @@ export const ResetPasswordContainer: React.FC<Props> = ({ isRequested }) => {
utils.auth.checkPasswordChangeRequest.invalidate();
},
onError: (error) => {
addToast({ title: 'Reset password error', description: error.message, status: 'error' });
toast.error(`Failed to reset password ${error.message}`);
},
});
const cancelRequest = trpc.auth.cancelPasswordChangeRequest.useMutation({
onSuccess: () => {
utils.auth.checkPasswordChangeRequest.invalidate();
addToast({ title: 'Password change request cancelled', status: 'success' });
toast.success('Password change request cancelled');
},
});

View file

@ -1,8 +1,6 @@
import React from 'react';
import { server } from '@/client/mocks/server';
import { getTRPCMock, getTRPCMockError } from '@/client/mocks/getTrpcMock';
import { useToastStore } from '@/client/state/toastStore';
import { renderHook } from '@testing-library/react';
import { faker } from '@faker-js/faker';
import { render, screen, waitFor, fireEvent } from '../../../../../../tests/test-utils';
import { ChangePasswordForm } from './ChangePasswordForm';
@ -10,7 +8,6 @@ import { ChangePasswordForm } from './ChangePasswordForm';
describe('<ChangePasswordForm />', () => {
it('should show success toast upon password change', async () => {
// arrange
const { result } = renderHook(() => useToastStore());
server.use(getTRPCMock({ path: ['auth', 'changePassword'], type: 'mutation', response: true }));
render(<ChangePasswordForm />);
const currentPasswordInput = screen.getByRole('textbox', { name: 'currentPassword' });
@ -27,15 +24,12 @@ describe('<ChangePasswordForm />', () => {
// assert
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0]?.status).toEqual('success');
expect(result.current.toasts[0]?.description).toEqual('Password successfully changed');
expect(screen.getByText('Password successfully changed')).toBeInTheDocument();
});
});
it('should show error toast if change password failed', async () => {
// arrange
const { result } = renderHook(() => useToastStore());
server.use(getTRPCMockError({ path: ['auth', 'changePassword'], type: 'mutation', message: 'Invalid password' }));
render(<ChangePasswordForm />);
const currentPasswordInput = screen.getByRole('textbox', { name: 'currentPassword' });
@ -52,10 +46,7 @@ describe('<ChangePasswordForm />', () => {
// assert
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0]?.status).toEqual('error');
expect(result.current.toasts[0]?.title).toEqual('Error');
expect(result.current.toasts[0]?.description).toEqual('Invalid password');
expect(screen.getByText(/Invalid password/)).toBeInTheDocument();
});
});

View file

@ -2,11 +2,11 @@ import React from 'react';
import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';
import { trpc } from '@/utils/trpc';
import { useToastStore } from '@/client/state/toastStore';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/router';
import { toast } from 'react-hot-toast';
const schema = z
.object({
@ -28,13 +28,12 @@ type FormValues = z.infer<typeof schema>;
export const ChangePasswordForm = () => {
const router = useRouter();
const { addToast } = useToastStore();
const changePassword = trpc.auth.changePassword.useMutation({
onError: (e) => {
addToast({ title: 'Error', description: e.message, status: 'error' });
toast.error(`Error changing password: ${e.message}`);
},
onSuccess: () => {
addToast({ title: 'Success', description: 'Password successfully changed', status: 'success' });
toast.success('Password successfully changed');
router.push('/');
},
});

View file

@ -1,8 +1,6 @@
import React from 'react';
import { server } from '@/client/mocks/server';
import { getTRPCMock, getTRPCMockError } from '@/client/mocks/getTrpcMock';
import { useToastStore } from '@/client/state/toastStore';
import { renderHook } from '@testing-library/react';
import { render, screen, waitFor, fireEvent } from '../../../../../../tests/test-utils';
import { OtpForm } from './OtpForm';
@ -48,7 +46,6 @@ describe('<OtpForm />', () => {
it('should show show error toast if password is incorrect while enabling 2FA', async () => {
// arrange
const { result } = renderHook(() => useToastStore());
server.use(getTRPCMock({ path: ['auth', 'me'], response: { totp_enabled: false, id: 12, username: 'test' } }));
server.use(getTRPCMockError({ path: ['auth', 'getTotpUri'], type: 'mutation', message: 'Invalid password' }));
render(<OtpForm />);
@ -71,16 +68,12 @@ describe('<OtpForm />', () => {
// assert
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0]?.status).toEqual('error');
expect(result.current.toasts[0]?.title).toEqual('Error');
expect(result.current.toasts[0]?.description).toEqual('Invalid password');
expect(screen.getByText(/Invalid password/)).toBeInTheDocument();
});
});
it('should show show error toast if password is incorrect while disabling 2FA', async () => {
// arrange
const { result } = renderHook(() => useToastStore());
server.use(getTRPCMock({ path: ['auth', 'me'], response: { totp_enabled: true, id: 12, username: 'test' } }));
server.use(getTRPCMockError({ path: ['auth', 'disableTotp'], type: 'mutation', message: 'Invalid password' }));
render(<OtpForm />);
@ -104,16 +97,12 @@ describe('<OtpForm />', () => {
// assert
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0]?.status).toEqual('error');
expect(result.current.toasts[0]?.title).toEqual('Error');
expect(result.current.toasts[0]?.description).toEqual('Invalid password');
expect(screen.getByText(/Invalid password/)).toBeInTheDocument();
});
});
it('should show success toast if password is correct while disabling 2FA', async () => {
// arrange
const { result } = renderHook(() => useToastStore());
server.use(getTRPCMock({ path: ['auth', 'me'], response: { totp_enabled: true, id: 12, username: 'test' } }));
server.use(getTRPCMock({ path: ['auth', 'disableTotp'], type: 'mutation', response: true }));
@ -138,10 +127,7 @@ describe('<OtpForm />', () => {
// assert
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0]?.status).toEqual('success');
expect(result.current.toasts[0]?.title).toEqual('Success');
expect(result.current.toasts[0]?.description).toEqual('Two-factor authentication disabled');
expect(screen.getByText('Two-factor authentication disabled')).toBeInTheDocument();
});
});
@ -174,7 +160,6 @@ describe('<OtpForm />', () => {
it('should show error toast if submitted totp code is invalid', async () => {
// arrange
const { result } = renderHook(() => useToastStore());
server.use(getTRPCMock({ path: ['auth', 'getTotpUri'], type: 'mutation', response: { key: 'test', uri: 'test' } }));
server.use(getTRPCMockError({ path: ['auth', 'setupTotp'], type: 'mutation', message: 'Invalid code' }));
@ -210,16 +195,12 @@ describe('<OtpForm />', () => {
// assert
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0]?.status).toEqual('error');
expect(result.current.toasts[0]?.title).toEqual('Error');
expect(result.current.toasts[0]?.description).toEqual('Invalid code');
expect(screen.getByText(/Invalid code/)).toBeInTheDocument();
});
});
it('should show success toast if submitted totp code is valid', async () => {
// arrange
const { result } = renderHook(() => useToastStore());
server.use(getTRPCMock({ path: ['auth', 'getTotpUri'], type: 'mutation', response: { key: 'test', uri: 'test' } }));
server.use(getTRPCMock({ path: ['auth', 'setupTotp'], type: 'mutation', response: true }));
render(<OtpForm />);
@ -253,10 +234,7 @@ describe('<OtpForm />', () => {
// assert
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0]?.status).toEqual('success');
expect(result.current.toasts[0]?.title).toEqual('Success');
expect(result.current.toasts[0]?.description).toEqual('Two-factor authentication enabled');
expect(screen.getByText('Two-factor authentication enabled')).toBeInTheDocument();
});
});

View file

@ -1,15 +1,14 @@
import React from 'react';
import { trpc } from '@/utils/trpc';
import { Switch } from '@/components/ui/Switch';
import { useToastStore } from '@/client/state/toastStore';
import { Button } from '@/components/ui/Button';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
import { Input } from '@/components/ui/Input';
import { QRCodeSVG } from 'qrcode.react';
import { OtpInput } from '@/components/ui/OtpInput';
import { toast } from 'react-hot-toast';
export const OtpForm = () => {
const { addToast } = useToastStore();
const [password, setPassword] = React.useState('');
const [key, setKey] = React.useState('');
const [uri, setUri] = React.useState('');
@ -28,7 +27,7 @@ export const OtpForm = () => {
},
onError: (e) => {
setPassword('');
addToast({ title: 'Error', description: e.message, status: 'error' });
toast.error(`Error getting TOTP URI: ${e.message}`);
},
onSuccess: (data) => {
setKey(data.key);
@ -40,13 +39,13 @@ export const OtpForm = () => {
onMutate: () => {},
onError: (e) => {
setTotpCode('');
addToast({ title: 'Error', description: e.message, status: 'error' });
toast.error(`Error setting up TOTP: ${e.message}`);
},
onSuccess: () => {
setTotpCode('');
setKey('');
setUri('');
addToast({ title: 'Success', description: 'Two-factor authentication enabled', status: 'success' });
toast.success('Two-factor authentication enabled');
ctx.auth.me.invalidate();
},
});
@ -57,10 +56,10 @@ export const OtpForm = () => {
},
onError: (e) => {
setPassword('');
addToast({ title: 'Error', description: e.message, status: 'error' });
toast.error(`Error disabling TOTP: ${e.message}`);
},
onSuccess: () => {
addToast({ title: 'Success', description: 'Two-factor authentication disabled', status: 'success' });
toast.success('Two-factor authentication disabled');
ctx.auth.me.invalidate();
},
});

View file

@ -1,9 +1,8 @@
import React from 'react';
import { useToastStore } from '@/client/state/toastStore';
import { getTRPCMock, getTRPCMockError } from '@/client/mocks/getTrpcMock';
import { server } from '@/client/mocks/server';
import { GeneralActions } from './GeneralActions';
import { fireEvent, render, renderHook, screen, waitFor } from '../../../../../../tests/test-utils';
import { fireEvent, render, screen, waitFor } from '../../../../../../tests/test-utils';
describe('Test: GeneralActions', () => {
it('should render without error', () => {
@ -14,7 +13,6 @@ describe('Test: GeneralActions', () => {
it('should show toast if update mutation fails', async () => {
// arrange
const { result } = renderHook(() => useToastStore());
server.use(getTRPCMock({ path: ['system', 'getVersion'], response: { current: '1.0.0', latest: '2.0.0' } }));
server.use(getTRPCMockError({ path: ['system', 'update'], type: 'mutation', status: 500, message: 'Something went wrong' }));
render(<GeneralActions />);
@ -30,10 +28,7 @@ describe('Test: GeneralActions', () => {
// assert
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0].status).toEqual('error');
expect(result.current.toasts[0].title).toEqual('Error');
expect(result.current.toasts[0].description).toEqual('Something went wrong');
expect(screen.getByText(/Something went wrong/)).toBeInTheDocument();
});
});
@ -61,7 +56,6 @@ describe('Test: GeneralActions', () => {
it('should show toast if restart mutation fails', async () => {
// arrange
const { result } = renderHook(() => useToastStore());
server.use(getTRPCMockError({ path: ['system', 'restart'], type: 'mutation', status: 500, message: 'Something went wrong' }));
render(<GeneralActions />);
const restartButton = screen.getByRole('button', { name: /Restart/i });
@ -73,10 +67,7 @@ describe('Test: GeneralActions', () => {
// assert
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0].status).toEqual('error');
expect(result.current.toasts[0].title).toEqual('Error');
expect(result.current.toasts[0].description).toEqual('Something went wrong');
expect(screen.getByText(/Something went wrong/)).toBeInTheDocument();
});
});

View file

@ -1,8 +1,8 @@
import React from 'react';
import semver from 'semver';
import { toast } from 'react-hot-toast';
import { Button } from '../../../../components/ui/Button';
import { useDisclosure } from '../../../../hooks/useDisclosure';
import { useToastStore } from '../../../../state/toastStore';
import { RestartModal } from '../../components/RestartModal';
import { UpdateModal } from '../../components/UpdateModal/UpdateModal';
import { trpc } from '../../../../utils/trpc';
@ -12,7 +12,6 @@ export const GeneralActions = () => {
const versionQuery = trpc.system.getVersion.useQuery(undefined, { staleTime: 0 });
const [loading, setLoading] = React.useState(false);
const { addToast } = useToastStore();
const { setPollStatus } = useSystemStore();
const restartDisclosure = useDisclosure();
const updateDisclosure = useDisclosure();
@ -30,7 +29,7 @@ export const GeneralActions = () => {
},
onError: (error) => {
updateDisclosure.close();
addToast({ title: 'Error', description: error.message, status: 'error' });
toast.error(`Error updating instance: ${error.message}`);
},
onSettled: () => {
setLoading(false);
@ -47,7 +46,7 @@ export const GeneralActions = () => {
},
onError: (error) => {
restartDisclosure.close();
addToast({ title: 'Error', description: error.message, status: 'error' });
toast.error(`Error restarting instance: ${error.message}`);
},
onSettled: () => {
setLoading(false);

View file

@ -1,9 +1,8 @@
import React from 'react';
import { server } from '@/client/mocks/server';
import { getTRPCMockError } from '@/client/mocks/getTrpcMock';
import { useToastStore } from '../../../../state/toastStore';
import { SettingsContainer } from './SettingsContainer';
import { fireEvent, render, renderHook, screen, waitFor } from '../../../../../../tests/test-utils';
import { fireEvent, render, screen, waitFor } from '../../../../../../tests/test-utils';
describe('Test: SettingsContainer', () => {
it('should render without error', () => {
@ -14,7 +13,6 @@ describe('Test: SettingsContainer', () => {
it('should show toast if updateSettings mutation fails', async () => {
// arrange
const { result } = renderHook(() => useToastStore());
server.use(getTRPCMockError({ path: ['system', 'updateSettings'], type: 'mutation', status: 500, message: 'Something went wrong' }));
render(<SettingsContainer />);
const submitButton = screen.getByRole('button', { name: 'Save' });
@ -28,9 +26,7 @@ describe('Test: SettingsContainer', () => {
// assert
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0].status).toEqual('error');
expect(result.current.toasts[0].title).toEqual('Error saving settings');
expect(screen.getByText(/Something went wrong/)).toBeInTheDocument();
});
});
@ -50,7 +46,6 @@ describe('Test: SettingsContainer', () => {
it('should show toast if updateSettings mutation succeeds', async () => {
// arrange
const { result } = renderHook(() => useToastStore());
render(<SettingsContainer />);
const submitButton = screen.getByRole('button', { name: 'Save' });
@ -59,8 +54,7 @@ describe('Test: SettingsContainer', () => {
// assert
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
expect(result.current.toasts[0].status).toEqual('success');
expect(screen.getByText(/Settings updated. Restart your instance to apply new settings./)).toBeInTheDocument();
});
});
});

View file

@ -1,22 +1,21 @@
import React, { useState } from 'react';
import { trpc } from '@/utils/trpc';
import { useToastStore } from '../../../../state/toastStore';
import { toast } from 'react-hot-toast';
import { SettingsForm, SettingsFormValues } from '../../components/SettingsForm';
export const SettingsContainer = () => {
const [errors, setErrors] = useState<Record<string, string>>({});
const { addToast } = useToastStore();
const getSettings = trpc.system.getSettings.useQuery();
const updateSettings = trpc.system.updateSettings.useMutation({
onSuccess: () => {
addToast({ title: 'Settings updated', description: 'Restart your instance for settings to take effect', status: 'success' });
toast.success('Settings updated. Restart your instance to apply new settings.');
},
onError: (e) => {
if (e.shape?.data.zodError) {
setErrors(e.shape.data.zodError);
}
addToast({ title: 'Error saving settings', description: e.message, status: 'error' });
toast.error(`Error saving settings: ${e.message}`);
},
});