Skip to content

Commit df706fe

Browse files
committed
feat: support unnamed component selectors
1 parent 195449e commit df706fe

File tree

7 files changed

+103
-46
lines changed

7 files changed

+103
-46
lines changed

src/lib/find-vue-components.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// @flow
2+
import selectorTypes from './get-selector-type'
23

34
function findAllVueComponentsFromVm (vm: Component, components: Array<Component> = []): Array<Component> {
45
components.push(vm)
@@ -28,12 +29,18 @@ export function vmCtorMatchesName (vm: Component, name: string): boolean {
2829
vm.$options && vm.$options.name === name
2930
}
3031

31-
export default function findVueComponents (root: Component, componentName: string): Array<Component> {
32+
export function vmCtorMatchesSelector (component: Component, Ctor: Object) {
33+
return Ctor[0] === component.__proto__.constructor // eslint-disable-line no-proto
34+
}
35+
36+
export default function findVueComponents (root: Component, selectorType: string, selector: Object): Array<Component> {
3237
const components = root._isVue ? findAllVueComponentsFromVm(root) : findAllVueComponentsFromVnode(root)
3338
return components.filter((component) => {
3439
if (!component.$vnode && !component.$options.extends) {
3540
return false
3641
}
37-
return vmCtorMatchesName(component, componentName)
42+
return selectorType === selectorTypes.VUE_COMPONENT
43+
? vmCtorMatchesSelector(component, selector._Ctor)
44+
: vmCtorMatchesName(component, selector.name)
3845
})
3946
}

src/lib/get-selector-type.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,35 @@
11
// @flow
22

3-
import { isDomSelector, isVueComponent, isRefSelector } from './validators.js'
3+
import {
4+
isDomSelector,
5+
isNameSelector,
6+
isRefSelector,
7+
isVueComponent
8+
} from './validators.js'
49
import { throwError } from '../lib/util'
510

611
export const selectorTypes = {
712
DOM_SELECTOR: 'DOM_SELECTOR',
8-
VUE_COMPONENT: 'VUE_COMPONENT',
9-
OPTIONS_OBJECT: 'OPTIONS_OBJECT'
13+
NAME_SELECTOR: 'NAME_SELECTOR',
14+
REF_SELECTOR: 'REF_SELECTOR',
15+
VUE_COMPONENT: 'VUE_COMPONENT'
1016
}
1117

1218
function getSelectorType (selector: Selector): string | void {
1319
if (isDomSelector(selector)) {
1420
return selectorTypes.DOM_SELECTOR
1521
}
1622

23+
if (isNameSelector(selector)) {
24+
return selectorTypes.NAME_SELECTOR
25+
}
26+
1727
if (isVueComponent(selector)) {
1828
return selectorTypes.VUE_COMPONENT
1929
}
2030

2131
if (isRefSelector(selector)) {
22-
return selectorTypes.OPTIONS_OBJECT
32+
return selectorTypes.REF_SELECTOR
2333
}
2434
}
2535

src/lib/validators.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,15 @@ export function isVueComponent (component: any): boolean {
3535
return false
3636
}
3737

38-
return typeof component.render === 'function' || !!component.extends
38+
if (component.extends) {
39+
return true
40+
}
41+
42+
if (component._Ctor) {
43+
return true
44+
}
45+
46+
return typeof component.render === 'function'
3947
}
4048

4149
export function isValidSelector (selector: any): boolean {
@@ -47,6 +55,10 @@ export function isValidSelector (selector: any): boolean {
4755
return true
4856
}
4957

58+
if (isNameSelector(selector)) {
59+
return true
60+
}
61+
5062
return isRefSelector(selector)
5163
}
5264

@@ -72,3 +84,15 @@ export function isRefSelector (refOptionsObject: any) {
7284

7385
return isValid
7486
}
87+
88+
export function isNameSelector (nameOptionsObject: any): boolean {
89+
if (typeof nameOptionsObject !== 'object') {
90+
return false
91+
}
92+
93+
if (nameOptionsObject === null) {
94+
return false
95+
}
96+
97+
return !!nameOptionsObject.name
98+
}

src/wrappers/wrapper.js

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,12 @@ export default class Wrapper implements BaseWrapper {
7373
contains (selector: Selector) {
7474
const selectorType = getSelectorTypeOrThrow(selector, 'contains')
7575

76-
if (selectorType === selectorTypes.VUE_COMPONENT) {
76+
if (selectorType === selectorTypes.NAME_SELECTOR || selectorType === selectorTypes.VUE_COMPONENT) {
7777
const vm = this.vm || this.vnode.context.$root
78-
return findVueComponents(vm, selector.name).length > 0 || this.is(selector)
78+
return findVueComponents(vm, selector, selector).length > 0 || this.is(selector)
7979
}
8080

81-
if (selectorType === selectorTypes.OPTIONS_OBJECT) {
81+
if (selectorType === selectorTypes.REF_SELECTOR) {
8282
if (!this.vm) {
8383
throwError('$ref selectors can only be used on Vue component wrappers')
8484
}
@@ -228,19 +228,19 @@ export default class Wrapper implements BaseWrapper {
228228
*/
229229
find (selector: Selector): Wrapper | ErrorWrapper | VueWrapper {
230230
const selectorType = getSelectorTypeOrThrow(selector, 'find')
231-
if (selectorType === selectorTypes.VUE_COMPONENT) {
232-
if (!selector.name) {
233-
throwError('.find() requires component to have a name property')
234-
}
231+
232+
if (selectorType === selectorTypes.VUE_COMPONENT ||
233+
selectorType === selectorTypes.NAME_SELECTOR) {
235234
const root = this.vm || this.vnode
236-
const components = findVueComponents(root, selector.name)
235+
// $FlowIgnore warning about selectorType being undefined
236+
const components = findVueComponents(root, selectorType, selector)
237237
if (components.length === 0) {
238238
return new ErrorWrapper('Component')
239239
}
240240
return new VueWrapper(components[0], this.options)
241241
}
242242

243-
if (selectorType === selectorTypes.OPTIONS_OBJECT) {
243+
if (selectorType === selectorTypes.REF_SELECTOR) {
244244
if (!this.vm) {
245245
throwError('$ref selectors can only be used on Vue component wrappers')
246246
}
@@ -268,16 +268,15 @@ export default class Wrapper implements BaseWrapper {
268268
findAll (selector: Selector): WrapperArray {
269269
const selectorType = getSelectorTypeOrThrow(selector, 'findAll')
270270

271-
if (selectorType === selectorTypes.VUE_COMPONENT) {
272-
if (!selector.name) {
273-
throwError('.findAll() requires component to have a name property')
274-
}
271+
if (selectorType === selectorTypes.VUE_COMPONENT ||
272+
selectorType === selectorTypes.NAME_SELECTOR) {
275273
const root = this.vm || this.vnode
276-
const components = findVueComponents(root, selector.name)
274+
// $FlowIgnore warning about selectorType being undefined
275+
const components = findVueComponents(root, selectorType, selector)
277276
return new WrapperArray(components.map(component => new VueWrapper(component, this.options)))
278277
}
279278

280-
if (selectorType === selectorTypes.OPTIONS_OBJECT) {
279+
if (selectorType === selectorTypes.REF_SELECTOR) {
281280
if (!this.vm) {
282281
throwError('$ref selectors can only be used on Vue component wrappers')
283282
}
@@ -311,14 +310,14 @@ export default class Wrapper implements BaseWrapper {
311310
is (selector: Selector): boolean {
312311
const selectorType = getSelectorTypeOrThrow(selector, 'is')
313312

314-
if (selectorType === selectorTypes.VUE_COMPONENT && this.vm) {
315-
if (typeof selector.name !== 'string') {
316-
throwError('a Component used as a selector must have a name property')
313+
if (selectorType === selectorTypes.NAME_SELECTOR) {
314+
if (!this.vm) {
315+
return false
317316
}
318317
return vmCtorMatchesName(this.vm, selector.name)
319318
}
320319

321-
if (selectorType === selectorTypes.OPTIONS_OBJECT) {
320+
if (selectorType === selectorTypes.REF_SELECTOR) {
322321
throwError('$ref selectors can not be used with wrapper.is()')
323322
}
324323

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

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -140,13 +140,6 @@ describe('find', () => {
140140
expect(span.find(AComponent).exists()).to.equal(false)
141141
})
142142

143-
it('throws error if component does not have a name property', () => {
144-
const wrapper = mount(Component)
145-
const message = '[vue-test-utils]: .find() requires component to have a name property'
146-
const fn = () => wrapper.find(ComponentWithoutName)
147-
expect(fn).to.throw().with.property('message', message)
148-
})
149-
150143
it('returns empty Wrapper with error if no nodes are found', () => {
151144
const wrapper = mount(Component)
152145
const selector = 'pre'
@@ -211,6 +204,21 @@ describe('find', () => {
211204
expect(error.selector).to.equal('ref="foo"')
212205
})
213206

207+
it('returns Wrapper matching component that has no name property', () => {
208+
const TestComponent = {
209+
template: `
210+
<div>
211+
<component-without-name />
212+
</div>
213+
`,
214+
components: {
215+
ComponentWithoutName
216+
}
217+
}
218+
const wrapper = mount(TestComponent)
219+
expect(wrapper.find(ComponentWithoutName).exists()).to.equal(true)
220+
})
221+
214222
it('throws an error if selector is not a valid selector', () => {
215223
const wrapper = mount(Component)
216224
const invalidSelectors = [

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,22 @@ describe('findAll', () => {
157157
const span = wrapper.find('span')
158158
expect(span.findAll(AComponent).length).to.equal(1)
159159
})
160-
it('throws an error if component does not have a name property', () => {
161-
const wrapper = mount(Component)
162-
const message = '[vue-test-utils]: .findAll() requires component to have a name property'
163-
expect(() => wrapper.findAll(ComponentWithoutName)).to.throw().with.property('message', message)
160+
161+
it('returns matching Vue components that have no name property', () => {
162+
const TestComponent = {
163+
template: `
164+
<div>
165+
<component-without-name />
166+
<component-without-name />
167+
<component-without-name />
168+
</div>
169+
`,
170+
components: {
171+
ComponentWithoutName
172+
}
173+
}
174+
const wrapper = mount(TestComponent)
175+
expect(wrapper.findAll(ComponentWithoutName).length).to.equal(3)
164176
})
165177

166178
it('returns VueWrapper with length 0 if no nodes matching selector are found', () => {

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

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { compileToFunctions } from 'vue-template-compiler'
22
import { mount } from '~vue-test-utils'
33
import ComponentWithChild from '~resources/components/component-with-child.vue'
44
import Component from '~resources/components/component.vue'
5+
import ComponentWithoutName from '~resources/components/component-without-name.vue'
56

67
describe('is', () => {
78
it('returns true if root node matches tag selector', () => {
@@ -39,6 +40,11 @@ describe('is', () => {
3940
expect(wrapper.is(Component)).to.equal(true)
4041
})
4142

43+
it.skip('returns true if root node matches Component without a name', () => {
44+
const wrapper = mount(ComponentWithoutName)
45+
expect(wrapper.is(ComponentWithoutName)).to.equal(true)
46+
})
47+
4248
it('returns false if root node is not a Vue Component', () => {
4349
const wrapper = mount(ComponentWithChild)
4450
const input = wrapper.findAll('span').at(0)
@@ -72,15 +78,6 @@ describe('is', () => {
7278
expect(fn).to.throw().with.property('message', message)
7379
})
7480

75-
it('throws error if component passed to use as identifier does not have a name', () => {
76-
const compiled = compileToFunctions('<div />')
77-
const wrapper = mount(compiled)
78-
79-
const message = '[vue-test-utils]: a Component used as a selector must have a name property'
80-
const fn = () => wrapper.is({ render: () => {} })
81-
expect(fn).to.throw().with.property('message', message)
82-
})
83-
8481
it('throws an error if selector is not a valid selector', () => {
8582
const compiled = compileToFunctions('<div />')
8683
const wrapper = mount(compiled)

0 commit comments

Comments
 (0)