From aa7b76d5996a24bfaca74989907c0982fdeaa013 Mon Sep 17 00:00:00 2001 From: Dobromir Hristov Date: Mon, 27 Apr 2020 13:29:16 +0300 Subject: [PATCH] feat: return nextTick from setters, fix #1515 (#1517) * feat: return promise from set methods * fix: fix error types for error wrapper * refactor: change promise assertion method * docs: mention that some methods can be awaited Generally improve async docs * fix: fix TS types * fix: fix nextTick for vue < 2.1 * docs: fix eslint error * chore: revert dist * chore: revert committed dist files Co-authored-by: Lachlan Miller --- docs/api/wrapper/trigger.md | 9 ++- docs/guides/common-tips.md | 34 ++++-------- docs/guides/getting-started.md | 29 ++++++---- docs/guides/testing-async-components.md | 50 ++++++++++++----- flow/wrapper.flow.js | 12 ++-- packages/shared/util.js | 13 +++++ packages/test-utils/src/wrapper.js | 67 ++++++++++++----------- packages/test-utils/types/index.d.ts | 12 ++-- test/resources/utils.js | 3 + test/specs/wrapper/setChecked.spec.js | 64 ++++++++++------------ test/specs/wrapper/setData.spec.js | 73 +++++++++++++------------ test/specs/wrapper/setProps.spec.js | 55 +++++++++++-------- test/specs/wrapper/setSelected.spec.js | 16 ++---- test/specs/wrapper/setValue.spec.js | 21 ++++--- test/specs/wrapper/trigger.spec.js | 16 +++++- 15 files changed, 265 insertions(+), 209 deletions(-) diff --git a/docs/api/wrapper/trigger.md b/docs/api/wrapper/trigger.md index 069ea67de..eed00ed1e 100644 --- a/docs/api/wrapper/trigger.md +++ b/docs/api/wrapper/trigger.md @@ -3,6 +3,7 @@ Triggers an event asynchronously on the `Wrapper` DOM node. `trigger` takes an optional `options` object. The properties in the `options` object are added to the Event. +`trigger` returns a Promise, which when resolved, guarantees the component is updated. - **Arguments:** @@ -22,18 +23,16 @@ test('trigger demo', async () => { propsData: { clickHandler } }) - wrapper.trigger('click') + await wrapper.trigger('click') - wrapper.trigger('click', { + await wrapper.trigger('click', { button: 0 }) - wrapper.trigger('click', { + await wrapper.trigger('click', { ctrlKey: true // For testing @click.ctrl handlers }) - await wrapper.vm.$nextTick() // Wait until trigger events have been handled - expect(clickHandler.called).toBe(true) }) ``` diff --git a/docs/guides/common-tips.md b/docs/guides/common-tips.md index 17b7c67b2..c95bf183d 100644 --- a/docs/guides/common-tips.md +++ b/docs/guides/common-tips.md @@ -35,28 +35,27 @@ When using either the `mount` or `shallowMount` methods, you can expect your com Additionally, the component will not be automatically destroyed at the end of each spec, and it is up to the user to stub or manually clean up tasks that will continue to run (`setInterval` or `setTimeout`, for example) before the end of each spec. -### Writing asynchronous tests using `nextTick` (new) +### Writing asynchronous tests (new) By default, Vue batches updates to run asynchronously (on the next "tick"). This is to prevent unnecessary DOM re-renders, and watcher computations ([see the docs](https://vuejs.org/v2/guide/reactivity.html#Async-Update-Queue) for more details). -This means you **must** wait for updates to run after you set a reactive property. You can wait for updates with `Vue.nextTick()`: +This means that you **must** wait for updates to run after you change a reactive property. You can do that by awaiting mutation methods like `trigger`: ```js it('updates text', async () => { const wrapper = mount(Component) - wrapper.trigger('click') - await Vue.nextTick() + await wrapper.trigger('click') expect(wrapper.text()).toContain('updated') + await wrapper.trigger('click') + wrapper.text().toContain('some different text') }) // Or if you're without async/await it('render text', done => { const wrapper = mount(TestComponent) - wrapper.trigger('click') - Vue.nextTick(() => { - wrapper.text().toContain('some text') - wrapper.trigger('click') - Vue.nextTick(() => { + wrapper.trigger('click').then(() => { + wrapper.text().toContain('updated') + wrapper.trigger('click').then(() => { wrapper.text().toContain('some different text') done() }) @@ -64,14 +63,7 @@ it('render text', done => { }) ``` -The following methods often cause watcher updates that require you to wait for the next tick: - -- `setChecked` -- `setData` -- `setSelected` -- `setProps` -- `setValue` -- `trigger` +Learn more in the [Testing Asynchronous Behavior](../guides/README.md#testing-asynchronous-behavior) ### Asserting Emitted Events @@ -213,16 +205,15 @@ test('should render Foo, then hide it', async () => { const wrapper = mount(Foo) expect(wrapper.text()).toMatch(/Foo/) - wrapper.setData({ + await wrapper.setData({ show: false }) - await wrapper.vm.$nextTick() expect(wrapper.text()).not.toMatch(/Foo/) }) ``` -In practice, although we are calling `setData` then waiting for the `nextTick` to ensure the DOM is updated, this test fails. This is an ongoing issue related to how Vue implements the `` component, that we would like to solve before version 1.0. For now, there are some workarounds: +In practice, although we are calling and awaiting `setData` to ensure the DOM is updated, this test fails. This is an ongoing issue related to how Vue implements the `` component, that we would like to solve before version 1.0. For now, there are some workarounds: #### Using a `transitionStub` helper @@ -241,10 +232,9 @@ test('should render Foo, then hide it', async () => { }) expect(wrapper.text()).toMatch(/Foo/) - wrapper.setData({ + await wrapper.setData({ show: false }) - await wrapper.vm.$nextTick() expect(wrapper.text()).not.toMatch(/Foo/) }) diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md index 2c6298039..96d37dfea 100644 --- a/docs/guides/getting-started.md +++ b/docs/guides/getting-started.md @@ -122,30 +122,34 @@ it('button click should increment the count', () => { In order to test that the counter text has updated, we need to learn about `nextTick`. -### Using `nextTick` +### Using `nextTick` and awaiting actions -Anytime you make a change (in computed, data, vuex state, etc) which updates the DOM (ex. show a component from v-if), you should await the `nextTick` function before running the test. This is because Vue batches pending DOM updates and _applies them asynchronously_ to prevent unnecessary re-renders caused by multiple data mutations. +Anytime you make a change (in computed, data, vuex state, etc) which updates the DOM (ex. show a component from v-if or display dynamic text), you should await the `nextTick` function before running the assertion. +This is because Vue batches pending DOM updates and _applies them asynchronously_ to prevent unnecessary re-renders caused by multiple data mutations. _You can read more about asynchronous updates in the [Vue docs](https://vuejs.org/v2/guide/reactivity.html#Async-Update-Queue)_ -We need to use `wrapper.vm.$nextTick` to wait until Vue has performed the DOM update after we set a reactive property. In the counter example, setting the `count` property schedules a DOM update to run on the next tick. +After updating a reactive property we can await methods like `trigger` or `wrapper.vm.$nextTick` directly, until Vue has performed the DOM update. In the counter example, setting the `count` property schedules a DOM update to run on the next tick. -We can `await` `wrapper.vm.$nextTick()` by writing the tests in an async function: +Lets see how we can `await trigger()` by writing the tests in an async function: ```js it('button click should increment the count text', async () => { expect(wrapper.text()).toContain('0') const button = wrapper.find('button') - button.trigger('click') - await wrapper.vm.$nextTick() + await button.trigger('click') expect(wrapper.text()).toContain('1') }) ``` -When you use `nextTick` in your test files, be aware that any errors thrown inside it may not be caught by your test runner as it uses promises internally. There are two approaches to fixing this: either you can set the `done` callback as Vue's global error handler at the start of the test, or you can call `nextTick` without an argument and return it as a promise: +`trigger` returns a promise, which can be awaited as seen above or chained with `then` like a regular promise callback. Methods like `trigger` just return `Vue.nextTick` internally. +You can read more in depth about [Testing Asynchronous Components](../guides/README.md#testing-async-components). + +If for some reason you choose to use `nextTick` instead in your test files, be aware that any errors thrown inside it may not be caught by your test runner as it uses promises internally. There are two approaches to fixing this: +either you can set the `done` callback as Vue's global error handler at the start of the test, or you can call `nextTick` without an argument and return it as a promise: ```js -// this will not be caught +// errors will not be caught it('will time out', done => { Vue.nextTick(() => { expect(true).toBe(false) @@ -174,7 +178,12 @@ it('will catch the error using async/await', async () => { }) ``` +`Vue.nextTick` is equal to `component.vm.$nextTick`, where `component` can be the result of `mount` or `find`. + +As mentioned in the beginning, in most cases, awaiting `trigger` is the recommended way to go. + ### What's Next -- Integrate Vue Test Utils into your project by [choosing a test runner](./choosing-a-test-runner.md). -- Learn more about [common techniques when writing tests](./common-tips.md). +- Learn more about [common techniques when writing tests](./README.md#knowing-what-to-test). +- Integrate Vue Test Utils into your project by [choosing a test runner](./README.md#choosing-a-test-runner). +- Learn more about [Testing Asynchronous Behavior](./README.md#testing-asynchronous-behavior) diff --git a/docs/guides/testing-async-components.md b/docs/guides/testing-async-components.md index 56f46e23e..daaf47b74 100644 --- a/docs/guides/testing-async-components.md +++ b/docs/guides/testing-async-components.md @@ -11,17 +11,22 @@ Vue batches pending DOM updates and applies them asynchronously to prevent unnec _You can read more about asynchronous updates in the [Vue docs](https://vuejs.org/v2/guide/reactivity.html#Async-Update-Queue)_ -In practice, this means you have to use `Vue.nextTick()` to wait until Vue has performed updates after you set a reactive property. - -The easiest way to use `Vue.nextTick()` is to write your tests in an async function: +In practice, this means that after mutating a reactive property, to assert that change your test has to wait while Vue is performing updates. +One way is to use `await Vue.nextTick()`, but an easier and cleaner way is to just `await` the method that you mutated the state with, like `trigger`. ```js -// import Vue at the top of file -import Vue from 'vue' +// inside test-suite, add this test case +it('button click should increment the count text', async () => { + expect(wrapper.text()).toContain('0') + const button = wrapper.find('button') + await button.trigger('click') + expect(wrapper.text()).toContain('1') +}) +``` -// other code snippet... +Awaiting the trigger above is the same as doing: -// inside test-suite, add this test case +```js it('button click should increment the count text', async () => { expect(wrapper.text()).toContain('0') const button = wrapper.find('button') @@ -31,6 +36,15 @@ it('button click should increment the count text', async () => { }) ``` +Methods that can be awaited are: + +- [setData](../api/wrapper/README.md#setdata) +- [setValue](../api/wrapper/README.md#setvalue) +- [setChecked](../api/wrapper/README.md#setchecked) +- [setSelected](../api/wrapper/README.md#setselected) +- [setProps](../api/wrapper/README.md#setprops) +- [trigger](../api/wrapper/README.md#trigger) + ## Asynchronous behavior outside of Vue One of the most common asynchronous behaviors outside of Vue is API calls in Vuex actions. The following examples shows how to test a method that makes an API call. This example uses Jest to run the test and to mock the HTTP library `axios`. More about Jest manual mocks can be found [here](https://jestjs.io/docs/en/manual-mocks.html#content). @@ -47,7 +61,7 @@ The below component makes an API call when a button is clicked, then assigns the ```html