diff --git a/src/components/ModalsContainer/OnboardingFlow/GraphDetailsStep/index.tsx b/src/components/ModalsContainer/OnboardingFlow/GraphDetailsStep/index.tsx index 007ba80ac..4aed78d21 100644 --- a/src/components/ModalsContainer/OnboardingFlow/GraphDetailsStep/index.tsx +++ b/src/components/ModalsContainer/OnboardingFlow/GraphDetailsStep/index.tsx @@ -1,164 +1,164 @@ -import { Button } from '@mui/material' -import { FC, useEffect } from 'react' -import { useFormContext } from 'react-hook-form' -import { MdError } from 'react-icons/md' -import styled from 'styled-components' -import { noSpacePattern } from '~/components/AddItemModal/SourceTypeStep/constants' -import { Flex } from '~/components/common/Flex' -import { Text } from '~/components/common/Text' -import { TextInput } from '~/components/common/TextInput' -import { requiredRule } from '~/constants' -import { colors } from '~/utils' - -type Props = { - onSubmit: () => void - error?: string -} - -export const GraphDetailsStep: FC = ({ onSubmit, error }) => { - const { - formState: { isSubmitting }, - watch, - } = useFormContext() - - const title = watch('title') - const description = watch('description') - - const isFormValid = !!title?.trim() && !!description?.trim() - - useEffect(() => { - const titleInput = document.getElementById('graph-title') as HTMLInputElement - - if (titleInput) { - titleInput.focus() - } - }, []) - - return ( - - - Welcome to SecondBrain - Set a name and short description for your graph. - - - - - - - - - - - - - {error ? ( - - - - {error} - - - ) : null} - - ) -} - -const StyledText = styled(Text)` - font-size: 22px; - font-weight: 600; - font-family: 'Barlow'; - margin-bottom: 10px; -` - -const StyledSubText = styled(Text)` - font-size: 14px; - font-family: 'Barlow'; - margin-bottom: 20px; -` - -const StyledWrapper = styled(Flex)` - width: 100%; - display: flex; - justify-content: center; - gap: 10px; - margin: 0 0 15px 0; - - .input__wrapper { - display: flex; - gap: 23px; - max-height: 225px; - overflow-y: auto; - padding-right: 20px; - width: calc(100% + 20px); - } -` - -const StyledErrorText = styled(Flex)` - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - gap: 2px; - - .errorIcon { - display: block; - font-size: 13px; - min-height: 13px; - min-width: 13px; - } - - span { - display: -webkit-box; - -webkit-line-clamp: 1; - -webkit-box-orient: vertical; - overflow: hidden; - white-space: normal; - letter-spacing: 0.2px; - padding-left: 4px; - font-size: 13px; - font-family: Barlow; - line-height: 18px; - } -` - -const StyledError = styled(Flex)` - display: flex; - align-items: center; - color: ${colors.primaryRed}; - position: relative; - margin-top: 20px; -` +import { Button } from '@mui/material' +import { FC, useEffect } from 'react' +import { useFormContext } from 'react-hook-form' +import { MdError } from 'react-icons/md' +import styled from 'styled-components' +import { noSpacePattern } from '~/components/AddItemModal/SourceTypeStep/constants' +import { Flex } from '~/components/common/Flex' +import { Text } from '~/components/common/Text' +import { TextInput } from '~/components/common/TextInput' +import { requiredRule } from '~/constants' +import { colors } from '~/utils' + +type Props = { + onSubmit: () => void + error?: string +} + +export const GraphDetailsStep: FC = ({ onSubmit, error }) => { + const { + formState: { isSubmitting }, + watch, + } = useFormContext() + + const title = watch('title') + const description = watch('description') + + const isFormValid = !!title?.trim() && !!description?.trim() + + useEffect(() => { + const titleInput = document.getElementById('graph-title') as HTMLInputElement + + if (titleInput) { + titleInput.focus() + } + }, []) + + return ( + + + Welcome to SecondBrain + Set a name and short description for your graph. + + + + + + + + + + + + + {error ? ( + + + + {error} + + + ) : null} + + ) +} + +const StyledText = styled(Text)` + font-size: 22px; + font-weight: 600; + font-family: 'Barlow'; + margin-bottom: 10px; +` + +const StyledSubText = styled(Text)` + font-size: 14px; + font-family: 'Barlow'; + margin-bottom: 20px; +` + +const StyledWrapper = styled(Flex)` + width: 100%; + display: flex; + justify-content: center; + gap: 10px; + margin: 0 0 15px 0; + + .input__wrapper { + display: flex; + gap: 23px; + max-height: 225px; + overflow-y: auto; + padding-right: 20px; + width: calc(100% + 20px); + } +` + +const StyledErrorText = styled(Flex)` + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 2px; + + .errorIcon { + display: block; + font-size: 13px; + min-height: 13px; + min-width: 13px; + } + + span { + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; + white-space: normal; + letter-spacing: 0.2px; + padding-left: 4px; + font-size: 13px; + font-family: Barlow; + line-height: 18px; + } +` + +const StyledError = styled(Flex)` + display: flex; + align-items: center; + color: ${colors.primaryRed}; + position: relative; + margin-top: 20px; +` diff --git a/src/components/ModalsContainer/OnboardingFlow/__tests__/index.tsx b/src/components/ModalsContainer/OnboardingFlow/__tests__/index.tsx index 520e1c7c2..5715ec5e7 100644 --- a/src/components/ModalsContainer/OnboardingFlow/__tests__/index.tsx +++ b/src/components/ModalsContainer/OnboardingFlow/__tests__/index.tsx @@ -1,120 +1,120 @@ -import '@testing-library/jest-dom' -import { fireEvent, render, screen, waitFor } from '@testing-library/react' -import React from 'react' -import { postAboutData } from '~/network/fetchSourcesData' -import { useModal } from '~/stores/useModalStore' -import { OnboardingModal } from '../index' - -jest.mock('~/network/fetchSourcesData', () => ({ - postAboutData: jest.fn(), -})) - -jest.mock('~/stores/useModalStore', () => ({ - useModal: jest.fn(), -})) - -const useModalMock = useModal as jest.MockedFunction -const postAboutDataMock = postAboutData as jest.MockedFunction - -describe('OnboardingModal Component', () => { - beforeEach(() => { - jest.clearAllMocks() - - useModalMock.mockReturnValue({ - close: jest.fn(), - visible: true, - }) - }) - - test('renders the onboarding modal', () => { - render() - expect(screen.getByText('Welcome to SecondBrain')).toBeInTheDocument() - expect(screen.getByText('Set a name and short description for your graph.')).toBeInTheDocument() - }) - - test('submits form successfully', async () => { - postAboutDataMock.mockResolvedValue({ status: 'success' }) - - render() - - fireEvent.change(screen.getByPlaceholderText('Type graph title here...'), { target: { value: 'Test Title' } }) - - fireEvent.change(screen.getByPlaceholderText('Type graph description here...'), { - target: { value: 'Test Description' }, - }) - - fireEvent.click(screen.getByText('Confirm')) - - await waitFor(() => { - expect(postAboutDataMock).toHaveBeenCalledWith({ - title: 'Test Title', - description: 'Test Description', - }) - }) - }) - - test('displays error on form submission failure', async () => { - postAboutDataMock.mockRejectedValue({ status: 400, json: async () => ({ errorCode: 'Error occurred' }) }) - - render() - - fireEvent.change(screen.getByPlaceholderText('Type graph title here...'), { target: { value: 'Test Title' } }) - - fireEvent.change(screen.getByPlaceholderText('Type graph description here...'), { - target: { value: 'Test Description' }, - }) - - fireEvent.click(screen.getByText('Confirm')) - - await waitFor(() => { - expect(screen.getByText('Error occurred')).toBeInTheDocument() - }) - }) - - test('closes modal on successful submission', async () => { - const closeMock = jest.fn() - - useModalMock.mockReturnValue({ - close: closeMock, - visible: true, - }) - - postAboutDataMock.mockResolvedValue({ status: 'success' }) - - render() - - fireEvent.change(screen.getByPlaceholderText('Type graph title here...'), { target: { value: 'Test Title' } }) - - fireEvent.change(screen.getByPlaceholderText('Type graph description here...'), { - target: { value: 'Test Description' }, - }) - - fireEvent.click(screen.getByText('Confirm')) - - await waitFor(() => { - expect(closeMock).toHaveBeenCalled() - }) - }) - - test('resets form and error on modal close', async () => { - const { rerender } = render() - - fireEvent.change(screen.getByPlaceholderText('Type graph title here...'), { target: { value: 'Test Title' } }) - - fireEvent.change(screen.getByPlaceholderText('Type graph description here...'), { - target: { value: 'Test Description' }, - }) - - useModalMock.mockReturnValue({ - close: jest.fn(), - visible: false, - }) - - rerender() - - waitFor(() => { - expect(screen.getByPlaceholderText('Type graph title here...')).toHaveValue('') - expect(screen.getByPlaceholderText('Type graph description here...')).toHaveValue('') - }) - }) -}) +import '@testing-library/jest-dom' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import React from 'react' +import { postAboutData } from '~/network/fetchSourcesData' +import { useModal } from '~/stores/useModalStore' +import { OnboardingModal } from '../index' + +jest.mock('~/network/fetchSourcesData', () => ({ + postAboutData: jest.fn(), +})) + +jest.mock('~/stores/useModalStore', () => ({ + useModal: jest.fn(), +})) + +const useModalMock = useModal as jest.MockedFunction +const postAboutDataMock = postAboutData as jest.MockedFunction + +describe('OnboardingModal Component', () => { + beforeEach(() => { + jest.clearAllMocks() + + useModalMock.mockReturnValue({ + close: jest.fn(), + visible: true, + }) + }) + + test('renders the onboarding modal', () => { + render() + expect(screen.getByText('Welcome to SecondBrain')).toBeInTheDocument() + expect(screen.getByText('Set a name and short description for your graph.')).toBeInTheDocument() + }) + + test('submits form successfully', async () => { + postAboutDataMock.mockResolvedValue({ status: 'success' }) + + render() + + fireEvent.change(screen.getByPlaceholderText('Type graph title here...'), { target: { value: 'Test Title' } }) + + fireEvent.change(screen.getByPlaceholderText('Type graph description here...'), { + target: { value: 'Test Description' }, + }) + + fireEvent.click(screen.getByText('Confirm')) + + await waitFor(() => { + expect(postAboutDataMock).toHaveBeenCalledWith({ + title: 'Test Title', + description: 'Test Description', + }) + }) + }) + + test('displays error on form submission failure', async () => { + postAboutDataMock.mockRejectedValue({ status: 400, json: async () => ({ errorCode: 'Error occurred' }) }) + + render() + + fireEvent.change(screen.getByPlaceholderText('Type graph title here...'), { target: { value: 'Test Title' } }) + + fireEvent.change(screen.getByPlaceholderText('Type graph description here...'), { + target: { value: 'Test Description' }, + }) + + fireEvent.click(screen.getByText('Confirm')) + + await waitFor(() => { + expect(screen.getByText('Error occurred')).toBeInTheDocument() + }) + }) + + test('closes modal on successful submission', async () => { + const closeMock = jest.fn() + + useModalMock.mockReturnValue({ + close: closeMock, + visible: true, + }) + + postAboutDataMock.mockResolvedValue({ status: 'success' }) + + render() + + fireEvent.change(screen.getByPlaceholderText('Type graph title here...'), { target: { value: 'Test Title' } }) + + fireEvent.change(screen.getByPlaceholderText('Type graph description here...'), { + target: { value: 'Test Description' }, + }) + + fireEvent.click(screen.getByText('Confirm')) + + await waitFor(() => { + expect(closeMock).toHaveBeenCalled() + }) + }) + + test('resets form and error on modal close', async () => { + const { rerender } = render() + + fireEvent.change(screen.getByPlaceholderText('Type graph title here...'), { target: { value: 'Test Title' } }) + + fireEvent.change(screen.getByPlaceholderText('Type graph description here...'), { + target: { value: 'Test Description' }, + }) + + useModalMock.mockReturnValue({ + close: jest.fn(), + visible: false, + }) + + rerender() + + waitFor(() => { + expect(screen.getByPlaceholderText('Type graph title here...')).toHaveValue('') + expect(screen.getByPlaceholderText('Type graph description here...')).toHaveValue('') + }) + }) +}) diff --git a/src/components/ModalsContainer/OnboardingFlow/index.tsx b/src/components/ModalsContainer/OnboardingFlow/index.tsx index 2d5123381..a697fe099 100644 --- a/src/components/ModalsContainer/OnboardingFlow/index.tsx +++ b/src/components/ModalsContainer/OnboardingFlow/index.tsx @@ -1,75 +1,75 @@ -import { useEffect, useState } from 'react' -import { FormProvider, useForm } from 'react-hook-form' -import { SuccessNotify } from '~/components/common/SuccessToast' -import { BaseModal } from '~/components/Modal' -import { NODE_ADD_ERROR } from '~/constants' -import { postAboutData, TAboutParams } from '~/network/fetchSourcesData' -import { useModal } from '~/stores/useModalStore' -import { GraphDetailsStep } from './GraphDetailsStep' - -export type FormData = { - title: string - description: string -} - -export const OnboardingModal = () => { - const { close, visible } = useModal('onboardingFlow') - const form = useForm({ mode: 'onChange' }) - const { reset } = form - const [error, setError] = useState('') - - useEffect(() => { - if (!visible) { - reset() - setError('') - } - }, [visible, reset]) - - const submitGraphDetails = async ( - data: TAboutParams, - successCallback: () => void, - onError: (error: string) => void, - ) => { - try { - const res = (await postAboutData(data)) as Awaited<{ status: string }> - - if (res.status === 'success') { - SuccessNotify('Graph details saved') - successCallback() - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (err: any) { - let errorMessage = NODE_ADD_ERROR - - if (err?.status === 400) { - const errorRes = await err.json() - - errorMessage = errorRes.errorCode || errorRes?.status || NODE_ADD_ERROR - } else if (err instanceof Error) { - errorMessage = err.message - } - - onError(String(errorMessage)) - } - } - - const onSubmit = form.handleSubmit(async (data) => { - await submitGraphDetails( - data, - () => { - close() - }, - setError, - ) - }) - - return ( - - -
- - -
-
- ) -} +import { useEffect, useState } from 'react' +import { FormProvider, useForm } from 'react-hook-form' +import { SuccessNotify } from '~/components/common/SuccessToast' +import { BaseModal } from '~/components/Modal' +import { NODE_ADD_ERROR } from '~/constants' +import { postAboutData, TAboutParams } from '~/network/fetchSourcesData' +import { useModal } from '~/stores/useModalStore' +import { GraphDetailsStep } from './GraphDetailsStep' + +export type FormData = { + title: string + description: string +} + +export const OnboardingModal = () => { + const { close, visible } = useModal('onboardingFlow') + const form = useForm({ mode: 'onChange' }) + const { reset } = form + const [error, setError] = useState('') + + useEffect(() => { + if (!visible) { + reset() + setError('') + } + }, [visible, reset]) + + const submitGraphDetails = async ( + data: TAboutParams, + successCallback: () => void, + onError: (error: string) => void, + ) => { + try { + const res = (await postAboutData(data)) as Awaited<{ status: string }> + + if (res.status === 'success') { + SuccessNotify('Graph details saved') + successCallback() + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { + let errorMessage = NODE_ADD_ERROR + + if (err?.status === 400) { + const errorRes = await err.json() + + errorMessage = errorRes.errorCode || errorRes?.status || NODE_ADD_ERROR + } else if (err instanceof Error) { + errorMessage = err.message + } + + onError(String(errorMessage)) + } + } + + const onSubmit = form.handleSubmit(async (data) => { + await submitGraphDetails( + data, + () => { + close() + }, + setError, + ) + }) + + return ( + + +
+ + +
+
+ ) +}