diff --git a/packages/shared/util.js b/packages/shared/util.js index 1b78c8506..e4df811e8 100644 --- a/packages/shared/util.js +++ b/packages/shared/util.js @@ -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. */ diff --git a/packages/shared/validators.js b/packages/shared/validators.js index 8259e753f..bac7519d5 100644 --- a/packages/shared/validators.js +++ b/packages/shared/validators.js @@ -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') { @@ -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 diff --git a/packages/test-utils/src/consts.js b/packages/test-utils/src/consts.js index 276c020f8..6da066747 100644 --- a/packages/test-utils/src/consts.js +++ b/packages/test-utils/src/consts.js @@ -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]}` diff --git a/packages/test-utils/src/find-vue-components.js b/packages/test-utils/src/find-vue-components.js index d9858c49b..63c2cd3dc 100644 --- a/packages/test-utils/src/find-vue-components.js +++ b/packages/test-utils/src/find-vue-components.js @@ -91,6 +91,15 @@ export function vmFunctionalCtorMatchesSelector ( return Ctors.some(c => Ctor[c] === component[FUNCTIONAL_OPTIONS]._Ctor[c]) } +function findComponentsByTag ( + selector: string, + components: Array = [] +): Array { + return components.filter(component => + component.$options._componentTag === selector + ) +} + export default function findVueComponents ( root: Component, selectorType: ?string, @@ -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 diff --git a/packages/test-utils/src/find.js b/packages/test-utils/src/find.js index 1217343d4..bafdfa038 100644 --- a/packages/test-utils/src/find.js +++ b/packages/test-utils/src/find.js @@ -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' @@ -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 [] diff --git a/packages/test-utils/src/get-selector-type.js b/packages/test-utils/src/get-selector-type.js index a4b25473c..cddb91173 100644 --- a/packages/test-utils/src/get-selector-type.js +++ b/packages/test-utils/src/get-selector-type.js @@ -4,6 +4,7 @@ import { isDomSelector, isNameSelector, isRefSelector, + isTagSelector, isVueComponent } from 'shared/validators' import { throwError } from 'shared/util' @@ -11,13 +12,15 @@ 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 diff --git a/test/specs/wrapper/find.spec.js b/test/specs/wrapper/find.spec.js index a45e8fc53..4487356fc 100644 --- a/test/specs/wrapper/find.spec.js +++ b/test/specs/wrapper/find.spec.js @@ -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: '
' + }) + const TestComponent = { + template: '', + 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: '
', + 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: ` +
+ +
+ `, + 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('
') const wrapper = mountingMethod(compiled)