From ed6fa54927f62a71fce21415fca62a04f94868f7 Mon Sep 17 00:00:00 2001 From: Illya Klymov Date: Sun, 3 Oct 2021 22:44:16 +0300 Subject: [PATCH] fix: restore chaining and CSS selectors for findComponent Allow findComponent / findAllComponents to be chained from DOM selector and allow CSS selectors to be used --- docs/api/wrapper/findAllComponents.md | 6 ++- docs/api/wrapper/findComponent.md | 30 +++++++++++++- packages/shared/util.js | 4 ++ packages/test-utils/src/wrapper.js | 49 +++++++++-------------- test/specs/wrapper/find.spec.js | 39 ++++++++++++------ test/specs/wrapper/findAll.spec.js | 57 +++++++++++++++++++++------ test/specs/wrapper/setValue.spec.js | 2 +- 7 files changed, 127 insertions(+), 60 deletions(-) diff --git a/docs/api/wrapper/findAllComponents.md b/docs/api/wrapper/findAllComponents.md index 6b3256e16..041f4ff68 100644 --- a/docs/api/wrapper/findAllComponents.md +++ b/docs/api/wrapper/findAllComponents.md @@ -4,7 +4,7 @@ Returns a [`WrapperArray`](../wrapper-array/) of all matching Vue components. - **Arguments:** - - `{Component|ref|name} selector` + - `selector` Use any valid [selector](../selectors.md) - **Returns:** `{WrapperArray}` @@ -21,3 +21,7 @@ expect(bar.exists()).toBeTruthy() const bars = wrapper.findAllComponents(Bar) expect(bars).toHaveLength(1) ``` + +::: warning Usage with CSS selectors +Using `findAllComponents` with CSS selector is subject to same limitations as [findComponent](api/wrapper/findComponent.md) +::: diff --git a/docs/api/wrapper/findComponent.md b/docs/api/wrapper/findComponent.md index cfe4a4fe8..febbb41e3 100644 --- a/docs/api/wrapper/findComponent.md +++ b/docs/api/wrapper/findComponent.md @@ -4,7 +4,7 @@ Returns `Wrapper` of first matching Vue component. - **Arguments:** - - `{Component|ref|name} selector` + - `{Component|ref|string} selector` - **Returns:** `{Wrapper}` @@ -24,3 +24,31 @@ expect(barByName.exists()).toBe(true) const barRef = wrapper.findComponent({ ref: 'bar' }) // => finds Bar by `ref` expect(barRef.exists()).toBe(true) ``` + +::: warning Usage with CSS selectors +Using `findAllComponents` with CSS selector might have confusing behavior + +Consider this example: + +```js +const ChildComponent = { + name: 'Child', + template: '
' +} + +const RootComponent = { + name: 'Root', + components: { ChildComponent }, + template: '' +} + +const wrapper = mount(RootComponent) + +const rootByCss = wrapper.findComponent('.root') // => finds Root +expect(rootByCss.vm.$options.name).toBe('Root') +const childByCss = wrapper.findComponent('.child') +expect(childByCss.vm.$options.name).toBe('Root') // => still Root +``` + +The reason for such behavior is that `RootComponent` and `ChildComponent` are sharing same DOM node and only first matching component is included for each unique DOM node +::: diff --git a/packages/shared/util.js b/packages/shared/util.js index 679315f63..19ede4cf6 100644 --- a/packages/shared/util.js +++ b/packages/shared/util.js @@ -111,3 +111,7 @@ export function warnDeprecated(method: string, fallback: string = '') { warn(msg) } } + +export function isVueWrapper(wrapper: Object) { + return wrapper.vm || wrapper.isFunctionalComponent +} diff --git a/packages/test-utils/src/wrapper.js b/packages/test-utils/src/wrapper.js index 82f98cce4..7d5e2c724 100644 --- a/packages/test-utils/src/wrapper.js +++ b/packages/test-utils/src/wrapper.js @@ -16,7 +16,8 @@ import { isPhantomJS, nextTick, warn, - warnDeprecated + warnDeprecated, + isVueWrapper } from 'shared/util' import { isElementVisible } from 'shared/is-visible' import find from './find' @@ -275,17 +276,6 @@ export default class Wrapper implements BaseWrapper { this.__warnIfDestroyed() const selector = getSelector(rawSelector, 'findComponent') - if (!this.vm && !this.isFunctionalComponent) { - throwError( - 'You cannot chain findComponent off a DOM element. It can only be used on Vue Components.' - ) - } - - if (selector.type === DOM_SELECTOR) { - throwError( - 'findComponent requires a Vue constructor or valid find object. If you are searching for DOM nodes, use `find` instead' - ) - } return this.__find(rawSelector, selector) } @@ -327,28 +317,25 @@ export default class Wrapper implements BaseWrapper { this.__warnIfDestroyed() const selector = getSelector(rawSelector, 'findAll') - if (!this.vm) { - throwError( - 'You cannot chain findAllComponents off a DOM element. It can only be used on Vue Components.' - ) - } - if (selector.type === DOM_SELECTOR) { - throwError( - 'findAllComponents requires a Vue constructor or valid find object. If you are searching for DOM nodes, use `find` instead' - ) - } - return this.__findAll(rawSelector, selector) + + return this.__findAll(rawSelector, selector, isVueWrapper) } - __findAll(rawSelector: Selector, selector: Object): WrapperArray { + __findAll( + rawSelector: Selector, + selector: Object, + filterFn: Function = () => true + ): WrapperArray { const nodes = find(this.rootNode, this.vm, selector) - const wrappers = nodes.map(node => { - // Using CSS Selector, returns a VueWrapper instance if the root element - // binds a Vue instance. - const wrapper = createWrapper(node, this.options) - wrapper.selector = rawSelector - return wrapper - }) + const wrappers = nodes + .map(node => { + // Using CSS Selector, returns a VueWrapper instance if the root element + // binds a Vue instance. + const wrapper = createWrapper(node, this.options) + wrapper.selector = rawSelector + return wrapper + }) + .filter(filterFn) const wrapperArray = new WrapperArray(wrappers) wrapperArray.selector = rawSelector diff --git a/test/specs/wrapper/find.spec.js b/test/specs/wrapper/find.spec.js index 2e53577ee..861551317 100644 --- a/test/specs/wrapper/find.spec.js +++ b/test/specs/wrapper/find.spec.js @@ -194,20 +194,33 @@ describeWithShallowAndMount('find', mountingMethod => { expect(wrapper.findComponent(Component).vnode).toBeTruthy() }) - it('throws an error if findComponent selector is a CSS selector', () => { - const wrapper = mountingMethod(Component) - const message = - '[vue-test-utils]: findComponent requires a Vue constructor or valid find object. If you are searching for DOM nodes, use `find` instead' - const fn = () => wrapper.findComponent('#foo') - expect(fn).toThrow(message) - }) + it('findComponent returns top-level component when multiple components are matching', () => { + const DeepNestedChild = { + name: 'DeepNestedChild', + template: '
I am deeply nested
' + } + const NestedChild = { + name: 'NestedChild', + components: { DeepNestedChild }, + template: '' + } + const RootComponent = { + name: 'RootComponent', + components: { NestedChild }, + template: '
' + } - it('throws an error if findComponent is chained off a DOM element', () => { - const wrapper = mountingMethod(ComponentWithChild) - const message = - '[vue-test-utils]: You cannot chain findComponent off a DOM element. It can only be used on Vue Components.' - const fn = () => wrapper.find('span').findComponent('#foo') - expect(fn).toThrow(message) + const wrapper = mountingMethod(RootComponent, { stubs: { NestedChild } }) + + expect(wrapper.findComponent('.in-root').vm.$options.name).toEqual( + 'NestedChild' + ) + + // someone might expect DeepNestedChild here, but + // we always return TOP component matching DOM element + expect(wrapper.findComponent('.in-child').vm.$options.name).toEqual( + 'NestedChild' + ) }) it('allows using findComponent on functional component', () => { diff --git a/test/specs/wrapper/findAll.spec.js b/test/specs/wrapper/findAll.spec.js index b74c095d7..94aa783d7 100644 --- a/test/specs/wrapper/findAll.spec.js +++ b/test/specs/wrapper/findAll.spec.js @@ -149,20 +149,51 @@ describeWithShallowAndMount('findAll', mountingMethod => { expect(componentArr.length).toEqual(1) }) - it('throws an error if findAllComponents selector is a CSS selector', () => { - const wrapper = mountingMethod(Component) - const message = - '[vue-test-utils]: findAllComponents requires a Vue constructor or valid find object. If you are searching for DOM nodes, use `find` instead' - const fn = () => wrapper.findAllComponents('#foo') - expect(fn).toThrow(message) - }) + it('findAllComponents ignores DOM nodes matching same CSS selector', () => { + const RootComponent = { + components: { Component }, + template: '
' + } + const wrapper = mountingMethod(RootComponent) + expect(wrapper.findAllComponents('.foo')).toHaveLength(1) + expect( + wrapper + .findAllComponents('.foo') + .at(0) + .is(Component) + ).toBe(true) + }) + + it('findAllComponents returns top-level components when components are nested', () => { + const DeepNestedChild = { + name: 'DeepNestedChild', + template: '
I am deeply nested
' + } + const NestedChild = { + name: 'NestedChild', + components: { DeepNestedChild }, + template: '' + } + const RootComponent = { + name: 'RootComponent', + components: { NestedChild }, + template: '
' + } - it('throws an error if chaining findAllComponents off a DOM element', () => { - const wrapper = mountingMethod(ComponentWithChild) - const message = - '[vue-test-utils]: You cannot chain findAllComponents off a DOM element. It can only be used on Vue Components.' - const fn = () => wrapper.find('span').findAllComponents('#foo') - expect(fn).toThrow(message) + const wrapper = mountingMethod(RootComponent, { stubs: { NestedChild } }) + + expect(wrapper.findAllComponents('.in-root')).toHaveLength(1) + expect( + wrapper.findAllComponents('.in-root').at(0).vm.$options.name + ).toEqual('NestedChild') + + expect(wrapper.findAllComponents('.in-child')).toHaveLength(1) + + // someone might expect DeepNestedChild here, but + // we always return TOP component matching DOM element + expect( + wrapper.findAllComponents('.in-child').at(0).vm.$options.name + ).toEqual('NestedChild') }) it('returns correct number of Vue Wrapper when component has a v-for', () => { diff --git a/test/specs/wrapper/setValue.spec.js b/test/specs/wrapper/setValue.spec.js index dc5d9efdd..a7bb86c01 100644 --- a/test/specs/wrapper/setValue.spec.js +++ b/test/specs/wrapper/setValue.spec.js @@ -66,7 +66,7 @@ describeWithShallowAndMount('setValue', mountingMethod => { }) if (process.env.TEST_ENV !== 'browser') { - it.only('sets element of multiselect value', async () => { + it('sets element of multiselect value', async () => { const wrapper = mountingMethod(ComponentWithInput) const select = wrapper.find('select.multiselect') await select.setValue(['selectA', 'selectC'])