Skip to content

Commit 48fdb64

Browse files
committed
wrapper.refs
1 parent a974042 commit 48fdb64

File tree

12 files changed

+209
-61
lines changed

12 files changed

+209
-61
lines changed

flow/wrapper.flow.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,
@@ -36,3 +36,7 @@ declare type WrapperOptions = { // eslint-disable-line no-undef
3636
attachedToDocument: boolean,
3737
error?: string
3838
}
39+
40+
declare type FindOptions = { // eslint-disable-line no-undef
41+
ref: string,
42+
}

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,9 +1,10 @@
11
// @flow
22

33
import Vue from 'vue'
4-
import { isValidSelector } from '../lib/validators'
4+
import { isValidSelector, isVueComponent, isValidFindOption } from '../lib/validators'
55
import findVueComponents, { vmCtorMatchesName } from '../lib/find-vue-components'
6-
import findMatchingVNodes from '../lib/find-matching-vnodes'
6+
import findVNodesBySelector from '../lib/find-vnodes-by-selector'
7+
import findVNodesByRef from '../lib/find-vnodes-by-ref'
78
import VueWrapper from './vue-wrapper'
89
import WrapperArray from './wrapper-array'
910
import ErrorWrapper from './error-wrapper'
@@ -174,12 +175,13 @@ export default class Wrapper implements BaseWrapper {
174175
/**
175176
* Finds first node in tree of the current wrapper that matches the provided selector.
176177
*/
177-
find (selector: string): Wrapper | ErrorWrapper | VueWrapper {
178-
if (!isValidSelector(selector)) {
179-
throwError('wrapper.find() must be passed a valid CSS selector or a Vue constructor')
178+
find (selector: Selector | FindOptions): Wrapper | ErrorWrapper | VueWrapper {
179+
const isValidOptionObject = isValidFindOption(selector)
180+
if (!isValidSelector(selector) && !isValidOptionObject) {
181+
throwError('wrapper.find() must be passed a valid CSS selector, Vue constructor, or valid find option object')
180182
}
181183

182-
if (typeof selector === 'object') {
184+
if (typeof selector === 'object' && isVueComponent(selector)) {
183185
if (!selector.name) {
184186
throwError('.find() requires component to have a name property')
185187
}
@@ -191,7 +193,18 @@ export default class Wrapper implements BaseWrapper {
191193
return new VueWrapper(components[0], this.options)
192194
}
193195

194-
const nodes = findMatchingVNodes(this.vnode, selector)
196+
if (typeof selector === 'object' && isValidOptionObject) {
197+
if (!this.isVueComponent) {
198+
throwError('$ref selectors can only be used on Vue component wrappers')
199+
}
200+
const nodes = findVNodesByRef(this.vnode, selector.ref)
201+
if (nodes.length === 0) {
202+
return new ErrorWrapper(`ref="${selector.ref}"`)
203+
}
204+
return new Wrapper(nodes[0], this.update, this.options)
205+
}
206+
207+
const nodes = findVNodesBySelector(this.vnode, selector)
195208

196209
if (nodes.length === 0) {
197210
return new ErrorWrapper(selector)
@@ -202,12 +215,12 @@ export default class Wrapper implements BaseWrapper {
202215
/**
203216
* Finds node in tree of the current wrapper that matches the provided selector.
204217
*/
205-
findAll (selector: Selector): WrapperArray {
206-
if (!isValidSelector(selector)) {
207-
throwError('wrapper.findAll() must be passed a valid CSS selector or a Vue constructor')
218+
findAll (selector: Selector | FindOptions): WrapperArray {
219+
if (!isValidSelector(selector) && !isValidFindOption(selector)) {
220+
throwError('wrapper.findAll() must be passed a valid CSS selector, Vue constructor, or valid find option object')
208221
}
209222

210-
if (typeof selector === 'object') {
223+
if (typeof selector === 'object' && isVueComponent(selector)) {
211224
if (!selector.name) {
212225
throwError('.findAll() requires component to have a name property')
213226
}
@@ -216,11 +229,19 @@ export default class Wrapper implements BaseWrapper {
216229
return new WrapperArray(components.map(component => new VueWrapper(component, this.options)))
217230
}
218231

232+
if (typeof selector === 'object' && isValidFindOption(selector)) {
233+
if (!this.isVueComponent) {
234+
throwError('$ref selectors can only be used on Vue component wrappers')
235+
}
236+
const nodes = findVNodesByRef(this.vnode, selector.ref)
237+
return new WrapperArray(nodes.map(node => new Wrapper(node, this.update, this.options)))
238+
}
239+
219240
function nodeMatchesSelector (node, selector) {
220241
return node.elm && node.elm.getAttribute && node.elm.matches(selector)
221242
}
222243

223-
const nodes = findMatchingVNodes(this.vnode, selector)
244+
const nodes = findVNodesBySelector(this.vnode, selector)
224245
const matchingNodes = nodes.filter(node => nodeMatchesSelector(node, selector))
225246

226247
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)