Skip to content

Commit 805ac7d

Browse files
committed
feat(find): allow chaining find with findComponent
1 parent df43338 commit 805ac7d

File tree

5 files changed

+182
-59
lines changed

5 files changed

+182
-59
lines changed

src/baseWrapper.ts

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { textContent } from './utils'
22
import type { TriggerOptions } from './createDomEvent'
3-
import { nextTick } from 'vue'
3+
import { ComponentPublicInstance, nextTick } from 'vue'
44
import { createDOMEvent } from './createDomEvent'
5-
import { DomEventName, DomEventNameWithModifier } from './constants/dom-events'
5+
import { DomEventNameWithModifier } from './constants/dom-events'
6+
import type { VueWrapper } from './vueWrapper'
7+
import type { DOMWrapper } from './domWrapper'
8+
import { FindAllComponentsSelector, FindComponentSelector } from './types'
69

7-
export default class BaseWrapper<ElementType extends Element> {
10+
export default abstract class BaseWrapper<ElementType extends Element> {
811
private readonly wrapperElement: ElementType
912

1013
get element() {
@@ -15,6 +18,16 @@ export default class BaseWrapper<ElementType extends Element> {
1518
this.wrapperElement = element
1619
}
1720

21+
abstract find(selector: string): DOMWrapper<Element>
22+
abstract findAll(selector: string): DOMWrapper<Element>[]
23+
abstract findComponent<T extends ComponentPublicInstance>(
24+
selector: FindComponentSelector | (new () => T)
25+
): VueWrapper<T>
26+
abstract findAllComponents(
27+
selector: FindAllComponentsSelector
28+
): VueWrapper<any>[]
29+
abstract html(): string
30+
1831
classes(): string[]
1932
classes(className: string): boolean
2033
classes(className?: string): string[] | boolean {
@@ -45,6 +58,45 @@ export default class BaseWrapper<ElementType extends Element> {
4558
return true
4659
}
4760

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

src/domWrapper.ts

Lines changed: 46 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,47 @@ 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: ComponentInternalInstance = (this.element as any)
63+
.__vueParentComponent
64+
if (typeof selector === 'object' && 'ref' in selector) {
65+
const result = parentComponent.refs[selector.ref]
66+
if (result && !(result instanceof HTMLElement)) {
67+
return createWrapper(null, result as T)
68+
} else {
69+
return createWrapperError('VueWrapper')
70+
}
71+
}
72+
73+
if (
74+
matches(parentComponent.vnode, selector) &&
75+
this.element.contains(parentComponent.vnode.el as Node)
76+
) {
77+
return createWrapper(null, parentComponent.proxy!)
78+
}
79+
80+
const result = find(parentComponent.subTree, selector).filter((v) =>
81+
this.element.contains(v.$el)
82+
)
83+
84+
if (result.length) {
85+
return createWrapper(null, result[0])
86+
}
87+
88+
return createWrapperError('VueWrapper')
89+
}
90+
91+
findAllComponents(selector: FindAllComponentsSelector): VueWrapper<any>[] {
92+
const parentComponent: ComponentInternalInstance = (this.element as any)
93+
.__vueParentComponent
94+
95+
return find(parentComponent.subTree, selector)
96+
.filter((v) => this.element.contains(v.$el))
97+
.map((c) => createWrapper(null, c))
98+
}
99+
70100
private async setChecked(checked: boolean = true) {
71101
// typecast so we get type safety
72102
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> {
@@ -176,30 +160,7 @@ export class VueWrapper<T extends ComponentPublicInstance>
176160
return createWrapperError('VueWrapper')
177161
}
178162

179-
getComponent<T extends ComponentPublicInstance>(
180-
selector: FindComponentSelector | (new () => T)
181-
): Omit<VueWrapper<T>, 'exists'> {
182-
const result = this.findComponent(selector)
183-
184-
if (result instanceof VueWrapper) {
185-
return result as VueWrapper<T>
186-
}
187-
188-
let message = 'Unable to get '
189-
if (typeof selector === 'string') {
190-
message += `component with selector ${selector}`
191-
} else if ('name' in selector) {
192-
message += `component with name ${selector.name}`
193-
} else if ('ref' in selector) {
194-
message += `component with ref ${selector.ref}`
195-
} else {
196-
message += 'specified component'
197-
}
198-
message += ` within: ${this.html()}`
199-
throw new Error(message)
200-
}
201-
202-
findAllComponents(selector: FindAllComponentsSelector): VueWrapper<T>[] {
163+
findAllComponents(selector: FindAllComponentsSelector): VueWrapper<any>[] {
203164
return find(this.vm.$.subTree, selector).map((c) => createWrapper(null, c))
204165
}
205166

tests/findAllComponents.spec.ts

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

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)