Skip to content

Commit

Permalink
feat(find): allow chaining find with findComponent
Browse files Browse the repository at this point in the history
  • Loading branch information
xanf committed Aug 30, 2021
1 parent 437aac9 commit 809b58b
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 60 deletions.
66 changes: 62 additions & 4 deletions src/baseWrapper.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { textContent } from './utils'
import type { TriggerOptions } from './createDomEvent'
import { nextTick } from 'vue'
import {
ComponentInternalInstance,
ComponentPublicInstance,
nextTick
} from 'vue'
import { createDOMEvent } from './createDomEvent'
import { DomEventName, DomEventNameWithModifier } from './constants/dom-events'
import { DomEventNameWithModifier } from './constants/dom-events'
import type { VueWrapper } from './vueWrapper'
import type { DOMWrapper } from './domWrapper'
import { FindAllComponentsSelector, FindComponentSelector } from './types'

export default class BaseWrapper<ElementType extends Element> {
private readonly wrapperElement: ElementType
export default abstract class BaseWrapper<ElementType extends Element> {
private readonly wrapperElement: ElementType & {
__vueParentComponent?: ComponentInternalInstance
}

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

abstract find(selector: string): DOMWrapper<Element>
abstract findAll(selector: string): DOMWrapper<Element>[]
abstract findComponent<T extends ComponentPublicInstance>(
selector: FindComponentSelector | (new () => T)
): VueWrapper<T>
abstract findAllComponents(
selector: FindAllComponentsSelector
): VueWrapper<any>[]
abstract html(): string

classes(): string[]
classes(className: string): boolean
classes(className?: string): string[] | boolean {
Expand Down Expand Up @@ -45,6 +64,45 @@ export default class BaseWrapper<ElementType extends Element> {
return true
}

get<K extends keyof HTMLElementTagNameMap>(
selector: K
): Omit<DOMWrapper<HTMLElementTagNameMap[K]>, 'exists'>
get<K extends keyof SVGElementTagNameMap>(
selector: K
): Omit<DOMWrapper<SVGElementTagNameMap[K]>, 'exists'>
get<T extends Element>(selector: string): Omit<DOMWrapper<T>, 'exists'>
get(selector: string): Omit<DOMWrapper<Element>, 'exists'> {
const result = this.find(selector)
if (result.exists()) {
return result
}

throw new Error(`Unable to get ${selector} within: ${this.html()}`)
}

getComponent<T extends ComponentPublicInstance>(
selector: FindComponentSelector | (new () => T)
): Omit<VueWrapper<T>, 'exists'> {
const result = this.findComponent(selector)

if (result.exists()) {
return result as VueWrapper<T>
}

let message = 'Unable to get '
if (typeof selector === 'string') {
message += `component with selector ${selector}`
} else if ('name' in selector) {
message += `component with name ${selector.name}`
} else if ('ref' in selector) {
message += `component with ref ${selector.ref}`
} else {
message += 'specified component'
}
message += ` within: ${this.html()}`
throw new Error(message)
}

protected isDisabled = () => {
const validTagsToBeDisabled = [
'BUTTON',
Expand Down
66 changes: 50 additions & 16 deletions src/domWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { isElementVisible } from './utils/isElementVisible'
import BaseWrapper from './baseWrapper'
import { createWrapperError } from './errorWrapper'
import WrapperLike from './interfaces/wrapperLike'
import { ComponentInternalInstance, ComponentPublicInstance } from 'vue'
import { FindAllComponentsSelector, FindComponentSelector } from './types'
import { VueWrapper } from 'src'
import { matches, find } from './utils/find'
import { createWrapper } from './vueWrapper'

export class DOMWrapper<ElementType extends Element>
extends BaseWrapper<ElementType>
Expand Down Expand Up @@ -38,22 +43,6 @@ export class DOMWrapper<ElementType extends Element>
return createWrapperError('DOMWrapper')
}

get<K extends keyof HTMLElementTagNameMap>(
selector: K
): Omit<DOMWrapper<HTMLElementTagNameMap[K]>, 'exists'>
get<K extends keyof SVGElementTagNameMap>(
selector: K
): Omit<DOMWrapper<SVGElementTagNameMap[K]>, 'exists'>
get<T extends Element>(selector: string): Omit<DOMWrapper<T>, 'exists'>
get(selector: string): Omit<DOMWrapper<Element>, 'exists'> {
const result = this.find(selector)
if (result instanceof DOMWrapper) {
return result
}

throw new Error(`Unable to get ${selector} within: ${this.html()}`)
}

findAll<K extends keyof HTMLElementTagNameMap>(
selector: K
): DOMWrapper<HTMLElementTagNameMap[K]>[]
Expand All @@ -67,6 +56,51 @@ export class DOMWrapper<ElementType extends Element>
)
}

findComponent<T extends ComponentPublicInstance>(
selector: FindComponentSelector | (new () => T)
): VueWrapper<T> {
const parentComponent = this.element.__vueParentComponent

if (!parentComponent) {
return createWrapperError('VueWrapper')
}

if (typeof selector === 'object' && 'ref' in selector) {
const result = parentComponent.refs[selector.ref]
if (result && !(result instanceof HTMLElement)) {
return createWrapper(null, result as T)
} else {
return createWrapperError('VueWrapper')
}
}

if (
matches(parentComponent.vnode, selector) &&
this.element.contains(parentComponent.vnode.el as Node)
) {
return createWrapper(null, parentComponent.proxy!)
}

const result = find(parentComponent.subTree, selector).filter((v) =>
this.element.contains(v.$el)
)

if (result.length) {
return createWrapper(null, result[0])
}

return createWrapperError('VueWrapper')
}

findAllComponents(selector: FindAllComponentsSelector): VueWrapper<any>[] {
const parentComponent: ComponentInternalInstance = (this.element as any)
.__vueParentComponent

return find(parentComponent.subTree, selector)
.filter((v) => this.element.contains(v.$el))
.map((c) => createWrapper(null, c))
}

private async setChecked(checked: boolean = true) {
// typecast so we get type safety
const element = this.element as unknown as HTMLInputElement
Expand Down
41 changes: 1 addition & 40 deletions src/vueWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,22 +132,6 @@ export class VueWrapper<T extends ComponentPublicInstance>
return createWrapperError('DOMWrapper')
}

get<K extends keyof HTMLElementTagNameMap>(
selector: K
): Omit<DOMWrapper<HTMLElementTagNameMap[K]>, 'exists'>
get<K extends keyof SVGElementTagNameMap>(
selector: K
): Omit<DOMWrapper<SVGElementTagNameMap[K]>, 'exists'>
get<T extends Element>(selector: string): Omit<DOMWrapper<T>, 'exists'>
get(selector: string): Omit<DOMWrapper<Element>, 'exists'> {
const result = this.find(selector)
if (result instanceof DOMWrapper) {
return result
}

throw new Error(`Unable to get ${selector} within: ${this.html()}`)
}

findComponent<T extends ComponentPublicInstance>(
selector: FindComponentSelector | (new () => T)
): VueWrapper<T> {
Expand Down Expand Up @@ -182,30 +166,7 @@ export class VueWrapper<T extends ComponentPublicInstance>
return createWrapperError('VueWrapper')
}

getComponent<T extends ComponentPublicInstance>(
selector: FindComponentSelector | (new () => T)
): Omit<VueWrapper<T>, 'exists'> {
const result = this.findComponent(selector)

if (result instanceof VueWrapper) {
return result as VueWrapper<T>
}

let message = 'Unable to get '
if (typeof selector === 'string') {
message += `component with selector ${selector}`
} else if ('name' in selector) {
message += `component with name ${selector.name}`
} else if ('ref' in selector) {
message += `component with ref ${selector.ref}`
} else {
message += 'specified component'
}
message += ` within: ${this.html()}`
throw new Error(message)
}

findAllComponents(selector: FindAllComponentsSelector): VueWrapper<T>[] {
findAllComponents(selector: FindAllComponentsSelector): VueWrapper<any>[] {
if (typeof selector === 'string') {
throw Error(
'findAllComponents requires a Vue constructor or valid find object. If you are searching for DOM nodes, use `find` instead'
Expand Down
11 changes: 11 additions & 0 deletions tests/findAllComponents.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,15 @@ describe('findAllComponents', () => {
)
expect(wrapper.findAllComponents(Hello)[0].text()).toBe('Hello world')
})

it('finds all deeply nested vue components when chained from dom wrapper', () => {
const Component = defineComponent({
components: { Hello },
template:
'<div><Hello /><div class="nested"><Hello /><Hello /></div></div>'
})
const wrapper = mount(Component)
expect(wrapper.findAllComponents(Hello)).toHaveLength(3)
expect(wrapper.find('.nested').findAllComponents(Hello)).toHaveLength(2)
})
})
69 changes: 69 additions & 0 deletions tests/findComponent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,4 +347,73 @@ describe('findComponent', () => {
})
expect(wrapper.findComponent(Func).exists()).toBe(true)
})

describe('chaining from dom wrapper', () => {
it('finds a component nested inside a node', () => {
const Comp = defineComponent({
components: { Hello: Hello },
template: '<div><div class="nested"><Hello /></div></div>'
})

const wrapper = mount(Comp)
expect(wrapper.find('.nested').findComponent(Hello).exists()).toBe(true)
})

it('finds a component inside DOM node', () => {
const Comp = defineComponent({
components: { Hello: Hello },
template:
'<div><Hello class="one"/><div class="nested"><Hello class="two" /></div>'
})

const wrapper = mount(Comp)
expect(wrapper.find('.nested').findComponent(Hello).classes('two')).toBe(
true
)
})

it('returns correct instance of recursive component', () => {
const Comp = defineComponent({
name: 'Comp',
props: ['firstLevel'],
template:
'<div class="first"><div class="nested"><Comp v-if="firstLevel" class="second" /></div>'
})

const wrapper = mount(Comp, { props: { firstLevel: true } })
expect(
wrapper.find('.nested').findComponent(Comp).classes('second')
).toBe(true)
})

it('returns top-level component if it matches', () => {
const Comp = defineComponent({
name: 'Comp',
template: '<div class="top"></div>'
})

const wrapper = mount(Comp)
expect(wrapper.find('.top').findComponent(Comp).classes('top')).toBe(true)
})

it('uses refs of correct component when searching by ref', () => {
const Child = defineComponent({
components: { Hello },
template: '<div><Hello ref="testRef" class="inside"></div>'
})
const Comp = defineComponent({
components: { Child, Hello },
template:
'<div><Child class="nested" /><Hello ref="testRef" class="outside" /></div>'
})

const wrapper = mount(Comp)
expect(
wrapper
.find('.nested')
.findComponent({ ref: 'testRef' })
.classes('inside')
).toBe(true)
})
})
})

0 comments on commit 809b58b

Please sign in to comment.