Skip to content

Commit 65023b0

Browse files
committed
fix: add support for DOM nodes outside of VDOM
fixes #361
1 parent 4e75559 commit 65023b0

File tree

9 files changed

+198
-17
lines changed

9 files changed

+198
-17
lines changed

src/lib/find-dom-nodes.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// @flow
2+
3+
export default function findVnodes (
4+
element: Element | null,
5+
selector: string
6+
): Array<VNode> {
7+
const nodes = []
8+
if (!element || !element.querySelectorAll || !element.matches) {
9+
return nodes
10+
}
11+
12+
if (element.matches(selector)) {
13+
nodes.push(element)
14+
}
15+
// $FlowIgnore
16+
return nodes.concat([].slice.call(element.querySelectorAll(selector)))
17+
}

src/lib/find.js

+25-4
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,47 @@
22

33
import findVnodes from './find-vnodes'
44
import findVueComponents from './find-vue-components'
5+
import findDOMNodes from './find-dom-nodes'
56
import {
67
COMPONENT_SELECTOR,
7-
NAME_SELECTOR
8+
NAME_SELECTOR,
9+
DOM_SELECTOR
810
} from './consts'
911
import Vue from 'vue'
12+
import getSelectorTypeOrThrow from './get-selector-type'
13+
import { throwError } from './util'
1014

1115
export default function find (
1216
vm: Component | null,
13-
vnode: VNode,
14-
selectorType: ?string,
17+
vnode: VNode | null,
18+
element: Element,
1519
selector: Selector
1620
): Array<VNode | Component> {
21+
const selectorType = getSelectorTypeOrThrow(selector, 'find')
22+
23+
if (!vnode && !vm && selectorType !== DOM_SELECTOR) {
24+
throwError('cannot find a Vue instance on a DOM node. The node you are calling find on does not exist in the VDom. Are you adding the node as innerHTML?')
25+
}
26+
1727
if (selectorType === COMPONENT_SELECTOR || selectorType === NAME_SELECTOR) {
1828
const root = vm || vnode
29+
if (!root) {
30+
return []
31+
}
1932
return findVueComponents(root, selectorType, selector)
2033
}
2134

2235
if (vm && vm.$refs && selector.ref in vm.$refs && vm.$refs[selector.ref] instanceof Vue) {
2336
return [vm.$refs[selector.ref]]
2437
}
2538

26-
return findVnodes(vnode, vm, selectorType, selector)
39+
if (vnode) {
40+
const nodes = findVnodes(vnode, vm, selectorType, selector)
41+
if (selectorType !== DOM_SELECTOR) {
42+
return nodes
43+
}
44+
return nodes.length > 0 ? nodes : findDOMNodes(element, selector)
45+
}
46+
47+
return findDOMNodes(element, selector)
2748
}

src/wrappers/wrapper.js

+24-12
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,24 @@ import findAll from '../lib/find'
2020
import createWrapper from './create-wrapper'
2121

2222
export default class Wrapper implements BaseWrapper {
23-
vnode: VNode;
23+
vnode: VNode | null;
2424
vm: Component | null;
2525
_emitted: { [name: string]: Array<Array<any>> };
2626
_emittedByOrder: Array<{ name: string; args: Array<any> }>;
2727
isVueComponent: boolean;
28-
element: HTMLElement;
28+
element: Element;
2929
update: Function;
3030
options: WrapperOptions;
3131
version: number
3232

33-
constructor (vnode: VNode, update: Function, options: WrapperOptions) {
34-
this.vnode = vnode
35-
this.element = vnode.elm
33+
constructor (node: VNode | Element, update: Function, options: WrapperOptions) {
34+
if (node instanceof Element) {
35+
this.element = node
36+
this.vnode = null
37+
} else {
38+
this.vnode = node
39+
this.element = node.elm
40+
}
3641
this.update = update
3742
this.options = options
3843
this.version = Number(`${Vue.version.split('.')[0]}.${Vue.version.split('.')[1]}`)
@@ -82,7 +87,7 @@ export default class Wrapper implements BaseWrapper {
8287
*/
8388
contains (selector: Selector) {
8489
const selectorType = getSelectorTypeOrThrow(selector, 'contains')
85-
const nodes = findAll(this.vm, this.vnode, selectorType, selector)
90+
const nodes = findAll(this.vm, this.vnode, this.element, selector)
8691
const is = selectorType === REF_SELECTOR ? false : this.is(selector)
8792
return nodes.length > 0 || is
8893
}
@@ -222,14 +227,15 @@ export default class Wrapper implements BaseWrapper {
222227
const body = document.querySelector('body')
223228
const mockElement = document.createElement('div')
224229

225-
if (!(body instanceof HTMLElement)) {
230+
if (!(body instanceof Element)) {
226231
return false
227232
}
228233
const mockNode = body.insertBefore(mockElement, null)
229234
// $FlowIgnore : Flow thinks style[style] returns a number
230235
mockElement.style[style] = value
231236

232-
if (!this.options.attachedToDocument) {
237+
if (!this.options.attachedToDocument && (this.vm || this.vnode)) {
238+
// $FlowIgnore : Possible null value, will be removed in 1.0.0
233239
const vm = this.vm || this.vnode.context.$root
234240
body.insertBefore(vm.$root._vnode.elm, null)
235241
}
@@ -243,8 +249,7 @@ export default class Wrapper implements BaseWrapper {
243249
* Finds first node in tree of the current wrapper that matches the provided selector.
244250
*/
245251
find (selector: Selector): Wrapper | ErrorWrapper | VueWrapper {
246-
const selectorType = getSelectorTypeOrThrow(selector, 'find')
247-
const nodes = findAll(this.vm, this.vnode, selectorType, selector)
252+
const nodes = findAll(this.vm, this.vnode, this.element, selector)
248253
if (nodes.length === 0) {
249254
if (selector.ref) {
250255
return new ErrorWrapper(`ref="${selector.ref}"`)
@@ -258,8 +263,8 @@ export default class Wrapper implements BaseWrapper {
258263
* Finds node in tree of the current wrapper that matches the provided selector.
259264
*/
260265
findAll (selector: Selector): WrapperArray {
261-
const selectorType = getSelectorTypeOrThrow(selector, 'findAll')
262-
const nodes = findAll(this.vm, this.vnode, selectorType, selector)
266+
getSelectorTypeOrThrow(selector, 'findAll')
267+
const nodes = findAll(this.vm, this.vnode, this.element, selector)
263268
const wrappers = nodes.map(node =>
264269
createWrapper(node, this.update, this.options)
265270
)
@@ -313,6 +318,9 @@ export default class Wrapper implements BaseWrapper {
313318
* Checks if node is empty
314319
*/
315320
isEmpty (): boolean {
321+
if (!this.vnode) {
322+
return this.element.innerHTML === ''
323+
}
316324
return this.vnode.children === undefined || this.vnode.children.length === 0
317325
}
318326

@@ -331,6 +339,10 @@ export default class Wrapper implements BaseWrapper {
331339
return this.vm.$options.name
332340
}
333341

342+
if (!this.vnode) {
343+
return this.element.tagName
344+
}
345+
334346
return this.vnode.tag
335347
}
336348

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

+15
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,21 @@ describe('contains', () => {
111111
expect(wrapper.contains({ ref: 'foo' })).to.equal(false)
112112
})
113113

114+
it('works correctly with innerHTML', () => {
115+
const TestComponent = {
116+
render (createElement) {
117+
return createElement('div', {
118+
domProps: {
119+
innerHTML: '<svg></svg>'
120+
}
121+
})
122+
}
123+
}
124+
const wrapper = mount(TestComponent)
125+
expect(wrapper.contains('svg')).to.equal(true)
126+
expect(wrapper.find('svg').contains('svg')).to.equal(true)
127+
})
128+
114129
it('throws an error if selector is not a valid selector', () => {
115130
const wrapper = mount(Component)
116131
const invalidSelectors = [

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

+44
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,50 @@ describe('find', () => {
163163
}
164164
})
165165

166+
it('works correctly with innerHTML', () => {
167+
const TestComponent = {
168+
render (createElement) {
169+
return createElement('div', {
170+
domProps: {
171+
innerHTML: '<svg></svg>'
172+
}
173+
})
174+
}
175+
}
176+
const wrapper = mount(TestComponent)
177+
expect(wrapper.find('svg').find('svg').exists()).to.equal(true)
178+
})
179+
180+
it('throws errror when searching for a component on an element Wrapper', () => {
181+
const TestComponent = {
182+
render (createElement) {
183+
return createElement('div', {
184+
domProps: {
185+
innerHTML: '<svg></svg>'
186+
}
187+
})
188+
}
189+
}
190+
const fn = () => mount(TestComponent).find('svg').find(Component)
191+
const message = '[vue-test-utils]: cannot find a Vue instance on a DOM node. The node you are calling find on does not exist in the VDom. Are you adding the node as innerHTML?'
192+
expect(fn).to.throw().with.property('message', message)
193+
})
194+
195+
it('throws errror when using ref selector on an element Wrapper', () => {
196+
const TestComponent = {
197+
render (createElement) {
198+
return createElement('div', {
199+
domProps: {
200+
innerHTML: '<svg></svg>'
201+
}
202+
})
203+
}
204+
}
205+
const fn = () => mount(TestComponent).find('svg').find({ ref: 'some-ref' })
206+
const message = '[vue-test-utils]: cannot find a Vue instance on a DOM node. The node you are calling find on does not exist in the VDom. Are you adding the node as innerHTML?'
207+
expect(fn).to.throw().with.property('message', message)
208+
})
209+
166210
it('returns correct number of Vue Wrappers when component has a v-for', () => {
167211
const items = [{ id: 1 }, { id: 2 }, { id: 3 }]
168212
const wrapper = mount(ComponentWithVFor, { propsData: { items }})

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

+14
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,20 @@ describe('findAll', () => {
6262
expect(wrapper.findAll('p').length).to.equal(3)
6363
})
6464

65+
it('works correctly with innerHTML', () => {
66+
const TestComponent = {
67+
render (createElement) {
68+
return createElement('div', {
69+
domProps: {
70+
innerHTML: '<svg></svg>'
71+
}
72+
})
73+
}
74+
}
75+
const wrapper = mount(TestComponent)
76+
expect(wrapper.findAll('svg').length).to.equal(1)
77+
})
78+
6579
it('returns an array of Wrappers of elements matching id selector passed', () => {
6680
const compiled = compileToFunctions('<div><div id="foo" /></div>')
6781
const wrapper = mount(compiled)

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

+14
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,20 @@ describe('is', () => {
4848
expect(wrapper.is(ComponentWithoutName)).to.equal(true)
4949
})
5050

51+
it('works correctly with innerHTML', () => {
52+
const TestComponent = {
53+
render (createElement) {
54+
return createElement('div', {
55+
domProps: {
56+
innerHTML: '<svg></svg>'
57+
}
58+
})
59+
}
60+
}
61+
const wrapper = mount(TestComponent)
62+
expect(wrapper.find('svg').is('svg')).to.equal(true)
63+
})
64+
5165
it('returns true if root node matches functional Component', () => {
5266
if (!functionalSFCsSupported()) {
5367
return

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

+31
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,37 @@ describe('isEmpty', () => {
99
expect(wrapper.isEmpty()).to.equal(true)
1010
})
1111

12+
it('returns true if innerHTML is empty', () => {
13+
const TestComponent = {
14+
render (createElement) {
15+
return createElement('div', {
16+
domProps: {
17+
innerHTML: '<svg />'
18+
}
19+
})
20+
}
21+
}
22+
const wrapper = mount(TestComponent)
23+
expect(wrapper.find('svg').isEmpty()).to.equal(true)
24+
})
25+
26+
it('returns false if innerHTML is not empty', () => {
27+
if (/HeadlessChrome/.test(window.navigator.userAgent)) {
28+
return
29+
}
30+
const TestComponent = {
31+
render (createElement) {
32+
return createElement('div', {
33+
domProps: {
34+
innerHTML: '<svg><p>not empty</p></svg>'
35+
}
36+
})
37+
}
38+
}
39+
const wrapper = mount(TestComponent)
40+
expect(wrapper.find('svg').isEmpty()).to.equal(false)
41+
})
42+
1243
it('returns true contains empty slot', () => {
1344
const compiled = compileToFunctions('<div><slot></slot></div>')
1445
const wrapper = mount(compiled)

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

+14-1
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,23 @@ describe('name', () => {
88
expect(wrapper.name()).to.equal('component')
99
})
1010

11+
it('returns the name of the tag if there is no vnode', () => {
12+
const TestComponent = {
13+
render (createElement) {
14+
return createElement('div', {
15+
domProps: {
16+
innerHTML: '<svg></svg>'
17+
}
18+
})
19+
}
20+
}
21+
const wrapper = mount(TestComponent)
22+
expect(wrapper.find('svg').name()).to.equal('svg')
23+
})
24+
1125
it('returns the tag name of the element if it is not a Vue component', () => {
1226
const compiled = compileToFunctions('<div><p /></div>')
1327
const wrapper = mount(compiled)
1428
expect(wrapper.find('p').name()).to.equal('p')
1529
})
1630
})
17-

0 commit comments

Comments
 (0)