diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md
index da06d1f7..85d1eb13 100644
--- a/INSTRUCTIONS.md
+++ b/INSTRUCTIONS.md
@@ -31,6 +31,7 @@ through it on your own if you like.
* [What types of testing are there?](#what-types-of-testing-are-there-1)
* [What's a test](#whats-a-test-1)
* [Intro to Jest](#intro-to-jest)
+ * [Testing a React Component](#testing-a-react-component)
* [Configuring Jest](#configuring-jest)
* [Unit testing components](#unit-testing-components)
* [Effective Snapshot Testing](#effective-snapshot-testing)
@@ -39,9 +40,10 @@ through it on your own if you like.
* [End-to-end testing](#end-to-end-testing)
* [Write tests. Not too many. Mostly integration.](#write-tests-not-too-many-mostly-integration-1)
* [Shared Content](#shared-content)
+ * [What's a test](#whats-a-test-2)
* [What types of testing are there?](#what-types-of-testing-are-there-2)
* [Jest](#jest)
- * [Code Coverage](#code-coverage)
+ * [Code Coverage](#code-coverage)
* [Write tests. Not too many. Mostly integration.](#write-tests-not-too-many-mostly-integration-2)
@@ -284,23 +286,43 @@ See below in the shared content
* Configure Cypress for a web application
* Write E2E (end-to-end) tests with Cypress
-### What's a test
+### What types of testing are there?
See below in the shared content
-### What types of testing are there?
+### What's a test
+
+See below in the shared content
> NOTE: This is duplicate content from the practices and principles workshop
> In this one however, folks should just watch the instructor go through things
> to make time for the rest of the content and not bore those who have already
> gone through this material.
-See below in the shared content
-
### Intro to Jest
See below in the shared content
+### Testing a React Component
+
+**Instruction**:
+
+* Nothing much here, direct people to the exercise and inform them they can
+ use the solution for reference
+
+**Exercise**:
+
+* Start the simple react tests in watch mode with `npm run test:react`
+* Open `other/simple-react/item-list.js` and `other/simple-react/__tests__/item-list.todo.js`
+* Follow the instructions to test the component
+
+**Takeaways**
+
+* The key here is to render the component and assert on the output.
+* Assuming this were the only component for your entire application, attempt to
+ use it the way the user would and let that inform your decisions of how you
+ test it.
+
### Configuring Jest
**New Things**:
diff --git a/client/src/__tests__/app.login.todo.js b/client/src/__tests__/app.login.todo.js
index 6b0329cb..a8c60f73 100644
--- a/client/src/__tests__/app.login.todo.js
+++ b/client/src/__tests__/app.login.todo.js
@@ -32,7 +32,7 @@ test('login as an existing user', async () => {
// 3. Change submitted from `false` to `true`
// 4. And you're all done!
/*
-http://ws.kcd.im/?ws=Testing&e=login.step-3&em=
+http://ws.kcd.im/?ws=Testing&e=app.login&em=
*/
test.skip('I submitted my elaboration and feedback', () => {
const submitted = false // change this when you've submitted!
diff --git a/client/src/components/__tests__/__snapshots__/login.step-4.js.snap b/client/src/components/__tests__/__snapshots__/login.step-4.js.snap
new file mode 100644
index 00000000..fd9d0402
--- /dev/null
+++ b/client/src/components/__tests__/__snapshots__/login.step-4.js.snap
@@ -0,0 +1,148 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`snapshot 1`] = `
+.css-0,
+[data-css-0] {
+ display: -webkit-box;
+ display: -moz-box;
+ display: -ms-flexbox;
+ display: -webkit-flex;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+ min-width: 200px;
+ max-width: 400px;
+ margin-left: -116px;
+ -webkit-box-orient: vertical;
+ -webkit-box-direction: normal;
+ -webkit-flex-direction: column;
+ -webkit-box-align: center;
+ -webkit-align-items: center;
+}
+
+.css-1,
+[data-css-1] {
+ display: grid;
+ grid-template-columns: 100px 1fr;
+ grid-gap: 16px;
+ font-size: 18px;
+ align-items: center;
+ margin-top: 30px;
+ margin-bottom: 30px;
+ width: 100%;
+ -ms-grid-template-columns: 100px 1fr;
+ -ms-grid-gap: 16px;
+ -webkit-box-align: center;
+ -webkit-align-items: center;
+}
+
+.css-2,
+[data-css-2] {
+ background: white;
+ height: 50px;
+ border: none;
+ border-radius: 10px;
+ box-shadow: var(--shadow);
+ border-bottom: 5px;
+ width: 100%;
+ min-width: 150px;
+ display: block;
+ padding-left: 10px;
+}
+
+.css-2::-webkit-input-placeholder,
+[data-css-2]::-webkit-input-placeholder {
+ opacity: 0.5;
+}
+
+.css-2::-moz-placeholder,
+[data-css-2]::-moz-placeholder {
+ opacity: 0.5;
+}
+
+.css-2::-ms-input-placeholder,
+[data-css-2]::-ms-input-placeholder {
+ opacity: 0.5;
+}
+
+.css-2::placeholder,
+[data-css-2]::placeholder {
+ opacity: 0.5;
+}
+
+.css-7,
+[data-css-7] {
+ font-size: 13px;
+ font-family: Raleway,sans-serif;
+ background: var(--green);
+ padding: 10px 20px;
+ display: block;
+ margin-left: auto;
+ color: white;
+ border: none;
+ border-radius: 10px;
+ box-shadow: var(--shadow);
+ cursor: pointer;
+ transition: 0.5s;
+ -webkit-transition: 0.5s;
+ -moz-transition: 0.5s;
+}
+
+.css-7:hover,
+[data-css-7]:hover {
+ box-shadow: var(--shadowHover);
+}
+
+@media only screen and (max-width: 819px) {
+ .css-0,
+ [data-css-0] {
+ margin-left: 0;
+ width: 90%;
+ }
+}
+
+
+`;
diff --git a/client/src/components/__tests__/login.final.js b/client/src/components/__tests__/login.final.js
index a1683f69..506ec2fe 100644
--- a/client/src/components/__tests__/login.final.js
+++ b/client/src/components/__tests__/login.final.js
@@ -1,29 +1,33 @@
import React from 'react'
-import {generate, render, Simulate} from 'til-client-test-utils'
+import {
+ generate,
+ renderIntoDocument,
+ cleanup,
+ render,
+} from 'til-client-test-utils'
import Login from '../login'
+afterEach(cleanup)
+
test('calls onSubmit with the username and password when submitted', () => {
// Arrange
const fakeUser = generate.loginForm()
const handleSubmit = jest.fn()
- const {container, queryByLabelText, queryByText} = render(
+ const {getByLabelText, getByText} = renderIntoDocument(
,
)
- const usernameNode = queryByLabelText('username')
- const passwordNode = queryByLabelText('password')
- const formNode = container.querySelector('form')
- const submitButtonNode = queryByText('submit')
+ const usernameNode = getByLabelText('username')
+ const passwordNode = getByLabelText('password')
// Act
usernameNode.value = fakeUser.username
passwordNode.value = fakeUser.password
- Simulate.submit(formNode)
+ getByText('submit').click()
// Assert
expect(handleSubmit).toHaveBeenCalledTimes(1)
expect(handleSubmit).toHaveBeenCalledWith(fakeUser)
- expect(submitButtonNode.type).toBe('submit')
})
test('snapshot', () => {
diff --git a/client/src/components/__tests__/login.step-2.js b/client/src/components/__tests__/login.step-2.js
index df6177e6..84f56633 100644
--- a/client/src/components/__tests__/login.step-2.js
+++ b/client/src/components/__tests__/login.step-2.js
@@ -6,14 +6,14 @@ test('calls onSubmit with the username and password when submitted', () => {
// Arrange
const fakeUser = generate.loginForm()
const handleSubmit = jest.fn()
- const {container, queryByLabelText, queryByText} = render(
+ const {container, getByLabelText, getByText} = render(
,
)
- const usernameNode = queryByLabelText('username')
- const passwordNode = queryByLabelText('password')
+ const usernameNode = getByLabelText('username')
+ const passwordNode = getByLabelText('password')
const formNode = container.querySelector('form')
- const submitButtonNode = queryByText('submit')
+ const submitButtonNode = getByText('submit')
// Act
usernameNode.value = fakeUser.username
diff --git a/client/src/components/__tests__/login.step-2.todo.js b/client/src/components/__tests__/login.step-2.todo.js
index fe8f2071..2871e37d 100644
--- a/client/src/components/__tests__/login.step-2.todo.js
+++ b/client/src/components/__tests__/login.step-2.todo.js
@@ -2,6 +2,8 @@ import React from 'react'
import ReactDOM from 'react-dom'
// you'll need these:
// import {generate, render, Simulate} from 'til-client-test-utils'
+// note that til-client-test-utils is found in `client/test/til-client-test-utils`
+// and it re-exports some utilities from react-testing-library (like render and Simulate)
import Login from '../login'
test('calls onSubmit with the username and password when submitted', () => {
@@ -11,7 +13,7 @@ test('calls onSubmit with the username and password when submitted', () => {
const handleSubmit = jest.fn()
// use: render()
// It'll give you back an object with
- // `queryByLabelText` and `queryByText` functions
+ // `getByLabelText` and `getByText` functions
// so you don't need a div anymore!
const div = document.createElement('div')
ReactDOM.render(, div)
@@ -44,7 +46,7 @@ test('calls onSubmit with the username and password when submitted', () => {
// 3. Change submitted from `false` to `true`
// 4. And you're all done!
/*
-http://ws.kcd.im/?ws=Testing&e=login.step-2&em=
+http://ws.kcd.im/?ws=Testing&e=login.step-2%20(react-testing-library)&em=
*/
test.skip('I submitted my elaboration and feedback', () => {
const submitted = false // change this when you've submitted!
diff --git a/client/src/components/__tests__/login.step-3.js b/client/src/components/__tests__/login.step-3.js
new file mode 100644
index 00000000..909b582f
--- /dev/null
+++ b/client/src/components/__tests__/login.step-3.js
@@ -0,0 +1,28 @@
+import React from 'react'
+import {generate, renderIntoDocument, cleanup} from 'til-client-test-utils'
+import Login from '../login'
+
+// If you render your components with renderIntoDocument via react-testing-library
+// then you can get automatic cleanup of any components rendered like this:
+afterEach(cleanup)
+
+test('calls onSubmit with the username and password when submitted', () => {
+ // Arrange
+ const fakeUser = generate.loginForm()
+ const handleSubmit = jest.fn()
+ const {getByLabelText, getByText} = renderIntoDocument(
+ ,
+ )
+
+ const usernameNode = getByLabelText('username')
+ const passwordNode = getByLabelText('password')
+
+ // Act
+ usernameNode.value = fakeUser.username
+ passwordNode.value = fakeUser.password
+ getByText('submit').click()
+
+ // Assert
+ expect(handleSubmit).toHaveBeenCalledTimes(1)
+ expect(handleSubmit).toHaveBeenCalledWith(fakeUser)
+})
diff --git a/client/src/components/__tests__/login.step-3.todo.js b/client/src/components/__tests__/login.step-3.todo.js
index ec3debda..2e51c26d 100644
--- a/client/src/components/__tests__/login.step-3.todo.js
+++ b/client/src/components/__tests__/login.step-3.todo.js
@@ -2,18 +2,33 @@ import React from 'react'
import {generate, render, Simulate} from 'til-client-test-utils'
import Login from '../login'
+// Due to the fact that our element is not in the document, the
+// click event on the submit button will not be treated as a
+// submit event on the form which is why we're simulating a submit
+// event on the form rather than clicking the button and then
+// asserting the button's type is set to submit rather than just
+// clicking on the button.
+//
+// Alternatively, we could actually insert the element directly into
+// the document, then click on the button and that should work!
+// Try doing that!
+// (Tip: document.body.appendChild(container) and getByText('submit').click())
+//
+// Bonus: Don't forget to cleanup after yourselve when you're finished so you don't
+// have things hanging out in the document!
+
test('calls onSubmit with the username and password when submitted', () => {
// Arrange
const fakeUser = generate.loginForm()
const handleSubmit = jest.fn()
- const {container, queryByLabelText, queryByText} = render(
+ const {container, getByLabelText, getByText} = render(
,
)
- const usernameNode = queryByLabelText('username')
- const passwordNode = queryByLabelText('password')
+ const usernameNode = getByLabelText('username')
+ const passwordNode = getByLabelText('password')
const formNode = container.querySelector('form')
- const submitButtonNode = queryByText('submit')
+ const submitButtonNode = getByText('submit')
// Act
usernameNode.value = fakeUser.username
@@ -26,11 +41,6 @@ test('calls onSubmit with the username and password when submitted', () => {
expect(submitButtonNode.type).toBe('submit')
})
-test('snapshot', () => {
- // render the login, this will give you back an object with a `container` property
- // expect the `container` property to match a snapshot
-})
-
//////// Elaboration & Feedback /////////
// When you've finished with the exercises:
// 1. Copy the URL below into your browser and fill out the form
@@ -38,7 +48,7 @@ test('snapshot', () => {
// 3. Change submitted from `false` to `true`
// 4. And you're all done!
/*
-http://ws.kcd.im/?ws=Testing&e=login.step-3&em=
+http://ws.kcd.im/?ws=Testing&e=login.step-3%20(renderIntoDocument)&em=
*/
test.skip('I submitted my elaboration and feedback', () => {
const submitted = false // change this when you've submitted!
diff --git a/client/src/components/__tests__/login.step-4.js b/client/src/components/__tests__/login.step-4.js
new file mode 100644
index 00000000..ba76da8d
--- /dev/null
+++ b/client/src/components/__tests__/login.step-4.js
@@ -0,0 +1,41 @@
+import React from 'react'
+import {
+ generate,
+ renderIntoDocument,
+ render,
+ cleanup,
+} from 'til-client-test-utils'
+import Login from '../login'
+
+afterEach(cleanup)
+
+test('calls onSubmit with the username and password when submitted', () => {
+ // Arrange
+ const fakeUser = generate.loginForm()
+ const handleSubmit = jest.fn()
+ const {getByLabelText, getByText} = renderIntoDocument(
+ ,
+ )
+
+ const usernameNode = getByLabelText('username')
+ const passwordNode = getByLabelText('password')
+
+ // Act
+ usernameNode.value = fakeUser.username
+ passwordNode.value = fakeUser.password
+ getByText('submit').click()
+
+ // Assert
+ expect(handleSubmit).toHaveBeenCalledTimes(1)
+ expect(handleSubmit).toHaveBeenCalledWith(fakeUser)
+})
+
+test('snapshot', () => {
+ // note that we don't need to render this into the document so we'll just
+ // use a regular `render` here.
+ const {container} = render()
+ // note that we're snapshotting the firstChild rather than the container.
+ // That's just because the container will always be the same. A div.
+ // So no reason to include that in the snapshot.
+ expect(container.firstChild).toMatchSnapshot()
+})
diff --git a/client/src/components/__tests__/login.step-4.todo.js b/client/src/components/__tests__/login.step-4.todo.js
new file mode 100644
index 00000000..e0dd2203
--- /dev/null
+++ b/client/src/components/__tests__/login.step-4.todo.js
@@ -0,0 +1,46 @@
+import React from 'react'
+import {generate, renderIntoDocument, cleanup} from 'til-client-test-utils'
+import Login from '../login'
+
+afterEach(cleanup)
+
+test('calls onSubmit with the username and password when submitted', () => {
+ // Arrange
+ const fakeUser = generate.loginForm()
+ const handleSubmit = jest.fn()
+ const {getByLabelText, getByText} = renderIntoDocument(
+ ,
+ )
+
+ const usernameNode = getByLabelText('username')
+ const passwordNode = getByLabelText('password')
+
+ // Act
+ usernameNode.value = fakeUser.username
+ passwordNode.value = fakeUser.password
+ getByText('submit').click()
+
+ // Assert
+ expect(handleSubmit).toHaveBeenCalledTimes(1)
+ expect(handleSubmit).toHaveBeenCalledWith(fakeUser)
+})
+
+test('snapshot', () => {
+ // render the login, this will give you back an object with a `container` property
+ // expect the `container` property to match a snapshot
+})
+
+//////// Elaboration & Feedback /////////
+// When you've finished with the exercises:
+// 1. Copy the URL below into your browser and fill out the form
+// 2. remove the `.skip` from the test below
+// 3. Change submitted from `false` to `true`
+// 4. And you're all done!
+/*
+http://ws.kcd.im/?ws=Testing&e=login.step-4%20(snapshots)&em=
+*/
+test.skip('I submitted my elaboration and feedback', () => {
+ const submitted = false // change this when you've submitted!
+ expect(submitted).toBe(true)
+})
+////////////////////////////////
diff --git a/client/src/screens/__tests__/editor.js b/client/src/screens/__tests__/editor.js
index 7568b973..4b215be7 100644
--- a/client/src/screens/__tests__/editor.js
+++ b/client/src/screens/__tests__/editor.js
@@ -1,7 +1,16 @@
import React from 'react'
-import {generate, render, Simulate, wait} from 'til-client-test-utils'
+import {
+ generate,
+ wait,
+ cleanup,
+ fireEvent,
+ renderIntoDocument,
+ render,
+} from 'til-client-test-utils'
import Editor from '../editor'
+afterEach(cleanup)
+
test('calls onSubmit with the username and password when submitted', async () => {
// Arrange
const fakeUser = generate.userData({id: generate.id()})
@@ -12,19 +21,17 @@ test('calls onSubmit with the username and password when submitted', async () =>
create: jest.fn(() => Promise.resolve()),
},
}
- const {container, getByText, getByLabelText} = render(
+ const {getByText, getByLabelText} = renderIntoDocument(
,
)
getByLabelText('Title').value = fakePost.title
getByLabelText('Content').value = fakePost.content
getByLabelText('Tags').value = fakePost.tags.join(', ')
- const submitButtonNode = getByText('submit')
- const formNode = container.querySelector('form')
const preDate = Date.now()
// Act
- Simulate.submit(formNode)
+ fireEvent.click(getByText('submit'))
// Assert
expect(fakeApi.posts.create).toHaveBeenCalledTimes(1)
@@ -39,7 +46,6 @@ test('calls onSubmit with the username and password when submitted', async () =>
const date = new Date(fakeApi.posts.create.mock.calls[0][0].date).getTime()
expect(date).toBeGreaterThanOrEqual(preDate)
expect(date).toBeLessThanOrEqual(postDate)
- expect(submitButtonNode.type).toBe('submit')
})
test('snapshot', () => {
diff --git a/client/test/til-client-test-utils.js b/client/test/til-client-test-utils.js
index 459d2d16..55e687ca 100644
--- a/client/test/til-client-test-utils.js
+++ b/client/test/til-client-test-utils.js
@@ -1,6 +1,6 @@
import React from 'react'
import {Router} from 'react-router-dom'
-import {render, wait, Simulate} from 'react-testing-library'
+import {render, wait} from 'react-testing-library'
import {createMemoryHistory} from 'history'
import 'jest-dom/extend-expect'
import * as generate from 'til-shared/generate'
@@ -17,4 +17,12 @@ function renderWithRouter(ui, {route = '/', ...renderOptions} = {}) {
}
}
-export {renderWithRouter, generate, render, wait, Simulate}
+export {
+ Simulate,
+ wait,
+ render,
+ cleanup,
+ renderIntoDocument,
+ fireEvent,
+} from 'react-testing-library'
+export {renderWithRouter, generate}
diff --git a/other/jest.config.js b/other/jest.config.js
index ed83dcaf..af465b49 100644
--- a/other/jest.config.js
+++ b/other/jest.config.js
@@ -11,5 +11,6 @@ module.exports = {
'./other/whats-a-mock',
'./other/configuration/calculator.solution',
'./other/jest-expect',
+ './other/simple-react',
],
}
diff --git a/other/simple-react/.babelrc b/other/simple-react/.babelrc
new file mode 100644
index 00000000..bed2455c
--- /dev/null
+++ b/other/simple-react/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": "../configuration/calculator.solution/.babelrc.js"
+}
diff --git a/other/simple-react/__tests__/item-list.js b/other/simple-react/__tests__/item-list.js
new file mode 100644
index 00000000..db24e6f9
--- /dev/null
+++ b/other/simple-react/__tests__/item-list.js
@@ -0,0 +1,17 @@
+import React from 'react'
+import ReactDOM from 'react-dom'
+import ItemList from '../item-list'
+
+test('renders "no items" when the item list is empty', () => {
+ const container = document.createElement('div')
+ ReactDOM.render(, container)
+ expect(container.textContent).toMatch('no items')
+})
+
+test('renders the items in a list', () => {
+ const container = document.createElement('div')
+ ReactDOM.render(, container)
+ expect(container.textContent).toMatch('apple')
+ expect(container.textContent).toMatch('orange')
+ expect(container.textContent).toMatch('pear')
+})
diff --git a/other/simple-react/__tests__/item-list.todo.js b/other/simple-react/__tests__/item-list.todo.js
new file mode 100644
index 00000000..e0e75a76
--- /dev/null
+++ b/other/simple-react/__tests__/item-list.todo.js
@@ -0,0 +1,42 @@
+// Your job:
+// Test the case where the items provided is empty:
+//
+// Test the case where there are items in the list:
+//
+//
+// Don't overthink it. This is just a practice run to warm you up
+// to testing react components.
+
+// So you can use JSX (which transpiles down to React.createElement):
+// import React from 'react'
+//
+// So you can render the component for testing:
+// import ReactDOM from 'react-dom'
+//
+// So you can create a react element for the component you're testing:
+// import ItemList from '../item-list'
+
+// and here's an outline example of your first test:
+// Create a "container" to render your component into (tip: use document.createElement('div'))
+//
+// Render your component (tip: use ReactDOM.render(JSX, container))
+//
+// Make your assertion(s) on the textContent of the container
+// (tip: expect's toMatch function might be what you want)
+//
+// For your second test, it will be very similar to the first.
+
+//////// Elaboration & Feedback /////////
+// When you've finished with the exercises:
+// 1. Copy the URL below into your browser and fill out the form
+// 2. remove the `.skip` from the test below
+// 3. Change submitted from `false` to `true`
+// 4. And you're all done!
+/*
+http://ws.kcd.im/?ws=Testing&e=basic%20react%20test&em=
+*/
+test.skip('I submitted my elaboration and feedback', () => {
+ const submitted = false // change this when you've submitted!
+ expect(submitted).toBe(true)
+})
+////////////////////////////////
diff --git a/other/simple-react/item-list.js b/other/simple-react/item-list.js
new file mode 100644
index 00000000..4482225b
--- /dev/null
+++ b/other/simple-react/item-list.js
@@ -0,0 +1,11 @@
+import React from 'react'
+
+function ItemList({items}) {
+ return items.length ? (
+
+ ) : (
+ 'no items'
+ )
+}
+
+export default ItemList
diff --git a/other/simple-react/jest.config.js b/other/simple-react/jest.config.js
new file mode 100644
index 00000000..0dd0e5cd
--- /dev/null
+++ b/other/simple-react/jest.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ displayName: 'react',
+}
diff --git a/package.json b/package.json
index fe969e42..69a4a93e 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"test:run": "jest --config ./other/jest.config.js --coverage",
"test:mock": "jest --config ./other/whats-a-mock/jest.config.js --watch",
"test:expect": "jest --config ./other/jest-expect/jest.config.js --watch",
+ "test:react": "jest --config ./other/simple-react/jest.config.js --watch",
"pretest:e2e:run": "npm run build --silent",
"test:e2e:run": "as-a E2E npm-run-all --parallel --race start:core cy:run",
"test:e2e": "as-a E2E npm-run-all --parallel --race dev:core cy:open",