Skip to content

Wrapper: find/findAll accepts ref options object #68

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 10, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion flow/wrapper.flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import type Wrapper from '~src/Wrapper'
import type WrapperArray from '~src/WrapperArray'

declare type Selector = string | Component
declare type Selector = any

declare interface BaseWrapper { // eslint-disable-line no-undef
at(index: number): Wrapper | void,
Expand Down
38 changes: 0 additions & 38 deletions src/lib/find-matching-vnodes.js

This file was deleted.

15 changes: 15 additions & 0 deletions src/lib/find-vnodes-by-ref.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// @flow

import { removeDuplicateNodes, findAllVNodes } from './vnode-utils'

function nodeMatchesRef (node: VNode, refName: string): boolean {
return node.data && node.data.ref === refName
}

export default function findVNodesByRef (vNode: VNode, refName: string): Array<VNode> {
const nodes = findAllVNodes(vNode)
const refFilteredNodes = nodes.filter(node => nodeMatchesRef(node, refName))
// Only return refs defined on top-level VNode to provide the same behavior as selecting via vm.$ref.{someRefName}
const mainVNodeFilteredNodes = refFilteredNodes.filter(node => !!vNode.context.$refs[node.data.ref])
return removeDuplicateNodes(mainVNodeFilteredNodes)
}
13 changes: 13 additions & 0 deletions src/lib/find-vnodes-by-selector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// @flow

import { removeDuplicateNodes, findAllVNodes } from './vnode-utils'

function nodeMatchesSelector (node: VNode, selector: string): boolean {
return node.elm && node.elm.getAttribute && node.elm.matches(selector)
}

export default function findVNodesBySelector (vNode: VNode, selector: string): Array<VNode> {
const nodes = findAllVNodes(vNode)
const filteredNodes = nodes.filter(node => nodeMatchesSelector(node, selector))
return removeDuplicateNodes(filteredNodes)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file includes all non-shared functionality from find-matching-vnodes

32 changes: 32 additions & 0 deletions src/lib/get-selector-type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// @flow

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

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

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

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

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

export default function getSelectorTypeOrThrow (selector: Selector, methodName: string): string | void {
const selectorType = getSelectorType(selector)
if (!selectorType) {
throwError(`wrapper.${methodName}() must be passed a valid CSS selector, Vue constructor, or valid find option object`)
}
return selectorType
}
29 changes: 28 additions & 1 deletion src/lib/validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,32 @@ export function isValidSelector (selector: any): boolean {
return true
}

return isVueComponent(selector)
if (isVueComponent(selector)) {
return true
}

return isRefSelector(selector)
}

export function isRefSelector (refOptionsObject: any) {
if (typeof refOptionsObject !== 'object') {
return false
}

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

const validFindKeys = ['ref']
const entries = Object.entries(refOptionsObject)

if (!entries.length) {
return false
}

const isValid = entries.every(([key, value]) => {
return validFindKeys.includes(key) && typeof value === 'string'
})

return isValid
}
28 changes: 28 additions & 0 deletions src/lib/vnode-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// @flow

export function findAllVNodes (vnode: VNode, nodes: Array<VNode> = []): Array<VNode> {
nodes.push(vnode)

if (Array.isArray(vnode.children)) {
vnode.children.forEach((childVNode) => {
findAllVNodes(childVNode, nodes)
})
}

if (vnode.child) {
findAllVNodes(vnode.child._vnode, nodes)
}

return nodes
}

export function removeDuplicateNodes (vNodes: Array<VNode>): Array<VNode> {
const uniqueNodes = []
vNodes.forEach((vNode) => {
const exists = uniqueNodes.some(node => vNode.elm === node.elm)
if (!exists) {
uniqueNodes.push(vNode)
}
})
return uniqueNodes
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file includes all shared functionality from find-vnodes-by-selector and find-vnodes-by-ref

75 changes: 50 additions & 25 deletions src/wrappers/wrapper.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// @flow

import Vue from 'vue'
import { isValidSelector } from '../lib/validators'
import getSelectorTypeOrThrow, { selectorTypes } from '../lib/get-selector-type'
import findVueComponents, { vmCtorMatchesName } from '../lib/find-vue-components'
import findMatchingVNodes from '../lib/find-matching-vnodes'
import findVNodesBySelector from '../lib/find-vnodes-by-selector'
import findVNodesByRef from '../lib/find-vnodes-by-ref'
import VueWrapper from './vue-wrapper'
import WrapperArray from './wrapper-array'
import ErrorWrapper from './error-wrapper'
Expand Down Expand Up @@ -36,16 +37,22 @@ export default class Wrapper implements BaseWrapper {
* Checks if wrapper contains provided selector.
*/
contains (selector: Selector) {
if (!isValidSelector(selector)) {
throwError('wrapper.contains() must be passed a valid CSS selector or a Vue constructor')
}
const selectorType = getSelectorTypeOrThrow(selector, 'contains')

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

if (typeof selector === 'string' && this.element instanceof HTMLElement) {
if (selectorType === selectorTypes.OPTIONS_OBJECT) {
if (!this.isVueComponent) {
throwError('$ref selectors can only be used on Vue component wrappers')
}
const nodes = findVNodesByRef(this.vnode, selector.ref)
return nodes.length > 0
}

if (selectorType === selectorTypes.DOM_SELECTOR && this.element instanceof HTMLElement) {
return this.element.querySelectorAll(selector).length > 0
}

Expand Down Expand Up @@ -174,12 +181,10 @@ export default class Wrapper implements BaseWrapper {
/**
* Finds first node in tree of the current wrapper that matches the provided selector.
*/
find (selector: string): Wrapper | ErrorWrapper | VueWrapper {
if (!isValidSelector(selector)) {
throwError('wrapper.find() must be passed a valid CSS selector or a Vue constructor')
}
find (selector: Selector): Wrapper | ErrorWrapper | VueWrapper {
const selectorType = getSelectorTypeOrThrow(selector, 'find')

if (typeof selector === 'object') {
if (selectorType === selectorTypes.VUE_COMPONENT) {
if (!selector.name) {
throwError('.find() requires component to have a name property')
}
Expand All @@ -191,7 +196,18 @@ export default class Wrapper implements BaseWrapper {
return new VueWrapper(components[0], this.options)
}

const nodes = findMatchingVNodes(this.vnode, selector)
if (selectorType === selectorTypes.OPTIONS_OBJECT) {
if (!this.isVueComponent) {
throwError('$ref selectors can only be used on Vue component wrappers')
}
const nodes = findVNodesByRef(this.vnode, selector.ref)
if (nodes.length === 0) {
return new ErrorWrapper(`ref="${selector.ref}"`)
}
return new Wrapper(nodes[0], this.update, this.options)
}

const nodes = findVNodesBySelector(this.vnode, selector)

if (nodes.length === 0) {
return new ErrorWrapper(selector)
Expand All @@ -203,11 +219,9 @@ export default class Wrapper implements BaseWrapper {
* Finds node in tree of the current wrapper that matches the provided selector.
*/
findAll (selector: Selector): WrapperArray {
if (!isValidSelector(selector)) {
throwError('wrapper.findAll() must be passed a valid CSS selector or a Vue constructor')
}
const selectorType = getSelectorTypeOrThrow(selector, 'findAll')

if (typeof selector === 'object') {
if (selectorType === selectorTypes.VUE_COMPONENT) {
if (!selector.name) {
throwError('.findAll() requires component to have a name property')
}
Expand All @@ -216,11 +230,19 @@ export default class Wrapper implements BaseWrapper {
return new WrapperArray(components.map(component => new VueWrapper(component, this.options)))
}

if (selectorType === selectorTypes.OPTIONS_OBJECT) {
if (!this.isVueComponent) {
throwError('$ref selectors can only be used on Vue component wrappers')
}
const nodes = findVNodesByRef(this.vnode, selector.ref)
return new WrapperArray(nodes.map(node => new Wrapper(node, this.update, this.options)))
}

function nodeMatchesSelector (node, selector) {
return node.elm && node.elm.getAttribute && node.elm.matches(selector)
}

const nodes = findMatchingVNodes(this.vnode, selector)
const nodes = findVNodesBySelector(this.vnode, selector)
const matchingNodes = nodes.filter(node => nodeMatchesSelector(node, selector))

return new WrapperArray(matchingNodes.map(node => new Wrapper(node, this.update, this.options)))
Expand All @@ -237,20 +259,23 @@ export default class Wrapper implements BaseWrapper {
* Checks if node matches selector
*/
is (selector: Selector): boolean {
if (!isValidSelector(selector)) {
throwError('wrapper.is() must be passed a valid CSS selector or a Vue constructor')
}
const selectorType = getSelectorTypeOrThrow(selector, 'is')

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

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

if (typeof selector === 'object') {
return false
}

return !!(this.element &&
this.element.getAttribute &&
this.element.matches(selector))
Expand Down
2 changes: 1 addition & 1 deletion test/resources/components/component-with-child.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div>
<span>
<child-component />
<child-component ref="child"/>
</span>
</div>
</template>
Expand Down
2 changes: 1 addition & 1 deletion test/resources/components/component-with-v-for.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div>
<AComponent v-for="item in items" :key="item.id" />
<AComponent v-for="item in items" :key="item.id" ref="item"/>
</div>
</template>

Expand Down
25 changes: 23 additions & 2 deletions test/unit/specs/mount/Wrapper/contains.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,40 @@ describe('contains', () => {
expect(wrapper.contains(Component)).to.equal(true)
})

it('returns true if wrapper contains element specified by ref selector', () => {
const compiled = compileToFunctions('<div><input ref="foo" /></div>')
const wrapper = mount(compiled)
expect(wrapper.contains({ ref: 'foo' })).to.equal(true)
})

it('throws an error when ref selector is called on a wrapper that is not a Vue component', () => {
const compiled = compileToFunctions('<div><a href="/"></a></div>')
const wrapper = mount(compiled)
const a = wrapper.find('a')
const message = '[vue-test-utils]: $ref selectors can only be used on Vue component wrappers'
const fn = () => a.contains({ ref: 'foo' })
expect(fn).to.throw().with.property('message', message)
})

it('returns false if wrapper does not contain element', () => {
const compiled = compileToFunctions('<div><input /></div>')
const wrapper = mount(compiled)
expect(wrapper.contains('doesntexist')).to.equal(false)
})

it('returns false if wrapper does not contain element specified by ref selector', () => {
const compiled = compileToFunctions('<div><input ref="bar" /></div>')
const wrapper = mount(compiled)
expect(wrapper.contains({ ref: 'foo' })).to.equal(false)
})

it('throws an error if selector is not a valid selector', () => {
const wrapper = mount(Component)
const invalidSelectors = [
undefined, null, NaN, 0, 2, true, false, () => {}, {}, { name: undefined }, []
undefined, null, NaN, 0, 2, true, false, () => {}, {}, { name: undefined }, { ref: 'foo', nope: true }, []
]
invalidSelectors.forEach((invalidSelector) => {
const message = '[vue-test-utils]: wrapper.contains() must be passed a valid CSS selector or a Vue constructor'
const message = '[vue-test-utils]: wrapper.contains() must be passed a valid CSS selector, Vue constructor, or valid find option object'
const fn = () => wrapper.contains(invalidSelector)
expect(fn).to.throw().with.property('message', message)
})
Expand Down
Loading