import React from 'react'; import { cleanup, fireEvent, render, RenderResult, waitFor, waitForElementToBeRemoved, } from '@testing-library/react'; import startCase from 'lodash/startCase'; import { constructProjectURL, dataFixtures, hostDefaults, urlFixtures, } from '../data'; import { defaultMongoNavContext, MongoNavContextProps, MongoNavProvider, } from '../MongoNavContext'; import { ActiveNavElement, CurrentProjectInterface, Mode } from '../types'; import ProjectNav from './ProjectNav'; import { ProjectNavProps } from './ProjectNav.types'; const productToTestId: Record = { atlas: 'project-nav-data-services', cloudManager: 'project-nav-cloud-manager', charts: 'project-nav-charts', realm: 'project-nav-app-services', }; // data const { currentOrganization, currentProject, projects } = dataFixtures; const { projectNav: { alerts, activityFeed, invite }, } = urlFixtures; // this avoids having to explicitly type orgNav with nullable fields // and then extend it to allow string indexes const linkNamesToUrls: Record = { alerts, activityFeed, invite, }; const linkNamesToTestIds: Record = { alerts: 'project-nav-alerts', activityFeed: 'project-nav-activity-feed', invite: 'project-nav-invite', }; describe('packages/mongo-nav/src/project-nav', () => { let onProjectChange: jest.Mock; let fetchMock: jest.Mock; const originalFetch: typeof window.fetch = window.fetch; let fetchData: Array; beforeEach(() => { onProjectChange = jest.fn(); fetchMock = jest.fn(); fetchData = []; fetchMock.mockResolvedValue({ ok: true, json: () => Promise.resolve(fetchData), }); window.fetch = fetchMock; }); afterEach(() => { window.fetch = originalFetch; jest.restoreAllMocks(); cleanup(); }); function renderComponent( props?: Partial, withContext?: Partial, ): RenderResult { const context: MongoNavContextProps = { ...defaultMongoNavContext, currentOrg: currentOrganization, currentProject: currentProject, activeNav: ActiveNavElement.OrgNavOrgSettings, activeProduct: 'cloud', ...withContext, }; return render( , ); } describe('when rendered with default props', () => { let queryByTestId: RenderResult['queryByTestId']; let getByTestId: RenderResult['getByTestId']; beforeEach(async () => { fetchData = []; ({ queryByTestId, getByTestId } = renderComponent( {}, { currentProject }, )); }); Object.keys(productToTestId).forEach(product => { const isVisible = product !== 'cloudManager'; it(`${ isVisible ? 'displays' : 'does not display' } the ${product} in the nav`, () => { const foundProduct = queryByTestId(productToTestId[product]); if (isVisible) { expect(foundProduct).toBeVisible(); } else { expect(foundProduct).toBeNull(); } }); }); Object.keys(linkNamesToUrls).forEach(linkName => { it(`displays the ${startCase(linkName)} iconButton`, () => { const navLink = queryByTestId(linkNamesToTestIds[linkName]); expect(navLink).toBeVisible(); expect((navLink as HTMLAnchorElement).href).toEqual( linkNamesToUrls[linkName], ); }); }); it(`does not display the project status badge in the nav`, () => { const statusBadge = queryByTestId('project-nav-project-status-badge'); expect(statusBadge).not.toBeInTheDocument(); }); it('atlas tab shows the correct link', () => { expect( getByTestId(productToTestId['atlas']) .closest('a') ?.getAttribute('href'), ).toEqual('https://cloud.mongodb.com/v2/fakeProjectId1#'); }); it('does not show alerts badge', () => { expect(queryByTestId('project-nav-alerts-badge')).toBeNull(); }); it('has a `product` data attribute on the tab', () => { // This is to ensure there's no layout shift when hovering tabs expect(getByTestId(productToTestId['atlas'])).toHaveAttribute( 'data-product', 'Data Services', ); }); }); describe('when the current organization uses Cloud Manager', () => { const cloudManagerProject = projects[1]; let queryByTestId: RenderResult['queryByTestId']; beforeEach(() => { ({ queryByTestId } = renderComponent( {}, { currentProject: cloudManagerProject as CurrentProjectInterface }, )); }); Object.keys(productToTestId).forEach(product => { const isVisible = product === 'cloudManager'; it(`${ isVisible ? 'displays' : 'does not display' } the ${product} in the nav`, () => { const foundProduct = queryByTestId(productToTestId[product]); if (isVisible) { expect(foundProduct).toBeVisible(); } else { expect(foundProduct).toBeNull(); } }); }); Object.keys(linkNamesToUrls).forEach(linkName => { it(`displays the ${startCase(linkName)} iconButton`, () => { const navLink = queryByTestId(linkNamesToTestIds[linkName]); expect(navLink).toBeVisible(); expect((navLink as HTMLAnchorElement).href).toEqual( linkNamesToUrls[linkName], ); }); }); }); describe('when the environment is "government"', () => { test('Atlas is the only product tab displayed', () => { const { getByTestId, queryByTestId } = renderComponent({ environment: 'government', }); expect(getByTestId(productToTestId['atlas'])).toBeVisible(); expect(queryByTestId('project-nav-charts')).toBeNull(); expect(queryByTestId('project-nav-realm')).toBeNull(); }); }); describe('when useCNRegionsOnly is "true"', () => { let getByTestId: RenderResult['getByTestId']; let queryByTestId: RenderResult['queryByTestId']; beforeEach(async () => { const useCNRegionsOnlyProject = { ...currentProject, useCNRegionsOnly: true, }; fetchData = [{}, {}]; ({ getByTestId, queryByTestId } = renderComponent( {}, { currentProject: useCNRegionsOnlyProject as CurrentProjectInterface }, )); }); it('atlas tab shows the correct link', () => { expect(getByTestId(productToTestId['atlas'])).toBeVisible(); expect( getByTestId(productToTestId['atlas']) .closest('a') ?.getAttribute('href'), ).toEqual('https://cloud.mongodb.com/v2/fakeProjectId1#'); }); it('App Services tab shows no link', () => { expect(getByTestId(productToTestId['realm'])).toBeVisible(); expect( getByTestId(productToTestId['realm']).getAttribute('href'), ).toBeNull(); }); it('hovering over App Services tab shows a tooltip', async () => { fireEvent.mouseEnter(getByTestId(productToTestId['realm'])); await waitFor(() => getByTestId('project-nav-tooltip-app-services'), { timeout: 500, }); expect( getByTestId('project-nav-tooltip-app-services'), ).toBeInTheDocument(); fireEvent.mouseLeave(getByTestId(productToTestId['realm'])); await waitForElementToBeRemoved( getByTestId('project-nav-tooltip-app-services'), { timeout: 500 }, ); expect( queryByTestId('project-nav-tooltip-app-services'), ).not.toBeInTheDocument(); }); it('charts tab shows no link', () => { expect(getByTestId(productToTestId['charts'])).toBeVisible(); expect( getByTestId(productToTestId['charts']).getAttribute('href'), ).toBeNull(); }); it('hovering over charts tab shows a tooltip', async () => { fireEvent.mouseEnter(getByTestId(productToTestId['charts'])); await waitFor(() => getByTestId('project-nav-tooltip-charts'), { timeout: 500, }); expect(getByTestId('project-nav-tooltip-charts')).toBeInTheDocument(); fireEvent.mouseLeave(getByTestId(productToTestId['charts'])); await waitForElementToBeRemoved( getByTestId('project-nav-tooltip-charts'), { timeout: 500 }, ); expect( queryByTestId('project-nav-tooltip-charts'), ).not.toBeInTheDocument(); }); }); describe('alerts polling behavior', () => { describe('when it should poll', () => { let getByTestId: RenderResult['getByTestId']; beforeEach(() => { fetchData = [{}, {}]; ({ getByTestId } = renderComponent({}, { currentProject })); }); it('calls fetch with correct arguments', () => { expect(fetchMock).toHaveBeenCalledWith( 'https://cloud.mongodb.com/user/shared/alerts/project/fakeProjectId1', { credentials: 'include', method: 'GET', mode: 'cors' }, ); }); it('displays expected alerts badge', () => { expect(getByTestId('project-nav-alerts-badge').innerHTML).toContain( '2', ); }); }); describe('when in dev mode', () => { let getByTestId: RenderResult['getByTestId']; beforeEach(() => { fetchData = [{}, {}]; ({ getByTestId } = renderComponent( { mode: Mode.Dev }, { currentProject }, )); }); it('does not call fetch', () => { expect(fetchMock).not.toHaveBeenCalled(); }); it('displays value from currentProject', () => { expect(getByTestId('project-nav-alerts-badge').innerHTML).toContain( '1', ); }); }); }); describe('when rendered without a current project', () => { let getByTestId: RenderResult['getByTestId']; beforeEach( () => ({ getByTestId } = renderComponent({}, { currentProject: undefined })), ); test('atlas tab shows the correct link', () => { expect( getByTestId(productToTestId['atlas']) .closest('a') ?.getAttribute('href'), ).toEqual('https://cloud.mongodb.com'); }); }); describe('when admin is set to true', () => { it(`displays the project status badge in the nav`, () => { const { queryByTestId } = renderComponent( { admin: true }, { currentProject }, ); const statusBadge = queryByTestId('project-nav-project-status-badge'); expect(statusBadge).toBeInTheDocument(); expect(statusBadge!.textContent).toEqual('ACTIVE'); }); it('displays the provided displayName in the project status badge', () => { const { queryByTestId } = renderComponent( { admin: true, }, { currentProject: { ...currentProject, status: 'DEAD', statusDisplayName: 'INACTIVE', } as CurrentProjectInterface, }, ); const statusBadge = queryByTestId('project-nav-project-status-badge'); expect(statusBadge).toBeInTheDocument(); expect(statusBadge!.textContent).toEqual('INACTIVE'); }); it('displays the project status badge when no displayName is provided', () => { const { queryByTestId } = renderComponent( { admin: true, }, { currentProject: { ...currentProject, statusDisplayName: undefined, } as CurrentProjectInterface, }, ); const statusBadge = queryByTestId('project-nav-project-status-badge'); expect(statusBadge).toBeInTheDocument(); expect(statusBadge!.textContent).toEqual(currentProject!.status); }); }); describe('accessibility props are properly set on menu', () => { let getByTestId: RenderResult['getByTestId']; beforeEach(() => ({ getByTestId } = renderComponent())); test('"aria-expanded" is set to false when menu is closed', () => { expect( getByTestId('project-nav-project-menu').getAttribute('aria-expanded'), ).toBe('false'); }); test('"aria-expanded" is set to true when menu is open', () => { const menu = getByTestId('project-nav-project-menu'); fireEvent.click(menu); waitFor(() => { expect(menu.getAttribute('aria-expanded')).toBe('true'); }); }); }); });