Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 203 additions & 0 deletions src/components/Header.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import { useHistory, useLocation } from 'react-router-dom'
import useOffSetTop from 'hooks/useOffSetTop'
import useResponsive from 'hooks/useResponsive'
import useSettings from 'hooks/useSettings'
import { useLocalStorage } from 'hooks/useLocalStorage'
import { useSession } from 'providers/Session'
import { useGetAplTeamsQuery } from 'redux/otomiApi'
import Header from './Header'

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: jest.fn(),
useLocation: jest.fn(),
}))

jest.mock('hooks/useOffSetTop')
jest.mock('hooks/useResponsive')
jest.mock('hooks/useSettings')
jest.mock('hooks/useLocalStorage')
jest.mock('providers/Session')
jest.mock('redux/otomiApi')

jest.mock('./AccountPopover', () => () => null)

jest.mock('./animate', () => ({
IconButtonAnimate: ({ children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button type='button' {...props}>
{children}
</button>
),
}))

jest.mock('./Iconify', () => () => null)

jest.mock('config', () => ({
HEADER: {
MOBILE_HEIGHT: 64,
DASHBOARD_DESKTOP_HEIGHT: 80,
DASHBOARD_DESKTOP_OFFSET_HEIGHT: 64,
},
NAVBAR: {
DASHBOARD_WIDTH: 280,
DASHBOARD_COLLAPSE_WIDTH: 88,
},
}))

const mockedUseHistory = useHistory as jest.MockedFunction<typeof useHistory>
const mockedUseLocation = useLocation as jest.MockedFunction<typeof useLocation>
const mockedUseOffSetTop = useOffSetTop as jest.MockedFunction<typeof useOffSetTop>
const mockedUseResponsive = useResponsive as jest.MockedFunction<typeof useResponsive>
const mockedUseSettings = useSettings as jest.MockedFunction<typeof useSettings>
const mockedUseLocalStorage = useLocalStorage as jest.MockedFunction<typeof useLocalStorage>
const mockedUseSession = useSession as jest.MockedFunction<typeof useSession>
const mockedUseGetAplTeamsQuery = useGetAplTeamsQuery as jest.MockedFunction<typeof useGetAplTeamsQuery>

const mockPush = jest.fn()
const mockSetOboTeamId = jest.fn()
const mockOnChangeView = jest.fn()

type RenderHeaderOptions = {
pathname?: string
oboTeamId?: string
themeView?: 'team' | 'platform'
isPlatformAdmin?: boolean
}

const renderHeader = ({
pathname = '/teams/alpha',
oboTeamId = 'alpha',
themeView = 'team',
isPlatformAdmin = false,
}: RenderHeaderOptions = {}) => {
mockedUseLocation.mockReturnValue({
pathname,
search: '',
hash: '',
state: undefined,
})
Comment thread
dennisvankekem marked this conversation as resolved.

mockedUseSession.mockReturnValue({
user: {
email: 'user@example.com',
teams: ['alpha', 'beta'],
isPlatformAdmin,
},
oboTeamId,
setOboTeamId: mockSetOboTeamId,
} as unknown as ReturnType<typeof useSession>)

mockedUseSettings.mockReturnValue({
themeView,
onChangeView: mockOnChangeView,
} as unknown as ReturnType<typeof useSettings>)

mockedUseLocalStorage.mockReturnValue([undefined, jest.fn()] as unknown as ReturnType<typeof useLocalStorage>)

mockedUseGetAplTeamsQuery.mockReturnValue({
data: undefined,
} as unknown as ReturnType<typeof useGetAplTeamsQuery>)

return render(<Header onOpenSidebar={jest.fn()} />)
}

const selectTeam = async (currentTeamName: string, nextTeamName: string) => {
const teamSelect = screen.getByRole('button', {
name: currentTeamName,
})

fireEvent.mouseDown(teamSelect)

const option = await screen.findByRole('option', {
name: nextTeamName,
})

fireEvent.click(option)
}

describe('Header team switching', () => {
beforeEach(() => {
jest.clearAllMocks()

mockedUseHistory.mockReturnValue({
push: mockPush,
} as unknown as ReturnType<typeof useHistory>)

mockedUseOffSetTop.mockReturnValue(false)
mockedUseResponsive.mockReturnValue(true)
})

it('switches from one team dashboard to another team dashboard', async () => {
renderHeader({
pathname: '/teams/alpha',
})

await selectTeam('alpha', 'beta')

expect(mockSetOboTeamId).toHaveBeenCalledWith('beta')
expect(mockPush).toHaveBeenCalledWith('/teams/beta')
})

it('preserves the section when switching from a section overview', async () => {
renderHeader({
pathname: '/teams/alpha/services',
})

await selectTeam('alpha', 'beta')

expect(mockSetOboTeamId).toHaveBeenCalledWith('beta')
expect(mockPush).toHaveBeenCalledWith('/teams/beta/services')
})

it('redirects a resource detail page to the section overview', async () => {
renderHeader({
pathname: '/teams/alpha/services/myservice',
})

await selectTeam('alpha', 'beta')

expect(mockSetOboTeamId).toHaveBeenCalledWith('beta')
expect(mockPush).toHaveBeenCalledWith('/teams/beta/services')
})

it('redirects deeply nested resource pages to the section overview', async () => {
renderHeader({
pathname: '/teams/alpha/services/myservice/edit',
})

await selectTeam('alpha', 'beta')

expect(mockSetOboTeamId).toHaveBeenCalledWith('beta')
expect(mockPush).toHaveBeenCalledWith('/teams/beta/services')
})

it('redirects non-team routes to the selected team dashboard', async () => {
renderHeader({
pathname: '/some-other-page',
})

await selectTeam('alpha', 'beta')

expect(mockSetOboTeamId).toHaveBeenCalledWith('beta')
expect(mockPush).toHaveBeenCalledWith('/teams/beta')
})

it('does nothing when the currently active team remains selected', () => {
renderHeader({
pathname: '/teams/alpha/services',
})

const hiddenInput = document.querySelector('[data-cy="select-oboteam"] input')

fireEvent.change(hiddenInput, {
target: {
value: 'alpha',
},
})
Comment thread
dennisvankekem marked this conversation as resolved.

expect(mockSetOboTeamId).not.toHaveBeenCalled()
expect(mockPush).not.toHaveBeenCalled()
})
})
30 changes: 27 additions & 3 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { skipToken } from '@reduxjs/toolkit/query/react'
import { AppBar, Box, MenuItem, Select, Stack, Toolbar, Typography, styled } from '@mui/material'
import { SelectChangeEvent } from '@mui/material/Select'
import { HEADER, NAVBAR } from 'config'
import useOffSetTop from 'hooks/useOffSetTop'
import useResponsive from 'hooks/useResponsive'
Expand Down Expand Up @@ -76,11 +77,13 @@ export default function Header({ onOpenSidebar, isCollapse = false, verticalLayo
const isDesktop = useResponsive('up', 'lg')
const history = useHistory()
const { pathname } = useLocation()

const {
user: { email, teams: userTeams, isPlatformAdmin },
oboTeamId: sessionOboTeamId,
setOboTeamId,
} = useSession()

const [localOboTeamId] = useLocalStorage<string>('oboTeamId', undefined)
const oboTeamId = sessionOboTeamId || localOboTeamId || undefined

Expand Down Expand Up @@ -126,17 +129,32 @@ export default function Header({ onOpenSidebar, isCollapse = false, verticalLayo

const getNextPathname = (nextTeamId: string): string => {
if (redirectToDashboard(nextTeamId)) return '/'

if (pathname === '/apps/admin' && themeView === 'platform') return pathname
return pathname.replace(oboTeamId, nextTeamId)

const segments = pathname.split('/').filter(Boolean)

// Not on a team route
if (segments[0] !== 'teams') return `/teams/${nextTeamId}`

// /teams/:teamId
if (segments.length === 2) return `/teams/${nextTeamId}`

Comment thread
dennisvankekem marked this conversation as resolved.
// /teams/:teamId/:section
// /teams/:teamId/:section/:anything
// Always return to the section overview.
return `/teams/${nextTeamId}/${segments[2]}`
}

const handleChangeTeam = (event) => {
const handleChangeTeam = (event: SelectChangeEvent<unknown>) => {
const nextTeamId = event.target.value as string

if (nextTeamId === oboTeamId) return

const nextPathname = getNextPathname(nextTeamId)

setOboTeamId(nextTeamId)
history.push(nextPathname)
event.preventDefault()
}

if (!teams.length && oboTeamId) teams = [{ value: oboTeamId, label: oboTeamId }]
Expand All @@ -159,11 +177,14 @@ export default function Header({ onOpenSidebar, isCollapse = false, verticalLayo
<Iconify icon='eva:menu-2-fill' />
</IconButtonAnimate>
)}

<Box sx={{ flexGrow: 1 }} />

<Stack direction='row' alignItems='center' spacing={{ xs: 0.5, sm: 1.5 }}>
{themeView === 'team' && (
<>
<Typography variant='body1'>Team:</Typography>

<StyledSelect
size='small'
color='secondary'
Expand All @@ -179,9 +200,11 @@ export default function Header({ onOpenSidebar, isCollapse = false, verticalLayo
</StyledSelect>
</>
)}

{isPlatformAdmin && (
<>
<Typography>View:</Typography>

<StyledSelect
size='small'
color='secondary'
Expand All @@ -194,6 +217,7 @@ export default function Header({ onOpenSidebar, isCollapse = false, verticalLayo
</StyledSelect>
</>
)}

<AccountPopover email={email} />
</Stack>
</Toolbar>
Expand Down