From 805ac7dd149c8aec213af4c173dc422fe85fdd6a Mon Sep 17 00:00:00 2001 From: Illya Klymov Date: Sat, 28 Aug 2021 18:34:32 +0300 Subject: [PATCH] feat(find): allow chaining find with findComponent --- src/baseWrapper.ts | 58 +++++++++++++++++++++++++-- src/domWrapper.ts | 62 +++++++++++++++++++++-------- src/vueWrapper.ts | 41 +------------------- tests/findAllComponents.spec.ts | 11 ++++++ tests/findComponent.spec.ts | 69 +++++++++++++++++++++++++++++++++ 5 files changed, 182 insertions(+), 59 deletions(-) diff --git a/src/baseWrapper.ts b/src/baseWrapper.ts index c9765487b3..bd2ee284c2 100644 --- a/src/baseWrapper.ts +++ b/src/baseWrapper.ts @@ -1,10 +1,13 @@ import { textContent } from './utils' import type { TriggerOptions } from './createDomEvent' -import { nextTick } from 'vue' +import { 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 { +export default abstract class BaseWrapper { private readonly wrapperElement: ElementType get element() { @@ -15,6 +18,16 @@ export default class BaseWrapper { this.wrapperElement = element } + abstract find(selector: string): DOMWrapper + abstract findAll(selector: string): DOMWrapper[] + abstract findComponent( + selector: FindComponentSelector | (new () => T) + ): VueWrapper + abstract findAllComponents( + selector: FindAllComponentsSelector + ): VueWrapper[] + abstract html(): string + classes(): string[] classes(className: string): boolean classes(className?: string): string[] | boolean { @@ -45,6 +58,45 @@ export default class BaseWrapper { return true } + get( + selector: K + ): Omit, 'exists'> + get( + selector: K + ): Omit, 'exists'> + get(selector: string): Omit, 'exists'> + get(selector: string): Omit, 'exists'> { + const result = this.find(selector) + if (result.exists()) { + return result + } + + throw new Error(`Unable to get ${selector} within: ${this.html()}`) + } + + getComponent( + selector: FindComponentSelector | (new () => T) + ): Omit, 'exists'> { + const result = this.findComponent(selector) + + if (result.exists()) { + return result as VueWrapper + } + + 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', diff --git a/src/domWrapper.ts b/src/domWrapper.ts index c71490c8a8..61f1e9c226 100644 --- a/src/domWrapper.ts +++ b/src/domWrapper.ts @@ -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 extends BaseWrapper @@ -38,22 +43,6 @@ export class DOMWrapper return createWrapperError('DOMWrapper') } - get( - selector: K - ): Omit, 'exists'> - get( - selector: K - ): Omit, 'exists'> - get(selector: string): Omit, 'exists'> - get(selector: string): Omit, 'exists'> { - const result = this.find(selector) - if (result instanceof DOMWrapper) { - return result - } - - throw new Error(`Unable to get ${selector} within: ${this.html()}`) - } - findAll( selector: K ): DOMWrapper[] @@ -67,6 +56,47 @@ export class DOMWrapper ) } + findComponent( + selector: FindComponentSelector | (new () => T) + ): VueWrapper { + const parentComponent: ComponentInternalInstance = (this.element as any) + .__vueParentComponent + 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[] { + 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 diff --git a/src/vueWrapper.ts b/src/vueWrapper.ts index 941302c307..58260759a4 100644 --- a/src/vueWrapper.ts +++ b/src/vueWrapper.ts @@ -132,22 +132,6 @@ export class VueWrapper return createWrapperError('DOMWrapper') } - get( - selector: K - ): Omit, 'exists'> - get( - selector: K - ): Omit, 'exists'> - get(selector: string): Omit, 'exists'> - get(selector: string): Omit, 'exists'> { - const result = this.find(selector) - if (result instanceof DOMWrapper) { - return result - } - - throw new Error(`Unable to get ${selector} within: ${this.html()}`) - } - findComponent( selector: FindComponentSelector | (new () => T) ): VueWrapper { @@ -176,30 +160,7 @@ export class VueWrapper return createWrapperError('VueWrapper') } - getComponent( - selector: FindComponentSelector | (new () => T) - ): Omit, 'exists'> { - const result = this.findComponent(selector) - - if (result instanceof VueWrapper) { - return result as VueWrapper - } - - 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[] { + findAllComponents(selector: FindAllComponentsSelector): VueWrapper[] { return find(this.vm.$.subTree, selector).map((c) => createWrapper(null, c)) } diff --git a/tests/findAllComponents.spec.ts b/tests/findAllComponents.spec.ts index b13e65460b..91a8db7e4e 100644 --- a/tests/findAllComponents.spec.ts +++ b/tests/findAllComponents.spec.ts @@ -25,4 +25,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: + '
' + }) + const wrapper = mount(Component) + expect(wrapper.findAllComponents(Hello)).toHaveLength(3) + expect(wrapper.find('.nested').findAllComponents(Hello)).toHaveLength(2) + }) }) diff --git a/tests/findComponent.spec.ts b/tests/findComponent.spec.ts index d212ecfaf5..079d097cbb 100644 --- a/tests/findComponent.spec.ts +++ b/tests/findComponent.spec.ts @@ -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: '
' + }) + + 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: + '
' + }) + + 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: + '
' + }) + + 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: '
' + }) + + 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: '
' + }) + const Comp = defineComponent({ + components: { Child, Hello }, + template: + '
' + }) + + const wrapper = mount(Comp) + expect( + wrapper + .find('.nested') + .findComponent({ ref: 'testRef' }) + .classes('inside') + ).toBe(true) + }) + }) })