Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test suite for client #8

Merged
merged 8 commits into from
Jun 20, 2019
Merged

Test suite for client #8

merged 8 commits into from
Jun 20, 2019

Conversation

hallettj
Copy link
Contributor

This change sets up Jest and Enzyme for testing React components. It includes fixtures and helpers to mock GraphQL requests.

const app = mount(<App />, {
mocks: [$.getAccountMock],
route: "accounts/1/dashboard"
})
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mount test helper renders a React component for testing. It sets up a provider to handle GraphQL queries originating from the component. It also sets up a LocationProvider to provide the initial URL set up by the test, and to keep track of any URL changes that components make.

In most cases you will need to provide an array of mocks. Each of these is a predefined response to a GraphQL query. You will need to provide one mock for each query. If the same query runs more than once, you will need to specify a mock for each request.

})

it("archives a conversation from the conversation view", async () => {
const history = createHistory(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Poodle uses @reach/router to render different React components when the URL changes. By default the router reads the location from window.location. But we can provide a different history source for the router to read and update location. In tests we use a "memory source" which stores the location in a plain javascript object. In this test I create a history object explicitly and pass it to mount so that I can check history.location after I expect the app to have changed the URL.

})
await updates(app)
app.find("button[aria-label='archive']").simulate("click")
await updates(app, 10)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the updates helper when you need to allow for a delay for the component to update after an asynchronous process, such as a GraphQL request. You can provide a second argument to specify a longer delay, given in milliseconds.

setIsRead()
}
}, [data, setIsRead])
const setIsReadResult = useSetIsRead(data && data.conversation)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to simplify the code here by replacing the graphql mutation and effect hook with a single invocation of a custom hook.

@@ -103,7 +103,7 @@ const useStyles = makeStyles(theme => ({
}
}))

export default function Dashboard({ accountId }: Props) {
export default function Dashboard({ accountId, navigate }: Props) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I switched from using the imported navigate function to getting navigate from props. Dashboard is a child component of a Router component. Router injects the navigate prop into its children. The navigate function in props uses the mock history set up in LocationProvider, which is important for testing.

@@ -239,7 +240,7 @@ function SelectedActionsBar({
>
<Toolbar className={classes.toolbar}>
<span className={classes.title} />
<IconButton onClick={onArchive}>
<IconButton aria-label="archive" onClick={onArchive}>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding an aria-label here is useful for accessibility, and it also gives me a selector I can use to find the archive button in tests.

)}
{conversations.map((conversation, index) => {
return (
<React.Fragment key={conversation.id}>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the key prop from index to conversation.id. React uses the key prop to match up elements that should be considered "the same" from one render to the next. When performing updates we want React to treat rows representing the same conversation as the same, and using the conversation ID as a key makes this work.

</List>
</Paper>
)
}

function ConversationRow({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I split out a new component to display each row in the conversation list view. This way I can easily locate conversation rows in tests with app.find("ConversationRow").

import { MutationResult } from "react-apollo"
import * as graphql from "../generated/graphql"

export default function useSetisRead(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a custom hook; this pattern is highly useful for encapsulating logic to keep components as simple as possible. A custom hook is a function. The name always starts with use so that ESLint can correctly make check uses of the hook.

Notice that a custom hook can call other hooks. Hooks only work when they are called either from function components or from custom hooks.

mounts = []
})

export function mount(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a helper for rendering React components for tests. Most of our components will only work correctly if they are rendered inside of an Apollo provider component that handles making GraphQL queries. In this case MockedProvider sends back predefined responses instead of calling a real API. In addition LocationProvider tells @reach/router to use the provided mock history object instead of using window.history, which would attempt to use the real window location.

There is an afterEach callback here that automatically unmounts any mounted components after each test.

@hallettj hallettj requested a review from breitman June 20, 2019 14:46
@hallettj hallettj merged commit fda8cd4 into master Jun 20, 2019
@hallettj hallettj deleted the jh/test-suite-for-client branch June 20, 2019 15:24
@hallettj
Copy link
Contributor Author

hallettj commented Nov 3, 2019

🎉 This PR is included in version 1.0.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@hallettj
Copy link
Contributor Author

hallettj commented Nov 4, 2019

🎉 This PR is included in version 1.0.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants