Skip to content

Allow find to work with component tag #897

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

Closed
wants to merge 4 commits into from
Closed
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
26 changes: 26 additions & 0 deletions packages/shared/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,32 @@ export const camelize = (str: string): string => {
return camelizedStr.charAt(0).toLowerCase() + camelizedStr.slice(1)
}

export const htmlTags = [
'html', 'body', 'base', 'head', 'link', 'meta', 'style', 'title',
'address', 'article', 'aside', 'footer', 'header', 'h1', 'h2', 'h3',
'h4', 'h5', 'h6', 'hgroup', 'nav', 'section', 'div', 'dd', 'dl', 'dt',
'figcaption', 'figure', 'picture', 'hr', 'img', 'li', 'main', 'ol',
'p', 'pre', 'ul', 'a', 'b', 'abbr', 'bdi', 'bdo', 'br', 'cite', 'code',
'data', 'dfn', 'em', 'i', 'kbd', 'mark', 'q', 'rp', 'rt', 'rtc', 'ruby',
's', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'time',
'u', 'var', 'wbr', 'area', 'audio', 'map', 'track', 'video',
'embed', 'object', 'param', 'source', 'canvas', 'script',
'noscript', 'del', 'ins', 'caption', 'col', 'colgroup', 'table',
'thead', 'tbody', 'td', 'th', 'tr', 'button', 'datalist', 'fieldset',
'form', 'input', 'label', 'legend', 'meter', 'optgroup', 'option',
'output', 'progress', 'select', 'textarea', 'details', 'dialog', 'menu',
'menuitem', 'summary', 'content', 'element', 'shadow', 'template',
'blockquote', 'iframe', 'tfoot'
]

export const svgElements = [
'svg', 'animate', 'circle', 'clippath', 'cursor', 'defs', 'desc', 'ellipse',
'filter', 'font-face', 'foreignObject', 'g', 'glyph', 'image', 'line',
'marker', 'mask', 'missing-glyph', 'path', 'pattern', 'polygon',
'polyline', 'rect', 'switch', 'symbol', 'text', 'textpath', 'tspan', 'use',
'view'
]

/**
* Capitalize a string.
*/
Expand Down
47 changes: 46 additions & 1 deletion packages/shared/validators.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
// @flow
import { throwError, capitalize, camelize, hyphenate } from './util'
// import concat from 'lodash/concat'
import {
camelize,
capitalize,
htmlTags,
hyphenate,
svgElements,
throwError
} from './util'

export function isDomSelector (selector: any): boolean {
if (typeof selector !== 'string') {
Expand Down Expand Up @@ -76,6 +84,43 @@ export function isNameSelector (nameOptionsObject: any): boolean {
return !!nameOptionsObject.name
}

export function isTagSelector (selector: any) {
if (typeof selector !== 'string') {
return false
}
const pseudoSelectors = ['.', '#', '[', ':', '>', ' ']

if (htmlTags.includes(selector) ||
svgElements.includes(selector) ||
selector.split('').some(char => pseudoSelectors.includes(char))) {
/* TODO: Throw error for cases such as find("a > my-component")
const tags = selector.split(/\.|\[|\s|>|#|:/).filter(tag => tag.length > 0)
const componentTags =
tags.filter(tag => {
return !concat(htmlTags, pseudoSelectors, svgElements).includes(tag)
})

componentTags.forEach(tag => {
const selectorWithoutWhitespace = selector.replace(/\s/g, '')
const tagIndex = selectorWithoutWhitespace.indexOf(tag)
if (
pseudoSelectors.includes(
selectorWithoutWhitespace[tagIndex + tag.length]) ||
(tagIndex !== 0 &&
pseudoSelectors.includes(
selectorWithoutWhitespace[tagIndex - 1]))
) {
throwError(
'Pseudo selectors cannot be used with component tag selectors')
}
})
*/
return false
} else {
return true
}
}

export function templateContainsComponent (
template: string,
name: string
Expand Down
1 change: 1 addition & 0 deletions packages/test-utils/src/consts.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Vue from 'vue'
export const NAME_SELECTOR = 'NAME_SELECTOR'
export const COMPONENT_SELECTOR = 'COMPONENT_SELECTOR'
export const REF_SELECTOR = 'REF_SELECTOR'
export const TAG_SELECTOR = 'TAG_SELECTOR'
export const DOM_SELECTOR = 'DOM_SELECTOR'
export const VUE_VERSION = Number(
`${Vue.version.split('.')[0]}.${Vue.version.split('.')[1]}`
Expand Down
17 changes: 17 additions & 0 deletions packages/test-utils/src/find-vue-components.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ export function vmFunctionalCtorMatchesSelector (
return Ctors.some(c => Ctor[c] === component[FUNCTIONAL_OPTIONS]._Ctor[c])
}

function findComponentsByTag (
selector: string,
components: Array<Component> = []
): Array<Component> {
return components.filter(component =>
component.$options._componentTag === selector
)
}

export default function findVueComponents (
root: Component,
selectorType: ?string,
Expand All @@ -111,6 +120,14 @@ export default function findVueComponents (
const components = root._isVue
? findAllVueComponentsFromVm(root)
: findAllVueComponentsFromVnode(root)

if (typeof selector === 'string') {
const componentsWithMatchingTag = findComponentsByTag(selector, components)
if (componentsWithMatchingTag.length > 0) {
return componentsWithMatchingTag
}
}

return components.filter(component => {
if (!component.$vnode && !component.$options.extends) {
return false
Expand Down
11 changes: 9 additions & 2 deletions packages/test-utils/src/find.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
import findVnodes from './find-vnodes'
import findVueComponents from './find-vue-components'
import findDOMNodes from './find-dom-nodes'
import { COMPONENT_SELECTOR, NAME_SELECTOR, DOM_SELECTOR } from './consts'
import {
COMPONENT_SELECTOR,
DOM_SELECTOR,
NAME_SELECTOR,
TAG_SELECTOR
} from './consts'
import Vue from 'vue'
import getSelectorTypeOrThrow from './get-selector-type'
import { throwError } from 'shared/util'
Expand All @@ -24,7 +29,9 @@ export default function find (
)
}

if (selectorType === COMPONENT_SELECTOR || selectorType === NAME_SELECTOR) {
if (selectorType === COMPONENT_SELECTOR ||
selectorType === NAME_SELECTOR ||
selectorType === TAG_SELECTOR) {
const root = vm || vnode
if (!root) {
return []
Expand Down
5 changes: 4 additions & 1 deletion packages/test-utils/src/get-selector-type.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,23 @@ import {
isDomSelector,
isNameSelector,
isRefSelector,
isTagSelector,
isVueComponent
} from 'shared/validators'
import { throwError } from 'shared/util'
import {
REF_SELECTOR,
COMPONENT_SELECTOR,
NAME_SELECTOR,
DOM_SELECTOR
DOM_SELECTOR,
TAG_SELECTOR
} from './consts'

export default function getSelectorTypeOrThrow (
selector: Selector,
methodName: string
): string | void {
if (isTagSelector(selector)) return TAG_SELECTOR
if (isDomSelector(selector)) return DOM_SELECTOR
if (isNameSelector(selector)) return NAME_SELECTOR
if (isVueComponent(selector)) return COMPONENT_SELECTOR
Expand Down
74 changes: 74 additions & 0 deletions test/specs/wrapper/find.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,80 @@ describeWithShallowAndMount('find', mountingMethod => {
expect(wrapper.find('p').vm).to.equal(undefined)
})

it('returns Wrapper matching component tag passed', () => {
const wrapper = mountingMethod(ComponentWithChild)
expect(wrapper.find('child-component').vnode).to.be.an('object')
})

it('returns Wrapper matching sub class component tag passed', () => {
const ChildComponent = Vue.extend({
template: '<div />'
})
const TestComponent = {
template: '<child-component />',
components: { ChildComponent }
}
const wrapper = mountingMethod(TestComponent)
expect(wrapper.find('child-component').vnode).to.be.an('object')
})

it('returns Wrapper matching extended component tag passed', () => {
const BaseComponent = {
template: '<div><a-component /></div>',
components: {
AComponent: Component
}
}
const TestComponent = {
extends: BaseComponent,
name: 'test-component'
}
const wrapper = mountingMethod(TestComponent)
expect(wrapper.find('a-component').exists()).to.equal(true)
expect(wrapper.find('a-component').isVueInstance()).to.equal(true)
})

xit('returns Wrapper matching functional component tag passed', () => {
if (!functionalSFCsSupported) {
return
}
const TestComponent = {
template: `
<div>
<functional-component />
</div>
`,
components: {
FunctionalComponent
}
}

const wrapper = mountingMethod(TestComponent)
console.log(wrapper.html())
expect(wrapper.find('functional-component').vnode).to.be.an('object')
expect(wrapper.find('functional-component').vm).to.equal(undefined)
})

it('throws an error when passed a hierarchy pseudo selector and component tag', () => {
const wrapper = mountingMethod(ComponentWithChild)
const message =
'[vue-test-utils]: Pseudo selectors cannot be used with component tag selectors'
const fn = () => wrapper.find('a > child-component')
expect(fn)
.to.throw()
.with.property('message', message)
})

it('throws an error when passed a class pseudo selector and component tag', () => {
const wrapper = mountingMethod(ComponentWithChild)
const message =
'[vue-test-utils]: Pseudo selectors cannot be used with component tag selectors'
const fn = () => wrapper.find('child-component.class-name')
expect(fn)
.to.throw()
.with.property('message', message)
})

it('returns Wrapper matching class selector passed', () => {
const compiled = compileToFunctions('<div><div class="foo" /></div>')
const wrapper = mountingMethod(compiled)
Expand Down