Skip to content

Commit

Permalink
Added Users Index page
Browse files Browse the repository at this point in the history
  • Loading branch information
littlelazer committed Sep 30, 2024
1 parent 1c23184 commit 2a5a752
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 1 deletion.
10 changes: 10 additions & 0 deletions src/factories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import sample from 'lodash.sample'
import { EmailTemplateIndex } from './network/useEmailTemplates'
import { StateAbbreviation } from './utils/statesAndTerritories'
import { DEPARTMENT_SEALS } from './utils/departmentSeals'
import { UsersIndex } from './network/useUsers'

export const randomObject = () => {
return { [faker.lorem.word()]: faker.lorem.words(3) }
Expand Down Expand Up @@ -136,6 +137,15 @@ export const buildEmailTemplateIndex = (
}
}

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

export const buildUseQueryResult = <T extends any>(
options?: Partial<UseQueryResult<T>>,
): UseQueryResult<T> => {
Expand Down
27 changes: 27 additions & 0 deletions src/network/useUsers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useQuery } from '@tanstack/react-query'
import { useAuthedFetch } from './useAuthedFetch'

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

export const QUERY_KEY = 'useUsers'

export const useUsers = () => {
const authedFetch = useAuthedFetch()

return useQuery({
queryKey: [QUERY_KEY],
queryFn: async () => {
const result = await authedFetch<{ users: UsersIndex[] }>({
path: '/users',
method: 'GET',
})
return result
.json!.users.map((user) => ({ ...user, role: user.role ?? 'member' }))
.sort((a, b) => a.email.localeCompare(b.email))
},
})
}
82 changes: 82 additions & 0 deletions src/pages/__tests__/users.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from 'react'
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, urlFor } from 'src/testHelpers'
import { useUsers } from 'src/network/useUsers'
import { UsersIndex } from 'src/network/useUsers'
import { faker } from '@faker-js/faker'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

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

describe('Users page', () => {
const renderUsersPage = () => {
return render(
<QueryClientProvider client={new QueryClient()}>
<UsersPage />
</QueryClientProvider>,
)
}

it('is displayed in a layout', () => {
const query = buildUseQueryResult<UsersIndex[]>({ isLoading: true, data: undefined })
asMock(useUsers).mockReturnValue(query)
const { baseElement } = renderUsersPage()
expect(baseElement.querySelector('.layout')).not.toBeNull()
})

it('displays the sidebar navigation', () => {
const query = buildUseQueryResult<UsersIndex[]>({ isLoading: true, data: undefined })
asMock(useUsers).mockReturnValue(query)
const { queryByTestId } = renderUsersPage()
expect(queryByTestId(sidebarNavigationTestId)).not.toBeNull()
})

describe('when loading', () => {
it('displays an loading spinner', () => {
const query = buildUseQueryResult<UsersIndex[]>({ isLoading: true, data: undefined })
asMock(useUsers).mockReturnValue(query)
const { queryByText } = renderUsersPage()
expect(queryByText('Loading the users')).not.toBeNull()
})
})

describe('when successful', () => {
it('displays the users', () => {
const users = [buildUserIndex({ role: 'admin' }), buildUserIndex()]
const [user1, user2] = users
const query = buildUseQueryResult({ data: users })
asMock(useUsers).mockReturnValue(query)

const { queryByText } = renderUsersPage()

const firstEmail = queryByText(user1.email)
expect(firstEmail).not.toBeNull()

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()
})
})

describe('when there is an error', () => {
it('displays an error', () => {
const error = new Error(faker.lorem.sentence())
const query = buildUseQueryResult<UsersIndex[]>({ error, isError: true })
asMock(useUsers).mockReturnValue(query)
const { queryByText } = renderUsersPage()
expect(queryByText(error.message)).not.toBeNull()
})
})
})
22 changes: 22 additions & 0 deletions src/pages/users.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.user-list {
margin-top: 3rem;
margin-bottom: 3rem;
}

.user-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
border-bottom: 1px solid var(--gray-light);
padding: 1.5rem 0;
}

.user-email {
font-size: 1.25rem;
font-weight: bold;
}

.user-role {
color: var(--gray-dark);
font-style: italic;
}
54 changes: 54 additions & 0 deletions src/pages/users.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React, { FC } from 'react'
import capitalize from 'lodash.capitalize'
import { HeadFC } from 'gatsby'
import {
Heading,
Layout,
List,
PageContent,
Paragraph,
Sidebar,
SkipNavContent,
SpacedContainer,
SidebarNavigation,
LoadingOverlay,
} from 'src/ui'
import { formatPageTitle } from 'src/utils/formatPageTitle'
import { useUsers } from 'src/network/useUsers'
import './users.css'

const UsersPage: FC = () => {
const { data: users, isLoading, error } = useUsers()

return (
<Layout element="div">
<Sidebar>
<SidebarNavigation />
</Sidebar>
<PageContent element="main">
<SkipNavContent />
<SpacedContainer>
<Heading element="h1">Users</Heading>
<Paragraph>All of 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>
<span className="user-role">{capitalize(user.role)}</span>
</li>
))}
</List>
)}

{isLoading && <LoadingOverlay description="Loading the users" />}
</SpacedContainer>
</PageContent>
</Layout>
)
}

export default UsersPage

export const Head: HeadFC = () => <title>{formatPageTitle('Users')}</title>
1 change: 1 addition & 0 deletions src/ui/SidebarNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +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" />} />
</WhenSignedIn>
<SpacedLink
to="/tips-and-tricks"
Expand Down
20 changes: 20 additions & 0 deletions src/ui/__tests__/SidebarNavigation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ describe('SidebarNavigation', () => {
expect(link.tagName).toEqual('A')
expect(link.href).toEqual(urlFor('/my-library'))
})

it('displays a users link', () => {
const { getByRole } = render(
<AuthProvider>
<SidebarNavigation />
</AuthProvider>,
)
const link: HTMLAnchorElement = getByRole('link', { name: 'Users' }) as any
expect(link.tagName).toEqual('A')
expect(link.href).toEqual(urlFor('/users'))
})
})

describe('when signed out', () => {
Expand All @@ -68,6 +79,15 @@ describe('SidebarNavigation', () => {
)
expect(queryByText('My Library')).toBeNull()
})

it('does not display a users link', () => {
const { queryByText } = render(
<AuthProvider>
<SidebarNavigation />
</AuthProvider>,
)
expect(queryByText('Users')).toBeNull()
})
})

describe('settings is available', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/utils/currentUrlSearchParamsFor.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const currentUrlSearchParamsFor = (key: string) => {
return typeof window !== 'undefined' ? new URLSearchParams(window.location.search).get(key) : null
}
}

0 comments on commit 2a5a752

Please sign in to comment.