Skip to content

Commit a351831

Browse files
authored
fix: restore chaining and CSS selectors for findComponent (#1910)
Allow findComponent / findAllComponents to be chained from DOM selector and allow CSS selectors to be used
1 parent 69b4c34 commit a351831

File tree

7 files changed

+127
-60
lines changed

7 files changed

+127
-60
lines changed

docs/api/wrapper/findAllComponents.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Returns a [`WrapperArray`](../wrapper-array/) of all matching Vue components.
44

55
- **Arguments:**
66

7-
- `{Component|ref|name} selector`
7+
- `selector` Use any valid [selector](../selectors.md)
88

99
- **Returns:** `{WrapperArray}`
1010

@@ -21,3 +21,7 @@ expect(bar.exists()).toBeTruthy()
2121
const bars = wrapper.findAllComponents(Bar)
2222
expect(bars).toHaveLength(1)
2323
```
24+
25+
::: warning Usage with CSS selectors
26+
Using `findAllComponents` with CSS selector is subject to same limitations as [findComponent](api/wrapper/findComponent.md)
27+
:::

docs/api/wrapper/findComponent.md

+29-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Returns `Wrapper` of first matching Vue component.
44

55
- **Arguments:**
66

7-
- `{Component|ref|name} selector`
7+
- `{Component|ref|string} selector`
88

99
- **Returns:** `{Wrapper}`
1010

@@ -24,3 +24,31 @@ expect(barByName.exists()).toBe(true)
2424
const barRef = wrapper.findComponent({ ref: 'bar' }) // => finds Bar by `ref`
2525
expect(barRef.exists()).toBe(true)
2626
```
27+
28+
::: warning Usage with CSS selectors
29+
Using `findAllComponents` with CSS selector might have confusing behavior
30+
31+
Consider this example:
32+
33+
```js
34+
const ChildComponent = {
35+
name: 'Child',
36+
template: '<div class="child"></div>'
37+
}
38+
39+
const RootComponent = {
40+
name: 'Root',
41+
components: { ChildComponent },
42+
template: '<child-component class="root" />'
43+
}
44+
45+
const wrapper = mount(RootComponent)
46+
47+
const rootByCss = wrapper.findComponent('.root') // => finds Root
48+
expect(rootByCss.vm.$options.name).toBe('Root')
49+
const childByCss = wrapper.findComponent('.child')
50+
expect(childByCss.vm.$options.name).toBe('Root') // => still Root
51+
```
52+
53+
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
54+
:::

packages/shared/util.js

+4
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,7 @@ export function warnDeprecated(method: string, fallback: string = '') {
111111
warn(msg)
112112
}
113113
}
114+
115+
export function isVueWrapper(wrapper: Object) {
116+
return wrapper.vm || wrapper.isFunctionalComponent
117+
}

packages/test-utils/src/wrapper.js

+18-31
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import {
1616
isPhantomJS,
1717
nextTick,
1818
warn,
19-
warnDeprecated
19+
warnDeprecated,
20+
isVueWrapper
2021
} from 'shared/util'
2122
import { isElementVisible } from 'shared/is-visible'
2223
import find from './find'
@@ -275,17 +276,6 @@ export default class Wrapper implements BaseWrapper {
275276
this.__warnIfDestroyed()
276277

277278
const selector = getSelector(rawSelector, 'findComponent')
278-
if (!this.vm && !this.isFunctionalComponent) {
279-
throwError(
280-
'You cannot chain findComponent off a DOM element. It can only be used on Vue Components.'
281-
)
282-
}
283-
284-
if (selector.type === DOM_SELECTOR) {
285-
throwError(
286-
'findComponent requires a Vue constructor or valid find object. If you are searching for DOM nodes, use `find` instead'
287-
)
288-
}
289279

290280
return this.__find(rawSelector, selector)
291281
}
@@ -327,28 +317,25 @@ export default class Wrapper implements BaseWrapper {
327317
this.__warnIfDestroyed()
328318

329319
const selector = getSelector(rawSelector, 'findAll')
330-
if (!this.vm) {
331-
throwError(
332-
'You cannot chain findAllComponents off a DOM element. It can only be used on Vue Components.'
333-
)
334-
}
335-
if (selector.type === DOM_SELECTOR) {
336-
throwError(
337-
'findAllComponents requires a Vue constructor or valid find object. If you are searching for DOM nodes, use `find` instead'
338-
)
339-
}
340-
return this.__findAll(rawSelector, selector)
320+
321+
return this.__findAll(rawSelector, selector, isVueWrapper)
341322
}
342323

343-
__findAll(rawSelector: Selector, selector: Object): WrapperArray {
324+
__findAll(
325+
rawSelector: Selector,
326+
selector: Object,
327+
filterFn: Function = () => true
328+
): WrapperArray {
344329
const nodes = find(this.rootNode, this.vm, selector)
345-
const wrappers = nodes.map(node => {
346-
// Using CSS Selector, returns a VueWrapper instance if the root element
347-
// binds a Vue instance.
348-
const wrapper = createWrapper(node, this.options)
349-
wrapper.selector = rawSelector
350-
return wrapper
351-
})
330+
const wrappers = nodes
331+
.map(node => {
332+
// Using CSS Selector, returns a VueWrapper instance if the root element
333+
// binds a Vue instance.
334+
const wrapper = createWrapper(node, this.options)
335+
wrapper.selector = rawSelector
336+
return wrapper
337+
})
338+
.filter(filterFn)
352339

353340
const wrapperArray = new WrapperArray(wrappers)
354341
wrapperArray.selector = rawSelector

test/specs/wrapper/find.spec.js

+26-13
Original file line numberDiff line numberDiff line change
@@ -194,20 +194,33 @@ describeWithShallowAndMount('find', mountingMethod => {
194194
expect(wrapper.findComponent(Component).vnode).toBeTruthy()
195195
})
196196

197-
it('throws an error if findComponent selector is a CSS selector', () => {
198-
const wrapper = mountingMethod(Component)
199-
const message =
200-
'[vue-test-utils]: findComponent requires a Vue constructor or valid find object. If you are searching for DOM nodes, use `find` instead'
201-
const fn = () => wrapper.findComponent('#foo')
202-
expect(fn).toThrow(message)
203-
})
197+
it('findComponent returns top-level component when multiple components are matching', () => {
198+
const DeepNestedChild = {
199+
name: 'DeepNestedChild',
200+
template: '<div>I am deeply nested</div>'
201+
}
202+
const NestedChild = {
203+
name: 'NestedChild',
204+
components: { DeepNestedChild },
205+
template: '<deep-nested-child class="in-child" />'
206+
}
207+
const RootComponent = {
208+
name: 'RootComponent',
209+
components: { NestedChild },
210+
template: '<div><nested-child class="in-root"></nested-child></div>'
211+
}
204212

205-
it('throws an error if findComponent is chained off a DOM element', () => {
206-
const wrapper = mountingMethod(ComponentWithChild)
207-
const message =
208-
'[vue-test-utils]: You cannot chain findComponent off a DOM element. It can only be used on Vue Components.'
209-
const fn = () => wrapper.find('span').findComponent('#foo')
210-
expect(fn).toThrow(message)
213+
const wrapper = mountingMethod(RootComponent, { stubs: { NestedChild } })
214+
215+
expect(wrapper.findComponent('.in-root').vm.$options.name).toEqual(
216+
'NestedChild'
217+
)
218+
219+
// someone might expect DeepNestedChild here, but
220+
// we always return TOP component matching DOM element
221+
expect(wrapper.findComponent('.in-child').vm.$options.name).toEqual(
222+
'NestedChild'
223+
)
211224
})
212225

213226
it('allows using findComponent on functional component', () => {

test/specs/wrapper/findAll.spec.js

+44-13
Original file line numberDiff line numberDiff line change
@@ -149,20 +149,51 @@ describeWithShallowAndMount('findAll', mountingMethod => {
149149
expect(componentArr.length).toEqual(1)
150150
})
151151

152-
it('throws an error if findAllComponents selector is a CSS selector', () => {
153-
const wrapper = mountingMethod(Component)
154-
const message =
155-
'[vue-test-utils]: findAllComponents requires a Vue constructor or valid find object. If you are searching for DOM nodes, use `find` instead'
156-
const fn = () => wrapper.findAllComponents('#foo')
157-
expect(fn).toThrow(message)
158-
})
152+
it('findAllComponents ignores DOM nodes matching same CSS selector', () => {
153+
const RootComponent = {
154+
components: { Component },
155+
template: '<div><Component class="foo" /><div class="foo"></div></div>'
156+
}
157+
const wrapper = mountingMethod(RootComponent)
158+
expect(wrapper.findAllComponents('.foo')).toHaveLength(1)
159+
expect(
160+
wrapper
161+
.findAllComponents('.foo')
162+
.at(0)
163+
.is(Component)
164+
).toBe(true)
165+
})
166+
167+
it('findAllComponents returns top-level components when components are nested', () => {
168+
const DeepNestedChild = {
169+
name: 'DeepNestedChild',
170+
template: '<div>I am deeply nested</div>'
171+
}
172+
const NestedChild = {
173+
name: 'NestedChild',
174+
components: { DeepNestedChild },
175+
template: '<deep-nested-child class="in-child" />'
176+
}
177+
const RootComponent = {
178+
name: 'RootComponent',
179+
components: { NestedChild },
180+
template: '<div><nested-child class="in-root"></nested-child></div>'
181+
}
159182

160-
it('throws an error if chaining findAllComponents off a DOM element', () => {
161-
const wrapper = mountingMethod(ComponentWithChild)
162-
const message =
163-
'[vue-test-utils]: You cannot chain findAllComponents off a DOM element. It can only be used on Vue Components.'
164-
const fn = () => wrapper.find('span').findAllComponents('#foo')
165-
expect(fn).toThrow(message)
183+
const wrapper = mountingMethod(RootComponent, { stubs: { NestedChild } })
184+
185+
expect(wrapper.findAllComponents('.in-root')).toHaveLength(1)
186+
expect(
187+
wrapper.findAllComponents('.in-root').at(0).vm.$options.name
188+
).toEqual('NestedChild')
189+
190+
expect(wrapper.findAllComponents('.in-child')).toHaveLength(1)
191+
192+
// someone might expect DeepNestedChild here, but
193+
// we always return TOP component matching DOM element
194+
expect(
195+
wrapper.findAllComponents('.in-child').at(0).vm.$options.name
196+
).toEqual('NestedChild')
166197
})
167198

168199
it('returns correct number of Vue Wrapper when component has a v-for', () => {

test/specs/wrapper/setValue.spec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ describeWithShallowAndMount('setValue', mountingMethod => {
6666
})
6767

6868
if (process.env.TEST_ENV !== 'browser') {
69-
it.only('sets element of multiselect value', async () => {
69+
it('sets element of multiselect value', async () => {
7070
const wrapper = mountingMethod(ComponentWithInput)
7171
const select = wrapper.find('select.multiselect')
7272
await select.setValue(['selectA', 'selectC'])

0 commit comments

Comments
 (0)