diff --git a/pages/accountLists/[accountListId]/settings/integrations/index.page.test.tsx b/pages/accountLists/[accountListId]/settings/integrations/index.page.test.tsx index c12f4854c..613fbd7f5 100644 --- a/pages/accountLists/[accountListId]/settings/integrations/index.page.test.tsx +++ b/pages/accountLists/[accountListId]/settings/integrations/index.page.test.tsx @@ -22,6 +22,7 @@ const push = jest.fn(); const router = { query: { accountListId }, + pathname: '/accountLists/[accountListId]/settings/integrations', isReady: true, push, }; diff --git a/pages/accountLists/[accountListId]/settings/integrations/index.page.tsx b/pages/accountLists/[accountListId]/settings/integrations/index.page.tsx index ada4b543c..ae632395f 100644 --- a/pages/accountLists/[accountListId]/settings/integrations/index.page.tsx +++ b/pages/accountLists/[accountListId]/settings/integrations/index.page.tsx @@ -29,7 +29,7 @@ const Integrations: React.FC = () => { const accountListId = useAccountListId() || ''; const { appName } = useGetAppSettings(); const { enqueueSnackbar } = useSnackbar(); - const { settingUp } = useSetupContext(); + const { onSetupTour } = useSetupContext(); const [setup, setSetup] = useState(0); const setupAccordions = ['google', 'mailchimp', 'prayerletters.com']; @@ -37,7 +37,7 @@ const Integrations: React.FC = () => { const [updateUserOptions] = useUpdateUserOptionsMutation(); const handleSetupChange = async () => { - if (!settingUp) { + if (!onSetupTour) { return; } const nextNav = setup + 1; @@ -70,10 +70,10 @@ const Integrations: React.FC = () => { }, []); useEffect(() => { - if (settingUp) { + if (onSetupTour) { setExpandedPanel(setupAccordions[0]); } - }, [settingUp]); + }, [onSetupTour]); return ( { pageHeading={t('Connect Services')} selectedMenuId="integrations" > - {settingUp && ( + {onSetupTour && ( { diff --git a/pages/accountLists/[accountListId]/settings/notifications.page.test.tsx b/pages/accountLists/[accountListId]/settings/notifications.page.test.tsx index 9dc078d7c..a8e8bf8a0 100644 --- a/pages/accountLists/[accountListId]/settings/notifications.page.test.tsx +++ b/pages/accountLists/[accountListId]/settings/notifications.page.test.tsx @@ -23,6 +23,7 @@ const push = jest.fn(); const router = { query: { accountListId }, + pathname: '/accountLists/[accountListId]/settings/notifications', isReady: true, push, }; diff --git a/pages/accountLists/[accountListId]/settings/notifications.page.tsx b/pages/accountLists/[accountListId]/settings/notifications.page.tsx index 3eec9cd72..385fc0420 100644 --- a/pages/accountLists/[accountListId]/settings/notifications.page.tsx +++ b/pages/accountLists/[accountListId]/settings/notifications.page.tsx @@ -19,12 +19,12 @@ const Notifications: React.FC = () => { const accountListId = useAccountListId() || ''; const { push } = useRouter(); const { enqueueSnackbar } = useSnackbar(); - const { settingUp } = useSetupContext(); + const { onSetupTour } = useSetupContext(); const [updateUserOptions] = useUpdateUserOptionsMutation(); const handleSetupChange = async () => { - if (!settingUp) { + if (!onSetupTour) { return; } @@ -48,7 +48,7 @@ const Notifications: React.FC = () => { pageHeading={t('Notifications')} selectedMenuId="notifications" > - {settingUp && ( + {onSetupTour && ( { const accountListId = useAccountListId() || ''; const { push, query } = useRouter(); const { enqueueSnackbar } = useSnackbar(); - const { settingUp } = useSetupContext(); + const { onSetupTour } = useSetupContext(); const setupAccordions = ['locale', 'monthly goal', 'home country']; const [setup, setSetup] = useState(0); @@ -85,10 +85,10 @@ const Preferences: React.FC = () => { }, []); useEffect(() => { - if (settingUp) { + if (onSetupTour) { setExpandedPanel(setupAccordions[0]); } - }, [settingUp]); + }, [onSetupTour]); const handleAccordionChange = (panel: string) => { const panelLowercase = panel.toLowerCase(); @@ -111,7 +111,7 @@ const Preferences: React.FC = () => { }; const handleSetupChange = async () => { - if (!settingUp) { + if (!onSetupTour) { return; } const nextNav = setup + 1; @@ -154,7 +154,7 @@ const Preferences: React.FC = () => { pageHeading={t('Preferences')} selectedMenuId={'preferences'} > - {settingUp && ( + {onSetupTour && ( { handleAccordionChange={handleAccordionChange} expandedPanel={expandedPanel} locale={personalPreferencesData?.user?.preferences?.locale || ''} - disabled={settingUp} + disabled={onSetupTour} /> { localeDisplay={ personalPreferencesData?.user?.preferences?.localeDisplay || '' } - disabled={settingUp && setup !== 0} + disabled={onSetupTour && setup !== 0} handleSetupChange={handleSetupChange} /> { defaultAccountList={ personalPreferencesData?.user?.defaultAccountList || '' } - disabled={settingUp} + disabled={onSetupTour} /> { personalPreferencesData?.user?.preferences?.timeZone || '' } timeZones={timeZones} - disabled={settingUp} + disabled={onSetupTour} /> { personalPreferencesData?.user?.preferences ?.hourToSendNotifications || null } - disabled={settingUp} + disabled={onSetupTour} /> )} @@ -242,7 +242,7 @@ const Preferences: React.FC = () => { expandedPanel={expandedPanel} name={accountPreferencesData?.accountList?.name || ''} accountListId={accountListId} - disabled={settingUp} + disabled={onSetupTour} /> { currency={ accountPreferencesData?.accountList?.settings?.currency || '' } - disabled={settingUp && setup !== 1} + disabled={onSetupTour && setup !== 1} handleSetupChange={handleSetupChange} /> { } accountListId={accountListId} countries={countries} - disabled={settingUp && setup !== 2} + disabled={onSetupTour && setup !== 2} handleSetupChange={handleSetupChange} /> { accountPreferencesData?.accountList?.settings?.currency || '' } accountListId={accountListId} - disabled={settingUp} + disabled={onSetupTour} /> {userOrganizationAccountsData?.userOrganizationAccounts && userOrganizationAccountsData?.userOrganizationAccounts?.length > @@ -290,7 +290,7 @@ const Preferences: React.FC = () => { '' } accountListId={accountListId} - disabled={settingUp} + disabled={onSetupTour} /> )} { accountPreferencesData?.accountList?.settings?.tester || false } accountListId={accountListId} - disabled={settingUp} + disabled={onSetupTour} /> { accountPreferencesData?.accountList?.settings?.currency || '' } accountListId={accountListId} - disabled={settingUp} + disabled={onSetupTour} /> {canUserExportData?.canUserExportData.allowed && ( { } accountListId={accountListId} data={personalPreferencesData} - disabled={settingUp} + disabled={onSetupTour} /> )} diff --git a/src/components/Layouts/Primary/LogoLink/LogoLink.test.tsx b/src/components/Layouts/Primary/LogoLink/LogoLink.test.tsx new file mode 100644 index 000000000..042a26366 --- /dev/null +++ b/src/components/Layouts/Primary/LogoLink/LogoLink.test.tsx @@ -0,0 +1,23 @@ +import { render } from '@testing-library/react'; +import { TestSetupProvider } from 'src/components/Setup/SetupProvider'; +import { LogoLink } from './LogoLink'; + +describe('LogoLink', () => { + it('renders a link when not on the setup tour', () => { + const { getByRole } = render( + + + , + ); + expect(getByRole('link')).toBeInTheDocument(); + }); + + it('does not render a link when on the setup tour', () => { + const { queryByRole } = render( + + + , + ); + expect(queryByRole('link')).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/Layouts/Primary/LogoLink/LogoLink.tsx b/src/components/Layouts/Primary/LogoLink/LogoLink.tsx new file mode 100644 index 000000000..8ccbe0398 --- /dev/null +++ b/src/components/Layouts/Primary/LogoLink/LogoLink.tsx @@ -0,0 +1,17 @@ +import NextLink from 'next/link'; +import { useSetupContext } from 'src/components/Setup/SetupProvider'; + +export const LogoLink: React.FC = () => { + const { onSetupTour } = useSetupContext(); + + const logo = logo; + + return onSetupTour ? ( + logo + ) : ( + + {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} + {logo} + + ); +}; diff --git a/src/components/Layouts/Primary/NavBar/NavBar.test.tsx b/src/components/Layouts/Primary/NavBar/NavBar.test.tsx index e57deb2f5..8c012870d 100644 --- a/src/components/Layouts/Primary/NavBar/NavBar.test.tsx +++ b/src/components/Layouts/Primary/NavBar/NavBar.test.tsx @@ -2,42 +2,64 @@ import React from 'react'; import { MockedProvider } from '@apollo/client/testing'; import { ThemeProvider } from '@mui/material/styles'; import { render } from '@testing-library/react'; +import TestRouter from '__tests__/util/TestRouter'; +import { TestSetupProvider } from 'src/components/Setup/SetupProvider'; import theme from 'src/theme'; import { getTopBarMultipleMock } from '../TopBar/TopBar.mock'; import { NavBar } from './NavBar'; -jest.mock('next/router', () => ({ - useRouter: () => { - return { - query: { accountListId: 'abc' }, - isReady: true, - }; - }, -})); +const router = { + query: { accountListId: 'abc' }, + isReady: true, + push: jest.fn(), +}; + +interface TestComponentProps { + openMobile?: boolean; + onSetupTour?: boolean; +} + +const TestComponent: React.FC = ({ + openMobile = false, + onSetupTour, +}) => ( + + + + + + + + + +); const onMobileClose = jest.fn(); const mocks = [getTopBarMultipleMock()]; describe('NavBar', () => { - it('default', async () => { - const { queryByTestId } = render( - - - , - ); + it('default', () => { + const { queryByTestId } = render(); expect(queryByTestId('NavBarDrawer')).not.toBeInTheDocument(); }); - it('opened', async () => { - const { queryByTestId } = render( - - - - - , + it('opened', () => { + const { getAllByRole, queryByTestId } = render( + , ); expect(queryByTestId('NavBarDrawer')).toBeInTheDocument(); + expect( + getAllByRole('button', { name: 'Dashboard' })[0], + ).toBeInTheDocument(); + }); + + it('hides links during the setup tour', () => { + const { queryByRole } = render(); + + expect( + queryByRole('button', { name: 'Dashboard' }), + ).not.toBeInTheDocument(); }); }); diff --git a/src/components/Layouts/Primary/NavBar/NavBar.tsx b/src/components/Layouts/Primary/NavBar/NavBar.tsx index 8e27881a6..9a9a9d2b2 100644 --- a/src/components/Layouts/Primary/NavBar/NavBar.tsx +++ b/src/components/Layouts/Primary/NavBar/NavBar.tsx @@ -1,13 +1,15 @@ -import NextLink, { LinkProps } from 'next/link'; +import { LinkProps } from 'next/link'; import { useRouter } from 'next/router'; import React, { ReactElement, useEffect } from 'react'; import type { FC } from 'react'; import { Box, Drawer, Hidden, List, Theme, useMediaQuery } from '@mui/material'; import { useTranslation } from 'react-i18next'; import { makeStyles } from 'tss-react/mui'; +import { useSetupContext } from 'src/components/Setup/SetupProvider'; import { reportNavItems } from 'src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenuItems'; import { ToolsListNav } from 'src/components/Tool/Home/ToolsListNav'; import { useAccountListId } from 'src/hooks/useAccountListId'; +import { LogoLink } from '../LogoLink/LogoLink'; import { toolsRedirectLinks } from '../TopBar/Items/NavMenu/NavMenu'; import { NavItem } from './NavItem/NavItem'; import { NavTools } from './NavTools/NavTools'; @@ -114,6 +116,7 @@ export const NavBar: FC = ({ onMobileClose, openMobile }) => { const accountListId = useAccountListId(); const { pathname } = useRouter(); const { t } = useTranslation(); + const { onSetupTour } = useSetupContext(); const sections: Section[] = [ { @@ -174,21 +177,17 @@ export const NavBar: FC = ({ onMobileClose, openMobile }) => { variant="temporary" > - - logo - - - - {renderNavItems({ - accountListId, - items: sections, - pathname, - })} + + {!onSetupTour && ( + + {renderNavItems({ + accountListId, + items: sections, + pathname, + })} + + )} diff --git a/src/components/Layouts/Primary/NavBar/NavTools/NavTools.test.tsx b/src/components/Layouts/Primary/NavBar/NavTools/NavTools.test.tsx index 69137a242..6343ad7e2 100644 --- a/src/components/Layouts/Primary/NavBar/NavTools/NavTools.test.tsx +++ b/src/components/Layouts/Primary/NavBar/NavTools/NavTools.test.tsx @@ -1,32 +1,51 @@ import React from 'react'; import { MockedProvider } from '@apollo/client/testing'; import { ThemeProvider } from '@mui/material/styles'; -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; +import TestRouter from '__tests__/util/TestRouter'; +import { TestSetupProvider } from 'src/components/Setup/SetupProvider'; import theme from 'src/theme'; import { getTopBarMultipleMock } from '../../TopBar/TopBar.mock'; import { NavTools } from './NavTools'; -jest.mock('next/router', () => ({ - useRouter: () => { - return { - query: { accountListId: 'abc' }, - isReady: true, - }; - }, -})); +const router = { + query: { accountListId: 'abc' }, + isReady: true, + push: jest.fn(), +}; -const mocks = [getTopBarMultipleMock()]; +interface TestComponentProps { + onSetupTour?: boolean; +} -describe('AddMenuPanel', () => { - it('default', async () => { - const { getByTestId } = render( - - +const TestComponent: React.FC = ({ onSetupTour }) => ( + + + + - - , - ); + + + + +); + +describe('NavTools', () => { + it('default', async () => { + const { findByText, getByTestId, getByText } = render(); expect(getByTestId('NavTools')).toBeInTheDocument(); + expect(getByText('Add')).toBeInTheDocument(); + expect(await findByText('John Smith')).toBeInTheDocument(); + }); + + it('hides links during the setup tour', async () => { + const { queryByText, getByTestId } = render(); + + expect(getByTestId('NavTools')).toBeInTheDocument(); + expect(queryByText('Add')).not.toBeInTheDocument(); + await waitFor(() => + expect(queryByText('John Smith')).not.toBeInTheDocument(), + ); }); }); diff --git a/src/components/Layouts/Primary/NavBar/NavTools/NavTools.tsx b/src/components/Layouts/Primary/NavBar/NavTools/NavTools.tsx index 3553a4671..567003d81 100644 --- a/src/components/Layouts/Primary/NavBar/NavTools/NavTools.tsx +++ b/src/components/Layouts/Primary/NavBar/NavTools/NavTools.tsx @@ -6,6 +6,7 @@ import NotificationsIcon from '@mui/icons-material/Notifications'; import SearchIcon from '@mui/icons-material/Search'; import { List } from '@mui/material'; import { useTranslation } from 'react-i18next'; +import { useSetupContext } from 'src/components/Setup/SetupProvider'; import { useGetTopBarQuery } from '../../TopBar/GetTopBar.generated'; import NotificationMenu from '../../TopBar/Items/NotificationMenu/NotificationMenu'; import { NavItem } from '../NavItem/NavItem'; @@ -16,18 +17,23 @@ import { SearchMenuPanel } from './SearchMenuPanel/SearchMenuPanel'; export const NavTools: FC = () => { const { data } = useGetTopBarQuery(); const { t } = useTranslation(); + const { onSetupTour } = useSetupContext(); return ( - - - - - - - - - + {!onSetupTour && ( + <> + + + + + + + + + + + )} { - it('default', async () => { - const { getByTestId } = render( - - +interface TestComponentProps { + onSetupTour?: boolean; +} + +const TestComponent: React.FC = ({ onSetupTour }) => ( + + + + - - , - ); + + + + +); + +describe('ProfileMenuPanelForNavBar', () => { + it('default', () => { + const { getByTestId } = render(); expect(getByTestId('ProfileMenuPanelForNavBar')).toBeInTheDocument(); }); it('render an account list button', async () => { - const { getByTestId } = render( - - - - - - - , - ); + const { findByTestId, getByTestId, getByText } = render(); - await waitFor(() => - expect(getByTestId('accountListSelectorButton')).toBeInTheDocument(), - ); - userEvent.click(getByTestId('accountListSelectorButton')); + userEvent.click(await findByTestId('accountListSelectorButton')); expect(getByTestId('accountListButton-1')).toBeInTheDocument(); expect(getByTestId('accountListButton-1')).toHaveStyle( 'backgroundColor: #383F43;', ); + expect(getByText('Preferences')).toBeInTheDocument(); }); it('should toggle the account list selector drawer', async () => { - const { getByTestId, queryByTestId } = render( - - - - - - - , + const { findByTestId, getByTestId, queryByTestId } = render( + , ); - await waitFor(() => - expect(getByTestId('accountListSelectorButton')).toBeInTheDocument(), - ); + expect(await findByTestId('accountListSelectorButton')).toBeInTheDocument(); expect( queryByTestId('closeAccountListDrawerButton'), ).not.toBeInTheDocument(); @@ -71,20 +64,9 @@ describe('ProfileMenuPanelForNavBar', () => { }); it('should call router push', async () => { - const { getByTestId } = render( - - - - - - - , - ); + const { findByTestId, getByTestId } = render(); - await waitFor(() => - expect(getByTestId('accountListSelectorButton')).toBeInTheDocument(), - ); - userEvent.click(getByTestId('accountListSelectorButton')); + userEvent.click(await findByTestId('accountListSelectorButton')); userEvent.click(getByTestId('accountListButton-1')); await waitFor(() => expect(router.push).toHaveBeenCalledWith({ @@ -94,19 +76,21 @@ describe('ProfileMenuPanelForNavBar', () => { ); }); - it('Ensure Sign Out is called with callback', async () => { - const { getByText } = render( - - - - - - - , - ); + it('Ensure Sign Out is called with callback', () => { + const { getByRole } = render(); - await waitFor(() => expect(getByText(/sign out/i)).toBeInTheDocument()); - userEvent.click(getByText(/sign out/i)); + userEvent.click(getByRole('button', { name: 'Sign Out' })); expect(signOut).toHaveBeenCalledWith({ callbackUrl: 'signOut' }); }); + + it('hides links during the setup tour', async () => { + const { findByTestId, getByRole, getByTestId, queryByText } = render( + , + ); + + userEvent.click(await findByTestId('accountListSelectorButton')); + userEvent.click(getByTestId('accountListButton-1')); + expect(getByRole('button', { name: 'Sign Out' })).toBeInTheDocument(); + expect(queryByText('Preferences')).not.toBeInTheDocument(); + }); }); diff --git a/src/components/Layouts/Primary/NavBar/NavTools/ProfileMenuPanel/ProfileMenuPanel.tsx b/src/components/Layouts/Primary/NavBar/NavTools/ProfileMenuPanel/ProfileMenuPanel.tsx index e3a2f1867..7fc34f428 100644 --- a/src/components/Layouts/Primary/NavBar/NavTools/ProfileMenuPanel/ProfileMenuPanel.tsx +++ b/src/components/Layouts/Primary/NavBar/NavTools/ProfileMenuPanel/ProfileMenuPanel.tsx @@ -7,6 +7,7 @@ import { Box, Button, Drawer, List } from '@mui/material'; import { styled } from '@mui/material/styles'; import { signOut } from 'next-auth/react'; import { useTranslation } from 'react-i18next'; +import { useSetupContext } from 'src/components/Setup/SetupProvider'; import { PrivacyPolicyLink, TermsOfUseLink, @@ -83,6 +84,7 @@ export const ProfileMenuPanel: React.FC = () => { const accountListId = useAccountListId(); const { push, pathname } = useRouter(); const client = useApolloClient(); + const { onSetupTour } = useSetupContext(); const [accountsDrawerOpen, setAccountsDrawerOpen] = useState(false); const toggleAccountsDrawer = (): void => { @@ -162,54 +164,58 @@ export const ProfileMenuPanel: React.FC = () => { )} - {addProfileContent.map(({ text, path, onClick }, index) => ( - - - {t(text)} - - - ))} - {(data?.user?.admin || - !!data?.user?.administrativeOrganizations?.nodes?.length) && ( - - - {t('Manage Organizations')} - - - )} - {(data?.user?.admin || data?.user?.developer) && ( - - - {t('Admin Console')} - - - )} - {data?.user?.developer && ( - - - - {t('Backend Admin')} - - - - )} - {data?.user?.developer && ( - - - - {t('Sidekiq')} - - - + {!onSetupTour && ( + <> + {addProfileContent.map(({ text, path, onClick }, index) => ( + + + {t(text)} + + + ))} + {(data?.user?.admin || + !!data?.user?.administrativeOrganizations?.nodes?.length) && ( + + + {t('Manage Organizations')} + + + )} + {(data?.user?.admin || data?.user?.developer) && ( + + + {t('Admin Console')} + + + )} + {data?.user?.developer && ( + + + + {t('Backend Admin')} + + + + )} + {data?.user?.developer && ( + + + + {t('Sidekiq')} + + + + )} + )} diff --git a/src/components/Layouts/Primary/Primary.test.tsx b/src/components/Layouts/Primary/Primary.test.tsx index 5d1ecf2aa..22b4a89e7 100644 --- a/src/components/Layouts/Primary/Primary.test.tsx +++ b/src/components/Layouts/Primary/Primary.test.tsx @@ -4,6 +4,7 @@ import { ThemeProvider } from '@mui/material/styles'; import { render } from '@testing-library/react'; import TestWrapper from '__tests__/util/TestWrapper'; import matchMediaMock from '__tests__/util/matchMediaMock'; +import { SetupProvider } from 'src/components/Setup/SetupProvider'; import theme from '../../../theme'; import { getNotificationsMocks } from './TopBar/Items/NotificationMenu/NotificationMenu.mock'; import { getTopBarMock } from './TopBar/TopBar.mock'; @@ -28,9 +29,11 @@ describe('Primary', () => { const { getByTestId } = render( - -
-
+ + +
+
+
, ); diff --git a/src/components/Layouts/Primary/TopBar/Items/ProfileMenu/ProfileMenu.test.tsx b/src/components/Layouts/Primary/TopBar/Items/ProfileMenu/ProfileMenu.test.tsx index 02410f618..d4abc718c 100644 --- a/src/components/Layouts/Primary/TopBar/Items/ProfileMenu/ProfileMenu.test.tsx +++ b/src/components/Layouts/Primary/TopBar/Items/ProfileMenu/ProfileMenu.test.tsx @@ -1,4 +1,6 @@ +import { NextRouter } from 'next/router'; import React from 'react'; +import { MockedResponse } from '@apollo/client/testing'; import { ThemeProvider } from '@mui/material/styles'; import userEvent from '@testing-library/user-event'; import fetchMock from 'jest-fetch-mock'; @@ -7,6 +9,7 @@ import { session } from '__tests__/fixtures/session'; import TestRouter from '__tests__/util/TestRouter'; import TestWrapper from '__tests__/util/TestWrapper'; import { render, waitFor } from '__tests__/util/testingLibraryReactMock'; +import { TestSetupProvider } from 'src/components/Setup/SetupProvider'; import theme from '../../../../../../theme'; import { getTopBarMock, @@ -27,7 +30,7 @@ jest.mock('notistack', () => ({ }, })); -const router = { +const defaultRouter = { pathname: '/accountLists/[accountListId]/test', query: { accountListId: '1' }, isReady: true, @@ -40,36 +43,48 @@ const routerNoAccountListId = { push: jest.fn(), }; +interface TestComponentProps { + router?: Partial; + mocks?: MockedResponse[]; + onSetupTour?: boolean; +} + +const TestComponent: React.FC = ({ + router, + mocks, + onSetupTour, +}) => ( + + + + + + + + + +); + describe('ProfileMenu', () => { it('default', async () => { - const { getByTestId, queryByText, getByText } = render( - - - - - - - , + const { getByTestId, getByRole, getByText, findByText } = render( + , ); - await waitFor(() => expect(getByText('John Smith')).toBeInTheDocument()); + expect(await findByText('John Smith')).toBeInTheDocument(); userEvent.click(getByTestId('profileMenuButton')); - expect(queryByText('Manage Organizations')).toBeInTheDocument(); - expect(queryByText('Admin Console')).toBeInTheDocument(); - expect(queryByText('Backend Admin')).toBeInTheDocument(); - expect(queryByText('Sidekiq')).toBeInTheDocument(); + expect(getByText('Preferences')).toBeInTheDocument(); + expect(getByText('Manage Organizations')).toBeInTheDocument(); + expect(getByText('Admin Console')).toBeInTheDocument(); + expect(getByText('Backend Admin')).toBeInTheDocument(); + expect(getByText('Sidekiq')).toBeInTheDocument(); + expect(getByRole('button', { name: 'Sign Out' })).toBeInTheDocument(); }); it('should not show setting links when no accountListId selected', async () => { - const { getByTestId, queryByText, getByText } = render( - - - - - - - , + const { getByTestId, queryByText, findByText } = render( + , ); - await waitFor(() => expect(getByText('John Smith')).toBeInTheDocument()); + expect(await findByText('John Smith')).toBeInTheDocument(); userEvent.click(getByTestId('profileMenuButton')); expect(queryByText('Manage Organizations')).not.toBeInTheDocument(); expect(queryByText('Admin Console')).not.toBeInTheDocument(); @@ -78,21 +93,13 @@ describe('ProfileMenu', () => { }); it('should change account list in the router', async () => { - const { getByTestId, getByText } = render( - - - - - - - , - ); - await waitFor(() => expect(getByText('John Smith')).toBeInTheDocument()); + const { getByTestId, findByText } = render(); + expect(await findByText('John Smith')).toBeInTheDocument(); userEvent.click(getByTestId('profileMenuButton')); userEvent.click(getByTestId('accountListSelector')); userEvent.click(getByTestId('accountListButton-1')); - await waitFor(() => expect(router.push).toHaveBeenCalled()); - expect(router.push).toHaveBeenCalledWith({ + await waitFor(() => expect(defaultRouter.push).toHaveBeenCalled()); + expect(defaultRouter.push).toHaveBeenCalledWith({ pathname: '/accountLists/[accountListId]/test', query: { accountListId: '1', @@ -101,27 +108,21 @@ describe('ProfileMenu', () => { }); it('should change account list in the router and persist query parameters', async () => { - const { getByTestId, getByText } = render( - - - - - - - , + const { getByTestId, findByText } = render( + , ); - await waitFor(() => expect(getByText('John Smith')).toBeInTheDocument()); + expect(await findByText('John Smith')).toBeInTheDocument(); userEvent.click(getByTestId('profileMenuButton')); userEvent.click(getByTestId('accountListSelector')); userEvent.click(getByTestId('accountListButton-1')); - await waitFor(() => expect(router.push).toHaveBeenCalled()); - expect(router.push).toHaveBeenCalledWith({ + await waitFor(() => expect(defaultRouter.push).toHaveBeenCalled()); + expect(defaultRouter.push).toHaveBeenCalledWith({ pathname: '/accountLists/[accountListId]/test?searchTerm=Cool', query: { searchTerm: 'Cool', @@ -131,16 +132,10 @@ describe('ProfileMenu', () => { }); it('should route to path with account list', async () => { - const { getByTestId, getByText, queryByTestId } = render( - - - - - - - , + const { getByTestId, findByText, queryByTestId } = render( + , ); - await waitFor(() => expect(getByText('John Smith')).toBeInTheDocument()); + expect(await findByText('John Smith')).toBeInTheDocument(); expect(queryByTestId('accountListName')).not.toBeInTheDocument(); userEvent.click(getByTestId('profileMenuButton')); userEvent.click(getByTestId('accountListSelector')); @@ -155,31 +150,19 @@ describe('ProfileMenu', () => { }); it('should display account name if user has two or more account lists', async () => { - const { getByTestId, getByText } = render( - - - - - - - , + const { findByText, getByTestId, getByText } = render( + , ); - await waitFor(() => expect(getByText('John Smith')).toBeInTheDocument()); + expect(await findByText('John Smith')).toBeInTheDocument(); expect(getByTestId('accountListName')).toBeInTheDocument(); expect(getByText('Staff Account')).toBeInTheDocument(); }); it('Ensure Sign Out is called with callback', async () => { - const { getByTestId, getByText, queryByTestId } = render( - - - - - - - , + const { findByText, getByTestId, getByText, queryByTestId } = render( + , ); - await waitFor(() => expect(getByText('John Smith')).toBeInTheDocument()); + expect(await findByText('John Smith')).toBeInTheDocument(); expect(queryByTestId('accountListName')).not.toBeInTheDocument(); userEvent.click(getByTestId('profileMenuButton')); await waitFor(() => @@ -189,6 +172,17 @@ describe('ProfileMenu', () => { userEvent.click(getByText(/sign out/i)); expect(signOut).toHaveBeenCalledWith({ callbackUrl: 'signOut' }); }); + + it('hides links during the setup tour', async () => { + const { findByText, getByRole, getByTestId, queryByText } = render( + , + ); + + expect(await findByText('John Smith')).toBeInTheDocument(); + userEvent.click(getByTestId('profileMenuButton')); + expect(queryByText('Preferences')).not.toBeInTheDocument(); + expect(getByRole('button', { name: 'Sign Out' })).toBeInTheDocument(); + }); }); describe('ProfileMenu while Impersonating', () => { @@ -220,15 +214,7 @@ describe('ProfileMenu while Impersonating', () => { { status: 200 }, ]); - const { getByTestId, getByText, queryByTestId } = render( - - - - - - - , - ); + const { getByTestId, getByText, queryByTestId } = render(); await waitFor(() => expect(getByText('Impersonating John Smith')).toBeInTheDocument(), ); diff --git a/src/components/Layouts/Primary/TopBar/Items/ProfileMenu/ProfileMenu.tsx b/src/components/Layouts/Primary/TopBar/Items/ProfileMenu/ProfileMenu.tsx index 478cf794a..df08cf12f 100644 --- a/src/components/Layouts/Primary/TopBar/Items/ProfileMenu/ProfileMenu.tsx +++ b/src/components/Layouts/Primary/TopBar/Items/ProfileMenu/ProfileMenu.tsx @@ -21,6 +21,7 @@ import { styled } from '@mui/material/styles'; import { signOut } from 'next-auth/react'; import { useSnackbar } from 'notistack'; import { useTranslation } from 'react-i18next'; +import { useSetupContext } from 'src/components/Setup/SetupProvider'; import { PrivacyPolicyLink, TermsOfUseLink, @@ -124,6 +125,7 @@ const ProfileMenu = (): ReactElement => { const { contactId: _, ...queryWithoutContactId } = router.query; const accountListId = useAccountListId(); const { data } = useGetTopBarQuery(); + const { onSetupTour } = useSetupContext(); const [profileMenuAnchorEl, setProfileMenuAnchorEl] = useState(); const profileMenuOpen = Boolean(profileMenuAnchorEl); @@ -275,7 +277,7 @@ const ProfileMenu = (): ReactElement => { ))} - {accountListId && ( + {!onSetupTour && accountListId && (
({ }, })); -describe('TopBar', () => { - const useRouter = jest.spyOn(nextRouter, 'useRouter'); - const mocks = [getTopBarMultipleMock(), ...getNotificationsMocks()]; - beforeEach(() => { - ( - useRouter as jest.SpyInstance< - Pick - > - ).mockImplementation(() => ({ - query: { accountListId }, - isReady: true, - })); - }); +interface TestComponentProps { + onSetupTour?: boolean; +} +const TestComponent: React.FC = ({ onSetupTour }) => ( + + + + + + + + + + + +); + +describe('TopBar', () => { it('default', () => { - const { getByTestId } = render( - - - - - - - - - , - ); + const { getByTestId, getByText } = render(); expect(getByTestId('TopBar')).toBeInTheDocument(); + expect(getByText('Dashboard')).toBeInTheDocument(); + }); + + it('hides links during the setup tour', () => { + const { queryByText } = render(); + + expect(queryByText('Dashboard')).not.toBeInTheDocument(); }); }); diff --git a/src/components/Layouts/Primary/TopBar/TopBar.tsx b/src/components/Layouts/Primary/TopBar/TopBar.tsx index 40316d84f..f98f63b31 100644 --- a/src/components/Layouts/Primary/TopBar/TopBar.tsx +++ b/src/components/Layouts/Primary/TopBar/TopBar.tsx @@ -1,4 +1,3 @@ -import NextLink from 'next/link'; import React, { ReactElement } from 'react'; import MenuIcon from '@mui/icons-material/Menu'; import { @@ -11,6 +10,8 @@ import { useScrollTrigger, } from '@mui/material'; import { styled } from '@mui/material/styles'; +import { useSetupContext } from 'src/components/Setup/SetupProvider'; +import { LogoLink } from '../LogoLink/LogoLink'; import AddMenu from './Items/AddMenu/AddMenu'; import NavMenu from './Items/NavMenu/NavMenu'; import NotificationMenu from './Items/NotificationMenu/NotificationMenu'; @@ -32,6 +33,7 @@ const TopBar = ({ accountListId, onMobileNavOpen, }: TopBarProps): ReactElement => { + const { onSetupTour } = useSetupContext(); const trigger = useScrollTrigger({ disableHysteresis: true, threshold: 0, @@ -54,15 +56,10 @@ const TopBar = ({ )} - - logo - + - {accountListId && ( + {onSetupTour && } + {!onSetupTour && accountListId && ( <> diff --git a/src/components/Setup/SetupProvider.test.tsx b/src/components/Setup/SetupProvider.test.tsx index 18fd0d69e..8dd148e9e 100644 --- a/src/components/Setup/SetupProvider.test.tsx +++ b/src/components/Setup/SetupProvider.test.tsx @@ -14,11 +14,13 @@ interface TestComponentProps { } const ContextTestingComponent = () => { - const { settingUp } = useSetupContext(); + const { onSetupTour } = useSetupContext(); return (
- {typeof settingUp === 'undefined' ? 'undefined' : settingUp.toString()} + {typeof onSetupTour === 'undefined' + ? 'undefined' + : onSetupTour.toString()}
); }; @@ -100,7 +102,7 @@ describe('SetupProvider', () => { await waitFor(() => expect(push).not.toHaveBeenCalled()); }); - describe('settingUp context', () => { + describe('onSetupTour context', () => { it('is undefined while data is loading', () => { const { getByTestId } = render( , @@ -109,11 +111,12 @@ describe('SetupProvider', () => { expect(getByTestId('setting-up')).toHaveTextContent('undefined'); }); - it('is true when setup is set', async () => { + it('is true when setup is set on a tour page', async () => { const { getByTestId } = render( , ); @@ -122,9 +125,13 @@ describe('SetupProvider', () => { ); }); - it('is true when setup_position is set', async () => { + it('is true when setup_position is set on a tour page', async () => { const { getByTestId } = render( - , + , ); await waitFor(() => @@ -132,6 +139,16 @@ describe('SetupProvider', () => { ); }); + it('is false when not on a tour page', async () => { + const { getByTestId } = render( + , + ); + + await waitFor(() => + expect(getByTestId('setting-up')).toHaveTextContent('false'), + ); + }); + it('is false when setup_position is not set', async () => { const { getByTestId } = render( , diff --git a/src/components/Setup/SetupProvider.tsx b/src/components/Setup/SetupProvider.tsx index b84974af9..4ec1da508 100644 --- a/src/components/Setup/SetupProvider.tsx +++ b/src/components/Setup/SetupProvider.tsx @@ -1,5 +1,6 @@ import { useRouter } from 'next/router'; import React, { + PropsWithChildren, ReactNode, createContext, useContext, @@ -10,29 +11,37 @@ import { UserSetupStageEnum } from 'src/graphql/types.generated'; import { useSetupStageQuery } from './Setup.generated'; export interface SetupContext { - settingUp?: boolean; + /** + * `true` if the user is on a setup page and is in the process of completing + * the setup tour. `false` if the user isn't on a setup page or isn't setting + * up their account. `undefined` if the data needed to determine whether the + * user is on the setup tour hasn't loaded yet. + */ + onSetupTour?: boolean; } -const SetupContext = createContext(null); +const SetupContext = createContext({ onSetupTour: undefined }); -export const useSetupContext = (): SetupContext => { - const setupContext = useContext(SetupContext); - if (!setupContext) { - throw new Error( - 'SetupProvider not found! Make sure that you are calling useSetupContext inside a component wrapped by .', - ); - } +export const useSetupContext = (): SetupContext => useContext(SetupContext); - return setupContext; -}; +// The list of page pathnames that are part of the setup tour +const setupPages = new Set([ + '/setup/start', + '/setup/connect', + '/setup/account', + '/accountLists/[accountListId]/settings/preferences', + '/accountLists/[accountListId]/settings/notifications', + '/accountLists/[accountListId]/settings/integrations', + '/accountLists/[accountListId]/setup/finish', +]); -interface Props { +interface SetupProviderProps { children: ReactNode; } // This context component ensures that users have gone through the setup process // and provides the setup state to the rest of the application -export const SetupProvider: React.FC = ({ children }) => { +export const SetupProvider: React.FC = ({ children }) => { const { data } = useSetupStageQuery(); const { push, pathname } = useRouter(); @@ -59,7 +68,7 @@ export const SetupProvider: React.FC = ({ children }) => { } }, [data]); - const settingUp = useMemo(() => { + const onSetupTour = useMemo(() => { if (!data) { return undefined; } @@ -68,16 +77,29 @@ export const SetupProvider: React.FC = ({ children }) => { return false; } - return ( + const onSetupPage = setupPages.has(pathname); + const settingUp = data.userOptions.some( (option) => option.key === 'setup_position' && option.value !== '', - ) || data.user.setup !== null - ); - }, [data]); + ) || data.user.setup !== null; + return onSetupPage && settingUp; + }, [data, pathname]); return ( - + {children} ); }; + +// This provider is meant for use in tests. It lets tests easily override the +// onSetupTour without needing to mock useSetupProvider or the pathname and +// SetupStage GraphQL query. +export const TestSetupProvider: React.FC> = ({ + children, + onSetupTour, +}) => ( + + {children} + +); diff --git a/src/components/Shared/MultiPageLayout/MultiPageHeader.test.tsx b/src/components/Shared/MultiPageLayout/MultiPageHeader.test.tsx index 4dac70fa9..ebce11a75 100644 --- a/src/components/Shared/MultiPageLayout/MultiPageHeader.test.tsx +++ b/src/components/Shared/MultiPageLayout/MultiPageHeader.test.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { ThemeProvider } from '@mui/material/styles'; import { render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { TestSetupProvider } from 'src/components/Setup/SetupProvider'; import theme from 'src/theme'; import { HeaderTypeEnum, MultiPageHeader } from './MultiPageHeader'; @@ -9,19 +10,33 @@ const totalBalance = 'CA111'; const title = 'test title'; const onNavListToggle = jest.fn(); +interface TestComponentProps { + headerType?: HeaderTypeEnum; + noRightExtra?: boolean; + onSetupTour?: boolean; +} + +const TestComponent: React.FC = ({ + headerType = HeaderTypeEnum.Report, + noRightExtra = false, + onSetupTour, +}) => ( + + + + + +); + describe('MultiPageHeader', () => { it('default', async () => { - const { getByRole, getByText } = render( - - - , - ); + const { getByRole, getByText } = render(); expect(getByText(title)).toBeInTheDocument(); expect(getByText('CA111')).toBeInTheDocument(); @@ -32,49 +47,41 @@ describe('MultiPageHeader', () => { }); it('should not render rightExtra if undefined', async () => { - const { queryByText } = render( - - - , + const { queryByText } = render(); + + expect(queryByText('CA111')).not.toBeInTheDocument(); + }); + + it('should render the Reports menu', async () => { + const { getByTestId, getByText } = render( + , ); - expect(queryByText('CA111')).toBeNull(); + expect(getByText('Toggle Navigation Panel')).toBeInTheDocument(); + expect(getByTestId('ReportsFilterIcon')).toBeInTheDocument(); }); it('should render the Settings menu', async () => { const { getByTestId, getByText } = render( - - - , + , ); expect(getByText('Toggle Preferences Menu')).toBeInTheDocument(); expect(getByTestId('SettingsMenuIcon')).toBeInTheDocument(); }); + it('should not render the Settings menu during the setup tour', async () => { + const { queryByTestId, queryByText } = render( + , + ); + + expect(queryByText('Toggle Preferences Menu')).not.toBeInTheDocument(); + expect(queryByTestId('SettingsMenuIcon')).not.toBeInTheDocument(); + }); + it('should render the Tools menu', async () => { const { getByTestId, getByText } = render( - - - , + , ); expect(getByText('Toggle Tools Menu')).toBeInTheDocument(); diff --git a/src/components/Shared/MultiPageLayout/MultiPageHeader.tsx b/src/components/Shared/MultiPageLayout/MultiPageHeader.tsx index c8d0f571d..c677c308e 100644 --- a/src/components/Shared/MultiPageLayout/MultiPageHeader.tsx +++ b/src/components/Shared/MultiPageLayout/MultiPageHeader.tsx @@ -4,6 +4,7 @@ import MenuIcon from '@mui/icons-material/Menu'; import { Box, IconButton, Typography } from '@mui/material'; import { styled } from '@mui/material/styles'; import { useTranslation } from 'react-i18next'; +import { useSetupContext } from 'src/components/Setup/SetupProvider'; import theme from 'src/theme'; export enum HeaderTypeEnum { @@ -65,6 +66,7 @@ export const MultiPageHeader: FC = ({ headerType, }) => { const { t } = useTranslation(); + const { onSetupTour } = useSetupContext(); let titleAccess; if (headerType === HeaderTypeEnum.Report) { @@ -90,7 +92,7 @@ export const MultiPageHeader: FC = ({ data-testid="ReportsFilterIcon" /> )} - {headerType === HeaderTypeEnum.Settings && ( + {!onSetupTour && headerType === HeaderTypeEnum.Settings && (