Skip to content

Commit

Permalink
Render the whole app in page-level tests (#565)
Browse files Browse the repository at this point in the history
* proof of concept for making all page-level tests into integration tests

* same treatment for project create

* tweaks, eliminate all warnings

* automatically update packer-id

* tip from good guy KCD cleans things up quite a bit

https://github.com/kentcdodds/bookshelf/blob/8cbbd999d/src/test/app-test-utils.js#L15

* no longer need to separate *CreatePage and *CreateForm

* minor test tweaks

* automatically update packer-id

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
david-crespo and github-actions[bot] committed Jan 4, 2022
1 parent bd3af89 commit 438e56b
Show file tree
Hide file tree
Showing 7 changed files with 406 additions and 423 deletions.
153 changes: 69 additions & 84 deletions app/pages/ProjectCreatePage.tsx
Expand Up @@ -13,7 +13,6 @@ import {
Success16Icon,
FieldTitle,
} from '@oxide/ui'
import type { Project } from '@oxide/api'
import { useApiMutation, useApiQueryClient } from '@oxide/api'
import { useParams, useToast } from '../hooks'
import { getServerError } from '../util/errors'
Expand All @@ -24,105 +23,91 @@ const ERROR_CODES = {
'A project with that name already exists in this organization',
}

// exists primarily so we can test it without worrying about route params
export function ProjectCreateForm({
orgName,
onSuccess,
}: {
orgName: string
onSuccess: (p: Project) => void
}) {
const createProject = useApiMutation('organizationProjectsPost', {
onSuccess,
})
return (
<Formik
initialValues={{ name: '', description: '' }}
onSubmit={({ name, description }) => {
createProject.mutate({
organizationName: orgName,
body: { name, description },
})
}}
>
<Form>
<div className="mb-4">
<FieldTitle htmlFor="project-name">Choose a name</FieldTitle>
<TextField
id="project-name"
name="name"
placeholder="Enter name"
validate={validateName}
autoComplete="off"
/>
<TextFieldError name="name" />
</div>
<div className="mb-8">
<FieldTitle htmlFor="project-description">
Choose a description
</FieldTitle>
<TextFieldHint id="description-hint">
What is unique about your project?
</TextFieldHint>
<TextField
id="project-description"
name="description"
aria-describedby="description-hint"
placeholder="A project"
autoComplete="off"
/>
</div>
<Button
type="submit"
variant="dim"
className="w-[30rem]"
disabled={createProject.isLoading}
>
Create project
</Button>
<div className="text-red-500 mt-2">
{getServerError(createProject.error, ERROR_CODES)}
</div>
</Form>
</Formik>
)
}

export default function ProjectCreatePage() {
const queryClient = useApiQueryClient()
const addToast = useToast()
const navigate = useNavigate()

const { orgName } = useParams('orgName')

const createProject = useApiMutation('organizationProjectsPost', {
onSuccess(project) {
// refetch list of projects in sidebar
queryClient.invalidateQueries('organizationProjectsGet', {
organizationName: orgName,
})
// avoid the project fetch when the project page loads since we have the data
queryClient.setQueryData(
'organizationProjectsGetProject',
{ organizationName: orgName, projectName: project.name },
project
)
addToast({
icon: <Success16Icon />,
title: 'Success!',
content: 'Your project has been created.',
timeout: 5000,
})
navigate(`../${project.name}`)
},
})

return (
<>
<PageHeader>
<PageTitle icon={<Folder24Icon title="Projects" />}>
Create a new project
</PageTitle>
</PageHeader>
<ProjectCreateForm
orgName={orgName}
onSuccess={(project) => {
// refetch list of projects in sidebar
queryClient.invalidateQueries('organizationProjectsGet', {
<Formik
initialValues={{ name: '', description: '' }}
onSubmit={({ name, description }) => {
createProject.mutate({
organizationName: orgName,
body: { name, description },
})
// avoid the project fetch when the project page loads since we have the data
queryClient.setQueryData(
'organizationProjectsGetProject',
{ organizationName: orgName, projectName: project.name },
project
)
addToast({
icon: <Success16Icon />,
title: 'Success!',
content: 'Your project has been created.',
timeout: 5000,
})
navigate(`../${project.name}`)
}}
/>
>
<Form>
<div className="mb-4">
<FieldTitle htmlFor="project-name">Choose a name</FieldTitle>
<TextField
id="project-name"
name="name"
placeholder="Enter name"
validate={validateName}
autoComplete="off"
/>
<TextFieldError name="name" />
</div>
<div className="mb-8">
<FieldTitle htmlFor="project-description">
Choose a description
</FieldTitle>
<TextFieldHint id="description-hint">
What is unique about your project?
</TextFieldHint>
<TextField
id="project-description"
name="description"
aria-describedby="description-hint"
placeholder="A project"
autoComplete="off"
/>
</div>
<Button
type="submit"
variant="dim"
className="w-[30rem]"
disabled={createProject.isLoading}
>
Create project
</Button>
<div className="text-red-500 mt-2">
{getServerError(createProject.error, ERROR_CODES)}
</div>
</Form>
</Formik>
</>
)
}
@@ -1,17 +1,14 @@
import React from 'react'
import {
fireEvent,
lastBody,
renderWithRouter,
lastPostBody,
renderAppAt,
screen,
waitFor,
} from '../../test-utils'
import fetchMock from 'fetch-mock'

import { org, project, instance } from '@oxide/api-mocks'

import { InstanceCreateForm } from '../project/instances/create/InstancesCreatePage'

const submitButton = () =>
screen.getByRole('button', { name: 'Create instance' })

Expand All @@ -20,67 +17,66 @@ const instancesUrl = `${projectUrl}/instances`
const disksUrl = `${projectUrl}/disks`
const vpcsUrl = `${projectUrl}/vpcs`

let successSpy: jest.Mock

describe('InstanceCreateForm', () => {
beforeEach(() => {
// existing disk modal fetches disks on render even if it's not visible
fetchMock.get(disksUrl, 200)
fetchMock.get(vpcsUrl, 200)
successSpy = jest.fn()
renderWithRouter(
<InstanceCreateForm
orgName={org.name}
projectName={project.name}
onSuccess={successSpy}
/>
)
})
const formUrl = `/orgs/${org.name}/projects/${project.name}/instances/new`

const renderPage = () => {
// existing disk modal fetches disks on render even if it's not visible
fetchMock.get(disksUrl, 200)
fetchMock.get(vpcsUrl, 200)
fetchMock.get(projectUrl, 200)
return renderAppAt(formUrl)
}

describe('InstanceCreatePage', () => {
afterEach(() => {
fetchMock.reset()
})

it('disables submit button on submit and enables on response', async () => {
const mock = fetchMock.post(instancesUrl, 201)
it('disables submit button on submit', async () => {
fetchMock.post(instancesUrl, 201)
renderPage()

const submit = submitButton()
expect(submit).not.toBeDisabled()

fireEvent.click(submit)

expect(mock.called(instancesUrl)).toBeFalsy()
await waitFor(() => expect(submit).toBeDisabled())
expect(mock.done()).toBeTruthy()
expect(submit).not.toBeDisabled()
})

it('shows specific message for known server error code', async () => {
fetchMock.post(instancesUrl, {
status: 400,
body: { error_code: 'ObjectAlreadyExists' },
})
renderPage()

fireEvent.click(submitButton())

await screen.findByText(
'An instance with that name already exists in this project'
)
// don't nav away
expect(window.location.pathname).toEqual(formUrl)
})

it('shows generic message for unknown server error', async () => {
fetchMock.post(instancesUrl, {
status: 400,
body: { error_code: 'UnknownCode' },
})
renderPage()

fireEvent.click(submitButton())

await screen.findByText('Unknown error from server')
// don't nav away
expect(window.location.pathname).toEqual(formUrl)
})

it('posts form on submit', async () => {
const mock = fetchMock.post(instancesUrl, 201)
renderPage()

fireEvent.change(screen.getByLabelText('Choose a name'), {
target: { value: 'new-instance' },
Expand All @@ -89,7 +85,7 @@ describe('InstanceCreateForm', () => {
fireEvent.click(submitButton())

await waitFor(() =>
expect(lastBody(mock)).toEqual({
expect(lastPostBody(mock)).toEqual({
name: 'new-instance',
description: 'An instance in project: mock-project',
hostname: '',
Expand All @@ -99,15 +95,17 @@ describe('InstanceCreateForm', () => {
)
})

it('calls onSuccess on success', async () => {
it('navigates to project instances page on success', async () => {
const mock = fetchMock.post(instancesUrl, { status: 201, body: instance })
renderPage()

expect(successSpy).not.toHaveBeenCalled()
const instancesPage = `/orgs/${org.name}/projects/${project.name}/instances`
expect(window.location.pathname).not.toEqual(instancesPage)

fireEvent.click(submitButton())

await waitFor(() => expect(mock.called(instancesUrl)).toBeTruthy())
await waitFor(() => expect(mock.done()).toBeTruthy())
await waitFor(() => expect(successSpy).toHaveBeenCalled())
await waitFor(() => expect(window.location.pathname).toEqual(instancesPage))
})
})

0 comments on commit 438e56b

Please sign in to comment.