From 1566ce674527da765de6e7feefcc5deded530521 Mon Sep 17 00:00:00 2001 From: Ankit Mehta Date: Thu, 5 Feb 2026 12:04:33 +0530 Subject: [PATCH 01/13] Prefill and validate phone number on reset password form --- src/containers/Auth/Auth.tsx | 9 +++++---- .../ResetPasswordConfirmOTP.test.tsx | 18 ++++++++++++++++++ .../ResetPassword/ResetPasswordConfirmOTP.tsx | 1 + 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/containers/Auth/Auth.tsx b/src/containers/Auth/Auth.tsx index 2251dadc6b..fce5cb672f 100644 --- a/src/containers/Auth/Auth.tsx +++ b/src/containers/Auth/Auth.tsx @@ -136,10 +136,10 @@ export const Auth = ({ const handlePhone = () => - (value: string): void => { - // eslint-disable-next-line - initialFormValues.phone = value; - }; + (value: string): void => { + // eslint-disable-next-line + initialFormValues.phone = value; + }; let formElements; // we should not render form elements when displaying success message @@ -155,6 +155,7 @@ export const Auth = ({ { setLoading(true); diff --git a/src/containers/Auth/ResetPassword/ResetPasswordConfirmOTP.test.tsx b/src/containers/Auth/ResetPassword/ResetPasswordConfirmOTP.test.tsx index 54f7058242..550634dce5 100644 --- a/src/containers/Auth/ResetPassword/ResetPasswordConfirmOTP.test.tsx +++ b/src/containers/Auth/ResetPassword/ResetPasswordConfirmOTP.test.tsx @@ -76,4 +76,22 @@ describe('', () => { expect(sendOptMock).toHaveBeenCalledWith('919967665667'); }); }); + + test('it should show validation error if phone number is empty in state', async () => { + const WrapperEmpty = ( + + + } /> + + + ); + + render(WrapperEmpty); + const saveButton = screen.getByText('Save'); + await user.click(saveButton); + + await waitFor(() => { + expect(screen.getAllByText('Input required').length).toBeGreaterThan(0); + }); + }); }); diff --git a/src/containers/Auth/ResetPassword/ResetPasswordConfirmOTP.tsx b/src/containers/Auth/ResetPassword/ResetPasswordConfirmOTP.tsx index 74e8c0b8ec..78a460e1ef 100644 --- a/src/containers/Auth/ResetPassword/ResetPasswordConfirmOTP.tsx +++ b/src/containers/Auth/ResetPassword/ResetPasswordConfirmOTP.tsx @@ -66,6 +66,7 @@ export const ResetPasswordConfirmOTP = () => { ]; const FormSchema = Yup.object().shape({ + phoneNumber: Yup.string().required(t('Input required')), OTP: Yup.string().required(t('Input required')), password: yupPasswordValidation(t), }); From 147674a263fd2a972b7a9b479a8eb6602b781ee5 Mon Sep 17 00:00:00 2001 From: Ankit Mehta Date: Thu, 5 Feb 2026 20:34:23 +0530 Subject: [PATCH 02/13] Add frontend support for editing email on My Account page --- src/containers/MyAccount/MyAccount.test.tsx | 18 ++++++++ src/containers/MyAccount/MyAccount.tsx | 51 +++++++++++++++------ src/graphql/mutations/User.ts | 2 + src/mocks/User.tsx | 26 ++++++++++- 4 files changed, 82 insertions(+), 15 deletions(-) diff --git a/src/containers/MyAccount/MyAccount.test.tsx b/src/containers/MyAccount/MyAccount.test.tsx index 8d30e7fe58..c406ad5875 100644 --- a/src/containers/MyAccount/MyAccount.test.tsx +++ b/src/containers/MyAccount/MyAccount.test.tsx @@ -198,4 +198,22 @@ describe('', () => { const saveButton = screen.getByText('Save'); await user.click(saveButton); }); + + test('update profile (name and email) flow', async () => { + const { container } = render(wrapper); + + await screen.findByTestId('MyAccount'); + + // change email + const emailInput = container.querySelector('input[name="email"]') as HTMLInputElement; + fireEvent.change(emailInput, { target: { value: 'newemail@domain.com' } }); + + // save button should appear + const saveButton = await screen.findByText('Save'); + await user.click(saveButton); + + await waitFor(() => { + expect(screen.getByText('Profile updated successfully!')).toBeInTheDocument(); + }); + }); }); diff --git a/src/containers/MyAccount/MyAccount.tsx b/src/containers/MyAccount/MyAccount.tsx index 40b752dd62..fdb0795b7d 100644 --- a/src/containers/MyAccount/MyAccount.tsx +++ b/src/containers/MyAccount/MyAccount.tsx @@ -139,6 +139,11 @@ export const MyAccount = () => { password: yupPasswordValidation(t), }); + const UserFormSchema = Yup.object().shape({ + name: Yup.string().required(t('Name is required')), + email: Yup.string().email(t('Invalid email address')).required(t('Email is required')), + }); + const userformFields = [ { component: Input, @@ -156,24 +161,42 @@ export const MyAccount = () => { component: Input, name: 'email', label: t('Email'), - disabled: true, + disabled: false, }, ]; const userForm = ( - { }}> -
- {userformFields.map((field) => ( -
- {field.label && ( - - {field.label} - - )} - -
- ))} -
+ { + setMessage(t('Profile updated successfully!')); + updateCurrentUser({ + variables: { input: { name: values.name, email: values.email } }, + }); + }} + > + {({ dirty, submitForm }) => ( +
+ {userformFields.map((field) => ( +
+ {field.label && ( + + {field.label} + + )} + +
+ ))} + {dirty && ( +
+ +
+ )} +
+ )}
); diff --git a/src/graphql/mutations/User.ts b/src/graphql/mutations/User.ts index 03e8924eae..f14ab153b2 100644 --- a/src/graphql/mutations/User.ts +++ b/src/graphql/mutations/User.ts @@ -39,6 +39,8 @@ export const UPDATE_CURRENT_USER = gql` user { id name + phone + email } errors { key diff --git a/src/mocks/User.tsx b/src/mocks/User.tsx index 9634b6459f..4d825d5941 100644 --- a/src/mocks/User.tsx +++ b/src/mocks/User.tsx @@ -122,7 +122,31 @@ export const updateUserQuery = [ data: { updateCurrentUser: { errors: null, - user: null, + user: { + id: '1', + name: 'John Doe', + phone: '+919820198765', + email: 'you@domain.com', + }, + }, + }, + }, + }, + { + request: { + query: UPDATE_CURRENT_USER, + variables: { input: { name: 'John Doe', email: 'newemail@domain.com' } }, + }, + result: { + data: { + updateCurrentUser: { + errors: null, + user: { + id: '1', + name: 'John Doe', + phone: '+919820198765', + email: 'newemail@domain.com', + }, }, }, }, From f14ecc0ed1f0bd7874e51cee74d11852aa99c9dd Mon Sep 17 00:00:00 2001 From: Ankit Mehta Date: Sun, 15 Feb 2026 02:02:41 +0530 Subject: [PATCH 03/13] Update email editing logic as per review feedback --- src/containers/MyAccount/MyAccount.test.tsx | 33 +++++++++++++++++++++ src/containers/MyAccount/MyAccount.tsx | 10 +++++-- src/mocks/User.tsx | 15 ++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/containers/MyAccount/MyAccount.test.tsx b/src/containers/MyAccount/MyAccount.test.tsx index c406ad5875..4bce5c7b0a 100644 --- a/src/containers/MyAccount/MyAccount.test.tsx +++ b/src/containers/MyAccount/MyAccount.test.tsx @@ -6,6 +6,7 @@ import { MemoryRouter } from 'react-router'; import { vi } from 'vitest'; import { getCurrentUserQuery, updateUserQuery } from 'mocks/User'; +import { UPDATE_CURRENT_USER } from 'graphql/mutations/User'; import { getOrganizationLanguagesQuery } from 'mocks/Organization'; import { MyAccount } from './MyAccount'; @@ -15,6 +16,20 @@ const mocks = [ getCurrentUserQuery, getOrganizationLanguagesQuery, getOrganizationLanguagesQuery, + { + request: { + query: UPDATE_CURRENT_USER, + variables: { input: { name: 'John Doe', email: 'error@domain.com' } }, + }, + result: { + data: { + updateCurrentUser: { + errors: [{ message: 'Email already exists' }], + user: null, + }, + }, + }, + }, ]; vi.mock('axios'); @@ -216,4 +231,22 @@ describe('', () => { expect(screen.getByText('Profile updated successfully!')).toBeInTheDocument(); }); }); + + test('update profile error flow', async () => { + const { container } = render(wrapper); + + await screen.findByTestId('MyAccount'); + + // change email to one that triggers an error + const emailInput = container.querySelector('input[name="email"]') as HTMLInputElement; + fireEvent.change(emailInput, { target: { value: 'error@domain.com' } }); + + // save button should appear + const saveButton = await screen.findByText('Save'); + await user.click(saveButton); + + await waitFor(() => { + expect(screen.getByText('Email already exists')).toBeInTheDocument(); + }); + }); }); diff --git a/src/containers/MyAccount/MyAccount.tsx b/src/containers/MyAccount/MyAccount.tsx index fdb0795b7d..e2c2cc2311 100644 --- a/src/containers/MyAccount/MyAccount.tsx +++ b/src/containers/MyAccount/MyAccount.tsx @@ -51,13 +51,19 @@ export const MyAccount = () => { const [updateCurrentUser] = useMutation(UPDATE_CURRENT_USER, { onCompleted: (data) => { if (data.updateCurrentUser.errors) { - if (data.updateCurrentUser.errors[0].message === 'incorrect_code') { + const error = data.updateCurrentUser.errors[0]; + if (error.message === 'incorrect_code') { setToastMessageInfo({ severity: 'error', message: t('Please enter a valid OTP') }); - } else { + } else if (error.message === 'Too many attempts') { setToastMessageInfo({ severity: 'error', message: t('Too many attempts, please retry after sometime.'), }); + } else { + setToastMessageInfo({ + severity: 'error', + message: t(error.message), + }); } } else { setShowOTPButton(true); diff --git a/src/mocks/User.tsx b/src/mocks/User.tsx index 4d825d5941..9dfa78ff63 100644 --- a/src/mocks/User.tsx +++ b/src/mocks/User.tsx @@ -79,6 +79,7 @@ export const updateUserQuery = [ user: { id: '2', name: 'Updated Name', + phone: '+919820198765', email: 'you@domain.com', }, }, @@ -151,6 +152,20 @@ export const updateUserQuery = [ }, }, }, + { + request: { + query: UPDATE_CURRENT_USER, + variables: { input: { name: 'John Doe', email: 'error@domain.com' } }, + }, + result: { + data: { + updateCurrentUser: { + errors: [{ message: 'Email already exists' }], + user: null, + }, + }, + }, + }, ]; export const getCurrentUserErrorQuery = { From 73a6dbb57c4207ed75cd29397ca7e3a1242453c5 Mon Sep 17 00:00:00 2001 From: Ankit Mehta Date: Mon, 23 Feb 2026 15:20:15 +0530 Subject: [PATCH 04/13] Fix: Align updateCurrentUser mocks with schema (add key field) --- src/containers/MyAccount/MyAccount.test.tsx | 2 +- src/mocks/User.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/containers/MyAccount/MyAccount.test.tsx b/src/containers/MyAccount/MyAccount.test.tsx index 4bce5c7b0a..e9b82125e4 100644 --- a/src/containers/MyAccount/MyAccount.test.tsx +++ b/src/containers/MyAccount/MyAccount.test.tsx @@ -24,7 +24,7 @@ const mocks = [ result: { data: { updateCurrentUser: { - errors: [{ message: 'Email already exists' }], + errors: [{ key: 'email', message: 'Email already exists' }], user: null, }, }, diff --git a/src/mocks/User.tsx b/src/mocks/User.tsx index 9dfa78ff63..b807d9dc94 100644 --- a/src/mocks/User.tsx +++ b/src/mocks/User.tsx @@ -94,7 +94,7 @@ export const updateUserQuery = [ result: { data: { updateCurrentUser: { - errors: [{ message: 'incorrect_code' }], + errors: [{ key: 'otp', message: 'incorrect_code' }], user: null, }, }, @@ -108,7 +108,7 @@ export const updateUserQuery = [ result: { data: { updateCurrentUser: { - errors: [{ message: 'Too many attempts' }], + errors: [{ key: 'otp', message: 'Too many attempts' }], user: null, }, }, @@ -160,7 +160,7 @@ export const updateUserQuery = [ result: { data: { updateCurrentUser: { - errors: [{ message: 'Email already exists' }], + errors: [{ key: 'email', message: 'Email already exists' }], user: null, }, }, From 5f957ead5f43b93186d10d56bdd01dd31fe8511b Mon Sep 17 00:00:00 2001 From: Ankit Mehta Date: Mon, 23 Feb 2026 20:35:07 +0530 Subject: [PATCH 05/13] Fix Formik state handling and integration test issues --- src/assets/images/icons/AdvancedSearch.tsx | 1 - src/components/UI/SearchBar/SearchBar.tsx | 5 +- src/components/floweditor/FlowEditor.test.tsx | 48 +- src/containers/Auth/Auth.tsx | 25 +- .../ChatConversations.test.tsx | 4 +- .../ConversationList.test.tsx | 16 +- .../Chat/ChatInterface/ChatInterface.test.tsx | 456 ++-- .../Chat/ChatMessages/ChatMessages.test.tsx | 12 +- .../ChatMessages/StatusBar/StatusBar.test.tsx | 4 +- .../ChatSubscription.test.tsx | 14 +- src/containers/MyAccount/MyAccount.test.tsx | 17 +- src/containers/MyAccount/MyAccount.tsx | 1 + .../TrialRegistration.test.tsx | 184 +- .../GroupInterface.test.tsx | 6 +- src/mocks/Chat.tsx | 5 + src/mocks/Collection.tsx | 2 + src/mocks/Contact.tsx | 13 +- src/mocks/Flow.tsx | 1 + src/mocks/Organization.tsx | 31 +- src/mocks/Search.tsx | 96 +- src/mocks/Simulator.tsx | 14 + src/mocks/StatusBar.ts | 4 + src/mocks/User.tsx | 11 +- .../AuthenticatedRoute.test.tsx | 2 +- test_output.txt | 1121 +++++++++ yarn.lock | 2038 +++++++++-------- 26 files changed, 2784 insertions(+), 1347 deletions(-) create mode 100644 test_output.txt diff --git a/src/assets/images/icons/AdvancedSearch.tsx b/src/assets/images/icons/AdvancedSearch.tsx index 1cd21235bd..745a4dd950 100644 --- a/src/assets/images/icons/AdvancedSearch.tsx +++ b/src/assets/images/icons/AdvancedSearch.tsx @@ -3,7 +3,6 @@ export const AdvancedSearchIcon = ({ isActive = false }: { isActive?: boolean }) const strokeColor = isActive ? 'none' : '#d9d9d9'; return (
{errorMessage}
; } + let displayInlineSuccessMessage: any = null; + if (inlineSuccessMessage) { + displayInlineSuccessMessage =
{inlineSuccessMessage}
; + } + const handlePasswordVisibility = () => { setShowPassword(!showPassword); }; @@ -134,12 +145,7 @@ export const Auth = ({ togglePassword: showPassword, }; - const handlePhone = - () => - (value: string): void => { - // eslint-disable-next-line - initialFormValues.phone = value; - }; + let formElements; // we should not render form elements when displaying success message @@ -162,7 +168,7 @@ export const Auth = ({ saveHandler(item); }} > - {({ submitForm, values }) => ( + {({ submitForm, values, setFieldValue }) => (
{formFields.map((field, index) => { @@ -171,7 +177,7 @@ export const Auth = ({ fieldInfo = { ...field, ...passwordFieldAdditionalInfo }; } if (field.type === 'phone') { - fieldInfo = { ...field, handlePhone }; + fieldInfo = { ...field, handlePhone: () => (value: string) => { setFieldValue('phoneNumber', value); } }; } const key = index; return ( @@ -232,6 +238,7 @@ export const Auth = ({
{displayErrorMessage} + {displayInlineSuccessMessage}
)}
diff --git a/src/containers/Chat/ChatConversations/ChatConversations.test.tsx b/src/containers/Chat/ChatConversations/ChatConversations.test.tsx index 8ff461f7d3..16e68db4de 100644 --- a/src/containers/Chat/ChatConversations/ChatConversations.test.tsx +++ b/src/containers/Chat/ChatConversations/ChatConversations.test.tsx @@ -10,7 +10,7 @@ import { ChatConversationMocks } from './ChatConversations.test.helper'; import { setUserSession } from 'services/AuthService'; setUserSession(JSON.stringify({ organization: { id: '1' }, roles: ['Admin'] })); -const cache = new InMemoryCache({ addTypename: false }); +const cache = new InMemoryCache(); cache.writeQuery({ query: SEARCH_QUERY, variables: { @@ -96,7 +96,7 @@ const simulatorParams = { const chatConversation = ( - + diff --git a/src/containers/Chat/ChatConversations/ConversationList/ConversationList.test.tsx b/src/containers/Chat/ChatConversations/ConversationList/ConversationList.test.tsx index 0a18cd9e8b..d9fe7a0a1c 100644 --- a/src/containers/Chat/ChatConversations/ConversationList/ConversationList.test.tsx +++ b/src/containers/Chat/ChatConversations/ConversationList/ConversationList.test.tsx @@ -11,8 +11,8 @@ import { cache as collectionCache } from 'config/apolloclient'; import { searchGroupQuery, waGroup } from 'mocks/Groups'; import { collection, collectionWithLoadMore, contact } from 'containers/Chat/ChatMessages/ChatMessages.test'; -const contactCache = new InMemoryCache({ addTypename: false }); -const groupsCache = new InMemoryCache({ addTypename: false }); +const contactCache = new InMemoryCache(); +const groupsCache = new InMemoryCache(); contactCache.writeQuery(contact); @@ -88,7 +88,7 @@ test('it should render conversation collection list with readMore', async () => }); }); -const collectionCacheWithSearch = new InMemoryCache({ addTypename: false }); +const collectionCacheWithSearch = new InMemoryCache(); collectionCacheWithSearch.writeQuery(collection); const searchCollectionMocks = [ @@ -104,7 +104,7 @@ test('it should render conversation collection list with searched value', async props.savedSearchCriteriaId = '2'; const { container } = render( - + @@ -133,7 +133,7 @@ const contactProps: any = { test('It render contact collection with multi-search', async () => { const { container } = render( - + @@ -160,7 +160,7 @@ test('It render contact collection with multi-search', async () => { test('It render contact collection with no result', async () => { contactProps.searchVal = ''; const { container } = render( - + @@ -195,7 +195,7 @@ const clientForGroup = new ApolloClient({ test('it renders whatsapp groups with phone number filter', async () => { const { getByText } = render( - + @@ -217,7 +217,7 @@ test('it renders whatsapp groups for multi search', async () => { searchVal: 'group 2', }; const { getByText } = render( - + diff --git a/src/containers/Chat/ChatInterface/ChatInterface.test.tsx b/src/containers/Chat/ChatInterface/ChatInterface.test.tsx index 5eaf8c5e6b..8e2fb76015 100644 --- a/src/containers/Chat/ChatInterface/ChatInterface.test.tsx +++ b/src/containers/Chat/ChatInterface/ChatInterface.test.tsx @@ -1,17 +1,16 @@ -import { MemoryRouter } from 'react-router'; +import { MemoryRouter, Route, Routes } from 'react-router'; import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { describe, expect } from 'vitest'; -import { MockedProvider } from '@apollo/client/testing'; -import { InMemoryCache } from '@apollo/client'; +import { describe, expect, vi, afterEach, test } from 'vitest'; +import { MockLink } from '@apollo/client/testing'; +import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client'; -import { DEFAULT_ENTITY_LIMIT, DEFAULT_MESSAGE_LIMIT } from 'common/constants'; +import { DEFAULT_ENTITY_LIMIT, DEFAULT_MESSAGE_LIMIT, SEARCH_QUERY_VARIABLES } from 'common/constants'; import { SEARCH_QUERY } from 'graphql/queries/Search'; import { setUserSession } from 'services/AuthService'; import { getAttachmentPermissionMock } from 'mocks/Attachment'; import { collectionCountQuery, - conversationMock, markAsReadMock, savedSearchQuery, savedSearchStatusQuery, @@ -22,90 +21,138 @@ import { collectionCountSubscription, searchQuery, searchWithDateFilters } from import ChatInterface from './ChatInterface'; import { getWhatsAppManagedPhonesStatusMock } from 'mocks/StatusBar'; +import { GET_WA_MANAGED_PHONES_STATUS } from 'graphql/queries/WaGroups'; +import { GET_ORGANIZATION_STATUS } from 'graphql/queries/Organization'; import { getAllCollectionsQuery } from 'mocks/Collection'; import { getUsersQuery } from 'mocks/User'; import { getAllFlowLabelsQuery } from 'mocks/Flow'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; -import { loadMoreQuery } from '../ChatMessages/ChatMessages.test'; dayjs.extend(utc); dayjs.extend(timezone); +window.HTMLElement.prototype.scrollIntoView = vi.fn(); + +class IntersectionObserverMock { + observe = vi.fn(); + unobserve = vi.fn(); + disconnect = vi.fn(); +} +window.IntersectionObserver = IntersectionObserverMock as any; + +vi.mock('../../../components/UI/Form/WhatsAppEditor/WhatsAppEditor', () => ({ + default: ({ setEditorState, sendMessage }: any) => ( +