Skip to content

Commit

Permalink
Added a destroy email template dialog
Browse files Browse the repository at this point in the history
  • Loading branch information
crismali committed May 24, 2024
1 parent 4764cbc commit 8795e59
Show file tree
Hide file tree
Showing 9 changed files with 228 additions and 14 deletions.
21 changes: 15 additions & 6 deletions src/pages/__tests__/my-library.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { asMock, buildEmailTemplateIndex, buildUseQueryResult, urlFor } from 'sr
import { useEmailTemplates } from 'src/network/useEmailTemplates'
import { EmailTemplateIndex } from 'src/network/useEmailTemplates'
import { faker } from '@faker-js/faker'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

jest.mock('src/network/useEmailTemplates', () => {
return {
Expand All @@ -14,25 +15,33 @@ jest.mock('src/network/useEmailTemplates', () => {
})

describe('My Library page', () => {
const renderMyLibraryPage = () => {
return render(
<QueryClientProvider client={new QueryClient()}>
<MyLibraryPage />
</QueryClientProvider>,
)
}

it('is displayed in a layout', () => {
const query = buildUseQueryResult<EmailTemplateIndex[]>({ isLoading: true, data: undefined })
asMock(useEmailTemplates).mockReturnValue(query)
const { baseElement } = render(<MyLibraryPage />)
const { baseElement } = renderMyLibraryPage()
expect(baseElement.querySelector('.layout')).not.toBeNull()
})

it('displays the sidebar navigation', () => {
const query = buildUseQueryResult<EmailTemplateIndex[]>({ isLoading: true, data: undefined })
asMock(useEmailTemplates).mockReturnValue(query)
const { queryByTestId } = render(<MyLibraryPage />)
const { queryByTestId } = renderMyLibraryPage()
expect(queryByTestId(sidebarNavigationTestId)).not.toBeNull()
})

describe('when loading', () => {
it('displays an loading spinner', () => {
const query = buildUseQueryResult<EmailTemplateIndex[]>({ isLoading: true, data: undefined })
asMock(useEmailTemplates).mockReturnValue(query)
const { queryByText } = render(<MyLibraryPage />)
const { queryByText } = renderMyLibraryPage()
expect(queryByText('Loading your email templates')).not.toBeNull()
})
})
Expand All @@ -44,7 +53,7 @@ describe('My Library page', () => {
const query = buildUseQueryResult({ data: emailTemplates })
asMock(useEmailTemplates).mockReturnValue(query)

const { queryByText } = render(<MyLibraryPage />)
const { queryByText } = renderMyLibraryPage()

const firstLink: HTMLAnchorElement | null = queryByText(emailTemplate1.name) as any
expect(firstLink).not.toBeNull()
Expand All @@ -60,7 +69,7 @@ describe('My Library page', () => {
it('displays an empty message', () => {
const query = buildUseQueryResult<EmailTemplateIndex[]>({ data: [] })
asMock(useEmailTemplates).mockReturnValue(query)
const { baseElement } = render(<MyLibraryPage />)
const { baseElement } = renderMyLibraryPage()
expect(baseElement).toHaveTextContent("Looks like you don't have any saved templates yet")
})
})
Expand All @@ -70,7 +79,7 @@ describe('My Library page', () => {
const error = new Error(faker.lorem.sentence())
const query = buildUseQueryResult<EmailTemplateIndex[]>({ error, isError: true })
asMock(useEmailTemplates).mockReturnValue(query)
const { queryByText } = render(<MyLibraryPage />)
const { queryByText } = renderMyLibraryPage()
expect(queryByText(error.message)).not.toBeNull()
})
})
Expand Down
2 changes: 1 addition & 1 deletion src/pages/email-templates/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const EmailTemplateShowPage: FC<Props> = ({ params }) => {
<EmailEditorSidebar
emailTemplate={emailTemplate}
heading={
<h1 style={{ fontSize: '1.5rem' }}>
<h1 style={{ fontSize: '1.5rem', paddingLeft: '0.5rem' }}>
{byQueryState(query, {
data: ({ name }) => name,
loading: () => 'Loading...',
Expand Down
21 changes: 21 additions & 0 deletions src/pages/library.css
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,23 @@
border-bottom: 0;
}

.library-item .delete-trigger {
cursor: pointer;
margin-left: 0.75rem;
padding-top: 0.35rem;
transition: filter ease-in-out 150ms;
}

.library-item .delete-trigger:focus img,
.library-item .delete-trigger:hover img {
filter: invert(13%) sepia(80%) saturate(4782%) hue-rotate(354deg) brightness(80%) contrast(105%);
}

.library-name-container {
align-items: center;
display: flex;
}

.library-name {
color: var(--black);
font-weight: var(--weight-bold);
Expand All @@ -65,3 +82,7 @@
.library-empty-message {
padding-top: 7.5rem;
}

.destroy-email-template-form .delete-button {
background-color: var(--alert-error-dark);
}
16 changes: 10 additions & 6 deletions src/pages/my-library.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
SpacedContainer,
} from 'src/ui'
import { useEmailTemplates } from 'src/network/useEmailTemplates'
import { DestroyEmailTemplate } from 'src/ui/MyLibrary/DestroyEmailTemplate'

const MyLibraryPage: FC = () => {
const { data: emailTemplates, isLoading, error } = useEmailTemplates()
Expand All @@ -31,12 +32,15 @@ const MyLibraryPage: FC = () => {
{error && <Paragraph>{error.message}</Paragraph>}
{emailTemplates && emailTemplates.length > 0 && (
<List className="library-list">
{emailTemplates.map(({ id, name, description }) => (
<li key={id} className="library-item">
<Link to={`/email-templates/${id}`} className="library-name">
{name}
</Link>
<p className="library-description">{description}</p>
{emailTemplates.map((emailTemplate) => (
<li key={emailTemplate.id} className="library-item">
<div className="library-name-container">
<Link to={`/email-templates/${emailTemplate.id}`} className="library-name">
{emailTemplate.name}
</Link>
<DestroyEmailTemplate emailTemplate={emailTemplate} />
</div>
<p className="library-description">{emailTemplate.description}</p>
</li>
))}
</List>
Expand Down
6 changes: 6 additions & 0 deletions src/ui/Button.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,9 @@
background-color: var(--gray-medium-dark);
cursor: unset;
}

.button-like {
background-color: transparent;
border: 0;
font-size: 1rem;
}
4 changes: 4 additions & 0 deletions src/ui/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ export interface Props
export const Button = forwardRef<HTMLButtonElement, Props>(({ className, ...props }, ref) => {
return <button ref={ref} className={classNames('button', className)} {...props} />
})

export const ButtonLike = forwardRef<HTMLButtonElement, Props>(({ className, ...props }, ref) => {
return <button ref={ref} className={classNames('button-like', className)} {...props} />
})
43 changes: 43 additions & 0 deletions src/ui/MyLibrary/DestroyEmailTemplate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { FC } from 'react'
import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
import { useDestroyEmailTemplate } from 'src/network/useDestroyEmailTemplate'
import { EmailTemplateIndex } from 'src/network/useEmailTemplates'
import { Dialog, Form, LoadingOverlay, ButtonLike, Button, UswdsIcon } from 'src/ui'

interface DestroyEmailTemplateProps {
emailTemplate: EmailTemplateIndex
}

export const DestroyEmailTemplate: FC<DestroyEmailTemplateProps> = ({ emailTemplate }) => {
const { mutateAsync, isPending, error } = useDestroyEmailTemplate()

return (
<Dialog
trigger={
<ButtonLike className="delete-trigger">
<VisuallyHidden>Delete</VisuallyHidden>
<UswdsIcon icon="Delete" />
</ButtonLike>
}
title="Delete Email Template"
description={`Are you sure you want to delete ${emailTemplate.name}?`}
contents={({ close }) => (
<>
<Form
className="destroy-email-template-form"
errorMessage={error?.message}
onSubmit={async () => {
await mutateAsync(emailTemplate.id)
close()
}}
>
<Button type="submit" className="delete-button">
Delete
</Button>
</Form>
{isPending && <LoadingOverlay description="Deleting email template" />}
</>
)}
/>
)
}
86 changes: 86 additions & 0 deletions src/ui/MyLibrary/__tests__/DestroyEmailTemplate.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React from 'react'
import { buildEmailTemplateIndex, buildUseMutationResult } from 'src/factories'
import { useDestroyEmailTemplate } from 'src/network/useDestroyEmailTemplate'
import { asMock } from 'src/testHelpers'
import { DestroyEmailTemplate } from '../DestroyEmailTemplate'
import { render } from '@testing-library/react'
import userEvent, { UserEvent } from '@testing-library/user-event'
import { randomUUID } from 'crypto'
import { EmailTemplateIndex } from 'src/network/useEmailTemplates'
import { faker } from '@faker-js/faker'

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

describe('DestroyEmailTemplate', () => {
let user: UserEvent
let id: string
let emailTemplate: EmailTemplateIndex

beforeEach(() => {
user = userEvent.setup()
id = randomUUID()
emailTemplate = buildEmailTemplateIndex({ id })
})

describe('when closed', () => {
beforeEach(() => {
const mutationResult = buildUseMutationResult<ReturnType<typeof useDestroyEmailTemplate>>()
asMock(useDestroyEmailTemplate).mockReturnValue(mutationResult)
})

it('can be opened', async () => {
const { getByRole, queryByRole } = render(
<DestroyEmailTemplate emailTemplate={emailTemplate} />,
)
expect(queryByRole('dialog')).toBeNull()
await user.click(getByRole('button', { name: 'Delete' }))
expect(queryByRole('dialog')).not.toBeNull()
})
})

describe('when open', () => {
const renderAndOpen = async () => {
const rendered = render(<DestroyEmailTemplate emailTemplate={emailTemplate} />)
await user.click(rendered.getByRole('button', { name: 'Delete' }))
return rendered
}

it('confirms and destroys the email template successfully and closes the modal', async () => {
const mutateAsync = jest.fn()
const mutationResult = buildUseMutationResult<ReturnType<typeof useDestroyEmailTemplate>>({
mutateAsync,
})
asMock(useDestroyEmailTemplate).mockReturnValue(mutationResult)
const { getByRole, queryByRole } = await renderAndOpen()

expect(mutateAsync).not.toHaveBeenCalled()
expect(queryByRole('dialog')).not.toBeNull()

await user.click(getByRole('button', { name: 'Delete' }))

expect(mutateAsync).toHaveBeenCalledWith(id)
expect(queryByRole('dialog')).toBeNull()
})

it('displays a loading spinner when destroying the email template', async () => {
const mutationResult = buildUseMutationResult<ReturnType<typeof useDestroyEmailTemplate>>({
isPending: true,
})
asMock(useDestroyEmailTemplate).mockReturnValue(mutationResult)
const { baseElement } = await renderAndOpen()
expect(baseElement).toHaveTextContent('Deleting email template')
})

it('displays an error when destroying the email template fails', async () => {
const errorMessage = faker.lorem.sentence()
const mutationResult = buildUseMutationResult<ReturnType<typeof useDestroyEmailTemplate>>({
error: new Error(errorMessage),
})
asMock(useDestroyEmailTemplate).mockReturnValue(mutationResult)
const { baseElement } = await renderAndOpen()
expect(baseElement).toHaveTextContent(errorMessage)
})
})
})
43 changes: 42 additions & 1 deletion src/ui/__tests__/Button.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'
import { Button, Props } from '../Button'
import { Button, ButtonLike, Props } from '../Button'
import { render } from '@testing-library/react'
import { faker } from '@faker-js/faker'
import userEvent from '@testing-library/user-event'
Expand Down Expand Up @@ -44,3 +44,44 @@ describe('Button', () => {
expect(button.className).toEqual(`button ${className}`)
})
})

describe('ButtonLike', () => {
const renderButtonLink = (props: Partial<Props>) => {
return render(<ButtonLike {...props} />)
}

it('is a button', () => {
const { queryByRole } = renderButtonLink({})
expect(queryByRole('button')).not.toBeNull()
})

it('displays children', () => {
const text = faker.lorem.paragraph()
const { baseElement } = renderButtonLink({
children: <p>{text}</p>,
})
expect(baseElement).toContainHTML(`<p>${text}</p>`)
})

it('handles clicks', async () => {
const user = userEvent.setup()
const handleClick = jest.fn()
const { getByRole } = renderButtonLink({ onClick: handleClick })
expect(handleClick).not.toHaveBeenCalled()
await user.click(getByRole('button'))
expect(handleClick).toHaveBeenCalled()
})

it('accepts button props', () => {
const { getByRole } = renderButtonLink({ type: 'submit' })
const button: HTMLButtonElement = getByRole('button') as any
expect(button.type).toEqual('submit')
})

it('accepts className', () => {
const className = faker.lorem.word()
const { getByRole } = renderButtonLink({ className })
const button = getByRole('button')
expect(button.className).toEqual(`button-like ${className}`)
})
})

0 comments on commit 8795e59

Please sign in to comment.