diff --git a/package-lock.json b/package-lock.json index 2c6f66cb..0e3aa9e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1491,12 +1491,13 @@ "integrity": "sha512-vTCdPp/T/Q3oSqwHmZ5Kpa9oI7iLtGl3RQaA/NyLHikvcrPxACkkKVr/XzkSPJWXHRhKGzVvb0urJsbMlRxi1Q==" }, "@testing-library/dom": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-5.0.1.tgz", - "integrity": "sha512-HmyN4b/PmSaSB1ku0tWjgnTtyrwNBXEpp44wgfNaDhyj6IJTCWp1GAf4AANoLGItgMsYjepwWOdMyuJ/8iyStQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-5.2.0.tgz", + "integrity": "sha512-nFaZes/bzDfMqwZpQXdiPyj3WXU16FYf5k5NCFu/qJM4JdRJLHEtSRYtrETmk7nCf+qLVoHCqRduGi/4KE83Gw==", "requires": { "@babel/runtime": "^7.4.5", "@sheerun/mutationobserver-shim": "^0.3.2", + "aria-query": "3.0.0", "pretty-format": "^24.8.0", "wait-for-expect": "^1.2.0" } @@ -1721,6 +1722,15 @@ "sprintf-js": "~1.0.2" } }, + "aria-query": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", + "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=", + "requires": { + "ast-types-flow": "0.0.7", + "commander": "^2.11.0" + } + }, "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -1803,6 +1813,11 @@ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", "dev": true }, + "ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=" + }, "astral-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", @@ -2541,8 +2556,7 @@ "commander": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", - "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", - "dev": true + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==" }, "component-emitter": { "version": "1.3.0", diff --git a/package.json b/package.json index 982c50f2..e69913f4 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "author": "Daniel Cook", "license": "MIT", "dependencies": { - "@testing-library/dom": "^5.0.1", + "@testing-library/dom": "^5.2.0", "@vue/test-utils": "^1.0.0-beta.29", "vue": "^2.6.10", "vue-template-compiler": "^2.6.10" diff --git a/tests/__tests__/__snapshots__/axios-mock.js.snap b/tests/__tests__/__snapshots__/axios-mock.js.snap new file mode 100644 index 00000000..8083a688 --- /dev/null +++ b/tests/__tests__/__snapshots__/axios-mock.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`makes an API call and displays the greeting when load-greeting is clicked 1`] = ` +
+ hello there +
+`; diff --git a/tests/__tests__/__snapshots__/fetch.js.snap b/tests/__tests__/__snapshots__/fetch.js.snap deleted file mode 100644 index b2894825..00000000 --- a/tests/__tests__/__snapshots__/fetch.js.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Fetch makes an API call and displays the greeting when load-greeting is clicked 1`] = ` -
- hello there -
-`; diff --git a/tests/__tests__/axios-mock.js b/tests/__tests__/axios-mock.js new file mode 100644 index 00000000..dd62a910 --- /dev/null +++ b/tests/__tests__/axios-mock.js @@ -0,0 +1,27 @@ +import axiosMock from 'axios' +import { render, fireEvent } from '@testing-library/vue' +import Component from './components/Fetch.vue' +import 'jest-dom/extend-expect' + +test('makes an API call and displays the greeting when load-greeting is clicked', async () => { + axiosMock.get.mockImplementationOnce(() => + Promise.resolve({ + data: { greeting: 'hello there' } + }) + ) + + const { html, getByText } = render(Component, { props: { url: '/greeting' } }) + + // Act + await fireEvent.click(getByText('Fetch')) + + expect(axiosMock.get).toHaveBeenCalledTimes(1) + expect(axiosMock.get).toHaveBeenCalledWith('/greeting') + getByText('hello there') + + // You can render component snapshots by using html(). However, bear in mind + // that Snapshot Testing should not be treated as a replacement for regular + // tests. + // More about the topic: https://twitter.com/searls/status/919594505938112512 + expect(html()).toMatchSnapshot() +}) diff --git a/tests/__tests__/components/Button.vue b/tests/__tests__/components/Button.vue index 7b9ea7ee..cfbf37f1 100644 --- a/tests/__tests__/components/Button.vue +++ b/tests/__tests__/components/Button.vue @@ -1,5 +1,5 @@ diff --git a/tests/__tests__/router/programmatic-routing/components/LocationDisplay.vue b/tests/__tests__/router/programmatic-routing/components/LocationDisplay.vue deleted file mode 100644 index eecf2959..00000000 --- a/tests/__tests__/router/programmatic-routing/components/LocationDisplay.vue +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/tests/__tests__/router/programmatic-routing/components/NoMatch.vue b/tests/__tests__/router/programmatic-routing/components/NoMatch.vue deleted file mode 100644 index 8118731c..00000000 --- a/tests/__tests__/router/programmatic-routing/components/NoMatch.vue +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/tests/__tests__/router/programmatic-routing/index.js b/tests/__tests__/router/programmatic-routing/index.js deleted file mode 100644 index 7396f3ff..00000000 --- a/tests/__tests__/router/programmatic-routing/index.js +++ /dev/null @@ -1,22 +0,0 @@ -import 'jest-dom/extend-expect' - -import App from './components/App.vue' -import Home from './components/Home.vue' -import About from './components/About.vue' - -import { render, fireEvent } from '@testing-library/vue' - -const routes = [ - { path: '/', component: Home }, - { path: '/about', component: About }, - { path: '*', redirect: '/about' } -] - -test('navigating programmatically', async () => { - const { queryByTestId } = render(App, { routes }) - - expect(queryByTestId('location-display')).toHaveTextContent('/') - await fireEvent.click(queryByTestId('go-to-about')) - - expect(queryByTestId('location-display')).toHaveTextContent('/about') -}) diff --git a/tests/__tests__/simple-button.js b/tests/__tests__/simple-button.js index 7112dbaf..27da8ac2 100644 --- a/tests/__tests__/simple-button.js +++ b/tests/__tests__/simple-button.js @@ -1,22 +1,29 @@ import { render, cleanup, fireEvent } from '@testing-library/vue' -import SimpleButton from './components/Button' +import Button from './components/Button' +import 'jest-dom/extend-expect' afterEach(cleanup) test('renders button with text', () => { - const buttonText = "Click me; I'm sick" - const { getByText } = render(SimpleButton, { - props: { text: buttonText } + const text = "Click me; I'm sick" + + // Set the prop value by using the second argument of `render()` + const { getByRole } = render(Button, { + props: { text } }) - getByText(buttonText) + expect(getByRole('button')).toHaveTextContent(text) }) -test('click event is emitted when button is clicked', () => { +test('click event is emitted when button is clicked', async () => { const text = 'Click me' - const { getByText, emitted } = render(SimpleButton, { + + const { getByRole, emitted } = render(Button, { props: { text } }) - fireEvent.click(getByText(text)) + + // Send a click event to the element with a 'button' role + await fireEvent.click(getByRole('button')) + expect(emitted().click).toHaveLength(1) }) diff --git a/tests/__tests__/stopwatch.js b/tests/__tests__/stopwatch.js index 363f860d..505f4ce8 100644 --- a/tests/__tests__/stopwatch.js +++ b/tests/__tests__/stopwatch.js @@ -10,10 +10,13 @@ test('unmounts a component', async () => { const { unmount, isUnmounted, getByText } = render(StopWatch) await fireEvent.click(getByText('Start')) + // Destroys a Vue component instance. unmount() + expect(isUnmounted()).toBe(true) await wait() + expect(console.error).not.toHaveBeenCalled() }) @@ -23,11 +26,21 @@ test('updates component state', async () => { const startButton = getByText('Start') const elapsedTime = getByTestId('elapsed') + // Assert initial state. expect(elapsedTime).toHaveTextContent('0ms') + getByText('Start') await fireEvent.click(startButton) + + getByText('Stop') + + // Wait for one tick of the event loop. await wait() + + // Stop the timer. await fireEvent.click(startButton) + // We can't assert a specific amount of time. Instead, we assert that the + // content has changed. expect(elapsedTime).not.toHaveTextContent('0ms') }) diff --git a/tests/__tests__/update-props.js b/tests/__tests__/update-props.js new file mode 100644 index 00000000..a732ce89 --- /dev/null +++ b/tests/__tests__/update-props.js @@ -0,0 +1,19 @@ +import NumberDisplay from './components/NumberDisplay.vue' +import { render } from '@testing-library/vue' +import 'jest-dom/extend-expect' + +test('calling render with the same component but different props does not remount', async () => { + const { getByTestId, updateProps } = render(NumberDisplay, { + props: { number: 1 } + }) + + expect(getByTestId('number-display')).toHaveTextContent('1') + + await updateProps({ number: 2 }) + + expect(getByTestId('number-display')).toHaveTextContent('2') + + // Assert that, even after updating props, the component hasn't remounted, + // meaning we are testing the same component instance we rendered initially. + expect(getByTestId('instance-id')).toHaveTextContent('1') +}) diff --git a/tests/__tests__/validate-plugin.js b/tests/__tests__/validate-plugin.js index 9b933a94..d562f20a 100644 --- a/tests/__tests__/validate-plugin.js +++ b/tests/__tests__/validate-plugin.js @@ -5,14 +5,24 @@ import { render, fireEvent } from '@testing-library/vue' import Validate from './components/Validate' test('can validate using plugin', async () => { - const { getByPlaceholderText, queryByTestId } = render(Validate, {}, vue => - vue.use(VeeValidate, { events: 'blur' }) + // The third argument of `render` is a callback function that receives the + // Vue instance as a parameter. This way, we can register plugins such as + // VeeValidate. + const { getByPlaceholderText, queryByTestId, getByTestId } = render( + Validate, + {}, + vue => vue.use(VeeValidate, { events: 'blur' }) ) + // Assert error messages are not in the DOM when rendering the component. + expect(queryByTestId('username-errors')).toBeNull() + const usernameInput = getByPlaceholderText('Username...') await fireEvent.touch(usernameInput) - expect(queryByTestId('username-errors')).toHaveTextContent( + // After "touching" the input (focusing and blurring), validation error + // should appear. + expect(getByTestId('username-errors')).toHaveTextContent( 'The username field is required.' ) }) diff --git a/tests/__tests__/vue-router.js b/tests/__tests__/vue-router.js index 76fd0dee..e4509796 100644 --- a/tests/__tests__/vue-router.js +++ b/tests/__tests__/vue-router.js @@ -15,16 +15,20 @@ const routes = [ afterEach(cleanup) test('full app rendering/navigating', async () => { + // Notice how we pass a `routes` object to our render function. const { queryByTestId } = render(App, { routes }) - // normally I'd use a data-testid, but just wanted to show this is also possible expect(queryByTestId('location-display')).toHaveTextContent('/') + await fireEvent.click(queryByTestId('about-link')) expect(queryByTestId('location-display')).toHaveTextContent('/about') }) test('setting initial route', () => { + // The callback function receives three parameters: the Vue instance where + // the component is mounted, the store instance (if any) and the router + // object. const { queryByTestId } = render(App, { routes }, (vue, store, router) => { router.push('/about') })