Skip to content

Commit 4e901ba

Browse files
committed
wrapper.refs
1 parent 954c3c3 commit 4e901ba

12 files changed

+209
-61
lines changed

flow/wrapper.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ declare interface BaseWrapper { // eslint-disable-line no-undef
1515
hasClass(className: string): boolean | void,
1616
hasProp(prop: string, value: string): boolean | void,
1717
hasStyle(style: string, value: string): boolean | void,
18-
find(selector: Selector): Wrapper | void,
19-
findAll(selector: Selector): WrapperArray | void,
18+
find(selector: Selector | FindOptions): Wrapper | void,
19+
findAll(selector: Selector | FindOptions): WrapperArray | void,
2020
html(): string | void,
2121
is(selector: Selector): boolean | void,
2222
isEmpty(): boolean | void,
@@ -34,3 +34,7 @@ declare type WrapperOptions = { // eslint-disable-line no-undef
3434
attachedToDocument: boolean,
3535
error?: string
3636
}
37+
38+
declare type FindOptions = { // eslint-disable-line no-undef
39+
ref: string,
40+
}

src/lib/find-matching-vnodes.js

Lines changed: 0 additions & 38 deletions
This file was deleted.

src/lib/find-vnodes-by-ref.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// @flow
2+
3+
import { removeDuplicateNodes, findAllVNodes } from './vnode-utils'
4+
5+
function nodeMatchesRef (node: VNode, refName: string): boolean {
6+
return node.data && node.data.ref === refName
7+
}
8+
9+
export default function findVNodesByRef (vNode: VNode, refName: string): Array<VNode> {
10+
const nodes = findAllVNodes(vNode)
11+
const refFilteredNodes = nodes.filter(node => nodeMatchesRef(node, refName))
12+
// Only return refs defined on top-level VNode to provide the same behavior as selecting via vm.$ref.{someRefName}
13+
const mainVNodeFilteredNodes = refFilteredNodes.filter(node => !!vNode.context.$refs[node.data.ref])
14+
return removeDuplicateNodes(mainVNodeFilteredNodes)
15+
}

src/lib/find-vnodes-by-selector.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// @flow
2+
3+
import { removeDuplicateNodes, findAllVNodes } from './vnode-utils'
4+
5+
function nodeMatchesSelector (node: VNode, selector: string): boolean {
6+
return node.elm && node.elm.getAttribute && node.elm.matches(selector)
7+
}
8+
9+
export default function findVNodesBySelector (vNode: VNode, selector: string): Array<VNode> {
10+
const nodes = findAllVNodes(vNode)
11+
const filteredNodes = nodes.filter(node => nodeMatchesSelector(node, selector))
12+
return removeDuplicateNodes(filteredNodes)
13+
}

src/lib/validators.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,26 @@ export function isValidSelector (selector: any): boolean {
4545

4646
return isVueComponent(selector)
4747
}
48+
49+
export function isValidFindOption (findOptions: any) {
50+
if (typeof findOptions !== 'object') {
51+
return false
52+
}
53+
54+
if (findOptions === null) {
55+
return false
56+
}
57+
58+
const validFindKeys = ['ref']
59+
const entries = Object.entries(findOptions)
60+
61+
if (!entries.length) {
62+
return false
63+
}
64+
65+
const isValid = entries.every(([key, value]) => {
66+
return validFindKeys.includes(key) && typeof value === 'string'
67+
})
68+
69+
return isValid
70+
}

src/lib/vnode-utils.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// @flow
2+
3+
export function findAllVNodes (vnode: VNode, nodes: Array<VNode> = []): Array<VNode> {
4+
nodes.push(vnode)
5+
6+
if (Array.isArray(vnode.children)) {
7+
vnode.children.forEach((childVNode) => {
8+
findAllVNodes(childVNode, nodes)
9+
})
10+
}
11+
12+
if (vnode.child) {
13+
findAllVNodes(vnode.child._vnode, nodes)
14+
}
15+
16+
return nodes
17+
}
18+
19+
export function removeDuplicateNodes (vNodes: Array<VNode>): Array<VNode> {
20+
const uniqueNodes = []
21+
vNodes.forEach((vNode) => {
22+
const exists = uniqueNodes.some(node => vNode.elm === node.elm)
23+
if (!exists) {
24+
uniqueNodes.push(vNode)
25+
}
26+
})
27+
return uniqueNodes
28+
}

src/wrappers/wrapper.js

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
// @flow
22

3-
import { isValidSelector } from '../lib/validators'
3+
import { isValidSelector, isVueComponent, isValidFindOption } from '../lib/validators'
44
import findVueComponents, { vmCtorMatchesName } from '../lib/find-vue-components'
5-
import findMatchingVNodes from '../lib/find-matching-vnodes'
5+
import findVNodesBySelector from '../lib/find-vnodes-by-selector'
6+
import findVNodesByRef from '../lib/find-vnodes-by-ref'
67
import VueWrapper from './vue-wrapper'
78
import WrapperArray from './wrapper-array'
89
import ErrorWrapper from './error-wrapper'
@@ -160,12 +161,13 @@ export default class Wrapper implements BaseWrapper {
160161
/**
161162
* Finds first node in tree of the current wrapper that matches the provided selector.
162163
*/
163-
find (selector: string): Wrapper | ErrorWrapper | VueWrapper {
164-
if (!isValidSelector(selector)) {
165-
throwError('wrapper.find() must be passed a valid CSS selector or a Vue constructor')
164+
find (selector: Selector | FindOptions): Wrapper | ErrorWrapper | VueWrapper {
165+
const isValidOptionObject = isValidFindOption(selector)
166+
if (!isValidSelector(selector) && !isValidOptionObject) {
167+
throwError('wrapper.find() must be passed a valid CSS selector, Vue constructor, or valid find option object')
166168
}
167169

168-
if (typeof selector === 'object') {
170+
if (typeof selector === 'object' && isVueComponent(selector)) {
169171
if (!selector.name) {
170172
throwError('.find() requires component to have a name property')
171173
}
@@ -177,7 +179,18 @@ export default class Wrapper implements BaseWrapper {
177179
return new VueWrapper(components[0], this.options)
178180
}
179181

180-
const nodes = findMatchingVNodes(this.vnode, selector)
182+
if (typeof selector === 'object' && isValidOptionObject) {
183+
if (!this.isVueComponent) {
184+
throwError('$ref selectors can only be used on Vue component wrappers')
185+
}
186+
const nodes = findVNodesByRef(this.vnode, selector.ref)
187+
if (nodes.length === 0) {
188+
return new ErrorWrapper(`ref="${selector.ref}"`)
189+
}
190+
return new Wrapper(nodes[0], this.update, this.options)
191+
}
192+
193+
const nodes = findVNodesBySelector(this.vnode, selector)
181194

182195
if (nodes.length === 0) {
183196
return new ErrorWrapper(selector)
@@ -188,12 +201,12 @@ export default class Wrapper implements BaseWrapper {
188201
/**
189202
* Finds node in tree of the current wrapper that matches the provided selector.
190203
*/
191-
findAll (selector: Selector): WrapperArray {
192-
if (!isValidSelector(selector)) {
193-
throwError('wrapper.findAll() must be passed a valid CSS selector or a Vue constructor')
204+
findAll (selector: Selector | FindOptions): WrapperArray {
205+
if (!isValidSelector(selector) && !isValidFindOption(selector)) {
206+
throwError('wrapper.findAll() must be passed a valid CSS selector, Vue constructor, or valid find option object')
194207
}
195208

196-
if (typeof selector === 'object') {
209+
if (typeof selector === 'object' && isVueComponent(selector)) {
197210
if (!selector.name) {
198211
throwError('.findAll() requires component to have a name property')
199212
}
@@ -202,11 +215,19 @@ export default class Wrapper implements BaseWrapper {
202215
return new WrapperArray(components.map(component => new VueWrapper(component, this.options)))
203216
}
204217

218+
if (typeof selector === 'object' && isValidFindOption(selector)) {
219+
if (!this.isVueComponent) {
220+
throwError('$ref selectors can only be used on Vue component wrappers')
221+
}
222+
const nodes = findVNodesByRef(this.vnode, selector.ref)
223+
return new WrapperArray(nodes.map(node => new Wrapper(node, this.update, this.options)))
224+
}
225+
205226
function nodeMatchesSelector (node, selector) {
206227
return node.elm && node.elm.getAttribute && node.elm.matches(selector)
207228
}
208229

209-
const nodes = findMatchingVNodes(this.vnode, selector)
230+
const nodes = findVNodesBySelector(this.vnode, selector)
210231
const matchingNodes = nodes.filter(node => nodeMatchesSelector(node, selector))
211232

212233
return new WrapperArray(matchingNodes.map(node => new Wrapper(node, this.update, this.options)))

test/resources/components/component-with-child.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<div>
33
<span>
4-
<child-component />
4+
<child-component ref="child"/>
55
</span>
66
</div>
77
</template>

test/resources/components/component-with-v-for.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<div>
3-
<AComponent v-for="item in items" :key="item.id" />
3+
<AComponent v-for="item in items" :key="item.id" ref="item"/>
44
</div>
55
</template>
66

test/unit/specs/mount/Wrapper/find.spec.js

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ describe('find', () => {
7575
it('throws an error when passed an invalid DOM selector', () => {
7676
const compiled = compileToFunctions('<div><a href="/"></a></div>')
7777
const wrapper = mount(compiled)
78-
const message = '[vue-test-utils]: wrapper.find() must be passed a valid CSS selector or a Vue constructor'
78+
const message = '[vue-test-utils]: wrapper.find() must be passed a valid CSS selector, Vue constructor, or valid find option object'
7979
const fn = () => wrapper.find('[href=&6"/"]')
8080
expect(fn).to.throw().with.property('message', message)
8181
})
@@ -103,7 +103,7 @@ describe('find', () => {
103103
expect(wrapper.find(Component)).to.be.instanceOf(Wrapper)
104104
})
105105

106-
it('returns correct number of Vue Wrapper when component has a v-for', () => {
106+
it('returns correct number of Vue Wrappers when component has a v-for', () => {
107107
const items = [{ id: 1 }, { id: 2 }, { id: 3 }]
108108
const wrapper = mount(ComponentWithVFor, { propsData: { items }})
109109
expect(wrapper.find(Component)).to.be.instanceOf(Wrapper)
@@ -144,13 +144,46 @@ describe('find', () => {
144144
expect(error.selector).to.equal('Component')
145145
})
146146

147+
it('returns Wrapper of elements matching the ref in options object', () => {
148+
const compiled = compileToFunctions('<div><p ref="foo"></p></div>')
149+
const wrapper = mount(compiled)
150+
expect(wrapper.find({ ref: 'foo' })).to.be.instanceOf(Wrapper)
151+
})
152+
153+
it('returns Wrapper of Vue Components matching the ref in options object', () => {
154+
const wrapper = mount(ComponentWithChild)
155+
expect(wrapper.find({ ref: 'child' })).to.be.instanceOf(Wrapper)
156+
})
157+
158+
it('throws an error when ref selector is called on a wrapper that is not a Vue component', () => {
159+
const compiled = compileToFunctions('<div><a href="/"></a></div>')
160+
const wrapper = mount(compiled)
161+
const a = wrapper.find('a')
162+
const message = '[vue-test-utils]: $ref selectors can only be used on Vue component wrappers'
163+
const fn = () => a.find({ ref: 'foo' })
164+
expect(fn).to.throw().with.property('message', message)
165+
})
166+
167+
it('returns Wrapper matching ref selector in options object passed if nested in a transition', () => {
168+
const compiled = compileToFunctions('<transition><div ref="foo"/></transition>')
169+
const wrapper = mount(compiled)
170+
expect(wrapper.find({ ref: 'foo' })).to.be.instanceOf(Wrapper)
171+
})
172+
173+
it('returns empty Wrapper with error if no nodes are found via ref in options object', () => {
174+
const wrapper = mount(Component)
175+
const error = wrapper.find({ ref: 'foo' })
176+
expect(error).to.be.instanceOf(ErrorWrapper)
177+
expect(error.selector).to.equal('ref="foo"')
178+
})
179+
147180
it('throws an error if selector is not a valid selector', () => {
148181
const wrapper = mount(Component)
149182
const invalidSelectors = [
150-
undefined, null, NaN, 0, 2, true, false, () => {}, {}, { name: undefined }, []
183+
undefined, null, NaN, 0, 2, true, false, () => {}, {}, { name: undefined }, { ref: 'foo', nope: true }, []
151184
]
152185
invalidSelectors.forEach((invalidSelector) => {
153-
const message = '[vue-test-utils]: wrapper.find() must be passed a valid CSS selector or a Vue constructor'
186+
const message = '[vue-test-utils]: wrapper.find() must be passed a valid CSS selector, Vue constructor, or valid find option object'
154187
const fn = () => wrapper.find(invalidSelector)
155188
expect(fn).to.throw().with.property('message', message)
156189
})

test/unit/specs/mount/Wrapper/findAll.spec.js

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ describe('findAll', () => {
8282
it('throws an error when passed an invalid DOM selector', () => {
8383
const compiled = compileToFunctions('<div><a href="/"></a></div>')
8484
const wrapper = mount(compiled)
85-
const message = '[vue-test-utils]: wrapper.findAll() must be passed a valid CSS selector or a Vue constructor'
85+
const message = '[vue-test-utils]: wrapper.findAll() must be passed a valid CSS selector, Vue constructor, or valid find option object'
8686
const fn = () => wrapper.findAll('[href=&6"/"]')
8787
expect(fn).to.throw().with.property('message', message)
8888
})
@@ -161,13 +161,53 @@ describe('findAll', () => {
161161
expect(preArray.wrappers).to.deep.equal([])
162162
})
163163

164+
it('returns an array of Wrapper of elements matching the ref in options object', () => {
165+
const compiled = compileToFunctions('<div><div ref="foo" /></div>')
166+
const wrapper = mount(compiled)
167+
const fooArr = wrapper.findAll({ ref: 'foo' })
168+
expect(fooArr).to.be.instanceOf(WrapperArray)
169+
expect(fooArr.length).to.equal(1)
170+
})
171+
172+
it('throws an error when ref selector is called on a wrapper that is not a Vue component', () => {
173+
const compiled = compileToFunctions('<div><a href="/"></a></div>')
174+
const wrapper = mount(compiled)
175+
const a = wrapper.find('a')
176+
const message = '[vue-test-utils]: $ref selectors can only be used on Vue component wrappers'
177+
const fn = () => a.findAll({ ref: 'foo' })
178+
expect(fn).to.throw().with.property('message', message)
179+
})
180+
181+
it('returns an array of Wrapper of elements matching the ref in options object if they are nested in a transition', () => {
182+
const compiled = compileToFunctions('<transition><div ref="foo" /></transition>')
183+
const wrapper = mount(compiled)
184+
const divArr = wrapper.findAll({ ref: 'foo' })
185+
expect(divArr).to.be.instanceOf(WrapperArray)
186+
expect(divArr.length).to.equal(1)
187+
})
188+
189+
it('returns correct number of Vue Wrapper when component has a v-for and matches the ref in options object', () => {
190+
const items = [{ id: 1 }, { id: 2 }, { id: 3 }]
191+
const wrapper = mount(ComponentWithVFor, { propsData: { items }})
192+
const componentArray = wrapper.findAll({ ref: 'item' })
193+
expect(componentArray).to.be.instanceOf(WrapperArray)
194+
expect(componentArray.length).to.equal(items.length)
195+
})
196+
197+
it('returns VueWrapper with length 0 if no nodes matching the ref in options object are found', () => {
198+
const wrapper = mount(Component)
199+
const preArray = wrapper.findAll({ ref: 'foo' })
200+
expect(preArray.length).to.equal(0)
201+
expect(preArray.wrappers).to.deep.equal([])
202+
})
203+
164204
it('throws an error if selector is not a valid selector', () => {
165205
const wrapper = mount(Component)
166206
const invalidSelectors = [
167-
undefined, null, NaN, 0, 2, true, false, () => {}, {}, { name: undefined }, []
207+
undefined, null, NaN, 0, 2, true, false, () => {}, {}, { name: undefined }, { ref: 'foo', nope: true }, []
168208
]
169209
invalidSelectors.forEach((invalidSelector) => {
170-
const message = '[vue-test-utils]: wrapper.findAll() must be passed a valid CSS selector or a Vue constructor'
210+
const message = '[vue-test-utils]: wrapper.findAll() must be passed a valid CSS selector, Vue constructor, or valid find option object'
171211
const fn = () => wrapper.findAll(invalidSelector)
172212
expect(fn).to.throw().with.property('message', message)
173213
})

0 commit comments

Comments
 (0)