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); + }); + }); +});