diff --git a/src/components/Header/Header.test.tsx b/src/components/Header/Header.test.tsx index f377c22..50f2860 100644 --- a/src/components/Header/Header.test.tsx +++ b/src/components/Header/Header.test.tsx @@ -1,125 +1,138 @@ import React from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; -import { shallow } from 'enzyme'; +import { render, fireEvent, waitForElementToBeRemoved, waitForElement } from '@testing-library/react'; import Header from './Header'; -describe('
component with logged in state', () => { - let wrapper; - let routerWrapper; - let instance; - let props; +const headerProps = { + username: 'verddacio-user', + scope: 'test scope', + withoutSearch: true, + handleToggleLoginModal: jest.fn(), + handleLogout: jest.fn(), +}; - beforeEach(() => { - props = { - username: 'test user', - handleLogout: jest.fn(), - logo: '', - onToggleLoginModal: jest.fn(), - scope: 'test scope', - withoutSearch: true, - }; - routerWrapper = shallow( +/* eslint-disable react/jsx-no-bind*/ +describe('
component with logged in state', () => { + test('should load the component in logged out state', () => { + const { container, queryByTestId, getByText } = render( -
+
); - wrapper = routerWrapper.find(Header).dive(); - instance = wrapper.instance(); + + expect(container.firstChild).toMatchSnapshot(); + expect(queryByTestId('header--menu-acountcircle')).toBeNull(); + expect(getByText('Login')).toBeTruthy(); }); test('should load the component in logged in state', () => { - const state = { - openInfoDialog: false, - packages: undefined, - registryUrl: 'http://localhost', - showMobileNavBar: false, - }; - - expect(wrapper.state()).toEqual(state); - expect(routerWrapper.html()).toMatchSnapshot(); - }); - - test('handleLoggedInMenu: set anchorEl to html element value in state', () => { - // creates a sample menu - const div = document.createElement('div'); - const text = document.createTextNode('sample menu'); - div.appendChild(text); - - const event = { - currentTarget: div, - }; - - instance.handleLoggedInMenu(event); - expect(wrapper.state('anchorEl')).toEqual(div); - }); -}); - -describe('
component with logged out state', () => { - let wrapper; - let routerWrapper; - let instance; - let props; - - beforeEach(() => { - props = { - handleLogout: jest.fn(), - onToggleLoginModal: jest.fn(), - scope: 'test scope', - logo: '', - withoutSearch: true, - }; - routerWrapper = shallow( + const { container, getByTestId, queryByText } = render(
); - wrapper = routerWrapper.find(Header).dive(); - instance = wrapper.instance(); + + expect(container.firstChild).toMatchSnapshot(); + expect(getByTestId('header--menu-acountcircle')).toBeTruthy(); + expect(queryByText('Login')).toBeNull(); }); - test('should load the component in logged out state', () => { - const state = { - openInfoDialog: false, - packages: undefined, - registryUrl: 'http://localhost', - showMobileNavBar: false, - }; - expect(wrapper.state()).toEqual(state); - expect(routerWrapper.html()).toMatchSnapshot(); + test('should open login dialog', async () => { + const { getByText } = render( + +
+ + ); + + const loginBtn = getByText('Login'); + fireEvent.click(loginBtn); + expect(headerProps.handleToggleLoginModal).toHaveBeenCalled(); }); - test('handleLoggedInMenuClose: set anchorEl value to null in state', () => { - instance.handleLoggedInMenuClose(); - expect(wrapper.state('anchorEl')).toBeNull(); + test('should logout the user', async () => { + const { getByText, getByTestId } = render( + +
+ + ); + + const headerMenuAccountCircle = getByTestId('header--menu-acountcircle'); + fireEvent.click(headerMenuAccountCircle); + + // wait for button Logout's appearance and return the element + const logoutBtn = await waitForElement(() => getByText('Logout')); + fireEvent.click(logoutBtn); + expect(headerProps.handleLogout).toHaveBeenCalled(); }); - test('handleOpenRegistryInfoDialog: set openInfoDialog to be truthy in state', () => { - instance.handleOpenRegistryInfoDialog(); - expect(wrapper.state('openInfoDialog')).toBeTruthy(); + test("The question icon should open a new tab of verdaccio's website - installation doc", async () => { + const { getByTestId } = render( + +
+ + ); + + const documentationBtn = getByTestId('header--tooltip-documentation'); + expect(documentationBtn.getAttribute('href')).toBe('https://verdaccio.org/docs/en/installation'); }); - test('handleCloseRegistryInfoDialog: set openInfoDialog to be falsy in state', () => { - instance.handleCloseRegistryInfoDialog(); - expect(wrapper.state('openInfoDialog')).toBeFalsy(); + test('should open the registrationInfo modal when clicking on the info icon', async () => { + const { getByTestId } = render( + +
+ + ); + + const infoBtn = getByTestId('header--tooltip-info'); + fireEvent.click(infoBtn); + + // wait for registrationInfo modal appearance and return the element + const registrationInfoModal = await waitForElement(() => getByTestId('registryInfo--dialog')); + expect(registrationInfoModal).toBeTruthy(); }); - test('handleToggleLogin: close/open popover menu', () => { - instance.handleToggleLogin(); - expect(wrapper.state('anchorEl')).toBeNull(); - expect(props.onToggleLoginModal).toHaveBeenCalled(); + test('should close the registrationInfo modal when clicking on the button close', async () => { + const { getByTestId, getByText, queryByTestId } = render( + +
+ + ); + + const infoBtn = getByTestId('header--tooltip-info'); + fireEvent.click(infoBtn); + + // wait for Close's button of registrationInfo modal appearance and return the element + const closeBtn = await waitForElement(() => getByText('CLOSE')); + fireEvent.click(closeBtn); + + const hasRegistrationInfoModalBeenRemoved = await waitForElementToBeRemoved(() => queryByTestId('registryInfo--dialog')); + expect(hasRegistrationInfoModalBeenRemoved).toBeTruthy(); }); + test.todo('autocompletion should display suggestions according to the type value'); }); diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 888854f..737a08c 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -1,35 +1,13 @@ -import React, { SyntheticEvent, Component, Fragment, ReactElement } from 'react'; -import { Link } from 'react-router-dom'; -import { css } from 'emotion'; -import MenuItem from '@material-ui/core/MenuItem'; -import Menu from '@material-ui/core/Menu'; -import Info from '@material-ui/icons/Info'; -import Help from '@material-ui/icons/Help'; -import AccountCircle from '@material-ui/icons/AccountCircle'; -import { default as IconSearch } from '@material-ui/icons/Search'; +import React, { useState } from 'react'; +import Search from '../Search'; import { getRegistryURL } from '../../utils/url'; -import Logo from '../Logo'; -import RegistryInfoDialog from '../RegistryInfoDialog/RegistryInfoDialog'; -import Label from '../Label/Label'; -import Search from '../Search/Search'; -import RegistryInfoContent from '../RegistryInfoContent/RegistryInfoContent'; -import IconButton from '../../muiComponents/IconButton'; -import Tooltip from '../../muiComponents/Tooltip'; import Button from '../../muiComponents/Button'; -import { - Greetings, - NavBar, - InnerNavBar, - MobileNavBar, - InnerMobileNavBar, - LeftSide, - RightSide, - IconSearchButton, - SearchWrapper, - StyledExternalLink, -} from './styles'; +import { NavBar, InnerNavBar, MobileNavBar, InnerMobileNavBar } from './styles'; +import HeaderLeft from './HeaderLeft'; +import HeaderRight from './HeaderRight'; +import HeaderInfoDialog from './HeaderInfoDialog'; interface Props { logo?: string; @@ -40,248 +18,38 @@ interface Props { withoutSearch?: boolean; } -interface State { - anchorEl?: null | Element | ((element: Element) => Element); - openInfoDialog: boolean; - registryUrl: string; - showMobileNavBar: boolean; -} +/* eslint-disable react/jsx-max-depth */ +/* eslint-disable react/jsx-no-bind*/ +const Header: React.FC = ({ logo, withoutSearch, username, onLogout, onToggleLoginModal, scope }) => { + const [isInfoDialogOpen, setOpenInfoDialog] = useState(); + const [showMobileNavBar, setShowMobileNavBar] = useState(); -type ToolTipType = 'search' | 'help' | 'info'; - -class Header extends Component { - constructor(props: Props) { - super(props); - this.state = { - openInfoDialog: false, - registryUrl: getRegistryURL(), - showMobileNavBar: false, - }; - } - - public render(): ReactElement { - const { showMobileNavBar } = this.state; - const { withoutSearch = false } = this.props; - return ( -
- - - {this.renderLeftSide()} - {this.renderRightSide()} - - {this.renderInfoDialog()} - - {showMobileNavBar && !withoutSearch && ( - - - - - - - )} -
- ); - } - - /** - * opens popover menu for logged in user. - */ - public handleLoggedInMenu = (event: SyntheticEvent) => { - this.setState({ - anchorEl: event.currentTarget, - }); - }; - - /** - * closes popover menu for logged in user - */ - public handleLoggedInMenuClose = () => { - this.setState({ - anchorEl: null, - }); - }; - - /** - * opens registry information dialog. - */ - public handleOpenRegistryInfoDialog = () => { - this.setState({ - openInfoDialog: true, - }); - }; - - /** - * closes registry information dialog. - */ - public handleCloseRegistryInfoDialog = () => { - this.setState({ - openInfoDialog: false, - }); - }; - - /** - * close/open popover menu for logged in users. - */ - public handleToggleLogin = () => { - const { onToggleLoginModal } = this.props; - this.setState( - { - anchorEl: null, - }, - onToggleLoginModal - ); - }; - - public handleToggleMNav = () => { - const { showMobileNavBar } = this.state; - this.setState({ - showMobileNavBar: !showMobileNavBar, - }); - }; - - public handleDismissMNav = () => { - this.setState({ - showMobileNavBar: false, - }); - }; - - public renderLeftSide = () => { - const { withoutSearch = false } = this.props; - return ( - - - {this.renderLogo()} - - {!withoutSearch && ( - + return ( + <> + + + + setOpenInfoDialog(true)} + onToggleLogin={onToggleLoginModal} + onToggleMobileNav={() => setShowMobileNavBar(!showMobileNavBar)} + username={username} + withoutSearch={withoutSearch} + /> + + setOpenInfoDialog(false)} registryUrl={getRegistryURL()} scope={scope} /> + + {showMobileNavBar && !withoutSearch && ( + + - - )} - - ); - }; - - public renderLogo = () => { - const { logo } = this.props; - - if (logo) { - return logo; - } else { - return ; - } - }; - - public renderToolTipIcon = (title: string, type: ToolTipType) => { - let content; - switch (type) { - case 'help': - content = ( - - - - - - ); - break; - case 'info': - content = ( - - - - ); - break; - case 'search': - content = ( - - - - ); - break; - } - return ( - - {content} - - ); - }; - - public renderRightSide = () => { - const { username = '', withoutSearch = false } = this.props; - return ( - - {!withoutSearch && this.renderToolTipIcon('Search packages', 'search')} - {this.renderToolTipIcon('Documentation', 'help')} - {this.renderToolTipIcon('Registry Information', 'info')} - {username ? ( - this.renderMenu() - ) : ( - - )} - - ); - }; - - private renderGreetings = () => { - const { username = '' } = this.props; - return ( - - {'Hi,'} - - ); - }; - - /** - * render popover menu - */ - private renderMenu = () => { - const { onLogout } = this.props; - const { anchorEl } = this.state; - const open = Boolean(anchorEl); - return ( - <> - - - - - {this.renderGreetings()} - - {'Logout'} - - - - ); - }; - - private renderInfoDialog = () => { - const { scope } = this.props; - const { openInfoDialog, registryUrl } = this.state; - return ( - - - - ); - }; -} + + + + )} + + ); +}; export default Header; diff --git a/src/components/Header/HeaderGreetings.tsx b/src/components/Header/HeaderGreetings.tsx new file mode 100644 index 0000000..4a76ae8 --- /dev/null +++ b/src/components/Header/HeaderGreetings.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import Label from '../Label'; + +import { Greetings } from './styles'; + +interface Props { + username: string; +} + +const HeaderGreetings: React.FC = ({ username }) => ( + <> + {'Hi,'} +