diff --git a/src/CONST.ts b/src/CONST.ts
index a1816688868d..54aef6c6f439 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -687,7 +687,6 @@ const CONST = {
COMBINED_TRACK_SUBMIT: 'combinedTrackSubmit',
CATEGORY_AND_TAG_APPROVERS: 'categoryAndTagApprovers',
PER_DIEM: 'newDotPerDiem',
- PRODUCT_TRAINING: 'productTraining',
NEWDOT_MERGE_ACCOUNTS: 'newDotMergeAccounts',
},
BUTTON_STATES: {
diff --git a/tests/ui/components/ProductTrainingContextProvider.tsx b/tests/ui/components/ProductTrainingContextProvider.tsx
new file mode 100644
index 000000000000..0177908ec87d
--- /dev/null
+++ b/tests/ui/components/ProductTrainingContextProvider.tsx
@@ -0,0 +1,301 @@
+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 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();
+
+ // 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 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();
+ 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);
+ });
+ 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', () => {
+ 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);
+ });
+ });
+});