From ae925a197f04489a5e87537f4f30a781ee06b2bc Mon Sep 17 00:00:00 2001
From: Ishpaul Singh <ishpaul207@gmail.com>
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}) => <ProductTrainingContextProvider>{children}</ProductTrainingContextProvider>;
+
+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<typeof useResponsiveLayout>;
+    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 <ishpaul207@gmail.com>
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 <ishpaul207@gmail.com>
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 <ishpaul207@gmail.com>
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',
