Skip to content

Commit 1c3684d

Browse files
authored
feat(find): allow chaining find with findComponent (#897)
1 parent 99c0515 commit 1c3684d

File tree

5 files changed

+193
-60
lines changed

5 files changed

+193
-60
lines changed

src/baseWrapper.ts

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
import { textContent } from './utils'
22
import type { TriggerOptions } from './createDomEvent'
3-
import { nextTick } from 'vue'
3+
import {
4+
ComponentInternalInstance,
5+
ComponentPublicInstance,
6+
nextTick
7+
} from 'vue'
48
import { createDOMEvent } from './createDomEvent'
5-
import { DomEventName, DomEventNameWithModifier } from './constants/dom-events'
9+
import { DomEventNameWithModifier } from './constants/dom-events'
10+
import type { VueWrapper } from './vueWrapper'
11+
import type { DOMWrapper } from './domWrapper'
12+
import { FindAllComponentsSelector, FindComponentSelector } from './types'
613

7-
export default class BaseWrapper<ElementType extends Element> {
8-
private readonly wrapperElement: ElementType
14+
export default abstract class BaseWrapper<ElementType extends Element> {
15+
private readonly wrapperElement: ElementType & {
16+
__vueParentComponent?: ComponentInternalInstance
17+
}
918

1019
get element() {
1120
return this.wrapperElement
@@ -15,6 +24,16 @@ export default class BaseWrapper<ElementType extends Element> {
1524
this.wrapperElement = element
1625
}
1726

27+
abstract find(selector: string): DOMWrapper<Element>
28+
abstract findAll(selector: string): DOMWrapper<Element>[]
29+
abstract findComponent<T extends ComponentPublicInstance>(
30+
selector: FindComponentSelector | (new () => T)
31+
): VueWrapper<T>
32+
abstract findAllComponents(
33+
selector: FindAllComponentsSelector
34+
): VueWrapper<any>[]
35+
abstract html(): string
36+
1837
classes(): string[]
1938
classes(className: string): boolean
2039
classes(className?: string): string[] | boolean {
@@ -45,6 +64,45 @@ export default class BaseWrapper<ElementType extends Element> {
4564
return true
4665
}
4766

67+
get<K extends keyof HTMLElementTagNameMap>(
68+
selector: K
69+
): Omit<DOMWrapper<HTMLElementTagNameMap[K]>, 'exists'>
70+
get<K extends keyof SVGElementTagNameMap>(
71+
selector: K
72+
): Omit<DOMWrapper<SVGElementTagNameMap[K]>, 'exists'>
73+
get<T extends Element>(selector: string): Omit<DOMWrapper<T>, 'exists'>
74+
get(selector: string): Omit<DOMWrapper<Element>, 'exists'> {
75+
const result = this.find(selector)
76+
if (result.exists()) {
77+
return result
78+
}
79+
80+
throw new Error(`Unable to get ${selector} within: ${this.html()}`)
81+
}
82+
83+
getComponent<T extends ComponentPublicInstance>(
84+
selector: FindComponentSelector | (new () => T)
85+
): Omit<VueWrapper<T>, 'exists'> {
86+
const result = this.findComponent(selector)
87+
88+
if (result.exists()) {
89+
return result as VueWrapper<T>
90+
}
91+
92+
let message = 'Unable to get '
93+
if (typeof selector === 'string') {
94+
message += `component with selector ${selector}`
95+
} else if ('name' in selector) {
96+
message += `component with name ${selector.name}`
97+
} else if ('ref' in selector) {
98+
message += `component with ref ${selector.ref}`
99+
} else {
100+
message += 'specified component'
101+
}
102+
message += ` within: ${this.html()}`
103+
throw new Error(message)
104+
}
105+
48106
protected isDisabled = () => {
49107
const validTagsToBeDisabled = [
50108
'BUTTON',

src/domWrapper.ts

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import { isElementVisible } from './utils/isElementVisible'
33
import BaseWrapper from './baseWrapper'
44
import { createWrapperError } from './errorWrapper'
55
import WrapperLike from './interfaces/wrapperLike'
6+
import { ComponentInternalInstance, ComponentPublicInstance } from 'vue'
7+
import { FindAllComponentsSelector, FindComponentSelector } from './types'
8+
import { VueWrapper } from 'src'
9+
import { matches, find } from './utils/find'
10+
import { createWrapper } from './vueWrapper'
611

712
export class DOMWrapper<ElementType extends Element>
813
extends BaseWrapper<ElementType>
@@ -38,22 +43,6 @@ export class DOMWrapper<ElementType extends Element>
3843
return createWrapperError('DOMWrapper')
3944
}
4045

41-
get<K extends keyof HTMLElementTagNameMap>(
42-
selector: K
43-
): Omit<DOMWrapper<HTMLElementTagNameMap[K]>, 'exists'>
44-
get<K extends keyof SVGElementTagNameMap>(
45-
selector: K
46-
): Omit<DOMWrapper<SVGElementTagNameMap[K]>, 'exists'>
47-
get<T extends Element>(selector: string): Omit<DOMWrapper<T>, 'exists'>
48-
get(selector: string): Omit<DOMWrapper<Element>, 'exists'> {
49-
const result = this.find(selector)
50-
if (result instanceof DOMWrapper) {
51-
return result
52-
}
53-
54-
throw new Error(`Unable to get ${selector} within: ${this.html()}`)
55-
}
56-
5746
findAll<K extends keyof HTMLElementTagNameMap>(
5847
selector: K
5948
): DOMWrapper<HTMLElementTagNameMap[K]>[]
@@ -67,6 +56,51 @@ export class DOMWrapper<ElementType extends Element>
6756
)
6857
}
6958

59+
findComponent<T extends ComponentPublicInstance>(
60+
selector: FindComponentSelector | (new () => T)
61+
): VueWrapper<T> {
62+
const parentComponent = this.element.__vueParentComponent
63+
64+
if (!parentComponent) {
65+
return createWrapperError('VueWrapper')
66+
}
67+
68+
if (typeof selector === 'object' && 'ref' in selector) {
69+
const result = parentComponent.refs[selector.ref]
70+
if (result && !(result instanceof HTMLElement)) {
71+
return createWrapper(null, result as T)
72+
} else {
73+
return createWrapperError('VueWrapper')
74+
}
75+
}
76+
77+
if (
78+
matches(parentComponent.vnode, selector) &&
79+
this.element.contains(parentComponent.vnode.el as Node)
80+
) {
81+
return createWrapper(null, parentComponent.proxy!)
82+
}
83+
84+
const result = find(parentComponent.subTree, selector).filter((v) =>
85+
this.element.contains(v.$el)
86+
)
87+
88+
if (result.length) {
89+
return createWrapper(null, result[0])
90+
}
91+
92+
return createWrapperError('VueWrapper')
93+
}
94+
95+
findAllComponents(selector: FindAllComponentsSelector): VueWrapper<any>[] {
96+
const parentComponent: ComponentInternalInstance = (this.element as any)
97+
.__vueParentComponent
98+
99+
return find(parentComponent.subTree, selector)
100+
.filter((v) => this.element.contains(v.$el))
101+
.map((c) => createWrapper(null, c))
102+
}
103+
70104
private async setChecked(checked: boolean = true) {
71105
// typecast so we get type safety
72106
const element = this.element as unknown as HTMLInputElement

src/vueWrapper.ts

Lines changed: 1 addition & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -132,22 +132,6 @@ export class VueWrapper<T extends ComponentPublicInstance>
132132
return createWrapperError('DOMWrapper')
133133
}
134134

135-
get<K extends keyof HTMLElementTagNameMap>(
136-
selector: K
137-
): Omit<DOMWrapper<HTMLElementTagNameMap[K]>, 'exists'>
138-
get<K extends keyof SVGElementTagNameMap>(
139-
selector: K
140-
): Omit<DOMWrapper<SVGElementTagNameMap[K]>, 'exists'>
141-
get<T extends Element>(selector: string): Omit<DOMWrapper<T>, 'exists'>
142-
get(selector: string): Omit<DOMWrapper<Element>, 'exists'> {
143-
const result = this.find(selector)
144-
if (result instanceof DOMWrapper) {
145-
return result
146-
}
147-
148-
throw new Error(`Unable to get ${selector} within: ${this.html()}`)
149-
}
150-
151135
findComponent<T extends ComponentPublicInstance>(
152136
selector: FindComponentSelector | (new () => T)
153137
): VueWrapper<T> {
@@ -182,30 +166,7 @@ export class VueWrapper<T extends ComponentPublicInstance>
182166
return createWrapperError('VueWrapper')
183167
}
184168

185-
getComponent<T extends ComponentPublicInstance>(
186-
selector: FindComponentSelector | (new () => T)
187-
): Omit<VueWrapper<T>, 'exists'> {
188-
const result = this.findComponent(selector)
189-
190-
if (result instanceof VueWrapper) {
191-
return result as VueWrapper<T>
192-
}
193-
194-
let message = 'Unable to get '
195-
if (typeof selector === 'string') {
196-
message += `component with selector ${selector}`
197-
} else if ('name' in selector) {
198-
message += `component with name ${selector.name}`
199-
} else if ('ref' in selector) {
200-
message += `component with ref ${selector.ref}`
201-
} else {
202-
message += 'specified component'
203-
}
204-
message += ` within: ${this.html()}`
205-
throw new Error(message)
206-
}
207-
208-
findAllComponents(selector: FindAllComponentsSelector): VueWrapper<T>[] {
169+
findAllComponents(selector: FindAllComponentsSelector): VueWrapper<any>[] {
209170
if (typeof selector === 'string') {
210171
throw Error(
211172
'findAllComponents requires a Vue constructor or valid find object. If you are searching for DOM nodes, use `find` instead'

tests/findAllComponents.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,15 @@ describe('findAllComponents', () => {
2424
)
2525
expect(wrapper.findAllComponents(Hello)[0].text()).toBe('Hello world')
2626
})
27+
28+
it('finds all deeply nested vue components when chained from dom wrapper', () => {
29+
const Component = defineComponent({
30+
components: { Hello },
31+
template:
32+
'<div><Hello /><div class="nested"><Hello /><Hello /></div></div>'
33+
})
34+
const wrapper = mount(Component)
35+
expect(wrapper.findAllComponents(Hello)).toHaveLength(3)
36+
expect(wrapper.find('.nested').findAllComponents(Hello)).toHaveLength(2)
37+
})
2738
})

tests/findComponent.spec.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,4 +347,73 @@ describe('findComponent', () => {
347347
})
348348
expect(wrapper.findComponent(Func).exists()).toBe(true)
349349
})
350+
351+
describe('chaining from dom wrapper', () => {
352+
it('finds a component nested inside a node', () => {
353+
const Comp = defineComponent({
354+
components: { Hello: Hello },
355+
template: '<div><div class="nested"><Hello /></div></div>'
356+
})
357+
358+
const wrapper = mount(Comp)
359+
expect(wrapper.find('.nested').findComponent(Hello).exists()).toBe(true)
360+
})
361+
362+
it('finds a component inside DOM node', () => {
363+
const Comp = defineComponent({
364+
components: { Hello: Hello },
365+
template:
366+
'<div><Hello class="one"/><div class="nested"><Hello class="two" /></div>'
367+
})
368+
369+
const wrapper = mount(Comp)
370+
expect(wrapper.find('.nested').findComponent(Hello).classes('two')).toBe(
371+
true
372+
)
373+
})
374+
375+
it('returns correct instance of recursive component', () => {
376+
const Comp = defineComponent({
377+
name: 'Comp',
378+
props: ['firstLevel'],
379+
template:
380+
'<div class="first"><div class="nested"><Comp v-if="firstLevel" class="second" /></div>'
381+
})
382+
383+
const wrapper = mount(Comp, { props: { firstLevel: true } })
384+
expect(
385+
wrapper.find('.nested').findComponent(Comp).classes('second')
386+
).toBe(true)
387+
})
388+
389+
it('returns top-level component if it matches', () => {
390+
const Comp = defineComponent({
391+
name: 'Comp',
392+
template: '<div class="top"></div>'
393+
})
394+
395+
const wrapper = mount(Comp)
396+
expect(wrapper.find('.top').findComponent(Comp).classes('top')).toBe(true)
397+
})
398+
399+
it('uses refs of correct component when searching by ref', () => {
400+
const Child = defineComponent({
401+
components: { Hello },
402+
template: '<div><Hello ref="testRef" class="inside"></div>'
403+
})
404+
const Comp = defineComponent({
405+
components: { Child, Hello },
406+
template:
407+
'<div><Child class="nested" /><Hello ref="testRef" class="outside" /></div>'
408+
})
409+
410+
const wrapper = mount(Comp)
411+
expect(
412+
wrapper
413+
.find('.nested')
414+
.findComponent({ ref: 'testRef' })
415+
.classes('inside')
416+
).toBe(true)
417+
})
418+
})
350419
})

0 commit comments

Comments
 (0)