From ae925a197f04489a5e87537f4f30a781ee06b2bc Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Tue, 17 Dec 2024 04:38:56 +0530 Subject: [PATCH 1/4] change timestamp type from Date to string and add tests for product traing context hook --- src/CONST.ts | 1 - src/types/onyx/DismissedProductTraining.ts | 18 +- .../ProductTrainingContextProvider.tsx | 272 ++++++++++++++++++ 3 files changed, 281 insertions(+), 10 deletions(-) create mode 100644 tests/ui/components/ProductTrainingContextProvider.tsx diff --git a/src/CONST.ts b/src/CONST.ts index eddccd74c5e0..cacb36949218 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -685,7 +685,6 @@ const CONST = { COMBINED_TRACK_SUBMIT: 'combinedTrackSubmit', CATEGORY_AND_TAG_APPROVERS: 'categoryAndTagApprovers', PER_DIEM: 'newDotPerDiem', - PRODUCT_TRAINING: 'productTraining', }, BUTTON_STATES: { DEFAULT: 'default', diff --git a/src/types/onyx/DismissedProductTraining.ts b/src/types/onyx/DismissedProductTraining.ts index 53df7c403ca0..aba386448c09 100644 --- a/src/types/onyx/DismissedProductTraining.ts +++ b/src/types/onyx/DismissedProductTraining.ts @@ -17,47 +17,47 @@ type DismissedProductTraining = { /** * When user dismisses the nudgeMigration Welcome Modal, we store the timestamp here. */ - [CONST.MIGRATED_USER_WELCOME_MODAL]: Date; + [CONST.MIGRATED_USER_WELCOME_MODAL]: string; /** * When user dismisses the conciergeLHNGBR product training tooltip, we store the timestamp here. */ - [CONCEIRGE_LHN_GBR]: Date; + [CONCEIRGE_LHN_GBR]: string; /** * When user dismisses the renameSavedSearch product training tooltip, we store the timestamp here. */ - [RENAME_SAVED_SEARCH]: Date; + [RENAME_SAVED_SEARCH]: string; /** * When user dismisses the workspaceChatCreate product training tooltip, we store the timestamp here. */ - [WORKSAPCE_CHAT_CREATE]: Date; + [WORKSAPCE_CHAT_CREATE]: string; /** * When user dismisses the quickActionButton product training tooltip, we store the timestamp here. */ - [QUICK_ACTION_BUTTON]: Date; + [QUICK_ACTION_BUTTON]: string; /** * When user dismisses the searchFilterButtonTooltip product training tooltip, we store the timestamp here. */ - [SEARCH_FILTER_BUTTON_TOOLTIP]: Date; + [SEARCH_FILTER_BUTTON_TOOLTIP]: string; /** * When user dismisses the bottomNavInboxTooltip product training tooltip, we store the timestamp here. */ - [BOTTOM_NAV_INBOX_TOOLTIP]: Date; + [BOTTOM_NAV_INBOX_TOOLTIP]: string; /** * When user dismisses the lhnWorkspaceChatTooltip product training tooltip, we store the timestamp here. */ - [LHN_WORKSPACE_CHAT_TOOLTIP]: Date; + [LHN_WORKSPACE_CHAT_TOOLTIP]: string; /** * When user dismisses the globalCreateTooltip product training tooltip, we store the timestamp here. */ - [GLOBAL_CREATE_TOOLTIP]: Date; + [GLOBAL_CREATE_TOOLTIP]: string; }; export default DismissedProductTraining; diff --git a/tests/ui/components/ProductTrainingContextProvider.tsx b/tests/ui/components/ProductTrainingContextProvider.tsx new file mode 100644 index 000000000000..054daa88924f --- /dev/null +++ b/tests/ui/components/ProductTrainingContextProvider.tsx @@ -0,0 +1,272 @@ +import {renderHook} from '@testing-library/react-native'; +import Onyx from 'react-native-onyx'; +import {ProductTrainingContextProvider, useProductTrainingContext} from '@components/ProductTrainingContext'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import DateUtils from '@libs/DateUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import * as TestHelper from '../../utils/TestHelper'; +import waitForBatchedUpdatesWithAct from '../../utils/waitForBatchedUpdatesWithAct'; +import wrapOnyxWithWaitForBatchedUpdates from '../../utils/wrapOnyxWithWaitForBatchedUpdates'; + +jest.mock('@hooks/useResponsiveLayout', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + __esModule: true, + default: jest.fn(), +})); + +const DEFAULT_USE_RESPONSIVE_LAYOUT_VALUE = { + shouldUseNarrowLayout: true, + isSmallScreenWidth: true, + isInNarrowPaneModal: false, + isExtraSmallScreenHeight: false, + isMediumScreenWidth: false, + isLargeScreenWidth: false, + isExtraSmallScreenWidth: false, + isSmallScreen: false, + onboardingIsMediumOrLargerScreenWidth: false, +}; + +const TEST_USER_ACCOUNT_ID = 1; +const TEST_USER_LOGIN = 'test@test.com'; + +const wrapper = ({children}: {children: React.ReactNode}) => {children}; + +const signUpWithTestUser = () => { + TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN); +}; + +describe('ProductTrainingContextProvider', () => { + beforeAll(() => { + // Initialize Onyx + Onyx.init({ + keys: ONYXKEYS, + }); + }); + + beforeEach(() => { + // Set up test environment before each test + wrapOnyxWithWaitForBatchedUpdates(Onyx); + Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); + signUpWithTestUser(); + }); + + afterEach(async () => { + // Clean up test environment after each test + await Onyx.clear(); + await waitForBatchedUpdatesWithAct(); + }); + + const mockUseResponsiveLayout = useResponsiveLayout as jest.MockedFunction; + mockUseResponsiveLayout.mockReturnValue({...DEFAULT_USE_RESPONSIVE_LAYOUT_VALUE, shouldUseNarrowLayout: false}); + + describe('Basic Tooltip Registration', () => { + it('should not register tooltips when onboarding is not completed', async () => { + // When onboarding is not completed + Onyx.merge(ONYXKEYS.NVP_ONBOARDING, {hasCompletedGuidedSetupFlow: false}); + await waitForBatchedUpdatesWithAct(); + + const testTooltip = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.GLOBAL_CREATE_TOOLTIP; + const {result} = renderHook(() => useProductTrainingContext(testTooltip), {wrapper}); + + // Then tooltip should not show + expect(result.current.shouldShowProductTrainingTooltip).toBe(false); + }); + + it('should register tooltips when onboarding is completed and user is not migrated', async () => { + // When onboarding is completed + Onyx.merge(ONYXKEYS.NVP_ONBOARDING, {hasCompletedGuidedSetupFlow: true}); + await waitForBatchedUpdatesWithAct(); + + const testTooltip = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.GLOBAL_CREATE_TOOLTIP; + const {result} = renderHook(() => useProductTrainingContext(testTooltip), {wrapper}); + + // Then tooltip should show + expect(result.current.shouldShowProductTrainingTooltip).toBe(true); + }); + }); + + describe('Migrated User Scenarios', () => { + it('should not show tooltips for migration users before welcome modal dismissal', async () => { + // When user is a migration user and welcome modal is not dismissed + Onyx.merge(ONYXKEYS.NVP_ONBOARDING, {hasCompletedGuidedSetupFlow: true}); + Onyx.merge(ONYXKEYS.NVP_TRYNEWDOT, {nudgeMigration: {timestamp: new Date()}}); + await waitForBatchedUpdatesWithAct(); + + // Then tooltips should not show + const testTooltip = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.GLOBAL_CREATE_TOOLTIP; + const {result} = renderHook(() => useProductTrainingContext(testTooltip), {wrapper}); + + // Expect tooltip to be hidden + expect(result.current.shouldShowProductTrainingTooltip).toBe(false); + }); + + it('should show tooltips for migration users after welcome modal dismissal', async () => { + // When migration user has dismissed welcome modal + Onyx.merge(ONYXKEYS.NVP_ONBOARDING, {hasCompletedGuidedSetupFlow: true}); + Onyx.merge(ONYXKEYS.NVP_TRYNEWDOT, {nudgeMigration: {timestamp: new Date()}}); + const date = new Date(); + Onyx.set(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, { + migratedUserWelcomeModal: DateUtils.getDBTime(date.valueOf()), + }); + await waitForBatchedUpdatesWithAct(); + + const testTooltip = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.GLOBAL_CREATE_TOOLTIP; + const {result} = renderHook(() => useProductTrainingContext(testTooltip), {wrapper}); + + // Then tooltip should show + expect(result.current.shouldShowProductTrainingTooltip).toBe(true); + }); + }); + + describe('Tooltip Dismissal', () => { + it('should not show dismissed tooltips', async () => { + // When a tooltip has been dismissed + const date = new Date(); + Onyx.merge(ONYXKEYS.NVP_ONBOARDING, {hasCompletedGuidedSetupFlow: true}); + const testTooltip = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.GLOBAL_CREATE_TOOLTIP; + Onyx.merge(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, { + migratedUserWelcomeModal: DateUtils.getDBTime(date.valueOf()), + [testTooltip]: DateUtils.getDBTime(date.valueOf()), + }); + await waitForBatchedUpdatesWithAct(); + + const {result} = renderHook(() => useProductTrainingContext(testTooltip), {wrapper}); + + // Then tooltip should not show + expect(result.current.shouldShowProductTrainingTooltip).toBe(false); + }); + }); + + describe('Layout Specific Behavior', () => { + it('should handle narrow layout specific tooltips based on screen width', async () => { + // When narrow layout is false + mockUseResponsiveLayout.mockReturnValue({...DEFAULT_USE_RESPONSIVE_LAYOUT_VALUE, shouldUseNarrowLayout: false}); + + Onyx.merge(ONYXKEYS.NVP_ONBOARDING, {hasCompletedGuidedSetupFlow: true}); + await waitForBatchedUpdatesWithAct(); + + const testTooltip = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.CONCEIRGE_LHN_GBR; + const {result, rerender} = renderHook(() => useProductTrainingContext(testTooltip), {wrapper}); + // Then narrow layout tooltip should not show + expect(result.current.shouldShowProductTrainingTooltip).toBe(false); + + // When narrow layout changes to true + mockUseResponsiveLayout.mockReturnValue({...DEFAULT_USE_RESPONSIVE_LAYOUT_VALUE, shouldUseNarrowLayout: true}); + rerender({}); + await waitForBatchedUpdatesWithAct(); + + // Then narrow layout tooltip should show + expect(result.current.shouldShowProductTrainingTooltip).toBe(true); + }); + it('should handle wide layout specific tooltips based on screen width', async () => { + // When narrow layout is true + mockUseResponsiveLayout.mockReturnValue({...DEFAULT_USE_RESPONSIVE_LAYOUT_VALUE, shouldUseNarrowLayout: true}); + + Onyx.merge(ONYXKEYS.NVP_ONBOARDING, {hasCompletedGuidedSetupFlow: true}); + await waitForBatchedUpdatesWithAct(); + + const testTooltip = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.RENAME_SAVED_SEARCH; + const {result, rerender} = renderHook(() => useProductTrainingContext(testTooltip), {wrapper}); + // Then wide layout tooltip should not show + expect(result.current.shouldShowProductTrainingTooltip).toBe(false); + + // When narrow layout changes to false + mockUseResponsiveLayout.mockReturnValue({...DEFAULT_USE_RESPONSIVE_LAYOUT_VALUE, shouldUseNarrowLayout: false}); + rerender({}); + await waitForBatchedUpdatesWithAct(); + + // Then wide layout tooltip should show + expect(result.current.shouldShowProductTrainingTooltip).toBe(true); + }); + }); + + describe('Priority Handling', () => { + it('should show only highest priority tooltip when multiple are active', async () => { + // When multiple tooltips are registered and no tooltips are dismissed + Onyx.merge(ONYXKEYS.NVP_ONBOARDING, {hasCompletedGuidedSetupFlow: true}); + const date = new Date(); + Onyx.merge(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, { + migratedUserWelcomeModal: DateUtils.getDBTime(date.valueOf()), + }); + await waitForBatchedUpdatesWithAct(); + + // Then only highest priority tooltip should show + const highPriorityTooltip = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.GLOBAL_CREATE_TOOLTIP; + const lowPriorityTooltip = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SEARCH_FILTER_BUTTON_TOOLTIP; + + const {result} = renderHook( + () => ({ + higher: useProductTrainingContext(highPriorityTooltip), + lower: useProductTrainingContext(lowPriorityTooltip), + }), + {wrapper}, + ); + + // Expect only higher priority tooltip to be visible + expect(result.current.higher.shouldShowProductTrainingTooltip).toBe(true); + expect(result.current.lower.shouldShowProductTrainingTooltip).toBe(false); + }); + + it('should show lower priority tooltip when higher priority is dismissed', async () => { + // When higher priority tooltip is dismissed + Onyx.merge(ONYXKEYS.NVP_ONBOARDING, {hasCompletedGuidedSetupFlow: true}); + const date = new Date(); + const highPriorityTooltip = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.GLOBAL_CREATE_TOOLTIP; + const lowPriorityTooltip = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SEARCH_FILTER_BUTTON_TOOLTIP; + + Onyx.merge(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, { + migratedUserWelcomeModal: DateUtils.getDBTime(date.valueOf()), + [highPriorityTooltip]: DateUtils.getDBTime(date.valueOf()), + }); + await waitForBatchedUpdatesWithAct(); + + // Then lower priority tooltip should show + const {result} = renderHook( + () => ({ + higher: useProductTrainingContext(highPriorityTooltip), + lower: useProductTrainingContext(lowPriorityTooltip), + }), + {wrapper}, + ); + + // Expect higher priority tooltip to be hidden and lower priority to be visible + expect(result.current.higher.shouldShowProductTrainingTooltip).toBe(false); + expect(result.current.lower.shouldShowProductTrainingTooltip).toBe(true); + }); + + it('should transition to next priority tooltip when current is dismissed', async () => { + // When starting with all tooltips visible + Onyx.merge(ONYXKEYS.NVP_ONBOARDING, {hasCompletedGuidedSetupFlow: true}); + const date = new Date(); + Onyx.merge(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, { + migratedUserWelcomeModal: DateUtils.getDBTime(date.valueOf()), + }); + await waitForBatchedUpdatesWithAct(); + + const highPriorityTooltip = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.GLOBAL_CREATE_TOOLTIP; + const lowPriorityTooltip = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SEARCH_FILTER_BUTTON_TOOLTIP; + + const {result} = renderHook( + () => ({ + higher: useProductTrainingContext(highPriorityTooltip), + lower: useProductTrainingContext(lowPriorityTooltip), + }), + {wrapper}, + ); + + // Then initially higher priority should be visible + expect(result.current.higher.shouldShowProductTrainingTooltip).toBe(true); + expect(result.current.lower.shouldShowProductTrainingTooltip).toBe(false); + + // When dismissing higher priority tooltip + Onyx.merge(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, { + [highPriorityTooltip]: DateUtils.getDBTime(date.valueOf()), + }); + await waitForBatchedUpdatesWithAct(); + + // Then lower priority tooltip should become visible + expect(result.current.lower.shouldShowProductTrainingTooltip).toBe(true); + }); + }); +}); From 77f47456b4e934b1cd9adeef1a4d42e96bc35a96 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Tue, 17 Dec 2024 04:47:40 +0530 Subject: [PATCH 2/4] update test descriptions for migrated users in ProductTrainingContextProvider --- tests/ui/components/ProductTrainingContextProvider.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/ui/components/ProductTrainingContextProvider.tsx b/tests/ui/components/ProductTrainingContextProvider.tsx index 054daa88924f..ea1b567c10b3 100644 --- a/tests/ui/components/ProductTrainingContextProvider.tsx +++ b/tests/ui/components/ProductTrainingContextProvider.tsx @@ -87,8 +87,8 @@ describe('ProductTrainingContextProvider', () => { }); describe('Migrated User Scenarios', () => { - it('should not show tooltips for migration users before welcome modal dismissal', async () => { - // When user is a migration user and welcome modal is not dismissed + it('should not show tooltips for migrated users before welcome modal dismissal', async () => { + // When user is a migrated user and welcome modal is not dismissed Onyx.merge(ONYXKEYS.NVP_ONBOARDING, {hasCompletedGuidedSetupFlow: true}); Onyx.merge(ONYXKEYS.NVP_TRYNEWDOT, {nudgeMigration: {timestamp: new Date()}}); await waitForBatchedUpdatesWithAct(); @@ -101,8 +101,8 @@ describe('ProductTrainingContextProvider', () => { expect(result.current.shouldShowProductTrainingTooltip).toBe(false); }); - it('should show tooltips for migration users after welcome modal dismissal', async () => { - // When migration user has dismissed welcome modal + it('should show tooltips for migrated users after welcome modal dismissal', async () => { + // When migrated user has dismissed welcome modal Onyx.merge(ONYXKEYS.NVP_ONBOARDING, {hasCompletedGuidedSetupFlow: true}); Onyx.merge(ONYXKEYS.NVP_TRYNEWDOT, {nudgeMigration: {timestamp: new Date()}}); const date = new Date(); From 55edcf8659e228dc8076e8f0f215544c0d74cd85 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Thu, 2 Jan 2025 00:03:52 +0530 Subject: [PATCH 3/4] Add test for hiding product training tooltip on user dismissal --- .../ProductTrainingContextProvider.tsx | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/ui/components/ProductTrainingContextProvider.tsx b/tests/ui/components/ProductTrainingContextProvider.tsx index ea1b567c10b3..0177908ec87d 100644 --- a/tests/ui/components/ProductTrainingContextProvider.tsx +++ b/tests/ui/components/ProductTrainingContextProvider.tsx @@ -136,6 +136,35 @@ describe('ProductTrainingContextProvider', () => { // Then tooltip should not show expect(result.current.shouldShowProductTrainingTooltip).toBe(false); }); + it('should hide tooltip when hideProductTrainingTooltip is called', async () => { + // When migrated user has dismissed welcome modal + Onyx.merge(ONYXKEYS.NVP_ONBOARDING, {hasCompletedGuidedSetupFlow: true}); + Onyx.merge(ONYXKEYS.NVP_TRYNEWDOT, {nudgeMigration: {timestamp: new Date()}}); + const date = new Date(); + Onyx.set(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, { + migratedUserWelcomeModal: DateUtils.getDBTime(date.valueOf()), + }); + await waitForBatchedUpdatesWithAct(); + const testTooltip = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.GLOBAL_CREATE_TOOLTIP; + const {result, rerender} = renderHook(() => useProductTrainingContext(testTooltip), {wrapper}); + // When the user dismiss the tooltip + result.current.hideProductTrainingTooltip(); + rerender({}); + // Then tooltip should not show + expect(result.current.shouldShowProductTrainingTooltip).toBe(false); + // And dismissed tooltip should be recorded in Onyx + const dismissedTooltipsOnyxState = await new Promise((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, + callback: (dismissedTooltips) => { + Onyx.disconnect(connection); + resolve(dismissedTooltips); + }, + }); + }); + // Expect dismissed tooltip to be recorded + expect(dismissedTooltipsOnyxState).toHaveProperty(testTooltip); + }); }); describe('Layout Specific Behavior', () => { From 15d0dabe7825fc7a62720b4744788a3d911ad47c Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Thu, 2 Jan 2025 23:22:01 +0530 Subject: [PATCH 4/4] Add NEWDOT_MERGE_ACCOUNTS constant to CONST --- src/CONST.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CONST.ts b/src/CONST.ts index ee99c8e49563..54aef6c6f439 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -687,6 +687,7 @@ const CONST = { COMBINED_TRACK_SUBMIT: 'combinedTrackSubmit', CATEGORY_AND_TAG_APPROVERS: 'categoryAndTagApprovers', PER_DIEM: 'newDotPerDiem', + NEWDOT_MERGE_ACCOUNTS: 'newDotMergeAccounts', }, BUTTON_STATES: { DEFAULT: 'default',