Skip to content

Commit

Permalink
fix: restore chaining and CSS selectors for findComponent
Browse files Browse the repository at this point in the history
Allow findComponent / findAllComponents to be chained from
DOM selector and allow CSS selectors to be used
  • Loading branch information
xanf committed Nov 5, 2021
1 parent 60a3438 commit ee731ef
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 52 deletions.
6 changes: 5 additions & 1 deletion docs/api/wrapper/findAllComponents.md
Expand Up @@ -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}`

Expand All @@ -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)
:::
30 changes: 29 additions & 1 deletion docs/api/wrapper/findComponent.md
Expand Up @@ -4,7 +4,7 @@ Returns `Wrapper` of first matching Vue component.

- **Arguments:**

- `{Component|ref|name} selector`
- `{Component|ref|string} selector`

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

Expand All @@ -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: '<div class="child"></div>'
}

const RootComponent = {
name: 'Root',
components: { ChildComponent },
template: '<child-component class="root" />'
}

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
:::
4 changes: 4 additions & 0 deletions packages/shared/util.js
Expand Up @@ -111,3 +111,7 @@ export function warnDeprecated(method: string, fallback: string = '') {
warn(msg)
}
}

export function isVueWrapper(wrapper: Object) {
return wrapper.vm || wrapper.isFunctionalComponent
}
27 changes: 4 additions & 23 deletions packages/test-utils/src/wrapper.js
Expand Up @@ -16,7 +16,8 @@ import {
isPhantomJS,
nextTick,
warn,
warnDeprecated
warnDeprecated,
isVueWrapper
} from 'shared/util'
import { isElementVisible } from 'shared/is-visible'
import find from './find'
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -327,17 +317,8 @@ 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 {
Expand Down
39 changes: 26 additions & 13 deletions test/specs/wrapper/find.spec.js
Expand Up @@ -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: '<div>I am deeply nested</div>'
}
const NestedChild = {
name: 'NestedChild',
components: { DeepNestedChild },
template: '<deep-nested-child class="in-child" />'
}
const RootComponent = {
name: 'RootComponent',
components: { NestedChild },
template: '<div><nested-child class="in-root"></nested-child></div>'
}

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', () => {
Expand Down
57 changes: 44 additions & 13 deletions test/specs/wrapper/findAll.spec.js
Expand Up @@ -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: '<div><Component class="foo" /><div class="foo"></div></div>'
}
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: '<div>I am deeply nested</div>'
}
const NestedChild = {
name: 'NestedChild',
components: { DeepNestedChild },
template: '<deep-nested-child class="in-child" />'
}
const RootComponent = {
name: 'RootComponent',
components: { NestedChild },
template: '<div><nested-child class="in-root"></nested-child></div>'
}

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', () => {
Expand Down
2 changes: 1 addition & 1 deletion test/specs/wrapper/setValue.spec.js
Expand Up @@ -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'])
Expand Down

0 comments on commit ee731ef

Please sign in to comment.