Skip to content

Commit

Permalink
Added a show page for users
Browse files Browse the repository at this point in the history
  • Loading branch information
crismali committed Oct 1, 2024
1 parent 66ea8c0 commit 5242bfc
Show file tree
Hide file tree
Showing 9 changed files with 251 additions and 12 deletions.
10 changes: 10 additions & 0 deletions src/factories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { EmailTemplateIndex } from './network/useEmailTemplates'
import { StateAbbreviation } from './utils/statesAndTerritories'
import { DEPARTMENT_SEALS } from './utils/departmentSeals'
import { UsersIndex } from './network/useUsers'
import { UserShow } from './network/useUser'

export const randomObject = () => {
return { [faker.lorem.word()]: faker.lorem.words(3) }
Expand Down Expand Up @@ -146,6 +147,15 @@ export const buildUserIndex = (options?: Partial<UsersIndex>): UsersIndex => {
}
}

export const buildUserShow = (options?: Partial<UserShow>): UserShow => {
return {
id: uniqueId(),
email: faker.internet.email(),
role: 'member',
...options,
}
}

export const buildUseQueryResult = <T extends any>(
options?: Partial<UseQueryResult<T>>,
): UseQueryResult<T> => {
Expand Down
42 changes: 42 additions & 0 deletions src/network/__tests__/useUser.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react'
import { renderHook, waitFor } from '@testing-library/react'
import { UserShow, useUser } from '../useUser'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { AuthProvider } from 'src/utils/AuthContext'
import { asMock, buildUserShow, userIsSignedIn } from 'src/testHelpers'
import { AuthedFetch, useAuthedFetch } from '../useAuthedFetch'

jest.mock('../useAuthedFetch')

describe('useUser', () => {
let mockAuthedFetch: AuthedFetch

beforeEach(() => {
userIsSignedIn()
mockAuthedFetch = jest.fn()
asMock(useAuthedFetch).mockReturnValue(mockAuthedFetch)
})

it('queries for the email template with the given id', async () => {
const client = new QueryClient()
const user: UserShow = buildUserShow()
asMock(mockAuthedFetch).mockResolvedValue({ statusCode: 200, json: { user } })

const { result } = renderHook(() => useUser(user.id), {
wrapper: ({ children }) => {
return (
<QueryClientProvider client={client}>
<AuthProvider>{children}</AuthProvider>
</QueryClientProvider>
)
},
})

await waitFor(() => expect(result.current.isSuccess).toEqual(true))
expect(mockAuthedFetch).toHaveBeenCalledWith({
path: `/users/${user.id}`,
method: 'GET',
})
expect(result.current.data).toEqual(user)
})
})
26 changes: 26 additions & 0 deletions src/network/useUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useQuery } from '@tanstack/react-query'
import { useAuthedFetch } from './useAuthedFetch'

export interface UserShow {
id: string
email: string
role: 'admin' | 'member'
}

const QUERY_KEY = 'useUser'

export const useUser = (id: string) => {
const authedFetch = useAuthedFetch()

return useQuery({
queryKey: [QUERY_KEY, id],
queryFn: async () => {
const result = await authedFetch<{ user: UserShow }>({
path: `/users/${id}`,
method: 'GET',
})
const user = result.json!.user
return { ...user, role: user.role ?? 'member' }
},
})
}
14 changes: 8 additions & 6 deletions src/pages/__tests__/users.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { render } from '@testing-library/react'
import capitalize from 'lodash/capitalize'
import UsersPage from '../users'
import { SIDEBAR_NAVIGATION_TEST_ID as sidebarNavigationTestId } from 'src/ui/SidebarNavigation'
import { asMock, buildUserIndex, buildUseQueryResult } from 'src/testHelpers'
import { asMock, buildUserIndex, buildUseQueryResult, urlFor } from 'src/testHelpers'
import { useUsers } from 'src/network/useUsers'
import { UsersIndex } from 'src/network/useUsers'
import { faker } from '@faker-js/faker'
Expand Down Expand Up @@ -56,15 +56,17 @@ describe('Users page', () => {

const { queryByText } = renderUsersPage()

const firstEmail = queryByText(user1.email)
expect(firstEmail).not.toBeNull()
const firstLink: HTMLAnchorElement | null = queryByText(user1.email) as any
expect(firstLink).not.toBeNull()
expect(firstLink!.href).toEqual(urlFor(`/users/${user1.id}`))

const secondLink: HTMLAnchorElement | null = queryByText(user2.email) as any
expect(secondLink).not.toBeNull()
expect(secondLink!.href).toEqual(urlFor(`/users/${user2.id}`))

const firstRole = queryByText(capitalize(user1.role))
expect(firstRole).not.toBeNull()

const secondEmail = queryByText(user2.email)
expect(secondEmail).not.toBeNull()

const secondRole = queryByText(capitalize(user2.role))
expect(secondRole).not.toBeNull()
})
Expand Down
6 changes: 6 additions & 0 deletions src/pages/users.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@
padding: 1.5rem 0;
}

.user-item:last-of-type {
border-bottom: 0;
}

.user-email {
color: var(--black);
font-size: 1.25rem;
font-weight: bold;
text-decoration: none;
}

.user-role {
Expand Down
8 changes: 5 additions & 3 deletions src/pages/users.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { FC } from 'react'
import capitalize from 'lodash.capitalize'
import { HeadFC } from 'gatsby'
import { HeadFC, Link } from 'gatsby'
import {
Heading,
Layout,
Expand Down Expand Up @@ -29,13 +29,15 @@ const UsersPage: FC = () => {
<SkipNavContent />
<SpacedContainer>
<Heading element="h1">Users</Heading>
<Paragraph>All of users can be found here</Paragraph>
<Paragraph>All of the users can be found here</Paragraph>
{error && <Paragraph>{error.message}</Paragraph>}
{users && users.length > 0 && (
<List className="user-list">
{users.map((user) => (
<li key={user.id} className="user-item">
<span className="user-email">{user.email}</span>
<Link to={`/users/${user.id}`} className="user-email">
{user.email}
</Link>
<span className="user-role">{capitalize(user.role)}</span>
</li>
))}
Expand Down
50 changes: 50 additions & 0 deletions src/pages/users/[id].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { HeadFC, PageProps } from 'gatsby'
import capitalize from 'lodash.capitalize'
import React, { FC } from 'react'
import { useUser } from 'src/network/useUser'
import {
Heading,
Layout,
PageContent,
Paragraph,
Sidebar,
SkipNavContent,
SpacedContainer,
SidebarNavigation,
LoadingOverlay,
} from 'src/ui'
import { formatPageTitle } from 'src/utils/formatPageTitle'

export type Props = PageProps<null, null, null>

const UserShowPage: FC<Props> = ({ params }) => {
const query = useUser(params.id)
const { data: user, isLoading, error } = query

return (
<Layout element="div">
<Sidebar>
<SidebarNavigation />
</Sidebar>
<PageContent element="main">
<SkipNavContent />
<SpacedContainer>
<Heading element="h1">User</Heading>
{error && <Paragraph>{error.message}</Paragraph>}

{user && (
<div key={user.id} className="user-item">
<span className="user-email">{user.email}</span>
<span className="user-role">{capitalize(user.role)}</span>
</div>
)}
{isLoading && <LoadingOverlay description="Loading user" />}
</SpacedContainer>
</PageContent>
</Layout>
)
}

export default UserShowPage

export const Head: HeadFC = () => <title>{formatPageTitle('User Details')}</title>
95 changes: 95 additions & 0 deletions src/pages/users/__tests__/[id].test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React from 'react'
import { render } from '@testing-library/react'
import UserShowPage, { Props } from '../[id]'
import { asMock, buildUseQueryResult, buildUserShow } from 'src/testHelpers'
import { useUser } from 'src/network/useUser'
import { UserShow } from 'src/network/useUser'
import { faker } from '@faker-js/faker'
import { randomUUID } from 'crypto'
import { SIDEBAR_NAVIGATION_TEST_ID as sidebarNavigationTestId } from 'src/ui/SidebarNavigation'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import capitalize from 'lodash.capitalize'

jest.mock('src/network/useUser', () => {
return {
useUser: jest.fn(),
}
})

describe('User Show Page', () => {
const renderPage = (props?: Partial<Props>) => {
return render(
<QueryClientProvider client={new QueryClient()}>
<UserShowPage
pageContext={null}
uri=""
path=""
location={{} as any}
pageResources={{} as any}
params={{ id: faker.lorem.word() }}
children={undefined}
data={null}
serverData={{}}
{...props}
/>
</QueryClientProvider>,
)
}

it('is displayed in a layout', () => {
const query = buildUseQueryResult<UserShow>({ isLoading: true, data: undefined })
asMock(useUser).mockReturnValue(query)
const { baseElement } = renderPage()
expect(baseElement.querySelector('.layout')).not.toBeNull()
})

it('displays the sidebar navigation', () => {
const query = buildUseQueryResult<UserShow>({ isLoading: true, data: undefined })
asMock(useUser).mockReturnValue(query)
const { queryByTestId } = renderPage()
expect(queryByTestId(sidebarNavigationTestId)).not.toBeNull()
})

it('loads the correct user', () => {
const userId = randomUUID()
const query = buildUseQueryResult<UserShow>({ isLoading: true, data: undefined })
asMock(useUser).mockReturnValue(query)
renderPage({ params: { id: userId } })
expect(useUser).toHaveBeenCalledWith(userId)
})

describe('when loading', () => {
it('displays an loading spinner', () => {
const query = buildUseQueryResult<UserShow>({ isLoading: true, data: undefined })
asMock(useUser).mockReturnValue(query)
const { queryByText } = renderPage()
expect(queryByText('Loading user')).not.toBeNull()
})
})

describe('when successful', () => {
let user: UserShow

beforeEach(() => {
user = buildUserShow()
const query = buildUseQueryResult({ data: user })
asMock(useUser).mockReturnValue(query)
})

it('displays the user', () => {
const { queryByText } = renderPage()
expect(queryByText(user.email)).not.toBeNull()
expect(queryByText(capitalize(user.role))).not.toBeNull()
})
})

describe('when there is an error', () => {
it('displays an error', () => {
const error = new Error(faker.lorem.sentence())
const query = buildUseQueryResult<UserShow>({ error, isError: true })
asMock(useUser).mockReturnValue(query)
const { queryByText } = renderPage()
expect(queryByText(error.message)).not.toBeNull()
})
})
})
12 changes: 9 additions & 3 deletions src/ui/SidebarNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const SidebarNavigation: FC<Props> = () => {
<SpacedLink to="/library" text="Library" icon={<UswdsIcon icon="AccountBalance" />} />
<WhenSignedIn>
<SpacedLink to="/my-library" text="My Library" icon={<UswdsIcon icon="FolderOpen" />} />
<SpacedLink to="/users" text="Users" icon={<UswdsIcon icon="People" />} />
<SpacedLink to="/users" text="Users" icon={<UswdsIcon icon="People" />} partiallyActive />
</WhenSignedIn>
<SpacedLink
to="/tips-and-tricks"
Expand All @@ -42,17 +42,23 @@ export const SidebarNavigation: FC<Props> = () => {
interface SpacedLinkProps {
bottom?: boolean
icon: ReactElement
partiallyActive?: boolean
text: string
to: string
}

const SpacedLink: FC<SpacedLinkProps> = ({ bottom, icon, text, to }) => {
const SpacedLink: FC<SpacedLinkProps> = ({ bottom, icon, partiallyActive, text, to }) => {
const Comp = bottom ? SideBarListItemBottom : SideBarListItem

return (
<Comp>
<SpacedSidebarContainer>
<Link activeClassName="sidebar-active-link" className={classNames({ bottom })} to={to}>
<Link
activeClassName="sidebar-active-link"
className={classNames({ bottom })}
to={to}
partiallyActive={partiallyActive}
>
{!bottom && icon}
<span>{text}</span>
{bottom && icon}
Expand Down

0 comments on commit 5242bfc

Please sign in to comment.