Skip to content

Commit

Permalink
feat: support unnamed component selectors
Browse files Browse the repository at this point in the history
  • Loading branch information
eddyerburgh committed Dec 24, 2017
1 parent 195449e commit df706fe
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 46 deletions.
11 changes: 9 additions & 2 deletions src/lib/find-vue-components.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// @flow
import selectorTypes from './get-selector-type'

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

export default function findVueComponents (root: Component, componentName: string): Array<Component> {
export function vmCtorMatchesSelector (component: Component, Ctor: Object) {
return Ctor[0] === component.__proto__.constructor // eslint-disable-line no-proto
}

export default function findVueComponents (root: Component, selectorType: string, selector: Object): Array<Component> {
const components = root._isVue ? findAllVueComponentsFromVm(root) : findAllVueComponentsFromVnode(root)
return components.filter((component) => {
if (!component.$vnode && !component.$options.extends) {
return false
}
return vmCtorMatchesName(component, componentName)
return selectorType === selectorTypes.VUE_COMPONENT
? vmCtorMatchesSelector(component, selector._Ctor)
: vmCtorMatchesName(component, selector.name)
})
}
18 changes: 14 additions & 4 deletions src/lib/get-selector-type.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
// @flow

import { isDomSelector, isVueComponent, isRefSelector } from './validators.js'
import {
isDomSelector,
isNameSelector,
isRefSelector,
isVueComponent
} from './validators.js'
import { throwError } from '../lib/util'

export const selectorTypes = {
DOM_SELECTOR: 'DOM_SELECTOR',
VUE_COMPONENT: 'VUE_COMPONENT',
OPTIONS_OBJECT: 'OPTIONS_OBJECT'
NAME_SELECTOR: 'NAME_SELECTOR',
REF_SELECTOR: 'REF_SELECTOR',
VUE_COMPONENT: 'VUE_COMPONENT'
}

function getSelectorType (selector: Selector): string | void {
if (isDomSelector(selector)) {
return selectorTypes.DOM_SELECTOR
}

if (isNameSelector(selector)) {
return selectorTypes.NAME_SELECTOR
}

if (isVueComponent(selector)) {
return selectorTypes.VUE_COMPONENT
}

if (isRefSelector(selector)) {
return selectorTypes.OPTIONS_OBJECT
return selectorTypes.REF_SELECTOR
}
}

Expand Down
26 changes: 25 additions & 1 deletion src/lib/validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,15 @@ export function isVueComponent (component: any): boolean {
return false
}

return typeof component.render === 'function' || !!component.extends
if (component.extends) {
return true
}

if (component._Ctor) {
return true
}

return typeof component.render === 'function'
}

export function isValidSelector (selector: any): boolean {
Expand All @@ -47,6 +55,10 @@ export function isValidSelector (selector: any): boolean {
return true
}

if (isNameSelector(selector)) {
return true
}

return isRefSelector(selector)
}

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

return isValid
}

export function isNameSelector (nameOptionsObject: any): boolean {
if (typeof nameOptionsObject !== 'object') {
return false
}

if (nameOptionsObject === null) {
return false
}

return !!nameOptionsObject.name
}
37 changes: 18 additions & 19 deletions src/wrappers/wrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,12 @@ export default class Wrapper implements BaseWrapper {
contains (selector: Selector) {
const selectorType = getSelectorTypeOrThrow(selector, 'contains')

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

if (selectorType === selectorTypes.OPTIONS_OBJECT) {
if (selectorType === selectorTypes.REF_SELECTOR) {
if (!this.vm) {
throwError('$ref selectors can only be used on Vue component wrappers')
}
Expand Down Expand Up @@ -228,19 +228,19 @@ export default class Wrapper implements BaseWrapper {
*/
find (selector: Selector): Wrapper | ErrorWrapper | VueWrapper {
const selectorType = getSelectorTypeOrThrow(selector, 'find')
if (selectorType === selectorTypes.VUE_COMPONENT) {
if (!selector.name) {
throwError('.find() requires component to have a name property')
}

if (selectorType === selectorTypes.VUE_COMPONENT ||
selectorType === selectorTypes.NAME_SELECTOR) {
const root = this.vm || this.vnode
const components = findVueComponents(root, selector.name)
// $FlowIgnore warning about selectorType being undefined
const components = findVueComponents(root, selectorType, selector)
if (components.length === 0) {
return new ErrorWrapper('Component')
}
return new VueWrapper(components[0], this.options)
}

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

if (selectorType === selectorTypes.VUE_COMPONENT) {
if (!selector.name) {
throwError('.findAll() requires component to have a name property')
}
if (selectorType === selectorTypes.VUE_COMPONENT ||
selectorType === selectorTypes.NAME_SELECTOR) {
const root = this.vm || this.vnode
const components = findVueComponents(root, selector.name)
// $FlowIgnore warning about selectorType being undefined
const components = findVueComponents(root, selectorType, selector)
return new WrapperArray(components.map(component => new VueWrapper(component, this.options)))
}

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

if (selectorType === selectorTypes.VUE_COMPONENT && this.vm) {
if (typeof selector.name !== 'string') {
throwError('a Component used as a selector must have a name property')
if (selectorType === selectorTypes.NAME_SELECTOR) {
if (!this.vm) {
return false
}
return vmCtorMatchesName(this.vm, selector.name)
}

if (selectorType === selectorTypes.OPTIONS_OBJECT) {
if (selectorType === selectorTypes.REF_SELECTOR) {
throwError('$ref selectors can not be used with wrapper.is()')
}

Expand Down
22 changes: 15 additions & 7 deletions test/unit/specs/mount/Wrapper/find.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,6 @@ describe('find', () => {
expect(span.find(AComponent).exists()).to.equal(false)
})

it('throws error if component does not have a name property', () => {
const wrapper = mount(Component)
const message = '[vue-test-utils]: .find() requires component to have a name property'
const fn = () => wrapper.find(ComponentWithoutName)
expect(fn).to.throw().with.property('message', message)
})

it('returns empty Wrapper with error if no nodes are found', () => {
const wrapper = mount(Component)
const selector = 'pre'
Expand Down Expand Up @@ -211,6 +204,21 @@ describe('find', () => {
expect(error.selector).to.equal('ref="foo"')
})

it('returns Wrapper matching component that has no name property', () => {
const TestComponent = {
template: `
<div>
<component-without-name />
</div>
`,
components: {
ComponentWithoutName
}
}
const wrapper = mount(TestComponent)
expect(wrapper.find(ComponentWithoutName).exists()).to.equal(true)
})

it('throws an error if selector is not a valid selector', () => {
const wrapper = mount(Component)
const invalidSelectors = [
Expand Down
20 changes: 16 additions & 4 deletions test/unit/specs/mount/Wrapper/findAll.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,22 @@ describe('findAll', () => {
const span = wrapper.find('span')
expect(span.findAll(AComponent).length).to.equal(1)
})
it('throws an error if component does not have a name property', () => {
const wrapper = mount(Component)
const message = '[vue-test-utils]: .findAll() requires component to have a name property'
expect(() => wrapper.findAll(ComponentWithoutName)).to.throw().with.property('message', message)

it('returns matching Vue components that have no name property', () => {
const TestComponent = {
template: `
<div>
<component-without-name />
<component-without-name />
<component-without-name />
</div>
`,
components: {
ComponentWithoutName
}
}
const wrapper = mount(TestComponent)
expect(wrapper.findAll(ComponentWithoutName).length).to.equal(3)
})

it('returns VueWrapper with length 0 if no nodes matching selector are found', () => {
Expand Down
15 changes: 6 additions & 9 deletions test/unit/specs/mount/Wrapper/is.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { compileToFunctions } from 'vue-template-compiler'
import { mount } from '~vue-test-utils'
import ComponentWithChild from '~resources/components/component-with-child.vue'
import Component from '~resources/components/component.vue'
import ComponentWithoutName from '~resources/components/component-without-name.vue'

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

it.skip('returns true if root node matches Component without a name', () => {
const wrapper = mount(ComponentWithoutName)
expect(wrapper.is(ComponentWithoutName)).to.equal(true)
})

it('returns false if root node is not a Vue Component', () => {
const wrapper = mount(ComponentWithChild)
const input = wrapper.findAll('span').at(0)
Expand Down Expand Up @@ -72,15 +78,6 @@ describe('is', () => {
expect(fn).to.throw().with.property('message', message)
})

it('throws error if component passed to use as identifier does not have a name', () => {
const compiled = compileToFunctions('<div />')
const wrapper = mount(compiled)

const message = '[vue-test-utils]: a Component used as a selector must have a name property'
const fn = () => wrapper.is({ render: () => {} })
expect(fn).to.throw().with.property('message', message)
})

it('throws an error if selector is not a valid selector', () => {
const compiled = compileToFunctions('<div />')
const wrapper = mount(compiled)
Expand Down

0 comments on commit df706fe

Please sign in to comment.