Skip to content

Commit

Permalink
Merge pull request #171 from jbrunton/test-header
Browse files Browse the repository at this point in the history
test: header
  • Loading branch information
jbrunton authored Aug 31, 2024
2 parents 79cc12a + 69c7dda commit 6220d07
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 30 deletions.
11 changes: 8 additions & 3 deletions client/src/features/auth/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type LoadingState = {
signIn?: never
signOut?: never
token?: never
userName?: never
}

type SignedInState = {
Expand All @@ -14,6 +15,7 @@ type SignedInState = {
signIn?: never
signOut: () => void
token: string
userName?: string
}

type SignedOutState = {
Expand All @@ -22,6 +24,7 @@ type SignedOutState = {
signIn: () => void
signOut?: never
token?: never
userName?: never
}

export type AuthContextInterface = LoadingState | SignedInState | SignedOutState
Expand All @@ -37,29 +40,31 @@ const signedOutState = (signIn: () => void): AuthContextInterface => ({
signIn,
})

const signedInState = (token: string, signOut: () => void): AuthContextInterface => ({
const signedInState = (token: string, userName: string | undefined, signOut: () => void): AuthContextInterface => ({
isLoading: false,
isAuthenticated: true,
signOut,
token,
userName,
})

export const AuthContext = createContext<AuthContextInterface>(loadingState)

type GetAuthStateParams = {
isLoading: boolean
token?: string
userName?: string
signOut: () => void
signIn: () => void
}

export const getAuthState = ({ isLoading, token, signIn, signOut }: GetAuthStateParams) => {
export const getAuthState = ({ isLoading, token, userName, signIn, signOut }: GetAuthStateParams) => {
if (isLoading) {
return loadingState
}

if (token) {
return signedInState(token, signOut)
return signedInState(token, userName, signOut)
}

return signedOutState(signIn)
Expand Down
9 changes: 3 additions & 6 deletions client/src/features/auth/organisms/sign-in-button.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import React from 'react'
import { useAuth0 } from '@auth0/auth0-react'
import { Button, Spinner } from '@chakra-ui/react'
import { AiOutlineLogout } from 'react-icons/ai'
import { DefaultUserIcon } from '../../room/atoms/default-user-icon'
import { useAuth } from '..'

export const SignInButton = () => {
const { user, isAuthenticated, isLoading, loginWithRedirect, logout } = useAuth0()

const signIn = () => loginWithRedirect()
const signOut = () => logout({ returnTo: window.location.origin })
const { isAuthenticated, isLoading, signIn, signOut, userName } = useAuth()

if (isLoading) {
return (
Expand All @@ -28,7 +25,7 @@ export const SignInButton = () => {

return (
<Button leftIcon={<AiOutlineLogout />} variant='drawer' onClick={signOut}>
Sign Out {user?.name}
Sign Out {userName}
</Button>
)
}
5 changes: 3 additions & 2 deletions client/src/features/auth/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import debug from 'debug'
const log = debug('auth')

export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { isLoading: isAuth0Loading, loginWithRedirect, logout } = useAuth0()
const { isLoading: isAuth0Loading, loginWithRedirect, logout, user } = useAuth0()
const { accessToken, isLoading: isTokenLoading } = useAccessToken()
const [isLoading, setIsLoading] = useState(true)

Expand All @@ -28,7 +28,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
log('signOut called')
setIsLoading(true)
clearBearerToken()
logout()
logout({ returnTo: window.location.origin })
}

const signIn = () => {
Expand All @@ -40,6 +40,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
const state = getAuthState({
isLoading,
token: accessToken,
userName: user?.name,
signIn,
signOut,
})
Expand Down
15 changes: 5 additions & 10 deletions client/src/features/room/pages/room.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import { render } from '../../../test/fixtures'
describe('RoomPage', () => {
it('loads messages for the room', async () => {
render(<RoomPage />, {
path: '/room/:roomId',
initialEntry: '/room/room:100-can-manage',
routes: { path: '/room/:roomId', initialEntry: '/room/room:100-can-manage' },
})

await waitFor(() => {
Expand All @@ -17,8 +16,7 @@ describe('RoomPage', () => {

it('shows a message if the user lacks permissions to read the room', async () => {
render(<RoomPage />, {
path: '/room/:roomId',
initialEntry: '/room/room:200-can-join',
routes: { path: '/room/:roomId', initialEntry: '/room/room:200-can-join' },
})

await waitFor(() => {
Expand All @@ -28,8 +26,7 @@ describe('RoomPage', () => {

it('shows an alert if the user can join the room', async () => {
render(<RoomPage />, {
path: '/room/:roomId',
initialEntry: '/room/room:200-can-join',
routes: { path: '/room/:roomId', initialEntry: '/room/room:200-can-join' },
})

await waitFor(() => {
Expand All @@ -41,8 +38,7 @@ describe('RoomPage', () => {

it('shows an alert if the user can request to join', async () => {
render(<RoomPage />, {
path: '/room/:roomId',
initialEntry: '/room/room:300-can-request-approval',
routes: { path: '/room/:roomId', initialEntry: '/room/room:300-can-request-approval' },
})

await waitFor(() => {
Expand All @@ -54,8 +50,7 @@ describe('RoomPage', () => {

it('shows an alert if the user requires an invite to join', async () => {
render(<RoomPage />, {
path: '/room/:roomId',
initialEntry: '/room/room:400-requires-invite',
routes: { path: '/room/:roomId', initialEntry: '/room/room:400-requires-invite' },
})

await waitFor(() => {
Expand Down
6 changes: 6 additions & 0 deletions client/src/mocks/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ export const handlers = [
const response = loadResponse(`./messages/get/${roomId}.json`)
return res(ctx.json(response))
}),

rest.get('users/:userId', (req, res, ctx) => {
const userId = req.params['userId']
const response = loadResponse(`./users/get/${userId}.json`)
return res(ctx.json(response))
}),
]

const loadResponse = (relativePath: string): JSON => {
Expand Down
17 changes: 17 additions & 0 deletions client/src/mocks/users/me.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"user": {
"id": "user:google-oauth2|123",
"name": "Test User",
"picture": "https://ui-avatars.com/api/?name=Joe+Bloggs",
"email": "[email protected]"
},
"rooms": [
{
"id": "room:100",
"name": "Test Room",
"ownerId": "user:google-oauth2|123",
"contentPolicy": "private",
"joinPolicy": "invite"
}
]
}
1 change: 1 addition & 0 deletions client/src/mocks/users/system.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "id": "system", "name": "System", "email": "[email protected]" }
78 changes: 78 additions & 0 deletions client/src/shared/navigation/organisms/header.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react'
import { render } from '../../../test/fixtures'
import { waitFor, screen } from '@testing-library/react'
import { getAuthState } from '../../../features/auth/context'
import { Header } from './header'
import userEvent from '@testing-library/user-event'

describe('Header', () => {
const signIn = vitest.fn()
const signOut = vitest.fn()

const getMenuButton = () => screen.getByRole('button', { name: 'Open Menu' })
const getMenu = () => screen.queryByRole('dialog')
const getSignInButton = () => screen.getByRole('button', { name: 'Sign In' })
const getSignOutButton = () => screen.getByRole('button', { name: 'Sign Out Test User' })

const signedInState = () =>
getAuthState({ token: 'a1b2c3', isLoading: false, userName: 'Test User', signOut, signIn })
const signedOutState = () => getAuthState({ isLoading: false, signOut, signIn })

it('hides the navigation menu by default', () => {
render(<Header />, { auth: signedOutState() })
expect(getMenuButton()).toBeVisible()
expect(getMenu()).not.toBeInTheDocument()
})

it('shows the navigation menu when the menu button is clicked', async () => {
render(<Header />, { auth: signedOutState() })
await userEvent.click(getMenuButton())
expect(getMenu()).toBeVisible()
})

describe('when signed out', () => {
it('has a sign in button', async () => {
// arrange
render(<Header />, {
auth: signedOutState(),
})

// act
await userEvent.click(getMenuButton())

// assert
await waitFor(() => {
expect(getSignInButton()).toBeVisible()
})

await userEvent.click(getSignInButton())

expect(signIn).toHaveBeenCalledOnce()
})
})

describe('when signed in', () => {
it('has a sign out button', async () => {
// arrange
render(<Header />, {
auth: signedInState(),
})

// act
await waitFor(() => {
expect(getMenuButton()).toBeVisible()
})

// assert
userEvent.click(getMenuButton())

await waitFor(() => {
expect(getSignOutButton()).toBeVisible()
})

await userEvent.click(getSignOutButton())

expect(signOut).toHaveBeenCalledOnce()
})
})
})
30 changes: 21 additions & 9 deletions client/src/test/fixtures.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,39 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render } from '@testing-library/react'
import { ReactElement } from 'react'
import { RouterProvider, createMemoryRouter } from 'react-router-dom'
import { AuthContext, AuthContextInterface } from '../features/auth/context'

export type RenderOptions = {
path: string
initialEntry: string
routes?: {
path: string
initialEntry: string
}
auth?: AuthContextInterface
}

const customRender = (ui: ReactElement, opts: RenderOptions | undefined) => {
const customRender = (ui: ReactElement, opts: RenderOptions = {}) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
const router = opts
? createMemoryRouter([{ path: opts.path, element: ui }], {
initialEntries: [opts.initialEntry],
})
: undefined

const routes = opts.routes ?? { path: '/', initialEntry: '/' }

const router = createMemoryRouter([{ path: routes.path, element: ui }], {
initialEntries: [routes.initialEntry],
})

const auth: AuthContextInterface = opts.auth ?? { isAuthenticated: false, isLoading: false, signIn: vitest.fn() }

render(
<QueryClientProvider client={queryClient}>{router ? <RouterProvider router={router} /> : ui}</QueryClientProvider>,
<AuthContext.Provider value={auth}>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</AuthContext.Provider>,
)
}

Expand Down

0 comments on commit 6220d07

Please sign in to comment.